terminal-smart-cli 0.49.0 → 0.50.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.
package/bin/ts.js CHANGED
@@ -1333,6 +1333,60 @@ async function metaCmd() {
1333
1333
  }
1334
1334
 
1335
1335
  // ── Uso / conta / idioma ─────────────────────────────────────────────────────
1336
+ // ── ts diagnosticar — MODO INVESTIGAÇÃO (hipótese→sonda→veredito→causa raiz) ──
1337
+ async function diagnosticarCmd(args) {
1338
+ const token = needToken();
1339
+ const en = (cfg.lang === 'en');
1340
+ const _val = (names) => { const i = rawArgs.findIndex(a => names.includes(a)); return i >= 0 ? rawArgs[i + 1] : null; };
1341
+ const target = _val(['--remoto', '--remote', '--ssh']) || ''; // ex: "ssh -i key user@host"
1342
+ const model = _val(['--modelo', '--model']) || null;
1343
+ const rounds = Number(_val(['--rodadas', '--rounds'])) || 12;
1344
+ // sintoma = positionais, exceto os valores consumidos por flags (--remoto/--modelo/--rodadas)
1345
+ const consumed = new Set([target, model, String(rounds)].filter(Boolean));
1346
+ const sym = args.filter(a => !a.startsWith('-') && !consumed.has(a)).join(' ').trim();
1347
+ if (!sym) { console.log(ui.errLine(en ? 'usage: ts diagnosticar "<symptom/error>" [--remoto "ssh user@host"]' : 'uso: ts diagnosticar "<sintoma/erro>" [--remoto "ssh user@host"]')); return; }
1348
+
1349
+ console.log('\n' + ui.box([
1350
+ C.bold(en ? 'Diagnosis mode' : 'Modo diagnóstico') + C.dim(en ? ' — hypothesis → probe → verdict' : ' — hipótese → sonda → veredito'),
1351
+ C.dim(en ? 'symptom: ' : 'sintoma: ') + sym.slice(0, 60) + (sym.length > 60 ? '…' : ''),
1352
+ target ? C.dim(en ? 'target: ' : 'alvo: ') + target : C.dim(en ? 'target: this machine' : 'alvo: esta máquina'),
1353
+ ], { title: 'ts' }) + '\n');
1354
+
1355
+ const diag = require('../lib/diagnose');
1356
+ const catIcon = { environment: '🌐', config: '⚙', permission: '🔒', resource: '📦', network: '🔌', data: '🗄', version: '🔀', logic: '🧩', unknown: '·' };
1357
+ const r = await diag.diagnose(sym, {
1358
+ token, lang: cfg.lang || 'pt', model, target, maxRounds: rounds,
1359
+ onEvent: (e) => {
1360
+ if (e.type === 'think') console.log(' ' + C.dim('R' + e.round) + ' ' + (catIcon[e.category] || '·') + ' ' + C.bold(e.hypothesis || '—') + (e.confidence ? C.dim(' ' + e.confidence + '%') : ''));
1361
+ else if (e.type === 'probe') console.log(' ' + C.dim('$ ') + e.probe.slice(0, 90));
1362
+ else if (e.type === 'blocked') console.log(' ' + C.err(en ? 'probe blocked (not read-only)' : 'sonda bloqueada (não é só-leitura)'));
1363
+ else if (e.type === 'skip') console.log(' ' + C.dim(en ? 'repeated probe skipped' : 'sonda repetida pulada'));
1364
+ else if (e.type === 'stuck') console.log(' ' + C.dim(en ? 'exhausted hypotheses' : 'hipóteses esgotadas'));
1365
+ },
1366
+ });
1367
+
1368
+ console.log('');
1369
+ if (r.status === 'no_credits') { console.log(ui.errLine(T.no_credits)); return; }
1370
+ if (r.status === 'solved') {
1371
+ console.log(ui.box([
1372
+ C.ok(en ? 'ROOT CAUSE FOUND' : 'CAUSA RAIZ ENCONTRADA') + C.dim(' (' + r.category + ', ' + r.rounds + (en ? ' rounds' : ' rodadas') + ', ' + r.credits + ' cr)'),
1373
+ '',
1374
+ C.bold(en ? 'Cause: ' : 'Causa: ') + r.rootCause,
1375
+ '',
1376
+ C.bold(en ? 'Fix:' : 'Conserto:'),
1377
+ ...String(r.fix || '—').split('\n').map(l => C.dim(' ' + l)),
1378
+ ], { title: 'ts diagnóstico' }));
1379
+ console.log('\n' + C.dim(en ? 'Apply it with: ' : 'Aplique com: ') + 'ts agente "' + (en ? 'apply this fix: ' : 'aplique este conserto: ') + String(r.rootCause).slice(0, 40) + '…"');
1380
+ } else {
1381
+ console.log(ui.box([
1382
+ C.err(en ? 'NO ROOT CAUSE (honest)' : 'SEM CAUSA RAIZ (honesto)') + C.dim(' (' + r.rounds + (en ? ' rounds' : ' rodadas') + ', ' + r.credits + ' cr)'),
1383
+ '',
1384
+ C.dim(en ? 'Best guess: ' : 'Melhor palpite: ') + (r.rootCause || r.reason || (en ? 'unclear' : 'inconclusivo')),
1385
+ C.dim(en ? 'Ruled out: ' : 'Descartado: ') + r.evidence.map(e => e.hypothesis).filter((v, i, a) => v && a.indexOf(v) === i).slice(0, 5).join('; ').slice(0, 120),
1386
+ ], { title: 'ts diagnóstico' }));
1387
+ }
1388
+ }
1389
+
1336
1390
  // ── ts cloud — caixa de dev na nuvem (Fase 1) ────────────────────────────────
1337
1391
  async function cloudCmd(args) {
1338
1392
  const token = needToken();
@@ -1543,6 +1597,7 @@ function recallCmd(args) {
1543
1597
  case 'recall': case 'lembrei': case 'sessoes': case 'sessões': return recallCmd(POS.slice(1));
1544
1598
  case 'vps': case 'servidor': return vpsCmd(POS.slice(1));
1545
1599
  case 'cloud': case 'nuvem': return cloudCmd(POS.slice(1));
1600
+ case 'diagnosticar': case 'diagnose': case 'investigar': case 'debug': return diagnosticarCmd(POS.slice(1));
1546
1601
  case 'meta': case 'missao': case 'mission': return metaCmd();
1547
1602
  case 'runs': return runsCmd();
1548
1603
  case 'status': return statusCmd(POS[1]);
@@ -0,0 +1,97 @@
1
+ // lib/diagnose.js — MODO DIAGNÓSTICO do TS: um loop de INVESTIGAÇÃO (não de build).
2
+ // Dado um SINTOMA (erro/comando que falha), o agente forma HIPÓTESES rankeadas, testa
3
+ // cada uma com uma SONDA somente-leitura, lê o resultado, dá um VEREDITO (confirma/refuta)
4
+ // e itera até achar a CAUSA RAIZ — distinguindo ambiente/config de dado/lógica interna.
5
+ // É o que faltava pro TS encarar legado obscuro (ex: instalar um servidor de MMO) em vez de
6
+ // só construir do zero: hipótese → teste → descarte → nova hipótese.
7
+ const { api, ApiError } = require('./api');
8
+ const agent = require('./agent');
9
+ const tools = require('./tools');
10
+
11
+ // SONDA = comando de INVESTIGAÇÃO (só leitura). Bloqueia qualquer verbo que MUTE o sistema —
12
+ // o diagnóstico observa, não conserta (o fix vem depois, com aprovação, no `ts agente`).
13
+ const MUTATE = /(^|[\s;|&(])(rm|rmdir|mv|cp|dd|mkfs\S*|chmod|chown|ln|truncate|tee|shred|install|apt|apt-get|yum|dnf|snap|pip|pip3|npm|yarn|pnpm|gem|cargo|make|gcc|g\+\+|systemctl|service|kill|pkill|killall|reboot|shutdown|halt|poweroff|mount|umount|iptables|ufw|crontab|useradd|userdel|passwd|dpkg|growpart|resize2fs|mkswap|fdisk|parted)([\s;|&)]|$)|(\bmysql\b[^|]*\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|GRANT|TRUNCATE|REPLACE)\b)|(^|[^>])>[^>|&]|>>|\bsed\b[^|]*-i|\bgit\b\s+(commit|push|reset|checkout|clean|rm|merge|rebase|pull)/i;
14
+ function isProbe(cmd) { return !MUTATE.test(String(cmd || '')); }
15
+
16
+ function sysPrompt(lang, target) {
17
+ const where = target ? (lang === 'en' ? `The system under investigation is REMOTE: probes run via "${target}".` : `O sistema investigado é REMOTO: as sondas rodam via "${target}".`) : (lang === 'en' ? 'The system under investigation is THIS machine.' : 'O sistema investigado é ESTA máquina.');
18
+ return lang === 'en'
19
+ ? `You are a SENIOR DIAGNOSTICIAN. Given a symptom, find the ROOT CAUSE by investigation — never guess a fix blindly.
20
+ ${where}
21
+ METHOD each round: pick your single strongest HYPOTHESIS and ONE cheap READ-ONLY probe that would CONFIRM or REFUTE it. Read the probe output next round, then keep or discard the hypothesis and move on. Rank likely causes first (recent changes, environment, config, permissions, resources, data/version, then logic).
22
+ HARD RULES: (1) probe must be READ-ONLY (ldd, strace -f, cat, head, tail, ls, stat, file, readelf, md5sum, ss, ps, grep, find, ldconfig -p, dpkg -l, journalctl, "mysql ... SELECT/SHOW"). NEVER install/rm/mv/chmod/restart/write. (2) Don't repeat a probe already run. (3) A missing library that "exists" often means its OWN dependency is missing — probe the lib itself. (4) Distinguish ENVIRONMENT (missing lib, perm, port, config) from DATA/INTERNAL (version mismatch, corrupt/incompatible file, logic). (5) When the evidence proves the cause, STOP and report it with a concrete fix.
23
+ Reply ONLY with a JSON object, no prose:
24
+ {"thought":"1-2 sentences reasoning from the latest evidence","status":"investigating|solved|stuck","hypothesis":"current leading hypothesis (short)","category":"environment|config|permission|resource|network|data|version|logic|unknown","probe":"ONE read-only shell command (empty if solved/stuck)","expect_if_true":"what in the output confirms it","root_cause":"(only if solved) the confirmed root cause","fix":"(only if solved) the concrete fix — commands/steps","confidence":0}`
25
+ : `Você é um DIAGNOSTICADOR SÊNIOR. Dado um sintoma, ache a CAUSA RAIZ investigando — nunca chute o conserto às cegas.
26
+ ${where}
27
+ MÉTODO por rodada: escolha sua HIPÓTESE mais forte e UMA sonda barata SÓ-LEITURA que a CONFIRME ou REFUTE. Leia a saída na rodada seguinte, mantenha ou descarte a hipótese e siga. Rankeie causas prováveis primeiro (mudanças recentes, ambiente, config, permissões, recursos, dado/versão, e por fim lógica).
28
+ REGRAS DURAS: (1) a sonda tem que ser SÓ-LEITURA (ldd, strace -f, cat, head, tail, ls, stat, file, readelf, md5sum, ss, ps, grep, find, ldconfig -p, dpkg -l, journalctl, "mysql ... SELECT/SHOW"). NUNCA install/rm/mv/chmod/restart/escrever. (2) Não repita uma sonda já feita. (3) Biblioteca "não encontrada" que EXISTE quase sempre é DEPENDÊNCIA-DA-DEPENDÊNCIA faltando — sonde a própria lib. (4) Distinga AMBIENTE (lib faltando, permissão, porta, config) de DADO/INTERNO (mismatch de versão, arquivo corrompido/incompatível, lógica). (5) Quando a evidência provar a causa, PARE e reporte com um conserto concreto.
29
+ Responda SÓ com um objeto JSON, sem prosa:
30
+ {"thought":"raciocínio de 1-2 frases a partir da última evidência","status":"investigating|solved|stuck","hypothesis":"hipótese atual (curta)","category":"environment|config|permission|resource|network|data|version|logic|unknown","probe":"UM comando shell só-leitura (vazio se solved/stuck)","expect_if_true":"o que na saída confirma","root_cause":"(só se solved) a causa raiz confirmada","fix":"(só se solved) o conserto concreto — comandos/passos","confidence":0}`;
31
+ }
32
+
33
+ function extractJson(s) {
34
+ const t = String(s || '');
35
+ const a = t.indexOf('{'); const b = t.lastIndexOf('}');
36
+ if (a < 0 || b <= a) return null;
37
+ try { return JSON.parse(t.slice(a, b + 1)); } catch (_) {
38
+ // tenta reparar aspas/vírgulas comuns
39
+ try { return JSON.parse(t.slice(a, b + 1).replace(/,\s*}/g, '}').replace(/,\s*]/g, ']')); } catch (_) { return null; }
40
+ }
41
+ }
42
+
43
+ async function diagnose(symptom, opts = {}) {
44
+ const { token, lang = 'pt', model = null, cwd = process.cwd(), target = '', maxRounds = 12, onEvent = () => {} } = opts;
45
+ const k = await api('/api/ai/key?feature=cli_agent', { token, timeoutMs: 20000 });
46
+ if (!k || !k.key) throw new ApiError('ai_key', {});
47
+ const wrap = (probe) => target ? `${target} ${JSON.stringify(probe)}` : probe; // remoto = prefixo ssh + sonda quotada
48
+
49
+ const evidence = []; // { round, hypothesis, probe, output }
50
+ const seenProbes = new Set();
51
+ let inTok = 0, outTok = 0;
52
+ let result = { status: 'stuck', rootCause: '', fix: '', category: 'unknown', evidence, rounds: 0 };
53
+
54
+ for (let round = 1; round <= maxRounds; round++) {
55
+ const evLog = evidence.map(e => `--- Rodada ${e.round} · hipótese: ${e.hypothesis}\n$ ${e.probe}\n${String(e.output).slice(0, 1600)}`).join('\n\n') || '(nenhuma sonda ainda)';
56
+ const userMsg = (lang === 'en' ? 'SYMPTOM:\n' : 'SINTOMA:\n') + symptom + '\n\n' + (lang === 'en' ? 'EVIDENCE SO FAR:\n' : 'EVIDÊNCIAS ATÉ AGORA:\n') + evLog + '\n\n' + (lang === 'en' ? `Round ${round}/${maxRounds}. Next step as JSON.` : `Rodada ${round}/${maxRounds}. Próximo passo em JSON.`);
57
+ let r;
58
+ try {
59
+ r = await agent.llm({ baseUrl: k.baseUrl, key: k.key, model, noTools: true, signalMs: 90000, messages: [{ role: 'system', content: sysPrompt(lang, target) }, { role: 'user', content: userMsg }] });
60
+ } catch (e) { if (e.code === 'no_credits') { result.status = 'no_credits'; break; } throw e; }
61
+ inTok += (r.usage.prompt_tokens || 0); outTok += (r.usage.completion_tokens || 0);
62
+ const j = extractJson(r.msg.content) || { status: 'stuck', thought: 'resposta não-JSON', probe: '' };
63
+ result.rounds = round;
64
+
65
+ onEvent({ type: 'think', round, hypothesis: j.hypothesis || '', category: j.category || '', thought: j.thought || '', confidence: j.confidence || 0 });
66
+
67
+ if (j.status === 'solved' && (j.root_cause || j.fix)) {
68
+ result = { status: 'solved', rootCause: j.root_cause || j.hypothesis || '', fix: j.fix || '', category: j.category || 'unknown', confidence: j.confidence || 0, evidence, rounds: round };
69
+ onEvent({ type: 'solved', rootCause: result.rootCause, fix: result.fix, category: result.category });
70
+ break;
71
+ }
72
+ if (j.status === 'stuck' || !j.probe) {
73
+ result = { status: 'stuck', rootCause: j.root_cause || '', fix: j.fix || '', category: j.category || 'unknown', reason: j.thought || '', evidence, rounds: round };
74
+ onEvent({ type: 'stuck', reason: j.thought || '' });
75
+ break;
76
+ }
77
+
78
+ const probe = String(j.probe).trim();
79
+ if (seenProbes.has(probe)) { onEvent({ type: 'skip', reason: 'sonda repetida', probe }); evidence.push({ round, hypothesis: j.hypothesis || '', probe, output: '(sonda repetida — ignorada; mude de abordagem)' }); continue; }
80
+ seenProbes.add(probe);
81
+ if (!isProbe(probe)) { onEvent({ type: 'blocked', probe }); evidence.push({ round, hypothesis: j.hypothesis || '', probe, output: '(BLOQUEADA: sonda não é só-leitura. Investigue sem alterar o sistema.)' }); continue; }
82
+
83
+ onEvent({ type: 'probe', round, hypothesis: j.hypothesis || '', probe, expect: j.expect_if_true || '' });
84
+ let out;
85
+ try {
86
+ const ex = await tools.execute('executar_comando', { comando: wrap(probe) }, { baseDir: cwd, token });
87
+ out = ex.erro ? ('ERRO: ' + ex.erro) : ([ex.stdout, ex.stderr].filter(Boolean).join('\n') || ('(sem saída, exit ' + (ex.codigo ?? '?') + ')'));
88
+ } catch (e) { out = 'FALHA ao rodar sonda: ' + (e.message || e); }
89
+ evidence.push({ round, hypothesis: j.hypothesis || '', probe, output: out });
90
+ onEvent({ type: 'evidence', round, output: String(out).slice(0, 500) });
91
+ }
92
+
93
+ const credits = Math.max(1, Math.round((inTok * 0.15 + outTok * 0.6) / 1000));
94
+ return { ...result, tokens: { inTok, outTok }, credits };
95
+ }
96
+
97
+ module.exports = { diagnose, isProbe, _extractJson: extractJson };
package/lib/i18n.js CHANGED
@@ -31,6 +31,8 @@ const STR = {
31
31
  { title: 'Agente LOCAL (mãos nesta máquina)', items: [
32
32
  ['ts agente "tarefa"', 'executa DE VERDADE aqui: comandos, arquivos, diagnóstico'],
33
33
  ['ts agente "..." --yes', 'autônomo (destrutivo pede aprovação no Telegram)'],
34
+ ['ts diagnosticar "erro"', 'INVESTIGA a causa raiz: hipótese→sonda→veredito (só-leitura)'],
35
+ ['ts diagnosticar "..." --remoto "ssh user@host"', 'investiga uma máquina remota'],
34
36
  ['ts agente --continuar "..."', 'retoma o trabalho anterior desta pasta'],
35
37
  ['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
36
38
  ['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.49.0",
3
+ "version": "0.50.1",
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"