engramx 3.0.1 → 3.3.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,28 @@ All notable changes to engram are documented here. Format based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added — v3.3 "Cost Lens" (in progress, target: 2026-05-08)
10
+ - New `engram cost` subcommand: aggregates token-savings telemetry from existing `.engram/hook-log.jsonl` files across one or many project roots. Outputs a terminal table, JSON, or a weekly Markdown digest at `~/.engram/cost-report-YYYY-Www.md`.
11
+ - New `src/cost/` module: `types.ts` (CostEvent / CostSummary / CostConfig), `aggregator.ts` (read + summarize), `formatter.ts` (one-liner / table / Markdown digest), `digest.ts` (ISO-week digest writer with idempotent file output).
12
+ - 13 new tests in `tests/cost.test.ts`, hermetic — use tmp dirs with synthetic logs, no real engram state required.
13
+ - USD estimate uses configurable `inputUsdPerMillion` rate. Default $3.00/M matches Claude Sonnet 4.6 input pricing as of 2026-04-27.
14
+
15
+ ### Why
16
+ Cost Lens is the baseline for everything in the v3.3 → v4.0 roadmap. We need a measured number that survives between releases so future features (Mesh, Vector, Bridge) can be evaluated against the real-world impact, not against a single benchmark file. The PRD lives at `01-prds/03-engram-mesh-ruflo-integration-PRD.md`.
17
+
18
+ ## [3.0.2] — 2026-04-24 — "MCP Registry"
19
+
20
+ Chore release. No runtime changes. Adds the `mcpName` field to `package.json`
21
+ required by the Official MCP Registry (`registry.modelcontextprotocol.io`)
22
+ for namespace-ownership proof.
23
+
24
+ ### Added
25
+ - `package.json` → top-level `"mcpName": "io.github.NickCirv/engram"`. Registry-side check reads the published npm tarball's `package.json` and verifies the field matches the server name in `server.json`. Without it, `mcp-publisher publish` returns HTTP 400 with the guidance message.
26
+ - Also tightened `server.json` description fields to the registry's 100-char limit (top-level description + 5 environment-variable descriptions).
27
+
28
+ ### Why not bundled into 3.0.1
29
+ The `preuninstall` fix needed to ship ASAP to stop new users hitting the orphaned-hooks bug. MCP Registry integration was a separate problem surfaced during the submission flow.
30
+
9
31
  ## [3.0.1] — 2026-04-24 — "Clean Uninstall"
10
32
 
11
33
  **Patch release fixing the orphaned-hooks bug reported by @freenow82 within
package/README.md CHANGED
@@ -1,119 +1,181 @@
1
1
  <p align="center">
2
- <img src="assets/banner-v3.png" alt="EngramX — the cached context spine for AI coding agents (v3.0 'Spine')" width="100%">
2
+ <img src="assets/banner-v3.png" alt="EngramX — the memory layer for AI coding agents" width="100%">
3
3
  </p>
4
4
 
5
- <!-- ============================================================
6
- 24-second product showcase (Hyperframes-rendered MP4 + WebM).
7
- Source: docs/demos/showcase.html · scenes drive both the
8
- live HTML player and this MP4. Edit scene-table.md to change.
9
- If the MP4 isn't rendered yet, GitHub gracefully shows the
10
- poster image and links to the live HTML player.
11
- ============================================================ -->
12
5
  <p align="center">
13
- <video src="https://raw.githubusercontent.com/NickCirv/engram/main/docs/demos/showcase.mp4"
14
- controls
15
- muted
16
- playsinline
17
- poster="docs/demos/poster.svg"
18
- width="100%">
19
- <a href="docs/demos/showcase.html">
20
- <img src="docs/demos/poster.svg" alt="engram — 24-second showcase (click to open the live HTML player)" width="100%">
21
- </a>
22
- </video>
23
- </p>
24
-
25
- <p align="center">
26
- <sub>
27
- <a href="docs/install.html"><strong>Install Page</strong></a> ·
28
- <a href="docs/demos/showcase.html"><strong>Live Demo</strong></a> ·
29
- <a href="docs/demos/scene-table.md"><strong>Scene Table</strong></a> ·
30
- rendered with <a href="https://github.com/heygen-com/hyperframes">Hyperframes</a>
31
- </sub>
32
- </p>
33
-
34
- <p align="center">
35
- <a href="#install"><strong>Install</strong></a> ·
36
- <a href="#quickstart"><strong>Quickstart</strong></a> ·
37
- <a href="#dashboard"><strong>Dashboard</strong></a> ·
38
- <a href="#benchmark"><strong>Benchmark</strong></a> ·
39
- <a href="#ide-integrations"><strong>IDE Integrations</strong></a> ·
40
- <a href="#http-api"><strong>HTTP API</strong></a> ·
41
- <a href="#ecp-spec"><strong>ECP Spec</strong></a> ·
42
- <a href="#contributing"><strong>Contributing</strong></a>
6
+ <strong>The memory layer that stretches every Claude session.</strong>
43
7
  </p>
44
8
 
45
9
  <p align="center">
46
10
  <a href="https://github.com/NickCirv/engram/actions"><img src="https://github.com/NickCirv/engram/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
47
11
  <a href="https://www.npmjs.com/package/engramx"><img src="https://img.shields.io/npm/v/engramx?color=blue" alt="npm version"></a>
12
+ <a href="https://www.npmjs.com/package/engramx"><img src="https://img.shields.io/npm/dm/engramx?color=blue" alt="npm downloads"></a>
48
13
  <img src="https://img.shields.io/badge/license-Apache%202.0-blue" alt="License">
49
14
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node">
50
- <img src="https://img.shields.io/badge/tests-876%20passing-brightgreen" alt="Tests">
15
+ <img src="https://img.shields.io/badge/tests-878%20passing-brightgreen" alt="Tests">
51
16
  <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
17
  <img src="https://img.shields.io/badge/native%20deps-zero-green" alt="Zero native deps">
54
- <img src="https://img.shields.io/badge/LLM%20cost-$0-green" alt="Zero LLM cost">
18
+ <a href="https://discord.gg/engramx"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
19
+ <a href="https://github.com/NickCirv/engram/stargazers"><img src="https://img.shields.io/github/stars/NickCirv/engram?style=social" alt="Stars"></a>
20
+ </p>
21
+
22
+ <p align="center">
23
+ <a href="#anthropic-capped-your-week-engram-extends-it">Why</a> ·
24
+ <a href="#install">Install</a> ·
25
+ <a href="#per-agent-setup">Per-agent setup</a> ·
26
+ <a href="#see-what-your-agent-has-remembered">engram remembers</a> ·
27
+ <a href="#how-it-works">How it works</a> ·
28
+ <a href="ARCHITECTURE.md">Architecture</a> ·
29
+ <a href="https://discord.gg/engramx">Discord</a>
55
30
  </p>
56
31
 
57
32
  ---
58
33
 
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.
34
+ ## Anthropic capped your week. engram extends it.
35
+
36
+ In November 2025, Anthropic tightened weekly limits on Claude Pro and Max. Heavy Claude Code users now hit caps mid-week. Some by Wednesday. The honest reality nobody is naming out loud:
37
+
38
+ > **Most of your weekly tokens are spent re-introducing yourself to an agent that forgets.**
39
+
40
+ Every Monday starts from zero. The agent re-reads the codebase. Re-asks setup questions. Repeats last week's wrong fix. Re-decides architecture you already locked in. By Friday you're rate-limited. Not because you built a lot. Because the agent never got smarter.
41
+
42
+ engram is the memory layer that fixes that. A persistent knowledge graph, plus a mistake replay buffer, plus a provider mesh that wires in mempalace, obsidian, context7, MCP servers, and Anthropic's own auto-memory. The agent stops being single-shot. It learns from its own history.
60
43
 
61
44
  ---
62
45
 
63
- # EngramX the cached context spine for AI coding agents.
46
+ ### What changes when your agent has memory
64
47
 
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.
48
+ | | Without engram | With engram |
49
+ |---|---|---|
50
+ | **Monday** | Agent re-reads codebase from scratch (~40K tokens) | Reads structural graph (~3K tokens) |
51
+ | **Tuesday** | Repeats Monday's wrong fix | ⚠️ Warned: *"You tried this Monday, broke parser.rs:42"* |
52
+ | **Wednesday** | Re-decides architecture you already locked | Surfaces Monday's decision: *"We chose Saga over 2PC because…"* |
53
+ | **Thursday** | Asks the same 5 setup questions | Pulls config from `mempalace`, `obsidian`, `context7` providers |
54
+ | **Friday** | Cap hit by 3pm | Cap hit Sunday, if at all |
55
+
56
+ Token savings (89.1% measured per Read interception, reproducible benchmark below) are the side-effect. Compounding agent intelligence is the product.
57
+
58
+ ---
66
59
 
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.
60
+ ## Install
68
61
 
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.
62
+ ### macOS / Linux (recommended)
70
63
 
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).
64
+ ```bash
65
+ brew install engramx
66
+ ```
72
67
 
73
- ### One command to everything
68
+ ### Cross-platform fallback
74
69
 
75
70
  ```bash
76
71
  npm install -g engramx
77
- cd ~/my-project
78
- engram setup
79
72
  ```
80
73
 
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.
74
+ ### Zero-dep one-liner
82
75
 
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.
76
+ ```bash
77
+ curl -fsSL engramx.dev/install | sh
78
+ ```
79
+
80
+ Verify: `engram --version` should show `3.x` or later. Requires Node.js 20+. Zero native deps. No build tools, no Rust, no Python, no system libs.
81
+
82
+ > **Note:** "engram" the audio plugin and "engram" the neuroscience term are different things. We're `engramx` on npm, `engram` on the CLI. Also not [Go-Engram](https://github.com/Gentleman-Programming/engram) (a salience-gated chat memory in Go) and not DeepSeek's January 2026 "Engram" paper (research artifact, not a product).
84
83
 
85
84
  ---
86
85
 
87
- ## I'm not a developer — what does this actually do?
86
+ ## Per-agent setup
88
87
 
89
- Short answer: **your AI coding assistant stops charging you for the same information twice.**
88
+ One command for your stack:
90
89
 
91
- Long answer:
90
+ ```bash
91
+ engram init --agent claude # Claude Code (default)
92
+ engram init --agent cursor # Cursor
93
+ engram init --agent windsurf # Windsurf
94
+ engram init --agent codex # OpenAI Codex
95
+ engram init --agent gemini # Gemini CLI
96
+ engram init --agent cline # Cline / Roo Code
97
+ engram init --agent copilot # GitHub Copilot CLI
98
+ engram init --agent kilocode # Kilo Code
99
+ engram init --agent antigravity # Google Antigravity
100
+ ```
92
101
 
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.
102
+ One run wires the right hooks, settings, and per-agent config files. Restart your AI tool. engram is live.
97
103
 
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.
104
+ Prefer the all-in-one bootstrap? `engram setup` runs `engram init` + `engram install-hook` + IDE detection + dual-emits `AGENTS.md` and `CLAUDE.md` + `engram doctor`. Under 30 seconds on most projects.
99
105
 
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.
106
+ ---
101
107
 
102
- **Want out?** Clean uninstall is one command:
108
+ ## See what your agent has remembered
103
109
 
104
110
  ```bash
105
- npm uninstall -g engramx # 3.0.1+ auto-runs preuninstall hook-cleanup
111
+ $ engram remembers
112
+
113
+ 43 mistakes avoided ⚠️ surfaced before the agent could repeat them
114
+ 127 decisions surfaced 📜 prior architectural choices recalled in context
115
+ 18 cross-session bridges 🔗 sessions that picked up where the last one ended
116
+ 86K tokens saved 🎟️ ~ 4.3 hours of weekly cap, reclaimed
117
+ 7 days indexed 📅 since engram init
118
+
119
+ Your subscription, stretched.
120
+ ```
121
+
122
+ Cumulative since `engram init`. Run it weekly. Share the screenshot.
123
+
124
+ ---
125
+
126
+ ## How it works
127
+
128
+ ```
129
+ Without engram: With engram:
130
+
131
+ Claude → reads file.rs (8,000 tokens) Claude → reads file.rs
132
+
133
+ engram intercepts → graph context (800 tokens)
134
+
135
+ Claude sees: structure
136
+ + last week's mistakes (⚠️ pre-mortem)
137
+ + relevant decisions
138
+ + git co-changes
139
+ + cross-session memory
106
140
  ```
107
141
 
108
- If you installed 3.0.0 and ran `npm uninstall` before the 3.0.1 patch shipped, your Claude Code hooks may be orphaned. Run `engram repair-hooks --scope user` (install 3.0.1 first if needed) or see the [`CHANGELOG.md`](CHANGELOG.md#301--2026-04-24--clean-uninstall) for the manual `jq`-based recovery one-liner.
142
+ Nine providers ship by default and every one is pluggable:
143
+
144
+ | Provider | Surfaces |
145
+ |---|---|
146
+ | `structure` | AST-derived class/function/import graph of the project |
147
+ | `mistakes` | What broke last week. Pre-mortem warnings before the agent re-makes the error. Bi-temporal: refactored-away mistakes stop firing. |
148
+ | `git` | Hot files, co-change pairs, authorship signals |
149
+ | `mempalace` | Your local semantic memory (mempalace MCP / ChromaDB) |
150
+ | `context7` | Up-to-date library docs (Context7 MCP) |
151
+ | `obsidian` | Your knowledge vault, queried at agent-time |
152
+ | `anthropic-memory` | Anthropic's auto-memory bridge |
153
+ | `mcp-client` | Any MCP server. engram talks to all of them. |
154
+ | `lsp` | Live language-server symbols (Serena, etc.) |
155
+
156
+ Add your own: drop a 10-line `.mjs` into `~/.engram/plugins/`. Validated before install.
157
+
158
+ ---
159
+
160
+ ## Why this exists
161
+
162
+ Stateless agents are amnesiacs with PhDs. They solve the problem in front of them, then never get smarter at *your* codebase. Multiply that by Anthropic's weekly caps and every session burns tokens re-learning what last session already learned.
163
+
164
+ engram is the spine that connects sessions. It does what stateless tools physically can't:
165
+
166
+ 1. **Persistence.** `.engram/graph.db` survives every restart, every cap reset, every laptop reboot. Your agent gets a brain that remembers.
167
+ 2. **Mistake memory.** Pre-mortem warnings before the agent repeats last week's error. Surfaced at the top of context, automatically.
168
+ 3. **Provider mesh.** Runtime composition across knowledge sources you already use. mempalace, obsidian, context7, MCP servers, all wired in.
169
+
170
+ Token compression is downstream of those.
109
171
 
110
172
  ---
111
173
 
112
174
  ## Proof, not promises
113
175
 
114
- 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.
176
+ Everything above is measured. `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.
115
177
 
116
- 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)):
178
+ 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)):
117
179
 
118
180
  | Metric | Value |
119
181
  |---|---|
@@ -128,23 +190,36 @@ Reproduce on your own code:
128
190
 
129
191
  ```bash
130
192
  cd your-project
131
- engram init # first-time setup for this project
193
+ engram init
132
194
  npx tsx /path/to/engram/bench/real-world.ts --project . --files 50
133
195
  ```
134
196
 
135
- 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.
197
+ Small projects score lower. Dense structural projects score higher. It's real arithmetic on your files. You can audit every number.
198
+
199
+ ---
200
+
201
+ ## Companion tools
202
+
203
+ engram compresses what the codebase *is* (file contents into graph context). For compressing what the system is *doing* (shell command output) pair it with [rtk](https://github.com/rtk-ai/rtk):
204
+
205
+ ```bash
206
+ brew install rtk # 60-90% savings on git/npm/cargo/grep/etc. (Bash)
207
+ brew install engramx # 89% savings + memory + mistake-guard (Read)
208
+ ```
209
+
210
+ Both register PreToolUse hooks. They don't conflict. rtk owns Bash, engram owns Read. Run both for a 3-5x weekly cap stretch end to end.
136
211
 
137
212
  ---
138
213
 
139
- ## What engramx is not
214
+ ## Clean uninstall
140
215
 
141
- The "engram" name is contested. To save you a search:
216
+ One command:
142
217
 
143
- - **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`).
144
- - **Not DeepSeek's "Engram" paper** — January 2026 academic work on conditional memory. Research artifact, not a product.
145
- - **Not MemPalace** — adjacent positioning ("knowledge-graph memory," "method-of-loci"), but conversational memory, not code-structural.
218
+ ```bash
219
+ npm uninstall -g engramx # 3.0.1+ auto-runs preuninstall hook-cleanup
220
+ ```
146
221
 
147
- `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.
222
+ If you installed 3.0.0 and ran `npm uninstall` before the 3.0.1 patch shipped, your Claude Code hooks may be orphaned. Run `engram repair-hooks --scope user` (install 3.0.1 first) or see the [`CHANGELOG.md`](CHANGELOG.md#301--2026-04-24--clean-uninstall) for the manual `jq`-based recovery one-liner.
148
223
 
149
224
  ---
150
225
 
@@ -293,18 +368,6 @@ External providers cache into SQLite at SessionStart. Per-read resolution is a c
293
368
 
294
369
  ---
295
370
 
296
- ## Install
297
-
298
- ```bash
299
- npm install -g engramx
300
- ```
301
-
302
- Requires Node.js 20+. Zero native dependencies. No build tools. Local SQLite via sql.js WASM — no Rust, no Python, no system libs.
303
-
304
- > **Prefer a designed walkthrough?** Open [**docs/install.html**](docs/install.html) — three-step install, benefits matrix, IDE coverage, FAQ. Local file, opens in any browser. Brand-matched terminal-mono aesthetic.
305
-
306
- ---
307
-
308
371
  ## Quickstart
309
372
 
310
373
  **One command, zero friction:**
package/dist/cli.js CHANGED
@@ -1393,6 +1393,51 @@ async function handleCwdChanged(payload) {
1393
1393
  }
1394
1394
  }
1395
1395
 
1396
+ // src/cost/instrument.ts
1397
+ import { statSync as statSync3 } from "fs";
1398
+ var CHARS_PER_TOKEN = 4;
1399
+ function tokensFromChars(chars) {
1400
+ if (!Number.isFinite(chars) || chars <= 0) return 0;
1401
+ return Math.ceil(chars / CHARS_PER_TOKEN);
1402
+ }
1403
+ function extractInjectedTokens(result) {
1404
+ if (!result || typeof result !== "object") return 0;
1405
+ try {
1406
+ const hook = result.hookSpecificOutput;
1407
+ if (!hook || typeof hook !== "object") return 0;
1408
+ const reason = hook.permissionDecisionReason;
1409
+ if (typeof reason === "string" && reason.length > 0) {
1410
+ return tokensFromChars(reason.length);
1411
+ }
1412
+ const ctx = hook.additionalContext;
1413
+ if (typeof ctx === "string" && ctx.length > 0) {
1414
+ return tokensFromChars(ctx.length);
1415
+ }
1416
+ } catch {
1417
+ }
1418
+ return 0;
1419
+ }
1420
+ function estimateWouldHaveReadTokens(tool, filePath) {
1421
+ if (tool !== "Read") return 0;
1422
+ if (!filePath || typeof filePath !== "string") return 0;
1423
+ try {
1424
+ const size = statSync3(filePath).size;
1425
+ return tokensFromChars(size);
1426
+ } catch {
1427
+ return 0;
1428
+ }
1429
+ }
1430
+ function composeCostFields(tool, filePath, result) {
1431
+ const injected = extractInjectedTokens(result);
1432
+ const wouldHaveRead = estimateWouldHaveReadTokens(tool, filePath);
1433
+ const tokensSaved = Math.max(0, wouldHaveRead - injected);
1434
+ const out = {};
1435
+ if (wouldHaveRead > 0) out.wouldHaveRead = wouldHaveRead;
1436
+ if (injected > 0) out.injected = injected;
1437
+ if (tokensSaved > 0) out.tokensSaved = tokensSaved;
1438
+ return out;
1439
+ }
1440
+
1396
1441
  // src/intercept/dispatch.ts
1397
1442
  function validatePayload(raw) {
1398
1443
  if (raw === null || typeof raw !== "object") return null;
@@ -1468,11 +1513,13 @@ async function dispatchPreToolUse(payload) {
1468
1513
  if (projectRoot) {
1469
1514
  const decision = extractPreToolDecision(result);
1470
1515
  const filePath = typeof handlerPayload.tool_input?.file_path === "string" ? handlerPayload.tool_input.file_path : void 0;
1516
+ const cost = composeCostFields(tool, filePath, result);
1471
1517
  logHookEvent(projectRoot, {
1472
1518
  event: "PreToolUse",
1473
1519
  tool,
1474
1520
  path: filePath,
1475
- decision
1521
+ decision,
1522
+ ...cost
1476
1523
  });
1477
1524
  }
1478
1525
  }
@@ -1494,7 +1541,7 @@ function extractPreToolDecision(result) {
1494
1541
 
1495
1542
  // src/dashboard.ts
1496
1543
  import chalk from "chalk";
1497
- import { existsSync as existsSync5, statSync as statSync3 } from "fs";
1544
+ import { existsSync as existsSync5, statSync as statSync4 } from "fs";
1498
1545
  import { join as join5, resolve as resolve6, basename as basename4 } from "path";
1499
1546
  var AMBER = chalk.hex("#d97706");
1500
1547
  var DIM = chalk.dim;
@@ -1617,7 +1664,7 @@ function startDashboard(projectRoot, options = {}) {
1617
1664
  try {
1618
1665
  const logPath = join5(root, ".engram", "hook-log.jsonl");
1619
1666
  if (existsSync5(logPath)) {
1620
- const currentSize = statSync3(logPath).size;
1667
+ const currentSize = statSync4(logPath).size;
1621
1668
  if (currentSize !== lastSize) {
1622
1669
  cachedEntries = readHookLog(root);
1623
1670
  lastSize = currentSize;
@@ -1680,7 +1727,7 @@ import {
1680
1727
  readFileSync as readFileSync3,
1681
1728
  writeFileSync,
1682
1729
  renameSync,
1683
- statSync as statSync4
1730
+ statSync as statSync5
1684
1731
  } from "fs";
1685
1732
  import { join as join6 } from "path";
1686
1733
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
@@ -1757,7 +1804,7 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
1757
1804
  try {
1758
1805
  let existing = "";
1759
1806
  if (existsSync6(memoryPath)) {
1760
- const st = statSync4(memoryPath);
1807
+ const st = statSync5(memoryPath);
1761
1808
  if (st.size > MAX_MEMORY_FILE_BYTES) {
1762
1809
  return false;
1763
1810
  }
@@ -2166,6 +2213,47 @@ program.command("bench").description("Run token reduction benchmark").option("-p
2166
2213
  }
2167
2214
  console.log();
2168
2215
  });
2216
+ program.command("cost").description("Show token-savings telemetry from engram hook logs").option(
2217
+ "-p, --project <path...>",
2218
+ "One or more project roots. Defaults to current dir if omitted."
2219
+ ).option("--digest", "Write weekly Markdown digest to ~/.engram/").option("--json", "Emit machine-readable JSON instead of a terminal table").action(
2220
+ async (opts) => {
2221
+ const cost = await import("./cost-CSILPTZT.js");
2222
+ const roots = opts.project && opts.project.length > 0 ? opts.project.map((p) => pathResolve2(p)) : [pathResolve2(".")];
2223
+ if (opts.digest) {
2224
+ const result = cost.writeWeeklyDigest(roots);
2225
+ console.log(
2226
+ chalk2.green(
2227
+ `wrote ${result.isoWeek} digest \u2192 ${result.path} (${result.rows.length} project${result.rows.length === 1 ? "" : "s"})`
2228
+ )
2229
+ );
2230
+ return;
2231
+ }
2232
+ const rows = cost.summarizeProjects(roots);
2233
+ if (opts.json) {
2234
+ console.log(JSON.stringify(rows, null, 2));
2235
+ return;
2236
+ }
2237
+ console.log(chalk2.bold("\nengram cost lens\n"));
2238
+ console.log(cost.formatTable(rows));
2239
+ const totalSaved = rows.reduce(
2240
+ (a, r) => a + r.summary.tokensSaved,
2241
+ 0
2242
+ );
2243
+ const totalEvents = rows.reduce((a, r) => a + r.summary.events, 0);
2244
+ const totalUsd = rows.reduce(
2245
+ (a, r) => a + r.summary.approxUsdSaved,
2246
+ 0
2247
+ );
2248
+ console.log(
2249
+ chalk2.dim(
2250
+ `
2251
+ total: ${cost.formatNumber(totalSaved)} tokens saved \xB7 ${cost.formatUsd(totalUsd)} \xB7 ${totalEvents} events
2252
+ `
2253
+ )
2254
+ );
2255
+ }
2256
+ );
2169
2257
  var hooks = program.command("hooks").description("Manage git hooks");
2170
2258
  hooks.command("install").description("Install post-commit and post-checkout hooks").argument("[path]", "Project directory", ".").action((p) => console.log(install(p)));
2171
2259
  hooks.command("uninstall").description("Remove engram git hooks").argument("[path]", "Project directory", ".").action((p) => console.log(uninstall(p)));
@@ -2761,7 +2849,7 @@ program.command("stress-test").description("Run stress tests: memory, concurrenc
2761
2849
  }
2762
2850
  });
2763
2851
  program.command("server").description("Start engram HTTP REST server (binds to 127.0.0.1 only)").option("--http", "Enable HTTP server (default)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
2764
- const { startHttpServer } = await import("./server-2ZQKXJ5M.js");
2852
+ const { startHttpServer } = await import("./server-LEYILLJ2.js");
2765
2853
  await startHttpServer(pathResolve2(opts.project), parseInt(opts.port, 10));
2766
2854
  });
2767
2855
  program.command("ui").description("Open the web dashboard (auto-starts HTTP server if needed)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").option("--no-open", "Don't launch browser, just print the URL").action(async (opts) => {
@@ -2989,7 +3077,7 @@ pluginCmd.command("list").description("List installed provider plugins").action(
2989
3077
  }
2990
3078
  });
2991
3079
  pluginCmd.command("install").description("Install a plugin by copying its .mjs file into ~/.engram/plugins/").argument("<file>", "Path to plugin .mjs file").action(async (file) => {
2992
- const { copyFileSync: copyFileSync2, statSync: statSync5 } = await import("fs");
3080
+ const { copyFileSync: copyFileSync2, statSync: statSync6 } = await import("fs");
2993
3081
  const { basename: basename6 } = await import("path");
2994
3082
  const { getPluginsDir, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-SQQB6V74.js");
2995
3083
  const { pathToFileURL } = await import("url");
@@ -2998,7 +3086,7 @@ pluginCmd.command("install").description("Install a plugin by copying its .mjs f
2998
3086
  console.error(chalk2.red(`File not found: ${absPath}`));
2999
3087
  process.exit(1);
3000
3088
  }
3001
- if (!statSync5(absPath).isFile()) {
3089
+ if (!statSync6(absPath).isFile()) {
3002
3090
  console.error(chalk2.red(`Not a file: ${absPath}`));
3003
3091
  process.exit(1);
3004
3092
  }
@@ -0,0 +1,227 @@
1
+ // src/cost/types.ts
2
+ var DEFAULT_COST_CONFIG = {
3
+ inputUsdPerMillion: 3,
4
+ currency: "USD"
5
+ };
6
+
7
+ // src/cost/aggregator.ts
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ var LOG_FILES = ["hook-log.jsonl", "hook-log.jsonl.1"];
11
+ function readEvents(projectRoot) {
12
+ const out = [];
13
+ for (const name of LOG_FILES) {
14
+ const p = join(projectRoot, ".engram", name);
15
+ if (!existsSync(p)) continue;
16
+ let raw = "";
17
+ try {
18
+ raw = readFileSync(p, "utf8");
19
+ } catch {
20
+ continue;
21
+ }
22
+ for (const line of raw.split("\n")) {
23
+ if (!line.trim()) continue;
24
+ try {
25
+ const parsed = JSON.parse(line);
26
+ out.push(toCostEvent(parsed));
27
+ } catch {
28
+ }
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ function toCostEvent(raw) {
34
+ const tokensSaved = numOrUndef(raw.tokensSaved);
35
+ const injected = numOrUndef(raw.injected);
36
+ const wouldHaveRead = numOrUndef(
37
+ raw.wouldHaveRead
38
+ );
39
+ return {
40
+ ts: typeof raw.ts === "string" ? raw.ts : (/* @__PURE__ */ new Date(0)).toISOString(),
41
+ event: typeof raw.event === "string" ? raw.event : "unknown",
42
+ tool: strOrUndef(raw.tool),
43
+ path: strOrUndef(raw.path),
44
+ wouldHaveRead,
45
+ injected,
46
+ tokensSaved
47
+ };
48
+ }
49
+ function numOrUndef(v) {
50
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
51
+ }
52
+ function strOrUndef(v) {
53
+ return typeof v === "string" ? v : void 0;
54
+ }
55
+ function summarize(events, config = DEFAULT_COST_CONFIG) {
56
+ let saved = 0;
57
+ let injected = 0;
58
+ let wouldHave = 0;
59
+ let firstTs = "";
60
+ let lastTs = "";
61
+ for (const e of events) {
62
+ if (e.tokensSaved) saved += e.tokensSaved;
63
+ if (e.injected) injected += e.injected;
64
+ if (e.wouldHaveRead) wouldHave += e.wouldHaveRead;
65
+ if (!firstTs || e.ts < firstTs) firstTs = e.ts;
66
+ if (!lastTs || e.ts > lastTs) lastTs = e.ts;
67
+ }
68
+ const denom = wouldHave > 0 ? wouldHave : saved + injected;
69
+ const reductionRatio = denom > 0 ? saved / denom : 0;
70
+ const approxUsdSaved = saved / 1e6 * config.inputUsdPerMillion;
71
+ return {
72
+ fromTs: firstTs,
73
+ toTs: lastTs,
74
+ events: events.length,
75
+ tokensSaved: saved,
76
+ tokensInjected: injected,
77
+ tokensWouldHave: wouldHave,
78
+ reductionRatio,
79
+ approxUsdSaved
80
+ };
81
+ }
82
+ function summarizeProjects(projectRoots, config = DEFAULT_COST_CONFIG) {
83
+ return projectRoots.map((projectRoot) => ({
84
+ projectRoot,
85
+ summary: summarize(readEvents(projectRoot), config)
86
+ }));
87
+ }
88
+
89
+ // src/cost/formatter.ts
90
+ function formatNumber(n) {
91
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
92
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
93
+ return String(Math.round(n));
94
+ }
95
+ function formatUsd(n) {
96
+ if (n >= 1) return `$${n.toFixed(2)}`;
97
+ if (n >= 0.01) return `$${n.toFixed(3)}`;
98
+ return `$${n.toFixed(4)}`;
99
+ }
100
+ function formatPct(ratio) {
101
+ return `${(ratio * 100).toFixed(1)}%`;
102
+ }
103
+ function formatOneLine(s) {
104
+ return [
105
+ `${formatNumber(s.tokensSaved)} tokens saved`,
106
+ `${formatPct(s.reductionRatio)} reduction`,
107
+ `~${formatUsd(s.approxUsdSaved)}`,
108
+ `${s.events} events`
109
+ ].join(" \xB7 ");
110
+ }
111
+ function formatTable(rows) {
112
+ if (rows.length === 0) return "(no projects with hook-log.jsonl)";
113
+ const lines = [];
114
+ lines.push("Project Tokens saved Reduction Approx USD Events");
115
+ lines.push("\u2500".repeat(86));
116
+ for (const r of rows) {
117
+ const name = truncate(basenameOf(r.projectRoot), 32).padEnd(34);
118
+ const saved = formatNumber(r.summary.tokensSaved).padStart(13);
119
+ const pct = formatPct(r.summary.reductionRatio).padStart(11);
120
+ const usd = formatUsd(r.summary.approxUsdSaved).padStart(12);
121
+ const ev = String(r.summary.events).padStart(7);
122
+ lines.push(`${name}${saved} ${pct} ${usd} ${ev}`);
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+ function formatMarkdownDigest(rows, totals, isoWeek) {
127
+ const lines = [];
128
+ lines.push(`# Engram Cost Digest \u2014 ${isoWeek}`);
129
+ lines.push("");
130
+ lines.push(`**Total tokens saved:** ${formatNumber(totals.tokensSaved)} (${formatPct(totals.reductionRatio)} reduction, ~${formatUsd(totals.approxUsdSaved)})`);
131
+ lines.push("");
132
+ lines.push("## Per-project");
133
+ lines.push("");
134
+ lines.push("| Project | Tokens saved | Reduction | Approx USD | Events |");
135
+ lines.push("|---|---:|---:|---:|---:|");
136
+ for (const r of rows) {
137
+ lines.push([
138
+ "",
139
+ basenameOf(r.projectRoot),
140
+ formatNumber(r.summary.tokensSaved),
141
+ formatPct(r.summary.reductionRatio),
142
+ formatUsd(r.summary.approxUsdSaved),
143
+ String(r.summary.events),
144
+ ""
145
+ ].join("|"));
146
+ }
147
+ lines.push("");
148
+ lines.push("_Generated by `engram cost --digest` (v3.3 Cost Lens)_");
149
+ return lines.join("\n");
150
+ }
151
+ function basenameOf(p) {
152
+ const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
153
+ return idx >= 0 ? p.slice(idx + 1) : p;
154
+ }
155
+ function truncate(s, n) {
156
+ return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
157
+ }
158
+
159
+ // src/cost/digest.ts
160
+ import { mkdirSync, writeFileSync } from "fs";
161
+ import { homedir } from "os";
162
+ import { join as join2 } from "path";
163
+ function isoWeekLabel(d = /* @__PURE__ */ new Date()) {
164
+ const target = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
165
+ const dayNum = (target.getUTCDay() + 6) % 7;
166
+ target.setUTCDate(target.getUTCDate() - dayNum + 3);
167
+ const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
168
+ const weekNum = 1 + Math.round(
169
+ ((target.getTime() - firstThursday.getTime()) / 864e5 - 3 + (firstThursday.getUTCDay() + 6) % 7) / 7
170
+ );
171
+ return `${target.getUTCFullYear()}-W${String(weekNum).padStart(2, "0")}`;
172
+ }
173
+ function writeWeeklyDigest(projectRoots, config = DEFAULT_COST_CONFIG, outDir = join2(homedir(), ".engram"), now = /* @__PURE__ */ new Date()) {
174
+ mkdirSync(outDir, { recursive: true });
175
+ const rows = summarizeProjects(projectRoots, config);
176
+ const totals = sumRows(rows, config);
177
+ const isoWeek = isoWeekLabel(now);
178
+ const md = formatMarkdownDigest(rows, totals, isoWeek);
179
+ const path = join2(outDir, `cost-report-${isoWeek}.md`);
180
+ writeFileSync(path, md, "utf8");
181
+ return { path, isoWeek, rows };
182
+ }
183
+ function sumRows(rows, config) {
184
+ let saved = 0;
185
+ let injected = 0;
186
+ let wouldHave = 0;
187
+ let events = 0;
188
+ let firstTs = "";
189
+ let lastTs = "";
190
+ for (const r of rows) {
191
+ saved += r.summary.tokensSaved;
192
+ injected += r.summary.tokensInjected;
193
+ wouldHave += r.summary.tokensWouldHave;
194
+ events += r.summary.events;
195
+ if (r.summary.fromTs && (!firstTs || r.summary.fromTs < firstTs)) {
196
+ firstTs = r.summary.fromTs;
197
+ }
198
+ if (r.summary.toTs && (!lastTs || r.summary.toTs > lastTs)) {
199
+ lastTs = r.summary.toTs;
200
+ }
201
+ }
202
+ const denom = wouldHave > 0 ? wouldHave : saved + injected;
203
+ return {
204
+ fromTs: firstTs,
205
+ toTs: lastTs,
206
+ events,
207
+ tokensSaved: saved,
208
+ tokensInjected: injected,
209
+ tokensWouldHave: wouldHave,
210
+ reductionRatio: denom > 0 ? saved / denom : 0,
211
+ approxUsdSaved: saved / 1e6 * config.inputUsdPerMillion
212
+ };
213
+ }
214
+ export {
215
+ DEFAULT_COST_CONFIG,
216
+ formatMarkdownDigest,
217
+ formatNumber,
218
+ formatOneLine,
219
+ formatPct,
220
+ formatTable,
221
+ formatUsd,
222
+ isoWeekLabel,
223
+ readEvents,
224
+ summarize,
225
+ summarizeProjects,
226
+ writeWeeklyDigest
227
+ };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "3.0.1",
3
+ "version": "3.3.0",
4
+ "mcpName": "io.github.NickCirv/engram",
4
5
  "description": "The context spine for AI coding agents. 9 built-in providers + mcpConfig plugin contract (wrap any MCP server in 10 lines), generic MCP-client aggregator (stdio), pre-mortem mistake-guard, bi-temporal mistake memory, Anthropic Auto-Memory bridge, SSE streaming context packets, dual-emit AGENTS.md+CLAUDE.md. 90.8% measured real-world token savings (reproducible bench included). Local SQLite, zero cloud.",
5
6
  "repository": {
6
7
  "type": "git",
@@ -1,7 +1,3 @@
1
- import {
2
- ContextCache,
3
- getContextCache
4
- } from "./chunk-CIQQ5Y3S.js";
5
1
  import {
6
2
  getOrCreateToken,
7
3
  isHostValid,
@@ -9,6 +5,10 @@ import {
9
5
  parseCookies,
10
6
  safeEqual
11
7
  } from "./chunk-N6PPKOPK.js";
8
+ import {
9
+ ContextCache,
10
+ getContextCache
11
+ } from "./chunk-CIQQ5Y3S.js";
12
12
  import {
13
13
  summarizeHookLog
14
14
  } from "./chunk-FKY6HIT2.js";