@kodax-ai/kodax-cli 0.7.38

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +1304 -0
  2. package/LICENSE +191 -0
  3. package/README.md +1167 -0
  4. package/README_CN.md +631 -0
  5. package/dist/builtin/code-review/SKILL.md +63 -0
  6. package/dist/builtin/git-workflow/SKILL.md +84 -0
  7. package/dist/builtin/skill-creator/SKILL.md +122 -0
  8. package/dist/builtin/skill-creator/agents/analyzer.md +12 -0
  9. package/dist/builtin/skill-creator/agents/comparator.md +13 -0
  10. package/dist/builtin/skill-creator/agents/grader.md +13 -0
  11. package/dist/builtin/skill-creator/references/schemas.md +227 -0
  12. package/dist/builtin/skill-creator/scripts/aggregate-benchmark.d.ts +46 -0
  13. package/dist/builtin/skill-creator/scripts/aggregate-benchmark.js +209 -0
  14. package/dist/builtin/skill-creator/scripts/analyze-benchmark.d.ts +46 -0
  15. package/dist/builtin/skill-creator/scripts/analyze-benchmark.js +289 -0
  16. package/dist/builtin/skill-creator/scripts/compare-runs.d.ts +62 -0
  17. package/dist/builtin/skill-creator/scripts/compare-runs.js +333 -0
  18. package/dist/builtin/skill-creator/scripts/generate-review.d.ts +33 -0
  19. package/dist/builtin/skill-creator/scripts/generate-review.js +415 -0
  20. package/dist/builtin/skill-creator/scripts/grade-evals.d.ts +73 -0
  21. package/dist/builtin/skill-creator/scripts/grade-evals.js +405 -0
  22. package/dist/builtin/skill-creator/scripts/improve-description.d.ts +23 -0
  23. package/dist/builtin/skill-creator/scripts/improve-description.js +161 -0
  24. package/dist/builtin/skill-creator/scripts/init-skill.d.ts +14 -0
  25. package/dist/builtin/skill-creator/scripts/init-skill.js +153 -0
  26. package/dist/builtin/skill-creator/scripts/install-skill.d.ts +29 -0
  27. package/dist/builtin/skill-creator/scripts/install-skill.js +176 -0
  28. package/dist/builtin/skill-creator/scripts/package-skill.d.ts +38 -0
  29. package/dist/builtin/skill-creator/scripts/package-skill.js +124 -0
  30. package/dist/builtin/skill-creator/scripts/quick-validate.d.ts +8 -0
  31. package/dist/builtin/skill-creator/scripts/quick-validate.js +166 -0
  32. package/dist/builtin/skill-creator/scripts/run-eval.d.ts +66 -0
  33. package/dist/builtin/skill-creator/scripts/run-eval.js +356 -0
  34. package/dist/builtin/skill-creator/scripts/run-loop.d.ts +49 -0
  35. package/dist/builtin/skill-creator/scripts/run-loop.js +243 -0
  36. package/dist/builtin/skill-creator/scripts/run-trigger-eval.d.ts +58 -0
  37. package/dist/builtin/skill-creator/scripts/run-trigger-eval.js +225 -0
  38. package/dist/builtin/skill-creator/scripts/utils.js +278 -0
  39. package/dist/builtin/tdd/SKILL.md +56 -0
  40. package/dist/index.js +1717 -0
  41. package/dist/kodax_cli.js +1870 -0
  42. package/package.json +122 -0
  43. package/scripts/kodax-bin.cjs +27 -0
  44. package/scripts/production-env.cjs +16 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { ensureDirectory, getDefaultSkillsDir, pathExists } from './utils.js';
7
+
8
+ function titleFromSlug(name) {
9
+ return name
10
+ .split('-')
11
+ .filter(Boolean)
12
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
13
+ .join(' ');
14
+ }
15
+
16
+ export function renderSkillTemplate(name, description) {
17
+ const title = titleFromSlug(name);
18
+ return `---
19
+ name: ${name}
20
+ description: ${description}
21
+ user-invocable: true
22
+ allowed-tools: "Read, Grep, Glob"
23
+ compatibility: "Optimized for KodaX and Agent Skills style directories."
24
+ ---
25
+
26
+ # ${title}
27
+
28
+ Describe the task this skill handles, the trigger boundary, and the expected outcome.
29
+
30
+ ## When to use
31
+
32
+ - Use this skill when the user asks for ${title.toLowerCase()}.
33
+ - Do not use this skill when the request only needs a short explanation or is out of scope.
34
+
35
+ ## Workflow
36
+
37
+ 1. Restate the task boundary in one sentence.
38
+ 2. Gather the minimum context needed to do the work well.
39
+ 3. Execute the task, preferring small, verifiable steps.
40
+ 4. Summarize what changed, what was validated, and any remaining risks.
41
+
42
+ ## References
43
+
44
+ - Put deeper guidance in \`references/\`.
45
+ - Put reusable templates or static files in \`assets/\`.
46
+ - Put repeatable helper scripts in \`scripts/\`.
47
+ `;
48
+ }
49
+
50
+ export function renderEvalTemplate(name) {
51
+ return JSON.stringify({
52
+ skill_name: name,
53
+ evals: [
54
+ {
55
+ id: 1,
56
+ prompt: 'TODO: Add a realistic user request to evaluate this skill.',
57
+ expected_output: 'Describe what a successful result should accomplish.',
58
+ files: [],
59
+ assertions: [],
60
+ },
61
+ ],
62
+ }, null, 2);
63
+ }
64
+
65
+ export async function initSkill(options) {
66
+ const baseDir = path.resolve(options.baseDir ?? getDefaultSkillsDir());
67
+ const skillDir = path.join(baseDir, options.name);
68
+ const description = options.description
69
+ ?? `Describe what ${options.name} does and when it should be used.`;
70
+
71
+ if (await pathExists(skillDir)) {
72
+ if (!options.force) {
73
+ throw new Error(`Skill directory already exists: ${skillDir}`);
74
+ }
75
+ }
76
+
77
+ await ensureDirectory(skillDir);
78
+ await ensureDirectory(path.join(skillDir, 'references'));
79
+ await ensureDirectory(path.join(skillDir, 'assets'));
80
+ await ensureDirectory(path.join(skillDir, 'scripts'));
81
+
82
+ await writeFile(
83
+ path.join(skillDir, 'SKILL.md'),
84
+ renderSkillTemplate(options.name, description),
85
+ 'utf8'
86
+ );
87
+
88
+ if (options.includeEvals !== false) {
89
+ await ensureDirectory(path.join(skillDir, 'evals'));
90
+ await writeFile(
91
+ path.join(skillDir, 'evals', 'evals.json'),
92
+ `${renderEvalTemplate(options.name)}\n`,
93
+ 'utf8'
94
+ );
95
+ }
96
+
97
+ return {
98
+ skillDir,
99
+ created: [
100
+ 'SKILL.md',
101
+ ...(options.includeEvals !== false ? ['evals/evals.json'] : []),
102
+ 'references/',
103
+ 'assets/',
104
+ 'scripts/',
105
+ ],
106
+ };
107
+ }
108
+
109
+ function parseArgs(argv) {
110
+ const args = {
111
+ name: argv[2],
112
+ baseDir: undefined,
113
+ description: undefined,
114
+ force: false,
115
+ includeEvals: true,
116
+ };
117
+
118
+ for (let index = 3; index < argv.length; index += 1) {
119
+ const token = argv[index];
120
+ if ((token === '--path' || token === '--dest') && argv[index + 1]) {
121
+ args.baseDir = argv[++index];
122
+ } else if (token === '--description' && argv[index + 1]) {
123
+ args.description = argv[++index];
124
+ } else if (token === '--force') {
125
+ args.force = true;
126
+ } else if (token === '--no-evals') {
127
+ args.includeEvals = false;
128
+ }
129
+ }
130
+
131
+ return args;
132
+ }
133
+
134
+ async function main() {
135
+ const args = parseArgs(process.argv);
136
+ if (!args.name) {
137
+ console.error('Usage: node scripts/init-skill.js <skill-name> [--path <skills-dir>] [--description <text>] [--force] [--no-evals]');
138
+ process.exit(1);
139
+ }
140
+
141
+ const result = await initSkill(args);
142
+ console.log(`Initialized ${result.skillDir}`);
143
+ }
144
+
145
+ const isDirectRun = process.argv[1]
146
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
147
+
148
+ if (isDirectRun) {
149
+ main().catch((error) => {
150
+ console.error(error instanceof Error ? error.message : String(error));
151
+ process.exit(1);
152
+ });
153
+ }
@@ -0,0 +1,29 @@
1
+ import type { SkillPackageManifest } from './package-skill.js';
2
+
3
+ export interface InstalledSkillResult {
4
+ skillName: string;
5
+ installedTo: string;
6
+ source: string;
7
+ manifest: SkillPackageManifest | null;
8
+ }
9
+
10
+ export function readSkillPackageBuffer(buffer: Uint8Array): {
11
+ skillName: string;
12
+ manifest: SkillPackageManifest | null;
13
+ entries: Array<{ relativePath: string; bytes: Uint8Array }>;
14
+ };
15
+
16
+ export function installSkillArchive(
17
+ archivePath: string,
18
+ options?: { skillsDir?: string; force?: boolean }
19
+ ): Promise<InstalledSkillResult>;
20
+
21
+ export function installSkillDirectory(
22
+ skillDir: string,
23
+ options?: { skillsDir?: string; force?: boolean }
24
+ ): Promise<InstalledSkillResult>;
25
+
26
+ export function installSkill(
27
+ inputPath: string,
28
+ options?: { skillsDir?: string; force?: boolean }
29
+ ): Promise<InstalledSkillResult>;
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { strFromU8, unzipSync } from 'fflate';
7
+ import { validateSkillDirectory } from './quick-validate.js';
8
+ import { getDefaultSkillsDir, parseSkillMarkdown, pathExists } from './utils.js';
9
+
10
+ const PACKAGE_MANIFEST_PATH = '.kodax-package.json';
11
+
12
+ function normalizeArchivePath(relativePath) {
13
+ return relativePath.replace(/\\/g, '/').replace(/^\.?\//, '');
14
+ }
15
+
16
+ function assertSafeArchivePath(relativePath) {
17
+ const normalized = normalizeArchivePath(relativePath);
18
+ if (!normalized || normalized.startsWith('/') || normalized.includes('../')) {
19
+ throw new Error(`Unsafe archive entry path: ${relativePath}`);
20
+ }
21
+ return normalized;
22
+ }
23
+
24
+ function isZipLike(filePath) {
25
+ return /\.(skill|zip)$/i.test(filePath);
26
+ }
27
+
28
+ export function readSkillPackageBuffer(buffer) {
29
+ const archive = unzipSync(buffer);
30
+ const entries = Object.entries(archive).map(([relativePath, bytes]) => ({
31
+ relativePath: assertSafeArchivePath(relativePath),
32
+ bytes,
33
+ }));
34
+
35
+ const skillEntry = entries.find((entry) => entry.relativePath === 'SKILL.md');
36
+ if (!skillEntry) {
37
+ throw new Error('Archive is missing SKILL.md');
38
+ }
39
+
40
+ const parsedSkill = parseSkillMarkdown(strFromU8(skillEntry.bytes));
41
+ const skillName = typeof parsedSkill.frontmatter.name === 'string'
42
+ ? parsedSkill.frontmatter.name.trim()
43
+ : '';
44
+ const description = typeof parsedSkill.frontmatter.description === 'string'
45
+ ? parsedSkill.frontmatter.description.trim()
46
+ : '';
47
+ if (!skillName || !description) {
48
+ throw new Error('Archive SKILL.md is missing required frontmatter fields.');
49
+ }
50
+
51
+ const manifestEntry = entries.find((entry) => entry.relativePath === PACKAGE_MANIFEST_PATH);
52
+ const manifest = manifestEntry
53
+ ? JSON.parse(strFromU8(manifestEntry.bytes))
54
+ : null;
55
+
56
+ return {
57
+ skillName,
58
+ manifest,
59
+ entries: entries.filter((entry) => entry.relativePath !== PACKAGE_MANIFEST_PATH),
60
+ };
61
+ }
62
+
63
+ async function ensureInstallTarget(targetDir, force) {
64
+ if (await pathExists(targetDir)) {
65
+ if (!force) {
66
+ throw new Error(`Target skill already exists: ${targetDir}`);
67
+ }
68
+ await rm(targetDir, { recursive: true, force: true });
69
+ }
70
+ await mkdir(targetDir, { recursive: true });
71
+ }
72
+
73
+ export async function installSkillArchive(archivePath, options = {}) {
74
+ const skillsDir = path.resolve(options.skillsDir ?? getDefaultSkillsDir());
75
+ const archive = readSkillPackageBuffer(await readFile(archivePath));
76
+ const targetDir = path.join(skillsDir, archive.skillName);
77
+ await ensureInstallTarget(targetDir, options.force === true);
78
+
79
+ for (const entry of archive.entries) {
80
+ const destination = path.join(targetDir, entry.relativePath);
81
+ await mkdir(path.dirname(destination), { recursive: true });
82
+ await writeFile(destination, entry.bytes);
83
+ }
84
+
85
+ return {
86
+ skillName: archive.skillName,
87
+ installedTo: targetDir,
88
+ source: path.resolve(archivePath),
89
+ manifest: archive.manifest,
90
+ };
91
+ }
92
+
93
+ export async function installSkillDirectory(skillDir, options = {}) {
94
+ const validation = await validateSkillDirectory(skillDir);
95
+ if (!validation.valid) {
96
+ throw new Error(`Cannot install invalid skill:\n- ${validation.errors.join('\n- ')}`);
97
+ }
98
+
99
+ const skillFilePath = path.join(skillDir, 'SKILL.md');
100
+ const skill = parseSkillMarkdown(await readFile(skillFilePath, 'utf8'));
101
+ const skillsDir = path.resolve(options.skillsDir ?? getDefaultSkillsDir());
102
+ const targetDir = path.join(skillsDir, skill.frontmatter.name);
103
+ if (await pathExists(targetDir)) {
104
+ if (!options.force) {
105
+ throw new Error(`Target skill already exists: ${targetDir}`);
106
+ }
107
+ await rm(targetDir, { recursive: true, force: true });
108
+ }
109
+ await mkdir(skillsDir, { recursive: true });
110
+ await cp(skillDir, targetDir, { recursive: true });
111
+
112
+ return {
113
+ skillName: skill.frontmatter.name,
114
+ installedTo: targetDir,
115
+ source: path.resolve(skillDir),
116
+ manifest: null,
117
+ };
118
+ }
119
+
120
+ export async function installSkill(inputPath, options = {}) {
121
+ const resolvedInput = path.resolve(inputPath);
122
+ const inputStat = await stat(resolvedInput).catch(() => null);
123
+ if (!inputStat) {
124
+ throw new Error(`Input not found: ${resolvedInput}`);
125
+ }
126
+
127
+ if (inputStat.isDirectory()) {
128
+ return installSkillDirectory(resolvedInput, options);
129
+ }
130
+
131
+ if (inputStat.isFile() && isZipLike(resolvedInput)) {
132
+ return installSkillArchive(resolvedInput, options);
133
+ }
134
+
135
+ throw new Error('Input must be a skill directory or a .skill/.zip archive.');
136
+ }
137
+
138
+ function parseArgs(argv) {
139
+ const args = {
140
+ input: argv[2],
141
+ skillsDir: undefined,
142
+ force: false,
143
+ };
144
+
145
+ for (let index = 3; index < argv.length; index += 1) {
146
+ const token = argv[index];
147
+ if (token === '--dest' && argv[index + 1]) {
148
+ args.skillsDir = argv[++index];
149
+ } else if (token === '--force') {
150
+ args.force = true;
151
+ }
152
+ }
153
+
154
+ return args;
155
+ }
156
+
157
+ async function main() {
158
+ const args = parseArgs(process.argv);
159
+ if (!args.input) {
160
+ console.error('Usage: node scripts/install-skill.js <skill-dir-or-archive> [--dest <skills-dir>] [--force]');
161
+ process.exit(1);
162
+ }
163
+
164
+ const result = await installSkill(args.input, args);
165
+ console.log(`Installed ${result.skillName} to ${result.installedTo}`);
166
+ }
167
+
168
+ const isDirectRun = process.argv[1]
169
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
170
+
171
+ if (isDirectRun) {
172
+ main().catch((error) => {
173
+ console.error(error instanceof Error ? error.message : String(error));
174
+ process.exit(1);
175
+ });
176
+ }
@@ -0,0 +1,38 @@
1
+ export interface PackagedSkillFile {
2
+ path: string;
3
+ size: number;
4
+ sha256: string;
5
+ }
6
+
7
+ export interface SkillPackageManifest {
8
+ format: 'kodax-skill-package';
9
+ version: 1;
10
+ created_at: string;
11
+ entrypoint: 'SKILL.md';
12
+ skill: {
13
+ name: string;
14
+ description: string;
15
+ compatibility: string | null;
16
+ user_invocable: boolean;
17
+ disable_model_invocation: boolean;
18
+ };
19
+ files: PackagedSkillFile[];
20
+ note: string;
21
+ }
22
+
23
+ export function createPackageManifest(
24
+ skill: { frontmatter: Record<string, unknown> },
25
+ files: Array<{ relativePath: string; bytes: Uint8Array; sha256: string }>,
26
+ options?: { createdAt?: string }
27
+ ): SkillPackageManifest;
28
+
29
+ export function buildSkillPackage(
30
+ skillDir: string,
31
+ options?: { createdAt?: string }
32
+ ): Promise<{ manifest: SkillPackageManifest; bytes: Uint8Array }>;
33
+
34
+ export function writeSkillPackage(
35
+ skillDir: string,
36
+ outputPath: string,
37
+ options?: { createdAt?: string }
38
+ ): Promise<{ manifest: SkillPackageManifest; bytes: Uint8Array; outputPath: string }>;
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { createHash } from 'node:crypto';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { zipSync, strToU8 } from 'fflate';
8
+ import { validateSkillDirectory } from './quick-validate.js';
9
+ import { collectFiles, loadSkill } from './utils.js';
10
+
11
+ const PACKAGE_MANIFEST_PATH = '.kodax-package.json';
12
+
13
+ function toUint8Array(value) {
14
+ return value instanceof Uint8Array ? value : new Uint8Array(value);
15
+ }
16
+
17
+ function sha256(buffer) {
18
+ return createHash('sha256').update(buffer).digest('hex');
19
+ }
20
+
21
+ export function createPackageManifest(skill, files, options = {}) {
22
+ return {
23
+ format: 'kodax-skill-package',
24
+ version: 1,
25
+ created_at: options.createdAt ?? new Date().toISOString(),
26
+ entrypoint: 'SKILL.md',
27
+ skill: {
28
+ name: skill.frontmatter.name,
29
+ description: skill.frontmatter.description,
30
+ compatibility: skill.frontmatter.compatibility ?? null,
31
+ user_invocable: skill.frontmatter['user-invocable'] ?? true,
32
+ disable_model_invocation: skill.frontmatter['disable-model-invocation'] ?? false,
33
+ },
34
+ files: files.map((file) => ({
35
+ path: file.relativePath,
36
+ size: file.bytes.length,
37
+ sha256: file.sha256,
38
+ })),
39
+ note: 'This archive is a zip file with a .skill extension. Compatible agents can extract and install the included skill directory.',
40
+ };
41
+ }
42
+
43
+ export async function buildSkillPackage(skillDir, options = {}) {
44
+ const validation = await validateSkillDirectory(skillDir);
45
+ if (!validation.valid) {
46
+ throw new Error(`Cannot package invalid skill:\n- ${validation.errors.join('\n- ')}`);
47
+ }
48
+
49
+ const skill = await loadSkill(skillDir);
50
+ const discoveredFiles = await collectFiles(skillDir);
51
+ const files = [];
52
+ for (const file of discoveredFiles) {
53
+ if (file.relativePath.endsWith('.skill')) {
54
+ continue;
55
+ }
56
+ const bytes = toUint8Array(await readFile(file.absolutePath));
57
+ files.push({
58
+ ...file,
59
+ bytes,
60
+ sha256: sha256(bytes),
61
+ });
62
+ }
63
+
64
+ const manifest = createPackageManifest(skill, files, options);
65
+ const archiveEntries = {
66
+ [PACKAGE_MANIFEST_PATH]: strToU8(JSON.stringify(manifest, null, 2)),
67
+ };
68
+ for (const file of files) {
69
+ archiveEntries[file.relativePath] = file.bytes;
70
+ }
71
+
72
+ return {
73
+ manifest,
74
+ bytes: zipSync(archiveEntries, { level: 6 }),
75
+ };
76
+ }
77
+
78
+ export async function writeSkillPackage(skillDir, outputPath, options = {}) {
79
+ const result = await buildSkillPackage(skillDir, options);
80
+ await writeFile(outputPath, result.bytes);
81
+ return {
82
+ ...result,
83
+ outputPath: path.resolve(outputPath),
84
+ };
85
+ }
86
+
87
+ function parseArgs(argv) {
88
+ const args = {
89
+ skillDir: argv[2],
90
+ output: undefined,
91
+ };
92
+
93
+ for (let index = 3; index < argv.length; index += 1) {
94
+ const token = argv[index];
95
+ if (token === '--output' && argv[index + 1]) {
96
+ args.output = argv[++index];
97
+ }
98
+ }
99
+
100
+ return args;
101
+ }
102
+
103
+ async function main() {
104
+ const args = parseArgs(process.argv);
105
+ if (!args.skillDir) {
106
+ console.error('Usage: node scripts/package-skill.js <skill-dir> [--output <file.skill>]');
107
+ process.exit(1);
108
+ }
109
+
110
+ const skill = await loadSkill(args.skillDir);
111
+ const outputPath = args.output ?? path.join(path.dirname(args.skillDir), `${skill.frontmatter.name}.skill`);
112
+ const result = await writeSkillPackage(args.skillDir, outputPath);
113
+ console.log(`Wrote ${result.outputPath}`);
114
+ }
115
+
116
+ const isDirectRun = process.argv[1]
117
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
118
+
119
+ if (isDirectRun) {
120
+ main().catch((error) => {
121
+ console.error(error instanceof Error ? error.message : String(error));
122
+ process.exit(1);
123
+ });
124
+ }
@@ -0,0 +1,8 @@
1
+ export interface SkillValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ warnings: string[];
5
+ frontmatter: Record<string, unknown> | null;
6
+ }
7
+
8
+ export function validateSkillDirectory(skillDir: string): Promise<SkillValidationResult>;
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadSkill } from './utils.js';
6
+
7
+ const ALLOWED_FRONTMATTER_KEYS = new Set([
8
+ 'name',
9
+ 'description',
10
+ 'license',
11
+ 'compatibility',
12
+ 'metadata',
13
+ 'allowed-tools',
14
+ 'disable-model-invocation',
15
+ 'user-invocable',
16
+ 'argument-hint',
17
+ 'context',
18
+ 'agent',
19
+ 'model',
20
+ 'hooks',
21
+ ]);
22
+
23
+ export async function validateSkillDirectory(skillDir) {
24
+ const result = {
25
+ valid: false,
26
+ errors: [],
27
+ warnings: [],
28
+ frontmatter: null,
29
+ };
30
+
31
+ let skill;
32
+ try {
33
+ skill = await loadSkill(skillDir);
34
+ } catch (error) {
35
+ result.errors.push(error instanceof Error ? error.message : String(error));
36
+ return result;
37
+ }
38
+
39
+ const { frontmatter, body } = skill;
40
+ result.frontmatter = frontmatter;
41
+
42
+ const unexpectedKeys = Object.keys(frontmatter).filter((key) => !ALLOWED_FRONTMATTER_KEYS.has(key));
43
+ if (unexpectedKeys.length > 0) {
44
+ result.errors.push(
45
+ `Unexpected frontmatter keys: ${unexpectedKeys.sort().join(', ')}`
46
+ );
47
+ }
48
+
49
+ const name = typeof frontmatter.name === 'string' ? frontmatter.name.trim() : '';
50
+ const description = typeof frontmatter.description === 'string'
51
+ ? frontmatter.description.trim()
52
+ : '';
53
+
54
+ if (!name) {
55
+ result.errors.push('Missing required frontmatter field: name');
56
+ } else {
57
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
58
+ result.errors.push(
59
+ `Invalid skill name "${name}". Use lowercase kebab-case and avoid consecutive hyphens.`
60
+ );
61
+ }
62
+ if (name.length > 64) {
63
+ result.errors.push(`Skill name is too long (${name.length}/64).`);
64
+ }
65
+
66
+ const dirName = path.basename(path.resolve(skillDir));
67
+ if (dirName !== name) {
68
+ result.warnings.push(
69
+ `Skill directory name "${dirName}" does not match frontmatter name "${name}".`
70
+ );
71
+ }
72
+ }
73
+
74
+ if (!description) {
75
+ result.errors.push('Missing required frontmatter field: description');
76
+ } else {
77
+ if (description.length > 1024) {
78
+ result.errors.push(`Description is too long (${description.length}/1024).`);
79
+ }
80
+ if (/[<>]/.test(description)) {
81
+ result.errors.push('Description cannot contain angle brackets.');
82
+ }
83
+ if (!/[。.!?]/.test(description)) {
84
+ result.warnings.push('Description may be too terse; consider making the trigger conditions clearer.');
85
+ }
86
+ }
87
+
88
+ if (!body.trim()) {
89
+ result.errors.push('SKILL.md body is empty.');
90
+ }
91
+
92
+ if (frontmatter.compatibility != null) {
93
+ if (typeof frontmatter.compatibility !== 'string') {
94
+ result.errors.push('compatibility must be a string if provided.');
95
+ } else if (frontmatter.compatibility.length > 500) {
96
+ result.errors.push(`compatibility is too long (${frontmatter.compatibility.length}/500).`);
97
+ }
98
+ }
99
+
100
+ if (frontmatter['allowed-tools'] != null) {
101
+ const allowedTools = frontmatter['allowed-tools'];
102
+ const validAllowedTools = typeof allowedTools === 'string'
103
+ || (Array.isArray(allowedTools) && allowedTools.every((item) => typeof item === 'string'));
104
+ if (!validAllowedTools) {
105
+ result.errors.push('allowed-tools must be a string or string array.');
106
+ }
107
+ }
108
+
109
+ if (frontmatter['disable-model-invocation'] != null
110
+ && typeof frontmatter['disable-model-invocation'] !== 'boolean') {
111
+ result.errors.push('disable-model-invocation must be a boolean.');
112
+ }
113
+
114
+ if (frontmatter['user-invocable'] != null && typeof frontmatter['user-invocable'] !== 'boolean') {
115
+ result.errors.push('user-invocable must be a boolean.');
116
+ }
117
+
118
+ if (frontmatter.context != null && frontmatter.context !== 'fork') {
119
+ result.errors.push('context must be "fork" when provided.');
120
+ }
121
+
122
+ if (frontmatter.hooks != null && (typeof frontmatter.hooks !== 'object' || Array.isArray(frontmatter.hooks))) {
123
+ result.errors.push('hooks must be an object when provided.');
124
+ }
125
+
126
+ result.valid = result.errors.length === 0;
127
+ return result;
128
+ }
129
+
130
+ async function main() {
131
+ const skillDir = process.argv[2];
132
+ if (!skillDir) {
133
+ console.error('Usage: node scripts/quick-validate.js <skill-directory>');
134
+ process.exit(1);
135
+ }
136
+
137
+ const result = await validateSkillDirectory(skillDir);
138
+
139
+ if (result.errors.length === 0) {
140
+ console.log('Skill is valid.');
141
+ } else {
142
+ console.error('Skill validation failed:');
143
+ for (const error of result.errors) {
144
+ console.error(`- ${error}`);
145
+ }
146
+ }
147
+
148
+ if (result.warnings.length > 0) {
149
+ console.log('Warnings:');
150
+ for (const warning of result.warnings) {
151
+ console.log(`- ${warning}`);
152
+ }
153
+ }
154
+
155
+ process.exit(result.valid ? 0 : 1);
156
+ }
157
+
158
+ const isDirectRun = process.argv[1]
159
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
160
+
161
+ if (isDirectRun) {
162
+ main().catch((error) => {
163
+ console.error(error instanceof Error ? error.message : String(error));
164
+ process.exit(1);
165
+ });
166
+ }