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,702 @@
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
+ import { MEMTRACE_MCP_ENV } from './shared.js';
7
+ const PLUGIN_NAME = 'memtrace-skills';
8
+ const MARKETPLACE_NAME = 'memtrace';
9
+ const MARKETPLACE_REPO = 'syncable-dev/memtrace-public';
10
+ const MARKETPLACE_GIT_URL = `https://github.com/${MARKETPLACE_REPO}.git`;
11
+ const PLUGIN_KEY = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
12
+ const MARKETPLACE_SETTING_KEYS = [
13
+ MARKETPLACE_NAME,
14
+ 'syncable-dev-memtrace',
15
+ 'syncable-dev-memtrace-public',
16
+ ];
17
+ const MARKETPLACE_SETTING_CONTAINERS = [
18
+ 'extraKnownMarketplaces',
19
+ 'marketplaces',
20
+ 'pluginMarketplaces',
21
+ 'knownMarketplaces',
22
+ ];
23
+ function isRecord(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+ /**
27
+ * Read the existing env block (if any) for the memtrace MCP entry in a Claude
28
+ * settings/mcp.json file. Returns an empty object if the file or entry is
29
+ * missing/malformed. Used to deep-merge user-set env keys
30
+ * (e.g. MEMTRACE_LICENSE_KEY) so they survive `memtrace install`
31
+ * re-registration. Mirrors the codex transformer's env-preservation contract.
32
+ */
33
+ function readExistingMemtraceEnv(filePath, serverKey = 'mcpServers') {
34
+ if (!fs.existsSync(filePath))
35
+ return {};
36
+ const { value, corrupted } = safeReadJson(filePath);
37
+ if (corrupted || !value)
38
+ return {};
39
+ const servers = value[serverKey];
40
+ if (!isRecord(servers))
41
+ return {};
42
+ const entry = servers['memtrace'];
43
+ if (!isRecord(entry))
44
+ return {};
45
+ const env = entry.env;
46
+ if (!isRecord(env))
47
+ return {};
48
+ const out = {};
49
+ for (const [k, v] of Object.entries(env)) {
50
+ if (typeof v === 'string')
51
+ out[k] = v;
52
+ }
53
+ return out;
54
+ }
55
+ /**
56
+ * Try to register the memtrace MCP server via `claude mcp add-json`.
57
+ * Returns true on success, false on timeout/error/missing CLI.
58
+ * 5-second timeout — we fall back to direct JSON merge fast.
59
+ *
60
+ * The env block deep-merges any user-set env keys discovered in the existing
61
+ * settings.json on top of MEMTRACE_MCP_ENV defaults so that a re-install never
62
+ * stomps user-added keys like MEMTRACE_LICENSE_KEY.
63
+ */
64
+ async function tryMcpAddJson(memtraceBinary) {
65
+ if (!(await commandExists('claude')))
66
+ return false;
67
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
68
+ const existingEnv = readExistingMemtraceEnv(settingsFile);
69
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
70
+ const config = JSON.stringify({
71
+ command: memtraceBinary,
72
+ args: ['mcp'],
73
+ env: mergedEnv,
74
+ });
75
+ // Shell-quote the JSON. Use single quotes; escape any embedded single quote.
76
+ const escaped = config.replace(/'/g, `'\\''`);
77
+ try {
78
+ await execCommand(`claude mcp add-json --scope user memtrace '${escaped}'`, { timeoutMs: 5_000 });
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Transform a skill into Claude Code plugin format.
87
+ * Each skill becomes a directory with SKILL.md inside skills/<skill-name>/
88
+ */
89
+ export function transformForClaude(skill) {
90
+ const skillName = skill.filename.replace(/\.md$/, '');
91
+ const safeDesc = skill.frontmatter.description
92
+ .replace(/"/g, '\\"')
93
+ .trim();
94
+ const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
95
+ return [{ relativePath: `skills/${skillName}/SKILL.md`, content }];
96
+ }
97
+ /**
98
+ * Root directory for all cached versions of this plugin.
99
+ */
100
+ function getPluginCacheRoot() {
101
+ return path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, PLUGIN_NAME);
102
+ }
103
+ /**
104
+ * Find the cache directory that Claude Code's CLI created (from `claude plugin install`).
105
+ */
106
+ function findCliInstalledCacheDir() {
107
+ const root = getPluginCacheRoot();
108
+ if (!fs.existsSync(root))
109
+ return null;
110
+ for (const entry of fs.readdirSync(root)) {
111
+ const dir = path.join(root, entry);
112
+ if (!fs.statSync(dir).isDirectory())
113
+ continue;
114
+ return dir;
115
+ }
116
+ return null;
117
+ }
118
+ /**
119
+ * Fallback cache directory when the CLI install didn't create one.
120
+ */
121
+ export function getClaudePluginCacheDir() {
122
+ return path.join(getPluginCacheRoot(), '0.0.0');
123
+ }
124
+ // ────────────────────────────────────────────────────────────────────────────
125
+ // Installation strategy (in priority order):
126
+ //
127
+ // 1. `claude plugin marketplace add` + `claude plugin install`
128
+ // The official flow. Registers the marketplace, clones the plugin,
129
+ // caches it, AND auto-enables it in settings.
130
+ //
131
+ // 2. Manual write: cache files + enabledPlugins in settings.json
132
+ // If the CLI is unavailable, write plugin files directly to the
133
+ // cache directory AND register in ~/.claude/settings.json.
134
+ // ────────────────────────────────────────────────────────────────────────────
135
+ /**
136
+ * Try to install the plugin via the Claude Code CLI.
137
+ */
138
+ async function tryClaudeCliInstall() {
139
+ const hasClaude = await commandExists('claude');
140
+ if (!hasClaude)
141
+ return false;
142
+ try {
143
+ await execCommand(`claude plugin marketplace add --scope user ${MARKETPLACE_GIT_URL}`);
144
+ }
145
+ catch {
146
+ // Marketplace may already exist
147
+ }
148
+ try {
149
+ await execCommand(`claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`);
150
+ return true;
151
+ }
152
+ catch {
153
+ try {
154
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
155
+ if (fs.existsSync(settingsPath)) {
156
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
157
+ if (settings.enabledPlugins?.[PLUGIN_KEY] === true) {
158
+ return true;
159
+ }
160
+ }
161
+ }
162
+ catch {
163
+ // Fall through to manual
164
+ }
165
+ return false;
166
+ }
167
+ }
168
+ /**
169
+ * Write the plugin.json manifest inside the cache directory.
170
+ */
171
+ function writePluginManifest(cacheDir) {
172
+ const manifestDir = path.join(cacheDir, '.claude-plugin');
173
+ fs.mkdirSync(manifestDir, { recursive: true });
174
+ const version = path.basename(cacheDir);
175
+ const manifest = {
176
+ name: PLUGIN_NAME,
177
+ 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.',
178
+ version,
179
+ author: {
180
+ name: 'Syncable',
181
+ email: 'support@memtrace.io',
182
+ },
183
+ homepage: 'https://memtrace.io',
184
+ repository: `https://github.com/${MARKETPLACE_REPO}`,
185
+ license: 'SEE LICENSE IN LICENSE',
186
+ keywords: ['memtrace', 'code-intelligence', 'knowledge-graph', 'mcp', 'temporal-analysis'],
187
+ };
188
+ fs.writeFileSync(path.join(manifestDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
189
+ }
190
+ /**
191
+ * Merge-add the memtrace MCP server into a Claude settings.json file.
192
+ * Never overwrites a malformed file — backs it up and returns registered=false.
193
+ */
194
+ export function registerMcpInSettingsAt(settingsPath, memtraceBinary) {
195
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
196
+ if (corrupted) {
197
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped MCP registration for Claude.`);
198
+ console.warn(`memtrace: fix the file and run 'memtrace install --repair'.`);
199
+ return { registered: false, backupPath };
200
+ }
201
+ const settings = (value ?? {});
202
+ settings.mcpServers = settings.mcpServers ?? {};
203
+ // Deep-merge env so user-set keys (e.g. MEMTRACE_LICENSE_KEY) survive
204
+ // re-registration. memtrace-owned keys (MEMTRACE_MCP_ENV) override on
205
+ // conflict so installer-managed values stay authoritative.
206
+ const existing = settings.mcpServers['memtrace'];
207
+ const existingEnv = (isRecord(existing) && isRecord(existing.env))
208
+ ? existing.env
209
+ : {};
210
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
211
+ settings.mcpServers['memtrace'] = {
212
+ command: memtraceBinary,
213
+ args: ['mcp'],
214
+ env: mergedEnv,
215
+ };
216
+ writeJsonAtomic(settingsPath, settings);
217
+ return { registered: true };
218
+ }
219
+ /**
220
+ * The tool surfaces Rail intercepts. Native `Grep`/`Glob` are the high-volume
221
+ * offenders; `Bash` catches rg/grep/find/fd commands.
222
+ */
223
+ const RAIL_HOOK_MATCHER = 'Grep|Glob|Bash';
224
+ /** Substring that identifies a memtrace-owned Rail hook entry (for idempotent
225
+ * re-register + clean removal). */
226
+ const RAIL_HOOK_MARKER = 'route --hook';
227
+ const LEGACY_HOOK_SCRIPT_BASENAMES = [
228
+ 'userprompt-claude.sh',
229
+ 'posttool-mcp-telemetry.sh',
230
+ ];
231
+ /**
232
+ * Register the Memtrace Rail PreToolUse hook in a Claude settings.json.
233
+ *
234
+ * This is what makes Rail "part of memtrace" rather than a manual install:
235
+ * `memtrace install` wires a PreToolUse hook that pipes every Grep/Glob/Bash
236
+ * attempt through `memtrace route --hook`. The hook defaults to **observe**
237
+ * (zero behavior change) — `MEMTRACE_RAIL=nudge|rail|strict` opts into more.
238
+ * Mode is intentionally NOT baked into the command so the user can change it
239
+ * via env without re-installing.
240
+ *
241
+ * Idempotent: any prior Rail entry (identified by {@link RAIL_HOOK_MARKER}) is
242
+ * replaced, and unrelated PreToolUse hooks are preserved. Never overwrites a
243
+ * malformed file.
244
+ */
245
+ export function registerRailHookInSettingsAt(settingsPath, memtraceBinary) {
246
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
247
+ if (corrupted) {
248
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Rail hook registration.`);
249
+ return { registered: false, backupPath };
250
+ }
251
+ const settings = (value ?? {});
252
+ settings.hooks = isRecord(settings.hooks) ? settings.hooks : {};
253
+ pruneMemtraceOwnedHookEntries(settings.hooks);
254
+ const existing = Array.isArray(settings.hooks.PreToolUse)
255
+ ? settings.hooks.PreToolUse
256
+ : [];
257
+ // Drop any prior memtrace Rail entry so re-install doesn't duplicate it.
258
+ const preserved = existing.filter((entry) => !isMemtraceOwnedHookEntry(entry));
259
+ preserved.push({
260
+ matcher: RAIL_HOOK_MATCHER,
261
+ hooks: [
262
+ {
263
+ type: 'command',
264
+ command: `${memtraceBinary} route --hook`,
265
+ },
266
+ ],
267
+ });
268
+ settings.hooks.PreToolUse = preserved;
269
+ writeJsonAtomic(settingsPath, settings);
270
+ return { registered: true };
271
+ }
272
+ /** True when a PreToolUse entry is a memtrace-owned Rail hook. */
273
+ function isRailHookEntry(entry) {
274
+ return hookCommands(entry).some((command) => command.includes(RAIL_HOOK_MARKER));
275
+ }
276
+ function isLegacyMemtraceHookEntry(entry) {
277
+ return hookCommands(entry).some((command) => (LEGACY_HOOK_SCRIPT_BASENAMES.some((basename) => command.includes(basename))));
278
+ }
279
+ function isMemtraceOwnedHookEntry(entry) {
280
+ return isRailHookEntry(entry) || isLegacyMemtraceHookEntry(entry);
281
+ }
282
+ function hookCommands(entry) {
283
+ if (!isRecord(entry))
284
+ return [];
285
+ const commands = [];
286
+ if (typeof entry.command === 'string') {
287
+ commands.push(entry.command);
288
+ }
289
+ if (Array.isArray(entry.hooks)) {
290
+ for (const hook of entry.hooks) {
291
+ if (isRecord(hook) && typeof hook.command === 'string') {
292
+ commands.push(hook.command);
293
+ }
294
+ }
295
+ }
296
+ return commands;
297
+ }
298
+ function pruneMemtraceOwnedHookEntries(hooks) {
299
+ let changed = false;
300
+ for (const key of Object.keys(hooks)) {
301
+ const entries = hooks[key];
302
+ if (!Array.isArray(entries))
303
+ continue;
304
+ const preserved = entries.filter((entry) => !isMemtraceOwnedHookEntry(entry));
305
+ if (preserved.length === entries.length)
306
+ continue;
307
+ changed = true;
308
+ hooks[key] = preserved.length === 0 ? undefined : preserved;
309
+ }
310
+ return changed;
311
+ }
312
+ function hasHookSettings(hooks) {
313
+ return Object.values(hooks).some((entries) => (entries !== undefined && (!Array.isArray(entries) || entries.length > 0)));
314
+ }
315
+ /**
316
+ * Remove the Memtrace Rail PreToolUse hook from a Claude settings.json.
317
+ * Preserves unrelated hooks; prunes empty containers. Never touches a
318
+ * malformed file.
319
+ */
320
+ export function removeRailHookFromSettingsAt(settingsPath) {
321
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
322
+ if (corrupted) {
323
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Rail hook cleanup.`);
324
+ return { changed: false, backupPath };
325
+ }
326
+ if (!value)
327
+ return { changed: false };
328
+ const settings = value;
329
+ if (!isRecord(settings.hooks) || !Array.isArray(settings.hooks.PreToolUse)) {
330
+ return { changed: false };
331
+ }
332
+ const before = settings.hooks.PreToolUse;
333
+ const after = before.filter((entry) => !isRailHookEntry(entry));
334
+ if (after.length === before.length)
335
+ return { changed: false };
336
+ if (after.length === 0) {
337
+ delete settings.hooks.PreToolUse;
338
+ }
339
+ else {
340
+ settings.hooks.PreToolUse = after;
341
+ }
342
+ if (Object.keys(settings.hooks).length === 0)
343
+ delete settings.hooks;
344
+ writeJsonAtomic(settingsPath, settings);
345
+ return { changed: true };
346
+ }
347
+ /**
348
+ * Merge-add plugin + marketplace entries into a Claude settings.json file.
349
+ * Never overwrites a malformed file.
350
+ */
351
+ export function enablePluginInSettingsAt(settingsPath) {
352
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
353
+ if (corrupted) {
354
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}.`);
355
+ return { registered: false, backupPath };
356
+ }
357
+ const settings = (value ?? {});
358
+ settings.enabledPlugins = settings.enabledPlugins ?? {};
359
+ settings.enabledPlugins[PLUGIN_KEY] = true;
360
+ settings.extraKnownMarketplaces = settings.extraKnownMarketplaces ?? {};
361
+ settings.extraKnownMarketplaces[MARKETPLACE_NAME] = {
362
+ source: { source: 'git', url: MARKETPLACE_GIT_URL },
363
+ };
364
+ writeJsonAtomic(settingsPath, settings);
365
+ return { registered: true };
366
+ }
367
+ /**
368
+ * Enable the plugin in ~/.claude/settings.json.
369
+ */
370
+ function enablePluginInSettings() {
371
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
372
+ enablePluginInSettingsAt(settingsFile);
373
+ }
374
+ /**
375
+ * Remove every Claude settings entry Memtrace may have written across
376
+ * installer versions and Claude marketplace schema changes.
377
+ */
378
+ export function removeClaudeSettingsEntriesAt(settingsPath) {
379
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
380
+ if (corrupted) {
381
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Claude settings cleanup.`);
382
+ return { changed: false, backupPath };
383
+ }
384
+ if (!value)
385
+ return { changed: false };
386
+ const settings = value;
387
+ let changed = false;
388
+ const enabledPlugins = settings.enabledPlugins;
389
+ if (isRecord(enabledPlugins)) {
390
+ for (const key of Object.keys(enabledPlugins)) {
391
+ if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes('memtrace'))) {
392
+ delete enabledPlugins[key];
393
+ changed = true;
394
+ }
395
+ }
396
+ if (Object.keys(enabledPlugins).length === 0)
397
+ delete settings.enabledPlugins;
398
+ }
399
+ const mcpServers = settings.mcpServers;
400
+ if (isRecord(mcpServers) && Object.hasOwn(mcpServers, 'memtrace')) {
401
+ delete mcpServers.memtrace;
402
+ changed = true;
403
+ if (Object.keys(mcpServers).length === 0)
404
+ delete settings.mcpServers;
405
+ }
406
+ // ─── memtrace-rail ─── Remove current Rail and legacy telemetry hooks on uninstall.
407
+ if (isRecord(settings.hooks)) {
408
+ changed = pruneMemtraceOwnedHookEntries(settings.hooks) || changed;
409
+ if (!hasHookSettings(settings.hooks))
410
+ settings.hooks = undefined;
411
+ }
412
+ for (const containerName of MARKETPLACE_SETTING_CONTAINERS) {
413
+ const container = settings[containerName];
414
+ if (!isRecord(container))
415
+ continue;
416
+ for (const key of MARKETPLACE_SETTING_KEYS) {
417
+ if (Object.hasOwn(container, key)) {
418
+ delete container[key];
419
+ changed = true;
420
+ }
421
+ }
422
+ if (Object.keys(container).length === 0)
423
+ delete settings[containerName];
424
+ }
425
+ if (changed)
426
+ writeJsonAtomic(settingsPath, settings);
427
+ return { changed };
428
+ }
429
+ function removeClaudeMarketplaceCacheDirs() {
430
+ const marketplacesRoot = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces');
431
+ for (const entry of MARKETPLACE_SETTING_KEYS) {
432
+ const dir = path.join(marketplacesRoot, entry);
433
+ if (fs.existsSync(dir)) {
434
+ fs.rmSync(dir, { recursive: true, force: true });
435
+ }
436
+ }
437
+ }
438
+ function removeClaudeInstalledPluginMetadata() {
439
+ const installedPluginsFile = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
440
+ const { value, corrupted } = safeReadJson(installedPluginsFile);
441
+ if (corrupted || !value || !isRecord(value.plugins))
442
+ return;
443
+ let changed = false;
444
+ for (const key of Object.keys(value.plugins)) {
445
+ if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes('memtrace'))) {
446
+ delete value.plugins[key];
447
+ changed = true;
448
+ }
449
+ }
450
+ if (changed)
451
+ writeJsonAtomic(installedPluginsFile, value);
452
+ }
453
+ /**
454
+ * Register the memtrace MCP server in Claude Code's settings.
455
+ * This adds the MCP server config so Claude Code can connect to memtrace tools.
456
+ */
457
+ async function registerMcpServer(memtraceBinaryPath) {
458
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
459
+ // ─── memtrace-rail ─── Always wire the Memtrace-first discovery hook so it
460
+ // ships as part of `memtrace install` (not a manual step). Hooks live in
461
+ // settings.json regardless of which MCP-registration strategy wins, and the
462
+ // hook defaults to observe mode (zero behavior change) until the user opts
463
+ // into MEMTRACE_RAIL=nudge|rail|strict. Failure here must not block MCP
464
+ // registration.
465
+ // `memtrace install --no-hooks` (memtrace-public #25) propagates here via
466
+ // MEMTRACE_INSTALL_NO_HOOKS=1 — skip ALL settings.json hook registration.
467
+ if (process.env.MEMTRACE_INSTALL_NO_HOOKS !== '1') {
468
+ try {
469
+ registerRailHookInSettingsAt(settingsFile, memtraceBinaryPath);
470
+ }
471
+ catch (e) {
472
+ console.warn(`memtrace: failed to register Rail hook: ${e.message}`);
473
+ }
474
+ }
475
+ // Strategy 1 (preferred): claude CLI's own add-json path
476
+ const viaCli = await tryMcpAddJson(memtraceBinaryPath);
477
+ if (viaCli)
478
+ return;
479
+ // Strategy 2 (fallback): direct settings.json merge (safe + atomic)
480
+ registerMcpInSettingsAt(settingsFile, memtraceBinaryPath);
481
+ }
482
+ /**
483
+ * Full Claude Code plugin installation.
484
+ *
485
+ * 1. Try `claude plugin marketplace add` + `claude plugin install`
486
+ * 2. Fall back to manual: write cache files + update settings.json
487
+ * 3. Register MCP server for memtrace tools
488
+ */
489
+ export async function installClaudePlugin(skills, memtraceBinaryPath) {
490
+ // Step 1: make sure Claude sees the public HTTPS marketplace source before
491
+ // the CLI attempts to clone/update it.
492
+ enablePluginInSettings();
493
+ removeClaudeMarketplaceCacheDirs();
494
+ // Step 2: Try CLI install
495
+ await tryClaudeCliInstall();
496
+ // Step 3: Find or create cache dir
497
+ let cacheDir = findCliInstalledCacheDir();
498
+ if (cacheDir) {
499
+ const orphanedFile = path.join(cacheDir, '.orphaned_at');
500
+ if (fs.existsSync(orphanedFile))
501
+ fs.unlinkSync(orphanedFile);
502
+ }
503
+ else {
504
+ cacheDir = getClaudePluginCacheDir();
505
+ }
506
+ // Step 4: Clean up old versions
507
+ const pluginRoot = getPluginCacheRoot();
508
+ if (fs.existsSync(pluginRoot)) {
509
+ const activeDirName = path.basename(cacheDir);
510
+ for (const entry of fs.readdirSync(pluginRoot)) {
511
+ if (entry !== activeDirName && entry !== '.DS_Store') {
512
+ fs.rmSync(path.join(pluginRoot, entry), { recursive: true, force: true });
513
+ }
514
+ }
515
+ }
516
+ // Step 5: Write skills
517
+ const skillsDir = path.join(cacheDir, 'skills');
518
+ if (fs.existsSync(skillsDir)) {
519
+ fs.rmSync(skillsDir, { recursive: true });
520
+ }
521
+ for (const skill of skills) {
522
+ const results = transformForClaude(skill);
523
+ for (const { relativePath, content } of results) {
524
+ const fullPath = path.join(cacheDir, relativePath);
525
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
526
+ fs.writeFileSync(fullPath, content);
527
+ }
528
+ }
529
+ writePluginManifest(cacheDir);
530
+ enablePluginInSettings();
531
+ // Step 6: Register MCP server
532
+ await registerMcpServer(memtraceBinaryPath);
533
+ // Skills ship via the memtrace-skills@memtrace plugin only. Writing
534
+ // bare copies to ~/.claude/skills/memtrace-* duplicates every skill
535
+ // in Claude Code when the plugin is also enabled (issue #11).
536
+ return { cacheDir, skillCount: skills.length };
537
+ }
538
+ /**
539
+ * Write skills to ~/.claude/skills/ for SDK-based integrations.
540
+ */
541
+ function writeUserLevelSkills(skills) {
542
+ const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
543
+ if (fs.existsSync(userSkillsDir)) {
544
+ for (const entry of fs.readdirSync(userSkillsDir)) {
545
+ if (entry.startsWith('memtrace-')) {
546
+ fs.rmSync(path.join(userSkillsDir, entry), { recursive: true, force: true });
547
+ }
548
+ }
549
+ }
550
+ for (const skill of skills) {
551
+ const skillName = skill.filename.replace(/\.md$/, '');
552
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
553
+ const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
554
+ const outDir = path.join(userSkillsDir, skillName);
555
+ fs.mkdirSync(outDir, { recursive: true });
556
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
557
+ }
558
+ }
559
+ /**
560
+ * Remove the Claude Code plugin and MCP server registration.
561
+ */
562
+ export async function uninstallClaudePlugin() {
563
+ const hasClaude = await commandExists('claude');
564
+ if (hasClaude) {
565
+ try {
566
+ await execCommand(`claude plugin uninstall ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`);
567
+ }
568
+ catch { /* fall through */ }
569
+ }
570
+ // Manual cleanup
571
+ const cacheRoot = getPluginCacheRoot();
572
+ if (fs.existsSync(cacheRoot)) {
573
+ fs.rmSync(cacheRoot, { recursive: true });
574
+ }
575
+ // Remove from settings.json
576
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
577
+ if (fs.existsSync(settingsFile)) {
578
+ try {
579
+ removeClaudeSettingsEntriesAt(settingsFile);
580
+ }
581
+ catch { /* */ }
582
+ }
583
+ removeClaudeMarketplaceCacheDirs();
584
+ removeClaudeInstalledPluginMetadata();
585
+ // Clean up user-level skills
586
+ const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
587
+ if (fs.existsSync(userSkillsDir)) {
588
+ for (const entry of fs.readdirSync(userSkillsDir)) {
589
+ if (entry.startsWith('memtrace-')) {
590
+ const entryPath = path.join(userSkillsDir, entry);
591
+ const stat = fs.statSync(entryPath);
592
+ if (stat.isDirectory()) {
593
+ fs.rmSync(entryPath, { recursive: true });
594
+ }
595
+ else if (entry.endsWith('.md')) {
596
+ fs.unlinkSync(entryPath);
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+ /**
603
+ * Write/merge the memtrace MCP server entry into a project-local .mcp.json.
604
+ * Safe-reads existing config; never overwrites a malformed file.
605
+ */
606
+ function writeClaudeLocalMcp(mcpPath, binary) {
607
+ const { value, corrupted, backupPath } = safeReadJson(mcpPath);
608
+ if (corrupted) {
609
+ console.warn(`memtrace: ${mcpPath} is malformed; backed up to ${backupPath}. Skipped MCP registration.`);
610
+ return false;
611
+ }
612
+ const cfg = (value ?? {});
613
+ cfg.mcpServers = cfg.mcpServers ?? {};
614
+ // Deep-merge env: preserve user-set keys, memtrace defaults win on conflict.
615
+ const existing = cfg.mcpServers['memtrace'];
616
+ const existingEnv = (isRecord(existing) && isRecord(existing.env))
617
+ ? existing.env
618
+ : {};
619
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
620
+ cfg.mcpServers['memtrace'] = {
621
+ command: binary,
622
+ args: ['mcp'],
623
+ env: mergedEnv,
624
+ };
625
+ writeJsonAtomic(mcpPath, cfg);
626
+ return true;
627
+ }
628
+ export const claudeTransformer = {
629
+ name: 'claude',
630
+ async install(skills, ctx) {
631
+ if (ctx.scope === 'global') {
632
+ const { skillCount } = await installClaudePlugin(skills, ctx.memtraceBinary);
633
+ return {
634
+ agent: 'claude',
635
+ skillsWritten: skillCount,
636
+ skillsDir: path.join(os.homedir(), '.claude', 'skills'),
637
+ mcpConfigPath: path.join(os.homedir(), '.claude', 'settings.json'),
638
+ mcpRegistered: !ctx.skipMcp,
639
+ warnings: [],
640
+ };
641
+ }
642
+ // Local scope: write skills into <cwd>/.claude/skills/
643
+ const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
644
+ if (fs.existsSync(skillsDir)) {
645
+ for (const entry of fs.readdirSync(skillsDir)) {
646
+ if (entry.startsWith('memtrace-')) {
647
+ fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
648
+ }
649
+ }
650
+ }
651
+ let count = 0;
652
+ for (const skill of skills) {
653
+ const skillName = skill.filename.replace(/\.md$/, '');
654
+ const outDir = path.join(skillsDir, skillName);
655
+ fs.mkdirSync(outDir, { recursive: true });
656
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
657
+ const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
658
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
659
+ count++;
660
+ }
661
+ // MCP for local scope: .mcp.json at project root (Claude's project-level convention)
662
+ const mcpConfigPath = path.join(ctx.cwd, '.mcp.json');
663
+ let mcpRegistered = false;
664
+ if (!ctx.skipMcp) {
665
+ mcpRegistered = writeClaudeLocalMcp(mcpConfigPath, ctx.memtraceBinary);
666
+ }
667
+ return {
668
+ agent: 'claude',
669
+ skillsWritten: count,
670
+ skillsDir,
671
+ mcpConfigPath,
672
+ mcpRegistered,
673
+ warnings: [],
674
+ };
675
+ },
676
+ async uninstall(ctx) {
677
+ if (ctx.scope === 'global') {
678
+ await uninstallClaudePlugin();
679
+ return;
680
+ }
681
+ // Local: remove <cwd>/.claude/skills/memtrace-* and the memtrace entry from <cwd>/.mcp.json
682
+ const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
683
+ if (fs.existsSync(skillsDir)) {
684
+ for (const entry of fs.readdirSync(skillsDir)) {
685
+ if (entry.startsWith('memtrace-')) {
686
+ fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
687
+ }
688
+ }
689
+ }
690
+ const mcpPath = path.join(ctx.cwd, '.mcp.json');
691
+ const { value, corrupted } = safeReadJson(mcpPath);
692
+ if (!corrupted && value?.mcpServers?.['memtrace']) {
693
+ delete value.mcpServers['memtrace'];
694
+ if (Object.keys(value.mcpServers).length === 0)
695
+ delete value.mcpServers;
696
+ if (Object.keys(value).length === 0)
697
+ fs.unlinkSync(mcpPath);
698
+ else
699
+ writeJsonAtomic(mcpPath, value);
700
+ }
701
+ },
702
+ };