engrm 0.4.0 → 0.4.3

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/README.md CHANGED
@@ -2,17 +2,21 @@
2
2
 
3
3
  **Cross-device memory for AI coding agents.** Every session remembers what you learned yesterday — on any machine, for every team member.
4
4
 
5
+ For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installed.
6
+
5
7
  ```
6
8
  npx engrm init
7
9
  ```
8
10
 
11
+ Public beta. The current source of truth for agent capability differences is [AGENT_SUPPORT.md](AGENT_SUPPORT.md).
12
+
9
13
  ---
10
14
 
11
15
  ## What It Does
12
16
 
13
17
  Your AI agent forgets everything between sessions. Engrm fixes that.
14
18
 
15
- - **Captures automatically** — hooks into Claude Code to record discoveries, bugfixes, decisions, and patterns as you work
19
+ - **Captures automatically** — hooks into Claude Code, and into Codex where its public hook surface allows
16
20
  - **Remembers across devices** — fix a bug on your laptop, continue on your desktop with full context
17
21
  - **Shares with your team** — one developer's hard-won insight becomes everyone's knowledge
18
22
  - **Works offline** — local SQLite is the source of truth; syncs when connected
@@ -30,7 +34,7 @@ Visit [engrm.dev](https://engrm.dev) and create an account.
30
34
  npx engrm init
31
35
  ```
32
36
 
33
- This opens your browser for authentication, writes config to `~/.engrm/`, and registers the MCP server + hooks in Claude Code. Takes about 30 seconds.
37
+ This opens your browser for authentication, writes config to `~/.engrm/`, and registers Engrm in both Claude Code and Codex when those configs are available. Takes about 30 seconds.
34
38
 
35
39
  **Alternative methods:**
36
40
  ```bash
@@ -44,12 +48,12 @@ npx engrm init --url=https://vector.internal.company.com
44
48
  npx engrm init --manual
45
49
  ```
46
50
 
47
- ### 3. Use Claude Code normally
51
+ ### 3. Use your agent normally
48
52
 
49
53
  That's it. Engrm works in the background:
50
54
 
51
55
  - **Session start** — injects relevant project memory into context
52
- - **While you work** — captures observations from tool use (edits, writes, commands)
56
+ - **While you work** — captures observations from tool use where the agent exposes that hook surface
53
57
  - **Session end** — generates a session digest, syncs to cloud, shows summary
54
58
 
55
59
  ```
@@ -79,7 +83,9 @@ Engrm Status
79
83
  Plan: Pro ($15/mo)
80
84
  Candengo: https://www.candengo.com
81
85
  MCP server: registered
86
+ Codex MCP: registered
82
87
  Hooks: registered (6 hooks)
88
+ Codex hooks: registered (2 hooks)
83
89
 
84
90
  Observations: 1,247 active
85
91
  By type: change: 412, discovery: 289, bugfix: 187, ...
@@ -117,6 +123,28 @@ Claude Code session
117
123
  Available on all your devices + team members
118
124
  ```
119
125
 
126
+ ```
127
+ Codex session
128
+
129
+ ├─ SessionStart hook ──→ inject relevant memory into context
130
+
131
+ ├─ MCP tools ──────────→ search, save, inspect, message, stats
132
+
133
+ └─ Stop hook ──────────→ session digest + sync + summary
134
+ ```
135
+
136
+ ### Agent Support
137
+
138
+ | Capability | Claude Code | Codex |
139
+ |---|---|---|
140
+ | MCP server tools | ✓ | ✓ |
141
+ | Session-start context injection | ✓ | ✓ |
142
+ | Stop/session summary hook | ✓ | ✓ |
143
+ | Per-tool automatic capture | ✓ | Partial via MCP/manual flows only |
144
+ | Pre-write Sentinel hook | ✓ | Not yet exposed by Codex public hooks |
145
+ | Pre-compact reinjection | ✓ | Not exposed |
146
+ | ElicitationResult capture | ✓ | Not exposed |
147
+
120
148
  ### MCP Tools
121
149
 
122
150
  The MCP server exposes tools that Claude can call directly:
@@ -128,6 +156,9 @@ The MCP server exposes tools that Claude can call directly:
128
156
  | `get_observations` | Fetch full details by ID |
129
157
  | `save_observation` | Manually save something worth remembering |
130
158
  | `install_pack` | Load a curated knowledge pack for your stack |
159
+ | `send_message` | Leave a cross-device or team note |
160
+ | `recent_activity` | Inspect what Engrm captured most recently |
161
+ | `memory_stats` | View high-level capture and sync health |
131
162
 
132
163
  ### Observation Types
133
164
 
@@ -227,11 +258,13 @@ Place in your project root to override project identity for non-git projects:
227
258
  }
228
259
  ```
229
260
 
230
- ### Claude Code integration
261
+ ### Agent integration
231
262
 
232
263
  Engrm auto-registers in:
233
264
  - `~/.claude.json` — MCP server (`engrm`)
234
265
  - `~/.claude/settings.json` — 6 lifecycle hooks
266
+ - `~/.codex/config.toml` — MCP server (`engrm`) + `codex_hooks` feature flag
267
+ - `~/.codex/hooks.json` — `SessionStart` and `Stop` hooks
235
268
 
236
269
  ---
237
270
 
@@ -259,4 +292,15 @@ See [LICENSE](LICENSE) for full terms.
259
292
 
260
293
  ---
261
294
 
295
+ ## Project
296
+
297
+ - Architecture: [ARCHITECTURE.md](ARCHITECTURE.md)
298
+ - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
299
+ - Security: [SECURITY.md](SECURITY.md)
300
+ - Roadmap: [ROADMAP.md](ROADMAP.md)
301
+
302
+ Maintainers: run `node scripts/check-public-docs.mjs` to verify the repo only contains the approved public docs set at the root.
303
+
304
+ ---
305
+
262
306
  Built by the [Engrm](https://engrm.dev) team, powered by [Candengo Vector](https://www.candengo.com).
package/dist/cli.js CHANGED
@@ -20,8 +20,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
  // src/cli.ts
21
21
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, statSync } from "fs";
22
22
  import { hostname as hostname2, homedir as homedir3, networkInterfaces as networkInterfaces2 } from "os";
23
- import { join as join6 } from "path";
23
+ import { dirname as dirname4, join as join6 } from "path";
24
24
  import { createHash as createHash2 } from "crypto";
25
+ import { fileURLToPath as fileURLToPath4 } from "url";
25
26
 
26
27
  // src/config.ts
27
28
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -726,8 +727,8 @@ class MemDatabase {
726
727
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(id) ?? null;
727
728
  }
728
729
  insertObservation(obs) {
729
- const now = Math.floor(Date.now() / 1000);
730
- const createdAt = new Date().toISOString();
730
+ const now = obs.created_at_epoch ?? Math.floor(Date.now() / 1000);
731
+ const createdAt = obs.created_at ?? new Date(now * 1000).toISOString();
731
732
  const result = this.db.query(`INSERT INTO observations (
732
733
  session_id, project_id, type, title, narrative, facts, concepts,
733
734
  files_read, files_modified, quality, lifecycle, sensitivity,
@@ -744,11 +745,14 @@ class MemDatabase {
744
745
  getObservationById(id) {
745
746
  return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
746
747
  }
747
- getObservationsByIds(ids) {
748
+ getObservationsByIds(ids, userId) {
748
749
  if (ids.length === 0)
749
750
  return [];
750
751
  const placeholders = ids.map(() => "?").join(",");
751
- return this.db.query(`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`).all(...ids);
752
+ const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
753
+ return this.db.query(`SELECT * FROM observations
754
+ WHERE id IN (${placeholders})${visibilityClause}
755
+ ORDER BY created_at_epoch DESC`).all(...ids, ...userId ? [userId] : []);
752
756
  }
753
757
  getRecentObservations(projectId, sincEpoch, limit = 50) {
754
758
  return this.db.query(`SELECT * FROM observations
@@ -756,8 +760,9 @@ class MemDatabase {
756
760
  ORDER BY created_at_epoch DESC
757
761
  LIMIT ?`).all(projectId, sincEpoch, limit);
758
762
  }
759
- searchFts(query, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20) {
763
+ searchFts(query, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20, userId) {
760
764
  const lifecyclePlaceholders = lifecycles.map(() => "?").join(",");
765
+ const visibilityClause = userId ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
761
766
  if (projectId !== null) {
762
767
  return this.db.query(`SELECT o.id, observations_fts.rank
763
768
  FROM observations_fts
@@ -765,33 +770,39 @@ class MemDatabase {
765
770
  WHERE observations_fts MATCH ?
766
771
  AND o.project_id = ?
767
772
  AND o.lifecycle IN (${lifecyclePlaceholders})
773
+ ${visibilityClause}
768
774
  ORDER BY observations_fts.rank
769
- LIMIT ?`).all(query, projectId, ...lifecycles, limit);
775
+ LIMIT ?`).all(query, projectId, ...lifecycles, ...userId ? [userId] : [], limit);
770
776
  }
771
777
  return this.db.query(`SELECT o.id, observations_fts.rank
772
778
  FROM observations_fts
773
779
  JOIN observations o ON o.id = observations_fts.rowid
774
780
  WHERE observations_fts MATCH ?
775
781
  AND o.lifecycle IN (${lifecyclePlaceholders})
782
+ ${visibilityClause}
776
783
  ORDER BY observations_fts.rank
777
- LIMIT ?`).all(query, ...lifecycles, limit);
784
+ LIMIT ?`).all(query, ...lifecycles, ...userId ? [userId] : [], limit);
778
785
  }
779
- getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
780
- const anchor = this.getObservationById(anchorId);
786
+ getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, userId) {
787
+ const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
788
+ const anchor = this.db.query(`SELECT * FROM observations WHERE id = ?${visibilityClause}`).get(anchorId, ...userId ? [userId] : []) ?? null;
781
789
  if (!anchor)
782
790
  return [];
783
791
  const projectFilter = projectId !== null ? "AND project_id = ?" : "";
784
792
  const projectParams = projectId !== null ? [projectId] : [];
793
+ const visibilityParams = userId ? [userId] : [];
785
794
  const before = this.db.query(`SELECT * FROM observations
786
795
  WHERE created_at_epoch < ? ${projectFilter}
787
796
  AND lifecycle IN ('active', 'aging', 'pinned')
797
+ ${visibilityClause}
788
798
  ORDER BY created_at_epoch DESC
789
- LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, depthBefore);
799
+ LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, ...visibilityParams, depthBefore);
790
800
  const after = this.db.query(`SELECT * FROM observations
791
801
  WHERE created_at_epoch > ? ${projectFilter}
792
802
  AND lifecycle IN ('active', 'aging', 'pinned')
803
+ ${visibilityClause}
793
804
  ORDER BY created_at_epoch ASC
794
- LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, depthAfter);
805
+ LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, ...visibilityParams, depthAfter);
795
806
  return [...before.reverse(), anchor, ...after];
796
807
  }
797
808
  pinObservation(id, pinned) {
@@ -905,11 +916,12 @@ class MemDatabase {
905
916
  return;
906
917
  this.db.query("DELETE FROM vec_observations WHERE observation_id = ?").run(observationId);
907
918
  }
908
- searchVec(queryEmbedding, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20) {
919
+ searchVec(queryEmbedding, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20, userId) {
909
920
  if (!this.vecAvailable)
910
921
  return [];
911
922
  const lifecyclePlaceholders = lifecycles.map(() => "?").join(",");
912
923
  const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
924
+ const visibilityClause = userId ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
913
925
  if (projectId !== null) {
914
926
  return this.db.query(`SELECT v.observation_id, v.distance
915
927
  FROM vec_observations v
@@ -918,7 +930,7 @@ class MemDatabase {
918
930
  AND k = ?
919
931
  AND o.project_id = ?
920
932
  AND o.lifecycle IN (${lifecyclePlaceholders})
921
- AND o.superseded_by IS NULL`).all(embeddingBlob, limit, projectId, ...lifecycles);
933
+ AND o.superseded_by IS NULL` + visibilityClause).all(embeddingBlob, limit, projectId, ...lifecycles, ...userId ? [userId] : []);
922
934
  }
923
935
  return this.db.query(`SELECT v.observation_id, v.distance
924
936
  FROM vec_observations v
@@ -926,7 +938,7 @@ class MemDatabase {
926
938
  WHERE v.embedding MATCH ?
927
939
  AND k = ?
928
940
  AND o.lifecycle IN (${lifecyclePlaceholders})
929
- AND o.superseded_by IS NULL`).all(embeddingBlob, limit, ...lifecycles);
941
+ AND o.superseded_by IS NULL` + visibilityClause).all(embeddingBlob, limit, ...lifecycles, ...userId ? [userId] : []);
930
942
  }
931
943
  getUnembeddedCount() {
932
944
  if (!this.vecAvailable)
@@ -1580,6 +1592,9 @@ import { join as join2, dirname } from "node:path";
1580
1592
  import { fileURLToPath } from "node:url";
1581
1593
  var CLAUDE_JSON = join2(homedir2(), ".claude.json");
1582
1594
  var CLAUDE_SETTINGS = join2(homedir2(), ".claude", "settings.json");
1595
+ var CODEX_CONFIG = join2(homedir2(), ".codex", "config.toml");
1596
+ var CODEX_HOOKS = join2(homedir2(), ".codex", "hooks.json");
1597
+ var LEGACY_CODEX_SERVER_NAME = `candengo-${"mem"}`;
1583
1598
  function isBuiltDist() {
1584
1599
  const thisDir = dirname(fileURLToPath(import.meta.url));
1585
1600
  return thisDir.endsWith("/dist") || thisDir.endsWith("\\dist");
@@ -1626,6 +1641,15 @@ function writeJsonFile(path, data) {
1626
1641
  writeFileSync2(path, JSON.stringify(data, null, 2) + `
1627
1642
  `, "utf-8");
1628
1643
  }
1644
+ function ensureParentDir(path) {
1645
+ const dir = dirname(path);
1646
+ if (!existsSync2(dir)) {
1647
+ mkdirSync2(dir, { recursive: true });
1648
+ }
1649
+ }
1650
+ function escapeTomlString(value) {
1651
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1652
+ }
1629
1653
  function registerMcpServer() {
1630
1654
  const runtime = findRuntime();
1631
1655
  const root = findPackageRoot();
@@ -1642,6 +1666,123 @@ function registerMcpServer() {
1642
1666
  writeJsonFile(CLAUDE_JSON, config);
1643
1667
  return { path: CLAUDE_JSON, added: true };
1644
1668
  }
1669
+ function upsertCodexMcpServerConfig(existing, entry) {
1670
+ const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
1671
+ const targetHeaders = new Set([
1672
+ `[mcp_servers.${entry.name}]`,
1673
+ `[mcp_servers.${LEGACY_CODEX_SERVER_NAME}]`
1674
+ ]);
1675
+ const output = [];
1676
+ let skipping = false;
1677
+ let inFeatures = false;
1678
+ let featuresInserted = false;
1679
+ for (const line of lines) {
1680
+ const trimmed = line.trim();
1681
+ if (/^\[.*\]$/.test(trimmed)) {
1682
+ if (inFeatures && !featuresInserted) {
1683
+ output.push(`codex_hooks = true`);
1684
+ featuresInserted = true;
1685
+ }
1686
+ skipping = targetHeaders.has(trimmed);
1687
+ inFeatures = trimmed === "[features]";
1688
+ if (!skipping)
1689
+ output.push(line);
1690
+ continue;
1691
+ }
1692
+ if (!skipping) {
1693
+ if (inFeatures && trimmed.startsWith("codex_hooks")) {
1694
+ if (!featuresInserted) {
1695
+ output.push(`codex_hooks = true`);
1696
+ featuresInserted = true;
1697
+ }
1698
+ continue;
1699
+ }
1700
+ output.push(line);
1701
+ }
1702
+ }
1703
+ if (inFeatures && !featuresInserted) {
1704
+ output.push(`codex_hooks = true`);
1705
+ featuresInserted = true;
1706
+ }
1707
+ while (output.length > 0 && output[output.length - 1] === "") {
1708
+ output.pop();
1709
+ }
1710
+ if (output.length > 0) {
1711
+ output.push("");
1712
+ }
1713
+ output.push(`[mcp_servers.${entry.name}]`);
1714
+ output.push(`enabled = true`);
1715
+ output.push(`command = "${escapeTomlString(entry.command)}"`);
1716
+ output.push(`args = [${entry.args.map((arg) => `"${escapeTomlString(arg)}"`).join(", ")}]`);
1717
+ output.push(`startup_timeout_sec = 15`);
1718
+ output.push(`tool_timeout_sec = 30`);
1719
+ output.push("");
1720
+ if (!featuresInserted) {
1721
+ output.push(`[features]`);
1722
+ output.push(`codex_hooks = true`);
1723
+ output.push("");
1724
+ }
1725
+ return output.join(`
1726
+ `);
1727
+ }
1728
+ function registerCodexMcpServer() {
1729
+ const runtime = findRuntime();
1730
+ const root = findPackageRoot();
1731
+ const dist = isBuiltDist();
1732
+ const serverPath = dist ? join2(root, "dist", "server.js") : join2(root, "src", "server.ts");
1733
+ const args = dist ? [serverPath] : ["run", serverPath];
1734
+ const existing = existsSync2(CODEX_CONFIG) ? readFileSync2(CODEX_CONFIG, "utf-8") : "";
1735
+ const updated = upsertCodexMcpServerConfig(existing, {
1736
+ name: "engrm",
1737
+ command: runtime,
1738
+ args
1739
+ });
1740
+ ensureParentDir(CODEX_CONFIG);
1741
+ writeFileSync2(CODEX_CONFIG, updated, "utf-8");
1742
+ return { path: CODEX_CONFIG, added: true };
1743
+ }
1744
+ function buildCodexHooksConfig(sessionStartCommand, stopCommand) {
1745
+ return JSON.stringify({
1746
+ hooks: {
1747
+ SessionStart: [
1748
+ {
1749
+ hooks: [
1750
+ {
1751
+ type: "command",
1752
+ command: sessionStartCommand,
1753
+ statusMessage: "loading Engrm context"
1754
+ }
1755
+ ]
1756
+ }
1757
+ ],
1758
+ Stop: [
1759
+ {
1760
+ hooks: [
1761
+ {
1762
+ type: "command",
1763
+ command: stopCommand,
1764
+ statusMessage: "saving Engrm session summary"
1765
+ }
1766
+ ]
1767
+ }
1768
+ ]
1769
+ }
1770
+ }, null, 2) + `
1771
+ `;
1772
+ }
1773
+ function registerCodexHooks() {
1774
+ const runtime = findRuntime();
1775
+ const root = findPackageRoot();
1776
+ const dist = isBuiltDist();
1777
+ const hooksDir = dist ? join2(root, "dist", "hooks") : join2(root, "hooks");
1778
+ const ext = dist ? ".js" : ".ts";
1779
+ const runArg = dist ? [] : ["run"];
1780
+ const commandFor = (name) => [runtime, ...runArg, join2(hooksDir, `${name}${ext}`)].join(" ");
1781
+ const content = buildCodexHooksConfig(commandFor("session-start"), commandFor("codex-stop"));
1782
+ ensureParentDir(CODEX_HOOKS);
1783
+ writeFileSync2(CODEX_HOOKS, content, "utf-8");
1784
+ return { path: CODEX_HOOKS, added: true };
1785
+ }
1645
1786
  function registerHooks() {
1646
1787
  const runtime = findRuntime();
1647
1788
  const root = findPackageRoot();
@@ -1686,9 +1827,27 @@ function replaceEngrmHook(existing, newEntry, hookFilename) {
1686
1827
  return [...others, newEntry];
1687
1828
  }
1688
1829
  function registerAll() {
1830
+ let mcp = { path: CLAUDE_JSON, added: false };
1831
+ let hooks = { path: CLAUDE_SETTINGS, added: false };
1832
+ let codex = { path: CODEX_CONFIG, added: false };
1833
+ let codexHooks = { path: CODEX_HOOKS, added: false };
1834
+ try {
1835
+ mcp = registerMcpServer();
1836
+ } catch {}
1837
+ try {
1838
+ hooks = registerHooks();
1839
+ } catch {}
1840
+ try {
1841
+ codex = registerCodexMcpServer();
1842
+ } catch {}
1843
+ try {
1844
+ codexHooks = registerCodexHooks();
1845
+ } catch {}
1689
1846
  return {
1690
- mcp: registerMcpServer(),
1691
- hooks: registerHooks()
1847
+ mcp,
1848
+ hooks,
1849
+ codex,
1850
+ codexHooks
1692
1851
  };
1693
1852
  }
1694
1853
 
@@ -1862,6 +2021,12 @@ function scoreQuality(input) {
1862
2021
  case "digest":
1863
2022
  score += 0.3;
1864
2023
  break;
2024
+ case "standard":
2025
+ score += 0.25;
2026
+ break;
2027
+ case "message":
2028
+ score += 0.1;
2029
+ break;
1865
2030
  }
1866
2031
  if (input.narrative && input.narrative.length > 50) {
1867
2032
  score += 0.15;
@@ -2543,8 +2708,11 @@ async function installRulePacks(db, config, packNames) {
2543
2708
  }
2544
2709
 
2545
2710
  // src/cli.ts
2711
+ var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
2546
2712
  var args = process.argv.slice(2);
2547
2713
  var command = args[0];
2714
+ var THIS_DIR = dirname4(fileURLToPath4(import.meta.url));
2715
+ var IS_BUILT_DIST = THIS_DIR.endsWith("/dist") || THIS_DIR.endsWith("\\dist");
2548
2716
  switch (command) {
2549
2717
  case "init":
2550
2718
  await handleInit(args.slice(1));
@@ -2957,9 +3125,15 @@ function handleStatus() {
2957
3125
  console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
2958
3126
  const claudeJson = join6(homedir3(), ".claude.json");
2959
3127
  const claudeSettings = join6(homedir3(), ".claude", "settings.json");
3128
+ const codexConfig = join6(homedir3(), ".codex", "config.toml");
3129
+ const codexHooks = join6(homedir3(), ".codex", "hooks.json");
2960
3130
  const mcpRegistered = existsSync6(claudeJson) && readFileSync6(claudeJson, "utf-8").includes('"engrm"');
2961
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") : "";
2962
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}]`);
3136
+ const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
2963
3137
  let hookCount = 0;
2964
3138
  if (hooksRegistered) {
2965
3139
  try {
@@ -2978,7 +3152,9 @@ function handleStatus() {
2978
3152
  } catch {}
2979
3153
  }
2980
3154
  console.log(` MCP server: ${mcpRegistered ? "registered" : "not registered"}`);
3155
+ console.log(` Codex MCP: ${codexRegistered ? "registered" : "not registered"}`);
2981
3156
  console.log(` Hooks: ${hooksRegistered ? `registered (${hookCount || "?"} hooks)` : "not registered"}`);
3157
+ console.log(` Codex hooks: ${codexHooksRegistered ? "registered (2 hooks)" : "not registered"}`);
2982
3158
  if (config.sentinel?.enabled) {
2983
3159
  console.log(`
2984
3160
  Sentinel`);
@@ -3071,6 +3247,8 @@ function handleStatus() {
3071
3247
  Files`);
3072
3248
  console.log(` Config: ${getSettingsPath()}`);
3073
3249
  console.log(` Database: ${getDbPath()}`);
3250
+ console.log(` Codex config: ${join6(homedir3(), ".codex", "config.toml")}`);
3251
+ console.log(` Codex hooks: ${join6(homedir3(), ".codex", "hooks.json")}`);
3074
3252
  }
3075
3253
  function formatTimeAgo(epoch) {
3076
3254
  const ago = Math.floor(Date.now() / 1000) - epoch;
@@ -3203,12 +3381,14 @@ function handleUpdate() {
3203
3381
  try {
3204
3382
  execSync2("npm install -g engrm@latest", { stdio: "inherit" });
3205
3383
  console.log(`
3206
- Update complete. Re-registering hooks...`);
3384
+ Update complete. Re-registering integrations...`);
3207
3385
  const result = registerAll();
3208
- console.log(` MCP server registered \u2192 ${result.mcp.path}`);
3209
- console.log(` Hooks registered \u2192 ${result.hooks.path}`);
3386
+ console.log(` Claude MCP registered \u2192 ${result.mcp.path}`);
3387
+ console.log(` Claude hooks registered \u2192 ${result.hooks.path}`);
3388
+ console.log(` Codex MCP registered \u2192 ${result.codex.path}`);
3389
+ console.log(` Codex hooks registered \u2192 ${result.codexHooks.path}`);
3210
3390
  console.log(`
3211
- Restart Claude Code to use the new version.`);
3391
+ Restart Claude Code or Codex to use the new version.`);
3212
3392
  } catch (error) {
3213
3393
  console.error("Update failed. Try manually: npm install -g engrm@latest");
3214
3394
  }
@@ -3299,6 +3479,36 @@ async function handleDoctor() {
3299
3479
  } catch {
3300
3480
  warn("Could not check hooks registration");
3301
3481
  }
3482
+ const codexConfig = join6(homedir3(), ".codex", "config.toml");
3483
+ 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}]`)) {
3487
+ pass("MCP server registered in Codex");
3488
+ } else {
3489
+ warn("MCP server not registered in Codex");
3490
+ }
3491
+ } else {
3492
+ warn("Codex config not found (~/.codex/config.toml)");
3493
+ }
3494
+ } catch {
3495
+ warn("Could not check Codex MCP registration");
3496
+ }
3497
+ const codexHooks = join6(homedir3(), ".codex", "hooks.json");
3498
+ try {
3499
+ if (existsSync6(codexHooks)) {
3500
+ const content = readFileSync6(codexHooks, "utf-8");
3501
+ if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
3502
+ pass("Hooks registered in Codex");
3503
+ } else {
3504
+ warn("Codex hooks config found, but Engrm hooks are missing");
3505
+ }
3506
+ } else {
3507
+ warn("Codex hooks config not found (~/.codex/hooks.json)");
3508
+ }
3509
+ } catch {
3510
+ warn("Could not check Codex hooks registration");
3511
+ }
3302
3512
  if (config.candengo_url) {
3303
3513
  try {
3304
3514
  const controller = new AbortController;
@@ -3440,16 +3650,23 @@ async function checkDeviceLimits(baseUrl, apiKey) {
3440
3650
  }
3441
3651
  function printPostInit() {
3442
3652
  console.log(`
3443
- Registering with Claude Code...`);
3653
+ Registering with Claude Code and Codex...`);
3444
3654
  try {
3445
3655
  const result = registerAll();
3446
- console.log(` MCP server registered \u2192 ${result.mcp.path}`);
3447
- console.log(` Hooks registered \u2192 ${result.hooks.path}`);
3656
+ console.log(` Claude MCP registered \u2192 ${result.mcp.path}`);
3657
+ console.log(` Claude hooks registered \u2192 ${result.hooks.path}`);
3658
+ console.log(` Codex MCP registered \u2192 ${result.codex.path}`);
3659
+ console.log(` Codex hooks registered \u2192 ${result.codexHooks.path}`);
3448
3660
  console.log(`
3449
- Engrm is ready! Start a new Claude Code session to use memory.`);
3661
+ Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
3450
3662
  } catch (error) {
3663
+ const packageRoot = join6(THIS_DIR, "..");
3664
+ 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")}`;
3451
3668
  console.log(`
3452
- Could not auto-register with Claude Code.`);
3669
+ Could not auto-register with Claude Code and Codex.`);
3453
3670
  console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
3454
3671
  console.log(`
3455
3672
  Manual setup \u2014 add to ~/.claude.json:`);
@@ -3458,13 +3675,56 @@ Manual setup \u2014 add to ~/.claude.json:`);
3458
3675
  "mcpServers": {
3459
3676
  "engrm": {
3460
3677
  "type": "stdio",
3461
- "command": "bun",
3462
- "args": ["run", "${process.cwd()}/src/server.ts"]
3678
+ "command": "${runtime}",
3679
+ "args": ${JSON.stringify(serverArgs)}
3463
3680
  }
3464
3681
  }
3465
3682
  }`);
3683
+ console.log(`
3684
+ And add to ~/.codex/config.toml:`);
3685
+ console.log(`
3686
+ [mcp_servers.engrm]
3687
+ enabled = true
3688
+ command = "${runtime}"
3689
+ args = ${formatTomlArray(serverArgs)}
3690
+ startup_timeout_sec = 15
3691
+ tool_timeout_sec = 30
3692
+ [features]
3693
+ codex_hooks = true
3694
+ `);
3695
+ console.log(`
3696
+ And add to ~/.codex/hooks.json:`);
3697
+ console.log(`
3698
+ {
3699
+ "hooks": {
3700
+ "SessionStart": [
3701
+ {
3702
+ "hooks": [
3703
+ {
3704
+ "type": "command",
3705
+ "command": "${sessionStartCommand}"
3706
+ }
3707
+ ]
3708
+ }
3709
+ ],
3710
+ "Stop": [
3711
+ {
3712
+ "hooks": [
3713
+ {
3714
+ "type": "command",
3715
+ "command": "${codexStopCommand}"
3716
+ }
3717
+ ]
3718
+ }
3719
+ ]
3466
3720
  }
3467
3721
  }
3722
+ `);
3723
+ }
3724
+ }
3725
+ function formatTomlArray(values) {
3726
+ return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
3727
+ }
3468
3728
  function printUsage() {
3469
3729
  console.log(`Engrm \u2014 Memory layer for AI coding agents
3470
3730
  `);
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ // hooks/codex-stop.ts
4
+ import { spawn } from "node:child_process";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ var thisDir = dirname(fileURLToPath(import.meta.url));
8
+ var delegatePath = join(thisDir, "stop.ts");
9
+ async function readStdin() {
10
+ const chunks = [];
11
+ for await (const chunk of process.stdin) {
12
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
13
+ }
14
+ return chunks.join("");
15
+ }
16
+ async function main() {
17
+ const input = await readStdin();
18
+ const isBun = process.execPath.endsWith("bun");
19
+ const childArgs = isBun ? ["run", delegatePath] : [delegatePath];
20
+ const child = spawn(process.execPath, childArgs, {
21
+ stdio: ["pipe", "pipe", "pipe"]
22
+ });
23
+ if (input.length > 0) {
24
+ child.stdin.write(input);
25
+ }
26
+ child.stdin.end();
27
+ let stdout = "";
28
+ let stderr = "";
29
+ child.stdout.on("data", (chunk) => {
30
+ stdout += chunk.toString();
31
+ });
32
+ child.stderr.on("data", (chunk) => {
33
+ stderr += chunk.toString();
34
+ });
35
+ const exitCode = await new Promise((resolve, reject) => {
36
+ child.on("error", reject);
37
+ child.on("close", (code) => resolve(code ?? 1));
38
+ });
39
+ const messages = [stdout.trim(), stderr.trim()].filter(Boolean);
40
+ const systemMessage = messages.length > 0 ? messages.join(`
41
+ `) : null;
42
+ if (exitCode === 0) {
43
+ console.log(JSON.stringify({
44
+ continue: true,
45
+ ...systemMessage ? { systemMessage } : {}
46
+ }));
47
+ process.exit(0);
48
+ }
49
+ console.log(JSON.stringify({
50
+ continue: true,
51
+ ...systemMessage ? { systemMessage: `Engrm stop hook failed:
52
+ ${systemMessage}` } : {}
53
+ }));
54
+ process.exit(0);
55
+ }
56
+ main().catch((error) => {
57
+ console.log(JSON.stringify({
58
+ continue: true,
59
+ systemMessage: `Engrm stop hook failed: ${error instanceof Error ? error.message : String(error)}`
60
+ }));
61
+ process.exit(0);
62
+ });