terminal-smart-cli 0.97.13 → 0.97.15

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.
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const PACKS = Object.freeze({
6
+ web: Object.freeze({
7
+ id: 'web', label: 'Web/App',
8
+ prompt: 'Se a interface for criada ou alterada, prove em navegador real: desktop e mobile sem overflow, fluxo principal completo, validacao de entrada, persistencia apos reload, console/rede sem erros e screenshots. Prefira navegador ja instalado com playwright-core, sem baixar binario. Mantenha um script reproduzivel e AUTOCONTIDO chamado audit:browser (ou audit:web/audit:e2e): ele deve iniciar o servidor quando necessario, aguardar a URL, testar e encerrar somente o processo que iniciou em bloco finally. Rode-o antes de concluir.',
9
+ }),
10
+ office: Object.freeze({
11
+ id: 'office', label: 'Office/Arquivos',
12
+ prompt: 'Edite o arquivo original por padrao; crie copia somente se o usuario pedir. Preserve backup e formato. Em XLSX, agrupe celulas e formulas do mesmo arquivo em UMA chamada editar_planilha. Em DOCX, agrupe todas as mudancas em UMA chamada atomica editar_documento: use substituicoes para texto e alteracoes_tabela com rotulo/valor para celulas de tabela; nunca envie varias chamadas paralelas para o mesmo DOCX. Reabra com o leitor nativo do formato e prove conteudo, estrutura, formulas/tabelas/slides relevantes e caminho final. Hash ou existencia do arquivo nao provam conteudo. Nunca declare uma alteracao bloqueada ou falha como concluida.',
13
+ }),
14
+ productivity: Object.freeze({
15
+ id: 'productivity', label: 'E-mail/Drive',
16
+ prompt: 'Cheque o status da integracao antes de agir. Para leitura, registre ids e filtros usados. Para mutacao, confirme alvo e estado final pela API. Nunca infira destinatario ausente. Crie rascunho por padrao quando o pedido nao autorizar explicitamente envio; envio, exclusao e movimentacao exigem os gates normais.',
17
+ }),
18
+ vps: Object.freeze({
19
+ id: 'vps', label: 'VPS/Infra',
20
+ prompt: 'Antes de mudar, registre status, configuracao e porta afetados; preserve snapshot/backup. Aplique mudanca minima. Depois prove config valida, servico ativo, porta/HTTP esperado e logs sem regressao. Nao use exclusao ampla nem reinicio indiscriminado.',
21
+ }),
22
+ generic: Object.freeze({
23
+ id: 'generic', label: 'Geral',
24
+ prompt: 'Defina criterios executaveis antes de concluir. Prove o fluxo principal e pelo menos uma falha relevante; registre comandos, codigos de saida e artefatos. Nao aceite narrativa como substituto de evidencia.',
25
+ }),
26
+ });
27
+
28
+ function _has(cwd, name) { try { return fs.existsSync(path.join(cwd || process.cwd(), name)); } catch (_) { return false; } }
29
+
30
+ function selectAuditPack(taskText, cwd) {
31
+ const t = String(taskText || '').toLocaleLowerCase();
32
+ if (/\b(?:gmail|outlook|hotmail|e-?mail|drive|google docs|google sheets|workspace|onedrive|calend[aá]rio|agenda)\b/i.test(t)) return PACKS.productivity;
33
+ if (/\b(?:vps|servidor|ssh|deploy|docker|nginx|traefik|systemd|firewall|porta|produ[cç][aã]o)\b/i.test(t)) return PACKS.vps;
34
+ if (/\b(?:excel|xlsx|planilha|word|docx|powerpoint|pptx|apresenta[cç][aã]o|pdf)\b/i.test(t)) return PACKS.office;
35
+ if (/\b(?:site|p[aá]gina|web|frontend|front-end|interface|navegador|dashboard|painel|html|css|jogo)\b/i.test(t)
36
+ || _has(cwd, 'index.html') || _has(cwd, 'vite.config.js') || _has(cwd, 'next.config.js')) return PACKS.web;
37
+ return PACKS.generic;
38
+ }
39
+
40
+ function promptBlock(pack, lang = 'pt') {
41
+ if (!pack) return '';
42
+ const title = lang === 'en' ? 'MANDATORY AUDIT PACK' : 'PACOTE DE AUDITORIA OBRIGATORIO';
43
+ return `\n\n${title} [${pack.id} — ${pack.label}]: ${pack.prompt}`;
44
+ }
45
+
46
+ function deriveCriteria(cwd, pack) {
47
+ if (!pack || pack.id !== 'web') return [];
48
+ let pkg = null;
49
+ try { pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')); } catch (_) {}
50
+ const scripts = (pkg && pkg.scripts) || {};
51
+ const names = ['audit:browser', 'audit:web', 'audit:e2e'];
52
+ const picked = names.find(n => scripts[n]);
53
+ return picked ? [{ type: 'command', cmd: `npm run ${picked}`, exit: 0, timeout_s: 300, label: `auditoria real (${picked})` }] : [];
54
+ }
55
+
56
+ module.exports = { PACKS, selectAuditPack, promptBlock, deriveCriteria };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const core = require('./core');
5
+
6
+ function sanitizePattern(value) {
7
+ return String(core.redactSecrets(value || ''))
8
+ .replace(/[\u0000-\u001f\u007f]/g, ' ')
9
+ .replace(/\b(?:nvapi-|AIza)[A-Za-z0-9._-]{12,}/gi, '<secret>')
10
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<email>')
11
+ .replace(/https?:\/\/[^\s"']+/gi, '<url>')
12
+ .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '<ip>')
13
+ .replace(/[A-Za-z]:\\[^\s"']+/g, '<path>')
14
+ .replace(/\/(?:home|root|opt|var|tmp|Users|mnt)\/[^\s"']+/g, '<path>')
15
+ .replace(/\b[0-9a-f]{32,}\b/gi, '<id>')
16
+ .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, '<id>')
17
+ .replace(/\b\d{5,}\b/g, '#')
18
+ .replace(/\s+/g, ' ').trim().slice(0, 300);
19
+ }
20
+
21
+ function buildEvents(toolErrors = [], actions = [], context = {}) {
22
+ const recoveredTools = new Set((actions || []).map(action => action && action.name).filter(Boolean));
23
+ const seen = new Set(); const events = [];
24
+ for (const error of (Array.isArray(toolErrors) ? toolErrors : []).slice(-50)) {
25
+ const tool = String(error.tool || 'unknown').slice(0, 80);
26
+ const errorClass = String(error.errorClass || error.class || 'unknown').slice(0, 80);
27
+ const pattern = sanitizePattern(error.evidence || error.message || '');
28
+ const stage = String(error.stage || 'execution').slice(0, 40);
29
+ const fingerprint = crypto.createHash('sha256').update([tool, errorClass, stage, pattern].join('|')).digest('hex');
30
+ if (seen.has(fingerprint)) continue;
31
+ seen.add(fingerprint);
32
+ events.push({ tool, errorClass, pattern, stage, fingerprint,
33
+ retryable: error.retryable === true, recovered: recoveredTools.has(tool),
34
+ surface: context.surface || 'cli', appVersion: context.appVersion || 'unknown', model: context.model || 'unknown' });
35
+ if (events.length >= 20) break;
36
+ }
37
+ return events;
38
+ }
39
+
40
+ function enabled(config = {}, env = process.env) {
41
+ if (String(env.TS_DIAGNOSTICS || '').toLowerCase() === '0' || String(env.TS_DIAGNOSTICS || '').toLowerCase() === 'off') return false;
42
+ return config.diagnosticsEnabled !== false;
43
+ }
44
+
45
+ module.exports = { sanitizePattern, buildEvents, enabled };
package/lib/i18n.js CHANGED
@@ -78,6 +78,7 @@ const STR = {
78
78
  ['ts quem', 'conta conectada'],
79
79
  ['ts conta exportar', 'baixa seus dados sem senhas, tokens ou chaves privadas'],
80
80
  ['ts conta abrir', 'abre Minha Conta para privacidade e exclusão segura'],
81
+ ['ts diagnosticos status', 'controla telemetria sanitizada de erros (ativar/desativar)'],
81
82
  ['ts doctor', 'diagnóstico do ambiente (Node, login, gateway, versão, deps opcionais)'],
82
83
  ['ts privacidade', 'mostra o endereço da Política de Privacidade'],
83
84
  ['ts tema', 'paleta do terminal (7 temas; a cor indica o ESTADO)'],
@@ -247,6 +248,7 @@ const STR = {
247
248
  ['ts quem', 'connected account'],
248
249
  ['ts account export', 'download your data without passwords, tokens or private keys'],
249
250
  ['ts account open', 'open Account for privacy controls and safe deletion'],
251
+ ['ts diagnostics status', 'controls sanitized error telemetry (on/off)'],
250
252
  ['ts privacy', 'shows the Privacy Policy address'],
251
253
  ['ts tema', 'terminal palette (7 themes; color means STATE)'],
252
254
  ['ts idioma pt|en', 'language (default pt-BR)'],
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ function imageJobError(message, code) {
4
+ const error = new Error(message);
5
+ error.code = code;
6
+ return error;
7
+ }
8
+
9
+ async function waitForImageJob(jobId, options = {}) {
10
+ const request = options.request;
11
+ if (typeof request !== 'function') throw new TypeError('request é obrigatório');
12
+ const sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
13
+ const intervalMs = Math.max(0, Number(options.intervalMs) || 2000);
14
+ const maxAttempts = Math.max(1, Number(options.maxAttempts) || 180);
15
+ const id = String(jobId || '').trim();
16
+ if (!id) throw imageJobError('Job de imagem inválido.', 'image_job_invalid');
17
+
18
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
19
+ if (attempt > 1) await sleep(intervalMs);
20
+ const job = await request('/api/ia/image/jobs/' + encodeURIComponent(id));
21
+ if (job && job.status === 'completed' && job.url) return job;
22
+ if (job && job.status === 'failed') throw imageJobError(job.message || 'Falha ao gerar imagem Premium.', 'image_job_failed');
23
+ if (job && !['processing', 'queued'].includes(job.status)) {
24
+ throw imageJobError(job.message || `Estado inesperado da geração: ${job.status || 'vazio'}.`, 'image_job_protocol');
25
+ }
26
+ if (typeof options.onProgress === 'function') options.onProgress({ attempt, maxAttempts, job });
27
+ }
28
+ throw imageJobError('A imagem continua sendo processada. Consulte a conversa no Terminal Smart para acompanhar.', 'image_job_timeout');
29
+ }
30
+
31
+ module.exports = { waitForImageJob, imageJobError };
@@ -197,7 +197,7 @@ const AGENT_ROLES = Object.freeze({
197
197
  }),
198
198
  executor: Object.freeze({
199
199
  prompt: 'Execute somente a etapa recebida com as ferramentas autorizadas. Não declare sucesso sem resultado verificável.',
200
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
200
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'claude-haiku-4-5'] }),
201
201
  }),
202
202
  inspector: Object.freeze({
203
203
  prompt: 'Inspecione em modo somente leitura e compare evidências com os critérios de aceite. Não altere nada e não elogie por cortesia.',
@@ -92,27 +92,82 @@ function _replaceInXmlText(xml, search, replacement, replaceAll) {
92
92
  return { xml: output, count: selected.length };
93
93
  }
94
94
 
95
+ function _replaceTableValueByLabel(xml, label, value, replaceAll = false) {
96
+ const rowRe = /<w:tr(?:\s[^>]*)?>[\s\S]*?<\/w:tr>/g;
97
+ const rows = []; let match;
98
+ while ((match = rowRe.exec(xml))) {
99
+ const raw = match[0];
100
+ const cells = [...raw.matchAll(/<w:tc(?:\s[^>]*)?>[\s\S]*?<\/w:tc>/g)].map(cell => ({
101
+ raw: cell[0], start: cell.index,
102
+ text: _decodeXml([...cell[0].matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g)].map(node => node[1]).join('')),
103
+ }));
104
+ const labelIndex = cells.findIndex(cell => cell.text.trim() === label.trim());
105
+ if (labelIndex >= 0 && cells[labelIndex + 1]) rows.push({ start: match.index, end: rowRe.lastIndex, raw, cells, labelIndex });
106
+ }
107
+ if (!rows.length) return { xml, count: 0 };
108
+ if (rows.length > 1 && !replaceAll) throw new Error(`Rótulo de tabela ambíguo: ${rows.length} ocorrências para ${label}.`);
109
+ const selected = replaceAll ? rows : rows.slice(0, 1);
110
+ let output = xml;
111
+ for (let index = selected.length - 1; index >= 0; index--) {
112
+ const row = selected[index];
113
+ const target = row.cells[row.labelIndex + 1];
114
+ const changed = _replaceInXmlText(target.raw, target.text, String(value ?? ''), false);
115
+ if (!changed.count) throw new Error(`Não foi possível alterar a célula ao lado de ${label}.`);
116
+ const rowXml = row.raw.slice(0, target.start) + changed.xml + row.raw.slice(target.start + target.raw.length);
117
+ output = output.slice(0, row.start) + rowXml + output.slice(row.end);
118
+ }
119
+ return { xml: output, count: selected.length };
120
+ }
121
+
95
122
  async function editarDocumento(input = {}) {
96
123
  try {
97
124
  const original = _existing(input.caminho, '.docx');
98
- const search = String(input.buscar || '');
99
- const replacement = String(input.substituir ?? '');
100
- if (!search) throw new Error('Informe buscar com o texto exato que será alterado.');
125
+ const rules = Array.isArray(input.substituicoes) && input.substituicoes.length
126
+ ? input.substituicoes.slice(0, 50)
127
+ : (input.buscar != null ? [{ buscar: input.buscar, substituir: input.substituir, todas: input.todas }] : []);
128
+ const tableRules = Array.isArray(input.alteracoes_tabela) ? input.alteracoes_tabela.slice(0, 50) : [];
129
+ if (!rules.length && !tableRules.length) throw new Error('Informe substituicoes ou alteracoes_tabela.');
130
+ if (rules.some(rule => !String(rule && rule.buscar || ''))) {
131
+ throw new Error('Informe buscar com o texto exato em cada substituição.');
132
+ }
101
133
  const destination = _destination(original, input, '.docx');
102
134
  const zip = await JSZip.loadAsync(fs.readFileSync(original));
103
135
  const xmlFiles = Object.keys(zip.files).filter(name => /^word\/(document|header\d+|footer\d+|footnotes|endnotes)\.xml$/.test(name));
104
- let changes = 0;
105
- for (const name of xmlFiles) {
106
- const xml = await zip.file(name).async('string');
107
- const result = _replaceInXmlText(xml, search, replacement, !!input.todas);
108
- if (result.count) { zip.file(name, result.xml); changes += result.count; }
109
- if (changes && !input.todas) break;
136
+ let changes = 0; const applied = [];
137
+ for (const rule of rules) {
138
+ const search = String(rule.buscar || '');
139
+ const replacement = String(rule.substituir ?? '');
140
+ const replaceAll = !!rule.todas;
141
+ let ruleChanges = 0;
142
+ for (const name of xmlFiles) {
143
+ const xml = await zip.file(name).async('string');
144
+ const result = _replaceInXmlText(xml, search, replacement, replaceAll);
145
+ if (result.count) { zip.file(name, result.xml); ruleChanges += result.count; }
146
+ if (ruleChanges && !replaceAll) break;
147
+ }
148
+ if (!ruleChanges) throw new Error(`Texto não encontrado no documento: ${search.slice(0, 100)}`);
149
+ changes += ruleChanges;
150
+ applied.push({ buscar: search.slice(0, 120), ocorrencias: ruleChanges });
151
+ }
152
+ for (const rule of tableRules) {
153
+ const label = String(rule && rule.rotulo || '');
154
+ if (!label) throw new Error('Informe rotulo em cada alteração de tabela.');
155
+ let ruleChanges = 0;
156
+ for (const name of xmlFiles) {
157
+ const xml = await zip.file(name).async('string');
158
+ const result = _replaceTableValueByLabel(xml, label, rule.valor, !!rule.todas);
159
+ if (result.count) { zip.file(name, result.xml); ruleChanges += result.count; }
160
+ if (ruleChanges && !rule.todas) break;
161
+ }
162
+ if (!ruleChanges) throw new Error(`Rótulo não encontrado em tabela: ${label.slice(0, 100)}`);
163
+ changes += ruleChanges;
164
+ applied.push({ rotulo: label.slice(0, 120), ocorrencias: ruleChanges });
110
165
  }
111
- if (!changes) throw new Error('Texto não encontrado no documento. Leia o arquivo e copie um trecho exato.');
112
166
  const backup = destination === original ? _backup(original) : null;
113
167
  const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
114
168
  fs.writeFileSync(destination, buffer);
115
- return { ok: true, caminho: destination, editou_original: destination === original, ocorrencias: changes, backup };
169
+ return { ok: true, caminho: destination, editou_original: destination === original,
170
+ ocorrencias: changes, substituicoes_aplicadas: applied, backup };
116
171
  } catch (error) { return { ok: false, erro: 'Falha ao editar DOCX: ' + error.message }; }
117
172
  }
118
173
 
@@ -128,7 +183,9 @@ async function editarPlanilha(input = {}) {
128
183
  const backup = destination === original ? _backup(original) : null;
129
184
  const { editXlsxCompat } = require('./xlsx-compat-editor');
130
185
  const result = await editXlsxCompat(original, destination, input);
131
- return { ok: true, caminho: destination, editou_original: destination === original, celulas_alteradas: result.changes.length, alteracoes: result.changes.slice(0, 100), backup, engine: result.engine, compatibilidade: readError.message };
186
+ return { ok: true, caminho: destination, editou_original: destination === original, celulas_alteradas: result.changes.length,
187
+ alteracoes: result.changes.slice(0, 100), formulas_invalidadas: result.formulasInvalidated,
188
+ recalcular_ao_abrir: result.recalculateOnOpen, backup, engine: result.engine, compatibilidade: readError.message };
132
189
  }
133
190
  const changes = [];
134
191
  for (const update of (Array.isArray(input.alteracoes) ? input.alteracoes : []).slice(0, 500)) {
@@ -162,10 +219,27 @@ async function editarPlanilha(input = {}) {
162
219
  if (!replaced) throw new Error(`Texto não encontrado na planilha: ${search.slice(0, 80)}`);
163
220
  }
164
221
  if (!changes.length) throw new Error('Informe alteracoes ou substituicoes.');
222
+ // ExcelJS preserves cached formula results that may depend on edited input cells.
223
+ // Those caches become stale for previews/readers until Excel recalculates. Keep
224
+ // each formula, discard only its old result and request a full calculation on open.
225
+ let formulasInvalidated = 0;
226
+ for (const sheet of workbook.worksheets) {
227
+ sheet.eachRow({ includeEmpty: false }, row => row.eachCell({ includeEmpty: false }, cell => {
228
+ const value = cell.value;
229
+ if (!value || typeof value !== 'object' || (!value.formula && !value.sharedFormula)) return;
230
+ if (Object.prototype.hasOwnProperty.call(value, 'result')) {
231
+ const fresh = { ...value };
232
+ delete fresh.result;
233
+ cell.value = fresh;
234
+ formulasInvalidated++;
235
+ }
236
+ }));
237
+ }
238
+ workbook.calcProperties.fullCalcOnLoad = true;
165
239
  const backup = destination === original ? _backup(original) : null;
166
240
  await workbook.xlsx.writeFile(destination);
167
- return { ok: true, caminho: destination, editou_original: destination === original, celulas_alteradas: changes.length, alteracoes: changes.slice(0, 100), backup, engine: 'exceljs' };
241
+ return { ok: true, caminho: destination, editou_original: destination === original, celulas_alteradas: changes.length, alteracoes: changes.slice(0, 100), formulas_invalidadas: formulasInvalidated, recalcular_ao_abrir: true, backup, engine: 'exceljs' };
168
242
  } catch (error) { return { ok: false, erro: 'Falha ao editar XLSX: ' + error.message }; }
169
243
  }
170
244
 
171
- module.exports = { editarDocumento, editarPlanilha, _replaceInXmlText };
245
+ module.exports = { editarDocumento, editarPlanilha, _replaceInXmlText, _replaceTableValueByLabel };
@@ -103,4 +103,17 @@ async function lerApresentacao(input = {}) {
103
103
  }
104
104
  }
105
105
 
106
- module.exports = { lerDocumento, lerApresentacao };
106
+ async function lerPlanilha(input = {}) {
107
+ try {
108
+ const p = path.resolve(String(input.caminho || ''));
109
+ if (!p.toLowerCase().endsWith('.xlsx')) throw new Error('O arquivo precisa terminar em .xlsx.');
110
+ if (!fs.existsSync(p) || !fs.statSync(p).isFile()) throw new Error('Arquivo não encontrado: ' + p);
111
+ if (fs.statSync(p).size > 80 * 1048576) throw new Error('Arquivo maior que 80 MB.');
112
+ const data = await require('./xlsx-compat-editor').readXlsxCompat(p, input);
113
+ return { ok: true, arquivo: p, tamanho_bytes: fs.statSync(p).size, ...data };
114
+ } catch (error) {
115
+ return { ok: false, erro: 'Falha ao ler XLSX: ' + error.message };
116
+ }
117
+ }
118
+
119
+ module.exports = { lerDocumento, lerPlanilha, lerApresentacao };
package/lib/recovery.js CHANGED
@@ -63,9 +63,17 @@ const RECOVERABLE = Object.keys(STRATEGIES); // classes com estratégia conhecid
63
63
  function strategiesFor(errorClass) { return STRATEGIES[errorClass] || null; }
64
64
 
65
65
  // Texto acionável pra INJETAR no resultado da ferramenta que o modelo lê. '' se classe sem estratégia.
66
- function recoveryHint(errorClass) {
66
+ function recoveryHint(errorClass, options = {}) {
67
67
  const s = STRATEGIES[errorClass];
68
68
  if (!s) return '';
69
+ if (errorClass === 'not_found' && Array.isArray(options.availableTools)) {
70
+ const available = new Set(options.availableTools);
71
+ const probes = ['listar_diretorio', 'buscar_arquivos'].filter(name => available.has(name));
72
+ if (!probes.length) {
73
+ return 'ESCOPO ESTRITO: o alvo não foi encontrado e as ferramentas de procura não foram autorizadas. Não contorne a restrição, não invente outro caminho e não repita a mesma leitura. Encerre com a falha objetiva e informe que listar_diretorio ou buscar_arquivos permitiria investigar.';
74
+ }
75
+ return `ESTRATÉGIA CONHECIDA (not_found) — use somente ferramentas disponíveis:\n1) DIAGNOSTIQUE com ${probes.join(' ou ')} para confirmar o caminho exato.\n2) Se encontrar um candidato inequívoco, tente ler uma única vez; caso contrário, encerre com a evidência sem inventar caminho.`;
76
+ }
69
77
  return `ESTRATÉGIA CONHECIDA (${errorClass}) — não chute:\n1) DIAGNOSTIQUE: ${s.diag}\n2) CONSERTO: ${s.fix}`;
70
78
  }
71
79
 
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' } } } } },
@@ -377,7 +396,11 @@ function _shellGotcha(cmd) {
377
396
  // significado: `mkdir -p app` cria por engano uma pasta literal chamada `-p`.
378
397
  if (process.platform === 'win32' && /(^|[&|]\s*|\s)mkdir\s+-p(?:\s|$)/i.test(s))
379
398
  return 'Este shell é cmd.exe: "mkdir -p" criaria uma pasta chamada "-p". Use "mkdir pasta" ou PowerShell New-Item.';
380
- if (process.platform === 'win32' && /(^|[&|]\s*|\s)ls(?:\s|$)/i.test(s))
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))
381
404
  return 'Este shell é cmd.exe: use "dir" (ou chame PowerShell explicitamente) em vez de "ls".';
382
405
  if (process.platform === 'win32' && /(?:^|\s)\/[a-zA-Z]\//.test(s))
383
406
  return 'Caminho no formato /c/... não é válido no cmd.exe. Use C:\\... ou um caminho relativo ao diretório de trabalho.';
@@ -623,6 +646,25 @@ async function execute(name, input, opts = {}) {
623
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.` };
624
647
  return { conteudo: txt, linhas_total: total };
625
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
+ }
626
668
  case 'escrever_arquivo': {
627
669
  let p = _abs(input.caminho, baseDir);
628
670
  { const g = _guardTsHome(p); if (g) return { erro: g }; }
@@ -707,7 +749,8 @@ async function execute(name, input, opts = {}) {
707
749
  case 'listar_diretorio': {
708
750
  const p = _abs(input.caminho || '.', baseDir);
709
751
  if (_outsideScope(p)) return _scopeError(p);
710
- 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 => {
711
754
  let size = null; try { if (d.isFile()) size = fs.statSync(path.join(p, d.name)).size; } catch (_) {}
712
755
  return { nome: d.name, tipo: d.isDirectory() ? 'dir' : 'arquivo', bytes: size };
713
756
  });
@@ -879,6 +922,11 @@ async function execute(name, input, opts = {}) {
879
922
  if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
880
923
  return result;
881
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
+ }
882
930
  case 'status_microsoft365':
883
931
  return await require('./api').api('/api/integrations/microsoft/status', { token: opts.token });
884
932
  case 'conectar_microsoft365': {
@@ -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.13",
3
+ "version": "0.97.15",
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/video-generation.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",