@skillsmith/cli 0.3.0 → 0.3.2
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 +158 -0
- package/assets/skillsmith-skill/SKILL.md +235 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/commands/author/index.d.ts +16 -0
- package/dist/src/commands/author/index.d.ts.map +1 -0
- package/dist/src/commands/author/index.js +18 -0
- package/dist/src/commands/author/index.js.map +1 -0
- package/dist/src/commands/author/init.d.ts +47 -0
- package/dist/src/commands/author/init.d.ts.map +1 -0
- package/dist/src/commands/author/init.js +346 -0
- package/dist/src/commands/author/init.js.map +1 -0
- package/dist/src/commands/author/mcp-init.d.ts +20 -0
- package/dist/src/commands/author/mcp-init.d.ts.map +1 -0
- package/dist/src/commands/author/mcp-init.js +183 -0
- package/dist/src/commands/author/mcp-init.js.map +1 -0
- package/dist/src/commands/author/subagent.d.ts +22 -0
- package/dist/src/commands/author/subagent.d.ts.map +1 -0
- package/dist/src/commands/author/subagent.js +166 -0
- package/dist/src/commands/author/subagent.js.map +1 -0
- package/dist/src/commands/author/transform.d.ts +22 -0
- package/dist/src/commands/author/transform.d.ts.map +1 -0
- package/dist/src/commands/author/transform.js +141 -0
- package/dist/src/commands/author/transform.js.map +1 -0
- package/dist/src/commands/author/utils.d.ts +27 -0
- package/dist/src/commands/author/utils.d.ts.map +1 -0
- package/dist/src/commands/author/utils.js +118 -0
- package/dist/src/commands/author/utils.js.map +1 -0
- package/dist/src/commands/index.d.ts +3 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +6 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/install-skill.d.ts +13 -0
- package/dist/src/commands/install-skill.d.ts.map +1 -0
- package/dist/src/commands/install-skill.js +137 -0
- package/dist/src/commands/install-skill.js.map +1 -0
- package/dist/src/commands/manage.d.ts +4 -1
- package/dist/src/commands/manage.d.ts.map +1 -1
- package/dist/src/commands/manage.js +56 -10
- package/dist/src/commands/manage.js.map +1 -1
- package/dist/src/commands/merge.d.ts +17 -0
- package/dist/src/commands/merge.d.ts.map +1 -0
- package/dist/src/commands/merge.js +160 -0
- package/dist/src/commands/merge.js.map +1 -0
- package/dist/src/commands/recommend.d.ts +1 -4
- package/dist/src/commands/recommend.d.ts.map +1 -1
- package/dist/src/commands/recommend.helpers.d.ts +58 -0
- package/dist/src/commands/recommend.helpers.d.ts.map +1 -0
- package/dist/src/commands/recommend.helpers.js +428 -0
- package/dist/src/commands/recommend.helpers.js.map +1 -0
- package/dist/src/commands/recommend.js +50 -372
- package/dist/src/commands/recommend.js.map +1 -1
- package/dist/src/commands/recommend.types.d.ts +66 -0
- package/dist/src/commands/recommend.types.d.ts.map +1 -0
- package/dist/src/commands/recommend.types.js +14 -0
- package/dist/src/commands/recommend.types.js.map +1 -0
- package/dist/src/commands/search.d.ts.map +1 -1
- package/dist/src/commands/search.js +133 -18
- package/dist/src/commands/search.js.map +1 -1
- package/dist/src/commands/sync.d.ts.map +1 -1
- package/dist/src/commands/sync.js +6 -46
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +7 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/import.d.ts +1 -0
- package/dist/src/import.d.ts.map +1 -1
- package/dist/src/import.js +20 -5
- package/dist/src/import.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/formatters.d.ts +39 -0
- package/dist/src/utils/formatters.d.ts.map +1 -0
- package/dist/src/utils/formatters.js +69 -0
- package/dist/src/utils/formatters.js.map +1 -0
- package/dist/src/utils/license.test.js +6 -1
- package/dist/src/utils/license.test.js.map +1 -1
- package/dist/src/utils/node-version.d.ts +41 -0
- package/dist/src/utils/node-version.d.ts.map +1 -0
- package/dist/src/utils/node-version.js +123 -0
- package/dist/src/utils/node-version.js.map +1 -0
- package/dist/tests/author.test.js +45 -45
- package/dist/tests/author.test.js.map +1 -1
- package/dist/tests/e2e/search.e2e.test.js +62 -6
- package/dist/tests/e2e/search.e2e.test.js.map +1 -1
- package/dist/tests/e2e/utils/hardcoded-detector.d.ts.map +1 -1
- package/dist/tests/e2e/utils/hardcoded-detector.js +44 -3
- package/dist/tests/e2e/utils/hardcoded-detector.js.map +1 -1
- package/dist/tests/install-skill.test.d.ts +8 -0
- package/dist/tests/install-skill.test.d.ts.map +1 -0
- package/dist/tests/install-skill.test.js +409 -0
- package/dist/tests/install-skill.test.js.map +1 -0
- package/dist/tests/manage.test.js +284 -8
- package/dist/tests/manage.test.js.map +1 -1
- package/dist/tests/node-version.test.d.ts +8 -0
- package/dist/tests/node-version.test.d.ts.map +1 -0
- package/dist/tests/node-version.test.js +200 -0
- package/dist/tests/node-version.test.js.map +1 -0
- package/dist/tests/recommend.test.js +94 -0
- package/dist/tests/recommend.test.js.map +1 -1
- package/package.json +3 -2
- package/dist/src/commands/author.d.ts +0 -90
- package/dist/src/commands/author.d.ts.map +0 -1
- package/dist/src/commands/author.js +0 -902
- package/dist/src/commands/author.js.map +0 -1
|
@@ -1,902 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SMI-746: Skill Authoring Commands
|
|
3
|
-
* SMI-1389: Subagent Generation Command
|
|
4
|
-
* SMI-1390: Transform Command
|
|
5
|
-
* SMI-1433: MCP Server Scaffolding
|
|
6
|
-
*
|
|
7
|
-
* Provides CLI commands for creating, validating, publishing skills,
|
|
8
|
-
* and generating companion subagents.
|
|
9
|
-
*/
|
|
10
|
-
import { Command } from 'commander';
|
|
11
|
-
import { input, confirm, select } from '@inquirer/prompts';
|
|
12
|
-
import chalk from 'chalk';
|
|
13
|
-
import ora from 'ora';
|
|
14
|
-
import { mkdir, writeFile, readFile, stat, access } from 'fs/promises';
|
|
15
|
-
import { dirname, join, resolve } from 'path';
|
|
16
|
-
import { createHash } from 'crypto';
|
|
17
|
-
import { homedir } from 'os';
|
|
18
|
-
import { SkillParser } from '@skillsmith/core';
|
|
19
|
-
import { SKILL_MD_TEMPLATE, README_MD_TEMPLATE, renderSubagentTemplate, renderClaudeMdSnippet, renderMcpServerTemplates, } from '../templates/index.js';
|
|
20
|
-
import { sanitizeError } from '../utils/sanitize.js';
|
|
21
|
-
import { analyzeToolRequirements, formatToolList, parseToolsString, validateTools, } from '../utils/tool-analyzer.js';
|
|
22
|
-
/**
|
|
23
|
-
* Valid categories for skill initialization
|
|
24
|
-
*/
|
|
25
|
-
const VALID_CATEGORIES = [
|
|
26
|
-
'development',
|
|
27
|
-
'productivity',
|
|
28
|
-
'communication',
|
|
29
|
-
'data',
|
|
30
|
-
'security',
|
|
31
|
-
'other',
|
|
32
|
-
];
|
|
33
|
-
/**
|
|
34
|
-
* Initialize a new skill directory
|
|
35
|
-
*/
|
|
36
|
-
async function initSkill(name, targetPath, options = {}) {
|
|
37
|
-
// Interactive prompts if name not provided
|
|
38
|
-
const skillName = name ||
|
|
39
|
-
(await input({
|
|
40
|
-
message: 'Skill name:',
|
|
41
|
-
validate: (value) => {
|
|
42
|
-
if (!value.trim())
|
|
43
|
-
return 'Name is required';
|
|
44
|
-
if (!/^[a-zA-Z][a-zA-Z0-9-_]*$/.test(value)) {
|
|
45
|
-
return 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores';
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
},
|
|
49
|
-
}));
|
|
50
|
-
// Use provided options or prompt interactively
|
|
51
|
-
const description = options.description ||
|
|
52
|
-
(await input({
|
|
53
|
-
message: 'Description:',
|
|
54
|
-
default: `A Claude skill for ${skillName}`,
|
|
55
|
-
}));
|
|
56
|
-
const author = options.author ||
|
|
57
|
-
(await input({
|
|
58
|
-
message: 'Author:',
|
|
59
|
-
default: process.env['USER'] || 'author',
|
|
60
|
-
}));
|
|
61
|
-
// Validate category if provided via CLI
|
|
62
|
-
if (options.category && !VALID_CATEGORIES.includes(options.category)) {
|
|
63
|
-
console.error(chalk.red(`Invalid category: ${options.category}. Valid categories: ${VALID_CATEGORIES.join(', ')}`));
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
const category = options.category ||
|
|
67
|
-
(await select({
|
|
68
|
-
message: 'Category:',
|
|
69
|
-
choices: [
|
|
70
|
-
{ name: 'Development', value: 'development' },
|
|
71
|
-
{ name: 'Productivity', value: 'productivity' },
|
|
72
|
-
{ name: 'Communication', value: 'communication' },
|
|
73
|
-
{ name: 'Data', value: 'data' },
|
|
74
|
-
{ name: 'Security', value: 'security' },
|
|
75
|
-
{ name: 'Other', value: 'other' },
|
|
76
|
-
],
|
|
77
|
-
}));
|
|
78
|
-
const skillDir = resolve(targetPath, skillName);
|
|
79
|
-
// Check if directory already exists
|
|
80
|
-
try {
|
|
81
|
-
await stat(skillDir);
|
|
82
|
-
// Skip confirmation if --yes flag is set
|
|
83
|
-
if (!options.yes) {
|
|
84
|
-
const overwrite = await confirm({
|
|
85
|
-
message: `Directory ${skillDir} already exists. Overwrite?`,
|
|
86
|
-
default: false,
|
|
87
|
-
});
|
|
88
|
-
if (!overwrite) {
|
|
89
|
-
console.log(chalk.yellow('Initialization cancelled'));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// Directory doesn't exist, continue
|
|
96
|
-
}
|
|
97
|
-
const spinner = ora('Creating skill structure...').start();
|
|
98
|
-
try {
|
|
99
|
-
// Create directory structure
|
|
100
|
-
await mkdir(skillDir, { recursive: true });
|
|
101
|
-
await mkdir(join(skillDir, 'scripts'), { recursive: true });
|
|
102
|
-
await mkdir(join(skillDir, 'resources'), { recursive: true });
|
|
103
|
-
// Generate SKILL.md from template
|
|
104
|
-
const skillMdContent = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, skillName)
|
|
105
|
-
.replace(/\{\{description\}\}/g, description)
|
|
106
|
-
.replace(/\{\{author\}\}/g, author)
|
|
107
|
-
.replace(/\{\{category\}\}/g, category)
|
|
108
|
-
.replace(/\{\{date\}\}/g, new Date().toISOString().split('T')[0] || '');
|
|
109
|
-
await writeFile(join(skillDir, 'SKILL.md'), skillMdContent, 'utf-8');
|
|
110
|
-
// Generate README.md from template
|
|
111
|
-
const readmeContent = README_MD_TEMPLATE.replace(/\{\{name\}\}/g, skillName).replace(/\{\{description\}\}/g, description);
|
|
112
|
-
await writeFile(join(skillDir, 'README.md'), readmeContent, 'utf-8');
|
|
113
|
-
// Create placeholder script
|
|
114
|
-
const placeholderScript = `#!/usr/bin/env node
|
|
115
|
-
/**
|
|
116
|
-
* ${skillName} - Example Script
|
|
117
|
-
*
|
|
118
|
-
* Add your skill's automation scripts here.
|
|
119
|
-
*/
|
|
120
|
-
|
|
121
|
-
console.log('${skillName} script executed');
|
|
122
|
-
`;
|
|
123
|
-
await writeFile(join(skillDir, 'scripts', 'example.js'), placeholderScript, 'utf-8');
|
|
124
|
-
// Create .gitignore
|
|
125
|
-
const gitignore = `# Dependencies
|
|
126
|
-
node_modules/
|
|
127
|
-
|
|
128
|
-
# Build output
|
|
129
|
-
dist/
|
|
130
|
-
|
|
131
|
-
# Environment
|
|
132
|
-
.env
|
|
133
|
-
.env.local
|
|
134
|
-
|
|
135
|
-
# OS files
|
|
136
|
-
.DS_Store
|
|
137
|
-
Thumbs.db
|
|
138
|
-
`;
|
|
139
|
-
await writeFile(join(skillDir, '.gitignore'), gitignore, 'utf-8');
|
|
140
|
-
spinner.succeed(`Created skill at ${skillDir}`);
|
|
141
|
-
console.log(chalk.bold('\nNext steps:'));
|
|
142
|
-
console.log(chalk.dim(` 1. cd ${skillDir}`));
|
|
143
|
-
console.log(chalk.dim(' 2. Edit SKILL.md to customize your skill'));
|
|
144
|
-
console.log(chalk.dim(' 3. Add scripts to the scripts/ directory'));
|
|
145
|
-
console.log(chalk.dim(' 4. Run skillsmith validate to check your skill'));
|
|
146
|
-
console.log(chalk.dim(' 5. Run skillsmith publish to prepare for sharing'));
|
|
147
|
-
console.log();
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
spinner.fail(`Failed to create skill: ${sanitizeError(error)}`);
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Pretty print validation errors and warnings
|
|
156
|
-
*/
|
|
157
|
-
function printValidationResult(result, filePath) {
|
|
158
|
-
console.log(chalk.bold(`\nValidation Result for ${filePath}:\n`));
|
|
159
|
-
if (result.valid) {
|
|
160
|
-
console.log(chalk.green.bold(' VALID'));
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
console.log(chalk.red.bold(' INVALID'));
|
|
164
|
-
}
|
|
165
|
-
if (result.errors.length > 0) {
|
|
166
|
-
console.log(chalk.red.bold('\nErrors:'));
|
|
167
|
-
for (const error of result.errors) {
|
|
168
|
-
console.log(chalk.red(` - ${error}`));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (result.warnings.length > 0) {
|
|
172
|
-
console.log(chalk.yellow.bold('\nWarnings:'));
|
|
173
|
-
for (const warning of result.warnings) {
|
|
174
|
-
console.log(chalk.yellow(` - ${warning}`));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
console.log();
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Validate a local SKILL.md file
|
|
181
|
-
*/
|
|
182
|
-
async function validateSkill(skillPath) {
|
|
183
|
-
const spinner = ora('Validating skill...').start();
|
|
184
|
-
try {
|
|
185
|
-
// Resolve path
|
|
186
|
-
let filePath = resolve(skillPath);
|
|
187
|
-
// Check if it's a directory, look for SKILL.md
|
|
188
|
-
try {
|
|
189
|
-
const stats = await stat(filePath);
|
|
190
|
-
if (stats.isDirectory()) {
|
|
191
|
-
filePath = join(filePath, 'SKILL.md');
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// If path doesn't exist, try adding SKILL.md
|
|
196
|
-
if (!filePath.endsWith('.md')) {
|
|
197
|
-
filePath = join(filePath, 'SKILL.md');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
// Read file
|
|
201
|
-
const content = await readFile(filePath, 'utf-8');
|
|
202
|
-
// Parse and validate
|
|
203
|
-
const parser = new SkillParser({ requireName: true });
|
|
204
|
-
const { validation, metadata, frontmatter } = parser.parseWithValidation(content);
|
|
205
|
-
spinner.stop();
|
|
206
|
-
printValidationResult(validation, filePath);
|
|
207
|
-
if (metadata) {
|
|
208
|
-
console.log(chalk.bold('Parsed Metadata:'));
|
|
209
|
-
console.log(chalk.dim(` Name: ${metadata.name}`));
|
|
210
|
-
console.log(chalk.dim(` Description: ${metadata.description || 'N/A'}`));
|
|
211
|
-
console.log(chalk.dim(` Author: ${metadata.author || 'N/A'}`));
|
|
212
|
-
console.log(chalk.dim(` Version: ${metadata.version || 'N/A'}`));
|
|
213
|
-
console.log(chalk.dim(` Tags: ${metadata.tags.join(', ') || 'None'}`));
|
|
214
|
-
console.log(chalk.dim(` Trust Tier: ${parser.inferTrustTier(metadata)}`));
|
|
215
|
-
console.log();
|
|
216
|
-
}
|
|
217
|
-
if (frontmatter) {
|
|
218
|
-
console.log(chalk.bold('Frontmatter Fields:'));
|
|
219
|
-
for (const [key, value] of Object.entries(frontmatter)) {
|
|
220
|
-
if (value !== undefined && value !== null) {
|
|
221
|
-
const displayValue = Array.isArray(value) ? value.join(', ') : String(value);
|
|
222
|
-
console.log(chalk.dim(` ${key}: ${displayValue}`));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
console.log();
|
|
226
|
-
}
|
|
227
|
-
return validation.valid;
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
spinner.fail(`Validation failed: ${sanitizeError(error)}`);
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Prepare skill for publishing
|
|
236
|
-
* @returns true if publishing succeeded, false if validation failed
|
|
237
|
-
*/
|
|
238
|
-
async function publishSkill(skillPath) {
|
|
239
|
-
const spinner = ora('Preparing skill for publishing...').start();
|
|
240
|
-
try {
|
|
241
|
-
// Resolve path
|
|
242
|
-
let dirPath = resolve(skillPath || '.');
|
|
243
|
-
// Check if it's a file, get directory
|
|
244
|
-
try {
|
|
245
|
-
const stats = await stat(dirPath);
|
|
246
|
-
if (!stats.isDirectory()) {
|
|
247
|
-
dirPath = dirname(dirPath);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
// Path doesn't exist
|
|
252
|
-
spinner.fail(`Directory not found: ${dirPath}`);
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
const skillMdPath = join(dirPath, 'SKILL.md');
|
|
256
|
-
// Validate first
|
|
257
|
-
spinner.text = 'Validating skill...';
|
|
258
|
-
const content = await readFile(skillMdPath, 'utf-8');
|
|
259
|
-
const parser = new SkillParser({ requireName: true });
|
|
260
|
-
const { validation, metadata } = parser.parseWithValidation(content);
|
|
261
|
-
if (!validation.valid) {
|
|
262
|
-
spinner.fail('Skill validation failed');
|
|
263
|
-
printValidationResult(validation, skillMdPath);
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
if (!metadata) {
|
|
267
|
-
spinner.fail('Could not parse skill metadata');
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
// Generate checksum
|
|
271
|
-
spinner.text = 'Generating checksum...';
|
|
272
|
-
const checksum = createHash('sha256').update(content).digest('hex');
|
|
273
|
-
// Create publish info
|
|
274
|
-
const publishInfo = {
|
|
275
|
-
name: metadata.name,
|
|
276
|
-
version: metadata.version || '1.0.0',
|
|
277
|
-
checksum,
|
|
278
|
-
publishedAt: new Date().toISOString(),
|
|
279
|
-
trustTier: parser.inferTrustTier(metadata),
|
|
280
|
-
};
|
|
281
|
-
// Write publish manifest
|
|
282
|
-
const manifestPath = join(dirPath, '.skillsmith-publish.json');
|
|
283
|
-
await writeFile(manifestPath, JSON.stringify(publishInfo, null, 2), 'utf-8');
|
|
284
|
-
spinner.succeed('Skill prepared for publishing');
|
|
285
|
-
console.log(chalk.bold('\nPublish Information:'));
|
|
286
|
-
console.log(chalk.dim(` Name: ${publishInfo.name}`));
|
|
287
|
-
console.log(chalk.dim(` Version: ${publishInfo.version}`));
|
|
288
|
-
console.log(chalk.dim(` Checksum: ${publishInfo.checksum.slice(0, 16)}...`));
|
|
289
|
-
console.log(chalk.dim(` Trust Tier: ${publishInfo.trustTier}`));
|
|
290
|
-
console.log();
|
|
291
|
-
console.log(chalk.bold('To share this skill:'));
|
|
292
|
-
console.log(chalk.cyan('\n Option 1: GitHub'));
|
|
293
|
-
console.log(chalk.dim(' 1. Push to a GitHub repository'));
|
|
294
|
-
console.log(chalk.dim(' 2. Add topic "claude-skill" to the repository'));
|
|
295
|
-
console.log(chalk.dim(' 3. The skill will be automatically discovered'));
|
|
296
|
-
console.log(chalk.cyan('\n Option 2: Manual Installation'));
|
|
297
|
-
console.log(chalk.dim(` 1. Share the ${dirPath} directory`));
|
|
298
|
-
console.log(chalk.dim(' 2. Users can copy to ~/.claude/skills/'));
|
|
299
|
-
console.log(chalk.cyan('\n Option 3: Archive'));
|
|
300
|
-
console.log(chalk.dim(` 1. Create archive: tar -czf ${metadata.name}.tar.gz ${dirPath}`));
|
|
301
|
-
console.log(chalk.dim(' 2. Share the archive'));
|
|
302
|
-
console.log();
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
spinner.fail(`Publishing failed: ${sanitizeError(error)}`);
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Create init command
|
|
312
|
-
* SMI-1473: Added non-interactive flags for E2E testing
|
|
313
|
-
*/
|
|
314
|
-
export function createInitCommand() {
|
|
315
|
-
return new Command('init')
|
|
316
|
-
.description('Initialize a new skill directory')
|
|
317
|
-
.argument('[name]', 'Skill name')
|
|
318
|
-
.option('-p, --path <path>', 'Target directory', '.')
|
|
319
|
-
.option('-d, --description <description>', 'Skill description (non-interactive)')
|
|
320
|
-
.option('-a, --author <author>', 'Skill author (non-interactive)')
|
|
321
|
-
.option('-c, --category <category>', 'Skill category: development|productivity|communication|data|security|other (non-interactive)')
|
|
322
|
-
.option('-y, --yes', 'Auto-confirm overwrite (non-interactive)')
|
|
323
|
-
.action(async (name, opts) => {
|
|
324
|
-
const targetPath = opts['path'] || '.';
|
|
325
|
-
try {
|
|
326
|
-
await initSkill(name, targetPath, {
|
|
327
|
-
description: opts['description'],
|
|
328
|
-
author: opts['author'],
|
|
329
|
-
category: opts['category'],
|
|
330
|
-
yes: opts['yes'],
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
console.error(chalk.red('Error initializing skill:'), sanitizeError(error));
|
|
335
|
-
process.exit(1);
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Create validate command
|
|
341
|
-
*/
|
|
342
|
-
export function createValidateCommand() {
|
|
343
|
-
return new Command('validate')
|
|
344
|
-
.description('Validate a local SKILL.md file')
|
|
345
|
-
.argument('[path]', 'Path to SKILL.md or skill directory', '.')
|
|
346
|
-
.action(async (skillPath) => {
|
|
347
|
-
try {
|
|
348
|
-
const valid = await validateSkill(skillPath);
|
|
349
|
-
process.exit(valid ? 0 : 1);
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
console.error(chalk.red('Error validating skill:'), sanitizeError(error));
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Create publish command
|
|
359
|
-
*/
|
|
360
|
-
export function createPublishCommand() {
|
|
361
|
-
return new Command('publish')
|
|
362
|
-
.description('Prepare skill for sharing')
|
|
363
|
-
.argument('[path]', 'Path to skill directory', '.')
|
|
364
|
-
.action(async (skillPath) => {
|
|
365
|
-
try {
|
|
366
|
-
const success = await publishSkill(skillPath);
|
|
367
|
-
process.exit(success ? 0 : 1);
|
|
368
|
-
}
|
|
369
|
-
catch (error) {
|
|
370
|
-
console.error(chalk.red('Error publishing skill:'), sanitizeError(error));
|
|
371
|
-
process.exit(1);
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* SMI-1389: Extract trigger phrases from skill description
|
|
377
|
-
*/
|
|
378
|
-
function extractTriggerPhrases(description) {
|
|
379
|
-
const phrases = [];
|
|
380
|
-
// Pattern: "Use when [phrases]" or "when the user asks to [phrases]"
|
|
381
|
-
const patterns = [
|
|
382
|
-
/use when (?:the user asks to )?["']([^"']+)["']/gi,
|
|
383
|
-
/when (?:the user asks to )?["']([^"']+)["']/gi,
|
|
384
|
-
/trigger(?:ed)? (?:by|when|phrases?)[\s:]+["']([^"']+)["']/gi,
|
|
385
|
-
/invoke when (?:the user )?["']([^"']+)["']/gi,
|
|
386
|
-
];
|
|
387
|
-
for (const pattern of patterns) {
|
|
388
|
-
const matches = description.matchAll(pattern);
|
|
389
|
-
for (const match of matches) {
|
|
390
|
-
if (match[1]) {
|
|
391
|
-
phrases.push(match[1]);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return phrases;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* SMI-1389: Validate subagent definition structure
|
|
399
|
-
*/
|
|
400
|
-
function validateSubagentDefinition(content) {
|
|
401
|
-
const errors = [];
|
|
402
|
-
const warnings = [];
|
|
403
|
-
// Check for YAML frontmatter
|
|
404
|
-
if (!content.trim().startsWith('---')) {
|
|
405
|
-
errors.push('Missing YAML frontmatter');
|
|
406
|
-
}
|
|
407
|
-
// Extract and validate frontmatter
|
|
408
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
409
|
-
if (frontmatterMatch) {
|
|
410
|
-
const frontmatter = frontmatterMatch[1] || '';
|
|
411
|
-
const requiredFields = ['name', 'description', 'skills', 'tools', 'model'];
|
|
412
|
-
for (const field of requiredFields) {
|
|
413
|
-
if (!frontmatter.includes(`${field}:`)) {
|
|
414
|
-
errors.push(`Missing required field: ${field}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
errors.push('Could not parse YAML frontmatter');
|
|
420
|
-
}
|
|
421
|
-
// Check for operating protocol section
|
|
422
|
-
if (!content.includes('## Operating Protocol')) {
|
|
423
|
-
warnings.push('Missing Operating Protocol section');
|
|
424
|
-
}
|
|
425
|
-
// Check for output format section
|
|
426
|
-
if (!content.includes('## Output Format')) {
|
|
427
|
-
warnings.push('Missing Output Format section');
|
|
428
|
-
}
|
|
429
|
-
return {
|
|
430
|
-
valid: errors.length === 0,
|
|
431
|
-
errors,
|
|
432
|
-
warnings,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Ensure ~/.claude/agents directory exists
|
|
437
|
-
*/
|
|
438
|
-
async function ensureAgentsDirectory(customPath) {
|
|
439
|
-
const agentsDir = customPath
|
|
440
|
-
? resolve(customPath.replace(/^~/, homedir()))
|
|
441
|
-
: join(homedir(), '.claude', 'agents');
|
|
442
|
-
await mkdir(agentsDir, { recursive: true });
|
|
443
|
-
return agentsDir;
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Check if file exists
|
|
447
|
-
*/
|
|
448
|
-
async function fileExists(path) {
|
|
449
|
-
try {
|
|
450
|
-
await access(path);
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
catch {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* SMI-1389: Generate a companion subagent for a skill
|
|
459
|
-
*/
|
|
460
|
-
async function generateSubagent(skillPath, options) {
|
|
461
|
-
const spinner = ora('Generating subagent...').start();
|
|
462
|
-
try {
|
|
463
|
-
// Resolve skill path
|
|
464
|
-
let dirPath = resolve(skillPath || '.');
|
|
465
|
-
let skillMdPath;
|
|
466
|
-
// Check if it's a directory or file
|
|
467
|
-
try {
|
|
468
|
-
const stats = await stat(dirPath);
|
|
469
|
-
if (stats.isDirectory()) {
|
|
470
|
-
skillMdPath = join(dirPath, 'SKILL.md');
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
skillMdPath = dirPath;
|
|
474
|
-
dirPath = dirname(dirPath);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
// Try adding SKILL.md
|
|
479
|
-
skillMdPath = dirPath.endsWith('.md') ? dirPath : join(dirPath, 'SKILL.md');
|
|
480
|
-
}
|
|
481
|
-
// Read and parse SKILL.md
|
|
482
|
-
spinner.text = 'Reading SKILL.md...';
|
|
483
|
-
const content = await readFile(skillMdPath, 'utf-8');
|
|
484
|
-
const parser = new SkillParser({ requireName: true });
|
|
485
|
-
const { validation, metadata } = parser.parseWithValidation(content);
|
|
486
|
-
if (!validation.valid || !metadata) {
|
|
487
|
-
spinner.fail('SKILL.md validation failed');
|
|
488
|
-
printValidationResult(validation, skillMdPath);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
// Analyze tool requirements
|
|
492
|
-
spinner.text = 'Analyzing tool requirements...';
|
|
493
|
-
const toolAnalysis = analyzeToolRequirements(content);
|
|
494
|
-
// Override tools if specified
|
|
495
|
-
let tools = toolAnalysis.requiredTools;
|
|
496
|
-
if (options.tools) {
|
|
497
|
-
const customTools = parseToolsString(options.tools);
|
|
498
|
-
const validation = validateTools(customTools);
|
|
499
|
-
if (!validation.valid) {
|
|
500
|
-
spinner.fail(`Unrecognized tools: ${validation.unrecognized.join(', ')}`);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
tools = customTools;
|
|
504
|
-
}
|
|
505
|
-
// Extract trigger phrases
|
|
506
|
-
const triggerPhrases = extractTriggerPhrases(metadata.description || '');
|
|
507
|
-
// Determine model
|
|
508
|
-
const model = options.model || 'sonnet';
|
|
509
|
-
if (!['sonnet', 'opus', 'haiku'].includes(model)) {
|
|
510
|
-
spinner.fail(`Invalid model: ${model}. Must be sonnet, opus, or haiku.`);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
// Generate subagent content
|
|
514
|
-
spinner.text = 'Generating subagent definition...';
|
|
515
|
-
const subagentContent = renderSubagentTemplate({
|
|
516
|
-
skillName: metadata.name,
|
|
517
|
-
description: metadata.description || `Specialist for ${metadata.name}`,
|
|
518
|
-
triggerPhrases,
|
|
519
|
-
tools,
|
|
520
|
-
model,
|
|
521
|
-
});
|
|
522
|
-
// Validate generated content
|
|
523
|
-
const subagentValidation = validateSubagentDefinition(subagentContent);
|
|
524
|
-
if (!subagentValidation.valid) {
|
|
525
|
-
spinner.fail('Generated subagent is invalid');
|
|
526
|
-
console.log(chalk.red('\nGeneration errors:'));
|
|
527
|
-
for (const error of subagentValidation.errors) {
|
|
528
|
-
console.log(chalk.red(` - ${error}`));
|
|
529
|
-
}
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
// Ensure agents directory exists
|
|
533
|
-
const agentsDir = await ensureAgentsDirectory(options.output);
|
|
534
|
-
const subagentPath = join(agentsDir, `${metadata.name}-specialist.md`);
|
|
535
|
-
// Check if subagent already exists
|
|
536
|
-
if (await fileExists(subagentPath)) {
|
|
537
|
-
if (!options.force) {
|
|
538
|
-
spinner.warn(`Subagent already exists: ${subagentPath}`);
|
|
539
|
-
console.log(chalk.yellow(' Use --force to overwrite'));
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
// Write subagent file
|
|
544
|
-
await writeFile(subagentPath, subagentContent, 'utf-8');
|
|
545
|
-
spinner.succeed(`Generated subagent: ${subagentPath}`);
|
|
546
|
-
// Show tool analysis
|
|
547
|
-
console.log(chalk.bold('\nTool Analysis:'));
|
|
548
|
-
console.log(chalk.dim(` Model: ${model}`));
|
|
549
|
-
console.log(chalk.dim(` Confidence: ${toolAnalysis.confidence}`));
|
|
550
|
-
console.log(chalk.dim(` Tools: ${formatToolList(tools)}`));
|
|
551
|
-
if (toolAnalysis.detectedPatterns.length > 0) {
|
|
552
|
-
console.log(chalk.dim(' Detected patterns:'));
|
|
553
|
-
for (const pattern of toolAnalysis.detectedPatterns.slice(0, 5)) {
|
|
554
|
-
console.log(chalk.dim(` - ${pattern}`));
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
// Generate and display CLAUDE.md snippet
|
|
558
|
-
if (!options.skipClaudeMd) {
|
|
559
|
-
const snippet = renderClaudeMdSnippet({
|
|
560
|
-
skillName: metadata.name,
|
|
561
|
-
description: metadata.description || '',
|
|
562
|
-
triggerPhrases,
|
|
563
|
-
tools,
|
|
564
|
-
model,
|
|
565
|
-
});
|
|
566
|
-
console.log(chalk.bold('\nCLAUDE.md Integration Snippet:'));
|
|
567
|
-
console.log(chalk.cyan('─'.repeat(50)));
|
|
568
|
-
console.log(snippet);
|
|
569
|
-
console.log(chalk.cyan('─'.repeat(50)));
|
|
570
|
-
console.log(chalk.dim('\nAdd this snippet to your project CLAUDE.md to enable delegation.'));
|
|
571
|
-
}
|
|
572
|
-
console.log();
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
spinner.fail(`Failed to generate subagent: ${sanitizeError(error)}`);
|
|
576
|
-
throw error;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* SMI-1390: Transform existing skill by generating subagent (non-destructive)
|
|
581
|
-
*/
|
|
582
|
-
async function transformSkill(skillPath, options) {
|
|
583
|
-
const spinner = ora('Transforming skill...').start();
|
|
584
|
-
try {
|
|
585
|
-
const dirPath = resolve(skillPath || '.');
|
|
586
|
-
// Check if batch mode
|
|
587
|
-
if (options.batch) {
|
|
588
|
-
spinner.text = 'Processing batch...';
|
|
589
|
-
let skillDirs = [];
|
|
590
|
-
// Support comma-separated paths
|
|
591
|
-
if (dirPath.includes(',')) {
|
|
592
|
-
skillDirs = dirPath.split(',').map((p) => resolve(p.trim()));
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
// It's a directory, scan for subdirectories with SKILL.md
|
|
596
|
-
const { readdir } = await import('fs/promises');
|
|
597
|
-
const subdirs = await readdir(dirPath, { withFileTypes: true });
|
|
598
|
-
for (const entry of subdirs) {
|
|
599
|
-
if (entry.isDirectory()) {
|
|
600
|
-
const skillMdPath = join(dirPath, entry.name, 'SKILL.md');
|
|
601
|
-
if (await fileExists(skillMdPath)) {
|
|
602
|
-
skillDirs.push(join(dirPath, entry.name));
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
if (skillDirs.length === 0) {
|
|
608
|
-
spinner.warn('No skills found');
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
spinner.succeed(`Batch processing ${skillDirs.length} skills`);
|
|
612
|
-
// Also output to stdout for test assertions (spinner goes to stderr)
|
|
613
|
-
console.log(chalk.green(`Batch processing ${skillDirs.length} skills`));
|
|
614
|
-
// Process each skill
|
|
615
|
-
for (const skillDir of skillDirs) {
|
|
616
|
-
console.log(chalk.dim(`\nProcessing: ${skillDir}`));
|
|
617
|
-
await transformSkill(skillDir, {
|
|
618
|
-
...options,
|
|
619
|
-
batch: false, // Don't recurse
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
// Single skill transform
|
|
625
|
-
const skillMdPath = join(dirPath, 'SKILL.md');
|
|
626
|
-
if (!(await fileExists(skillMdPath))) {
|
|
627
|
-
spinner.fail(`No SKILL.md found at: ${skillMdPath}`);
|
|
628
|
-
throw new Error(`No SKILL.md found at: ${skillMdPath}`);
|
|
629
|
-
}
|
|
630
|
-
// Read and parse
|
|
631
|
-
spinner.text = 'Reading SKILL.md...';
|
|
632
|
-
const content = await readFile(skillMdPath, 'utf-8');
|
|
633
|
-
const parser = new SkillParser({ requireName: true });
|
|
634
|
-
const { validation, metadata } = parser.parseWithValidation(content);
|
|
635
|
-
if (!validation.valid || !metadata) {
|
|
636
|
-
spinner.fail('SKILL.md validation failed');
|
|
637
|
-
printValidationResult(validation, skillMdPath);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
// Check if subagent already exists
|
|
641
|
-
const agentsDir = join(homedir(), '.claude', 'agents');
|
|
642
|
-
const subagentPath = join(agentsDir, `${metadata.name}-specialist.md`);
|
|
643
|
-
if (await fileExists(subagentPath)) {
|
|
644
|
-
if (!options.force) {
|
|
645
|
-
spinner.warn(`Subagent already exists: ${subagentPath}`);
|
|
646
|
-
console.log(chalk.yellow(' Use --force to overwrite'));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
if (options.dryRun) {
|
|
651
|
-
spinner.succeed('Dry run - would generate:');
|
|
652
|
-
// Also output to stdout for test assertions (spinner goes to stderr)
|
|
653
|
-
console.log(chalk.green('Dry run - would generate:'));
|
|
654
|
-
console.log(chalk.dim(` Subagent: ${subagentPath}`));
|
|
655
|
-
console.log(chalk.dim(` Skill: ${metadata.name}`));
|
|
656
|
-
// Show tool analysis
|
|
657
|
-
const toolAnalysis = analyzeToolRequirements(content);
|
|
658
|
-
console.log(chalk.dim(` Required Tools: ${formatToolList(toolAnalysis.requiredTools)}`));
|
|
659
|
-
console.log(chalk.dim(` Confidence: ${toolAnalysis.confidence}`));
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
spinner.stop();
|
|
663
|
-
// Generate subagent using existing function
|
|
664
|
-
await generateSubagent(dirPath, {
|
|
665
|
-
force: options.force,
|
|
666
|
-
tools: options.tools,
|
|
667
|
-
model: options.model,
|
|
668
|
-
skipClaudeMd: false,
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
spinner.fail(`Failed to transform skill: ${sanitizeError(error)}`);
|
|
673
|
-
throw error;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Create subagent command
|
|
678
|
-
*/
|
|
679
|
-
export function createSubagentCommand() {
|
|
680
|
-
return new Command('subagent')
|
|
681
|
-
.description('Generate a companion subagent for a skill')
|
|
682
|
-
.argument('[path]', 'Path to skill directory', '.')
|
|
683
|
-
.option('-o, --output <path>', 'Output directory', '~/.claude/agents')
|
|
684
|
-
.option('--tools <tools>', 'Override detected tools (comma-separated)')
|
|
685
|
-
.option('--model <model>', 'Model for subagent: sonnet|opus|haiku', 'sonnet')
|
|
686
|
-
.option('--skip-claude-md', 'Skip CLAUDE.md snippet generation')
|
|
687
|
-
.option('--force', 'Overwrite existing subagent definition')
|
|
688
|
-
.action(async (skillPath, opts) => {
|
|
689
|
-
try {
|
|
690
|
-
await generateSubagent(skillPath, {
|
|
691
|
-
output: opts['output'],
|
|
692
|
-
tools: opts['tools'],
|
|
693
|
-
model: opts['model'],
|
|
694
|
-
skipClaudeMd: opts['skipClaudeMd'],
|
|
695
|
-
force: opts['force'],
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
catch (error) {
|
|
699
|
-
console.error(chalk.red('Error generating subagent:'), sanitizeError(error));
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Create transform command
|
|
706
|
-
*/
|
|
707
|
-
export function createTransformCommand() {
|
|
708
|
-
return new Command('transform')
|
|
709
|
-
.description('Upgrade existing skill with subagent configuration')
|
|
710
|
-
.argument('[path]', 'Path to skill directory', '.')
|
|
711
|
-
.option('--dry-run', 'Preview what would be generated')
|
|
712
|
-
.option('--force', 'Overwrite existing subagent')
|
|
713
|
-
.option('--batch', 'Process directory of skills')
|
|
714
|
-
.option('--tools <tools>', 'Override detected tools (comma-separated)')
|
|
715
|
-
.option('--model <model>', 'Model for subagent: sonnet|opus|haiku', 'sonnet')
|
|
716
|
-
.action(async (skillPath, opts) => {
|
|
717
|
-
try {
|
|
718
|
-
await transformSkill(skillPath, {
|
|
719
|
-
dryRun: opts['dryRun'],
|
|
720
|
-
force: opts['force'],
|
|
721
|
-
batch: opts['batch'],
|
|
722
|
-
tools: opts['tools'],
|
|
723
|
-
model: opts['model'],
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
catch (error) {
|
|
727
|
-
console.error(chalk.red('Error transforming skill:'), sanitizeError(error));
|
|
728
|
-
process.exit(1);
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* SMI-1433: Initialize a new MCP server project
|
|
734
|
-
*/
|
|
735
|
-
async function initMcpServer(name, options) {
|
|
736
|
-
// Interactive prompts if name not provided
|
|
737
|
-
const serverName = name ||
|
|
738
|
-
(await input({
|
|
739
|
-
message: 'MCP server name:',
|
|
740
|
-
validate: (value) => {
|
|
741
|
-
if (!value.trim())
|
|
742
|
-
return 'Name is required';
|
|
743
|
-
if (!/^[a-z][a-z0-9-]*$/.test(value)) {
|
|
744
|
-
return 'Name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens';
|
|
745
|
-
}
|
|
746
|
-
return true;
|
|
747
|
-
},
|
|
748
|
-
}));
|
|
749
|
-
const description = await input({
|
|
750
|
-
message: 'Description:',
|
|
751
|
-
default: `An MCP server for ${serverName}`,
|
|
752
|
-
});
|
|
753
|
-
const author = await input({
|
|
754
|
-
message: 'Author:',
|
|
755
|
-
default: process.env['USER'] || 'author',
|
|
756
|
-
});
|
|
757
|
-
// Parse initial tools if provided
|
|
758
|
-
const initialTools = [];
|
|
759
|
-
const toolNameRegex = /^[a-z][a-z0-9_-]*$/;
|
|
760
|
-
if (options.tools) {
|
|
761
|
-
const toolNames = options.tools
|
|
762
|
-
.split(',')
|
|
763
|
-
.map((t) => t.trim())
|
|
764
|
-
.filter((t) => t.length > 0);
|
|
765
|
-
for (const toolName of toolNames) {
|
|
766
|
-
if (!toolNameRegex.test(toolName)) {
|
|
767
|
-
console.log(chalk.red(`Invalid tool name: ${toolName}. Must be lowercase, start with a letter, and contain only letters, numbers, underscores, and hyphens.`));
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
initialTools.push({
|
|
771
|
-
name: toolName,
|
|
772
|
-
description: `${toolName} tool`,
|
|
773
|
-
parameters: [],
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
// Ask about tools if none specified
|
|
778
|
-
if (initialTools.length === 0) {
|
|
779
|
-
const addTools = await confirm({
|
|
780
|
-
message: 'Would you like to define initial tools interactively?',
|
|
781
|
-
default: false,
|
|
782
|
-
});
|
|
783
|
-
if (addTools) {
|
|
784
|
-
let addMore = true;
|
|
785
|
-
while (addMore) {
|
|
786
|
-
const toolName = await input({
|
|
787
|
-
message: 'Tool name:',
|
|
788
|
-
validate: (value) => {
|
|
789
|
-
if (!value.trim())
|
|
790
|
-
return 'Tool name is required';
|
|
791
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(value)) {
|
|
792
|
-
return 'Tool name must be lowercase with letters, numbers, underscores, and hyphens';
|
|
793
|
-
}
|
|
794
|
-
return true;
|
|
795
|
-
},
|
|
796
|
-
});
|
|
797
|
-
const toolDescription = await input({
|
|
798
|
-
message: 'Tool description:',
|
|
799
|
-
default: `${toolName} tool`,
|
|
800
|
-
});
|
|
801
|
-
initialTools.push({
|
|
802
|
-
name: toolName,
|
|
803
|
-
description: toolDescription,
|
|
804
|
-
parameters: [],
|
|
805
|
-
});
|
|
806
|
-
addMore = await confirm({
|
|
807
|
-
message: 'Add another tool?',
|
|
808
|
-
default: false,
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
const targetDir = options.output ? resolve(options.output) : resolve('.', serverName);
|
|
814
|
-
// Check if directory already exists
|
|
815
|
-
try {
|
|
816
|
-
await stat(targetDir);
|
|
817
|
-
if (!options.force) {
|
|
818
|
-
const overwrite = await confirm({
|
|
819
|
-
message: `Directory ${targetDir} already exists. Overwrite?`,
|
|
820
|
-
default: false,
|
|
821
|
-
});
|
|
822
|
-
if (!overwrite) {
|
|
823
|
-
console.log(chalk.yellow('Initialization cancelled'));
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
catch {
|
|
829
|
-
// Directory doesn't exist, continue
|
|
830
|
-
}
|
|
831
|
-
const spinner = ora('Creating MCP server...').start();
|
|
832
|
-
try {
|
|
833
|
-
// Generate templates
|
|
834
|
-
const files = renderMcpServerTemplates({
|
|
835
|
-
name: serverName,
|
|
836
|
-
description,
|
|
837
|
-
tools: initialTools,
|
|
838
|
-
author,
|
|
839
|
-
});
|
|
840
|
-
// Create directory structure
|
|
841
|
-
await mkdir(targetDir, { recursive: true });
|
|
842
|
-
await mkdir(join(targetDir, 'src'), { recursive: true });
|
|
843
|
-
await mkdir(join(targetDir, 'src', 'tools'), { recursive: true });
|
|
844
|
-
// Write all files
|
|
845
|
-
for (const [filePath, content] of files) {
|
|
846
|
-
const fullPath = join(targetDir, filePath);
|
|
847
|
-
const dir = dirname(fullPath);
|
|
848
|
-
await mkdir(dir, { recursive: true });
|
|
849
|
-
await writeFile(fullPath, content, 'utf-8');
|
|
850
|
-
}
|
|
851
|
-
spinner.succeed(`Created MCP server at ${targetDir}`);
|
|
852
|
-
console.log(chalk.bold('\nNext steps:'));
|
|
853
|
-
console.log(chalk.dim(` 1. cd ${targetDir}`));
|
|
854
|
-
console.log(chalk.dim(' 2. npm install'));
|
|
855
|
-
console.log(chalk.dim(' 3. npm run dev # Run in development mode'));
|
|
856
|
-
console.log(chalk.dim(' 4. Edit src/tools/ to add your tool implementations'));
|
|
857
|
-
console.log();
|
|
858
|
-
console.log(chalk.bold('Configure in Claude Code:'));
|
|
859
|
-
console.log(chalk.cyan('─'.repeat(50)));
|
|
860
|
-
console.log(chalk.dim(`Add to ~/.claude/settings.json:`));
|
|
861
|
-
console.log(chalk.white(`{
|
|
862
|
-
"mcpServers": {
|
|
863
|
-
"${serverName}": {
|
|
864
|
-
"command": "npx",
|
|
865
|
-
"args": ["tsx", "${join(targetDir, 'src', 'index.ts')}"]
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}`));
|
|
869
|
-
console.log(chalk.cyan('─'.repeat(50)));
|
|
870
|
-
console.log();
|
|
871
|
-
}
|
|
872
|
-
catch (error) {
|
|
873
|
-
spinner.fail(`Failed to create MCP server: ${sanitizeError(error)}`);
|
|
874
|
-
throw error;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
/**
|
|
878
|
-
* Create mcp-init command
|
|
879
|
-
*/
|
|
880
|
-
export function createMcpInitCommand() {
|
|
881
|
-
return new Command('mcp-init')
|
|
882
|
-
.description('Scaffold a new MCP server project')
|
|
883
|
-
.argument('[name]', 'MCP server name')
|
|
884
|
-
.option('-o, --output <path>', 'Output directory')
|
|
885
|
-
.option('--tools <tools>', 'Initial tools (comma-separated)')
|
|
886
|
-
.option('--force', 'Overwrite existing directory')
|
|
887
|
-
.action(async (name, opts) => {
|
|
888
|
-
try {
|
|
889
|
-
await initMcpServer(name, {
|
|
890
|
-
output: opts['output'],
|
|
891
|
-
tools: opts['tools'],
|
|
892
|
-
force: opts['force'],
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
catch (error) {
|
|
896
|
-
console.error(chalk.red('Error creating MCP server:'), sanitizeError(error));
|
|
897
|
-
process.exit(1);
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
export { initSkill, validateSkill, publishSkill, generateSubagent, transformSkill, initMcpServer };
|
|
902
|
-
//# sourceMappingURL=author.js.map
|