memtrace-skills 0.7.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/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.js +199 -0
- package/dist/commands/enterprise-install.d.ts +7 -0
- package/dist/commands/enterprise-install.js +129 -0
- package/dist/commands/install.d.ts +9 -0
- package/dist/commands/install.js +104 -0
- package/dist/commands/picker.d.ts +6 -0
- package/dist/commands/picker.js +22 -0
- package/dist/commands/rail-install.d.ts +7 -0
- package/dist/commands/rail-install.js +37 -0
- package/dist/fs-safe.d.ts +21 -0
- package/dist/fs-safe.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +87 -0
- package/dist/rail-install.d.ts +20 -0
- package/dist/rail-install.js +183 -0
- package/dist/skills.d.ts +17 -0
- package/dist/skills.js +64 -0
- package/dist/transformers/claude.d.ts +71 -0
- package/dist/transformers/claude.js +702 -0
- package/dist/transformers/codex.d.ts +8 -0
- package/dist/transformers/codex.js +294 -0
- package/dist/transformers/cursor.d.ts +7 -0
- package/dist/transformers/cursor.js +124 -0
- package/dist/transformers/gemini.d.ts +3 -0
- package/dist/transformers/gemini.js +78 -0
- package/dist/transformers/hermes.d.ts +5 -0
- package/dist/transformers/hermes.js +136 -0
- package/dist/transformers/index.d.ts +14 -0
- package/dist/transformers/index.js +24 -0
- package/dist/transformers/kiro.d.ts +2 -0
- package/dist/transformers/kiro.js +69 -0
- package/dist/transformers/opencode.d.ts +2 -0
- package/dist/transformers/opencode.js +77 -0
- package/dist/transformers/rail-hooks.d.ts +56 -0
- package/dist/transformers/rail-hooks.js +303 -0
- package/dist/transformers/shared.d.ts +18 -0
- package/dist/transformers/shared.js +129 -0
- package/dist/transformers/types.d.ts +40 -0
- package/dist/transformers/types.js +1 -0
- package/dist/transformers/vscode.d.ts +3 -0
- package/dist/transformers/vscode.js +53 -0
- package/dist/transformers/windsurf.d.ts +3 -0
- package/dist/transformers/windsurf.js +43 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +50 -0
- package/plugins/memtrace-skills/.claude-plugin/plugin.json +23 -0
- package/plugins/memtrace-skills/references/mcp-parameters.md +302 -0
- package/plugins/memtrace-skills/skills/memtrace-api-topology/SKILL.md +58 -0
- package/plugins/memtrace-skills/skills/memtrace-change-impact-analysis/SKILL.md +75 -0
- package/plugins/memtrace-skills/skills/memtrace-cochange/SKILL.md +71 -0
- package/plugins/memtrace-skills/skills/memtrace-code-review/SKILL.md +41 -0
- package/plugins/memtrace-skills/skills/memtrace-codebase-exploration/SKILL.md +94 -0
- package/plugins/memtrace-skills/skills/memtrace-continuous-memory/SKILL.md +96 -0
- package/plugins/memtrace-skills/skills/memtrace-episode-replay/SKILL.md +94 -0
- package/plugins/memtrace-skills/skills/memtrace-evolution/SKILL.md +128 -0
- package/plugins/memtrace-skills/skills/memtrace-first/SKILL.md +194 -0
- package/plugins/memtrace-skills/skills/memtrace-fleet-coordination/SKILL.md +80 -0
- package/plugins/memtrace-skills/skills/memtrace-fleet-first/SKILL.md +125 -0
- package/plugins/memtrace-skills/skills/memtrace-fleet-publish-intent/SKILL.md +48 -0
- package/plugins/memtrace-skills/skills/memtrace-fleet-record-episode/SKILL.md +44 -0
- package/plugins/memtrace-skills/skills/memtrace-fleet-resolve/SKILL.md +54 -0
- package/plugins/memtrace-skills/skills/memtrace-graph/SKILL.md +67 -0
- package/plugins/memtrace-skills/skills/memtrace-impact/SKILL.md +58 -0
- package/plugins/memtrace-skills/skills/memtrace-incident-investigation/SKILL.md +112 -0
- package/plugins/memtrace-skills/skills/memtrace-index/SKILL.md +65 -0
- package/plugins/memtrace-skills/skills/memtrace-quality/SKILL.md +63 -0
- package/plugins/memtrace-skills/skills/memtrace-refactoring-guide/SKILL.md +103 -0
- package/plugins/memtrace-skills/skills/memtrace-relationships/SKILL.md +67 -0
- package/plugins/memtrace-skills/skills/memtrace-search/SKILL.md +93 -0
- package/plugins/memtrace-skills/skills/memtrace-session-continuity/SKILL.md +93 -0
- package/plugins/memtrace-skills/skills/memtrace-style-fingerprint/SKILL.md +105 -0
- package/skills/commands/memtrace-api-topology.md +65 -0
- package/skills/commands/memtrace-cochange.md +76 -0
- package/skills/commands/memtrace-evolution.md +135 -0
- 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/commands/memtrace-graph.md +75 -0
- package/skills/commands/memtrace-impact.md +64 -0
- package/skills/commands/memtrace-index.md +71 -0
- package/skills/commands/memtrace-quality.md +69 -0
- package/skills/commands/memtrace-relationships.md +73 -0
- package/skills/commands/memtrace-search.md +93 -0
- package/skills/workflows/memtrace-change-impact-analysis.md +85 -0
- package/skills/workflows/memtrace-code-review.md +48 -0
- package/skills/workflows/memtrace-codebase-exploration.md +108 -0
- package/skills/workflows/memtrace-continuous-memory.md +104 -0
- package/skills/workflows/memtrace-episode-replay.md +100 -0
- package/skills/workflows/memtrace-first.md +194 -0
- package/skills/workflows/memtrace-fleet-coordination.md +87 -0
- package/skills/workflows/memtrace-fleet-first.md +132 -0
- package/skills/workflows/memtrace-incident-investigation.md +125 -0
- package/skills/workflows/memtrace-refactoring-guide.md +116 -0
- package/skills/workflows/memtrace-session-continuity.md +98 -0
- package/skills/workflows/memtrace-style-fingerprint.md +111 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { claudeTransformer } from './claude.js';
|
|
2
|
+
import { cursorTransformer } from './cursor.js';
|
|
3
|
+
import { codexTransformer } from './codex.js';
|
|
4
|
+
import { geminiTransformer } from './gemini.js';
|
|
5
|
+
import { windsurfTransformer } from './windsurf.js';
|
|
6
|
+
import { vscodeTransformer } from './vscode.js';
|
|
7
|
+
import { hermesTransformer } from './hermes.js';
|
|
8
|
+
import { opencodeTransformer } from './opencode.js';
|
|
9
|
+
import { kiroTransformer } from './kiro.js';
|
|
10
|
+
export const ALL_TRANSFORMERS = [
|
|
11
|
+
claudeTransformer,
|
|
12
|
+
cursorTransformer,
|
|
13
|
+
codexTransformer,
|
|
14
|
+
geminiTransformer,
|
|
15
|
+
windsurfTransformer,
|
|
16
|
+
vscodeTransformer,
|
|
17
|
+
hermesTransformer,
|
|
18
|
+
opencodeTransformer,
|
|
19
|
+
kiroTransformer,
|
|
20
|
+
];
|
|
21
|
+
export function findTransformer(name) {
|
|
22
|
+
return ALL_TRANSFORMERS.find(t => t.name === name);
|
|
23
|
+
}
|
|
24
|
+
export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { registerMcpServersJsonAt, removeMcpServersJsonAt, skillName, } from './shared.js';
|
|
5
|
+
function steeringRoot(ctx) {
|
|
6
|
+
return ctx.scope === 'global'
|
|
7
|
+
? path.join(os.homedir(), '.kiro', 'steering')
|
|
8
|
+
: path.join(ctx.cwd, '.kiro', 'steering');
|
|
9
|
+
}
|
|
10
|
+
function mcpPath(ctx) {
|
|
11
|
+
return ctx.scope === 'global'
|
|
12
|
+
? path.join(os.homedir(), '.kiro', 'settings', 'mcp.json')
|
|
13
|
+
: path.join(ctx.cwd, '.kiro', 'settings', 'mcp.json');
|
|
14
|
+
}
|
|
15
|
+
function writeSteering(skill, rootDir) {
|
|
16
|
+
const name = skillName(skill);
|
|
17
|
+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
18
|
+
const content = [
|
|
19
|
+
'---',
|
|
20
|
+
'inclusion: auto',
|
|
21
|
+
`name: ${name}`,
|
|
22
|
+
`description: "${safeDesc}"`,
|
|
23
|
+
'---',
|
|
24
|
+
'',
|
|
25
|
+
skill.body,
|
|
26
|
+
].join('\n');
|
|
27
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(rootDir, `${name}.md`), content);
|
|
29
|
+
}
|
|
30
|
+
function removeMemtraceSteering(rootDir) {
|
|
31
|
+
if (!fs.existsSync(rootDir))
|
|
32
|
+
return;
|
|
33
|
+
for (const entry of fs.readdirSync(rootDir)) {
|
|
34
|
+
if (entry.startsWith('memtrace-') && entry.endsWith('.md')) {
|
|
35
|
+
fs.rmSync(path.join(rootDir, entry), { force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export const kiroTransformer = {
|
|
40
|
+
name: 'kiro',
|
|
41
|
+
async install(skills, ctx) {
|
|
42
|
+
const rootDir = steeringRoot(ctx);
|
|
43
|
+
removeMemtraceSteering(rootDir);
|
|
44
|
+
for (const skill of skills)
|
|
45
|
+
writeSteering(skill, rootDir);
|
|
46
|
+
let mcpRegistered = false;
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const mcpConfigPath = mcpPath(ctx);
|
|
49
|
+
if (!ctx.skipMcp) {
|
|
50
|
+
const result = registerMcpServersJsonAt(mcpConfigPath, ctx.memtraceBinary);
|
|
51
|
+
mcpRegistered = result.registered;
|
|
52
|
+
if (!mcpRegistered && result.backupPath) {
|
|
53
|
+
warnings.push(`Kiro MCP config was malformed; backed up to ${result.backupPath}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
agent: 'kiro',
|
|
58
|
+
skillsWritten: skills.length,
|
|
59
|
+
skillsDir: rootDir,
|
|
60
|
+
mcpConfigPath,
|
|
61
|
+
mcpRegistered,
|
|
62
|
+
warnings,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
async uninstall(ctx) {
|
|
66
|
+
removeMemtraceSteering(steeringRoot(ctx));
|
|
67
|
+
removeMcpServersJsonAt(mcpPath(ctx));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
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
|
+
}
|
|
10
|
+
function skillsRoot(ctx) {
|
|
11
|
+
return ctx.scope === 'global'
|
|
12
|
+
? path.join(opencodeConfigDir(), 'skills')
|
|
13
|
+
: path.join(ctx.cwd, '.opencode', 'skills');
|
|
14
|
+
}
|
|
15
|
+
function mcpPath(ctx) {
|
|
16
|
+
return ctx.scope === 'global'
|
|
17
|
+
? path.join(opencodeConfigDir(), 'opencode.json')
|
|
18
|
+
: path.join(ctx.cwd, 'opencode.json');
|
|
19
|
+
}
|
|
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
|
+
export const opencodeTransformer = {
|
|
32
|
+
name: 'opencode',
|
|
33
|
+
async install(skills, ctx) {
|
|
34
|
+
const rootDir = skillsRoot(ctx);
|
|
35
|
+
const count = writeSkills(skills, rootDir, { compatibility: 'opencode' });
|
|
36
|
+
let mcpRegistered = false;
|
|
37
|
+
const warnings = [];
|
|
38
|
+
const mcpConfigPath = mcpPath(ctx);
|
|
39
|
+
if (!ctx.skipMcp) {
|
|
40
|
+
const result = registerOpenCodeMcpAt(mcpConfigPath, ctx.memtraceBinary);
|
|
41
|
+
mcpRegistered = result.registered;
|
|
42
|
+
if (!mcpRegistered && result.backupPath) {
|
|
43
|
+
warnings.push(`OpenCode config was malformed; backed up to ${result.backupPath}.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ─── memtrace-rail ─── Write the in-process discovery plugin (OpenCode
|
|
47
|
+
// has no stdio hook). Defaults to observe; fail-open. Best effort.
|
|
48
|
+
try {
|
|
49
|
+
const src = openCodePluginSource(ctx.memtraceBinary);
|
|
50
|
+
for (const pluginPath of railPluginPaths(ctx)) {
|
|
51
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
52
|
+
fs.writeFileSync(pluginPath, src);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
warnings.push(`failed to write OpenCode Rail plugin: ${e.message}`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
agent: 'opencode',
|
|
60
|
+
skillsWritten: count,
|
|
61
|
+
skillsDir: rootDir,
|
|
62
|
+
mcpConfigPath,
|
|
63
|
+
mcpRegistered,
|
|
64
|
+
warnings,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
async uninstall(ctx) {
|
|
68
|
+
removeMemtraceSkills(skillsRoot(ctx));
|
|
69
|
+
removeOpenCodeMcpAt(mcpPath(ctx));
|
|
70
|
+
for (const p of railPluginPaths(ctx)) {
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(p, { force: true });
|
|
73
|
+
}
|
|
74
|
+
catch { /* best effort */ }
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Substring identifying a memtrace-owned Rail hook entry. */
|
|
2
|
+
export declare const RAIL_HOOK_MARKER = "route --hook";
|
|
3
|
+
/** The discovery tools each host surfaces. */
|
|
4
|
+
export declare const CLAUDE_MATCHER = "Grep|Glob|Bash";
|
|
5
|
+
export declare const CODEX_MATCHER = "Bash";
|
|
6
|
+
export declare const GEMINI_MATCHER = "run_shell_command|read_file|read_many_files|glob|search_file_content";
|
|
7
|
+
export declare function railCommand(binary: string, host: string): string;
|
|
8
|
+
export declare function isRailHookEntry(entry: unknown): boolean;
|
|
9
|
+
export interface HookResult {
|
|
10
|
+
registered: boolean;
|
|
11
|
+
backupPath?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CleanupResult {
|
|
14
|
+
changed: boolean;
|
|
15
|
+
backupPath?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a Claude-style settings.json hook (Codex `PreToolUse`, Gemini
|
|
19
|
+
* `BeforeTool`). Idempotent; preserves unrelated hooks; never overwrites a
|
|
20
|
+
* malformed file.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerSettingsHook(settingsPath: string, binary: string, host: string, eventName: string, matcher: string, extraSettings?: Record<string, unknown>): HookResult;
|
|
23
|
+
/** Remove a Rail hook from a Claude-style settings.json event array. */
|
|
24
|
+
export declare function removeSettingsHook(settingsPath: string, eventName: string): CleanupResult;
|
|
25
|
+
/**
|
|
26
|
+
* Register the Cursor Rail hook in `.cursor/hooks.json`. Cursor uses a distinct
|
|
27
|
+
* schema: `{version, hooks: {beforeShellExecution: [{command}]}}`. We intercept
|
|
28
|
+
* both shell execution (where rg/grep/find run) and file reads.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerCursorRailHook(hooksPath: string, binary: string): HookResult;
|
|
31
|
+
/** Remove the Cursor Rail hook entries. */
|
|
32
|
+
/** Windsurf Cascade hooks path (user-level). */
|
|
33
|
+
export declare function windsurfHooksPath(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Register Windsurf `pre_run_command` Rail hook. Uses exit codes (not JSON stdout);
|
|
36
|
+
* `show_output: true` so nudge context on stdout is visible in Cascade.
|
|
37
|
+
*/
|
|
38
|
+
export declare function registerWindsurfRailHook(hooksPath: string, binary: string): HookResult;
|
|
39
|
+
export declare function removeWindsurfRailHook(hooksPath: string): CleanupResult;
|
|
40
|
+
/** VS Code / Copilot user hooks file (global). */
|
|
41
|
+
export declare function vscodeCopilotRailHookPath(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Register VS Code agent `PreToolUse` hook (Claude-compatible JSON on stdio).
|
|
44
|
+
* Writes a dedicated file under `~/.copilot/hooks/` (loaded by default in recent VS Code).
|
|
45
|
+
*/
|
|
46
|
+
export declare function registerVsCodeRailHook(hookFilePath: string, binary: string): HookResult;
|
|
47
|
+
export declare function removeVsCodeRailHook(hookFilePath: string): CleanupResult;
|
|
48
|
+
export declare function removeCursorRailHook(hooksPath: string): CleanupResult;
|
|
49
|
+
/**
|
|
50
|
+
* The OpenCode in-process plugin source. OpenCode has no stdio hook — plugins
|
|
51
|
+
* are TS/JS modules with a `tool.execute.before` async hook. This plugin shells
|
|
52
|
+
* out to the SAME router via `--host generic` (a ToolAttempt in, a
|
|
53
|
+
* DecisionEnvelope out) and, on suppression, throws to block the raw tool with
|
|
54
|
+
* the Memtrace result as the error the agent sees.
|
|
55
|
+
*/
|
|
56
|
+
export declare function openCodePluginSource(binary: string): string;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memtrace-rail — per-host installer wiring for the Memtrace-first discovery
|
|
3
|
+
* hook. Each supported agent intercepts tool calls differently, so each gets a
|
|
4
|
+
* thin host-specific registration that points at the SAME router
|
|
5
|
+
* (`memtrace route --hook --host <h>`). All entries are idempotent and remove
|
|
6
|
+
* cleanly. Hooks default to observe mode (zero behavior change); the user opts
|
|
7
|
+
* into more via `MEMTRACE_RAIL=nudge|rail|strict`.
|
|
8
|
+
*
|
|
9
|
+
* Host families:
|
|
10
|
+
* - Claude Code → `.claude/settings.json` hooks.PreToolUse (in claude.ts)
|
|
11
|
+
* - Codex → `.codex/hooks.json` hooks.PreToolUse
|
|
12
|
+
* - Gemini CLI → `.gemini/settings.json` hooks.BeforeTool
|
|
13
|
+
* - Cursor → `.cursor/hooks.json` beforeShellExecution/beforeReadFile
|
|
14
|
+
* - OpenCode → `.opencode/plugin/memtrace-rail.js` (in-process plugin)
|
|
15
|
+
* - Windsurf → `~/.codeium/windsurf/hooks.json` pre_run_command (exit-code contract)
|
|
16
|
+
* - VS Code → `~/.copilot/hooks/memtrace-rail.json` PreToolUse (Claude-shaped)
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
22
|
+
/** Substring identifying a memtrace-owned Rail hook entry. */
|
|
23
|
+
export const RAIL_HOOK_MARKER = 'route --hook';
|
|
24
|
+
/** The discovery tools each host surfaces. */
|
|
25
|
+
export const CLAUDE_MATCHER = 'Grep|Glob|Bash';
|
|
26
|
+
export const CODEX_MATCHER = 'Bash';
|
|
27
|
+
export const GEMINI_MATCHER = 'run_shell_command|read_file|read_many_files|glob|search_file_content';
|
|
28
|
+
export function railCommand(binary, host) {
|
|
29
|
+
return `${binary} route --hook --host ${host}`;
|
|
30
|
+
}
|
|
31
|
+
function isRecord(v) {
|
|
32
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
33
|
+
}
|
|
34
|
+
export function isRailHookEntry(entry) {
|
|
35
|
+
if (!isRecord(entry) || !Array.isArray(entry.hooks))
|
|
36
|
+
return false;
|
|
37
|
+
return entry.hooks.some((h) => isRecord(h) && typeof h.command === 'string' && h.command.includes(RAIL_HOOK_MARKER));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register a Claude-style settings.json hook (Codex `PreToolUse`, Gemini
|
|
41
|
+
* `BeforeTool`). Idempotent; preserves unrelated hooks; never overwrites a
|
|
42
|
+
* malformed file.
|
|
43
|
+
*/
|
|
44
|
+
export function registerSettingsHook(settingsPath, binary, host, eventName, matcher, extraSettings) {
|
|
45
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
46
|
+
if (corrupted) {
|
|
47
|
+
console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Rail hook.`);
|
|
48
|
+
return { registered: false, backupPath };
|
|
49
|
+
}
|
|
50
|
+
const settings = (value ?? {});
|
|
51
|
+
// Some hosts gate the hook system behind top-level feature flags (e.g. Gemini
|
|
52
|
+
// requires `enableHooks` + `enableMessageBusIntegration` or BeforeTool hooks
|
|
53
|
+
// never fire). Merge them in without clobbering a value the user already set.
|
|
54
|
+
if (extraSettings) {
|
|
55
|
+
for (const [k, v] of Object.entries(extraSettings)) {
|
|
56
|
+
if (!(k in settings))
|
|
57
|
+
settings[k] = v;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
settings.hooks = settings.hooks ?? {};
|
|
61
|
+
const existing = Array.isArray(settings.hooks[eventName])
|
|
62
|
+
? settings.hooks[eventName]
|
|
63
|
+
: [];
|
|
64
|
+
const preserved = existing.filter((e) => !isRailHookEntry(e));
|
|
65
|
+
preserved.push({
|
|
66
|
+
matcher,
|
|
67
|
+
hooks: [{ type: 'command', command: railCommand(binary, host) }],
|
|
68
|
+
});
|
|
69
|
+
settings.hooks[eventName] = preserved;
|
|
70
|
+
writeJsonAtomic(settingsPath, settings);
|
|
71
|
+
return { registered: true };
|
|
72
|
+
}
|
|
73
|
+
/** Remove a Rail hook from a Claude-style settings.json event array. */
|
|
74
|
+
export function removeSettingsHook(settingsPath, eventName) {
|
|
75
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
76
|
+
if (corrupted)
|
|
77
|
+
return { changed: false, backupPath };
|
|
78
|
+
if (!value)
|
|
79
|
+
return { changed: false };
|
|
80
|
+
const settings = value;
|
|
81
|
+
if (!isRecord(settings.hooks) || !Array.isArray(settings.hooks[eventName])) {
|
|
82
|
+
return { changed: false };
|
|
83
|
+
}
|
|
84
|
+
const before = settings.hooks[eventName];
|
|
85
|
+
const after = before.filter((e) => !isRailHookEntry(e));
|
|
86
|
+
if (after.length === before.length)
|
|
87
|
+
return { changed: false };
|
|
88
|
+
if (after.length === 0)
|
|
89
|
+
delete settings.hooks[eventName];
|
|
90
|
+
else
|
|
91
|
+
settings.hooks[eventName] = after;
|
|
92
|
+
if (Object.keys(settings.hooks).length === 0)
|
|
93
|
+
delete settings.hooks;
|
|
94
|
+
writeJsonAtomic(settingsPath, settings);
|
|
95
|
+
return { changed: true };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Register the Cursor Rail hook in `.cursor/hooks.json`. Cursor uses a distinct
|
|
99
|
+
* schema: `{version, hooks: {beforeShellExecution: [{command}]}}`. We intercept
|
|
100
|
+
* both shell execution (where rg/grep/find run) and file reads.
|
|
101
|
+
*/
|
|
102
|
+
export function registerCursorRailHook(hooksPath, binary) {
|
|
103
|
+
const { value, corrupted, backupPath } = safeReadJson(hooksPath);
|
|
104
|
+
if (corrupted) {
|
|
105
|
+
console.warn(`memtrace: ${hooksPath} is malformed; backed up to ${backupPath}. Skipped Cursor Rail hook.`);
|
|
106
|
+
return { registered: false, backupPath };
|
|
107
|
+
}
|
|
108
|
+
const cfg = (value ?? {});
|
|
109
|
+
cfg.version = cfg.version ?? 1;
|
|
110
|
+
cfg.hooks = cfg.hooks ?? {};
|
|
111
|
+
const cmd = railCommand(binary, 'cursor');
|
|
112
|
+
// Only `beforeShellExecution` — that's where rg/grep/find run. We do NOT wire
|
|
113
|
+
// `beforeReadFile`: a file Read always classifies non-discovery (→ allow), so
|
|
114
|
+
// hooking it would spawn a router subprocess on every file read for no benefit.
|
|
115
|
+
const event = 'beforeShellExecution';
|
|
116
|
+
const existing = Array.isArray(cfg.hooks[event]) ? cfg.hooks[event] : [];
|
|
117
|
+
const preserved = existing.filter((e) => !(isRecord(e) && typeof e.command === 'string' && e.command.includes(RAIL_HOOK_MARKER)));
|
|
118
|
+
preserved.push({ command: cmd });
|
|
119
|
+
cfg.hooks[event] = preserved;
|
|
120
|
+
writeJsonAtomic(hooksPath, cfg);
|
|
121
|
+
return { registered: true };
|
|
122
|
+
}
|
|
123
|
+
/** Remove the Cursor Rail hook entries. */
|
|
124
|
+
/** Windsurf Cascade hooks path (user-level). */
|
|
125
|
+
export function windsurfHooksPath() {
|
|
126
|
+
return path.join(os.homedir(), '.codeium', 'windsurf', 'hooks.json');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Register Windsurf `pre_run_command` Rail hook. Uses exit codes (not JSON stdout);
|
|
130
|
+
* `show_output: true` so nudge context on stdout is visible in Cascade.
|
|
131
|
+
*/
|
|
132
|
+
export function registerWindsurfRailHook(hooksPath, binary) {
|
|
133
|
+
const { value, corrupted, backupPath } = safeReadJson(hooksPath);
|
|
134
|
+
if (corrupted) {
|
|
135
|
+
console.warn(`memtrace: ${hooksPath} is malformed; backed up to ${backupPath}. Skipped Windsurf Rail hook.`);
|
|
136
|
+
return { registered: false, backupPath };
|
|
137
|
+
}
|
|
138
|
+
const cfg = (value ?? {});
|
|
139
|
+
cfg.hooks = cfg.hooks ?? {};
|
|
140
|
+
const event = 'pre_run_command';
|
|
141
|
+
const cmd = railCommand(binary, 'windsurf');
|
|
142
|
+
const existing = Array.isArray(cfg.hooks[event]) ? cfg.hooks[event] : [];
|
|
143
|
+
const preserved = existing.filter((e) => !(isRecord(e) &&
|
|
144
|
+
typeof e.command === 'string' &&
|
|
145
|
+
e.command.includes(RAIL_HOOK_MARKER)));
|
|
146
|
+
preserved.push({ command: cmd, show_output: true });
|
|
147
|
+
cfg.hooks[event] = preserved;
|
|
148
|
+
writeJsonAtomic(hooksPath, cfg);
|
|
149
|
+
return { registered: true };
|
|
150
|
+
}
|
|
151
|
+
export function removeWindsurfRailHook(hooksPath) {
|
|
152
|
+
const { value, corrupted, backupPath } = safeReadJson(hooksPath);
|
|
153
|
+
if (corrupted)
|
|
154
|
+
return { changed: false, backupPath };
|
|
155
|
+
if (!value)
|
|
156
|
+
return { changed: false };
|
|
157
|
+
const cfg = value;
|
|
158
|
+
if (!isRecord(cfg.hooks) || !Array.isArray(cfg.hooks.pre_run_command)) {
|
|
159
|
+
return { changed: false };
|
|
160
|
+
}
|
|
161
|
+
const before = cfg.hooks.pre_run_command;
|
|
162
|
+
const after = before.filter((e) => !(isRecord(e) &&
|
|
163
|
+
typeof e.command === 'string' &&
|
|
164
|
+
e.command.includes(RAIL_HOOK_MARKER)));
|
|
165
|
+
if (after.length === before.length)
|
|
166
|
+
return { changed: false };
|
|
167
|
+
if (after.length === 0)
|
|
168
|
+
delete cfg.hooks.pre_run_command;
|
|
169
|
+
else
|
|
170
|
+
cfg.hooks.pre_run_command = after;
|
|
171
|
+
if (Object.keys(cfg.hooks).length === 0)
|
|
172
|
+
delete cfg.hooks;
|
|
173
|
+
writeJsonAtomic(hooksPath, cfg);
|
|
174
|
+
return { changed: true };
|
|
175
|
+
}
|
|
176
|
+
/** VS Code / Copilot user hooks file (global). */
|
|
177
|
+
export function vscodeCopilotRailHookPath() {
|
|
178
|
+
return path.join(os.homedir(), '.copilot', 'hooks', 'memtrace-rail.json');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Register VS Code agent `PreToolUse` hook (Claude-compatible JSON on stdio).
|
|
182
|
+
* Writes a dedicated file under `~/.copilot/hooks/` (loaded by default in recent VS Code).
|
|
183
|
+
*/
|
|
184
|
+
export function registerVsCodeRailHook(hookFilePath, binary) {
|
|
185
|
+
const { value, corrupted, backupPath } = safeReadJson(hookFilePath);
|
|
186
|
+
if (corrupted) {
|
|
187
|
+
console.warn(`memtrace: ${hookFilePath} is malformed; backed up to ${backupPath}. Skipped VS Code Rail hook.`);
|
|
188
|
+
return { registered: false, backupPath };
|
|
189
|
+
}
|
|
190
|
+
const cmd = railCommand(binary, 'vscode');
|
|
191
|
+
const existing = (value ?? {});
|
|
192
|
+
const pre = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
|
|
193
|
+
const preserved = pre.filter((e) => !(isRecord(e) &&
|
|
194
|
+
typeof e.command === 'string' &&
|
|
195
|
+
e.command.includes(RAIL_HOOK_MARKER)));
|
|
196
|
+
preserved.push({ type: 'command', command: cmd, timeout: 30 });
|
|
197
|
+
writeJsonAtomic(hookFilePath, {
|
|
198
|
+
hooks: {
|
|
199
|
+
PreToolUse: preserved,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return { registered: true };
|
|
203
|
+
}
|
|
204
|
+
export function removeVsCodeRailHook(hookFilePath) {
|
|
205
|
+
const { value, corrupted, backupPath } = safeReadJson(hookFilePath);
|
|
206
|
+
if (corrupted)
|
|
207
|
+
return { changed: false, backupPath };
|
|
208
|
+
if (!value)
|
|
209
|
+
return { changed: false };
|
|
210
|
+
const cfg = value;
|
|
211
|
+
if (!Array.isArray(cfg.hooks?.PreToolUse))
|
|
212
|
+
return { changed: false };
|
|
213
|
+
const before = cfg.hooks.PreToolUse;
|
|
214
|
+
const after = before.filter((e) => !(isRecord(e) &&
|
|
215
|
+
typeof e.command === 'string' &&
|
|
216
|
+
e.command.includes(RAIL_HOOK_MARKER)));
|
|
217
|
+
if (after.length === before.length)
|
|
218
|
+
return { changed: false };
|
|
219
|
+
if (after.length === 0) {
|
|
220
|
+
try {
|
|
221
|
+
fs.rmSync(hookFilePath, { force: true });
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
writeJsonAtomic(hookFilePath, { hooks: {} });
|
|
225
|
+
}
|
|
226
|
+
return { changed: true };
|
|
227
|
+
}
|
|
228
|
+
writeJsonAtomic(hookFilePath, { hooks: { PreToolUse: after } });
|
|
229
|
+
return { changed: true };
|
|
230
|
+
}
|
|
231
|
+
export function removeCursorRailHook(hooksPath) {
|
|
232
|
+
const { value, corrupted, backupPath } = safeReadJson(hooksPath);
|
|
233
|
+
if (corrupted)
|
|
234
|
+
return { changed: false, backupPath };
|
|
235
|
+
if (!value)
|
|
236
|
+
return { changed: false };
|
|
237
|
+
const cfg = value;
|
|
238
|
+
if (!isRecord(cfg.hooks))
|
|
239
|
+
return { changed: false };
|
|
240
|
+
let changed = false;
|
|
241
|
+
for (const event of ['beforeShellExecution', 'beforeReadFile']) {
|
|
242
|
+
if (!Array.isArray(cfg.hooks[event]))
|
|
243
|
+
continue;
|
|
244
|
+
const before = cfg.hooks[event];
|
|
245
|
+
const after = before.filter((e) => !(isRecord(e) && typeof e.command === 'string' && e.command.includes(RAIL_HOOK_MARKER)));
|
|
246
|
+
if (after.length !== before.length) {
|
|
247
|
+
changed = true;
|
|
248
|
+
if (after.length === 0)
|
|
249
|
+
delete cfg.hooks[event];
|
|
250
|
+
else
|
|
251
|
+
cfg.hooks[event] = after;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (changed) {
|
|
255
|
+
if (isRecord(cfg.hooks) && Object.keys(cfg.hooks).length === 0)
|
|
256
|
+
delete cfg.hooks;
|
|
257
|
+
writeJsonAtomic(hooksPath, cfg);
|
|
258
|
+
}
|
|
259
|
+
return { changed };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The OpenCode in-process plugin source. OpenCode has no stdio hook — plugins
|
|
263
|
+
* are TS/JS modules with a `tool.execute.before` async hook. This plugin shells
|
|
264
|
+
* out to the SAME router via `--host generic` (a ToolAttempt in, a
|
|
265
|
+
* DecisionEnvelope out) and, on suppression, throws to block the raw tool with
|
|
266
|
+
* the Memtrace result as the error the agent sees.
|
|
267
|
+
*/
|
|
268
|
+
export function openCodePluginSource(binary) {
|
|
269
|
+
return `// AUTO-GENERATED by memtrace install — Memtrace Rail discovery hook.
|
|
270
|
+
// Routes OpenCode tool calls through \`${binary} route\`. Defaults to observe
|
|
271
|
+
// (no-op) unless MEMTRACE_RAIL=nudge|rail|strict. Remove via 'memtrace uninstall'.
|
|
272
|
+
import { execFileSync } from 'node:child_process';
|
|
273
|
+
|
|
274
|
+
const BIN = ${JSON.stringify(binary)};
|
|
275
|
+
|
|
276
|
+
export const MemtraceRail = async ({ project, client, $ }) => ({
|
|
277
|
+
'tool.execute.before': async (input, output) => {
|
|
278
|
+
try {
|
|
279
|
+
const tool = input?.tool;
|
|
280
|
+
if (tool !== 'bash' && tool !== 'grep' && tool !== 'glob') return;
|
|
281
|
+
const attempt = {
|
|
282
|
+
host: 'open-code',
|
|
283
|
+
cwd: output?.args?.cwd || process.cwd(),
|
|
284
|
+
tool: tool === 'bash' ? 'Bash' : tool === 'grep' ? 'Grep' : 'Glob',
|
|
285
|
+
input: output?.args || {},
|
|
286
|
+
session_id: input?.sessionID || 'opencode',
|
|
287
|
+
};
|
|
288
|
+
const out = execFileSync(BIN, ['route', '--json', '-'], {
|
|
289
|
+
input: JSON.stringify(attempt), timeout: 1500, encoding: 'utf8',
|
|
290
|
+
});
|
|
291
|
+
const decision = JSON.parse(out);
|
|
292
|
+
if (decision.action === 'suppress_raw_with_result') {
|
|
293
|
+
throw new Error('[Memtrace Rail] ' + (decision.message || 'Use Memtrace for code discovery.') +
|
|
294
|
+
(decision.result ? '\\n' + JSON.stringify(decision.result, null, 2) : ''));
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
// Fail open on any router/exec error — never block the agent.
|
|
298
|
+
if (e && String(e.message || '').startsWith('[Memtrace Rail]')) throw e;
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Skill } from '../skills.js';
|
|
2
|
+
export declare const MCP_SERVER_NAME = "memtrace";
|
|
3
|
+
export declare const MEMTRACE_MCP_ENV: Record<string, string>;
|
|
4
|
+
export declare function skillName(skill: Skill): string;
|
|
5
|
+
export declare function skillMarkdown(skill: Skill, extraFrontmatter?: Record<string, string>): string;
|
|
6
|
+
export declare function writeSkills(skills: Skill[], rootDir: string, extraFrontmatter?: Record<string, string>): number;
|
|
7
|
+
export declare function removeMemtraceSkills(rootDir: string): void;
|
|
8
|
+
export declare function writeTextAtomic(filePath: string, content: string): void;
|
|
9
|
+
export interface JsonMcpResult {
|
|
10
|
+
registered: boolean;
|
|
11
|
+
backupPath?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function registerMcpServersJsonAt(filePath: string, binary: string): JsonMcpResult;
|
|
14
|
+
export declare function removeMcpServersJsonAt(filePath: string): void;
|
|
15
|
+
export declare function registerVsCodeMcpAt(filePath: string, binary: string): JsonMcpResult;
|
|
16
|
+
export declare function removeVsCodeMcpAt(filePath: string): void;
|
|
17
|
+
export declare function registerOpenCodeMcpAt(filePath: string, binary: string): JsonMcpResult;
|
|
18
|
+
export declare function removeOpenCodeMcpAt(filePath: string): void;
|