brito-cli 0.5.2 → 0.6.1

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 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
- - **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 ([Kùzu](https://kuzudb.com)), local and incremental.
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. Tests run in CI on macOS, Linux and Windows (use 0.6.1 or later on Windows/Linux). 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
- - **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 ([Kùzu](https://kuzudb.com)), local e incremental.
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. Os testes rodam no CI em macOS, Linux e Windows (use a 0.6.1 ou posterior no Windows/Linux). 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 kuzu aceita um único processo com acesso de escrita: indexa, lê as stats e solta o banco para o servidor MCP.
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.
@@ -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 kuzu aceita um único processo com escrita: indexa, lê as stats e solta o banco para o servidor MCP.
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);
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import kuzu from "kuzu";
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 ou fazendo checkpoint: o .shadow/.wal aparece, some ou fica negado por instantes (a mensagem muda por SO). Passa sozinho. */
25
+ const isCreatingError = (e) => /shadow file|\.(shadow|wal)\b|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 = 4000;
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 = 300;
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
- const store = new GraphStore(path.join(dir, "graph.kuzu"), waitMs);
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
- if (!isLockError(e))
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
- await sleep(100 + Math.random() * 150);
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
- await this.acquire(forceWrite || WRITE_RE.test(cypher));
122
- try {
123
- return await this.exec(cypher, params);
124
- }
125
- finally {
126
- this.touch();
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.5.2",
3
+ "version": "0.6.1",
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,9 +21,9 @@
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
26
  "commander": "^14.0.3",
26
- "kuzu": "^0.11.3",
27
27
  "string-width": "^8.3.0",
28
28
  "tinyglobby": "^0.2.17",
29
29
  "tree-sitter-wasms": "^0.1.13",