@tickernelz/paperclip-pro-skills-catalog 2026.925.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.
Files changed (76) hide show
  1. package/LICENSE +22 -0
  2. package/catalog/bundled/docs/doc-maintenance/SKILL.md +75 -0
  3. package/catalog/bundled/paperclip-operations/issue-triage/SKILL.md +74 -0
  4. package/catalog/bundled/paperclip-operations/reflection-coach/SKILL.md +202 -0
  5. package/catalog/bundled/paperclip-operations/status-card-query/SKILL.md +137 -0
  6. package/catalog/bundled/paperclip-operations/summarize-status/SKILL.md +121 -0
  7. package/catalog/bundled/paperclip-operations/task-planning/SKILL.md +84 -0
  8. package/catalog/bundled/product/paperclip-capsules/SKILL.md +99 -0
  9. package/catalog/bundled/product/paperclip-capsules/references/generator-workflows.md +120 -0
  10. package/catalog/bundled/product/paperclip-capsules/references/hero-capsule-bank.md +189 -0
  11. package/catalog/bundled/product/paperclip-capsules/references/identicon-prototyper.md +213 -0
  12. package/catalog/bundled/product/paperclip-capsules/references/individual-status-capsules.md +113 -0
  13. package/catalog/bundled/product/wireframe/SKILL.md +193 -0
  14. package/catalog/bundled/product/wireframe/assets/site-template.html +356 -0
  15. package/catalog/bundled/product/wireframe/assets/template-mobile.svg +21 -0
  16. package/catalog/bundled/product/wireframe/assets/template.svg +24 -0
  17. package/catalog/bundled/product/wireframe/references/components.md +482 -0
  18. package/catalog/bundled/product/wireframe/references/examples.md +362 -0
  19. package/catalog/bundled/product/wireframe/references/grid-system.md +107 -0
  20. package/catalog/bundled/quality/qa-acceptance/SKILL.md +93 -0
  21. package/catalog/bundled/software-development/github-pr-workflow/SKILL.md +93 -0
  22. package/catalog/optional/browser/agent-browser/SKILL.md +93 -0
  23. package/catalog/optional/content/release-announcement/SKILL.md +218 -0
  24. package/catalog/optional/content/simplified-english/SKILL.md +40 -0
  25. package/catalog/optional/finance/ramp/SKILL.md +98 -0
  26. package/catalog/optional/product/design-critique/SKILL.md +121 -0
  27. package/catalog/optional/research/last30days/catalog-ref.json +44 -0
  28. package/catalog/optional/software-development/prepare-mcp-integration/SKILL.md +230 -0
  29. package/catalog/optional/software-development/prepare-mcp-integration/examples/notion-mcp-research-gate.md +43 -0
  30. package/dist/generated/catalog.json +1165 -0
  31. package/dist/scripts/build-catalog-manifest.d.ts +2 -0
  32. package/dist/scripts/build-catalog-manifest.d.ts.map +1 -0
  33. package/dist/scripts/build-catalog-manifest.js +15 -0
  34. package/dist/scripts/build-catalog-manifest.js.map +1 -0
  35. package/dist/scripts/validate-catalog.d.ts +2 -0
  36. package/dist/scripts/validate-catalog.d.ts.map +1 -0
  37. package/dist/scripts/validate-catalog.js +15 -0
  38. package/dist/scripts/validate-catalog.js.map +1 -0
  39. package/dist/src/catalog-builder.d.ts +16 -0
  40. package/dist/src/catalog-builder.d.ts.map +1 -0
  41. package/dist/src/catalog-builder.js +688 -0
  42. package/dist/src/catalog-builder.js.map +1 -0
  43. package/dist/src/catalog-builder.test.d.ts +2 -0
  44. package/dist/src/catalog-builder.test.d.ts.map +1 -0
  45. package/dist/src/catalog-builder.test.js +355 -0
  46. package/dist/src/catalog-builder.test.js.map +1 -0
  47. package/dist/src/frontmatter.d.ts +3 -0
  48. package/dist/src/frontmatter.d.ts.map +1 -0
  49. package/dist/src/frontmatter.js +3 -0
  50. package/dist/src/frontmatter.js.map +1 -0
  51. package/dist/src/frontmatter.test.d.ts +2 -0
  52. package/dist/src/frontmatter.test.d.ts.map +1 -0
  53. package/dist/src/frontmatter.test.js +31 -0
  54. package/dist/src/frontmatter.test.js.map +1 -0
  55. package/dist/src/index.d.ts +7 -0
  56. package/dist/src/index.d.ts.map +1 -0
  57. package/dist/src/index.js +21 -0
  58. package/dist/src/index.js.map +1 -0
  59. package/dist/src/packaged-artifacts.test.d.ts +2 -0
  60. package/dist/src/packaged-artifacts.test.d.ts.map +1 -0
  61. package/dist/src/packaged-artifacts.test.js +48 -0
  62. package/dist/src/packaged-artifacts.test.js.map +1 -0
  63. package/dist/src/release-content-cases-contract.test.d.ts +2 -0
  64. package/dist/src/release-content-cases-contract.test.d.ts.map +1 -0
  65. package/dist/src/release-content-cases-contract.test.js +37 -0
  66. package/dist/src/release-content-cases-contract.test.js.map +1 -0
  67. package/dist/src/shipped-catalog.test.d.ts +2 -0
  68. package/dist/src/shipped-catalog.test.d.ts.map +1 -0
  69. package/dist/src/shipped-catalog.test.js +179 -0
  70. package/dist/src/shipped-catalog.test.js.map +1 -0
  71. package/dist/src/types.d.ts +54 -0
  72. package/dist/src/types.d.ts.map +1 -0
  73. package/dist/src/types.js +2 -0
  74. package/dist/src/types.js.map +1 -0
  75. package/generated/catalog.json +1165 -0
  76. package/package.json +54 -0
@@ -0,0 +1,688 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { asBoolean, isPlainRecord, asString, asStringArray, parseFrontmatterMarkdown, } from "./frontmatter.js";
6
+ const CATALOG_PACKAGE_NAME = "@tickernelz/paperclip-pro-skills-catalog";
7
+ const CATALOG_SCHEMA_VERSION = 1;
8
+ const SKILL_ENTRYPOINT = "SKILL.md";
9
+ const CATALOG_REFERENCE_FILE = "catalog-ref.json";
10
+ const MAX_CATALOG_FILE_BYTES = 1024 * 1024;
11
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
+ const CATALOG_KINDS = new Set(["bundled", "optional"]);
13
+ export function formatCatalogManifest(manifest) {
14
+ return `${JSON.stringify(manifest, null, 2)}\n`;
15
+ }
16
+ export async function buildExpectedCatalogManifest(packageDir) {
17
+ const existing = await readExistingManifest(packageDir);
18
+ const firstPass = await buildCatalogManifest({
19
+ packageDir,
20
+ generatedAt: existing?.generatedAt ?? new Date().toISOString(),
21
+ });
22
+ if (existing && sameManifestExceptGeneratedAt(existing, firstPass.manifest)) {
23
+ return firstPass;
24
+ }
25
+ return buildCatalogManifest({
26
+ packageDir,
27
+ generatedAt: new Date().toISOString(),
28
+ });
29
+ }
30
+ export async function buildCatalogManifest(options) {
31
+ const packageDir = path.resolve(options.packageDir);
32
+ const packageJson = await readPackageJson(packageDir);
33
+ const existingManifest = await readExistingManifest(packageDir);
34
+ const existingSkillsById = new Map(existingManifest?.skills.map((skill) => [skill.id, skill]) ?? []);
35
+ const errors = [];
36
+ const candidates = await discoverSkillCandidates(packageDir, errors);
37
+ const skills = [];
38
+ collectCandidateUniquenessErrors(candidates, errors);
39
+ for (const candidate of candidates) {
40
+ const skill = await buildCatalogSkill(packageDir, candidate, errors, existingSkillsById.get(skillIdForCandidate(candidate)) ?? null);
41
+ if (skill)
42
+ skills.push(skill);
43
+ }
44
+ skills.sort((a, b) => a.id.localeCompare(b.id));
45
+ collectUniquenessErrors(skills, errors);
46
+ return {
47
+ manifest: {
48
+ schemaVersion: CATALOG_SCHEMA_VERSION,
49
+ packageName: CATALOG_PACKAGE_NAME,
50
+ packageVersion: packageJson.version,
51
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
52
+ skills,
53
+ },
54
+ errors,
55
+ };
56
+ }
57
+ export async function validateCatalog(packageDir) {
58
+ const expected = await buildExpectedCatalogManifest(packageDir);
59
+ const generatedPath = path.join(packageDir, "generated", "catalog.json");
60
+ const errors = [...expected.errors];
61
+ let generatedText = null;
62
+ try {
63
+ generatedText = await fs.readFile(generatedPath, "utf8");
64
+ JSON.parse(generatedText);
65
+ }
66
+ catch (error) {
67
+ errors.push(`generated/catalog.json is missing or invalid: ${errorMessage(error)}`);
68
+ }
69
+ if (generatedText !== null) {
70
+ const expectedText = formatCatalogManifest(expected.manifest);
71
+ if (generatedText !== expectedText) {
72
+ errors.push("generated/catalog.json is stale. Run pnpm --filter @tickernelz/paperclip-pro-skills-catalog build:manifest.");
73
+ }
74
+ }
75
+ return {
76
+ manifest: expected.manifest,
77
+ errors,
78
+ };
79
+ }
80
+ export async function writeCatalogManifest(packageDir) {
81
+ const result = await buildExpectedCatalogManifest(packageDir);
82
+ if (result.errors.length > 0)
83
+ return result;
84
+ const generatedDir = path.join(packageDir, "generated");
85
+ await fs.mkdir(generatedDir, { recursive: true });
86
+ await fs.writeFile(path.join(generatedDir, "catalog.json"), formatCatalogManifest(result.manifest), "utf8");
87
+ return result;
88
+ }
89
+ async function readPackageJson(packageDir) {
90
+ const packageJsonPath = path.join(packageDir, "package.json");
91
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
92
+ const version = asString(packageJson.version);
93
+ if (!version)
94
+ throw new Error(`${packageJsonPath} must declare a package version.`);
95
+ return { version };
96
+ }
97
+ async function readExistingManifest(packageDir) {
98
+ try {
99
+ return JSON.parse(await fs.readFile(path.join(packageDir, "generated", "catalog.json"), "utf8"));
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ async function discoverSkillCandidates(packageDir, errors) {
106
+ const catalogDir = path.join(packageDir, "catalog");
107
+ const candidates = [];
108
+ if (!existsSync(catalogDir)) {
109
+ errors.push("catalog directory is missing.");
110
+ return candidates;
111
+ }
112
+ await collectMisplacedSkillFiles(catalogDir, errors);
113
+ for (const kind of ["bundled", "optional"]) {
114
+ const kindDir = path.join(catalogDir, kind);
115
+ if (!existsSync(kindDir))
116
+ continue;
117
+ for (const categoryEntry of await sortedDirEntries(kindDir)) {
118
+ if (!categoryEntry.isDirectory())
119
+ continue;
120
+ const category = categoryEntry.name;
121
+ const categoryDir = path.join(kindDir, category);
122
+ for (const slugEntry of await sortedDirEntries(categoryDir)) {
123
+ if (!slugEntry.isDirectory())
124
+ continue;
125
+ const slug = slugEntry.name;
126
+ const skillDir = path.join(categoryDir, slug);
127
+ const hasLocalSkill = existsSync(path.join(skillDir, SKILL_ENTRYPOINT));
128
+ const descriptorPath = path.join(skillDir, CATALOG_REFERENCE_FILE);
129
+ const hasReference = existsSync(descriptorPath);
130
+ if (hasLocalSkill && hasReference) {
131
+ errors.push(`${relativePackagePath(packageDir, skillDir)} must contain either SKILL.md or ${CATALOG_REFERENCE_FILE}, not both.`);
132
+ continue;
133
+ }
134
+ if (!hasLocalSkill && !hasReference) {
135
+ errors.push(`${relativePackagePath(packageDir, skillDir)} is missing SKILL.md or ${CATALOG_REFERENCE_FILE}.`);
136
+ continue;
137
+ }
138
+ candidates.push(hasReference
139
+ ? { kind, category, slug, absolutePath: skillDir, source: "reference", descriptorPath }
140
+ : { kind, category, slug, absolutePath: skillDir, source: "local" });
141
+ }
142
+ }
143
+ }
144
+ return candidates;
145
+ }
146
+ async function collectMisplacedSkillFiles(catalogDir, errors) {
147
+ async function visit(dir) {
148
+ for (const entry of await sortedDirEntries(dir)) {
149
+ const absolutePath = path.join(dir, entry.name);
150
+ if (entry.isDirectory()) {
151
+ await visit(absolutePath);
152
+ continue;
153
+ }
154
+ if (entry.name !== SKILL_ENTRYPOINT && entry.name !== CATALOG_REFERENCE_FILE)
155
+ continue;
156
+ const relativePath = toPosixPath(path.relative(catalogDir, absolutePath));
157
+ const parts = relativePath.split("/");
158
+ const kind = parts[0];
159
+ if (parts.length !== 4 || !CATALOG_KINDS.has(kind)) {
160
+ errors.push(`catalog/${relativePath} is not under catalog/<bundled|optional>/<category>/<slug>/{SKILL.md,${CATALOG_REFERENCE_FILE}}.`);
161
+ }
162
+ }
163
+ }
164
+ await visit(catalogDir);
165
+ }
166
+ async function buildCatalogSkill(packageDir, candidate, errors, existingSkill) {
167
+ if (candidate.source === "reference") {
168
+ return buildReferencedCatalogSkill(packageDir, candidate, errors, existingSkill);
169
+ }
170
+ const prefix = relativePackagePath(packageDir, candidate.absolutePath);
171
+ validateSlug("category", candidate.category, prefix, errors);
172
+ validateSlug("slug", candidate.slug, prefix, errors);
173
+ const id = `paperclipai:${candidate.kind}:${candidate.category}:${candidate.slug}`;
174
+ const key = `paperclipai/${candidate.kind}/${candidate.category}/${candidate.slug}`;
175
+ const skillMarkdownPath = path.join(candidate.absolutePath, SKILL_ENTRYPOINT);
176
+ const parsed = parseFrontmatterMarkdown(await fs.readFile(skillMarkdownPath, "utf8"));
177
+ if (!parsed.hasFrontmatter) {
178
+ errors.push(`${prefix}/SKILL.md must start with YAML frontmatter.`);
179
+ }
180
+ const name = asString(parsed.frontmatter.name);
181
+ if (!name)
182
+ errors.push(`${prefix}/SKILL.md frontmatter must include name.`);
183
+ const description = asString(parsed.frontmatter.description);
184
+ if (!description)
185
+ errors.push(`${prefix}/SKILL.md frontmatter must include description.`);
186
+ const explicitKey = asString(parsed.frontmatter.key);
187
+ if (explicitKey && explicitKey !== key) {
188
+ errors.push(`${prefix}/SKILL.md key must be ${key}.`);
189
+ }
190
+ const explicitSlug = asString(parsed.frontmatter.slug);
191
+ if (explicitSlug && explicitSlug !== candidate.slug) {
192
+ errors.push(`${prefix}/SKILL.md slug must be ${candidate.slug}.`);
193
+ }
194
+ const defaultInstall = asBoolean(parsed.frontmatter.defaultInstall) ?? false;
195
+ const recommendedForRoles = readStringArrayField(parsed.frontmatter.recommendedForRoles, "recommendedForRoles", prefix, errors);
196
+ const requires = readStringArrayField(parsed.frontmatter.requires, "requires", prefix, errors);
197
+ const tags = readStringArrayField(parsed.frontmatter.tags, "tags", prefix, errors);
198
+ const files = await collectSkillFiles(packageDir, candidate.absolutePath, prefix, errors);
199
+ if (!name || !description)
200
+ return null;
201
+ return {
202
+ id,
203
+ key,
204
+ kind: candidate.kind,
205
+ category: candidate.category,
206
+ slug: candidate.slug,
207
+ name,
208
+ description,
209
+ path: toPosixPath(path.relative(packageDir, candidate.absolutePath)),
210
+ entrypoint: SKILL_ENTRYPOINT,
211
+ trustLevel: deriveTrustLevel(files),
212
+ compatibility: "compatible",
213
+ defaultInstall,
214
+ recommendedForRoles,
215
+ requires,
216
+ tags,
217
+ files,
218
+ contentHash: buildContentHash(files),
219
+ };
220
+ }
221
+ function skillIdForCandidate(candidate) {
222
+ return `paperclipai:${candidate.kind}:${candidate.category}:${candidate.slug}`;
223
+ }
224
+ async function buildReferencedCatalogSkill(packageDir, candidate, errors, existingSkill) {
225
+ const prefix = relativePackagePath(packageDir, candidate.absolutePath);
226
+ validateSlug("category", candidate.category, prefix, errors);
227
+ validateSlug("slug", candidate.slug, prefix, errors);
228
+ const descriptor = await readReferencedSkillDescriptor(candidate.descriptorPath, prefix, errors);
229
+ if (!descriptor)
230
+ return null;
231
+ const id = `paperclipai:${candidate.kind}:${candidate.category}:${candidate.slug}`;
232
+ const key = `paperclipai/${candidate.kind}/${candidate.category}/${candidate.slug}`;
233
+ const source = buildCatalogSkillSource(descriptor.source, errors, `${prefix}/${CATALOG_REFERENCE_FILE}`);
234
+ if (!source)
235
+ return null;
236
+ const fallbackSkill = canReuseExistingReferencedSkill(existingSkill, candidate, source, toPosixPath(path.relative(packageDir, candidate.absolutePath)))
237
+ ? existingSkill
238
+ : null;
239
+ const errorStart = errors.length;
240
+ const files = await collectReferencedSkillFiles(source, descriptor.files ?? [SKILL_ENTRYPOINT], prefix, errors);
241
+ const skillMarkdown = await readReferencedFileText(source, SKILL_ENTRYPOINT, prefix, errors);
242
+ if (!skillMarkdown) {
243
+ const nextErrors = errors.slice(errorStart);
244
+ if (fallbackSkill && canFallbackToExistingReferencedSkill(nextErrors)) {
245
+ errors.splice(errorStart, nextErrors.length);
246
+ return fallbackSkill;
247
+ }
248
+ return null;
249
+ }
250
+ const parsed = parseFrontmatterMarkdown(skillMarkdown);
251
+ if (!parsed.hasFrontmatter) {
252
+ errors.push(`${source.url}/${SKILL_ENTRYPOINT} must start with YAML frontmatter.`);
253
+ }
254
+ const name = asString(parsed.frontmatter.name);
255
+ if (!name)
256
+ errors.push(`${source.url}/${SKILL_ENTRYPOINT} frontmatter must include name.`);
257
+ const description = asString(parsed.frontmatter.description);
258
+ if (!description)
259
+ errors.push(`${source.url}/${SKILL_ENTRYPOINT} frontmatter must include description.`);
260
+ const explicitKey = asString(parsed.frontmatter.key);
261
+ if (explicitKey && explicitKey !== key) {
262
+ errors.push(`${source.url}/${SKILL_ENTRYPOINT} key must be ${key}.`);
263
+ }
264
+ const explicitSlug = asString(parsed.frontmatter.slug);
265
+ if (explicitSlug && explicitSlug !== candidate.slug) {
266
+ errors.push(`${source.url}/${SKILL_ENTRYPOINT} slug must be ${candidate.slug}.`);
267
+ }
268
+ const defaultInstall = asBoolean(descriptor.defaultInstall) ?? false;
269
+ const recommendedForRoles = readStringArrayField(descriptor.recommendedForRoles, "recommendedForRoles", prefix, errors);
270
+ const requires = readStringArrayField(descriptor.requires, "requires", prefix, errors);
271
+ const tags = readStringArrayField(descriptor.tags, "tags", prefix, errors);
272
+ const hasSkillEntrypoint = files.some((file) => file.path === SKILL_ENTRYPOINT && file.kind === "skill");
273
+ if (!hasSkillEntrypoint) {
274
+ errors.push(`${prefix} referenced inventory does not contain SKILL.md.`);
275
+ const nextErrors = errors.slice(errorStart);
276
+ if (fallbackSkill && canFallbackToExistingReferencedSkill(nextErrors)) {
277
+ errors.splice(errorStart, nextErrors.length);
278
+ return fallbackSkill;
279
+ }
280
+ }
281
+ if (!name || !description) {
282
+ const nextErrors = errors.slice(errorStart);
283
+ if (fallbackSkill && canFallbackToExistingReferencedSkill(nextErrors)) {
284
+ errors.splice(errorStart, nextErrors.length);
285
+ return fallbackSkill;
286
+ }
287
+ return null;
288
+ }
289
+ return {
290
+ id,
291
+ key,
292
+ kind: candidate.kind,
293
+ category: candidate.category,
294
+ slug: candidate.slug,
295
+ name,
296
+ description,
297
+ path: toPosixPath(path.relative(packageDir, candidate.absolutePath)),
298
+ entrypoint: SKILL_ENTRYPOINT,
299
+ trustLevel: deriveTrustLevel(files),
300
+ compatibility: "compatible",
301
+ defaultInstall,
302
+ recommendedForRoles,
303
+ requires,
304
+ tags,
305
+ files,
306
+ contentHash: buildContentHash(files),
307
+ source,
308
+ };
309
+ }
310
+ function canReuseExistingReferencedSkill(existingSkill, candidate, source, expectedPath) {
311
+ if (!existingSkill || existingSkill.source?.type !== "github")
312
+ return false;
313
+ const existingSource = existingSkill.source;
314
+ return (existingSkill.id === skillIdForCandidate(candidate) &&
315
+ existingSkill.path === expectedPath &&
316
+ existingSource.hostname === source.hostname &&
317
+ existingSource.owner === source.owner &&
318
+ existingSource.repo === source.repo &&
319
+ existingSource.ref === source.ref &&
320
+ existingSource.commit === source.commit &&
321
+ existingSource.path === source.path);
322
+ }
323
+ function canFallbackToExistingReferencedSkill(errors) {
324
+ if (errors.length === 0)
325
+ return false;
326
+ const hasRecoverableFetchError = errors.some((error) => isRecoverableReferencedFetchError(error));
327
+ return (hasRecoverableFetchError &&
328
+ errors.every((error) => isReferencedFetchError(error) ||
329
+ error.includes("referenced inventory does not contain SKILL.md.")));
330
+ }
331
+ function isReferencedFetchError(error) {
332
+ return error.includes("failed to fetch GitHub tree:") || error.includes("failed to fetch pinned GitHub file:");
333
+ }
334
+ function isRecoverableReferencedFetchError(error) {
335
+ if (!isReferencedFetchError(error))
336
+ return false;
337
+ const statusMatch = /HTTP (\d+)/.exec(error);
338
+ if (!statusMatch)
339
+ return true;
340
+ const status = Number(statusMatch[1]);
341
+ return status === 403 || status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
342
+ }
343
+ async function readReferencedSkillDescriptor(descriptorPath, prefix, errors) {
344
+ let raw;
345
+ try {
346
+ raw = JSON.parse(await fs.readFile(descriptorPath, "utf8"));
347
+ }
348
+ catch (error) {
349
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} is missing or invalid JSON: ${errorMessage(error)}`);
350
+ return null;
351
+ }
352
+ if (!isPlainRecord(raw) || !isPlainRecord(raw.source)) {
353
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} must include a source object.`);
354
+ return null;
355
+ }
356
+ const sourceRaw = raw.source;
357
+ if (asString(sourceRaw.type) !== "github") {
358
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} source.type must be "github".`);
359
+ return null;
360
+ }
361
+ const owner = asString(sourceRaw.owner);
362
+ const repo = asString(sourceRaw.repo);
363
+ const ref = asString(sourceRaw.ref);
364
+ const commit = asString(sourceRaw.commit);
365
+ const sourcePath = asString(sourceRaw.path);
366
+ if (!owner || !repo || !ref || !commit || sourcePath === null) {
367
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} GitHub source must include owner, repo, ref, commit, and path.`);
368
+ return null;
369
+ }
370
+ const descriptor = {
371
+ source: {
372
+ type: "github",
373
+ hostname: asString(sourceRaw.hostname) ?? "github.com",
374
+ owner,
375
+ repo,
376
+ ref,
377
+ commit,
378
+ path: sourcePath,
379
+ },
380
+ defaultInstall: asBoolean(raw.defaultInstall) ?? false,
381
+ files: asStringArray(raw.files ?? undefined) ?? undefined,
382
+ recommendedForRoles: asStringArray(raw.recommendedForRoles ?? undefined) ?? undefined,
383
+ requires: asStringArray(raw.requires ?? undefined) ?? undefined,
384
+ tags: asStringArray(raw.tags ?? undefined) ?? undefined,
385
+ };
386
+ if (raw.files !== undefined && !descriptor.files)
387
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} files must be an array of strings.`);
388
+ if (raw.recommendedForRoles !== undefined && !descriptor.recommendedForRoles)
389
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} recommendedForRoles must be an array of strings.`);
390
+ if (raw.requires !== undefined && !descriptor.requires)
391
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} requires must be an array of strings.`);
392
+ if (raw.tags !== undefined && !descriptor.tags)
393
+ errors.push(`${prefix}/${CATALOG_REFERENCE_FILE} tags must be an array of strings.`);
394
+ return descriptor;
395
+ }
396
+ function buildCatalogSkillSource(descriptor, errors, prefix) {
397
+ if (!/^[0-9a-f]{40}$/i.test(descriptor.commit)) {
398
+ errors.push(`${prefix} source.commit must be a 40-character Git commit SHA.`);
399
+ }
400
+ const sourcePath = normalizeReferencedPath(descriptor.path);
401
+ if (sourcePath === null) {
402
+ errors.push(`${prefix} source.path must be a portable path within the repository.`);
403
+ }
404
+ const hostname = descriptor.hostname ?? "github.com";
405
+ const url = `https://${hostname}/${descriptor.owner}/${descriptor.repo}/tree/${descriptor.ref}/${sourcePath ?? ""}`.replace(/\/$/, "");
406
+ if (!/^[0-9a-f]{40}$/i.test(descriptor.commit) || sourcePath === null)
407
+ return null;
408
+ return {
409
+ type: "github",
410
+ hostname,
411
+ owner: descriptor.owner,
412
+ repo: descriptor.repo,
413
+ ref: descriptor.ref,
414
+ commit: descriptor.commit,
415
+ path: sourcePath ?? "",
416
+ url,
417
+ };
418
+ }
419
+ async function collectReferencedSkillFiles(source, includePatterns, prefix, errors) {
420
+ const tree = await fetchGitHubTree(source, prefix, errors);
421
+ const sourceRoot = source.path ? `${source.path}/` : "";
422
+ const normalizedPatterns = [];
423
+ for (const pattern of includePatterns) {
424
+ const normalizedPattern = normalizeReferencedPath(pattern);
425
+ if (normalizedPattern === null) {
426
+ errors.push(`${prefix} referenced include path is invalid: ${pattern}`);
427
+ continue;
428
+ }
429
+ if (normalizedPattern)
430
+ normalizedPatterns.push(normalizedPattern);
431
+ }
432
+ const files = [];
433
+ for (const entry of tree) {
434
+ if (entry.type !== "blob")
435
+ continue;
436
+ if (!entry.path.startsWith(sourceRoot))
437
+ continue;
438
+ const relativePath = entry.path.slice(sourceRoot.length);
439
+ if (!normalizedPatterns.some((pattern) => referencedPathMatches(relativePath, pattern)))
440
+ continue;
441
+ if ((entry.size ?? 0) > MAX_CATALOG_FILE_BYTES) {
442
+ errors.push(`${prefix}/${relativePath} exceeds ${MAX_CATALOG_FILE_BYTES} bytes.`);
443
+ continue;
444
+ }
445
+ const bytes = await fetchReferencedFileBytes(source, relativePath, prefix, errors);
446
+ if (!bytes)
447
+ continue;
448
+ files.push({
449
+ path: relativePath,
450
+ kind: classifyCatalogFile(relativePath),
451
+ sizeBytes: bytes.byteLength,
452
+ sha256: sha256(bytes),
453
+ });
454
+ }
455
+ files.sort((a, b) => {
456
+ if (a.path === SKILL_ENTRYPOINT)
457
+ return -1;
458
+ if (b.path === SKILL_ENTRYPOINT)
459
+ return 1;
460
+ return a.path.localeCompare(b.path);
461
+ });
462
+ return files;
463
+ }
464
+ async function fetchGitHubTree(source, prefix, errors) {
465
+ const url = `${githubApiBase(source.hostname)}/repos/${source.owner}/${source.repo}/git/trees/${source.commit}?recursive=1`;
466
+ try {
467
+ const response = await fetch(url, { headers: { accept: "application/vnd.github+json" } });
468
+ if (!response.ok) {
469
+ errors.push(`${prefix} failed to fetch GitHub tree: HTTP ${response.status}.`);
470
+ return [];
471
+ }
472
+ const body = await response.json();
473
+ if (body.truncated)
474
+ errors.push(`${prefix} GitHub tree response was truncated.`);
475
+ return Array.isArray(body.tree) ? body.tree : [];
476
+ }
477
+ catch (error) {
478
+ errors.push(`${prefix} failed to fetch GitHub tree: ${errorMessage(error)}.`);
479
+ return [];
480
+ }
481
+ }
482
+ async function readReferencedFileText(source, relativePath, prefix, errors) {
483
+ const bytes = await fetchReferencedFileBytes(source, relativePath, prefix, errors);
484
+ return bytes ? bytes.toString("utf8") : null;
485
+ }
486
+ async function fetchReferencedFileBytes(source, relativePath, prefix, errors) {
487
+ const normalizedPath = normalizeReferencedPath(relativePath);
488
+ if (!normalizedPath) {
489
+ errors.push(`${prefix} referenced file path is invalid: ${relativePath}`);
490
+ return null;
491
+ }
492
+ const url = rawGitHubUrl(source, normalizedPath);
493
+ try {
494
+ const response = await fetch(url);
495
+ if (!response.ok) {
496
+ errors.push(`${prefix}/${normalizedPath} failed to fetch pinned GitHub file: HTTP ${response.status}.`);
497
+ return null;
498
+ }
499
+ return Buffer.from(await response.arrayBuffer());
500
+ }
501
+ catch (error) {
502
+ errors.push(`${prefix}/${normalizedPath} failed to fetch pinned GitHub file: ${errorMessage(error)}.`);
503
+ return null;
504
+ }
505
+ }
506
+ async function collectSkillFiles(packageDir, skillDir, prefix, errors) {
507
+ const files = [];
508
+ const skillRoot = await fs.realpath(skillDir);
509
+ async function visit(dir) {
510
+ for (const entry of await sortedDirEntries(dir)) {
511
+ const absolutePath = path.join(dir, entry.name);
512
+ const lstat = await fs.lstat(absolutePath);
513
+ let stat = lstat;
514
+ let realPath = absolutePath;
515
+ if (lstat.isSymbolicLink()) {
516
+ try {
517
+ realPath = await fs.realpath(absolutePath);
518
+ stat = await fs.stat(absolutePath);
519
+ }
520
+ catch {
521
+ errors.push(`${relativePackagePath(packageDir, absolutePath)} is a broken symlink.`);
522
+ continue;
523
+ }
524
+ if (!isPathInside(skillRoot, realPath)) {
525
+ errors.push(`${relativePackagePath(packageDir, absolutePath)} points outside its skill directory.`);
526
+ continue;
527
+ }
528
+ if (stat.isDirectory()) {
529
+ errors.push(`${relativePackagePath(packageDir, absolutePath)} is a directory symlink; copy files into the skill directory instead.`);
530
+ continue;
531
+ }
532
+ }
533
+ if (stat.isDirectory()) {
534
+ await visit(absolutePath);
535
+ continue;
536
+ }
537
+ if (!stat.isFile())
538
+ continue;
539
+ const relativePath = toPosixPath(path.relative(skillDir, absolutePath));
540
+ if (path.isAbsolute(relativePath) || relativePath.split("/").includes("..")) {
541
+ errors.push(`${prefix}/${relativePath} has an invalid inventory path.`);
542
+ continue;
543
+ }
544
+ if (stat.size > MAX_CATALOG_FILE_BYTES) {
545
+ errors.push(`${prefix}/${relativePath} exceeds ${MAX_CATALOG_FILE_BYTES} bytes.`);
546
+ }
547
+ const contents = await fs.readFile(absolutePath);
548
+ files.push({
549
+ path: relativePath,
550
+ kind: classifyCatalogFile(relativePath),
551
+ sizeBytes: stat.size,
552
+ sha256: sha256(contents),
553
+ });
554
+ }
555
+ }
556
+ await visit(skillDir);
557
+ files.sort((a, b) => {
558
+ if (a.path === SKILL_ENTRYPOINT)
559
+ return -1;
560
+ if (b.path === SKILL_ENTRYPOINT)
561
+ return 1;
562
+ return a.path.localeCompare(b.path);
563
+ });
564
+ if (!files.some((file) => file.path === SKILL_ENTRYPOINT && file.kind === "skill")) {
565
+ errors.push(`${prefix} inventory does not contain SKILL.md.`);
566
+ }
567
+ return files;
568
+ }
569
+ function readStringArrayField(value, field, prefix, errors) {
570
+ const parsed = asStringArray(value);
571
+ if (!parsed) {
572
+ errors.push(`${prefix}/SKILL.md frontmatter field ${field} must be an array of strings.`);
573
+ return [];
574
+ }
575
+ return parsed;
576
+ }
577
+ function classifyCatalogFile(relativePath) {
578
+ if (relativePath === SKILL_ENTRYPOINT)
579
+ return "skill";
580
+ if (relativePath.startsWith("references/"))
581
+ return "reference";
582
+ if (relativePath.startsWith("scripts/"))
583
+ return "script";
584
+ if (relativePath.startsWith("assets/"))
585
+ return "asset";
586
+ if (relativePath.endsWith(".md") || relativePath.endsWith(".mdx"))
587
+ return "markdown";
588
+ return "other";
589
+ }
590
+ function deriveTrustLevel(files) {
591
+ if (files.some((file) => file.kind === "script"))
592
+ return "scripts_executables";
593
+ if (files.some((file) => file.kind === "asset" || file.kind === "other"))
594
+ return "assets";
595
+ return "markdown_only";
596
+ }
597
+ function buildContentHash(files) {
598
+ const hashInput = files.map((file) => ({
599
+ path: file.path,
600
+ sha256: file.sha256,
601
+ }));
602
+ return `sha256:${sha256(Buffer.from(JSON.stringify(hashInput)))}`;
603
+ }
604
+ function collectUniquenessErrors(skills, errors) {
605
+ collectDuplicateErrors(skills, "id", errors);
606
+ collectDuplicateErrors(skills, "key", errors);
607
+ collectDuplicateErrors(skills, "slug", errors);
608
+ }
609
+ function collectCandidateUniquenessErrors(candidates, errors) {
610
+ const projected = candidates.map((candidate) => ({
611
+ id: `paperclipai:${candidate.kind}:${candidate.category}:${candidate.slug}`,
612
+ key: `paperclipai/${candidate.kind}/${candidate.category}/${candidate.slug}`,
613
+ slug: candidate.slug,
614
+ path: toPosixPath(path.join("catalog", candidate.kind, candidate.category, candidate.slug)),
615
+ }));
616
+ collectUniquenessErrors(projected, errors);
617
+ }
618
+ function collectDuplicateErrors(fieldSkills, field, errors) {
619
+ const seen = new Map();
620
+ for (const skill of fieldSkills) {
621
+ const value = skill[field];
622
+ const first = seen.get(value);
623
+ if (first) {
624
+ errors.push(`Duplicate catalog ${field} "${value}" in ${first} and ${skill.path}.`);
625
+ continue;
626
+ }
627
+ seen.set(value, skill.path);
628
+ }
629
+ }
630
+ function validateSlug(label, value, prefix, errors) {
631
+ if (!SLUG_PATTERN.test(value)) {
632
+ errors.push(`${prefix} has invalid ${label} "${value}"; use lowercase URL slugs.`);
633
+ }
634
+ }
635
+ async function sortedDirEntries(dir) {
636
+ return (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
637
+ }
638
+ function sameManifestExceptGeneratedAt(a, b) {
639
+ return JSON.stringify({ ...a, generatedAt: "" }) === JSON.stringify({ ...b, generatedAt: "" });
640
+ }
641
+ function sha256(contents) {
642
+ return createHash("sha256").update(contents).digest("hex");
643
+ }
644
+ function relativePackagePath(packageDir, absolutePath) {
645
+ return toPosixPath(path.relative(packageDir, absolutePath));
646
+ }
647
+ function toPosixPath(input) {
648
+ return input.split(path.sep).join("/");
649
+ }
650
+ function normalizeReferencedPath(input) {
651
+ const normalized = input.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
652
+ if (normalized === "")
653
+ return "";
654
+ const parts = normalized.split("/");
655
+ if (parts.includes("") || parts.includes(".") || parts.includes("..") || path.posix.isAbsolute(normalized)) {
656
+ return null;
657
+ }
658
+ return normalized;
659
+ }
660
+ function referencedPathMatches(relativePath, pattern) {
661
+ if (pattern.endsWith("/**")) {
662
+ const directory = pattern.slice(0, -3);
663
+ return relativePath === directory || relativePath.startsWith(`${directory}/`);
664
+ }
665
+ return relativePath === pattern;
666
+ }
667
+ function githubApiBase(hostname) {
668
+ const normalized = hostname.toLowerCase();
669
+ return normalized === "github.com" || normalized === "www.github.com"
670
+ ? "https://api.github.com"
671
+ : `https://${hostname}/api/v3`;
672
+ }
673
+ function rawGitHubUrl(source, relativePath) {
674
+ const fullPath = source.path ? `${source.path}/${relativePath}` : relativePath;
675
+ const encodedPath = fullPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
676
+ const normalized = source.hostname.toLowerCase();
677
+ return normalized === "github.com" || normalized === "www.github.com"
678
+ ? `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${source.commit}/${encodedPath}`
679
+ : `https://${source.hostname}/raw/${source.owner}/${source.repo}/${source.commit}/${encodedPath}`;
680
+ }
681
+ function isPathInside(parent, child) {
682
+ const relativePath = path.relative(parent, child);
683
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
684
+ }
685
+ function errorMessage(error) {
686
+ return error instanceof Error ? error.message : String(error);
687
+ }
688
+ //# sourceMappingURL=catalog-builder.js.map