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 +2 -2
- package/README.pt-BR.md +2 -2
- package/dist/cli.js +8 -2
- package/dist/graph/store.js +31 -5
- package/dist/mcp/server.js +1 -1
- package/dist/ui/banner.js +55 -23
- package/dist/ui/web-art.js +131 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
+
});
|
package/dist/graph/store.js
CHANGED
|
@@ -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
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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 = [
|
package/dist/mcp/server.js
CHANGED
|
@@ -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.
|
|
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,
|
|
4
|
-
|
|
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 ?
|
|
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
|
-
|
|
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.
|
|
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": {
|