brito-cli 0.3.0 → 0.3.2

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
@@ -133,13 +133,13 @@ Brito has **no database drivers and connects to nothing on its own**. When it ne
133
133
 
134
134
  ## Configuration
135
135
 
136
- `BRITO_MODEL`, `BRITO_EFFORT`, `BRITO_EXPLORE_MODEL`, `BRITO_LEARN_MODEL`, `BRITO_HOME` (where global skills and history live), `BRITO_CONFIG`, `BRITO_COLOR`, `BRITO_INDEX_IGNORED`, `BRITO_SKILL_MIN_TOOLS` (how long a turn must be to trigger reflection when nothing else does).
136
+ `BRITO_MODEL`, `BRITO_EFFORT`, `BRITO_EXPLORE_MODEL`, `BRITO_LEARN_MODEL`, `BRITO_HOME` (where global skills and history live), `BRITO_CONFIG`, `BRITO_COLOR`, `BRITO_BANNER` (`compact` or `full`; default: full screen on windows of 95+ columns and 28+ rows), `BRITO_INDEX_IGNORED`, `BRITO_SKILL_MIN_TOOLS` (how long a turn must be to trigger reflection when nothing else does).
137
137
 
138
138
  Project data lives in `.brito/` (graph, project skills, agents) and global data in `~/.brito`. The graph, memory and skills stay on your machine; as with any coding agent, the code the model reads is sent to the model provider you configured.
139
139
 
140
140
  ## Limitations
141
141
 
142
- - Early software (v0.3.0), one maintainer.
142
+ - Early software (v0.3.1), one maintainer.
143
143
  - **The interface and the agent's default language are Brazilian Portuguese.** Translating them is a welcome contribution.
144
144
  - Skill learning is a heuristic: it proposes, you decide. Expect some proposals to be rejected.
145
145
 
package/README.pt-BR.md CHANGED
@@ -133,13 +133,13 @@ O Brito **não tem drivers de banco e não se conecta a nada sozinho**. Quando p
133
133
 
134
134
  ## Configuração
135
135
 
136
- `BRITO_MODEL`, `BRITO_EFFORT`, `BRITO_EXPLORE_MODEL`, `BRITO_LEARN_MODEL`, `BRITO_HOME` (onde ficam skills globais e histórico), `BRITO_CONFIG`, `BRITO_COLOR`, `BRITO_INDEX_IGNORED`, `BRITO_SKILL_MIN_TOOLS` (tamanho mínimo do turno para disparar a reflexão quando nada mais dispara).
136
+ `BRITO_MODEL`, `BRITO_EFFORT`, `BRITO_EXPLORE_MODEL`, `BRITO_LEARN_MODEL`, `BRITO_HOME` (onde ficam skills globais e histórico), `BRITO_CONFIG`, `BRITO_COLOR`, `BRITO_BANNER` (`compact` ou `full`; padrão: tela cheia em janelas de 95+ colunas e 28+ linhas), `BRITO_INDEX_IGNORED`, `BRITO_SKILL_MIN_TOOLS` (tamanho mínimo do turno para disparar a reflexão quando nada mais dispara).
137
137
 
138
138
  Os dados do projeto ficam em `.brito/` (grafo, skills do projeto, agentes) e os globais em `~/.brito`. O grafo, a memória e as skills ficam na sua máquina; como em qualquer agente de código, o código que o modelo lê é enviado ao provedor de modelo que você configurou.
139
139
 
140
140
  ## Limitações
141
141
 
142
- - Software em fase inicial (v0.3.0), um mantenedor.
142
+ - Software em fase inicial (v0.3.1), um mantenedor.
143
143
  - **A interface e o idioma padrão do agente são português do Brasil.** Traduzi-los é uma contribuição bem-vinda.
144
144
  - O aprendizado de skills é heurístico: ele propõe, você decide. Espere rejeitar algumas propostas.
145
145
 
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import os from "node:os";
12
12
  import fs from "node:fs";
13
13
  import { PLAN_NOTE, perms, describeClaudeTool } from "./permissions.js";
14
14
  import { loadProjectAgents, listAgents } from "./agent/agents.js";
15
- import { GraphStore } from "./graph/store.js";
15
+ import { GraphLockedError, GraphStore } from "./graph/store.js";
16
16
  import { indexProject } from "./graph/indexer.js";
17
17
  import { Agent, MODEL } from "./agent/loop.js";
18
18
  import { EFFORTS, MODEL_ALIASES, MODEL_HELP, effortLabel, initialEffort, isEffort, loadReasoningDisplay, parseModelArgs, saveEffort, saveModel, saveReasoningDisplay } from "./model.js";
@@ -559,4 +559,10 @@ program
559
559
  },
560
560
  });
561
561
  });
562
- program.parseAsync();
562
+ program.parseAsync().catch((e) => {
563
+ if (e instanceof GraphLockedError) {
564
+ console.error(c.err("✗ ") + e.message);
565
+ process.exit(1);
566
+ }
567
+ throw e;
568
+ });
@@ -12,6 +12,19 @@ async function drain(r, read = true) {
12
12
  x.close();
13
13
  }
14
14
  }
15
+ /** Outro processo do Brito está com o banco aberto: o Kùzu só deixa um processo por projeto. */
16
+ export class GraphLockedError extends Error {
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.");
21
+ this.name = "GraphLockedError";
22
+ }
23
+ }
24
+ const isLockError = (e) => /could not set lock/i.test(String(e?.message ?? e));
25
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
+ /** Quanto esperar o banco liberar antes de desistir: cobre sobreposições curtas (a reflexão de skills e o turno, por exemplo). */
27
+ const LOCK_WAIT_MS = 4000;
15
28
  export class GraphStore {
16
29
  db;
17
30
  conn;
@@ -20,13 +33,26 @@ export class GraphStore {
20
33
  this.db = db;
21
34
  this.conn = conn;
22
35
  }
23
- static async open(root) {
36
+ static async open(root, waitMs = LOCK_WAIT_MS) {
24
37
  const dir = path.join(root, ".brito");
25
38
  fs.mkdirSync(dir, { recursive: true });
26
- const db = new kuzu.Database(path.join(dir, "graph.kuzu"));
27
- const store = new GraphStore(db, new kuzu.Connection(db));
28
- await store.migrate();
29
- return store;
39
+ const file = path.join(dir, "graph.kuzu");
40
+ const deadline = Date.now() + waitMs;
41
+ for (;;) {
42
+ 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;
47
+ }
48
+ catch (e) {
49
+ if (!isLockError(e))
50
+ throw e;
51
+ if (Date.now() >= deadline)
52
+ throw new GraphLockedError(file);
53
+ await sleep(250);
54
+ }
55
+ }
30
56
  }
31
57
  async migrate() {
32
58
  const ddl = [
@@ -64,7 +64,7 @@ export async function serveMcp(root) {
64
64
  ask: async () => true,
65
65
  sync: async () => { await indexProject(root, graph); },
66
66
  };
67
- const server = new Server({ name: MCP_NAME, version: "0.3.0" }, { capabilities: { tools: {} }, instructions: MCP_INSTRUCTIONS });
67
+ const server = new Server({ name: MCP_NAME, version: "0.3.1" }, { capabilities: { tools: {} }, instructions: MCP_INSTRUCTIONS });
68
68
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
69
69
  tools: [{
70
70
  name: APPROVE_TOOL,
package/dist/ui/banner.js CHANGED
@@ -1,21 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import stringWidth from "string-width";
3
- import { c, cols, mix, palette, stripAnsi, truncAnsi } from "./theme.js";
4
- const LOGO = [
5
- "████ ████ █████ █████ ███ ",
6
- "█ █ █ █ █ █ █ █",
7
- "████ ████ █ █ █ █",
8
- "█ █ █ █ █ █ █ █",
9
- "████ █ █ █████ █ ███ ",
10
- ];
11
- /** Arte do lado esquerdo: um pequeno grafo, a identidade do Brito. */
12
- const HERO = [
13
- " ◉───────◉───────◉ ",
14
- " │ \\ / │ \\ / │ ",
15
- " │ ◉────◉───◉ │ ",
16
- " │ / \\ │ / \\ │ ",
17
- " ◉───────◉───────◉ ",
18
- ];
3
+ import { c, cols, stripAnsi, truncAnsi } from "./theme.js";
4
+ import { LOGO, WEB_W, overlayText, serialize, stampLogo, webCanvas, webLogo } from "./web-art.js";
19
5
  const GROUPS = [
20
6
  ["arquivos", (n) => ["read_file", "write_file", "edit_file", "glob", "grep"].includes(n)],
21
7
  ["shell", (n) => n === "bash"],
@@ -49,7 +35,57 @@ function listLine(label, items, width) {
49
35
  const rest = items.length - shown.length;
50
36
  return head + c.soft(shown.join(", ")) + (rest > 0 ? c.dim(` +${rest}`) : "");
51
37
  }
38
+ /** Linhas livres embaixo da janela: moldura (2) + dock com status e prompt. */
39
+ const BELOW_BANNER = 7;
40
+ const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
41
+ /** BRITO_BANNER=compact força o banner pequeno; =full força a tela cheia; sem nada, é tela cheia em janela grande. */
42
+ function wantsFull() {
43
+ const mode = process.env.BRITO_BANNER;
44
+ if (mode === "compact")
45
+ return false;
46
+ if (mode === "full")
47
+ return true;
48
+ return (process.stdout.rows ?? 0) >= 28 && cols() >= 95;
49
+ }
50
+ /** Tela cheia: a teia ocupa a janela inteira, com o nome no centro e o painel por cima. */
51
+ function renderFull(i) {
52
+ const total = cols();
53
+ const inner = total - 4;
54
+ const rows = process.stdout.rows || 30;
55
+ const h = Math.max(20, rows - BELOW_BANNER);
56
+ const grid = webCanvas(inner, h, { rings: h >= 26 ? 4 : 3, spokes: inner >= 120 ? 16 : 12 });
57
+ const blockW = Math.min(inner - 4, 72);
58
+ const home = process.env.HOME ?? "\0";
59
+ const info = [
60
+ truncAnsi(`${c.soft(i.model)} ${c.dim("·")} ${c.muted(`${fmtK(i.ctxWindow)} ctx`)} ${c.dim("·")} ${c.muted(i.dir.replace(home, "~"))} ${c.dim("·")} ${c.dim(`sessão ${i.session}`)}`, blockW),
61
+ "",
62
+ ];
63
+ for (const [label, test] of GROUPS) {
64
+ const names = i.tools.filter(test);
65
+ if (names.length)
66
+ info.push(listLine(label, names, blockW));
67
+ }
68
+ info.push(c.muted("subagentes: ") + c.soft(i.agents.join(", ") || "—"));
69
+ const project = i.skills.filter((s) => s.scope === "project").map((s) => s.name);
70
+ const global = i.skills.filter((s) => s.scope === "global").map((s) => s.name);
71
+ if (project.length)
72
+ info.push(listLine("skills do projeto", project, blockW));
73
+ if (global.length)
74
+ info.push(listLine("skills globais", global, blockW));
75
+ if (!i.skills.length)
76
+ info.push(c.muted("skills: ") + c.dim("nenhuma ainda · nascem sozinhas após tarefas complexas"));
77
+ info.push("", c.dim(`${i.tools.length} tools · ${i.skills.length} ${i.skills.length === 1 ? "skill" : "skills"} · ${i.symbols} símbolos · ${i.notes} notas · /help`));
78
+ const top = Math.max(0, Math.floor((h - (LOGO.length + 1 + info.length)) / 2));
79
+ stampLogo(grid, Math.floor((inner - LOGO[0].length) / 2), top);
80
+ const x0 = Math.floor((inner - blockW) / 2);
81
+ info.forEach((l, k) => overlayText(grid, x0, top + LOGO.length + 1 + k, l));
82
+ const title = ` ${i.name} v${version()} `;
83
+ const head = c.accent("╭─") + c.bold(c.light(title)) + c.accent("─".repeat(Math.max(1, total - title.length - 3)) + "╮");
84
+ return [head, ...serialize(grid).map((r) => c.accent("│ ") + r + c.accent(" │")), c.accent("╰" + "─".repeat(total - 2) + "╯")].join("\n");
85
+ }
52
86
  export function renderBanner(i) {
87
+ if (wantsFull())
88
+ return renderFull(i);
53
89
  const total = Math.min(cols(), 110);
54
90
  const wide = total >= 95;
55
91
  const inner = total - 4;
@@ -59,15 +95,11 @@ export function renderBanner(i) {
59
95
  c.muted(i.dir.replace(home, "~")),
60
96
  c.dim(`sessão ${i.session}`),
61
97
  ];
62
- const leftW = wide ? Math.max(LOGO[0].length, HERO[0].length) + 2 : inner;
98
+ const leftW = wide ? WEB_W + 2 : inner;
63
99
  const rightW = wide ? inner - leftW - 3 : inner;
64
100
  const left = [];
65
- if (wide) {
66
- LOGO.forEach((l, k) => left.push(c.rgb(mix(palette.accent, palette.soft, k / (LOGO.length - 1)))(l)));
67
- left.push("");
68
- HERO.forEach((l) => left.push(l.replace(/[◉]/g, (m) => c.light(m)).replace(/[─│\\/]/g, (m) => c.accent(m))));
69
- left.push("");
70
- }
101
+ if (wide)
102
+ left.push(...webLogo(), "");
71
103
  left.push(...context);
72
104
  const right = [c.bold(c.light("Ferramentas disponíveis"))];
73
105
  for (const [label, test] of GROUPS) {
@@ -0,0 +1,131 @@
1
+ import { RESET, c, mix, palette } from "./theme.js";
2
+ /**
3
+ * A identidade visual do Brito: uma teia (raios + anéis em polígono, com nós nos cruzamentos), meio grafo, com o nome
4
+ * por cima. É gerada em vez de desenhada à mão para servir a qualquer tamanho de janela e os traços ficarem alinhados.
5
+ * Só usa tons derivados do roxo da marca (BRITO_COLOR): claro no centro, escurecendo até a borda.
6
+ */
7
+ export const LOGO = [
8
+ "████ ████ █████ █████ ███ ",
9
+ "█ █ █ █ █ █ █ █",
10
+ "████ ████ █ █ █ █",
11
+ "█ █ █ █ █ █ █ █",
12
+ "████ █ █ █████ █ ███ ",
13
+ ];
14
+ /** Tamanho da teia do banner compacto. */
15
+ export const WEB_W = 33;
16
+ export const WEB_H = 13;
17
+ const blank = () => ({ ch: " ", kind: "empty" });
18
+ const paintOf = (col) => c.rgb(col);
19
+ /** Caractere de um segmento inteiro, pelo ângulo visual (a célula tem ~2x mais altura que largura): evita ─╱╲ misturados. */
20
+ function strand(x0, y0, x1, y1) {
21
+ const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0) * 2;
22
+ const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
23
+ if (angle < 25)
24
+ return "─";
25
+ if (angle > 65)
26
+ return "│";
27
+ return (x1 - x0) * (y1 - y0) > 0 ? "╲" : "╱";
28
+ }
29
+ /** Traço de a até b (Bresenham) com um caractere só; só preenche célula vazia, então o que veio antes prevalece. */
30
+ function line(grid, x0, y0, x1, y1, ch, paintAt) {
31
+ const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0);
32
+ const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
33
+ let err = dx - dy, x = x0, y = y0;
34
+ while (x !== x1 || y !== y1) {
35
+ const e2 = 2 * err;
36
+ if (e2 > -dy) {
37
+ err -= dy;
38
+ x += sx;
39
+ }
40
+ if (e2 < dx) {
41
+ err += dx;
42
+ y += sy;
43
+ }
44
+ if (grid[y]?.[x]?.kind === "empty")
45
+ grid[y][x] = { ch, kind: "strand", paint: paintAt(x, y) };
46
+ }
47
+ }
48
+ /** A teia crua de w x h células, centrada: anéis sólidos, raios pontilhados e nós nos cruzamentos. */
49
+ export function webCanvas(w, h, { rings, spokes }) {
50
+ const grid = Array.from({ length: h }, () => Array.from({ length: w }, blank));
51
+ const cx = (w - 1) / 2, cy = (h - 1) / 2;
52
+ const vertex = (k, f) => {
53
+ const a = (2 * Math.PI * k) / spokes;
54
+ return [Math.round(cx + cx * f * Math.cos(a)), Math.round(cy + cy * f * Math.sin(a))];
55
+ };
56
+ /** 0 no centro, 1 na borda. */
57
+ const dist = (x, y) => Math.min(1, Math.hypot((x - cx) / Math.max(1, cx), (y - cy) / Math.max(1, cy)));
58
+ const ringPaint = (x, y) => paintOf(mix(palette.light, palette.accent, Math.pow(dist(x, y), 0.8)));
59
+ const spokePaint = (x, y) => paintOf(mix(palette.soft, palette.accent, 0.35 + 0.65 * dist(x, y)));
60
+ for (let i = 1; i <= rings; i++) {
61
+ const f = i / rings;
62
+ for (let k = 0; k < spokes; k++) {
63
+ const [x0, y0] = vertex(k, f), [x1, y1] = vertex((k + 1) % spokes, f);
64
+ line(grid, x0, y0, x1, y1, strand(x0, y0, x1, y1), ringPaint);
65
+ }
66
+ }
67
+ for (let k = 0; k < spokes; k++) {
68
+ const [x, y] = vertex(k, 1);
69
+ line(grid, Math.round(cx), Math.round(cy), x, y, "·", spokePaint);
70
+ }
71
+ for (let i = 1; i <= rings; i++) {
72
+ for (let k = 0; k < spokes; k++) {
73
+ const [x, y] = vertex(k, i / rings);
74
+ grid[y][x] = { ch: i === rings ? "◉" : "○", kind: "node", paint: paintOf(mix(palette.soft, palette.accent, 0.15 + 0.7 * dist(x, y))) };
75
+ }
76
+ }
77
+ grid[Math.round(cy)][Math.round(cx)] = { ch: "◉", kind: "node", paint: paintOf(palette.soft) };
78
+ return grid;
79
+ }
80
+ /** O nome por cima da teia. Na faixa das letras os traços somem (só os nós aparecem pelos vãos): o nome fica nítido. */
81
+ export function stampLogo(grid, left, top) {
82
+ LOGO.forEach((l, r) => {
83
+ const row = grid[top + r];
84
+ if (!row)
85
+ return;
86
+ const first = l.search(/\S/), last = l.search(/\s*$/) - 1;
87
+ for (let x = first - 1; x <= last + 1; x++)
88
+ if (row[left + x]?.kind === "strand")
89
+ row[left + x] = blank();
90
+ for (let x = 0; x < l.length; x++) {
91
+ if (l[x] !== " " && row[left + x])
92
+ row[left + x] = { ch: l[x], kind: "text", paint: paintOf(mix(palette.accent, palette.soft, r / (LOGO.length - 1))) };
93
+ }
94
+ });
95
+ }
96
+ /** Uma linha já colorida (ANSI) vira células: o que a cor não alcança fica sem pintura. */
97
+ function ansiCells(s) {
98
+ const out = [];
99
+ let prefix = "";
100
+ for (const part of s.split(/(\x1b\[[0-9;]*m)/)) {
101
+ if (!part)
102
+ continue;
103
+ if (part.startsWith("\x1b[")) {
104
+ prefix = part === RESET ? "" : prefix + part;
105
+ continue;
106
+ }
107
+ const p = prefix;
108
+ for (const ch of part)
109
+ out.push({ ch, kind: "text", paint: p ? (t) => p + t + RESET : undefined });
110
+ }
111
+ return out;
112
+ }
113
+ /** Escreve texto sobre a teia; a faixa da linha (mais 1 coluna de cada lado) é limpa para o texto ler bem. */
114
+ export function overlayText(grid, x, y, ansi) {
115
+ const row = grid[y];
116
+ if (!row)
117
+ return;
118
+ const cells = ansiCells(ansi);
119
+ for (let k = -1; k <= cells.length; k++)
120
+ if (row[x + k])
121
+ row[x + k] = blank();
122
+ cells.forEach((cell, k) => { if (row[x + k])
123
+ row[x + k] = cell; });
124
+ }
125
+ export const serialize = (grid) => grid.map((row) => row.map((cell) => (cell.paint ? cell.paint(cell.ch) : cell.ch)).join(""));
126
+ /** O logotipo do banner compacto: BRITO sobre uma teia pequena. */
127
+ export function webLogo() {
128
+ const grid = webCanvas(WEB_W, WEB_H, { rings: 3, spokes: 8 });
129
+ stampLogo(grid, Math.floor((WEB_W - LOGO[0].length) / 2), Math.round((WEB_H - 1) / 2) - Math.floor(LOGO.length / 2));
130
+ return serialize(grid);
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brito-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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": {