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 +68 -15
- package/lib/agent.js +72 -16
- package/lib/core.js +31 -6
- package/lib/i18n.js +2 -0
- package/lib/meta.js +2 -2
- package/lib/ssh.js +18 -7
- package/lib/tools.js +10 -2
- package/lib/ui.js +13 -1
- package/lib/verify.js +35 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
|
890
|
+
const r = await askApproveRoute(payload);
|
|
841
891
|
sp.start();
|
|
842
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1816
|
-
return
|
|
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
|
-
*
|
|
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 —
|
|
492
|
-
: `LOOP DETECTED: you already did "${name}(${argsShort(name, input)})" ${_nSig}× and the result is NOT changing. STOP repeating —
|
|
516
|
+
? `LOOP DETECTADO: você já fez "${name}(${argsShort(name, input)})" ${_nSig}× e o resultado NÃO muda. PARE de repetir. Chutar não resolve — vá à 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 (
|
|
521
|
-
|
|
522
|
-
|
|
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 (
|
|
570
|
-
else
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
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', () => {
|
|
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
|
-
|
|
372
|
-
|
|
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 };
|