terminal-smart-cli 0.97.63 → 0.97.65
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 +44 -7
- package/lib/agent.js +28 -6
- package/lib/durable-operation.js +6 -1
- package/lib/meta.js +5 -0
- package/lib/structured-json.js +57 -0
- package/lib/tools.js +5 -0
- package/package.json +2 -2
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)) {
|
|
@@ -299,7 +299,11 @@ async function ensureConv(token, kind = 'chat', cwd = process.cwd(), forceNew =
|
|
|
299
299
|
try { await api(`/api/conversations/${scopes[scope]}`, { method: 'PUT', token, body: { workstreamId, onlyIfEmpty: true } }); } catch (_) {}
|
|
300
300
|
return scopes[scope];
|
|
301
301
|
}
|
|
302
|
-
const
|
|
302
|
+
const contextOnly = kind === 'agent';
|
|
303
|
+
const r = await api('/api/conversations', { method: 'POST', token, body: {
|
|
304
|
+
title: conversationScope.conversationTitle(cwd, 'CLI'), scope: 'cloud', workstreamId,
|
|
305
|
+
sourceSurface:'cli', historyVisible:false, purpose:contextOnly ? 'agent-context' : 'chat',
|
|
306
|
+
} });
|
|
303
307
|
scopes[scope] = r.id;
|
|
304
308
|
// convId permanece como ponte de compatibilidade para instalações antigas;
|
|
305
309
|
// toda nova leitura usa convScopes, nunca o id global legado.
|
|
@@ -1031,6 +1035,7 @@ async function chatRepl() {
|
|
|
1031
1035
|
if (['sair', 'exit', 'quit', '/sair', '/exit'].includes(low)) break;
|
|
1032
1036
|
try {
|
|
1033
1037
|
if (['/nova', '/new'].includes(low)) {
|
|
1038
|
+
_resetLocalAgentState(agentCwd);
|
|
1034
1039
|
await ensureConv(token, 'chat', process.cwd(), true);
|
|
1035
1040
|
console.log(ui.okLine(T.new_conv) + '\n');
|
|
1036
1041
|
} else if (['/ajuda', '/help', '/?'].includes(low)) {
|
|
@@ -1509,17 +1514,33 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1509
1514
|
let sharedConversationId = null;
|
|
1510
1515
|
let personalAgentLlm = null;
|
|
1511
1516
|
let personalAgentMode = null;
|
|
1517
|
+
try { sharedConversationId = await ensureConv(token, 'agent', startCwd); } catch (e) {
|
|
1518
|
+
if (!streamJson) _hlog(' ' + C.warn('▲ contexto compartilhado indisponível; a execução local continua sem memória remota.'));
|
|
1519
|
+
}
|
|
1520
|
+
let shared = null;
|
|
1512
1521
|
try {
|
|
1513
|
-
sharedConversationId = await
|
|
1514
|
-
const shared = await api('/api/channel/resolve', { method:'POST', token, body:{
|
|
1522
|
+
if (sharedConversationId) shared = await api('/api/channel/resolve', { method:'POST', token, body:{
|
|
1515
1523
|
surface:'cli', text:task, conversationId:sharedConversationId,
|
|
1516
1524
|
} });
|
|
1517
1525
|
if (shared?.ok && shared.promptBlock) sharedChannelPrompt = shared.promptBlock;
|
|
1526
|
+
} catch (_) {
|
|
1527
|
+
if (!streamJson) _hlog(' ' + C.warn('▲ não foi possível carregar o agente/projeto compartilhado; seguindo no diretório atual.'));
|
|
1528
|
+
}
|
|
1529
|
+
let personalSelectionRequired = false;
|
|
1530
|
+
try {
|
|
1518
1531
|
const personalState = await api('/api/personal-ai/status', { token });
|
|
1519
|
-
|
|
1532
|
+
// O modo é da CONTA e precisa ser idêntico no Telegram/Web/App/CLI. Uma
|
|
1533
|
+
// preferência local antiga não pode vencer silenciosamente o estado remoto
|
|
1534
|
+
// e consumir créditos TS quando a conta está em ChatGPT/Claude.
|
|
1535
|
+
const selectedPersonal = personalState?.ok && personalState.mode !== 'automatico';
|
|
1536
|
+
personalSelectionRequired = selectedPersonal;
|
|
1537
|
+
const personalReady = selectedPersonal
|
|
1520
1538
|
&& (personalState.mode === 'chatgpt' ? personalState.chatgpt
|
|
1521
1539
|
: personalState.mode === 'claude' ? personalState.claude
|
|
1522
1540
|
: personalState.chatgpt && personalState.claude);
|
|
1541
|
+
if (selectedPersonal && !personalReady) {
|
|
1542
|
+
throw new Error(`modo ${personalState.mode} selecionado, mas a conta necessária não está conectada`);
|
|
1543
|
+
}
|
|
1523
1544
|
if (personalReady) {
|
|
1524
1545
|
personalAgentMode = personalState.mode;
|
|
1525
1546
|
personalAgentLlm = require('../lib/personal-agent-llm').createPersonalAgentLlm({
|
|
@@ -1528,7 +1549,14 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
|
|
|
1528
1549
|
});
|
|
1529
1550
|
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
1551
|
}
|
|
1531
|
-
} catch (
|
|
1552
|
+
} catch (e) {
|
|
1553
|
+
if (personalSelectionRequired || (cfg.personalModePreference && cfg.personalModePreference !== 'automatico')) {
|
|
1554
|
+
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.`));
|
|
1555
|
+
durableOps.interrupt(startCwd, 'personal_ai_unavailable');
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
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).'));
|
|
1559
|
+
}
|
|
1532
1560
|
let out;
|
|
1533
1561
|
const durableCwd = _wt ? _wt.base : startCwd;
|
|
1534
1562
|
durableOps.begin(durableCwd, task, { engine:'cli', naturalResume:!!(interruptedOp && naturalResume) });
|
|
@@ -2877,7 +2905,7 @@ async function conectarCmd(words) {
|
|
|
2877
2905
|
// ts conectar nuvem — volta a usar os créditos do TS
|
|
2878
2906
|
if (sub === 'nuvem' || sub === 'cloud' || sub === 'ts') {
|
|
2879
2907
|
keyring.usar(null);
|
|
2880
|
-
cfg = config.save({ forceTsCloud: true });
|
|
2908
|
+
cfg = config.save({ forceTsCloud: true, personalModePreference:'automatico' });
|
|
2881
2909
|
console.log(' ' + C.ok('✔ ') + (en ? 'back to TS Cloud (your credits).' : 'de volta ao TS Cloud (seus créditos).'));
|
|
2882
2910
|
return;
|
|
2883
2911
|
}
|
|
@@ -3374,8 +3402,17 @@ function idioma(l) {
|
|
|
3374
3402
|
console.log(ui.okLine(T.lang_set(lang)));
|
|
3375
3403
|
}
|
|
3376
3404
|
|
|
3405
|
+
function _resetLocalAgentState(cwd) {
|
|
3406
|
+
const _p = require('path'), _fs = require('fs'), _os = require('os'), _cr = require('crypto');
|
|
3407
|
+
const root = _p.resolve(cwd || process.cwd());
|
|
3408
|
+
const localSession = _p.join(_os.homedir(), '.ts', 'agente', _cr.createHash('md5').update(root.toLowerCase()).digest('hex').slice(0, 12) + '.json');
|
|
3409
|
+
try { _fs.unlinkSync(localSession); } catch (e) { if (!e || e.code !== 'ENOENT') throw e; }
|
|
3410
|
+
require('../lib/durable-operation').reset(root);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3377
3413
|
async function nova() {
|
|
3378
3414
|
const token = needToken();
|
|
3415
|
+
_resetLocalAgentState(process.cwd());
|
|
3379
3416
|
await ensureConv(token, 'chat', process.cwd(), true);
|
|
3380
3417
|
console.log(ui.okLine(T.new_conv));
|
|
3381
3418
|
}
|
package/lib/agent.js
CHANGED
|
@@ -241,10 +241,24 @@ function isInspectionCommand(command) {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
function parseStageJson(text) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 (
|
|
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 } };
|
package/lib/durable-operation.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.97.65",
|
|
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",
|