brito-cli 0.7.5 → 0.9.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/README.md +54 -4
- package/README.pt-BR.md +54 -4
- package/dist/agent/approval.js +19 -2
- package/dist/agent/claude-cli.js +10 -4
- package/dist/agent/loop.js +38 -9
- package/dist/agent/prompt.js +27 -1
- package/dist/agent/sessions.js +98 -0
- package/dist/cli.js +296 -26
- package/dist/desktop/panels.js +93 -0
- package/dist/desktop/protocol.js +5 -0
- package/dist/desktop/serve.js +191 -23
- package/dist/engine.js +28 -4
- package/dist/local/digest.js +202 -0
- package/dist/local/slm.js +218 -0
- package/dist/mcp/server.js +36 -3
- package/dist/model.js +21 -0
- package/dist/orchestrator.js +131 -0
- package/dist/permissions.js +4 -0
- package/dist/provider/anthropic-params.js +32 -0
- package/dist/provider/anthropic.js +13 -9
- package/dist/provider/fallback.js +67 -8
- package/dist/provider/index.js +4 -0
- package/dist/provider/local.js +88 -0
- package/dist/provider/resolve.js +8 -0
- package/dist/repl.js +49 -6
- package/dist/rules.js +102 -0
- package/dist/skills/builtin.js +89 -0
- package/dist/skills/describe.js +3 -2
- package/dist/skills/learn.js +2 -2
- package/dist/skills/store.js +64 -8
- package/dist/skills/trial.js +3 -3
- package/dist/tools/delegate.js +31 -22
- package/dist/tools/digest.js +29 -0
- package/dist/tools/fs.js +51 -8
- package/dist/tools/implement.js +50 -0
- package/dist/tools/registry.js +10 -1
- package/dist/tools/skills.js +10 -6
- package/dist/tools/spec.js +47 -0
- package/dist/tools/todo.js +22 -0
- package/dist/ui/ui.js +12 -0
- package/dist/util/todos.js +4 -0
- package/package.json +10 -6
- package/dist/setup.js +0 -110
package/dist/tools/delegate.js
CHANGED
|
@@ -7,11 +7,38 @@ import { subagentModel } from "../engine.js";
|
|
|
7
7
|
import { initialModel } from "../model.js";
|
|
8
8
|
import { ui } from "../ui/ui.js";
|
|
9
9
|
let seq = 0;
|
|
10
|
-
/** Conjunto de tools de um agente; `delegate` nunca
|
|
10
|
+
/** Conjunto de tools de um agente; `delegate` e `implement` nunca são repassadas (sem recursão), nem `spec_save` (a spec é do principal). */
|
|
11
11
|
function toolSet(def) {
|
|
12
12
|
const names = def.tools === "all" ? allTools().map((t) => t.name) : def.tools;
|
|
13
13
|
// Sem recursão e sem aprender: criar/alterar skills é papel do agente principal.
|
|
14
|
-
|
|
14
|
+
// A lista de tarefas é do usuário com o agente principal: um subagente não a reescreve.
|
|
15
|
+
return new Set(names.filter((n) => !["delegate", "implement", "spec_save", "skill_save", "skill_delete", "exit_plan", "todo_write"].includes(n)));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Roda um subagente em contexto isolado e devolve o texto final com o uso. O subgrafo dos símbolos de `focus` vai junto
|
|
19
|
+
* da tarefa: é a vantagem do grafo, o filho já começa sabendo onde está.
|
|
20
|
+
*/
|
|
21
|
+
export async function runSubagent(def, task, focus, ctx, opts = {}) {
|
|
22
|
+
const label = `${def.name}#${++seq}`;
|
|
23
|
+
const log = opts.log ? (s) => opts.log(`[${label}] ${s}`) : (s) => ui.sub(label, s);
|
|
24
|
+
let seed = "";
|
|
25
|
+
for (const sym of focus) {
|
|
26
|
+
const r = await execTool("graph_context", { symbol: sym }, ctx);
|
|
27
|
+
seed += `\n\n### graph_context(${sym})\n${r.text}`;
|
|
28
|
+
}
|
|
29
|
+
log(`iniciado: ${task.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
30
|
+
const sub = new Agent(buildSubagentPrompt(ctx.root, def), ctx, {
|
|
31
|
+
model: subagentModel(def, ctx.model?.() ?? initialModel(), process.env),
|
|
32
|
+
maxSteps: opts.maxSteps ?? 25,
|
|
33
|
+
quiet: true,
|
|
34
|
+
tools: toolSet(def),
|
|
35
|
+
log,
|
|
36
|
+
sink: opts.sink,
|
|
37
|
+
});
|
|
38
|
+
const text = await sub.send(seed ? `${task}\n\n# Contexto do grafo (pré-carregado)${seed}` : task);
|
|
39
|
+
const tokens = Math.round((sub.usage.input + sub.usage.output) / 1000);
|
|
40
|
+
log(`concluído: ${sub.usage.steps} passos, ~${tokens}k tokens`);
|
|
41
|
+
return { label, text, steps: sub.usage.steps, tokens, model: sub.model };
|
|
15
42
|
}
|
|
16
43
|
/** Agente somente-leitura: nenhuma tool dele escreve arquivos ou exige aprovação. */
|
|
17
44
|
function isReadOnly(def) {
|
|
@@ -39,25 +66,7 @@ defineTool({
|
|
|
39
66
|
const def = getAgent(agent);
|
|
40
67
|
if (!def)
|
|
41
68
|
throw new Error(`Agente desconhecido: ${agent}. Disponíveis: ${listAgents().map((a) => a.name).join(", ")}`);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
// O subgrafo pré-carregado é a vantagem do grafo: o filho já começa sabendo onde está.
|
|
45
|
-
let seed = "";
|
|
46
|
-
for (const sym of focus) {
|
|
47
|
-
const r = await execTool("graph_context", { symbol: sym }, ctx);
|
|
48
|
-
seed += `\n\n### graph_context(${sym})\n${r.text}`;
|
|
49
|
-
}
|
|
50
|
-
log(`iniciado: ${task.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
51
|
-
const sub = new Agent(buildSubagentPrompt(ctx.root, def), ctx, {
|
|
52
|
-
model: subagentModel(def, ctx.model?.() ?? initialModel(), process.env),
|
|
53
|
-
maxSteps: 25,
|
|
54
|
-
quiet: true,
|
|
55
|
-
tools: toolSet(def),
|
|
56
|
-
log,
|
|
57
|
-
});
|
|
58
|
-
const text = await sub.send(seed ? `${task}\n\n# Contexto do grafo (pré-carregado)${seed}` : task);
|
|
59
|
-
const tokens = Math.round((sub.usage.input + sub.usage.output) / 1000);
|
|
60
|
-
log(`concluído: ${sub.usage.steps} passos, ~${tokens}k tokens`);
|
|
61
|
-
return `[subagente ${label} · ${sub.usage.steps} passos · ~${tokens}k tokens]\n\n${text.slice(0, 12_000)}`;
|
|
69
|
+
const r = await runSubagent(def, task, focus, ctx);
|
|
70
|
+
return `[subagente ${r.label} · ${r.steps} passos · ~${r.tokens}k tokens]\n\n${r.text.slice(0, 12_000)}`;
|
|
62
71
|
},
|
|
63
72
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defineTool } from "./registry.js";
|
|
5
|
+
import { digest } from "../local/digest.js";
|
|
6
|
+
import { slmDisabled, slmStatus } from "../local/slm.js";
|
|
7
|
+
defineTool({
|
|
8
|
+
name: "digest",
|
|
9
|
+
description: "USE ESTA TOOL PRIMEIRO ao pesquisar um assunto dentro de um texto LONGO (livro, PDF extraído, ata; arquivo acima de ~100 KB), em vez de Grep + vários Read: um modelo local pequeno percorre o arquivo e devolve um plano em markdown com só TRECHOS relevantes ao objetivo, com página, conferidos no texto-fonte por código. A seleção pode ser incompleta: use as páginas indicadas para aprofundar com Read e peça mais blocos (max_chunks) se faltar algo. Exige `brito slm pull`. Não use em arquivos curtos.",
|
|
10
|
+
schema: z.object({
|
|
11
|
+
path: z.string().describe("arquivo de texto dentro do projeto"),
|
|
12
|
+
objective: z.string().describe("o que você precisa achar/montar (ex.: 'estrutura em cinco livros do saltério')"),
|
|
13
|
+
max_chunks: z.number().int().min(1).max(30).optional().describe("blocos a analisar (padrão 8; cada um leva alguns segundos)"),
|
|
14
|
+
}),
|
|
15
|
+
async run({ path: p, objective, max_chunks }, ctx) {
|
|
16
|
+
if (slmDisabled())
|
|
17
|
+
throw new Error("SLM desligado (BRITO_SLM=off): leia o arquivo com read_file.");
|
|
18
|
+
const st = await slmStatus();
|
|
19
|
+
if (!st.ready)
|
|
20
|
+
throw new Error(`SLM local indisponível${st.lib ? " (modelo não baixado: rode `brito slm pull`)" : " (instale node-llama-cpp)"}: leia o arquivo com read_file.`);
|
|
21
|
+
const full = path.resolve(ctx.root, p);
|
|
22
|
+
const rel = path.relative(path.resolve(ctx.root), full);
|
|
23
|
+
if (rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel))
|
|
24
|
+
throw new Error("Caminho fora do projeto.");
|
|
25
|
+
const text = fs.readFileSync(full, "utf8");
|
|
26
|
+
const r = await digest({ text, source: rel.split(path.sep).join("/"), objective, maxChunks: max_chunks });
|
|
27
|
+
return r.markdown;
|
|
28
|
+
},
|
|
29
|
+
});
|
package/dist/tools/fs.js
CHANGED
|
@@ -12,41 +12,83 @@ const abs = (root, p) => {
|
|
|
12
12
|
throw new Error("Caminho fora do projeto.");
|
|
13
13
|
return full;
|
|
14
14
|
};
|
|
15
|
+
/** Estado atual do arquivo, ou `undefined` se ele não existe. */
|
|
16
|
+
const stampOf = (full) => {
|
|
17
|
+
try {
|
|
18
|
+
const s = fs.statSync(full);
|
|
19
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const remember = (ctx, full) => {
|
|
26
|
+
const s = stampOf(full);
|
|
27
|
+
if (s)
|
|
28
|
+
(ctx.readFiles ??= new Map()).set(full, s);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Editar exige ter lido o arquivo nesta sessão e que ele não tenha mudado desde então (por bash, pelo usuário ou por
|
|
32
|
+
* outro processo). Sem isso o modelo edita de memória e pode desfazer o que mudou. Arquivo novo não precisa de leitura.
|
|
33
|
+
*/
|
|
34
|
+
function staleReason(ctx, p) {
|
|
35
|
+
let full;
|
|
36
|
+
try {
|
|
37
|
+
full = abs(ctx.root, p);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
} // o run devolve o erro de caminho
|
|
42
|
+
const now = stampOf(full);
|
|
43
|
+
if (!now)
|
|
44
|
+
return undefined;
|
|
45
|
+
const seen = ctx.readFiles?.get(full);
|
|
46
|
+
if (!seen)
|
|
47
|
+
return `${p} ainda não foi lido nesta sessão. Leia com read_file antes de alterar.`;
|
|
48
|
+
if (seen.mtimeMs !== now.mtimeMs || seen.size !== now.size)
|
|
49
|
+
return `${p} mudou desde a última leitura (por um comando, pelo usuário ou por outro processo). Leia de novo com read_file antes de alterar.`;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
15
52
|
defineTool({
|
|
16
53
|
name: "read_file",
|
|
17
54
|
concurrent: true,
|
|
18
55
|
description: "Lê um arquivo do projeto, com números de linha. Use offset/limit para arquivos grandes.",
|
|
19
56
|
schema: z.object({ path: z.string(), offset: z.number().int().min(1).optional(), limit: z.number().int().min(1).optional() }),
|
|
20
|
-
async run({ path: p, offset = 1, limit = 2000 },
|
|
21
|
-
const
|
|
57
|
+
async run({ path: p, offset = 1, limit = 2000 }, ctx) {
|
|
58
|
+
const full = abs(ctx.root, p);
|
|
59
|
+
const lines = fs.readFileSync(full, "utf8").split("\n");
|
|
60
|
+
remember(ctx, full);
|
|
22
61
|
return lines.slice(offset - 1, offset - 1 + limit).map((l, i) => `${offset + i}\t${l}`).join("\n");
|
|
23
62
|
},
|
|
24
63
|
});
|
|
25
64
|
defineTool({
|
|
26
65
|
name: "write_file",
|
|
27
|
-
description: "Cria ou sobrescreve um arquivo.",
|
|
66
|
+
description: "Cria ou sobrescreve um arquivo. Para sobrescrever um arquivo existente, leia-o antes com read_file.",
|
|
28
67
|
schema: z.object({ path: z.string(), content: z.string() }),
|
|
29
68
|
dangerous: true,
|
|
30
69
|
edit: true,
|
|
31
70
|
mutates: true,
|
|
32
71
|
describe: (i) => `escrever ${i.path} (${i.content.length} chars)`,
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
precheck: (i, ctx) => staleReason(ctx, i.path),
|
|
73
|
+
async run({ path: p, content }, ctx) {
|
|
74
|
+
const full = abs(ctx.root, p);
|
|
35
75
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
36
76
|
fs.writeFileSync(full, content);
|
|
77
|
+
remember(ctx, full); // o modelo sabe o conteúdo que acabou de escrever
|
|
37
78
|
return `Escrito ${p}`;
|
|
38
79
|
},
|
|
39
80
|
});
|
|
40
81
|
defineTool({
|
|
41
82
|
name: "edit_file",
|
|
42
|
-
description: "Substitui old_string por new_string em um arquivo. old_string deve ser único (ou use replace_all).",
|
|
83
|
+
description: "Substitui old_string por new_string em um arquivo. old_string deve ser único (ou use replace_all). Leia o arquivo com read_file antes.",
|
|
43
84
|
schema: z.object({ path: z.string(), old_string: z.string(), new_string: z.string(), replace_all: z.boolean().optional() }),
|
|
44
85
|
dangerous: true,
|
|
45
86
|
edit: true,
|
|
46
87
|
mutates: true,
|
|
47
88
|
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, "⏎")}`,
|
|
48
|
-
|
|
49
|
-
|
|
89
|
+
precheck: (i, ctx) => staleReason(ctx, i.path),
|
|
90
|
+
async run({ path: p, old_string, new_string, replace_all }, ctx) {
|
|
91
|
+
const full = abs(ctx.root, p);
|
|
50
92
|
const src = fs.readFileSync(full, "utf8");
|
|
51
93
|
const count = src.split(old_string).length - 1;
|
|
52
94
|
if (count === 0)
|
|
@@ -54,6 +96,7 @@ defineTool({
|
|
|
54
96
|
if (count > 1 && !replace_all)
|
|
55
97
|
throw new Error(`old_string aparece ${count}x; torne-o único ou use replace_all.`);
|
|
56
98
|
fs.writeFileSync(full, replace_all ? src.split(old_string).join(new_string) : src.replace(old_string, () => new_string));
|
|
99
|
+
remember(ctx, full);
|
|
57
100
|
return `Editado ${p} (${replace_all ? count : 1} substituição)`;
|
|
58
101
|
},
|
|
59
102
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { defineTool, execTool } from "./registry.js";
|
|
4
|
+
import { runSubagent } from "./delegate.js";
|
|
5
|
+
import { IMPLEMENTER, implStatus, orch } from "../orchestrator.js";
|
|
6
|
+
import { initialModel } from "../model.js";
|
|
7
|
+
/** Saída de um git só de leitura; fora de um repositório (ou sem git), `null`. */
|
|
8
|
+
function git(root, args) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
execFile("git", args, { cwd: root, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => resolve(err ? null : stdout.trim()));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Progresso e avisos do implementador; o servidor MCP troca o destino (o stdout de lá é o protocolo). */
|
|
14
|
+
export const implementIo = {};
|
|
15
|
+
defineTool({
|
|
16
|
+
name: "implement",
|
|
17
|
+
description: "Só no modo orquestrador: manda UMA task ao modelo implementador (contexto ISOLADO: ele não vê esta conversa). " +
|
|
18
|
+
"Ele edita os arquivos e roda a verificação; a tool devolve o relato dele, o git status/diff e a saída do `verify`, " +
|
|
19
|
+
"rodado aqui, fora do controle dele. Tasks rodam uma por vez.",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
task: z.string().min(10).describe("task autocontida: objetivo, contratos que não podem mudar e critério de pronto"),
|
|
22
|
+
files: z.array(z.string()).max(20).default([]).describe("arquivos que a task deve criar ou alterar"),
|
|
23
|
+
verify: z.string().optional().describe("comando que prova a task (ex.: npm test -- foo); roda depois do implementador"),
|
|
24
|
+
focus: z.array(z.string()).max(5).default([]).describe("símbolos para pré-carregar o subgrafo"),
|
|
25
|
+
}),
|
|
26
|
+
mutates: true,
|
|
27
|
+
async run({ task, files, verify, focus }, ctx) {
|
|
28
|
+
if (!orch.on)
|
|
29
|
+
return "O modo orquestrador está desligado; implemente você mesmo.";
|
|
30
|
+
const status = implStatus(ctx.model?.() ?? initialModel(), orch.model, process.env);
|
|
31
|
+
if (!status.ready)
|
|
32
|
+
throw new Error(`implementador (${orch.model}) indisponível: ${status.reason}`);
|
|
33
|
+
let full = task;
|
|
34
|
+
if (files.length)
|
|
35
|
+
full += `\n\n# Arquivos\n${files.map((f) => `- ${f}`).join("\n")}`;
|
|
36
|
+
if (verify)
|
|
37
|
+
full += `\n\n# Como verificar\n${verify}`;
|
|
38
|
+
const r = await runSubagent(IMPLEMENTER, full, focus, ctx, { maxSteps: 40, ...implementIo });
|
|
39
|
+
// Fatos que não dependem do relato do implementador: o que mudou no disco e o que a verificação diz.
|
|
40
|
+
const st = await git(ctx.root, ["status", "--short"]);
|
|
41
|
+
const diff = st === null ? null : await git(ctx.root, ["diff", "--stat"]);
|
|
42
|
+
let out = `[implementador ${r.label} · ${status.label} · ${r.steps} passos · ~${r.tokens}k tokens]\n\n${r.text.slice(0, 8_000)}`;
|
|
43
|
+
out += st === null ? "\n\n# git\n(não é um repositório git: confira os arquivos lendo-os)" : `\n\n# git status --short\n${st || "(nada mudou)"}\n\n# git diff --stat\n${diff || "(sem diff em arquivos rastreados)"}`;
|
|
44
|
+
if (verify) {
|
|
45
|
+
const v = await execTool("bash", { command: verify }, ctx);
|
|
46
|
+
out += `\n\n# verificação: ${verify}\n${v.isError ? `[não rodou] ${v.text}` : v.text.slice(-6_000)}`;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
},
|
|
50
|
+
});
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { perms } from "../permissions.js";
|
|
3
|
+
import { decide, loadRules } from "../rules.js";
|
|
3
4
|
const registry = new Map();
|
|
4
5
|
export function defineTool(t) {
|
|
5
6
|
registry.set(t.name, t);
|
|
@@ -29,10 +30,18 @@ export async function execTool(name, input, ctx) {
|
|
|
29
30
|
const parsed = tool.schema.safeParse(input);
|
|
30
31
|
if (!parsed.success)
|
|
31
32
|
return { text: `Input inválido: ${z.prettifyError(parsed.error)}`, isError: true };
|
|
33
|
+
// `deny` vale em qualquer modo, inclusive "tudo liberado"; `allow` só dispensa a pergunta.
|
|
34
|
+
const decision = decide(loadRules(ctx.root), tool.name, parsed.data, ctx.root);
|
|
35
|
+
if (decision === "deny")
|
|
36
|
+
return { text: `${tool.name} bloqueada por uma regra deny das permissões (.brito/settings.json). Não tente contornar; peça ao usuário se precisar.`, isError: true };
|
|
37
|
+
// Antes da aprovação: não faz sentido o usuário aprovar uma chamada que vai falhar.
|
|
38
|
+
const problem = tool.precheck?.(parsed.data, ctx);
|
|
39
|
+
if (problem)
|
|
40
|
+
return { text: problem, isError: true };
|
|
32
41
|
if (tool.dangerous) {
|
|
33
42
|
if (perms.mode === "plan")
|
|
34
43
|
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);
|
|
44
|
+
const skip = perms.mode === "bypass" || (perms.mode === "auto" && tool.edit) || decision === "allow";
|
|
36
45
|
const ok = skip || await ctx.ask(`${tool.name}: ${tool.describe?.(parsed.data, ctx) ?? JSON.stringify(parsed.data).slice(0, 200)}`);
|
|
37
46
|
if (!ok)
|
|
38
47
|
return { text: "Usuário negou a execução.", isError: true };
|
package/dist/tools/skills.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineTool } from "./registry.js";
|
|
3
|
-
import { bumpUses, deleteSkill, getSkill, listPending, saveSkill, validate } from "../skills/store.js";
|
|
3
|
+
import { bumpUses, deleteSkill, getSkill, listPending, saveSkill, suggestSkills, validate } from "../skills/store.js";
|
|
4
4
|
import { saveAuto } from "../skills/trial.js";
|
|
5
5
|
import { describeSave } from "../skills/describe.js";
|
|
6
6
|
import { perms } from "../permissions.js";
|
|
@@ -10,11 +10,13 @@ defineTool({
|
|
|
10
10
|
schema: z.object({ name: z.string() }),
|
|
11
11
|
async run({ name }, ctx) {
|
|
12
12
|
const s = getSkill(ctx.root, name);
|
|
13
|
-
if (!s)
|
|
14
|
-
|
|
13
|
+
if (!s) {
|
|
14
|
+
const sug = suggestSkills(ctx.root, name.replace(/[-_]/g, " "));
|
|
15
|
+
throw new Error(`Skill não encontrada: ${name}.${sug.length ? ` Parecidas: ${sug.map((x) => `${x.skill.name} (${x.skill.description})`).join("; ")}` : ""}`);
|
|
16
|
+
}
|
|
15
17
|
bumpUses(ctx.root, name);
|
|
16
18
|
(ctx.loadedSkills ??= new Set()).add(name);
|
|
17
|
-
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.)`;
|
|
19
|
+
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}${s.model ? `\n(Modelo recomendado: ${s.model}.)` : ""}${s.lastLoss ? `\n(Atenção: o último uso com tropeço falhou em: ${s.lastLoss})` : ""}\n\n${s.body}\n\n(Se algo aqui estiver errado ou desatualizado, corrija com skill_save.)`;
|
|
18
20
|
},
|
|
19
21
|
});
|
|
20
22
|
defineTool({
|
|
@@ -27,15 +29,17 @@ defineTool({
|
|
|
27
29
|
refs: z.array(z.string()).default([]).describe("arquivos/símbolos relevantes"),
|
|
28
30
|
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."),
|
|
29
31
|
reason: z.string().describe("por que vale salvar/alterar (mostrado ao usuário)"),
|
|
32
|
+
invoke: z.enum(["auto", "manual"]).optional().describe("manual = só roda quando o usuário digita /nome (fica fora do índice)"),
|
|
33
|
+
model: z.string().optional().describe("modelo recomendado para seguir a skill (dica)"),
|
|
30
34
|
}),
|
|
31
35
|
dangerous: true,
|
|
32
36
|
describe: (i, ctx) => describeSave(ctx.root, i),
|
|
33
|
-
async run({ name, description, body, refs, scope, reason }, ctx) {
|
|
37
|
+
async run({ name, description, body, refs, scope, reason, invoke, model }, ctx) {
|
|
34
38
|
validate({ name, description, body });
|
|
35
39
|
// Em bypass nada pede aprovação: usa a mesma política segura da reflexão autônoma (em teste; o arriscado vai para a fila de revisão).
|
|
36
40
|
if (perms.mode === "bypass")
|
|
37
41
|
return saveAuto(ctx.root, { name, description, body, refs, reason }).msg;
|
|
38
|
-
const r = saveSkill(ctx.root, { name, description, body, refs }, scope);
|
|
42
|
+
const r = saveSkill(ctx.root, { name, description, body, refs, invoke, model }, scope);
|
|
39
43
|
return r.created ? `Skill ${name} criada (${r.scope}, v1).` : `Skill ${name} atualizada (${r.scope}, v${r.version}); a anterior ficou em history/.`;
|
|
40
44
|
},
|
|
41
45
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defineTool } from "./registry.js";
|
|
5
|
+
import { orch } from "../orchestrator.js";
|
|
6
|
+
/** Pasta, na raiz do projeto, onde o orquestrador grava a spec e as tasks de cada feature para o usuário avaliar. */
|
|
7
|
+
export const SPEC_DIR = "spec";
|
|
8
|
+
export const SPEC_FILES = ["spec.md", "tasks.md"];
|
|
9
|
+
/**
|
|
10
|
+
* Única escrita do principal no modo orquestrador: só `spec/<feature>/spec.md` e `tasks.md`, nada de código. O caminho
|
|
11
|
+
* é montado aqui (slug + nome fixo), então não há como escapar da pasta.
|
|
12
|
+
*/
|
|
13
|
+
defineTool({
|
|
14
|
+
name: "spec_save",
|
|
15
|
+
description: "Só no modo orquestrador: grava (cria ou sobrescreve) a spec ou a lista de tasks de uma feature em " +
|
|
16
|
+
"spec/<feature>/spec.md ou spec/<feature>/tasks.md, para o usuário avaliar e para o implementador ler. " +
|
|
17
|
+
"Mande o arquivo inteiro em `content`.",
|
|
18
|
+
schema: z.object({
|
|
19
|
+
feature: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "use um slug: minúsculas, números e hífens (ex.: login-sso)").describe("slug da feature: nome da pasta em spec/"),
|
|
20
|
+
file: z.enum(SPEC_FILES).describe("spec.md (o quê e por quê) ou tasks.md (lista numerada de tasks)"),
|
|
21
|
+
content: z.string().min(10).describe("conteúdo completo do arquivo em markdown"),
|
|
22
|
+
}),
|
|
23
|
+
dangerous: true,
|
|
24
|
+
edit: true,
|
|
25
|
+
mutates: true,
|
|
26
|
+
describe: (i) => `gravar ${SPEC_DIR}/${i.feature}/${i.file} (${i.content.length} chars)`,
|
|
27
|
+
async run({ feature, file, content }, { root }) {
|
|
28
|
+
if (!orch.on)
|
|
29
|
+
return "O modo orquestrador está desligado; spec_save só existe nele.";
|
|
30
|
+
const rel = `${SPEC_DIR}/${feature}/${file}`;
|
|
31
|
+
const full = path.join(root, SPEC_DIR, feature, file);
|
|
32
|
+
const data = content.endsWith("\n") ? content : content + "\n";
|
|
33
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
34
|
+
// Cria com "wx" (falha se já existir) em vez de existsSync + write: checagem e escrita numa operação só, sem TOCTOU.
|
|
35
|
+
let existed = false;
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(full, data, { flag: "wx" });
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (e.code !== "EEXIST")
|
|
41
|
+
throw e;
|
|
42
|
+
existed = true;
|
|
43
|
+
fs.writeFileSync(full, data);
|
|
44
|
+
}
|
|
45
|
+
return `${existed ? "Atualizado" : "Criado"} ${rel}`;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "./registry.js";
|
|
3
|
+
import { renderTodos } from "../util/todos.js";
|
|
4
|
+
defineTool({
|
|
5
|
+
name: "todo_write",
|
|
6
|
+
description: "Mantém a lista de tarefas visível para o usuário. Use em tarefas com 3+ passos ou quando o usuário pede várias coisas: " +
|
|
7
|
+
"crie a lista no início, marque um item in_progress antes de começar e completed logo ao terminar (não acumule). " +
|
|
8
|
+
"Mande sempre a lista inteira; ela substitui a anterior. Não use para tarefas de um passo.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
todos: z.array(z.object({
|
|
11
|
+
content: z.string().min(1).describe("o que fazer, no imperativo"),
|
|
12
|
+
status: z.enum(["pending", "in_progress", "completed"]),
|
|
13
|
+
})).max(50),
|
|
14
|
+
}),
|
|
15
|
+
async run({ todos }, ctx) {
|
|
16
|
+
ctx.todos = todos;
|
|
17
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
18
|
+
const doing = todos.filter((t) => t.status === "in_progress").length;
|
|
19
|
+
const note = doing > 1 ? "\n(aviso: mais de um item in_progress; mantenha um por vez)" : "";
|
|
20
|
+
return `${renderTodos(todos)}\n\n${done}/${todos.length} concluídas${note}`;
|
|
21
|
+
},
|
|
22
|
+
});
|
package/dist/ui/ui.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BOLD, CODE, ITALIC, OFF, c, cols, palette, useColor } from "./theme.js";
|
|
2
2
|
import { renderBanner } from "./banner.js";
|
|
3
3
|
import { renderTable } from "./table.js";
|
|
4
|
+
import { todoLine } from "../util/todos.js";
|
|
4
5
|
import stringWidth from "string-width";
|
|
5
6
|
export const NAME = "Brito";
|
|
6
7
|
const isTTY = !!process.stdout.isTTY;
|
|
@@ -405,6 +406,15 @@ class UI {
|
|
|
405
406
|
this.running.delete(id);
|
|
406
407
|
this.textEnd();
|
|
407
408
|
this.print(this.toolLine(r.name, r.input, Date.now() - r.t0, isError));
|
|
409
|
+
if (r.name === "todo_write" && !isError)
|
|
410
|
+
this.printTodos(r.input?.todos ?? []);
|
|
411
|
+
}
|
|
412
|
+
/** A lista do `todo_write`, logo abaixo da linha da tool: em andamento destacado, concluído apagado. */
|
|
413
|
+
printTodos(todos) {
|
|
414
|
+
for (const t of todos) {
|
|
415
|
+
const line = todoLine(t);
|
|
416
|
+
this.print(` ${t.status === "in_progress" ? c.bold(c.soft(line)) : t.status === "completed" ? c.dim(line) : c.muted(line)}`);
|
|
417
|
+
}
|
|
408
418
|
}
|
|
409
419
|
/** Só o prefixo + emoji + nome, sem argumento nem tempo. */
|
|
410
420
|
toolLabel(name) { return `${c.accent("┊")} ${emojiOf(name)} ${c.soft(name)}`; }
|
|
@@ -461,6 +471,7 @@ class UI {
|
|
|
461
471
|
...(s.project ? [[0, c.light("📁 ") + c.bold(c.soft(s.project))]] : []),
|
|
462
472
|
...(s.mode ? [[0, s.mode]] : []),
|
|
463
473
|
[0, c.light("⬢ ") + c.bold(c.soft(s.model))],
|
|
474
|
+
...(s.engine ? [[s.engine.fallback ? 0 : 1, s.engine.fallback ? c.bold(c.rgb(palette.warn)(s.engine.text)) : c.muted(s.engine.text)]] : []),
|
|
464
475
|
[0, label("contexto") + c.muted(`${fmtTokens(s.ctxTokens)}/${fmtTokens(s.ctxWindow)}`) + ` [${bar}] ${colour(`${pct}%`)}`],
|
|
465
476
|
...(hit !== undefined ? [[2, label("cache") + hitColour(`◎ ${Math.round(hit)}%`)]] : []),
|
|
466
477
|
[3, label("tokens") + c.muted(`↑${fmtTokens(s.input)} ↓${fmtTokens(s.output)}`)],
|
|
@@ -486,6 +497,7 @@ class UI {
|
|
|
486
497
|
["cache", "% do prompt reaproveitado do cache (◎). Alto (verde) = respostas mais rápidas e baratas. Some enquanto não há dado."],
|
|
487
498
|
["tokens", "total da sessão: ↑ enviados ao modelo (inclui cache) · ↓ gerados por ele."],
|
|
488
499
|
["custo", "gasto estimado da sessão em dólares."],
|
|
500
|
+
["motor", "de onde vêm as respostas: ☁ nuvem (Claude), 🌐 endpoint (ex.: qwen) ou 💻 local (SLM). ⚠ FALLBACK = troca automática ativa, com a hora em que o Claude é tentado de novo."],
|
|
489
501
|
["tempo", "há quanto tempo a sessão está aberta · (ativo) soma do tempo gasto em todos os turnos."],
|
|
490
502
|
["📁", "projeto (pasta) em que o Brito está trabalhando — também aparece no título da janela do terminal."],
|
|
491
503
|
["skill", "skill carregada (skill_load) no turno em andamento."],
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const TODO_MARK = { pending: "☐", in_progress: "◐", completed: "☑" };
|
|
2
|
+
export const todoLine = (t) => `${TODO_MARK[t.status]} ${t.content}`;
|
|
3
|
+
/** Lista em texto puro (resultado da tool, app de desktop); o terminal colore a mesma coisa. */
|
|
4
|
+
export const renderTodos = (todos) => (todos.length ? todos.map(todoLine).join("\n") : "(lista vazia)");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Coding-agent CLI with a code knowledge graph, memory and reviewable skills / CLI de agente de código com grafo de conhecimento, memória e skills revisáveis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
"desktop": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && npm --prefix desktop start",
|
|
18
18
|
"bundle": "node build-bundle.mjs",
|
|
19
19
|
"desktop:app": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --install",
|
|
20
|
-
"desktop:dmg": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --dmg"
|
|
20
|
+
"desktop:dmg": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && node desktop/package.mjs --dmg",
|
|
21
|
+
"release": "node scripts/release.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@anthropic-ai/sdk": "^
|
|
24
|
-
"@ladybugdb/core": "0.
|
|
25
|
-
"@modelcontextprotocol/sdk": "^1.32.
|
|
24
|
+
"@anthropic-ai/sdk": "^1.5.0",
|
|
25
|
+
"@ladybugdb/core": "^0.22.3",
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
26
27
|
"commander": "^14.0.3",
|
|
27
28
|
"string-width": "^8.3.0",
|
|
28
29
|
"tinyglobby": "^0.2.17",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"zod": "^4.6.5"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"@types/node": "
|
|
35
|
+
"@types/node": "^26.6.6",
|
|
35
36
|
"esbuild": "^0.28.2",
|
|
36
37
|
"tsx": "latest",
|
|
37
38
|
"typescript": "latest"
|
|
@@ -73,5 +74,8 @@
|
|
|
73
74
|
],
|
|
74
75
|
"overrides": {
|
|
75
76
|
"tar": "^7.5.22"
|
|
77
|
+
},
|
|
78
|
+
"optionalDependencies": {
|
|
79
|
+
"node-llama-cpp": "^3.22.3"
|
|
76
80
|
}
|
|
77
81
|
}
|
package/dist/setup.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
import { hasClaudeCli } from "./agent/claude-cli.js";
|
|
3
|
-
import { DEFAULT_MODEL, MODEL_ALIASES, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
|
|
4
|
-
import { c } from "./ui/theme.js";
|
|
5
|
-
const out = (s = "") => process.stdout.write(s + "\n");
|
|
6
|
-
/** Menu de setas (↑/↓, j/k, número, Enter). Ctrl+C cancela o assistente. */
|
|
7
|
-
async function select(title, options, current = 0) {
|
|
8
|
-
const stdin = process.stdin;
|
|
9
|
-
readline.emitKeypressEvents(stdin);
|
|
10
|
-
stdin.setRawMode(true);
|
|
11
|
-
stdin.resume();
|
|
12
|
-
let idx = current;
|
|
13
|
-
let drawn = 0;
|
|
14
|
-
const render = () => {
|
|
15
|
-
if (drawn)
|
|
16
|
-
process.stdout.write(`\x1b[${drawn}A\x1b[J`);
|
|
17
|
-
const lines = [c.bold(title)];
|
|
18
|
-
options.forEach((o, i) => {
|
|
19
|
-
const on = i === idx;
|
|
20
|
-
lines.push(` ${on ? c.accent("❯") : " "} ${on ? c.light(o.label) : o.label}${o.hint ? c.muted(" " + o.hint) : ""}`);
|
|
21
|
-
});
|
|
22
|
-
lines.push(c.muted(" ↑/↓ navegar · Enter confirmar"));
|
|
23
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
24
|
-
drawn = lines.length;
|
|
25
|
-
};
|
|
26
|
-
render();
|
|
27
|
-
return new Promise((resolve) => {
|
|
28
|
-
const onKey = (_, key) => {
|
|
29
|
-
if (key.ctrl && key.name === "c") {
|
|
30
|
-
stdin.setRawMode(false);
|
|
31
|
-
out();
|
|
32
|
-
process.exit(130);
|
|
33
|
-
}
|
|
34
|
-
if (key.name === "up" || key.name === "k")
|
|
35
|
-
idx = (idx + options.length - 1) % options.length;
|
|
36
|
-
else if (key.name === "down" || key.name === "j")
|
|
37
|
-
idx = (idx + 1) % options.length;
|
|
38
|
-
else if (key.sequence && /^[1-9]$/.test(key.sequence) && Number(key.sequence) <= options.length)
|
|
39
|
-
idx = Number(key.sequence) - 1;
|
|
40
|
-
else if (key.name === "return") {
|
|
41
|
-
stdin.off("keypress", onKey);
|
|
42
|
-
stdin.setRawMode(false);
|
|
43
|
-
stdin.pause();
|
|
44
|
-
process.stdout.write(`\x1b[${drawn}A\x1b[J`);
|
|
45
|
-
out(`${c.bold(title)} ${c.ok("✓")} ${c.light(options[idx].label)}`);
|
|
46
|
-
return resolve(options[idx].value);
|
|
47
|
-
}
|
|
48
|
-
render();
|
|
49
|
-
};
|
|
50
|
-
stdin.on("keypress", onKey);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const step = (n, total, name) => out(`\n${c.accent(`◆ Passo ${n}/${total}`)} ${c.muted("·")} ${c.bold(name)}`);
|
|
54
|
-
export async function runSetup(opts = {}) {
|
|
55
|
-
const interactive = !!process.stdin.isTTY && !!process.stdout.isTTY && !opts.yes;
|
|
56
|
-
const total = 4;
|
|
57
|
-
const hasKey = !!process.env.ANTHROPIC_API_KEY;
|
|
58
|
-
const hasCli = hasClaudeCli();
|
|
59
|
-
out();
|
|
60
|
-
out(c.accent(" ╭─────────────────────────────────────╮"));
|
|
61
|
-
out(c.accent(" │") + c.bold(" Brito · assistente de configuração".padEnd(37)) + c.accent("│"));
|
|
62
|
-
out(c.accent(" ╰─────────────────────────────────────╯"));
|
|
63
|
-
out(c.muted(" Escolhas ficam em ~/.brito/config.json; rode `brito setup` de novo quando quiser mudar."));
|
|
64
|
-
// 1 · autenticação
|
|
65
|
-
step(1, total, "Autenticação");
|
|
66
|
-
out(` ${hasKey ? c.ok("✓") : c.muted("·")} ANTHROPIC_API_KEY ${hasKey ? "definida" : "não definida"}`);
|
|
67
|
-
out(` ${hasCli ? c.ok("✓") : c.muted("·")} Claude Code (claude CLI) ${hasCli ? "encontrado" : "não encontrado"}`);
|
|
68
|
-
if (!hasKey && !hasCli) {
|
|
69
|
-
out(c.warn(" Nenhum acesso detectado. Escolha um e depois rode `brito`:"));
|
|
70
|
-
out(c.muted(" • export ANTHROPIC_API_KEY=sk-ant-… (chave da API)"));
|
|
71
|
-
out(c.muted(" • npm i -g @anthropic-ai/claude-code && claude → /login (assinatura)"));
|
|
72
|
-
}
|
|
73
|
-
let model = DEFAULT_MODEL;
|
|
74
|
-
let effort;
|
|
75
|
-
let showReasoning = true;
|
|
76
|
-
if (interactive) {
|
|
77
|
-
// 2 · modelo
|
|
78
|
-
step(2, total, "Modelo padrão");
|
|
79
|
-
const names = Object.keys(MODEL_ALIASES);
|
|
80
|
-
const hints = { fable: "mais capaz", opus: "raciocínio profundo", sonnet: "equilíbrio (recomendado)", haiku: "rápido e barato" };
|
|
81
|
-
model = await select("Qual modelo usar por padrão?", names.map((n) => ({ label: n, hint: hints[n], value: MODEL_ALIASES[n] })), names.indexOf("sonnet"));
|
|
82
|
-
// 3 · esforço
|
|
83
|
-
step(3, total, "Esforço de raciocínio");
|
|
84
|
-
effort = await select("Quanto o modelo deve pensar?", [
|
|
85
|
-
{ label: "padrão", hint: "deixa o claude decidir", value: undefined },
|
|
86
|
-
{ label: "low", hint: "respostas rápidas", value: "low" },
|
|
87
|
-
{ label: "medium", value: "medium" },
|
|
88
|
-
{ label: "high", hint: "tarefas difíceis", value: "high" },
|
|
89
|
-
{ label: "max", hint: "mais lento e caro", value: "max" },
|
|
90
|
-
]);
|
|
91
|
-
// 4 · exibição
|
|
92
|
-
step(4, total, "Exibição");
|
|
93
|
-
showReasoning = await select("Mostrar o raciocínio do modelo?", [
|
|
94
|
-
{ label: "sim", hint: "recortado, como no Hermes", value: true },
|
|
95
|
-
{ label: "não", value: false },
|
|
96
|
-
]);
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
out(c.muted(`\n Modo não interativo: usando padrões (${DEFAULT_MODEL}).`));
|
|
100
|
-
}
|
|
101
|
-
saveModel(model);
|
|
102
|
-
saveEffort(effort);
|
|
103
|
-
saveReasoningDisplay({ show: showReasoning });
|
|
104
|
-
const row = (k, v) => out(` ${c.muted(k.padEnd(12))} ${v}`);
|
|
105
|
-
out(`\n${c.ok("✓")} ${c.bold("Tudo pronto")}`);
|
|
106
|
-
row("modelo", model);
|
|
107
|
-
row("esforço", effort ?? "padrão");
|
|
108
|
-
row("raciocínio", showReasoning ? "visível" : "oculto");
|
|
109
|
-
out(`\n Próximo passo: ${c.light("cd seu-projeto && brito")}\n`);
|
|
110
|
-
}
|