@michengai/dsh-skills-manager 1.1.19 → 1.1.21

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.
package/lib/core.js CHANGED
@@ -118,7 +118,7 @@ function userRoots() {
118
118
  key: "agents",
119
119
  path: join(resolveAgentsHome(), "skills"),
120
120
  label: "\u516C\u5171 Agent",
121
- mutable: false,
121
+ mutable: true,
122
122
  toggleable: true,
123
123
  native: true,
124
124
  rank: 450
@@ -611,11 +611,11 @@ function rootDefinition(root) {
611
611
  function rootByKey(key) {
612
612
  return userRoots().find((item) => item.key === key) || null;
613
613
  }
614
- function writableRootDefinition(root, allowRepositoryRoot = false) {
614
+ function writableRootDefinition(root) {
615
615
  const definition = rootDefinition(root);
616
- if (allowRepositoryRoot && definition?.key === "agents") {
616
+ if (definition?.key === "agents") {
617
617
  const canonical = rootByKey("agents");
618
- return canonical && resolve(definition.path) === resolve(canonical.path) ? { ...canonical, mutable: true } : null;
618
+ return canonical && resolve(definition.path) === resolve(canonical.path) ? canonical : null;
619
619
  }
620
620
  if (!definition || definition.mutable !== true) return null;
621
621
  if (definition.key === "dsh")
@@ -624,16 +624,18 @@ function writableRootDefinition(root, allowRepositoryRoot = false) {
624
624
  return null;
625
625
  return definition;
626
626
  }
627
- async function checkedWritableRootDefinition(root, allowRepositoryRoot = false) {
628
- const definition = writableRootDefinition(root, allowRepositoryRoot);
629
- if (definition && definition.scope !== "project" && allowRepositoryRoot) {
630
- for (let path = resolve(definition.path); ; path = dirname(path)) {
631
- const st = await lstatOrNull(path);
632
- if (st && (!st.isDirectory() || st.isSymbolicLink()))
633
- return { ok: false, error: `\u6280\u80FD\u76EE\u5F55\u4E0D\u5B89\u5168\uFF0C\u62D2\u7EDD\u5199\u5165: ${path}`, code: "error.root.unsafe", params: { path } };
634
- if (dirname(path) === path) break;
635
- }
627
+ async function checkedUserRootAncestors(definition) {
628
+ for (let path = resolve(definition.path); ; path = dirname(path)) {
629
+ const st = await lstatOrNull(path);
630
+ if (st && (!st.isDirectory() || st.isSymbolicLink()))
631
+ return { ok: false, error: `\u6280\u80FD\u76EE\u5F55\u4E0D\u5B89\u5168\uFF0C\u62D2\u7EDD\u5199\u5165: ${path}`, code: "error.root.unsafe", params: { path } };
632
+ if (dirname(path) === path) break;
636
633
  }
634
+ return definition;
635
+ }
636
+ async function checkedWritableRootDefinition(root) {
637
+ const definition = writableRootDefinition(root);
638
+ if (definition?.key === "agents") return checkedUserRootAncestors(definition);
637
639
  if (!definition || definition.scope !== "project") return definition;
638
640
  if (await overlapsUserSkillRoot(definition.path)) {
639
641
  return {
@@ -656,12 +658,17 @@ async function checkedWritableRootDefinition(root, allowRepositoryRoot = false)
656
658
  }
657
659
  return definition;
658
660
  }
661
+ async function checkedRepositoryRootDefinition(root) {
662
+ const definition = await checkedWritableRootDefinition(root);
663
+ if (!definition || definition.ok === false || definition.scope === "project" || definition.key === "agents") return definition;
664
+ return checkedUserRootAncestors(definition);
665
+ }
659
666
  async function repositoryDestinations() {
660
667
  const result = [];
661
668
  for (const root of userRoots().filter((item) => item.key === "dsh" || item.key === "agents")) {
662
669
  let error;
663
670
  try {
664
- const checked = await checkedWritableRootDefinition(root, true);
671
+ const checked = await checkedRepositoryRootDefinition(root);
665
672
  if (!checked || checked.ok === false) error = checked || { code: "error.root.unsafe", error: "\u6280\u80FD\u5B89\u88C5\u4F4D\u7F6E\u4E0D\u53EF\u7528" };
666
673
  } catch (caught) {
667
674
  const exception = caught;
@@ -768,21 +775,22 @@ function parseSkillDoc(text) {
768
775
  const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(lines[i]);
769
776
  if (m) {
770
777
  fields.push({ key: m[1], raw: m[2] });
771
- const block = /^([>|])[-+]?\s*$/.exec(m[2]);
778
+ const block = /^([>|])(?:([1-9])[-+]?|[-+]([1-9])?)?\s*$/.exec(m[2]);
772
779
  if (block) {
773
780
  const content = [];
774
781
  while (i + 1 < end && (/^\s/.test(lines[i + 1]) || lines[i + 1] === "")) {
775
782
  i++;
776
783
  content.push(lines[i]);
777
784
  }
778
- const indentation = content.filter((line) => line.trim() !== "").reduce(
785
+ const explicitIndentation = block[2] || block[3];
786
+ const indentation = explicitIndentation ? Number(explicitIndentation) : content.filter((line) => line.trim() !== "").reduce(
779
787
  (min, line) => Math.min(min, (/^\s*/.exec(line) || [""])[0].length),
780
788
  Infinity
781
789
  );
782
790
  const normalized = content.map(
783
791
  (line) => Number.isFinite(indentation) ? line.slice(Math.min(indentation, line.length)) : line
784
792
  );
785
- map[m[1]] = block[1] === ">" ? normalized.join(" ").replace(/\s+/g, " ").trim() : normalized.join("\n").trim();
793
+ map[m[1]] = block[1] === ">" ? normalized.join(" ").replace(/\s+/g, " ").trim() : explicitIndentation ? normalized.join("\n").replace(/^\n+|\n+$/g, "") : normalized.join("\n").trim();
786
794
  } else {
787
795
  map[m[1]] = decodeYamlScalar(m[2]);
788
796
  }
@@ -894,7 +902,7 @@ function parseBoolValue(raw) {
894
902
  }
895
903
  async function resolveEntry(root, name) {
896
904
  const definition = rootDefinition(root);
897
- if (definition && !definition.mutable) {
905
+ if (definition && (!definition.mutable || definition.key === "agents")) {
898
906
  if (!validDiscoveryName(name)) return null;
899
907
  return (await discoverReadonlyEntries(definition.path)).entries.find(
900
908
  (entry) => entry.name === name
@@ -960,7 +968,7 @@ function entryOf(name, kind, docPath, doc) {
960
968
  });
961
969
  if (doc.hasFrontmatter && !declaredName)
962
970
  diagnostics.push({ level: "error", code: "diagnostic.name.missing" });
963
- else if (declaredName && !KEBAB_RE.test(declaredName))
971
+ else if (declaredName && !KEBAB_RE.test(toKebab(declaredName)))
964
972
  diagnostics.push({
965
973
  level: "error",
966
974
  code: "diagnostic.name.invalid",
@@ -984,13 +992,13 @@ function entryOf(name, kind, docPath, doc) {
984
992
  invocationPolicyValid,
985
993
  hasFrontmatter: doc.hasFrontmatter,
986
994
  // 调用策略值异常可以由 manager 本地策略覆盖;结构本身合法即可加载。
987
- loadable: doc.hasFrontmatter && KEBAB_RE.test(declaredName) && description !== "",
995
+ loadable: doc.hasFrontmatter && description !== "" && KEBAB_RE.test(KEBAB_RE.test(declaredName) ? declaredName : toKebab(declaredName)),
988
996
  diagnostics
989
997
  };
990
998
  }
991
999
  async function scanEntries(root, options = {}) {
992
1000
  const definition = rootDefinition(root);
993
- if (definition && !definition.mutable) {
1001
+ if (definition && (!definition.mutable || definition.key === "agents")) {
994
1002
  const discovered = await discoverReadonlyEntries(definition.path);
995
1003
  if (options.metadataOnly) return discovered;
996
1004
  const entries2 = [];
@@ -1113,7 +1121,7 @@ function defaultManagerState() {
1113
1121
  disabledSkills[root.key] = [];
1114
1122
  enabledSkills[root.key] = [];
1115
1123
  }
1116
- return { version: 1, sources, disabledSkills, enabledSkills, categories: /* @__PURE__ */ Object.create(null), categoryMembers: /* @__PURE__ */ Object.create(null) };
1124
+ return { version: 1, sources, disabledSkills, enabledSkills, categories: /* @__PURE__ */ Object.create(null), categoryMembers: /* @__PURE__ */ Object.create(null), categoryOrder: /* @__PURE__ */ Object.create(null) };
1117
1125
  }
1118
1126
  function validStateSkillName(name) {
1119
1127
  return validDiscoveryName(name);
@@ -1214,6 +1222,7 @@ function normalizeManagerState(value) {
1214
1222
  }
1215
1223
  normalized.categories = normalizeCategories(value.categories);
1216
1224
  normalized.categoryMembers = normalizeCategoryMembers(value.categoryMembers, normalized.categories);
1225
+ normalized.categoryOrder = normalizeCategoryOrder(value.categoryOrder, normalized.categories);
1217
1226
  return normalized;
1218
1227
  }
1219
1228
  const CATEGORY_NAME_MAX = 24;
@@ -1252,6 +1261,20 @@ function normalizeCategoryMembers(value, categories) {
1252
1261
  }
1253
1262
  return members;
1254
1263
  }
1264
+ function normalizeCategoryOrder(value, categories) {
1265
+ const order = /* @__PURE__ */ Object.create(null);
1266
+ if (!value || typeof value !== "object" || Array.isArray(value)) return order;
1267
+ for (const [key, groups] of Object.entries(value)) {
1268
+ if (!categoryRootKey(key) || !groups || typeof groups !== "object" || Array.isArray(groups)) continue;
1269
+ const validIds = /* @__PURE__ */ new Set(["", ...(categories[key] || []).map((item) => item.id)]);
1270
+ order[key] = /* @__PURE__ */ Object.create(null);
1271
+ for (const [id, names] of Object.entries(groups)) {
1272
+ if (!validIds.has(id) || !Array.isArray(names)) continue;
1273
+ order[key][id] = [...new Set(names.filter(validStateSkillName))];
1274
+ }
1275
+ }
1276
+ return order;
1277
+ }
1255
1278
  async function readManagerState() {
1256
1279
  try {
1257
1280
  const raw = await fs.readFile(managerStatePath(), "utf8");
@@ -1480,6 +1503,7 @@ async function writableCategoryState(key, options = {}) {
1480
1503
  if (current.writable === false) return invalidManagerStateWrite();
1481
1504
  if (!current.state.categories[root.key]) current.state.categories[root.key] = [];
1482
1505
  if (!current.state.categoryMembers[root.key]) current.state.categoryMembers[root.key] = /* @__PURE__ */ Object.create(null);
1506
+ if (!current.state.categoryOrder[root.key]) current.state.categoryOrder[root.key] = /* @__PURE__ */ Object.create(null);
1483
1507
  return { rootKey: root.key, root, state: current.state };
1484
1508
  }
1485
1509
  async function createCategory(key, name, log, options = {}) {
@@ -1492,6 +1516,7 @@ async function createCategory(key, name, log, options = {}) {
1492
1516
  return categoryError("error.category.duplicate", `\u5DF2\u6709\u540C\u540D\u5206\u7C7B: ${label}`, { name: label });
1493
1517
  const category = { id: randomBytes(8).toString("hex"), name: label };
1494
1518
  ready.state.categories[ready.rootKey].push(category);
1519
+ ready.state.categoryOrder[ready.rootKey][category.id] = [];
1495
1520
  await writeManagerState(ready.state);
1496
1521
  if (log) log("category-create", `\u5728 ${ready.rootKey} \u6DFB\u52A0\u5206\u7C7B ${label}`);
1497
1522
  return { root: ready.rootKey, category };
@@ -1518,6 +1543,7 @@ async function deleteCategory(key, id, log, options = {}) {
1518
1543
  const list = ready.state.categories[ready.rootKey];
1519
1544
  if (!list.some((item) => item.id === id)) return categoryError("error.category.notFound", "\u5206\u7C7B\u4E0D\u5B58\u5728", { id });
1520
1545
  ready.state.categories[ready.rootKey] = list.filter((item) => item.id !== id);
1546
+ delete ready.state.categoryOrder[ready.rootKey][id];
1521
1547
  for (const [skillName, categoryId] of Object.entries(ready.state.categoryMembers[ready.rootKey])) {
1522
1548
  if (categoryId === id) delete ready.state.categoryMembers[ready.rootKey][skillName];
1523
1549
  }
@@ -1536,12 +1562,67 @@ async function assignSkillCategory(key, skillName, categoryId, log, options = {}
1536
1562
  return categoryError("error.skill.notFound", `\u6280\u80FD\u4E0D\u5B58\u5728: ${skillName}`, { name: skillName });
1537
1563
  if (!ready.state.categories[ready.rootKey].some((item) => item.id === categoryId))
1538
1564
  return categoryError("error.category.notFound", "\u5206\u7C7B\u4E0D\u5B58\u5728", { id: categoryId });
1565
+ const previousId = ready.state.categoryMembers[ready.rootKey][skillName] || "";
1566
+ if (previousId !== categoryId) {
1567
+ ready.state.categoryOrder[ready.rootKey][previousId] = (ready.state.categoryOrder[ready.rootKey][previousId] || []).filter((name) => name !== skillName);
1568
+ const targetOrder = ready.state.categoryOrder[ready.rootKey][categoryId] || [];
1569
+ if (!targetOrder.includes(skillName)) targetOrder.push(skillName);
1570
+ ready.state.categoryOrder[ready.rootKey][categoryId] = targetOrder;
1571
+ }
1539
1572
  ready.state.categoryMembers[ready.rootKey][skillName] = categoryId;
1540
- } else delete ready.state.categoryMembers[ready.rootKey][skillName];
1573
+ } else {
1574
+ const previousId = ready.state.categoryMembers[ready.rootKey][skillName] || "";
1575
+ if (previousId) {
1576
+ ready.state.categoryOrder[ready.rootKey][previousId] = (ready.state.categoryOrder[ready.rootKey][previousId] || []).filter((name) => name !== skillName);
1577
+ const looseOrder = ready.state.categoryOrder[ready.rootKey][""] || [];
1578
+ if (!looseOrder.includes(skillName)) looseOrder.push(skillName);
1579
+ ready.state.categoryOrder[ready.rootKey][""] = looseOrder;
1580
+ }
1581
+ delete ready.state.categoryMembers[ready.rootKey][skillName];
1582
+ }
1541
1583
  await writeManagerState(ready.state);
1542
1584
  if (log) log("category-assign", `${ready.rootKey}/${skillName} \u5F52\u5165 ${categoryId || "\u672A\u5206\u7C7B"}`);
1543
1585
  return { root: ready.rootKey, name: skillName, categoryId: categoryId || null };
1544
1586
  }
1587
+ async function moveSkillInCategory(key, skillName, categoryId, targetName, position = "before", log, options = {}) {
1588
+ const ready = await writableCategoryState(key, options);
1589
+ if ("ok" in ready) return ready;
1590
+ if (!validStateSkillName(skillName))
1591
+ return categoryError("error.skill.notFound", `\u6280\u80FD\u4E0D\u5B58\u5728: ${skillName}`, { name: skillName });
1592
+ if (categoryId && !ready.state.categories[ready.rootKey].some((item) => item.id === categoryId))
1593
+ return categoryError("error.category.notFound", "\u5206\u7C7B\u4E0D\u5B58\u5728", { id: categoryId });
1594
+ if (position !== "before" && position !== "after")
1595
+ return categoryError("error.category.position", "\u6280\u80FD\u6392\u5E8F\u4F4D\u7F6E\u65E0\u6548");
1596
+ const entries = (await scanEntries(ready.root)).entries.map((entry) => entry.name);
1597
+ if (!entries.includes(skillName))
1598
+ return categoryError("error.skill.notFound", `\u6280\u80FD\u4E0D\u5B58\u5728: ${skillName}`, { name: skillName });
1599
+ if (targetName && (!validStateSkillName(targetName) || !entries.includes(targetName)))
1600
+ return categoryError("error.skill.notFound", `\u6392\u5E8F\u76EE\u6807\u4E0D\u5B58\u5728: ${targetName}`, { name: targetName });
1601
+ const members = ready.state.categoryMembers[ready.rootKey];
1602
+ const orders = ready.state.categoryOrder[ready.rootKey];
1603
+ const previousId = members[skillName] || "";
1604
+ const orderedNames = (id) => {
1605
+ const available = entries.filter((name) => (members[name] || "") === id).sort((a, b) => a.localeCompare(b));
1606
+ const availableSet = new Set(available);
1607
+ const saved = (orders[id] || []).filter((name) => availableSet.has(name));
1608
+ return [...saved, ...available.filter((name) => !saved.includes(name))];
1609
+ };
1610
+ if (targetName && (members[targetName] || "") !== categoryId)
1611
+ return categoryError("error.category.target", "\u6392\u5E8F\u76EE\u6807\u4E0D\u5C5E\u4E8E\u76EE\u6807\u5206\u7C7B", { name: targetName, category: categoryId });
1612
+ const sourceOrder = orderedNames(previousId).filter((name) => name !== skillName);
1613
+ const targetOrder = previousId === categoryId ? sourceOrder : orderedNames(categoryId).filter((name) => name !== skillName);
1614
+ let index = targetName ? targetOrder.indexOf(targetName) : -1;
1615
+ if (index < 0) index = targetOrder.length;
1616
+ else if (position === "after") index += 1;
1617
+ targetOrder.splice(index, 0, skillName);
1618
+ if (previousId !== categoryId) orders[previousId] = sourceOrder;
1619
+ orders[categoryId] = targetOrder;
1620
+ if (categoryId) members[skillName] = categoryId;
1621
+ else delete members[skillName];
1622
+ await writeManagerState(ready.state);
1623
+ if (log) log("category-order", `${ready.rootKey}/${skillName} \u79FB\u81F3 ${categoryId || "\u672A\u5206\u7C7B"} ${position} ${targetName || "\u672B\u5C3E"}`);
1624
+ return { root: ready.rootKey, name: skillName, categoryId: categoryId || null, order: targetOrder };
1625
+ }
1545
1626
  async function setCategoryEnabled(key, categoryId, enabled, log, options = {}) {
1546
1627
  const ready = await writableCategoryState(key, options);
1547
1628
  if ("ok" in ready) return ready;
@@ -1647,7 +1728,7 @@ function trashRootMetadata(definition) {
1647
1728
  async function restoreRootDefinition(metadata, options = {}) {
1648
1729
  if (!metadata.root) return rootByKey("dsh");
1649
1730
  if (metadata.root.scope === "user" && rootByKey(metadata.root.key))
1650
- return checkedWritableRootDefinition(rootByKey(metadata.root.key), metadata.root.key !== "dsh");
1731
+ return checkedWritableRootDefinition(rootByKey(metadata.root.key));
1651
1732
  if (metadata.root.scope !== "project" || metadata.root.kind !== "project-dsh" || typeof metadata.root.key !== "string" || typeof metadata.root.projectRoot !== "string" || !isAbsolute(metadata.root.projectRoot))
1652
1733
  return {
1653
1734
  ok: false,
@@ -1670,7 +1751,7 @@ async function restoreRootDefinition(metadata, options = {}) {
1670
1751
  return checkedWritableRootDefinition(root);
1671
1752
  }
1672
1753
  async function deleteSkill(root, name, log, options = {}) {
1673
- const definition = await checkedWritableRootDefinition(root, options.allowRepositoryRoot === true);
1754
+ const definition = await checkedWritableRootDefinition(root);
1674
1755
  if (definition && definition.ok === false) return definition;
1675
1756
  if (!definition) return readonlyError("delete");
1676
1757
  const resolved = await resolveEntry(definition, name);
@@ -1683,7 +1764,13 @@ async function deleteSkill(root, name, log, options = {}) {
1683
1764
  };
1684
1765
  const targets = (await safeExistingEntryPaths(definition.path, name)).filter((target) => !options.repositoryEntryOnly || target.fileName === name);
1685
1766
  if (!targets.length) return { ok: false, error: `\u6280\u80FD\u4E0D\u5B58\u5728: ${name}`, code: "error.skill.notFound", params: { name } };
1686
- const id = options.repositoryTrash?.id || `${Date.now()}-${randomUUID()}`;
1767
+ if (definition.key === "agents") {
1768
+ for (const target of targets) {
1769
+ if ((await lstatOrNull(target.path))?.isSymbolicLink())
1770
+ return { ok: false, error: `\u6280\u80FD\u76EE\u5F55\u4E0D\u5B89\u5168\uFF0C\u62D2\u7EDD\u5199\u5165: ${target.path}`, code: "error.root.unsafe", params: { path: target.path } };
1771
+ }
1772
+ }
1773
+ const id = options.repositoryTrash?.id || options.catalogTrash?.id || `${Date.now()}-${randomUUID()}`;
1687
1774
  if (!/^\d+-[a-f0-9-]{36}$/.test(id)) throw codedError("\u56DE\u6536\u7AD9\u6761\u76EE\u7F16\u53F7\u975E\u6CD5", "error.trash.invalid", { id });
1688
1775
  const trashRoot = trashRootPath();
1689
1776
  const stage = join(trashRoot, `.stage-${randomUUID()}`);
@@ -1712,7 +1799,8 @@ async function deleteSkill(root, name, log, options = {}) {
1712
1799
  deletedAt: (/* @__PURE__ */ new Date()).toISOString(),
1713
1800
  entries: moved.map((item) => item.fileName),
1714
1801
  root: trashRootMetadata(definition),
1715
- ...options.repositoryTrash ? { repository: options.repositoryTrash.repository } : {}
1802
+ ...options.repositoryTrash ? { repository: options.repositoryTrash.repository } : {},
1803
+ ...options.catalogTrash ? { catalog: options.catalogTrash.ids } : {}
1716
1804
  };
1717
1805
  await fs.writeFile(
1718
1806
  join(stage, "metadata.json"),
@@ -1840,8 +1928,15 @@ async function forgetCategoryMember(metadata) {
1840
1928
  const current = await readManagerState();
1841
1929
  if (current.writable === false) return;
1842
1930
  const members = current.state.categoryMembers[key];
1843
- if (!members || !members[metadata.name]) return;
1844
- delete members[metadata.name];
1931
+ const orders = current.state.categoryOrder[key];
1932
+ const hasMember = !!(members && members[metadata.name]);
1933
+ const hasOrder = !!(orders && Object.values(orders).some((names) => names.includes(metadata.name)));
1934
+ if (!hasMember && !hasOrder) return;
1935
+ if (members) delete members[metadata.name];
1936
+ if (orders) {
1937
+ for (const id of Object.keys(orders))
1938
+ orders[id] = orders[id].filter((name) => name !== metadata.name);
1939
+ }
1845
1940
  await writeManagerState(current.state);
1846
1941
  }
1847
1942
  async function permanentlyDeleteTrash(id, log) {
@@ -2092,7 +2187,7 @@ async function replaceWithCopy(source, dest, isDir, existing = []) {
2092
2187
  }
2093
2188
  async function importSkill(source, log, options = {}) {
2094
2189
  const requestedRoot = Object.prototype.hasOwnProperty.call(options, "root") ? options.root : rootByKey("dsh");
2095
- const definition = await checkedWritableRootDefinition(requestedRoot, options.allowRepositoryRoot === true);
2190
+ const definition = await checkedWritableRootDefinition(requestedRoot);
2096
2191
  if (definition && definition.ok === false) return definition;
2097
2192
  if (!definition) return readonlyError("create");
2098
2193
  const targetRoot = definition.path;
@@ -2214,6 +2309,7 @@ async function importSkill(source, log, options = {}) {
2214
2309
  if (pending.length === 0 && conflicts.length === 0) return failureResult();
2215
2310
  return { kind: analysis.kind, pending, conflicts, failed };
2216
2311
  }
2312
+ if (!dryRun && options.beforeImport) await options.beforeImport(pending, conflicts, failed);
2217
2313
  if (pending.length > 0 || conflict === "overwrite" && conflicts.length > 0) {
2218
2314
  await fs.mkdir(targetRoot, { recursive: true });
2219
2315
  }
@@ -2449,7 +2545,7 @@ function yamlString(value) {
2449
2545
  }
2450
2546
  async function createSkill(input, log, options = {}) {
2451
2547
  const requestedRoot = Object.prototype.hasOwnProperty.call(options, "root") ? options.root : rootByKey("dsh");
2452
- const definition = await checkedWritableRootDefinition(requestedRoot, options.allowRepositoryRoot === true);
2548
+ const definition = await checkedWritableRootDefinition(requestedRoot);
2453
2549
  if (definition && definition.ok === false) return definition;
2454
2550
  if (!definition) return readonlyError("create");
2455
2551
  const root = definition.path;
@@ -2575,12 +2671,12 @@ async function listProviderCandidates(options = {}) {
2575
2671
  const hasExplicitDisable = group.some((item) => item.policy.override === false);
2576
2672
  if (!hasNativeCopy && !hasExplicitDisable && !policy.sourceEnabled) continue;
2577
2673
  const project = root.scope === "project";
2578
- const policyOnly = root.mutable;
2674
+ const policyOnly = root.mutable && root.key !== "agents";
2579
2675
  const overlayRank = group.length > 1 ? Math.min(...group.map((item) => Math.min(item.root.rank, item.entry.providerRank ?? item.root.rank))) - 1 : void 0;
2580
2676
  const needsOverlay = !policyOnly || group.length > 1 || policy.override !== void 0 || !entry.invocationPolicyValid || policyResult.writable === false;
2581
2677
  if (!needsOverlay) continue;
2582
2678
  candidates.push({
2583
- name: entry.declaredName,
2679
+ name: canonicalSkillName({ root, entry, policy }),
2584
2680
  description: entry.description,
2585
2681
  invocation: {
2586
2682
  modelInvocable: policy.modelInvocable,
@@ -2636,8 +2732,9 @@ async function getProviderSkill(candidate, options = {}) {
2636
2732
  await fs.readFile(entry.realDocPath || entry.docPath, "utf8")
2637
2733
  );
2638
2734
  const summary = entryOf(locator.entryName, entry.kind, entry.docPath, doc);
2639
- if (!summary.loadable || summary.declaredName !== candidate.name)
2640
- return void 0;
2735
+ if (!summary.loadable) return void 0;
2736
+ const invocationName = KEBAB_RE.test(summary.declaredName) ? summary.declaredName : toKebab(summary.declaredName);
2737
+ if (invocationName !== candidate.name && summary.declaredName !== candidate.name) return void 0;
2641
2738
  return {
2642
2739
  name: candidate.name,
2643
2740
  description: candidate.description,
@@ -2654,7 +2751,10 @@ async function getProviderSkill(candidate, options = {}) {
2654
2751
  }
2655
2752
  }
2656
2753
  function canonicalSkillName(item) {
2657
- return item.entry.declaredName || item.entry.name;
2754
+ const declared = item.entry.declaredName || "";
2755
+ if (KEBAB_RE.test(declared)) return declared;
2756
+ const kebab = toKebab(declared);
2757
+ return kebab || declared || item.entry.name;
2658
2758
  }
2659
2759
  function groupLoadableSkillsByName(items) {
2660
2760
  const groups = /* @__PURE__ */ new Map();
@@ -2834,7 +2934,8 @@ async function state(options = {}) {
2834
2934
  truncated: truncated === true,
2835
2935
  enabled: policyResult.writable !== false && (root.kind === "project-dsh" || policyResult.state.sources[root.key] !== false),
2836
2936
  skills,
2837
- categories: policyResult.state.categories[root.key] || []
2937
+ categories: policyResult.state.categories[root.key] || [],
2938
+ categoryOrder: policyResult.state.categoryOrder[root.key] || {}
2838
2939
  });
2839
2940
  }
2840
2941
  const userItems = all.filter((item) => item.root.scope !== "project");
@@ -2876,6 +2977,7 @@ export {
2876
2977
  KEBAB_RE,
2877
2978
  assignSkillCategory,
2878
2979
  browseDirectories,
2980
+ checkedRepositoryRootDefinition,
2879
2981
  createCategory,
2880
2982
  createSkill,
2881
2983
  createWorkspaceIndex,
@@ -2892,6 +2994,7 @@ export {
2892
2994
  logPath,
2893
2995
  managerHomePath,
2894
2996
  managerStatePath,
2997
+ moveSkillInCategory,
2895
2998
  parseBoolValue,
2896
2999
  parseSkillDoc,
2897
3000
  permanentlyDeleteTrash,
package/lib/index.js CHANGED
@@ -7,9 +7,8 @@ import {
7
7
  renameCategory,
8
8
  deleteCategory,
9
9
  assignSkillCategory,
10
+ moveSkillInCategory,
10
11
  setCategoryEnabled,
11
- deleteSkill,
12
- permanentlyDeleteTrash,
13
12
  importSkill,
14
13
  importUploadedSkill,
15
14
  browseDirectories,
@@ -27,6 +26,9 @@ import {
27
26
  import { registerPluginUpdater } from "./plugin-updater.js";
28
27
  import { createRepositoryManager } from "./repositories.js";
29
28
  import { installationSourceKey } from "./repository-updates.js";
29
+ import { listClawHub, searchClawHub } from "./clawhub-catalog.js";
30
+ import { searchSkillsSh } from "./skills-sh-catalog.js";
31
+ import { loadCatalogPackage, readCatalogSkill } from "./catalog-skill.js";
30
32
  const name = "skills-manager";
31
33
  const inject = ["webServer", "skills", "tools", "sessions"];
32
34
  const CLIENT_MARKER_HEADER = "x-dsh-skills-manager";
@@ -362,7 +364,6 @@ function apply(ctx) {
362
364
  return { projectCwds: target.root.workspaceCwds ?? [] };
363
365
  };
364
366
  const scopedOptions = (scope) => scope ?? projectOptions();
365
- const userWriteOptions = (body, target) => ({ allowRepositoryRoot: !target && body.root === "agents" });
366
367
  const requestRoot = async (key, cwds) => cwds ? (await projectRoots(cwds)).find((root) => root.key === key) : rootByKey[key] || (await projectRoots(projectOptions().projectCwds)).find((root) => root.key === key);
367
368
  const withScope = async (body, action) => {
368
369
  const scope = await workspaceScope(body);
@@ -396,7 +397,25 @@ function apply(ctx) {
396
397
  return run(res, async () => ({ items: await listWorkspaces(registeredWorkspacePaths(ctx) ?? []) }), void 0, req.method === "HEAD");
397
398
  }
398
399
  if ((req.method === "GET" || req.method === "HEAD") && path === "/api/dsh-skills-manager/repositories") {
399
- return run(res, () => repositories.list(), void 0, req.method === "HEAD");
400
+ return run(res, () => enqueueMutation(() => repositories.list()), void 0, req.method === "HEAD");
401
+ }
402
+ if (req.method === "GET" && (path === "/api/dsh-skills-manager/clawhub/list" || path === "/api/dsh-skills-manager/clawhub/search")) {
403
+ const searching = path.endsWith("/search");
404
+ const allowed = searching ? ["q", "limit"] : ["limit", "cursor", "sort"];
405
+ if ([...u.searchParams.keys()].some((key) => !allowed.includes(key) || u.searchParams.getAll(key).length !== 1) || searching && !u.searchParams.has("q")) {
406
+ json(res, 400, { ok: false, code: "error.catalog.query", error: "\u76EE\u5F55\u67E5\u8BE2\u53C2\u6570\u65E0\u6548" });
407
+ return;
408
+ }
409
+ const parameter = (key) => u.searchParams.get(key) ?? void 0;
410
+ return run(res, () => searching ? searchClawHub(parameter("q"), { limit: parameter("limit") }) : listClawHub({ limit: parameter("limit"), sort: parameter("sort"), cursor: parameter("cursor") }));
411
+ }
412
+ if (req.method === "GET" && path === "/api/dsh-skills-manager/skills-sh/search") {
413
+ const queries = u.searchParams.getAll("q");
414
+ if (queries.length !== 1) {
415
+ json(res, 400, { ok: false, code: "error.catalog.query", error: "\u641C\u7D22\u8BCD\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u8D85\u8FC7 100 \u4E2A\u5B57\u7B26" });
416
+ return;
417
+ }
418
+ return run(res, () => searchSkillsSh(queries[0]));
400
419
  }
401
420
  if (req.method === "HEAD") {
402
421
  if (path === "/api/dsh-skills-manager/state") return run(res, stateWithSources, void 0, true);
@@ -421,6 +440,18 @@ function apply(ctx) {
421
440
  return run(res, () => browseDirectories(body.path));
422
441
  }
423
442
  if (path === "/api/dsh-skills-manager/repositories/refresh") return run(res, () => repositories.refresh(body));
443
+ if (path === "/api/dsh-skills-manager/catalog/detail") return run(res, () => readCatalogSkill(body));
444
+ if (path === "/api/dsh-skills-manager/catalog/install") {
445
+ return run(res, async () => {
446
+ if (body.root !== void 0 && body.root !== "dsh" && body.root !== "agents") throw Object.assign(new Error("\u6280\u80FD\u5B89\u88C5\u4F4D\u7F6E\u65E0\u6548"), { statusCode: 400, code: "error.repo.invalid" });
447
+ if (body.workspace !== void 0 && body.root !== void 0 && body.root !== "dsh") throw Object.assign(new Error("\u5DE5\u4F5C\u533A\u53EA\u80FD\u5B89\u88C5\u5230\u9879\u76EE DSH skills"), { statusCode: 400, code: "error.repo.invalid" });
448
+ const target = await workspaceTarget(body);
449
+ if (target && "ok" in target && target.ok === false) return target;
450
+ const pack = await loadCatalogPackage(body);
451
+ const catalogId = body.catalog === "skillsh" ? `skillsh:${body.source}/${body.skillId}` : `clawhub:${body.owner}/${body.slug}`;
452
+ return enqueueMutation(() => repositories.installCatalog(catalogId, pack, { root: body.root, ...body.workspace === void 0 ? {} : { workspace: body.workspace } }));
453
+ }, afterWrite);
454
+ }
424
455
  return enqueueMutation(() => {
425
456
  switch (path) {
426
457
  case "/api/dsh-skills-manager/repositories/add":
@@ -453,23 +484,30 @@ function apply(ctx) {
453
484
  case "/api/dsh-skills-manager/source-disable":
454
485
  return run(res, () => withScope(body, (scope) => setSourceEnabled(String(body.root || ""), false, log, scopedOptions(scope))), afterWrite);
455
486
  case "/api/dsh-skills-manager/category-create":
456
- return run(res, () => withScope(body, (scope) => createCategory(String(body.root || ""), String(body.name || ""), log, scopedOptions(scope))), afterWrite);
487
+ return run(res, () => withScope(body, (scope) => createCategory(String(body.root || ""), String(body.name || ""), log, scopedOptions(scope))));
457
488
  case "/api/dsh-skills-manager/category-rename":
458
- return run(res, () => withScope(body, (scope) => renameCategory(String(body.root || ""), String(body.id || ""), String(body.name || ""), log, scopedOptions(scope))), afterWrite);
489
+ return run(res, () => withScope(body, (scope) => renameCategory(String(body.root || ""), String(body.id || ""), String(body.name || ""), log, scopedOptions(scope))));
459
490
  case "/api/dsh-skills-manager/category-delete":
460
- return run(res, () => withScope(body, (scope) => deleteCategory(String(body.root || ""), String(body.id || ""), log, scopedOptions(scope))), afterWrite);
491
+ return run(res, () => withScope(body, (scope) => deleteCategory(String(body.root || ""), String(body.id || ""), log, scopedOptions(scope))));
461
492
  case "/api/dsh-skills-manager/category-assign":
462
- return run(res, () => withScope(body, (scope) => assignSkillCategory(String(body.root || ""), String(body.name || ""), String(body.category || ""), log, scopedOptions(scope))), afterWrite);
493
+ return run(res, () => withScope(body, (scope) => assignSkillCategory(String(body.root || ""), String(body.name || ""), String(body.category || ""), log, scopedOptions(scope))));
494
+ case "/api/dsh-skills-manager/category-move":
495
+ return run(res, () => withScope(body, (scope) => moveSkillInCategory(String(body.root || ""), String(body.name || ""), String(body.category || ""), body.targetName ? String(body.targetName) : void 0, body.position === void 0 ? "before" : String(body.position), log, scopedOptions(scope))));
463
496
  case "/api/dsh-skills-manager/category-enable":
464
497
  return run(res, () => withScope(body, (scope) => setCategoryEnabled(String(body.root || ""), String(body.category || ""), true, log, scopedOptions(scope))), afterWrite);
465
498
  case "/api/dsh-skills-manager/category-disable":
466
499
  return run(res, () => withScope(body, (scope) => setCategoryEnabled(String(body.root || ""), String(body.category || ""), false, log, scopedOptions(scope))), afterWrite);
467
500
  case "/api/dsh-skills-manager/delete":
468
- return run(res, () => withScope(body, async (scope) => deleteSkill(await requestRoot(String(body.root || "dsh"), scope?.projectCwds), String(body.name || ""), log)), afterWrite);
501
+ return run(res, () => withScope(body, async (scope) => {
502
+ const target = await workspaceTarget(body);
503
+ if (target && "ok" in target && target.ok === false) return target;
504
+ const root = target ? target.root : await requestRoot(String(body.root || "dsh"), scope?.projectCwds);
505
+ return repositories.trashSkill(root, String(body.name || ""));
506
+ }), afterWrite);
469
507
  case "/api/dsh-skills-manager/trash-restore":
470
508
  return run(res, () => withScope(body, (scope) => repositories.restoreTrash(String(body.id || ""), { ...scope ?? {}, registeredWorkspaces: registeredWorkspacePaths(ctx) })), afterWrite);
471
509
  case "/api/dsh-skills-manager/trash-delete":
472
- return run(res, () => permanentlyDeleteTrash(String(body.id || ""), log));
510
+ return run(res, () => repositories.permanentlyDeleteTrash(String(body.id || "")));
473
511
  case "/api/dsh-skills-manager/detail":
474
512
  return run(res, () => withScope(body, (scope) => skillDetail(String(body.root || "dsh"), String(body.name || ""), scope ? { ...scope, workspaceBound: true } : projectOptions())));
475
513
  case "/api/dsh-skills-manager/create":
@@ -479,7 +517,7 @@ function apply(ctx) {
479
517
  return createSkill(
480
518
  { name: body.name, description: body.description, body: body.body },
481
519
  log,
482
- { root: target ? target.root : await requestRoot(String(body.root || "dsh")), ...userWriteOptions(body, target) }
520
+ { root: target ? target.root : await requestRoot(String(body.root || "dsh")) }
483
521
  );
484
522
  }, afterWrite);
485
523
  case "/api/dsh-skills-manager/import":
@@ -489,8 +527,7 @@ function apply(ctx) {
489
527
  return importSkill(String(body.source || ""), log, {
490
528
  conflict: body.conflict === "overwrite" ? "overwrite" : "skip",
491
529
  dryRun: body.dryRun === true,
492
- ...target ? { root: target.root } : body.root === "agents" ? { root: await requestRoot("agents") } : {},
493
- ...userWriteOptions(body, target)
530
+ ...target ? { root: target.root } : body.root === "agents" ? { root: await requestRoot("agents") } : {}
494
531
  });
495
532
  }, body.dryRun === true ? void 0 : afterWrite);
496
533
  case "/api/dsh-skills-manager/upload":
@@ -499,8 +536,7 @@ function apply(ctx) {
499
536
  if (target && "ok" in target && target.ok === false) return target;
500
537
  return importUploadedSkill({ name: body.name, entries: body.entries, zip: body.zip }, log, {
501
538
  conflict: body.conflict === "overwrite" ? "overwrite" : "skip",
502
- ...target ? { root: target.root } : body.root === "agents" ? { root: await requestRoot("agents") } : {},
503
- ...userWriteOptions(body, target)
539
+ ...target ? { root: target.root } : body.root === "agents" ? { root: await requestRoot("agents") } : {}
504
540
  });
505
541
  }, afterWrite);
506
542
  default: