@sentry/junior 0.7.0 → 0.9.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.
@@ -1,5 +1,7 @@
1
- // src/chat/fs-utils.ts
2
- import { statSync } from "fs";
1
+ // src/chat/discovery.ts
2
+ import fs, { readdirSync, statSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
3
5
  function isDirectory(targetPath) {
4
6
  try {
5
7
  return statSync(targetPath).isDirectory();
@@ -14,11 +16,6 @@ function isFile(targetPath) {
14
16
  return false;
15
17
  }
16
18
  }
17
-
18
- // src/chat/discovery-roots.ts
19
- import { readdirSync } from "fs";
20
- import path from "path";
21
- import { fileURLToPath } from "url";
22
19
  function normalizePath(targetPath) {
23
20
  return path.resolve(targetPath);
24
21
  }
@@ -95,11 +92,16 @@ function discoverNodeModulesDirs(cwd = process.cwd(), options) {
95
92
  ]);
96
93
  }
97
94
  function discoverProjectRoots(cwd = process.cwd(), options) {
98
- const roots = discoverNodeModulesDirs(cwd, options?.nodeModulesDirs ? { candidateDirs: options.nodeModulesDirs } : void 0).map((nodeModulesDir) => path.dirname(nodeModulesDir));
95
+ const roots = discoverNodeModulesDirs(
96
+ cwd,
97
+ options?.nodeModulesDirs ? { candidateDirs: options.nodeModulesDirs } : void 0
98
+ ).map((nodeModulesDir) => path.dirname(nodeModulesDir));
99
99
  return uniqueResolvedPathsInOrder([cwd, ...roots]);
100
100
  }
101
101
  function listTopLevelPackages(nodeModulesDir) {
102
- const entries = readdirSync(nodeModulesDir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== ".bin" && entry.name !== ".pnpm").sort((left, right) => left.name.localeCompare(right.name));
102
+ const entries = readdirSync(nodeModulesDir, { withFileTypes: true }).filter(
103
+ (entry) => !entry.name.startsWith(".") && entry.name !== ".bin" && entry.name !== ".pnpm"
104
+ ).sort((left, right) => left.name.localeCompare(right.name));
103
105
  const packages = [];
104
106
  for (const entry of entries) {
105
107
  const entryPath = path.join(nodeModulesDir, entry.name);
@@ -107,9 +109,9 @@ function listTopLevelPackages(nodeModulesDir) {
107
109
  if (!isDirectory(entryPath)) {
108
110
  continue;
109
111
  }
110
- const scopedEntries = readdirSync(entryPath, { withFileTypes: true }).sort(
111
- (left, right) => left.name.localeCompare(right.name)
112
- );
112
+ const scopedEntries = readdirSync(entryPath, {
113
+ withFileTypes: true
114
+ }).sort((left, right) => left.name.localeCompare(right.name));
113
115
  for (const scopedEntry of scopedEntries) {
114
116
  const packageName = `${entry.name}/${scopedEntry.name}`;
115
117
  const packagePath = path.join(entryPath, scopedEntry.name);
@@ -127,6 +129,107 @@ function listTopLevelPackages(nodeModulesDir) {
127
129
  }
128
130
  return packages;
129
131
  }
132
+ function unique(values) {
133
+ return [...new Set(values)];
134
+ }
135
+ function pathExists(targetPath) {
136
+ try {
137
+ fs.accessSync(targetPath);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function hasAnyDataMarkers(appDir) {
144
+ return pathExists(path.join(appDir, "SOUL.md")) || pathExists(path.join(appDir, "ABOUT.md"));
145
+ }
146
+ function scoreAppCandidate(appDir) {
147
+ let score = 0;
148
+ if (pathExists(path.join(appDir, "SOUL.md"))) {
149
+ score += 4;
150
+ }
151
+ if (pathExists(path.join(appDir, "ABOUT.md"))) {
152
+ score += 2;
153
+ }
154
+ if (pathExists(path.join(appDir, "skills"))) {
155
+ score += 1;
156
+ }
157
+ if (pathExists(path.join(appDir, "plugins"))) {
158
+ score += 1;
159
+ }
160
+ return score;
161
+ }
162
+ function resolveCandidateAppDirs(cwd, projectRoots) {
163
+ const roots = projectRoots ?? discoverProjectRoots(cwd);
164
+ const resolved = [];
165
+ const seen = /* @__PURE__ */ new Set();
166
+ for (const root of roots) {
167
+ const appDir = path.resolve(root, "app");
168
+ if (!pathExists(appDir)) {
169
+ continue;
170
+ }
171
+ if (seen.has(appDir)) {
172
+ continue;
173
+ }
174
+ seen.add(appDir);
175
+ resolved.push(appDir);
176
+ }
177
+ return resolved;
178
+ }
179
+ function homeDir() {
180
+ return resolveHomeDir();
181
+ }
182
+ function resolveHomeDir(cwd = process.cwd(), options) {
183
+ const resolvedCwd = path.resolve(cwd);
184
+ const directApp = path.resolve(resolvedCwd, "app");
185
+ if (pathExists(directApp) && hasAnyDataMarkers(directApp)) {
186
+ return directApp;
187
+ }
188
+ const candidates = resolveCandidateAppDirs(
189
+ resolvedCwd,
190
+ options?.projectRoots
191
+ );
192
+ if (candidates.length === 0) {
193
+ return directApp;
194
+ }
195
+ candidates.sort((left, right) => {
196
+ const leftScore = scoreAppCandidate(left);
197
+ const rightScore = scoreAppCandidate(right);
198
+ if (leftScore !== rightScore) {
199
+ return rightScore - leftScore;
200
+ }
201
+ const leftDistance = path.relative(resolvedCwd, left).split(path.sep).length;
202
+ const rightDistance = path.relative(resolvedCwd, right).split(path.sep).length;
203
+ if (leftDistance !== rightDistance) {
204
+ return leftDistance - rightDistance;
205
+ }
206
+ return left.localeCompare(right);
207
+ });
208
+ return candidates[0];
209
+ }
210
+ function resolveContentRoots(subdir) {
211
+ if (subdir === "data") {
212
+ return [homeDir()];
213
+ }
214
+ return [path.join(homeDir(), subdir)];
215
+ }
216
+ function dataRoots() {
217
+ return unique(resolveContentRoots("data"));
218
+ }
219
+ function skillRoots() {
220
+ return unique(resolveContentRoots("skills"));
221
+ }
222
+ function pluginRoots() {
223
+ return unique(resolveContentRoots("plugins"));
224
+ }
225
+ function soulPathCandidates() {
226
+ const candidates = dataRoots().map((root) => path.join(root, "SOUL.md"));
227
+ return unique(candidates);
228
+ }
229
+ function aboutPathCandidates() {
230
+ const candidates = dataRoots().map((root) => path.join(root, "ABOUT.md"));
231
+ return unique(candidates);
232
+ }
130
233
 
131
234
  // src/chat/plugins/package-discovery.ts
132
235
  import { readFileSync, readdirSync as readdirSync2 } from "fs";
@@ -380,7 +483,7 @@ function discoverInstalledPluginPackageContent(cwd = process.cwd(), options) {
380
483
  configuredPackageNames
381
484
  );
382
485
  const manifestRoots = [];
383
- const skillRoots = [];
486
+ const skillRoots2 = [];
384
487
  const tracingIncludes = [];
385
488
  for (const pkg of discoveredPackages) {
386
489
  const tracingBasePath = pkg.nodeModulesDir ? pathForTracingInclude(
@@ -400,7 +503,7 @@ function discoverInstalledPluginPackageContent(cwd = process.cwd(), options) {
400
503
  }
401
504
  }
402
505
  if (pkg.hasSkillsDir) {
403
- skillRoots.push(path2.join(pkg.dir, "skills"));
506
+ skillRoots2.push(path2.join(pkg.dir, "skills"));
404
507
  if (tracingBasePath) {
405
508
  tracingIncludes.push(`${tracingBasePath}/skills/**/*`);
406
509
  }
@@ -421,7 +524,7 @@ function discoverInstalledPluginPackageContent(cwd = process.cwd(), options) {
421
524
  }
422
525
  }
423
526
  if (isDirectory(path2.join(pluginDir, "skills"))) {
424
- skillRoots.push(path2.join(pluginDir, "skills"));
527
+ skillRoots2.push(path2.join(pluginDir, "skills"));
425
528
  if (tracingBasePath) {
426
529
  tracingIncludes.push(`${tracingBasePath}/skills/**/*`);
427
530
  }
@@ -432,7 +535,7 @@ function discoverInstalledPluginPackageContent(cwd = process.cwd(), options) {
432
535
  discoveredPackages.map((pkg) => pkg.name)
433
536
  ),
434
537
  manifestRoots: uniqueStringsInOrder(manifestRoots),
435
- skillRoots: uniqueStringsInOrder(skillRoots),
538
+ skillRoots: uniqueStringsInOrder(skillRoots2),
436
539
  tracingIncludes: uniqueStringsInOrder(tracingIncludes)
437
540
  };
438
541
  }
@@ -440,6 +543,10 @@ function discoverInstalledPluginPackageContent(cwd = process.cwd(), options) {
440
543
  export {
441
544
  isDirectory,
442
545
  discoverNodeModulesDirs,
443
- discoverProjectRoots,
546
+ homeDir,
547
+ skillRoots,
548
+ pluginRoots,
549
+ soulPathCandidates,
550
+ aboutPathCandidates,
444
551
  discoverInstalledPluginPackageContent
445
552
  };
@@ -0,0 +1,448 @@
1
+ import {
2
+ getPluginCapabilityProviders,
3
+ getPluginForSkillPath,
4
+ getPluginSkillRoots
5
+ } from "./chunk-ZBWWHP6Q.js";
6
+ import {
7
+ skillRoots
8
+ } from "./chunk-KCLEEKYX.js";
9
+ import {
10
+ logInfo,
11
+ logWarn
12
+ } from "./chunk-ZW4OVKF5.js";
13
+
14
+ // src/chat/skills.ts
15
+ import fs from "fs/promises";
16
+ import path from "path";
17
+ import { z } from "zod";
18
+ import { parse as parseYaml } from "yaml";
19
+
20
+ // src/chat/capabilities/catalog.ts
21
+ function getCapabilityCatalog() {
22
+ const providers = getPluginCapabilityProviders();
23
+ const capabilityToProvider = /* @__PURE__ */ new Map();
24
+ const configKeys = /* @__PURE__ */ new Set();
25
+ for (const provider of providers) {
26
+ for (const capability of provider.capabilities) {
27
+ if (capabilityToProvider.has(capability)) {
28
+ throw new Error(
29
+ `Duplicate capability registration for "${capability}"`
30
+ );
31
+ }
32
+ capabilityToProvider.set(capability, provider);
33
+ }
34
+ for (const configKey of provider.configKeys) {
35
+ configKeys.add(configKey);
36
+ }
37
+ }
38
+ return {
39
+ providers,
40
+ capabilityToProvider,
41
+ configKeys
42
+ };
43
+ }
44
+ function getCapabilityProvider(capability) {
45
+ return getCapabilityCatalog().capabilityToProvider.get(capability);
46
+ }
47
+ function isKnownCapability(capability) {
48
+ return getCapabilityCatalog().capabilityToProvider.has(capability);
49
+ }
50
+ function isKnownConfigKey(key) {
51
+ return getCapabilityCatalog().configKeys.has(key);
52
+ }
53
+ function listCapabilityProviders() {
54
+ return getCapabilityCatalog().providers.map((provider) => ({
55
+ ...provider,
56
+ capabilities: [...provider.capabilities],
57
+ configKeys: [...provider.configKeys]
58
+ }));
59
+ }
60
+ var startupCatalogSignature = null;
61
+ function logCapabilityCatalogLoadedOnce() {
62
+ const providers = listCapabilityProviders();
63
+ const signature = JSON.stringify(
64
+ providers.map((provider) => ({
65
+ provider: provider.provider,
66
+ capabilities: provider.capabilities,
67
+ configKeys: provider.configKeys,
68
+ target: provider.target
69
+ }))
70
+ );
71
+ if (startupCatalogSignature === signature) {
72
+ return;
73
+ }
74
+ startupCatalogSignature = signature;
75
+ const capabilityNames = providers.flatMap((provider) => provider.capabilities).sort();
76
+ const configKeys = [
77
+ ...new Set(providers.flatMap((provider) => provider.configKeys))
78
+ ].sort();
79
+ logInfo(
80
+ "capability_catalog_loaded",
81
+ {},
82
+ {
83
+ "app.capability.providers": providers.map(
84
+ (provider) => provider.provider
85
+ ),
86
+ "app.capability.count": capabilityNames.length,
87
+ "app.capability.names": capabilityNames,
88
+ "app.config.key_count": configKeys.length,
89
+ "app.config.keys": configKeys
90
+ },
91
+ "Loaded capability provider catalog"
92
+ );
93
+ }
94
+
95
+ // src/chat/skills.ts
96
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
97
+ var SKILL_NAME_RE = /^[a-z0-9-]+$/;
98
+ var DOTTED_TOKEN_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
99
+ var MAX_NAME_LENGTH = 64;
100
+ var MAX_DESCRIPTION_LENGTH = 1024;
101
+ var MAX_COMPATIBILITY_LENGTH = 500;
102
+ function hasAngleBrackets(value) {
103
+ return value.includes("<") || value.includes(">");
104
+ }
105
+ function validateSkillName(name) {
106
+ if (!name) return "name must not be empty";
107
+ if (name.length > MAX_NAME_LENGTH)
108
+ return `name must be <= ${MAX_NAME_LENGTH} characters`;
109
+ if (!SKILL_NAME_RE.test(name))
110
+ return "name must contain only lowercase letters, digits, and hyphens";
111
+ if (name.startsWith("-") || name.endsWith("-"))
112
+ return "name must not start or end with a hyphen";
113
+ if (name.includes("--")) return "name must not contain consecutive hyphens";
114
+ return null;
115
+ }
116
+ function createTokenFieldSchema(fieldName, example) {
117
+ return z.string({
118
+ error: `Frontmatter field "${fieldName}" must be a string when present`
119
+ }).superRefine((value, ctx) => {
120
+ const tokens = value.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
121
+ for (const token of tokens) {
122
+ if (!DOTTED_TOKEN_RE.test(token)) {
123
+ ctx.addIssue({
124
+ code: z.ZodIssueCode.custom,
125
+ message: `${fieldName} token "${token}" is invalid; expected dotted lowercase tokens (for example "${example}")`
126
+ });
127
+ return;
128
+ }
129
+ }
130
+ });
131
+ }
132
+ function parseTokenList(value) {
133
+ if (typeof value !== "string") {
134
+ return void 0;
135
+ }
136
+ const tokens = value.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
137
+ return tokens.length > 0 ? tokens : void 0;
138
+ }
139
+ var skillFrontmatterSchema = z.object({
140
+ name: z.string({ error: 'Frontmatter field "name" must be a string' }).superRefine((value, ctx) => {
141
+ const nameError = validateSkillName(value);
142
+ if (nameError) {
143
+ ctx.addIssue({
144
+ code: z.ZodIssueCode.custom,
145
+ message: nameError
146
+ });
147
+ }
148
+ }),
149
+ description: z.string({ error: 'Frontmatter field "description" must be a string' }).superRefine((value, ctx) => {
150
+ if (!value.trim()) {
151
+ ctx.addIssue({
152
+ code: z.ZodIssueCode.custom,
153
+ message: "description must not be empty"
154
+ });
155
+ return;
156
+ }
157
+ if (value.length > MAX_DESCRIPTION_LENGTH) {
158
+ ctx.addIssue({
159
+ code: z.ZodIssueCode.custom,
160
+ message: `description must be <= ${MAX_DESCRIPTION_LENGTH} characters`
161
+ });
162
+ return;
163
+ }
164
+ if (hasAngleBrackets(value)) {
165
+ ctx.addIssue({
166
+ code: z.ZodIssueCode.custom,
167
+ message: 'description must not contain "<" or ">"'
168
+ });
169
+ }
170
+ }),
171
+ metadata: z.record(z.string(), z.unknown(), {
172
+ error: 'Frontmatter field "metadata" must be an object when present'
173
+ }).optional(),
174
+ compatibility: z.string({
175
+ error: 'Frontmatter field "compatibility" must be a string when present'
176
+ }).superRefine((value, ctx) => {
177
+ if (value.length > MAX_COMPATIBILITY_LENGTH) {
178
+ ctx.addIssue({
179
+ code: z.ZodIssueCode.custom,
180
+ message: `compatibility must be <= ${MAX_COMPATIBILITY_LENGTH} characters`
181
+ });
182
+ }
183
+ }).optional(),
184
+ license: z.string({
185
+ error: 'Frontmatter field "license" must be a string when present'
186
+ }).optional(),
187
+ "allowed-tools": z.string({
188
+ error: 'Frontmatter field "allowed-tools" must be a string when present'
189
+ }).optional(),
190
+ "requires-capabilities": createTokenFieldSchema(
191
+ "requires-capabilities",
192
+ "github.issues.write"
193
+ ).optional(),
194
+ "uses-config": createTokenFieldSchema(
195
+ "uses-config",
196
+ "github.repo"
197
+ ).optional()
198
+ }).passthrough();
199
+ function stripFrontmatter(raw) {
200
+ return raw.replace(FRONTMATTER_RE, "").trim();
201
+ }
202
+ function parseSkillFile(raw, expectedName) {
203
+ const match = FRONTMATTER_RE.exec(raw);
204
+ if (!match) {
205
+ return { ok: false, error: "Missing YAML frontmatter at start of file" };
206
+ }
207
+ let parsed;
208
+ try {
209
+ parsed = parseYaml(match[1]);
210
+ } catch (error) {
211
+ return {
212
+ ok: false,
213
+ error: `Invalid YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`
214
+ };
215
+ }
216
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
217
+ return { ok: false, error: "Frontmatter must be a YAML object" };
218
+ }
219
+ const result = skillFrontmatterSchema.safeParse(parsed);
220
+ if (!result.success) {
221
+ return {
222
+ ok: false,
223
+ error: result.error.issues[0]?.message ?? "Invalid YAML frontmatter"
224
+ };
225
+ }
226
+ if (expectedName && result.data.name !== expectedName) {
227
+ return {
228
+ ok: false,
229
+ error: `name "${result.data.name}" must match directory "${expectedName}"`
230
+ };
231
+ }
232
+ const allowedTools = parseTokenList(result.data["allowed-tools"]);
233
+ const requiresCapabilities = parseTokenList(
234
+ result.data["requires-capabilities"]
235
+ );
236
+ const usesConfig = parseTokenList(result.data["uses-config"]);
237
+ return {
238
+ ok: true,
239
+ skill: {
240
+ name: result.data.name,
241
+ description: result.data.description,
242
+ body: stripFrontmatter(raw),
243
+ ...result.data.metadata ? { metadata: result.data.metadata } : {},
244
+ ...result.data.compatibility !== void 0 ? { compatibility: result.data.compatibility } : {},
245
+ ...result.data.license !== void 0 ? { license: result.data.license } : {},
246
+ ...allowedTools ? { allowedTools } : {},
247
+ ...requiresCapabilities ? { requiresCapabilities } : {},
248
+ ...usesConfig ? { usesConfig } : {}
249
+ }
250
+ };
251
+ }
252
+ var SKILL_CACHE_TTL_MS = 5e3;
253
+ var skillCache = null;
254
+ function resolveSkillRoots(options) {
255
+ const additionalRoots = options?.additionalRoots ?? [];
256
+ const envRoots = process.env.SKILL_DIRS?.split(path.delimiter).filter(Boolean) ?? [];
257
+ const defaults = skillRoots();
258
+ const pluginRoots = getPluginSkillRoots();
259
+ const seen = /* @__PURE__ */ new Set();
260
+ const resolved = [];
261
+ for (const root of [
262
+ ...additionalRoots,
263
+ ...envRoots,
264
+ ...defaults,
265
+ ...pluginRoots
266
+ ]) {
267
+ const normalized = path.resolve(root);
268
+ if (seen.has(normalized)) {
269
+ continue;
270
+ }
271
+ seen.add(normalized);
272
+ resolved.push(normalized);
273
+ }
274
+ return resolved;
275
+ }
276
+ function validateSkillMetadata(input) {
277
+ const unknownCapabilities = (input.requiresCapabilities ?? []).filter(
278
+ (capability) => !isKnownCapability(capability)
279
+ );
280
+ if (unknownCapabilities.length > 0) {
281
+ return `Unknown requires-capabilities values: ${unknownCapabilities.join(", ")}`;
282
+ }
283
+ const unknownConfigKeys = (input.usesConfig ?? []).filter(
284
+ (configKey) => !isKnownConfigKey(configKey)
285
+ );
286
+ if (unknownConfigKeys.length > 0) {
287
+ return `Unknown uses-config values: ${unknownConfigKeys.join(", ")}`;
288
+ }
289
+ return void 0;
290
+ }
291
+ async function readSkillDirectory(skillDir) {
292
+ const skillFile = path.join(skillDir, "SKILL.md");
293
+ try {
294
+ const raw = await fs.readFile(skillFile, "utf8");
295
+ const parsed = parseSkillFile(raw, path.basename(skillDir));
296
+ if (!parsed.ok) {
297
+ logWarn(
298
+ "skill_frontmatter_invalid",
299
+ {},
300
+ {
301
+ "file.path": skillDir,
302
+ "error.message": parsed.error
303
+ },
304
+ "Invalid skill frontmatter"
305
+ );
306
+ return null;
307
+ }
308
+ const {
309
+ name,
310
+ description,
311
+ allowedTools,
312
+ requiresCapabilities,
313
+ usesConfig
314
+ } = parsed.skill;
315
+ const plugin = getPluginForSkillPath(skillDir);
316
+ const metadataError = validateSkillMetadata({
317
+ requiresCapabilities,
318
+ usesConfig
319
+ });
320
+ if (metadataError) {
321
+ logWarn(
322
+ "skill_frontmatter_invalid",
323
+ {},
324
+ {
325
+ "file.path": skillDir,
326
+ "error.message": metadataError
327
+ },
328
+ "Invalid skill frontmatter"
329
+ );
330
+ return null;
331
+ }
332
+ return {
333
+ name,
334
+ description,
335
+ skillPath: skillDir,
336
+ ...plugin ? { pluginProvider: plugin.manifest.name } : {},
337
+ allowedTools,
338
+ requiresCapabilities,
339
+ usesConfig
340
+ };
341
+ } catch (error) {
342
+ logWarn(
343
+ "skill_directory_read_failed",
344
+ {},
345
+ {
346
+ "file.path": skillDir,
347
+ "error.message": error instanceof Error ? error.message : String(error)
348
+ },
349
+ "Failed to read skill directory"
350
+ );
351
+ return null;
352
+ }
353
+ }
354
+ async function discoverSkills(options) {
355
+ const roots = resolveSkillRoots(options);
356
+ const cacheKey = roots.join(path.delimiter);
357
+ if (skillCache && skillCache.expiresAt > Date.now() && skillCache.key === cacheKey) {
358
+ return skillCache.skills;
359
+ }
360
+ const discovered = [];
361
+ const seen = /* @__PURE__ */ new Set();
362
+ for (const root of roots) {
363
+ try {
364
+ const entries = await fs.readdir(root, { withFileTypes: true });
365
+ for (const entry of entries.sort(
366
+ (a, b) => a.name.localeCompare(b.name)
367
+ )) {
368
+ if (!entry.isDirectory()) {
369
+ continue;
370
+ }
371
+ const skill = await readSkillDirectory(path.join(root, entry.name));
372
+ if (skill && !seen.has(skill.name)) {
373
+ seen.add(skill.name);
374
+ discovered.push(skill);
375
+ }
376
+ }
377
+ } catch (error) {
378
+ logWarn(
379
+ "skill_root_read_failed",
380
+ {},
381
+ {
382
+ "file.directory": root,
383
+ "error.message": error instanceof Error ? error.message : String(error)
384
+ },
385
+ "Failed to read skill root"
386
+ );
387
+ }
388
+ }
389
+ const sorted = discovered.sort((a, b) => a.name.localeCompare(b.name));
390
+ skillCache = {
391
+ expiresAt: Date.now() + SKILL_CACHE_TTL_MS,
392
+ key: cacheKey,
393
+ skills: sorted
394
+ };
395
+ return sorted;
396
+ }
397
+ function parseSkillInvocation(messageText, availableSkills) {
398
+ const trimmed = messageText.trim();
399
+ const match = /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(
400
+ trimmed
401
+ );
402
+ if (!match) {
403
+ return null;
404
+ }
405
+ const skillName = match[1].toLowerCase();
406
+ if (!availableSkills.some((skill) => skill.name === skillName)) {
407
+ return null;
408
+ }
409
+ return {
410
+ skillName,
411
+ args: (match[2] ?? "").trim()
412
+ };
413
+ }
414
+ function findSkillByName(skillName, available) {
415
+ return available.find((skill) => skill.name === skillName) ?? null;
416
+ }
417
+ async function loadSkillsByName(skillNames, available) {
418
+ const selected = new Set(skillNames);
419
+ const skills = [];
420
+ for (const meta of available) {
421
+ if (!selected.has(meta.name)) {
422
+ continue;
423
+ }
424
+ const skillFile = path.join(meta.skillPath, "SKILL.md");
425
+ const raw = await fs.readFile(skillFile, "utf8");
426
+ const parsed = parseSkillFile(raw, meta.name);
427
+ if (!parsed.ok) {
428
+ throw new Error(`Invalid skill file in ${skillFile}: ${parsed.error}`);
429
+ }
430
+ skills.push({
431
+ ...meta,
432
+ body: parsed.skill.body
433
+ });
434
+ }
435
+ return skills;
436
+ }
437
+
438
+ export {
439
+ getCapabilityProvider,
440
+ listCapabilityProviders,
441
+ logCapabilityCatalogLoadedOnce,
442
+ stripFrontmatter,
443
+ parseSkillFile,
444
+ discoverSkills,
445
+ parseSkillInvocation,
446
+ findSkillByName,
447
+ loadSkillsByName
448
+ };