engramx 3.0.0 → 3.0.1
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 +40 -0
- package/README.md +8 -0
- package/dist/cli.js +1 -1
- package/package.json +6 -2
- package/scripts/postinstall.mjs +32 -0
- package/scripts/preuninstall.mjs +200 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,46 @@ All notable changes to engram are documented here. Format based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.0.1] — 2026-04-24 — "Clean Uninstall"
|
|
10
|
+
|
|
11
|
+
**Patch release fixing the orphaned-hooks bug reported by @freenow82 within
|
|
12
|
+
hours of 3.0.0 going live on npm. No feature changes — this release is
|
|
13
|
+
purely about not leaving users stranded when they uninstall.**
|
|
14
|
+
|
|
15
|
+
### The bug (what 3.0.0 shipped with)
|
|
16
|
+
|
|
17
|
+
`npm uninstall -g engramx` removed the binary from PATH but left the hook
|
|
18
|
+
entries in `~/.claude/settings.json` pointing at a `engram intercept`
|
|
19
|
+
command that no longer existed. Claude Code fires those hooks on every
|
|
20
|
+
tool call — the hook commands failed with ENOENT — and user-visible
|
|
21
|
+
behaviour was "Claude Code stopped executing anything." Recovery required
|
|
22
|
+
reinstalling engramx just to run `engram uninstall-hook` before
|
|
23
|
+
uninstalling again.
|
|
24
|
+
|
|
25
|
+
That is a bad experience. Sorry to anyone who hit it.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`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.
|
|
30
|
+
- **`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`.
|
|
31
|
+
- **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()`.
|
|
32
|
+
- Both scripts included in the `files` allowlist of `package.json` so they ship in the tarball.
|
|
33
|
+
|
|
34
|
+
### For users still stranded on 3.0.0
|
|
35
|
+
|
|
36
|
+
If you ran `npm uninstall -g engramx` before this patch shipped and Claude Code is still broken, you have two paths:
|
|
37
|
+
|
|
38
|
+
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`.
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
### Tests
|
|
42
|
+
|
|
43
|
+
- 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.
|
|
44
|
+
|
|
45
|
+
### Thanks
|
|
46
|
+
|
|
47
|
+
[@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.
|
|
48
|
+
|
|
9
49
|
## [3.0.0] — 2026-04-24 — "Spine"
|
|
10
50
|
|
|
11
51
|
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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"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
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"lint": "tsc --noEmit",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"bench": "tsx bench/runner.ts",
|
|
28
|
-
"stress": "tsx bench/stress-test.ts"
|
|
28
|
+
"stress": "tsx bench/stress-test.ts",
|
|
29
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
30
|
+
"preuninstall": "node scripts/preuninstall.mjs"
|
|
29
31
|
},
|
|
30
32
|
"keywords": [
|
|
31
33
|
"structural-code-graph",
|
|
@@ -51,6 +53,8 @@
|
|
|
51
53
|
},
|
|
52
54
|
"files": [
|
|
53
55
|
"dist",
|
|
56
|
+
"scripts/preuninstall.mjs",
|
|
57
|
+
"scripts/postinstall.mjs",
|
|
54
58
|
"LICENSE",
|
|
55
59
|
"README.md",
|
|
56
60
|
"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);
|