engrm 0.4.43 → 0.4.44

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.
@@ -1058,6 +1058,7 @@ function ensureObservationTypes(db) {
1058
1058
  DROP TABLE observations;
1059
1059
  ALTER TABLE observations_repair RENAME TO observations;
1060
1060
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
1061
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
1061
1062
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
1062
1063
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
1063
1064
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1338,6 +1339,7 @@ class MemDatabase {
1338
1339
  this.db = openDatabase(dbPath);
1339
1340
  this.db.exec("PRAGMA journal_mode = WAL");
1340
1341
  this.db.exec("PRAGMA foreign_keys = ON");
1342
+ this.db.exec("PRAGMA busy_timeout = 5000");
1341
1343
  this.vecAvailable = this.loadVecExtension();
1342
1344
  runMigrations(this.db);
1343
1345
  ensureObservationTypes(this.db);
@@ -1359,8 +1361,16 @@ class MemDatabase {
1359
1361
  this.db.close();
1360
1362
  }
1361
1363
  upsertProject(project) {
1364
+ const canonicalId = project.canonical_id?.trim();
1365
+ const name = project.name?.trim();
1366
+ if (!canonicalId) {
1367
+ throw new Error("Project canonical_id is required");
1368
+ }
1369
+ if (!name) {
1370
+ throw new Error("Project name is required");
1371
+ }
1362
1372
  const now = Math.floor(Date.now() / 1000);
1363
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1373
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1364
1374
  if (existing) {
1365
1375
  this.db.query(`UPDATE projects SET
1366
1376
  local_path = COALESCE(?, local_path),
@@ -1375,7 +1385,7 @@ class MemDatabase {
1375
1385
  };
1376
1386
  }
1377
1387
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1378
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1388
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1379
1389
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1380
1390
  }
1381
1391
  getProjectByCanonicalId(canonicalId) {
@@ -2525,10 +2535,11 @@ function readProjectConfigFile(directory) {
2525
2535
  }
2526
2536
  }
2527
2537
  function detectProject(directory) {
2528
- const remoteUrl = getGitRemoteUrl(directory);
2538
+ const resolvedDirectory = resolve(directory);
2539
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
2529
2540
  if (remoteUrl) {
2530
2541
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
2531
- const repoRoot = getGitTopLevel(directory) ?? directory;
2542
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
2532
2543
  return {
2533
2544
  canonical_id: canonicalId,
2534
2545
  name: projectNameFromCanonicalId(canonicalId),
@@ -2536,21 +2547,22 @@ function detectProject(directory) {
2536
2547
  local_path: repoRoot
2537
2548
  };
2538
2549
  }
2539
- const configFile = readProjectConfigFile(directory);
2550
+ const configFile = readProjectConfigFile(resolvedDirectory);
2540
2551
  if (configFile) {
2541
2552
  return {
2542
2553
  canonical_id: configFile.project_id,
2543
2554
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
2544
2555
  remote_url: null,
2545
- local_path: directory
2556
+ local_path: resolvedDirectory
2546
2557
  };
2547
2558
  }
2548
- const dirName = basename(directory);
2559
+ const dirName = basename(resolvedDirectory);
2560
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
2549
2561
  return {
2550
- canonical_id: `local/${dirName}`,
2551
- name: dirName,
2562
+ canonical_id: `local/${safeDirName}`,
2563
+ name: safeDirName,
2552
2564
  remote_url: null,
2553
- local_path: directory
2565
+ local_path: resolvedDirectory
2554
2566
  };
2555
2567
  }
2556
2568
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -3434,9 +3446,21 @@ function incrementObserverSaveCount(sessionId) {
3434
3446
  // src/capture/recall.ts
3435
3447
  var VEC_DISTANCE_THRESHOLD = 0.25;
3436
3448
  function extractErrorSignature(output) {
3437
- if (!output || output.length < 10)
3449
+ if (output === null || output === undefined)
3438
3450
  return null;
3439
- const lines = output.split(`
3451
+ let text;
3452
+ if (typeof output === "string") {
3453
+ text = output;
3454
+ } else {
3455
+ try {
3456
+ text = JSON.stringify(output);
3457
+ } catch {
3458
+ text = String(output);
3459
+ }
3460
+ }
3461
+ if (!text || text.length < 10)
3462
+ return null;
3463
+ const lines = text.split(`
3440
3464
  `);
3441
3465
  for (let i = lines.length - 1;i >= 0; i--) {
3442
3466
  const line = lines[i].trim();
@@ -3766,7 +3790,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
3766
3790
  return null;
3767
3791
  }
3768
3792
  function compactLine(value) {
3769
- const trimmed = value?.replace(/\s+/g, " ").trim();
3793
+ if (value === null || value === undefined)
3794
+ return null;
3795
+ let text;
3796
+ if (typeof value === "string") {
3797
+ text = value;
3798
+ } else {
3799
+ try {
3800
+ text = JSON.stringify(value);
3801
+ } catch {
3802
+ text = String(value);
3803
+ }
3804
+ }
3805
+ const trimmed = text.replace(/\s+/g, " ").trim();
3770
3806
  if (!trimmed)
3771
3807
  return null;
3772
3808
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -4507,7 +4543,19 @@ function parseJsonArray3(value) {
4507
4543
  }
4508
4544
  }
4509
4545
  function compactLine2(value) {
4510
- const trimmed = value?.replace(/\s+/g, " ").trim();
4546
+ if (value === null || value === undefined)
4547
+ return null;
4548
+ let text;
4549
+ if (typeof value === "string") {
4550
+ text = value;
4551
+ } else {
4552
+ try {
4553
+ text = JSON.stringify(value);
4554
+ } catch {
4555
+ text = String(value);
4556
+ }
4557
+ }
4558
+ const trimmed = text.replace(/\s+/g, " ").trim();
4511
4559
  if (!trimmed)
4512
4560
  return null;
4513
4561
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -4539,7 +4587,7 @@ async function main() {
4539
4587
  db.setSyncState("hook_post_tool_last_payload_preview", truncatePreview(raw, 400) ?? "parsed");
4540
4588
  try {
4541
4589
  if (event.session_id) {
4542
- persistRawToolChronology(event, config.user_id, config.device_id);
4590
+ persistRawToolChronology(db, event, config.user_id, config.device_id);
4543
4591
  await syncTranscriptChat(db, config, event.session_id, event.cwd);
4544
4592
  }
4545
4593
  const textToScan = extractScanText(event);
@@ -4645,24 +4693,22 @@ async function main() {
4645
4693
  db.close();
4646
4694
  }
4647
4695
  }
4648
- function persistRawToolChronology(event, userId, deviceId) {
4649
- const rawDb = new MemDatabase(getDbPath());
4696
+ function persistRawToolChronology(rawDb, event, userId, deviceId) {
4697
+ const detected = detectProjectForEvent(event);
4698
+ const project = rawDb.upsertProject({
4699
+ canonical_id: detected.canonical_id,
4700
+ name: detected.name,
4701
+ local_path: detected.local_path,
4702
+ remote_url: detected.remote_url ?? null
4703
+ });
4704
+ rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
4705
+ const metricsIncrement = {
4706
+ toolCalls: 1
4707
+ };
4708
+ if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
4709
+ metricsIncrement.files = 1;
4710
+ }
4650
4711
  try {
4651
- const detected = detectProjectForEvent(event);
4652
- const project = rawDb.upsertProject({
4653
- canonical_id: detected.canonical_id,
4654
- name: detected.name,
4655
- local_path: detected.local_path,
4656
- remote_url: detected.remote_url ?? null
4657
- });
4658
- rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
4659
- const metricsIncrement = {
4660
- toolCalls: 1
4661
- };
4662
- if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
4663
- metricsIncrement.files = 1;
4664
- }
4665
- rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
4666
4712
  rawDb.insertToolEvent({
4667
4713
  session_id: event.session_id,
4668
4714
  project_id: project.id,
@@ -4675,8 +4721,13 @@ function persistRawToolChronology(event, userId, deviceId) {
4675
4721
  device_id: deviceId,
4676
4722
  agent: "claude-code"
4677
4723
  });
4678
- } finally {
4679
- rawDb.close();
4724
+ rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
4725
+ rawDb.setSyncState("hook_post_tool_last_store_status", "stored");
4726
+ rawDb.setSyncState("hook_post_tool_last_store_error", "");
4727
+ } catch (error) {
4728
+ rawDb.setSyncState("hook_post_tool_last_store_status", "store-error");
4729
+ rawDb.setSyncState("hook_post_tool_last_store_error", truncatePreview(error instanceof Error ? error.message : String(error), 400) ?? "unknown");
4730
+ throw error;
4680
4731
  }
4681
4732
  }
4682
4733
  async function withTimeout(promise, timeoutMs) {
@@ -4761,9 +4812,21 @@ function safeSerializeToolInput(toolInput) {
4761
4812
  }
4762
4813
  }
4763
4814
  function truncatePreview(value, maxLen) {
4764
- if (!value)
4815
+ if (value === null || value === undefined)
4816
+ return null;
4817
+ let text;
4818
+ if (typeof value === "string") {
4819
+ text = value;
4820
+ } else {
4821
+ try {
4822
+ text = JSON.stringify(value);
4823
+ } catch {
4824
+ text = String(value);
4825
+ }
4826
+ }
4827
+ if (!text)
4765
4828
  return null;
4766
- const normalized = value.replace(/\s+/g, " ").trim();
4829
+ const normalized = text.replace(/\s+/g, " ").trim();
4767
4830
  if (!normalized)
4768
4831
  return null;
4769
4832
  if (normalized.length <= maxLen)
@@ -852,6 +852,7 @@ function ensureObservationTypes(db) {
852
852
  DROP TABLE observations;
853
853
  ALTER TABLE observations_repair RENAME TO observations;
854
854
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
855
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
855
856
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
856
857
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
857
858
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1132,6 +1133,7 @@ class MemDatabase {
1132
1133
  this.db = openDatabase(dbPath);
1133
1134
  this.db.exec("PRAGMA journal_mode = WAL");
1134
1135
  this.db.exec("PRAGMA foreign_keys = ON");
1136
+ this.db.exec("PRAGMA busy_timeout = 5000");
1135
1137
  this.vecAvailable = this.loadVecExtension();
1136
1138
  runMigrations(this.db);
1137
1139
  ensureObservationTypes(this.db);
@@ -1153,8 +1155,16 @@ class MemDatabase {
1153
1155
  this.db.close();
1154
1156
  }
1155
1157
  upsertProject(project) {
1158
+ const canonicalId = project.canonical_id?.trim();
1159
+ const name = project.name?.trim();
1160
+ if (!canonicalId) {
1161
+ throw new Error("Project canonical_id is required");
1162
+ }
1163
+ if (!name) {
1164
+ throw new Error("Project name is required");
1165
+ }
1156
1166
  const now = Math.floor(Date.now() / 1000);
1157
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1167
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1158
1168
  if (existing) {
1159
1169
  this.db.query(`UPDATE projects SET
1160
1170
  local_path = COALESCE(?, local_path),
@@ -1169,7 +1179,7 @@ class MemDatabase {
1169
1179
  };
1170
1180
  }
1171
1181
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1172
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1182
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1173
1183
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1174
1184
  }
1175
1185
  getProjectByCanonicalId(canonicalId) {
@@ -1937,10 +1947,11 @@ function readProjectConfigFile(directory) {
1937
1947
  }
1938
1948
  }
1939
1949
  function detectProject(directory) {
1940
- const remoteUrl = getGitRemoteUrl(directory);
1950
+ const resolvedDirectory = resolve(directory);
1951
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
1941
1952
  if (remoteUrl) {
1942
1953
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
1943
- const repoRoot = getGitTopLevel(directory) ?? directory;
1954
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
1944
1955
  return {
1945
1956
  canonical_id: canonicalId,
1946
1957
  name: projectNameFromCanonicalId(canonicalId),
@@ -1948,21 +1959,22 @@ function detectProject(directory) {
1948
1959
  local_path: repoRoot
1949
1960
  };
1950
1961
  }
1951
- const configFile = readProjectConfigFile(directory);
1962
+ const configFile = readProjectConfigFile(resolvedDirectory);
1952
1963
  if (configFile) {
1953
1964
  return {
1954
1965
  canonical_id: configFile.project_id,
1955
1966
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
1956
1967
  remote_url: null,
1957
- local_path: directory
1968
+ local_path: resolvedDirectory
1958
1969
  };
1959
1970
  }
1960
- const dirName = basename(directory);
1971
+ const dirName = basename(resolvedDirectory);
1972
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
1961
1973
  return {
1962
- canonical_id: `local/${dirName}`,
1963
- name: dirName,
1974
+ canonical_id: `local/${safeDirName}`,
1975
+ name: safeDirName,
1964
1976
  remote_url: null,
1965
- local_path: directory
1977
+ local_path: resolvedDirectory
1966
1978
  };
1967
1979
  }
1968
1980
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -3387,7 +3399,19 @@ function parseJsonArray2(value) {
3387
3399
  }
3388
3400
  }
3389
3401
  function compactLine(value) {
3390
- const trimmed = value?.replace(/\s+/g, " ").trim();
3402
+ if (value === null || value === undefined)
3403
+ return null;
3404
+ let text;
3405
+ if (typeof value === "string") {
3406
+ text = value;
3407
+ } else {
3408
+ try {
3409
+ text = JSON.stringify(value);
3410
+ } catch {
3411
+ text = String(value);
3412
+ }
3413
+ }
3414
+ const trimmed = text.replace(/\s+/g, " ").trim();
3391
3415
  if (!trimmed)
3392
3416
  return null;
3393
3417
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -928,6 +928,7 @@ function ensureObservationTypes(db) {
928
928
  DROP TABLE observations;
929
929
  ALTER TABLE observations_repair RENAME TO observations;
930
930
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
931
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
931
932
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
932
933
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
933
934
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1208,6 +1209,7 @@ class MemDatabase {
1208
1209
  this.db = openDatabase(dbPath);
1209
1210
  this.db.exec("PRAGMA journal_mode = WAL");
1210
1211
  this.db.exec("PRAGMA foreign_keys = ON");
1212
+ this.db.exec("PRAGMA busy_timeout = 5000");
1211
1213
  this.vecAvailable = this.loadVecExtension();
1212
1214
  runMigrations(this.db);
1213
1215
  ensureObservationTypes(this.db);
@@ -1229,8 +1231,16 @@ class MemDatabase {
1229
1231
  this.db.close();
1230
1232
  }
1231
1233
  upsertProject(project) {
1234
+ const canonicalId = project.canonical_id?.trim();
1235
+ const name = project.name?.trim();
1236
+ if (!canonicalId) {
1237
+ throw new Error("Project canonical_id is required");
1238
+ }
1239
+ if (!name) {
1240
+ throw new Error("Project name is required");
1241
+ }
1232
1242
  const now = Math.floor(Date.now() / 1000);
1233
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1243
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1234
1244
  if (existing) {
1235
1245
  this.db.query(`UPDATE projects SET
1236
1246
  local_path = COALESCE(?, local_path),
@@ -1245,7 +1255,7 @@ class MemDatabase {
1245
1255
  };
1246
1256
  }
1247
1257
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1248
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1258
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1249
1259
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1250
1260
  }
1251
1261
  getProjectByCanonicalId(canonicalId) {
@@ -2067,10 +2077,11 @@ function readProjectConfigFile(directory) {
2067
2077
  }
2068
2078
  }
2069
2079
  function detectProject(directory) {
2070
- const remoteUrl = getGitRemoteUrl(directory);
2080
+ const resolvedDirectory = resolve(directory);
2081
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
2071
2082
  if (remoteUrl) {
2072
2083
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
2073
- const repoRoot = getGitTopLevel(directory) ?? directory;
2084
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
2074
2085
  return {
2075
2086
  canonical_id: canonicalId,
2076
2087
  name: projectNameFromCanonicalId(canonicalId),
@@ -2078,21 +2089,22 @@ function detectProject(directory) {
2078
2089
  local_path: repoRoot
2079
2090
  };
2080
2091
  }
2081
- const configFile = readProjectConfigFile(directory);
2092
+ const configFile = readProjectConfigFile(resolvedDirectory);
2082
2093
  if (configFile) {
2083
2094
  return {
2084
2095
  canonical_id: configFile.project_id,
2085
2096
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
2086
2097
  remote_url: null,
2087
- local_path: directory
2098
+ local_path: resolvedDirectory
2088
2099
  };
2089
2100
  }
2090
- const dirName = basename(directory);
2101
+ const dirName = basename(resolvedDirectory);
2102
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
2091
2103
  return {
2092
- canonical_id: `local/${dirName}`,
2093
- name: dirName,
2104
+ canonical_id: `local/${safeDirName}`,
2105
+ name: safeDirName,
2094
2106
  remote_url: null,
2095
- local_path: directory
2107
+ local_path: resolvedDirectory
2096
2108
  };
2097
2109
  }
2098
2110
  function detectProjectForPath(filePath, fallbackCwd) {