brito-cli 0.5.1 → 0.6.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 +4 -2
- package/README.pt-BR.md +4 -2
- package/dist/cli.js +29 -1
- package/dist/desktop/serve.js +1 -1
- package/dist/graph/indexer.js +56 -0
- package/dist/graph/migrate.js +82 -0
- package/dist/graph/store.js +69 -13
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ Brito is an agent for software development. Use it in the terminal (`brito-cli`
|
|
|
12
12
|
|
|
13
13
|
## Highlights
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
> **Upgrading to 0.6:** the graph engine changed from Kùzu (discontinued) to LadybugDB. The graph is rebuilt automatically on first use; your old `.brito/graph.kuzu` is kept. Brito asks once whether to copy your notes (memory) to the new graph, or run `brito migrar-notas` any time. Validated on macOS; Linux/Windows not tested by hand. See the [CHANGELOG](CHANGELOG.md).
|
|
16
|
+
|
|
17
|
+
- **Graph-first exploration.** `graph_search`, `graph_context`, `graph_callers`, `graph_callees` and `graph_impact` answer "where is it, who uses it, what breaks" without reading whole files. The graph is embedded ([LadybugDB](https://github.com/LadybugDB/ladybug), fork mantido do Kùzu), local and incremental.
|
|
16
18
|
- **Wide coverage.** TypeScript/JavaScript, Python and the script blocks of `.svelte`, `.vue` and `.astro` components. Import aliases (`tsconfig` paths, `package.json` imports, SvelteKit's `$lib`) become real edges. Files ignored by `.gitignore` stay out.
|
|
17
19
|
- **Skills that learn on their own, on probation.** After a task that tripped or was corrected, Brito reflects and creates a skill *in trial* without interrupting you. A skill proves itself by use: 3 clean uses promote it, 2 stumbles archive it (recoverable). Anything risky, or any change to a skill you approved, still waits for your `/skills approve`. `BRITO_SKILLS=ask` restores approval-for-everything; `off` disables learning.
|
|
18
20
|
- **Safety rails for what gets learned.** A lint rejects secrets, credentials embedded in URLs, `curl | sh` and prompt-injection phrasing, and **warns about destructive commands** (see below).
|
|
@@ -187,7 +189,7 @@ npm test # 190+ tests, including a real MCP server and a real REPL
|
|
|
187
189
|
|
|
188
190
|
| Path | What |
|
|
189
191
|
|---|---|
|
|
190
|
-
| `src/graph/` | indexer (tree-sitter), `.svelte/.vue/.astro` support, import aliases, Kùzu store |
|
|
192
|
+
| `src/graph/` | indexer (tree-sitter), `.svelte/.vue/.astro` support, import aliases, Ladybug (Kùzu) store |
|
|
191
193
|
| `src/skills/` | store, lint, reflection, journal, consolidation |
|
|
192
194
|
| `src/agent/` | agent loop, prompts, subagents, `claude` CLI backend |
|
|
193
195
|
| `src/tools/` | graph, memory, skills, filesystem and shell tools |
|
package/README.pt-BR.md
CHANGED
|
@@ -12,7 +12,9 @@ O Brito é um agente para desenvolvimento de software. Use no terminal (`brito-c
|
|
|
12
12
|
|
|
13
13
|
## Destaques
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
> **Atualizando para a 0.6:** o motor do grafo mudou do Kùzu (descontinuado) para o LadybugDB. O grafo é reconstruído sozinho no primeiro uso; o seu `.brito/graph.kuzu` antigo é mantido. O Brito pergunta uma vez se copia as suas notas (memória) para o grafo novo, ou rode `brito migrar-notas` quando quiser. Validado no macOS; Linux/Windows não foram testados manualmente. Veja o [CHANGELOG](CHANGELOG.md).
|
|
16
|
+
|
|
17
|
+
- **Exploração guiada pelo grafo.** `graph_search`, `graph_context`, `graph_callers`, `graph_callees` e `graph_impact` respondem "onde está, quem usa, o que quebra" sem ler arquivos inteiros. O grafo é embutido ([LadybugDB](https://github.com/LadybugDB/ladybug), fork mantido do Kùzu), local e incremental.
|
|
16
18
|
- **Cobertura ampla.** TypeScript/JavaScript, Python e os blocos de script de componentes `.svelte`, `.vue` e `.astro`. Aliases de import (`paths` do tsconfig, `imports` do package.json, `$lib` do SvelteKit) viram arestas de verdade. O que o `.gitignore` ignora fica de fora.
|
|
17
19
|
- **Skills que aprendem sozinhas, em período de teste.** Depois de uma tarefa em que algo tropeçou ou foi corrigido, o Brito reflete e cria uma skill *em teste* sem te interromper. A skill se prova pelo uso: 3 usos limpos a promovem, 2 tropeços a arquivam (recuperável). O que é arriscado, ou qualquer mudança numa skill que você aprovou, continua esperando o seu `/skills approve`. `BRITO_SKILLS=ask` volta a pedir aprovação para tudo; `off` desliga o aprendizado.
|
|
18
20
|
- **Proteções sobre o que é aprendido.** Um lint rejeita segredos, credenciais embutidas em URL, `curl | sh` e frases de injeção de prompt, e **avisa sobre comandos destrutivos** (veja abaixo).
|
|
@@ -183,7 +185,7 @@ npm test # 190+ testes, incluindo um servidor MCP real e um REPL rea
|
|
|
183
185
|
|
|
184
186
|
| Caminho | O quê |
|
|
185
187
|
|---|---|
|
|
186
|
-
| `src/graph/` | indexador (tree-sitter), suporte a `.svelte/.vue/.astro`, aliases de import, store Kùzu |
|
|
188
|
+
| `src/graph/` | indexador (tree-sitter), suporte a `.svelte/.vue/.astro`, aliases de import, store Ladybug (Kùzu) |
|
|
187
189
|
| `src/skills/` | store, lint, reflexão, journal, consolidação |
|
|
188
190
|
| `src/agent/` | loop do agente, prompts, subagentes, backend da CLI `claude` |
|
|
189
191
|
| `src/tools/` | tools de grafo, memória, skills, arquivos e shell |
|
package/dist/cli.js
CHANGED
|
@@ -49,6 +49,18 @@ program
|
|
|
49
49
|
.option("-d, --dir <dir>")
|
|
50
50
|
.option("--resume <id>", "retoma uma conversa do claude")
|
|
51
51
|
.action(async (o) => { const { serveDesktop } = await import("./desktop/serve.js"); await serveDesktop(rootOf(o), o.resume); });
|
|
52
|
+
program
|
|
53
|
+
.command("migrar-notas")
|
|
54
|
+
.description("Copia as notas (memória) do grafo antigo do Kùzu para o novo grafo")
|
|
55
|
+
.option("-d, --dir <dir>", "raiz do projeto")
|
|
56
|
+
.action(async (o) => {
|
|
57
|
+
const { migrateNotes } = await import("./graph/migrate.js");
|
|
58
|
+
const r = await migrateNotes(rootOf(o), (m) => console.log(m));
|
|
59
|
+
if ("none" in r)
|
|
60
|
+
console.log(r.none);
|
|
61
|
+
else
|
|
62
|
+
console.log(`OK: ${r.found} nota(s) no banco antigo, ${r.added} copiada(s), ${r.skipped} já existiam. O .brito/graph.kuzu foi mantido; apague-o quando quiser.`);
|
|
63
|
+
});
|
|
52
64
|
program
|
|
53
65
|
.command("query <cypher>")
|
|
54
66
|
.description("Executa Cypher no grafo")
|
|
@@ -79,6 +91,14 @@ async function plainAsk(q) {
|
|
|
79
91
|
rl.close();
|
|
80
92
|
return /^(s|y)/i.test(a.trim());
|
|
81
93
|
}
|
|
94
|
+
/** Pergunta de sim/não (padrão: não) fora do REPL; só roda com terminal interativo. */
|
|
95
|
+
async function migrationAsk(q) {
|
|
96
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
97
|
+
console.log(c.warn("⚠ ") + c.bold(c.soft(q)));
|
|
98
|
+
const a = await new Promise((r) => rl.question(c.muted(" copiar? [s]im/[N]ão: "), r));
|
|
99
|
+
rl.close();
|
|
100
|
+
return /^(s|y)/i.test(a.trim());
|
|
101
|
+
}
|
|
82
102
|
/** Sem API key: o `claude` oficial (login da assinatura) faz o modelo; grafo/memória/skills chegam via MCP. */
|
|
83
103
|
/** Menus ↑↓+Enter do `/model`: modelo e depois escopo. null = cancelado. */
|
|
84
104
|
async function pickModel(current) {
|
|
@@ -374,7 +394,11 @@ async function runViaClaude(root, promptParts, yes) {
|
|
|
374
394
|
// O REPL troca `io.ask`; em prompt único cai na pergunta simples.
|
|
375
395
|
const io = { ask: async (q, _opts) => perms.mode === "bypass" || plainAsk(q) };
|
|
376
396
|
const approval = startApprovalServer({ ask: (q, opts) => io.ask(q, opts) });
|
|
377
|
-
// O
|
|
397
|
+
// O banco aceita um único processo com acesso de escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
398
|
+
{
|
|
399
|
+
const { offerNoteMigration } = await import("./graph/migrate.js");
|
|
400
|
+
await offerNoteMigration(root, (q) => migrationAsk(q), (m) => ui.info(m));
|
|
401
|
+
}
|
|
378
402
|
const graph = await GraphStore.open(root);
|
|
379
403
|
ui.info("sincronizando grafo…");
|
|
380
404
|
await indexProject(root, graph);
|
|
@@ -465,6 +489,10 @@ program
|
|
|
465
489
|
}
|
|
466
490
|
const root = rootOf(o);
|
|
467
491
|
const startedAt = Date.now();
|
|
492
|
+
{
|
|
493
|
+
const { offerNoteMigration } = await import("./graph/migrate.js");
|
|
494
|
+
await offerNoteMigration(root, (q) => migrationAsk(q), (m) => ui.info(m));
|
|
495
|
+
}
|
|
468
496
|
const graph = await GraphStore.open(root);
|
|
469
497
|
// A contagem do prompt acompanha cada sincronização: sem isso o modelo veria "0 símbolos" a sessão inteira
|
|
470
498
|
// em projeto que começou vazio e nasceu código depois.
|
package/dist/desktop/serve.js
CHANGED
|
@@ -112,7 +112,7 @@ export async function serveDesktop(root, resumeId) {
|
|
|
112
112
|
else {
|
|
113
113
|
const approval = startApprovalServer({ ask });
|
|
114
114
|
closeApproval = approval.close;
|
|
115
|
-
// O
|
|
115
|
+
// O banco aceita um único processo com escrita: indexa, lê as stats e solta o banco para o servidor MCP.
|
|
116
116
|
if (!isHome)
|
|
117
117
|
try {
|
|
118
118
|
const graph = await GraphStore.open(root);
|
package/dist/graph/indexer.js
CHANGED
|
@@ -237,6 +237,61 @@ const count = async (store, q) => Number((await store.run(q))[0].n);
|
|
|
237
237
|
* como diff contra o banco.
|
|
238
238
|
*/
|
|
239
239
|
export async function indexProject(root, store, opts = {}) {
|
|
240
|
+
const release = await acquireIndexLock(root, opts.onProgress);
|
|
241
|
+
try {
|
|
242
|
+
return await indexProjectLocked(root, store, opts);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
release();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Só um indexador por projeto de cada vez (o banco solta o lock entre consultas, então sem isto dois
|
|
250
|
+
* indexadores se intercalariam e criariam os mesmos arquivos). Quem chega depois espera e, ao entrar,
|
|
251
|
+
* encontra o trabalho feito: o diff incremental fica vazio.
|
|
252
|
+
*/
|
|
253
|
+
async function acquireIndexLock(root, onProgress) {
|
|
254
|
+
const dir = path.join(root, ".brito");
|
|
255
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
256
|
+
const file = path.join(dir, "index.lock");
|
|
257
|
+
let warned = false;
|
|
258
|
+
for (;;) {
|
|
259
|
+
try {
|
|
260
|
+
fs.writeFileSync(file, `${process.pid} ${Date.now()}`, { flag: "wx" });
|
|
261
|
+
return () => { try {
|
|
262
|
+
fs.rmSync(file, { force: true });
|
|
263
|
+
}
|
|
264
|
+
catch { /* já removido */ } };
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
if (e.code !== "EEXIST")
|
|
268
|
+
throw e;
|
|
269
|
+
const [pid, ts] = (() => { try {
|
|
270
|
+
return fs.readFileSync(file, "utf8").split(" ").map(Number);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return [NaN, NaN];
|
|
274
|
+
} })();
|
|
275
|
+
const alive = (() => { try {
|
|
276
|
+
process.kill(pid, 0);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
return err.code === "EPERM";
|
|
281
|
+
} })();
|
|
282
|
+
if (!alive || (Number.isFinite(ts) && Date.now() - ts > 30 * 60_000)) {
|
|
283
|
+
fs.rmSync(file, { force: true });
|
|
284
|
+
continue;
|
|
285
|
+
} // dono morreu ou travou
|
|
286
|
+
if (!warned) {
|
|
287
|
+
onProgress?.("outro Brito está indexando este projeto; aguardando…");
|
|
288
|
+
warned = true;
|
|
289
|
+
}
|
|
290
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function indexProjectLocked(root, store, opts = {}) {
|
|
240
295
|
const aliases = new Aliases(root);
|
|
241
296
|
const current = await scan(root, aliases);
|
|
242
297
|
if (opts.full)
|
|
@@ -299,6 +354,7 @@ export async function indexProject(root, store, opts = {}) {
|
|
|
299
354
|
await store.run("MATCH (f:File {path:$f}), (s:Symbol {id:$id}) CREATE (f)-[:DEFINES]->(s)", { f: rel, id: s.id });
|
|
300
355
|
}
|
|
301
356
|
stored.set(rel, { hash, calls, imports: [...new Set(info.imports)], bindings });
|
|
357
|
+
await store.yieldIfWaited(); // dá a vez a outra sessão que só quer consultar
|
|
302
358
|
}
|
|
303
359
|
// 3. Recalcula arestas desejadas em memória (sem parsear) e aplica diff.
|
|
304
360
|
const known = new Set(stored.keys());
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { GraphStore } from "./store.js";
|
|
6
|
+
const READER = `const k = require("kuzu");
|
|
7
|
+
const db = new k.Database(process.argv[2], 0, true, true);
|
|
8
|
+
const c = new k.Connection(db);
|
|
9
|
+
c.query("MATCH (n:Note) RETURN n.id AS id, n.kind AS kind, n.text AS text, n.refs AS refs, n.ts AS ts").then((r) => r.getAll()).then((rows) => {
|
|
10
|
+
process.stdout.write(JSON.stringify(rows, (_, v) => (typeof v === "bigint" ? Number(v) : v)));
|
|
11
|
+
process.exit(0);
|
|
12
|
+
});`;
|
|
13
|
+
/** Copia as notas (memória) do grafo antigo (.brito/graph.kuzu) para o novo (.brito/graph.lbug).
|
|
14
|
+
* O Kùzu só é instalado numa pasta temporária, durante esta chamada: o pacote principal não depende mais dele. */
|
|
15
|
+
export async function migrateNotes(root, log = () => { }) {
|
|
16
|
+
const dir = path.join(root, ".brito");
|
|
17
|
+
const old = path.join(dir, "graph.kuzu");
|
|
18
|
+
if (!fs.existsSync(old))
|
|
19
|
+
return { none: "não há grafo antigo (.brito/graph.kuzu) neste projeto" };
|
|
20
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "brito-migrar-"));
|
|
21
|
+
try {
|
|
22
|
+
// cópia: o original fica intocado e o Kùzu não precisa de lock sobre ele
|
|
23
|
+
fs.copyFileSync(old, path.join(tmp, "g.kuzu"));
|
|
24
|
+
for (const ext of [".wal", ".shadow"])
|
|
25
|
+
if (fs.existsSync(old + ext))
|
|
26
|
+
fs.copyFileSync(old + ext, path.join(tmp, "g.kuzu" + ext));
|
|
27
|
+
fs.writeFileSync(path.join(tmp, "package.json"), '{"name":"brito-migrar","private":true}');
|
|
28
|
+
fs.writeFileSync(path.join(tmp, "read.cjs"), READER);
|
|
29
|
+
log("baixando o Kùzu (só para esta migração)…");
|
|
30
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
31
|
+
const inst = spawnSync(npm, ["i", "kuzu@0.11.3", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: tmp, encoding: "utf8" });
|
|
32
|
+
if (inst.status !== 0)
|
|
33
|
+
throw new Error(`não consegui instalar o kuzu para ler o banco antigo:\n${inst.stderr || inst.error}`);
|
|
34
|
+
const run = spawnSync(process.execPath, ["read.cjs", "g.kuzu"], { cwd: tmp, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
35
|
+
if (run.status !== 0)
|
|
36
|
+
throw new Error(`falha ao ler as notas do banco antigo:\n${run.stderr}`);
|
|
37
|
+
const notes = JSON.parse(run.stdout || "[]");
|
|
38
|
+
const g = await GraphStore.open(root);
|
|
39
|
+
try {
|
|
40
|
+
let added = 0;
|
|
41
|
+
for (const n of notes) {
|
|
42
|
+
const [{ c }] = await g.run("MATCH (x:Note {id:$id}) RETURN count(x) AS c", { id: n.id });
|
|
43
|
+
if (Number(c) > 0)
|
|
44
|
+
continue; // idempotente: rodar de novo não duplica
|
|
45
|
+
await g.run("CREATE (:Note {id:$id, kind:$k, text:$t, refs:$r, ts:$ts})", { id: n.id, k: n.kind, t: n.text, r: n.refs ?? [], ts: n.ts ?? 0 });
|
|
46
|
+
added++;
|
|
47
|
+
}
|
|
48
|
+
return { found: notes.length, added, skipped: notes.length - added };
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await g.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const markerOf = (root) => path.join(root, ".brito", ".notas-migradas");
|
|
59
|
+
/** Só no primeiro uso depois da troca de motor: há grafo antigo, ainda não há grafo novo e a pergunta não foi respondida. */
|
|
60
|
+
export function pendingNoteMigration(root) {
|
|
61
|
+
const dir = path.join(root, ".brito");
|
|
62
|
+
return fs.existsSync(path.join(dir, "graph.kuzu")) && !fs.existsSync(path.join(dir, "graph.lbug")) && !fs.existsSync(markerOf(root));
|
|
63
|
+
}
|
|
64
|
+
/** Pergunta (uma vez por projeto) e migra as notas. Qualquer resposta, inclusive "não", encerra a pergunta; `brito migrar-notas` continua disponível. */
|
|
65
|
+
export async function offerNoteMigration(root, ask, log) {
|
|
66
|
+
if (!process.stdin.isTTY || !pendingNoteMigration(root))
|
|
67
|
+
return;
|
|
68
|
+
const yes = await ask("Este projeto tem um grafo antigo (Kùzu), que pode ter notas (memória). Copiar as notas para o grafo novo? Baixa o Kùzu (~100 MB) só desta vez.");
|
|
69
|
+
fs.writeFileSync(markerOf(root), new Date().toISOString());
|
|
70
|
+
if (!yes) {
|
|
71
|
+
log("ok, nada copiado. Quando quiser: brito migrar-notas");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const r = await migrateNotes(root, log);
|
|
76
|
+
log("none" in r ? r.none : `${r.added} nota(s) copiada(s) do grafo antigo.`);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
fs.rmSync(markerOf(root), { force: true }); // falhou: pergunta de novo na próxima
|
|
80
|
+
log(`não consegui migrar as notas (${e.message.split("\n")[0]}). Tente: brito migrar-notas`);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/graph/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import kuzu from "
|
|
1
|
+
import kuzu from "@ladybugdb/core";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
/** Lê as linhas e fecha cada QueryResult na hora: o finalizer do GC depois de db.close() derruba o processo (segfault). */
|
|
@@ -21,11 +21,15 @@ export class GraphLockedError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
const isLockError = (e) => /could not set lock/i.test(String(e?.message ?? e));
|
|
24
|
+
/** Outro processo está criando o banco agora: o .shadow aparece antes do arquivo principal. Passa sozinho em instantes. */
|
|
25
|
+
const isCreatingError = (e) => /shadow file|Cannot open file .*\.(shadow|wal)|checkpoint is in progress/i.test(String(e?.message ?? e));
|
|
24
26
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
27
|
/** Quanto esperar o banco liberar antes de desistir: cobre sobreposições curtas (a reflexão de skills e o turno, por exemplo). */
|
|
26
|
-
const LOCK_WAIT_MS =
|
|
28
|
+
const LOCK_WAIT_MS = 15_000;
|
|
27
29
|
/** Tempo ocioso até soltar o banco: o lock só existe enquanto há consultas em andamento. */
|
|
28
|
-
const IDLE_MS =
|
|
30
|
+
const IDLE_MS = 150;
|
|
31
|
+
/** Intervalo máximo entre tentativas de quem espera o banco. */
|
|
32
|
+
const RETRY_MAX_MS = 250;
|
|
29
33
|
/** Cypher que altera o banco precisa do modo escrita; o resto abre somente leitura (vários processos em paralelo). */
|
|
30
34
|
const WRITE_RE = /\b(CREATE|MERGE|SET|DELETE|REMOVE|DROP|ALTER|COPY|LOAD|INSTALL|ATTACH|DETACH)\b/i;
|
|
31
35
|
export class GraphStore {
|
|
@@ -44,7 +48,12 @@ export class GraphStore {
|
|
|
44
48
|
static async open(root, waitMs = LOCK_WAIT_MS) {
|
|
45
49
|
const dir = path.join(root, ".brito");
|
|
46
50
|
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
-
|
|
51
|
+
// O Ladybug (fork mantido do Kùzu) não lê o formato antigo: usa arquivo novo e deixa o graph.kuzu intocado.
|
|
52
|
+
const file = path.join(dir, "graph.lbug");
|
|
53
|
+
if (!fs.existsSync(file) && fs.existsSync(path.join(dir, "graph.kuzu"))) {
|
|
54
|
+
process.stderr.write("brito: o grafo mudou de motor (Kùzu → Ladybug); o código será reindexado. O banco antigo (.brito/graph.kuzu) foi mantido; para trazer as notas, rode `brito migrar-notas`.\n");
|
|
55
|
+
}
|
|
56
|
+
const store = new GraphStore(file, waitMs);
|
|
48
57
|
await store.run("RETURN 1 AS ok", {}, true); // cria o banco e migra; solta o lock logo em seguida
|
|
49
58
|
return store;
|
|
50
59
|
}
|
|
@@ -65,6 +74,7 @@ export class GraphStore {
|
|
|
65
74
|
this.db = db;
|
|
66
75
|
this.conn = new kuzu.Connection(db);
|
|
67
76
|
this.mode = ro ? "r" : "w";
|
|
77
|
+
this.clearWaiting();
|
|
68
78
|
if (!ro && !this.migrated) {
|
|
69
79
|
await this.migrate();
|
|
70
80
|
this.migrated = true;
|
|
@@ -73,14 +83,45 @@ export class GraphStore {
|
|
|
73
83
|
}
|
|
74
84
|
catch (e) {
|
|
75
85
|
await this.release();
|
|
76
|
-
|
|
86
|
+
const locked = isLockError(e);
|
|
87
|
+
if (!locked && !isCreatingError(e))
|
|
77
88
|
throw e;
|
|
78
89
|
if (Date.now() >= deadline)
|
|
79
|
-
throw new GraphLockedError(this.file);
|
|
80
|
-
|
|
90
|
+
throw locked ? new GraphLockedError(this.file) : e;
|
|
91
|
+
this.signalWaiting();
|
|
92
|
+
await sleep(100 + Math.random() * (RETRY_MAX_MS - 100));
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
}
|
|
96
|
+
/** Sinal de que há processo esperando o banco: o arquivo é renovado a cada tentativa e vale poucos segundos. */
|
|
97
|
+
get waitingFile() { return path.join(path.dirname(this.file), "graph.waiting"); }
|
|
98
|
+
signalWaiting() { try {
|
|
99
|
+
fs.writeFileSync(this.waitingFile, String(process.pid));
|
|
100
|
+
}
|
|
101
|
+
catch { /* sem sinal: só perde a cortesia */ } }
|
|
102
|
+
clearWaiting() { try {
|
|
103
|
+
fs.rmSync(this.waitingFile, { force: true });
|
|
104
|
+
}
|
|
105
|
+
catch { /* idem */ } }
|
|
106
|
+
/** Para tarefas longas (indexação): se outro processo está esperando o banco, solta e dá a vez a ele. Barato quando ninguém espera. */
|
|
107
|
+
async yieldIfWaited() {
|
|
108
|
+
let age = Infinity;
|
|
109
|
+
try {
|
|
110
|
+
age = Date.now() - fs.statSync(this.waitingFile).mtimeMs;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (age > 2000)
|
|
116
|
+
return;
|
|
117
|
+
await this.queue.catch(() => undefined);
|
|
118
|
+
if (this.idle) {
|
|
119
|
+
clearTimeout(this.idle);
|
|
120
|
+
this.idle = null;
|
|
121
|
+
}
|
|
122
|
+
await this.release();
|
|
123
|
+
await sleep(RETRY_MAX_MS + 150); // mais que o intervalo de tentativa de quem espera: ele leva o banco
|
|
124
|
+
}
|
|
84
125
|
async release() {
|
|
85
126
|
const { conn, db } = this;
|
|
86
127
|
this.conn = this.db = null;
|
|
@@ -118,12 +159,27 @@ export class GraphStore {
|
|
|
118
159
|
/** Serializa as queries: leituras paralelas de subagentes dividem uma única Connection. */
|
|
119
160
|
run(cypher, params = {}, forceWrite = false) {
|
|
120
161
|
const next = this.queue.then(async () => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
162
|
+
const write = forceWrite || WRITE_RE.test(cypher);
|
|
163
|
+
const deadline = Date.now() + this.waitMs;
|
|
164
|
+
for (;;) {
|
|
165
|
+
await this.acquire(write);
|
|
166
|
+
try {
|
|
167
|
+
return await this.exec(cypher, params);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
// o binding abre o banco de forma preguiçosa: o lock/checkpoint de outro processo pode estourar só aqui, antes de executar a query
|
|
171
|
+
const locked = isLockError(e);
|
|
172
|
+
if (!locked && !isCreatingError(e))
|
|
173
|
+
throw e;
|
|
174
|
+
await this.release();
|
|
175
|
+
if (Date.now() >= deadline)
|
|
176
|
+
throw locked ? new GraphLockedError(this.file) : e;
|
|
177
|
+
this.signalWaiting();
|
|
178
|
+
await sleep(100 + Math.random() * (RETRY_MAX_MS - 100));
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
this.touch();
|
|
182
|
+
}
|
|
127
183
|
}
|
|
128
184
|
});
|
|
129
185
|
this.queue = next.catch(() => undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@anthropic-ai/sdk": "^0.131.0",
|
|
24
|
+
"@ladybugdb/core": "0.21.2",
|
|
24
25
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
25
|
-
"commander": "
|
|
26
|
-
"kuzu": "latest",
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
27
|
"string-width": "^8.3.0",
|
|
28
28
|
"tinyglobby": "^0.2.17",
|
|
29
|
-
"tree-sitter-wasms": "
|
|
29
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
30
30
|
"web-tree-sitter": "0.24.7",
|
|
31
|
-
"zod": "
|
|
31
|
+
"zod": "^4.6.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "latest",
|