memtrace 0.7.15 → 0.7.20
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/installer/dist/commands/enterprise-install.d.ts +7 -0
- package/installer/dist/commands/enterprise-install.js +129 -0
- package/installer/dist/index.js +16 -0
- package/installer/dist/transformers/claude.js +52 -20
- package/installer/package.json +1 -1
- package/lib/claude-integration.js +77 -5
- package/package.json +7 -7
- package/uninstall.js +6 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
5
|
+
import { resolveMemtraceBinary } from './install.js';
|
|
6
|
+
import { registerCursorMcpAt } from '../transformers/cursor.js';
|
|
7
|
+
function loadSeed(seedPath) {
|
|
8
|
+
const raw = fs.readFileSync(seedPath, 'utf8');
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
if (!parsed.memdb?.database) {
|
|
11
|
+
throw new Error('seed JSON must include memdb.database');
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
function loadWriterToken(seedDir) {
|
|
16
|
+
const tokPath = path.join(seedDir, 'tokens.plaintext.json');
|
|
17
|
+
if (!fs.existsSync(tokPath)) {
|
|
18
|
+
throw new Error(`Missing ${tokPath} — generate bundle via enterprise admin first`);
|
|
19
|
+
}
|
|
20
|
+
const tokens = JSON.parse(fs.readFileSync(tokPath, 'utf8'));
|
|
21
|
+
if (!tokens.writer)
|
|
22
|
+
throw new Error('tokens.plaintext.json must include writer token');
|
|
23
|
+
return tokens.writer;
|
|
24
|
+
}
|
|
25
|
+
function resolveLicenseKey(seed, seedDir) {
|
|
26
|
+
if (seed.license?.key)
|
|
27
|
+
return seed.license.key;
|
|
28
|
+
const seedJson = path.join(seedDir, 'seed.json');
|
|
29
|
+
if (fs.existsSync(seedJson)) {
|
|
30
|
+
const full = JSON.parse(fs.readFileSync(seedJson, 'utf8'));
|
|
31
|
+
if (full.license?.key)
|
|
32
|
+
return full.license.key;
|
|
33
|
+
}
|
|
34
|
+
const env = process.env.MEMTRACE_LICENSE_KEY;
|
|
35
|
+
if (env)
|
|
36
|
+
return env;
|
|
37
|
+
throw new Error('License key not found in seed; set MEMTRACE_LICENSE_KEY or include license.key');
|
|
38
|
+
}
|
|
39
|
+
const DEFAULT_AUTH_URL = 'https://www.memtrace.io';
|
|
40
|
+
async function verifySubscriptionWithApi(organizationId, licenseKey) {
|
|
41
|
+
const base = (process.env.MEMTRACE_AUTH_URL ?? DEFAULT_AUTH_URL).replace(/\/$/, '');
|
|
42
|
+
const resp = await fetch(`${base}/api/enterprise/install/verify`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ organizationId, licenseKey }),
|
|
46
|
+
signal: AbortSignal.timeout(10_000),
|
|
47
|
+
});
|
|
48
|
+
if (!resp.ok) {
|
|
49
|
+
const payload = await resp.json().catch(() => ({}));
|
|
50
|
+
throw new Error(payload.error ?? `Enterprise install blocked (${resp.status})`);
|
|
51
|
+
}
|
|
52
|
+
const payload = await resp.json();
|
|
53
|
+
if (!payload.ok) {
|
|
54
|
+
throw new Error('Enterprise subscription verification failed');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function validateRemoteEndpoint(endpoint, token) {
|
|
58
|
+
const url = endpoint.replace(/\/$/, '');
|
|
59
|
+
// gRPC health is not available from Node without deps; ping via HTTP metrics if exposed.
|
|
60
|
+
const metricsUrl = url.replace(/:50051$/, ':9090');
|
|
61
|
+
try {
|
|
62
|
+
const resp = await fetch(`${metricsUrl}/metrics`, {
|
|
63
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
64
|
+
signal: AbortSignal.timeout(5000),
|
|
65
|
+
});
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
console.warn(` ⚠ MemDB metrics at ${metricsUrl} returned ${resp.status} (gRPC may still work)`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(` ✓ MemDB metrics reachable at ${metricsUrl}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.warn(` ⚠ Could not reach MemDB at ${endpoint}: ${e.message}`);
|
|
75
|
+
console.warn(' Continuing — verify VPN/firewall if remote mode fails at runtime.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function mcpPath(scope) {
|
|
79
|
+
const base = scope === 'global' ? os.homedir() : process.cwd();
|
|
80
|
+
return path.join(base, '.cursor', 'mcp.json');
|
|
81
|
+
}
|
|
82
|
+
export async function runEnterpriseInstall(opts) {
|
|
83
|
+
const seedPath = path.resolve(opts.seedPath);
|
|
84
|
+
const seedDir = path.dirname(seedPath);
|
|
85
|
+
const seed = loadSeed(seedPath);
|
|
86
|
+
const scope = opts.scope ?? 'global';
|
|
87
|
+
const endpoint = seed.memdb.endpoint ?? process.env.MEMTRACE_MEMDB_ENDPOINT;
|
|
88
|
+
if (!endpoint) {
|
|
89
|
+
throw new Error('memdb.endpoint required in seed or MEMTRACE_MEMDB_ENDPOINT env');
|
|
90
|
+
}
|
|
91
|
+
const writerToken = loadWriterToken(seedDir);
|
|
92
|
+
const licenseKey = resolveLicenseKey(seed, seedDir);
|
|
93
|
+
const binary = opts.binary ?? (await resolveMemtraceBinary()) ?? 'memtrace';
|
|
94
|
+
console.log('\n Memtrace Enterprise Installer\n');
|
|
95
|
+
console.log(` Org: ${seed.orgId}`);
|
|
96
|
+
console.log(` MemDB: ${endpoint}`);
|
|
97
|
+
console.log(` Database: ${seed.memdb.database}`);
|
|
98
|
+
console.log(` Scope: ${scope}\n`);
|
|
99
|
+
console.log(' Verifying enterprise subscription…');
|
|
100
|
+
await verifySubscriptionWithApi(seed.orgId, licenseKey);
|
|
101
|
+
console.log(' ✓ Subscription active');
|
|
102
|
+
await validateRemoteEndpoint(endpoint, writerToken);
|
|
103
|
+
const mcpFile = mcpPath(scope);
|
|
104
|
+
const { value, corrupted, backupPath } = safeReadJson(mcpFile);
|
|
105
|
+
if (corrupted) {
|
|
106
|
+
console.warn(` ⚠ ${mcpFile} malformed; backed up to ${backupPath}`);
|
|
107
|
+
}
|
|
108
|
+
const cfg = (value ?? {});
|
|
109
|
+
cfg.mcpServers = cfg.mcpServers ?? {};
|
|
110
|
+
cfg.mcpServers.memtrace = {
|
|
111
|
+
command: binary,
|
|
112
|
+
args: ['mcp'],
|
|
113
|
+
env: {
|
|
114
|
+
MEMTRACE_LICENSE_KEY: licenseKey,
|
|
115
|
+
MEMTRACE_MEMDB_MODE: 'external',
|
|
116
|
+
MEMTRACE_MEMDB_ENDPOINT: endpoint,
|
|
117
|
+
MEMTRACE_MEMDB_AUTH_TOKEN: writerToken,
|
|
118
|
+
MEMTRACE_MEMDB_DB: seed.memdb.database,
|
|
119
|
+
MEMTRACE_WORKSPACE_ROOT: '${workspaceFolder}',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
writeJsonAtomic(mcpFile, cfg);
|
|
123
|
+
console.log(` ✓ Wrote enterprise MCP config → ${mcpFile}`);
|
|
124
|
+
// Also register via cursor helper when file is the standard cursor path
|
|
125
|
+
if (mcpFile.endsWith('.cursor/mcp.json')) {
|
|
126
|
+
registerCursorMcpAt(mcpFile, binary);
|
|
127
|
+
}
|
|
128
|
+
console.log('\n Done. Restart Cursor and run `memtrace auth login` if needed.\n');
|
|
129
|
+
}
|
package/installer/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { runInstall, runUninstall } from './commands/install.js';
|
|
4
4
|
import { runDoctor } from './commands/doctor.js';
|
|
5
5
|
import { runInstallRail, runUninstallRail } from './commands/rail-install.js';
|
|
6
|
+
import { runEnterpriseInstall } from './commands/enterprise-install.js';
|
|
6
7
|
const program = new Command();
|
|
7
8
|
program
|
|
8
9
|
.name('memtrace-skills')
|
|
@@ -46,6 +47,21 @@ program
|
|
|
46
47
|
only: opts.only,
|
|
47
48
|
});
|
|
48
49
|
});
|
|
50
|
+
program
|
|
51
|
+
.command('install-enterprise')
|
|
52
|
+
.description('Configure Memtrace MCP for self-hosted enterprise MemDB (remote mode)')
|
|
53
|
+
.requiredOption('--seed <path>', 'path to enterprise seed.json from your admin bundle')
|
|
54
|
+
.option('--local', 'write MCP config to the current project', false)
|
|
55
|
+
.option('--binary <path>', 'memtrace executable (default: on PATH)')
|
|
56
|
+
.option('-y, --yes', 'non-interactive')
|
|
57
|
+
.action(async (opts) => {
|
|
58
|
+
await runEnterpriseInstall({
|
|
59
|
+
seedPath: opts.seed,
|
|
60
|
+
scope: opts.local ? 'local' : 'global',
|
|
61
|
+
binary: opts.binary,
|
|
62
|
+
yes: opts.yes,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
49
65
|
program
|
|
50
66
|
.command('install-rail')
|
|
51
67
|
.description('Wire Memtrace Rail discovery hooks for Claude, Cursor, Codex, Gemini, OpenCode')
|
|
@@ -224,6 +224,10 @@ const RAIL_HOOK_MATCHER = 'Grep|Glob|Bash';
|
|
|
224
224
|
/** Substring that identifies a memtrace-owned Rail hook entry (for idempotent
|
|
225
225
|
* re-register + clean removal). */
|
|
226
226
|
const RAIL_HOOK_MARKER = 'route --hook';
|
|
227
|
+
const LEGACY_HOOK_SCRIPT_BASENAMES = [
|
|
228
|
+
'userprompt-claude.sh',
|
|
229
|
+
'posttool-mcp-telemetry.sh',
|
|
230
|
+
];
|
|
227
231
|
/**
|
|
228
232
|
* Register the Memtrace Rail PreToolUse hook in a Claude settings.json.
|
|
229
233
|
*
|
|
@@ -245,12 +249,13 @@ export function registerRailHookInSettingsAt(settingsPath, memtraceBinary) {
|
|
|
245
249
|
return { registered: false, backupPath };
|
|
246
250
|
}
|
|
247
251
|
const settings = (value ?? {});
|
|
248
|
-
settings.hooks = settings.hooks
|
|
252
|
+
settings.hooks = isRecord(settings.hooks) ? settings.hooks : {};
|
|
253
|
+
pruneMemtraceOwnedHookEntries(settings.hooks);
|
|
249
254
|
const existing = Array.isArray(settings.hooks.PreToolUse)
|
|
250
255
|
? settings.hooks.PreToolUse
|
|
251
256
|
: [];
|
|
252
257
|
// Drop any prior memtrace Rail entry so re-install doesn't duplicate it.
|
|
253
|
-
const preserved = existing.filter((entry) => !
|
|
258
|
+
const preserved = existing.filter((entry) => !isMemtraceOwnedHookEntry(entry));
|
|
254
259
|
preserved.push({
|
|
255
260
|
matcher: RAIL_HOOK_MATCHER,
|
|
256
261
|
hooks: [
|
|
@@ -266,9 +271,46 @@ export function registerRailHookInSettingsAt(settingsPath, memtraceBinary) {
|
|
|
266
271
|
}
|
|
267
272
|
/** True when a PreToolUse entry is a memtrace-owned Rail hook. */
|
|
268
273
|
function isRailHookEntry(entry) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
return hookCommands(entry).some((command) => command.includes(RAIL_HOOK_MARKER));
|
|
275
|
+
}
|
|
276
|
+
function isLegacyMemtraceHookEntry(entry) {
|
|
277
|
+
return hookCommands(entry).some((command) => (LEGACY_HOOK_SCRIPT_BASENAMES.some((basename) => command.includes(basename))));
|
|
278
|
+
}
|
|
279
|
+
function isMemtraceOwnedHookEntry(entry) {
|
|
280
|
+
return isRailHookEntry(entry) || isLegacyMemtraceHookEntry(entry);
|
|
281
|
+
}
|
|
282
|
+
function hookCommands(entry) {
|
|
283
|
+
if (!isRecord(entry))
|
|
284
|
+
return [];
|
|
285
|
+
const commands = [];
|
|
286
|
+
if (typeof entry.command === 'string') {
|
|
287
|
+
commands.push(entry.command);
|
|
288
|
+
}
|
|
289
|
+
if (Array.isArray(entry.hooks)) {
|
|
290
|
+
for (const hook of entry.hooks) {
|
|
291
|
+
if (isRecord(hook) && typeof hook.command === 'string') {
|
|
292
|
+
commands.push(hook.command);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return commands;
|
|
297
|
+
}
|
|
298
|
+
function pruneMemtraceOwnedHookEntries(hooks) {
|
|
299
|
+
let changed = false;
|
|
300
|
+
for (const key of Object.keys(hooks)) {
|
|
301
|
+
const entries = hooks[key];
|
|
302
|
+
if (!Array.isArray(entries))
|
|
303
|
+
continue;
|
|
304
|
+
const preserved = entries.filter((entry) => !isMemtraceOwnedHookEntry(entry));
|
|
305
|
+
if (preserved.length === entries.length)
|
|
306
|
+
continue;
|
|
307
|
+
changed = true;
|
|
308
|
+
hooks[key] = preserved.length === 0 ? undefined : preserved;
|
|
309
|
+
}
|
|
310
|
+
return changed;
|
|
311
|
+
}
|
|
312
|
+
function hasHookSettings(hooks) {
|
|
313
|
+
return Object.values(hooks).some((entries) => (entries !== undefined && (!Array.isArray(entries) || entries.length > 0)));
|
|
272
314
|
}
|
|
273
315
|
/**
|
|
274
316
|
* Remove the Memtrace Rail PreToolUse hook from a Claude settings.json.
|
|
@@ -361,21 +403,11 @@ export function removeClaudeSettingsEntriesAt(settingsPath) {
|
|
|
361
403
|
if (Object.keys(mcpServers).length === 0)
|
|
362
404
|
delete settings.mcpServers;
|
|
363
405
|
}
|
|
364
|
-
// ─── memtrace-rail ─── Remove
|
|
365
|
-
if (isRecord(settings.hooks)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
changed = true;
|
|
370
|
-
if (after.length === 0) {
|
|
371
|
-
delete settings.hooks.PreToolUse;
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
settings.hooks.PreToolUse = after;
|
|
375
|
-
}
|
|
376
|
-
if (Object.keys(settings.hooks).length === 0)
|
|
377
|
-
delete settings.hooks;
|
|
378
|
-
}
|
|
406
|
+
// ─── memtrace-rail ─── Remove current Rail and legacy telemetry hooks on uninstall.
|
|
407
|
+
if (isRecord(settings.hooks)) {
|
|
408
|
+
changed = pruneMemtraceOwnedHookEntries(settings.hooks) || changed;
|
|
409
|
+
if (!hasHookSettings(settings.hooks))
|
|
410
|
+
settings.hooks = undefined;
|
|
379
411
|
}
|
|
380
412
|
for (const containerName of MARKETPLACE_SETTING_CONTAINERS) {
|
|
381
413
|
const container = settings[containerName];
|
package/installer/package.json
CHANGED
|
@@ -87,6 +87,72 @@ const CLAUDE_MD_BREADCRUMB = CLAUDE_MD_BREADCRUMB_LINES.join("\n");
|
|
|
87
87
|
|
|
88
88
|
const HOOK_MANAGED_TAG = "memtrace";
|
|
89
89
|
|
|
90
|
+
// ── Pure functions: hook command string (cross-platform) ──────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build the `command` string Claude Code runs for one of our hook
|
|
94
|
+
* scripts. Pure — the platform is injected so the win32 branch is
|
|
95
|
+
* unit-testable on any host.
|
|
96
|
+
*
|
|
97
|
+
* Claude Code executes hook commands through Git Bash on Windows. Two
|
|
98
|
+
* things break a bare `path.join(hooksDir, script)` value there
|
|
99
|
+
* (issue #34):
|
|
100
|
+
*
|
|
101
|
+
* 1. A bare `.sh` path is not directly executable on Windows — it has
|
|
102
|
+
* to be handed to `bash`.
|
|
103
|
+
* 2. `path.join` yields backslashes (`C:\Users\…`), and bash eats
|
|
104
|
+
* backslashes as escape sequences, mangling the path into a
|
|
105
|
+
* "No such file or directory" on every UserPromptSubmit and every
|
|
106
|
+
* `mcp__memtrace__*` PostToolUse.
|
|
107
|
+
*
|
|
108
|
+
* The form that works under Git Bash is:
|
|
109
|
+
* bash "C:/Users/…/userprompt-claude.sh"
|
|
110
|
+
* (forward slashes, double-quoted, `bash ` prefix).
|
|
111
|
+
*
|
|
112
|
+
* On non-win32 (mac/linux) we keep the existing bare absolute path —
|
|
113
|
+
* those scripts are executable via their shebang and existing installs
|
|
114
|
+
* must not regress.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} hooksDir absolute directory containing the hook scripts
|
|
117
|
+
* @param {string} scriptName e.g. `"userprompt-claude.sh"`
|
|
118
|
+
* @param {string} [platform] defaults to `process.platform`; pass
|
|
119
|
+
* `"win32"` to exercise the Windows branch in
|
|
120
|
+
* tests without running on Windows
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
function hookCommandString(hooksDir, scriptName, platform = process.platform) {
|
|
124
|
+
if (platform === "win32") {
|
|
125
|
+
// Join with the win32 semantics so a test on POSIX still produces
|
|
126
|
+
// a Windows-shaped path, then normalise every separator to '/'.
|
|
127
|
+
const joined = path.win32.join(hooksDir, scriptName).replace(/\\/g, "/");
|
|
128
|
+
return `bash "${joined}"`;
|
|
129
|
+
}
|
|
130
|
+
return path.join(hooksDir, scriptName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Pull the hook script basename out of a `command` string, robust to
|
|
135
|
+
* BOTH a bare path AND a `bash "<path>"`-wrapped command, with either
|
|
136
|
+
* '/' or '\\' separators and surrounding/embedded quotes (issue #34).
|
|
137
|
+
*
|
|
138
|
+
* `path.basename` alone is not enough once we emit `bash "C:/…/x.sh"`:
|
|
139
|
+
* its basename is `x.sh"` (trailing quote) on POSIX, and it doesn't
|
|
140
|
+
* understand backslash separators off-Windows. Both break the dedup
|
|
141
|
+
* that keeps installs idempotent. We strip quotes, normalise '\\'→'/',
|
|
142
|
+
* and take the final segment so the bare and wrapped forms collapse to
|
|
143
|
+
* the same basename.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} command
|
|
146
|
+
* @returns {string|null}
|
|
147
|
+
*/
|
|
148
|
+
function hookScriptBasename(command) {
|
|
149
|
+
if (typeof command !== "string") return null;
|
|
150
|
+
const cleaned = command.replace(/["']/g, "").replace(/\\/g, "/").trim();
|
|
151
|
+
const segments = cleaned.split("/");
|
|
152
|
+
const last = segments[segments.length - 1];
|
|
153
|
+
return last || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
// ── Pure functions: MEMTRACE.md content ───────────────────────────────
|
|
91
157
|
|
|
92
158
|
/**
|
|
@@ -184,13 +250,13 @@ function applySettingsHooks(settings, hooksDir) {
|
|
|
184
250
|
|
|
185
251
|
const userPromptHook = {
|
|
186
252
|
type: "command",
|
|
187
|
-
command:
|
|
253
|
+
command: hookCommandString(hooksDir, "userprompt-claude.sh"),
|
|
188
254
|
_managed_by: HOOK_MANAGED_TAG,
|
|
189
255
|
_hook_kind: "userprompt-advisory",
|
|
190
256
|
};
|
|
191
257
|
const postToolHook = {
|
|
192
258
|
type: "command",
|
|
193
|
-
command:
|
|
259
|
+
command: hookCommandString(hooksDir, "posttool-mcp-telemetry.sh"),
|
|
194
260
|
_managed_by: HOOK_MANAGED_TAG,
|
|
195
261
|
_hook_kind: "posttool-mcp-telemetry",
|
|
196
262
|
};
|
|
@@ -269,15 +335,19 @@ function applySettingsHooks(settings, hooksDir) {
|
|
|
269
335
|
* @returns {Array}
|
|
270
336
|
*/
|
|
271
337
|
function mergeHookList(existing, ourEntry, ourHook) {
|
|
272
|
-
|
|
273
|
-
|
|
338
|
+
// Use the quote/separator-robust extractor (not raw path.basename) so
|
|
339
|
+
// a `bash "C:/…/x.sh"`-wrapped Windows command (issue #34) collapses to
|
|
340
|
+
// the same basename as the bare POSIX form — otherwise path.basename
|
|
341
|
+
// would yield `x.sh"` (trailing quote) and the dedup would stop
|
|
342
|
+
// recognising the working Windows form, re-injecting a duplicate.
|
|
343
|
+
const ourBasename = hookScriptBasename(ourHook.command);
|
|
274
344
|
const isOurs = (h) =>
|
|
275
345
|
!!h && (
|
|
276
346
|
(h._managed_by === HOOK_MANAGED_TAG && h._hook_kind === ourHook._hook_kind) ||
|
|
277
347
|
(typeof h.command === "string" && h.command === ourHook.command) ||
|
|
278
348
|
(ourBasename !== null &&
|
|
279
349
|
typeof h.command === "string" &&
|
|
280
|
-
|
|
350
|
+
hookScriptBasename(h.command) === ourBasename)
|
|
281
351
|
);
|
|
282
352
|
|
|
283
353
|
// Phase 1: walk every block, strip every "ours" hook, drop blocks
|
|
@@ -499,6 +569,8 @@ module.exports = {
|
|
|
499
569
|
CLAUDE_MD_BREADCRUMB,
|
|
500
570
|
|
|
501
571
|
// Pure functions
|
|
572
|
+
hookCommandString,
|
|
573
|
+
hookScriptBasename,
|
|
502
574
|
buildMemtraceMdContent,
|
|
503
575
|
applyClaudeMdBreadcrumb,
|
|
504
576
|
removeClaudeMdBreadcrumb,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrace",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.20",
|
|
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.7.
|
|
43
|
-
"@memtrace/linux-x64": "0.7.
|
|
44
|
-
"@memtrace/linux-arm64": "0.7.
|
|
45
|
-
"@memtrace/win32-x64": "0.7.
|
|
46
|
-
"@memtrace/linux-x64-noavx2": "0.7.
|
|
47
|
-
"@memtrace/win32-x64-noavx2": "0.7.
|
|
42
|
+
"@memtrace/darwin-arm64": "0.7.20",
|
|
43
|
+
"@memtrace/linux-x64": "0.7.20",
|
|
44
|
+
"@memtrace/linux-arm64": "0.7.20",
|
|
45
|
+
"@memtrace/win32-x64": "0.7.20",
|
|
46
|
+
"@memtrace/linux-x64-noavx2": "0.7.20",
|
|
47
|
+
"@memtrace/win32-x64-noavx2": "0.7.20"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=18"
|
package/uninstall.js
CHANGED
|
@@ -131,7 +131,12 @@ function isMemtraceHook(h) {
|
|
|
131
131
|
if (typeof h.command === "string") {
|
|
132
132
|
if (h.command.includes(MT_RAIL_HOOK_MARKER)) return true;
|
|
133
133
|
for (const script of MT_HOOK_SCRIPTS) {
|
|
134
|
-
|
|
134
|
+
// `.includes`, not `.endsWith`: the Windows form is wrapped as
|
|
135
|
+
// `bash "C:/…/userprompt-claude.sh"` (issue #34), so the command
|
|
136
|
+
// ends in a double-quote, not the bare script name. `.includes`
|
|
137
|
+
// recognises both the bare POSIX path and the bash-wrapped Windows
|
|
138
|
+
// command so uninstall removes either form.
|
|
139
|
+
if (h.command.includes(script)) return true;
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
return false;
|