engrm 0.4.3 → 0.4.5
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 +23 -20
- package/dist/hooks/session-start.js +174 -1
- package/dist/hooks/stop.js +9 -0
- package/dist/server.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
|
|
5
5
|
For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installed.
|
|
6
6
|
|
|
@@ -8,17 +8,18 @@ For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installe
|
|
|
8
8
|
npx engrm init
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Public beta. The current source of truth for agent capability differences is [AGENT_SUPPORT.md](AGENT_SUPPORT.md).
|
|
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
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
## What It Does
|
|
16
16
|
|
|
17
|
-
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.
|
|
18
18
|
|
|
19
|
-
- **
|
|
19
|
+
- **Works across agents** — Claude Code and Codex integrate directly, and OpenClaw can use Engrm through published skill bundles
|
|
20
20
|
- **Remembers across devices** — fix a bug on your laptop, continue on your desktop with full context
|
|
21
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
|
|
22
23
|
- **Works offline** — local SQLite is the source of truth; syncs when connected
|
|
23
24
|
- **Guards your code** — Sentinel audits changes in real-time before they land
|
|
24
25
|
|
|
@@ -34,7 +35,7 @@ Visit [engrm.dev](https://engrm.dev) and create an account.
|
|
|
34
35
|
npx engrm init
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
This opens your browser for authentication, writes config to `~/.engrm/`, and registers Engrm in
|
|
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.
|
|
38
39
|
|
|
39
40
|
**Alternative methods:**
|
|
40
41
|
```bash
|
|
@@ -54,7 +55,7 @@ That's it. Engrm works in the background:
|
|
|
54
55
|
|
|
55
56
|
- **Session start** — injects relevant project memory into context
|
|
56
57
|
- **While you work** — captures observations from tool use where the agent exposes that hook surface
|
|
57
|
-
- **Session end** — generates a session digest, syncs to cloud,
|
|
58
|
+
- **Session end** — generates a session digest, syncs to cloud, and turns recent work into a denser project brief
|
|
58
59
|
|
|
59
60
|
```
|
|
60
61
|
━━━ Engrm Session Summary ━━━
|
|
@@ -80,7 +81,7 @@ Engrm Status
|
|
|
80
81
|
User: david
|
|
81
82
|
Email: david@example.com
|
|
82
83
|
Device: macbook-a1b2c3d4
|
|
83
|
-
Plan: Pro (
|
|
84
|
+
Plan: Pro (£9.99/mo)
|
|
84
85
|
Candengo: https://www.candengo.com
|
|
85
86
|
MCP server: registered
|
|
86
87
|
Codex MCP: registered
|
|
@@ -135,19 +136,21 @@ Codex session
|
|
|
135
136
|
|
|
136
137
|
### Agent Support
|
|
137
138
|
|
|
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 |
|
|
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.
|
|
147
150
|
|
|
148
151
|
### MCP Tools
|
|
149
152
|
|
|
150
|
-
The MCP server exposes tools that
|
|
153
|
+
The MCP server exposes tools that supported agents can call directly:
|
|
151
154
|
|
|
152
155
|
| Tool | Purpose |
|
|
153
156
|
|------|---------|
|
|
@@ -217,9 +220,9 @@ Observations age gracefully: **active** (30 days, full weight) → **aging** (0.
|
|
|
217
220
|
|
|
218
221
|
| | Free | Vibe | Pro | Team |
|
|
219
222
|
|---|---|---|---|---|
|
|
220
|
-
| **Price** |
|
|
221
|
-
| **Observations** | 5,000 | 25,000 |
|
|
222
|
-
| **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 |
|
|
223
226
|
| **Cloud sync** | ✓ | ✓ | ✓ | ✓ |
|
|
224
227
|
| **Sentinel** | — | Advisory (50/day) | Advisory (200/day) | Blocking (unlimited) |
|
|
225
228
|
| **Retention** | 30 days | 90 days | 1 year | Unlimited |
|
|
@@ -2413,7 +2413,8 @@ async function main() {
|
|
|
2413
2413
|
available: remaining,
|
|
2414
2414
|
securityFindings: context.securityFindings?.length ?? 0,
|
|
2415
2415
|
unreadMessages: msgCount,
|
|
2416
|
-
synced: syncedCount
|
|
2416
|
+
synced: syncedCount,
|
|
2417
|
+
context
|
|
2417
2418
|
});
|
|
2418
2419
|
let packLine = "";
|
|
2419
2420
|
try {
|
|
@@ -2481,8 +2482,180 @@ function formatSplashScreen(data) {
|
|
|
2481
2482
|
}
|
|
2482
2483
|
lines.push("");
|
|
2483
2484
|
lines.push(` ${c2.dim}Dashboard: https://engrm.dev/dashboard${c2.reset}`);
|
|
2485
|
+
const brief = formatVisibleStartupBrief(data.context);
|
|
2486
|
+
if (brief.length > 0) {
|
|
2487
|
+
lines.push("");
|
|
2488
|
+
lines.push(` ${c2.bold}Startup brief${c2.reset}`);
|
|
2489
|
+
for (const line of brief) {
|
|
2490
|
+
lines.push(` ${line}`);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2484
2493
|
lines.push("");
|
|
2485
2494
|
return lines.join(`
|
|
2486
2495
|
`);
|
|
2487
2496
|
}
|
|
2497
|
+
function formatVisibleStartupBrief(context) {
|
|
2498
|
+
const lines = [];
|
|
2499
|
+
const latest = pickBestSummary(context);
|
|
2500
|
+
if (latest) {
|
|
2501
|
+
const sections = [
|
|
2502
|
+
["Investigated", latest.investigated],
|
|
2503
|
+
["Learned", latest.learned],
|
|
2504
|
+
["Completed", latest.completed],
|
|
2505
|
+
["Next Steps", latest.next_steps]
|
|
2506
|
+
];
|
|
2507
|
+
for (const [label, value] of sections) {
|
|
2508
|
+
const formatted = toSplashBullet(value, label === "Next Steps" ? 140 : 180);
|
|
2509
|
+
if (formatted) {
|
|
2510
|
+
lines.push(`${c2.cyan}${label}:${c2.reset} ${formatted}`);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const stale = pickRelevantStaleDecision(context, latest);
|
|
2515
|
+
if (stale) {
|
|
2516
|
+
lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
|
|
2517
|
+
}
|
|
2518
|
+
if (lines.length === 0 && context.observations.length > 0) {
|
|
2519
|
+
const top = context.observations.slice(0, 2);
|
|
2520
|
+
for (const obs of top) {
|
|
2521
|
+
lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return lines.slice(0, 5);
|
|
2525
|
+
}
|
|
2526
|
+
function toSplashBullet(value, maxLen) {
|
|
2527
|
+
if (!value)
|
|
2528
|
+
return null;
|
|
2529
|
+
const cleaned = dedupeFragments(value.split(`
|
|
2530
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).join("; "));
|
|
2531
|
+
if (!cleaned)
|
|
2532
|
+
return null;
|
|
2533
|
+
return truncateInline(cleaned, maxLen);
|
|
2534
|
+
}
|
|
2535
|
+
function pickBestSummary(context) {
|
|
2536
|
+
const summaries = context.summaries || [];
|
|
2537
|
+
if (!summaries.length)
|
|
2538
|
+
return null;
|
|
2539
|
+
return [...summaries].sort((a, b) => scoreSummary(b) - scoreSummary(a))[0] ?? null;
|
|
2540
|
+
}
|
|
2541
|
+
function scoreSummary(summary) {
|
|
2542
|
+
let score = 0;
|
|
2543
|
+
if (summary.request)
|
|
2544
|
+
score += 3;
|
|
2545
|
+
if (summary.investigated)
|
|
2546
|
+
score += 4;
|
|
2547
|
+
if (summary.learned)
|
|
2548
|
+
score += 5;
|
|
2549
|
+
if (summary.completed)
|
|
2550
|
+
score += 5;
|
|
2551
|
+
if (summary.next_steps)
|
|
2552
|
+
score += 4;
|
|
2553
|
+
score += Math.min(4, sectionItemCount(summary.completed) + sectionItemCount(summary.learned));
|
|
2554
|
+
return score;
|
|
2555
|
+
}
|
|
2556
|
+
function sectionItemCount(value) {
|
|
2557
|
+
if (!value)
|
|
2558
|
+
return 0;
|
|
2559
|
+
return value.split(`
|
|
2560
|
+
`).map((line) => line.trim()).filter(Boolean).length;
|
|
2561
|
+
}
|
|
2562
|
+
function dedupeFragments(text) {
|
|
2563
|
+
const parts = text.split(";").map((part) => part.trim()).filter(Boolean);
|
|
2564
|
+
const seen = new Set;
|
|
2565
|
+
const deduped = [];
|
|
2566
|
+
for (const part of parts) {
|
|
2567
|
+
const normalized = part.toLowerCase();
|
|
2568
|
+
if (seen.has(normalized))
|
|
2569
|
+
continue;
|
|
2570
|
+
seen.add(normalized);
|
|
2571
|
+
deduped.push(part);
|
|
2572
|
+
}
|
|
2573
|
+
return deduped.join("; ");
|
|
2574
|
+
}
|
|
2575
|
+
function pickRelevantStaleDecision(context, summary) {
|
|
2576
|
+
const stale = context.staleDecisions || [];
|
|
2577
|
+
if (!stale.length)
|
|
2578
|
+
return null;
|
|
2579
|
+
const summaryText = [
|
|
2580
|
+
summary?.request,
|
|
2581
|
+
summary?.investigated,
|
|
2582
|
+
summary?.learned,
|
|
2583
|
+
summary?.completed,
|
|
2584
|
+
summary?.next_steps
|
|
2585
|
+
].filter(Boolean).join(" ");
|
|
2586
|
+
let best = null;
|
|
2587
|
+
let bestScore = 0;
|
|
2588
|
+
for (const item of stale) {
|
|
2589
|
+
if ((item.days_ago ?? 999) > 21)
|
|
2590
|
+
continue;
|
|
2591
|
+
const overlap = keywordOverlap(item.title || "", summaryText);
|
|
2592
|
+
const similarity = item.best_match_similarity ?? 0;
|
|
2593
|
+
const score = overlap * 4 + similarity;
|
|
2594
|
+
if (score > bestScore && overlap > 0) {
|
|
2595
|
+
best = item;
|
|
2596
|
+
bestScore = score;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
return best;
|
|
2600
|
+
}
|
|
2601
|
+
function keywordOverlap(a, b) {
|
|
2602
|
+
if (!a || !b)
|
|
2603
|
+
return 0;
|
|
2604
|
+
const stop = new Set([
|
|
2605
|
+
"the",
|
|
2606
|
+
"and",
|
|
2607
|
+
"for",
|
|
2608
|
+
"with",
|
|
2609
|
+
"from",
|
|
2610
|
+
"into",
|
|
2611
|
+
"this",
|
|
2612
|
+
"that",
|
|
2613
|
+
"was",
|
|
2614
|
+
"were",
|
|
2615
|
+
"have",
|
|
2616
|
+
"has",
|
|
2617
|
+
"had",
|
|
2618
|
+
"but",
|
|
2619
|
+
"not",
|
|
2620
|
+
"you",
|
|
2621
|
+
"your",
|
|
2622
|
+
"our",
|
|
2623
|
+
"their",
|
|
2624
|
+
"about",
|
|
2625
|
+
"added",
|
|
2626
|
+
"fixed",
|
|
2627
|
+
"created",
|
|
2628
|
+
"updated",
|
|
2629
|
+
"modified",
|
|
2630
|
+
"changed",
|
|
2631
|
+
"investigate",
|
|
2632
|
+
"next",
|
|
2633
|
+
"steps",
|
|
2634
|
+
"decision",
|
|
2635
|
+
"still",
|
|
2636
|
+
"looks",
|
|
2637
|
+
"unfinished"
|
|
2638
|
+
]);
|
|
2639
|
+
const wordsA = new Set(a.toLowerCase().match(/[a-z0-9_+-]{4,}/g)?.filter((w) => !stop.has(w)) || []);
|
|
2640
|
+
const wordsB = new Set(b.toLowerCase().match(/[a-z0-9_+-]{4,}/g)?.filter((w) => !stop.has(w)) || []);
|
|
2641
|
+
if (!wordsA.size || !wordsB.size)
|
|
2642
|
+
return 0;
|
|
2643
|
+
let overlap = 0;
|
|
2644
|
+
for (const word of wordsA) {
|
|
2645
|
+
if (wordsB.has(word))
|
|
2646
|
+
overlap++;
|
|
2647
|
+
}
|
|
2648
|
+
return overlap / Math.max(1, Math.min(wordsA.size, wordsB.size));
|
|
2649
|
+
}
|
|
2650
|
+
function truncateInline(text, maxLen) {
|
|
2651
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
2652
|
+
if (compact.length <= maxLen)
|
|
2653
|
+
return compact;
|
|
2654
|
+
return `${compact.slice(0, maxLen - 1).trimEnd()}\u2026`;
|
|
2655
|
+
}
|
|
2656
|
+
function capitalize(value) {
|
|
2657
|
+
if (!value)
|
|
2658
|
+
return value;
|
|
2659
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
2660
|
+
}
|
|
2488
2661
|
runHook("session-start", main);
|
package/dist/hooks/stop.js
CHANGED
|
@@ -1475,10 +1475,14 @@ function buildSummaryVectorDocument(summary, config, project) {
|
|
|
1475
1475
|
const parts = [];
|
|
1476
1476
|
if (summary.request)
|
|
1477
1477
|
parts.push(`Request: ${summary.request}`);
|
|
1478
|
+
if (summary.investigated)
|
|
1479
|
+
parts.push(`Investigated: ${summary.investigated}`);
|
|
1478
1480
|
if (summary.learned)
|
|
1479
1481
|
parts.push(`Learned: ${summary.learned}`);
|
|
1480
1482
|
if (summary.completed)
|
|
1481
1483
|
parts.push(`Completed: ${summary.completed}`);
|
|
1484
|
+
if (summary.next_steps)
|
|
1485
|
+
parts.push(`Next Steps: ${summary.next_steps}`);
|
|
1482
1486
|
return {
|
|
1483
1487
|
site_id: config.site_id,
|
|
1484
1488
|
namespace: config.namespace,
|
|
@@ -1492,6 +1496,11 @@ function buildSummaryVectorDocument(summary, config, project) {
|
|
|
1492
1496
|
project_name: project.name,
|
|
1493
1497
|
user_id: summary.user_id,
|
|
1494
1498
|
session_id: summary.session_id,
|
|
1499
|
+
request: summary.request,
|
|
1500
|
+
investigated: summary.investigated,
|
|
1501
|
+
learned: summary.learned,
|
|
1502
|
+
completed: summary.completed,
|
|
1503
|
+
next_steps: summary.next_steps,
|
|
1495
1504
|
created_at_epoch: summary.created_at_epoch,
|
|
1496
1505
|
local_id: summary.id
|
|
1497
1506
|
}
|
package/dist/server.js
CHANGED
|
@@ -16508,10 +16508,14 @@ function buildSummaryVectorDocument(summary, config2, project) {
|
|
|
16508
16508
|
const parts = [];
|
|
16509
16509
|
if (summary.request)
|
|
16510
16510
|
parts.push(`Request: ${summary.request}`);
|
|
16511
|
+
if (summary.investigated)
|
|
16512
|
+
parts.push(`Investigated: ${summary.investigated}`);
|
|
16511
16513
|
if (summary.learned)
|
|
16512
16514
|
parts.push(`Learned: ${summary.learned}`);
|
|
16513
16515
|
if (summary.completed)
|
|
16514
16516
|
parts.push(`Completed: ${summary.completed}`);
|
|
16517
|
+
if (summary.next_steps)
|
|
16518
|
+
parts.push(`Next Steps: ${summary.next_steps}`);
|
|
16515
16519
|
return {
|
|
16516
16520
|
site_id: config2.site_id,
|
|
16517
16521
|
namespace: config2.namespace,
|
|
@@ -16525,6 +16529,11 @@ function buildSummaryVectorDocument(summary, config2, project) {
|
|
|
16525
16529
|
project_name: project.name,
|
|
16526
16530
|
user_id: summary.user_id,
|
|
16527
16531
|
session_id: summary.session_id,
|
|
16532
|
+
request: summary.request,
|
|
16533
|
+
investigated: summary.investigated,
|
|
16534
|
+
learned: summary.learned,
|
|
16535
|
+
completed: summary.completed,
|
|
16536
|
+
next_steps: summary.next_steps,
|
|
16528
16537
|
created_at_epoch: summary.created_at_epoch,
|
|
16529
16538
|
local_id: summary.id
|
|
16530
16539
|
}
|