skillsets 0.2.4 → 0.3.0
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.
- package/README.md +55 -55
- package/dist/commands/audit.d.ts +5 -1
- package/dist/commands/audit.js +58 -34
- package/dist/commands/init.js +11 -199
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +61 -1
- package/dist/commands/submit.js +1 -17
- package/dist/index.js +4 -2
- package/dist/lib/validate-mcp.d.ts +9 -0
- package/dist/lib/validate-mcp.js +294 -0
- package/dist/lib/versions.d.ts +5 -0
- package/dist/lib/versions.js +17 -0
- package/dist/types/index.d.ts +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
1
|
# Skillsets CLI
|
|
2
2
|
|
|
3
|
+
## Purpose
|
|
3
4
|
Command-line tool for discovering, installing, and contributing verified Claude Code skillsets.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Browse available skillsets
|
|
9
|
-
npx skillsets list
|
|
10
|
-
|
|
11
|
-
# Sort by popularity
|
|
12
|
-
npx skillsets list --sort downloads
|
|
13
|
-
|
|
14
|
-
# Search by keyword
|
|
15
|
-
npx skillsets search "sdlc"
|
|
16
|
-
|
|
17
|
-
# Install a skillset
|
|
18
|
-
npx skillsets install @supercollectible/The_Skillset
|
|
6
|
+
## Architecture
|
|
19
7
|
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `-f, --force` - Overwrite existing files
|
|
45
|
-
- `-b, --backup` - Backup existing files before install
|
|
46
|
-
|
|
47
|
-
## Live Stats
|
|
48
|
-
|
|
49
|
-
The CLI fetches live star and download counts from the API, so you always see current numbers (not stale build-time data).
|
|
50
|
-
|
|
51
|
-
## Development
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
npm install # Install dependencies
|
|
55
|
-
npm run build # Build TypeScript
|
|
56
|
-
npm test # Run tests
|
|
8
|
+
cli/
|
|
9
|
+
├── src/
|
|
10
|
+
│ ├── index.ts # CLI entry point
|
|
11
|
+
│ ├── commands/ # Command implementations
|
|
12
|
+
│ │ ├── list.ts
|
|
13
|
+
│ │ ├── search.ts
|
|
14
|
+
│ │ ├── install.ts
|
|
15
|
+
│ │ ├── init.ts
|
|
16
|
+
│ │ ├── audit.ts
|
|
17
|
+
│ │ └── submit.ts
|
|
18
|
+
│ ├── lib/ # Shared utilities
|
|
19
|
+
│ │ ├── api.ts
|
|
20
|
+
│ │ ├── checksum.ts
|
|
21
|
+
│ │ ├── constants.ts
|
|
22
|
+
│ │ ├── errors.ts
|
|
23
|
+
│ │ ├── filesystem.ts
|
|
24
|
+
│ │ ├── validate-mcp.ts
|
|
25
|
+
│ │ └── versions.ts
|
|
26
|
+
│ └── types/
|
|
27
|
+
│ └── index.ts
|
|
28
|
+
└── docs_cli/ # Documentation
|
|
29
|
+
├── ARC_cli.md
|
|
30
|
+
├── commands/
|
|
31
|
+
└── lib/
|
|
57
32
|
```
|
|
58
33
|
|
|
59
|
-
##
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
34
|
+
## Files
|
|
35
|
+
|
|
36
|
+
| File | Purpose | Documentation |
|
|
37
|
+
|------|---------|---------------|
|
|
38
|
+
| — | Architecture, data flow, key patterns | [ARC_cli.md](./docs_cli/ARC_cli.md) |
|
|
39
|
+
|
|
40
|
+
### Commands
|
|
41
|
+
| File | Purpose | Documentation |
|
|
42
|
+
|------|---------|---------------|
|
|
43
|
+
| `list.ts` | Browse all skillsets with live stats | [Docs](./docs_cli/commands/list.md) |
|
|
44
|
+
| `search.ts` | Fuzzy search by name, description, tags | [Docs](./docs_cli/commands/search.md) |
|
|
45
|
+
| `install.ts` | Install skillset via degit + MCP warning + verify checksums | [Docs](./docs_cli/commands/install.md) |
|
|
46
|
+
| `init.ts` | Scaffold new skillset for contribution | [Docs](./docs_cli/commands/init.md) |
|
|
47
|
+
| `audit.ts` | Validate skillset + MCP servers before submission | [Docs](./docs_cli/commands/audit.md) |
|
|
48
|
+
| `submit.ts` | Open PR to registry | [Docs](./docs_cli/commands/submit.md) |
|
|
49
|
+
|
|
50
|
+
### Lib
|
|
51
|
+
| File | Purpose | Documentation |
|
|
52
|
+
|------|---------|---------------|
|
|
53
|
+
| `api.ts` | API client for skillsets.cc | [Docs](./docs_cli/lib/api.md) |
|
|
54
|
+
| `checksum.ts` | SHA-256 verification | [Docs](./docs_cli/lib/checksum.md) |
|
|
55
|
+
| `constants.ts` | Shared constants | [Docs](./docs_cli/lib/constants.md) |
|
|
56
|
+
| `errors.ts` | Error types | [Docs](./docs_cli/lib/errors.md) |
|
|
57
|
+
| `filesystem.ts` | File utilities | [Docs](./docs_cli/lib/filesystem.md) |
|
|
58
|
+
| `versions.ts` | Semver comparison | [Docs](./docs_cli/lib/versions.md) |
|
|
59
|
+
| `validate-mcp.ts` | MCP server bidirectional validation | [Docs](./docs_cli/lib/validate-mcp.md) |
|
|
60
|
+
|
|
61
|
+
## Related Documentation
|
|
62
|
+
- [CLI Style Guide](../.claude/resources/cli_styleguide.md)
|
package/dist/commands/audit.d.ts
CHANGED
package/dist/commands/audit.js
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, openSync, readSync, closeSync } from 'fs';
|
|
4
4
|
import { join, relative } from 'path';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
6
|
import { fetchSkillsetMetadata } from '../lib/api.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* -1 if a < b, 0 if a == b, 1 if a > b
|
|
10
|
-
*/
|
|
11
|
-
function compareVersions(a, b) {
|
|
12
|
-
const partsA = a.split('.').map(Number);
|
|
13
|
-
const partsB = b.split('.').map(Number);
|
|
14
|
-
for (let i = 0; i < 3; i++) {
|
|
15
|
-
const numA = partsA[i] || 0;
|
|
16
|
-
const numB = partsB[i] || 0;
|
|
17
|
-
if (numA < numB)
|
|
18
|
-
return -1;
|
|
19
|
-
if (numA > numB)
|
|
20
|
-
return 1;
|
|
21
|
-
}
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
7
|
+
import { validateMcpServers } from '../lib/validate-mcp.js';
|
|
8
|
+
import { compareVersions } from '../lib/versions.js';
|
|
24
9
|
const MAX_FILE_SIZE = 1048576; // 1MB
|
|
25
10
|
const TEXT_EXTENSIONS = new Set([
|
|
26
11
|
'.md', '.txt', '.json', '.yaml', '.yml',
|
|
@@ -69,9 +54,9 @@ function isBinaryFile(filePath) {
|
|
|
69
54
|
// Check for null bytes in first 512 bytes
|
|
70
55
|
try {
|
|
71
56
|
const buffer = Buffer.alloc(512);
|
|
72
|
-
const fd =
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
const fd = openSync(filePath, 'r');
|
|
58
|
+
readSync(fd, buffer, 0, 512, 0);
|
|
59
|
+
closeSync(fd);
|
|
75
60
|
return buffer.includes(0);
|
|
76
61
|
}
|
|
77
62
|
catch {
|
|
@@ -152,8 +137,8 @@ function validateManifest(cwd) {
|
|
|
152
137
|
if (!data.author?.handle || !/^@[A-Za-z0-9_-]+$/.test(data.author.handle)) {
|
|
153
138
|
errors.push('author.handle must start with @ (e.g., @username)');
|
|
154
139
|
}
|
|
155
|
-
if (!data.verification?.
|
|
156
|
-
errors.push('verification.
|
|
140
|
+
if (!Array.isArray(data.verification?.production_links) || data.verification.production_links.length === 0) {
|
|
141
|
+
errors.push('verification.production_links must be an array with at least one entry');
|
|
157
142
|
}
|
|
158
143
|
if (!data.verification?.audit_report) {
|
|
159
144
|
errors.push('verification.audit_report is required');
|
|
@@ -181,7 +166,7 @@ function formatSize(bytes) {
|
|
|
181
166
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
182
167
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
183
168
|
}
|
|
184
|
-
function generateReport(results, cwd) {
|
|
169
|
+
function generateReport(results, cwd, enforceMcp = false) {
|
|
185
170
|
const timestamp = new Date().toISOString();
|
|
186
171
|
const allPassed = results.manifest.status === 'PASS' &&
|
|
187
172
|
results.requiredFiles.status === 'PASS' &&
|
|
@@ -189,7 +174,8 @@ function generateReport(results, cwd) {
|
|
|
189
174
|
results.fileSize.status !== 'FAIL' &&
|
|
190
175
|
results.secrets.status === 'PASS' &&
|
|
191
176
|
results.readmeLinks.status === 'PASS' &&
|
|
192
|
-
results.versionCheck.status === 'PASS'
|
|
177
|
+
results.versionCheck.status === 'PASS' &&
|
|
178
|
+
(enforceMcp ? results.mcpServers.status === 'PASS' : true);
|
|
193
179
|
const submissionType = results.isUpdate
|
|
194
180
|
? `Update (${results.existingVersion} → ${results.skillsetVersion})`
|
|
195
181
|
: 'New submission';
|
|
@@ -221,6 +207,7 @@ function generateReport(results, cwd) {
|
|
|
221
207
|
| Secret Detection | ${statusIcon(results.secrets.status)} | ${results.secrets.details} |
|
|
222
208
|
| README Links | ${statusIcon(results.readmeLinks.status)} | ${results.readmeLinks.details} |
|
|
223
209
|
| Version Check | ${statusIcon(results.versionCheck.status)} | ${results.versionCheck.details} |
|
|
210
|
+
| MCP Servers | ${statusIcon(results.mcpServers.status)} | ${results.mcpServers.details} |
|
|
224
211
|
|
|
225
212
|
---
|
|
226
213
|
|
|
@@ -265,6 +252,10 @@ ${results.readmeLinks.findings || 'All links use valid GitHub URLs.'}
|
|
|
265
252
|
|
|
266
253
|
${results.relativeLinks.length > 0 ? '**Relative Links Found:**\n' + results.relativeLinks.map(l => `- Line ${l.line}: ${l.link}`).join('\n') : ''}
|
|
267
254
|
|
|
255
|
+
### 8. MCP Server Validation
|
|
256
|
+
|
|
257
|
+
${results.mcpServers.findings || 'MCP server declarations are consistent between content and manifest.'}
|
|
258
|
+
|
|
268
259
|
---
|
|
269
260
|
|
|
270
261
|
## File Inventory
|
|
@@ -303,7 +294,7 @@ ${allPassed
|
|
|
303
294
|
`;
|
|
304
295
|
return report;
|
|
305
296
|
}
|
|
306
|
-
export async function audit() {
|
|
297
|
+
export async function audit(options = {}) {
|
|
307
298
|
const spinner = ora('Auditing skillset...').start();
|
|
308
299
|
const cwd = process.cwd();
|
|
309
300
|
const results = {
|
|
@@ -315,6 +306,7 @@ export async function audit() {
|
|
|
315
306
|
secrets: { status: 'PASS', details: '' },
|
|
316
307
|
versionCheck: { status: 'PASS', details: '' },
|
|
317
308
|
readmeLinks: { status: 'PASS', details: '' },
|
|
309
|
+
mcpServers: { status: 'PASS', details: '' },
|
|
318
310
|
isUpdate: false,
|
|
319
311
|
files: [],
|
|
320
312
|
largeFiles: [],
|
|
@@ -474,11 +466,34 @@ export async function audit() {
|
|
|
474
466
|
else {
|
|
475
467
|
results.versionCheck = { status: 'PASS', details: 'Skipped (no manifest)' };
|
|
476
468
|
}
|
|
469
|
+
// 9. MCP server validation
|
|
470
|
+
spinner.text = 'Validating MCP servers...';
|
|
471
|
+
const mcpResult = validateMcpServers(cwd);
|
|
472
|
+
if (mcpResult.valid) {
|
|
473
|
+
results.mcpServers = { status: 'PASS', details: 'MCP declarations valid' };
|
|
474
|
+
}
|
|
475
|
+
else if (options.check) {
|
|
476
|
+
results.mcpServers = {
|
|
477
|
+
status: 'FAIL',
|
|
478
|
+
details: `${mcpResult.errors.length} MCP error(s)`,
|
|
479
|
+
findings: mcpResult.errors.map(e => `- ${e}`).join('\n'),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
results.mcpServers = {
|
|
484
|
+
status: 'WARNING',
|
|
485
|
+
details: 'Pending qualitative review',
|
|
486
|
+
findings: 'MCP servers detected in content. The `/audit-skill` will populate `skillset.yaml` and CI will re-validate.\n\n' +
|
|
487
|
+
mcpResult.errors.map(e => `- ${e}`).join('\n'),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
477
490
|
// Generate report
|
|
478
491
|
spinner.text = 'Generating audit report...';
|
|
479
|
-
const report = generateReport(results, cwd);
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
const report = generateReport(results, cwd, options.check);
|
|
493
|
+
if (!options.check) {
|
|
494
|
+
writeFileSync(join(cwd, 'AUDIT_REPORT.md'), report);
|
|
495
|
+
}
|
|
496
|
+
spinner.succeed(options.check ? 'Validation complete' : 'Audit complete');
|
|
482
497
|
// Summary
|
|
483
498
|
const allPassed = results.manifest.status === 'PASS' &&
|
|
484
499
|
results.requiredFiles.status === 'PASS' &&
|
|
@@ -486,7 +501,8 @@ export async function audit() {
|
|
|
486
501
|
results.fileSize.status !== 'FAIL' &&
|
|
487
502
|
results.secrets.status === 'PASS' &&
|
|
488
503
|
results.readmeLinks.status === 'PASS' &&
|
|
489
|
-
results.versionCheck.status === 'PASS'
|
|
504
|
+
results.versionCheck.status === 'PASS' &&
|
|
505
|
+
(options.check ? results.mcpServers.status === 'PASS' : true);
|
|
490
506
|
console.log('\n' + chalk.bold('Audit Summary:'));
|
|
491
507
|
console.log('');
|
|
492
508
|
const icon = (status) => {
|
|
@@ -504,15 +520,23 @@ export async function audit() {
|
|
|
504
520
|
console.log(` ${icon(results.secrets.status)} Secrets: ${results.secrets.details}`);
|
|
505
521
|
console.log(` ${icon(results.readmeLinks.status)} README Links: ${results.readmeLinks.details}`);
|
|
506
522
|
console.log(` ${icon(results.versionCheck.status)} Version: ${results.versionCheck.details}`);
|
|
523
|
+
console.log(` ${icon(results.mcpServers.status)} MCP Servers: ${results.mcpServers.details}`);
|
|
507
524
|
console.log('');
|
|
508
525
|
if (allPassed) {
|
|
509
526
|
console.log(chalk.green('✓ READY FOR SUBMISSION'));
|
|
510
|
-
|
|
511
|
-
|
|
527
|
+
if (!options.check) {
|
|
528
|
+
console.log(chalk.gray('\nGenerated: AUDIT_REPORT.md'));
|
|
529
|
+
console.log(chalk.cyan('\nNext: npx skillsets submit'));
|
|
530
|
+
}
|
|
512
531
|
}
|
|
513
532
|
else {
|
|
514
533
|
console.log(chalk.red('✗ NOT READY - Fix issues above'));
|
|
515
|
-
|
|
516
|
-
|
|
534
|
+
if (!options.check) {
|
|
535
|
+
console.log(chalk.gray('\nGenerated: AUDIT_REPORT.md'));
|
|
536
|
+
console.log(chalk.cyan('\nRe-run after fixes: npx skillsets audit'));
|
|
537
|
+
}
|
|
538
|
+
if (options.check) {
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
517
541
|
}
|
|
518
542
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import ora from 'ora';
|
|
|
3
3
|
import { input, confirm, checkbox } from '@inquirer/prompts';
|
|
4
4
|
import { existsSync, mkdirSync, copyFileSync, readdirSync, writeFileSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import degit from 'degit';
|
|
6
7
|
const SKILLSET_YAML_TEMPLATE = `schema_version: "1.0"
|
|
7
8
|
|
|
8
9
|
# Identity
|
|
@@ -16,7 +17,8 @@ author:
|
|
|
16
17
|
|
|
17
18
|
# Verification
|
|
18
19
|
verification:
|
|
19
|
-
|
|
20
|
+
production_links:
|
|
21
|
+
- url: "{{PRODUCTION_URL}}"
|
|
20
22
|
production_proof: "./PROOF.md"
|
|
21
23
|
audit_report: "./AUDIT_REPORT.md"
|
|
22
24
|
|
|
@@ -75,200 +77,6 @@ This skillset has been verified in production.
|
|
|
75
77
|
|
|
76
78
|
[List projects or products built using this skillset]
|
|
77
79
|
`;
|
|
78
|
-
const AUDIT_SKILL_MD = `---
|
|
79
|
-
name: audit-skill
|
|
80
|
-
description: Qualitative review of skillset content against Claude Code best practices. Evaluates all primitives (skills, agents, hooks, MCP, CLAUDE.md) for proper frontmatter, descriptions, and structure. Appends analysis to AUDIT_REPORT.md.
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
# Skillset Qualitative Audit
|
|
84
|
-
|
|
85
|
-
## Task
|
|
86
|
-
|
|
87
|
-
1. Verify \`AUDIT_REPORT.md\` shows "READY FOR SUBMISSION"
|
|
88
|
-
2. Identify all primitives in \`content/\`:
|
|
89
|
-
- Skills: \`**/SKILL.md\`
|
|
90
|
-
- Agents: \`**/AGENT.md\` or \`**/*.agent.md\`
|
|
91
|
-
- Hooks: \`**/hooks.json\`
|
|
92
|
-
- MCP: \`**/.mcp.json\` or \`**/mcp.json\`
|
|
93
|
-
- CLAUDE.md: \`CLAUDE.md\` or \`.claude/settings.json\`
|
|
94
|
-
3. Evaluate each against [CRITERIA.md](CRITERIA.md)
|
|
95
|
-
4. Append findings to \`AUDIT_REPORT.md\`
|
|
96
|
-
|
|
97
|
-
## Per-Primitive Evaluation
|
|
98
|
-
|
|
99
|
-
### Skills
|
|
100
|
-
- Frontmatter has \`name\` and \`description\`
|
|
101
|
-
- Description includes trigger phrases ("Use when...")
|
|
102
|
-
- Body under 500 lines
|
|
103
|
-
- \`allowed-tools\` if restricting access
|
|
104
|
-
- \`disable-model-invocation\` for side-effect commands
|
|
105
|
-
|
|
106
|
-
### Agents
|
|
107
|
-
- Description has \`<example>\` blocks
|
|
108
|
-
- System prompt has role, responsibilities, process, output format
|
|
109
|
-
- \`tools\` array if restricting access
|
|
110
|
-
|
|
111
|
-
### Hooks
|
|
112
|
-
- Valid JSON structure
|
|
113
|
-
- Matchers are specific (not just \`.*\`)
|
|
114
|
-
- Reasonable timeouts
|
|
115
|
-
- Prompts are actionable
|
|
116
|
-
|
|
117
|
-
### MCP
|
|
118
|
-
- Uses \`\${CLAUDE_PLUGIN_ROOT}\` for paths
|
|
119
|
-
- Env vars use \`\${VAR}\` syntax
|
|
120
|
-
- No hardcoded secrets
|
|
121
|
-
|
|
122
|
-
### CLAUDE.md
|
|
123
|
-
- Under 300 lines (check line count)
|
|
124
|
-
- Has WHAT/WHY/HOW sections
|
|
125
|
-
- Uses \`file:line\` pointers, not code snippets
|
|
126
|
-
- Progressive disclosure for large content
|
|
127
|
-
|
|
128
|
-
## Output
|
|
129
|
-
|
|
130
|
-
Append to \`AUDIT_REPORT.md\`:
|
|
131
|
-
|
|
132
|
-
\`\`\`markdown
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Qualitative Review
|
|
136
|
-
|
|
137
|
-
**Reviewed by:** Claude (Opus)
|
|
138
|
-
**Date:** [ISO timestamp]
|
|
139
|
-
|
|
140
|
-
### Primitives Found
|
|
141
|
-
|
|
142
|
-
| Type | Count | Files |
|
|
143
|
-
|------|-------|-------|
|
|
144
|
-
| Skills | N | [list] |
|
|
145
|
-
| Agents | N | [list] |
|
|
146
|
-
| Hooks | N | [list] |
|
|
147
|
-
| MCP | N | [list] |
|
|
148
|
-
| CLAUDE.md | Y/N | [path] |
|
|
149
|
-
|
|
150
|
-
### Issues
|
|
151
|
-
|
|
152
|
-
[List each issue with file:line and specific fix needed]
|
|
153
|
-
|
|
154
|
-
### Verdict
|
|
155
|
-
|
|
156
|
-
**[APPROVED / NEEDS REVISION]**
|
|
157
|
-
|
|
158
|
-
[If needs revision: prioritized list of must-fix items]
|
|
159
|
-
\`\`\`
|
|
160
|
-
`;
|
|
161
|
-
const AUDIT_CRITERIA_MD = `# Evaluation Criteria
|
|
162
|
-
|
|
163
|
-
Rubric for qualitative skillset review. Each primitive type has specific requirements.
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
## Skills (SKILL.md)
|
|
168
|
-
|
|
169
|
-
Skills and slash commands are now unified. File at \`.claude/skills/[name]/SKILL.md\` creates \`/name\`.
|
|
170
|
-
|
|
171
|
-
### Frontmatter Requirements
|
|
172
|
-
|
|
173
|
-
| Field | Required | Notes |
|
|
174
|
-
|-------|----------|-------|
|
|
175
|
-
| \`name\` | Yes | Becomes the \`/slash-command\`, lowercase with hyphens |
|
|
176
|
-
| \`description\` | Yes | **Critical for discoverability** - Claude uses this to decide when to load |
|
|
177
|
-
| \`version\` | No | Semver for tracking |
|
|
178
|
-
| \`allowed-tools\` | No | Restricts tool access (e.g., \`Read, Write, Bash(git:*)\`) |
|
|
179
|
-
| \`model\` | No | \`sonnet\`, \`opus\`, or \`haiku\` |
|
|
180
|
-
| \`disable-model-invocation\` | No | \`true\` = only user can invoke (for side-effect commands) |
|
|
181
|
-
| \`user-invocable\` | No | \`false\` = only Claude can invoke (background knowledge) |
|
|
182
|
-
|
|
183
|
-
### Description Quality
|
|
184
|
-
|
|
185
|
-
**GOOD:** Includes trigger phrases ("Use when reviewing PRs, checking vulnerabilities...")
|
|
186
|
-
**POOR:** Vague ("Helps with code review")
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Agents (AGENT.md)
|
|
191
|
-
|
|
192
|
-
### Frontmatter Requirements
|
|
193
|
-
|
|
194
|
-
| Field | Required | Notes |
|
|
195
|
-
|-------|----------|-------|
|
|
196
|
-
| \`name\` | Yes | Agent identifier |
|
|
197
|
-
| \`description\` | Yes | **Must include \`<example>\` blocks** for reliable triggering |
|
|
198
|
-
| \`model\` | No | \`inherit\`, \`sonnet\`, \`opus\`, \`haiku\` |
|
|
199
|
-
| \`color\` | No | UI color hint |
|
|
200
|
-
| \`tools\` | No | Array of allowed tools |
|
|
201
|
-
|
|
202
|
-
### System Prompt (Body)
|
|
203
|
-
|
|
204
|
-
- Clear role definition ("You are...")
|
|
205
|
-
- Core responsibilities numbered
|
|
206
|
-
- Process/workflow steps
|
|
207
|
-
- Expected output format
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Hooks (hooks.json)
|
|
212
|
-
|
|
213
|
-
### Event Types
|
|
214
|
-
|
|
215
|
-
| Event | When | Use For |
|
|
216
|
-
|-------|------|---------|
|
|
217
|
-
| \`PreToolUse\` | Before tool executes | Validation, security checks |
|
|
218
|
-
| \`PostToolUse\` | After tool completes | Feedback, logging |
|
|
219
|
-
| \`Stop\` | Task completion | Quality gates, notifications |
|
|
220
|
-
| \`SessionStart\` | Session begins | Context loading, env setup |
|
|
221
|
-
|
|
222
|
-
### Quality Checks
|
|
223
|
-
|
|
224
|
-
- Matchers are specific (avoid \`.*\` unless intentional)
|
|
225
|
-
- Timeouts are reasonable
|
|
226
|
-
- Prompts are concise and actionable
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## MCP Servers (.mcp.json)
|
|
231
|
-
|
|
232
|
-
### Quality Checks
|
|
233
|
-
|
|
234
|
-
- Uses \`\${CLAUDE_PLUGIN_ROOT}\` for paths
|
|
235
|
-
- Environment variables use \`\${VAR}\` syntax
|
|
236
|
-
- Sensitive values reference env vars, not hardcoded
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## CLAUDE.md
|
|
241
|
-
|
|
242
|
-
### Critical Constraints
|
|
243
|
-
|
|
244
|
-
- **Under 300 lines** (ideally <60)
|
|
245
|
-
- LLMs follow ~150-200 instructions; Claude Code system prompt uses ~50
|
|
246
|
-
|
|
247
|
-
### Required Content (WHAT, WHY, HOW)
|
|
248
|
-
|
|
249
|
-
- **WHAT**: Tech stack, project structure, codebase map
|
|
250
|
-
- **WHY**: Project purpose, component functions
|
|
251
|
-
- **HOW**: Dev workflows, tools, testing, verification
|
|
252
|
-
|
|
253
|
-
### What to Avoid
|
|
254
|
-
|
|
255
|
-
- Task-specific instructions
|
|
256
|
-
- Code style rules (use linters + hooks)
|
|
257
|
-
- Code snippets (use \`file:line\` pointers)
|
|
258
|
-
- Hardcoded dates/versions
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Verdict Rules
|
|
263
|
-
|
|
264
|
-
- **APPROVED**: All primitives meet requirements, minor issues only
|
|
265
|
-
- **NEEDS REVISION**: Missing required fields, poor descriptions, oversized files
|
|
266
|
-
|
|
267
|
-
Priority:
|
|
268
|
-
1. Missing/poor descriptions (affects discoverability)
|
|
269
|
-
2. Oversized CLAUDE.md (degrades all instructions)
|
|
270
|
-
3. Missing agent examples (unreliable triggering)
|
|
271
|
-
`;
|
|
272
80
|
function copyDirRecursive(src, dest) {
|
|
273
81
|
if (!existsSync(dest)) {
|
|
274
82
|
mkdirSync(dest, { recursive: true });
|
|
@@ -419,11 +227,15 @@ export async function init(options) {
|
|
|
419
227
|
// Generate PROOF.md
|
|
420
228
|
const proof = PROOF_TEMPLATE.replace('{{PRODUCTION_URL}}', productionUrl);
|
|
421
229
|
writeFileSync(join(cwd, 'PROOF.md'), proof);
|
|
422
|
-
// Install audit-skill
|
|
230
|
+
// Install audit-skill from registry
|
|
231
|
+
spinner.text = 'Fetching audit-skill...';
|
|
423
232
|
const skillDir = join(cwd, '.claude', 'skills', 'audit-skill');
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
233
|
+
const emitter = degit('skillsets-cc/main/tools/audit-skill', {
|
|
234
|
+
cache: false,
|
|
235
|
+
force: true,
|
|
236
|
+
verbose: false,
|
|
237
|
+
});
|
|
238
|
+
await emitter.clone(skillDir);
|
|
427
239
|
spinner.succeed('Skillset structure created');
|
|
428
240
|
// Summary
|
|
429
241
|
console.log(chalk.green('\n✓ Initialized skillset submission:\n'));
|
package/dist/commands/install.js
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
import degit from 'degit';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
+
import { confirm } from '@inquirer/prompts';
|
|
4
5
|
import { detectConflicts, backupFiles } from '../lib/filesystem.js';
|
|
5
6
|
import { verifyChecksums } from '../lib/checksum.js';
|
|
7
|
+
import { fetchSkillsetMetadata } from '../lib/api.js';
|
|
6
8
|
import { REGISTRY_REPO, DOWNLOADS_URL } from '../lib/constants.js';
|
|
9
|
+
function formatMcpWarning(mcpServers, skillsetId) {
|
|
10
|
+
let output = chalk.yellow('\n⚠ This skillset includes MCP servers:\n');
|
|
11
|
+
const nativeServers = mcpServers.filter(s => s.type !== 'docker');
|
|
12
|
+
const dockerServers = mcpServers.filter(s => s.type === 'docker');
|
|
13
|
+
if (nativeServers.length > 0) {
|
|
14
|
+
output += chalk.white('\n Claude Code managed:\n');
|
|
15
|
+
for (const server of nativeServers) {
|
|
16
|
+
const detail = server.type === 'stdio'
|
|
17
|
+
? `(${server.command} ${(server.args || []).join(' ')})`
|
|
18
|
+
: `(${server.url})`;
|
|
19
|
+
output += ` ${server.type}: ${server.name} ${detail}\n`;
|
|
20
|
+
output += chalk.gray(` Reputation: ${server.mcp_reputation} (as of ${server.researched_at})\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (dockerServers.length > 0) {
|
|
24
|
+
output += chalk.white('\n Docker hosted:\n');
|
|
25
|
+
for (const server of dockerServers) {
|
|
26
|
+
output += ` image: ${server.image}\n`;
|
|
27
|
+
output += chalk.gray(` Reputation: ${server.mcp_reputation} (as of ${server.researched_at})\n`);
|
|
28
|
+
if (server.servers && server.servers.length > 0) {
|
|
29
|
+
output += ` Runs: ${server.servers.map(s => s.name).join(', ')}\n`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
output += chalk.gray('\n MCP packages are fetched at runtime and may have changed since audit.\n');
|
|
34
|
+
output += chalk.cyan(`\n Review before installing:\n https://github.com/skillsets-cc/main/tree/main/skillsets/${skillsetId}/content\n`);
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
7
37
|
export async function install(skillsetId, options) {
|
|
8
38
|
const spinner = ora(`Installing ${skillsetId}...`).start();
|
|
9
39
|
// Check for conflicts
|
|
@@ -22,10 +52,40 @@ export async function install(skillsetId, options) {
|
|
|
22
52
|
spinner.text = 'Backing up existing files...';
|
|
23
53
|
await backupFiles(conflicts, process.cwd());
|
|
24
54
|
}
|
|
55
|
+
// Fetch metadata and check for MCP servers BEFORE degit
|
|
56
|
+
spinner.text = 'Fetching skillset metadata...';
|
|
57
|
+
try {
|
|
58
|
+
const metadata = await fetchSkillsetMetadata(skillsetId);
|
|
59
|
+
if (metadata?.mcp_servers && metadata.mcp_servers.length > 0) {
|
|
60
|
+
spinner.stop();
|
|
61
|
+
// Non-interactive check
|
|
62
|
+
if (!process.stdin.isTTY && !options.acceptMcp) {
|
|
63
|
+
console.log(chalk.red('This skillset includes MCP servers. Use --accept-mcp to install in non-interactive environments.'));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!options.acceptMcp) {
|
|
68
|
+
console.log(formatMcpWarning(metadata.mcp_servers, skillsetId));
|
|
69
|
+
const accepted = await confirm({
|
|
70
|
+
message: 'Install MCP servers?',
|
|
71
|
+
default: false,
|
|
72
|
+
});
|
|
73
|
+
if (!accepted) {
|
|
74
|
+
console.log(chalk.gray('\nInstallation cancelled.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
spinner.start('Downloading skillset...');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// If metadata fetch fails, continue without MCP check
|
|
83
|
+
// (registry might be down, don't block install)
|
|
84
|
+
}
|
|
25
85
|
// Install using degit (extract content/ subdirectory)
|
|
26
86
|
spinner.text = 'Downloading skillset...';
|
|
27
87
|
const emitter = degit(`${REGISTRY_REPO}/skillsets/${skillsetId}/content`, {
|
|
28
|
-
cache:
|
|
88
|
+
cache: false,
|
|
29
89
|
force: true,
|
|
30
90
|
verbose: false,
|
|
31
91
|
});
|
package/dist/commands/submit.js
CHANGED
|
@@ -6,24 +6,8 @@ import { join } from 'path';
|
|
|
6
6
|
import yaml from 'js-yaml';
|
|
7
7
|
import { tmpdir } from 'os';
|
|
8
8
|
import { fetchSkillsetMetadata } from '../lib/api.js';
|
|
9
|
+
import { compareVersions } from '../lib/versions.js';
|
|
9
10
|
const REGISTRY_REPO = 'skillsets-cc/main';
|
|
10
|
-
/**
|
|
11
|
-
* Compare semver versions. Returns:
|
|
12
|
-
* -1 if a < b, 0 if a == b, 1 if a > b
|
|
13
|
-
*/
|
|
14
|
-
function compareVersions(a, b) {
|
|
15
|
-
const partsA = a.split('.').map(Number);
|
|
16
|
-
const partsB = b.split('.').map(Number);
|
|
17
|
-
for (let i = 0; i < 3; i++) {
|
|
18
|
-
const numA = partsA[i] || 0;
|
|
19
|
-
const numB = partsB[i] || 0;
|
|
20
|
-
if (numA < numB)
|
|
21
|
-
return -1;
|
|
22
|
-
if (numA > numB)
|
|
23
|
-
return 1;
|
|
24
|
-
}
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
11
|
const REGISTRY_URL = `https://github.com/${REGISTRY_REPO}`;
|
|
28
12
|
function checkGhCli() {
|
|
29
13
|
try {
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,7 @@ program
|
|
|
46
46
|
.argument('<skillsetId>', 'Skillset ID (e.g., @user/skillset-name)')
|
|
47
47
|
.option('-f, --force', 'Overwrite existing files')
|
|
48
48
|
.option('-b, --backup', 'Backup existing files before install')
|
|
49
|
+
.option('--accept-mcp', 'Accept MCP servers without prompting (required for non-interactive environments)')
|
|
49
50
|
.action(async (skillsetId, options) => {
|
|
50
51
|
try {
|
|
51
52
|
await install(skillsetId, options);
|
|
@@ -70,9 +71,10 @@ program
|
|
|
70
71
|
program
|
|
71
72
|
.command('audit')
|
|
72
73
|
.description('Validate skillset and generate audit report')
|
|
73
|
-
.
|
|
74
|
+
.option('--check', 'Validate without writing AUDIT_REPORT.md (used by CI)')
|
|
75
|
+
.action(async (options) => {
|
|
74
76
|
try {
|
|
75
|
-
await audit();
|
|
77
|
+
await audit(options);
|
|
76
78
|
}
|
|
77
79
|
catch (error) {
|
|
78
80
|
handleError(error);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface McpValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
errors: string[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Validates MCP server declarations between content files and skillset.yaml.
|
|
7
|
+
* Bidirectional: content→manifest and manifest→content.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateMcpServers(skillsetDir: string): McpValidationResult;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
/**
|
|
5
|
+
* Validates MCP server declarations between content files and skillset.yaml.
|
|
6
|
+
* Bidirectional: content→manifest and manifest→content.
|
|
7
|
+
*/
|
|
8
|
+
export function validateMcpServers(skillsetDir) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
// 1. Collect from content
|
|
11
|
+
const contentServers = collectContentServers(skillsetDir, errors);
|
|
12
|
+
// 2. Collect from manifest
|
|
13
|
+
const manifestServers = collectManifestServers(skillsetDir, errors);
|
|
14
|
+
// If we hit parse errors, return early
|
|
15
|
+
if (errors.length > 0) {
|
|
16
|
+
return { valid: false, errors };
|
|
17
|
+
}
|
|
18
|
+
// 3. No MCPs anywhere = pass
|
|
19
|
+
if (contentServers.length === 0 && manifestServers.length === 0) {
|
|
20
|
+
return { valid: true, errors: [] };
|
|
21
|
+
}
|
|
22
|
+
// 4. Content→manifest check
|
|
23
|
+
for (const cs of contentServers) {
|
|
24
|
+
const match = findManifestMatch(cs, manifestServers);
|
|
25
|
+
if (!match) {
|
|
26
|
+
errors.push(`MCP server '${cs.name}' found in content but not declared in skillset.yaml mcp_servers`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 5. Manifest→content check
|
|
30
|
+
for (const ms of manifestServers) {
|
|
31
|
+
if (ms.type === 'docker') {
|
|
32
|
+
// Check Docker inner servers
|
|
33
|
+
for (const inner of ms.servers || []) {
|
|
34
|
+
const match = contentServers.find(cs => cs.source === 'docker' && cs.name === inner.name);
|
|
35
|
+
if (!match) {
|
|
36
|
+
errors.push(`Docker inner server '${inner.name}' declared in manifest but not found in content docker config`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Check Docker image exists in compose
|
|
40
|
+
validateDockerImage(skillsetDir, ms.image, errors);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const match = contentServers.find(cs => cs.source === 'native' && cs.name === ms.name);
|
|
44
|
+
if (!match) {
|
|
45
|
+
errors.push(`MCP server '${ms.name}' declared in skillset.yaml but not found in content`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { valid: errors.length === 0, errors };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collect MCP servers from content files (.mcp.json, .claude/settings.json, docker configs)
|
|
53
|
+
*/
|
|
54
|
+
function collectContentServers(skillsetDir, errors) {
|
|
55
|
+
const servers = [];
|
|
56
|
+
const contentDir = join(skillsetDir, 'content');
|
|
57
|
+
if (!existsSync(contentDir)) {
|
|
58
|
+
return servers;
|
|
59
|
+
}
|
|
60
|
+
// 1. Check .mcp.json
|
|
61
|
+
const mcpJsonPath = join(contentDir, '.mcp.json');
|
|
62
|
+
if (existsSync(mcpJsonPath)) {
|
|
63
|
+
try {
|
|
64
|
+
const content = readFileSync(mcpJsonPath, 'utf-8');
|
|
65
|
+
const data = JSON.parse(content);
|
|
66
|
+
if (data.mcpServers && typeof data.mcpServers === 'object') {
|
|
67
|
+
for (const [name, config] of Object.entries(data.mcpServers)) {
|
|
68
|
+
servers.push({
|
|
69
|
+
name,
|
|
70
|
+
source: 'native',
|
|
71
|
+
command: config.command,
|
|
72
|
+
args: config.args,
|
|
73
|
+
url: config.url,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
errors.push(`Failed to parse .mcp.json: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 2. Check .claude/settings.json
|
|
83
|
+
const settingsPath = join(contentDir, '.claude', 'settings.json');
|
|
84
|
+
if (existsSync(settingsPath)) {
|
|
85
|
+
try {
|
|
86
|
+
const content = readFileSync(settingsPath, 'utf-8');
|
|
87
|
+
const data = JSON.parse(content);
|
|
88
|
+
if (data.mcpServers && typeof data.mcpServers === 'object') {
|
|
89
|
+
for (const [name, config] of Object.entries(data.mcpServers)) {
|
|
90
|
+
// Avoid duplicates (same name might be in both files)
|
|
91
|
+
if (!servers.some(s => s.name === name && s.source === 'native')) {
|
|
92
|
+
servers.push({
|
|
93
|
+
name,
|
|
94
|
+
source: 'native',
|
|
95
|
+
command: config.command,
|
|
96
|
+
args: config.args,
|
|
97
|
+
url: config.url,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
errors.push(`Failed to parse .claude/settings.json: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 3. Check .claude/settings.local.json
|
|
108
|
+
const settingsLocalPath = join(contentDir, '.claude', 'settings.local.json');
|
|
109
|
+
if (existsSync(settingsLocalPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const content = readFileSync(settingsLocalPath, 'utf-8');
|
|
112
|
+
const data = JSON.parse(content);
|
|
113
|
+
if (data.mcpServers && typeof data.mcpServers === 'object') {
|
|
114
|
+
for (const [name, config] of Object.entries(data.mcpServers)) {
|
|
115
|
+
if (!servers.some(s => s.name === name && s.source === 'native')) {
|
|
116
|
+
servers.push({
|
|
117
|
+
name,
|
|
118
|
+
source: 'native',
|
|
119
|
+
command: config.command,
|
|
120
|
+
args: config.args,
|
|
121
|
+
url: config.url,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
errors.push(`Failed to parse .claude/settings.local.json: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// 4. Check docker/**/*.yaml and docker/**/*.yml for mcp_servers key
|
|
132
|
+
const dockerDir = join(contentDir, 'docker');
|
|
133
|
+
if (existsSync(dockerDir)) {
|
|
134
|
+
const yamlFiles = findYamlFiles(dockerDir);
|
|
135
|
+
for (const yamlPath of yamlFiles) {
|
|
136
|
+
try {
|
|
137
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
138
|
+
const data = yaml.load(content);
|
|
139
|
+
if (data.mcp_servers && typeof data.mcp_servers === 'object') {
|
|
140
|
+
for (const [name, config] of Object.entries(data.mcp_servers)) {
|
|
141
|
+
// Avoid duplicates across multiple yaml files
|
|
142
|
+
if (!servers.some(s => s.name === name && s.source === 'docker')) {
|
|
143
|
+
servers.push({
|
|
144
|
+
name,
|
|
145
|
+
source: 'docker',
|
|
146
|
+
command: config.command,
|
|
147
|
+
args: config.args,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
errors.push(`Failed to parse ${yamlPath}: ${error.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return servers;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Collect MCP servers from skillset.yaml manifest
|
|
162
|
+
*/
|
|
163
|
+
function collectManifestServers(skillsetDir, errors) {
|
|
164
|
+
const manifestPath = join(skillsetDir, 'skillset.yaml');
|
|
165
|
+
if (!existsSync(manifestPath)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const content = readFileSync(manifestPath, 'utf-8');
|
|
170
|
+
const data = yaml.load(content);
|
|
171
|
+
if (!data.mcp_servers || !Array.isArray(data.mcp_servers)) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
return data.mcp_servers;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
errors.push(`Failed to parse skillset.yaml: ${error.message}`);
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Find a matching manifest entry for a content server
|
|
183
|
+
*/
|
|
184
|
+
function findManifestMatch(contentServer, manifestServers) {
|
|
185
|
+
for (const ms of manifestServers) {
|
|
186
|
+
if (contentServer.source === 'native') {
|
|
187
|
+
// Native server: match by name and verify command/url
|
|
188
|
+
if (ms.name !== contentServer.name || ms.type === 'docker') {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// For stdio type, check command and args
|
|
192
|
+
if (ms.type === 'stdio') {
|
|
193
|
+
if (ms.command === contentServer.command) {
|
|
194
|
+
// Check args match (both undefined or same array)
|
|
195
|
+
const msArgs = ms.args || [];
|
|
196
|
+
const csArgs = contentServer.args || [];
|
|
197
|
+
if (arraysEqual(msArgs, csArgs)) {
|
|
198
|
+
return ms;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// For http type, check url
|
|
203
|
+
if (ms.type === 'http' && ms.url === contentServer.url) {
|
|
204
|
+
return ms;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (contentServer.source === 'docker') {
|
|
208
|
+
// Docker server: match within servers array
|
|
209
|
+
if (ms.type === 'docker' && ms.servers) {
|
|
210
|
+
for (const inner of ms.servers) {
|
|
211
|
+
if (inner.name === contentServer.name) {
|
|
212
|
+
// Check command and args match
|
|
213
|
+
if (inner.command === contentServer.command) {
|
|
214
|
+
const innerArgs = inner.args || [];
|
|
215
|
+
const csArgs = contentServer.args || [];
|
|
216
|
+
if (arraysEqual(innerArgs, csArgs)) {
|
|
217
|
+
return ms;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Validate that a Docker image exists in any YAML file under docker/ (compose or otherwise)
|
|
229
|
+
*/
|
|
230
|
+
function validateDockerImage(skillsetDir, image, errors) {
|
|
231
|
+
const contentDir = join(skillsetDir, 'content');
|
|
232
|
+
const dockerDir = join(contentDir, 'docker');
|
|
233
|
+
if (!existsSync(dockerDir)) {
|
|
234
|
+
errors.push(`Docker image '${image}' declared but no docker directory found`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const yamlFiles = findYamlFiles(dockerDir);
|
|
238
|
+
let found = false;
|
|
239
|
+
for (const yamlPath of yamlFiles) {
|
|
240
|
+
try {
|
|
241
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
242
|
+
const data = yaml.load(content);
|
|
243
|
+
// Check if any service uses the specified image
|
|
244
|
+
if (data.services && typeof data.services === 'object') {
|
|
245
|
+
for (const service of Object.values(data.services)) {
|
|
246
|
+
if (service.image === image) {
|
|
247
|
+
found = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Parse errors for compose files are non-fatal here — already caught in content scanning
|
|
255
|
+
}
|
|
256
|
+
if (found)
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (!found) {
|
|
260
|
+
errors.push(`Docker image '${image}' not found in any YAML file under docker/`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Recursively find all YAML files (.yaml, .yml) in a directory
|
|
265
|
+
*/
|
|
266
|
+
function findYamlFiles(dir) {
|
|
267
|
+
const results = [];
|
|
268
|
+
if (!existsSync(dir)) {
|
|
269
|
+
return results;
|
|
270
|
+
}
|
|
271
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const fullPath = join(dir, entry.name);
|
|
274
|
+
if (entry.isDirectory()) {
|
|
275
|
+
results.push(...findYamlFiles(fullPath));
|
|
276
|
+
}
|
|
277
|
+
else if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
|
|
278
|
+
results.push(fullPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return results;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Compare two arrays for equality
|
|
285
|
+
*/
|
|
286
|
+
function arraysEqual(a, b) {
|
|
287
|
+
if (a.length !== b.length)
|
|
288
|
+
return false;
|
|
289
|
+
for (let i = 0; i < a.length; i++) {
|
|
290
|
+
if (a[i] !== b[i])
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare semver versions. Returns:
|
|
3
|
+
* -1 if a < b, 0 if a == b, 1 if a > b
|
|
4
|
+
*/
|
|
5
|
+
export function compareVersions(a, b) {
|
|
6
|
+
const partsA = a.split('.').map(Number);
|
|
7
|
+
const partsB = b.split('.').map(Number);
|
|
8
|
+
for (let i = 0; i < 3; i++) {
|
|
9
|
+
const numA = partsA[i] || 0;
|
|
10
|
+
const numB = partsB[i] || 0;
|
|
11
|
+
if (numA < numB)
|
|
12
|
+
return -1;
|
|
13
|
+
if (numA > numB)
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,7 +17,10 @@ export interface SearchIndexEntry {
|
|
|
17
17
|
version: string;
|
|
18
18
|
status: 'active' | 'deprecated' | 'archived';
|
|
19
19
|
verification: {
|
|
20
|
-
|
|
20
|
+
production_links: Array<{
|
|
21
|
+
url: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
}>;
|
|
21
24
|
production_proof?: string;
|
|
22
25
|
audit_report: string;
|
|
23
26
|
};
|
|
@@ -28,6 +31,7 @@ export interface SearchIndexEntry {
|
|
|
28
31
|
entry_point: string;
|
|
29
32
|
checksum: string;
|
|
30
33
|
files: Record<string, string>;
|
|
34
|
+
mcp_servers?: McpServer[];
|
|
31
35
|
}
|
|
32
36
|
export interface StatsResponse {
|
|
33
37
|
stars: Record<string, number>;
|
|
@@ -43,7 +47,10 @@ export interface Skillset {
|
|
|
43
47
|
url: string;
|
|
44
48
|
};
|
|
45
49
|
verification: {
|
|
46
|
-
|
|
50
|
+
production_links: Array<{
|
|
51
|
+
url: string;
|
|
52
|
+
label?: string;
|
|
53
|
+
}>;
|
|
47
54
|
production_proof?: string;
|
|
48
55
|
audit_report: string;
|
|
49
56
|
};
|
|
@@ -54,4 +61,23 @@ export interface Skillset {
|
|
|
54
61
|
};
|
|
55
62
|
status: 'active' | 'deprecated' | 'archived';
|
|
56
63
|
entry_point: string;
|
|
64
|
+
mcp_servers?: McpServer[];
|
|
65
|
+
}
|
|
66
|
+
export interface McpServerInner {
|
|
67
|
+
name: string;
|
|
68
|
+
command: string;
|
|
69
|
+
args?: string[];
|
|
70
|
+
mcp_reputation: string;
|
|
71
|
+
researched_at: string;
|
|
72
|
+
}
|
|
73
|
+
export interface McpServer {
|
|
74
|
+
name: string;
|
|
75
|
+
type: 'stdio' | 'http' | 'docker';
|
|
76
|
+
command?: string;
|
|
77
|
+
args?: string[];
|
|
78
|
+
url?: string;
|
|
79
|
+
image?: string;
|
|
80
|
+
servers?: McpServerInner[];
|
|
81
|
+
mcp_reputation: string;
|
|
82
|
+
researched_at: string;
|
|
57
83
|
}
|