@agentforge/skills 0.15.0

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/dist/index.js ADDED
@@ -0,0 +1,828 @@
1
+ import matter from 'gray-matter';
2
+ import { existsSync, readdirSync, statSync, readFileSync, realpathSync } from 'fs';
3
+ import { resolve, basename, isAbsolute, relative } from 'path';
4
+ import { homedir } from 'os';
5
+ import { createLogger, LogLevel, ToolBuilder, ToolCategory } from '@agentforge/core';
6
+ import { z } from 'zod';
7
+
8
+ // src/types.ts
9
+ var TrustPolicyReason = /* @__PURE__ */ ((TrustPolicyReason2) => {
10
+ TrustPolicyReason2["NOT_SCRIPT"] = "not-script";
11
+ TrustPolicyReason2["WORKSPACE_TRUST"] = "workspace-trust";
12
+ TrustPolicyReason2["TRUSTED_ROOT"] = "trusted-root";
13
+ TrustPolicyReason2["UNTRUSTED_SCRIPT_DENIED"] = "untrusted-script-denied";
14
+ TrustPolicyReason2["UNTRUSTED_SCRIPT_ALLOWED"] = "untrusted-script-allowed-override";
15
+ TrustPolicyReason2["UNKNOWN_TRUST_LEVEL"] = "unknown-trust-level";
16
+ return TrustPolicyReason2;
17
+ })(TrustPolicyReason || {});
18
+ var SkillRegistryEvent = /* @__PURE__ */ ((SkillRegistryEvent2) => {
19
+ SkillRegistryEvent2["SKILL_DISCOVERED"] = "skill:discovered";
20
+ SkillRegistryEvent2["SKILL_WARNING"] = "skill:warning";
21
+ SkillRegistryEvent2["SKILL_ACTIVATED"] = "skill:activated";
22
+ SkillRegistryEvent2["SKILL_RESOURCE_LOADED"] = "skill:resource-loaded";
23
+ SkillRegistryEvent2["TRUST_POLICY_DENIED"] = "trust:policy-denied";
24
+ SkillRegistryEvent2["TRUST_POLICY_ALLOWED"] = "trust:policy-allowed";
25
+ return SkillRegistryEvent2;
26
+ })(SkillRegistryEvent || {});
27
+ var SKILL_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
28
+ var SKILL_NAME_MAX_LENGTH = 64;
29
+ var SKILL_DESCRIPTION_MAX_LENGTH = 1024;
30
+ function validateSkillName(name) {
31
+ const errors = [];
32
+ if (name === void 0 || name === null) {
33
+ errors.push({ field: "name", message: "name is required" });
34
+ return errors;
35
+ }
36
+ if (typeof name !== "string") {
37
+ errors.push({ field: "name", message: "name must be a string" });
38
+ return errors;
39
+ }
40
+ if (name.length === 0) {
41
+ errors.push({ field: "name", message: "name must not be empty" });
42
+ return errors;
43
+ }
44
+ if (name.length > SKILL_NAME_MAX_LENGTH) {
45
+ errors.push({
46
+ field: "name",
47
+ message: `name must be at most ${SKILL_NAME_MAX_LENGTH} characters (got ${name.length})`
48
+ });
49
+ return errors;
50
+ }
51
+ if (!SKILL_NAME_PATTERN.test(name)) {
52
+ errors.push({
53
+ field: "name",
54
+ message: "name must be lowercase alphanumeric with hyphens, no leading/trailing/consecutive hyphens"
55
+ });
56
+ }
57
+ if (name.includes("--")) {
58
+ errors.push({
59
+ field: "name",
60
+ message: "name must not contain consecutive hyphens"
61
+ });
62
+ }
63
+ return errors;
64
+ }
65
+ function validateSkillDescription(description) {
66
+ const errors = [];
67
+ if (description === void 0 || description === null) {
68
+ errors.push({ field: "description", message: "description is required" });
69
+ return errors;
70
+ }
71
+ if (typeof description !== "string") {
72
+ errors.push({ field: "description", message: "description must be a string" });
73
+ return errors;
74
+ }
75
+ if (description.trim().length === 0) {
76
+ errors.push({ field: "description", message: "description must not be empty" });
77
+ return errors;
78
+ }
79
+ if (description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
80
+ errors.push({
81
+ field: "description",
82
+ message: `description must be at most ${SKILL_DESCRIPTION_MAX_LENGTH} characters (got ${description.length})`
83
+ });
84
+ }
85
+ return errors;
86
+ }
87
+ function validateSkillNameMatchesDir(name, dirName) {
88
+ if (name !== dirName) {
89
+ return [{
90
+ field: "name",
91
+ message: `name "${name}" must match parent directory name "${dirName}"`
92
+ }];
93
+ }
94
+ return [];
95
+ }
96
+ function parseSkillContent(content, dirName) {
97
+ let parsed;
98
+ try {
99
+ parsed = matter(content);
100
+ } catch (err) {
101
+ return {
102
+ success: false,
103
+ error: `Failed to parse frontmatter: ${err instanceof Error ? err.message : String(err)}`
104
+ };
105
+ }
106
+ const data = parsed.data;
107
+ const errors = [
108
+ ...validateSkillName(data.name),
109
+ ...validateSkillDescription(data.description)
110
+ ];
111
+ if (typeof data.name === "string" && data.name.length > 0 && SKILL_NAME_PATTERN.test(data.name)) {
112
+ errors.push(...validateSkillNameMatchesDir(data.name, dirName));
113
+ }
114
+ if (errors.length > 0) {
115
+ return {
116
+ success: false,
117
+ error: errors.map((e) => `${e.field}: ${e.message}`).join("; ")
118
+ };
119
+ }
120
+ const metadata = {
121
+ name: data.name,
122
+ description: data.description
123
+ };
124
+ if (data.license !== void 0) {
125
+ metadata.license = String(data.license);
126
+ }
127
+ if (Array.isArray(data.compatibility)) {
128
+ metadata.compatibility = data.compatibility.map(String);
129
+ }
130
+ if (data.metadata !== void 0 && typeof data.metadata === "object" && data.metadata !== null) {
131
+ metadata.metadata = data.metadata;
132
+ }
133
+ if (Array.isArray(data["allowed-tools"])) {
134
+ metadata.allowedTools = data["allowed-tools"].map(String);
135
+ }
136
+ return {
137
+ success: true,
138
+ metadata,
139
+ body: parsed.content
140
+ };
141
+ }
142
+ var logger = createLogger("agentforge:skills:scanner", { level: LogLevel.INFO });
143
+ function expandHome(p) {
144
+ if (p.startsWith("~/") || p === "~") {
145
+ return resolve(homedir(), p.slice(2));
146
+ }
147
+ return p;
148
+ }
149
+ function scanSkillRoot(rootPath) {
150
+ const resolvedRoot = resolve(expandHome(rootPath));
151
+ const candidates = [];
152
+ if (!existsSync(resolvedRoot)) {
153
+ logger.debug("Skill root does not exist, skipping", { rootPath: resolvedRoot });
154
+ return candidates;
155
+ }
156
+ let entries;
157
+ try {
158
+ entries = readdirSync(resolvedRoot);
159
+ } catch (err) {
160
+ logger.warn("Failed to read skill root directory", {
161
+ rootPath: resolvedRoot,
162
+ error: err instanceof Error ? err.message : String(err)
163
+ });
164
+ return candidates;
165
+ }
166
+ for (const entry of entries) {
167
+ const entryPath = resolve(resolvedRoot, entry);
168
+ let stat;
169
+ try {
170
+ stat = statSync(entryPath);
171
+ } catch {
172
+ continue;
173
+ }
174
+ if (!stat.isDirectory()) {
175
+ continue;
176
+ }
177
+ const skillMdPath = resolve(entryPath, "SKILL.md");
178
+ if (!existsSync(skillMdPath)) {
179
+ continue;
180
+ }
181
+ try {
182
+ const content = readFileSync(skillMdPath, "utf-8");
183
+ candidates.push({
184
+ skillPath: entryPath,
185
+ dirName: basename(entryPath),
186
+ content,
187
+ rootPath: resolvedRoot
188
+ });
189
+ } catch (err) {
190
+ logger.warn("Failed to read SKILL.md", {
191
+ path: skillMdPath,
192
+ error: err instanceof Error ? err.message : String(err)
193
+ });
194
+ }
195
+ }
196
+ logger.debug("Scanned skill root", {
197
+ rootPath: resolvedRoot,
198
+ candidatesFound: candidates.length
199
+ });
200
+ return candidates;
201
+ }
202
+ function scanAllSkillRoots(skillRoots) {
203
+ const allCandidates = [];
204
+ for (const root of skillRoots) {
205
+ const candidates = scanSkillRoot(root);
206
+ allCandidates.push(...candidates);
207
+ }
208
+ logger.info("Skill discovery complete", {
209
+ rootsScanned: skillRoots.length,
210
+ totalCandidates: allCandidates.length
211
+ });
212
+ return allCandidates;
213
+ }
214
+
215
+ // src/trust.ts
216
+ var SCRIPT_PATH_PREFIX = "scripts/";
217
+ var SCRIPT_PATH_EXACT = "scripts";
218
+ function normalizeRootConfig(root) {
219
+ if (typeof root === "string") {
220
+ return { path: root, trust: "untrusted" };
221
+ }
222
+ return root;
223
+ }
224
+ function isScriptResource(resourcePath) {
225
+ let normalized = resourcePath.trim().replace(/\\/g, "/");
226
+ normalized = normalized.replace(/\/+/g, "/");
227
+ while (normalized.startsWith("./")) {
228
+ normalized = normalized.slice(2);
229
+ }
230
+ const lower = normalized.toLowerCase();
231
+ return lower === SCRIPT_PATH_EXACT || lower.startsWith(SCRIPT_PATH_PREFIX);
232
+ }
233
+ function evaluateTrustPolicy(resourcePath, trustLevel, allowUntrustedScripts = false) {
234
+ if (!isScriptResource(resourcePath)) {
235
+ return {
236
+ allowed: true,
237
+ reason: "not-script" /* NOT_SCRIPT */,
238
+ message: "Resource is not a script \u2014 no trust check required"
239
+ };
240
+ }
241
+ switch (trustLevel) {
242
+ case "workspace":
243
+ return {
244
+ allowed: true,
245
+ reason: "workspace-trust" /* WORKSPACE_TRUST */,
246
+ message: "Script allowed \u2014 skill root has workspace trust"
247
+ };
248
+ case "trusted":
249
+ return {
250
+ allowed: true,
251
+ reason: "trusted-root" /* TRUSTED_ROOT */,
252
+ message: "Script allowed \u2014 skill root is explicitly trusted"
253
+ };
254
+ case "untrusted":
255
+ if (allowUntrustedScripts) {
256
+ return {
257
+ allowed: true,
258
+ reason: "untrusted-script-allowed-override" /* UNTRUSTED_SCRIPT_ALLOWED */,
259
+ message: "Script from untrusted root allowed via allowUntrustedScripts override"
260
+ };
261
+ }
262
+ return {
263
+ allowed: false,
264
+ reason: "untrusted-script-denied" /* UNTRUSTED_SCRIPT_DENIED */,
265
+ message: `Script access denied \u2014 skill root is untrusted. Scripts from untrusted roots are blocked by default for security. To allow, set 'allowUntrustedScripts: true' in SkillRegistryConfig or promote the skill root to 'trusted' or 'workspace' trust level.`
266
+ };
267
+ default:
268
+ return {
269
+ allowed: false,
270
+ reason: "unknown-trust-level" /* UNKNOWN_TRUST_LEVEL */,
271
+ message: `Script access denied \u2014 trust level "${trustLevel}" is unknown and is treated as untrusted for security.`
272
+ };
273
+ }
274
+ }
275
+
276
+ // src/activation.ts
277
+ var logger2 = createLogger("agentforge:skills:activation", { level: LogLevel.INFO });
278
+ var activateSkillSchema = z.object({
279
+ name: z.string().describe('The name of the skill to activate (e.g., "code-review")')
280
+ });
281
+ var readSkillResourceSchema = z.object({
282
+ name: z.string().describe("The name of the skill that owns the resource"),
283
+ path: z.string().describe('Relative path to the resource file within the skill directory (e.g., "references/GUIDE.md", "scripts/setup.sh")')
284
+ });
285
+ function resolveResourcePath(skillPath, resourcePath) {
286
+ if (isAbsolute(resourcePath)) {
287
+ return { success: false, error: "Absolute resource paths are not allowed" };
288
+ }
289
+ const segments = resourcePath.split(/[/\\]/);
290
+ if (segments.some((seg) => seg === "..")) {
291
+ return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
292
+ }
293
+ const resolvedPath = resolve(skillPath, resourcePath);
294
+ const resolvedSkillPath = resolve(skillPath);
295
+ const rel = relative(resolvedSkillPath, resolvedPath);
296
+ if (rel.startsWith("..") || resolve(resolvedSkillPath, rel) !== resolvedPath) {
297
+ return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
298
+ }
299
+ try {
300
+ const realSkillRoot = realpathSync(resolvedSkillPath);
301
+ const realTarget = realpathSync(resolvedPath);
302
+ const realRel = relative(realSkillRoot, realTarget);
303
+ if (realRel.startsWith("..") || isAbsolute(realRel)) {
304
+ return { success: false, error: "Symlink target escapes the skill directory \u2014 access denied" };
305
+ }
306
+ } catch {
307
+ }
308
+ return { success: true, resolvedPath };
309
+ }
310
+ function createActivateSkillTool(registry) {
311
+ return new ToolBuilder().name("activate-skill").description(
312
+ "Activate an Agent Skill by name, loading its full instructions. Returns the complete SKILL.md body content for the named skill. Use this when you see a relevant skill in <available_skills> and want to follow its instructions."
313
+ ).category(ToolCategory.SKILLS).tags(["skill", "activation", "agent-skills"]).schema(activateSkillSchema).implement(async ({ name }) => {
314
+ const skill = registry.get(name);
315
+ if (!skill) {
316
+ const availableNames = registry.getNames();
317
+ const suggestion = availableNames.length > 0 ? ` Available skills: ${availableNames.join(", ")}` : " No skills are currently registered.";
318
+ const errorMsg = `Skill "${name}" not found.${suggestion}`;
319
+ logger2.warn("Skill activation failed \u2014 not found", { name, availableCount: availableNames.length });
320
+ return errorMsg;
321
+ }
322
+ const skillMdPath = resolve(skill.skillPath, "SKILL.md");
323
+ try {
324
+ const content = readFileSync(skillMdPath, "utf-8");
325
+ const body = extractBody(content);
326
+ logger2.info("Skill activated", {
327
+ name: skill.metadata.name,
328
+ skillPath: skill.skillPath,
329
+ bodyLength: body.length
330
+ });
331
+ registry.emitEvent("skill:activated" /* SKILL_ACTIVATED */, {
332
+ name: skill.metadata.name,
333
+ skillPath: skill.skillPath,
334
+ bodyLength: body.length
335
+ });
336
+ return body;
337
+ } catch (error) {
338
+ const errorMsg = `Failed to read skill "${name}" instructions: ${error instanceof Error ? error.message : String(error)}`;
339
+ logger2.error("Skill activation failed \u2014 read error", {
340
+ name,
341
+ skillPath: skill.skillPath,
342
+ error: error instanceof Error ? error.message : String(error)
343
+ });
344
+ return errorMsg;
345
+ }
346
+ }).build();
347
+ }
348
+ function createReadSkillResourceTool(registry) {
349
+ return new ToolBuilder().name("read-skill-resource").description(
350
+ "Read a resource file from an activated Agent Skill. Returns the content of a file within the skill directory (e.g., references/, scripts/, assets/). The path must be relative to the skill root and cannot traverse outside it."
351
+ ).category(ToolCategory.SKILLS).tags(["skill", "resource", "agent-skills"]).schema(readSkillResourceSchema).implement(async ({ name, path: resourcePath }) => {
352
+ const skill = registry.get(name);
353
+ if (!skill) {
354
+ const availableNames = registry.getNames();
355
+ const suggestion = availableNames.length > 0 ? ` Available skills: ${availableNames.join(", ")}` : " No skills are currently registered.";
356
+ const errorMsg = `Skill "${name}" not found.${suggestion}`;
357
+ logger2.warn("Skill resource load failed \u2014 skill not found", { name, resourcePath });
358
+ return errorMsg;
359
+ }
360
+ const pathResult = resolveResourcePath(skill.skillPath, resourcePath);
361
+ if (!pathResult.success) {
362
+ logger2.warn("Skill resource load blocked \u2014 path traversal", {
363
+ name,
364
+ resourcePath,
365
+ error: pathResult.error
366
+ });
367
+ return pathResult.error;
368
+ }
369
+ const policyDecision = evaluateTrustPolicy(
370
+ resourcePath,
371
+ skill.trustLevel,
372
+ registry.getAllowUntrustedScripts()
373
+ );
374
+ if (!policyDecision.allowed) {
375
+ logger2.warn("Skill resource load blocked \u2014 trust policy", {
376
+ name,
377
+ resourcePath,
378
+ trustLevel: skill.trustLevel,
379
+ reason: policyDecision.reason,
380
+ message: policyDecision.message
381
+ });
382
+ registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
383
+ name: skill.metadata.name,
384
+ resourcePath,
385
+ trustLevel: skill.trustLevel,
386
+ reason: policyDecision.reason,
387
+ message: policyDecision.message
388
+ });
389
+ return policyDecision.message;
390
+ }
391
+ if (policyDecision.reason !== "not-script" /* NOT_SCRIPT */) {
392
+ logger2.info("Skill resource trust policy \u2014 allowed", {
393
+ name,
394
+ resourcePath,
395
+ trustLevel: skill.trustLevel,
396
+ reason: policyDecision.reason
397
+ });
398
+ registry.emitEvent("trust:policy-allowed" /* TRUST_POLICY_ALLOWED */, {
399
+ name: skill.metadata.name,
400
+ resourcePath,
401
+ trustLevel: skill.trustLevel,
402
+ reason: policyDecision.reason
403
+ });
404
+ }
405
+ try {
406
+ const content = readFileSync(pathResult.resolvedPath, "utf-8");
407
+ logger2.info("Skill resource loaded", {
408
+ name: skill.metadata.name,
409
+ resourcePath,
410
+ resolvedPath: pathResult.resolvedPath,
411
+ contentLength: content.length
412
+ });
413
+ registry.emitEvent("skill:resource-loaded" /* SKILL_RESOURCE_LOADED */, {
414
+ name: skill.metadata.name,
415
+ resourcePath,
416
+ resolvedPath: pathResult.resolvedPath,
417
+ contentLength: content.length
418
+ });
419
+ return content;
420
+ } catch (error) {
421
+ const errorMsg = `Failed to read resource "${resourcePath}" from skill "${name}": ${error instanceof Error ? error.message : String(error)}`;
422
+ logger2.warn("Skill resource load failed \u2014 file not found or unreadable", {
423
+ name,
424
+ resourcePath,
425
+ error: error instanceof Error ? error.message : String(error)
426
+ });
427
+ return errorMsg;
428
+ }
429
+ }).build();
430
+ }
431
+ function createSkillActivationTools(registry) {
432
+ return [
433
+ createActivateSkillTool(registry),
434
+ createReadSkillResourceTool(registry)
435
+ ];
436
+ }
437
+ function extractBody(content) {
438
+ return matter(content).content.trim();
439
+ }
440
+ var logger3 = createLogger("agentforge:skills:registry", { level: LogLevel.INFO });
441
+ var SkillRegistry = class {
442
+ skills = /* @__PURE__ */ new Map();
443
+ eventHandlers = /* @__PURE__ */ new Map();
444
+ config;
445
+ scanErrors = [];
446
+ /** Maps resolved root paths → trust levels for skill trust assignment */
447
+ rootTrustMap = /* @__PURE__ */ new Map();
448
+ /**
449
+ * Create a SkillRegistry and immediately scan configured roots for skills.
450
+ *
451
+ * @param config - Registry configuration with skill root paths
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * const registry = new SkillRegistry({
456
+ * skillRoots: ['.agentskills', '~/.agentskills', './project-skills'],
457
+ * });
458
+ * console.log(`Discovered ${registry.size()} skills`);
459
+ * ```
460
+ */
461
+ constructor(config) {
462
+ this.config = config;
463
+ this.discover();
464
+ }
465
+ /**
466
+ * Scan all configured roots and populate the registry.
467
+ *
468
+ * Called automatically during construction. Can be called again
469
+ * to re-scan (clears existing skills first).
470
+ */
471
+ discover() {
472
+ this.skills.clear();
473
+ this.scanErrors = [];
474
+ this.rootTrustMap.clear();
475
+ const normalizedRoots = this.config.skillRoots.map(normalizeRootConfig);
476
+ const plainPaths = normalizedRoots.map((r) => r.path);
477
+ for (const root of normalizedRoots) {
478
+ const resolvedPath = resolve(expandHome(root.path));
479
+ this.rootTrustMap.set(resolvedPath, root.trust);
480
+ }
481
+ const candidates = scanAllSkillRoots(plainPaths);
482
+ let successCount = 0;
483
+ let warningCount = 0;
484
+ for (const candidate of candidates) {
485
+ const result = parseSkillContent(candidate.content, candidate.dirName);
486
+ if (!result.success) {
487
+ warningCount++;
488
+ this.scanErrors.push({
489
+ path: candidate.skillPath,
490
+ error: result.error || "Unknown parse error"
491
+ });
492
+ this.emit("skill:warning" /* SKILL_WARNING */, {
493
+ skillPath: candidate.skillPath,
494
+ rootPath: candidate.rootPath,
495
+ error: result.error
496
+ });
497
+ logger3.warn("Skipping invalid skill", {
498
+ skillPath: candidate.skillPath,
499
+ error: result.error
500
+ });
501
+ continue;
502
+ }
503
+ const skill = {
504
+ metadata: result.metadata,
505
+ skillPath: candidate.skillPath,
506
+ rootPath: candidate.rootPath,
507
+ trustLevel: this.rootTrustMap.get(candidate.rootPath) ?? "untrusted"
508
+ };
509
+ if (this.skills.has(skill.metadata.name)) {
510
+ const existing = this.skills.get(skill.metadata.name);
511
+ warningCount++;
512
+ const warningMsg = `Duplicate skill name "${skill.metadata.name}" from "${candidate.rootPath}" \u2014 keeping version from "${existing.rootPath}" (first root takes precedence)`;
513
+ this.scanErrors.push({
514
+ path: candidate.skillPath,
515
+ error: warningMsg
516
+ });
517
+ this.emit("skill:warning" /* SKILL_WARNING */, {
518
+ skillPath: candidate.skillPath,
519
+ rootPath: candidate.rootPath,
520
+ duplicateOf: existing.skillPath,
521
+ error: warningMsg
522
+ });
523
+ logger3.warn("Duplicate skill name, keeping first", {
524
+ name: skill.metadata.name,
525
+ kept: existing.skillPath,
526
+ skipped: candidate.skillPath
527
+ });
528
+ continue;
529
+ }
530
+ this.skills.set(skill.metadata.name, skill);
531
+ successCount++;
532
+ this.emit("skill:discovered" /* SKILL_DISCOVERED */, skill);
533
+ logger3.debug("Skill discovered", {
534
+ name: skill.metadata.name,
535
+ description: skill.metadata.description.slice(0, 80),
536
+ skillPath: skill.skillPath
537
+ });
538
+ }
539
+ logger3.info("Skill registry populated", {
540
+ rootsScanned: this.config.skillRoots.length,
541
+ skillsDiscovered: successCount,
542
+ warnings: warningCount
543
+ });
544
+ }
545
+ // ─── Query API (parallel to ToolRegistry) ──────────────────────────────
546
+ /**
547
+ * Get a skill by name.
548
+ *
549
+ * @param name - The skill name
550
+ * @returns The skill, or undefined if not found
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * const skill = registry.get('code-review');
555
+ * if (skill) {
556
+ * console.log(skill.metadata.description);
557
+ * }
558
+ * ```
559
+ */
560
+ get(name) {
561
+ return this.skills.get(name);
562
+ }
563
+ /**
564
+ * Get all discovered skills.
565
+ *
566
+ * @returns Array of all skills
567
+ *
568
+ * @example
569
+ * ```ts
570
+ * const allSkills = registry.getAll();
571
+ * console.log(`Total skills: ${allSkills.length}`);
572
+ * ```
573
+ */
574
+ getAll() {
575
+ return Array.from(this.skills.values());
576
+ }
577
+ /**
578
+ * Check if a skill exists in the registry.
579
+ *
580
+ * @param name - The skill name
581
+ * @returns True if the skill exists
582
+ *
583
+ * @example
584
+ * ```ts
585
+ * if (registry.has('code-review')) {
586
+ * console.log('Skill available!');
587
+ * }
588
+ * ```
589
+ */
590
+ has(name) {
591
+ return this.skills.has(name);
592
+ }
593
+ /**
594
+ * Get the number of discovered skills.
595
+ *
596
+ * @returns Number of skills in the registry
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * console.log(`Registry has ${registry.size()} skills`);
601
+ * ```
602
+ */
603
+ size() {
604
+ return this.skills.size;
605
+ }
606
+ /**
607
+ * Get all skill names.
608
+ *
609
+ * @returns Array of skill names
610
+ */
611
+ getNames() {
612
+ return Array.from(this.skills.keys());
613
+ }
614
+ /**
615
+ * Get errors/warnings from the last scan.
616
+ *
617
+ * Useful for diagnostics and observability.
618
+ *
619
+ * @returns Array of scan errors with paths
620
+ */
621
+ getScanErrors() {
622
+ return this.scanErrors;
623
+ }
624
+ /**
625
+ * Check whether untrusted script access is allowed via config override.
626
+ *
627
+ * Used by activation tools to pass the override flag to trust policy checks.
628
+ *
629
+ * @returns True if `allowUntrustedScripts` is set in config
630
+ */
631
+ getAllowUntrustedScripts() {
632
+ return this.config.allowUntrustedScripts ?? false;
633
+ }
634
+ /**
635
+ * Get the `allowed-tools` list for a skill.
636
+ *
637
+ * Returns the `allowedTools` array from the skill's frontmatter metadata,
638
+ * enabling agents to filter their tool set based on what the skill expects.
639
+ *
640
+ * @param name - The skill name
641
+ * @returns Array of allowed tool names, or undefined if skill not found or field not set
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * const allowed = registry.getAllowedTools('code-review');
646
+ * if (allowed) {
647
+ * const filteredTools = allTools.filter(t => allowed.includes(t.name));
648
+ * }
649
+ * ```
650
+ */
651
+ getAllowedTools(name) {
652
+ const skill = this.skills.get(name);
653
+ return skill?.metadata.allowedTools;
654
+ }
655
+ // ─── Prompt Generation ─────────────────────────────────────────────────
656
+ /**
657
+ * Generate an `<available_skills>` XML block for system prompt injection.
658
+ *
659
+ * Returns an empty string when:
660
+ * - `config.enabled` is `false` (default) — agents operate with unmodified prompts
661
+ * - No skills match the filter criteria
662
+ *
663
+ * The output composes naturally with `toolRegistry.generatePrompt()` —
664
+ * simply concatenate both into the system prompt.
665
+ *
666
+ * @param options - Optional filtering (subset of skill names)
667
+ * @returns XML string or empty string
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * // All skills
672
+ * const xml = registry.generatePrompt();
673
+ *
674
+ * // Subset for a focused agent
675
+ * const xml = registry.generatePrompt({ skills: ['code-review', 'testing'] });
676
+ *
677
+ * // Compose with tool prompt
678
+ * const systemPrompt = [
679
+ * toolRegistry.generatePrompt(),
680
+ * skillRegistry.generatePrompt(),
681
+ * ].filter(Boolean).join('\n\n');
682
+ * ```
683
+ */
684
+ generatePrompt(options) {
685
+ if (!this.config.enabled) {
686
+ logger3.debug("Skill prompt generation skipped (disabled)", {
687
+ enabled: this.config.enabled ?? false
688
+ });
689
+ return "";
690
+ }
691
+ let skills = this.getAll();
692
+ if (options?.skills && options.skills.length > 0) {
693
+ const requested = new Set(options.skills);
694
+ skills = skills.filter((s) => requested.has(s.metadata.name));
695
+ }
696
+ if (this.config.maxDiscoveredSkills !== void 0 && this.config.maxDiscoveredSkills >= 0) {
697
+ skills = skills.slice(0, this.config.maxDiscoveredSkills);
698
+ }
699
+ if (skills.length === 0) {
700
+ logger3.debug("Skill prompt generation produced empty result", {
701
+ totalDiscovered: this.size(),
702
+ filterApplied: !!(options?.skills && options.skills.length > 0),
703
+ maxCap: this.config.maxDiscoveredSkills
704
+ });
705
+ return "";
706
+ }
707
+ const skillEntries = skills.map((skill) => {
708
+ const lines = [
709
+ " <skill>",
710
+ ` <name>${escapeXml(skill.metadata.name)}</name>`,
711
+ ` <description>${escapeXml(skill.metadata.description)}</description>`,
712
+ ` <location>${escapeXml(skill.skillPath)}</location>`,
713
+ " </skill>"
714
+ ];
715
+ return lines.join("\n");
716
+ });
717
+ const xml = `<available_skills>
718
+ ${skillEntries.join("\n")}
719
+ </available_skills>`;
720
+ const estimatedTokens = Math.ceil(xml.length / 4);
721
+ logger3.info("Skill prompt generated", {
722
+ skillCount: skills.length,
723
+ totalDiscovered: this.size(),
724
+ filterApplied: !!(options?.skills && options.skills.length > 0),
725
+ maxCap: this.config.maxDiscoveredSkills,
726
+ estimatedTokens,
727
+ xmlLength: xml.length
728
+ });
729
+ return xml;
730
+ }
731
+ // ─── Event System ──────────────────────────────────────────────────────
732
+ /**
733
+ * Register an event handler.
734
+ *
735
+ * @param event - The event to listen for
736
+ * @param handler - The handler function
737
+ *
738
+ * @example
739
+ * ```ts
740
+ * registry.on(SkillRegistryEvent.SKILL_DISCOVERED, (skill) => {
741
+ * console.log('Found skill:', skill.metadata.name);
742
+ * });
743
+ * ```
744
+ */
745
+ on(event, handler) {
746
+ if (!this.eventHandlers.has(event)) {
747
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
748
+ }
749
+ this.eventHandlers.get(event).add(handler);
750
+ }
751
+ /**
752
+ * Unregister an event handler.
753
+ *
754
+ * @param event - The event to stop listening for
755
+ * @param handler - The handler function to remove
756
+ */
757
+ off(event, handler) {
758
+ const handlers = this.eventHandlers.get(event);
759
+ if (handlers) {
760
+ handlers.delete(handler);
761
+ }
762
+ }
763
+ /**
764
+ * Emit an event to all registered handlers.
765
+ *
766
+ * @param event - The event to emit
767
+ * @param data - The event data
768
+ * @private
769
+ */
770
+ emit(event, data) {
771
+ const handlers = this.eventHandlers.get(event);
772
+ if (handlers) {
773
+ handlers.forEach((handler) => {
774
+ try {
775
+ handler(data);
776
+ } catch (error) {
777
+ logger3.error("Skill event handler error", {
778
+ event,
779
+ error: error instanceof Error ? error.message : String(error),
780
+ stack: error instanceof Error ? error.stack : void 0
781
+ });
782
+ }
783
+ });
784
+ }
785
+ }
786
+ /**
787
+ * Emit an event (public API for activation tools).
788
+ *
789
+ * Used by skill activation tools to emit `skill:activated` and
790
+ * `skill:resource-loaded` events through the registry's event system.
791
+ *
792
+ * @param event - The event to emit
793
+ * @param data - The event data
794
+ */
795
+ emitEvent(event, data) {
796
+ this.emit(event, data);
797
+ }
798
+ // ─── Tool Integration ────────────────────────────────────────────────
799
+ /**
800
+ * Create activation tools pre-wired to this registry instance.
801
+ *
802
+ * Returns `activate-skill` and `read-skill-resource` tools that
803
+ * agents can use to load skill instructions and resources on demand.
804
+ *
805
+ * @returns Array of [activate-skill, read-skill-resource] tools
806
+ *
807
+ * @example
808
+ * ```ts
809
+ * const agent = createReActAgent({
810
+ * model: llm,
811
+ * tools: [
812
+ * ...toolRegistry.toLangChainTools(),
813
+ * ...skillRegistry.toActivationTools(),
814
+ * ],
815
+ * });
816
+ * ```
817
+ */
818
+ toActivationTools() {
819
+ return createSkillActivationTools(this);
820
+ }
821
+ };
822
+ function escapeXml(str) {
823
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
824
+ }
825
+
826
+ export { SkillRegistry, SkillRegistryEvent, TrustPolicyReason, createActivateSkillTool, createReadSkillResourceTool, createSkillActivationTools, evaluateTrustPolicy, expandHome, isScriptResource, normalizeRootConfig, parseSkillContent, resolveResourcePath, scanAllSkillRoots, scanSkillRoot, validateSkillDescription, validateSkillName, validateSkillNameMatchesDir };
827
+ //# sourceMappingURL=index.js.map
828
+ //# sourceMappingURL=index.js.map