terminal-smart-cli 0.97.63 → 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)) {
@@ -1509,17 +1510,33 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1509
1510
  let sharedConversationId = null;
1510
1511
  let personalAgentLlm = null;
1511
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;
1512
1517
  try {
1513
- sharedConversationId = await ensureConv(token, 'agent', startCwd);
1514
- const shared = await api('/api/channel/resolve', { method:'POST', token, body:{
1518
+ if (sharedConversationId) shared = await api('/api/channel/resolve', { method:'POST', token, body:{
1515
1519
  surface:'cli', text:task, conversationId:sharedConversationId,
1516
1520
  } });
1517
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 {
1518
1527
  const personalState = await api('/api/personal-ai/status', { token });
1519
- 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
1520
1534
  && (personalState.mode === 'chatgpt' ? personalState.chatgpt
1521
1535
  : personalState.mode === 'claude' ? personalState.claude
1522
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
+ }
1523
1540
  if (personalReady) {
1524
1541
  personalAgentMode = personalState.mode;
1525
1542
  personalAgentLlm = require('../lib/personal-agent-llm').createPersonalAgentLlm({
@@ -1528,7 +1545,14 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1528
1545
  });
1529
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`));
1530
1547
  }
1531
- } 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
+ }
1532
1556
  let out;
1533
1557
  const durableCwd = _wt ? _wt.base : startCwd;
1534
1558
  durableOps.begin(durableCwd, task, { engine:'cli', naturalResume:!!(interruptedOp && naturalResume) });
@@ -2877,7 +2901,7 @@ async function conectarCmd(words) {
2877
2901
  // ts conectar nuvem — volta a usar os créditos do TS
2878
2902
  if (sub === 'nuvem' || sub === 'cloud' || sub === 'ts') {
2879
2903
  keyring.usar(null);
2880
- cfg = config.save({ forceTsCloud: true });
2904
+ cfg = config.save({ forceTsCloud: true, personalModePreference:'automatico' });
2881
2905
  console.log(' ' + C.ok('✔ ') + (en ? 'back to TS Cloud (your credits).' : 'de volta ao TS Cloud (seus créditos).'));
2882
2906
  return;
2883
2907
  }
@@ -3374,8 +3398,17 @@ function idioma(l) {
3374
3398
  console.log(ui.okLine(T.lang_set(lang)));
3375
3399
  }
3376
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
+
3377
3409
  async function nova() {
3378
3410
  const token = needToken();
3411
+ _resetLocalAgentState(process.cwd());
3379
3412
  await ensureConv(token, 'chat', process.cwd(), true);
3380
3413
  console.log(ui.okLine(T.new_conv));
3381
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.63",
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",