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.
Files changed (97) hide show
  1. package/dist/commands/doctor.d.ts +16 -0
  2. package/dist/commands/doctor.js +199 -0
  3. package/dist/commands/enterprise-install.d.ts +7 -0
  4. package/dist/commands/enterprise-install.js +129 -0
  5. package/dist/commands/install.d.ts +9 -0
  6. package/dist/commands/install.js +104 -0
  7. package/dist/commands/picker.d.ts +6 -0
  8. package/dist/commands/picker.js +22 -0
  9. package/dist/commands/rail-install.d.ts +7 -0
  10. package/dist/commands/rail-install.js +37 -0
  11. package/dist/fs-safe.d.ts +21 -0
  12. package/dist/fs-safe.js +35 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +87 -0
  15. package/dist/rail-install.d.ts +20 -0
  16. package/dist/rail-install.js +183 -0
  17. package/dist/skills.d.ts +17 -0
  18. package/dist/skills.js +64 -0
  19. package/dist/transformers/claude.d.ts +71 -0
  20. package/dist/transformers/claude.js +702 -0
  21. package/dist/transformers/codex.d.ts +8 -0
  22. package/dist/transformers/codex.js +294 -0
  23. package/dist/transformers/cursor.d.ts +7 -0
  24. package/dist/transformers/cursor.js +124 -0
  25. package/dist/transformers/gemini.d.ts +3 -0
  26. package/dist/transformers/gemini.js +78 -0
  27. package/dist/transformers/hermes.d.ts +5 -0
  28. package/dist/transformers/hermes.js +136 -0
  29. package/dist/transformers/index.d.ts +14 -0
  30. package/dist/transformers/index.js +24 -0
  31. package/dist/transformers/kiro.d.ts +2 -0
  32. package/dist/transformers/kiro.js +69 -0
  33. package/dist/transformers/opencode.d.ts +2 -0
  34. package/dist/transformers/opencode.js +77 -0
  35. package/dist/transformers/rail-hooks.d.ts +56 -0
  36. package/dist/transformers/rail-hooks.js +303 -0
  37. package/dist/transformers/shared.d.ts +18 -0
  38. package/dist/transformers/shared.js +129 -0
  39. package/dist/transformers/types.d.ts +40 -0
  40. package/dist/transformers/types.js +1 -0
  41. package/dist/transformers/vscode.d.ts +3 -0
  42. package/dist/transformers/vscode.js +53 -0
  43. package/dist/transformers/windsurf.d.ts +3 -0
  44. package/dist/transformers/windsurf.js +43 -0
  45. package/dist/utils.d.ts +5 -0
  46. package/dist/utils.js +22 -0
  47. package/package.json +50 -0
  48. package/plugins/memtrace-skills/.claude-plugin/plugin.json +23 -0
  49. package/plugins/memtrace-skills/references/mcp-parameters.md +302 -0
  50. package/plugins/memtrace-skills/skills/memtrace-api-topology/SKILL.md +58 -0
  51. package/plugins/memtrace-skills/skills/memtrace-change-impact-analysis/SKILL.md +75 -0
  52. package/plugins/memtrace-skills/skills/memtrace-cochange/SKILL.md +71 -0
  53. package/plugins/memtrace-skills/skills/memtrace-code-review/SKILL.md +41 -0
  54. package/plugins/memtrace-skills/skills/memtrace-codebase-exploration/SKILL.md +94 -0
  55. package/plugins/memtrace-skills/skills/memtrace-continuous-memory/SKILL.md +96 -0
  56. package/plugins/memtrace-skills/skills/memtrace-episode-replay/SKILL.md +94 -0
  57. package/plugins/memtrace-skills/skills/memtrace-evolution/SKILL.md +128 -0
  58. package/plugins/memtrace-skills/skills/memtrace-first/SKILL.md +194 -0
  59. package/plugins/memtrace-skills/skills/memtrace-fleet-coordination/SKILL.md +80 -0
  60. package/plugins/memtrace-skills/skills/memtrace-fleet-first/SKILL.md +125 -0
  61. package/plugins/memtrace-skills/skills/memtrace-fleet-publish-intent/SKILL.md +48 -0
  62. package/plugins/memtrace-skills/skills/memtrace-fleet-record-episode/SKILL.md +44 -0
  63. package/plugins/memtrace-skills/skills/memtrace-fleet-resolve/SKILL.md +54 -0
  64. package/plugins/memtrace-skills/skills/memtrace-graph/SKILL.md +67 -0
  65. package/plugins/memtrace-skills/skills/memtrace-impact/SKILL.md +58 -0
  66. package/plugins/memtrace-skills/skills/memtrace-incident-investigation/SKILL.md +112 -0
  67. package/plugins/memtrace-skills/skills/memtrace-index/SKILL.md +65 -0
  68. package/plugins/memtrace-skills/skills/memtrace-quality/SKILL.md +63 -0
  69. package/plugins/memtrace-skills/skills/memtrace-refactoring-guide/SKILL.md +103 -0
  70. package/plugins/memtrace-skills/skills/memtrace-relationships/SKILL.md +67 -0
  71. package/plugins/memtrace-skills/skills/memtrace-search/SKILL.md +93 -0
  72. package/plugins/memtrace-skills/skills/memtrace-session-continuity/SKILL.md +93 -0
  73. package/plugins/memtrace-skills/skills/memtrace-style-fingerprint/SKILL.md +105 -0
  74. package/skills/commands/memtrace-api-topology.md +65 -0
  75. package/skills/commands/memtrace-cochange.md +76 -0
  76. package/skills/commands/memtrace-evolution.md +135 -0
  77. package/skills/commands/memtrace-fleet-publish-intent.md +51 -0
  78. package/skills/commands/memtrace-fleet-record-episode.md +48 -0
  79. package/skills/commands/memtrace-fleet-resolve.md +59 -0
  80. package/skills/commands/memtrace-graph.md +75 -0
  81. package/skills/commands/memtrace-impact.md +64 -0
  82. package/skills/commands/memtrace-index.md +71 -0
  83. package/skills/commands/memtrace-quality.md +69 -0
  84. package/skills/commands/memtrace-relationships.md +73 -0
  85. package/skills/commands/memtrace-search.md +93 -0
  86. package/skills/workflows/memtrace-change-impact-analysis.md +85 -0
  87. package/skills/workflows/memtrace-code-review.md +48 -0
  88. package/skills/workflows/memtrace-codebase-exploration.md +108 -0
  89. package/skills/workflows/memtrace-continuous-memory.md +104 -0
  90. package/skills/workflows/memtrace-episode-replay.md +100 -0
  91. package/skills/workflows/memtrace-first.md +194 -0
  92. package/skills/workflows/memtrace-fleet-coordination.md +87 -0
  93. package/skills/workflows/memtrace-fleet-first.md +132 -0
  94. package/skills/workflows/memtrace-incident-investigation.md +125 -0
  95. package/skills/workflows/memtrace-refactoring-guide.md +116 -0
  96. package/skills/workflows/memtrace-session-continuity.md +98 -0
  97. package/skills/workflows/memtrace-style-fingerprint.md +111 -0
@@ -0,0 +1,8 @@
1
+ import { Transformer, InstallContext } from './types.js';
2
+ export declare function codexConfigPath(ctx: InstallContext): string;
3
+ export declare function stripCodexMcpServer(raw: string): string;
4
+ export interface CodexMcpResult {
5
+ registered: boolean;
6
+ }
7
+ export declare function registerCodexMcpAt(configFile: string, binary: string): CodexMcpResult;
8
+ export declare const codexTransformer: Transformer;
@@ -0,0 +1,294 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { commandExists, execCommand } from '../utils.js';
5
+ import { registerSettingsHook, removeSettingsHook, CODEX_MATCHER, } from './rail-hooks.js';
6
+ const MCP_SERVER_NAME = 'memtrace';
7
+ const CODEX_MCP_SHIM_NAME = 'memtrace-mcp-codex-shim.cjs';
8
+ function codexRailHooksPath(ctx) {
9
+ const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
10
+ return path.join(base, 'hooks.json');
11
+ }
12
+ function skillsRoot(ctx) {
13
+ const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
14
+ return path.join(base, '.agents', 'skills');
15
+ }
16
+ export function codexConfigPath(ctx) {
17
+ const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
18
+ return path.join(base, 'config.toml');
19
+ }
20
+ function writeSkill(skill, rootDir) {
21
+ const name = skill.filename.replace(/\.md$/, '');
22
+ const outDir = path.join(rootDir, name);
23
+ fs.mkdirSync(outDir, { recursive: true });
24
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
25
+ const content = `---\nname: ${name}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
26
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
27
+ }
28
+ function tomlString(value) {
29
+ return `"${value
30
+ .replace(/\\/g, '\\\\')
31
+ .replace(/"/g, '\\"')
32
+ .replace(/\n/g, '\\n')
33
+ .replace(/\r/g, '\\r')}"`;
34
+ }
35
+ function writeTextAtomic(filePath, content) {
36
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
37
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
38
+ fs.writeFileSync(tmpPath, content);
39
+ fs.renameSync(tmpPath, filePath);
40
+ }
41
+ function codexMcpShimSource() {
42
+ return `#!/usr/bin/env node
43
+ const { spawn } = require('node:child_process');
44
+
45
+ const binary = process.argv[2] || 'memtrace';
46
+ const child = spawn(binary, ['mcp'], { stdio: ['pipe', 'pipe', 'pipe'] });
47
+
48
+ process.stdin.pipe(child.stdin);
49
+ child.stderr.pipe(process.stderr);
50
+
51
+ function stripAnnotations(line) {
52
+ let message;
53
+ try {
54
+ message = JSON.parse(line);
55
+ } catch {
56
+ return line;
57
+ }
58
+
59
+ const content = message && message.result && message.result.content;
60
+ if (!Array.isArray(content)) return line;
61
+
62
+ const assistantItems = [];
63
+ const fallbackItems = [];
64
+ for (const item of content) {
65
+ if (!item || typeof item !== 'object') continue;
66
+ const clean = { ...item };
67
+ const annotations = clean.annotations;
68
+ delete clean.annotations;
69
+ fallbackItems.push(clean);
70
+ const audience = annotations && annotations.audience;
71
+ if (Array.isArray(audience) && audience.includes('assistant')) {
72
+ assistantItems.push(clean);
73
+ }
74
+ }
75
+
76
+ message.result.content = assistantItems.length > 0 ? assistantItems : fallbackItems;
77
+ return JSON.stringify(message);
78
+ }
79
+
80
+ let buffer = '';
81
+ child.stdout.on('data', (chunk) => {
82
+ buffer += chunk.toString('utf8');
83
+ let index = buffer.indexOf('\\n');
84
+ while (index !== -1) {
85
+ const line = buffer.slice(0, index);
86
+ buffer = buffer.slice(index + 1);
87
+ process.stdout.write(stripAnnotations(line) + '\\n');
88
+ index = buffer.indexOf('\\n');
89
+ }
90
+ });
91
+
92
+ child.stdout.on('end', () => {
93
+ if (buffer.length > 0) process.stdout.write(stripAnnotations(buffer));
94
+ });
95
+
96
+ child.on('exit', (code, signal) => {
97
+ if (signal) process.kill(process.pid, signal);
98
+ process.exit(code ?? 0);
99
+ });
100
+ `;
101
+ }
102
+ function codexMcpShimPath(configFile) {
103
+ return path.join(path.dirname(configFile), CODEX_MCP_SHIM_NAME);
104
+ }
105
+ function writeCodexMcpShim(configFile) {
106
+ const shimPath = codexMcpShimPath(configFile);
107
+ writeTextAtomic(shimPath, codexMcpShimSource());
108
+ try {
109
+ fs.chmodSync(shimPath, 0o755);
110
+ }
111
+ catch {
112
+ // Windows and locked-down filesystems may reject chmod; Codex invokes it
113
+ // through Node, so executable bits are a convenience, not a requirement.
114
+ }
115
+ return shimPath;
116
+ }
117
+ function isMemtraceMcpTable(tableName) {
118
+ return tableName === `mcp_servers.${MCP_SERVER_NAME}`
119
+ || tableName === `mcp_servers."${MCP_SERVER_NAME}"`
120
+ || tableName.startsWith(`mcp_servers.${MCP_SERVER_NAME}.`)
121
+ || tableName.startsWith(`mcp_servers."${MCP_SERVER_NAME}".`);
122
+ }
123
+ /**
124
+ * Tables we want stripped on re-registration.
125
+ *
126
+ * PRESERVE [mcp_servers.memtrace.env] so user-set env vars
127
+ * (MEMTRACE_LICENSE_KEY, etc.) survive `memtrace install` re-registration.
128
+ * Only the top-level table + non-env sub-tables get stripped.
129
+ */
130
+ function isMemtraceMcpTableToStrip(tableName) {
131
+ if (tableName === `mcp_servers.${MCP_SERVER_NAME}.env`)
132
+ return false;
133
+ if (tableName === `mcp_servers."${MCP_SERVER_NAME}".env`)
134
+ return false;
135
+ return isMemtraceMcpTable(tableName);
136
+ }
137
+ export function stripCodexMcpServer(raw) {
138
+ const lines = raw.split(/\r?\n/);
139
+ const kept = [];
140
+ let skipping = false;
141
+ for (const line of lines) {
142
+ const table = line.match(/^\s*\[([^\]]+)\]\s*$/);
143
+ if (table) {
144
+ skipping = isMemtraceMcpTableToStrip(table[1].trim());
145
+ if (skipping)
146
+ continue;
147
+ }
148
+ if (!skipping)
149
+ kept.push(line);
150
+ }
151
+ return kept.join('\n').trimEnd();
152
+ }
153
+ /**
154
+ * Extracts the preserved `[mcp_servers.memtrace.env]` block (if any) and
155
+ * returns:
156
+ * - rest: the input with the env block removed
157
+ * - envBlock: the env block as a string (empty if absent)
158
+ *
159
+ * Used by `registerCodexMcpAt` so the env block can be reattached AFTER the
160
+ * freshly-written `[mcp_servers.memtrace]` parent for readable ordering.
161
+ */
162
+ function extractMemtraceEnvBlock(raw) {
163
+ const lines = raw.split(/\r?\n/);
164
+ const rest = [];
165
+ const envLines = [];
166
+ let inEnv = false;
167
+ for (const line of lines) {
168
+ const table = line.match(/^\s*\[([^\]]+)\]\s*$/);
169
+ if (table) {
170
+ const name = table[1].trim();
171
+ const isEnv = name === `mcp_servers.${MCP_SERVER_NAME}.env`
172
+ || name === `mcp_servers."${MCP_SERVER_NAME}".env`;
173
+ if (isEnv) {
174
+ inEnv = true;
175
+ envLines.push(line);
176
+ continue;
177
+ }
178
+ // Any other table header closes the env capture.
179
+ inEnv = false;
180
+ }
181
+ if (inEnv) {
182
+ envLines.push(line);
183
+ }
184
+ else {
185
+ rest.push(line);
186
+ }
187
+ }
188
+ return {
189
+ rest: rest.join('\n').replace(/\n+$/, ''),
190
+ envBlock: envLines.join('\n').replace(/\n+$/, ''),
191
+ };
192
+ }
193
+ export function registerCodexMcpAt(configFile, binary) {
194
+ const existing = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf-8') : '';
195
+ // Strip leaves the env sub-table intact; split it out so we can reorder it
196
+ // after the freshly written parent block for readability.
197
+ const stripped = stripCodexMcpServer(existing);
198
+ const { rest, envBlock } = extractMemtraceEnvBlock(stripped);
199
+ const shimPath = writeCodexMcpShim(configFile);
200
+ const block = [
201
+ `[mcp_servers.${MCP_SERVER_NAME}]`,
202
+ `command = ${tomlString(process.execPath)}`,
203
+ `args = [${[shimPath, binary].map(tomlString).join(', ')}]`,
204
+ ].join('\n');
205
+ const parts = [];
206
+ if (rest)
207
+ parts.push(rest);
208
+ parts.push(block);
209
+ if (envBlock)
210
+ parts.push(envBlock);
211
+ const next = `${parts.join('\n\n')}\n`;
212
+ writeTextAtomic(configFile, next);
213
+ return { registered: true };
214
+ }
215
+ function shellQuote(value) {
216
+ if (process.platform === 'win32')
217
+ return `"${value.replace(/"/g, '\\"')}"`;
218
+ return `'${value.replace(/'/g, `'\\''`)}'`;
219
+ }
220
+ async function tryCodexMcpAdd(binary) {
221
+ if (!(await commandExists('codex')))
222
+ return false;
223
+ try {
224
+ await execCommand(`codex mcp add ${MCP_SERVER_NAME} -- ${shellQuote(binary)} mcp`, { timeoutMs: 5_000 });
225
+ return true;
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ }
231
+ export const codexTransformer = {
232
+ name: 'codex',
233
+ async install(skills, ctx) {
234
+ const rootDir = skillsRoot(ctx);
235
+ if (fs.existsSync(rootDir)) {
236
+ for (const entry of fs.readdirSync(rootDir)) {
237
+ if (entry.startsWith('memtrace-')) {
238
+ fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
239
+ }
240
+ }
241
+ }
242
+ for (const s of skills)
243
+ writeSkill(s, rootDir);
244
+ let mcpRegistered = false;
245
+ const warnings = [];
246
+ const mcpConfigPath = codexConfigPath(ctx);
247
+ if (!ctx.skipMcp) {
248
+ const cliRegistered = ctx.scope === 'global' && await tryCodexMcpAdd(ctx.memtraceBinary);
249
+ registerCodexMcpAt(mcpConfigPath, ctx.memtraceBinary);
250
+ mcpRegistered = true;
251
+ if (ctx.scope === 'global' && !cliRegistered) {
252
+ warnings.push('codex CLI MCP registration was unavailable; wrote ~/.codex/config.toml directly.');
253
+ }
254
+ }
255
+ // ─── memtrace-rail ─── Wire the discovery hook into .codex/hooks.json
256
+ // (PreToolUse, Bash matcher — Codex code discovery runs through the shell).
257
+ try {
258
+ registerSettingsHook(codexRailHooksPath(ctx), ctx.memtraceBinary, 'codex', 'PreToolUse', CODEX_MATCHER);
259
+ }
260
+ catch (e) {
261
+ warnings.push(`failed to register Codex Rail hook: ${e.message}`);
262
+ }
263
+ return {
264
+ agent: 'codex',
265
+ skillsWritten: skills.length,
266
+ skillsDir: rootDir,
267
+ mcpConfigPath,
268
+ mcpRegistered,
269
+ warnings,
270
+ };
271
+ },
272
+ async uninstall(ctx) {
273
+ const rootDir = skillsRoot(ctx);
274
+ if (fs.existsSync(rootDir)) {
275
+ for (const entry of fs.readdirSync(rootDir)) {
276
+ if (entry.startsWith('memtrace-')) {
277
+ fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
278
+ }
279
+ }
280
+ }
281
+ try {
282
+ removeSettingsHook(codexRailHooksPath(ctx), 'PreToolUse');
283
+ }
284
+ catch { /* best effort */ }
285
+ const configFile = codexConfigPath(ctx);
286
+ if (fs.existsSync(configFile)) {
287
+ const next = stripCodexMcpServer(fs.readFileSync(configFile, 'utf-8'));
288
+ if (next.trim())
289
+ writeTextAtomic(configFile, `${next}\n`);
290
+ else
291
+ fs.unlinkSync(configFile);
292
+ }
293
+ },
294
+ };
@@ -0,0 +1,7 @@
1
+ import { Transformer } from './types.js';
2
+ export interface CursorMcpResult {
3
+ registered: boolean;
4
+ backupPath?: string;
5
+ }
6
+ export declare function registerCursorMcpAt(mcpFile: string, binary: string): CursorMcpResult;
7
+ export declare const cursorTransformer: Transformer;
@@ -0,0 +1,124 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
5
+ import { MEMTRACE_MCP_ENV } from './shared.js';
6
+ import { registerCursorRailHook, removeCursorRailHook } from './rail-hooks.js';
7
+ function skillsRoot(ctx) {
8
+ const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
9
+ return path.join(base, '.cursor', 'skills');
10
+ }
11
+ function mcpPath(ctx) {
12
+ const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
13
+ return path.join(base, '.cursor', 'mcp.json');
14
+ }
15
+ function railHooksPath(ctx) {
16
+ const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
17
+ return path.join(base, '.cursor', 'hooks.json');
18
+ }
19
+ function writeSkill(skill, rootDir) {
20
+ const name = skill.filename.replace(/\.md$/, '');
21
+ const outDir = path.join(rootDir, name);
22
+ fs.mkdirSync(outDir, { recursive: true });
23
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
24
+ // Cursor requires frontmatter `name` matching the folder name.
25
+ const content = `---\nname: ${name}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
26
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
27
+ }
28
+ function isRecord(value) {
29
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ }
31
+ export function registerCursorMcpAt(mcpFile, binary) {
32
+ const { value, corrupted, backupPath } = safeReadJson(mcpFile);
33
+ if (corrupted) {
34
+ console.warn(`memtrace: ${mcpFile} is malformed; backed up to ${backupPath}. Skipped Cursor MCP registration.`);
35
+ return { registered: false, backupPath };
36
+ }
37
+ const cfg = (value ?? {});
38
+ cfg.mcpServers = cfg.mcpServers ?? {};
39
+ // Deep-merge env: preserve user-set keys (e.g. MEMTRACE_LICENSE_KEY) across
40
+ // re-registration; memtrace-owned defaults (MEMTRACE_MCP_ENV) win on conflict.
41
+ const existing = cfg.mcpServers['memtrace'];
42
+ const existingEnv = (isRecord(existing) && isRecord(existing.env))
43
+ ? existing.env
44
+ : {};
45
+ const mergedEnv = {
46
+ ...existingEnv,
47
+ ...MEMTRACE_MCP_ENV,
48
+ MEMTRACE_WORKSPACE_ROOT: '${workspaceFolder}',
49
+ };
50
+ cfg.mcpServers['memtrace'] = {
51
+ command: binary,
52
+ args: ['mcp'],
53
+ env: mergedEnv,
54
+ };
55
+ writeJsonAtomic(mcpFile, cfg);
56
+ return { registered: true };
57
+ }
58
+ export const cursorTransformer = {
59
+ name: 'cursor',
60
+ async install(skills, ctx) {
61
+ const rootDir = skillsRoot(ctx);
62
+ if (fs.existsSync(rootDir)) {
63
+ for (const entry of fs.readdirSync(rootDir)) {
64
+ if (entry.startsWith('memtrace-')) {
65
+ fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
66
+ }
67
+ }
68
+ }
69
+ for (const s of skills)
70
+ writeSkill(s, rootDir);
71
+ let mcpRegistered = false;
72
+ if (!ctx.skipMcp) {
73
+ const result = registerCursorMcpAt(mcpPath(ctx), ctx.memtraceBinary);
74
+ mcpRegistered = result.registered;
75
+ }
76
+ // ─── memtrace-rail ─── Wire the Memtrace-first discovery hook (default
77
+ // observe). Failure must not block the rest of the install.
78
+ try {
79
+ registerCursorRailHook(railHooksPath(ctx), ctx.memtraceBinary);
80
+ }
81
+ catch (e) {
82
+ console.warn(`memtrace: failed to register Cursor Rail hook: ${e.message}`);
83
+ }
84
+ return {
85
+ agent: 'cursor',
86
+ skillsWritten: skills.length,
87
+ skillsDir: rootDir,
88
+ mcpConfigPath: mcpPath(ctx),
89
+ mcpRegistered,
90
+ warnings: [],
91
+ };
92
+ },
93
+ async uninstall(ctx) {
94
+ // 1. Remove memtrace-* skill dirs
95
+ const rootDir = skillsRoot(ctx);
96
+ if (fs.existsSync(rootDir)) {
97
+ for (const entry of fs.readdirSync(rootDir)) {
98
+ if (entry.startsWith('memtrace-')) {
99
+ fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
100
+ }
101
+ }
102
+ }
103
+ // 1b. Remove the Rail hook.
104
+ try {
105
+ removeCursorRailHook(railHooksPath(ctx));
106
+ }
107
+ catch { /* best effort */ }
108
+ // 2. Remove memtrace entry from mcp.json; delete file if empty
109
+ const mcpFile = mcpPath(ctx);
110
+ const { value, corrupted } = safeReadJson(mcpFile);
111
+ if (!corrupted && value?.mcpServers?.['memtrace']) {
112
+ delete value.mcpServers['memtrace'];
113
+ if (Object.keys(value.mcpServers).length === 0) {
114
+ delete value.mcpServers;
115
+ }
116
+ if (Object.keys(value).length === 0) {
117
+ fs.unlinkSync(mcpFile);
118
+ }
119
+ else {
120
+ writeJsonAtomic(mcpFile, value);
121
+ }
122
+ }
123
+ },
124
+ };
@@ -0,0 +1,3 @@
1
+ import { Transformer, InstallContext } from './types.js';
2
+ export declare function geminiSettingsPath(ctx: InstallContext): string;
3
+ export declare const geminiTransformer: Transformer;
@@ -0,0 +1,78 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { registerMcpServersJsonAt, removeMcpServersJsonAt, removeMemtraceSkills, writeSkills, } from './shared.js';
4
+ import { registerSettingsHook, removeSettingsHook, GEMINI_MATCHER, } from './rail-hooks.js';
5
+ /**
6
+ * Gemini CLI transformer.
7
+ *
8
+ * Gemini CLI (v0.26+) supports:
9
+ * - MCP servers in `.gemini/settings.json` under `mcpServers` — the SAME
10
+ * shape as the generic JSON registration (`command`/`args`/`env`).
11
+ * - Lifecycle hooks under `hooks.BeforeTool` (matcher against the tool name,
12
+ * e.g. `run_shell_command`) — where Memtrace Rail wires the discovery hook.
13
+ *
14
+ * Both live in the one `settings.json`; we register MCP first, then the Rail
15
+ * hook (each is a read-modify-write that preserves the other's keys).
16
+ */
17
+ function geminiBase(ctx) {
18
+ return ctx.scope === 'global' ? path.join(os.homedir(), '.gemini') : path.join(ctx.cwd, '.gemini');
19
+ }
20
+ function skillsRoot(ctx) {
21
+ return path.join(geminiBase(ctx), 'skills');
22
+ }
23
+ export function geminiSettingsPath(ctx) {
24
+ return path.join(geminiBase(ctx), 'settings.json');
25
+ }
26
+ export const geminiTransformer = {
27
+ name: 'gemini',
28
+ async install(skills, ctx) {
29
+ const warnings = [];
30
+ const rootDir = skillsRoot(ctx);
31
+ // Skills as Markdown context files (Gemini reads MCP-server instructions
32
+ // for tool guidance; the skill files document the Memtrace-first workflow).
33
+ const skillsWritten = writeSkills(skills, rootDir);
34
+ const settingsPath = geminiSettingsPath(ctx);
35
+ let mcpRegistered = false;
36
+ if (!ctx.skipMcp) {
37
+ const result = registerMcpServersJsonAt(settingsPath, ctx.memtraceBinary);
38
+ mcpRegistered = result.registered;
39
+ if (!mcpRegistered && result.backupPath) {
40
+ warnings.push(`Gemini settings.json was malformed; backed up to ${result.backupPath}.`);
41
+ }
42
+ }
43
+ // ─── memtrace-rail ─── Wire the BeforeTool discovery hook (default
44
+ // observe). Matches Gemini's shell/read/glob/grep tool names. Gemini gates
45
+ // its hook system behind `enableHooks` + `enableMessageBusIntegration`
46
+ // (PR #14460) — without them a registered BeforeTool hook NEVER fires, so we
47
+ // enable both here (only if the user hasn't already set them).
48
+ try {
49
+ registerSettingsHook(settingsPath, ctx.memtraceBinary, 'gemini', 'BeforeTool', GEMINI_MATCHER, {
50
+ enableHooks: true,
51
+ enableMessageBusIntegration: true,
52
+ });
53
+ }
54
+ catch (e) {
55
+ warnings.push(`failed to register Gemini Rail hook: ${e.message}`);
56
+ }
57
+ return {
58
+ agent: 'gemini',
59
+ skillsWritten,
60
+ skillsDir: rootDir,
61
+ mcpConfigPath: settingsPath,
62
+ mcpRegistered,
63
+ warnings,
64
+ };
65
+ },
66
+ async uninstall(ctx) {
67
+ removeMemtraceSkills(skillsRoot(ctx));
68
+ const settingsPath = geminiSettingsPath(ctx);
69
+ try {
70
+ removeSettingsHook(settingsPath, 'BeforeTool');
71
+ }
72
+ catch { /* best effort */ }
73
+ try {
74
+ removeMcpServersJsonAt(settingsPath);
75
+ }
76
+ catch { /* best effort */ }
77
+ },
78
+ };
@@ -0,0 +1,5 @@
1
+ import { Transformer } from './types.js';
2
+ export declare function hermesConfigPath(): string;
3
+ export declare function registerHermesMcpAt(configFile: string, binary: string): void;
4
+ export declare function removeHermesMcpAt(configFile: string): void;
5
+ export declare const hermesTransformer: Transformer;
@@ -0,0 +1,136 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { MEMTRACE_MCP_ENV, MCP_SERVER_NAME, removeMemtraceSkills, writeSkills, writeTextAtomic, } from './shared.js';
5
+ function skillsRoot(ctx) {
6
+ void ctx;
7
+ return path.join(os.homedir(), '.hermes', 'skills');
8
+ }
9
+ export function hermesConfigPath() {
10
+ return path.join(os.homedir(), '.hermes', 'config.yaml');
11
+ }
12
+ function yamlQuote(value) {
13
+ return `"${value
14
+ .replace(/\\/g, '\\\\')
15
+ .replace(/"/g, '\\"')
16
+ .replace(/\n/g, '\\n')
17
+ .replace(/\r/g, '\\r')}"`;
18
+ }
19
+ function memtraceYamlBlock(binary) {
20
+ return [
21
+ ` ${MCP_SERVER_NAME}:`,
22
+ ` command: ${yamlQuote(binary)}`,
23
+ ' args:',
24
+ ' - "mcp"',
25
+ ' env:',
26
+ ...Object.entries(MEMTRACE_MCP_ENV).map(([key, value]) => ` ${key}: ${yamlQuote(value)}`),
27
+ ' enabled: true',
28
+ ];
29
+ }
30
+ function removeMemtraceYamlBlock(raw) {
31
+ const lines = raw.split(/\r?\n/);
32
+ const out = [];
33
+ let inMcpServers = false;
34
+ let skippingMemtrace = false;
35
+ for (const line of lines) {
36
+ if (/^\S/.test(line) && !line.startsWith('mcp_servers:')) {
37
+ inMcpServers = false;
38
+ skippingMemtrace = false;
39
+ }
40
+ if (/^mcp_servers:\s*$/.test(line)) {
41
+ inMcpServers = true;
42
+ skippingMemtrace = false;
43
+ out.push(line);
44
+ continue;
45
+ }
46
+ if (inMcpServers && new RegExp(`^\\s{2}${MCP_SERVER_NAME}:\\s*$`).test(line)) {
47
+ skippingMemtrace = true;
48
+ continue;
49
+ }
50
+ if (skippingMemtrace) {
51
+ if (/^\s{2}\S/.test(line) || /^\S/.test(line)) {
52
+ skippingMemtrace = false;
53
+ }
54
+ else {
55
+ continue;
56
+ }
57
+ }
58
+ if (!skippingMemtrace)
59
+ out.push(line);
60
+ }
61
+ return out.join('\n').trimEnd();
62
+ }
63
+ function removeEmptyMcpServersSection(raw) {
64
+ const lines = raw.split(/\r?\n/);
65
+ const out = [];
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i];
68
+ if (!/^mcp_servers:\s*$/.test(line)) {
69
+ out.push(line);
70
+ continue;
71
+ }
72
+ let j = i + 1;
73
+ while (j < lines.length && /^\s*$/.test(lines[j]))
74
+ j++;
75
+ if (j >= lines.length || /^\S/.test(lines[j])) {
76
+ continue;
77
+ }
78
+ out.push(line);
79
+ }
80
+ return out.join('\n').trimEnd();
81
+ }
82
+ export function registerHermesMcpAt(configFile, binary) {
83
+ const existing = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf-8') : '';
84
+ const withoutMemtrace = removeMemtraceYamlBlock(existing);
85
+ const hasMcpServers = /^mcp_servers:\s*$/m.test(withoutMemtrace);
86
+ const block = memtraceYamlBlock(binary);
87
+ let next;
88
+ if (hasMcpServers) {
89
+ const lines = withoutMemtrace.split(/\r?\n/);
90
+ const index = lines.findIndex(line => /^mcp_servers:\s*$/.test(line));
91
+ lines.splice(index + 1, 0, ...block);
92
+ next = lines.join('\n');
93
+ }
94
+ else {
95
+ next = `${withoutMemtrace}${withoutMemtrace ? '\n\n' : ''}mcp_servers:\n${block.join('\n')}`;
96
+ }
97
+ writeTextAtomic(configFile, `${next.trimEnd()}\n`);
98
+ }
99
+ export function removeHermesMcpAt(configFile) {
100
+ if (!fs.existsSync(configFile))
101
+ return;
102
+ const next = removeEmptyMcpServersSection(removeMemtraceYamlBlock(fs.readFileSync(configFile, 'utf-8')));
103
+ if (next.trim())
104
+ writeTextAtomic(configFile, `${next.trimEnd()}\n`);
105
+ else
106
+ fs.unlinkSync(configFile);
107
+ }
108
+ export const hermesTransformer = {
109
+ name: 'hermes',
110
+ async install(skills, ctx) {
111
+ const rootDir = skillsRoot(ctx);
112
+ const count = writeSkills(skills, rootDir);
113
+ const warnings = [];
114
+ let mcpRegistered = false;
115
+ const mcpConfigPath = hermesConfigPath();
116
+ if (!ctx.skipMcp) {
117
+ registerHermesMcpAt(mcpConfigPath, ctx.memtraceBinary);
118
+ mcpRegistered = true;
119
+ if (ctx.scope === 'local') {
120
+ warnings.push('Hermes skills and MCP config are user-level; wrote ~/.hermes/skills and ~/.hermes/config.yaml.');
121
+ }
122
+ }
123
+ return {
124
+ agent: 'hermes',
125
+ skillsWritten: count,
126
+ skillsDir: rootDir,
127
+ mcpConfigPath,
128
+ mcpRegistered,
129
+ warnings,
130
+ };
131
+ },
132
+ async uninstall(ctx) {
133
+ removeMemtraceSkills(skillsRoot(ctx));
134
+ removeHermesMcpAt(hermesConfigPath());
135
+ },
136
+ };
@@ -0,0 +1,14 @@
1
+ import { Transformer } from './types.js';
2
+ import { claudeTransformer } from './claude.js';
3
+ import { cursorTransformer } from './cursor.js';
4
+ import { codexTransformer } from './codex.js';
5
+ import { geminiTransformer } from './gemini.js';
6
+ import { windsurfTransformer } from './windsurf.js';
7
+ import { vscodeTransformer } from './vscode.js';
8
+ import { hermesTransformer } from './hermes.js';
9
+ import { opencodeTransformer } from './opencode.js';
10
+ import { kiroTransformer } from './kiro.js';
11
+ export declare const ALL_TRANSFORMERS: Transformer[];
12
+ export declare function findTransformer(name: string): Transformer | undefined;
13
+ export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
14
+ export type { Transformer, InstallContext, InstallResult, TransformResult } from './types.js';