memtrace-skills 1.2.10 → 1.2.13
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/install.d.ts +1 -0
- package/dist/commands/install.js +3 -2
- package/dist/index.js +9 -0
- package/dist/managed-skills.d.ts +4 -0
- package/dist/managed-skills.js +95 -0
- package/dist/rail-hook.js +28 -12
- package/dist/rail-lifecycle.js +45 -2
- package/dist/rail-runtime.d.ts +38 -2
- package/dist/rail-runtime.js +141 -8
- package/dist/tool-names.js +1 -0
- package/dist/transformers/claude.d.ts +6 -4
- package/dist/transformers/claude.js +54 -65
- package/dist/transformers/codex-config.d.ts +2 -0
- package/dist/transformers/codex-config.js +33 -0
- package/dist/transformers/codex.js +18 -22
- package/dist/transformers/cursor.js +7 -21
- package/dist/transformers/gemini.js +5 -4
- package/dist/transformers/kiro.js +5 -14
- package/dist/transformers/opencode.js +8 -7
- package/dist/transformers/pi.js +8 -6
- package/dist/transformers/rail-hooks.js +42 -5
- package/dist/transformers/shared.js +6 -13
- package/dist/transformers/types.d.ts +2 -0
- package/package.json +4 -3
- package/plugins/memtrace-skills/references/mcp-parameters.md +14 -1
- package/plugins/memtrace-skills/skills/memtrace-first/SKILL.md +42 -17
- package/skills/managed-stock-sha256.txt +364 -0
- package/skills/workflows/memtrace-first.md +42 -17
|
@@ -3,9 +3,10 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { copyCanonicalPluginAssets, copyCanonicalPluginManifest, readCanonicalPluginVersion, } from '../../plugin-assets/index.js';
|
|
6
|
+
import { writeManagedSkill, removeManagedSkills } from '../managed-skills.js';
|
|
6
7
|
import { execCommand, commandExists } from '../utils.js';
|
|
7
8
|
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
8
|
-
import { railIsActive, railHookInvocation, railHookCommand, RAIL_GUARD_MARKER, enrichEnabled, railMode } from '../rail-runtime.js';
|
|
9
|
+
import { railIsActive, railHookInvocation, railHookCommand, RAIL_GUARD_MARKER, enrichEnabled, railMode, railConfig } from '../rail-runtime.js';
|
|
9
10
|
import { LEGACY_MCP_SERVER_NAME, MEMTRACE_MCP_ENV, removeLegacyMcpServer, } from './shared.js';
|
|
10
11
|
const PLUGIN_NAME = 'memtrace-skills';
|
|
11
12
|
const MARKETPLACE_NAME = 'memtrace';
|
|
@@ -262,8 +263,8 @@ export function claudeRailHookCommand(_memtraceBinary, platform = process.platfo
|
|
|
262
263
|
* This is what makes Rail "part of memtrace" rather than a manual install:
|
|
263
264
|
* `memtrace install` wires a PreToolUse hook that pipes every Grep/Glob/Bash
|
|
264
265
|
* attempt through the Rail router (see {@link claudeRailHookCommand} for the
|
|
265
|
-
* transport). While a daemon is running the hook defaults to **
|
|
266
|
-
*
|
|
266
|
+
* transport). While a daemon is running the hook defaults to **nudge** —
|
|
267
|
+
* context alongside ordinary tools. Explicit modes are preserved. Mode is intentionally NOT
|
|
267
268
|
* baked into the command so the user can change it via env without
|
|
268
269
|
* re-installing.
|
|
269
270
|
*
|
|
@@ -284,8 +285,9 @@ export function registerRailHookInSettingsAt(settingsPath, memtraceBinary, platf
|
|
|
284
285
|
const settings = (value ?? {});
|
|
285
286
|
settings.hooks = isRecord(settings.hooks) ? settings.hooks : {};
|
|
286
287
|
pruneMemtraceOwnedHookEntries(settings.hooks);
|
|
287
|
-
const
|
|
288
|
-
const mode = options.mode ?? railMode();
|
|
288
|
+
const config = railConfig();
|
|
289
|
+
const mode = options.mode ?? railMode(process.env, config);
|
|
290
|
+
const enrich = options.enrich ?? enrichEnabled(process.env, { ...config, mode });
|
|
289
291
|
const hookFor = (extra) => ({
|
|
290
292
|
type: 'command',
|
|
291
293
|
...(platform === 'win32'
|
|
@@ -552,7 +554,10 @@ export function claudeUserConfigFile() {
|
|
|
552
554
|
* 2. Fall back to manual: write cache files + update settings.json
|
|
553
555
|
* 3. Register MCP server for memtrace tools
|
|
554
556
|
*/
|
|
555
|
-
export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
557
|
+
export async function installClaudePlugin(skills, memtraceBinaryPath, options = {}) {
|
|
558
|
+
// Retire known stock bare copies when moving to plugin delivery. Customized
|
|
559
|
+
// strict policies/opt-outs survive; names alone never prove ownership.
|
|
560
|
+
removeManagedSkills(path.join(os.homedir(), '.claude', 'skills'));
|
|
556
561
|
// Step 1: make sure Claude sees the public HTTPS marketplace source before
|
|
557
562
|
// the CLI attempts to clone/update it.
|
|
558
563
|
enablePluginInSettings();
|
|
@@ -582,44 +587,57 @@ export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
|
582
587
|
// Step 5: Write plugin assets and skills
|
|
583
588
|
copyBundledPluginAssetsAt(cacheDir);
|
|
584
589
|
const skillsDir = path.join(cacheDir, 'skills');
|
|
585
|
-
|
|
586
|
-
fs.rmSync(skillsDir, { recursive: true });
|
|
587
|
-
}
|
|
590
|
+
let skillCount = 0;
|
|
588
591
|
for (const skill of skills) {
|
|
589
592
|
const results = transformForClaude(skill);
|
|
590
593
|
for (const { relativePath, content } of results) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
fs.writeFileSync(fullPath, content);
|
|
594
|
+
if (writeManagedSkill(skillsDir, path.basename(path.dirname(relativePath)), content))
|
|
595
|
+
skillCount++;
|
|
594
596
|
}
|
|
595
597
|
}
|
|
596
598
|
enablePluginInSettings();
|
|
597
599
|
// Step 6: Register MCP server
|
|
598
|
-
|
|
600
|
+
if (!options.skipMcp)
|
|
601
|
+
await registerMcpServer(memtraceBinaryPath);
|
|
599
602
|
// Skills ship via the memtrace-skills@memtrace plugin only. Writing
|
|
600
603
|
// bare copies to ~/.claude/skills/memtrace-* duplicates every skill
|
|
601
604
|
// in Claude Code when the plugin is also enabled (issue #11).
|
|
602
|
-
return { cacheDir, skillCount
|
|
605
|
+
return { cacheDir, skillCount };
|
|
606
|
+
}
|
|
607
|
+
function refreshEnabledClaudePlugin(skills) {
|
|
608
|
+
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
609
|
+
if (!fs.existsSync(settingsFile))
|
|
610
|
+
return 0;
|
|
611
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
612
|
+
if (settings.enabledPlugins?.[PLUGIN_KEY] !== true)
|
|
613
|
+
return 0;
|
|
614
|
+
const root = getPluginCacheRoot();
|
|
615
|
+
if (!fs.existsSync(root))
|
|
616
|
+
return 0;
|
|
617
|
+
let skillCount = 0;
|
|
618
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
619
|
+
if (!entry.isDirectory())
|
|
620
|
+
continue;
|
|
621
|
+
const cacheDir = path.join(root, entry.name);
|
|
622
|
+
copyBundledPluginAssetsAt(cacheDir);
|
|
623
|
+
for (const skill of skills) {
|
|
624
|
+
for (const asset of transformForClaude(skill)) {
|
|
625
|
+
const name = path.basename(path.dirname(asset.relativePath));
|
|
626
|
+
if (writeManagedSkill(path.join(cacheDir, 'skills'), name, asset.content))
|
|
627
|
+
skillCount++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return skillCount;
|
|
603
632
|
}
|
|
604
633
|
/**
|
|
605
634
|
* Write skills to ~/.claude/skills/ for SDK-based integrations.
|
|
606
635
|
*/
|
|
607
636
|
function writeUserLevelSkills(skills) {
|
|
608
637
|
const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
609
|
-
if (fs.existsSync(userSkillsDir)) {
|
|
610
|
-
for (const entry of fs.readdirSync(userSkillsDir)) {
|
|
611
|
-
if (entry.startsWith('memtrace-')) {
|
|
612
|
-
fs.rmSync(path.join(userSkillsDir, entry), { recursive: true, force: true });
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
638
|
for (const skill of skills) {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
620
|
-
const outDir = path.join(userSkillsDir, skillName);
|
|
621
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
622
|
-
fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
|
|
639
|
+
const content = transformForClaude(skill)[0].content;
|
|
640
|
+
writeManagedSkill(userSkillsDir, skill.filename.replace(/\.md$/, ''), content);
|
|
623
641
|
}
|
|
624
642
|
}
|
|
625
643
|
/**
|
|
@@ -652,22 +670,8 @@ export async function uninstallClaudePlugin() {
|
|
|
652
670
|
}
|
|
653
671
|
removeClaudeMarketplaceCacheDirs();
|
|
654
672
|
removeClaudeInstalledPluginMetadata();
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
if (fs.existsSync(userSkillsDir)) {
|
|
658
|
-
for (const entry of fs.readdirSync(userSkillsDir)) {
|
|
659
|
-
if (entry.startsWith('memtrace-')) {
|
|
660
|
-
const entryPath = path.join(userSkillsDir, entry);
|
|
661
|
-
const stat = fs.statSync(entryPath);
|
|
662
|
-
if (stat.isDirectory()) {
|
|
663
|
-
fs.rmSync(entryPath, { recursive: true });
|
|
664
|
-
}
|
|
665
|
-
else if (entry.endsWith('.md')) {
|
|
666
|
-
fs.unlinkSync(entryPath);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
673
|
+
// Customized skills and their extra assets are not installer-owned.
|
|
674
|
+
removeManagedSkills(path.join(os.homedir(), '.claude', 'skills'));
|
|
671
675
|
}
|
|
672
676
|
/**
|
|
673
677
|
* Write/merge the memtrace MCP server entry into a project-local .mcp.json.
|
|
@@ -700,7 +704,9 @@ export const claudeTransformer = {
|
|
|
700
704
|
name: 'claude',
|
|
701
705
|
async install(skills, ctx) {
|
|
702
706
|
if (ctx.scope === 'global') {
|
|
703
|
-
const
|
|
707
|
+
const skillCount = ctx.mode === 'refresh'
|
|
708
|
+
? refreshEnabledClaudePlugin(skills)
|
|
709
|
+
: (await installClaudePlugin(skills, ctx.memtraceBinary, { skipMcp: ctx.skipMcp })).skillCount;
|
|
704
710
|
return {
|
|
705
711
|
agent: 'claude',
|
|
706
712
|
skillsWritten: skillCount,
|
|
@@ -712,22 +718,11 @@ export const claudeTransformer = {
|
|
|
712
718
|
}
|
|
713
719
|
// Local scope: write skills into <cwd>/.claude/skills/
|
|
714
720
|
const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
|
|
715
|
-
if (fs.existsSync(skillsDir)) {
|
|
716
|
-
for (const entry of fs.readdirSync(skillsDir)) {
|
|
717
|
-
if (entry.startsWith('memtrace-')) {
|
|
718
|
-
fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
721
|
let count = 0;
|
|
723
722
|
for (const skill of skills) {
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
728
|
-
const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
729
|
-
fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
|
|
730
|
-
count++;
|
|
723
|
+
const content = transformForClaude(skill)[0].content;
|
|
724
|
+
if (writeManagedSkill(skillsDir, skill.filename.replace(/\.md$/, ''), content))
|
|
725
|
+
count++;
|
|
731
726
|
}
|
|
732
727
|
// MCP for local scope: .mcp.json at project root (Claude's project-level convention)
|
|
733
728
|
const mcpConfigPath = path.join(ctx.cwd, '.mcp.json');
|
|
@@ -751,13 +746,7 @@ export const claudeTransformer = {
|
|
|
751
746
|
}
|
|
752
747
|
// Local: remove <cwd>/.claude/skills/memtrace-* and the memtrace entry from <cwd>/.mcp.json
|
|
753
748
|
const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
|
|
754
|
-
|
|
755
|
-
for (const entry of fs.readdirSync(skillsDir)) {
|
|
756
|
-
if (entry.startsWith('memtrace-')) {
|
|
757
|
-
fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
749
|
+
removeManagedSkills(skillsDir);
|
|
761
750
|
const mcpPath = path.join(ctx.cwd, '.mcp.json');
|
|
762
751
|
const { value, corrupted } = safeReadJson(mcpPath);
|
|
763
752
|
if (!corrupted && value?.mcpServers?.['memtrace']) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { parse } from 'smol-toml';
|
|
3
|
+
const readConfig = (raw) => parse(raw, { integersAsBigInt: 'asNeeded' });
|
|
4
|
+
function memtraceServer(config) {
|
|
5
|
+
const servers = config.mcp_servers;
|
|
6
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers))
|
|
7
|
+
return undefined;
|
|
8
|
+
const server = servers.memtrace;
|
|
9
|
+
return server && typeof server === 'object' && !Array.isArray(server) ? server : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function hasCodexMemtraceServer(raw) {
|
|
12
|
+
return memtraceServer(readConfig(raw)) !== undefined;
|
|
13
|
+
}
|
|
14
|
+
export function refreshCodexCommand(raw, command) {
|
|
15
|
+
const expected = readConfig(raw);
|
|
16
|
+
const server = memtraceServer(expected);
|
|
17
|
+
if (!server || typeof server.command !== 'string' || server.command === command)
|
|
18
|
+
return raw;
|
|
19
|
+
server.command = command;
|
|
20
|
+
let inServer = false;
|
|
21
|
+
const next = raw.split(/(?<=\n)/).map(line => {
|
|
22
|
+
if (/^\s*\[/.test(line)) {
|
|
23
|
+
inServer = /^\s*\[\s*mcp_servers\s*\.\s*(?:memtrace|"memtrace"|'memtrace')\s*\]\s*(?:#.*)?$/.test(line.trimEnd());
|
|
24
|
+
}
|
|
25
|
+
if (!inServer)
|
|
26
|
+
return line;
|
|
27
|
+
return line.replace(/^(\s*command\s*=\s*)(["'])(.*?)\2([ \t]*(?:#.*)?)(\r?\n)?$/, (_match, prefix, _quote, _value, comment, newline) => (`${prefix}${JSON.stringify(command)}${comment}${newline ?? ''}`));
|
|
28
|
+
}).join('');
|
|
29
|
+
if (!isDeepStrictEqual(readConfig(next), expected)) {
|
|
30
|
+
throw new Error('Cannot safely update the Codex MCP command; existing configuration was left unchanged.');
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { writeManagedSkill, removeManagedSkills } from '../managed-skills.js';
|
|
4
5
|
import { commandExists, execCommand } from '../utils.js';
|
|
6
|
+
import { hasCodexMemtraceServer, refreshCodexCommand } from './codex-config.js';
|
|
5
7
|
import { registerSettingsHook, removeSettingsHook, CODEX_MATCHER, } from './rail-hooks.js';
|
|
6
8
|
const MCP_SERVER_NAME = 'memtrace';
|
|
7
9
|
const LEGACY_MCP_SERVER_NAME = 'memfleet';
|
|
@@ -19,11 +21,9 @@ export function codexConfigPath(ctx) {
|
|
|
19
21
|
}
|
|
20
22
|
function writeSkill(skill, rootDir) {
|
|
21
23
|
const name = skill.filename.replace(/\.md$/, '');
|
|
22
|
-
const outDir = path.join(rootDir, name);
|
|
23
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
24
24
|
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
25
25
|
const content = `---\nname: ${name}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
26
|
-
|
|
26
|
+
return writeManagedSkill(rootDir, name, content);
|
|
27
27
|
}
|
|
28
28
|
function tomlString(value) {
|
|
29
29
|
return `"${value
|
|
@@ -124,6 +124,12 @@ function extractMemtraceEnvBlock(raw) {
|
|
|
124
124
|
}
|
|
125
125
|
export function registerCodexMcpAt(configFile, binary) {
|
|
126
126
|
const existing = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf-8') : '';
|
|
127
|
+
if (hasCodexMemtraceServer(existing)) {
|
|
128
|
+
const next = refreshCodexCommand(existing, binary);
|
|
129
|
+
if (next !== existing)
|
|
130
|
+
writeTextAtomic(configFile, next);
|
|
131
|
+
return { registered: true };
|
|
132
|
+
}
|
|
127
133
|
// Strip leaves the env sub-table intact; split it out so we can reorder it
|
|
128
134
|
// after the freshly written parent block for readability.
|
|
129
135
|
const stripped = stripCodexMcpServer(existing);
|
|
@@ -145,6 +151,8 @@ export function registerCodexMcpAt(configFile, binary) {
|
|
|
145
151
|
if (envBlock)
|
|
146
152
|
parts.push(envBlock);
|
|
147
153
|
const next = `${parts.join('\n\n')}\n`;
|
|
154
|
+
if (!hasCodexMemtraceServer(next))
|
|
155
|
+
throw new Error('Codex MCP registration could not be validated.');
|
|
148
156
|
writeTextAtomic(configFile, next);
|
|
149
157
|
return { registered: true };
|
|
150
158
|
}
|
|
@@ -168,20 +176,13 @@ export const codexTransformer = {
|
|
|
168
176
|
name: 'codex',
|
|
169
177
|
async install(skills, ctx) {
|
|
170
178
|
const rootDir = skillsRoot(ctx);
|
|
171
|
-
|
|
172
|
-
for (const entry of fs.readdirSync(rootDir)) {
|
|
173
|
-
if (entry.startsWith('memtrace-')) {
|
|
174
|
-
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
for (const s of skills)
|
|
179
|
-
writeSkill(s, rootDir);
|
|
179
|
+
const written = skills.filter(skill => writeSkill(skill, rootDir)).length;
|
|
180
180
|
let mcpRegistered = false;
|
|
181
181
|
const warnings = [];
|
|
182
182
|
const mcpConfigPath = codexConfigPath(ctx);
|
|
183
183
|
if (!ctx.skipMcp) {
|
|
184
|
-
const cliRegistered = ctx.scope === 'global' &&
|
|
184
|
+
const cliRegistered = ctx.scope === 'global' && !fs.existsSync(mcpConfigPath)
|
|
185
|
+
&& await tryCodexMcpAdd(ctx.memtraceBinary);
|
|
185
186
|
registerCodexMcpAt(mcpConfigPath, ctx.memtraceBinary);
|
|
186
187
|
mcpRegistered = true;
|
|
187
188
|
if (ctx.scope === 'global' && !cliRegistered) {
|
|
@@ -191,14 +192,15 @@ export const codexTransformer = {
|
|
|
191
192
|
// ─── memtrace-rail ─── Wire the discovery hook into .codex/hooks.json
|
|
192
193
|
// (PreToolUse, Bash matcher — Codex code discovery runs through the shell).
|
|
193
194
|
try {
|
|
194
|
-
|
|
195
|
+
if (ctx.mode !== 'refresh')
|
|
196
|
+
registerSettingsHook(codexRailHooksPath(ctx), ctx.memtraceBinary, 'codex', 'PreToolUse', CODEX_MATCHER);
|
|
195
197
|
}
|
|
196
198
|
catch (e) {
|
|
197
199
|
warnings.push(`failed to register Codex Rail hook: ${e.message}`);
|
|
198
200
|
}
|
|
199
201
|
return {
|
|
200
202
|
agent: 'codex',
|
|
201
|
-
skillsWritten:
|
|
203
|
+
skillsWritten: written,
|
|
202
204
|
skillsDir: rootDir,
|
|
203
205
|
mcpConfigPath,
|
|
204
206
|
mcpRegistered,
|
|
@@ -207,13 +209,7 @@ export const codexTransformer = {
|
|
|
207
209
|
},
|
|
208
210
|
async uninstall(ctx) {
|
|
209
211
|
const rootDir = skillsRoot(ctx);
|
|
210
|
-
|
|
211
|
-
for (const entry of fs.readdirSync(rootDir)) {
|
|
212
|
-
if (entry.startsWith('memtrace-')) {
|
|
213
|
-
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
212
|
+
removeManagedSkills(rootDir);
|
|
217
213
|
try {
|
|
218
214
|
removeSettingsHook(codexRailHooksPath(ctx), 'PreToolUse');
|
|
219
215
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { writeManagedSkill, removeManagedSkills } from '../managed-skills.js';
|
|
4
5
|
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
5
6
|
import { MEMTRACE_MCP_ENV, removeLegacyMcpServer } from './shared.js';
|
|
6
7
|
import { registerCursorRailHook, removeCursorRailHook } from './rail-hooks.js';
|
|
@@ -18,12 +19,10 @@ function railHooksPath(ctx) {
|
|
|
18
19
|
}
|
|
19
20
|
function writeSkill(skill, rootDir) {
|
|
20
21
|
const name = skill.filename.replace(/\.md$/, '');
|
|
21
|
-
const outDir = path.join(rootDir, name);
|
|
22
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
23
22
|
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
24
23
|
// Cursor requires frontmatter `name` matching the folder name.
|
|
25
24
|
const content = `---\nname: ${name}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
26
|
-
|
|
25
|
+
return writeManagedSkill(rootDir, name, content);
|
|
27
26
|
}
|
|
28
27
|
function isRecord(value) {
|
|
29
28
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -96,15 +95,7 @@ export const cursorTransformer = {
|
|
|
96
95
|
async install(skills, ctx) {
|
|
97
96
|
const rootDir = skillsRoot(ctx);
|
|
98
97
|
const warnings = cursorWorkspaceBoundaryWarnings(ctx);
|
|
99
|
-
|
|
100
|
-
for (const entry of fs.readdirSync(rootDir)) {
|
|
101
|
-
if (entry.startsWith('memtrace-')) {
|
|
102
|
-
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const s of skills)
|
|
107
|
-
writeSkill(s, rootDir);
|
|
98
|
+
const written = skills.filter(skill => writeSkill(skill, rootDir)).length;
|
|
108
99
|
let mcpRegistered = false;
|
|
109
100
|
if (!ctx.skipMcp) {
|
|
110
101
|
const result = registerCursorMcpAt(mcpPath(ctx), ctx.memtraceBinary);
|
|
@@ -116,14 +107,15 @@ export const cursorTransformer = {
|
|
|
116
107
|
// ─── memtrace-rail ─── Wire the Memtrace-first discovery hook (default
|
|
117
108
|
// observe). Failure must not block the rest of the install.
|
|
118
109
|
try {
|
|
119
|
-
|
|
110
|
+
if (ctx.mode !== 'refresh')
|
|
111
|
+
registerCursorRailHook(railHooksPath(ctx), ctx.memtraceBinary);
|
|
120
112
|
}
|
|
121
113
|
catch (e) {
|
|
122
114
|
console.warn(`memtrace: failed to register Cursor Rail hook: ${e.message}`);
|
|
123
115
|
}
|
|
124
116
|
return {
|
|
125
117
|
agent: 'cursor',
|
|
126
|
-
skillsWritten:
|
|
118
|
+
skillsWritten: written,
|
|
127
119
|
skillsDir: rootDir,
|
|
128
120
|
mcpConfigPath: mcpPath(ctx),
|
|
129
121
|
mcpRegistered,
|
|
@@ -133,13 +125,7 @@ export const cursorTransformer = {
|
|
|
133
125
|
async uninstall(ctx) {
|
|
134
126
|
// 1. Remove memtrace-* skill dirs
|
|
135
127
|
const rootDir = skillsRoot(ctx);
|
|
136
|
-
|
|
137
|
-
for (const entry of fs.readdirSync(rootDir)) {
|
|
138
|
-
if (entry.startsWith('memtrace-')) {
|
|
139
|
-
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
128
|
+
removeManagedSkills(rootDir);
|
|
143
129
|
// 1b. Remove the Rail hook.
|
|
144
130
|
try {
|
|
145
131
|
removeCursorRailHook(railHooksPath(ctx));
|
|
@@ -46,10 +46,11 @@ export const geminiTransformer = {
|
|
|
46
46
|
// (PR #14460) — without them a registered BeforeTool hook NEVER fires, so we
|
|
47
47
|
// enable both here (only if the user hasn't already set them).
|
|
48
48
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if (ctx.mode !== 'refresh')
|
|
50
|
+
registerSettingsHook(settingsPath, ctx.memtraceBinary, 'gemini', 'BeforeTool', GEMINI_MATCHER, {
|
|
51
|
+
enableHooks: true,
|
|
52
|
+
enableMessageBusIntegration: true,
|
|
53
|
+
});
|
|
53
54
|
}
|
|
54
55
|
catch (e) {
|
|
55
56
|
warnings.push(`failed to register Gemini Rail hook: ${e.message}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { writeManagedSkill, removeManagedSkills } from '../managed-skills.js';
|
|
4
4
|
import { registerMcpServersJsonAt, removeMcpServersJsonAt, skillName, } from './shared.js';
|
|
5
5
|
function steeringRoot(ctx) {
|
|
6
6
|
return ctx.scope === 'global'
|
|
@@ -24,25 +24,16 @@ function writeSteering(skill, rootDir) {
|
|
|
24
24
|
'',
|
|
25
25
|
skill.body,
|
|
26
26
|
].join('\n');
|
|
27
|
-
|
|
28
|
-
fs.writeFileSync(path.join(rootDir, `${name}.md`), content);
|
|
27
|
+
return writeManagedSkill(rootDir, name, content, true);
|
|
29
28
|
}
|
|
30
29
|
function removeMemtraceSteering(rootDir) {
|
|
31
|
-
|
|
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
|
-
}
|
|
30
|
+
removeManagedSkills(rootDir, true);
|
|
38
31
|
}
|
|
39
32
|
export const kiroTransformer = {
|
|
40
33
|
name: 'kiro',
|
|
41
34
|
async install(skills, ctx) {
|
|
42
35
|
const rootDir = steeringRoot(ctx);
|
|
43
|
-
|
|
44
|
-
for (const skill of skills)
|
|
45
|
-
writeSteering(skill, rootDir);
|
|
36
|
+
const written = skills.filter(skill => writeSteering(skill, rootDir)).length;
|
|
46
37
|
let mcpRegistered = false;
|
|
47
38
|
const warnings = [];
|
|
48
39
|
const mcpConfigPath = mcpPath(ctx);
|
|
@@ -55,7 +46,7 @@ export const kiroTransformer = {
|
|
|
55
46
|
}
|
|
56
47
|
return {
|
|
57
48
|
agent: 'kiro',
|
|
58
|
-
skillsWritten:
|
|
49
|
+
skillsWritten: written,
|
|
59
50
|
skillsDir: rootDir,
|
|
60
51
|
mcpConfigPath,
|
|
61
52
|
mcpRegistered,
|
|
@@ -31,13 +31,14 @@ export const opencodeTransformer = {
|
|
|
31
31
|
// ─── memtrace-rail ─── Write the in-process discovery plugin (OpenCode
|
|
32
32
|
// has no stdio hook). Defaults to observe; fail-open. Best effort.
|
|
33
33
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (ctx.mode !== 'refresh') {
|
|
35
|
+
if (railIsActive()) {
|
|
36
|
+
writeOpenCodeRailPlugin(ctx, openCodePluginSource(ctx.memtraceBinary));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
for (const pluginPath of openCodeRailPluginPaths(ctx)) {
|
|
40
|
+
fs.rmSync(pluginPath, { force: true });
|
|
41
|
+
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
}
|
package/dist/transformers/pi.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { removeMemtraceSkills, skillName } from './shared.js';
|
|
6
|
+
import { writeManagedSkill } from '../managed-skills.js';
|
|
6
7
|
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
7
8
|
import { commandExists } from '../utils.js';
|
|
8
9
|
import { MEMTRACE_TOOL_NAMES } from '../tool-names.js';
|
|
@@ -80,15 +81,14 @@ function skillMarkdownForPi(skill, toolNames) {
|
|
|
80
81
|
return `---\nname: ${name}\ndescription: "${safeDesc}"${toolsLine}\n---\n\n${body}`;
|
|
81
82
|
}
|
|
82
83
|
function writeSkillsForPi(skills, rootDir) {
|
|
83
|
-
|
|
84
|
+
let written = 0;
|
|
84
85
|
const toolNames = memtraceToolNames(skills);
|
|
85
86
|
for (const skill of skills) {
|
|
86
87
|
const name = skillName(skill);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
fs.writeFileSync(path.join(outDir, 'SKILL.md'), skillMarkdownForPi(skill, toolNames));
|
|
88
|
+
if (writeManagedSkill(rootDir, name, skillMarkdownForPi(skill, toolNames)))
|
|
89
|
+
written++;
|
|
90
90
|
}
|
|
91
|
-
return
|
|
91
|
+
return written;
|
|
92
92
|
}
|
|
93
93
|
function packageEntrySource(entry) {
|
|
94
94
|
if (typeof entry === 'string')
|
|
@@ -153,7 +153,9 @@ export const piTransformer = {
|
|
|
153
153
|
warnings: [`${e.message}`],
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
-
const result =
|
|
156
|
+
const result = ctx.mode === 'refresh'
|
|
157
|
+
? { registered: false, packagePath, backupPath: undefined }
|
|
158
|
+
: registerPiPackageAt(settingsFile, packagePath);
|
|
157
159
|
if (!result.registered && result.backupPath) {
|
|
158
160
|
warnings.push(`Pi settings.json was malformed; backed up to ${result.backupPath}.`);
|
|
159
161
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* hook. Each supported agent intercepts tool calls differently, so each gets a
|
|
4
4
|
* thin host-specific registration that points at the SAME router
|
|
5
5
|
* (`memtrace route --hook --host <h>`). All entries are idempotent and remove
|
|
6
|
-
* cleanly. Hooks default to
|
|
7
|
-
*
|
|
6
|
+
* cleanly. Hooks default to nudge mode (context without suppressing ordinary
|
|
7
|
+
* tools); explicit `MEMTRACE_RAIL` and rail.json modes remain authoritative.
|
|
8
8
|
*
|
|
9
9
|
* Host families:
|
|
10
10
|
* - Claude Code → `.claude/settings.json` hooks.PreToolUse (in claude.ts)
|
|
@@ -182,7 +182,8 @@ export function registerCursorRailHook(hooksPath, binary, options = {}) {
|
|
|
182
182
|
preserved.push({ command: cmd });
|
|
183
183
|
cfg.hooks[event] = preserved;
|
|
184
184
|
// Grep enrichment: Cursor `postToolUse` on its Shell and Grep tools answers
|
|
185
|
-
// with `additional_context` (design section 9).
|
|
185
|
+
// with `additional_context` (design section 9). Fresh Nudge enables it;
|
|
186
|
+
// explicit mode/enrichment settings keep their existing meaning.
|
|
186
187
|
const post = Array.isArray(cfg.hooks[CURSOR_ENRICH_EVENT]) ? cfg.hooks[CURSOR_ENRICH_EVENT] : [];
|
|
187
188
|
const postPreserved = post.filter((e) => !(isRecord(e) && typeof e.command === 'string' && isRailCommand(e.command)));
|
|
188
189
|
if (options.enrich ?? enrichEnabled()) {
|
|
@@ -356,19 +357,28 @@ export function removeCursorRailHook(hooksPath) {
|
|
|
356
357
|
*/
|
|
357
358
|
export function openCodePluginSource(binary) {
|
|
358
359
|
const hook = railHookInvocation('open-code');
|
|
360
|
+
const runtimeModule = new URL('../rail-runtime.js', import.meta.url).href;
|
|
359
361
|
return `// AUTO-GENERATED by memtrace install — Memtrace Rail discovery hook.
|
|
360
|
-
// Routes OpenCode tool calls through \`${binary} route\`.
|
|
361
|
-
//
|
|
362
|
+
// Routes OpenCode tool calls through \`${binary} route\`.
|
|
363
|
+
// Defaults to non-blocking context; explicit Rail mode overrides are retained.
|
|
364
|
+
// Remove via 'memtrace uninstall'.
|
|
362
365
|
import { execFileSync } from 'node:child_process';
|
|
366
|
+
import { enrichEnabled, railConfig, railMode } from ${JSON.stringify(runtimeModule)};
|
|
363
367
|
|
|
364
368
|
const BIN = ${JSON.stringify(hook.command)};
|
|
365
369
|
const ARGS = ${JSON.stringify([...hook.args, '--json'])};
|
|
370
|
+
const POST_ARGS = ${JSON.stringify([...hook.args, '--phase', 'post'])};
|
|
366
371
|
|
|
367
372
|
export const MemtraceRail = async ({ directory }) => ({
|
|
368
373
|
'tool.execute.before': async (input, output) => {
|
|
369
374
|
try {
|
|
370
375
|
const tool = input?.tool;
|
|
371
376
|
if (tool !== 'bash' && tool !== 'grep' && tool !== 'glob') return;
|
|
377
|
+
const config = railConfig();
|
|
378
|
+
const mode = railMode(process.env, config);
|
|
379
|
+
// The post hook supplies Nudge context once, after the ordinary search.
|
|
380
|
+
// Rail and Strict retain their existing pre-execution enforcement.
|
|
381
|
+
if (enrichEnabled(process.env, config) && mode !== 'rail' && mode !== 'strict') return;
|
|
372
382
|
const attempt = {
|
|
373
383
|
host: 'open-code',
|
|
374
384
|
cwd: output?.args?.cwd || directory || process.cwd(),
|
|
@@ -389,6 +399,33 @@ export const MemtraceRail = async ({ directory }) => ({
|
|
|
389
399
|
if (e && String(e.message || '').startsWith('[Memtrace Rail]')) throw e;
|
|
390
400
|
}
|
|
391
401
|
},
|
|
402
|
+
// OpenCode @opencode-ai/plugin 1.19.4: native registry tools expose the
|
|
403
|
+
// mutable {title, output, metadata} tool.execute.after contract.
|
|
404
|
+
'tool.execute.after': async (input, output) => {
|
|
405
|
+
try {
|
|
406
|
+
const tool = input?.tool;
|
|
407
|
+
if ((tool !== 'bash' && tool !== 'grep') || typeof output?.output !== 'string'
|
|
408
|
+
|| !enrichEnabled() || typeof input?.sessionID !== 'string') return;
|
|
409
|
+
const payload = {
|
|
410
|
+
cwd: input?.args?.cwd || directory || process.cwd(),
|
|
411
|
+
session_id: input.sessionID,
|
|
412
|
+
tool_use_id: input.callID,
|
|
413
|
+
tool_name: tool === 'bash' ? 'Bash' : 'Grep',
|
|
414
|
+
tool_input: input.args || {},
|
|
415
|
+
tool_response: { ...(output.metadata || {}), stdout: output.output },
|
|
416
|
+
};
|
|
417
|
+
const raw = execFileSync(BIN, POST_ARGS, {
|
|
418
|
+
input: JSON.stringify(payload), timeout: 2000, encoding: 'utf8', windowsHide: true,
|
|
419
|
+
});
|
|
420
|
+
const text = JSON.parse(raw)?.hookSpecificOutput?.additionalContext;
|
|
421
|
+
if (typeof text === 'string' && text.trim() && Buffer.byteLength(text, 'utf8') <= 4000) {
|
|
422
|
+
output.output += '\\n\\n' + text;
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
// Missing owner, invalid query, timeout or malformed output leaves the
|
|
426
|
+
// original ordinary-tool response byte-for-byte unchanged.
|
|
427
|
+
}
|
|
428
|
+
},
|
|
392
429
|
});
|
|
393
430
|
`;
|
|
394
431
|
}
|