engrm 0.4.7 → 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) {
@@ -1410,6 +1580,64 @@ var MIGRATIONS2 = [
1410
1580
  );
1411
1581
  INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
1412
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
+ `
1413
1641
  }
1414
1642
  ];
1415
1643
  function isVecExtensionLoaded2(db) {
@@ -1420,14 +1648,14 @@ function isVecExtensionLoaded2(db) {
1420
1648
  return false;
1421
1649
  }
1422
1650
  }
1423
- function getSchemaVersion(db) {
1651
+ function getSchemaVersion2(db) {
1424
1652
  const result = db.query("PRAGMA user_version").get();
1425
1653
  return result.user_version;
1426
1654
  }
1427
1655
  var LATEST_SCHEMA_VERSION2 = MIGRATIONS2.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
1428
1656
 
1429
1657
  // src/provisioning/provision.ts
1430
- var DEFAULT_CANDENGO_URL = "https://www.candengo.com";
1658
+ var DEFAULT_CANDENGO_URL = "https://engrm.dev";
1431
1659
 
1432
1660
  class ProvisionError extends Error {
1433
1661
  status;
@@ -1819,6 +2047,7 @@ function registerHooks() {
1819
2047
  return [runtime, ...runArg, join2(hooksDir, `${name}${ext}`)].join(" ");
1820
2048
  }
1821
2049
  const sessionStartCmd = hookCmd("session-start");
2050
+ const userPromptSubmitCmd = hookCmd("user-prompt-submit");
1822
2051
  const preCompactCmd = hookCmd("pre-compact");
1823
2052
  const preToolUseCmd = hookCmd("sentinel");
1824
2053
  const postToolUseCmd = hookCmd("post-tool-use");
@@ -1827,6 +2056,7 @@ function registerHooks() {
1827
2056
  const settings = readJsonFile(CLAUDE_SETTINGS);
1828
2057
  const hooks = settings["hooks"] ?? {};
1829
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");
1830
2060
  hooks["PreCompact"] = replaceEngrmHook(hooks["PreCompact"], { hooks: [{ type: "command", command: preCompactCmd }] }, "pre-compact");
1831
2061
  hooks["PreToolUse"] = replaceEngrmHook(hooks["PreToolUse"], {
1832
2062
  matcher: "Edit|Write",
@@ -2813,8 +3043,103 @@ async function installRulePacks(db, config, packNames) {
2813
3043
  return { installed, skipped };
2814
3044
  }
2815
3045
 
2816
- // 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";
2817
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"}`;
2818
3143
  var args = process.argv.slice(2);
2819
3144
  var command = args[0];
2820
3145
  var THIS_DIR = dirname4(fileURLToPath4(import.meta.url));
@@ -3041,13 +3366,13 @@ function writeConfigFromProvision(baseUrl, result) {
3041
3366
  console.log(`Database initialised at ${getDbPath()}`);
3042
3367
  }
3043
3368
  function initFromFile(configPath) {
3044
- if (!existsSync6(configPath)) {
3369
+ if (!existsSync7(configPath)) {
3045
3370
  console.error(`Config file not found: ${configPath}`);
3046
3371
  process.exit(1);
3047
3372
  }
3048
3373
  let parsed;
3049
3374
  try {
3050
- const raw = readFileSync6(configPath, "utf-8");
3375
+ const raw = readFileSync7(configPath, "utf-8");
3051
3376
  parsed = JSON.parse(raw);
3052
3377
  } catch {
3053
3378
  console.error(`Invalid JSON in ${configPath}`);
@@ -3134,7 +3459,7 @@ async function initManual() {
3134
3459
  return;
3135
3460
  }
3136
3461
  }
3137
- 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): ");
3138
3463
  const apiKey = await prompt("API key (cvk_...): ");
3139
3464
  const siteId = await prompt("Site ID: ");
3140
3465
  const namespace = await prompt("Namespace: ");
@@ -3227,18 +3552,18 @@ function handleStatus() {
3227
3552
  console.log(` Plan: ${tierLabels[tier] ?? tier}`);
3228
3553
  console.log(`
3229
3554
  Integration`);
3230
- console.log(` Candengo: ${config.candengo_url || "(not set)"}`);
3555
+ console.log(` Server: ${config.candengo_url ? normalizeBaseUrl(config.candengo_url) : "(not set)"}`);
3231
3556
  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}]`);
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}]`);
3242
3567
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
3243
3568
  let hookCount = 0;
3244
3569
  if (hooksRegistered) {
@@ -3249,7 +3574,7 @@ function handleStatus() {
3249
3574
  if (Array.isArray(entries)) {
3250
3575
  for (const entry of entries) {
3251
3576
  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"))) {
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"))) {
3253
3578
  hookCount++;
3254
3579
  }
3255
3580
  }
@@ -3269,7 +3594,7 @@ function handleStatus() {
3269
3594
  if (config.sentinel.provider) {
3270
3595
  console.log(` Provider: ${config.sentinel.provider}${config.sentinel.model ? ` (${config.sentinel.model})` : ""}`);
3271
3596
  }
3272
- if (existsSync6(getDbPath())) {
3597
+ if (existsSync7(getDbPath())) {
3273
3598
  try {
3274
3599
  const db = new MemDatabase(getDbPath());
3275
3600
  const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
@@ -3282,7 +3607,7 @@ function handleStatus() {
3282
3607
  console.log(`
3283
3608
  Sentinel: disabled`);
3284
3609
  }
3285
- if (existsSync6(getDbPath())) {
3610
+ if (existsSync7(getDbPath())) {
3286
3611
  try {
3287
3612
  const db = new MemDatabase(getDbPath());
3288
3613
  const obsCount = db.getActiveObservationCount();
@@ -3301,6 +3626,10 @@ function handleStatus() {
3301
3626
  } catch {}
3302
3627
  const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
3303
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"}`);
3304
3633
  try {
3305
3634
  const activeObservations = db.db.query(`SELECT * FROM observations
3306
3635
  WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
@@ -3374,8 +3703,8 @@ function handleStatus() {
3374
3703
  Files`);
3375
3704
  console.log(` Config: ${getSettingsPath()}`);
3376
3705
  console.log(` Database: ${getDbPath()}`);
3377
- console.log(` Codex config: ${join6(homedir3(), ".codex", "config.toml")}`);
3378
- 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")}`);
3379
3708
  }
3380
3709
  function formatTimeAgo(epoch) {
3381
3710
  const ago = Math.floor(Date.now() / 1000) - epoch;
@@ -3397,7 +3726,7 @@ function formatSyncTime(epochStr) {
3397
3726
  }
3398
3727
  function ensureConfigDir() {
3399
3728
  const dir = getConfigDir();
3400
- if (!existsSync6(dir)) {
3729
+ if (!existsSync7(dir)) {
3401
3730
  mkdirSync3(dir, { recursive: true });
3402
3731
  }
3403
3732
  }
@@ -3418,7 +3747,7 @@ function generateDeviceId2() {
3418
3747
  break;
3419
3748
  }
3420
3749
  const material = `${host}:${mac || "no-mac"}`;
3421
- const suffix = createHash2("sha256").update(material).digest("hex").slice(0, 8);
3750
+ const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
3422
3751
  return `${host}-${suffix}`;
3423
3752
  }
3424
3753
  async function handleInstallPack(flags) {
@@ -3552,7 +3881,7 @@ async function handleDoctor() {
3552
3881
  return;
3553
3882
  }
3554
3883
  try {
3555
- const currentVersion = getSchemaVersion(db.db);
3884
+ const currentVersion = getSchemaVersion2(db.db);
3556
3885
  if (currentVersion >= LATEST_SCHEMA_VERSION2) {
3557
3886
  pass(`Database schema is current (v${currentVersion})`);
3558
3887
  } else {
@@ -3561,10 +3890,10 @@ async function handleDoctor() {
3561
3890
  } catch {
3562
3891
  warn("Could not check database schema version");
3563
3892
  }
3564
- const claudeJson = join6(homedir3(), ".claude.json");
3893
+ const claudeJson = join7(homedir4(), ".claude.json");
3565
3894
  try {
3566
- if (existsSync6(claudeJson)) {
3567
- const content = readFileSync6(claudeJson, "utf-8");
3895
+ if (existsSync7(claudeJson)) {
3896
+ const content = readFileSync7(claudeJson, "utf-8");
3568
3897
  if (content.includes('"engrm"')) {
3569
3898
  pass("MCP server registered in Claude Code");
3570
3899
  } else {
@@ -3576,10 +3905,10 @@ async function handleDoctor() {
3576
3905
  } catch {
3577
3906
  warn("Could not check MCP server registration");
3578
3907
  }
3579
- const claudeSettings = join6(homedir3(), ".claude", "settings.json");
3908
+ const claudeSettings = join7(homedir4(), ".claude", "settings.json");
3580
3909
  try {
3581
- if (existsSync6(claudeSettings)) {
3582
- const content = readFileSync6(claudeSettings, "utf-8");
3910
+ if (existsSync7(claudeSettings)) {
3911
+ const content = readFileSync7(claudeSettings, "utf-8");
3583
3912
  let hookCount = 0;
3584
3913
  try {
3585
3914
  const settings = JSON.parse(content);
@@ -3588,7 +3917,7 @@ async function handleDoctor() {
3588
3917
  if (Array.isArray(entries)) {
3589
3918
  for (const entry of entries) {
3590
3919
  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"))) {
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"))) {
3592
3921
  hookCount++;
3593
3922
  }
3594
3923
  }
@@ -3606,11 +3935,11 @@ async function handleDoctor() {
3606
3935
  } catch {
3607
3936
  warn("Could not check hooks registration");
3608
3937
  }
3609
- const codexConfig = join6(homedir3(), ".codex", "config.toml");
3938
+ const codexConfig = join7(homedir4(), ".codex", "config.toml");
3610
3939
  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}]`)) {
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}]`)) {
3614
3943
  pass("MCP server registered in Codex");
3615
3944
  } else {
3616
3945
  warn("MCP server not registered in Codex");
@@ -3621,10 +3950,10 @@ async function handleDoctor() {
3621
3950
  } catch {
3622
3951
  warn("Could not check Codex MCP registration");
3623
3952
  }
3624
- const codexHooks = join6(homedir3(), ".codex", "hooks.json");
3953
+ const codexHooks = join7(homedir4(), ".codex", "hooks.json");
3625
3954
  try {
3626
- if (existsSync6(codexHooks)) {
3627
- const content = readFileSync6(codexHooks, "utf-8");
3955
+ if (existsSync7(codexHooks)) {
3956
+ const content = readFileSync7(codexHooks, "utf-8");
3628
3957
  if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
3629
3958
  pass("Hooks registered in Codex");
3630
3959
  } else {
@@ -3638,14 +3967,23 @@ async function handleDoctor() {
3638
3967
  }
3639
3968
  if (config.candengo_url) {
3640
3969
  try {
3970
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
3641
3971
  const controller = new AbortController;
3642
3972
  const timeout = setTimeout(() => controller.abort(), 5000);
3643
3973
  const start = Date.now();
3644
- 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
+ }
3645
3983
  clearTimeout(timeout);
3646
3984
  const elapsed = Date.now() - start;
3647
- if (res.ok) {
3648
- const host = new URL(config.candengo_url).hostname;
3985
+ if (res.ok || res.status === 400) {
3986
+ const host = new URL(baseUrl).hostname;
3649
3987
  pass(`Server connectivity (${host}, ${elapsed}ms)`);
3650
3988
  } else {
3651
3989
  fail(`Server returned HTTP ${res.status}`);
@@ -3659,16 +3997,16 @@ async function handleDoctor() {
3659
3997
  }
3660
3998
  if (config.candengo_url && config.candengo_api_key) {
3661
3999
  try {
4000
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
3662
4001
  const controller = new AbortController;
3663
4002
  const timeout = setTimeout(() => controller.abort(), 5000);
3664
- const res = await fetch(`${config.candengo_url}/v1/account/me`, {
4003
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
3665
4004
  headers: { Authorization: `Bearer ${config.candengo_api_key}` },
3666
4005
  signal: controller.signal
3667
4006
  });
3668
4007
  clearTimeout(timeout);
3669
4008
  if (res.ok) {
3670
- const data = await res.json();
3671
- const email = data.email ?? config.user_email ?? "unknown";
4009
+ const email = config.user_email ?? "configured";
3672
4010
  pass(`Authentication valid (${email})`);
3673
4011
  } else if (res.status === 401 || res.status === 403) {
3674
4012
  fail("Authentication failed \u2014 API key may be expired");
@@ -3717,9 +4055,21 @@ async function handleDoctor() {
3717
4055
  } catch {
3718
4056
  warn("Could not count observations");
3719
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
+ }
3720
4070
  try {
3721
4071
  const dbPath = getDbPath();
3722
- if (existsSync6(dbPath)) {
4072
+ if (existsSync7(dbPath)) {
3723
4073
  const stats = statSync(dbPath);
3724
4074
  const sizeMB = stats.size / (1024 * 1024);
3725
4075
  const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
@@ -3787,11 +4137,11 @@ Registering with Claude Code and Codex...`);
3787
4137
  console.log(`
3788
4138
  Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
3789
4139
  } catch (error) {
3790
- const packageRoot = join6(THIS_DIR, "..");
4140
+ const packageRoot = join7(THIS_DIR, "..");
3791
4141
  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")}`;
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")}`;
3795
4145
  console.log(`
3796
4146
  Could not auto-register with Claude Code and Codex.`);
3797
4147
  console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);