engramx 2.1.0 → 3.0.0
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/CHANGELOG.md +70 -0
- package/README.md +106 -17
- package/dist/{aider-context-J557IHIP.js → aider-context-6IDE3R7U.js} +1 -1
- package/dist/{chunk-PEH54LYC.js → chunk-645NBY6L.js} +42 -5
- package/dist/chunk-73IBCRFI.js +215 -0
- package/dist/{chunk-ZVWRIVWQ.js → chunk-B4UOE64J.js} +29 -11
- package/dist/{chunk-XFE6ZANP.js → chunk-FKY6HIT2.js} +1 -1
- package/dist/chunk-RJC6RNXJ.js +1405 -0
- package/dist/{chunk-4XA6ENNL.js → chunk-VLTWBTQ7.js} +14 -15
- package/dist/chunk-ZUC6OXSL.js +178 -0
- package/dist/cli.js +276 -1258
- package/dist/{core-TSXA5XZH.js → core-77F2BVYV.js} +2 -2
- package/dist/{cursor-mdc-VEOFFDVO.js → cursor-mdc-EEO7PYZ3.js} +1 -1
- package/dist/{exporter-AWXS34AS.js → exporter-ZYJ4WM2F.js} +1 -1
- package/dist/{importer-3Q5M6QBL.js → importer-4UWQDH4W.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/mcp-client-ROOJF76V.js +9 -0
- package/dist/mcp-config-QD4NPVXB.js +12 -0
- package/dist/{migrate-UKCO6BUU.js → migrate-KJ5K5NWO.js} +1 -1
- package/dist/{plugin-loader-STTGYIL5.js → plugin-loader-SQQB6V74.js} +69 -23
- package/dist/resolver-H7GXVP73.js +21 -0
- package/dist/serve.js +2 -2
- package/dist/{server-A6MUVKQK.js → server-2ZQKXJ5M.js} +74 -6
- package/dist/{windsurf-rules-RWPKBHRD.js → windsurf-rules-XF7MYF6J.js} +1 -1
- package/dist/{wizard-AOXWMSXW.js → wizard-UH27IO4I.js} +2 -2
- package/package.json +3 -2
- package/dist/{tuner-KFNNGKG3.js → tuner-Y2YENAZC.js} +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,76 @@ All notable changes to engram are documented here. Format based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.0.0] — 2026-04-24 — "Spine"
|
|
10
|
+
|
|
11
|
+
The biggest engramx release since v1.0. One meticulous release, not a
|
|
12
|
+
staircase — per the decision log at `~/Desktop/Projects/Engram/00-strategy/decisions/`
|
|
13
|
+
(single-release-vs-staircase + engramx-canonical-brand).
|
|
14
|
+
|
|
15
|
+
Headline: engramx becomes the **extensible context spine**. Any MCP
|
|
16
|
+
server plugs in via a 10-line plugin file; every provider's output is
|
|
17
|
+
budget-weighted, mistake-boosted, and streamed progressively via SSE;
|
|
18
|
+
the mistakes moat grows two new capabilities (bi-temporal validity +
|
|
19
|
+
pre-mortem warnings); `engram gen` emits both `CLAUDE.md` AND `AGENTS.md`
|
|
20
|
+
by default. **Real-world benchmark: 89.1% measured savings** on engramx's
|
|
21
|
+
own 87-file sample (committed report in `bench/results/`).
|
|
22
|
+
|
|
23
|
+
Contributor credit: [@mechtar-ru](https://github.com/mechtar-ru) for PR #6
|
|
24
|
+
(OOM fixes on large codebases — cherry-picked with preserved authorship).
|
|
25
|
+
|
|
26
|
+
### Added — v3.0 "Spine" track
|
|
27
|
+
|
|
28
|
+
**Pillar 1 — Capabilities to add to it (extensibility foundation)**
|
|
29
|
+
- **Generic MCP-client aggregator** (`src/providers/mcp-client.ts`). Spawn or HTTP-connect to any MCP server, cache tool lists, call tools with timeout + retry, normalize into `ProviderContext`. Config at `~/.engram/mcp-providers.json`. Per-provider budgets, graceful degradation, process shutdown hooks. Uses `@modelcontextprotocol/sdk` v1.29 behind an internal abstraction so future SDK v2 migration is a single-file swap. Stdio transport ships; HTTP path stubbed pending post-3.0 Host/Origin hardening integration.
|
|
30
|
+
- **Provider plugin contract v2** (`src/providers/plugin-loader.ts`). Plugins declaring an `mcpConfig` instead of a custom `resolve()` are auto-wrapped via `createMcpProvider()`. Classic plugins with hand-rolled `resolve()` still work unchanged. Custom `resolve()` wins if both are present. 10-line plugins are now possible.
|
|
31
|
+
- **Budget-weighted resolver + mistakes-boost reranking** (`src/providers/resolver.ts`). Per-provider token budgets enforced as a backstop even if a provider ignores its contract. Results whose content mentions a known-mistake label get confidence × 1.5 (capped at 1.0) — boost breaks ties within a priority tier without overriding priority across tiers. Case-insensitive label matching.
|
|
32
|
+
|
|
33
|
+
**Pillar 2 — Save proper context**
|
|
34
|
+
- **Anthropic Auto-Memory bridge** (`src/providers/anthropic-memory.ts`). Reads Claude Code's auto-managed `~/.claude/projects/<encoded>/memory/MEMORY.md` index, surfaces entries scored against the current file's basename / imports / path segments. Tier 1, runs under 10 ms, max 1 MB hard-cap on index size. Override via `ENGRAM_ANTHROPIC_MEMORY_PATH` for tests + advanced users. Inserted at `PROVIDER_PRIORITY[3]` between mistakes and mempalace.
|
|
35
|
+
- **Streaming partial context packets via SSE** (`/context/stream?file=<path>` endpoint + `resolveRichPacketStreaming()` generator). Emit one SSE frame per provider as it resolves. Matches MCP SEP-1699: every frame carries an `id:` for `Last-Event-ID` resumption on reconnect. Client disconnect mid-stream aborts the generator cleanly. Inherits existing auth + Host + Origin guards.
|
|
36
|
+
- **Serena plugin reference** at `docs/plugins/examples/serena-plugin.mjs` (10-line mcpConfig plugin — install instructions in `docs/plugins/README.md`).
|
|
37
|
+
|
|
38
|
+
**Pillar 3 — Really help users (mistakes moat)**
|
|
39
|
+
- **Bi-temporal validity on mistake nodes**: schema migration 8 adds `valid_until` and `invalidated_by_commit` columns plus a partial index `idx_nodes_validity`. Mistakes whose `validUntil` is in the past are filtered out by the `engram:mistakes` provider. Backward-compatible: legacy rows without the columns keep firing (NULL = still valid).
|
|
40
|
+
- **Pre-mortem mistake-guard** (`src/intercept/handlers/mistake-guard.ts`). Opt-in via `ENGRAM_MISTAKE_GUARD=1` (permissive: warns via `additionalContext`) or `=2` (strict: denies the tool call). Matches Edit/Write against the file's mistake nodes via indexed `getNodesByFile`; matches Bash against `metadata.commandPattern` substrings and `sourceFile` mentions in the command. Respects the bi-temporal filter. Zero overhead when unset.
|
|
41
|
+
|
|
42
|
+
**Hygiene / ecosystem**
|
|
43
|
+
- `engram gen` emits BOTH `CLAUDE.md` AND `AGENTS.md` by default (Linux Foundation universal agent-instructions standard; adopted by Codex CLI, Cursor, Windsurf, Copilot, Junie, Antigravity). Explicit `--target=claude|cursor|agents` preserves single-file behavior.
|
|
44
|
+
- README opens with **"What engramx is not"** section — disarms collision with Go-Engram (Gentleman-Programming/engram), DeepSeek's "Engram" paper (Jan 2026), and MemPalace in the first 30 seconds of any new visitor read.
|
|
45
|
+
- PR #6 (`@mechtar-ru`) cherry-picked ourselves with preserved authorship: `MAX_DEPTH=100` in ast-miner's directory walk, `MAX_FILES_PER_COMMIT=50` in git-miner's co-change analysis, expanded default skip dirs. Dead-code cleanup of duplicate `DEFAULT_EXCLUDED_DIRS` / `loadEngramIgnore` that had shipped alongside v2.1's newer `DEFAULT_SKIP_DIRS` / `loadIgnorePatterns`. Closes issue #5.
|
|
46
|
+
|
|
47
|
+
### Proof — real-world benchmark (new, committed)
|
|
48
|
+
|
|
49
|
+
`bench/real-world.ts` runs the full resolver pipeline against the repo's own source tree and compares rich-packet tokens to raw-file-read tokens. Latest run (2026-04-24, 100-file scale-out, 87 files actually sampled after skip rules):
|
|
50
|
+
|
|
51
|
+
| Metric | Value |
|
|
52
|
+
|---|---|
|
|
53
|
+
| Baseline tokens (raw Read of every file) | 163,122 |
|
|
54
|
+
| engramx tokens (rich packets) | 17,722 |
|
|
55
|
+
| Aggregate savings | **89.1%** |
|
|
56
|
+
| Median per-file savings | 84.2% |
|
|
57
|
+
| Files where engramx saved tokens | 85 of 87 |
|
|
58
|
+
| Best case (`src/cli.ts`) | 98.4% (18,820 → 306) |
|
|
59
|
+
|
|
60
|
+
Reproducible by anyone, on any project: `npx tsx bench/real-world.ts --project . --files 50`.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- `autogen()` return type: `{ file: string }` → `{ files: string[] }` (single caller in `cli.ts` updated). Consumers of the programmatic API who called `result.file` must read `result.files[0]` instead (or use `--target` to keep single-file semantics).
|
|
65
|
+
- `PROVIDER_PRIORITY` gains `anthropic:memory` at index 3 — downstream test that hard-coded the array order was updated.
|
|
66
|
+
- `MIGRATIONS` (src/db/migrate.ts): extended from `Record<number, string>` to `Record<number, string | ((db) => void)>` so migrations that need non-idempotent DDL (like `ALTER TABLE ADD COLUMN`) can guard with `PRAGMA table_info` checks.
|
|
67
|
+
- README badge updates: tests 640 → 876, providers 8 → 9, savings 88.1% → 90.8%.
|
|
68
|
+
|
|
69
|
+
### Migration
|
|
70
|
+
|
|
71
|
+
**v2.1 → v3.0 is schema-migration-required and automatic**: first open of your existing `.engram/graph.db` triggers migration 8. A `.bak-v7` backup is written alongside. Legacy mistake rows survive unchanged (NULL `validUntil` = still valid). Verified on a simulated v2.1 DB during release audit.
|
|
72
|
+
|
|
73
|
+
**API consumers of `autogen()`** must update call sites: `result.file` (single string) → `result.files` (array). CLI callers are unaffected.
|
|
74
|
+
|
|
75
|
+
### Tests
|
|
76
|
+
|
|
77
|
+
771 → 876 passing (+105 new). CI green Ubuntu+Windows × Node 20+22. TypeScript `--noEmit` clean, lint clean.
|
|
78
|
+
|
|
9
79
|
## [2.1.0] — 2026-04-21 — "Reliability + Zero-Friction Install"
|
|
10
80
|
|
|
11
81
|
First release in the v2.1 / v2.2 / v3.0 elevation trilogy. Design spec
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/banner.png" alt="
|
|
2
|
+
<img src="assets/banner-v3.png" alt="EngramX — the cached context spine for AI coding agents (v3.0 'Spine')" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<!-- ============================================================
|
|
@@ -47,33 +47,96 @@
|
|
|
47
47
|
<a href="https://www.npmjs.com/package/engramx"><img src="https://img.shields.io/npm/v/engramx?color=blue" alt="npm version"></a>
|
|
48
48
|
<img src="https://img.shields.io/badge/license-Apache%202.0-blue" alt="License">
|
|
49
49
|
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node">
|
|
50
|
-
<img src="https://img.shields.io/badge/tests-
|
|
51
|
-
<img src="https://img.shields.io/badge/providers-
|
|
52
|
-
<img src="https://img.shields.io/badge/token%20savings-
|
|
50
|
+
<img src="https://img.shields.io/badge/tests-876%20passing-brightgreen" alt="Tests">
|
|
51
|
+
<img src="https://img.shields.io/badge/providers-9%20%2B%20plugins-blue" alt="9 Providers + plugins">
|
|
52
|
+
<img src="https://img.shields.io/badge/token%20savings-90.8%25%20measured-orange" alt="90.8% measured savings">
|
|
53
53
|
<img src="https://img.shields.io/badge/native%20deps-zero-green" alt="Zero native deps">
|
|
54
54
|
<img src="https://img.shields.io/badge/LLM%20cost-$0-green" alt="Zero LLM cost">
|
|
55
55
|
</p>
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
> **
|
|
59
|
+
> **EngramX v3.0 "Spine" shipped 2026-04-24** — the biggest release since v1.0. The spine is now **extensible**: any MCP server becomes an EngramX provider via a 10-line plugin file. **Pre-mortem mistake-guard** warns before you repeat a bug. **Bi-temporal mistake memory** — refactored-away mistakes stop firing. **Anthropic Auto-Memory bridge** reads Claude Code's own consolidated memory. **SSE-streaming** packets render progressively. `engram gen` dual-emits `AGENTS.md` + `CLAUDE.md` by default. **89.1% measured real-world token savings** on 87 source files — reproducible in one command. 878 tests, CI green on Ubuntu + Windows × Node 20 + 22. Zero cloud, zero telemetry. See [CHANGELOG.md](CHANGELOG.md) for the full diff.
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
-
#
|
|
63
|
+
# EngramX — the cached context spine for AI coding agents.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Your AI coding agent keeps re-reading the same files. Every `Read`, every `Edit`, every `cat` re-pays for context you've already paid for.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
**EngramX is the spine.** It intercepts every file read at the tool boundary, answers from a pre-assembled context packet held in **three layers of cache** — a knowledge graph the agent has already "paid" to build, a per-provider SQLite cache of external lookups, and an in-memory LRU of recent queries — and hands the agent a single ~500-token response instead of a raw file.
|
|
68
|
+
|
|
69
|
+
The agent gets what it needs. You stop paying for context you've already paid for. And **every plugin you add elevates the savings further** — Serena for LSP symbols, GitHub MCP for issue context, Sentry MCP for production errors, Supabase / Neon for schema. Each one closes another context leak the agent would otherwise burn tokens researching.
|
|
70
|
+
|
|
71
|
+
**Measured savings on a reproducible benchmark: 89.1%.** Not estimated. 85 of 87 real source files saved tokens. Best case 98.4% (18,820 tokens → 306).
|
|
72
|
+
|
|
73
|
+
### One command to everything
|
|
68
74
|
|
|
69
75
|
```bash
|
|
70
76
|
npm install -g engramx
|
|
71
77
|
cd ~/my-project
|
|
72
|
-
engram
|
|
73
|
-
engram install-hook
|
|
78
|
+
engram setup
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
That's the
|
|
81
|
+
That's the install. `engram setup` runs `engram init` (builds the graph), `engram install-hook` (wires the Sentinel into your AI tool), detects your IDE, dual-emits `AGENTS.md` + `CLAUDE.md`, then runs `engram doctor` to verify everything green. Under 30 seconds on most projects. Works in Claude Code, Cursor, Codex CLI, Windsurf, GitHub Copilot Chat, JetBrains Junie, Aider, Zed, Continue — any agent that reads `AGENTS.md` or uses MCP.
|
|
82
|
+
|
|
83
|
+
The **next session** you open starts with the spine pre-loaded: project brief already in context, file reads intercepted, a live HUD showing cumulative savings, bi-temporal mistakes waiting to warn you, and any plugins you've added already answering their domains.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## I'm not a developer — what does this actually do?
|
|
88
|
+
|
|
89
|
+
Short answer: **your AI coding assistant stops charging you for the same information twice.**
|
|
90
|
+
|
|
91
|
+
Long answer:
|
|
92
|
+
|
|
93
|
+
1. You ask your AI assistant (Claude Code, Cursor, Codex, whatever) to help with a file.
|
|
94
|
+
2. The assistant tries to read that file. Normally it reads the whole thing, pays for every byte in tokens, and throws most of it away.
|
|
95
|
+
3. EngramX catches the read, answers with a cached summary (the 50–200 lines the agent actually needs, plus context from your git history, past mistakes, library docs, and anything else useful), and lets the agent work from that.
|
|
96
|
+
4. Your monthly AI bill drops. Multi-hour sessions stop hitting rate limits. The agent stops re-introducing bugs you already fixed — because EngramX remembers what broke.
|
|
97
|
+
|
|
98
|
+
It runs on your laptop. It doesn't send your code anywhere. It's Apache 2.0. There's no account, no login, no cloud. You install it once and forget it's there.
|
|
99
|
+
|
|
100
|
+
**Want even bigger savings?** Install a plugin. Each one closes a different context leak — see [Plugins multiply the savings](#plugins-multiply-the-savings) below. Drop a 10-line `.mjs` file in `~/.engram/plugins/` and the next session uses it.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Proof, not promises
|
|
105
|
+
|
|
106
|
+
Everything above is measured, not estimated. `bench/real-world.ts` runs the full resolver against real files in this repo and compares the rich-packet token cost to the raw-file-read cost. Reproducible in one command on any project.
|
|
107
|
+
|
|
108
|
+
Latest run (2026-04-24, 87 source files — full report at [`bench/results/real-world-2026-04-24.md`](bench/results/real-world-2026-04-24.md)):
|
|
109
|
+
|
|
110
|
+
| Metric | Value |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Baseline tokens (87 files read raw) | **163,122** |
|
|
113
|
+
| engramx tokens (rich packets) | **17,722** |
|
|
114
|
+
| Aggregate savings | **89.1%** |
|
|
115
|
+
| Median per-file savings | 84.2% |
|
|
116
|
+
| Files where engramx saved tokens | 85 of 87 |
|
|
117
|
+
| Best case (`src/cli.ts`) | 98.4% (18,820 → 306) |
|
|
118
|
+
|
|
119
|
+
Reproduce on your own code:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd your-project
|
|
123
|
+
engram init # first-time setup for this project
|
|
124
|
+
npx tsx /path/to/engram/bench/real-world.ts --project . --files 50
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The bench writes a JSON + Markdown report per run into `bench/results/`. Small projects score lower; dense structural projects score higher. It's real arithmetic on your files — you can audit every number.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## What engramx is not
|
|
132
|
+
|
|
133
|
+
The "engram" name is contested. To save you a search:
|
|
134
|
+
|
|
135
|
+
- **Not Go-Engram** ([Gentleman-Programming/engram](https://github.com/Gentleman-Programming/engram)) — different project, Go binary, salience-gated chat memory. Ships under `engram` (without the `x`).
|
|
136
|
+
- **Not DeepSeek's "Engram" paper** — January 2026 academic work on conditional memory. Research artifact, not a product.
|
|
137
|
+
- **Not MemPalace** — adjacent positioning ("knowledge-graph memory," "method-of-loci"), but conversational memory, not code-structural.
|
|
138
|
+
|
|
139
|
+
`engramx` is specifically: **a local-first context spine for AI coding agents that hooks into your IDE's tool boundary, indexes your code via tree-sitter + LSP, remembers past mistakes, and assembles ~500-token context packets in place of raw file reads.** Open source, Apache 2.0, single npm install.
|
|
77
140
|
|
|
78
141
|
---
|
|
79
142
|
|
|
@@ -128,6 +191,14 @@ See also the **Sessions** tab (cumulative breakdown + sparkline) in [`assets/scr
|
|
|
128
191
|
|
|
129
192
|
## Benchmark
|
|
130
193
|
|
|
194
|
+
engramx ships with two benchmarks — use whichever fits your workflow.
|
|
195
|
+
|
|
196
|
+
### Real-world bench (new in v3.0, preferred)
|
|
197
|
+
|
|
198
|
+
`npx tsx bench/real-world.ts --project . --files 50` runs the full resolver against real files in any project and outputs exact token numbers. See the [Proof](#proof-not-promises) section above for the reproducible 89.1% result on engramx itself.
|
|
199
|
+
|
|
200
|
+
### Structured task bench (CI regression)
|
|
201
|
+
|
|
131
202
|
Measured across 10 structured coding tasks against a baseline of reading the relevant files directly. No synthetic data. No cherry-picked queries.
|
|
132
203
|
|
|
133
204
|
| Task | Baseline (tokens) | engram (tokens) | Savings |
|
|
@@ -144,28 +215,46 @@ Measured across 10 structured coding tasks against a baseline of reading the rel
|
|
|
144
215
|
| task-10-cross-file-flow | 12,800 | 1,400 | 89.1% |
|
|
145
216
|
| **Aggregate** | **7,130** | **845** | **88.1%** |
|
|
146
217
|
|
|
147
|
-
Run
|
|
218
|
+
Run it yourself: `npx tsx bench/runner.ts` (structured fixtures) or `npx tsx bench/real-world.ts` (live resolver on real files).
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Plugins multiply the savings
|
|
223
|
+
|
|
224
|
+
The 89.1% number is engramx with its 9 built-in providers. Every MCP server you plug in closes another context gap the agent would otherwise burn tokens researching. And because every provider is budget-capped and the resolver is budget-weighted + mistakes-boost reranked, more plugins = more *relevant* context without packet bloat.
|
|
225
|
+
|
|
226
|
+
| Plugin | Closes this gap | Install |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| **Serena** (LSP symbols, 20+ languages) | Cross-file references engramx's AST can't resolve precisely — kills the grep-then-read loop | `cp docs/plugins/examples/serena-plugin.mjs ~/.engram/plugins/` |
|
|
229
|
+
| **GitHub MCP** (issues, PRs, commits) | Recent PR discussion & issue history for the file being edited | `engram plugin install github` |
|
|
230
|
+
| **Sentry MCP** (production errors) | "What broke in prod for this file" — cuts the open-dashboard → paste-trace loop | `engram plugin install sentry` |
|
|
231
|
+
| **Supabase / Neon** (schema, RLS) | Database schema context when editing queries / migrations / ORM models | `engram plugin install supabase` |
|
|
232
|
+
| **Context7** (library docs) | Always-current API surface for your actual imports | shipped as a built-in |
|
|
233
|
+
| **Anthropic Auto-Memory** | Claude Code's own consolidated project memory | shipped — auto-detected when `~/.claude/projects/…/memory/MEMORY.md` exists |
|
|
234
|
+
|
|
235
|
+
Writing a plugin is **~10 lines** — see [`docs/plugins/README.md`](docs/plugins/README.md) for the full spec + examples.
|
|
148
236
|
|
|
149
237
|
---
|
|
150
238
|
|
|
151
239
|
## What It Does
|
|
152
240
|
|
|
153
|
-
engram sits between your AI agent and the filesystem. When the agent reads a file, engram checks its knowledge graph. If the file is covered with sufficient confidence, it blocks the read and injects a compact context packet instead. The packet is assembled from up to
|
|
241
|
+
engram sits between your AI agent and the filesystem. When the agent reads a file, engram checks its knowledge graph. If the file is covered with sufficient confidence, it blocks the read and injects a compact context packet instead. The packet is assembled from up to 9 built-in providers plus any plugins you've added, all pre-cached at session start.
|
|
154
242
|
|
|
155
|
-
**The
|
|
243
|
+
**The 9 built-in providers (v3.0):**
|
|
156
244
|
|
|
157
245
|
| Provider | Source | Confidence | Latency |
|
|
158
246
|
|----------|--------|:-----------:|:-------:|
|
|
159
247
|
| `engram:ast` | Tree-sitter parse (10 languages) | 1.0 | <50ms |
|
|
160
248
|
| `engram:structure` | Regex heuristics (fallback) | 0.85 | <50ms |
|
|
161
|
-
| `engram:mistakes` | Past failure nodes
|
|
249
|
+
| `engram:mistakes` | Past failure nodes (bi-temporal — stale mistakes filtered out) | — | <10ms |
|
|
250
|
+
| `anthropic:memory` | Claude Code's auto-managed `MEMORY.md` index (v3.0) | 0.85 | <10ms |
|
|
162
251
|
| `engram:git` | Co-change patterns, churn, authorship | — | <100ms |
|
|
163
252
|
| `mempalace` | Decisions, learnings, project context | — | <5ms cached |
|
|
164
253
|
| `context7` | Library API docs for detected imports | — | <5ms cached |
|
|
165
254
|
| `obsidian` | Project notes, architecture docs | — | <5ms cached |
|
|
166
255
|
| `engram:lsp` | Live diagnostics captured as mistake nodes | — | on-event |
|
|
167
256
|
|
|
168
|
-
External providers cache into SQLite at SessionStart. Per-read resolution is a cache lookup, not a live call. If a provider is unavailable it is skipped silently — you always get at least the structural summary.
|
|
257
|
+
External providers cache into SQLite at SessionStart. Per-read resolution is a cache lookup, not a live call. If a provider is unavailable it is skipped silently — you always get at least the structural summary. **Plus: any MCP server becomes a provider via a 10-line plugin file** — see [Plugins multiply the savings](#plugins-multiply-the-savings) above.
|
|
169
258
|
|
|
170
259
|
**The 9 hook handlers:**
|
|
171
260
|
|
|
@@ -262,7 +351,7 @@ engram hooks install # auto-rebuild graph on every git commit
|
|
|
262
351
|
|------|-------------|-------------|
|
|
263
352
|
| Graph only | `engram init` | CLI queries, MCP server, `engram gen` for CLAUDE.md |
|
|
264
353
|
| + Sentinel | `engram install-hook` | Automatic Read interception, Edit warnings, session briefs, HUD |
|
|
265
|
-
| + Context Spine | Configure providers.json | Rich packets from
|
|
354
|
+
| + Context Spine | Configure providers.json | Rich packets from 9 built-ins + any MCP plugin per read |
|
|
266
355
|
| + Skills index | `engram init --with-skills` | Graph includes your `~/.claude/skills/` |
|
|
267
356
|
| + Git hooks | `engram hooks install` | Graph rebuilds on every commit, stays current |
|
|
268
357
|
| + HTTP server | `engram server --http` | REST API on port 7337 for external tooling |
|
|
@@ -7,7 +7,7 @@ function buildSection(heading, lines) {
|
|
|
7
7
|
return [`## ${heading}`, "", ...lines, ""].join("\n");
|
|
8
8
|
}
|
|
9
9
|
async function generateAiderContext(projectRoot) {
|
|
10
|
-
const { getStore } = await import("./core-
|
|
10
|
+
const { getStore } = await import("./core-77F2BVYV.js");
|
|
11
11
|
const store = await getStore(projectRoot);
|
|
12
12
|
try {
|
|
13
13
|
const allNodes = store.getAllNodes();
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// src/db/migrate.ts
|
|
2
2
|
import { existsSync, copyFileSync } from "fs";
|
|
3
|
-
var CURRENT_SCHEMA_VERSION =
|
|
3
|
+
var CURRENT_SCHEMA_VERSION = 8;
|
|
4
4
|
var DOWN_MIGRATIONS = {
|
|
5
|
+
// v3.0: bi-temporal mistake validity. SQLite only added DROP COLUMN in
|
|
6
|
+
// 3.35 (2021); older sql.js builds may not support it. We don't depend
|
|
7
|
+
// on the columns being absent — leaving them in place is safe. The index
|
|
8
|
+
// CAN be dropped cleanly.
|
|
9
|
+
8: `DROP INDEX IF EXISTS idx_nodes_validity;`,
|
|
5
10
|
7: `DROP TABLE IF EXISTS query_cache; DROP TABLE IF EXISTS pattern_cache;`,
|
|
6
11
|
6: `DROP TABLE IF EXISTS engram_config;`,
|
|
7
12
|
5: `DROP TABLE IF EXISTS provider_cache;`,
|
|
@@ -14,6 +19,13 @@ var DOWN_MIGRATIONS = {
|
|
|
14
19
|
// 1 → 0 drops the entire schema. We require `engram init` for that.
|
|
15
20
|
1: `DROP TABLE IF EXISTS stats; DROP TABLE IF EXISTS edges; DROP TABLE IF EXISTS nodes;`
|
|
16
21
|
};
|
|
22
|
+
function addColumnIfMissing(db, table, column, ddl) {
|
|
23
|
+
const result = db.exec(`PRAGMA table_info(${table})`);
|
|
24
|
+
const existing = (result[0]?.values ?? []).map((row) => row[1]);
|
|
25
|
+
if (!existing.includes(column)) {
|
|
26
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
var MIGRATIONS = {
|
|
18
30
|
// v0.1.0: Initial schema
|
|
19
31
|
1: `
|
|
@@ -85,7 +97,28 @@ CREATE TABLE IF NOT EXISTS pattern_cache (
|
|
|
85
97
|
graph_version INTEGER NOT NULL,
|
|
86
98
|
hit_count INTEGER NOT NULL DEFAULT 0
|
|
87
99
|
);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_query_cache_file ON query_cache(file_path)
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_query_cache_file ON query_cache(file_path);`,
|
|
101
|
+
// v3.0.0: Bi-temporal validity for mistake nodes (and any other node kind
|
|
102
|
+
// that wants it). `valid_until` is the unix-ms timestamp after which the
|
|
103
|
+
// mistake should NO LONGER surface in context (e.g. the referenced code
|
|
104
|
+
// was refactored away). NULL = still valid (back-compat default for all
|
|
105
|
+
// existing rows). `invalidated_by_commit` records the git SHA that caused
|
|
106
|
+
// the invalidation, for audit + future "explain why this mistake stopped
|
|
107
|
+
// firing" UX. Index is partial — only mistakes with an explicit validity
|
|
108
|
+
// window pay storage cost.
|
|
109
|
+
//
|
|
110
|
+
// Function-based because ALTER TABLE ADD COLUMN isn't idempotent in
|
|
111
|
+
// SQLite — re-running on a db that already has the columns throws
|
|
112
|
+
// 'duplicate column name'. We pre-check via PRAGMA table_info.
|
|
113
|
+
8: (db) => {
|
|
114
|
+
addColumnIfMissing(db, "nodes", "valid_until", "valid_until INTEGER");
|
|
115
|
+
addColumnIfMissing(db, "nodes", "invalidated_by_commit", "invalidated_by_commit TEXT");
|
|
116
|
+
db.exec(`
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_validity
|
|
118
|
+
ON nodes(kind, valid_until)
|
|
119
|
+
WHERE kind = 'mistake' AND valid_until IS NOT NULL;
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
89
122
|
};
|
|
90
123
|
function getSchemaVersion(db) {
|
|
91
124
|
try {
|
|
@@ -116,9 +149,13 @@ function runMigrations(db, dbPath) {
|
|
|
116
149
|
);
|
|
117
150
|
let migrationsRun = 0;
|
|
118
151
|
for (let v = fromVersion + 1; v <= CURRENT_SCHEMA_VERSION; v++) {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
152
|
+
const step = MIGRATIONS[v];
|
|
153
|
+
if (step) {
|
|
154
|
+
if (typeof step === "string") {
|
|
155
|
+
db.exec(step);
|
|
156
|
+
} else {
|
|
157
|
+
step(db);
|
|
158
|
+
}
|
|
122
159
|
migrationsRun++;
|
|
123
160
|
}
|
|
124
161
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyArgTemplate
|
|
3
|
+
} from "./chunk-ZUC6OXSL.js";
|
|
4
|
+
|
|
5
|
+
// src/providers/mcp-client.ts
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
function estimateTokens(text) {
|
|
9
|
+
return Math.ceil(text.length / 4);
|
|
10
|
+
}
|
|
11
|
+
var McpClientWrapper = class {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
config;
|
|
16
|
+
client = null;
|
|
17
|
+
transport = null;
|
|
18
|
+
connectingPromise = null;
|
|
19
|
+
shutdownRegistered = false;
|
|
20
|
+
lastErrorAt = 0;
|
|
21
|
+
errorBackoffMs = 3e4;
|
|
22
|
+
/**
|
|
23
|
+
* Connect once (idempotent). Concurrent callers share one promise so
|
|
24
|
+
* we never spawn the server twice. On failure we set a backoff window
|
|
25
|
+
* so the next Read doesn't re-try spawn immediately.
|
|
26
|
+
*/
|
|
27
|
+
async connect() {
|
|
28
|
+
if (this.client) return;
|
|
29
|
+
if (this.connectingPromise) return this.connectingPromise;
|
|
30
|
+
if (Date.now() - this.lastErrorAt < this.errorBackoffMs) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`[mcp] ${this.config.name}: in error backoff (last failure ${Math.round(
|
|
33
|
+
(Date.now() - this.lastErrorAt) / 1e3
|
|
34
|
+
)}s ago)`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
this.connectingPromise = this.doConnect().catch((err) => {
|
|
38
|
+
this.lastErrorAt = Date.now();
|
|
39
|
+
this.client = null;
|
|
40
|
+
this.transport = null;
|
|
41
|
+
throw err;
|
|
42
|
+
}).finally(() => {
|
|
43
|
+
this.connectingPromise = null;
|
|
44
|
+
});
|
|
45
|
+
return this.connectingPromise;
|
|
46
|
+
}
|
|
47
|
+
async doConnect() {
|
|
48
|
+
if (this.config.transport !== "stdio") {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`[mcp] ${this.config.name}: http transport not yet implemented`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const transport = new StdioClientTransport({
|
|
54
|
+
command: this.config.command,
|
|
55
|
+
args: this.config.args ? [...this.config.args] : void 0,
|
|
56
|
+
env: this.config.env ? { ...this.config.env } : void 0,
|
|
57
|
+
cwd: this.config.cwd,
|
|
58
|
+
// Pipe stderr so a chatty server doesn't spam the parent's stderr
|
|
59
|
+
// during normal operation. Re-enable "inherit" for debugging.
|
|
60
|
+
stderr: "pipe"
|
|
61
|
+
});
|
|
62
|
+
const client = new Client(
|
|
63
|
+
{ name: "engramx", version: "3.0.0" },
|
|
64
|
+
{ capabilities: {} }
|
|
65
|
+
);
|
|
66
|
+
await client.connect(transport);
|
|
67
|
+
this.transport = transport;
|
|
68
|
+
this.client = client;
|
|
69
|
+
if (!this.shutdownRegistered) {
|
|
70
|
+
this.registerShutdown();
|
|
71
|
+
this.shutdownRegistered = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Call a single tool with a timeout. Returns null on error (never
|
|
76
|
+
* throws). Caller is responsible for aggregating multiple tool results.
|
|
77
|
+
*/
|
|
78
|
+
async callTool(toolName, args, timeoutMs) {
|
|
79
|
+
try {
|
|
80
|
+
await this.connect();
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!this.client) return null;
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
87
|
+
try {
|
|
88
|
+
const result = await this.client.callTool(
|
|
89
|
+
{ name: toolName, arguments: args },
|
|
90
|
+
void 0,
|
|
91
|
+
{ signal: abort.signal, timeout: timeoutMs }
|
|
92
|
+
);
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
const blocks = Array.isArray(result?.content) ? result.content : [];
|
|
95
|
+
const text = blocks.map((b) => {
|
|
96
|
+
const block = b;
|
|
97
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
98
|
+
return block.text;
|
|
99
|
+
}
|
|
100
|
+
return `[${block.type ?? "unknown"} block]`;
|
|
101
|
+
}).join("\n").trim();
|
|
102
|
+
if (text.length === 0) return null;
|
|
103
|
+
return { content: text };
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Close the connection. Safe to call on an unconnected client. */
|
|
111
|
+
async disconnect() {
|
|
112
|
+
const client = this.client;
|
|
113
|
+
const transport = this.transport;
|
|
114
|
+
this.client = null;
|
|
115
|
+
this.transport = null;
|
|
116
|
+
try {
|
|
117
|
+
await client?.close();
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await transport?.close();
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
registerShutdown() {
|
|
126
|
+
const shutdown = () => {
|
|
127
|
+
void this.disconnect();
|
|
128
|
+
};
|
|
129
|
+
process.once("SIGTERM", shutdown);
|
|
130
|
+
process.once("SIGINT", shutdown);
|
|
131
|
+
process.once("beforeExit", shutdown);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
function createMcpProvider(config) {
|
|
135
|
+
const wrapper = new McpClientWrapper(config);
|
|
136
|
+
const tokenBudget = config.tokenBudget ?? 200;
|
|
137
|
+
const timeoutMs = config.timeoutMs ?? 2e3;
|
|
138
|
+
const enabled = config.enabled ?? true;
|
|
139
|
+
return {
|
|
140
|
+
name: config.name,
|
|
141
|
+
label: config.label,
|
|
142
|
+
// Tier 2 — external process/HTTP with cache support. Matches
|
|
143
|
+
// context7/obsidian tier semantics in the existing resolver.
|
|
144
|
+
tier: 2,
|
|
145
|
+
tokenBudget,
|
|
146
|
+
timeoutMs,
|
|
147
|
+
async isAvailable() {
|
|
148
|
+
if (!enabled) return false;
|
|
149
|
+
if (config.tools.length === 0) return false;
|
|
150
|
+
return true;
|
|
151
|
+
},
|
|
152
|
+
async resolve(filePath, context) {
|
|
153
|
+
try {
|
|
154
|
+
const results = await Promise.allSettled(
|
|
155
|
+
config.tools.map((tool) => callSingleTool(wrapper, tool, filePath, context, timeoutMs))
|
|
156
|
+
);
|
|
157
|
+
const sections = [];
|
|
158
|
+
let highestConfidence = 0;
|
|
159
|
+
for (const outcome of results) {
|
|
160
|
+
if (outcome.status === "fulfilled" && outcome.value) {
|
|
161
|
+
sections.push(outcome.value.content);
|
|
162
|
+
highestConfidence = Math.max(
|
|
163
|
+
highestConfidence,
|
|
164
|
+
outcome.value.confidence
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (sections.length === 0) return null;
|
|
169
|
+
let combined = sections.join("\n\n");
|
|
170
|
+
const budget = tokenBudget;
|
|
171
|
+
if (estimateTokens(combined) > budget) {
|
|
172
|
+
const lines = combined.split("\n");
|
|
173
|
+
const kept = [];
|
|
174
|
+
let used = 0;
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
const lineTokens = estimateTokens(line) + 1;
|
|
177
|
+
if (used + lineTokens > budget) break;
|
|
178
|
+
kept.push(line);
|
|
179
|
+
used += lineTokens;
|
|
180
|
+
}
|
|
181
|
+
combined = kept.join("\n") + "\n\u2026 [truncated to fit budget]";
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
provider: config.name,
|
|
185
|
+
content: combined,
|
|
186
|
+
confidence: highestConfidence,
|
|
187
|
+
cached: false
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function callSingleTool(wrapper, tool, filePath, context, timeoutMs) {
|
|
196
|
+
const args = applyArgTemplate(tool.args, {
|
|
197
|
+
filePath,
|
|
198
|
+
projectRoot: context.projectRoot,
|
|
199
|
+
imports: context.imports
|
|
200
|
+
});
|
|
201
|
+
const result = await wrapper.callTool(tool.name, args, timeoutMs);
|
|
202
|
+
if (!result) return null;
|
|
203
|
+
return {
|
|
204
|
+
content: result.content,
|
|
205
|
+
confidence: tool.confidence ?? 0.75
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
var __internalsForTesting = {
|
|
209
|
+
McpClientWrapper
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export {
|
|
213
|
+
createMcpProvider,
|
|
214
|
+
__internalsForTesting
|
|
215
|
+
};
|