terminal-smart-cli 0.64.0 → 0.66.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/bin/ts.js CHANGED
@@ -1183,6 +1183,14 @@ async function metaCmd() {
1183
1183
  const intervaloMin = flagNum('--intervalo', 5); // min de espera entre janelas ao aguardar crédito
1184
1184
  // Anti-loop-infinito: se ninguém definiu teto, cai num limite de janelas seguro.
1185
1185
  const maxJanelasEff = maxJanelas || ((budgetTotal || maxHoras) ? 0 : 20);
1186
+ // CRITÉRIOS EXECUTÁVEIS (verify): --criterios <arquivo.json> = a PROVA de que a missão
1187
+ // cumpriu o pedido (não basta compilar/abrir). Falhou → missão sai "NÃO verificada".
1188
+ let criteria = [];
1189
+ const _critFile = (() => { const i = rawArgs.findIndex(a => a === '--criterios' || a === '--criteria'); return i >= 0 ? rawArgs[i + 1] : null; })();
1190
+ if (_critFile) {
1191
+ try { const j = JSON.parse(require('fs').readFileSync(_critFile, 'utf8')); criteria = Array.isArray(j) ? j : (j.criteria || []); }
1192
+ catch (e) { console.error(ui.errLine((cfg.lang === 'en' ? 'criteria file: ' : 'arquivo de critérios: ') + (e.message || e))); process.exit(2); }
1193
+ }
1186
1194
 
1187
1195
  const existing = metaMod.load(dir);
1188
1196
  if (FLAGS.has('--status')) {
@@ -1229,7 +1237,7 @@ async function metaCmd() {
1229
1237
  const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
1230
1238
  try {
1231
1239
  st = await metaMod.run(goalArg, {
1232
- token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch,
1240
+ token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria,
1233
1241
  onAlert: ({ type, text }) => {
1234
1242
  sp.stop();
1235
1243
  const ic = type === 'human' ? C.warn('🙋') : type === 'escalate' ? C.indigo('🧠') : type === 'stagnated' ? C.err('🛑') : type === 'design' ? C.cyan('🎨') : type === 'retry' || type === 'conn' ? C.warn('📡') : C.warn('⏱');
@@ -1304,6 +1312,11 @@ async function metaCmd() {
1304
1312
  const _vtag = (_vl.ok ? C.ok : C.warn)(` · ${_vl.icon} ${_vl.txt}`);
1305
1313
  console.log((_vl.ok ? ui.okLine : ui.infoLine)(C.bold(T.meta_done) + C.dim(` · ${st.rounds.length} rodada(s) · ${st.creditsSpent} créditos · ${secs} min` + (st.escalations ? ` · ${st.escalations} escalonamento(s)` : '')) + _vtag));
1306
1314
  if (!_vl.ok) console.log(' ' + C.dim((cfg.lang === 'en' ? 'Heads up: the deliverable was produced but NOT fully proven — check it before relying on it.' : 'Atenção: o entregável foi produzido mas NÃO foi totalmente provado — confira antes de confiar.') + (st.runNote ? ' (' + String(st.runNote).trim() + ')' : '')));
1315
+ // Evidence Ledger da missão: mostra cada critério executável provado/falho.
1316
+ if (st.verifyReport && st.verifyReport.total) {
1317
+ console.log(' ' + C.dim((cfg.lang === 'en' ? 'Criteria: ' : 'Critérios: ') + st.verifyReport.passed + '/' + st.verifyReport.total));
1318
+ for (const r of st.verifyReport.results) console.log(' ' + (r.ok ? C.ok('✔') : C.err('✗')) + ' ' + C.dim((r.label || '') + (r.ok ? '' : ' — ' + (r.detail || ''))));
1319
+ }
1307
1320
  metaMod.notify(token, T.meta_notify_done(st.goal, done, st.creditsSpent) + (_vl.ok ? '' : `\n⚠️ ${_vl.txt}`));
1308
1321
  } else if (st.pause_reason === 'awaiting_human' && st.humanRequest) {
1309
1322
  console.log(ui.infoLine(C.bold('🙋 Preciso de você: ') + st.humanRequest.motivo));
@@ -1671,6 +1684,49 @@ function buscarCmd(args) {
1671
1684
  ], { title: 'ts buscar' }));
1672
1685
  }
1673
1686
 
1687
+ // ── ts verificar — PROVA determinística de que a tarefa terminou (verificadores) ──
1688
+ async function verificarCmd(args) {
1689
+ const en = (cfg.lang === 'en');
1690
+ const _fs = require('fs');
1691
+ const verify = require('../lib/verify');
1692
+ const _val = (names) => { const i = rawArgs.findIndex(a => names.includes(a)); return i >= 0 ? rawArgs[i + 1] : null; };
1693
+ const cwd = process.cwd();
1694
+
1695
+ // 1) arquivo de critérios (JSON: array ou {criteria:[...]}) OU 2) critérios inline via flags
1696
+ let criteria = [];
1697
+ const file = (args || []).find(a => !a.startsWith('-') && /\.json$/i.test(a));
1698
+ if (file) {
1699
+ try { const j = JSON.parse(_fs.readFileSync(file, 'utf8')); criteria = Array.isArray(j) ? j : (j.criteria || []); }
1700
+ catch (e) { console.log(ui.errLine((en ? 'could not read criteria file: ' : 'não deu pra ler o arquivo de critérios: ') + (e.message || e))); process.exit(2); }
1701
+ } else {
1702
+ const cmd = _val(['--cmd', '--comando']); if (cmd) criteria.push({ type: 'command', cmd, exit: Number(_val(['--exit', '--codigo'])) || 0 });
1703
+ const ex = _val(['--existe', '--exists']); if (ex) criteria.push({ type: 'file_exists', path: ex });
1704
+ const arq = _val(['--arquivo', '--file']); const cont = _val(['--contem', '--contains']);
1705
+ if (arq && cont != null) criteria.push({ type: 'file_contains', path: arq, text: cont });
1706
+ const url = _val(['--url']); if (url) criteria.push({ type: 'http', url, status: Number(_val(['--status'])) || 200, ...(cont != null && !arq ? { contains: cont } : {}) });
1707
+ const porta = _val(['--porta', '--port']); if (porta) criteria.push({ type: 'port', port: Number(porta) });
1708
+ }
1709
+ if (!criteria.length) {
1710
+ console.log(ui.infoLine(en
1711
+ ? 'usage: ts verificar criterios.json OR ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'
1712
+ : 'uso: ts verificar criterios.json OU ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'));
1713
+ return;
1714
+ }
1715
+
1716
+ if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
1717
+ const rep = await verify.runAll(criteria, { cwd });
1718
+ if (JSON_OUT) { console.log(JSON.stringify(rep)); process.exit(rep.allOk ? 0 : 1); }
1719
+
1720
+ for (const r of rep.results) {
1721
+ const mark = r.ok ? C.ok('✔') : C.err('✗');
1722
+ console.log(' ' + mark + ' ' + C.bold(r.label || r.type) + C.dim(' ' + (r.detail || '')) + (r.errorClass ? C.dim(' [' + r.errorClass + ']') : ''));
1723
+ if (!r.ok && r.evidence) console.log(' ' + C.dim('┄ ' + String(r.evidence).slice(0, 120)));
1724
+ }
1725
+ console.log('\n' + (rep.allOk ? ui.okLine : ui.errLine)(
1726
+ (rep.allOk ? (en ? 'VERIFIED' : 'VERIFICADO') : (en ? 'NOT verified' : 'NÃO verificado')) + C.dim(` ${rep.passed}/${rep.total} ` + (en ? 'passed' : 'ok'))));
1727
+ process.exit(rep.allOk ? 0 : 1);
1728
+ }
1729
+
1674
1730
  // ── ts perfil — personas com COFRE DE MEMÓRIA isolado (Mempalace/Hermes) ──────
1675
1731
  function perfilCmd(args) {
1676
1732
  const en = (cfg.lang === 'en');
@@ -1933,6 +1989,7 @@ function recallCmd(args) {
1933
1989
  case 'indexar': case 'index': return indexarCmd();
1934
1990
  case 'buscar': case 'search': case 'procurar': return buscarCmd(POS.slice(1));
1935
1991
  case 'perfil': case 'persona': case 'profile': return perfilCmd(POS.slice(1));
1992
+ case 'verificar': case 'verify': case 'provar': return verificarCmd(POS.slice(1));
1936
1993
  case 'meta': case 'missao': case 'mission': return metaCmd();
1937
1994
  case 'runs': return runsCmd();
1938
1995
  case 'status': return statusCmd(POS[1]);
package/lib/i18n.js CHANGED
@@ -43,8 +43,10 @@ const STR = {
43
43
  ['ts agente --continuar "..."', 'retoma o trabalho anterior desta pasta'],
44
44
  ['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
45
45
  ['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
46
+ ['ts meta "..." --criterios prova.json', 'só declara "verificada" se os critérios executáveis (verify) passarem de verdade'],
46
47
  ['ts meta --status', 'estado da missão deste diretório'],
47
48
  ['ts eval suite.json', 'avalia o agente numa suíte de casos (juiz de IA + nota)'],
49
+ ['ts verificar --cmd "npm test" --url ... --porta N', 'PROVA determinística de conclusão (build/http/porta/arquivo); exit 1 pra CI'],
48
50
  ['ts acp', 'servidor Agent Client Protocol (conecta o ts a editores tipo Zed)'],
49
51
  ['ts mcp', 'servidores MCP: as ferramentas deles entram no agente'],
50
52
  ] },
package/lib/meta.js CHANGED
@@ -1001,6 +1001,20 @@ async function makeChecklist(goal, token) {
1001
1001
  * onStep/askApprove/onRemote (repassados pro agente)
1002
1002
  * Retorna o estado final {status: done|paused, ...}.
1003
1003
  */
1004
+ // CRITÉRIOS EXECUTÁVEIS (verify): a prova FINAL da missão. Rodados quando os gates de
1005
+ // build/run passam, ANTES de declarar 'verified'. Se algum critério do usuário falha, a
1006
+ // missão NÃO é verificada (honestidade — não basta compilar/abrir; tem que CUMPRIR o pedido).
1007
+ // Sem critérios → nada a provar (retorna true). runAll nunca lança.
1008
+ async function _checkCriteria(st, dir) {
1009
+ if (!st.criteria || !st.criteria.length) return true;
1010
+ try {
1011
+ const rep = await require('./verify').runAll(st.criteria, { cwd: dir });
1012
+ st.verifyReport = { passed: rep.passed, total: rep.total, allOk: rep.allOk, results: rep.results.map(r => ({ ok: r.ok, label: r.label, detail: r.detail })) };
1013
+ if (!rep.allOk) st.runNote = (st.runNote || '') + ` critérios: ${rep.passed}/${rep.total} passaram · falharam: ${rep.results.filter(r => !r.ok).map(r => r.label).slice(0, 3).join(' | ')}`;
1014
+ return rep.allOk;
1015
+ } catch (_) { return true; }
1016
+ }
1017
+
1004
1018
  async function run(goal, opts = {}) {
1005
1019
  const { token, lang = 'pt', yes = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true } = opts;
1006
1020
  const onChecklist = opts.onChecklist || (() => {});
@@ -1043,7 +1057,7 @@ async function run(goal, opts = {}) {
1043
1057
  }
1044
1058
  const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token);
1045
1059
  st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
1046
- budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, created_at: new Date().toISOString() };
1060
+ budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: Array.isArray(opts.criteria) ? opts.criteria : [], created_at: new Date().toISOString() };
1047
1061
  save(st, dir);
1048
1062
  } else {
1049
1063
  st.status = 'running';
@@ -1126,9 +1140,9 @@ async function run(goal, opts = {}) {
1126
1140
  // Aprovado: impecável OU só com nitpicks cosméticos (não vale queimar rodada cara).
1127
1141
  st.visualOk = true;
1128
1142
  if (vg.severity === 'ressalvas') { st.runNote = (st.runNote || '') + ' visual aprovado com ressalvas cosméticas (sem defeito estrutural)'; onAlert({ type: 'design', text: `👁 ts: visual APROVADO — sobrou só nitpick cosmético (sem defeito estrutural). Não vou gastar polimento caro à toa.` }); }
1129
- st.verification = 'verified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
1143
+ st.verification = (await _checkCriteria(st, dir)) ? 'verified' : 'unverified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
1130
1144
  }
1131
- } else { st.visualOk = true; st.verification = 'verified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st; }
1145
+ } else { st.visualOk = true; st.verification = (await _checkCriteria(st, dir)) ? 'verified' : 'unverified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st; }
1132
1146
  } else {
1133
1147
  // CRASHOU ao abrir → trata como erro pro executor corrigir (loop igual ao build)
1134
1148
  st.buildFixes = (st.buildFixes || 0) + 1;
@@ -1143,7 +1157,8 @@ async function run(goal, opts = {}) {
1143
1157
  let wg = { ok: true }; try { wg = await webRunGate(b, { token, eye, eyeGate: !!eye && (st.visualFixes || 0) < MAX_VISUAL_FIXES }); } catch (_) {}
1144
1158
  st.creditsSpent += wg.credits || 0;
1145
1159
  if (wg.ok) {
1146
- st.buildVerified = true; st.runVerified = true; st.visualOk = true; st.verification = 'verified';
1160
+ st.buildVerified = true; st.runVerified = true; st.visualOk = true;
1161
+ st.verification = (await _checkCriteria(st, dir)) ? 'verified' : 'unverified';
1147
1162
  if (wg.shot) st.webShot = wg.shot;
1148
1163
  st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
1149
1164
  } else {
@@ -1381,4 +1396,4 @@ function verificationLabel(ver, lang) {
1381
1396
  }
1382
1397
  }
1383
1398
 
1384
- module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel };
1399
+ module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel, _checkCriteria };
package/lib/verify.js ADDED
@@ -0,0 +1,104 @@
1
+ // VERIFICADORES POR DOMÍNIO — a PROVA determinística de que uma tarefa terminou.
2
+ // Insight das análises externas (GPT): "o modelo propõe; o harness observa e COMPROVA".
3
+ // Cada critério é EXECUTÁVEL e checado por código (não pela narrativa do modelo nem por um
4
+ // LLM-judge). É a base do TaskSpec (critérios executáveis) e do Evidence Ledger, e o motor
5
+ // do comando `ts verificar` (o "Prove que terminou"). Usa o classificador de erro do core.
6
+ 'use strict';
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const net = require('net');
10
+ const { execSync } = require('child_process');
11
+ const core = require('./core');
12
+
13
+ // ── verificadores individuais (cada um devolve { ok, detail }) ───────────────
14
+ function _abs(cwd, p) { return path.isAbsolute(p) ? p : path.join(cwd || process.cwd(), p); }
15
+
16
+ function vCommand(c, cwd) {
17
+ const want = c.exit != null ? Number(c.exit) : 0;
18
+ try {
19
+ const out = execSync(c.cmd, { cwd, encoding: 'utf8', timeout: (c.timeout_s || 120) * 1000, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
20
+ const ok = want === 0;
21
+ return { ok, detail: ok ? 'exit 0' : `exit 0 mas esperava ${want}`, evidence: String(out).slice(-200).replace(/\s+/g, ' ') };
22
+ } catch (e) {
23
+ const code = typeof e.status === 'number' ? e.status : 1;
24
+ const ok = code === want;
25
+ const cls = core.classifyError(e.stderr || e.stdout || e.message);
26
+ return { ok, detail: ok ? `exit ${code} (esperado)` : `exit ${code} (esperava ${want})`, errorClass: ok ? null : cls, evidence: String(e.stderr || e.stdout || e.message || '').slice(-200).replace(/\s+/g, ' ') };
27
+ }
28
+ }
29
+ function vFileExists(c, cwd) { const p = _abs(cwd, c.path); const ok = fs.existsSync(p); return { ok, detail: ok ? 'existe' : 'não existe: ' + c.path }; }
30
+ function vFileAbsent(c, cwd) { const p = _abs(cwd, c.path); const ok = !fs.existsSync(p); return { ok, detail: ok ? 'ausente (ok)' : 'existe (não deveria): ' + c.path }; }
31
+ function vFileContains(c, cwd) {
32
+ const p = _abs(cwd, c.path);
33
+ let src = null; try { src = fs.readFileSync(p, 'utf8'); } catch (_) { return { ok: false, detail: 'não deu pra ler: ' + c.path }; }
34
+ const hit = c.regex ? new RegExp(c.text, c.flags || '').test(src) : src.includes(c.text);
35
+ const want = c.absent ? !hit : hit;
36
+ return { ok: want, detail: want ? 'ok' : (c.absent ? `contém "${c.text}" (não deveria)` : `NÃO contém "${c.text}"`) };
37
+ }
38
+ async function vHttp(c) {
39
+ const want = c.status != null ? Number(c.status) : 200;
40
+ const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), (c.timeout_s || 15) * 1000);
41
+ try {
42
+ const res = await fetch(c.url, { method: c.method || 'GET', signal: ctrl.signal, headers: c.headers || {}, ...(c.body ? { body: typeof c.body === 'string' ? c.body : JSON.stringify(c.body) } : {}) });
43
+ let body = ''; try { body = await res.text(); } catch (_) {}
44
+ const statusOk = res.status === want;
45
+ const containsOk = c.contains ? body.includes(c.contains) : true;
46
+ const ok = statusOk && containsOk;
47
+ return { ok, detail: ok ? `HTTP ${res.status}` : (!statusOk ? `HTTP ${res.status} (esperava ${want})` : `HTTP ${res.status} mas sem "${c.contains}"`), evidence: body.slice(0, 160).replace(/\s+/g, ' ') };
48
+ } catch (e) { return { ok: false, detail: 'sem resposta: ' + (e.message || e), errorClass: 'network' }; }
49
+ finally { clearTimeout(t); }
50
+ }
51
+ function vPort(c) {
52
+ return new Promise((resolve) => {
53
+ const host = c.host || '127.0.0.1'; const port = Number(c.port);
54
+ const sock = new net.Socket(); let done = false;
55
+ const finish = (ok, detail) => { if (done) return; done = true; try { sock.destroy(); } catch (_) {} resolve({ ok, detail }); };
56
+ sock.setTimeout((c.timeout_s || 5) * 1000);
57
+ sock.once('connect', () => finish(true, `porta ${port} escutando`));
58
+ sock.once('timeout', () => finish(false, `porta ${port} sem resposta (timeout)`));
59
+ sock.once('error', () => finish(false, `porta ${port} fechada/recusada`));
60
+ sock.connect(port, host);
61
+ });
62
+ }
63
+
64
+ const VERIFIERS = {
65
+ command: (c, cwd) => vCommand(c, cwd),
66
+ file_exists: (c, cwd) => vFileExists(c, cwd),
67
+ file_absent: (c, cwd) => vFileAbsent(c, cwd),
68
+ file_contains: (c, cwd) => vFileContains(c, cwd),
69
+ http: (c) => vHttp(c),
70
+ port: (c) => vPort(c),
71
+ };
72
+ const VERIFIER_TYPES = Object.keys(VERIFIERS);
73
+
74
+ // Roda UM critério. Sempre resolve { ok, type, detail, ... } — nunca lança.
75
+ async function runCriterion(c, opts = {}) {
76
+ const cwd = opts.cwd || process.cwd();
77
+ const type = c && c.type;
78
+ const fn = VERIFIERS[type];
79
+ if (!fn) return { ok: false, type: type || '?', detail: 'tipo de critério desconhecido: ' + type, unknown: true };
80
+ try { const r = await fn(c, cwd); return Object.assign({ type, label: c.label || _labelFor(c) }, r); }
81
+ catch (e) { return { ok: false, type, label: c.label || _labelFor(c), detail: 'erro no verificador: ' + (e.message || e) }; }
82
+ }
83
+ function _labelFor(c) {
84
+ switch (c.type) {
85
+ case 'command': return '`' + String(c.cmd).slice(0, 50) + '` → exit ' + (c.exit != null ? c.exit : 0);
86
+ case 'file_exists': return 'existe ' + c.path;
87
+ case 'file_absent': return 'ausente ' + c.path;
88
+ case 'file_contains': return c.path + (c.absent ? ' SEM ' : ' contém ') + '"' + String(c.text).slice(0, 30) + '"';
89
+ case 'http': return (c.method || 'GET') + ' ' + c.url + ' → ' + (c.status || 200);
90
+ case 'port': return 'porta ' + c.port + ' escutando';
91
+ default: return c.type || '?';
92
+ }
93
+ }
94
+
95
+ // Roda uma LISTA de critérios. Retorna resumo + resultados (o Evidence Ledger mínimo).
96
+ async function runAll(criteria, opts = {}) {
97
+ const list = Array.isArray(criteria) ? criteria : (criteria && Array.isArray(criteria.criteria) ? criteria.criteria : []);
98
+ const results = [];
99
+ for (const c of list) results.push(await runCriterion(c, opts));
100
+ const passed = results.filter(r => r.ok).length;
101
+ return { allOk: results.length > 0 && passed === results.length, passed, total: results.length, results };
102
+ }
103
+
104
+ module.exports = { runCriterion, runAll, VERIFIERS, VERIFIER_TYPES, _labelFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.64.0",
3
+ "version": "0.66.0",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"