O ChatCLI suporta um sistema de plugins para estender suas funcionalidades. Um plugin é um executável que segue um contrato específico, permitindo que o ChatCLI o descubra, execute e interaja com ele de forma segura.
Isso permite criar comandos customizados (como @kind, @deploy) que podem orquestrar ferramentas, interagir com APIs ou realizar qualquer lógica que você possa programar.
Para Usuários: Gerenciando Plugins
Listar plugins instalados
Mostra todos os comandos de plugin disponíveis, incluindo plugins [builtin] (como @coder) e [remote] (de servidores conectados).
Instalar um plugin
Instale diretamente de um repositório Git:
/plugin install https://github.com/usuário/meu-plugin-chatcli.git
O ChatCLI irá clonar, compilar (se for Go) e instalar o executável em ~/.chatcli/plugins/.
Segurança: A instalação de um plugin envolve baixar e executar código de terceiros. Instale plugins apenas de fontes que você confia.
Ver detalhes de um plugin
/plugin show <nome-do-plugin>
Desinstalar um plugin
/plugin uninstall <nome-do-plugin>
Recarregar plugins
O ChatCLI monitora automaticamente ~/.chatcli/plugins/ e recarrega quando detecta mudanças (criação, remoção, modificação). Um debounce de 500ms evita recarregamentos múltiplos.
Para forçar um recarregamento manual:
Desenvolva plugins iterativamente: edite o código, recompile, envie ao diretório de plugins — o ChatCLI detectará a mudança automaticamente.
Para Desenvolvedores: Criando um Plugin
O contrato do plugin
- Executável — O plugin deve ser um arquivo executável (qualquer linguagem)
- Localização — Colocado em
~/.chatcli/plugins/
- Nome do comando — O nome do arquivo vira o comando. Ex: arquivo
kind = comando @kind
- Metadados (
--metadata) — Obrigatório. O executável deve responder a está flag com JSON:
{
"name": "@meu-comando",
"description": "Uma breve descrição do que o plugin faz.",
"usage": "@meu-comando <subcomando> [--flag value]",
"version": "1.0.0"
}
- Schema (
--schema) — Opcional. Descreve os parâmetros aceitos:
{
"parameters": [
{
"name": "cluster-name",
"type": "string",
"required": true,
"description": "Nome do cluster Kubernetes"
}
]
}
- Comunicação (stdout vs stderr):
- stdout — Apenas o resultado final (retornado ao ChatCLI/IA)
- stderr — Logs de progresso, status e avisos (exibidos em tempo real ao usuário)
Exemplo: Plugin “Hello World” em Go
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"time"
)
type Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Usage string `json:"usage"`
Version string `json:"version"`
}
// logf envia mensagens de progresso para o usuário (via stderr).
func logf(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, format, v...)
}
func main() {
metadataFlag := flag.Bool("metadata", false, "Exibe os metadados do plugin")
schemaFlag := flag.Bool("schema", false, "Exibe o schema de parâmetros")
flag.Parse()
if *metadataFlag {
meta := Metadata{
Name: "@hello",
Description: "Plugin de exemplo que demonstra o fluxo stdout/stderr.",
Usage: "@hello [seu-nome]",
Version: "1.0.0",
}
jsonMeta, _ := json.Marshal(meta)
fmt.Println(string(jsonMeta))
return
}
if *schemaFlag {
schema := map[string]interface{}{
"parameters": []map[string]interface{}{
{
"name": "nome",
"type": "string",
"required": false,
"description": "Nome da pessoa a ser cumprimentada",
"default": "Mundo",
},
},
}
jsonSchema, _ := json.Marshal(schema)
fmt.Println(string(jsonSchema))
return
}
// Lógica principal
logf("Plugin 'hello' iniciado!\n")
time.Sleep(2 * time.Second)
logf(" - Realizando uma tarefa demorada...\n")
time.Sleep(2 * time.Second)
name := "Mundo"
if len(flag.Args()) > 0 {
name = flag.Args()[0]
}
logf("Tarefa concluída!\n")
// Resultado final vai para stdout
fmt.Printf("Olá, %s! A hora agora é %s.", name, time.Now().Format(time.RFC1123))
}
Compilação e instalação
# 1. Compilar
go build -o hello ./hello/main.go
# 2. Dar permissão de execução
chmod +x hello
# 3. Criar diretório de plugins (se não existir)
mkdir -p ~/.chatcli/plugins/
# 4. Mover o executável
mv hello ~/.chatcli/plugins/
# 5. Usar no ChatCLI
/agent Olá meu nome é Fulano
Capability Interfaces (Opt-in)
Plugins podem expor capabilities através de interfaces Go opcionais. Plugins legados que não implementam continuam funcionando — todas as interfaces são fail-closed (default conservador). Implementando, o plugin participa de otimizações do orquestrador:
| Interface | Método | Para que serve |
|---|
ReadOnlyAware | IsReadOnly(args []string) bool | Habilita capability gate (security auto-allow) e participa de batches paralelos |
ConcurrencySafeAware | IsConcurrencySafe(args []string) bool | Sinaliza que múltiplas chamadas podem rodar em paralelo |
DescriberWithInput | DescribeCall(args []string) string | Label dinâmico no spinner (“Reading: main.go” em vez de “EXECUTANDO: @read”) |
JSONSchemaAware | JSONSchema() string | Schema draft-2020-12 validado antes do dispatch — input inválido vira ToolResult{IsError:true, ErrorCode:"InvalidArgs"} |
TruncationAware | MaxResultChars() int | Per-tool cap de output (versus o global de 30 000) |
Prompter | Prompt(opts PromptOpts) (string, error) | Slice do system prompt contextual |
StreamingInputAware | UpdateStreamingInput(field, value string) | Eventos de input parcial enquanto o LLM streama args |
StructuredExecutor | ExecuteStructured(...) (StructuredResult, error) | Retorno tipado em vez do (string, error) legado |
Exemplo — um plugin que quer auto-allow + paralelização + label customizado:
func (p *MyReadOnlyPlugin) IsReadOnly(_ []string) bool { return true }
func (p *MyReadOnlyPlugin) IsConcurrencySafe(_ []string) bool { return true }
func (p *MyReadOnlyPlugin) DescribeCall(args []string) string {
return i18n.T("plugins.myplugin.describe", extractTarget(args))
}
func (p *MyReadOnlyPlugin) JSONSchema() string {
return `{"type":"object","properties":{"file":{"type":"string"}},"required":["file"]}`
}
Os 4 plugins atômicos (@read, @search, @tree, @todo) — veja Tools Atômicos — usam todas as capabilities relevantes como exemplo de referência.
Verificação de Assinatura
A partir desta versão, plugins requerem assinatura digital Ed25519 por padrão. Isso garante que apenas plugins de fontes confiáveis sejam carregados e executados.
Como Funciona
Cada plugin deve ter um arquivo .sig correspondente no mesmo diretório:
~/.chatcli/plugins/
meu-plugin # executável do plugin
meu-plugin.sig # assinatura Ed25519 do executável
O ChatCLI verifica a assinatura contra as chaves públicas registradas antes de carregar o plugin. Se a verificação falhar, o plugin é rejeitado.
Gerenciando Chaves Confiáveis
As chaves públicas Ed25519 ficam no diretório ~/.chatcli/trusted-keys/:
Gerar um par de chaves
# Gera chave privada e pública
chatcli plugin keygen --name minha-org
# Cria: ~/.chatcli/trusted-keys/minha-org.pub
# Cria: ~/.chatcli/signing-keys/minha-org.key (privada - proteja!)
Assinar um plugin
chatcli plugin sign --key ~/.chatcli/signing-keys/minha-org.key meu-plugin
# Cria: meu-plugin.sig
Distribuir a chave pública
Compartilhe o arquivo .pub com os usuários que devem confiar nos seus plugins. Eles devem colocá-lo em ~/.chatcli/trusted-keys/.
Permissões de Arquivo
O diretório de plugins usa permissões 0o700 (somente o dono pode ler, escrever e executar). O ChatCLI verifica as permissões na inicialização e emite um aviso se estiverem mais permissivas.
Modo de Desenvolvimento
Para desenvolvimento local, é possível desabilitar a verificação de assinatura:
export CHATCLI_ALLOW_UNSIGNED_PLUGINS=true
chatcli
Nunca habilite CHATCLI_ALLOW_UNSIGNED_PLUGINS em produção. Plugins não assinados podem executar código arbitrário com as permissões do processo ChatCLI.
Plugins Remotos
Ao conectar a um servidor via chatcli connect, plugins do servidor são descobertos automaticamente:
- Aparecem em
/plugin list com a tag [remote]
- São executados no servidor (não baixados localmente por padrão)
- Plugins locais e remotos coexistem sem conflito