Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/skillkit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
AgentListCommand,
AgentShowCommand,
AgentCreateCommand,
AgentFromSkillCommand,
AgentTranslateCommand,
AgentSyncCommand,
AgentValidateCommand,
Expand Down Expand Up @@ -153,6 +154,7 @@ cli.register(AgentCommand);
cli.register(AgentListCommand);
cli.register(AgentShowCommand);
cli.register(AgentCreateCommand);
cli.register(AgentFromSkillCommand);
cli.register(AgentTranslateCommand);
cli.register(AgentSyncCommand);
cli.register(AgentValidateCommand);
Expand Down
188 changes: 181 additions & 7 deletions packages/cli/src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import chalk from 'chalk';
import { Command, Option } from 'clipanion';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { join, basename } from 'node:path';
import { homedir } from 'node:os';
import {
findAllAgents,
Expand All @@ -17,8 +17,14 @@ import {
validateAgent,
translateAgent,
getAgentTargetDirectory,
discoverSkills,
readSkillContent,
generateSubagentFromSkill,
type CustomAgent,
type AgentType,
type Skill,
type AgentPermissionMode,
type SkillToSubagentOptions,
} from '@skillkit/core';
import {
getBundledAgents,
Expand All @@ -43,17 +49,19 @@ export class AgentCommand extends Command {
that can be invoked with @mentions or the --agent flag.
Sub-commands:
agent list - List all installed agents
agent show - Show agent details
agent create - Create a new agent
agent translate - Translate agents between formats
agent sync - Sync agents to target AI agent
agent validate - Validate agent definitions
agent list - List all installed agents
agent show - Show agent details
agent create - Create a new agent
agent from-skill - Convert a skill to a subagent
agent translate - Translate agents between formats
agent sync - Sync agents to target AI agent
agent validate - Validate agent definitions
`,
examples: [
['List all agents', '$0 agent list'],
['Show agent details', '$0 agent show architect'],
['Create new agent', '$0 agent create security-reviewer'],
['Convert skill to subagent', '$0 agent from-skill code-simplifier'],
['Translate to Cursor format', '$0 agent translate --to cursor'],
['Sync agents', '$0 agent sync --agent claude-code'],
],
Expand All @@ -64,6 +72,7 @@ export class AgentCommand extends Command {
console.log(' agent list List all installed agents');
console.log(' agent show <name> Show agent details');
console.log(' agent create <name> Create a new agent');
console.log(' agent from-skill <name> Convert a skill to a subagent');
console.log(' agent translate Translate agents between formats');
console.log(' agent sync Sync agents to target AI agent');
console.log(' agent validate [path] Validate agent definitions');
Expand Down Expand Up @@ -831,9 +840,174 @@ export class AgentAvailableCommand extends Command {
}
}

export class AgentFromSkillCommand extends Command {
static override paths = [['agent', 'from-skill']];

static override usage = Command.Usage({
description: 'Convert a skill into a Claude Code subagent',
details: `
Converts a SkillKit skill into a Claude Code native subagent format.
The generated .md file can be used with @mentions in Claude Code.
By default, the subagent references the skill (skills: [skill-name]).
Use --inline to embed the full skill content in the system prompt.
`,
examples: [
['Convert skill to subagent', '$0 agent from-skill code-simplifier'],
['Create global subagent', '$0 agent from-skill code-simplifier --global'],
['Embed skill content inline', '$0 agent from-skill code-simplifier --inline'],
['Set model for subagent', '$0 agent from-skill code-simplifier --model opus'],
['Preview without writing', '$0 agent from-skill code-simplifier --dry-run'],
],
});

skillName = Option.String({ required: true });

inline = Option.Boolean('--inline,-i', false, {
description: 'Embed full skill content in system prompt',
});

model = Option.String('--model,-m', {
description: 'Model to use (sonnet, opus, haiku, inherit)',
});

permission = Option.String('--permission,-p', {
description: 'Permission mode (default, plan, auto-edit, full-auto, bypassPermissions)',
});

global = Option.Boolean('--global,-g', false, {
description: 'Create in ~/.claude/agents/ instead of .claude/agents/',
});

output = Option.String('--output,-o', {
description: 'Custom output filename (without .md)',
});

dryRun = Option.Boolean('--dry-run,-n', false, {
description: 'Preview without writing files',
});

async execute(): Promise<number> {
const skills = discoverSkills(process.cwd());
const skill = skills.find((s: Skill) => s.name === this.skillName);

if (!skill) {
console.log(chalk.red(`Skill not found: ${this.skillName}`));
console.log(chalk.dim('Available skills:'));
for (const s of skills.slice(0, 10)) {
console.log(chalk.dim(` - ${s.name}`));
}
if (skills.length > 10) {
console.log(chalk.dim(` ... and ${skills.length - 10} more`));
}
return 1;
}

const skillContent = readSkillContent(skill.path);
if (!skillContent) {
console.log(chalk.red(`Could not read skill content: ${skill.path}`));
return 1;
}

const options: SkillToSubagentOptions = {
inline: this.inline,
};

if (this.model) {
const validModels = ['sonnet', 'opus', 'haiku', 'inherit'];
if (!validModels.includes(this.model)) {
console.log(chalk.red(`Invalid model: ${this.model}`));
console.log(chalk.dim(`Valid options: ${validModels.join(', ')}`));
return 1;
}
options.model = this.model as 'sonnet' | 'opus' | 'haiku' | 'inherit';
}

if (this.permission) {
const validModes = ['default', 'plan', 'auto-edit', 'full-auto', 'bypassPermissions'];
if (!validModes.includes(this.permission)) {
console.log(chalk.red(`Invalid permission mode: ${this.permission}`));
console.log(chalk.dim(`Valid options: ${validModes.join(', ')}`));
return 1;
}
options.permissionMode = this.permission as AgentPermissionMode;
}

const content = generateSubagentFromSkill(skill, skillContent, options);

const targetDir = this.global
? join(homedir(), '.claude', 'agents')
: join(process.cwd(), '.claude', 'agents');

let filename: string;
if (this.output) {
const sanitized = sanitizeFilename(this.output);
if (!sanitized) {
console.log(chalk.red(`Invalid output filename: ${this.output}`));
console.log(chalk.dim('Filename must contain only alphanumeric characters, hyphens, and underscores'));
return 1;
}
filename = `${sanitized}.md`;
} else {
filename = `${skill.name}.md`;
}

const outputPath = join(targetDir, filename);

if (this.dryRun) {
console.log(chalk.cyan('Preview (dry run):\n'));
console.log(chalk.dim(`Would write to: ${outputPath}`));
console.log(chalk.dim('─'.repeat(50)));
console.log(content);
console.log(chalk.dim('─'.repeat(50)));
return 0;
}

if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}

if (existsSync(outputPath)) {
console.log(chalk.yellow(`Overwriting existing file: ${outputPath}`));
}

writeFileSync(outputPath, content);

console.log(chalk.green(`Created subagent: ${outputPath}`));
console.log();
console.log(chalk.dim(`Invoke with: @${skill.name}`));
if (!this.inline) {
console.log(chalk.dim(`Skills referenced: ${skill.name}`));
} else {
console.log(chalk.dim('Skill content embedded inline'));
}

return 0;
}
}

function formatCategoryName(category: string): string {
return category
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}

function sanitizeFilename(input: string): string | null {
const base = basename(input);
const stem = base.replace(/\.md$/i, '');

if (!stem || stem.startsWith('.') || stem.startsWith('-')) {
return null;
}

if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(stem)) {
return null;
}

if (stem.length > 64) {
return null;
}

return stem;
}
1 change: 1 addition & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export {
AgentListCommand,
AgentShowCommand,
AgentCreateCommand,
AgentFromSkillCommand,
AgentTranslateCommand,
AgentSyncCommand,
AgentValidateCommand,
Expand Down
Loading