terminal-smart-cli 0.97.62 → 0.97.64

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 CHANGED
@@ -244,7 +244,7 @@ async function personalAiCmd(args = []) {
244
244
  if (['usar','use','modo','mode'].includes(sub)) {
245
245
  const mode=String(args[1] || '').toLowerCase();
246
246
  const state=await api('/api/personal-ai/mode',{method:'POST',token,body:{mode}});
247
- cfg = config.save({ forceTsCloud: mode === 'automatico' ? true : undefined });
247
+ cfg = config.save({ forceTsCloud: mode === 'automatico' ? true : undefined, personalModePreference:mode });
248
248
  show(state); return;
249
249
  }
250
250
  if (['desconectar','disconnect'].includes(sub)) {
@@ -1031,6 +1031,7 @@ async function chatRepl() {
1031
1031
  if (['sair', 'exit', 'quit', '/sair', '/exit'].includes(low)) break;
1032
1032
  try {
1033
1033
  if (['/nova', '/new'].includes(low)) {
1034
+ _resetLocalAgentState(agentCwd);
1034
1035
  await ensureConv(token, 'chat', process.cwd(), true);
1035
1036
  console.log(ui.okLine(T.new_conv) + '\n');
1036
1037
  } else if (['/ajuda', '/help', '/?'].includes(low)) {
@@ -1429,6 +1430,10 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1429
1430
  // --navegador/--browser (EXPERIMENTAL, Evolve 5): dá ao agente um Chrome headless real
1430
1431
  // (abrir/ler/clicar/digitar/print com visão). Fora da flag a tool nem existe.
1431
1432
  const useBrowser = process.argv.includes('--navegador') || process.argv.includes('--browser');
1433
+ // Recuperação explícita quando o planejador está indisponível. O executor ainda
1434
+ // respeita política, aprovações, orçamento, ferramentas e provas; apenas evita
1435
+ // que uma otimização de planejamento vire ponto único de falha.
1436
+ const directExecution = process.argv.includes('--direto') || process.argv.includes('--direct');
1432
1437
  const _fti = process.argv.findIndex(a => a === '--ferramentas' || a === '--tools');
1433
1438
  const allowedTools = _fti >= 0
1434
1439
  ? String(process.argv[_fti + 1] || '').split(',').map(v => v.trim()).filter(Boolean)
@@ -1505,17 +1510,33 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1505
1510
  let sharedConversationId = null;
1506
1511
  let personalAgentLlm = null;
1507
1512
  let personalAgentMode = null;
1513
+ try { sharedConversationId = await ensureConv(token, 'agent', startCwd); } catch (e) {
1514
+ if (!streamJson) _hlog(' ' + C.warn('▲ contexto compartilhado indisponível; a execução local continua sem memória remota.'));
1515
+ }
1516
+ let shared = null;
1508
1517
  try {
1509
- sharedConversationId = await ensureConv(token, 'agent', startCwd);
1510
- const shared = await api('/api/channel/resolve', { method:'POST', token, body:{
1518
+ if (sharedConversationId) shared = await api('/api/channel/resolve', { method:'POST', token, body:{
1511
1519
  surface:'cli', text:task, conversationId:sharedConversationId,
1512
1520
  } });
1513
1521
  if (shared?.ok && shared.promptBlock) sharedChannelPrompt = shared.promptBlock;
1522
+ } catch (_) {
1523
+ if (!streamJson) _hlog(' ' + C.warn('▲ não foi possível carregar o agente/projeto compartilhado; seguindo no diretório atual.'));
1524
+ }
1525
+ let personalSelectionRequired = false;
1526
+ try {
1514
1527
  const personalState = await api('/api/personal-ai/status', { token });
1515
- const personalReady = !cfg.forceTsCloud && personalState?.ok && personalState.mode !== 'automatico'
1528
+ // O modo é da CONTA e precisa ser idêntico no Telegram/Web/App/CLI. Uma
1529
+ // preferência local antiga não pode vencer silenciosamente o estado remoto
1530
+ // e consumir créditos TS quando a conta está em ChatGPT/Claude.
1531
+ const selectedPersonal = personalState?.ok && personalState.mode !== 'automatico';
1532
+ personalSelectionRequired = selectedPersonal;
1533
+ const personalReady = selectedPersonal
1516
1534
  && (personalState.mode === 'chatgpt' ? personalState.chatgpt
1517
1535
  : personalState.mode === 'claude' ? personalState.claude
1518
1536
  : personalState.chatgpt && personalState.claude);
1537
+ if (selectedPersonal && !personalReady) {
1538
+ throw new Error(`modo ${personalState.mode} selecionado, mas a conta necessária não está conectada`);
1539
+ }
1519
1540
  if (personalReady) {
1520
1541
  personalAgentMode = personalState.mode;
1521
1542
  personalAgentLlm = require('../lib/personal-agent-llm').createPersonalAgentLlm({
@@ -1524,7 +1545,14 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1524
1545
  });
1525
1546
  if (!streamJson) _hlog(' ' + C.dim(`IA pessoal como executora local: ${personalAgentMode === 'chatgpt' ? 'ChatGPT/Codex' : personalAgentMode === 'claude' ? 'Claude' : 'ChatGPT/Codex + Claude'} · 0 créditos TS`));
1526
1547
  }
1527
- } catch (_) {}
1548
+ } catch (e) {
1549
+ if (personalSelectionRequired || (cfg.personalModePreference && cfg.personalModePreference !== 'automatico')) {
1550
+ if (!streamJson) _hlog(' ' + C.warn(`▲ IA pessoal indisponível (${String(e.message || e).slice(0, 120)}). Execução cancelada para não consumir créditos TS sem aviso.`));
1551
+ durableOps.interrupt(startCwd, 'personal_ai_unavailable');
1552
+ return;
1553
+ }
1554
+ if (!streamJson) _hlog(' ' + C.warn('▲ não foi possível confirmar a IA da conta; usando o modo Automático do TS nesta execução (pode consumir créditos).'));
1555
+ }
1528
1556
  let out;
1529
1557
  const durableCwd = _wt ? _wt.base : startCwd;
1530
1558
  durableOps.begin(durableCwd, task, { engine:'cli', naturalResume:!!(interruptedOp && naturalResume) });
@@ -1540,7 +1568,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1540
1568
  const _maxDurationMs = 1000 * (_ti >= 0 ? Number(process.argv[_ti + 1]) : (_inlineMaxSeconds || 0));
1541
1569
  const _maxTokens = _tki >= 0 ? Number(process.argv[_tki + 1]) : _inlineMaxTokens;
1542
1570
  out = await agent.run(task, {
1543
- token, lang: cfg.lang || 'pt', yes: YES, autoAll: (YOLO || _inlineYolo || autoAllIn), model, priorMessages, cwd: startCwd, readOnly, plan, accountPlan: cfg.plan || 'free', browser: useBrowser, allowedTools, maxIter: _maxPassos,
1571
+ token, lang: cfg.lang || 'pt', yes: YES, autoAll: (YOLO || _inlineYolo || autoAllIn), model, priorMessages, cwd: startCwd, readOnly, plan, accountPlan: cfg.plan || 'free', browser: useBrowser, allowedTools, maxIter: _maxPassos, orchestrate: !directExecution,
1544
1572
  conversationId:sharedConversationId, sharedChannelPrompt,
1545
1573
  llmFn:personalAgentLlm || undefined,
1546
1574
  keyOverride:personalAgentLlm ? { key:'personal-subscription', baseUrl:'personal://subscription', source:'personal', billingAuthoritative:true } : undefined,
@@ -2873,7 +2901,7 @@ async function conectarCmd(words) {
2873
2901
  // ts conectar nuvem — volta a usar os créditos do TS
2874
2902
  if (sub === 'nuvem' || sub === 'cloud' || sub === 'ts') {
2875
2903
  keyring.usar(null);
2876
- cfg = config.save({ forceTsCloud: true });
2904
+ cfg = config.save({ forceTsCloud: true, personalModePreference:'automatico' });
2877
2905
  console.log(' ' + C.ok('✔ ') + (en ? 'back to TS Cloud (your credits).' : 'de volta ao TS Cloud (seus créditos).'));
2878
2906
  return;
2879
2907
  }
@@ -3370,8 +3398,17 @@ function idioma(l) {
3370
3398
  console.log(ui.okLine(T.lang_set(lang)));
3371
3399
  }
3372
3400
 
3401
+ function _resetLocalAgentState(cwd) {
3402
+ const _p = require('path'), _fs = require('fs'), _os = require('os'), _cr = require('crypto');
3403
+ const root = _p.resolve(cwd || process.cwd());
3404
+ const localSession = _p.join(_os.homedir(), '.ts', 'agente', _cr.createHash('md5').update(root.toLowerCase()).digest('hex').slice(0, 12) + '.json');
3405
+ try { _fs.unlinkSync(localSession); } catch (e) { if (!e || e.code !== 'ENOENT') throw e; }
3406
+ require('../lib/durable-operation').reset(root);
3407
+ }
3408
+
3373
3409
  async function nova() {
3374
3410
  const token = needToken();
3411
+ _resetLocalAgentState(process.cwd());
3375
3412
  await ensureConv(token, 'chat', process.cwd(), true);
3376
3413
  console.log(ui.okLine(T.new_conv));
3377
3414
  }
package/lib/agent.js CHANGED
@@ -241,10 +241,24 @@ function isInspectionCommand(command) {
241
241
  }
242
242
 
243
243
  function parseStageJson(text) {
244
- const src = String(text || '').replace(/```(?:json)?/gi, '').trim();
245
- const a = src.indexOf('{'), b = src.lastIndexOf('}');
246
- if (a < 0 || b <= a) return null;
247
- try { return JSON.parse(src.slice(a, b + 1)); } catch (_) { return null; }
244
+ return require('./structured-json').parse(text);
245
+ }
246
+
247
+ function powershellFileChainIssue(command) {
248
+ if (process.platform !== 'win32') return false;
249
+ const s = String(command || '');
250
+ return /^\s*(?:powershell|pwsh)(?:\.exe)?\b[\s\S]*?\s-File\s+(?:"[^"]+"|'[^']+'|\S+)[\s\S]*?(?:;|&&|\|\|)/i.test(s);
251
+ }
252
+
253
+ function reconcileBillingClaims(text, { credits = 0, steps = 0, personalSubscription = false } = {}) {
254
+ let out = String(text || '').trim();
255
+ const deniesCharge = /(?:nenhum|n[aã]o houve|sem)\s+(?:cr[eé]dito\w*\s+)?(?:foi\s+|foram\s+)?(?:cobrad|consumid|gasto)|0\s+cr[eé]ditos?\s+(?:foram\s+)?(?:cobrad|consumid)/i.test(out);
256
+ const deniesExecution = /nenhuma\s+(?:a[cç][aã]o|execu[cç][aã]o)\s+(?:foi\s+)?(?:iniciad|executad|realizad)/i.test(out);
257
+ const notes = [];
258
+ if (Number(credits) > 0 && deniesCharge) notes.push(`Registro real de cobrança: ${Number(credits)} crédito(s) TS consumido(s).`);
259
+ if (Number(steps) > 0 && deniesExecution) notes.push(`Registro real de execução: ${Number(steps)} etapa(s) executada(s).`);
260
+ if (personalSubscription && Number(credits) === 0 && !/0\s+cr[eé]ditos?\s+TS/i.test(out)) notes.push('Cobrança TS nesta execução: 0 créditos (assinatura pessoal).');
261
+ return notes.length ? `${out}\n\n${notes.join(' ')}`.trim() : out;
248
262
  }
249
263
 
250
264
  const MATERIAL_DECISIONS = new Set(['recipient', 'destination', 'irreversible', 'credential', 'external_authorization', 'payment', 'business_choice']);
@@ -1907,7 +1921,10 @@ async function run(task, opts = {}) {
1907
1921
  // Só o comando LOCAL (executar_comando roda NESTE processo/cwd); o remoto tem outro pid/cwd
1908
1922
  // e segue pelo gate de aprovação comum.
1909
1923
  if (result === undefined && name === 'executar_comando') {
1910
- if (windowsUnsupportedUnixCommand(input.comando)) {
1924
+ if (powershellFileChainIssue(input.comando)) {
1925
+ onStep({ name, detail: argsShort(name, input), blocked: true });
1926
+ result = { erro: 'COMANDO BLOQUEADO ANTES DA EXECUÇÃO: no PowerShell, -File encerra a linha de comando e não pode receber uma cadeia com ;, && ou ||. Execute o script em uma chamada e o próximo comando em outra.', _recovery: 'Separe a cadeia: primeiro `powershell -NoProfile -File "script.ps1"`; depois faça uma nova chamada executar_comando para o comando seguinte.' };
1927
+ } else if (windowsUnsupportedUnixCommand(input.comando)) {
1911
1928
  const hint = commandRecoveryHint(input.comando, 'preflight');
1912
1929
  onStep({ name, detail: argsShort(name, input), blocked: true });
1913
1930
  result = { erro: 'COMANDO BLOQUEADO ANTES DA EXECUÇÃO: incompatível com cmd.exe no Windows. Não execute nem repita este comando.', _recovery: hint };
@@ -2441,6 +2458,11 @@ async function run(task, opts = {}) {
2441
2458
  ? `Não declarei a tarefa como concluída porque nenhuma ação verificável foi executada.\n\n${finalText}`
2442
2459
  : `I did not mark the task complete because no verifiable action was executed.\n\n${finalText}`;
2443
2460
 
2461
+ finalText = reconcileBillingClaims(finalText, {
2462
+ credits: charged + _visionCredits, steps,
2463
+ personalSubscription: !!opts.keyOverride && opts.keyOverride.source === 'personal',
2464
+ });
2465
+
2444
2466
  if (_hooks._any) { try { _hooksMod.run(_hooks, 'Stop', { cwd, text: finalText, steps }); } catch (_) {} }
2445
2467
  _logEp(stopped ? 'cancel' : (completion.ok ? 'done' : 'stuck'));
2446
2468
  await _bill();
@@ -2534,4 +2556,4 @@ async function run(task, opts = {}) {
2534
2556
  return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
2535
2557
  }
2536
2558
 
2537
- module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
2559
+ module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, powershellFileChainIssue, reconcileBillingClaims, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
@@ -56,6 +56,11 @@ function findInterrupted(cwd) {
56
56
  if (op.status === 'running') return interrupt(cwd, 'process_restart');
57
57
  return op;
58
58
  }
59
+ function reset(cwd) {
60
+ const target = fileFor(cwd);
61
+ try { fs.unlinkSync(target); return true; }
62
+ catch (e) { if (e && e.code === 'ENOENT') return false; throw e; }
63
+ }
59
64
  function isNaturalResume(text) { return /\b(?:continu(?:a|ar|e|emos)|retom(?:a|ar|e|emos)|seguir|prossiga|onde\s+parou)\b/i.test(String(text || '')); }
60
65
 
61
- module.exports = { DIR, keyFor, fileFor, load, save, begin, patch, toolStarted, complete, interrupt, finish, findInterrupted, isNaturalResume };
66
+ module.exports = { DIR, keyFor, fileFor, load, save, begin, patch, toolStarted, complete, interrupt, finish, findInterrupted, reset, isNaturalResume };
package/lib/meta.js CHANGED
@@ -186,6 +186,9 @@ function normalizeMetaArtifact(relativePath, raw) {
186
186
  const start = a >= 0 && (o < 0 || a < o) ? a : o;
187
187
  const end = Math.max(text.lastIndexOf(']'), text.lastIndexOf('}'));
188
188
  if (start < 0 || end < start) throw new Error('resposta sem JSON');
189
+ // Artefato FINAL permanece estrito. O parser tolerante é reservado ao plano,
190
+ // que ainda passa por validação; aceitar conteúdo truncado aqui produziria
191
+ // um arquivo válido porém semanticamente incompleto.
189
192
  const parsed = JSON.parse(text.slice(start, end + 1));
190
193
  text = JSON.stringify(parsed, null, 2) + '\n';
191
194
  }
@@ -1222,6 +1225,8 @@ async function designPhase(goal, designer, token, opts = {}) {
1222
1225
  // Parser tolerante: modelos pequenos às vezes devolvem JSON com fence, vírgula
1223
1226
  // sobrando, ou TRUNCADO no meio (max_tokens acabou). Recupera o máximo possível.
1224
1227
  function _tolerantJson(text) {
1228
+ const structured = require('./structured-json').parse(text);
1229
+ if (structured != null) return structured;
1225
1230
  let s = String(text).replace(/```(?:json)?/gi, '').trim();
1226
1231
  const open = s.indexOf('{');
1227
1232
  if (open >= 0) s = s.slice(open);
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ // Extrai o primeiro JSON completo respeitando strings/escapes. Se a resposta foi
4
+ // truncada, fecha apenas containers ainda abertos; validação semântica continua
5
+ // sendo responsabilidade do chamador (ex.: quantidade mínima de itens).
6
+ function candidate(text) {
7
+ const src = String(text || '').replace(/```(?:json)?/gi, '').trim();
8
+ const start = [...src].findIndex(ch => ch === '{' || ch === '[');
9
+ if (start < 0) return '';
10
+ const stack = [];
11
+ let quoted = false, escaped = false, lastSafe = start, lastComplete = start;
12
+ for (let i = start; i < src.length; i++) {
13
+ const ch = src[i];
14
+ if (quoted) {
15
+ if (escaped) escaped = false;
16
+ else if (ch === '\\') escaped = true;
17
+ else if (ch === '"') quoted = false;
18
+ continue;
19
+ }
20
+ if (ch === '"') { quoted = true; continue; }
21
+ if (ch === '{') stack.push('}');
22
+ else if (ch === '[') stack.push(']');
23
+ else if (ch === '}' || ch === ']') {
24
+ if (!stack.length || stack[stack.length - 1] !== ch) return '';
25
+ stack.pop();
26
+ lastComplete = i + 1;
27
+ if (!stack.length) return src.slice(start, i + 1);
28
+ }
29
+ if (!quoted && stack.length && /[,}\]]/.test(ch)) lastSafe = i + 1;
30
+ }
31
+ // Não tente adivinhar o restante de uma string cortada. Volte ao último
32
+ // delimitador estrutural seguro e feche os containers ainda existentes.
33
+ let out = src.slice(start, quoted ? lastComplete : src.length).replace(/,\s*$/, '');
34
+ const open = [];
35
+ quoted = false; escaped = false;
36
+ for (const ch of out) {
37
+ if (quoted) {
38
+ if (escaped) escaped = false;
39
+ else if (ch === '\\') escaped = true;
40
+ else if (ch === '"') quoted = false;
41
+ } else if (ch === '"') quoted = true;
42
+ else if (ch === '{') open.push('}');
43
+ else if (ch === '[') open.push(']');
44
+ else if ((ch === '}' || ch === ']') && open[open.length - 1] === ch) open.pop();
45
+ }
46
+ return out + open.reverse().join('');
47
+ }
48
+
49
+ function parse(text) {
50
+ const raw = candidate(text);
51
+ if (!raw) return null;
52
+ const attempts = [raw, raw.replace(/,\s*([}\]])/g, '$1')];
53
+ for (const value of attempts) { try { return JSON.parse(value); } catch (_) {} }
54
+ return null;
55
+ }
56
+
57
+ module.exports = { candidate, parse };
package/lib/tools.js CHANGED
@@ -452,6 +452,9 @@ function _normalizeWindowsReadOnlyCommand(cmd, platform = process.platform) {
452
452
  function _shellGotcha(cmd) {
453
453
  const s = String(cmd || '');
454
454
  const multi = /[\r\n]/.test(s);
455
+ if (process.platform === 'win32'
456
+ && /^\s*(?:powershell|pwsh)(?:\.exe)?\b[\s\S]*?\s-File\s+(?:"[^"]+"|'[^']+'|\S+)[\s\S]*?(?:;|&&|\|\|)/i.test(s))
457
+ return 'PowerShell -File não aceita uma cadeia posterior na mesma chamada. Execute o .ps1 sozinho e faça o próximo comando em outra chamada.';
455
458
  // executar_comando usa cmd.exe. Modelos alternam facilmente entre os dois
456
459
  // dialetos e `Select-String`/`Get-Content` soltos viram "não reconhecido",
457
460
  // desperdiçando uma rodada antes de o recovery engine conseguir explicar.
@@ -805,6 +808,7 @@ async function execute(name, input, opts = {}) {
805
808
  if (!Object.prototype.hasOwnProperty.call(input, 'conteudo')) {
806
809
  return { erro: 'Informe o conteúdo do arquivo em conteudo. A escrita foi recusada para não apagar o arquivo com conteúdo ausente.' };
807
810
  }
811
+ if (String(input.conteudo ?? '').includes('\uFFFD')) return { erro: 'CODIFICAÇÃO BLOQUEADA: o conteúdo contém o caractere de substituição �. Releia a fonte em UTF-8 e corrija o texto antes de gravar.' };
808
812
  let p = _abs(input.caminho, baseDir);
809
813
  { const g = _guardTsHome(p); if (g) return { erro: g }; }
810
814
  // Confinamento (missões): modelos fracos inventam pastas absolutas
@@ -863,6 +867,7 @@ async function execute(name, input, opts = {}) {
863
867
  }
864
868
  const orig = fs.readFileSync(p, 'utf8');
865
869
  let buscar = String(input.buscar ?? ''), substituir = String(input.substituir ?? '');
870
+ if (substituir.includes('\uFFFD') && !orig.includes('\uFFFD')) return { erro: 'CODIFICAÇÃO BLOQUEADA: a edição introduziria o caractere de substituição �. Releia a fonte em UTF-8 e envie o texto correto.' };
866
871
  if (!buscar) return { erro: 'buscar vazio.' };
867
872
  // tolerância CRLF: o modelo manda \n mas arquivo Windows tem \r\n — normaliza o par
868
873
  if (!orig.includes(buscar) && orig.includes('\r\n')) {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.62",
3
+ "version": "0.97.64",
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/durable-operation.test.js && node test/mission-tool-cache.test.js && node test/content-quality-runtime-files.test.js && node test/personal-agent-llm.test.js && node test/personal-inspector-efficiency.test.js && node test/personal-inspector-context.test.js && node test/planner-fallback.test.js && node test/cli-agent-regressions.test.js"
9
+ "test": "node test/durable-operation.test.js && node test/mission-tool-cache.test.js && node test/content-quality-runtime-files.test.js && node test/personal-agent-llm.test.js && node test/personal-inspector-efficiency.test.js && node test/personal-inspector-context.test.js && node test/planner-fallback.test.js && node test/cli-agent-regressions.test.js && node test/harness-hardening.test.js"
10
10
  },
11
11
  "files": [
12
12
  "bin",