terminal-smart-cli 0.62.0 → 0.64.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 +67 -20
- package/lib/diagnose.js +3 -3
- package/lib/i18n.js +1 -1
- package/lib/sentinela.js +2 -1
- package/lib/stack.js +51 -0
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -682,33 +682,29 @@ async function hooksCmd(words) {
|
|
|
682
682
|
async function initCmd() {
|
|
683
683
|
const _fs = require('fs'), _p = require('path');
|
|
684
684
|
const cwd = process.cwd();
|
|
685
|
+
const auto = FLAGS.has('--auto') || FLAGS.has('--stack');
|
|
685
686
|
const dest = _p.join(cwd, '.ts-memoria.md');
|
|
686
|
-
const has = (f) => _fs.existsSync(_p.join(cwd, f));
|
|
687
|
-
const readJson = (f) => { try { return JSON.parse(_fs.readFileSync(_p.join(cwd, f), 'utf8')); } catch (_) { return null; } };
|
|
688
687
|
const en = cfg.lang === 'en';
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
tipo = 'Node.js' + (pkg.dependencies && (pkg.dependencies.react || pkg.dependencies.next) ? ' (React/Next)' : '');
|
|
693
|
-
const s = pkg.scripts || {};
|
|
694
|
-
if (s.build) build.push('npm run build'); if (s.dev || s.start) run.push('npm run ' + (s.dev ? 'dev' : 'start')); if (s.test) test.push('npm test');
|
|
695
|
-
} else if (has('pubspec.yaml')) { tipo = 'Flutter/Dart'; build = ['flutter build']; run = ['flutter run']; test = ['flutter test']; }
|
|
696
|
-
else if (has('requirements.txt') || has('pyproject.toml')) { tipo = 'Python'; run = ['python main.py']; test = ['pytest']; }
|
|
697
|
-
else if (has('go.mod')) { tipo = 'Go'; build = ['go build ./...']; run = ['go run .']; test = ['go test ./...']; }
|
|
698
|
-
else if (has('Cargo.toml')) { tipo = 'Rust'; build = ['cargo build']; run = ['cargo run']; test = ['cargo test']; }
|
|
699
|
-
else if (has('index.html')) { tipo = en ? 'Static web (HTML/JS)' : 'Web estático (HTML/JS)'; run = ['python -m http.server']; }
|
|
688
|
+
const st = require('../lib/stack').detectStack(cwd);
|
|
689
|
+
let { tipo, build, run, test } = st;
|
|
690
|
+
if (tipo === 'desconhecido' && en) tipo = 'unknown';
|
|
700
691
|
const top = _fs.readdirSync(cwd).filter(n => !n.startsWith('.') && n !== 'node_modules').slice(0, 25).join(', ');
|
|
701
692
|
const nl = (a) => a.length ? a.map(x => '`' + x + '`').join(' · ') : (en ? '(fill in)' : '(preencher)');
|
|
693
|
+
// Seção de convenções: com --auto vem PREENCHIDA pelo detector (determinístico); senão fica em branco.
|
|
694
|
+
const convBlock = (auto && st.conv.length)
|
|
695
|
+
? st.conv.map(c => '- ' + c).join('\n') + '\n- ' + (en ? '(add project-specific pitfalls / how to deploy)' : '(acrescente armadilhas específicas / como fazer deploy)')
|
|
696
|
+
: '- ' + (en ? '(add what the agent should always know: architecture, pitfalls, how to deploy)' : '(anote o que o agente sempre deve saber: arquitetura, pegadinhas, como fazer deploy)');
|
|
702
697
|
const body = (en
|
|
703
|
-
? `# Project memory (ts)\n\n> Auto-generated by \`ts init\`. Edit freely — the ts agent reads this every session.\n\n- **Type:** ${tipo}\n- **Build:** ${nl(build)}\n- **Run:** ${nl(run)}\n- **Test:** ${nl(test)}\n- **Top-level:** ${top}\n\n## Conventions / gotchas\n
|
|
704
|
-
: `# Memória do projeto (ts)\n\n> Gerado por \`ts init\`. Edite à vontade — o agente ts lê isto toda sessão.\n\n- **Tipo:** ${tipo}\n- **Build:** ${nl(build)}\n- **Rodar:** ${nl(run)}\n- **Testar:** ${nl(test)}\n- **Raiz:** ${top}\n\n## Convenções / armadilhas\n
|
|
698
|
+
? `# Project memory (ts)\n\n> Auto-generated by \`ts init${auto ? ' --auto' : ''}\`. Edit freely — the ts agent reads this every session.\n\n- **Type:** ${tipo}\n- **Build:** ${nl(build)}\n- **Run:** ${nl(run)}\n- **Test:** ${nl(test)}\n- **Top-level:** ${top}\n\n## Conventions / gotchas\n${convBlock}\n`
|
|
699
|
+
: `# Memória do projeto (ts)\n\n> Gerado por \`ts init${auto ? ' --auto' : ''}\`. Edite à vontade — o agente ts lê isto toda sessão.\n\n- **Tipo:** ${tipo}\n- **Build:** ${nl(build)}\n- **Rodar:** ${nl(run)}\n- **Testar:** ${nl(test)}\n- **Raiz:** ${top}\n\n## Convenções / armadilhas\n${convBlock}\n`);
|
|
705
700
|
if (_fs.existsSync(dest)) {
|
|
706
701
|
const ok = await ui.ask(C.warn('▲ ') + (en ? '.ts-memoria.md already exists. Overwrite? (backup kept) [s/N] ' : '.ts-memoria.md já existe. Sobrescrever? (com backup) [s/N] '));
|
|
707
702
|
if (!['s', 'sim', 'y', 'yes'].includes(String(ok).trim().toLowerCase())) { console.log(' ' + C.dim(en ? 'cancelled.' : 'cancelado.')); return; }
|
|
708
703
|
let r; try { r = await require('../lib/tools').execute('escrever_arquivo', { caminho: dest, conteudo: body }, { baseDir: cwd }); } catch (_) {}
|
|
709
704
|
if (!r || r.erro) { try { _fs.writeFileSync(dest, body); } catch (e) { console.error(ui.errLine((en ? 'Failed to write .ts-memoria.md: ' : 'Falha ao escrever .ts-memoria.md: ') + e.message)); return; } }
|
|
710
705
|
} else { _fs.writeFileSync(dest, body); }
|
|
711
|
-
|
|
706
|
+
const detN = auto ? st.conv.length : 0;
|
|
707
|
+
console.log(' ' + C.ok('✔ ') + (en ? 'Created ' : 'Criado ') + C.bold('.ts-memoria.md') + C.dim(' — ' + tipo + (detN ? (en ? ` · ${detN} convention(s) detected` : ` · ${detN} convenção(ões) detectada(s)`) : '')) + '\n ' + C.dim(auto ? (en ? 'Detected your stack automatically. Add project-specific notes and it loads every session.' : 'Detectei sua stack sozinho. Acrescente notas específicas — carrega toda sessão.') : (en ? 'Tip: `ts init --auto` detects your stack and fills the conventions.' : 'Dica: `ts init --auto` detecta a stack e preenche as convenções.')));
|
|
712
708
|
}
|
|
713
709
|
|
|
714
710
|
// ── git worktrees: isola uma run numa cópia descartável do repo ──
|
|
@@ -1432,13 +1428,14 @@ async function sentinelaCmd(args) {
|
|
|
1432
1428
|
regra: _regraFromFlags(),
|
|
1433
1429
|
avisar: _val(['--avisar', '--notify']) || 'falha',
|
|
1434
1430
|
escalar: FLAGS.has('--escalar') || FLAGS.has('--escalate'),
|
|
1431
|
+
autofix: FLAGS.has('--autofix') || FLAGS.has('--auto-fix'),
|
|
1435
1432
|
});
|
|
1436
1433
|
console.log('\n' + ui.box([
|
|
1437
1434
|
C.ok(en ? 'Sentinel added' : 'Sentinela adicionada') + C.dim(' ' + check.id),
|
|
1438
1435
|
'',
|
|
1439
1436
|
C.bold(check.nome) + C.dim(' ' + (check.target ? check.target.split(' ').pop() : (en ? 'local' : 'local'))),
|
|
1440
1437
|
C.dim('$ ') + check.cmd.slice(0, 70),
|
|
1441
|
-
C.dim((en ? 'rule: ' : 'regra: ') + _regraTxt(check.regra) + (en ? ' · every ' : ' · a cada ') + (check.cron || sen.parseEvery(_val(['--cada']) || '').ms / 60000 + 'm') + (check.escalar ? C.warn(' · escala p/ IA se falhar') : '')),
|
|
1438
|
+
C.dim((en ? 'rule: ' : 'regra: ') + _regraTxt(check.regra) + (en ? ' · every ' : ' · a cada ') + (check.cron || sen.parseEvery(_val(['--cada']) || '').ms / 60000 + 'm') + (check.autofix ? C.warn(' · 🔧 autofix (conserta c/ aprovação)') : check.escalar ? C.warn(' · escala p/ IA se falhar') : '')),
|
|
1442
1439
|
], { title: 'ts sentinela' }));
|
|
1443
1440
|
console.log('\n' + C.dim(en ? 'Activate the schedule with: ' : 'Ative o agendamento com: ') + 'ts sentinela instalar');
|
|
1444
1441
|
return;
|
|
@@ -1452,7 +1449,7 @@ async function sentinelaCmd(args) {
|
|
|
1452
1449
|
C.bold(en ? 'Sentinels' : 'Sentinelas') + C.dim(' (' + list.length + ')'),
|
|
1453
1450
|
...list.flatMap(c => [
|
|
1454
1451
|
'',
|
|
1455
|
-
_icon(!c.ultimo || c.ultimo.ok) + ' ' + C.bold(c.nome) + C.dim(' ' + c.id + (c.escalar ? ' ⚡' : '') + (c.target ? ' ' + c.target.split(' ').pop() : '')),
|
|
1452
|
+
_icon(!c.ultimo || c.ultimo.ok) + ' ' + C.bold(c.nome) + C.dim(' ' + c.id + (c.autofix ? ' 🔧' : c.escalar ? ' ⚡' : '') + (c.target ? ' ' + c.target.split(' ').pop() : '')),
|
|
1456
1453
|
C.dim(' $ ' + c.cmd.slice(0, 66)),
|
|
1457
1454
|
C.dim(' ' + _regraTxt(c.regra) + ' · ' + (c.cron || ((c.every_ms / 60000) + 'm')) + (c.ultimo ? ' · ' + (en ? 'last: ' : 'último: ') + (c.ultimo.ok ? C.ok('ok') : C.err(c.ultimo.motivo)) : ' · ' + (en ? 'never run' : 'nunca rodou'))),
|
|
1458
1455
|
]),
|
|
@@ -1493,17 +1490,24 @@ async function sentinelaCmd(args) {
|
|
|
1493
1490
|
const out = ((r.res.stdout || '') + (r.res.stderr || '')).trim();
|
|
1494
1491
|
if (out && !r.verdict.ok) msg += '\n' + out.split('\n').slice(0, 6).join('\n').slice(0, 500);
|
|
1495
1492
|
// ESCALA pra IA só quando falha e --escalar (aqui nasce a sugestão de conserto)
|
|
1493
|
+
let diagResult = null;
|
|
1496
1494
|
if (!r.verdict.ok && check.escalar && token) {
|
|
1497
1495
|
try {
|
|
1498
1496
|
const diag = require('../lib/diagnose');
|
|
1499
|
-
|
|
1497
|
+
diagResult = await diag.diagnose(`sentinela "${check.nome}" falhou: ${r.verdict.motivo}. comando: ${check.cmd}. saída: ${out.slice(0, 400)}`, {
|
|
1500
1498
|
token, lang: cfg.lang || 'pt', target: check.target || '', maxRounds: 4, onEvent: () => {},
|
|
1501
1499
|
});
|
|
1502
|
-
if (
|
|
1500
|
+
if (diagResult.status === 'solved') msg += '\n\n🧠 causa provável: ' + String(diagResult.rootCause).slice(0, 200) + '\n🔧 conserto: ' + String(diagResult.fix || '—').slice(0, 240);
|
|
1503
1501
|
} catch (_) { /* diagnose best-effort */ }
|
|
1504
1502
|
}
|
|
1505
1503
|
if (token) { try { await metaMod.notify(token, msg); } catch (_) {} }
|
|
1506
1504
|
if (!quiet) console.log(msg);
|
|
1505
|
+
// ── AUTOFIX (Incident Autopilot): conserto verificado + comando seguro → aprovação no Telegram → aplica → re-valida.
|
|
1506
|
+
// NUNCA aplica sem OK humano (aprovação remota). Só quando o diagnóstico sobreviveu à refutação.
|
|
1507
|
+
if (!r.verdict.ok && check.autofix && token && diagResult && diagResult.status === 'solved' && diagResult.verified && diagResult.fixCommand) {
|
|
1508
|
+
const applied = await _sentinelaAutofix(check, diagResult, r, token, quiet, en);
|
|
1509
|
+
if (applied != null) alerts++;
|
|
1510
|
+
}
|
|
1507
1511
|
}
|
|
1508
1512
|
sen._save(list);
|
|
1509
1513
|
if (!quiet) console.log(C.dim(`\n${en ? 'ran' : 'rodou'} ${ran} · ${alerts} ${en ? 'alert(s)' : 'alerta(s)'}`));
|
|
@@ -1556,6 +1560,49 @@ async function sentinelaCmd(args) {
|
|
|
1556
1560
|
}
|
|
1557
1561
|
function execSyncQuiet(c) { return require('child_process').execSync(c, { stdio: 'ignore', windowsHide: true }); }
|
|
1558
1562
|
|
|
1563
|
+
// AUTOFIX da sentinela (Incident Autopilot): propõe o conserto no Telegram, e SÓ aplica com
|
|
1564
|
+
// aprovação humana. Aplica no alvo (local ou ssh remoto), re-valida o check e reporta. Retorna
|
|
1565
|
+
// true/false se aplicou (consertou ou não), null se não houve aprovação/canal.
|
|
1566
|
+
async function _sentinelaAutofix(check, diag, r, token, quiet, en) {
|
|
1567
|
+
const sen = require('../lib/sentinela');
|
|
1568
|
+
const metaMod = require('../lib/meta');
|
|
1569
|
+
const cmd = String(diag.fixCommand).trim();
|
|
1570
|
+
const alvo = check.target ? check.target.split(' ').pop() : os.hostname();
|
|
1571
|
+
// 1) aprovação remota (mesmo canal do agente): /api/cli/approval/start → poll
|
|
1572
|
+
let st = null;
|
|
1573
|
+
try { st = await api('/api/cli/approval/start', { method: 'POST', token, body: { comando: `[sentinela ${check.nome}@${alvo}] ${cmd}`, host: alvo }, timeoutMs: 20000 }); } catch (_) { return null; }
|
|
1574
|
+
if (!st || !st.success || !st.id) { // sem canal Telegram → só propõe e para
|
|
1575
|
+
if (!quiet) console.log(' ' + C.dim(en ? 'autofix: no Telegram channel to approve — proposed only.' : 'autofix: sem canal Telegram pra aprovar — só proposto.'));
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
if (!quiet) console.log(' ' + C.warn('🔧 ') + C.dim((en ? 'autofix: sent to Telegram for approval — ' : 'autofix: enviei pro Telegram pra aprovar — ') + cmd.slice(0, 60)));
|
|
1579
|
+
let approved = false;
|
|
1580
|
+
const t0 = Date.now();
|
|
1581
|
+
while (Date.now() - t0 < 150000) {
|
|
1582
|
+
await new Promise(rs => setTimeout(rs, 3000));
|
|
1583
|
+
let p = null;
|
|
1584
|
+
try { p = await api('/api/cli/approval/poll', { method: 'POST', token, body: { id: st.id }, timeoutMs: 15000 }); } catch (_) { continue; }
|
|
1585
|
+
if (p && p.status === 'approved') { approved = true; break; }
|
|
1586
|
+
if (p && (p.status === 'denied' || p.status === 'expired')) break;
|
|
1587
|
+
}
|
|
1588
|
+
if (!approved) {
|
|
1589
|
+
try { await metaMod.notify(token, `⏹️ [sentinela] ${check.nome}: conserto NÃO aplicado (não aprovado). Comando proposto: ${cmd}`); } catch (_) {}
|
|
1590
|
+
if (!quiet) console.log(' ' + C.dim(en ? 'autofix: not approved — nothing applied.' : 'autofix: não aprovado — nada aplicado.'));
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
// 2) aplica no alvo + 3) re-valida o check
|
|
1594
|
+
const ap = sen.runShell(cmd, { target: check.target, timeoutMs: 60000 });
|
|
1595
|
+
const rr = sen.runOne(check); // re-roda o check → atualiza estado
|
|
1596
|
+
const fixedOk = rr.verdict.ok;
|
|
1597
|
+
const rerun = ((rr.res.stdout || '') + (rr.res.stderr || '')).trim().split('\n').slice(0, 4).join('\n').slice(0, 300);
|
|
1598
|
+
const finalMsg = fixedOk
|
|
1599
|
+
? `✅ [sentinela] ${check.nome}: CONSERTADO por autofix. Apliquei \`${cmd}\` e o check voltou a passar.`
|
|
1600
|
+
: `⚠️ [sentinela] ${check.nome}: autofix aplicado (\`${cmd}\`) mas o check AINDA falha (${rr.verdict.motivo}). Precisa de você.\n${rerun}`;
|
|
1601
|
+
try { await metaMod.notify(token, finalMsg); } catch (_) {}
|
|
1602
|
+
if (!quiet) console.log(' ' + (fixedOk ? C.ok('✔ ') + (en ? 'autofix worked — check passes again' : 'autofix funcionou — check voltou a passar') : C.err('✗ ') + (en ? 'applied but still failing' : 'aplicado mas ainda falha')) + C.dim((ap.code !== 0 ? ' (fix exit ' + ap.code + ')' : '')));
|
|
1603
|
+
return fixedOk;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1559
1606
|
// ── ts sonhar — DREAM SESSION: consolida a memória episódica em camadas ───────
|
|
1560
1607
|
async function sonharCmd(args) {
|
|
1561
1608
|
const en = (cfg.lang === 'en');
|
package/lib/diagnose.js
CHANGED
|
@@ -25,13 +25,13 @@ ${where}
|
|
|
25
25
|
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).
|
|
26
26
|
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.
|
|
27
27
|
Reply ONLY with a JSON object, no prose:
|
|
28
|
-
{"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}`
|
|
28
|
+
{"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","fix_command":"(only if solved AND the fix is ONE safe idempotent shell command that applies it — e.g. restart a service, free a port, install a dep; empty if it needs code edits/multiple steps/anything risky)","confidence":0}`
|
|
29
29
|
: `Você é um DIAGNOSTICADOR SÊNIOR. Dado um sintoma, ache a CAUSA RAIZ investigando — nunca chute o conserto às cegas.
|
|
30
30
|
${where}
|
|
31
31
|
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).
|
|
32
32
|
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.
|
|
33
33
|
Responda SÓ com um objeto JSON, sem prosa:
|
|
34
|
-
{"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}`;
|
|
34
|
+
{"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","fix_command":"(só se solved E o conserto for UM comando shell seguro e idempotente que o aplica — ex.: reiniciar um serviço, liberar uma porta, instalar uma dep; vazio se exigir edição de código/vários passos/qualquer coisa arriscada)","confidence":0}`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function extractJson(s) {
|
|
@@ -95,7 +95,7 @@ async function diagnose(symptom, opts = {}) {
|
|
|
95
95
|
continue; // a própria conclusão caiu na verificação → volta a investigar
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
result = { status: 'solved', rootCause: claim, fix: j.fix || '', category: j.category || 'unknown', confidence: j.confidence || 0, verified: !!rprobe, evidence, rounds: round };
|
|
98
|
+
result = { status: 'solved', rootCause: claim, fix: j.fix || '', fixCommand: String(j.fix_command || '').trim(), category: j.category || 'unknown', confidence: j.confidence || 0, verified: !!rprobe, evidence, rounds: round };
|
|
99
99
|
onEvent({ type: 'solved', rootCause: result.rootCause, fix: result.fix, category: result.category, verified: result.verified });
|
|
100
100
|
break;
|
|
101
101
|
}
|
package/lib/i18n.js
CHANGED
|
@@ -38,7 +38,7 @@ const STR = {
|
|
|
38
38
|
['ts agente "..." --yes', 'autônomo (destrutivo pede aprovação no Telegram)'],
|
|
39
39
|
['ts diagnosticar "erro"', 'INVESTIGA a causa raiz: hipótese→sonda→veredito (só-leitura)'],
|
|
40
40
|
['ts diagnosticar "..." --remoto "ssh user@host"', 'investiga uma máquina remota'],
|
|
41
|
-
['ts sentinela add "nome" --cmd "..."', 'VIGIA determinístico (regra do if, ZERO token);
|
|
41
|
+
['ts sentinela add "nome" --cmd "..."', 'VIGIA determinístico (regra do if, ZERO token); --escalar diagnostica se falhar; --autofix conserta (com aprovação no Telegram)'],
|
|
42
42
|
['ts sentinela instalar', 'agenda os checks no SO (cron/Task Scheduler) e avisa no Telegram'],
|
|
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)'],
|
package/lib/sentinela.js
CHANGED
|
@@ -91,7 +91,8 @@ function add(def) {
|
|
|
91
91
|
every_ms: ev.ms, cron: ev.cron,
|
|
92
92
|
regra: def.regra || { tipo: 'exit0' },
|
|
93
93
|
avisar: def.avisar || 'falha', // 'falha' | 'sempre' | 'mudanca'
|
|
94
|
-
escalar: !!def.escalar,
|
|
94
|
+
escalar: !!def.escalar || !!def.autofix, // escala pro diagnose quando falha (autofix implica escalar)
|
|
95
|
+
autofix: !!def.autofix, // propõe o conserto no Telegram (aprovação) e re-valida
|
|
95
96
|
criado: Date.now(),
|
|
96
97
|
proximo: ev.ms ? Date.now() : 0, // cron: 0 = decide por horário
|
|
97
98
|
ultimo: null, // { ts, ok, motivo, hash }
|
package/lib/stack.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Detecta a STACK de um projeto de forma DETERMINÍSTICA (zero IA): tipo, comandos
|
|
2
|
+
// (build/run/test) e CONVENÇÕES (o que o agente sempre deve saber). Lê config files +
|
|
3
|
+
// deps do package.json. Puro/testável. Usado por `ts init --auto` (e reutilizável pelo meta).
|
|
4
|
+
'use strict';
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
function detectStack(cwd) {
|
|
9
|
+
const dir = cwd || process.cwd();
|
|
10
|
+
const has = (f) => { try { return fs.existsSync(path.join(dir, f)); } catch (_) { return false; } };
|
|
11
|
+
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { return null; } };
|
|
12
|
+
let tipo = 'desconhecido', build = [], run = [], test = [];
|
|
13
|
+
const conv = [];
|
|
14
|
+
const pkg = has('package.json') && readJson('package.json');
|
|
15
|
+
if (pkg) {
|
|
16
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
17
|
+
const dep = (n) => Object.prototype.hasOwnProperty.call(deps, n);
|
|
18
|
+
tipo = dep('next') ? 'Next.js' : dep('nuxt') ? 'Nuxt' : dep('@remix-run/react') ? 'Remix' : dep('react') ? 'React' : dep('vue') ? 'Vue' : dep('svelte') ? 'Svelte' : dep('@angular/core') ? 'Angular' : (dep('express') || dep('fastify') || dep('koa')) ? 'Node backend' : 'Node.js';
|
|
19
|
+
const s = pkg.scripts || {};
|
|
20
|
+
if (s.build) build.push('npm run build'); if (s.dev || s.start) run.push('npm run ' + (s.dev ? 'dev' : 'start')); if (s.test) test.push('npm test'); if (s.lint) test.push('npm run lint');
|
|
21
|
+
if (has('tsconfig.json') || dep('typescript')) conv.push('**TypeScript** — mantenha os tipos corretos; rode o type-check (tsc/`npm run build`) antes de concluir. Não use `any` à toa.');
|
|
22
|
+
if (dep('next')) conv.push('**Next.js** — respeite o roteamento do framework (App Router em `app/` ou Pages em `pages/`); não reinvente SSR/rotas/data-fetching.');
|
|
23
|
+
if (dep('vite') || has('vite.config.js') || has('vite.config.ts')) conv.push('**Vite** — dev/build pelo Vite; imports com alias respeitam `vite.config`.');
|
|
24
|
+
if (dep('tailwindcss') || has('tailwind.config.js') || has('tailwind.config.ts')) conv.push('**Tailwind CSS** — estilize com classes utilitárias; evite CSS solto/inline fora do padrão do projeto.');
|
|
25
|
+
if (dep('@prisma/client') || dep('prisma') || has('prisma/schema.prisma')) conv.push('**Prisma** — altere `schema.prisma` + `prisma migrate`; NÃO edite SQL/tabela na mão.');
|
|
26
|
+
if (dep('drizzle-orm')) conv.push('**Drizzle ORM** — schema em código + migrations; não edite o banco direto.');
|
|
27
|
+
if (dep('jest') || dep('vitest') || dep('mocha')) conv.push('**Testes** (' + (dep('vitest') ? 'vitest' : dep('jest') ? 'jest' : 'mocha') + ') — rode e mantenha verdes; toda mudança de lógica precisa de teste.');
|
|
28
|
+
if (has('.eslintrc') || has('.eslintrc.js') || has('.eslintrc.json') || has('eslint.config.js') || dep('eslint')) conv.push('**ESLint** — rode o lint antes de concluir; siga as regras do projeto.');
|
|
29
|
+
if (has('.prettierrc') || has('.prettierrc.json') || dep('prettier')) conv.push('**Prettier** — mantenha a formatação (não reformate arquivos inteiros à toa).');
|
|
30
|
+
if (pkg.workspaces || has('pnpm-workspace.yaml') || has('turbo.json') || has('lerna.json')) conv.push('**Monorepo** — trabalhe no pacote/workspace certo; não misture dependências entre pacotes.');
|
|
31
|
+
} else if (has('pubspec.yaml')) { tipo = 'Flutter/Dart'; build = ['flutter build']; run = ['flutter run']; test = ['flutter test']; conv.push('**Flutter** — widgets + state management do projeto; `flutter analyze` limpo antes de concluir.'); }
|
|
32
|
+
else if (has('requirements.txt') || has('pyproject.toml')) {
|
|
33
|
+
tipo = 'Python'; run = ['python main.py']; test = ['pytest'];
|
|
34
|
+
const req = has('requirements.txt') ? (() => { try { return fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf8').toLowerCase(); } catch (_) { return ''; } })() : '';
|
|
35
|
+
if (/django/.test(req)) { tipo = 'Python/Django'; run = ['python manage.py runserver']; conv.push('**Django** — use models/migrations (`makemigrations`+`migrate`); respeite apps e settings.'); }
|
|
36
|
+
else if (/fastapi/.test(req)) { tipo = 'Python/FastAPI'; run = ['uvicorn main:app --reload']; conv.push('**FastAPI** — rotas com Pydantic; mantenha os schemas tipados.'); }
|
|
37
|
+
else if (/flask/.test(req)) { tipo = 'Python/Flask'; run = ['flask run']; }
|
|
38
|
+
conv.push('**Python** — use o venv do projeto; não instale global. `pytest` verde antes de concluir.');
|
|
39
|
+
}
|
|
40
|
+
else if (has('go.mod')) { tipo = 'Go'; build = ['go build ./...']; run = ['go run .']; test = ['go test ./...']; conv.push('**Go** — `go build`/`go vet`/`go test ./...` limpos; erros tratados explicitamente.'); }
|
|
41
|
+
else if (has('Cargo.toml')) { tipo = 'Rust'; build = ['cargo build']; run = ['cargo run']; test = ['cargo test']; conv.push('**Rust** — `cargo clippy` e `cargo test` limpos antes de concluir.'); }
|
|
42
|
+
else if (has('composer.json')) { tipo = 'PHP'; run = ['php -S localhost:8000']; conv.push('**PHP/Composer** — `composer install`; siga o autoload PSR do projeto.'); }
|
|
43
|
+
else if (has('index.html')) { tipo = 'Web estático (HTML/JS)'; run = ['python -m http.server']; }
|
|
44
|
+
// sinais transversais (independem da linguagem)
|
|
45
|
+
if (has('Dockerfile') || has('docker-compose.yml') || has('docker-compose.yaml') || has('compose.yaml')) conv.push('**Docker** — mudanças de ambiente/infra passam pelo Docker; rebuild da imagem quando alterar deps.');
|
|
46
|
+
if (has('.env.example') || has('.env.sample')) conv.push('**Config via .env** — NUNCA commite segredos; use `.env.example` como referência das variáveis.');
|
|
47
|
+
if (has('.git')) conv.push('**Git** — commits pequenos e descritivos; não faça `push --force` na branch principal.');
|
|
48
|
+
return { tipo, build, run, test, conv };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { detectStack };
|