terminal-smart-cli 0.98.1 → 0.99.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/README.md +7 -5
- package/bin/ts.js +344 -196
- package/hub/cli.js +255 -0
- package/hub/controle.js +262 -0
- package/hub/dpapi.js +55 -0
- package/hub/estado.js +274 -0
- package/hub/exportar-telas.js +149 -0
- package/hub/hub.js +717 -0
- package/hub/mascara.js +67 -0
- package/hub/parar.js +151 -0
- package/hub/principal.js +5 -0
- package/hub/proto/PROTOCOLO.md +789 -0
- package/hub/proto/ts-proto.js +1643 -0
- package/hub/relay-conexao.js +200 -0
- package/hub/telas-arquivo.js +130 -0
- package/hub/tmux.js +189 -0
- package/hub/vps/README.md +109 -0
- package/hub/vps/tmux-agentes.conf +5 -0
- package/hub/vps/ts-hub-exportar-telas.service +30 -0
- package/hub/vps/ts-hub-parar-consumir +67 -0
- package/hub/vps/ts-hub-parar.path +16 -0
- package/hub/vps/ts-hub-parar.service +23 -0
- package/hub/vps/ts-hub-parar.tmpfiles.conf +10 -0
- package/hub/vps/ts-hub.service +39 -0
- package/hub/vps/ts-tmux.service +19 -0
- package/lib/acp.js +8 -2
- package/lib/agent.js +183 -85
- package/lib/chat-local.js +139 -0
- package/lib/core.js +336 -23
- package/lib/doctor.js +13 -5
- package/lib/i18n.js +69 -31
- package/lib/ia-local.js +388 -0
- package/lib/intelligence-core.js +6 -1
- package/lib/keyring.js +0 -1
- package/lib/meta.js +8 -8
- package/lib/personal-agent-llm.js +99 -26
- package/lib/sentinela.js +2 -2
- package/lib/tools.js +638 -8
- package/package.json +4 -2
package/bin/ts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// ⌁ Terminal Smart CLI — agente DevOps no seu terminal.
|
|
3
|
-
//
|
|
3
|
+
// Cliente do backend (orquestração, uso); o chat roda com a IA do próprio usuário neste PC.
|
|
4
|
+
// Docs: terminalsmart.com.br/cli
|
|
4
5
|
'use strict';
|
|
5
6
|
const pkg = require('../package.json');
|
|
6
7
|
const config = require('../lib/config');
|
|
@@ -92,9 +93,14 @@ async function readStdin() {
|
|
|
92
93
|
for await (const c of process.stdin) { d += c; if (d.length > 120000) break; }
|
|
93
94
|
return d;
|
|
94
95
|
}
|
|
96
|
+
// Erros da IA local (lib/ia-local e lib/chat-local): a mensagem já diz o que fazer.
|
|
97
|
+
const _CODIGOS_IA_LOCAL = new Set(['sem_ia_local', 'ia_local_indisponivel', 'nao_instalada', 'nao_logada', 'sessao_expirada',
|
|
98
|
+
'desatualizada', 'limite', 'timeout', 'execucao_insegura', 'falha_ia_local', 'sem_resposta', 'modo_invalido', 'saida_grande', 'falha_execucao']);
|
|
99
|
+
function _erroIaLocal(e) { return !!(e && !(e instanceof ApiError) && _CODIGOS_IA_LOCAL.has(e.code)); }
|
|
95
100
|
function fail(e) {
|
|
96
101
|
if (JSON_OUT) { console.log(JSON.stringify({ ok: false, error: (e && e.message) || 'erro', code: e && e.code })); process.exit(1); }
|
|
97
|
-
if (e
|
|
102
|
+
if (_erroIaLocal(e)) console.error(ui.errLine(e.message));
|
|
103
|
+
else if (e instanceof ApiError) {
|
|
98
104
|
if (e.code === 'auth') console.error(ui.errLine(T.session_expired));
|
|
99
105
|
else if (e.code === 'conn') console.error(ui.errLine(T.err_conn));
|
|
100
106
|
else if (e.code === 'no_credits') console.error(ui.errLine(T.no_credits));
|
|
@@ -112,10 +118,65 @@ function providerCapacityHint(e) {
|
|
|
112
118
|
if (!active) return '';
|
|
113
119
|
const provider = active.provedor || active.id || 'provedor particular';
|
|
114
120
|
return cfg.lang === 'en'
|
|
115
|
-
? `Your own ${provider} key has insufficient provider balance/capacity.
|
|
116
|
-
: `Sua chave própria do ${provider} está sem saldo/capacidade no provedor.
|
|
121
|
+
? `Your own ${provider} key has insufficient provider balance/capacity. Nothing was charged by Terminal Smart. Add balance at the provider, switch providers with \`ts conectar <provider>\` (or local Ollama) or use your subscription with \`ts ia conectar chatgpt|claude\`.`
|
|
122
|
+
: `Sua chave própria do ${provider} está sem saldo/capacidade no provedor. Nada foi cobrado pelo Terminal Smart. Adicione saldo no provedor, troque de provedor com \`ts conectar <provedor>\` (ou Ollama local) ou use sua assinatura com \`ts ia conectar chatgpt|claude\`.`;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// 402/teto de custo com a chave PRÓPRIA (BYOK) é saldo/limite do provedor, não falta da IA
|
|
126
|
+
// do TS: a mensagem de "sem créditos legados" apontaria a causa errada. Mesma condição do
|
|
127
|
+
// keyring.resolve para a execução ter rodado na chave do usuário.
|
|
128
|
+
function byokNoBalanceHint(e) {
|
|
129
|
+
if (!e || !(e.code === 'no_credits' || Number(e.status) === 402)) return '';
|
|
130
|
+
let active = null;
|
|
131
|
+
try { active = require('../lib/keyring').ativo(); } catch (_) {}
|
|
132
|
+
if (!active || !(active.key || (require('../lib/providers').info(active.id) || {}).semChave)) return '';
|
|
133
|
+
return T.byok_no_balance(active.provedor || active.id || (cfg.lang === 'en' ? 'private provider' : 'provedor particular'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Falha do resumo automático (compactar_contexto bloqueado) não é bloqueio de política: é
|
|
137
|
+
// perda da memória antiga. Linha amarela própria, igual no `ts agente` e no `ts meta`.
|
|
138
|
+
function compactFailLine(name, blocked, detail) {
|
|
139
|
+
if (name !== 'compactar_contexto' || !blocked) return '';
|
|
140
|
+
return C.warn('▲ ' + (cfg.lang === 'en' ? 'context: ' : 'contexto: ') + (detail || ''));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// De onde vem a IA desta conta, em linguagem de produto: via sua assinatura (a CLI oficial do
|
|
144
|
+
// ChatGPT/Codex ou do Claude logada NESTE PC), via sua chave (<provedor>) ou via Ollama local.
|
|
145
|
+
// Só estado LOCAL — nenhuma consulta ao servidor. O critério é o do `ts agente`: o modo da conta
|
|
146
|
+
// (`ts ia usar`) vale quando está SELECIONADO e a CLI exigida está pronta, e aí vence a chave
|
|
147
|
+
// local. Modo selecionado sem a CLI pronta: o agente cancela a execução — sai um aviso (hint,
|
|
148
|
+
// blocking), nunca "via sua assinatura". CLI logada com o modo automático e sem chave: a dica é
|
|
149
|
+
// selecionar (ts ia usar), não conectar de novo. { via:null } = nenhuma IA própria (no agente
|
|
150
|
+
// sobra o legado do TS; o chat não tem reserva).
|
|
151
|
+
function _modoIaLocal() {
|
|
152
|
+
const m = String(cfg.personalModePreference || 'automatico').toLowerCase();
|
|
153
|
+
return require('../lib/ia-local').modoValido(m) ? m : 'automatico';
|
|
154
|
+
}
|
|
155
|
+
const _personalLabel = (mode) => require('../lib/ia-local').rotuloModo(mode);
|
|
156
|
+
async function aiSource(_token, { timeoutMs = 3000 } = {}) {
|
|
157
|
+
const iaLocal = require('../lib/ia-local');
|
|
158
|
+
const mode = _modoIaLocal();
|
|
159
|
+
if (mode !== 'automatico') {
|
|
160
|
+
const st = await iaLocal.status({ prazoMs: timeoutMs });
|
|
161
|
+
if (iaLocal.pronto(mode, st)) return { via: T.via_subscription + ' (' + _personalLabel(mode) + ')', unknown: false };
|
|
162
|
+
const okC = iaLocal.pronto('chatgpt', st), okA = iaLocal.pronto('claude', st);
|
|
163
|
+
const falta = mode === 'chatgpt' || mode === 'claude' ? mode : (!okC && !okA ? 'chatgpt|claude' : !okC ? 'chatgpt' : 'claude');
|
|
164
|
+
return { via: null, unknown: false, blocking: true, hint: T.uso_ai_sub_disconnected(_personalLabel(mode), falta) };
|
|
165
|
+
}
|
|
166
|
+
let at = null;
|
|
167
|
+
try { at = require('../lib/keyring').ativo(); } catch (_) {}
|
|
168
|
+
if (at) return { via: at.id === 'ollama' ? T.via_ollama : T.via_key(at.id), unknown: false };
|
|
169
|
+
// automático e sem chave: alguma CLI da conta já logada neste PC? (só pelo arquivo, sem processo)
|
|
170
|
+
const st = await iaLocal.status({ rapido: true });
|
|
171
|
+
const logada = (p) => !!(st[p] && st[p].instalada && st[p].logada === true);
|
|
172
|
+
if (logada('chatgpt') || logada('claude')) {
|
|
173
|
+
return { via: null, unknown: false, hint: T.uso_ai_sub_not_selected(logada('chatgpt') && logada('claude') ? 'chatgpt|claude' : logada('chatgpt') ? 'chatgpt' : 'claude') };
|
|
174
|
+
}
|
|
175
|
+
return { via: null, unknown: false };
|
|
176
|
+
}
|
|
177
|
+
// texto da linha "IA em uso" quando não há IA própria confirmada
|
|
178
|
+
const aiSourceMissing = (src) => C.warn(src && src.hint ? src.hint : src && src.unknown ? T.uso_ai_unknown : T.uso_ai_none);
|
|
179
|
+
|
|
119
180
|
// ── Ajuda ────────────────────────────────────────────────────────────────────
|
|
120
181
|
function help() {
|
|
121
182
|
const out = [ui.banner(pkg.version, T.tagline)];
|
|
@@ -227,57 +288,89 @@ async function logout() {
|
|
|
227
288
|
console.log(ui.okLine(T.logout_ok));
|
|
228
289
|
}
|
|
229
290
|
|
|
230
|
-
// ──
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
291
|
+
// ── IA da sua conta: ChatGPT/Codex e Claude Code logados NESTE PC ────────────
|
|
292
|
+
// Tudo local: o modo fica na config deste PC (`ts ia usar`) e o login é o oficial de cada CLI,
|
|
293
|
+
// feito aqui no terminal. Nada disso passa pelo servidor do Terminal Smart.
|
|
294
|
+
async function personalAiCmd(args = [], { interativo = true } = {}) {
|
|
295
|
+
const en = cfg.lang === 'en'; const sub = String(args[0] || 'status').toLowerCase();
|
|
296
|
+
const iaLocal = require('../lib/ia-local');
|
|
297
|
+
const label = (p) => iaLocal.nomeDe(p);
|
|
298
|
+
const ajuda = 'Use: ts ia status | conectar chatgpt|claude | usar automatico|chatgpt|claude|paralelo | desconectar chatgpt|claude';
|
|
299
|
+
const estado = (s) => !s.instalada ? (en ? 'not installed' : 'não instalado')
|
|
300
|
+
: s.detalhe ? (en ? 'installed · cannot run safely' : 'instalado · sem execução segura')
|
|
301
|
+
: s.logada === true ? (en ? 'installed · logged in' : 'instalado · logado')
|
|
302
|
+
: s.logada === false ? (en ? 'installed · not logged in' : 'instalado · não logado')
|
|
303
|
+
: (en ? 'installed · login not confirmed' : 'instalado · login não confirmado');
|
|
304
|
+
const show = async () => {
|
|
305
|
+
const mode = _modoIaLocal();
|
|
306
|
+
const st = await iaLocal.status();
|
|
307
|
+
if (JSON_OUT) { console.log(JSON.stringify({ ok: true, local: true, mode, chatgpt: st.chatgpt, claude: st.claude })); return; }
|
|
308
|
+
const lines = [
|
|
309
|
+
`${en ? 'Mode' : 'Modo'}: ${mode}`,
|
|
310
|
+
`ChatGPT/Codex (codex): ${estado(st.chatgpt)}`,
|
|
311
|
+
`Claude (claude): ${estado(st.claude)}`,
|
|
312
|
+
];
|
|
313
|
+
if (mode !== 'automatico' && !iaLocal.pronto(mode, st)) {
|
|
314
|
+
const okC = iaLocal.pronto('chatgpt', st), okA = iaLocal.pronto('claude', st);
|
|
315
|
+
const falta = mode === 'paralelo' ? (!okC && !okA ? 'chatgpt|claude' : !okC ? 'chatgpt' : 'claude') : mode;
|
|
316
|
+
lines.push('', C.warn('▲ ' + T.uso_ai_sub_disconnected(_personalLabel(mode), falta)));
|
|
317
|
+
}
|
|
318
|
+
lines.push('', ...(en
|
|
319
|
+
? ['Runs on this PC with your own account —', 'nothing goes through the Terminal Smart server and no TS credits are used.']
|
|
320
|
+
: ['Roda neste PC com a sua própria conta —', 'nada passa pelo servidor do Terminal Smart e não usa créditos do TS.']).map(l => C.dim(l)));
|
|
321
|
+
console.log('\n' + ui.box(lines, { title: en ? 'Your account AI (this PC)' : 'IA da sua conta (neste PC)' }) + '\n');
|
|
242
322
|
};
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
323
|
+
const provedor = () => {
|
|
324
|
+
const p = String(args[1] || '').toLowerCase();
|
|
325
|
+
if (!['chatgpt', 'claude'].includes(p)) throw new Error(`Use: ts ia ${sub} chatgpt|claude`);
|
|
326
|
+
return p;
|
|
327
|
+
};
|
|
328
|
+
if (sub === 'status') return show();
|
|
329
|
+
if (['usar', 'use', 'modo', 'mode'].includes(sub)) {
|
|
330
|
+
const mode = String(args[1] || '').toLowerCase();
|
|
331
|
+
if (!iaLocal.modoValido(mode)) throw new Error('Use: ts ia usar automatico|chatgpt|claude|paralelo');
|
|
332
|
+
cfg = config.save({ personalModePreference: mode });
|
|
333
|
+
if (!JSON_OUT) console.log(ui.okLine((en ? 'AI mode saved on this PC: ' : 'modo da IA salvo neste PC: ') + C.bold(mode)));
|
|
334
|
+
return show();
|
|
335
|
+
}
|
|
336
|
+
if (['desconectar', 'disconnect'].includes(sub)) {
|
|
337
|
+
const p = provedor();
|
|
338
|
+
const passos = p === 'chatgpt'
|
|
339
|
+
? [en ? 'Run the official logout on this PC:' : 'Rode o logout oficial neste PC:', ' ' + C.cyan('codex logout')]
|
|
340
|
+
: [en ? 'Log out with the official CLI on this PC:' : 'Saia pela CLI oficial neste PC:', ' ' + C.cyan('claude') + C.dim(en ? ' → type /logout' : ' → digite /logout')];
|
|
341
|
+
const mode = _modoIaLocal();
|
|
342
|
+
if (mode === p || mode === 'paralelo') passos.push('', C.dim(en ? `The "${mode}" mode stays selected; switch with \`ts ia usar automatico\`.` : `O modo "${mode}" continua escolhido; troque com \`ts ia usar automatico\`.`));
|
|
343
|
+
console.log('\n' + ui.box(passos, { title: (en ? 'Disconnect ' : 'Desconectar ') + label(p) }) + '\n');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (['conectar', 'connect'].includes(sub)) {
|
|
347
|
+
const p = provedor();
|
|
348
|
+
const st = await iaLocal.statusDe(p);
|
|
349
|
+
if (!st.instalada) { console.log(ui.infoLine(`${label(p)} ${en ? 'is not installed on this PC.' : 'não está instalado neste PC.'} ${iaLocal.comoInstalar(p)}`)); return; }
|
|
350
|
+
if (st.detalhe) throw new Error(`${label(p)}: ${st.detalhe}`);
|
|
351
|
+
if (st.logada === true) {
|
|
352
|
+
console.log(ui.okLine(`${label(p)} ${en ? 'is already logged in on this PC.' : 'já está logado neste PC.'}`) + C.dim(en ? ` To use it: ts ia usar ${p}` : ` Para usar: ts ia usar ${p}`));
|
|
353
|
+
return;
|
|
271
354
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
355
|
+
// no REPL (interativo:false) o login não abre aqui: o readline dele disputaria o teclado
|
|
356
|
+
if (p === 'claude' || !interativo || !process.stdin.isTTY || JSON_OUT) {
|
|
357
|
+
const passos = p === 'claude'
|
|
358
|
+
? [`1. ${en ? 'Run' : 'Rode'} ${C.cyan('claude')} ${en ? 'in this terminal' : 'neste terminal'}`,
|
|
359
|
+
`2. ${en ? 'Type' : 'Digite'} ${C.cyan('/login')} ${en ? 'and finish in the browser' : 'e conclua no navegador'}`,
|
|
360
|
+
`3. ${en ? 'Leave with' : 'Saia com'} ${C.cyan('/exit')} ${en ? 'and run' : 'e rode'} ${C.cyan('ts ia usar claude')}`]
|
|
361
|
+
: [`1. ${en ? 'Run' : 'Rode'} ${C.cyan('codex login')} ${en ? 'in a terminal on this PC' : 'num terminal deste PC'}`,
|
|
362
|
+
`2. ${en ? 'Then run' : 'Depois rode'} ${C.cyan('ts ia usar chatgpt')}`];
|
|
363
|
+
console.log('\n' + ui.box([...passos, '', C.dim(en ? 'The login is the official one, on this PC — Terminal Smart never sees your password or token.' : 'O login é o oficial, neste PC — o Terminal Smart nunca vê sua senha nem seu token.')], { title: `Login ${label(p)}` }) + '\n');
|
|
364
|
+
return;
|
|
277
365
|
}
|
|
278
|
-
|
|
366
|
+
console.log(ui.infoLine(en ? 'Opening the official Codex login in this terminal…' : 'Abrindo o login oficial do Codex neste terminal…'));
|
|
367
|
+
await iaLocal.abrirLogin('chatgpt');
|
|
368
|
+
const depois = await iaLocal.statusDe('chatgpt');
|
|
369
|
+
if (depois.logada === false) throw new Error(en ? 'The login was not completed. Try again with `codex login`.' : 'O login não foi concluído. Tente de novo com `codex login`.');
|
|
370
|
+
console.log(ui.okLine(`${label(p)} ${en ? 'logged in on this PC.' : 'logado neste PC.'}`) + C.dim(en ? ' To use it: ts ia usar chatgpt' : ' Para usar: ts ia usar chatgpt'));
|
|
371
|
+
return;
|
|
279
372
|
}
|
|
280
|
-
throw new Error(
|
|
373
|
+
throw new Error(ajuda);
|
|
281
374
|
}
|
|
282
375
|
|
|
283
376
|
// ── Chat ─────────────────────────────────────────────────────────────────────
|
|
@@ -352,7 +445,7 @@ async function imageCmd(args) {
|
|
|
352
445
|
const selected = options.find(o => o.id === choice);
|
|
353
446
|
if (!selected || selected.allowed === false) {
|
|
354
447
|
throw new ApiError((selected && selected.upgradeRequired)
|
|
355
|
-
? (en ? 'GPT Image 2
|
|
448
|
+
? (en ? 'GPT Image 2 is not available on your plan.' : 'GPT Image 2 não está disponível no seu plano.')
|
|
356
449
|
: (en ? 'Invalid image option.' : 'Opção de imagem inválida.'), { code: 'plan_limit' });
|
|
357
450
|
}
|
|
358
451
|
if (choice === 'none') {
|
|
@@ -481,43 +574,17 @@ async function generateVideoCmd(args) {
|
|
|
481
574
|
if (JSON_OUT) console.log(JSON.stringify(output)); else console.log('\n' + ui.okLine((en ? 'video saved: ' : 'vídeo salvo: ') + C.cyan(target)) + '\n ' + C.dim(`${job.model} · ${job.charged} créditos · ${job.seconds}s`));
|
|
482
575
|
} finally { spinner.stop(); }
|
|
483
576
|
}
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
// reutilizar um agente/modelo premium antigo, fora da equipe oficial.
|
|
496
|
-
token, body: { conversationId: convId, content, projectId: memoryScope.projectId, workstreamId: memoryScope.workstreamId, surface: 'cli', mode: 'auto' },
|
|
497
|
-
onEvent: (ev) => {
|
|
498
|
-
if (ev.delta) { buf += ev.delta; sp.text(`${T.receiving} ${fmtK(buf.length)}`); }
|
|
499
|
-
if (ev.error) errMsg = ev.error;
|
|
500
|
-
if (ev.tool) sp.text(String(ev.tool).slice(0, 64));
|
|
501
|
-
},
|
|
502
|
-
});
|
|
503
|
-
} catch (e) { sp.stop(); throw e; }
|
|
504
|
-
sp.stop();
|
|
505
|
-
if (errMsg && !buf) throw new ApiError(errMsg, {});
|
|
506
|
-
return buf;
|
|
507
|
-
};
|
|
508
|
-
let convId = await ensureConv(token);
|
|
509
|
-
try { return await doStream(convId); }
|
|
510
|
-
catch (e) {
|
|
511
|
-
if (e instanceof ApiError && e.status === 404) {
|
|
512
|
-
const scope = _convScope('chat');
|
|
513
|
-
const scopes = Object.assign({}, cfg.convScopes || {}); delete scopes[scope];
|
|
514
|
-
cfg = config.save({ convId: undefined, convScopes: scopes });
|
|
515
|
-
convId = await ensureConv(token); return doStream(convId);
|
|
516
|
-
}
|
|
517
|
-
throw e;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
function printAnswer(text, ms) {
|
|
577
|
+
// Chat LOCAL: a IA é a do usuário, neste PC — o modo da conta (`ts ia usar` → CLI oficial
|
|
578
|
+
// logada aqui) ou, sem modo, a chave do `ts conectar` (inclusive Ollama). O histórico fica em
|
|
579
|
+
// ~/.ts/chat, por pasta de trabalho. Nada da conversa vai ao servidor do Terminal Smart.
|
|
580
|
+
function _chatLocal() { return require('../lib/chat-local').criarChatLocal(); }
|
|
581
|
+
async function sendMessage(_token, content) {
|
|
582
|
+
const sp = JSON_OUT ? { stop() {} } : ui.spinner(T.thinking).start();
|
|
583
|
+
try {
|
|
584
|
+
const r = await _chatLocal().responder({ escopo: _convScope('chat'), conteudo: content, modo: _modoIaLocal(), lang: cfg.lang || 'pt' });
|
|
585
|
+
return r.texto;
|
|
586
|
+
} finally { sp.stop(); }
|
|
587
|
+
}function printAnswer(text, ms) {
|
|
521
588
|
const en = cfg.lang === 'en';
|
|
522
589
|
const w = Math.min(process.stdout.columns || 80, 96);
|
|
523
590
|
console.log('');
|
|
@@ -980,13 +1047,26 @@ async function chat(question) {
|
|
|
980
1047
|
// ── Modo conversa (REPL) — o modo PADRÃO do ts: digite naturalmente ──────────
|
|
981
1048
|
async function chatRepl() {
|
|
982
1049
|
const token = needToken();
|
|
983
|
-
// Cabeçalho: banner + conta +
|
|
1050
|
+
// Cabeçalho: banner + conta + DE ONDE VEM A IA (via sua assinatura/chave/Ollama local).
|
|
1051
|
+
// Crédito só aparece como saldo LEGADO e só sem IA própria (best-effort, não trava a entrada).
|
|
984
1052
|
console.log(ui.banner(pkg.version, T.tagline));
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1053
|
+
{
|
|
1054
|
+
// as duas consultas em PARALELO, com prazo curto e sem re-tentativa: sem rede, a entrada
|
|
1055
|
+
// espera ~3 s (antes eram duas em sequência, com re-tentativas)
|
|
1056
|
+
const [srcR, crR] = await Promise.allSettled([
|
|
1057
|
+
aiSource(token, { timeoutMs: 3000 }),
|
|
1058
|
+
api('/api/credits', { token, timeoutMs: 3000, retry: false }),
|
|
1059
|
+
]);
|
|
1060
|
+
const src = srcR.status === 'fulfilled' ? srcR.value : { via: null, unknown: true };
|
|
1061
|
+
let legacy = null;
|
|
1062
|
+
if (!src.via && crR.status === 'fulfilled' && crR.value) {
|
|
1063
|
+
const cr = crR.value, rest = Number(cr.remaining) || 0;
|
|
1064
|
+
legacy = (cr.unlimited || cr.granted < 0) ? T.uso_unlimited : (rest > 0 ? T.repl_credits(rest.toLocaleString('pt-BR')) : null);
|
|
1065
|
+
}
|
|
1066
|
+
// modo selecionado sem a conta conectada: o agente não roda, então o aviso vence o saldo legado
|
|
1067
|
+
const extra = src.via ? C.cyan(src.via) : (legacy && !src.blocking) ? C.cyan(legacy) : aiSourceMissing(src);
|
|
1068
|
+
console.log(' ' + C.dim(T.repl_account(cfg.username || '?', cfg.plan || '?') + ' · ') + extra);
|
|
1069
|
+
}
|
|
990
1070
|
console.log(' ' + C.dim(T.repl_hello) + '\n');
|
|
991
1071
|
|
|
992
1072
|
// Onboarding: só na PRIMEIRA vez — depois a linha de dica do cabeçalho basta
|
|
@@ -1036,6 +1116,7 @@ async function chatRepl() {
|
|
|
1036
1116
|
try {
|
|
1037
1117
|
if (['/nova', '/new'].includes(low)) {
|
|
1038
1118
|
_resetLocalAgentState(agentCwd);
|
|
1119
|
+
_chatLocal().limpar(_convScope('chat'));
|
|
1039
1120
|
await ensureConv(token, 'chat', process.cwd(), true);
|
|
1040
1121
|
console.log(ui.okLine(T.new_conv) + '\n');
|
|
1041
1122
|
} else if (['/ajuda', '/help', '/?'].includes(low)) {
|
|
@@ -1112,11 +1193,13 @@ async function chatRepl() {
|
|
|
1112
1193
|
} else {
|
|
1113
1194
|
const personalIntent = require('../lib/personal-ai-intent').parsePersonalAiIntent(msg);
|
|
1114
1195
|
if (personalIntent) {
|
|
1115
|
-
if (personalIntent.action === 'connect') await personalAiCmd(['conectar', personalIntent.provider]);
|
|
1196
|
+
if (personalIntent.action === 'connect') await personalAiCmd(['conectar', personalIntent.provider], { interativo: false });
|
|
1116
1197
|
else if (personalIntent.action === 'disconnect') await personalAiCmd(['desconectar', personalIntent.provider]);
|
|
1117
1198
|
else if (personalIntent.action === 'use') await personalAiCmd(['usar', personalIntent.mode]);
|
|
1118
1199
|
else if (personalIntent.action === 'status') await personalAiCmd(['status']);
|
|
1119
|
-
else console.log(ui.infoLine(
|
|
1200
|
+
else console.log(ui.infoLine(cfg.lang === 'en'
|
|
1201
|
+
? 'Do not paste login codes or tokens here: the login is done in the official CLI on this PC (`ts ia conectar chatgpt|claude`).'
|
|
1202
|
+
: 'Não cole códigos nem tokens de login aqui: o login é feito na CLI oficial neste PC (`ts ia conectar chatgpt|claude`).'));
|
|
1120
1203
|
continue;
|
|
1121
1204
|
}
|
|
1122
1205
|
// ROTEADOR: linguagem natural pura — o sistema decide o caminho e AVISA
|
|
@@ -1151,7 +1234,8 @@ async function chatRepl() {
|
|
|
1151
1234
|
}
|
|
1152
1235
|
}
|
|
1153
1236
|
} catch (e) {
|
|
1154
|
-
if (e
|
|
1237
|
+
if (_erroIaLocal(e)) console.error(ui.errLine(e.message));
|
|
1238
|
+
else if (e instanceof ApiError && e.code === 'conn') console.error(ui.errLine(T.err_conn));
|
|
1155
1239
|
else if (e instanceof ApiError && e.code === 'auth') { console.error(ui.errLine(T.session_expired)); break; }
|
|
1156
1240
|
else if (e instanceof ApiError && e.code === 'no_credits') console.error(ui.errLine(T.no_credits));
|
|
1157
1241
|
else if (e instanceof ApiError && e.code === 'plan_limit') console.error(ui.errLine(e.message));
|
|
@@ -1396,7 +1480,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1396
1480
|
const token = needToken();
|
|
1397
1481
|
// flags que levam VALOR: o filtro remove só a flag (começa com "-"), deixando o VALOR nos
|
|
1398
1482
|
// words → tira o valor daqui pra ele não virar parte do texto da tarefa.
|
|
1399
|
-
for (const flag of ['--modelo', '--model', '--output-format', '--orcamento-creditos', '--max-creditos', '--tempo-max-seg', '--max-tokens', '--ferramentas', '--tools']) {
|
|
1483
|
+
for (const flag of ['--modelo', '--model', '--output-format', '--orcamento', '--orcamento-creditos', '--max-creditos', '--tempo-max-seg', '--max-tokens', '--ferramentas', '--tools']) {
|
|
1400
1484
|
const _i = process.argv.findIndex(a => a === flag); const _v = _i >= 0 ? process.argv[_i + 1] : null;
|
|
1401
1485
|
if (_v) { const j = words.indexOf(_v); if (j >= 0) words = words.slice(0, j).concat(words.slice(j + 1)); }
|
|
1402
1486
|
}
|
|
@@ -1406,7 +1490,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1406
1490
|
let _inlinePassos, _inlineYolo = false, _inlineMaxCredits, _inlineMaxSeconds, _inlineMaxTokens;
|
|
1407
1491
|
task = task
|
|
1408
1492
|
.replace(/(?:^|\s)--(?:passos|steps|max-passos)[\s=]+(\d{1,3})\b/gi, (_m, n) => { _inlinePassos = Number(n); return ' '; })
|
|
1409
|
-
.replace(/(?:^|\s)--(?:orcamento-creditos|max-creditos)[\s=]+(\d{1,5})\b/gi, (_m, n) => { _inlineMaxCredits = Number(n); return ' '; })
|
|
1493
|
+
.replace(/(?:^|\s)--(?:orcamento-creditos|max-creditos|orcamento)[\s=]+(\d{1,5})\b/gi, (_m, n) => { _inlineMaxCredits = Number(n); return ' '; })
|
|
1410
1494
|
.replace(/(?:^|\s)--tempo-max-seg[\s=]+(\d{1,5})\b/gi, (_m, n) => { _inlineMaxSeconds = Number(n); return ' '; })
|
|
1411
1495
|
.replace(/(?:^|\s)--max-tokens[\s=]+(\d{3,7})\b/gi, (_m, n) => { _inlineMaxTokens = Number(n); return ' '; })
|
|
1412
1496
|
.replace(/(?:^|\s)--(?:yolo|full-auto|auto-tudo)\b/gi, () => { _inlineYolo = true; return ' '; })
|
|
@@ -1447,10 +1531,11 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1447
1531
|
const _p = require('path'), _fs = require('fs'), _os = require('os'), _cr = require('crypto');
|
|
1448
1532
|
const sessFile = _p.join(_os.homedir(), '.ts', 'agente', _cr.createHash('md5').update(process.cwd().toLowerCase()).digest('hex').slice(0, 12) + '.json');
|
|
1449
1533
|
let priorMessages = null;
|
|
1534
|
+
let priorContext = null; // medidor da última chamada da sessão anterior (folha de decisão em 95%)
|
|
1450
1535
|
// cwd da sessão: o do REPL (cwdIn) tem prioridade; senão a pasta de lançamento.
|
|
1451
1536
|
let startCwd = cwdIn || process.cwd();
|
|
1452
1537
|
if (process.argv.includes('--continuar') || process.argv.includes('-c')) {
|
|
1453
|
-
try { const s = JSON.parse(_fs.readFileSync(sessFile, 'utf8')); priorMessages = s.messages; if (!cwdIn && s.cwd) startCwd = s.cwd; if (!streamJson) console.log(' ' + C.dim((cfg.lang === 'en' ? '↻ resuming this folder\'s session (' : '↻ continuando a sessão desta pasta (') + (s.messages ? s.messages.length : 0) + ' msgs)')); }
|
|
1538
|
+
try { const s = JSON.parse(_fs.readFileSync(sessFile, 'utf8')); priorMessages = s.messages; priorContext = s.context || null; if (!cwdIn && s.cwd) startCwd = s.cwd; if (!streamJson) console.log(' ' + C.dim((cfg.lang === 'en' ? '↻ resuming this folder\'s session (' : '↻ continuando a sessão desta pasta (') + (s.messages ? s.messages.length : 0) + ' msgs)')); }
|
|
1454
1539
|
catch (_) { if (!streamJson) console.log(' ' + C.dim(cfg.lang === 'en' ? '(no prior session here — starting fresh)' : '(sem sessão anterior aqui — começando nova)')); }
|
|
1455
1540
|
}
|
|
1456
1541
|
// Retomada por linguagem natural depois de queda/restart. O estado parcial
|
|
@@ -1492,11 +1577,55 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1492
1577
|
// 40min no PW). _hlog escreve SÍNCRONO no fd 1 (fs.writeSync), descarregando cada linha na
|
|
1493
1578
|
// hora. No TTY segue o console.log normal (o spinner cuida do redesenho).
|
|
1494
1579
|
const _hlog = (s) => { if (_noTTY && !streamJson) { try { require('fs').writeSync(1, s + '\n'); return; } catch (_) {} } console.log(s); };
|
|
1580
|
+
// FOLHA DE DECISÃO: a sessão retomada já chegou a 95% do limite útil (ou chega com a
|
|
1581
|
+
// mensagem nova). Com pessoa na frente: Compactar e continuar (padrão) / Começar sessão
|
|
1582
|
+
// nova / Continuar assim. Sem TTY (--yes, --yolo, ACP, stream-json, pipe):
|
|
1583
|
+
// compacta sozinho e avisa. No meio da execução quem cuida é a compactação automática.
|
|
1584
|
+
let compactFirst = false;
|
|
1585
|
+
// "Continuar assim": a execução não compacta pelo limiar (só no limite útil ou num 400 de contexto)
|
|
1586
|
+
let noProactiveCompact = false;
|
|
1587
|
+
// no stream-json o primeiro evento é SEMPRE system(init): o aviso espera e sai logo depois dele
|
|
1588
|
+
let _autoCompactEvent = null;
|
|
1589
|
+
if (Array.isArray(priorMessages) && priorMessages.length && priorContext) {
|
|
1590
|
+
const en = cfg.lang === 'en';
|
|
1591
|
+
// REPL alimentado por pipe também tem askFn, mas a "resposta" seria a próxima linha do
|
|
1592
|
+
// pipe — por isso a pergunta exige terminal de verdade nos dois lados.
|
|
1593
|
+
const interactive = !streamJson && !JSON_OUT && !YES && !YOLO && !_inlineYolo && !autoAllIn
|
|
1594
|
+
&& !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
1595
|
+
const dec = core.resumeContextDecision(priorContext, { extraTok: Math.ceil(String(task || '').length / 4), interactive });
|
|
1596
|
+
const meterTxt = core.formatContextMeter(priorContext, cfg.lang);
|
|
1597
|
+
if (dec.action === 'ask') {
|
|
1598
|
+
console.log('\n ' + C.warn('▲ ') + C.bold(en ? `This session is at ${dec.pct}% of the usable context` : `A sessão está em ${dec.pct}% do contexto útil`) + C.dim(' · ' + meterTxt));
|
|
1599
|
+
console.log(' ' + C.cyan('1') + C.dim(en ? ' Compact and continue (default) — older history becomes a summary' : ' Compactar e continuar (padrão) — o histórico antigo vira um resumo'));
|
|
1600
|
+
console.log(' ' + C.cyan('2') + C.dim(en ? ' Start a new session — the previous one is archived' : ' Começar sessão nova — a anterior fica arquivada'));
|
|
1601
|
+
console.log(' ' + C.cyan('3') + C.dim(en ? ' Continue as is — the model may lose the start of the conversation' : ' Continuar assim — o modelo pode perder o começo da conversa'));
|
|
1602
|
+
const choice = String(await ask(en ? 'choice (1/2/3) [1]: ' : 'escolha (1/2/3) [1]: ') || '').trim();
|
|
1603
|
+
if (choice.startsWith('2')) {
|
|
1604
|
+
const archived = sessFile.replace(/\.json$/, '') + '.arquivada-' + new Date().toISOString().replace(/[:.]/g, '-') + '.json';
|
|
1605
|
+
try { _fs.renameSync(sessFile, archived); } catch (_) {}
|
|
1606
|
+
priorMessages = null;
|
|
1607
|
+
console.log(' ' + C.dim(en ? '→ new session; previous one archived at ' : '→ sessão nova; a anterior foi arquivada em ') + C.dim(archived) + '\n');
|
|
1608
|
+
} else if (choice.startsWith('3')) {
|
|
1609
|
+
noProactiveCompact = true;
|
|
1610
|
+
console.log(' ' + C.dim(en ? '→ continuing without compacting' : '→ seguindo sem compactar') + '\n');
|
|
1611
|
+
} else {
|
|
1612
|
+
compactFirst = true;
|
|
1613
|
+
console.log(' ' + C.dim(en ? '→ compacting before the first step' : '→ compactando antes do primeiro passo') + '\n');
|
|
1614
|
+
}
|
|
1615
|
+
} else if (dec.action === 'auto') {
|
|
1616
|
+
compactFirst = true;
|
|
1617
|
+
const note = en
|
|
1618
|
+
? `resumed session at ${dec.pct}% of the usable context — compacting automatically before continuing (no interactive terminal)`
|
|
1619
|
+
: `sessão retomada em ${dec.pct}% do contexto útil — compactando sozinho antes de continuar (sem terminal interativo)`;
|
|
1620
|
+
if (streamJson) _autoCompactEvent = core.AgentEvents.tool({ subtype: 'context_auto_compact', tool: 'compactar_contexto', detail: note });
|
|
1621
|
+
else if (!JSON_OUT) _hlog(' ' + C.warn('▲ ' + note));
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1495
1624
|
const sp = (streamJson || _noTTY) ? { text() {}, start() {}, stop() {} } : ui.spinner(T.agent_thinking).start();
|
|
1496
1625
|
if (_noTTY && !streamJson) _hlog(' ' + C.dim(T.agent_thinking));
|
|
1497
1626
|
const t0 = Date.now();
|
|
1498
1627
|
// STATUS LINE ao vivo (estilo Claude/Verboo): tempo · modelo · tokens · passo — atualiza a cada 1s.
|
|
1499
|
-
const _live = { steps: 0, inTok: 0, outTok: 0, model: model || 'auto' };
|
|
1628
|
+
const _live = { steps: 0, inTok: 0, outTok: 0, model: model || 'auto', compacting: false };
|
|
1500
1629
|
const _fmtTime = (ms) => { const s = Math.floor(ms / 1000), m = Math.floor(s / 60); return m > 0 ? `${m}m${String(s % 60).padStart(2, '0')}s` : `${s}s`; };
|
|
1501
1630
|
const _fmtTok = (n) => n >= 1000 ? (n / 1000).toFixed(1).replace('.0', '') + 'k' : String(n);
|
|
1502
1631
|
const _statusLine = () => {
|
|
@@ -1504,11 +1633,15 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1504
1633
|
const parts = [_fmtTime(Date.now() - t0), String(_live.model).slice(0, 22)];
|
|
1505
1634
|
if (tok > 0) parts.push('↓ ' + _fmtTok(tok) + ' tok');
|
|
1506
1635
|
if (_live.steps > 0) parts.push('passo ' + _live.steps);
|
|
1507
|
-
|
|
1636
|
+
const head = _live.compacting ? (cfg.lang === 'en' ? 'summarizing history' : 'resumindo o histórico') : T.agent_thinking;
|
|
1637
|
+
return head + ' · ' + parts.join(' · '); // texto puro; o spinner aplica a cor
|
|
1508
1638
|
};
|
|
1509
1639
|
const _liveTimer = (!_noTTY && !streamJson) ? setInterval(() => sp.text(_statusLine()), 1000) : null;
|
|
1510
|
-
if (streamJson)
|
|
1511
|
-
|
|
1640
|
+
if (streamJson) {
|
|
1641
|
+
_emit(core.AgentEvents.systemInit({ model: model || 'auto', cwd: startCwd, mode: plan ? 'plan' : readOnly ? 'ask' : 'agent' }));
|
|
1642
|
+
if (_autoCompactEvent) _emit(_autoCompactEvent);
|
|
1643
|
+
}
|
|
1644
|
+
// Mesmo agente de projeto e mesmo fluxo usados na Web e no App.
|
|
1512
1645
|
// É best-effort para o CLI continuar funcional durante indisponibilidade do backend.
|
|
1513
1646
|
let sharedChannelPrompt = '';
|
|
1514
1647
|
let sharedConversationId = null;
|
|
@@ -1528,34 +1661,29 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1528
1661
|
}
|
|
1529
1662
|
let personalSelectionRequired = false;
|
|
1530
1663
|
try {
|
|
1531
|
-
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1534
|
-
|
|
1535
|
-
const
|
|
1664
|
+
// O modo da conta é LOCAL (`ts ia usar`) e a IA roda na CLI oficial logada neste PC
|
|
1665
|
+
// (lib/ia-local). Uma escolha explícita nunca cai em silêncio para os créditos TS: se a CLI
|
|
1666
|
+
// exigida não está pronta, a execução é cancelada com o motivo.
|
|
1667
|
+
const iaLocal = require('../lib/ia-local');
|
|
1668
|
+
const modoLocal = _modoIaLocal();
|
|
1669
|
+
const selectedPersonal = modoLocal !== 'automatico';
|
|
1536
1670
|
personalSelectionRequired = selectedPersonal;
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
personalAgentMode = personalState.mode;
|
|
1546
|
-
personalAgentLlm = require('../lib/personal-agent-llm').createPersonalAgentLlm({
|
|
1547
|
-
api, token, mode:personalAgentMode, conversationId:sharedConversationId,
|
|
1548
|
-
agentId:shared?.agent?.id || null,
|
|
1549
|
-
});
|
|
1550
|
-
if (!streamJson) _hlog(' ' + C.dim(`IA pessoal como executora local: ${personalAgentMode === 'chatgpt' ? 'ChatGPT/Codex' : personalAgentMode === 'claude' ? 'Claude' : 'ChatGPT/Codex + Claude'} · 0 créditos TS`));
|
|
1671
|
+
if (selectedPersonal) {
|
|
1672
|
+
const personalState = await iaLocal.status();
|
|
1673
|
+
if (!iaLocal.pronto(modoLocal, personalState)) {
|
|
1674
|
+
throw new Error(`modo ${modoLocal} selecionado, mas ${iaLocal.faltando(modoLocal, personalState)} — veja \`ts ia status\``);
|
|
1675
|
+
}
|
|
1676
|
+
personalAgentMode = modoLocal;
|
|
1677
|
+
personalAgentLlm = require('../lib/personal-agent-llm').createPersonalAgentLlm({ mode:personalAgentMode });
|
|
1678
|
+
if (!streamJson) _hlog(' ' + C.dim(`IA da sua conta como executora local: ${_personalLabel(personalAgentMode)} neste PC · ${T.via_subscription}`));
|
|
1551
1679
|
}
|
|
1552
1680
|
} catch (e) {
|
|
1553
|
-
if (personalSelectionRequired
|
|
1554
|
-
if (!streamJson) _hlog(' ' + C.warn(`▲ IA pessoal indisponível (${String(e.message || e).slice(0,
|
|
1681
|
+
if (personalSelectionRequired) {
|
|
1682
|
+
if (!streamJson) _hlog(' ' + C.warn(`▲ IA pessoal indisponível (${String(e.message || e).slice(0, 160)}). Execução cancelada para não consumir créditos TS sem aviso.`));
|
|
1555
1683
|
durableOps.interrupt(startCwd, 'personal_ai_unavailable');
|
|
1556
1684
|
return;
|
|
1557
1685
|
}
|
|
1558
|
-
if (!streamJson) _hlog(' ' + C.warn('▲ não foi possível confirmar a IA da conta;
|
|
1686
|
+
if (!streamJson) _hlog(' ' + C.warn('▲ não foi possível confirmar a IA da conta; seguindo com a sua chave, se houver, ou com a IA do Terminal Smart (créditos legados) nesta execução.'));
|
|
1559
1687
|
}
|
|
1560
1688
|
let out;
|
|
1561
1689
|
const durableCwd = _wt ? _wt.base : startCwd;
|
|
@@ -1565,7 +1693,9 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1565
1693
|
// Pra missões grandes ("faça tudo de uma vez") sem o pára-e-continua.
|
|
1566
1694
|
const _pi = process.argv.findIndex(a => a === '--passos' || a === '--steps' || a === '--max-passos');
|
|
1567
1695
|
const _maxPassos = _pi >= 0 ? Number(process.argv[_pi + 1]) : (_inlinePassos ?? maxIterIn ?? undefined);
|
|
1568
|
-
|
|
1696
|
+
// --orcamento N (apelidos antigos: --orcamento-creditos / --max-creditos). Só limita
|
|
1697
|
+
// a IA do Terminal Smart (créditos legados); com chave/assinatura própria não há cobrança do TS.
|
|
1698
|
+
const _ci = process.argv.findIndex(a => a === '--orcamento' || a === '--orcamento-creditos' || a === '--max-creditos');
|
|
1569
1699
|
const _ti = process.argv.findIndex(a => a === '--tempo-max-seg');
|
|
1570
1700
|
const _tki = process.argv.findIndex(a => a === '--max-tokens');
|
|
1571
1701
|
const _maxCredits = _ci >= 0 ? Number(process.argv[_ci + 1]) : _inlineMaxCredits;
|
|
@@ -1578,9 +1708,11 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1578
1708
|
keyOverride:personalAgentLlm ? { key:'personal-subscription', baseUrl:'personal://subscription', source:'personal', billingAuthoritative:true } : undefined,
|
|
1579
1709
|
personalSubscription:!!personalAgentLlm,
|
|
1580
1710
|
maxCredits: _maxCredits, maxDurationMs: _maxDurationMs || undefined, maxTokens: _maxTokens,
|
|
1711
|
+
compactFirst, noProactiveCompact,
|
|
1581
1712
|
onThinking: (p) => {
|
|
1582
1713
|
if (p && typeof p === 'object') {
|
|
1583
1714
|
_live.steps = p.steps ?? _live.steps; _live.inTok = p.inTok ?? _live.inTok; _live.outTok = p.outTok ?? _live.outTok;
|
|
1715
|
+
_live.compacting = !!p.compacting;
|
|
1584
1716
|
if (p.model && p.model !== 'smart') _live.model = p.model;
|
|
1585
1717
|
}
|
|
1586
1718
|
sp.text(_statusLine());
|
|
@@ -1590,6 +1722,8 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1590
1722
|
if (name && !blocked && !durableInternalStep) durableOps.toolStarted(durableCwd, name);
|
|
1591
1723
|
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
|
|
1592
1724
|
sp.stop();
|
|
1725
|
+
const _compactFail = compactFailLine(name, blocked, detail);
|
|
1726
|
+
if (_compactFail) { _hlog(' ' + _compactFail); sp.start(); return; }
|
|
1593
1727
|
const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('›');
|
|
1594
1728
|
// nome AMIGÁVEL (Rodando no servidor…) em vez do técnico (executar_remoto); detalhe sem cortar palavra
|
|
1595
1729
|
const _label = ui.friendlyTool(name, cfg.lang === 'en');
|
|
@@ -1610,18 +1744,14 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1610
1744
|
sp.start();
|
|
1611
1745
|
return r;
|
|
1612
1746
|
},
|
|
1613
|
-
onRemote: ({ ttl }) => {
|
|
1614
|
-
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
|
|
1615
|
-
sp.stop();
|
|
1616
|
-
_hlog(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120)));
|
|
1617
|
-
sp.start();
|
|
1618
|
-
},
|
|
1619
1747
|
});
|
|
1620
1748
|
} catch (e) {
|
|
1621
1749
|
durableOps.interrupt(durableCwd, e && (e.code || e.message) || 'agent_error');
|
|
1622
1750
|
if (_liveTimer) clearInterval(_liveTimer);
|
|
1623
1751
|
sp.stop();
|
|
1624
|
-
|
|
1752
|
+
// saldo/limite do PROVEDOR da chave própria (BYOK) não é falta da IA do TS; no modo
|
|
1753
|
+
// assinatura a execução não passou pela chave, então a dica não vale
|
|
1754
|
+
const hint = providerCapacityHint(e) || (!personalAgentLlm ? byokNoBalanceHint(e) : '');
|
|
1625
1755
|
if (hint) {
|
|
1626
1756
|
const err = new ApiError(hint, { status: Number(e && e.status) || 402, code: 'byok_provider_capacity' });
|
|
1627
1757
|
err.cause = e;
|
|
@@ -1638,7 +1768,12 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1638
1768
|
// senão --continuar retomaria num diretório que o `ts worktrees clean` já apagou).
|
|
1639
1769
|
if (onCwd && !_wt && effCwd !== startCwd) onCwd(effCwd);
|
|
1640
1770
|
const persistCwd = _wt ? _wt.base : effCwd;
|
|
1641
|
-
|
|
1771
|
+
// corte seguro: preserva o resumo fixo da compactação e não começa numa resposta 'tool' órfã;
|
|
1772
|
+
// context = medidor da última chamada, lido pela folha de decisão do próximo --continuar.
|
|
1773
|
+
// Retorno sem histórico não apaga a sessão salva (o --continuar perderia tudo, até o resumo).
|
|
1774
|
+
if (Array.isArray(out.messages) && out.messages.length) {
|
|
1775
|
+
try { _fs.mkdirSync(_p.dirname(sessFile), { recursive: true }); _fs.writeFileSync(sessFile, JSON.stringify({ cwd: persistCwd, at: new Date().toISOString(), context: out.context || null, messages: core.safeTail(out.messages, 60) })); } catch (_) {}
|
|
1776
|
+
}
|
|
1642
1777
|
const secs = _fmtTime(Date.now() - t0); // tempo total já em min+seg (ex: 12m44s), não segundos crus
|
|
1643
1778
|
// WORKTREE: commita o que mudou e prepara o resumo (merge/descarte).
|
|
1644
1779
|
const _wtInfo = _wt ? _worktreeFinish(_wt) : null;
|
|
@@ -1657,10 +1792,10 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1657
1792
|
// HEADLESS stream-json: fecha com assistant (texto) + result (métricas) e sai.
|
|
1658
1793
|
if (streamJson) {
|
|
1659
1794
|
_emit(core.AgentEvents.assistant(out.text || ''));
|
|
1660
|
-
_emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, guard: out.guard || null, observability: out.observability || null, duration_ms: Date.now() - t0, worktree: _wt ? { branch: _wt.branch, changed: (_wtInfo && _wtInfo.changed) || 0 } : null }));
|
|
1795
|
+
_emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, compaction: out.compaction || null, needHuman: out.needHuman || null, guard: out.guard || null, observability: out.observability || null, duration_ms: Date.now() - t0, worktree: _wt ? { branch: _wt.branch, changed: (_wtInfo && _wtInfo.changed) || 0 } : null }));
|
|
1661
1796
|
return;
|
|
1662
1797
|
}
|
|
1663
|
-
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; }
|
|
1798
|
+
if (JSON_OUT) { console.log(JSON.stringify({ ok: true, result: out.text, steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context, compaction: out.compaction || null, needHuman: out.needHuman, guard: out.guard || null })); return; }
|
|
1664
1799
|
// BLOQUEIO HUMANO: o agente pediu uma ação que só o usuário faz — mostra o pedido
|
|
1665
1800
|
// (antes o retorno vinha com texto vazio e o usuário via só um cabeçalho em branco).
|
|
1666
1801
|
if (out.needHuman) {
|
|
@@ -1672,20 +1807,20 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1672
1807
|
return;
|
|
1673
1808
|
}
|
|
1674
1809
|
printAnswer(out.text);
|
|
1675
|
-
// medidor de contexto: ocupação
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
// rodapé enriquecido: passos · créditos · tempo · MODELO · tokens (· contexto)
|
|
1810
|
+
// medidor de contexto: ocupação da janela na ÚLTIMA chamada (não a soma do turno), sobre o
|
|
1811
|
+
// limite útil, com o selo medido/≈estimado; amarelo a partir de 80% (perto da folha de 95%).
|
|
1812
|
+
const _cxMeter = core.contextMeter(out.context);
|
|
1813
|
+
const _cxTxt = core.formatContextMeter(out.context, cfg.lang);
|
|
1814
|
+
const _cx = _cxTxt ? C.dim(' · ') + (_cxMeter.pct >= 80 ? C.warn(_cxTxt) : C.dim(_cxTxt)) : '';
|
|
1815
|
+
// rodapé enriquecido: passos · via (ou créditos legados) · tempo · MODELO · tokens (· contexto)
|
|
1682
1816
|
const _tokTot = (out.tokens && ((out.tokens.inTok || 0) + (out.tokens.outTok || 0))) || 0;
|
|
1683
1817
|
const _extra = (out.model ? ' · ' + String(out.model).slice(0, 22) : '') + (_tokTot ? ' · ↓ ' + _fmtTok(_tokTot) + ' tok' : '');
|
|
1684
|
-
// Com
|
|
1685
|
-
//
|
|
1818
|
+
// Com assinatura ou chave própria nada é cobrado pelo TS — mostrar "créditos" aqui seria
|
|
1819
|
+
// enganoso. Diz por onde a IA rodou; créditos só no caminho legado da IA do Terminal Smart.
|
|
1686
1820
|
const _byok = require('../lib/keyring').ativo();
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1821
|
+
const _via = personalAgentLlm ? T.via_subscription : _byok ? (_byok.id === 'ollama' ? T.via_ollama : T.via_key(_byok.id)) : null;
|
|
1822
|
+
const _base = _via ? T.agent_footer_via(out.steps, _via, secs) : T.agent_footer(out.steps, out.credits, secs);
|
|
1823
|
+
console.log(' ' + C.dim(_base) + C.dim(_extra) + _cx + C.dim(' · ts agente --continuar "..." pra seguir') + '\n');
|
|
1689
1824
|
_printWt();
|
|
1690
1825
|
}
|
|
1691
1826
|
|
|
@@ -1774,7 +1909,7 @@ async function evalCmd(words) {
|
|
|
1774
1909
|
// no teto padrão de uma missão só porque um provedor demorou a encerrar.
|
|
1775
1910
|
const _evalSteps = Number(_val(['--passos', '--steps']));
|
|
1776
1911
|
const _evalSeconds = Number(_val(['--tempo-max-seg', '--max-seconds']));
|
|
1777
|
-
const _evalCredits = Number(_val(['--orcamento-creditos', '--max-creditos']));
|
|
1912
|
+
const _evalCredits = Number(_val(['--orcamento', '--orcamento-creditos', '--max-creditos']));
|
|
1778
1913
|
const evalLimits = {
|
|
1779
1914
|
maxIter: Number.isFinite(_evalSteps) && _evalSteps > 0 ? Math.min(60, Math.floor(_evalSteps)) : undefined,
|
|
1780
1915
|
maxDurationMs: Number.isFinite(_evalSeconds) && _evalSeconds > 0 ? Math.min(3600, _evalSeconds) * 1000 : undefined,
|
|
@@ -1813,7 +1948,7 @@ async function evalCmd(words) {
|
|
|
1813
1948
|
if (models.length < 2) { _die(en ? '--modelos needs at least 2 models (comma-separated)' : '--modelos precisa de pelo menos 2 modelos (separados por vírgula)', 2); }
|
|
1814
1949
|
if (!JSON_OUT) {
|
|
1815
1950
|
console.log('\n ' + ui.gradient('⌁ ' + (en ? 'Model Lab' : 'Model Lab')) + ' ' + C.bold(suite.name) + C.dim(' · ' + suite.cases.length + (en ? ' case(s) × ' : ' caso(s) × ') + models.length + (en ? ' models' : ' modelos')));
|
|
1816
|
-
console.log(' ' + C.dim(en ? 'same suite on each executor; a fixed judge scores all (fair).
|
|
1951
|
+
console.log(' ' + C.dim(en ? 'same suite on each executor; a fixed judge scores all (fair). Runs via your key or local Ollama (ts conectar); without one, on TS legacy credits.' : 'mesma suíte em cada executor; um juiz fixo pontua todos (justo). Roda via sua chave ou Ollama local (ts conectar); sem elas, nos créditos legados do TS.') + '\n');
|
|
1817
1952
|
}
|
|
1818
1953
|
const spM = JSON_OUT ? { start() {}, stop() {}, text() {} } : ui.spinner('…').start();
|
|
1819
1954
|
let mx;
|
|
@@ -1823,7 +1958,7 @@ async function evalCmd(words) {
|
|
|
1823
1958
|
onModel: ({ model, phase, row }) => { if (JSON_OUT) return; if (phase === 'start') spM.text((en ? 'testing ' : 'testando ') + model + '…'); else if (phase === 'done') { spM.stop(); console.log(' ' + C.cyan('◆') + ' ' + C.bold(model) + C.dim(' ' + row.passed + '/' + row.total + ' · score ' + row.avgScore + ' · ' + row.credits + ' cr') + (row.error ? C.err(' ' + row.error) : '')); spM.start(); } },
|
|
1824
1959
|
onCase: ({ phase, id, pass, score }) => { if (!JSON_OUT && phase === 'done') { spM.stop(); console.log(' ' + (pass ? C.ok('✔') : C.err('✗')) + C.dim(' ' + id + ' ' + score)); spM.start(); } },
|
|
1825
1960
|
});
|
|
1826
|
-
} catch (e) { spM.stop(); if (e && (e.code === 'no_credits' || e.status === 402)) { console.error('\n' + ui.errLine(
|
|
1961
|
+
} catch (e) { spM.stop(); if (e && (e.code === 'no_credits' || e.status === 402)) { console.error('\n' + ui.errLine(byokNoBalanceHint(e) || T.no_credits)); process.exit(1); } throw e; }
|
|
1827
1962
|
spM.stop();
|
|
1828
1963
|
if (JSON_OUT) { console.log(JSON.stringify(mx)); return; }
|
|
1829
1964
|
// placar ordenado (qualidade → custo)
|
|
@@ -1839,7 +1974,7 @@ async function evalCmd(words) {
|
|
|
1839
1974
|
|
|
1840
1975
|
if (!JSON_OUT) {
|
|
1841
1976
|
console.log('\n ' + ui.gradient('⌁ ' + (en ? 'Eval' : 'Avaliação')) + ' ' + C.bold(suite.name) + C.dim(' · ' + suite.cases.length + (en ? ' case(s)' : ' caso(s)')));
|
|
1842
|
-
console.log(' ' + C.dim(en ? 'runs the real agent per case (
|
|
1977
|
+
console.log(' ' + C.dim(en ? 'runs the real agent per case (via your key or local Ollama; without one, TS legacy credits), an AI judge scores each' : 'roda o agente de verdade por caso (via sua chave ou Ollama local; sem elas, créditos legados do TS); um juiz de IA pontua cada um') + '\n');
|
|
1843
1978
|
}
|
|
1844
1979
|
const sp = JSON_OUT ? { start() {}, stop() {}, text() {} } : ui.spinner('…').start();
|
|
1845
1980
|
let res;
|
|
@@ -1859,8 +1994,8 @@ async function evalCmd(words) {
|
|
|
1859
1994
|
});
|
|
1860
1995
|
} catch (e) {
|
|
1861
1996
|
sp.stop();
|
|
1862
|
-
//
|
|
1863
|
-
if (e && (e.code === 'no_credits' || e.status === 402)) { console.error('\n' + ui.errLine(
|
|
1997
|
+
// sem IA do TS disponível → explica como usar a IA própria, nunca "HTTP 402" cru.
|
|
1998
|
+
if (e && (e.code === 'no_credits' || e.status === 402)) { console.error('\n' + ui.errLine(byokNoBalanceHint(e) || T.no_credits)); process.exit(1); }
|
|
1864
1999
|
throw e;
|
|
1865
2000
|
}
|
|
1866
2001
|
sp.stop();
|
|
@@ -2087,7 +2222,7 @@ async function metaCmd() {
|
|
|
2087
2222
|
if (goal && st0 && st0.status !== 'done') { console.error(ui.infoLine(T.meta_exists(st0.goal))); process.exit(2); }
|
|
2088
2223
|
if (!goal && (!st0 || st0.status === 'done')) { console.error(ui.infoLine(T.meta_need)); process.exit(2); }
|
|
2089
2224
|
|
|
2090
|
-
const yes = YES || !process.stdin.isTTY; // noturno/cron = autônomo (
|
|
2225
|
+
const yes = YES || !process.stdin.isTTY; // noturno/cron = autônomo (o perigoso é recusado sem alguém no terminal)
|
|
2091
2226
|
console.log(ui.banner(pkg.version, T.tagline));
|
|
2092
2227
|
if (st0 && st0.status !== 'done') {
|
|
2093
2228
|
const d = st0.checklist.filter(i => i.passes).length;
|
|
@@ -2127,7 +2262,7 @@ async function metaCmd() {
|
|
|
2127
2262
|
const rest = String(text).split('\n').slice(1).filter(Boolean);
|
|
2128
2263
|
for (const l of rest) console.log(' ' + C.dim(l));
|
|
2129
2264
|
console.log('');
|
|
2130
|
-
metaMod.notify(token, text).catch(() => {}); //
|
|
2265
|
+
metaMod.notify(token, text).catch(() => {}); // aviso fora do terminal: virá pelo hub (hoje não envia nada)
|
|
2131
2266
|
sp.start();
|
|
2132
2267
|
},
|
|
2133
2268
|
onChecklist: (itens) => { sp.stop(); console.log(_metaChecklistBox(itens) + '\n'); sp.start(); },
|
|
@@ -2135,12 +2270,13 @@ async function metaCmd() {
|
|
|
2135
2270
|
onThinking: () => sp.text(T.agent_thinking),
|
|
2136
2271
|
onStep: ({ name, detail, blocked, loop, retry, auto }) => {
|
|
2137
2272
|
sp.stop();
|
|
2138
|
-
|
|
2273
|
+
const _compactFail = compactFailLine(name, blocked, detail);
|
|
2274
|
+
if (_compactFail) console.log(' ' + _compactFail);
|
|
2275
|
+
else 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) : ''));
|
|
2139
2276
|
sp.start();
|
|
2140
2277
|
},
|
|
2141
2278
|
onStepDone: (e) => { sp.stop(); console.log(stepDoneLine(e, ' ')); sp.start(); },
|
|
2142
2279
|
askApprove: async (payload) => { sp.stop(); const r = await askApproveRoute(payload); sp.start(); return r; },
|
|
2143
|
-
onRemote: ({ ttl }) => { sp.stop(); console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120))); sp.start(); },
|
|
2144
2280
|
onRoundDone: ({ checklist, spent }) => {
|
|
2145
2281
|
sp.stop();
|
|
2146
2282
|
const d = checklist.filter(i => i.passes).length;
|
|
@@ -2238,7 +2374,6 @@ async function metaCmd() {
|
|
|
2238
2374
|
for (const b of blocked) console.log(' ' + C.err('!') + ' ' + b.desc.slice(0, 80));
|
|
2239
2375
|
}
|
|
2240
2376
|
console.log('');
|
|
2241
|
-
await new Promise(r => setTimeout(r, 1200)); // dá tempo do notify sair antes do exit
|
|
2242
2377
|
if (st.status !== 'done') process.exit(3);
|
|
2243
2378
|
}
|
|
2244
2379
|
|
|
@@ -2434,8 +2569,8 @@ async function sentinelaCmd(args) {
|
|
|
2434
2569
|
}
|
|
2435
2570
|
if (token) { try { await metaMod.notify(token, msg); } catch (_) {} }
|
|
2436
2571
|
if (!quiet) console.log(msg);
|
|
2437
|
-
// ── AUTOFIX (Incident Autopilot): conserto verificado + comando seguro → aprovação no
|
|
2438
|
-
// NUNCA aplica sem OK humano
|
|
2572
|
+
// ── AUTOFIX (Incident Autopilot): conserto verificado + comando seguro → aprovação no terminal → aplica → re-valida.
|
|
2573
|
+
// NUNCA aplica sem OK humano. Só quando o diagnóstico sobreviveu à refutação.
|
|
2439
2574
|
if (!r.verdict.ok && check.autofix && token && diagResult && diagResult.status === 'solved' && diagResult.verified && diagResult.fixCommand) {
|
|
2440
2575
|
const applied = await _sentinelaAutofix(check, diagResult, r, token, quiet, en);
|
|
2441
2576
|
if (applied != null) alerts++;
|
|
@@ -2492,33 +2627,36 @@ async function sentinelaCmd(args) {
|
|
|
2492
2627
|
}
|
|
2493
2628
|
function execSyncQuiet(c) { return require('child_process').execSync(c, { stdio: 'ignore', windowsHide: true }); }
|
|
2494
2629
|
|
|
2495
|
-
// AUTOFIX da sentinela (Incident Autopilot): propõe o conserto
|
|
2496
|
-
//
|
|
2497
|
-
// true/false se aplicou (consertou ou não), null se não houve aprovação
|
|
2630
|
+
// AUTOFIX da sentinela (Incident Autopilot): propõe o conserto e SÓ aplica com aprovação humana
|
|
2631
|
+
// LOCAL, no terminal. Aplica no alvo (local ou ssh remoto), re-valida o check e reporta. Retorna
|
|
2632
|
+
// true/false se aplicou (consertou ou não), null se não houve aprovação.
|
|
2498
2633
|
async function _sentinelaAutofix(check, diag, r, token, quiet, en) {
|
|
2499
2634
|
const sen = require('../lib/sentinela');
|
|
2500
2635
|
const metaMod = require('../lib/meta');
|
|
2501
2636
|
const cmd = String(diag.fixCommand).trim();
|
|
2502
|
-
|
|
2503
|
-
//
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2637
|
+
// o comando MOSTRADO (na aprovação e nos avisos) vai MASCARADO (`mysql -pSENHA`, `sshpass -p`,
|
|
2638
|
+
// PGPASSWORD=…); o comando original só roda aqui. A máscara nunca esconde pedaço que o shell
|
|
2639
|
+
// executa; se precisaria esconder, não há aprovação possível — quem aprova não veria o que
|
|
2640
|
+
// roda. Os segredos do próprio ts (token de sessão, chaves, senha SSH, auth de MCP) são
|
|
2641
|
+
// mascarados também pelo VALOR.
|
|
2642
|
+
const shown = require('../lib/core').redactForApproval(cmd, { known: require('../lib/tools').knownSecretsFor([token]) });
|
|
2643
|
+
if (shown.hidden) {
|
|
2644
|
+
if (!quiet) console.log(' ' + C.dim(en ? 'autofix: the command carries a secret that cannot be masked without hiding part of what runs — proposed only.' : 'autofix: o comando traz um segredo que não dá para mascarar sem esconder parte do que roda — só proposto.'));
|
|
2508
2645
|
return null;
|
|
2509
2646
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
if (p && (p.status === 'denied' || p.status === 'expired')) break;
|
|
2647
|
+
const cmdSafe = shown.text;
|
|
2648
|
+
const alvo = check.target ? check.target.split(' ').pop() : os.hostname();
|
|
2649
|
+
// 1) aprovação LOCAL: só com alguém no terminal. Pelo agendador (--quiet / sem TTY) o conserto
|
|
2650
|
+
// fica só proposto — o aviso pelo celular virá pelo hub.
|
|
2651
|
+
if (quiet || !process.stdin.isTTY) {
|
|
2652
|
+
try { await metaMod.notify(token, `🔧 [sentinela] ${check.nome}@${alvo}: conserto proposto e NÃO aplicado (sem aprovação no terminal). Comando proposto: ${cmdSafe}`); } catch (_) {}
|
|
2653
|
+
if (!quiet) console.log(' ' + C.dim(en ? 'autofix: nobody at the terminal to approve — proposed only.' : 'autofix: ninguém no terminal para aprovar — só proposto.'));
|
|
2654
|
+
return null;
|
|
2519
2655
|
}
|
|
2656
|
+
const dec = await askDestructive(cmdSafe, { en, warning: (en ? 'sentinel autofix on ' : 'conserto da sentinela em ') + `${check.nome}@${alvo}` });
|
|
2657
|
+
const approved = dec === true || dec === 'all';
|
|
2520
2658
|
if (!approved) {
|
|
2521
|
-
try { await metaMod.notify(token, `⏹️ [sentinela] ${check.nome}: conserto NÃO aplicado (não aprovado). Comando proposto: ${
|
|
2659
|
+
try { await metaMod.notify(token, `⏹️ [sentinela] ${check.nome}: conserto NÃO aplicado (não aprovado). Comando proposto: ${cmdSafe}`); } catch (_) {}
|
|
2522
2660
|
if (!quiet) console.log(' ' + C.dim(en ? 'autofix: not approved — nothing applied.' : 'autofix: não aprovado — nada aplicado.'));
|
|
2523
2661
|
return null;
|
|
2524
2662
|
}
|
|
@@ -2528,8 +2666,8 @@ async function _sentinelaAutofix(check, diag, r, token, quiet, en) {
|
|
|
2528
2666
|
const fixedOk = rr.verdict.ok;
|
|
2529
2667
|
const rerun = ((rr.res.stdout || '') + (rr.res.stderr || '')).trim().split('\n').slice(0, 4).join('\n').slice(0, 300);
|
|
2530
2668
|
const finalMsg = fixedOk
|
|
2531
|
-
? `✅ [sentinela] ${check.nome}: CONSERTADO por autofix. Apliquei \`${
|
|
2532
|
-
: `⚠️ [sentinela] ${check.nome}: autofix aplicado (\`${
|
|
2669
|
+
? `✅ [sentinela] ${check.nome}: CONSERTADO por autofix. Apliquei \`${cmdSafe}\` e o check voltou a passar.`
|
|
2670
|
+
: `⚠️ [sentinela] ${check.nome}: autofix aplicado (\`${cmdSafe}\`) mas o check AINDA falha (${rr.verdict.motivo}). Precisa de você.\n${require('../lib/tools').maskKnownSecrets(require('../lib/core').redactSecrets(rerun), [token])}`;
|
|
2533
2671
|
try { await metaMod.notify(token, finalMsg); } catch (_) {}
|
|
2534
2672
|
if (!quiet) console.log(' ' + (fixedOk ? C.ok('✔ ') + (en ? 'autofix worked — check passes again' : 'autofix funcionou — check voltou a passar') : C.err('✗ ') + (en ? 'applied but still failing' : 'aplicado mas ainda falha')) + C.dim((ap.code !== 0 ? ' (fix exit ' + ap.code + ')' : '')));
|
|
2535
2673
|
return fixedOk;
|
|
@@ -3021,7 +3159,7 @@ async function checkpointsCmd(words) {
|
|
|
3021
3159
|
console.log('\n' + ui.box(linhas, { title: 'ts checkpoints' }));
|
|
3022
3160
|
}
|
|
3023
3161
|
|
|
3024
|
-
// ── ts conectar — BYOK:
|
|
3162
|
+
// ── ts conectar — BYOK: o agente roda via SUA chave de IA (ou via Ollama local) ──
|
|
3025
3163
|
// O agente já fala OpenAI-compat, então qualquer provedor desses entra sem mexer no loop.
|
|
3026
3164
|
// O cadastro TESTA de verdade: a chave responde? e o modelo CHAMA FERRAMENTA? A segunda
|
|
3027
3165
|
// pergunta é a que importa — modelo que não faz tool-call vira um chat que narra sucesso.
|
|
@@ -3039,7 +3177,7 @@ async function conectarCmd(words) {
|
|
|
3039
3177
|
return a
|
|
3040
3178
|
? C.ok('● ') + (en ? 'using YOUR key: ' : 'usando SUA chave: ') + C.bold((providers.info(a.id) || {}).nome || a.id) +
|
|
3041
3179
|
C.dim(' ' + (a.modelo || '') + (a.toolsOk ? '' : (en ? ' (tools: unreliable)' : ' (ferramentas: não confiável)')))
|
|
3042
|
-
: C.cyan('● ') + (en ? '
|
|
3180
|
+
: C.cyan('● ') + (en ? 'no key of your own — account AI (your subscription, if connected, or TS legacy credits)' : 'sem chave própria — IA da conta (sua assinatura, se conectada, ou créditos legados do TS)');
|
|
3043
3181
|
};
|
|
3044
3182
|
|
|
3045
3183
|
// ts conectar listar
|
|
@@ -3052,11 +3190,11 @@ async function conectarCmd(words) {
|
|
|
3052
3190
|
return;
|
|
3053
3191
|
}
|
|
3054
3192
|
|
|
3055
|
-
// ts conectar nuvem —
|
|
3193
|
+
// ts conectar nuvem — desliga a chave própria (volta à IA da conta / créditos legados)
|
|
3056
3194
|
if (sub === 'nuvem' || sub === 'cloud' || sub === 'ts') {
|
|
3057
3195
|
keyring.usar(null);
|
|
3058
3196
|
cfg = config.save({ forceTsCloud: true, personalModePreference:'automatico' });
|
|
3059
|
-
console.log(' ' + C.ok('✔ ') + (en ? '
|
|
3197
|
+
console.log(' ' + C.ok('✔ ') + (en ? 'own key turned off — runs use the account AI (your subscription, if selected, or TS legacy credits). To use your subscription: ts ia usar chatgpt|claude.' : 'chave própria desativada — as execuções usam a IA da conta (sua assinatura, se estiver selecionada, ou os créditos legados do TS). Para usar sua assinatura: ts ia usar chatgpt|claude.'));
|
|
3060
3198
|
return;
|
|
3061
3199
|
}
|
|
3062
3200
|
|
|
@@ -3106,7 +3244,8 @@ async function conectarCmd(words) {
|
|
|
3106
3244
|
C.dim((p.gratuito ? (en ? 'free · ' : 'grátis · ') : '') + p.limite));
|
|
3107
3245
|
}
|
|
3108
3246
|
linhas.push('', C.dim(en ? 'Connect with: ' : 'Conecte com: ') + C.cyan('ts conectar <provedor>'));
|
|
3109
|
-
linhas.push(C.dim(en ? '
|
|
3247
|
+
linhas.push(C.dim(en ? 'Your subscription: ' : 'Sua assinatura: ') + C.cyan('ts ia conectar chatgpt|claude'));
|
|
3248
|
+
linhas.push(C.dim(en ? 'Turn off your own key: ' : 'Desativar a chave própria: ') + C.cyan('ts conectar nuvem'));
|
|
3110
3249
|
console.log('\n' + ui.box(linhas, { title: 'ts conectar' }));
|
|
3111
3250
|
return;
|
|
3112
3251
|
}
|
|
@@ -3158,7 +3297,7 @@ async function conectarCmd(words) {
|
|
|
3158
3297
|
const listaNuvem = keyring.SO_NA_NUVEM.map(([o, p2]) => ' ' + C.dim('· ' + o + ' — ' + p2));
|
|
3159
3298
|
console.log('\n' + ui.box(
|
|
3160
3299
|
[(en ? 'Now running on YOUR key. Still needs TS Cloud:' : 'Agora roda na SUA chave. Ainda depende do TS Cloud:'), ...listaNuvem,
|
|
3161
|
-
'', C.dim(en ? '
|
|
3300
|
+
'', C.dim(en ? 'Turn your key off anytime: ' : 'Para desligar a sua chave: ') + C.cyan('ts conectar nuvem')],
|
|
3162
3301
|
{ title: en ? 'what changes' : 'o que muda' }));
|
|
3163
3302
|
}
|
|
3164
3303
|
|
|
@@ -3174,6 +3313,8 @@ async function doctorCmd() {
|
|
|
3174
3313
|
// Plano autoritativo. O de ~/.ts é do último login e envelhece a cada assinatura,
|
|
3175
3314
|
// troca ou cancelamento — o diagnóstico mostrava um plano que já não existe mais.
|
|
3176
3315
|
planoAtual: async () => { try { const r = await api('/api/credits', { token: cfg.token, timeoutMs: 8000 }); return (r && r.plan) || null; } catch (_) { return null; } },
|
|
3316
|
+
// de onde vem a IA sem chave própria: a assinatura selecionada e conectada (critério do ts agente)
|
|
3317
|
+
fonteIa: cfg.token ? () => aiSource(cfg.token, { timeoutMs: 5000 }) : undefined,
|
|
3177
3318
|
// última versão publicada no npm (best-effort).
|
|
3178
3319
|
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; } },
|
|
3179
3320
|
});
|
|
@@ -3338,7 +3479,8 @@ async function cloudCmd(args) {
|
|
|
3338
3479
|
console.log(C.dim(en ? 'provisioning your cloud box…' : 'provisionando sua caixa na nuvem…'));
|
|
3339
3480
|
try {
|
|
3340
3481
|
const r = await call('up', { method: 'POST', body: { confirmed: true, ...(requestedSlug ? { slug: requestedSlug } : {}) }, timeoutMs: 120000 });
|
|
3341
|
-
|
|
3482
|
+
// plan_required: o texto do servidor já explica (recurso só dos planos antigos); nada de venda aqui
|
|
3483
|
+
if (!r.ok) { console.log(ui.errLine(r.error || 'falha')); return; }
|
|
3342
3484
|
if (JSON_OUT) { console.log(JSON.stringify(r)); return; }
|
|
3343
3485
|
console.log('\n' + ui.box([
|
|
3344
3486
|
C.ok(en ? 'Cloud box is up!' : 'Caixa na nuvem no ar!'), '',
|
|
@@ -3413,7 +3555,7 @@ async function cloudCmd(args) {
|
|
|
3413
3555
|
if (!slug) return;
|
|
3414
3556
|
console.log(C.dim(en ? 'creating box…' : 'criando caixa…'));
|
|
3415
3557
|
const up = await call('up', { method: 'POST', body: { confirmed: true, slug }, timeoutMs: 120000 });
|
|
3416
|
-
if (!up.ok) { console.log(ui.errLine(up.error || 'falha'));
|
|
3558
|
+
if (!up.ok) { console.log(ui.errLine(up.error || 'falha')); return; }
|
|
3417
3559
|
}
|
|
3418
3560
|
console.log(C.dim(en ? 'running ts meta inside the cloud box (may take a while)…' : 'rodando ts meta dentro da caixa (pode demorar)…'));
|
|
3419
3561
|
const safe = goal.replace(/(["\\$`])/g, '\\$1');
|
|
@@ -3462,15 +3604,17 @@ async function uso() {
|
|
|
3462
3604
|
const token = needToken();
|
|
3463
3605
|
const r = await api('/api/credits', { token });
|
|
3464
3606
|
if (JSON_OUT) { console.log(JSON.stringify(r)); return; }
|
|
3607
|
+
// Mostra o plano e POR ONDE a IA roda. Crédito aparece só como saldo LEGADO (honrado
|
|
3608
|
+
// até acabar), sem barra de recarga — o TS não vende IA.
|
|
3465
3609
|
const unlimited = r.unlimited || (r.granted < 0);
|
|
3610
|
+
const src = await aiSource(token);
|
|
3611
|
+
const restante = Number(r.remaining) || 0;
|
|
3466
3612
|
const lines = [
|
|
3467
3613
|
C.dim(T.uso_plan + ': ') + C.bold(String(r.plan || '?')),
|
|
3468
|
-
'',
|
|
3469
|
-
unlimited
|
|
3470
|
-
? C.ok(T.uso_unlimited) + C.dim(` · ${r.used || 0} ${T.uso_credits}`)
|
|
3471
|
-
// A barra mostra o que RESTA, igual ao número ao lado dela.
|
|
3472
|
-
: 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}`)}`,
|
|
3614
|
+
C.dim(T.uso_ai + ': ') + (src.via ? C.cyan(src.via) : aiSourceMissing(src)),
|
|
3473
3615
|
];
|
|
3616
|
+
if (unlimited) lines.push('', C.ok(T.uso_unlimited) + C.dim(` · ${r.used || 0} ${T.uso_credits}`));
|
|
3617
|
+
else if (restante > 0) lines.push('', C.dim(T.uso_credits + ': ') + C.bold(String(restante)) + C.dim(` ${T.uso_remaining}`));
|
|
3474
3618
|
const ledger = (r.ledger || []).slice(0, 5);
|
|
3475
3619
|
if (ledger.length) {
|
|
3476
3620
|
lines.push('', C.dim(T.uso_last + ':'));
|
|
@@ -3563,6 +3707,7 @@ function _resetLocalAgentState(cwd) {
|
|
|
3563
3707
|
async function nova() {
|
|
3564
3708
|
const token = needToken();
|
|
3565
3709
|
_resetLocalAgentState(process.cwd());
|
|
3710
|
+
_chatLocal().limpar(_convScope('chat')); // histórico do chat local desta pasta
|
|
3566
3711
|
await ensureConv(token, 'chat', process.cwd(), true);
|
|
3567
3712
|
console.log(ui.okLine(T.new_conv));
|
|
3568
3713
|
}
|
|
@@ -3626,6 +3771,9 @@ function recallCmd(args) {
|
|
|
3626
3771
|
case 'video': case 'v': return videoCmd(POS.slice(1));
|
|
3627
3772
|
case 'arquivar': case 'archive': return arquivarCmd(POS.slice(1));
|
|
3628
3773
|
case 'qr': return qrCmd(POS.slice(1));
|
|
3774
|
+
// `ts hub ...`: usa os argumentos crus (o ID de aparelho pode começar com "-", que o POS
|
|
3775
|
+
// descartaria como flag). Tudo local: não exige login.
|
|
3776
|
+
case 'hub': return require('../hub/cli').main(rawArgs.slice(rawArgs.findIndex(a => a.toLowerCase() === 'hub') + 1));
|
|
3629
3777
|
case 'desfazer': case 'undo': return desfazerCmd(POS.slice(1));
|
|
3630
3778
|
case 'init': case 'iniciar': return initCmd();
|
|
3631
3779
|
case 'hooks': case 'ganchos': return hooksCmd(POS.slice(1));
|