brito-cli 0.0.0-stage → 0.3.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/dist/ui/ui.js ADDED
@@ -0,0 +1,426 @@
1
+ import { BOLD, CODE, ITALIC, OFF, c, cols, palette, useColor } from "./theme.js";
2
+ import { renderBanner } from "./banner.js";
3
+ import stringWidth from "string-width";
4
+ export const NAME = "Brito";
5
+ const isTTY = !!process.stdout.isTTY;
6
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
+ const FACES = ["(。•́︿•̀。)", "(⊙_⊙)", "(•̀ᴗ•́)و", "(¬‿¬)", "ʕ•ᴥ•ʔ", "(◕‿◕)", "(・・?)", "(`・ω・´)"];
8
+ const VERBS = ["pensando", "ponderando", "investigando", "mapeando o grafo", "conectando os pontos", "analisando", "rastreando chamadas", "ruminando", "fazendo hipóteses"];
9
+ const DONE = ["(•̀ᴗ•́)و", "ヽ(´▽`)/", "(◕‿◕)"];
10
+ const EMOJI = {
11
+ read_file: "📄", write_file: "📝", edit_file: "📝", glob: "📁", grep: "🔍", bash: "💻",
12
+ remember: "🧠", recall: "🧠", delegate: "🤖", skill_load: "🎓", skill_save: "🎓", skill_delete: "🎓",
13
+ };
14
+ const emojiOf = (name) => EMOJI[name] ?? (name.startsWith("graph_") ? "🔗" : "🔧");
15
+ const fmtMs = (ms) => (ms < 1000 ? `${(ms / 1000).toFixed(1)}s` : ms < 60_000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.floor(ms / 60_000)}m${Math.round((ms % 60_000) / 1000)}s`);
16
+ /** Duração acumulada: 45.2s · 12m03s · 1h02m. */
17
+ export const fmtDur = (ms) => {
18
+ const s = Math.round(ms / 1000);
19
+ if (ms < 60_000)
20
+ return `${(ms / 1000).toFixed(1)}s`;
21
+ if (s < 3600)
22
+ return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
23
+ return `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, "0")}m`;
24
+ };
25
+ export const fmtTokens = (n) => (n >= 1_000_000 ? `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M` : n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}K` : String(n));
26
+ /** Primeiro argumento "legível" da chamada, para a linha da tool. */
27
+ function preview(input) {
28
+ if (!input || typeof input !== "object")
29
+ return "";
30
+ const v = Object.values(input).find((x) => typeof x === "string" && x.length > 0);
31
+ return v ? v.replace(/\s+/g, " ").slice(0, 60) : "";
32
+ }
33
+ const skillName = (input) => {
34
+ const n = input?.name;
35
+ return typeof n === "string" ? n : "";
36
+ };
37
+ /** Quebra uma linha por palavra em no máximo `w` colunas (palavra maior que `w` fica inteira). */
38
+ function wrapLine(line, w) {
39
+ if (stringWidth(line) <= w)
40
+ return [line];
41
+ const out = [];
42
+ let cur = "";
43
+ for (const word of line.split(/\s+/).filter(Boolean)) {
44
+ if (cur && stringWidth(cur) + 1 + stringWidth(word) > w) {
45
+ out.push(cur);
46
+ cur = word;
47
+ }
48
+ else
49
+ cur = cur ? `${cur} ${word}` : word;
50
+ }
51
+ if (cur)
52
+ out.push(cur);
53
+ return out;
54
+ }
55
+ class UI {
56
+ busy = false;
57
+ paused = false;
58
+ visible = false; // spinner desenhado agora
59
+ timer;
60
+ t0 = 0;
61
+ frame = 0;
62
+ running = new Map();
63
+ textOpen = false;
64
+ lineStart = true;
65
+ seed = Math.floor(Math.random() * FACES.length);
66
+ dock = null;
67
+ /** soma do tempo de todos os turnos da sessão */
68
+ totalMs = 0;
69
+ /** skill carregada no turno atual (aparece na barra de status) */
70
+ activeSkill = "";
71
+ /** `/reasoning show|hide|full|clamp` */
72
+ reasoning = { show: true, full: false };
73
+ /** Com dock, toda saída vai para cima dele e o spinner mora na linha própria do dock. */
74
+ attachDock(d) { this.dock = d; }
75
+ /** Menu com setas + Enter (Esc cancela). Sem dock devolve null. */
76
+ select(title, options, current = 0) {
77
+ return this.dock ? this.dock.select(title, options, current) : Promise.resolve(null);
78
+ }
79
+ get canSelect() { return this.dock !== null; }
80
+ out(s) { if (this.dock)
81
+ this.dock.write(s);
82
+ else
83
+ process.stdout.write(s); }
84
+ // ---------- baixo nível ----------
85
+ clear() {
86
+ if (this.dock) {
87
+ this.dock.setSpinner(null);
88
+ return;
89
+ }
90
+ if (this.visible && isTTY) {
91
+ process.stdout.write("\r\x1b[2K");
92
+ this.visible = false;
93
+ }
94
+ }
95
+ spinnerLabel() {
96
+ const secs = (Date.now() - this.t0) / 1000;
97
+ const f = c.light(FRAMES[this.frame % FRAMES.length]);
98
+ if (this.running.size > 0) {
99
+ const names = [...new Set([...this.running.values()].map((r) => (r.name === "skill_load" && skillName(r.input) ? `skill ${skillName(r.input)}` : r.name)))];
100
+ const what = this.running.size === 1 ? names[0] : `${this.running.size} ferramentas`;
101
+ return `${f} ${c.muted(`executando ${what}…`)} ${c.dim(`(${secs.toFixed(1)}s)`)}`;
102
+ }
103
+ const face = FACES[(this.seed + Math.floor(secs / 2)) % FACES.length];
104
+ const verb = VERBS[(this.seed + Math.floor(secs / 3)) % VERBS.length];
105
+ return `${f} ${c.soft(face)} ${c.muted(`${verb}…`)} ${c.dim(`(${secs.toFixed(1)}s)`)}`;
106
+ }
107
+ draw() {
108
+ if (!isTTY)
109
+ return;
110
+ const show = this.busy && !this.paused && !this.textOpen;
111
+ if (this.dock) {
112
+ this.dock.setSpinner(show ? this.spinnerLabel() : null);
113
+ return;
114
+ }
115
+ if (!show)
116
+ return;
117
+ process.stdout.write(`\r\x1b[2K${this.spinnerLabel()}`);
118
+ this.visible = true;
119
+ }
120
+ /** Imprime uma linha sem quebrar o spinner. */
121
+ print(line = "") {
122
+ this.clear();
123
+ this.out(line + "\n");
124
+ this.draw();
125
+ }
126
+ // ---------- ciclo do turno ----------
127
+ turnStart() {
128
+ this.busy = true;
129
+ this.t0 = Date.now();
130
+ this.frame = 0;
131
+ if (isTTY)
132
+ this.timer = setInterval(() => { this.frame++; this.draw(); }, 90);
133
+ this.draw();
134
+ }
135
+ turnEnd(ok = true) {
136
+ this.textEnd();
137
+ this.busy = false;
138
+ this.running.clear();
139
+ this.activeSkill = "";
140
+ if (this.timer)
141
+ clearInterval(this.timer);
142
+ this.clear();
143
+ const ms = this.t0 ? Date.now() - this.t0 : 0;
144
+ this.t0 = 0;
145
+ this.totalMs += ms;
146
+ if (ok && ms)
147
+ this.print(c.dim(`${DONE[this.seed % DONE.length]} pronto! (${(ms / 1000).toFixed(1)}s · total ${fmtDur(this.totalMs)})`));
148
+ }
149
+ /** Libera o terminal para um prompt interativo (permissão). */
150
+ suspend() { this.paused = true; this.clear(); }
151
+ resume() { this.paused = false; this.draw(); }
152
+ // ---------- resposta do modelo ----------
153
+ lineW = 0;
154
+ word = "";
155
+ // markdown em streaming: b=**negrito**, i=*itálico*, k=`código`, fence=bloco ```
156
+ st = { b: false, i: false, k: false, fence: false };
157
+ wordStart = ""; // estilo ativo quando a palavra em andamento começou
158
+ run = { ch: "", n: 0, bol: false }; // sequência pendente de * ou ` (decidida pelo próximo caractere)
159
+ skip = false; // descarta o resto da linha do ``` (nome da linguagem)
160
+ bol = true; // só espaços até agora nesta linha
161
+ prev = " "; // último caractere resolvido
162
+ seq() {
163
+ const { b, i, k, fence } = this.st;
164
+ return k || fence ? CODE : (i ? ITALIC : "") + (b ? BOLD : "");
165
+ }
166
+ active() { const s = this.st; return s.b || s.i || s.k || s.fence; }
167
+ /** Aplica a mudança de estilo: no meio da palavra vira código ANSI; entre palavras vira o estilo inicial da próxima. */
168
+ restyle() {
169
+ if (this.word)
170
+ this.word += OFF + this.seq();
171
+ else
172
+ this.wordStart = this.seq();
173
+ }
174
+ resetStyle() {
175
+ this.st = { b: false, i: false, k: false, fence: false };
176
+ this.run = { ch: "", n: 0, bol: false };
177
+ this.wordStart = "";
178
+ this.skip = false;
179
+ this.bol = true;
180
+ this.prev = " ";
181
+ }
182
+ /** Descarrega a palavra em andamento, quebrando a linha antes dela se não couber. */
183
+ emitWord() {
184
+ if (!this.word)
185
+ return;
186
+ const w = stringWidth(this.word);
187
+ const room = cols() - 2;
188
+ if (this.lineW > 0 && this.lineW + w > room) {
189
+ this.out("\n");
190
+ this.lineStart = true;
191
+ this.lineW = 0;
192
+ }
193
+ if (this.lineStart) {
194
+ this.out(c.accent("│ "));
195
+ this.lineStart = false;
196
+ }
197
+ // o estilo é reaplicado por palavra: a borda e os espaços não herdam a cor
198
+ this.out(this.wordStart + this.word + (this.active() ? OFF : ""));
199
+ this.lineW += w;
200
+ this.word = "";
201
+ this.wordStart = this.seq();
202
+ }
203
+ /** Decide o que fazer com a sequência pendente de `*`/`` ` `` olhando o próximo caractere. */
204
+ resolve(next) {
205
+ const { ch, n, bol } = this.run;
206
+ this.run = { ch: "", n: 0, bol: false };
207
+ const s = this.st;
208
+ const lit = () => { for (let j = 0; j < n; j++)
209
+ this.put(ch); };
210
+ if (ch === "`") {
211
+ if (n >= 3 && bol) {
212
+ s.fence = !s.fence;
213
+ this.restyle();
214
+ this.skip = true;
215
+ return;
216
+ }
217
+ if (n <= 2 && !s.fence) {
218
+ s.k = !s.k;
219
+ this.restyle();
220
+ return;
221
+ }
222
+ return lit();
223
+ }
224
+ if (n > 3)
225
+ return lit();
226
+ const keys = n === 1 ? ["i"] : n === 2 ? ["b"] : ["b", "i"];
227
+ const nextWs = /\s/.test(next), prevWs = /\s/.test(this.prev);
228
+ if (keys.every((x) => s[x]) && !prevWs) {
229
+ keys.forEach((x) => (s[x] = false));
230
+ this.restyle();
231
+ }
232
+ else if (keys.every((x) => !s[x]) && !nextWs && !/[\p{L}\p{N}]/u.test(this.prev)) {
233
+ keys.forEach((x) => (s[x] = true));
234
+ this.restyle();
235
+ }
236
+ else
237
+ lit(); // `2 ** 3`, `a*b`, marcador solto: fica literal
238
+ }
239
+ feed(ch) {
240
+ if (ch === this.run.ch) {
241
+ this.run.n++;
242
+ return;
243
+ }
244
+ if (this.run.n)
245
+ this.resolve(ch);
246
+ if (this.skip) {
247
+ if (ch === "\n")
248
+ this.skip = false;
249
+ return;
250
+ }
251
+ const s = this.st;
252
+ const tick = ch === "`" && (!s.fence || this.bol);
253
+ const star = ch === "*" && !s.k && !s.fence;
254
+ if (tick || star) {
255
+ this.run = { ch, n: 1, bol: this.bol };
256
+ return;
257
+ }
258
+ this.put(ch);
259
+ }
260
+ put(ch) {
261
+ const blank = this.prev === "\n";
262
+ this.prev = ch;
263
+ if (ch === "\n") {
264
+ this.emitWord();
265
+ this.bol = true;
266
+ this.st.k = false; // código inline não atravessa linhas
267
+ if (blank && !this.st.fence) {
268
+ this.st.b = false;
269
+ this.st.i = false;
270
+ } // linha em branco encerra ênfase aberta
271
+ this.wordStart = this.seq();
272
+ this.out((this.lineStart ? c.accent("│") : "") + "\n");
273
+ this.lineStart = true;
274
+ this.lineW = 0;
275
+ }
276
+ else if (ch === " ") {
277
+ this.emitWord();
278
+ if (this.lineStart) {
279
+ this.out(c.accent("│ "));
280
+ this.lineStart = false;
281
+ }
282
+ if (this.lineW + 1 > cols() - 2) {
283
+ this.out("\n");
284
+ this.lineStart = true;
285
+ this.lineW = 0;
286
+ }
287
+ else {
288
+ this.out(" ");
289
+ this.lineW++;
290
+ }
291
+ }
292
+ else {
293
+ this.word += ch;
294
+ this.bol = false;
295
+ }
296
+ }
297
+ /** Texto do modelo em streaming, dentro da caixa, com quebra por palavra e borda em todas as linhas. */
298
+ text(chunk) {
299
+ if (!this.textOpen) {
300
+ this.clear();
301
+ const label = ` ${NAME} `;
302
+ this.out(c.accent("╭─") + c.bold(c.light(label)) + c.accent("─".repeat(Math.max(2, cols() - label.length - 2))) + "\n");
303
+ this.textOpen = true;
304
+ this.lineStart = true;
305
+ this.lineW = 0;
306
+ this.word = "";
307
+ this.resetStyle();
308
+ }
309
+ for (const ch of chunk)
310
+ this.feed(ch);
311
+ }
312
+ textEnd() {
313
+ if (!this.textOpen)
314
+ return;
315
+ if (this.run.n)
316
+ this.resolve("\n"); // marcador no fim da resposta: fecha se havia abertura, senão fica literal
317
+ this.emitWord();
318
+ this.resetStyle();
319
+ if (!this.lineStart)
320
+ this.out("\n");
321
+ this.out(c.accent("╰" + "─".repeat(cols() - 1)) + "\n");
322
+ this.textOpen = false;
323
+ this.draw();
324
+ }
325
+ // ---------- ferramentas ----------
326
+ toolStart(id, name, input) {
327
+ this.running.set(id, { name, input, t0: Date.now() });
328
+ if (name === "skill_load" && skillName(input))
329
+ this.activeSkill = skillName(input);
330
+ this.draw();
331
+ }
332
+ toolEnd(id, isError) {
333
+ const r = this.running.get(id);
334
+ if (!r)
335
+ return;
336
+ this.running.delete(id);
337
+ this.textEnd();
338
+ this.print(this.toolLine(r.name, r.input, Date.now() - r.t0, isError));
339
+ }
340
+ /** Só o prefixo + emoji + nome, sem argumento nem tempo. */
341
+ toolLabel(name) { return `${c.accent("┊")} ${emojiOf(name)} ${c.soft(name)}`; }
342
+ toolLine(name, input, ms, isError, indent = "") {
343
+ const arg = preview(input);
344
+ const mark = isError ? c.err("✗") : "";
345
+ return `${indent}${c.accent("┊")} ${emojiOf(name)} ${isError ? c.err(name) : c.soft(name)}${arg ? " " + c.muted(arg) : ""} ${c.dim(`(${fmtMs(ms)})`)} ${mark}`.trimEnd();
346
+ }
347
+ /** Linha de progresso de um subagente (indentada, com o rótulo dele). */
348
+ sub(label, text) { this.textEnd(); this.print(` ${c.accent("┊")} ${c.light(`[${label}]`)} ${text}`); }
349
+ /** Raciocínio do modelo (`/reasoning show`): caixa tênue; recorta em 10 linhas, salvo `/reasoning full`. */
350
+ thinking(text) {
351
+ if (!this.reasoning.show)
352
+ return;
353
+ const body = text.trim();
354
+ if (!body)
355
+ return;
356
+ this.textEnd();
357
+ const w = cols();
358
+ const wrapped = body.split("\n").flatMap((l) => wrapLine(l.trimEnd(), w - 4));
359
+ const MAX = 10;
360
+ const cut = !this.reasoning.full && wrapped.length > MAX;
361
+ const lines = cut ? wrapped.slice(0, MAX) : wrapped;
362
+ const label = " raciocínio ";
363
+ this.print(c.dim(`┌─${label}${"─".repeat(Math.max(0, w - 3 - label.length))}┐`));
364
+ for (const l of lines)
365
+ this.print(c.dim(`│ ${l}`));
366
+ if (cut)
367
+ this.print(c.dim(`│ … +${wrapped.length - MAX} linhas (/reasoning full mostra tudo)`));
368
+ this.print(c.dim(`└${"─".repeat(w - 2)}┘`));
369
+ }
370
+ info(msg) { this.textEnd(); this.print(c.muted(msg)); }
371
+ warn(msg) { this.textEnd(); this.print(c.rgb(palette.warn)(msg)); }
372
+ error(msg) { this.textEnd(); this.print(c.err(msg)); }
373
+ // ---------- estrutura fixa ----------
374
+ rule() { return c.accent("─".repeat(cols())); }
375
+ /**
376
+ * Barra de status: cada indicador tem um rótulo para quem não conhece os números.
377
+ * Se não couber na largura do terminal, descarta primeiro os de menor prioridade (tempo, extra, tokens…).
378
+ */
379
+ statusLine(s) {
380
+ const pct = Math.min(100, Math.round((s.ctxTokens / s.ctxWindow) * 100));
381
+ const slots = 8, filled = Math.round((pct / 100) * slots);
382
+ const colour = pct >= 95 ? c.err : pct >= 80 ? c.rgb(palette.orange) : pct >= 50 ? c.warn : c.ok;
383
+ const bar = colour("█".repeat(filled)) + c.dim("░".repeat(slots - filled));
384
+ const mins = Math.floor((Date.now() - s.startedAt) / 60_000);
385
+ const active = this.totalMs + (this.busy && this.t0 ? Date.now() - this.t0 : 0);
386
+ const label = (t) => c.dim(t + " ");
387
+ // Taxa de acerto do cache de prompt (quanto mais alta, mais barato): só aparece quando há dado, como no Hermes.
388
+ const hit = s.cacheRead && s.input > 0 ? Math.min(100, (s.cacheRead / s.input) * 100) : undefined;
389
+ const hitColour = hit === undefined ? c.muted : hit >= 70 ? c.ok : hit >= 40 ? c.warn : c.err;
390
+ // [prioridade (maior = descarta primeiro), texto]
391
+ const segs = [
392
+ ...(s.mode ? [[0, s.mode]] : []),
393
+ [0, c.light("⬢ ") + c.bold(c.soft(s.model))],
394
+ [0, label("contexto") + c.muted(`${fmtTokens(s.ctxTokens)}/${fmtTokens(s.ctxWindow)}`) + ` [${bar}] ${colour(`${pct}%`)}`],
395
+ ...(hit !== undefined ? [[2, label("cache") + hitColour(`◎ ${Math.round(hit)}%`)]] : []),
396
+ [3, label("tokens") + c.muted(`↑${fmtTokens(s.input)} ↓${fmtTokens(s.output)}`)],
397
+ ...(s.cost !== undefined ? [[1, label("custo") + c.muted(`$${s.cost.toFixed(2)}`)]] : []),
398
+ ...(this.activeSkill ? [[1, c.light("🎓 ") + label("skill") + c.bold(c.soft(this.activeSkill))]] : []),
399
+ [4, label("tempo") + c.muted(mins < 1 ? "<1m" : `${mins}m`) + (active >= 1000 ? c.dim(` (ativo ${fmtDur(active)})`) : "")],
400
+ ...(s.extra ? [[5, c.muted(s.extra)]] : []),
401
+ ];
402
+ const sep = c.accent(" │ ");
403
+ const render = () => segs.map(([, t]) => t).join(sep);
404
+ while (segs.length > 2 && stringWidth(render()) > cols()) {
405
+ const worst = segs.reduce((w, x, i) => (x[0] > segs[w][0] ? i : w), 0);
406
+ if (segs[worst][0] === 0)
407
+ break;
408
+ segs.splice(worst, 1);
409
+ }
410
+ return render();
411
+ }
412
+ /** Legenda dos indicadores (comando /indicadores). */
413
+ statusLegend() {
414
+ return [
415
+ ["contexto", "tokens que o modelo está lendo agora / tamanho máximo da janela. Perto de 100% a conversa precisa ser compactada."],
416
+ ["cache", "% do prompt reaproveitado do cache (◎). Alto (verde) = respostas mais rápidas e baratas. Some enquanto não há dado."],
417
+ ["tokens", "total da sessão: ↑ enviados ao modelo (inclui cache) · ↓ gerados por ele."],
418
+ ["custo", "gasto estimado da sessão em dólares."],
419
+ ["tempo", "há quanto tempo a sessão está aberta · (ativo) soma do tempo gasto em todos os turnos."],
420
+ ["skill", "skill carregada (skill_load) no turno em andamento."],
421
+ ].map(([k, v]) => ` ${c.soft(k.padEnd(9))} ${c.muted(v)}`);
422
+ }
423
+ banner(info) { this.out(renderBanner(info) + "\n"); }
424
+ }
425
+ export const ui = new UI();
426
+ export { c, useColor };
@@ -0,0 +1,36 @@
1
+ /** Diff de linhas (LCS) com contexto curto, para mostrar ao usuário o que muda numa skill. */
2
+ export function lineDiff(before, after, context = 2) {
3
+ const a = before.split("\n"), b = after.split("\n");
4
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
5
+ for (let i = a.length - 1; i >= 0; i--)
6
+ for (let j = b.length - 1; j >= 0; j--)
7
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
8
+ const ops = [];
9
+ let i = 0, j = 0;
10
+ while (i < a.length && j < b.length) {
11
+ if (a[i] === b[j]) {
12
+ ops.push({ t: " ", s: a[i] });
13
+ i++;
14
+ j++;
15
+ }
16
+ else if (lcs[i + 1][j] >= lcs[i][j + 1])
17
+ ops.push({ t: "-", s: a[i++] });
18
+ else
19
+ ops.push({ t: "+", s: b[j++] });
20
+ }
21
+ while (i < a.length)
22
+ ops.push({ t: "-", s: a[i++] });
23
+ while (j < b.length)
24
+ ops.push({ t: "+", s: b[j++] });
25
+ if (ops.every((o) => o.t === " "))
26
+ return [];
27
+ const keep = ops.map((o, k) => o.t !== " " || ops.slice(Math.max(0, k - context), k + context + 1).some((x) => x.t !== " "));
28
+ const out = [];
29
+ ops.forEach((o, k) => {
30
+ if (keep[k])
31
+ out.push(`${o.t} ${o.s}`);
32
+ else if (out[out.length - 1] !== " …")
33
+ out.push(" …");
34
+ });
35
+ return out;
36
+ }
@@ -0,0 +1,16 @@
1
+ export function parseFrontmatter(text) {
2
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
3
+ if (!m)
4
+ return null;
5
+ const meta = {};
6
+ for (const line of m[1].split(/\r?\n/)) {
7
+ const kv = line.match(/^([\w-]+):\s*(.*)$/);
8
+ if (kv)
9
+ meta[kv[1]] = kv[2].trim();
10
+ }
11
+ return { meta, body: m[2] };
12
+ }
13
+ export function stringifyFrontmatter(meta, body) {
14
+ const head = Object.entries(meta).map(([k, v]) => `${k}: ${String(v).replace(/\s*\n\s*/g, " ")}`).join("\n");
15
+ return `---\n${head}\n---\n${body.trim()}\n`;
16
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "brito-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "brito": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx src/cli.ts",
11
+ "build": "tsc",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "tsx --test test/*.test.ts",
14
+ "eval:learn": "tsx test/eval-learn.ts",
15
+ "prepare": "tsc",
16
+ "prepublishOnly": "npm run typecheck && npm test"
17
+ },
18
+ "dependencies": {
19
+ "@anthropic-ai/sdk": "^0.131.0",
20
+ "@modelcontextprotocol/sdk": "^1.32.0",
21
+ "commander": "latest",
22
+ "fast-glob": "latest",
23
+ "kuzu": "latest",
24
+ "string-width": "^8.3.0",
25
+ "tree-sitter-wasms": "latest",
26
+ "web-tree-sitter": "0.24.7",
27
+ "zod": "latest"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "latest",
31
+ "tsx": "latest",
32
+ "typescript": "latest"
33
+ },
34
+ "license": "PolyForm-Noncommercial-1.0.0",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/wilsonbrito/brito.git"
38
+ },
39
+ "homepage": "https://github.com/wilsonbrito/brito#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/wilsonbrito/brito/issues"
42
+ },
43
+ "author": "Wilson Brito",
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ ".brito/agents",
50
+ "README.md",
51
+ "README.pt-BR.md",
52
+ "LICENSE"
53
+ ],
54
+ "keywords": [
55
+ "cli",
56
+ "agent",
57
+ "coding-agent",
58
+ "llm",
59
+ "knowledge-graph",
60
+ "code-graph",
61
+ "mcp",
62
+ "claude",
63
+ "tree-sitter",
64
+ "svelte",
65
+ "typescript",
66
+ "python"
67
+ ]
68
+ }