terminal-smart-cli 0.97.49 → 0.97.54

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
@@ -484,7 +484,10 @@ async function sendMessage(token, content) {
484
484
  let buf = '', errMsg = null;
485
485
  try {
486
486
  await sse('/api/ia/chat', {
487
- token, body: { conversationId: convId, content, projectId: memoryScope.projectId, workstreamId: memoryScope.workstreamId, surface: 'cli' },
487
+ // Paridade com Web/App: o chat comum do CLI também declara o modo
488
+ // automático. Sem isso o backend tratava a chamada como legado e podia
489
+ // reutilizar um agente/modelo premium antigo, fora da equipe oficial.
490
+ token, body: { conversationId: convId, content, projectId: memoryScope.projectId, workstreamId: memoryScope.workstreamId, surface: 'cli', mode: 'auto' },
488
491
  onEvent: (ev) => {
489
492
  if (ev.delta) { buf += ev.delta; sp.text(`${T.receiving} ${fmtK(buf.length)}`); }
490
493
  if (ev.error) errMsg = ev.error;
@@ -1100,6 +1103,15 @@ async function chatRepl() {
1100
1103
  console.log(' ' + C.dim((cfg.lang === 'en' ? 'working folder: ' : 'pasta de trabalho: ') + agentCwd));
1101
1104
  } catch (_) { console.error(ui.errLine((cfg.lang === 'en' ? 'no such folder: ' : 'pasta inexistente: ') + target)); }
1102
1105
  } else {
1106
+ const personalIntent = require('../lib/personal-ai-intent').parsePersonalAiIntent(msg);
1107
+ if (personalIntent) {
1108
+ if (personalIntent.action === 'connect') await personalAiCmd(['conectar', personalIntent.provider]);
1109
+ else if (personalIntent.action === 'disconnect') await personalAiCmd(['desconectar', personalIntent.provider]);
1110
+ else if (personalIntent.action === 'use') await personalAiCmd(['usar', personalIntent.mode]);
1111
+ else if (personalIntent.action === 'status') await personalAiCmd(['status']);
1112
+ else console.log(ui.infoLine('Para proteger credenciais, use o fluxo interativo de login exibido pelo CLI.'));
1113
+ continue;
1114
+ }
1103
1115
  // ROTEADOR: linguagem natural pura — o sistema decide o caminho e AVISA
1104
1116
  const r = await router.route(msg, token);
1105
1117
  if (r.dest === 'agente') {
@@ -1430,6 +1442,25 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1430
1442
  try { const s = JSON.parse(_fs.readFileSync(sessFile, 'utf8')); priorMessages = s.messages; if (!cwdIn && s.cwd) startCwd = s.cwd; if (!streamJson) console.log(' ' + C.dim((cfg.lang === 'en' ? '↻ resuming this folder\'s session (' : '↻ continuando a sessão desta pasta (') + (s.messages ? s.messages.length : 0) + ' msgs)')); }
1431
1443
  catch (_) { if (!streamJson) console.log(' ' + C.dim(cfg.lang === 'en' ? '(no prior session here — starting fresh)' : '(sem sessão anterior aqui — começando nova)')); }
1432
1444
  }
1445
+ // Retomada por linguagem natural depois de queda/restart. O estado parcial
1446
+ // não é tratado como concluído: o agente recebe a tarefa original e deve
1447
+ // inspecionar o projeto antes de continuar. Se alguma ferramenta chegou a
1448
+ // iniciar, a retomada pede confirmação para não repetir uma ação às cegas.
1449
+ const durableOps = require('../lib/durable-operation');
1450
+ const interruptedOp = durableOps.findInterrupted(startCwd);
1451
+ const naturalResume = durableOps.isNaturalResume(task);
1452
+ if (interruptedOp && naturalResume) {
1453
+ if (interruptedOp.hadTools && !(YES || YOLO || _inlineYolo || autoAllIn)) {
1454
+ if (!process.stdin.isTTY && !askFn) {
1455
+ if (!streamJson) console.log(' ' + C.warn('▲ tarefa interrompida após uma ação; rode novamente com --continuar após revisar o estado.'));
1456
+ return;
1457
+ }
1458
+ const answer = await (askFn || ui.ask)('A tarefa anterior já havia iniciado ações. Retomar após conferir o estado atual? [s/N] ');
1459
+ if (!/^s|^y/i.test(String(answer || ''))) { durableOps.patch(startCwd, { status:'paused' }); return; }
1460
+ }
1461
+ task = `${interruptedOp.task}\n\nRETOMADA APÓS INTERRUPÇÃO: confira primeiro o estado real do projeto e não repita ações que já estejam aplicadas. Pedido atual: ${task}`;
1462
+ if (!streamJson) console.log(' ' + C.dim('↻ retomando a tarefa interrompida desta pasta pela linguagem natural'));
1463
+ }
1433
1464
  // WORKTREE: cria a cópia isolada e passa a trabalhar nela (todas as edições ficam confinadas ali).
1434
1465
  let _wt = null;
1435
1466
  if (useWorktree) {
@@ -1470,25 +1501,31 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1470
1501
  // É best-effort para o CLI continuar funcional durante indisponibilidade do backend.
1471
1502
  let sharedChannelPrompt = '';
1472
1503
  let sharedConversationId = null;
1504
+ let personalAgentLlm = null;
1505
+ let personalAgentMode = null;
1473
1506
  try {
1474
1507
  sharedConversationId = await ensureConv(token, 'agent', startCwd);
1475
1508
  const shared = await api('/api/channel/resolve', { method:'POST', token, body:{
1476
1509
  surface:'cli', text:task, conversationId:sharedConversationId,
1477
1510
  } });
1478
1511
  if (shared?.ok && shared.promptBlock) sharedChannelPrompt = shared.promptBlock;
1479
- const personal = await api('/api/personal-ai/respond', { method:'POST', token, body:{
1480
- surface:'cli', text:task, conversationId:sharedConversationId,
1481
- agentId:shared?.agent?.id || null, usageId:`cli-${sharedConversationId}-${Date.now()}`,
1482
- } });
1483
- if (personal?.ok && Array.isArray(personal.results) && personal.results.length) {
1484
- const collaboration = personal.results.map(item =>
1485
- `### ${item.provider === 'chatgpt' ? 'ChatGPT/Codex' : 'Claude'} (plano pessoal)\n${String(item.text || '').slice(0, 12000)}`
1486
- ).join('\n\n');
1487
- sharedChannelPrompt += `\n\nPLANEJAMENTO/REVISÃO DE MODELOS PESSOAIS (valide com ferramentas locais antes de afirmar execução):\n${collaboration}`;
1488
- if (!streamJson) _hlog(' ' + C.dim(`IA pessoal: ${personal.results.map(item => item.provider === 'chatgpt' ? 'ChatGPT/Codex' : 'Claude').join(' + ')} · 0 créditos TS`));
1512
+ const personalState = await api('/api/personal-ai/status', { token });
1513
+ const personalReady = personalState?.ok && personalState.mode !== 'automatico'
1514
+ && (personalState.mode === 'chatgpt' ? personalState.chatgpt
1515
+ : personalState.mode === 'claude' ? personalState.claude
1516
+ : personalState.chatgpt && personalState.claude);
1517
+ if (personalReady) {
1518
+ personalAgentMode = personalState.mode;
1519
+ personalAgentLlm = require('../lib/personal-agent-llm').createPersonalAgentLlm({
1520
+ api, token, mode:personalAgentMode, conversationId:sharedConversationId,
1521
+ agentId:shared?.agent?.id || null,
1522
+ });
1523
+ if (!streamJson) _hlog(' ' + C.dim(`IA pessoal como executora local: ${personalAgentMode === 'chatgpt' ? 'ChatGPT/Codex' : personalAgentMode === 'claude' ? 'Claude' : 'ChatGPT/Codex + Claude'} · 0 créditos TS`));
1489
1524
  }
1490
1525
  } catch (_) {}
1491
1526
  let out;
1527
+ const durableCwd = _wt ? _wt.base : startCwd;
1528
+ durableOps.begin(durableCwd, task, { engine:'cli', naturalResume:!!(interruptedOp && naturalResume) });
1492
1529
  try {
1493
1530
  // --passos N (ou --steps): quantas iterações o agente pode fazer numa run (default 15, teto 120).
1494
1531
  // Pra missões grandes ("faça tudo de uma vez") sem o pára-e-continua.
@@ -1503,6 +1540,9 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1503
1540
  out = await agent.run(task, {
1504
1541
  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,
1505
1542
  conversationId:sharedConversationId, sharedChannelPrompt,
1543
+ llmFn:personalAgentLlm || undefined,
1544
+ keyOverride:personalAgentLlm ? { key:'personal-subscription', baseUrl:'personal://subscription', source:'personal', billingAuthoritative:true } : undefined,
1545
+ personalSubscription:!!personalAgentLlm,
1506
1546
  maxCredits: _maxCredits, maxDurationMs: _maxDurationMs || undefined, maxTokens: _maxTokens,
1507
1547
  onThinking: (p) => {
1508
1548
  if (p && typeof p === 'object') {
@@ -1512,6 +1552,8 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1512
1552
  sp.text(_statusLine());
1513
1553
  },
1514
1554
  onStep: ({ name, detail, blocked, loop, retry, auto }) => {
1555
+ const durableInternalStep = /^(?:agente_.+|fallback_modelo|fallback_planejador|nova_tentativa|compactar_contexto|gateway|provar_conclusao)$/i.test(String(name || ''));
1556
+ if (name && !blocked && !durableInternalStep) durableOps.toolStarted(durableCwd, name);
1515
1557
  if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
1516
1558
  sp.stop();
1517
1559
  const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('›');
@@ -1542,6 +1584,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1542
1584
  },
1543
1585
  });
1544
1586
  } catch (e) {
1587
+ durableOps.interrupt(durableCwd, e && (e.code || e.message) || 'agent_error');
1545
1588
  if (_liveTimer) clearInterval(_liveTimer);
1546
1589
  sp.stop();
1547
1590
  const hint = providerCapacityHint(e);
@@ -1554,6 +1597,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1554
1597
  }
1555
1598
  if (_liveTimer) clearInterval(_liveTimer);
1556
1599
  sp.stop();
1600
+ durableOps.finish(durableCwd, out);
1557
1601
  // cwd EFETIVO (o agente pode ter feito cd/mudar_diretorio) → devolve ao REPL e persiste.
1558
1602
  const effCwd = out.cwd || startCwd;
1559
1603
  // em worktree NÃO propaga o cwd (o REPL/sessão devem ficar na base, não na cópia descartável —
package/lib/agent.js CHANGED
@@ -145,6 +145,27 @@ function canCloseFromProofs(verifyReport, actions) {
145
145
  return !!(verifyReport && verifyReport.allOk && verifyReport.total > 0 && Array.isArray(actions) && actions.length > 0);
146
146
  }
147
147
 
148
+ // Assinaturas pessoais executam cada papel por um processo separado do Codex/
149
+ // Claude CLI. Para uma alteração local simples, chamar outro modelo apenas para
150
+ // reler o mesmo ledger de ferramentas adiciona bastante contexto sem acrescentar
151
+ // uma prova independente. Nesses casos o harness (resultado da escrita + gates
152
+ // determinísticos) encerra a missão; tarefas complexas, comandos, ações externas,
153
+ // erros ou lotes maiores continuam recebendo o inspetor de IA.
154
+ function independentInspectorDecision({ personalSubscription = false, complexTask = false,
155
+ actions = [], verifyReport = null, unresolvedErrors = [] } = {}) {
156
+ if (!personalSubscription) return { run:true, reason:'managed_plan' };
157
+ if (complexTask) return { run:true, reason:'complex_task' };
158
+ if (Array.isArray(unresolvedErrors) && unresolvedErrors.length) return { run:true, reason:'tool_errors' };
159
+ const rows = Array.isArray(actions) ? actions : [];
160
+ const mutations = rows.filter(a => ['escrever_arquivo', 'editar_arquivo', 'editar_documento', 'editar_planilha'].includes(a && a.name));
161
+ if (!mutations.length) return { run:true, reason:'non_file_action' };
162
+ if (mutations.length > 2 || rows.some(a => ['executar_comando', 'executar_remoto'].includes(a && a.name))) {
163
+ return { run:true, reason:'broader_change' };
164
+ }
165
+ if (verifyReport && verifyReport.total > 0 && !verifyReport.allOk) return { run:true, reason:'failed_proof' };
166
+ return { run:false, reason:verifyReport && verifyReport.total > 0 ? 'deterministic_proof' : 'simple_file_ledger' };
167
+ }
168
+
148
169
  function missionBudgetSuggestion({ limit = 0, spent = 0, required = 0, available = 0 } = {}) {
149
170
  const missing = Math.max(0, Number(required) - Number(available));
150
171
  return Math.max(Number(limit) + 1, Number(spent) + Number(required), Number(limit) + missing);
@@ -1557,6 +1578,7 @@ async function run(task, opts = {}) {
1557
1578
  // cobrança Smart Credits (best-effort — o teto da sk-hub já protege no gateway)
1558
1579
  const _bill = async () => {
1559
1580
  if (acc.inTok + acc.outTok <= 0) return;
1581
+ if (opts.personalSubscription) return;
1560
1582
  if (k.billingAuthoritative) return;
1561
1583
  try {
1562
1584
  const c = await api('/api/credit/charge', { method: 'POST', token, body: { model: usedModel, inTok: acc.inTok, outTok: acc.outTok, cachedTok: acc.cachedTok } });
@@ -1590,27 +1612,19 @@ async function run(task, opts = {}) {
1590
1612
  : ''),
1591
1613
  validatePlannerDecision);
1592
1614
  if (!planner) {
1593
- finalText = 'Não iniciei a execução porque o planejador não produziu um plano válido dentro do orçamento da fase. Nenhum arquivo foi alterado. Você pode tentar novamente; o sistema registrou a falha objetiva para diagnóstico.';
1594
- await _bill();
1595
- _logEp('planner_failed');
1596
- return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx,
1597
- orchestration: { plan: null, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage },
1598
- completion: { ok: false, reason: 'planner_invalid' },
1599
- report: {
1600
- models: _roleTrace.map(x => ({ role: x.role, model: x.model, ok: x.ok })),
1601
- attempts: _roleTrace.length,
1602
- fallbacks: _fallbackTrace,
1603
- proofs: [],
1604
- credits: charged + _visionCredits,
1605
- tokens: Object.assign({}, acc),
1606
- durationMs: Date.now() - missionStartedAt,
1607
- phaseBudgets: _phaseBudgets,
1608
- phaseUsage: _phaseUsage,
1609
- reason: 'planner_invalid',
1610
- },
1611
- missionCache: _missionCache.stats() };
1615
+ // O planejador é uma otimização, não um ponto único de falha. A própria
1616
+ // tarefa original e os gates determinísticos continuam válidos; portanto
1617
+ // siga com o executor e registre a degradação de forma observável.
1618
+ _planDecision = normalizePlannerDecision({
1619
+ decision:'execute', decision_kind:'technical',
1620
+ reason:'Planejador indisponível; execução continuou com os gates determinísticos.',
1621
+ assumptions:[], steps:[], acceptance:[],
1622
+ });
1623
+ onStep({ name:'fallback_planejador', detail:'planejador indisponível — seguindo com o executor', retry:true });
1624
+ messages.splice(messages.length - 1, 0, { role:'user', content:'O PLANEJADOR FICOU INDISPONÍVEL. Continue diretamente com a tarefa original. Inspecione o estado real, use as ferramentas necessárias e prove a conclusão. Não invente resultados.' });
1625
+ } else {
1626
+ _planDecision = normalizePlannerDecision(planner.json);
1612
1627
  }
1613
- _planDecision = normalizePlannerDecision(planner.json);
1614
1628
  if (_planDecision.decision === 'ask') {
1615
1629
  finalText = `Preciso de uma decisão sua antes de continuar: ${_planDecision.question}`;
1616
1630
  await _bill();
@@ -1722,19 +1736,25 @@ async function run(task, opts = {}) {
1722
1736
  // INSPETOR independente: recebe apenas objetivo, plano e EVIDÊNCIAS. Não tem
1723
1737
  // ferramentas nem pode alterar o projeto. Se confirmar uma falha, o próximo
1724
1738
  // turno muda para o papel corretor e recebe somente essas falhas.
1725
- if (!roMode && _actionExpected && actions.length && !guardStopped && opts.orchestrate !== false && _inspectorAttempts < 2) {
1739
+ const recoveredTools = new Set(actions.map(a => a.name));
1740
+ const unresolvedErrors = _toolErrs.filter(e => !recoveredTools.has(e.tool)).slice(-8);
1741
+ const inspectorDecision = independentInspectorDecision({
1742
+ personalSubscription:!!opts.personalSubscription, complexTask:_complexTask,
1743
+ actions, verifyReport, unresolvedErrors,
1744
+ });
1745
+ if (!roMode && _actionExpected && actions.length && !guardStopped && opts.orchestrate !== false
1746
+ && _inspectorAttempts < 2 && inspectorDecision.run) {
1726
1747
  _inspectorAttempts++;
1727
1748
  const proofEvidence = (verifyReport && verifyReport.results || []).map(p => ({ ok: p.ok, label: p.label || p.type, detail: p.detail })).slice(0, 12);
1728
1749
  const mutationActions = actions.filter(a => ['escrever_arquivo', 'editar_arquivo', 'editar_documento', 'editar_planilha'].includes(a.name));
1729
- const recoveredTools = new Set(actions.map(a => a.name));
1730
- const unresolvedErrors = _toolErrs.filter(e => !recoveredTools.has(e.tool)).slice(-8);
1731
1750
  const inspector = await _callRole('inspector',
1732
1751
  'Responda APENAS JSON válido: {"verdict":"pass|fail","confirmed_failures":[],"summary":""}. '
1733
- + 'Falhe somente por divergência concreta entre pedido/critério e evidência. Não invente requisito, não peça melhoria opcional e não aceite narrativa sem prova.',
1734
- + ' O ledger de ferramentas e runFacts sao evidencia deterministica do harness: fileMutationCount=0 prova zero mutacoes por ferramentas de arquivo nesta rodada. Alvos de comando sao resumos de telemetria; nao falhe apenas por corte visual se a evidencia registra exit 0.',
1752
+ + 'Falhe somente por divergência concreta entre pedido/critério e evidência. Não invente requisito, não peça melhoria opcional e não aceite narrativa sem prova. '
1753
+ + 'O ledger de ferramentas e runFacts é evidência determinística do harness: fileMutationCount=0 prova zero mutações por ferramentas de arquivo nesta rodada. Alvos de comando são resumos de telemetria; não falhe apenas por corte visual se a evidência registra exit 0.',
1735
1754
  JSON.stringify({ task: taskText, auditPack: _auditPack && _auditPack.id, plan: _planDecision, actions, proofs: proofEvidence,
1736
1755
  runFacts: { fileMutationCount: mutationActions.length, fileMutations: mutationActions },
1737
- unresolvedToolErrors: unresolvedErrors, executorSummary: candidateText.slice(0, 4000) }));
1756
+ unresolvedToolErrors: unresolvedErrors, executorSummary: candidateText.slice(0, 4000) }),
1757
+ value => !!(value && /^(?:pass|fail)$/i.test(String(value.verdict || ''))));
1738
1758
  const inspection = normalizeInspectorDecision(inspector && inspector.json);
1739
1759
  _inspectionTrace.push(Object.assign({ model: inspector && inspector.model || '' }, inspection));
1740
1760
  if (!inspection.passed && inspection.failures.length && !_guard()) {
@@ -2457,4 +2477,4 @@ async function run(task, opts = {}) {
2457
2477
  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() };
2458
2478
  }
2459
2479
 
2460
- module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
2480
+ module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
@@ -4,6 +4,11 @@ const path=require('path');
4
4
 
5
5
  const TEXT_EXT=new Set(['.html','.htm','.css','.js','.mjs','.cjs','.ts','.tsx','.jsx','.json','.md','.txt','.svg','.xml','.yml','.yaml']);
6
6
  const SKIP=new Set(['node_modules','.git','.cache','.idea','.vscode','coverage']);
7
+ // Arquivos `.ts-*` pertencem ao runtime do Terminal Smart (checkpoint, memória,
8
+ // erros, episódios etc.). Eles não são artefatos entregues pelo projeto e podem
9
+ // conter a saída bruta de processos externos em outra página de código. Incluí-los
10
+ // na prova de qualidade gera falsos negativos sem dizer nada sobre o produto.
11
+ const isInternalRuntimeEntry=name=>String(name || '').toLowerCase().startsWith('.ts-');
7
12
  const MOJIBAKE=[
8
13
  { regex:/\uFFFD/g, label:'caractere de substituição Unicode (�)' },
9
14
  { regex:/Ã[\u0080-\u00BF]/g, label:'UTF-8 interpretado como Latin-1 (Ã…)' },
@@ -27,6 +32,7 @@ function scanProject(root,{maxFiles=1500,maxBytes=2*1024*1024}={}) {
27
32
  let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true});}catch(_){return;}
28
33
  for(const entry of entries){
29
34
  if(scanned>=maxFiles) break;
35
+ if(isInternalRuntimeEntry(entry.name)) continue;
30
36
  if(entry.isDirectory()){ if(!SKIP.has(entry.name)) walk(path.join(dir,entry.name)); continue; }
31
37
  const file=path.join(dir,entry.name); const ext=path.extname(entry.name).toLowerCase();
32
38
  if(!TEXT_EXT.has(ext)) continue;
@@ -42,4 +48,4 @@ function scanProject(root,{maxFiles=1500,maxBytes=2*1024*1024}={}) {
42
48
  return {ok:defects.length===0,defects,scanned,truncated:scanned>=maxFiles};
43
49
  }
44
50
 
45
- module.exports={inspectText,scanProject};
51
+ module.exports={inspectText,scanProject,isInternalRuntimeEntry};
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ const DIR = path.join(os.homedir(), '.ts', 'agent-operations');
9
+ function keyFor(cwd) { return crypto.createHash('sha256').update(path.resolve(cwd).toLowerCase()).digest('hex').slice(0, 20); }
10
+ function fileFor(cwd) { return path.join(DIR, keyFor(cwd) + '.json'); }
11
+ function load(cwd) {
12
+ try { return JSON.parse(fs.readFileSync(fileFor(cwd), 'utf8')); } catch (_) { return null; }
13
+ }
14
+ function save(cwd, value) {
15
+ fs.mkdirSync(DIR, { recursive:true, mode:0o700 });
16
+ const target = fileFor(cwd), tmp = target + '.' + process.pid + '.tmp';
17
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2), { encoding:'utf8', mode:0o600 });
18
+ fs.renameSync(tmp, target);
19
+ return value;
20
+ }
21
+ function begin(cwd, task, meta = {}) {
22
+ const previous = load(cwd);
23
+ const now = new Date().toISOString();
24
+ return save(cwd, {
25
+ id:crypto.randomUUID(), cwd:path.resolve(cwd), task:String(task || ''), status:'running',
26
+ startedAt:now, updatedAt:now, attemptCount:Number(previous?.attemptCount || 0) + 1,
27
+ hadTools:false, lastTool:null, ...meta,
28
+ });
29
+ }
30
+ function patch(cwd, changes) {
31
+ const current = load(cwd); if (!current) return null;
32
+ return save(cwd, { ...current, ...changes, updatedAt:new Date().toISOString() });
33
+ }
34
+ function toolStarted(cwd, name) { return patch(cwd, { hadTools:true, lastTool:String(name || '') }); }
35
+ function complete(cwd, result = {}) { return patch(cwd, { status:'done', completedAt:new Date().toISOString(), result }); }
36
+ function interrupt(cwd, reason, result = null) {
37
+ return patch(cwd, {
38
+ status:'interrupted', interruptedAt:new Date().toISOString(),
39
+ reason:String(reason || 'interrupted').slice(0, 300),
40
+ ...(result == null ? {} : { result }),
41
+ });
42
+ }
43
+ function finish(cwd, outcome = {}) {
44
+ const result = {
45
+ steps:Number(outcome.steps || 0), credits:Number(outcome.credits || 0),
46
+ model:String(outcome.model || ''), completion:outcome.completion || null,
47
+ };
48
+ if (outcome.completion && outcome.completion.ok === false) {
49
+ return interrupt(cwd, outcome.completion.reason || 'mission_incomplete', result);
50
+ }
51
+ return complete(cwd, result);
52
+ }
53
+ function findInterrupted(cwd) {
54
+ const op = load(cwd);
55
+ if (!op || !['running','interrupted'].includes(op.status)) return null;
56
+ if (op.status === 'running') return interrupt(cwd, 'process_restart');
57
+ return op;
58
+ }
59
+ 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
+
61
+ module.exports = { DIR, keyFor, fileFor, load, save, begin, patch, toolStarted, complete, interrupt, finish, findInterrupted, isNaturalResume };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function clipped(value, limit) {
6
+ const text = typeof value === 'string' ? value : JSON.stringify(value || '');
7
+ return text.length <= limit ? text : text.slice(0, limit) + '\n[contexto truncado pelo CLI]';
8
+ }
9
+
10
+ function compactMessages(messages = [], { includeSystem = true } = {}) {
11
+ const rows = [];
12
+ const system = messages.find(item => item && item.role === 'system');
13
+ if (includeSystem && system) rows.push(`SYSTEM:\n${clipped(system.content, 7500)}`);
14
+ const tail = messages.filter(item => item && item !== system).slice(-8);
15
+ let remaining = 7000;
16
+ for (const item of tail) {
17
+ if (remaining <= 0) break;
18
+ const calls = Array.isArray(item.tool_calls)
19
+ ? `\nTOOL_CALLS ANTERIORES: ${clipped(item.tool_calls, 1200)}` : '';
20
+ const row = `${String(item.role || 'user').toUpperCase()}:\n${clipped(item.content, Math.min(2200, remaining))}${calls}`;
21
+ rows.push(row); remaining -= row.length;
22
+ }
23
+ return rows.join('\n\n---\n\n');
24
+ }
25
+
26
+ function compactTools(definitions = []) {
27
+ return definitions.slice(0, 80).map(item => {
28
+ const fn = item && item.function || {};
29
+ return { name:String(fn.name || ''), description:clipped(fn.description || '', 260), parameters:fn.parameters || { type:'object' } };
30
+ }).filter(item => item.name);
31
+ }
32
+
33
+ function jsonObject(text) {
34
+ const source = String(text || '').replace(/```(?:json)?/gi, '').replace(/```/g, '').trim();
35
+ const start = source.indexOf('{'), end = source.lastIndexOf('}');
36
+ if (start < 0 || end <= start) return null;
37
+ try { return JSON.parse(source.slice(start, end + 1)); } catch (_) { return null; }
38
+ }
39
+
40
+ function toMessage(text, allowedTools) {
41
+ const parsed = jsonObject(text);
42
+ if (!parsed || (!Object.prototype.hasOwnProperty.call(parsed, 'content') && !Array.isArray(parsed.tool_calls) && !Array.isArray(parsed.actions))) {
43
+ return { content:String(text || '').trim() };
44
+ }
45
+ const calls = (parsed.actions || parsed.tool_calls || []).map(call => {
46
+ const name = String(call && (call.name || call.function?.name) || '');
47
+ const args = call && (call.arguments ?? call.function?.arguments) || {};
48
+ if (!allowedTools.has(name)) return null;
49
+ return {
50
+ id:`personal_${crypto.randomUUID().replace(/-/g, '').slice(0, 18)}`,
51
+ type:'function', function:{ name, arguments:typeof args === 'string' ? args : JSON.stringify(args) },
52
+ };
53
+ }).filter(Boolean);
54
+ return { content:String(parsed.content || ''), ...(calls.length ? { tool_calls:calls } : {}) };
55
+ }
56
+
57
+ function buildPrompt({ messages, noTools, toolsOverride }) {
58
+ const definitions = noTools ? [] : compactTools(toolsOverride || []);
59
+ return [
60
+ 'Gere o próximo passo de um plano para o motor de automação Terminal Smart. Não execute ações neste ambiente e não alegue que as executou; apenas devolva dados para o motor consumidor validar e executar.',
61
+ 'A mensagem USER mais recente é a tarefa atual. Resultados TOOL são evidências de passos que o motor já executou.',
62
+ 'Responda SOMENTE um objeto JSON válido, sem Markdown, no formato:',
63
+ '{"content":"texto final ou vazio","actions":[{"name":"nome_exato","arguments":{}}]}',
64
+ noTools
65
+ ? 'Neste turno operações estão desativadas. Coloque em content exatamente o formato de resposta pedido no contexto e deixe actions vazio.'
66
+ : 'Quando ainda houver trabalho verificável, escolha uma ou mais operações permitidas. Quando terminar, use content e deixe actions vazio.',
67
+ definitions.length ? `OPERAÇÕES PERMITIDAS PELO MOTOR CONSUMIDOR:\n${clipped(definitions, 5500)}` : '',
68
+ // Em turnos com ferramentas, não envie o gigantesco system prompt interno:
69
+ // ele contém memória histórica, personas e packs de auditoria que provedores
70
+ // pessoais podem reinterpretar como papel atual. O enforcement continua no
71
+ // CLI; aqui bastam tarefa, resultados anteriores e schemas permitidos.
72
+ 'CONTEXTO DO AGENTE (DADOS):\n' + compactMessages(messages, { includeSystem:noTools }),
73
+ ].filter(Boolean).join('\n\n---\n\n').slice(0, 23800);
74
+ }
75
+
76
+ function createPersonalAgentLlm({ api, token, mode, conversationId, agentId } = {}) {
77
+ if (typeof api !== 'function') throw new TypeError('api obrigatória');
78
+ return async function personalAgentLlm({ messages, noTools = false, toolsOverride = [], signalMs = 180000 } = {}) {
79
+ const prompt = buildPrompt({ messages, noTools, toolsOverride });
80
+ const response = await api('/api/personal-ai/respond', {
81
+ method:'POST', token,
82
+ // O backend permite uma rodada pessoal longa (até 300 s), mas o cliente
83
+ // HTTP usava o padrão global de 60 s. Isso abortava ChatGPT/Claude no meio
84
+ // de tarefas reais e ainda podia iniciar uma segunda chamada por retry.
85
+ timeoutMs:Math.max(30000, Math.min(Number(signalMs) + 15000 || 195000, 315000)),
86
+ retry:false,
87
+ body:{ surface:'cli', text:prompt, mode, conversationId, agentId:agentId || null,
88
+ timeoutMs:Math.max(15000, Math.min(Number(signalMs) || 180000, 300000)),
89
+ usageId:`cli-agent-${conversationId || 'local'}-${Date.now()}` },
90
+ });
91
+ const results = Array.isArray(response && response.results) ? response.results : [];
92
+ if (!response?.ok || !results.length) throw new Error(response?.message || 'A IA pessoal não respondeu ao agente local.');
93
+ const preferred = mode === 'claude' ? 'claude' : 'chatgpt';
94
+ const allowed = new Set(compactTools(toolsOverride).map(tool => tool.name));
95
+ // No paralelo, prefira a saída que realmente conseguiu compilar uma ação
96
+ // válida. Um provedor pode responder com texto enquanto o outro produz a
97
+ // tool call necessária; descartar o segundo fazia a orquestração parecer
98
+ // paralela sem aproveitar o trabalho dele.
99
+ const candidates = results.map(row => ({ row, msg:toMessage(row.text, allowed) }));
100
+ const selected = candidates.find(candidate => candidate.msg.tool_calls?.length)
101
+ || candidates.find(candidate => candidate.row.provider === preferred)
102
+ || candidates[0];
103
+ const item = selected.row;
104
+ const usage = item.meta?.usage || {};
105
+ return {
106
+ msg:selected.msg,
107
+ usage:{ prompt_tokens:Number(usage.input_tokens || 0), completion_tokens:Number(usage.output_tokens || 0),
108
+ prompt_tokens_details:{ cached_tokens:Number(usage.cached_input_tokens || 0) } },
109
+ model:`personal:${item.provider}:${item.meta?.model || 'subscription'}`,
110
+ billing:{ charged:0, authoritative:true, source:'user_subscription' },
111
+ };
112
+ };
113
+ }
114
+
115
+ module.exports = { createPersonalAgentLlm, _test:{ clipped, compactMessages, compactTools, jsonObject, toMessage, buildPrompt } };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ function plain(value) {
4
+ return String(value || '').trim().toLowerCase()
5
+ .normalize('NFD').replace(/[\u0300-\u036f]/g, '');
6
+ }
7
+ function providerFrom(text) {
8
+ if (/\b(?:chat\s*gpt|codex)\b/.test(text)) return 'chatgpt';
9
+ if (/\bclaude\b/.test(text)) return 'claude';
10
+ return null;
11
+ }
12
+ function parsePersonalAiIntent(value) {
13
+ const text = plain(value);
14
+ if (!text) return null;
15
+ const provider = providerFrom(text);
16
+ const isQuestion = /^(?:como|tem como|posso|consigo|e possivel|seria possivel|o que acontece)\b/.test(text);
17
+ const code = text.match(/\b(?:codigo|token)\s+(?:do\s+)?claude\s*(?:e|:|=)\s*([^\s].{5,1999})$/i);
18
+ if (code) return { action:'code', provider:'claude', code:code[1].trim() };
19
+ if (provider && /\b(?:desconectar|desconecte|desvincular|desvincule|sair|saia|remover|remova)\b/.test(text)) return { action:'disconnect', provider };
20
+ if (!isQuestion && provider
21
+ && /\b(?:conectar|conecte|entrar|entre|logar|logue|vincular|vincule|adicionar|adicione)\b/.test(text)
22
+ && /\b(?:conta|plano|assinatura|terminal smart|ts|claude|chat\s*gpt|codex)\b/.test(text)) return { action:'connect', provider };
23
+ if (/\b(?:usar|use|ativar|ative|mudar|mude|trocar|troque|selecionar|selecione)\b/.test(text)) {
24
+ if (/\bparalel[oa]\b/.test(text)) return { action:'use', mode:'paralelo' };
25
+ if (/\bautomatic[oa]\b/.test(text)) return { action:'use', mode:'automatico' };
26
+ if (provider) return { action:'use', mode:provider };
27
+ }
28
+ if (/\b(?:status|situacao|conectad[oa]s?|qual ia|modo de ia)\b/.test(text)
29
+ && /\b(?:ia|chat\s*gpt|codex|claude|modelo|conta)\b/.test(text)) return { action:'status' };
30
+ return null;
31
+ }
32
+ module.exports = { parsePersonalAiIntent };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.49",
3
+ "version": "0.97.54",
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/conversation-scope.test.js && node test/channel-agent-continuity.test.js && node test/windows-shell-normalization.test.js && node test/gateways.test.js && node test/agent-recovery-guard.test.js && node test/agent-plan-model-contract.test.js && node test/agent-mission-limits.test.js && node test/agent-external-approval.test.js && node test/agent-prompt-injection.test.js && node test/file-concurrency.test.js && node test/mission-observability.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/owner-audit.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"
9
+ "test": "node test/durable-operation.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/planner-fallback.test.js"
10
10
  },
11
11
  "files": [
12
12
  "bin",