@max-null/dsh-skill-mcp-center 0.5.4 → 0.6.1
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/README.md +1 -1
- package/client.js +136 -40
- package/dist/rpc.js +7 -0
- package/dist/service.d.ts +43 -0
- package/dist/service.js +116 -2
- package/package.json +1 -1
- package/release-notes.md +24 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Skill 与 MCP 管理中心:在设置里管理 skills 与 MCP 服务器,右
|
|
|
10
10
|
|
|
11
11
|
## Features / 功能
|
|
12
12
|
|
|
13
|
-
- **Skill management / Skill 管理** — browse every skill grouped by source (Global / Workspace / Built-in DSH official, plus one group per plugin that ships skills), toggle model invocation via the `disable-model-invocation` frontmatter (disk-backed skills only). Covers skills shipped **inside plugin packages** (e.g. `@max-null/dsh-skills`), not just the user-level roots — the host-level skill filesystem is disabled in web-app, so a plugin's own `skills/` directory would otherwise be invisible while its skills are loaded and in effect. Also covers a plugin that mounts `@deepseek-ai/dsh-skill-filesystem` under an id of its own and points `bundledSkillDir` at its skills (e.g. `dsh-plugin-zhihu-search`), including when the patch computes that path with a `!!js` expression — such a path is read from the entry's fiber, where the Loader has already evaluated it.
|
|
13
|
+
- **Skill management / Skill 管理** — browse every skill grouped by source (Global / Workspace / Built-in DSH official, plus one group per plugin that ships skills), toggle model invocation via the `disable-model-invocation` frontmatter (disk-backed skills only). Covers skills shipped **inside plugin packages** (e.g. `@max-null/dsh-skills`), not just the user-level roots — the host-level skill filesystem is disabled in web-app, so a plugin's own `skills/` directory would otherwise be invisible while its skills are loaded and in effect. Also covers a plugin that mounts `@deepseek-ai/dsh-skill-filesystem` under an id of its own and points `bundledSkillDir` at its skills (e.g. `dsh-plugin-zhihu-search`), including when the patch computes that path with a `!!js` expression — such a path is read from the entry's fiber, where the Loader has already evaluated it. **Deleting** a user-level or project-level skill is offered on the same rows, behind a confirmation that names the skill and its path; the entry is *moved* to `~/.dsh/.skill-trash/` rather than destroyed, so a mistaken deletion stays recoverable. Plugin and built-in skills stay read-only.
|
|
14
14
|
- **MCP management / MCP 管理** — add / edit / remove `mcp-client` servers, enable/disable without deleting config, all **hot-applied** through `ctx.loader` (no restart).
|
|
15
15
|
- **Live status / 实时状态** — a sidebar "MCP" tab (via `dsh-better-sidebar`) showing per-server connection state; **click a server row to expand its tools' names and descriptions** (collapsed by default — a server can expose dozens of them). Names come from `tools.schemas()`; the redundant `mcp__<server>__` prefix is hidden while the full name stays on hover. Polled while visible and following the session.
|
|
16
16
|
- **Skin-compatible / 皮肤兼容** — every color uses `var(--dsw-*)` tokens.
|
package/client.js
CHANGED
|
@@ -81,6 +81,10 @@ var CSS = `
|
|
|
81
81
|
.smc-toggle::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: left .15s; }
|
|
82
82
|
.smc-toggle.on::after { left: 21px; }
|
|
83
83
|
.smc-toggle:disabled { opacity: 0.4; cursor: default; }
|
|
84
|
+
.smc-skill-group { margin-bottom: 10px; }
|
|
85
|
+
.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); }
|
|
86
|
+
.smc-skill-group-count { font-weight: 400; opacity: .7; }
|
|
87
|
+
.smc-skill-group-note { padding: 0 2px 6px; font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-caption); }
|
|
84
88
|
|
|
85
89
|
.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
90
|
.smc-input:focus { border-color: var(--dsw-alias-brand-primary); }
|
|
@@ -236,6 +240,11 @@ var zhDict = {
|
|
|
236
240
|
groupGlobal: "\u5168\u5C40",
|
|
237
241
|
groupWorkspace: "\u5DE5\u4F5C\u533A",
|
|
238
242
|
groupBundled: "\u5185\u7F6E\uFF08DSH \u5B98\u65B9\uFF09",
|
|
243
|
+
groupProject: "\u9879\u76EE\u7EA7",
|
|
244
|
+
groupUser: "\u7528\u6237\u7EA7",
|
|
245
|
+
groupPlugin: "\u63D2\u4EF6\u81EA\u5E26",
|
|
246
|
+
groupRuntime: "\u4EE3\u7801\u6CE8\u518C",
|
|
247
|
+
groupRuntimeNote: "\u7531\u63D2\u4EF6\u5728\u8FD0\u884C\u65F6\u6CE8\u518C\uFF0C\u4E0D\u53EF\u5728\u6B64\u7981\u7528",
|
|
239
248
|
readOnlyBadge: "\u5185\u7F6E\u53EA\u8BFB",
|
|
240
249
|
noMatch: "\u6CA1\u6709\u5339\u914D\u7684 skill",
|
|
241
250
|
detailPath: "\u8DEF\u5F84",
|
|
@@ -248,7 +257,11 @@ var zhDict = {
|
|
|
248
257
|
viewMd: "\u67E5\u770B SKILL.md",
|
|
249
258
|
hideMd: "\u6536\u8D77",
|
|
250
259
|
mdLoadFailed: "\u8BFB\u53D6\u5931\u8D25\uFF1A{e}",
|
|
251
|
-
refOpen: "\u6253\u5F00\u6587\u4EF6\u5F15\u7528"
|
|
260
|
+
refOpen: "\u6253\u5F00\u6587\u4EF6\u5F15\u7528",
|
|
261
|
+
deleteSkill: "\u5220\u9664\u6280\u80FD",
|
|
262
|
+
deleteConfirm: "\u5220\u9664\u6280\u80FD\u300C{name}\u300D\uFF1F\n\n{path}\n\n\u5B83\u4F1A\u88AB\u79FB\u5230\u56DE\u6536\u76EE\u5F55\uFF08~/.dsh/.skill-trash\uFF09\uFF0C\u9700\u8981\u65F6\u53EF\u4EE5\u624B\u52A8\u79FB\u56DE\u53BB\u3002",
|
|
263
|
+
skillDeleted: "\u5DF2\u5220\u9664 {name}\uFF0C\u79FB\u5230 {path}",
|
|
264
|
+
deleteFailed: "\u5220\u9664\u5931\u8D25\uFF1A{e}"
|
|
252
265
|
};
|
|
253
266
|
var enDict = {
|
|
254
267
|
loading: "Loading\u2026",
|
|
@@ -291,6 +304,11 @@ var enDict = {
|
|
|
291
304
|
groupGlobal: "Global",
|
|
292
305
|
groupWorkspace: "Workspace",
|
|
293
306
|
groupBundled: "Built-in (DSH official)",
|
|
307
|
+
groupProject: "Project",
|
|
308
|
+
groupUser: "User",
|
|
309
|
+
groupPlugin: "Plugin-bundled",
|
|
310
|
+
groupRuntime: "Code-registered",
|
|
311
|
+
groupRuntimeNote: "Registered by a plugin at runtime; cannot be disabled here",
|
|
294
312
|
readOnlyBadge: "built-in read-only",
|
|
295
313
|
noMatch: "No matching skills",
|
|
296
314
|
detailPath: "path",
|
|
@@ -303,7 +321,11 @@ var enDict = {
|
|
|
303
321
|
viewMd: "View SKILL.md",
|
|
304
322
|
hideMd: "Hide",
|
|
305
323
|
mdLoadFailed: "Failed to read: {e}",
|
|
306
|
-
refOpen: "Open file reference"
|
|
324
|
+
refOpen: "Open file reference",
|
|
325
|
+
deleteSkill: "Delete skill",
|
|
326
|
+
deleteConfirm: 'Delete skill "{name}"?\n\n{path}\n\nIt will be moved to ~/.dsh/.skill-trash and can be moved back manually if needed.',
|
|
327
|
+
skillDeleted: "Deleted {name}, moved to {path}",
|
|
328
|
+
deleteFailed: "Delete failed: {e}"
|
|
307
329
|
};
|
|
308
330
|
var rpc = async () => {
|
|
309
331
|
throw new Error("skill-mcp-center: rpc not wired");
|
|
@@ -432,6 +454,21 @@ function SkillView() {
|
|
|
432
454
|
}
|
|
433
455
|
);
|
|
434
456
|
};
|
|
457
|
+
const remove = (s) => {
|
|
458
|
+
if (!window.confirm(t("deleteConfirm", { name: s.name, path: s.path }))) return;
|
|
459
|
+
setBusy(s.path);
|
|
460
|
+
void rpc("deleteSkill", { path: s.path }).then(
|
|
461
|
+
(v) => {
|
|
462
|
+
setBusy(null);
|
|
463
|
+
load();
|
|
464
|
+
showToast(t("skillDeleted", { name: s.name, path: v.trashPath }));
|
|
465
|
+
},
|
|
466
|
+
(e) => {
|
|
467
|
+
setBusy(null);
|
|
468
|
+
showToast(t("deleteFailed", { e: e instanceof Error ? e.message : String(e) }), "error");
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
};
|
|
435
472
|
const openMd = (s) => {
|
|
436
473
|
if (mdPath === s.path) {
|
|
437
474
|
setMdPath(null);
|
|
@@ -508,7 +545,19 @@ function SkillView() {
|
|
|
508
545
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
|
|
509
546
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: () => {
|
|
510
547
|
openMd(s);
|
|
511
|
-
}, children: mdPath === s.path ? t("hideMd") : t("viewMd") })
|
|
548
|
+
}, children: mdPath === s.path ? t("hideMd") : t("viewMd") }),
|
|
549
|
+
s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
550
|
+
"button",
|
|
551
|
+
{
|
|
552
|
+
type: "button",
|
|
553
|
+
className: "smc-btn danger",
|
|
554
|
+
disabled: busy === s.path,
|
|
555
|
+
onClick: () => {
|
|
556
|
+
remove(s);
|
|
557
|
+
},
|
|
558
|
+
children: t("deleteSkill")
|
|
559
|
+
}
|
|
560
|
+
)
|
|
512
561
|
] }),
|
|
513
562
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail-row", children: [
|
|
514
563
|
t("detailPath"),
|
|
@@ -800,6 +849,21 @@ function sourceLabel(source, short = false) {
|
|
|
800
849
|
const name = pkg.split("/").pop() ?? pkg;
|
|
801
850
|
return short ? name : `plugin \xB7 ${name}`;
|
|
802
851
|
}
|
|
852
|
+
var SKILL_GROUP_ORDER = ["project", "user", "plugin", "bundled", "runtime"];
|
|
853
|
+
var SKILL_GROUP_LABEL = {
|
|
854
|
+
project: "groupProject",
|
|
855
|
+
user: "groupUser",
|
|
856
|
+
plugin: "groupPlugin",
|
|
857
|
+
bundled: "groupBundled",
|
|
858
|
+
runtime: "groupRuntime"
|
|
859
|
+
};
|
|
860
|
+
function skillGroupOf(source) {
|
|
861
|
+
if (source === "project-dsh" || source === "project-agents") return "project";
|
|
862
|
+
if (source === "user-dsh" || source === "user-agents") return "user";
|
|
863
|
+
if (source.startsWith("plugin:")) return "plugin";
|
|
864
|
+
if (source === "bundled") return "bundled";
|
|
865
|
+
return "runtime";
|
|
866
|
+
}
|
|
803
867
|
function skillInNamespace(s, ns) {
|
|
804
868
|
if (ns === "all") return true;
|
|
805
869
|
if (ns === "global") {
|
|
@@ -829,6 +893,18 @@ function SidebarSkillTab({ visible, cwd }) {
|
|
|
829
893
|
if (!visible) return;
|
|
830
894
|
load();
|
|
831
895
|
}, [visible, load]);
|
|
896
|
+
const removeSkill = (s) => {
|
|
897
|
+
if (!window.confirm(t("deleteConfirm", { name: s.name, path: s.path }))) return;
|
|
898
|
+
void rpc("deleteSkill", { path: s.path, cwd }).then(
|
|
899
|
+
(v) => {
|
|
900
|
+
load();
|
|
901
|
+
showToast(t("skillDeleted", { name: s.name, path: v.trashPath }));
|
|
902
|
+
},
|
|
903
|
+
(e) => {
|
|
904
|
+
showToast(t("deleteFailed", { e: e instanceof Error ? e.message : String(e) }), "error");
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
};
|
|
832
908
|
const openMd = (path) => {
|
|
833
909
|
if (mdPath === path) {
|
|
834
910
|
setMdPath(null);
|
|
@@ -880,44 +956,64 @@ function SidebarSkillTab({ visible, cwd }) {
|
|
|
880
956
|
k
|
|
881
957
|
)) })
|
|
882
958
|
] }),
|
|
883
|
-
/* @__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") }) :
|
|
884
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-
|
|
885
|
-
|
|
886
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className:
|
|
887
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
|
|
888
|
-
!s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("modelDisabled") }),
|
|
889
|
-
!s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: sourceLabel(s.source, true) }),
|
|
890
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
891
|
-
"button",
|
|
892
|
-
{
|
|
893
|
-
type: "button",
|
|
894
|
-
className: "smc-btn",
|
|
895
|
-
style: { height: 20, padding: "0 6px", fontSize: 11 },
|
|
896
|
-
onClick: () => {
|
|
897
|
-
openMd(s.path);
|
|
898
|
-
},
|
|
899
|
-
"aria-label": mdPath === s.path ? t("hideMd") : t("viewMd"),
|
|
900
|
-
children: mdPath === s.path ? t("hideMd") : "MD"
|
|
901
|
-
}
|
|
902
|
-
),
|
|
903
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
904
|
-
"button",
|
|
905
|
-
{
|
|
906
|
-
type: "button",
|
|
907
|
-
className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
|
|
908
|
-
title: t("toggleModelVisible"),
|
|
909
|
-
onClick: () => {
|
|
910
|
-
void rpc("toggleSkill", { path: s.path }).then(() => {
|
|
911
|
-
load();
|
|
912
|
-
});
|
|
913
|
-
},
|
|
914
|
-
"aria-label": s.modelInvocable ? t("disable") : t("enable")
|
|
915
|
-
}
|
|
916
|
-
)
|
|
959
|
+
/* @__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: [
|
|
960
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-skill-group-head", children: [
|
|
961
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t(SKILL_GROUP_LABEL[group.kind]) }),
|
|
962
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-skill-group-count", children: group.items.length })
|
|
917
963
|
] }),
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
964
|
+
group.kind === "runtime" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-skill-group-note", children: t("groupRuntimeNote") }),
|
|
965
|
+
group.items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
966
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `${s.description}
|
|
967
|
+
${s.path}`, children: [
|
|
968
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
|
|
969
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
|
|
970
|
+
!s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("modelDisabled") }),
|
|
971
|
+
!s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: sourceLabel(s.source, true) }),
|
|
972
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
973
|
+
"button",
|
|
974
|
+
{
|
|
975
|
+
type: "button",
|
|
976
|
+
className: "smc-btn",
|
|
977
|
+
style: { height: 20, padding: "0 6px", fontSize: 11 },
|
|
978
|
+
onClick: () => {
|
|
979
|
+
openMd(s.path);
|
|
980
|
+
},
|
|
981
|
+
"aria-label": mdPath === s.path ? t("hideMd") : t("viewMd"),
|
|
982
|
+
children: mdPath === s.path ? t("hideMd") : "MD"
|
|
983
|
+
}
|
|
984
|
+
),
|
|
985
|
+
s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
986
|
+
"button",
|
|
987
|
+
{
|
|
988
|
+
type: "button",
|
|
989
|
+
className: "smc-btn danger",
|
|
990
|
+
style: { height: 20, padding: "0 6px", fontSize: 11 },
|
|
991
|
+
onClick: () => {
|
|
992
|
+
removeSkill(s);
|
|
993
|
+
},
|
|
994
|
+
"aria-label": t("deleteSkill"),
|
|
995
|
+
children: t("remove")
|
|
996
|
+
}
|
|
997
|
+
),
|
|
998
|
+
s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
999
|
+
"button",
|
|
1000
|
+
{
|
|
1001
|
+
type: "button",
|
|
1002
|
+
className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
|
|
1003
|
+
title: t("toggleModelVisible"),
|
|
1004
|
+
onClick: () => {
|
|
1005
|
+
void rpc("toggleSkill", { path: s.path }).then(() => {
|
|
1006
|
+
load();
|
|
1007
|
+
});
|
|
1008
|
+
},
|
|
1009
|
+
"aria-label": s.modelInvocable ? t("disable") : t("enable")
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
] }),
|
|
1013
|
+
s.description !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-srv-desc", children: s.description }),
|
|
1014
|
+
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 }) }))
|
|
1015
|
+
] }, s.path === "" ? `runtime:${s.name}` : s.path))
|
|
1016
|
+
] }, group.kind)) })
|
|
921
1017
|
] });
|
|
922
1018
|
}
|
|
923
1019
|
var inject = ["slots", "connection", "locale"];
|
package/dist/rpc.js
CHANGED
|
@@ -100,6 +100,13 @@ async function dispatch(ctx, body) {
|
|
|
100
100
|
const cwd = p.cwd;
|
|
101
101
|
return { ok: true, value: await ctx.skillMcp.readSkill(path, typeof cwd === 'string' ? cwd : undefined) };
|
|
102
102
|
}
|
|
103
|
+
case 'deleteSkill': {
|
|
104
|
+
const path = p.path;
|
|
105
|
+
if (typeof path !== 'string' || path === '')
|
|
106
|
+
return internal('deleteSkill: path is required');
|
|
107
|
+
const cwd = p.cwd;
|
|
108
|
+
return { ok: true, value: await ctx.skillMcp.deleteSkill(path, typeof cwd === 'string' ? cwd : undefined) };
|
|
109
|
+
}
|
|
103
110
|
case 'listMcpServers':
|
|
104
111
|
return { ok: true, value: await ctx.skillMcp.listMcpServers() };
|
|
105
112
|
case 'createMcpServer':
|
package/dist/service.d.ts
CHANGED
|
@@ -126,6 +126,23 @@ export declare function pluginSkillRoots(entries: Iterable<SkillRootEntry>, node
|
|
|
126
126
|
dir: string;
|
|
127
127
|
label: string;
|
|
128
128
|
}[];
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the entry one deletion removes, or reject the request.
|
|
131
|
+
*
|
|
132
|
+
* `path` is a SKILL.md path as `listSkills` reported it. A bundle
|
|
133
|
+
* (`<root>/<name>/SKILL.md`) is removed as its directory; a flat file
|
|
134
|
+
* (`<root>/<name>.md`) as itself.
|
|
135
|
+
*
|
|
136
|
+
* The entry must be a **direct child** of one of `roots`. Direct-child
|
|
137
|
+
* placement, rather than a prefix check, is what keeps this from being aimed
|
|
138
|
+
* at a root itself, at a nested reference file inside a skill, or at anything
|
|
139
|
+
* under a plugin package — those roots are not in the list to begin with.
|
|
140
|
+
* @param path - candidate SKILL.md path.
|
|
141
|
+
* @param roots - roots a deletion may target.
|
|
142
|
+
* @returns the absolute entry to remove.
|
|
143
|
+
* @throws {Error} `skill-not-found` when the path is not a skill entry sitting directly under an allowed root.
|
|
144
|
+
*/
|
|
145
|
+
export declare function deleteTargetOf(path: string, roots: readonly string[]): string;
|
|
129
146
|
export declare class SkillMcpService extends Service {
|
|
130
147
|
static inject: string[];
|
|
131
148
|
private readonly officialSkillDirs;
|
|
@@ -146,6 +163,16 @@ export declare class SkillMcpService extends Service {
|
|
|
146
163
|
* bundled inside loaded plugin packages, and any configured official roots.
|
|
147
164
|
*/
|
|
148
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;
|
|
149
176
|
/** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
|
|
150
177
|
toggleSkill(path: string): Promise<SkillView>;
|
|
151
178
|
/**
|
|
@@ -154,6 +181,22 @@ export declare class SkillMcpService extends Service {
|
|
|
154
181
|
* scans, so the RPC cannot be used to read arbitrary files.
|
|
155
182
|
*/
|
|
156
183
|
readSkill(path: string, cwd?: string): Promise<string>;
|
|
184
|
+
/**
|
|
185
|
+
* Delete one user-level or project-level skill.
|
|
186
|
+
*
|
|
187
|
+
* The entry is **moved**, not unlinked: a mistaken deletion lands in
|
|
188
|
+
* `<home>/.dsh/.skill-trash/` and stays recoverable, and the destination is
|
|
189
|
+
* returned so the surface can tell the user where it went. Plugin packages
|
|
190
|
+
* and the official bundled roots are not candidates — see
|
|
191
|
+
* {@link deletableRoots}.
|
|
192
|
+
* @param path - absolute SKILL.md path, as `listSkills` reported it.
|
|
193
|
+
* @param cwd - session workspace, when the caller has one.
|
|
194
|
+
* @returns the trash path the skill was moved to.
|
|
195
|
+
* @throws {Error} `skill-not-found` when the path is not deletable; `skill-delete-failed` when the move fails.
|
|
196
|
+
*/
|
|
197
|
+
deleteSkill(path: string, cwd?: string): Promise<{
|
|
198
|
+
trashPath: string;
|
|
199
|
+
}>;
|
|
157
200
|
/** Every `mcp-client` loader entry as a server card. */
|
|
158
201
|
listMcpServers(): Promise<McpServer[]>;
|
|
159
202
|
/** Add one mcp-client entry — hot-connects (create → init) and persists. */
|
package/dist/service.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Service } from '@deepseek-ai/cordis';
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
|
-
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
|
-
import { join, sep } from 'node:path';
|
|
13
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { parseSkillFrontmatter, setDisableModelInvocation } from "./frontmatter.js";
|
|
16
16
|
/** Runtime mirror of cordis FiberState (a cross-package const enum). */
|
|
@@ -190,6 +190,45 @@ export function pluginSkillRoots(entries, nodeModules) {
|
|
|
190
190
|
}
|
|
191
191
|
return out;
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Skill roots a deletion may target: the user-level roots plus the given
|
|
195
|
+
* workspace's, and nothing else.
|
|
196
|
+
*
|
|
197
|
+
* Deliberately narrower than the set `readSkill` accepts: plugin packages and
|
|
198
|
+
* the official bundled roots stay read-only by contract, so they are not
|
|
199
|
+
* candidates even though the surface lists them.
|
|
200
|
+
* @param cwd - session workspace, when the caller has one.
|
|
201
|
+
* @returns the roots a deletion may target.
|
|
202
|
+
*/
|
|
203
|
+
function deletableRoots(cwd) {
|
|
204
|
+
const roots = SKILL_ROOTS.map(root => root.path);
|
|
205
|
+
if (cwd !== undefined && cwd !== '') {
|
|
206
|
+
roots.push(join(cwd, '.agents', 'skills'), join(cwd, '.dsh', 'skills'));
|
|
207
|
+
}
|
|
208
|
+
return roots;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the entry one deletion removes, or reject the request.
|
|
212
|
+
*
|
|
213
|
+
* `path` is a SKILL.md path as `listSkills` reported it. A bundle
|
|
214
|
+
* (`<root>/<name>/SKILL.md`) is removed as its directory; a flat file
|
|
215
|
+
* (`<root>/<name>.md`) as itself.
|
|
216
|
+
*
|
|
217
|
+
* The entry must be a **direct child** of one of `roots`. Direct-child
|
|
218
|
+
* placement, rather than a prefix check, is what keeps this from being aimed
|
|
219
|
+
* at a root itself, at a nested reference file inside a skill, or at anything
|
|
220
|
+
* under a plugin package — those roots are not in the list to begin with.
|
|
221
|
+
* @param path - candidate SKILL.md path.
|
|
222
|
+
* @param roots - roots a deletion may target.
|
|
223
|
+
* @returns the absolute entry to remove.
|
|
224
|
+
* @throws {Error} `skill-not-found` when the path is not a skill entry sitting directly under an allowed root.
|
|
225
|
+
*/
|
|
226
|
+
export function deleteTargetOf(path, roots) {
|
|
227
|
+
const entry = basename(path) === 'SKILL.md' ? dirname(path) : path;
|
|
228
|
+
if (!roots.some(root => dirname(entry) === root))
|
|
229
|
+
throw new Error('skill-not-found');
|
|
230
|
+
return entry;
|
|
231
|
+
}
|
|
193
232
|
export class SkillMcpService extends Service {
|
|
194
233
|
static inject = ['loader', 'tools'];
|
|
195
234
|
officialSkillDirs;
|
|
@@ -229,8 +268,48 @@ export class SkillMcpService extends Service {
|
|
|
229
268
|
for (const dir of this.officialSkillDirs) {
|
|
230
269
|
skills.push(...await scanSkillRoot(dir, 'bundled', false, 'dsh-official'));
|
|
231
270
|
}
|
|
271
|
+
skills.push(...await this.runtimeOnlySkills(skills));
|
|
232
272
|
return skills;
|
|
233
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
|
+
}
|
|
234
313
|
/** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
|
|
235
314
|
async toggleSkill(path) {
|
|
236
315
|
let text;
|
|
@@ -282,6 +361,41 @@ export class SkillMcpService extends Service {
|
|
|
282
361
|
}
|
|
283
362
|
return text;
|
|
284
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Delete one user-level or project-level skill.
|
|
366
|
+
*
|
|
367
|
+
* The entry is **moved**, not unlinked: a mistaken deletion lands in
|
|
368
|
+
* `<home>/.dsh/.skill-trash/` and stays recoverable, and the destination is
|
|
369
|
+
* returned so the surface can tell the user where it went. Plugin packages
|
|
370
|
+
* and the official bundled roots are not candidates — see
|
|
371
|
+
* {@link deletableRoots}.
|
|
372
|
+
* @param path - absolute SKILL.md path, as `listSkills` reported it.
|
|
373
|
+
* @param cwd - session workspace, when the caller has one.
|
|
374
|
+
* @returns the trash path the skill was moved to.
|
|
375
|
+
* @throws {Error} `skill-not-found` when the path is not deletable; `skill-delete-failed` when the move fails.
|
|
376
|
+
*/
|
|
377
|
+
async deleteSkill(path, cwd) {
|
|
378
|
+
const entry = deleteTargetOf(path, deletableRoots(cwd));
|
|
379
|
+
const trashRoot = join(homedir(), '.dsh', '.skill-trash');
|
|
380
|
+
const trashPath = join(trashRoot, `${Date.now()}-${basename(entry)}`);
|
|
381
|
+
try {
|
|
382
|
+
await mkdir(trashRoot, { recursive: true });
|
|
383
|
+
try {
|
|
384
|
+
await rename(entry, trashPath);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
// Renaming across volumes fails with EXDEV; copy then remove instead.
|
|
388
|
+
if (error.code !== 'EXDEV')
|
|
389
|
+
throw error;
|
|
390
|
+
await cp(entry, trashPath, { recursive: true });
|
|
391
|
+
await rm(entry, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
throw new Error('skill-delete-failed');
|
|
396
|
+
}
|
|
397
|
+
return { trashPath };
|
|
398
|
+
}
|
|
285
399
|
/** Every `mcp-client` loader entry as a server card. */
|
|
286
400
|
async listMcpServers() {
|
|
287
401
|
const servers = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-skill-mcp-center",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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": {
|
package/release-notes.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Release Notes — @max-null/dsh-skill-mcp-center
|
|
2
2
|
|
|
3
|
+
## 0.6.0 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- **删除用户级 / 项目级技能**。设置页与侧栏的技能行都多了一个「删除」按钮,只出现在
|
|
8
|
+
`writable` 的技能上 —— 也就是 `~/.dsh/skills`、`~/.agents/skills`,以及当前工作区的
|
|
9
|
+
`<cwd>/.agents/skills` 与 `<cwd>/.dsh/skills`。插件技能与 DSH 官方内置技能(`plugin:` /
|
|
10
|
+
`bundled` / `dsh-official`)保持只读,不出现该按钮。
|
|
11
|
+
|
|
12
|
+
点删除先弹确认,写明技能名与完整路径;确认后该技能从磁盘移除 —— 目录形态
|
|
13
|
+
(`<name>/SKILL.md`)删整个目录,平铺形态(`<name>.md`)删那个文件。
|
|
14
|
+
|
|
15
|
+
**不直接销毁**:条目被移到 `~/.dsh/.skill-trash/`,成功提示里给出具体位置,误删可手动
|
|
16
|
+
移回去。
|
|
17
|
+
|
|
18
|
+
### 兼容性
|
|
19
|
+
|
|
20
|
+
- 新增一个 RPC 端点 `deleteSkill`(`{ path, cwd? }`);现有端点与请求信封不变。
|
|
21
|
+
- 删除面**刻意窄于**查看面:`readSkill` 接受插件与内置根,删除只接受用户级与项目级根,
|
|
22
|
+
并且要求目标**正好是某个允许根的直接子项** —— 根自身、技能内部的嵌套文件、名字相近的
|
|
23
|
+
兄弟目录一律拒绝(`skill-not-found`)。
|
|
24
|
+
- 新增 10 条测试:7 条覆盖删除目标的解析与拒绝面,3 条做真实文件操作(整目录 / 平铺 /
|
|
25
|
+
拒绝后文件原样不动)。
|
|
26
|
+
|
|
3
27
|
## 0.5.4 (2026-10-04)
|
|
4
28
|
|
|
5
29
|
### 修复
|