@letta-ai/letta-code 0.7.3 → 0.7.4-next.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.
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env npx ts-node
2
+ /**
3
+ * Skill Initializer - Creates a new skill from template
4
+ *
5
+ * Usage:
6
+ * npx ts-node init-skill.ts <skill-name> --path <path>
7
+ *
8
+ * Examples:
9
+ * npx ts-node init-skill.ts my-new-skill --path .skills
10
+ * npx ts-node init-skill.ts my-api-helper --path ~/.letta/skills
11
+ */
12
+
13
+ import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+
16
+ const SKILL_TEMPLATE = `---
17
+ name: {skill_name}
18
+ description: "[TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]"
19
+ ---
20
+
21
+ # {skill_title}
22
+
23
+ ## Overview
24
+
25
+ [TODO: 1-2 sentences explaining what this skill enables]
26
+
27
+ ## Structuring This Skill
28
+
29
+ [TODO: Choose the structure that best fits this skill's purpose. Common patterns:
30
+
31
+ **1. Workflow-Based** (best for sequential processes)
32
+ - Works well when there are clear step-by-step procedures
33
+ - Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing"
34
+ - Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
35
+
36
+ **2. Task-Based** (best for tool collections)
37
+ - Works well when the skill offers different operations/capabilities
38
+ - Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text"
39
+ - Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
40
+
41
+ **3. Reference/Guidelines** (best for standards or specifications)
42
+ - Works well for brand guidelines, coding standards, or requirements
43
+ - Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features"
44
+ - Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
45
+
46
+ **4. Capabilities-Based** (best for integrated systems)
47
+ - Works well when the skill provides multiple interrelated features
48
+ - Example: Product Management with "Core Capabilities" → numbered capability list
49
+ - Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
50
+
51
+ Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
52
+
53
+ Delete this entire "Structuring This Skill" section when done - it's just guidance.]
54
+
55
+ ## [TODO: Replace with the first main section based on chosen structure]
56
+
57
+ [TODO: Add content here. See examples in existing skills:
58
+ - Code samples for technical skills
59
+ - Decision trees for complex workflows
60
+ - Concrete examples with realistic user requests
61
+ - References to scripts/templates/references as needed]
62
+
63
+ ## Resources
64
+
65
+ This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
66
+
67
+ ### scripts/
68
+ Executable code (TypeScript/Python/Bash/etc.) that can be run directly to perform specific operations.
69
+
70
+ **Appropriate for:** Scripts that perform automation, data processing, or specific operations.
71
+
72
+ **Note:** Scripts may be executed without loading into context, but can still be read by the Letta Code agent for patching or environment adjustments.
73
+
74
+ ### references/
75
+ Documentation and reference material intended to be loaded into context to inform the Letta Code agent's process and thinking.
76
+
77
+ **Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that the Letta Code agent should reference while working.
78
+
79
+ ### assets/
80
+ Files not intended to be loaded into context, but rather used within the output the Letta Code agent produces.
81
+
82
+ **Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
83
+
84
+ ---
85
+
86
+ **Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
87
+ `;
88
+
89
+ const EXAMPLE_SCRIPT = `#!/usr/bin/env npx ts-node
90
+ /**
91
+ * Example helper script for {skill_name}
92
+ *
93
+ * This is a placeholder script that can be executed directly.
94
+ * Replace with actual implementation or delete if not needed.
95
+ */
96
+
97
+ function main() {
98
+ console.log("This is an example script for {skill_name}");
99
+ // TODO: Add actual script logic here
100
+ // This could be data processing, file conversion, API calls, etc.
101
+ }
102
+
103
+ main();
104
+ `;
105
+
106
+ const EXAMPLE_REFERENCE = `# Reference Documentation for {skill_title}
107
+
108
+ This is a placeholder for detailed reference documentation.
109
+ Replace with actual reference content or delete if not needed.
110
+
111
+ ## When Reference Docs Are Useful
112
+
113
+ Reference docs are ideal for:
114
+ - Comprehensive API documentation
115
+ - Detailed workflow guides
116
+ - Complex multi-step processes
117
+ - Information too lengthy for main SKILL.md
118
+ - Content that's only needed for specific use cases
119
+
120
+ ## Structure Suggestions
121
+
122
+ ### API Reference Example
123
+ - Overview
124
+ - Authentication
125
+ - Endpoints with examples
126
+ - Error codes
127
+ - Rate limits
128
+
129
+ ### Workflow Guide Example
130
+ - Prerequisites
131
+ - Step-by-step instructions
132
+ - Common patterns
133
+ - Troubleshooting
134
+ - Best practices
135
+ `;
136
+
137
+ const EXAMPLE_ASSET = `# Example Asset File
138
+
139
+ This placeholder represents where asset files would be stored.
140
+ Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
141
+
142
+ Asset files are NOT intended to be loaded into context, but rather used within
143
+ the output the Letta Code agent produces.
144
+
145
+ ## Common Asset Types
146
+
147
+ - Templates: .pptx, .docx, boilerplate directories
148
+ - Images: .png, .jpg, .svg, .gif
149
+ - Fonts: .ttf, .otf, .woff, .woff2
150
+ - Boilerplate code: Project directories, starter files
151
+ - Icons: .ico, .svg
152
+ - Data files: .csv, .json, .xml, .yaml
153
+
154
+ Note: This is a text placeholder. Actual assets can be any file type.
155
+ `;
156
+
157
+ function titleCaseSkillName(skillName: string): string {
158
+ return skillName
159
+ .split("-")
160
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
161
+ .join(" ");
162
+ }
163
+
164
+ function initSkill(skillName: string, path: string): string | null {
165
+ const skillDir = resolve(path, skillName);
166
+
167
+ // Check if directory already exists
168
+ if (existsSync(skillDir)) {
169
+ console.error(`Error: Skill directory already exists: ${skillDir}`);
170
+ return null;
171
+ }
172
+
173
+ // Create skill directory
174
+ try {
175
+ mkdirSync(skillDir, { recursive: true });
176
+ console.log(`Created skill directory: ${skillDir}`);
177
+ } catch (e) {
178
+ console.error(
179
+ `Error creating directory: ${e instanceof Error ? e.message : String(e)}`,
180
+ );
181
+ return null;
182
+ }
183
+
184
+ const skillTitle = titleCaseSkillName(skillName);
185
+
186
+ // Create SKILL.md from template
187
+ const skillContent = SKILL_TEMPLATE.replace(
188
+ /{skill_name}/g,
189
+ skillName,
190
+ ).replace(/{skill_title}/g, skillTitle);
191
+
192
+ try {
193
+ writeFileSync(join(skillDir, "SKILL.md"), skillContent);
194
+ console.log("Created SKILL.md");
195
+ } catch (e) {
196
+ console.error(
197
+ `Error creating SKILL.md: ${e instanceof Error ? e.message : String(e)}`,
198
+ );
199
+ return null;
200
+ }
201
+
202
+ // Create resource directories with example files
203
+ try {
204
+ // Create scripts/ directory with example script
205
+ const scriptsDir = join(skillDir, "scripts");
206
+ mkdirSync(scriptsDir, { recursive: true });
207
+ const exampleScriptPath = join(scriptsDir, "example.ts");
208
+ writeFileSync(
209
+ exampleScriptPath,
210
+ EXAMPLE_SCRIPT.replace(/{skill_name}/g, skillName),
211
+ );
212
+ chmodSync(exampleScriptPath, 0o755);
213
+ console.log("Created scripts/example.ts");
214
+
215
+ // Create references/ directory with example reference doc
216
+ const referencesDir = join(skillDir, "references");
217
+ mkdirSync(referencesDir, { recursive: true });
218
+ writeFileSync(
219
+ join(referencesDir, "api-reference.md"),
220
+ EXAMPLE_REFERENCE.replace(/{skill_title}/g, skillTitle),
221
+ );
222
+ console.log("Created references/api-reference.md");
223
+
224
+ // Create assets/ directory with example asset placeholder
225
+ const assetsDir = join(skillDir, "assets");
226
+ mkdirSync(assetsDir, { recursive: true });
227
+ writeFileSync(join(assetsDir, "example-asset.txt"), EXAMPLE_ASSET);
228
+ console.log("Created assets/example-asset.txt");
229
+ } catch (e) {
230
+ console.error(
231
+ `Error creating resource directories: ${e instanceof Error ? e.message : String(e)}`,
232
+ );
233
+ return null;
234
+ }
235
+
236
+ // Print next steps
237
+ console.log(`\nSkill '${skillName}' initialized successfully at ${skillDir}`);
238
+ console.log("\nNext steps:");
239
+ console.log(
240
+ "1. Edit SKILL.md to complete the TODO items and update the description",
241
+ );
242
+ console.log(
243
+ "2. Customize or delete the example files in scripts/, references/, and assets/",
244
+ );
245
+ console.log("3. Run the validator when ready to check the skill structure");
246
+
247
+ return skillDir;
248
+ }
249
+
250
+ // CLI entry point
251
+ if (require.main === module) {
252
+ const args = process.argv.slice(2);
253
+
254
+ if (args.length < 3 || args[1] !== "--path") {
255
+ console.log("Usage: npx ts-node init-skill.ts <skill-name> --path <path>");
256
+ console.log("\nSkill name requirements:");
257
+ console.log(" - Hyphen-case identifier (e.g., 'data-analyzer')");
258
+ console.log(" - Lowercase letters, digits, and hyphens only");
259
+ console.log(" - Max 64 characters");
260
+ console.log(" - Must match directory name exactly");
261
+ console.log("\nExamples:");
262
+ console.log(" npx ts-node init-skill.ts my-new-skill --path .skills");
263
+ console.log(
264
+ " npx ts-node init-skill.ts my-api-helper --path ~/.letta/skills",
265
+ );
266
+ process.exit(1);
267
+ }
268
+
269
+ const skillName = args[0] as string;
270
+ const path = args[2] as string;
271
+
272
+ console.log(`Initializing skill: ${skillName}`);
273
+ console.log(`Location: ${path}\n`);
274
+
275
+ const result = initSkill(skillName, path);
276
+ process.exit(result ? 0 : 1);
277
+ }
278
+
279
+ export { initSkill, titleCaseSkillName };
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env npx ts-node
2
+ /**
3
+ * Skill Packager - Creates a distributable .skill file of a skill folder
4
+ *
5
+ * Usage:
6
+ * npx ts-node package-skill.ts <path/to/skill-folder> [output-directory]
7
+ *
8
+ * Example:
9
+ * npx ts-node package-skill.ts .skills/my-skill
10
+ * npx ts-node package-skill.ts .skills/my-skill ./dist
11
+ */
12
+
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ readdirSync,
17
+ readFileSync,
18
+ statSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { basename, dirname, join, relative, resolve } from "node:path";
22
+ // Simple zip implementation using Node.js built-in zlib
23
+ // For a proper zip file, we'll create the structure manually
24
+ import { deflateSync } from "node:zlib";
25
+ import { validateSkill } from "./validate-skill";
26
+
27
+ interface ZipEntry {
28
+ path: string;
29
+ data: Buffer;
30
+ isDirectory: boolean;
31
+ }
32
+
33
+ function createZip(entries: ZipEntry[]): Buffer {
34
+ // Use archiver-like approach with raw buffer manipulation
35
+ // For simplicity, we'll use a basic ZIP format
36
+
37
+ const localHeaders: Buffer[] = [];
38
+ const centralHeaders: Buffer[] = [];
39
+ let offset = 0;
40
+
41
+ for (const entry of entries) {
42
+ const pathBuffer = Buffer.from(entry.path, "utf8");
43
+ const compressedData = entry.isDirectory
44
+ ? Buffer.alloc(0)
45
+ : deflateSync(entry.data);
46
+ const uncompressedSize = entry.isDirectory ? 0 : entry.data.length;
47
+ const compressedSize = compressedData.length;
48
+
49
+ // CRC32 calculation
50
+ const crc = entry.isDirectory ? 0 : crc32(entry.data);
51
+
52
+ // Local file header
53
+ const localHeader = Buffer.alloc(30 + pathBuffer.length);
54
+ localHeader.writeUInt32LE(0x04034b50, 0); // Local file header signature
55
+ localHeader.writeUInt16LE(20, 4); // Version needed to extract
56
+ localHeader.writeUInt16LE(0, 6); // General purpose bit flag
57
+ localHeader.writeUInt16LE(entry.isDirectory ? 0 : 8, 8); // Compression method (8 = deflate)
58
+ localHeader.writeUInt16LE(0, 10); // File last modification time
59
+ localHeader.writeUInt16LE(0, 12); // File last modification date
60
+ localHeader.writeUInt32LE(crc, 14); // CRC-32
61
+ localHeader.writeUInt32LE(compressedSize, 18); // Compressed size
62
+ localHeader.writeUInt32LE(uncompressedSize, 22); // Uncompressed size
63
+ localHeader.writeUInt16LE(pathBuffer.length, 26); // File name length
64
+ localHeader.writeUInt16LE(0, 28); // Extra field length
65
+ pathBuffer.copy(localHeader, 30);
66
+
67
+ localHeaders.push(localHeader);
68
+ localHeaders.push(compressedData);
69
+
70
+ // Central directory header
71
+ const centralHeader = Buffer.alloc(46 + pathBuffer.length);
72
+ centralHeader.writeUInt32LE(0x02014b50, 0); // Central directory signature
73
+ centralHeader.writeUInt16LE(20, 4); // Version made by
74
+ centralHeader.writeUInt16LE(20, 6); // Version needed to extract
75
+ centralHeader.writeUInt16LE(0, 8); // General purpose bit flag
76
+ centralHeader.writeUInt16LE(entry.isDirectory ? 0 : 8, 10); // Compression method
77
+ centralHeader.writeUInt16LE(0, 12); // File last modification time
78
+ centralHeader.writeUInt16LE(0, 14); // File last modification date
79
+ centralHeader.writeUInt32LE(crc, 16); // CRC-32
80
+ centralHeader.writeUInt32LE(compressedSize, 20); // Compressed size
81
+ centralHeader.writeUInt32LE(uncompressedSize, 24); // Uncompressed size
82
+ centralHeader.writeUInt16LE(pathBuffer.length, 28); // File name length
83
+ centralHeader.writeUInt16LE(0, 30); // Extra field length
84
+ centralHeader.writeUInt16LE(0, 32); // File comment length
85
+ centralHeader.writeUInt16LE(0, 34); // Disk number start
86
+ centralHeader.writeUInt16LE(0, 36); // Internal file attributes
87
+ centralHeader.writeUInt32LE(entry.isDirectory ? 0x10 : 0, 38); // External file attributes
88
+ centralHeader.writeUInt32LE(offset, 42); // Relative offset of local header
89
+ pathBuffer.copy(centralHeader, 46);
90
+
91
+ centralHeaders.push(centralHeader);
92
+
93
+ offset += localHeader.length + compressedData.length;
94
+ }
95
+
96
+ const centralDirOffset = offset;
97
+ const centralDirSize = centralHeaders.reduce((sum, h) => sum + h.length, 0);
98
+
99
+ // End of central directory record
100
+ const endRecord = Buffer.alloc(22);
101
+ endRecord.writeUInt32LE(0x06054b50, 0); // End of central directory signature
102
+ endRecord.writeUInt16LE(0, 4); // Number of this disk
103
+ endRecord.writeUInt16LE(0, 6); // Disk where central directory starts
104
+ endRecord.writeUInt16LE(entries.length, 8); // Number of central directory records on this disk
105
+ endRecord.writeUInt16LE(entries.length, 10); // Total number of central directory records
106
+ endRecord.writeUInt32LE(centralDirSize, 12); // Size of central directory
107
+ endRecord.writeUInt32LE(centralDirOffset, 16); // Offset of start of central directory
108
+ endRecord.writeUInt16LE(0, 20); // Comment length
109
+
110
+ return Buffer.concat([...localHeaders, ...centralHeaders, endRecord]);
111
+ }
112
+
113
+ // CRC32 implementation
114
+ function crc32(data: Buffer): number {
115
+ let crc = 0xffffffff;
116
+ const table = getCrc32Table();
117
+
118
+ for (let i = 0; i < data.length; i++) {
119
+ // biome-ignore lint/style/noNonNullAssertion: array access within bounds
120
+ crc = (crc >>> 8) ^ (table[(crc ^ data[i]!) & 0xff] as number);
121
+ }
122
+
123
+ return (crc ^ 0xffffffff) >>> 0;
124
+ }
125
+
126
+ let crc32Table: number[] | null = null;
127
+ function getCrc32Table(): number[] {
128
+ if (crc32Table) return crc32Table;
129
+
130
+ crc32Table = [];
131
+ for (let i = 0; i < 256; i++) {
132
+ let c = i;
133
+ for (let j = 0; j < 8; j++) {
134
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
135
+ }
136
+ crc32Table[i] = c;
137
+ }
138
+ return crc32Table;
139
+ }
140
+
141
+ function getAllFiles(dir: string, baseDir: string): ZipEntry[] {
142
+ const entries: ZipEntry[] = [];
143
+ const items = readdirSync(dir, { withFileTypes: true });
144
+
145
+ for (const item of items) {
146
+ const fullPath = join(dir, item.name);
147
+ const relativePath = relative(baseDir, fullPath);
148
+
149
+ if (item.isDirectory()) {
150
+ entries.push({
151
+ path: `${relativePath}/`,
152
+ data: Buffer.alloc(0),
153
+ isDirectory: true,
154
+ });
155
+ entries.push(...getAllFiles(fullPath, baseDir));
156
+ } else {
157
+ entries.push({
158
+ path: relativePath,
159
+ data: readFileSync(fullPath),
160
+ isDirectory: false,
161
+ });
162
+ }
163
+ }
164
+
165
+ return entries;
166
+ }
167
+
168
+ function packageSkill(skillPath: string, outputDir?: string): string | null {
169
+ const resolvedPath = resolve(skillPath);
170
+
171
+ // Validate skill folder exists
172
+ if (!existsSync(resolvedPath)) {
173
+ console.error(`Error: Skill folder not found: ${resolvedPath}`);
174
+ return null;
175
+ }
176
+
177
+ if (!statSync(resolvedPath).isDirectory()) {
178
+ console.error(`Error: Path is not a directory: ${resolvedPath}`);
179
+ return null;
180
+ }
181
+
182
+ // Validate SKILL.md exists
183
+ const skillMdPath = join(resolvedPath, "SKILL.md");
184
+ if (!existsSync(skillMdPath)) {
185
+ console.error(`Error: SKILL.md not found in ${resolvedPath}`);
186
+ return null;
187
+ }
188
+
189
+ // Run validation before packaging
190
+ console.log("Validating skill...");
191
+ const { valid, message } = validateSkill(resolvedPath);
192
+ if (!valid) {
193
+ console.error(`Validation failed: ${message}`);
194
+ console.error("Please fix the validation errors before packaging.");
195
+ return null;
196
+ }
197
+ console.log(`${message}\n`);
198
+
199
+ // Determine output location
200
+ const skillName = basename(resolvedPath);
201
+ const outputPath = outputDir ? resolve(outputDir) : process.cwd();
202
+
203
+ if (outputDir && !existsSync(outputPath)) {
204
+ mkdirSync(outputPath, { recursive: true });
205
+ }
206
+
207
+ const skillFilename = join(outputPath, `${skillName}.skill`);
208
+
209
+ // Create the .skill file (zip format)
210
+ try {
211
+ // Get all files, using parent directory as base so skill folder is included
212
+ const parentDir = dirname(resolvedPath);
213
+ const entries = getAllFiles(resolvedPath, parentDir);
214
+
215
+ // Add the skill directory itself
216
+ entries.unshift({
217
+ path: `${skillName}/`,
218
+ data: Buffer.alloc(0),
219
+ isDirectory: true,
220
+ });
221
+
222
+ const zipBuffer = createZip(entries);
223
+ writeFileSync(skillFilename, zipBuffer);
224
+
225
+ for (const entry of entries) {
226
+ if (!entry.isDirectory) {
227
+ console.log(` Added: ${entry.path}`);
228
+ }
229
+ }
230
+
231
+ console.log(`\nSuccessfully packaged skill to: ${skillFilename}`);
232
+ return skillFilename;
233
+ } catch (e) {
234
+ console.error(
235
+ `Error creating .skill file: ${e instanceof Error ? e.message : String(e)}`,
236
+ );
237
+ return null;
238
+ }
239
+ }
240
+
241
+ // CLI entry point
242
+ if (require.main === module) {
243
+ const args = process.argv.slice(2);
244
+
245
+ if (args.length < 1) {
246
+ console.log(
247
+ "Usage: npx ts-node package-skill.ts <path/to/skill-folder> [output-directory]",
248
+ );
249
+ console.log("\nExample:");
250
+ console.log(" npx ts-node package-skill.ts .skills/my-skill");
251
+ console.log(" npx ts-node package-skill.ts .skills/my-skill ./dist");
252
+ process.exit(1);
253
+ }
254
+
255
+ const skillPath = args[0] as string;
256
+ const outputDir = args[1];
257
+
258
+ console.log(`Packaging skill: ${skillPath}`);
259
+ if (outputDir) {
260
+ console.log(`Output directory: ${outputDir}`);
261
+ }
262
+ console.log();
263
+
264
+ const result = packageSkill(skillPath, outputDir);
265
+ process.exit(result ? 0 : 1);
266
+ }
267
+
268
+ export { packageSkill };
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env npx ts-node
2
+ /**
3
+ * Skill Validator - Validates skill structure and frontmatter
4
+ *
5
+ * Usage:
6
+ * npx ts-node validate-skill.ts <skill-directory>
7
+ *
8
+ * Example:
9
+ * npx ts-node validate-skill.ts .skills/my-skill
10
+ */
11
+
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { parse as parseYaml } from "yaml";
15
+
16
+ interface ValidationResult {
17
+ valid: boolean;
18
+ message: string;
19
+ }
20
+
21
+ const ALLOWED_PROPERTIES = new Set([
22
+ "name",
23
+ "description",
24
+ "license",
25
+ "allowed-tools",
26
+ "metadata",
27
+ ]);
28
+
29
+ export function validateSkill(skillPath: string): ValidationResult {
30
+ // Check SKILL.md exists
31
+ const skillMdPath = join(skillPath, "SKILL.md");
32
+ if (!existsSync(skillMdPath)) {
33
+ return { valid: false, message: "SKILL.md not found" };
34
+ }
35
+
36
+ // Read content
37
+ const content = readFileSync(skillMdPath, "utf-8");
38
+
39
+ // Check for frontmatter
40
+ if (!content.startsWith("---")) {
41
+ return { valid: false, message: "No YAML frontmatter found" };
42
+ }
43
+
44
+ // Extract frontmatter
45
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
46
+ if (!match) {
47
+ return { valid: false, message: "Invalid frontmatter format" };
48
+ }
49
+
50
+ const frontmatterText = match[1] as string;
51
+
52
+ // Parse YAML frontmatter
53
+ let frontmatter: Record<string, unknown>;
54
+ try {
55
+ frontmatter = parseYaml(frontmatterText);
56
+ if (typeof frontmatter !== "object" || frontmatter === null) {
57
+ return { valid: false, message: "Frontmatter must be a YAML dictionary" };
58
+ }
59
+ } catch (e) {
60
+ return {
61
+ valid: false,
62
+ message: `Invalid YAML in frontmatter: ${e instanceof Error ? e.message : String(e)}`,
63
+ };
64
+ }
65
+
66
+ // Check for unexpected properties
67
+ const unexpectedKeys = Object.keys(frontmatter).filter(
68
+ (key) => !ALLOWED_PROPERTIES.has(key),
69
+ );
70
+ if (unexpectedKeys.length > 0) {
71
+ return {
72
+ valid: false,
73
+ message: `Unexpected key(s) in SKILL.md frontmatter: ${unexpectedKeys.sort().join(", ")}. Allowed properties are: ${[...ALLOWED_PROPERTIES].sort().join(", ")}`,
74
+ };
75
+ }
76
+
77
+ // Check required fields
78
+ if (!("name" in frontmatter)) {
79
+ return { valid: false, message: "Missing 'name' in frontmatter" };
80
+ }
81
+ if (!("description" in frontmatter)) {
82
+ return { valid: false, message: "Missing 'description' in frontmatter" };
83
+ }
84
+
85
+ // Validate name
86
+ const name = frontmatter.name;
87
+ if (typeof name !== "string") {
88
+ return {
89
+ valid: false,
90
+ message: `Name must be a string, got ${typeof name}`,
91
+ };
92
+ }
93
+ const trimmedName = name.trim();
94
+ if (trimmedName) {
95
+ // Check naming convention (hyphen-case: lowercase with hyphens)
96
+ if (!/^[a-z0-9-]+$/.test(trimmedName)) {
97
+ return {
98
+ valid: false,
99
+ message: `Name '${trimmedName}' should be hyphen-case (lowercase letters, digits, and hyphens only)`,
100
+ };
101
+ }
102
+ if (
103
+ trimmedName.startsWith("-") ||
104
+ trimmedName.endsWith("-") ||
105
+ trimmedName.includes("--")
106
+ ) {
107
+ return {
108
+ valid: false,
109
+ message: `Name '${trimmedName}' cannot start/end with hyphen or contain consecutive hyphens`,
110
+ };
111
+ }
112
+ // Check name length (max 64 characters)
113
+ if (trimmedName.length > 64) {
114
+ return {
115
+ valid: false,
116
+ message: `Name is too long (${trimmedName.length} characters). Maximum is 64 characters.`,
117
+ };
118
+ }
119
+ }
120
+
121
+ // Validate description
122
+ const description = frontmatter.description;
123
+ if (typeof description !== "string") {
124
+ return {
125
+ valid: false,
126
+ message: `Description must be a string, got ${typeof description}`,
127
+ };
128
+ }
129
+ const trimmedDescription = description.trim();
130
+ if (trimmedDescription) {
131
+ // Check for angle brackets
132
+ if (trimmedDescription.includes("<") || trimmedDescription.includes(">")) {
133
+ return {
134
+ valid: false,
135
+ message: "Description cannot contain angle brackets (< or >)",
136
+ };
137
+ }
138
+ // Check description length (max 1024 characters)
139
+ if (trimmedDescription.length > 1024) {
140
+ return {
141
+ valid: false,
142
+ message: `Description is too long (${trimmedDescription.length} characters). Maximum is 1024 characters.`,
143
+ };
144
+ }
145
+ }
146
+
147
+ return { valid: true, message: "Skill is valid!" };
148
+ }
149
+
150
+ // CLI entry point
151
+ if (require.main === module) {
152
+ const args = process.argv.slice(2);
153
+ if (args.length !== 1) {
154
+ console.log("Usage: npx ts-node validate-skill.ts <skill-directory>");
155
+ process.exit(1);
156
+ }
157
+
158
+ const { valid, message } = validateSkill(args[0] as string);
159
+ console.log(message);
160
+ process.exit(valid ? 0 : 1);
161
+ }