brito-cli 0.5.0 → 0.5.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.
@@ -38,7 +38,13 @@ function extract(root, lang, file) {
38
38
  const bindings = [];
39
39
  const isPy = lang === "python";
40
40
  const mk = (name, kind, n) => {
41
- const s = { id: `${file}::${name}:${n.startPosition.row + 1}`, name, kind, file, startLine: n.startPosition.row + 1, endLine: n.endPosition.row + 1 };
41
+ const id = `${file}::${name}:${n.startPosition.row + 1}`;
42
+ const dup = syms.find((x) => x.id === id); // getter+setter (ou overloads) na mesma linha: id único
43
+ if (dup) {
44
+ dup.endLine = Math.max(dup.endLine, n.endPosition.row + 1);
45
+ return dup;
46
+ }
47
+ const s = { id, name, kind, file, startLine: n.startPosition.row + 1, endLine: n.endPosition.row + 1 };
42
48
  syms.push(s);
43
49
  return s;
44
50
  };
@@ -373,7 +373,13 @@ export function extractLang(root, lang, file) {
373
373
  const syms = [], calls = [];
374
374
  const acc = { imports: [], bindings: [] };
375
375
  const mk = (name, kind, n) => {
376
- const s = { id: `${file}::${name}:${n.startPosition.row + 1}`, name, kind, file, startLine: n.startPosition.row + 1, endLine: n.endPosition.row + 1 };
376
+ const id = `${file}::${name}:${n.startPosition.row + 1}`;
377
+ const dup = syms.find((x) => x.id === id); // getter+setter (ou overloads) na mesma linha: id único
378
+ if (dup) {
379
+ dup.endLine = Math.max(dup.endLine, n.endPosition.row + 1);
380
+ return dup;
381
+ }
382
+ const s = { id, name, kind, file, startLine: n.startPosition.row + 1, endLine: n.endPosition.row + 1 };
377
383
  syms.push(s);
378
384
  return s;
379
385
  };
@@ -15,9 +15,8 @@ async function drain(r, read = true) {
15
15
  /** Outro processo do Brito está com o banco aberto: o Kùzu só deixa um processo por projeto. */
16
16
  export class GraphLockedError extends Error {
17
17
  constructor(file) {
18
- super(`O grafo deste projeto está em uso por outro processo do Brito (${file}).\n` +
19
- "O banco aceita um processo por vez: provavelmente há outro terminal com o Brito, ou uma sessão do Claude Code usando o Brito, neste mesmo diretório.\n" +
20
- "Feche o outro, ou espere o turno em andamento terminar, e tente de novo.");
18
+ super(`Outro processo do Brito está gravando no grafo deste projeto (${file}) há mais tempo que o esperado.\n` +
19
+ "As sessões soltam o banco quando ficam ociosas; uma indexação longa em outro terminal pode segurá-lo. Espere ela terminar e tente de novo.");
21
20
  this.name = "GraphLockedError";
22
21
  }
23
22
  }
@@ -25,35 +24,82 @@ const isLockError = (e) => /could not set lock/i.test(String(e?.message ?? e));
25
24
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
25
  /** Quanto esperar o banco liberar antes de desistir: cobre sobreposições curtas (a reflexão de skills e o turno, por exemplo). */
27
26
  const LOCK_WAIT_MS = 4000;
27
+ /** Tempo ocioso até soltar o banco: o lock só existe enquanto há consultas em andamento. */
28
+ const IDLE_MS = 300;
29
+ /** Cypher que altera o banco precisa do modo escrita; o resto abre somente leitura (vários processos em paralelo). */
30
+ const WRITE_RE = /\b(CREATE|MERGE|SET|DELETE|REMOVE|DROP|ALTER|COPY|LOAD|INSTALL|ATTACH|DETACH)\b/i;
28
31
  export class GraphStore {
29
- db;
30
- conn;
32
+ db = null;
33
+ conn = null;
34
+ mode = null;
35
+ idle = null;
36
+ migrated = false;
31
37
  queue = Promise.resolve();
32
- constructor(db, conn) {
33
- this.db = db;
34
- this.conn = conn;
38
+ file;
39
+ waitMs;
40
+ constructor(file, waitMs) {
41
+ this.file = file;
42
+ this.waitMs = waitMs;
35
43
  }
36
44
  static async open(root, waitMs = LOCK_WAIT_MS) {
37
45
  const dir = path.join(root, ".brito");
38
46
  fs.mkdirSync(dir, { recursive: true });
39
- const file = path.join(dir, "graph.kuzu");
40
- const deadline = Date.now() + waitMs;
47
+ const store = new GraphStore(path.join(dir, "graph.kuzu"), waitMs);
48
+ await store.run("RETURN 1 AS ok", {}, true); // cria o banco e migra; solta o lock logo em seguida
49
+ return store;
50
+ }
51
+ /** Abre (ou promove) o banco no modo pedido, esperando o lock de outro processo. Roda dentro da fila. */
52
+ async acquire(write) {
53
+ if (this.idle) {
54
+ clearTimeout(this.idle);
55
+ this.idle = null;
56
+ }
57
+ if (this.db && (this.mode === "w" || !write))
58
+ return;
59
+ await this.release(); // leitura -> escrita: fecha antes de reabrir
60
+ const deadline = Date.now() + this.waitMs;
41
61
  for (;;) {
42
62
  try {
43
- const db = new kuzu.Database(file);
44
- const store = new GraphStore(db, new kuzu.Connection(db));
45
- await store.migrate();
46
- return store;
63
+ const ro = !write && fs.existsSync(this.file) && this.migrated;
64
+ const db = ro ? new kuzu.Database(this.file, 0, true, true) : new kuzu.Database(this.file);
65
+ this.db = db;
66
+ this.conn = new kuzu.Connection(db);
67
+ this.mode = ro ? "r" : "w";
68
+ if (!ro && !this.migrated) {
69
+ await this.migrate();
70
+ this.migrated = true;
71
+ }
72
+ return;
47
73
  }
48
74
  catch (e) {
75
+ await this.release();
49
76
  if (!isLockError(e))
50
77
  throw e;
51
78
  if (Date.now() >= deadline)
52
- throw new GraphLockedError(file);
53
- await sleep(250);
79
+ throw new GraphLockedError(this.file);
80
+ await sleep(100 + Math.random() * 150);
54
81
  }
55
82
  }
56
83
  }
84
+ async release() {
85
+ const { conn, db } = this;
86
+ this.conn = this.db = null;
87
+ this.mode = null;
88
+ try {
89
+ await conn?.close();
90
+ }
91
+ catch { /* já fechado */ }
92
+ try {
93
+ await db?.close();
94
+ }
95
+ catch { /* já fechado */ }
96
+ }
97
+ touch() {
98
+ if (this.idle)
99
+ clearTimeout(this.idle);
100
+ this.idle = setTimeout(() => { this.queue = this.queue.then(() => this.release()).catch(() => undefined); }, IDLE_MS);
101
+ this.idle.unref();
102
+ }
57
103
  async migrate() {
58
104
  const ddl = [
59
105
  "CREATE NODE TABLE IF NOT EXISTS File(path STRING, lang STRING, hash STRING, calls STRING[], imports STRING[], bindings STRING[], PRIMARY KEY(path))",
@@ -70,19 +116,28 @@ export class GraphStore {
70
116
  await drain(await this.conn.query(`ALTER TABLE File ADD IF NOT EXISTS ${col} STRING[] DEFAULT []`), false);
71
117
  }
72
118
  /** Serializa as queries: leituras paralelas de subagentes dividem uma única Connection. */
73
- run(cypher, params = {}) {
74
- const next = this.queue.then(() => this.exec(cypher, params));
119
+ run(cypher, params = {}, forceWrite = false) {
120
+ 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();
127
+ }
128
+ });
75
129
  this.queue = next.catch(() => undefined);
76
130
  return next;
77
131
  }
78
132
  async exec(cypher, params) {
133
+ const conn = this.conn;
79
134
  if (Object.keys(params).length === 0) {
80
- return drain(await this.conn.query(cypher));
135
+ return drain(await conn.query(cypher));
81
136
  }
82
- const ps = await this.conn.prepare(cypher);
137
+ const ps = await conn.prepare(cypher);
83
138
  if (!ps.isSuccess())
84
139
  throw new Error(ps.getErrorMessage());
85
- return drain(await this.conn.execute(ps, params));
140
+ return drain(await conn.execute(ps, params));
86
141
  }
87
142
  /** Apaga só o grafo de código; notas (memória) são preservadas. */
88
143
  async clearCode() {
@@ -90,7 +145,11 @@ export class GraphStore {
90
145
  await this.run("MATCH (f:File) DETACH DELETE f");
91
146
  }
92
147
  async close() {
93
- await this.conn.close();
94
- await this.db.close();
148
+ await this.queue.catch(() => undefined);
149
+ if (this.idle) {
150
+ clearTimeout(this.idle);
151
+ this.idle = null;
152
+ }
153
+ await this.release();
95
154
  }
96
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brito-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.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": {