engrm 0.4.7 → 0.4.9

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
@@ -18,10 +18,10 @@ var __export = (target, all) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/cli.ts
21
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, statSync } from "fs";
22
- import { hostname as hostname2, homedir as homedir3, networkInterfaces as networkInterfaces2 } from "os";
23
- import { dirname as dirname4, join as join6 } from "path";
24
- import { createHash as createHash2 } from "crypto";
21
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, statSync } from "fs";
22
+ import { hostname as hostname2, homedir as homedir4, networkInterfaces as networkInterfaces2 } from "os";
23
+ import { dirname as dirname5, join as join7 } from "path";
24
+ import { createHash as createHash3 } from "crypto";
25
25
  import { fileURLToPath as fileURLToPath4 } from "url";
26
26
 
27
27
  // src/config.ts
@@ -539,6 +539,64 @@ var MIGRATIONS = [
539
539
  );
540
540
  INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
541
541
  `
542
+ },
543
+ {
544
+ version: 9,
545
+ description: "Add first-class user prompt capture",
546
+ sql: `
547
+ CREATE TABLE IF NOT EXISTS user_prompts (
548
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
549
+ session_id TEXT NOT NULL,
550
+ project_id INTEGER REFERENCES projects(id),
551
+ prompt_number INTEGER NOT NULL,
552
+ prompt TEXT NOT NULL,
553
+ prompt_hash TEXT NOT NULL,
554
+ cwd TEXT,
555
+ user_id TEXT NOT NULL,
556
+ device_id TEXT NOT NULL,
557
+ agent TEXT DEFAULT 'claude-code',
558
+ created_at_epoch INTEGER NOT NULL,
559
+ UNIQUE(session_id, prompt_number)
560
+ );
561
+
562
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_session
563
+ ON user_prompts(session_id, prompt_number DESC);
564
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_project
565
+ ON user_prompts(project_id, created_at_epoch DESC);
566
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_created
567
+ ON user_prompts(created_at_epoch DESC);
568
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_hash
569
+ ON user_prompts(prompt_hash);
570
+ `
571
+ },
572
+ {
573
+ version: 10,
574
+ description: "Add first-class tool event chronology",
575
+ sql: `
576
+ CREATE TABLE IF NOT EXISTS tool_events (
577
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
578
+ session_id TEXT NOT NULL,
579
+ project_id INTEGER REFERENCES projects(id),
580
+ tool_name TEXT NOT NULL,
581
+ tool_input_json TEXT,
582
+ tool_response_preview TEXT,
583
+ file_path TEXT,
584
+ command TEXT,
585
+ user_id TEXT NOT NULL,
586
+ device_id TEXT NOT NULL,
587
+ agent TEXT DEFAULT 'claude-code',
588
+ created_at_epoch INTEGER NOT NULL
589
+ );
590
+
591
+ CREATE INDEX IF NOT EXISTS idx_tool_events_session
592
+ ON tool_events(session_id, created_at_epoch DESC, id DESC);
593
+ CREATE INDEX IF NOT EXISTS idx_tool_events_project
594
+ ON tool_events(project_id, created_at_epoch DESC, id DESC);
595
+ CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name
596
+ ON tool_events(tool_name, created_at_epoch DESC);
597
+ CREATE INDEX IF NOT EXISTS idx_tool_events_created
598
+ ON tool_events(created_at_epoch DESC, id DESC);
599
+ `
542
600
  }
543
601
  ];
544
602
  function isVecExtensionLoaded(db) {
@@ -620,9 +678,14 @@ function ensureObservationTypes(db) {
620
678
  }
621
679
  }
622
680
  }
681
+ function getSchemaVersion(db) {
682
+ const result = db.query("PRAGMA user_version").get();
683
+ return result.user_version;
684
+ }
623
685
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
624
686
 
625
687
  // src/storage/sqlite.ts
688
+ import { createHash as createHash2 } from "node:crypto";
626
689
  var IS_BUN = typeof globalThis.Bun !== "undefined";
627
690
  function openDatabase(dbPath) {
628
691
  if (IS_BUN) {
@@ -742,6 +805,15 @@ class MemDatabase {
742
805
  }
743
806
  return row;
744
807
  }
808
+ reassignObservationProject(observationId, projectId) {
809
+ const existing = this.getObservationById(observationId);
810
+ if (!existing)
811
+ return false;
812
+ if (existing.project_id === projectId)
813
+ return true;
814
+ this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
815
+ return true;
816
+ }
745
817
  getObservationById(id) {
746
818
  return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
747
819
  }
@@ -875,8 +947,13 @@ class MemDatabase {
875
947
  }
876
948
  upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
877
949
  const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
878
- if (existing)
950
+ if (existing) {
951
+ if (existing.project_id === null && projectId !== null) {
952
+ this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
953
+ return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
954
+ }
879
955
  return existing;
956
+ }
880
957
  const now = Math.floor(Date.now() / 1000);
881
958
  this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
882
959
  VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
@@ -886,6 +963,110 @@ class MemDatabase {
886
963
  const now = Math.floor(Date.now() / 1000);
887
964
  this.db.query("UPDATE sessions SET status = 'completed', completed_at_epoch = ? WHERE session_id = ?").run(now, sessionId);
888
965
  }
966
+ getSessionById(sessionId) {
967
+ return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId) ?? null;
968
+ }
969
+ getRecentSessions(projectId, limit = 10, userId) {
970
+ const visibilityClause = userId ? " AND s.user_id = ?" : "";
971
+ if (projectId !== null) {
972
+ return this.db.query(`SELECT
973
+ s.*,
974
+ p.name AS project_name,
975
+ ss.request AS request,
976
+ ss.completed AS completed,
977
+ (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
978
+ (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
979
+ FROM sessions s
980
+ LEFT JOIN projects p ON p.id = s.project_id
981
+ LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
982
+ WHERE s.project_id = ?${visibilityClause}
983
+ ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
984
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
985
+ }
986
+ return this.db.query(`SELECT
987
+ s.*,
988
+ p.name AS project_name,
989
+ ss.request AS request,
990
+ ss.completed AS completed,
991
+ (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
992
+ (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
993
+ FROM sessions s
994
+ LEFT JOIN projects p ON p.id = s.project_id
995
+ LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
996
+ WHERE 1 = 1${visibilityClause}
997
+ ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
998
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
999
+ }
1000
+ insertUserPrompt(input) {
1001
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1002
+ const normalizedPrompt = input.prompt.trim();
1003
+ const promptHash = hashPrompt(normalizedPrompt);
1004
+ const latest = this.db.query(`SELECT * FROM user_prompts
1005
+ WHERE session_id = ?
1006
+ ORDER BY prompt_number DESC
1007
+ LIMIT 1`).get(input.session_id);
1008
+ if (latest && latest.prompt_hash === promptHash) {
1009
+ return latest;
1010
+ }
1011
+ const promptNumber = (latest?.prompt_number ?? 0) + 1;
1012
+ const result = this.db.query(`INSERT INTO user_prompts (
1013
+ session_id, project_id, prompt_number, prompt, prompt_hash, cwd,
1014
+ user_id, device_id, agent, created_at_epoch
1015
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, promptNumber, normalizedPrompt, promptHash, input.cwd ?? null, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt);
1016
+ return this.getUserPromptById(Number(result.lastInsertRowid));
1017
+ }
1018
+ getUserPromptById(id) {
1019
+ return this.db.query("SELECT * FROM user_prompts WHERE id = ?").get(id) ?? null;
1020
+ }
1021
+ getRecentUserPrompts(projectId, limit = 10, userId) {
1022
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1023
+ if (projectId !== null) {
1024
+ return this.db.query(`SELECT * FROM user_prompts
1025
+ WHERE project_id = ?${visibilityClause}
1026
+ ORDER BY created_at_epoch DESC, prompt_number DESC
1027
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1028
+ }
1029
+ return this.db.query(`SELECT * FROM user_prompts
1030
+ WHERE 1 = 1${visibilityClause}
1031
+ ORDER BY created_at_epoch DESC, prompt_number DESC
1032
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1033
+ }
1034
+ getSessionUserPrompts(sessionId, limit = 20) {
1035
+ return this.db.query(`SELECT * FROM user_prompts
1036
+ WHERE session_id = ?
1037
+ ORDER BY prompt_number ASC
1038
+ LIMIT ?`).all(sessionId, limit);
1039
+ }
1040
+ insertToolEvent(input) {
1041
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1042
+ const result = this.db.query(`INSERT INTO tool_events (
1043
+ session_id, project_id, tool_name, tool_input_json, tool_response_preview,
1044
+ file_path, command, user_id, device_id, agent, created_at_epoch
1045
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.tool_name, input.tool_input_json ?? null, input.tool_response_preview ?? null, input.file_path ?? null, input.command ?? null, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt);
1046
+ return this.getToolEventById(Number(result.lastInsertRowid));
1047
+ }
1048
+ getToolEventById(id) {
1049
+ return this.db.query("SELECT * FROM tool_events WHERE id = ?").get(id) ?? null;
1050
+ }
1051
+ getSessionToolEvents(sessionId, limit = 20) {
1052
+ return this.db.query(`SELECT * FROM tool_events
1053
+ WHERE session_id = ?
1054
+ ORDER BY created_at_epoch ASC, id ASC
1055
+ LIMIT ?`).all(sessionId, limit);
1056
+ }
1057
+ getRecentToolEvents(projectId, limit = 20, userId) {
1058
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1059
+ if (projectId !== null) {
1060
+ return this.db.query(`SELECT * FROM tool_events
1061
+ WHERE project_id = ?${visibilityClause}
1062
+ ORDER BY created_at_epoch DESC, id DESC
1063
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1064
+ }
1065
+ return this.db.query(`SELECT * FROM tool_events
1066
+ WHERE 1 = 1${visibilityClause}
1067
+ ORDER BY created_at_epoch DESC, id DESC
1068
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1069
+ }
889
1070
  addToOutbox(recordType, recordId) {
890
1071
  const now = Math.floor(Date.now() / 1000);
891
1072
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1056,6 +1237,9 @@ class MemDatabase {
1056
1237
  this.db.query("INSERT OR REPLACE INTO packs_installed (name, installed_at, observation_count) VALUES (?, ?, ?)").run(name, now, observationCount);
1057
1238
  }
1058
1239
  }
1240
+ function hashPrompt(prompt) {
1241
+ return createHash2("sha256").update(prompt).digest("hex");
1242
+ }
1059
1243
 
1060
1244
  // src/storage/outbox.ts
1061
1245
  function getOutboxStats(db) {
@@ -1410,6 +1594,64 @@ var MIGRATIONS2 = [
1410
1594
  );
1411
1595
  INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
1412
1596
  `
1597
+ },
1598
+ {
1599
+ version: 9,
1600
+ description: "Add first-class user prompt capture",
1601
+ sql: `
1602
+ CREATE TABLE IF NOT EXISTS user_prompts (
1603
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1604
+ session_id TEXT NOT NULL,
1605
+ project_id INTEGER REFERENCES projects(id),
1606
+ prompt_number INTEGER NOT NULL,
1607
+ prompt TEXT NOT NULL,
1608
+ prompt_hash TEXT NOT NULL,
1609
+ cwd TEXT,
1610
+ user_id TEXT NOT NULL,
1611
+ device_id TEXT NOT NULL,
1612
+ agent TEXT DEFAULT 'claude-code',
1613
+ created_at_epoch INTEGER NOT NULL,
1614
+ UNIQUE(session_id, prompt_number)
1615
+ );
1616
+
1617
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_session
1618
+ ON user_prompts(session_id, prompt_number DESC);
1619
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_project
1620
+ ON user_prompts(project_id, created_at_epoch DESC);
1621
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_created
1622
+ ON user_prompts(created_at_epoch DESC);
1623
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_hash
1624
+ ON user_prompts(prompt_hash);
1625
+ `
1626
+ },
1627
+ {
1628
+ version: 10,
1629
+ description: "Add first-class tool event chronology",
1630
+ sql: `
1631
+ CREATE TABLE IF NOT EXISTS tool_events (
1632
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1633
+ session_id TEXT NOT NULL,
1634
+ project_id INTEGER REFERENCES projects(id),
1635
+ tool_name TEXT NOT NULL,
1636
+ tool_input_json TEXT,
1637
+ tool_response_preview TEXT,
1638
+ file_path TEXT,
1639
+ command TEXT,
1640
+ user_id TEXT NOT NULL,
1641
+ device_id TEXT NOT NULL,
1642
+ agent TEXT DEFAULT 'claude-code',
1643
+ created_at_epoch INTEGER NOT NULL
1644
+ );
1645
+
1646
+ CREATE INDEX IF NOT EXISTS idx_tool_events_session
1647
+ ON tool_events(session_id, created_at_epoch DESC, id DESC);
1648
+ CREATE INDEX IF NOT EXISTS idx_tool_events_project
1649
+ ON tool_events(project_id, created_at_epoch DESC, id DESC);
1650
+ CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name
1651
+ ON tool_events(tool_name, created_at_epoch DESC);
1652
+ CREATE INDEX IF NOT EXISTS idx_tool_events_created
1653
+ ON tool_events(created_at_epoch DESC, id DESC);
1654
+ `
1413
1655
  }
1414
1656
  ];
1415
1657
  function isVecExtensionLoaded2(db) {
@@ -1420,14 +1662,14 @@ function isVecExtensionLoaded2(db) {
1420
1662
  return false;
1421
1663
  }
1422
1664
  }
1423
- function getSchemaVersion(db) {
1665
+ function getSchemaVersion2(db) {
1424
1666
  const result = db.query("PRAGMA user_version").get();
1425
1667
  return result.user_version;
1426
1668
  }
1427
1669
  var LATEST_SCHEMA_VERSION2 = MIGRATIONS2.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
1428
1670
 
1429
1671
  // src/provisioning/provision.ts
1430
- var DEFAULT_CANDENGO_URL = "https://www.candengo.com";
1672
+ var DEFAULT_CANDENGO_URL = "https://engrm.dev";
1431
1673
 
1432
1674
  class ProvisionError extends Error {
1433
1675
  status;
@@ -1819,6 +2061,7 @@ function registerHooks() {
1819
2061
  return [runtime, ...runArg, join2(hooksDir, `${name}${ext}`)].join(" ");
1820
2062
  }
1821
2063
  const sessionStartCmd = hookCmd("session-start");
2064
+ const userPromptSubmitCmd = hookCmd("user-prompt-submit");
1822
2065
  const preCompactCmd = hookCmd("pre-compact");
1823
2066
  const preToolUseCmd = hookCmd("sentinel");
1824
2067
  const postToolUseCmd = hookCmd("post-tool-use");
@@ -1827,6 +2070,7 @@ function registerHooks() {
1827
2070
  const settings = readJsonFile(CLAUDE_SETTINGS);
1828
2071
  const hooks = settings["hooks"] ?? {};
1829
2072
  hooks["SessionStart"] = replaceEngrmHook(hooks["SessionStart"], { hooks: [{ type: "command", command: sessionStartCmd }] }, "session-start");
2073
+ hooks["UserPromptSubmit"] = replaceEngrmHook(hooks["UserPromptSubmit"], { hooks: [{ type: "command", command: userPromptSubmitCmd }] }, "user-prompt-submit");
1830
2074
  hooks["PreCompact"] = replaceEngrmHook(hooks["PreCompact"], { hooks: [{ type: "command", command: preCompactCmd }] }, "pre-compact");
1831
2075
  hooks["PreToolUse"] = replaceEngrmHook(hooks["PreToolUse"], {
1832
2076
  matcher: "Edit|Write",
@@ -1878,7 +2122,7 @@ function registerAll() {
1878
2122
 
1879
2123
  // src/packs/loader.ts
1880
2124
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
1881
- import { join as join4, dirname as dirname2 } from "node:path";
2125
+ import { join as join4, dirname as dirname3 } from "node:path";
1882
2126
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1883
2127
 
1884
2128
  // src/tools/save.ts
@@ -2206,7 +2450,7 @@ function looksMeaningful(value) {
2206
2450
  // src/storage/projects.ts
2207
2451
  import { execSync } from "node:child_process";
2208
2452
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
2209
- import { basename, join as join3 } from "node:path";
2453
+ import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
2210
2454
  function normaliseGitRemoteUrl(remoteUrl) {
2211
2455
  let url = remoteUrl.trim();
2212
2456
  url = url.replace(/^(?:https?|ssh|git):\/\//, "");
@@ -2260,6 +2504,19 @@ function getGitRemoteUrl(directory) {
2260
2504
  }
2261
2505
  }
2262
2506
  }
2507
+ function getGitTopLevel(directory) {
2508
+ try {
2509
+ const root = execSync("git rev-parse --show-toplevel", {
2510
+ cwd: directory,
2511
+ encoding: "utf-8",
2512
+ timeout: 5000,
2513
+ stdio: ["pipe", "pipe", "pipe"]
2514
+ }).trim();
2515
+ return root || null;
2516
+ } catch {
2517
+ return null;
2518
+ }
2519
+ }
2263
2520
  function readProjectConfigFile(directory) {
2264
2521
  const configPath = join3(directory, ".engrm.json");
2265
2522
  if (!existsSync3(configPath))
@@ -2282,11 +2539,12 @@ function detectProject(directory) {
2282
2539
  const remoteUrl = getGitRemoteUrl(directory);
2283
2540
  if (remoteUrl) {
2284
2541
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
2542
+ const repoRoot = getGitTopLevel(directory) ?? directory;
2285
2543
  return {
2286
2544
  canonical_id: canonicalId,
2287
2545
  name: projectNameFromCanonicalId(canonicalId),
2288
2546
  remote_url: remoteUrl,
2289
- local_path: directory
2547
+ local_path: repoRoot
2290
2548
  };
2291
2549
  }
2292
2550
  const configFile = readProjectConfigFile(directory);
@@ -2306,6 +2564,32 @@ function detectProject(directory) {
2306
2564
  local_path: directory
2307
2565
  };
2308
2566
  }
2567
+ function detectProjectForPath(filePath, fallbackCwd) {
2568
+ const absolutePath = resolve(fallbackCwd ?? process.cwd(), filePath);
2569
+ const candidateDir = existsSync3(absolutePath) && !absolutePath.endsWith("/") ? dirname2(absolutePath) : dirname2(absolutePath);
2570
+ const detected = detectProject(candidateDir);
2571
+ if (detected.canonical_id.startsWith("local/"))
2572
+ return null;
2573
+ return detected;
2574
+ }
2575
+ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
2576
+ const counts = new Map;
2577
+ for (const rawPath of paths) {
2578
+ if (!rawPath || !rawPath.trim())
2579
+ continue;
2580
+ const detected = detectProjectForPath(rawPath, fallbackCwd);
2581
+ if (!detected)
2582
+ continue;
2583
+ const existing = counts.get(detected.canonical_id);
2584
+ if (existing) {
2585
+ existing.count += 1;
2586
+ } else {
2587
+ counts.set(detected.canonical_id, { project: detected, count: 1 });
2588
+ }
2589
+ }
2590
+ const ranked = [...counts.values()].sort((a, b) => b.count - a.count || a.project.name.localeCompare(b.project.name));
2591
+ return ranked[0]?.project ?? detectProject(fallbackCwd);
2592
+ }
2309
2593
 
2310
2594
  // src/embeddings/embedder.ts
2311
2595
  var _available = null;
@@ -2584,7 +2868,8 @@ async function saveObservation(db, config, input) {
2584
2868
  return { success: false, reason: "Title is required" };
2585
2869
  }
2586
2870
  const cwd = input.cwd ?? process.cwd();
2587
- const detected = detectProject(cwd);
2871
+ const touchedPaths = [...input.files_read ?? [], ...input.files_modified ?? []];
2872
+ const detected = touchedPaths.length > 0 ? detectProjectFromTouchedPaths(touchedPaths, cwd) : detectProject(cwd);
2588
2873
  const project = db.upsertProject({
2589
2874
  canonical_id: detected.canonical_id,
2590
2875
  name: detected.name,
@@ -2711,7 +2996,7 @@ function toRelativePath(filePath, projectRoot) {
2711
2996
 
2712
2997
  // src/packs/loader.ts
2713
2998
  function getPacksDir() {
2714
- const thisDir = dirname2(fileURLToPath2(import.meta.url));
2999
+ const thisDir = dirname3(fileURLToPath2(import.meta.url));
2715
3000
  return join4(thisDir, "..", "..", "packs");
2716
3001
  }
2717
3002
  function listPacks() {
@@ -2761,10 +3046,10 @@ async function installPack(db, config, packName, cwd) {
2761
3046
 
2762
3047
  // src/sentinel/rules.ts
2763
3048
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
2764
- import { join as join5, dirname as dirname3 } from "node:path";
3049
+ import { join as join5, dirname as dirname4 } from "node:path";
2765
3050
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2766
3051
  function getRulePacksDir() {
2767
- const thisDir = dirname3(fileURLToPath3(import.meta.url));
3052
+ const thisDir = dirname4(fileURLToPath3(import.meta.url));
2768
3053
  return join5(thisDir, "rule-packs");
2769
3054
  }
2770
3055
  function listRulePacks() {
@@ -2813,11 +3098,152 @@ async function installRulePacks(db, config, packNames) {
2813
3098
  return { installed, skipped };
2814
3099
  }
2815
3100
 
2816
- // src/cli.ts
3101
+ // src/tools/capture-status.ts
3102
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
3103
+ import { homedir as homedir3 } from "node:os";
3104
+ import { join as join6 } from "node:path";
2817
3105
  var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
3106
+ function getCaptureStatus(db, input = {}) {
3107
+ const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
3108
+ const sinceEpoch = Math.floor(Date.now() / 1000) - hours * 3600;
3109
+ const home = input.home_dir ?? homedir3();
3110
+ const claudeJson = join6(home, ".claude.json");
3111
+ const claudeSettings = join6(home, ".claude", "settings.json");
3112
+ const codexConfig = join6(home, ".codex", "config.toml");
3113
+ const codexHooks = join6(home, ".codex", "hooks.json");
3114
+ const claudeJsonContent = existsSync6(claudeJson) ? readFileSync6(claudeJson, "utf-8") : "";
3115
+ const claudeSettingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
3116
+ const codexConfigContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
3117
+ const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
3118
+ const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
3119
+ const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
3120
+ const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
3121
+ const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
3122
+ let claudeHookCount = 0;
3123
+ let claudeSessionStartHook = false;
3124
+ let claudeUserPromptHook = false;
3125
+ let claudePostToolHook = false;
3126
+ let claudeStopHook = false;
3127
+ if (claudeHooksRegistered) {
3128
+ try {
3129
+ const settings = JSON.parse(claudeSettingsContent);
3130
+ const hooks = settings?.hooks ?? {};
3131
+ claudeSessionStartHook = Array.isArray(hooks["SessionStart"]);
3132
+ claudeUserPromptHook = Array.isArray(hooks["UserPromptSubmit"]);
3133
+ claudePostToolHook = Array.isArray(hooks["PostToolUse"]);
3134
+ claudeStopHook = Array.isArray(hooks["Stop"]);
3135
+ for (const entries of Object.values(hooks)) {
3136
+ if (!Array.isArray(entries))
3137
+ continue;
3138
+ for (const entry of entries) {
3139
+ const e = entry;
3140
+ if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
3141
+ claudeHookCount++;
3142
+ }
3143
+ }
3144
+ }
3145
+ } catch {}
3146
+ }
3147
+ let codexSessionStartHook = false;
3148
+ let codexStopHook = false;
3149
+ try {
3150
+ const hooks = codexHooksContent ? JSON.parse(codexHooksContent)?.hooks ?? {} : {};
3151
+ codexSessionStartHook = Array.isArray(hooks["SessionStart"]);
3152
+ codexStopHook = Array.isArray(hooks["Stop"]);
3153
+ } catch {}
3154
+ const visibilityClause = input.user_id ? " AND user_id = ?" : "";
3155
+ const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
3156
+ const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
3157
+ WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
3158
+ const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
3159
+ WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
3160
+ const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
3161
+ FROM sessions s
3162
+ WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
3163
+ ${input.user_id ? "AND s.user_id = ?" : ""}
3164
+ AND (
3165
+ EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
3166
+ OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
3167
+ )`).get(...params)?.count ?? 0;
3168
+ const recentSessionsWithPartialCapture = db.db.query(`SELECT COUNT(*) as count
3169
+ FROM sessions s
3170
+ WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
3171
+ ${input.user_id ? "AND s.user_id = ?" : ""}
3172
+ AND (
3173
+ (s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
3174
+ OR (
3175
+ EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
3176
+ AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
3177
+ )
3178
+ )`).get(...params)?.count ?? 0;
3179
+ const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
3180
+ WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
3181
+ ORDER BY created_at_epoch DESC, prompt_number DESC
3182
+ LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
3183
+ const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
3184
+ WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
3185
+ ORDER BY created_at_epoch DESC, id DESC
3186
+ LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
3187
+ const latestPostToolHookEpoch = parseNullableInt(db.getSyncState("hook_post_tool_last_seen_epoch"));
3188
+ const latestPostToolParseStatus = db.getSyncState("hook_post_tool_last_parse_status");
3189
+ const latestPostToolName = db.getSyncState("hook_post_tool_last_tool_name");
3190
+ const schemaVersion = getSchemaVersion(db.db);
3191
+ return {
3192
+ schema_version: schemaVersion,
3193
+ schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
3194
+ claude_mcp_registered: claudeMcpRegistered,
3195
+ claude_hooks_registered: claudeHooksRegistered,
3196
+ claude_hook_count: claudeHookCount,
3197
+ claude_session_start_hook: claudeSessionStartHook,
3198
+ claude_user_prompt_hook: claudeUserPromptHook,
3199
+ claude_post_tool_hook: claudePostToolHook,
3200
+ claude_stop_hook: claudeStopHook,
3201
+ codex_mcp_registered: codexMcpRegistered,
3202
+ codex_hooks_registered: codexHooksRegistered,
3203
+ codex_session_start_hook: codexSessionStartHook,
3204
+ codex_stop_hook: codexStopHook,
3205
+ codex_raw_chronology_supported: false,
3206
+ recent_user_prompts: recentUserPrompts,
3207
+ recent_tool_events: recentToolEvents,
3208
+ recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
3209
+ recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
3210
+ latest_prompt_epoch: latestPromptEpoch,
3211
+ latest_tool_event_epoch: latestToolEventEpoch,
3212
+ latest_post_tool_hook_epoch: latestPostToolHookEpoch,
3213
+ latest_post_tool_parse_status: latestPostToolParseStatus,
3214
+ latest_post_tool_name: latestPostToolName,
3215
+ raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
3216
+ };
3217
+ }
3218
+ function parseNullableInt(value) {
3219
+ if (!value)
3220
+ return null;
3221
+ const parsed = Number.parseInt(value, 10);
3222
+ return Number.isFinite(parsed) ? parsed : null;
3223
+ }
3224
+
3225
+ // src/sync/auth.ts
3226
+ var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
3227
+ function normalizeBaseUrl(url) {
3228
+ const trimmed = url.trim();
3229
+ if (!trimmed)
3230
+ return trimmed;
3231
+ try {
3232
+ const parsed = new URL(trimmed);
3233
+ if (LEGACY_PUBLIC_HOSTS.has(parsed.hostname)) {
3234
+ parsed.hostname = "engrm.dev";
3235
+ }
3236
+ return parsed.toString().replace(/\/$/, "");
3237
+ } catch {
3238
+ return trimmed.replace(/\/$/, "");
3239
+ }
3240
+ }
3241
+
3242
+ // src/cli.ts
3243
+ var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
2818
3244
  var args = process.argv.slice(2);
2819
3245
  var command = args[0];
2820
- var THIS_DIR = dirname4(fileURLToPath4(import.meta.url));
3246
+ var THIS_DIR = dirname5(fileURLToPath4(import.meta.url));
2821
3247
  var IS_BUILT_DIST = THIS_DIR.endsWith("/dist") || THIS_DIR.endsWith("\\dist");
2822
3248
  switch (command) {
2823
3249
  case "init":
@@ -3041,13 +3467,13 @@ function writeConfigFromProvision(baseUrl, result) {
3041
3467
  console.log(`Database initialised at ${getDbPath()}`);
3042
3468
  }
3043
3469
  function initFromFile(configPath) {
3044
- if (!existsSync6(configPath)) {
3470
+ if (!existsSync7(configPath)) {
3045
3471
  console.error(`Config file not found: ${configPath}`);
3046
3472
  process.exit(1);
3047
3473
  }
3048
3474
  let parsed;
3049
3475
  try {
3050
- const raw = readFileSync6(configPath, "utf-8");
3476
+ const raw = readFileSync7(configPath, "utf-8");
3051
3477
  parsed = JSON.parse(raw);
3052
3478
  } catch {
3053
3479
  console.error(`Invalid JSON in ${configPath}`);
@@ -3134,7 +3560,7 @@ async function initManual() {
3134
3560
  return;
3135
3561
  }
3136
3562
  }
3137
- const candengoUrl = await prompt("Candengo Vector URL (e.g. https://www.candengo.com): ");
3563
+ const candengoUrl = await prompt("Engrm server URL (e.g. https://engrm.dev): ");
3138
3564
  const apiKey = await prompt("API key (cvk_...): ");
3139
3565
  const siteId = await prompt("Site ID: ");
3140
3566
  const namespace = await prompt("Namespace: ");
@@ -3227,18 +3653,18 @@ function handleStatus() {
3227
3653
  console.log(` Plan: ${tierLabels[tier] ?? tier}`);
3228
3654
  console.log(`
3229
3655
  Integration`);
3230
- console.log(` Candengo: ${config.candengo_url || "(not set)"}`);
3656
+ console.log(` Server: ${config.candengo_url ? normalizeBaseUrl(config.candengo_url) : "(not set)"}`);
3231
3657
  console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
3232
- const claudeJson = join6(homedir3(), ".claude.json");
3233
- const claudeSettings = join6(homedir3(), ".claude", "settings.json");
3234
- const codexConfig = join6(homedir3(), ".codex", "config.toml");
3235
- const codexHooks = join6(homedir3(), ".codex", "hooks.json");
3236
- const mcpRegistered = existsSync6(claudeJson) && readFileSync6(claudeJson, "utf-8").includes('"engrm"');
3237
- const settingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
3238
- const codexContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
3239
- const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
3240
- const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start");
3241
- const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
3658
+ const claudeJson = join7(homedir4(), ".claude.json");
3659
+ const claudeSettings = join7(homedir4(), ".claude", "settings.json");
3660
+ const codexConfig = join7(homedir4(), ".codex", "config.toml");
3661
+ const codexHooks = join7(homedir4(), ".codex", "hooks.json");
3662
+ const mcpRegistered = existsSync7(claudeJson) && readFileSync7(claudeJson, "utf-8").includes('"engrm"');
3663
+ const settingsContent = existsSync7(claudeSettings) ? readFileSync7(claudeSettings, "utf-8") : "";
3664
+ const codexContent = existsSync7(codexConfig) ? readFileSync7(codexConfig, "utf-8") : "";
3665
+ const codexHooksContent = existsSync7(codexHooks) ? readFileSync7(codexHooks, "utf-8") : "";
3666
+ const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start") || settingsContent.includes("user-prompt-submit");
3667
+ const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`);
3242
3668
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
3243
3669
  let hookCount = 0;
3244
3670
  if (hooksRegistered) {
@@ -3249,7 +3675,7 @@ function handleStatus() {
3249
3675
  if (Array.isArray(entries)) {
3250
3676
  for (const entry of entries) {
3251
3677
  const e = entry;
3252
- if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
3678
+ if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
3253
3679
  hookCount++;
3254
3680
  }
3255
3681
  }
@@ -3269,7 +3695,7 @@ function handleStatus() {
3269
3695
  if (config.sentinel.provider) {
3270
3696
  console.log(` Provider: ${config.sentinel.provider}${config.sentinel.model ? ` (${config.sentinel.model})` : ""}`);
3271
3697
  }
3272
- if (existsSync6(getDbPath())) {
3698
+ if (existsSync7(getDbPath())) {
3273
3699
  try {
3274
3700
  const db = new MemDatabase(getDbPath());
3275
3701
  const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
@@ -3282,7 +3708,7 @@ function handleStatus() {
3282
3708
  console.log(`
3283
3709
  Sentinel: disabled`);
3284
3710
  }
3285
- if (existsSync6(getDbPath())) {
3711
+ if (existsSync7(getDbPath())) {
3286
3712
  try {
3287
3713
  const db = new MemDatabase(getDbPath());
3288
3714
  const obsCount = db.getActiveObservationCount();
@@ -3301,6 +3727,19 @@ function handleStatus() {
3301
3727
  } catch {}
3302
3728
  const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
3303
3729
  console.log(` Sessions: ${summaryCount} summarised`);
3730
+ const capture = getCaptureStatus(db, { user_id: config.user_id });
3731
+ console.log(` Raw capture: ${capture.raw_capture_active ? "active" : "observations-only so far"}`);
3732
+ console.log(` Prompts/tools: ${capture.recent_user_prompts}/${capture.recent_tool_events} in last 24h`);
3733
+ if (capture.recent_sessions_with_partial_capture > 0) {
3734
+ console.log(` Partial raw: ${capture.recent_sessions_with_partial_capture} recent session${capture.recent_sessions_with_partial_capture === 1 ? "" : "s"} missing some chronology`);
3735
+ }
3736
+ console.log(` Hook state: Claude ${capture.claude_user_prompt_hook && capture.claude_post_tool_hook ? "raw-ready" : "partial"}, Codex ${capture.codex_raw_chronology_supported ? "raw-ready" : "start/stop only"}`);
3737
+ if (capture.latest_post_tool_hook_epoch) {
3738
+ const lastSeen = new Date(capture.latest_post_tool_hook_epoch * 1000).toISOString();
3739
+ const parseStatus = capture.latest_post_tool_parse_status ?? "unknown";
3740
+ const toolName = capture.latest_post_tool_name ?? "unknown";
3741
+ console.log(` PostToolUse: ${parseStatus} (${toolName}, ${lastSeen})`);
3742
+ }
3304
3743
  try {
3305
3744
  const activeObservations = db.db.query(`SELECT * FROM observations
3306
3745
  WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
@@ -3374,8 +3813,8 @@ function handleStatus() {
3374
3813
  Files`);
3375
3814
  console.log(` Config: ${getSettingsPath()}`);
3376
3815
  console.log(` Database: ${getDbPath()}`);
3377
- console.log(` Codex config: ${join6(homedir3(), ".codex", "config.toml")}`);
3378
- console.log(` Codex hooks: ${join6(homedir3(), ".codex", "hooks.json")}`);
3816
+ console.log(` Codex config: ${join7(homedir4(), ".codex", "config.toml")}`);
3817
+ console.log(` Codex hooks: ${join7(homedir4(), ".codex", "hooks.json")}`);
3379
3818
  }
3380
3819
  function formatTimeAgo(epoch) {
3381
3820
  const ago = Math.floor(Date.now() / 1000) - epoch;
@@ -3397,7 +3836,7 @@ function formatSyncTime(epochStr) {
3397
3836
  }
3398
3837
  function ensureConfigDir() {
3399
3838
  const dir = getConfigDir();
3400
- if (!existsSync6(dir)) {
3839
+ if (!existsSync7(dir)) {
3401
3840
  mkdirSync3(dir, { recursive: true });
3402
3841
  }
3403
3842
  }
@@ -3418,7 +3857,7 @@ function generateDeviceId2() {
3418
3857
  break;
3419
3858
  }
3420
3859
  const material = `${host}:${mac || "no-mac"}`;
3421
- const suffix = createHash2("sha256").update(material).digest("hex").slice(0, 8);
3860
+ const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
3422
3861
  return `${host}-${suffix}`;
3423
3862
  }
3424
3863
  async function handleInstallPack(flags) {
@@ -3552,7 +3991,7 @@ async function handleDoctor() {
3552
3991
  return;
3553
3992
  }
3554
3993
  try {
3555
- const currentVersion = getSchemaVersion(db.db);
3994
+ const currentVersion = getSchemaVersion2(db.db);
3556
3995
  if (currentVersion >= LATEST_SCHEMA_VERSION2) {
3557
3996
  pass(`Database schema is current (v${currentVersion})`);
3558
3997
  } else {
@@ -3561,10 +4000,10 @@ async function handleDoctor() {
3561
4000
  } catch {
3562
4001
  warn("Could not check database schema version");
3563
4002
  }
3564
- const claudeJson = join6(homedir3(), ".claude.json");
4003
+ const claudeJson = join7(homedir4(), ".claude.json");
3565
4004
  try {
3566
- if (existsSync6(claudeJson)) {
3567
- const content = readFileSync6(claudeJson, "utf-8");
4005
+ if (existsSync7(claudeJson)) {
4006
+ const content = readFileSync7(claudeJson, "utf-8");
3568
4007
  if (content.includes('"engrm"')) {
3569
4008
  pass("MCP server registered in Claude Code");
3570
4009
  } else {
@@ -3576,27 +4015,46 @@ async function handleDoctor() {
3576
4015
  } catch {
3577
4016
  warn("Could not check MCP server registration");
3578
4017
  }
3579
- const claudeSettings = join6(homedir3(), ".claude", "settings.json");
4018
+ const claudeSettings = join7(homedir4(), ".claude", "settings.json");
3580
4019
  try {
3581
- if (existsSync6(claudeSettings)) {
3582
- const content = readFileSync6(claudeSettings, "utf-8");
4020
+ if (existsSync7(claudeSettings)) {
4021
+ const content = readFileSync7(claudeSettings, "utf-8");
3583
4022
  let hookCount = 0;
4023
+ let hasSessionStart = false;
4024
+ let hasUserPrompt = false;
4025
+ let hasPostToolUse = false;
4026
+ let hasStop = false;
3584
4027
  try {
3585
4028
  const settings = JSON.parse(content);
3586
4029
  const hooks = settings?.hooks ?? {};
4030
+ hasSessionStart = Array.isArray(hooks["SessionStart"]);
4031
+ hasUserPrompt = Array.isArray(hooks["UserPromptSubmit"]);
4032
+ hasPostToolUse = Array.isArray(hooks["PostToolUse"]);
4033
+ hasStop = Array.isArray(hooks["Stop"]);
3587
4034
  for (const entries of Object.values(hooks)) {
3588
4035
  if (Array.isArray(entries)) {
3589
4036
  for (const entry of entries) {
3590
4037
  const e = entry;
3591
- if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
4038
+ if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
3592
4039
  hookCount++;
3593
4040
  }
3594
4041
  }
3595
4042
  }
3596
4043
  }
3597
4044
  } catch {}
3598
- if (hookCount > 0) {
4045
+ const missingCritical = [];
4046
+ if (!hasSessionStart)
4047
+ missingCritical.push("SessionStart");
4048
+ if (!hasUserPrompt)
4049
+ missingCritical.push("UserPromptSubmit");
4050
+ if (!hasPostToolUse)
4051
+ missingCritical.push("PostToolUse");
4052
+ if (!hasStop)
4053
+ missingCritical.push("Stop");
4054
+ if (hookCount > 0 && missingCritical.length === 0) {
3599
4055
  pass(`Hooks registered (${hookCount} hook${hookCount === 1 ? "" : "s"})`);
4056
+ } else if (hookCount > 0) {
4057
+ warn(`Hooks registered but incomplete \u2014 missing ${missingCritical.join(", ")}`);
3600
4058
  } else {
3601
4059
  warn("No Engrm hooks found in Claude Code settings");
3602
4060
  }
@@ -3606,11 +4064,11 @@ async function handleDoctor() {
3606
4064
  } catch {
3607
4065
  warn("Could not check hooks registration");
3608
4066
  }
3609
- const codexConfig = join6(homedir3(), ".codex", "config.toml");
4067
+ const codexConfig = join7(homedir4(), ".codex", "config.toml");
3610
4068
  try {
3611
- if (existsSync6(codexConfig)) {
3612
- const content = readFileSync6(codexConfig, "utf-8");
3613
- if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`)) {
4069
+ if (existsSync7(codexConfig)) {
4070
+ const content = readFileSync7(codexConfig, "utf-8");
4071
+ if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`)) {
3614
4072
  pass("MCP server registered in Codex");
3615
4073
  } else {
3616
4074
  warn("MCP server not registered in Codex");
@@ -3621,10 +4079,10 @@ async function handleDoctor() {
3621
4079
  } catch {
3622
4080
  warn("Could not check Codex MCP registration");
3623
4081
  }
3624
- const codexHooks = join6(homedir3(), ".codex", "hooks.json");
4082
+ const codexHooks = join7(homedir4(), ".codex", "hooks.json");
3625
4083
  try {
3626
- if (existsSync6(codexHooks)) {
3627
- const content = readFileSync6(codexHooks, "utf-8");
4084
+ if (existsSync7(codexHooks)) {
4085
+ const content = readFileSync7(codexHooks, "utf-8");
3628
4086
  if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
3629
4087
  pass("Hooks registered in Codex");
3630
4088
  } else {
@@ -3638,14 +4096,23 @@ async function handleDoctor() {
3638
4096
  }
3639
4097
  if (config.candengo_url) {
3640
4098
  try {
4099
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
3641
4100
  const controller = new AbortController;
3642
4101
  const timeout = setTimeout(() => controller.abort(), 5000);
3643
4102
  const start = Date.now();
3644
- const res = await fetch(`${config.candengo_url}/health`, { signal: controller.signal });
4103
+ let res = await fetch(`${baseUrl}/health`, { signal: controller.signal });
4104
+ if (res.status === 404) {
4105
+ res = await fetch(`${baseUrl}/v1/mem/provision`, {
4106
+ method: "POST",
4107
+ headers: { "Content-Type": "application/json" },
4108
+ body: "{}",
4109
+ signal: controller.signal
4110
+ });
4111
+ }
3645
4112
  clearTimeout(timeout);
3646
4113
  const elapsed = Date.now() - start;
3647
- if (res.ok) {
3648
- const host = new URL(config.candengo_url).hostname;
4114
+ if (res.ok || res.status === 400) {
4115
+ const host = new URL(baseUrl).hostname;
3649
4116
  pass(`Server connectivity (${host}, ${elapsed}ms)`);
3650
4117
  } else {
3651
4118
  fail(`Server returned HTTP ${res.status}`);
@@ -3659,16 +4126,16 @@ async function handleDoctor() {
3659
4126
  }
3660
4127
  if (config.candengo_url && config.candengo_api_key) {
3661
4128
  try {
4129
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
3662
4130
  const controller = new AbortController;
3663
4131
  const timeout = setTimeout(() => controller.abort(), 5000);
3664
- const res = await fetch(`${config.candengo_url}/v1/account/me`, {
4132
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
3665
4133
  headers: { Authorization: `Bearer ${config.candengo_api_key}` },
3666
4134
  signal: controller.signal
3667
4135
  });
3668
4136
  clearTimeout(timeout);
3669
4137
  if (res.ok) {
3670
- const data = await res.json();
3671
- const email = data.email ?? config.user_email ?? "unknown";
4138
+ const email = config.user_email ?? "configured";
3672
4139
  pass(`Authentication valid (${email})`);
3673
4140
  } else if (res.status === 401 || res.status === 403) {
3674
4141
  fail("Authentication failed \u2014 API key may be expired");
@@ -3717,9 +4184,27 @@ async function handleDoctor() {
3717
4184
  } catch {
3718
4185
  warn("Could not count observations");
3719
4186
  }
4187
+ try {
4188
+ const capture = getCaptureStatus(db, { user_id: config.user_id });
4189
+ if (capture.raw_capture_active && capture.recent_tool_events > 0 && capture.recent_sessions_with_partial_capture === 0) {
4190
+ pass(`Raw chronology active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h)`);
4191
+ } else if (capture.raw_capture_active && capture.recent_sessions_with_partial_capture > 0) {
4192
+ warn(`Raw chronology is only partially active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h; ${capture.recent_sessions_with_partial_capture} recent session${capture.recent_sessions_with_partial_capture === 1 ? "" : "s"} missing some chronology).`);
4193
+ if (capture.latest_post_tool_hook_epoch) {
4194
+ info(`Last PostToolUse hook: ${new Date(capture.latest_post_tool_hook_epoch * 1000).toISOString()} (${capture.latest_post_tool_parse_status ?? "unknown"}${capture.latest_post_tool_name ? `, ${capture.latest_post_tool_name}` : ""})`);
4195
+ }
4196
+ } else if (capture.claude_hooks_registered || capture.codex_hooks_registered) {
4197
+ const guidance = capture.claude_user_prompt_hook && capture.claude_post_tool_hook ? "Claude is raw-ready; open a fresh Claude Code session and perform a few actions to verify capture." : "Claude raw chronology hooks are incomplete, and Codex currently supports start/stop capture only.";
4198
+ warn(`Hooks are registered, but no raw prompt/tool chronology has been captured in the last 24h. ${guidance}`);
4199
+ } else {
4200
+ warn("Raw chronology inactive \u2014 hook registration is incomplete");
4201
+ }
4202
+ } catch {
4203
+ warn("Could not check raw chronology capture");
4204
+ }
3720
4205
  try {
3721
4206
  const dbPath = getDbPath();
3722
- if (existsSync6(dbPath)) {
4207
+ if (existsSync7(dbPath)) {
3723
4208
  const stats = statSync(dbPath);
3724
4209
  const sizeMB = stats.size / (1024 * 1024);
3725
4210
  const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
@@ -3787,11 +4272,11 @@ Registering with Claude Code and Codex...`);
3787
4272
  console.log(`
3788
4273
  Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
3789
4274
  } catch (error) {
3790
- const packageRoot = join6(THIS_DIR, "..");
4275
+ const packageRoot = join7(THIS_DIR, "..");
3791
4276
  const runtime = IS_BUILT_DIST ? process.execPath : "bun";
3792
- const serverArgs = IS_BUILT_DIST ? [join6(packageRoot, "dist", "server.js")] : ["run", join6(packageRoot, "src", "server.ts")];
3793
- const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join6(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join6(packageRoot, "hooks", "session-start.ts")}`;
3794
- const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join6(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join6(packageRoot, "hooks", "codex-stop.ts")}`;
4277
+ const serverArgs = IS_BUILT_DIST ? [join7(packageRoot, "dist", "server.js")] : ["run", join7(packageRoot, "src", "server.ts")];
4278
+ const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join7(packageRoot, "hooks", "session-start.ts")}`;
4279
+ const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join7(packageRoot, "hooks", "codex-stop.ts")}`;
3795
4280
  console.log(`
3796
4281
  Could not auto-register with Claude Code and Codex.`);
3797
4282
  console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);