dsh-agent-plugins-market 0.7.1 → 0.7.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.
Files changed (61) hide show
  1. package/README.md +29 -24
  2. package/README.zh.md +29 -24
  3. package/client/client.js +1172 -159
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog.js +14 -2
  6. package/lib/application/install-store.js +1 -1
  7. package/lib/application/mcp-service.js +185 -30
  8. package/lib/catalog/archive.js +15 -0
  9. package/lib/catalog/manifests.js +124 -13
  10. package/lib/catalog/scan-resolvers.js +117 -12
  11. package/lib/catalog/surfaces.js +73 -27
  12. package/lib/catalog/validate.js +205 -54
  13. package/lib/contracts/market.js +2 -0
  14. package/lib/index.js +6 -2
  15. package/lib/model/layouts.js +34 -0
  16. package/lib/model/preset-source.js +8 -0
  17. package/lib/routes.js +31 -2
  18. package/lib/runtime/agent-role-router.js +176 -51
  19. package/lib/runtime/host-locale.js +6 -12
  20. package/lib/runtime/mcp-client/connection.js +3 -2
  21. package/lib/runtime/mcp-client/oauth.js +82 -14
  22. package/lib/runtime/mcp-client/tools.js +2 -1
  23. package/lib/runtime/mcp-config.js +178 -34
  24. package/lib/runtime/mcp-direct-config.js +49 -6
  25. package/lib/runtime/mcp-mounts.js +15 -3
  26. package/lib/runtime/mcp-overrides.js +131 -1
  27. package/lib/runtime/mcp-redaction.js +8 -0
  28. package/lib/runtime/mcp-status.js +21 -5
  29. package/lib/runtime/server-config.js +49 -8
  30. package/lib/runtime/subagent-catalog.js +62 -22
  31. package/lib/runtime/tool-registry-observer.js +8 -4
  32. package/lib/types/application/catalog.d.ts +10 -1
  33. package/lib/types/application/install-store.d.ts +1 -1
  34. package/lib/types/application/mcp-service.d.ts +32 -3
  35. package/lib/types/application/queries.d.ts +6 -1
  36. package/lib/types/catalog/manifests.d.ts +6 -2
  37. package/lib/types/catalog/surfaces.d.ts +14 -4
  38. package/lib/types/catalog/validate.d.ts +72 -11
  39. package/lib/types/contracts/market.d.ts +67 -0
  40. package/lib/types/contracts/mcp-status.d.ts +11 -0
  41. package/lib/types/model/layouts.d.ts +20 -0
  42. package/lib/types/model/preset-source.d.ts +18 -0
  43. package/lib/types/model/types.d.ts +37 -1
  44. package/lib/types/runtime/agent-role-router.d.ts +90 -16
  45. package/lib/types/runtime/host-locale.d.ts +1 -1
  46. package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
  47. package/lib/types/runtime/mcp-config.d.ts +75 -13
  48. package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
  49. package/lib/types/runtime/mcp-overrides.d.ts +44 -0
  50. package/lib/types/runtime/mcp-redaction.d.ts +6 -0
  51. package/lib/types/runtime/server-config.d.ts +26 -1
  52. package/lib/types/runtime/subagent-catalog.d.ts +14 -2
  53. package/lib/types/runtime/tool-registry-observer.d.ts +5 -3
  54. package/package.json +8 -8
  55. package/schemas/1.0.0/mcp.schema.json +0 -22
  56. package/schemas/1.1.0/mcp.schema.json +120 -0
  57. package/schemas/1.1.0/plugin.schema.json +65 -0
  58. package/schemas/README.md +25 -19
  59. package/schemas/agent-plugins/spec.md +23 -9
  60. package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
  61. package/schemas/com.deepseek.harness/spec.md +52 -0
@@ -18,14 +18,17 @@
18
18
  * 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
19
19
  * `<root>/<name>/SKILL.md` collections.
20
20
  */
21
- import { realpath } from 'node:fs/promises';
21
+ import { realpath, readFile } from 'node:fs/promises';
22
22
  import { join, resolve } from 'node:path';
23
23
  import { isWithin, sanitizeId } from './paths.js';
24
24
  import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
25
+ import { EXTENSION_NAMESPACE, NAMESPACE_DIR, NAMESPACE_HOOKS_FILE, NAMESPACE_LSP_FILE } from '../model/layouts.js';
25
26
  import { componentDeclarations, hasSuiteManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
26
27
  import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
27
28
  import { discoverMarkdownResources, isUnknownArray } from './component-files.js';
28
29
  import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
30
+ import { parseLspServers } from './lsp-spec.js';
31
+ import { normalizeHookDocuments } from './project-hooks.js';
29
32
  import { runScanChain } from './scan-pipeline.js';
30
33
  const CONTAINER_DIRS = ['plugins', 'external_plugins', 'skills'];
31
34
  /**
@@ -327,10 +330,11 @@ export async function hasSkillFiles(dir) {
327
330
  /** Read one suite root into the normalized shape, or undefined when no manifest parses. */
328
331
  export async function readSuite(root, sourceId, dimension, hint, notes = []) {
329
332
  const errors = [];
333
+ const manifestNotes = [];
330
334
  const fallbacks = [];
331
335
  const declaredManifest = await hasSuiteManifest(root);
332
- let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks) : await syntheticManifest(root);
333
- if (declaredManifest && (manifest === undefined || errors.length > 0)) {
336
+ let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks, manifestNotes) : await syntheticManifest(root);
337
+ if (declaredManifest && manifest === undefined) {
334
338
  notes.push(...errors, ...fallbacks, `suite ${root}: rejected declared manifest`);
335
339
  return undefined;
336
340
  }
@@ -338,6 +342,7 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
338
342
  // higher-priority declaration was skipped visible without failing the suite.
339
343
  if (fallbacks.length > 0)
340
344
  notes.push(`suite ${root}: a higher-priority manifest was rejected`, ...fallbacks);
345
+ notes.push(...manifestNotes);
341
346
  // A declaration-only suite (official CC lsp plugins ship just a README):
342
347
  // the marketplace entry's inline lspServers are its manifest.
343
348
  if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
@@ -354,24 +359,41 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
354
359
  }
355
360
  if (manifest === undefined)
356
361
  return undefined;
362
+ // The portable v1 dialect reads only the spec's fixed locations plus this
363
+ // client's extension namespace (§6.1, §8.2): skills and MCP from their
364
+ // fixed locations, extension surfaces from `com.deepseek.harness/`.
365
+ // Marketplace-entry and hint declarations carry no inline components for
366
+ // this dialect, so `components` stays empty and every discovery call below
367
+ // takes its namespace path.
368
+ const portable = manifest.layout === 'agent-plugin-v1';
369
+ // The namespace seats (manifest data and directory) open only when the
370
+ // suite declares our namespace with a supported contract version; an
371
+ // unsupported or missing declaration reads the portable core alone.
372
+ const namespaceOpen = portable && (manifest.harness !== undefined || manifest.schemaVersion !== undefined) && namespaceReadable(manifest);
357
373
  let declared = manifest.components?.skills;
358
374
  if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
359
375
  declared = ['skills', ...(isUnknownArray(declared) ? declared : [declared])];
360
376
  }
361
- const skills = await discoverSkills(root, errors, declared);
377
+ if (portable)
378
+ await noteUnreadRootComponents(root, notes);
379
+ const skills = await discoverSkills(root, errors, declared, portable);
362
380
  if (manifest.layout === 'skill-collection' && skills.length === 0) {
363
381
  notes.push(...errors);
364
382
  return undefined;
365
383
  }
366
- const mcp = await discoverMcp(root, errors, manifest);
384
+ const mcp = await discoverMcp(root, errors, manifest, portable);
367
385
  // Effective component declarations include marketplace fallbacks and manifest overrides.
368
- const lsp = await discoverSuiteLsp(root, manifest, errors);
369
- const hooks = await discoverSuiteHooks(root, manifest, errors);
370
- const resources = {
371
- commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
372
- agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
373
- };
374
- const systemPrompt = await discoverSystemPrompt(root, manifest, errors);
386
+ const lsp = namespaceOpen ? await discoverNamespaceLsp(root, manifest, errors, notes) : await discoverSuiteLsp(root, manifest, errors);
387
+ const hooks = namespaceOpen ? await discoverNamespaceHooks(root, manifest, errors, notes) : await discoverSuiteHooks(root, manifest, errors);
388
+ const resources = namespaceOpen
389
+ ? await discoverNamespaceMarkdown(root, notes)
390
+ : portable
391
+ ? { commands: [], agents: [] }
392
+ : {
393
+ commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
394
+ agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
395
+ };
396
+ const systemPrompt = portable ? undefined : await discoverSystemPrompt(root, manifest, errors);
375
397
  const surfaces = await countSurfaces(root, skills, mcp, lsp);
376
398
  surfaces.commands = resources.commands.length;
377
399
  surfaces.agents = resources.agents.length;
@@ -428,3 +450,86 @@ async function syntheticManifest(root) {
428
450
  name
429
451
  };
430
452
  }
453
+ // ---------------------------------------------------------------------------
454
+ // Extension namespace surfaces (Agent Plugins §8.2)
455
+ //
456
+ // A portable v1 suite carries this client's extension surfaces in the
457
+ // top-level directory named after the namespace. Contents outside it — the
458
+ // dialect root directories other layouts read — are package files this
459
+ // manager does not mount for the portable dialect, and their presence is
460
+ // reported through scan notes rather than read silently.
461
+ // ---------------------------------------------------------------------------
462
+ /** Root component locations other dialects read; under v1 their presence is reported, never read. */
463
+ const ROOT_COMPONENT_DIRS = ['commands', 'agents', 'hooks'];
464
+ /** `.mcp.json` is an alternative MCP path the portable format forbids; `mcp.json` itself is the fixed location. */
465
+ const ROOT_COMPONENT_FILES = ['hooks.json', 'lsp.json', '.lsp.json', '.mcp.json'];
466
+ /**
467
+ * Whether the namespace contract allows reading the extension seats: the
468
+ * manifest must carry our namespace with a supported `schemaVersion`. A
469
+ * directory without manifest data stays closed until the suite declares the
470
+ * contract, so a name collision on the filesystem cannot silently activate
471
+ * extension reading.
472
+ */
473
+ function namespaceReadable(manifest) {
474
+ return manifest.harness !== undefined;
475
+ }
476
+ /** Report portable-suite content this manager does not mount, so nothing fails silently. */
477
+ async function noteUnreadRootComponents(root, notes) {
478
+ for (const dir of ROOT_COMPONENT_DIRS) {
479
+ if (await isDirectory(join(root, dir)))
480
+ notes.push(`suite ${root}: "${dir}/" is outside the portable layout and the ${EXTENSION_NAMESPACE} namespace; not read for this dialect`);
481
+ }
482
+ for (const file of ROOT_COMPONENT_FILES) {
483
+ if (await isFile(join(root, file)))
484
+ notes.push(`suite ${root}: "${file}" is outside the portable layout; not read for this dialect`);
485
+ }
486
+ }
487
+ /** Commands and agents from `<namespace>/commands/` and `<namespace>/agents/`. */
488
+ async function discoverNamespaceMarkdown(root, notes) {
489
+ const namespaceRoot = join(root, NAMESPACE_DIR);
490
+ if (!(await isDirectory(namespaceRoot)))
491
+ return { commands: [], agents: [] };
492
+ const errors = [];
493
+ const commands = await discoverMarkdownResources(namespaceRoot, 'commands', undefined, join(namespaceRoot, 'plugin.json'), errors);
494
+ const agents = await discoverMarkdownResources(namespaceRoot, 'agents', undefined, join(namespaceRoot, 'plugin.json'), errors);
495
+ notes.push(...errors.map(error => `${EXTENSION_NAMESPACE}: ${error}`));
496
+ return { commands, agents };
497
+ }
498
+ /** Hooks from `<namespace>/hooks/hooks.json`, normalized through the command-hook bridge. */
499
+ async function discoverNamespaceHooks(root, manifest, errors, notes) {
500
+ const file = join(root, NAMESPACE_HOOKS_FILE);
501
+ if (!(await isFile(file)))
502
+ return undefined;
503
+ let value;
504
+ try {
505
+ value = JSON.parse(await readFile(file, 'utf8'));
506
+ }
507
+ catch (error) {
508
+ notes.push(`${NAMESPACE_HOOKS_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
509
+ return undefined;
510
+ }
511
+ const events = value?.hooks ?? value;
512
+ if (typeof events !== 'object' || events === null) {
513
+ notes.push(`${NAMESPACE_HOOKS_FILE}: must contain an event table`);
514
+ return undefined;
515
+ }
516
+ return normalizeHookDocuments(root, [{ file: NAMESPACE_HOOKS_FILE, settings: { hooks: events } }], errors);
517
+ }
518
+ /** LSP servers from `<namespace>/lsp.json`. */
519
+ async function discoverNamespaceLsp(root, manifest, errors, notes) {
520
+ const file = join(root, NAMESPACE_LSP_FILE);
521
+ if (!(await isFile(file)))
522
+ return undefined;
523
+ let value;
524
+ try {
525
+ value = JSON.parse(await readFile(file, 'utf8'));
526
+ }
527
+ catch (error) {
528
+ notes.push(`${NAMESPACE_LSP_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
529
+ return undefined;
530
+ }
531
+ const servers = Object.create(null);
532
+ const document = value.lspServers ?? value;
533
+ Object.assign(servers, parseLspServers(document, notes));
534
+ return Object.keys(servers).length > 0 ? { servers } : undefined;
535
+ }
@@ -10,10 +10,10 @@
10
10
  * thrown discovery.
11
11
  */
12
12
  import { readFile, readdir, stat } from 'node:fs/promises';
13
- import { dirname, join } from 'node:path';
13
+ import { dirname, join, relative } from 'node:path';
14
14
  import { parseSkillFrontmatter } from './skills-parse.js';
15
15
  import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
16
- import { validateMcpJson } from './validate.js';
16
+ import { validateMcpJson, pathContainmentError, recognizedSpecVersion } from './validate.js';
17
17
  import { readManifest } from './manifests.js';
18
18
  import { componentDocuments, componentPath, firstComponentFile, isUnknownArray } from './component-files.js';
19
19
  /**
@@ -38,16 +38,29 @@ async function declaredSkillDirs(root, declared, errors) {
38
38
  }
39
39
  return dirs;
40
40
  }
41
- /** Discover SKILL.md files under the suite's skills directory, up to 3 levels deep. */
42
- export async function discoverSkills(root, errors, declared) {
41
+ /**
42
+ * Discover SKILL.md files under the suite's skills directory, up to 3 levels
43
+ * deep. Portable v1 suites (§7.1) instead discover exactly one level of
44
+ * `skills/` subdirectories each carrying a `SKILL.md` — the spec forbids
45
+ * recursive deeper search and root-level or flat skill files are outside the
46
+ * portable discovery shape.
47
+ */
48
+ export async function discoverSkills(root, errors, declared, portable = false) {
43
49
  const skills = [];
44
- const rootSkill = join(root, 'SKILL.md');
45
- const rootName = root.split(/[\\/]/).at(-1) ?? 'plugin';
46
- const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
47
- if (rootParsed !== undefined)
48
- skills.push(rootParsed);
49
50
  const skillsDirs = await declaredSkillDirs(root, declared, errors);
50
- if (declared === undefined) {
51
+ if (!portable) {
52
+ const rootSkill = join(root, 'SKILL.md');
53
+ const rootName = root.split(/[\\/]/).at(-1) ?? 'plugin';
54
+ const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
55
+ if (rootParsed !== undefined)
56
+ skills.push(rootParsed);
57
+ if (declared === undefined) {
58
+ const fallback = join(root, 'skills');
59
+ if (await isDirectory(fallback))
60
+ skillsDirs.push(fallback);
61
+ }
62
+ }
63
+ else if (declared === undefined) {
51
64
  const fallback = join(root, 'skills');
52
65
  if (await isDirectory(fallback))
53
66
  skillsDirs.push(fallback);
@@ -60,6 +73,25 @@ export async function discoverSkills(root, errors, declared) {
60
73
  skills.push(skill);
61
74
  };
62
75
  for (const skillsDir of skillsDirs) {
76
+ if (portable) {
77
+ // §7.1: each immediate child directory containing `SKILL.md` is one
78
+ // skill; no deeper descendants are searched and flat files are not
79
+ // skills. Each candidate also passes a realpath containment check so a
80
+ // symlink pointing outside the plugin root is skipped (§4.1 boundary 3).
81
+ for (const child of await listChildDirs(skillsDir)) {
82
+ const skillFile = join(child, 'SKILL.md');
83
+ if (!(await isFile(skillFile)))
84
+ continue;
85
+ const reason = await pathContainmentError(root, `./${relative(root, skillFile).replace(/\\/g, '/')}`);
86
+ if (reason !== undefined) {
87
+ errors.push(`skill "${child.split(/[\\/]/).at(-1) ?? ''}": ${reason}`);
88
+ continue;
89
+ }
90
+ const name = child.split(/[\\/]/).at(-1) ?? '';
91
+ pushUnique(await parseOneSkill(skillFile, child, name, errors));
92
+ }
93
+ continue;
94
+ }
63
95
  if (await isFile(skillsDir)) {
64
96
  pushUnique(await parseOneSkill(skillsDir, dirname(skillsDir), '', errors));
65
97
  continue;
@@ -145,23 +177,34 @@ async function parseOneSkill(file, directory, fallbackName, errors) {
145
177
  }
146
178
  return { ...verdict, directory };
147
179
  }
148
- /** Read the suite's MCP config: `mcp.json` or `.mcp.json`, else the winning manifest's inline `mcpServers`. */
149
- export async function discoverMcp(root, errors, manifest) {
180
+ /**
181
+ * Read the suite's MCP config: `mcp.json` or `.mcp.json`, else the winning
182
+ * manifest's inline `mcpServers`. Portable v1 suites read only `mcp.json` at
183
+ * the plugin root (§7.2.1: no alternative core path, no inline declarations).
184
+ */
185
+ export async function discoverMcp(root, errors, manifest, portable = false) {
150
186
  const resolved = manifest ?? (await readManifest(root, errors, undefined));
151
187
  const layout = resolved?.layout;
152
- const names = layout === 'zcode'
153
- ? ['.mcp.json']
154
- : layout === 'qoder'
155
- ? ['.mcp.json', 'mcp.json']
156
- : layout === 'github-copilot' || layout === 'universal'
157
- ? ['.mcp.json', '.github/mcp.json', 'mcp.json']
158
- : layout === 'codex' || layout === 'claude-code'
159
- ? ['.mcp.json', 'mcp.json']
160
- : layout === 'cursor'
161
- ? ['mcp.json']
162
- : ['mcp.json', '.mcp.json'];
163
- const declared = resolved?.components?.mcpServers;
164
- const fallback = layout === 'kimi' ? undefined : await firstComponentFile(root, names);
188
+ const names = portable
189
+ ? ['mcp.json']
190
+ : layout === 'zcode'
191
+ ? ['.mcp.json']
192
+ : layout === 'qoder'
193
+ ? ['.mcp.json', 'mcp.json']
194
+ : layout === 'github-copilot' || layout === 'universal'
195
+ ? ['.mcp.json', '.github/mcp.json', 'mcp.json']
196
+ : layout === 'codex' || layout === 'claude-code'
197
+ ? ['.mcp.json', 'mcp.json']
198
+ : layout === 'cursor'
199
+ ? ['mcp.json']
200
+ : ['mcp.json', '.mcp.json'];
201
+ const declared = portable ? undefined : resolved?.components?.mcpServers;
202
+ // Portable mode reads exactly the fixed location `mcp.json` (§7.2.1), so
203
+ // the candidate list is the location itself rather than a fallback probe.
204
+ // The file is optional for portable suites too — §7.2 fixes its location,
205
+ // not its presence — so an absent file resolves to zero servers without a
206
+ // diagnostic; an existing but unreadable path still fails loudly below.
207
+ const fallback = portable ? ((await isFile(join(root, 'mcp.json'))) ? 'mcp.json' : undefined) : layout === 'kimi' ? undefined : await firstComponentFile(root, names);
165
208
  const additive = layout === 'zcode' || layout === 'claude-code';
166
209
  const values = declared === undefined
167
210
  ? fallback === undefined
@@ -177,8 +220,11 @@ export async function discoverMcp(root, errors, manifest) {
177
220
  let schema = '';
178
221
  for (const document of documents) {
179
222
  const isBareFile = document.path === join(root, 'mcp.json');
180
- const strict = isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder'));
181
- const result = await validateMcpJson(root, document.value, { strict });
223
+ const strict = portable || (isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder')));
224
+ const result = await validateMcpJson(root, document.value, {
225
+ strict,
226
+ ...(portable && resolved?.schemaVersion !== undefined ? { manifestVersion: recognizedSpecVersion(resolved.schemaVersion)?.version } : {})
227
+ });
182
228
  errors.push(...result.errors);
183
229
  if (result.config === undefined)
184
230
  return undefined;