@mulmoclaude/core 3.0.1 → 3.2.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 (73) hide show
  1. package/assets/helps/collection-skills.md +72 -8
  2. package/assets/helps/custom-view.md +9 -1
  3. package/assets/helps/error-recovery.md +36 -0
  4. package/assets/helps/feeds.md +6 -4
  5. package/dist/calendarGrid-Csy2rpjp.js.map +1 -1
  6. package/dist/calendarGrid-p8K-7cuB.cjs.map +1 -1
  7. package/dist/collection/core/presentCollection.d.ts +49 -0
  8. package/dist/collection/core/schema.d.ts +16 -1
  9. package/dist/collection/index.cjs +56 -7
  10. package/dist/collection/index.cjs.map +1 -1
  11. package/dist/collection/index.js +53 -8
  12. package/dist/collection/index.js.map +1 -1
  13. package/dist/collection/registry/server/importWriter.d.ts +2 -1
  14. package/dist/collection/registry/server/index.cjs +31 -10
  15. package/dist/collection/registry/server/index.cjs.map +1 -1
  16. package/dist/collection/registry/server/index.js +30 -9
  17. package/dist/collection/registry/server/index.js.map +1 -1
  18. package/dist/collection/server/host.d.ts +34 -4
  19. package/dist/collection/server/index.cjs +4 -2
  20. package/dist/collection/server/index.d.ts +1 -1
  21. package/dist/collection/server/index.js +4 -3
  22. package/dist/collection/server/manageTool.d.ts +19 -0
  23. package/dist/collection/server/schemaDocs.d.ts +5 -1
  24. package/dist/collection-watchers/config.d.ts +15 -2
  25. package/dist/collection-watchers/index.cjs +310 -198
  26. package/dist/collection-watchers/index.cjs.map +1 -1
  27. package/dist/collection-watchers/index.js +310 -198
  28. package/dist/collection-watchers/index.js.map +1 -1
  29. package/dist/collection-watchers/reconciler.d.ts +1 -5
  30. package/dist/collection-watchers/watcher.d.ts +31 -26
  31. package/dist/{discovery-4Z-ZWFam.js → discovery-CC7kuwHf.js} +21 -7
  32. package/dist/discovery-CC7kuwHf.js.map +1 -0
  33. package/dist/{discovery-CtiZMlsX.cjs → discovery-CDOjfctD.cjs} +33 -7
  34. package/dist/discovery-CDOjfctD.cjs.map +1 -0
  35. package/dist/feeds/server/host.d.ts +12 -0
  36. package/dist/feeds/server/index.cjs +27 -6
  37. package/dist/feeds/server/index.cjs.map +1 -1
  38. package/dist/feeds/server/index.d.ts +1 -1
  39. package/dist/feeds/server/index.js +27 -7
  40. package/dist/feeds/server/index.js.map +1 -1
  41. package/dist/feeds/server/scheduledRefresh.d.ts +16 -1
  42. package/dist/files/index.cjs +6 -5
  43. package/dist/files/index.cjs.map +1 -1
  44. package/dist/files/index.d.ts +1 -0
  45. package/dist/files/index.js +2 -2
  46. package/dist/files/root.d.ts +15 -0
  47. package/dist/google/index.cjs +3 -3
  48. package/dist/google/index.cjs.map +1 -1
  49. package/dist/google/index.js +2 -2
  50. package/dist/remote-host/index.cjs +3 -1
  51. package/dist/remote-host/index.d.ts +30 -0
  52. package/dist/remote-host/index.js +2 -2
  53. package/dist/remote-host/server/index.cjs +1 -1
  54. package/dist/remote-host/server/index.js +1 -1
  55. package/dist/{remote-host-Dba4lF3Z.js → remote-host-CFnl1I-L.js} +35 -2
  56. package/dist/{remote-host-Dba4lF3Z.js.map → remote-host-CFnl1I-L.js.map} +1 -1
  57. package/dist/{remote-host-CtQjagPt.cjs → remote-host-EQPlccB9.cjs} +46 -1
  58. package/dist/{remote-host-CtQjagPt.cjs.map → remote-host-EQPlccB9.cjs.map} +1 -1
  59. package/dist/{atomic-DPpdrJzO.js → root-BMroU_mB.js} +21 -2
  60. package/dist/root-BMroU_mB.js.map +1 -0
  61. package/dist/{atomic-DhUk8uiM.cjs → root-rPH6FGDT.cjs} +26 -1
  62. package/dist/root-rPH6FGDT.cjs.map +1 -0
  63. package/dist/{server-CedIocRS.js → server-AA2kfDfk.js} +137 -42
  64. package/dist/server-AA2kfDfk.js.map +1 -0
  65. package/dist/{server-DOZFzx5l.cjs → server-WU35UYlF.cjs} +138 -43
  66. package/dist/server-WU35UYlF.cjs.map +1 -0
  67. package/package.json +1 -1
  68. package/dist/atomic-DPpdrJzO.js.map +0 -1
  69. package/dist/atomic-DhUk8uiM.cjs.map +0 -1
  70. package/dist/discovery-4Z-ZWFam.js.map +0 -1
  71. package/dist/discovery-CtiZMlsX.cjs.map +0 -1
  72. package/dist/server-CedIocRS.js.map +0 -1
  73. package/dist/server-DOZFzx5l.cjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
2
2
  const require_dist = require("./dist-CsgSfWwR.cjs");
3
- const require_atomic = require("./atomic-DhUk8uiM.cjs");
3
+ const require_root = require("./root-rPH6FGDT.cjs");
4
4
  const require_relPath = require("./relPath-CTAjGdCL.cjs");
5
5
  const require_calendarGrid = require("./calendarGrid-p8K-7cuB.cjs");
6
6
  const require_promptSafety = require("./promptSafety-CNEW3_qS.cjs");
7
- const require_discovery = require("./discovery-CtiZMlsX.cjs");
7
+ const require_discovery = require("./discovery-CDOjfctD.cjs");
8
8
  const require_feeds_paths = require("./feeds/paths.cjs");
9
9
  const require_skill_bridge_index = require("./skill-bridge/index.cjs");
10
10
  let node_path = require("node:path");
@@ -39,7 +39,8 @@ async function readSourceAwareFile(collection, relPath, opts) {
39
39
  const safeSlug = require_discovery.safeSlugName(collection.slug);
40
40
  if (safeSlug === null) return null;
41
41
  const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
42
- const bases = collection.source === "project" ? [node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), safeSlug), collection.skillDir] : [collection.skillDir];
42
+ const staging = collection.source === "project" ? require_discovery.stagingSkillDir(workspaceRoot, safeSlug) : null;
43
+ const bases = staging === null ? [collection.skillDir] : [staging, collection.skillDir];
43
44
  for (const base of bases) {
44
45
  const resolved = require_discovery.resolveTemplatePath(base, relPath);
45
46
  if (resolved === null) continue;
@@ -1026,10 +1027,6 @@ function deleteCollectionRefusalMessage(result) {
1026
1027
  "path-escape": `a directory for collection '${slug}' escapes the workspace`
1027
1028
  }[result.kind];
1028
1029
  }
1029
- /** The canonical staging dir for a slug: `data/skills/<slug>`. */
1030
- function stagingSkillDir(workspaceRoot, slug) {
1031
- return node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), slug);
1032
- }
1033
1030
  async function pathExists(target) {
1034
1031
  try {
1035
1032
  await (0, node_fs_promises.stat)(target);
@@ -1045,8 +1042,9 @@ function todayStamp() {
1045
1042
  /** Every directory the delete will touch must resolve under the
1046
1043
  * workspace root — guards against a symlinked ancestor escaping it. */
1047
1044
  function deleteTargets(collection, workspaceRoot) {
1045
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, collection.slug);
1048
1046
  return [
1049
- stagingSkillDir(workspaceRoot, collection.slug),
1047
+ ...staging === null ? [] : [staging],
1050
1048
  collection.skillDir,
1051
1049
  collection.dataDir,
1052
1050
  require_feeds_paths.ingestStatePath(collection.slug, workspaceRoot),
@@ -1097,14 +1095,22 @@ function restoreRecordsStep(schema) {
1097
1095
  files, not to these records (there may be many; copy them, do not
1098
1096
  Write them one by one).`;
1099
1097
  }
1100
- function buildRestoreDoc(collection) {
1101
- const { slug, schema } = collection;
1102
- return `# Restore "${schema.title}" (collection \`${slug}\`)
1103
-
1104
- This folder is an automatic backup made when the collection was deleted.
1105
- Follow these steps to restore it.
1098
+ /** Step 1 of the restore doc, which depends on the root's authoring layout.
1099
+ *
1100
+ * A staged root routes around the `.claude/` permission gate and relies on the
1101
+ * bridge hook; a root with no staging tree has neither, so the same
1102
+ * instructions would have the user restore into `data/skills/<slug>/`, where
1103
+ * nothing would ever read it — the collection would stay deleted while the
1104
+ * document said it was restored. */
1105
+ function restoreSkillStep(slug, staged) {
1106
+ if (!staged) return `1. Recreate the skill files in \`.claude/skills/${slug}/\`: read each
1107
+ file under \`skill/\` and write it to the matching path — \`SKILL.md\`,
1108
+ \`schema.json\`, any \`templates/*\`, and any \`views/*\`.
1106
1109
 
1107
- 1. Recreate the skill files in \`data/skills/${slug}/\` using the **Write
1110
+ This project has no \`data/skills/\` staging tree and no bridge hook, so
1111
+ the skill directory is read directly and no mirroring step follows. Do
1112
+ NOT restore into \`data/skills/${slug}/\` — nothing reads it there.`;
1113
+ return `1. Recreate the skill files in \`data/skills/${slug}/\` using the **Write
1108
1114
  tool**: read each file under \`skill/\` and Write it to the matching
1109
1115
  path — \`SKILL.md\`, \`schema.json\`, and any \`templates/*\`.
1110
1116
 
@@ -1115,7 +1121,16 @@ Follow these steps to restore it.
1115
1121
  \`cp\` would leave the files in staging with no \`.claude/skills/\`
1116
1122
  mirror — the collection would stay invisible. (Writing
1117
1123
  \`.claude/skills/\` directly is not an option either: that path is
1118
- permission-gated.)
1124
+ permission-gated.)`;
1125
+ }
1126
+ function buildRestoreDoc(collection, staged) {
1127
+ const { slug, schema } = collection;
1128
+ return `# Restore "${schema.title}" (collection \`${slug}\`)
1129
+
1130
+ This folder is an automatic backup made when the collection was deleted.
1131
+ Follow these steps to restore it.
1132
+
1133
+ ${restoreSkillStep(slug, staged)}
1119
1134
 
1120
1135
  ${restoreRecordsStep(schema)}
1121
1136
 
@@ -1128,8 +1143,8 @@ ${restoreRecordsStep(schema)}
1128
1143
  }
1129
1144
  /** Copy one skill copy + the records + RESTORE.md into `archiveDir`. */
1130
1145
  async function writeArchive(collection, archiveDir, workspaceRoot) {
1131
- const staging = stagingSkillDir(workspaceRoot, collection.slug);
1132
- const skillSrc = await pathExists(staging) ? staging : collection.skillDir;
1146
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, collection.slug);
1147
+ const skillSrc = staging !== null && await pathExists(staging) ? staging : collection.skillDir;
1133
1148
  await (0, node_fs_promises.cp)(skillSrc, node_path.default.join(archiveDir, "skill"), { recursive: true });
1134
1149
  if (await pathExists(collection.dataDir)) await (0, node_fs_promises.cp)(collection.dataDir, node_path.default.join(archiveDir, "records"), { recursive: true });
1135
1150
  if (collection.storageFile !== void 0 && await pathExists(collection.storageFile)) {
@@ -1144,13 +1159,14 @@ async function writeArchive(collection, archiveDir, workspaceRoot) {
1144
1159
  if (await pathExists(sidecar)) await (0, node_fs_promises.cp)(sidecar, node_path.default.join(archiveDir, node_path.default.basename(sidecar)));
1145
1160
  }
1146
1161
  }
1147
- await (0, node_fs_promises.writeFile)(node_path.default.join(archiveDir, "RESTORE.md"), buildRestoreDoc(collection), "utf-8");
1162
+ await (0, node_fs_promises.writeFile)(node_path.default.join(archiveDir, "RESTORE.md"), buildRestoreDoc(collection, staging !== null), "utf-8");
1148
1163
  }
1149
1164
  /** Remove all three locations. `rm -rf`-style (force) so a missing dir
1150
1165
  * is a no-op; the now-empty data parent (`data/<slug>/` after its
1151
1166
  * `items/` is gone) is swept too, but only when empty. */
1152
1167
  async function removeLocations(collection, workspaceRoot) {
1153
- await (0, node_fs_promises.rm)(stagingSkillDir(workspaceRoot, collection.slug), {
1168
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, collection.slug);
1169
+ if (staging !== null) await (0, node_fs_promises.rm)(staging, {
1154
1170
  recursive: true,
1155
1171
  force: true
1156
1172
  });
@@ -1234,8 +1250,8 @@ async function fileExists(target) {
1234
1250
  * deletes agree on both layouts. */
1235
1251
  async function canonicalBase(collection, workspaceRoot, safeSlug) {
1236
1252
  if (collection.source !== "project") return collection.skillDir;
1237
- const staging = node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), safeSlug);
1238
- if (await fileExists(node_path.default.join(staging, "schema.json"))) return staging;
1253
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, safeSlug);
1254
+ if (staging !== null && await fileExists(node_path.default.join(staging, "schema.json"))) return staging;
1239
1255
  return collection.skillDir;
1240
1256
  }
1241
1257
  /** Every on-disk schema.json that must reflect the removal. The active
@@ -1246,9 +1262,9 @@ async function canonicalBase(collection, workspaceRoot, safeSlug) {
1246
1262
  async function schemaWriteTargets(collection, workspaceRoot, safeSlug) {
1247
1263
  const active = node_path.default.join(collection.skillDir, require_discovery.SCHEMA_FILE);
1248
1264
  if (collection.source !== "project") return [active];
1249
- const stagingSchema = node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), safeSlug, require_discovery.SCHEMA_FILE);
1265
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, safeSlug);
1250
1266
  const targets = [];
1251
- if (await fileExists(stagingSchema)) targets.push(stagingSchema);
1267
+ if (staging !== null && await fileExists(node_path.default.join(staging, "schema.json"))) targets.push(node_path.default.join(staging, require_discovery.SCHEMA_FILE));
1252
1268
  targets.push(active);
1253
1269
  return targets;
1254
1270
  }
@@ -1274,7 +1290,7 @@ async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug
1274
1290
  views: parsed.views.filter((entry) => !require_dist.isRecord(entry) || entry.id !== viewId)
1275
1291
  } : parsed;
1276
1292
  const serialized = `${JSON.stringify(next, null, 2)}\n`;
1277
- for (const target of await schemaWriteTargets(collection, workspaceRoot, safeSlug)) await require_atomic.writeFileAtomic(target, serialized);
1293
+ for (const target of await schemaWriteTargets(collection, workspaceRoot, safeSlug)) await require_root.writeFileAtomic(target, serialized);
1278
1294
  }
1279
1295
  /** Delete one custom view from `collection`: unlink its HTML file and drop it
1280
1296
  * from every schema.json copy. User-scope and preset (mc-*) collections are
@@ -1306,6 +1322,43 @@ async function deleteCustomView(collection, viewId, opts = {}) {
1306
1322
  viewId
1307
1323
  };
1308
1324
  }
1325
+ //#endregion
1326
+ //#region src/collection/server/schemaDocs.ts
1327
+ /** Fenced authoring variants inside the reference doc.
1328
+ *
1329
+ * A collection's authoring LOCATION depends on the root it lives in, and the
1330
+ * doc is served to the agent by both hosts:
1331
+ *
1332
+ * - a MANAGED workspace gates writes into `.claude/` and runs a skill-bridge
1333
+ * hook, so the agent authors under `data/skills/<slug>/` and the hook
1334
+ * mirrors it across (`staged`);
1335
+ * - a plain project root has neither gate nor bridge, so `data/skills/` is
1336
+ * never discovered — writing there produces nothing, silently — and the
1337
+ * agent must author under `.claude/skills/<slug>/` (`direct`).
1338
+ *
1339
+ * The two variants are fenced in the markdown so everything else stays shared
1340
+ * and the doc keeps reading as one document. Fence lines are removed with
1341
+ * their newline, so the `staged` render is byte-identical to the doc as it was
1342
+ * before the fences existed — which is what protects the single-workspace
1343
+ * host. */
1344
+ var VARIANT_FENCE = /^<!-- \/?authoring:(staged|direct) -->$/;
1345
+ /** Strip the fences and drop whichever variant's body doesn't apply. Runs
1346
+ * BEFORE section parsing, so a variant may contain headings or code fences. */
1347
+ function applyAuthoringVariant(doc, variant) {
1348
+ if (!doc.includes("<!-- authoring:")) return doc;
1349
+ const kept = [];
1350
+ let dropping = null;
1351
+ for (const line of doc.split("\n")) {
1352
+ const match = VARIANT_FENCE.exec(line.trim());
1353
+ if (match) {
1354
+ const [, name = ""] = match;
1355
+ dropping = line.trim().startsWith("<!-- /") ? null : name === variant ? null : name;
1356
+ continue;
1357
+ }
1358
+ if (dropping === null) kept.push(line);
1359
+ }
1360
+ return kept.join("\n");
1361
+ }
1309
1362
  /** Ceiling for ANY assembled reply — topic or default — safely inside
1310
1363
  * the agent's per-result limit (the full doc is what overflowed it).
1311
1364
  * A matched section too large to fit degrades to its own prose plus a
@@ -1446,7 +1499,8 @@ function renderTopic(lines, sections, topic) {
1446
1499
  * up outside any single piece's budget, and the ceiling must hold no
1447
1500
  * matter what shape of document arrives. Only the explicit \`"all"\`
1448
1501
  * opt-in and the small-doc verbatim path may exceed it. */
1449
- function renderSchemaDocs(doc, topic) {
1502
+ function renderSchemaDocs(rawDoc, topic, variant = "staged") {
1503
+ const doc = applyAuthoringVariant(rawDoc, variant);
1450
1504
  const requested = topic?.trim() ?? "";
1451
1505
  if (normalize(requested) === "all") return doc;
1452
1506
  if (doc.length <= 2e4) return doc;
@@ -1476,11 +1530,33 @@ var MAX_SCHEMA_ISSUES = 20;
1476
1530
  /** The workspace help-docs dir both hosts seed (`@mulmoclaude/core/workspace-setup`
1477
1531
  * syncs the bundled assets here) — the user-editable copy schemaDocs prefers. */
1478
1532
  var HELPS_DIR = "config/helps";
1533
+ /** `schemaDocs` names no collection, but the authoring layout is resolved per
1534
+ * slug (staging is `<staging>/<slug>`). This stands in, so the doc variant and
1535
+ * a later `putSchema` on any slug in this root cannot disagree. */
1536
+ var SCHEMA_DOCS_PROBE_SLUG = "_";
1479
1537
  /** Resolve the workspace root the same way every collections call does:
1480
1538
  * the injected override (tests) or the configured collection host. */
1481
1539
  function resolveBase(deps) {
1482
1540
  return deps.workspaceRoot ?? require_discovery.getWorkspaceRoot();
1483
1541
  }
1542
+ /** Where a collection skill is authored in THIS root, and therefore both which
1543
+ * authoring guide `schemaDocs` serves and where `getSchema` / `putSchema`
1544
+ * read and write. `null` staging means "author in the active skill dir".
1545
+ *
1546
+ * ONE predicate on purpose. Two knobs describe this — the host's
1547
+ * `stagedSkillAuthoring` and its `skillsStagingDir` — and either alone can
1548
+ * disagree with the other: a host that says `stagedSkillAuthoring: false`
1549
+ * while still returning a staging path would have the agent told to write
1550
+ * `.claude/skills/<slug>/` while `putSchema` wrote `data/skills/` and mirrored
1551
+ * from there, so the tool would silently contradict its own documentation.
1552
+ * Staged requires BOTH to agree; anything else is direct. */
1553
+ function authoringTarget(deps, slug) {
1554
+ const stagingDir = deps.stagedSkillAuthoring === false ? null : require_discovery.stagingSkillDir(resolveBase(deps), slug);
1555
+ return {
1556
+ variant: stagingDir === null ? "direct" : "staged",
1557
+ stagingDir
1558
+ };
1559
+ }
1484
1560
  /** Shared "unknown collection" message — its schema.json is missing or
1485
1561
  * failed validation, so discovery skipped it. */
1486
1562
  function unknownCollection(slug) {
@@ -1747,9 +1823,10 @@ async function handleGetOntology(deps) {
1747
1823
  * fallback. Both reads guarded; if neither resolves the agent still
1748
1824
  * gets an actionable message instead of a thrown call. */
1749
1825
  async function handleSchemaDocs(deps, topic) {
1826
+ const { variant } = authoringTarget(deps, SCHEMA_DOCS_PROBE_SLUG);
1750
1827
  const candidates = [node_path.default.join(resolveBase(deps), HELPS_DIR, SCHEMA_DOCS_FILE), ...deps.bundledHelpsDir ? [node_path.default.join(deps.bundledHelpsDir(), SCHEMA_DOCS_FILE)] : []];
1751
1828
  for (const candidate of candidates) try {
1752
- return renderSchemaDocs(await (0, node_fs_promises.readFile)(candidate, "utf-8"), topic);
1829
+ return renderSchemaDocs(await (0, node_fs_promises.readFile)(candidate, "utf-8"), topic, variant);
1753
1830
  } catch {}
1754
1831
  return `manageCollection: could not read the collection-authoring reference (${SCHEMA_DOCS_FILE}).`;
1755
1832
  }
@@ -1760,16 +1837,25 @@ async function handleSchemaDocs(deps, topic) {
1760
1837
  async function handleGetSchema(slug, deps) {
1761
1838
  const collection = await require_discovery.loadCollection(slug, deps);
1762
1839
  if (!collection) return unknownCollection(slug);
1763
- const candidates = [node_path.default.join(require_skill_bridge_index.dataSkillDir(resolveBase(deps), collection.slug), SCHEMA_FILE), node_path.default.join(collection.skillDir, SCHEMA_FILE)];
1840
+ const { stagingDir } = authoringTarget(deps, collection.slug);
1841
+ const candidates = [...stagingDir === null ? [] : [node_path.default.join(stagingDir, SCHEMA_FILE)], node_path.default.join(collection.skillDir, SCHEMA_FILE)];
1764
1842
  for (const candidate of candidates) try {
1765
1843
  return await (0, node_fs_promises.readFile)(candidate, "utf-8");
1766
1844
  } catch {}
1767
1845
  return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' has no readable ${SCHEMA_FILE}.`;
1768
1846
  }
1847
+ /** Where the agent should CREATE a collection skill in this root, named in the
1848
+ * messages that tell it to. Naming `data/skills/` under a root that has no
1849
+ * staging tree sends the agent to a directory nothing reads — the exact
1850
+ * silent failure the root-aware authoring guide exists to prevent, reappearing
1851
+ * in an error string the agent is far more likely to act on immediately. */
1852
+ function authoringDirLabel(deps, slug) {
1853
+ return authoringTarget(deps, slug).variant === "staged" ? `data/skills/${slug}/` : `.claude/skills/${slug}/`;
1854
+ }
1769
1855
  /** Refuse a schema edit the host can't honour: user-scope/feed collections
1770
1856
  * are read-only, and presets (mc-*) re-seed on restart so an edit is lost. */
1771
- function schemaEditRefusal(collection, slug) {
1772
- if (collection.source !== "project") return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is ${collection.source}-scope and read-only here — only project collections (data/skills/) can be edited.`;
1857
+ function schemaEditRefusal(collection, slug, deps) {
1858
+ if (collection.source !== "project") return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is ${collection.source}-scope and read-only here — only project collections (${authoringDirLabel(deps, "<slug>")}) can be edited.`;
1773
1859
  if (isPresetSlug(slug)) return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is a preset (mc-*) and re-seeds on restart — copy it under a different slug to customise.`;
1774
1860
  return null;
1775
1861
  }
@@ -1783,14 +1869,23 @@ function formatSchemaIssues(issues) {
1783
1869
  }
1784
1870
  /** Write the validated schema to the canonical staging copy, then mirror
1785
1871
  * it into the active `.claude/skills/` tree discovery reads — an internal
1786
- * write doesn't fire the skill-bridge hook, so we mirror explicitly. */
1787
- async function writeAndMirrorSchema(slug, schema, deps) {
1872
+ * write doesn't fire the skill-bridge hook, so we mirror explicitly.
1873
+ *
1874
+ * A root with NO staging tree has no bridge to mirror through either, so the
1875
+ * active skill dir IS the canonical copy and is written directly. Mirroring
1876
+ * there would read a staging file that never exists. */
1877
+ async function writeAndMirrorSchema(slug, skillDir, schema, deps) {
1788
1878
  const base = resolveBase(deps);
1789
- await require_atomic.writeFileAtomic(node_path.default.join(require_skill_bridge_index.dataSkillDir(base, slug), SCHEMA_FILE), `${JSON.stringify(schema, null, 2)}\n`);
1790
- require_skill_bridge_index.mirrorSkillWrite(base, {
1791
- slug,
1792
- relSegments: [SCHEMA_FILE]
1793
- });
1879
+ const serialized = `${JSON.stringify(schema, null, 2)}\n`;
1880
+ const { stagingDir } = authoringTarget(deps, slug);
1881
+ if (stagingDir === null) await require_root.writeFileAtomic(node_path.default.join(skillDir, SCHEMA_FILE), serialized);
1882
+ else {
1883
+ await require_root.writeFileAtomic(node_path.default.join(stagingDir, SCHEMA_FILE), serialized);
1884
+ require_skill_bridge_index.mirrorSkillWrite(base, {
1885
+ slug,
1886
+ relSegments: [SCHEMA_FILE]
1887
+ });
1888
+ }
1794
1889
  try {
1795
1890
  await deps.refreshAfterWrite?.();
1796
1891
  } catch {}
@@ -1810,19 +1905,19 @@ function schemaDiscoveryGate(schema, base) {
1810
1905
  return null;
1811
1906
  }
1812
1907
  /** Validate a schema against CollectionSchemaZ and, on success, persist it.
1813
- * Edit-only: a new collection is created by writing SKILL.md + schema.json
1814
- * under data/skills/<slug>/ (the normal create flow), not through here. */
1908
+ * Edit-only: a new collection is created by writing SKILL.md + schema.json in
1909
+ * the root's authoring dir (the normal create flow), not through here. */
1815
1910
  async function handlePutSchema(slug, schemaArg, deps) {
1816
1911
  if (!schemaArg || typeof schemaArg !== "object" || Array.isArray(schemaArg)) return "manageCollection: `schema` is required for putSchema — the full collection schema object.";
1817
1912
  const collection = await require_discovery.loadCollection(slug, deps);
1818
- if (!collection) return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — create it by writing SKILL.md + ${SCHEMA_FILE} under data/skills/${require_promptSafety.defangForPrompt(slug)}/, then edit it here.`;
1819
- const refusal = schemaEditRefusal(collection, slug);
1913
+ if (!collection) return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — create it by writing SKILL.md + ${SCHEMA_FILE} under ${authoringDirLabel(deps, require_promptSafety.defangForPrompt(slug))}, then edit it here.`;
1914
+ const refusal = schemaEditRefusal(collection, slug, deps);
1820
1915
  if (refusal) return refusal;
1821
1916
  const parsed = require_discovery.CollectionSchemaZ.safeParse(schemaArg);
1822
1917
  if (!parsed.success) return formatSchemaIssues(parsed.error.issues);
1823
1918
  const gate = schemaDiscoveryGate(parsed.data, resolveBase(deps));
1824
1919
  if (gate) return `manageCollection: schema rejected — ${gate} (call schemaDocs for the field reference). It passes basic validation but discovery would skip it, hiding the collection.`;
1825
- await writeAndMirrorSchema(collection.slug, parsed.data, deps);
1920
+ await writeAndMirrorSchema(collection.slug, collection.skillDir, parsed.data, deps);
1826
1921
  return JSON.stringify({
1827
1922
  collection: collection.slug,
1828
1923
  written: true
@@ -2160,4 +2255,4 @@ Object.defineProperty(exports, "validateRecordObject", {
2160
2255
  }
2161
2256
  });
2162
2257
 
2163
- //# sourceMappingURL=server-DOZFzx5l.cjs.map
2258
+ //# sourceMappingURL=server-WU35UYlF.cjs.map