terminal-smart-cli 0.75.0 → 0.76.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 +63 -14
- package/lib/agent.js +60 -14
- package/lib/core.js +10 -6
- package/lib/i18n.js +2 -0
- package/lib/meta.js +2 -2
- package/lib/ui.js +13 -1
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -19,12 +19,54 @@ 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 } = {}) {
|
|
41
|
+
const raw = String(cmd || '');
|
|
42
|
+
const { shown, hidden } = ui.cmdSummary(raw);
|
|
43
|
+
const more = hidden > 0 ? C.dim(` … +${hidden} ${en ? 'lines' : 'linhas'}`) : '';
|
|
44
|
+
for (;;) {
|
|
45
|
+
const q = C.warn(' ▲ ' + (en ? 'destructive: ' : 'destrutivo: ')) + C.bold(shown) + more
|
|
46
|
+
+ C.dim(en ? ' [y/N · a=all · v=view] ' : ' [s/N · a=tudo · v=ver] ');
|
|
47
|
+
const a = String(await ui.ask(q)).trim().toLowerCase();
|
|
48
|
+
if (['v', 'ver', 'view'].includes(a)) { console.log('\n' + raw.split('\n').map(l => ' ' + C.dim(l)).join('\n') + '\n'); continue; }
|
|
49
|
+
if (['a', 'all', 'tudo'].includes(a)) return 'all';
|
|
50
|
+
return ['s', 'sim', 'y', 'yes'].includes(a);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// askLabelApprove: aprovação de um LABEL já formatado (skill/MCP, não é comando cru). s/N + a=tudo.
|
|
54
|
+
async function askLabelApprove(label, { en = false } = {}) {
|
|
55
|
+
const a = String(await ui.ask(C.warn('▲ ') + T.agent_approve(C.bold(label)) + C.dim(en ? '(a=all) ' : '(a=tudo) '))).trim().toLowerCase();
|
|
56
|
+
if (['a', 'all', 'tudo'].includes(a)) return 'all';
|
|
57
|
+
return ['s', 'sim', 'y', 'yes'].includes(a);
|
|
58
|
+
}
|
|
59
|
+
// Roteia o payload do askApprove do agente (obj destrutivo → resumo; string label → skill/MCP).
|
|
60
|
+
async function askApproveRoute(payload) {
|
|
61
|
+
const en = cfg.lang === 'en';
|
|
62
|
+
const isObj = payload && typeof payload === 'object';
|
|
63
|
+
const cmd = isObj ? payload.cmd : String(payload);
|
|
64
|
+
return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en }) : askLabelApprove(cmd, { en });
|
|
65
|
+
}
|
|
66
|
+
// Linha de STATUS por etapa (✓ verde / ✗ vermelho) sob a linha ⚙, com recuo `pad`.
|
|
67
|
+
function stepDoneLine({ ok, evidence }, pad = ' ') {
|
|
68
|
+
return pad + (ok ? C.ok('✓') : C.err('✗')) + (evidence ? ' ' + C.dim(String(evidence).slice(0, 80)) : C.dim(ok ? ' ok' : ' erro'));
|
|
69
|
+
}
|
|
28
70
|
function needToken() {
|
|
29
71
|
if (cfg.token) return cfg.token;
|
|
30
72
|
console.error(ui.errLine(T.need_login));
|
|
@@ -824,22 +866,28 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
|
|
|
824
866
|
let out;
|
|
825
867
|
try {
|
|
826
868
|
out = await agent.run(task, {
|
|
827
|
-
token, lang: cfg.lang || 'pt', yes: YES, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
|
|
869
|
+
token, lang: cfg.lang || 'pt', yes: YES, autoAll: YOLO, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
|
|
828
870
|
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; }
|
|
871
|
+
onStep: ({ name, detail, blocked, loop, retry, auto }) => {
|
|
872
|
+
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
|
|
831
873
|
sp.stop();
|
|
832
|
-
const tag = retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
|
|
874
|
+
const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
|
|
833
875
|
console.log(' ' + tag + ' ' + C.bold(name) + (detail ? C.dim(' · ' + detail) : ''));
|
|
834
876
|
sp.start();
|
|
835
877
|
},
|
|
836
|
-
|
|
878
|
+
onStepDone: (e) => {
|
|
879
|
+
if (streamJson) return; // o evento 'result' já cobre; aqui é só a UI bonita
|
|
880
|
+
sp.stop();
|
|
881
|
+
console.log(stepDoneLine(e, ' '));
|
|
882
|
+
sp.start();
|
|
883
|
+
},
|
|
884
|
+
askApprove: async (payload) => {
|
|
837
885
|
// 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; }
|
|
886
|
+
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
887
|
sp.stop();
|
|
840
|
-
const
|
|
888
|
+
const r = await askApproveRoute(payload);
|
|
841
889
|
sp.start();
|
|
842
|
-
return
|
|
890
|
+
return r;
|
|
843
891
|
},
|
|
844
892
|
onRemote: ({ ttl }) => {
|
|
845
893
|
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
|
|
@@ -1276,7 +1324,7 @@ async function metaCmd() {
|
|
|
1276
1324
|
const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
|
|
1277
1325
|
try {
|
|
1278
1326
|
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,
|
|
1327
|
+
token, lang: cfg.lang || 'pt', yes, autoAll: YOLO, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
|
|
1280
1328
|
onAlert: ({ type, text }) => {
|
|
1281
1329
|
sp.stop();
|
|
1282
1330
|
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 +1338,13 @@ async function metaCmd() {
|
|
|
1290
1338
|
onChecklist: (itens) => { sp.stop(); console.log(_metaChecklistBox(itens) + '\n'); sp.start(); },
|
|
1291
1339
|
onRound: ({ n, item, attempt }) => { sp.stop(); console.log(' ' + C.indigo('◆') + ' ' + C.bold(T.meta_round(n, item.slice(0, 70), attempt))); sp.start(); },
|
|
1292
1340
|
onThinking: () => sp.text(T.agent_thinking),
|
|
1293
|
-
onStep: ({ name, detail, blocked, loop, retry }) => {
|
|
1341
|
+
onStep: ({ name, detail, blocked, loop, retry, auto }) => {
|
|
1294
1342
|
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) : ''));
|
|
1343
|
+
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
1344
|
sp.start();
|
|
1297
1345
|
},
|
|
1298
|
-
|
|
1346
|
+
onStepDone: (e) => { sp.stop(); console.log(stepDoneLine(e, ' ')); sp.start(); },
|
|
1347
|
+
askApprove: async (payload) => { sp.stop(); const r = await askApproveRoute(payload); sp.start(); return r; },
|
|
1299
1348
|
onRemote: ({ ttl }) => { sp.stop(); console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120))); sp.start(); },
|
|
1300
1349
|
onRoundDone: ({ checklist, spent }) => {
|
|
1301
1350
|
sp.stop();
|
|
@@ -1812,8 +1861,8 @@ async function runbookCmd(args) {
|
|
|
1812
1861
|
cwd: process.cwd(),
|
|
1813
1862
|
askApprove: async (cmd) => {
|
|
1814
1863
|
if (YES || !process.stdin.isTTY) return false; // não-interativo não aprova destrutivo
|
|
1815
|
-
const
|
|
1816
|
-
return
|
|
1864
|
+
const d = await askDestructive(String(cmd), { en }); // resumo curto + ver completo (v)
|
|
1865
|
+
return d === true || d === 'all'; // runbook aprova passo-a-passo (sem sessão full-auto)
|
|
1817
1866
|
},
|
|
1818
1867
|
onStep: (e) => {
|
|
1819
1868
|
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
|
@@ -153,6 +153,15 @@ function loopDecision({ nSig, cycling, warnedThis, loopWarned, WARN, BREAK }) {
|
|
|
153
153
|
return 'ok';
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// Decisão de aprovação de passo destrutivo (PURO/testável, fonte única do mapeamento).
|
|
157
|
+
// decision vem do askApprove interativo: true = aprovou este · 'all' = aprovar TUDO na sessão
|
|
158
|
+
// (full-auto) · false = recusou. autoAll = a sessão já está em full-auto (aprova sem perguntar).
|
|
159
|
+
function decideApproval({ autoAll, decision }) {
|
|
160
|
+
if (autoAll) return { approved: true, enableAll: true };
|
|
161
|
+
if (decision === 'all') return { approved: true, enableAll: true };
|
|
162
|
+
return { approved: decision === true, enableAll: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
156
165
|
// SUB-AGENTE de exploração (padrão Claude Code): contexto PRÓPRIO, só-leitura, poucos passos.
|
|
157
166
|
// A leitura pesada acontece AQUI e só o RESUMO volta pro agente principal → economia de contexto.
|
|
158
167
|
async function _subAgent({ task, k, model, cwd, lang, onStep }) {
|
|
@@ -215,10 +224,17 @@ async function _remoteApprove(comando, token, onRemote) {
|
|
|
215
224
|
/**
|
|
216
225
|
* Roda o agente. opts:
|
|
217
226
|
* token (sessão ts) · lang · yes (auto-aprova NÃO-destrutivos; destrutivo recusa)
|
|
218
|
-
*
|
|
227
|
+
* autoAll (FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos — o gate anti-catástrofe
|
|
228
|
+
* de AUTO-destrutivos segue recusando na hora, full-auto não abre mão dele)
|
|
229
|
+
* onStep({name, detail, auto}) — passo iniciado (pra UI; auto=destrutivo aprovado em full-auto)
|
|
230
|
+
* onStepDone({name, ok, status, evidence}) — passo concluído (✓/✗ por etapa)
|
|
231
|
+
* askApprove(cmd|{kind,cmd}) → Promise<boolean|'all'> ('all' liga o full-auto na sessão)
|
|
219
232
|
*/
|
|
220
233
|
async function run(task, opts = {}) {
|
|
221
|
-
const { token, lang = 'pt', yes = false, model = null, confineDir = null, onStep = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
|
|
234
|
+
const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
|
|
235
|
+
// FULL-AUTO (autoridade total): aprova destrutivos sem perguntar. Começa por --yolo/--full-auto
|
|
236
|
+
// (opts.autoAll) e pode ser LIGADO no meio da sessão quando o humano responde "a" (aprovar tudo).
|
|
237
|
+
let autoApproveDestructive = !!autoAll;
|
|
222
238
|
// CWD da SESSÃO: base de todo caminho relativo e do shell. Persiste entre
|
|
223
239
|
// mensagens (o chamador passa opts.cwd e lê o out.cwd de volta). Missão usa a
|
|
224
240
|
// pasta confinada. É MUTÁVEL: a ferramenta mudar_diretorio e o "cd" no início
|
|
@@ -465,6 +481,7 @@ async function run(task, opts = {}) {
|
|
|
465
481
|
const name = (tc.function && tc.function.name) || '';
|
|
466
482
|
let input = {}; try { input = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
|
|
467
483
|
let result;
|
|
484
|
+
let _ran = false; // true só quando uma ferramenta REALMENTE executou (não gate/bloqueio) → status ✓/✗
|
|
468
485
|
|
|
469
486
|
// ── WATCHDOG anti-loop: mede repetição ANTES de qualquer gate/execução ──
|
|
470
487
|
const _sig = loopSig(name, input);
|
|
@@ -517,9 +534,28 @@ async function run(task, opts = {}) {
|
|
|
517
534
|
// Interativo pergunta no terminal; em --yes (cron) tenta APROVAÇÃO REMOTA no Telegram do dono.
|
|
518
535
|
if (result === undefined && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
|
|
519
536
|
let approved = false, remoteTried = false;
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
537
|
+
if (autoApproveDestructive) {
|
|
538
|
+
// FULL-AUTO não abre mão do anti-catástrofe: os auto-destrutivos de máquina/processo/
|
|
539
|
+
// pasta já caíram no gate acima (selfDestructiveReason). O que sobra é ~/.ts por shell
|
|
540
|
+
// (bypass dos gates dedicados skill_gerenciar/hooks) — esse NUNCA é auto-aprovado nem em
|
|
541
|
+
// --yolo: exige o humano. Todo o resto: aprova sem perguntar, mas VISÍVEL (auto:true).
|
|
542
|
+
if (core.touchesTsConfig(String(input.comando || ''))) {
|
|
543
|
+
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
544
|
+
result = { erro: lang !== 'en'
|
|
545
|
+
? '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.'
|
|
546
|
+
: '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.' };
|
|
547
|
+
} else {
|
|
548
|
+
approved = true;
|
|
549
|
+
onStep({ name, detail: argsShort(name, input), auto: true });
|
|
550
|
+
}
|
|
551
|
+
} else if (!yes) {
|
|
552
|
+
// 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 || '') });
|
|
554
|
+
const d = decideApproval({ autoAll: false, decision: dec });
|
|
555
|
+
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
556
|
+
} else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
|
|
557
|
+
// result === undefined evita sobrescrever a mensagem específica do bloqueio ~/.ts do full-auto
|
|
558
|
+
if (!approved && result === undefined) {
|
|
523
559
|
result = { erro: yes
|
|
524
560
|
? (remoteTried
|
|
525
561
|
? '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 +585,8 @@ async function run(task, opts = {}) {
|
|
|
549
585
|
const prev = input.acao === 'criar'
|
|
550
586
|
? `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
587
|
: `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);
|
|
588
|
+
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);
|
|
589
|
+
if (okS === 'all') autoApproveDestructive = true;
|
|
553
590
|
if (!okS) {
|
|
554
591
|
result = { erro: lang !== 'en'
|
|
555
592
|
? 'O usuário RECUSOU a mudança de skill. Não repita nesta sessão; siga com a tarefa normalmente.'
|
|
@@ -566,8 +603,12 @@ async function run(task, opts = {}) {
|
|
|
566
603
|
const _js = JSON.stringify(input);
|
|
567
604
|
const _lbl = 'MCP ' + _mcp.route[name].server + ' → ' + _mcp.route[name].realName + ' ' + (_js.length > 1500 ? _js.slice(0, 1500) + ' …(+' + (_js.length - 1500) + ' chars)' : _js);
|
|
568
605
|
let approved = false, remoteTried = false;
|
|
569
|
-
if (
|
|
570
|
-
else
|
|
606
|
+
if (autoApproveDestructive) { approved = true; onStep({ name, detail: argsShort(name, input), auto: true }); }
|
|
607
|
+
else if (!yes) {
|
|
608
|
+
const dec = await askApprove(_lbl);
|
|
609
|
+
const d = decideApproval({ autoAll: false, decision: dec });
|
|
610
|
+
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
611
|
+
} else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
|
|
571
612
|
if (!approved) {
|
|
572
613
|
result = { erro: yes
|
|
573
614
|
? (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 +630,9 @@ async function run(task, opts = {}) {
|
|
|
589
630
|
// 1ª chamada a ESTE servidor na sessão pede um OK (mesmo tool de leitura): os ARGUMENTOS
|
|
590
631
|
// saem da máquina pro servidor externo, e podem ter sido influenciados por prompt-injection
|
|
591
632
|
// 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)) {
|
|
633
|
+
if (!rt.needsApproval && !yes && !autoApproveDestructive && !_mcpSeen.has(rt.server)) {
|
|
593
634
|
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?');
|
|
635
|
+
if (okFirst === 'all') autoApproveDestructive = true;
|
|
594
636
|
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
637
|
}
|
|
596
638
|
if (result === undefined) {
|
|
@@ -598,7 +640,7 @@ async function run(task, opts = {}) {
|
|
|
598
640
|
onStep({ name: 'mcp:' + rt.realName, detail: rt.server });
|
|
599
641
|
try { result = { resultado: await require('./mcp').callTool(rt.endpoint, rt.auth, rt.realName, input) }; }
|
|
600
642
|
catch (e) { result = { erro: 'MCP falhou: ' + String((e && e.message) || e).slice(0, 200) }; }
|
|
601
|
-
steps++;
|
|
643
|
+
steps++; _ran = true;
|
|
602
644
|
}
|
|
603
645
|
}
|
|
604
646
|
// LIMITE DE PESQUISA (convergência): conta buscar_web + navegador abrir/ler. Ao passar o
|
|
@@ -636,7 +678,7 @@ async function run(task, opts = {}) {
|
|
|
636
678
|
}
|
|
637
679
|
else result = { erro: 'acao inválida — use abrir|clicar|digitar|ler|print.' };
|
|
638
680
|
} catch (e) { result = { erro: String((e && e.message) || e).slice(0, 300) }; }
|
|
639
|
-
steps++;
|
|
681
|
+
steps++; _ran = true;
|
|
640
682
|
}
|
|
641
683
|
// SUB-AGENTE: 'explorar' roda em contexto próprio (só-leitura) e devolve só o resumo.
|
|
642
684
|
if (result === undefined && name === 'explorar') {
|
|
@@ -644,12 +686,12 @@ async function run(task, opts = {}) {
|
|
|
644
686
|
const sub = await _subAgent({ task: input.tarefa || input.pergunta || '', k, model, cwd, lang, onStep });
|
|
645
687
|
acc.inTok += sub._tin; acc.outTok += sub._tout; acc.cachedTok += sub._tcach || 0;
|
|
646
688
|
result = { resumo: sub.resumo };
|
|
647
|
-
steps++;
|
|
689
|
+
steps++; _ran = true;
|
|
648
690
|
}
|
|
649
691
|
if (result === undefined) {
|
|
650
692
|
onStep({ name, detail: argsShort(name, input) });
|
|
651
693
|
result = await tools.execute(name, input, { confineDir, baseDir: cwd, token });
|
|
652
|
-
steps++;
|
|
694
|
+
steps++; _ran = true;
|
|
653
695
|
// TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
|
|
654
696
|
// sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
|
|
655
697
|
// não falha de execução — não conta pro ledger nem pro sinal de erro da run.
|
|
@@ -681,6 +723,10 @@ async function run(task, opts = {}) {
|
|
|
681
723
|
&& _researchCalls >= Math.ceil(RESEARCH_MAX * 2 / 3) && _researchCalls <= RESEARCH_MAX) {
|
|
682
724
|
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
725
|
}
|
|
726
|
+
// STATUS POR ETAPA (UX): ✓ verde / ✗ vermelho por passo, a partir do ToolResult TIPADO
|
|
727
|
+
// (core.classifyToolResult) — o usuário vê na hora o que deu certo/errado (antes as ⚙
|
|
728
|
+
// não indicavam resultado nenhum). Só para passos que REALMENTE rodaram (não gate/bloqueio).
|
|
729
|
+
if (_ran) { const _c = core.classifyToolResult(result); onStepDone({ name, ok: _c.ok, status: _c.status, evidence: _c.evidence }); }
|
|
684
730
|
// BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz →
|
|
685
731
|
// encerra o turno devolvendo o pedido; a missão pausa e chama o usuário.
|
|
686
732
|
if (result && result._needHuman) {
|
|
@@ -731,4 +777,4 @@ async function run(task, opts = {}) {
|
|
|
731
777
|
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr };
|
|
732
778
|
}
|
|
733
779
|
|
|
734
|
-
module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision } };
|
|
780
|
+
module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval } };
|
package/lib/core.js
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
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
|
+
|
|
15
23
|
// Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
|
|
16
24
|
// (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
|
|
17
25
|
const DESTRUCTIVE = [
|
|
@@ -25,11 +33,7 @@ const DESTRUCTIVE = [
|
|
|
25
33
|
/\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
34
|
/\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
35
|
/\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,
|
|
36
|
+
TS_CONFIG_RE, // ~/.ts por shell = bypass dos gates dedicados (ver comentário acima)
|
|
33
37
|
];
|
|
34
38
|
function isDestructive(cmd) { return DESTRUCTIVE.some(re => re.test(String(cmd || ''))); }
|
|
35
39
|
|
|
@@ -141,7 +145,7 @@ function classifyToolResult(raw) {
|
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
module.exports = {
|
|
144
|
-
DESTRUCTIVE, isDestructive,
|
|
148
|
+
DESTRUCTIVE, isDestructive, touchesTsConfig,
|
|
145
149
|
MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
|
|
146
150
|
AgentEvents, EVENT_TYPES,
|
|
147
151
|
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/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 };
|