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.
- package/bin/ts.js +33 -1
- package/lib/agent.js +602 -32
- 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 +1 -1
- package/lib/office-editors.js +88 -14
- package/lib/office-readers.js +14 -1
- package/lib/recovery.js +9 -1
- package/lib/tools.js +52 -4
- package/lib/xlsx-compat-editor.js +78 -2
- package/package.json +2 -2
|
@@ -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)'],
|
package/lib/image-job.js
ADDED
|
@@ -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 };
|
package/lib/intelligence-core.js
CHANGED
|
@@ -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.',
|
package/lib/office-editors.js
CHANGED
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
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,
|
|
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 };
|
package/lib/office-readers.js
CHANGED
|
@@ -103,4 +103,17 @@ async function lerApresentacao(input = {}) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|