brito-cli 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.brito/agents/reviewer.md +7 -0
- package/LICENSE +75 -0
- package/README.md +166 -2
- package/README.pt-BR.md +167 -0
- package/dist/agent/agents.js +65 -0
- package/dist/agent/claude-cli.js +216 -0
- package/dist/agent/context.js +40 -0
- package/dist/agent/loop.js +125 -0
- package/dist/agent/prompt.js +54 -0
- package/dist/cli.js +562 -0
- package/dist/graph/aliases.js +196 -0
- package/dist/graph/indexer.js +438 -0
- package/dist/graph/langs.js +526 -0
- package/dist/graph/sfc.js +52 -0
- package/dist/graph/store.js +70 -0
- package/dist/mcp/server.js +98 -0
- package/dist/model.js +136 -0
- package/dist/permissions.js +39 -0
- package/dist/repl.js +297 -0
- package/dist/skills/consolidate.js +76 -0
- package/dist/skills/describe.js +14 -0
- package/dist/skills/journal.js +72 -0
- package/dist/skills/learn-cli.js +55 -0
- package/dist/skills/learn.js +217 -0
- package/dist/skills/store.js +234 -0
- package/dist/tools/bash.js +17 -0
- package/dist/tools/delegate.js +61 -0
- package/dist/tools/fs.js +91 -0
- package/dist/tools/graph.js +117 -0
- package/dist/tools/plan.js +17 -0
- package/dist/tools/registry.js +56 -0
- package/dist/tools/skills.js +58 -0
- package/dist/ui/banner.js +106 -0
- package/dist/ui/dock.js +616 -0
- package/dist/ui/theme.js +63 -0
- package/dist/ui/ui.js +426 -0
- package/dist/util/diff.js +36 -0
- package/dist/util/frontmatter.js +16 -0
- package/package.json +66 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool, allTools, execTool } from "./registry.js";
|
|
3
|
+
import { Agent } from "../agent/loop.js";
|
|
4
|
+
import { getAgent, listAgents } from "../agent/agents.js";
|
|
5
|
+
import { buildSubagentPrompt } from "../agent/prompt.js";
|
|
6
|
+
import { ui } from "../ui/ui.js";
|
|
7
|
+
let seq = 0;
|
|
8
|
+
/** Conjunto de tools de um agente; `delegate` nunca é repassada (sem recursão). */
|
|
9
|
+
function toolSet(def) {
|
|
10
|
+
const names = def.tools === "all" ? allTools().map((t) => t.name) : def.tools;
|
|
11
|
+
// Sem recursão e sem aprender: criar/alterar skills é papel do agente principal.
|
|
12
|
+
return new Set(names.filter((n) => !["delegate", "skill_save", "skill_delete", "exit_plan"].includes(n)));
|
|
13
|
+
}
|
|
14
|
+
/** Agente somente-leitura: nenhuma tool dele escreve arquivos ou exige aprovação. */
|
|
15
|
+
function isReadOnly(def) {
|
|
16
|
+
if (!def)
|
|
17
|
+
return false;
|
|
18
|
+
const set = toolSet(def);
|
|
19
|
+
return allTools().filter((t) => set.has(t.name)).every((t) => !t.dangerous && !t.mutates && t.name !== "remember" && t.name !== "graph_index");
|
|
20
|
+
}
|
|
21
|
+
defineTool({
|
|
22
|
+
name: "delegate",
|
|
23
|
+
get description() {
|
|
24
|
+
return ("Delega uma tarefa a um subagente com contexto ISOLADO (ele não vê esta conversa; ponha tudo que ele precisa em `task`). " +
|
|
25
|
+
"Só o resultado final volta para você, o que poupa seu contexto. Vários `delegate` de agentes somente-leitura no mesmo turno rodam em paralelo.\n" +
|
|
26
|
+
"Use `focus` com nomes de símbolos para já entregar ao subagente o subgrafo (quem chama / o que chama / notas) deles.\n" +
|
|
27
|
+
"Agentes disponíveis:\n" +
|
|
28
|
+
listAgents().map((a) => `- ${a.name}${isReadOnly(a) ? " (somente leitura)" : ""}: ${a.description}`).join("\n"));
|
|
29
|
+
},
|
|
30
|
+
schema: z.object({
|
|
31
|
+
agent: z.string().describe("nome do agente"),
|
|
32
|
+
task: z.string().min(10).describe("tarefa autocontida, com objetivo e formato de resposta esperado"),
|
|
33
|
+
focus: z.array(z.string()).max(5).default([]).describe("símbolos para pré-carregar o subgrafo"),
|
|
34
|
+
}),
|
|
35
|
+
concurrent: (i) => isReadOnly(getAgent(i.agent)),
|
|
36
|
+
async run({ agent, task, focus }, ctx) {
|
|
37
|
+
const def = getAgent(agent);
|
|
38
|
+
if (!def)
|
|
39
|
+
throw new Error(`Agente desconhecido: ${agent}. Disponíveis: ${listAgents().map((a) => a.name).join(", ")}`);
|
|
40
|
+
const label = `${def.name}#${++seq}`;
|
|
41
|
+
const log = (s) => ui.sub(label, s);
|
|
42
|
+
// O subgrafo pré-carregado é a vantagem do grafo: o filho já começa sabendo onde está.
|
|
43
|
+
let seed = "";
|
|
44
|
+
for (const sym of focus) {
|
|
45
|
+
const r = await execTool("graph_context", { symbol: sym }, ctx);
|
|
46
|
+
seed += `\n\n### graph_context(${sym})\n${r.text}`;
|
|
47
|
+
}
|
|
48
|
+
log(`iniciado: ${task.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
49
|
+
const sub = new Agent(buildSubagentPrompt(ctx.root, def), ctx, {
|
|
50
|
+
model: def.model,
|
|
51
|
+
maxSteps: 25,
|
|
52
|
+
quiet: true,
|
|
53
|
+
tools: toolSet(def),
|
|
54
|
+
log,
|
|
55
|
+
});
|
|
56
|
+
const text = await sub.send(seed ? `${task}\n\n# Contexto do grafo (pré-carregado)${seed}` : task);
|
|
57
|
+
const tokens = Math.round((sub.usage.input + sub.usage.output) / 1000);
|
|
58
|
+
log(`concluído: ${sub.usage.steps} passos, ~${tokens}k tokens`);
|
|
59
|
+
return `[subagente ${label} · ${sub.usage.steps} passos · ~${tokens}k tokens]\n\n${text.slice(0, 12_000)}`;
|
|
60
|
+
},
|
|
61
|
+
});
|
package/dist/tools/fs.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
import { defineTool } from "./registry.js";
|
|
6
|
+
const abs = (root, p) => {
|
|
7
|
+
const full = path.resolve(root, p);
|
|
8
|
+
if (!full.startsWith(path.resolve(root)))
|
|
9
|
+
throw new Error("Caminho fora do projeto.");
|
|
10
|
+
return full;
|
|
11
|
+
};
|
|
12
|
+
defineTool({
|
|
13
|
+
name: "read_file",
|
|
14
|
+
concurrent: true,
|
|
15
|
+
description: "Lê um arquivo do projeto, com números de linha. Use offset/limit para arquivos grandes.",
|
|
16
|
+
schema: z.object({ path: z.string(), offset: z.number().int().min(1).optional(), limit: z.number().int().min(1).optional() }),
|
|
17
|
+
async run({ path: p, offset = 1, limit = 2000 }, { root }) {
|
|
18
|
+
const lines = fs.readFileSync(abs(root, p), "utf8").split("\n");
|
|
19
|
+
return lines.slice(offset - 1, offset - 1 + limit).map((l, i) => `${offset + i}\t${l}`).join("\n");
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
defineTool({
|
|
23
|
+
name: "write_file",
|
|
24
|
+
description: "Cria ou sobrescreve um arquivo.",
|
|
25
|
+
schema: z.object({ path: z.string(), content: z.string() }),
|
|
26
|
+
dangerous: true,
|
|
27
|
+
edit: true,
|
|
28
|
+
mutates: true,
|
|
29
|
+
describe: (i) => `escrever ${i.path} (${i.content.length} chars)`,
|
|
30
|
+
async run({ path: p, content }, { root }) {
|
|
31
|
+
const full = abs(root, p);
|
|
32
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
33
|
+
fs.writeFileSync(full, content);
|
|
34
|
+
return `Escrito ${p}`;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
defineTool({
|
|
38
|
+
name: "edit_file",
|
|
39
|
+
description: "Substitui old_string por new_string em um arquivo. old_string deve ser único (ou use replace_all).",
|
|
40
|
+
schema: z.object({ path: z.string(), old_string: z.string(), new_string: z.string(), replace_all: z.boolean().optional() }),
|
|
41
|
+
dangerous: true,
|
|
42
|
+
edit: true,
|
|
43
|
+
mutates: true,
|
|
44
|
+
describe: (i) => `editar ${i.path}\n - ${i.old_string.slice(0, 120).replace(/\n/g, "⏎")}\n + ${i.new_string.slice(0, 120).replace(/\n/g, "⏎")}`,
|
|
45
|
+
async run({ path: p, old_string, new_string, replace_all }, { root }) {
|
|
46
|
+
const full = abs(root, p);
|
|
47
|
+
const src = fs.readFileSync(full, "utf8");
|
|
48
|
+
const count = src.split(old_string).length - 1;
|
|
49
|
+
if (count === 0)
|
|
50
|
+
throw new Error("old_string não encontrado.");
|
|
51
|
+
if (count > 1 && !replace_all)
|
|
52
|
+
throw new Error(`old_string aparece ${count}x; torne-o único ou use replace_all.`);
|
|
53
|
+
fs.writeFileSync(full, replace_all ? src.split(old_string).join(new_string) : src.replace(old_string, () => new_string));
|
|
54
|
+
return `Editado ${p} (${replace_all ? count : 1} substituição)`;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
defineTool({
|
|
58
|
+
name: "glob",
|
|
59
|
+
concurrent: true,
|
|
60
|
+
description: "Lista arquivos por padrão glob (ex: src/**/*.ts).",
|
|
61
|
+
schema: z.object({ pattern: z.string() }),
|
|
62
|
+
async run({ pattern }, { root }) {
|
|
63
|
+
const r = await fg(pattern, { cwd: root, ignore: ["**/node_modules/**", "**/.git/**", "**/.brito/**", "**/dist/**"] });
|
|
64
|
+
return r.slice(0, 500).join("\n") || "(nenhum)";
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
defineTool({
|
|
68
|
+
name: "grep",
|
|
69
|
+
concurrent: true,
|
|
70
|
+
description: "Busca regex em arquivos do projeto. Retorna arquivo:linha:texto.",
|
|
71
|
+
schema: z.object({ pattern: z.string(), glob: z.string().optional() }),
|
|
72
|
+
async run({ pattern, glob = "**/*" }, { root }) {
|
|
73
|
+
const re = new RegExp(pattern);
|
|
74
|
+
const files = await fg(glob, { cwd: root, ignore: ["**/node_modules/**", "**/.git/**", "**/.brito/**", "**/dist/**"] });
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
let txt;
|
|
78
|
+
try {
|
|
79
|
+
txt = fs.readFileSync(path.join(root, f), "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (txt.includes("\0"))
|
|
85
|
+
continue;
|
|
86
|
+
txt.split("\n").forEach((l, i) => { if (re.test(l) && out.length < 300)
|
|
87
|
+
out.push(`${f}:${i + 1}:${l.slice(0, 200)}`); });
|
|
88
|
+
}
|
|
89
|
+
return out.join("\n") || "(sem resultados)";
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { defineTool } from "./registry.js";
|
|
4
|
+
import { indexProject } from "../graph/indexer.js";
|
|
5
|
+
import { listSkills } from "../skills/store.js";
|
|
6
|
+
const fmt = (rows, cols) => (rows.length ? rows.map((r) => cols.map((c) => r[c]).join(" ")).join("\n") : "(vazio)");
|
|
7
|
+
defineTool({
|
|
8
|
+
name: "graph_index",
|
|
9
|
+
description: "Atualiza o grafo de código. É incremental (só reparseia arquivos alterados) e roda automaticamente após edições; use full=true só para reconstruir do zero.",
|
|
10
|
+
schema: z.object({ full: z.boolean().optional() }),
|
|
11
|
+
async run({ full }, { root, graph }) {
|
|
12
|
+
const s = await indexProject(root, graph, { full });
|
|
13
|
+
return `Grafo: ${s.files} arquivos, ${s.symbols} símbolos, ${s.calls} chamadas, ${s.imports} imports. (+${s.added} ~${s.changed} -${s.removed} =${s.unchanged})`;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
defineTool({
|
|
17
|
+
name: "graph_search",
|
|
18
|
+
concurrent: true,
|
|
19
|
+
readsGraph: true,
|
|
20
|
+
description: "Busca símbolos (funções, classes, métodos…) por nome (substring, case-insensitive). PREFIRA isto a grep para localizar definições.",
|
|
21
|
+
schema: z.object({ name: z.string(), kind: z.string().optional() }),
|
|
22
|
+
async run({ name, kind }, { graph }) {
|
|
23
|
+
const rows = await graph.run(`MATCH (s:Symbol) WHERE lower(s.name) CONTAINS lower($n) ${kind ? "AND s.kind = $k" : ""} RETURN s.kind AS kind, s.name AS name, s.file AS file, s.startLine AS line, s.endLine AS endLine LIMIT 40`, kind ? { n: name, k: kind } : { n: name });
|
|
24
|
+
return fmt(rows, ["kind", "name", "file", "line", "endLine"]);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
defineTool({
|
|
28
|
+
name: "graph_callers",
|
|
29
|
+
concurrent: true,
|
|
30
|
+
readsGraph: true,
|
|
31
|
+
description: "Quem chama o símbolo (transitivo até depth). Use antes de alterar uma função para medir impacto.",
|
|
32
|
+
schema: z.object({ symbol: z.string(), depth: z.number().int().min(1).max(5).default(2) }),
|
|
33
|
+
async run({ symbol, depth }, { graph }) {
|
|
34
|
+
const rows = await graph.run(`MATCH (c:Symbol)-[:CALLS*1..${depth}]->(t:Symbol) WHERE t.name = $n OR t.name ENDS WITH $dn RETURN DISTINCT c.name AS name, c.file AS file, c.startLine AS line LIMIT 60`, { n: symbol, dn: "." + symbol });
|
|
35
|
+
return fmt(rows, ["name", "file", "line"]);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
defineTool({
|
|
39
|
+
name: "graph_callees",
|
|
40
|
+
concurrent: true,
|
|
41
|
+
readsGraph: true,
|
|
42
|
+
description: "O que o símbolo chama (transitivo até depth). Use para entender um fluxo sem ler tudo.",
|
|
43
|
+
schema: z.object({ symbol: z.string(), depth: z.number().int().min(1).max(5).default(2) }),
|
|
44
|
+
async run({ symbol, depth }, { graph }) {
|
|
45
|
+
const rows = await graph.run(`MATCH (s:Symbol)-[:CALLS*1..${depth}]->(c:Symbol) WHERE s.name = $n OR s.name ENDS WITH $dn RETURN DISTINCT c.name AS name, c.file AS file, c.startLine AS line LIMIT 60`, { n: symbol, dn: "." + symbol });
|
|
46
|
+
return fmt(rows, ["name", "file", "line"]);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
defineTool({
|
|
50
|
+
name: "graph_impact",
|
|
51
|
+
concurrent: true,
|
|
52
|
+
readsGraph: true,
|
|
53
|
+
description: "Arquivos que dependem (importam, transitivamente) do arquivo dado. Responde: 'o que pode quebrar se eu mudar este arquivo?'",
|
|
54
|
+
schema: z.object({ file: z.string(), depth: z.number().int().min(1).max(6).default(3) }),
|
|
55
|
+
async run({ file, depth }, { graph }) {
|
|
56
|
+
const rows = await graph.run(`MATCH (a:File)-[:IMPORTS*1..${depth}]->(b:File {path:$f}) RETURN DISTINCT a.path AS path LIMIT 100`, { f: file });
|
|
57
|
+
return fmt(rows, ["path"]);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
defineTool({
|
|
61
|
+
name: "graph_context",
|
|
62
|
+
concurrent: true,
|
|
63
|
+
readsGraph: true,
|
|
64
|
+
description: "Visão completa de um símbolo: onde está, quem chama, o que chama, e notas de memória relacionadas. Use ANTES de ler o arquivo inteiro.",
|
|
65
|
+
schema: z.object({ symbol: z.string() }),
|
|
66
|
+
async run({ symbol }, { graph, root }) {
|
|
67
|
+
const defs = await graph.run(`MATCH (s:Symbol) WHERE s.name = $n OR s.name ENDS WITH $dn RETURN s.id AS id, s.kind AS kind, s.file AS file, s.startLine AS a, s.endLine AS b LIMIT 5`, { n: symbol, dn: "." + symbol });
|
|
68
|
+
if (!defs.length)
|
|
69
|
+
return "Símbolo não encontrado. Tente graph_search.";
|
|
70
|
+
const parts = [];
|
|
71
|
+
for (const d of defs) {
|
|
72
|
+
const callers = await graph.run(`MATCH (c:Symbol)-[:CALLS]->(:Symbol {id:$id}) RETURN c.name AS n, c.file AS f, c.startLine AS l LIMIT 15`, { id: d.id });
|
|
73
|
+
const callees = await graph.run(`MATCH (:Symbol {id:$id})-[:CALLS]->(c:Symbol) RETURN c.name AS n, c.file AS f, c.startLine AS l LIMIT 15`, { id: d.id });
|
|
74
|
+
const notes = await graph.run(`MATCH (n:Note) WHERE list_contains(n.refs, $name) OR list_contains(n.refs, $file) RETURN n.kind AS k, n.text AS t LIMIT 5`, { name: symbol, file: d.file });
|
|
75
|
+
parts.push(`## ${d.kind} ${d.id} (linhas ${d.a}-${d.b})\n` +
|
|
76
|
+
`chamado por:\n${fmt(callers, ["n", "f", "l"])}\n` +
|
|
77
|
+
`chama:\n${fmt(callees, ["n", "f", "l"])}\n` +
|
|
78
|
+
`notas:\n${fmt(notes, ["k", "t"])}` +
|
|
79
|
+
(() => {
|
|
80
|
+
const sk = listSkills(root).filter((s) => s.refs.includes(symbol) || s.refs.includes(d.file));
|
|
81
|
+
return sk.length ? `\nskills relacionadas (skill_load):\n${sk.map((s) => `${s.name}: ${s.description}`).join("\n")}` : "";
|
|
82
|
+
})());
|
|
83
|
+
}
|
|
84
|
+
return parts.join("\n\n");
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
defineTool({
|
|
88
|
+
name: "graph_query",
|
|
89
|
+
concurrent: true,
|
|
90
|
+
description: "Cypher somente-leitura no grafo. Schema: (File{path,lang})-[:DEFINES]->(Symbol{id,name,kind,file,startLine,endLine}); (Symbol)-[:CALLS]->(Symbol); (File)-[:IMPORTS]->(File); (Note{id,kind,text,refs[],ts}).",
|
|
91
|
+
schema: z.object({ cypher: z.string() }),
|
|
92
|
+
async run({ cypher }, { graph }) {
|
|
93
|
+
if (/\b(CREATE|DELETE|SET|MERGE|DROP|ALTER|COPY|REMOVE)\b/i.test(cypher))
|
|
94
|
+
throw new Error("Apenas consultas de leitura.");
|
|
95
|
+
const rows = await graph.run(cypher);
|
|
96
|
+
return JSON.stringify(rows.slice(0, 100), null, 1);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
defineTool({
|
|
100
|
+
name: "remember",
|
|
101
|
+
description: "Grava uma nota durável no grafo de memória (decisão, bug, convenção, aprendizado), ligada a arquivos/símbolos em refs. Será recuperada em graph_context e recall.",
|
|
102
|
+
schema: z.object({ kind: z.enum(["decision", "bug", "convention", "learning"]), text: z.string(), refs: z.array(z.string()).default([]) }),
|
|
103
|
+
async run({ kind, text, refs }, { graph }) {
|
|
104
|
+
await graph.run("CREATE (:Note {id:$id, kind:$k, text:$t, refs:$r, ts:$ts})", { id: crypto.randomUUID(), k: kind, t: text, r: refs, ts: Date.now() });
|
|
105
|
+
return "Nota salva.";
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
defineTool({
|
|
109
|
+
name: "recall",
|
|
110
|
+
concurrent: true,
|
|
111
|
+
description: "Busca notas de memória por texto ou por referência (arquivo/símbolo).",
|
|
112
|
+
schema: z.object({ query: z.string() }),
|
|
113
|
+
async run({ query }, { graph }) {
|
|
114
|
+
const rows = await graph.run("MATCH (n:Note) WHERE lower(n.text) CONTAINS lower($q) OR list_contains(n.refs, $q) RETURN n.kind AS kind, n.text AS text, n.refs AS refs ORDER BY n.ts DESC LIMIT 20", { q: query });
|
|
115
|
+
return rows.length ? rows.map((r) => `[${r.kind}] ${r.text} (refs: ${r.refs.join(", ")})`).join("\n") : "(nenhuma nota)";
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "./registry.js";
|
|
3
|
+
import { perms } from "../permissions.js";
|
|
4
|
+
defineTool({
|
|
5
|
+
name: "exit_plan",
|
|
6
|
+
description: "Só no modo plano: apresenta o plano final ao usuário e pede aprovação para sair do modo plano e executar.",
|
|
7
|
+
schema: z.object({ plan: z.string().min(10).describe("plano completo em markdown: arquivos, mudanças, riscos, verificação") }),
|
|
8
|
+
async run({ plan }, ctx) {
|
|
9
|
+
if (perms.mode !== "plan")
|
|
10
|
+
return "Você não está no modo plano; siga com a tarefa.";
|
|
11
|
+
const ok = await ctx.ask(`plano proposto — aprovar e sair do modo plano?\n${plan}`, { force: true });
|
|
12
|
+
if (!ok)
|
|
13
|
+
return "Plano não aprovado. Pergunte o que o usuário quer ajustar e revise o plano.";
|
|
14
|
+
perms.mode = "manual";
|
|
15
|
+
return "Plano aprovado. Modo manual ativo: execute o plano (edições e comandos pedem aprovação).";
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { perms } from "../permissions.js";
|
|
3
|
+
const registry = new Map();
|
|
4
|
+
export function defineTool(t) {
|
|
5
|
+
registry.set(t.name, t);
|
|
6
|
+
return t;
|
|
7
|
+
}
|
|
8
|
+
export const allTools = () => [...registry.values()];
|
|
9
|
+
export function toApiTools(allow) {
|
|
10
|
+
return allTools().filter((t) => !allow || allow.has(t.name)).map((t) => ({
|
|
11
|
+
name: t.name,
|
|
12
|
+
description: t.description,
|
|
13
|
+
input_schema: z.toJSONSchema(t.schema),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export function isConcurrent(name, input) {
|
|
17
|
+
const t = registry.get(name);
|
|
18
|
+
if (!t || t.dangerous || t.mutates || !t.concurrent)
|
|
19
|
+
return false;
|
|
20
|
+
if (typeof t.concurrent === "boolean")
|
|
21
|
+
return t.concurrent;
|
|
22
|
+
const parsed = t.schema.safeParse(input);
|
|
23
|
+
return parsed.success && t.concurrent(parsed.data);
|
|
24
|
+
}
|
|
25
|
+
export async function execTool(name, input, ctx) {
|
|
26
|
+
const tool = registry.get(name);
|
|
27
|
+
if (!tool)
|
|
28
|
+
return { text: `Tool desconhecida: ${name}`, isError: true };
|
|
29
|
+
const parsed = tool.schema.safeParse(input);
|
|
30
|
+
if (!parsed.success)
|
|
31
|
+
return { text: `Input inválido: ${z.prettifyError(parsed.error)}`, isError: true };
|
|
32
|
+
if (tool.dangerous) {
|
|
33
|
+
if (perms.mode === "plan")
|
|
34
|
+
return { text: `Modo plano: ${tool.name} está bloqueada. Termine o plano e chame exit_plan para pedir aprovação.`, isError: true };
|
|
35
|
+
const skip = perms.mode === "bypass" || (perms.mode === "auto" && tool.edit);
|
|
36
|
+
const ok = skip || await ctx.ask(`${tool.name}: ${tool.describe?.(parsed.data, ctx) ?? JSON.stringify(parsed.data).slice(0, 200)}`);
|
|
37
|
+
if (!ok)
|
|
38
|
+
return { text: "Usuário negou a execução.", isError: true };
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
if (tool.readsGraph) {
|
|
42
|
+
if (ctx.dirty) {
|
|
43
|
+
ctx.dirty = false;
|
|
44
|
+
ctx.syncing = ctx.sync().finally(() => { ctx.syncing = undefined; });
|
|
45
|
+
}
|
|
46
|
+
await ctx.syncing;
|
|
47
|
+
}
|
|
48
|
+
const text = await tool.run(parsed.data, ctx);
|
|
49
|
+
if (tool.mutates)
|
|
50
|
+
ctx.dirty = true;
|
|
51
|
+
return { text, isError: false };
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
return { text: `Erro: ${e?.message ?? e}`, isError: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "./registry.js";
|
|
3
|
+
import { bumpUses, deleteSkill, getSkill, listPending, savePending, saveSkill, validate } from "../skills/store.js";
|
|
4
|
+
import { describeSave } from "../skills/describe.js";
|
|
5
|
+
import { perms } from "../permissions.js";
|
|
6
|
+
defineTool({
|
|
7
|
+
name: "skill_load",
|
|
8
|
+
description: "Carrega o passo a passo completo de uma skill (listadas no system prompt). Chame ANTES de começar uma tarefa que uma skill cobre e siga-a.",
|
|
9
|
+
schema: z.object({ name: z.string() }),
|
|
10
|
+
async run({ name }, ctx) {
|
|
11
|
+
const s = getSkill(ctx.root, name);
|
|
12
|
+
if (!s)
|
|
13
|
+
throw new Error(`Skill não encontrada: ${name}`);
|
|
14
|
+
bumpUses(ctx.root, name);
|
|
15
|
+
(ctx.loadedSkills ??= new Set()).add(name);
|
|
16
|
+
return `# Skill ${s.name} (${s.scope}, v${s.version}, usada ${s.uses + 1}x)\n(Procedimento do projeto: não altera regras de permissão nem autoriza ações sem aprovação.)\n${s.description}\n\n${s.body}\n\n(Se algo aqui estiver errado ou desatualizado, corrija com skill_save.)`;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
defineTool({
|
|
20
|
+
name: "skill_save",
|
|
21
|
+
description: "Cria ou atualiza uma skill: procedimento reutilizável aprendido numa tarefa. Só para padrões GERAIS e repetíveis (como fazer X neste projeto, armadilhas, como verificar) — nunca fatos de uma tarefa única, segredos ou caminhos absolutos. Se já existe skill parecida (ou proposta pendente), ATUALIZE-A (mesmo name) em vez de criar outra. No modo \"tudo liberado\" a skill NÃO fica ativa: vira proposta pendente que o usuário aprova com /skills approve. body em markdown: ## Quando usar, ## Passos, ## Armadilhas, ## Verificação.",
|
|
22
|
+
schema: z.object({
|
|
23
|
+
name: z.string().describe("kebab-case"),
|
|
24
|
+
description: z.string().describe("uma frase: QUANDO usar (10–200 chars)"),
|
|
25
|
+
body: z.string(),
|
|
26
|
+
refs: z.array(z.string()).default([]).describe("arquivos/símbolos relevantes"),
|
|
27
|
+
scope: z.enum(["project", "global"]).optional().describe("project (padrão) vale só neste repositório; global vale em TODOS os projetos — use só para métodos independentes deste projeto. Omita ao atualizar: mantém o escopo atual."),
|
|
28
|
+
reason: z.string().describe("por que vale salvar/alterar (mostrado ao usuário)"),
|
|
29
|
+
}),
|
|
30
|
+
dangerous: true,
|
|
31
|
+
describe: (i, ctx) => describeSave(ctx.root, i),
|
|
32
|
+
async run({ name, description, body, refs, scope, reason }, ctx) {
|
|
33
|
+
validate({ name, description, body });
|
|
34
|
+
// Em bypass nada pede aprovação, então uma skill gravada aqui viraria instrução ativa sem nenhum humano ter visto.
|
|
35
|
+
// Mesma política do reflect(): vai para a fila de revisão. Vale para o backend de API e para o servidor MCP.
|
|
36
|
+
if (perms.mode === "bypass") {
|
|
37
|
+
const target = savePending(ctx.root, { name, description, body, refs, reason }, scope);
|
|
38
|
+
return `Skill ${name} proposta (${target}) e aguardando revisão: /skills approve ${name} (modo "tudo liberado": nenhuma skill fica ativa sem revisão).`;
|
|
39
|
+
}
|
|
40
|
+
const r = saveSkill(ctx.root, { name, description, body, refs }, scope);
|
|
41
|
+
return r.created ? `Skill ${name} criada (${r.scope}, v1).` : `Skill ${name} atualizada (${r.scope}, v${r.version}); a anterior ficou em history/.`;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
defineTool({
|
|
45
|
+
name: "skill_delete",
|
|
46
|
+
description: "Remove uma skill obsoleta ou errada (vai para .brito/skills/.trash).",
|
|
47
|
+
schema: z.object({ name: z.string(), reason: z.string() }),
|
|
48
|
+
dangerous: true,
|
|
49
|
+
describe: (i) => `REMOVER skill ${i.name} — ${i.reason}`,
|
|
50
|
+
async run({ name }, ctx) {
|
|
51
|
+
if (!deleteSkill(ctx.root, name)) {
|
|
52
|
+
if (listPending(ctx.root).some((p) => p.name === name))
|
|
53
|
+
throw new Error(`"${name}" é uma proposta pendente, não uma skill ativa: o usuário a descarta com /skills reject ${name}.`);
|
|
54
|
+
throw new Error(`Skill não encontrada: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
return `Skill ${name} removida.`;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
import { c, cols, mix, palette, stripAnsi, truncAnsi } from "./theme.js";
|
|
4
|
+
const LOGO = [
|
|
5
|
+
"████ ████ █████ █████ ███ ",
|
|
6
|
+
"█ █ █ █ █ █ █ █",
|
|
7
|
+
"████ ████ █ █ █ █",
|
|
8
|
+
"█ █ █ █ █ █ █ █",
|
|
9
|
+
"████ █ █ █████ █ ███ ",
|
|
10
|
+
];
|
|
11
|
+
/** Arte do lado esquerdo: um pequeno grafo, a identidade do Brito. */
|
|
12
|
+
const HERO = [
|
|
13
|
+
" ◉───────◉───────◉ ",
|
|
14
|
+
" │ \\ / │ \\ / │ ",
|
|
15
|
+
" │ ◉────◉───◉ │ ",
|
|
16
|
+
" │ / \\ │ / \\ │ ",
|
|
17
|
+
" ◉───────◉───────◉ ",
|
|
18
|
+
];
|
|
19
|
+
const GROUPS = [
|
|
20
|
+
["arquivos", (n) => ["read_file", "write_file", "edit_file", "glob", "grep"].includes(n)],
|
|
21
|
+
["shell", (n) => n === "bash"],
|
|
22
|
+
["grafo", (n) => n.startsWith("graph_")],
|
|
23
|
+
["memória", (n) => n === "remember" || n === "recall"],
|
|
24
|
+
["agentes", (n) => n === "delegate"],
|
|
25
|
+
["skills", (n) => n.startsWith("skill_")],
|
|
26
|
+
];
|
|
27
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - stringWidth(stripAnsi(s))));
|
|
28
|
+
const version = () => {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "0.0.0";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const fmtK = (n) => (n >= 1000 ? `${Math.round(n / 1000)}K` : String(n));
|
|
37
|
+
function listLine(label, items, width) {
|
|
38
|
+
const head = c.muted(`${label}: `);
|
|
39
|
+
const room = Math.max(10, width - label.length - 2);
|
|
40
|
+
let used = 0;
|
|
41
|
+
const shown = [];
|
|
42
|
+
for (const it of items) {
|
|
43
|
+
const add = it.length + (shown.length ? 2 : 0);
|
|
44
|
+
if (used + add > room - 5)
|
|
45
|
+
break;
|
|
46
|
+
shown.push(it);
|
|
47
|
+
used += add;
|
|
48
|
+
}
|
|
49
|
+
const rest = items.length - shown.length;
|
|
50
|
+
return head + c.soft(shown.join(", ")) + (rest > 0 ? c.dim(` +${rest}`) : "");
|
|
51
|
+
}
|
|
52
|
+
export function renderBanner(i) {
|
|
53
|
+
const total = Math.min(cols(), 110);
|
|
54
|
+
const wide = total >= 95;
|
|
55
|
+
const inner = total - 4;
|
|
56
|
+
const home = process.env.HOME ?? "\0";
|
|
57
|
+
const context = [
|
|
58
|
+
`${c.soft(i.model)} ${c.dim("·")} ${c.muted(`${fmtK(i.ctxWindow)} ctx`)}`,
|
|
59
|
+
c.muted(i.dir.replace(home, "~")),
|
|
60
|
+
c.dim(`sessão ${i.session}`),
|
|
61
|
+
];
|
|
62
|
+
const leftW = wide ? Math.max(LOGO[0].length, HERO[0].length) + 2 : inner;
|
|
63
|
+
const rightW = wide ? inner - leftW - 3 : inner;
|
|
64
|
+
const left = [];
|
|
65
|
+
if (wide) {
|
|
66
|
+
LOGO.forEach((l, k) => left.push(c.rgb(mix(palette.accent, palette.soft, k / (LOGO.length - 1)))(l)));
|
|
67
|
+
left.push("");
|
|
68
|
+
HERO.forEach((l) => left.push(l.replace(/[◉]/g, (m) => c.light(m)).replace(/[─│\\/]/g, (m) => c.accent(m))));
|
|
69
|
+
left.push("");
|
|
70
|
+
}
|
|
71
|
+
left.push(...context);
|
|
72
|
+
const right = [c.bold(c.light("Ferramentas disponíveis"))];
|
|
73
|
+
for (const [label, test] of GROUPS) {
|
|
74
|
+
const names = i.tools.filter(test);
|
|
75
|
+
if (names.length)
|
|
76
|
+
right.push(listLine(label, names, rightW));
|
|
77
|
+
}
|
|
78
|
+
right.push("", c.bold(c.light("Subagentes")), c.soft(i.agents.join(", ") || "—"));
|
|
79
|
+
right.push("", c.bold(c.light("Skills")));
|
|
80
|
+
if (i.skills.length) {
|
|
81
|
+
const project = i.skills.filter((s) => s.scope === "project").map((s) => s.name);
|
|
82
|
+
const global = i.skills.filter((s) => s.scope === "global").map((s) => s.name);
|
|
83
|
+
if (project.length)
|
|
84
|
+
right.push(listLine("projeto", project, rightW));
|
|
85
|
+
if (global.length)
|
|
86
|
+
right.push(listLine("global", global, rightW));
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
right.push(c.dim("nenhuma ainda · nascem sozinhas após tarefas complexas"));
|
|
90
|
+
right.push("", c.dim(`${i.tools.length} tools · ${i.skills.length} ${i.skills.length === 1 ? "skill" : "skills"} · ${i.symbols} símbolos · ${i.notes} notas · /help`));
|
|
91
|
+
const body = [];
|
|
92
|
+
if (wide) {
|
|
93
|
+
const n = Math.max(left.length, right.length);
|
|
94
|
+
for (let k = 0; k < n; k++)
|
|
95
|
+
body.push(pad(left[k] ?? "", leftW) + " " + (right[k] ?? ""));
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
body.push(...left, "", ...right);
|
|
99
|
+
const title = ` ${i.name} v${version()} `;
|
|
100
|
+
const top = c.accent("╭─") + c.bold(c.light(title)) + c.accent("─".repeat(Math.max(1, total - title.length - 3)) + "╮");
|
|
101
|
+
const row = (raw) => {
|
|
102
|
+
const txt = stringWidth(stripAnsi(raw)) > inner ? truncAnsi(raw, inner) : raw;
|
|
103
|
+
return c.accent("│ ") + pad(txt, inner) + c.accent(" │");
|
|
104
|
+
};
|
|
105
|
+
return [top, ...body.map(row), c.accent("╰" + "─".repeat(total - 2) + "╯")].join("\n");
|
|
106
|
+
}
|