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 +60 -13
- package/dist/cli.js +288 -28
- package/dist/hooks/codex-stop.js +62 -0
- package/dist/hooks/elicitation-result.js +1690 -1637
- package/dist/hooks/post-tool-use.js +284 -231
- package/dist/hooks/pre-compact.js +410 -78
- package/dist/hooks/sentinel.js +150 -103
- package/dist/hooks/session-start.js +2284 -2039
- package/dist/hooks/stop.js +196 -130
- package/dist/server.js +614 -118
- package/package.json +6 -5
- package/bin/build.mjs +0 -97
- package/bin/engrm.mjs +0 -13
package/README.md
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
# Engrm
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
53
|
-
- **Session end** — generates a session digest, syncs to cloud,
|
|
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 (
|
|
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
|
|
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** |
|
|
190
|
-
| **Observations** | 5,000 | 25,000 |
|
|
191
|
-
| **Devices** |
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
|
1691
|
-
hooks
|
|
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
|
|
3384
|
+
Update complete. Re-registering integrations...`);
|
|
3207
3385
|
const result = registerAll();
|
|
3208
|
-
console.log(` MCP
|
|
3209
|
-
console.log(`
|
|
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
|
|
3447
|
-
console.log(`
|
|
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": "
|
|
3462
|
-
"args":
|
|
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
|
`);
|