terminal-smart-cli 0.97.12 → 0.97.14

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
@@ -54,6 +54,13 @@ const DEFS = [
54
54
  inicio: { type: 'number', description: 'linha inicial (1-based, opcional)' },
55
55
  fim: { type: 'number', description: 'linha final (opcional)' },
56
56
  }, required: ['caminho'] } } },
57
+ { type: 'function', function: { name: 'ler_arquivos',
58
+ description: 'Le em LOTE varios arquivos ou trechos de texto numa unica chamada. Prefira quando precisar inspecionar 2 ou mais arquivos/paginas; reduz tempo e custo.',
59
+ parameters: { type: 'object', properties: {
60
+ arquivos: { type: 'array', description: 'Ate 10 itens {caminho,inicio,fim}. Para arquivos grandes, use trechos de aproximadamente 100 linhas.', items: { type: 'object', properties: {
61
+ caminho: { type: 'string' }, inicio: { type: 'number' }, fim: { type: 'number' },
62
+ }, required: ['caminho'] } },
63
+ }, required: ['arquivos'] } } },
57
64
  { type: 'function', function: { name: 'escrever_arquivo',
58
65
  description: 'Cria ou sobrescreve um arquivo local com o conteúdo dado (cria as pastas se preciso). Para MUDAR UM TRECHO de arquivo que JÁ EXISTE, prefira editar_arquivo (mais barato e preciso).',
59
66
  parameters: { type: 'object', properties: {
@@ -162,11 +169,17 @@ const DEFS = [
162
169
  inicio_char: { type: 'number' }, tamanho: { type: 'number' },
163
170
  }, required: ['caminho'] } } },
164
171
  { type: 'function', function: { name: 'editar_documento',
165
- description: 'Edita um .docx EXISTENTE no próprio arquivo por padrão, preserva o restante e cria backup automático. Só cria outro arquivo se o usuário pedir uma cópia.',
172
+ description: 'Edita um .docx EXISTENTE no próprio arquivo por padrão, preserva o restante e cria backup automático. Para várias mudanças no mesmo DOCX, agrupe todas em substituicoes numa ÚNICA chamada atômica. Só cria outro arquivo se o usuário pedir uma cópia.',
166
173
  parameters: { type: 'object', properties: {
167
174
  caminho: { type: 'string' }, buscar: { type: 'string' }, substituir: { type: 'string' },
168
175
  todas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, caminho_copia: { type: 'string' },
169
- }, required: ['caminho', 'buscar', 'substituir'] } } },
176
+ substituicoes: { type: 'array', description: 'Mudanças agrupadas e atômicas no mesmo DOCX.', items: { type: 'object', properties: {
177
+ buscar: { type: 'string' }, substituir: { type: 'string' }, todas: { type: 'boolean' },
178
+ }, required: ['buscar', 'substituir'] } },
179
+ alteracoes_tabela: { type: 'array', description: 'Altera atomicamente o valor da célula seguinte a um rótulo de tabela.', items: { type: 'object', properties: {
180
+ rotulo: { type: 'string' }, valor: { type: 'string' }, todas: { type: 'boolean' },
181
+ }, required: ['rotulo', 'valor'] } },
182
+ }, required: ['caminho'] } } },
170
183
  { type: 'function', function: { name: 'editar_planilha',
171
184
  description: 'Edita uma planilha .xlsx EXISTENTE no próprio arquivo por padrão. Altera células/fórmulas ou substitui texto, preserva as demais abas e cria backup automático. Só cria outro arquivo se o usuário pedir cópia.',
172
185
  parameters: { type: 'object', properties: {
@@ -179,6 +192,12 @@ const DEFS = [
179
192
  }, required: ['buscar', 'substituir'] } },
180
193
  criar_copia: { type: 'boolean' }, caminho_copia: { type: 'string' },
181
194
  }, required: ['caminho'] } } },
195
+ { type: 'function', function: { name: 'ler_planilha',
196
+ description: 'Lê uma planilha .xlsx existente de forma tipada, retornando endereços, valores, fórmulas e resultados em cache. Use antes e depois de editar; não adivinhe células.',
197
+ parameters: { type: 'object', properties: {
198
+ caminho: { type: 'string' }, aba: { type: 'string' }, intervalo: { type: 'string', description: 'Ex.: A1:I30' },
199
+ limite: { type: 'number' }, incluir_vazias: { type: 'boolean' },
200
+ }, required: ['caminho'] } } },
182
201
  { type: 'function', function: { name: 'status_microsoft365', description: 'Verifica a conexão com Outlook e Microsoft 365.', parameters: { type: 'object', properties: {} } } },
183
202
  { type: 'function', function: { name: 'conectar_microsoft365', description: 'Gera o link oficial para autorizar Outlook e Microsoft 365.', parameters: { type: 'object', properties: {} } } },
184
203
  { type: 'function', function: { name: 'listar_emails_outlook', description: 'Lista, pesquisa e prioriza e-mails do Outlook sem alterar nada.', parameters: { type: 'object', properties: { limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string' } } } } },
@@ -190,10 +209,12 @@ const DEFS = [
190
209
  { type: 'function', function: { name: 'obter_anexo_outlook', description: 'Obtém o link autenticado para baixar um anexo do Outlook.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, id_anexo: { type: 'string' } }, required: ['id_email','id_anexo'] } } },
191
210
  { type: 'function', function: { name: 'criar_rascunho_outlook', description: 'Cria rascunho no Outlook. Exige confirmação e não envia.', parameters: { type: 'object', properties: { para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' } }, required: ['para','assunto','corpo'] } } },
192
211
  { type: 'function', function: { name: 'enviar_rascunho_outlook', description: 'Envia um rascunho existente do Outlook. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
193
- { type: 'function', function: { name: 'status_google_workspace', description: 'Verifica a conexão com Gmail, Drive, Docs e Sheets.', parameters: { type: 'object', properties: {} } } },
194
- { type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link oficial para autorizar Gmail, Drive, Docs e Sheets.', parameters: { type: 'object', properties: {} } } },
195
- { type: 'function', function: { name: 'listar_emails_gmail', description: 'Lista e prioriza e-mails do Gmail, sem alterar nada.', parameters: { type: 'object', properties: {
196
- limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string', enum: ['inbox','sent','drafts','spam','trash','starred','important','all'] }
212
+ { type: 'function', function: { name: 'status_google_workspace', description: 'Verifica a conexão Google e informa se o acesso é Essencial ou Avançado.', parameters: { type: 'object', properties: {} } } },
213
+ { type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link Google. Essencial acessa arquivos escolhidos/criados pelo app; Avançado beta inclui Gmail completo e pesquisa global do Drive.', parameters: { type: 'object', properties: {
214
+ nivel: { type: 'string', enum: ['essencial', 'avancado'] }
215
+ } } } },
216
+ { type: 'function', function: { name: 'listar_emails_gmail', description: 'Lista e prioriza e-mails do Gmail, sem alterar nada. Use pagina com nextPageToken para continuar em lotes.', parameters: { type: 'object', properties: {
217
+ limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string', enum: ['inbox','sent','drafts','spam','trash','starred','important','all'] }, pagina: { type: 'string' }
197
218
  } } } },
198
219
  { type: 'function', function: { name: 'ler_email_gmail', description: 'Lê o corpo completo de um e-mail ou a conversa inteira sem marcar como lido.', parameters: { type: 'object', properties: {
199
220
  id_email: { type: 'string' }, conversa_completa: { type: 'boolean' }
@@ -215,9 +236,13 @@ const DEFS = [
215
236
  para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' }
216
237
  }, required: ['para', 'assunto', 'corpo'] } } },
217
238
  { type: 'function', function: { name: 'enviar_rascunho_gmail', description: 'Envia rascunho existente do Gmail. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
218
- { type: 'function', function: { name: 'listar_arquivos_drive', description: 'Lista arquivos no Google Drive por nome ou tipo.', parameters: { type: 'object', properties: {
239
+ { type: 'function', function: { name: 'listar_arquivos_drive', description: 'Lista arquivos acessíveis no Drive; no Essencial limita-se aos escolhidos/criados pelo app e no Avançado pesquisa todo o Drive.', parameters: { type: 'object', properties: {
219
240
  busca: { type: 'string' }, tipo_mime: { type: 'string' }, limite: { type: 'number' }
220
241
  } } } },
242
+ { type: 'function', function: { name: 'selecionar_arquivos_drive', description: 'Fornece o link da Central de Integrações para escolher arquivos existentes no Google Drive.', parameters: { type: 'object', properties: {} } } },
243
+ { type: 'function', function: { name: 'criar_google_docs', description: 'Cria um novo Google Docs. Exige confirmação.', parameters: { type: 'object', properties: {
244
+ nome: { type: 'string' }, texto_inicial: { type: 'string' }, id_pasta: { type: 'string' }
245
+ }, required: ['nome'] } } },
221
246
  { type: 'function', function: { name: 'ler_google_docs', description: 'Lê texto de Google Docs pelo ID.', parameters: { type: 'object', properties: { id_documento: { type: 'string' } }, required: ['id_documento'] } } },
222
247
  { type: 'function', function: { name: 'editar_google_docs', description: 'Edita o Google Docs original por padrão; cria cópia somente se solicitado. Exige confirmação.', parameters: { type: 'object', properties: {
223
248
  id_documento: { type: 'string' }, localizar: { type: 'string' }, substituir_por: { type: 'string' }, diferenciar_maiusculas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
@@ -225,6 +250,9 @@ const DEFS = [
225
250
  { type: 'function', function: { name: 'ler_google_sheets', description: 'Lê células de Google Sheets pelo ID e intervalo.', parameters: { type: 'object', properties: {
226
251
  id_planilha: { type: 'string' }, intervalo: { type: 'string' }
227
252
  }, required: ['id_planilha'] } } },
253
+ { type: 'function', function: { name: 'criar_google_sheets', description: 'Cria uma nova Google Sheets e pode preencher valores iniciais. Exige confirmação.', parameters: { type: 'object', properties: {
254
+ nome: { type: 'string' }, intervalo: { type: 'string' }, valores: { type: 'array', items: { type: 'array', items: {} } }, id_pasta: { type: 'string' }
255
+ }, required: ['nome'] } } },
228
256
  { type: 'function', function: { name: 'editar_google_sheets', description: 'Edita a Google Sheets original por padrão; cria cópia somente se solicitado. Exige confirmação.', parameters: { type: 'object', properties: {
229
257
  id_planilha: { type: 'string' }, intervalo: { type: 'string' }, valores: { type: 'array', items: { type: 'array', items: {} } }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
230
258
  }, required: ['id_planilha', 'intervalo', 'valores'] } } },
@@ -364,9 +392,18 @@ function _shellGotcha(cmd) {
364
392
  return 'python -c MULTILINHA falha em silêncio no Windows. Escreva um arquivo .py com escrever_arquivo e rode "python arquivo.py".';
365
393
  if (/\bnode\s+-e\b/.test(s) && (multi || /\bimport\s*\(/.test(s) || /\bawait\b/.test(s)))
366
394
  return 'node -e MULTILINHA ou com import()/await de topo falha no eval. Escreva um arquivo .mjs com escrever_arquivo e rode "node arquivo.mjs".';
367
- // timeout /t é do cmd; sob git-bash pega o timeout do coreutils e dá "invalid time interval"
368
- if (/\btimeout\s+\/t\b/i.test(s))
369
- return 'Este shell é bash (não cmd): "timeout /t N" falha. Para esperar use "sleep N". Comandos só-do-cmd (dir, type, if exist) rode via: cmd /c "…".';
395
+ // child_process.exec usa cmd.exe no Windows. Alguns comandos POSIX têm outro
396
+ // significado: `mkdir -p app` cria por engano uma pasta literal chamada `-p`.
397
+ if (process.platform === 'win32' && /(^|[&|]\s*|\s)mkdir\s+-p(?:\s|$)/i.test(s))
398
+ return 'Este shell é cmd.exe: "mkdir -p" criaria uma pasta chamada "-p". Use "mkdir pasta" ou PowerShell New-Item.';
399
+ // Bloqueie apenas `ls` como COMANDO. `npm ls` e texto dentro de um PowerShell
400
+ // explicito sao validos; o antigo `\sls` gerava falso positivo nos dois casos.
401
+ if (process.platform === 'win32'
402
+ && !/^\s*(?:powershell|pwsh)\b/i.test(s)
403
+ && /(?:^|(?:&&|\|\||[&|])\s*)ls(?:\s|$)/i.test(s))
404
+ return 'Este shell é cmd.exe: use "dir" (ou chame PowerShell explicitamente) em vez de "ls".';
405
+ if (process.platform === 'win32' && /(?:^|\s)\/[a-zA-Z]\//.test(s))
406
+ return 'Caminho no formato /c/... não é válido no cmd.exe. Use C:\\... ou um caminho relativo ao diretório de trabalho.';
370
407
  // matar processo por IMAGEM mata tudo com esse nome (inclusive a própria missão)
371
408
  if (/\btaskkill\b[^\n]*\/im\b/i.test(s))
372
409
  return 'taskkill /IM mata TODOS os processos com esse nome (inclusive os do usuário e possivelmente o próprio ts). Mate pelo PID da PORTA: netstat -ano | findstr :PORTA → taskkill /PID <pid> /F.';
@@ -609,6 +646,25 @@ async function execute(name, input, opts = {}) {
609
646
  if (total > MAXL) return { conteudo: linhas.slice(0, MAXL).join('\n'), linhas_total: total, aviso: `Arquivo grande (${total} linhas): mostrando 1-${MAXL}. Chame de novo com inicio/fim.` };
610
647
  return { conteudo: txt, linhas_total: total };
611
648
  }
649
+ case 'ler_arquivos': {
650
+ const arquivos = Array.isArray(input.arquivos) ? input.arquivos.slice(0, 10) : [];
651
+ if (!arquivos.length) return { erro: 'Informe arquivos com pelo menos um item {caminho,inicio,fim}.' };
652
+ const resultados = [];
653
+ let caracteres = 0;
654
+ const LIMITE_TOTAL = 22000;
655
+ for (const item of arquivos) {
656
+ if (!item || !item.caminho) continue;
657
+ const r = await execute('ler_arquivo', item, opts);
658
+ let conteudo = r && typeof r.conteudo === 'string' ? r.conteudo : '';
659
+ const restante = Math.max(0, LIMITE_TOTAL - caracteres);
660
+ if (conteudo.length > restante) conteudo = conteudo.slice(0, restante) + '\n...[lote truncado; solicite um trecho menor]';
661
+ const saida = Object.assign({ caminho: item.caminho }, r || {}, conteudo ? { conteudo } : {});
662
+ resultados.push(saida);
663
+ caracteres += conteudo.length;
664
+ if (caracteres >= LIMITE_TOTAL) break;
665
+ }
666
+ return { total: resultados.length, resultados, aviso: resultados.length < arquivos.length ? 'Lote limitado pelo teto de contexto; peca os trechos restantes em outro lote.' : undefined };
667
+ }
612
668
  case 'escrever_arquivo': {
613
669
  let p = _abs(input.caminho, baseDir);
614
670
  { const g = _guardTsHome(p); if (g) return { erro: g }; }
@@ -693,7 +749,8 @@ async function execute(name, input, opts = {}) {
693
749
  case 'listar_diretorio': {
694
750
  const p = _abs(input.caminho || '.', baseDir);
695
751
  if (_outsideScope(p)) return _scopeError(p);
696
- const items = fs.readdirSync(p, { withFileTypes: true }).slice(0, 200).map(d => {
752
+ const internos = new Set(['.ts-episodios.json', '.ts-erros.json', '.ts-indice.json']);
753
+ const items = fs.readdirSync(p, { withFileTypes: true }).filter(d => !internos.has(d.name)).slice(0, 200).map(d => {
697
754
  let size = null; try { if (d.isFile()) size = fs.statSync(path.join(p, d.name)).size; } catch (_) {}
698
755
  return { nome: d.name, tipo: d.isDirectory() ? 'dir' : 'arquivo', bytes: size };
699
756
  });
@@ -865,6 +922,11 @@ async function execute(name, input, opts = {}) {
865
922
  if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
866
923
  return result;
867
924
  }
925
+ case 'ler_planilha': {
926
+ const target = _abs(input.caminho, baseDir);
927
+ if (_outsideScope(target)) return _scopeError(target);
928
+ return await require('./office-readers').lerPlanilha({ ...input, caminho: target });
929
+ }
868
930
  case 'status_microsoft365':
869
931
  return await require('./api').api('/api/integrations/microsoft/status', { token: opts.token });
870
932
  case 'conectar_microsoft365': {
@@ -889,20 +951,24 @@ async function execute(name, input, opts = {}) {
889
951
  return await require('./api').api('/api/integrations/microsoft/mail/forwards', { method: 'POST', token: opts.token, body: { messageId: input.id_email, to: input.para, cc: input.cc || [], body: input.corpo || '', confirmed: true } });
890
952
  case 'obter_anexo_outlook':
891
953
  return { downloadUrl: '/api/integrations/microsoft/mail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
892
- case 'criar_rascunho_outlook':
893
- return await require('./api').api('/api/integrations/microsoft/mail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
954
+ case 'criar_rascunho_outlook': {
955
+ const data = await require('./api').api('/api/integrations/microsoft/mail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
956
+ return { ...data, rascunho_id: data.rascunho_id || data.draftId || data.draft?.id || data.id || null };
957
+ }
894
958
  case 'enviar_rascunho_outlook':
895
959
  return await require('./api').api('/api/integrations/microsoft/mail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
896
960
  case 'status_google_workspace':
897
961
  return await require('./api').api('/api/integrations/google/status', { token: opts.token });
898
962
  case 'conectar_google_workspace': {
899
- const data = await require('./api').api('/api/integrations/google/connect', { method: 'POST', body: { source: 'cli' }, token: opts.token });
900
- return { ok: true, authUrl: data.authUrl, instrucao: 'Abra o link no navegador, autorize sua conta Google e volte ao Terminal Smart.' };
963
+ const accessLevel = String(input.nivel || '').toLowerCase() === 'avancado' ? 'advanced' : 'essential';
964
+ const data = await require('./api').api('/api/integrations/google/connect', { method: 'POST', body: { source: 'cli', accessLevel }, token: opts.token });
965
+ return { ok: true, accessLevel, authUrl: data.authUrl, instrucao: `Abra o link no navegador, autorize o acesso Google ${accessLevel === 'advanced' ? 'Avançado (beta)' : 'Essencial'} e volte ao Terminal Smart.` };
901
966
  }
902
967
  case 'listar_emails_gmail': {
903
968
  const p = new URLSearchParams({ limit: String(Math.min(30, Math.max(1, parseInt(input.limite) || 15))) });
904
969
  if (input.somente_nao_lidos) p.set('unread', '1'); if (input.priorizar) p.set('priority', '1');
905
970
  if (input.consulta) p.set('query', input.consulta); if (input.pasta) p.set('folder', input.pasta);
971
+ if (input.pagina) p.set('page', input.pagina);
906
972
  return await require('./api').api('/api/integrations/google/gmail/messages?' + p, { token: opts.token });
907
973
  }
908
974
  case 'ler_email_gmail': {
@@ -919,15 +985,21 @@ async function execute(name, input, opts = {}) {
919
985
  return await require('./api').api('/api/integrations/google/gmail/forwards', { method: 'POST', token: opts.token, body: { messageId: input.id_email, to: input.para, cc: input.cc || [], body: input.corpo || '', confirmed: true } });
920
986
  case 'obter_anexo_gmail':
921
987
  return { downloadUrl: '/api/integrations/google/gmail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
922
- case 'criar_rascunho_gmail':
923
- return await require('./api').api('/api/integrations/google/gmail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
988
+ case 'criar_rascunho_gmail': {
989
+ const data = await require('./api').api('/api/integrations/google/gmail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
990
+ return { ...data, id_rascunho: data.id_rascunho || data.draftId || data.draft?.id || data.id || null };
991
+ }
924
992
  case 'enviar_rascunho_gmail':
925
- return await require('./api').api('/api/integrations/google/gmail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
993
+ return await require('./api').api('/api/integrations/google/gmail/send', { method: 'POST', token: opts.token, body: { draftId: input.id_rascunho, confirmed: true } });
926
994
  case 'listar_arquivos_drive': {
927
995
  const p = new URLSearchParams({ limit: String(Math.min(100, Math.max(1, parseInt(input.limite) || 30))) });
928
996
  if (input.busca) p.set('query', input.busca); if (input.tipo_mime) p.set('mimeType', input.tipo_mime);
929
997
  return await require('./api').api('/api/integrations/google/drive/files?' + p, { token: opts.token });
930
998
  }
999
+ case 'selecionar_arquivos_drive':
1000
+ return { url: 'https://terminalsmart.com.br/integracoes#google-files', instrucao: 'Abra o link e clique em Escolher arquivos do Drive.' };
1001
+ case 'criar_google_docs':
1002
+ return await require('./api').api('/api/integrations/google/docs/create', { method: 'POST', token: opts.token, body: { name: input.nome, text: input.texto_inicial || '', folderId: input.id_pasta, confirmed: true } });
931
1003
  case 'ler_google_docs':
932
1004
  return await require('./api').api('/api/integrations/google/docs/document?id=' + encodeURIComponent(input.id_documento), { token: opts.token });
933
1005
  case 'editar_google_docs':
@@ -936,6 +1008,8 @@ async function execute(name, input, opts = {}) {
936
1008
  const p = new URLSearchParams({ id: input.id_planilha }); if (input.intervalo) p.set('range', input.intervalo);
937
1009
  return await require('./api').api('/api/integrations/google/sheets/values?' + p, { token: opts.token });
938
1010
  }
1011
+ case 'criar_google_sheets':
1012
+ return await require('./api').api('/api/integrations/google/sheets/create', { method: 'POST', token: opts.token, body: { name: input.nome, range: input.intervalo || 'A1', values: input.valores || [], folderId: input.id_pasta, confirmed: true } });
939
1013
  case 'editar_google_sheets':
940
1014
  return await require('./api').api('/api/integrations/google/sheets/update', { method: 'POST', token: opts.token, body: { spreadsheetId: input.id_planilha, range: input.intervalo, values: input.valores, createCopy: input.criar_copia === true, copyName: input.nome_copia, confirmed: true } });
941
1015
  case 'ler_apresentacao': {
@@ -971,4 +1045,4 @@ async function execute(name, input, opts = {}) {
971
1045
  } catch (e) { return { erro: String((e && e.message) || e).slice(0, 400) }; }
972
1046
  }
973
1047
 
974
- module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, buildProjectMap };
1048
+ module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, buildProjectMap, _test: { shellGotcha: _shellGotcha } };
@@ -66,6 +66,15 @@ function cellRow(address) {
66
66
  return Number(address.match(/\d+$/)[0]);
67
67
  }
68
68
 
69
+ function addressInRange(address, range) {
70
+ if (!range) return true;
71
+ const match = String(range).toUpperCase().match(/^([A-Z]+\d+)(?::([A-Z]+\d+))?$/);
72
+ if (!match) throw new Error(`Intervalo inválido: ${range}`);
73
+ const start = match[1]; const end = match[2] || start;
74
+ return cellColumn(address) >= cellColumn(start) && cellColumn(address) <= cellColumn(end)
75
+ && cellRow(address) >= cellRow(start) && cellRow(address) <= cellRow(end);
76
+ }
77
+
69
78
  function setType(openTag, type) {
70
79
  let tag = openTag.replace(/\s+t="[^"]*"/i, '');
71
80
  if (type) tag = tag.replace(/\s*\/?>$/, ` t="${type}">`);
@@ -157,6 +166,42 @@ function parseSharedStrings(xml) {
157
166
  );
158
167
  }
159
168
 
169
+ function inspectCell(cellXml, sharedStrings) {
170
+ const open = cellXml.match(/^<[^>]+>/)?.[0] || '';
171
+ const type = attr(open, 't');
172
+ const formula = cellXml.match(/<(?:\w+:)?f\b[^>]*>([\s\S]*?)<\/(?:\w+:)?f>/i)?.[1];
173
+ const raw = cellXml.match(/<(?:\w+:)?v\b[^>]*>([\s\S]*?)<\/(?:\w+:)?v>/i)?.[1];
174
+ let value = cellText(cellXml, sharedStrings);
175
+ if (value == null && raw != null) {
176
+ const decoded = xmlDecode(raw);
177
+ if (type === 'b') value = decoded === '1';
178
+ else value = Number.isFinite(Number(decoded)) ? Number(decoded) : decoded;
179
+ }
180
+ return { valor: value, formula: formula == null ? undefined : xmlDecode(formula), resultado_cache: formula == null ? undefined : value };
181
+ }
182
+
183
+ async function readXlsxCompat(source, input = {}) {
184
+ const zip = await JSZip.loadAsync(fs.readFileSync(source));
185
+ const sheets = await workbookSheets(zip);
186
+ const selected = input.aba
187
+ ? sheets.find(sheet => sheet.name.toLowerCase() === String(input.aba).toLowerCase())
188
+ : sheets[0];
189
+ if (!selected) throw new Error(`Aba não encontrada: ${input.aba}`);
190
+ const sharedXml = zip.file('xl/sharedStrings.xml') ? await zip.file('xl/sharedStrings.xml').async('string') : '';
191
+ const sharedStrings = parseSharedStrings(sharedXml);
192
+ const xml = await zip.file(selected.file).async('string');
193
+ const cells = [];
194
+ for (const match of xml.matchAll(/(?:<(?:\w+:)?c\b(?=[^>]*\br="([A-Z]+\d+)")[^>]*\/>|<(?:\w+:)?c\b(?=[^>]*\br="([A-Z]+\d+)")[^>]*>[\s\S]*?<\/(?:\w+:)?c>)/gi)) {
195
+ const address = match[1] || match[2];
196
+ if (!addressInRange(address, input.intervalo)) continue;
197
+ const inspected = inspectCell(match[0], sharedStrings);
198
+ if (inspected.valor == null && inspected.formula == null && !input.incluir_vazias) continue;
199
+ cells.push({ celula: address, ...inspected });
200
+ if (cells.length >= Math.min(2000, Math.max(1, Number(input.limite) || 500))) break;
201
+ }
202
+ return { abas: sheets.map(sheet => sheet.name), aba: selected.name, intervalo: input.intervalo || 'usado', celulas: cells };
203
+ }
204
+
160
205
  function substituteInSheet(xml, rule, sharedStrings) {
161
206
  const search = String(rule.buscar || '');
162
207
  if (!search) return { xml, changes: [] };
@@ -180,6 +225,28 @@ function substituteInSheet(xml, rule, sharedStrings) {
180
225
  return { xml: changes.length ? output + xml.slice(cursor) : xml, changes };
181
226
  }
182
227
 
228
+ function invalidateFormulaCaches(xml) {
229
+ let count = 0;
230
+ const output = String(xml).replace(/<(?:\w+:)?c\b[^>]*>[\s\S]*?<\/(?:\w+:)?c>/gi, cell => {
231
+ if (!/<(?:\w+:)?f\b/i.test(cell)) return cell;
232
+ const next = cell.replace(/<(?:\w+:)?v\b[^>]*>[\s\S]*?<\/(?:\w+:)?v>/gi, '');
233
+ if (next !== cell) count++;
234
+ return next;
235
+ });
236
+ return { xml: output, count };
237
+ }
238
+
239
+ function requestFullCalculation(xml) {
240
+ const source = String(xml || '');
241
+ if (/<(?:\w+:)?calcPr\b/i.test(source)) {
242
+ return source.replace(/<(\w+:)?calcPr\b([^>]*?)\/?\s*>/i, (_all, prefix = '', attrs = '') => {
243
+ const clean = attrs.replace(/\s+fullCalcOnLoad="[^"]*"/i, '');
244
+ return `<${prefix || ''}calcPr${clean} fullCalcOnLoad="1"/>`;
245
+ });
246
+ }
247
+ return source.replace(/<\/(\w+:)?workbook>\s*$/i, '<$1calcPr fullCalcOnLoad="1"/></$1workbook>');
248
+ }
249
+
183
250
  async function editXlsxCompat(source, destination, input = {}) {
184
251
  const zip = await JSZip.loadAsync(fs.readFileSync(source));
185
252
  const sheets = await workbookSheets(zip);
@@ -219,10 +286,19 @@ async function editXlsxCompat(source, destination, input = {}) {
219
286
  if (!replaced) throw new Error(`Texto não encontrado na planilha: ${String(rule.buscar || '').slice(0, 80)}`);
220
287
  }
221
288
  if (!changes.length) throw new Error('Informe alteracoes ou substituicoes.');
289
+ let formulasInvalidated = 0;
290
+ for (const sheet of sheets) {
291
+ const invalidated = invalidateFormulaCaches(await readXml(sheet));
292
+ xmlByFile.set(sheet.file, invalidated.xml);
293
+ formulasInvalidated += invalidated.count;
294
+ }
222
295
  for (const [file, xml] of xmlByFile) zip.file(file, xml);
296
+ const workbookXml = await zip.file('xl/workbook.xml').async('string');
297
+ zip.file('xl/workbook.xml', requestFullCalculation(workbookXml));
223
298
  const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
224
299
  fs.writeFileSync(destination, buffer);
225
- return { changes, engine: 'ooxml-compat' };
300
+ return { changes, engine: 'ooxml-compat', formulasInvalidated, recalculateOnOpen: true };
226
301
  }
227
302
 
228
- module.exports = { editXlsxCompat, _updateCell: updateCell, _substituteInSheet: substituteInSheet };
303
+ module.exports = { editXlsxCompat, _updateCell: updateCell, _substituteInSheet: substituteInSheet,
304
+ readXlsxCompat, _invalidateFormulaCaches: invalidateFormulaCaches, _requestFullCalculation: requestFullCalculation };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.12",
3
+ "version": "0.97.14",
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/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/capability-pack.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/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"
10
10
  },
11
11
  "files": [
12
12
  "bin",
@@ -24,7 +24,7 @@
24
24
  "ssh",
25
25
  "agente"
26
26
  ],
27
- "author": "Terminal Smart <contato@terminalsmart.com.br>",
27
+ "author": "Terminal Smart <contato@g3tecnegocios.com>",
28
28
  "homepage": "https://terminalsmart.com.br/cli",
29
29
  "bugs": {
30
30
  "url": "https://terminalsmart.com.br/cli"