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/bin/ts.js +141 -10
- package/lib/agent.js +625 -38
- package/lib/api.js +9 -2
- package/lib/audit-packs.js +56 -0
- package/lib/evolution-telemetry.js +45 -0
- package/lib/i18n.js +2 -0
- package/lib/image-job.js +31 -0
- package/lib/intelligence-core.js +50 -13
- package/lib/meta.js +314 -14
- package/lib/office-editors.js +88 -14
- package/lib/office-readers.js +14 -1
- package/lib/tools.js +93 -19
- package/lib/xlsx-compat-editor.js +78 -2
- package/package.json +3 -3
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
|
-
|
|
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
|
|
194
|
-
{ type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
900
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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@
|
|
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"
|