engramx 2.0.1 → 2.1.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 +288 -0
- package/README.md +55 -0
- package/dist/{aider-context-BC5R2ZTA.js → aider-context-J557IHIP.js} +1 -1
- package/dist/auth-KB2ZRMS3.js +14 -0
- package/dist/check-2Z3MPZEJ.js +12 -0
- package/dist/{chunk-C6GBUOAL.js → chunk-4XA6ENNL.js} +1 -1
- package/dist/{chunk-533LR4I7.js → chunk-G4U3QOOW.js} +13 -97
- package/dist/chunk-N6PPKOPK.js +105 -0
- package/dist/chunk-RM2TBOVW.js +121 -0
- package/dist/chunk-SMU4WR3D.js +187 -0
- package/dist/chunk-XFE6ZANP.js +99 -0
- package/dist/chunk-XVYE4OX2.js +232 -0
- package/dist/{chunk-SJT7VS2G.js → chunk-ZVWRIVWQ.js} +17 -0
- package/dist/cli.js +593 -322
- package/dist/{core-6IY5L6II.js → core-TSXA5XZH.js} +1 -1
- package/dist/{cursor-mdc-GJ7E5LDD.js → cursor-mdc-VEOFFDVO.js} +1 -1
- package/dist/{exporter-GWU2GF23.js → exporter-AWXS34AS.js} +1 -1
- package/dist/{importer-V62NGZRK.js → importer-3Q5M6QBL.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/install-YVMVCFQW.js +121 -0
- package/dist/notify-5POGKMRX.js +36 -0
- package/dist/report-C3GTM3HY.js +12 -0
- package/dist/serve.js +4 -3
- package/dist/{server-6AOI7NQP.js → server-A6MUVKQK.js} +127 -31
- package/dist/{windsurf-rules-C7SVDHBL.js → windsurf-rules-RWPKBHRD.js} +1 -1
- package/dist/wizard-AOXWMSXW.js +274 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,294 @@ 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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [2.1.0] — 2026-04-21 — "Reliability + Zero-Friction Install"
|
|
10
|
+
|
|
11
|
+
First release in the v2.1 / v2.2 / v3.0 elevation trilogy. Design spec
|
|
12
|
+
at `docs/superpowers/specs/2026-04-20-engram-elevation-trilogy-design.md`.
|
|
13
|
+
|
|
14
|
+
Headline: `engram setup` is the new one-command first-run flow. Users
|
|
15
|
+
go from `npm install -g engramx` to a working Sentinel hook + indexed
|
|
16
|
+
graph in under 30 seconds. `engram doctor` reports component health
|
|
17
|
+
with remediation hints. `engram update` ships future hotfixes to every
|
|
18
|
+
install without surprise — passive notify, zero telemetry, one-command
|
|
19
|
+
upgrade. Plus fixes for issue #11 (AST/LSP path bug in flattened
|
|
20
|
+
bundles) and issue #14's Bash-ops half (auto-reindex on `rm`/`mv`/
|
|
21
|
+
`git rm` via an opt-in PostToolUse gate).
|
|
22
|
+
|
|
23
|
+
Contributor credit this release: [@gabiudrescu](https://github.com/gabiudrescu)
|
|
24
|
+
for PR #13 (reindex CLI + `install-hook --auto-reindex`), PR #12
|
|
25
|
+
(watcher prune on delete/rename), and the original v2.0.2 security
|
|
26
|
+
disclosure. [@ttessarolo](https://github.com/ttessarolo) for precise
|
|
27
|
+
forensics + suggested fix on issue #11.
|
|
28
|
+
|
|
29
|
+
### Added — v2.1 "Reliability + Zero-Friction Install" track
|
|
30
|
+
|
|
31
|
+
- **`engram update`** — one-command self-upgrade.
|
|
32
|
+
Passive notify on every `engram *` invocation when a newer version is
|
|
33
|
+
available (cached, at most one line on stderr, throttled to a 7-day
|
|
34
|
+
registry check). Manual trigger detects the package manager that owns
|
|
35
|
+
the engram install (npm / pnpm / yarn / bun via install-path markers)
|
|
36
|
+
and shells out to its global-upgrade command. `--check` for dry-probe,
|
|
37
|
+
`--force` to bypass the 7-day throttle, `--dry-run` to print the
|
|
38
|
+
upgrade command without executing it, `--manager <mgr>` override.
|
|
39
|
+
Zero telemetry: the only network call is an anonymous GET to
|
|
40
|
+
`registry.npmjs.org/engramx/latest`. `ENGRAM_NO_UPDATE_CHECK=1` and
|
|
41
|
+
`$CI` disable the entire subsystem. Addresses the "1,300 weekly
|
|
42
|
+
downloads, 10/day organic, near-zero hotfix reach" problem.
|
|
43
|
+
|
|
44
|
+
- **`engram doctor`** — component health report with remediation hints.
|
|
45
|
+
Wraps existing probes (HTTP, LSP, AST, IDE adapters) plus four new
|
|
46
|
+
checks: engram version freshness, `.engram/graph.db` presence,
|
|
47
|
+
Sentinel hook installation, IDE adapter count. Each check emits
|
|
48
|
+
severity (ok / warn / fail) + detail + optional remediation. Exit
|
|
49
|
+
code reflects overall severity (0 ok, 1 warn, 2 fail) so `doctor`
|
|
50
|
+
is CI-friendly. `--verbose` shows remediation hints; `--json` /
|
|
51
|
+
`--export` emits redacted JSON for bug-report attachment
|
|
52
|
+
(`projectRoot` intentionally omitted — can contain usernames).
|
|
53
|
+
|
|
54
|
+
- **`engram setup`** — zero-friction first-run wizard. One command for
|
|
55
|
+
"go from cloned repo to working engram in under 30 seconds."
|
|
56
|
+
Runs `init` (if `.engram/graph.db` missing) → `install-hook` (with
|
|
57
|
+
prompted scope, `local` default) → detects IDE adapters (Cursor,
|
|
58
|
+
Windsurf, Continue.dev, Aider) and suggests the matching `gen-*`
|
|
59
|
+
command for each → finishes with a `doctor` summary. Each step is
|
|
60
|
+
idempotent. `--yes` runs with defaults; `--dry-run` prints intent
|
|
61
|
+
without acting; `--scope` controls the install-hook scope. Drops
|
|
62
|
+
install-to-first-value from 4 commands to 1.
|
|
63
|
+
|
|
64
|
+
- **`engram init --with-hook`** — shorthand for `init` followed by
|
|
65
|
+
`install-hook` (local scope, idempotent). The #1 thing every user
|
|
66
|
+
does after `init` was `install-hook`; now it's one step.
|
|
67
|
+
|
|
68
|
+
- **First-run hint.** On any `engram` subcommand invoked in a repo
|
|
69
|
+
lacking `.engram/graph.db`, print one line on stderr:
|
|
70
|
+
`💡 First time in this repo? Run 'engram setup' for a zero-friction install.`
|
|
71
|
+
Throttled via `~/.engram/first-run-shown` (fires once per machine,
|
|
72
|
+
not per repo). Silenced in `$CI`, under `ENGRAM_NO_UPDATE_CHECK=1`,
|
|
73
|
+
and under the JSON-stdout commands (`intercept`, `cursor-intercept`,
|
|
74
|
+
`hud-label`, `setup`, `init`, `update`, `doctor`) so neither
|
|
75
|
+
pollutes the hook protocol.
|
|
76
|
+
|
|
77
|
+
- **Bash PostToolUse parser for auto-reindex** — closes half of
|
|
78
|
+
[#14](https://github.com/NickCirv/engram/issues/14).
|
|
79
|
+
`src/intercept/handlers/bash-postool.ts` parses file-mutating Bash
|
|
80
|
+
commands (`rm`, `mv`, `cp`, `git rm`, `git mv`, single-redirect
|
|
81
|
+
`<cmd> > <dst>`) into `FileOp { action, path }` records. Strict
|
|
82
|
+
parser: globs, pipes, subshells, command-substitution, directory
|
|
83
|
+
ops, and `touch` all pass through untouched. Wired into the
|
|
84
|
+
PostToolUse observer path in `handlers/post-tool.ts` — on Bash
|
|
85
|
+
PostToolUse events, each op is handed to `syncFile()` fire-and-forget.
|
|
86
|
+
Gated by `ENGRAM_AUTO_REINDEX=1` opt-in until
|
|
87
|
+
[#13](https://github.com/NickCirv/engram/pull/13)'s install-hook
|
|
88
|
+
`--auto-reindex` flag lands; that flag will toggle the env gate
|
|
89
|
+
implicitly.
|
|
90
|
+
|
|
91
|
+
### Fixed — v2.1 reliability
|
|
92
|
+
|
|
93
|
+
- **AST grammar detection in flattened bundles**
|
|
94
|
+
([#11](https://github.com/NickCirv/engram/issues/11) partial).
|
|
95
|
+
When `tsup`/`esbuild` flattens chunks to `engramx/dist/chunk-*.js`,
|
|
96
|
+
`import.meta.url` resolves to `engramx/dist` and the previous
|
|
97
|
+
candidates (`../grammars` and `../../dist/grammars`) both missed the
|
|
98
|
+
actual grammar dir. Added `join(here, "grammars")` as the first
|
|
99
|
+
candidate; dev-time layout (`src/intercept/`) still works via the
|
|
100
|
+
third candidate. Thanks [@ttessarolo](https://github.com/ttessarolo).
|
|
101
|
+
|
|
102
|
+
- **LSP socket candidate coverage**
|
|
103
|
+
([#11](https://github.com/NickCirv/engram/issues/11) partial).
|
|
104
|
+
`checkLsp` was looking for two socket names while
|
|
105
|
+
`lsp-connection.ts::candidateSockets()` probes six. Synced the list
|
|
106
|
+
so HUD availability matches actual provider availability. Kept
|
|
107
|
+
`.engram/lsp-available` as an explicit user opt-in marker for
|
|
108
|
+
back-compat.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **Locale-independent number formatting across the codebase.** All 10
|
|
113
|
+
`Number.prototype.toLocaleString()` callsites in `src/cli.ts`,
|
|
114
|
+
`src/serve.ts`, `src/dashboard.ts`, and `src/intercept/stats.ts` have
|
|
115
|
+
been migrated to a shared `formatThousands()` helper in
|
|
116
|
+
`src/graph/render-utils.ts`. Two wins:
|
|
117
|
+
|
|
118
|
+
1. **Deterministic performance.** First-call ICU init on Windows Node
|
|
119
|
+
has been observed to take multiple seconds in GitHub Actions VMs,
|
|
120
|
+
flaking tests at the 5000ms default timeout (seen on
|
|
121
|
+
`tests/intercept/stats.test.ts > formatStatsSummary` post-merge on
|
|
122
|
+
`9f99f5b`). The regex-based helper runs in microseconds with no
|
|
123
|
+
ICU dependency.
|
|
124
|
+
2. **Locale independence.** `toLocaleString()` emits `"1,234"` on
|
|
125
|
+
en-US but `"1.234"` on de-DE and `"1 234"` on fr-FR, giving users
|
|
126
|
+
running engram in non-US shells inconsistent output. All CLI +
|
|
127
|
+
MCP server + dashboard numbers now render with commas regardless
|
|
128
|
+
of system locale.
|
|
129
|
+
|
|
130
|
+
Added `tests/render-utils.test.ts > formatThousands` — 6 tests
|
|
131
|
+
covering single-digit, multi-group, negative, and locale-stable cases.
|
|
132
|
+
Also added `vitest.config.ts` with CI-only `retry: 1` +
|
|
133
|
+
`testTimeout: 15000ms` as defense-in-depth against other cold-worker
|
|
134
|
+
flakes.
|
|
135
|
+
|
|
136
|
+
- **`engram watch` now prunes graph nodes when watched files are deleted
|
|
137
|
+
or renamed** ([#9](https://github.com/NickCirv/engram/issues/9),
|
|
138
|
+
[#12](https://github.com/NickCirv/engram/pull/12)). Previously the
|
|
139
|
+
watcher only subscribed to `change` events, silently ignoring the
|
|
140
|
+
`rename` events that `fs.watch` fires for create/unlink across all
|
|
141
|
+
platforms. Deletions left stale nodes in the graph until the next
|
|
142
|
+
`engram init`; renames produced duplicate nodes under the old and new
|
|
143
|
+
`sourceFile` paths. Thanks [@gabiudrescu](https://github.com/gabiudrescu).
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
|
|
147
|
+
- **`syncFile(absPath, root)`** exported from `src/watcher.ts` — the shared
|
|
148
|
+
"exists → reindex; gone (and was indexed) → prune" primitive reused by
|
|
149
|
+
the upcoming `engram reindex` CLI subcommand ([#8](https://github.com/NickCirv/engram/issues/8)).
|
|
150
|
+
Returns a discriminated `SyncResult` (`indexed` | `pruned` | `skipped`).
|
|
151
|
+
- **`GraphStore.countBySourceFile(relPath)`** — noise-reduction gate so
|
|
152
|
+
`onDelete` only fires for files the graph actually indexed.
|
|
153
|
+
- **`onDelete` callback on `WatchOptions`** — fires with `(filePath, prunedCount)`
|
|
154
|
+
when the watcher prunes a deleted file's nodes.
|
|
155
|
+
- **`× <path> pruned (N nodes)`** log line in `engram watch`, distinct from
|
|
156
|
+
the existing green `↻` reindex line.
|
|
157
|
+
- **`gen-cursor --watch`, `gen-aider --watch`, `gen-windsurfrules --watch`**
|
|
158
|
+
now regenerate their output files on source-file delete (not just on
|
|
159
|
+
reindex), so generated artifacts no longer keep stale references to
|
|
160
|
+
deleted sources.
|
|
161
|
+
- **`engram reindex <file>` CLI subcommand**
|
|
162
|
+
([#8](https://github.com/NickCirv/engram/issues/8)) — re-indexes a
|
|
163
|
+
single file into the knowledge graph. The missing primitive for per-
|
|
164
|
+
edit freshness: Claude Code PostToolUse hooks, editor plugins, and CI
|
|
165
|
+
can now keep the graph in sync without running a long-lived watcher.
|
|
166
|
+
Reuses `syncFile()` so semantics match `engram watch`: exists →
|
|
167
|
+
reindex; missing-but-previously-indexed → prune; unsupported ext or
|
|
168
|
+
ignored directory → silent exit 0 (safe to fire on every edit). On
|
|
169
|
+
success prints a single line `engram: reindexed <file> (<N> nodes)`
|
|
170
|
+
(or `pruned`) using locale-stable `formatThousands`. `--verbose`
|
|
171
|
+
surfaces stack traces; default error output is a single stderr line.
|
|
172
|
+
Missing graph exits 1 with `engram: no graph found at <root>. Run
|
|
173
|
+
'engram init' first.`, matching `engram watch`.
|
|
174
|
+
- **`formatReindexLine(result, displayPath)`** exported from
|
|
175
|
+
`src/watcher.ts` — pure formatter shared by the new subcommand. Returns
|
|
176
|
+
`null` for skipped results so callers stay silent.
|
|
177
|
+
- **`engram reindex-hook` subcommand + `engram install-hook --auto-reindex`**
|
|
178
|
+
([#8](https://github.com/NickCirv/engram/issues/8), opt-in auto-wire).
|
|
179
|
+
`reindex-hook` reads Claude Code's PostToolUse payload from stdin and
|
|
180
|
+
re-indexes `tool_input.file_path` via the shared `syncFile()` primitive.
|
|
181
|
+
Contract: ALWAYS exits 0 — malformed JSON, missing fields, non-project
|
|
182
|
+
`cwd`, and all internal errors resolve to a silent no-op so the hook
|
|
183
|
+
can never fail Claude Code's tool cycle. `install-hook --auto-reindex`
|
|
184
|
+
appends a second PostToolUse entry with matcher `Edit|Write|MultiEdit`
|
|
185
|
+
calling `engram reindex-hook`; off by default so existing users aren't
|
|
186
|
+
surprised. The new entry is recognized by `isEngramHookEntry()` so
|
|
187
|
+
`engram uninstall-hook` strips it alongside the primary intercept
|
|
188
|
+
entries. Idempotent — reinstalling with `--auto-reindex` is a no-op
|
|
189
|
+
when the entry already exists.
|
|
190
|
+
- **`runReindexHook(payload)`** exported from `src/watcher.ts` — the
|
|
191
|
+
pure async handler behind the `reindex-hook` subcommand. Validates
|
|
192
|
+
payload shape, resolves project root from `cwd`, delegates to
|
|
193
|
+
`syncFile`. Swallows every error.
|
|
194
|
+
- **`buildReindexHookEntry()` + `ENGRAM_REINDEX_HOOK_MATCHER`
|
|
195
|
+
(`"Edit|Write|MultiEdit"`) + `DEFAULT_ENGRAM_REINDEX_HOOK_COMMAND`
|
|
196
|
+
(`"engram reindex-hook"`)** exported from `src/intercept/installer.ts`
|
|
197
|
+
— the data primitives for the optional entry. Added
|
|
198
|
+
`InstallOptions.autoReindex` and `InstallResult.autoReindexAdded` to
|
|
199
|
+
thread the opt-in through the existing installer surface.
|
|
200
|
+
|
|
201
|
+
### Notes
|
|
202
|
+
|
|
203
|
+
- Directory deletion (`rm -rf src/foo`) is intentionally not handled by the
|
|
204
|
+
watcher — `fs.watch` fires a single rename event on the directory path
|
|
205
|
+
with no per-file information. A full `engram init` handles that case
|
|
206
|
+
today; per-file directory-prefix pruning is tracked for v2.2.
|
|
207
|
+
|
|
208
|
+
## [2.0.2] — 2026-04-18 — Security hotfix: HTTP server auth & CORS
|
|
209
|
+
|
|
210
|
+
**This is a security release. Upgrade immediately if you run `engram server`
|
|
211
|
+
or `engram ui`.** Credit: [@gabiudrescu](https://github.com/gabiudrescu) for
|
|
212
|
+
responsible disclosure ([#7](https://github.com/NickCirv/engram/issues/7)).
|
|
213
|
+
|
|
214
|
+
### Security — fixed
|
|
215
|
+
|
|
216
|
+
- **Graph exfiltration + persistent prompt injection via cross-origin browser
|
|
217
|
+
tabs.** The HTTP server previously shipped with `Access-Control-Allow-Origin: *`
|
|
218
|
+
on every response and defaulted to no authentication. A malicious page the
|
|
219
|
+
developer visited could `fetch('http://127.0.0.1:7337/query')` to steal the
|
|
220
|
+
local graph, then `POST /learn` (with `Content-Type: text/plain`, a
|
|
221
|
+
CORS-safelisted content type) to persist `bug:` / `fix:` patterns that the
|
|
222
|
+
v2 Sentinel handlers later re-injected into the user's coding agent on
|
|
223
|
+
SessionStart and on every Edit/Write of the named file. Severity: High —
|
|
224
|
+
confidentiality + persistent indirect prompt injection.
|
|
225
|
+
|
|
226
|
+
**Fix (four stacked defenses):**
|
|
227
|
+
1. **Fail-closed auth.** Every route except `/health` and `/favicon.ico`
|
|
228
|
+
now requires `Authorization: Bearer <token>` or an HttpOnly
|
|
229
|
+
`engram_token` cookie. A random 64-character token is auto-generated
|
|
230
|
+
on first server start and persisted to `~/.engram/http-server.token`
|
|
231
|
+
with mode `0600`. `ENGRAM_API_TOKEN` env var still overrides.
|
|
232
|
+
2. **No wildcard CORS.** `Access-Control-Allow-Origin: *` has been removed
|
|
233
|
+
from every response. By default no CORS headers are emitted — the
|
|
234
|
+
dashboard is same-origin. Additional origins opt in via
|
|
235
|
+
`ENGRAM_ALLOWED_ORIGINS=a.com,b.com`.
|
|
236
|
+
3. **Host + Origin validation** (DNS-rebinding defense). Requests with a
|
|
237
|
+
`Host` header other than `127.0.0.1|localhost|::1` on the bound port
|
|
238
|
+
return 400. Requests with an `Origin` not in the same-origin or env
|
|
239
|
+
allowlist return 403.
|
|
240
|
+
4. **`Content-Type: application/json` enforced on mutations.** POST / PUT /
|
|
241
|
+
DELETE without `application/json` return 415. This blocks the
|
|
242
|
+
`text/plain` CSRF vector from the PoC and forces CORS preflight for
|
|
243
|
+
any cross-origin writer.
|
|
244
|
+
|
|
245
|
+
- **Timing side-channel on token comparison.** The previous
|
|
246
|
+
`header === \`Bearer ${token}\`` comparison was not constant-time.
|
|
247
|
+
Replaced with a length-first, constant-time `safeEqual()`.
|
|
248
|
+
|
|
249
|
+
### Added
|
|
250
|
+
|
|
251
|
+
- `src/server/auth.ts` — token management (get-or-create, safeEqual, cookie
|
|
252
|
+
parsing, Host/Origin validators).
|
|
253
|
+
- `tests/server/security.test.ts` — PoC-style tests covering fail-closed
|
|
254
|
+
auth (including empty Bearer / empty cookie guards), env-downgrade
|
|
255
|
+
rejection (token is snapshot at start), cookie auth, wildcard-CORS
|
|
256
|
+
absence, same-origin echo, foreign-origin 403, Host header validation
|
|
257
|
+
(including no-port rejection + case-insensitive hostname), `text/plain`
|
|
258
|
+
rejection on `/learn`, the `/ui?token=` cross-site oracle defence via
|
|
259
|
+
`Sec-Fetch-Site` gating, and the end-to-end exploit chain from #7.
|
|
260
|
+
- `SECURITY.md` at repo root with disclosure policy and scope.
|
|
261
|
+
- `GET /ui?token=<t>` bootstrap path for the browser dashboard. The CLI
|
|
262
|
+
passes the token once; the server exchanges it for an HttpOnly cookie via
|
|
263
|
+
a 302 redirect and strips the token from the URL. Dashboard JS never sees
|
|
264
|
+
the raw token.
|
|
265
|
+
|
|
266
|
+
### Changed
|
|
267
|
+
|
|
268
|
+
- `createHttpServer(projectRoot, port)` now resolves to `Promise<TokenInfo>`
|
|
269
|
+
(previously `Promise<void>`). The returned object exposes the token source
|
|
270
|
+
(env / file / generated) and the token file path. The CLI uses this to
|
|
271
|
+
print a one-time banner pointing users at `~/.engram/http-server.token`
|
|
272
|
+
when a fresh token is minted.
|
|
273
|
+
- `checkAuth` rewritten as fail-closed, accepts Bearer header OR
|
|
274
|
+
`engram_token` cookie, uses constant-time comparison.
|
|
275
|
+
- Server-Sent Events endpoint (`/api/sse`) no longer emits wildcard CORS and
|
|
276
|
+
inherits the same origin-allowlist behavior as every other route.
|
|
277
|
+
|
|
278
|
+
### Breaking
|
|
279
|
+
|
|
280
|
+
- **External callers (curl, scripts, CI probes) must now send the token.**
|
|
281
|
+
Fix the one-liner on each caller:
|
|
282
|
+
```bash
|
|
283
|
+
curl -H "Authorization: Bearer $(cat ~/.engram/http-server.token)" \
|
|
284
|
+
http://127.0.0.1:7337/stats
|
|
285
|
+
```
|
|
286
|
+
- Requests with `Host: something-else.com` are rejected 400 even if they
|
|
287
|
+
resolve to 127.0.0.1 locally. DNS rebinding defense — intended behavior.
|
|
288
|
+
- Cross-origin requests (`Origin: https://example.com`) are rejected 403
|
|
289
|
+
unless the origin is in `ENGRAM_ALLOWED_ORIGINS`. No legitimate caller
|
|
290
|
+
should be affected.
|
|
291
|
+
- `/ui` navigation from the browser now requires `?token=<t>` on first visit
|
|
292
|
+
(set automatically when you run `engram ui`) or a pre-existing
|
|
293
|
+
`engram_token` cookie.
|
|
294
|
+
|
|
7
295
|
## [2.0.1] — 2026-04-17 — Windows CI + favicon route
|
|
8
296
|
|
|
9
297
|
Patch release fixing two issues caught immediately after v2.0.0 shipped.
|
package/README.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
<img src="assets/banner.png" alt="engram — AI coding memory" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+
<!-- ============================================================
|
|
6
|
+
24-second product showcase (Hyperframes-rendered MP4 + WebM).
|
|
7
|
+
Source: docs/demos/showcase.html · scenes drive both the
|
|
8
|
+
live HTML player and this MP4. Edit scene-table.md to change.
|
|
9
|
+
If the MP4 isn't rendered yet, GitHub gracefully shows the
|
|
10
|
+
poster image and links to the live HTML player.
|
|
11
|
+
============================================================ -->
|
|
12
|
+
<p align="center">
|
|
13
|
+
<video src="https://raw.githubusercontent.com/NickCirv/engram/main/docs/demos/showcase.mp4"
|
|
14
|
+
controls
|
|
15
|
+
muted
|
|
16
|
+
playsinline
|
|
17
|
+
poster="docs/demos/poster.svg"
|
|
18
|
+
width="100%">
|
|
19
|
+
<a href="docs/demos/showcase.html">
|
|
20
|
+
<img src="docs/demos/poster.svg" alt="engram — 24-second showcase (click to open the live HTML player)" width="100%">
|
|
21
|
+
</a>
|
|
22
|
+
</video>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<sub>
|
|
27
|
+
<a href="docs/install.html"><strong>Install Page</strong></a> ·
|
|
28
|
+
<a href="docs/demos/showcase.html"><strong>Live Demo</strong></a> ·
|
|
29
|
+
<a href="docs/demos/scene-table.md"><strong>Scene Table</strong></a> ·
|
|
30
|
+
rendered with <a href="https://github.com/heygen-com/hyperframes">Hyperframes</a>
|
|
31
|
+
</sub>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
5
34
|
<p align="center">
|
|
6
35
|
<a href="#install"><strong>Install</strong></a> ·
|
|
7
36
|
<a href="#quickstart"><strong>Quickstart</strong></a> ·
|
|
@@ -175,10 +204,23 @@ npm install -g engramx
|
|
|
175
204
|
|
|
176
205
|
Requires Node.js 20+. Zero native dependencies. No build tools. Local SQLite via sql.js WASM — no Rust, no Python, no system libs.
|
|
177
206
|
|
|
207
|
+
> **Prefer a designed walkthrough?** Open [**docs/install.html**](docs/install.html) — three-step install, benefits matrix, IDE coverage, FAQ. Local file, opens in any browser. Brand-matched terminal-mono aesthetic.
|
|
208
|
+
|
|
178
209
|
---
|
|
179
210
|
|
|
180
211
|
## Quickstart
|
|
181
212
|
|
|
213
|
+
**One command, zero friction:**
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
cd ~/my-project
|
|
217
|
+
engram setup # init + install-hook + adapter detect + doctor
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`engram setup` runs the whole first-run flow interactively (or pass `-y` for defaults, `--dry-run` to preview). It is idempotent — safe to re-run, and skips any step already done.
|
|
221
|
+
|
|
222
|
+
<sub>Prefer the individual commands?</sub>
|
|
223
|
+
|
|
182
224
|
```bash
|
|
183
225
|
cd ~/my-project
|
|
184
226
|
engram init # scan codebase → .engram/graph.db (~40ms, 0 tokens)
|
|
@@ -186,6 +228,16 @@ engram install-hook # wire the Sentinel into Claude Code
|
|
|
186
228
|
engram ui # open the web dashboard in your browser
|
|
187
229
|
```
|
|
188
230
|
|
|
231
|
+
**Diagnostics + self-update:**
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
engram doctor # component health + remediation hints (0=ok, 1=warn, 2=fail)
|
|
235
|
+
engram update # check + upgrade via detected pkg manager (no telemetry)
|
|
236
|
+
engram update --check # check only, dry-probe the registry
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Set `ENGRAM_NO_UPDATE_CHECK=1` to disable the passive "newer version available" hint on every CLI invocation. `$CI` does the same automatically.
|
|
240
|
+
|
|
189
241
|
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:
|
|
190
242
|
|
|
191
243
|
```bash
|
|
@@ -275,6 +327,7 @@ engram install-hook # default: .claude/settings.local.json (git
|
|
|
275
327
|
engram install-hook --scope project # .claude/settings.json (committed)
|
|
276
328
|
engram install-hook --scope user # ~/.claude/settings.json (global)
|
|
277
329
|
engram install-hook --dry-run # preview changes without writing
|
|
330
|
+
engram install-hook --auto-reindex # also keep the graph fresh after every Edit/Write/MultiEdit (#8)
|
|
278
331
|
```
|
|
279
332
|
|
|
280
333
|
**Kill switch (if anything goes wrong):**
|
|
@@ -336,6 +389,8 @@ engram hook-enable # remove kill switch
|
|
|
336
389
|
|
|
337
390
|
```bash
|
|
338
391
|
engram watch [path] # live file watcher — incremental re-index on save
|
|
392
|
+
engram reindex <file> # re-index one file (editor/hook/CI primitive, issue #8)
|
|
393
|
+
engram reindex-hook # PostToolUse hook entry point (reads JSON from stdin, always exits 0)
|
|
339
394
|
engram dashboard [path] # live terminal dashboard
|
|
340
395
|
engram hud-label [path] # JSON label for Claude HUD --extra-cmd integration
|
|
341
396
|
engram hooks install # install post-commit + post-checkout git hooks
|
|
@@ -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-TSXA5XZH.js");
|
|
11
11
|
const store = await getStore(projectRoot);
|
|
12
12
|
try {
|
|
13
13
|
const allNodes = store.getAllNodes();
|
|
@@ -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-TSXA5XZH.js");
|
|
314
314
|
const store = await getStore(projectRoot);
|
|
315
315
|
try {
|
|
316
316
|
let view = VIEWS.general;
|
|
@@ -1,93 +1,3 @@
|
|
|
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
1
|
// src/intercept/component-status.ts
|
|
92
2
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
93
3
|
import { join, dirname } from "path";
|
|
@@ -112,10 +22,16 @@ function checkHttp(projectRoot) {
|
|
|
112
22
|
}
|
|
113
23
|
function checkLsp(projectRoot) {
|
|
114
24
|
if (existsSync(join(projectRoot, ".engram", "lsp-available"))) return true;
|
|
25
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
115
26
|
const tmp = tmpdir();
|
|
116
27
|
const candidates = [
|
|
117
|
-
join(tmp,
|
|
118
|
-
join(tmp, "
|
|
28
|
+
join(tmp, `tsserver-${uid}.sock`),
|
|
29
|
+
join(tmp, "lsp-server.sock"),
|
|
30
|
+
join(tmp, "typescript-language-server.sock"),
|
|
31
|
+
join(tmp, `pyright-${uid}.sock`),
|
|
32
|
+
join(tmp, "rust-analyzer.sock"),
|
|
33
|
+
// Legacy name kept for back-compat with older tsserver installs.
|
|
34
|
+
join(tmp, "tsserver.sock")
|
|
119
35
|
];
|
|
120
36
|
return candidates.some((c) => existsSync(c));
|
|
121
37
|
}
|
|
@@ -123,10 +39,12 @@ function checkAst(projectRoot) {
|
|
|
123
39
|
try {
|
|
124
40
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
125
41
|
const candidates = [
|
|
42
|
+
join(here, "grammars"),
|
|
43
|
+
// flattened bundle
|
|
126
44
|
join(here, "..", "grammars"),
|
|
127
|
-
//
|
|
45
|
+
// nested bundle
|
|
128
46
|
join(here, "..", "..", "dist", "grammars")
|
|
129
|
-
//
|
|
47
|
+
// dev-time
|
|
130
48
|
];
|
|
131
49
|
for (const dir of candidates) {
|
|
132
50
|
if (existsSync(dir)) return true;
|
|
@@ -212,9 +130,7 @@ function formatHudStatus(report) {
|
|
|
212
130
|
}
|
|
213
131
|
|
|
214
132
|
export {
|
|
215
|
-
|
|
216
|
-
summarizeHookLog,
|
|
217
|
-
formatStatsSummary,
|
|
133
|
+
refreshComponentStatus,
|
|
218
134
|
getComponentStatus,
|
|
219
135
|
formatHudStatus
|
|
220
136
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/server/auth.ts
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var TOKEN_MIN_LEN = 32;
|
|
13
|
+
var TOKEN_BYTES = 32;
|
|
14
|
+
function tokenDir() {
|
|
15
|
+
return join(homedir(), ".engram");
|
|
16
|
+
}
|
|
17
|
+
function tokenPath() {
|
|
18
|
+
return join(tokenDir(), "http-server.token");
|
|
19
|
+
}
|
|
20
|
+
function getOrCreateToken() {
|
|
21
|
+
const envToken = process.env.ENGRAM_API_TOKEN;
|
|
22
|
+
if (envToken && envToken.length >= TOKEN_MIN_LEN) {
|
|
23
|
+
return { token: envToken, source: "env", path: null };
|
|
24
|
+
}
|
|
25
|
+
const path = tokenPath();
|
|
26
|
+
if (existsSync(path)) {
|
|
27
|
+
try {
|
|
28
|
+
const cached = readFileSync(path, "utf8").trim();
|
|
29
|
+
if (cached.length >= TOKEN_MIN_LEN) {
|
|
30
|
+
return { token: cached, source: "file", path };
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const fresh = randomBytes(TOKEN_BYTES).toString("hex");
|
|
36
|
+
const dir = tokenDir();
|
|
37
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
38
|
+
writeFileSync(path, fresh + "\n", { mode: 384 });
|
|
39
|
+
try {
|
|
40
|
+
chmodSync(path, 384);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
return { token: fresh, source: "generated", path };
|
|
44
|
+
}
|
|
45
|
+
function safeEqual(a, b) {
|
|
46
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
47
|
+
if (a.length !== b.length) return false;
|
|
48
|
+
let diff = 0;
|
|
49
|
+
for (let i = 0; i < a.length; i++) {
|
|
50
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
51
|
+
}
|
|
52
|
+
return diff === 0;
|
|
53
|
+
}
|
|
54
|
+
function parseCookies(header) {
|
|
55
|
+
const out = {};
|
|
56
|
+
if (!header || typeof header !== "string") return out;
|
|
57
|
+
for (const pair of header.split(/;\s*/)) {
|
|
58
|
+
const eq = pair.indexOf("=");
|
|
59
|
+
if (eq < 0) continue;
|
|
60
|
+
const key = pair.slice(0, eq).trim();
|
|
61
|
+
const value = pair.slice(eq + 1).trim();
|
|
62
|
+
if (key) out[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function isHostValid(hostHeader, port) {
|
|
67
|
+
if (!hostHeader) return false;
|
|
68
|
+
let hostname;
|
|
69
|
+
let portStr;
|
|
70
|
+
if (hostHeader.startsWith("[")) {
|
|
71
|
+
const close = hostHeader.indexOf("]");
|
|
72
|
+
if (close < 0) return false;
|
|
73
|
+
hostname = hostHeader.slice(1, close);
|
|
74
|
+
portStr = hostHeader.slice(close + 2);
|
|
75
|
+
} else {
|
|
76
|
+
const colon = hostHeader.lastIndexOf(":");
|
|
77
|
+
if (colon < 0) {
|
|
78
|
+
hostname = hostHeader;
|
|
79
|
+
portStr = "";
|
|
80
|
+
} else {
|
|
81
|
+
hostname = hostHeader.slice(0, colon);
|
|
82
|
+
portStr = hostHeader.slice(colon + 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const h = hostname.toLowerCase();
|
|
86
|
+
if (h !== "127.0.0.1" && h !== "localhost" && h !== "::1") return false;
|
|
87
|
+
if (portStr !== String(port)) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function isOriginAllowed(origin, port) {
|
|
91
|
+
if (origin === `http://127.0.0.1:${port}`) return true;
|
|
92
|
+
if (origin === `http://localhost:${port}`) return true;
|
|
93
|
+
const env = process.env.ENGRAM_ALLOWED_ORIGINS;
|
|
94
|
+
if (!env) return false;
|
|
95
|
+
const list = env.split(",").map((s) => s.trim()).filter(Boolean);
|
|
96
|
+
return list.includes(origin);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
getOrCreateToken,
|
|
101
|
+
safeEqual,
|
|
102
|
+
parseCookies,
|
|
103
|
+
isHostValid,
|
|
104
|
+
isOriginAllowed
|
|
105
|
+
};
|