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 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="engram — AI coding memory" width="100%">
2
+ <img src="assets/banner-v3.png" alt="EngramXthe 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-640%20passing-brightgreen" alt="Tests">
51
- <img src="https://img.shields.io/badge/providers-8%20%2B%20plugins-blue" alt="8 Providers + plugins">
52
- <img src="https://img.shields.io/badge/token%20savings-88.1%25%20measured-orange" alt="88% Proven Savings">
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
- > **v2.0 "Ecosystem" shipped 2026-04-17** — web dashboard at `engram ui`, 3-layer memory cache (23μs/op at 99% hit rate), provider plugin system (`~/.engram/plugins/*.mjs`), `engram cache` CLI, schema rollback with automatic backup, incremental re-indexing (78% faster on large repos), auto-bundled tree-sitter grammars, Windsurf + Neovim + Emacs integrations. See [CHANGELOG.md](CHANGELOG.md) for the full diff.
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
- # The context spine for AI coding agents.
63
+ # EngramX — the cached context spine for AI coding agents.
64
64
 
65
- engram intercepts every file read your AI agent makes and replaces it with a pre-assembled context packet structure, decisions, git history, library docs, and known issues — from 8 providers, delivered in a single ~500-token response. The agent gets what it needs without reading the file. You stop paying for context you've already paid for.
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
- This is not a tool the agent calls. It hooks at the Claude Code tool boundary. Every `Read`, `Edit`, `Write`, and `cat` is intercepted automatically.
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 init
73
- engram install-hook
78
+ engram setup
74
79
  ```
75
80
 
76
- That's the full setup. The next Claude Code session starts with a project brief already loaded, file reads intercepted, and a live HUD showing cumulative savings.
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 the benchmark yourself: `engram bench` or `engram stress-test` for the full suite.
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 8 providers in parallel, all pre-cached at session start.
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 8 providers:**
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 from graph | — | <10ms |
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 all 8 providers per read |
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-TSXA5XZH.js");
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 = 7;
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 sql = MIGRATIONS[v];
120
- if (sql) {
121
- db.exec(sql);
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
+ };