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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import "./tools/fs.js";
|
|
6
|
+
import "./tools/bash.js";
|
|
7
|
+
import "./tools/graph.js";
|
|
8
|
+
import "./tools/delegate.js";
|
|
9
|
+
import "./tools/plan.js";
|
|
10
|
+
import net from "node:net";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { PLAN_NOTE, perms, describeClaudeTool } from "./permissions.js";
|
|
14
|
+
import { loadProjectAgents, listAgents } from "./agent/agents.js";
|
|
15
|
+
import { GraphStore } from "./graph/store.js";
|
|
16
|
+
import { indexProject } from "./graph/indexer.js";
|
|
17
|
+
import { Agent, MODEL } from "./agent/loop.js";
|
|
18
|
+
import { EFFORTS, MODEL_ALIASES, MODEL_HELP, effortLabel, initialEffort, isEffort, loadReasoningDisplay, parseModelArgs, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
|
|
19
|
+
import { hasClaudeCli, ClaudeSession } from "./agent/claude-cli.js";
|
|
20
|
+
import { serveMcp } from "./mcp/server.js";
|
|
21
|
+
import "./tools/skills.js";
|
|
22
|
+
import { approvePending, lintSkill, listPending, listSkills, rejectPending, skillsIndex, staleRefs } from "./skills/store.js";
|
|
23
|
+
import { applyProposal, describeProposal, proposeConsolidation, pruneCandidates } from "./skills/consolidate.js";
|
|
24
|
+
import { defaultComplete, reflect, shouldReflect } from "./skills/learn.js";
|
|
25
|
+
import { claudeCliComplete } from "./skills/learn-cli.js";
|
|
26
|
+
import { buildSystemPrompt, graphStatsLine } from "./agent/prompt.js";
|
|
27
|
+
import { allTools } from "./tools/registry.js";
|
|
28
|
+
import { ui, c, fmtTokens } from "./ui/ui.js";
|
|
29
|
+
import { runRepl } from "./repl.js";
|
|
30
|
+
const program = new Command().name("brito").description("CLI de agentes de desenvolvimento com grafo de conhecimento").enablePositionalOptions();
|
|
31
|
+
const rootOf = (o) => path.resolve(o.dir ?? process.cwd());
|
|
32
|
+
program
|
|
33
|
+
.command("index")
|
|
34
|
+
.description("Indexa o código do projeto no grafo")
|
|
35
|
+
.option("-d, --dir <dir>", "raiz do projeto")
|
|
36
|
+
.option("--full", "reconstrói o grafo do zero")
|
|
37
|
+
.action(async (o) => {
|
|
38
|
+
const root = rootOf(o);
|
|
39
|
+
const g = await GraphStore.open(root);
|
|
40
|
+
const t = Date.now();
|
|
41
|
+
const s = await indexProject(root, g, { full: o.full, onProgress: (m) => console.log(m) });
|
|
42
|
+
console.log(`OK: ${s.files} arquivos, ${s.symbols} símbolos, ${s.calls} chamadas, ${s.imports} imports | +${s.added} ~${s.changed} -${s.removed} =${s.unchanged} (${Date.now() - t}ms)`);
|
|
43
|
+
await g.close();
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.command("query <cypher>")
|
|
47
|
+
.description("Executa Cypher no grafo")
|
|
48
|
+
.option("-d, --dir <dir>")
|
|
49
|
+
.action(async (cypher, o) => {
|
|
50
|
+
const g = await GraphStore.open(rootOf(o));
|
|
51
|
+
console.log(JSON.stringify(await g.run(cypher), null, 2));
|
|
52
|
+
await g.close();
|
|
53
|
+
});
|
|
54
|
+
const CTX_WINDOW = Number(process.env.BRITO_CONTEXT_WINDOW ?? 200_000);
|
|
55
|
+
// Custo só aparece se você informar os preços (USD por milhão de tokens); não chuto valores.
|
|
56
|
+
const PRICE_IN = Number(process.env.BRITO_PRICE_IN ?? NaN);
|
|
57
|
+
const PRICE_OUT = Number(process.env.BRITO_PRICE_OUT ?? NaN);
|
|
58
|
+
program
|
|
59
|
+
.command("mcp-serve", { hidden: true })
|
|
60
|
+
.description("Servidor MCP (stdio) com grafo, memória e skills; usado pelo claude CLI")
|
|
61
|
+
.option("-d, --dir <dir>")
|
|
62
|
+
.action(async (o) => { await serveMcp(rootOf(o)); });
|
|
63
|
+
const skillInfo = (root) => listSkills(root).map((s) => ({ name: s.name, scope: s.scope }));
|
|
64
|
+
/** Pergunta de permissão sem REPL (prompt único). */
|
|
65
|
+
async function plainAsk(q) {
|
|
66
|
+
if (!process.stdin.isTTY)
|
|
67
|
+
return false;
|
|
68
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
const [first, ...rest] = q.split("\n");
|
|
70
|
+
console.log(c.warn("⚠ ") + c.bold(c.soft(first)) + (rest.length ? "\n" + c.muted(rest.join("\n")) : ""));
|
|
71
|
+
const a = await new Promise((r) => rl.question(c.warn(" permitir? ") + c.muted("[s]im/[n]ão: "), r));
|
|
72
|
+
rl.close();
|
|
73
|
+
return /^(s|y)/i.test(a.trim());
|
|
74
|
+
}
|
|
75
|
+
/** Sem API key: o `claude` oficial (login da assinatura) faz o modelo; grafo/memória/skills chegam via MCP. */
|
|
76
|
+
/**
|
|
77
|
+
* Aprovações do claude CLI: o claude chama a tool MCP `approve` (--permission-prompt-tool), que roda no
|
|
78
|
+
* processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal.
|
|
79
|
+
*/
|
|
80
|
+
function startApprovalServer(io) {
|
|
81
|
+
const sock = path.join(os.tmpdir(), `brito-approve-${process.pid}.sock`);
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(sock);
|
|
84
|
+
}
|
|
85
|
+
catch { /* não existia */ }
|
|
86
|
+
const server = net.createServer((conn) => {
|
|
87
|
+
let buf = "";
|
|
88
|
+
conn.on("data", async (d) => {
|
|
89
|
+
buf += d.toString();
|
|
90
|
+
const nl = buf.indexOf("\n");
|
|
91
|
+
if (nl < 0)
|
|
92
|
+
return;
|
|
93
|
+
let reply;
|
|
94
|
+
try {
|
|
95
|
+
const req = JSON.parse(buf.slice(0, nl));
|
|
96
|
+
const plan = req.tool_name === "ExitPlanMode";
|
|
97
|
+
const ok = await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
|
|
98
|
+
if (ok && plan)
|
|
99
|
+
perms.mode = "manual";
|
|
100
|
+
reply = ok ? { allow: true } : { allow: false, message: plan ? "Plano não aprovado pelo usuário. Pergunte o que ajustar." : "O usuário negou a execução." };
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
reply = { allow: false, message: `erro no pedido de aprovação: ${e?.message ?? e}` };
|
|
104
|
+
}
|
|
105
|
+
conn.end(JSON.stringify(reply) + "\n");
|
|
106
|
+
});
|
|
107
|
+
conn.on("error", () => { });
|
|
108
|
+
});
|
|
109
|
+
server.listen(sock);
|
|
110
|
+
server.unref();
|
|
111
|
+
const close = () => { server.close(); try {
|
|
112
|
+
fs.unlinkSync(sock);
|
|
113
|
+
}
|
|
114
|
+
catch { /* já removido */ } };
|
|
115
|
+
process.on("exit", close);
|
|
116
|
+
return { path: sock, close };
|
|
117
|
+
}
|
|
118
|
+
/** Menus ↑↓+Enter do `/model`: modelo e depois escopo. null = cancelado. */
|
|
119
|
+
async function pickModel(current) {
|
|
120
|
+
const models = Object.entries(MODEL_ALIASES);
|
|
121
|
+
const mi = await ui.select("modelo", models.map(([alias, id]) => ({ label: alias, hint: id === current ? `${id} (atual)` : id })), Math.max(0, models.findIndex(([, id]) => id === current)));
|
|
122
|
+
if (mi === null)
|
|
123
|
+
return null;
|
|
124
|
+
const scopes = [
|
|
125
|
+
{ scope: "session", label: "esta sessão", hint: "vale até fechar o Brito" },
|
|
126
|
+
{ scope: "global", label: "sempre", hint: "grava em ~/.brito/config.json" },
|
|
127
|
+
{ scope: "once", label: "só o próximo turno", hint: "depois volta ao modelo atual" },
|
|
128
|
+
];
|
|
129
|
+
const si = await ui.select(`escopo para ${models[mi][0]}`, scopes);
|
|
130
|
+
return si === null ? null : { model: models[mi][1], scope: scopes[si].scope };
|
|
131
|
+
}
|
|
132
|
+
/** Aplica o nível (vazio = padrão do claude) na sessão e, com `--global`, no config. */
|
|
133
|
+
function applyEffort(level, global, set) {
|
|
134
|
+
set(level);
|
|
135
|
+
ui.print(` esforço → ${c.soft(effortLabel(level))}`);
|
|
136
|
+
if (global) {
|
|
137
|
+
saveEffort(level ? level : undefined);
|
|
138
|
+
ui.print(c.muted(level ? " salvo como padrão (~/.brito/config.json)" : " esforço salvo removido (~/.brito/config.json)"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** `/effort [nível]` (só claude CLI): sem argumento abre o menu ↑↓+Enter; `--global` grava no config. */
|
|
142
|
+
function effortCommand(get, set) {
|
|
143
|
+
return {
|
|
144
|
+
desc: "nível de esforço do modelo (low gasta menos token)",
|
|
145
|
+
run: async (args) => {
|
|
146
|
+
let global = args.includes("--global");
|
|
147
|
+
let level = args.find((a) => !a.startsWith("--"))?.toLowerCase();
|
|
148
|
+
if (!level) {
|
|
149
|
+
if (!ui.canSelect) {
|
|
150
|
+
ui.print(` ${c.soft(effortLabel(get()))} ${c.muted(`uso: /effort ${EFFORTS.join("|")} [--global]`)}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const hint = { none: "sem raciocínio", minimal: "envia low", low: "mais barato", max: "mais caro", ultra: "envia max" };
|
|
154
|
+
const opts = [{ label: "padrão", hint: "o do claude" }, ...EFFORTS.map((e) => ({ label: e, hint: hint[e] ?? "" }))];
|
|
155
|
+
const i = await ui.select("esforço", opts, get() ? EFFORTS.indexOf(get()) + 1 : 0);
|
|
156
|
+
if (i === null) {
|
|
157
|
+
ui.info("cancelado");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
level = i === 0 ? "" : EFFORTS[i - 1];
|
|
161
|
+
if (level && ui.canSelect) {
|
|
162
|
+
const g = await ui.select(`escopo para ${level}`, [{ label: "esta sessão" }, { label: "sempre", hint: "grava em ~/.brito/config.json" }]);
|
|
163
|
+
if (g === null) {
|
|
164
|
+
ui.info("cancelado");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (g === 1)
|
|
168
|
+
global = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (level === "padrao" || level === "padrão")
|
|
172
|
+
level = "";
|
|
173
|
+
if (level && !isEffort(level)) {
|
|
174
|
+
ui.warn(`nível inválido: ${level} (${EFFORTS.join(", ")})`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
applyEffort(level, global, set);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** `/reasoning show|hide|full|clamp`: exibição do raciocínio (sempre grava, como no Hermes). */
|
|
182
|
+
const REASONING_TOGGLES = {
|
|
183
|
+
...Object.fromEntries(["show", "on"].map((k) => [k, ["show", true, "raciocínio visível"]])),
|
|
184
|
+
...Object.fromEntries(["hide", "off"].map((k) => [k, ["show", false, "raciocínio oculto"]])),
|
|
185
|
+
...Object.fromEntries(["full", "all"].map((k) => [k, ["full", true, "raciocínio completo (sem recorte)"]])),
|
|
186
|
+
...Object.fromEntries(["clamp", "collapse", "short"].map((k) => [k, ["full", false, "raciocínio recortado em 10 linhas"]])),
|
|
187
|
+
};
|
|
188
|
+
/** `/reasoning [nível|show|hide|full|clamp] [--global]`: mesma mecânica do Hermes (nível só na sessão, salvo com --global). */
|
|
189
|
+
function reasoningCommand(get, set) {
|
|
190
|
+
return {
|
|
191
|
+
desc: "esforço de raciocínio e exibição (nível|show|hide|full|clamp)",
|
|
192
|
+
run: async (args) => {
|
|
193
|
+
const global = args.includes("--global");
|
|
194
|
+
const arg = args.find((a) => !a.startsWith("--"))?.toLowerCase();
|
|
195
|
+
if (!arg) {
|
|
196
|
+
const { show, full } = ui.reasoning;
|
|
197
|
+
ui.print(` ${c.soft("esforço")} ${c.muted(effortLabel(get()))}`);
|
|
198
|
+
ui.print(` ${c.soft("exibição")} ${c.muted(`${show ? "visível" : "oculta"} · ${full ? "completo" : "recortado"}`)}`);
|
|
199
|
+
ui.print(c.muted(` uso: /reasoning ${EFFORTS.join("|")}|padrão [--global] · show|hide · full|clamp`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const toggle = REASONING_TOGGLES[arg];
|
|
203
|
+
if (toggle) {
|
|
204
|
+
const [key, value, msg] = toggle;
|
|
205
|
+
ui.reasoning[key] = value;
|
|
206
|
+
saveReasoningDisplay({ [key]: value });
|
|
207
|
+
ui.print(` ${c.soft(msg)} ${c.muted("(salvo)")}`);
|
|
208
|
+
if (key === "full" && value && !ui.reasoning.show)
|
|
209
|
+
ui.print(c.muted(" o raciocínio está oculto: /reasoning show para vê-lo"));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const level = ["padrao", "padrão", "default"].includes(arg) ? "" : ["false", "disabled"].includes(arg) ? "none" : arg;
|
|
213
|
+
if (level && !isEffort(level)) {
|
|
214
|
+
ui.warn(`argumento desconhecido: ${arg}`);
|
|
215
|
+
ui.print(c.muted(` níveis: ${EFFORTS.join(", ")} · exibição: show, hide, full, clamp · escopo: --global`));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
applyEffort(level, global, set);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* `/model [nome|alias] [--global|--session|--once]` (mesma semântica do Hermes). Vale para os dois backends:
|
|
224
|
+
* `get`/`set` leem e trocam o modelo vivo; `--once` devolve o anterior depois do turno (`restoreOnce`).
|
|
225
|
+
*/
|
|
226
|
+
function modelCommand(get, set, note) {
|
|
227
|
+
let restore = null;
|
|
228
|
+
return {
|
|
229
|
+
restoreOnce: () => { if (restore !== null) {
|
|
230
|
+
set(restore);
|
|
231
|
+
restore = null;
|
|
232
|
+
} },
|
|
233
|
+
cmd: {
|
|
234
|
+
desc: "mostra/troca o modelo (--global grava, --once só o próximo turno)",
|
|
235
|
+
run: async (args) => {
|
|
236
|
+
const req = parseModelArgs(args.join(" "));
|
|
237
|
+
if (req.errors.length) {
|
|
238
|
+
for (const e of req.errors)
|
|
239
|
+
ui.warn(e);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!req.model) {
|
|
243
|
+
if (!ui.canSelect) {
|
|
244
|
+
ui.print(` ${c.soft(get())} ${c.muted(note)}\n ${c.muted(MODEL_HELP)}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const picked = await pickModel(get());
|
|
248
|
+
if (!picked) {
|
|
249
|
+
ui.info("cancelado");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
req.model = picked.model;
|
|
253
|
+
req.scope = picked.scope;
|
|
254
|
+
}
|
|
255
|
+
const old = get();
|
|
256
|
+
if (req.scope === "once")
|
|
257
|
+
restore ??= old; // dois --once seguidos devolvem o modelo original, não o intermediário
|
|
258
|
+
set(req.model);
|
|
259
|
+
ui.print(` ${c.muted(old)} → ${c.soft(req.model)}`);
|
|
260
|
+
if (req.scope === "global") {
|
|
261
|
+
saveModel(req.model);
|
|
262
|
+
ui.print(c.muted(" salvo como padrão (~/.brito/config.json)"));
|
|
263
|
+
}
|
|
264
|
+
else if (req.scope === "once")
|
|
265
|
+
ui.print(c.muted(" só no próximo turno"));
|
|
266
|
+
else {
|
|
267
|
+
restore = null;
|
|
268
|
+
ui.print(c.muted(" só nesta sessão"));
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** `/skills`: lista, revisa propostas pendentes e consolida a biblioteca. Vale para os dois backends. */
|
|
275
|
+
const skillsCommand = (root, io, llm) => ({
|
|
276
|
+
desc: "lista skills; pending | approve <nome> | reject <nome> | consolidate",
|
|
277
|
+
run: async ([sub, name]) => {
|
|
278
|
+
if (sub === "consolidate") {
|
|
279
|
+
ui.info("✦ analisando a biblioteca de skills…");
|
|
280
|
+
ui.turnStart();
|
|
281
|
+
let proposals;
|
|
282
|
+
try {
|
|
283
|
+
proposals = await proposeConsolidation(root, llm.model, llm.complete);
|
|
284
|
+
ui.turnEnd(false);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
ui.turnEnd(false);
|
|
288
|
+
ui.error(`consolidação falhou: ${e?.message ?? e}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!proposals.length) {
|
|
292
|
+
ui.info("✦ biblioteca saudável, nada a consolidar");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
for (const p of proposals) {
|
|
296
|
+
// force: mesmo em bypass, cada merge/remoção exige um sim explícito.
|
|
297
|
+
if (!(await io.ask(describeProposal(root, p), { force: true }))) {
|
|
298
|
+
ui.info("ignorada");
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
ui.print(c.ok("✦ ") + applyProposal(root, p));
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
ui.error(e?.message ?? String(e));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (sub === "approve" || sub === "reject") {
|
|
311
|
+
if (!name) {
|
|
312
|
+
ui.warn(`uso: /skills ${sub} <nome>`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
if (sub === "approve") {
|
|
317
|
+
const p = listPending(root).find((x) => x.name === name);
|
|
318
|
+
if (!p) {
|
|
319
|
+
ui.warn(`sem proposta pendente: ${name}`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
ui.print(` ${c.soft(p.name)} ${c.muted(`(${p.scope}) ${p.description}`)}\n${p.body.split("\n").map((l) => ` │ ${l}`).join("\n")}`);
|
|
323
|
+
// Avisos do lint (ex.: comando destrutivo) ficam à vista de quem aprova, não só de quem propõe.
|
|
324
|
+
for (const w of lintSkill(p).warnings)
|
|
325
|
+
ui.print(c.warn(` ⚠ ${w}`));
|
|
326
|
+
if (!(await io.ask(`aprovar a skill ${name}?`, { force: true }))) {
|
|
327
|
+
ui.info("mantida como pendente");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const r = approvePending(root, name);
|
|
331
|
+
ui.print(c.ok("✦ ") + `skill ${name} ativa (${r.scope}, v${r.version})`);
|
|
332
|
+
}
|
|
333
|
+
else
|
|
334
|
+
ui.print(rejectPending(root, name) ? c.ok("✦ ") + `proposta ${name} descartada` : c.muted(`sem proposta pendente: ${name}`));
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
ui.error(e?.message ?? String(e));
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const pending = listPending(root);
|
|
342
|
+
if (sub === "pending") {
|
|
343
|
+
if (!pending.length) {
|
|
344
|
+
ui.info("nenhuma proposta pendente");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
for (const p of pending) {
|
|
348
|
+
const avisos = lintSkill(p).warnings.filter((w) => w.startsWith("comando destrutivo")).length;
|
|
349
|
+
ui.print(` ${c.soft(p.name)} ${c.muted(`(${p.scope}) ${p.description}`)}${avisos ? " " + c.warn(`⚠ ${avisos} comando(s) destrutivo(s)`) : ""}`);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const skills = listSkills(root);
|
|
354
|
+
for (const s of skills) {
|
|
355
|
+
const stale = staleRefs(root, s);
|
|
356
|
+
ui.print(` ${c.soft(s.name)} ${c.muted(`(${s.scope}) v${s.version} · ${s.uses} usos · último uso ${s.lastUsed} · ${s.description}`)}${stale.length ? " " + c.warn(`⚠ refs obsoletas: ${stale.join(", ")}`) : ""}`);
|
|
357
|
+
}
|
|
358
|
+
const prune = pruneCandidates(skills);
|
|
359
|
+
if (prune.length)
|
|
360
|
+
ui.info(`${prune.length} candidata(s) a poda (sem uso há mais de 30 dias): /skills consolidate`);
|
|
361
|
+
if (pending.length)
|
|
362
|
+
ui.info(`${pending.length} proposta(s) pendente(s): /skills pending`);
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
/** Reflexão pós-turno que propõe skills (gatilho automático ou `/learn`). Independe do backend: só muda a origem do turno e o modelo. */
|
|
366
|
+
function makeLearner(getTurns, ctx, complete, model) {
|
|
367
|
+
return async (force = false) => {
|
|
368
|
+
const turns = getTurns();
|
|
369
|
+
const turn = turns[turns.length - 1] ?? null;
|
|
370
|
+
if (!force && !shouldReflect(turn, ctx)) {
|
|
371
|
+
ctx.loadedSkills?.clear();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!turn) {
|
|
375
|
+
ui.info("Nada para refletir ainda.");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
ui.info("✦ refletindo sobre a tarefa para extrair skills…");
|
|
379
|
+
ui.turnStart();
|
|
380
|
+
try {
|
|
381
|
+
const notes = await reflect(turn, ctx, model, complete, turns[turns.length - 2]);
|
|
382
|
+
ui.turnEnd(false);
|
|
383
|
+
for (const n of notes)
|
|
384
|
+
ui.print(c.ok("✦ ") + n);
|
|
385
|
+
if (!notes.length)
|
|
386
|
+
ui.info("✦ nenhuma skill nova");
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
ui.turnEnd(false);
|
|
390
|
+
ui.error(`reflexão falhou: ${e?.message ?? e}`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
async function runViaClaude(root, promptParts, yes) {
|
|
395
|
+
const startedAt = Date.now();
|
|
396
|
+
if (yes)
|
|
397
|
+
perms.mode = "bypass";
|
|
398
|
+
// O REPL troca `io.ask`; em prompt único cai na pergunta simples.
|
|
399
|
+
const io = { ask: async (q, _opts) => perms.mode === "bypass" || plainAsk(q) };
|
|
400
|
+
const approval = startApprovalServer({ ask: (q, opts) => io.ask(q, opts) });
|
|
401
|
+
// O kuzu aceita um único processo com acesso de escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
402
|
+
const graph = await GraphStore.open(root);
|
|
403
|
+
ui.info("sincronizando grafo…");
|
|
404
|
+
await indexProject(root, graph);
|
|
405
|
+
const [{ n: syms }] = await graph.run("MATCH (s:Symbol) RETURN count(s) AS n");
|
|
406
|
+
const [{ n: notes }] = await graph.run("MATCH (s:Note) RETURN count(s) AS n");
|
|
407
|
+
await graph.close();
|
|
408
|
+
const session = new ClaudeSession({
|
|
409
|
+
// Foto da abertura: o índice é atualizado pelo servidor MCP (outro processo) e este processo não reabre o banco.
|
|
410
|
+
root, approvalSock: approval.path, stats: graphStatsLine(Number(syms), Number(notes), "start"),
|
|
411
|
+
mcpCommand: { command: process.execPath, args: [...process.execArgv, process.argv[1], "mcp-serve", "-d", root] },
|
|
412
|
+
});
|
|
413
|
+
loadProjectAgents(root);
|
|
414
|
+
// O grafo já foi solto para o servidor MCP; skill_save só usa o store de skills, então `graph` não é tocado.
|
|
415
|
+
const learnCtx = { root, graph: undefined, ask: (q, opts) => io.ask(q, opts), sync: async () => { }, dirty: false, loadedSkills: session.loadedSkills };
|
|
416
|
+
const learn = makeLearner(() => session.recentTurns, learnCtx, claudeCliComplete, process.env.BRITO_LEARN_MODEL ?? ""); // "" = modelo padrão do claude
|
|
417
|
+
if (promptParts.length) {
|
|
418
|
+
const ok = await session.send(promptParts.join(" "));
|
|
419
|
+
if (ok)
|
|
420
|
+
await learn();
|
|
421
|
+
else
|
|
422
|
+
process.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const modelCmd = modelCommand(() => session.requestedModel || session.model, (m) => { session.requestedModel = m; session.model = m; }, "(assinatura do claude CLI)");
|
|
426
|
+
session.effort = initialEffort();
|
|
427
|
+
ui.reasoning = loadReasoningDisplay();
|
|
428
|
+
const unavailable = { desc: "indisponível no modo claude CLI", run: async () => { ui.warn("não está disponível no modo claude CLI"); } };
|
|
429
|
+
const mcpTools = allTools().map((t) => t.name).filter((n) => /^(graph_|skill_)/.test(n) || n === "remember" || n === "recall");
|
|
430
|
+
const backend = {
|
|
431
|
+
model: () => session.model,
|
|
432
|
+
contextTokens: () => session.contextTokens,
|
|
433
|
+
usage: () => session.usage,
|
|
434
|
+
cost: () => (session.costUsd > 0 ? session.costUsd : undefined),
|
|
435
|
+
get ctxWindow() { return session.contextWindow ?? CTX_WINDOW; },
|
|
436
|
+
turn: (text) => session.send(text),
|
|
437
|
+
afterTurn: async () => { modelCmd.restoreOnce(); await learn(); },
|
|
438
|
+
abort: () => session.abort(),
|
|
439
|
+
reset: () => session.reset(),
|
|
440
|
+
commands: {
|
|
441
|
+
"/model": modelCmd.cmd,
|
|
442
|
+
"/effort": effortCommand(() => session.effort, (e) => { session.effort = e; }),
|
|
443
|
+
"/reasoning": reasoningCommand(() => session.effort, (e) => { session.effort = e; }),
|
|
444
|
+
"/tools": { desc: "ferramentas do grafo/memória/skills", run: async () => { for (const n of mcpTools)
|
|
445
|
+
ui.print(` ${ui.toolLabel(n)}`); } },
|
|
446
|
+
"/index": {
|
|
447
|
+
desc: "reconstrói o grafo do zero",
|
|
448
|
+
run: async () => {
|
|
449
|
+
ui.info("reconstruindo grafo…");
|
|
450
|
+
const g = await GraphStore.open(root);
|
|
451
|
+
ui.print(c.muted(JSON.stringify(await indexProject(root, g, { full: true }))));
|
|
452
|
+
await g.close();
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
"/context": unavailable,
|
|
456
|
+
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
457
|
+
"/skills": skillsCommand(root, io, { complete: claudeCliComplete, model: process.env.BRITO_LEARN_MODEL ?? "" }),
|
|
458
|
+
},
|
|
459
|
+
close: async () => { },
|
|
460
|
+
};
|
|
461
|
+
ui.info("modo claude CLI: usa o login da sua conta; tools do grafo/memória/skills via MCP.");
|
|
462
|
+
await runRepl(backend, {
|
|
463
|
+
root, yes, startedAt, io,
|
|
464
|
+
banner: {
|
|
465
|
+
dir: root, symbols: Number(syms), notes: Number(notes),
|
|
466
|
+
tools: ["read_file", "write_file", "edit_file", "glob", "grep", "bash", "delegate", ...mcpTools],
|
|
467
|
+
skills: skillInfo(root), agents: listAgents().map((a) => a.name),
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
program
|
|
472
|
+
.argument("[prompt...]", "prompt único (omita para modo interativo)")
|
|
473
|
+
.option("-d, --dir <dir>", "raiz do projeto")
|
|
474
|
+
.option("-y, --yes", "aprovar edições e comandos automaticamente")
|
|
475
|
+
.action(async (promptParts, o) => {
|
|
476
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
477
|
+
if (!hasClaudeCli()) {
|
|
478
|
+
console.error(c.err("Defina ANTHROPIC_API_KEY ou instale o Claude Code e rode `claude` → /login."));
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
await runViaClaude(rootOf(o), promptParts, !!o.yes);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const root = rootOf(o);
|
|
485
|
+
const startedAt = Date.now();
|
|
486
|
+
const graph = await GraphStore.open(root);
|
|
487
|
+
// A contagem do prompt acompanha cada sincronização: sem isso o modelo veria "0 símbolos" a sessão inteira
|
|
488
|
+
// em projeto que começou vazio e nasceu código depois.
|
|
489
|
+
let symCount = 0, noteCount = 0;
|
|
490
|
+
const sync = async () => { symCount = (await indexProject(root, graph)).symbols; };
|
|
491
|
+
ui.info("sincronizando grafo…");
|
|
492
|
+
await sync(); // incremental: barato quando nada mudou
|
|
493
|
+
const [{ n: notes }] = await graph.run("MATCH (s:Note) RETURN count(s) AS n");
|
|
494
|
+
noteCount = Number(notes);
|
|
495
|
+
// `io.ask` é substituída pelo REPL (dock); em prompt único cai na pergunta simples.
|
|
496
|
+
if (o.yes)
|
|
497
|
+
perms.mode = "bypass";
|
|
498
|
+
const io = { ask: async (q, _opts) => perms.mode === "bypass" || plainAsk(q) };
|
|
499
|
+
const ctx = { root, graph, ask: (q, opts) => io.ask(q, opts), sync, dirty: false, loadedSkills: new Set() };
|
|
500
|
+
// Função: o índice de skills e a contagem do grafo são relidos a cada passo e refletem o que mudou na sessão.
|
|
501
|
+
const agent = new Agent(() => buildSystemPrompt(root, graphStatsLine(symCount, noteCount, "now"), skillsIndex(root)) + (perms.mode === "plan" ? PLAN_NOTE : ""), ctx);
|
|
502
|
+
loadProjectAgents(root);
|
|
503
|
+
/** Depois de tarefas complexas (ou uso de skill), propõe criar/melhorar skills. */
|
|
504
|
+
const learn = makeLearner(() => agent.recentTurns, ctx, defaultComplete, process.env.BRITO_LEARN_MODEL ?? MODEL);
|
|
505
|
+
const turn = async (text, signal) => {
|
|
506
|
+
ui.turnStart();
|
|
507
|
+
let ok = true;
|
|
508
|
+
try {
|
|
509
|
+
await agent.send(text, signal);
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
ok = false;
|
|
513
|
+
signal.aborted ? ui.warn("⏹ interrompido") : ui.error(e?.message ?? String(e));
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
ui.turnEnd(ok);
|
|
517
|
+
}
|
|
518
|
+
return ok;
|
|
519
|
+
};
|
|
520
|
+
if (promptParts.length) {
|
|
521
|
+
if (await turn(promptParts.join(" "), new AbortController().signal))
|
|
522
|
+
await learn();
|
|
523
|
+
await graph.close();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const modelCmd = modelCommand(() => agent.model, (m) => { agent.model = m; }, "(troque com /model; subagentes explore usam BRITO_EXPLORE_MODEL)");
|
|
527
|
+
const backend = {
|
|
528
|
+
model: () => agent.model,
|
|
529
|
+
contextTokens: () => agent.contextTokens,
|
|
530
|
+
usage: () => agent.usage,
|
|
531
|
+
ctxWindow: CTX_WINDOW,
|
|
532
|
+
cost: () => (Number.isFinite(PRICE_IN) && Number.isFinite(PRICE_OUT) ? (agent.usage.input * PRICE_IN + agent.usage.output * PRICE_OUT) / 1e6 : undefined),
|
|
533
|
+
turn,
|
|
534
|
+
afterTurn: async () => { modelCmd.restoreOnce(); await learn(); },
|
|
535
|
+
reset: () => { agent.messages = []; agent.contextTokens = 0; agent.recentTurns = []; agent.lastTurn = null; },
|
|
536
|
+
onShell: () => { ctx.dirty = true; }, // `!cmd` pode ter mudado o código
|
|
537
|
+
commands: {
|
|
538
|
+
"/model": modelCmd.cmd,
|
|
539
|
+
"/tools": { desc: "ferramentas disponíveis", run: async () => { for (const t of allTools())
|
|
540
|
+
ui.print(` ${ui.toolLabel(t.name)}${c.dim(t.dangerous ? " (pede aprovação)" : "")}`); } },
|
|
541
|
+
"/context": {
|
|
542
|
+
desc: "uso da janela de contexto",
|
|
543
|
+
run: async () => {
|
|
544
|
+
const pct = Math.round((agent.contextTokens / CTX_WINDOW) * 100);
|
|
545
|
+
ui.print(` ${c.soft(`${fmtTokens(agent.contextTokens)} / ${fmtTokens(CTX_WINDOW)}`)} ${c.muted(`(${pct}%) · ${agent.messages.length} mensagens · compacta em ~${fmtTokens(Number(process.env.BRITO_COMPACT_TOKENS ?? 120_000))}`)}`);
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
"/index": { desc: "reconstrói o grafo do zero", run: async () => { ui.info("reconstruindo grafo…"); ui.print(c.muted(JSON.stringify(await indexProject(root, graph, { full: true })))); } },
|
|
549
|
+
"/learn": { desc: "extrai skills do último turno agora", run: async () => { await learn(true); } },
|
|
550
|
+
"/skills": skillsCommand(root, io, { complete: defaultComplete, model: process.env.BRITO_LEARN_MODEL ?? MODEL }),
|
|
551
|
+
},
|
|
552
|
+
close: async () => { await graph.close(); },
|
|
553
|
+
};
|
|
554
|
+
await runRepl(backend, {
|
|
555
|
+
root, yes: !!o.yes, startedAt, io,
|
|
556
|
+
banner: {
|
|
557
|
+
dir: root, symbols: symCount, notes: noteCount, tools: allTools().map((t) => t.name),
|
|
558
|
+
skills: skillInfo(root), agents: listAgents().map((a) => a.name),
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
program.parseAsync();
|