@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/agents/error-classifier.js +302 -0
  4. package/dist/agents/skills/security.js +217 -0
  5. package/dist/build-info.json +3 -3
  6. package/dist/cli/lazy-commands.example.js +113 -0
  7. package/dist/cli/lazy-commands.js +329 -0
  8. package/dist/cli/program/command-registry.js +13 -0
  9. package/dist/cli/program/register.skills.js +4 -0
  10. package/dist/config/config.js +1 -0
  11. package/dist/config/secrets-integration.js +88 -0
  12. package/dist/context-engine/index.js +33 -0
  13. package/dist/context-engine/legacy.js +181 -0
  14. package/dist/context-engine/registry.js +86 -0
  15. package/dist/context-engine/summarizing.js +293 -0
  16. package/dist/context-engine/types.js +7 -0
  17. package/dist/infra/abort-pattern.js +106 -0
  18. package/dist/infra/retry.js +94 -0
  19. package/dist/secrets/index.js +28 -0
  20. package/dist/secrets/resolver.js +185 -0
  21. package/dist/secrets/runtime.js +142 -0
  22. package/dist/secrets/types.js +11 -0
  23. package/dist/security/dangerous-tools.js +80 -0
  24. package/dist/security/types.js +12 -0
  25. package/dist/skills/commands.js +351 -0
  26. package/dist/skills/index.js +167 -0
  27. package/dist/skills/loader.js +282 -0
  28. package/dist/skills/parser.js +461 -0
  29. package/dist/skills/registry.js +397 -0
  30. package/dist/skills/security.js +318 -0
  31. package/dist/skills/types.js +21 -0
  32. package/dist/test-utils/index.js +219 -0
  33. package/dist/tui/index.js +595 -0
  34. package/docs/INTEGRATION_PLAN.md +475 -0
  35. package/docs/INTEGRATION_SUMMARY.md +215 -0
  36. package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
  37. package/docs/integrations/INTEGRATION_PLAN.md +424 -0
  38. package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
  39. package/docs/integrations/XYOPS_PLAN.md +978 -0
  40. package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
  41. package/docs/skills/SKILL.md +524 -0
  42. package/docs/skills.md +405 -0
  43. package/package.json +1 -1
  44. 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 };