@xxxyz/dsh-mcp-manager 2.2.0 → 2.2.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.
Files changed (2) hide show
  1. package/lib/index.js +61 -4
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -115,14 +115,27 @@ 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
+ // 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.
122
+ const userDef = await findUserSkill(name);
123
+ if (userDef) {
124
+ await editUserSkillInvocation(name, false);
125
+ changed = true;
126
+ continue;
127
+ }
128
+ const def = await ctx.skills.get(name);
119
129
  if (!def)
120
130
  continue;
121
131
  overrideSkills.set(name, { ...def, provider: OVERRIDE_PROVIDER, invocation: { modelInvocable: false, userInvocable: false } });
122
132
  changed = true;
123
133
  }
124
- if (changed && skillProviderControl)
125
- skillProviderControl.invalidate();
134
+ if (changed) {
135
+ await saveState([...overrideSkills.keys()]);
136
+ if (skillProviderControl)
137
+ skillProviderControl.invalidate();
138
+ }
126
139
  }
127
140
  ;
128
141
  ctx.on('skills/change', () => { void ensureRestored(); });
@@ -275,6 +288,38 @@ export default {
275
288
  const skills = snap.skills.concat(extra).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
276
289
  return { ok: true, skills, complete: snap.complete !== false };
277
290
  }
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
+ }
278
323
  async function skillToggle(args) {
279
324
  const name = String(args.name || '').trim();
280
325
  if (!name)
@@ -284,6 +329,18 @@ export default {
284
329
  await ensureRestored();
285
330
  const enabled = args.enabled !== false;
286
331
  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
+ const userDef = await findUserSkill(name);
336
+ if (userDef) {
337
+ // Also drop a stale override row if one was created pre-fix.
338
+ if (overrideSkills.delete(name)) {
339
+ await saveState([...overrideSkills.keys()]);
340
+ skillProviderControl?.invalidate();
341
+ }
342
+ return editUserSkillInvocation(name, enabled);
343
+ }
287
344
  if (enabled) {
288
345
  if (overrideSkills.delete(name)) {
289
346
  await saveState([...overrideSkills.keys()]);
@@ -292,7 +349,7 @@ export default {
292
349
  }
293
350
  else {
294
351
  if (!overrideSkills.has(name)) {
295
- const def = (await ctx.skills.get(name)) || (await findUserSkill(name));
352
+ const def = await ctx.skills.get(name);
296
353
  if (!def)
297
354
  return { ok: false, error: '技能不存在: ' + name };
298
355
  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.1",
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",