engrm 0.4.5 → 0.4.6
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 +160 -90
- package/dist/hooks/session-start.js +94 -17
- package/dist/hooks/stop.js +102 -19
- package/dist/server.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,41 +1,79 @@
|
|
|
1
1
|
# Engrm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](./LICENSE)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://modelcontextprotocol.io)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
**The only AI memory that syncs across devices and agents.**
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
Cross-device persistent memory for OpenClaw, Claude Code, Codex, and any MCP-compatible agent. Start free with 2 devices.
|
|
11
|
+
|
|
12
|
+
[Get Started](https://engrm.dev) • [Documentation](https://engrm.dev/developers) • [Blog](https://engrm.dev/blog)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why Engrm?
|
|
17
|
+
|
|
18
|
+
- **Cross-device sync** — Fix a bug on your laptop, continue on your desktop. No other memory tool does this.
|
|
19
|
+
- **Cross-agent compatible** — Works with OpenClaw, Claude Code, Codex, Cursor, Windsurf, Cline, Zed
|
|
20
|
+
- **Free tier** — 2 devices, 5,000 observations, full sync. £0 forever.
|
|
21
|
+
- **Offline-first** — Local SQLite + sqlite-vec. <50ms search. Works on a plane.
|
|
22
|
+
- **Delivery Review** — Compare what was promised vs what shipped
|
|
23
|
+
- **Sentinel** — Real-time code audit before changes land
|
|
24
|
+
- **Team memory** — Share insights across your whole team (Team plan)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## vs Other Memory Tools
|
|
29
|
+
|
|
30
|
+
| Feature | Engrm Free | Supermemory Pro | mem0 |
|
|
31
|
+
|---------|------------|-----------------|------|
|
|
32
|
+
| **Cost** | £0 | $20/mo | ~$2/mo + usage |
|
|
33
|
+
| **Cross-device** | ✅ 2 devices | ❌ Single device | ❌ Single device |
|
|
34
|
+
| **OpenClaw plugin** | ✅ Native | ✅ (Pro required) | ✅ (usage costs) |
|
|
35
|
+
| **Works with Claude/Codex** | ✅ | ❌ | ❌ |
|
|
36
|
+
| **Delivery Review** | ✅ | ❌ | ❌ |
|
|
37
|
+
| **Sentinel** | ✅ (Vibe+) | ❌ | ❌ |
|
|
10
38
|
|
|
11
|
-
|
|
39
|
+
[Read the full comparison →](https://engrm.dev/blog/engrm-openclaw-cross-device-memory)
|
|
12
40
|
|
|
13
41
|
---
|
|
14
42
|
|
|
15
|
-
##
|
|
43
|
+
## Installation
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
### For OpenClaw Users
|
|
18
46
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
```bash
|
|
48
|
+
# 1. Install the plugin
|
|
49
|
+
openclaw plugins install engrm-openclaw-plugin
|
|
50
|
+
|
|
51
|
+
# 2. Restart OpenClaw
|
|
52
|
+
# Quit and reopen, or restart gateway
|
|
25
53
|
|
|
26
|
-
|
|
54
|
+
# 3. Connect Engrm in chat
|
|
55
|
+
/engrm connect
|
|
56
|
+
|
|
57
|
+
# 4. Verify
|
|
58
|
+
/engrm status
|
|
59
|
+
```
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
**What works:**
|
|
62
|
+
- ✅ Session startup memory injection
|
|
63
|
+
- ✅ Automatic session capture
|
|
64
|
+
- ✅ Cross-device sync (unique to Engrm)
|
|
65
|
+
- ✅ `/engrm` slash commands
|
|
66
|
+
- ✅ Sentinel advisory mode (Vibe+ plans)
|
|
29
67
|
|
|
30
|
-
|
|
68
|
+
**Blog:** [Engrm Now Supports OpenClaw →](https://engrm.dev/blog/engrm-openclaw-cross-device-memory)
|
|
31
69
|
|
|
32
|
-
###
|
|
70
|
+
### For Claude Code / Codex
|
|
33
71
|
|
|
34
72
|
```bash
|
|
35
73
|
npx engrm init
|
|
36
74
|
```
|
|
37
75
|
|
|
38
|
-
This
|
|
76
|
+
This auto-configures MCP servers and hooks in `~/.claude.json` and `~/.codex/config.toml`.
|
|
39
77
|
|
|
40
78
|
**Alternative methods:**
|
|
41
79
|
```bash
|
|
@@ -49,9 +87,15 @@ npx engrm init --url=https://vector.internal.company.com
|
|
|
49
87
|
npx engrm init --manual
|
|
50
88
|
```
|
|
51
89
|
|
|
52
|
-
|
|
90
|
+
For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installed.
|
|
53
91
|
|
|
54
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## How It Works
|
|
95
|
+
|
|
96
|
+
### Background Operation
|
|
97
|
+
|
|
98
|
+
Engrm works automatically:
|
|
55
99
|
|
|
56
100
|
- **Session start** — injects relevant project memory into context
|
|
57
101
|
- **While you work** — captures observations from tool use where the agent exposes that hook surface
|
|
@@ -69,7 +113,7 @@ That's it. Engrm works in the background:
|
|
|
69
113
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
70
114
|
```
|
|
71
115
|
|
|
72
|
-
###
|
|
116
|
+
### Check Status
|
|
73
117
|
|
|
74
118
|
```bash
|
|
75
119
|
npx engrm status
|
|
@@ -95,49 +139,47 @@ Engrm Status
|
|
|
95
139
|
Security: 3 findings (1 high, 2 medium)
|
|
96
140
|
```
|
|
97
141
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
## How It Works
|
|
142
|
+
### Architecture
|
|
101
143
|
|
|
144
|
+
**Claude Code session:**
|
|
102
145
|
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Available on all your devices + team members
|
|
146
|
+
│
|
|
147
|
+
├─ SessionStart hook ──→ inject relevant memory into context
|
|
148
|
+
│
|
|
149
|
+
├─ PreToolUse hook ────→ Sentinel audits Edit/Write (optional)
|
|
150
|
+
│
|
|
151
|
+
├─ PostToolUse hook ───→ extract observations from tool results
|
|
152
|
+
│
|
|
153
|
+
├─ PreCompact hook ────→ re-inject memory before context compression
|
|
154
|
+
│
|
|
155
|
+
├─ ElicitationResult ──→ capture MCP form submissions
|
|
156
|
+
│
|
|
157
|
+
└─ Stop hook ──────────→ session digest + sync + summary
|
|
158
|
+
│
|
|
159
|
+
▼
|
|
160
|
+
Local SQLite (FTS5 + sqlite-vec)
|
|
161
|
+
│
|
|
162
|
+
▼ (sync every 30s)
|
|
163
|
+
Candengo Vector (cloud)
|
|
164
|
+
│
|
|
165
|
+
▼
|
|
166
|
+
Available on all your devices + team members
|
|
125
167
|
```
|
|
126
168
|
|
|
169
|
+
**Codex session:**
|
|
127
170
|
```
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
└─ Stop hook ──────────→ session digest + sync + summary
|
|
171
|
+
│
|
|
172
|
+
├─ SessionStart hook ──→ inject relevant memory into context
|
|
173
|
+
│
|
|
174
|
+
├─ MCP tools ──────────→ search, save, inspect, message, stats
|
|
175
|
+
│
|
|
176
|
+
└─ Stop hook ──────────→ session digest + sync + summary
|
|
135
177
|
```
|
|
136
178
|
|
|
137
|
-
### Agent
|
|
179
|
+
### Agent Capability Matrix
|
|
138
180
|
|
|
139
181
|
| Capability | Claude Code | Codex | OpenClaw |
|
|
140
|
-
|
|
182
|
+
|-----------|-------------|-------|----------|
|
|
141
183
|
| MCP server tools | ✓ | ✓ | Via skills / MCP |
|
|
142
184
|
| Session-start context injection | ✓ | ✓ | Via skill-guided workflow |
|
|
143
185
|
| Stop/session summary hook | ✓ | ✓ | Via skill-guided workflow |
|
|
@@ -146,7 +188,11 @@ Codex session
|
|
|
146
188
|
| Pre-compact reinjection | ✓ | Not exposed | Not exposed |
|
|
147
189
|
| ElicitationResult capture | ✓ | Not exposed | Not exposed |
|
|
148
190
|
|
|
149
|
-
|
|
191
|
+
See [AGENT_SUPPORT.md](AGENT_SUPPORT.md) for detailed comparison.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Features
|
|
150
196
|
|
|
151
197
|
### MCP Tools
|
|
152
198
|
|
|
@@ -166,7 +212,7 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
166
212
|
### Observation Types
|
|
167
213
|
|
|
168
214
|
| Type | What it captures |
|
|
169
|
-
|
|
215
|
+
|------|------------------|
|
|
170
216
|
| `discovery` | Learning about existing systems or codebases |
|
|
171
217
|
| `bugfix` | Something was broken, now fixed |
|
|
172
218
|
| `decision` | Architectural or design choice with rationale |
|
|
@@ -176,50 +222,55 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
176
222
|
| `pattern` | Recurring issue or technique |
|
|
177
223
|
| `digest` | Session summary (auto-generated) |
|
|
178
224
|
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## Features
|
|
182
|
-
|
|
183
225
|
### Hybrid Search
|
|
226
|
+
|
|
184
227
|
Local FTS5 + sqlite-vec (all-MiniLM-L6-v2, 384 dims) combined with Candengo Vector's BGE-M3 semantic search. Results merged via Reciprocal Rank Fusion.
|
|
185
228
|
|
|
186
|
-
### Sentinel
|
|
187
|
-
|
|
229
|
+
### Sentinel
|
|
230
|
+
|
|
231
|
+
LLM-powered review of every `Edit`/`Write` before it executes. Catches security issues, anti-patterns, and drift from team decisions.
|
|
188
232
|
|
|
189
233
|
```
|
|
190
|
-
⚠️
|
|
234
|
+
⚠️ Sentinel: SQL query uses string concatenation instead of parameterized query
|
|
191
235
|
Rule: sql-injection
|
|
192
236
|
(Advisory mode — change allowed)
|
|
193
237
|
```
|
|
194
238
|
|
|
195
|
-
|
|
239
|
+
**Built-in rule packs:** security, auth, api, react, database.
|
|
196
240
|
|
|
197
241
|
```bash
|
|
198
|
-
npx engrm sentinel init-rules
|
|
199
|
-
npx engrm sentinel rules
|
|
242
|
+
npx engrm sentinel init-rules # Install all rule packs
|
|
243
|
+
npx engrm sentinel rules # List available packs
|
|
200
244
|
```
|
|
201
245
|
|
|
202
|
-
###
|
|
246
|
+
### Knowledge Packs
|
|
247
|
+
|
|
203
248
|
Pre-loaded knowledge for your tech stack. Detected automatically on session start.
|
|
204
249
|
|
|
205
|
-
Available
|
|
250
|
+
**Available:** typescript-patterns, nextjs-patterns, node-security, python-django, react-gotchas, api-best-practices, web-security
|
|
206
251
|
|
|
207
252
|
```bash
|
|
208
253
|
npx engrm install-pack typescript-patterns
|
|
209
254
|
```
|
|
210
255
|
|
|
211
256
|
### Secret Scrubbing
|
|
257
|
+
|
|
212
258
|
Multi-layer regex scanning for API keys, passwords, tokens, and credentials. Sensitive content is redacted before storage and sync. Custom patterns configurable in `~/.engrm/settings.json`.
|
|
213
259
|
|
|
214
|
-
###
|
|
260
|
+
### Retention & Aging
|
|
261
|
+
|
|
215
262
|
Observations age gracefully: **active** (30 days, full weight) → **aging** (0.7x search weight) → **archived** (compacted into digests) → **purged** (after 12 months). Pinned observations never age.
|
|
216
263
|
|
|
217
264
|
---
|
|
218
265
|
|
|
219
266
|
## Pricing
|
|
220
267
|
|
|
268
|
+
**Free tier stays free forever.** No bait-and-switch.
|
|
269
|
+
|
|
270
|
+
Start with 2 devices and 5,000 observations. Upgrade when you need more.
|
|
271
|
+
|
|
221
272
|
| | Free | Vibe | Pro | Team |
|
|
222
|
-
|
|
273
|
+
|---|------|------|-----|------|
|
|
223
274
|
| **Price** | £0 | £5.99/mo | £9.99/mo | £12.99/seat/mo |
|
|
224
275
|
| **Observations** | 5,000 | 25,000 | 100,000 | Unlimited |
|
|
225
276
|
| **Devices** | 2 | 3 | 5 | Unlimited |
|
|
@@ -232,7 +283,7 @@ Sign up at [engrm.dev](https://engrm.dev).
|
|
|
232
283
|
|
|
233
284
|
---
|
|
234
285
|
|
|
235
|
-
## Self-
|
|
286
|
+
## Self-Hosted
|
|
236
287
|
|
|
237
288
|
Point Engrm at your own [Candengo Vector](https://www.candengo.com) instance:
|
|
238
289
|
|
|
@@ -246,11 +297,11 @@ Candengo Vector provides the backend: BGE-M3 hybrid search, multi-tenant namespa
|
|
|
246
297
|
|
|
247
298
|
## Configuration
|
|
248
299
|
|
|
249
|
-
###
|
|
300
|
+
### `~/.engrm/settings.json`
|
|
250
301
|
|
|
251
302
|
Created by `engrm init`. Contains API credentials, sync settings, search preferences, secret scrubbing patterns, and Sentinel configuration.
|
|
252
303
|
|
|
253
|
-
###
|
|
304
|
+
### `.engrm-project.json`
|
|
254
305
|
|
|
255
306
|
Place in your project root to override project identity for non-git projects:
|
|
256
307
|
|
|
@@ -261,24 +312,25 @@ Place in your project root to override project identity for non-git projects:
|
|
|
261
312
|
}
|
|
262
313
|
```
|
|
263
314
|
|
|
264
|
-
### Agent
|
|
315
|
+
### Agent Auto-Registration
|
|
265
316
|
|
|
266
317
|
Engrm auto-registers in:
|
|
318
|
+
|
|
267
319
|
- `~/.claude.json` — MCP server (`engrm`)
|
|
268
320
|
- `~/.claude/settings.json` — 6 lifecycle hooks
|
|
269
321
|
- `~/.codex/config.toml` — MCP server (`engrm`) + `codex_hooks` feature flag
|
|
270
|
-
- `~/.codex/hooks.json` —
|
|
322
|
+
- `~/.codex/hooks.json` — SessionStart and Stop hooks
|
|
271
323
|
|
|
272
324
|
---
|
|
273
325
|
|
|
274
|
-
##
|
|
326
|
+
## Technical Stack
|
|
275
327
|
|
|
276
|
-
- **Runtime
|
|
277
|
-
- **Local storage
|
|
278
|
-
- **Embeddings
|
|
279
|
-
- **Remote backend
|
|
280
|
-
- **MCP
|
|
281
|
-
- **AI extraction
|
|
328
|
+
- **Runtime:** TypeScript, runs on Bun (dev) or Node.js 18+ (npm)
|
|
329
|
+
- **Local storage:** SQLite via `better-sqlite3`, FTS5 full-text search, `sqlite-vec` for embeddings
|
|
330
|
+
- **Embeddings:** all-MiniLM-L6-v2 via `@xenova/transformers` (384 dims, ~23MB)
|
|
331
|
+
- **Remote backend:** Candengo Vector (BGE-M3, Qdrant, hybrid dense+sparse search)
|
|
332
|
+
- **MCP:** `@modelcontextprotocol/sdk` (stdio transport)
|
|
333
|
+
- **AI extraction:** `@anthropic-ai/claude-agent-sdk` (optional, for richer observations)
|
|
282
334
|
|
|
283
335
|
---
|
|
284
336
|
|
|
@@ -286,23 +338,41 @@ Engrm auto-registers in:
|
|
|
286
338
|
|
|
287
339
|
**FSL-1.1-ALv2** (Functional Source License) — part of the [Fair Source](https://fair.io) movement.
|
|
288
340
|
|
|
289
|
-
- Free to use, modify, and self-host
|
|
290
|
-
- You cannot offer this as a competing hosted service
|
|
291
|
-
- Each version converts to Apache 2.0 after 2 years
|
|
292
|
-
- Sentinel is a separate proprietary product
|
|
341
|
+
- ✅ Free to use, modify, and self-host
|
|
342
|
+
- ❌ You cannot offer this as a competing hosted service
|
|
343
|
+
- ✅ Each version converts to Apache 2.0 after 2 years
|
|
344
|
+
- ⚠️ Sentinel is a separate proprietary product
|
|
293
345
|
|
|
294
346
|
See [LICENSE](LICENSE) for full terms.
|
|
295
347
|
|
|
296
348
|
---
|
|
297
349
|
|
|
298
|
-
##
|
|
350
|
+
## Documentation
|
|
299
351
|
|
|
300
352
|
- Architecture: [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
301
353
|
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
302
354
|
- Security: [SECURITY.md](SECURITY.md)
|
|
303
355
|
- Roadmap: [ROADMAP.md](ROADMAP.md)
|
|
304
356
|
|
|
305
|
-
Maintainers
|
|
357
|
+
**Maintainers:** run `node scripts/check-public-docs.mjs` to verify the repo only contains the approved public docs set at the root.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Resources
|
|
362
|
+
|
|
363
|
+
- [Documentation](https://engrm.dev/developers)
|
|
364
|
+
- [Blog](https://engrm.dev/blog)
|
|
365
|
+
- [Pricing](https://engrm.dev/pricing)
|
|
366
|
+
- [Sentinel](https://engrm.dev/sentinel)
|
|
367
|
+
|
|
368
|
+
## Community
|
|
369
|
+
|
|
370
|
+
- [Twitter/X](https://twitter.com/engrm_dev)
|
|
371
|
+
- [GitHub Issues](https://github.com/dr12hes/engrm/issues)
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
**Found this useful?** ⭐ Star this repo to help other developers discover Engrm.
|
|
306
376
|
|
|
307
377
|
---
|
|
308
378
|
|
|
@@ -2497,17 +2497,22 @@ function formatSplashScreen(data) {
|
|
|
2497
2497
|
function formatVisibleStartupBrief(context) {
|
|
2498
2498
|
const lines = [];
|
|
2499
2499
|
const latest = pickBestSummary(context);
|
|
2500
|
+
const observationFallbacks = buildObservationFallbacks(context);
|
|
2500
2501
|
if (latest) {
|
|
2501
2502
|
const sections = [
|
|
2502
|
-
["
|
|
2503
|
-
["
|
|
2504
|
-
["
|
|
2505
|
-
["
|
|
2503
|
+
["Request", chooseRequest(latest.request, observationFallbacks.request), 1],
|
|
2504
|
+
["Investigated", chooseSection(latest.investigated, observationFallbacks.investigated, "Investigated"), 2],
|
|
2505
|
+
["Learned", latest.learned, 2],
|
|
2506
|
+
["Completed", chooseSection(latest.completed, observationFallbacks.completed, "Completed"), 2],
|
|
2507
|
+
["Next Steps", latest.next_steps, 2]
|
|
2506
2508
|
];
|
|
2507
|
-
for (const [label, value] of sections) {
|
|
2508
|
-
const formatted =
|
|
2509
|
-
if (formatted) {
|
|
2510
|
-
lines.push(`${c2.cyan}${label}:${c2.reset}
|
|
2509
|
+
for (const [label, value, maxItems] of sections) {
|
|
2510
|
+
const formatted = toSplashLines(value, maxItems ?? 2);
|
|
2511
|
+
if (formatted.length > 0) {
|
|
2512
|
+
lines.push(`${c2.cyan}${label}:${c2.reset}`);
|
|
2513
|
+
for (const item of formatted) {
|
|
2514
|
+
lines.push(` ${item}`);
|
|
2515
|
+
}
|
|
2511
2516
|
}
|
|
2512
2517
|
}
|
|
2513
2518
|
}
|
|
@@ -2516,21 +2521,19 @@ function formatVisibleStartupBrief(context) {
|
|
|
2516
2521
|
lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
|
|
2517
2522
|
}
|
|
2518
2523
|
if (lines.length === 0 && context.observations.length > 0) {
|
|
2519
|
-
const top = context.observations.slice(0, 2);
|
|
2524
|
+
const top = context.observations.filter((obs) => !looksLikeFileOperationTitle(obs.title)).slice(0, 2);
|
|
2520
2525
|
for (const obs of top) {
|
|
2521
2526
|
lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
|
|
2522
2527
|
}
|
|
2523
2528
|
}
|
|
2524
|
-
return lines.slice(0,
|
|
2529
|
+
return lines.slice(0, 10);
|
|
2525
2530
|
}
|
|
2526
|
-
function
|
|
2531
|
+
function toSplashLines(value, maxItems) {
|
|
2527
2532
|
if (!value)
|
|
2528
|
-
return
|
|
2529
|
-
const
|
|
2530
|
-
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).
|
|
2531
|
-
|
|
2532
|
-
return null;
|
|
2533
|
-
return truncateInline(cleaned, maxLen);
|
|
2533
|
+
return [];
|
|
2534
|
+
const lines = value.split(`
|
|
2535
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
|
|
2536
|
+
return dedupeFragmentsInLines(lines);
|
|
2534
2537
|
}
|
|
2535
2538
|
function pickBestSummary(context) {
|
|
2536
2539
|
const summaries = context.summaries || [];
|
|
@@ -2572,6 +2575,80 @@ function dedupeFragments(text) {
|
|
|
2572
2575
|
}
|
|
2573
2576
|
return deduped.join("; ");
|
|
2574
2577
|
}
|
|
2578
|
+
function dedupeFragmentsInLines(lines) {
|
|
2579
|
+
const seen = new Set;
|
|
2580
|
+
const deduped = [];
|
|
2581
|
+
for (const line of lines) {
|
|
2582
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2583
|
+
if (!normalized || seen.has(normalized))
|
|
2584
|
+
continue;
|
|
2585
|
+
seen.add(normalized);
|
|
2586
|
+
deduped.push(line);
|
|
2587
|
+
}
|
|
2588
|
+
return deduped;
|
|
2589
|
+
}
|
|
2590
|
+
function chooseRequest(primary, fallback) {
|
|
2591
|
+
if (primary && !looksLikeFileOperationTitle(primary))
|
|
2592
|
+
return primary;
|
|
2593
|
+
return fallback;
|
|
2594
|
+
}
|
|
2595
|
+
function chooseSection(primary, fallback, label) {
|
|
2596
|
+
if (!primary)
|
|
2597
|
+
return fallback;
|
|
2598
|
+
if (label === "Completed" && isWeakCompletedSection(primary))
|
|
2599
|
+
return fallback || primary;
|
|
2600
|
+
if (label === "Investigated" && sectionItemCount(primary) === 0)
|
|
2601
|
+
return fallback;
|
|
2602
|
+
return primary;
|
|
2603
|
+
}
|
|
2604
|
+
function isWeakCompletedSection(value) {
|
|
2605
|
+
const items = value.split(`
|
|
2606
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean);
|
|
2607
|
+
if (!items.length)
|
|
2608
|
+
return true;
|
|
2609
|
+
const weakCount = items.filter((item) => looksLikeFileOperationTitle(item)).length;
|
|
2610
|
+
return weakCount === items.length;
|
|
2611
|
+
}
|
|
2612
|
+
function looksLikeFileOperationTitle(value) {
|
|
2613
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
2614
|
+
}
|
|
2615
|
+
function scoreSplashLine(value) {
|
|
2616
|
+
let score = 0;
|
|
2617
|
+
if (!looksLikeFileOperationTitle(value))
|
|
2618
|
+
score += 2;
|
|
2619
|
+
if (/[:;]/.test(value))
|
|
2620
|
+
score += 1;
|
|
2621
|
+
if (value.length > 30)
|
|
2622
|
+
score += 0.5;
|
|
2623
|
+
return score;
|
|
2624
|
+
}
|
|
2625
|
+
function buildObservationFallbacks(context) {
|
|
2626
|
+
const request = context.observations.find((obs) => !looksLikeFileOperationTitle(obs.title))?.title ?? null;
|
|
2627
|
+
const investigated = collectObservationTitles(context, (obs) => obs.type === "discovery", 2);
|
|
2628
|
+
const completed = collectObservationTitles(context, (obs) => ["feature", "refactor", "change", "digest"].includes(obs.type) && !looksLikeFileOperationTitle(obs.title), 2);
|
|
2629
|
+
return {
|
|
2630
|
+
request,
|
|
2631
|
+
investigated,
|
|
2632
|
+
completed
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function collectObservationTitles(context, predicate, limit) {
|
|
2636
|
+
const seen = new Set;
|
|
2637
|
+
const picked = [];
|
|
2638
|
+
for (const obs of context.observations) {
|
|
2639
|
+
if (!predicate(obs))
|
|
2640
|
+
continue;
|
|
2641
|
+
const normalized = obs.title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2642
|
+
if (!normalized || seen.has(normalized))
|
|
2643
|
+
continue;
|
|
2644
|
+
seen.add(normalized);
|
|
2645
|
+
picked.push(`- ${obs.title}`);
|
|
2646
|
+
if (picked.length >= limit)
|
|
2647
|
+
break;
|
|
2648
|
+
}
|
|
2649
|
+
return picked.length ? picked.join(`
|
|
2650
|
+
`) : null;
|
|
2651
|
+
}
|
|
2575
2652
|
function pickRelevantStaleDecision(context, summary) {
|
|
2576
2653
|
const stale = context.staleDecisions || [];
|
|
2577
2654
|
if (!stale.length)
|
package/dist/hooks/stop.js
CHANGED
|
@@ -35,35 +35,34 @@ function extractInvestigated(observations) {
|
|
|
35
35
|
const discoveries = observations.filter((o) => o.type === "discovery");
|
|
36
36
|
if (discoveries.length === 0)
|
|
37
37
|
return null;
|
|
38
|
-
return discoveries
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}).join(`
|
|
43
|
-
`);
|
|
38
|
+
return formatObservationGroup(discoveries, {
|
|
39
|
+
limit: 4,
|
|
40
|
+
factsPerItem: 2
|
|
41
|
+
});
|
|
44
42
|
}
|
|
45
43
|
function extractLearned(observations) {
|
|
46
44
|
const learnTypes = new Set(["bugfix", "decision", "pattern"]);
|
|
47
45
|
const learned = observations.filter((o) => learnTypes.has(o.type));
|
|
48
46
|
if (learned.length === 0)
|
|
49
47
|
return null;
|
|
50
|
-
return learned
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}).join(`
|
|
55
|
-
`);
|
|
48
|
+
return formatObservationGroup(learned, {
|
|
49
|
+
limit: 4,
|
|
50
|
+
factsPerItem: 2
|
|
51
|
+
});
|
|
56
52
|
}
|
|
57
53
|
function extractCompleted(observations) {
|
|
58
54
|
const completeTypes = new Set(["change", "feature", "refactor"]);
|
|
59
55
|
const completed = observations.filter((o) => completeTypes.has(o.type));
|
|
60
56
|
if (completed.length === 0)
|
|
61
57
|
return null;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
const prioritized = dedupeObservationsByTitle(completed).sort((a, b) => scoreCompletedObservation(b) - scoreCompletedObservation(a)).slice(0, 4);
|
|
59
|
+
const lines = prioritized.map((o) => {
|
|
60
|
+
const title = normalizeCompletedTitle(o.title, o.files_modified);
|
|
61
|
+
const facts = extractTopFacts(o, 1);
|
|
62
|
+
return facts ? `- ${title}
|
|
63
|
+
${facts}` : `- ${title}`;
|
|
64
|
+
});
|
|
65
|
+
return dedupeBulletLines(lines).join(`
|
|
67
66
|
`);
|
|
68
67
|
}
|
|
69
68
|
function extractNextSteps(observations) {
|
|
@@ -77,11 +76,78 @@ function extractNextSteps(observations) {
|
|
|
77
76
|
return unresolved.map((o) => `- Investigate: ${o.title}`).slice(0, 3).join(`
|
|
78
77
|
`);
|
|
79
78
|
}
|
|
79
|
+
function formatObservationGroup(observations, options) {
|
|
80
|
+
const lines = dedupeObservationsByTitle(observations).slice(0, options.limit).map((o) => {
|
|
81
|
+
const facts = extractTopFacts(o, options.factsPerItem);
|
|
82
|
+
return facts ? `- ${o.title}
|
|
83
|
+
${facts}` : `- ${o.title}`;
|
|
84
|
+
});
|
|
85
|
+
const deduped = dedupeBulletLines(lines);
|
|
86
|
+
return deduped.length ? deduped.join(`
|
|
87
|
+
`) : null;
|
|
88
|
+
}
|
|
89
|
+
function dedupeBulletLines(lines) {
|
|
90
|
+
const seen = new Set;
|
|
91
|
+
const deduped = [];
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const normalized = line.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
94
|
+
if (!normalized || seen.has(normalized))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(normalized);
|
|
97
|
+
deduped.push(line);
|
|
98
|
+
}
|
|
99
|
+
return deduped;
|
|
100
|
+
}
|
|
101
|
+
function dedupeObservationsByTitle(observations) {
|
|
102
|
+
const seen = new Set;
|
|
103
|
+
const deduped = [];
|
|
104
|
+
for (const obs of observations) {
|
|
105
|
+
const normalized = normalizeObservationKey(obs.title);
|
|
106
|
+
if (!normalized || seen.has(normalized))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(normalized);
|
|
109
|
+
deduped.push(obs);
|
|
110
|
+
}
|
|
111
|
+
return deduped;
|
|
112
|
+
}
|
|
113
|
+
function scoreCompletedObservation(obs) {
|
|
114
|
+
let score = obs.quality || 0;
|
|
115
|
+
if (obs.type === "feature")
|
|
116
|
+
score += 0.5;
|
|
117
|
+
if (obs.type === "refactor")
|
|
118
|
+
score += 0.2;
|
|
119
|
+
if (hasMeaningfulFacts(obs))
|
|
120
|
+
score += 0.4;
|
|
121
|
+
if (looksLikeFileOperation(obs.title))
|
|
122
|
+
score -= 0.6;
|
|
123
|
+
if (obs.narrative && obs.narrative.length > 80)
|
|
124
|
+
score += 0.2;
|
|
125
|
+
return score;
|
|
126
|
+
}
|
|
127
|
+
function hasMeaningfulFacts(obs) {
|
|
128
|
+
return parseJsonArray(obs.facts).some((fact) => fact.trim().length > 20);
|
|
129
|
+
}
|
|
130
|
+
function looksLikeFileOperation(title) {
|
|
131
|
+
return /^(modified|updated|edited|touched|changed)\s+[A-Za-z0-9_.\-\/]+$/i.test(title.trim());
|
|
132
|
+
}
|
|
133
|
+
function normalizeCompletedTitle(title, filesModified) {
|
|
134
|
+
const trimmed = title.trim();
|
|
135
|
+
if (!trimmed)
|
|
136
|
+
return "Completed work";
|
|
137
|
+
if (!looksLikeFileOperation(trimmed))
|
|
138
|
+
return trimmed;
|
|
139
|
+
const files = parseJsonArray(filesModified);
|
|
140
|
+
const filename = files[0]?.split("/").pop();
|
|
141
|
+
if (filename) {
|
|
142
|
+
return `Updated implementation in ${filename}`;
|
|
143
|
+
}
|
|
144
|
+
return trimmed;
|
|
145
|
+
}
|
|
80
146
|
function extractTopFacts(obs, n) {
|
|
81
|
-
const facts = parseJsonArray(obs.facts);
|
|
147
|
+
const facts = parseJsonArray(obs.facts).filter((fact) => isUsefulFact(fact, obs.title)).slice(0, n);
|
|
82
148
|
if (facts.length === 0)
|
|
83
149
|
return null;
|
|
84
|
-
return facts.
|
|
150
|
+
return facts.map((f) => ` ${f}`).join(`
|
|
85
151
|
`);
|
|
86
152
|
}
|
|
87
153
|
function parseJsonArray(json) {
|
|
@@ -95,6 +161,23 @@ function parseJsonArray(json) {
|
|
|
95
161
|
} catch {}
|
|
96
162
|
return [];
|
|
97
163
|
}
|
|
164
|
+
function isUsefulFact(fact, title) {
|
|
165
|
+
const cleaned = fact.trim();
|
|
166
|
+
if (!cleaned)
|
|
167
|
+
return false;
|
|
168
|
+
const normalizedFact = normalizeObservationKey(cleaned);
|
|
169
|
+
const normalizedTitle = normalizeObservationKey(title);
|
|
170
|
+
if (normalizedFact && normalizedFact === normalizedTitle)
|
|
171
|
+
return false;
|
|
172
|
+
if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
|
|
173
|
+
return false;
|
|
174
|
+
if (/^\(?[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+\)?$/.test(cleaned))
|
|
175
|
+
return false;
|
|
176
|
+
return cleaned.length > 16 || /[:;]/.test(cleaned);
|
|
177
|
+
}
|
|
178
|
+
function normalizeObservationKey(value) {
|
|
179
|
+
return value.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\b(modified|updated|edited|touched|changed)\b/g, "").replace(/\s+/g, " ").trim();
|
|
180
|
+
}
|
|
98
181
|
|
|
99
182
|
// src/config.ts
|
|
100
183
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
package/dist/server.js
CHANGED
|
@@ -16947,7 +16947,7 @@ process.on("SIGTERM", () => {
|
|
|
16947
16947
|
});
|
|
16948
16948
|
var server = new McpServer({
|
|
16949
16949
|
name: "engrm",
|
|
16950
|
-
version: "0.
|
|
16950
|
+
version: "0.4.6"
|
|
16951
16951
|
});
|
|
16952
16952
|
server.tool("save_observation", "Save an observation to memory", {
|
|
16953
16953
|
type: exports_external.enum([
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engrm",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "Shared memory across devices, sessions, and coding agents",
|
|
5
|
+
"mcpName": "io.github.dr12hes/engrm",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/server.js",
|
|
7
8
|
"bin": {
|