memtrace-skills 1.2.10 → 1.2.11
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/managed-skills.d.ts +4 -0
- package/dist/managed-skills.js +95 -0
- package/dist/rail-hook.js +3 -2
- package/dist/rail-runtime.d.ts +3 -2
- package/dist/rail-runtime.js +13 -4
- package/dist/transformers/claude.d.ts +3 -3
- package/dist/transformers/claude.js +22 -62
- package/dist/transformers/codex.js +5 -20
- package/dist/transformers/cursor.js +5 -20
- package/dist/transformers/kiro.js +5 -14
- package/dist/transformers/pi.js +5 -5
- package/dist/transformers/rail-hooks.js +42 -5
- package/dist/transformers/shared.js +6 -13
- package/package.json +2 -2
- 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
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const RECEIPT_SUFFIX = ".memtrace-managed.sha256";
|
|
2
|
+
export declare function skillDigest(content: string): string;
|
|
3
|
+
export declare function writeManagedSkill(rootDir: string, name: string, content: string, steering?: boolean): boolean;
|
|
4
|
+
export declare function removeManagedSkills(rootDir: string, steering?: boolean): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
// A name is not ownership. Receipts bind the exact last-installed bytes; the
|
|
6
|
+
// stock catalogue admits upgrades from installers that predate receipts.
|
|
7
|
+
const stockFile = fileURLToPath(new URL('../skills/managed-stock-sha256.txt', import.meta.url));
|
|
8
|
+
const stock = new Set(fs.readFileSync(stockFile, 'utf8').split(/\r?\n/).filter(line => !line.startsWith('#')));
|
|
9
|
+
export const RECEIPT_SUFFIX = '.memtrace-managed.sha256';
|
|
10
|
+
export function skillDigest(content) {
|
|
11
|
+
return createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
function safeFile(rootDir, relative) {
|
|
14
|
+
const parts = relative.split(/[\\/]/);
|
|
15
|
+
if (parts.some(part => !part || part === '.' || part === '..'))
|
|
16
|
+
return;
|
|
17
|
+
let current = path.resolve(rootDir);
|
|
18
|
+
if (fs.existsSync(current) && (!fs.lstatSync(current).isDirectory() || fs.lstatSync(current).isSymbolicLink()))
|
|
19
|
+
return;
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
current = path.join(current, part);
|
|
22
|
+
try {
|
|
23
|
+
const stat = fs.lstatSync(current);
|
|
24
|
+
if (stat.isSymbolicLink() || (!stat.isDirectory() && current !== path.resolve(rootDir, relative)))
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== 'ENOENT')
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const receipt = fs.lstatSync(current + RECEIPT_SUFFIX);
|
|
34
|
+
if (!receipt.isFile() || receipt.isSymbolicLink())
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== 'ENOENT')
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return current;
|
|
42
|
+
}
|
|
43
|
+
function owned(file, name, content) {
|
|
44
|
+
const digest = skillDigest(content);
|
|
45
|
+
if (stock.has(`${name} ${digest}`))
|
|
46
|
+
return true;
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(file + RECEIPT_SUFFIX, 'utf8').trim() === digest;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code !== 'ENOENT')
|
|
52
|
+
throw error;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function writeManagedSkill(rootDir, name, content, steering = false) {
|
|
57
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name))
|
|
58
|
+
throw new Error(`Invalid managed skill name: ${name}`);
|
|
59
|
+
const file = safeFile(rootDir, steering ? `${name}.md` : `${name}/SKILL.md`);
|
|
60
|
+
if (!file)
|
|
61
|
+
return false;
|
|
62
|
+
if (fs.existsSync(file)) {
|
|
63
|
+
if (!fs.lstatSync(file).isFile())
|
|
64
|
+
return false;
|
|
65
|
+
const prior = fs.readFileSync(file, 'utf8');
|
|
66
|
+
if (prior !== content && !owned(file, name, prior)) {
|
|
67
|
+
console.warn(`memtrace: preserved customized skill ${file}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
72
|
+
fs.writeFileSync(file, content);
|
|
73
|
+
fs.writeFileSync(file + RECEIPT_SUFFIX, `${skillDigest(content)}\n`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
export function removeManagedSkills(rootDir, steering = false) {
|
|
77
|
+
if (!fs.existsSync(rootDir) || fs.lstatSync(rootDir).isSymbolicLink())
|
|
78
|
+
return;
|
|
79
|
+
for (const entry of fs.readdirSync(rootDir)) {
|
|
80
|
+
const name = steering ? entry.replace(/\.md$/, '') : entry;
|
|
81
|
+
if (!/^memtrace-[a-z0-9-]+$/.test(name) || (steering && !entry.endsWith('.md')))
|
|
82
|
+
continue;
|
|
83
|
+
const file = safeFile(rootDir, steering ? entry : `${entry}/SKILL.md`);
|
|
84
|
+
if (!file || !fs.existsSync(file) || !fs.lstatSync(file).isFile())
|
|
85
|
+
continue;
|
|
86
|
+
if (!owned(file, name, fs.readFileSync(file, 'utf8')))
|
|
87
|
+
continue;
|
|
88
|
+
fs.unlinkSync(file);
|
|
89
|
+
if (fs.existsSync(file + RECEIPT_SUFFIX))
|
|
90
|
+
fs.unlinkSync(file + RECEIPT_SUFFIX);
|
|
91
|
+
// Never recursively delete a skill folder: user references/assets survive.
|
|
92
|
+
if (!steering && fs.readdirSync(path.dirname(file)).length === 0)
|
|
93
|
+
fs.rmdirSync(path.dirname(file));
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/rail-hook.js
CHANGED
|
@@ -118,7 +118,8 @@ async function main() {
|
|
|
118
118
|
const phase = arg('--phase');
|
|
119
119
|
const event = arg('--event');
|
|
120
120
|
const enrichPath = phase === 'post' || event === 'precompact';
|
|
121
|
-
//
|
|
121
|
+
// Explicit off (or a non-Nudge mode without opt-in) means no stdin parse,
|
|
122
|
+
// no lease read and no HTTP. Fresh Nudge enables bounded context by default.
|
|
122
123
|
if (enrichPath && !enrichEnabled(env))
|
|
123
124
|
return;
|
|
124
125
|
const input = await readStdin();
|
|
@@ -169,7 +170,7 @@ async function main() {
|
|
|
169
170
|
// JSON-mode/OpenCode and Windsurf retain their CLI response contracts.
|
|
170
171
|
// Other hosts use the in-process daemon router with no native binary spawn.
|
|
171
172
|
if (!jsonMode && host !== 'windsurf' && !runtime.routeBinary) {
|
|
172
|
-
const query = new URLSearchParams({ host, mode: env.MEMTRACE_RAIL || '',
|
|
173
|
+
const query = new URLSearchParams({ host, mode: env.MEMTRACE_RAIL || '', enrich: env.MEMTRACE_ENRICH || '',
|
|
173
174
|
escape: env.MEMTRACE_RAW_SEARCH || '' });
|
|
174
175
|
const result = await postToDaemon(runtime, `/api/rail/hook?${query}`, input, 5500);
|
|
175
176
|
if (result.status === 200 && result.body && alive()) {
|
package/dist/rail-runtime.d.ts
CHANGED
|
@@ -34,10 +34,11 @@ export interface RailConfig {
|
|
|
34
34
|
export declare function railConfig(): RailConfig;
|
|
35
35
|
/**
|
|
36
36
|
* Grep enrichment on/off: `MEMTRACE_ENRICH`, then rail.json `"enrich"`, then
|
|
37
|
-
*
|
|
37
|
+
* non-blocking Nudge mode. Explicit Observe, Off, Rail and Strict modes do
|
|
38
|
+
* not implicitly enable this additional post-search path.
|
|
38
39
|
*/
|
|
39
40
|
export declare function enrichEnabled(env?: NodeJS.ProcessEnv, config?: RailConfig): boolean;
|
|
40
|
-
/** Rail mode: `MEMTRACE_RAIL`, then rail.json `"mode"`, then
|
|
41
|
+
/** Rail mode: `MEMTRACE_RAIL`, then rail.json `"mode"`, then non-blocking nudge. */
|
|
41
42
|
export declare function railMode(env?: NodeJS.ProcessEnv, config?: RailConfig): string;
|
|
42
43
|
/**
|
|
43
44
|
* Live leases. Reading them also deletes garbage: a lease whose owner and
|
package/dist/rail-runtime.js
CHANGED
|
@@ -93,19 +93,28 @@ function flag(value) {
|
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
95
|
* Grep enrichment on/off: `MEMTRACE_ENRICH`, then rail.json `"enrich"`, then
|
|
96
|
-
*
|
|
96
|
+
* non-blocking Nudge mode. Explicit Observe, Off, Rail and Strict modes do
|
|
97
|
+
* not implicitly enable this additional post-search path.
|
|
97
98
|
*/
|
|
98
99
|
export function enrichEnabled(env = process.env, config = railConfig()) {
|
|
99
|
-
|
|
100
|
+
// Durable opt-outs remain effective even when an old agent inherited an
|
|
101
|
+
// enabling environment. This is the same no-hooks gate used by the lease.
|
|
102
|
+
if (env.MEMTRACE_INSTALL_NO_HOOKS === '1' || config.hooksEnabled === false
|
|
103
|
+
|| (typeof config.mode === 'string' && config.mode.trim().toLowerCase() === 'off')
|
|
104
|
+
|| env.MEMTRACE_RAIL?.trim().toLowerCase() === 'off'
|
|
105
|
+
|| env.MEMTRACE_RAW_SEARCH?.trim().toLowerCase() === 'allow')
|
|
106
|
+
return false;
|
|
107
|
+
return flag(env.MEMTRACE_ENRICH)
|
|
108
|
+
?? (typeof config.enrich === 'boolean' ? config.enrich : railMode(env, config) === 'nudge');
|
|
100
109
|
}
|
|
101
|
-
/** Rail mode: `MEMTRACE_RAIL`, then rail.json `"mode"`, then
|
|
110
|
+
/** Rail mode: `MEMTRACE_RAIL`, then rail.json `"mode"`, then non-blocking nudge. */
|
|
102
111
|
export function railMode(env = process.env, config = railConfig()) {
|
|
103
112
|
const valid = ['off', 'observe', 'nudge', 'rail', 'strict'];
|
|
104
113
|
const fromEnv = env.MEMTRACE_RAIL?.trim().toLowerCase();
|
|
105
114
|
if (fromEnv && valid.includes(fromEnv))
|
|
106
115
|
return fromEnv;
|
|
107
116
|
const fromConfig = typeof config.mode === 'string' ? config.mode.trim().toLowerCase() : '';
|
|
108
|
-
return valid.includes(fromConfig) ? fromConfig : '
|
|
117
|
+
return valid.includes(fromConfig) ? fromConfig : 'nudge';
|
|
109
118
|
}
|
|
110
119
|
/**
|
|
111
120
|
* Live leases. Reading them also deletes garbage: a lease whose owner and
|
|
@@ -32,8 +32,8 @@ export declare function claudeRailHookCommand(_memtraceBinary: string, platform?
|
|
|
32
32
|
* This is what makes Rail "part of memtrace" rather than a manual install:
|
|
33
33
|
* `memtrace install` wires a PreToolUse hook that pipes every Grep/Glob/Bash
|
|
34
34
|
* attempt through the Rail router (see {@link claudeRailHookCommand} for the
|
|
35
|
-
* transport). While a daemon is running the hook defaults to **
|
|
36
|
-
*
|
|
35
|
+
* transport). While a daemon is running the hook defaults to **nudge** —
|
|
36
|
+
* context alongside ordinary tools. Explicit modes are preserved. Mode is intentionally NOT
|
|
37
37
|
* baked into the command so the user can change it via env without
|
|
38
38
|
* re-installing.
|
|
39
39
|
*
|
|
@@ -49,7 +49,7 @@ export declare const ENRICH_HOOK_MATCHER = "Grep|Bash|PowerShell";
|
|
|
49
49
|
/** Claude hook timeout for the enrichment hooks, in seconds. */
|
|
50
50
|
export declare const ENRICH_HOOK_TIMEOUT_S = 2;
|
|
51
51
|
export interface RailHookOptions {
|
|
52
|
-
/** Register
|
|
52
|
+
/** Register grep-enrichment hooks. Explicit env/config wins; implicit on only in Nudge. */
|
|
53
53
|
enrich?: boolean;
|
|
54
54
|
/** The Rail mode the PreToolUse decision depends on. Default: env, then rail.json. */
|
|
55
55
|
mode?: string;
|
|
@@ -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'
|
|
@@ -553,6 +555,9 @@ export function claudeUserConfigFile() {
|
|
|
553
555
|
* 3. Register MCP server for memtrace tools
|
|
554
556
|
*/
|
|
555
557
|
export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
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,15 +587,12 @@ 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();
|
|
@@ -599,27 +601,16 @@ export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
|
599
601
|
// Skills ship via the memtrace-skills@memtrace plugin only. Writing
|
|
600
602
|
// bare copies to ~/.claude/skills/memtrace-* duplicates every skill
|
|
601
603
|
// in Claude Code when the plugin is also enabled (issue #11).
|
|
602
|
-
return { cacheDir, skillCount
|
|
604
|
+
return { cacheDir, skillCount };
|
|
603
605
|
}
|
|
604
606
|
/**
|
|
605
607
|
* Write skills to ~/.claude/skills/ for SDK-based integrations.
|
|
606
608
|
*/
|
|
607
609
|
function writeUserLevelSkills(skills) {
|
|
608
610
|
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
611
|
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);
|
|
612
|
+
const content = transformForClaude(skill)[0].content;
|
|
613
|
+
writeManagedSkill(userSkillsDir, skill.filename.replace(/\.md$/, ''), content);
|
|
623
614
|
}
|
|
624
615
|
}
|
|
625
616
|
/**
|
|
@@ -652,22 +643,8 @@ export async function uninstallClaudePlugin() {
|
|
|
652
643
|
}
|
|
653
644
|
removeClaudeMarketplaceCacheDirs();
|
|
654
645
|
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
|
-
}
|
|
646
|
+
// Customized skills and their extra assets are not installer-owned.
|
|
647
|
+
removeManagedSkills(path.join(os.homedir(), '.claude', 'skills'));
|
|
671
648
|
}
|
|
672
649
|
/**
|
|
673
650
|
* Write/merge the memtrace MCP server entry into a project-local .mcp.json.
|
|
@@ -712,22 +689,11 @@ export const claudeTransformer = {
|
|
|
712
689
|
}
|
|
713
690
|
// Local scope: write skills into <cwd>/.claude/skills/
|
|
714
691
|
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
692
|
let count = 0;
|
|
723
693
|
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++;
|
|
694
|
+
const content = transformForClaude(skill)[0].content;
|
|
695
|
+
if (writeManagedSkill(skillsDir, skill.filename.replace(/\.md$/, ''), content))
|
|
696
|
+
count++;
|
|
731
697
|
}
|
|
732
698
|
// MCP for local scope: .mcp.json at project root (Claude's project-level convention)
|
|
733
699
|
const mcpConfigPath = path.join(ctx.cwd, '.mcp.json');
|
|
@@ -751,13 +717,7 @@ export const claudeTransformer = {
|
|
|
751
717
|
}
|
|
752
718
|
// Local: remove <cwd>/.claude/skills/memtrace-* and the memtrace entry from <cwd>/.mcp.json
|
|
753
719
|
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
|
-
}
|
|
720
|
+
removeManagedSkills(skillsDir);
|
|
761
721
|
const mcpPath = path.join(ctx.cwd, '.mcp.json');
|
|
762
722
|
const { value, corrupted } = safeReadJson(mcpPath);
|
|
763
723
|
if (!corrupted && value?.mcpServers?.['memtrace']) {
|
|
@@ -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 { commandExists, execCommand } from '../utils.js';
|
|
5
6
|
import { registerSettingsHook, removeSettingsHook, CODEX_MATCHER, } from './rail-hooks.js';
|
|
6
7
|
const MCP_SERVER_NAME = 'memtrace';
|
|
@@ -19,11 +20,9 @@ export function codexConfigPath(ctx) {
|
|
|
19
20
|
}
|
|
20
21
|
function writeSkill(skill, rootDir) {
|
|
21
22
|
const name = skill.filename.replace(/\.md$/, '');
|
|
22
|
-
const outDir = path.join(rootDir, name);
|
|
23
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
24
23
|
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
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 tomlString(value) {
|
|
29
28
|
return `"${value
|
|
@@ -168,15 +167,7 @@ export const codexTransformer = {
|
|
|
168
167
|
name: 'codex',
|
|
169
168
|
async install(skills, ctx) {
|
|
170
169
|
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);
|
|
170
|
+
const written = skills.filter(skill => writeSkill(skill, rootDir)).length;
|
|
180
171
|
let mcpRegistered = false;
|
|
181
172
|
const warnings = [];
|
|
182
173
|
const mcpConfigPath = codexConfigPath(ctx);
|
|
@@ -198,7 +189,7 @@ export const codexTransformer = {
|
|
|
198
189
|
}
|
|
199
190
|
return {
|
|
200
191
|
agent: 'codex',
|
|
201
|
-
skillsWritten:
|
|
192
|
+
skillsWritten: written,
|
|
202
193
|
skillsDir: rootDir,
|
|
203
194
|
mcpConfigPath,
|
|
204
195
|
mcpRegistered,
|
|
@@ -207,13 +198,7 @@ export const codexTransformer = {
|
|
|
207
198
|
},
|
|
208
199
|
async uninstall(ctx) {
|
|
209
200
|
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
|
-
}
|
|
201
|
+
removeManagedSkills(rootDir);
|
|
217
202
|
try {
|
|
218
203
|
removeSettingsHook(codexRailHooksPath(ctx), 'PreToolUse');
|
|
219
204
|
}
|
|
@@ -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);
|
|
@@ -123,7 +114,7 @@ export const cursorTransformer = {
|
|
|
123
114
|
}
|
|
124
115
|
return {
|
|
125
116
|
agent: 'cursor',
|
|
126
|
-
skillsWritten:
|
|
117
|
+
skillsWritten: written,
|
|
127
118
|
skillsDir: rootDir,
|
|
128
119
|
mcpConfigPath: mcpPath(ctx),
|
|
129
120
|
mcpRegistered,
|
|
@@ -133,13 +124,7 @@ export const cursorTransformer = {
|
|
|
133
124
|
async uninstall(ctx) {
|
|
134
125
|
// 1. Remove memtrace-* skill dirs
|
|
135
126
|
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
|
-
}
|
|
127
|
+
removeManagedSkills(rootDir);
|
|
143
128
|
// 1b. Remove the Rail hook.
|
|
144
129
|
try {
|
|
145
130
|
removeCursorRailHook(railHooksPath(ctx));
|
|
@@ -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,
|
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')
|
|
@@ -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
|
}
|