brito-cli 0.7.4 → 0.8.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 -7
- package/LICENSE +75 -75
- package/README.md +298 -259
- package/README.pt-BR.md +301 -262
- package/dist/agent/approval.js +19 -2
- package/dist/agent/claude-cli.js +1 -1
- package/dist/agent/loop.js +30 -7
- package/dist/agent/prompt.js +51 -25
- package/dist/agent/sessions.js +98 -0
- package/dist/cli.js +219 -25
- package/dist/desktop/serve.js +34 -11
- package/dist/engine.js +28 -4
- package/dist/graph/migrate.js +6 -6
- package/dist/local/digest.js +202 -0
- package/dist/local/slm.js +218 -0
- package/dist/mcp/server.js +4 -3
- package/dist/permissions.js +3 -3
- 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 +44 -5
- package/dist/rules.js +102 -0
- package/dist/skills/builtin.js +89 -0
- package/dist/skills/consolidate.js +7 -7
- package/dist/skills/describe.js +3 -2
- package/dist/skills/learn.js +23 -23
- package/dist/skills/store.js +64 -8
- package/dist/skills/trial.js +3 -3
- package/dist/tools/delegate.js +2 -1
- package/dist/tools/digest.js +29 -0
- package/dist/tools/fs.js +51 -8
- package/dist/tools/registry.js +10 -1
- package/dist/tools/skills.js +15 -13
- package/dist/tools/todo.js +22 -0
- package/dist/ui/ui.js +12 -0
- package/dist/util/todos.js +4 -0
- package/package.json +80 -77
- package/dist/setup.js +0 -110
package/dist/skills/store.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseFrontmatter, stringifyFrontmatter } from "../util/frontmatter.js";
|
|
5
|
+
import { BUILTIN_SKILLS } from "./builtin.js";
|
|
5
6
|
export const NAME_RE = /^[a-z0-9][a-z0-9-]{1,48}$/;
|
|
6
7
|
const today = () => new Date().toISOString().slice(0, 10);
|
|
7
8
|
/** Skills globais valem para todos os projetos. `BRITO_HOME` troca o local (útil em testes). */
|
|
@@ -28,12 +29,15 @@ function parse(text, scope) {
|
|
|
28
29
|
losses: Number(meta.losses) || 0,
|
|
29
30
|
body: body.trim(),
|
|
30
31
|
scope,
|
|
32
|
+
invoke: meta.invoke === "manual" ? "manual" : "auto",
|
|
33
|
+
model: meta.model ?? "",
|
|
34
|
+
lastLoss: meta.lastLoss ?? "",
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
export function writeSkill(root, s) {
|
|
34
38
|
const file = fileOf(root, s.scope, s.name);
|
|
35
39
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
-
fs.writeFileSync(file, stringifyFrontmatter({ name: s.name, description: s.description, version: s.version, uses: s.uses, created: s.created, updated: s.updated, lastUsed: s.lastUsed, refs: s.refs.join(", "), status: s.status, origin: s.origin, wins: s.wins, losses: s.losses }, s.body));
|
|
40
|
+
fs.writeFileSync(file, stringifyFrontmatter({ name: s.name, description: s.description, version: s.version, uses: s.uses, created: s.created, updated: s.updated, lastUsed: s.lastUsed, refs: s.refs.join(", "), status: s.status, origin: s.origin, wins: s.wins, losses: s.losses, ...(s.invoke === "manual" ? { invoke: "manual" } : {}), ...(s.model ? { model: s.model } : {}), ...(s.lastLoss ? { lastLoss: s.lastLoss } : {}) }, s.body));
|
|
37
41
|
}
|
|
38
42
|
/** Lê uma skill de um escopo específico. */
|
|
39
43
|
export function getSkillIn(root, name, scope) {
|
|
@@ -48,7 +52,19 @@ export function getSkillIn(root, name, scope) {
|
|
|
48
52
|
}
|
|
49
53
|
/** Resolve por nome: a skill do projeto sobrepõe a global de mesmo nome. */
|
|
50
54
|
export function getSkill(root, name) {
|
|
51
|
-
return getSkillIn(root, name, "project") ?? getSkillIn(root, name, "global");
|
|
55
|
+
return getSkillIn(root, name, "project") ?? getSkillIn(root, name, "global") ?? getBuiltin(name);
|
|
56
|
+
}
|
|
57
|
+
/** Skills embutidas: disponíveis em qualquer projeto sem gravar nada em disco. */
|
|
58
|
+
export function getBuiltin(name) {
|
|
59
|
+
const b = BUILTIN_SKILLS.find((x) => x.name === name);
|
|
60
|
+
if (!b)
|
|
61
|
+
return null;
|
|
62
|
+
const d = "2026-11-07";
|
|
63
|
+
return { name: b.name, description: b.description, version: 1, uses: 0, created: d, updated: d, lastUsed: d, refs: [], status: "active", origin: "manual", wins: 0, losses: 0, body: b.body, scope: "global", invoke: "manual", model: b.model ?? "", lastLoss: "", builtin: true };
|
|
64
|
+
}
|
|
65
|
+
/** Embutidas ainda não sobrepostas por uma skill do usuário. */
|
|
66
|
+
export function listBuiltins(root) {
|
|
67
|
+
return BUILTIN_SKILLS.flatMap((b) => (getSkillIn(root, b.name, "project") ?? getSkillIn(root, b.name, "global") ? [] : [getBuiltin(b.name)]));
|
|
52
68
|
}
|
|
53
69
|
function listScope(root, scope) {
|
|
54
70
|
const dir = dirOf(root, scope);
|
|
@@ -118,6 +134,17 @@ export function lintSkill(input) {
|
|
|
118
134
|
const destructive = DESTRUCTIVE_RULES.filter((r) => r.re.test(text)).map((r) => `comando destrutivo (${r.msg}): só mantenha se houver guarda (backup, cópia/ambiente de teste ou confirmação)`);
|
|
119
135
|
return { errors, warnings: [...urls, ...destructive] };
|
|
120
136
|
}
|
|
137
|
+
/** Limite suave: skills longas pioram a qualidade do agente ("dumb zone"); acima disso, sugerimos dividir. */
|
|
138
|
+
export const SOFT_BODY_LIMIT = 3000;
|
|
139
|
+
/** Dicas de qualidade (não bloqueiam nem entram em `lintSkill`): tamanho e critério de término/verificação. */
|
|
140
|
+
export function adviseSkill(input) {
|
|
141
|
+
const tips = [];
|
|
142
|
+
if (input.body.length > SOFT_BODY_LIMIT)
|
|
143
|
+
tips.push(`body com ${input.body.length} chars (> ${SOFT_BODY_LIMIT}): considere dividir em skills menores`);
|
|
144
|
+
if (!/verifica|crit[ée]rio de (t[ée]rmino|sa[ií]da|pronto)|done when|pronto quando/i.test(input.body))
|
|
145
|
+
tips.push("sem seção '## Verificação' ou critério de término: diga como saber que acabou");
|
|
146
|
+
return tips;
|
|
147
|
+
}
|
|
121
148
|
export function validate(input) {
|
|
122
149
|
if (!NAME_RE.test(input.name))
|
|
123
150
|
throw new Error("name deve ser kebab-case (a-z, 0-9, hífen), 2–49 chars.");
|
|
@@ -164,6 +191,9 @@ export function saveSkill(root, input, scope, opts = {}) {
|
|
|
164
191
|
losses: opts.auto && prev?.status === "trial" ? 0 : prev?.losses ?? 0,
|
|
165
192
|
body: input.body.trim(),
|
|
166
193
|
scope: target,
|
|
194
|
+
invoke: input.invoke ?? prev?.invoke ?? "auto",
|
|
195
|
+
model: input.model ?? prev?.model ?? "",
|
|
196
|
+
lastLoss: prev?.lastLoss ?? "",
|
|
167
197
|
};
|
|
168
198
|
writeSkill(root, skill);
|
|
169
199
|
return { created: !prev, version: skill.version, scope: target };
|
|
@@ -215,13 +245,13 @@ export function rejectPending(root, name) {
|
|
|
215
245
|
}
|
|
216
246
|
export function bumpUses(root, name) {
|
|
217
247
|
const s = getSkill(root, name);
|
|
218
|
-
if (s)
|
|
248
|
+
if (s && !s.builtin)
|
|
219
249
|
writeSkill(root, { ...s, uses: s.uses + 1, lastUsed: today() });
|
|
220
250
|
}
|
|
221
251
|
/** Move para `.trash` do próprio escopo em vez de apagar. */
|
|
222
252
|
export function deleteSkill(root, name, scope) {
|
|
223
253
|
const s = scope ? getSkillIn(root, name, scope) : getSkill(root, name);
|
|
224
|
-
if (!s)
|
|
254
|
+
if (!s || s.builtin)
|
|
225
255
|
return false;
|
|
226
256
|
const base = dirOf(root, s.scope);
|
|
227
257
|
fs.mkdirSync(path.join(base, ".trash"), { recursive: true });
|
|
@@ -239,8 +269,34 @@ export function promoteSkill(root, name) {
|
|
|
239
269
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
240
270
|
fs.renameSync(path.join(dirOf(root, "project"), name), dest);
|
|
241
271
|
}
|
|
242
|
-
/**
|
|
243
|
-
export
|
|
244
|
-
|
|
245
|
-
|
|
272
|
+
/** Quantas skills vão para o índice do system prompt; o resto é achado por `skill_load` (sugestões) ou `/skills suggest`. */
|
|
273
|
+
export const INDEX_MAX = 25;
|
|
274
|
+
/** Índice compacto para o system prompt (divulgação progressiva: o corpo só entra via skill_load). Skills `manual` ficam de fora. */
|
|
275
|
+
export function skillsIndex(root, max = INDEX_MAX, now = Date.now()) {
|
|
276
|
+
const eligible = listSkills(root, now).filter((s) => s.invoke !== "manual");
|
|
277
|
+
const all = eligible.slice(0, max);
|
|
278
|
+
if (!all.length)
|
|
279
|
+
return "";
|
|
280
|
+
const rest = eligible.length - all.length;
|
|
281
|
+
return all.map((s) => `- ${s.name}${s.scope === "global" ? " (global)" : ""}${s.status === "trial" ? " (em teste)" : ""}${staleRefs(root, s).length ? " [refs obsoletas]" : ""}: ${s.description}`).join("\n")
|
|
282
|
+
+ (rest > 0 ? `\n(+${rest} skills não listadas: chame skill_load com uma palavra da tarefa para receber sugestões)` : "");
|
|
283
|
+
}
|
|
284
|
+
const STOP = new Set(["para", "como", "com", "uma", "que", "the", "and", "uso", "usar", "skill", "fazer", "esta", "isso", "nos", "nas", "dos", "das", "por"]);
|
|
285
|
+
export const words = (t) => (t.toLowerCase().normalize("NFD").replace(/\p{M}/gu, "").match(/[a-z0-9]{3,}/g) ?? []).filter((w) => !STOP.has(w));
|
|
286
|
+
/** Skills (incluindo manuais e embutidas) ordenadas por afinidade lexical com o texto. Nome pesa mais que descrição. */
|
|
287
|
+
export function suggestSkills(root, text, limit = 5) {
|
|
288
|
+
const q = new Set(words(text));
|
|
289
|
+
if (!q.size)
|
|
290
|
+
return [];
|
|
291
|
+
return [...listSkills(root), ...listBuiltins(root)]
|
|
292
|
+
.map((skill) => {
|
|
293
|
+
const nm = new Set(words(skill.name)), ds = new Set(words(skill.description)), bd = new Set(words(skill.body));
|
|
294
|
+
let score = 0;
|
|
295
|
+
for (const w of q)
|
|
296
|
+
score += (nm.has(w) ? 3 : 0) + (ds.has(w) ? 2 : 0) + (bd.has(w) ? 0.5 : 0);
|
|
297
|
+
return { skill, score };
|
|
298
|
+
})
|
|
299
|
+
.filter((x) => x.score > 0)
|
|
300
|
+
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
|
|
301
|
+
.slice(0, limit);
|
|
246
302
|
}
|
package/dist/skills/trial.js
CHANGED
|
@@ -9,11 +9,11 @@ export const TRIAL_DAYS = 21;
|
|
|
9
9
|
/** Teto de skills novas criadas sozinhas por dia (contido: cada uma vira instrução para o agente). */
|
|
10
10
|
export const DAILY_LIMIT = 3;
|
|
11
11
|
/** Registra o resultado de um turno que usou a skill. Devolve um aviso se ela foi promovida ou arquivada. */
|
|
12
|
-
export function recordOutcome(root, name, win) {
|
|
12
|
+
export function recordOutcome(root, name, win, loss) {
|
|
13
13
|
const s = getSkill(root, name);
|
|
14
|
-
if (!s)
|
|
14
|
+
if (!s || s.builtin)
|
|
15
15
|
return null;
|
|
16
|
-
const next = { ...s, wins: s.wins + (win ? 1 : 0), losses: s.losses + (win ? 0 : 1) };
|
|
16
|
+
const next = { ...s, wins: s.wins + (win ? 1 : 0), losses: s.losses + (win ? 0 : 1), lastLoss: win ? s.lastLoss : (loss ?? s.lastLoss).replace(/\s+/g, " ").slice(0, 200) };
|
|
17
17
|
if (s.origin === "auto") {
|
|
18
18
|
if (s.status === "trial") {
|
|
19
19
|
if (next.wins >= PROMOTE_WINS && next.wins >= 3 * next.losses) {
|
package/dist/tools/delegate.js
CHANGED
|
@@ -11,7 +11,8 @@ let seq = 0;
|
|
|
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", "skill_save", "skill_delete", "exit_plan", "todo_write"].includes(n)));
|
|
15
16
|
}
|
|
16
17
|
/** Agente somente-leitura: nenhuma tool dele escreve arquivos ou exige aprovação. */
|
|
17
18
|
function isReadOnly(def) {
|
|
@@ -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
|
});
|
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,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineTool } from "./registry.js";
|
|
3
|
-
import { bumpUses, deleteSkill, getSkill, listPending,
|
|
3
|
+
import { bumpUses, deleteSkill, getSkill, listPending, saveSkill, suggestSkills, validate } from "../skills/store.js";
|
|
4
|
+
import { saveAuto } from "../skills/trial.js";
|
|
4
5
|
import { describeSave } from "../skills/describe.js";
|
|
5
6
|
import { perms } from "../permissions.js";
|
|
6
7
|
defineTool({
|
|
@@ -9,16 +10,18 @@ defineTool({
|
|
|
9
10
|
schema: z.object({ name: z.string() }),
|
|
10
11
|
async run({ name }, ctx) {
|
|
11
12
|
const s = getSkill(ctx.root, name);
|
|
12
|
-
if (!s)
|
|
13
|
-
|
|
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
|
+
}
|
|
14
17
|
bumpUses(ctx.root, name);
|
|
15
18
|
(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.)`;
|
|
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.)`;
|
|
17
20
|
},
|
|
18
21
|
});
|
|
19
22
|
defineTool({
|
|
20
23
|
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
|
|
24
|
+
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 entra em teste (se for segura) ou vira proposta pendente (comando destrutivo, ou mudança numa skill aprovada pelo usuário). body em markdown: ## Quando usar, ## Passos, ## Armadilhas, ## Verificação.",
|
|
22
25
|
schema: z.object({
|
|
23
26
|
name: z.string().describe("kebab-case"),
|
|
24
27
|
description: z.string().describe("uma frase: QUANDO usar (10–200 chars)"),
|
|
@@ -26,18 +29,17 @@ defineTool({
|
|
|
26
29
|
refs: z.array(z.string()).default([]).describe("arquivos/símbolos relevantes"),
|
|
27
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."),
|
|
28
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)"),
|
|
29
34
|
}),
|
|
30
35
|
dangerous: true,
|
|
31
36
|
describe: (i, ctx) => describeSave(ctx.root, i),
|
|
32
|
-
async run({ name, description, body, refs, scope, reason }, ctx) {
|
|
37
|
+
async run({ name, description, body, refs, scope, reason, invoke, model }, ctx) {
|
|
33
38
|
validate({ name, description, body });
|
|
34
|
-
// Em bypass nada pede aprovação
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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);
|
|
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).
|
|
40
|
+
if (perms.mode === "bypass")
|
|
41
|
+
return saveAuto(ctx.root, { name, description, body, refs, reason }).msg;
|
|
42
|
+
const r = saveSkill(ctx.root, { name, description, body, refs, invoke, model }, scope);
|
|
41
43
|
return r.created ? `Skill ${name} criada (${r.scope}, v1).` : `Skill ${name} atualizada (${r.scope}, v${r.version}); a anterior ficou em history/.`;
|
|
42
44
|
},
|
|
43
45
|
});
|
|
@@ -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,77 +1,80 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"brito": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"dev": "tsx src/cli.ts",
|
|
11
|
-
"build": "tsc",
|
|
12
|
-
"typecheck": "tsc --noEmit",
|
|
13
|
-
"test": "tsx --test test/*.test.ts",
|
|
14
|
-
"eval:learn": "tsx test/eval-learn.ts",
|
|
15
|
-
"prepare": "tsc",
|
|
16
|
-
"prepublishOnly": "npm run typecheck && npm test",
|
|
17
|
-
"desktop": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && npm --prefix desktop start",
|
|
18
|
-
"bundle": "node build-bundle.mjs",
|
|
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"
|
|
21
|
-
},
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@anthropic-ai/sdk": "^0.137.0",
|
|
24
|
-
"@ladybugdb/core": "0.21.2",
|
|
25
|
-
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
26
|
-
"commander": "^14.0.3",
|
|
27
|
-
"string-width": "^8.3.0",
|
|
28
|
-
"tinyglobby": "^0.2.17",
|
|
29
|
-
"tree-sitter-wasms": "^0.1.13",
|
|
30
|
-
"web-tree-sitter": "0.24.7",
|
|
31
|
-
"zod": "^4.6.5"
|
|
32
|
-
},
|
|
33
|
-
"devDependencies": {
|
|
34
|
-
"@types/node": "latest",
|
|
35
|
-
"esbuild": "^0.28.2",
|
|
36
|
-
"tsx": "latest",
|
|
37
|
-
"typescript": "latest"
|
|
38
|
-
},
|
|
39
|
-
"license": "PolyForm-Noncommercial-1.0.0",
|
|
40
|
-
"repository": {
|
|
41
|
-
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/wilsonbrito/brito.git"
|
|
43
|
-
},
|
|
44
|
-
"homepage": "https://github.com/wilsonbrito/brito#readme",
|
|
45
|
-
"bugs": {
|
|
46
|
-
"url": "https://github.com/wilsonbrito/brito/issues"
|
|
47
|
-
},
|
|
48
|
-
"author": "Wilson Brito",
|
|
49
|
-
"engines": {
|
|
50
|
-
"node": ">=20"
|
|
51
|
-
},
|
|
52
|
-
"files": [
|
|
53
|
-
"dist",
|
|
54
|
-
"!dist/cli.bundle.mjs",
|
|
55
|
-
".brito/agents",
|
|
56
|
-
"README.md",
|
|
57
|
-
"README.pt-BR.md",
|
|
58
|
-
"LICENSE"
|
|
59
|
-
],
|
|
60
|
-
"keywords": [
|
|
61
|
-
"cli",
|
|
62
|
-
"agent",
|
|
63
|
-
"coding-agent",
|
|
64
|
-
"llm",
|
|
65
|
-
"knowledge-graph",
|
|
66
|
-
"code-graph",
|
|
67
|
-
"mcp",
|
|
68
|
-
"claude",
|
|
69
|
-
"tree-sitter",
|
|
70
|
-
"svelte",
|
|
71
|
-
"typescript",
|
|
72
|
-
"python"
|
|
73
|
-
],
|
|
74
|
-
"overrides": {
|
|
75
|
-
"tar": "^7.5.22"
|
|
76
|
-
}
|
|
77
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "brito-cli",
|
|
3
|
+
"version": "0.8.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"brito": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/cli.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test": "tsx --test test/*.test.ts",
|
|
14
|
+
"eval:learn": "tsx test/eval-learn.ts",
|
|
15
|
+
"prepare": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run typecheck && npm test",
|
|
17
|
+
"desktop": "npm run build && npm run bundle && npm --prefix desktop install --no-audit --no-fund && npm --prefix desktop start",
|
|
18
|
+
"bundle": "node build-bundle.mjs",
|
|
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"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@anthropic-ai/sdk": "^0.137.0",
|
|
24
|
+
"@ladybugdb/core": "0.21.2",
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
|
+
"string-width": "^8.3.0",
|
|
28
|
+
"tinyglobby": "^0.2.17",
|
|
29
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
30
|
+
"web-tree-sitter": "0.24.7",
|
|
31
|
+
"zod": "^4.6.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "latest",
|
|
35
|
+
"esbuild": "^0.28.2",
|
|
36
|
+
"tsx": "latest",
|
|
37
|
+
"typescript": "latest"
|
|
38
|
+
},
|
|
39
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/wilsonbrito/brito.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/wilsonbrito/brito#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/wilsonbrito/brito/issues"
|
|
47
|
+
},
|
|
48
|
+
"author": "Wilson Brito",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"!dist/cli.bundle.mjs",
|
|
55
|
+
".brito/agents",
|
|
56
|
+
"README.md",
|
|
57
|
+
"README.pt-BR.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"keywords": [
|
|
61
|
+
"cli",
|
|
62
|
+
"agent",
|
|
63
|
+
"coding-agent",
|
|
64
|
+
"llm",
|
|
65
|
+
"knowledge-graph",
|
|
66
|
+
"code-graph",
|
|
67
|
+
"mcp",
|
|
68
|
+
"claude",
|
|
69
|
+
"tree-sitter",
|
|
70
|
+
"svelte",
|
|
71
|
+
"typescript",
|
|
72
|
+
"python"
|
|
73
|
+
],
|
|
74
|
+
"overrides": {
|
|
75
|
+
"tar": "^7.5.22"
|
|
76
|
+
},
|
|
77
|
+
"optionalDependencies": {
|
|
78
|
+
"node-llama-cpp": "^3.22.3"
|
|
79
|
+
}
|
|
80
|
+
}
|