Tutoriais · Plugins
Seu primeiro plugin em 10 minutos
Passo a passo do plugin de exemplo hello-ade: manifesto, sidecar, skill, teste local sem o ADE e importação pelo Marketplace.
Nesta página
O que vamos fazer
Vamos montar, rodar e importar o hello-ade: o plugin de exemplo que acompanha o Jarvis ADE (examples/plugins/hello-ade). Ele usa todas as contribuições de uma vez — um agente, uma skill, um servidor MCP, uma configuração e um sidecar — e por isso é um bom esqueleto. Os arquivos abaixo são cópias exatas do exemplo real; se você tem o repositório, pode importar a pasta pronta e só acompanhar a leitura.
Passo a passo
Crie a estrutura de pastas
O
ade.plugin.jsonfica na raiz. As demais pastas são livres — o manifesto é quem aponta para elas.hello-ade/ ├── ade.plugin.json ├── integrations/ │ └── echo.mjs └── skills/ └── hello-ade-starter/ └── SKILL.mdEscreva o manifesto
Este é o
ade.plugin.jsonreal do exemplo. Sóid,name,versionecontributessão obrigatórios; o resto é documentação e contribuições.ade.plugin.json { "id": "hello-ade", "name": "Hello ADE", "version": "0.1.0", "description": "Reference plugin for the ADE Plugin Marketplace: one agent, one skill and one MCP server, wired through a real ade.plugin.json.", "author": "ADE Team", "homepage": "https://github.com/vidiio/ade", "license": "MIT", "adeVersion": "0.1.0", "contributes": { "agents": [ { "agentId": "hello-ade-greeter", "name": "Hello ADE Greeter", "adapter": "claude", "model": "sonnet", "effort": "medium", "systemPrompt": "You are the Hello ADE Greeter, a minimal example agent shipped with the hello-ade plugin. Greet the user, briefly explain that you exist to prove the plugin's `contributes.agents` entry works end to end, and point them at the hello-ade-starter skill for a starting point when they write their own plugin.", "bestFor": ["Verifying a fresh plugin install", "Demoing the plugin system to a new contributor"] } ], "skills": [{ "path": "skills/hello-ade-starter" }], "settings": [ { "key": "greeting", "label": "Greeting", "type": "string", "default": "hello", "description": "Word the echo integration uses when it logs events." } ], "integrations": [ { "id": "echo", "name": "Echo", "description": "Reference sidecar: logs every ADE event it receives and notes back on created cards.", "command": "node", "args": ["integrations/echo.mjs"] } ], "mcpServers": { "hello-echo": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"], "env": {} } } } }agents: um agenteclaude/sonnetcom umsystemPrompt.skills:pathrelativo à raiz, apontando para uma pasta comSKILL.md.settings: uma configuraçãogreeting(texto) comdefault"hello"— tododefaulté uma string, mesmo para número e boolean.integrations: o sidecarecho, iniciado comnode integrations/echo.mjsdentro da pasta do plugin.mcpServers: um objeto (não um array) — a chavehello-echoé o nome do servidor.
Todos os campos, com as regras de cada um, estão na referência do manifesto.
Escreva o sidecar
O sidecar é um programa comum que lê uma linha JSON por evento no
stdine responde com uma linha JSON por mensagem nostdout. Este é oecho.mjsreal — zero dependências, só o Node:integrations/echo.mjs // Reference ADE integration sidecar (plugin system v2). Zero dependencies — // Node ≥ 18 only. ADE spawns this process and writes one JSON event per line // on stdin ({ v:1, id, type, at, ...payload }); we answer on stdout with one // JSON message per line. Anything that is NOT protocol output goes to stderr. import { createInterface } from 'node:readline' const settings = (() => { try { return JSON.parse(process.env.ADE_PLUGIN_SETTINGS ?? '{}') } catch { return {} } })() const greeting = settings.greeting || 'hello' /** Every stdout line MUST be a single JSON protocol message. */ const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`) send({ type: 'ready', name: 'hello-ade echo' }) const rl = createInterface({ input: process.stdin }) rl.on('line', (line) => { let event try { event = JSON.parse(line) } catch { send({ type: 'log', level: 'warn', message: `echo: unparseable line (${line.length} chars)` }) return } if (event.type === 'shutdown') { send({ type: 'log', level: 'info', message: `echo: ${greeting}, shutting down` }) process.exit(0) } send({ type: 'log', level: 'info', message: `echo: ${greeting}, saw ${event.type}` }) if (event.type === 'board.card_created' && event.card?.id) { send({ type: 'card.note', cardId: event.card.id, message: `echo saw ${event.type}` }) } }) // stdin closing without a shutdown line still means "ADE is done with us". rl.on('close', () => process.exit(0))- Lê as configurações de
ADE_PLUGIN_SETTINGS(um JSON de strings) e cai num valor padrão se o JSON estiver quebrado. - Envia
{ type: 'ready' }assim que inicializa — o app espera até 10 s por ele. - Trata a linha
shutdownsaindo com código 0 (o app dá 5 s antes de matar o processo). - Se o
stdinfechar sem umshutdown, também sai: o app terminou com você. - Em um
board.card_createdresponde comcard.note, que aparece na trilha de atividade do card como uma ação deplugin:hello-ade.
- Lê as configurações de
Escreva a skill
Uma skill é uma pasta com um
SKILL.md(frontmatter + instruções). O instalador não interpreta o arquivo — só copia a pasta. Esta é a skill inicial do exemplo, que serve de molde para a sua:skills/hello-ade-starter/SKILL.md --- name: hello-ade-starter description: "Starter skill shipped with the hello-ade example plugin — a minimal, working reference for the skills contributed by an ADE plugin." risk: safe source: hello-ade date_added: "2026-09-02" --- # Hello ADE Starter This skill exists to prove that `contributes.skills` in an `ade.plugin.json` manifest really lands a usable `SKILL.md` in the app — nothing more. Treat it as a template: copy this folder's shape (`skills/<skill-id>/SKILL.md`) when you write your own plugin's first skill. ## Use this skill when - Verifying that a freshly imported plugin's skill contribution shows up and reads correctly. - Looking for the minimal `SKILL.md` shape to start a new plugin skill from. ## Do not use this skill when - You need a real, product-specific skill — this one is intentionally a placeholder. Write your own once the plugin skeleton is proven to work. ## Instructions 1. Confirm you can see this skill listed for a plugin whose `pluginId` is `hello-ade`. 2. Copy `skills/hello-ade-starter/` as the starting point for a new skill in your own plugin, and rewrite the frontmatter (`name`, `description`) and body for what your skill actually teaches. 3. Nothing under a plugin's `skills/` path is executed or interpreted at install time — the installer only copies the folder and validates the manifest, the same rule `docs/plugins/03-como-construir-plugins.md` documents for the whole plugin.Teste o sidecar sem o ADE
Como o protocolo é só texto em pipes, dá para simular o app no terminal: alimente o sidecar com as linhas que o Jarvis ADE enviaria (
hello, um evento,shutdown). Dentro da pastahello-ade:terminal (dentro de hello-ade/) printf '%s\n' \ '{"v":1,"id":"1","type":"hello","at":0,"adeVersion":"0.1.0","pluginId":"hello-ade","integrationId":"echo","settings":{"greeting":"olá"}}' \ '{"v":1,"id":"2","type":"board.card_created","at":0,"card":{"id":"card-1"}}' \ '{"v":1,"id":"3","type":"shutdown","at":0}' \ | ADE_PLUGIN_SETTINGS='{"greeting":"olá"}' node integrations/echo.mjsSaída esperada — uma mensagem do protocolo por linha:
stdout {"type":"ready","name":"hello-ade echo"} {"type":"log","level":"info","message":"echo: olá, saw hello"} {"type":"log","level":"info","message":"echo: olá, saw board.card_created"} {"type":"card.note","cardId":"card-1","message":"echo saw board.card_created"} {"type":"log","level":"info","message":"echo: olá, shutting down"}Se o processo terminou sozinho e cada linha é um JSON válido, o sidecar está falando o protocolo. (Esta exata saída é conferida pelos testes do build deste tutorial.)
Importe pelo Marketplace
No Jarvis ADE: Marketplace de Plugins → aba Instalados → Importar plugin.
- Escolha a origem Pasta (as outras são
.zip,URL gitePacote npm). - Em Caminho da pasta do plugin, cole o caminho absoluto de
hello-ade, por exemplo/home/usuario/hello-ade. - Clique em Importar. Você verá
Hello ADE v0.1.0 importado.e o plugin entra na lista de instalados, já Ativado.
O app só valida o manifesto e copia os arquivos para
userData/plugins/hello-ade. Nada da pasta é executado na importação.- Escolha a origem Pasta (as outras são
Configure e veja o sidecar rodando
- Na linha do plugin, a seção Configurações mostra o campo Greeting. Mude o valor e clique em Salvar: o app reinicia o sidecar (envia
shutdown, espera até 5 s, inicia de novo) já com o valor novo. - O estado da integração passa por
startinge chega arunningquando oreadyé recebido. O painel Log mostra as linhasecho: <greeting>, saw <evento>. O botão Reiniciar força um novo ciclo. - Crie um card no quadro de uma missão: o sidecar responde com uma nota
echo saw board.card_createdna atividade do card.
- Na linha do plugin, a seção Configurações mostra o campo Greeting. Mude o valor e clique em Salvar: o app reinicia o sidecar (envia
Se a importação falhar
O erro aparece na própria tela de importação, com um código estável e uma mensagem legível:
PLUGIN_SOURCE_NOT_FOUND— a pasta (ou o.zip) não existe. Confira se o caminho é absoluto.PLUGIN_MANIFEST_NOT_FOUND— não háade.plugin.jsonna raiz (nas origenszipegit, o instalador ainda procura até duas pastas abaixo).PLUGIN_INVALID_MANIFEST— o arquivo não é JSON válido ou não bate com o schema; a mensagem traz o primeiro problema encontrado, comoplugin id must be kebab-case.
Agora faça o seu
Copie a pasta, troque o id (kebab-case) e o name, apague o que não precisa e escreva o que precisa. Um ponto de partida enxuto, com só um sidecar que reage a cards movidos:
{
"id": "meu-plugin",
"name": "Meu Plugin",
"version": "0.1.0",
"description": "O que o seu plugin faz, em uma frase.",
"author": "Seu Nome",
"homepage": "https://github.com/SEU-USUARIO/meu-plugin",
"license": "MIT",
"contributes": {
"integrations": [
{
"id": "principal",
"name": "Meu Plugin",
"command": "node",
"args": ["integrations/index.mjs"],
"events": ["board.card_moved"]
}
]
}
}// integrations/index.mjs — molde mínimo de sidecar, sem dependências (Node >= 18).
import { createInterface } from 'node:readline'
const settings = (() => {
try {
return JSON.parse(process.env.ADE_PLUGIN_SETTINGS ?? '{}')
} catch {
return {}
}
})()
/** Toda linha do stdout é UMA mensagem JSON do protocolo. */
const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`)
const log = (level, message) => send({ type: 'log', level, message })
send({ type: 'ready', name: 'meu-plugin' })
const rl = createInterface({ input: process.stdin })
rl.on('line', (line) => {
let event
try {
event = JSON.parse(line)
} catch {
log('warn', `linha ilegível (${line.length} caracteres)`)
return
}
if (event.type === 'hello') {
log('info', `hello: ADE ${event.adeVersion}, ${event.pluginId}/${event.integrationId}`)
} else if (event.type === 'shutdown') {
process.exit(0)
} else if (event.type === 'board.card_moved') {
send({
type: 'card.note',
cardId: event.card.id,
message: `movido: ${event.from.title} → ${event.to.title}`,
})
}
})
// stdin fechado sem uma linha shutdown também significa "o ADE acabou com você".
rl.on('close', () => process.exit(0))Próximos passos
- Entenda cada campo na referência do manifesto e cada mensagem no protocolo do sidecar.
- Precisa de uma chave de API ou de um botão “faça agora”? Veja configurações e segredos.