dsh-plugin-tool-management 0.5.1 → 0.7.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 (49) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +185 -238
  3. package/README_EN.md +185 -304
  4. package/cordis.patch.yml +9 -55
  5. package/docs/images/1/345/234/272/346/231/257.png +0 -0
  6. package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
  7. package/docs/images/2MCP.png +0 -0
  8. package/docs/images/2MCP_en.png +0 -0
  9. package/docs/images/3/346/212/200/350/203/275.png +0 -0
  10. package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
  11. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  12. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
  13. package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
  14. package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
  15. package/docs/images/6/350/256/260/345/277/206.png +0 -0
  16. package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
  17. package/docs/images/7/344/274/232/350/257/235.png +0 -0
  18. package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
  19. package/docs/images/8/345/205/274/345/256/271.png +0 -0
  20. package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
  21. package/docs/update.md +55 -0
  22. package/lib/agents-md/preset-id.js +49 -0
  23. package/lib/agents-md/service.js +180 -57
  24. package/lib/client.js +4710 -3560
  25. package/lib/compat/preset-reach.js +425 -0
  26. package/lib/compat/probe.js +665 -0
  27. package/lib/history/bridge.js +293 -0
  28. package/lib/history/workspace.js +498 -52
  29. package/lib/http-fence.js +94 -0
  30. package/lib/hub.js +160 -2
  31. package/lib/index.js +759 -157
  32. package/lib/mcp/override-blocks.js +195 -0
  33. package/lib/rules/provider.js +3 -3
  34. package/lib/rules/service.js +342 -31
  35. package/lib/scene-prompt-sync.js +112 -0
  36. package/lib/skills/core.js +123 -35
  37. package/lib/skills/service.js +9 -1
  38. package/lib/subagents/service.js +197 -23
  39. package/lib/subagents/tools.js +8 -2
  40. package/package.json +6 -3
  41. package/screenshots.json +10 -9
  42. package/docs/Changelog.md +0 -517
  43. package/docs/images/MCP.png +0 -0
  44. package/docs/images//344/274/232/350/257/235.png +0 -0
  45. package/docs/images//345/234/272/346/231/257.png +0 -0
  46. package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  47. package/docs/images//346/212/200/350/203/275.png +0 -0
  48. package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
  49. package/docs/images//350/256/260/345/277/206.png +0 -0
@@ -67,6 +67,10 @@ export function userRoots() {
67
67
  path: join(resolveDshHome(), "skills"),
68
68
  label: "DSH 技能",
69
69
  mutable: true,
70
+ // 默认来源:路径不可移除、不可停用(必须读取),但里面的技能可删(见 isDefaultSkillSource)。
71
+ deletable: true,
72
+ // 注意:`toggleable` 指的是「这个来源里的**单个技能**能否启停」,与来源开关无关 ——
73
+ // 来源层的启停/移除由 isDefaultSkillSource 判定,两者别混。
70
74
  toggleable: true,
71
75
  native: true,
72
76
  rank: 400,
@@ -77,12 +81,13 @@ export function userRoots() {
77
81
  // 官方 ~/.dsh/skills/ 仍作为可切换来源列出(不搬走、不删)。
78
82
  //
79
83
  // 界面名「导入技能」:它是插件导入/新建技能的落点,不是"管理器自己的一类技能"。
80
- // **用户级来源不可删**(dsh / hub 都是):技能只能停用,删除留给项目级来源。
84
+ // 与 dsh 同为默认来源:路径必须读取(不可移除、不可停用),里面的技能可删。
81
85
  key: "hub",
82
86
  path: join(resolveDshHome(), "tool-management", "skills"),
83
87
  label: "导入技能",
84
88
  localeKey: "hub",
85
89
  mutable: true,
90
+ deletable: true,
86
91
  toggleable: true,
87
92
  native: false,
88
93
  rank: 350,
@@ -116,6 +121,27 @@ export function userRoots() {
116
121
  },
117
122
  ];
118
123
  }
124
+ /**
125
+ * 默认来源:插件自己的读写根 —— `dsh`(官方技能目录)与 `hub`(新建/导入落点)。
126
+ *
127
+ * 用户裁定的是**不对称**语义,两个方向必须成对记住,只改一半就是漏改:
128
+ *
129
+ * - 来源**路径**:必须读取。既不能「移除来源」(连目录都不扫),也不能「停用来源」
130
+ * (仍然扫描但仍算不读取的策略位)—— 界面上没有来源开关,服务端也拒绝这两个写操作。
131
+ * 因为它们是插件自己的读写根:移除会让创建/导入无处落脚。
132
+ * - 路径**里面的技能**:可以删除(`deletable: true`,移入插件回收站,可恢复)。
133
+ *
134
+ * 其余来源(外部 Agent / 自定义绝对路径 / 项目级)恰好相反:来源可以停用或移除
135
+ * (「不再读取这个文件夹」),但目录里的技能只读、不可删除。
136
+ *
137
+ * 一句话记法:**`mutable` 与 `deletable` 同向,`removable` 与 `mutable` 反向。**
138
+ */
139
+ const DEFAULT_SOURCE_KEYS = Object.freeze(["dsh", "hub"]);
140
+ /** 是否为默认来源。接受 root 对象或 key 字符串;`dsh` / `hub` 的所有特判都走这里。 */
141
+ export function isDefaultSkillSource(rootOrKey) {
142
+ const key = rootOrKey && typeof rootOrKey === "object" ? rootOrKey.key : rootOrKey;
143
+ return DEFAULT_SOURCE_KEYS.includes(key);
144
+ }
119
145
  function projectIdentity(path) {
120
146
  const canonical = pathIdentity(path);
121
147
  return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
@@ -152,6 +178,9 @@ export function customRootsFromState(stateValue) {
152
178
  label: typeof item.label === "string" && item.label.trim()
153
179
  ? item.label.trim().slice(0, 64)
154
180
  : "自定义目录",
181
+ // 用户加进来的来源(相对「按约定发现」的 dsh / hub / agents / codex / claude):
182
+ // 界面据此决定要不要给「永久删除」——只有自定义来源能真正从插件里删掉记录。
183
+ custom: true,
155
184
  mutable: false,
156
185
  toggleable: true,
157
186
  native: false,
@@ -254,6 +283,10 @@ export async function removeCustomRoot(key, log) {
254
283
  delete current.state.sources[clean];
255
284
  delete current.state.disabledSkills[clean];
256
285
  delete current.state.enabledSkills[clean];
286
+ // 已移除记录一起清掉:否则「永久删除」之后状态文件里还留着一条指向不存在来源的脏键
287
+ // (读取时的 normalize 会丢弃它,但没理由写进去)。
288
+ if (Array.isArray(current.state.removedSources))
289
+ current.state.removedSources = current.state.removedSources.filter((key) => key !== clean);
257
290
  await writeManagerState(current.state);
258
291
  if (log)
259
292
  log("custom-remove", `移除自定义技能目录 ${target.path} (${clean})`);
@@ -333,8 +366,8 @@ export async function projectRoots(projectCwds = [], diagnostics) {
333
366
  const id = projectIdentity(project.root);
334
367
  const common = {
335
368
  mutable: false,
336
- // 项目级 DSH 根是**唯一**允许删除技能文件的来源;用户级来源(dsh / hub)一律不可删。
337
- // 用「作用域」推导而不是再加一个平行开关:这是项目根与用户根的本质差别。
369
+ // 未知/只读来源(外部 Agent 与自定义绝对路径)一律不可删;用户级 dsh / hub
370
+ // 与本仓库的项目级 .dsh/skills 都是可删的(见上方两处 `deletable: true`)。
338
371
  deletable: false,
339
372
  toggleable: false,
340
373
  native: true,
@@ -504,16 +537,38 @@ function readonlyError(action) {
504
537
  };
505
538
  }
506
539
  /**
507
- * 来源可写但**不允许删除**时的拒绝结果(用户级 DSH 技能 / 导入技能)。
508
- * 与「只读来源」区分开:这两种来源可以创建、可以停用,只是不提供删除 —— 提示要能说清
509
- * 「你还能做什么」,否则用户会以为是权限坏了。
540
+ * 来源可写但**没有删除权**时的拒绝结果 —— 防御性分支。
541
+ *
542
+ * 来源表里可写的三种根(dsh / hub / 项目级 `.dsh/skills`)现在都带 `deletable: true`,
543
+ * 正常路径走不到这里。它守的是「以后新增可写来源时忘了标 deletable」:那种情况下
544
+ * 必须拒绝删除,而不是照搬一个用户目录进回收站。
510
545
  */
511
546
  function notDeletableError(definition) {
512
547
  return {
513
548
  ok: false,
514
549
  code: "error.skill.notDeletable",
515
550
  params: { root: definition && definition.key ? definition.key : "" },
516
- error: "该来源的技能不能删除(技能只能停用):只在项目级来源(<项目>/.dsh/skills)提供删除",
551
+ error: "该来源没有开启删除(可写来源需要显式标记 deletable: true 才提供删除)",
552
+ };
553
+ }
554
+ /**
555
+ * 默认来源(dsh / hub)被要求「停用」或「移除」时的拒绝结果。
556
+ *
557
+ * 这两种来源是插件自己的读写根,**必须读取**:停用会让它退化成"读出来但不可调用",
558
+ * 移除则连目录都不扫,两者都与「路径不能不读取」冲突。
559
+ *
560
+ * 被拒的只有**来源层**操作 —— 来源**里面的技能**照常可以删除(那是 `deletable`,
561
+ * 与来源的 removable / 停用无关),提示里要写清这一点,否则用户会以为整个来源被锁死。
562
+ */
563
+ function reservedSourceError(root) {
564
+ const key = root && root.key ? root.key : "";
565
+ return {
566
+ ok: false,
567
+ code: "error.source.reserved",
568
+ params: { root: key },
569
+ error: key === "dsh"
570
+ ? "DSH 技能目录是默认来源,必须读取:不能停用或移除(里面的技能可以删除)"
571
+ : "导入技能目录是默认来源、插件新建/导入的落点,必须读取:不能停用或移除(里面的技能可以删除)",
517
572
  };
518
573
  }
519
574
  function rootDefinition(root) {
@@ -1089,7 +1144,9 @@ function defaultManagerState() {
1089
1144
  const disabledSkills = Object.create(null);
1090
1145
  const enabledSkills = Object.create(null);
1091
1146
  for (const root of userRoots()) {
1092
- if (root.key !== "dsh")
1147
+ // 默认来源(dsh / hub)没有来源开关,所以不落进 sources 表 —— 它们永远「读取」。
1148
+ // 旧版本可能往这里写过 hub 的 false,normalizeManagerState 会顺手丢掉。
1149
+ if (!isDefaultSkillSource(root.key))
1093
1150
  sources[root.key] = true;
1094
1151
  disabledSkills[root.key] = [];
1095
1152
  enabledSkills[root.key] = [];
@@ -1178,7 +1235,7 @@ function validManagerStateDocument(value) {
1178
1235
  return false;
1179
1236
  }
1180
1237
  for (const root of userRoots()) {
1181
- if (root.key === "dsh")
1238
+ if (isDefaultSkillSource(root.key))
1182
1239
  continue;
1183
1240
  if (typeof value.sources[root.key] !== "boolean")
1184
1241
  return false;
@@ -1205,7 +1262,9 @@ function normalizeManagerState(value) {
1205
1262
  normalized.customRoots = normalizeCustomRoots(value.customRoots);
1206
1263
  const customKeys = new Set(normalized.customRoots.map((root) => root.key));
1207
1264
  for (const root of userRoots()) {
1208
- if (root.key !== "dsh" &&
1265
+ // 默认来源不接收 sources 标志:旧版本写过的 `sources.hub = false` 在这里被丢弃,
1266
+ // 于是「曾经把 hub 停用掉」的用户升级后自动回到必须读取的状态(技能不丢,只是策略位作废)。
1267
+ if (!isDefaultSkillSource(root.key) &&
1209
1268
  value.sources &&
1210
1269
  typeof value.sources[root.key] === "boolean")
1211
1270
  normalized.sources[root.key] = value.sources[root.key];
@@ -1264,8 +1323,7 @@ function normalizeManagerState(value) {
1264
1323
  normalized.removedSources = [
1265
1324
  ...new Set(raw.filter((key) => typeof key === "string" &&
1266
1325
  known.has(key) &&
1267
- key !== "dsh" &&
1268
- key !== "hub")),
1326
+ !isDefaultSkillSource(key))),
1269
1327
  ].sort();
1270
1328
  }
1271
1329
  return normalized;
@@ -1393,7 +1451,7 @@ function managerSkillOverride(policy, rootKey, name, policyAliases = []) {
1393
1451
  }
1394
1452
  function effectiveSkillPolicy(policyResult, root, entry) {
1395
1453
  const override = managerSkillOverride(policyResult.state, root.key, entry.name, entry.policyAliases);
1396
- const sourceEnabled = root.key === "dsh" ||
1454
+ const sourceEnabled = isDefaultSkillSource(root) ||
1397
1455
  root.scope === "project" ||
1398
1456
  policyResult.state.sources[root.key] !== false;
1399
1457
  if (policyResult.writable === false || !sourceEnabled || override === false) {
@@ -1432,13 +1490,21 @@ function invalidManagerStateWrite() {
1432
1490
  error: `技能管理器状态文件不可读,已拒绝覆盖: ${managerStatePath()}`,
1433
1491
  };
1434
1492
  }
1493
+ /**
1494
+ * 停用 / 启用一个来源(仍然扫描目录、仍然列出技能,只是不可调用)。
1495
+ *
1496
+ * 默认来源(dsh / hub)不允许停用 —— 停用等于「不读取」,而它们必须读取。
1497
+ * 其余来源(外部 Agent、自定义绝对路径)随来源开关。
1498
+ */
1435
1499
  export async function setSourceEnabled(rootOrKey, enabled, log) {
1436
1500
  // 支持传 key(静态来源)或 definition 对象(自定义来源由 service 层从状态文件生成)。
1437
1501
  const root = rootOrKey && typeof rootOrKey === "object" && typeof rootOrKey.key === "string"
1438
1502
  ? rootOrKey
1439
1503
  : rootByKey(rootOrKey);
1440
- if (!root || root.key === "dsh" || !root.toggleable)
1504
+ if (!root || !root.toggleable)
1441
1505
  return readonlyError("toggle");
1506
+ if (isDefaultSkillSource(root))
1507
+ return reservedSourceError(root);
1442
1508
  const current = await readManagerState();
1443
1509
  if (current.writable === false)
1444
1510
  return invalidManagerStateWrite();
@@ -1457,7 +1523,7 @@ export async function setSourceEnabled(rootOrKey, enabled, log) {
1457
1523
  *
1458
1524
  * 与 `setSourceEnabled(false)` 的区别:停用仍然读目录、仍然列出技能(只是不可调用);
1459
1525
  * 移除是"当它不存在"。`dsh`(官方技能目录)与 `hub`(导入技能落点)不允许移除 ——
1460
- * 它们是插件自身的读写根,移除会让创建/导入无处落脚。
1526
+ * 它们是插件自身的读写根,移除会让创建/导入无处落脚(见 isDefaultSkillSource)。
1461
1527
  */
1462
1528
  export async function setSourceRemoved(rootOrKey, removed, log) {
1463
1529
  const root = rootOrKey && typeof rootOrKey === "object" && typeof rootOrKey.key === "string"
@@ -1465,15 +1531,8 @@ export async function setSourceRemoved(rootOrKey, removed, log) {
1465
1531
  : rootByKey(rootOrKey);
1466
1532
  if (!root)
1467
1533
  return readonlyError(removed === true ? "remove" : "restore");
1468
- if (root.key === "dsh" || root.key === "hub")
1469
- return {
1470
- ok: false,
1471
- code: "error.source.reserved",
1472
- params: { root: root.key },
1473
- error: root.key === "dsh"
1474
- ? "DSH 技能目录是官方来源,不能从管理器移除"
1475
- : "导入技能目录是插件自身的读写落点,不能移除",
1476
- };
1534
+ if (isDefaultSkillSource(root))
1535
+ return reservedSourceError(root);
1477
1536
  if (root.scope === "project")
1478
1537
  return readonlyError("remove");
1479
1538
  const current = await readManagerState();
@@ -1718,9 +1777,16 @@ async function publishTrashStage(stage, finalPath, metadata, renameOptions) {
1718
1777
  }
1719
1778
  return { fallback: true, cleanupError };
1720
1779
  }
1780
+ /**
1781
+ * 回收站条目的来源元数据 —— 恢复时靠它把技能放回原处。
1782
+ *
1783
+ * 用户级可写来源不止 dsh 一个(hub 也是,而且它是「导入技能」页删除的主战场),
1784
+ * 所以这里必须按 scope 分岔:早先只特判 `dsh`,hub 会被记成项目级、`projectRoot`
1785
+ * 为 undefined,恢复时被判「原项目当前不在活动工作区中」—— 删除进去就再也拿不回来。
1786
+ */
1721
1787
  function trashRootMetadata(definition) {
1722
- if (definition.key === "dsh")
1723
- return { key: "dsh", scope: "user", label: definition.label };
1788
+ if (definition.scope !== "project")
1789
+ return { key: definition.key, scope: "user", label: definition.label };
1724
1790
  return {
1725
1791
  key: definition.key,
1726
1792
  scope: "project",
@@ -1734,8 +1800,10 @@ async function restoreRootDefinition(metadata, options = {}) {
1734
1800
  // version 1 entries predate scoped Trash and always belong to $DSH_HOME/skills.
1735
1801
  if (!metadata.root)
1736
1802
  return rootByKey("dsh");
1737
- if (metadata.root.scope === "user" && metadata.root.key === "dsh")
1738
- return rootByKey("dsh");
1803
+ // 用户级来源按 key 重新解析:路径来自 userRoots(),不信元数据里记住的 path
1804
+ // (DSH_HOME 换过之后,旧路径可能已经不在读取范围内了)。
1805
+ if (metadata.root.scope === "user")
1806
+ return rootByKey(metadata.root.key);
1739
1807
  if (metadata.root.scope !== "project" ||
1740
1808
  metadata.root.kind !== "project-dsh" ||
1741
1809
  typeof metadata.root.key !== "string" ||
@@ -1769,8 +1837,8 @@ export async function deleteSkill(root, name, log, options = {}) {
1769
1837
  return definition;
1770
1838
  if (!definition)
1771
1839
  return readonlyError("delete");
1772
- // 用户级来源(dsh / hub)**不可删**:技能只能停用。删除会把用户自己放进去的技能
1773
- // 从磁盘上搬走,代价远大于收益;项目级来源(本仓库自己的 .dsh/skills)才允许删。
1840
+ // 删除只对插件可管理的来源开放(用户级 dsh / hub、项目级 .dsh/skills);外部 Agent
1841
+ // 目录与自定义绝对路径来源只读,返回明确错误而不是"假装删了"。
1774
1842
  if (definition.deletable !== true)
1775
1843
  return notDeletableError(definition);
1776
1844
  const resolved = await resolveEntry(definition, name);
@@ -2486,8 +2554,19 @@ export async function createSkill(input, log, options = {}) {
2486
2554
  const definition = await checkedWritableRootDefinition(requestedRoot);
2487
2555
  if (definition && definition.ok === false)
2488
2556
  return definition;
2489
- if (!definition)
2557
+ if (!definition) {
2558
+ // 区分「来源根本不存在」与「来源存在但只读」:旧实现两种都回
2559
+ // 「该技能来源不允许启用或停用」,创建失败时这条提示既指错动作又指错原因。
2560
+ if (!rootDefinition(requestedRoot)) {
2561
+ return {
2562
+ ok: false,
2563
+ code: "error.root.unknown",
2564
+ params: { root: String(requestedRoot == null ? "" : requestedRoot) },
2565
+ error: `技能来源不存在:${requestedRoot == null || requestedRoot === "" ? "(空)" : requestedRoot}`,
2566
+ };
2567
+ }
2490
2568
  return readonlyError("create");
2569
+ }
2491
2570
  const root = definition.path;
2492
2571
  const requestedName = String((input && input.name) || "").trim();
2493
2572
  const name = toKebab(requestedName);
@@ -2831,7 +2910,11 @@ export async function state(options = {}) {
2831
2910
  label: root.label,
2832
2911
  mutable: root.mutable,
2833
2912
  deletable: root.deletable === true,
2834
- removable: root.key !== "dsh" && root.key !== "hub" && root.scope !== "project",
2913
+ removable: !isDefaultSkillSource(root) && root.scope !== "project",
2914
+ // 自定义来源(用户手加的目录):只有它能「永久删除」——按约定发现的来源删不掉。
2915
+ custom: root.custom === true,
2916
+ // 默认来源(dsh / hub):界面据此隐藏来源开关、只显示「可管理」标记。
2917
+ defaultSource: isDefaultSkillSource(root),
2835
2918
  toggleable: root.toggleable,
2836
2919
  native: root.native,
2837
2920
  rank: root.rank,
@@ -2891,7 +2974,8 @@ export async function state(options = {}) {
2891
2974
  path: root.path,
2892
2975
  label: root.label,
2893
2976
  mutable: root.mutable,
2894
- // 是否提供「删除」:只有项目级 DSH 根为 true。用户级 dsh / hub 可写但不可删。
2977
+ // 是否提供「删除」:dsh / hub / 项目级 .dsh/skills 为 true;
2978
+ // 外部 Agent 与自定义绝对路径来源为 false(只读)。
2895
2979
  deletable: root.deletable === true,
2896
2980
  toggleable: root.toggleable,
2897
2981
  native: root.native,
@@ -2907,11 +2991,15 @@ export async function state(options = {}) {
2907
2991
  exists,
2908
2992
  truncated: truncated === true,
2909
2993
  // 界面用:能否从管理器「移除」(不再读取)。dsh / hub / 项目级不可移除。
2910
- removable: root.key !== "dsh" && root.key !== "hub" && root.scope !== "project",
2994
+ removable: !isDefaultSkillSource(root) && root.scope !== "project",
2995
+ // 自定义来源(用户手加的目录):只有它能「永久删除」——按约定发现的来源删不掉。
2996
+ custom: root.custom === true,
2997
+ // 默认来源(dsh / hub):界面据此隐藏来源开关(它们不能停用),换成「可管理」标记。
2998
+ defaultSource: isDefaultSkillSource(root),
2911
2999
  removed: false,
2912
3000
  enabled: policyResult.writable !== false &&
2913
3001
  (root.scope === "project" ||
2914
- root.key === "dsh" ||
3002
+ isDefaultSkillSource(root) ||
2915
3003
  policyResult.state.sources[root.key] !== false),
2916
3004
  skills,
2917
3005
  });
@@ -397,6 +397,9 @@ export function createSkillsService(ctx) {
397
397
  // 写操作清单:与下方 ops 表同文件同源维护;HTTP 端门禁由 index.ts 从本集合派生,勿在宿主端另抄一份。
398
398
  const writeOps = new Set([
399
399
  'skill-enable', 'skill-disable', 'skill-source-enable', 'skill-source-disable',
400
+ // 移除/恢复来源会改写本地来源状态(listProviderCandidates 随之变化,模型侧的技能目录
401
+ // 也变),是写操作;漏在这份清单里 = 配了访问令牌也不会被要求带令牌。
402
+ 'skill-source-remove', 'skill-source-restore',
400
403
  'skill-prefer', 'skill-unprefer',
401
404
  'skill-create', 'skill-import', 'skill-upload', 'skill-delete',
402
405
  'skill-trash-restore', 'skill-trash-delete', 'skill-custom-add', 'skill-custom-remove',
@@ -424,7 +427,12 @@ export function createSkillsService(ctx) {
424
427
  // 创建 / 导入 / 删除 / 回收站
425
428
  // 创建默认落 hub(`tool-management/skills/`):UI 不传 root 时走这里;
426
429
  // core.js 的 createSkill 默认值是第二道保险(同口径)。官方 `~/.dsh/skills/` 仍作为来源列出。
427
- 'skill-create': wrap((args) => write(async () => createSkill({ name: args.name, description: args.description, body: args.body }, log, { root: await requestRoot(String(args.root || 'hub')) })), afterWrite),
430
+ 'skill-create': wrap((args) => write(async () => {
431
+ // requestRoot 未命中时回传原始 key,让 core 的 error.root.unknown 带上来源名
432
+ // (否则传了不存在的 root 会收到「技能来源不存在:(空)」这种指错方向的提示)。
433
+ const key = String(args.root || 'hub');
434
+ return createSkill({ name: args.name, description: args.description, body: args.body }, log, { root: (await requestRoot(key)) || key });
435
+ }), afterWrite),
428
436
  'skill-import': wrap((args) => write(() => importSkill(String(args.source || ''), log, {
429
437
  conflict: args.conflict === 'overwrite' ? 'overwrite' : 'skip',
430
438
  dryRun: args.dryRun === true,