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 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 r = await api('/api/conversations', { method: 'POST', token, body: { title: conversationScope.conversationTitle(cwd, 'CLI'), scope: 'cloud', workstreamId } });
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 ensureConv(token, 'agent', startCwd);
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
- const personalReady = !cfg.forceTsCloud && personalState?.ok && personalState.mode !== 'automatico'
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
- const src = String(text || '').replace(/```(?:json)?/gi, '').trim();
245
- const a = src.indexOf('{'), b = src.lastIndexOf('}');
246
- if (a < 0 || b <= a) return null;
247
- try { return JSON.parse(src.slice(a, b + 1)); } catch (_) { return null; }
244
+ return require('./structured-json').parse(text);
245
+ }
246
+
247
+ function powershellFileChainIssue(command) {
248
+ if (process.platform !== 'win32') return false;
249
+ const s = String(command || '');
250
+ return /^\s*(?:powershell|pwsh)(?:\.exe)?\b[\s\S]*?\s-File\s+(?:"[^"]+"|'[^']+'|\S+)[\s\S]*?(?:;|&&|\|\|)/i.test(s);
251
+ }
252
+
253
+ function reconcileBillingClaims(text, { credits = 0, steps = 0, personalSubscription = false } = {}) {
254
+ let out = String(text || '').trim();
255
+ const deniesCharge = /(?:nenhum|n[aã]o houve|sem)\s+(?:cr[eé]dito\w*\s+)?(?:foi\s+|foram\s+)?(?:cobrad|consumid|gasto)|0\s+cr[eé]ditos?\s+(?:foram\s+)?(?:cobrad|consumid)/i.test(out);
256
+ const deniesExecution = /nenhuma\s+(?:a[cç][aã]o|execu[cç][aã]o)\s+(?:foi\s+)?(?:iniciad|executad|realizad)/i.test(out);
257
+ const notes = [];
258
+ if (Number(credits) > 0 && deniesCharge) notes.push(`Registro real de cobrança: ${Number(credits)} crédito(s) TS consumido(s).`);
259
+ if (Number(steps) > 0 && deniesExecution) notes.push(`Registro real de execução: ${Number(steps)} etapa(s) executada(s).`);
260
+ if (personalSubscription && Number(credits) === 0 && !/0\s+cr[eé]ditos?\s+TS/i.test(out)) notes.push('Cobrança TS nesta execução: 0 créditos (assinatura pessoal).');
261
+ return notes.length ? `${out}\n\n${notes.join(' ')}`.trim() : out;
248
262
  }
249
263
 
250
264
  const MATERIAL_DECISIONS = new Set(['recipient', 'destination', 'irreversible', 'credential', 'external_authorization', 'payment', 'business_choice']);
@@ -1907,7 +1921,10 @@ async function run(task, opts = {}) {
1907
1921
  // Só o comando LOCAL (executar_comando roda NESTE processo/cwd); o remoto tem outro pid/cwd
1908
1922
  // e segue pelo gate de aprovação comum.
1909
1923
  if (result === undefined && name === 'executar_comando') {
1910
- if (windowsUnsupportedUnixCommand(input.comando)) {
1924
+ if (powershellFileChainIssue(input.comando)) {
1925
+ onStep({ name, detail: argsShort(name, input), blocked: true });
1926
+ result = { erro: 'COMANDO BLOQUEADO ANTES DA EXECUÇÃO: no PowerShell, -File encerra a linha de comando e não pode receber uma cadeia com ;, && ou ||. Execute o script em uma chamada e o próximo comando em outra.', _recovery: 'Separe a cadeia: primeiro `powershell -NoProfile -File "script.ps1"`; depois faça uma nova chamada executar_comando para o comando seguinte.' };
1927
+ } else if (windowsUnsupportedUnixCommand(input.comando)) {
1911
1928
  const hint = commandRecoveryHint(input.comando, 'preflight');
1912
1929
  onStep({ name, detail: argsShort(name, input), blocked: true });
1913
1930
  result = { erro: 'COMANDO BLOQUEADO ANTES DA EXECUÇÃO: incompatível com cmd.exe no Windows. Não execute nem repita este comando.', _recovery: hint };
@@ -2441,6 +2458,11 @@ async function run(task, opts = {}) {
2441
2458
  ? `Não declarei a tarefa como concluída porque nenhuma ação verificável foi executada.\n\n${finalText}`
2442
2459
  : `I did not mark the task complete because no verifiable action was executed.\n\n${finalText}`;
2443
2460
 
2461
+ finalText = reconcileBillingClaims(finalText, {
2462
+ credits: charged + _visionCredits, steps,
2463
+ personalSubscription: !!opts.keyOverride && opts.keyOverride.source === 'personal',
2464
+ });
2465
+
2444
2466
  if (_hooks._any) { try { _hooksMod.run(_hooks, 'Stop', { cwd, text: finalText, steps }); } catch (_) {} }
2445
2467
  _logEp(stopped ? 'cancel' : (completion.ok ? 'done' : 'stuck'));
2446
2468
  await _bill();
@@ -2534,4 +2556,4 @@ async function run(task, opts = {}) {
2534
2556
  return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
2535
2557
  }
2536
2558
 
2537
- module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
2559
+ module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, powershellFileChainIssue, reconcileBillingClaims, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
@@ -56,6 +56,11 @@ function findInterrupted(cwd) {
56
56
  if (op.status === 'running') return interrupt(cwd, 'process_restart');
57
57
  return op;
58
58
  }
59
+ function reset(cwd) {
60
+ const target = fileFor(cwd);
61
+ try { fs.unlinkSync(target); return true; }
62
+ catch (e) { if (e && e.code === 'ENOENT') return false; throw e; }
63
+ }
59
64
  function isNaturalResume(text) { return /\b(?:continu(?:a|ar|e|emos)|retom(?:a|ar|e|emos)|seguir|prossiga|onde\s+parou)\b/i.test(String(text || '')); }
60
65
 
61
- module.exports = { DIR, keyFor, fileFor, load, save, begin, patch, toolStarted, complete, interrupt, finish, findInterrupted, isNaturalResume };
66
+ module.exports = { DIR, keyFor, fileFor, load, save, begin, patch, toolStarted, complete, interrupt, finish, findInterrupted, reset, isNaturalResume };
package/lib/meta.js CHANGED
@@ -186,6 +186,9 @@ function normalizeMetaArtifact(relativePath, raw) {
186
186
  const start = a >= 0 && (o < 0 || a < o) ? a : o;
187
187
  const end = Math.max(text.lastIndexOf(']'), text.lastIndexOf('}'));
188
188
  if (start < 0 || end < start) throw new Error('resposta sem JSON');
189
+ // Artefato FINAL permanece estrito. O parser tolerante é reservado ao plano,
190
+ // que ainda passa por validação; aceitar conteúdo truncado aqui produziria
191
+ // um arquivo válido porém semanticamente incompleto.
189
192
  const parsed = JSON.parse(text.slice(start, end + 1));
190
193
  text = JSON.stringify(parsed, null, 2) + '\n';
191
194
  }
@@ -1222,6 +1225,8 @@ async function designPhase(goal, designer, token, opts = {}) {
1222
1225
  // Parser tolerante: modelos pequenos às vezes devolvem JSON com fence, vírgula
1223
1226
  // sobrando, ou TRUNCADO no meio (max_tokens acabou). Recupera o máximo possível.
1224
1227
  function _tolerantJson(text) {
1228
+ const structured = require('./structured-json').parse(text);
1229
+ if (structured != null) return structured;
1225
1230
  let s = String(text).replace(/```(?:json)?/gi, '').trim();
1226
1231
  const open = s.indexOf('{');
1227
1232
  if (open >= 0) s = s.slice(open);
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ // Extrai o primeiro JSON completo respeitando strings/escapes. Se a resposta foi
4
+ // truncada, fecha apenas containers ainda abertos; validação semântica continua
5
+ // sendo responsabilidade do chamador (ex.: quantidade mínima de itens).
6
+ function candidate(text) {
7
+ const src = String(text || '').replace(/```(?:json)?/gi, '').trim();
8
+ const start = [...src].findIndex(ch => ch === '{' || ch === '[');
9
+ if (start < 0) return '';
10
+ const stack = [];
11
+ let quoted = false, escaped = false, lastSafe = start, lastComplete = start;
12
+ for (let i = start; i < src.length; i++) {
13
+ const ch = src[i];
14
+ if (quoted) {
15
+ if (escaped) escaped = false;
16
+ else if (ch === '\\') escaped = true;
17
+ else if (ch === '"') quoted = false;
18
+ continue;
19
+ }
20
+ if (ch === '"') { quoted = true; continue; }
21
+ if (ch === '{') stack.push('}');
22
+ else if (ch === '[') stack.push(']');
23
+ else if (ch === '}' || ch === ']') {
24
+ if (!stack.length || stack[stack.length - 1] !== ch) return '';
25
+ stack.pop();
26
+ lastComplete = i + 1;
27
+ if (!stack.length) return src.slice(start, i + 1);
28
+ }
29
+ if (!quoted && stack.length && /[,}\]]/.test(ch)) lastSafe = i + 1;
30
+ }
31
+ // Não tente adivinhar o restante de uma string cortada. Volte ao último
32
+ // delimitador estrutural seguro e feche os containers ainda existentes.
33
+ let out = src.slice(start, quoted ? lastComplete : src.length).replace(/,\s*$/, '');
34
+ const open = [];
35
+ quoted = false; escaped = false;
36
+ for (const ch of out) {
37
+ if (quoted) {
38
+ if (escaped) escaped = false;
39
+ else if (ch === '\\') escaped = true;
40
+ else if (ch === '"') quoted = false;
41
+ } else if (ch === '"') quoted = true;
42
+ else if (ch === '{') open.push('}');
43
+ else if (ch === '[') open.push(']');
44
+ else if ((ch === '}' || ch === ']') && open[open.length - 1] === ch) open.pop();
45
+ }
46
+ return out + open.reverse().join('');
47
+ }
48
+
49
+ function parse(text) {
50
+ const raw = candidate(text);
51
+ if (!raw) return null;
52
+ const attempts = [raw, raw.replace(/,\s*([}\]])/g, '$1')];
53
+ for (const value of attempts) { try { return JSON.parse(value); } catch (_) {} }
54
+ return null;
55
+ }
56
+
57
+ module.exports = { candidate, parse };
package/lib/tools.js CHANGED
@@ -452,6 +452,9 @@ function _normalizeWindowsReadOnlyCommand(cmd, platform = process.platform) {
452
452
  function _shellGotcha(cmd) {
453
453
  const s = String(cmd || '');
454
454
  const multi = /[\r\n]/.test(s);
455
+ if (process.platform === 'win32'
456
+ && /^\s*(?:powershell|pwsh)(?:\.exe)?\b[\s\S]*?\s-File\s+(?:"[^"]+"|'[^']+'|\S+)[\s\S]*?(?:;|&&|\|\|)/i.test(s))
457
+ return 'PowerShell -File não aceita uma cadeia posterior na mesma chamada. Execute o .ps1 sozinho e faça o próximo comando em outra chamada.';
455
458
  // executar_comando usa cmd.exe. Modelos alternam facilmente entre os dois
456
459
  // dialetos e `Select-String`/`Get-Content` soltos viram "não reconhecido",
457
460
  // desperdiçando uma rodada antes de o recovery engine conseguir explicar.
@@ -805,6 +808,7 @@ async function execute(name, input, opts = {}) {
805
808
  if (!Object.prototype.hasOwnProperty.call(input, 'conteudo')) {
806
809
  return { erro: 'Informe o conteúdo do arquivo em conteudo. A escrita foi recusada para não apagar o arquivo com conteúdo ausente.' };
807
810
  }
811
+ if (String(input.conteudo ?? '').includes('\uFFFD')) return { erro: 'CODIFICAÇÃO BLOQUEADA: o conteúdo contém o caractere de substituição �. Releia a fonte em UTF-8 e corrija o texto antes de gravar.' };
808
812
  let p = _abs(input.caminho, baseDir);
809
813
  { const g = _guardTsHome(p); if (g) return { erro: g }; }
810
814
  // Confinamento (missões): modelos fracos inventam pastas absolutas
@@ -863,6 +867,7 @@ async function execute(name, input, opts = {}) {
863
867
  }
864
868
  const orig = fs.readFileSync(p, 'utf8');
865
869
  let buscar = String(input.buscar ?? ''), substituir = String(input.substituir ?? '');
870
+ if (substituir.includes('\uFFFD') && !orig.includes('\uFFFD')) return { erro: 'CODIFICAÇÃO BLOQUEADA: a edição introduziria o caractere de substituição �. Releia a fonte em UTF-8 e envie o texto correto.' };
866
871
  if (!buscar) return { erro: 'buscar vazio.' };
867
872
  // tolerância CRLF: o modelo manda \n mas arquivo Windows tem \r\n — normaliza o par
868
873
  if (!orig.includes(buscar) && orig.includes('\r\n')) {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.63",
3
+ "version": "0.97.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",