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 +55 -11
- package/lib/agent.js +47 -27
- package/lib/content-quality.js +7 -1
- package/lib/durable-operation.js +61 -0
- package/lib/personal-agent-llm.js +115 -0
- package/lib/personal-ai-intent.js +32 -0
- package/package.json +2 -2
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
|
-
|
|
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
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
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
|
-
+ '
|
|
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 } };
|
package/lib/content-quality.js
CHANGED
|
@@ -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.
|
|
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/
|
|
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",
|