terminal-smart-cli 0.48.0 → 0.50.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 +75 -2
- package/lib/diagnose.js +97 -0
- package/package.json +1 -1
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();
|
|
@@ -1373,6 +1427,13 @@ async function cloudCmd(args) {
|
|
|
1373
1427
|
if (sub === 'exec' || sub === 'sh' || sub === 'run') {
|
|
1374
1428
|
const cmd = rest.join(' ').trim();
|
|
1375
1429
|
if (!cmd) { console.log(ui.errLine(en ? 'usage: ts cloud exec "<command>"' : 'uso: ts cloud exec "<comando>"')); return; }
|
|
1430
|
+
// Saída AO VIVO via SSE quando é terminal interativo; fallback pro modo bloqueante.
|
|
1431
|
+
if (process.stdout.isTTY) {
|
|
1432
|
+
try {
|
|
1433
|
+
await sse('/api/cloud/exec-stream', { token, body: { cmd, timeoutMs: 300000 }, timeoutMs: 310000, onEvent: (e) => { if (e.chunk) process.stdout.write(e.chunk); } });
|
|
1434
|
+
return;
|
|
1435
|
+
} catch (e) { if (e.code === 'auth') { console.log(ui.errLine(T.need_login)); return; } }
|
|
1436
|
+
}
|
|
1376
1437
|
const r = await call('exec', { method: 'POST', body: { cmd }, timeoutMs: 300000 });
|
|
1377
1438
|
if (r.out) process.stdout.write(r.out);
|
|
1378
1439
|
if (!r.ok && r.error) console.log(ui.errLine(r.error));
|
|
@@ -1386,8 +1447,19 @@ async function cloudCmd(args) {
|
|
|
1386
1447
|
if (!st.exists) { console.log(C.dim(en ? 'creating box…' : 'criando caixa…')); const up = await call('up', { method: 'POST', body: {}, timeoutMs: 120000 }); if (!up.ok) { console.log(ui.errLine(up.error || 'falha')); if (up.code === 'plan_required') console.log(C.dim('terminalsmart.com.br/planos')); return; } }
|
|
1387
1448
|
console.log(C.dim(en ? 'running ts meta inside the cloud box (may take a while)…' : 'rodando ts meta dentro da caixa (pode demorar)…'));
|
|
1388
1449
|
const safe = goal.replace(/(["\\$`])/g, '\\$1');
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1450
|
+
const cmdMeta = 'cd /workspace && ts meta "' + safe + '" --yes 2>&1';
|
|
1451
|
+
// Build AO VIVO via SSE quando é terminal interativo; fallback bloqueante com tail.
|
|
1452
|
+
let streamed = false;
|
|
1453
|
+
if (process.stdout.isTTY) {
|
|
1454
|
+
try {
|
|
1455
|
+
await sse('/api/cloud/exec-stream', { token, body: { cmd: cmdMeta, timeoutMs: 1800000 }, timeoutMs: 1860000, onEvent: (e) => { if (e.chunk) process.stdout.write(e.chunk); } });
|
|
1456
|
+
streamed = true;
|
|
1457
|
+
} catch (e) { if (e.code === 'auth') { console.log(ui.errLine(T.need_login)); return; } }
|
|
1458
|
+
}
|
|
1459
|
+
if (!streamed) {
|
|
1460
|
+
const r = await call('exec', { method: 'POST', body: { cmd: cmdMeta + ' | tail -50' }, timeoutMs: 1800000 });
|
|
1461
|
+
if (r.out) process.stdout.write(r.out);
|
|
1462
|
+
}
|
|
1391
1463
|
// EXPÕE o app na porta 3000 de forma PERSISTENTE (o startCmd fica salvo e é
|
|
1392
1464
|
// re-executado no resume, então sobrevive ao idle-suspend). npm start senão estático.
|
|
1393
1465
|
const startCmd = 'if [ -f package.json ] && grep -q \'"start"\' package.json; then PORT=3000 npm start; elif [ -f index.html ] || ls *.html >/dev/null 2>&1; then python3 -m http.server 3000; fi';
|
|
@@ -1525,6 +1597,7 @@ function recallCmd(args) {
|
|
|
1525
1597
|
case 'recall': case 'lembrei': case 'sessoes': case 'sessões': return recallCmd(POS.slice(1));
|
|
1526
1598
|
case 'vps': case 'servidor': return vpsCmd(POS.slice(1));
|
|
1527
1599
|
case 'cloud': case 'nuvem': return cloudCmd(POS.slice(1));
|
|
1600
|
+
case 'diagnosticar': case 'diagnose': case 'investigar': case 'debug': return diagnosticarCmd(POS.slice(1));
|
|
1528
1601
|
case 'meta': case 'missao': case 'mission': return metaCmd();
|
|
1529
1602
|
case 'runs': return runsCmd();
|
|
1530
1603
|
case 'status': return statusCmd(POS[1]);
|
package/lib/diagnose.js
ADDED
|
@@ -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 };
|