log-llm-config 1.5.11 → 1.5.15

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.
@@ -119,20 +119,20 @@ function autofixDialogLine(v) {
119
119
  const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
120
120
  return `• [${v.finding_formatted_id}] ${short}`;
121
121
  }
122
- /** Policy name(s) for preventive enforcement dialog copy. */
123
- function uniquePolicyLabelsFromViolations(appliedViolations) {
124
- const policyNames = [
122
+ /** Ruleset name(s) for preventive enforcement dialog copy. */
123
+ function uniqueRulesetLabelsFromViolations(appliedViolations) {
124
+ const rulesetNames = [
125
125
  ...new Set(appliedViolations
126
- .map((v) => v.policy_name?.trim() || v.finding_title?.trim() || '')
126
+ .map((v) => v.ruleset_name?.trim() || v.finding_title?.trim() || '')
127
127
  .filter(Boolean)),
128
128
  ];
129
- return policyNames.length > 0 ? policyNames.join('\n\n') : 'Agent security policy';
129
+ return rulesetNames.length > 0 ? rulesetNames.join('\n\n') : 'Agent security policy';
130
130
  }
131
131
  function formatPreventiveAutofixDialog(appliedViolations, applyLine) {
132
132
  const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
133
133
  const changePreviewBlock = changePreview ? `${changePreview}\n\n` : '';
134
134
  return ('Optimus Labs enforced a preventive agent security policy as determined by your security team:\n\n' +
135
- `${uniquePolicyLabelsFromViolations(appliedViolations)}\n\n` +
135
+ `${uniqueRulesetLabelsFromViolations(appliedViolations)}\n\n` +
136
136
  changePreviewBlock +
137
137
  `${applyLine}\n\n` +
138
138
  'Click OK to continue');
@@ -339,7 +339,7 @@ export async function runCompliancePromptGate() {
339
339
  ? formatCopilotAutofixDialog(appliedViolations)
340
340
  : ide === 'opencode'
341
341
  ? formatOpenCodeAutofixDialog(appliedViolations)
342
- : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
342
+ : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'ruleset violation' : 'ruleset violations'}:\n\n${appliedViolations
343
343
  .map((v) => autofixDialogLine(v))
344
344
  .join('\n')}${changePreviewSuffix}`;
345
345
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
@@ -349,7 +349,7 @@ export async function runCompliancePromptGate() {
349
349
  // Restarts: always executed by optimus-compliance-check.sh via execute_trusted_restarts (TS allowlist + spawn).
350
350
  return;
351
351
  }
352
- const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
352
+ const msg = recheck.violations[0]?.message ?? 'A security ruleset violation has been detected.';
353
353
  logRemediationApplyFailure('prompt_gate_block_recheck_failed', {
354
354
  reason: 'after autofix, local compliance recheck still reports violations',
355
355
  ide,
@@ -372,7 +372,7 @@ export async function runCompliancePromptGate() {
372
372
  console.log(blockPayload(ide, msg));
373
373
  return;
374
374
  }
375
- const msg = status.violations[0]?.message ?? 'A security policy violation has been detected.';
375
+ const msg = status.violations[0]?.message ?? 'A security ruleset violation has been detected.';
376
376
  logRemediationApplyFailure('prompt_gate_block_violations_no_autofix_applied', {
377
377
  reason: 'violations on submit but autofix did not succeed — see autofix_skipped_not_allowed, enforceRemediation, or compliance_check entries',
378
378
  ide,
@@ -25,18 +25,34 @@ function emptyTotals() {
25
25
  }
26
26
  function addUsage(totals, usage) {
27
27
  for (const key of Object.keys(totals)) {
28
- const value = usage[key];
29
- if (typeof value === 'number' && Number.isFinite(value))
28
+ const value = readTokenField(usage, key);
29
+ if (value > 0)
30
30
  totals[key] += value;
31
31
  }
32
32
  }
33
- /** Sum the token fields of one usage block (used for per-model attribution). */
33
+ /** Read one token field, preferring the nested 5m+1h cache-creation sum over the flat field. */
34
+ function readTokenField(usage, key) {
35
+ if (key === 'cache_creation_input_tokens') {
36
+ const nested = usage.cache_creation;
37
+ if (nested && typeof nested === 'object') {
38
+ const n = nested;
39
+ const has5m = typeof n.ephemeral_5m_input_tokens === 'number' && Number.isFinite(n.ephemeral_5m_input_tokens);
40
+ const has1h = typeof n.ephemeral_1h_input_tokens === 'number' && Number.isFinite(n.ephemeral_1h_input_tokens);
41
+ // Fall back to the flat field only when the nested block is truly absent — a
42
+ // legitimately-zero nested sum (both fields present as 0) must not be overridden by it.
43
+ if (has5m || has1h) {
44
+ return (has5m ? n.ephemeral_5m_input_tokens : 0) + (has1h ? n.ephemeral_1h_input_tokens : 0);
45
+ }
46
+ }
47
+ }
48
+ const value = usage[key];
49
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
50
+ }
51
+ /** Sum the token fields of one usage block (used for per-model attribution and dedup tiebreaks). */
34
52
  function usageSum(usage) {
35
53
  let sum = 0;
36
54
  for (const key of ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']) {
37
- const value = usage[key];
38
- if (typeof value === 'number' && Number.isFinite(value))
39
- sum += value;
55
+ sum += readTokenField(usage, key);
40
56
  }
41
57
  return sum;
42
58
  }
@@ -86,6 +102,14 @@ function collectClaudeTokenUsage(home = homedir()) {
86
102
  let version = '';
87
103
  let versionAt = '';
88
104
  let sawData = false;
105
+ // Claude Code writes one JSONL line per streaming snapshot of the same assistant message,
106
+ // so message.id/requestId repeat with a growing usage total; only the final snapshot is
107
+ // the true total. Dedup by (message.id, requestId), taking the per-field max across all
108
+ // snapshots for that key (order-independent — robust to out-of-order lines, and safe even
109
+ // if a snapshot has a genuinely-lower or reset value in one field while another snapshot in
110
+ // the same group peaks a different field). Entries without a message.id can't be deduped and
111
+ // are accumulated directly.
112
+ const dedupedUsage = new Map();
89
113
  for (const file of files) {
90
114
  let content;
91
115
  try {
@@ -125,16 +149,39 @@ function collectClaudeTokenUsage(home = homedir()) {
125
149
  models.add(model);
126
150
  if (msg.usage && typeof msg.usage === 'object') {
127
151
  const usage = msg.usage;
128
- addUsage(totals, usage);
129
- if (model) {
152
+ const messageId = typeof msg.id === 'string' ? msg.id : '';
153
+ if (messageId) {
154
+ const requestId = typeof entry.requestId === 'string' ? entry.requestId : '';
155
+ const dedupeKey = `${messageId} ${requestId}`;
156
+ const existing = dedupedUsage.get(dedupeKey) ?? { totals: emptyTotals(), model: '' };
157
+ for (const key of Object.keys(existing.totals)) {
158
+ const value = readTokenField(usage, key);
159
+ if (value > existing.totals[key])
160
+ existing.totals[key] = value;
161
+ }
162
+ if (model)
163
+ existing.model = model;
164
+ dedupedUsage.set(dedupeKey, existing);
165
+ }
166
+ else {
167
+ addUsage(totals, usage);
130
168
  const sum = usageSum(usage);
131
- if (sum > 0)
169
+ if (model && sum > 0)
132
170
  modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
133
171
  }
134
172
  }
135
173
  }
136
174
  }
137
175
  }
176
+ for (const { totals: merged, model } of dedupedUsage.values()) {
177
+ let sum = 0;
178
+ for (const key of Object.keys(totals)) {
179
+ totals[key] += merged[key];
180
+ sum += merged[key];
181
+ }
182
+ if (model && sum > 0)
183
+ modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
184
+ }
138
185
  if (!sawData)
139
186
  return [];
140
187
  return [
@@ -236,6 +236,7 @@ export { collectHermesTokenUsage } from './hermes_token_usage_collector.js';
236
236
  export { collectOpenclawTokenUsage } from './openclaw_token_usage_collector.js';
237
237
  export { collectPiTokenUsage } from './pi_token_usage_collector.js';
238
238
  export { collectCoworkDesktopVersion } from './cowork_desktop_version_collector.js';
239
+ export { collectGrokCliVersion } from './grok_cli_version_collector.js';
239
240
  export { collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, } from './claude_desktop_extensions_collector.js';
240
241
  export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
241
242
  export { determineFileTypeFromPath } from './file_type_rules.js';
@@ -188,6 +188,9 @@ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constant
188
188
  cursor_source: 'mcps',
189
189
  server_identifier: serverDir.name,
190
190
  };
191
+ if (metaObj) {
192
+ entry.cursor_server_metadata = metaObj;
193
+ }
191
194
  const cacheTools = cursorMcpCacheToolsForServer(mcpToolCache, label, serverDir.name);
192
195
  if (cacheTools !== undefined) {
193
196
  entry.cursor_mcp_cache_tools = cacheTools;
@@ -0,0 +1,53 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ /**
6
+ * Grok CLI version, read directly from the installed binary.
7
+ *
8
+ * Grok ships as a standalone native binary (not an npm/pip package, no manifest to read a
9
+ * version out of), installed at ~/.grok/bin/grok and typically symlinked onto PATH. The only
10
+ * way to get its version is to run it: `grok --version` prints e.g. "grok 0.2.114 (0c785038798)".
11
+ */
12
+ const FILE_TYPE = 'grok_cli_version';
13
+ const VERSION_RE = /^grok\s+(\d+\.\d+\.\d+)(?:\s+\(([0-9a-f]+)\))?/i;
14
+ function grokBinPath(home) {
15
+ return join(home, '.grok', 'bin', 'grok');
16
+ }
17
+ /** `<binPath> --version`, parsed into {version, build, raw}, or null if unavailable/unparsable. */
18
+ function readGrokVersion(binPath) {
19
+ let stdout;
20
+ try {
21
+ stdout = execFileSync(binPath, ['--version'], {
22
+ encoding: 'utf8',
23
+ stdio: ['ignore', 'pipe', 'ignore'],
24
+ timeout: 5000,
25
+ }).trim();
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ const match = VERSION_RE.exec(stdout);
31
+ if (!match)
32
+ return null;
33
+ return { version: match[1], build: match[2] ?? '', raw: stdout };
34
+ }
35
+ /**
36
+ * Emit the Grok CLI version as a single-element aggregate, or [] when Grok isn't installed
37
+ * or its version can't be determined.
38
+ */
39
+ function collectGrokCliVersion(home = homedir(), binPath = grokBinPath(home)) {
40
+ if (!existsSync(binPath))
41
+ return [];
42
+ const parsed = readGrokVersion(binPath);
43
+ if (!parsed)
44
+ return [];
45
+ return [
46
+ {
47
+ file_type: FILE_TYPE,
48
+ file_path: binPath,
49
+ raw_content: parsed,
50
+ },
51
+ ];
52
+ }
53
+ export { collectGrokCliVersion, FILE_TYPE as GROK_CLI_VERSION_FILE_TYPE };
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { parse as parseToml, stringify as stringifyTomlValue } from 'smol-toml';
2
3
  function stripJsoncComments(input) {
3
4
  let out = '';
4
5
  let inString = false;
@@ -176,4 +177,20 @@ function readInstalledExtensions(extensionsCachePath) {
176
177
  }
177
178
  return extensions;
178
179
  }
179
- export { readMCPConfig, readJSONFile, readMarkdownFile, readInstalledExtensions, parseJsonWithJsoncFallback, };
180
+ /** Parse TOML text into a nested object. Returns null on parse failure. */
181
+ function parseTomlText(raw) {
182
+ try {
183
+ const parsed = parseToml(raw);
184
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
185
+ ? parsed
186
+ : null;
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ /** Serialize a nested object to TOML (used when writing remediated .toml configs). */
193
+ function stringifyToml(value) {
194
+ return stringifyTomlValue(value);
195
+ }
196
+ export { readMCPConfig, readJSONFile, readMarkdownFile, readInstalledExtensions, parseJsonWithJsoncFallback, parseTomlText, stringifyToml, };
@@ -13,7 +13,7 @@
13
13
  import { existsSync, readFileSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { join } from 'node:path';
16
- import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
16
+ import { parseJsonWithJsoncFallback, parseTomlText } from '../readers/file_readers.js';
17
17
  import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
18
18
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
19
19
  import { resolveRemediationConfigPath, resolveRemediationUploadFileType } from './remediation_config_path.js';
@@ -212,7 +212,7 @@ function violationFromSecretScanFinding(entry, compliance, finding) {
212
212
  description: compliance.description,
213
213
  finding_title: entry.finding_title,
214
214
  finding_description: entry.finding_description,
215
- policy_name: entry.policy_name,
215
+ ruleset_name: entry.ruleset_name,
216
216
  severity: compliance.severity,
217
217
  autofix_allowed: compliance.autofix_allowed,
218
218
  config_file_path: entry.config_file_path,
@@ -280,7 +280,7 @@ function shouldMergeComposerShadowKeys(itemKeyFromPath, checkSettingPaths) {
280
280
  }
281
281
  /** @deprecated Import from vscdb_reader — re-export for existing tests. */
282
282
  export { mergeComposerShadowKeysFromReactiveBlob } from '../readers/vscdb_reader.js';
283
- /** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
283
+ /** Plain JSON/TOML file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
284
284
  function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
285
285
  const resolvedPath = resolveRemediationConfigPath(configFilePath);
286
286
  const hashIdx = resolvedPath.indexOf('#');
@@ -306,13 +306,23 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
306
306
  }
307
307
  if (!existsSync(resolvedPath))
308
308
  return { ok: false, reason: 'file_not_found' };
309
+ const rawText = readFileSync(resolvedPath, 'utf8');
310
+ if (resolvedPath.toLowerCase().endsWith('.toml')) {
311
+ const parsedToml = parseTomlText(rawText);
312
+ if (parsedToml === null)
313
+ return { ok: false, reason: 'parse_error' };
314
+ return { ok: true, json: parsedToml };
315
+ }
309
316
  // OpenCode configs are JSONC (comments / trailing commas); parse strict JSON first, then
310
317
  // fall back to JSONC sanitization so comment-bearing files are not skipped as parse_error.
311
- const parsed = parseJsonWithJsoncFallback(readFileSync(resolvedPath, 'utf8'));
318
+ const parsed = parseJsonWithJsoncFallback(rawText);
312
319
  if (parsed === null)
313
320
  return { ok: false, reason: 'parse_error' };
314
321
  return { ok: true, json: parsed };
315
322
  }
323
+ function isSupportedTextConfigFormat(fileFormat) {
324
+ return fileFormat === 'json' || fileFormat === 'toml';
325
+ }
316
326
  /**
317
327
  * Evaluate all checks in a secondary group against the group's config file.
318
328
  * Returns true only if every check passes (ops-based). Non-ops checks are treated as failing.
@@ -343,7 +353,7 @@ function violationFromCheck(entry, compliance, check, expected) {
343
353
  description: check.description,
344
354
  finding_title: entry.finding_title,
345
355
  finding_description: entry.finding_description,
346
- policy_name: entry.policy_name,
356
+ ruleset_name: entry.ruleset_name,
347
357
  severity: compliance.severity,
348
358
  autofix_allowed: compliance.autofix_allowed,
349
359
  config_file_path: entry.config_file_path,
@@ -354,7 +364,7 @@ function violationFromCheck(entry, compliance, check, expected) {
354
364
  /** Evaluate one manifest row against on-disk config (used by gate + post-restart verify). */
355
365
  export function evaluateManifestEntryCompliance(entry) {
356
366
  const compliance = entry.fix ?? entry.compliance;
357
- if (!compliance || compliance.file_format !== 'json')
367
+ if (!compliance || !isSupportedTextConfigFormat(compliance.file_format))
358
368
  return { violations: [] };
359
369
  const checks = compliance.checks ?? [];
360
370
  if (checks.length === 0)
@@ -463,9 +473,9 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
463
473
  skippedNoCompliance++;
464
474
  continue;
465
475
  }
466
- if (compliance.file_format !== 'json') {
476
+ if (!isSupportedTextConfigFormat(compliance.file_format)) {
467
477
  skippedNonJson++;
468
- hookRunLog(`compliance_check: skipping non-json entry uuid=${entry.uuid}`);
478
+ hookRunLog(`compliance_check: skipping unsupported file_format=${compliance.file_format} uuid=${entry.uuid}`);
469
479
  continue;
470
480
  }
471
481
  if ((compliance.checks ?? []).length === 0) {
@@ -795,7 +805,7 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
795
805
  continue;
796
806
  }
797
807
  const spec = remediationFixSpec(inst);
798
- const checks = spec?.file_format === 'json' ? (spec.checks ?? []) : [];
808
+ const checks = isSupportedTextConfigFormat(spec?.file_format) ? (spec?.checks ?? []) : [];
799
809
  if (checks.length === 0) {
800
810
  remaining.push(raw);
801
811
  continue;
@@ -14,7 +14,7 @@ import { ensureAuthentication } from '../auth/auth_flow.js';
14
14
  import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
15
15
  import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
16
16
  import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
17
- import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
17
+ import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, collectGrokCliVersion, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
18
18
  import { ensureCursorUserSettingsSnapshotInBatch } from '../collection/ensure_cursor_user_settings_snapshot.js';
19
19
  import { collectSkillsCliInstalled } from '../collection/skills_cli_collector.js';
20
20
  import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
@@ -170,6 +170,14 @@ async function collectAllConfigFiles(endpointBase) {
170
170
  configFiles.push(entry);
171
171
  }
172
172
  }
173
+ hookRunLog(`reading Grok CLI version`);
174
+ for (const entry of collectGrokCliVersion(HOME_DIR)) {
175
+ const key = `${entry.file_type}\t${entry.file_path}`;
176
+ if (!existingPaths.has(key)) {
177
+ existingPaths.add(key);
178
+ configFiles.push(entry);
179
+ }
180
+ }
173
181
  hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
174
182
  const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
175
183
  hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
- import { atomicWriteJson, getRemediationStateDir } from './management_storage.js';
8
+ import { atomicWriteJson, getDeferredVscdbApplyPath, getRemediationStateDir, } from './management_storage.js';
9
9
  import { hookRunLog } from './hook_logger.js';
10
10
  export const REMEDIATION_APPLY_TRACKING_BASENAME = 'remediation_apply_tracking.json';
11
11
  /** Consecutive post-restart verification failures before we stop autofix for a UUID. */
@@ -116,6 +116,9 @@ export function recordRemediationVerificationFailure(uuid, reason) {
116
116
  * supported for unit tests only — do not use a global violation list from compliance_check
117
117
  * in production: shadow-key merge bugs once made that list empty while the setting was still
118
118
  * wrong, which incorrectly marked remediations verified.
119
+ *
120
+ * If the deferred vscdb queue file is still on disk, Cursor restart / apply_deferred_vscdb has
121
+ * not completed — leave pending and do not quarantine (setting unchanged is expected).
119
122
  */
120
123
  export function processPendingPostRestartVerifications(violationProbe) {
121
124
  const isStillViolating = typeof violationProbe === 'function'
@@ -123,6 +126,7 @@ export function processPendingPostRestartVerifications(violationProbe) {
123
126
  : (uuid) => violationProbe.has(uuid);
124
127
  const file = readRemediationApplyTrackingFile();
125
128
  const outcomes = [];
129
+ const deferredQueuePending = existsSync(getDeferredVscdbApplyPath());
126
130
  for (const [uuid, entry] of Object.entries(file.entries)) {
127
131
  if (!entry.pending_post_restart_verify)
128
132
  continue;
@@ -131,6 +135,10 @@ export function processPendingPostRestartVerifications(violationProbe) {
131
135
  outcomes.push({ uuid, status: 'verified', consecutive_failures: 0 });
132
136
  continue;
133
137
  }
138
+ if (deferredQueuePending) {
139
+ hookRunLog(`remediation_tracking: defer verify uuid=${uuid} — deferred vscdb queue still pending (restart/apply not done yet)`);
140
+ continue;
141
+ }
134
142
  const reason = 'Setting unchanged after apply — the agent may have changed its config format and auto-fix cannot apply this policy.';
135
143
  const { entry: updated, newlyQuarantined } = recordRemediationVerificationFailure(uuid, reason);
136
144
  outcomes.push({
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rename
2
2
  import { delimiter, dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { executeBody } from '../../endpoint_client/http_transport.js';
5
- import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
5
+ import { parseJsonWithJsoncFallback, parseTomlText, stringifyToml } from '../readers/file_readers.js';
6
6
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
7
7
  import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
8
8
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
@@ -1031,10 +1031,15 @@ export async function applyDeferredVscdbFromDisk() {
1031
1031
  return true;
1032
1032
  }
1033
1033
  try {
1034
+ let applied = 0;
1035
+ let skippedMissing = 0;
1034
1036
  for (const it of payload.items) {
1037
+ // Workspace storage can disappear between queue and restart (Cursor GC). Skip those
1038
+ // items instead of aborting the whole batch — other workspaces still need the write.
1035
1039
  if (!existsSync(it.dbPath)) {
1036
- hookRunLog(`deferred_vscdb: database missing ${it.dbPath}`);
1037
- return false;
1040
+ hookRunLog(`deferred_vscdb: database missing — skipping ${it.dbPath}`);
1041
+ skippedMissing++;
1042
+ continue;
1038
1043
  }
1039
1044
  if (!assertSafeSqliteIdentifiersForItemTable(it.table, it.key_column, it.value_column)) {
1040
1045
  return false;
@@ -1047,8 +1052,9 @@ export async function applyDeferredVscdbFromDisk() {
1047
1052
  hookRunLog(`deferred_vscdb: INSERT OR REPLACE changed 0 rows key=${it.target_key} db=${it.dbPath} — keeping queue file`);
1048
1053
  return false;
1049
1054
  }
1055
+ applied++;
1050
1056
  }
1051
- hookRunLog(`deferred_vscdb: applied ${payload.items.length} queued update(s)`);
1057
+ hookRunLog(`deferred_vscdb: applied ${applied} queued update(s) skipped_missing=${skippedMissing}`);
1052
1058
  const authKey = readStoredAuthKey();
1053
1059
  for (const u of postApplyUploads) {
1054
1060
  if (!authKey) {
@@ -1422,7 +1428,7 @@ export function enforceRemediation(instruction) {
1422
1428
  }
1423
1429
  return { ok: true, deferredSqlite: false };
1424
1430
  }
1425
- if (fixSpec?.file_format !== 'json') {
1431
+ if (fixSpec?.file_format !== 'json' && fixSpec?.file_format !== 'toml') {
1426
1432
  return fail(`unsupported file format: ${String(fixSpec?.file_format ?? 'undefined')}`);
1427
1433
  }
1428
1434
  const worktreeRoot = parseWorktreeRootFromPath(inst.config_file_path);
@@ -1445,26 +1451,39 @@ export function enforceRemediation(instruction) {
1445
1451
  else if (!existsSync(dir)) {
1446
1452
  mkdirSync(dir, { recursive: true, mode: 0o700 });
1447
1453
  }
1448
- let configJson = {};
1454
+ const isToml = fixSpec?.file_format === 'toml';
1455
+ let configObj = {};
1449
1456
  if (existsSync(inst.config_file_path)) {
1450
- // OpenCode configs are JSONC; parse strict JSON first, then JSONC fallback so a
1451
- // comment-bearing opencode.json(c) keeps its existing settings instead of being reset to
1452
- // {} on a parse failure. NOTE: the write-back below is JSON.stringify, so comments and
1453
- // trailing commas in the original file are not preserved (same as every other agent's
1454
- // JSON config) — only the key/value settings are retained and patched.
1455
- const parsed = parseJsonWithJsoncFallback(readFileSync(inst.config_file_path, 'utf8'));
1456
- if (parsed !== null) {
1457
- configJson = parsed;
1457
+ const rawText = readFileSync(inst.config_file_path, 'utf8');
1458
+ if (isToml) {
1459
+ const parsed = parseTomlText(rawText);
1460
+ if (parsed !== null) {
1461
+ configObj = parsed;
1462
+ }
1463
+ else {
1464
+ hookRunLog(`remediation_enforce: could not parse existing TOML file, starting fresh uuid=${inst.uuid}`);
1465
+ }
1458
1466
  }
1459
1467
  else {
1460
- hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${inst.uuid}`);
1468
+ // OpenCode configs are JSONC; parse strict JSON first, then JSONC fallback so a
1469
+ // comment-bearing opencode.json(c) keeps its existing settings instead of being reset to
1470
+ // {} on a parse failure. NOTE: the write-back below is JSON.stringify, so comments and
1471
+ // trailing commas in the original file are not preserved (same as every other agent's
1472
+ // JSON config) — only the key/value settings are retained and patched.
1473
+ const parsed = parseJsonWithJsoncFallback(rawText);
1474
+ if (parsed !== null) {
1475
+ configObj = parsed;
1476
+ }
1477
+ else {
1478
+ hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${inst.uuid}`);
1479
+ }
1461
1480
  }
1462
1481
  }
1463
1482
  for (const check of checks) {
1464
- applyCheck(configJson, check);
1483
+ applyCheck(configObj, check);
1465
1484
  }
1466
- syncFlatGlobalCursorIgnoreListKey(configJson);
1467
- const content = JSON.stringify(configJson, null, 2);
1485
+ syncFlatGlobalCursorIgnoreListKey(configObj);
1486
+ const content = isToml ? stringifyToml(configObj) : JSON.stringify(configObj, null, 2);
1468
1487
  const tmp = `${inst.config_file_path}.tmp`;
1469
1488
  writeFileSync(tmp, content, 'utf8');
1470
1489
  renameSync(tmp, inst.config_file_path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.5.11",
3
+ "version": "1.5.15",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,8 +56,9 @@
56
56
  "vitest": "^4.1.8"
57
57
  },
58
58
  "dependencies": {
59
- "axios": "^1.15.2",
59
+ "axios": "^1.18.1",
60
60
  "canonicalize": "^2.1.0",
61
- "optimus-tofu": "^0.1.18"
61
+ "optimus-tofu": "^0.1.18",
62
+ "smol-toml": "^1.7.1"
62
63
  }
63
64
  }