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/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
- // Fase 1: cliente do backend (chat, orquestração, uso). Docs: terminalsmart.com.br/cli
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 instanceof ApiError) {
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. Terminal Smart plan credits were not consumed. Add balance at the provider or run \`ts conectar nuvem\` to use your TS plan.`
116
- : `Sua chave própria do ${provider} está sem saldo/capacidade no provedor. Os créditos do plano Terminal Smart não foram consumidos. Recarregue o provedor ou rode \`ts conectar nuvem\` para usar o plano TS.`;
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
- // ── Planos pessoais ChatGPT / Claude (mesma conta do Telegram/Web/App) ──────
231
- async function personalAiCmd(args = []) {
232
- const token=needToken(); const en=cfg.lang === 'en'; const sub=String(args[0] || 'status').toLowerCase();
233
- const label=p=>p === 'chatgpt' ? 'ChatGPT/Codex' : 'Claude';
234
- const show=state=>{
235
- if (JSON_OUT) { console.log(JSON.stringify(state)); return; }
236
- console.log('\n' + ui.box([
237
- `${en ? 'Mode' : 'Modo'}: ${state.mode || 'automatico'}`,
238
- `ChatGPT/Codex: ${state.chatgpt ? 'conectado' : 'não conectado'}`,
239
- `Claude: ${state.claude ? 'conectado' : 'não conectado'}`,
240
- '', C.dim(en ? 'Personal subscriptions use 0 TS model credits.' : 'Assinaturas pessoais usam 0 créditos de modelo TS.'),
241
- ], { title:'IA da conta' }) + '\n');
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
- if (sub === 'status') return show(await api('/api/personal-ai/status',{token}));
244
- if (['usar','use','modo','mode'].includes(sub)) {
245
- const mode=String(args[1] || '').toLowerCase();
246
- const state=await api('/api/personal-ai/mode',{method:'POST',token,body:{mode}});
247
- cfg = config.save({ forceTsCloud: mode === 'automatico' ? true : undefined, personalModePreference:mode });
248
- show(state); return;
249
- }
250
- if (['desconectar','disconnect'].includes(sub)) {
251
- const provider=String(args[1] || '').toLowerCase();
252
- const state=await api('/api/personal-ai/disconnect',{method:'POST',token,body:{provider}}); show(state); return;
253
- }
254
- if (['conectar','connect'].includes(sub)) {
255
- const provider=String(args[1] || '').toLowerCase();
256
- if (!['chatgpt','claude'].includes(provider)) throw new Error('Use: ts ia conectar chatgpt|claude');
257
- const login=await api('/api/personal-ai/login/start',{method:'POST',token,body:{provider}});
258
- if (login.alreadyConnected) return show(await api('/api/personal-ai/status',{token}));
259
- console.log('\n' + ui.box([
260
- login.url,
261
- ...(login.code ? ['', `${en ? 'Device code' : 'Código do aparelho'}: ${C.bold(login.code)}`] : []),
262
- '', C.dim(en ? 'Open the official page and finish authorization.' : 'Abra a página oficial e conclua a autorização.'),
263
- ], { title:`Login ${label(provider)}` }) + '\n');
264
- if (process.env.TS_NO_BROWSER !== '1') try {
265
- const { exec }=require('child_process'); const command=process.platform === 'win32' ? `start "" "${login.url}"` : process.platform === 'darwin' ? `open "${login.url}"` : `xdg-open "${login.url}"`;
266
- exec(command,()=>{});
267
- } catch (_) {}
268
- if (provider === 'claude' && process.stdin.isTTY) {
269
- const code=String(await ui.ask(en ? 'Paste the code returned by Claude: ' : 'Cole o código devolvido pelo Claude: ')).trim();
270
- if (code) await api('/api/personal-ai/login/code',{method:'POST',token,body:{provider,code}});
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
- for (let i=0;i<240;i++) {
273
- await new Promise(resolve=>setTimeout(resolve,2500));
274
- const state=await api(`/api/personal-ai/login/status?provider=${provider}`,{token});
275
- if (state.connected) { console.log(ui.okLine(`${label(provider)} ${en ? 'connected' : 'conectado'}.`)); return; }
276
- if (state.state === 'failed' || state.state === 'expired') throw new Error(state.error || 'O login não foi concluído.');
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
- throw new Error(en ? 'Login timed out.' : 'O login expirou.');
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('Use: ts ia status | conectar chatgpt|claude | usar automatico|chatgpt|claude|paralelo | desconectar chatgpt|claude');
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 requires Basic or Pro.' : 'GPT Image 2 requer plano Básico ou Pro.')
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
- // Envia uma mensagem na conversa ativa e devolve o texto (recria a conversa 1x se apagada na web).
485
- async function sendMessage(token, content) {
486
- const memoryBus = require('../lib/memory-bus');
487
- const memoryScope = memoryBus.scope(process.cwd());
488
- const doStream = async (convId) => {
489
- const sp = ui.spinner(T.thinking).start();
490
- let buf = '', errMsg = null;
491
- try {
492
- await sse('/api/ia/chat', {
493
- // Paridade com Web/App: o chat comum do CLI também declara o modo
494
- // automático. Sem isso o backend tratava a chamada como legado e podia
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 + créditos (best-effort, não trava a entrada)
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
- try {
986
- const cr = await api('/api/credits', { token, timeoutMs: 6000 });
987
- const cred = (cr.unlimited || cr.granted < 0) ? T.uso_unlimited : T.repl_credits((cr.remaining ?? 0).toLocaleString('pt-BR'));
988
- console.log(' ' + C.dim(T.repl_account(cfg.username || '?', cfg.plan || '?') + ' · ') + C.cyan(cred));
989
- } catch (_) {}
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('Para proteger credenciais, use o fluxo interativo de login exibido pelo CLI.'));
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 instanceof ApiError && e.code === 'conn') console.error(ui.errLine(T.err_conn));
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
- return T.agent_thinking + ' · ' + parts.join(' · '); // texto puro; o spinner aplica a cor
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) _emit(core.AgentEvents.systemInit({ model: model || 'auto', cwd: startCwd, mode: plan ? 'plan' : readOnly ? 'ask' : 'agent' }));
1511
- // Mesmo agente de projeto e mesmo fluxo usados no Telegram, Web e App.
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
- const personalState = await api('/api/personal-ai/status', { token });
1532
- // O modo é da CONTA e precisa ser idêntico no Telegram/Web/App/CLI. Uma
1533
- // preferência local antiga não pode vencer silenciosamente o estado remoto
1534
- // e consumir créditos TS quando a conta está em ChatGPT/Claude.
1535
- const selectedPersonal = personalState?.ok && personalState.mode !== 'automatico';
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
- const personalReady = selectedPersonal
1538
- && (personalState.mode === 'chatgpt' ? personalState.chatgpt
1539
- : personalState.mode === 'claude' ? personalState.claude
1540
- : personalState.chatgpt && personalState.claude);
1541
- if (selectedPersonal && !personalReady) {
1542
- throw new Error(`modo ${personalState.mode} selecionado, mas a conta necessária não está conectada`);
1543
- }
1544
- if (personalReady) {
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 || (cfg.personalModePreference && cfg.personalModePreference !== 'automatico')) {
1554
- if (!streamJson) _hlog(' ' + C.warn(`▲ IA pessoal indisponível (${String(e.message || e).slice(0, 120)}). Execução cancelada para não consumir créditos TS sem aviso.`));
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; usando o modo Automático do TS nesta execução (pode consumir créditos).'));
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
- const _ci = process.argv.findIndex(a => a === '--orcamento-creditos' || a === '--max-creditos');
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
- const hint = providerCapacityHint(e);
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
- try { _fs.mkdirSync(_p.dirname(sessFile), { recursive: true }); _fs.writeFileSync(sessFile, JSON.stringify({ cwd: persistCwd, at: new Date().toISOString(), messages: (out.messages || []).slice(-60) })); } catch (_) {}
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 REAL da janela na última chamada (só aparece se >25%)
1676
- const _cx = out.context && out.context.window
1677
- ? (out.context.used / out.context.window > 0.25
1678
- ? ' · ' + (cfg.lang === 'en' ? 'context ' : 'contexto ') + Math.round(out.context.used / 1000) + 'k/' + Math.round(out.context.window / 1000) + 'k (' + Math.round(100 * out.context.used / out.context.window) + '%)'
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 BYOK o gasto é na chave do usuário, não em crédito do TS — mostrar "créditos"
1685
- // aqui seria enganoso (nada foi cobrado). Troca pelo provedor que executou.
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 _base = _byok ? T.agent_footer_byok(out.steps, _byok.id, secs) : T.agent_footer(out.steps, out.credits, secs);
1688
- console.log(' ' + C.dim(_base) + C.dim(_extra) + C.dim(_cx) + C.dim(' · ts agente --continuar "..." pra seguir') + '\n');
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). Costs credits.' : 'mesma suíte em cada executor; um juiz fixo pontua todos (justo). Gasta créditos.') + '\n');
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(en ? 'AI limit reached — top up to run the bake-off.' : 'Limite de IA atingido — recarregue pra rodar o bake-off.')); process.exit(1); } throw e; }
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 (costs credits), an AI judge scores each' : 'roda o agente de verdade por caso (gasta créditos); um juiz de IA pontua cada um') + '\n');
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
- // teto de IA (free/plano) → CTA de upgrade limpo, nunca "HTTP 402" cru.
1863
- if (e && (e.code === 'no_credits' || e.status === 402)) { console.error('\n' + ui.errLine(en ? 'AI limit reached on this plan — top up or upgrade to run evals.' : 'Limite de IA do plano atingido — recarregue ou faça upgrade pra rodar evals.')); process.exit(1); }
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 (Telegram cobre o perigoso)
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(() => {}); // avisa no Telegram também
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
- 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) : ''));
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 Telegram → aplica → re-valida.
2438
- // NUNCA aplica sem OK humano (aprovação remota). Só quando o diagnóstico sobreviveu à refutação.
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 no Telegram, e SÓ aplica com
2496
- // aprovação humana. Aplica no alvo (local ou ssh remoto), re-valida o check e reporta. Retorna
2497
- // true/false se aplicou (consertou ou não), null se não houve aprovação/canal.
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
- const alvo = check.target ? check.target.split(' ').pop() : os.hostname();
2503
- // 1) aprovação remota (mesmo canal do agente): /api/cli/approval/start → poll
2504
- let st = null;
2505
- try { st = await api('/api/cli/approval/start', { method: 'POST', token, body: { comando: `[sentinela ${check.nome}@${alvo}] ${cmd}`, host: alvo }, timeoutMs: 20000 }); } catch (_) { return null; }
2506
- if (!st || !st.success || !st.id) { // sem canal Telegram → só propõe e para
2507
- if (!quiet) console.log(' ' + C.dim(en ? 'autofix: no Telegram channel to approve — proposed only.' : 'autofix: sem canal Telegram pra aprovar — só proposto.'));
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
- if (!quiet) console.log(' ' + C.warn('🔧 ') + C.dim((en ? 'autofix: sent to Telegram for approval — ' : 'autofix: enviei pro Telegram pra aprovar — ') + cmd.slice(0, 60)));
2511
- let approved = false;
2512
- const t0 = Date.now();
2513
- while (Date.now() - t0 < 150000) {
2514
- await new Promise(rs => setTimeout(rs, 3000));
2515
- let p = null;
2516
- try { p = await api('/api/cli/approval/poll', { method: 'POST', token, body: { id: st.id }, timeoutMs: 15000 }); } catch (_) { continue; }
2517
- if (p && p.status === 'approved') { approved = true; break; }
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: ${cmd}`); } catch (_) {}
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 \`${cmd}\` e o check voltou a passar.`
2532
- : `⚠️ [sentinela] ${check.nome}: autofix aplicado (\`${cmd}\`) mas o check AINDA falha (${rr.verdict.motivo}). Precisa de você.\n${rerun}`;
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: usar SUA chave de IA em vez dos créditos do TS ────────
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 ? 'using TS Cloud (your credits)' : 'usando o TS Cloud (seus créditos)');
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 — volta a usar os créditos do TS
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 ? 'back to TS Cloud (your credits).' : 'de volta ao TS Cloud (seus créditos).'));
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 ? 'Back to credits: ' : 'Voltar aos créditos: ') + C.cyan('ts conectar nuvem'));
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 ? 'Back anytime: ' : 'Voltar quando quiser: ') + C.cyan('ts conectar nuvem')],
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
- if (!r.ok) { console.log(ui.errLine(r.error || 'falha')); if (r.code === 'plan_required') console.log(C.dim(en ? 'Get Pro at terminalsmart.com.br/planos' : 'Assine o Pro em terminalsmart.com.br/planos')); return; }
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')); if (up.code === 'plan_required') console.log(C.dim('terminalsmart.com.br/planos')); return; }
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));