log-llm-config 1.3.22 → 1.3.23

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.
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { execSync } from 'node:child_process';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
3
4
  import { readFileCollectionVscdbContract, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
4
5
  /**
5
6
  * ItemTable keys that store a bare JSON boolean; compliance paths use `${key}.${field}`.
@@ -11,10 +12,15 @@ export const CURSOR_SCALAR_ITEMTABLE_FIELDS = {
11
12
  'cursor/autoOpenLocalhostUrls': 'autoOpenLocalhostUrls',
12
13
  };
13
14
  function querySqlite(dbPath, key) {
15
+ const bin = resolveSqlite3Binary();
16
+ if (!bin)
17
+ throw new Error('sqlite3 not found');
14
18
  const safe = key.replace(/'/g, "''");
15
- return execSync(`sqlite3 "${dbPath}" "SELECT value FROM ItemTable WHERE key='${safe}'"`, {
19
+ const script = `.timeout 60000\nSELECT value FROM ItemTable WHERE key='${safe}';\n`;
20
+ return execFileSync(bin, ['-noheader', dbPath], {
21
+ input: script,
16
22
  encoding: 'utf8',
17
- stdio: ['ignore', 'pipe', 'pipe'],
23
+ stdio: ['pipe', 'pipe', 'pipe'],
18
24
  }).trim();
19
25
  }
20
26
  /**
@@ -73,7 +79,8 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
73
79
  try {
74
80
  if (!dbPath || !existsSync(dbPath))
75
81
  return null;
76
- execSync('which sqlite3', { stdio: 'ignore' });
82
+ if (!resolveSqlite3Binary())
83
+ return null;
77
84
  }
78
85
  catch {
79
86
  return null;
@@ -218,10 +225,7 @@ export function readVSCDBState(dbPath, readQueries, mergeFromComposerStateKeys)
218
225
  try {
219
226
  if (!dbPath || !existsSync(dbPath))
220
227
  return null;
221
- try {
222
- execSync('which sqlite3', { stdio: 'ignore' });
223
- }
224
- catch {
228
+ if (!resolveSqlite3Binary()) {
225
229
  console.warn('sqlite3 command not found; skipping vscdb reading');
226
230
  return null;
227
231
  }
@@ -52,7 +52,9 @@ export function canonicalCursorUserStateVscdbPath(filePath) {
52
52
  function cursorStateVscdbAbsoluteBasePaths() {
53
53
  const h = homedir();
54
54
  if (process.platform === 'darwin') {
55
- return [join(h, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
55
+ const support = join(h, 'Library', 'Application Support');
56
+ const variants = ['Cursor', 'Cursor - Insiders', 'Cursor Nightly', 'Cursor Next'];
57
+ return variants.map((name) => join(support, name, 'User', 'globalStorage', 'state.vscdb'));
56
58
  }
57
59
  if (process.platform === 'win32') {
58
60
  const appData = process.env.APPDATA;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
2
- import { dirname } from 'node:path';
2
+ import { delimiter, dirname } from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
5
5
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
@@ -12,6 +12,29 @@ import { CURSOR_SCALAR_ITEMTABLE_FIELDS, persistVscdbComposerContractFromPattern
12
12
  import { sendConfigFile } from '../sender/batch_sender.js';
13
13
  import { getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
14
14
  import { resolveRemediationConfigPath } from './remediation_config_path.js';
15
+ import { resolveSqlite3Binary } from './sqlite_binary.js';
16
+ /** Best-effort detail from execFileSync failures (stderr, exit code, errno). */
17
+ function formatNodeChildException(err) {
18
+ if (!(err instanceof Error)) {
19
+ const s = String(err);
20
+ return { short: s, long: s };
21
+ }
22
+ const e = err;
23
+ const bits = [e.message];
24
+ if (e.code)
25
+ bits.push(`errno_code=${e.code}`);
26
+ if (typeof e.status === 'number')
27
+ bits.push(`exit_status=${e.status}`);
28
+ let stderr = '';
29
+ if (e.stderr != null) {
30
+ stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr.toString('utf8');
31
+ const t = stderr.trim();
32
+ if (t)
33
+ bits.push(`stderr=${t.slice(0, 1200)}`);
34
+ }
35
+ const long = bits.join(' | ');
36
+ return { short: bits.slice(0, 2).join(' | '), long };
37
+ }
15
38
  function reactiveStorageItemKeyFromContract() {
16
39
  const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
17
40
  return typeof k === 'string' && k.trim() !== '' ? k.trim() : undefined;
@@ -309,16 +332,19 @@ function mergePostApplyUploadIntoPayload(payload, hint) {
309
332
  payload.post_apply_uploads.push({ file_path: hint.file_path, file_type: ft });
310
333
  }
311
334
  function assertSqlite3Available() {
312
- try {
313
- execFileSync('which', ['sqlite3'], { stdio: 'ignore' });
335
+ const bin = resolveSqlite3Binary();
336
+ if (bin) {
337
+ complianceRunnerDiag(`sqlite_update: using sqlite3 binary ${bin}`);
314
338
  return true;
315
339
  }
316
- catch {
317
- const line = 'sqlite_update: sqlite3 command not found';
318
- hookRunLog(line);
319
- complianceRunnerDiag(line);
320
- return false;
321
- }
340
+ const pathEnv = process.env.PATH ?? '';
341
+ const n = pathEnv ? pathEnv.split(delimiter).filter(Boolean).length : 0;
342
+ const line = 'sqlite_update: sqlite3 command not found';
343
+ hookRunLog(line);
344
+ hookRunLog(`sqlite_update: no sqlite3 resolved after checking env OPTIMUS_SQLITE3/SQLITE3_PATH, common paths, and PATH (${n} entries)`);
345
+ hookRunLog('sqlite_update: hint set OPTIMUS_SQLITE3 to the full path (e.g. /usr/bin/sqlite3 on macOS) if sqlite3 is outside PATH');
346
+ complianceRunnerDiag(`${line} PATH_entry_count=${n}`);
347
+ return false;
322
348
  }
323
349
  /**
324
350
  * Unquoted SQLite identifiers must be [A-Za-z_][A-Za-z0-9_]* so values read from
@@ -610,30 +636,68 @@ function sqliteSelectValueCell(dbPath, table, key_column, value_column, target_k
610
636
  if (!assertSafeSqliteIdentifiersForItemTable(table, key_column, value_column)) {
611
637
  return '';
612
638
  }
639
+ const sqlite3 = resolveSqlite3Binary();
640
+ if (!sqlite3) {
641
+ const line = 'sqlite_update: sqliteSelectValueCell called with no resolved sqlite3 binary (logic error: assertSqlite3Available should run first)';
642
+ hookRunLog(line);
643
+ complianceRunnerDiag(line);
644
+ return '';
645
+ }
613
646
  const safeName = target_key.replace(/'/g, "''");
614
647
  const script = `.timeout 60000\nSELECT ${value_column} FROM ${table} WHERE ${key_column}='${safeName}';\n`;
615
- return execFileSync('sqlite3', ['-noheader', dbPath], {
616
- input: script,
617
- encoding: 'utf8',
618
- stdio: ['pipe', 'pipe', 'pipe'],
619
- }).trim();
648
+ try {
649
+ return execFileSync(sqlite3, ['-noheader', dbPath], {
650
+ input: script,
651
+ encoding: 'utf8',
652
+ stdio: ['pipe', 'pipe', 'pipe'],
653
+ }).trim();
654
+ }
655
+ catch (err) {
656
+ const { short, long } = formatNodeChildException(err);
657
+ hookRunLog(`sqlite_update: SELECT failed binary=${sqlite3} db=${dbPath} table=${table} target_key=${target_key} ${long}`);
658
+ complianceRunnerDiag(`sqlite_update: SELECT failed target_key=${target_key} ${long.slice(0, 1800)}`);
659
+ throw new Error(`sqlite3 SELECT failed: ${short}`);
660
+ }
620
661
  }
621
662
  function sqliteExecWithTimeout(dbPath, sqlBody) {
663
+ const sqlite3 = resolveSqlite3Binary();
664
+ if (!sqlite3)
665
+ throw new Error('sqlite3 not found');
622
666
  const script = `.timeout 60000\n${sqlBody}\n`;
623
- execFileSync('sqlite3', [dbPath], {
624
- input: script,
625
- encoding: 'utf8',
626
- stdio: ['pipe', 'pipe', 'pipe'],
627
- });
667
+ try {
668
+ execFileSync(sqlite3, [dbPath], {
669
+ input: script,
670
+ encoding: 'utf8',
671
+ stdio: ['pipe', 'pipe', 'pipe'],
672
+ });
673
+ }
674
+ catch (err) {
675
+ const { long } = formatNodeChildException(err);
676
+ hookRunLog(`sqlite_update: UPDATE/exec failed binary=${sqlite3} db=${dbPath} ${long}`);
677
+ complianceRunnerDiag(`sqlite_update: UPDATE/exec failed ${long.slice(0, 1800)}`);
678
+ throw err;
679
+ }
628
680
  }
629
681
  /** Runs a single UPDATE (or other SQL) and returns sqlite `changes()` for the last statement. */
630
682
  function sqliteRunUpdateReturningChanges(dbPath, updateSql) {
683
+ const sqlite3 = resolveSqlite3Binary();
684
+ if (!sqlite3)
685
+ throw new Error('sqlite3 not found');
631
686
  const script = `.timeout 60000\n${updateSql}\nSELECT changes();\n`;
632
- const out = execFileSync('sqlite3', [dbPath], {
633
- input: script,
634
- encoding: 'utf8',
635
- stdio: ['pipe', 'pipe', 'pipe'],
636
- }).trim();
687
+ let out;
688
+ try {
689
+ out = execFileSync(sqlite3, [dbPath], {
690
+ input: script,
691
+ encoding: 'utf8',
692
+ stdio: ['pipe', 'pipe', 'pipe'],
693
+ }).trim();
694
+ }
695
+ catch (err) {
696
+ const { long } = formatNodeChildException(err);
697
+ hookRunLog(`sqlite_update: batch UPDATE/changes failed binary=${sqlite3} db=${dbPath} ${long}`);
698
+ complianceRunnerDiag(`sqlite_update: batch UPDATE failed ${long.slice(0, 1800)}`);
699
+ throw err;
700
+ }
637
701
  const lines = out.split(/\r?\n/).filter((l) => l.length > 0);
638
702
  const last = lines[lines.length - 1] ?? '0';
639
703
  return parseInt(last, 10) || 0;
@@ -770,9 +834,12 @@ function assertSafeDeferredItemTableKey(targetKey) {
770
834
  }
771
835
  function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
772
836
  const dbPath = configPath.split('#')[0];
837
+ hookRunLog(`sqlite_update: deferred_queue begin configPath=${configPath} dbPath=${dbPath} dbExists=${existsSync(dbPath)} sqliteOps=${sqliteOps.length}`);
838
+ complianceRunnerDiag(`sqlite_update: deferred_queue begin dbExists=${existsSync(dbPath)} ops=${sqliteOps.length}`);
773
839
  if (!existsSync(dbPath)) {
774
840
  const line = `sqlite_update: database not found at ${dbPath}`;
775
841
  hookRunLog(line);
842
+ hookRunLog(`sqlite_update: parent dir exists=${existsSync(dirname(dbPath))} (if false, wrong Cursor profile path or never launched Cursor)`);
776
843
  complianceRunnerDiag(line);
777
844
  return {
778
845
  ok: false,
@@ -782,7 +849,7 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
782
849
  if (!assertSqlite3Available()) {
783
850
  return {
784
851
  ok: false,
785
- reason: 'sqlite3 CLI not found in PATH (IDE hooks often inherit a minimal PATH; install Xcode CLT / sqlite3 or fix PATH).',
852
+ reason: 'sqlite3 CLI not found (IDE hooks often have a minimal PATH). On macOS try OPTIMUS_SQLITE3=/usr/bin/sqlite3 or install sqlite3 and fix PATH.',
786
853
  };
787
854
  }
788
855
  const resolvedOps = sqliteOps.map((op) => resolveCursorComposerSqliteOp(dbPath, op));
@@ -806,8 +873,10 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
806
873
  }
807
874
  complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
808
875
  let currentJson = {};
876
+ let rawSelectLength = 0;
809
877
  try {
810
878
  const result = sqliteSelectValueCell(dbPath, first.table, first.key_column, first.value_column, first.target_key);
879
+ rawSelectLength = result.length;
811
880
  if (result) {
812
881
  const parsed = JSON.parse(result);
813
882
  currentJson = coerceItemTableValueToObjectRoot(first.target_key, parsed);
@@ -828,12 +897,16 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
828
897
  repairComposerStateEmptySegmentBug(currentJson);
829
898
  const updatedJson = serializeItemTableValueForWrite(first.target_key, currentJson);
830
899
  if (updatedJson === '{}' && Object.keys(currentJson).length === 0) {
831
- const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row)`;
900
+ const opSummary = ops
901
+ .map((o) => `path=${o.json_path ?? ''} updates=${JSON.stringify(o.updates ?? {})}`)
902
+ .join(' || ')
903
+ .slice(0, 900);
904
+ const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row) target_key=${first.target_key} raw_cell_len=${rawSelectLength} ops=${opSummary}`;
832
905
  hookRunLog(line);
833
- complianceRunnerDiag(line);
906
+ complianceRunnerDiag(line.slice(0, 2000));
834
907
  return {
835
908
  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).',
909
+ reason: `merge produced empty JSON after ops (target_key=${first.target_key}, raw_cell_len=${rawSelectLength}). Check sqlite_update lines above for SELECT errors or bad sqlite_op merge.`,
837
910
  };
838
911
  }
839
912
  queueDeferredVscdbItem({
@@ -974,7 +1047,12 @@ export function enforceRemediation(instruction) {
974
1047
  : undefined;
975
1048
  const q = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
976
1049
  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 });
1050
+ const dbOnly = inst.config_file_path.split('#')[0];
1051
+ return fail(`deferred state.vscdb queue failed — ${q.reason} (see sqlite_update lines above in hook_log for SELECT/exec details)`, {
1052
+ config_file_path: inst.config_file_path,
1053
+ resolved_db_path: dbOnly,
1054
+ sqlite3_binary: resolveSqlite3Binary() ?? 'unresolved',
1055
+ });
978
1056
  }
979
1057
  return { ok: true, deferredSqlite: true };
980
1058
  }
@@ -0,0 +1,92 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ let cached;
5
+ /**
6
+ * Resolve the sqlite3 CLI for remediations and vscdb reads.
7
+ *
8
+ * Cursor/IDE-launched hooks often inherit a minimal PATH (no Homebrew), while macOS still ships
9
+ * `/usr/bin/sqlite3`. Prefer explicit paths before `which` / bare `sqlite3`.
10
+ *
11
+ * Override: `OPTIMUS_SQLITE3`, `SQLITE3_PATH`, or `SQLITE3_BIN` = absolute path to the binary.
12
+ */
13
+ export function resolveSqlite3Binary() {
14
+ if (cached !== undefined)
15
+ return cached;
16
+ for (const envName of ['OPTIMUS_SQLITE3', 'SQLITE3_PATH', 'SQLITE3_BIN']) {
17
+ const v = process.env[envName]?.trim();
18
+ if (v && existsSync(v)) {
19
+ cached = v;
20
+ return cached;
21
+ }
22
+ }
23
+ const candidates = [];
24
+ if (process.platform === 'darwin') {
25
+ candidates.push('/usr/bin/sqlite3', '/opt/homebrew/bin/sqlite3', '/usr/local/bin/sqlite3', '/opt/local/bin/sqlite3');
26
+ }
27
+ else if (process.platform === 'linux') {
28
+ candidates.push('/usr/bin/sqlite3', '/usr/local/bin/sqlite3');
29
+ }
30
+ else if (process.platform === 'win32') {
31
+ const pf = process.env.ProgramFiles;
32
+ if (pf) {
33
+ candidates.push(join(pf, 'Git', 'usr', 'bin', 'sqlite3.exe'));
34
+ }
35
+ }
36
+ for (const p of candidates) {
37
+ if (p && existsSync(p)) {
38
+ cached = p;
39
+ return cached;
40
+ }
41
+ }
42
+ try {
43
+ if (process.platform === 'win32') {
44
+ const out = execFileSync('where.exe', ['sqlite3'], {
45
+ encoding: 'utf8',
46
+ stdio: ['ignore', 'pipe', 'ignore'],
47
+ }).trim();
48
+ const first = out.split(/\r?\n/).find((line) => {
49
+ const t = line.trim();
50
+ return t.length > 0 && existsSync(t);
51
+ });
52
+ if (first) {
53
+ cached = first.trim();
54
+ return cached;
55
+ }
56
+ }
57
+ else {
58
+ for (const whichBin of ['which', '/usr/bin/which']) {
59
+ try {
60
+ const out = execFileSync(whichBin, ['sqlite3'], {
61
+ encoding: 'utf8',
62
+ stdio: ['ignore', 'pipe', 'ignore'],
63
+ }).trim();
64
+ const first = out.split(/\n/)[0]?.trim();
65
+ if (first && existsSync(first)) {
66
+ cached = first;
67
+ return cached;
68
+ }
69
+ }
70
+ catch {
71
+ /* try next */
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ /* fall through */
78
+ }
79
+ try {
80
+ execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
81
+ cached = 'sqlite3';
82
+ return cached;
83
+ }
84
+ catch {
85
+ cached = null;
86
+ return null;
87
+ }
88
+ }
89
+ /** Test-only: reset memoized path after env/path changes. */
90
+ export function clearSqlite3BinaryCacheForTests() {
91
+ cached = undefined;
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.3.22",
3
+ "version": "1.3.23",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {