memtrace 0.5.15 → 0.6.10
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/hooks/userprompt-claude.sh +12 -6
- package/install.js +0 -19
- package/installer/dist/commands/rail-install.d.ts +7 -0
- package/installer/dist/commands/rail-install.js +37 -0
- package/installer/dist/index.js +20 -1
- package/installer/dist/rail-install.d.ts +20 -0
- package/installer/dist/rail-install.js +183 -0
- package/installer/dist/transformers/claude.d.ts +21 -0
- package/installer/dist/transformers/claude.js +118 -3
- package/installer/dist/transformers/codex.js +17 -0
- package/installer/dist/transformers/cursor.js +23 -1
- package/installer/dist/transformers/gemini.d.ts +3 -0
- package/installer/dist/transformers/gemini.js +78 -0
- package/installer/dist/transformers/index.d.ts +2 -1
- package/installer/dist/transformers/index.js +3 -1
- package/installer/dist/transformers/opencode.js +31 -0
- package/installer/dist/transformers/rail-hooks.d.ts +56 -0
- package/installer/dist/transformers/rail-hooks.js +303 -0
- package/installer/dist/transformers/shared.js +5 -6
- package/installer/dist/transformers/types.d.ts +1 -1
- package/installer/skills/commands/memtrace-fleet-publish-intent.md +51 -0
- package/installer/skills/commands/memtrace-fleet-record-episode.md +48 -0
- package/installer/skills/commands/memtrace-fleet-resolve.md +59 -0
- package/installer/skills/workflows/memtrace-code-review.md +6 -0
- package/installer/skills/workflows/memtrace-first.md +2 -0
- package/installer/skills/workflows/memtrace-fleet-coordination.md +87 -0
- package/installer/skills/workflows/memtrace-fleet-first.md +132 -0
- package/installer/skills/workflows/memtrace-style-fingerprint.md +111 -0
- package/lib/claude-integration.js +6 -0
- package/package.json +6 -6
- package/skills/commands/memtrace-fleet-publish-intent.md +51 -0
- package/skills/commands/memtrace-fleet-record-episode.md +48 -0
- package/skills/commands/memtrace-fleet-resolve.md +59 -0
- package/skills/workflows/memtrace-fleet-coordination.md +87 -0
- package/skills/workflows/memtrace-fleet-first.md +132 -0
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
# 0 : success (stdout is parsed for hook output)
|
|
29
29
|
# 2 : would block the prompt (we never want this)
|
|
30
30
|
#
|
|
31
|
-
# Hook output JSON shape (
|
|
32
|
-
# { "
|
|
31
|
+
# Hook output JSON shape (Claude Code UserPromptSubmit validator):
|
|
32
|
+
# { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
|
|
33
|
+
# "additionalContext": "..." } }
|
|
34
|
+
# To inject nothing: exit 0 with empty stdout (or print "{}").
|
|
33
35
|
#
|
|
34
36
|
# Override:
|
|
35
37
|
# MEMTRACE_HOOK_MODE=off → unconditional no-op (skips lock too)
|
|
@@ -172,8 +174,10 @@ if (( DEBOUNCE_SECS > 0 )) && [[ -f "$LOCK_FILE" ]]; then
|
|
|
172
174
|
# hook output and exit. We do NOT probe the daemon.
|
|
173
175
|
cat <<'EOF'
|
|
174
176
|
{
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
+
"hookSpecificOutput": {
|
|
178
|
+
"hookEventName": "UserPromptSubmit",
|
|
179
|
+
"additionalContext": ""
|
|
180
|
+
}
|
|
177
181
|
}
|
|
178
182
|
EOF
|
|
179
183
|
exit 0
|
|
@@ -253,7 +257,9 @@ shopt -u nocasematch
|
|
|
253
257
|
# to context for every matching prompt, so token cost is real.
|
|
254
258
|
cat <<'EOF'
|
|
255
259
|
{
|
|
256
|
-
"
|
|
257
|
-
|
|
260
|
+
"hookSpecificOutput": {
|
|
261
|
+
"hookEventName": "UserPromptSubmit",
|
|
262
|
+
"additionalContext": "Memtrace is active for this repository. For this code-discovery question, prefer the Memtrace MCP tools FIRST - `mcp__memtrace__find_code` for natural-language search, `mcp__memtrace__find_symbol` for exact lookup, `mcp__memtrace__get_symbol_context` for callers/callees, `mcp__memtrace__get_impact` for blast radius. They return exact file:start_line:end_line in one round-trip. Fall back to Read/Grep/Glob only for: (a) config files (.env, package.json, README, raw JSON/YAML/TOML), (b) file inventory questions, (c) paths confirmed outside any indexed repo, (d) reading exact lines you already have from a Memtrace result."
|
|
263
|
+
}
|
|
258
264
|
}
|
|
259
265
|
EOF
|
package/install.js
CHANGED
|
@@ -294,25 +294,6 @@ if (require.main === module) {
|
|
|
294
294
|
console.warn(`memtrace: failed to persist uninstall script: ${e.message}`);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
// 6. Background-daemon discovery hint (Leaf H / fortress-round-2).
|
|
298
|
-
// Operators on long-running setups (Sivant on Windows, Orbit on
|
|
299
|
-
// Linux, macOS lab hosts) shouldn't have to keep a terminal open
|
|
300
|
-
// to keep the indexer alive — point them at the subcommand. The
|
|
301
|
-
// hint is post-install only, never on update; CI systems don't
|
|
302
|
-
// benefit and we don't want to nag.
|
|
303
|
-
//
|
|
304
|
-
// The exact substring `memtrace daemon install` is contract-pinned
|
|
305
|
-
// by the F2F test row H-035
|
|
306
|
-
// (`npm_shim_post_install_prints_daemon_install_hint`). Renaming
|
|
307
|
-
// the subcommand here without updating that test is a regression.
|
|
308
|
-
try {
|
|
309
|
-
if (process.env.MEMTRACE_SUPPRESS_DAEMON_HINT !== "1") {
|
|
310
|
-
console.log(
|
|
311
|
-
"memtrace: want it to run as a background service? `memtrace daemon install` " +
|
|
312
|
-
"(macOS launchd / Linux systemd-user / Windows Service)."
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
} catch { /* best-effort — never fail install on a logging hiccup */ }
|
|
316
297
|
}
|
|
317
298
|
|
|
318
299
|
module.exports = {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface RailInstallOptions {
|
|
2
|
+
/** Explicit memtrace binary (from `memtrace rail enable`); wins over PATH lookup. */
|
|
3
|
+
binary?: string;
|
|
4
|
+
scope?: 'global' | 'local';
|
|
5
|
+
}
|
|
6
|
+
export declare function runInstallRail(options?: RailInstallOptions): Promise<void>;
|
|
7
|
+
export declare function runUninstallRail(options?: RailInstallOptions): Promise<void>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resolveMemtraceBinary } from './install.js';
|
|
2
|
+
import { installRailHooks, printRailInstallSummary, uninstallRailHooks, } from '../rail-install.js';
|
|
3
|
+
export async function runInstallRail(options = {}) {
|
|
4
|
+
const memtraceBin = options.binary?.trim() ||
|
|
5
|
+
(await resolveMemtraceBinary()) ||
|
|
6
|
+
'';
|
|
7
|
+
if (!memtraceBin) {
|
|
8
|
+
console.error(' Error: memtrace binary not found. Pass --binary or ensure memtrace is on PATH.\n');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const ctx = {
|
|
12
|
+
scope: options.scope ?? 'global',
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
memtraceBinary: memtraceBin,
|
|
15
|
+
skipMcp: true,
|
|
16
|
+
};
|
|
17
|
+
const results = installRailHooks(ctx);
|
|
18
|
+
printRailInstallSummary(results, 'installed');
|
|
19
|
+
const wired = results.filter((r) => r.registered).map((r) => r.host);
|
|
20
|
+
if (wired.length > 0) {
|
|
21
|
+
console.log(' Next steps:');
|
|
22
|
+
console.log(' 1. memtrace start (daemon for find_code)');
|
|
23
|
+
console.log(' 2. Reload your IDE (Cursor: reload window; Claude: new session)');
|
|
24
|
+
console.log(' 3. Shell-based search (Cursor native Grep is not hooked)\n');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function runUninstallRail(options = {}) {
|
|
28
|
+
const ctx = {
|
|
29
|
+
scope: options.scope ?? 'global',
|
|
30
|
+
cwd: process.cwd(),
|
|
31
|
+
memtraceBinary: options.binary?.trim() ?? '',
|
|
32
|
+
skipMcp: true,
|
|
33
|
+
};
|
|
34
|
+
const results = uninstallRailHooks(ctx);
|
|
35
|
+
printRailInstallSummary(results, 'removed');
|
|
36
|
+
console.log('');
|
|
37
|
+
}
|
package/installer/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { runInstall, runUninstall } from './commands/install.js';
|
|
4
4
|
import { runDoctor } from './commands/doctor.js';
|
|
5
|
+
import { runInstallRail, runUninstallRail } from './commands/rail-install.js';
|
|
5
6
|
const program = new Command();
|
|
6
7
|
program
|
|
7
8
|
.name('memtrace-skills')
|
|
@@ -13,7 +14,7 @@ function parseOnly(val) {
|
|
|
13
14
|
program
|
|
14
15
|
.command('install')
|
|
15
16
|
.description('Install memtrace skills and register MCP for selected agents')
|
|
16
|
-
.option('--only <agents>', 'comma-separated agent names (claude,cursor,codex,windsurf,vscode,hermes,opencode,kiro)', parseOnly)
|
|
17
|
+
.option('--only <agents>', 'comma-separated agent names (claude,cursor,codex,gemini,windsurf,vscode,hermes,opencode,kiro)', parseOnly)
|
|
17
18
|
.option('--local', 'install into the current project where the selected agent supports project scope', false)
|
|
18
19
|
.option('--global', 'install globally (~/.claude/, ~/.cursor/, ~/.agents/) [default]', false)
|
|
19
20
|
.option('--skip-mcp', 'write skills only, skip MCP server registration', false)
|
|
@@ -45,6 +46,24 @@ program
|
|
|
45
46
|
only: opts.only,
|
|
46
47
|
});
|
|
47
48
|
});
|
|
49
|
+
program
|
|
50
|
+
.command('install-rail')
|
|
51
|
+
.description('Wire Memtrace Rail discovery hooks for Claude, Cursor, Codex, Gemini, OpenCode')
|
|
52
|
+
.option('--binary <path>', 'memtrace executable (default: on PATH)')
|
|
53
|
+
.option('--local', 'project-scoped paths where supported', false)
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
await runInstallRail({
|
|
56
|
+
binary: opts.binary,
|
|
57
|
+
scope: opts.local ? 'local' : 'global',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('uninstall-rail')
|
|
62
|
+
.description('Remove Memtrace Rail hooks only (not skills/MCP)')
|
|
63
|
+
.option('--local', 'project-scoped paths where supported', false)
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
await runUninstallRail({ scope: opts.local ? 'local' : 'global' });
|
|
66
|
+
});
|
|
48
67
|
// Default: `memtrace-skills` with no subcommand → install with defaults (global, all agents)
|
|
49
68
|
program.action(async () => {
|
|
50
69
|
await runInstall({ scope: 'global' });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InstallContext } from './transformers/types.js';
|
|
2
|
+
export interface RailHostResult {
|
|
3
|
+
host: string;
|
|
4
|
+
registered: boolean;
|
|
5
|
+
path?: string;
|
|
6
|
+
skipped?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Hosts with no Rail hook surface yet (MCP/skills only). */
|
|
9
|
+
export declare const RAIL_UNSUPPORTED: ReadonlyArray<{
|
|
10
|
+
host: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Wire Rail discovery hooks for Claude, Cursor, Codex, Gemini, and OpenCode.
|
|
15
|
+
* Idempotent. Does not install skills or MCP — hooks only.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installRailHooks(ctx: InstallContext): RailHostResult[];
|
|
18
|
+
/** Remove only Memtrace-owned Rail hook entries (not skills/MCP). */
|
|
19
|
+
export declare function uninstallRailHooks(ctx: InstallContext): RailHostResult[];
|
|
20
|
+
export declare function printRailInstallSummary(results: RailHostResult[], action: 'installed' | 'removed'): void;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memtrace Rail hook wiring for every host that supports discovery interception.
|
|
3
|
+
* Called from `memtrace rail enable` (via `install-rail` CLI) and from full
|
|
4
|
+
* `memtrace install` (per-transformer, same helpers).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { registerRailHookInSettingsAt, removeRailHookFromSettingsAt, } from './transformers/claude.js';
|
|
10
|
+
import { geminiSettingsPath } from './transformers/gemini.js';
|
|
11
|
+
import { registerCursorRailHook, removeCursorRailHook, registerSettingsHook, removeSettingsHook, registerWindsurfRailHook, removeWindsurfRailHook, registerVsCodeRailHook, removeVsCodeRailHook, windsurfHooksPath, vscodeCopilotRailHookPath, CODEX_MATCHER, GEMINI_MATCHER, openCodePluginSource, } from './transformers/rail-hooks.js';
|
|
12
|
+
/** Hosts with no Rail hook surface yet (MCP/skills only). */
|
|
13
|
+
export const RAIL_UNSUPPORTED = [
|
|
14
|
+
{ host: 'hermes', reason: 'Hermes pre_tool_call adapter not wired yet' },
|
|
15
|
+
{ host: 'kiro', reason: 'Kiro IDE hooks omit tool stdin (CLI-only for now)' },
|
|
16
|
+
];
|
|
17
|
+
function codexHooksPath(ctx) {
|
|
18
|
+
const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
|
|
19
|
+
return path.join(base, 'hooks.json');
|
|
20
|
+
}
|
|
21
|
+
function cursorHooksPath(ctx) {
|
|
22
|
+
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
23
|
+
return path.join(base, '.cursor', 'hooks.json');
|
|
24
|
+
}
|
|
25
|
+
function opencodeConfigDir(ctx) {
|
|
26
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config');
|
|
27
|
+
return ctx.scope === 'global' ? path.join(xdg, 'opencode') : path.join(ctx.cwd, '.opencode');
|
|
28
|
+
}
|
|
29
|
+
function opencodeRailPluginPaths(ctx) {
|
|
30
|
+
const base = opencodeConfigDir(ctx);
|
|
31
|
+
return [
|
|
32
|
+
path.join(base, 'plugin', 'memtrace-rail.js'),
|
|
33
|
+
path.join(base, 'plugins', 'memtrace-rail.js'),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wire Rail discovery hooks for Claude, Cursor, Codex, Gemini, and OpenCode.
|
|
38
|
+
* Idempotent. Does not install skills or MCP — hooks only.
|
|
39
|
+
*/
|
|
40
|
+
export function installRailHooks(ctx) {
|
|
41
|
+
const bin = ctx.memtraceBinary?.trim() ?? '';
|
|
42
|
+
if (!bin) {
|
|
43
|
+
throw new Error('memtrace binary path is required to install Rail hooks');
|
|
44
|
+
}
|
|
45
|
+
const results = [];
|
|
46
|
+
const claudePath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
47
|
+
const claude = registerRailHookInSettingsAt(claudePath, bin);
|
|
48
|
+
results.push({
|
|
49
|
+
host: 'claude',
|
|
50
|
+
registered: claude.registered,
|
|
51
|
+
path: claudePath,
|
|
52
|
+
skipped: claude.registered ? undefined : 'settings.json malformed or unreadable',
|
|
53
|
+
});
|
|
54
|
+
const cursorPath = cursorHooksPath(ctx);
|
|
55
|
+
const cursor = registerCursorRailHook(cursorPath, bin);
|
|
56
|
+
results.push({
|
|
57
|
+
host: 'cursor',
|
|
58
|
+
registered: cursor.registered,
|
|
59
|
+
path: cursorPath,
|
|
60
|
+
skipped: cursor.registered ? undefined : 'hooks.json malformed or unreadable',
|
|
61
|
+
});
|
|
62
|
+
const codexPath = codexHooksPath(ctx);
|
|
63
|
+
const codex = registerSettingsHook(codexPath, bin, 'codex', 'PreToolUse', CODEX_MATCHER);
|
|
64
|
+
results.push({
|
|
65
|
+
host: 'codex',
|
|
66
|
+
registered: codex.registered,
|
|
67
|
+
path: codexPath,
|
|
68
|
+
skipped: codex.registered ? undefined : 'hooks.json malformed or unreadable',
|
|
69
|
+
});
|
|
70
|
+
const geminiPath = geminiSettingsPath(ctx);
|
|
71
|
+
const gemini = registerSettingsHook(geminiPath, bin, 'gemini', 'BeforeTool', GEMINI_MATCHER, {
|
|
72
|
+
enableHooks: true,
|
|
73
|
+
enableMessageBusIntegration: true,
|
|
74
|
+
});
|
|
75
|
+
results.push({
|
|
76
|
+
host: 'gemini',
|
|
77
|
+
registered: gemini.registered,
|
|
78
|
+
path: geminiPath,
|
|
79
|
+
skipped: gemini.registered ? undefined : 'settings.json malformed or unreadable',
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const src = openCodePluginSource(bin);
|
|
83
|
+
for (const pluginPath of opencodeRailPluginPaths(ctx)) {
|
|
84
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
85
|
+
fs.writeFileSync(pluginPath, src);
|
|
86
|
+
}
|
|
87
|
+
results.push({
|
|
88
|
+
host: 'opencode',
|
|
89
|
+
registered: true,
|
|
90
|
+
path: opencodeRailPluginPaths(ctx)[0],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
results.push({
|
|
95
|
+
host: 'opencode',
|
|
96
|
+
registered: false,
|
|
97
|
+
skipped: e.message,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const windsurfPath = windsurfHooksPath();
|
|
101
|
+
fs.mkdirSync(path.dirname(windsurfPath), { recursive: true });
|
|
102
|
+
const windsurf = registerWindsurfRailHook(windsurfPath, bin);
|
|
103
|
+
results.push({
|
|
104
|
+
host: 'windsurf',
|
|
105
|
+
registered: windsurf.registered,
|
|
106
|
+
path: windsurfPath,
|
|
107
|
+
skipped: windsurf.registered ? undefined : 'hooks.json malformed or unreadable',
|
|
108
|
+
});
|
|
109
|
+
const vscodePath = vscodeCopilotRailHookPath();
|
|
110
|
+
fs.mkdirSync(path.dirname(vscodePath), { recursive: true });
|
|
111
|
+
const vscode = registerVsCodeRailHook(vscodePath, bin);
|
|
112
|
+
results.push({
|
|
113
|
+
host: 'vscode',
|
|
114
|
+
registered: vscode.registered,
|
|
115
|
+
path: vscodePath,
|
|
116
|
+
skipped: vscode.registered ? undefined : 'hook file malformed or unreadable',
|
|
117
|
+
});
|
|
118
|
+
for (const { host, reason } of RAIL_UNSUPPORTED) {
|
|
119
|
+
results.push({ host, registered: false, skipped: reason });
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
/** Remove only Memtrace-owned Rail hook entries (not skills/MCP). */
|
|
124
|
+
export function uninstallRailHooks(ctx) {
|
|
125
|
+
const results = [];
|
|
126
|
+
const claudePath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
127
|
+
const claude = removeRailHookFromSettingsAt(claudePath);
|
|
128
|
+
results.push({ host: 'claude', registered: false, path: claudePath, skipped: claude.changed ? undefined : 'no rail hook' });
|
|
129
|
+
const cursorPath = cursorHooksPath(ctx);
|
|
130
|
+
const cursor = removeCursorRailHook(cursorPath);
|
|
131
|
+
results.push({ host: 'cursor', registered: false, path: cursorPath, skipped: cursor.changed ? undefined : 'no rail hook' });
|
|
132
|
+
const codexPath = codexHooksPath(ctx);
|
|
133
|
+
const codex = removeSettingsHook(codexPath, 'PreToolUse');
|
|
134
|
+
results.push({ host: 'codex', registered: false, path: codexPath, skipped: codex.changed ? undefined : 'no rail hook' });
|
|
135
|
+
const geminiPath = geminiSettingsPath(ctx);
|
|
136
|
+
const gemini = removeSettingsHook(geminiPath, 'BeforeTool');
|
|
137
|
+
results.push({ host: 'gemini', registered: false, path: geminiPath, skipped: gemini.changed ? undefined : 'no rail hook' });
|
|
138
|
+
for (const p of opencodeRailPluginPaths(ctx)) {
|
|
139
|
+
try {
|
|
140
|
+
fs.rmSync(p, { force: true });
|
|
141
|
+
}
|
|
142
|
+
catch { /* best effort */ }
|
|
143
|
+
}
|
|
144
|
+
results.push({ host: 'opencode', registered: false });
|
|
145
|
+
const windsurfPath = windsurfHooksPath();
|
|
146
|
+
const windsurf = removeWindsurfRailHook(windsurfPath);
|
|
147
|
+
results.push({
|
|
148
|
+
host: 'windsurf',
|
|
149
|
+
registered: false,
|
|
150
|
+
path: windsurfPath,
|
|
151
|
+
skipped: windsurf.changed ? undefined : 'no rail hook',
|
|
152
|
+
});
|
|
153
|
+
const vscodePath = vscodeCopilotRailHookPath();
|
|
154
|
+
const vscode = removeVsCodeRailHook(vscodePath);
|
|
155
|
+
results.push({
|
|
156
|
+
host: 'vscode',
|
|
157
|
+
registered: false,
|
|
158
|
+
path: vscodePath,
|
|
159
|
+
skipped: vscode.changed ? undefined : 'no rail hook',
|
|
160
|
+
});
|
|
161
|
+
for (const { host, reason } of RAIL_UNSUPPORTED) {
|
|
162
|
+
results.push({ host, registered: false, skipped: reason });
|
|
163
|
+
}
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
export function printRailInstallSummary(results, action) {
|
|
167
|
+
console.log(`\n Memtrace Rail hooks ${action}:\n`);
|
|
168
|
+
for (const r of results) {
|
|
169
|
+
if (r.registered && action === 'installed') {
|
|
170
|
+
console.log(` \x1b[32m✓\x1b[0m ${r.host}${r.path ? ` → ${r.path}` : ''}`);
|
|
171
|
+
}
|
|
172
|
+
else if (action === 'removed' && r.skipped === undefined) {
|
|
173
|
+
console.log(` \x1b[32m✓\x1b[0m ${r.host} (removed)`);
|
|
174
|
+
}
|
|
175
|
+
else if (r.skipped) {
|
|
176
|
+
console.log(` \x1b[2m–\x1b[0m ${r.host}: ${r.skipped}`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log(` \x1b[33m!\x1b[0m ${r.host}: not ${action}${r.path ? ` (${r.path})` : ''}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
@@ -18,6 +18,27 @@ export interface SettingsMutationResult {
|
|
|
18
18
|
* Never overwrites a malformed file — backs it up and returns registered=false.
|
|
19
19
|
*/
|
|
20
20
|
export declare function registerMcpInSettingsAt(settingsPath: string, memtraceBinary: string): SettingsMutationResult;
|
|
21
|
+
/**
|
|
22
|
+
* Register the Memtrace Rail PreToolUse hook in a Claude settings.json.
|
|
23
|
+
*
|
|
24
|
+
* This is what makes Rail "part of memtrace" rather than a manual install:
|
|
25
|
+
* `memtrace install` wires a PreToolUse hook that pipes every Grep/Glob/Bash
|
|
26
|
+
* attempt through `memtrace route --hook`. The hook defaults to **observe**
|
|
27
|
+
* (zero behavior change) — `MEMTRACE_RAIL=nudge|rail|strict` opts into more.
|
|
28
|
+
* Mode is intentionally NOT baked into the command so the user can change it
|
|
29
|
+
* via env without re-installing.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: any prior Rail entry (identified by {@link RAIL_HOOK_MARKER}) is
|
|
32
|
+
* replaced, and unrelated PreToolUse hooks are preserved. Never overwrites a
|
|
33
|
+
* malformed file.
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerRailHookInSettingsAt(settingsPath: string, memtraceBinary: string): SettingsMutationResult;
|
|
36
|
+
/**
|
|
37
|
+
* Remove the Memtrace Rail PreToolUse hook from a Claude settings.json.
|
|
38
|
+
* Preserves unrelated hooks; prunes empty containers. Never touches a
|
|
39
|
+
* malformed file.
|
|
40
|
+
*/
|
|
41
|
+
export declare function removeRailHookFromSettingsAt(settingsPath: string): SettingsCleanupResult;
|
|
21
42
|
/**
|
|
22
43
|
* Merge-add plugin + marketplace entries into a Claude settings.json file.
|
|
23
44
|
* Never overwrites a malformed file.
|
|
@@ -216,6 +216,92 @@ export function registerMcpInSettingsAt(settingsPath, memtraceBinary) {
|
|
|
216
216
|
writeJsonAtomic(settingsPath, settings);
|
|
217
217
|
return { registered: true };
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The tool surfaces Rail intercepts. Native `Grep`/`Glob` are the high-volume
|
|
221
|
+
* offenders; `Bash` catches rg/grep/find/fd commands.
|
|
222
|
+
*/
|
|
223
|
+
const RAIL_HOOK_MATCHER = 'Grep|Glob|Bash';
|
|
224
|
+
/** Substring that identifies a memtrace-owned Rail hook entry (for idempotent
|
|
225
|
+
* re-register + clean removal). */
|
|
226
|
+
const RAIL_HOOK_MARKER = 'route --hook';
|
|
227
|
+
/**
|
|
228
|
+
* Register the Memtrace Rail PreToolUse hook in a Claude settings.json.
|
|
229
|
+
*
|
|
230
|
+
* This is what makes Rail "part of memtrace" rather than a manual install:
|
|
231
|
+
* `memtrace install` wires a PreToolUse hook that pipes every Grep/Glob/Bash
|
|
232
|
+
* attempt through `memtrace route --hook`. The hook defaults to **observe**
|
|
233
|
+
* (zero behavior change) — `MEMTRACE_RAIL=nudge|rail|strict` opts into more.
|
|
234
|
+
* Mode is intentionally NOT baked into the command so the user can change it
|
|
235
|
+
* via env without re-installing.
|
|
236
|
+
*
|
|
237
|
+
* Idempotent: any prior Rail entry (identified by {@link RAIL_HOOK_MARKER}) is
|
|
238
|
+
* replaced, and unrelated PreToolUse hooks are preserved. Never overwrites a
|
|
239
|
+
* malformed file.
|
|
240
|
+
*/
|
|
241
|
+
export function registerRailHookInSettingsAt(settingsPath, memtraceBinary) {
|
|
242
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
243
|
+
if (corrupted) {
|
|
244
|
+
console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Rail hook registration.`);
|
|
245
|
+
return { registered: false, backupPath };
|
|
246
|
+
}
|
|
247
|
+
const settings = (value ?? {});
|
|
248
|
+
settings.hooks = settings.hooks ?? {};
|
|
249
|
+
const existing = Array.isArray(settings.hooks.PreToolUse)
|
|
250
|
+
? settings.hooks.PreToolUse
|
|
251
|
+
: [];
|
|
252
|
+
// Drop any prior memtrace Rail entry so re-install doesn't duplicate it.
|
|
253
|
+
const preserved = existing.filter((entry) => !isRailHookEntry(entry));
|
|
254
|
+
preserved.push({
|
|
255
|
+
matcher: RAIL_HOOK_MATCHER,
|
|
256
|
+
hooks: [
|
|
257
|
+
{
|
|
258
|
+
type: 'command',
|
|
259
|
+
command: `${memtraceBinary} route --hook`,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
settings.hooks.PreToolUse = preserved;
|
|
264
|
+
writeJsonAtomic(settingsPath, settings);
|
|
265
|
+
return { registered: true };
|
|
266
|
+
}
|
|
267
|
+
/** True when a PreToolUse entry is a memtrace-owned Rail hook. */
|
|
268
|
+
function isRailHookEntry(entry) {
|
|
269
|
+
if (!isRecord(entry) || !Array.isArray(entry.hooks))
|
|
270
|
+
return false;
|
|
271
|
+
return entry.hooks.some((h) => isRecord(h) && typeof h.command === 'string' && h.command.includes(RAIL_HOOK_MARKER));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Remove the Memtrace Rail PreToolUse hook from a Claude settings.json.
|
|
275
|
+
* Preserves unrelated hooks; prunes empty containers. Never touches a
|
|
276
|
+
* malformed file.
|
|
277
|
+
*/
|
|
278
|
+
export function removeRailHookFromSettingsAt(settingsPath) {
|
|
279
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
280
|
+
if (corrupted) {
|
|
281
|
+
console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Rail hook cleanup.`);
|
|
282
|
+
return { changed: false, backupPath };
|
|
283
|
+
}
|
|
284
|
+
if (!value)
|
|
285
|
+
return { changed: false };
|
|
286
|
+
const settings = value;
|
|
287
|
+
if (!isRecord(settings.hooks) || !Array.isArray(settings.hooks.PreToolUse)) {
|
|
288
|
+
return { changed: false };
|
|
289
|
+
}
|
|
290
|
+
const before = settings.hooks.PreToolUse;
|
|
291
|
+
const after = before.filter((entry) => !isRailHookEntry(entry));
|
|
292
|
+
if (after.length === before.length)
|
|
293
|
+
return { changed: false };
|
|
294
|
+
if (after.length === 0) {
|
|
295
|
+
delete settings.hooks.PreToolUse;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
settings.hooks.PreToolUse = after;
|
|
299
|
+
}
|
|
300
|
+
if (Object.keys(settings.hooks).length === 0)
|
|
301
|
+
delete settings.hooks;
|
|
302
|
+
writeJsonAtomic(settingsPath, settings);
|
|
303
|
+
return { changed: true };
|
|
304
|
+
}
|
|
219
305
|
/**
|
|
220
306
|
* Merge-add plugin + marketplace entries into a Claude settings.json file.
|
|
221
307
|
* Never overwrites a malformed file.
|
|
@@ -275,6 +361,22 @@ export function removeClaudeSettingsEntriesAt(settingsPath) {
|
|
|
275
361
|
if (Object.keys(mcpServers).length === 0)
|
|
276
362
|
delete settings.mcpServers;
|
|
277
363
|
}
|
|
364
|
+
// ─── memtrace-rail ─── Remove the Rail PreToolUse hook on uninstall.
|
|
365
|
+
if (isRecord(settings.hooks) && Array.isArray(settings.hooks.PreToolUse)) {
|
|
366
|
+
const pre = settings.hooks.PreToolUse;
|
|
367
|
+
const after = pre.filter((entry) => !isRailHookEntry(entry));
|
|
368
|
+
if (after.length !== pre.length) {
|
|
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
|
+
}
|
|
379
|
+
}
|
|
278
380
|
for (const containerName of MARKETPLACE_SETTING_CONTAINERS) {
|
|
279
381
|
const container = settings[containerName];
|
|
280
382
|
if (!isRecord(container))
|
|
@@ -321,12 +423,24 @@ function removeClaudeInstalledPluginMetadata() {
|
|
|
321
423
|
* This adds the MCP server config so Claude Code can connect to memtrace tools.
|
|
322
424
|
*/
|
|
323
425
|
async function registerMcpServer(memtraceBinaryPath) {
|
|
426
|
+
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
427
|
+
// ─── memtrace-rail ─── Always wire the Memtrace-first discovery hook so it
|
|
428
|
+
// ships as part of `memtrace install` (not a manual step). Hooks live in
|
|
429
|
+
// settings.json regardless of which MCP-registration strategy wins, and the
|
|
430
|
+
// hook defaults to observe mode (zero behavior change) until the user opts
|
|
431
|
+
// into MEMTRACE_RAIL=nudge|rail|strict. Failure here must not block MCP
|
|
432
|
+
// registration.
|
|
433
|
+
try {
|
|
434
|
+
registerRailHookInSettingsAt(settingsFile, memtraceBinaryPath);
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
console.warn(`memtrace: failed to register Rail hook: ${e.message}`);
|
|
438
|
+
}
|
|
324
439
|
// Strategy 1 (preferred): claude CLI's own add-json path
|
|
325
440
|
const viaCli = await tryMcpAddJson(memtraceBinaryPath);
|
|
326
441
|
if (viaCli)
|
|
327
442
|
return;
|
|
328
443
|
// Strategy 2 (fallback): direct settings.json merge (safe + atomic)
|
|
329
|
-
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
330
444
|
registerMcpInSettingsAt(settingsFile, memtraceBinaryPath);
|
|
331
445
|
}
|
|
332
446
|
/**
|
|
@@ -380,8 +494,9 @@ export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
|
380
494
|
enablePluginInSettings();
|
|
381
495
|
// Step 6: Register MCP server
|
|
382
496
|
await registerMcpServer(memtraceBinaryPath);
|
|
383
|
-
//
|
|
384
|
-
|
|
497
|
+
// Skills ship via the memtrace-skills@memtrace plugin only. Writing
|
|
498
|
+
// bare copies to ~/.claude/skills/memtrace-* duplicates every skill
|
|
499
|
+
// in Claude Code when the plugin is also enabled (issue #11).
|
|
385
500
|
return { cacheDir, skillCount: skills.length };
|
|
386
501
|
}
|
|
387
502
|
/**
|
|
@@ -2,7 +2,12 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { commandExists, execCommand } from '../utils.js';
|
|
5
|
+
import { registerSettingsHook, removeSettingsHook, CODEX_MATCHER, } from './rail-hooks.js';
|
|
5
6
|
const MCP_SERVER_NAME = 'memtrace';
|
|
7
|
+
function codexRailHooksPath(ctx) {
|
|
8
|
+
const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
|
|
9
|
+
return path.join(base, 'hooks.json');
|
|
10
|
+
}
|
|
6
11
|
function skillsRoot(ctx) {
|
|
7
12
|
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
8
13
|
return path.join(base, '.agents', 'skills');
|
|
@@ -173,6 +178,14 @@ export const codexTransformer = {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
}
|
|
181
|
+
// ─── memtrace-rail ─── Wire the discovery hook into .codex/hooks.json
|
|
182
|
+
// (PreToolUse, Bash matcher — Codex code discovery runs through the shell).
|
|
183
|
+
try {
|
|
184
|
+
registerSettingsHook(codexRailHooksPath(ctx), ctx.memtraceBinary, 'codex', 'PreToolUse', CODEX_MATCHER);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
warnings.push(`failed to register Codex Rail hook: ${e.message}`);
|
|
188
|
+
}
|
|
176
189
|
return {
|
|
177
190
|
agent: 'codex',
|
|
178
191
|
skillsWritten: skills.length,
|
|
@@ -191,6 +204,10 @@ export const codexTransformer = {
|
|
|
191
204
|
}
|
|
192
205
|
}
|
|
193
206
|
}
|
|
207
|
+
try {
|
|
208
|
+
removeSettingsHook(codexRailHooksPath(ctx), 'PreToolUse');
|
|
209
|
+
}
|
|
210
|
+
catch { /* best effort */ }
|
|
194
211
|
const configFile = codexConfigPath(ctx);
|
|
195
212
|
if (fs.existsSync(configFile)) {
|
|
196
213
|
const next = stripCodexMcpServer(fs.readFileSync(configFile, 'utf-8'));
|
|
@@ -3,6 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
5
5
|
import { MEMTRACE_MCP_ENV } from './shared.js';
|
|
6
|
+
import { registerCursorRailHook, removeCursorRailHook } from './rail-hooks.js';
|
|
6
7
|
function skillsRoot(ctx) {
|
|
7
8
|
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
8
9
|
return path.join(base, '.cursor', 'skills');
|
|
@@ -11,6 +12,10 @@ function mcpPath(ctx) {
|
|
|
11
12
|
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
12
13
|
return path.join(base, '.cursor', 'mcp.json');
|
|
13
14
|
}
|
|
15
|
+
function railHooksPath(ctx) {
|
|
16
|
+
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
17
|
+
return path.join(base, '.cursor', 'hooks.json');
|
|
18
|
+
}
|
|
14
19
|
function writeSkill(skill, rootDir) {
|
|
15
20
|
const name = skill.filename.replace(/\.md$/, '');
|
|
16
21
|
const outDir = path.join(rootDir, name);
|
|
@@ -37,7 +42,11 @@ export function registerCursorMcpAt(mcpFile, binary) {
|
|
|
37
42
|
const existingEnv = (isRecord(existing) && isRecord(existing.env))
|
|
38
43
|
? existing.env
|
|
39
44
|
: {};
|
|
40
|
-
const mergedEnv = {
|
|
45
|
+
const mergedEnv = {
|
|
46
|
+
...existingEnv,
|
|
47
|
+
...MEMTRACE_MCP_ENV,
|
|
48
|
+
MEMTRACE_WORKSPACE_ROOT: '${workspaceFolder}',
|
|
49
|
+
};
|
|
41
50
|
cfg.mcpServers['memtrace'] = {
|
|
42
51
|
command: binary,
|
|
43
52
|
args: ['mcp'],
|
|
@@ -64,6 +73,14 @@ export const cursorTransformer = {
|
|
|
64
73
|
const result = registerCursorMcpAt(mcpPath(ctx), ctx.memtraceBinary);
|
|
65
74
|
mcpRegistered = result.registered;
|
|
66
75
|
}
|
|
76
|
+
// ─── memtrace-rail ─── Wire the Memtrace-first discovery hook (default
|
|
77
|
+
// observe). Failure must not block the rest of the install.
|
|
78
|
+
try {
|
|
79
|
+
registerCursorRailHook(railHooksPath(ctx), ctx.memtraceBinary);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.warn(`memtrace: failed to register Cursor Rail hook: ${e.message}`);
|
|
83
|
+
}
|
|
67
84
|
return {
|
|
68
85
|
agent: 'cursor',
|
|
69
86
|
skillsWritten: skills.length,
|
|
@@ -83,6 +100,11 @@ export const cursorTransformer = {
|
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
}
|
|
103
|
+
// 1b. Remove the Rail hook.
|
|
104
|
+
try {
|
|
105
|
+
removeCursorRailHook(railHooksPath(ctx));
|
|
106
|
+
}
|
|
107
|
+
catch { /* best effort */ }
|
|
86
108
|
// 2. Remove memtrace entry from mcp.json; delete file if empty
|
|
87
109
|
const mcpFile = mcpPath(ctx);
|
|
88
110
|
const { value, corrupted } = safeReadJson(mcpFile);
|