terminal-smart-cli 0.76.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
@@ -37,10 +37,12 @@ function fmtK(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
37
37
  // heredoc SQL a 1ª linha é o comando externo, ex "mysql -u root pw <<'SQL'"). Aceita:
38
38
  // s/N = sim/não neste passo · a = aprovar TUDO na sessão (full-auto) · v = ver o comando inteiro
39
39
  // Retorna true | false | 'all'.
40
- async function askDestructive(cmd, { en = false } = {}) {
40
+ async function askDestructive(cmd, { en = false, warning = null } = {}) {
41
41
  const raw = String(cmd || '');
42
42
  const { shown, hidden } = ui.cmdSummary(raw);
43
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));
44
46
  for (;;) {
45
47
  const q = C.warn(' ▲ ' + (en ? 'destructive: ' : 'destrutivo: ')) + C.bold(shown) + more
46
48
  + C.dim(en ? ' [y/N · a=all · v=view] ' : ' [s/N · a=tudo · v=ver] ');
@@ -61,7 +63,7 @@ async function askApproveRoute(payload) {
61
63
  const en = cfg.lang === 'en';
62
64
  const isObj = payload && typeof payload === 'object';
63
65
  const cmd = isObj ? payload.cmd : String(payload);
64
- return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en }) : askLabelApprove(cmd, { en });
66
+ return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en, warning: payload.warning }) : askLabelApprove(cmd, { en });
65
67
  }
66
68
  // Linha de STATUS por etapa (✓ verde / ✗ vermelho) sob a linha ⚙, com recuo `pad`.
67
69
  function stepDoneLine({ ok, evidence }, pad = ' ') {
@@ -1802,7 +1804,9 @@ async function verificarCmd(args) {
1802
1804
  }
1803
1805
 
1804
1806
  if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
1805
- 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 });
1806
1810
  if (JSON_OUT) { console.log(JSON.stringify(rep)); process.exit(rep.allOk ? 0 : 1); }
1807
1811
 
1808
1812
  for (const r of rep.results) {
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}`
@@ -505,8 +513,8 @@ async function run(task, opts = {}) {
505
513
  _warnedSigs.add(_sig); _loopWarned = true;
506
514
  onStep({ name, detail: argsShort(name, input), loop: true });
507
515
  result = { erro: lang !== 'en'
508
- ? `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.`
509
- : `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.` };
510
518
  }
511
519
 
512
520
  // ENFORCEMENT do modo só-leitura PRIMEIRO: bloqueia ferramentas que alteram/rodam ANTES de
@@ -550,7 +558,9 @@ async function run(task, opts = {}) {
550
558
  }
551
559
  } else if (!yes) {
552
560
  // Passa payload ESTRUTURADO → a UI mostra um RESUMO curto (não o heredoc inteiro).
553
- const dec = await askApprove({ kind: 'destructive', cmd: String(input.comando || '') });
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) });
554
564
  const d = decideApproval({ autoAll: false, decision: dec });
555
565
  approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
556
566
  } else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
package/lib/core.js CHANGED
@@ -20,9 +20,29 @@
20
20
  const TS_CONFIG_RE = /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i;
21
21
  function touchesTsConfig(cmd) { return TS_CONFIG_RE.test(String(cmd || '')); }
22
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
+
23
42
  // Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
24
43
  // (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
25
44
  const DESTRUCTIVE = [
45
+ ...GLOBAL_STATE.map(g => g[0]), // mudanças de estado global também passam pela aprovação
26
46
  /\brm\s+(-[a-z]*[rf][a-z]*\s+)/i, /\brm\s+.*\*/, /\brmdir\b/i, /\bdel\s+\/[sq]/i, /\brd\s+\/s/i,
27
47
  /\bmkfs\b/i, /\bdd\s+if=/i, /(?:^|[\s&;|])format\s+[a-z]:/i, /\bdiskpart\b/i, /> ?\/dev\/sd/i,
28
48
  /\bshutdown\b/i, /\breboot\b/i, /\bhalt\b/i, /\bpoweroff\b/i,
@@ -146,6 +166,7 @@ function classifyToolResult(raw) {
146
166
 
147
167
  module.exports = {
148
168
  DESTRUCTIVE, isDestructive, touchesTsConfig,
169
+ GLOBAL_STATE, globalStateWarning,
149
170
  MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
150
171
  AgentEvents, EVENT_TYPES,
151
172
  ERROR_CLASSES, RETRYABLE_CLASSES, classifyError, classifyToolResult,
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/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.76.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"