@mulmoclaude/core 0.2.15 → 0.4.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 (67) hide show
  1. package/dist/collection/registry/guards.d.ts +2 -0
  2. package/dist/collection/registry/index.cjs +4 -0
  3. package/dist/collection/registry/index.d.ts +2 -0
  4. package/dist/collection/registry/index.js +2 -0
  5. package/dist/collection/registry/registryIndex.d.ts +48 -0
  6. package/dist/collection/registry/server/client.d.ts +60 -0
  7. package/dist/collection/registry/server/collectionFiles.d.ts +51 -0
  8. package/dist/collection/registry/server/exportCollection.d.ts +29 -0
  9. package/dist/collection/registry/server/fetch.d.ts +8 -0
  10. package/dist/collection/registry/server/importCollection.d.ts +35 -0
  11. package/dist/collection/registry/server/importWriter.d.ts +30 -0
  12. package/dist/collection/registry/server/index.cjs +1110 -0
  13. package/dist/collection/registry/server/index.cjs.map +1 -0
  14. package/dist/collection/registry/server/index.d.ts +28 -0
  15. package/dist/collection/registry/server/index.js +1076 -0
  16. package/dist/collection/registry/server/index.js.map +1 -0
  17. package/dist/collection/registry/server/performExport.d.ts +6 -0
  18. package/dist/collection/registry/server/registriesConfig.d.ts +11 -0
  19. package/dist/collection/registry/server/skillDescription.d.ts +4 -0
  20. package/dist/collection/registry/types.d.ts +37 -0
  21. package/dist/collection/server/host.d.ts +7 -0
  22. package/dist/collection/server/index.cjs +1 -1
  23. package/dist/collection/server/index.js +1 -1
  24. package/dist/collection/server/util.d.ts +1 -0
  25. package/dist/collection-watchers/index.cjs +1 -1
  26. package/dist/collection-watchers/index.js +1 -1
  27. package/dist/feeds/server/index.cjs +1 -1
  28. package/dist/feeds/server/index.cjs.map +1 -1
  29. package/dist/feeds/server/index.js +1 -1
  30. package/dist/feeds/server/index.js.map +1 -1
  31. package/dist/notifier-ChpY0XrY.js.map +1 -1
  32. package/dist/notifier-bS8IEeLA.cjs.map +1 -1
  33. package/dist/{server-CRlmOBVQ.js → server-DRoqc8dL.js} +8 -2
  34. package/dist/server-DRoqc8dL.js.map +1 -0
  35. package/dist/{server-BjXvqGrE.cjs → server-DoDXibCq.cjs} +31 -1
  36. package/dist/server-DoDXibCq.cjs.map +1 -0
  37. package/dist/slug-CdN-pQX1.js +37 -0
  38. package/dist/slug-CdN-pQX1.js.map +1 -0
  39. package/dist/slug-DbwORN9z.cjs +48 -0
  40. package/dist/slug-DbwORN9z.cjs.map +1 -0
  41. package/dist/types-BKVZrtyc.js +123 -0
  42. package/dist/types-BKVZrtyc.js.map +1 -0
  43. package/dist/types-CNqkLT4M.cjs +140 -0
  44. package/dist/types-CNqkLT4M.cjs.map +1 -0
  45. package/dist/whisper/client.cjs.map +1 -1
  46. package/dist/whisper/client.js.map +1 -1
  47. package/dist/wiki/graph.d.ts +33 -0
  48. package/dist/wiki/index-parse.d.ts +39 -0
  49. package/dist/wiki/index.cjs +493 -0
  50. package/dist/wiki/index.cjs.map +1 -0
  51. package/dist/wiki/index.d.ts +7 -0
  52. package/dist/wiki/index.js +465 -0
  53. package/dist/wiki/index.js.map +1 -0
  54. package/dist/wiki/link.d.ts +41 -0
  55. package/dist/wiki/lint.d.ts +26 -0
  56. package/dist/wiki/render.d.ts +19 -0
  57. package/dist/wiki/route.d.ts +32 -0
  58. package/dist/wiki/server/index.cjs +30 -0
  59. package/dist/wiki/server/index.cjs.map +1 -0
  60. package/dist/wiki/server/index.d.ts +1 -0
  61. package/dist/wiki/server/index.js +27 -0
  62. package/dist/wiki/server/index.js.map +1 -0
  63. package/dist/wiki/server/paths.d.ts +11 -0
  64. package/dist/wiki/slug.d.ts +24 -0
  65. package/package.json +25 -1
  66. package/dist/server-BjXvqGrE.cjs.map +0 -1
  67. package/dist/server-CRlmOBVQ.js.map +0 -1
@@ -0,0 +1,1110 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rolldown_runtime = require("../../../rolldown-runtime-D6vf50IK.cjs");
3
+ const require_server = require("../../../server-DoDXibCq.cjs");
4
+ const require_collection_paths = require("../../paths.cjs");
5
+ const require_types = require("../../../types-CNqkLT4M.cjs");
6
+ const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
7
+ let node_path = require("node:path");
8
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
9
+ let node_fs = require("node:fs");
10
+ let node_fs_promises = require("node:fs/promises");
11
+ //#region src/collection/registry/server/fetch.ts
12
+ var DEFAULT_FETCH_TIMEOUT_MS = 10 * require_server.ONE_SECOND_MS;
13
+ /** `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`
14
+ * elapses. Composes with a caller-supplied `signal` so external cancellation
15
+ * still works. */
16
+ async function fetchWithTimeout(url, init = {}) {
17
+ const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;
18
+ if (callerSignal?.aborted) throw callerSignal.reason ?? new DOMException("Aborted", "AbortError");
19
+ const controller = new AbortController();
20
+ const timer = setTimeout(() => {
21
+ controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, "TimeoutError"));
22
+ }, timeoutMs);
23
+ const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);
24
+ try {
25
+ return await fetch(url, {
26
+ ...rest,
27
+ signal: controller.signal
28
+ });
29
+ } finally {
30
+ clearTimeout(timer);
31
+ unsubscribeCaller?.();
32
+ }
33
+ }
34
+ function bridgeExternalSignal(external, controller) {
35
+ if (!external) return null;
36
+ const onAbort = () => controller.abort(external.reason);
37
+ external.addEventListener("abort", onAbort, { once: true });
38
+ return () => external.removeEventListener("abort", onAbort);
39
+ }
40
+ //#endregion
41
+ //#region src/collection/registry/server/registriesConfig.ts
42
+ var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
43
+ function isValidHttpsUrl(value) {
44
+ if (typeof value !== "string" || value.length === 0) return false;
45
+ let url;
46
+ try {
47
+ url = new URL(value);
48
+ } catch {
49
+ return false;
50
+ }
51
+ if (url.protocol !== "https:") return false;
52
+ if (url.username !== "" || url.password !== "") return false;
53
+ return true;
54
+ }
55
+ function parseEntry(value, index) {
56
+ if (!require_types.isRecord(value)) return `entry[${index}] is not an object`;
57
+ const { name, indexUrl, rawBaseUrl } = value;
58
+ if (typeof name !== "string" || !NAME_RE.test(name)) return `entry[${index}].name must match ${NAME_RE.source}`;
59
+ if (name === "official") return `entry[${index}].name "${require_types.OFFICIAL_REGISTRY_NAME}" is reserved`;
60
+ if (!isValidHttpsUrl(indexUrl)) return `entry[${index}].indexUrl must be a valid HTTPS URL (no credentials)`;
61
+ if (!isValidHttpsUrl(rawBaseUrl)) return `entry[${index}].rawBaseUrl must be a valid HTTPS URL (no credentials)`;
62
+ if (rawBaseUrl.includes("?") || rawBaseUrl.includes("#")) return `entry[${index}].rawBaseUrl must not contain a query (?) or fragment (#) — it's joined as a path prefix`;
63
+ let normalizedRawBase = rawBaseUrl;
64
+ while (normalizedRawBase.endsWith("/")) normalizedRawBase = normalizedRawBase.slice(0, -1);
65
+ return {
66
+ name,
67
+ indexUrl,
68
+ rawBaseUrl: normalizedRawBase
69
+ };
70
+ }
71
+ /** Parse the JSON text of `config/collections-registries.json`. Bad entries
72
+ * drop with a warning; valid entries are returned in source order. Exported
73
+ * for unit testing — the I/O wrapper `loadRegistriesConfig` below reads the
74
+ * workspace file and delegates here. */
75
+ function parseRegistriesConfig(raw) {
76
+ if (!Array.isArray(raw)) {
77
+ require_server.log.warn("collections-registry", "registries config is not an array — ignoring");
78
+ return [];
79
+ }
80
+ const seen = /* @__PURE__ */ new Set();
81
+ const out = [];
82
+ for (let i = 0; i < raw.length; i++) {
83
+ const parsed = parseEntry(raw[i], i);
84
+ if (typeof parsed === "string") {
85
+ require_server.log.warn("collections-registry", "registry config entry rejected", { reason: parsed });
86
+ continue;
87
+ }
88
+ if (seen.has(parsed.name)) {
89
+ require_server.log.warn("collections-registry", "registry config duplicate name dropped", { name: parsed.name });
90
+ continue;
91
+ }
92
+ seen.add(parsed.name);
93
+ out.push(parsed);
94
+ }
95
+ return out;
96
+ }
97
+ function readConfigTextOrNull(filePath) {
98
+ try {
99
+ return (0, node_fs.readFileSync)(filePath, "utf-8");
100
+ } catch (err) {
101
+ if (require_types.isRecord(err) && err.code === "ENOENT") return null;
102
+ throw err;
103
+ }
104
+ }
105
+ /** Read `config/collections-registries.json` from the configured workspace.
106
+ * Missing file ⇒ empty list. Malformed JSON or rejected entries log but never
107
+ * throw. */
108
+ function loadRegistriesConfig() {
109
+ const text = readConfigTextOrNull(require_server.collectionsRegistriesConfigPath());
110
+ if (text === null) return [];
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(text);
114
+ } catch (err) {
115
+ require_server.log.warn("collections-registry", "registries config is not valid JSON — ignoring", { error: err instanceof Error ? err.message : String(err) });
116
+ return [];
117
+ }
118
+ return parseRegistriesConfig(parsed);
119
+ }
120
+ //#endregion
121
+ //#region src/collection/registry/server/client.ts
122
+ var DEFAULT_OFFICIAL_INDEX_URL = "https://receptron.github.io/mulmoclaude-collections/index.json";
123
+ var DEFAULT_OFFICIAL_RAW_BASE = "https://raw.githubusercontent.com/receptron/mulmoclaude-collections/main";
124
+ var CACHE_TTL_MS = 300 * require_server.ONE_SECOND_MS;
125
+ var STALE_RETRY_BACKOFF_MS = 60 * require_server.ONE_SECOND_MS;
126
+ var FETCH_TIMEOUT_MS$1 = 10 * require_server.ONE_SECOND_MS;
127
+ var STATUS_BAD_GATEWAY$2 = 502;
128
+ var STATUS_UNAVAILABLE$1 = 503;
129
+ var cache = /* @__PURE__ */ new Map();
130
+ var lastFailureMs = /* @__PURE__ */ new Map();
131
+ function officialDescriptor() {
132
+ return {
133
+ name: require_types.OFFICIAL_REGISTRY_NAME,
134
+ indexUrl: process.env.COLLECTIONS_REGISTRY_URL ?? DEFAULT_OFFICIAL_INDEX_URL,
135
+ rawBaseUrl: process.env.COLLECTIONS_REGISTRY_RAW_BASE ?? DEFAULT_OFFICIAL_RAW_BASE
136
+ };
137
+ }
138
+ /** The full ordered registry list: official first, then user-configured ones.
139
+ * Re-reads the config file on every call so a Discover refresh picks up edits
140
+ * without a server restart (the config is small + read-rarely). */
141
+ function listRegistries() {
142
+ return [officialDescriptor(), ...loadRegistriesConfig()];
143
+ }
144
+ /** Look up one registry descriptor by name. Used by the preview / import path,
145
+ * which has the registry label from the entry and needs the rawBase. */
146
+ function findRegistry(name) {
147
+ return listRegistries().find((reg) => reg.name === name) ?? null;
148
+ }
149
+ async function loadFromNetwork(descriptor) {
150
+ let res;
151
+ try {
152
+ res = await fetchWithTimeout(descriptor.indexUrl, {
153
+ timeoutMs: FETCH_TIMEOUT_MS$1,
154
+ headers: { accept: "application/json" }
155
+ });
156
+ } catch (err) {
157
+ require_server.log.warn("collections-registry", "index fetch failed", {
158
+ registry: descriptor.name,
159
+ url: descriptor.indexUrl,
160
+ error: require_server.errorMessage(err)
161
+ });
162
+ return {
163
+ ok: false,
164
+ status: STATUS_UNAVAILABLE$1,
165
+ error: "registry unreachable"
166
+ };
167
+ }
168
+ if (!res.ok) return {
169
+ ok: false,
170
+ status: STATUS_BAD_GATEWAY$2,
171
+ error: `registry responded ${res.status}`
172
+ };
173
+ const parsed = require_types.parseRegistryIndex(await res.json().catch(() => null), descriptor.name);
174
+ if (!parsed.ok) {
175
+ require_server.log.warn("collections-registry", "index invalid", {
176
+ registry: descriptor.name,
177
+ url: descriptor.indexUrl,
178
+ error: parsed.error
179
+ });
180
+ return {
181
+ ok: false,
182
+ status: STATUS_BAD_GATEWAY$2,
183
+ error: `registry index invalid: ${parsed.error}`
184
+ };
185
+ }
186
+ return {
187
+ ok: true,
188
+ index: parsed.index,
189
+ stale: false
190
+ };
191
+ }
192
+ /** Cache key incorporates BOTH URLs so editing `indexUrl` or `rawBaseUrl` for
193
+ * an existing registry name invalidates the cached index automatically — same
194
+ * refresh cycle as if the user had renamed it (CodeRabbit review on #1837).
195
+ * Without this, the Discover catalog could keep serving the old upstream's
196
+ * entries while preview / import resolved the new rawBase, drifting until TTL
197
+ * expiry. Tab + key-content guarantees no collision between e.g. swapped
198
+ * name/url pairs. */
199
+ function descriptorCacheKey(descriptor) {
200
+ return `${descriptor.name}\t${descriptor.indexUrl}\t${descriptor.rawBaseUrl}`;
201
+ }
202
+ /** Fetch one registry's index. Same cache + stale-on-failure semantics as the
203
+ * original single-registry implementation — just keyed by descriptor identity
204
+ * (name + both URLs) so multiple registries don't fight over one slot AND a
205
+ * reconfigured registry doesn't serve a stale index from the prior URL. */
206
+ async function fetchRegistryIndex(descriptor, opts = {}) {
207
+ const nowMs = opts.nowMs ?? Date.now();
208
+ const loader = opts.loader ?? loadFromNetwork;
209
+ const key = descriptorCacheKey(descriptor);
210
+ const cached = cache.get(key);
211
+ if (!opts.force && cached && nowMs - cached.atMs < CACHE_TTL_MS) return {
212
+ ok: true,
213
+ index: cached.index,
214
+ stale: false
215
+ };
216
+ const failedAt = lastFailureMs.get(key);
217
+ if (!opts.force && cached && failedAt !== void 0 && nowMs - failedAt < STALE_RETRY_BACKOFF_MS) return {
218
+ ok: true,
219
+ index: cached.index,
220
+ stale: true
221
+ };
222
+ const fresh = await loader(descriptor);
223
+ if (fresh.ok) {
224
+ cache.set(key, {
225
+ index: fresh.index,
226
+ atMs: nowMs
227
+ });
228
+ lastFailureMs.delete(key);
229
+ return {
230
+ ok: true,
231
+ index: fresh.index,
232
+ stale: false
233
+ };
234
+ }
235
+ lastFailureMs.set(key, nowMs);
236
+ if (cached) return {
237
+ ok: true,
238
+ index: cached.index,
239
+ stale: true
240
+ };
241
+ return fresh;
242
+ }
243
+ /** Fetch every configured registry in parallel and return per-registry
244
+ * outcomes. Callers (the Discover route) concatenate `entries` from each.
245
+ * Failure of any single registry doesn't abort the others — that's the point
246
+ * of supporting multiple registries. */
247
+ async function fetchAllRegistries(opts = {}) {
248
+ const descriptors = listRegistries();
249
+ return await Promise.all(descriptors.map(async (descriptor) => {
250
+ const result = await fetchRegistryIndex(descriptor, opts);
251
+ if (!result.ok) return {
252
+ name: descriptor.name,
253
+ status: "failed",
254
+ generatedAt: null,
255
+ error: result.error,
256
+ entries: []
257
+ };
258
+ return {
259
+ name: descriptor.name,
260
+ status: result.stale ? "stale" : "ok",
261
+ generatedAt: result.index.generatedAt,
262
+ error: null,
263
+ entries: result.index.collections
264
+ };
265
+ }));
266
+ }
267
+ /** Test seam: reset the module cache + failure backoff state (all registries). */
268
+ function resetRegistryCache() {
269
+ cache.clear();
270
+ lastFailureMs.clear();
271
+ }
272
+ //#endregion
273
+ //#region src/collection/registry/server/collectionFiles.ts
274
+ var FETCH_TIMEOUT_MS = 10 * require_server.ONE_SECOND_MS;
275
+ var STATUS_BAD_GATEWAY$1 = 502;
276
+ var STATUS_UNAVAILABLE = 503;
277
+ var STATUS_NOT_FOUND$2 = 404;
278
+ /** Compose the raw URL for `<dirPath>/<relFile>` under a given registry's
279
+ * rawBase. Empty and traversal (`.`/`..`) segments are dropped so the URL can
280
+ * never escape the base, even if an upstream check is bypassed (the index
281
+ * parser already rejects such identifiers — this is defense-in-depth). The
282
+ * rawBase is trailing-slash-normalized: `parseRegistriesConfig` already trims
283
+ * user-config trailing slashes, but the official descriptor and any test
284
+ * bypass parse — repeating the trim here keeps the join `${base}/${path}`
285
+ * from producing `//` even when the caller bypassed config validation
286
+ * (CodeRabbit review on #1837). */
287
+ function collectionFileUrl(rawBase, dirPath, relFile) {
288
+ let base = rawBase;
289
+ while (base.endsWith("/")) base = base.slice(0, -1);
290
+ const segments = dirPath.split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..");
291
+ return `${base}/${segments.join("/")}/${relFile}`;
292
+ }
293
+ /** Fetch one file out of a registry collection. `rawBase` comes from the
294
+ * entry's source registry (`findRegistry(entry.registryName).rawBaseUrl`),
295
+ * not a module-level setting — that's what makes additional user-configured
296
+ * registries reachable. */
297
+ async function fetchCollectionFile(rawBase, dirPath, relFile) {
298
+ const url = collectionFileUrl(rawBase, dirPath, relFile);
299
+ let res;
300
+ try {
301
+ res = await fetchWithTimeout(url, { timeoutMs: FETCH_TIMEOUT_MS });
302
+ } catch (err) {
303
+ return {
304
+ ok: false,
305
+ status: STATUS_UNAVAILABLE,
306
+ error: `fetch failed: ${require_server.errorMessage(err)}`
307
+ };
308
+ }
309
+ if (!res.ok) return {
310
+ ok: false,
311
+ status: STATUS_BAD_GATEWAY$1,
312
+ error: `${relFile} responded ${res.status}`
313
+ };
314
+ return {
315
+ ok: true,
316
+ text: await res.text()
317
+ };
318
+ }
319
+ function parseJsonObject(text, label) {
320
+ let parsed;
321
+ try {
322
+ parsed = JSON.parse(text);
323
+ } catch {
324
+ return {
325
+ ok: false,
326
+ error: `${label} is not valid JSON`
327
+ };
328
+ }
329
+ if (!require_types.isRecord(parsed)) return {
330
+ ok: false,
331
+ error: `${label} is not an object`
332
+ };
333
+ return {
334
+ ok: true,
335
+ value: parsed
336
+ };
337
+ }
338
+ async function fetchJsonObject(rawBase, dirPath, relFile, label) {
339
+ const file = await fetchCollectionFile(rawBase, dirPath, relFile);
340
+ if (!file.ok) return {
341
+ ok: false,
342
+ status: file.status,
343
+ error: `${label}: ${file.error}`
344
+ };
345
+ const parsed = parseJsonObject(file.text, label);
346
+ if (!parsed.ok) return {
347
+ ok: false,
348
+ status: STATUS_BAD_GATEWAY$1,
349
+ error: parsed.error
350
+ };
351
+ return {
352
+ ok: true,
353
+ value: parsed.value
354
+ };
355
+ }
356
+ /** Resolve an entry's rawBase from its `registryName`. A missing match (the
357
+ * user removed the registry from config while a cached index still references
358
+ * it) returns null — the caller surfaces it as a 404 rather than crashing. */
359
+ function rawBaseForEntry(entry) {
360
+ return findRegistry(entry.registryName)?.rawBaseUrl ?? null;
361
+ }
362
+ /** Resolve an entry by author+slug across every cached registry's entries.
363
+ * When `registry` is passed we constrain to that registry — needed because
364
+ * multiple registries can publish the same author/slug, and the UI should
365
+ * follow the card it just clicked. */
366
+ function findEntryInMergedView(merged, author, slug, registry) {
367
+ for (const reg of merged) {
368
+ if (registry !== null && reg.name !== registry) continue;
369
+ const match = reg.entries.find((candidate) => candidate.author === author && candidate.slug === slug);
370
+ if (match) return match;
371
+ }
372
+ return null;
373
+ }
374
+ /** Preview a registry collection: confirm it's in some registry's published
375
+ * index, then fetch + parse its schema.json + meta.json so the Discover tab
376
+ * can show fields/views before import. With multi-registry support the
377
+ * `registry` arg disambiguates same-name collections from different sources. */
378
+ async function previewCollection(author, slug, registry = null) {
379
+ const entry = findEntryInMergedView(await fetchAllRegistries(), author, slug, registry);
380
+ if (!entry) return {
381
+ ok: false,
382
+ status: STATUS_NOT_FOUND$2,
383
+ error: `unknown collection: ${author}/${slug}`
384
+ };
385
+ const rawBase = rawBaseForEntry(entry);
386
+ if (!rawBase) return {
387
+ ok: false,
388
+ status: STATUS_NOT_FOUND$2,
389
+ error: `registry "${entry.registryName}" is no longer configured`
390
+ };
391
+ const schema = await fetchJsonObject(rawBase, entry.path, "schema.json", "schema.json");
392
+ if (!schema.ok) return schema;
393
+ const meta = await fetchJsonObject(rawBase, entry.path, "meta.json", "meta.json");
394
+ if (!meta.ok) return meta;
395
+ return {
396
+ ok: true,
397
+ entry,
398
+ schema: schema.value,
399
+ meta: meta.value
400
+ };
401
+ }
402
+ //#endregion
403
+ //#region src/collection/registry/server/importCollection.ts
404
+ var MANIFEST_FILE = "manifest.json";
405
+ var STATUS_BAD_GATEWAY = 502;
406
+ /** A manifest entry must be a relative path that stays inside the collection dir:
407
+ * no absolute paths, no backslashes, no empty / `.` / `..` segments. */
408
+ function isSafeBundlePath(rel) {
409
+ if (typeof rel !== "string" || rel.length === 0) return false;
410
+ if (rel.startsWith("/") || rel.includes("\\")) return false;
411
+ return !rel.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
412
+ }
413
+ function parseManifest(value) {
414
+ if (!require_types.isRecord(value) || !Array.isArray(value.files)) return {
415
+ ok: false,
416
+ error: "manifest is missing a files[] array"
417
+ };
418
+ const unsafe = value.files.find((file) => !isSafeBundlePath(file));
419
+ if (unsafe !== void 0) return {
420
+ ok: false,
421
+ error: `manifest contains an unsafe path: ${String(unsafe)}`
422
+ };
423
+ return {
424
+ ok: true,
425
+ files: value.files.filter(isSafeBundlePath)
426
+ };
427
+ }
428
+ /** `data/collections/<localSlug>/items` — the host owns dataPath, never the
429
+ * registry's authored value, so imported collections can't collide on disk. */
430
+ function normalizedDataPath(localSlug) {
431
+ return `data/collections/${localSlug}/items`;
432
+ }
433
+ function withNormalizedDataPath(schema, localSlug) {
434
+ return {
435
+ ...schema,
436
+ dataPath: normalizedDataPath(localSlug)
437
+ };
438
+ }
439
+ async function fetchManifest(entry) {
440
+ const rawBase = rawBaseForEntry(entry);
441
+ if (!rawBase) return {
442
+ ok: false,
443
+ status: STATUS_BAD_GATEWAY,
444
+ error: `registry "${entry.registryName}" is no longer configured`
445
+ };
446
+ const file = await fetchCollectionFile(rawBase, entry.path, MANIFEST_FILE);
447
+ if (!file.ok) return {
448
+ ok: false,
449
+ status: file.status,
450
+ error: `manifest.json: ${file.error}`
451
+ };
452
+ const obj = parseJsonObject(file.text, "manifest.json");
453
+ if (!obj.ok) return {
454
+ ok: false,
455
+ status: STATUS_BAD_GATEWAY,
456
+ error: obj.error
457
+ };
458
+ const manifest = parseManifest(obj.value);
459
+ if (!manifest.ok) return {
460
+ ok: false,
461
+ status: STATUS_BAD_GATEWAY,
462
+ error: manifest.error
463
+ };
464
+ return {
465
+ ok: true,
466
+ files: manifest.files
467
+ };
468
+ }
469
+ /** Fetch every manifest file. Paths are already safety-checked by parseManifest. */
470
+ async function fetchBundle(entry, fileList) {
471
+ const rawBase = rawBaseForEntry(entry);
472
+ if (!rawBase) return {
473
+ ok: false,
474
+ status: STATUS_BAD_GATEWAY,
475
+ error: `registry "${entry.registryName}" is no longer configured`
476
+ };
477
+ const files = /* @__PURE__ */ new Map();
478
+ for (const rel of fileList) {
479
+ const file = await fetchCollectionFile(rawBase, entry.path, rel);
480
+ if (!file.ok) return {
481
+ ok: false,
482
+ status: file.status,
483
+ error: `${rel}: ${file.error}`
484
+ };
485
+ files.set(rel, file.text);
486
+ }
487
+ return {
488
+ ok: true,
489
+ files
490
+ };
491
+ }
492
+ //#endregion
493
+ //#region src/collection/registry/server/importWriter.ts
494
+ var ORIGIN_FILE = ".origin.json";
495
+ var SEED_PREFIX = "seed/items/";
496
+ var SCHEMA_FILE = "schema.json";
497
+ var SKILL_FILE = "SKILL.md";
498
+ var STATUS_NOT_FOUND$1 = 404;
499
+ var STATUS_CONFLICT = 409;
500
+ var STATUS_UNPROCESSABLE = 422;
501
+ async function statType(target) {
502
+ try {
503
+ return (await (0, node_fs_promises.stat)(target)).isDirectory() ? "dir" : "other";
504
+ } catch (err) {
505
+ if (require_types.isRecord(err) && err.code === "ENOENT") return "absent";
506
+ return "other";
507
+ }
508
+ }
509
+ async function isEmptyOrAbsentDir(target) {
510
+ try {
511
+ return (await (0, node_fs_promises.readdir)(target)).length === 0;
512
+ } catch {
513
+ return true;
514
+ }
515
+ }
516
+ function originMatches(origin, registry, author, slug) {
517
+ return require_types.isRecord(origin) && origin.registry === registry && origin.author === author && origin.slug === slug;
518
+ }
519
+ async function readOrigin(targetDir) {
520
+ try {
521
+ return JSON.parse(await (0, node_fs_promises.readFile)(node_path.default.join(targetDir, ORIGIN_FILE), "utf-8"));
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
526
+ var MAX_SLUG_ATTEMPTS = 1e4;
527
+ var renameCandidate = (slug, attempt) => attempt === 0 ? slug : `${slug}-${attempt + 1}`;
528
+ function isRenameOf(name, slug) {
529
+ if (name === slug) return true;
530
+ if (!name.startsWith(`${slug}-`)) return false;
531
+ const suffix = name.slice(slug.length + 1);
532
+ return suffix.length > 0 && /^\d+$/.test(suffix);
533
+ }
534
+ async function findMatchingInstall(skillsDir, registry, entry) {
535
+ const names = await (0, node_fs_promises.readdir)(skillsDir).catch(() => []);
536
+ for (const name of names) {
537
+ if (!isRenameOf(name, entry.slug)) continue;
538
+ const dir = node_path.default.join(skillsDir, name);
539
+ if (await statType(dir) === "dir" && originMatches(await readOrigin(dir), registry, entry.author, entry.slug)) return name;
540
+ }
541
+ return null;
542
+ }
543
+ async function resolveTarget(workspaceRoot, registry, entry) {
544
+ const existing = await findMatchingInstall(node_path.default.dirname(require_skill_bridge_index.dataSkillDir(workspaceRoot, entry.slug)), registry, entry);
545
+ if (existing) return {
546
+ targetDir: require_skill_bridge_index.dataSkillDir(workspaceRoot, existing),
547
+ localSlug: existing,
548
+ updated: true
549
+ };
550
+ for (let attempt = 0; attempt < MAX_SLUG_ATTEMPTS; attempt += 1) {
551
+ const localSlug = renameCandidate(entry.slug, attempt);
552
+ const sourceAbsent = await statType(require_skill_bridge_index.dataSkillDir(workspaceRoot, localSlug)) === "absent";
553
+ const mirrorAbsent = await statType(require_skill_bridge_index.claudeSkillDir(workspaceRoot, localSlug)) === "absent";
554
+ if (sourceAbsent && mirrorAbsent) return {
555
+ targetDir: require_skill_bridge_index.dataSkillDir(workspaceRoot, localSlug),
556
+ localSlug,
557
+ updated: false
558
+ };
559
+ }
560
+ return { conflict: `couldn't find an available slug for '${entry.slug}'` };
561
+ }
562
+ function validateAndNormalize(bundle, localSlug, workspaceRoot) {
563
+ if (bundle.get(SKILL_FILE) === void 0) return { error: "bundle is missing SKILL.md" };
564
+ const schemaText = bundle.get(SCHEMA_FILE);
565
+ if (schemaText === void 0) return { error: "bundle is missing schema.json" };
566
+ let parsedJson;
567
+ try {
568
+ parsedJson = JSON.parse(schemaText);
569
+ } catch {
570
+ return { error: "schema.json is not valid JSON" };
571
+ }
572
+ const parsed = require_server.CollectionSchemaZ.safeParse(parsedJson);
573
+ if (!parsed.success) return { error: `schema.json failed validation: ${parsed.error.issues[0]?.message ?? "invalid"}` };
574
+ const schema = {
575
+ ...parsed.data,
576
+ dataPath: normalizedDataPath(localSlug)
577
+ };
578
+ const acceptance = require_server.acceptParsedSchema(schema, {
579
+ source: "project",
580
+ workspaceRoot
581
+ });
582
+ if (!acceptance.ok) return { error: `schema.json rejected: ${acceptance.reason}` };
583
+ return { schema };
584
+ }
585
+ async function writeBundleFiles(targetDir, bundle, schema) {
586
+ for (const [rel, content] of bundle) {
587
+ if (rel.startsWith(SEED_PREFIX)) continue;
588
+ const dest = node_path.default.join(targetDir, ...rel.split("/"));
589
+ if (dest !== targetDir && !dest.startsWith(targetDir + node_path.default.sep)) continue;
590
+ await (0, node_fs_promises.mkdir)(node_path.default.dirname(dest), { recursive: true });
591
+ await (0, node_fs_promises.writeFile)(dest, rel === SCHEMA_FILE ? `${JSON.stringify(schema, null, 2)}\n` : content, "utf-8");
592
+ }
593
+ }
594
+ async function materializeSeed(dataDir, bundle) {
595
+ const seedEntries = [...bundle].filter(([rel]) => rel.startsWith(SEED_PREFIX));
596
+ if (seedEntries.length === 0) return {
597
+ written: 0,
598
+ skipped: false
599
+ };
600
+ if (!await isEmptyOrAbsentDir(dataDir)) return {
601
+ written: 0,
602
+ skipped: true
603
+ };
604
+ await (0, node_fs_promises.mkdir)(dataDir, { recursive: true });
605
+ let written = 0;
606
+ for (const [rel, content] of seedEntries) {
607
+ const fileName = rel.slice(11);
608
+ if (fileName.includes("/") || require_server.safeRecordId(fileName.replace(/\.json$/, "")) === null) {
609
+ require_server.log.warn("collections-registry", "skipped unsafe seed record", { rel });
610
+ continue;
611
+ }
612
+ await (0, node_fs_promises.writeFile)(node_path.default.join(dataDir, fileName), content, "utf-8");
613
+ written += 1;
614
+ }
615
+ return {
616
+ written,
617
+ skipped: false
618
+ };
619
+ }
620
+ /** Compute the set of bundle-relative paths that should land in `.claude/skills/<slug>/`.
621
+ * Matches the bridge allowlist exactly — SKILL.md, schema.json,
622
+ * `templates/<safe>` — so anything else (seed, meta.json, views, README,
623
+ * assets) stays source-side. */
624
+ function bridgeAllowlistFiles(bundle) {
625
+ const wanted = /* @__PURE__ */ new Set([SKILL_FILE, SCHEMA_FILE]);
626
+ for (const rel of bundle.keys()) {
627
+ if (rel === SKILL_FILE || rel === SCHEMA_FILE) continue;
628
+ if (rel.startsWith(SEED_PREFIX)) continue;
629
+ if (!require_collection_paths.isSafeActionTemplatePath(rel)) continue;
630
+ wanted.add(rel);
631
+ }
632
+ return wanted;
633
+ }
634
+ /** Recursively list every file under `root`, returned as forward-slash paths
635
+ * relative to `root`. Empty list when `root` doesn't exist. Used to spot
636
+ * mirror files left over from a previous install so we can prune them after
637
+ * writing the new ones. */
638
+ async function listFilesRecursive(root, prefix = "") {
639
+ const entries = await (0, node_fs_promises.readdir)(prefix ? node_path.default.join(root, ...prefix.split("/")) : root, { withFileTypes: true }).catch(() => []);
640
+ const out = [];
641
+ for (const entry of entries) {
642
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
643
+ if (entry.isFile()) out.push(rel);
644
+ else if (entry.isDirectory()) out.push(...await listFilesRecursive(root, rel));
645
+ }
646
+ return out;
647
+ }
648
+ async function mirrorToClaudeSkills(workspaceRoot, localSlug, bundle) {
649
+ const wanted = bridgeAllowlistFiles(bundle);
650
+ for (const rel of wanted) require_skill_bridge_index.mirrorSkillWrite(workspaceRoot, {
651
+ slug: localSlug,
652
+ relSegments: rel.split("/")
653
+ });
654
+ const mirrorRoot = require_skill_bridge_index.claudeSkillDir(workspaceRoot, localSlug);
655
+ const existing = await listFilesRecursive(mirrorRoot);
656
+ for (const rel of existing) {
657
+ if (wanted.has(rel)) continue;
658
+ await (0, node_fs_promises.rm)(node_path.default.join(mirrorRoot, ...rel.split("/")), { force: true }).catch(() => void 0);
659
+ }
660
+ }
661
+ async function writeImportedCollection(params) {
662
+ const { registry, entry, bundle, workspaceRoot, nowIso } = params;
663
+ const target = await resolveTarget(workspaceRoot, registry, entry);
664
+ if ("conflict" in target) return {
665
+ ok: false,
666
+ status: STATUS_CONFLICT,
667
+ error: target.conflict
668
+ };
669
+ const { localSlug } = target;
670
+ const dataDir = node_path.default.join(workspaceRoot, ...normalizedDataPath(localSlug).split("/"));
671
+ if (await statType(dataDir) === "other") return {
672
+ ok: false,
673
+ status: STATUS_CONFLICT,
674
+ error: `data path for slug '${localSlug}' exists and is not a directory`
675
+ };
676
+ const validated = validateAndNormalize(bundle, localSlug, workspaceRoot);
677
+ if ("error" in validated) return {
678
+ ok: false,
679
+ status: STATUS_UNPROCESSABLE,
680
+ error: validated.error
681
+ };
682
+ const skillsParent = node_path.default.dirname(target.targetDir);
683
+ await (0, node_fs_promises.mkdir)(skillsParent, { recursive: true });
684
+ const staging = node_path.default.join(skillsParent, `.importing-${localSlug}`);
685
+ const backup = node_path.default.join(skillsParent, `.backup-${localSlug}`);
686
+ await (0, node_fs_promises.rm)(staging, {
687
+ recursive: true,
688
+ force: true
689
+ });
690
+ await (0, node_fs_promises.rm)(backup, {
691
+ recursive: true,
692
+ force: true
693
+ });
694
+ await (0, node_fs_promises.mkdir)(staging, { recursive: true });
695
+ await writeBundleFiles(staging, bundle, validated.schema);
696
+ const origin = {
697
+ registry,
698
+ author: entry.author,
699
+ slug: entry.slug,
700
+ version: entry.version,
701
+ contentSha: entry.contentSha,
702
+ importedAt: nowIso
703
+ };
704
+ await require_server.writeFileAtomic(node_path.default.join(staging, ORIGIN_FILE), `${JSON.stringify(origin, null, 2)}\n`);
705
+ if (target.updated) await (0, node_fs_promises.rename)(target.targetDir, backup);
706
+ try {
707
+ await (0, node_fs_promises.rename)(staging, target.targetDir);
708
+ } catch (err) {
709
+ if (target.updated) await (0, node_fs_promises.rename)(backup, target.targetDir).catch(() => void 0);
710
+ throw err;
711
+ }
712
+ await (0, node_fs_promises.rm)(backup, {
713
+ recursive: true,
714
+ force: true
715
+ });
716
+ try {
717
+ await mirrorToClaudeSkills(workspaceRoot, localSlug, bundle);
718
+ } catch (err) {
719
+ require_server.log.warn("collections-registry", "mirror to .claude/skills/ failed (data/skills write succeeded; prior mirror left intact)", {
720
+ localSlug,
721
+ error: require_server.errorMessage(err)
722
+ });
723
+ }
724
+ const seed = await materializeSeed(dataDir, bundle);
725
+ return {
726
+ ok: true,
727
+ localSlug,
728
+ updated: target.updated,
729
+ seedWritten: seed.written,
730
+ seedSkipped: seed.skipped
731
+ };
732
+ }
733
+ async function performImport(author, slug, workspaceRoot, registry = null) {
734
+ const merged = await fetchAllRegistries();
735
+ let entry;
736
+ for (const reg of merged) {
737
+ if (registry !== null && reg.name !== registry) continue;
738
+ entry = reg.entries.find((candidate) => candidate.author === author && candidate.slug === slug);
739
+ if (entry) break;
740
+ }
741
+ if (!entry) return {
742
+ ok: false,
743
+ status: STATUS_NOT_FOUND$1,
744
+ error: `unknown collection: ${author}/${slug}`
745
+ };
746
+ const manifest = await fetchManifest(entry);
747
+ if (!manifest.ok) return {
748
+ ok: false,
749
+ status: manifest.status,
750
+ error: manifest.error
751
+ };
752
+ const bundle = await fetchBundle(entry, manifest.files);
753
+ if (!bundle.ok) return {
754
+ ok: false,
755
+ status: bundle.status,
756
+ error: bundle.error
757
+ };
758
+ try {
759
+ return await writeImportedCollection({
760
+ registry: entry.registryName,
761
+ entry,
762
+ bundle: bundle.files,
763
+ workspaceRoot,
764
+ nowIso: (/* @__PURE__ */ new Date()).toISOString()
765
+ });
766
+ } catch (err) {
767
+ require_server.log.warn("collections-registry", "import write failed", {
768
+ author,
769
+ slug,
770
+ registry: entry.registryName,
771
+ error: require_server.errorMessage(err)
772
+ });
773
+ return {
774
+ ok: false,
775
+ status: 500,
776
+ error: `import failed: ${require_server.errorMessage(err)}`
777
+ };
778
+ }
779
+ }
780
+ //#endregion
781
+ //#region src/collection/registry/server/exportCollection.ts
782
+ var EXPORT_BASE = "data/registry-export";
783
+ var BUNDLE_FILES = ["SKILL.md", "schema.json"];
784
+ var OPTIONAL_FILES = ["screenshot.png"];
785
+ var BUNDLE_DIRS = ["views", "templates"];
786
+ var AUTHOR_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/;
787
+ var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
788
+ var STATUS_BAD_REQUEST = 400;
789
+ var SECRET_PATTERNS = [
790
+ /AKIA[0-9A-Z]{16}/,
791
+ /gh[pousr]_[A-Za-z0-9]{20,}/,
792
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
793
+ /sk-[A-Za-z0-9]{20,}/,
794
+ /xox[baprs]-[A-Za-z0-9-]{10,}/
795
+ ];
796
+ var hasSecret = (text) => SECRET_PATTERNS.some((pattern) => pattern.test(text));
797
+ var EMAIL_RE = /[\w.%+-]{1,64}@[\w-]{1,255}\.[a-z]{2,24}/i;
798
+ async function pathKind(target) {
799
+ try {
800
+ return (await (0, node_fs_promises.stat)(target)).isDirectory() ? "dir" : "file";
801
+ } catch {
802
+ return "absent";
803
+ }
804
+ }
805
+ async function copyBundle(skillDir, outDir) {
806
+ let count = 0;
807
+ for (const file of [...BUNDLE_FILES, ...OPTIONAL_FILES]) {
808
+ const src = node_path.default.join(skillDir, file);
809
+ if (await pathKind(src) !== "file") continue;
810
+ await (0, node_fs_promises.cp)(src, node_path.default.join(outDir, file));
811
+ count += 1;
812
+ }
813
+ for (const dir of BUNDLE_DIRS) {
814
+ const src = node_path.default.join(skillDir, dir);
815
+ if (await pathKind(src) !== "dir") continue;
816
+ await (0, node_fs_promises.cp)(src, node_path.default.join(outDir, dir), { recursive: true });
817
+ count += (await (0, node_fs_promises.readdir)(src)).filter((name) => !name.startsWith(".")).length;
818
+ }
819
+ return count;
820
+ }
821
+ async function exportSeed(dataDir, outDir) {
822
+ let names;
823
+ try {
824
+ names = (await (0, node_fs_promises.readdir)(dataDir)).filter((name) => name.endsWith(".json"));
825
+ } catch {
826
+ return {
827
+ count: 0,
828
+ skipped: 0,
829
+ warnings: []
830
+ };
831
+ }
832
+ if (names.length === 0) return {
833
+ count: 0,
834
+ skipped: 0,
835
+ warnings: []
836
+ };
837
+ const seedDir = node_path.default.join(outDir, "seed", "items");
838
+ await (0, node_fs_promises.mkdir)(seedDir, { recursive: true });
839
+ const warnings = [];
840
+ let count = 0;
841
+ let skipped = 0;
842
+ for (const name of names) {
843
+ const base = node_path.default.basename(name);
844
+ const text = await (0, node_fs_promises.readFile)(node_path.default.join(dataDir, base), "utf-8");
845
+ if (hasSecret(text)) {
846
+ warnings.push(`skipped ${base}: contains a possible credential`);
847
+ skipped += 1;
848
+ continue;
849
+ }
850
+ if (EMAIL_RE.test(text)) warnings.push(`${base}: contains a possible email/PII (kept — your responsibility)`);
851
+ await (0, node_fs_promises.writeFile)(node_path.default.join(seedDir, base), text, "utf-8");
852
+ count += 1;
853
+ }
854
+ return {
855
+ count,
856
+ skipped,
857
+ warnings
858
+ };
859
+ }
860
+ function resolveWithin(root, ...segments) {
861
+ const resolvedRoot = node_path.default.resolve(root);
862
+ const target = node_path.default.resolve(resolvedRoot, ...segments);
863
+ return target === resolvedRoot || target.startsWith(resolvedRoot + node_path.default.sep) ? target : null;
864
+ }
865
+ async function findMissingRequired(skillDir) {
866
+ for (const file of BUNDLE_FILES) if (await pathKind(node_path.default.join(skillDir, file)) !== "file") return file;
867
+ return null;
868
+ }
869
+ async function writeCollectionExport(params) {
870
+ const { workspaceRoot, skillDir, dataDir, meta, includeSeed } = params;
871
+ if (!AUTHOR_RE.test(meta.author)) return {
872
+ ok: false,
873
+ status: STATUS_BAD_REQUEST,
874
+ error: `author '${meta.author}' is not a valid GitHub login`
875
+ };
876
+ if (!SLUG_RE.test(meta.slug)) return {
877
+ ok: false,
878
+ status: STATUS_BAD_REQUEST,
879
+ error: `slug '${meta.slug}' is invalid`
880
+ };
881
+ const wsRoot = node_path.default.resolve(workspaceRoot);
882
+ const outDir = resolveWithin(node_path.default.join(wsRoot, ...EXPORT_BASE.split("/")), meta.author, meta.slug);
883
+ const safeSkillDir = resolveWithin(wsRoot, skillDir);
884
+ const safeDataDir = resolveWithin(wsRoot, dataDir);
885
+ if (!outDir || !safeSkillDir || !safeDataDir) return {
886
+ ok: false,
887
+ status: STATUS_BAD_REQUEST,
888
+ error: "resolved path escapes the workspace"
889
+ };
890
+ const missing = await findMissingRequired(safeSkillDir);
891
+ if (missing) return {
892
+ ok: false,
893
+ status: STATUS_BAD_REQUEST,
894
+ error: `required bundle file missing: ${missing}`
895
+ };
896
+ await (0, node_fs_promises.rm)(outDir, {
897
+ recursive: true,
898
+ force: true
899
+ });
900
+ await (0, node_fs_promises.mkdir)(outDir, { recursive: true });
901
+ const bundleCount = await copyBundle(safeSkillDir, outDir);
902
+ const seed = includeSeed ? await exportSeed(safeDataDir, outDir) : {
903
+ count: 0,
904
+ skipped: 0,
905
+ warnings: []
906
+ };
907
+ const metaOut = {
908
+ ...meta,
909
+ ...seed.count > 0 ? { dataConsent: true } : {}
910
+ };
911
+ await (0, node_fs_promises.writeFile)(node_path.default.join(outDir, "meta.json"), `${JSON.stringify(metaOut, null, 2)}\n`, "utf-8");
912
+ require_server.log.info("collections-registry", "exported collection", {
913
+ slug: meta.slug,
914
+ author: meta.author,
915
+ seedCount: seed.count
916
+ });
917
+ return {
918
+ ok: true,
919
+ outputPath: node_path.default.relative(wsRoot, outDir).split(node_path.default.sep).join("/"),
920
+ fileCount: bundleCount + 1,
921
+ seedCount: seed.count,
922
+ seedSkipped: seed.skipped,
923
+ warnings: seed.warnings
924
+ };
925
+ }
926
+ //#endregion
927
+ //#region src/collection/registry/server/skillDescription.ts
928
+ var FENCE = "---";
929
+ var KEY = "description:";
930
+ var BLOCK_SCALAR_INDICATORS = /* @__PURE__ */ new Set([
931
+ "|",
932
+ ">",
933
+ "|-",
934
+ ">-",
935
+ "|+",
936
+ ">+"
937
+ ]);
938
+ var isYamlSpace = (char) => char === " " || char === " ";
939
+ var isOnlyTrailingComment = (rest) => {
940
+ const trimmed = rest.trimStart();
941
+ return trimmed === "" || trimmed.startsWith("#");
942
+ };
943
+ var DOUBLE_QUOTE_ESCAPES = {
944
+ n: "\n",
945
+ t: " "
946
+ };
947
+ var unescapeDoubleQuote = (next) => DOUBLE_QUOTE_ESCAPES[next] ?? next;
948
+ function parseDoubleQuoted(value) {
949
+ const out = [];
950
+ for (let i = 1; i < value.length; i += 1) {
951
+ const char = value[i];
952
+ if (char === "\\" && i + 1 < value.length) {
953
+ out.push(unescapeDoubleQuote(value[i + 1]));
954
+ i += 1;
955
+ continue;
956
+ }
957
+ if (char === "\"") return isOnlyTrailingComment(value.slice(i + 1)) ? out.join("") : null;
958
+ out.push(char);
959
+ }
960
+ return null;
961
+ }
962
+ function parseSingleQuoted(value) {
963
+ const out = [];
964
+ for (let i = 1; i < value.length; i += 1) {
965
+ const char = value[i];
966
+ if (char === "'") {
967
+ if (value[i + 1] === "'") {
968
+ out.push("'");
969
+ i += 1;
970
+ continue;
971
+ }
972
+ return isOnlyTrailingComment(value.slice(i + 1)) ? out.join("") : null;
973
+ }
974
+ out.push(char);
975
+ }
976
+ return null;
977
+ }
978
+ function stripPlainComment(value) {
979
+ for (let i = 0; i < value.length; i += 1) if (value[i] === "#" && (i === 0 || isYamlSpace(value[i - 1]))) return value.slice(0, i);
980
+ return value;
981
+ }
982
+ /** Extract the frontmatter `description` from raw SKILL.md text. Returns "" when
983
+ * there's no `---` envelope, no `description:` key, or the value is a block
984
+ * scalar indicator. */
985
+ function parseSkillDescription(raw) {
986
+ const lines = raw.split(/\r?\n/);
987
+ if (lines[0]?.trim() !== FENCE) return "";
988
+ for (let i = 1; i < lines.length; i += 1) {
989
+ const line = lines[i];
990
+ if (line.trim() === FENCE) return "";
991
+ if (!line.startsWith(KEY)) continue;
992
+ const value = line.slice(12).trim();
993
+ if (value === "" || BLOCK_SCALAR_INDICATORS.has(value)) return "";
994
+ if (value.startsWith("\"")) return parseDoubleQuoted(value) ?? "";
995
+ if (value.startsWith("'")) return parseSingleQuoted(value) ?? "";
996
+ return stripPlainComment(value).trim();
997
+ }
998
+ return "";
999
+ }
1000
+ //#endregion
1001
+ //#region src/collection/registry/server/performExport.ts
1002
+ var STATUS_NOT_FOUND = 404;
1003
+ async function readJsonObject(file) {
1004
+ try {
1005
+ const parsed = JSON.parse(await (0, node_fs_promises.readFile)(file, "utf-8"));
1006
+ return require_types.isRecord(parsed) ? parsed : null;
1007
+ } catch {
1008
+ return null;
1009
+ }
1010
+ }
1011
+ async function performExport(slug, opts, workspaceRoot) {
1012
+ const collection = await require_server.loadCollection(slug);
1013
+ if (!collection) return {
1014
+ ok: false,
1015
+ status: STATUS_NOT_FOUND,
1016
+ error: `collection not found: ${slug}`
1017
+ };
1018
+ const skillMd = await (0, node_fs_promises.readFile)(node_path.default.join(collection.skillDir, "SKILL.md"), "utf-8").catch(() => "");
1019
+ const existingMeta = await readJsonObject(node_path.default.join(collection.skillDir, "meta.json"));
1020
+ const meta = {
1021
+ author: opts.author,
1022
+ slug,
1023
+ version: typeof existingMeta?.version === "string" ? existingMeta.version : "1.0.0",
1024
+ title: collection.schema.title,
1025
+ description: parseSkillDescription(skillMd),
1026
+ tags: [],
1027
+ license: opts.license
1028
+ };
1029
+ return writeCollectionExport({
1030
+ workspaceRoot,
1031
+ skillDir: collection.skillDir,
1032
+ dataDir: collection.dataDir,
1033
+ meta,
1034
+ includeSeed: opts.includeSeed
1035
+ });
1036
+ }
1037
+ //#endregion
1038
+ //#region src/collection/registry/server/index.ts
1039
+ /** Build the merged Discover catalog response (`GET …collectionsRegistry.list`).
1040
+ * Wraps `fetchAllRegistries` with the per-registry summary + stale-flag shaping
1041
+ * every host needs, so a downstream host doesn't re-implement the mapping. */
1042
+ async function listRegistry() {
1043
+ const merged = await fetchAllRegistries();
1044
+ return {
1045
+ registries: merged.map((reg) => ({
1046
+ name: reg.name,
1047
+ status: reg.status,
1048
+ generatedAt: reg.generatedAt,
1049
+ error: reg.error,
1050
+ entryCount: reg.entries.length
1051
+ })),
1052
+ stale: merged.some((reg) => reg.status === "stale"),
1053
+ collections: merged.flatMap((reg) => reg.entries)
1054
+ };
1055
+ }
1056
+ /** Import a registry collection and return the host-facing response shape
1057
+ * (`POST …collectionsRegistry.import`). On failure carries the HTTP status so
1058
+ * the host route can pass it straight through. */
1059
+ async function importRegistry(author, slug, workspaceRoot, registry = null) {
1060
+ const result = await performImport(author, slug, workspaceRoot, registry);
1061
+ if (!result.ok) return {
1062
+ ok: false,
1063
+ status: result.status,
1064
+ error: result.error
1065
+ };
1066
+ return {
1067
+ ok: true,
1068
+ response: {
1069
+ localSlug: result.localSlug,
1070
+ updated: result.updated,
1071
+ seedWritten: result.seedWritten,
1072
+ seedSkipped: result.seedSkipped
1073
+ }
1074
+ };
1075
+ }
1076
+ //#endregion
1077
+ exports.CACHE_TTL_MS = CACHE_TTL_MS;
1078
+ exports.EXPORT_BASE = EXPORT_BASE;
1079
+ exports.OFFICIAL_REGISTRY_NAME = require_types.OFFICIAL_REGISTRY_NAME;
1080
+ exports.STALE_RETRY_BACKOFF_MS = STALE_RETRY_BACKOFF_MS;
1081
+ exports.claudeSkillDir = require_skill_bridge_index.claudeSkillDir;
1082
+ exports.collectionFileUrl = collectionFileUrl;
1083
+ exports.dataSkillDir = require_skill_bridge_index.dataSkillDir;
1084
+ exports.fetchAllRegistries = fetchAllRegistries;
1085
+ exports.fetchBundle = fetchBundle;
1086
+ exports.fetchCollectionFile = fetchCollectionFile;
1087
+ exports.fetchManifest = fetchManifest;
1088
+ exports.fetchRegistryIndex = fetchRegistryIndex;
1089
+ exports.findRegistry = findRegistry;
1090
+ exports.importRegistry = importRegistry;
1091
+ exports.isSafeBundlePath = isSafeBundlePath;
1092
+ exports.listRegistries = listRegistries;
1093
+ exports.listRegistry = listRegistry;
1094
+ exports.loadRegistriesConfig = loadRegistriesConfig;
1095
+ exports.normalizedDataPath = normalizedDataPath;
1096
+ exports.parseJsonObject = parseJsonObject;
1097
+ exports.parseManifest = parseManifest;
1098
+ exports.parseRegistriesConfig = parseRegistriesConfig;
1099
+ exports.parseRegistryIndex = require_types.parseRegistryIndex;
1100
+ exports.parseSkillDescription = parseSkillDescription;
1101
+ exports.performExport = performExport;
1102
+ exports.performImport = performImport;
1103
+ exports.previewCollection = previewCollection;
1104
+ exports.rawBaseForEntry = rawBaseForEntry;
1105
+ exports.resetRegistryCache = resetRegistryCache;
1106
+ exports.withNormalizedDataPath = withNormalizedDataPath;
1107
+ exports.writeCollectionExport = writeCollectionExport;
1108
+ exports.writeImportedCollection = writeImportedCollection;
1109
+
1110
+ //# sourceMappingURL=index.cjs.map