@max-null/dsh-skill-mcp-center 0.5.3 → 0.6.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.
- package/README.md +1 -1
- package/client.js +63 -3
- package/dist/rpc.js +7 -0
- package/dist/service.d.ts +77 -5
- package/dist/service.js +129 -20
- package/package.json +1 -1
- package/release-notes.md +49 -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.
|
|
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
|
@@ -248,7 +248,11 @@ var zhDict = {
|
|
|
248
248
|
viewMd: "\u67E5\u770B SKILL.md",
|
|
249
249
|
hideMd: "\u6536\u8D77",
|
|
250
250
|
mdLoadFailed: "\u8BFB\u53D6\u5931\u8D25\uFF1A{e}",
|
|
251
|
-
refOpen: "\u6253\u5F00\u6587\u4EF6\u5F15\u7528"
|
|
251
|
+
refOpen: "\u6253\u5F00\u6587\u4EF6\u5F15\u7528",
|
|
252
|
+
deleteSkill: "\u5220\u9664\u6280\u80FD",
|
|
253
|
+
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",
|
|
254
|
+
skillDeleted: "\u5DF2\u5220\u9664 {name}\uFF0C\u79FB\u5230 {path}",
|
|
255
|
+
deleteFailed: "\u5220\u9664\u5931\u8D25\uFF1A{e}"
|
|
252
256
|
};
|
|
253
257
|
var enDict = {
|
|
254
258
|
loading: "Loading\u2026",
|
|
@@ -303,7 +307,11 @@ var enDict = {
|
|
|
303
307
|
viewMd: "View SKILL.md",
|
|
304
308
|
hideMd: "Hide",
|
|
305
309
|
mdLoadFailed: "Failed to read: {e}",
|
|
306
|
-
refOpen: "Open file reference"
|
|
310
|
+
refOpen: "Open file reference",
|
|
311
|
+
deleteSkill: "Delete skill",
|
|
312
|
+
deleteConfirm: 'Delete skill "{name}"?\n\n{path}\n\nIt will be moved to ~/.dsh/.skill-trash and can be moved back manually if needed.',
|
|
313
|
+
skillDeleted: "Deleted {name}, moved to {path}",
|
|
314
|
+
deleteFailed: "Delete failed: {e}"
|
|
307
315
|
};
|
|
308
316
|
var rpc = async () => {
|
|
309
317
|
throw new Error("skill-mcp-center: rpc not wired");
|
|
@@ -432,6 +440,21 @@ function SkillView() {
|
|
|
432
440
|
}
|
|
433
441
|
);
|
|
434
442
|
};
|
|
443
|
+
const remove = (s) => {
|
|
444
|
+
if (!window.confirm(t("deleteConfirm", { name: s.name, path: s.path }))) return;
|
|
445
|
+
setBusy(s.path);
|
|
446
|
+
void rpc("deleteSkill", { path: s.path }).then(
|
|
447
|
+
(v) => {
|
|
448
|
+
setBusy(null);
|
|
449
|
+
load();
|
|
450
|
+
showToast(t("skillDeleted", { name: s.name, path: v.trashPath }));
|
|
451
|
+
},
|
|
452
|
+
(e) => {
|
|
453
|
+
setBusy(null);
|
|
454
|
+
showToast(t("deleteFailed", { e: e instanceof Error ? e.message : String(e) }), "error");
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
};
|
|
435
458
|
const openMd = (s) => {
|
|
436
459
|
if (mdPath === s.path) {
|
|
437
460
|
setMdPath(null);
|
|
@@ -508,7 +531,19 @@ function SkillView() {
|
|
|
508
531
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
|
|
509
532
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: () => {
|
|
510
533
|
openMd(s);
|
|
511
|
-
}, children: mdPath === s.path ? t("hideMd") : t("viewMd") })
|
|
534
|
+
}, children: mdPath === s.path ? t("hideMd") : t("viewMd") }),
|
|
535
|
+
s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
536
|
+
"button",
|
|
537
|
+
{
|
|
538
|
+
type: "button",
|
|
539
|
+
className: "smc-btn danger",
|
|
540
|
+
disabled: busy === s.path,
|
|
541
|
+
onClick: () => {
|
|
542
|
+
remove(s);
|
|
543
|
+
},
|
|
544
|
+
children: t("deleteSkill")
|
|
545
|
+
}
|
|
546
|
+
)
|
|
512
547
|
] }),
|
|
513
548
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail-row", children: [
|
|
514
549
|
t("detailPath"),
|
|
@@ -829,6 +864,18 @@ function SidebarSkillTab({ visible, cwd }) {
|
|
|
829
864
|
if (!visible) return;
|
|
830
865
|
load();
|
|
831
866
|
}, [visible, load]);
|
|
867
|
+
const removeSkill = (s) => {
|
|
868
|
+
if (!window.confirm(t("deleteConfirm", { name: s.name, path: s.path }))) return;
|
|
869
|
+
void rpc("deleteSkill", { path: s.path, cwd }).then(
|
|
870
|
+
(v) => {
|
|
871
|
+
load();
|
|
872
|
+
showToast(t("skillDeleted", { name: s.name, path: v.trashPath }));
|
|
873
|
+
},
|
|
874
|
+
(e) => {
|
|
875
|
+
showToast(t("deleteFailed", { e: e instanceof Error ? e.message : String(e) }), "error");
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
};
|
|
832
879
|
const openMd = (path) => {
|
|
833
880
|
if (mdPath === path) {
|
|
834
881
|
setMdPath(null);
|
|
@@ -900,6 +947,19 @@ ${s.path}`, children: [
|
|
|
900
947
|
children: mdPath === s.path ? t("hideMd") : "MD"
|
|
901
948
|
}
|
|
902
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
|
+
),
|
|
903
963
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
904
964
|
"button",
|
|
905
965
|
{
|
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
|
@@ -83,13 +83,72 @@ export interface McpConfig {
|
|
|
83
83
|
* @returns the skills this root contributes, in directory order.
|
|
84
84
|
*/
|
|
85
85
|
export declare function scanSkillRoot(root: string, source: string, writable?: boolean, provider?: string): Promise<SkillView[]>;
|
|
86
|
+
/** The loader-entry fields plugin skill discovery reads. */
|
|
87
|
+
export interface SkillRootEntry {
|
|
88
|
+
readonly options: {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly name: string;
|
|
91
|
+
};
|
|
92
|
+
/** Absent while the entry is disabled, incompatible, or still loading. */
|
|
93
|
+
readonly fiber?: {
|
|
94
|
+
readonly config: unknown;
|
|
95
|
+
} | undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Skill roots contributed by loaded plugin packages.
|
|
99
|
+
*
|
|
100
|
+
* Two declarations reach such a directory, and both are needed:
|
|
101
|
+
*
|
|
102
|
+
* - the entry's module specifier, i.e. `<node_modules>/<pkg>/skills` — how
|
|
103
|
+
* `@max-null/dsh-skills` and `@max-null/dsh-plugin-center` ship theirs;
|
|
104
|
+
* - the entry's own `bundledSkillDir` config — the only way to see a plugin
|
|
105
|
+
* that mounts the official `@deepseek-ai/dsh-skill-filesystem` under an id
|
|
106
|
+
* of its own. `dsh-plugin-zhihu-search` is the live sample: it registers
|
|
107
|
+
* `name: '@deepseek-ai/dsh-skill-filesystem'`, and its `skills/` sits in a
|
|
108
|
+
* different `node_modules` tree than this plugin's.
|
|
109
|
+
*
|
|
110
|
+
* `bundledSkillDir` is read from the **fiber**, never from
|
|
111
|
+
* `entry.options.config`. A patch may compute the value with a `!!js`
|
|
112
|
+
* expression, and the Loader keeps that expression as a `{ __jsExpr }` node
|
|
113
|
+
* in the stored options while handing the owning fiber the evaluated copy —
|
|
114
|
+
* it interpolates on the `internal/config` waterfall and writes the raw node
|
|
115
|
+
* back on `internal/update` specifically so file write-back preserves the
|
|
116
|
+
* `!!js` form. The stored value is therefore an expression node rather than a
|
|
117
|
+
* path, and reading it can only miss.
|
|
118
|
+
*
|
|
119
|
+
* Only **loaded** entries are probed, so this costs a couple of existence
|
|
120
|
+
* checks per plugin instead of a scan of the whole `node_modules` tree.
|
|
121
|
+
* @param entries - loaded loader entries.
|
|
122
|
+
* @param nodeModules - this package's own `node_modules` tree, or null.
|
|
123
|
+
* @returns the skill roots that exist, deduplicated, in entry order.
|
|
124
|
+
*/
|
|
125
|
+
export declare function pluginSkillRoots(entries: Iterable<SkillRootEntry>, nodeModules: string | null): {
|
|
126
|
+
dir: string;
|
|
127
|
+
label: string;
|
|
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;
|
|
86
146
|
export declare class SkillMcpService extends Service {
|
|
87
147
|
static inject: string[];
|
|
88
148
|
private readonly officialSkillDirs;
|
|
89
149
|
constructor(ctx: Context, config?: SkillConfig);
|
|
90
150
|
/**
|
|
91
|
-
* Skill roots that live **inside loaded plugin packages
|
|
92
|
-
* `<node_modules>/<pkg>/skills`.
|
|
151
|
+
* Skill roots that live **inside loaded plugin packages**.
|
|
93
152
|
*
|
|
94
153
|
* The host-level skill filesystem is disabled in web-app (presets own
|
|
95
154
|
* discovery), so a plugin that ships skills — `@max-null/dsh-skills` and
|
|
@@ -97,9 +156,6 @@ export declare class SkillMcpService extends Service {
|
|
|
97
156
|
* package, where no user-level root can see them. Without this the
|
|
98
157
|
* management surface showed 17 user skills while 8 plugin skills were loaded
|
|
99
158
|
* and in effect (2026-09-14 用户报「skill 生效但不展示」).
|
|
100
|
-
*
|
|
101
|
-
* Only **loaded** entries are probed, so this costs one existence check per
|
|
102
|
-
* plugin rather than a scan of the whole node_modules tree.
|
|
103
159
|
*/
|
|
104
160
|
private pluginSkillDirs;
|
|
105
161
|
/**
|
|
@@ -115,6 +171,22 @@ export declare class SkillMcpService extends Service {
|
|
|
115
171
|
* scans, so the RPC cannot be used to read arbitrary files.
|
|
116
172
|
*/
|
|
117
173
|
readSkill(path: string, cwd?: string): Promise<string>;
|
|
174
|
+
/**
|
|
175
|
+
* Delete one user-level or project-level skill.
|
|
176
|
+
*
|
|
177
|
+
* The entry is **moved**, not unlinked: a mistaken deletion lands in
|
|
178
|
+
* `<home>/.dsh/.skill-trash/` and stays recoverable, and the destination is
|
|
179
|
+
* returned so the surface can tell the user where it went. Plugin packages
|
|
180
|
+
* and the official bundled roots are not candidates — see
|
|
181
|
+
* {@link deletableRoots}.
|
|
182
|
+
* @param path - absolute SKILL.md path, as `listSkills` reported it.
|
|
183
|
+
* @param cwd - session workspace, when the caller has one.
|
|
184
|
+
* @returns the trash path the skill was moved to.
|
|
185
|
+
* @throws {Error} `skill-not-found` when the path is not deletable; `skill-delete-failed` when the move fails.
|
|
186
|
+
*/
|
|
187
|
+
deleteSkill(path: string, cwd?: string): Promise<{
|
|
188
|
+
trashPath: string;
|
|
189
|
+
}>;
|
|
118
190
|
/** Every `mcp-client` loader entry as a server card. */
|
|
119
191
|
listMcpServers(): Promise<McpServer[]>;
|
|
120
192
|
/** 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). */
|
|
@@ -139,6 +139,96 @@ export async function scanSkillRoot(root, source, writable = true, provider = 'f
|
|
|
139
139
|
}
|
|
140
140
|
return skills;
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Skill roots contributed by loaded plugin packages.
|
|
144
|
+
*
|
|
145
|
+
* Two declarations reach such a directory, and both are needed:
|
|
146
|
+
*
|
|
147
|
+
* - the entry's module specifier, i.e. `<node_modules>/<pkg>/skills` — how
|
|
148
|
+
* `@max-null/dsh-skills` and `@max-null/dsh-plugin-center` ship theirs;
|
|
149
|
+
* - the entry's own `bundledSkillDir` config — the only way to see a plugin
|
|
150
|
+
* that mounts the official `@deepseek-ai/dsh-skill-filesystem` under an id
|
|
151
|
+
* of its own. `dsh-plugin-zhihu-search` is the live sample: it registers
|
|
152
|
+
* `name: '@deepseek-ai/dsh-skill-filesystem'`, and its `skills/` sits in a
|
|
153
|
+
* different `node_modules` tree than this plugin's.
|
|
154
|
+
*
|
|
155
|
+
* `bundledSkillDir` is read from the **fiber**, never from
|
|
156
|
+
* `entry.options.config`. A patch may compute the value with a `!!js`
|
|
157
|
+
* expression, and the Loader keeps that expression as a `{ __jsExpr }` node
|
|
158
|
+
* in the stored options while handing the owning fiber the evaluated copy —
|
|
159
|
+
* it interpolates on the `internal/config` waterfall and writes the raw node
|
|
160
|
+
* back on `internal/update` specifically so file write-back preserves the
|
|
161
|
+
* `!!js` form. The stored value is therefore an expression node rather than a
|
|
162
|
+
* path, and reading it can only miss.
|
|
163
|
+
*
|
|
164
|
+
* Only **loaded** entries are probed, so this costs a couple of existence
|
|
165
|
+
* checks per plugin instead of a scan of the whole `node_modules` tree.
|
|
166
|
+
* @param entries - loaded loader entries.
|
|
167
|
+
* @param nodeModules - this package's own `node_modules` tree, or null.
|
|
168
|
+
* @returns the skill roots that exist, deduplicated, in entry order.
|
|
169
|
+
*/
|
|
170
|
+
export function pluginSkillRoots(entries, nodeModules) {
|
|
171
|
+
const out = [];
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
const add = (dir, label) => {
|
|
174
|
+
if (seen.has(dir) || !existsSync(dir))
|
|
175
|
+
return;
|
|
176
|
+
seen.add(dir);
|
|
177
|
+
out.push({ dir, label });
|
|
178
|
+
};
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
const name = entry.options.name;
|
|
181
|
+
if (nodeModules !== null && name !== '' && !name.startsWith('.') && !name.startsWith('/')) {
|
|
182
|
+
add(join(nodeModules, ...name.split('/'), 'skills'), `plugin:${name}`);
|
|
183
|
+
}
|
|
184
|
+
const config = entry.fiber?.config;
|
|
185
|
+
const declared = typeof config === 'object' && config !== null && 'bundledSkillDir' in config
|
|
186
|
+
? config.bundledSkillDir
|
|
187
|
+
: undefined;
|
|
188
|
+
if (typeof declared === 'string')
|
|
189
|
+
add(declared, `plugin:${entry.options.id}`);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
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
|
+
}
|
|
142
232
|
export class SkillMcpService extends Service {
|
|
143
233
|
static inject = ['loader', 'tools'];
|
|
144
234
|
officialSkillDirs;
|
|
@@ -147,8 +237,7 @@ export class SkillMcpService extends Service {
|
|
|
147
237
|
this.officialSkillDirs = config.officialSkillDirs ?? [];
|
|
148
238
|
}
|
|
149
239
|
/**
|
|
150
|
-
* Skill roots that live **inside loaded plugin packages
|
|
151
|
-
* `<node_modules>/<pkg>/skills`.
|
|
240
|
+
* Skill roots that live **inside loaded plugin packages**.
|
|
152
241
|
*
|
|
153
242
|
* The host-level skill filesystem is disabled in web-app (presets own
|
|
154
243
|
* discovery), so a plugin that ships skills — `@max-null/dsh-skills` and
|
|
@@ -156,24 +245,9 @@ export class SkillMcpService extends Service {
|
|
|
156
245
|
* package, where no user-level root can see them. Without this the
|
|
157
246
|
* management surface showed 17 user skills while 8 plugin skills were loaded
|
|
158
247
|
* and in effect (2026-09-14 用户报「skill 生效但不展示」).
|
|
159
|
-
*
|
|
160
|
-
* Only **loaded** entries are probed, so this costs one existence check per
|
|
161
|
-
* plugin rather than a scan of the whole node_modules tree.
|
|
162
248
|
*/
|
|
163
249
|
pluginSkillDirs() {
|
|
164
|
-
|
|
165
|
-
if (nm === null)
|
|
166
|
-
return [];
|
|
167
|
-
const out = [];
|
|
168
|
-
for (const entry of this.ctx.loader.entries()) {
|
|
169
|
-
const name = entry.options.name;
|
|
170
|
-
if (typeof name !== 'string' || name === '' || name.startsWith('.') || name.startsWith('/'))
|
|
171
|
-
continue;
|
|
172
|
-
const dir = join(nm, ...name.split('/'), 'skills');
|
|
173
|
-
if (existsSync(dir))
|
|
174
|
-
out.push({ dir, label: `plugin:${name}` });
|
|
175
|
-
}
|
|
176
|
-
return out;
|
|
250
|
+
return pluginSkillRoots(this.ctx.loader.entries(), ownNodeModules());
|
|
177
251
|
}
|
|
178
252
|
/**
|
|
179
253
|
* User-level skills, project-level skills for the given workspace, skills
|
|
@@ -247,6 +321,41 @@ export class SkillMcpService extends Service {
|
|
|
247
321
|
}
|
|
248
322
|
return text;
|
|
249
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Delete one user-level or project-level skill.
|
|
326
|
+
*
|
|
327
|
+
* The entry is **moved**, not unlinked: a mistaken deletion lands in
|
|
328
|
+
* `<home>/.dsh/.skill-trash/` and stays recoverable, and the destination is
|
|
329
|
+
* returned so the surface can tell the user where it went. Plugin packages
|
|
330
|
+
* and the official bundled roots are not candidates — see
|
|
331
|
+
* {@link deletableRoots}.
|
|
332
|
+
* @param path - absolute SKILL.md path, as `listSkills` reported it.
|
|
333
|
+
* @param cwd - session workspace, when the caller has one.
|
|
334
|
+
* @returns the trash path the skill was moved to.
|
|
335
|
+
* @throws {Error} `skill-not-found` when the path is not deletable; `skill-delete-failed` when the move fails.
|
|
336
|
+
*/
|
|
337
|
+
async deleteSkill(path, cwd) {
|
|
338
|
+
const entry = deleteTargetOf(path, deletableRoots(cwd));
|
|
339
|
+
const trashRoot = join(homedir(), '.dsh', '.skill-trash');
|
|
340
|
+
const trashPath = join(trashRoot, `${Date.now()}-${basename(entry)}`);
|
|
341
|
+
try {
|
|
342
|
+
await mkdir(trashRoot, { recursive: true });
|
|
343
|
+
try {
|
|
344
|
+
await rename(entry, trashPath);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
// Renaming across volumes fails with EXDEV; copy then remove instead.
|
|
348
|
+
if (error.code !== 'EXDEV')
|
|
349
|
+
throw error;
|
|
350
|
+
await cp(entry, trashPath, { recursive: true });
|
|
351
|
+
await rm(entry, { recursive: true, force: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
throw new Error('skill-delete-failed');
|
|
356
|
+
}
|
|
357
|
+
return { trashPath };
|
|
358
|
+
}
|
|
250
359
|
/** Every `mcp-client` loader entry as a server card. */
|
|
251
360
|
async listMcpServers() {
|
|
252
361
|
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.0",
|
|
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,54 @@
|
|
|
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
|
+
|
|
27
|
+
## 0.5.4 (2026-10-04)
|
|
28
|
+
|
|
29
|
+
### 修复
|
|
30
|
+
|
|
31
|
+
- **插件用 `bundledSkillDir` 注册的技能扫不到**。面板原先只认一种形态:拿 loader
|
|
32
|
+
entry 的 `name` 当包名,去拼 `<node_modules>/<包名>/skills`。插件若把官方
|
|
33
|
+
`@deepseek-ai/dsh-skill-filesystem` 挂在自己的 id 下、用 `bundledSkillDir` 指向
|
|
34
|
+
自带 `skills/`(活样本 `dsh-plugin-zhihu-search`),说明符就是**另一个包名**,
|
|
35
|
+
拼出的路径不存在;而它的技能往往还在**另一棵** `node_modules` 树里。
|
|
36
|
+
|
|
37
|
+
真正的判据比「拼错了包名」更深一层:`bundledSkillDir` 在 `entry.options.config`
|
|
38
|
+
里保留的是 `!!js` 表达式节点(`{ __jsExpr }`),**不是路径**——Loader 只把求值后的
|
|
39
|
+
副本交给 entry 的 fiber,存下来的那份要留着让文件写回保住 `!!js` 写法。所以照
|
|
40
|
+
options 去拼路径必然落空,且**不报错**(静默漏掉)。
|
|
41
|
+
|
|
42
|
+
修法:该配置**从 fiber 读**,两条声明都探,并对同一目录去重。
|
|
43
|
+
|
|
44
|
+
### 兼容性
|
|
45
|
+
|
|
46
|
+
- 无接口变更:`listSkills()` 的出入参、端点集合与请求信封均未改,只是发现来源多了
|
|
47
|
+
一条。插件技能仍是只读展示(`writable: false`),未开放开关。
|
|
48
|
+
- 新增 12 条测试:10 条覆盖根发现(含「options 里的表达式节点不被当路径采信」这一
|
|
49
|
+
关键回归),2 条用真 cordis + 真 Loader 坐实「options 上是节点、fiber 上是求值后
|
|
50
|
+
的路径」。后者会在上游改变该行为时变红。
|
|
51
|
+
|
|
3
52
|
## 0.5.3 (2026-10-02)
|
|
4
53
|
|
|
5
54
|
### 修复
|