@poolzin/pool-bot 2026.3.7 → 2026.3.9
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.
- package/CHANGELOG.md +16 -0
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +302 -0
- package/dist/agents/skills/security.js +217 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/lazy-commands.example.js +113 -0
- package/dist/cli/lazy-commands.js +329 -0
- package/dist/cli/program/command-registry.js +13 -0
- package/dist/cli/program/register.skills.js +4 -0
- package/dist/config/config.js +1 -0
- package/dist/config/secrets-integration.js +88 -0
- package/dist/context-engine/index.js +33 -0
- package/dist/context-engine/legacy.js +181 -0
- package/dist/context-engine/registry.js +86 -0
- package/dist/context-engine/summarizing.js +293 -0
- package/dist/context-engine/types.js +7 -0
- package/dist/infra/abort-pattern.js +106 -0
- package/dist/infra/retry.js +94 -0
- package/dist/secrets/index.js +28 -0
- package/dist/secrets/resolver.js +185 -0
- package/dist/secrets/runtime.js +142 -0
- package/dist/secrets/types.js +11 -0
- package/dist/security/dangerous-tools.js +80 -0
- package/dist/security/types.js +12 -0
- package/dist/skills/commands.js +351 -0
- package/dist/skills/index.js +167 -0
- package/dist/skills/loader.js +282 -0
- package/dist/skills/parser.js +461 -0
- package/dist/skills/registry.js +397 -0
- package/dist/skills/security.js +318 -0
- package/dist/skills/types.js +21 -0
- package/dist/test-utils/index.js +219 -0
- package/dist/tui/index.js +595 -0
- package/docs/INTEGRATION_PLAN.md +475 -0
- package/docs/INTEGRATION_SUMMARY.md +215 -0
- package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
- package/docs/integrations/INTEGRATION_PLAN.md +424 -0
- package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
- package/docs/integrations/XYOPS_PLAN.md +978 -0
- package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
- package/docs/skills/SKILL.md +524 -0
- package/docs/skills.md +405 -0
- package/package.json +1 -1
- package/skills/example-skill/SKILL.md +195 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md parser with YAML frontmatter support
|
|
3
|
+
* Parses skill files into structured data
|
|
4
|
+
*
|
|
5
|
+
* @module skills/parser
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { basename, dirname } from "node:path";
|
|
9
|
+
import { SkillError, } from "./types.js";
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
14
|
+
const SECTION_REGEX = /^##\s+(.+)$/gm;
|
|
15
|
+
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
16
|
+
const VALID_CATEGORIES = [
|
|
17
|
+
"automation",
|
|
18
|
+
"integration",
|
|
19
|
+
"utility",
|
|
20
|
+
"development",
|
|
21
|
+
"communication",
|
|
22
|
+
"data",
|
|
23
|
+
"ai",
|
|
24
|
+
"custom",
|
|
25
|
+
];
|
|
26
|
+
// Required fields for agentskills.io format (strict mode)
|
|
27
|
+
const REQUIRED_FIELDS = ["id", "name", "description", "version", "category"];
|
|
28
|
+
// Required fields for PoolBot legacy format (lenient mode)
|
|
29
|
+
const _LEGACY_REQUIRED_FIELDS = ["name", "description"];
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// YAML Parser (lightweight, no external deps)
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Parse simple YAML frontmatter
|
|
35
|
+
* Handles basic key-value pairs, arrays, and nested objects
|
|
36
|
+
* Also handles JSON-style metadata values (for PoolBot legacy format)
|
|
37
|
+
*/
|
|
38
|
+
function parseYaml(yaml) {
|
|
39
|
+
const result = {};
|
|
40
|
+
const lines = yaml.split("\n");
|
|
41
|
+
let currentArray = null;
|
|
42
|
+
let currentKey = null;
|
|
43
|
+
let multilineValue = [];
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const line = lines[i];
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
// Skip empty lines and comments (but not inside multiline values)
|
|
48
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
49
|
+
if (multilineValue.length > 0) {
|
|
50
|
+
multilineValue.push(line);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Check for multiline value start (pipe | or greater-than >)
|
|
55
|
+
if (currentKey && (trimmed === "|" || trimmed === ">" || trimmed.startsWith("|-") || trimmed.startsWith(">-"))) {
|
|
56
|
+
multilineValue = [];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Collect multiline value
|
|
60
|
+
if (currentKey && multilineValue.length > 0) {
|
|
61
|
+
// Check if next line is still part of multiline (indented or empty)
|
|
62
|
+
const nextLine = lines[i + 1];
|
|
63
|
+
if (nextLine !== undefined && (nextLine.startsWith(" ") || nextLine.startsWith("\t") || nextLine.trim() === "")) {
|
|
64
|
+
multilineValue.push(line);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// End of multiline value
|
|
69
|
+
multilineValue.push(line);
|
|
70
|
+
result[currentKey] = multilineValue.join("\n").trim();
|
|
71
|
+
multilineValue = [];
|
|
72
|
+
currentKey = null;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check for array continuation
|
|
77
|
+
if (currentArray !== null && trimmed.startsWith("- ")) {
|
|
78
|
+
currentArray.push(trimmed.slice(2).trim());
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Reset array context if we see a new key
|
|
82
|
+
if (currentArray !== null && !trimmed.startsWith("- ")) {
|
|
83
|
+
currentArray = null;
|
|
84
|
+
}
|
|
85
|
+
// Parse key-value pair
|
|
86
|
+
const colonIndex = trimmed.indexOf(":");
|
|
87
|
+
if (colonIndex === -1)
|
|
88
|
+
continue;
|
|
89
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
90
|
+
let value = trimmed.slice(colonIndex + 1).trim();
|
|
91
|
+
// Store key for potential multiline value
|
|
92
|
+
currentKey = key;
|
|
93
|
+
// Handle JSON-style values (for PoolBot legacy format)
|
|
94
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
95
|
+
try {
|
|
96
|
+
result[key] = JSON.parse(value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Not valid JSON, treat as string
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Handle arrays
|
|
104
|
+
if (value === "" || value === "[]") {
|
|
105
|
+
// Check if next line starts array
|
|
106
|
+
currentArray = [];
|
|
107
|
+
result[key] = currentArray;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Handle inline arrays
|
|
111
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
112
|
+
result[key] = value
|
|
113
|
+
.slice(1, -1)
|
|
114
|
+
.split(",")
|
|
115
|
+
.map((v) => v.trim().replace(/^["']|["']$/g, ""))
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Handle multiline indicator
|
|
120
|
+
if (value === "|" || value === ">" || value.startsWith("|-") || value.startsWith(">-")) {
|
|
121
|
+
multilineValue = [];
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Handle quoted strings
|
|
125
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
126
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
127
|
+
value = value.slice(1, -1);
|
|
128
|
+
}
|
|
129
|
+
// Handle booleans
|
|
130
|
+
if (value === "true") {
|
|
131
|
+
result[key] = true;
|
|
132
|
+
}
|
|
133
|
+
else if (value === "false") {
|
|
134
|
+
result[key] = false;
|
|
135
|
+
}
|
|
136
|
+
else if (/^\d+$/.test(value)) {
|
|
137
|
+
// Handle numbers
|
|
138
|
+
result[key] = parseInt(value, 10);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
result[key] = value;
|
|
142
|
+
}
|
|
143
|
+
currentKey = null;
|
|
144
|
+
}
|
|
145
|
+
// Handle any remaining multiline value
|
|
146
|
+
if (currentKey && multilineValue.length > 0) {
|
|
147
|
+
result[currentKey] = multilineValue.join("\n").trim();
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Section Parser
|
|
153
|
+
// ============================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Parse markdown body into sections
|
|
156
|
+
*/
|
|
157
|
+
function parseSections(body) {
|
|
158
|
+
const sections = new Map();
|
|
159
|
+
const matches = Array.from(body.matchAll(SECTION_REGEX));
|
|
160
|
+
for (let i = 0; i < matches.length; i++) {
|
|
161
|
+
const match = matches[i];
|
|
162
|
+
const sectionName = match[1].toLowerCase().trim();
|
|
163
|
+
const startIndex = match.index + match[0].length;
|
|
164
|
+
const endIndex = i < matches.length - 1 ? matches[i + 1].index : body.length;
|
|
165
|
+
const content = body.slice(startIndex, endIndex).trim();
|
|
166
|
+
sections.set(sectionName, content);
|
|
167
|
+
}
|
|
168
|
+
return sections;
|
|
169
|
+
}
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Metadata Validation
|
|
172
|
+
// ============================================================================
|
|
173
|
+
/**
|
|
174
|
+
* Validate and transform raw frontmatter into SkillMetadata
|
|
175
|
+
* Supports both agentskills.io format and PoolBot legacy format
|
|
176
|
+
*/
|
|
177
|
+
function validateMetadata(frontmatter, options = {}) {
|
|
178
|
+
const required = options.requiredFields ?? REQUIRED_FIELDS;
|
|
179
|
+
const lenient = options.lenient ?? false;
|
|
180
|
+
// Detect format: agentskills.io has 'id', legacy has 'name' but no 'id'
|
|
181
|
+
const isLegacyFormat = !frontmatter.id && frontmatter.name;
|
|
182
|
+
// For legacy format, use name as id
|
|
183
|
+
let id;
|
|
184
|
+
if (isLegacyFormat) {
|
|
185
|
+
id = String(frontmatter.name).toLowerCase().replace(/\s+/g, "-");
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
id = frontmatter.id;
|
|
189
|
+
}
|
|
190
|
+
// Check required fields (skip id check in lenient mode for legacy)
|
|
191
|
+
for (const field of required) {
|
|
192
|
+
if (field === "id" && lenient && isLegacyFormat)
|
|
193
|
+
continue;
|
|
194
|
+
if (field === "category" && lenient && isLegacyFormat)
|
|
195
|
+
continue;
|
|
196
|
+
if (field === "version" && lenient && isLegacyFormat)
|
|
197
|
+
continue;
|
|
198
|
+
if (!(field in frontmatter) || frontmatter[field] === undefined) {
|
|
199
|
+
throw new SkillError("INVALID_METADATA", `Missing required field: ${field}`, id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Validate category (use 'custom' as default for legacy)
|
|
203
|
+
let category;
|
|
204
|
+
if (isLegacyFormat && lenient && !frontmatter.category) {
|
|
205
|
+
category = "custom";
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
category = frontmatter.category;
|
|
209
|
+
}
|
|
210
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
211
|
+
if (!lenient) {
|
|
212
|
+
throw new SkillError("INVALID_METADATA", `Invalid category: ${category}. Must be one of: ${VALID_CATEGORIES.join(", ")}`, id);
|
|
213
|
+
}
|
|
214
|
+
category = "custom";
|
|
215
|
+
}
|
|
216
|
+
// Validate id format (kebab-case) - only for non-legacy
|
|
217
|
+
if (!isLegacyFormat && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
|
|
218
|
+
throw new SkillError("INVALID_METADATA", `Invalid skill ID: ${id}. Must be kebab-case (e.g., 'my-skill-name')`, id);
|
|
219
|
+
}
|
|
220
|
+
// Validate version (basic semver check) - use '1.0.0' as default for legacy
|
|
221
|
+
let version;
|
|
222
|
+
if (isLegacyFormat && lenient && !frontmatter.version) {
|
|
223
|
+
version = "1.0.0";
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
version = frontmatter.version;
|
|
227
|
+
}
|
|
228
|
+
if (options.validateVersions !== false && !isLegacyFormat) {
|
|
229
|
+
if (!/^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/.test(version)) {
|
|
230
|
+
throw new SkillError("INVALID_METADATA", `Invalid version: ${version}. Must follow semver (e.g., '1.0.0')`, id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Parse dates
|
|
234
|
+
let createdAt;
|
|
235
|
+
let updatedAt;
|
|
236
|
+
if (frontmatter.createdAt) {
|
|
237
|
+
createdAt = new Date(frontmatter.createdAt);
|
|
238
|
+
}
|
|
239
|
+
if (frontmatter.updatedAt) {
|
|
240
|
+
updatedAt = new Date(frontmatter.updatedAt);
|
|
241
|
+
}
|
|
242
|
+
// Parse arrays
|
|
243
|
+
const tags = Array.isArray(frontmatter.tags)
|
|
244
|
+
? frontmatter.tags
|
|
245
|
+
: frontmatter.tags
|
|
246
|
+
? [String(frontmatter.tags)]
|
|
247
|
+
: [];
|
|
248
|
+
const dependencies = Array.isArray(frontmatter.dependencies)
|
|
249
|
+
? frontmatter.dependencies
|
|
250
|
+
: frontmatter.dependencies
|
|
251
|
+
? [String(frontmatter.dependencies)]
|
|
252
|
+
: undefined;
|
|
253
|
+
const externalTools = Array.isArray(frontmatter.externalTools)
|
|
254
|
+
? frontmatter.externalTools
|
|
255
|
+
: frontmatter.externalTools
|
|
256
|
+
? [String(frontmatter.externalTools)]
|
|
257
|
+
: undefined;
|
|
258
|
+
return {
|
|
259
|
+
id,
|
|
260
|
+
name: String(frontmatter.name),
|
|
261
|
+
description: String(frontmatter.description),
|
|
262
|
+
version,
|
|
263
|
+
author: frontmatter.author ? String(frontmatter.author) : undefined,
|
|
264
|
+
category: category,
|
|
265
|
+
tags,
|
|
266
|
+
minPoolBotVersion: frontmatter.minPoolBotVersion
|
|
267
|
+
? String(frontmatter.minPoolBotVersion)
|
|
268
|
+
: undefined,
|
|
269
|
+
dependencies,
|
|
270
|
+
externalTools,
|
|
271
|
+
createdAt,
|
|
272
|
+
updatedAt,
|
|
273
|
+
license: frontmatter.license ? String(frontmatter.license) : undefined,
|
|
274
|
+
repository: frontmatter.repository
|
|
275
|
+
? String(frontmatter.repository)
|
|
276
|
+
: undefined,
|
|
277
|
+
documentation: frontmatter.documentation
|
|
278
|
+
? String(frontmatter.documentation)
|
|
279
|
+
: undefined,
|
|
280
|
+
enabledByDefault: frontmatter.enabledByDefault === true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Content Extraction
|
|
285
|
+
// ============================================================================
|
|
286
|
+
/**
|
|
287
|
+
* Extract skill content from parsed sections
|
|
288
|
+
*/
|
|
289
|
+
function extractContent(sections) {
|
|
290
|
+
// Map section aliases
|
|
291
|
+
const quickstart = sections.get("quick start") ??
|
|
292
|
+
sections.get("quickstart") ??
|
|
293
|
+
sections.get("getting started");
|
|
294
|
+
const usage = sections.get("usage") ??
|
|
295
|
+
sections.get("how to use") ??
|
|
296
|
+
sections.get("using this skill") ??
|
|
297
|
+
"";
|
|
298
|
+
const configuration = sections.get("configuration") ??
|
|
299
|
+
sections.get("config") ??
|
|
300
|
+
sections.get("setup");
|
|
301
|
+
const environment = sections.get("environment") ??
|
|
302
|
+
sections.get("env") ??
|
|
303
|
+
sections.get("environment variables");
|
|
304
|
+
const examples = sections.get("examples") ??
|
|
305
|
+
sections.get("example") ??
|
|
306
|
+
sections.get("example usage");
|
|
307
|
+
const api = sections.get("api") ??
|
|
308
|
+
sections.get("api reference") ??
|
|
309
|
+
sections.get("reference");
|
|
310
|
+
const troubleshooting = sections.get("troubleshooting") ??
|
|
311
|
+
sections.get("troubleshoot") ??
|
|
312
|
+
sections.get("common issues");
|
|
313
|
+
return {
|
|
314
|
+
quickstart,
|
|
315
|
+
usage,
|
|
316
|
+
configuration,
|
|
317
|
+
environment,
|
|
318
|
+
examples,
|
|
319
|
+
api,
|
|
320
|
+
troubleshooting,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// Linked Files Discovery
|
|
325
|
+
// ============================================================================
|
|
326
|
+
/**
|
|
327
|
+
* Discover linked files in skill directory
|
|
328
|
+
*/
|
|
329
|
+
async function discoverLinkedFiles(skillDir) {
|
|
330
|
+
const linkedFiles = [];
|
|
331
|
+
try {
|
|
332
|
+
const { readdir } = await import("node:fs/promises");
|
|
333
|
+
const entries = await readdir(skillDir, { withFileTypes: true });
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (entry.isFile() && entry.name.toLowerCase() !== "skill.md") {
|
|
336
|
+
linkedFiles.push({
|
|
337
|
+
path: entry.name,
|
|
338
|
+
description: undefined, // Could parse from a manifest
|
|
339
|
+
required: false, // Could be determined by references in content
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Directory might not exist or be accessible
|
|
346
|
+
}
|
|
347
|
+
return linkedFiles;
|
|
348
|
+
}
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Main Parser Functions
|
|
351
|
+
// ============================================================================
|
|
352
|
+
/**
|
|
353
|
+
* Parse a SKILL.md file into structured data
|
|
354
|
+
*/
|
|
355
|
+
export async function parseSkillFile(filePath, options = {}) {
|
|
356
|
+
const maxSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
357
|
+
// Read file
|
|
358
|
+
let content;
|
|
359
|
+
try {
|
|
360
|
+
const stats = await import("node:fs/promises").then((fs) => fs.stat(filePath));
|
|
361
|
+
if (stats.size > maxSize) {
|
|
362
|
+
throw new SkillError("PARSE_ERROR", `Skill file too large: ${stats.size} bytes (max: ${maxSize})`, undefined);
|
|
363
|
+
}
|
|
364
|
+
content = await readFile(filePath, "utf-8");
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
if (error instanceof SkillError)
|
|
368
|
+
throw error;
|
|
369
|
+
throw new SkillError("PARSE_ERROR", `Failed to read skill file: ${error}`, undefined, error);
|
|
370
|
+
}
|
|
371
|
+
// Parse frontmatter
|
|
372
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
373
|
+
if (!match) {
|
|
374
|
+
throw new SkillError("PARSE_ERROR", "Invalid SKILL.md format: missing YAML frontmatter", undefined);
|
|
375
|
+
}
|
|
376
|
+
const [, yamlContent, bodyContent] = match;
|
|
377
|
+
// Parse YAML
|
|
378
|
+
let frontmatter;
|
|
379
|
+
try {
|
|
380
|
+
frontmatter = parseYaml(yamlContent);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
throw new SkillError("PARSE_ERROR", `Failed to parse YAML frontmatter: ${error}`, undefined, error);
|
|
384
|
+
}
|
|
385
|
+
// Parse sections
|
|
386
|
+
const sections = parseSections(bodyContent);
|
|
387
|
+
return {
|
|
388
|
+
frontmatter,
|
|
389
|
+
body: bodyContent,
|
|
390
|
+
sections,
|
|
391
|
+
sourcePath: filePath,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Parse and validate a complete skill
|
|
396
|
+
*/
|
|
397
|
+
export async function parseSkill(filePath, options = {}) {
|
|
398
|
+
// Parse the file
|
|
399
|
+
const parsed = await parseSkillFile(filePath, options);
|
|
400
|
+
// Validate metadata
|
|
401
|
+
const metadata = validateMetadata(parsed.frontmatter, options);
|
|
402
|
+
// Extract content
|
|
403
|
+
const content = extractContent(parsed.sections);
|
|
404
|
+
content.raw = parsed.body;
|
|
405
|
+
// Discover linked files
|
|
406
|
+
const skillDir = dirname(filePath.toString());
|
|
407
|
+
const linkedFiles = await discoverLinkedFiles(skillDir);
|
|
408
|
+
return {
|
|
409
|
+
metadata,
|
|
410
|
+
content,
|
|
411
|
+
linkedFiles,
|
|
412
|
+
sourcePath: filePath,
|
|
413
|
+
status: "installed",
|
|
414
|
+
verification: "unverified",
|
|
415
|
+
enabled: metadata.enabledByDefault,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Parse multiple skill files
|
|
420
|
+
*/
|
|
421
|
+
export async function parseSkills(filePaths, options = {}) {
|
|
422
|
+
const skills = [];
|
|
423
|
+
const errors = new Map();
|
|
424
|
+
await Promise.all(filePaths.map(async (path) => {
|
|
425
|
+
try {
|
|
426
|
+
const skill = await parseSkill(path, options);
|
|
427
|
+
skills.push(skill);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
errors.set(path, error);
|
|
431
|
+
}
|
|
432
|
+
}));
|
|
433
|
+
return { skills, errors };
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Check if a file is a valid SKILL.md
|
|
437
|
+
*/
|
|
438
|
+
export async function isSkillFile(filePath) {
|
|
439
|
+
try {
|
|
440
|
+
const fileName = basename(filePath.toString()).toLowerCase();
|
|
441
|
+
if (fileName !== "skill.md")
|
|
442
|
+
return false;
|
|
443
|
+
const content = await readFile(filePath, "utf-8");
|
|
444
|
+
return FRONTMATTER_REGEX.test(content);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Extract skill ID from file path
|
|
452
|
+
*/
|
|
453
|
+
export function extractSkillId(filePath) {
|
|
454
|
+
const dir = dirname(filePath.toString());
|
|
455
|
+
const base = basename(dir);
|
|
456
|
+
return base.toLowerCase().replace(/\s+/g, "-");
|
|
457
|
+
}
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// Re-exports
|
|
460
|
+
// ============================================================================
|
|
461
|
+
export { parseYaml, parseSections, validateMetadata, extractContent };
|