Menu
2/9 Seu primeiro pluginTodas as páginas

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

  1. Crie a estrutura de pastas

    O ade.plugin.json fica 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.md
  2. Escreva o manifesto

    Este é o ade.plugin.json real do exemplo. Só id, name, version e contributes sã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 agente claude/sonnet com um systemPrompt.
    • skills: path relativo à raiz, apontando para uma pasta com SKILL.md.
    • settings: uma configuração greeting (texto) com default "hello" — todo default é uma string, mesmo para número e boolean.
    • integrations: o sidecar echo, iniciado com node integrations/echo.mjs dentro da pasta do plugin.
    • mcpServers: um objeto (não um array) — a chave hello-echo é o nome do servidor.

    Todos os campos, com as regras de cada um, estão na referência do manifesto.

  3. Escreva o sidecar

    O sidecar é um programa comum que lê uma linha JSON por evento no stdin e responde com uma linha JSON por mensagem no stdout. Este é o echo.mjs real — 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 shutdown saindo com código 0 (o app dá 5 s antes de matar o processo).
    • Se o stdin fechar sem um shutdown, também sai: o app terminou com você.
    • Em um board.card_created responde com card.note, que aparece na trilha de atividade do card como uma ação de plugin:hello-ade.
  4. 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.
  5. 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 pasta hello-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.mjs

    Saí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.)

  6. Importe pelo Marketplace

    No Jarvis ADE: Marketplace de Plugins → aba Instalados → Importar plugin.

    • Escolha a origem Pasta (as outras são .zip, URL git e Pacote 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.

  7. 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 starting e chega a running quando o ready é recebido. O painel Log mostra as linhas echo: <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_created na atividade do card.

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.json na raiz (nas origens zip e git, 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, como plugin 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:

ade.plugin.json
{
  "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
// 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