engrm 0.4.6 → 0.4.8

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 dirname4, 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) {
@@ -886,6 +949,110 @@ class MemDatabase {
886
949
  const now = Math.floor(Date.now() / 1000);
887
950
  this.db.query("UPDATE sessions SET status = 'completed', completed_at_epoch = ? WHERE session_id = ?").run(now, sessionId);
888
951
  }
952
+ getSessionById(sessionId) {
953
+ return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId) ?? null;
954
+ }
955
+ getRecentSessions(projectId, limit = 10, userId) {
956
+ const visibilityClause = userId ? " AND s.user_id = ?" : "";
957
+ if (projectId !== null) {
958
+ return this.db.query(`SELECT
959
+ s.*,
960
+ p.name AS project_name,
961
+ ss.request AS request,
962
+ ss.completed AS completed,
963
+ (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
964
+ (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
965
+ FROM sessions s
966
+ LEFT JOIN projects p ON p.id = s.project_id
967
+ LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
968
+ WHERE s.project_id = ?${visibilityClause}
969
+ ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
970
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
971
+ }
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 1 = 1${visibilityClause}
983
+ ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
984
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
985
+ }
986
+ insertUserPrompt(input) {
987
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
988
+ const normalizedPrompt = input.prompt.trim();
989
+ const promptHash = hashPrompt(normalizedPrompt);
990
+ const latest = this.db.query(`SELECT * FROM user_prompts
991
+ WHERE session_id = ?
992
+ ORDER BY prompt_number DESC
993
+ LIMIT 1`).get(input.session_id);
994
+ if (latest && latest.prompt_hash === promptHash) {
995
+ return latest;
996
+ }
997
+ const promptNumber = (latest?.prompt_number ?? 0) + 1;
998
+ const result = this.db.query(`INSERT INTO user_prompts (
999
+ session_id, project_id, prompt_number, prompt, prompt_hash, cwd,
1000
+ user_id, device_id, agent, created_at_epoch
1001
+ ) 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);
1002
+ return this.getUserPromptById(Number(result.lastInsertRowid));
1003
+ }
1004
+ getUserPromptById(id) {
1005
+ return this.db.query("SELECT * FROM user_prompts WHERE id = ?").get(id) ?? null;
1006
+ }
1007
+ getRecentUserPrompts(projectId, limit = 10, userId) {
1008
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1009
+ if (projectId !== null) {
1010
+ return this.db.query(`SELECT * FROM user_prompts
1011
+ WHERE project_id = ?${visibilityClause}
1012
+ ORDER BY created_at_epoch DESC, prompt_number DESC
1013
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1014
+ }
1015
+ return this.db.query(`SELECT * FROM user_prompts
1016
+ WHERE 1 = 1${visibilityClause}
1017
+ ORDER BY created_at_epoch DESC, prompt_number DESC
1018
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1019
+ }
1020
+ getSessionUserPrompts(sessionId, limit = 20) {
1021
+ return this.db.query(`SELECT * FROM user_prompts
1022
+ WHERE session_id = ?
1023
+ ORDER BY prompt_number ASC
1024
+ LIMIT ?`).all(sessionId, limit);
1025
+ }
1026
+ insertToolEvent(input) {
1027
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1028
+ const result = this.db.query(`INSERT INTO tool_events (
1029
+ session_id, project_id, tool_name, tool_input_json, tool_response_preview,
1030
+ file_path, command, user_id, device_id, agent, created_at_epoch
1031
+ ) 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);
1032
+ return this.getToolEventById(Number(result.lastInsertRowid));
1033
+ }
1034
+ getToolEventById(id) {
1035
+ return this.db.query("SELECT * FROM tool_events WHERE id = ?").get(id) ?? null;
1036
+ }
1037
+ getSessionToolEvents(sessionId, limit = 20) {
1038
+ return this.db.query(`SELECT * FROM tool_events
1039
+ WHERE session_id = ?
1040
+ ORDER BY created_at_epoch ASC, id ASC
1041
+ LIMIT ?`).all(sessionId, limit);
1042
+ }
1043
+ getRecentToolEvents(projectId, limit = 20, userId) {
1044
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1045
+ if (projectId !== null) {
1046
+ return this.db.query(`SELECT * FROM tool_events
1047
+ WHERE project_id = ?${visibilityClause}
1048
+ ORDER BY created_at_epoch DESC, id DESC
1049
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1050
+ }
1051
+ return this.db.query(`SELECT * FROM tool_events
1052
+ WHERE 1 = 1${visibilityClause}
1053
+ ORDER BY created_at_epoch DESC, id DESC
1054
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1055
+ }
889
1056
  addToOutbox(recordType, recordId) {
890
1057
  const now = Math.floor(Date.now() / 1000);
891
1058
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1056,6 +1223,9 @@ class MemDatabase {
1056
1223
  this.db.query("INSERT OR REPLACE INTO packs_installed (name, installed_at, observation_count) VALUES (?, ?, ?)").run(name, now, observationCount);
1057
1224
  }
1058
1225
  }
1226
+ function hashPrompt(prompt) {
1227
+ return createHash2("sha256").update(prompt).digest("hex");
1228
+ }
1059
1229
 
1060
1230
  // src/storage/outbox.ts
1061
1231
  function getOutboxStats(db) {
@@ -1072,6 +1242,31 @@ function getOutboxStats(db) {
1072
1242
  return stats;
1073
1243
  }
1074
1244
 
1245
+ // src/intelligence/value-signals.ts
1246
+ var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
1247
+ function computeSessionValueSignals(observations, securityFindings = []) {
1248
+ const decisionsCount = observations.filter((o) => o.type === "decision").length;
1249
+ const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
1250
+ const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
1251
+ const featuresCount = observations.filter((o) => o.type === "feature").length;
1252
+ const refactorsCount = observations.filter((o) => o.type === "refactor").length;
1253
+ const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
1254
+ const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
1255
+ const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
1256
+ return {
1257
+ decisions_count: decisionsCount,
1258
+ lessons_count: lessonsCount,
1259
+ discoveries_count: discoveriesCount,
1260
+ features_count: featuresCount,
1261
+ refactors_count: refactorsCount,
1262
+ repeated_patterns_count: repeatedPatternsCount,
1263
+ security_findings_count: securityFindings.length,
1264
+ critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
1265
+ delivery_review_ready: hasRequestSignal && hasCompletionSignal,
1266
+ vibe_guardian_active: securityFindings.length > 0
1267
+ };
1268
+ }
1269
+
1075
1270
  // src/storage/migrations.ts
1076
1271
  var MIGRATIONS2 = [
1077
1272
  {
@@ -1385,6 +1580,64 @@ var MIGRATIONS2 = [
1385
1580
  );
1386
1581
  INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
1387
1582
  `
1583
+ },
1584
+ {
1585
+ version: 9,
1586
+ description: "Add first-class user prompt capture",
1587
+ sql: `
1588
+ CREATE TABLE IF NOT EXISTS user_prompts (
1589
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1590
+ session_id TEXT NOT NULL,
1591
+ project_id INTEGER REFERENCES projects(id),
1592
+ prompt_number INTEGER NOT NULL,
1593
+ prompt TEXT NOT NULL,
1594
+ prompt_hash TEXT NOT NULL,
1595
+ cwd TEXT,
1596
+ user_id TEXT NOT NULL,
1597
+ device_id TEXT NOT NULL,
1598
+ agent TEXT DEFAULT 'claude-code',
1599
+ created_at_epoch INTEGER NOT NULL,
1600
+ UNIQUE(session_id, prompt_number)
1601
+ );
1602
+
1603
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_session
1604
+ ON user_prompts(session_id, prompt_number DESC);
1605
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_project
1606
+ ON user_prompts(project_id, created_at_epoch DESC);
1607
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_created
1608
+ ON user_prompts(created_at_epoch DESC);
1609
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_hash
1610
+ ON user_prompts(prompt_hash);
1611
+ `
1612
+ },
1613
+ {
1614
+ version: 10,
1615
+ description: "Add first-class tool event chronology",
1616
+ sql: `
1617
+ CREATE TABLE IF NOT EXISTS tool_events (
1618
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1619
+ session_id TEXT NOT NULL,
1620
+ project_id INTEGER REFERENCES projects(id),
1621
+ tool_name TEXT NOT NULL,
1622
+ tool_input_json TEXT,
1623
+ tool_response_preview TEXT,
1624
+ file_path TEXT,
1625
+ command TEXT,
1626
+ user_id TEXT NOT NULL,
1627
+ device_id TEXT NOT NULL,
1628
+ agent TEXT DEFAULT 'claude-code',
1629
+ created_at_epoch INTEGER NOT NULL
1630
+ );
1631
+
1632
+ CREATE INDEX IF NOT EXISTS idx_tool_events_session
1633
+ ON tool_events(session_id, created_at_epoch DESC, id DESC);
1634
+ CREATE INDEX IF NOT EXISTS idx_tool_events_project
1635
+ ON tool_events(project_id, created_at_epoch DESC, id DESC);
1636
+ CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name
1637
+ ON tool_events(tool_name, created_at_epoch DESC);
1638
+ CREATE INDEX IF NOT EXISTS idx_tool_events_created
1639
+ ON tool_events(created_at_epoch DESC, id DESC);
1640
+ `
1388
1641
  }
1389
1642
  ];
1390
1643
  function isVecExtensionLoaded2(db) {
@@ -1395,14 +1648,14 @@ function isVecExtensionLoaded2(db) {
1395
1648
  return false;
1396
1649
  }
1397
1650
  }
1398
- function getSchemaVersion(db) {
1651
+ function getSchemaVersion2(db) {
1399
1652
  const result = db.query("PRAGMA user_version").get();
1400
1653
  return result.user_version;
1401
1654
  }
1402
1655
  var LATEST_SCHEMA_VERSION2 = MIGRATIONS2.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
1403
1656
 
1404
1657
  // src/provisioning/provision.ts
1405
- var DEFAULT_CANDENGO_URL = "https://www.candengo.com";
1658
+ var DEFAULT_CANDENGO_URL = "https://engrm.dev";
1406
1659
 
1407
1660
  class ProvisionError extends Error {
1408
1661
  status;
@@ -1794,6 +2047,7 @@ function registerHooks() {
1794
2047
  return [runtime, ...runArg, join2(hooksDir, `${name}${ext}`)].join(" ");
1795
2048
  }
1796
2049
  const sessionStartCmd = hookCmd("session-start");
2050
+ const userPromptSubmitCmd = hookCmd("user-prompt-submit");
1797
2051
  const preCompactCmd = hookCmd("pre-compact");
1798
2052
  const preToolUseCmd = hookCmd("sentinel");
1799
2053
  const postToolUseCmd = hookCmd("post-tool-use");
@@ -1802,6 +2056,7 @@ function registerHooks() {
1802
2056
  const settings = readJsonFile(CLAUDE_SETTINGS);
1803
2057
  const hooks = settings["hooks"] ?? {};
1804
2058
  hooks["SessionStart"] = replaceEngrmHook(hooks["SessionStart"], { hooks: [{ type: "command", command: sessionStartCmd }] }, "session-start");
2059
+ hooks["UserPromptSubmit"] = replaceEngrmHook(hooks["UserPromptSubmit"], { hooks: [{ type: "command", command: userPromptSubmitCmd }] }, "user-prompt-submit");
1805
2060
  hooks["PreCompact"] = replaceEngrmHook(hooks["PreCompact"], { hooks: [{ type: "command", command: preCompactCmd }] }, "pre-compact");
1806
2061
  hooks["PreToolUse"] = replaceEngrmHook(hooks["PreToolUse"], {
1807
2062
  matcher: "Edit|Write",
@@ -2104,6 +2359,80 @@ function findDuplicate(newTitle, candidates) {
2104
2359
  return bestMatch;
2105
2360
  }
2106
2361
 
2362
+ // src/capture/facts.ts
2363
+ var FACT_ELIGIBLE_TYPES = new Set([
2364
+ "bugfix",
2365
+ "decision",
2366
+ "discovery",
2367
+ "pattern",
2368
+ "feature",
2369
+ "refactor",
2370
+ "change"
2371
+ ]);
2372
+ function buildStructuredFacts(input) {
2373
+ const seedFacts = dedupeFacts(input.facts ?? []);
2374
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
2375
+ return seedFacts;
2376
+ }
2377
+ const derived = [...seedFacts];
2378
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
2379
+ derived.push(input.title.trim());
2380
+ }
2381
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
2382
+ derived.push(sentence);
2383
+ }
2384
+ const fileFact = buildFilesFact(input.filesModified);
2385
+ if (fileFact) {
2386
+ derived.push(fileFact);
2387
+ }
2388
+ return dedupeFacts(derived).slice(0, 4);
2389
+ }
2390
+ function extractNarrativeFacts(narrative) {
2391
+ if (!narrative)
2392
+ return [];
2393
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
2394
+ if (cleaned.length < 24)
2395
+ return [];
2396
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
2397
+ return parts.slice(0, 2);
2398
+ }
2399
+ function buildFilesFact(filesModified) {
2400
+ if (!filesModified || filesModified.length === 0)
2401
+ return null;
2402
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
2403
+ if (cleaned.length === 0)
2404
+ return null;
2405
+ if (cleaned.length === 1) {
2406
+ return `Touched ${cleaned[0]}`;
2407
+ }
2408
+ return `Touched ${cleaned.join(", ")}`;
2409
+ }
2410
+ function dedupeFacts(facts) {
2411
+ const seen = new Set;
2412
+ const result = [];
2413
+ for (const fact of facts) {
2414
+ const cleaned = fact.trim().replace(/\s+/g, " ");
2415
+ if (!looksMeaningful(cleaned))
2416
+ continue;
2417
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
2418
+ if (!key || seen.has(key))
2419
+ continue;
2420
+ seen.add(key);
2421
+ result.push(cleaned);
2422
+ }
2423
+ return result;
2424
+ }
2425
+ function looksMeaningful(value) {
2426
+ const cleaned = value.trim();
2427
+ if (cleaned.length < 12)
2428
+ return false;
2429
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
2430
+ return false;
2431
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
2432
+ return false;
2433
+ return true;
2434
+ }
2435
+
2107
2436
  // src/storage/projects.ts
2108
2437
  import { execSync } from "node:child_process";
2109
2438
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
@@ -2495,10 +2824,17 @@ async function saveObservation(db, config, input) {
2495
2824
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
2496
2825
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
2497
2826
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
2498
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
2499
2827
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
2500
2828
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
2501
2829
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
2830
+ const structuredFacts = buildStructuredFacts({
2831
+ type: input.type,
2832
+ title: input.title,
2833
+ narrative: input.narrative,
2834
+ facts: input.facts,
2835
+ filesModified
2836
+ });
2837
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
2502
2838
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
2503
2839
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
2504
2840
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
@@ -2707,8 +3043,103 @@ async function installRulePacks(db, config, packNames) {
2707
3043
  return { installed, skipped };
2708
3044
  }
2709
3045
 
2710
- // src/cli.ts
3046
+ // src/tools/capture-status.ts
3047
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
3048
+ import { homedir as homedir3 } from "node:os";
3049
+ import { join as join6 } from "node:path";
2711
3050
  var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
3051
+ function getCaptureStatus(db, input = {}) {
3052
+ const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
3053
+ const sinceEpoch = Math.floor(Date.now() / 1000) - hours * 3600;
3054
+ const home = input.home_dir ?? homedir3();
3055
+ const claudeJson = join6(home, ".claude.json");
3056
+ const claudeSettings = join6(home, ".claude", "settings.json");
3057
+ const codexConfig = join6(home, ".codex", "config.toml");
3058
+ const codexHooks = join6(home, ".codex", "hooks.json");
3059
+ const claudeJsonContent = existsSync6(claudeJson) ? readFileSync6(claudeJson, "utf-8") : "";
3060
+ const claudeSettingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
3061
+ const codexConfigContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
3062
+ const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
3063
+ const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
3064
+ const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
3065
+ const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
3066
+ const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
3067
+ let claudeHookCount = 0;
3068
+ if (claudeHooksRegistered) {
3069
+ try {
3070
+ const settings = JSON.parse(claudeSettingsContent);
3071
+ const hooks = settings?.hooks ?? {};
3072
+ for (const entries of Object.values(hooks)) {
3073
+ if (!Array.isArray(entries))
3074
+ continue;
3075
+ for (const entry of entries) {
3076
+ const e = entry;
3077
+ 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"))) {
3078
+ claudeHookCount++;
3079
+ }
3080
+ }
3081
+ }
3082
+ } catch {}
3083
+ }
3084
+ const visibilityClause = input.user_id ? " AND user_id = ?" : "";
3085
+ const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
3086
+ const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
3087
+ WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
3088
+ const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
3089
+ WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
3090
+ const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
3091
+ FROM sessions s
3092
+ WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
3093
+ ${input.user_id ? "AND s.user_id = ?" : ""}
3094
+ AND (
3095
+ EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
3096
+ OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
3097
+ )`).get(...params)?.count ?? 0;
3098
+ const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
3099
+ WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
3100
+ ORDER BY created_at_epoch DESC, prompt_number DESC
3101
+ LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
3102
+ const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
3103
+ WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
3104
+ ORDER BY created_at_epoch DESC, id DESC
3105
+ LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
3106
+ const schemaVersion = getSchemaVersion(db.db);
3107
+ return {
3108
+ schema_version: schemaVersion,
3109
+ schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
3110
+ claude_mcp_registered: claudeMcpRegistered,
3111
+ claude_hooks_registered: claudeHooksRegistered,
3112
+ claude_hook_count: claudeHookCount,
3113
+ codex_mcp_registered: codexMcpRegistered,
3114
+ codex_hooks_registered: codexHooksRegistered,
3115
+ recent_user_prompts: recentUserPrompts,
3116
+ recent_tool_events: recentToolEvents,
3117
+ recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
3118
+ latest_prompt_epoch: latestPromptEpoch,
3119
+ latest_tool_event_epoch: latestToolEventEpoch,
3120
+ raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
3121
+ };
3122
+ }
3123
+
3124
+ // src/sync/auth.ts
3125
+ var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
3126
+ function normalizeBaseUrl(url) {
3127
+ const trimmed = url.trim();
3128
+ if (!trimmed)
3129
+ return trimmed;
3130
+ try {
3131
+ const parsed = new URL(trimmed);
3132
+ if (LEGACY_PUBLIC_HOSTS.has(parsed.hostname)) {
3133
+ parsed.hostname = "engrm.dev";
3134
+ }
3135
+ return parsed.toString().replace(/\/$/, "");
3136
+ } catch {
3137
+ return trimmed.replace(/\/$/, "");
3138
+ }
3139
+ }
3140
+
3141
+ // src/cli.ts
3142
+ var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
2712
3143
  var args = process.argv.slice(2);
2713
3144
  var command = args[0];
2714
3145
  var THIS_DIR = dirname4(fileURLToPath4(import.meta.url));
@@ -2935,13 +3366,13 @@ function writeConfigFromProvision(baseUrl, result) {
2935
3366
  console.log(`Database initialised at ${getDbPath()}`);
2936
3367
  }
2937
3368
  function initFromFile(configPath) {
2938
- if (!existsSync6(configPath)) {
3369
+ if (!existsSync7(configPath)) {
2939
3370
  console.error(`Config file not found: ${configPath}`);
2940
3371
  process.exit(1);
2941
3372
  }
2942
3373
  let parsed;
2943
3374
  try {
2944
- const raw = readFileSync6(configPath, "utf-8");
3375
+ const raw = readFileSync7(configPath, "utf-8");
2945
3376
  parsed = JSON.parse(raw);
2946
3377
  } catch {
2947
3378
  console.error(`Invalid JSON in ${configPath}`);
@@ -3028,7 +3459,7 @@ async function initManual() {
3028
3459
  return;
3029
3460
  }
3030
3461
  }
3031
- const candengoUrl = await prompt("Candengo Vector URL (e.g. https://www.candengo.com): ");
3462
+ const candengoUrl = await prompt("Engrm server URL (e.g. https://engrm.dev): ");
3032
3463
  const apiKey = await prompt("API key (cvk_...): ");
3033
3464
  const siteId = await prompt("Site ID: ");
3034
3465
  const namespace = await prompt("Namespace: ");
@@ -3121,18 +3552,18 @@ function handleStatus() {
3121
3552
  console.log(` Plan: ${tierLabels[tier] ?? tier}`);
3122
3553
  console.log(`
3123
3554
  Integration`);
3124
- console.log(` Candengo: ${config.candengo_url || "(not set)"}`);
3555
+ console.log(` Server: ${config.candengo_url ? normalizeBaseUrl(config.candengo_url) : "(not set)"}`);
3125
3556
  console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
3126
- const claudeJson = join6(homedir3(), ".claude.json");
3127
- const claudeSettings = join6(homedir3(), ".claude", "settings.json");
3128
- const codexConfig = join6(homedir3(), ".codex", "config.toml");
3129
- const codexHooks = join6(homedir3(), ".codex", "hooks.json");
3130
- const mcpRegistered = existsSync6(claudeJson) && readFileSync6(claudeJson, "utf-8").includes('"engrm"');
3131
- const settingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
3132
- const codexContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
3133
- const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
3134
- const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start");
3135
- const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
3557
+ const claudeJson = join7(homedir4(), ".claude.json");
3558
+ const claudeSettings = join7(homedir4(), ".claude", "settings.json");
3559
+ const codexConfig = join7(homedir4(), ".codex", "config.toml");
3560
+ const codexHooks = join7(homedir4(), ".codex", "hooks.json");
3561
+ const mcpRegistered = existsSync7(claudeJson) && readFileSync7(claudeJson, "utf-8").includes('"engrm"');
3562
+ const settingsContent = existsSync7(claudeSettings) ? readFileSync7(claudeSettings, "utf-8") : "";
3563
+ const codexContent = existsSync7(codexConfig) ? readFileSync7(codexConfig, "utf-8") : "";
3564
+ const codexHooksContent = existsSync7(codexHooks) ? readFileSync7(codexHooks, "utf-8") : "";
3565
+ const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start") || settingsContent.includes("user-prompt-submit");
3566
+ const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`);
3136
3567
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
3137
3568
  let hookCount = 0;
3138
3569
  if (hooksRegistered) {
@@ -3143,7 +3574,7 @@ function handleStatus() {
3143
3574
  if (Array.isArray(entries)) {
3144
3575
  for (const entry of entries) {
3145
3576
  const e = entry;
3146
- 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"))) {
3577
+ 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"))) {
3147
3578
  hookCount++;
3148
3579
  }
3149
3580
  }
@@ -3163,7 +3594,7 @@ function handleStatus() {
3163
3594
  if (config.sentinel.provider) {
3164
3595
  console.log(` Provider: ${config.sentinel.provider}${config.sentinel.model ? ` (${config.sentinel.model})` : ""}`);
3165
3596
  }
3166
- if (existsSync6(getDbPath())) {
3597
+ if (existsSync7(getDbPath())) {
3167
3598
  try {
3168
3599
  const db = new MemDatabase(getDbPath());
3169
3600
  const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
@@ -3176,7 +3607,7 @@ function handleStatus() {
3176
3607
  console.log(`
3177
3608
  Sentinel: disabled`);
3178
3609
  }
3179
- if (existsSync6(getDbPath())) {
3610
+ if (existsSync7(getDbPath())) {
3180
3611
  try {
3181
3612
  const db = new MemDatabase(getDbPath());
3182
3613
  const obsCount = db.getActiveObservationCount();
@@ -3195,6 +3626,31 @@ function handleStatus() {
3195
3626
  } catch {}
3196
3627
  const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
3197
3628
  console.log(` Sessions: ${summaryCount} summarised`);
3629
+ const capture = getCaptureStatus(db, { user_id: config.user_id });
3630
+ console.log(` Raw capture: ${capture.raw_capture_active ? "active" : "observations-only so far"}`);
3631
+ console.log(` Prompts/tools: ${capture.recent_user_prompts}/${capture.recent_tool_events} in last 24h`);
3632
+ console.log(` Hook state: Claude ${capture.claude_hooks_registered ? "ok" : "missing"}, Codex ${capture.codex_hooks_registered ? "ok" : "missing"}`);
3633
+ try {
3634
+ const activeObservations = db.db.query(`SELECT * FROM observations
3635
+ WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
3636
+ const securityFindings = db.db.query(`SELECT * FROM security_findings
3637
+ ORDER BY created_at_epoch DESC
3638
+ LIMIT 500`).all();
3639
+ const signals = computeSessionValueSignals(activeObservations, securityFindings);
3640
+ const signalParts = [
3641
+ `lessons: ${signals.lessons_count}`,
3642
+ `decisions: ${signals.decisions_count}`,
3643
+ `discoveries: ${signals.discoveries_count}`,
3644
+ `features: ${signals.features_count}`
3645
+ ];
3646
+ if (signals.repeated_patterns_count > 0) {
3647
+ signalParts.push(`patterns: ${signals.repeated_patterns_count}`);
3648
+ }
3649
+ console.log(` Value: ${signalParts.join(", ")}`);
3650
+ if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
3651
+ console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ` + `${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
3652
+ }
3653
+ } catch {}
3198
3654
  try {
3199
3655
  const lastSummary = db.db.query(`SELECT request, created_at_epoch FROM session_summaries
3200
3656
  ORDER BY created_at_epoch DESC LIMIT 1`).get();
@@ -3247,8 +3703,8 @@ function handleStatus() {
3247
3703
  Files`);
3248
3704
  console.log(` Config: ${getSettingsPath()}`);
3249
3705
  console.log(` Database: ${getDbPath()}`);
3250
- console.log(` Codex config: ${join6(homedir3(), ".codex", "config.toml")}`);
3251
- console.log(` Codex hooks: ${join6(homedir3(), ".codex", "hooks.json")}`);
3706
+ console.log(` Codex config: ${join7(homedir4(), ".codex", "config.toml")}`);
3707
+ console.log(` Codex hooks: ${join7(homedir4(), ".codex", "hooks.json")}`);
3252
3708
  }
3253
3709
  function formatTimeAgo(epoch) {
3254
3710
  const ago = Math.floor(Date.now() / 1000) - epoch;
@@ -3270,7 +3726,7 @@ function formatSyncTime(epochStr) {
3270
3726
  }
3271
3727
  function ensureConfigDir() {
3272
3728
  const dir = getConfigDir();
3273
- if (!existsSync6(dir)) {
3729
+ if (!existsSync7(dir)) {
3274
3730
  mkdirSync3(dir, { recursive: true });
3275
3731
  }
3276
3732
  }
@@ -3291,7 +3747,7 @@ function generateDeviceId2() {
3291
3747
  break;
3292
3748
  }
3293
3749
  const material = `${host}:${mac || "no-mac"}`;
3294
- const suffix = createHash2("sha256").update(material).digest("hex").slice(0, 8);
3750
+ const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
3295
3751
  return `${host}-${suffix}`;
3296
3752
  }
3297
3753
  async function handleInstallPack(flags) {
@@ -3425,7 +3881,7 @@ async function handleDoctor() {
3425
3881
  return;
3426
3882
  }
3427
3883
  try {
3428
- const currentVersion = getSchemaVersion(db.db);
3884
+ const currentVersion = getSchemaVersion2(db.db);
3429
3885
  if (currentVersion >= LATEST_SCHEMA_VERSION2) {
3430
3886
  pass(`Database schema is current (v${currentVersion})`);
3431
3887
  } else {
@@ -3434,10 +3890,10 @@ async function handleDoctor() {
3434
3890
  } catch {
3435
3891
  warn("Could not check database schema version");
3436
3892
  }
3437
- const claudeJson = join6(homedir3(), ".claude.json");
3893
+ const claudeJson = join7(homedir4(), ".claude.json");
3438
3894
  try {
3439
- if (existsSync6(claudeJson)) {
3440
- const content = readFileSync6(claudeJson, "utf-8");
3895
+ if (existsSync7(claudeJson)) {
3896
+ const content = readFileSync7(claudeJson, "utf-8");
3441
3897
  if (content.includes('"engrm"')) {
3442
3898
  pass("MCP server registered in Claude Code");
3443
3899
  } else {
@@ -3449,10 +3905,10 @@ async function handleDoctor() {
3449
3905
  } catch {
3450
3906
  warn("Could not check MCP server registration");
3451
3907
  }
3452
- const claudeSettings = join6(homedir3(), ".claude", "settings.json");
3908
+ const claudeSettings = join7(homedir4(), ".claude", "settings.json");
3453
3909
  try {
3454
- if (existsSync6(claudeSettings)) {
3455
- const content = readFileSync6(claudeSettings, "utf-8");
3910
+ if (existsSync7(claudeSettings)) {
3911
+ const content = readFileSync7(claudeSettings, "utf-8");
3456
3912
  let hookCount = 0;
3457
3913
  try {
3458
3914
  const settings = JSON.parse(content);
@@ -3461,7 +3917,7 @@ async function handleDoctor() {
3461
3917
  if (Array.isArray(entries)) {
3462
3918
  for (const entry of entries) {
3463
3919
  const e = entry;
3464
- 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"))) {
3920
+ 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"))) {
3465
3921
  hookCount++;
3466
3922
  }
3467
3923
  }
@@ -3479,11 +3935,11 @@ async function handleDoctor() {
3479
3935
  } catch {
3480
3936
  warn("Could not check hooks registration");
3481
3937
  }
3482
- const codexConfig = join6(homedir3(), ".codex", "config.toml");
3938
+ const codexConfig = join7(homedir4(), ".codex", "config.toml");
3483
3939
  try {
3484
- if (existsSync6(codexConfig)) {
3485
- const content = readFileSync6(codexConfig, "utf-8");
3486
- if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`)) {
3940
+ if (existsSync7(codexConfig)) {
3941
+ const content = readFileSync7(codexConfig, "utf-8");
3942
+ if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`)) {
3487
3943
  pass("MCP server registered in Codex");
3488
3944
  } else {
3489
3945
  warn("MCP server not registered in Codex");
@@ -3494,10 +3950,10 @@ async function handleDoctor() {
3494
3950
  } catch {
3495
3951
  warn("Could not check Codex MCP registration");
3496
3952
  }
3497
- const codexHooks = join6(homedir3(), ".codex", "hooks.json");
3953
+ const codexHooks = join7(homedir4(), ".codex", "hooks.json");
3498
3954
  try {
3499
- if (existsSync6(codexHooks)) {
3500
- const content = readFileSync6(codexHooks, "utf-8");
3955
+ if (existsSync7(codexHooks)) {
3956
+ const content = readFileSync7(codexHooks, "utf-8");
3501
3957
  if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
3502
3958
  pass("Hooks registered in Codex");
3503
3959
  } else {
@@ -3511,14 +3967,23 @@ async function handleDoctor() {
3511
3967
  }
3512
3968
  if (config.candengo_url) {
3513
3969
  try {
3970
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
3514
3971
  const controller = new AbortController;
3515
3972
  const timeout = setTimeout(() => controller.abort(), 5000);
3516
3973
  const start = Date.now();
3517
- const res = await fetch(`${config.candengo_url}/health`, { signal: controller.signal });
3974
+ let res = await fetch(`${baseUrl}/health`, { signal: controller.signal });
3975
+ if (res.status === 404) {
3976
+ res = await fetch(`${baseUrl}/v1/mem/provision`, {
3977
+ method: "POST",
3978
+ headers: { "Content-Type": "application/json" },
3979
+ body: "{}",
3980
+ signal: controller.signal
3981
+ });
3982
+ }
3518
3983
  clearTimeout(timeout);
3519
3984
  const elapsed = Date.now() - start;
3520
- if (res.ok) {
3521
- const host = new URL(config.candengo_url).hostname;
3985
+ if (res.ok || res.status === 400) {
3986
+ const host = new URL(baseUrl).hostname;
3522
3987
  pass(`Server connectivity (${host}, ${elapsed}ms)`);
3523
3988
  } else {
3524
3989
  fail(`Server returned HTTP ${res.status}`);
@@ -3532,16 +3997,16 @@ async function handleDoctor() {
3532
3997
  }
3533
3998
  if (config.candengo_url && config.candengo_api_key) {
3534
3999
  try {
4000
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
3535
4001
  const controller = new AbortController;
3536
4002
  const timeout = setTimeout(() => controller.abort(), 5000);
3537
- const res = await fetch(`${config.candengo_url}/v1/account/me`, {
4003
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
3538
4004
  headers: { Authorization: `Bearer ${config.candengo_api_key}` },
3539
4005
  signal: controller.signal
3540
4006
  });
3541
4007
  clearTimeout(timeout);
3542
4008
  if (res.ok) {
3543
- const data = await res.json();
3544
- const email = data.email ?? config.user_email ?? "unknown";
4009
+ const email = config.user_email ?? "configured";
3545
4010
  pass(`Authentication valid (${email})`);
3546
4011
  } else if (res.status === 401 || res.status === 403) {
3547
4012
  fail("Authentication failed \u2014 API key may be expired");
@@ -3590,9 +4055,21 @@ async function handleDoctor() {
3590
4055
  } catch {
3591
4056
  warn("Could not count observations");
3592
4057
  }
4058
+ try {
4059
+ const capture = getCaptureStatus(db, { user_id: config.user_id });
4060
+ if (capture.raw_capture_active) {
4061
+ pass(`Raw chronology active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h)`);
4062
+ } else if (capture.claude_hooks_registered || capture.codex_hooks_registered) {
4063
+ warn("Hooks are registered, but no raw prompt/tool chronology has been captured in the last 24h");
4064
+ } else {
4065
+ warn("Raw chronology inactive \u2014 hook registration is incomplete");
4066
+ }
4067
+ } catch {
4068
+ warn("Could not check raw chronology capture");
4069
+ }
3593
4070
  try {
3594
4071
  const dbPath = getDbPath();
3595
- if (existsSync6(dbPath)) {
4072
+ if (existsSync7(dbPath)) {
3596
4073
  const stats = statSync(dbPath);
3597
4074
  const sizeMB = stats.size / (1024 * 1024);
3598
4075
  const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
@@ -3660,11 +4137,11 @@ Registering with Claude Code and Codex...`);
3660
4137
  console.log(`
3661
4138
  Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
3662
4139
  } catch (error) {
3663
- const packageRoot = join6(THIS_DIR, "..");
4140
+ const packageRoot = join7(THIS_DIR, "..");
3664
4141
  const runtime = IS_BUILT_DIST ? process.execPath : "bun";
3665
- const serverArgs = IS_BUILT_DIST ? [join6(packageRoot, "dist", "server.js")] : ["run", join6(packageRoot, "src", "server.ts")];
3666
- const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join6(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join6(packageRoot, "hooks", "session-start.ts")}`;
3667
- const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join6(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join6(packageRoot, "hooks", "codex-stop.ts")}`;
4142
+ const serverArgs = IS_BUILT_DIST ? [join7(packageRoot, "dist", "server.js")] : ["run", join7(packageRoot, "src", "server.ts")];
4143
+ const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join7(packageRoot, "hooks", "session-start.ts")}`;
4144
+ const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join7(packageRoot, "hooks", "codex-stop.ts")}`;
3668
4145
  console.log(`
3669
4146
  Could not auto-register with Claude Code and Codex.`);
3670
4147
  console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);