memtrace 0.6.20 → 0.6.30
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/bin/memtrace.js +67 -2
- package/install.js +167 -32
- package/installer/dist/transformers/claude.js +9 -5
- package/installer/dist/transformers/codex.js +85 -11
- package/lib/claude-integration.js +7 -1
- package/lib/install-consent.js +202 -0
- package/lib/install-manifest.js +138 -0
- package/package.json +7 -7
- package/uninstall.js +296 -5
package/bin/memtrace.js
CHANGED
|
@@ -36,6 +36,20 @@ const args = process.argv.slice(2);
|
|
|
36
36
|
// upgrade completes, we shell out to `memtrace <rest>` via PATH —
|
|
37
37
|
// which now resolves to the freshly-installed shim, not this old one.
|
|
38
38
|
|
|
39
|
+
// ── `memtrace setup` — (re)run the integration phases without npm ───────────
|
|
40
|
+
// The consent layer (memtrace-public #25) can defer configuration when
|
|
41
|
+
// `npm install -g memtrace` runs without a consent signal. This command is
|
|
42
|
+
// the documented way to complete (or re-run) setup afterwards. Flags:
|
|
43
|
+
// --yes/-y, --project-only, --no-hooks (see lib/install-consent.js).
|
|
44
|
+
if (args[0] === "setup") {
|
|
45
|
+
const result = spawnSync(
|
|
46
|
+
process.execPath,
|
|
47
|
+
[path.join(__dirname, "..", "install.js"), ...args.slice(1)],
|
|
48
|
+
{ stdio: "inherit" }
|
|
49
|
+
);
|
|
50
|
+
process.exit(result.status ?? 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
|
|
40
54
|
// `npm.cmd` on win32 needs `shell: true` since the CVE-2024-27980
|
|
41
55
|
// mitigation in Node 18.20+ / 20.12+ / 21.7+. The helpers in
|
|
@@ -43,6 +57,56 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
|
|
|
43
57
|
const npmCmd = platformBinary("npm", process.platform);
|
|
44
58
|
const memtraceCmd = platformBinary("memtrace", process.platform);
|
|
45
59
|
|
|
60
|
+
// ── Consent gate (memtrace-public #25) ─────────────────────────────────
|
|
61
|
+
// The npm postinstall hook that performs the actual config mutation runs
|
|
62
|
+
// non-interactively, so THIS process — the one that owns the TTY — asks
|
|
63
|
+
// for consent and forwards the grant (plus scoping flags) via env vars.
|
|
64
|
+
// A previously consented install (manifest / MEMTRACE.md / managed hooks
|
|
65
|
+
// present) counts as standing consent so upgrades stay friction-free.
|
|
66
|
+
const consent = require("../lib/install-consent");
|
|
67
|
+
const flags = consent.parseInstallFlags(args.slice(1));
|
|
68
|
+
const home = consent.resolveHome();
|
|
69
|
+
const consentEnv = {};
|
|
70
|
+
if (flags.projectOnly) consentEnv.MEMTRACE_INSTALL_PROJECT_ONLY = "1";
|
|
71
|
+
if (flags.noHooks) consentEnv.MEMTRACE_INSTALL_NO_HOOKS = "1";
|
|
72
|
+
|
|
73
|
+
const decision = consent.decideConsent({
|
|
74
|
+
yes: flags.yes,
|
|
75
|
+
isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
76
|
+
isNpmLifecycle: false,
|
|
77
|
+
hasPriorInstall: consent.detectPriorInstall(home),
|
|
78
|
+
});
|
|
79
|
+
const mutations = consent.buildMutationList({
|
|
80
|
+
home,
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
projectOnly: flags.projectOnly,
|
|
83
|
+
noHooks: flags.noHooks,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (decision === "abort") {
|
|
87
|
+
console.error(consent.formatMutationList(mutations, flags));
|
|
88
|
+
console.error("");
|
|
89
|
+
console.error("memtrace install: refusing to modify user configuration without consent in a non-interactive session.");
|
|
90
|
+
console.error("memtrace install: re-run with --yes (or -y) to approve these changes.");
|
|
91
|
+
process.exit(consent.EXIT_NO_CONSENT);
|
|
92
|
+
}
|
|
93
|
+
if (decision === "prompt") {
|
|
94
|
+
console.log(consent.formatMutationList(mutations, flags));
|
|
95
|
+
// Synchronous prompt: the remainder of this script is top-level code
|
|
96
|
+
// and would otherwise fall through to the Rust-binary delegation
|
|
97
|
+
// while an async prompt was still pending.
|
|
98
|
+
const ok = consent.promptYesNoSync("\nProceed with these changes? [y/N] ");
|
|
99
|
+
if (!ok) {
|
|
100
|
+
console.log("memtrace install: aborted — no files were modified. Re-run with --yes to skip this prompt.");
|
|
101
|
+
process.exit(consent.EXIT_NO_CONSENT);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
consentEnv.MEMTRACE_INSTALL_YES = "1";
|
|
105
|
+
runInstallBranch(npmCmd, memtraceCmd, flags.rest, consentEnv);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runInstallBranch(npmCmd, memtraceCmd, rest, consentEnv) {
|
|
109
|
+
|
|
46
110
|
// ── Pre-commit hook drift check (agent-precommit-optin, 2026-05-08) ──
|
|
47
111
|
// `memtrace install` is the natural place a user re-runs after an
|
|
48
112
|
// upgrade. If a prior version installed the pre-commit hook (now
|
|
@@ -72,7 +136,9 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
|
|
|
72
136
|
["install", "-g", "memtrace@latest"],
|
|
73
137
|
spawnOptionsForPlatform(process.platform, {
|
|
74
138
|
stdio: "inherit",
|
|
75
|
-
|
|
139
|
+
// consentEnv carries MEMTRACE_INSTALL_YES / _PROJECT_ONLY / _NO_HOOKS
|
|
140
|
+
// through npm into the postinstall hook (memtrace-public #25).
|
|
141
|
+
env: { ...process.env, MEMTRACE_INSTALL_PARENT: "1", ...consentEnv },
|
|
76
142
|
})
|
|
77
143
|
);
|
|
78
144
|
|
|
@@ -88,7 +154,6 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
|
|
|
88
154
|
process.exit(installResult.status ?? 1);
|
|
89
155
|
}
|
|
90
156
|
|
|
91
|
-
const rest = args.slice(1);
|
|
92
157
|
if (rest.length === 0) {
|
|
93
158
|
process.stdout.write("memtrace: latest installed.\n");
|
|
94
159
|
process.exit(0);
|
package/install.js
CHANGED
|
@@ -236,45 +236,87 @@ function selfHealPlatformPackage() {
|
|
|
236
236
|
return true;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
// ──
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
239
|
+
// ── Integration phases (post-consent) ───────────────────────────────────────
|
|
240
|
+
//
|
|
241
|
+
// Everything below this line mutates user configuration and therefore only
|
|
242
|
+
// runs AFTER the consent gate in `main()` (memtrace-public #25). The phases
|
|
243
|
+
// are identical to the pre-consent-era behavior; the gate, the scoping
|
|
244
|
+
// flags, and the backup manifest are the only additions.
|
|
245
|
+
//
|
|
246
|
+
// flags: { yes, projectOnly, noHooks } — see lib/install-consent.js.
|
|
247
|
+
function runIntegrationPhases({ home, flags }) {
|
|
248
|
+
const manifestLib = require("./lib/install-manifest");
|
|
249
|
+
const claudeDir = path.join(home, ".claude");
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
// Phase 0 — snapshot every user-global file we may mutate BEFORE any
|
|
252
|
+
// mutation, so uninstall can restore pre-install bytes. --project-only
|
|
253
|
+
// touches nothing under home, so there is nothing to back up.
|
|
254
|
+
let snapshot = null;
|
|
255
|
+
if (!flags.projectOnly) {
|
|
256
|
+
try {
|
|
257
|
+
snapshot = manifestLib.snapshotFiles(home, [
|
|
258
|
+
{
|
|
259
|
+
path: path.join(claudeDir, "settings.json"),
|
|
260
|
+
owned: {
|
|
261
|
+
kind: "json-keys",
|
|
262
|
+
keys: [
|
|
263
|
+
"hooks.* entries with _managed_by=memtrace (UserPromptSubmit, PostToolUse)",
|
|
264
|
+
"hooks.PreToolUse entries whose command contains 'route --hook' (Rail)",
|
|
265
|
+
"mcpServers.memtrace",
|
|
266
|
+
"enabledPlugins[\"memtrace-skills@memtrace\"]",
|
|
267
|
+
"extraKnownMarketplaces.memtrace (+ legacy marketplace containers)",
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
path: path.join(claudeDir, "CLAUDE.md"),
|
|
273
|
+
owned: { kind: "block", markers: ["BEGIN MEMTRACE BLOCK", "END MEMTRACE BLOCK"] },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
path: path.join(claudeDir, "MEMTRACE.md"),
|
|
277
|
+
owned: { kind: "file" },
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.warn(`memtrace: failed to write pre-install backups: ${e.message}`);
|
|
251
282
|
}
|
|
252
|
-
ensureExecutableBits(binaryPath);
|
|
253
|
-
console.log(`memtrace: binary installed at ${binaryPath}`);
|
|
254
|
-
} catch (e) {
|
|
255
|
-
console.warn(`memtrace: ${e.message}`);
|
|
256
283
|
}
|
|
257
284
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
// Phase 1 — delegate to compiled installer (skills + MCP + plugin).
|
|
286
|
+
// default: global, all agents, non-interactive
|
|
287
|
+
// --project-only: local scope, Claude only — writes <cwd>/.claude/skills
|
|
288
|
+
// and <cwd>/.mcp.json, never a home path
|
|
289
|
+
// --no-hooks: MEMTRACE_INSTALL_NO_HOOKS=1 tells the bundled
|
|
290
|
+
// installer to skip the Rail PreToolUse hook
|
|
291
|
+
if (process.env.MEMTRACE_INSTALL_SKIP_BUNDLED !== "1") {
|
|
292
|
+
const bundled = path.join(__dirname, "installer", "dist", "index.js");
|
|
293
|
+
if (fs.existsSync(bundled)) {
|
|
294
|
+
const instArgs = ["install", "--yes"];
|
|
295
|
+
if (flags.projectOnly) instArgs.push("--local", "--only", "claude");
|
|
296
|
+
const childEnv = { ...process.env, MEMTRACE_POSTINSTALL: "1" };
|
|
297
|
+
if (flags.noHooks) childEnv.MEMTRACE_INSTALL_NO_HOOKS = "1";
|
|
298
|
+
const result = spawnSync(process.execPath, [bundled, ...instArgs], {
|
|
299
|
+
stdio: "inherit",
|
|
300
|
+
env: childEnv,
|
|
301
|
+
});
|
|
302
|
+
if (result.status !== 0) {
|
|
303
|
+
console.warn(`memtrace: installer exited with code ${result.status}; continuing.`);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
console.warn("memtrace: bundled installer not found at installer/dist/index.js");
|
|
307
|
+
console.warn("memtrace: run 'memtrace install' manually after the package is built.");
|
|
267
308
|
}
|
|
268
|
-
} else {
|
|
269
|
-
console.warn("memtrace: bundled installer not found at installer/dist/index.js");
|
|
270
|
-
console.warn("memtrace: run 'memtrace install' manually after the package is built.");
|
|
271
309
|
}
|
|
272
310
|
|
|
273
|
-
//
|
|
311
|
+
// Phases 2+ are user-global by definition; --project-only stops here.
|
|
312
|
+
if (flags.projectOnly) return;
|
|
313
|
+
|
|
314
|
+
// Phase 2 — Claude Code adoption layer (v0.3.35+):
|
|
274
315
|
// - write ~/.claude/MEMTRACE.md (sidecar directive we own)
|
|
275
316
|
// - add a breadcrumb block to ~/.claude/CLAUDE.md if it exists
|
|
276
317
|
// - register UserPromptSubmit + PostToolUse hooks in
|
|
277
318
|
// ~/.claude/settings.json under `_managed_by: "memtrace"`
|
|
319
|
+
// (skipped entirely under --no-hooks)
|
|
278
320
|
//
|
|
279
321
|
// This layer is what makes Claude Code consistently reach for
|
|
280
322
|
// Memtrace MCP tools FIRST instead of falling back to Read/Grep
|
|
@@ -292,8 +334,7 @@ if (require.main === module) {
|
|
|
292
334
|
}
|
|
293
335
|
}
|
|
294
336
|
}
|
|
295
|
-
const
|
|
296
|
-
const summary = installToFs({ claudeDir, hooksDir });
|
|
337
|
+
const summary = installToFs({ claudeDir, hooksDir, noHooks: flags.noHooks });
|
|
297
338
|
if (summary.wrote.length > 0) {
|
|
298
339
|
console.log(
|
|
299
340
|
`memtrace: configured Claude Code integration (${summary.wrote.length} file(s) updated)`
|
|
@@ -303,9 +344,10 @@ if (require.main === module) {
|
|
|
303
344
|
console.warn(`memtrace: Claude Code integration setup skipped: ${e.message}`);
|
|
304
345
|
}
|
|
305
346
|
|
|
306
|
-
//
|
|
347
|
+
// Phase 3 — persist uninstall script to ~/.memtrace/ (survives
|
|
348
|
+
// `npm uninstall -g`, which skips preuninstall for global packages).
|
|
307
349
|
try {
|
|
308
|
-
const memtraceDir = path.join(
|
|
350
|
+
const memtraceDir = path.join(home, ".memtrace");
|
|
309
351
|
fs.mkdirSync(memtraceDir, { recursive: true });
|
|
310
352
|
fs.copyFileSync(
|
|
311
353
|
path.join(__dirname, "uninstall.js"),
|
|
@@ -316,11 +358,104 @@ if (require.main === module) {
|
|
|
316
358
|
console.warn(`memtrace: failed to persist uninstall script: ${e.message}`);
|
|
317
359
|
}
|
|
318
360
|
|
|
361
|
+
// Phase 4 — record post-install hashes + write the manifest that
|
|
362
|
+
// `memtrace uninstall` restores from.
|
|
363
|
+
if (snapshot) {
|
|
364
|
+
try {
|
|
365
|
+
manifestLib.finalizeManifest(home, snapshot, {
|
|
366
|
+
createdPaths: [
|
|
367
|
+
path.join(claudeDir, "plugins", "cache", "memtrace"),
|
|
368
|
+
path.join(home, ".memtrace", "uninstall.js"),
|
|
369
|
+
],
|
|
370
|
+
flags: { projectOnly: flags.projectOnly, noHooks: flags.noHooks },
|
|
371
|
+
version: require("./package.json").version,
|
|
372
|
+
});
|
|
373
|
+
console.log(`memtrace: backups + install manifest written to ${manifestLib.backupRoot(home)}`);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.warn(`memtrace: failed to write install manifest: ${e.message}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Main postinstall / `memtrace setup` entry ────────────────────────────────
|
|
381
|
+
|
|
382
|
+
async function main() {
|
|
383
|
+
const consent = require("./lib/install-consent");
|
|
384
|
+
|
|
385
|
+
// Step 1 — verify the platform binary exists + is executable. This is
|
|
386
|
+
// package-internal provisioning (never touches user config), so it runs
|
|
387
|
+
// before the consent gate: even a "defer" install must leave a working
|
|
388
|
+
// binary behind. MEMTRACE_INSTALL_SKIP_SELFHEAL=1 short-circuits it in
|
|
389
|
+
// tests (it can hit the network via `npm install`).
|
|
390
|
+
if (process.env.MEMTRACE_INSTALL_SKIP_SELFHEAL !== "1") {
|
|
391
|
+
selfHealPlatformPackage();
|
|
392
|
+
try {
|
|
393
|
+
const binaryPath = getBinaryPath();
|
|
394
|
+
if (!fs.existsSync(binaryPath)) {
|
|
395
|
+
throw new Error(`Binary not found at: ${binaryPath}`);
|
|
396
|
+
}
|
|
397
|
+
ensureExecutableBits(binaryPath);
|
|
398
|
+
console.log(`memtrace: binary installed at ${binaryPath}`);
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.warn(`memtrace: ${e.message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Step 2 — consent gate (memtrace-public #25). Print the full mutation
|
|
405
|
+
// list and require explicit approval before touching ANY user config.
|
|
406
|
+
const flags = consent.parseInstallFlags(process.argv.slice(2));
|
|
407
|
+
const home = consent.resolveHome();
|
|
408
|
+
const mutations = consent.buildMutationList({
|
|
409
|
+
home,
|
|
410
|
+
cwd: process.cwd(),
|
|
411
|
+
projectOnly: flags.projectOnly,
|
|
412
|
+
noHooks: flags.noHooks,
|
|
413
|
+
});
|
|
414
|
+
const decision = consent.decideConsent({
|
|
415
|
+
yes: flags.yes,
|
|
416
|
+
isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
417
|
+
isNpmLifecycle: process.env.npm_lifecycle_event === "postinstall",
|
|
418
|
+
hasPriorInstall: consent.detectPriorInstall(home),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (decision === "prompt") {
|
|
422
|
+
console.log(consent.formatMutationList(mutations, flags));
|
|
423
|
+
const ok = await consent.promptYesNo("\nProceed with these changes? [y/N] ");
|
|
424
|
+
if (!ok) {
|
|
425
|
+
console.log("memtrace: aborted — no files were modified. Re-run with --yes to skip this prompt.");
|
|
426
|
+
process.exit(consent.EXIT_NO_CONSENT);
|
|
427
|
+
}
|
|
428
|
+
} else if (decision === "abort") {
|
|
429
|
+
console.error(consent.formatMutationList(mutations, flags));
|
|
430
|
+
console.error("");
|
|
431
|
+
console.error("memtrace: refusing to modify user configuration without consent in a non-interactive session.");
|
|
432
|
+
console.error("memtrace: re-run with --yes (or set MEMTRACE_INSTALL_YES=1) to approve these changes.");
|
|
433
|
+
process.exit(consent.EXIT_NO_CONSENT);
|
|
434
|
+
} else if (decision === "defer") {
|
|
435
|
+
// npm postinstall without a consent signal: never silently mutate, but
|
|
436
|
+
// never fail the package install either (a non-zero lifecycle exit
|
|
437
|
+
// would abort `npm install -g memtrace` for every fresh CI user).
|
|
438
|
+
console.log(consent.formatMutationList(mutations, flags));
|
|
439
|
+
console.log("");
|
|
440
|
+
console.log("memtrace: skipped agent/Claude Code configuration — no consent signal in npm lifecycle.");
|
|
441
|
+
console.log("memtrace: run `memtrace setup --yes` (or set MEMTRACE_INSTALL_YES=1 before npm install) to complete setup.");
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
runIntegrationPhases({ home, flags });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (require.main === module) {
|
|
449
|
+
main().catch((e) => {
|
|
450
|
+
console.error(`memtrace: install failed: ${e.message}`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
});
|
|
319
453
|
}
|
|
320
454
|
|
|
321
455
|
module.exports = {
|
|
322
456
|
getBinaryPath,
|
|
323
457
|
ensureExecutableBits,
|
|
458
|
+
runIntegrationPhases,
|
|
324
459
|
selfHealPlatformPackage,
|
|
325
460
|
buildSelfHealEnv,
|
|
326
461
|
buildSelfHealInstallArgs,
|
|
@@ -430,11 +430,15 @@ async function registerMcpServer(memtraceBinaryPath) {
|
|
|
430
430
|
// hook defaults to observe mode (zero behavior change) until the user opts
|
|
431
431
|
// into MEMTRACE_RAIL=nudge|rail|strict. Failure here must not block MCP
|
|
432
432
|
// registration.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
433
|
+
// `memtrace install --no-hooks` (memtrace-public #25) propagates here via
|
|
434
|
+
// MEMTRACE_INSTALL_NO_HOOKS=1 — skip ALL settings.json hook registration.
|
|
435
|
+
if (process.env.MEMTRACE_INSTALL_NO_HOOKS !== '1') {
|
|
436
|
+
try {
|
|
437
|
+
registerRailHookInSettingsAt(settingsFile, memtraceBinaryPath);
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
console.warn(`memtrace: failed to register Rail hook: ${e.message}`);
|
|
441
|
+
}
|
|
438
442
|
}
|
|
439
443
|
// Strategy 1 (preferred): claude CLI's own add-json path
|
|
440
444
|
const viaCli = await tryMcpAddJson(memtraceBinaryPath);
|
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { commandExists, execCommand } from '../utils.js';
|
|
5
5
|
import { registerSettingsHook, removeSettingsHook, CODEX_MATCHER, } from './rail-hooks.js';
|
|
6
6
|
const MCP_SERVER_NAME = 'memtrace';
|
|
7
|
+
const CODEX_MCP_SHIM_NAME = 'memtrace-mcp-codex-shim.cjs';
|
|
7
8
|
function codexRailHooksPath(ctx) {
|
|
8
9
|
const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
|
|
9
10
|
return path.join(base, 'hooks.json');
|
|
@@ -37,6 +38,82 @@ function writeTextAtomic(filePath, content) {
|
|
|
37
38
|
fs.writeFileSync(tmpPath, content);
|
|
38
39
|
fs.renameSync(tmpPath, filePath);
|
|
39
40
|
}
|
|
41
|
+
function codexMcpShimSource() {
|
|
42
|
+
return `#!/usr/bin/env node
|
|
43
|
+
const { spawn } = require('node:child_process');
|
|
44
|
+
|
|
45
|
+
const binary = process.argv[2] || 'memtrace';
|
|
46
|
+
const child = spawn(binary, ['mcp'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
47
|
+
|
|
48
|
+
process.stdin.pipe(child.stdin);
|
|
49
|
+
child.stderr.pipe(process.stderr);
|
|
50
|
+
|
|
51
|
+
function stripAnnotations(line) {
|
|
52
|
+
let message;
|
|
53
|
+
try {
|
|
54
|
+
message = JSON.parse(line);
|
|
55
|
+
} catch {
|
|
56
|
+
return line;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const content = message && message.result && message.result.content;
|
|
60
|
+
if (!Array.isArray(content)) return line;
|
|
61
|
+
|
|
62
|
+
const assistantItems = [];
|
|
63
|
+
const fallbackItems = [];
|
|
64
|
+
for (const item of content) {
|
|
65
|
+
if (!item || typeof item !== 'object') continue;
|
|
66
|
+
const clean = { ...item };
|
|
67
|
+
const annotations = clean.annotations;
|
|
68
|
+
delete clean.annotations;
|
|
69
|
+
fallbackItems.push(clean);
|
|
70
|
+
const audience = annotations && annotations.audience;
|
|
71
|
+
if (Array.isArray(audience) && audience.includes('assistant')) {
|
|
72
|
+
assistantItems.push(clean);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
message.result.content = assistantItems.length > 0 ? assistantItems : fallbackItems;
|
|
77
|
+
return JSON.stringify(message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let buffer = '';
|
|
81
|
+
child.stdout.on('data', (chunk) => {
|
|
82
|
+
buffer += chunk.toString('utf8');
|
|
83
|
+
let index = buffer.indexOf('\\n');
|
|
84
|
+
while (index !== -1) {
|
|
85
|
+
const line = buffer.slice(0, index);
|
|
86
|
+
buffer = buffer.slice(index + 1);
|
|
87
|
+
process.stdout.write(stripAnnotations(line) + '\\n');
|
|
88
|
+
index = buffer.indexOf('\\n');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
child.stdout.on('end', () => {
|
|
93
|
+
if (buffer.length > 0) process.stdout.write(stripAnnotations(buffer));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.on('exit', (code, signal) => {
|
|
97
|
+
if (signal) process.kill(process.pid, signal);
|
|
98
|
+
process.exit(code ?? 0);
|
|
99
|
+
});
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
function codexMcpShimPath(configFile) {
|
|
103
|
+
return path.join(path.dirname(configFile), CODEX_MCP_SHIM_NAME);
|
|
104
|
+
}
|
|
105
|
+
function writeCodexMcpShim(configFile) {
|
|
106
|
+
const shimPath = codexMcpShimPath(configFile);
|
|
107
|
+
writeTextAtomic(shimPath, codexMcpShimSource());
|
|
108
|
+
try {
|
|
109
|
+
fs.chmodSync(shimPath, 0o755);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Windows and locked-down filesystems may reject chmod; Codex invokes it
|
|
113
|
+
// through Node, so executable bits are a convenience, not a requirement.
|
|
114
|
+
}
|
|
115
|
+
return shimPath;
|
|
116
|
+
}
|
|
40
117
|
function isMemtraceMcpTable(tableName) {
|
|
41
118
|
return tableName === `mcp_servers.${MCP_SERVER_NAME}`
|
|
42
119
|
|| tableName === `mcp_servers."${MCP_SERVER_NAME}"`
|
|
@@ -119,10 +196,11 @@ export function registerCodexMcpAt(configFile, binary) {
|
|
|
119
196
|
// after the freshly written parent block for readability.
|
|
120
197
|
const stripped = stripCodexMcpServer(existing);
|
|
121
198
|
const { rest, envBlock } = extractMemtraceEnvBlock(stripped);
|
|
199
|
+
const shimPath = writeCodexMcpShim(configFile);
|
|
122
200
|
const block = [
|
|
123
201
|
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
124
|
-
`command = ${tomlString(
|
|
125
|
-
`args = [${tomlString('
|
|
202
|
+
`command = ${tomlString(process.execPath)}`,
|
|
203
|
+
`args = [${[shimPath, binary].map(tomlString).join(', ')}]`,
|
|
126
204
|
].join('\n');
|
|
127
205
|
const parts = [];
|
|
128
206
|
if (rest)
|
|
@@ -167,15 +245,11 @@ export const codexTransformer = {
|
|
|
167
245
|
const warnings = [];
|
|
168
246
|
const mcpConfigPath = codexConfigPath(ctx);
|
|
169
247
|
if (!ctx.skipMcp) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
mcpRegistered = true;
|
|
176
|
-
if (ctx.scope === 'global') {
|
|
177
|
-
warnings.push('codex CLI MCP registration was unavailable; wrote ~/.codex/config.toml directly.');
|
|
178
|
-
}
|
|
248
|
+
const cliRegistered = ctx.scope === 'global' && await tryCodexMcpAdd(ctx.memtraceBinary);
|
|
249
|
+
registerCodexMcpAt(mcpConfigPath, ctx.memtraceBinary);
|
|
250
|
+
mcpRegistered = true;
|
|
251
|
+
if (ctx.scope === 'global' && !cliRegistered) {
|
|
252
|
+
warnings.push('codex CLI MCP registration was unavailable; wrote ~/.codex/config.toml directly.');
|
|
179
253
|
}
|
|
180
254
|
}
|
|
181
255
|
// ─── memtrace-rail ─── Wire the discovery hook into .codex/hooks.json
|
|
@@ -339,6 +339,8 @@ function removeSettingsHooks(settings) {
|
|
|
339
339
|
* @param {object} [opts]
|
|
340
340
|
* @param {string} [opts.claudeDir] default `~/.claude`
|
|
341
341
|
* @param {string} [opts.hooksDir] absolute dir where hook scripts live
|
|
342
|
+
* @param {boolean} [opts.noHooks] skip settings.json hook registration
|
|
343
|
+
* entirely (`memtrace install --no-hooks`)
|
|
342
344
|
* @returns {object} summary `{ wrote: [...], skipped: [...] }`
|
|
343
345
|
*/
|
|
344
346
|
function installToFs(opts = {}) {
|
|
@@ -372,8 +374,12 @@ function installToFs(opts = {}) {
|
|
|
372
374
|
summary.skipped.push(claudeMdPath + " (does not exist; not creating)");
|
|
373
375
|
}
|
|
374
376
|
|
|
375
|
-
// 3. Register hooks in settings.json.
|
|
377
|
+
// 3. Register hooks in settings.json (skipped under --no-hooks).
|
|
376
378
|
const settingsPath = path.join(claudeDir, "settings.json");
|
|
379
|
+
if (opts.noHooks) {
|
|
380
|
+
summary.skipped.push(settingsPath + " (--no-hooks)");
|
|
381
|
+
return summary;
|
|
382
|
+
}
|
|
377
383
|
let settings = {};
|
|
378
384
|
if (fs.existsSync(settingsPath)) {
|
|
379
385
|
try {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Consent + scoping layer for `memtrace install` / npm postinstall.
|
|
4
|
+
//
|
|
5
|
+
// memtrace-public issue #25: `memtrace install` mutated user-global
|
|
6
|
+
// Claude Code configuration (~/.claude/settings.json, ~/.claude/CLAUDE.md,
|
|
7
|
+
// ~/.claude/MEMTRACE.md, MCP/plugin/marketplace registries) with no
|
|
8
|
+
// consent prompt, no documented mutation list, and no opt-out flags —
|
|
9
|
+
// silently diverging dotfiles managed by chezmoi / Nix home-manager.
|
|
10
|
+
//
|
|
11
|
+
// This module is the single source of truth for:
|
|
12
|
+
// 1. The full mutation list (what install will touch, with paths)
|
|
13
|
+
// 2. Flag parsing (--yes/-y, --project-only, --no-hooks)
|
|
14
|
+
// 3. The consent decision table (pure, unit-tested)
|
|
15
|
+
//
|
|
16
|
+
// All side effects stay in install.js / uninstall.js / bin/memtrace.js.
|
|
17
|
+
//
|
|
18
|
+
// Environment knobs (set by the bin shim or the Rust `memtrace install`
|
|
19
|
+
// parent after IT obtained consent on the TTY it owns, or by CI):
|
|
20
|
+
// MEMTRACE_INSTALL_YES=1 — consent granted (same as --yes)
|
|
21
|
+
// MEMTRACE_ASSUME_YES=1 — alias for the above
|
|
22
|
+
// MEMTRACE_INSTALL_PROJECT_ONLY=1 — same as --project-only
|
|
23
|
+
// MEMTRACE_INSTALL_NO_HOOKS=1 — same as --no-hooks
|
|
24
|
+
// MEMTRACE_INSTALL_HOME=<dir> — home-dir override (tests)
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const path = require("path");
|
|
28
|
+
const os = require("os");
|
|
29
|
+
const readline = require("readline");
|
|
30
|
+
|
|
31
|
+
/** Exit code used whenever install aborts without consent. */
|
|
32
|
+
const EXIT_NO_CONSENT = 2;
|
|
33
|
+
|
|
34
|
+
function resolveHome(env = process.env) {
|
|
35
|
+
return env.MEMTRACE_INSTALL_HOME || os.homedir();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse install flags out of an argv slice. Unrecognised arguments are
|
|
40
|
+
* preserved in `rest` (the bin shim chains them: `memtrace install start`).
|
|
41
|
+
* Env vars provide the non-interactive channel through `npm install -g`.
|
|
42
|
+
*/
|
|
43
|
+
function parseInstallFlags(argv = [], env = process.env) {
|
|
44
|
+
const flags = {
|
|
45
|
+
yes: env.MEMTRACE_INSTALL_YES === "1" || env.MEMTRACE_ASSUME_YES === "1",
|
|
46
|
+
projectOnly: env.MEMTRACE_INSTALL_PROJECT_ONLY === "1",
|
|
47
|
+
noHooks: env.MEMTRACE_INSTALL_NO_HOOKS === "1",
|
|
48
|
+
rest: [],
|
|
49
|
+
};
|
|
50
|
+
for (const a of argv) {
|
|
51
|
+
if (a === "--yes" || a === "-y") flags.yes = true;
|
|
52
|
+
else if (a === "--project-only") flags.projectOnly = true;
|
|
53
|
+
else if (a === "--no-hooks") flags.noHooks = true;
|
|
54
|
+
else flags.rest.push(a);
|
|
55
|
+
}
|
|
56
|
+
return flags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The complete list of files/registries `memtrace install` may mutate,
|
|
61
|
+
* as `{ path, change }` entries. Pure. This is what gets printed before
|
|
62
|
+
* any mutation happens and what docs/INSTALL-FOOTPRINT.md documents.
|
|
63
|
+
*/
|
|
64
|
+
function buildMutationList({ home, cwd, projectOnly = false, noHooks = false }) {
|
|
65
|
+
const list = [];
|
|
66
|
+
if (projectOnly) {
|
|
67
|
+
list.push(
|
|
68
|
+
{ path: path.join(cwd, ".claude", "skills", "memtrace-*"), change: "create/replace Memtrace skill directories (project scope)" },
|
|
69
|
+
{ path: path.join(cwd, ".mcp.json"), change: "merge-add mcpServers.memtrace entry (project scope)" },
|
|
70
|
+
);
|
|
71
|
+
return list;
|
|
72
|
+
}
|
|
73
|
+
const claude = path.join(home, ".claude");
|
|
74
|
+
list.push(
|
|
75
|
+
{ path: path.join(claude, "MEMTRACE.md"), change: "create/overwrite (file is fully memtrace-owned)" },
|
|
76
|
+
{ path: path.join(claude, "CLAUDE.md"), change: "append one marker-delimited breadcrumb block (only if the file already exists)" },
|
|
77
|
+
{
|
|
78
|
+
path: path.join(claude, "settings.json"),
|
|
79
|
+
change: noHooks
|
|
80
|
+
? "merge-add mcpServers.memtrace, enabledPlugins[\"memtrace-skills@memtrace\"], extraKnownMarketplaces.memtrace (hooks SKIPPED via --no-hooks)"
|
|
81
|
+
: "merge-add hooks (UserPromptSubmit, PostToolUse[mcp__memtrace__.*], PreToolUse Rail[Grep|Glob|Bash]), mcpServers.memtrace, enabledPlugins[\"memtrace-skills@memtrace\"], extraKnownMarketplaces.memtrace",
|
|
82
|
+
},
|
|
83
|
+
{ path: path.join(claude, "plugins", "cache", "memtrace", "memtrace-skills") + path.sep, change: "create/replace plugin cache (skills payload)" },
|
|
84
|
+
{ path: path.join(claude, "plugins", "marketplaces", "{memtrace,syncable-dev-memtrace*}") + path.sep, change: "remove stale + re-clone marketplace caches (via `claude` CLI when available)" },
|
|
85
|
+
{ path: path.join(claude, "plugins", "installed_plugins.json"), change: "plugin registry entry (written by the `claude` CLI when available)" },
|
|
86
|
+
{ path: path.join(home, ".claude.json"), change: "possible memtrace MCP entry (written by `claude mcp add-json --scope user` when the `claude` CLI is available)" },
|
|
87
|
+
{ path: path.join(home, ".memtrace") + path.sep, change: "create memtrace data dir: persisted uninstall.js + install-backup/ (backups + manifest)" },
|
|
88
|
+
{ path: path.join(home, "{.cursor,.codex,.gemini,.codeium,.hermes,.kiro,.copilot,.config/opencode,VS Code user config}"), change: "skills + memtrace MCP registration for other detected agents (same merge-add pattern)" },
|
|
89
|
+
);
|
|
90
|
+
return list;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatMutationList(list, { projectOnly = false, noHooks = false } = {}) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
lines.push("memtrace install will modify the following files:");
|
|
96
|
+
lines.push("");
|
|
97
|
+
for (const m of list) {
|
|
98
|
+
lines.push(` ${m.path}`);
|
|
99
|
+
lines.push(` ${m.change}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(" Every change is marker-tagged or merge-added; a backup of each");
|
|
103
|
+
lines.push(" pre-existing file plus a manifest is written to ~/.memtrace/install-backup/.");
|
|
104
|
+
lines.push(" `memtrace uninstall` restores from that manifest (surgically removing only");
|
|
105
|
+
lines.push(" memtrace-added blocks if you edited a file after install).");
|
|
106
|
+
lines.push("");
|
|
107
|
+
const flagHints = [];
|
|
108
|
+
if (!projectOnly) flagHints.push("--project-only (never touch ~/.claude or any home path)");
|
|
109
|
+
if (!noHooks) flagHints.push("--no-hooks (skip all settings.json hook registration)");
|
|
110
|
+
flagHints.push("--yes / -y (skip this prompt)");
|
|
111
|
+
lines.push(` Flags: ${flagHints.join(", ")}`);
|
|
112
|
+
lines.push(" Full footprint: docs/INSTALL-FOOTPRINT.md (memtrace repo) / https://github.com/syncable-dev/memtrace-public");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A previous memtrace install on this machine implies prior consent: a
|
|
118
|
+
* re-run is a refresh of state the user already accepted, so upgrades
|
|
119
|
+
* (`memtrace install`, `npm i -g memtrace@latest`) stay friction-free
|
|
120
|
+
* and behave exactly as before this consent layer existed.
|
|
121
|
+
*/
|
|
122
|
+
function detectPriorInstall(home) {
|
|
123
|
+
try {
|
|
124
|
+
if (fs.existsSync(path.join(home, ".memtrace", "install-backup", "manifest.json"))) return true;
|
|
125
|
+
if (fs.existsSync(path.join(home, ".claude", "MEMTRACE.md"))) return true;
|
|
126
|
+
const settingsPath = path.join(home, ".claude", "settings.json");
|
|
127
|
+
if (fs.existsSync(settingsPath)) {
|
|
128
|
+
const text = fs.readFileSync(settingsPath, "utf8");
|
|
129
|
+
if (text.includes('"_managed_by": "memtrace"') || text.includes('"_managed_by":"memtrace"')) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// unreadable home — treat as fresh install (consent required)
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Pure consent decision table.
|
|
141
|
+
*
|
|
142
|
+
* proceed — consent established (--yes / env / prior consented install)
|
|
143
|
+
* prompt — interactive TTY available: print the list and ask
|
|
144
|
+
* defer — npm lifecycle (postinstall) without consent: print the
|
|
145
|
+
* list, SKIP all user-config mutation, exit 0. Exiting 2
|
|
146
|
+
* here would fail the entire `npm install -g memtrace`
|
|
147
|
+
* (lifecycle scripts propagate), which would break every
|
|
148
|
+
* fresh CI install. "Never silently mutate from a pipe" is
|
|
149
|
+
* honored: nothing is mutated and the skip is loud.
|
|
150
|
+
* abort — any other non-TTY invocation without consent: print the
|
|
151
|
+
* list and exit 2. Never silently mutate from a pipe.
|
|
152
|
+
*/
|
|
153
|
+
function decideConsent({ yes, isTTY, isNpmLifecycle, hasPriorInstall }) {
|
|
154
|
+
if (yes || hasPriorInstall) return "proceed";
|
|
155
|
+
if (isNpmLifecycle) return "defer";
|
|
156
|
+
if (isTTY) return "prompt";
|
|
157
|
+
return "abort";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Synchronous y/N prompt (default No) for callers whose remaining
|
|
162
|
+
* top-level code would otherwise race an async prompt (bin/memtrace.js).
|
|
163
|
+
* Blocking read on fd 0; any read error counts as "no".
|
|
164
|
+
*/
|
|
165
|
+
function promptYesNoSync(question) {
|
|
166
|
+
process.stdout.write(question);
|
|
167
|
+
const buf = Buffer.alloc(256);
|
|
168
|
+
let n = 0;
|
|
169
|
+
for (;;) {
|
|
170
|
+
try {
|
|
171
|
+
n = fs.readSync(0, buf, 0, 256);
|
|
172
|
+
break;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (e.code === "EAGAIN") continue; // non-blocking stdin: retry
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return /^y(es)?$/i.test(buf.toString("utf8", 0, n).trim());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Ask a y/N question on the current TTY. Default: No. */
|
|
182
|
+
function promptYesNo(question) {
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
185
|
+
rl.question(question, (answer) => {
|
|
186
|
+
rl.close();
|
|
187
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
EXIT_NO_CONSENT,
|
|
194
|
+
resolveHome,
|
|
195
|
+
parseInstallFlags,
|
|
196
|
+
buildMutationList,
|
|
197
|
+
formatMutationList,
|
|
198
|
+
detectPriorInstall,
|
|
199
|
+
decideConsent,
|
|
200
|
+
promptYesNo,
|
|
201
|
+
promptYesNoSync,
|
|
202
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Backup + manifest layer for `memtrace install` (memtrace-public #25).
|
|
4
|
+
//
|
|
5
|
+
// Before install mutates any user-global file it snapshots the file's
|
|
6
|
+
// pre-install bytes into ~/.memtrace/install-backup/<timestamp>/ and,
|
|
7
|
+
// after all phases complete, writes a manifest recording for each file:
|
|
8
|
+
//
|
|
9
|
+
// - whether it existed before install
|
|
10
|
+
// - where the backup copy lives
|
|
11
|
+
// - sha256 BEFORE install (preSha256)
|
|
12
|
+
// - sha256 AFTER install completed (postSha256)
|
|
13
|
+
// - exactly which memtrace-owned blocks/keys the file gained
|
|
14
|
+
//
|
|
15
|
+
// `memtrace uninstall` then has two safe paths per file:
|
|
16
|
+
//
|
|
17
|
+
// current sha == postSha256 → file untouched since install:
|
|
18
|
+
// restore the backup byte-for-byte
|
|
19
|
+
// (or delete the file if we created it)
|
|
20
|
+
// current sha != postSha256 → user edited the file after install:
|
|
21
|
+
// surgically remove ONLY the recorded
|
|
22
|
+
// memtrace-owned blocks/keys, preserving
|
|
23
|
+
// every user byte.
|
|
24
|
+
//
|
|
25
|
+
// The manifest lives at ~/.memtrace/install-backup/manifest.json and is
|
|
26
|
+
// renamed to manifest.consumed-<ts>.json after a successful restore so
|
|
27
|
+
// repeated uninstalls are idempotent no-ops.
|
|
28
|
+
|
|
29
|
+
const fs = require("fs");
|
|
30
|
+
const path = require("path");
|
|
31
|
+
const crypto = require("crypto");
|
|
32
|
+
|
|
33
|
+
const MANIFEST_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
function backupRoot(home) {
|
|
36
|
+
return path.join(home, ".memtrace", "install-backup");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function manifestPath(home) {
|
|
40
|
+
return path.join(backupRoot(home), "manifest.json");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sha256OfFile(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
46
|
+
} catch {
|
|
47
|
+
return null; // missing/unreadable
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot the pre-install state of `targets` (absolute paths).
|
|
53
|
+
* Copies each existing file into a timestamped backup dir.
|
|
54
|
+
* Returns `{ stamp, dir, entries }`; entries carry `owned` descriptors
|
|
55
|
+
* so uninstall knows exactly what memtrace added to each file.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} home
|
|
58
|
+
* @param {Array<{path: string, owned: object}>} targets
|
|
59
|
+
*/
|
|
60
|
+
function snapshotFiles(home, targets, now = new Date()) {
|
|
61
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
62
|
+
const dir = path.join(backupRoot(home), stamp);
|
|
63
|
+
const entries = [];
|
|
64
|
+
let idx = 0;
|
|
65
|
+
for (const t of targets) {
|
|
66
|
+
const existed = fs.existsSync(t.path);
|
|
67
|
+
let backupPath = null;
|
|
68
|
+
if (existed) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
backupPath = path.join(dir, `${idx}-${path.basename(t.path)}`);
|
|
71
|
+
fs.copyFileSync(t.path, backupPath);
|
|
72
|
+
}
|
|
73
|
+
entries.push({
|
|
74
|
+
path: t.path,
|
|
75
|
+
owned: t.owned,
|
|
76
|
+
existedBefore: existed,
|
|
77
|
+
backupPath,
|
|
78
|
+
preSha256: existed ? sha256OfFile(t.path) : null,
|
|
79
|
+
postSha256: null, // filled in by finalizeManifest
|
|
80
|
+
});
|
|
81
|
+
idx++;
|
|
82
|
+
}
|
|
83
|
+
return { stamp, dir, entries };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Record post-install hashes and persist the manifest. Call AFTER every
|
|
88
|
+
* install phase has finished mutating files.
|
|
89
|
+
*/
|
|
90
|
+
function finalizeManifest(home, snapshot, { createdPaths = [], flags = {}, version = null } = {}) {
|
|
91
|
+
for (const e of snapshot.entries) {
|
|
92
|
+
e.postSha256 = sha256OfFile(e.path);
|
|
93
|
+
}
|
|
94
|
+
const manifest = {
|
|
95
|
+
version: MANIFEST_VERSION,
|
|
96
|
+
memtraceVersion: version,
|
|
97
|
+
createdAt: new Date().toISOString(),
|
|
98
|
+
backupDir: snapshot.dir,
|
|
99
|
+
flags,
|
|
100
|
+
entries: snapshot.entries,
|
|
101
|
+
// Paths memtrace created wholesale (safe to delete on uninstall).
|
|
102
|
+
createdPaths,
|
|
103
|
+
};
|
|
104
|
+
fs.mkdirSync(backupRoot(home), { recursive: true });
|
|
105
|
+
fs.writeFileSync(manifestPath(home), JSON.stringify(manifest, null, 2) + "\n");
|
|
106
|
+
return manifest;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readManifest(home) {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(fs.readFileSync(manifestPath(home), "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Rename the manifest aside so a second uninstall is a no-op. */
|
|
118
|
+
function consumeManifest(home) {
|
|
119
|
+
const p = manifestPath(home);
|
|
120
|
+
if (!fs.existsSync(p)) return;
|
|
121
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
122
|
+
try {
|
|
123
|
+
fs.renameSync(p, path.join(backupRoot(home), `manifest.consumed-${stamp}.json`));
|
|
124
|
+
} catch {
|
|
125
|
+
try { fs.unlinkSync(p); } catch { /* best-effort */ }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
MANIFEST_VERSION,
|
|
131
|
+
backupRoot,
|
|
132
|
+
manifestPath,
|
|
133
|
+
sha256OfFile,
|
|
134
|
+
snapshotFiles,
|
|
135
|
+
finalizeManifest,
|
|
136
|
+
readManifest,
|
|
137
|
+
consumeManifest,
|
|
138
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrace",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.30",
|
|
4
4
|
"description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"fs-extra": "^11.0.0"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@memtrace/darwin-arm64": "0.6.
|
|
43
|
-
"@memtrace/linux-x64": "0.6.
|
|
44
|
-
"@memtrace/linux-arm64": "0.6.
|
|
45
|
-
"@memtrace/win32-x64": "0.6.
|
|
46
|
-
"@memtrace/linux-x64-noavx2": "0.6.
|
|
47
|
-
"@memtrace/win32-x64-noavx2": "0.6.
|
|
42
|
+
"@memtrace/darwin-arm64": "0.6.30",
|
|
43
|
+
"@memtrace/linux-x64": "0.6.30",
|
|
44
|
+
"@memtrace/linux-arm64": "0.6.30",
|
|
45
|
+
"@memtrace/win32-x64": "0.6.30",
|
|
46
|
+
"@memtrace/linux-x64-noavx2": "0.6.30",
|
|
47
|
+
"@memtrace/win32-x64-noavx2": "0.6.30"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=18"
|
package/uninstall.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const fs = require("fs");
|
|
7
|
+
const crypto = require("crypto");
|
|
7
8
|
const { execSync, spawnSync } = require("child_process");
|
|
8
9
|
|
|
9
10
|
// claude-integration may not be present in old installs running this
|
|
@@ -53,6 +54,235 @@ const SKILL_NAMES = [
|
|
|
53
54
|
"memtrace-refactoring-guide",
|
|
54
55
|
];
|
|
55
56
|
|
|
57
|
+
// ── Manifest-driven restore (memtrace-public #25) ───────────────────────────
|
|
58
|
+
//
|
|
59
|
+
// `memtrace install` writes ~/.memtrace/install-backup/manifest.json with a
|
|
60
|
+
// pre-install backup + pre/post sha256 for every user-global file it mutated.
|
|
61
|
+
// Restore strategy per file:
|
|
62
|
+
//
|
|
63
|
+
// current sha == postSha256 → untouched since install → byte-exact restore
|
|
64
|
+
// of the backup (or delete, if install created
|
|
65
|
+
// the file).
|
|
66
|
+
// current sha != postSha256 → user edited it after install → surgically
|
|
67
|
+
// remove ONLY memtrace-owned blocks/keys,
|
|
68
|
+
// preserving every user byte.
|
|
69
|
+
//
|
|
70
|
+
// Everything here is deliberately self-contained (constants inlined, no
|
|
71
|
+
// require of lib/claude-integration) because this script is persisted to
|
|
72
|
+
// ~/.memtrace/uninstall.js, where lib/ does not exist. That missing-lib
|
|
73
|
+
// fallback was exactly how pre-#25 uninstalls left the CLAUDE.md breadcrumb
|
|
74
|
+
// and the managed hooks behind.
|
|
75
|
+
|
|
76
|
+
const MT_BLOCK_BEGIN =
|
|
77
|
+
"<!-- BEGIN MEMTRACE BLOCK · managed by `memtrace install` · remove with `memtrace uninstall` -->";
|
|
78
|
+
const MT_BLOCK_END = "<!-- END MEMTRACE BLOCK -->";
|
|
79
|
+
const MT_HOOK_MANAGED_TAG = "memtrace";
|
|
80
|
+
const MT_HOOK_SCRIPTS = ["userprompt-claude.sh", "posttool-mcp-telemetry.sh"];
|
|
81
|
+
const MT_RAIL_HOOK_MARKER = "route --hook";
|
|
82
|
+
|
|
83
|
+
function installManifestPath(home = os.homedir()) {
|
|
84
|
+
return path.join(home, ".memtrace", "install-backup", "manifest.json");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readInstallManifest(home = os.homedir()) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(installManifestPath(home), "utf8"));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function consumeInstallManifest(home = os.homedir()) {
|
|
96
|
+
const p = installManifestPath(home);
|
|
97
|
+
if (!fs.existsSync(p)) return;
|
|
98
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
99
|
+
try {
|
|
100
|
+
fs.renameSync(p, path.join(path.dirname(p), `manifest.consumed-${stamp}.json`));
|
|
101
|
+
} catch {
|
|
102
|
+
try { fs.unlinkSync(p); } catch { /* best-effort */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sha256OfFile(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Remove the memtrace breadcrumb block from CLAUDE.md text. Tolerant:
|
|
115
|
+
* a dangling BEGIN without END leaves the text unchanged. */
|
|
116
|
+
function stripBreadcrumbText(text) {
|
|
117
|
+
const beginIdx = text.indexOf(MT_BLOCK_BEGIN);
|
|
118
|
+
if (beginIdx === -1) return text;
|
|
119
|
+
const endIdx = text.indexOf(MT_BLOCK_END, beginIdx);
|
|
120
|
+
if (endIdx === -1) return text;
|
|
121
|
+
const blockEnd = endIdx + MT_BLOCK_END.length;
|
|
122
|
+
const tailNewline = text.charAt(blockEnd) === "\n" ? 1 : 0;
|
|
123
|
+
return text.slice(0, beginIdx) + text.slice(blockEnd + tailNewline);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True when an inner hook object is memtrace-owned (managed tag, our hook
|
|
127
|
+
* scripts, or the Rail `route --hook` command). */
|
|
128
|
+
function isMemtraceHook(h) {
|
|
129
|
+
if (!isRecord(h)) return false;
|
|
130
|
+
if (h._managed_by === MT_HOOK_MANAGED_TAG) return true;
|
|
131
|
+
if (typeof h.command === "string") {
|
|
132
|
+
if (h.command.includes(MT_RAIL_HOOK_MARKER)) return true;
|
|
133
|
+
for (const script of MT_HOOK_SCRIPTS) {
|
|
134
|
+
if (h.command.endsWith(script)) return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Strip every memtrace-owned hook entry (managed UserPromptSubmit /
|
|
141
|
+
* PostToolUse + Rail PreToolUse) from a parsed settings object. */
|
|
142
|
+
function stripManagedHooksFromSettings(settings) {
|
|
143
|
+
const out = JSON.parse(JSON.stringify(settings || {}));
|
|
144
|
+
if (!isRecord(out.hooks)) return out;
|
|
145
|
+
for (const eventName of Object.keys(out.hooks)) {
|
|
146
|
+
const blocks = out.hooks[eventName];
|
|
147
|
+
if (!Array.isArray(blocks)) continue;
|
|
148
|
+
const cleaned = [];
|
|
149
|
+
for (const block of blocks) {
|
|
150
|
+
if (!isRecord(block) || !Array.isArray(block.hooks)) {
|
|
151
|
+
cleaned.push(block);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const remaining = block.hooks.filter((h) => !isMemtraceHook(h));
|
|
155
|
+
if (remaining.length === 0) continue; // block was entirely ours
|
|
156
|
+
cleaned.push({ ...block, hooks: remaining });
|
|
157
|
+
}
|
|
158
|
+
if (cleaned.length === 0) delete out.hooks[eventName];
|
|
159
|
+
else out.hooks[eventName] = cleaned;
|
|
160
|
+
}
|
|
161
|
+
if (isRecord(out.hooks) && Object.keys(out.hooks).length === 0) delete out.hooks;
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Surgically remove memtrace-owned content from one manifest-tracked file.
|
|
166
|
+
* Used when the file changed after install (hand-edited). */
|
|
167
|
+
function surgicalCleanFile(entry) {
|
|
168
|
+
const file = entry.path;
|
|
169
|
+
if (!fs.existsSync(file)) return false;
|
|
170
|
+
const kind = entry.owned && entry.owned.kind;
|
|
171
|
+
|
|
172
|
+
if (kind === "file") {
|
|
173
|
+
// Wholly memtrace-owned (MEMTRACE.md) — always safe to delete.
|
|
174
|
+
fs.unlinkSync(file);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (kind === "block") {
|
|
179
|
+
const existing = fs.readFileSync(file, "utf8");
|
|
180
|
+
const updated = stripBreadcrumbText(existing);
|
|
181
|
+
if (updated === existing) return false;
|
|
182
|
+
if (updated === "") fs.unlinkSync(file);
|
|
183
|
+
else fs.writeFileSync(file, updated);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (kind === "json-keys") {
|
|
188
|
+
let settings;
|
|
189
|
+
try {
|
|
190
|
+
settings = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
191
|
+
} catch {
|
|
192
|
+
console.warn(`memtrace: ${file} is not valid JSON; leaving it untouched.`);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
let out = stripManagedHooksFromSettings(settings);
|
|
196
|
+
if (isRecord(out.mcpServers) && out.mcpServers[MCP_SERVER_NAME]) {
|
|
197
|
+
delete out.mcpServers[MCP_SERVER_NAME];
|
|
198
|
+
if (Object.keys(out.mcpServers).length === 0) delete out.mcpServers;
|
|
199
|
+
}
|
|
200
|
+
if (isRecord(out.enabledPlugins)) {
|
|
201
|
+
for (const key of Object.keys(out.enabledPlugins)) {
|
|
202
|
+
if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes("memtrace"))) {
|
|
203
|
+
delete out.enabledPlugins[key];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(out.enabledPlugins).length === 0) delete out.enabledPlugins;
|
|
207
|
+
}
|
|
208
|
+
for (const containerName of MARKETPLACE_SETTING_CONTAINERS) {
|
|
209
|
+
const container = out[containerName];
|
|
210
|
+
if (!isRecord(container)) continue;
|
|
211
|
+
for (const key of MARKETPLACE_SETTING_KEYS) {
|
|
212
|
+
if (Object.prototype.hasOwnProperty.call(container, key)) delete container[key];
|
|
213
|
+
}
|
|
214
|
+
if (Object.keys(container).length === 0) delete out[containerName];
|
|
215
|
+
}
|
|
216
|
+
const before = JSON.stringify(settings);
|
|
217
|
+
const after = JSON.stringify(out);
|
|
218
|
+
if (before === after) return false;
|
|
219
|
+
if (Object.keys(out).length === 0) fs.unlinkSync(file);
|
|
220
|
+
else fs.writeFileSync(file, JSON.stringify(out, null, 2) + "\n");
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Restore user-global files from the install manifest. Returns a summary
|
|
229
|
+
* object, or null when no manifest exists (pre-#25 install — callers fall
|
|
230
|
+
* back to legacy surgical cleanup). Idempotent: the manifest is renamed
|
|
231
|
+
* aside after a successful pass.
|
|
232
|
+
*/
|
|
233
|
+
function restoreFromManifest(home = os.homedir()) {
|
|
234
|
+
const manifest = readInstallManifest(home);
|
|
235
|
+
if (!manifest || !Array.isArray(manifest.entries)) return null;
|
|
236
|
+
|
|
237
|
+
const summary = { restored: [], surgical: [], skipped: [] };
|
|
238
|
+
for (const entry of manifest.entries) {
|
|
239
|
+
try {
|
|
240
|
+
const currentSha = sha256OfFile(entry.path);
|
|
241
|
+
if (currentSha === null && entry.postSha256 === null) {
|
|
242
|
+
summary.skipped.push(`${entry.path} (never existed)`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (currentSha !== null && currentSha === entry.postSha256) {
|
|
246
|
+
// Untouched since install — byte-exact restore of pre-install state.
|
|
247
|
+
if (entry.existedBefore && entry.backupPath && fs.existsSync(entry.backupPath)) {
|
|
248
|
+
fs.copyFileSync(entry.backupPath, entry.path);
|
|
249
|
+
summary.restored.push(`${entry.path} (restored pre-install backup)`);
|
|
250
|
+
} else if (!entry.existedBefore) {
|
|
251
|
+
fs.unlinkSync(entry.path);
|
|
252
|
+
summary.restored.push(`${entry.path} (removed — created by install)`);
|
|
253
|
+
} else {
|
|
254
|
+
// Backup missing: fall back to surgical removal.
|
|
255
|
+
if (surgicalCleanFile(entry)) summary.surgical.push(entry.path);
|
|
256
|
+
else summary.skipped.push(`${entry.path} (backup missing, nothing to remove)`);
|
|
257
|
+
}
|
|
258
|
+
} else if (currentSha === null) {
|
|
259
|
+
summary.skipped.push(`${entry.path} (already removed)`);
|
|
260
|
+
} else {
|
|
261
|
+
// Hand-edited after install — remove ONLY memtrace-owned content.
|
|
262
|
+
if (surgicalCleanFile(entry)) {
|
|
263
|
+
summary.surgical.push(`${entry.path} (memtrace blocks removed, user edits preserved)`);
|
|
264
|
+
} else {
|
|
265
|
+
summary.skipped.push(`${entry.path} (no memtrace content found)`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.warn(`memtrace: manifest restore failed for ${entry.path}: ${e.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Paths install created wholesale (plugin cache etc.) are safe to delete.
|
|
274
|
+
for (const p of manifest.createdPaths || []) {
|
|
275
|
+
try {
|
|
276
|
+
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.warn(`memtrace: failed to remove ${p}: ${e.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
consumeInstallManifest(home);
|
|
283
|
+
return summary;
|
|
284
|
+
}
|
|
285
|
+
|
|
56
286
|
// ── Remove skills ───────────────────────────────────────────────────────────
|
|
57
287
|
//
|
|
58
288
|
// Each cleanup helper takes an optional `home` parameter so we can
|
|
@@ -271,14 +501,53 @@ function tryClaudeCliUninstall() {
|
|
|
271
501
|
|
|
272
502
|
function removeClaudeIntegration(home = os.homedir()) {
|
|
273
503
|
const ci = loadClaudeIntegration();
|
|
274
|
-
if (
|
|
504
|
+
if (ci) {
|
|
505
|
+
try {
|
|
506
|
+
const summary = ci.uninstallFromFs({ claudeDir: path.join(home, ".claude") });
|
|
507
|
+
if (summary.removed.length > 0) {
|
|
508
|
+
console.log(`memtrace: cleaned Claude Code integration (${summary.removed.length} item(s))`);
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
} catch (e) {
|
|
512
|
+
console.warn(`memtrace: Claude Code integration cleanup failed: ${e.message}`);
|
|
513
|
+
// fall through to the inline cleanup below
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Inline fallback (memtrace-public #25): when this script runs from its
|
|
518
|
+
// persisted copy at ~/.memtrace/uninstall.js, lib/claude-integration is
|
|
519
|
+
// not on disk. The old behavior silently no-op'd here, leaving the
|
|
520
|
+
// CLAUDE.md breadcrumb and the managed settings.json hooks behind.
|
|
275
521
|
try {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
522
|
+
const claudeDir = path.join(home, ".claude");
|
|
523
|
+
|
|
524
|
+
const memtraceMd = path.join(claudeDir, "MEMTRACE.md");
|
|
525
|
+
if (fs.existsSync(memtraceMd)) fs.unlinkSync(memtraceMd);
|
|
526
|
+
|
|
527
|
+
const claudeMd = path.join(claudeDir, "CLAUDE.md");
|
|
528
|
+
if (fs.existsSync(claudeMd)) {
|
|
529
|
+
const existing = fs.readFileSync(claudeMd, "utf8");
|
|
530
|
+
const updated = stripBreadcrumbText(existing);
|
|
531
|
+
if (updated !== existing) {
|
|
532
|
+
if (updated === "") fs.unlinkSync(claudeMd);
|
|
533
|
+
else fs.writeFileSync(claudeMd, updated);
|
|
534
|
+
}
|
|
279
535
|
}
|
|
536
|
+
|
|
537
|
+
const settingsFile = settingsPath(home);
|
|
538
|
+
if (fs.existsSync(settingsFile)) {
|
|
539
|
+
try {
|
|
540
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
|
|
541
|
+
const updated = stripManagedHooksFromSettings(settings);
|
|
542
|
+
if (JSON.stringify(updated) !== JSON.stringify(settings)) {
|
|
543
|
+
if (Object.keys(updated).length === 0) fs.unlinkSync(settingsFile);
|
|
544
|
+
else fs.writeFileSync(settingsFile, JSON.stringify(updated, null, 2) + "\n");
|
|
545
|
+
}
|
|
546
|
+
} catch { /* malformed settings — leave untouched */ }
|
|
547
|
+
}
|
|
548
|
+
console.log("memtrace: cleaned Claude Code integration (inline fallback)");
|
|
280
549
|
} catch (e) {
|
|
281
|
-
console.warn(`memtrace: Claude Code
|
|
550
|
+
console.warn(`memtrace: inline Claude Code cleanup failed: ${e.message}`);
|
|
282
551
|
}
|
|
283
552
|
}
|
|
284
553
|
|
|
@@ -376,6 +645,21 @@ function run() {
|
|
|
376
645
|
killRunningMemtraceProcesses();
|
|
377
646
|
console.log("memtrace: cleaning up...");
|
|
378
647
|
|
|
648
|
+
// Manifest-first restore (memtrace-public #25): put every file the
|
|
649
|
+
// install mutated back to its pre-install bytes (or surgically strip
|
|
650
|
+
// memtrace blocks if the user edited it since). The legacy cleanup
|
|
651
|
+
// below still runs afterwards — it is idempotent and catches state
|
|
652
|
+
// from older installer versions that wrote no manifest.
|
|
653
|
+
try {
|
|
654
|
+
const summary = restoreFromManifest(process.env.MEMTRACE_INSTALL_HOME || os.homedir());
|
|
655
|
+
if (summary) {
|
|
656
|
+
const n = summary.restored.length + summary.surgical.length;
|
|
657
|
+
if (n > 0) console.log(`memtrace: restored ${n} file(s) from the install manifest`);
|
|
658
|
+
}
|
|
659
|
+
} catch (e) {
|
|
660
|
+
console.warn(`memtrace: manifest restore failed: ${e.message}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
379
663
|
// Try delegating to the compiled installer (handles ALL agents via registry)
|
|
380
664
|
const bundled = path.join(__dirname, "installer", "dist", "index.js");
|
|
381
665
|
if (fs.existsSync(bundled)) {
|
|
@@ -428,6 +712,13 @@ module.exports = {
|
|
|
428
712
|
killRunningMemtraceProcesses,
|
|
429
713
|
removeClaudeIntegration,
|
|
430
714
|
legacyCleanup,
|
|
715
|
+
// Manifest restore (memtrace-public #25)
|
|
716
|
+
installManifestPath,
|
|
717
|
+
readInstallManifest,
|
|
718
|
+
restoreFromManifest,
|
|
719
|
+
stripBreadcrumbText,
|
|
720
|
+
stripManagedHooksFromSettings,
|
|
721
|
+
surgicalCleanFile,
|
|
431
722
|
};
|
|
432
723
|
|
|
433
724
|
// Run when called directly (npm preuninstall hook). When require()'d
|