@sentry/junior 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };