log-llm-config 1.4.9 → 1.4.11

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.
@@ -39,6 +39,8 @@ function parseIde() {
39
39
  return 'claude';
40
40
  if (v === 'copilot')
41
41
  return 'copilot';
42
+ if (v === 'opencode')
43
+ return 'opencode';
42
44
  return 'cursor';
43
45
  }
44
46
  if (process.argv.includes('--claude'))
@@ -48,6 +50,8 @@ function parseIde() {
48
50
  function defaultAgentFromIde(ide) {
49
51
  if (ide === 'copilot')
50
52
  return 'copilot';
53
+ if (ide === 'opencode')
54
+ return 'opencode';
51
55
  return ide === 'claude' ? 'claude' : 'cursor';
52
56
  }
53
57
  function parseAgent(ide) {
@@ -60,13 +64,13 @@ function parseAgent(ide) {
60
64
  return defaultAgentFromIde(ide);
61
65
  }
62
66
  function printAllow(ide) {
63
- if (ide === 'claude' || ide === 'copilot')
67
+ if (ide === 'claude' || ide === 'copilot' || ide === 'opencode')
64
68
  console.log('{}');
65
69
  else
66
70
  console.log(JSON.stringify({ continue: true }));
67
71
  }
68
72
  function printAllowWithAdvisory(ide, advisoryMessage) {
69
- if (ide === 'claude' || ide === 'copilot') {
73
+ if (ide === 'claude' || ide === 'copilot' || ide === 'opencode') {
70
74
  console.log(JSON.stringify({ __optimus_advisory: true, advisory_message: advisoryMessage }));
71
75
  }
72
76
  else {
@@ -80,7 +84,7 @@ function printAllowWithAdvisory(ide, advisoryMessage) {
80
84
  function blockPayload(ide, violationMessage) {
81
85
  const prefix = 'Prompt blocked by Optimus: ';
82
86
  const text = prefix + violationMessage;
83
- if (ide === 'claude' || ide === 'copilot') {
87
+ if (ide === 'claude' || ide === 'copilot' || ide === 'opencode') {
84
88
  return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
85
89
  }
86
90
  return JSON.stringify({ continue: false, user_message: text });
@@ -138,6 +142,10 @@ export function formatClaudeAutofixDialog(appliedViolations) {
138
142
  export function formatCopilotAutofixDialog(appliedViolations) {
139
143
  return formatPreventiveAutofixDialog(appliedViolations, 'Copilot will now apply this policy to your environment.');
140
144
  }
145
+ /** OpenCode dialog after enforced/preventive remediation is applied locally (terminal, no restart). */
146
+ export function formatOpenCodeAutofixDialog(appliedViolations) {
147
+ return formatPreventiveAutofixDialog(appliedViolations, 'OpenCode will now apply this policy to your environment.');
148
+ }
141
149
  /** Cursor restart dialog after enforced/preventive remediation is applied locally. */
142
150
  export function formatCursorRestartAutofixDialog(appliedViolations) {
143
151
  return formatPreventiveAutofixDialog(appliedViolations, 'Cursor will now restart to apply this policy, and your context will be retained.');
@@ -270,7 +278,7 @@ export async function runCompliancePromptGate() {
270
278
  // Claude / Copilot: JSON remediations are written immediately; merge/verify timing can still leave
271
279
  // the in-process recheck red for the same UUID — allow in that case for immediate JSON agents.
272
280
  const appliedUuids = new Set(appliedViolations.map((v) => v.uuid));
273
- const claudeRecheckStaleAfterImmediateApply = (ide === 'claude' || ide === 'copilot') &&
281
+ const claudeRecheckStaleAfterImmediateApply = (ide === 'claude' || ide === 'copilot' || ide === 'opencode') &&
274
282
  !recheckOk &&
275
283
  recheck.violations.length > 0 &&
276
284
  recheck.violations.every((v) => appliedUuids.has(v.uuid));
@@ -278,9 +286,16 @@ export async function runCompliancePromptGate() {
278
286
  hookRunLog(`compliance_prompt_gate: ${ide} — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)`);
279
287
  }
280
288
  if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
281
- const immediateVerified = restartCommands.length === 0 &&
282
- !deferredSqlitePending &&
283
- (recheckOk || claudeRecheckStaleAfterImmediateApply);
289
+ // Immediate-JSON agents (Claude / Claude Desktop, Copilot, OpenCode) write the config file to
290
+ // disk synchronously; a restart_command, if present, only reloads the already-compliant file
291
+ // into the running app — it is not the mechanism that makes the fix take effect. So report
292
+ // "verified" now instead of leaving the finding "pending" in the UI until the next prompt.
293
+ // Cursor's restart can carry a deferred state.vscdb write that only lands post-restart, so
294
+ // Cursor still verifies inline only when no restart is pending.
295
+ const immediateJsonAgent = ide === 'claude' || ide === 'copilot' || ide === 'opencode';
296
+ const immediateVerified = !deferredSqlitePending &&
297
+ (recheckOk || claudeRecheckStaleAfterImmediateApply) &&
298
+ (restartCommands.length === 0 || immediateJsonAgent);
284
299
  if (immediateVerified) {
285
300
  confirmAppliedAutofixVerified(appliedViolations, reportPromises);
286
301
  await Promise.allSettled(reportPromises);
@@ -294,9 +309,11 @@ export async function runCompliancePromptGate() {
294
309
  ? formatClaudeAutofixDialog(appliedViolations)
295
310
  : ide === 'copilot'
296
311
  ? formatCopilotAutofixDialog(appliedViolations)
297
- : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
298
- .map((v) => autofixDialogLine(v))
299
- .join('\n')}${changePreviewSuffix}`;
312
+ : ide === 'opencode'
313
+ ? formatOpenCodeAutofixDialog(appliedViolations)
314
+ : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
315
+ .map((v) => autofixDialogLine(v))
316
+ .join('\n')}${changePreviewSuffix}`;
300
317
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
301
318
  if (restartCommands.length > 0)
302
319
  payload.restart_commands = restartCommands;
@@ -69,7 +69,7 @@ function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs
69
69
  t.dir_subdir_filename = p.dir_subdir_filename;
70
70
  if (p.dir_subdir_source)
71
71
  t.dir_subdir_source = p.dir_subdir_source;
72
- const key = `${t.path}\t${t.file_type}`;
72
+ const key = `${t.path}\t${t.file_type}\t${t.isDirectory ? 'dir' : 'file'}\t${t.dir_glob ?? ''}`;
73
73
  if (!seenPaths.has(key)) {
74
74
  seenPaths.add(key);
75
75
  targets.push(t);
@@ -176,4 +176,4 @@ function readInstalledExtensions(extensionsCachePath) {
176
176
  }
177
177
  return extensions;
178
178
  }
179
- export { readMCPConfig, readJSONFile, readMarkdownFile, readInstalledExtensions };
179
+ export { readMCPConfig, readJSONFile, readMarkdownFile, readInstalledExtensions, parseJsonWithJsoncFallback, };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ItemTable key metadata from backend file_path_registry (vscdb_read_queries), cached in
3
+ * file_collection_vscdb_contract.json — no hardcoded Cursor paths in compliance/remediation.
4
+ */
5
+ import { readFileCollectionVscdbContract } from '../runtime/management_storage.js';
6
+ function sanitizeItemTableKey(raw) {
7
+ if (typeof raw !== 'string')
8
+ return undefined;
9
+ const trimmed = raw.trim();
10
+ if (!trimmed || trimmed.length > 512 || /['"\\]/.test(trimmed))
11
+ return undefined;
12
+ return trimmed;
13
+ }
14
+ function sanitizeStateKey(raw) {
15
+ if (typeof raw !== 'string')
16
+ return undefined;
17
+ const trimmed = raw.trim();
18
+ if (!trimmed || trimmed.length > 256)
19
+ return undefined;
20
+ return trimmed;
21
+ }
22
+ export function itemTableKeyToStateKeyFromReadQueries(queries) {
23
+ const out = {};
24
+ if (!queries?.length)
25
+ return out;
26
+ for (const step of queries) {
27
+ const stateKey = sanitizeStateKey(step.state_key);
28
+ if (!stateKey)
29
+ continue;
30
+ for (const rawKey of step.item_table_keys ?? []) {
31
+ const key = sanitizeItemTableKey(rawKey);
32
+ if (key)
33
+ out[key] = stateKey;
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+ /** Boolean ItemTable rows: field name under `${itemKey}.${field}` for compliance paths. */
39
+ export function scalarItemTableFieldByItemKeyFromReadQueries(queries) {
40
+ const out = {};
41
+ if (!queries?.length)
42
+ return out;
43
+ for (const step of queries) {
44
+ if (step.value_kind !== 'boolean')
45
+ continue;
46
+ const stateKey = sanitizeStateKey(step.state_key);
47
+ if (!stateKey)
48
+ continue;
49
+ for (const rawKey of step.item_table_keys ?? []) {
50
+ const key = sanitizeItemTableKey(rawKey);
51
+ if (key)
52
+ out[key] = stateKey;
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ export function scalarItemTableFieldForKey(itemKey) {
58
+ const key = sanitizeItemTableKey(itemKey);
59
+ if (!key)
60
+ return undefined;
61
+ const contract = readFileCollectionVscdbContract();
62
+ const map = contract?.scalar_item_table_field_by_item_key;
63
+ const field = map?.[key];
64
+ if (typeof field === 'string' && field.trim())
65
+ return field.trim();
66
+ // Older cached contracts may omit scalar map; state_key from registry matches compliance field names.
67
+ const stateKey = contract?.item_table_key_to_state_key?.[key];
68
+ return typeof stateKey === 'string' && stateKey.trim() ? stateKey.trim() : undefined;
69
+ }
70
+ /** Separate registry rows (different state_key) — do not remap compliance paths onto manifest # key. */
71
+ export function itemTableKeysAreDistinctStateRows(applyKey, verifyKey) {
72
+ const a = sanitizeItemTableKey(applyKey);
73
+ const b = sanitizeItemTableKey(verifyKey);
74
+ if (!a || !b)
75
+ return false;
76
+ const map = readFileCollectionVscdbContract()?.item_table_key_to_state_key;
77
+ if (!map)
78
+ return false;
79
+ const stateA = map[a];
80
+ const stateB = map[b];
81
+ return typeof stateA === 'string' && typeof stateB === 'string' && stateA !== stateB;
82
+ }
@@ -13,6 +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
17
  import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
17
18
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
18
19
  import { resolveRemediationConfigPath } from './remediation_config_path.js';
@@ -294,12 +295,12 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
294
295
  }
295
296
  if (!existsSync(resolvedPath))
296
297
  return { ok: false, reason: 'file_not_found' };
297
- try {
298
- return { ok: true, json: JSON.parse(readFileSync(resolvedPath, 'utf8')) };
299
- }
300
- catch {
298
+ // OpenCode configs are JSONC (comments / trailing commas); parse strict JSON first, then
299
+ // fall back to JSONC sanitization so comment-bearing files are not skipped as parse_error.
300
+ const parsed = parseJsonWithJsoncFallback(readFileSync(resolvedPath, 'utf8'));
301
+ if (parsed === null)
301
302
  return { ok: false, reason: 'parse_error' };
302
- }
303
+ return { ok: true, json: parsed };
303
304
  }
304
305
  /**
305
306
  * Evaluate all checks in a secondary group against the group's config file.
@@ -678,12 +679,8 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
678
679
  updatedContent = itemKey ? (readVscdbItemTableJson(dbPath, itemKey) ?? undefined) : undefined;
679
680
  }
680
681
  else {
681
- try {
682
- updatedContent = JSON.parse(readFileSync(configPathForDisk, 'utf8'));
683
- }
684
- catch {
685
- updatedContent = undefined;
686
- }
682
+ updatedContent =
683
+ parseJsonWithJsoncFallback(readFileSync(configPathForDisk, 'utf8')) ?? undefined;
687
684
  }
688
685
  if (updatedContent !== undefined) {
689
686
  const fileType = (inst.file_type ?? '').trim();
@@ -906,11 +903,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
906
903
  continue;
907
904
  }
908
905
  }
909
- let rawContent;
910
- try {
911
- rawContent = JSON.parse(readFileSync(diskPath, 'utf8'));
912
- }
913
- catch {
906
+ const rawContent = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
907
+ if (rawContent === null) {
914
908
  hookRunLog(`satisfied_upload: could not read path=${diskPath} uuid=${entry.uuid}`);
915
909
  continue;
916
910
  }
@@ -3,6 +3,7 @@ import { delimiter, dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { executeBody } from '../../endpoint_client/http_transport.js';
6
+ import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
6
7
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
7
8
  import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
8
9
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
@@ -1396,10 +1397,16 @@ export function enforceRemediation(instruction) {
1396
1397
  }
1397
1398
  let configJson = {};
1398
1399
  if (existsSync(inst.config_file_path)) {
1399
- try {
1400
- configJson = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
1400
+ // OpenCode configs are JSONC; parse strict JSON first, then JSONC fallback so a
1401
+ // comment-bearing opencode.json(c) keeps its existing settings instead of being reset to
1402
+ // {} on a parse failure. NOTE: the write-back below is JSON.stringify, so comments and
1403
+ // trailing commas in the original file are not preserved (same as every other agent's
1404
+ // JSON config) — only the key/value settings are retained and patched.
1405
+ const parsed = parseJsonWithJsoncFallback(readFileSync(inst.config_file_path, 'utf8'));
1406
+ if (parsed !== null) {
1407
+ configJson = parsed;
1401
1408
  }
1402
- catch {
1409
+ else {
1403
1410
  hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${inst.uuid}`);
1404
1411
  }
1405
1412
  }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // Runs after Cursor reopens: delayed compliance verify, then deferred config uploads if verified.
3
+ import { runPostApplyVerification } from './log_config_files/runtime/compliance_check.js';
4
+ import { hookRunLog } from './log_config_files/runtime/hook_logger.js';
5
+ import { clearDeferredPostApplyUploads, delayMs, POST_RESTART_VERIFY_DELAY_MS, runDeferredPostApplyUploadsFromSidecar, } from './log_config_files/runtime/remediation_sync.js';
6
+ import { finalizeAndUploadComplianceSessionLog, setActiveComplianceLogPath, } from './log_config_files/runtime/compliance_session_log.js';
7
+ import { isThisCliModule } from './cli_invocation_match.js';
8
+ const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim() || null;
9
+ if (complianceLogPath) {
10
+ setActiveComplianceLogPath(complianceLogPath);
11
+ }
12
+ export async function runPostRestartVerify() {
13
+ hookRunLog(`post_restart_verify: sleeping ${POST_RESTART_VERIFY_DELAY_MS}ms before verify`);
14
+ await delayMs(POST_RESTART_VERIFY_DELAY_MS);
15
+ const outcomes = await runPostApplyVerification();
16
+ if (outcomes.length > 0) {
17
+ hookRunLog(`post_restart_verify: outcomes count=${outcomes.length} verified=${outcomes.filter((o) => o.status === 'verified').length}`);
18
+ }
19
+ else {
20
+ hookRunLog('post_restart_verify: no pending post-restart verifications');
21
+ clearDeferredPostApplyUploads();
22
+ return true;
23
+ }
24
+ const allVerified = outcomes.every((o) => o.status === 'verified');
25
+ if (allVerified) {
26
+ await runDeferredPostApplyUploadsFromSidecar();
27
+ return true;
28
+ }
29
+ clearDeferredPostApplyUploads();
30
+ return false;
31
+ }
32
+ if (isThisCliModule(process.argv[1], import.meta.url)) {
33
+ runPostRestartVerify()
34
+ .then(async (ok) => {
35
+ if (complianceLogPath) {
36
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, ok ? 'success' : 'error');
37
+ }
38
+ process.exit(ok ? 0 : 1);
39
+ })
40
+ .catch(async (e) => {
41
+ hookRunLog(`post_restart_verify: error: ${e instanceof Error ? e.message : String(e)}`);
42
+ clearDeferredPostApplyUploads();
43
+ if (complianceLogPath) {
44
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
45
+ }
46
+ process.exit(1);
47
+ });
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {