engramx 1.0.0 → 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.
- package/CHANGELOG.md +118 -0
- package/README.md +67 -7
- package/dist/{aider-context-TNGSXMVY.js → aider-context-BC5R2ZTA.js} +1 -1
- package/dist/cache-AK6CF3BC.js +10 -0
- package/dist/chunk-22INHMKB.js +31 -0
- package/dist/chunk-533LR4I7.js +220 -0
- package/dist/{chunk-QOG4K427.js → chunk-C6GBUOAL.js} +1 -1
- package/dist/chunk-CIQQ5Y3S.js +338 -0
- package/dist/chunk-KL6NSPVA.js +59 -0
- package/dist/{chunk-SBHGK5WA.js → chunk-PEH54LYC.js} +85 -3
- package/dist/{chunk-6SFMVYUN.js → chunk-SJT7VS2G.js} +127 -23
- package/dist/cli.js +381 -264
- package/dist/{core-77MHT3QV.js → core-6IY5L6II.js} +2 -2
- package/dist/{cursor-mdc-HWVUZUZH.js → cursor-mdc-GJ7E5LDD.js} +1 -1
- package/dist/{exporter-A3VSLS4U.js → exporter-GWU2GF23.js} +1 -1
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/{importer-MCNFMV5O.js → importer-V62NGZRK.js} +4 -3
- package/dist/index.js +3 -3
- package/dist/{migrate-5ZJWF2HD.js → migrate-UKCO6BUU.js} +3 -1
- package/dist/plugin-loader-FCOMVOX7.js +100 -0
- package/dist/serve.js +2 -2
- package/dist/server-VBRTTECZ.js +1363 -0
- package/dist/{tuner-2LVIEE5V.js → tuner-KFNNGKG3.js} +4 -2
- package/dist/windsurf-rules-C7SVDHBL.js +59 -0
- package/package.json +4 -3
- package/dist/chunk-CEAANHHX.js +0 -88
- 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-
|
|
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%
|
|
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
|
-
| **
|
|
169
|
-
| **
|
|
170
|
-
| **Zed** | Context server | `engram context-server` |
|
|
225
|
+
| **Cursor** | MDC snapshot + native MCP | `engram gen-mdc` · [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-
|
|
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,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-
|
|
313
|
+
const { getStore } = await import("./core-6IY5L6II.js");
|
|
314
314
|
const store = await getStore(projectRoot);
|
|
315
315
|
try {
|
|
316
316
|
let view = VIEWS.general;
|