sabia 1.1.0 → 1.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sabia.mjs +94 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sabia",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Instala, atualiza e desinstala o Sabiá e os toolkits dele no Kiro, no Claude Code e no opencode, com um inventário em ~/.sabia/.",
5
5
  "type": "module",
6
6
  "bin": {
package/sabia.mjs CHANGED
@@ -15,14 +15,73 @@ const VERSAO = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.
15
15
  const REPO = "https://github.com/dxaraujo/sabia.git";
16
16
  const INDICE = "https://github.com/dxaraujo/sabia-toolkits.git";
17
17
  const ALVOS = ["kiro", "claude", "opencode"];
18
- const BANNER = `
19
- ▄▄▀
20
- ███████╗ █████╗ ██████╗ ██╗ █████╗
21
- ██╔════╝██╔══██╗██╔══██╗██║██╔══██╗
22
- ███████╗███████║██████╔╝██║███████║
23
- ╚════██║██╔══██║██╔══██╗██║██╔══██║
24
- ███████║██║ ██║██████╔╝██║██║ ██║
25
- ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝`;
18
+ const NOME = [
19
+ " ▄▄▀",
20
+ "███████╗ █████╗ ██████╗ ██╗ █████╗",
21
+ "██╔════╝██╔══██╗██╔══██╗██║██╔══██╗",
22
+ "███████╗███████║██████╔╝██║███████║",
23
+ "╚════██║██╔══██║██╔══██╗██║██╔══██║",
24
+ "███████║██║ ██║██████╔╝██║██║ ██║",
25
+ "╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝",
26
+ ];
27
+ // o sabiá do sabia-site/logo.svg: as sete facetas (uma por etapa) e o bico, cinza para aparecer no fundo escuro
28
+ const SABIA = [
29
+ ["#00BBCE", [56, 57, 70, 42, 90, 38, 104, 46, 106, 62, 84, 74, 57, 70]],
30
+ ["#B2D235", [84, 74, 106, 62, 118, 74, 116, 92, 92, 94]],
31
+ ["#0786E3", [57, 70, 84, 74, 92, 94, 86, 122, 70, 112, 60, 92]],
32
+ ["#2C5168", [92, 94, 116, 92, 134, 108, 112, 124, 86, 122]],
33
+ ["#76B043", [86, 122, 112, 124, 132, 152, 118, 170, 94, 146]],
34
+ ["#FAA61A", [134, 108, 150, 128, 150, 154, 132, 152, 112, 124]],
35
+ ["#0094AD", [132, 152, 150, 154, 180, 196, 164, 198]],
36
+ ["#8A8F8C", [36, 64, 56, 57, 57, 70]],
37
+ ];
38
+
39
+ function noSabia(x, y, p) {
40
+ let d = false;
41
+ for (let i = 0, j = p.length - 2; i < p.length; j = i, i += 2)
42
+ if (p[i + 1] > y !== p[j + 1] > y && x < ((p[j] - p[i]) * (y - p[i + 1])) / (p[j + 1] - p[i + 1]) + p[i]) d = !d;
43
+ return d;
44
+ }
45
+
46
+ /** O sabiá em meio-blocos coloridos: cada caractere são dois pixels de `passo` unidades do SVG. */
47
+ function desenharSabia(cor, passo = 13) {
48
+ // pixel: a cor que mais aparece em 4x4 amostras; vazio se menos da metade cai no sabiá
49
+ const pixel = (cx, cy) => {
50
+ if (Math.hypot(cx - 82, cy - 54) < passo * 0.55) return "#0E1210"; // o olho
51
+ const n = {};
52
+ for (let i = 0; i < 16; i++) {
53
+ const x = cx + ((i % 4) + 0.5 - 2) * (passo / 4), y = cy + (Math.floor(i / 4) + 0.5 - 2) * (passo / 4);
54
+ const c = SABIA.find(([, p]) => noSabia(x, y, p))?.[0];
55
+ if (c) n[c] = (n[c] || 0) + 1;
56
+ }
57
+ const total = Object.values(n).reduce((a, b) => a + b, 0);
58
+ return total >= 8 ? Object.entries(n).sort((a, b) => b[1] - a[1])[0][0] : null;
59
+ };
60
+ const linhas = [];
61
+ for (let y = 36; y < 200; y += 2 * passo) {
62
+ let l = "";
63
+ for (let x = 34; x < 182; x += passo) {
64
+ const a = pixel(x + passo / 2, y + passo / 2), b = pixel(x + passo / 2, y + 1.5 * passo);
65
+ l += !a && !b ? " " : !b ? cor(a) + "▀\x1b[0m" : !a ? cor(b) + "▄\x1b[0m" : cor(a) + cor(b, true) + "▀\x1b[0m";
66
+ }
67
+ linhas.push(l);
68
+ }
69
+ while (!linhas.at(-1).trim()) linhas.pop();
70
+ return linhas;
71
+ }
72
+
73
+ /** Sem terminal ou com NO_COLOR, só o nome; senão o sabiá ao lado, em 24 bits ou nos 256 tons. */
74
+ export function banner({ cores = process.stdout.isTTY && !process.env.NO_COLOR, bits24 = /truecolor|24bit/.test(process.env.COLORTERM ?? "") } = {}) {
75
+ if (!cores) return "\n" + NOME.join("\n");
76
+ const cor = (h, fundo) => {
77
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
78
+ return `\x1b[${fundo ? 48 : 38};` + (bits24 ? `2;${r};${g};${b}m` : `5;${16 + 36 * Math.round(r / 51) + 6 * Math.round(g / 51) + Math.round(b / 51)}m`);
79
+ };
80
+ const desenho = desenharSabia(cor), largura = [...desenho[0]].filter((c) => " ▀▄".includes(c)).length;
81
+ const texto = [...NOME.map((l) => `\x1b[1m${l}\x1b[0m`), "", `\x1b[2mengenharia agêntica, da ideia ao resultado aceito v${VERSAO}\x1b[0m`];
82
+ return "\n" + texto.map((t, i) => (desenho[i] ?? " ".repeat(largura)) + " " + t).join("\n");
83
+ }
84
+ const BANNER = banner();
26
85
 
27
86
  // trocáveis nos testes
28
87
  export const ctx = {
@@ -301,7 +360,8 @@ export async function instalar(opcoes = {}) {
301
360
  } finally {
302
361
  gravarInventario(inv);
303
362
  }
304
- ctx.log('Pronto. No projeto, peça "configurar o Sabiá" (ou "atualizar os agentes do Sabiá").');
363
+ ctx.log('Pronto: skills e MCPs instalados. Os agentes são de cada projeto: abra a ferramenta na raiz dele, na sessão\n' +
364
+ 'padrão, e peça "configurar o Sabiá" (ou, num projeto já configurado, "atualizar os agentes do Sabiá").');
305
365
  }
306
366
 
307
367
  export async function atualizar(opcoes = {}) {
@@ -371,6 +431,8 @@ export function validarToolkit(pasta, nomeEsperado = null) {
371
431
  const nomeOk = (n) => typeof n === "string" && /^[a-z0-9][a-z0-9-]*$/.test(n) && !n.startsWith("sa-");
372
432
  if (!nomeOk(m.nome)) erros.push(`nome inválido (kebab-case, sem sa-): ${m.nome}`);
373
433
  if (nomeEsperado && m.nome !== nomeEsperado) erros.push(`nome ${m.nome} ≠ ${nomeEsperado} do índice`);
434
+ if (m.customiza && (!nomeOk(m.customiza.toolkit) || m.customiza.toolkit === m.nome || typeof m.customiza.versao !== "string"))
435
+ erros.push("customiza: precisa de toolkit (outro nome, kebab-case) e versao da base");
374
436
  const existe = (c, de) => {
375
437
  try { if (!fs.existsSync(path.join(pasta, dentro(c, de)))) erros.push(`${de}: não existe ${c}`); } catch (e) { erros.push(e.message); }
376
438
  };
@@ -453,6 +515,11 @@ export async function instalarToolkit(alvo, opcoes = {}) {
453
515
  const pasta = sabia("toolkits", baixado);
454
516
  const m = JSON.parse(fs.readFileSync(path.join(pasta, "toolkit.json"), "utf8"));
455
517
  const inv = lerInventario();
518
+ // a customização é uma cópia modificada da base: as duas juntas no projeto dão skills e agentes repetidos
519
+ const familia = m.customiza?.toolkit ?? baixado;
520
+ const rival = Object.keys(inv.toolkits).find((n) => n !== baixado && (baseDe(n) ?? n) === familia);
521
+ // mesmos nomes de skill e de agente: as duas no usuário se sobrescreveriam
522
+ if (rival) throw new Error(`${baixado} e ${rival} são versões do mesmo toolkit (base ${familia}); instale um só: sabia toolkits remover ${rival}`);
456
523
  const tk = (inv.toolkits[baixado] ||= { versao: m.versao, alvos: {} });
457
524
  const alvos = opcoes.alvo ? opcoes.alvo.split(",") : Object.keys(inv.alvos).length ? Object.keys(inv.alvos) : await escolherAlvos(opcoes);
458
525
  try {
@@ -504,6 +571,12 @@ export async function atualizarToolkits(nome, opcoes = {}) {
504
571
  }
505
572
  }
506
573
 
574
+ /** A base que o toolkit baixado customiza (do toolkit.json em ~/.sabia/toolkits), ou null. */
575
+ function baseDe(nome) {
576
+ const f = sabia("toolkits", nome, "toolkit.json");
577
+ return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")).customiza?.toolkit ?? null : null;
578
+ }
579
+
507
580
  export function listarToolkits(opcoes = {}) {
508
581
  const inv = lerInventario();
509
582
  const projeto = path.join(process.cwd(), ".sabia", "config", "agentes.json");
@@ -517,6 +590,18 @@ export function listarToolkits(opcoes = {}) {
517
590
  }
518
591
  }
519
592
  for (const [n, t] of Object.entries(inv.toolkits)) if (!linhas.some((l) => l[0] === n)) linhas.push([n, "—", t.versao, doProjeto[n]?.versao || "—", "(fora dos índices)"]);
593
+ // a customização logo abaixo da base, com ↳ e de onde veio
594
+ for (const l of [...linhas]) {
595
+ const f = sabia("toolkits", l[0], "toolkit.json");
596
+ const c = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")).customiza : null;
597
+ if (!c) continue;
598
+ linhas.splice(linhas.indexOf(l), 1);
599
+ const i = linhas.findIndex((b) => b[0] === c.toolkit);
600
+ // só a primeira mudança (quem customizou), como resumo; a lista inteira está no toolkit.json
601
+ l[4] = `customiza ${c.toolkit} ${c.versao}${c.mudancas?.length ? `: ${c.mudancas[0]}${c.mudancas.length > 1 ? "; …" : ""}` : ""}${l[4] ? " — " + l[4] : ""}`;
602
+ l[0] = `↳ ${l[0]}`;
603
+ linhas.splice(i < 0 ? linhas.length : i + 1, 0, l);
604
+ }
520
605
  ctx.log(["| Toolkit | No índice | Instalado | Neste projeto | Descrição |", "|---|---|---|---|---|",
521
606
  ...linhas.map((l) => `| ${l.join(" | ")} |`)].join("\n"));
522
607
  return linhas;