terminal-smart-cli 0.97.7 → 0.97.9
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 +93 -8
- package/lib/agent.js +144 -13
- package/lib/doctor.js +12 -4
- package/lib/i18n.js +6 -0
- package/lib/intelligence-core.js +95 -6
- package/lib/keyring.js +11 -0
- package/lib/mission-tool-cache.js +55 -0
- package/lib/office-editors.js +171 -0
- package/lib/providers.js +24 -7
- package/lib/tools.js +158 -0
- package/lib/ui.js +13 -3
- package/lib/xlsx-compat-editor.js +228 -0
- package/package.json +4 -3
package/bin/ts.js
CHANGED
|
@@ -119,7 +119,37 @@ function help() {
|
|
|
119
119
|
console.log(out.join('\n'));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function privacyCmd() {
|
|
123
|
+
const privacyUrl = 'https://terminalsmart.com.br/privacidade';
|
|
124
|
+
console.log((cfg.lang === 'en' ? 'Privacy Policy: ' : 'Política de Privacidade: ') + privacyUrl);
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
// ── Login por código (device-code) ───────────────────────────────────────────
|
|
128
|
+
async function accountCmd(args) {
|
|
129
|
+
const action = String((args && args[0]) || '').toLowerCase();
|
|
130
|
+
const accountUrl = base() + '/minha-conta';
|
|
131
|
+
if (!action || ['abrir', 'open', 'excluir', 'delete'].includes(action)) {
|
|
132
|
+
console.log((cfg.lang === 'en' ? 'Account, data export and deletion: ' : 'Conta, exportação de dados e exclusão: ') + accountUrl);
|
|
133
|
+
if (process.env.TS_NO_BROWSER !== '1') try {
|
|
134
|
+
const { exec } = require('child_process');
|
|
135
|
+
const cmd = process.platform === 'win32' ? `start "" "${accountUrl}"` : process.platform === 'darwin' ? `open "${accountUrl}"` : `xdg-open "${accountUrl}"`;
|
|
136
|
+
exec(cmd, () => {});
|
|
137
|
+
} catch (_) {}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (['exportar', 'export', 'baixar', 'download'].includes(action)) {
|
|
141
|
+
const token = needToken();
|
|
142
|
+
const data = await api('/api/account/export', { token, retry: false });
|
|
143
|
+
const fs = require('fs'), path = require('path');
|
|
144
|
+
const safeUser = String(cfg.username || 'usuario').replace(/[^a-z0-9_-]/gi, '-');
|
|
145
|
+
const file = path.resolve(`terminal-smart-dados-${safeUser}-${new Date().toISOString().slice(0, 10)}.json`);
|
|
146
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
147
|
+
console.log(ui.okLine((cfg.lang === 'en' ? 'Data saved to: ' : 'Dados salvos em: ') + file));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.log(ui.infoLine(cfg.lang === 'en' ? 'Use: ts account export | ts account open' : 'Use: ts conta exportar | ts conta abrir'));
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
async function login() {
|
|
124
154
|
if (cfg.token) {
|
|
125
155
|
try {
|
|
@@ -1134,16 +1164,19 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1134
1164
|
const token = needToken();
|
|
1135
1165
|
// flags que levam VALOR: o filtro remove só a flag (começa com "-"), deixando o VALOR nos
|
|
1136
1166
|
// words → tira o valor daqui pra ele não virar parte do texto da tarefa.
|
|
1137
|
-
for (const flag of ['--modelo', '--model', '--output-format']) {
|
|
1167
|
+
for (const flag of ['--modelo', '--model', '--output-format', '--orcamento-creditos', '--max-creditos', '--tempo-max-seg', '--max-tokens']) {
|
|
1138
1168
|
const _i = process.argv.findIndex(a => a === flag); const _v = _i >= 0 ? process.argv[_i + 1] : null;
|
|
1139
1169
|
if (_v) { const j = words.indexOf(_v); if (j >= 0) words = words.slice(0, j).concat(words.slice(j + 1)); }
|
|
1140
1170
|
}
|
|
1141
1171
|
let task = words.join(' ').trim();
|
|
1142
1172
|
// Flags INLINE no texto (pro REPL, onde não há argv): --passos N / --steps N / --yolo.
|
|
1143
1173
|
// Assim "faça tudo ... --passos 60 --yolo" digitado no chat vale igual à linha de comando.
|
|
1144
|
-
let _inlinePassos, _inlineYolo = false;
|
|
1174
|
+
let _inlinePassos, _inlineYolo = false, _inlineMaxCredits, _inlineMaxSeconds, _inlineMaxTokens;
|
|
1145
1175
|
task = task
|
|
1146
1176
|
.replace(/(?:^|\s)--(?:passos|steps|max-passos)[\s=]+(\d{1,3})\b/gi, (_m, n) => { _inlinePassos = Number(n); return ' '; })
|
|
1177
|
+
.replace(/(?:^|\s)--(?:orcamento-creditos|max-creditos)[\s=]+(\d{1,5})\b/gi, (_m, n) => { _inlineMaxCredits = Number(n); return ' '; })
|
|
1178
|
+
.replace(/(?:^|\s)--tempo-max-seg[\s=]+(\d{1,5})\b/gi, (_m, n) => { _inlineMaxSeconds = Number(n); return ' '; })
|
|
1179
|
+
.replace(/(?:^|\s)--max-tokens[\s=]+(\d{3,7})\b/gi, (_m, n) => { _inlineMaxTokens = Number(n); return ' '; })
|
|
1147
1180
|
.replace(/(?:^|\s)--(?:yolo|full-auto|auto-tudo)\b/gi, () => { _inlineYolo = true; return ' '; })
|
|
1148
1181
|
.replace(/\s{2,}/g, ' ').trim();
|
|
1149
1182
|
const piped = await readStdin();
|
|
@@ -1221,8 +1254,15 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1221
1254
|
// Pra missões grandes ("faça tudo de uma vez") sem o pára-e-continua.
|
|
1222
1255
|
const _pi = process.argv.findIndex(a => a === '--passos' || a === '--steps' || a === '--max-passos');
|
|
1223
1256
|
const _maxPassos = _pi >= 0 ? Number(process.argv[_pi + 1]) : (_inlinePassos ?? maxIterIn ?? undefined);
|
|
1257
|
+
const _ci = process.argv.findIndex(a => a === '--orcamento-creditos' || a === '--max-creditos');
|
|
1258
|
+
const _ti = process.argv.findIndex(a => a === '--tempo-max-seg');
|
|
1259
|
+
const _tki = process.argv.findIndex(a => a === '--max-tokens');
|
|
1260
|
+
const _maxCredits = _ci >= 0 ? Number(process.argv[_ci + 1]) : _inlineMaxCredits;
|
|
1261
|
+
const _maxDurationMs = 1000 * (_ti >= 0 ? Number(process.argv[_ti + 1]) : (_inlineMaxSeconds || 0));
|
|
1262
|
+
const _maxTokens = _tki >= 0 ? Number(process.argv[_tki + 1]) : _inlineMaxTokens;
|
|
1224
1263
|
out = await agent.run(task, {
|
|
1225
1264
|
token, lang: cfg.lang || 'pt', yes: YES, autoAll: (YOLO || _inlineYolo || autoAllIn), model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser, maxIter: _maxPassos,
|
|
1265
|
+
maxCredits: _maxCredits, maxDurationMs: _maxDurationMs || undefined, maxTokens: _maxTokens,
|
|
1226
1266
|
onThinking: (p) => {
|
|
1227
1267
|
if (p && typeof p === 'object') {
|
|
1228
1268
|
_live.steps = p.steps ?? _live.steps; _live.inTok = p.inTok ?? _live.inTok; _live.outTok = p.outTok ?? _live.outTok;
|
|
@@ -1288,10 +1328,10 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1288
1328
|
// HEADLESS stream-json: fecha com assistant (texto) + result (métricas) e sai.
|
|
1289
1329
|
if (streamJson) {
|
|
1290
1330
|
_emit(core.AgentEvents.assistant(out.text || ''));
|
|
1291
|
-
_emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, toolErrors: out.toolErrors || [], cwd: effCwd, duration_ms: Date.now() - t0, worktree: _wt ? { path: _wt.path, branch: _wt.branch, base: _wt.base, changed: (_wtInfo && _wtInfo.changed) || 0 } : null }));
|
|
1331
|
+
_emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, toolErrors: out.toolErrors || [], guard: out.guard || null, cwd: effCwd, duration_ms: Date.now() - t0, worktree: _wt ? { path: _wt.path, branch: _wt.branch, base: _wt.base, changed: (_wtInfo && _wtInfo.changed) || 0 } : null }));
|
|
1292
1332
|
return;
|
|
1293
1333
|
}
|
|
1294
|
-
if (JSON_OUT) { console.log(JSON.stringify({ ok: true, result: out.text, steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context, needHuman: out.needHuman })); return; }
|
|
1334
|
+
if (JSON_OUT) { console.log(JSON.stringify({ ok: true, result: out.text, steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context, needHuman: out.needHuman, guard: out.guard || null })); return; }
|
|
1295
1335
|
// BLOQUEIO HUMANO: o agente pediu uma ação que só o usuário faz — mostra o pedido
|
|
1296
1336
|
// (antes o retorno vinha com texto vazio e o usuário via só um cabeçalho em branco).
|
|
1297
1337
|
if (out.needHuman) {
|
|
@@ -2594,6 +2634,9 @@ async function doctorCmd() {
|
|
|
2594
2634
|
cfg, version: pkg.version, base: base(),
|
|
2595
2635
|
// backend no ar? 401 conta como "respondeu" (só não autenticado).
|
|
2596
2636
|
pingBackend: async () => { try { await api('/api/auth/check', { token: cfg.token, timeoutMs: 8000 }); return true; } catch (e) { return (e && e.status === 401); } },
|
|
2637
|
+
// Plano autoritativo. O de ~/.ts é do último login e envelhece a cada assinatura,
|
|
2638
|
+
// troca ou cancelamento — o diagnóstico mostrava um plano que já não existe mais.
|
|
2639
|
+
planoAtual: async () => { try { const r = await api('/api/credits', { token: cfg.token, timeoutMs: 8000 }); return (r && r.plan) || null; } catch (_) { return null; } },
|
|
2597
2640
|
// última versão publicada no npm (best-effort).
|
|
2598
2641
|
latestVersion: async () => { try { const r = await fetch('https://registry.npmjs.org/terminal-smart-cli/latest', { signal: AbortSignal.timeout(8000) }); const j = await r.json(); return j && j.version; } catch (_) { return null; } },
|
|
2599
2642
|
});
|
|
@@ -2662,9 +2705,10 @@ async function siteCmd(args) {
|
|
|
2662
2705
|
if (JSON_OUT) return console.log(JSON.stringify(r));
|
|
2663
2706
|
if (!r.sites || !r.sites.length) return console.log(ui.infoLine('Nenhum site criado. Use: ts site criar <nome> "<descrição>"'));
|
|
2664
2707
|
console.log('\n' + ui.box(r.sites.map(s =>
|
|
2665
|
-
C.bold(s.slug) + C.dim(' · ') + (s.status === 'active' ? C.ok('online') : C.dim(s.status)) +
|
|
2666
|
-
C.dim(s.preview ? '
|
|
2667
|
-
|
|
2708
|
+
C.bold(s.name || s.slug) + C.dim(' · ') + (s.status === 'active' ? C.ok('online') : C.dim(s.status)) +
|
|
2709
|
+
C.dim(` · ${s.kind || (s.preview ? 'preview' : 'permanente')}`) + '\n ' + C.cyan(s.url) +
|
|
2710
|
+
(s.expiresAt ? C.dim('\n expira: ' + s.expiresAt) : '')
|
|
2711
|
+
), { title: `ts site · ${r.plan} · ${(r.limits && r.limits.used) ?? r.sites.length}/${(r.limits && r.limits.maximum) || r.siteLimit || 'demo'}` }) + '\n');
|
|
2668
2712
|
return;
|
|
2669
2713
|
}
|
|
2670
2714
|
if (['criar', 'create', 'gerar', 'generate', 'editar', 'edit', 'alterar', 'update'].includes(sub)) {
|
|
@@ -2871,7 +2915,8 @@ async function uso() {
|
|
|
2871
2915
|
'',
|
|
2872
2916
|
unlimited
|
|
2873
2917
|
? C.ok(T.uso_unlimited) + C.dim(` · ${r.used || 0} ${T.uso_credits}`)
|
|
2874
|
-
|
|
2918
|
+
// A barra mostra o que RESTA, igual ao número ao lado dela.
|
|
2919
|
+
: ui.bar(Math.max(0, r.remaining ?? 0) / Math.max(1, r.granted || 1), 22, { restante: true }) + ` ${C.bold(r.remaining ?? '?')} ${C.dim(`/ ${r.granted} ${T.uso_remaining}`)}`,
|
|
2875
2920
|
];
|
|
2876
2921
|
const ledger = (r.ledger || []).slice(0, 5);
|
|
2877
2922
|
if (ledger.length) {
|
|
@@ -2893,6 +2938,43 @@ async function quem() {
|
|
|
2893
2938
|
], { title: T.quem_title }) + '\n');
|
|
2894
2939
|
}
|
|
2895
2940
|
|
|
2941
|
+
async function integracoesCmd(args) {
|
|
2942
|
+
const token = needToken(); const en = cfg.lang === 'en';
|
|
2943
|
+
const sub = String(args[0] || 'status').toLowerCase();
|
|
2944
|
+
const rawProvider = String(args[1] || '').toLowerCase();
|
|
2945
|
+
const provider = ['google', 'gmail', 'drive'].includes(rawProvider) ? 'google'
|
|
2946
|
+
: ['microsoft', 'outlook', 'hotmail', 'office'].includes(rawProvider) ? 'microsoft' : '';
|
|
2947
|
+
const names = { google: 'Google Workspace', microsoft: 'Microsoft/Outlook' };
|
|
2948
|
+
const getStatus = async p => {
|
|
2949
|
+
try { return await api(`/api/integrations/${p}/status`, { token }); }
|
|
2950
|
+
catch (e) { return { connected: false, error: e.message }; }
|
|
2951
|
+
};
|
|
2952
|
+
if (['status', 'listar', 'list', 'ls'].includes(sub)) {
|
|
2953
|
+
const [google, microsoft] = await Promise.all([getStatus('google'), getStatus('microsoft')]);
|
|
2954
|
+
const data = { google, microsoft };
|
|
2955
|
+
if (JSON_OUT) { console.log(JSON.stringify(data)); return; }
|
|
2956
|
+
const line = (label, item) => C.bold(label.padEnd(22)) + (item.connected ? C.ok(en ? 'connected' : 'conectado') + C.dim(item.email ? ` · ${item.email}` : '') : C.dim(en ? 'not connected' : 'não conectado'));
|
|
2957
|
+
console.log('\n' + ui.box([line('Google Workspace', google), line('Microsoft/Outlook', microsoft), '', C.dim(en ? 'Connect: ts integrations connect google|microsoft' : 'Conectar: ts integracoes conectar google|microsoft'), C.dim(en ? 'Manage on web: terminalsmart.com.br/integracoes' : 'Gerenciar na Web: terminalsmart.com.br/integracoes')], { title: en ? 'Integrations' : 'Integrações' }) + '\n');
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
if (!provider) { console.error(ui.infoLine(en ? 'Choose google or microsoft.' : 'Escolha google ou microsoft.')); process.exit(2); }
|
|
2961
|
+
if (['conectar', 'connect', 'reconectar', 'reconnect'].includes(sub)) {
|
|
2962
|
+
const data = await api(`/api/integrations/${provider}/connect`, { method: 'POST', token, body: { source: 'cli' } });
|
|
2963
|
+
if (JSON_OUT) { console.log(JSON.stringify({ provider, authUrl: data.authUrl })); return; }
|
|
2964
|
+
console.log('\n' + ui.box([C.bold(en ? `Authorize ${names[provider]}` : `Autorize ${names[provider]}`), '', data.authUrl, '', C.dim(en ? 'After authorization, run: ts integrations' : 'Depois de autorizar, rode: ts integracoes')], { title: 'OAuth' }) + '\n');
|
|
2965
|
+
try { require('child_process').exec(process.platform === 'win32' ? `start "" "${data.authUrl}"` : process.platform === 'darwin' ? `open "${data.authUrl}"` : `xdg-open "${data.authUrl}"`); } catch (_) {}
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
if (['desconectar', 'disconnect', 'remover', 'remove'].includes(sub)) {
|
|
2969
|
+
let confirmed = FLAGS.has('--confirmar') || FLAGS.has('--confirm');
|
|
2970
|
+
if (!confirmed && process.stdin.isTTY) confirmed = ['s','sim','y','yes'].includes(String(await ui.ask(C.warn(`Desconectar ${names[provider]}? (s/N) `))).trim().toLowerCase());
|
|
2971
|
+
if (!confirmed) { console.log(ui.infoLine(en ? 'Cancelled. Use --confirm to automate this specific action.' : 'Cancelado. Use --confirmar para automatizar esta ação específica.')); return; }
|
|
2972
|
+
await api(`/api/integrations/${provider}/disconnect`, { method: 'POST', token, body: { confirmed: true } });
|
|
2973
|
+
console.log(ui.okLine(`${names[provider]} ${en ? 'disconnected' : 'desconectado'}`)); return;
|
|
2974
|
+
}
|
|
2975
|
+
console.log(ui.infoLine(en ? 'Usage: ts integrations [status|connect|disconnect] [google|microsoft]' : 'Uso: ts integracoes [status|conectar|desconectar] [google|microsoft]'));
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2896
2978
|
function idioma(l) {
|
|
2897
2979
|
const lang = String(l || '').toLowerCase();
|
|
2898
2980
|
if (!['pt', 'en'].includes(lang)) { console.error(ui.infoLine(T.lang_invalid)); process.exit(2); }
|
|
@@ -2993,6 +3075,9 @@ function recallCmd(args) {
|
|
|
2993
3075
|
case 'runbook': case 'runbooks': case 'procedimento': return runbookCmd(POS.slice(1));
|
|
2994
3076
|
case 'trilha': case 'trail': case 'auditoria': return trilhaCmd();
|
|
2995
3077
|
case 'doctor': case 'diagnostico-ambiente': case 'checkup': return doctorCmd();
|
|
3078
|
+
case 'privacidade': case 'privacy': return privacyCmd();
|
|
3079
|
+
case 'conta': case 'account': return accountCmd(POS.slice(1));
|
|
3080
|
+
case 'integracoes': case 'integrações': case 'integrations': return integracoesCmd(POS.slice(1));
|
|
2996
3081
|
case 'meta': case 'missao': case 'mission': return metaCmd();
|
|
2997
3082
|
case 'runs': return runsCmd();
|
|
2998
3083
|
case 'status': return statusCmd(POS[1]);
|
package/lib/agent.js
CHANGED
|
@@ -8,10 +8,12 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { api, ApiError, withRetry } = require('./api');
|
|
10
10
|
const keyring = require('./keyring');
|
|
11
|
+
const providers = require('./providers');
|
|
11
12
|
const policy = require('./policy');
|
|
12
13
|
const checkpoint = require('./checkpoint');
|
|
13
14
|
const skillIndex = require('./skill-index');
|
|
14
15
|
const tools = require('./tools');
|
|
16
|
+
const { MissionToolCache, cacheReference } = require('./mission-tool-cache');
|
|
15
17
|
|
|
16
18
|
// SKILLS INSTALADAS (~/.ts/skills/<slug>/SKILL.md): lê nome+descrição do frontmatter pra
|
|
17
19
|
// oferecer ao agente. O agente LÊ o SKILL.md completo (com ler_arquivo) quando a skill é útil.
|
|
@@ -36,6 +38,44 @@ function installedSkills() {
|
|
|
36
38
|
|
|
37
39
|
const MAX_ITER = 15;
|
|
38
40
|
const TOOL_RESULT_CAP = 6000; // chars por resultado no contexto (compactor já reduz antes)
|
|
41
|
+
const DEFAULT_MISSION_CREDIT_CAP = 40;
|
|
42
|
+
const DEFAULT_MISSION_TOKEN_CAP = 180000;
|
|
43
|
+
const DEFAULT_MISSION_TIME_MS = 120000;
|
|
44
|
+
const DEFAULT_EQUIVALENT_FAILURE_CAP = 2;
|
|
45
|
+
|
|
46
|
+
function failureFingerprint(tool, classified, result) {
|
|
47
|
+
const evidence = String((classified && classified.evidence) || (result && (result.erro || result.stderr)) || '')
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[a-f0-9]{16,}/g, '<id>')
|
|
50
|
+
.replace(/\d+/g, '#')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim()
|
|
53
|
+
.slice(0, 180);
|
|
54
|
+
return `${String(tool || 'tool')}|${String((classified && classified.errorClass) || 'unknown')}|${evidence}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validarModeloByok(k) {
|
|
58
|
+
if (!k || k.source !== 'byok') return null;
|
|
59
|
+
const estado = providers.estadoModelo(k.provedor, k.modelo);
|
|
60
|
+
if (estado.disponivel) return null;
|
|
61
|
+
const dica = estado.substituto ? ' Troque para "' + estado.substituto + '"' : ' Escolha outro modelo';
|
|
62
|
+
return new ApiError('O modelo BYOK "' + k.modelo + '" foi aposentado: ' + estado.motivo + '.' + dica +
|
|
63
|
+
' com `ts conectar ' + k.provedor + '`. O modo Automático pode usar fallback com `ts conectar nuvem`.',
|
|
64
|
+
{ status: 410, code: 'model_retired' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function missionGuardDecision({ credits = 0, maxCredits = DEFAULT_MISSION_CREDIT_CAP,
|
|
68
|
+
tokens = 0, maxTokens = DEFAULT_MISSION_TOKEN_CAP, elapsedMs = 0,
|
|
69
|
+
maxMs = DEFAULT_MISSION_TIME_MS, equivalentFailures = 0,
|
|
70
|
+
maxEquivalentFailures = DEFAULT_EQUIVALENT_FAILURE_CAP } = {}) {
|
|
71
|
+
if (maxCredits > 0 && credits >= maxCredits) return { kind: 'credits', limit: maxCredits, spent: credits };
|
|
72
|
+
if (maxTokens > 0 && tokens >= maxTokens) return { kind: 'tokens', limit: maxTokens, spent: tokens };
|
|
73
|
+
if (maxMs > 0 && elapsedMs >= maxMs) return { kind: 'time', limit: maxMs, spent: elapsedMs };
|
|
74
|
+
if (maxEquivalentFailures > 0 && equivalentFailures >= maxEquivalentFailures) {
|
|
75
|
+
return { kind: 'equivalent_failures', limit: maxEquivalentFailures, spent: equivalentFailures };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
39
79
|
|
|
40
80
|
// ── NÚCLEO CANÔNICO ──────────────────────────────────────────────────────────
|
|
41
81
|
// Executor padrão, janelas de contexto e auto-compactação vêm de lib/core.js (fonte
|
|
@@ -122,8 +162,9 @@ RULES:
|
|
|
122
162
|
// fechamento garantido pra o modelo não tentar chamar ferramenta de novo).
|
|
123
163
|
// Ferramentas SÓ-LEITURA: usadas no modo Ask (--ler), no Plan (--plano) e no sub-agente
|
|
124
164
|
// de exploração (nunca escrevem/rodam comando destrutivo → seguras por construção).
|
|
125
|
-
const READONLY = new Set(['ler_arquivo', 'ler_documento', 'ler_apresentacao', 'listar_diretorio', 'buscar_arquivos', 'buscar_codigo', 'mapa_projeto', 'info_sistema', 'buscar_web', 'buscar_skill', 'android_dispositivos', 'android_logs']);
|
|
165
|
+
const READONLY = new Set(['ler_arquivo', 'ler_documento', 'ler_apresentacao', 'listar_diretorio', 'buscar_arquivos', 'buscar_codigo', 'mapa_projeto', 'info_sistema', 'buscar_web', 'buscar_skill', 'android_dispositivos', 'android_logs', 'status_microsoft365', 'listar_emails_outlook', 'ler_email_outlook', 'listar_pastas_outlook', 'obter_anexo_outlook', 'status_google_workspace', 'listar_emails_gmail', 'ler_email_gmail', 'listar_pastas_gmail', 'obter_anexo_gmail', 'listar_arquivos_drive', 'ler_google_docs', 'ler_google_sheets']);
|
|
126
166
|
const DEVICE_MUTATING = new Set(['android_parear', 'android_conectar', 'android_instalar', 'android_iniciar', 'android_capturar_tela']);
|
|
167
|
+
const CLOUD_MUTATING = new Set(['conectar_microsoft365', 'alterar_email_outlook', 'criar_resposta_outlook', 'criar_encaminhamento_outlook', 'criar_rascunho_outlook', 'enviar_rascunho_outlook', 'alterar_email_gmail', 'criar_resposta_gmail', 'criar_encaminhamento_gmail', 'criar_rascunho_gmail', 'enviar_rascunho_gmail', 'editar_google_docs', 'editar_google_sheets']);
|
|
127
168
|
async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null }) {
|
|
128
169
|
// RESILIÊNCIA: o gateway CDC pode reiniciar/oscilar no meio de uma missão longa.
|
|
129
170
|
// withRetry cobre conn/timeout/5xx (backoff+jitter); NUNCA re-tenta no_credits/auth.
|
|
@@ -268,6 +309,11 @@ async function _remoteApprove(comando, token, onRemote) {
|
|
|
268
309
|
async function run(task, opts = {}) {
|
|
269
310
|
const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
|
|
270
311
|
const maxIter = Math.max(1, Math.min(Number(opts.maxIter) || MAX_ITER, 120)); // --passos N (teto 120)
|
|
312
|
+
const maxCredits = Math.max(1, Math.min(Number(opts.maxCredits) || DEFAULT_MISSION_CREDIT_CAP, 5000));
|
|
313
|
+
const maxTokens = Math.max(1000, Math.min(Number(opts.maxTokens) || DEFAULT_MISSION_TOKEN_CAP, 2000000));
|
|
314
|
+
const maxDurationMs = Math.max(10000, Math.min(Number(opts.maxDurationMs) || DEFAULT_MISSION_TIME_MS, 3600000));
|
|
315
|
+
const maxEquivalentFailures = Math.max(1, Math.min(Number(opts.maxEquivalentFailures) || DEFAULT_EQUIVALENT_FAILURE_CAP, 10));
|
|
316
|
+
const missionStartedAt = Date.now();
|
|
271
317
|
// FULL-AUTO (autoridade total): aprova destrutivos sem perguntar. Começa por --yolo/--full-auto
|
|
272
318
|
// (opts.autoAll) e pode ser LIGADO no meio da sessão quando o humano responde "a" (aprovar tudo).
|
|
273
319
|
let autoApproveDestructive = !!autoAll;
|
|
@@ -296,6 +342,11 @@ async function run(task, opts = {}) {
|
|
|
296
342
|
// o gate por plano no backend (402 plan_limit → mensagem de upgrade).
|
|
297
343
|
const k = await keyring.resolve(token, { feature: 'cli_agent' });
|
|
298
344
|
if (!k || !k.key) throw new ApiError('ai_key', {});
|
|
345
|
+
// BYOK é uma escolha explícita do usuário: nunca substituímos silenciosamente
|
|
346
|
+
// o modelo. Se ele foi aposentado, paramos ANTES da primeira chamada e indicamos
|
|
347
|
+
// como trocar. No modo TS Cloud/Automático, o gateway continua livre para fallback.
|
|
348
|
+
const erroModeloByok = validarModeloByok(k);
|
|
349
|
+
if (erroModeloByok) throw erroModeloByok;
|
|
299
350
|
let selectedModel = model;
|
|
300
351
|
// BYOK: o catálogo do roteador é do GATEWAY (ids tipo "deepseek-v4-flash"); o provedor
|
|
301
352
|
// do usuário tem os seus ("deepseek-ai/deepseek-v4-flash") e devolve 404 pro id errado.
|
|
@@ -444,6 +495,7 @@ async function run(task, opts = {}) {
|
|
|
444
495
|
// ação (mesmo comando falhando, ciclo A/B/A/B). WARN = empurra a mudar de abordagem; 2º strike
|
|
445
496
|
// (ou já avisado e ainda em ciclo) = encerra o loop e cai no fechamento honesto garantido.
|
|
446
497
|
const _callCounts = new Map(); const _recentSigs = []; const _warnedSigs = new Set();
|
|
498
|
+
const _missionCache = new MissionToolCache();
|
|
447
499
|
let _loopWarned = false, loopedOut = false;
|
|
448
500
|
const LOOP_WARN = Number(process.env.TS_LOOP_WARN) > 0 ? Number(process.env.TS_LOOP_WARN) : 3;
|
|
449
501
|
const LOOP_BREAK = Number(process.env.TS_LOOP_BREAK) > 0 ? Number(process.env.TS_LOOP_BREAK) : 5;
|
|
@@ -491,7 +543,15 @@ async function run(task, opts = {}) {
|
|
|
491
543
|
const _mcpSeen = new Set(); // servidores MCP já autorizados NESTA sessão (1ª chamada pede OK)
|
|
492
544
|
const actions = []; // ações REAIS bem-sucedidas (evidência objetiva pro marcador do meta)
|
|
493
545
|
const _toolErrs = []; // erros de ferramenta TIPADOS na run (core.classifyToolResult) — sinal duro anti-done-falso
|
|
546
|
+
const _failureCounts = new Map();
|
|
494
547
|
let finalText = '', usedModel = 'smart', steps = 0, charged = 0, _visionCredits = 0;
|
|
548
|
+
let guardStopped = null;
|
|
549
|
+
const _guard = (equivalentFailures = 0) => missionGuardDecision({
|
|
550
|
+
credits: charged + _visionCredits, maxCredits,
|
|
551
|
+
tokens: acc.inTok + acc.outTok, maxTokens,
|
|
552
|
+
elapsedMs: Date.now() - missionStartedAt, maxMs: maxDurationMs,
|
|
553
|
+
equivalentFailures, maxEquivalentFailures,
|
|
554
|
+
});
|
|
495
555
|
const ctxWindow = winFor(selectedModel);
|
|
496
556
|
// MEMÓRIA EPISÓDICA: ao fim da run, grava UM episódio (o que fez aqui) → a próxima run
|
|
497
557
|
// deste projeto LEMBRA e dá continuidade (resolve o "esquecimento entre execuções").
|
|
@@ -569,6 +629,8 @@ async function run(task, opts = {}) {
|
|
|
569
629
|
let stopped = false;
|
|
570
630
|
for (let iter = 0; iter < maxIter; iter++) {
|
|
571
631
|
if (opts.shouldStop && opts.shouldStop()) { stopped = true; break; }
|
|
632
|
+
guardStopped = _guard();
|
|
633
|
+
if (guardStopped) break;
|
|
572
634
|
// passa o snapshot de progresso pra a status line ao vivo (tempo é contado no cliente)
|
|
573
635
|
onThinking({ iter, inTok: acc.inTok, outTok: acc.outTok, cachedTok: acc.cachedTok, steps, model: usedModel });
|
|
574
636
|
await _compactIfNeeded(false); // proativo: compacta ao cruzar ~65% da janela
|
|
@@ -600,12 +662,29 @@ async function run(task, opts = {}) {
|
|
|
600
662
|
}
|
|
601
663
|
messages.push({ role: 'assistant', content: r.msg.content || '', tool_calls: tcs });
|
|
602
664
|
|
|
665
|
+
// A chamada que acabou de responder já consumiu créditos/tokens. Se ela atingiu o
|
|
666
|
+
// teto, não executamos as ferramentas propostas nem fazemos outra chamada cara.
|
|
667
|
+
guardStopped = _guard();
|
|
668
|
+
if (guardStopped) {
|
|
669
|
+
for (const tc of tcs) messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({
|
|
670
|
+
erro: 'MISSÃO INTERROMPIDA PELO LIMITE DE CUSTO/TEMPO. Nenhuma ferramenta deste lote foi executada.',
|
|
671
|
+
guard: guardStopped,
|
|
672
|
+
}) });
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
|
|
603
676
|
for (const tc of tcs) {
|
|
604
677
|
const name = (tc.function && tc.function.name) || '';
|
|
605
678
|
let input = {}; try { input = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
|
|
606
679
|
let result;
|
|
680
|
+
let _cachedContent = '';
|
|
607
681
|
let _ran = false; // true só quando uma ferramenta REALMENTE executou (não gate/bloqueio) → status ✓/✗
|
|
608
682
|
|
|
683
|
+
guardStopped = _guard();
|
|
684
|
+
if (guardStopped) {
|
|
685
|
+
result = { erro: 'MISSÃO INTERROMPIDA PELO LIMITE DE CUSTO/TEMPO. Esta ferramenta não foi executada.', guard: guardStopped };
|
|
686
|
+
}
|
|
687
|
+
|
|
609
688
|
// ── WATCHDOG anti-loop: mede repetição ANTES de qualquer gate/execução ──
|
|
610
689
|
const _sig = loopSig(name, input);
|
|
611
690
|
_recentSigs.push(_sig); if (_recentSigs.length > 8) _recentSigs.shift();
|
|
@@ -722,6 +801,24 @@ async function run(task, opts = {}) {
|
|
|
722
801
|
// Dispositivos físicos: parear/conectar/instalar/iniciar/capturar alteram o
|
|
723
802
|
// aparelho ou coletam sua tela. Passam por consentimento explícito sem
|
|
724
803
|
// transformar IP/código/caminho em comando shell.
|
|
804
|
+
// Nuvem pessoal: cada alteração exige consentimento específico. Nem --yes nem
|
|
805
|
+
// "aprovar tudo" silenciam este gate.
|
|
806
|
+
if (result === undefined && CLOUD_MUTATING.has(name)) {
|
|
807
|
+
const payload = JSON.stringify(input || {});
|
|
808
|
+
const provider = name.includes('outlook') || name.includes('microsoft365') ? 'MICROSOFT 365' : 'GOOGLE WORKSPACE';
|
|
809
|
+
const label = `${provider} → ${name}: ${payload.length > 1500 ? payload.slice(0, 1500) + ` …(+${payload.length - 1500} chars)` : payload}`;
|
|
810
|
+
let approved = false, remoteTried = false;
|
|
811
|
+
if (!yes) {
|
|
812
|
+
const dec = await askApprove({ kind: 'cloud_mutation', cmd: label, warning: name.includes('enviar_') ? 'O e-mail será enviado ao destinatário.' : `Dados da conta ${provider} serão alterados.` });
|
|
813
|
+
approved = decideApproval({ autoAll: false, decision: dec }).approved;
|
|
814
|
+
} else ({ approved, remoteTried } = await _remoteApprove(label, token, onRemote));
|
|
815
|
+
if (!approved) {
|
|
816
|
+
result = { erro: yes
|
|
817
|
+
? (remoteTried ? `RECUSADO: o dono negou ou não respondeu à alteração ${provider}.` : `RECUSADO: alteração ${provider} exige aprovação remota no modo --yes.`)
|
|
818
|
+
: `O usuário recusou a alteração na conta ${provider}. Não repita nesta sessão.` };
|
|
819
|
+
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
725
822
|
if (result === undefined && DEVICE_MUTATING.has(name)) {
|
|
726
823
|
const safe = name === 'android_parear'
|
|
727
824
|
? `${input.host || '?'}:${input.porta || '?'} (código oculto)`
|
|
@@ -877,6 +974,15 @@ async function run(task, opts = {}) {
|
|
|
877
974
|
result = { resumo: sub.resumo };
|
|
878
975
|
steps++; _ran = true;
|
|
879
976
|
}
|
|
977
|
+
if (result === undefined && READONLY.has(name)) {
|
|
978
|
+
const _cached = _missionCache.get(name, input);
|
|
979
|
+
if (_cached) {
|
|
980
|
+
const _visible = messages.some(m => m && m.role === 'tool' && m.tool_call_id === _cached.toolCallId);
|
|
981
|
+
_cachedContent = cacheReference(_cached, name, _visible);
|
|
982
|
+
result = { cached: true, tool: name };
|
|
983
|
+
onStep({ name, detail: 'cache da missão: ' + argsShort(name, input) });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
880
986
|
if (result === undefined) {
|
|
881
987
|
onStep({ name, detail: argsShort(name, input) });
|
|
882
988
|
result = await tools.execute(name, input, { confineDir, baseDir: cwd, token, allowedTools: _allowedToolNames });
|
|
@@ -887,6 +993,16 @@ async function run(task, opts = {}) {
|
|
|
887
993
|
const _tr = core.classifyToolResult(result);
|
|
888
994
|
if (!_tr.ok && _tr.status !== 'blocked') {
|
|
889
995
|
_toolErrs.push({ tool: name, class: _tr.errorClass, retryable: _tr.retryable, evidence: _tr.evidence });
|
|
996
|
+
const _failureKey = failureFingerprint(name, _tr, result);
|
|
997
|
+
const _equivalentFailures = (_failureCounts.get(_failureKey) || 0) + 1;
|
|
998
|
+
_failureCounts.set(_failureKey, _equivalentFailures);
|
|
999
|
+
const _failureGuard = _guard(_equivalentFailures);
|
|
1000
|
+
if (_failureGuard && _failureGuard.kind === 'equivalent_failures') {
|
|
1001
|
+
guardStopped = Object.assign({}, _failureGuard, { tool: name, evidence: String(_tr.evidence || '').slice(0, 240) });
|
|
1002
|
+
result = Object.assign({}, result, {
|
|
1003
|
+
_guard: `A mesma falha operacional ocorreu ${_equivalentFailures} vezes. O agente foi interrompido para não gastar tentando variações sem prova de progresso.`,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
890
1006
|
// LEDGER DE ERROS (Evolve 3): registro determinístico, zero IA.
|
|
891
1007
|
try { require('./memoria').logErro(confineDir || cwd, name, String(result.erro || result.stderr || ('exit ' + result.codigo))); } catch (_) {}
|
|
892
1008
|
// RECOVERY ENGINE: erro de classe CONHECIDA → injeta a estratégia de conserto no
|
|
@@ -901,7 +1017,7 @@ async function run(task, opts = {}) {
|
|
|
901
1017
|
if (result && result._setCwd) { cwd = result._setCwd; delete result._setCwd; }
|
|
902
1018
|
// registra a ação se deu certo (arquivo escrito / comando com código 0)
|
|
903
1019
|
if (!result.erro) {
|
|
904
|
-
if (
|
|
1020
|
+
if (['escrever_arquivo', 'editar_arquivo', 'editar_documento', 'editar_planilha'].includes(name) && result.ok) {
|
|
905
1021
|
actions.push({ name, target: result.caminho });
|
|
906
1022
|
// CHECKPOINT da sessão: anota o que mudou e onde está o backup (que o _snapshot
|
|
907
1023
|
// já criou). É o que permite "desfaz tudo o que o agente fez", em vez de só um
|
|
@@ -919,6 +1035,12 @@ async function run(task, opts = {}) {
|
|
|
919
1035
|
if (result && (result._backup !== undefined || result._acao !== undefined)) {
|
|
920
1036
|
result = Object.assign({}, result); delete result._backup; delete result._acao;
|
|
921
1037
|
}
|
|
1038
|
+
const _cacheClass = core.classifyToolResult(result);
|
|
1039
|
+
if (_cacheClass.ok && READONLY.has(name)) {
|
|
1040
|
+
_missionCache.remember(name, input, { toolCallId: tc.id, content: JSON.stringify(result).slice(0, TOOL_RESULT_CAP) });
|
|
1041
|
+
} else if (_cacheClass.ok && !READONLY.has(name)) {
|
|
1042
|
+
_missionCache.invalidate();
|
|
1043
|
+
}
|
|
922
1044
|
}
|
|
923
1045
|
// AVISO SUAVE de convergência: a partir de 2/3 do teto de pesquisa, empurra o modelo a
|
|
924
1046
|
// concluir (o limite DURO acima corta de vez; este só sinaliza antes, sem bloquear).
|
|
@@ -938,15 +1060,24 @@ async function run(task, opts = {}) {
|
|
|
938
1060
|
_logEp('human');
|
|
939
1061
|
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, cwd, context: lastCtx, needHuman: { motivo: result.motivo, o_que_fazer: result.o_que_fazer } };
|
|
940
1062
|
}
|
|
941
|
-
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result).slice(0, TOOL_RESULT_CAP) });
|
|
1063
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: _cachedContent || JSON.stringify(result).slice(0, TOOL_RESULT_CAP) });
|
|
1064
|
+
if (guardStopped) break;
|
|
942
1065
|
}
|
|
943
|
-
if (loopedOut) break; // watchdog cortou →
|
|
1066
|
+
if (loopedOut || guardStopped) break; // watchdog/trava de orçamento cortou → fechamento determinístico
|
|
944
1067
|
|
|
945
1068
|
}
|
|
946
1069
|
|
|
947
1070
|
// CANCELADO cooperativamente: não faz a chamada de fechamento (não gastar mais IA);
|
|
948
1071
|
// devolve um texto curto e o que já rolou.
|
|
949
1072
|
if (stopped && !finalText) finalText = lang !== 'en' ? '(execução cancelada)' : '(run cancelled)';
|
|
1073
|
+
if (guardStopped && !finalText) {
|
|
1074
|
+
const labels = lang !== 'en'
|
|
1075
|
+
? { credits: 'o teto de créditos', tokens: 'o teto de tokens', time: 'o tempo máximo', equivalent_failures: 'a mesma falha repetida' }
|
|
1076
|
+
: { credits: 'the credit cap', tokens: 'the token cap', time: 'the time limit', equivalent_failures: 'the same repeated failure' };
|
|
1077
|
+
finalText = lang !== 'en'
|
|
1078
|
+
? `Interrompi esta missão porque ela atingiu ${labels[guardStopped.kind] || 'o limite de segurança'}. Foram executados ${steps} passo(s) e consumidos ${charged + _visionCredits} crédito(s). O que já foi concluído permanece válido; a etapa que falhou não foi declarada como pronta. Revise o último erro antes de continuar ou aumente o orçamento explicitamente.`
|
|
1079
|
+
: `I stopped this mission because it reached ${labels[guardStopped.kind] || 'the safety limit'}. ${steps} step(s) ran and ${charged + _visionCredits} credit(s) were consumed. Completed work remains valid; the failed step was not reported as done. Review the last error before continuing or explicitly raise the budget.`;
|
|
1080
|
+
}
|
|
950
1081
|
// FECHAMENTO GARANTIDO: o loop NUNCA termina em silêncio. Se saiu sem texto
|
|
951
1082
|
// (esgotou MAX_ITER ainda chamando ferramentas, ou o modelo devolveu vazio),
|
|
952
1083
|
// faz UMA chamada final SEM ferramentas pedindo um resumo honesto ao usuário.
|
|
@@ -974,7 +1105,7 @@ async function run(task, opts = {}) {
|
|
|
974
1105
|
}
|
|
975
1106
|
|
|
976
1107
|
if (_hooks._any) { try { _hooksMod.run(_hooks, 'Stop', { cwd, text: finalText, steps }); } catch (_) {} }
|
|
977
|
-
_logEp(stopped ? 'cancel' : (loopedOut ? 'stuck' : 'done'));
|
|
1108
|
+
_logEp(stopped ? 'cancel' : ((loopedOut || guardStopped) ? 'stuck' : 'done'));
|
|
978
1109
|
await _bill();
|
|
979
1110
|
// toolErrors: erros de ferramenta que NÃO foram seguidos de uma ação bem-sucedida da MESMA
|
|
980
1111
|
// ferramenta depois (heurística leve de "não-recuperado") — sinal duro pro meta não marcar done falso.
|
|
@@ -1000,12 +1131,12 @@ async function run(task, opts = {}) {
|
|
|
1000
1131
|
const _handoff = {
|
|
1001
1132
|
workstreamId: _workstreamId,
|
|
1002
1133
|
goal: taskText,
|
|
1003
|
-
status: stopped ? 'paused' : (loopedOut ? 'blocked' : 'done'),
|
|
1004
|
-
changedFiles: actions.filter(a =>
|
|
1134
|
+
status: stopped ? 'paused' : ((loopedOut || guardStopped) ? 'blocked' : 'done'),
|
|
1135
|
+
changedFiles: actions.filter(a => ['escrever_arquivo', 'editar_arquivo', 'editar_documento', 'editar_planilha'].includes(a.name)).map(a => a.target),
|
|
1005
1136
|
evidence: actions.map(a => `${a.name}: ${a.target}`),
|
|
1006
1137
|
failedApproaches: _toolErrs.map(e => `${e.tool || 'tool'}: ${e.errorClass || e.message || 'failed'}`),
|
|
1007
|
-
nextActions: loopedOut ? ['
|
|
1008
|
-
budget: { currency: 'credits', spent: charged + _visionCredits, remaining:
|
|
1138
|
+
nextActions: (loopedOut || guardStopped) ? ['Revisar o último erro e retomar com outra abordagem ou orçamento explícito.'] : [],
|
|
1139
|
+
budget: { currency: 'credits', spent: charged + _visionCredits, limit: maxCredits, remaining: Math.max(0, maxCredits - charged - _visionCredits), stoppedBy: guardStopped && guardStopped.kind },
|
|
1009
1140
|
};
|
|
1010
1141
|
const _actionExpected = /\b(cri|fa[cç]|implemente|corrija|edite|altere|instale|execute|rode|deploy|publique|remova|apague|delete|escreva|salve)\w*/i.test(taskText);
|
|
1011
1142
|
const _syncTasks = [
|
|
@@ -1021,8 +1152,8 @@ async function run(task, opts = {}) {
|
|
|
1021
1152
|
model: usedModel,
|
|
1022
1153
|
taskKind: /\b(c[oó]digo|code|bug|teste|test|node|javascript|typescript|python|html|css|api|arquivo|projeto)\b/i.test(taskText) ? 'code' : 'general',
|
|
1023
1154
|
usedTools: steps > 0,
|
|
1024
|
-
success: !stopped && !loopedOut,
|
|
1025
|
-
falseDone: !stopped && !loopedOut && _actionExpected && actions.length === 0,
|
|
1155
|
+
success: !stopped && !loopedOut && !guardStopped,
|
|
1156
|
+
falseDone: !stopped && !loopedOut && !guardStopped && _actionExpected && actions.length === 0,
|
|
1026
1157
|
protocolError: _toolErrs.some(e => e.class === 'invalid_schema'),
|
|
1027
1158
|
usage: { input_tokens: acc.inTok, output_tokens: acc.outTok, cached_tokens: acc.cachedTok },
|
|
1028
1159
|
source: 'cli',
|
|
@@ -1030,7 +1161,7 @@ async function run(task, opts = {}) {
|
|
|
1030
1161
|
}));
|
|
1031
1162
|
await Promise.all(_syncTasks);
|
|
1032
1163
|
} catch (_) {}
|
|
1033
|
-
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr };
|
|
1164
|
+
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped };
|
|
1034
1165
|
}
|
|
1035
1166
|
|
|
1036
|
-
module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval } };
|
|
1167
|
+
module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, validarModeloByok } };
|
package/lib/doctor.js
CHANGED
|
@@ -12,10 +12,16 @@ const { execFileSync } = require('child_process');
|
|
|
12
12
|
// ── helpers PUROS (testáveis) ────────────────────────────────────────────────
|
|
13
13
|
function _nodeMajor(v) { const m = String(v || '').replace(/^v/, '').split('.'); return Number(m[0]) || 0; }
|
|
14
14
|
function _nodeOk(v) { return _nodeMajor(v) >= 18; }
|
|
15
|
-
//
|
|
16
|
-
|
|
15
|
+
// Estado de autenticação. O plano vem do SERVIDOR quando disponível: o valor em ~/.ts é
|
|
16
|
+
// só o que estava valendo no login, e continua lá depois de assinar, trocar ou cancelar
|
|
17
|
+
// um plano — o diagnóstico exibia "ultra" para uma conta que o servidor já tratava como
|
|
18
|
+
// "pro". Sem rede, o valor guardado ainda é melhor que nada, mas fica marcado como tal.
|
|
19
|
+
function _authLevel(cfg, planoDoServidor) {
|
|
17
20
|
if (!cfg || !cfg.token) return { level: 'fail', detail: 'não logado', dica: 'rode: ts login' };
|
|
18
|
-
|
|
21
|
+
const plano = planoDoServidor || cfg.plan;
|
|
22
|
+
const local = !planoDoServidor && cfg.plan;
|
|
23
|
+
return { level: 'ok', detail: 'logado' + (cfg.username ? ' como ' + cfg.username : '')
|
|
24
|
+
+ (plano ? ' · plano ' + plano + (local ? ' (do último login)' : '') : '') };
|
|
19
25
|
}
|
|
20
26
|
// compara versão instalada × última do npm → ok / warn (desatualizado)
|
|
21
27
|
function _versionLevel(current, latest) {
|
|
@@ -55,7 +61,9 @@ async function run(opts = {}) {
|
|
|
55
61
|
results.push({ nome: 'Node.js', level: _nodeOk(nv) ? 'ok' : 'fail', detail: nv + (_nodeOk(nv) ? '' : ' (precisa 18+)'), dica: _nodeOk(nv) ? undefined : 'instale Node 18 ou superior' });
|
|
56
62
|
|
|
57
63
|
// 2) config ~/.ts + login
|
|
58
|
-
|
|
64
|
+
let planoDoServidor = null;
|
|
65
|
+
if (typeof opts.planoAtual === 'function') { try { planoDoServidor = await opts.planoAtual(); } catch (_) {} }
|
|
66
|
+
const auth = _authLevel(cfg, planoDoServidor); results.push(Object.assign({ nome: 'Login' }, auth));
|
|
59
67
|
const dir = path.join(os.homedir(), '.ts');
|
|
60
68
|
results.push({ nome: 'Config (~/.ts)', level: fs.existsSync(dir) ? 'ok' : 'warn', detail: fs.existsSync(dir) ? dir : 'ainda não criado (normal se nunca rodou nada)' });
|
|
61
69
|
|
package/lib/i18n.js
CHANGED
|
@@ -75,7 +75,10 @@ const STR = {
|
|
|
75
75
|
['ts login · ts logout', 'conecta / desconecta este terminal'],
|
|
76
76
|
['ts uso', 'créditos e consumo do mês'],
|
|
77
77
|
['ts quem', 'conta conectada'],
|
|
78
|
+
['ts conta exportar', 'baixa seus dados sem senhas, tokens ou chaves privadas'],
|
|
79
|
+
['ts conta abrir', 'abre Minha Conta para privacidade e exclusão segura'],
|
|
78
80
|
['ts doctor', 'diagnóstico do ambiente (Node, login, gateway, versão, deps opcionais)'],
|
|
81
|
+
['ts privacidade', 'mostra o endereço da Política de Privacidade'],
|
|
79
82
|
['ts tema', 'paleta do terminal (7 temas; a cor indica o ESTADO)'],
|
|
80
83
|
['ts idioma pt|en', 'idioma (padrão pt-BR)'],
|
|
81
84
|
] },
|
|
@@ -240,6 +243,9 @@ const STR = {
|
|
|
240
243
|
['ts login · ts logout', 'connect / disconnect this terminal'],
|
|
241
244
|
['ts uso', 'monthly credits and usage'],
|
|
242
245
|
['ts quem', 'connected account'],
|
|
246
|
+
['ts account export', 'download your data without passwords, tokens or private keys'],
|
|
247
|
+
['ts account open', 'open Account for privacy controls and safe deletion'],
|
|
248
|
+
['ts privacy', 'shows the Privacy Policy address'],
|
|
243
249
|
['ts tema', 'terminal palette (7 themes; color means STATE)'],
|
|
244
250
|
['ts idioma pt|en', 'language (default pt-BR)'],
|
|
245
251
|
] },
|