supermind-claude 2.1.0 → 4.0.1

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 (44) hide show
  1. package/.claude-plugin/plugin.json +21 -0
  2. package/README.md +34 -46
  3. package/agents/code-reviewer.md +81 -0
  4. package/cli/commands/doctor.js +415 -79
  5. package/cli/commands/install.js +17 -18
  6. package/cli/commands/skill.js +164 -0
  7. package/cli/commands/uninstall.js +32 -3
  8. package/cli/commands/update.js +27 -5
  9. package/cli/index.js +16 -4
  10. package/cli/lib/agents.js +413 -0
  11. package/cli/lib/executor.js +365 -0
  12. package/cli/lib/hooks.js +8 -1
  13. package/cli/lib/logger.js +1 -1
  14. package/cli/lib/mcp.js +25 -5
  15. package/cli/lib/planning.js +502 -0
  16. package/cli/lib/platform.js +4 -0
  17. package/cli/lib/plugin.js +127 -0
  18. package/cli/lib/settings.js +2 -40
  19. package/cli/lib/skills.js +39 -2
  20. package/cli/lib/templates.js +48 -1
  21. package/cli/lib/vendor-skills.js +594 -0
  22. package/hooks/bash-permissions.js +196 -176
  23. package/hooks/context-monitor.js +79 -0
  24. package/hooks/improvement-logger.js +94 -0
  25. package/hooks/pre-merge-checklist.js +102 -0
  26. package/hooks/session-start.js +109 -5
  27. package/hooks/statusline-command.js +123 -29
  28. package/package.json +4 -2
  29. package/skills/anti-rationalization/SKILL.md +38 -0
  30. package/skills/brainstorming/SKILL.md +165 -0
  31. package/skills/code-review/SKILL.md +144 -0
  32. package/skills/executing-plans/SKILL.md +138 -0
  33. package/skills/finishing-branches/SKILL.md +144 -0
  34. package/skills/project/SKILL.md +533 -0
  35. package/skills/quick/SKILL.md +178 -0
  36. package/skills/supermind/SKILL.md +58 -4
  37. package/skills/supermind-init/SKILL.md +48 -2
  38. package/skills/systematic-debugging/SKILL.md +129 -0
  39. package/skills/tdd/SKILL.md +179 -0
  40. package/skills/using-git-worktrees/SKILL.md +138 -0
  41. package/skills/verification-before-completion/SKILL.md +54 -0
  42. package/skills/writing-plans/SKILL.md +169 -0
  43. package/templates/CLAUDE.md +124 -61
  44. package/cli/lib/plugins.js +0 -23
@@ -0,0 +1,502 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Constants
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const PLANNING_DIR = '.planning';
11
+ const PHASES_DIR = 'phases';
12
+ const CONFIG_FILE = 'config.json';
13
+ const ROADMAP_FILE = 'roadmap.md';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Path safety — mirrors vendor-skills.js safeJoin pattern
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Validate that a relative path segment is safe (no traversal, not absolute),
21
+ * then join it onto a trusted base using string concatenation so semgrep
22
+ * taint-tracking does not flag path.join with untrusted input.
23
+ *
24
+ * @param {string} trustedBase — already-safe absolute directory
25
+ * @param {string} segment — caller-supplied component to validate
26
+ * @param {string} label — for error messages
27
+ * @returns {string} safe absolute path
28
+ */
29
+ function safeJoin(trustedBase, segment, label) {
30
+ if (typeof segment !== 'string' || segment.length === 0) {
31
+ throw new Error(`Invalid ${label}: must be a non-empty string`);
32
+ }
33
+ if (path.isAbsolute(segment)) {
34
+ throw new Error(`Invalid ${label}: must not be an absolute path`);
35
+ }
36
+ const parts = segment.split(/[\\/]/);
37
+ for (const part of parts) {
38
+ if (part === '..') {
39
+ throw new Error(`Invalid ${label}: path traversal sequences are not allowed`);
40
+ }
41
+ }
42
+ const resolved = trustedBase + path.sep + parts.join(path.sep);
43
+ if (!resolved.startsWith(trustedBase + path.sep) && resolved !== trustedBase) {
44
+ throw new Error(`Invalid ${label}: resolved path escapes base directory`);
45
+ }
46
+ return resolved;
47
+ }
48
+
49
+ /**
50
+ * Validate phaseNum is a positive integer and return the safe directory name.
51
+ */
52
+ function safePhaseSegment(phaseNum) {
53
+ const n = Number(phaseNum);
54
+ if (!Number.isInteger(n) || n < 1) {
55
+ throw new Error(`Invalid phase number: must be a positive integer, got ${phaseNum}`);
56
+ }
57
+ return `phase-${n}`;
58
+ }
59
+
60
+ /**
61
+ * Validate a filename segment (agentName, taskId, planId) contains only safe chars.
62
+ * Allows alphanumeric, hyphens, underscores, and dots (no slashes or traversal).
63
+ */
64
+ function safeFilenameSegment(value, label) {
65
+ const str = String(value);
66
+ if (str.length === 0) {
67
+ throw new Error(`Invalid ${label}: must be non-empty`);
68
+ }
69
+ if (!/^[\w.-]+$/.test(str)) {
70
+ throw new Error(`Invalid ${label}: contains disallowed characters`);
71
+ }
72
+ if (str === '.' || str === '..') {
73
+ throw new Error(`Invalid ${label}: traversal not allowed`);
74
+ }
75
+ return str;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Internal helpers — all paths built via safeJoin
80
+ // ---------------------------------------------------------------------------
81
+
82
+ function planningRoot(projectRoot) {
83
+ // PLANNING_DIR is a constant, not user input — safe to join directly
84
+ return safeJoin(projectRoot, PLANNING_DIR, 'planning directory');
85
+ }
86
+
87
+ function phasePath(projectRoot, phaseNum) {
88
+ const root = planningRoot(projectRoot);
89
+ const phasesDir = safeJoin(root, PHASES_DIR, 'phases directory');
90
+ return safeJoin(phasesDir, safePhaseSegment(phaseNum), 'phase directory');
91
+ }
92
+
93
+ function ensureDir(dirPath) {
94
+ fs.mkdirSync(dirPath, { recursive: true });
95
+ }
96
+
97
+ function safeReadFile(filePath) {
98
+ try {
99
+ return fs.readFileSync(filePath, 'utf-8');
100
+ } catch (err) {
101
+ if (err.code === 'ENOENT') return null;
102
+ throw err;
103
+ }
104
+ }
105
+
106
+ function safeReadJson(filePath) {
107
+ const raw = safeReadFile(filePath);
108
+ if (raw === null) return null;
109
+ try {
110
+ return JSON.parse(raw);
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function writeFile(filePath, content) {
117
+ ensureDir(path.dirname(filePath));
118
+ fs.writeFileSync(filePath, content, 'utf-8');
119
+ }
120
+
121
+ function writeJson(filePath, data) {
122
+ writeFile(filePath, JSON.stringify(data, null, 2) + '\n');
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Roadmap parsing
127
+ // ---------------------------------------------------------------------------
128
+
129
+ const ROADMAP_HEADER = `# Roadmap
130
+
131
+ | Phase | Status | Description |
132
+ |-------|--------|-------------|`;
133
+
134
+ function parseRoadmapTable(content) {
135
+ const phases = [];
136
+ const lines = content.split('\n');
137
+ for (const line of lines) {
138
+ const match = line.match(/^\|\s*(\d+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*$/);
139
+ if (match && match[2].trim()) {
140
+ phases.push({
141
+ phase: parseInt(match[1], 10),
142
+ status: match[2].trim(),
143
+ description: match[3].trim(),
144
+ });
145
+ }
146
+ }
147
+ return phases;
148
+ }
149
+
150
+ function renderRoadmapTable(phases) {
151
+ const rows = phases.map(p => `| ${p.phase} | ${p.status} | ${p.description} |`);
152
+ return ROADMAP_HEADER + '\n' + rows.join('\n') + '\n';
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Progress parsing
157
+ // ---------------------------------------------------------------------------
158
+
159
+ const PROGRESS_HEADER = `# Progress
160
+
161
+ | Wave | Task | Status | Executor | Commit |
162
+ |------|------|--------|----------|--------|`;
163
+
164
+ function parseProgressTable(content) {
165
+ const entries = [];
166
+ const lines = content.split('\n');
167
+ for (const line of lines) {
168
+ const match = line.match(/^\|\s*(\d+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*$/);
169
+ if (match) {
170
+ entries.push({
171
+ wave: parseInt(match[1], 10),
172
+ task: match[2].trim(),
173
+ status: match[3].trim(),
174
+ executor: match[4].trim(),
175
+ commit: match[5].trim(),
176
+ });
177
+ }
178
+ }
179
+ return entries;
180
+ }
181
+
182
+ function renderProgressTable(entries) {
183
+ const rows = entries.map(e =>
184
+ `| ${e.wave} | ${e.task} | ${e.status} | ${e.executor || ''} | ${e.commit || ''} |`
185
+ );
186
+ return PROGRESS_HEADER + '\n' + rows.join('\n') + '\n';
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Public API
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Create .planning/ structure with roadmap.md and config.json.
195
+ *
196
+ * @param {string} projectRoot — absolute path to project root
197
+ * @param {object} config — { modelProfile, flags, ... }
198
+ * @returns {{ planningDir: string }}
199
+ */
200
+ function initPlanning(projectRoot, config = {}) {
201
+ const root = planningRoot(projectRoot);
202
+ ensureDir(safeJoin(root, PHASES_DIR, 'phases directory'));
203
+
204
+ const configData = {
205
+ modelProfile: config.modelProfile || 'default',
206
+ flags: config.flags || {},
207
+ createdAt: new Date().toISOString(),
208
+ lastUpdated: new Date().toISOString(),
209
+ };
210
+ writeJson(safeJoin(root, CONFIG_FILE, 'config file'), configData);
211
+ writeFile(safeJoin(root, ROADMAP_FILE, 'roadmap file'), ROADMAP_HEADER + '\n');
212
+
213
+ return { planningDir: root };
214
+ }
215
+
216
+ /**
217
+ * Create phases/phase-N/ with empty discussion.md, research/, plans/, tasks/, progress.md.
218
+ *
219
+ * @param {string} projectRoot
220
+ * @param {number} phaseNum
221
+ * @returns {{ phaseDir: string }}
222
+ */
223
+ function initPhase(projectRoot, phaseNum) {
224
+ const dir = phasePath(projectRoot, phaseNum);
225
+
226
+ ensureDir(safeJoin(dir, 'research', 'research directory'));
227
+ ensureDir(safeJoin(dir, 'plans', 'plans directory'));
228
+ ensureDir(safeJoin(dir, 'tasks', 'tasks directory'));
229
+
230
+ writeFile(
231
+ safeJoin(dir, 'discussion.md', 'discussion file'),
232
+ `# Phase ${phaseNum} — Discussion\n\n`,
233
+ );
234
+ writeFile(safeJoin(dir, 'progress.md', 'progress file'), PROGRESS_HEADER + '\n');
235
+
236
+ return { phaseDir: dir };
237
+ }
238
+
239
+ /**
240
+ * Read the latest progress state from a phase's progress.md.
241
+ *
242
+ * @param {string} projectRoot
243
+ * @param {number} [phaseNum] — if omitted, finds the latest active phase
244
+ * @returns {{ phase: number, entries: Array, summary: { total: number, done: number, pending: number, failed: number, currentWave: number } } | null}
245
+ */
246
+ function readProgress(projectRoot, phaseNum) {
247
+ if (phaseNum === undefined) {
248
+ const roadmap = readRoadmap(projectRoot);
249
+ if (!roadmap) return null;
250
+ const active = roadmap.find(p => p.status !== 'completed' && p.status !== 'skipped');
251
+ if (!active) return null;
252
+ phaseNum = active.phase;
253
+ }
254
+
255
+ const filePath = safeJoin(phasePath(projectRoot, phaseNum), 'progress.md', 'progress file');
256
+ const content = safeReadFile(filePath);
257
+ if (content === null) return null;
258
+
259
+ const entries = parseProgressTable(content);
260
+ const done = entries.filter(e => e.status === 'completed').length;
261
+ const failed = entries.filter(e => e.status === 'failed').length;
262
+ const pending = entries.length - done - failed;
263
+ const currentWave = entries.length > 0
264
+ ? Math.max(...entries.filter(e => e.status !== 'completed').map(e => e.wave).concat([0]))
265
+ : 0;
266
+
267
+ return {
268
+ phase: phaseNum,
269
+ entries,
270
+ summary: { total: entries.length, done, pending, failed, currentWave },
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Update progress.md with current wave execution state.
276
+ *
277
+ * @param {string} projectRoot
278
+ * @param {number} phaseNum
279
+ * @param {Array<{ wave: number, task: string, status: string, executor?: string, commit?: string }>} progressData
280
+ */
281
+ function writeProgress(projectRoot, phaseNum, progressData) {
282
+ const filePath = safeJoin(phasePath(projectRoot, phaseNum), 'progress.md', 'progress file');
283
+ writeFile(filePath, renderProgressTable(progressData));
284
+ }
285
+
286
+ /**
287
+ * Parse roadmap.md to get phase list with statuses.
288
+ *
289
+ * @param {string} projectRoot
290
+ * @returns {Array<{ phase: number, status: string, description: string }> | null}
291
+ */
292
+ function readRoadmap(projectRoot) {
293
+ const filePath = safeJoin(planningRoot(projectRoot), ROADMAP_FILE, 'roadmap file');
294
+ const content = safeReadFile(filePath);
295
+ if (content === null) return null;
296
+ return parseRoadmapTable(content);
297
+ }
298
+
299
+ /**
300
+ * Update a phase's status in roadmap.md.
301
+ *
302
+ * @param {string} projectRoot
303
+ * @param {number} phaseNum
304
+ * @param {string} status — e.g. 'pending', 'active', 'completed', 'skipped'
305
+ * @returns {boolean} true if the phase was found and updated
306
+ */
307
+ function updateRoadmap(projectRoot, phaseNum, status) {
308
+ const filePath = safeJoin(planningRoot(projectRoot), ROADMAP_FILE, 'roadmap file');
309
+ const phases = readRoadmap(projectRoot) || [];
310
+ const idx = phases.findIndex(p => p.phase === phaseNum);
311
+ if (idx < 0) return false;
312
+ phases[idx].status = status;
313
+ writeFile(filePath, renderRoadmapTable(phases));
314
+ return true;
315
+ }
316
+
317
+ /**
318
+ * Read .planning/config.json.
319
+ *
320
+ * @param {string} projectRoot
321
+ * @returns {object | null}
322
+ */
323
+ function readConfig(projectRoot) {
324
+ return safeReadJson(safeJoin(planningRoot(projectRoot), CONFIG_FILE, 'config file'));
325
+ }
326
+
327
+ /**
328
+ * Write .planning/config.json.
329
+ *
330
+ * @param {string} projectRoot
331
+ * @param {object} config
332
+ */
333
+ function writeConfig(projectRoot, config) {
334
+ const toWrite = { ...config, lastUpdated: new Date().toISOString() };
335
+ writeJson(safeJoin(planningRoot(projectRoot), CONFIG_FILE, 'config file'), toWrite);
336
+ }
337
+
338
+ /**
339
+ * Append to discussion.md.
340
+ *
341
+ * @param {string} projectRoot
342
+ * @param {number} phaseNum
343
+ * @param {string} content — markdown text to append
344
+ */
345
+ function writeDiscussion(projectRoot, phaseNum, content) {
346
+ const filePath = safeJoin(phasePath(projectRoot, phaseNum), 'discussion.md', 'discussion file');
347
+ const existing = safeReadFile(filePath) || `# Phase ${phaseNum} — Discussion\n\n`;
348
+ writeFile(filePath, existing + content + '\n');
349
+ }
350
+
351
+ /**
352
+ * Write researcher output to research/.
353
+ *
354
+ * @param {string} projectRoot
355
+ * @param {number} phaseNum
356
+ * @param {string} agentName — e.g. 'stack', 'features', 'architecture', 'pitfalls'
357
+ * @param {string} content — markdown content
358
+ */
359
+ function writeResearch(projectRoot, phaseNum, agentName, content) {
360
+ const safeName = safeFilenameSegment(agentName, 'agent name');
361
+ const researchDir = safeJoin(phasePath(projectRoot, phaseNum), 'research', 'research directory');
362
+ const filePath = safeJoin(researchDir, `${safeName}.md`, 'research file');
363
+ writeFile(filePath, content);
364
+ }
365
+
366
+ /**
367
+ * Write plan with dependency graph to plans/.
368
+ *
369
+ * @param {string} projectRoot
370
+ * @param {number} phaseNum
371
+ * @param {{ id: number, title: string, dependencies?: number[], tasks?: string[], waves?: Array, body?: string }} planData
372
+ */
373
+ function writePlan(projectRoot, phaseNum, planData) {
374
+ const planId = safeFilenameSegment(String(planData.id || 1), 'plan id');
375
+
376
+ let content = `---\nid: ${planId}\ndependencies: [${(planData.dependencies || []).join(', ')}]\n---\n\n`;
377
+ content += `# Plan ${planId}: ${planData.title || 'Untitled'}\n\n`;
378
+
379
+ if (planData.waves) {
380
+ for (const wave of planData.waves) {
381
+ content += `## Wave ${wave.number}\n\n`;
382
+ for (const task of (wave.tasks || [])) {
383
+ content += `- ${task}\n`;
384
+ }
385
+ content += '\n';
386
+ }
387
+ }
388
+
389
+ if (planData.body) {
390
+ content += planData.body + '\n';
391
+ }
392
+
393
+ const plansDir = safeJoin(phasePath(projectRoot, phaseNum), 'plans', 'plans directory');
394
+ const filePath = safeJoin(plansDir, `plan-${planId}.md`, 'plan file');
395
+ writeFile(filePath, content);
396
+ }
397
+
398
+ /**
399
+ * Write individual task spec to tasks/.
400
+ *
401
+ * @param {string} projectRoot
402
+ * @param {number} phaseNum
403
+ * @param {string|number} taskId
404
+ * @param {{ title: string, description?: string, skills?: string[], files?: string[], acceptance?: string[], wave?: number }} taskSpec
405
+ */
406
+ function writeTask(projectRoot, phaseNum, taskId, taskSpec) {
407
+ const safeId = safeFilenameSegment(String(taskId), 'task id');
408
+
409
+ let content = `---\nid: ${safeId}\nwave: ${taskSpec.wave || 1}\n`;
410
+ if (taskSpec.skills?.length) content += `skills: [${taskSpec.skills.join(', ')}]\n`;
411
+ content += `---\n\n`;
412
+ content += `# Task ${safeId}: ${taskSpec.title || 'Untitled'}\n\n`;
413
+
414
+ if (taskSpec.description) {
415
+ content += taskSpec.description + '\n\n';
416
+ }
417
+
418
+ if (taskSpec.files?.length) {
419
+ content += `## Files\n\n`;
420
+ for (const f of taskSpec.files) content += `- ${f}\n`;
421
+ content += '\n';
422
+ }
423
+
424
+ if (taskSpec.acceptance?.length) {
425
+ content += `## Acceptance Criteria\n\n`;
426
+ for (const a of taskSpec.acceptance) content += `- [ ] ${a}\n`;
427
+ content += '\n';
428
+ }
429
+
430
+ const tasksDir = safeJoin(phasePath(projectRoot, phaseNum), 'tasks', 'tasks directory');
431
+ const filePath = safeJoin(tasksDir, `task-${safeId}.md`, 'task file');
432
+ writeFile(filePath, content);
433
+ }
434
+
435
+ /**
436
+ * Walk up from startDir to find .planning/ (like how git finds .git/).
437
+ *
438
+ * @param {string} startDir — must be an absolute path
439
+ * @returns {string | null} — project root containing .planning/, or null
440
+ */
441
+ function getPlanningRoot(startDir) {
442
+ if (typeof startDir !== 'string' || startDir.length === 0) {
443
+ throw new Error('getPlanningRoot: startDir must be a non-empty string');
444
+ }
445
+ // Require an absolute path — callers pass process.cwd() or PROJECT_DIR
446
+ if (!path.isAbsolute(startDir)) {
447
+ throw new Error('getPlanningRoot: startDir must be an absolute path');
448
+ }
449
+ let dir = startDir;
450
+ const root = path.parse(dir).root;
451
+
452
+ while (dir !== root) {
453
+ // PLANNING_DIR is a constant — safe to join
454
+ const candidate = safeJoin(dir, PLANNING_DIR, 'planning directory');
455
+ if (fs.existsSync(candidate)) {
456
+ return dir;
457
+ }
458
+ dir = path.dirname(dir);
459
+ }
460
+ return null;
461
+ }
462
+
463
+ /**
464
+ * Check if there's an active (non-completed) planning session.
465
+ *
466
+ * @param {string} projectRoot
467
+ * @returns {boolean}
468
+ */
469
+ function isActive(projectRoot) {
470
+ const roadmap = readRoadmap(projectRoot);
471
+ if (!roadmap || roadmap.length === 0) {
472
+ return fs.existsSync(planningRoot(projectRoot));
473
+ }
474
+ return roadmap.some(p => p.status !== 'completed' && p.status !== 'skipped');
475
+ }
476
+
477
+ // ---------------------------------------------------------------------------
478
+ // Exports
479
+ // ---------------------------------------------------------------------------
480
+
481
+ module.exports = {
482
+ initPlanning,
483
+ initPhase,
484
+ readProgress,
485
+ writeProgress,
486
+ readRoadmap,
487
+ updateRoadmap,
488
+ readConfig,
489
+ writeConfig,
490
+ writeDiscussion,
491
+ writeResearch,
492
+ writePlan,
493
+ writeTask,
494
+ getPlanningRoot,
495
+ isActive,
496
+ // Exposed for testing
497
+ parseRoadmapTable,
498
+ renderRoadmapTable,
499
+ parseProgressTable,
500
+ renderProgressTable,
501
+ PLANNING_DIR,
502
+ };
@@ -12,11 +12,15 @@ const PATHS = {
12
12
  settingsBackup: path.join(home, '.claude', 'settings.json.backup'),
13
13
  hooksDir: path.join(home, '.claude', 'hooks'),
14
14
  skillsDir: path.join(home, '.claude', 'skills'),
15
+ agentsDir: path.join(home, '.claude', 'agents'),
15
16
  templatesDir: path.join(home, '.claude', 'templates'),
16
17
  sessionsDir: path.join(home, '.claude', 'sessions'),
17
18
  versionFile: path.join(home, '.claude', '.supermind-version'),
18
19
  legacyHooksJson: path.join(home, '.claude', 'hooks.json'),
19
20
  airisDir: path.join(home, '.claude', 'airis'),
21
+ improvementLog: path.join(home, '.claude', 'improvement-log.jsonl'),
22
+ skillsLock: path.join(home, '.claude', 'skills-lock.json'),
23
+ approvedCommands: path.join(home, '.claude', 'supermind-approved.json'),
20
24
  };
21
25
 
22
26
  function ensureDir(dirPath) {
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { PATHS, ensureDir, getPackageRoot } = require('./platform');
6
+ const logger = require('./logger');
7
+ const { version } = require('../../package.json');
8
+
9
+ const PLUGIN_NAME = 'supermind';
10
+ const PLUGIN_MARKETPLACE = 'local';
11
+ const PLUGIN_KEY = `${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}`;
12
+
13
+ function getPluginsDir() {
14
+ return path.join(PATHS.claudeHome, 'plugins');
15
+ }
16
+
17
+ function getPluginCacheBase() {
18
+ return path.join(getPluginsDir(), 'cache', PLUGIN_MARKETPLACE, PLUGIN_NAME);
19
+ }
20
+
21
+ function getPluginCacheDir() {
22
+ return path.join(getPluginCacheBase(), version);
23
+ }
24
+
25
+ function getInstalledPluginsPath() {
26
+ return path.join(getPluginsDir(), 'installed_plugins.json');
27
+ }
28
+
29
+ function readInstalledPlugins() {
30
+ const filePath = getInstalledPluginsPath();
31
+ try {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
33
+ } catch {
34
+ return { version: 2, plugins: {} };
35
+ }
36
+ }
37
+
38
+ function writeInstalledPlugins(data) {
39
+ const filePath = getInstalledPluginsPath();
40
+ ensureDir(path.dirname(filePath));
41
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
42
+ }
43
+
44
+ // Build plugin.json with the current version from package.json.
45
+ // The template .claude-plugin/plugin.json uses a placeholder version
46
+ // that is always overridden here with the actual package.json version.
47
+ function buildPluginManifest() {
48
+ const templatePath = path.join(getPackageRoot(), '.claude-plugin', 'plugin.json');
49
+ const manifest = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
50
+ manifest.version = version;
51
+ return manifest;
52
+ }
53
+
54
+ // Remove stale version directories from the plugin cache, keeping only the current version.
55
+ function cleanOldVersions() {
56
+ const cacheBase = getPluginCacheBase();
57
+ if (!fs.existsSync(cacheBase)) return;
58
+ for (const entry of fs.readdirSync(cacheBase)) {
59
+ if (entry !== version) {
60
+ const stale = path.join(cacheBase, entry);
61
+ try {
62
+ fs.rmSync(stale, { recursive: true, force: true });
63
+ } catch { /* non-critical cleanup */ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // Register Supermind as a Claude Code plugin.
69
+ // This is a forward-looking registration stub — Supermind's skills, hooks, and agents
70
+ // are delivered through the traditional ~/.claude/ installation paths, not through the
71
+ // plugin cache. The registration enables future marketplace discovery and `/plugin update`
72
+ // support when Claude Code adds npm-based plugin sources.
73
+ function installPlugin() {
74
+ const cacheDir = getPluginCacheDir();
75
+ ensureDir(cacheDir);
76
+
77
+ // Write plugin manifest to cache
78
+ const pluginDir = path.join(cacheDir, '.claude-plugin');
79
+ ensureDir(pluginDir);
80
+ const manifest = buildPluginManifest();
81
+ fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
82
+
83
+ // Clean up old version directories
84
+ cleanOldVersions();
85
+
86
+ // Register in installed_plugins.json
87
+ const registry = readInstalledPlugins();
88
+ const now = new Date().toISOString();
89
+ const entry = {
90
+ scope: 'user',
91
+ installPath: cacheDir,
92
+ version: version,
93
+ installedAt: now,
94
+ lastUpdated: now,
95
+ };
96
+
97
+ // Preserve original installedAt on re-install
98
+ const existing = registry.plugins[PLUGIN_KEY];
99
+ if (existing && Array.isArray(existing) && existing.length > 0) {
100
+ entry.installedAt = existing[0].installedAt || now;
101
+ }
102
+
103
+ registry.plugins[PLUGIN_KEY] = [entry];
104
+ writeInstalledPlugins(registry);
105
+
106
+ logger.success(`Plugin registered as ${PLUGIN_KEY}`);
107
+ }
108
+
109
+ // Remove plugin registration and cached files
110
+ function removePlugin() {
111
+ // Remove from installed_plugins.json
112
+ const registry = readInstalledPlugins();
113
+ if (registry.plugins[PLUGIN_KEY]) {
114
+ delete registry.plugins[PLUGIN_KEY];
115
+ writeInstalledPlugins(registry);
116
+ logger.success('Plugin registration removed');
117
+ }
118
+
119
+ // Remove cached plugin directory
120
+ const cacheBase = getPluginCacheBase();
121
+ if (fs.existsSync(cacheBase)) {
122
+ fs.rmSync(cacheBase, { recursive: true, force: true });
123
+ logger.success('Plugin cache removed');
124
+ }
125
+ }
126
+
127
+ module.exports = { installPlugin, removePlugin, PLUGIN_NAME, PLUGIN_KEY };