sabia 1.1.0 → 1.1.3
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/package.json +1 -1
- package/sabia.mjs +111 -15
package/package.json
CHANGED
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
|
|
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.
|
|
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 = {}) {
|
|
@@ -367,10 +427,15 @@ export function validarToolkit(pasta, nomeEsperado = null) {
|
|
|
367
427
|
let m;
|
|
368
428
|
try { m = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) { return [`toolkit.json inválido: ${e.message}`]; }
|
|
369
429
|
const erros = [];
|
|
370
|
-
for (const c of ["nome", "versao", "descricao", "tipos-de-projeto", "skills"]) if (!(c in m)) erros.push(`falta o campo ${c}`);
|
|
430
|
+
for (const c of ["nome", "versao", "descricao", "autor", "tipos-de-projeto", "skills"]) if (!(c in m)) erros.push(`falta o campo ${c}`);
|
|
431
|
+
// quem fez responde pelo toolkit: nome e área aparecem no listar e na setup
|
|
432
|
+
if (m.autor && (typeof m.autor.nome !== "string" || typeof m.autor.area !== "string" || !m.autor.nome.trim() || !m.autor.area.trim()))
|
|
433
|
+
erros.push("autor: precisa de nome e area");
|
|
371
434
|
const nomeOk = (n) => typeof n === "string" && /^[a-z0-9][a-z0-9-]*$/.test(n) && !n.startsWith("sa-");
|
|
372
435
|
if (!nomeOk(m.nome)) erros.push(`nome inválido (kebab-case, sem sa-): ${m.nome}`);
|
|
373
436
|
if (nomeEsperado && m.nome !== nomeEsperado) erros.push(`nome ${m.nome} ≠ ${nomeEsperado} do índice`);
|
|
437
|
+
if (m.customiza && (!nomeOk(m.customiza.toolkit) || m.customiza.toolkit === m.nome || typeof m.customiza.versao !== "string"))
|
|
438
|
+
erros.push("customiza: precisa de toolkit (outro nome, kebab-case) e versao da base");
|
|
374
439
|
const existe = (c, de) => {
|
|
375
440
|
try { if (!fs.existsSync(path.join(pasta, dentro(c, de)))) erros.push(`${de}: não existe ${c}`); } catch (e) { erros.push(e.message); }
|
|
376
441
|
};
|
|
@@ -401,7 +466,7 @@ export function lerIndice(fonte) {
|
|
|
401
466
|
const m = JSON.parse(fs.readFileSync(path.join(raiz, ".claude-plugin", "marketplace.json"), "utf8"));
|
|
402
467
|
return (m.plugins || []).map((p) => {
|
|
403
468
|
const s = p.source;
|
|
404
|
-
const base = { nome: p.name, versao: p.version, descricao: p.description, indice: fonte };
|
|
469
|
+
const base = { nome: p.name, versao: p.version, descricao: p.description, customiza: p.customiza, autor: p.autor, indice: fonte };
|
|
405
470
|
if (typeof s === "string") return { ...base, url: local ? path.resolve(raiz, dentro(s.replace(/^\.\//, ""), fonte)) : fonte, subpasta: local ? null : s.replace(/^\.\//, "") };
|
|
406
471
|
if (s?.source === "github") return { ...base, url: `https://github.com/${s.repo}.git`, ref: s.ref, sha: s.sha };
|
|
407
472
|
if (s?.source === "url") return { ...base, url: s.url, ref: s.ref, sha: s.sha };
|
|
@@ -453,6 +518,11 @@ export async function instalarToolkit(alvo, opcoes = {}) {
|
|
|
453
518
|
const pasta = sabia("toolkits", baixado);
|
|
454
519
|
const m = JSON.parse(fs.readFileSync(path.join(pasta, "toolkit.json"), "utf8"));
|
|
455
520
|
const inv = lerInventario();
|
|
521
|
+
// a customização é uma cópia modificada da base: as duas juntas no projeto dão skills e agentes repetidos
|
|
522
|
+
const familia = m.customiza?.toolkit ?? baixado;
|
|
523
|
+
const rival = Object.keys(inv.toolkits).find((n) => n !== baixado && (baseDe(n) ?? n) === familia);
|
|
524
|
+
// mesmos nomes de skill e de agente: as duas no usuário se sobrescreveriam
|
|
525
|
+
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
526
|
const tk = (inv.toolkits[baixado] ||= { versao: m.versao, alvos: {} });
|
|
457
527
|
const alvos = opcoes.alvo ? opcoes.alvo.split(",") : Object.keys(inv.alvos).length ? Object.keys(inv.alvos) : await escolherAlvos(opcoes);
|
|
458
528
|
try {
|
|
@@ -504,20 +574,46 @@ export async function atualizarToolkits(nome, opcoes = {}) {
|
|
|
504
574
|
}
|
|
505
575
|
}
|
|
506
576
|
|
|
577
|
+
/** A base que o toolkit baixado customiza (do toolkit.json em ~/.sabia/toolkits), ou null. */
|
|
578
|
+
function baseDe(nome) {
|
|
579
|
+
const f = sabia("toolkits", nome, "toolkit.json");
|
|
580
|
+
return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")).customiza?.toolkit ?? null : null;
|
|
581
|
+
}
|
|
582
|
+
|
|
507
583
|
export function listarToolkits(opcoes = {}) {
|
|
508
584
|
const inv = lerInventario();
|
|
509
585
|
const projeto = path.join(process.cwd(), ".sabia", "config", "agentes.json");
|
|
510
586
|
const doProjeto = fs.existsSync(projeto) ? JSON.parse(fs.readFileSync(projeto, "utf8")).toolkits || {} : {};
|
|
511
|
-
|
|
587
|
+
// autor e customiza: do toolkit baixado (o toolkit.json) ou, sem ele, da entrada do índice
|
|
588
|
+
const manifesto = (n) => { const f = sabia("toolkits", n, "toolkit.json"); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : {}; };
|
|
589
|
+
const linhas = [], doIndice = {};
|
|
512
590
|
for (const i of opcoes.indice ? [opcoes.indice] : indices()) {
|
|
513
591
|
try {
|
|
514
|
-
for (const e of lerIndice(i))
|
|
592
|
+
for (const e of lerIndice(i)) {
|
|
593
|
+
doIndice[e.nome] = e;
|
|
594
|
+
linhas.push([e.nome, "", e.versao || "—", inv.toolkits[e.nome]?.versao || "—", doProjeto[e.nome]?.versao || "—", e.descricao || ""]);
|
|
595
|
+
}
|
|
515
596
|
} catch (err) {
|
|
516
597
|
ctx.log(`índice ${i}: ${err.message}`);
|
|
517
598
|
}
|
|
518
599
|
}
|
|
519
|
-
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)"]);
|
|
520
|
-
|
|
600
|
+
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)"]);
|
|
601
|
+
for (const l of linhas) {
|
|
602
|
+
const a = manifesto(l[0]).autor ?? doIndice[l[0]]?.autor;
|
|
603
|
+
l[1] = a ? `${a.nome} (${a.area})` : "—";
|
|
604
|
+
}
|
|
605
|
+
// a customização logo abaixo da base, com ↳ e de onde veio
|
|
606
|
+
for (const l of [...linhas]) {
|
|
607
|
+
const c = manifesto(l[0]).customiza ?? doIndice[l[0]]?.customiza;
|
|
608
|
+
if (!c) continue;
|
|
609
|
+
linhas.splice(linhas.indexOf(l), 1);
|
|
610
|
+
const i = linhas.findIndex((b) => b[0] === c.toolkit);
|
|
611
|
+
// só a primeira mudança (quem customizou), como resumo; a lista inteira está no toolkit.json
|
|
612
|
+
l[5] = `customiza ${c.toolkit} ${c.versao}${c.mudancas?.length ? `: ${c.mudancas[0]}${c.mudancas.length > 1 ? "; …" : ""}` : ""}${l[5] ? " — " + l[5] : ""}`;
|
|
613
|
+
l[0] = `↳ ${l[0]}`;
|
|
614
|
+
linhas.splice(i < 0 ? linhas.length : i + 1, 0, l);
|
|
615
|
+
}
|
|
616
|
+
ctx.log(["| Toolkit | Autor | No índice | Instalado | Neste projeto | Descrição |", "|---|---|---|---|---|---|",
|
|
521
617
|
...linhas.map((l) => `| ${l.join(" | ")} |`)].join("\n"));
|
|
522
618
|
return linhas;
|
|
523
619
|
}
|