engramx 3.0.2 → 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 +9 -0
- package/README.md +149 -86
- package/dist/cli.js +96 -8
- package/dist/cost-CSILPTZT.js +227 -0
- package/package.json +1 -1
- package/dist/{server-2ZQKXJ5M.js → server-LEYILLJ2.js} +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ 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
|
+
|
|
9
18
|
## [3.0.2] — 2026-04-24 — "MCP Registry"
|
|
10
19
|
|
|
11
20
|
Chore release. No runtime changes. Adds the `mcpName` field to `package.json`
|
package/README.md
CHANGED
|
@@ -1,119 +1,181 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/banner-v3.png" alt="EngramX — the
|
|
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
|
-
<
|
|
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-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
### What changes when your agent has memory
|
|
64
47
|
|
|
65
|
-
|
|
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
|
-
|
|
60
|
+
## Install
|
|
68
61
|
|
|
69
|
-
|
|
62
|
+
### macOS / Linux (recommended)
|
|
70
63
|
|
|
71
|
-
|
|
64
|
+
```bash
|
|
65
|
+
brew install engramx
|
|
66
|
+
```
|
|
72
67
|
|
|
73
|
-
###
|
|
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
|
-
|
|
74
|
+
### Zero-dep one-liner
|
|
82
75
|
|
|
83
|
-
|
|
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
|
-
##
|
|
86
|
+
## Per-agent setup
|
|
88
87
|
|
|
89
|
-
|
|
88
|
+
One command for your stack:
|
|
90
89
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
---
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
## See what your agent has remembered
|
|
103
109
|
|
|
104
110
|
```bash
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
193
|
+
engram init
|
|
132
194
|
npx tsx /path/to/engram/bench/real-world.ts --project . --files 50
|
|
133
195
|
```
|
|
134
196
|
|
|
135
|
-
|
|
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
|
-
##
|
|
214
|
+
## Clean uninstall
|
|
140
215
|
|
|
141
|
-
|
|
216
|
+
One command:
|
|
142
217
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
218
|
+
```bash
|
|
219
|
+
npm uninstall -g engramx # 3.0.1+ auto-runs preuninstall hook-cleanup
|
|
220
|
+
```
|
|
146
221
|
|
|
147
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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-
|
|
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:
|
|
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 (!
|
|
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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"mcpName": "io.github.NickCirv/engram",
|
|
5
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.",
|
|
6
6
|
"repository": {
|
|
@@ -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";
|