log-llm-config-staging 1.3.83 → 1.3.84

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/dist/cli.js CHANGED
@@ -38,6 +38,12 @@ const main = async () => {
38
38
  await import('./compliance_check_runner.js');
39
39
  return;
40
40
  }
41
+ if (args[0] === 'dialog_prefs') {
42
+ const { runDialogPrefsCli } = await import('./dialog_prefs_cli.js');
43
+ await runDialogPrefsCli();
44
+ process.exit(0);
45
+ return;
46
+ }
41
47
  // `npx log-llm-config@latest <name>` runs this file (default bin) with args[0] set — not the named bin file.
42
48
  if (args[0] === 'execute-trusted-restarts') {
43
49
  const { runExecuteTrustedRestartsFromStdin } = await import('./execute_trusted_restarts.js');
@@ -6,10 +6,10 @@
6
6
  * When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
7
7
  * the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
8
8
  */
9
- import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
10
- import { isRemediationQuarantined } from './log_config_files/runtime/remediation_apply_tracking.js';
9
+ import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, uploadSatisfiedManifestConfigs, } from './log_config_files/runtime/compliance_check.js';
10
+ import { isRemediationQuarantined, markRemediationApplyVerified, } from './log_config_files/runtime/remediation_apply_tracking.js';
11
11
  import { existsSync, readFileSync, statSync } from 'node:fs';
12
- import { getRemediationInstructionsPath, readRemediationInstructionsFile } from './log_config_files/runtime/management_storage.js';
12
+ import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
13
13
  import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
14
14
  import { isThisCliModule } from './cli_invocation_match.js';
15
15
  import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
@@ -17,6 +17,7 @@ import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
17
17
  import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid.js';
18
18
  import { syncRemediations } from './log_config_files/runtime/remediation_sync.js';
19
19
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
20
+ import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
20
21
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
21
22
  function parseIde() {
22
23
  const eq = process.argv.find((a) => a.startsWith('--ide='));
@@ -103,8 +104,11 @@ function uniquePolicyLabelsFromViolations(appliedViolations) {
103
104
  return policyNames.length > 0 ? policyNames.join('\n\n') : 'Agent security policy';
104
105
  }
105
106
  function formatPreventiveAutofixDialog(appliedViolations, applyLine) {
107
+ const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
108
+ const changePreviewBlock = changePreview ? `${changePreview}\n\n` : '';
106
109
  return ('Optimus Labs enforced a preventive agent security policy as determined by your security team:\n\n' +
107
110
  `${uniquePolicyLabelsFromViolations(appliedViolations)}\n\n` +
111
+ changePreviewBlock +
108
112
  `${applyLine}\n\n` +
109
113
  'Click OK to continue');
110
114
  }
@@ -166,17 +170,14 @@ export async function runCompliancePromptGate() {
166
170
  // unavailable server never delays the gate significantly.
167
171
  const hw = tryResolveHardwareUuid();
168
172
  if (hw) {
169
- const { remediations } = readRemediationInstructionsFile();
170
- if (Array.isArray(remediations) && remediations.length > 0) {
171
- try {
172
- await Promise.race([
173
- syncRemediations(loadEndpointBase(), hw),
174
- new Promise((resolve) => setTimeout(resolve, 3000)),
175
- ]);
176
- }
177
- catch {
178
- // Network or auth failure — fall back to local file state.
179
- }
173
+ try {
174
+ await Promise.race([
175
+ syncRemediations(loadEndpointBase(), hw),
176
+ new Promise((resolve) => setTimeout(resolve, 3000)),
177
+ ]);
178
+ }
179
+ catch {
180
+ // Network or auth failure — fall back to local file state.
180
181
  }
181
182
  }
182
183
  const status = runLocalRemediationComplianceCheck(agent);
@@ -248,13 +249,22 @@ export async function runCompliancePromptGate() {
248
249
  hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
249
250
  }
250
251
  if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
252
+ // For immediate (non-restart, non-deferred) fixes that the inline recheck confirmed,
253
+ // mark verified now so the next hook run does not false-quarantine due to pending flag.
254
+ if (recheckOk && restartCommands.length === 0 && !deferredSqlitePending) {
255
+ for (const v of appliedViolations) {
256
+ markRemediationApplyVerified(v.uuid);
257
+ }
258
+ }
259
+ const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
260
+ const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
251
261
  const autofixMessage = ide === 'cursor' && restartCommands.length > 0
252
262
  ? formatCursorRestartAutofixDialog(appliedViolations)
253
263
  : ide === 'claude'
254
264
  ? formatClaudeAutofixDialog(appliedViolations)
255
265
  : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
256
266
  .map((v) => autofixDialogLine(v))
257
- .join('\n')}`;
267
+ .join('\n')}${changePreviewSuffix}`;
258
268
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
259
269
  if (restartCommands.length > 0)
260
270
  payload.restart_commands = restartCommands;
@@ -300,6 +310,7 @@ export async function runCompliancePromptGate() {
300
310
  if (pruned.removed > 0) {
301
311
  await Promise.allSettled(pruned.reportPromises);
302
312
  }
313
+ await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
303
314
  printAllow(ide);
304
315
  }
305
316
  if (isRunAsCliModule()) {
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for macOS non-restart autofix dialogs and dialog preferences in ~/opt-ai-sec/management.
4
+ * Invoked from optimus-compliance-check.sh hooks.
5
+ */
6
+ import { execFileSync } from 'node:child_process';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { isNonRestartAutofixDialogSuppressed, setSuppressNonRestartAutofixDialogs, } from './log_config_files/runtime/dialog_preferences.js';
9
+ export const STOP_NOTIFICATIONS_BUTTON = 'Mute Alerts';
10
+ export const OK_BUTTON = 'OK';
11
+ function cliParts() {
12
+ const a = process.argv;
13
+ if (a[2] === 'dialog_prefs')
14
+ return a.slice(2);
15
+ return a.slice(2);
16
+ }
17
+ function parseArgValue(parts, flag) {
18
+ const eq = parts.find((p) => p.startsWith(`${flag}=`));
19
+ if (eq)
20
+ return eq.slice(flag.length + 1);
21
+ const idx = parts.indexOf(flag);
22
+ if (idx >= 0 && parts[idx + 1])
23
+ return parts[idx + 1];
24
+ return undefined;
25
+ }
26
+ function escapeAppleScriptString(text) {
27
+ return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
28
+ }
29
+ function icnsIconClause() {
30
+ const p = (process.env.OPTIMUS_DIALOG_ICNS ?? '').trim();
31
+ if (!p || !existsSync(p))
32
+ return 'with icon caution';
33
+ try {
34
+ const buf = readFileSync(p);
35
+ if (buf.length < 8 || buf.subarray(0, 4).toString('ascii') !== 'icns') {
36
+ return 'with icon caution';
37
+ }
38
+ }
39
+ catch {
40
+ return 'with icon caution';
41
+ }
42
+ return `with icon POSIX file "${escapeAppleScriptString(p)}"`;
43
+ }
44
+ export function buildNonRestartAutofixDialogScript(message, target) {
45
+ const msg = escapeAppleScriptString(message);
46
+ const icon = icnsIconClause();
47
+ const mute = escapeAppleScriptString(STOP_NOTIFICATIONS_BUTTON);
48
+ const body = `display dialog "${msg}" with title "Optimus Labs" buttons {"${mute}", "${OK_BUTTON}"} default button "${OK_BUTTON}" ${icon}`;
49
+ if (target === 'cursor') {
50
+ return `tell application "Cursor" to ${body}`;
51
+ }
52
+ if (target === 'claude-app') {
53
+ return `tell application "Claude" to ${body}`;
54
+ }
55
+ return body;
56
+ }
57
+ export function showNonRestartAutofixDialog(message, target) {
58
+ const script = buildNonRestartAutofixDialogScript(message, target);
59
+ let out = '';
60
+ try {
61
+ out = execFileSync('osascript', ['-'], { input: script, encoding: 'utf8' });
62
+ }
63
+ catch (err) {
64
+ const e = err;
65
+ out = typeof e.stdout === 'string' ? e.stdout : '';
66
+ // User dismissed or osascript failed — do not block callers; preference only set on explicit Stop.
67
+ if (!out.includes(STOP_NOTIFICATIONS_BUTTON)) {
68
+ throw err;
69
+ }
70
+ }
71
+ if (out.includes(STOP_NOTIFICATIONS_BUTTON)) {
72
+ setSuppressNonRestartAutofixDialogs(true);
73
+ return 'stop_notifications';
74
+ }
75
+ return 'ok';
76
+ }
77
+ function readMessageFile(path) {
78
+ if (!path || !existsSync(path)) {
79
+ throw new Error(`message file not found: ${path}`);
80
+ }
81
+ return readFileSync(path, 'utf8');
82
+ }
83
+ export async function runDialogPrefsCli() {
84
+ const parts = cliParts();
85
+ const sub = parts[1];
86
+ if (sub === 'is_suppressed') {
87
+ console.log(isNonRestartAutofixDialogSuppressed() ? 'true' : 'false');
88
+ return;
89
+ }
90
+ if (sub === 'set_suppress') {
91
+ setSuppressNonRestartAutofixDialogs(true);
92
+ return;
93
+ }
94
+ if (sub === 'show') {
95
+ const messageFile = parseArgValue(parts, '--message-file');
96
+ const targetRaw = parseArgValue(parts, '--target') ?? 'terminal';
97
+ if (targetRaw !== 'cursor' && targetRaw !== 'claude-app' && targetRaw !== 'terminal') {
98
+ throw new Error(`invalid --target: ${targetRaw}`);
99
+ }
100
+ const message = readMessageFile(messageFile ?? '');
101
+ showNonRestartAutofixDialog(message, targetRaw);
102
+ return;
103
+ }
104
+ throw new Error(`unknown dialog_prefs subcommand: ${sub ?? '(none)'}`);
105
+ }
@@ -16,7 +16,8 @@ import { join } from 'node:path';
16
16
  import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
17
17
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
18
18
  import { resolveRemediationConfigPath } from './remediation_config_path.js';
19
- import { isRemediationQuarantined, markRemediationApplyPendingVerification, processPendingPostRestartVerifications, } from './remediation_apply_tracking.js';
19
+ import { resolveOpsTargetPath } from './ops_target_path.js';
20
+ import { isRemediationQuarantined, markRemediationApplyPendingVerification, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
20
21
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
21
22
  import { loadEndpointBase } from '../sender/endpoint_config.js';
22
23
  import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
@@ -166,13 +167,8 @@ function verifyOpsApplied(configJson, settingPath, ops) {
166
167
  const add = ops.add ?? {};
167
168
  const remove = ops.remove ?? {};
168
169
  const keys = new Set([...Object.keys(set), ...Object.keys(add), ...Object.keys(remove)]);
169
- // If ops targets a single leaf key, check at settingPath; otherwise treat keys as subkeys under parent.
170
170
  for (const k of keys) {
171
- const targetPath = k === leafKey || (keys.size === 1 && (k === leafKey || k === ''))
172
- ? settingPath
173
- : parentPath
174
- ? `${parentPath}.${k}`
175
- : k;
171
+ const targetPath = resolveOpsTargetPath(settingPath, k);
176
172
  if (Object.prototype.hasOwnProperty.call(set, k)) {
177
173
  const cur = getByPath(configJson, targetPath);
178
174
  const expected = set[k];
@@ -770,6 +766,68 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
770
766
  }
771
767
  return { removed, reportPromises };
772
768
  }
769
+ /** Throttle satisfied-manifest uploads (disk already matches ops; autofix did not run). */
770
+ const SATISFIED_MANIFEST_UPLOAD_COOLDOWN_MS = 5 * 60 * 1000;
771
+ /**
772
+ * When local compliance already passes, autofix is skipped — so settings never reach the server.
773
+ * Upload satisfied JSON remediations (throttled) so scan can resolve findings.
774
+ */
775
+ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
776
+ const { remediations } = readRemediationInstructionsFile();
777
+ const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
778
+ const promises = [];
779
+ for (const entry of entries) {
780
+ const { violations } = evaluateManifestEntryCompliance(entry);
781
+ if (violations.length > 0)
782
+ continue;
783
+ const inst = entry;
784
+ const fileType = (inst.file_type ?? '').trim();
785
+ if (!fileType)
786
+ continue;
787
+ const diskPath = resolveRemediationConfigPath(entry.config_file_path);
788
+ if (diskPath.includes('#'))
789
+ continue;
790
+ const tracking = readRemediationApplyTrackingFile();
791
+ const prev = tracking.entries[entry.uuid];
792
+ const lastUpload = prev?.last_satisfied_upload_at;
793
+ if (lastUpload) {
794
+ const elapsed = Date.now() - Date.parse(lastUpload);
795
+ if (!Number.isNaN(elapsed) && elapsed < SATISFIED_MANIFEST_UPLOAD_COOLDOWN_MS) {
796
+ continue;
797
+ }
798
+ }
799
+ let rawContent;
800
+ try {
801
+ rawContent = JSON.parse(readFileSync(diskPath, 'utf8'));
802
+ }
803
+ catch {
804
+ hookRunLog(`satisfied_upload: could not read path=${diskPath} uuid=${entry.uuid}`);
805
+ continue;
806
+ }
807
+ const hw = tryResolveHardwareUuid();
808
+ if (!hw) {
809
+ hookRunLog(`satisfied_upload: skip uuid=${entry.uuid} (hardware UUID unavailable)`);
810
+ continue;
811
+ }
812
+ const uploadPath = entry.config_file_path.trim() || diskPath;
813
+ promises.push(ensureAuthentication(hw)
814
+ .then((authKey) => sendConfigFile({ file_type: fileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
815
+ .then((sentOk) => {
816
+ hookRunLog(`satisfied_upload: uuid=${entry.uuid} path=${uploadPath} ok=${sentOk}`);
817
+ if (sentOk) {
818
+ const file = readRemediationApplyTrackingFile();
819
+ const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
820
+ ent.last_satisfied_upload_at = new Date().toISOString();
821
+ file.entries[entry.uuid] = ent;
822
+ writeRemediationApplyTrackingFile(file);
823
+ }
824
+ })
825
+ .catch((err) => {
826
+ hookRunLog(`satisfied_upload: failed uuid=${entry.uuid} err=${err instanceof Error ? err.message : String(err)}`);
827
+ }));
828
+ }
829
+ return promises;
830
+ }
773
831
  /**
774
832
  * Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
775
833
  * Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
@@ -789,5 +847,6 @@ export async function runComplianceCheck() {
789
847
  if (pruned.removed > 0) {
790
848
  await Promise.allSettled(pruned.reportPromises);
791
849
  }
850
+ await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
792
851
  }
793
852
  }
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { atomicWriteJson, getDialogPreferencesPath, } from './management_storage.js';
3
+ export function readDialogPreferences() {
4
+ const path = getDialogPreferencesPath();
5
+ if (!existsSync(path)) {
6
+ return { version: 1, suppress_non_restart_autofix_dialogs: false };
7
+ }
8
+ try {
9
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
10
+ if (parsed?.version !== 1)
11
+ return { version: 1, suppress_non_restart_autofix_dialogs: false };
12
+ return {
13
+ version: 1,
14
+ suppress_non_restart_autofix_dialogs: Boolean(parsed.suppress_non_restart_autofix_dialogs),
15
+ };
16
+ }
17
+ catch {
18
+ return { version: 1, suppress_non_restart_autofix_dialogs: false };
19
+ }
20
+ }
21
+ export function isNonRestartAutofixDialogSuppressed() {
22
+ return readDialogPreferences().suppress_non_restart_autofix_dialogs === true;
23
+ }
24
+ export function setSuppressNonRestartAutofixDialogs(suppress) {
25
+ const next = {
26
+ version: 1,
27
+ suppress_non_restart_autofix_dialogs: suppress,
28
+ updated_at: new Date().toISOString(),
29
+ };
30
+ atomicWriteJson(getDialogPreferencesPath(), next);
31
+ }
@@ -17,6 +17,8 @@ export const DEFERRED_VSCDB_RESTART_LOG_BASENAME = 'deferred_vscdb_restart.log';
17
17
  * of hardcoded Cursor paths.
18
18
  */
19
19
  export const FILE_COLLECTION_VSCDB_CONTRACT_BASENAME = 'file_collection_vscdb_contract.json';
20
+ /** User preferences for Optimus macOS dialogs (hook-driven). */
21
+ export const DIALOG_PREFERENCES_BASENAME = 'optimus_dialog_preferences.json';
20
22
  export function sanitizeReactiveStorageItemKey(raw) {
21
23
  if (typeof raw !== 'string')
22
24
  return undefined;
@@ -59,6 +61,9 @@ export function getDeferredVscdbRestartLogPath() {
59
61
  export function getFileCollectionVscdbContractPath() {
60
62
  return join(getManagementDir(), FILE_COLLECTION_VSCDB_CONTRACT_BASENAME);
61
63
  }
64
+ export function getDialogPreferencesPath() {
65
+ return join(getManagementDir(), DIALOG_PREFERENCES_BASENAME);
66
+ }
62
67
  export function readFileCollectionVscdbContract() {
63
68
  const path = getFileCollectionVscdbContractPath();
64
69
  if (!existsSync(path))
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Map ops.add/set/remove keys to JSON dot-paths for apply + verify.
3
+ *
4
+ * Server may use either the leaf key (e.g. `deny`) or the full setting_path
5
+ * (e.g. `cursor.general.globalCursorIgnoreList`) as the op map key.
6
+ */
7
+ export function resolveOpsTargetPath(settingPath, opKey) {
8
+ const parts = settingPath.split('.');
9
+ const leafKey = parts[parts.length - 1] ?? '';
10
+ const parentPath = parts.slice(0, -1).join('.');
11
+ if (opKey === settingPath || opKey === leafKey) {
12
+ return settingPath;
13
+ }
14
+ if (opKey.includes('.')) {
15
+ return opKey;
16
+ }
17
+ return parentPath ? `${parentPath}.${opKey}` : opKey;
18
+ }
@@ -6,6 +6,30 @@ import { join } from 'node:path';
6
6
  * (see optimus_security/endpoint/log_config_file/handler._canonical_cursor_user_state_vscdb_path).
7
7
  */
8
8
  export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state.vscdb';
9
+ /** Portable key for Cursor User settings.json (globalCursorIgnoreList). */
10
+ export const PORTABLE_CURSOR_USER_SETTINGS = 'Cursor/User/settings.json';
11
+ function expandLeadingTilde(configFilePath) {
12
+ const t = configFilePath.trim();
13
+ if (t.startsWith('~/')) {
14
+ return join(homedir(), t.slice(2));
15
+ }
16
+ return configFilePath;
17
+ }
18
+ function cursorUserSettingsAbsolutePaths() {
19
+ const h = homedir();
20
+ if (process.platform === 'darwin') {
21
+ const support = join(h, 'Library', 'Application Support');
22
+ const variants = ['Cursor', 'Cursor - Insiders', 'Cursor Nightly', 'Cursor Next'];
23
+ return variants.map((name) => join(support, name, 'User', 'settings.json'));
24
+ }
25
+ if (process.platform === 'win32') {
26
+ const appData = process.env.APPDATA;
27
+ if (appData)
28
+ return [join(appData, 'Cursor', 'User', 'settings.json')];
29
+ return [join(h, 'AppData', 'Roaming', 'Cursor', 'User', 'settings.json')];
30
+ }
31
+ return [join(h, '.config', 'Cursor', 'User', 'settings.json')];
32
+ }
9
33
  function normalizeSlashes(p) {
10
34
  return p.trim().replace(/\\/g, '/');
11
35
  }
@@ -74,13 +98,23 @@ function cursorStateVscdbAbsoluteBasePaths() {
74
98
  * No-op for absolute paths and for relative paths that are not the portable vscdb key.
75
99
  */
76
100
  export function resolveRemediationConfigPath(configFilePath) {
77
- const t = normalizeSlashes(configFilePath);
101
+ const expandedTilde = expandLeadingTilde(configFilePath);
102
+ const t = normalizeSlashes(expandedTilde);
78
103
  const hash = t.indexOf('#');
79
104
  const base = hash >= 0 ? t.slice(0, hash) : t;
80
105
  const frag = hash >= 0 ? t.slice(hash) : '';
81
106
  const baseNorm = base.replace(/\/+$/, '');
82
107
  if (baseNorm.startsWith('/') || /^[a-zA-Z]:\//.test(baseNorm)) {
83
- return configFilePath;
108
+ return expandedTilde;
109
+ }
110
+ const portableSettings = PORTABLE_CURSOR_USER_SETTINGS;
111
+ if (baseNorm === portableSettings || baseNorm.endsWith('/' + portableSettings)) {
112
+ const candidates = cursorUserSettingsAbsolutePaths();
113
+ for (const abs of candidates) {
114
+ if (existsSync(abs))
115
+ return abs;
116
+ }
117
+ return candidates[0];
84
118
  }
85
119
  const portable = PORTABLE_CURSOR_USER_STATE_VSCDB;
86
120
  if (baseNorm !== portable && !baseNorm.endsWith('/' + portable)) {
@@ -2,10 +2,11 @@ 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 { execFileSync } from 'node:child_process';
5
- import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
5
+ import { executeBody } from '../../endpoint_client/http_transport.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';
9
+ import { ensureAuthentication } from '../auth/auth_flow.js';
9
10
  import { createSignature } from '../sender/signing.js';
10
11
  import { loadEndpointBase } from '../sender/endpoint_config.js';
11
12
  import { tryResolveHardwareUuid } from './hardware_uuid.js';
@@ -16,6 +17,7 @@ import { parseWorktreeRootFromPath } from './worktree_scanner.js';
16
17
  import { reportAbsentWorktrees } from './worktree_absent.js';
17
18
  import { buildApiUrl, getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
18
19
  import { resolveRemediationConfigPath } from './remediation_config_path.js';
20
+ import { resolveOpsTargetPath } from './ops_target_path.js';
19
21
  import { resolveSqlite3Binary } from './sqlite_binary.js';
20
22
  import { CURSOR_COMPOSER_SHADOW_KEYS } from '../readers/cursor_shadow_merge_policy.js';
21
23
  import { REACTIVE_STORAGE_ITEM_KEY_SUFFIX, composerShadowKeysFromContract, reactiveStorageItemKeyForVscdb, } from '../readers/vscdb_reactive_storage.js';
@@ -156,38 +158,95 @@ function transferQuarantineForRetiredUuids(removed, added, retiredInstructions,
156
158
  if (changed)
157
159
  writeRemediationApplyTrackingFile(file);
158
160
  }
161
+ async function ensureAuthKeyForMachine(machineUuid) {
162
+ const stored = readStoredAuthKey();
163
+ if (stored?.key)
164
+ return stored;
165
+ try {
166
+ return await ensureAuthentication(machineUuid);
167
+ }
168
+ catch (err) {
169
+ hookRunLog(`remediation_sync: ensureAuthentication failed: ${err instanceof Error ? err.message : String(err)}`);
170
+ complianceRunnerDiag(`remediation_sync: ensureAuthentication failed: ${err instanceof Error ? err.message : String(err)}`);
171
+ return null;
172
+ }
173
+ }
174
+ function buildSignedMachinePostBody(machineUuid, fields, authKey) {
175
+ const payload = { machine_uuid: machineUuid, ...fields };
176
+ const signature = createSignature(payload, authKey.key);
177
+ return JSON.stringify({ ...payload, signature });
178
+ }
159
179
  export async function fetchSync(endpointBase, machineUuid, activeUuids, timeoutMs = 8000) {
160
- const uuidsParam = activeUuids.join(',');
161
- const url = `${buildApiUrl(endpointBase, '/api/findings/remediations/sync/')}?machine_uuid=${encodeURIComponent(machineUuid)}&active_uuids=${encodeURIComponent(uuidsParam)}`;
162
- const { statusCode, body } = await executeGet(url, timeoutMs);
163
- if (statusCode !== 200 || !body) {
164
- const line = `remediation_sync_get: url=${url} status=${statusCode} bytes=${body?.length ?? 0}`;
180
+ const sortedUuids = [...new Set(activeUuids.map((u) => u.trim()).filter(Boolean))].sort();
181
+ const authKey = await ensureAuthKeyForMachine(machineUuid);
182
+ if (!authKey) {
183
+ hookRunLog('remediation_sync_post: no auth key, skipping');
184
+ complianceRunnerDiag('remediation_sync_post: no auth key');
185
+ return null;
186
+ }
187
+ const url = buildApiUrl(endpointBase, '/api/findings/remediations/sync/');
188
+ const body = buildSignedMachinePostBody(machineUuid, { active_uuids: sortedUuids }, authKey);
189
+ let statusCode = 0;
190
+ let responseBody = '';
191
+ try {
192
+ const result = await executeBody(url, 'POST', body, timeoutMs);
193
+ statusCode = result.statusCode;
194
+ responseBody = result.body;
195
+ }
196
+ catch (err) {
197
+ const line = `remediation_sync_post: url=${url} error=${err instanceof Error ? err.message : String(err)}`;
198
+ hookRunLog(line);
199
+ complianceRunnerDiag(line);
200
+ return null;
201
+ }
202
+ if (statusCode !== 200 || !responseBody) {
203
+ const line = `remediation_sync_post: url=${url} status=${statusCode} bytes=${responseBody?.length ?? 0}`;
165
204
  hookRunLog(line);
166
205
  complianceRunnerDiag(line);
167
206
  return null;
168
207
  }
169
208
  try {
170
- return JSON.parse(body);
209
+ return JSON.parse(responseBody);
171
210
  }
172
211
  catch {
173
- hookRunLog('remediation_sync_get: invalid JSON body');
174
- complianceRunnerDiag('remediation_sync_get: invalid JSON body');
212
+ hookRunLog('remediation_sync_post: invalid JSON body');
213
+ complianceRunnerDiag('remediation_sync_post: invalid JSON body');
175
214
  return null;
176
215
  }
177
216
  }
178
217
  async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000) {
179
- const url = `${buildApiUrl(endpointBase, '/api/findings/remediations/manifest/')}?machine_uuid=${encodeURIComponent(machineUuid)}&uuids=${encodeURIComponent(uuids.join(','))}`;
180
- const { statusCode, body } = await executeGet(url, timeoutMs);
181
- if (statusCode !== 200 || !body) {
182
- const line = `remediation_manifest_get: url=${url} status=${statusCode} bytes=${body?.length ?? 0}`;
218
+ const sortedUuids = [...new Set(uuids.map((u) => u.trim()).filter(Boolean))].sort();
219
+ const authKey = await ensureAuthKeyForMachine(machineUuid);
220
+ if (!authKey) {
221
+ hookRunLog('remediation_manifest_post: no auth key, skipping');
222
+ complianceRunnerDiag('remediation_manifest_post: no auth key');
223
+ return null;
224
+ }
225
+ const url = buildApiUrl(endpointBase, '/api/findings/remediations/manifest/');
226
+ const body = buildSignedMachinePostBody(machineUuid, { uuids: sortedUuids }, authKey);
227
+ let statusCode = 0;
228
+ let responseBody = '';
229
+ try {
230
+ const result = await executeBody(url, 'POST', body, timeoutMs);
231
+ statusCode = result.statusCode;
232
+ responseBody = result.body;
233
+ }
234
+ catch (err) {
235
+ const line = `remediation_manifest_post: url=${url} error=${err instanceof Error ? err.message : String(err)}`;
236
+ hookRunLog(line);
237
+ complianceRunnerDiag(line);
238
+ return null;
239
+ }
240
+ if (statusCode !== 200 || !responseBody) {
241
+ const line = `remediation_manifest_post: url=${url} status=${statusCode} bytes=${responseBody?.length ?? 0}`;
183
242
  hookRunLog(line);
184
243
  complianceRunnerDiag(line);
185
244
  return null;
186
245
  }
187
246
  try {
188
- const parsed = JSON.parse(body);
247
+ const parsed = JSON.parse(responseBody);
189
248
  if (!Array.isArray(parsed.remediations)) {
190
- complianceRunnerDiag('remediation_manifest_get: JSON remediations is not an array');
249
+ complianceRunnerDiag('remediation_manifest_post: JSON remediations is not an array');
191
250
  return null;
192
251
  }
193
252
  return parsed.remediations.map((raw) => {
@@ -197,7 +256,7 @@ async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000)
197
256
  });
198
257
  }
199
258
  catch {
200
- complianceRunnerDiag('remediation_manifest_get: invalid JSON body');
259
+ complianceRunnerDiag('remediation_manifest_post: invalid JSON body');
201
260
  return null;
202
261
  }
203
262
  }
@@ -308,11 +367,7 @@ function applyCheck(configJson, check) {
308
367
  ...Object.keys(remove ?? {}),
309
368
  ]);
310
369
  for (const k of keys) {
311
- const targetPath = k === leafKey || keys.size === 1 && (k === leafKey || k === '')
312
- ? check.setting_path
313
- : parentPath
314
- ? `${parentPath}.${k}`
315
- : k;
370
+ const targetPath = resolveOpsTargetPath(check.setting_path, k);
316
371
  if (set && Object.prototype.hasOwnProperty.call(set, k)) {
317
372
  setByPath(configJson, targetPath, set[k]);
318
373
  continue;
@@ -431,7 +486,11 @@ const TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND = 'REPO_ROOT=$(git rev-pars
431
486
  'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
432
487
  'OPTIMUS_DEFERRED_LOG="${HOME}/opt-ai-sec/management/deferred_vscdb_restart.log" && mkdir -p "$(dirname "$OPTIMUS_DEFERRED_LOG")" && export OPTIMUS_DEFERRED_LOG && ' +
433
488
  "nohup bash -c 'exec >>\"\$OPTIMUS_DEFERRED_LOG\" 2>&1; echo deferred_restart:begin ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ) REPO_ROOT=\"\$REPO_ROOT\" CURSOR_PROJECT=\"\$CURSOR_PROJECT\"; sleep 2; STATIC_DEV_ENV=\"\$HOME/opt-ai-sec/management/optimus_dev.env\"; ENV_LABEL=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then ENV_LABEL=\$(grep -E \"^(environment|ENVIRONMENT|OPTIMUS_ENVIRONMENT)=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; if [ -z \"\$ENV_LABEL\" ] && [ -n \"\${OPTIMUS_ENVIRONMENT:-}\" ]; then ENV_LABEL=\$(printf \"%s\" \"\$OPTIMUS_ENVIRONMENT\" | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; [ -z \"\$ENV_LABEL\" ] && ENV_LABEL=\"production\"; NPX_BASE=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then _nb=\$(grep -E \"^npx=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | xargs); [ -n \"\$_nb\" ] && [ -d \"\$_nb\" ] && NPX_BASE=\"\$_nb\"; fi; p=\"\${npm_config_prefix:-\${NPM_CONFIG_PREFIX:-}}\"; gp=\"\${npm_config_global_prefix:-}\"; gc=\"\${npm_config_globalconfig:-}\"; if [[ \"\$p\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gp\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gc\" == *\"/Applications/Cursor.app/\"* ]]; then unset npm_config_prefix NPM_CONFIG_PREFIX npm_config_global_prefix npm_config_globalconfig npm_node_execpath 2>/dev/null; fi; APPLY_EC=0; if [ -f \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_dev_npx_packages; node \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif [ \"\$ENV_LABEL\" = \"development\" ] && [ -n \"\$NPX_BASE\" ] && [ -f \"\$NPX_BASE/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_optimus_npx_base; node \"\$NPX_BASE/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif command -v npx >/dev/null 2>&1; then echo deferred_restart:apply_via_npx env=\"\$ENV_LABEL\"; cd \"\$REPO_ROOT\" || true; if [ \"\$ENV_LABEL\" = \"staging\" ]; then npx --yes --package=log-llm-config-staging@latest apply-deferred-vscdb-staging; APPLY_EC=\$?; else npx --yes --package=log-llm-config@latest apply-deferred-vscdb; APPLY_EC=\$?; fi; else echo deferred_restart:no_npx; APPLY_EC=127; fi; echo deferred_restart:apply_exit=\$APPLY_EC; if [ \$APPLY_EC -ne 0 ]; then echo deferred_restart:APPLY_FAILED_see_messages_above; fi; echo deferred_restart:open_cursor; env -u npm_config_package -u npm_lifecycle_event -u npm_lifecycle_script -u npm_config_local_prefix open -a Cursor \"\$CURSOR_PROJECT\"; echo deferred_restart:open_exit=\$?; echo deferred_restart:end ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ)' >/dev/null 2>&1 & killall -9 Cursor";
434
- const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = 'CURSOR_PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export CURSOR_PROJECT && nohup bash -c \'sleep 2 && open -a Cursor "$CURSOR_PROJECT"\' >/dev/null 2>&1 & killall -9 Cursor';
489
+ /** Legacy manifests; hooks may run with cwd under `.cursor` where `pwd` is wrong. */
490
+ const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY = 'CURSOR_PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export CURSOR_PROJECT && nohup bash -c \'sleep 2 && open -a Cursor "$CURSOR_PROJECT"\' >/dev/null 2>&1 & killall -9 Cursor';
491
+ const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT && ' +
492
+ 'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
493
+ "nohup bash -c 'sleep 2 && open -a Cursor \"$CURSOR_PROJECT\"' >/dev/null 2>&1 & killall -9 Cursor";
435
494
  const TRUSTED_CLAUDE_RESTART_COMMAND = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
436
495
  export function isClaudeRestartCommand(cmd) {
437
496
  return cmd.trim() === TRUSTED_CLAUDE_RESTART_COMMAND;
@@ -439,7 +498,8 @@ export function isClaudeRestartCommand(cmd) {
439
498
  export function isCursorRestartCommand(cmd) {
440
499
  const t = cmd.trim();
441
500
  return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
442
- t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND);
501
+ t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
502
+ t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY);
443
503
  }
444
504
  /**
445
505
  * Autofix restart_command allowlist: manifest strings are attacker-controlled if JSON is tampered.
@@ -452,6 +512,7 @@ export function isTrustedRestartCommandForAutofix(cmd) {
452
512
  return false;
453
513
  return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
454
514
  t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
515
+ t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY ||
455
516
  t === TRUSTED_CLAUDE_RESTART_COMMAND);
456
517
  }
457
518
  /** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
@@ -927,6 +988,9 @@ export function buildDeferredCursorRestartCommand() {
927
988
  // (package bin) so published installs work without a local dev_npx_packages copy.
928
989
  return TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND;
929
990
  }
991
+ export function buildCursorJsonSettingsRestartCommand() {
992
+ return TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND;
993
+ }
930
994
  function sqliteRowGroupKey(dbPath, op) {
931
995
  return `${dbPath}|${op.table}|${op.key_column}|${op.value_column}|${op.target_key}`;
932
996
  }
@@ -7,6 +7,7 @@ import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
7
7
  import { getDeferredVscdbRestartLogPath } from './management_storage.js';
8
8
  import { normalizeAgentToken } from './compliance_check.js';
9
9
  import { isClaudeRestartCommand, isCursorRestartCommand, isTrustedRestartCommandForAutofix } from './remediation_sync.js';
10
+ import { resolveProjectRoot } from './workspace_repo.js';
10
11
  const FALLBACK_PATH = '/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin';
11
12
  /** Vars set by `npx --package=...` that break unrelated scoped `npx @scope/pkg` after Cursor reopen. */
12
13
  const NPM_RESTART_POLLUTION = [
@@ -72,11 +73,14 @@ export function executeTrustedRestartCommands(commands) {
72
73
  hookRunLog('execute_trusted_restarts: spawning trusted restart (see compliance_runner.log [RESTART])');
73
74
  appendComplianceRunnerLine('RESTART', 'spawn trusted_restart sh -c (non-deferred)');
74
75
  }
76
+ const projectRoot = resolveProjectRoot();
75
77
  const child = spawn('sh', ['-c', t], {
76
78
  detached: true,
77
79
  stdio: 'ignore',
80
+ cwd: projectRoot,
78
81
  env: envForRestartSpawn({
79
82
  ...process.env,
83
+ CURSOR_PROJECT_DIR: process.env.CURSOR_PROJECT_DIR || projectRoot,
80
84
  PATH: process.env.PATH && String(process.env.PATH).trim().length > 0
81
85
  ? process.env.PATH
82
86
  : FALLBACK_PATH,
@@ -0,0 +1,182 @@
1
+ import { readRemediationInstructionsFile } from './log_config_files/runtime/management_storage.js';
2
+ import { remediationFixSpec } from './log_config_files/runtime/remediation_sync.js';
3
+ export const MAX_PREVIEW_LINES = 5;
4
+ export const MAX_PREVIEW_LINE_CHARS = 72;
5
+ function truncateLine(text, max = MAX_PREVIEW_LINE_CHARS) {
6
+ const trimmed = text.trim();
7
+ if (trimmed.length <= max)
8
+ return trimmed;
9
+ return `${trimmed.slice(0, max - 1)}…`;
10
+ }
11
+ function formatScalar(value) {
12
+ if (value === null)
13
+ return 'null';
14
+ if (value === undefined)
15
+ return 'undefined';
16
+ if (typeof value === 'string')
17
+ return `"${value}"`;
18
+ if (typeof value === 'boolean' || typeof value === 'number')
19
+ return String(value);
20
+ if (Array.isArray(value)) {
21
+ if (value.length === 0)
22
+ return '[]';
23
+ const parts = value.slice(0, 2).map((entry) => formatScalar(entry));
24
+ if (value.length > 2)
25
+ return `${parts.join(', ')} …+${value.length - 2}`;
26
+ return parts.join(', ');
27
+ }
28
+ if (typeof value === 'object') {
29
+ const record = value;
30
+ if (typeof record.serverName === 'string')
31
+ return `"${record.serverName}"`;
32
+ const keys = Object.keys(record);
33
+ if (keys.length === 1 && keys[0])
34
+ return formatScalar(record[keys[0]]);
35
+ if (keys.length <= 3)
36
+ return `{${keys.join(', ')}}`;
37
+ return `{${keys.slice(0, 2).join(', ')}, …}`;
38
+ }
39
+ return String(value);
40
+ }
41
+ function formatListDeltaItems(items) {
42
+ if (items.length === 0)
43
+ return '—';
44
+ const shown = items.slice(0, 2).map((entry) => formatScalar(entry));
45
+ if (items.length > 2)
46
+ return `${shown.join(', ')} …+${items.length - 2}`;
47
+ return shown.join(', ');
48
+ }
49
+ function leafFromSettingPath(settingPath, opKey) {
50
+ const path = settingPath.trim();
51
+ if (!path)
52
+ return opKey;
53
+ if (path.endsWith(`.${opKey}`))
54
+ return opKey;
55
+ const last = path.split('.').pop();
56
+ return last && last.length > 0 ? last : opKey;
57
+ }
58
+ function linesFromOps(check) {
59
+ const lines = [];
60
+ const settingPath = check.setting_path ?? '';
61
+ const ops = check.ops;
62
+ if (!ops || typeof ops !== 'object')
63
+ return lines;
64
+ const setOps = ops.set;
65
+ if (setOps && typeof setOps === 'object') {
66
+ for (const [key, value] of Object.entries(setOps)) {
67
+ const leaf = Object.keys(setOps).length === 1 ? leafFromSettingPath(settingPath, key) : key;
68
+ lines.push(`Set ${leaf} → ${formatScalar(value)}`);
69
+ }
70
+ }
71
+ const addOps = ops.add;
72
+ if (addOps && typeof addOps === 'object') {
73
+ for (const [key, items] of Object.entries(addOps)) {
74
+ if (!Array.isArray(items))
75
+ continue;
76
+ lines.push(`Add to ${key}: ${formatListDeltaItems(items)}`);
77
+ }
78
+ }
79
+ const removeOps = ops.remove;
80
+ if (removeOps && typeof removeOps === 'object') {
81
+ for (const [key, items] of Object.entries(removeOps)) {
82
+ if (Array.isArray(items)) {
83
+ lines.push(`Remove from ${key}: ${formatListDeltaItems(items)}`);
84
+ continue;
85
+ }
86
+ if (items && typeof items === 'object') {
87
+ lines.push(`Remove from ${key}: ${formatListDeltaItems(Object.keys(items))}`);
88
+ }
89
+ }
90
+ }
91
+ return lines;
92
+ }
93
+ function linesFromSqliteOp(check) {
94
+ const sqliteOp = check.sqlite_op;
95
+ if (!sqliteOp || typeof sqliteOp !== 'object')
96
+ return [];
97
+ const lines = [];
98
+ const settingPath = check.setting_path ?? '';
99
+ const updates = sqliteOp.updates;
100
+ if (updates && typeof updates === 'object') {
101
+ for (const [key, value] of Object.entries(updates)) {
102
+ lines.push(`Set ${leafFromSettingPath(settingPath, key)} → ${formatScalar(value)}`);
103
+ }
104
+ }
105
+ const unionAdd = sqliteOp.array_union_add;
106
+ if (Array.isArray(unionAdd) && unionAdd.length > 0) {
107
+ const target = sqliteOp.target_key ?? 'list';
108
+ lines.push(`Add to ${target}: ${formatListDeltaItems(unionAdd)}`);
109
+ }
110
+ return lines;
111
+ }
112
+ function linesFromCheck(check) {
113
+ const fromOps = linesFromOps(check);
114
+ if (fromOps.length > 0)
115
+ return fromOps;
116
+ return linesFromSqliteOp(check);
117
+ }
118
+ function linesFromFixSpec(spec) {
119
+ if (!spec)
120
+ return [];
121
+ if (Array.isArray(spec.checks) && spec.checks.length > 0) {
122
+ return spec.checks.flatMap((check) => linesFromCheck(check));
123
+ }
124
+ if (spec.setting_path || spec.ops) {
125
+ return linesFromCheck({ setting_path: spec.setting_path, ops: spec.ops });
126
+ }
127
+ return [];
128
+ }
129
+ /** Basename for macOS dialog copy (vscdb paths shortened). */
130
+ export function previewFileLabel(configFilePath) {
131
+ if (!configFilePath?.trim())
132
+ return 'config file';
133
+ const normalized = configFilePath.replace(/\\/g, '/');
134
+ const hashIdx = normalized.indexOf('#');
135
+ const pathPart = hashIdx >= 0 ? normalized.slice(0, hashIdx) : normalized;
136
+ const fragment = hashIdx >= 0 ? normalized.slice(hashIdx + 1) : '';
137
+ const segments = pathPart.split('/').filter(Boolean);
138
+ const base = segments[segments.length - 1] ?? pathPart;
139
+ if (base.endsWith('.vscdb') || base === 'state.vscdb') {
140
+ if (fragment && fragment !== 'composerState') {
141
+ const short = fragment.length > 28
142
+ ? fragment.includes('/')
143
+ ? (fragment.split('/').pop() ?? fragment)
144
+ : fragment
145
+ : fragment;
146
+ return `state.vscdb (${short})`;
147
+ }
148
+ return 'state.vscdb';
149
+ }
150
+ return base || 'config file';
151
+ }
152
+ export function collectRemediationPreviewLines(appliedViolations, remediations, maxLines = MAX_PREVIEW_LINES) {
153
+ const byUuid = new Map(remediations.map((row) => [row.uuid, row]));
154
+ const fileLabels = new Set();
155
+ const allLines = [];
156
+ for (const violation of appliedViolations) {
157
+ const instruction = byUuid.get(violation.uuid);
158
+ const spec = instruction ? remediationFixSpec(instruction) : null;
159
+ fileLabels.add(previewFileLabel(instruction?.config_file_path ?? violation.config_file_path ?? ''));
160
+ allLines.push(...linesFromFixSpec(spec));
161
+ }
162
+ const lines = allLines.slice(0, maxLines);
163
+ const hiddenCount = Math.max(0, allLines.length - lines.length);
164
+ return { fileLabels: [...fileLabels], lines, hiddenCount };
165
+ }
166
+ export function formatRemediationChangePreviewSection(appliedViolations, remediations) {
167
+ const { fileLabels, lines, hiddenCount } = collectRemediationPreviewLines(appliedViolations, remediations);
168
+ if (lines.length === 0)
169
+ return '';
170
+ const filePart = fileLabels.length === 1
171
+ ? `Changes in ${fileLabels[0]}:`
172
+ : `Changes in ${fileLabels.slice(0, 2).join(', ')}${fileLabels.length > 2 ? ', …' : ''}:`;
173
+ const body = lines.map((line) => ` • ${truncateLine(line)}`).join('\n');
174
+ const tail = hiddenCount > 0
175
+ ? `\n … and ${hiddenCount} more change${hiddenCount === 1 ? '' : 's'}`
176
+ : '';
177
+ return `${filePart}\n${body}${tail}`;
178
+ }
179
+ export function formatRemediationChangePreviewForApplied(appliedViolations) {
180
+ const { remediations } = readRemediationInstructionsFile();
181
+ return formatRemediationChangePreviewSection(appliedViolations, remediations);
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config-staging",
3
- "version": "1.3.83",
3
+ "version": "1.3.84",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {