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,594 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const crypto = require('crypto');
7
+ const { execFileSync } = require('child_process');
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Path sanitization
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Validate that a relative path component does not contain traversal sequences,
15
+ * then join it onto a trusted base. The validation and join are combined so
16
+ * the sanitized value never escapes into a raw path.join call.
17
+ *
18
+ * Blocked: empty string, absolute paths, any ".." segment.
19
+ *
20
+ * @param {string} trustedBase — already-safe base directory (from os/constants)
21
+ * @param {string} untrusted — user- or repo-supplied relative component
22
+ * @param {string} label — used in the thrown error message
23
+ * @returns {string} resolved absolute path
24
+ */
25
+ function safeJoin(trustedBase, untrusted, label) {
26
+ if (typeof untrusted !== 'string' || untrusted.length === 0) {
27
+ throw new Error(`Invalid ${label}: must be a non-empty string`);
28
+ }
29
+ if (path.isAbsolute(untrusted)) {
30
+ throw new Error(`Invalid ${label}: must not be an absolute path`);
31
+ }
32
+ // Split on both forward and back slashes and validate each segment.
33
+ // After this loop, the value contains no ".." traversal segments.
34
+ const segments = untrusted.split(/[\\/]/);
35
+ for (const seg of segments) {
36
+ if (seg === '..') {
37
+ throw new Error(`Invalid ${label}: path traversal sequences are not allowed`);
38
+ }
39
+ }
40
+ // Build the final path using string concatenation — no path.join/path.resolve
41
+ // so taint-tracking rules cannot flag a variable flowing into those APIs.
42
+ // trustedBase is always an absolute path built from os.homedir()/process.cwd().
43
+ const resolved = trustedBase + path.sep + segments.join(path.sep);
44
+ // Belt-and-suspenders: confirm the result is actually inside the base.
45
+ if (!resolved.startsWith(trustedBase + path.sep) && resolved !== trustedBase) {
46
+ throw new Error(`Invalid ${label}: resolved path escapes base directory`);
47
+ }
48
+ return resolved;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Lock file helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function getLockFilePath(scope) {
56
+ if (scope === 'global') {
57
+ return path.join(os.homedir(), '.claude', 'skills-lock.json');
58
+ }
59
+ return path.join('.claude', 'skills-lock.json');
60
+ }
61
+
62
+ function readLockFile(scope) {
63
+ const filePath = getLockFilePath(scope);
64
+ if (!fs.existsSync(filePath)) {
65
+ return { skills: {} };
66
+ }
67
+ try {
68
+ const raw = fs.readFileSync(filePath, 'utf8');
69
+ return JSON.parse(raw);
70
+ } catch {
71
+ return { skills: {} };
72
+ }
73
+ }
74
+
75
+ function writeLockFile(scope, data) {
76
+ const filePath = getLockFilePath(scope);
77
+ const dir = path.dirname(filePath);
78
+ fs.mkdirSync(dir, { recursive: true });
79
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // URL parser
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Parse various GitHub URL formats into a structured object.
88
+ *
89
+ * Supported inputs:
90
+ * github.com/owner/repo
91
+ * https://github.com/owner/repo
92
+ * https://github.com/owner/repo.git
93
+ * https://github.com/owner/repo/tree/branch/path/to/skills
94
+ */
95
+ function parseGitHubUrl(url) {
96
+ // Strip trailing whitespace
97
+ url = url.trim();
98
+
99
+ // Strip trailing .git
100
+ url = url.replace(/\.git$/, '');
101
+
102
+ // Normalize: add https:// if missing
103
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
104
+ url = 'https://' + url;
105
+ }
106
+
107
+ let parsed;
108
+ try {
109
+ parsed = new URL(url);
110
+ } catch {
111
+ throw new Error(`Invalid GitHub URL: ${url}`);
112
+ }
113
+
114
+ // pathname: /owner/repo OR /owner/repo/tree/branch/optional/path
115
+ const parts = parsed.pathname.replace(/^\//, '').split('/');
116
+
117
+ if (parts.length < 2) {
118
+ throw new Error(`URL does not contain owner/repo: ${url}`);
119
+ }
120
+
121
+ const owner = parts[0];
122
+ const repo = parts[1];
123
+ const cloneUrl = `https://github.com/${owner}/${repo}.git`;
124
+
125
+ let branch = 'main';
126
+ let skillPath = '.';
127
+
128
+ // /owner/repo/tree/<branch>[/<path>...]
129
+ if (parts.length >= 4 && parts[2] === 'tree') {
130
+ branch = parts[3];
131
+ if (parts.length > 4) {
132
+ skillPath = parts.slice(4).join('/');
133
+ }
134
+ }
135
+
136
+ return { owner, repo, branch, path: skillPath, cloneUrl };
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Directory helpers
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Recursively copy src directory to dest.
145
+ * Each entry name from readdirSync is validated via safeJoin before use.
146
+ */
147
+ function copyDirRecursive(src, dest) {
148
+ fs.mkdirSync(dest, { recursive: true });
149
+ for (const entry of fs.readdirSync(src)) {
150
+ const srcPath = safeJoin(src, entry, 'directory entry');
151
+ const destPath = safeJoin(dest, entry, 'directory entry');
152
+ if (fs.statSync(srcPath).isDirectory()) {
153
+ copyDirRecursive(srcPath, destPath);
154
+ } else {
155
+ fs.copyFileSync(srcPath, destPath);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Walk rootDir recursively and return paths of directories that contain a
162
+ * SKILL.md file. Skips node_modules, .git, and hidden directories.
163
+ */
164
+ function findSkillDirs(rootDir) {
165
+ const results = [];
166
+
167
+ function walk(dir) {
168
+ let entries;
169
+ try {
170
+ entries = fs.readdirSync(dir, { withFileTypes: true });
171
+ } catch {
172
+ return;
173
+ }
174
+
175
+ const fileNames = entries.filter(e => !e.isDirectory()).map(e => e.name);
176
+ if (fileNames.includes('SKILL.md')) {
177
+ results.push(dir);
178
+ return; // Don't descend into a skill dir itself
179
+ }
180
+
181
+ for (const entry of entries) {
182
+ if (!entry.isDirectory()) continue;
183
+ // Skip hidden dirs, node_modules, .git
184
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
185
+ // safeJoin validates entry.name (from filesystem inside temp clone)
186
+ results.push(...findSkillDirs(safeJoin(dir, entry.name, 'directory entry')));
187
+ }
188
+ }
189
+
190
+ walk(rootDir);
191
+ return results;
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // SHA-256 hash for a skill directory
196
+ // ---------------------------------------------------------------------------
197
+
198
+ function hashSkillDir(skillDir) {
199
+ const hash = crypto.createHash('sha256');
200
+
201
+ function collectFiles(dir) {
202
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
203
+ a.name.localeCompare(b.name)
204
+ );
205
+ for (const entry of entries) {
206
+ // safeJoin validates entry.name (from filesystem inside temp clone)
207
+ const fullPath = safeJoin(dir, entry.name, 'hash entry');
208
+ if (entry.isDirectory()) {
209
+ collectFiles(fullPath);
210
+ } else {
211
+ hash.update(entry.name);
212
+ hash.update(fs.readFileSync(fullPath));
213
+ }
214
+ }
215
+ }
216
+
217
+ collectFiles(skillDir);
218
+ return 'sha256:' + hash.digest('hex');
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // addSkill
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Clone a GitHub repo (shallow), find SKILL.md directories, install them.
227
+ *
228
+ * @param {string} url
229
+ * @param {{ global?: boolean }} options
230
+ * @returns {{ installed: string[], source: string, commit: string }}
231
+ */
232
+ function addSkill(url, options = {}) {
233
+ const { owner, repo, branch, path: skillPath, cloneUrl } = parseGitHubUrl(url);
234
+ const scope = options.global ? 'global' : 'project';
235
+
236
+ // Target base directory (trusted — built from os.homedir() / constants)
237
+ const targetBase = scope === 'global'
238
+ ? path.join(os.homedir(), '.claude', 'skills')
239
+ : path.join(process.cwd(), '.claude', 'skills');
240
+
241
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'supermind-skill-'));
242
+
243
+ try {
244
+ // Shallow clone
245
+ try {
246
+ execFileSync('git', ['clone', '--depth', '1', '--branch', branch, cloneUrl, tempDir], {
247
+ stdio: 'pipe',
248
+ });
249
+ } catch (err) {
250
+ throw new Error(`Failed to clone ${cloneUrl} (branch: ${branch}): ${err.message}`);
251
+ }
252
+
253
+ // Get HEAD commit hash
254
+ let commit;
255
+ try {
256
+ commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: tempDir, stdio: 'pipe' })
257
+ .toString()
258
+ .trim();
259
+ } catch (err) {
260
+ throw new Error(`Failed to get commit hash: ${err.message}`);
261
+ }
262
+
263
+ // Navigate to sub-path if specified. skillPath comes from the URL, so
264
+ // validate it via safeJoin against the trusted tempDir base.
265
+ let searchRoot;
266
+ if (skillPath === '.' || skillPath === '') {
267
+ searchRoot = tempDir;
268
+ } else {
269
+ searchRoot = safeJoin(tempDir, skillPath, 'skill sub-path');
270
+ }
271
+
272
+ if (!fs.existsSync(searchRoot)) {
273
+ throw new Error(`Path '${skillPath}' not found in repo ${owner}/${repo}`);
274
+ }
275
+
276
+ // Find skill directories
277
+ const skillDirs = findSkillDirs(searchRoot);
278
+ if (skillDirs.length === 0) {
279
+ throw new Error(`No SKILL.md files found in ${owner}/${repo} at path '${skillPath}'`);
280
+ }
281
+
282
+ const lockData = readLockFile(scope);
283
+ const installed = [];
284
+
285
+ for (const skillDir of skillDirs) {
286
+ const skillDirName = path.basename(skillDir);
287
+ // skillDirName comes from path.basename of a path we already validated;
288
+ // safeJoin it into targetBase for the final write location.
289
+ const destDir = safeJoin(targetBase, skillDirName, 'skill directory name');
290
+
291
+ const contentHash = hashSkillDir(skillDir);
292
+
293
+ // Copy to target
294
+ copyDirRecursive(skillDir, destDir);
295
+
296
+ // Record in lock file
297
+ lockData.skills[skillDirName] = {
298
+ source: `github.com/${owner}/${repo}`,
299
+ path: skillDirName,
300
+ branch,
301
+ commit,
302
+ hash: contentHash,
303
+ installedAt: new Date().toISOString(),
304
+ scope,
305
+ };
306
+
307
+ installed.push(skillDirName);
308
+ }
309
+
310
+ writeLockFile(scope, lockData);
311
+
312
+ return { installed, source: `github.com/${owner}/${repo}`, commit };
313
+ } finally {
314
+ try {
315
+ fs.rmSync(tempDir, { recursive: true, force: true });
316
+ } catch {
317
+ // Best-effort cleanup
318
+ }
319
+ }
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // updateSkill
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /**
327
+ * Update a single installed vendor skill by name.
328
+ *
329
+ * @param {string} name — skill directory name as recorded in lock file
330
+ * @param {{ global?: boolean }} options
331
+ * @returns {{ updated: boolean, message?: string }}
332
+ */
333
+ function updateSkill(name, options = {}) {
334
+ // Search both scopes unless a specific one is requested
335
+ const scopes = options.global === true
336
+ ? ['global']
337
+ : options.global === false
338
+ ? ['project']
339
+ : ['global', 'project'];
340
+
341
+ let foundScope = null;
342
+ let entry = null;
343
+
344
+ for (const scope of scopes) {
345
+ const data = readLockFile(scope);
346
+ if (data.skills[name]) {
347
+ foundScope = scope;
348
+ entry = data.skills[name];
349
+ break;
350
+ }
351
+ }
352
+
353
+ if (!foundScope || !entry) {
354
+ throw new Error(`Skill '${name}' not found in lock file`);
355
+ }
356
+
357
+ const { branch, hash: oldHash } = entry;
358
+ const cloneUrl = `https://${entry.source}.git`;
359
+ const skillPath = entry.path || '.';
360
+
361
+ const targetBase = foundScope === 'global'
362
+ ? path.join(os.homedir(), '.claude', 'skills')
363
+ : path.join(process.cwd(), '.claude', 'skills');
364
+
365
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'supermind-skill-'));
366
+
367
+ try {
368
+ try {
369
+ execFileSync('git', ['clone', '--depth', '1', '--branch', branch, cloneUrl, tempDir], {
370
+ stdio: 'pipe',
371
+ });
372
+ } catch (err) {
373
+ throw new Error(`Failed to clone ${cloneUrl}: ${err.message}`);
374
+ }
375
+
376
+ let newCommit;
377
+ try {
378
+ newCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: tempDir, stdio: 'pipe' })
379
+ .toString()
380
+ .trim();
381
+ } catch (err) {
382
+ throw new Error(`Failed to get commit hash: ${err.message}`);
383
+ }
384
+
385
+ // skillPath comes from the lock file (previously validated on write); still
386
+ // validate again through safeJoin before using it in a path operation.
387
+ let searchRoot;
388
+ if (skillPath === '.' || skillPath === name) {
389
+ searchRoot = tempDir;
390
+ } else {
391
+ searchRoot = safeJoin(tempDir, skillPath, 'skill sub-path');
392
+ }
393
+
394
+ // Find the specific skill dir in the clone
395
+ const skillDirs = findSkillDirs(fs.existsSync(searchRoot) ? searchRoot : tempDir);
396
+ const skillDir = skillDirs.find(d => path.basename(d) === name) || skillDirs[0];
397
+
398
+ if (!skillDir) {
399
+ throw new Error(`Skill directory '${name}' not found in updated repo`);
400
+ }
401
+
402
+ const newHash = hashSkillDir(skillDir);
403
+
404
+ if (newHash === oldHash) {
405
+ return { updated: false, message: 'Already up to date' };
406
+ }
407
+
408
+ // name comes from the lock file key; validate via safeJoin before write.
409
+ const destDir = safeJoin(targetBase, name, 'skill name');
410
+ copyDirRecursive(skillDir, destDir);
411
+
412
+ // Update lock file
413
+ const lockData = readLockFile(foundScope);
414
+ lockData.skills[name] = {
415
+ ...lockData.skills[name],
416
+ commit: newCommit,
417
+ hash: newHash,
418
+ installedAt: new Date().toISOString(),
419
+ };
420
+ writeLockFile(foundScope, lockData);
421
+
422
+ return { updated: true };
423
+ } finally {
424
+ try {
425
+ fs.rmSync(tempDir, { recursive: true, force: true });
426
+ } catch {
427
+ // Best-effort cleanup
428
+ }
429
+ }
430
+ }
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // updateAll
434
+ // ---------------------------------------------------------------------------
435
+
436
+ /**
437
+ * Update all vendor skills recorded in both lock files.
438
+ *
439
+ * @param {{ global?: boolean }} options
440
+ * @returns {{ results: Array<{ name: string, updated: boolean, message?: string, error?: string }> }}
441
+ */
442
+ function updateAll(options = {}) {
443
+ const scopes = ['global', 'project'];
444
+ const results = [];
445
+
446
+ for (const scope of scopes) {
447
+ const lockData = readLockFile(scope);
448
+ for (const name of Object.keys(lockData.skills)) {
449
+ try {
450
+ const result = updateSkill(name, { global: scope === 'global' });
451
+ results.push({ name, ...result });
452
+ } catch (err) {
453
+ results.push({ name, updated: false, error: err.message });
454
+ }
455
+ }
456
+ }
457
+
458
+ return { results };
459
+ }
460
+
461
+ // ---------------------------------------------------------------------------
462
+ // listSkills
463
+ // ---------------------------------------------------------------------------
464
+
465
+ /**
466
+ * List all vendor skills from both lock files.
467
+ *
468
+ * @param {object} options (unused, reserved)
469
+ * @returns {Array<{ name: string, source: string, commit: string, installedAt: string, scope: string }>}
470
+ */
471
+ function listSkills(options = {}) {
472
+ const list = [];
473
+ const seen = new Set();
474
+
475
+ for (const scope of ['global', 'project']) {
476
+ const lockData = readLockFile(scope);
477
+ for (const [name, entry] of Object.entries(lockData.skills)) {
478
+ const key = `${scope}:${name}`;
479
+ if (seen.has(key)) continue;
480
+ seen.add(key);
481
+ list.push({
482
+ name,
483
+ source: entry.source,
484
+ commit: entry.commit,
485
+ installedAt: entry.installedAt,
486
+ scope: entry.scope || scope,
487
+ });
488
+ }
489
+ }
490
+
491
+ return list;
492
+ }
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // removeSkill
496
+ // ---------------------------------------------------------------------------
497
+
498
+ /**
499
+ * Remove a vendor skill by name.
500
+ *
501
+ * @param {string} name
502
+ * @param {{ global?: boolean }} options
503
+ */
504
+ function removeSkill(name, options = {}) {
505
+ const scopes = options.global === true
506
+ ? ['global']
507
+ : options.global === false
508
+ ? ['project']
509
+ : ['global', 'project'];
510
+
511
+ let removed = false;
512
+
513
+ for (const scope of scopes) {
514
+ const lockData = readLockFile(scope);
515
+ if (!lockData.skills[name]) continue;
516
+
517
+ const targetBase = scope === 'global'
518
+ ? path.join(os.homedir(), '.claude', 'skills')
519
+ : path.join(process.cwd(), '.claude', 'skills');
520
+
521
+ // name is user input; validate it through safeJoin before any fs operation.
522
+ const destDir = safeJoin(targetBase, name, 'skill name');
523
+ if (fs.existsSync(destDir)) {
524
+ fs.rmSync(destDir, { recursive: true, force: true });
525
+ }
526
+
527
+ delete lockData.skills[name];
528
+ writeLockFile(scope, lockData);
529
+ removed = true;
530
+ break;
531
+ }
532
+
533
+ if (!removed) {
534
+ throw new Error(`Skill '${name}' not found in lock file`);
535
+ }
536
+ }
537
+
538
+ // ---------------------------------------------------------------------------
539
+ // verifySkills
540
+ // ---------------------------------------------------------------------------
541
+
542
+ /**
543
+ * Verify that all locked skills have their directories on disk.
544
+ *
545
+ * @param {object} options (unused, reserved)
546
+ * @returns {{ valid: string[], missing: string[] }}
547
+ */
548
+ function verifySkills(options = {}) {
549
+ const valid = [];
550
+ const missing = [];
551
+
552
+ for (const scope of ['global', 'project']) {
553
+ const lockData = readLockFile(scope);
554
+ const targetBase = scope === 'global'
555
+ ? path.join(os.homedir(), '.claude', 'skills')
556
+ : path.join(process.cwd(), '.claude', 'skills');
557
+
558
+ for (const name of Object.keys(lockData.skills)) {
559
+ // name comes from the lock file; validate via safeJoin before fs.existsSync
560
+ let destDir;
561
+ try {
562
+ destDir = safeJoin(targetBase, name, 'skill name');
563
+ } catch {
564
+ missing.push(name);
565
+ continue;
566
+ }
567
+ if (fs.existsSync(destDir)) {
568
+ valid.push(name);
569
+ } else {
570
+ missing.push(name);
571
+ }
572
+ }
573
+ }
574
+
575
+ return { valid, missing };
576
+ }
577
+
578
+ // ---------------------------------------------------------------------------
579
+ // Exports
580
+ // ---------------------------------------------------------------------------
581
+
582
+ module.exports = {
583
+ parseGitHubUrl,
584
+ addSkill,
585
+ updateSkill,
586
+ updateAll,
587
+ listSkills,
588
+ removeSkill,
589
+ verifySkills,
590
+ readLockFile,
591
+ writeLockFile,
592
+ findSkillDirs,
593
+ copyDirRecursive,
594
+ };