engramx 3.0.2 → 3.4.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,47 @@ All notable changes to engram are documented here. Format based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.4.0] — 2026-05-02 — "Universal Spine"
10
+
11
+ The release that turns engram from a Claude Code tool into a universal context spine across every major AI coding tool. Same engram, same graph, same 89.1% reduction — now plugged into 8 IDEs out of the box.
12
+
13
+ ### Added
14
+
15
+ - **Universal init detector.** `src/setup/detect.ts` adds three new detectors: `detectCline` (probes for VS Code's Cline globalStorage), `detectZed` (probes for Zed's config and `.zed/settings.json`), `detectCodex` (probes for `~/.codex` and `AGENTS.md`). `detectAllIdes()` now returns 8 entries (was 5). Per-IDE setup hints in `wizard.ts` now include the exact MCP config snippet for Cline.
16
+ - **Anthropic Claude Code plugin manifest.** `plugins/anthropic-marketplace/` — submission-ready `marketplace.json`, `plugin.json`, three skills (`/engram:cost`, `/engram:query`, `/engram:mistakes`), and the MCP server config that registers `engram-serve` automatically. Verified against `code.claude.com/docs/en/plugins`.
17
+ - **VS Code / Cursor extension.** `extensions/vscode/` — thin wrapper around the engramx CLI. Six commands (init, gen-mdc, gen, cost, dashboard, doctor), status-bar entry, two configuration settings. Compiles to a single `out/extension.js` and packs via `vsce` for OpenVSX. Works in VS Code, Cursor, and any VS Code fork.
18
+ - **Cline integration documented.** `docs/integrations/cline.md` — Cline supports MCP natively, so the integration is one config-snippet away. Cross-linked from `docs/integrations/README.md`.
19
+ - **engramx-continue.** Sister npm package at `adapters/continue/` — Continue.dev `@engram` context provider with HTTP and CLI fallback transports. Tarball verified clean (2.2 KB, 4 files).
20
+
21
+ ### Changed
22
+
23
+ - **README.md.** Top-of-file callout now leads with the May 2026 market context (Cursor pricing crisis + Claude Code rate-limit pain) so the positioning matches what users are actually searching for. IDE matrix expanded from 8 to 11. "How It Compares" rewritten as the May 2026 8-row competitive matrix (engramx vs Cursor index / Aider repo map / Cline / Continue / Mem0 / claude-mem / CartoGopher); legacy table moved to a collapsed details block.
24
+ - **docs/install.html.** Title, meta description, OG title + description, hero pill, nav link, and IDE matrix all refreshed for v3.4 framing.
25
+ - **GitHub repo description and topics.** Description shortened and re-led with "context spine that 10x's every AI coding session." Topics maxed at 20 with `cline` and `universal-spine` added.
26
+
27
+ ### Tests
28
+ - Net-new: 3 detector tests in `tests/setup/detect.test.ts` (detectCline / detectZed / detectCodex).
29
+ - Total: **910 passing** (was 907 baseline).
30
+
31
+ ### Process
32
+
33
+ This is the first release that walks the new 8-phase release ritual codified at `~/.claude/skills/engram-release/SKILL.md`. Phase 3 (public surface refresh) caught README + install.html + topics drift in one pass. Future releases follow the same checklist by default.
34
+
35
+ ### Why
36
+
37
+ Three things broke at the same time in 2026. Cursor went usage-based and people started getting $1,400 surprise bills. Anthropic tightened Claude Code limits, then quietly tested removing the product from the $20 Pro plan. AI coding fragmented into 8 IDEs with no common context layer. v3.4 puts engram into all of them — one install, one graph, every tool benefits. Audit at `~/Desktop/Projects/Engram/00-strategy/2026-05-02-strategic-audit-v34-pivot.md`.
38
+
39
+ ## [3.3.0] — 2026-05-02 — "Cost Lens"
40
+
41
+ ### Added
42
+ - 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`.
43
+ - New `src/cost/` module: `types.ts`, `aggregator.ts`, `formatter.ts`, `digest.ts`, `instrument.ts`. Pure functions, hermetic tests, NaN-safe math.
44
+ - Dispatch instrumentation in `src/intercept/dispatch.ts` — every PreToolUse log entry now carries `wouldHaveRead`, `injected`, and `tokensSaved` fields when applicable.
45
+ - 31 new tests across `tests/cost.test.ts` and `tests/cost-instrument.test.ts`, hermetic.
46
+
47
+ ### Why
48
+ Cost Lens is the baseline for the v3.3 → v4.0 roadmap. Future features (Bridge, Mesh, Vector) get evaluated against the real-world impact, not a single static benchmark. PRD: `01-prds/03-engram-mesh-ruflo-integration-PRD.md`.
49
+
9
50
  ## [3.0.2] — 2026-04-24 — "MCP Registry"
10
51
 
11
52
  Chore release. No runtime changes. Adds the `mcpName` field to `package.json`
package/README.md CHANGED
@@ -47,16 +47,28 @@
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-876%20passing-brightgreen" alt="Tests">
50
+ <img src="https://img.shields.io/badge/tests-910%20passing-brightgreen" alt="Tests">
51
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">
52
+ <img src="https://img.shields.io/badge/token%20savings-89.1%25%20measured-orange" alt="89.1% 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
- > **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.
59
+ ## Why this exists, May 2026
60
+
61
+ Three things broke at the same time. Cursor went usage-based and people started getting $1,400 surprise bills. Anthropic tightened Claude Code limits, then quietly tested removing it from the $20 Pro plan. Half the AI coding crowd migrated from one tool to the other, hit the new ceiling within a week, and started looking for any way to make a session last longer.
62
+
63
+ Engramx is what makes the session last longer. It indexes your codebase into a local SQLite knowledge graph once. Then it intercepts file reads at the agent boundary and replaces them with a structural summary the agent already has the working memory for. Same edit, same diff, same code shipped — fewer tokens consumed in the round trip.
64
+
65
+ On a real 87-file repo, the measured reduction is **89.1%**. That's not a marketing number. The benchmark is committed to this repo as `bench/real-world.ts` and runs against any project you point it at. Independent migration guides ([dev.to/56kode](https://dev.to/56_kode/why-were-moving-from-cursor-to-claude-code-and-why-you-should-too-9kh), [SpectrumAI Lab](https://spectrumailab.com/blog/claude-code-vs-cursor)) cite engram as the strongest measured number in the category.
66
+
67
+ Works in 8 IDEs and counting — Claude Code, Cursor, Cline, Continue.dev, Aider, Windsurf, Zed, OpenAI Codex CLI. One install, one graph, every tool benefits. Apache 2.0. Local SQLite. Nothing leaves your machine.
68
+
69
+ > **v3.4 "Universal Spine" shipped 2026-05-02** — multi-IDE detector covers 8 tools, Anthropic Claude Code plugin (`/plugin install engram`), VS Code / Cursor extension on OpenVSX, `engramx-continue` on npm, Cline integration documented. Cost Lens telemetry from v3.3.0 now feeds a weekly Markdown digest at `~/.engram/cost-report-YYYY-Www.md`. 910 tests, CI green on Ubuntu + Windows × Node 20 + 22. See [CHANGELOG.md](CHANGELOG.md) for the v3.3 + v3.4 diff.
70
+
71
+ > **EngramX v3.0 "Spine" shipped 2026-04-24** — the biggest release before v3.4. The spine is **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.
60
72
 
61
73
  ---
62
74
 
@@ -368,14 +380,19 @@ engram hooks install # auto-rebuild graph on every git commit
368
380
 
369
381
  ## IDE Integrations
370
382
 
383
+ `engram setup` auto-detects every supported IDE on your machine and prints the right next-step for each. You don't have to remember which command to run — the detector knows.
384
+
371
385
  | IDE | Integration | Setup |
372
386
  |-----|------------|-------|
373
- | **Claude Code** | Hook-based interception (native, automatic) | `engram install-hook` |
374
- | **Cursor** | MDC snapshot + native MCP | `engram gen-mdc` &middot; [docs/integrations/cursor-mcp.md](docs/integrations/cursor-mcp.md) |
375
- | **Continue.dev** | `@engram` context provider | [docs/integrations/continue.md](docs/integrations/continue.md) |
376
- | **Zed** | Context server (`/engram`) | `engram context-server` |
377
- | **Aider** | Context file generation | `engram gen-aider` |
387
+ | **Claude Code** | Hook-based interception (native, automatic) — **plus** `/plugin install engram` for slash-commands | `engram install-hook` &middot; [docs/integrations/claude-code.md](docs/integrations/claude-code.md) |
388
+ | **Cursor** | MDC snapshot + native MCP + VS Code extension on OpenVSX | `engram gen-mdc` &middot; [docs/integrations/cursor-mcp.md](docs/integrations/cursor-mcp.md) |
389
+ | **Cline** | MCP server (5M+ VS Code installs, no native answer to token burn) | [docs/integrations/cline.md](docs/integrations/cline.md) |
390
+ | **Continue.dev** | `@engram` context provider via [`engramx-continue`](https://www.npmjs.com/package/engramx-continue) | [docs/integrations/continue.md](docs/integrations/continue.md) |
391
+ | **Aider** | Context file generation | `engram gen-aider` &middot; [docs/integrations/aider.md](docs/integrations/aider.md) |
378
392
  | **Windsurf** (Codeium) | `.windsurfrules` snapshot + MCP | `engram gen-windsurfrules` |
393
+ | **Zed** | Context server (`/engram`) | `engram context-server` &middot; [docs/integrations/zed.md](docs/integrations/zed.md) |
394
+ | **OpenAI Codex CLI** | `AGENTS.md` auto-emit (universal Linux Foundation standard) | `engram gen` (default emits both `AGENTS.md` + `CLAUDE.md`) |
395
+ | **VS Code (any agent)** | Status-bar entry + 6 commands wrapping the CLI | `code --install-extension engram-vscode` (OpenVSX) |
379
396
  | **Neovim** | MCP via codecompanion / avante | [docs/integrations/neovim.md](docs/integrations/neovim.md) |
380
397
  | **Emacs** | MCP via gptel-mcp | [docs/integrations/emacs.md](docs/integrations/emacs.md) |
381
398
 
@@ -385,17 +402,40 @@ Per-IDE setup guides are in [`docs/integrations/`](docs/integrations/).
385
402
 
386
403
  ## How It Compares
387
404
 
405
+ The "context spine" slot — local-first, code-aware, works in any MCP runtime, with a reproducible benchmark — is currently unowned. Here's the field as of May 2026:
406
+
407
+ | | **engramx** | Cursor index | Aider repo map | Cline | Continue.dev | Mem0 | claude-mem | CartoGopher |
408
+ |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
409
+ | Works in any MCP runtime | ✅ | IDE-locked | Aider only | VS Code only | VS Code only | ✅ | Claude Code only | ✅ |
410
+ | Local-first (nothing leaves machine) | ✅ | cloud-synced | ✅ | ✅ | ✅ | optional | ✅ | ✅ |
411
+ | Code-aware AST graph | ✅ | proprietary | ✅ | — | — | — | — | ✅ |
412
+ | Reproducible benchmark | ✅ **89.1%** | — | — | — | — | — | — | claims 88% |
413
+ | Bi-temporal mistake memory | ✅ | — | — | — | — | — | partial | — |
414
+ | `AGENTS.md` + `CLAUDE.md` dual-emit | ✅ | — | — | — | — | — | — | — |
415
+ | Single npm install | ✅ | full IDE | pip | VS Code ext | VS Code ext | pip / npm | claude plugin | Go binary |
416
+ | License | Apache 2.0 | proprietary | Apache 2.0 | Apache 2.0 | Apache 2.0 | Apache 2.0 | MIT | unknown |
417
+ | GitHub stars (May 2026) | 108 | proprietary | 39K | 61.2K | 32.4K | 47.8K | new | unknown |
418
+
419
+ The matrix isn't a slight at any of them — most do something engram doesn't. Cursor's index is great inside Cursor. Aider's repo map is great in Aider. Cline's full-file rewrite model is honest about what it is. The point is that nobody else covers all eight rows. Engram is the only tool that does.
420
+
421
+ For the legacy comparison vs `Continue @RepoMap` / `Cursor .cursorrules` / `@199-bio/engram` (small repo-map approaches), see the matrix below.
422
+
423
+ <details>
424
+ <summary><strong>Legacy detailed comparison</strong></summary>
425
+
388
426
  | | engram | Continue @RepoMap | Cursor .cursorrules | Aider repo-map | @199-bio/engram |
389
427
  |---|---|---|---|---|---|
390
428
  | **Interception model** | Hook-based, automatic on every Read | Fetched at @-mention time | Static file, manual | Per-session map | MCP server, called explicitly |
391
429
  | **Cache strategy** | SQLite at SessionStart, <5ms per read | No cache — live fetch | No cache | Per-session only | No cache |
392
430
  | **Persistent memory** | Decisions, mistakes, patterns across sessions | No | Manual text file | No | No |
393
- | **Multiple providers** | 8 (AST, git, mistakes, MemPalace, Context7, Obsidian, LSP) | Repo structure only | No | Repo structure only | Graph query only |
431
+ | **Multiple providers** | 9 (AST, git, mistakes, MemPalace, Context7, Obsidian, LSP, Anthropic Memory, MCP plugins) | Repo structure only | No | Repo structure only | Graph query only |
394
432
  | **Mistake tracking** | LSP diagnostics → mistake nodes, ⚠️ on Edit | No | No | No | No |
395
433
  | **Survives compaction** | Yes (PreCompact hook) | No | Yes (static file) | No | No |
396
434
  | **LLM cost** | $0 | $0 | $0 | $0 | $0 |
397
435
  | **Native deps** | Zero | No | No | No | No |
398
436
 
437
+ </details>
438
+
399
439
  ---
400
440
 
401
441
  ## Install + Configuration
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
  }
@@ -3193,7 +3281,7 @@ program.command("setup").description("Zero-friction first-run wizard (init + ins
3193
3281
  "local"
3194
3282
  ).action(
3195
3283
  async (opts) => {
3196
- const { runSetup } = await import("./wizard-UH27IO4I.js");
3284
+ const { runSetup } = await import("./wizard-WGBAIZLF.js");
3197
3285
  const scope = opts.scope === "local" || opts.scope === "project" || opts.scope === "user" ? opts.scope : "local";
3198
3286
  const result = await runSetup({
3199
3287
  projectPath: opts.project,
@@ -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
+ };
@@ -99,13 +99,66 @@ function detectAider(projectRoot) {
99
99
  status: !installed ? "not detected" : configured ? ".aider-context.md present" : "detected \u2014 run `engram gen-aider`"
100
100
  };
101
101
  }
102
+ function detectCline() {
103
+ const candidates = [
104
+ join(homedir(), "Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev"),
105
+ join(homedir(), ".config/Code/User/globalStorage/saoudrizwan.claude-dev"),
106
+ join(homedir(), "AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev")
107
+ ];
108
+ const installed = candidates.some(existsSync);
109
+ let configured = false;
110
+ if (installed) {
111
+ try {
112
+ configured = candidates.filter(existsSync).some((p) => {
113
+ const settings = join(p, "settings", "cline_mcp_settings.json");
114
+ return existsSync(settings) && readFileSync(settings, "utf-8").includes("engram");
115
+ });
116
+ } catch {
117
+ configured = false;
118
+ }
119
+ }
120
+ return {
121
+ name: "Cline",
122
+ installed,
123
+ configured,
124
+ status: !installed ? "not detected" : configured ? "engram MCP server registered" : "detected \u2014 add engram-serve to cline_mcp_settings.json"
125
+ };
126
+ }
127
+ function detectZed(projectRoot) {
128
+ const candidates = [
129
+ join(homedir(), ".config/zed"),
130
+ join(homedir(), "Library/Application Support/Zed"),
131
+ join(homedir(), "AppData/Roaming/Zed")
132
+ ];
133
+ const installed = candidates.some(existsSync);
134
+ const configured = existsSync(join(projectRoot, ".zed", "settings.json"));
135
+ return {
136
+ name: "Zed",
137
+ installed,
138
+ configured,
139
+ status: !installed ? "not detected" : configured ? "Zed project settings present" : "detected \u2014 add engram context server"
140
+ };
141
+ }
142
+ function detectCodex(projectRoot) {
143
+ const installed = existsSync(join(homedir(), ".codex"));
144
+ const configured = existsSync(join(projectRoot, "AGENTS.md"));
145
+ return {
146
+ name: "Codex CLI",
147
+ installed,
148
+ configured,
149
+ status: !installed ? "not detected" : configured ? "AGENTS.md present" : "detected \u2014 run `engram gen` to create AGENTS.md"
150
+ };
151
+ }
102
152
  function detectAllIdes(projectRoot) {
103
153
  return [
104
154
  detectClaudeCode(projectRoot),
105
155
  detectCursor(projectRoot),
106
- detectWindsurf(projectRoot),
156
+ detectCline(),
107
157
  detectContinue(),
108
- detectAider(projectRoot)
158
+ detectWindsurf(projectRoot),
159
+ detectAider(projectRoot),
160
+ detectZed(projectRoot),
161
+ detectCodex(projectRoot)
109
162
  ];
110
163
  }
111
164
 
@@ -214,7 +267,11 @@ async function offerIdeAdapters(opts, rl) {
214
267
  const suggest = {
215
268
  Cursor: "engram gen-mdc",
216
269
  Windsurf: "engram gen-windsurfrules",
217
- Aider: "engram gen-aider"
270
+ Aider: "engram gen-aider",
271
+ "Codex CLI": "engram gen --target agents",
272
+ Cline: "Add to cline_mcp_settings.json: { engram: { command: 'engram-serve', args: ['" + root + "'] } }",
273
+ "Continue.dev": "Add to ~/.continue/config.json: { contextProviders: [{ name: 'engramx-continue' }] }",
274
+ Zed: "Register engram-serve as a Zed context server (see Docs/integrations/zed.md)"
218
275
  };
219
276
  const suggested = [];
220
277
  for (const ide of unconfigured) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "3.0.2",
3
+ "version": "3.4.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";