memtrace-skills 1.2.9 → 1.2.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/dist/rail-hook.d.ts +7 -1
- package/dist/rail-hook.js +176 -57
- package/dist/rail-install.js +3 -6
- package/dist/rail-lifecycle.js +6 -3
- package/dist/rail-runtime.d.ts +37 -3
- package/dist/rail-runtime.js +79 -7
- package/dist/tool-names.js +6 -0
- package/dist/transformers/claude.d.ts +14 -2
- package/dist/transformers/claude.js +50 -23
- package/dist/transformers/opencode.js +6 -6
- package/dist/transformers/rail-hooks.d.ts +6 -1
- package/dist/transformers/rail-hooks.js +31 -9
- package/dist/transformers/shared.d.ts +2 -1
- package/dist/transformers/shared.js +22 -10
- package/dist/transformers/vscode.js +3 -1
- package/package.json +2 -2
- package/plugins/memtrace-skills/references/mcp-parameters.md +14 -3
- package/plugins/memtrace-skills/skills/memtrace-codebase-exploration/SKILL.md +4 -0
- package/plugins/memtrace-skills/skills/memtrace-daily/SKILL.md +4 -0
- package/plugins/memtrace-skills/skills/memtrace-first/SKILL.md +14 -8
- package/plugins/memtrace-skills/skills/memtrace-graph/SKILL.md +4 -0
- package/plugins/memtrace-skills/skills/memtrace-guided-refactor/SKILL.md +88 -0
- package/plugins/memtrace-skills/skills/memtrace-impact/SKILL.md +4 -0
- package/plugins/memtrace-skills/skills/memtrace-index/SKILL.md +21 -0
- package/plugins/memtrace-skills/skills/memtrace-preflight/SKILL.md +4 -0
- package/plugins/memtrace-skills/skills/memtrace-quality/SKILL.md +16 -2
- package/plugins/memtrace-skills/skills/memtrace-refactoring-guide/SKILL.md +4 -0
- package/plugins/memtrace-skills/skills/memtrace-search/SKILL.md +1 -0
- package/plugins/memtrace-skills/skills/memtrace-session-continuity/SKILL.md +4 -0
- package/skills/commands/memtrace-daily.md +4 -0
- package/skills/commands/memtrace-graph.md +4 -0
- package/skills/commands/memtrace-impact.md +4 -0
- package/skills/commands/memtrace-index.md +22 -0
- package/skills/commands/memtrace-preflight.md +4 -0
- package/skills/commands/memtrace-quality.md +19 -2
- package/skills/commands/memtrace-search.md +1 -0
- package/skills/workflows/memtrace-codebase-exploration.md +4 -0
- package/skills/workflows/memtrace-first.md +14 -8
- package/skills/workflows/memtrace-guided-refactor.md +89 -0
- package/skills/workflows/memtrace-refactoring-guide.md +4 -0
- package/skills/workflows/memtrace-session-continuity.md +4 -0
package/dist/rail-hook.d.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Post-phase gate, before any lease read or network call (design 4.1): only a
|
|
3
|
+
* native Grep, or a shell command that runs a search program and did not
|
|
4
|
+
* fail, is worth one loopback POST. Glob, Read and every other shell command
|
|
5
|
+
* exit here.
|
|
6
|
+
*/
|
|
7
|
+
export declare function enrichEligible(payload: any): boolean;
|
package/dist/rail-hook.js
CHANGED
|
@@ -1,77 +1,186 @@
|
|
|
1
1
|
/** Cached host hooks stop here when Memtrace is absent. No output and no network. */
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import http from 'node:http';
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { activeRailRuntimes, enrichEnabled, pidAlive, runtimeForCwd, ENRICH_HOOK_KILL_MS, SEARCH_COMMAND_RE, } from './rail-runtime.js';
|
|
9
|
+
const argv = process.argv;
|
|
10
|
+
function arg(name) {
|
|
11
|
+
const index = argv.indexOf(name);
|
|
12
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
13
|
+
}
|
|
14
|
+
function off(value) {
|
|
15
|
+
return value?.trim().toLowerCase() === 'off';
|
|
16
|
+
}
|
|
17
|
+
const SEARCH_TOOLS = new Set(['grep', 'search_file_content', 'grep_search']);
|
|
18
|
+
const SHELL_TOOLS = new Set(['bash', 'powershell', 'shell', 'run_shell_command', 'run_terminal_cmd', 'execute_command', 'run_in_terminal']);
|
|
19
|
+
/**
|
|
20
|
+
* Post-phase gate, before any lease read or network call (design 4.1): only a
|
|
21
|
+
* native Grep, or a shell command that runs a search program and did not
|
|
22
|
+
* fail, is worth one loopback POST. Glob, Read and every other shell command
|
|
23
|
+
* exit here.
|
|
24
|
+
*/
|
|
25
|
+
export function enrichEligible(payload) {
|
|
26
|
+
if (!payload || typeof payload !== 'object')
|
|
27
|
+
return false;
|
|
28
|
+
const tool = String(payload.tool_name ?? '').toLowerCase();
|
|
29
|
+
const input = payload.tool_input ?? {};
|
|
30
|
+
if (SEARCH_TOOLS.has(tool))
|
|
31
|
+
return true;
|
|
32
|
+
const command = typeof input.command === 'string' ? input.command
|
|
33
|
+
: (!tool && typeof payload.command === 'string' ? payload.command : undefined);
|
|
34
|
+
if (!(SHELL_TOOLS.has(tool) || (!tool && command !== undefined)))
|
|
35
|
+
return false;
|
|
36
|
+
if (typeof command !== 'string' || !SEARCH_COMMAND_RE.test(command))
|
|
37
|
+
return false;
|
|
38
|
+
// A failed command (exit >= 2, nothing printed) is not a search result.
|
|
39
|
+
// grep's exit 1 with no output is a real zero-match and stays eligible.
|
|
40
|
+
const response = payload.tool_response ?? payload.tool_output ?? {};
|
|
41
|
+
if (response && typeof response === 'object') {
|
|
42
|
+
const code = Number(response.exit_code ?? response.exitCode ?? response.returnCode);
|
|
43
|
+
const stdout = response.stdout ?? response.output ?? response.llmContent ?? '';
|
|
44
|
+
if (Number.isFinite(code) && code >= 2 && String(stdout).trim() === '')
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function ledgerDirectory() {
|
|
50
|
+
const home = process.env.MEMTRACE_HOME?.trim();
|
|
51
|
+
return home || path.join(os.homedir(), '.memtrace');
|
|
52
|
+
}
|
|
53
|
+
/** One timing row per enrichment hook call, written after stdout (design 10). */
|
|
54
|
+
function logHookTiming(row) {
|
|
55
|
+
if (process.env.MEMTRACE_TELEMETRY?.trim().toLowerCase() === 'off')
|
|
9
56
|
return;
|
|
57
|
+
try {
|
|
58
|
+
const dir = ledgerDirectory();
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
// One write per line: concurrent hooks never merge records.
|
|
61
|
+
fs.appendFileSync(path.join(dir, 'rail-enrich-hook.jsonl'), JSON.stringify(row) + '\n');
|
|
62
|
+
}
|
|
63
|
+
catch { /* timing is best effort */ }
|
|
64
|
+
}
|
|
65
|
+
/** POST to the daemon; resolves empty on timeout, error or daemon death. */
|
|
66
|
+
function postToDaemon(runtime, requestPath, input, killMs) {
|
|
67
|
+
return new Promise(resolve => {
|
|
68
|
+
let finished = false;
|
|
69
|
+
const finish = (value = { status: 0, body: '', headers: {} }) => {
|
|
70
|
+
if (finished)
|
|
71
|
+
return;
|
|
72
|
+
finished = true;
|
|
73
|
+
clearTimeout(deadline);
|
|
74
|
+
clearInterval(monitor);
|
|
75
|
+
resolve(value);
|
|
76
|
+
};
|
|
77
|
+
const request = http.request({ hostname: '127.0.0.1', port: runtime.port,
|
|
78
|
+
method: 'POST', path: requestPath,
|
|
79
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(input) },
|
|
80
|
+
}, response => {
|
|
81
|
+
let value = '';
|
|
82
|
+
response.on('data', chunk => {
|
|
83
|
+
value += chunk;
|
|
84
|
+
if (value.length > 2_000_000) {
|
|
85
|
+
finish();
|
|
86
|
+
request.destroy();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
response.on('error', () => finish());
|
|
90
|
+
response.on('end', () => finish({ status: response.statusCode ?? 0, body: value, headers: response.headers }));
|
|
91
|
+
});
|
|
92
|
+
const deadline = setTimeout(() => { finish(); request.destroy(); }, killMs);
|
|
93
|
+
const monitor = setInterval(() => {
|
|
94
|
+
if (!pidAlive(runtime.pid) || !pidAlive(runtime.guardianPid)) {
|
|
95
|
+
finish();
|
|
96
|
+
request.destroy();
|
|
97
|
+
}
|
|
98
|
+
}, 50);
|
|
99
|
+
request.on('error', () => finish());
|
|
100
|
+
request.end(input);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function readStdin() {
|
|
10
104
|
let input = '';
|
|
11
105
|
for await (const chunk of process.stdin) {
|
|
12
106
|
input += chunk;
|
|
13
107
|
if (input.length > 2_000_000)
|
|
14
|
-
return;
|
|
108
|
+
return undefined;
|
|
15
109
|
}
|
|
110
|
+
return input;
|
|
111
|
+
}
|
|
112
|
+
async function main() {
|
|
113
|
+
const started = Date.now();
|
|
114
|
+
const env = process.env;
|
|
115
|
+
if (off(env.MEMTRACE_RAIL) || env.MEMTRACE_RAW_SEARCH?.trim().toLowerCase() === 'allow')
|
|
116
|
+
return;
|
|
117
|
+
const host = arg('--host') ?? 'generic';
|
|
118
|
+
const phase = arg('--phase');
|
|
119
|
+
const event = arg('--event');
|
|
120
|
+
const enrichPath = phase === 'post' || event === 'precompact';
|
|
121
|
+
// Grep enrichment is opt-in: off means no stdin parse, no lease read, no HTTP.
|
|
122
|
+
if (enrichPath && !enrichEnabled(env))
|
|
123
|
+
return;
|
|
124
|
+
const input = await readStdin();
|
|
125
|
+
if (input === undefined)
|
|
126
|
+
return;
|
|
16
127
|
const payload = JSON.parse(input);
|
|
128
|
+
if (phase === 'post' && !enrichEligible(payload))
|
|
129
|
+
return;
|
|
17
130
|
const cwd = payload.cwd || payload.workspace_roots?.[0] || process.cwd();
|
|
18
131
|
if (typeof cwd !== 'string')
|
|
19
132
|
return;
|
|
20
|
-
|
|
133
|
+
// Leases are read ONCE per call.
|
|
134
|
+
const runtime = runtimeForCwd(cwd, activeRailRuntimes());
|
|
21
135
|
if (!runtime)
|
|
22
136
|
return;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
137
|
+
const alive = () => pidAlive(runtime.pid) && pidAlive(runtime.guardianPid);
|
|
138
|
+
if (event === 'precompact') {
|
|
139
|
+
const session = typeof payload.session_id === 'string' ? payload.session_id : '';
|
|
140
|
+
await postToDaemon(runtime, `/api/rail/enrich/reset?${new URLSearchParams({ session })}`, input, ENRICH_HOOK_KILL_MS);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (phase === 'post') {
|
|
144
|
+
const query = new URLSearchParams({ host, phase: 'post', enrich: env.MEMTRACE_ENRICH || '',
|
|
145
|
+
mode: env.MEMTRACE_RAIL || '', escape: env.MEMTRACE_RAW_SEARCH || '' });
|
|
146
|
+
const result = await postToDaemon(runtime, `/api/rail/enrich?${query}`, input, ENRICH_HOOK_KILL_MS);
|
|
147
|
+
let wrote = 0;
|
|
148
|
+
let malformed = false;
|
|
149
|
+
if (result.status === 200 && result.body && alive()) {
|
|
150
|
+
try {
|
|
151
|
+
JSON.parse(result.body); // never emit malformed endpoint output into an agent
|
|
152
|
+
process.stdout.write(result.body);
|
|
153
|
+
wrote = Buffer.byteLength(result.body);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
malformed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
logHookTiming({
|
|
160
|
+
ts: new Date().toISOString(), host, phase: 'post', tool: payload.tool_name ?? null,
|
|
161
|
+
status: result.status,
|
|
162
|
+
outcome: malformed ? 'error' : result.headers['x-memtrace-enrich-outcome'] ?? (result.status ? null : 'no_daemon'),
|
|
163
|
+
bytes: Number(result.headers['x-memtrace-enrich-bytes'] ?? 0), stdout_bytes: wrote,
|
|
164
|
+
server_ms: Number(result.headers['x-memtrace-enrich-server-ms'] ?? 0), hook_ms: Date.now() - started,
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const jsonMode = argv.includes('--json');
|
|
26
169
|
// JSON-mode/OpenCode and Windsurf retain their CLI response contracts.
|
|
27
170
|
// Other hosts use the in-process daemon router with no native binary spawn.
|
|
28
171
|
if (!jsonMode && host !== 'windsurf' && !runtime.routeBinary) {
|
|
29
|
-
const query = new URLSearchParams({ host, mode:
|
|
30
|
-
escape:
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return;
|
|
36
|
-
finished = true;
|
|
37
|
-
clearTimeout(deadline);
|
|
38
|
-
clearInterval(monitor);
|
|
39
|
-
resolve(value);
|
|
40
|
-
};
|
|
41
|
-
const request = http.request({ hostname: '127.0.0.1', port: runtime.port,
|
|
42
|
-
method: 'POST', path: `/api/rail/hook?${query}`,
|
|
43
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(input) },
|
|
44
|
-
}, response => {
|
|
45
|
-
let value = '';
|
|
46
|
-
response.on('data', chunk => {
|
|
47
|
-
value += chunk;
|
|
48
|
-
if (value.length > 2_000_000) {
|
|
49
|
-
finish();
|
|
50
|
-
request.destroy();
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
response.on('error', () => finish());
|
|
54
|
-
response.on('end', () => finish(response.statusCode === 200 ? value : ''));
|
|
55
|
-
});
|
|
56
|
-
const deadline = setTimeout(() => { finish(); request.destroy(); }, 5500);
|
|
57
|
-
const monitor = setInterval(() => {
|
|
58
|
-
if (!pidAlive(runtime.pid) || !pidAlive(runtime.guardianPid)) {
|
|
59
|
-
finish();
|
|
60
|
-
request.destroy();
|
|
61
|
-
}
|
|
62
|
-
}, 50);
|
|
63
|
-
request.on('error', () => finish());
|
|
64
|
-
request.end(input);
|
|
65
|
-
});
|
|
66
|
-
if (body && runtimeForCwd(cwd)) {
|
|
67
|
-
JSON.parse(body); // never emit malformed endpoint output into an agent
|
|
68
|
-
process.stdout.write(body);
|
|
172
|
+
const query = new URLSearchParams({ host, mode: env.MEMTRACE_RAIL || '',
|
|
173
|
+
escape: env.MEMTRACE_RAW_SEARCH || '' });
|
|
174
|
+
const result = await postToDaemon(runtime, `/api/rail/hook?${query}`, input, 5500);
|
|
175
|
+
if (result.status === 200 && result.body && alive()) {
|
|
176
|
+
JSON.parse(result.body); // never emit malformed endpoint output into an agent
|
|
177
|
+
process.stdout.write(result.body);
|
|
69
178
|
}
|
|
70
179
|
return;
|
|
71
180
|
}
|
|
72
181
|
const child = spawn(runtime.routeBinary || runtime.binary, jsonMode ? ['route', '--json', '-'] : ['route', '--hook', '--host', host], {
|
|
73
182
|
cwd, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
-
env: { ...
|
|
183
|
+
env: { ...env, MEMTRACE_RAIL_SEARCH_URL: `http://127.0.0.1:${runtime.port}` },
|
|
75
184
|
});
|
|
76
185
|
let output = '';
|
|
77
186
|
let stderr = '';
|
|
@@ -83,17 +192,15 @@ async function main() {
|
|
|
83
192
|
child.stdin.end(input);
|
|
84
193
|
const timer = setTimeout(() => child.kill(), 5500);
|
|
85
194
|
// If the daemon exits while a request is in flight, discard the response.
|
|
86
|
-
const monitor = setInterval(() => {
|
|
87
|
-
|
|
88
|
-
child.kill();
|
|
89
|
-
}, 50);
|
|
195
|
+
const monitor = setInterval(() => { if (!alive())
|
|
196
|
+
child.kill(); }, 50);
|
|
90
197
|
const code = await new Promise(resolve => {
|
|
91
198
|
child.once('error', () => resolve(null));
|
|
92
199
|
child.once('close', resolve);
|
|
93
200
|
});
|
|
94
201
|
clearTimeout(timer);
|
|
95
202
|
clearInterval(monitor);
|
|
96
|
-
if (!
|
|
203
|
+
if (!alive())
|
|
97
204
|
return;
|
|
98
205
|
if (code === 0)
|
|
99
206
|
process.stdout.write(output);
|
|
@@ -102,4 +209,16 @@ async function main() {
|
|
|
102
209
|
process.exitCode = 2;
|
|
103
210
|
}
|
|
104
211
|
}
|
|
105
|
-
|
|
212
|
+
// Only run as a hook process, not when a test imports `enrichEligible`.
|
|
213
|
+
const invokedDirectly = (() => {
|
|
214
|
+
try {
|
|
215
|
+
const self = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
216
|
+
return fs.realpathSync(path.resolve(process.argv[1] ?? '')) === self;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
if (invokedDirectly) {
|
|
223
|
+
main().catch(() => { });
|
|
224
|
+
}
|
package/dist/rail-install.js
CHANGED
|
@@ -9,7 +9,7 @@ import path from 'path';
|
|
|
9
9
|
import { railIsActive } from './rail-runtime.js';
|
|
10
10
|
import { registerRailHookInSettingsAt, removeRailHookFromSettingsAt, } from './transformers/claude.js';
|
|
11
11
|
import { geminiSettingsPath } from './transformers/gemini.js';
|
|
12
|
-
import { registerCursorRailHook, removeCursorRailHook, registerSettingsHook, removeSettingsHook, registerWindsurfRailHook, removeWindsurfRailHook, registerVsCodeRailHook, removeVsCodeRailHook, windsurfHooksPath, vscodeCopilotRailHookPath, CODEX_MATCHER, GEMINI_MATCHER, openCodeRailPluginPaths, openCodePluginSource, } from './transformers/rail-hooks.js';
|
|
12
|
+
import { registerCursorRailHook, removeCursorRailHook, registerSettingsHook, removeSettingsHook, registerWindsurfRailHook, removeWindsurfRailHook, registerVsCodeRailHook, removeVsCodeRailHook, windsurfHooksPath, vscodeCopilotRailHookPath, CODEX_MATCHER, GEMINI_MATCHER, openCodeRailPluginPaths, openCodePluginSource, writeOpenCodeRailPlugin, } from './transformers/rail-hooks.js';
|
|
13
13
|
/** Hosts with no Rail hook surface yet (MCP/skills only). */
|
|
14
14
|
export const RAIL_UNSUPPORTED = [
|
|
15
15
|
{ host: 'hermes', reason: 'Hermes pre_tool_call adapter not wired yet' },
|
|
@@ -73,14 +73,11 @@ export function installRailHooks(ctx) {
|
|
|
73
73
|
});
|
|
74
74
|
try {
|
|
75
75
|
const src = openCodePluginSource(bin);
|
|
76
|
-
|
|
77
|
-
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
78
|
-
fs.writeFileSync(pluginPath, src);
|
|
79
|
-
}
|
|
76
|
+
const pluginPath = writeOpenCodeRailPlugin(ctx, src);
|
|
80
77
|
results.push({
|
|
81
78
|
host: 'opencode',
|
|
82
79
|
registered: true,
|
|
83
|
-
path:
|
|
80
|
+
path: pluginPath,
|
|
84
81
|
});
|
|
85
82
|
}
|
|
86
83
|
catch (e) {
|
package/dist/rail-lifecycle.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { installRailHooks, uninstallRailHooks } from './rail-install.js';
|
|
6
|
-
import { runtimeDirectory, railDisabled, activeRailRuntimes, pidAlive, LEASE_REFRESH_INTERVAL_MS } from './rail-runtime.js';
|
|
6
|
+
import { runtimeDirectory, railDisabled, activeRailRuntimes, pidAlive, LEASE_REFRESH_INTERVAL_MS, enrichEnabled, railMode } from './rail-runtime.js';
|
|
7
7
|
import { writeJsonAtomic } from './fs-safe.js';
|
|
8
8
|
const arg = (name) => {
|
|
9
9
|
const index = process.argv.indexOf(name);
|
|
@@ -43,13 +43,16 @@ async function main() {
|
|
|
43
43
|
}
|
|
44
44
|
writeJsonAtomic(lease, { pid, guardianPid: process.pid, updatedAt: Date.now(), cwd, binary, port, routeBinary });
|
|
45
45
|
const enabled = !railDisabled();
|
|
46
|
-
|
|
46
|
+
// Grep enrichment and the Rail mode decide WHICH hooks are registered,
|
|
47
|
+
// so a rail.json change to either re-wires them without a restart.
|
|
48
|
+
const signature = enabled ? `on:${enrichEnabled()}:${railMode()}` : 'off';
|
|
49
|
+
if (signature === wired)
|
|
47
50
|
return;
|
|
48
51
|
if (enabled)
|
|
49
52
|
installRailHooks(ctx);
|
|
50
53
|
else
|
|
51
54
|
uninstallRailHooks(ctx);
|
|
52
|
-
wired =
|
|
55
|
+
wired = signature;
|
|
53
56
|
};
|
|
54
57
|
const stop = () => {
|
|
55
58
|
if (done)
|
package/dist/rail-runtime.d.ts
CHANGED
|
@@ -15,13 +15,47 @@ export declare function runtimeDirectory(): string;
|
|
|
15
15
|
export declare function railDisabled(): boolean;
|
|
16
16
|
export declare function persistHookOptOut(): void;
|
|
17
17
|
export declare function pidAlive(pid: number): boolean;
|
|
18
|
+
/** A lease this old whose owner and guardian are both gone is garbage. */
|
|
19
|
+
export declare const LEASE_PRUNE_AGE_MS = 60000;
|
|
20
|
+
/** Grep enrichment's hook kill (design section 10): the hook never waits longer. */
|
|
21
|
+
export declare const ENRICH_HOOK_KILL_MS = 600;
|
|
22
|
+
/**
|
|
23
|
+
* Cheap pre-network gate for shell commands: only these programs are code
|
|
24
|
+
* searches worth a loopback round trip (design 4.1). `git tag v1` must not
|
|
25
|
+
* match `ag`, so a program must start a word.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SEARCH_COMMAND_RE: RegExp;
|
|
28
|
+
export interface RailConfig {
|
|
29
|
+
mode?: string;
|
|
30
|
+
enrich?: boolean;
|
|
31
|
+
hooksEnabled?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** `~/.memtrace/rail.json` (or `MEMTRACE_RAIL_CONFIG`); `{}` when absent or unreadable. */
|
|
34
|
+
export declare function railConfig(): RailConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Grep enrichment on/off: `MEMTRACE_ENRICH`, then rail.json `"enrich"`, then
|
|
37
|
+
* OFF — the default stays off until the A/B gate passes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function enrichEnabled(env?: NodeJS.ProcessEnv, config?: RailConfig): boolean;
|
|
40
|
+
/** Rail mode: `MEMTRACE_RAIL`, then rail.json `"mode"`, then observe. */
|
|
41
|
+
export declare function railMode(env?: NodeJS.ProcessEnv, config?: RailConfig): string;
|
|
42
|
+
/**
|
|
43
|
+
* Live leases. Reading them also deletes garbage: a lease whose owner and
|
|
44
|
+
* guardian are both dead and whose heartbeat stopped over a minute ago.
|
|
45
|
+
* Every hook call reads every lease, and old daemons used to leave dozens.
|
|
46
|
+
*/
|
|
18
47
|
export declare function activeRailRuntimes(): RailRuntime[];
|
|
19
48
|
export declare function railIsActive(): boolean;
|
|
20
|
-
export declare function runtimeForCwd(cwd: string): RailRuntime | undefined;
|
|
21
|
-
|
|
49
|
+
export declare function runtimeForCwd(cwd: string, runtimes?: RailRuntime[]): RailRuntime | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Exec-form hook invocation. `extra` selects the hook's role: none for the
|
|
52
|
+
* Rail PreToolUse router, `--phase post` for grep enrichment, `--event
|
|
53
|
+
* precompact` for the enrichment dedupe reset.
|
|
54
|
+
*/
|
|
55
|
+
export declare function railHookInvocation(host: string, extra?: string[]): {
|
|
22
56
|
command: string;
|
|
23
57
|
args: string[];
|
|
24
58
|
};
|
|
25
59
|
/** POSIX and Windows hosts that accept only command text. Claude uses exec form. */
|
|
26
|
-
export declare function railHookCommand(host: string, platform?: NodeJS.Platform): string;
|
|
60
|
+
export declare function railHookCommand(host: string, platform?: NodeJS.Platform, extra?: string[]): string;
|
|
27
61
|
export declare function isRailCommand(command: string): boolean;
|
package/dist/rail-runtime.js
CHANGED
|
@@ -54,6 +54,64 @@ export function pidAlive(pid) {
|
|
|
54
54
|
return error.code === 'EPERM';
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
/** A lease this old whose owner and guardian are both gone is garbage. */
|
|
58
|
+
export const LEASE_PRUNE_AGE_MS = 60_000;
|
|
59
|
+
/** Grep enrichment's hook kill (design section 10): the hook never waits longer. */
|
|
60
|
+
export const ENRICH_HOOK_KILL_MS = 600;
|
|
61
|
+
/**
|
|
62
|
+
* Cheap pre-network gate for shell commands: only these programs are code
|
|
63
|
+
* searches worth a loopback round trip (design 4.1). `git tag v1` must not
|
|
64
|
+
* match `ag`, so a program must start a word.
|
|
65
|
+
*/
|
|
66
|
+
export const SEARCH_COMMAND_RE = /(^|[\s;|&(`])(rg|grep|egrep|fgrep|rgrep|ugrep|ug|ag|ack|git\s+grep)(\s|$)/;
|
|
67
|
+
/** `~/.memtrace/rail.json` (or `MEMTRACE_RAIL_CONFIG`); `{}` when absent or unreadable. */
|
|
68
|
+
export function railConfig() {
|
|
69
|
+
const configPath = process.env.MEMTRACE_RAIL_CONFIG?.trim()
|
|
70
|
+
|| path.join(os.homedir(), '.memtrace', 'rail.json');
|
|
71
|
+
try {
|
|
72
|
+
const value = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
73
|
+
return value && typeof value === 'object' ? value : {};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function flag(value) {
|
|
80
|
+
switch (value?.trim().toLowerCase()) {
|
|
81
|
+
case 'on':
|
|
82
|
+
case '1':
|
|
83
|
+
case 'true':
|
|
84
|
+
case 'yes':
|
|
85
|
+
case 'enabled': return true;
|
|
86
|
+
case 'off':
|
|
87
|
+
case '0':
|
|
88
|
+
case 'false':
|
|
89
|
+
case 'no':
|
|
90
|
+
case 'disabled': return false;
|
|
91
|
+
default: return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Grep enrichment on/off: `MEMTRACE_ENRICH`, then rail.json `"enrich"`, then
|
|
96
|
+
* OFF — the default stays off until the A/B gate passes.
|
|
97
|
+
*/
|
|
98
|
+
export function enrichEnabled(env = process.env, config = railConfig()) {
|
|
99
|
+
return flag(env.MEMTRACE_ENRICH) ?? (typeof config.enrich === 'boolean' ? config.enrich : false);
|
|
100
|
+
}
|
|
101
|
+
/** Rail mode: `MEMTRACE_RAIL`, then rail.json `"mode"`, then observe. */
|
|
102
|
+
export function railMode(env = process.env, config = railConfig()) {
|
|
103
|
+
const valid = ['off', 'observe', 'nudge', 'rail', 'strict'];
|
|
104
|
+
const fromEnv = env.MEMTRACE_RAIL?.trim().toLowerCase();
|
|
105
|
+
if (fromEnv && valid.includes(fromEnv))
|
|
106
|
+
return fromEnv;
|
|
107
|
+
const fromConfig = typeof config.mode === 'string' ? config.mode.trim().toLowerCase() : '';
|
|
108
|
+
return valid.includes(fromConfig) ? fromConfig : 'observe';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Live leases. Reading them also deletes garbage: a lease whose owner and
|
|
112
|
+
* guardian are both dead and whose heartbeat stopped over a minute ago.
|
|
113
|
+
* Every hook call reads every lease, and old daemons used to leave dozens.
|
|
114
|
+
*/
|
|
57
115
|
export function activeRailRuntimes() {
|
|
58
116
|
if (railDisabled())
|
|
59
117
|
return [];
|
|
@@ -61,8 +119,17 @@ export function activeRailRuntimes() {
|
|
|
61
119
|
const now = Date.now();
|
|
62
120
|
return fs.readdirSync(runtimeDirectory()).filter(name => name.endsWith('.json')).flatMap(name => {
|
|
63
121
|
try {
|
|
64
|
-
const
|
|
122
|
+
const file = path.join(runtimeDirectory(), name);
|
|
123
|
+
const runtime = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
65
124
|
const age = now - runtime.updatedAt;
|
|
125
|
+
if (Number.isFinite(age) && age > LEASE_PRUNE_AGE_MS
|
|
126
|
+
&& !pidAlive(runtime.pid) && !pidAlive(runtime.guardianPid)) {
|
|
127
|
+
try {
|
|
128
|
+
fs.rmSync(file, { force: true });
|
|
129
|
+
}
|
|
130
|
+
catch { /* best effort */ }
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
66
133
|
if (!Number.isFinite(age) || age < -1000 || age > LEASE_MAX_AGE_MS
|
|
67
134
|
|| !pidAlive(runtime.pid) || !pidAlive(runtime.guardianPid)
|
|
68
135
|
|| typeof runtime.binary !== 'string' || !path.isAbsolute(runtime.binary)
|
|
@@ -91,20 +158,25 @@ function normalizedWorkspacePath(directory) {
|
|
|
91
158
|
const normalized = resolved.replace(/\\/g, '/').replace(/\/$/, '');
|
|
92
159
|
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
93
160
|
}
|
|
94
|
-
export function runtimeForCwd(cwd) {
|
|
161
|
+
export function runtimeForCwd(cwd, runtimes = activeRailRuntimes()) {
|
|
95
162
|
const target = normalizedWorkspacePath(cwd);
|
|
96
|
-
return
|
|
163
|
+
return runtimes
|
|
97
164
|
.map(runtime => ({ runtime, root: normalizedWorkspacePath(runtime.cwd) }))
|
|
98
165
|
.filter(({ root }) => target === root || target.startsWith(root + '/'))
|
|
99
166
|
.sort((a, b) => b.root.length - a.root.length || b.runtime.updatedAt - a.runtime.updatedAt)[0]?.runtime;
|
|
100
167
|
}
|
|
101
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Exec-form hook invocation. `extra` selects the hook's role: none for the
|
|
170
|
+
* Rail PreToolUse router, `--phase post` for grep enrichment, `--event
|
|
171
|
+
* precompact` for the enrichment dedupe reset.
|
|
172
|
+
*/
|
|
173
|
+
export function railHookInvocation(host, extra = []) {
|
|
102
174
|
return { command: process.execPath,
|
|
103
|
-
args: [fileURLToPath(new URL('./rail-hook.js', import.meta.url)), '--host', host] };
|
|
175
|
+
args: [fileURLToPath(new URL('./rail-hook.js', import.meta.url)), '--host', host, ...extra] };
|
|
104
176
|
}
|
|
105
177
|
/** POSIX and Windows hosts that accept only command text. Claude uses exec form. */
|
|
106
|
-
export function railHookCommand(host, platform = process.platform) {
|
|
107
|
-
const invocation = railHookInvocation(host);
|
|
178
|
+
export function railHookCommand(host, platform = process.platform, extra = []) {
|
|
179
|
+
const invocation = railHookInvocation(host, extra);
|
|
108
180
|
const quote = platform === 'win32'
|
|
109
181
|
? (s) => '"' + s.replace(/\\/g, '/').replace(/"/g, '\\"') + '"'
|
|
110
182
|
: (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
package/dist/tool-names.js
CHANGED
|
@@ -18,6 +18,7 @@ export const MEMTRACE_TOOL_NAMES = Object.freeze([
|
|
|
18
18
|
'detect_changes',
|
|
19
19
|
'embed_diag',
|
|
20
20
|
'embed_reset_breaker',
|
|
21
|
+
'explain',
|
|
21
22
|
'export_query_audit',
|
|
22
23
|
'find_api_calls',
|
|
23
24
|
'find_api_endpoints',
|
|
@@ -66,6 +67,7 @@ export const MEMTRACE_TOOL_NAMES = Object.freeze([
|
|
|
66
67
|
'get_function_quality_metrics',
|
|
67
68
|
'get_impact',
|
|
68
69
|
'get_process_flow',
|
|
70
|
+
'get_query_revision',
|
|
69
71
|
'get_repository_stats',
|
|
70
72
|
'get_service_diagram',
|
|
71
73
|
'get_source_window',
|
|
@@ -74,6 +76,7 @@ export const MEMTRACE_TOOL_NAMES = Object.freeze([
|
|
|
74
76
|
'get_timeline',
|
|
75
77
|
'governing_contracts',
|
|
76
78
|
'governing_rules',
|
|
79
|
+
'guided_refactor',
|
|
77
80
|
'index_directory',
|
|
78
81
|
'ingest_graph_fragment',
|
|
79
82
|
'link_repositories',
|
|
@@ -86,10 +89,13 @@ export const MEMTRACE_TOOL_NAMES = Object.freeze([
|
|
|
86
89
|
'list_watched_paths',
|
|
87
90
|
'list_worktrees',
|
|
88
91
|
'mem_diag',
|
|
92
|
+
'pdg_query',
|
|
89
93
|
'preflight_check',
|
|
94
|
+
'query_graph',
|
|
90
95
|
'read_doc',
|
|
91
96
|
'recall_decision',
|
|
92
97
|
'record_external_episode',
|
|
98
|
+
'rename_symbol',
|
|
93
99
|
'replay_history',
|
|
94
100
|
'review_agent_sessions',
|
|
95
101
|
'review_github_pr',
|
|
@@ -19,7 +19,7 @@ export interface SettingsMutationResult {
|
|
|
19
19
|
* Merge-add the memtrace MCP server into a Claude settings.json file.
|
|
20
20
|
* Never overwrites a malformed file — backs it up and returns registered=false.
|
|
21
21
|
*/
|
|
22
|
-
export declare function registerMcpInSettingsAt(settingsPath: string, memtraceBinary: string, legacyEnv?: Record<string, string
|
|
22
|
+
export declare function registerMcpInSettingsAt(settingsPath: string, memtraceBinary: string, legacyEnv?: Record<string, string>, legacyArgs?: string[]): SettingsMutationResult;
|
|
23
23
|
/**
|
|
24
24
|
* Every host enters a local liveness guard before contacting Memtrace. The
|
|
25
25
|
* daemon lifecycle registers/removes this command; a cached registration is
|
|
@@ -41,7 +41,19 @@ export declare function claudeRailHookCommand(_memtraceBinary: string, platform?
|
|
|
41
41
|
* is replaced, and unrelated PreToolUse hooks are preserved. Never overwrites
|
|
42
42
|
* a malformed file.
|
|
43
43
|
*/
|
|
44
|
-
export declare function registerRailHookInSettingsAt(settingsPath: string, memtraceBinary: string, platform?: NodeJS.Platform): SettingsMutationResult;
|
|
44
|
+
export declare function registerRailHookInSettingsAt(settingsPath: string, memtraceBinary: string, platform?: NodeJS.Platform, options?: RailHookOptions): SettingsMutationResult;
|
|
45
|
+
/** Grep enrichment rides PostToolUse on these tools (design section 9). Claude Code on
|
|
46
|
+
* Windows runs shell commands through its PowerShell tool, so rg/grep/git grep there
|
|
47
|
+
* arrive as PowerShell calls. */
|
|
48
|
+
export declare const ENRICH_HOOK_MATCHER = "Grep|Bash|PowerShell";
|
|
49
|
+
/** Claude hook timeout for the enrichment hooks, in seconds. */
|
|
50
|
+
export declare const ENRICH_HOOK_TIMEOUT_S = 2;
|
|
51
|
+
export interface RailHookOptions {
|
|
52
|
+
/** Register the grep-enrichment hooks. Default: `MEMTRACE_ENRICH`, then rail.json, then off. */
|
|
53
|
+
enrich?: boolean;
|
|
54
|
+
/** The Rail mode the PreToolUse decision depends on. Default: env, then rail.json. */
|
|
55
|
+
mode?: string;
|
|
56
|
+
}
|
|
45
57
|
/**
|
|
46
58
|
* Remove the Memtrace Rail PreToolUse hook from a Claude settings.json.
|
|
47
59
|
* Preserves unrelated hooks; prunes empty containers. Never touches a
|