Skip to content

Commit 8496e3c

Browse files
authored
feat(publish): add RFC 8615 well-known skills support (#36)
* feat(agents): add skill-to-subagent converter command Add `skillkit agent from-skill` command to convert SkillKit skills into Claude Code native subagent format (.md files in .claude/agents/). Features: - Reference mode (default): generates subagent with `skills: [skill-name]` - Inline mode (--inline): embeds full skill content in system prompt - Options: --model, --permission, --global, --output, --dry-run New files: - packages/core/src/agents/skill-converter.ts - packages/core/src/agents/__tests__/skill-converter.test.ts (23 tests) Closes #22 * feat(publish): add RFC 8615 well-known skills support Redesign publish workflow to align with industry standard: - Add WellKnownProvider for auto-discovery from any domain - skillkit add https://example.com discovers skills via /.well-known/skills/ - skillkit publish generates well-known hosting structure - skillkit publish submit opens GitHub issue (legacy workflow) Provider discovers skills from: - /.well-known/skills/index.json (manifest) - /.well-known/skills/{skill-name}/SKILL.md (skill files) Includes 16 tests for WellKnownProvider and structure generation. * fix(wellknown): correct baseSkillsUrl calculation for skills.json fallback When using the /.well-known/skills.json fallback URL, the previous logic produced a malformed URL with duplicated .well-known path segment: - Before: https://example.com/.well-known/.well-known/skills - After: https://example.com/.well-known/skills Extract calculateBaseSkillsUrl helper function for better testability and add 4 new unit tests covering both URL formats. * docs: update publish command documentation for well-known skills - README.md: Add self-hosting section with well-known URI structure - commands.mdx: Add Publishing Commands section - marketplace.mdx: Expand publish section with self-hosting instructions - skills.mdx: Add self-hosting workflow documentation Documents the new `skillkit publish` workflow that generates RFC 8615 well-known URI structures for decentralized skill hosting. * fix(security): address path traversal and CRLF vulnerabilities - agent.ts: Sanitize skill.name when building default filename - publish.ts: Add sanitizeSkillName() to validate skill names before using in file paths, verify resolved paths stay within target dir - skill-converter.ts: Normalize CRLF to LF before extracting body content to handle Windows-style line endings - wellknown.ts: Add sanitizeSkillName() to validate remote skill names, verify resolved paths stay within temp dir, encode URL components * fix(agents): improve YAML string escaping logic Only escape strings that actually need it for valid YAML parsing: - Strings containing newlines or colons - Strings starting with special YAML characters (-, *, &, !, {, [, >, |, @, `) - Strings starting with quotes This fixes unnecessary escaping of strings like "code-quality" where hyphens in the middle don't need escaping. * fix(wellknown): address security review feedback - Use path.sep instead of hardcoded '/' for cross-platform compatibility - Add sanitization and path containment check in generateWellKnownStructure - Remove unused 'vi' import in wellknown.test.ts
1 parent 847af0f commit 8496e3c

File tree

14 files changed

+863
-105
lines changed

14 files changed

+863
-105
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,31 @@ skillkit methodology load # Load a methodology
428428
### Publishing & Sharing
429429

430430
```bash
431-
skillkit publish # Publish skill to marketplace
431+
skillkit publish # Generate well-known hosting structure
432+
skillkit publish submit # Submit to SkillKit marketplace
432433
skillkit create # Create new skill
433434
skillkit init # Initialize in project
434435
```
435436

437+
#### Self-Hosting Skills (RFC 8615)
438+
439+
Generate a well-known URI structure to host skills on your own domain:
440+
441+
```bash
442+
# Generate hosting structure
443+
skillkit publish ./my-skills --output ./public
444+
445+
# Users install from your domain
446+
skillkit add https://your-domain.com
447+
```
448+
449+
This creates:
450+
```
451+
.well-known/skills/
452+
index.json # Skill manifest
453+
my-skill/SKILL.md # Skill files
454+
```
455+
436456
### Configuration
437457

438458
```bash

apps/skillkit/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
AICommand,
4949
AuditCommand,
5050
PublishCommand,
51+
PublishSubmitCommand,
5152
AgentCommand,
5253
AgentListCommand,
5354
AgentShowCommand,
@@ -150,6 +151,7 @@ cli.register(CommandCmd);
150151
cli.register(AICommand);
151152
cli.register(AuditCommand);
152153
cli.register(PublishCommand);
154+
cli.register(PublishSubmitCommand);
153155
cli.register(AgentCommand);
154156
cli.register(AgentListCommand);
155157
cli.register(AgentShowCommand);

docs/fumadocs/content/docs/commands.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,18 @@ skillkit init # Initialize project
233233
skillkit create <name> # Create new skill
234234
skillkit validate [path] # Validate skill format
235235
skillkit read <skills> # Read skill content
236-
skillkit publish # Publish to marketplace
237236
skillkit settings --set key=value # Configure settings
238237
```
239238

239+
## Publishing Commands
240+
241+
```bash
242+
skillkit publish [path] # Generate well-known hosting structure
243+
skillkit publish --output dir # Output to specific directory
244+
skillkit publish --dry-run # Preview without writing
245+
skillkit publish submit # Submit to SkillKit marketplace
246+
```
247+
240248
## Interactive TUI
241249

242250
```bash

docs/fumadocs/content/docs/marketplace.mdx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,39 @@ skillkit install anthropics/skills --agent claude-code,cursor
4848

4949
## Publish Skills
5050

51+
### Self-Host on Your Domain (Recommended)
52+
53+
Generate a well-known URI structure (RFC 8615) to host skills on your own domain:
54+
5155
```bash
56+
# Create and validate your skill
5257
skillkit create my-skill
5358
skillkit validate my-skill
54-
skillkit publish
59+
60+
# Generate hosting structure
61+
skillkit publish ./my-skill --output ./public
62+
```
63+
64+
This creates:
65+
```
66+
.well-known/skills/
67+
index.json # Skill manifest for auto-discovery
68+
my-skill/
69+
SKILL.md # Your skill content
70+
```
71+
72+
Deploy the `.well-known` folder to your web server. Users can then install via:
73+
74+
```bash
75+
skillkit add https://your-domain.com
76+
```
77+
78+
### Submit to SkillKit Marketplace
79+
80+
To submit your skill for inclusion in the central marketplace:
81+
82+
```bash
83+
skillkit publish submit
5584
```
85+
86+
This opens a GitHub issue for review by maintainers.

docs/fumadocs/content/docs/skills.mdx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ Or manually create a `SKILL.md` file following the format above.
9494
# Test locally
9595
skillkit validate ./my-skill
9696

97-
# Publish to marketplace
98-
skillkit publish
97+
# Generate well-known hosting structure for self-hosting
98+
skillkit publish ./my-skill --output ./public
99+
100+
# Or submit to SkillKit marketplace
101+
skillkit publish submit
99102
```
103+
104+
### Self-Hosting
105+
106+
The `skillkit publish` command generates an RFC 8615 well-known URI structure:
107+
108+
```
109+
.well-known/skills/
110+
index.json # Manifest for auto-discovery
111+
my-skill/
112+
SKILL.md # Skill content
113+
```
114+
115+
Deploy to your domain and users can install via `skillkit add https://your-domain.com`.

packages/cli/src/commands/agent.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
isAgentInstalled,
3535
type BundledAgent,
3636
} from '@skillkit/resources';
37-
// Agent discovery uses root directories, not skill directories
3837

3938
export class AgentCommand extends Command {
4039
static override paths = [['agent']];
@@ -263,39 +262,33 @@ export class AgentCreateCommand extends Command {
263262
});
264263

265264
async execute(): Promise<number> {
266-
// Validate agent name format
267265
const namePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
268266
if (!namePattern.test(this.name)) {
269267
console.log(chalk.red('Invalid agent name: must be lowercase alphanumeric with hyphens'));
270268
console.log(chalk.dim('Examples: my-agent, code-reviewer, security-expert'));
271269
return 1;
272270
}
273271

274-
// Determine target directory
275272
let targetDir: string;
276273
if (this.global) {
277274
targetDir = join(homedir(), '.claude', 'agents');
278275
} else {
279276
targetDir = join(process.cwd(), '.claude', 'agents');
280277
}
281278

282-
// Create directory if needed
283279
if (!existsSync(targetDir)) {
284280
mkdirSync(targetDir, { recursive: true });
285281
}
286282

287-
// Check if agent already exists
288283
const agentPath = join(targetDir, `${this.name}.md`);
289284
if (existsSync(agentPath)) {
290285
console.log(chalk.red(`Agent already exists: ${agentPath}`));
291286
return 1;
292287
}
293288

294-
// Generate content
295289
const description = this.description || `${this.name} agent`;
296290
const content = generateAgentTemplate(this.name, description, this.model);
297291

298-
// Write file
299292
writeFileSync(agentPath, content);
300293

301294
console.log(chalk.green(`Created agent: ${agentPath}`));
@@ -359,11 +352,9 @@ export class AgentTranslateCommand extends Command {
359352
const searchDirs = [process.cwd()];
360353
const targetAgent = this.to as AgentType;
361354

362-
// Get agents to translate
363355
let agents: CustomAgent[];
364356

365357
if (this.source) {
366-
// Translate from custom source path
367358
const sourcePath = this.source.startsWith('/')
368359
? this.source
369360
: join(process.cwd(), this.source);
@@ -400,7 +391,6 @@ export class AgentTranslateCommand extends Command {
400391
return 0;
401392
}
402393

403-
// Determine output directory
404394
const outputDir = this.output || getAgentTargetDirectory(process.cwd(), targetAgent);
405395

406396
console.log(chalk.cyan(`Translating ${agents.length} agent(s) to ${targetAgent} format...\n`));
@@ -433,7 +423,6 @@ export class AgentTranslateCommand extends Command {
433423
}
434424
}
435425
} else {
436-
// Create directory if needed
437426
if (!existsSync(outputDir)) {
438427
mkdirSync(outputDir, { recursive: true });
439428
}
@@ -538,12 +527,10 @@ export class AgentValidateCommand extends Command {
538527
let hasErrors = false;
539528

540529
if (this.agentPath) {
541-
// Validate specific path
542530
const result = validateAgent(this.agentPath);
543531
printValidationResult(this.agentPath, result);
544532
hasErrors = !result.valid;
545533
} else if (this.all) {
546-
// Validate all agents
547534
const searchDirs = [process.cwd()];
548535
const agents = findAllAgents(searchDirs);
549536

@@ -568,8 +555,6 @@ export class AgentValidateCommand extends Command {
568555
}
569556
}
570557

571-
// Helper functions
572-
573558
function printAgent(agent: CustomAgent): void {
574559
const status = agent.enabled ? chalk.green('✓') : chalk.red('○');
575560
const name = agent.enabled ? agent.name : chalk.dim(agent.name);
@@ -949,7 +934,13 @@ export class AgentFromSkillCommand extends Command {
949934
}
950935
filename = `${sanitized}.md`;
951936
} else {
952-
filename = `${skill.name}.md`;
937+
const sanitized = sanitizeFilename(skill.name);
938+
if (!sanitized) {
939+
console.log(chalk.red(`Invalid skill name for filename: ${skill.name}`));
940+
console.log(chalk.dim('Skill name must contain only alphanumeric characters, hyphens, and underscores'));
941+
return 1;
942+
}
943+
filename = `${sanitized}.md`;
953944
}
954945

955946
const outputPath = join(targetDir, filename);

packages/cli/src/commands/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export { PlanCommand } from './plan.js';
4343
export { CommandCmd, CommandAvailableCommand, CommandInstallCommand } from './command.js';
4444
export { AICommand } from './ai.js';
4545
export { AuditCommand } from './audit.js';
46-
export { PublishCommand } from './publish.js';
46+
export { PublishCommand, PublishSubmitCommand } from './publish.js';
4747
export {
4848
AgentCommand,
4949
AgentListCommand,

0 commit comments

Comments
 (0)