memtrace 0.8.2 → 0.8.4
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 +43 -15
- package/hooks/posttool-mcp-telemetry.sh +0 -0
- package/hooks/userprompt-claude.sh +0 -0
- package/installer/dist/commands/doctor.js +11 -0
- package/installer/dist/index.js +1 -1
- package/installer/dist/rail-install.js +4 -15
- package/installer/dist/transformers/codex.js +7 -80
- package/installer/dist/transformers/index.d.ts +2 -1
- package/installer/dist/transformers/index.js +3 -1
- package/installer/dist/transformers/opencode.js +5 -21
- package/installer/dist/transformers/rail-hooks.d.ts +13 -0
- package/installer/dist/transformers/rail-hooks.js +50 -0
- package/installer/dist/transformers/types.d.ts +1 -1
- package/installer/dist/transformers/warp.d.ts +4 -0
- package/installer/dist/transformers/warp.js +83 -0
- package/installer/package.json +1 -1
- package/installer/skills/commands/memtrace-decision-recall.md +7 -4
- package/installer/skills/commands/memtrace-docs-ask.md +99 -0
- package/installer/skills/commands/memtrace-docs-read.md +91 -0
- package/installer/skills/commands/memtrace-docs-search.md +94 -0
- package/installer/skills/commands/memtrace-preflight.md +17 -1
- package/installer/skills/commands/memtrace-provenance.md +7 -4
- package/installer/skills/workflows/memtrace-change-impact-analysis.md +23 -7
- package/installer/skills/workflows/memtrace-decision-memory.md +16 -12
- package/installer/skills/workflows/memtrace-docs.md +129 -0
- package/installer/skills/workflows/memtrace-first.md +25 -3
- package/installer/skills/workflows/memtrace-refactoring-guide.md +25 -9
- package/lib/child-supervisor.js +104 -0
- package/lib/cuda-ep.js +6 -1
- package/lib/install-consent.js +1 -1
- package/package.json +7 -7
- package/skills/commands/memtrace-decision-recall.md +7 -4
- package/skills/commands/memtrace-docs-ask.md +99 -0
- package/skills/commands/memtrace-docs-read.md +91 -0
- package/skills/commands/memtrace-docs-search.md +94 -0
- package/skills/commands/memtrace-preflight.md +17 -1
- package/skills/commands/memtrace-provenance.md +7 -4
- package/skills/workflows/memtrace-change-impact-analysis.md +23 -7
- package/skills/workflows/memtrace-decision-memory.md +16 -12
- package/skills/workflows/memtrace-docs.md +129 -0
- package/skills/workflows/memtrace-first.md +25 -3
- package/skills/workflows/memtrace-refactoring-guide.md +25 -9
package/bin/memtrace.js
CHANGED
|
@@ -17,6 +17,17 @@ const { fetchLatestVersion, readCachedVersion } = require("../lib/update-check")
|
|
|
17
17
|
// is the silent-disappearance bug from /tmp/mt-debug/exit.
|
|
18
18
|
const { propagateChildExit } = require("../lib/exit-code");
|
|
19
19
|
|
|
20
|
+
// ─── Signal-forwarding supervisor (the "npx memtrace start isn't working" fix) ──
|
|
21
|
+
// Spawns the native binary async and forwards SIGINT/SIGTERM/SIGHUP from
|
|
22
|
+
// this shim to the child. Pre-fix, `mcp` forwarded NO signals and the
|
|
23
|
+
// `else` branch (which serves `start`) used `spawnSync` — so a SIGTERM
|
|
24
|
+
// from npx / a supervisor / a terminal close killed the Node shim but
|
|
25
|
+
// orphaned the Rust `memtrace start` daemon (still holding port 3030 +
|
|
26
|
+
// the MemDB lock + the embed model). The next `start` then hit
|
|
27
|
+
// "already running" / port-in-use. See lib/child-supervisor.js + its
|
|
28
|
+
// unit/property/e2e tests.
|
|
29
|
+
const { forwardSignalsTo } = require("../lib/child-supervisor");
|
|
30
|
+
|
|
20
31
|
// ── Handle `memtrace uninstall` before delegating to the Rust binary ────────
|
|
21
32
|
// npm v7+ does NOT fire preuninstall hooks for global packages (npm/cli#3042).
|
|
22
33
|
// This command provides reliable cleanup users can run before `npm uninstall -g`.
|
|
@@ -352,11 +363,14 @@ async function maybePromptForUpgrade(command) {
|
|
|
352
363
|
|
|
353
364
|
if (args[0] === "mcp") {
|
|
354
365
|
// MCP mode: async spawn so Node's event loop keeps running long enough
|
|
355
|
-
// for the update check to print its notice to the agent's stderr
|
|
366
|
+
// for the update check to print its notice to the agent's stderr, AND so
|
|
367
|
+
// forwardSignalsTo can route supervisor/terminal signals to the child
|
|
368
|
+
// (pre-fix the mcp branch orphaned the server on SIGTERM-to-shim).
|
|
356
369
|
const child = spawn(binaryPath, args, {
|
|
357
370
|
stdio: "inherit",
|
|
358
371
|
env: process.env,
|
|
359
372
|
});
|
|
373
|
+
forwardSignalsTo(child);
|
|
360
374
|
|
|
361
375
|
child.on("error", (e) => {
|
|
362
376
|
process.stderr.write(`memtrace: failed to start: ${e.message}\n`);
|
|
@@ -372,12 +386,21 @@ if (args[0] === "mcp") {
|
|
|
372
386
|
});
|
|
373
387
|
|
|
374
388
|
} else {
|
|
375
|
-
// All other commands
|
|
376
|
-
// user a chance to upgrade if cached check found a
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
389
|
+
// All other commands (notably the long-running `memtrace start` daemon):
|
|
390
|
+
// FIRST give the user a chance to upgrade if the cached check found a
|
|
391
|
+
// newer version (gated on TTY + start/index + cache says newer — see
|
|
392
|
+
// lib/update-prompt.js). When the user accepts, maybePromptForUpgrade
|
|
393
|
+
// calls process.exit() and we never reach the spawn below.
|
|
394
|
+
//
|
|
395
|
+
// Then spawn the native binary ASYNC with signal forwarding — not
|
|
396
|
+
// `spawnSync`. Pre-fix this branch used `spawnSync`, which blocks Node
|
|
397
|
+
// and cannot forward a SIGTERM/SIGHUP delivered to the shim itself (only
|
|
398
|
+
// a foreground-process-group Ctrl-C reached the child). Under
|
|
399
|
+
// `npx memtrace@latest start`, a supervisor SIGTERM or terminal close
|
|
400
|
+
// killed the Node shim and orphaned the Rust daemon (port 3030 stuck),
|
|
401
|
+
// so the next `start` reported "already running" / port-in-use. The
|
|
402
|
+
// async spawn + forwardSignalsTo pair is the fix; see
|
|
403
|
+
// lib/child-supervisor.js + test/child-supervisor.test.js.
|
|
381
404
|
(async () => {
|
|
382
405
|
try {
|
|
383
406
|
await maybePromptForUpgrade(args[0]);
|
|
@@ -387,17 +410,22 @@ if (args[0] === "mcp") {
|
|
|
387
410
|
`[memtrace] Update prompt failed (continuing): ${e.message}\n`,
|
|
388
411
|
);
|
|
389
412
|
}
|
|
390
|
-
const
|
|
413
|
+
const child = spawn(binaryPath, args, {
|
|
391
414
|
stdio: "inherit",
|
|
392
415
|
env: process.env,
|
|
393
416
|
});
|
|
394
|
-
|
|
395
|
-
|
|
417
|
+
forwardSignalsTo(child);
|
|
418
|
+
|
|
419
|
+
child.on("error", (e) => {
|
|
420
|
+
console.error(`Failed to run memtrace: ${e.message}`);
|
|
396
421
|
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
422
|
+
});
|
|
423
|
+
child.on("exit", (code, signal) => {
|
|
424
|
+
// ─── Leaf B T5 ─── same propagation contract as the mcp branch.
|
|
425
|
+
// The child's typed exit code (or 128+sig) is forwarded so a
|
|
426
|
+
// supervisor can distinguish a clean `start` shutdown (0) from
|
|
427
|
+
// SIGKILL (137) from the Phase-2 partial (75).
|
|
428
|
+
process.exit(propagateChildExit({ code, signal }));
|
|
429
|
+
});
|
|
402
430
|
})();
|
|
403
431
|
}
|
|
File without changes
|
|
File without changes
|
|
@@ -159,6 +159,17 @@ function checkAgent(agent) {
|
|
|
159
159
|
mcpConfigPath,
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
|
+
if (agent === 'warp') {
|
|
163
|
+
const skillsDir = path.join(os.homedir(), '.warp', 'skills');
|
|
164
|
+
const mcpConfigPath = path.join(os.homedir(), '.warp', '.mcp.json');
|
|
165
|
+
return {
|
|
166
|
+
agent,
|
|
167
|
+
skillsFound: countMemtraceSkills(skillsDir),
|
|
168
|
+
skillsDir,
|
|
169
|
+
mcpRegistered: mcpHasMemtrace(mcpConfigPath),
|
|
170
|
+
mcpConfigPath,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
162
173
|
const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
|
|
163
174
|
const mcpConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
164
175
|
return {
|
package/installer/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ function parseOnly(val) {
|
|
|
15
15
|
program
|
|
16
16
|
.command('install')
|
|
17
17
|
.description('Install memtrace skills and register MCP for selected agents')
|
|
18
|
-
.option('--only <agents>', 'comma-separated agent names (claude,cursor,codex,gemini,windsurf,vscode,hermes,opencode,kiro)', parseOnly)
|
|
18
|
+
.option('--only <agents>', 'comma-separated agent names (claude,cursor,codex,gemini,windsurf,vscode,hermes,opencode,kiro,warp)', parseOnly)
|
|
19
19
|
.option('--local', 'install into the current project where the selected agent supports project scope', false)
|
|
20
20
|
.option('--global', 'install globally (~/.claude/, ~/.cursor/, ~/.agents/) [default]', false)
|
|
21
21
|
.option('--skip-mcp', 'write skills only, skip MCP server registration', false)
|
|
@@ -8,7 +8,7 @@ import os from 'os';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { registerRailHookInSettingsAt, removeRailHookFromSettingsAt, } from './transformers/claude.js';
|
|
10
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';
|
|
11
|
+
import { registerCursorRailHook, removeCursorRailHook, registerSettingsHook, removeSettingsHook, registerWindsurfRailHook, removeWindsurfRailHook, registerVsCodeRailHook, removeVsCodeRailHook, windsurfHooksPath, vscodeCopilotRailHookPath, CODEX_MATCHER, GEMINI_MATCHER, openCodeRailPluginPaths, openCodePluginSource, } from './transformers/rail-hooks.js';
|
|
12
12
|
/** Hosts with no Rail hook surface yet (MCP/skills only). */
|
|
13
13
|
export const RAIL_UNSUPPORTED = [
|
|
14
14
|
{ host: 'hermes', reason: 'Hermes pre_tool_call adapter not wired yet' },
|
|
@@ -22,17 +22,6 @@ function cursorHooksPath(ctx) {
|
|
|
22
22
|
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
23
23
|
return path.join(base, '.cursor', 'hooks.json');
|
|
24
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
25
|
/**
|
|
37
26
|
* Wire Rail discovery hooks for Claude, Cursor, Codex, Gemini, and OpenCode.
|
|
38
27
|
* Idempotent. Does not install skills or MCP — hooks only.
|
|
@@ -80,14 +69,14 @@ export function installRailHooks(ctx) {
|
|
|
80
69
|
});
|
|
81
70
|
try {
|
|
82
71
|
const src = openCodePluginSource(bin);
|
|
83
|
-
for (const pluginPath of
|
|
72
|
+
for (const pluginPath of openCodeRailPluginPaths(ctx)) {
|
|
84
73
|
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
85
74
|
fs.writeFileSync(pluginPath, src);
|
|
86
75
|
}
|
|
87
76
|
results.push({
|
|
88
77
|
host: 'opencode',
|
|
89
78
|
registered: true,
|
|
90
|
-
path:
|
|
79
|
+
path: openCodeRailPluginPaths(ctx)[0],
|
|
91
80
|
});
|
|
92
81
|
}
|
|
93
82
|
catch (e) {
|
|
@@ -135,7 +124,7 @@ export function uninstallRailHooks(ctx) {
|
|
|
135
124
|
const geminiPath = geminiSettingsPath(ctx);
|
|
136
125
|
const gemini = removeSettingsHook(geminiPath, 'BeforeTool');
|
|
137
126
|
results.push({ host: 'gemini', registered: false, path: geminiPath, skipped: gemini.changed ? undefined : 'no rail hook' });
|
|
138
|
-
for (const p of
|
|
127
|
+
for (const p of openCodeRailPluginPaths(ctx)) {
|
|
139
128
|
try {
|
|
140
129
|
fs.rmSync(p, { force: true });
|
|
141
130
|
}
|
|
@@ -4,7 +4,6 @@ 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';
|
|
8
7
|
function codexRailHooksPath(ctx) {
|
|
9
8
|
const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
|
|
10
9
|
return path.join(base, 'hooks.json');
|
|
@@ -38,82 +37,6 @@ function writeTextAtomic(filePath, content) {
|
|
|
38
37
|
fs.writeFileSync(tmpPath, content);
|
|
39
38
|
fs.renameSync(tmpPath, filePath);
|
|
40
39
|
}
|
|
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
|
-
}
|
|
117
40
|
function isMemtraceMcpTable(tableName) {
|
|
118
41
|
return tableName === `mcp_servers.${MCP_SERVER_NAME}`
|
|
119
42
|
|| tableName === `mcp_servers."${MCP_SERVER_NAME}"`
|
|
@@ -196,11 +119,15 @@ export function registerCodexMcpAt(configFile, binary) {
|
|
|
196
119
|
// after the freshly written parent block for readability.
|
|
197
120
|
const stripped = stripCodexMcpServer(existing);
|
|
198
121
|
const { rest, envBlock } = extractMemtraceEnvBlock(stripped);
|
|
199
|
-
|
|
122
|
+
// Bare `memtrace mcp` — no shim. The server now emits a Codex-safe, plain
|
|
123
|
+
// (annotation-free) tool response when the client identifies as Codex at
|
|
124
|
+
// `initialize` (MemtraceMcpServer / shape_tool_content), so Codex talks to the
|
|
125
|
+
// binary directly. This removes the Node shim that hand-configuring users
|
|
126
|
+
// (who wrote `command = "memtrace"` themselves) used to miss.
|
|
200
127
|
const block = [
|
|
201
128
|
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
202
|
-
`command = ${tomlString(
|
|
203
|
-
`args = [
|
|
129
|
+
`command = ${tomlString(binary)}`,
|
|
130
|
+
`args = ["mcp"]`,
|
|
204
131
|
].join('\n');
|
|
205
132
|
const parts = [];
|
|
206
133
|
if (rest)
|
|
@@ -8,7 +8,8 @@ import { vscodeTransformer } from './vscode.js';
|
|
|
8
8
|
import { hermesTransformer } from './hermes.js';
|
|
9
9
|
import { opencodeTransformer } from './opencode.js';
|
|
10
10
|
import { kiroTransformer } from './kiro.js';
|
|
11
|
+
import { warpTransformer } from './warp.js';
|
|
11
12
|
export declare const ALL_TRANSFORMERS: Transformer[];
|
|
12
13
|
export declare function findTransformer(name: string): Transformer | undefined;
|
|
13
|
-
export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
|
|
14
|
+
export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, warpTransformer, };
|
|
14
15
|
export type { Transformer, InstallContext, InstallResult, TransformResult } from './types.js';
|
|
@@ -7,6 +7,7 @@ import { vscodeTransformer } from './vscode.js';
|
|
|
7
7
|
import { hermesTransformer } from './hermes.js';
|
|
8
8
|
import { opencodeTransformer } from './opencode.js';
|
|
9
9
|
import { kiroTransformer } from './kiro.js';
|
|
10
|
+
import { warpTransformer } from './warp.js';
|
|
10
11
|
export const ALL_TRANSFORMERS = [
|
|
11
12
|
claudeTransformer,
|
|
12
13
|
cursorTransformer,
|
|
@@ -17,8 +18,9 @@ export const ALL_TRANSFORMERS = [
|
|
|
17
18
|
hermesTransformer,
|
|
18
19
|
opencodeTransformer,
|
|
19
20
|
kiroTransformer,
|
|
21
|
+
warpTransformer,
|
|
20
22
|
];
|
|
21
23
|
export function findTransformer(name) {
|
|
22
24
|
return ALL_TRANSFORMERS.find(t => t.name === name);
|
|
23
25
|
}
|
|
24
|
-
export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
|
|
26
|
+
export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, warpTransformer, };
|
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import { registerOpenCodeMcpAt, removeMemtraceSkills, removeOpenCodeMcpAt, writeSkills, } from './shared.js';
|
|
5
|
-
import { openCodePluginSource } from './rail-hooks.js';
|
|
6
|
-
function opencodeConfigDir() {
|
|
7
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config');
|
|
8
|
-
return path.join(xdg, 'opencode');
|
|
9
|
-
}
|
|
4
|
+
import { openCodeConfigDir, openCodeRailPluginPaths, openCodePluginSource, } from './rail-hooks.js';
|
|
10
5
|
function skillsRoot(ctx) {
|
|
11
6
|
return ctx.scope === 'global'
|
|
12
|
-
? path.join(
|
|
7
|
+
? path.join(openCodeConfigDir(ctx), 'skills')
|
|
13
8
|
: path.join(ctx.cwd, '.opencode', 'skills');
|
|
14
9
|
}
|
|
15
10
|
function mcpPath(ctx) {
|
|
16
11
|
return ctx.scope === 'global'
|
|
17
|
-
? path.join(
|
|
12
|
+
? path.join(openCodeConfigDir(ctx), 'opencode.json')
|
|
18
13
|
: path.join(ctx.cwd, 'opencode.json');
|
|
19
14
|
}
|
|
20
|
-
// OpenCode's plugin directory name changed across versions: current official
|
|
21
|
-
// docs use `plugins/` (plural) while older/community builds load `plugin/`
|
|
22
|
-
// (singular). We write the plugin to BOTH so it loads regardless of version;
|
|
23
|
-
// uninstall removes both. (Identical content — whichever the runtime reads.)
|
|
24
|
-
function railPluginPaths(ctx) {
|
|
25
|
-
const base = ctx.scope === 'global' ? opencodeConfigDir() : path.join(ctx.cwd, '.opencode');
|
|
26
|
-
return [
|
|
27
|
-
path.join(base, 'plugin', 'memtrace-rail.js'),
|
|
28
|
-
path.join(base, 'plugins', 'memtrace-rail.js'),
|
|
29
|
-
];
|
|
30
|
-
}
|
|
31
15
|
export const opencodeTransformer = {
|
|
32
16
|
name: 'opencode',
|
|
33
17
|
async install(skills, ctx) {
|
|
@@ -47,7 +31,7 @@ export const opencodeTransformer = {
|
|
|
47
31
|
// has no stdio hook). Defaults to observe; fail-open. Best effort.
|
|
48
32
|
try {
|
|
49
33
|
const src = openCodePluginSource(ctx.memtraceBinary);
|
|
50
|
-
for (const pluginPath of
|
|
34
|
+
for (const pluginPath of openCodeRailPluginPaths(ctx)) {
|
|
51
35
|
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
52
36
|
fs.writeFileSync(pluginPath, src);
|
|
53
37
|
}
|
|
@@ -67,7 +51,7 @@ export const opencodeTransformer = {
|
|
|
67
51
|
async uninstall(ctx) {
|
|
68
52
|
removeMemtraceSkills(skillsRoot(ctx));
|
|
69
53
|
removeOpenCodeMcpAt(mcpPath(ctx));
|
|
70
|
-
for (const p of
|
|
54
|
+
for (const p of openCodeRailPluginPaths(ctx)) {
|
|
71
55
|
try {
|
|
72
56
|
fs.rmSync(p, { force: true });
|
|
73
57
|
}
|
|
@@ -5,6 +5,19 @@ export declare const CLAUDE_MATCHER = "Grep|Glob|Bash";
|
|
|
5
5
|
export declare const CODEX_MATCHER = "Bash";
|
|
6
6
|
export declare const GEMINI_MATCHER = "run_shell_command|read_file|read_many_files|glob|search_file_content";
|
|
7
7
|
export declare function railCommand(binary: string, host: string): string;
|
|
8
|
+
export interface OpenCodePathContext {
|
|
9
|
+
scope: 'global' | 'local';
|
|
10
|
+
cwd: string;
|
|
11
|
+
}
|
|
12
|
+
export interface OpenCodePathOptions {
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
homedir?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function openCodeHomeDir(options?: OpenCodePathOptions): string;
|
|
18
|
+
export declare function openCodeGlobalConfigDir(options?: OpenCodePathOptions): string;
|
|
19
|
+
export declare function openCodeConfigDir(ctx: OpenCodePathContext, options?: OpenCodePathOptions): string;
|
|
20
|
+
export declare function openCodeRailPluginPaths(ctx: OpenCodePathContext, options?: OpenCodePathOptions): string[];
|
|
8
21
|
export declare function isRailHookEntry(entry: unknown): boolean;
|
|
9
22
|
export interface HookResult {
|
|
10
23
|
registered: boolean;
|
|
@@ -28,6 +28,56 @@ export const GEMINI_MATCHER = 'run_shell_command|read_file|read_many_files|glob|
|
|
|
28
28
|
export function railCommand(binary, host) {
|
|
29
29
|
return `${binary} route --hook --host ${host}`;
|
|
30
30
|
}
|
|
31
|
+
function trimmed(value) {
|
|
32
|
+
const clean = value?.trim();
|
|
33
|
+
return clean ? clean : undefined;
|
|
34
|
+
}
|
|
35
|
+
function windowsProfileHome(env) {
|
|
36
|
+
const userProfile = trimmed(env.USERPROFILE);
|
|
37
|
+
if (userProfile)
|
|
38
|
+
return userProfile;
|
|
39
|
+
const drive = trimmed(env.HOMEDRIVE);
|
|
40
|
+
const homePath = trimmed(env.HOMEPATH);
|
|
41
|
+
return drive && homePath ? `${drive}${homePath}` : undefined;
|
|
42
|
+
}
|
|
43
|
+
export function openCodeHomeDir(options = {}) {
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const platform = options.platform ?? process.platform;
|
|
46
|
+
const fallbackHome = trimmed(options.homedir ?? os.homedir());
|
|
47
|
+
const posixHome = trimmed(env.HOME);
|
|
48
|
+
const winHome = windowsProfileHome(env);
|
|
49
|
+
if (platform === 'win32') {
|
|
50
|
+
return winHome ?? posixHome ?? fallbackHome ?? '';
|
|
51
|
+
}
|
|
52
|
+
return posixHome ?? fallbackHome ?? winHome ?? '';
|
|
53
|
+
}
|
|
54
|
+
export function openCodeGlobalConfigDir(options = {}) {
|
|
55
|
+
const env = options.env ?? process.env;
|
|
56
|
+
const xdg = trimmed(env.XDG_CONFIG_HOME);
|
|
57
|
+
if (xdg)
|
|
58
|
+
return path.join(xdg, 'opencode');
|
|
59
|
+
const home = openCodeHomeDir(options);
|
|
60
|
+
if (!home) {
|
|
61
|
+
throw new Error('OpenCode config path unavailable: HOME or USERPROFILE is not set');
|
|
62
|
+
}
|
|
63
|
+
return path.join(home, '.config', 'opencode');
|
|
64
|
+
}
|
|
65
|
+
export function openCodeConfigDir(ctx, options = {}) {
|
|
66
|
+
return ctx.scope === 'global'
|
|
67
|
+
? openCodeGlobalConfigDir(options)
|
|
68
|
+
: path.join(ctx.cwd, '.opencode');
|
|
69
|
+
}
|
|
70
|
+
// OpenCode's plugin directory name changed across versions: current official
|
|
71
|
+
// docs use `plugins/` (plural) while older/community builds load `plugin/`
|
|
72
|
+
// (singular). We write the plugin to BOTH so it loads regardless of version;
|
|
73
|
+
// uninstall removes both. (Identical content — whichever the runtime reads.)
|
|
74
|
+
export function openCodeRailPluginPaths(ctx, options = {}) {
|
|
75
|
+
const base = openCodeConfigDir(ctx, options);
|
|
76
|
+
return [
|
|
77
|
+
path.join(base, 'plugin', 'memtrace-rail.js'),
|
|
78
|
+
path.join(base, 'plugins', 'memtrace-rail.js'),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
31
81
|
function isRecord(v) {
|
|
32
82
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
33
83
|
}
|
|
@@ -29,7 +29,7 @@ export interface InstallResult {
|
|
|
29
29
|
mcpRegistered: boolean;
|
|
30
30
|
warnings: string[];
|
|
31
31
|
}
|
|
32
|
-
export type AgentName = 'claude' | 'cursor' | 'codex' | 'gemini' | 'windsurf' | 'vscode' | 'hermes' | 'opencode' | 'kiro';
|
|
32
|
+
export type AgentName = 'claude' | 'cursor' | 'codex' | 'gemini' | 'windsurf' | 'vscode' | 'hermes' | 'opencode' | 'kiro' | 'warp';
|
|
33
33
|
/**
|
|
34
34
|
* One transformer per supported AI coding agent.
|
|
35
35
|
*/
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { registerMcpServersJsonAt, removeMcpServersJsonAt, removeMemtraceSkills, writeSkills, } from './shared.js';
|
|
4
|
+
// Warp (https://docs.warp.dev/agent-platform/capabilities/mcp/) is a terminal
|
|
5
|
+
// with a built-in agent that reads the open Agent Skills format (SKILL.md +
|
|
6
|
+
// name/description frontmatter) and stdio MCP servers from JSON config.
|
|
7
|
+
//
|
|
8
|
+
// Two things make Warp's wiring different from the other transformers:
|
|
9
|
+
//
|
|
10
|
+
// 1. Skills + MCP both live under `~/.warp/` (global) or `<cwd>/.warp/`
|
|
11
|
+
// (project). We write to Warp's NATIVE provider paths rather than the
|
|
12
|
+
// `~/.agents/` "Other agents" paths, because the Warp provider row
|
|
13
|
+
// auto-spawns global servers by default — the "Other agents" row
|
|
14
|
+
// requires the user to toggle "Auto-spawn servers from third-party
|
|
15
|
+
// agents" on. Native paths = MCP works on first launch, zero toggling.
|
|
16
|
+
//
|
|
17
|
+
// 2. The MCP config file is a dotfile: `~/.warp/.mcp.json` (note the
|
|
18
|
+
// leading dot on the filename). The shared JSON helpers don't care
|
|
19
|
+
// about the basename — they read/merge/write atomically — so the only
|
|
20
|
+
// thing we customize is the path.
|
|
21
|
+
//
|
|
22
|
+
// Warp uses the same `mcpServers` schema as Claude Code / Windsurf / Kiro
|
|
23
|
+
// (each entry: `{ command, args, env? }`), so we reuse
|
|
24
|
+
// `registerMcpServersJsonAt` / `removeMcpServersJsonAt` from shared.ts —
|
|
25
|
+
// corrupted-file backup, unrelated-server preservation, and empty-file
|
|
26
|
+
// deletion on uninstall all come for free.
|
|
27
|
+
function warpRoot(ctx) {
|
|
28
|
+
return ctx.scope === 'global'
|
|
29
|
+
? path.join(os.homedir(), '.warp')
|
|
30
|
+
: path.join(ctx.cwd, '.warp');
|
|
31
|
+
}
|
|
32
|
+
function skillsRoot(ctx) {
|
|
33
|
+
return path.join(warpRoot(ctx), 'skills');
|
|
34
|
+
}
|
|
35
|
+
/** Global Warp MCP config: `~/.warp/.mcp.json` (auto-spawns by default). */
|
|
36
|
+
export function warpMcpPath() {
|
|
37
|
+
return path.join(os.homedir(), '.warp', '.mcp.json');
|
|
38
|
+
}
|
|
39
|
+
function mcpPath(ctx) {
|
|
40
|
+
// Global: the Warp provider file (auto-spawns). Local: project-scoped
|
|
41
|
+
// `.warp/.mcp.json` at the repo root — Warp detects these but NEVER
|
|
42
|
+
// auto-spawns them (security: a cloned repo can't start arbitrary local
|
|
43
|
+
// commands). The install warning below tells the user to toggle it on.
|
|
44
|
+
return ctx.scope === 'global'
|
|
45
|
+
? warpMcpPath()
|
|
46
|
+
: path.join(ctx.cwd, '.warp', '.mcp.json');
|
|
47
|
+
}
|
|
48
|
+
export const warpTransformer = {
|
|
49
|
+
name: 'warp',
|
|
50
|
+
async install(skills, ctx) {
|
|
51
|
+
const rootDir = skillsRoot(ctx);
|
|
52
|
+
const count = writeSkills(skills, rootDir);
|
|
53
|
+
const warnings = [];
|
|
54
|
+
let mcpRegistered = false;
|
|
55
|
+
const mcpConfigPath = mcpPath(ctx);
|
|
56
|
+
if (!ctx.skipMcp) {
|
|
57
|
+
const result = registerMcpServersJsonAt(mcpConfigPath, ctx.memtraceBinary);
|
|
58
|
+
mcpRegistered = result.registered;
|
|
59
|
+
if (!mcpRegistered && result.backupPath) {
|
|
60
|
+
warnings.push(`Warp MCP config was malformed; backed up to ${result.backupPath}.`);
|
|
61
|
+
}
|
|
62
|
+
// Project-scoped Warp servers never auto-spawn — the user must toggle
|
|
63
|
+
// the server on from Settings > Agents > MCP servers after install.
|
|
64
|
+
// Surface that so a `--local` install doesn't look like a no-op.
|
|
65
|
+
if (ctx.scope === 'local') {
|
|
66
|
+
warnings.push('Warp project-scoped MCP servers never auto-spawn — ' +
|
|
67
|
+
'enable "memtrace" under Settings > Agents > MCP servers in Warp.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
agent: 'warp',
|
|
72
|
+
skillsWritten: count,
|
|
73
|
+
skillsDir: rootDir,
|
|
74
|
+
mcpConfigPath,
|
|
75
|
+
mcpRegistered,
|
|
76
|
+
warnings,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
async uninstall(ctx) {
|
|
80
|
+
removeMemtraceSkills(skillsRoot(ctx));
|
|
81
|
+
removeMcpServersJsonAt(mcpPath(ctx));
|
|
82
|
+
},
|
|
83
|
+
};
|
package/installer/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memtrace-decision-recall
|
|
3
|
-
description: "Recall ranked decisions, bans, and conventions from Cortex decision memory by free-text query. Use when the user asks what was decided
|
|
3
|
+
description: "Recall ranked decisions, bans, and conventions from Cortex decision memory by free-text query through the normal Memtrace MCP server. Use when the user asks what was decided/chosen/rejected, whether there is a convention/ban/policy, and before any non-trivial edit/refactor/delete or re-picking a library, pattern, architecture, or subsystem behavior that may already be settled. Do not reconstruct decisions from git log or guesswork. To verify whether a known decision held, use memtrace-intent-verification; for symbol lineage/contracts, use memtrace-provenance."
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- mcp__memtrace__recall_decision
|
|
6
6
|
- mcp__memtrace__verify_intent
|
|
@@ -16,7 +16,8 @@ metadata:
|
|
|
16
16
|
`recall_decision` is the **free-text entry point** to decision memory. Given a query,
|
|
17
17
|
it returns the statistically-ranked set of decisions/conversations that bear on it —
|
|
18
18
|
including **bans** ("never use X", "don't do Y"), which are recorded as decisions.
|
|
19
|
-
Use it before re-litigating a settled choice
|
|
19
|
+
Use it before re-litigating a settled choice, changing existing behavior, or
|
|
20
|
+
contradicting a convention.
|
|
20
21
|
|
|
21
22
|
This is the one decision-memory tool that takes plain text. The ranked decisions it
|
|
22
23
|
returns carry the `decision_id`s the other tools (`verify_intent`, `get_arc`) need.
|
|
@@ -41,7 +42,8 @@ Full parameter spec for every Memtrace tool: `references/mcp-parameters.md` (bun
|
|
|
41
42
|
|
|
42
43
|
`recall_decision(query)` — `query` is free text. Use the noun phrase of the thing in
|
|
43
44
|
question: a library (`"redis vs in-memory cache"`), a pattern (`"error handling
|
|
44
|
-
strategy"`), a subsystem (`"auth tokens"`), or the exact
|
|
45
|
+
strategy"`), a subsystem (`"auth tokens"`), a symbol, or the exact edit/refactor/delete
|
|
46
|
+
you are about to do.
|
|
45
47
|
|
|
46
48
|
### 2. Read the ranked result
|
|
47
49
|
|
|
@@ -71,7 +73,8 @@ require a Decision node and will honestly return CannotProve. See
|
|
|
71
73
|
|
|
72
74
|
| Situation | Action |
|
|
73
75
|
|-----------|--------|
|
|
74
|
-
| About to
|
|
76
|
+
| About to edit/refactor/delete existing code where intent may matter | `recall_decision("<symbol/subsystem/behavior>")` FIRST — you may be crossing a recorded decision, ban, or convention |
|
|
77
|
+
| About to choose or replace a library/pattern/architecture | `recall_decision` FIRST — you may be undoing a deliberate choice or ban |
|
|
75
78
|
| User asks "did we decide X?" / "what's our convention on Y?" | `recall_decision("X" / "Y")` |
|
|
76
79
|
| You suspect a "don't do this" rule exists | `recall_decision` — bans are decisions and will surface |
|
|
77
80
|
| Recall returns a decision you're about to contradict | Surface it to the user verbatim; don't silently override |
|