terminal-smart-cli 0.97.29 → 0.97.30

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/lib/tools.js CHANGED
@@ -6,6 +6,7 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const crypto = require('crypto');
8
8
  const compactor = require('./compactor');
9
+ const fileLock = require('./file-lock');
9
10
 
10
11
  // ── SNAPSHOT antes de sobrescrever ──────────────────────────────────────────
11
12
  // Toda escrita/edição faz uma cópia do original em ~/.ts/backups/<hash>/<nome>.<ts>.bak
@@ -38,6 +39,23 @@ function _snapshot(absPath) {
38
39
  // pra compatibilidade (tools.isDestructive segue funcionando pra quem já importava).
39
40
  const { isDestructive } = require('./core');
40
41
 
42
+ // Conteúdo de e-mail/site/arquivo pode tentar convencer o modelo a mandar
43
+ // segredos para fora. O prompt ajuda, mas a defesa precisa existir também no
44
+ // executor: se um comando combina transporte de rede com fonte sensível, ele
45
+ // nunca é executado, inclusive quando foi formado por uma injeção indireta.
46
+ function secretExfiltrationReason(cmd) {
47
+ const text = String(cmd || '');
48
+ const transports = /\b(?:curl|wget|invoke-webrequest|invoke-restmethod|iwr|irm)\b/i.test(text)
49
+ || /\b(?:fetch|axios|requests\.(?:get|post|put)|http\.request|https\.request)\b/i.test(text);
50
+ if (!transports) return null;
51
+ const sensitiveFile = /(?:^|[\s"'=:@])(?:~[\\/])?(?:\.env(?:\.[a-z0-9_-]+)?|\.ssh[\\/](?:id_[a-z0-9_-]+|authorized_keys|config)|(?:id_rsa|id_ed25519|credentials(?:\.json)?|service[-_]?account[^\s"']*\.json))\b/i.test(text);
52
+ const sensitiveRuntime = /\b(?:process\.env|os\.environ|\$env:|get-childitem\s+env:)\b/i.test(text);
53
+ if (sensitiveFile || sensitiveRuntime) {
54
+ return 'BLOQUEADO: possível exfiltração de segredo. Um comando de rede não pode ler/enviar .env, chaves SSH, credenciais ou variáveis de ambiente. Nunca transmita segredos; use apenas dados públicos explicitamente necessários.';
55
+ }
56
+ return null;
57
+ }
58
+
41
59
  // Definições no formato OpenAI function-calling — schemas SIMPLES de propósito
42
60
  // (Gemini via CDC rejeita propertyNames/additionalProperties; só type/properties/required).
43
61
  const DEFS = [
@@ -382,11 +400,55 @@ function _guardTsHome(p) {
382
400
  // por algo que JAMAIS deve rodar é desperdício puro: recusamos NA HORA, apontando a
383
401
  // alternativa cirúrgica. Retorna a mensagem (motivo pro modelo) quando é auto-destrutivo,
384
402
  // ou null quando é seguro (aí segue o fluxo normal, inclusive o gate de aprovação comum).
403
+ // Apenas conversões de leitura, de UMA operação e sem encadeamento. Não tentamos
404
+ // "adivinhar" comandos mutantes: nesses casos, a recusa explicada é mais segura.
405
+ function _psLiteral(value) { return String(value || '').replace(/'/g, "''"); }
406
+ function _normalizeWindowsReadOnlyCommand(cmd, platform = process.platform) {
407
+ if (platform !== 'win32') return null;
408
+ const s = String(cmd || '').trim();
409
+ if (!s || /[\r\n;&|><`]/.test(s) || /^\s*(?:powershell|pwsh)(?:\.exe)?\b/i.test(s)) return null;
410
+ const drivePath = (raw) => {
411
+ const found = String(raw || '').trim().match(/^\/([a-zA-Z])\/(.+)$/);
412
+ return found ? `${found[1].toUpperCase()}:\\${found[2].replace(/\//g, '\\')}` : String(raw || '').trim();
413
+ };
414
+ let m = s.match(/^grep\s+(-n\s+)?(?:--\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))\s+([^\s]+)$/i);
415
+ if (m) {
416
+ const pattern = m[2] || m[3] || m[4];
417
+ const file = drivePath(m[5]);
418
+ return {
419
+ command: `powershell -NoProfile -Command "Select-String -LiteralPath '${_psLiteral(file)}' -Pattern '${_psLiteral(pattern)}' | ForEach-Object { '{0}:{1}' -f $_.LineNumber,$_.Line }"`,
420
+ note: 'grep convertido para Select-String (somente leitura).',
421
+ };
422
+ }
423
+ m = s.match(/^(tail|head)\s+-n\s+(\d{1,5})\s+([^\s]+)$/i);
424
+ if (m) {
425
+ const count = Math.min(10000, Math.max(1, Number(m[2])));
426
+ const file = drivePath(m[3]);
427
+ const part = m[1].toLowerCase() === 'tail' ? `-Tail ${count}` : `-TotalCount ${count}`;
428
+ return {
429
+ command: `powershell -NoProfile -Command "Get-Content -LiteralPath '${_psLiteral(file)}' ${part}"`,
430
+ note: `${m[1].toLowerCase()} convertido para Get-Content (somente leitura).`,
431
+ };
432
+ }
433
+ if (/^ls(?:\s+-[a-z]+)?$/i.test(s)) return { command: 'dir /a', note: 'ls convertido para dir /a (somente leitura).' };
434
+ if (/^pwd$/i.test(s)) return { command: 'cd', note: 'pwd convertido para cd (somente leitura).' };
435
+ return null;
436
+ }
437
+
385
438
  // Gotchas de shell no Windows que falham de um jeito CONFUSO (o agente perdia vários
386
439
  // passos tentando de novo). Devolve a mensagem com o CONSERTO pronto, ou null.
387
440
  function _shellGotcha(cmd) {
388
441
  const s = String(cmd || '');
389
442
  const multi = /[\r\n]/.test(s);
443
+ // executar_comando usa cmd.exe. Modelos alternam facilmente entre os dois
444
+ // dialetos e `Select-String`/`Get-Content` soltos viram "não reconhecido",
445
+ // desperdiçando uma rodada antes de o recovery engine conseguir explicar.
446
+ // Recusar antes da execução preserva a etapa e entrega a forma correta.
447
+ if (process.platform === 'win32'
448
+ && !/^\s*(?:powershell|pwsh)(?:\.exe)?\b/i.test(s)
449
+ && /(?:^|[&|]\s*|\s)(?:Select-String|Get-(?:Content|ChildItem|Item|FileHash|NetTCPConnection)|Test-Path|Set-(?:Content|Location)|Add-Content|Remove-Item|Copy-Item|Move-Item|New-Item)(?:\s|$)/i.test(s)) {
450
+ return 'Este shell é cmd.exe, mas o comando é do PowerShell. Chame explicitamente: powershell -NoProfile -Command "<comando>". Não repita o cmdlet solto.';
451
+ }
390
452
  // python -c / node -e multilinha ou com import() dinâmico: quebra no eval / morre calado
391
453
  if (/\bpython3?\s+-c\b/.test(s) && multi)
392
454
  return 'python -c MULTILINHA falha em silêncio no Windows. Escreva um arquivo .py com escrever_arquivo e rode "python arquivo.py".';
@@ -410,6 +472,39 @@ function _shellGotcha(cmd) {
410
472
  return null;
411
473
  }
412
474
 
475
+ // `path.resolve` protege contra "..", mas não contra `raiz/link -> /fora`.
476
+ // Para não ler/escrever através de junction/symlink, comparamos também o caminho
477
+ // REAL do alvo (ou do ancestral existente quando o arquivo ainda será criado).
478
+ function _pathStartsWith(child, parent) {
479
+ const norm = (p) => process.platform === 'win32' ? String(p).toLowerCase() : String(p);
480
+ const c = norm(child), r = norm(parent);
481
+ return c === r || c.startsWith(r + path.sep);
482
+ }
483
+ function _nearestExistingReal(p) {
484
+ let cur = path.resolve(p);
485
+ for (;;) {
486
+ try { return fs.realpathSync.native ? fs.realpathSync.native(cur) : fs.realpathSync(cur); } catch (_) {}
487
+ const parent = path.dirname(cur);
488
+ if (parent === cur) return null;
489
+ cur = parent;
490
+ }
491
+ }
492
+ function _scopeContains(confineDir, candidate) {
493
+ const root = path.resolve(confineDir);
494
+ const abs = path.resolve(candidate);
495
+ if (!_pathStartsWith(abs, root)) return false;
496
+ const rootReal = _nearestExistingReal(root);
497
+ const targetReal = _nearestExistingReal(abs);
498
+ // Se não conseguimos resolver (raiz removida no meio da operação), falha
499
+ // fechada: é mais seguro recusar que abrir caminho fora da missão.
500
+ return !!rootReal && !!targetReal && _pathStartsWith(targetReal, rootReal);
501
+ }
502
+ function _acquireFileMutation(target) {
503
+ const lock = fileLock.acquire(target);
504
+ if (lock) return { lock };
505
+ return { erro: 'ARQUIVO_OCUPADO: outro processo do Terminal Smart está alterando este arquivo. Aguarde e tente uma vez; nunca force a remoção do lock.' };
506
+ }
507
+
413
508
  function _commandScopeViolation(cmd, confineDir) {
414
509
  if (!confineDir) return null;
415
510
  const text = String(cmd || '');
@@ -422,9 +517,14 @@ function _commandScopeViolation(cmd, confineDir) {
422
517
  const candidates = [];
423
518
  for (const m of text.matchAll(/[A-Za-z]:[\\/][^ \t\r\n"'|;&<>]*/g)) candidates.push(m[0]);
424
519
  for (const m of text.matchAll(/(?:^|[\s"'=])((?:\/(?!\/))[^ \t\r\n"'|;&<>]*)/g)) candidates.push(m[1]);
520
+ // Alvos relativos (ex.: `echo x > link/arquivo`) também podem atravessar
521
+ // um symlink. Capturamos apenas tokens com separador, ignorando flags/URLs.
522
+ for (const m of text.matchAll(/(?:^|[\s"'=<>])((?:\.?[\\/])?[A-Za-z0-9_.@-]+(?:[\\/][A-Za-z0-9_.@-]+)+)/g)) {
523
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(m[1])) candidates.push(m[1]);
524
+ }
425
525
  for (const raw of candidates) {
426
- const abs = path.resolve(raw);
427
- if (abs !== root && !abs.startsWith(root + path.sep)) {
526
+ const abs = path.resolve(root, raw);
527
+ if (!_scopeContains(root, abs)) {
428
528
  return `CAPABILITY_DENIED: comando mutante referencia "${raw}", fora da raiz autorizada "${root}".`;
429
529
  }
430
530
  }
@@ -553,9 +653,7 @@ async function execute(name, input, opts = {}) {
553
653
  }
554
654
  const _outsideScope = (p) => {
555
655
  if (!opts.confineDir) return false;
556
- const root = path.resolve(opts.confineDir);
557
- const abs = path.resolve(p);
558
- return abs !== root && !abs.startsWith(root + path.sep);
656
+ return !_scopeContains(opts.confineDir, p);
559
657
  };
560
658
  const _scopeError = (p) => ({ erro: `CAPABILITY_DENIED: "${path.resolve(p)}" está fora da raiz autorizada "${path.resolve(opts.confineDir)}".` });
561
659
  try {
@@ -563,6 +661,7 @@ async function execute(name, input, opts = {}) {
563
661
  case 'executar_comando': {
564
662
  const comando = String(input.comando || '');
565
663
  { const scope = _commandScopeViolation(comando, opts.confineDir); if (scope) return { erro: scope }; }
664
+ { const exfil = secretExfiltrationReason(comando); if (exfil) return { erro: exfil }; }
566
665
  // RECUSA INSTANTÂNEA (defesa em profundidade — o gate do agent.js já pega antes):
567
666
  // comando auto-destrutivo NUNCA roda e NUNCA espera aprovação.
568
667
  { const sd = selfDestructiveReason(comando, { cwd: baseDir }); if (sd) return { erro: sd }; }
@@ -570,14 +669,16 @@ async function execute(name, input, opts = {}) {
570
669
  // Barrar ANTES de rodar (com o conserto na mão) economiza o ciclo erro→tentativa→erro
571
670
  // que o agente repetia: python -c multilinha morria em silêncio, node -e com import()
572
671
  // quebrava no eval, e `timeout /t` (cmd) virava o timeout do coreutils sob git-bash.
573
- { const g = _shellGotcha(comando); if (g) return { erro: g }; }
672
+ const normalized = _normalizeWindowsReadOnlyCommand(comando);
673
+ const comandoExecutado = normalized ? normalized.command : comando;
674
+ { const g = _shellGotcha(comandoExecutado); if (g) return { erro: g }; }
574
675
  // Comandos de INSTALL/BUILD (apt/npm/pip/make/gcc/docker build…) levam muito mais que 60s —
575
676
  // sem isso o apt-get de vários pacotes MORRIA no timeout padrão (nada instalava, em silêncio).
576
677
  // Eles ganham 10min de default (até 30min se o agente pedir timeout_s); resto segue 60s.
577
- const _slow = /(^|[\s;|&(])(apt|apt-get|aptitude|dpkg|yum|dnf|zypper|pacman|snap|brew|pip|pip3|npm|yarn|pnpm|gem|cargo|go\s+(get|build|install)|make|cmake|meson|ninja|gcc|g\+\+|clang|mvn|gradle|\.\/gradlew|dotnet|composer|bundle|flutter\s+(build|pub)|docker\s+(build|pull|compose)|configure|\.\/configure|\.\/build)([\s;|&)]|$)/i.test(comando);
678
+ const _slow = /(^|[\s;|&(])(apt|apt-get|aptitude|dpkg|yum|dnf|zypper|pacman|snap|brew|pip|pip3|npm|yarn|pnpm|gem|cargo|go\s+(get|build|install)|make|cmake|meson|ninja|gcc|g\+\+|clang|mvn|gradle|\.\/gradlew|dotnet|composer|bundle|flutter\s+(build|pub)|docker\s+(build|pull|compose)|configure|\.\/configure|\.\/build)([\s;|&)]|$)/i.test(comandoExecutado);
578
679
  const _dflt = _slow ? 600 : 60;
579
680
  const timeout = Math.min(1800, Math.max(5, Number(input.timeout_s) || _dflt)) * 1000;
580
- const cmd = process.platform === 'win32' ? `chcp 65001>nul & ${comando}` : comando;
681
+ const cmd = process.platform === 'win32' ? `chcp 65001>nul & ${comandoExecutado}` : comandoExecutado;
581
682
  return await new Promise((res) => {
582
683
  require('child_process').exec(cmd, { timeout, shell: true, windowsHide: true, maxBuffer: 4 * 1024 * 1024, cwd: fs.existsSync(baseDir) ? baseDir : undefined }, (e, out, err) => {
583
684
  const c = compactor.compact(comando, out || '', { codigo: e?.code ?? 0 });
@@ -585,6 +686,7 @@ async function execute(name, input, opts = {}) {
585
686
  const _rd = require('./core').redactSecrets;
586
687
  const r = { stdout: _rd(c.out), stderr: _rd(compactor.stripAnsi(err || '').slice(0, 1500)), codigo: e?.code ?? 0 };
587
688
  if (c.note) r.aviso = c.note;
689
+ if (normalized) r.normalizado = normalized.note;
588
690
  if (!r.stdout.trim() && !r.stderr.trim() && r.codigo === 0 && /python3? -c/.test(comando) && comando.includes('\n'))
589
691
  r.aviso = 'stdout VAZIO: no Windows, python -c multi-linha falha em silêncio. ESCREVA um .py com escrever_arquivo e rode "python arquivo.py".';
590
692
  res(r);
@@ -683,16 +785,20 @@ async function execute(name, input, opts = {}) {
683
785
  _rebased = p;
684
786
  }
685
787
  }
686
- fs.mkdirSync(path.dirname(p), { recursive: true });
687
- const existed = fs.existsSync(p);
688
- // O caminho do backup era DESCARTADO aqui. Agora volta no resultado (_backup) pra
689
- // o loop registrar no checkpoint da sessão e permitir o "desfaz tudo".
690
- const _bk = existed ? _snapshot(p) : null; // backup antes de sobrescrever
691
- fs.writeFileSync(p, String(input.conteudo ?? ''), 'utf8');
692
- const r = { ok: true, caminho: p, bytes: Buffer.byteLength(String(input.conteudo ?? '')), sobrescreveu: existed };
693
- r._backup = _bk || ''; r._acao = existed ? 'alterado' : 'criado';
694
- if (_rebased) r.aviso = 'Caminho fora do diretório de trabalho foi RE-BASEADO para dentro dele. Use ESTE caminho a partir de agora: ' + p;
695
- return r;
788
+ if (_outsideScope(p)) return _scopeError(p);
789
+ const held = _acquireFileMutation(p); if (held.erro) return { erro: held.erro };
790
+ try {
791
+ fs.mkdirSync(path.dirname(p), { recursive: true });
792
+ const existed = fs.existsSync(p);
793
+ // O caminho do backup era DESCARTADO aqui. Agora volta no resultado (_backup) pra
794
+ // o loop registrar no checkpoint da sessão e permitir o "desfaz tudo".
795
+ const _bk = existed ? _snapshot(p) : null; // backup antes de sobrescrever
796
+ fs.writeFileSync(p, String(input.conteudo ?? ''), 'utf8');
797
+ const r = { ok: true, caminho: p, bytes: Buffer.byteLength(String(input.conteudo ?? '')), sobrescreveu: existed };
798
+ r._backup = _bk || ''; r._acao = existed ? 'alterado' : 'criado';
799
+ if (_rebased) r.aviso = 'Caminho fora do diretório de trabalho foi RE-BASEADO para dentro dele. Use ESTE caminho a partir de agora: ' + p;
800
+ return r;
801
+ } finally { held.lock.release(); }
696
802
  }
697
803
  case 'editar_arquivo': {
698
804
  // Edição por ÂNCORA (ideia do hashline do OMYP): troca SÓ o trecho buscado, sem
@@ -708,6 +814,9 @@ async function execute(name, input, opts = {}) {
708
814
  p = path.join(base, i >= 0 ? parts.slice(i).join(path.sep) : parts[parts.length - 1]);
709
815
  }
710
816
  }
817
+ if (_outsideScope(p)) return _scopeError(p);
818
+ const held = _acquireFileMutation(p); if (held.erro) return { erro: held.erro };
819
+ try {
711
820
  if (!fs.existsSync(p)) {
712
821
  const v = _vizinhos(p);
713
822
  return { erro: 'Arquivo não existe: ' + p + (v.length ? '. Nomes parecidos na mesma pasta: ' + v.join(', ') + ' — confira se não errou o nome.' : '') + ' Pra criar arquivo NOVO use escrever_arquivo.' };
@@ -733,10 +842,13 @@ async function execute(name, input, opts = {}) {
733
842
  const _bk2 = _snapshot(p); // backup antes de editar (reversível via restaurar_arquivo)
734
843
  fs.writeFileSync(p, novo, 'utf8');
735
844
  return { ok: true, caminho: p, ocorrencias: input.todas ? count : 1, bytes_antes: Buffer.byteLength(orig), bytes_depois: Buffer.byteLength(novo), _backup: _bk2 || '', _acao: 'alterado' };
845
+ } finally { held.lock.release(); }
736
846
  }
737
847
  case 'restaurar_arquivo': {
738
848
  const p = _abs(input.caminho, baseDir);
739
849
  if (_outsideScope(p)) return _scopeError(p);
850
+ const held = _acquireFileMutation(p); if (held.erro) return { erro: held.erro };
851
+ try {
740
852
  const dir = path.join(BK_DIR, _bkKey(p));
741
853
  let baks = [];
742
854
  try { baks = fs.readdirSync(dir).filter(f => f.endsWith('.bak')).sort(); } catch (_) {}
@@ -745,6 +857,7 @@ async function execute(name, input, opts = {}) {
745
857
  _snapshot(p); // guarda o estado ATUAL antes de reverter (permite "refazer")
746
858
  fs.copyFileSync(ultimo, p);
747
859
  return { ok: true, caminho: p, restaurado: true, de_backup: path.basename(ultimo) };
860
+ } finally { held.lock.release(); }
748
861
  }
749
862
  case 'listar_diretorio': {
750
863
  const p = _abs(input.caminho || '.', baseDir);
@@ -909,18 +1022,24 @@ async function execute(name, input, opts = {}) {
909
1022
  if (_outsideScope(target)) return _scopeError(target);
910
1023
  const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
911
1024
  if (copy && _outsideScope(copy)) return _scopeError(copy);
912
- const result = await require('./office-editors').editarDocumento({ ...input, caminho: target, caminho_copia: copy });
913
- if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
914
- return result;
1025
+ const held = _acquireFileMutation(target); if (held.erro) return { erro: held.erro };
1026
+ try {
1027
+ const result = await require('./office-editors').editarDocumento({ ...input, caminho: target, caminho_copia: copy });
1028
+ if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
1029
+ return result;
1030
+ } finally { held.lock.release(); }
915
1031
  }
916
1032
  case 'editar_planilha': {
917
1033
  const target = _abs(input.caminho, baseDir);
918
1034
  if (_outsideScope(target)) return _scopeError(target);
919
1035
  const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
920
1036
  if (copy && _outsideScope(copy)) return _scopeError(copy);
921
- const result = await require('./office-editors').editarPlanilha({ ...input, caminho: target, caminho_copia: copy });
922
- if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
923
- return result;
1037
+ const held = _acquireFileMutation(target); if (held.erro) return { erro: held.erro };
1038
+ try {
1039
+ const result = await require('./office-editors').editarPlanilha({ ...input, caminho: target, caminho_copia: copy });
1040
+ if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
1041
+ return result;
1042
+ } finally { held.lock.release(); }
924
1043
  }
925
1044
  case 'ler_planilha': {
926
1045
  const target = _abs(input.caminho, baseDir);
@@ -1045,4 +1164,5 @@ async function execute(name, input, opts = {}) {
1045
1164
  } catch (e) { return { erro: String((e && e.message) || e).slice(0, 400) }; }
1046
1165
  }
1047
1166
 
1048
- module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, buildProjectMap, _test: { shellGotcha: _shellGotcha } };
1167
+ module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, secretExfiltrationReason, buildProjectMap,
1168
+ _test: { shellGotcha: _shellGotcha, normalizeWindowsReadOnlyCommand: _normalizeWindowsReadOnlyCommand } };
package/lib/ui.js CHANGED
@@ -129,6 +129,11 @@ const _TOOL_PT = {
129
129
  navegador: 'Navegando na web', explorar: 'Explorando o projeto', lembrar: 'Salvando na memória',
130
130
  skill_gerenciar: 'Ajustando uma habilidade', compactar_contexto: 'Resumindo o histórico',
131
131
  pedir_ajuda_humano: 'Pedindo sua ajuda', ler_planilha: 'Lendo a planilha',
132
+ ler_planilha_office_web: 'Lendo a planilha', editar_planilha_office_web: 'Editando a planilha',
133
+ ler_documento_office_web: 'Lendo o documento', editar_documento_office_web: 'Editando o documento',
134
+ ler_apresentacao_office_web: 'Lendo a apresentação', editar_apresentacao_office_web: 'Editando a apresentação',
135
+ google_mail_list: 'Verificando e-mails', google_mail_search: 'Procurando e-mails', google_mail_draft: 'Criando rascunho de e-mail',
136
+ microsoft_mail_list: 'Verificando e-mails', microsoft_mail_search: 'Procurando e-mails', microsoft_mail_draft: 'Criando rascunho de e-mail',
132
137
  };
133
138
  const _TOOL_EN = {
134
139
  executar_comando: 'Running command', executar_remoto: 'Running on the server',
@@ -139,6 +144,11 @@ const _TOOL_EN = {
139
144
  navegador: 'Browsing the web', explorar: 'Exploring the project', lembrar: 'Saving to memory',
140
145
  skill_gerenciar: 'Tuning a skill', compactar_contexto: 'Summarizing history',
141
146
  pedir_ajuda_humano: 'Asking for your help', ler_planilha: 'Reading the spreadsheet',
147
+ ler_planilha_office_web: 'Reading the spreadsheet', editar_planilha_office_web: 'Editing the spreadsheet',
148
+ ler_documento_office_web: 'Reading the document', editar_documento_office_web: 'Editing the document',
149
+ ler_apresentacao_office_web: 'Reading the presentation', editar_apresentacao_office_web: 'Editing the presentation',
150
+ google_mail_list: 'Checking email', google_mail_search: 'Searching email', google_mail_draft: 'Creating an email draft',
151
+ microsoft_mail_list: 'Checking email', microsoft_mail_search: 'Searching email', microsoft_mail_draft: 'Creating an email draft',
142
152
  };
143
153
  function friendlyTool(name, en) {
144
154
  const map = en ? _TOOL_EN : _TOOL_PT;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.29",
3
+ "version": "0.97.30",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "node test/core.test.js && node test/audit-packs.test.js && node test/intelligence-core.test.js && node test/cloud-slug.test.js && node test/eval-model.test.js && node test/project-cache.test.js && node test/memory-bus.test.js && node test/capabilities.test.js && node test/mcp-e2e.test.js && node test/erros.test.js && node test/evolution-telemetry.test.js && node test/capability-pack.test.js && node test/video-generation.test.js && node test/image-job.test.js && node test/byok.test.js && node test/conhecimento.test.js && node test/policy.test.js && node test/temas.test.js && node test/skill-index.test.js && node test/doctor.test.js && node test/google-workspace-tools.test.js && node test/office-editors.test.js"
9
+ "test": "node test/core.test.js && node test/windows-shell-normalization.test.js && node test/gateways.test.js && node test/agent-recovery-guard.test.js && node test/agent-plan-model-contract.test.js && node test/agent-mission-limits.test.js && node test/agent-external-approval.test.js && node test/agent-prompt-injection.test.js && node test/file-concurrency.test.js && node test/mission-observability.test.js && node test/audit-packs.test.js && node test/intelligence-core.test.js && node test/cloud-slug.test.js && node test/eval-model.test.js && node test/project-cache.test.js && node test/memory-bus.test.js && node test/capabilities.test.js && node test/mcp-e2e.test.js && node test/erros.test.js && node test/evolution-telemetry.test.js && node test/owner-audit.test.js && node test/capability-pack.test.js && node test/video-generation.test.js && node test/image-job.test.js && node test/byok.test.js && node test/conhecimento.test.js && node test/policy.test.js && node test/temas.test.js && node test/skill-index.test.js && node test/doctor.test.js && node test/google-workspace-tools.test.js && node test/office-editors.test.js"
10
10
  },
11
11
  "files": [
12
12
  "bin",