Initial commit

CLI tool to validate skill.md files against the agent skill specification.
Supports local files and remote URLs, with human-readable and JSON output.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ethan Beard
2026-02-02 15:27:07 -08:00
commit 67af4e2125
13 changed files with 920 additions and 0 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
dist/
*.log
.DS_Store
.env
.claude/
coverage/
.nyc_output/

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

113
README.md Normal file
View File

@@ -0,0 +1,113 @@
# skill-md-validator
Validate `skill.md` files against the agent skill specification.
## What is skill.md?
Agents share capabilities via `skill.md` files — markdown documents with YAML frontmatter describing what a skill does, how to use it, and metadata for discovery. This tool validates those files to catch errors before publishing.
## Install
```bash
npm install -g skill-md-validator
```
## Usage
```bash
# Validate local file
skill-validate ./skill.md
# Validate remote URL
skill-validate https://api.example.com/skill.md
# Strict mode (warnings become errors)
skill-validate --strict ./skill.md
# Output JSON (for programmatic use)
skill-validate --json ./skill.md
```
## Exit Codes
| Code | Meaning |
|------|---------|
| `0` | Valid (may have warnings) |
| `1` | Invalid (has errors) |
| `2` | File not found / fetch failed |
## Validation Rules
### Required Fields
| Field | Format | Description |
|-------|--------|-------------|
| `name` | `^[a-z][a-z0-9-]*$` | Lowercase alphanumeric with hyphens |
| `version` | semver | e.g., `1.0.0` |
| `description` | 10-200 chars | One-line description |
### Optional Fields
| Field | Format | Description |
|-------|--------|-------------|
| `homepage` | URL | Project homepage |
| `repository` | URL | Source repository |
| `author` | string | Author name or handle |
| `license` | string | SPDX license identifier |
| `metadata` | object | Arbitrary key-value pairs |
### Content Checks
- Body after frontmatter must not be empty
- Should have at least one markdown heading
- Warns if missing code examples
- Warns if missing `## Install` or `## Usage` sections
## Example Output
```
✓ skill.md is valid
name: my-skill
version: 1.2.0
description: Does something useful for agents...
Warnings:
⚠ Missing 'repository' field
⚠ No "## Install" section found
```
## JSON Output
```json
{
"valid": true,
"file": "./skill.md",
"errors": [],
"warnings": [
{"code": "missing-repository", "message": "Missing 'repository' field"}
],
"parsed": {
"name": "my-skill",
"version": "1.2.0",
"description": "Does something useful for agents..."
}
}
```
## Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Run locally
npm start ./skill.md
```
## License
MIT

196
package-lock.json generated Normal file
View File

@@ -0,0 +1,196 @@
{
"name": "skill-md-validator",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "skill-md-validator",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"commander": "^12.1.0",
"gray-matter": "^4.0.3",
"semver": "^7.6.3"
},
"bin": {
"skill-validate": "dist/index.js"
},
"devDependencies": {
"@types/node": "^20.14.0",
"@types/semver": "^7.5.8",
"typescript": "^5.5.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@types/node": {
"version": "20.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

45
package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "skill-md-validator",
"version": "1.0.0",
"description": "Validate skill.md files against the agent skill specification",
"main": "dist/index.js",
"bin": {
"skill-validate": "dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js",
"prepublishOnly": "npm run build"
},
"keywords": [
"skill",
"markdown",
"validator",
"agent",
"ai"
],
"author": "",
"license": "MIT",
"dependencies": {
"commander": "^12.1.0",
"gray-matter": "^4.0.3",
"semver": "^7.6.3"
},
"devDependencies": {
"@types/node": "^20.14.0",
"@types/semver": "^7.5.8",
"typescript": "^5.5.0"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/severith/skill-md-validator.git"
},
"homepage": "https://github.com/severith/skill-md-validator"
}

41
skill.md Normal file
View File

@@ -0,0 +1,41 @@
---
name: skill-md-validator
version: 1.0.0
description: Validate skill.md files against the agent skill specification
homepage: https://github.com/severith/skill-md-validator
repository: https://github.com/severith/skill-md-validator.git
license: MIT
metadata:
emoji: ✅
category: tooling
---
# skill-md-validator
Validate your skill.md files before publishing.
## Install
```bash
npm install -g skill-md-validator
```
## Usage
```bash
# Local file
skill-validate ./skill.md
# Remote URL
skill-validate https://example.com/skill.md
# Strict mode (warnings become errors)
skill-validate --strict ./skill.md
# JSON output
skill-validate --json ./skill.md
```
## Why
skill.md files are how agents discover and understand capabilities. A malformed file means agents can't find you or misunderstand what you offer. Validate before you ship.

23
src/fetcher.ts Normal file
View File

@@ -0,0 +1,23 @@
export async function fetchSkillContent(url: string): Promise<string> {
const response = await fetch(url, {
headers: {
'Accept': 'text/plain, text/markdown, */*',
'User-Agent': 'skill-md-validator/1.0.0',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}
return response.text();
}
export function isUrl(input: string): boolean {
try {
const url = new URL(input);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}

81
src/index.ts Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { validateSkillInput } from './validator.js';
import { ValidationResult } from './types.js';
const program = new Command();
program
.name('skill-validate')
.description('Validate skill.md files against the agent skill specification')
.version('1.0.0')
.argument('<file>', 'Path to skill.md file or URL')
.option('-s, --strict', 'Treat warnings as errors')
.option('-j, --json', 'Output results as JSON')
.action(async (file: string, options: { strict?: boolean; json?: boolean }) => {
const result = await validateSkillInput(file, { strict: options.strict });
// In strict mode, warnings become errors
if (options.strict && result.warnings.length > 0) {
result.errors.push(...result.warnings);
result.warnings = [];
result.valid = result.errors.length === 0;
}
if (options.json) {
outputJson(result);
} else {
outputHuman(result);
}
// Exit codes: 0 = valid, 1 = invalid, 2 = file not found/fetch failed
if (!result.valid) {
const isFetchOrFileError = result.errors.some(
(e) => e.code === 'file-not-found' || e.code === 'fetch-failed'
);
process.exit(isFetchOrFileError ? 2 : 1);
}
});
function outputJson(result: ValidationResult): void {
console.log(JSON.stringify(result, null, 2));
}
function outputHuman(result: ValidationResult): void {
if (result.valid) {
console.log(`${result.file} is valid\n`);
} else {
console.log(`${result.file} is invalid\n`);
}
if (result.parsed) {
console.log(` name: ${result.parsed.name}`);
console.log(` version: ${result.parsed.version}`);
console.log(` description: ${truncate(result.parsed.description, 50)}`);
console.log();
}
if (result.errors.length > 0) {
console.log(' Errors:');
for (const error of result.errors) {
console.log(`${error.message}`);
}
console.log();
}
if (result.warnings.length > 0) {
console.log(' Warnings:');
for (const warning of result.warnings) {
console.log(`${warning.message}`);
}
console.log();
}
}
function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + '...';
}
program.parse();

191
src/rules.ts Normal file
View File

@@ -0,0 +1,191 @@
import * as semver from 'semver';
import { ValidationIssue, SkillMetadata } from './types.js';
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
const MIN_DESCRIPTION_LENGTH = 10;
const MAX_DESCRIPTION_LENGTH = 200;
const WARN_DESCRIPTION_LENGTH = 100;
export function validateName(data: Record<string, unknown>): ValidationIssue | null {
if (!data.name) {
return { code: 'missing-name', message: "Missing required field 'name'" };
}
if (typeof data.name !== 'string') {
return { code: 'invalid-name', message: "'name' must be a string" };
}
if (!NAME_PATTERN.test(data.name)) {
return {
code: 'invalid-name-format',
message: "'name' must be lowercase alphanumeric with hyphens (e.g., 'my-skill')",
};
}
return null;
}
export function validateVersion(data: Record<string, unknown>): ValidationIssue | null {
if (!data.version) {
return { code: 'missing-version', message: "Missing required field 'version'" };
}
if (typeof data.version !== 'string') {
return { code: 'invalid-version', message: "'version' must be a string" };
}
if (!semver.valid(data.version)) {
return {
code: 'invalid-version-format',
message: `'version' must be valid semver (e.g., '1.0.0'), got '${data.version}'`,
};
}
return null;
}
export function validateDescription(data: Record<string, unknown>): {
error: ValidationIssue | null;
warning: ValidationIssue | null;
} {
if (!data.description) {
return {
error: { code: 'missing-description', message: "Missing required field 'description'" },
warning: null,
};
}
if (typeof data.description !== 'string') {
return {
error: { code: 'invalid-description', message: "'description' must be a string" },
warning: null,
};
}
if (data.description.length < MIN_DESCRIPTION_LENGTH) {
return {
error: {
code: 'description-too-short',
message: `'description' must be at least ${MIN_DESCRIPTION_LENGTH} characters`,
},
warning: null,
};
}
if (data.description.length > MAX_DESCRIPTION_LENGTH) {
return {
error: {
code: 'description-too-long',
message: `'description' must be at most ${MAX_DESCRIPTION_LENGTH} characters`,
},
warning: null,
};
}
if (data.description.length > WARN_DESCRIPTION_LENGTH) {
return {
error: null,
warning: {
code: 'description-truncation-risk',
message: `'description' over ${WARN_DESCRIPTION_LENGTH} chars may be truncated in some displays`,
},
};
}
return { error: null, warning: null };
}
export function validateUrl(value: unknown, field: string): ValidationIssue | null {
if (value === undefined) {
return null;
}
if (typeof value !== 'string') {
return { code: `invalid-${field}`, message: `'${field}' must be a string` };
}
try {
new URL(value);
return null;
} catch {
return { code: `invalid-${field}-url`, message: `'${field}' must be a valid URL` };
}
}
export function validateHomepage(data: Record<string, unknown>): {
error: ValidationIssue | null;
warning: ValidationIssue | null;
} {
const error = validateUrl(data.homepage, 'homepage');
if (error) {
return { error, warning: null };
}
if (!data.homepage) {
return {
error: null,
warning: { code: 'missing-homepage', message: "Missing 'homepage' field" },
};
}
return { error: null, warning: null };
}
export function validateRepository(data: Record<string, unknown>): {
error: ValidationIssue | null;
warning: ValidationIssue | null;
} {
const error = validateUrl(data.repository, 'repository');
if (error) {
return { error, warning: null };
}
if (!data.repository) {
return {
error: null,
warning: { code: 'missing-repository', message: "Missing 'repository' field" },
};
}
return { error: null, warning: null };
}
export function validateApiBase(data: Record<string, unknown>): ValidationIssue | null {
const metadata = data.metadata as Record<string, unknown> | undefined;
if (!metadata?.api_base) {
return null;
}
return validateUrl(metadata.api_base, 'metadata.api_base');
}
export function validateContent(content: string): {
errors: ValidationIssue[];
warnings: ValidationIssue[];
} {
const errors: ValidationIssue[] = [];
const warnings: ValidationIssue[] = [];
const trimmed = content.trim();
if (!trimmed) {
errors.push({ code: 'empty-content', message: 'Body content after frontmatter is empty' });
return { errors, warnings };
}
if (!/^#+\s+.+/m.test(content)) {
warnings.push({ code: 'no-heading', message: 'No markdown heading found in body' });
}
if (!/```[\s\S]*?```/.test(content)) {
warnings.push({ code: 'no-code-examples', message: 'No code examples found in body' });
}
if (!/^##\s+(Install|Installation)/im.test(content)) {
warnings.push({ code: 'no-install-section', message: 'No "## Install" section found' });
}
if (!/^##\s+Usage/im.test(content)) {
warnings.push({ code: 'no-usage-section', message: 'No "## Usage" section found' });
}
return { errors, warnings };
}
export function extractMetadata(data: Record<string, unknown>): SkillMetadata | null {
if (!data.name || !data.version || !data.description) {
return null;
}
return {
name: data.name as string,
version: data.version as string,
description: data.description as string,
homepage: data.homepage as string | undefined,
repository: data.repository as string | undefined,
author: data.author as string | undefined,
license: data.license as string | undefined,
metadata: data.metadata as Record<string, unknown> | undefined,
};
}

28
src/types.ts Normal file
View File

@@ -0,0 +1,28 @@
export interface ValidationIssue {
code: string;
message: string;
line?: number;
}
export interface SkillMetadata {
name: string;
version: string;
description: string;
homepage?: string;
repository?: string;
author?: string;
license?: string;
metadata?: Record<string, unknown>;
}
export interface ValidationResult {
valid: boolean;
file: string;
errors: ValidationIssue[];
warnings: ValidationIssue[];
parsed: SkillMetadata | null;
}
export interface ValidatorOptions {
strict?: boolean;
}

139
src/validator.ts Normal file
View File

@@ -0,0 +1,139 @@
import matter from 'gray-matter';
import { ValidationResult, ValidationIssue, ValidatorOptions } from './types.js';
import {
validateName,
validateVersion,
validateDescription,
validateHomepage,
validateRepository,
validateApiBase,
validateContent,
extractMetadata,
} from './rules.js';
import { fetchSkillContent, isUrl } from './fetcher.js';
import * as fs from 'fs';
import * as path from 'path';
export function validateSkill(
content: string,
filename: string,
_options: ValidatorOptions = {}
): ValidationResult {
const errors: ValidationIssue[] = [];
const warnings: ValidationIssue[] = [];
// Check for frontmatter markers
if (!content.startsWith('---')) {
errors.push({
code: 'missing-frontmatter',
message: 'File must start with YAML frontmatter (---)',
});
return { valid: false, file: filename, errors, warnings, parsed: null };
}
// Parse frontmatter
let parsed: matter.GrayMatterFile<string>;
try {
parsed = matter(content);
} catch (err) {
errors.push({
code: 'invalid-frontmatter',
message: `Failed to parse frontmatter: ${err instanceof Error ? err.message : 'Unknown error'}`,
});
return { valid: false, file: filename, errors, warnings, parsed: null };
}
const data = parsed.data as Record<string, unknown>;
// Validate required fields
const nameError = validateName(data);
if (nameError) errors.push(nameError);
const versionError = validateVersion(data);
if (versionError) errors.push(versionError);
const descResult = validateDescription(data);
if (descResult.error) errors.push(descResult.error);
if (descResult.warning) warnings.push(descResult.warning);
// Validate optional URL fields
const homepageResult = validateHomepage(data);
if (homepageResult.error) errors.push(homepageResult.error);
if (homepageResult.warning) warnings.push(homepageResult.warning);
const repoResult = validateRepository(data);
if (repoResult.error) errors.push(repoResult.error);
if (repoResult.warning) warnings.push(repoResult.warning);
const apiBaseError = validateApiBase(data);
if (apiBaseError) errors.push(apiBaseError);
// Validate content body
const contentResult = validateContent(parsed.content);
errors.push(...contentResult.errors);
warnings.push(...contentResult.warnings);
// Extract metadata for result
const metadata = extractMetadata(data);
return {
valid: errors.length === 0,
file: filename,
errors,
warnings,
parsed: metadata,
};
}
export async function validateSkillFile(
filepath: string,
options: ValidatorOptions = {}
): Promise<ValidationResult> {
const absolutePath = path.resolve(filepath);
if (!fs.existsSync(absolutePath)) {
return {
valid: false,
file: filepath,
errors: [{ code: 'file-not-found', message: `File not found: ${filepath}` }],
warnings: [],
parsed: null,
};
}
const content = fs.readFileSync(absolutePath, 'utf-8');
return validateSkill(content, filepath, options);
}
export async function validateSkillUrl(
url: string,
options: ValidatorOptions = {}
): Promise<ValidationResult> {
try {
const content = await fetchSkillContent(url);
return validateSkill(content, url, options);
} catch (err) {
return {
valid: false,
file: url,
errors: [
{
code: 'fetch-failed',
message: err instanceof Error ? err.message : 'Failed to fetch URL',
},
],
warnings: [],
parsed: null,
};
}
}
export async function validateSkillInput(
input: string,
options: ValidatorOptions = {}
): Promise<ValidationResult> {
if (isUrl(input)) {
return validateSkillUrl(input, options);
}
return validateSkillFile(input, options);
}

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}