terminal-smart-cli 0.97.7 → 0.97.9

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.
@@ -9,7 +9,7 @@
9
9
  const crypto = require('crypto');
10
10
 
11
11
  const INTELLIGENCE_CONTRACT = 1;
12
- const PRICE_REVISION = '2026-07-28';
12
+ const PRICE_REVISION = '2026-08-06';
13
13
 
14
14
  const MODEL_CATALOG = Object.freeze({
15
15
  smart: {
@@ -42,8 +42,8 @@ const MODEL_CATALOG = Object.freeze({
42
42
  capabilities: ['tools', 'vision', 'classification', 'summarization', 'review', 'long-context'], toolReliability: 0.84, quality: 0.80,
43
43
  },
44
44
  'qwen-plus': {
45
- upstreamId: 'qwen/qwen-plus',
46
- provider: 'qwen', contextWindow: 1000000, price: { input: 0.26, output: 0.78, cachedInput: 0.052 },
45
+ upstreamId: 'qwen3.7-plus',
46
+ provider: 'qwen', contextWindow: 1000000, price: { input: 0.42, output: 1.68, cachedInput: 0.042 },
47
47
  capabilities: ['tools', 'content', 'code', 'summarization'], toolReliability: 0.76, quality: 0.76,
48
48
  },
49
49
  'kimi-k2.7-code-highspeed': {
@@ -89,7 +89,7 @@ const MODEL_CATALOG = Object.freeze({
89
89
  'gpt-5.6-luna': {
90
90
  upstreamId: 'openai/gpt-5.6-luna',
91
91
  provider: 'openai', contextWindow: 1000000, price: { input: 1.00, output: 6.00, cachedInput: 0.10 },
92
- capabilities: ['tools', 'code', 'summarization', 'review', 'long-context'], toolReliability: 0.93, quality: 0.89,
92
+ capabilities: ['tools', 'code', 'planning', 'execution', 'summarization', 'review', 'long-context'], toolReliability: 0.93, quality: 0.89,
93
93
  },
94
94
  'claude-sonnet-5': {
95
95
  upstreamId: 'anthropic/claude-sonnet-5',
@@ -101,6 +101,24 @@ const MODEL_CATALOG = Object.freeze({
101
101
  provider: 'anthropic', contextWindow: 1000000, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
102
102
  capabilities: ['tools', 'code', 'planning', 'review'], toolReliability: 0.96, quality: 0.95,
103
103
  },
104
+ 'claude-haiku-4-5': {
105
+ upstreamId: 'anthropic/claude-haiku-4.5',
106
+ provider: 'anthropic', contextWindow: 200000, price: { input: 1.00, output: 5.00, cachedInput: 0.10 },
107
+ capabilities: ['tools', 'code', 'planning', 'execution', 'review', 'vision'], toolReliability: 0.94, quality: 0.88,
108
+ },
109
+ // Não é um modelo que o produto oferece ao cliente — é o que o assistente interno usa
110
+ // quando o provedor configurado é o Anthropic. Entra no catálogo pelo PREÇO: sem entrada
111
+ // própria o cálculo cai no `_default` (US$ 0,30 entrada / US$ 2,50 saída), o que superestima
112
+ // pouco na entrada e o DOBRO na saída.
113
+ //
114
+ // Isso passou a importar quando o painel começou a decidir preço de plano com esse custo:
115
+ // modelo interno mal precificado empurra a margem medida para baixo e leva a corrigir preço
116
+ // sem precisar. E, ao contrário de um erro que quebra, este só produz um número plausível.
117
+ 'claude-3-haiku-20240307': {
118
+ upstreamId: 'anthropic/claude-3-haiku',
119
+ provider: 'anthropic', contextWindow: 200000, price: { input: 0.25, output: 1.25, cachedInput: 0.03 },
120
+ capabilities: ['tools'], toolReliability: 0.75, quality: 0.62,
121
+ },
104
122
  'grok-4.5': {
105
123
  upstreamId: 'x-ai/grok-4.5',
106
124
  provider: 'xai', contextWindow: 500000, price: { input: 2.00, output: 6.00, cachedInput: 0.30 },
@@ -130,6 +148,49 @@ const MODEL_ALIASES = Object.freeze({
130
148
  glm: 'glm-4.6',
131
149
  mimo: 'mimo-v2.5',
132
150
  'gemini-lite': 'gemini-2.5-flash-lite',
151
+ 'claude-haiku': 'claude-haiku-4-5',
152
+ });
153
+
154
+ // Contrato dos agentes funcionais. Isto não é fine-tuning: cada papel recebe um
155
+ // prompt curto, uma cadeia de modelos por plano e continua sujeito à prova das
156
+ // ferramentas. O primeiro modelo permitido é o padrão; os seguintes são fallback.
157
+ const AGENT_ROLES = Object.freeze({
158
+ classifier: Object.freeze({
159
+ prompt: 'Classifique a intenção e devolva somente a estrutura pedida. Não execute ferramentas nem invente dados.',
160
+ models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash-lite', 'gpt-4o-mini'], pro: ['gemini-2.5-flash-lite', 'gpt-4o-mini'] }),
161
+ }),
162
+ planner: Object.freeze({
163
+ prompt: 'Decomponha o objetivo em etapas executáveis, dependências, riscos, critérios de aceite e provas. Não execute a tarefa.',
164
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'glm-5.2'] }),
165
+ }),
166
+ executor: Object.freeze({
167
+ prompt: 'Execute somente a etapa recebida com as ferramentas autorizadas. Não declare sucesso sem resultado verificável.',
168
+ 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'] }),
169
+ }),
170
+ inspector: Object.freeze({
171
+ 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.',
172
+ models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash', 'gpt-4o-mini'], pro: ['claude-haiku-4-5', 'gemini-2.5-flash', 'gpt-5.6-luna'] }),
173
+ }),
174
+ corrector: Object.freeze({
175
+ prompt: 'Corrija apenas falhas confirmadas pelo inspetor, preserve o que funciona e repita as provas afetadas. Não amplie o escopo.',
176
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['deepseek-v4-pro', 'claude-haiku-4-5', 'glm-5.2'] }),
177
+ }),
178
+ vision: Object.freeze({
179
+ prompt: 'Extraia fatos observáveis da imagem e separe leitura, inferência e incerteza. Não invente texto ilegível.',
180
+ models: Object.freeze({ free: ['smart'], basic: ['gemini-2.5-flash'], pro: ['gemini-2.5-flash', 'claude-haiku-4-5'] }),
181
+ }),
182
+ document: Object.freeze({
183
+ prompt: 'Produza conteúdo estruturado e use o motor nativo do formato solicitado. Preserve editabilidade e valide o arquivo gerado.',
184
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
185
+ }),
186
+ site_builder: Object.freeze({
187
+ prompt: 'Crie ou edite o site completo, responsivo e acessível. Preserve o existente quando editar e valide a URL publicada.',
188
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'deepseek-v4-flash', 'claude-haiku-4-5'] }),
189
+ }),
190
+ cloud_box: Object.freeze({
191
+ prompt: 'Opere a box com mudanças mínimas, registre comandos e valide serviço, porta e URL. Nunca exponha segredos.',
192
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
193
+ }),
133
194
  });
134
195
 
135
196
  const DEFAULT_PRICE = Object.freeze({ input: 0.30, output: 2.50, cachedInput: 0.03 });
@@ -302,6 +363,33 @@ function createHandoff(input) {
302
363
  };
303
364
  }
304
365
 
366
+ function normalizeAgentPlan(value) {
367
+ const id = String(value || 'free').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
368
+ if (['basic', 'basico'].includes(id)) return 'basic';
369
+ if (['pro', 'ultra', 'business', 'max', 'pro+'].includes(id)) return 'pro';
370
+ return 'free';
371
+ }
372
+
373
+ function agentRoleContract(role, plan, options) {
374
+ const id = Object.prototype.hasOwnProperty.call(AGENT_ROLES, role) ? role : 'executor';
375
+ const planId = normalizeAgentPlan(plan);
376
+ const spec = AGENT_ROLES[id];
377
+ const requested = spec.models[planId] || spec.models.free;
378
+ const allowedRaw = options && Array.isArray(options.allowedModels) ? options.allowedModels : null;
379
+ const allowAll = !allowedRaw || allowedRaw.includes('*');
380
+ const allowed = allowAll ? null : new Set(allowedRaw.map(normalizeModelId));
381
+ const candidates = requested.map(normalizeModelId).filter(model => model === 'smart' || MODEL_CATALOG[model])
382
+ .filter(model => !allowed || allowed.has(model) || model === 'smart');
383
+ // O fallback nunca atravessa a fronteira comercial. Se a configuração vier
384
+ // inconsistente, usa o primeiro modelo explicitamente permitido, ou smart.
385
+ if (!candidates.length && allowed) {
386
+ const safe = [...allowed].find(model => MODEL_CATALOG[model] || model === 'smart');
387
+ if (safe) candidates.push(safe);
388
+ }
389
+ if (!candidates.length) candidates.push('smart');
390
+ return Object.freeze({ role: id, plan: planId, prompt: spec.prompt, model: candidates[0], candidates: Object.freeze(candidates) });
391
+ }
392
+
305
393
  function routeModel(task, reliability) {
306
394
  const t = task || {};
307
395
  const required = new Set(t.capabilities || []);
@@ -326,7 +414,8 @@ function routeModel(task, reliability) {
326
414
 
327
415
  module.exports = {
328
416
  INTELLIGENCE_CONTRACT, PRICE_REVISION, MODEL_CATALOG, MODEL_ALIASES, DEFAULT_PRICE,
329
- MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES,
417
+ MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES, AGENT_ROLES,
330
418
  normalizeModelId, modelInfo, legacyPriceMap, normalizeUsage, estimateCostUsd, catalogFreshness,
331
- createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff, routeModel,
419
+ createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff,
420
+ normalizeAgentPlan, agentRoleContract, routeModel,
332
421
  };
package/lib/keyring.js CHANGED
@@ -151,6 +151,15 @@ async function testar(id, { key = '', baseUrl = '', modelo = '', timeoutMs = 150
151
151
  if (!url) return { ok: false, erro: 'baseUrl não definida' };
152
152
  const mdl = modelo || p.modeloPadrao;
153
153
  if (!mdl) return { ok: false, erro: 'modelo não definido' };
154
+ const estado = providers.estadoModelo(id, mdl);
155
+ if (!estado.disponivel) {
156
+ return {
157
+ ok: false, aposentado: true, status: 410, modelo: mdl,
158
+ substituto: estado.substituto || '',
159
+ erro: 'o modelo "' + mdl + '" foi aposentado: ' + estado.motivo + '. ' +
160
+ (estado.substituto ? 'Troque para "' + estado.substituto + '" com `ts conectar ' + id + '`.' : 'Escolha outro modelo com `ts conectar ' + id + '`.')
161
+ };
162
+ }
154
163
  const _fetch = fetchImpl || fetch;
155
164
 
156
165
  const chamar = async (body) => {
@@ -191,6 +200,8 @@ async function testar(id, { key = '', baseUrl = '', modelo = '', timeoutMs = 150
191
200
  }
192
201
  if (r1.status === 401 || r1.status === 403) return { ok: false, erro: 'chave recusada pelo provedor (HTTP ' + r1.status + ')' };
193
202
  if (r1.status === 404) return { ok: false, erro: 'modelo "' + mdl + '" não existe nesse provedor (HTTP 404)' };
203
+ if (r1.status === 410) return { ok: false, aposentado: true, status: 410, modelo: mdl,
204
+ erro: 'o provedor aposentou o modelo "' + mdl + '" (HTTP 410). Escolha outro com `ts conectar ' + id + '`.' };
194
205
  if (r1.status >= 400) {
195
206
  const msg = (r1.json && (r1.json.error?.message || r1.json.message)) || ('HTTP ' + r1.status);
196
207
  return { ok: false, erro: String(msg).slice(0, 200) };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ function stable(value) {
4
+ if (Array.isArray(value)) return value.map(stable);
5
+ if (value && typeof value === 'object') {
6
+ const out = {};
7
+ for (const key of Object.keys(value).sort()) out[key] = stable(value[key]);
8
+ return out;
9
+ }
10
+ return value;
11
+ }
12
+
13
+ function signature(tool, input) {
14
+ return String(tool || '') + '|' + JSON.stringify(stable(input || {}));
15
+ }
16
+
17
+ class MissionToolCache {
18
+ constructor({ maxEntries = 64 } = {}) {
19
+ this.maxEntries = Math.max(1, maxEntries);
20
+ this.entries = new Map();
21
+ this.hits = 0;
22
+ this.invalidations = 0;
23
+ }
24
+ get(tool, input) {
25
+ const key = signature(tool, input);
26
+ const entry = this.entries.get(key);
27
+ if (!entry) return null;
28
+ this.entries.delete(key); this.entries.set(key, entry);
29
+ this.hits++;
30
+ return entry;
31
+ }
32
+ remember(tool, input, entry) {
33
+ const key = signature(tool, input);
34
+ this.entries.delete(key);
35
+ this.entries.set(key, Object.assign({}, entry));
36
+ while (this.entries.size > this.maxEntries) this.entries.delete(this.entries.keys().next().value);
37
+ }
38
+ invalidate() {
39
+ if (this.entries.size) this.invalidations++;
40
+ this.entries.clear();
41
+ }
42
+ stats() { return { entries: this.entries.size, hits: this.hits, invalidations: this.invalidations }; }
43
+ }
44
+
45
+ function cacheReference(entry, tool, originalStillVisible) {
46
+ if (!originalStillVisible && entry && entry.content) return entry.content;
47
+ return JSON.stringify({
48
+ cached: true,
49
+ tool,
50
+ message: 'Resultado idêntico já fornecido nesta missão; a fonte não foi consultada novamente. Use o resultado anterior.',
51
+ originalToolCallId: entry && entry.toolCallId,
52
+ });
53
+ }
54
+
55
+ module.exports = { MissionToolCache, signature, cacheReference };
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const JSZip = require('jszip');
8
+
9
+ const MAX_BYTES = 80 * 1024 * 1024;
10
+ const BACKUP_ROOT = path.join(os.homedir(), '.terminal-smart', 'backups', 'office');
11
+
12
+ function _existing(file, extension) {
13
+ const target = path.resolve(String(file || ''));
14
+ if (!target.toLowerCase().endsWith(extension)) throw new Error(`O arquivo precisa terminar em ${extension}.`);
15
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error('Arquivo não encontrado: ' + target);
16
+ if (fs.statSync(target).size > MAX_BYTES) throw new Error('Arquivo maior que 80 MB; edite um recorte ou uma cópia reduzida.');
17
+ return target;
18
+ }
19
+
20
+ function _destination(original, input, extension) {
21
+ if (!input.criar_copia) return original;
22
+ const copy = path.resolve(String(input.caminho_copia || ''));
23
+ if (!copy.toLowerCase().endsWith(extension)) throw new Error(`Para criar cópia, informe caminho_copia terminando em ${extension}.`);
24
+ if (copy === original) throw new Error('caminho_copia deve ser diferente do arquivo original.');
25
+ fs.mkdirSync(path.dirname(copy), { recursive: true });
26
+ return copy;
27
+ }
28
+
29
+ function _backup(file) {
30
+ const key = crypto.createHash('sha256').update(file.toLowerCase()).digest('hex').slice(0, 16);
31
+ const dir = path.join(BACKUP_ROOT, key);
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
34
+ const backup = path.join(dir, `${path.basename(file)}.${stamp}.bak`);
35
+ fs.copyFileSync(file, backup);
36
+ const backups = fs.readdirSync(dir).filter(name => name.endsWith('.bak')).sort();
37
+ while (backups.length > 10) fs.unlinkSync(path.join(dir, backups.shift()));
38
+ return backup;
39
+ }
40
+
41
+ function _decodeXml(value) {
42
+ return String(value || '').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
43
+ .replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
44
+ }
45
+
46
+ function _encodeXml(value) {
47
+ return String(value || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
48
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
49
+ }
50
+
51
+ function _replaceInXmlText(xml, search, replacement, replaceAll) {
52
+ const re = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g;
53
+ const nodes = [];
54
+ let match;
55
+ while ((match = re.exec(xml))) nodes.push({ start: match.index, end: re.lastIndex, raw: match[0], text: _decodeXml(match[1]) });
56
+ const combined = nodes.map(node => node.text).join('');
57
+ const hits = [];
58
+ let at = 0;
59
+ while (search && (at = combined.indexOf(search, at)) >= 0) {
60
+ hits.push(at);
61
+ at += Math.max(1, search.length);
62
+ }
63
+ if (!hits.length) return { xml, count: 0 };
64
+ if (hits.length > 1 && !replaceAll) throw new Error(`Texto ambíguo: ${hits.length} ocorrências. Dê mais contexto ou use todas=true.`);
65
+
66
+ const starts = [];
67
+ let cursor = 0;
68
+ for (const node of nodes) { starts.push(cursor); cursor += node.text.length; }
69
+ const selected = replaceAll ? hits : hits.slice(0, 1);
70
+ for (let h = selected.length - 1; h >= 0; h--) {
71
+ const begin = selected[h];
72
+ const finish = begin + search.length;
73
+ let first = nodes.findIndex((node, index) => begin >= starts[index] && begin < starts[index] + node.text.length);
74
+ let last = nodes.findIndex((node, index) => finish > starts[index] && finish <= starts[index] + node.text.length);
75
+ if (first < 0 || last < 0) throw new Error('Não foi possível mapear o texto no DOCX.');
76
+ const left = begin - starts[first];
77
+ const right = finish - starts[last];
78
+ if (first === last) nodes[first].text = nodes[first].text.slice(0, left) + replacement + nodes[first].text.slice(right);
79
+ else {
80
+ nodes[first].text = nodes[first].text.slice(0, left) + replacement;
81
+ for (let index = first + 1; index < last; index++) nodes[index].text = '';
82
+ nodes[last].text = nodes[last].text.slice(right);
83
+ }
84
+ }
85
+ let output = ''; let sourceAt = 0;
86
+ for (const node of nodes) {
87
+ output += xml.slice(sourceAt, node.start);
88
+ output += node.raw.replace(/(<w:t(?:\s[^>]*)?>)[\s\S]*?(<\/w:t>)/, `$1${_encodeXml(node.text)}$2`);
89
+ sourceAt = node.end;
90
+ }
91
+ output += xml.slice(sourceAt);
92
+ return { xml: output, count: selected.length };
93
+ }
94
+
95
+ async function editarDocumento(input = {}) {
96
+ try {
97
+ 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.');
101
+ const destination = _destination(original, input, '.docx');
102
+ const zip = await JSZip.loadAsync(fs.readFileSync(original));
103
+ 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;
110
+ }
111
+ if (!changes) throw new Error('Texto não encontrado no documento. Leia o arquivo e copie um trecho exato.');
112
+ const backup = destination === original ? _backup(original) : null;
113
+ const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
114
+ fs.writeFileSync(destination, buffer);
115
+ return { ok: true, caminho: destination, editou_original: destination === original, ocorrencias: changes, backup };
116
+ } catch (error) { return { ok: false, erro: 'Falha ao editar DOCX: ' + error.message }; }
117
+ }
118
+
119
+ async function editarPlanilha(input = {}) {
120
+ try {
121
+ const original = _existing(input.caminho, '.xlsx');
122
+ const destination = _destination(original, input, '.xlsx');
123
+ const ExcelJS = require('exceljs');
124
+ const workbook = new ExcelJS.Workbook();
125
+ try {
126
+ await workbook.xlsx.readFile(original);
127
+ } catch (readError) {
128
+ const backup = destination === original ? _backup(original) : null;
129
+ const { editXlsxCompat } = require('./xlsx-compat-editor');
130
+ 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 };
132
+ }
133
+ const changes = [];
134
+ for (const update of (Array.isArray(input.alteracoes) ? input.alteracoes : []).slice(0, 500)) {
135
+ const sheet = workbook.getWorksheet(String(update.aba || '')) || (!update.aba ? workbook.worksheets[0] : null);
136
+ if (!sheet) throw new Error(`Aba não encontrada: ${update.aba}`);
137
+ const address = String(update.celula || '').toUpperCase();
138
+ if (!/^[A-Z]{1,3}[1-9][0-9]{0,6}$/.test(address)) throw new Error(`Célula inválida: ${update.celula}`);
139
+ const cell = sheet.getCell(address);
140
+ if (update.formula != null) cell.value = { formula: String(update.formula).replace(/^=/, '') };
141
+ else {
142
+ const raw = update.valor;
143
+ cell.value = typeof raw === 'string' && /^-?\d+(?:[.,]\d+)?$/.test(raw.trim())
144
+ ? Number(raw.trim().replace(',', '.'))
145
+ : (raw == null ? null : raw);
146
+ }
147
+ changes.push(`${sheet.name}!${address}`);
148
+ }
149
+ for (const rule of (Array.isArray(input.substituicoes) ? input.substituicoes : []).slice(0, 50)) {
150
+ const search = String(rule.buscar || '');
151
+ if (!search) continue;
152
+ let replaced = 0;
153
+ for (const sheet of workbook.worksheets) {
154
+ if (rule.aba && sheet.name.toLowerCase() !== String(rule.aba).toLowerCase()) continue;
155
+ sheet.eachRow(row => row.eachCell(cell => {
156
+ if (!rule.todas && replaced) return;
157
+ if (typeof cell.value !== 'string' || !cell.value.includes(search)) return;
158
+ cell.value = rule.todas ? cell.value.split(search).join(String(rule.substituir ?? '')) : cell.value.replace(search, String(rule.substituir ?? ''));
159
+ replaced++; changes.push(`${sheet.name}!${cell.address}`);
160
+ }));
161
+ }
162
+ if (!replaced) throw new Error(`Texto não encontrado na planilha: ${search.slice(0, 80)}`);
163
+ }
164
+ if (!changes.length) throw new Error('Informe alteracoes ou substituicoes.');
165
+ const backup = destination === original ? _backup(original) : null;
166
+ 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' };
168
+ } catch (error) { return { ok: false, erro: 'Falha ao editar XLSX: ' + error.message }; }
169
+ }
170
+
171
+ module.exports = { editarDocumento, editarPlanilha, _replaceInXmlText };
package/lib/providers.js CHANGED
@@ -16,13 +16,11 @@ const PROVEDORES = {
16
16
  nvidia: {
17
17
  nome: 'NVIDIA NIM',
18
18
  baseUrl: 'https://integrate.api.nvidia.com/v1',
19
- // MEDIDO na API real (28/07/2026): o 70b estourou 120s de COLD START e o
20
- // nemotron-70b é listado em /v1/models mas devolve 404 no /chat/completions.
21
- // O v4-flash respondeu em 16s E chamou ferramenta — é o mesmo executor que o
22
- // resto do TS já usa, então é o padrão sensato aqui.
23
- modeloPadrao: 'deepseek-ai/deepseek-v4-flash',
19
+ // O id sem sufixo foi retirado pela NVIDIA (HTTP 410). O catálogo atual expõe
20
+ // a revisão datada; manter o id antigo como padrão faria toda chave nova falhar.
21
+ modeloPadrao: 'deepseek-ai/deepseek-v4-flash-0731',
24
22
  sugestoes: [
25
- ['deepseek-ai/deepseek-v4-flash', 'equilíbrio chama ferramenta, ~16s'],
23
+ ['deepseek-ai/deepseek-v4-flash-0731', 'revisão atual do DeepSeek V4 Flash'],
26
24
  ['meta/llama-3.1-8b-instruct', 'o mais rápido — ~1s, bom pra chat/roteamento'],
27
25
  ['mistralai/mistral-medium-3.5-128b', 'mais capaz — ~43s'],
28
26
  ],
@@ -88,6 +86,17 @@ const PROVEDORES = {
88
86
  },
89
87
  };
90
88
 
89
+ // IDs que o provedor encerrou. Esta lista é deliberadamente conservadora: só
90
+ // bloqueamos localmente quando a aposentadoria foi confirmada na API real.
91
+ const MODELOS_APOSENTADOS = {
92
+ nvidia: {
93
+ 'deepseek-ai/deepseek-v4-flash': {
94
+ substituto: 'deepseek-ai/deepseek-v4-flash-0731',
95
+ motivo: 'a NVIDIA retirou esse id (HTTP 410)',
96
+ },
97
+ },
98
+ };
99
+
91
100
  function info(id) { return PROVEDORES[String(id || '').toLowerCase()] || null; }
92
101
  function ids() { return Object.keys(PROVEDORES); }
93
102
 
@@ -115,4 +124,12 @@ function baseUrlDe(id, override) {
115
124
  return p ? p.baseUrl : '';
116
125
  }
117
126
 
118
- module.exports = { PROVEDORES, info, ids, validarChave, baseUrlDe };
127
+ function estadoModelo(id, modelo) {
128
+ const pid = String(id || '').toLowerCase();
129
+ const mid = String(modelo || '').trim();
130
+ const aposentado = MODELOS_APOSENTADOS[pid] && MODELOS_APOSENTADOS[pid][mid];
131
+ return aposentado ? { disponivel: false, aposentado: true, modelo: mid, ...aposentado }
132
+ : { disponivel: true, aposentado: false, modelo: mid };
133
+ }
134
+
135
+ module.exports = { PROVEDORES, MODELOS_APOSENTADOS, info, ids, validarChave, baseUrlDe, estadoModelo };
package/lib/tools.js CHANGED
@@ -161,6 +161,73 @@ const DEFS = [
161
161
  caminho: { type: 'string' }, buscar: { type: 'string' },
162
162
  inicio_char: { type: 'number' }, tamanho: { type: 'number' },
163
163
  }, required: ['caminho'] } } },
164
+ { 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.',
166
+ parameters: { type: 'object', properties: {
167
+ caminho: { type: 'string' }, buscar: { type: 'string' }, substituir: { type: 'string' },
168
+ todas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, caminho_copia: { type: 'string' },
169
+ }, required: ['caminho', 'buscar', 'substituir'] } } },
170
+ { type: 'function', function: { name: 'editar_planilha',
171
+ 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
+ parameters: { type: 'object', properties: {
173
+ caminho: { type: 'string' },
174
+ alteracoes: { type: 'array', items: { type: 'object', properties: {
175
+ aba: { type: 'string' }, celula: { type: 'string' }, valor: { type: 'string' }, formula: { type: 'string' },
176
+ }, required: ['celula'] } },
177
+ substituicoes: { type: 'array', items: { type: 'object', properties: {
178
+ aba: { type: 'string' }, buscar: { type: 'string' }, substituir: { type: 'string' }, todas: { type: 'boolean' },
179
+ }, required: ['buscar', 'substituir'] } },
180
+ criar_copia: { type: 'boolean' }, caminho_copia: { type: 'string' },
181
+ }, required: ['caminho'] } } },
182
+ { type: 'function', function: { name: 'status_microsoft365', description: 'Verifica a conexão com Outlook e Microsoft 365.', parameters: { type: 'object', properties: {} } } },
183
+ { type: 'function', function: { name: 'conectar_microsoft365', description: 'Gera o link oficial para autorizar Outlook e Microsoft 365.', parameters: { type: 'object', properties: {} } } },
184
+ { 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' } } } } },
185
+ { type: 'function', function: { name: 'ler_email_outlook', description: 'Lê o corpo completo de um e-mail do Outlook sem marcá-lo como lido.', parameters: { type: 'object', properties: { id_email: { type: 'string' } }, required: ['id_email'] } } },
186
+ { type: 'function', function: { name: 'listar_pastas_outlook', description: 'Lista pastas de e-mail do Outlook.', parameters: { type: 'object', properties: {} } } },
187
+ { type: 'function', function: { name: 'alterar_email_outlook', description: 'Altera estado ou pasta de um e-mail do Outlook. Exige confirmação.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, acao: { type: 'string', enum: ['lido','nao_lido','importante','normal','arquivar','caixa_entrada','spam','lixeira','restaurar','mover','excluir_permanente'] }, id_pasta: { type: 'string' } }, required: ['id_email','acao'] } } },
188
+ { type: 'function', function: { name: 'criar_resposta_outlook', description: 'Cria rascunho de resposta no Outlook. Não envia e exige confirmação.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, corpo: { type: 'string' }, para_todos: { type: 'boolean' } }, required: ['id_email','corpo'] } } },
189
+ { type: 'function', function: { name: 'criar_encaminhamento_outlook', description: 'Cria rascunho de encaminhamento no Outlook. Não envia e exige confirmação.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, corpo: { type: 'string' } }, required: ['id_email','para'] } } },
190
+ { 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
+ { 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
+ { type: 'function', function: { name: 'enviar_rascunho_outlook', description: 'Envia um rascunho existente do Outlook. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
193
+ { type: 'function', function: { name: 'status_google_workspace', description: 'Verifica a conexão com Gmail, Drive, Docs e Sheets.', parameters: { type: 'object', properties: {} } } },
194
+ { type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link oficial para autorizar Gmail, Drive, Docs e Sheets.', parameters: { type: 'object', properties: {} } } },
195
+ { type: 'function', function: { name: 'listar_emails_gmail', description: 'Lista e prioriza e-mails do Gmail, sem alterar nada.', parameters: { type: 'object', properties: {
196
+ limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string', enum: ['inbox','sent','drafts','spam','trash','starred','important','all'] }
197
+ } } } },
198
+ { 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
+ id_email: { type: 'string' }, conversa_completa: { type: 'boolean' }
200
+ }, required: ['id_email'] } } },
201
+ { type: 'function', function: { name: 'listar_pastas_gmail', description: 'Lista pastas e marcadores do Gmail.', parameters: { type: 'object', properties: {} } } },
202
+ { type: 'function', function: { name: 'alterar_email_gmail', description: 'Altera estado, pasta ou marcador de um e-mail. Exige confirmação.', parameters: { type: 'object', properties: {
203
+ id_email: { type: 'string' }, acao: { type: 'string', enum: ['lido','nao_lido','arquivar','caixa_entrada','estrela','remover_estrela','importante','remover_importante','spam','nao_spam','lixeira','restaurar','aplicar_marcador','remover_marcador','mover','excluir_permanente'] }, marcador: { type: 'string' }
204
+ }, required: ['id_email','acao'] } } },
205
+ { type: 'function', function: { name: 'criar_resposta_gmail', description: 'Cria rascunho de resposta na mesma conversa. Não envia e exige confirmação.', parameters: { type: 'object', properties: {
206
+ id_email: { type: 'string' }, corpo: { type: 'string' }, para_todos: { type: 'boolean' }, formato_html: { type: 'boolean' }
207
+ }, required: ['id_email','corpo'] } } },
208
+ { type: 'function', function: { name: 'criar_encaminhamento_gmail', description: 'Cria rascunho de encaminhamento. Não envia e exige confirmação.', parameters: { type: 'object', properties: {
209
+ id_email: { type: 'string' }, para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, corpo: { type: 'string' }
210
+ }, required: ['id_email','para'] } } },
211
+ { type: 'function', function: { name: 'obter_anexo_gmail', description: 'Obtém o link autenticado para baixar um anexo do Gmail.', parameters: { type: 'object', properties: {
212
+ id_email: { type: 'string' }, id_anexo: { type: 'string' }
213
+ }, required: ['id_email','id_anexo'] } } },
214
+ { type: 'function', function: { name: 'criar_rascunho_gmail', description: 'Cria rascunho no Gmail. Exige confirmação e não envia.', parameters: { type: 'object', properties: {
215
+ para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' }
216
+ }, required: ['para', 'assunto', 'corpo'] } } },
217
+ { type: 'function', function: { name: 'enviar_rascunho_gmail', description: 'Envia rascunho existente do Gmail. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
218
+ { type: 'function', function: { name: 'listar_arquivos_drive', description: 'Lista arquivos no Google Drive por nome ou tipo.', parameters: { type: 'object', properties: {
219
+ busca: { type: 'string' }, tipo_mime: { type: 'string' }, limite: { type: 'number' }
220
+ } } } },
221
+ { 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
+ { 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
+ id_documento: { type: 'string' }, localizar: { type: 'string' }, substituir_por: { type: 'string' }, diferenciar_maiusculas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
224
+ }, required: ['id_documento', 'localizar'] } } },
225
+ { type: 'function', function: { name: 'ler_google_sheets', description: 'Lê células de Google Sheets pelo ID e intervalo.', parameters: { type: 'object', properties: {
226
+ id_planilha: { type: 'string' }, intervalo: { type: 'string' }
227
+ }, required: ['id_planilha'] } } },
228
+ { 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
+ id_planilha: { type: 'string' }, intervalo: { type: 'string' }, valores: { type: 'array', items: { type: 'array', items: {} } }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
230
+ }, required: ['id_planilha', 'intervalo', 'valores'] } } },
164
231
  { type: 'function', function: { name: 'ler_apresentacao',
165
232
  description: 'Lê PowerPoint .pptx por slides, retornando o texto estruturado e a quantidade de imagens.',
166
233
  parameters: { type: 'object', properties: {
@@ -780,6 +847,97 @@ async function execute(name, input, opts = {}) {
780
847
  if (_outsideScope(target)) return _scopeError(target);
781
848
  return await require('./office-readers').lerDocumento({ ...input, caminho: target });
782
849
  }
850
+ case 'editar_documento': {
851
+ const target = _abs(input.caminho, baseDir);
852
+ if (_outsideScope(target)) return _scopeError(target);
853
+ const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
854
+ if (copy && _outsideScope(copy)) return _scopeError(copy);
855
+ const result = await require('./office-editors').editarDocumento({ ...input, caminho: target, caminho_copia: copy });
856
+ if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
857
+ return result;
858
+ }
859
+ case 'editar_planilha': {
860
+ const target = _abs(input.caminho, baseDir);
861
+ if (_outsideScope(target)) return _scopeError(target);
862
+ const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
863
+ if (copy && _outsideScope(copy)) return _scopeError(copy);
864
+ const result = await require('./office-editors').editarPlanilha({ ...input, caminho: target, caminho_copia: copy });
865
+ if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
866
+ return result;
867
+ }
868
+ case 'status_microsoft365':
869
+ return await require('./api').api('/api/integrations/microsoft/status', { token: opts.token });
870
+ case 'conectar_microsoft365': {
871
+ const data = await require('./api').api('/api/integrations/microsoft/connect', { method: 'POST', body: { source: 'cli' }, token: opts.token });
872
+ return { ok: true, authUrl: data.authUrl, instrucao: 'Abra o link no navegador, autorize sua conta Microsoft e volte ao Terminal Smart.' };
873
+ }
874
+ case 'listar_emails_outlook': {
875
+ const p = new URLSearchParams({ limit: String(Math.min(30, Math.max(1, parseInt(input.limite) || 15))) });
876
+ if (input.somente_nao_lidos) p.set('unread', '1'); if (input.priorizar) p.set('priority', '1');
877
+ if (input.consulta) p.set('query', input.consulta); if (input.pasta) p.set('folder', input.pasta);
878
+ return await require('./api').api('/api/integrations/microsoft/mail/messages?' + p, { token: opts.token });
879
+ }
880
+ case 'ler_email_outlook':
881
+ return await require('./api').api('/api/integrations/microsoft/mail/message?' + new URLSearchParams({ id: input.id_email }), { token: opts.token });
882
+ case 'listar_pastas_outlook':
883
+ return await require('./api').api('/api/integrations/microsoft/mail/folders', { token: opts.token });
884
+ case 'alterar_email_outlook':
885
+ return await require('./api').api('/api/integrations/microsoft/mail/modify', { method: 'POST', token: opts.token, body: { messageId: input.id_email, action: input.acao, folderId: input.id_pasta, confirmed: true } });
886
+ case 'criar_resposta_outlook':
887
+ return await require('./api').api('/api/integrations/microsoft/mail/replies', { method: 'POST', token: opts.token, body: { messageId: input.id_email, body: input.corpo, replyAll: input.para_todos === true, confirmed: true } });
888
+ case 'criar_encaminhamento_outlook':
889
+ 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
+ case 'obter_anexo_outlook':
891
+ return { downloadUrl: '/api/integrations/microsoft/mail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
892
+ case 'criar_rascunho_outlook':
893
+ return await require('./api').api('/api/integrations/microsoft/mail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
894
+ case 'enviar_rascunho_outlook':
895
+ return await require('./api').api('/api/integrations/microsoft/mail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
896
+ case 'status_google_workspace':
897
+ return await require('./api').api('/api/integrations/google/status', { token: opts.token });
898
+ case 'conectar_google_workspace': {
899
+ const data = await require('./api').api('/api/integrations/google/connect', { method: 'POST', body: { source: 'cli' }, token: opts.token });
900
+ return { ok: true, authUrl: data.authUrl, instrucao: 'Abra o link no navegador, autorize sua conta Google e volte ao Terminal Smart.' };
901
+ }
902
+ case 'listar_emails_gmail': {
903
+ const p = new URLSearchParams({ limit: String(Math.min(30, Math.max(1, parseInt(input.limite) || 15))) });
904
+ if (input.somente_nao_lidos) p.set('unread', '1'); if (input.priorizar) p.set('priority', '1');
905
+ if (input.consulta) p.set('query', input.consulta); if (input.pasta) p.set('folder', input.pasta);
906
+ return await require('./api').api('/api/integrations/google/gmail/messages?' + p, { token: opts.token });
907
+ }
908
+ case 'ler_email_gmail': {
909
+ const p = new URLSearchParams({ id: input.id_email }); if (input.conversa_completa) p.set('thread', '1');
910
+ return await require('./api').api('/api/integrations/google/gmail/message?' + p, { token: opts.token });
911
+ }
912
+ case 'listar_pastas_gmail':
913
+ return await require('./api').api('/api/integrations/google/gmail/labels', { token: opts.token });
914
+ case 'alterar_email_gmail':
915
+ return await require('./api').api('/api/integrations/google/gmail/modify', { method: 'POST', token: opts.token, body: { messageId: input.id_email, action: input.acao, label: input.marcador, confirmed: true } });
916
+ case 'criar_resposta_gmail':
917
+ return await require('./api').api('/api/integrations/google/gmail/replies', { method: 'POST', token: opts.token, body: { messageId: input.id_email, body: input.corpo, replyAll: input.para_todos === true, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
918
+ case 'criar_encaminhamento_gmail':
919
+ 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
+ case 'obter_anexo_gmail':
921
+ return { downloadUrl: '/api/integrations/google/gmail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
922
+ case 'criar_rascunho_gmail':
923
+ return await require('./api').api('/api/integrations/google/gmail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
924
+ case 'enviar_rascunho_gmail':
925
+ return await require('./api').api('/api/integrations/google/gmail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
926
+ case 'listar_arquivos_drive': {
927
+ const p = new URLSearchParams({ limit: String(Math.min(100, Math.max(1, parseInt(input.limite) || 30))) });
928
+ if (input.busca) p.set('query', input.busca); if (input.tipo_mime) p.set('mimeType', input.tipo_mime);
929
+ return await require('./api').api('/api/integrations/google/drive/files?' + p, { token: opts.token });
930
+ }
931
+ case 'ler_google_docs':
932
+ return await require('./api').api('/api/integrations/google/docs/document?id=' + encodeURIComponent(input.id_documento), { token: opts.token });
933
+ case 'editar_google_docs':
934
+ return await require('./api').api('/api/integrations/google/docs/replace', { method: 'POST', token: opts.token, body: { documentId: input.id_documento, search: input.localizar, replacement: input.substituir_por || '', matchCase: input.diferenciar_maiusculas !== false, createCopy: input.criar_copia === true, copyName: input.nome_copia, confirmed: true } });
935
+ case 'ler_google_sheets': {
936
+ const p = new URLSearchParams({ id: input.id_planilha }); if (input.intervalo) p.set('range', input.intervalo);
937
+ return await require('./api').api('/api/integrations/google/sheets/values?' + p, { token: opts.token });
938
+ }
939
+ case 'editar_google_sheets':
940
+ 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 } });
783
941
  case 'ler_apresentacao': {
784
942
  const target = _abs(input.caminho, baseDir);
785
943
  if (_outsideScope(target)) return _scopeError(target);