@max-null/dsh-skill-mcp-center 0.6.0 → 0.6.2

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/client.js CHANGED
@@ -31,6 +31,17 @@ __export(index_exports, {
31
31
  });
32
32
  module.exports = __toCommonJS(index_exports);
33
33
  var import_react = require("react");
34
+
35
+ // client/skill-key.ts
36
+ var RUNTIME_KEY_PREFIX = "\0runtime:";
37
+ function skillKeyOf(skill) {
38
+ return skill.path === "" ? `${RUNTIME_KEY_PREFIX}${skill.name}` : skill.path;
39
+ }
40
+ function hasSkillFile(skill) {
41
+ return skill.path !== "";
42
+ }
43
+
44
+ // client/index.tsx
34
45
  var import_jsx_runtime = require("react/jsx-runtime");
35
46
  var CSS = `
36
47
  .smc-title { font-size: 18px; font-weight: 600; line-height: 26px; color: var(--dsw-alias-label-primary); }
@@ -81,6 +92,10 @@ var CSS = `
81
92
  .smc-toggle::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: left .15s; }
82
93
  .smc-toggle.on::after { left: 21px; }
83
94
  .smc-toggle:disabled { opacity: 0.4; cursor: default; }
95
+ .smc-skill-group { margin-bottom: 10px; }
96
+ .smc-skill-group-head { display: flex; align-items: baseline; gap: 6px; padding: 6px 2px 4px; font-size: 11px; font-weight: 600; letter-spacing: .04em; color: var(--dsw-alias-label-caption); }
97
+ .smc-skill-group-count { font-weight: 400; opacity: .7; }
98
+ .smc-skill-group-note { padding: 0 2px 6px; font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-caption); }
84
99
 
85
100
  .smc-input { height: 34px; padding: 0 12px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; outline: none; font-family: inherit; }
86
101
  .smc-input:focus { border-color: var(--dsw-alias-brand-primary); }
@@ -236,6 +251,11 @@ var zhDict = {
236
251
  groupGlobal: "\u5168\u5C40",
237
252
  groupWorkspace: "\u5DE5\u4F5C\u533A",
238
253
  groupBundled: "\u5185\u7F6E\uFF08DSH \u5B98\u65B9\uFF09",
254
+ groupProject: "\u9879\u76EE\u7EA7",
255
+ groupUser: "\u7528\u6237\u7EA7",
256
+ groupPlugin: "\u63D2\u4EF6\u81EA\u5E26",
257
+ groupRuntime: "\u4EE3\u7801\u6CE8\u518C",
258
+ groupRuntimeNote: "\u7531\u63D2\u4EF6\u5728\u8FD0\u884C\u65F6\u6CE8\u518C\uFF0C\u4E0D\u53EF\u5728\u6B64\u7981\u7528",
239
259
  readOnlyBadge: "\u5185\u7F6E\u53EA\u8BFB",
240
260
  noMatch: "\u6CA1\u6709\u5339\u914D\u7684 skill",
241
261
  detailPath: "\u8DEF\u5F84",
@@ -246,6 +266,7 @@ var zhDict = {
246
266
  nsWorkspace: "\u5DE5\u4F5C\u533A",
247
267
  noWorkspace: "\u672A\u9009\u62E9\u5DE5\u4F5C\u533A",
248
268
  viewMd: "\u67E5\u770B SKILL.md",
269
+ noSkillFile: "\u65E0 SKILL.md",
249
270
  hideMd: "\u6536\u8D77",
250
271
  mdLoadFailed: "\u8BFB\u53D6\u5931\u8D25\uFF1A{e}",
251
272
  refOpen: "\u6253\u5F00\u6587\u4EF6\u5F15\u7528",
@@ -295,6 +316,11 @@ var enDict = {
295
316
  groupGlobal: "Global",
296
317
  groupWorkspace: "Workspace",
297
318
  groupBundled: "Built-in (DSH official)",
319
+ groupProject: "Project",
320
+ groupUser: "User",
321
+ groupPlugin: "Plugin-bundled",
322
+ groupRuntime: "Code-registered",
323
+ groupRuntimeNote: "Registered by a plugin at runtime; cannot be disabled here",
298
324
  readOnlyBadge: "built-in read-only",
299
325
  noMatch: "No matching skills",
300
326
  detailPath: "path",
@@ -305,6 +331,7 @@ var enDict = {
305
331
  nsWorkspace: "Workspace",
306
332
  noWorkspace: "No workspace selected",
307
333
  viewMd: "View SKILL.md",
334
+ noSkillFile: "no SKILL.md",
308
335
  hideMd: "Hide",
309
336
  mdLoadFailed: "Failed to read: {e}",
310
337
  refOpen: "Open file reference",
@@ -456,13 +483,14 @@ function SkillView() {
456
483
  );
457
484
  };
458
485
  const openMd = (s) => {
459
- if (mdPath === s.path) {
486
+ if (!hasSkillFile(s)) return;
487
+ if (mdPath === skillKeyOf(s)) {
460
488
  setMdPath(null);
461
489
  setMdText(null);
462
490
  setMdError(null);
463
491
  return;
464
492
  }
465
- setMdPath(s.path);
493
+ setMdPath(skillKeyOf(s));
466
494
  setMdText(null);
467
495
  setMdError(null);
468
496
  void rpc("readSkill", { path: s.path }).then(
@@ -484,7 +512,7 @@ function SkillView() {
484
512
  else other.push(s);
485
513
  }
486
514
  const renderCard = (s) => {
487
- const open = expanded === s.path;
515
+ const open = expanded === skillKeyOf(s);
488
516
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card", children: [
489
517
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card-head", children: [
490
518
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -495,7 +523,7 @@ function SkillView() {
495
523
  "aria-expanded": open,
496
524
  title: t("clickForDetail"),
497
525
  onClick: () => {
498
- setExpanded(open ? null : s.path);
526
+ setExpanded(open ? null : skillKeyOf(s));
499
527
  },
500
528
  children: [
501
529
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-card-name", children: s.name }),
@@ -529,9 +557,9 @@ function SkillView() {
529
557
  !s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("modelDisabled") }),
530
558
  !s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("readOnlyBadge") }),
531
559
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
532
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: () => {
560
+ !hasSkillFile(s) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("noSkillFile") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: () => {
533
561
  openMd(s);
534
- }, children: mdPath === s.path ? t("hideMd") : t("viewMd") }),
562
+ }, children: mdPath === skillKeyOf(s) ? t("hideMd") : t("viewMd") }),
535
563
  s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
536
564
  "button",
537
565
  {
@@ -548,11 +576,11 @@ function SkillView() {
548
576
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail-row", children: [
549
577
  t("detailPath"),
550
578
  "\uFF1A",
551
- s.path
579
+ hasSkillFile(s) ? s.path : t("noSkillFile")
552
580
  ] }),
553
- mdPath === s.path && (mdError !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md-err", children: t("mdLoadFailed", { e: mdError }) }) : mdText === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: t("loading") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MdWithRefs, { text: mdText }) }))
581
+ mdPath === skillKeyOf(s) && (mdError !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md-err", children: t("mdLoadFailed", { e: mdError }) }) : mdText === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: t("loading") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MdWithRefs, { text: mdText }) }))
554
582
  ] })
555
- ] }, s.path);
583
+ ] }, skillKeyOf(s));
556
584
  };
557
585
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
558
586
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-toolbar", children: [
@@ -835,6 +863,21 @@ function sourceLabel(source, short = false) {
835
863
  const name = pkg.split("/").pop() ?? pkg;
836
864
  return short ? name : `plugin \xB7 ${name}`;
837
865
  }
866
+ var SKILL_GROUP_ORDER = ["project", "user", "plugin", "bundled", "runtime"];
867
+ var SKILL_GROUP_LABEL = {
868
+ project: "groupProject",
869
+ user: "groupUser",
870
+ plugin: "groupPlugin",
871
+ bundled: "groupBundled",
872
+ runtime: "groupRuntime"
873
+ };
874
+ function skillGroupOf(source) {
875
+ if (source === "project-dsh" || source === "project-agents") return "project";
876
+ if (source === "user-dsh" || source === "user-agents") return "user";
877
+ if (source.startsWith("plugin:")) return "plugin";
878
+ if (source === "bundled") return "bundled";
879
+ return "runtime";
880
+ }
838
881
  function skillInNamespace(s, ns) {
839
882
  if (ns === "all") return true;
840
883
  if (ns === "global") {
@@ -877,6 +920,7 @@ function SidebarSkillTab({ visible, cwd }) {
877
920
  );
878
921
  };
879
922
  const openMd = (path) => {
923
+ if (path === "") return;
880
924
  if (mdPath === path) {
881
925
  setMdPath(null);
882
926
  setMdText(null);
@@ -927,57 +971,64 @@ function SidebarSkillTab({ visible, cwd }) {
927
971
  k
928
972
  )) })
929
973
  ] }),
930
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar-list", children: noWorkspace ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noWorkspace") }) : visibleItems.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: q !== "" ? t("noMatch") : t("noSkills") }) : visibleItems.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
931
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `${s.description}
932
- ${s.path}`, children: [
933
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
934
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
935
- !s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("modelDisabled") }),
936
- !s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: sourceLabel(s.source, true) }),
937
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
938
- "button",
939
- {
940
- type: "button",
941
- className: "smc-btn",
942
- style: { height: 20, padding: "0 6px", fontSize: 11 },
943
- onClick: () => {
944
- openMd(s.path);
945
- },
946
- "aria-label": mdPath === s.path ? t("hideMd") : t("viewMd"),
947
- children: mdPath === s.path ? t("hideMd") : "MD"
948
- }
949
- ),
950
- s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
951
- "button",
952
- {
953
- type: "button",
954
- className: "smc-btn danger",
955
- style: { height: 20, padding: "0 6px", fontSize: 11 },
956
- onClick: () => {
957
- removeSkill(s);
958
- },
959
- "aria-label": t("deleteSkill"),
960
- children: t("remove")
961
- }
962
- ),
963
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
964
- "button",
965
- {
966
- type: "button",
967
- className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
968
- title: t("toggleModelVisible"),
969
- onClick: () => {
970
- void rpc("toggleSkill", { path: s.path }).then(() => {
971
- load();
972
- });
973
- },
974
- "aria-label": s.modelInvocable ? t("disable") : t("enable")
975
- }
976
- )
974
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar-list", children: noWorkspace ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noWorkspace") }) : visibleItems.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: q !== "" ? t("noMatch") : t("noSkills") }) : SKILL_GROUP_ORDER.map((kind) => ({ kind, items: visibleItems.filter((s) => skillGroupOf(s.source) === kind) })).filter((group) => group.items.length > 0).map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-skill-group", children: [
975
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-skill-group-head", children: [
976
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t(SKILL_GROUP_LABEL[group.kind]) }),
977
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-skill-group-count", children: group.items.length })
977
978
  ] }),
978
- s.description !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-srv-desc", children: s.description }),
979
- mdPath === s.path && (mdError !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md-err", children: t("mdLoadFailed", { e: mdError }) }) : mdText === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: t("loading") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MdWithRefs, { text: mdText }) }))
980
- ] }, s.path)) })
979
+ group.kind === "runtime" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-skill-group-note", children: t("groupRuntimeNote") }),
980
+ group.items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
981
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `${s.description}
982
+ ${s.path}`, children: [
983
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
984
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
985
+ !s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("modelDisabled") }),
986
+ !s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: sourceLabel(s.source, true) }),
987
+ hasSkillFile(s) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
988
+ "button",
989
+ {
990
+ type: "button",
991
+ className: "smc-btn",
992
+ style: { height: 20, padding: "0 6px", fontSize: 11 },
993
+ onClick: () => {
994
+ openMd(s.path);
995
+ },
996
+ "aria-label": mdPath === s.path ? t("hideMd") : t("viewMd"),
997
+ children: mdPath === s.path ? t("hideMd") : "MD"
998
+ }
999
+ ),
1000
+ s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1001
+ "button",
1002
+ {
1003
+ type: "button",
1004
+ className: "smc-btn danger",
1005
+ style: { height: 20, padding: "0 6px", fontSize: 11 },
1006
+ onClick: () => {
1007
+ removeSkill(s);
1008
+ },
1009
+ "aria-label": t("deleteSkill"),
1010
+ children: t("remove")
1011
+ }
1012
+ ),
1013
+ s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1014
+ "button",
1015
+ {
1016
+ type: "button",
1017
+ className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
1018
+ title: t("toggleModelVisible"),
1019
+ onClick: () => {
1020
+ void rpc("toggleSkill", { path: s.path }).then(() => {
1021
+ load();
1022
+ });
1023
+ },
1024
+ "aria-label": s.modelInvocable ? t("disable") : t("enable")
1025
+ }
1026
+ )
1027
+ ] }),
1028
+ s.description !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-srv-desc", children: s.description }),
1029
+ mdPath === s.path && (mdError !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md-err", children: t("mdLoadFailed", { e: mdError }) }) : mdText === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: t("loading") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MdWithRefs, { text: mdText }) }))
1030
+ ] }, skillKeyOf(s)))
1031
+ ] }, group.kind)) })
981
1032
  ] });
982
1033
  }
983
1034
  var inject = ["slots", "connection", "locale"];
package/dist/service.d.ts CHANGED
@@ -163,6 +163,16 @@ export declare class SkillMcpService extends Service {
163
163
  * bundled inside loaded plugin packages, and any configured official roots.
164
164
  */
165
165
  listSkills(cwd?: string): Promise<SkillView[]>;
166
+ /**
167
+ * 补上「运行时注册」的 skill:`ctx.skills.register()` 注册的条目没有 SKILL.md,
168
+ * 磁盘扫描(本项目的主要取数方式)看不到它们——dsh-jubensha 的 DM 手册、office、
169
+ * genui 都属于这一类。用户因此报了「面板少了很多 skill」。
170
+ *
171
+ * skills 服务是**可选**的:web-app 宿主把它关掉(presets 拥有发现,没有全局层可列),
172
+ * 所以这里 feature-detect,拿不到就原样返回空,不改变既有行为。
173
+ * 这些条目没有可写的 SKILL.md,标成 `writable: false`——开关对它们没有意义。
174
+ */
175
+ private runtimeOnlySkills;
166
176
  /** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
167
177
  toggleSkill(path: string): Promise<SkillView>;
168
178
  /**
package/dist/service.js CHANGED
@@ -268,8 +268,48 @@ export class SkillMcpService extends Service {
268
268
  for (const dir of this.officialSkillDirs) {
269
269
  skills.push(...await scanSkillRoot(dir, 'bundled', false, 'dsh-official'));
270
270
  }
271
+ skills.push(...await this.runtimeOnlySkills(skills));
271
272
  return skills;
272
273
  }
274
+ /**
275
+ * 补上「运行时注册」的 skill:`ctx.skills.register()` 注册的条目没有 SKILL.md,
276
+ * 磁盘扫描(本项目的主要取数方式)看不到它们——dsh-jubensha 的 DM 手册、office、
277
+ * genui 都属于这一类。用户因此报了「面板少了很多 skill」。
278
+ *
279
+ * skills 服务是**可选**的:web-app 宿主把它关掉(presets 拥有发现,没有全局层可列),
280
+ * 所以这里 feature-detect,拿不到就原样返回空,不改变既有行为。
281
+ * 这些条目没有可写的 SKILL.md,标成 `writable: false`——开关对它们没有意义。
282
+ */
283
+ async runtimeOnlySkills(diskBacked) {
284
+ const skills = this.ctx.get('skills');
285
+ if (skills === undefined)
286
+ return [];
287
+ let catalog;
288
+ try {
289
+ catalog = await skills.list();
290
+ }
291
+ catch {
292
+ // 目录服务不可用不该让整个面板失败——磁盘那部分仍然有价值。
293
+ return [];
294
+ }
295
+ const known = new Set(diskBacked.map(skill => skill.name));
296
+ const extra = [];
297
+ for (const entry of catalog) {
298
+ if (known.has(entry.name))
299
+ continue;
300
+ extra.push({
301
+ name: entry.name,
302
+ description: entry.description,
303
+ source: 'runtime',
304
+ provider: entry.provider,
305
+ modelInvocable: entry.invocation.modelInvocable,
306
+ userInvocable: entry.invocation.userInvocable,
307
+ writable: false,
308
+ path: '',
309
+ });
310
+ }
311
+ return extra;
312
+ }
273
313
  /** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
274
314
  async toggleSkill(path) {
275
315
  let text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-skill-mcp-center",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Skill & MCP management center for DeepSeek Harness — manage skills and MCP servers in Settings, live MCP status in the sidebar",
5
5
  "license": "MIT",
6
6
  "publishConfig": {