@xxxyz/dsh-mcp-manager 2.2.1 → 2.2.3

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,13 +115,12 @@ export default {
115
115
  for (const name of await readState()) {
116
116
  if (overrideSkills.has(name))
117
117
  continue;
118
- // Migrate pre-fix state entries: user-level skills are file-driven now
119
- // (the scoped layer ignores global overrides), so rewrite the SKILL.md
120
- // frontmatter and drop the stale state entry instead of creating an
121
- // override that cannot shadow them.
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
122
  const userDef = await findUserSkill(name);
123
123
  if (userDef) {
124
- await editUserSkillInvocation(name, false);
125
124
  changed = true;
126
125
  continue;
127
126
  }
@@ -143,11 +142,15 @@ export default {
143
142
  // dsh-skill-filesystem in the agent-preset SCOPED layer, which a scope-less
144
143
  // ctx.skills.snapshot({}) (global layer only) never reaches — so the
145
144
  // management page would silently hide them. Scan the directory ourselves
146
- // and merge into skill-list. Toggling still goes through the rank-0
147
- // override above: it shadows by NAME, so it works across layers — but
148
- // ctx.skills.get(name) is also scope-less, hence the scan fallback when
149
- // synthesizing the override definition. DSH_MCP_MANAGER_SKILLS_DIR
150
- // 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.
151
154
  function userSkillsDir(home, sep) {
152
155
  const env = process.env.DSH_MCP_MANAGER_SKILLS_DIR;
153
156
  return env && env.trim() ? env.trim() : home + sep + 'skills';
@@ -288,38 +291,18 @@ export default {
288
291
  const skills = snap.skills.concat(extra).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
289
292
  return { ok: true, skills, complete: snap.complete !== false };
290
293
  }
291
- // Toggle a USER-LEVEL filesystem skill by editing its SKILL.md frontmatter.
292
- // This is the only mechanism that works: the rank-0 override provider lives
293
- // in the global layer, but ctx.skills reads merge the viewing scope's chain
294
- // and the scoped layer's filesystem entry WINS the duplicate name outright
295
- // (collectFresh does `merged.set(name, entry)` per layer, last wins) so an
296
- // override can never hide a user skill from the / menu. Editing the source
297
- // file makes the real dsh-skill-filesystem provider (which hot-reloads via
298
- // its watcher) return the disabled invocation on every scope.
299
- async function editUserSkillInvocation(name, enabled) {
300
- const def = await findUserSkill(name);
301
- if (!def || !def.path)
302
- return { ok: false, error: '技能不存在: ' + name };
303
- const raw = await fs.readText(await fs.resolve(def.path)).catch(() => '');
304
- if (!raw)
305
- return { ok: false, error: '无法读取技能文件: ' + def.path };
306
- // Strip any existing invocation flags from the frontmatter block, then
307
- // re-append the ones matching the requested state. Everything else —
308
- // body content and other frontmatter keys — is preserved verbatim.
309
- const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
310
- if (!fmMatch)
311
- return { ok: false, error: '技能文件缺少 frontmatter: ' + def.path };
312
- const fmLines = fmMatch[1].split(/\r?\n/);
313
- const rest = raw.slice(fmMatch[0].length);
314
- const stripped = fmLines.filter((line) => !/^(user-invocable|disable-model-invocation):/i.test(line.trim()));
315
- if (!enabled) {
316
- stripped.push('user-invocable: false');
317
- stripped.push('disable-model-invocation: true');
318
- }
319
- const next = '---\n' + stripped.join('\n') + '\n---' + rest;
320
- await fs.writeText(await fs.resolve(def.path), next);
321
- return { ok: true };
322
- }
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 字段';
323
306
  async function skillToggle(args) {
324
307
  const name = String(args.name || '').trim();
325
308
  if (!name)
@@ -329,17 +312,16 @@ export default {
329
312
  await ensureRestored();
330
313
  const enabled = args.enabled !== false;
331
314
  return withWriteLock(async () => {
332
- // User-level skills bypass the override provider entirely — editing the
333
- // SKILL.md frontmatter is what makes the real provider (and thus the
334
- // / menu) see the change.
335
315
  const userDef = await findUserSkill(name);
336
316
  if (userDef) {
337
- // Also drop a stale override row if one was created pre-fix.
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.
338
320
  if (overrideSkills.delete(name)) {
339
321
  await saveState([...overrideSkills.keys()]);
340
322
  skillProviderControl?.invalidate();
341
323
  }
342
- return editUserSkillInvocation(name, enabled);
324
+ return { ok: false, error: USER_SKILL_VIEW_ONLY_ERROR };
343
325
  }
344
326
  if (enabled) {
345
327
  if (overrideSkills.delete(name)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxxyz/dsh-mcp-manager",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
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",
package/show.png CHANGED
Binary file