engramx 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +67 -7
  3. package/dist/{aider-context-TNGSXMVY.js → aider-context-BC5R2ZTA.js} +1 -1
  4. package/dist/cache-AK6CF3BC.js +10 -0
  5. package/dist/chunk-22INHMKB.js +31 -0
  6. package/dist/chunk-533LR4I7.js +220 -0
  7. package/dist/{chunk-QOG4K427.js → chunk-C6GBUOAL.js} +1 -1
  8. package/dist/chunk-CIQQ5Y3S.js +338 -0
  9. package/dist/chunk-KL6NSPVA.js +59 -0
  10. package/dist/{chunk-SBHGK5WA.js → chunk-PEH54LYC.js} +85 -3
  11. package/dist/{chunk-6SFMVYUN.js → chunk-SJT7VS2G.js} +127 -23
  12. package/dist/cli.js +380 -258
  13. package/dist/{core-77MHT3QV.js → core-6IY5L6II.js} +2 -2
  14. package/dist/{cursor-mdc-HWVUZUZH.js → cursor-mdc-GJ7E5LDD.js} +1 -1
  15. package/dist/{exporter-A3VSLS4U.js → exporter-GWU2GF23.js} +1 -1
  16. package/dist/grammars/tree-sitter-go.wasm +0 -0
  17. package/dist/grammars/tree-sitter-javascript.wasm +0 -0
  18. package/dist/grammars/tree-sitter-python.wasm +0 -0
  19. package/dist/grammars/tree-sitter-rust.wasm +0 -0
  20. package/dist/grammars/tree-sitter-tsx.wasm +0 -0
  21. package/dist/grammars/tree-sitter-typescript.wasm +0 -0
  22. package/dist/{importer-LU2YFZDY.js → importer-V62NGZRK.js} +1 -1
  23. package/dist/index.js +3 -3
  24. package/dist/{migrate-5ZJWF2HD.js → migrate-UKCO6BUU.js} +3 -1
  25. package/dist/plugin-loader-FCOMVOX7.js +100 -0
  26. package/dist/serve.js +2 -2
  27. package/dist/server-VBRTTECZ.js +1363 -0
  28. package/dist/{tuner-2LVIEE5V.js → tuner-KFNNGKG3.js} +4 -2
  29. package/dist/windsurf-rules-C7SVDHBL.js +59 -0
  30. package/package.json +4 -3
  31. package/dist/chunk-CEAANHHX.js +0 -88
  32. package/dist/server-I3C74ZLB.js +0 -193
package/CHANGELOG.md CHANGED
@@ -4,6 +4,124 @@ All notable changes to engram are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.0.0] — 2026-04-17 — "Ecosystem"
8
+
9
+ The biggest release since v1.0.0. Completes the v2.0 roadmap Phases 1–4:
10
+ Foundation, Web Dashboard, Integration Expansion, and Solidification.
11
+ 640 tests (up from 579). All changes backward-compatible with v1.x graph
12
+ files — existing `.engram/graph.db` files auto-migrate to schema v7.
13
+
14
+ ### Added — Phase 3 (Integration Expansion)
15
+
16
+ - **`engram gen-windsurfrules`** — generate `.windsurfrules` for Windsurf
17
+ (Codeium) IDE. Plain markdown (no frontmatter), auto-picked-up by Windsurf
18
+ on every chat session. Supports `--watch` for live regeneration on graph
19
+ changes. See `docs/integrations/README.md`.
20
+ - **Integration docs**: `docs/integrations/neovim.md` (codecompanion +
21
+ avante.nvim via mcphub), `docs/integrations/cursor-mcp.md` (Cursor's
22
+ native MCP support alongside the MDC path), `docs/integrations/emacs.md`
23
+ (gptel + gptel-mcp), plus a new `docs/integrations/README.md` index that
24
+ maps every supported IDE to its mechanism.
25
+
26
+ ### Added — Phase 4 (Solidification)
27
+
28
+ - **Provider plugin system** — third-party context providers installable
29
+ at `~/.engram/plugins/*.mjs`. Each plugin default-exports a
30
+ `ContextProviderPlugin` object and is dynamically loaded by the resolver.
31
+ Validation-before-install refuses malformed plugins; duplicate names
32
+ can't shadow built-ins. New CLI:
33
+ - `engram plugin list` — show installed plugins with tier/budget/version
34
+ - `engram plugin install <file.mjs>` — validate + copy into plugins dir
35
+ - `engram plugin remove <filename>`
36
+ - **`engram cache` CLI namespace** — inspect and manage the context cache:
37
+ - `engram cache stats` — hit rate, entries per layer, hot file count
38
+ - `engram cache clear` — flush all layers
39
+ - `engram cache warm` — pre-warm hot files from access frequency
40
+ - **Schema rollback** — `engram db rollback --to <version>` reverts the
41
+ schema with automatic backup at `<dbPath>.bak-v<fromVersion>`. Requires
42
+ explicit `--yes` confirmation (data loss). Rollback is in-session only;
43
+ reopen auto-migrates forward again by design — the backup is the
44
+ recovery path for pinning an older version.
45
+ - **Schema v7** — adds `query_cache` and `pattern_cache` tables with
46
+ `idx_query_cache_file` index. Retroactive DOWN migrations defined for
47
+ versions 1–7 so any version is rollback-target-safe.
48
+
49
+ ### Added — Phase 1 (Foundation)
50
+
51
+ - **Tree-sitter grammar bundling** — 6 WASM grammar files (TypeScript, TSX,
52
+ JavaScript, Python, Go, Rust) now ship inside the npm package at
53
+ `dist/grammars/`. The `engram:ast` provider works out of the box for npm
54
+ users without needing local `node_modules` tree-sitter packages.
55
+ New: `scripts/bundle-grammars.mjs` (pure Node ESM, no tsx dep).
56
+ `npm run build` bundles them automatically; `build:nogrammars` as escape hatch.
57
+ - **Incremental indexing** — `init()` accepts `{ incremental: true }` and the
58
+ CLI accepts `--incremental` to skip files whose mtime hasn't changed since
59
+ last index. File mtimes persisted as JSON in the stats table. On engram's
60
+ own source (117 TS files): 53ms vs 244ms full init — **78% faster**.
61
+ - **`.engramignore` support** — gitignore-like syntax for excluding directories
62
+ and files from indexing. Loaded from project root.
63
+ - **Memory cache system** (`src/intelligence/cache.ts`, ~330 LOC) — 3-layer
64
+ compound savings engine:
65
+ - **Query result cache** — resolved context packets per file, SQLite-backed
66
+ + in-memory LRU (100 entries). Invalidated on file mtime change.
67
+ Benchmarked at 23μs/op, 99% hit rate under 10k random reads.
68
+ - **Pattern cache** — structural query answers memoized with graph version
69
+ tracking. LRU (50 entries). Auto-invalidates on graph mutation.
70
+ - **Hot file cache** — `warmHotFiles()` pre-loads top-N most-accessed files
71
+ at SessionStart for zero first-hit latency.
72
+ - `engram cache` CLI namespace (stats/clear/warm) — planned for Phase 4.
73
+ - **9 new HTTP API endpoints** serving the dashboard + external integrations:
74
+ - `GET /api/hook-log` — paginated hook log entries
75
+ - `GET /api/hook-log/summary` — aggregated event/tool/decision stats
76
+ - `GET /api/tokens` — cumulative token savings
77
+ - `GET /api/files/heatmap` — file interception frequency ranking
78
+ - `GET /api/providers/health` — component status
79
+ - `GET /api/cache/stats` — cache hit/miss rates, entry counts
80
+ - `GET /api/graph/nodes` — paginated graph nodes
81
+ - `GET /api/graph/god-nodes` — top-connected entities
82
+ - `GET /api/sse` — Server-Sent Events, 1s hook-log polling, auto-cleans on
83
+ disconnect
84
+ - Load tested at 200 concurrent mixed requests in 295ms (~1.5ms/req).
85
+
86
+ ### Added — Phase 2 (Web Dashboard)
87
+
88
+ - **Zero-dependency web dashboard at `GET /ui`** — 35KB self-contained
89
+ HTML/CSS/JS as TypeScript template literals. No external CDNs, no build
90
+ pipeline, works offline / on air-gapped machines.
91
+ - Security: CSP meta tag (`default-src 'self'; connect-src 'self'`) + single
92
+ `esc()` helper at every JS→HTML boundary defends against XSS from
93
+ attacker-controllable file paths/labels mined from repos.
94
+ - 6 tabs: Overview, Sessions, Activity (live SSE), Files, Graph (Canvas 2D
95
+ force-directed, ~200 LOC, handles <500 nodes at 60fps with pan/zoom/click),
96
+ Providers.
97
+ - SVG chart library (`ui-components.ts`): donut, stacked bars, sparkline,
98
+ cache/graph stat blocks. Zero dependencies.
99
+ - **`engram ui` CLI command** — auto-starts the HTTP server if not running
100
+ (PID file check), opens the default browser to `http://127.0.0.1:7337/ui`.
101
+ `--no-open` flag for print-only mode.
102
+
103
+ ### Changed
104
+
105
+ - `npm run build` now always bundles tree-sitter grammars into `dist/grammars/`
106
+ (previously only `prepublishOnly` did, which masked the missing-grammar
107
+ scenario for anyone testing a built `dist/` locally).
108
+ - `--incremental` flag wired into the CLI (previously API-only and undiscoverable).
109
+ - Default skip directories expanded: added `.next`, `.nuxt`, `coverage`,
110
+ `target`, `venv`, `.venv`, `.cache`, `.turbo`, `.output`, `.git`.
111
+ - `extractFile()` now returns `lineCount` from content already parsed,
112
+ eliminating a redundant `readFileSync` per file during extraction.
113
+ - `GraphStore` extended with `runSql()`, `prepare()` (public), and
114
+ `removeNodesForFile()` for incremental mode and cache module.
115
+ - Test count: 579 → 640 (+61 tests: 7 incremental + 17 cache + 15 API + 6
116
+ windsurf + 5 rollback + 11 plugin-loader).
117
+
118
+ ### Fixed
119
+
120
+ - `dist/grammars/` was empty after `npm run build` alone — only populated by
121
+ `prepublishOnly`. Masked in dev by `grammar-loader.ts` falling back to
122
+ `node_modules/tree-sitter-*/`, but shipped broken to anyone running `dist/`
123
+ locally after build. Root cause: tsup's `clean: true` wiped the dir.
124
+
7
125
  ## [1.0.0] — 2026-04-17 — "Protocol"
8
126
 
9
127
  ### Added
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  <p align="center">
6
6
  <a href="#install"><strong>Install</strong></a> ·
7
7
  <a href="#quickstart"><strong>Quickstart</strong></a> ·
8
+ <a href="#dashboard"><strong>Dashboard</strong></a> ·
8
9
  <a href="#benchmark"><strong>Benchmark</strong></a> ·
9
10
  <a href="#ide-integrations"><strong>IDE Integrations</strong></a> ·
10
11
  <a href="#http-api"><strong>HTTP API</strong></a> ·
@@ -14,17 +15,22 @@
14
15
 
15
16
  <p align="center">
16
17
  <a href="https://github.com/NickCirv/engram/actions"><img src="https://github.com/NickCirv/engram/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
18
+ <a href="https://www.npmjs.com/package/engramx"><img src="https://img.shields.io/npm/v/engramx?color=blue" alt="npm version"></a>
17
19
  <img src="https://img.shields.io/badge/license-Apache%202.0-blue" alt="License">
18
20
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node">
19
- <img src="https://img.shields.io/badge/tests-579%20passing-brightgreen" alt="Tests">
20
- <img src="https://img.shields.io/badge/providers-8-blue" alt="8 Providers">
21
- <img src="https://img.shields.io/badge/token%20savings-88%25%20proven-orange" alt="88% Proven Savings">
21
+ <img src="https://img.shields.io/badge/tests-640%20passing-brightgreen" alt="Tests">
22
+ <img src="https://img.shields.io/badge/providers-8%20%2B%20plugins-blue" alt="8 Providers + plugins">
23
+ <img src="https://img.shields.io/badge/token%20savings-88.1%25%20measured-orange" alt="88% Proven Savings">
22
24
  <img src="https://img.shields.io/badge/native%20deps-zero-green" alt="Zero native deps">
23
25
  <img src="https://img.shields.io/badge/LLM%20cost-$0-green" alt="Zero LLM cost">
24
26
  </p>
25
27
 
26
28
  ---
27
29
 
30
+ > **v2.0 "Ecosystem" shipped 2026-04-17** — web dashboard at `engram ui`, 3-layer memory cache (23μs/op at 99% hit rate), provider plugin system (`~/.engram/plugins/*.mjs`), `engram cache` CLI, schema rollback with automatic backup, incremental re-indexing (78% faster on large repos), auto-bundled tree-sitter grammars, Windsurf + Neovim + Emacs integrations. See [CHANGELOG.md](CHANGELOG.md) for the full diff.
31
+
32
+ ---
33
+
28
34
  # The context spine for AI coding agents.
29
35
 
30
36
  engram intercepts every file read your AI agent makes and replaces it with a pre-assembled context packet — structure, decisions, git history, library docs, and known issues — from 8 providers, delivered in a single ~500-token response. The agent gets what it needs without reading the file. You stop paying for context you've already paid for.
@@ -42,6 +48,55 @@ That's the full setup. The next Claude Code session starts with a project brief
42
48
 
43
49
  ---
44
50
 
51
+ ## Dashboard
52
+
53
+ A zero-dependency web dashboard ships built-in. One command, opens in your browser:
54
+
55
+ ```bash
56
+ engram ui
57
+ ```
58
+
59
+ <p align="center">
60
+ <img src="assets/screenshots/01-overview.png" alt="engram dashboard — Overview tab" width="100%">
61
+ </p>
62
+
63
+ The **Overview** tab: real metrics from your sessions — tokens saved, cost saved at $3/M rate, session-level hit rate, cache performance, graph health.
64
+
65
+ <p align="center">
66
+ <img src="assets/screenshots/03-activity.png" alt="engram dashboard — Activity tab" width="100%">
67
+ </p>
68
+
69
+ **Activity** — live hook events streamed via Server-Sent Events. See every `Read` / `Edit` / `Write` decision (deny = intercepted, passthrough = engram couldn't help). Per-tool breakdown on the right shows where the savings come from.
70
+
71
+ <p align="center">
72
+ <img src="assets/screenshots/04-files.png" alt="engram dashboard — Files heatmap" width="100%">
73
+ </p>
74
+
75
+ **Files** — the heatmap ranks your hot files by interception count. Cursor knows this view.
76
+
77
+ <p align="center">
78
+ <img src="assets/screenshots/05-graph.png" alt="engram dashboard — Knowledge graph visualization" width="100%">
79
+ </p>
80
+
81
+ **Graph** — Canvas 2D force-directed visualization of the knowledge graph. God nodes are larger and labeled. Drag to pan, scroll to zoom, click for details. 300+ nodes at 60fps.
82
+
83
+ <p align="center">
84
+ <img src="assets/screenshots/06-providers.png" alt="engram dashboard — Providers + cache health" width="100%">
85
+ </p>
86
+
87
+ **Providers** — component health (HTTP / LSP / AST / IDE count) and per-layer cache stats (entries + cross-session hit counts).
88
+
89
+ ### Design
90
+
91
+ - **35KB total** — one HTTP response, zero external CDN calls, works offline and on air-gapped machines.
92
+ - **Zero runtime dependencies** — all CSS and JS inlined as TypeScript template literals; SVG charts and Canvas 2D graph hand-rolled (~400 LOC total).
93
+ - **CSP-hardened** — `default-src 'self'; connect-src 'self'` meta tag plus `esc()` at every data-to-HTML boundary. Defends against attacker-controlled file paths and labels mined from untrusted repos.
94
+ - **Live-updating** — SSE stream pushes new hook events to the Activity tab within 1 second.
95
+
96
+ See also the **Sessions** tab (cumulative breakdown + sparkline) in [`assets/screenshots/02-sessions.png`](assets/screenshots/02-sessions.png).
97
+
98
+ ---
99
+
45
100
  ## Benchmark
46
101
 
47
102
  Measured across 10 structured coding tasks against a baseline of reading the relevant files directly. No synthetic data. No cherry-picked queries.
@@ -128,12 +183,14 @@ Requires Node.js 20+. Zero native dependencies. No build tools. Local SQLite via
128
183
  cd ~/my-project
129
184
  engram init # scan codebase → .engram/graph.db (~40ms, 0 tokens)
130
185
  engram install-hook # wire the Sentinel into Claude Code
186
+ engram ui # open the web dashboard in your browser
131
187
  ```
132
188
 
133
189
  Open a Claude Code session. When the agent reads a well-covered file you will see a system-reminder with the structural summary instead of file contents. After the session:
134
190
 
135
191
  ```bash
136
- engram hook-stats # what was intercepted, tokens saved
192
+ engram hook-stats # what was intercepted, tokens saved (CLI)
193
+ engram ui # same data, richer view, real-time updates
137
194
  engram hook-preview src/auth.ts # dry-run: see what the hook would inject for one file
138
195
  ```
139
196
 
@@ -165,10 +222,13 @@ engram hooks install # auto-rebuild graph on every git commit
165
222
  | IDE | Integration | Setup |
166
223
  |-----|------------|-------|
167
224
  | **Claude Code** | Hook-based interception (native, automatic) | `engram install-hook` |
168
- | **Continue.dev** | `@engram` context provider | See [docs/integrations/continue.md](docs/integrations/continue.md) |
169
- | **Cursor** | MDC generation | `engram gen-mdc` |
170
- | **Zed** | Context server | `engram context-server` |
225
+ | **Cursor** | MDC snapshot + native MCP | `engram gen-mdc` &middot; [docs/integrations/cursor-mcp.md](docs/integrations/cursor-mcp.md) |
226
+ | **Continue.dev** | `@engram` context provider | [docs/integrations/continue.md](docs/integrations/continue.md) |
227
+ | **Zed** | Context server (`/engram`) | `engram context-server` |
171
228
  | **Aider** | Context file generation | `engram gen-aider` |
229
+ | **Windsurf** (Codeium) | `.windsurfrules` snapshot + MCP | `engram gen-windsurfrules` |
230
+ | **Neovim** | MCP via codecompanion / avante | [docs/integrations/neovim.md](docs/integrations/neovim.md) |
231
+ | **Emacs** | MCP via gptel-mcp | [docs/integrations/emacs.md](docs/integrations/emacs.md) |
172
232
 
173
233
  Per-IDE setup guides are in [`docs/integrations/`](docs/integrations/).
174
234
 
@@ -7,7 +7,7 @@ function buildSection(heading, lines) {
7
7
  return [`## ${heading}`, "", ...lines, ""].join("\n");
8
8
  }
9
9
  async function generateAiderContext(projectRoot) {
10
- const { getStore } = await import("./core-77MHT3QV.js");
10
+ const { getStore } = await import("./core-6IY5L6II.js");
11
11
  const store = await getStore(projectRoot);
12
12
  try {
13
13
  const allNodes = store.getAllNodes();
@@ -0,0 +1,10 @@
1
+ import {
2
+ ContextCache,
3
+ _resetContextCache,
4
+ getContextCache
5
+ } from "./chunk-CIQQ5Y3S.js";
6
+ export {
7
+ ContextCache,
8
+ _resetContextCache,
9
+ getContextCache
10
+ };
@@ -0,0 +1,31 @@
1
+ // src/tuner/config.ts
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ var DEFAULTS = {
5
+ confidenceThreshold: 0.7,
6
+ totalTokenBudget: 600,
7
+ providers: {}
8
+ };
9
+ function readConfig(projectRoot) {
10
+ const configPath = join(projectRoot, ".engram", "config.json");
11
+ if (!existsSync(configPath)) return DEFAULTS;
12
+ try {
13
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
14
+ return {
15
+ ...DEFAULTS,
16
+ ...raw,
17
+ providers: { ...DEFAULTS.providers, ...raw.providers ?? {} }
18
+ };
19
+ } catch {
20
+ return DEFAULTS;
21
+ }
22
+ }
23
+ function writeConfig(projectRoot, config) {
24
+ const configPath = join(projectRoot, ".engram", "config.json");
25
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
26
+ }
27
+
28
+ export {
29
+ readConfig,
30
+ writeConfig
31
+ };
@@ -0,0 +1,220 @@
1
+ // src/intercept/stats.ts
2
+ var ESTIMATED_TOKENS_PER_READ_DENY = 1200;
3
+ function summarizeHookLog(entries) {
4
+ const byEvent = {};
5
+ const byTool = {};
6
+ const byDecision = {};
7
+ let readDenyCount = 0;
8
+ let firstEntryTs = null;
9
+ let lastEntryTs = null;
10
+ for (const entry of entries) {
11
+ const event = entry.event ?? "unknown";
12
+ byEvent[event] = (byEvent[event] ?? 0) + 1;
13
+ const tool = entry.tool ?? "unknown";
14
+ byTool[tool] = (byTool[tool] ?? 0) + 1;
15
+ if (entry.decision) {
16
+ byDecision[entry.decision] = (byDecision[entry.decision] ?? 0) + 1;
17
+ }
18
+ if (event === "PreToolUse" && tool === "Read" && entry.decision === "deny") {
19
+ readDenyCount += 1;
20
+ }
21
+ const ts = entry.ts;
22
+ if (typeof ts === "string") {
23
+ if (firstEntryTs === null || ts < firstEntryTs) firstEntryTs = ts;
24
+ if (lastEntryTs === null || ts > lastEntryTs) lastEntryTs = ts;
25
+ }
26
+ }
27
+ return {
28
+ totalInvocations: entries.length,
29
+ byEvent: Object.freeze(byEvent),
30
+ byTool: Object.freeze(byTool),
31
+ byDecision: Object.freeze(byDecision),
32
+ readDenyCount,
33
+ estimatedTokensSaved: readDenyCount * ESTIMATED_TOKENS_PER_READ_DENY,
34
+ firstEntry: firstEntryTs,
35
+ lastEntry: lastEntryTs
36
+ };
37
+ }
38
+ function formatStatsSummary(summary) {
39
+ if (summary.totalInvocations === 0) {
40
+ return "engram hook stats: no log entries yet.\n\nRun engram install-hook in a project, then use Claude Code to see interceptions.";
41
+ }
42
+ const lines = [];
43
+ lines.push(`engram hook stats (${summary.totalInvocations} invocations)`);
44
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
45
+ if (summary.firstEntry && summary.lastEntry) {
46
+ lines.push(`Time range: ${summary.firstEntry} \u2192 ${summary.lastEntry}`);
47
+ lines.push("");
48
+ }
49
+ lines.push("By event:");
50
+ const eventEntries = Object.entries(summary.byEvent).sort(
51
+ (a, b) => b[1] - a[1]
52
+ );
53
+ for (const [event, count] of eventEntries) {
54
+ const pct = (count / summary.totalInvocations * 100).toFixed(1);
55
+ lines.push(` ${event.padEnd(18)} ${String(count).padStart(5)} (${pct}%)`);
56
+ }
57
+ lines.push("");
58
+ lines.push("By tool:");
59
+ const toolEntries = Object.entries(summary.byTool).filter(([k]) => k !== "unknown").sort((a, b) => b[1] - a[1]);
60
+ for (const [tool, count] of toolEntries) {
61
+ lines.push(` ${tool.padEnd(18)} ${String(count).padStart(5)}`);
62
+ }
63
+ if (toolEntries.length === 0) {
64
+ lines.push(" (no tool-tagged entries)");
65
+ }
66
+ lines.push("");
67
+ const decisionEntries = Object.entries(summary.byDecision);
68
+ if (decisionEntries.length > 0) {
69
+ lines.push("PreToolUse decisions:");
70
+ for (const [decision, count] of decisionEntries.sort(
71
+ (a, b) => b[1] - a[1]
72
+ )) {
73
+ lines.push(` ${decision.padEnd(18)} ${String(count).padStart(5)}`);
74
+ }
75
+ lines.push("");
76
+ }
77
+ if (summary.readDenyCount > 0) {
78
+ lines.push(
79
+ `Estimated tokens saved: ~${summary.estimatedTokensSaved.toLocaleString()}`
80
+ );
81
+ lines.push(
82
+ ` (${summary.readDenyCount} Read denies \xD7 ${ESTIMATED_TOKENS_PER_READ_DENY} tok/deny avg)`
83
+ );
84
+ } else {
85
+ lines.push("Estimated tokens saved: 0");
86
+ lines.push(" (no PreToolUse:Read denies recorded yet)");
87
+ }
88
+ return lines.join("\n");
89
+ }
90
+
91
+ // src/intercept/component-status.ts
92
+ import { existsSync, readFileSync, writeFileSync } from "fs";
93
+ import { join, dirname } from "path";
94
+ import { fileURLToPath } from "url";
95
+ import { tmpdir, homedir } from "os";
96
+ function statusPath(projectRoot) {
97
+ return join(projectRoot, ".engram", "component-status.json");
98
+ }
99
+ function readCachedStatus(projectRoot) {
100
+ const path = statusPath(projectRoot);
101
+ if (!existsSync(path)) return null;
102
+ try {
103
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
104
+ if (Date.now() - raw.generatedAt > 3e4) return null;
105
+ return raw;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ function checkHttp(projectRoot) {
111
+ return existsSync(join(projectRoot, ".engram", "http-server.pid"));
112
+ }
113
+ function checkLsp(projectRoot) {
114
+ if (existsSync(join(projectRoot, ".engram", "lsp-available"))) return true;
115
+ const tmp = tmpdir();
116
+ const candidates = [
117
+ join(tmp, "tsserver.sock"),
118
+ join(tmp, "typescript-language-server.sock")
119
+ ];
120
+ return candidates.some((c) => existsSync(c));
121
+ }
122
+ function checkAst(projectRoot) {
123
+ try {
124
+ const here = dirname(fileURLToPath(import.meta.url));
125
+ const candidates = [
126
+ join(here, "..", "grammars"),
127
+ // from dist/intercept/
128
+ join(here, "..", "..", "dist", "grammars")
129
+ // from src/intercept/ dev
130
+ ];
131
+ for (const dir of candidates) {
132
+ if (existsSync(dir)) return true;
133
+ }
134
+ } catch {
135
+ }
136
+ if (existsSync(join(projectRoot, "node_modules", "web-tree-sitter"))) return true;
137
+ return false;
138
+ }
139
+ function countIdeAdapters(projectRoot) {
140
+ let count = 0;
141
+ if (existsSync(join(projectRoot, ".cursor", "rules", "engram-context.mdc"))) {
142
+ count += 1;
143
+ }
144
+ const continueConfig = join(homedir(), ".continue", "config.json");
145
+ if (existsSync(continueConfig)) {
146
+ try {
147
+ const cfg = readFileSync(continueConfig, "utf-8");
148
+ if (cfg.includes("engram")) count += 1;
149
+ } catch {
150
+ }
151
+ }
152
+ const zedSettings = join(homedir(), ".config", "zed", "settings.json");
153
+ if (existsSync(zedSettings)) {
154
+ try {
155
+ const cfg = readFileSync(zedSettings, "utf-8");
156
+ if (cfg.includes("engram")) count += 1;
157
+ } catch {
158
+ }
159
+ }
160
+ for (const f of ["settings.local.json", "settings.json"]) {
161
+ const claudeSettings = join(projectRoot, ".claude", f);
162
+ if (existsSync(claudeSettings)) {
163
+ try {
164
+ const cfg = readFileSync(claudeSettings, "utf-8");
165
+ if (cfg.includes("engram")) {
166
+ count += 1;
167
+ break;
168
+ }
169
+ } catch {
170
+ }
171
+ }
172
+ }
173
+ if (existsSync(join(projectRoot, ".windsurfrules"))) count += 1;
174
+ if (existsSync(join(projectRoot, ".aider-context.md"))) count += 1;
175
+ if (existsSync(join(projectRoot, ".context", "index.md"))) count += 1;
176
+ return count;
177
+ }
178
+ function refreshComponentStatus(projectRoot) {
179
+ const now = Date.now();
180
+ const components = [
181
+ { name: "http", available: checkHttp(projectRoot), checkedAt: now },
182
+ { name: "lsp", available: checkLsp(projectRoot), checkedAt: now },
183
+ { name: "ast", available: checkAst(projectRoot), checkedAt: now }
184
+ ];
185
+ const ideCount = countIdeAdapters(projectRoot);
186
+ const report = {
187
+ components,
188
+ ideCount,
189
+ generatedAt: now
190
+ };
191
+ try {
192
+ writeFileSync(statusPath(projectRoot), JSON.stringify(report), "utf-8");
193
+ } catch {
194
+ }
195
+ return report;
196
+ }
197
+ function getComponentStatus(projectRoot) {
198
+ const cached = readCachedStatus(projectRoot);
199
+ if (cached) return cached;
200
+ return refreshComponentStatus(projectRoot);
201
+ }
202
+ function formatHudStatus(report) {
203
+ const parts = [];
204
+ for (const c of report.components) {
205
+ const icon = c.available ? "\u2713" : "\u2717";
206
+ parts.push(`${c.name.toUpperCase()} ${icon}`);
207
+ }
208
+ if (report.ideCount > 0) {
209
+ parts.push(`${report.ideCount} IDE${report.ideCount > 1 ? "s" : ""}`);
210
+ }
211
+ return parts.join(" | ");
212
+ }
213
+
214
+ export {
215
+ ESTIMATED_TOKENS_PER_READ_DENY,
216
+ summarizeHookLog,
217
+ formatStatsSummary,
218
+ getComponentStatus,
219
+ formatHudStatus
220
+ };
@@ -310,7 +310,7 @@ function writeToFile(filePath, summary) {
310
310
  writeFileSync2(filePath, newContent);
311
311
  }
312
312
  async function autogen(projectRoot, target, task) {
313
- const { getStore } = await import("./core-77MHT3QV.js");
313
+ const { getStore } = await import("./core-6IY5L6II.js");
314
314
  const store = await getStore(projectRoot);
315
315
  try {
316
316
  let view = VIEWS.general;