memtrace 0.1.38 → 0.1.47

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 (54) hide show
  1. package/README.md +2 -2
  2. package/bin/memtrace.js +102 -14
  3. package/install.js +21 -181
  4. package/installer/dist/commands/doctor.d.ts +16 -0
  5. package/installer/dist/commands/doctor.js +86 -0
  6. package/installer/dist/commands/install.d.ts +9 -0
  7. package/installer/dist/commands/install.js +104 -0
  8. package/installer/dist/commands/picker.d.ts +6 -0
  9. package/installer/dist/commands/picker.js +22 -0
  10. package/installer/dist/fs-safe.d.ts +21 -0
  11. package/installer/dist/fs-safe.js +35 -0
  12. package/installer/dist/index.d.ts +2 -0
  13. package/installer/dist/index.js +52 -0
  14. package/installer/dist/skills.d.ts +17 -0
  15. package/installer/dist/skills.js +64 -0
  16. package/installer/dist/transformers/claude.d.ts +41 -0
  17. package/installer/dist/transformers/claude.js +400 -0
  18. package/installer/dist/transformers/cursor.d.ts +7 -0
  19. package/installer/dist/transformers/cursor.js +84 -0
  20. package/installer/dist/transformers/index.d.ts +7 -0
  21. package/installer/dist/transformers/index.js +7 -0
  22. package/installer/dist/transformers/types.d.ts +39 -0
  23. package/installer/dist/transformers/types.js +1 -0
  24. package/installer/dist/utils.d.ts +5 -0
  25. package/installer/dist/utils.js +22 -0
  26. package/installer/package.json +49 -0
  27. package/installer/skills/commands/memtrace-api-topology.md +65 -0
  28. package/installer/skills/commands/memtrace-cochange.md +76 -0
  29. package/installer/skills/commands/memtrace-evolution.md +135 -0
  30. package/installer/skills/commands/memtrace-graph.md +117 -0
  31. package/installer/skills/commands/memtrace-impact.md +64 -0
  32. package/installer/skills/commands/memtrace-index.md +66 -0
  33. package/installer/skills/commands/memtrace-quality.md +69 -0
  34. package/installer/skills/commands/memtrace-relationships.md +73 -0
  35. package/installer/skills/commands/memtrace-search.md +67 -0
  36. package/installer/skills/workflows/memtrace-change-impact-analysis.md +85 -0
  37. package/installer/skills/workflows/memtrace-codebase-exploration.md +108 -0
  38. package/installer/skills/workflows/memtrace-episode-replay.md +100 -0
  39. package/installer/skills/workflows/memtrace-first.md +120 -0
  40. package/installer/skills/workflows/memtrace-incident-investigation.md +125 -0
  41. package/installer/skills/workflows/memtrace-refactoring-guide.md +116 -0
  42. package/installer/skills/workflows/memtrace-session-continuity.md +98 -0
  43. package/package.json +10 -5
  44. package/skills/commands/memtrace-api-topology.md +3 -0
  45. package/skills/commands/memtrace-cochange.md +3 -0
  46. package/skills/commands/memtrace-evolution.md +3 -0
  47. package/skills/commands/memtrace-graph.md +54 -4
  48. package/skills/commands/memtrace-impact.md +3 -0
  49. package/skills/commands/memtrace-index.md +3 -0
  50. package/skills/commands/memtrace-quality.md +3 -0
  51. package/skills/commands/memtrace-relationships.md +3 -0
  52. package/skills/commands/memtrace-search.md +18 -13
  53. package/skills/workflows/memtrace-first.md +12 -0
  54. package/uninstall.js +22 -28
@@ -0,0 +1,21 @@
1
+ export interface SafeReadResult<T> {
2
+ /** Parsed value, or null if file missing or corrupt. */
3
+ value: T | null;
4
+ /** True if the file existed but failed to parse. */
5
+ corrupted: boolean;
6
+ /** Path to the backup copy, if we made one. */
7
+ backupPath?: string;
8
+ }
9
+ /**
10
+ * Read and JSON-parse a file without ever silently losing data.
11
+ * On parse failure, copies the file to <path>.corrupt-<ISO-timestamp>
12
+ * and returns { value: null, corrupted: true, backupPath }.
13
+ * Caller is responsible for deciding whether to proceed with writes.
14
+ */
15
+ export declare function safeReadJson<T = unknown>(filePath: string): SafeReadResult<T>;
16
+ /**
17
+ * Write JSON atomically: write to a sibling .tmp file, then rename.
18
+ * Rename is atomic on POSIX and good enough on Windows for config files.
19
+ * Creates parent directories as needed.
20
+ */
21
+ export declare function writeJsonAtomic(filePath: string, data: unknown): void;
@@ -0,0 +1,35 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Read and JSON-parse a file without ever silently losing data.
5
+ * On parse failure, copies the file to <path>.corrupt-<ISO-timestamp>
6
+ * and returns { value: null, corrupted: true, backupPath }.
7
+ * Caller is responsible for deciding whether to proceed with writes.
8
+ */
9
+ export function safeReadJson(filePath) {
10
+ if (!fs.existsSync(filePath)) {
11
+ return { value: null, corrupted: false };
12
+ }
13
+ const raw = fs.readFileSync(filePath, 'utf-8');
14
+ try {
15
+ return { value: JSON.parse(raw), corrupted: false };
16
+ }
17
+ catch {
18
+ const iso = new Date().toISOString().replace(/[:.]/g, '-');
19
+ const backupPath = `${filePath}.corrupt-${iso}`;
20
+ fs.copyFileSync(filePath, backupPath);
21
+ return { value: null, corrupted: true, backupPath };
22
+ }
23
+ }
24
+ /**
25
+ * Write JSON atomically: write to a sibling .tmp file, then rename.
26
+ * Rename is atomic on POSIX and good enough on Windows for config files.
27
+ * Creates parent directories as needed.
28
+ */
29
+ export function writeJsonAtomic(filePath, data) {
30
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
31
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
32
+ const payload = JSON.stringify(data, null, 2) + '\n';
33
+ fs.writeFileSync(tmpPath, payload);
34
+ fs.renameSync(tmpPath, filePath);
35
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { runInstall, runUninstall } from './commands/install.js';
4
+ import { runDoctor } from './commands/doctor.js';
5
+ const program = new Command();
6
+ program
7
+ .name('memtrace-skills')
8
+ .description('Install Memtrace skills and MCP for AI coding agents')
9
+ .version('0.1.0');
10
+ function parseOnly(val) {
11
+ return val.split(',').map(s => s.trim()).filter(Boolean);
12
+ }
13
+ program
14
+ .command('install')
15
+ .description('Install memtrace skills and register MCP for selected agents')
16
+ .option('--only <agents>', 'comma-separated agent names (claude,cursor)', parseOnly)
17
+ .option('--local', 'install into the current project (./.claude/, ./.cursor/)', false)
18
+ .option('--global', 'install globally (~/.claude/, ~/.cursor/) [default]', false)
19
+ .option('--skip-mcp', 'write skills only, skip MCP server registration', false)
20
+ .option('--repair', 'alias for install — run after fixing a corrupt settings file')
21
+ .option('-y, --yes', 'non-interactive, accept defaults')
22
+ .action(async (opts) => {
23
+ const options = {
24
+ scope: opts.local ? 'local' : 'global',
25
+ only: opts.only,
26
+ skipMcp: opts.skipMcp,
27
+ yes: opts.yes,
28
+ };
29
+ await runInstall(options);
30
+ });
31
+ program
32
+ .command('doctor')
33
+ .description('Check memtrace install health across all agents')
34
+ .action(async () => {
35
+ process.exit(await runDoctor());
36
+ });
37
+ program
38
+ .command('uninstall')
39
+ .description('Remove memtrace skills and MCP registrations')
40
+ .option('--only <agents>', 'comma-separated agent names', parseOnly)
41
+ .option('--local', 'uninstall from the current project', false)
42
+ .action(async (opts) => {
43
+ await runUninstall({
44
+ scope: opts.local ? 'local' : 'global',
45
+ only: opts.only,
46
+ });
47
+ });
48
+ // Default: `memtrace-skills` with no subcommand → install with defaults (global, all agents)
49
+ program.action(async () => {
50
+ await runInstall({ scope: 'global' });
51
+ });
52
+ program.parse();
@@ -0,0 +1,17 @@
1
+ export interface SkillFrontmatter {
2
+ name?: string;
3
+ description: string;
4
+ 'allowed-tools'?: string[];
5
+ 'user-invocable'?: boolean;
6
+ }
7
+ export interface Skill {
8
+ filename: string;
9
+ category: 'commands' | 'workflows';
10
+ frontmatter: SkillFrontmatter;
11
+ body: string;
12
+ raw: string;
13
+ }
14
+ /**
15
+ * Load all skills from a skills directory (expects commands/ and workflows/ subdirs).
16
+ */
17
+ export declare function loadSkills(skillsDir: string): Skill[];
@@ -0,0 +1,64 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Parse YAML-ish frontmatter from a skill markdown file.
5
+ * Handles the subset of YAML used in skill files (no nested objects).
6
+ */
7
+ function parseFrontmatter(raw) {
8
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
9
+ if (!match)
10
+ return null;
11
+ const frontmatterRaw = match[1];
12
+ const body = match[2];
13
+ const frontmatter = {};
14
+ // Parse description
15
+ const descMatch = frontmatterRaw.match(/^description:\s*"?(.*?)"?\s*$/m);
16
+ if (descMatch)
17
+ frontmatter.description = descMatch[1];
18
+ // Parse name
19
+ const nameMatch = frontmatterRaw.match(/^name:\s*(.+)$/m);
20
+ if (nameMatch)
21
+ frontmatter.name = nameMatch[1].trim();
22
+ // Parse user-invocable
23
+ const invocableMatch = frontmatterRaw.match(/^user-invocable:\s*(.+)$/m);
24
+ if (invocableMatch)
25
+ frontmatter['user-invocable'] = invocableMatch[1].trim() === 'true';
26
+ // Parse allowed-tools (YAML list)
27
+ const toolsMatch = frontmatterRaw.match(/^allowed-tools:\n((?:\s+-\s+.+\n?)+)/m);
28
+ if (toolsMatch) {
29
+ frontmatter['allowed-tools'] = toolsMatch[1]
30
+ .split('\n')
31
+ .map(line => line.replace(/^\s+-\s+/, '').trim())
32
+ .filter(Boolean);
33
+ }
34
+ return { frontmatter, body };
35
+ }
36
+ /**
37
+ * Load all skills from a skills directory (expects commands/ and workflows/ subdirs).
38
+ */
39
+ export function loadSkills(skillsDir) {
40
+ const skills = [];
41
+ for (const category of ['commands', 'workflows']) {
42
+ const categoryDir = path.join(skillsDir, category);
43
+ if (!fs.existsSync(categoryDir))
44
+ continue;
45
+ for (const file of fs.readdirSync(categoryDir)) {
46
+ if (!file.endsWith('.md'))
47
+ continue;
48
+ const raw = fs.readFileSync(path.join(categoryDir, file), 'utf-8');
49
+ const parsed = parseFrontmatter(raw);
50
+ if (!parsed || !parsed.frontmatter.description) {
51
+ console.warn(`Warning: could not parse frontmatter for ${file}, skipping`);
52
+ continue;
53
+ }
54
+ skills.push({
55
+ filename: file,
56
+ category,
57
+ frontmatter: parsed.frontmatter,
58
+ body: parsed.body,
59
+ raw,
60
+ });
61
+ }
62
+ }
63
+ return skills;
64
+ }
@@ -0,0 +1,41 @@
1
+ import { Skill } from '../skills.js';
2
+ import { Transformer, TransformResult } from './types.js';
3
+ /**
4
+ * Transform a skill into Claude Code plugin format.
5
+ * Each skill becomes a directory with SKILL.md inside skills/<skill-name>/
6
+ */
7
+ export declare function transformForClaude(skill: Skill): TransformResult[];
8
+ /**
9
+ * Fallback cache directory when the CLI install didn't create one.
10
+ */
11
+ export declare function getClaudePluginCacheDir(): string;
12
+ export interface SettingsMutationResult {
13
+ registered: boolean;
14
+ backupPath?: string;
15
+ }
16
+ /**
17
+ * Merge-add the memtrace MCP server into a Claude settings.json file.
18
+ * Never overwrites a malformed file — backs it up and returns registered=false.
19
+ */
20
+ export declare function registerMcpInSettingsAt(settingsPath: string, memtraceBinary: string): SettingsMutationResult;
21
+ /**
22
+ * Merge-add plugin + marketplace entries into a Claude settings.json file.
23
+ * Never overwrites a malformed file.
24
+ */
25
+ export declare function enablePluginInSettingsAt(settingsPath: string): SettingsMutationResult;
26
+ /**
27
+ * Full Claude Code plugin installation.
28
+ *
29
+ * 1. Try `claude plugin marketplace add` + `claude plugin install`
30
+ * 2. Fall back to manual: write cache files + update settings.json
31
+ * 3. Register MCP server for memtrace tools
32
+ */
33
+ export declare function installClaudePlugin(skills: Skill[], memtraceBinaryPath: string): Promise<{
34
+ cacheDir: string;
35
+ skillCount: number;
36
+ }>;
37
+ /**
38
+ * Remove the Claude Code plugin and MCP server registration.
39
+ */
40
+ export declare function uninstallClaudePlugin(): Promise<void>;
41
+ export declare const claudeTransformer: Transformer;
@@ -0,0 +1,400 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { execCommand, commandExists } from '../utils.js';
5
+ import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
6
+ const PLUGIN_NAME = 'memtrace-skills';
7
+ const MARKETPLACE_NAME = 'memtrace';
8
+ const MARKETPLACE_REPO = 'syncable-dev/memtrace';
9
+ /**
10
+ * Try to register the memtrace MCP server via `claude mcp add-json`.
11
+ * Returns true on success, false on timeout/error/missing CLI.
12
+ * 5-second timeout — we fall back to direct JSON merge fast.
13
+ */
14
+ async function tryMcpAddJson(memtraceBinary) {
15
+ if (!(await commandExists('claude')))
16
+ return false;
17
+ const config = JSON.stringify({
18
+ command: memtraceBinary,
19
+ args: ['mcp'],
20
+ env: { MEMGRAPH_URL: 'bolt://localhost:7687' },
21
+ });
22
+ // Shell-quote the JSON. Use single quotes; escape any embedded single quote.
23
+ const escaped = config.replace(/'/g, `'\\''`);
24
+ try {
25
+ await execCommand(`claude mcp add-json --scope user memtrace '${escaped}'`, { timeoutMs: 5_000 });
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Transform a skill into Claude Code plugin format.
34
+ * Each skill becomes a directory with SKILL.md inside skills/<skill-name>/
35
+ */
36
+ export function transformForClaude(skill) {
37
+ const skillName = skill.filename.replace(/\.md$/, '');
38
+ const safeDesc = skill.frontmatter.description
39
+ .replace(/"/g, '\\"')
40
+ .trim();
41
+ const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
42
+ return [{ relativePath: `skills/${skillName}/SKILL.md`, content }];
43
+ }
44
+ /**
45
+ * Root directory for all cached versions of this plugin.
46
+ */
47
+ function getPluginCacheRoot() {
48
+ return path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, PLUGIN_NAME);
49
+ }
50
+ /**
51
+ * Find the cache directory that Claude Code's CLI created (from `claude plugin install`).
52
+ */
53
+ function findCliInstalledCacheDir() {
54
+ const root = getPluginCacheRoot();
55
+ if (!fs.existsSync(root))
56
+ return null;
57
+ for (const entry of fs.readdirSync(root)) {
58
+ const dir = path.join(root, entry);
59
+ if (!fs.statSync(dir).isDirectory())
60
+ continue;
61
+ return dir;
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Fallback cache directory when the CLI install didn't create one.
67
+ */
68
+ export function getClaudePluginCacheDir() {
69
+ return path.join(getPluginCacheRoot(), '0.0.0');
70
+ }
71
+ // ────────────────────────────────────────────────────────────────────────────
72
+ // Installation strategy (in priority order):
73
+ //
74
+ // 1. `claude plugin marketplace add` + `claude plugin install`
75
+ // The official flow. Registers the marketplace, clones the plugin,
76
+ // caches it, AND auto-enables it in settings.
77
+ //
78
+ // 2. Manual write: cache files + enabledPlugins in settings.json
79
+ // If the CLI is unavailable, write plugin files directly to the
80
+ // cache directory AND register in ~/.claude/settings.json.
81
+ // ────────────────────────────────────────────────────────────────────────────
82
+ /**
83
+ * Try to install the plugin via the Claude Code CLI.
84
+ */
85
+ async function tryClaudeCliInstall() {
86
+ const hasClaude = await commandExists('claude');
87
+ if (!hasClaude)
88
+ return false;
89
+ try {
90
+ await execCommand(`claude plugin marketplace add ${MARKETPLACE_REPO}`);
91
+ }
92
+ catch {
93
+ // Marketplace may already exist
94
+ }
95
+ try {
96
+ await execCommand(`claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`);
97
+ return true;
98
+ }
99
+ catch {
100
+ try {
101
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
102
+ if (fs.existsSync(settingsPath)) {
103
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
104
+ const key = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
105
+ if (settings.enabledPlugins?.[key] === true) {
106
+ return true;
107
+ }
108
+ }
109
+ }
110
+ catch {
111
+ // Fall through to manual
112
+ }
113
+ return false;
114
+ }
115
+ }
116
+ /**
117
+ * Write the plugin.json manifest inside the cache directory.
118
+ */
119
+ function writePluginManifest(cacheDir) {
120
+ const manifestDir = path.join(cacheDir, '.claude-plugin');
121
+ fs.mkdirSync(manifestDir, { recursive: true });
122
+ const version = path.basename(cacheDir);
123
+ const manifest = {
124
+ name: PLUGIN_NAME,
125
+ description: 'Memtrace skills for codebase exploration, code search, relationship analysis, temporal evolution, blast radius impact, code quality, graph algorithms, API topology, and multi-step workflows.',
126
+ version,
127
+ author: {
128
+ name: 'Syncable',
129
+ email: 'support@syncable.dev',
130
+ },
131
+ homepage: 'https://memtrace.io',
132
+ repository: `https://github.com/${MARKETPLACE_REPO}`,
133
+ license: 'SEE LICENSE IN LICENSE',
134
+ keywords: ['memtrace', 'code-intelligence', 'knowledge-graph', 'mcp', 'temporal-analysis'],
135
+ };
136
+ fs.writeFileSync(path.join(manifestDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
137
+ }
138
+ /**
139
+ * Merge-add the memtrace MCP server into a Claude settings.json file.
140
+ * Never overwrites a malformed file — backs it up and returns registered=false.
141
+ */
142
+ export function registerMcpInSettingsAt(settingsPath, memtraceBinary) {
143
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
144
+ if (corrupted) {
145
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped MCP registration for Claude.`);
146
+ console.warn(`memtrace: fix the file and run 'memtrace install --repair'.`);
147
+ return { registered: false, backupPath };
148
+ }
149
+ const settings = (value ?? {});
150
+ settings.mcpServers = settings.mcpServers ?? {};
151
+ settings.mcpServers['memtrace'] = {
152
+ command: memtraceBinary,
153
+ args: ['mcp'],
154
+ env: { MEMGRAPH_URL: 'bolt://localhost:7687' },
155
+ };
156
+ writeJsonAtomic(settingsPath, settings);
157
+ return { registered: true };
158
+ }
159
+ /**
160
+ * Merge-add plugin + marketplace entries into a Claude settings.json file.
161
+ * Never overwrites a malformed file.
162
+ */
163
+ export function enablePluginInSettingsAt(settingsPath) {
164
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
165
+ if (corrupted) {
166
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}.`);
167
+ return { registered: false, backupPath };
168
+ }
169
+ const settings = (value ?? {});
170
+ settings.enabledPlugins = settings.enabledPlugins ?? {};
171
+ settings.enabledPlugins[`${PLUGIN_NAME}@${MARKETPLACE_NAME}`] = true;
172
+ settings.extraKnownMarketplaces = settings.extraKnownMarketplaces ?? {};
173
+ settings.extraKnownMarketplaces[MARKETPLACE_NAME] = {
174
+ source: { source: 'github', repo: MARKETPLACE_REPO },
175
+ };
176
+ writeJsonAtomic(settingsPath, settings);
177
+ return { registered: true };
178
+ }
179
+ /**
180
+ * Enable the plugin in ~/.claude/settings.json.
181
+ */
182
+ function enablePluginInSettings() {
183
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
184
+ enablePluginInSettingsAt(settingsFile);
185
+ }
186
+ /**
187
+ * Register the memtrace MCP server in Claude Code's settings.
188
+ * This adds the MCP server config so Claude Code can connect to memtrace tools.
189
+ */
190
+ async function registerMcpServer(memtraceBinaryPath) {
191
+ // Strategy 1 (preferred): claude CLI's own add-json path
192
+ const viaCli = await tryMcpAddJson(memtraceBinaryPath);
193
+ if (viaCli)
194
+ return;
195
+ // Strategy 2 (fallback): direct settings.json merge (safe + atomic)
196
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
197
+ registerMcpInSettingsAt(settingsFile, memtraceBinaryPath);
198
+ }
199
+ /**
200
+ * Full Claude Code plugin installation.
201
+ *
202
+ * 1. Try `claude plugin marketplace add` + `claude plugin install`
203
+ * 2. Fall back to manual: write cache files + update settings.json
204
+ * 3. Register MCP server for memtrace tools
205
+ */
206
+ export async function installClaudePlugin(skills, memtraceBinaryPath) {
207
+ // Step 1: Try CLI install
208
+ await tryClaudeCliInstall();
209
+ // Step 2: Find or create cache dir
210
+ let cacheDir = findCliInstalledCacheDir();
211
+ if (cacheDir) {
212
+ const orphanedFile = path.join(cacheDir, '.orphaned_at');
213
+ if (fs.existsSync(orphanedFile))
214
+ fs.unlinkSync(orphanedFile);
215
+ }
216
+ else {
217
+ cacheDir = getClaudePluginCacheDir();
218
+ }
219
+ // Step 3: Clean up old versions
220
+ const pluginRoot = getPluginCacheRoot();
221
+ if (fs.existsSync(pluginRoot)) {
222
+ const activeDirName = path.basename(cacheDir);
223
+ for (const entry of fs.readdirSync(pluginRoot)) {
224
+ if (entry !== activeDirName && entry !== '.DS_Store') {
225
+ fs.rmSync(path.join(pluginRoot, entry), { recursive: true, force: true });
226
+ }
227
+ }
228
+ }
229
+ // Step 4: Write skills
230
+ const skillsDir = path.join(cacheDir, 'skills');
231
+ if (fs.existsSync(skillsDir)) {
232
+ fs.rmSync(skillsDir, { recursive: true });
233
+ }
234
+ for (const skill of skills) {
235
+ const results = transformForClaude(skill);
236
+ for (const { relativePath, content } of results) {
237
+ const fullPath = path.join(cacheDir, relativePath);
238
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
239
+ fs.writeFileSync(fullPath, content);
240
+ }
241
+ }
242
+ writePluginManifest(cacheDir);
243
+ enablePluginInSettings();
244
+ // Step 5: Register MCP server
245
+ await registerMcpServer(memtraceBinaryPath);
246
+ // Step 6: Write user-level skills for SDK-based integrations
247
+ writeUserLevelSkills(skills);
248
+ return { cacheDir, skillCount: skills.length };
249
+ }
250
+ /**
251
+ * Write skills to ~/.claude/skills/ for SDK-based integrations.
252
+ */
253
+ function writeUserLevelSkills(skills) {
254
+ const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
255
+ for (const skill of skills) {
256
+ const skillName = skill.filename.replace(/\.md$/, '');
257
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
258
+ const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
259
+ const outDir = path.join(userSkillsDir, skillName);
260
+ fs.mkdirSync(outDir, { recursive: true });
261
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
262
+ }
263
+ }
264
+ /**
265
+ * Remove the Claude Code plugin and MCP server registration.
266
+ */
267
+ export async function uninstallClaudePlugin() {
268
+ const hasClaude = await commandExists('claude');
269
+ if (hasClaude) {
270
+ try {
271
+ await execCommand(`claude plugin uninstall ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`);
272
+ }
273
+ catch { /* fall through */ }
274
+ }
275
+ // Manual cleanup
276
+ const cacheRoot = getPluginCacheRoot();
277
+ if (fs.existsSync(cacheRoot)) {
278
+ fs.rmSync(cacheRoot, { recursive: true });
279
+ }
280
+ // Remove from settings.json
281
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
282
+ if (fs.existsSync(settingsFile)) {
283
+ try {
284
+ const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
285
+ const pluginKey = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
286
+ if (settings.enabledPlugins && typeof settings.enabledPlugins === 'object') {
287
+ delete settings.enabledPlugins[pluginKey];
288
+ }
289
+ if (settings.mcpServers && typeof settings.mcpServers === 'object') {
290
+ delete settings.mcpServers['memtrace'];
291
+ }
292
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
293
+ }
294
+ catch { /* */ }
295
+ }
296
+ // Clean up user-level skills
297
+ const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
298
+ if (fs.existsSync(userSkillsDir)) {
299
+ for (const entry of fs.readdirSync(userSkillsDir)) {
300
+ if (entry.startsWith('memtrace-')) {
301
+ const entryPath = path.join(userSkillsDir, entry);
302
+ const stat = fs.statSync(entryPath);
303
+ if (stat.isDirectory()) {
304
+ fs.rmSync(entryPath, { recursive: true });
305
+ }
306
+ else if (entry.endsWith('.md')) {
307
+ fs.unlinkSync(entryPath);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ /**
314
+ * Write/merge the memtrace MCP server entry into a project-local .mcp.json.
315
+ * Safe-reads existing config; never overwrites a malformed file.
316
+ */
317
+ function writeClaudeLocalMcp(mcpPath, binary) {
318
+ const { value, corrupted, backupPath } = safeReadJson(mcpPath);
319
+ if (corrupted) {
320
+ console.warn(`memtrace: ${mcpPath} is malformed; backed up to ${backupPath}. Skipped MCP registration.`);
321
+ return false;
322
+ }
323
+ const cfg = (value ?? {});
324
+ cfg.mcpServers = cfg.mcpServers ?? {};
325
+ cfg.mcpServers['memtrace'] = {
326
+ command: binary,
327
+ args: ['mcp'],
328
+ env: { MEMGRAPH_URL: 'bolt://localhost:7687' },
329
+ };
330
+ writeJsonAtomic(mcpPath, cfg);
331
+ return true;
332
+ }
333
+ export const claudeTransformer = {
334
+ name: 'claude',
335
+ async install(skills, ctx) {
336
+ if (ctx.scope === 'global') {
337
+ const { skillCount } = await installClaudePlugin(skills, ctx.memtraceBinary);
338
+ return {
339
+ agent: 'claude',
340
+ skillsWritten: skillCount,
341
+ skillsDir: path.join(os.homedir(), '.claude', 'skills'),
342
+ mcpConfigPath: path.join(os.homedir(), '.claude', 'settings.json'),
343
+ mcpRegistered: !ctx.skipMcp,
344
+ warnings: [],
345
+ };
346
+ }
347
+ // Local scope: write skills into <cwd>/.claude/skills/
348
+ const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
349
+ let count = 0;
350
+ for (const skill of skills) {
351
+ const skillName = skill.filename.replace(/\.md$/, '');
352
+ const outDir = path.join(skillsDir, skillName);
353
+ fs.mkdirSync(outDir, { recursive: true });
354
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
355
+ const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
356
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
357
+ count++;
358
+ }
359
+ // MCP for local scope: .mcp.json at project root (Claude's project-level convention)
360
+ const mcpConfigPath = path.join(ctx.cwd, '.mcp.json');
361
+ let mcpRegistered = false;
362
+ if (!ctx.skipMcp) {
363
+ mcpRegistered = writeClaudeLocalMcp(mcpConfigPath, ctx.memtraceBinary);
364
+ }
365
+ return {
366
+ agent: 'claude',
367
+ skillsWritten: count,
368
+ skillsDir,
369
+ mcpConfigPath,
370
+ mcpRegistered,
371
+ warnings: [],
372
+ };
373
+ },
374
+ async uninstall(ctx) {
375
+ if (ctx.scope === 'global') {
376
+ await uninstallClaudePlugin();
377
+ return;
378
+ }
379
+ // Local: remove <cwd>/.claude/skills/memtrace-* and the memtrace entry from <cwd>/.mcp.json
380
+ const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
381
+ if (fs.existsSync(skillsDir)) {
382
+ for (const entry of fs.readdirSync(skillsDir)) {
383
+ if (entry.startsWith('memtrace-')) {
384
+ fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
385
+ }
386
+ }
387
+ }
388
+ const mcpPath = path.join(ctx.cwd, '.mcp.json');
389
+ const { value, corrupted } = safeReadJson(mcpPath);
390
+ if (!corrupted && value?.mcpServers?.['memtrace']) {
391
+ delete value.mcpServers['memtrace'];
392
+ if (Object.keys(value.mcpServers).length === 0)
393
+ delete value.mcpServers;
394
+ if (Object.keys(value).length === 0)
395
+ fs.unlinkSync(mcpPath);
396
+ else
397
+ writeJsonAtomic(mcpPath, value);
398
+ }
399
+ },
400
+ };
@@ -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;