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