terminal-smart-cli 0.75.0 → 0.77.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
@@ -19,12 +19,56 @@ const FLAGS = new Set(rawArgs.filter(a => a.startsWith('-')));
19
19
  const POS = rawArgs.filter(a => !a.startsWith('-'));
20
20
  const JSON_OUT = FLAGS.has('--json');
21
21
  const YES = FLAGS.has('--yes') || FLAGS.has('-y');
22
+ // --yolo / --full-auto: modo FULL-AUTO (autoridade total) — aprova TUDO nesta sessão, inclusive
23
+ // destrutivos, sem perguntar. NÃO reusa --auto (já é "noturno" no meta e "detectar stack" no init).
24
+ // O gate anti-catástrofe de AUTO-destrutivos (rm -rf ~, mexer em ~/.ts…) segue RECUSANDO na hora
25
+ // mesmo aqui: full-auto não abre mão da proteção que mataria a própria máquina/missão.
26
+ const YOLO = FLAGS.has('--yolo') || FLAGS.has('--full-auto');
22
27
 
23
28
  let cfg = config.load();
24
29
  let T = t(cfg.lang || 'pt');
25
30
  const { C } = ui;
26
31
 
27
32
  function fmtK(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
33
+
34
+ // ── APROVAÇÃO DE PASSO (UX) ──────────────────────────────────────────────────
35
+ // askDestructive: prompt de comando DESTRUTIVO. Mostra um RESUMO curto em AMARELO (destaque,
36
+ // não vermelho = não é erro) — a 1ª linha do comando + quantas linhas ficam escondidas (num
37
+ // heredoc SQL a 1ª linha é o comando externo, ex "mysql -u root pw <<'SQL'"). Aceita:
38
+ // s/N = sim/não neste passo · a = aprovar TUDO na sessão (full-auto) · v = ver o comando inteiro
39
+ // Retorna true | false | 'all'.
40
+ async function askDestructive(cmd, { en = false, warning = null } = {}) {
41
+ const raw = String(cmd || '');
42
+ const { shown, hidden } = ui.cmdSummary(raw);
43
+ const more = hidden > 0 ? C.dim(` … +${hidden} ${en ? 'lines' : 'linhas'}`) : '';
44
+ // efeito colateral GLOBAL (ex: ALTER USER root quebra o sudo mysql): avisa UMA vez, antes do prompt.
45
+ if (warning) console.log(C.warn(' ⚠ ' + (en ? 'side effect: ' : 'efeito colateral: ')) + C.dim(warning));
46
+ for (;;) {
47
+ const q = C.warn(' ▲ ' + (en ? 'destructive: ' : 'destrutivo: ')) + C.bold(shown) + more
48
+ + C.dim(en ? ' [y/N · a=all · v=view] ' : ' [s/N · a=tudo · v=ver] ');
49
+ const a = String(await ui.ask(q)).trim().toLowerCase();
50
+ if (['v', 'ver', 'view'].includes(a)) { console.log('\n' + raw.split('\n').map(l => ' ' + C.dim(l)).join('\n') + '\n'); continue; }
51
+ if (['a', 'all', 'tudo'].includes(a)) return 'all';
52
+ return ['s', 'sim', 'y', 'yes'].includes(a);
53
+ }
54
+ }
55
+ // askLabelApprove: aprovação de um LABEL já formatado (skill/MCP, não é comando cru). s/N + a=tudo.
56
+ async function askLabelApprove(label, { en = false } = {}) {
57
+ const a = String(await ui.ask(C.warn('▲ ') + T.agent_approve(C.bold(label)) + C.dim(en ? '(a=all) ' : '(a=tudo) '))).trim().toLowerCase();
58
+ if (['a', 'all', 'tudo'].includes(a)) return 'all';
59
+ return ['s', 'sim', 'y', 'yes'].includes(a);
60
+ }
61
+ // Roteia o payload do askApprove do agente (obj destrutivo → resumo; string label → skill/MCP).
62
+ async function askApproveRoute(payload) {
63
+ const en = cfg.lang === 'en';
64
+ const isObj = payload && typeof payload === 'object';
65
+ const cmd = isObj ? payload.cmd : String(payload);
66
+ return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en, warning: payload.warning }) : askLabelApprove(cmd, { en });
67
+ }
68
+ // Linha de STATUS por etapa (✓ verde / ✗ vermelho) sob a linha ⚙, com recuo `pad`.
69
+ function stepDoneLine({ ok, evidence }, pad = ' ') {
70
+ return pad + (ok ? C.ok('✓') : C.err('✗')) + (evidence ? ' ' + C.dim(String(evidence).slice(0, 80)) : C.dim(ok ? ' ok' : ' erro'));
71
+ }
28
72
  function needToken() {
29
73
  if (cfg.token) return cfg.token;
30
74
  console.error(ui.errLine(T.need_login));
@@ -824,22 +868,28 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
824
868
  let out;
825
869
  try {
826
870
  out = await agent.run(task, {
827
- token, lang: cfg.lang || 'pt', yes: YES, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
871
+ token, lang: cfg.lang || 'pt', yes: YES, autoAll: YOLO, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
828
872
  onThinking: () => sp.text(T.agent_thinking),
829
- onStep: ({ name, detail, blocked, loop, retry }) => {
830
- if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : 'started', tool: name, detail: detail || '' })); return; }
873
+ onStep: ({ name, detail, blocked, loop, retry, auto }) => {
874
+ if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
831
875
  sp.stop();
832
- const tag = retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
876
+ const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
833
877
  console.log(' ' + tag + ' ' + C.bold(name) + (detail ? C.dim(' · ' + detail) : ''));
834
878
  sp.start();
835
879
  },
836
- askApprove: async (cmd) => {
880
+ onStepDone: (e) => {
881
+ if (streamJson) return; // o evento 'result' já cobre; aqui é só a UI bonita
882
+ sp.stop();
883
+ console.log(stepDoneLine(e, ' '));
884
+ sp.start();
885
+ },
886
+ askApprove: async (payload) => {
837
887
  // headless (stream-json): não dá pra perguntar → NEGA o destrutivo e sinaliza o evento.
838
- if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
888
+ if (streamJson) { const cmd = payload && typeof payload === 'object' ? payload.cmd : payload; _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
839
889
  sp.stop();
840
- const ans = String(await ask(C.err('▲ ') + T.agent_approve(C.bold(cmd)))).trim().toLowerCase();
890
+ const r = await askApproveRoute(payload);
841
891
  sp.start();
842
- return ['s', 'sim', 'y', 'yes'].includes(ans);
892
+ return r;
843
893
  },
844
894
  onRemote: ({ ttl }) => {
845
895
  if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
@@ -1276,7 +1326,7 @@ async function metaCmd() {
1276
1326
  const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
1277
1327
  try {
1278
1328
  st = await metaMod.run(goalArg, {
1279
- token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
1329
+ token, lang: cfg.lang || 'pt', yes, autoAll: YOLO, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
1280
1330
  onAlert: ({ type, text }) => {
1281
1331
  sp.stop();
1282
1332
  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('⏱');
@@ -1290,12 +1340,13 @@ async function metaCmd() {
1290
1340
  onChecklist: (itens) => { sp.stop(); console.log(_metaChecklistBox(itens) + '\n'); sp.start(); },
1291
1341
  onRound: ({ n, item, attempt }) => { sp.stop(); console.log(' ' + C.indigo('◆') + ' ' + C.bold(T.meta_round(n, item.slice(0, 70), attempt))); sp.start(); },
1292
1342
  onThinking: () => sp.text(T.agent_thinking),
1293
- onStep: ({ name, detail, blocked, loop, retry }) => {
1343
+ onStep: ({ name, detail, blocked, loop, retry, auto }) => {
1294
1344
  sp.stop();
1295
- console.log(' ' + (retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
1345
+ console.log(' ' + (auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
1296
1346
  sp.start();
1297
1347
  },
1298
- askApprove: async (cmd) => { sp.stop(); const a = String(await ui.ask(C.err('▲ ') + T.agent_approve(C.bold(cmd)))).trim().toLowerCase(); sp.start(); return ['s', 'sim', 'y', 'yes'].includes(a); },
1348
+ onStepDone: (e) => { sp.stop(); console.log(stepDoneLine(e, ' ')); sp.start(); },
1349
+ askApprove: async (payload) => { sp.stop(); const r = await askApproveRoute(payload); sp.start(); return r; },
1299
1350
  onRemote: ({ ttl }) => { sp.stop(); console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120))); sp.start(); },
1300
1351
  onRoundDone: ({ checklist, spent }) => {
1301
1352
  sp.stop();
@@ -1753,7 +1804,9 @@ async function verificarCmd(args) {
1753
1804
  }
1754
1805
 
1755
1806
  if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
1756
- const rep = await verify.runAll(criteria, { cwd });
1807
+ // remoteExec: liga o verificador ldd remoto à conexão SSH ativa (binário PW mora na VPS).
1808
+ const _ssh = require('../lib/ssh');
1809
+ const rep = await verify.runAll(criteria, { cwd, remoteExec: _ssh.isConnected() ? (cmd => _ssh.exec(cmd, { timeoutMs: 30000 })) : undefined });
1757
1810
  if (JSON_OUT) { console.log(JSON.stringify(rep)); process.exit(rep.allOk ? 0 : 1); }
1758
1811
 
1759
1812
  for (const r of rep.results) {
@@ -1812,8 +1865,8 @@ async function runbookCmd(args) {
1812
1865
  cwd: process.cwd(),
1813
1866
  askApprove: async (cmd) => {
1814
1867
  if (YES || !process.stdin.isTTY) return false; // não-interativo não aprova destrutivo
1815
- const a = await ui.ask(C.warn(' ▲ ' + (en ? 'destructive step: ' : 'passo destrutivo: ')) + cmd + C.dim(' [s/N] '));
1816
- return ['s', 'sim', 'y', 'yes'].includes(String(a).trim().toLowerCase());
1868
+ const d = await askDestructive(String(cmd), { en }); // resumo curto + ver completo (v)
1869
+ return d === true || d === 'all'; // runbook aprova passo-a-passo (sem sessão full-auto)
1817
1870
  },
1818
1871
  onStep: (e) => {
1819
1872
  if (e.phase === 'start') console.log(' ' + C.indigo('◆') + ' ' + (e.desc ? C.bold(e.desc) + C.dim(' · ') : '') + C.dim(e.cmd.slice(0, 60)));
package/lib/agent.js CHANGED
@@ -63,6 +63,10 @@ REGRAS:
63
63
  - Windows: python -c multi-linha falha em silêncio — escreva um .py com escrever_arquivo e execute "python arquivo.py".
64
64
  - Se faltar dependência, instale (winget/apt/pip/npm) e prossiga.
65
65
  - LINUX/apt: NUNCA rode "apt-get install" cru. (a) Em máquina nova o unattended-upgrades segura o lock e a instalação PENDURA. (b) Install que baixa VÁRIOS pacotes (gcc, mariadb, tomcat…) leva mais de 60s e MORRE no timeout padrão → nada instala. Instale SEMPRE assim, com timeout LONGO: chame executar_comando com timeout_s: 300 e comando "sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 install -y <pacotes>" (Lock::Timeout ESPERA o lock; noninteractive+-y evita prompts sem TTY; timeout_s:300 dá 5min pro download). PPA com "add-apt-repository -y" + "apt-get update". Se um comando de install voltar vazio/sem efeito, quase sempre foi timeout curto — refaça com timeout_s: 300.
66
+ - FONTE AUTÊNTICA antes de improvisar: antes de GERAR do zero um arquivo estruturado (config, schema SQL, .conf, dados, manifesto), PROCURE se já existe a versão real — no projeto atual, no pack/instalação de referência que o usuário citou, ou numa instalação funcionante. Se existir, USE-A (copie/adapte o mínimo). A versão inventada de memória quase sempre sai sutilmente errada/incompleta (falta uma linha, uma procedure, um valor). Só improvise se comprovadamente não existir — e nesse caso AVISE que é uma reconstrução (pode ter erro) e peça pra validar.
67
+ - PRIVILÉGIO MÍNIMO em infra: pra dar acesso de uma aplicação a um serviço (banco, broker), crie uma CREDENCIAL DEDICADA com o mínimo de permissão (ex: CREATE USER 'app' + GRANT só no schema necessário) e aponte a app pra ela. NUNCA altere a autenticação/config GLOBAL existente (root/admin) só pra uma app conectar — isso afeta TUDO que já usa aquele acesso (backups, outros serviços, seu próprio acesso). Se precisar MESMO mudar algo global, AVISE explicitamente o que mais será afetado e por quê.
68
+ - NUNCA peça segredo ao usuário: jamais peça senha/chave/token/credencial no chat. Se uma operação remota (ssh/scp) falhar por autenticação, REUSE a conexão/chave que JÁ funcionou nesta sessão (mesmo host/user/chave — não reconstrua o comando do zero esquecendo o -i da chave). Se ainda assim não conectar, peça pro usuário EXECUTAR a ação — nunca entregar o segredo.
69
+ - BINÁRIO NATIVO: ao subir/instalar um executável numa máquina, valide com file (arquitetura: 32 vs 64-bit) E ldd (as bibliotecas resolvem?) ANTES de declarar "pronto/instalado" — "está no lugar" NÃO é "roda". Se ldd mostrar "not found", instale a lib faltante e revalide.
66
70
  - Termine SEMPRE com um resumo curto: o que foi feito, resultado e caminhos de arquivos criados/alterados.
67
71
  - Responda no idioma do usuário (padrão: português do Brasil).`
68
72
  : 'You are the Terminal Smart agent running ON THIS machine via CLI, with REAL tools. Act for real — never say "I will do it" without calling the tool in the same turn, and never invent results that did not come from a tool.'
@@ -80,6 +84,10 @@ RULES:
80
84
  - Windows: multi-line python -c fails silently — write a .py with escrever_arquivo and run "python file.py".
81
85
  - If a dependency is missing, install it (winget/apt/pip/npm) and continue.
82
86
  - LINUX/apt: NEVER run a bare "apt-get install" — on a freshly-created machine unattended-upgrades holds the apt lock and the install HANGS silently (no output). ALWAYS install like this: "sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 install -y <packages>" (the -o Lock::Timeout WAITS for the lock up to 10min instead of hanging; noninteractive+-y avoids prompts that block without a TTY). Use "add-apt-repository -y" + "apt-get update" for PPAs. A command that doesn't return in ~1min is likely stuck on the lock/prompt — don't wait forever.
87
+ - AUTHENTIC SOURCE before improvising: before GENERATING a structured file from scratch (config, SQL schema, .conf, data, manifest), SEARCH for whether a real version already exists — in the current project, in the reference pack/install the user mentioned, or in a working install. If it exists, USE IT (copy/adapt minimally). A version invented from memory almost always comes out subtly wrong/incomplete (a missing line, a missing procedure, a wrong value). Only improvise if it truly doesn't exist — and then WARN that it's a reconstruction (may have errors) and ask to validate.
88
+ - LEAST PRIVILEGE in infra: to give an app access to a service (DB, broker), create a DEDICATED credential with minimal permission (e.g. CREATE USER 'app' + GRANT on the needed schema only) and point the app at it. NEVER alter the existing GLOBAL auth/config (root/admin) just to make an app connect — that affects EVERYTHING already using that access (backups, other services, your own access). If you truly must change something global, explicitly WARN what else will be affected and why.
89
+ - NEVER ask the user for a secret: never ask for a password/key/token/credential in chat. If a remote op (ssh/scp) fails on auth, REUSE the connection/key that ALREADY worked this session (same host/user/key — don't rebuild the command dropping the -i key flag). If it still won't connect, ask the user to PERFORM the action — never to hand over the secret.
90
+ - NATIVE BINARY: when uploading/installing an executable on a machine, validate with file (architecture: 32 vs 64-bit) AND ldd (do the libraries resolve?) BEFORE declaring "done/installed" — "it's in place" is NOT "it runs". If ldd shows "not found", install the missing lib and re-validate.
83
91
  - ALWAYS end with a short summary: what was done, the result, and paths of created/changed files.
84
92
  - Answer in the user's language.`);
85
93
  const volatile = `\n\nMáquina: ${process.platform} ${os.release()} · host ${os.hostname()} · user ${os.userInfo().username}`
@@ -153,6 +161,15 @@ function loopDecision({ nSig, cycling, warnedThis, loopWarned, WARN, BREAK }) {
153
161
  return 'ok';
154
162
  }
155
163
 
164
+ // Decisão de aprovação de passo destrutivo (PURO/testável, fonte única do mapeamento).
165
+ // decision vem do askApprove interativo: true = aprovou este · 'all' = aprovar TUDO na sessão
166
+ // (full-auto) · false = recusou. autoAll = a sessão já está em full-auto (aprova sem perguntar).
167
+ function decideApproval({ autoAll, decision }) {
168
+ if (autoAll) return { approved: true, enableAll: true };
169
+ if (decision === 'all') return { approved: true, enableAll: true };
170
+ return { approved: decision === true, enableAll: false };
171
+ }
172
+
156
173
  // SUB-AGENTE de exploração (padrão Claude Code): contexto PRÓPRIO, só-leitura, poucos passos.
157
174
  // A leitura pesada acontece AQUI e só o RESUMO volta pro agente principal → economia de contexto.
158
175
  async function _subAgent({ task, k, model, cwd, lang, onStep }) {
@@ -215,10 +232,17 @@ async function _remoteApprove(comando, token, onRemote) {
215
232
  /**
216
233
  * Roda o agente. opts:
217
234
  * token (sessão ts) · lang · yes (auto-aprova NÃO-destrutivos; destrutivo recusa)
218
- * onStep({name, detail}) passo executado (pra UI) · askApprove(cmd) Promise<boolean>
235
+ * autoAll (FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos o gate anti-catástrofe
236
+ * de AUTO-destrutivos segue recusando na hora, full-auto não abre mão dele)
237
+ * onStep({name, detail, auto}) — passo iniciado (pra UI; auto=destrutivo aprovado em full-auto)
238
+ * onStepDone({name, ok, status, evidence}) — passo concluído (✓/✗ por etapa)
239
+ * askApprove(cmd|{kind,cmd}) → Promise<boolean|'all'> ('all' liga o full-auto na sessão)
219
240
  */
220
241
  async function run(task, opts = {}) {
221
- const { token, lang = 'pt', yes = false, model = null, confineDir = null, onStep = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
242
+ const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
243
+ // FULL-AUTO (autoridade total): aprova destrutivos sem perguntar. Começa por --yolo/--full-auto
244
+ // (opts.autoAll) e pode ser LIGADO no meio da sessão quando o humano responde "a" (aprovar tudo).
245
+ let autoApproveDestructive = !!autoAll;
222
246
  // CWD da SESSÃO: base de todo caminho relativo e do shell. Persiste entre
223
247
  // mensagens (o chamador passa opts.cwd e lê o out.cwd de volta). Missão usa a
224
248
  // pasta confinada. É MUTÁVEL: a ferramenta mudar_diretorio e o "cd" no início
@@ -465,6 +489,7 @@ async function run(task, opts = {}) {
465
489
  const name = (tc.function && tc.function.name) || '';
466
490
  let input = {}; try { input = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
467
491
  let result;
492
+ let _ran = false; // true só quando uma ferramenta REALMENTE executou (não gate/bloqueio) → status ✓/✗
468
493
 
469
494
  // ── WATCHDOG anti-loop: mede repetição ANTES de qualquer gate/execução ──
470
495
  const _sig = loopSig(name, input);
@@ -488,8 +513,8 @@ async function run(task, opts = {}) {
488
513
  _warnedSigs.add(_sig); _loopWarned = true;
489
514
  onStep({ name, detail: argsShort(name, input), loop: true });
490
515
  result = { erro: lang !== 'en'
491
- ? `LOOP DETECTADO: você já fez "${name}(${argsShort(name, input)})" ${_nSig}× e o resultado NÃO muda. PARE de repetir — mude de abordagem (outra ferramenta/estratégia) OU conclua honestamente. Repetir de novo encerra a missão.`
492
- : `LOOP DETECTED: you already did "${name}(${argsShort(name, input)})" ${_nSig}× and the result is NOT changing. STOP repeating — change approach (different tool/strategy) OR conclude honestly. Repeating again ends the mission.` };
516
+ ? `LOOP DETECTADO: você já fez "${name}(${argsShort(name, input)})" ${_nSig}× e o resultado NÃO muda. PARE de repetir. Chutar não resolve à CAUSA RAIZ: LEIA a fonte autêntica (o código-fonte que gera o erro, o .conf/manual real, o log completo) com ler_arquivo/buscar_codigo/explorar, entenda o formato exato e AÍ conserte. Se for grande demais pra este modo, diga que precisa escalar (o "ts meta" chama o modelo forte). Repetir a mesma ação de novo encerra a missão.`
517
+ : `LOOP DETECTED: you already did "${name}(${argsShort(name, input)})" ${_nSig}× and the result is NOT changing. STOP repeating. Guessing won't fix it go to the ROOT CAUSE: READ the authentic source (the source code that emits the error, the real .conf/manual, the full log) with ler_arquivo/buscar_codigo/explorar, learn the exact format, THEN fix. If it's too big for this mode, say you need to escalate ("ts meta" calls the strong model). Repeating the same action again ends the mission.` };
493
518
  }
494
519
 
495
520
  // ENFORCEMENT do modo só-leitura PRIMEIRO: bloqueia ferramentas que alteram/rodam ANTES de
@@ -517,9 +542,30 @@ async function run(task, opts = {}) {
517
542
  // Interativo pergunta no terminal; em --yes (cron) tenta APROVAÇÃO REMOTA no Telegram do dono.
518
543
  if (result === undefined && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
519
544
  let approved = false, remoteTried = false;
520
- if (!yes) approved = await askApprove(String(input.comando || ''));
521
- else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
522
- if (!approved) {
545
+ if (autoApproveDestructive) {
546
+ // FULL-AUTO não abre mão do anti-catástrofe: os auto-destrutivos de máquina/processo/
547
+ // pasta já caíram no gate acima (selfDestructiveReason). O que sobra é ~/.ts por shell
548
+ // (bypass dos gates dedicados skill_gerenciar/hooks) — esse NUNCA é auto-aprovado nem em
549
+ // --yolo: exige o humano. Todo o resto: aprova sem perguntar, mas VISÍVEL (auto:true).
550
+ if (core.touchesTsConfig(String(input.comando || ''))) {
551
+ onStep({ name, detail: argsShort(name, input), blocked: true });
552
+ result = { erro: lang !== 'en'
553
+ ? 'FULL-AUTO (--yolo) NÃO auto-aprova comando que mexe em ~/.ts (skills/hooks/mcp/config do ts via shell) — isso burlaria os gates dedicados. Use a ferramenta própria (skill_gerenciar) ou rode SEM --yolo pra aprovar manualmente.'
554
+ : 'FULL-AUTO (--yolo) will NOT auto-approve a command that touches ~/.ts (ts skills/hooks/mcp/config via shell) — it would bypass the dedicated gates. Use the proper tool (skill_gerenciar) or run WITHOUT --yolo to approve manually.' };
555
+ } else {
556
+ approved = true;
557
+ onStep({ name, detail: argsShort(name, input), auto: true });
558
+ }
559
+ } else if (!yes) {
560
+ // Passa payload ESTRUTURADO → a UI mostra um RESUMO curto (não o heredoc inteiro).
561
+ // warning: aviso ESPECÍFICO de efeito colateral se o comando muda estado global (ex: ALTER USER root).
562
+ const _cmd = String(input.comando || '');
563
+ const dec = await askApprove({ kind: 'destructive', cmd: _cmd, warning: core.globalStateWarning(_cmd) });
564
+ const d = decideApproval({ autoAll: false, decision: dec });
565
+ approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
566
+ } else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
567
+ // result === undefined evita sobrescrever a mensagem específica do bloqueio ~/.ts do full-auto
568
+ if (!approved && result === undefined) {
523
569
  result = { erro: yes
524
570
  ? (remoteTried
525
571
  ? 'RECUSADO: o dono NEGOU (ou não respondeu em 2 min) a aprovação remota deste comando destrutivo. Não repita; siga sem ele e relate no resumo.'
@@ -549,7 +595,8 @@ async function run(task, opts = {}) {
549
595
  const prev = input.acao === 'criar'
550
596
  ? `criar skill "${_cl(input.slug)}" — ${_cl(input.descricao || input.nome).slice(0, 120)}\n motivo: ${_cl(input.motivo).slice(0, 200)}\n instruções:\n ${_full.slice(0, CAP)}${_nota}`
551
597
  : `melhorar skill "${_cl(input.slug)}"\n motivo: ${_cl(input.motivo).slice(0, 200)}\n trocar: ${_cl(input.buscar || (lang !== 'en' ? '(adicionar seção no fim)' : '(append section at the end)')).slice(0, 400)}\n por: ${_full.slice(0, Math.min(CAP, 400))}${_nota}`;
552
- const okS = await askApprove((lang !== 'en' ? 'O agente quer ATUALIZAR as próprias skills:\n ' : 'The agent wants to UPDATE its own skills:\n ') + prev);
598
+ const okS = autoApproveDestructive ? true : await askApprove((lang !== 'en' ? 'O agente quer ATUALIZAR as próprias skills:\n ' : 'The agent wants to UPDATE its own skills:\n ') + prev);
599
+ if (okS === 'all') autoApproveDestructive = true;
553
600
  if (!okS) {
554
601
  result = { erro: lang !== 'en'
555
602
  ? 'O usuário RECUSOU a mudança de skill. Não repita nesta sessão; siga com a tarefa normalmente.'
@@ -566,8 +613,12 @@ async function run(task, opts = {}) {
566
613
  const _js = JSON.stringify(input);
567
614
  const _lbl = 'MCP ' + _mcp.route[name].server + ' → ' + _mcp.route[name].realName + ' ' + (_js.length > 1500 ? _js.slice(0, 1500) + ' …(+' + (_js.length - 1500) + ' chars)' : _js);
568
615
  let approved = false, remoteTried = false;
569
- if (!yes) approved = await askApprove(_lbl);
570
- else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
616
+ if (autoApproveDestructive) { approved = true; onStep({ name, detail: argsShort(name, input), auto: true }); }
617
+ else if (!yes) {
618
+ const dec = await askApprove(_lbl);
619
+ const d = decideApproval({ autoAll: false, decision: dec });
620
+ approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
621
+ } else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
571
622
  if (!approved) {
572
623
  result = { erro: yes
573
624
  ? (remoteTried ? 'RECUSADO: o dono NEGOU (ou não respondeu) a aprovação remota desta ferramenta MCP mutante. Não repita; siga sem ela.' : 'RECUSADO automaticamente: ferramenta MCP mutante não roda em --yes sem aprovação remota (Telegram). Siga sem ela.')
@@ -589,8 +640,9 @@ async function run(task, opts = {}) {
589
640
  // 1ª chamada a ESTE servidor na sessão pede um OK (mesmo tool de leitura): os ARGUMENTOS
590
641
  // saem da máquina pro servidor externo, e podem ter sido influenciados por prompt-injection
591
642
  // de algo que o agente leu. Em --yes (cron) o dono já pré-autorizou os servers → não pergunta.
592
- if (!rt.needsApproval && !yes && !_mcpSeen.has(rt.server)) {
643
+ if (!rt.needsApproval && !yes && !autoApproveDestructive && !_mcpSeen.has(rt.server)) {
593
644
  const okFirst = await askApprove('1ª chamada ao servidor MCP "' + rt.server + '" (tool ' + rt.realName + '). Dados sairão pra ele. Permitir este servidor nesta sessão?');
645
+ if (okFirst === 'all') autoApproveDestructive = true;
594
646
  if (!okFirst) { result = { erro: 'O usuário NÃO autorizou o servidor MCP "' + rt.server + '" nesta sessão. Não use tools desse servidor; siga sem elas.' }; onStep({ name, detail: argsShort(name, input), blocked: true }); }
595
647
  }
596
648
  if (result === undefined) {
@@ -598,7 +650,7 @@ async function run(task, opts = {}) {
598
650
  onStep({ name: 'mcp:' + rt.realName, detail: rt.server });
599
651
  try { result = { resultado: await require('./mcp').callTool(rt.endpoint, rt.auth, rt.realName, input) }; }
600
652
  catch (e) { result = { erro: 'MCP falhou: ' + String((e && e.message) || e).slice(0, 200) }; }
601
- steps++;
653
+ steps++; _ran = true;
602
654
  }
603
655
  }
604
656
  // LIMITE DE PESQUISA (convergência): conta buscar_web + navegador abrir/ler. Ao passar o
@@ -636,7 +688,7 @@ async function run(task, opts = {}) {
636
688
  }
637
689
  else result = { erro: 'acao inválida — use abrir|clicar|digitar|ler|print.' };
638
690
  } catch (e) { result = { erro: String((e && e.message) || e).slice(0, 300) }; }
639
- steps++;
691
+ steps++; _ran = true;
640
692
  }
641
693
  // SUB-AGENTE: 'explorar' roda em contexto próprio (só-leitura) e devolve só o resumo.
642
694
  if (result === undefined && name === 'explorar') {
@@ -644,12 +696,12 @@ async function run(task, opts = {}) {
644
696
  const sub = await _subAgent({ task: input.tarefa || input.pergunta || '', k, model, cwd, lang, onStep });
645
697
  acc.inTok += sub._tin; acc.outTok += sub._tout; acc.cachedTok += sub._tcach || 0;
646
698
  result = { resumo: sub.resumo };
647
- steps++;
699
+ steps++; _ran = true;
648
700
  }
649
701
  if (result === undefined) {
650
702
  onStep({ name, detail: argsShort(name, input) });
651
703
  result = await tools.execute(name, input, { confineDir, baseDir: cwd, token });
652
- steps++;
704
+ steps++; _ran = true;
653
705
  // TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
654
706
  // sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
655
707
  // não falha de execução — não conta pro ledger nem pro sinal de erro da run.
@@ -681,6 +733,10 @@ async function run(task, opts = {}) {
681
733
  && _researchCalls >= Math.ceil(RESEARCH_MAX * 2 / 3) && _researchCalls <= RESEARCH_MAX) {
682
734
  result = Object.assign({}, result, { _aviso: 'Você já pesquisou ' + _researchCalls + 'x (limite ' + RESEARCH_MAX + '). Se já tem o suficiente, PARE de pesquisar e ENTREGUE o resultado agora.' });
683
735
  }
736
+ // STATUS POR ETAPA (UX): ✓ verde / ✗ vermelho por passo, a partir do ToolResult TIPADO
737
+ // (core.classifyToolResult) — o usuário vê na hora o que deu certo/errado (antes as ⚙
738
+ // não indicavam resultado nenhum). Só para passos que REALMENTE rodaram (não gate/bloqueio).
739
+ if (_ran) { const _c = core.classifyToolResult(result); onStepDone({ name, ok: _c.ok, status: _c.status, evidence: _c.evidence }); }
684
740
  // BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz →
685
741
  // encerra o turno devolvendo o pedido; a missão pausa e chama o usuário.
686
742
  if (result && result._needHuman) {
@@ -731,4 +787,4 @@ async function run(task, opts = {}) {
731
787
  return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr };
732
788
  }
733
789
 
734
- module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision } };
790
+ module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval } };
package/lib/core.js CHANGED
@@ -12,9 +12,37 @@
12
12
  // — só funções puras e constantes, pra ser importável por qualquer superfície e testável.
13
13
 
14
14
  // ── 1) GATE DE DESTRUTIVO ─────────────────────────────────────────────────────
15
+ // ~/.ts = config do agente (skills viram INSTRUÇÕES futuras; hooks executam comandos;
16
+ // mcp.json guarda auth). Tocar nisso por shell burlaria os gates dedicados
17
+ // (skill_gerenciar/lembrar) → prompt-injection persistente. Exige a barra antes de ".ts/"
18
+ // — não casa arquivos TypeScript (utils.ts). É o subconjunto "endurecido" do gate destrutivo:
19
+ // o ÚNICO destrutivo que o FULL-AUTO (--yolo) NÃO libera sozinho — sempre exige o humano.
20
+ const TS_CONFIG_RE = /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i;
21
+ function touchesTsConfig(cmd) { return TS_CONFIG_RE.test(String(cmd || '')); }
22
+
23
+ // Comandos que mudam ESTADO GLOBAL de um serviço/host: o efeito colateral vai ALÉM do que o
24
+ // usuário pediu. Cada um traz um AVISO ESPECÍFICO (não "destrutivo" genérico) pra a aprovação.
25
+ // Nasceu de um ALTER USER root que fez uma app conectar MAS quebrou o `sudo mysql` sem senha.
26
+ const GLOBAL_STATE = [
27
+ [/\bALTER\s+USER\b[^;]*IDENTIFIED\s+BY\b/i, 'muda a senha/auth de um usuário do banco — quebra TUDO que já conecta com ele (backups, sudo mysql, outros serviços). O certo é criar um usuário DEDICADO (CREATE USER + GRANT no schema) em vez de mexer no root.'],
28
+ [/\b(SET\s+PASSWORD|GRANT\s+ALL\s+PRIVILEGES)\b/i, 'muda credencial/privilégio GLOBAL do banco — afeta tudo que usa esse acesso.'],
29
+ [/\bDROP\s+USER\b/i, 'remove um usuário do banco — quebra tudo que autentica com ele.'],
30
+ [/\/etc\/ssh\/sshd_config/i, 'edita a config do SSH do host — pode te TRANCAR pra fora (porta/auth). Faça backup e não reinicie o sshd sem confirmar que ainda entra.'],
31
+ [/\b(ufw|iptables)\b[^\n]*(-F|flush|reset|\bdeny\b|\bDROP\b)/i, 'mexe no firewall do host — pode cortar seu PRÓPRIO acesso SSH.'],
32
+ [/\busermod\b|\/etc\/sudoers|\bvisudo\b/i, 'muda usuários/sudoers do sistema — efeito global de permissão.'],
33
+ [/\bchmod\b[^\n]*\/etc\b|\bchown\b[^\n]*\/etc\b/i, 'muda permissão/dono em /etc — config global do sistema.'],
34
+ ];
35
+ // aviso específico do efeito colateral, ou null se o comando não mexe em estado global.
36
+ function globalStateWarning(cmd) {
37
+ const s = String(cmd || '');
38
+ for (const [re, msg] of GLOBAL_STATE) if (re.test(s)) return msg;
39
+ return null;
40
+ }
41
+
15
42
  // Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
16
43
  // (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
17
44
  const DESTRUCTIVE = [
45
+ ...GLOBAL_STATE.map(g => g[0]), // mudanças de estado global também passam pela aprovação
18
46
  /\brm\s+(-[a-z]*[rf][a-z]*\s+)/i, /\brm\s+.*\*/, /\brmdir\b/i, /\bdel\s+\/[sq]/i, /\brd\s+\/s/i,
19
47
  /\bmkfs\b/i, /\bdd\s+if=/i, /(?:^|[\s&;|])format\s+[a-z]:/i, /\bdiskpart\b/i, /> ?\/dev\/sd/i,
20
48
  /\bshutdown\b/i, /\breboot\b/i, /\bhalt\b/i, /\bpoweroff\b/i,
@@ -25,11 +53,7 @@ const DESTRUCTIVE = [
25
53
  /\btaskkill\b.*\/im\b/i, /\btaskkill\b.*\/f\b.*\/im/i, // taskkill /IM mata TODOS os processos daquele nome (ex: node.exe = a própria missão)
26
54
  /\bStop-Process\b[^|;&]*-Name\b/i, /\bGet-Process\b[^|]*\|[^|]*\bStop-Process\b/i, // PowerShell: matar por NOME mata todos (=taskkill /IM); Stop-Process -Id <pid> segue liberado
27
55
  /\bsystemctl\s+(stop|disable|mask)\b/i, /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/i,
28
- // ~/.ts = config do agente (skills viram INSTRUÇÕES futuras; hooks executam comandos;
29
- // mcp.json guarda auth). Tocar nisso por shell burlaria os gates dedicados
30
- // (skill_gerenciar/lembrar) → prompt-injection persistente. Sempre pede aprovação.
31
- // Exige a barra antes de ".ts/" — não casa arquivos TypeScript (utils.ts).
32
- /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i,
56
+ TS_CONFIG_RE, // ~/.ts por shell = bypass dos gates dedicados (ver comentário acima)
33
57
  ];
34
58
  function isDestructive(cmd) { return DESTRUCTIVE.some(re => re.test(String(cmd || ''))); }
35
59
 
@@ -141,7 +165,8 @@ function classifyToolResult(raw) {
141
165
  }
142
166
 
143
167
  module.exports = {
144
- DESTRUCTIVE, isDestructive,
168
+ DESTRUCTIVE, isDestructive, touchesTsConfig,
169
+ GLOBAL_STATE, globalStateWarning,
145
170
  MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
146
171
  AgentEvents, EVENT_TYPES,
147
172
  ERROR_CLASSES, RETRYABLE_CLASSES, classifyError, classifyToolResult,
package/lib/i18n.js CHANGED
@@ -41,6 +41,7 @@ const STR = {
41
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
+ ['ts agente "..." --yolo', 'FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos (o gate anti-catástrofe ~/.ts/máquina segue ativo)'],
44
45
  ['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
45
46
  ['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
46
47
  ['ts meta "..." --criterios prova.json', 'só declara "verificada" se os critérios executáveis (verify) passarem de verdade'],
@@ -197,6 +198,7 @@ const STR = {
197
198
  ['ts agente "task"', 'actually does it here: commands, files, diagnosis'],
198
199
  ['ts agente "..." --yes', 'autonomous (destructive asks on Telegram)'],
199
200
  ['ts agente --continuar "..."', 'resume this folder\'s previous work'],
201
+ ['ts agente "..." --yolo', 'FULL-AUTO: approves EVERYTHING this session, destructive included (~/.ts & machine anti-catastrophe still on)'],
200
202
  ['ts agente "..." --navegador', 'EXPERIMENTAL: gives the agent a real Chrome (open/read/click/print+vision)'],
201
203
  ['ts sentinela add "name" --cmd "..."', 'DETERMINISTIC watch (if-rule, ZERO tokens); escalates to AI only on failure'],
202
204
  ['ts sentinela instalar', 'schedule checks on the OS (cron/Task Scheduler), alert on Telegram'],
package/lib/meta.js CHANGED
@@ -1028,7 +1028,7 @@ async function _checkCriteria(st, dir) {
1028
1028
  }
1029
1029
 
1030
1030
  async function run(goal, opts = {}) {
1031
- 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, prove = false } = opts;
1031
+ const { token, lang = 'pt', yes = false, autoAll = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true, prove = false } = opts;
1032
1032
  const onChecklist = opts.onChecklist || (() => {});
1033
1033
  const onRound = opts.onRound || (() => {});
1034
1034
  const onRoundDone = opts.onRoundDone || (() => {});
@@ -1320,7 +1320,7 @@ async function run(goal, opts = {}) {
1320
1320
  }
1321
1321
  let out = null, connErr = null;
1322
1322
  for (let att = 0; att < 3 && !out; att++) {
1323
- try { out = await agent.run(task, { token, lang, yes, model: roundModel, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking }); }
1323
+ try { out = await agent.run(task, { token, lang, yes, autoAll, model: roundModel, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking }); }
1324
1324
  catch (e) {
1325
1325
  connErr = e;
1326
1326
  // teto de IA estourado → PAUSA limpa e resumível com CTA de upgrade (nunca segue em silêncio)
package/lib/ssh.js CHANGED
@@ -6,7 +6,8 @@ const path = require('path');
6
6
  const os = require('os');
7
7
 
8
8
  let _conn = null; // cliente ssh2 conectado
9
- let _info = null; // { host, port, user }
9
+ let _info = null; // { host, port, user, keyPath } — some no 'close'
10
+ let _lastGood = null; // ÚLTIMO login que DEU CERTO nesta sessão { host, port, user, keyPath } — SOBREVIVE à queda
10
11
  let Client = null;
11
12
  try { Client = require('ssh2').Client; } catch (_) { /* ssh2 ausente → erro amigável no connect */ }
12
13
 
@@ -47,17 +48,22 @@ function parseLoginFile(p) {
47
48
 
48
49
  function isConnected() { return !!_conn; }
49
50
  function info() { return _info; }
51
+ // Último login que funcionou nesta sessão — pra reconectar sem o usuário reinformar host/chave,
52
+ // e pra o agente NUNCA "esquecer" que já entrou (foi o que causou IP errado e scp sem -i).
53
+ function lastGood() { return _lastGood; }
50
54
 
51
55
  function connect(opts = {}) {
52
56
  return new Promise((resolve, reject) => {
53
57
  if (!Client) return reject(new Error('Biblioteca ssh2 não instalada no CLI.'));
54
58
  const prof = loadProfile() || {};
55
- const host = opts.host || prof.host;
56
- const port = opts.port || prof.port || 22;
57
- const user = opts.user || prof.user || 'root';
59
+ // ordem de resolução: o que foi passado agora → perfil salvo → ÚLTIMO login que deu certo nesta sessão.
60
+ // assim, se a conexão caiu, um conectar_vps SEM args reusa host+chave que já funcionaram (não pede nada).
61
+ const host = opts.host || prof.host || (_lastGood && _lastGood.host);
62
+ const port = opts.port || prof.port || (_lastGood && _lastGood.port) || 22;
63
+ const user = opts.user || prof.user || (_lastGood && _lastGood.user) || 'root';
58
64
  let password = opts.pass || prof.pass;
59
65
  let privateKey;
60
- const keyPath = opts.keyPath || prof.keyPath;
66
+ const keyPath = opts.keyPath || prof.keyPath || (_lastGood && _lastGood.keyPath);
61
67
  if (keyPath) { try { privateKey = fs.readFileSync(keyPath); } catch (_) {} }
62
68
  if (!host) return reject(new Error('Sem host — configure com "ts vps set" ou passe --host.'));
63
69
  if (!password && !privateKey) return reject(new Error('Sem senha nem chave — configure a credencial.'));
@@ -66,7 +72,12 @@ function connect(opts = {}) {
66
72
  const c = new Client();
67
73
  const cfg = { host, port, username: user, readyTimeout: 20000, keepaliveInterval: 15000 };
68
74
  if (privateKey) cfg.privateKey = privateKey; else cfg.password = password;
69
- c.on('ready', () => { _conn = c; _info = { host, port, user }; resolve({ host, port, user }); });
75
+ c.on('ready', () => {
76
+ _conn = c;
77
+ _info = { host, port, user, keyPath: keyPath || null };
78
+ _lastGood = { host, port, user, keyPath: keyPath || null }; // grava a conexão boa (sobrevive ao close)
79
+ resolve({ host, port, user });
80
+ });
70
81
  c.on('error', (e) => { _conn = null; reject(new Error('Falha ao conectar: ' + (e && e.message || e))); });
71
82
  c.on('close', () => { if (_conn === c) { _conn = null; _info = null; } });
72
83
  try { c.connect(cfg); } catch (e) { reject(e); }
@@ -90,4 +101,4 @@ function exec(cmd, { timeoutMs = 120000 } = {}) {
90
101
 
91
102
  function disconnect() { if (_conn) { try { _conn.end(); } catch (_) {} _conn = null; _info = null; return true; } return false; }
92
103
 
93
- module.exports = { connect, exec, disconnect, isConnected, info, loadProfile, saveProfile, parseLoginFile };
104
+ module.exports = { connect, exec, disconnect, isConnected, info, lastGood, loadProfile, saveProfile, parseLoginFile };
package/lib/tools.js CHANGED
@@ -368,8 +368,16 @@ async function execute(name, input, opts = {}) {
368
368
  try {
369
369
  const r = await ssh.connect({ host: input.host, user: input.usuario, keyPath: input.chave });
370
370
  const who = await ssh.exec('echo "$(whoami)@$(hostname) | $(. /etc/os-release 2>/dev/null; echo $PRETTY_NAME) | pwd=$(pwd)"');
371
- return { conectado: true, servidor: `${r.user}@${r.host}:${r.port}`, info: (who.stdout || '').trim() };
372
- } catch (e) { return { conectado: false, erro: String(e.message).slice(0, 300) + ' — configure com "ts vps set" ou passe host/usuario/chave.' }; }
371
+ // memoriza a conexão boa: nas próximas vezes basta executar_remoto (não reconstrua ssh/scp cru).
372
+ return { conectado: true, servidor: `${r.user}@${r.host}:${r.port}`, info: (who.stdout || '').trim(), memorizado: 'Conexão ativa e memorizada use executar_remoto/enviar_arquivo (NÃO monte ssh/scp cru) e não repasse host/chave.' };
373
+ } catch (e) {
374
+ // se JÁ conectamos com sucesso nesta sessão, o certo é REUSAR (não pedir credencial ao usuário).
375
+ const lg = ssh.lastGood && ssh.lastGood();
376
+ const dica = lg
377
+ ? ` — mas você JÁ conectou nesta sessão em ${lg.user}@${lg.host}${lg.keyPath ? ' (chave ' + require('path').basename(lg.keyPath) + ')' : ''}. Chame conectar_vps SEM argumentos pra reusar essa conexão; NÃO peça senha ao usuário.`
378
+ : ' — configure com "ts vps set" ou passe host/usuario/chave.';
379
+ return { conectado: false, erro: String(e.message).slice(0, 300) + dica };
380
+ }
373
381
  }
374
382
  case 'executar_remoto': {
375
383
  const ssh = require('./ssh');
package/lib/ui.js CHANGED
@@ -99,8 +99,20 @@ function ask(question) {
99
99
  });
100
100
  }
101
101
 
102
+ // Resumo de 1 linha de um comando pro prompt de aprovação: a 1ª linha SIGNIFICATIVA (que
103
+ // num heredoc é o comando externo, ex "mysql -u root pw <<'SQL'") + quantas linhas ficam
104
+ // escondidas. PURO/testável — a cor e as teclas do prompt ficam no bin. Evita despejar 50+
105
+ // linhas de um heredoc SQL antes do usuário decidir.
106
+ function cmdSummary(cmd, max = 72) {
107
+ const raw = String(cmd == null ? '' : cmd);
108
+ const lines = raw.split('\n');
109
+ const first = (lines.find(l => l.trim()) || raw).trim();
110
+ const shown = first.length > max ? first.slice(0, max - 1) + '…' : first;
111
+ return { shown, hidden: Math.max(0, lines.length - 1), lines: lines.length };
112
+ }
113
+
102
114
  const okLine = (s) => ' ' + C.ok('✔') + ' ' + s;
103
115
  const errLine = (s) => ' ' + C.err('✖') + ' ' + s;
104
116
  const infoLine = (s) => ' ' + C.cyan('⌁') + ' ' + s;
105
117
 
106
- module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, okLine, errLine, infoLine };
118
+ module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, cmdSummary, okLine, errLine, infoLine };
package/lib/verify.js CHANGED
@@ -61,6 +61,35 @@ function vPort(c) {
61
61
  });
62
62
  }
63
63
 
64
+ // Analisa a saída de `ldd`: quais libs NÃO resolvem. Separado pra ser testável sem ldd real.
65
+ // "arquivo está no lugar" ≠ "roda": foi o buraco do deploy que só checou `file` (arquitetura).
66
+ function _analyzeLdd(out) {
67
+ const s = String(out || '');
68
+ if (/not a dynamic executable|não é um executável dinâmico|statically linked/i.test(s)) return { ok: true, missing: [], detail: 'binário estático (sem deps dinâmicas)' };
69
+ const missing = (s.match(/^\s*(\S+)\s*=>\s*not found/gim) || []).map(l => l.trim().split(/\s+/)[0]);
70
+ if (missing.length) return { ok: false, missing, detail: 'libs faltando: ' + missing.join(', ') };
71
+ if (/=>/.test(s)) return { ok: true, missing: [], detail: 'todas as libs resolvem' };
72
+ return { ok: false, missing: [], detail: 'ldd sem saída válida (arquivo existe? é ELF?)' };
73
+ }
74
+ // opts.remoteExec(cmd) → Promise<{stdout,stderr}> é INJETADO por quem chama (bin/ts.js liga ao ssh),
75
+ // pra o verify.js seguir portável (só núcleo+builtins). Sem ele, ldd remoto avisa em vez de importar ssh.
76
+ async function vLdd(c, cwd, opts = {}) {
77
+ const target = c.remote ? String(c.path) : _abs(cwd, c.path);
78
+ const cmd = 'ldd ' + JSON.stringify(target);
79
+ let out = '';
80
+ try {
81
+ if (c.remote) {
82
+ if (typeof opts.remoteExec !== 'function') return { ok: false, detail: 'binário remoto mas sem executor SSH ligado — conecte a VPS antes', errorClass: 'network' };
83
+ const r = await opts.remoteExec(cmd);
84
+ out = (r.stdout || '') + (r.stderr || '');
85
+ } else {
86
+ out = execSync(cmd, { cwd, encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
87
+ }
88
+ } catch (e) { out = String(e.stdout || '') + String(e.stderr || e.message || ''); }
89
+ const a = _analyzeLdd(out);
90
+ return { ok: a.ok, detail: a.detail, errorClass: a.ok ? null : 'missing_dep', evidence: String(out).slice(0, 200).replace(/\s+/g, ' ') };
91
+ }
92
+
64
93
  const VERIFIERS = {
65
94
  command: (c, cwd) => vCommand(c, cwd),
66
95
  file_exists: (c, cwd) => vFileExists(c, cwd),
@@ -68,6 +97,7 @@ const VERIFIERS = {
68
97
  file_contains: (c, cwd) => vFileContains(c, cwd),
69
98
  http: (c) => vHttp(c),
70
99
  port: (c) => vPort(c),
100
+ ldd: (c, cwd, opts) => vLdd(c, cwd, opts),
71
101
  };
72
102
  const VERIFIER_TYPES = Object.keys(VERIFIERS);
73
103
 
@@ -77,7 +107,7 @@ async function runCriterion(c, opts = {}) {
77
107
  const type = c && c.type;
78
108
  const fn = VERIFIERS[type];
79
109
  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); }
110
+ try { const r = await fn(c, cwd, opts); return Object.assign({ type, label: c.label || _labelFor(c) }, r); }
81
111
  catch (e) { return { ok: false, type, label: c.label || _labelFor(c), detail: 'erro no verificador: ' + (e.message || e) }; }
82
112
  }
83
113
  function _labelFor(c) {
@@ -88,6 +118,7 @@ function _labelFor(c) {
88
118
  case 'file_contains': return c.path + (c.absent ? ' SEM ' : ' contém ') + '"' + String(c.text).slice(0, 30) + '"';
89
119
  case 'http': return (c.method || 'GET') + ' ' + c.url + ' → ' + (c.status || 200);
90
120
  case 'port': return 'porta ' + c.port + ' escutando';
121
+ case 'ldd': return 'libs de ' + c.path + ' resolvem' + (c.remote ? ' (VPS)' : '');
91
122
  default: return c.type || '?';
92
123
  }
93
124
  }
@@ -104,7 +135,7 @@ async function runAll(criteria, opts = {}) {
104
135
  // ── COMPILADOR de critérios (TaskSpec): o LLM/usuário PROPÕE, o harness VALIDA ────
105
136
  // Aceita só tipos conhecidos com os campos obrigatórios preenchidos; descarta o resto
106
137
  // (o modelo não impõe critério malformado). Determinístico. Retorna { criteria, dropped }.
107
- const _REQUIRED = { command: ['cmd'], file_exists: ['path'], file_absent: ['path'], file_contains: ['path', 'text'], http: ['url'], port: ['port'] };
138
+ const _REQUIRED = { command: ['cmd'], file_exists: ['path'], file_absent: ['path'], file_contains: ['path', 'text'], http: ['url'], port: ['port'], ldd: ['path'] };
108
139
  function compileCriteria(raw) {
109
140
  const list = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.criteria) ? raw.criteria : []);
110
141
  const criteria = [], dropped = [];
@@ -113,7 +144,7 @@ function compileCriteria(raw) {
113
144
  const missing = _REQUIRED[c.type].some(k => c[k] === undefined || c[k] === null || c[k] === '');
114
145
  if (missing) { dropped.push(c); continue; }
115
146
  const clean = { type: c.type };
116
- for (const k of ['cmd', 'exit', 'path', 'text', 'regex', 'flags', 'absent', 'url', 'method', 'status', 'contains', 'port', 'host', 'timeout_s', 'label']) if (c[k] !== undefined) clean[k] = c[k];
147
+ for (const k of ['cmd', 'exit', 'path', 'text', 'regex', 'flags', 'absent', 'url', 'method', 'status', 'contains', 'port', 'host', 'remote', 'timeout_s', 'label']) if (c[k] !== undefined) clean[k] = c[k];
117
148
  criteria.push(clean);
118
149
  }
119
150
  return { criteria, dropped };
@@ -128,4 +159,4 @@ function deriveFromStack(cwd) {
128
159
  return out;
129
160
  }
130
161
 
131
- module.exports = { runCriterion, runAll, compileCriteria, deriveFromStack, VERIFIERS, VERIFIER_TYPES, _labelFor };
162
+ module.exports = { runCriterion, runAll, compileCriteria, deriveFromStack, VERIFIERS, VERIFIER_TYPES, _labelFor, _analyzeLdd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.75.0",
3
+ "version": "0.77.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"