engramx 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,59 @@ All notable changes to engram are documented here. Format based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.0.2] — 2026-04-24 — "MCP Registry"
10
+
11
+ Chore release. No runtime changes. Adds the `mcpName` field to `package.json`
12
+ required by the Official MCP Registry (`registry.modelcontextprotocol.io`)
13
+ for namespace-ownership proof.
14
+
15
+ ### Added
16
+ - `package.json` → top-level `"mcpName": "io.github.NickCirv/engram"`. Registry-side check reads the published npm tarball's `package.json` and verifies the field matches the server name in `server.json`. Without it, `mcp-publisher publish` returns HTTP 400 with the guidance message.
17
+ - Also tightened `server.json` description fields to the registry's 100-char limit (top-level description + 5 environment-variable descriptions).
18
+
19
+ ### Why not bundled into 3.0.1
20
+ The `preuninstall` fix needed to ship ASAP to stop new users hitting the orphaned-hooks bug. MCP Registry integration was a separate problem surfaced during the submission flow.
21
+
22
+ ## [3.0.1] — 2026-04-24 — "Clean Uninstall"
23
+
24
+ **Patch release fixing the orphaned-hooks bug reported by @freenow82 within
25
+ hours of 3.0.0 going live on npm. No feature changes — this release is
26
+ purely about not leaving users stranded when they uninstall.**
27
+
28
+ ### The bug (what 3.0.0 shipped with)
29
+
30
+ `npm uninstall -g engramx` removed the binary from PATH but left the hook
31
+ entries in `~/.claude/settings.json` pointing at a `engram intercept`
32
+ command that no longer existed. Claude Code fires those hooks on every
33
+ tool call — the hook commands failed with ENOENT — and user-visible
34
+ behaviour was "Claude Code stopped executing anything." Recovery required
35
+ reinstalling engramx just to run `engram uninstall-hook` before
36
+ uninstalling again.
37
+
38
+ That is a bad experience. Sorry to anyone who hit it.
39
+
40
+ ### Fixed
41
+
42
+ - **`scripts/preuninstall.mjs`** now runs automatically before `npm uninstall -g engramx`. It reads `~/.claude/settings.json`, strips every hook entry that references engram (case-insensitive match on the command string), drops engram's statusLine/HUD, backs up the original to a timestamped `.bak` file, and writes the result atomically via rename. It NEVER fails the uninstall — if the settings file is missing, unparseable, or unwritable, it logs a single-line hint and exits 0. Contract: the user's `npm uninstall` always succeeds, with or without hook cleanup.
43
+ - **`scripts/postinstall.mjs`** prints a one-time info banner on `npm install -g engramx` showing the recommended next step (`engram setup`) and the clean-uninstall flow. Respects `$CI` and `$ENGRAM_NO_POSTINSTALL=1`.
44
+ - **New `engram repair-hooks` alias** — literally the same as `engram uninstall-hook`, but named so users who ended up with orphaned hooks after a bad uninstall can find it by the word they'd actually search for. No code duplication — `commander.alias()`.
45
+ - Both scripts included in the `files` allowlist of `package.json` so they ship in the tarball.
46
+
47
+ ### For users still stranded on 3.0.0
48
+
49
+ If you ran `npm uninstall -g engramx` before this patch shipped and Claude Code is still broken, you have two paths:
50
+
51
+ 1. **Fast, no reinstall:** edit `~/.claude/settings.json` manually (or run the one-line `jq` filter posted in the 3.0.1 announcement thread) to strip every entry whose `command` contains the word `engram`.
52
+ 2. **Works from 3.0.1:** `npm install -g engramx@3.0.1 && engram repair-hooks --scope user` — the install no longer stops execution (hooks are in place), run repair-hooks, then `npm uninstall -g engramx` again if you want engramx gone. This time the preuninstall cleans up automatically.
53
+
54
+ ### Tests
55
+
56
+ - New regression test: the preuninstall script on a fixture with a mix of engram hooks + unrelated hooks + a custom statusLine verifies 3 engram entries removed, unrelated keys preserved byte-for-byte, backup written, atomic rename completed.
57
+
58
+ ### Thanks
59
+
60
+ [@freenow82](https://www.reddit.com/user/freenow82) for the bug report and the transparency about the pain it caused. That feedback is the entire point of a public launch — the tool is measurably better for it.
61
+
9
62
  ## [3.0.0] — 2026-04-24 — "Spine"
10
63
 
11
64
  The biggest engramx release since v1.0. One meticulous release, not a
package/README.md CHANGED
@@ -99,6 +99,14 @@ It runs on your laptop. It doesn't send your code anywhere. It's Apache 2.0. The
99
99
 
100
100
  **Want even bigger savings?** Install a plugin. Each one closes a different context leak — see [Plugins multiply the savings](#plugins-multiply-the-savings) below. Drop a 10-line `.mjs` file in `~/.engram/plugins/` and the next session uses it.
101
101
 
102
+ **Want out?** Clean uninstall is one command:
103
+
104
+ ```bash
105
+ npm uninstall -g engramx # 3.0.1+ auto-runs preuninstall hook-cleanup
106
+ ```
107
+
108
+ If you installed 3.0.0 and ran `npm uninstall` before the 3.0.1 patch shipped, your Claude Code hooks may be orphaned. Run `engram repair-hooks --scope user` (install 3.0.1 first if needed) or see the [`CHANGELOG.md`](CHANGELOG.md#301--2026-04-24--clean-uninstall) for the manual `jq`-based recovery one-liner.
109
+
102
110
  ---
103
111
 
104
112
  ## Proof, not promises
package/dist/cli.js CHANGED
@@ -2487,7 +2487,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
2487
2487
  );
2488
2488
  }
2489
2489
  );
2490
- program.command("uninstall-hook").description("Remove engram hook entries from Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
2490
+ program.command("uninstall-hook").alias("repair-hooks").description("Remove engram hook entries from Claude Code settings (also: 'repair-hooks' \u2014 same thing, named for users who ended up with orphaned hooks after npm uninstall)").option("--scope <scope>", "local | project | user (default user on fresh runs)", "local").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
2491
2491
  const settingsPath = resolveSettingsPath(opts.scope, opts.project);
2492
2492
  if (!settingsPath) {
2493
2493
  console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
+ "mcpName": "io.github.NickCirv/engram",
4
5
  "description": "The context spine for AI coding agents. 9 built-in providers + mcpConfig plugin contract (wrap any MCP server in 10 lines), generic MCP-client aggregator (stdio), pre-mortem mistake-guard, bi-temporal mistake memory, Anthropic Auto-Memory bridge, SSE streaming context packets, dual-emit AGENTS.md+CLAUDE.md. 90.8% measured real-world token savings (reproducible bench included). Local SQLite, zero cloud.",
5
6
  "repository": {
6
7
  "type": "git",
@@ -25,7 +26,9 @@
25
26
  "lint": "tsc --noEmit",
26
27
  "prepublishOnly": "npm run build",
27
28
  "bench": "tsx bench/runner.ts",
28
- "stress": "tsx bench/stress-test.ts"
29
+ "stress": "tsx bench/stress-test.ts",
30
+ "postinstall": "node scripts/postinstall.mjs",
31
+ "preuninstall": "node scripts/preuninstall.mjs"
29
32
  },
30
33
  "keywords": [
31
34
  "structural-code-graph",
@@ -51,6 +54,8 @@
51
54
  },
52
55
  "files": [
53
56
  "dist",
57
+ "scripts/preuninstall.mjs",
58
+ "scripts/postinstall.mjs",
54
59
  "LICENSE",
55
60
  "README.md",
56
61
  "CHANGELOG.md"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall — one-time info banner on `npm install -g engramx`.
4
+ * Prints the 'what to do next' hint + the clean-uninstall flow so users
5
+ * don't end up with orphaned hooks (see CHANGELOG v3.0.1 context).
6
+ *
7
+ * Contract:
8
+ * - Never fails the install. Always exit 0.
9
+ * - Respects $CI (quiet in CI environments).
10
+ * - Respects $ENGRAM_NO_POSTINSTALL=1 (ops lever for automated rollouts).
11
+ */
12
+ if (process.env.CI || process.env.ENGRAM_NO_POSTINSTALL === "1") {
13
+ process.exit(0);
14
+ }
15
+
16
+ const lines = [
17
+ "",
18
+ " ✅ engramx installed.",
19
+ "",
20
+ " Get started:",
21
+ " cd <your-project> && engram setup",
22
+ "",
23
+ " To remove cleanly later (avoids orphaned Claude Code hooks):",
24
+ " engram uninstall-hook && npm uninstall -g engramx",
25
+ " (npm uninstall -g engramx by itself also works now — preuninstall",
26
+ " hook-cleanup is automatic in 3.0.1+)",
27
+ "",
28
+ " Docs: https://github.com/NickCirv/engram",
29
+ "",
30
+ ];
31
+ process.stdout.write(lines.join("\n"));
32
+ process.exit(0);
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * preuninstall — cleans up engramx's hook entries in the user's
4
+ * Claude Code settings BEFORE the binary is removed by npm.
5
+ *
6
+ * Why this file exists: without it, `npm uninstall -g engramx` leaves
7
+ * stale hook entries in ~/.claude/settings.json pointing at a binary
8
+ * that no longer exists. Claude Code then fires those hooks on every
9
+ * tool call, exec fails with ENOENT, and user-visible behavior is
10
+ * "Claude Code stopped executing anything." Reported by @freenow82 in
11
+ * 3.0.0's post-launch window — see CHANGELOG v3.0.1.
12
+ *
13
+ * Contract (critical):
14
+ * - NEVER fail the uninstall. We always exit 0. If cleanup hits any
15
+ * problem, we print a one-line hint and move on. The user's goal is
16
+ * to uninstall; we will not be the thing that blocks them.
17
+ * - Self-contained: this script must work even if `engram` is not on
18
+ * PATH at script time (npm's script env usually has it, but we're
19
+ * defensive — edge cases exist).
20
+ * - Scoped conservatively: only touch ~/.claude/settings.json (the
21
+ * USER scope, which is what a global install writes to). Do not
22
+ * walk arbitrary project directories.
23
+ * - Back up before edit. Atomic rename on write.
24
+ */
25
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { homedir } from "node:os";
28
+
29
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
30
+
31
+ // ── safe helpers ────────────────────────────────────────────────────
32
+
33
+ function parseJsonSafe(text) {
34
+ try {
35
+ return text.trim() ? JSON.parse(text) : {};
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * A hook entry is "engram-owned" if its command references the engram
43
+ * binary or any engram-related shell. We match conservatively: the
44
+ * substring "engram" (case-insensitive) anywhere in the command string.
45
+ * This is aggressive but safe on uninstall — if a user has a hook
46
+ * unrelated to engramx that happens to contain the word "engram", they
47
+ * wrote that themselves and can re-add it. On uninstall, err toward
48
+ * cleaning more rather than leaving orphans.
49
+ */
50
+ function isEngramHook(entry) {
51
+ if (!entry || typeof entry !== "object") return false;
52
+ const cmd = typeof entry.command === "string" ? entry.command : "";
53
+ return /engram/i.test(cmd);
54
+ }
55
+
56
+ /**
57
+ * Walk the entire hooks structure. `hooks` may be:
58
+ * hooks[event] = [{ matcher, hooks: [{ command, ... }, ...] }, ...]
59
+ * We rebuild each inner `hooks` array without engram entries, drop
60
+ * matchers whose inner array is now empty, drop event keys whose list
61
+ * is now empty.
62
+ */
63
+ function stripEngramHooks(settings) {
64
+ const changes = { hooksRemoved: 0, eventsAffected: new Set() };
65
+ if (!settings || typeof settings !== "object") return { settings, changes };
66
+ const { hooks } = settings;
67
+ if (!hooks || typeof hooks !== "object") return { settings, changes };
68
+
69
+ for (const event of Object.keys(hooks)) {
70
+ const list = Array.isArray(hooks[event]) ? hooks[event] : null;
71
+ if (!list) continue;
72
+ const kept = [];
73
+ for (const matcher of list) {
74
+ if (!matcher || typeof matcher !== "object") {
75
+ kept.push(matcher);
76
+ continue;
77
+ }
78
+ const innerHooks = Array.isArray(matcher.hooks) ? matcher.hooks : null;
79
+ if (!innerHooks) {
80
+ kept.push(matcher);
81
+ continue;
82
+ }
83
+ const innerKept = innerHooks.filter((h) => {
84
+ if (isEngramHook(h)) {
85
+ changes.hooksRemoved++;
86
+ changes.eventsAffected.add(event);
87
+ return false;
88
+ }
89
+ return true;
90
+ });
91
+ if (innerKept.length > 0) {
92
+ kept.push({ ...matcher, hooks: innerKept });
93
+ } else {
94
+ // entire matcher was engram-only — drop it
95
+ changes.eventsAffected.add(event);
96
+ }
97
+ }
98
+ if (kept.length > 0) {
99
+ hooks[event] = kept;
100
+ } else {
101
+ delete hooks[event];
102
+ }
103
+ }
104
+
105
+ // If hooks is now empty, drop the key entirely
106
+ if (hooks && Object.keys(hooks).length === 0) {
107
+ delete settings.hooks;
108
+ }
109
+
110
+ // Also drop engram statusLine (HUD)
111
+ if (
112
+ settings.statusLine &&
113
+ typeof settings.statusLine === "object" &&
114
+ typeof settings.statusLine.command === "string" &&
115
+ /engram/i.test(settings.statusLine.command)
116
+ ) {
117
+ delete settings.statusLine;
118
+ changes.eventsAffected.add("statusLine");
119
+ }
120
+
121
+ return { settings, changes };
122
+ }
123
+
124
+ // ── main ────────────────────────────────────────────────────────────
125
+
126
+ function main() {
127
+ // If no settings file, nothing to clean. Silent exit.
128
+ if (!existsSync(SETTINGS_PATH)) {
129
+ return;
130
+ }
131
+
132
+ let raw;
133
+ try {
134
+ raw = readFileSync(SETTINGS_PATH, "utf-8");
135
+ } catch {
136
+ return; // unreadable — leave alone, user will handle
137
+ }
138
+
139
+ const parsed = parseJsonSafe(raw);
140
+ if (parsed === null) {
141
+ console.log(
142
+ "[engramx preuninstall] skipped: could not parse " +
143
+ SETTINGS_PATH +
144
+ " (settings unchanged)."
145
+ );
146
+ return;
147
+ }
148
+
149
+ const { settings, changes } = stripEngramHooks(parsed);
150
+ if (changes.hooksRemoved === 0 && changes.eventsAffected.size === 0) {
151
+ return; // nothing to do
152
+ }
153
+
154
+ // Back up before any write.
155
+ const backupPath = `${SETTINGS_PATH}.engramx-preuninstall-${new Date()
156
+ .toISOString()
157
+ .replace(/[:.]/g, "-")}.bak`;
158
+ try {
159
+ copyFileSync(SETTINGS_PATH, backupPath);
160
+ } catch {
161
+ // if we can't back up, don't write — safety first
162
+ console.log(
163
+ "[engramx preuninstall] skipped: could not write backup next to " +
164
+ SETTINGS_PATH +
165
+ " (settings unchanged)."
166
+ );
167
+ return;
168
+ }
169
+
170
+ // Atomic write via rename.
171
+ try {
172
+ const tmp = `${SETTINGS_PATH}.engramx-preuninstall-tmp`;
173
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
174
+ renameSync(tmp, SETTINGS_PATH);
175
+ } catch (err) {
176
+ console.log(
177
+ "[engramx preuninstall] skipped: " + String(err) + " (settings unchanged)."
178
+ );
179
+ return;
180
+ }
181
+
182
+ console.log(
183
+ `[engramx] cleaned up ${changes.hooksRemoved} hook entr${changes.hooksRemoved === 1 ? "y" : "ies"} from ${SETTINGS_PATH}`
184
+ );
185
+ console.log(`[engramx] backup saved: ${backupPath}`);
186
+ console.log(
187
+ "[engramx] if anything looks off, restore with: cp " +
188
+ backupPath +
189
+ " " +
190
+ SETTINGS_PATH
191
+ );
192
+ }
193
+
194
+ try {
195
+ main();
196
+ } catch (err) {
197
+ // HARD REQUIREMENT: never fail uninstall. Swallow anything.
198
+ console.log("[engramx preuninstall] error (ignored): " + String(err));
199
+ }
200
+ process.exit(0);