log-llm-config 1.3.20 → 1.3.22

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.
@@ -110,6 +110,12 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
110
110
  if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
111
111
  return { [itemKey]: { [field]: parsed !== 0 } };
112
112
  }
113
+ if (typeof parsed === 'string') {
114
+ const lower = parsed.trim().toLowerCase();
115
+ return {
116
+ [itemKey]: { [field]: lower === 'true' || lower === '1' || lower === 'yes' },
117
+ };
118
+ }
113
119
  }
114
120
  return { [itemKey]: parsed };
115
121
  }
@@ -14,6 +14,7 @@ import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vsc
14
14
  import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
15
15
  import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
16
16
  import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
17
+ import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js';
17
18
  import { createSignature, canonicalizePayload } from '../sender/signing.js';
18
19
  const PROJECT_ROOT = process.cwd();
19
20
  async function collectAllConfigFiles(endpointBase) {
@@ -73,7 +74,7 @@ async function sendAllConfigFiles(configFiles, hardwareUuid, authKey) {
73
74
  const hookTypeRaw = (process.env.OPTIMUS_HOOK_TYPE || 'claude').toLowerCase();
74
75
  const hookType = hookTypeRaw === 'cursor' ? 'cursor' : 'claude';
75
76
  const workspaceRepo = process.env.OPTIMUS_WORKSPACE_REPO || process.env.GITHUB_REPOSITORY || '';
76
- const manifest = configFiles.map((c) => c.file_path);
77
+ const manifest = configFiles.map((c) => canonicalCursorUserStateVscdbPath(c.file_path));
77
78
  const hookRequestId = await sendHookRequestCreate(hardwareUuid, authKey, hookType, workspaceRepo);
78
79
  hookRunLog(`hook-request id=${hookRequestId ?? 'none'}`);
79
80
  const ingestSessionId = await sendIngestSessionStart(hardwareUuid, authKey);
@@ -9,6 +9,46 @@ export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state
9
9
  function normalizeSlashes(p) {
10
10
  return p.trim().replace(/\\/g, '/');
11
11
  }
12
+ /**
13
+ * Canonical `file_path` for Cursor User globalStorage state.vscdb uploads, matching
14
+ * `optimus_security.endpoint.log_config_file.handler._canonical_cursor_user_state_vscdb_path`.
15
+ * Always use before sending log-config-file(s) so findings dedupe regardless of absolute vs portable input.
16
+ */
17
+ export function canonicalCursorUserStateVscdbPath(filePath) {
18
+ const s = normalizeSlashes(filePath);
19
+ const lower = s.toLowerCase();
20
+ const needle = 'cursor/user/globalstorage/state.vscdb';
21
+ const idx = lower.indexOf(needle);
22
+ if (idx >= 0) {
23
+ const tail = s.slice(idx + needle.length);
24
+ if (tail.startsWith('#')) {
25
+ return `${PORTABLE_CURSOR_USER_STATE_VSCDB}${tail}`;
26
+ }
27
+ if (tail === '') {
28
+ return PORTABLE_CURSOR_USER_STATE_VSCDB;
29
+ }
30
+ return filePath.trim();
31
+ }
32
+ const vscdb = 'state.vscdb';
33
+ const vlen = vscdb.length;
34
+ const slashKey = '/' + vscdb;
35
+ let start;
36
+ const pos = lower.lastIndexOf(slashKey);
37
+ if (pos >= 0) {
38
+ start = pos + 1;
39
+ }
40
+ else if (lower.startsWith(vscdb)) {
41
+ start = 0;
42
+ }
43
+ else {
44
+ return filePath.trim();
45
+ }
46
+ const tail = s.slice(start + vlen);
47
+ if (tail.startsWith('#') || tail === '') {
48
+ return `${PORTABLE_CURSOR_USER_STATE_VSCDB}${tail}`;
49
+ }
50
+ return filePath.trim();
51
+ }
12
52
  function cursorStateVscdbAbsoluteBasePaths() {
13
53
  const h = homedir();
14
54
  if (process.platform === 'darwin') {
@@ -476,25 +476,49 @@ function mergeSqliteOpIntoJson(currentJson, sqliteOp) {
476
476
  const parentParts = parts.slice(0, -1);
477
477
  let node = currentJson;
478
478
  for (const p of parentParts) {
479
- if (node == null || typeof node !== 'object')
479
+ if (node == null || typeof node !== 'object' || Array.isArray(node))
480
480
  return;
481
- node = node[p];
481
+ const rec = node;
482
+ const child = rec[p];
483
+ if (child == null) {
484
+ rec[p] = {};
485
+ node = rec[p];
486
+ }
487
+ else if (typeof child === 'object' && !Array.isArray(child)) {
488
+ node = child;
489
+ }
490
+ else {
491
+ return;
492
+ }
482
493
  }
483
494
  if (node == null || typeof node !== 'object' || Array.isArray(node))
484
495
  return;
485
496
  const container = node;
486
- const arr = container[arrayKey];
487
- if (!Array.isArray(arr))
497
+ const existing = container[arrayKey];
498
+ let arr;
499
+ if (Array.isArray(existing)) {
500
+ arr = existing;
501
+ }
502
+ else if (existing == null) {
503
+ arr = [];
504
+ container[arrayKey] = arr;
505
+ }
506
+ else {
488
507
  return;
508
+ }
489
509
  const idx = arr.findIndex((item) => {
490
510
  if (item === null || typeof item !== 'object')
491
511
  return false;
492
512
  const o = item;
493
513
  return Object.entries(where).every(([k, v]) => o[k] === v);
494
514
  });
495
- if (idx < 0)
515
+ if (idx >= 0) {
516
+ Object.assign(arr[idx], sqliteOp.updates);
496
517
  return;
497
- Object.assign(arr[idx], sqliteOp.updates);
518
+ }
519
+ const newItem = { ...where };
520
+ Object.assign(newItem, sqliteOp.updates);
521
+ arr.push(newItem);
498
522
  return;
499
523
  }
500
524
  mergeJsonAtSqlitePath(currentJson, sqliteOp.json_path, sqliteOp.updates);
@@ -750,10 +774,17 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
750
774
  const line = `sqlite_update: database not found at ${dbPath}`;
751
775
  hookRunLog(line);
752
776
  complianceRunnerDiag(line);
753
- return false;
777
+ return {
778
+ ok: false,
779
+ reason: `state.vscdb not on disk at ${dbPath} (Cursor may not have created globalStorage yet — open Cursor once).`,
780
+ };
781
+ }
782
+ if (!assertSqlite3Available()) {
783
+ return {
784
+ ok: false,
785
+ reason: 'sqlite3 CLI not found in PATH (IDE hooks often inherit a minimal PATH; install Xcode CLT / sqlite3 or fix PATH).',
786
+ };
754
787
  }
755
- if (!assertSqlite3Available())
756
- return false;
757
788
  const resolvedOps = sqliteOps.map((op) => resolveCursorComposerSqliteOp(dbPath, op));
758
789
  const groups = new Map();
759
790
  for (const op of resolvedOps) {
@@ -771,7 +802,7 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
771
802
  const line = `sqlite_update: rejected unsafe or empty target_key for deferred queue (refusing to write state.vscdb)`;
772
803
  hookRunLog(line);
773
804
  complianceRunnerDiag(`${line} target_key_preview=${first.target_key.slice(0, 80)}`);
774
- return false;
805
+ return { ok: false, reason: 'unsafe or empty ItemTable target_key (see hook_log).' };
775
806
  }
776
807
  complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
777
808
  let currentJson = {};
@@ -786,7 +817,10 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
786
817
  const line = `sqlite_update: error querying database: ${e instanceof Error ? e.message : String(e)}`;
787
818
  hookRunLog(line);
788
819
  complianceRunnerDiag(line);
789
- return false;
820
+ return {
821
+ ok: false,
822
+ reason: `sqlite read/parse failed: ${e instanceof Error ? e.message : String(e)}`,
823
+ };
790
824
  }
791
825
  for (const op of ops) {
792
826
  mergeSqliteOpIntoJson(currentJson, op);
@@ -797,7 +831,10 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
797
831
  const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row)`;
798
832
  hookRunLog(line);
799
833
  complianceRunnerDiag(line);
800
- return false;
834
+ return {
835
+ ok: false,
836
+ reason: 'merge produced empty JSON (no-op merge — often missing modes4/agent structure or outdated log-llm-config; publish latest package and retry).',
837
+ };
801
838
  }
802
839
  queueDeferredVscdbItem({
803
840
  dbPath,
@@ -811,13 +848,16 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
811
848
  hookRunLog(okLine);
812
849
  complianceRunnerDiag(okLine);
813
850
  }
814
- return true;
851
+ return { ok: true };
815
852
  }
816
853
  catch (err) {
817
854
  const line = `sqlite_update: unexpected error: ${err instanceof Error ? err.message : String(err)}`;
818
855
  hookRunLog(line);
819
856
  complianceRunnerDiag(line);
820
- return false;
857
+ return {
858
+ ok: false,
859
+ reason: `unexpected: ${err instanceof Error ? err.message : String(err)}`,
860
+ };
821
861
  }
822
862
  }
823
863
  /**
@@ -932,11 +972,11 @@ export function enforceRemediation(instruction) {
932
972
  const postApplyUpload = ft && inst.config_file_path.includes('#')
933
973
  ? { file_path: inst.config_file_path, file_type: ft }
934
974
  : undefined;
935
- const ok = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
936
- if (!ok) {
937
- return fail('deferred state.vscdb queue failed (database missing, sqlite3 unavailable, or read/merge error — see sqlite_update lines above in hook_log)', { config_file_path: inst.config_file_path });
975
+ const q = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
976
+ if (!q.ok) {
977
+ return fail(`deferred state.vscdb queue failed ${q.reason} (see sqlite_update lines in ~/opt-ai-sec/management/hook_log.txt)`, { config_file_path: inst.config_file_path });
938
978
  }
939
- return { ok, deferredSqlite: ok };
979
+ return { ok: true, deferredSqlite: true };
940
980
  }
941
981
  let allSuccess = true;
942
982
  for (const check of sqliteOps) {
@@ -2,6 +2,7 @@ import { postStartupPayload, patchPayload } from '../../endpoint_client/index.js
2
2
  import { createSignature } from './signing.js';
3
3
  import { loadEndpointBase } from './endpoint_config.js';
4
4
  import { hookRunLog } from '../runtime/hook_logger.js';
5
+ import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
5
6
  /** Chunk size per batch request. */
6
7
  export const BATCH_CHUNK_SIZE = 20;
7
8
  const MAX_BATCH_SIZE_BYTES = 500 * 1024; // 500KB
@@ -51,7 +52,11 @@ function buildBatchChunks(configFiles, basePayloadSize) {
51
52
  return chunks;
52
53
  }
53
54
  function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
54
- const config_files = chunk.map((c) => ({ file_type: c.file_type, file_path: c.file_path, raw_content: c.raw_content }));
55
+ const config_files = chunk.map((c) => ({
56
+ file_type: c.file_type,
57
+ file_path: canonicalCursorUserStateVscdbPath(c.file_path),
58
+ raw_content: c.raw_content,
59
+ }));
55
60
  const payload = { hardware_uuid: hardwareUuid, metadata, config_files };
56
61
  if (hookRequestId != null)
57
62
  payload.hook_request_id = hookRequestId;
@@ -153,18 +158,19 @@ async function sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId) {
153
158
  async function sendConfigFile(configFile, hardwareUuid, authKey) {
154
159
  const endpoint = loadEndpointBase();
155
160
  const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
156
- const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: configFile.file_path, raw_content: configFile.raw_content };
161
+ const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
162
+ const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
157
163
  const signature = createSignature(payload, authKey.key);
158
164
  const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', repo_identifier: process.env.GITHUB_REPOSITORY || process.env.GH_REPOSITORY || '' } };
159
165
  try {
160
166
  const response = await postStartupPayload(apiUrl, body);
161
167
  if (response.status !== 'accepted') {
162
- hookRunLog(`sendConfigFile: rejected status=${response.status} error=${response.error ?? ''} path=${configFile.file_path}`);
168
+ hookRunLog(`sendConfigFile: rejected status=${response.status} error=${response.error ?? ''} path=${uploadPath}`);
163
169
  }
164
170
  return response.status === 'accepted';
165
171
  }
166
172
  catch (err) {
167
- hookRunLog(`sendConfigFile: request failed path=${configFile.file_path} err=${err instanceof Error ? err.message : String(err)}`);
173
+ hookRunLog(`sendConfigFile: request failed path=${uploadPath} err=${err instanceof Error ? err.message : String(err)}`);
168
174
  return false;
169
175
  }
170
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {