@xxxyz/dsh-mcp-manager 2.2.0 → 2.2.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/lib/client.js CHANGED
@@ -404,6 +404,11 @@ const factory = (require) => {
404
404
  collapsed ? null : pItems.map((s) => {
405
405
  const overridden = s.provider === 'dsh-mcp-manager-override'
406
406
  const available = !!(s.invocation && s.invocation.modelInvocable)
407
+ // User-level filesystem skills are view-only: they live in
408
+ // the scoped layer which a global-layer override cannot
409
+ // shadow, and the plugin sandbox blocks writing their
410
+ // SKILL.md — so no enable/disable button is offered.
411
+ const userLevel = s.source === 'user-dsh' || s.source === 'user-agents'
407
412
  return React.createElement('div', { className: 'mcpm-row', key: s.name },
408
413
  React.createElement('div', { className: 'mcpm-row-head' },
409
414
  React.createElement('span', { className: 'mcpm-name' }, s.name),
@@ -412,7 +417,9 @@ const factory = (require) => {
412
417
  React.createElement('span', { className: 'mcpm-chip live' }, s.provider)),
413
418
  React.createElement('div', { className: 'mcpm-sub' }, s.description || ''),
414
419
  React.createElement('div', { className: 'mcpm-row-actions' },
415
- React.createElement('button', { className: 'mcpm-btn', disabled: busy === s.name, onClick: () => toggle(s) }, overridden ? '启用' : '禁用')))
420
+ userLevel
421
+ ? React.createElement('span', { className: 'mcpm-note' }, '仅查看(由 DSH 文件系统管理)')
422
+ : React.createElement('button', { className: 'mcpm-btn', disabled: busy === s.name, onClick: () => toggle(s) }, overridden ? '启用' : '禁用')))
416
423
  }))
417
424
  }))
418
425
  }))
package/lib/index.js CHANGED
@@ -115,14 +115,26 @@ export default {
115
115
  for (const name of await readState()) {
116
116
  if (overrideSkills.has(name))
117
117
  continue;
118
- const def = (await ctx.skills.get(name)) || (await findUserSkill(name));
118
+ // Pre-2.2.2 migration: user-level skills are view-only now. Drop any
119
+ // stale state entry without touching the file — the sandbox blocks
120
+ // writing ~/.dsh/skills from a profile plugin, and an override could
121
+ // never shadow the scoped-layer filesystem entry anyway.
122
+ const userDef = await findUserSkill(name);
123
+ if (userDef) {
124
+ changed = true;
125
+ continue;
126
+ }
127
+ const def = await ctx.skills.get(name);
119
128
  if (!def)
120
129
  continue;
121
130
  overrideSkills.set(name, { ...def, provider: OVERRIDE_PROVIDER, invocation: { modelInvocable: false, userInvocable: false } });
122
131
  changed = true;
123
132
  }
124
- if (changed && skillProviderControl)
125
- skillProviderControl.invalidate();
133
+ if (changed) {
134
+ await saveState([...overrideSkills.keys()]);
135
+ if (skillProviderControl)
136
+ skillProviderControl.invalidate();
137
+ }
126
138
  }
127
139
  ;
128
140
  ctx.on('skills/change', () => { void ensureRestored(); });
@@ -130,11 +142,15 @@ export default {
130
142
  // dsh-skill-filesystem in the agent-preset SCOPED layer, which a scope-less
131
143
  // ctx.skills.snapshot({}) (global layer only) never reaches — so the
132
144
  // management page would silently hide them. Scan the directory ourselves
133
- // and merge into skill-list. Toggling still goes through the rank-0
134
- // override above: it shadows by NAME, so it works across layers — but
135
- // ctx.skills.get(name) is also scope-less, hence the scan fallback when
136
- // synthesizing the override definition. DSH_MCP_MANAGER_SKILLS_DIR
137
- // overrides the directory (used by tests).
145
+ // and merge into skill-list.
146
+ //
147
+ // Disable/enable is NOT offered for user skills: (a) a rank-0 override in
148
+ // the global layer cannot shadow the scoped layer's filesystem entry
149
+ // (collectFresh merges layers; the nearest layer wins); (b) writing the
150
+ // SKILL.md frontmatter is blocked by the fs sandbox (profile plugin,
151
+ // workspace-write mode — ~/.dsh/skills is outside its scope). Users
152
+ // disable such skills by editing `user-invocable: false` in the file
153
+ // directly. DSH_MCP_MANAGER_SKILLS_DIR overrides the directory for tests.
138
154
  function userSkillsDir(home, sep) {
139
155
  const env = process.env.DSH_MCP_MANAGER_SKILLS_DIR;
140
156
  return env && env.trim() ? env.trim() : home + sep + 'skills';
@@ -275,6 +291,18 @@ export default {
275
291
  const skills = snap.skills.concat(extra).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
276
292
  return { ok: true, skills, complete: snap.complete !== false };
277
293
  }
294
+ // User-level skills (~/.dsh/skills) are VIEW-ONLY in this page. They are
295
+ // discovered by dsh-skill-filesystem in the agent-preset SCOPED layer; a
296
+ // scope-less snapshot({}) (global layer only) never sees them, so we scan
297
+ // the directory and merge rows into skill-list. Disable/enable is NOT
298
+ // supported for them: (a) the rank-0 override provider lives in the global
299
+ // layer and a scoped read merges the scope chain with the scoped entry
300
+ // winning the duplicate name outright (collectFresh last-wins), so an
301
+ // override cannot hide them from the / menu; (b) rewriting the SKILL.md
302
+ // frontmatter is blocked by the fs sandbox (profile plugin, workspace-write
303
+ // mode — ~/.dsh/skills is outside its scope). Users disable such skills by
304
+ // editing `user-invocable: false` in the file directly.
305
+ const USER_SKILL_VIEW_ONLY_ERROR = '用户级技能由 DSH 文件系统管理,仅支持查看;如需禁用请直接编辑 SKILL.md 的 user-invocable 字段';
278
306
  async function skillToggle(args) {
279
307
  const name = String(args.name || '').trim();
280
308
  if (!name)
@@ -284,6 +312,17 @@ export default {
284
312
  await ensureRestored();
285
313
  const enabled = args.enabled !== false;
286
314
  return withWriteLock(async () => {
315
+ const userDef = await findUserSkill(name);
316
+ if (userDef) {
317
+ // Drop any stale override row created by older versions (pre-2.2.2
318
+ // migration) — it can never shadow the scoped entry anyway, and the
319
+ // toggle for user skills is not supported.
320
+ if (overrideSkills.delete(name)) {
321
+ await saveState([...overrideSkills.keys()]);
322
+ skillProviderControl?.invalidate();
323
+ }
324
+ return { ok: false, error: USER_SKILL_VIEW_ONLY_ERROR };
325
+ }
287
326
  if (enabled) {
288
327
  if (overrideSkills.delete(name)) {
289
328
  await saveState([...overrideSkills.keys()]);
@@ -292,7 +331,7 @@ export default {
292
331
  }
293
332
  else {
294
333
  if (!overrideSkills.has(name)) {
295
- const def = (await ctx.skills.get(name)) || (await findUserSkill(name));
334
+ const def = await ctx.skills.get(name);
296
335
  if (!def)
297
336
  return { ok: false, error: '技能不存在: ' + name };
298
337
  overrideSkills.set(name, { ...def, provider: OVERRIDE_PROVIDER, invocation: { modelInvocable: false, userInvocable: false } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxxyz/dsh-mcp-manager",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "DSH-standard MCP manager plugin: Settings UI + HTTP API + model-facing mcp_manager_* tools. Install with one command: dsh plugin --profile web add @xxxyz/dsh-mcp-manager@latest",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",