engrm 0.4.1 → 0.4.4

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
@@ -1,20 +1,25 @@
1
1
  # Engrm
2
2
 
3
- **Cross-device memory for AI coding agents.** Every session remembers what you learned yesterday on any machine, for every team member.
3
+ **Shared memory and delivery review for AI coding agents.** Engrm keeps context, decisions, and project state moving across your machines, your team, and the agents you switch between.
4
+
5
+ For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installed.
4
6
 
5
7
  ```
6
8
  npx engrm init
7
9
  ```
8
10
 
11
+ Public beta. Engrm is built for Claude Code, Codex, OpenClaw skills, and other MCP-native coding workflows. 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
- Your AI agent forgets everything between sessions. Engrm fixes that.
17
+ Your AI agent forgets everything between sessions. Engrm fixes that, and helps you check whether the work really matched the brief.
14
18
 
15
- - **Captures automatically** — hooks into Claude Code to record discoveries, bugfixes, decisions, and patterns as you work
19
+ - **Works across agents** — Claude Code and Codex integrate directly, and OpenClaw can use Engrm through published skill bundles
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
22
+ - **Reviews delivery** — tie plans, decisions, and sessions together so you can see what actually shipped
18
23
  - **Works offline** — local SQLite is the source of truth; syncs when connected
19
24
  - **Guards your code** — Sentinel audits changes in real-time before they land
20
25
 
@@ -30,7 +35,7 @@ Visit [engrm.dev](https://engrm.dev) and create an account.
30
35
  npx engrm init
31
36
  ```
32
37
 
33
- This opens your browser for authentication, writes config to `~/.engrm/`, and registers the MCP server + hooks in Claude Code. Takes about 30 seconds.
38
+ This opens your browser for authentication, writes config to `~/.engrm/`, and registers Engrm in Claude Code and Codex when those configs are available. OpenClaw support is provided through the packaged skills in [`openclaw/`](openclaw/). Takes about 30 seconds.
34
39
 
35
40
  **Alternative methods:**
36
41
  ```bash
@@ -44,13 +49,13 @@ npx engrm init --url=https://vector.internal.company.com
44
49
  npx engrm init --manual
45
50
  ```
46
51
 
47
- ### 3. Use Claude Code normally
52
+ ### 3. Use your agent normally
48
53
 
49
54
  That's it. Engrm works in the background:
50
55
 
51
56
  - **Session start** — injects relevant project memory into context
52
- - **While you work** — captures observations from tool use (edits, writes, commands)
53
- - **Session end** — generates a session digest, syncs to cloud, shows summary
57
+ - **While you work** — captures observations from tool use where the agent exposes that hook surface
58
+ - **Session end** — generates a session digest, syncs to cloud, and turns recent work into a denser project brief
54
59
 
55
60
  ```
56
61
  ━━━ Engrm Session Summary ━━━
@@ -76,10 +81,12 @@ Engrm Status
76
81
  User: david
77
82
  Email: david@example.com
78
83
  Device: macbook-a1b2c3d4
79
- Plan: Pro ($15/mo)
84
+ Plan: Pro (£9.99/mo)
80
85
  Candengo: https://www.candengo.com
81
86
  MCP server: registered
87
+ Codex MCP: registered
82
88
  Hooks: registered (6 hooks)
89
+ Codex hooks: registered (2 hooks)
83
90
 
84
91
  Observations: 1,247 active
85
92
  By type: change: 412, discovery: 289, bugfix: 187, ...
@@ -117,9 +124,33 @@ Claude Code session
117
124
  Available on all your devices + team members
118
125
  ```
119
126
 
127
+ ```
128
+ Codex session
129
+
130
+ ├─ SessionStart hook ──→ inject relevant memory into context
131
+
132
+ ├─ MCP tools ──────────→ search, save, inspect, message, stats
133
+
134
+ └─ Stop hook ──────────→ session digest + sync + summary
135
+ ```
136
+
137
+ ### Agent Support
138
+
139
+ | Capability | Claude Code | Codex | OpenClaw |
140
+ |---|---|---|---|
141
+ | MCP server tools | ✓ | ✓ | Via skills / MCP |
142
+ | Session-start context injection | ✓ | ✓ | Via skill-guided workflow |
143
+ | Stop/session summary hook | ✓ | ✓ | Via skill-guided workflow |
144
+ | Per-tool automatic capture | ✓ | Partial via MCP/manual flows only | Manual / skill-guided |
145
+ | Pre-write Sentinel hook | ✓ | Not yet exposed by Codex public hooks | Not exposed |
146
+ | Pre-compact reinjection | ✓ | Not exposed | Not exposed |
147
+ | ElicitationResult capture | ✓ | Not exposed | Not exposed |
148
+
149
+ OpenClaw support is packaged in [`openclaw/`](openclaw/) as `engrm-memory`, `engrm-delivery-review`, and `engrm-sentinel` skills for ClawHub-style distribution.
150
+
120
151
  ### MCP Tools
121
152
 
122
- The MCP server exposes tools that Claude can call directly:
153
+ The MCP server exposes tools that supported agents can call directly:
123
154
 
124
155
  | Tool | Purpose |
125
156
  |------|---------|
@@ -128,6 +159,9 @@ The MCP server exposes tools that Claude can call directly:
128
159
  | `get_observations` | Fetch full details by ID |
129
160
  | `save_observation` | Manually save something worth remembering |
130
161
  | `install_pack` | Load a curated knowledge pack for your stack |
162
+ | `send_message` | Leave a cross-device or team note |
163
+ | `recent_activity` | Inspect what Engrm captured most recently |
164
+ | `memory_stats` | View high-level capture and sync health |
131
165
 
132
166
  ### Observation Types
133
167
 
@@ -186,9 +220,9 @@ Observations age gracefully: **active** (30 days, full weight) → **aging** (0.
186
220
 
187
221
  | | Free | Vibe | Pro | Team |
188
222
  |---|---|---|---|---|
189
- | **Price** | $0 | $9/mo | $15/mo | £19/seat/mo |
190
- | **Observations** | 5,000 | 25,000 | Unlimited | Unlimited |
191
- | **Devices** | 1 | 3 | Unlimited | Unlimited |
223
+ | **Price** | £0 | £5.99/mo | £9.99/mo | £12.99/seat/mo |
224
+ | **Observations** | 5,000 | 25,000 | 100,000 | Unlimited |
225
+ | **Devices** | 2 | 3 | 5 | Unlimited |
192
226
  | **Cloud sync** | ✓ | ✓ | ✓ | ✓ |
193
227
  | **Sentinel** | — | Advisory (50/day) | Advisory (200/day) | Blocking (unlimited) |
194
228
  | **Retention** | 30 days | 90 days | 1 year | Unlimited |
@@ -227,11 +261,13 @@ Place in your project root to override project identity for non-git projects:
227
261
  }
228
262
  ```
229
263
 
230
- ### Claude Code integration
264
+ ### Agent integration
231
265
 
232
266
  Engrm auto-registers in:
233
267
  - `~/.claude.json` — MCP server (`engrm`)
234
268
  - `~/.claude/settings.json` — 6 lifecycle hooks
269
+ - `~/.codex/config.toml` — MCP server (`engrm`) + `codex_hooks` feature flag
270
+ - `~/.codex/hooks.json` — `SessionStart` and `Stop` hooks
235
271
 
236
272
  ---
237
273
 
@@ -259,4 +295,15 @@ See [LICENSE](LICENSE) for full terms.
259
295
 
260
296
  ---
261
297
 
298
+ ## Project
299
+
300
+ - Architecture: [ARCHITECTURE.md](ARCHITECTURE.md)
301
+ - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
302
+ - Security: [SECURITY.md](SECURITY.md)
303
+ - Roadmap: [ROADMAP.md](ROADMAP.md)
304
+
305
+ Maintainers: run `node scripts/check-public-docs.mjs` to verify the repo only contains the approved public docs set at the root.
306
+
307
+ ---
308
+
262
309
  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
  `);