engrm 0.4.5 → 0.4.7
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/cli.js +128 -1
- package/dist/hooks/elicitation-result.js +82 -1
- package/dist/hooks/post-tool-use.js +82 -1
- package/dist/hooks/pre-compact.js +30 -5
- package/dist/hooks/session-start.js +127 -22
- package/dist/hooks/stop.js +389 -29
- package/dist/server.js +361 -19
- 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
|
|
package/dist/cli.js
CHANGED
|
@@ -1072,6 +1072,31 @@ function getOutboxStats(db) {
|
|
|
1072
1072
|
return stats;
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
1075
|
+
// src/intelligence/value-signals.ts
|
|
1076
|
+
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
1077
|
+
function computeSessionValueSignals(observations, securityFindings = []) {
|
|
1078
|
+
const decisionsCount = observations.filter((o) => o.type === "decision").length;
|
|
1079
|
+
const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
|
|
1080
|
+
const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
|
|
1081
|
+
const featuresCount = observations.filter((o) => o.type === "feature").length;
|
|
1082
|
+
const refactorsCount = observations.filter((o) => o.type === "refactor").length;
|
|
1083
|
+
const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
|
|
1084
|
+
const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
|
|
1085
|
+
const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
|
|
1086
|
+
return {
|
|
1087
|
+
decisions_count: decisionsCount,
|
|
1088
|
+
lessons_count: lessonsCount,
|
|
1089
|
+
discoveries_count: discoveriesCount,
|
|
1090
|
+
features_count: featuresCount,
|
|
1091
|
+
refactors_count: refactorsCount,
|
|
1092
|
+
repeated_patterns_count: repeatedPatternsCount,
|
|
1093
|
+
security_findings_count: securityFindings.length,
|
|
1094
|
+
critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
|
|
1095
|
+
delivery_review_ready: hasRequestSignal && hasCompletionSignal,
|
|
1096
|
+
vibe_guardian_active: securityFindings.length > 0
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1075
1100
|
// src/storage/migrations.ts
|
|
1076
1101
|
var MIGRATIONS2 = [
|
|
1077
1102
|
{
|
|
@@ -2104,6 +2129,80 @@ function findDuplicate(newTitle, candidates) {
|
|
|
2104
2129
|
return bestMatch;
|
|
2105
2130
|
}
|
|
2106
2131
|
|
|
2132
|
+
// src/capture/facts.ts
|
|
2133
|
+
var FACT_ELIGIBLE_TYPES = new Set([
|
|
2134
|
+
"bugfix",
|
|
2135
|
+
"decision",
|
|
2136
|
+
"discovery",
|
|
2137
|
+
"pattern",
|
|
2138
|
+
"feature",
|
|
2139
|
+
"refactor",
|
|
2140
|
+
"change"
|
|
2141
|
+
]);
|
|
2142
|
+
function buildStructuredFacts(input) {
|
|
2143
|
+
const seedFacts = dedupeFacts(input.facts ?? []);
|
|
2144
|
+
if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
|
|
2145
|
+
return seedFacts;
|
|
2146
|
+
}
|
|
2147
|
+
const derived = [...seedFacts];
|
|
2148
|
+
if (seedFacts.length === 0 && looksMeaningful(input.title)) {
|
|
2149
|
+
derived.push(input.title.trim());
|
|
2150
|
+
}
|
|
2151
|
+
for (const sentence of extractNarrativeFacts(input.narrative)) {
|
|
2152
|
+
derived.push(sentence);
|
|
2153
|
+
}
|
|
2154
|
+
const fileFact = buildFilesFact(input.filesModified);
|
|
2155
|
+
if (fileFact) {
|
|
2156
|
+
derived.push(fileFact);
|
|
2157
|
+
}
|
|
2158
|
+
return dedupeFacts(derived).slice(0, 4);
|
|
2159
|
+
}
|
|
2160
|
+
function extractNarrativeFacts(narrative) {
|
|
2161
|
+
if (!narrative)
|
|
2162
|
+
return [];
|
|
2163
|
+
const cleaned = narrative.replace(/\s+/g, " ").trim();
|
|
2164
|
+
if (cleaned.length < 24)
|
|
2165
|
+
return [];
|
|
2166
|
+
const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
|
|
2167
|
+
return parts.slice(0, 2);
|
|
2168
|
+
}
|
|
2169
|
+
function buildFilesFact(filesModified) {
|
|
2170
|
+
if (!filesModified || filesModified.length === 0)
|
|
2171
|
+
return null;
|
|
2172
|
+
const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
|
|
2173
|
+
if (cleaned.length === 0)
|
|
2174
|
+
return null;
|
|
2175
|
+
if (cleaned.length === 1) {
|
|
2176
|
+
return `Touched ${cleaned[0]}`;
|
|
2177
|
+
}
|
|
2178
|
+
return `Touched ${cleaned.join(", ")}`;
|
|
2179
|
+
}
|
|
2180
|
+
function dedupeFacts(facts) {
|
|
2181
|
+
const seen = new Set;
|
|
2182
|
+
const result = [];
|
|
2183
|
+
for (const fact of facts) {
|
|
2184
|
+
const cleaned = fact.trim().replace(/\s+/g, " ");
|
|
2185
|
+
if (!looksMeaningful(cleaned))
|
|
2186
|
+
continue;
|
|
2187
|
+
const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
2188
|
+
if (!key || seen.has(key))
|
|
2189
|
+
continue;
|
|
2190
|
+
seen.add(key);
|
|
2191
|
+
result.push(cleaned);
|
|
2192
|
+
}
|
|
2193
|
+
return result;
|
|
2194
|
+
}
|
|
2195
|
+
function looksMeaningful(value) {
|
|
2196
|
+
const cleaned = value.trim();
|
|
2197
|
+
if (cleaned.length < 12)
|
|
2198
|
+
return false;
|
|
2199
|
+
if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
|
|
2200
|
+
return false;
|
|
2201
|
+
if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
|
|
2202
|
+
return false;
|
|
2203
|
+
return true;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2107
2206
|
// src/storage/projects.ts
|
|
2108
2207
|
import { execSync } from "node:child_process";
|
|
2109
2208
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -2495,10 +2594,17 @@ async function saveObservation(db, config, input) {
|
|
|
2495
2594
|
const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
|
|
2496
2595
|
const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
|
|
2497
2596
|
const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
|
|
2498
|
-
const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
|
|
2499
2597
|
const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
|
|
2500
2598
|
const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
|
|
2501
2599
|
const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
|
|
2600
|
+
const structuredFacts = buildStructuredFacts({
|
|
2601
|
+
type: input.type,
|
|
2602
|
+
title: input.title,
|
|
2603
|
+
narrative: input.narrative,
|
|
2604
|
+
facts: input.facts,
|
|
2605
|
+
filesModified
|
|
2606
|
+
});
|
|
2607
|
+
const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
|
|
2502
2608
|
const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
|
|
2503
2609
|
const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
|
|
2504
2610
|
let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
|
|
@@ -3195,6 +3301,27 @@ function handleStatus() {
|
|
|
3195
3301
|
} catch {}
|
|
3196
3302
|
const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
|
|
3197
3303
|
console.log(` Sessions: ${summaryCount} summarised`);
|
|
3304
|
+
try {
|
|
3305
|
+
const activeObservations = db.db.query(`SELECT * FROM observations
|
|
3306
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
|
|
3307
|
+
const securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
3308
|
+
ORDER BY created_at_epoch DESC
|
|
3309
|
+
LIMIT 500`).all();
|
|
3310
|
+
const signals = computeSessionValueSignals(activeObservations, securityFindings);
|
|
3311
|
+
const signalParts = [
|
|
3312
|
+
`lessons: ${signals.lessons_count}`,
|
|
3313
|
+
`decisions: ${signals.decisions_count}`,
|
|
3314
|
+
`discoveries: ${signals.discoveries_count}`,
|
|
3315
|
+
`features: ${signals.features_count}`
|
|
3316
|
+
];
|
|
3317
|
+
if (signals.repeated_patterns_count > 0) {
|
|
3318
|
+
signalParts.push(`patterns: ${signals.repeated_patterns_count}`);
|
|
3319
|
+
}
|
|
3320
|
+
console.log(` Value: ${signalParts.join(", ")}`);
|
|
3321
|
+
if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
|
|
3322
|
+
console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ` + `${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
|
|
3323
|
+
}
|
|
3324
|
+
} catch {}
|
|
3198
3325
|
try {
|
|
3199
3326
|
const lastSummary = db.db.query(`SELECT request, created_at_epoch FROM session_summaries
|
|
3200
3327
|
ORDER BY created_at_epoch DESC LIMIT 1`).get();
|
|
@@ -250,6 +250,80 @@ function findDuplicate(newTitle, candidates) {
|
|
|
250
250
|
return bestMatch;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// src/capture/facts.ts
|
|
254
|
+
var FACT_ELIGIBLE_TYPES = new Set([
|
|
255
|
+
"bugfix",
|
|
256
|
+
"decision",
|
|
257
|
+
"discovery",
|
|
258
|
+
"pattern",
|
|
259
|
+
"feature",
|
|
260
|
+
"refactor",
|
|
261
|
+
"change"
|
|
262
|
+
]);
|
|
263
|
+
function buildStructuredFacts(input) {
|
|
264
|
+
const seedFacts = dedupeFacts(input.facts ?? []);
|
|
265
|
+
if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
|
|
266
|
+
return seedFacts;
|
|
267
|
+
}
|
|
268
|
+
const derived = [...seedFacts];
|
|
269
|
+
if (seedFacts.length === 0 && looksMeaningful(input.title)) {
|
|
270
|
+
derived.push(input.title.trim());
|
|
271
|
+
}
|
|
272
|
+
for (const sentence of extractNarrativeFacts(input.narrative)) {
|
|
273
|
+
derived.push(sentence);
|
|
274
|
+
}
|
|
275
|
+
const fileFact = buildFilesFact(input.filesModified);
|
|
276
|
+
if (fileFact) {
|
|
277
|
+
derived.push(fileFact);
|
|
278
|
+
}
|
|
279
|
+
return dedupeFacts(derived).slice(0, 4);
|
|
280
|
+
}
|
|
281
|
+
function extractNarrativeFacts(narrative) {
|
|
282
|
+
if (!narrative)
|
|
283
|
+
return [];
|
|
284
|
+
const cleaned = narrative.replace(/\s+/g, " ").trim();
|
|
285
|
+
if (cleaned.length < 24)
|
|
286
|
+
return [];
|
|
287
|
+
const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
|
|
288
|
+
return parts.slice(0, 2);
|
|
289
|
+
}
|
|
290
|
+
function buildFilesFact(filesModified) {
|
|
291
|
+
if (!filesModified || filesModified.length === 0)
|
|
292
|
+
return null;
|
|
293
|
+
const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
|
|
294
|
+
if (cleaned.length === 0)
|
|
295
|
+
return null;
|
|
296
|
+
if (cleaned.length === 1) {
|
|
297
|
+
return `Touched ${cleaned[0]}`;
|
|
298
|
+
}
|
|
299
|
+
return `Touched ${cleaned.join(", ")}`;
|
|
300
|
+
}
|
|
301
|
+
function dedupeFacts(facts) {
|
|
302
|
+
const seen = new Set;
|
|
303
|
+
const result = [];
|
|
304
|
+
for (const fact of facts) {
|
|
305
|
+
const cleaned = fact.trim().replace(/\s+/g, " ");
|
|
306
|
+
if (!looksMeaningful(cleaned))
|
|
307
|
+
continue;
|
|
308
|
+
const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
309
|
+
if (!key || seen.has(key))
|
|
310
|
+
continue;
|
|
311
|
+
seen.add(key);
|
|
312
|
+
result.push(cleaned);
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
function looksMeaningful(value) {
|
|
317
|
+
const cleaned = value.trim();
|
|
318
|
+
if (cleaned.length < 12)
|
|
319
|
+
return false;
|
|
320
|
+
if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
|
|
321
|
+
return false;
|
|
322
|
+
if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
|
|
323
|
+
return false;
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
253
327
|
// src/storage/projects.ts
|
|
254
328
|
import { execSync } from "node:child_process";
|
|
255
329
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -630,10 +704,17 @@ async function saveObservation(db, config, input) {
|
|
|
630
704
|
const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
|
|
631
705
|
const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
|
|
632
706
|
const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
|
|
633
|
-
const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
|
|
634
707
|
const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
|
|
635
708
|
const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
|
|
636
709
|
const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
|
|
710
|
+
const structuredFacts = buildStructuredFacts({
|
|
711
|
+
type: input.type,
|
|
712
|
+
title: input.title,
|
|
713
|
+
narrative: input.narrative,
|
|
714
|
+
facts: input.facts,
|
|
715
|
+
filesModified
|
|
716
|
+
});
|
|
717
|
+
const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
|
|
637
718
|
const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
|
|
638
719
|
const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
|
|
639
720
|
let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
|