skilld-sdk 0.1.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.
@@ -0,0 +1,2440 @@
1
+ import { z } from "zod";
2
+ //#region src/contract/core.ts
3
+ /**
4
+ * The skilld public API contract, version 1.
5
+ *
6
+ * One descriptor per operation drives four things: the OpenAPI document, the
7
+ * server's request and response checks, the `skilld-sdk` client, and the
8
+ * route parity test. Every descriptor checks itself when it is defined, so a
9
+ * contract that cannot hold never reaches a route or a client.
10
+ *
11
+ * The wire format is the one `/api/v1` already shipped to the skilld CLI:
12
+ * bare JSON bodies, and `application/problem+json` errors. ADR-0006 records
13
+ * why there is no `{ data, meta }` envelope.
14
+ */
15
+ const SKILLD_V1_VERSION = "1";
16
+ const SKILLD_V1_PATH_PREFIX = "/api/v1";
17
+ const SKILLD_V1_ORIGIN = "https://skilld.dev";
18
+ const SKILLD_V1_RESPONSE_HEADERS = {
19
+ requestId: "X-Request-Id",
20
+ operation: "Skilld-Operation",
21
+ retryAfter: "Retry-After",
22
+ deprecation: "Deprecation"
23
+ };
24
+ const SKILLD_V1_METHODS = [
25
+ "DELETE",
26
+ "GET",
27
+ "PATCH",
28
+ "POST",
29
+ "PUT"
30
+ ];
31
+ const SKILLD_V1_ERROR_CODES = [
32
+ "INVALID_REQUEST",
33
+ "AUTH_REQUIRED",
34
+ "FORBIDDEN",
35
+ "NOT_FOUND",
36
+ "CONFLICT",
37
+ "RATE_LIMITED",
38
+ "INTERNAL_ERROR",
39
+ "SERVICE_UNAVAILABLE"
40
+ ];
41
+ const SKILLD_V1_ERROR_STATUS = {
42
+ INVALID_REQUEST: 400,
43
+ AUTH_REQUIRED: 401,
44
+ FORBIDDEN: 403,
45
+ NOT_FOUND: 404,
46
+ CONFLICT: 409,
47
+ RATE_LIMITED: 429,
48
+ INTERNAL_ERROR: 500,
49
+ SERVICE_UNAVAILABLE: 503
50
+ };
51
+ const SKILLD_V1_ERROR_TITLES = {
52
+ INVALID_REQUEST: "Invalid request",
53
+ AUTH_REQUIRED: "Authentication required",
54
+ FORBIDDEN: "Forbidden",
55
+ NOT_FOUND: "Not found",
56
+ CONFLICT: "Conflict",
57
+ RATE_LIMITED: "Request rate limited",
58
+ INTERNAL_ERROR: "Internal error",
59
+ SERVICE_UNAVAILABLE: "Service unavailable"
60
+ };
61
+ /** Every operation can answer these, so no descriptor lists them. */
62
+ const SKILLD_V1_IMPLICIT_ERRORS = ["INTERNAL_ERROR", "SERVICE_UNAVAILABLE"];
63
+ /** A retry can succeed for these codes, and for no others. */
64
+ const SKILLD_V1_RETRYABLE_ERRORS = /* @__PURE__ */ new Set(["RATE_LIMITED", "SERVICE_UNAVAILABLE"]);
65
+ function pathParameterNames(path) {
66
+ return Array.from(path.matchAll(/\{([^{}]+)\}/g), (match) => match[1]);
67
+ }
68
+ const SEGMENT = String.raw`(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\{[a-z][A-Za-z0-9]*\})`;
69
+ const PATH_TEMPLATE = new RegExp(String.raw`^/api/v1(?:/${SEGMENT})+$`);
70
+ function objectSchemaKeys(schema) {
71
+ return schema instanceof z.ZodObject ? Object.keys(schema.shape) : null;
72
+ }
73
+ function assertRequestLocations(operation) {
74
+ for (const location of [
75
+ "params",
76
+ "query",
77
+ "body"
78
+ ]) if (!(location in operation.request) || operation.request[location] === void 0) throw new TypeError(`${operation.id}: request.${location} must be a schema or null`);
79
+ for (const location of ["params", "query"]) {
80
+ const schema = operation.request[location];
81
+ if (schema !== null && objectSchemaKeys(schema) === null) throw new TypeError(`${operation.id}: request.${location} must be an object schema or null`);
82
+ }
83
+ for (const key of operation.request.query ? objectSchemaKeys(operation.request.query) ?? [] : []) if (!/^[a-z][A-Za-z0-9]*$/.test(key)) throw new TypeError(`${operation.id}: query key ${key} must be camelCase`);
84
+ if (operation.method === "GET" && operation.request.body !== null) throw new TypeError(`${operation.id}: a GET operation cannot take a body`);
85
+ }
86
+ function assertPathContract(operation) {
87
+ if (!PATH_TEMPLATE.test(operation.path)) throw new TypeError(`${operation.id}: path must be a safe literal /api/v1 template`);
88
+ const names = pathParameterNames(operation.path);
89
+ if (new Set(names).size !== names.length) throw new TypeError(`${operation.id}: path parameters must be unique`);
90
+ if (names.length === 0) {
91
+ if (operation.request.params !== null) throw new TypeError(`${operation.id}: params must be null when the path has no parameters`);
92
+ return;
93
+ }
94
+ const keys = operation.request.params ? objectSchemaKeys(operation.request.params) : null;
95
+ if (!keys) throw new TypeError(`${operation.id}: path parameters require an object params schema`);
96
+ if (names.join("\0") !== keys.join("\0")) throw new TypeError(`${operation.id}: params schema keys must match the path parameter order`);
97
+ }
98
+ function assertSemantics(operation) {
99
+ const { method, semantics, access, cache } = operation;
100
+ if (semantics.kind === "query" && method !== "GET") throw new TypeError(`${operation.id}: a query must be a GET`);
101
+ if (semantics.kind === "mutation" && method === "GET") throw new TypeError(`${operation.id}: a mutation cannot be a GET`);
102
+ if (semantics.kind === "mutation" && semantics.retry === "idempotent" && method !== "PUT" && method !== "DELETE") throw new TypeError(`${operation.id}: only PUT and DELETE mutations may retry`);
103
+ if (cache._tag === "public") {
104
+ if (semantics.kind !== "query" || access !== "public") throw new TypeError(`${operation.id}: only public queries may use a shared cache`);
105
+ if (!Number.isSafeInteger(cache.maxAgeSeconds) || cache.maxAgeSeconds < 1) throw new TypeError(`${operation.id}: cache max age must be a positive number of seconds`);
106
+ if (!Number.isSafeInteger(cache.staleWhileRevalidateSeconds) || cache.staleWhileRevalidateSeconds < 0) throw new TypeError(`${operation.id}: stale-while-revalidate must be a nonnegative number of seconds`);
107
+ }
108
+ }
109
+ function assertResponse(operation) {
110
+ const { status, body } = operation.response;
111
+ if (status === 204 !== (body === null)) throw new TypeError(`${operation.id}: a 204 response has no body, and every other response has one`);
112
+ if (status === 201 && operation.method !== "POST") throw new TypeError(`${operation.id}: only POST creates with 201`);
113
+ }
114
+ function assertErrors(operation) {
115
+ const errors = new Set(operation.errors);
116
+ if (errors.size !== operation.errors.length) throw new TypeError(`${operation.id}: errors must be unique`);
117
+ for (const code of SKILLD_V1_IMPLICIT_ERRORS) if (errors.has(code)) throw new TypeError(`${operation.id}: ${code} is implicit, do not list it`);
118
+ if ((operation.request.params || operation.request.query || operation.request.body) && !errors.has("INVALID_REQUEST")) throw new TypeError(`${operation.id}: an operation that takes input must declare INVALID_REQUEST`);
119
+ if (operation.access === "account" && !errors.has("AUTH_REQUIRED")) throw new TypeError(`${operation.id}: an account operation must declare AUTH_REQUIRED`);
120
+ if (operation.access === "public" && errors.has("AUTH_REQUIRED")) throw new TypeError(`${operation.id}: a public operation cannot answer AUTH_REQUIRED`);
121
+ }
122
+ function assertExampleLocation(operation, example, location) {
123
+ const schema = operation.request[location];
124
+ const supplied = Object.hasOwn(example.request, location);
125
+ if (schema === null) {
126
+ if (supplied) throw new TypeError(`${operation.id}: example supplies request.${location} without a schema`);
127
+ return;
128
+ }
129
+ const value = supplied ? example.request[location] : location === "body" ? void 0 : {};
130
+ const parsed = schema.safeParse(value);
131
+ if (!parsed.success) throw new TypeError(`${operation.id}: example request.${location} does not satisfy its schema: ${parsed.error.message}`);
132
+ }
133
+ function assertExamples(operation) {
134
+ if (operation.docs.examples.length === 0) throw new TypeError(`${operation.id}: at least one example is required`);
135
+ for (const example of operation.docs.examples) {
136
+ for (const location of [
137
+ "params",
138
+ "query",
139
+ "body"
140
+ ]) assertExampleLocation(operation, example, location);
141
+ const body = operation.response.body;
142
+ if (body === null) {
143
+ if (example.response !== null) throw new TypeError(`${operation.id}: a 204 example response must be null`);
144
+ continue;
145
+ }
146
+ const parsed = body.producer.safeParse(example.response);
147
+ if (!parsed.success) throw new TypeError(`${operation.id}: example response does not satisfy the producer schema: ${parsed.error.message}`);
148
+ }
149
+ }
150
+ function assertOperation(operation) {
151
+ if (!/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/.test(operation.id)) throw new TypeError(`${operation.id}: operation ID must be a dotted lowercase identifier`);
152
+ assertRequestLocations(operation);
153
+ assertPathContract(operation);
154
+ assertSemantics(operation);
155
+ assertResponse(operation);
156
+ assertErrors(operation);
157
+ if (!operation.docs.summary || !operation.docs.description || !operation.docs.tag) throw new TypeError(`${operation.id}: summary, description, and tag are required`);
158
+ if (operation.lifecycle.deprecated && Number.isNaN(Date.parse(operation.lifecycle.deprecated.at))) throw new TypeError(`${operation.id}: deprecation must declare a date`);
159
+ assertExamples(operation);
160
+ }
161
+ function defineOperation(operation) {
162
+ assertOperation(operation);
163
+ return operation;
164
+ }
165
+ function defineRegistry(registry) {
166
+ if (!/^[a-z][A-Za-z0-9]*$/.test(registry.namespace)) throw new TypeError(`${registry.namespace}: registry namespace must be camelCase`);
167
+ for (const key of Object.keys(registry.operations)) if (!/^[a-z][A-Za-z0-9]*$/.test(key)) throw new TypeError(`${registry.namespace}.${key}: operation keys must be camelCase`);
168
+ return registry;
169
+ }
170
+ function defineProtocol(protocol) {
171
+ const ids = /* @__PURE__ */ new Set();
172
+ const routes = /* @__PURE__ */ new Map();
173
+ for (const [name, registry] of Object.entries(protocol.registries)) {
174
+ if (name !== registry.namespace) throw new TypeError(`${name}: the registry key must equal its namespace ${registry.namespace}`);
175
+ for (const operation of Object.values(registry.operations)) {
176
+ if (ids.has(operation.id)) throw new TypeError(`Duplicate operation ID ${operation.id}`);
177
+ ids.add(operation.id);
178
+ const route = `${operation.method} ${operation.path.replace(/\{[^{}]+\}/g, "{}")}`;
179
+ const existing = routes.get(route);
180
+ if (existing) throw new TypeError(`${operation.id} and ${existing} share the route ${route}`);
181
+ routes.set(route, operation.id);
182
+ }
183
+ }
184
+ return protocol;
185
+ }
186
+ function listOperations(protocol) {
187
+ return Object.values(protocol.registries).flatMap((registry) => Object.entries(registry.operations).map(([key, operation]) => ({
188
+ registry,
189
+ key,
190
+ operation
191
+ })));
192
+ }
193
+ /** A response object with a strict server shape and a lenient client shape. */
194
+ function defineResponseObject(shape) {
195
+ return {
196
+ producer: z.strictObject(shape),
197
+ client: z.looseObject(shape)
198
+ };
199
+ }
200
+ /**
201
+ * One list response. Every list answers `{ items, total }`, the shape the
202
+ * shipped `skills.search` answer already has. `total` counts the whole result,
203
+ * not this page, so a caller pages with `offset` until it has `total` items.
204
+ */
205
+ function defineListResponse(item) {
206
+ return {
207
+ producer: z.strictObject({
208
+ items: z.array(item.producer),
209
+ total: z.number().int().nonnegative()
210
+ }),
211
+ client: z.looseObject({
212
+ items: z.array(item.client),
213
+ total: z.number().int().nonnegative()
214
+ })
215
+ };
216
+ }
217
+ /** `limit` and `offset` for a list query. A query string carries text, so both coerce. */
218
+ function pageQueryShape(options) {
219
+ return {
220
+ limit: z.coerce.number().int().min(1).max(options.maxLimit).default(options.defaultLimit),
221
+ offset: z.coerce.number().int().min(0).max(1e4).default(0)
222
+ };
223
+ }
224
+ function buildOperationPath(operation, params) {
225
+ if (pathParameterNames(operation.path).length === 0) return operation.path;
226
+ if (operation.request.params === null) throw new TypeError(`${operation.id}: operation has an invalid path contract`);
227
+ const parsed = operation.request.params.parse(params);
228
+ return operation.path.replace(/\{([^{}]+)\}/g, (_match, name) => {
229
+ const value = parsed[name];
230
+ if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`${operation.id}: path parameter ${name} must be a string or number`);
231
+ const serialized = String(value);
232
+ if (!serialized || serialized === "." || serialized === "..") throw new TypeError(`${operation.id}: path parameter ${name} cannot be empty or a dot segment`);
233
+ return encodeURIComponent(serialized);
234
+ });
235
+ }
236
+ /**
237
+ * RFC 9457 problem details. The skilld CLI parses exactly these six fields
238
+ * and rejects any other, so the producer is strict and stays this shape.
239
+ */
240
+ const problemSchema = defineResponseObject({
241
+ type: z.string().min(1),
242
+ title: z.string().min(1),
243
+ status: z.number().int().min(400).max(599),
244
+ detail: z.string().max(1e3).optional(),
245
+ instance: z.string().optional(),
246
+ code: z.enum(SKILLD_V1_ERROR_CODES)
247
+ });
248
+ function problemType(code) {
249
+ return `${SKILLD_V1_ORIGIN}/problems/${code.toLowerCase().replaceAll("_", "-")}`;
250
+ }
251
+ //#endregion
252
+ //#region src/contract/schemas.ts
253
+ /**
254
+ * Field schemas shared by every registry. One field means one thing across
255
+ * the whole contract, so a client reads `owner` or `pageUrl` the same way on
256
+ * every operation.
257
+ *
258
+ * Conventions every operation follows:
259
+ * - Field names are camelCase. Times are ISO 8601 strings. A known-absent value is `null`, never a missing key.
260
+ * - A Skill always carries its provenance: `owner`, `repository`, and `sourceUrl`, the SKILL.md in the author's Repository (VISION principle 1).
261
+ * - Machine-generated text is named for what it is: `generatedSummary`, never `summary`.
262
+ * - Install counts never appear. Stars and likes are the only counts (VISION anti-scope 4).
263
+ */
264
+ /** A GitHub login or organization name. */
265
+ const ownerSchema = z.string().min(1).max(39).regex(/^[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?$/i, "must be a GitHub login");
266
+ const loginSchema = ownerSchema;
267
+ /** A GitHub Repository name. */
268
+ const repositorySchema = z.string().min(1).max(100).regex(/^[\w.-]+$/, "must be a GitHub Repository name");
269
+ /** A Skill directory name as the registry stores it. */
270
+ const skillNameSchema = z.string().min(1).max(100).regex(/^[\w.-]+$/, "must be a Skill name");
271
+ /** A collection slug, as it appears in `/@login/slug`. */
272
+ const collectionSlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "must be lowercase letters, digits, and hyphens");
273
+ const isoDateTimeSchema = z.iso.datetime({ offset: true });
274
+ const urlSchema = z.url();
275
+ const countSchema = z.number().int().nonnegative();
276
+ const skillParams = z.strictObject({
277
+ owner: ownerSchema,
278
+ repository: repositorySchema,
279
+ name: skillNameSchema
280
+ });
281
+ const repositoryParams = z.strictObject({
282
+ owner: ownerSchema,
283
+ repository: repositorySchema
284
+ });
285
+ /** The fields every Skill card carries, on every list. */
286
+ const skillSummaryShape = {
287
+ owner: ownerSchema,
288
+ repository: repositorySchema,
289
+ name: skillNameSchema,
290
+ displayName: z.string(),
291
+ description: z.string().nullable(),
292
+ stars: countSchema,
293
+ likes: countSchema,
294
+ /** When the SKILL.md last changed upstream. */
295
+ updatedAt: isoDateTimeSchema.nullable(),
296
+ pageUrl: urlSchema,
297
+ /** The SKILL.md in the author's Repository. `null` only while the first sync is pending. */
298
+ sourceUrl: urlSchema.nullable(),
299
+ runCommand: z.string(),
300
+ installCommand: z.string()
301
+ };
302
+ const skillSummarySchema = defineResponseObject(skillSummaryShape);
303
+ const exampleSkillSummary = {
304
+ owner: "vercel-labs",
305
+ repository: "agent-skills",
306
+ name: "web-design-guidelines",
307
+ displayName: "Web Design Guidelines",
308
+ description: "Review UI code for compliance with web interface guidelines.",
309
+ stars: 18204,
310
+ likes: 41,
311
+ updatedAt: "2026-09-28T14:02:11.000Z",
312
+ pageUrl: "https://skilld.dev/gh/vercel-labs/agent-skills/web-design-guidelines",
313
+ sourceUrl: "https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md",
314
+ runCommand: "npx skilld run vercel-labs/agent-skills/web-design-guidelines",
315
+ installCommand: "npx skilld install vercel-labs/agent-skills/web-design-guidelines"
316
+ };
317
+ //#endregion
318
+ //#region src/contract/account.ts
319
+ /**
320
+ * Loop 2: the signed-in account, its likes, its watches, its imported GitHub
321
+ * stars, the changes its digest reports, and its skilld tokens.
322
+ *
323
+ * Every operation here needs a skilld.dev sign-in or a skilld token sent as a
324
+ * Bearer credential, and no answer is cached. Account deletion is not in the
325
+ * API: it needs a skilld.dev sign-in in the browser.
326
+ */
327
+ const accountErrors = ["AUTH_REQUIRED"];
328
+ const accountInputErrors = ["INVALID_REQUEST", "AUTH_REQUIRED"];
329
+ const accountSchema = defineResponseObject({
330
+ login: loginSchema,
331
+ /** The GitHub profile name. */
332
+ name: z.string().nullable(),
333
+ avatarUrl: urlSchema.nullable(),
334
+ /** The Author page, `/@login`. */
335
+ pageUrl: urlSchema,
336
+ /** The address skilld emails: the one you saved, else your GitHub address. */
337
+ email: z.string().nullable(),
338
+ /** True when the digest of your watched Repositories goes to `email`. */
339
+ digest: z.boolean(),
340
+ /** True when the weekly goes to `email`. A GitHub address alone is not consent: save an address, or turn on `digest`. */
341
+ weekly: z.boolean(),
342
+ /** True when anyone can read your liked Skills at `/@login/liked`. */
343
+ likesPublic: z.boolean(),
344
+ /** True when skilld may scan your public Repositories for Skills. */
345
+ repositoryIndexing: z.boolean(),
346
+ /** When your last GitHub star import finished. */
347
+ starsImportedAt: isoDateTimeSchema.nullable()
348
+ });
349
+ const exampleAccount = {
350
+ login: "harlan-zw",
351
+ name: "Harlan Wilton",
352
+ avatarUrl: "https://avatars.githubusercontent.com/u/5326365?v=4",
353
+ pageUrl: "https://skilld.dev/@harlan-zw",
354
+ email: "harlan@example.com",
355
+ digest: true,
356
+ weekly: true,
357
+ likesPublic: true,
358
+ repositoryIndexing: true,
359
+ starsImportedAt: "2026-09-21T08:14:02.000Z"
360
+ };
361
+ const accountUpdateBody = z.strictObject({
362
+ email: z.email().max(254).transform((address) => address.toLowerCase()).optional(),
363
+ digest: z.boolean().optional(),
364
+ weekly: z.boolean().optional(),
365
+ likesPublic: z.boolean().optional(),
366
+ repositoryIndexing: z.boolean().optional()
367
+ }).refine((body) => Object.values(body).some((value) => value !== void 0), { message: "Send at least one setting to change" });
368
+ const repositoryScanSchema = defineResponseObject({
369
+ /**
370
+ * `complete`: the scan read every Repository. `partial`: it read only some,
371
+ * so run it again later. A `github-*` outcome means GitHub refused or failed
372
+ * the search.
373
+ */
374
+ outcome: z.enum([
375
+ "complete",
376
+ "partial",
377
+ "github-auth-failure",
378
+ "github-rate-limited",
379
+ "github-failure"
380
+ ]),
381
+ /** Repositories that hold a SKILL.md. */
382
+ repositoriesFound: countSchema,
383
+ /** Repositories the registry indexed during this scan. */
384
+ repositoriesIndexed: countSchema,
385
+ repositoriesFailed: countSchema
386
+ });
387
+ const likedSkillSchema = defineResponseObject({
388
+ ...skillSummaryShape,
389
+ likedAt: isoDateTimeSchema
390
+ });
391
+ const watchSchema = defineResponseObject({
392
+ owner: ownerSchema,
393
+ repository: repositorySchema,
394
+ /** The Repository page on skilld.dev. */
395
+ pageUrl: urlSchema,
396
+ /**
397
+ * Why the watch exists. `direct`: you watched the Repository. `like`: a like
398
+ * added it, and removing your last like in the Repository removes it.
399
+ * `star-import`: you watched it from your imported stars. `collection`: you
400
+ * watched a collection that names it.
401
+ */
402
+ reason: z.enum([
403
+ "direct",
404
+ "like",
405
+ "star-import",
406
+ "collection"
407
+ ]),
408
+ watchedAt: isoDateTimeSchema
409
+ });
410
+ const starredRepositorySchema = defineResponseObject({
411
+ owner: ownerSchema,
412
+ repository: repositorySchema,
413
+ /** The Repository page on skilld.dev. */
414
+ pageUrl: urlSchema,
415
+ starredAt: isoDateTimeSchema,
416
+ /** True when you watch the Repository. */
417
+ watching: z.boolean(),
418
+ /** Skills the registry holds for the Repository. */
419
+ skillCount: countSchema
420
+ });
421
+ const starsImportSchema = defineResponseObject({
422
+ page: z.number().int().min(1).max(10),
423
+ /** The page to import next, or `null` when the import is finished. */
424
+ nextPage: z.number().int().min(2).max(10).nullable(),
425
+ /** Starred Repositories imported so far. */
426
+ imported: countSchema,
427
+ /** Imported Repositories that hold Skills. */
428
+ withSkills: countSchema,
429
+ /** Set when the last page is imported. */
430
+ importedAt: isoDateTimeSchema.nullable()
431
+ });
432
+ const changedSkillShape = {
433
+ ...skillSummaryShape,
434
+ /** The latest change in the window. */
435
+ changedAt: isoDateTimeSchema,
436
+ /** Changes in the window. */
437
+ changeCount: countSchema,
438
+ /** Commit messages from the author's Repository, newest first. The author wrote them, not skilld. */
439
+ commitMessages: z.array(z.string()),
440
+ /** The latest commit on GitHub. */
441
+ changeUrl: urlSchema
442
+ };
443
+ const changesWindowShape = {
444
+ since: isoDateTimeSchema,
445
+ /** Send this as `since` next time to read only newer changes. */
446
+ until: isoDateTimeSchema
447
+ };
448
+ const accountChangesSchema = {
449
+ producer: z.strictObject({
450
+ ...changesWindowShape,
451
+ items: z.array(z.strictObject(changedSkillShape))
452
+ }),
453
+ client: z.looseObject({
454
+ ...changesWindowShape,
455
+ items: z.array(z.looseObject(changedSkillShape))
456
+ })
457
+ };
458
+ const tokenSchema = defineResponseObject({
459
+ id: z.number().int().positive(),
460
+ label: z.string().nullable(),
461
+ /** `oauth`: a `skilld auth login` sign-in. `pat`: a token you created. `oidc`: a short CI token from GitHub Actions. */
462
+ kind: z.enum([
463
+ "oauth",
464
+ "pat",
465
+ "oidc"
466
+ ]),
467
+ createdAt: isoDateTimeSchema,
468
+ lastUsedAt: isoDateTimeSchema,
469
+ /** When the token stops working. `null` means it has no set end. */
470
+ expiresAt: isoDateTimeSchema.nullable(),
471
+ /** True for the token that sent this request. */
472
+ current: z.boolean()
473
+ });
474
+ const issuedTokenSchema = defineResponseObject({
475
+ id: z.number().int().positive(),
476
+ label: z.string(),
477
+ expiresAt: isoDateTimeSchema.nullable(),
478
+ /** The secret. This answer is the only place it appears. */
479
+ token: z.string().min(1)
480
+ });
481
+ const tokenParams = z.strictObject({ id: z.coerce.number().int().positive() });
482
+ const exampleWatch = {
483
+ owner: "vercel-labs",
484
+ repository: "agent-skills",
485
+ pageUrl: "https://skilld.dev/gh/vercel-labs/agent-skills",
486
+ reason: "like",
487
+ watchedAt: "2026-09-12T10:31:44.000Z"
488
+ };
489
+ const accountV1 = defineRegistry({
490
+ namespace: "account",
491
+ description: "Read and change the signed-in account, and manage the Skills of your own Repositories.",
492
+ operations: {
493
+ get: defineOperation({
494
+ id: "account.get",
495
+ method: "GET",
496
+ path: "/api/v1/account",
497
+ access: "account",
498
+ semantics: { kind: "query" },
499
+ cache: { _tag: "private" },
500
+ request: {
501
+ params: null,
502
+ query: null,
503
+ body: null
504
+ },
505
+ response: {
506
+ status: 200,
507
+ body: accountSchema
508
+ },
509
+ errors: accountErrors,
510
+ lifecycle: { introduced: "1.0.0" },
511
+ docs: {
512
+ summary: "Get your account",
513
+ description: "Your login, your Author page, the address skilld emails, and your email and privacy settings.",
514
+ tag: "Account",
515
+ examples: [{
516
+ request: {},
517
+ response: exampleAccount
518
+ }]
519
+ }
520
+ }),
521
+ update: defineOperation({
522
+ id: "account.update",
523
+ method: "PATCH",
524
+ path: "/api/v1/account",
525
+ access: "account",
526
+ semantics: {
527
+ kind: "mutation",
528
+ retry: "never"
529
+ },
530
+ cache: { _tag: "private" },
531
+ request: {
532
+ params: null,
533
+ query: null,
534
+ body: accountUpdateBody
535
+ },
536
+ response: {
537
+ status: 200,
538
+ body: accountSchema
539
+ },
540
+ errors: accountInputErrors,
541
+ lifecycle: { introduced: "1.0.0" },
542
+ docs: {
543
+ summary: "Change your settings",
544
+ description: "Send only the settings to change. A setting you leave out keeps its value. The new address takes effect at once, without a confirmation email. To turn on `digest` or `weekly`, the account needs an address: send `email`, or keep the saved address or your GitHub address. You cannot remove an address; turn both emails off instead. The answer is the account after the change.",
545
+ tag: "Account",
546
+ examples: [{
547
+ request: { body: {
548
+ email: "harlan@example.com",
549
+ digest: true
550
+ } },
551
+ response: exampleAccount
552
+ }, {
553
+ request: { body: { likesPublic: false } },
554
+ response: {
555
+ ...exampleAccount,
556
+ likesPublic: false
557
+ }
558
+ }]
559
+ }
560
+ }),
561
+ scanRepositories: defineOperation({
562
+ id: "account.repositories.scan",
563
+ method: "POST",
564
+ path: "/api/v1/account/repositories/scan",
565
+ access: "account",
566
+ semantics: {
567
+ kind: "mutation",
568
+ retry: "never"
569
+ },
570
+ cache: { _tag: "private" },
571
+ request: {
572
+ params: null,
573
+ query: null,
574
+ body: null
575
+ },
576
+ response: {
577
+ status: 200,
578
+ body: repositoryScanSchema
579
+ },
580
+ errors: ["AUTH_REQUIRED", "FORBIDDEN"],
581
+ lifecycle: { introduced: "1.0.0" },
582
+ docs: {
583
+ summary: "Scan your Repositories for Skills",
584
+ description: "Searches the public Repositories of your GitHub account and its organizations for Skills, and indexes them. Sign-in runs the same scan. If `repositoryIndexing` is off, the answer is FORBIDDEN. If skilld holds no GitHub access for the account, the answer is AUTH_REQUIRED: sign in on skilld.dev again.",
585
+ tag: "Account",
586
+ examples: [{
587
+ request: {},
588
+ response: {
589
+ outcome: "complete",
590
+ repositoriesFound: 3,
591
+ repositoriesIndexed: 2,
592
+ repositoriesFailed: 0
593
+ }
594
+ }]
595
+ }
596
+ }),
597
+ unpublishRepository: defineOperation({
598
+ id: "account.repositories.unpublish",
599
+ method: "DELETE",
600
+ path: "/api/v1/account/repositories/{owner}/{repository}",
601
+ access: "account",
602
+ semantics: {
603
+ kind: "mutation",
604
+ retry: "idempotent"
605
+ },
606
+ cache: { _tag: "private" },
607
+ request: {
608
+ params: repositoryParams,
609
+ query: null,
610
+ body: null
611
+ },
612
+ response: {
613
+ status: 204,
614
+ body: null
615
+ },
616
+ errors: [
617
+ "INVALID_REQUEST",
618
+ "AUTH_REQUIRED",
619
+ "FORBIDDEN"
620
+ ],
621
+ lifecycle: { introduced: "1.0.0" },
622
+ docs: {
623
+ summary: "Unpublish one of your Repositories",
624
+ description: "Removes every Skill of the Repository from the registry. The Owner must be your own login; any other Owner is FORBIDDEN.",
625
+ tag: "Account",
626
+ examples: [{
627
+ request: { params: {
628
+ owner: "harlan-zw",
629
+ repository: "skills"
630
+ } },
631
+ response: null
632
+ }]
633
+ }
634
+ })
635
+ }
636
+ });
637
+ const likesV1 = defineRegistry({
638
+ namespace: "likes",
639
+ description: "Like Skills. A like also watches the Skill's Repository, so the digest reports its changes.",
640
+ operations: {
641
+ list: defineOperation({
642
+ id: "likes.list",
643
+ method: "GET",
644
+ path: "/api/v1/account/likes",
645
+ access: "account",
646
+ semantics: { kind: "query" },
647
+ cache: { _tag: "private" },
648
+ request: {
649
+ params: null,
650
+ query: z.object(pageQueryShape({
651
+ defaultLimit: 20,
652
+ maxLimit: 50
653
+ })),
654
+ body: null
655
+ },
656
+ response: {
657
+ status: 200,
658
+ body: defineListResponse(likedSkillSchema)
659
+ },
660
+ errors: accountInputErrors,
661
+ lifecycle: { introduced: "1.0.0" },
662
+ docs: {
663
+ summary: "List your liked Skills",
664
+ description: "Your liked Skills, newest like first.",
665
+ tag: "Likes",
666
+ examples: [{
667
+ request: { query: { limit: 1 } },
668
+ response: {
669
+ items: [{
670
+ ...exampleSkillSummary,
671
+ likedAt: "2026-09-12T10:31:44.000Z"
672
+ }],
673
+ total: 14
674
+ }
675
+ }]
676
+ }
677
+ }),
678
+ create: defineOperation({
679
+ id: "likes.create",
680
+ method: "PUT",
681
+ path: "/api/v1/account/likes/{owner}/{repository}/{name}",
682
+ access: "account",
683
+ semantics: {
684
+ kind: "mutation",
685
+ retry: "idempotent"
686
+ },
687
+ cache: { _tag: "private" },
688
+ request: {
689
+ params: skillParams,
690
+ query: null,
691
+ body: null
692
+ },
693
+ response: {
694
+ status: 204,
695
+ body: null
696
+ },
697
+ errors: [
698
+ "INVALID_REQUEST",
699
+ "AUTH_REQUIRED",
700
+ "NOT_FOUND",
701
+ "RATE_LIMITED"
702
+ ],
703
+ lifecycle: { introduced: "1.0.0" },
704
+ docs: {
705
+ summary: "Like a Skill",
706
+ description: "Likes the Skill and watches its Repository, so the digest reports changes to the Skill. If you already watch the Repository, that watch stays as it is. An account can add 200 likes a day.",
707
+ tag: "Likes",
708
+ examples: [{
709
+ request: { params: {
710
+ owner: "vercel-labs",
711
+ repository: "agent-skills",
712
+ name: "web-design-guidelines"
713
+ } },
714
+ response: null
715
+ }]
716
+ }
717
+ }),
718
+ delete: defineOperation({
719
+ id: "likes.delete",
720
+ method: "DELETE",
721
+ path: "/api/v1/account/likes/{owner}/{repository}/{name}",
722
+ access: "account",
723
+ semantics: {
724
+ kind: "mutation",
725
+ retry: "idempotent"
726
+ },
727
+ cache: { _tag: "private" },
728
+ request: {
729
+ params: skillParams,
730
+ query: null,
731
+ body: null
732
+ },
733
+ response: {
734
+ status: 204,
735
+ body: null
736
+ },
737
+ errors: accountInputErrors,
738
+ lifecycle: { introduced: "1.0.0" },
739
+ docs: {
740
+ summary: "Remove a like",
741
+ description: "Removes your like. If it was your last like in the Repository, the watch that a like added goes too. A watch you added another way stays.",
742
+ tag: "Likes",
743
+ examples: [{
744
+ request: { params: {
745
+ owner: "vercel-labs",
746
+ repository: "agent-skills",
747
+ name: "web-design-guidelines"
748
+ } },
749
+ response: null
750
+ }]
751
+ }
752
+ })
753
+ }
754
+ });
755
+ const watchesV1 = defineRegistry({
756
+ namespace: "watches",
757
+ description: "Watch Repositories, so the digest reports changes to their Skills.",
758
+ operations: {
759
+ list: defineOperation({
760
+ id: "watches.list",
761
+ method: "GET",
762
+ path: "/api/v1/account/watches",
763
+ access: "account",
764
+ semantics: { kind: "query" },
765
+ cache: { _tag: "private" },
766
+ request: {
767
+ params: null,
768
+ query: z.object(pageQueryShape({
769
+ defaultLimit: 50,
770
+ maxLimit: 100
771
+ })),
772
+ body: null
773
+ },
774
+ response: {
775
+ status: 200,
776
+ body: defineListResponse(watchSchema)
777
+ },
778
+ errors: accountInputErrors,
779
+ lifecycle: { introduced: "1.0.0" },
780
+ docs: {
781
+ summary: "List your watched Repositories",
782
+ description: "The Repositories your digest reports on, newest watch first, with the reason each watch exists.",
783
+ tag: "Watches",
784
+ examples: [{
785
+ request: { query: { limit: 1 } },
786
+ response: {
787
+ items: [exampleWatch],
788
+ total: 6
789
+ }
790
+ }]
791
+ }
792
+ }),
793
+ create: defineOperation({
794
+ id: "watches.create",
795
+ method: "PUT",
796
+ path: "/api/v1/account/watches/{owner}/{repository}",
797
+ access: "account",
798
+ semantics: {
799
+ kind: "mutation",
800
+ retry: "idempotent"
801
+ },
802
+ cache: { _tag: "private" },
803
+ request: {
804
+ params: repositoryParams,
805
+ query: null,
806
+ body: null
807
+ },
808
+ response: {
809
+ status: 204,
810
+ body: null
811
+ },
812
+ errors: [
813
+ "INVALID_REQUEST",
814
+ "AUTH_REQUIRED",
815
+ "NOT_FOUND"
816
+ ],
817
+ lifecycle: { introduced: "1.0.0" },
818
+ docs: {
819
+ summary: "Watch a Repository",
820
+ description: "Watches a Repository the registry holds. A watch that a like added becomes a `direct` watch, so removing the like no longer removes it.",
821
+ tag: "Watches",
822
+ examples: [{
823
+ request: { params: {
824
+ owner: "vercel-labs",
825
+ repository: "agent-skills"
826
+ } },
827
+ response: null
828
+ }]
829
+ }
830
+ }),
831
+ delete: defineOperation({
832
+ id: "watches.delete",
833
+ method: "DELETE",
834
+ path: "/api/v1/account/watches/{owner}/{repository}",
835
+ access: "account",
836
+ semantics: {
837
+ kind: "mutation",
838
+ retry: "idempotent"
839
+ },
840
+ cache: { _tag: "private" },
841
+ request: {
842
+ params: repositoryParams,
843
+ query: null,
844
+ body: null
845
+ },
846
+ response: {
847
+ status: 204,
848
+ body: null
849
+ },
850
+ errors: accountInputErrors,
851
+ lifecycle: { introduced: "1.0.0" },
852
+ docs: {
853
+ summary: "Stop watching a Repository",
854
+ description: "Removes the watch, whatever its reason. Your likes stay.",
855
+ tag: "Watches",
856
+ examples: [{
857
+ request: { params: {
858
+ owner: "vercel-labs",
859
+ repository: "agent-skills"
860
+ } },
861
+ response: null
862
+ }]
863
+ }
864
+ })
865
+ }
866
+ });
867
+ const starsV1 = defineRegistry({
868
+ namespace: "stars",
869
+ description: "Import your GitHub stars and find the starred Repositories that hold Skills.",
870
+ operations: {
871
+ list: defineOperation({
872
+ id: "stars.list",
873
+ method: "GET",
874
+ path: "/api/v1/account/stars",
875
+ access: "account",
876
+ semantics: { kind: "query" },
877
+ cache: { _tag: "private" },
878
+ request: {
879
+ params: null,
880
+ query: z.object(pageQueryShape({
881
+ defaultLimit: 50,
882
+ maxLimit: 100
883
+ })),
884
+ body: null
885
+ },
886
+ response: {
887
+ status: 200,
888
+ body: defineListResponse(starredRepositorySchema)
889
+ },
890
+ errors: accountInputErrors,
891
+ lifecycle: { introduced: "1.0.0" },
892
+ docs: {
893
+ summary: "List your starred Repositories that hold Skills",
894
+ description: "Reads your last import, newest star first. Run `stars.import` to read GitHub again.",
895
+ tag: "Stars",
896
+ examples: [{
897
+ request: { query: { limit: 1 } },
898
+ response: {
899
+ items: [{
900
+ owner: "vercel-labs",
901
+ repository: "agent-skills",
902
+ pageUrl: "https://skilld.dev/gh/vercel-labs/agent-skills",
903
+ starredAt: "2026-08-30T21:05:10.000Z",
904
+ watching: false,
905
+ skillCount: 6
906
+ }],
907
+ total: 3
908
+ }
909
+ }]
910
+ }
911
+ }),
912
+ import: defineOperation({
913
+ id: "stars.import",
914
+ method: "POST",
915
+ path: "/api/v1/account/stars/import",
916
+ access: "account",
917
+ semantics: {
918
+ kind: "mutation",
919
+ retry: "never"
920
+ },
921
+ cache: { _tag: "private" },
922
+ request: {
923
+ params: null,
924
+ query: null,
925
+ body: z.strictObject({ page: z.number().int().min(1).max(10).default(1) })
926
+ },
927
+ response: {
928
+ status: 200,
929
+ body: starsImportSchema
930
+ },
931
+ errors: accountInputErrors,
932
+ lifecycle: { introduced: "1.0.0" },
933
+ docs: {
934
+ summary: "Import your GitHub stars",
935
+ description: "Imports one page of 100 GitHub stars and keeps the Repositories whose name mentions a Skill. Page 1 replaces the last import. Send each `nextPage` until it is `null`; the import stops after page 10. If skilld holds no GitHub access for the account, the answer is AUTH_REQUIRED: sign in on skilld.dev again.",
936
+ tag: "Stars",
937
+ examples: [{
938
+ request: { body: {} },
939
+ response: {
940
+ page: 1,
941
+ nextPage: 2,
942
+ imported: 4,
943
+ withSkills: 2,
944
+ importedAt: null
945
+ }
946
+ }, {
947
+ request: { body: { page: 2 } },
948
+ response: {
949
+ page: 2,
950
+ nextPage: null,
951
+ imported: 5,
952
+ withSkills: 3,
953
+ importedAt: "2026-10-01T07:45:00.000Z"
954
+ }
955
+ }]
956
+ }
957
+ })
958
+ }
959
+ });
960
+ const changesV1 = defineRegistry({
961
+ namespace: "changes",
962
+ description: "Read what changed in your watched Repositories: the content of your digest.",
963
+ operations: { list: defineOperation({
964
+ id: "changes.list",
965
+ method: "GET",
966
+ path: "/api/v1/account/changes",
967
+ access: "account",
968
+ semantics: { kind: "query" },
969
+ cache: { _tag: "private" },
970
+ request: {
971
+ params: null,
972
+ query: z.object({ since: isoDateTimeSchema.optional() }),
973
+ body: null
974
+ },
975
+ response: {
976
+ status: 200,
977
+ body: accountChangesSchema
978
+ },
979
+ errors: accountInputErrors,
980
+ lifecycle: { introduced: "1.0.0" },
981
+ docs: {
982
+ summary: "List changes to your watched Skills",
983
+ description: "The Skills that changed in your watched Repositories between `since` and now, the same selection as your digest. A watch that a like added reports only the Skills you liked. Without `since`, the window is the last 30 days. The answer covers at most the 30 Repositories with the most changes.",
984
+ tag: "Changes",
985
+ examples: [{
986
+ request: { query: { since: "2026-09-01T00:00:00.000Z" } },
987
+ response: {
988
+ since: "2026-09-01T00:00:00.000Z",
989
+ until: "2026-10-01T09:00:00.000Z",
990
+ items: [{
991
+ ...exampleSkillSummary,
992
+ changedAt: "2026-09-28T14:02:11.000Z",
993
+ changeCount: 2,
994
+ commitMessages: ["docs: add focus ring rules", "docs: clarify form labels"],
995
+ changeUrl: "https://github.com/vercel-labs/agent-skills/commit/4f1c2a9e0b7d3c5a8e6f1b2d4c7a9e0f3b5d8c1a"
996
+ }]
997
+ }
998
+ }]
999
+ }
1000
+ }) }
1001
+ });
1002
+ const tokensV1 = defineRegistry({
1003
+ namespace: "tokens",
1004
+ description: "Create, list, and revoke the skilld tokens that act for your account.",
1005
+ operations: {
1006
+ list: defineOperation({
1007
+ id: "tokens.list",
1008
+ method: "GET",
1009
+ path: "/api/v1/account/tokens",
1010
+ access: "account",
1011
+ semantics: { kind: "query" },
1012
+ cache: { _tag: "private" },
1013
+ request: {
1014
+ params: null,
1015
+ query: z.object(pageQueryShape({
1016
+ defaultLimit: 50,
1017
+ maxLimit: 100
1018
+ })),
1019
+ body: null
1020
+ },
1021
+ response: {
1022
+ status: 200,
1023
+ body: defineListResponse(tokenSchema)
1024
+ },
1025
+ errors: accountInputErrors,
1026
+ lifecycle: { introduced: "1.0.0" },
1027
+ docs: {
1028
+ summary: "List your tokens",
1029
+ description: "Your tokens that still work, most recently used first. A revoked or expired token is not listed. The answer never holds a secret.",
1030
+ tag: "Tokens",
1031
+ examples: [{
1032
+ request: {},
1033
+ response: {
1034
+ items: [{
1035
+ id: 412,
1036
+ label: "CI deploy",
1037
+ kind: "pat",
1038
+ createdAt: "2026-09-02T11:20:00.000Z",
1039
+ lastUsedAt: "2026-10-01T06:58:31.000Z",
1040
+ expiresAt: "2026-12-01T11:20:00.000Z",
1041
+ current: true
1042
+ }],
1043
+ total: 1
1044
+ }
1045
+ }]
1046
+ }
1047
+ }),
1048
+ create: defineOperation({
1049
+ id: "tokens.create",
1050
+ method: "POST",
1051
+ path: "/api/v1/account/tokens",
1052
+ access: "account",
1053
+ semantics: {
1054
+ kind: "mutation",
1055
+ retry: "never"
1056
+ },
1057
+ cache: { _tag: "private" },
1058
+ request: {
1059
+ params: null,
1060
+ query: null,
1061
+ body: z.strictObject({
1062
+ label: z.string().trim().min(1).max(80),
1063
+ /** Days until the token stops working. Leave it out for a token with no set end. */
1064
+ ttlDays: z.number().int().min(1).max(3650).optional()
1065
+ })
1066
+ },
1067
+ response: {
1068
+ status: 201,
1069
+ body: issuedTokenSchema
1070
+ },
1071
+ errors: [...accountInputErrors, "FORBIDDEN"],
1072
+ lifecycle: { introduced: "1.0.0" },
1073
+ docs: {
1074
+ summary: "Create a token",
1075
+ description: "Creates a skilld token that acts for your account. Send it as a Bearer credential. Store `token` now: no later answer shows it. A one-hour GitHub Actions token cannot create a token, so that request is FORBIDDEN.",
1076
+ tag: "Tokens",
1077
+ examples: [{
1078
+ request: { body: {
1079
+ label: "CI deploy",
1080
+ ttlDays: 90
1081
+ } },
1082
+ response: {
1083
+ id: 412,
1084
+ label: "CI deploy",
1085
+ expiresAt: "2026-12-31T09:00:00.000Z",
1086
+ token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsInRpZCI6NDEyfQ.c2lnbmF0dXJl"
1087
+ }
1088
+ }]
1089
+ }
1090
+ }),
1091
+ revoke: defineOperation({
1092
+ id: "tokens.revoke",
1093
+ method: "DELETE",
1094
+ path: "/api/v1/account/tokens/{id}",
1095
+ access: "account",
1096
+ semantics: {
1097
+ kind: "mutation",
1098
+ retry: "idempotent"
1099
+ },
1100
+ cache: { _tag: "private" },
1101
+ request: {
1102
+ params: tokenParams,
1103
+ query: null,
1104
+ body: null
1105
+ },
1106
+ response: {
1107
+ status: 204,
1108
+ body: null
1109
+ },
1110
+ errors: [
1111
+ "INVALID_REQUEST",
1112
+ "AUTH_REQUIRED",
1113
+ "NOT_FOUND"
1114
+ ],
1115
+ lifecycle: { introduced: "1.0.0" },
1116
+ docs: {
1117
+ summary: "Revoke a token",
1118
+ description: "Revokes one of your tokens. It stops working at once, even if it sent this request. A token of another account is NOT_FOUND.",
1119
+ tag: "Tokens",
1120
+ examples: [{
1121
+ request: { params: { id: 412 } },
1122
+ response: null
1123
+ }]
1124
+ }
1125
+ })
1126
+ }
1127
+ });
1128
+ //#endregion
1129
+ //#region src/contract/collections.ts
1130
+ /**
1131
+ * Slugs that already name a static route under `/@login/`. A collection with
1132
+ * one of them would be created and then never reached, because the static
1133
+ * route wins.
1134
+ */
1135
+ const RESERVED_COLLECTION_SLUGS = /* @__PURE__ */ new Set(["liked"]);
1136
+ const curatorParams = z.strictObject({ login: loginSchema });
1137
+ const collectionParams = z.strictObject({
1138
+ login: loginSchema,
1139
+ slug: collectionSlugSchema
1140
+ });
1141
+ const collectionSkillParams = z.strictObject({
1142
+ login: loginSchema,
1143
+ slug: collectionSlugSchema,
1144
+ owner: ownerSchema,
1145
+ repository: repositorySchema,
1146
+ name: skillNameSchema
1147
+ });
1148
+ /** Why the curator picked the Skill, in their own words. */
1149
+ const reasonSchema = z.string().trim().min(1).max(2e3);
1150
+ const curatorIdentityFields = {
1151
+ login: loginSchema,
1152
+ /** The GitHub profile name, when the account has one. */
1153
+ name: z.string().nullable(),
1154
+ avatarUrl: urlSchema.nullable()
1155
+ };
1156
+ const collectionFields = {
1157
+ slug: collectionSlugSchema,
1158
+ title: z.string(),
1159
+ /** The curator's introduction to the collection. */
1160
+ description: z.string().nullable(),
1161
+ pageUrl: urlSchema,
1162
+ /** Installs every Skill the collection names. */
1163
+ installCommand: z.string()
1164
+ };
1165
+ const curatorIdentitySchema = defineResponseObject(curatorIdentityFields);
1166
+ /** One Skill in a collection, with the curator's reason for it. */
1167
+ const collectionSkillSchema = defineResponseObject({
1168
+ ...skillSummaryShape,
1169
+ reason: z.string().nullable()
1170
+ });
1171
+ const collectionSkillPage = defineListResponse(collectionSkillSchema);
1172
+ const collectionDetailSchema = {
1173
+ producer: z.strictObject({
1174
+ ...collectionFields,
1175
+ curator: curatorIdentitySchema.producer,
1176
+ skills: collectionSkillPage.producer
1177
+ }),
1178
+ client: z.looseObject({
1179
+ ...collectionFields,
1180
+ curator: curatorIdentitySchema.client,
1181
+ skills: collectionSkillPage.client
1182
+ })
1183
+ };
1184
+ const collectionSummarySchema = defineResponseObject({
1185
+ ...collectionFields,
1186
+ /** Every Skill the collection names, also a Skill the registry cannot show now. */
1187
+ skillCount: countSchema
1188
+ });
1189
+ const curatorFields = {
1190
+ ...curatorIdentityFields,
1191
+ pageUrl: urlSchema
1192
+ };
1193
+ const curatorListItemSchema = defineResponseObject({
1194
+ ...curatorFields,
1195
+ collectionCount: countSchema
1196
+ });
1197
+ const curatorDetailFields = {
1198
+ ...curatorFields,
1199
+ /** Installs every Skill that the curator's collections name. */
1200
+ installCommand: z.string()
1201
+ };
1202
+ const curatorDetailSchema = {
1203
+ producer: z.strictObject({
1204
+ ...curatorDetailFields,
1205
+ collections: z.array(collectionSummarySchema.producer)
1206
+ }),
1207
+ client: z.looseObject({
1208
+ ...curatorDetailFields,
1209
+ collections: z.array(collectionSummarySchema.client)
1210
+ })
1211
+ };
1212
+ const collectionSkillEntry = z.strictObject({
1213
+ owner: ownerSchema,
1214
+ repository: repositorySchema,
1215
+ name: skillNameSchema,
1216
+ reason: reasonSchema.nullable().optional()
1217
+ });
1218
+ const createCollectionBody = z.strictObject({
1219
+ slug: collectionSlugSchema.refine((slug) => !RESERVED_COLLECTION_SLUGS.has(slug), "is reserved"),
1220
+ title: z.string().trim().min(1).max(120),
1221
+ description: z.string().trim().min(1).max(4e3).nullable().optional(),
1222
+ skills: z.array(collectionSkillEntry).max(100).refine((skills) => new Set(skills.map((skill) => `${skill.owner}/${skill.repository}/${skill.name}`)).size === skills.length, "must name each Skill once").default([])
1223
+ });
1224
+ const exampleCurator = {
1225
+ login: "harlan-zw",
1226
+ name: "Harlan Wilton",
1227
+ avatarUrl: "https://avatars.githubusercontent.com/u/5326365?v=4"
1228
+ };
1229
+ const exampleCollectionFields = {
1230
+ slug: "design-engineering-essentials",
1231
+ title: "Design Engineering Essentials",
1232
+ description: "The Skills I give an agent before it touches interface code.",
1233
+ pageUrl: "https://skilld.dev/@harlan-zw/design-engineering-essentials",
1234
+ installCommand: "npx skilld add @harlan-zw/design-engineering-essentials"
1235
+ };
1236
+ const exampleCollectionSkill = {
1237
+ ...exampleSkillSummary,
1238
+ reason: "Checks interface code against a published list of rules before review."
1239
+ };
1240
+ const exampleCollection = {
1241
+ ...exampleCollectionFields,
1242
+ curator: exampleCurator,
1243
+ skills: {
1244
+ items: [exampleCollectionSkill],
1245
+ total: 1
1246
+ }
1247
+ };
1248
+ const curatorsV1 = defineRegistry({
1249
+ namespace: "curators",
1250
+ description: "Read the curators who assemble collections on skilld.dev, their collections, and the Skills they like.",
1251
+ operations: {
1252
+ list: defineOperation({
1253
+ id: "curators.list",
1254
+ method: "GET",
1255
+ path: "/api/v1/curators",
1256
+ access: "public",
1257
+ semantics: { kind: "query" },
1258
+ cache: {
1259
+ _tag: "public",
1260
+ maxAgeSeconds: 300,
1261
+ staleWhileRevalidateSeconds: 3600
1262
+ },
1263
+ request: {
1264
+ params: null,
1265
+ query: z.object(pageQueryShape({
1266
+ defaultLimit: 30,
1267
+ maxLimit: 60
1268
+ })),
1269
+ body: null
1270
+ },
1271
+ response: {
1272
+ status: 200,
1273
+ body: defineListResponse(curatorListItemSchema)
1274
+ },
1275
+ errors: ["INVALID_REQUEST"],
1276
+ lifecycle: { introduced: "1.0.0" },
1277
+ docs: {
1278
+ summary: "List curators",
1279
+ description: "The skilld.dev/community directory. It holds the first 60 curators: curators with a featured collection come first, then the rest by GitHub stars. A curator who publishes Skills but no collection yet has a `collectionCount` of 0.",
1280
+ tag: "Curators",
1281
+ examples: [{
1282
+ request: { query: { limit: 1 } },
1283
+ response: {
1284
+ items: [{
1285
+ ...exampleCurator,
1286
+ pageUrl: "https://skilld.dev/@harlan-zw",
1287
+ collectionCount: 2
1288
+ }],
1289
+ total: 41
1290
+ }
1291
+ }]
1292
+ }
1293
+ }),
1294
+ get: defineOperation({
1295
+ id: "curators.get",
1296
+ method: "GET",
1297
+ path: "/api/v1/curators/{login}",
1298
+ access: "public",
1299
+ semantics: { kind: "query" },
1300
+ cache: {
1301
+ _tag: "public",
1302
+ maxAgeSeconds: 60,
1303
+ staleWhileRevalidateSeconds: 300
1304
+ },
1305
+ request: {
1306
+ params: curatorParams,
1307
+ query: null,
1308
+ body: null
1309
+ },
1310
+ response: {
1311
+ status: 200,
1312
+ body: curatorDetailSchema
1313
+ },
1314
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
1315
+ lifecycle: { introduced: "1.0.0" },
1316
+ docs: {
1317
+ summary: "Get a curator",
1318
+ description: "One curator and their collections, newest first. `installCommand` installs every Skill that the collections name. If no skilld.dev account has the login, the answer is NOT_FOUND.",
1319
+ tag: "Curators",
1320
+ examples: [{
1321
+ request: { params: { login: "harlan-zw" } },
1322
+ response: {
1323
+ ...exampleCurator,
1324
+ pageUrl: "https://skilld.dev/@harlan-zw",
1325
+ installCommand: "npx skilld add @harlan-zw",
1326
+ collections: [{
1327
+ ...exampleCollectionFields,
1328
+ skillCount: 8
1329
+ }]
1330
+ }
1331
+ }]
1332
+ }
1333
+ }),
1334
+ likes: defineOperation({
1335
+ id: "curators.likes",
1336
+ method: "GET",
1337
+ path: "/api/v1/curators/{login}/likes",
1338
+ access: "public",
1339
+ semantics: { kind: "query" },
1340
+ cache: {
1341
+ _tag: "public",
1342
+ maxAgeSeconds: 60,
1343
+ staleWhileRevalidateSeconds: 300
1344
+ },
1345
+ request: {
1346
+ params: curatorParams,
1347
+ query: z.object(pageQueryShape({
1348
+ defaultLimit: 50,
1349
+ maxLimit: 100
1350
+ })),
1351
+ body: null
1352
+ },
1353
+ response: {
1354
+ status: 200,
1355
+ body: defineListResponse(defineResponseObject(skillSummaryShape))
1356
+ },
1357
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
1358
+ lifecycle: { introduced: "1.0.0" },
1359
+ docs: {
1360
+ summary: "List the Skills a curator likes",
1361
+ description: "The 200 Skills the curator liked most recently, newest like first: the list at skilld.dev/@login/liked. A curator can keep the list private. If the list is private, or no account has the login, the answer is NOT_FOUND. This operation never shows a private list, not even to its owner. Use `likes.list` for your own likes.",
1362
+ tag: "Curators",
1363
+ examples: [{
1364
+ request: {
1365
+ params: { login: "harlan-zw" },
1366
+ query: { limit: 1 }
1367
+ },
1368
+ response: {
1369
+ items: [exampleSkillSummary],
1370
+ total: 12
1371
+ }
1372
+ }]
1373
+ }
1374
+ })
1375
+ }
1376
+ });
1377
+ const collectionsV1 = defineRegistry({
1378
+ namespace: "collections",
1379
+ description: "Read a collection, build your own, and watch one so the digest reports changes to its Skills.",
1380
+ operations: {
1381
+ get: defineOperation({
1382
+ id: "collections.get",
1383
+ method: "GET",
1384
+ path: "/api/v1/collections/{login}/{slug}",
1385
+ access: "public",
1386
+ semantics: { kind: "query" },
1387
+ cache: {
1388
+ _tag: "public",
1389
+ maxAgeSeconds: 60,
1390
+ staleWhileRevalidateSeconds: 300
1391
+ },
1392
+ request: {
1393
+ params: collectionParams,
1394
+ query: z.object(pageQueryShape({
1395
+ defaultLimit: 100,
1396
+ maxLimit: 100
1397
+ })),
1398
+ body: null
1399
+ },
1400
+ response: {
1401
+ status: 200,
1402
+ body: collectionDetailSchema
1403
+ },
1404
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
1405
+ lifecycle: { introduced: "1.0.0" },
1406
+ docs: {
1407
+ summary: "Get a collection",
1408
+ description: "One collection at skilld.dev/@login/slug, with its Skills in the curator's order and the reason for each. A collection can name a whole Repository: that entry shows the Repository's most recently changed Skill. A Skill the registry cannot show now, for example because its Repository is broken, is left out of `skills`.",
1409
+ tag: "Collections",
1410
+ examples: [{
1411
+ request: { params: {
1412
+ login: "harlan-zw",
1413
+ slug: "design-engineering-essentials"
1414
+ } },
1415
+ response: exampleCollection
1416
+ }]
1417
+ }
1418
+ }),
1419
+ create: defineOperation({
1420
+ id: "collections.create",
1421
+ method: "POST",
1422
+ path: "/api/v1/collections",
1423
+ access: "account",
1424
+ semantics: {
1425
+ kind: "mutation",
1426
+ retry: "never"
1427
+ },
1428
+ cache: { _tag: "private" },
1429
+ request: {
1430
+ params: null,
1431
+ query: null,
1432
+ body: createCollectionBody
1433
+ },
1434
+ response: {
1435
+ status: 201,
1436
+ body: collectionDetailSchema
1437
+ },
1438
+ errors: [
1439
+ "INVALID_REQUEST",
1440
+ "AUTH_REQUIRED",
1441
+ "CONFLICT"
1442
+ ],
1443
+ lifecycle: { introduced: "1.0.0" },
1444
+ docs: {
1445
+ summary: "Create a collection",
1446
+ description: "Creates a collection at skilld.dev/@your-login/slug, with up to 100 Skills in the order you send them. Every Skill must be in the registry, or the answer is INVALID_REQUEST. If you already have a collection with the slug, the answer is CONFLICT.",
1447
+ tag: "Collections",
1448
+ examples: [{
1449
+ request: { body: {
1450
+ slug: "design-engineering-essentials",
1451
+ title: "Design Engineering Essentials",
1452
+ description: "The Skills I give an agent before it touches interface code.",
1453
+ skills: [{
1454
+ owner: "vercel-labs",
1455
+ repository: "agent-skills",
1456
+ name: "web-design-guidelines",
1457
+ reason: "Checks interface code against a published list of rules before review."
1458
+ }]
1459
+ } },
1460
+ response: exampleCollection
1461
+ }]
1462
+ }
1463
+ }),
1464
+ addSkill: defineOperation({
1465
+ id: "collections.skills.add",
1466
+ method: "PUT",
1467
+ path: "/api/v1/collections/{login}/{slug}/skills/{owner}/{repository}/{name}",
1468
+ access: "account",
1469
+ semantics: {
1470
+ kind: "mutation",
1471
+ retry: "idempotent"
1472
+ },
1473
+ cache: { _tag: "private" },
1474
+ request: {
1475
+ params: collectionSkillParams,
1476
+ query: null,
1477
+ body: z.strictObject({ reason: reasonSchema.nullable().optional() }).default({})
1478
+ },
1479
+ response: {
1480
+ status: 200,
1481
+ body: collectionSkillSchema
1482
+ },
1483
+ errors: [
1484
+ "INVALID_REQUEST",
1485
+ "AUTH_REQUIRED",
1486
+ "FORBIDDEN",
1487
+ "NOT_FOUND"
1488
+ ],
1489
+ lifecycle: { introduced: "1.0.0" },
1490
+ docs: {
1491
+ summary: "Add a Skill to a collection",
1492
+ description: "Adds the Skill to the end of one of your collections, and answers the collection entry. If the collection already has the Skill, its place stays the same. Send `reason` to set the reason, or `null` to clear it. If you leave out `reason`, the stored reason stays. Only the curator can change a collection: for another login the answer is FORBIDDEN.",
1493
+ tag: "Collections",
1494
+ examples: [{
1495
+ request: {
1496
+ params: {
1497
+ login: "harlan-zw",
1498
+ slug: "design-engineering-essentials",
1499
+ owner: "vercel-labs",
1500
+ repository: "agent-skills",
1501
+ name: "web-design-guidelines"
1502
+ },
1503
+ body: { reason: "Checks interface code against a published list of rules before review." }
1504
+ },
1505
+ response: exampleCollectionSkill
1506
+ }]
1507
+ }
1508
+ }),
1509
+ removeSkill: defineOperation({
1510
+ id: "collections.skills.remove",
1511
+ method: "DELETE",
1512
+ path: "/api/v1/collections/{login}/{slug}/skills/{owner}/{repository}/{name}",
1513
+ access: "account",
1514
+ semantics: {
1515
+ kind: "mutation",
1516
+ retry: "idempotent"
1517
+ },
1518
+ cache: { _tag: "private" },
1519
+ request: {
1520
+ params: collectionSkillParams,
1521
+ query: null,
1522
+ body: null
1523
+ },
1524
+ response: {
1525
+ status: 204,
1526
+ body: null
1527
+ },
1528
+ errors: [
1529
+ "INVALID_REQUEST",
1530
+ "AUTH_REQUIRED",
1531
+ "FORBIDDEN",
1532
+ "NOT_FOUND"
1533
+ ],
1534
+ lifecycle: { introduced: "1.0.0" },
1535
+ docs: {
1536
+ summary: "Remove a Skill from a collection",
1537
+ description: "Removes the Skill from one of your collections. If the Skill shows for a whole-Repository entry, that entry goes. If the collection does not have the Skill, nothing changes. Only the curator can change a collection: for another login the answer is FORBIDDEN.",
1538
+ tag: "Collections",
1539
+ examples: [{
1540
+ request: { params: {
1541
+ login: "harlan-zw",
1542
+ slug: "design-engineering-essentials",
1543
+ owner: "vercel-labs",
1544
+ repository: "agent-skills",
1545
+ name: "web-design-guidelines"
1546
+ } },
1547
+ response: null
1548
+ }]
1549
+ }
1550
+ }),
1551
+ watch: defineOperation({
1552
+ id: "collections.watch",
1553
+ method: "PUT",
1554
+ path: "/api/v1/collections/{login}/{slug}/watch",
1555
+ access: "account",
1556
+ semantics: {
1557
+ kind: "mutation",
1558
+ retry: "idempotent"
1559
+ },
1560
+ cache: { _tag: "private" },
1561
+ request: {
1562
+ params: collectionParams,
1563
+ query: null,
1564
+ body: null
1565
+ },
1566
+ response: {
1567
+ status: 200,
1568
+ body: defineResponseObject({
1569
+ /** The Repositories of the collection, all of which you now watch. */
1570
+ watched: countSchema })
1571
+ },
1572
+ errors: [
1573
+ "INVALID_REQUEST",
1574
+ "AUTH_REQUIRED",
1575
+ "NOT_FOUND"
1576
+ ],
1577
+ lifecycle: { introduced: "1.0.0" },
1578
+ docs: {
1579
+ summary: "Watch a collection",
1580
+ description: "Watches every Repository the collection names, so your digest reports their changes. A Repository you already watch stays watched. A Repository added to the collection later is not watched until you send this again.",
1581
+ tag: "Collections",
1582
+ examples: [{
1583
+ request: { params: {
1584
+ login: "harlan-zw",
1585
+ slug: "design-engineering-essentials"
1586
+ } },
1587
+ response: { watched: 8 }
1588
+ }]
1589
+ }
1590
+ })
1591
+ }
1592
+ });
1593
+ //#endregion
1594
+ //#region src/contract/registry.ts
1595
+ const repositoryProfileFields = {
1596
+ owner: ownerSchema,
1597
+ repository: repositorySchema,
1598
+ /** The Repository description on GitHub. */
1599
+ description: z.string().nullable(),
1600
+ stars: countSchema,
1601
+ /** The Repository's last push, as the registry last read it. */
1602
+ pushedAt: isoDateTimeSchema.nullable(),
1603
+ repositoryUrl: urlSchema,
1604
+ pageUrl: urlSchema,
1605
+ /** Installs every Skill in the Repository. */
1606
+ installCommand: z.string()
1607
+ };
1608
+ const repositoryProfileSchema = {
1609
+ producer: z.strictObject({
1610
+ ...repositoryProfileFields,
1611
+ skills: z.array(skillSummarySchema.producer)
1612
+ }),
1613
+ client: z.looseObject({
1614
+ ...repositoryProfileFields,
1615
+ skills: z.array(skillSummarySchema.client)
1616
+ })
1617
+ };
1618
+ const repositoriesV1 = defineRegistry({
1619
+ namespace: "repositories",
1620
+ description: "Read one Repository and every Skill the registry holds from it.",
1621
+ operations: { get: defineOperation({
1622
+ id: "repositories.get",
1623
+ method: "GET",
1624
+ path: "/api/v1/repositories/{owner}/{repository}",
1625
+ access: "public",
1626
+ semantics: { kind: "query" },
1627
+ cache: {
1628
+ _tag: "public",
1629
+ maxAgeSeconds: 60,
1630
+ staleWhileRevalidateSeconds: 300
1631
+ },
1632
+ request: {
1633
+ params: repositoryParams,
1634
+ query: null,
1635
+ body: null
1636
+ },
1637
+ response: {
1638
+ status: 200,
1639
+ body: repositoryProfileSchema
1640
+ },
1641
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
1642
+ lifecycle: { introduced: "1.0.0" },
1643
+ docs: {
1644
+ summary: "Get a Repository",
1645
+ description: "One Repository and every Skill the registry holds from it, most recently changed first. `installCommand` installs all of them. If the registry holds no Skill from the Repository, the answer is NOT_FOUND: send an index request to add it.",
1646
+ tag: "Repositories",
1647
+ examples: [{
1648
+ request: { params: {
1649
+ owner: "vercel-labs",
1650
+ repository: "agent-skills"
1651
+ } },
1652
+ response: {
1653
+ owner: "vercel-labs",
1654
+ repository: "agent-skills",
1655
+ description: "Skills for AI coding agents.",
1656
+ stars: 18204,
1657
+ pushedAt: "2026-09-30T09:12:44.000Z",
1658
+ repositoryUrl: "https://github.com/vercel-labs/agent-skills",
1659
+ pageUrl: "https://skilld.dev/gh/vercel-labs/agent-skills",
1660
+ installCommand: "npx skilld add vercel-labs/agent-skills",
1661
+ skills: [exampleSkillSummary]
1662
+ }
1663
+ }]
1664
+ }
1665
+ }) }
1666
+ });
1667
+ /**
1668
+ * `owner/repository`, or a URL. The server reads a URL the way the skilld.dev
1669
+ * search box does: a `tree` or `blob` link, a `.git` suffix, or a trailing
1670
+ * slash still names the Repository, and a host other than github.com is
1671
+ * INVALID_REQUEST. No `i` flag, because JSON Schema patterns cannot carry one.
1672
+ */
1673
+ const REPOSITORY_REFERENCE = /^(?:[\w.-]+\/[\w.-]+|https?:\/\/\S+)$/;
1674
+ const indexProgressSchema = z.discriminatedUnion("stage", [
1675
+ z.strictObject({ stage: z.literal("queued") }),
1676
+ z.strictObject({ stage: z.literal("checking") }),
1677
+ z.strictObject({
1678
+ stage: z.literal("indexing"),
1679
+ indexed: countSchema,
1680
+ total: countSchema
1681
+ })
1682
+ ]);
1683
+ const indexProgressClientSchema = z.discriminatedUnion("stage", [
1684
+ z.looseObject({ stage: z.literal("queued") }),
1685
+ z.looseObject({ stage: z.literal("checking") }),
1686
+ z.looseObject({
1687
+ stage: z.literal("indexing"),
1688
+ indexed: countSchema,
1689
+ total: countSchema
1690
+ })
1691
+ ]);
1692
+ const indexTargetFields = {
1693
+ owner: ownerSchema,
1694
+ repository: repositorySchema
1695
+ };
1696
+ const indexQueuedFields = {
1697
+ status: z.literal("queued"),
1698
+ /** Poll `indexRequests.get` with this until the status changes. */
1699
+ id: z.uuid(),
1700
+ ...indexTargetFields
1701
+ };
1702
+ const indexIndexedFields = {
1703
+ status: z.literal("indexed"),
1704
+ ...indexTargetFields
1705
+ };
1706
+ const indexFailedFields = {
1707
+ status: z.literal("failed"),
1708
+ ...indexTargetFields,
1709
+ /** Why the registry could not index the Repository, in words a person can act on. */
1710
+ reason: z.string()
1711
+ };
1712
+ const indexQueued = {
1713
+ producer: z.strictObject({
1714
+ ...indexQueuedFields,
1715
+ progress: indexProgressSchema
1716
+ }),
1717
+ client: z.looseObject({
1718
+ ...indexQueuedFields,
1719
+ progress: indexProgressClientSchema
1720
+ })
1721
+ };
1722
+ const indexIndexed = {
1723
+ producer: z.strictObject({
1724
+ ...indexIndexedFields,
1725
+ skills: z.array(skillSummarySchema.producer)
1726
+ }),
1727
+ client: z.looseObject({
1728
+ ...indexIndexedFields,
1729
+ skills: z.array(skillSummarySchema.client)
1730
+ })
1731
+ };
1732
+ const indexFailed = {
1733
+ producer: z.strictObject(indexFailedFields),
1734
+ client: z.looseObject(indexFailedFields)
1735
+ };
1736
+ const indexRequestCreatedSchema = {
1737
+ producer: z.discriminatedUnion("status", [indexIndexed.producer, indexQueued.producer]),
1738
+ client: z.discriminatedUnion("status", [indexIndexed.client, indexQueued.client])
1739
+ };
1740
+ const indexRequestSchema = {
1741
+ producer: z.discriminatedUnion("status", [
1742
+ indexQueued.producer,
1743
+ indexIndexed.producer,
1744
+ indexFailed.producer
1745
+ ]),
1746
+ client: z.discriminatedUnion("status", [
1747
+ indexQueued.client,
1748
+ indexIndexed.client,
1749
+ indexFailed.client
1750
+ ])
1751
+ };
1752
+ const exampleIndexRequestId = "0f8b5c1e-3d4a-4f6b-9a2c-7e1d5b8c9a04";
1753
+ const indexRequestsV1 = defineRegistry({
1754
+ namespace: "indexRequests",
1755
+ description: "Ask the registry to index a GitHub Repository, then poll until its Skills are in.",
1756
+ operations: {
1757
+ create: defineOperation({
1758
+ id: "index_requests.create",
1759
+ method: "POST",
1760
+ path: "/api/v1/index-requests",
1761
+ access: "public",
1762
+ semantics: {
1763
+ kind: "mutation",
1764
+ retry: "never"
1765
+ },
1766
+ cache: { _tag: "private" },
1767
+ request: {
1768
+ params: null,
1769
+ query: null,
1770
+ body: z.strictObject({ repository: z.string().trim().min(3).max(2048).regex(REPOSITORY_REFERENCE, "must be owner/repository or a github.com URL") })
1771
+ },
1772
+ response: {
1773
+ status: 201,
1774
+ body: indexRequestCreatedSchema
1775
+ },
1776
+ errors: ["INVALID_REQUEST"],
1777
+ lifecycle: { introduced: "1.0.0" },
1778
+ docs: {
1779
+ summary: "Index a Repository",
1780
+ description: "If the registry already holds Skills from the Repository, the status is `indexed` and the answer lists them. If not, the status is `queued`: poll `indexRequests.get` with the `id`. A second request for a Repository that is already queued answers the same `id`.",
1781
+ tag: "Index requests",
1782
+ examples: [{
1783
+ request: { body: { repository: "https://github.com/vercel-labs/agent-skills" } },
1784
+ response: {
1785
+ status: "queued",
1786
+ id: exampleIndexRequestId,
1787
+ owner: "vercel-labs",
1788
+ repository: "agent-skills",
1789
+ progress: { stage: "queued" }
1790
+ }
1791
+ }, {
1792
+ request: { body: { repository: "vercel-labs/agent-skills" } },
1793
+ response: {
1794
+ status: "indexed",
1795
+ owner: "vercel-labs",
1796
+ repository: "agent-skills",
1797
+ skills: [exampleSkillSummary]
1798
+ }
1799
+ }]
1800
+ }
1801
+ }),
1802
+ get: defineOperation({
1803
+ id: "index_requests.get",
1804
+ method: "GET",
1805
+ path: "/api/v1/index-requests/{id}",
1806
+ access: "public",
1807
+ semantics: { kind: "query" },
1808
+ cache: { _tag: "private" },
1809
+ request: {
1810
+ params: z.strictObject({ id: z.uuid() }),
1811
+ query: null,
1812
+ body: null
1813
+ },
1814
+ response: {
1815
+ status: 200,
1816
+ body: indexRequestSchema
1817
+ },
1818
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
1819
+ lifecycle: { introduced: "1.0.0" },
1820
+ docs: {
1821
+ summary: "Get an index request",
1822
+ description: "The state of one index request. Poll while the status is `queued`. `indexed` lists the Skills the registry found. `failed` gives the reason.",
1823
+ tag: "Index requests",
1824
+ examples: [{
1825
+ request: { params: { id: exampleIndexRequestId } },
1826
+ response: {
1827
+ status: "queued",
1828
+ id: exampleIndexRequestId,
1829
+ owner: "vercel-labs",
1830
+ repository: "agent-skills",
1831
+ progress: {
1832
+ stage: "indexing",
1833
+ indexed: 3,
1834
+ total: 6
1835
+ }
1836
+ }
1837
+ }, {
1838
+ request: { params: { id: exampleIndexRequestId } },
1839
+ response: {
1840
+ status: "failed",
1841
+ owner: "vercel-labs",
1842
+ repository: "agent-skills",
1843
+ reason: "No supported SKILL.md files were found."
1844
+ }
1845
+ }]
1846
+ }
1847
+ })
1848
+ }
1849
+ });
1850
+ const ownerRepositorySchema = defineResponseObject({
1851
+ repository: repositorySchema,
1852
+ description: z.string().nullable(),
1853
+ stars: countSchema,
1854
+ /** The Skills the registry holds from this Repository. */
1855
+ skillCount: countSchema,
1856
+ pageUrl: urlSchema
1857
+ });
1858
+ const ownerFields = {
1859
+ login: ownerSchema,
1860
+ /** The GitHub profile name, when the registry has read one. */
1861
+ name: z.string().nullable(),
1862
+ avatarUrl: urlSchema,
1863
+ kind: z.enum(["user", "organization"]),
1864
+ pageUrl: urlSchema
1865
+ };
1866
+ const ownerProfileSchema = {
1867
+ producer: z.strictObject({
1868
+ ...ownerFields,
1869
+ repositories: z.array(ownerRepositorySchema.producer)
1870
+ }),
1871
+ client: z.looseObject({
1872
+ ...ownerFields,
1873
+ repositories: z.array(ownerRepositorySchema.client)
1874
+ })
1875
+ };
1876
+ const ownersV1 = defineRegistry({
1877
+ namespace: "owners",
1878
+ description: "Read one GitHub Owner and the Repositories it publishes Skills from.",
1879
+ operations: { get: defineOperation({
1880
+ id: "owners.get",
1881
+ method: "GET",
1882
+ path: "/api/v1/owners/{owner}",
1883
+ access: "public",
1884
+ semantics: { kind: "query" },
1885
+ cache: {
1886
+ _tag: "public",
1887
+ maxAgeSeconds: 60,
1888
+ staleWhileRevalidateSeconds: 300
1889
+ },
1890
+ request: {
1891
+ params: z.strictObject({ owner: ownerSchema }),
1892
+ query: null,
1893
+ body: null
1894
+ },
1895
+ response: {
1896
+ status: 200,
1897
+ body: ownerProfileSchema
1898
+ },
1899
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
1900
+ lifecycle: { introduced: "1.0.0" },
1901
+ docs: {
1902
+ summary: "Get an Owner",
1903
+ description: "One GitHub organization or user, and each Repository it publishes Skills from, the Repository with the most Skills first. If the registry holds no Skill from the Owner, the answer is NOT_FOUND.",
1904
+ tag: "Owners",
1905
+ examples: [{
1906
+ request: { params: { owner: "vercel-labs" } },
1907
+ response: {
1908
+ login: "vercel-labs",
1909
+ name: "Vercel Labs",
1910
+ avatarUrl: "https://github.com/vercel-labs.png",
1911
+ kind: "organization",
1912
+ pageUrl: "https://skilld.dev/gh/vercel-labs",
1913
+ repositories: [{
1914
+ repository: "agent-skills",
1915
+ description: "Skills for AI coding agents.",
1916
+ stars: 18204,
1917
+ skillCount: 6,
1918
+ pageUrl: "https://skilld.dev/gh/vercel-labs/agent-skills"
1919
+ }]
1920
+ }
1921
+ }]
1922
+ }
1923
+ }) }
1924
+ });
1925
+ const trackSlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "must be lowercase letters, digits, and hyphens");
1926
+ const trackFields = {
1927
+ slug: trackSlugSchema,
1928
+ label: z.string(),
1929
+ /** The second-person line a person wrote for the track. */
1930
+ line: z.string(),
1931
+ pageUrl: urlSchema
1932
+ };
1933
+ const trackSummarySchema = defineResponseObject({
1934
+ ...trackFields,
1935
+ skillCount: countSchema
1936
+ });
1937
+ const trackDetailSchema = {
1938
+ producer: z.strictObject({
1939
+ ...trackFields,
1940
+ items: z.array(skillSummarySchema.producer),
1941
+ total: countSchema
1942
+ }),
1943
+ client: z.looseObject({
1944
+ ...trackFields,
1945
+ items: z.array(skillSummarySchema.client),
1946
+ total: countSchema
1947
+ })
1948
+ };
1949
+ const exampleTrack = {
1950
+ slug: "design",
1951
+ label: "Design and interface work",
1952
+ line: "You care how the interface looks, moves, and reads.",
1953
+ pageUrl: "https://skilld.dev/skills/design"
1954
+ };
1955
+ const tracksV1 = defineRegistry({
1956
+ namespace: "tracks",
1957
+ description: "Tracks: Skills for one kind of work. A person writes each label and line and pins the Skills that lead it.",
1958
+ operations: {
1959
+ list: defineOperation({
1960
+ id: "tracks.list",
1961
+ method: "GET",
1962
+ path: "/api/v1/tracks",
1963
+ access: "public",
1964
+ semantics: { kind: "query" },
1965
+ cache: {
1966
+ _tag: "public",
1967
+ maxAgeSeconds: 300,
1968
+ staleWhileRevalidateSeconds: 3600
1969
+ },
1970
+ request: {
1971
+ params: null,
1972
+ query: null,
1973
+ body: null
1974
+ },
1975
+ response: {
1976
+ status: 200,
1977
+ body: defineListResponse(trackSummarySchema)
1978
+ },
1979
+ errors: [],
1980
+ lifecycle: { introduced: "1.0.0" },
1981
+ docs: {
1982
+ summary: "List tracks",
1983
+ description: "Every track that holds at least one Skill, the track with the most Skills first. The list is short, so it has no pages.",
1984
+ tag: "Tracks",
1985
+ examples: [{
1986
+ request: {},
1987
+ response: {
1988
+ items: [{
1989
+ ...exampleTrack,
1990
+ skillCount: 41
1991
+ }],
1992
+ total: 14
1993
+ }
1994
+ }]
1995
+ }
1996
+ }),
1997
+ get: defineOperation({
1998
+ id: "tracks.get",
1999
+ method: "GET",
2000
+ path: "/api/v1/tracks/{slug}",
2001
+ access: "public",
2002
+ semantics: { kind: "query" },
2003
+ cache: {
2004
+ _tag: "public",
2005
+ maxAgeSeconds: 300,
2006
+ staleWhileRevalidateSeconds: 3600
2007
+ },
2008
+ request: {
2009
+ params: z.strictObject({ slug: trackSlugSchema }),
2010
+ query: z.object(pageQueryShape({
2011
+ defaultLimit: 20,
2012
+ maxLimit: 100
2013
+ })),
2014
+ body: null
2015
+ },
2016
+ response: {
2017
+ status: 200,
2018
+ body: trackDetailSchema
2019
+ },
2020
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
2021
+ lifecycle: { introduced: "1.0.0" },
2022
+ docs: {
2023
+ summary: "Get a track",
2024
+ description: "One track and its Skills, in the order its page shows them: the pinned Skills first, then the rest by stars. `total` counts every Skill in the track.",
2025
+ tag: "Tracks",
2026
+ examples: [{
2027
+ request: {
2028
+ params: { slug: "design" },
2029
+ query: { limit: 1 }
2030
+ },
2031
+ response: {
2032
+ ...exampleTrack,
2033
+ items: [exampleSkillSummary],
2034
+ total: 41
2035
+ }
2036
+ }]
2037
+ }
2038
+ })
2039
+ }
2040
+ });
2041
+ const trendingPostSchema = defineResponseObject({
2042
+ url: urlSchema,
2043
+ platform: z.enum(["x", "bsky"]),
2044
+ authorHandle: z.string(),
2045
+ /** The display name the network sent with the post, if any. */
2046
+ authorName: z.string().nullable(),
2047
+ text: z.string(),
2048
+ postedAt: isoDateTimeSchema
2049
+ });
2050
+ const socialFields = {
2051
+ /** Separate accounts that posted about the Skill in the window. */
2052
+ authorCount: countSchema,
2053
+ mentionCount: countSchema
2054
+ };
2055
+ const starSurgeFields = {
2056
+ /** Stars the Repository gained on the surge day. */
2057
+ starGain: countSchema,
2058
+ /** UTC midnight of the surge day. */
2059
+ surgedOn: isoDateTimeSchema
2060
+ };
2061
+ /**
2062
+ * Why a Skill is on the board. ADR-0004 makes each row state its own reason,
2063
+ * and a row a post put there ships that post.
2064
+ *
2065
+ * - `social`: devs posted about the Skill on X or Bluesky. `post` is one of those posts.
2066
+ * - `star-surge`: the Repository gained stars fast, and it holds only this Skill.
2067
+ * - `social-and-star-surge`: both of the above.
2068
+ * - `star-count`: the socials were quiet, so the board filled up with a well-starred Skill. Nobody posted about it.
2069
+ */
2070
+ const trendingSignalSchema = {
2071
+ producer: z.discriminatedUnion("kind", [
2072
+ z.strictObject({
2073
+ kind: z.literal("social"),
2074
+ ...socialFields,
2075
+ post: trendingPostSchema.producer
2076
+ }),
2077
+ z.strictObject({
2078
+ kind: z.literal("star-surge"),
2079
+ ...starSurgeFields
2080
+ }),
2081
+ z.strictObject({
2082
+ kind: z.literal("social-and-star-surge"),
2083
+ ...socialFields,
2084
+ post: trendingPostSchema.producer,
2085
+ ...starSurgeFields
2086
+ }),
2087
+ z.strictObject({ kind: z.literal("star-count") })
2088
+ ]),
2089
+ client: z.discriminatedUnion("kind", [
2090
+ z.looseObject({
2091
+ kind: z.literal("social"),
2092
+ ...socialFields,
2093
+ post: trendingPostSchema.client
2094
+ }),
2095
+ z.looseObject({
2096
+ kind: z.literal("star-surge"),
2097
+ ...starSurgeFields
2098
+ }),
2099
+ z.looseObject({
2100
+ kind: z.literal("social-and-star-surge"),
2101
+ ...socialFields,
2102
+ post: trendingPostSchema.client,
2103
+ ...starSurgeFields
2104
+ }),
2105
+ z.looseObject({ kind: z.literal("star-count") })
2106
+ ])
2107
+ };
2108
+ const trendingSkillSchema = {
2109
+ producer: z.strictObject({
2110
+ ...skillSummaryShape,
2111
+ signal: trendingSignalSchema.producer
2112
+ }),
2113
+ client: z.looseObject({
2114
+ ...skillSummaryShape,
2115
+ signal: trendingSignalSchema.client
2116
+ })
2117
+ };
2118
+ /** The board's two windows, in the hours the ranking reads. */
2119
+ const TRENDING_WINDOW_HOURS = {
2120
+ week: 168,
2121
+ month: 720
2122
+ };
2123
+ const trendingV1 = defineRegistry({
2124
+ namespace: "trending",
2125
+ description: "Skills devs talk about on X and Bluesky, each with the reason it is on the board (ADR-0004). Never ranked by installs or likes.",
2126
+ operations: { list: defineOperation({
2127
+ id: "trending.list",
2128
+ method: "GET",
2129
+ path: "/api/v1/trending",
2130
+ access: "public",
2131
+ semantics: { kind: "query" },
2132
+ cache: {
2133
+ _tag: "public",
2134
+ maxAgeSeconds: 300,
2135
+ staleWhileRevalidateSeconds: 600
2136
+ },
2137
+ request: {
2138
+ params: null,
2139
+ query: z.object({
2140
+ window: z.enum(["week", "month"]).default("week"),
2141
+ limit: z.coerce.number().int().min(1).max(30).default(30)
2142
+ }),
2143
+ body: null
2144
+ },
2145
+ response: {
2146
+ status: 200,
2147
+ body: defineListResponse(trendingSkillSchema)
2148
+ },
2149
+ errors: ["INVALID_REQUEST"],
2150
+ lifecycle: { introduced: "1.0.0" },
2151
+ docs: {
2152
+ summary: "List trending Skills",
2153
+ description: "The trending board at skilld.dev/skills/trending, in rank order. Rows with a `social` or `star-surge` reason always rank above `star-count` rows. `total` counts the whole board, at most 30 rows.",
2154
+ tag: "Trending",
2155
+ examples: [{
2156
+ request: { query: {
2157
+ window: "week",
2158
+ limit: 1
2159
+ } },
2160
+ response: {
2161
+ items: [{
2162
+ ...exampleSkillSummary,
2163
+ signal: {
2164
+ kind: "social",
2165
+ authorCount: 3,
2166
+ mentionCount: 4,
2167
+ post: {
2168
+ url: "https://x.com/ada_ships/status/1972000000000000000",
2169
+ platform: "x",
2170
+ authorHandle: "ada_ships",
2171
+ authorName: "Ada",
2172
+ text: "web-design-guidelines catches the UI mistakes I used to catch in review.",
2173
+ postedAt: "2026-09-29T16:40:00.000Z"
2174
+ }
2175
+ }
2176
+ }],
2177
+ total: 30
2178
+ }
2179
+ }]
2180
+ }
2181
+ }) }
2182
+ });
2183
+ //#endregion
2184
+ //#region src/contract/skills.ts
2185
+ /**
2186
+ * A Skill name as the Agent Skills specification allows it. Search answers
2187
+ * only names that pass, because the skilld CLI rejects any other.
2188
+ */
2189
+ const specSkillNameSchema = z.string().max(64).regex(/^[a-z0-9](?:[a-z0-9]|-(?!-)){0,62}[a-z0-9]$|^[a-z0-9]$/);
2190
+ const isSpecSkillName = (value) => specSkillNameSchema.safeParse(value).success;
2191
+ const searchSource = z.strictObject({
2192
+ provider: z.literal("github"),
2193
+ owner: z.string().min(1).max(39),
2194
+ repository: z.string().min(1).max(100),
2195
+ selector: z.strictObject({
2196
+ type: z.literal("named-skill"),
2197
+ name: specSkillNameSchema
2198
+ })
2199
+ });
2200
+ /**
2201
+ * Frozen. skilld 3.2.0 parses this answer with `deny_unknown_fields`, so a new
2202
+ * field here fails every released `skilld search`. Add new fields to
2203
+ * `skills.get` instead.
2204
+ */
2205
+ const searchItemShape = {
2206
+ name: specSkillNameSchema,
2207
+ description: z.string().max(500).nullable(),
2208
+ source: searchSource,
2209
+ stargazerCount: countSchema
2210
+ };
2211
+ const skillSearchResponse = {
2212
+ producer: z.strictObject({
2213
+ items: z.array(z.strictObject(searchItemShape)).max(50),
2214
+ total: countSchema
2215
+ }),
2216
+ client: z.looseObject({
2217
+ items: z.array(z.looseObject(searchItemShape)).max(50),
2218
+ total: countSchema
2219
+ })
2220
+ };
2221
+ const skillDetailSchema = defineResponseObject({
2222
+ ...skillSummaryShape,
2223
+ /** The GitHub profile name of the Owner, when the registry has synced it. */
2224
+ authorName: z.string().nullable(),
2225
+ license: z.string().nullable(),
2226
+ repositoryUrl: urlSchema,
2227
+ /** The SKILL.md path inside the Repository. */
2228
+ skillPath: z.string().nullable(),
2229
+ /** The commit the registry last read. */
2230
+ sourceCommit: z.string().nullable(),
2231
+ /** True when the SKILL.md is gone upstream. The registry keeps the last copy it read. */
2232
+ sourceGone: z.boolean(),
2233
+ /** The Repository's last push. `updatedAt` is the SKILL.md's own last change. */
2234
+ pushedAt: isoDateTimeSchema.nullable(),
2235
+ tags: z.array(z.string()),
2236
+ /** The `allowed-tools` the SKILL.md frontmatter asks for. */
2237
+ allowedTools: z.array(z.string()),
2238
+ /** Files beside the SKILL.md. `run --file` reads one. */
2239
+ files: z.array(z.strictObject({
2240
+ path: z.string(),
2241
+ size: countSchema
2242
+ })),
2243
+ /** Machine-generated from the SKILL.md. Never written by the author. */
2244
+ generatedSummary: z.string().nullable(),
2245
+ /** The SKILL.md text the registry last read, frontmatter included. */
2246
+ markdown: z.string().nullable()
2247
+ });
2248
+ /**
2249
+ * The skilld.dev/skills listing. Stars order it by default. Likes order it
2250
+ * only when the caller asks (ADR-0003), and install counts never do.
2251
+ */
2252
+ const skillBrowseQuery = z.object({
2253
+ q: z.string().trim().min(1).max(200).optional(),
2254
+ owner: ownerSchema.optional(),
2255
+ tag: z.string().trim().toLowerCase().min(1).max(64).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "must be a tag slug").optional(),
2256
+ sort: z.enum([
2257
+ "stars",
2258
+ "likes",
2259
+ "updated"
2260
+ ]).default("stars"),
2261
+ ...pageQueryShape({
2262
+ defaultLimit: 20,
2263
+ maxLimit: 100
2264
+ })
2265
+ });
2266
+ const skillsV1 = defineRegistry({
2267
+ namespace: "skills",
2268
+ description: "Search the registry and read one Skill with its provenance.",
2269
+ operations: {
2270
+ search: defineOperation({
2271
+ id: "skills.search",
2272
+ method: "GET",
2273
+ path: "/api/v1/skills",
2274
+ access: "public",
2275
+ semantics: { kind: "query" },
2276
+ cache: {
2277
+ _tag: "public",
2278
+ maxAgeSeconds: 60,
2279
+ staleWhileRevalidateSeconds: 300
2280
+ },
2281
+ request: {
2282
+ params: null,
2283
+ query: z.strictObject({
2284
+ q: z.string().trim().min(1).max(200),
2285
+ limit: z.coerce.number().int().min(1).max(50).default(20)
2286
+ }),
2287
+ body: null
2288
+ },
2289
+ response: {
2290
+ status: 200,
2291
+ body: skillSearchResponse
2292
+ },
2293
+ errors: ["INVALID_REQUEST"],
2294
+ lifecycle: { introduced: "1.0.0" },
2295
+ docs: {
2296
+ summary: "Search Skills",
2297
+ description: "Semantic search over admitted Skills. The answer shape is frozen for skilld 3.2.0, so use `skills.get` for anything beyond the source and the star count.",
2298
+ tag: "Skills",
2299
+ examples: [{
2300
+ request: { query: {
2301
+ q: "tailwind",
2302
+ limit: 1
2303
+ } },
2304
+ response: {
2305
+ items: [{
2306
+ name: "tailwind-v4",
2307
+ description: "Tailwind CSS v4 patterns and migration notes.",
2308
+ source: {
2309
+ provider: "github",
2310
+ owner: "nuxt",
2311
+ repository: "ui",
2312
+ selector: {
2313
+ type: "named-skill",
2314
+ name: "tailwind-v4"
2315
+ }
2316
+ },
2317
+ stargazerCount: 5421
2318
+ }],
2319
+ total: 12
2320
+ }
2321
+ }]
2322
+ }
2323
+ }),
2324
+ get: defineOperation({
2325
+ id: "skills.get",
2326
+ method: "GET",
2327
+ path: "/api/v1/skills/{owner}/{repository}/{name}",
2328
+ access: "public",
2329
+ semantics: { kind: "query" },
2330
+ cache: {
2331
+ _tag: "public",
2332
+ maxAgeSeconds: 60,
2333
+ staleWhileRevalidateSeconds: 300
2334
+ },
2335
+ request: {
2336
+ params: skillParams,
2337
+ query: null,
2338
+ body: null
2339
+ },
2340
+ response: {
2341
+ status: 200,
2342
+ body: skillDetailSchema
2343
+ },
2344
+ errors: ["INVALID_REQUEST", "NOT_FOUND"],
2345
+ lifecycle: { introduced: "1.0.0" },
2346
+ docs: {
2347
+ summary: "Get a Skill",
2348
+ description: "One Skill with its provenance: the Owner, the exact SKILL.md, the commit the registry read, and the run and install commands.",
2349
+ tag: "Skills",
2350
+ examples: [{
2351
+ request: { params: {
2352
+ owner: "vercel-labs",
2353
+ repository: "agent-skills",
2354
+ name: "web-design-guidelines"
2355
+ } },
2356
+ response: {
2357
+ ...exampleSkillSummary,
2358
+ authorName: "Vercel Labs",
2359
+ license: "MIT",
2360
+ repositoryUrl: "https://github.com/vercel-labs/agent-skills",
2361
+ skillPath: "skills/web-design-guidelines/SKILL.md",
2362
+ sourceCommit: "4f1c2a9e0b7d3c5a8e6f1b2d4c7a9e0f3b5d8c1a",
2363
+ sourceGone: false,
2364
+ pushedAt: "2026-09-30T09:12:44.000Z",
2365
+ tags: ["design", "accessibility"],
2366
+ allowedTools: [],
2367
+ files: [{
2368
+ path: "references/checklist.md",
2369
+ size: 4210
2370
+ }],
2371
+ generatedSummary: "Checks interface code against a published list of web design rules.",
2372
+ markdown: "---\nname: web-design-guidelines\ndescription: Review UI code for compliance with web interface guidelines.\n---\n\n# Web Design Guidelines\n"
2373
+ }
2374
+ }]
2375
+ }
2376
+ }),
2377
+ browse: defineOperation({
2378
+ id: "skills.browse",
2379
+ method: "GET",
2380
+ path: "/api/v1/browse",
2381
+ access: "public",
2382
+ semantics: { kind: "query" },
2383
+ cache: {
2384
+ _tag: "public",
2385
+ maxAgeSeconds: 60,
2386
+ staleWhileRevalidateSeconds: 300
2387
+ },
2388
+ request: {
2389
+ params: null,
2390
+ query: skillBrowseQuery,
2391
+ body: null
2392
+ },
2393
+ response: {
2394
+ status: 200,
2395
+ body: defineListResponse(skillSummarySchema)
2396
+ },
2397
+ errors: ["INVALID_REQUEST"],
2398
+ lifecycle: { introduced: "1.0.0" },
2399
+ docs: {
2400
+ summary: "Browse Skills",
2401
+ description: "The listing behind skilld.dev/skills, filtered by `owner` and `tag`. `sort` orders it by stars, likes, or the last SKILL.md change. With `q`, Skills rank by relevance to the query and `sort` does not apply. Use `tracks.get` for the Skills of one track.",
2402
+ tag: "Skills",
2403
+ examples: [{
2404
+ request: { query: {
2405
+ owner: "vercel-labs",
2406
+ sort: "stars",
2407
+ limit: 1
2408
+ } },
2409
+ response: {
2410
+ items: [exampleSkillSummary],
2411
+ total: 6
2412
+ }
2413
+ }]
2414
+ }
2415
+ })
2416
+ }
2417
+ });
2418
+ //#endregion
2419
+ //#region src/contract/index.ts
2420
+ const skilldV1Protocol = defineProtocol({
2421
+ version: "1",
2422
+ registries: {
2423
+ skills: skillsV1,
2424
+ repositories: repositoriesV1,
2425
+ indexRequests: indexRequestsV1,
2426
+ owners: ownersV1,
2427
+ tracks: tracksV1,
2428
+ trending: trendingV1,
2429
+ curators: curatorsV1,
2430
+ collections: collectionsV1,
2431
+ account: accountV1,
2432
+ likes: likesV1,
2433
+ watches: watchesV1,
2434
+ stars: starsV1,
2435
+ changes: changesV1,
2436
+ tokens: tokensV1
2437
+ }
2438
+ });
2439
+ //#endregion
2440
+ export { RESERVED_COLLECTION_SLUGS, SKILLD_V1_ERROR_CODES, SKILLD_V1_ERROR_STATUS, SKILLD_V1_ERROR_TITLES, SKILLD_V1_IMPLICIT_ERRORS, SKILLD_V1_METHODS, SKILLD_V1_ORIGIN, SKILLD_V1_PATH_PREFIX, SKILLD_V1_RESPONSE_HEADERS, SKILLD_V1_RETRYABLE_ERRORS, SKILLD_V1_VERSION, TRENDING_WINDOW_HOURS, accountChangesSchema, accountSchema, accountV1, buildOperationPath, changesV1, collectionDetailSchema, collectionSkillSchema, collectionSlugSchema, collectionsV1, countSchema, createCollectionBody, curatorDetailSchema, curatorListItemSchema, curatorsV1, defineListResponse, defineOperation, defineProtocol, defineRegistry, defineResponseObject, exampleSkillSummary, indexRequestCreatedSchema, indexRequestSchema, indexRequestsV1, isSpecSkillName, isoDateTimeSchema, issuedTokenSchema, likedSkillSchema, likesV1, listOperations, loginSchema, ownerProfileSchema, ownerSchema, ownersV1, pageQueryShape, problemSchema, problemType, repositoriesV1, repositoryParams, repositoryProfileSchema, repositoryScanSchema, repositorySchema, skillBrowseQuery, skillDetailSchema, skillNameSchema, skillParams, skillSearchResponse, skillSummarySchema, skillSummaryShape, skilldV1Protocol, skillsV1, specSkillNameSchema, starredRepositorySchema, starsImportSchema, starsV1, tokenSchema, tokensV1, trackDetailSchema, trackSlugSchema, trackSummarySchema, tracksV1, trendingSignalSchema, trendingSkillSchema, trendingV1, urlSchema, watchSchema, watchesV1 };