terminal-smart-cli 0.94.2 → 0.97.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 CHANGED
@@ -56,5 +56,14 @@ docker logs api --tail 200 | ts "tem erro grave aqui?"
56
56
  git diff HEAD~1 | ts "revise este diff procurando problemas de segurança"
57
57
  ```
58
58
 
59
+ ## Capability Pack: Office e Android
60
+
61
+ O CLI lê DOCX/PPTX de forma paginada e opera Android/Android TV por ferramentas
62
+ ADB tipadas (`android_dispositivos`, pareamento, conexão, instalação, abertura,
63
+ logcat e captura de tela). Ações no aparelho passam por aprovação; caminhos
64
+ continuam confinados à raiz da missão. Os motores pesados de criação
65
+ DOCX/XLSX/PDF/PPTX permanecem no App para manter o CLI leve e sem
66
+ vulnerabilidades de produção.
67
+
59
68
  ---
60
69
  Feito por [Terminal Smart](https://terminalsmart.com.br) · a conversa do CLI aparece também na web (/ia).
package/bin/ts.js CHANGED
@@ -249,12 +249,26 @@ async function videoCmd(words) {
249
249
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tsvid-'));
250
250
  const sp = ui.spinner(cfg.lang === 'en' ? 'Fetching transcript…' : 'Buscando a transcrição…').start();
251
251
  const out = path.join(tmp, 'v');
252
- const r = _cmd(yt.bin, [...yt.pre, '--skip-download', '--write-auto-subs', '--write-subs',
253
- '--sub-langs', 'pt.*,pt-BR,en.*,en', '--sub-format', 'vtt', '--convert-subs', 'vtt',
252
+ // LEGENDAS EM 2 TEMPOS (o pedido único de 4 faixas — pt.*,pt-BR,en.*,en — fazia o
253
+ // yt-dlp baixar uma atrás da outra e o YouTube devolvia HTTP 429). Primeiro tenta só a
254
+ // faixa ORIGINAL do vídeo (1 download); só se não vier nada é que amplia. `--sleep-requests`
255
+ // espaça as chamadas, que é o que o próprio yt-dlp recomenda contra rate limit.
256
+ const _baixarLegenda = (langs) => _cmd(yt.bin, [...yt.pre, '--skip-download', '--write-auto-subs', '--write-subs',
257
+ '--sub-langs', langs, '--sub-format', 'vtt', '--convert-subs', 'vtt',
258
+ '--sleep-requests', '1.5', '--retries', '3',
254
259
  '-o', out + '.%(ext)s', url], 180000);
260
+ const _achaVtt = () => {
261
+ try { const f = fs.readdirSync(tmp).find(n => n.endsWith('.vtt')); if (f) return fs.readFileSync(path.join(tmp, f), 'utf8'); } catch (_) {}
262
+ return null;
263
+ };
264
+ let r = _baixarLegenda('pt-orig,en-orig');
265
+ let vtt = _achaVtt();
266
+ if (!vtt) { // sem faixa "original" nomeada → tenta as normais
267
+ sp.text(cfg.lang === 'en' ? 'no original track, widening…' : 'sem faixa original, ampliando…');
268
+ r = _baixarLegenda('pt-BR,pt,en');
269
+ vtt = _achaVtt();
270
+ }
255
271
  sp.stop();
256
- let vtt = null;
257
- try { const f = fs.readdirSync(tmp).find(n => n.endsWith('.vtt')); if (f) vtt = fs.readFileSync(path.join(tmp, f), 'utf8'); } catch (_) {}
258
272
  try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
259
273
  if (!vtt) {
260
274
  console.error(ui.errLine(cfg.lang === 'en'
@@ -280,8 +294,50 @@ function _monolith() { const r = _cmd('monolith', ['--version'], 12000); return
280
294
  // ts desfazer <arquivo> — restaura o backup automático mais recente (sem gastar IA).
281
295
  async function desfazerCmd(words) {
282
296
  const _path = require('path');
297
+ const en = (cfg.lang === 'en');
298
+
299
+ // ts desfazer --sessao [id] — desfaz TUDO o que o agente mudou naquela sessão.
300
+ // Sem id = a mais recente. O plano checa hash: arquivo que VOCÊ editou depois entra como
301
+ // conflito e é PULADO (restaurar apagaria seu trabalho) — só passa com --forcar.
302
+ if (rawArgs.includes('--sessao') || rawArgs.includes('--session')) {
303
+ const cp = require('../lib/checkpoint');
304
+ const _i = rawArgs.findIndex(a => a === '--sessao' || a === '--session');
305
+ let id = rawArgs[_i + 1];
306
+ if (!id || id.startsWith('--')) { const l = cp.listar({ limite: 1 }); id = l.length ? l[0].id : null; }
307
+ if (!id) { console.error(ui.errLine(en ? 'no session recorded' : 'nenhuma sessão registrada')); process.exit(1); }
308
+ const forcar = rawArgs.includes('--forcar') || rawArgs.includes('--force');
309
+ const sess = cp.carregar(id);
310
+ if (!sess) { console.error(ui.errLine((en ? 'session not found: ' : 'sessão não encontrada: ') + id)); process.exit(1); }
311
+
312
+ const plano = cp.planejar(sess);
313
+ if (!plano.acoes.length && !(forcar && plano.conflitos.length)) {
314
+ console.log(' ' + C.dim(en ? 'nothing to undo' : 'nada a desfazer') +
315
+ (plano.conflitos.length ? C.warn(en ? ' (' + plano.conflitos.length + ' conflict(s) — use --forcar)' : ' (' + plano.conflitos.length + ' conflito(s) — use --forcar)') : ''));
316
+ for (const c2 of plano.conflitos) console.log(' ' + C.warn('▲ ') + C.dim(c2.caminho) + ' — ' + c2.motivo);
317
+ return;
318
+ }
319
+ // Mostra o plano e pede OK: desfazer é destrutivo do outro lado (reverte trabalho feito).
320
+ console.log('\n' + ui.box([
321
+ C.dim(en ? 'session: ' : 'sessão: ') + id,
322
+ C.dim(en ? 'task: ' : 'tarefa: ') + String(sess.tarefa || '—').slice(0, 60), '',
323
+ ...plano.acoes.map(a => (a.tipo === 'apagar' ? C.err('apagar ') : C.ok('restaurar ')) + C.dim(a.caminho)),
324
+ ...(plano.conflitos.length ? ['', C.warn(en ? 'skipped (changed after the session):' : 'pulados (mudaram depois da sessão):'),
325
+ ...plano.conflitos.map(c2 => C.dim(' ' + c2.caminho + ' — ' + c2.motivo))] : []),
326
+ ...(plano.jaDesfeita ? ['', C.warn(en ? 'this session was already undone once' : 'esta sessão já foi desfeita uma vez')] : []),
327
+ ], { title: 'ts desfazer --sessao' }));
328
+ const resposta = await ui.ask((en ? 'undo these ' : 'desfazer estas ') + (forcar ? plano.acoes.length + plano.conflitos.length : plano.acoes.length) + (en ? ' change(s)? [y/N] ' : ' mudança(s)? [s/N] '));
329
+ if (!/^(s|sim|y|yes)$/i.test(resposta)) { console.log(' ' + C.dim(en ? 'cancelled.' : 'cancelado.')); return; }
330
+
331
+ const r = cp.desfazer(id, { forcar });
332
+ console.log(' ' + C.ok('✔ ') + r.feitos.length + (en ? ' undone' : ' desfeita(s)') +
333
+ (r.falhas.length ? C.err(' ' + r.falhas.length + (en ? ' failed' : ' falharam')) : '') +
334
+ (r.pulados.length ? C.dim(' ' + r.pulados.length + (en ? ' skipped' : ' puladas')) : ''));
335
+ for (const f of r.falhas) console.log(' ' + C.err('✖ ') + f.caminho + ' — ' + f.erro);
336
+ return;
337
+ }
338
+
283
339
  const alvo = (words.join(' ') || '').trim();
284
- if (!alvo) { console.error(ui.infoLine(cfg.lang === 'en' ? 'Usage: ts desfazer <file> — restores the last auto-backup' : 'Uso: ts desfazer <arquivo> — restaura o último backup automático')); process.exit(2); }
340
+ if (!alvo) { console.error(ui.infoLine(cfg.lang === 'en' ? 'Usage: ts desfazer <file> — restores the last auto-backup · ts desfazer --sessao [id] — undoes the whole session' : 'Uso: ts desfazer <arquivo> — restaura o último backup · ts desfazer --sessao [id] — desfaz a sessão inteira')); process.exit(2); }
285
341
  const abs = _path.resolve(process.cwd(), alvo);
286
342
  const r = await require('../lib/tools').execute('restaurar_arquivo', { caminho: abs }, { baseDir: process.cwd() });
287
343
  if (r && r.erro) { console.error(ui.errLine(r.erro)); process.exit(1); }
@@ -339,9 +395,83 @@ async function skillsCmd(words) {
339
395
  const sub = (words[0] || '').toLowerCase();
340
396
  const arg = words.slice(1).join(' ').trim();
341
397
  // subcomandos 100% LOCAIS primeiro (revisão de pendências não exige pareamento)
342
- const _localSubs = ['pendentes', 'pending', 'ver-pendente', 'show-pending', 'aprovar', 'approve', 'recusar', 'reject', 'export', 'exportar', 'publish-git'];
398
+ const _localSubs = ['pendentes', 'pending', 'ver-pendente', 'show-pending', 'aprovar', 'approve', 'recusar', 'reject', 'export', 'exportar', 'publish-git', 'buscar', 'search', 'fontes', 'sources', 'indexar', 'index'];
343
399
  const token = _localSubs.includes(sub) ? null : needToken();
344
400
 
401
+ // ── DESCOBERTA: buscar/fontes/indexar (o "existe skill pra isso?") ─────────
402
+ const si = require('../lib/skill-index');
403
+ const _en = cfg.lang === 'en';
404
+
405
+ // ts skills buscar "o que preciso fazer"
406
+ if (sub === 'buscar' || sub === 'search') {
407
+ if (!arg) { console.error(ui.infoLine(_en ? 'usage: ts skills buscar "review a pull request"' : 'uso: ts skills buscar "revisar um pull request"')); process.exit(2); }
408
+ const achadas = si.buscar(si.todas(), arg, { k: 6 });
409
+ if (!achadas.length) {
410
+ console.log(' ' + C.dim(_en ? 'no skill covers that (searched installed + catalog).' : 'nenhuma skill cobre isso (procurei instaladas + catálogo).'));
411
+ const cache = si.lerCache();
412
+ if (!cache) console.log(' ' + C.dim(_en ? 'the catalog was never indexed: ' : 'o catálogo nunca foi indexado: ') + C.cyan('ts skills indexar'));
413
+ return;
414
+ }
415
+ const linhas = achadas.map(a => (a.instalada ? C.ok('●') : C.dim('○')) + ' ' + C.cyan(a.slug.padEnd(28)) +
416
+ C.dim(String(a.descricao || '').slice(0, 60)) + C.dim(' [' + a.origem + ']'));
417
+ linhas.push('', C.dim(_en ? '● installed · ○ available — install with ' : '● instalada · ○ disponível — instale com ') + C.cyan('ts skills add <slug>'));
418
+ console.log('\n' + ui.box(linhas, { title: 'ts skills buscar' }));
419
+ return;
420
+ }
421
+
422
+ // ts skills fontes [add <org>/<repo> | rm <org>/<repo>]
423
+ if (sub === 'fontes' || sub === 'sources') {
424
+ const acao = (words[1] || '').toLowerCase();
425
+ if (acao === 'add' && words[2]) {
426
+ try { si.addFonte(words[2]); console.log(' ' + C.ok('✔ ') + (_en ? 'source added: ' : 'fonte adicionada: ') + words[2] + C.dim(_en ? ' — now run: ts skills indexar' : ' — agora rode: ts skills indexar')); }
427
+ catch (e) { console.error(ui.errLine(e.message)); process.exit(2); }
428
+ return;
429
+ }
430
+ if ((acao === 'rm' || acao === 'remover') && words[2]) {
431
+ si.rmFonte(words[2]); console.log(' ' + C.ok('✔ ') + (_en ? 'removed: ' : 'removida: ') + words[2]); return;
432
+ }
433
+ const fs2 = si.fontes();
434
+ const linhas = fs2.length ? fs2.map(f => C.cyan('· ' + f.repo) + C.dim(' (' + (f.pasta || 'skills') + '/)'))
435
+ : [C.dim(_en ? 'no external source' : 'nenhuma fonte externa')];
436
+ linhas.push('', C.dim(_en ? 'add: ' : 'adicionar: ') + C.cyan('ts skills fontes add NVIDIA/skills'));
437
+ console.log('\n' + ui.box(linhas, { title: 'ts skills fontes' }));
438
+ return;
439
+ }
440
+
441
+ // ts skills indexar — monta o índice de busca (galeria + fontes externas)
442
+ if (sub === 'indexar' || sub === 'index') {
443
+ const _tok = (() => { try { return cfg.token; } catch (_) { return null; } })();
444
+ const sp = ui.spinner(_en ? 'indexing…' : 'indexando…').start();
445
+ const fetchGaleria = _tok ? (async () => {
446
+ const r = await require('../lib/skills').list(_tok);
447
+ return Array.isArray(r) ? r : (r && r.skills) || [];
448
+ }) : null;
449
+ const fetchExternas = async () => {
450
+ const out = [];
451
+ for (const f of si.fontes()) {
452
+ sp.text((_en ? 'indexing ' : 'indexando ') + f.repo + '…');
453
+ try {
454
+ // pasta:null = detecta sozinho (nem todo repo usa `skills/`)
455
+ const r = await si.indexarRepo(f.repo, { pasta: f.pasta || null, onProgresso: (i, n) => sp.text(f.repo + ' ' + i + '/' + n) });
456
+ out.push(...r.skills);
457
+ // Avisa SÓ quando o teto cortou de verdade. A diferença total×lidas normalmente é
458
+ // skill sem descrição ou cópia em pasta de outro agente — dizer "teto" ali seria
459
+ // alarme falso.
460
+ if (r.truncado) console.error(' ' + C.warn('▲ ') + f.repo + ': ' + (_en ? 'cap reached — some skills are not searchable' : 'teto atingido — algumas skills ficaram de fora'));
461
+ else if (r.lidas === 0) console.error(' ' + C.warn('▲ ') + f.repo + (_en ? ': no skill with description found' : ': nenhuma skill com descrição encontrada'));
462
+ } catch (e) { console.error(' ' + C.warn('▲ ') + f.repo + ': ' + e.message); }
463
+ }
464
+ return out;
465
+ };
466
+ const todas2 = await si.atualizar({ fetchGaleria, fetchExternas });
467
+ sp.stop();
468
+ console.log(' ' + C.ok('✔ ') + todas2.length + (_en ? ' skills in the index' : ' skills no índice') +
469
+ C.dim(' (' + (_en ? 'installed: ' : 'instaladas: ') + si.locais().length + ')'));
470
+ console.log(' ' + C.dim(_en ? 'try: ' : 'teste: ') + C.cyan('ts skills buscar "' + (_en ? 'what you need' : 'o que você precisa') + '"'));
471
+ return;
472
+ }
473
+
474
+
345
475
  // export [pasta] — exporta as skills INSTALADAS (~/.ts/skills) pro formato aberto Agent Skills
346
476
  // (cada skill vira <slug>/SKILL.md) + README, pronto pra virar um repo `npx skills add owner/repo`.
347
477
  if (sub === 'export' || sub === 'exportar' || sub === 'publish-git') {
@@ -431,6 +561,28 @@ async function skillsCmd(words) {
431
561
 
432
562
  // add <slug> (instala, com transparência + confirmação)
433
563
  if (sub === 'add' || sub === 'instalar' || sub === 'install') {
564
+ // Skill de CATÁLOGO EXTERNO: a galeria não conhece, então baixa o SKILL.md do repo de
565
+ // origem. Sem isto, o `ts skills add <slug>` que o próprio agente sugere falharia com
566
+ // "skill não encontrada" para tudo que não fosse da galeria.
567
+ const _ext = (si.lerCache() || { skills: [] }).skills.find(x => x.slug === arg && x.caminhoRepo);
568
+ if (_ext) {
569
+ const _fs2 = require('fs'), _p2 = require('path'), _os2 = require('os');
570
+ console.log(' ' + C.bold(C.cyan(_ext.slug)) + C.dim(' ' + _ext.origem));
571
+ if (_ext.descricao) console.log(' ' + C.dim(String(_ext.descricao).slice(0, 160)));
572
+ console.log(' ' + C.dim(_en ? 'source: ' : 'origem: ') + (_ext.url || _ext.origem));
573
+ // Conteúdo de terceiro: mostra de onde vem e PEDE confirmação antes de gravar.
574
+ if (process.stdin.isTTY && !YES) {
575
+ const a = String(await ui.ask(' ' + (_en ? 'Install from this third-party repo? (y/N) ' : 'Instalar desse repositório de terceiro? (s/N) '))).trim().toLowerCase();
576
+ if (!['s', 'sim', 'y', 'yes'].includes(a)) { console.log(' ' + C.dim(_en ? 'cancelled.' : 'cancelado.')); return; }
577
+ }
578
+ let md; try { md = await si.baixarSkill(_ext); } catch (e) { console.error(ui.errLine(e.message)); process.exit(1); }
579
+ const destino = _p2.join(_os2.homedir(), '.ts', 'skills', _ext.slug);
580
+ _fs2.mkdirSync(destino, { recursive: true });
581
+ _fs2.writeFileSync(_p2.join(destino, 'SKILL.md'), md, 'utf8');
582
+ console.log(' ' + ui.gradient('⌁') + ' ' + (_en ? 'installed at ' : 'instalada em ') + C.cyan(destino));
583
+ console.log(' ' + C.dim(_en ? 'read it before trusting: ' : 'leia antes de confiar: ') + C.cyan('ts skills ver ' + _ext.slug));
584
+ return;
585
+ }
434
586
  const s = await skills.get(token, arg);
435
587
  if (!s) { console.error(ui.infoLine(cfg.lang === 'en' ? 'skill not found.' : 'skill não encontrada.')); process.exit(1); }
436
588
  console.log(' ' + C.bold(C.cyan(s.name)) + C.dim(' · por ' + s.author + ' · ' + s.installs + ' instalações'));
@@ -564,6 +716,44 @@ async function chatRepl() {
564
716
  console.log('');
565
717
  } else if (low === '/uso') {
566
718
  await uso();
719
+ } else if (low === '/modelo' || low.startsWith('/modelo ') || low === '/model' || low.startsWith('/model ')) {
720
+ // /modelo — mostra o que está executando (e de onde vem); /modelo X troca na hora.
721
+ // Trocar no meio da conversa é o pedido mais repetido em CLI de agente.
722
+ const novo = msg.replace(/^\/\w+\s*/, '').trim();
723
+ const _kr = require('../lib/keyring');
724
+ const at = _kr.ativo();
725
+ if (novo) {
726
+ if (!at) {
727
+ console.log(' ' + C.warn('▲ ') + (cfg.lang === 'en'
728
+ ? 'model choice applies to your own key — set one with `ts conectar`.'
729
+ : 'a escolha de modelo vale pra SUA chave — configure com `ts conectar`.'));
730
+ } else {
731
+ _kr.salvar(at.id, { modelo: novo });
732
+ console.log(' ' + C.ok('✔ ') + (cfg.lang === 'en' ? 'model: ' : 'modelo: ') + C.cyan(novo) + C.dim(' (' + at.id + ')'));
733
+ }
734
+ } else {
735
+ console.log(' ' + (at
736
+ ? C.cyan('● ') + (cfg.lang === 'en' ? 'your key: ' : 'sua chave: ') + at.id + C.dim(' ' + (at.modelo || '—'))
737
+ : C.cyan('● ') + (cfg.lang === 'en' ? 'TS Cloud — the router picks per task' : 'TS Cloud — o roteador escolhe por tarefa')));
738
+ console.log(' ' + C.dim(cfg.lang === 'en' ? 'change: /modelo <name>' : 'trocar: /modelo <nome>'));
739
+ }
740
+ } else if (low === '/custo' || low === '/cost') {
741
+ // Com chave própria não há crédito do TS pra mostrar — dizer "0" seria mentira.
742
+ const at = require('../lib/keyring').ativo();
743
+ if (at) {
744
+ console.log(' ' + C.cyan('● ') + (cfg.lang === 'en'
745
+ ? 'running on YOUR key (' + at.id + ') — billed by the provider, not by TS.'
746
+ : 'rodando na SUA chave (' + at.id + ') — quem cobra é o provedor, não o TS.'));
747
+ } else { await uso(); }
748
+ } else if (low === '/tema' || low.startsWith('/tema ') || low === '/theme' || low.startsWith('/theme ')) {
749
+ temaCmd(msg.replace(/^\/\w+\s*/, '').trim().split(/\s+/).filter(Boolean));
750
+ if (msg.replace(/^\/\w+\s*/, '').trim()) console.log(' ' + C.dim(cfg.lang === 'en' ? '(applies fully on the next run)' : '(vale por completo na próxima execução)'));
751
+ } else if (low === '/limpar' || low === '/clear' || low === '/cls') {
752
+ // limpa a TELA, não o contexto (/nova é que zera a conversa) — a confusão entre os
753
+ // dois é clássica, por isso o lembrete.
754
+ process.stdout.write('\x1b[2J\x1b[H');
755
+ console.log(' ' + C.dim(cfg.lang === 'en' ? 'screen cleared — context intact (/nova resets the conversation)'
756
+ : 'tela limpa — o contexto continua (/nova zera a conversa)') + '\n');
567
757
  } else if (low === '/runs') {
568
758
  await runsCmd();
569
759
  } else if (low.startsWith('/run')) {
@@ -1031,7 +1221,11 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1031
1221
  // rodapé enriquecido: passos · créditos · tempo · MODELO · tokens (· contexto)
1032
1222
  const _tokTot = (out.tokens && ((out.tokens.inTok || 0) + (out.tokens.outTok || 0))) || 0;
1033
1223
  const _extra = (out.model ? ' · ' + String(out.model).slice(0, 22) : '') + (_tokTot ? ' · ↓ ' + _fmtTok(_tokTot) + ' tok' : '');
1034
- console.log(' ' + C.dim(T.agent_footer(out.steps, out.credits, secs)) + C.dim(_extra) + C.dim(_cx) + C.dim(' · ts agente --continuar "..." pra seguir') + '\n');
1224
+ // Com BYOK o gasto é na chave do usuário, não em crédito do TS mostrar "créditos"
1225
+ // aqui seria enganoso (nada foi cobrado). Troca pelo provedor que executou.
1226
+ const _byok = require('../lib/keyring').ativo();
1227
+ const _base = _byok ? T.agent_footer_byok(out.steps, _byok.id, secs) : T.agent_footer(out.steps, out.credits, secs);
1228
+ console.log(' ' + C.dim(_base) + C.dim(_extra) + C.dim(_cx) + C.dim(' · ts agente --continuar "..." pra seguir') + '\n');
1035
1229
  _printWt();
1036
1230
  }
1037
1231
 
@@ -1582,6 +1776,32 @@ async function diagnosticarCmd(args) {
1582
1776
  C.dim(en ? 'Ruled out: ' : 'Descartado: ') + r.evidence.map(e => e.hypothesis).filter((v, i, a) => v && a.indexOf(v) === i).slice(0, 5).join('; ').slice(0, 120),
1583
1777
  ], { title: 'ts diagnóstico' }));
1584
1778
  }
1779
+
1780
+ // --registrar: a investigação vira NOTA DE CONHECIMENTO em vez de sumir com o terminal.
1781
+ // Guarda a trilha inteira, inclusive o que foi REFUTADO — na próxima vez que o sintoma
1782
+ // voltar, o caminho já descartado não precisa ser refeito. Vale também quando o
1783
+ // diagnóstico NÃO conclui (aí a nota é justamente a lista do que já se sabe que não é).
1784
+ if (rawArgs.includes('--registrar') || rawArgs.includes('--record')) {
1785
+ try {
1786
+ const conhecimento = require('../lib/conhecimento');
1787
+ const core = require('../lib/core');
1788
+ const { meta, corpo } = conhecimento.montarIncidente(r, {
1789
+ sintoma: sym,
1790
+ alvo: target,
1791
+ projeto: require('path').basename(process.cwd()),
1792
+ redigir: core.redactSecrets, // saída de sonda vem com token/senha
1793
+ });
1794
+ const arq = conhecimento.salvar({ tipo: 'incidente', meta, corpo });
1795
+ console.log('\n ' + C.ok('✔ ') + (en ? 'incident note saved: ' : 'nota de incidente salva: ') +
1796
+ C.bold(require('path').relative(process.cwd(), arq)));
1797
+ console.log(' ' + C.dim(en
1798
+ ? 'confidence: ' + meta.confianca + ' · edit it and set origem: humano to make it yours'
1799
+ : 'confiança: ' + meta.confianca + ' · edite e troque para origem: humano para a IA não reescrever'));
1800
+ } catch (e) {
1801
+ // Registrar é BÔNUS: falhar aqui não pode invalidar o diagnóstico que já foi feito.
1802
+ console.error(' ' + C.warn('▲ ') + (en ? 'could not save the note: ' : 'não deu pra salvar a nota: ') + e.message);
1803
+ }
1804
+ }
1585
1805
  }
1586
1806
 
1587
1807
  // ── ts sentinela — vigia DETERMINÍSTICO (cron --no-agent, zero token) ────────
@@ -1810,7 +2030,7 @@ async function sonharCmd(args) {
1810
2030
  if (cfg.token) {
1811
2031
  const agent = require('../lib/agent');
1812
2032
  summarizeFn = async (bruto) => {
1813
- const k = await api('/api/ai/key?feature=cli_agent', { token: cfg.token, timeoutMs: 20000 });
2033
+ const k = await require('../lib/keyring').resolve(cfg.token, { feature: 'cli_agent' });
1814
2034
  if (!k || !k.key) return '';
1815
2035
  const prompt = (en
1816
2036
  ? 'Consolidate this project run-history into a SHORT, durable memory (5-8 bullet lines): what the project is, what was done, key decisions, what remains. Keep the history\'s language. No preamble.\n\n'
@@ -2005,6 +2225,275 @@ function trilhaCmd() {
2005
2225
  console.log('\n' + ui.box(lines, { title: 'ts trilha' }));
2006
2226
  }
2007
2227
 
2228
+ // ── ts tema — paleta do CLI (cor é SEMÂNTICA: ok/aprovação/bloqueado) ────────
2229
+ function temaCmd(words) {
2230
+ const en = (cfg.lang === 'en');
2231
+ const temas = require('../lib/temas');
2232
+ const alvo = (words[0] || '').toLowerCase();
2233
+
2234
+ if (alvo && !temas.existe(alvo)) {
2235
+ console.error(ui.errLine((en ? 'unknown theme: ' : 'tema desconhecido: ') + alvo + ' (' + temas.nomes().join(', ') + ')'));
2236
+ process.exit(2);
2237
+ }
2238
+ if (alvo) {
2239
+ config.save({ tema: alvo });
2240
+ // repinta com o tema NOVO: o ui.js já foi carregado com o antigo, então monta na hora
2241
+ const C2 = temas.montar(alvo, ui.TTY);
2242
+ console.log(' ' + C2.ok('✔ ') + (en ? 'theme: ' : 'tema: ') + C2.cyan(temas.info(alvo).nome));
2243
+ console.log(' ' + temas.gradienteDe(alvo, 'T E R M I N A L S M A R T', ui.TTY) + ' ' +
2244
+ C2.ok('✔') + ' ' + C2.warn('▲') + ' ' + C2.err('✖') + ' ' + C2.dim('·'));
2245
+ return;
2246
+ }
2247
+
2248
+ // sem argumento: mostra a amostra de TODOS (é assim que se escolhe tema)
2249
+ const linhas = [];
2250
+ for (const id of temas.nomes()) {
2251
+ const C2 = temas.montar(id, ui.TTY);
2252
+ const marca = (id === ui.TEMA) ? C2.ok('●') : C2.dim('○');
2253
+ linhas.push(marca + ' ' + C2.cyan(id.padEnd(10)) +
2254
+ temas.gradienteDe(id, 'Terminal Smart', ui.TTY) + ' ' +
2255
+ C2.ok('✔') + ' ' + C2.warn('▲') + ' ' + C2.err('✖') + ' ' + C2.dim('leitura') +
2256
+ C2.dim(' ' + temas.info(id).nome));
2257
+ }
2258
+ linhas.push('', C.dim(en ? 'pick one: ' : 'escolha: ') + C.cyan('ts tema <nome>'));
2259
+ linhas.push(C.dim(en ? 'colors mean state: ✔ proven · ▲ needs approval · ✖ blocked · dim read-only'
2260
+ : 'a cor É o estado: ✔ comprovado · ▲ pede aprovação · ✖ bloqueado · apagado só-leitura'));
2261
+ console.log('\n' + ui.box(linhas, { title: 'ts tema' }));
2262
+ }
2263
+
2264
+ // ── ts politica — permissões declarativas por projeto ────────────────────────
2265
+ async function politicaCmd(words) {
2266
+ const en = (cfg.lang === 'en');
2267
+ const policy = require('../lib/policy');
2268
+ const _fs = require('fs'), _path = require('path');
2269
+ const sub = (words[0] || '').toLowerCase();
2270
+ const arqProjeto = _path.join(process.cwd(), policy.ARQ_PROJETO);
2271
+
2272
+ // ts politica exemplo — imprime um modelo comentado
2273
+ if (sub === 'exemplo' || sub === 'example') {
2274
+ console.log(JSON.stringify(policy.EXEMPLO, null, 2));
2275
+ console.log('\n' + C.dim(en ? 'save as ' : 'salve como ') + C.cyan(policy.ARQ_PROJETO) +
2276
+ C.dim(en ? ' (project) or ' : ' (projeto) ou ') + C.cyan('~/.ts/policy.json') + C.dim(en ? ' (global)' : ' (global)'));
2277
+ return;
2278
+ }
2279
+
2280
+ // ts politica testar "<comando>" [--tool nome]
2281
+ if (sub === 'testar' || sub === 'test') {
2282
+ const _ti = rawArgs.indexOf('--tool');
2283
+ const tool = _ti >= 0 ? rawArgs[_ti + 1] : 'executar_comando';
2284
+ const valor = words.slice(1).filter(a => a !== '--tool' && a !== tool).join(' ').trim();
2285
+ if (!valor) { console.error(ui.infoLine(en ? 'usage: ts politica testar "rm -rf build" [--tool executar_comando]' : 'uso: ts politica testar "rm -rf build" [--tool executar_comando]')); process.exit(2); }
2286
+ const pol = policy.carregar({ dir: process.cwd() });
2287
+ if (!pol.temPolitica) { console.log(' ' + C.dim(en ? 'no policy file — the normal gate decides' : 'sem arquivo de política — quem decide é o gate normal')); return; }
2288
+ const d = policy.decidir(pol, tool, {}, { valor });
2289
+ const cor = { allow: C.ok, ask: C.warn, deny: C.err };
2290
+ if (!d) { console.log(' ' + C.dim(en ? 'the policy does not rule on this — normal gate decides' : 'a política não opina sobre isso — decide o gate normal')); return; }
2291
+ console.log('\n' + ui.box([
2292
+ (cor[d.veredito] || C.dim)(d.veredito.toUpperCase()) + C.dim(' ' + (en ? 'category: ' : 'categoria: ') + policy.categoriaDe(tool)),
2293
+ C.dim(en ? 'rule: ' : 'regra: ') + d.regra + C.dim(' (' + d.origem + ')'),
2294
+ ...(d.nota ? ['', C.warn('▲ ' + d.nota)] : []),
2295
+ ], { title: 'ts politica' }));
2296
+ return;
2297
+ }
2298
+
2299
+ // ts politica → mostra o que está valendo
2300
+ const pol = policy.carregar({ dir: process.cwd() });
2301
+ const linhas = [];
2302
+ const _desc = (arq, obj) => {
2303
+ if (!obj) { linhas.push(C.dim('○ ' + arq + (en ? ' — none' : ' — nenhuma'))); return; }
2304
+ const erros = policy.validar(obj);
2305
+ linhas.push((erros.length ? C.err('✖ ') : C.ok('● ')) + C.bold(arq));
2306
+ for (const [cat, val] of Object.entries(obj)) {
2307
+ const txt = (typeof val === 'string') ? val
2308
+ : Object.entries(val).map(([g, v]) => g + '=' + v).join(' ');
2309
+ linhas.push(' ' + C.cyan(cat.padEnd(12)) + C.dim(txt));
2310
+ }
2311
+ for (const e of erros) linhas.push(' ' + C.err('✖ ' + e));
2312
+ };
2313
+ _desc('~/.ts/policy.json' + (en ? ' (global)' : ' (global)'), pol.global);
2314
+ linhas.push('');
2315
+ _desc(policy.ARQ_PROJETO + (en ? ' (project)' : ' (projeto)'), pol.projeto);
2316
+ linhas.push('', C.dim(en
2317
+ ? 'the project policy can only RESTRICT the global one (repo content is untrusted)'
2318
+ : 'a política do projeto só pode RESTRINGIR a global (conteúdo de repo não é confiável)'));
2319
+ linhas.push(C.dim(en ? 'try: ' : 'teste: ') + C.cyan('ts politica testar "rm -rf build"') + C.dim(' · ') + C.cyan('ts politica exemplo'));
2320
+ console.log('\n' + ui.box(linhas, { title: 'ts politica' }));
2321
+ if (!pol.temPolitica) console.log(' ' + C.dim(en ? 'no policy yet — the normal gate is in charge.' : 'sem política ainda — quem manda é o gate normal.'));
2322
+ }
2323
+
2324
+ // ── ts checkpoints — o que o agente mudou em cada sessão (e como desfazer tudo) ──
2325
+ async function checkpointsCmd(words) {
2326
+ const en = (cfg.lang === 'en');
2327
+ const cp = require('../lib/checkpoint');
2328
+ const sub = (words[0] || '').toLowerCase();
2329
+
2330
+ if (sub === 'limpar' || sub === 'clean') {
2331
+ console.log(' ' + C.ok('✔ ') + cp.podar() + (en ? ' old session(s) removed' : ' sessão(ões) antiga(s) removida(s)'));
2332
+ return;
2333
+ }
2334
+
2335
+ // ts checkpoints ver <id>
2336
+ if (sub === 'ver' || sub === 'show') {
2337
+ const sess = cp.carregar(words[1]);
2338
+ if (!sess) { console.error(ui.errLine(en ? 'session not found' : 'sessão não encontrada')); process.exit(1); }
2339
+ const plano = cp.planejar(sess);
2340
+ const linhas = [C.dim(en ? 'task: ' : 'tarefa: ') + String(sess.tarefa || '—').slice(0, 70), ''];
2341
+ for (const a of plano.acoes) linhas.push(C.ok('↶ ') + (a.tipo === 'apagar' ? (en ? 'delete ' : 'apagar ') : (en ? 'restore ' : 'restaura')) + ' ' + C.dim(a.caminho));
2342
+ for (const c2 of plano.conflitos) linhas.push(C.warn('▲ ') + C.dim(c2.caminho) + ' ' + C.warn(c2.motivo));
2343
+ if (plano.jaDesfeita) linhas.push('', C.dim(en ? 'already undone once' : 'já foi desfeita uma vez'));
2344
+ console.log('\n' + ui.box(linhas, { title: 'checkpoint ' + sess.id }));
2345
+ return;
2346
+ }
2347
+
2348
+ const l = cp.listar();
2349
+ if (!l.length) { console.log(' ' + C.dim(en ? 'no session recorded yet' : 'nenhuma sessão registrada ainda')); return; }
2350
+ const linhas = l.map(s => (s.desfeitaEm ? C.dim('↶') : C.ok('●')) + ' ' + C.bold(s.id) +
2351
+ C.dim(' ' + s.arquivos + (en ? ' file(s)' : ' arquivo(s)') + ' · ' + s.mutacoes + (en ? ' change(s)' : ' mudança(s)')) +
2352
+ C.dim(' ' + String(s.tarefa || '').slice(0, 40)));
2353
+ linhas.push('', C.dim(en ? 'undo everything: ' : 'desfazer tudo: ') + C.cyan('ts desfazer --sessao ' + l[0].id));
2354
+ console.log('\n' + ui.box(linhas, { title: 'ts checkpoints' }));
2355
+ }
2356
+
2357
+ // ── ts conectar — BYOK: usar SUA chave de IA em vez dos créditos do TS ────────
2358
+ // O agente já fala OpenAI-compat, então qualquer provedor desses entra sem mexer no loop.
2359
+ // O cadastro TESTA de verdade: a chave responde? e o modelo CHAMA FERRAMENTA? A segunda
2360
+ // pergunta é a que importa — modelo que não faz tool-call vira um chat que narra sucesso.
2361
+ async function conectarCmd(words) {
2362
+ const en = (cfg.lang === 'en');
2363
+ const providers = require('../lib/providers');
2364
+ const keyring = require('../lib/keyring');
2365
+ const sub = (words[0] || '').toLowerCase();
2366
+
2367
+ const _rl = () => require('readline').createInterface({ input: process.stdin, output: process.stdout });
2368
+ const perguntar = (q) => new Promise((res) => { const rl = _rl(); rl.question(q, (a) => { rl.close(); res(String(a || '').trim()); }); });
2369
+
2370
+ const _statusAtual = () => {
2371
+ const a = keyring.ativo();
2372
+ return a
2373
+ ? C.ok('● ') + (en ? 'using YOUR key: ' : 'usando SUA chave: ') + C.bold((providers.info(a.id) || {}).nome || a.id) +
2374
+ C.dim(' ' + (a.modelo || '') + (a.toolsOk ? '' : (en ? ' (tools: unreliable)' : ' (ferramentas: não confiável)')))
2375
+ : C.cyan('● ') + (en ? 'using TS Cloud (your credits)' : 'usando o TS Cloud (seus créditos)');
2376
+ };
2377
+
2378
+ // ts conectar listar
2379
+ if (sub === 'listar' || sub === 'list' || sub === 'ls') {
2380
+ const l = keyring.listar();
2381
+ if (!l.length) { console.log(' ' + C.dim(en ? 'no provider configured. Run: ts conectar' : 'nenhum provedor configurado. Rode: ts conectar')); return; }
2382
+ const linhas = l.map(p => (p.ativo ? C.ok('●') : C.dim('○')) + ' ' + C.bold(((providers.info(p.id) || {}).nome) || p.id).padEnd(28) +
2383
+ C.dim(p.chaveMascarada.padEnd(16) + ' ' + (p.modelo || '') + (p.toolsOk ? '' : (en ? ' [no tools]' : ' [sem ferramentas]'))));
2384
+ console.log('\n' + ui.box(linhas, { title: 'ts conectar' }));
2385
+ return;
2386
+ }
2387
+
2388
+ // ts conectar nuvem — volta a usar os créditos do TS
2389
+ if (sub === 'nuvem' || sub === 'cloud' || sub === 'ts') {
2390
+ keyring.usar(null);
2391
+ console.log(' ' + C.ok('✔ ') + (en ? 'back to TS Cloud (your credits).' : 'de volta ao TS Cloud (seus créditos).'));
2392
+ return;
2393
+ }
2394
+
2395
+ // ts conectar usar <id>
2396
+ if (sub === 'usar' || sub === 'use') {
2397
+ try { keyring.usar((words[1] || '').toLowerCase()); }
2398
+ catch (e) { console.error(ui.errLine(e.message)); process.exit(1); }
2399
+ console.log(' ' + C.ok('✔ ') + _statusAtual());
2400
+ return;
2401
+ }
2402
+
2403
+ // ts conectar remover <id>
2404
+ if (sub === 'remover' || sub === 'rm' || sub === 'remove') {
2405
+ const id = (words[1] || '').toLowerCase();
2406
+ console.log(' ' + (keyring.remover(id)
2407
+ ? C.ok('✔ ') + (en ? 'removed: ' : 'removido: ') + id
2408
+ : C.warn('· ') + (en ? 'not configured: ' : 'não estava configurado: ') + id));
2409
+ return;
2410
+ }
2411
+
2412
+ // ts conectar testar [id]
2413
+ if (sub === 'testar' || sub === 'test') {
2414
+ const a = (words[1] ? { id: words[1].toLowerCase() } : keyring.ativo());
2415
+ if (!a) { console.error(ui.errLine(en ? 'no active provider. Run: ts conectar' : 'nenhum provedor ativo. Rode: ts conectar')); process.exit(1); }
2416
+ const salvo = keyring.listar().find(p => p.id === a.id) || {};
2417
+ const st = keyring._load().provedores || {};
2418
+ const sp = ui.spinner(en ? 'testing…' : 'testando…').start();
2419
+ const r = await keyring.testar(a.id, { key: (st[a.id] || {}).key, baseUrl: salvo.baseUrl, modelo: salvo.modelo });
2420
+ sp.stop();
2421
+ if (!r.ok) { console.error(ui.errLine(r.erro)); process.exit(1); }
2422
+ keyring.salvar(a.id, { toolsOk: r.toolsOk, testadoEm: new Date().toISOString() });
2423
+ console.log(' ' + C.ok('✔ ') + (en ? 'key works' : 'a chave funciona') + C.dim(' · ' + r.modelo));
2424
+ console.log(' ' + (r.toolsOk ? C.ok('✔ ') + (en ? 'calls tools' : 'chama ferramentas') : C.warn('▲ ') + (en ? 'does NOT call tools — ' : 'NÃO chama ferramentas — ') + r.obsTools));
2425
+ return;
2426
+ }
2427
+
2428
+ // ts conectar → mostra o estado e o menu
2429
+ // ts conectar <provedor> → cadastra
2430
+ const alvo = sub;
2431
+ if (!alvo) {
2432
+ const linhas = [_statusAtual(), ''];
2433
+ linhas.push(C.dim(en ? 'Available providers:' : 'Provedores disponíveis:'));
2434
+ for (const id of providers.ids()) {
2435
+ if (id === 'custom') continue;
2436
+ const p = providers.info(id);
2437
+ linhas.push(' ' + C.cyan(id.padEnd(12)) + C.bold(p.nome.padEnd(22)) +
2438
+ C.dim((p.gratuito ? (en ? 'free · ' : 'grátis · ') : '') + p.limite));
2439
+ }
2440
+ linhas.push('', C.dim(en ? 'Connect with: ' : 'Conecte com: ') + C.cyan('ts conectar <provedor>'));
2441
+ linhas.push(C.dim(en ? 'Back to credits: ' : 'Voltar aos créditos: ') + C.cyan('ts conectar nuvem'));
2442
+ console.log('\n' + ui.box(linhas, { title: 'ts conectar' }));
2443
+ return;
2444
+ }
2445
+
2446
+ const p = providers.info(alvo);
2447
+ if (!p) {
2448
+ console.error(ui.errLine((en ? 'unknown provider: ' : 'provedor desconhecido: ') + alvo +
2449
+ ' (' + providers.ids().filter(i => i !== 'custom').join(', ') + ')'));
2450
+ process.exit(2);
2451
+ }
2452
+
2453
+ console.log('\n ' + ui.gradient('⌁') + ' ' + C.bold(p.nome) + C.dim(' ' + p.cadastro));
2454
+ let baseUrl = p.baseUrl;
2455
+ if (alvo === 'custom' || !baseUrl) {
2456
+ baseUrl = await perguntar(' ' + (en ? 'baseUrl (ends in /v1): ' : 'baseUrl (termina em /v1): '));
2457
+ if (!baseUrl) { console.error(ui.errLine(en ? 'baseUrl required.' : 'baseUrl é obrigatória.')); process.exit(2); }
2458
+ }
2459
+ let key = '';
2460
+ if (!p.semChave) {
2461
+ key = await perguntar(' ' + (en ? 'paste your API key: ' : 'cole sua chave de API: '));
2462
+ const errChave = providers.validarChave(alvo, key);
2463
+ if (errChave) { console.error(ui.errLine(errChave)); process.exit(2); }
2464
+ }
2465
+ // A escolha do modelo é o que decide a experiência (latência e tool-calling), então
2466
+ // mostro o que já foi MEDIDO em vez de deixar o usuário adivinhar entre 100+ opções.
2467
+ if (Array.isArray(p.sugestoes) && p.sugestoes.length) {
2468
+ console.log(' ' + C.dim(en ? 'measured options:' : 'opções já medidas:'));
2469
+ for (const [m, nota] of p.sugestoes) console.log(' ' + C.cyan(m) + C.dim(' ' + nota));
2470
+ }
2471
+ const modelo = (await perguntar(' ' + (en ? 'model' : 'modelo') + C.dim(' [' + (p.modeloPadrao || '?') + ']') + ': ')) || p.modeloPadrao;
2472
+
2473
+ const sp = ui.spinner(en ? 'testing the key…' : 'testando a chave…').start();
2474
+ const r = await keyring.testar(alvo, { key, baseUrl, modelo });
2475
+ sp.stop();
2476
+ if (!r.ok) { console.error(ui.errLine(r.erro)); process.exit(1); }
2477
+
2478
+ keyring.salvar(alvo, { key, baseUrl: r.baseUrl, modelo: r.modelo, toolsOk: r.toolsOk, testadoEm: new Date().toISOString() });
2479
+ keyring.usar(alvo);
2480
+
2481
+ console.log(' ' + C.ok('✔ ') + (en ? 'key works' : 'a chave funciona') + C.dim(' · ' + r.modelo));
2482
+ if (r.toolsOk) {
2483
+ console.log(' ' + C.ok('✔ ') + (en ? 'calls tools — good for the agent' : 'chama ferramentas — serve pro agente'));
2484
+ } else {
2485
+ // Aviso honesto: sem tool-call o agente não EXECUTA, só conversa.
2486
+ console.log(' ' + C.warn('▲ ') + (en ? 'does NOT call tools (' : 'NÃO chama ferramentas (') + r.obsTools + ')');
2487
+ console.log(' ' + C.dim(en ? 'use it for chat/plan; for `ts agente` prefer another model.'
2488
+ : 'use pra chat/plano; pro `ts agente` prefira outro modelo.'));
2489
+ }
2490
+ const listaNuvem = keyring.SO_NA_NUVEM.map(([o, p2]) => ' ' + C.dim('· ' + o + ' — ' + p2));
2491
+ console.log('\n' + ui.box(
2492
+ [(en ? 'Now running on YOUR key. Still needs TS Cloud:' : 'Agora roda na SUA chave. Ainda depende do TS Cloud:'), ...listaNuvem,
2493
+ '', C.dim(en ? 'Back anytime: ' : 'Voltar quando quiser: ') + C.cyan('ts conectar nuvem')],
2494
+ { title: en ? 'what changes' : 'o que muda' }));
2495
+ }
2496
+
2008
2497
  // ── ts doctor — auto-diagnóstico do ambiente (Node/login/gateway/versão/deps) ──
2009
2498
  async function doctorCmd() {
2010
2499
  const en = (cfg.lang === 'en');
@@ -2280,6 +2769,10 @@ function recallCmd(args) {
2280
2769
  case 'hooks': case 'ganchos': return hooksCmd(POS.slice(1));
2281
2770
  case 'worktrees': case 'worktree': case 'wt': return worktreesCmd(POS.slice(1));
2282
2771
  case 'eval': case 'avaliar': case 'evals': return evalCmd(POS.slice(1));
2772
+ case 'tema': case 'theme': return temaCmd(POS.slice(1));
2773
+ case 'politica': case 'política': case 'policy': return politicaCmd(POS.slice(1));
2774
+ case 'checkpoints': case 'checkpoint': return checkpointsCmd(POS.slice(1));
2775
+ case 'conectar': case 'connect': case 'provedor': case 'provider': return conectarCmd(POS.slice(1));
2283
2776
  case 'mcp': return mcpCmd(POS.slice(1));
2284
2777
  case 'acp': return acpCmd();
2285
2778
  case 'skills': case 'skill': return skillsCmd(POS.slice(1));