@letta-ai/letta-code 0.7.4-next.1 → 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,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
+ }