dsh-agent-plugins-market 0.6.0 → 0.6.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 (134) hide show
  1. package/README.md +143 -169
  2. package/README.zh.md +136 -170
  3. package/client/client.js +8202 -684
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog.js +236 -47
  6. package/lib/application/details.js +19 -10
  7. package/lib/application/panel-resources.js +95 -0
  8. package/lib/application/project-agent-roles.js +39 -0
  9. package/lib/catalog/codex-mcp.js +114 -0
  10. package/lib/catalog/component-files.js +151 -0
  11. package/lib/catalog/manifests.js +105 -44
  12. package/lib/catalog/native-project.js +44 -23
  13. package/lib/catalog/paths.js +6 -6
  14. package/lib/catalog/project-config.js +96 -0
  15. package/lib/catalog/project-hooks.js +108 -0
  16. package/lib/catalog/scan-pipeline.js +6 -1
  17. package/lib/catalog/scan-resolvers.js +70 -26
  18. package/lib/catalog/source-catalog.js +16 -3
  19. package/lib/catalog/suite-components.js +125 -0
  20. package/lib/catalog/surfaces.js +60 -46
  21. package/lib/catalog/validate.js +16 -11
  22. package/lib/contracts/market.js +16 -1
  23. package/lib/index.js +187 -35
  24. package/lib/model/layouts.js +49 -0
  25. package/lib/routes.js +106 -6
  26. package/lib/runtime/agent-role-router.js +226 -0
  27. package/lib/runtime/commands-mounts.js +27 -67
  28. package/lib/runtime/feedback-tool.js +181 -0
  29. package/lib/runtime/hooks-mounts.js +66 -13
  30. package/lib/runtime/host-locale.js +23 -7
  31. package/lib/runtime/legacy-root-migration.js +12 -0
  32. package/lib/runtime/lsp-direct-config.js +6 -3
  33. package/lib/runtime/lsp-mounts.js +38 -24
  34. package/lib/runtime/lsp-server-state.js +19 -0
  35. package/lib/runtime/lsp-status.js +3 -2
  36. package/lib/runtime/mcp-backend.js +6 -2
  37. package/lib/runtime/mcp-client/config.js +6 -0
  38. package/lib/runtime/mcp-client/connection.js +8 -2
  39. package/lib/runtime/mcp-client/tools.js +6 -3
  40. package/lib/runtime/mcp-config.js +26 -13
  41. package/lib/runtime/mcp-direct-config.js +56 -0
  42. package/lib/runtime/mcp-mounts.js +25 -9
  43. package/lib/runtime/mcp-overrides.js +11 -5
  44. package/lib/runtime/mcp-status.js +10 -7
  45. package/lib/runtime/model-catalog.js +48 -0
  46. package/lib/runtime/project-runtime.js +136 -0
  47. package/lib/runtime/reconciler.js +38 -28
  48. package/lib/runtime/server-config.js +75 -0
  49. package/lib/runtime/skills-provider.js +31 -77
  50. package/lib/runtime/storage-migration.js +169 -0
  51. package/lib/runtime/subagent-catalog.js +129 -0
  52. package/lib/runtime/user-commands.js +86 -0
  53. package/lib/runtime/user-panels.js +176 -0
  54. package/lib/runtime/user-store.js +142 -0
  55. package/lib/types/application/catalog.d.ts +39 -9
  56. package/lib/types/application/panel-resources.d.ts +13 -0
  57. package/lib/types/application/project-agent-roles.d.ts +3 -0
  58. package/lib/types/application/queries.d.ts +17 -1
  59. package/lib/types/catalog/codex-mcp.d.ts +4 -0
  60. package/lib/types/catalog/component-files.d.ts +17 -0
  61. package/lib/types/catalog/manifests.d.ts +14 -9
  62. package/lib/types/catalog/native-project.d.ts +2 -5
  63. package/lib/types/catalog/paths.d.ts +4 -4
  64. package/lib/types/catalog/project-config.d.ts +6 -0
  65. package/lib/types/catalog/project-hooks.d.ts +10 -0
  66. package/lib/types/catalog/scan-pipeline.d.ts +2 -0
  67. package/lib/types/catalog/scan-resolvers.d.ts +4 -3
  68. package/lib/types/catalog/source-catalog.d.ts +1 -1
  69. package/lib/types/catalog/suite-components.d.ts +5 -0
  70. package/lib/types/catalog/surfaces.d.ts +2 -2
  71. package/lib/types/catalog/validate.d.ts +2 -0
  72. package/lib/types/contracts/market.d.ts +60 -0
  73. package/lib/types/contracts/mcp-status.d.ts +5 -1
  74. package/lib/types/index.d.ts +2 -2
  75. package/lib/types/model/layouts.d.ts +102 -0
  76. package/lib/types/model/types.d.ts +51 -6
  77. package/lib/types/routes.d.ts +6 -1
  78. package/lib/types/runtime/agent-role-router.d.ts +95 -0
  79. package/lib/types/runtime/commands-mounts.d.ts +16 -7
  80. package/lib/types/runtime/feedback-tool.d.ts +49 -0
  81. package/lib/types/runtime/hooks-mounts.d.ts +4 -0
  82. package/lib/types/runtime/host-locale.d.ts +1 -1
  83. package/lib/types/runtime/legacy-root-migration.d.ts +2 -0
  84. package/lib/types/runtime/lsp-mounts.d.ts +4 -0
  85. package/lib/types/runtime/lsp-server-state.d.ts +3 -0
  86. package/lib/types/runtime/lsp-status.d.ts +1 -0
  87. package/lib/types/runtime/mcp-backend.d.ts +9 -1
  88. package/lib/types/runtime/mcp-client/config.d.ts +4 -3
  89. package/lib/types/runtime/mcp-client/tools.d.ts +3 -0
  90. package/lib/types/runtime/mcp-config.d.ts +11 -5
  91. package/lib/types/runtime/mcp-direct-config.d.ts +7 -0
  92. package/lib/types/runtime/mcp-mounts.d.ts +2 -1
  93. package/lib/types/runtime/mcp-overrides.d.ts +2 -0
  94. package/lib/types/runtime/mcp-status.d.ts +1 -1
  95. package/lib/types/runtime/model-catalog.d.ts +7 -0
  96. package/lib/types/runtime/project-runtime.d.ts +32 -0
  97. package/lib/types/runtime/reconciler.d.ts +2 -0
  98. package/lib/types/runtime/server-config.d.ts +10 -0
  99. package/lib/types/runtime/skills-provider.d.ts +1 -5
  100. package/lib/types/runtime/storage-migration.d.ts +15 -0
  101. package/lib/types/runtime/subagent-catalog.d.ts +52 -0
  102. package/lib/types/runtime/user-commands.d.ts +24 -0
  103. package/lib/types/runtime/user-panels.d.ts +91 -0
  104. package/lib/types/runtime/user-store.d.ts +51 -0
  105. package/package.json +10 -3
  106. package/schemas/README.md +69 -7
  107. package/schemas/agent-plugins/spec.md +48 -0
  108. package/schemas/claude-code/marketplace.schema.json +143 -0
  109. package/schemas/claude-code/plugin.schema.json +105 -0
  110. package/schemas/claude-code/spec.md +108 -0
  111. package/schemas/codex/marketplace.schema.json +78 -0
  112. package/schemas/codex/plugin.schema.json +77 -0
  113. package/schemas/codex/spec.md +91 -0
  114. package/schemas/cursor/marketplace.schema.json +82 -0
  115. package/schemas/cursor/plugin.schema.json +58 -0
  116. package/schemas/cursor/spec.md +92 -0
  117. package/schemas/github-copilot/marketplace.schema.json +107 -0
  118. package/schemas/github-copilot/plugin.schema.json +89 -0
  119. package/schemas/github-copilot/spec.md +105 -0
  120. package/schemas/kimi/marketplace.schema.json +38 -0
  121. package/schemas/kimi/plugin.schema.json +84 -0
  122. package/schemas/kimi/spec.md +94 -0
  123. package/schemas/qoder/marketplace.schema.json +115 -0
  124. package/schemas/qoder/plugin.schema.json +127 -0
  125. package/schemas/qoder/spec.md +111 -0
  126. package/schemas/skill-collection/spec.md +29 -0
  127. package/schemas/universal/marketplace.schema.json +98 -0
  128. package/schemas/universal/plugin.schema.json +49 -0
  129. package/schemas/universal/spec.md +53 -0
  130. package/schemas/zcode/marketplace.schema.json +161 -0
  131. package/schemas/zcode/plugin.schema.json +89 -0
  132. package/schemas/zcode/spec.md +102 -0
  133. package/lib/catalog/legacy-root-migration.js +0 -51
  134. package/lib/types/catalog/legacy-root-migration.d.ts +0 -14
@@ -9,12 +9,13 @@
9
9
  * fail-closed: broken files produce a diagnostic and are skipped, never a
10
10
  * thrown discovery.
11
11
  */
12
- import { readFile, readdir, realpath, stat } from 'node:fs/promises';
13
- import { join, resolve } from 'node:path';
12
+ import { readFile, readdir, stat } from 'node:fs/promises';
13
+ import { dirname, join } from 'node:path';
14
14
  import { parseSkillFrontmatter } from './skills-parse.js';
15
15
  import { isDirectory } from './paths.js';
16
16
  import { validateMcpJson } from './validate.js';
17
- import { declaredMcpServers } from './manifests.js';
17
+ import { readManifest } from './manifests.js';
18
+ import { componentDocuments, componentPath, firstComponentFile } from './component-files.js';
18
19
  const DOT_DIRS = new Set(['.git', '.github', '.claude', '.cursor', '.kimi', '.plugin', '.sources', 'node_modules']);
19
20
  /**
20
21
  * Resolve a manifest-declared skills path into absolute directories (string or
@@ -22,35 +23,22 @@ const DOT_DIRS = new Set(['.git', '.github', '.claude', '.cursor', '.kimi', '.pl
22
23
  * declared path may be (or pass through) a symlink whose target leaves the
23
24
  * suite root, which lexical resolution cannot see.
24
25
  */
25
- async function declaredSkillDirs(root, declared) {
26
+ async function declaredSkillDirs(root, declared, errors) {
26
27
  const values = Array.isArray(declared) ? declared : [declared];
27
28
  const dirs = [];
28
- const realRoot = await realpath(root).catch(() => root);
29
29
  for (const value of values) {
30
- if (typeof value !== 'string' || value === '')
30
+ if (typeof value !== 'string' || value === '') {
31
+ if (declared !== undefined)
32
+ errors.push('skills: paths must be non-empty strings');
31
33
  continue;
34
+ }
32
35
  const cleaned = value.replace(/^\.\//, '');
33
- const path = resolve(root, cleaned);
34
- // A missing path is not an escape; the discovery walk reports it absent.
35
- const realPath = await realpath(path).catch(() => undefined);
36
- if (realPath === undefined)
37
- continue;
38
- if (isWithin(realRoot, realPath))
36
+ const path = await componentPath(root, cleaned, errors);
37
+ if (path !== undefined)
39
38
  dirs.push(path);
40
39
  }
41
40
  return dirs;
42
41
  }
43
- /**
44
- * Whether `candidate` stays inside `root` once both are resolved. Callers pass
45
- * resolved paths: a sibling directory (`<root>-evil`) and any `../` escape
46
- * must be rejected — a bare string-prefix test admits both.
47
- */
48
- function isWithin(root, candidate) {
49
- if (candidate === root)
50
- return true;
51
- const separator = candidate.includes('\\') && !candidate.includes('/') ? '\\' : '/';
52
- return candidate.startsWith(`${root}${separator}`);
53
- }
54
42
  /** Discover SKILL.md files under the suite's skills directory, up to 3 levels deep. */
55
43
  export async function discoverSkills(root, errors, declared) {
56
44
  const skills = [];
@@ -59,13 +47,13 @@ export async function discoverSkills(root, errors, declared) {
59
47
  const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
60
48
  if (rootParsed !== undefined)
61
49
  skills.push(rootParsed);
62
- const skillsDirs = await declaredSkillDirs(root, declared);
63
- if (skillsDirs.length === 0) {
50
+ const skillsDirs = await declaredSkillDirs(root, declared, errors);
51
+ if (declared === undefined) {
64
52
  const fallback = join(root, 'skills');
65
53
  if (await isDirectory(fallback))
66
54
  skillsDirs.push(fallback);
67
55
  }
68
- const seen = new Set();
56
+ const seen = new Set(skills.map(skill => skill.name));
69
57
  const pushUnique = (skill) => {
70
58
  if (skill === undefined || seen.has(skill.name))
71
59
  return;
@@ -73,6 +61,13 @@ export async function discoverSkills(root, errors, declared) {
73
61
  skills.push(skill);
74
62
  };
75
63
  for (const skillsDir of skillsDirs) {
64
+ if (await isFile(skillsDir)) {
65
+ pushUnique(await parseOneSkill(skillsDir, dirname(skillsDir), '', errors));
66
+ continue;
67
+ }
68
+ for (const name of await listMdFiles(skillsDir)) {
69
+ pushUnique(await parseOneSkill(join(skillsDir, name), skillsDir, name.replace(/\.md$/, ''), errors));
70
+ }
76
71
  // A declared path may be one skill directory (a manifest listing
77
72
  // individual skills, e.g. mattpocock) or a container of skills.
78
73
  if (await isFile(join(skillsDir, 'SKILL.md'))) {
@@ -152,29 +147,48 @@ async function parseOneSkill(file, directory, fallbackName, errors) {
152
147
  return { ...verdict, directory };
153
148
  }
154
149
  /** Read the suite's MCP config: `mcp.json` or `.mcp.json`, else the winning manifest's inline `mcpServers`. */
155
- export async function discoverMcp(root, errors) {
156
- for (const name of ['mcp.json', '.mcp.json']) {
157
- const path = join(root, name);
158
- if (!(await isFile(path)))
159
- continue;
160
- let raw;
161
- try {
162
- raw = JSON.parse(await readFile(path, 'utf8'));
163
- }
164
- catch (error) {
165
- errors.push(`${name} unparsable: ${error instanceof Error ? error.message : String(error)}`);
150
+ export async function discoverMcp(root, errors, manifest) {
151
+ const resolved = manifest ?? (await readManifest(root, errors, undefined));
152
+ const layout = resolved?.layout;
153
+ const names = layout === 'zcode'
154
+ ? ['.mcp.json']
155
+ : layout === 'qoder'
156
+ ? ['.mcp.json', 'mcp.json']
157
+ : layout === 'github-copilot' || layout === 'universal'
158
+ ? ['.mcp.json', '.github/mcp.json', 'mcp.json']
159
+ : layout === 'codex' || layout === 'claude-code'
160
+ ? ['.mcp.json', 'mcp.json']
161
+ : layout === 'cursor'
162
+ ? ['mcp.json']
163
+ : ['mcp.json', '.mcp.json'];
164
+ const declared = resolved?.components?.mcpServers;
165
+ const fallback = layout === 'kimi' ? undefined : await firstComponentFile(root, names);
166
+ const additive = layout === 'zcode' || layout === 'claude-code';
167
+ const values = declared === undefined
168
+ ? fallback === undefined
169
+ ? []
170
+ : [fallback]
171
+ : [...(additive && fallback !== undefined ? [fallback] : []), ...(Array.isArray(declared) ? declared : [declared])];
172
+ if (values.length === 0)
173
+ return undefined;
174
+ const documents = await componentDocuments(root, values, errors);
175
+ if (documents === undefined)
176
+ return undefined;
177
+ const servers = Object.create(null);
178
+ let schema = '';
179
+ for (const document of documents) {
180
+ const isBareFile = document.path === join(root, 'mcp.json');
181
+ const strict = isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder'));
182
+ const result = await validateMcpJson(root, document.value, { strict });
183
+ errors.push(...result.errors);
184
+ if (result.config === undefined)
166
185
  return undefined;
186
+ if (result.config !== undefined) {
187
+ Object.assign(servers, result.config.servers);
188
+ schema = result.config.schema;
167
189
  }
168
- const result = await validateMcpJson(root, raw, { strict: name === 'mcp.json' });
169
- errors.push(...result.errors);
170
- return result.config;
171
190
  }
172
- const inline = await declaredMcpServers(root);
173
- if (inline === undefined)
174
- return undefined;
175
- const result = await validateMcpJson(root, { mcpServers: inline }, { strict: false });
176
- errors.push(...result.errors);
177
- return result.config;
191
+ return { schema, servers };
178
192
  }
179
193
  /** Count surfaces for a suite; mcp counts only validated servers, lsp counts inline servers plus directory entries. */
180
194
  export async function countSurfaces(root, skills, mcp, lsp) {
@@ -14,6 +14,7 @@ import { fileURLToPath } from 'node:url';
14
14
  // The 2020-12 dist build's d.ts resolves to a CJS namespace under NodeNext;
15
15
  // the runtime default export is the class itself (module.exports = Ajv2020).
16
16
  import Ajv2020Default from 'ajv/dist/2020.js';
17
+ import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../model/layouts.js';
17
18
  const SCHEMAS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas', '1.0.0');
18
19
  const Ajv2020Class = Ajv2020Default;
19
20
  export const PLUGIN_SCHEMA_ID = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json';
@@ -51,7 +52,7 @@ export function isRecognizedSchema(value) {
51
52
  * @returns `undefined` when contained, or the rejection reason.
52
53
  */
53
54
  export async function pathContainmentError(pluginRoot, value) {
54
- const raw = value.replace(/^\$\{PLUGIN_ROOT\}(\/|$)/, './');
55
+ const raw = value.replace(/^\$\{([A-Z_]+)\}(\/|$)/, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? './' : match));
55
56
  if (!raw.startsWith('./'))
56
57
  return `path "${value}" must begin with "./" (or ${'${PLUGIN_ROOT}'})`;
57
58
  const candidate = resolve(pluginRoot, raw.slice(2));
@@ -118,7 +119,7 @@ export async function validateMcpJson(pluginRoot, raw, options) {
118
119
  }
119
120
  servers = record;
120
121
  }
121
- const valid = {};
122
+ const valid = Object.create(null);
122
123
  for (const [name, value] of Object.entries(servers)) {
123
124
  if (typeof value !== 'object' || value === null) {
124
125
  errors.push(`server "${name}": not an object`);
@@ -127,7 +128,7 @@ export async function validateMcpJson(pluginRoot, raw, options) {
127
128
  const server = value;
128
129
  // Claude Code dialects: `http`/`streamable-http` are the same remote
129
130
  // transport, and `local` or a missing `type` (with `command`) mean stdio.
130
- let type = typeof server.type === 'string' ? server.type : server.command !== undefined ? 'stdio' : undefined;
131
+ let type = typeof server.type === 'string' ? server.type : server.command !== undefined ? 'stdio' : !strict && 'url' in server ? 'streamable-http' : undefined;
131
132
  if (type === 'local')
132
133
  type = 'stdio';
133
134
  if (type === 'http')
@@ -146,17 +147,21 @@ export async function validateMcpJson(pluginRoot, raw, options) {
146
147
  errors.push(`server "${name}": stdio servers require a command`);
147
148
  continue;
148
149
  }
149
- if (stdioServer.command.includes('/')) {
150
- if (!stdioServer.command.startsWith('./')) {
151
- problems.push(`command "${stdioServer.command}" must be a bare executable name or a plugin-relative path beginning with "./"`);
150
+ const command = stdioServer.command.replace(/^\$\{([A-Z_]+)\}\//, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? './' : match));
151
+ if (options?.pathMode !== 'project' && command.includes('/')) {
152
+ if (!command.startsWith('./')) {
153
+ problems.push(`command "${command}" must be a bare executable name or a plugin-relative path beginning with "./"`);
152
154
  }
153
155
  else {
154
- const reason = await pathContainmentError(pluginRoot, stdioServer.command);
156
+ const reason = await pathContainmentError(pluginRoot, command);
155
157
  if (reason !== undefined)
156
158
  problems.push(reason);
157
159
  }
158
160
  }
159
- if (stdioServer.cwd !== undefined && !stdioServer.cwd.startsWith('${PLUGIN_DATA}')) {
161
+ if (stdioServer.cwd !== undefined && typeof stdioServer.cwd !== 'string') {
162
+ problems.push('cwd must be a string');
163
+ }
164
+ else if (options?.pathMode !== 'project' && stdioServer.cwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => stdioServer.cwd.startsWith(`\${${name}}`))) {
160
165
  // `.` is the Codex dialect spelling for the plugin root.
161
166
  const cwd = stdioServer.cwd === '.' ? './' : stdioServer.cwd;
162
167
  const reason = await pathContainmentError(pluginRoot, cwd);
@@ -167,7 +172,7 @@ export async function validateMcpJson(pluginRoot, raw, options) {
167
172
  errors.push(...problems.map(problem => `server "${name}": ${problem}`));
168
173
  }
169
174
  else {
170
- valid[name] = { ...server, type: 'stdio' };
175
+ valid[name] = { ...server, command, type: 'stdio' };
171
176
  }
172
177
  }
173
178
  return { config: { schema: typeof record['$schema'] === 'string' ? record['$schema'] : 'native-client', servers: valid }, errors };
@@ -177,9 +182,9 @@ export async function validateMcpJson(pluginRoot, raw, options) {
177
182
  * `${NAME:-default}` fallbacks are honored for unset/empty env vars. */
178
183
  export function expandPlaceholders(value, pluginRoot, pluginData, env = process.env) {
179
184
  return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => {
180
- if (name === 'PLUGIN_ROOT' || name === 'CLAUDE_PLUGIN_ROOT')
185
+ if (PLUGIN_ROOT_VARIABLES.has(name))
181
186
  return pluginRoot;
182
- if (name === 'PLUGIN_DATA' || name === 'CLAUDE_PLUGIN_DATA')
187
+ if (PLUGIN_DATA_VARIABLES.has(name))
183
188
  return pluginData;
184
189
  return env[name] || fallback || '';
185
190
  });
@@ -5,10 +5,15 @@ export const MARKET_API_PREFIX = '/api/agent-plugins/';
5
5
  export const MARKET_ROUTES = {
6
6
  overview: `${MARKET_API_PREFIX}overview`,
7
7
  mcpStatus: `${MARKET_API_PREFIX}mcp-status`,
8
+ addMcpServer: `${MARKET_API_PREFIX}mcp-servers/add`,
8
9
  lspStatus: `${MARKET_API_PREFIX}lsp-status`,
9
10
  lspServers: `${MARKET_API_PREFIX}lsp-servers`,
11
+ addLspServer: `${MARKET_API_PREFIX}lsp-servers/add`,
12
+ serverConfig: `${MARKET_API_PREFIX}server-config`,
13
+ saveServerConfig: `${MARKET_API_PREFIX}server-config/save`,
10
14
  progress: `${MARKET_API_PREFIX}progress`,
11
15
  config: `${MARKET_API_PREFIX}config`,
16
+ modelCatalog: `${MARKET_API_PREFIX}model-catalog`,
12
17
  suite: `${MARKET_API_PREFIX}suite`,
13
18
  skill: `${MARKET_API_PREFIX}skill`,
14
19
  addSource: `${MARKET_API_PREFIX}sources/add`,
@@ -25,8 +30,18 @@ export const MARKET_ROUTES = {
25
30
  mcpRetry: `${MARKET_API_PREFIX}mcp-retry`,
26
31
  mcpReauthorize: `${MARKET_API_PREFIX}mcp-reauthorize`,
27
32
  mcpBackend: `${MARKET_API_PREFIX}mcp-backend`,
28
- setMcpBackend: `${MARKET_API_PREFIX}set-mcp-backend`
33
+ setMcpBackend: `${MARKET_API_PREFIX}set-mcp-backend`,
34
+ userPanel: `${MARKET_API_PREFIX}user-panel`
29
35
  };
36
+ /** Build one user-panel list route URL (`GET`, no mutation). */
37
+ export function userPanelRoute(kind) {
38
+ return `${MARKET_ROUTES.userPanel}/${kind}`;
39
+ }
40
+ /** Build one user-panel mutation route URL (all POSTs). */
41
+ export function userPanelMutationRoute(kind, mutation, name) {
42
+ const base = `${userPanelRoute(kind)}/${mutation}`;
43
+ return name === undefined ? base : `${base}?name=${encodeURIComponent(name)}`;
44
+ }
30
45
  /** Build a suite-detail URL without duplicating route or query encoding logic. */
31
46
  export function suiteRoute(sourceId, suiteId) {
32
47
  return `${MARKET_ROUTES.suite}?sourceId=${encodeURIComponent(sourceId)}&suiteId=${encodeURIComponent(suiteId)}`;
package/lib/index.js CHANGED
@@ -1,38 +1,62 @@
1
1
  import { Catalog } from './application/catalog.js';
2
2
  import { RuntimeReconciler } from './runtime/reconciler.js';
3
3
  import { inspectToolRegistry } from './runtime/tool-registry-observer.js';
4
- import { migrateLegacyDataRoot } from './catalog/legacy-root-migration.js';
5
- import { resolveDataRoot, resolveDshHome, resolveUserRoot } from './catalog/paths.js';
6
- import { join } from 'node:path';
4
+ import { migratePluginStorage } from './runtime/storage-migration.js';
5
+ import { mountAgentRoleTool } from './runtime/agent-role-router.js';
6
+ import { projectAgentRoles } from './application/project-agent-roles.js';
7
+ import { mountProjectCommands, mountProjectMcp, mountProjectHooks, mountSuiteInstructions } from './runtime/project-runtime.js';
8
+ import { createPanelResources } from './application/panel-resources.js';
9
+ import { resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
7
10
  import { mountSuiteRoutes } from './routes.js';
8
11
  import { MCP_SETTINGS_NAMESPACE, MarketSettingsSchema, readMcpBackend } from './runtime/mcp-backend.js';
9
12
  import { narrowDownloadRegion } from './runtime/regions.js';
10
13
  import { SuiteSkillProvider } from './runtime/skills-provider.js';
11
14
  import { loadLspServers } from './runtime/lsp-direct-config.js';
15
+ import { loadDisabledLspServers } from './runtime/lsp-server-state.js';
12
16
  import { bindHostLocale, loadHostLocale } from './runtime/host-locale.js';
17
+ import { mountFeedbackTool } from './runtime/feedback-tool.js';
18
+ import { createUserPanelStores } from './runtime/user-panels.js';
19
+ import { UserPanelSkillProvider } from './runtime/user-panels.js';
20
+ import { UserCommandMountRegistry } from './runtime/user-commands.js';
13
21
  export const name = 'dsh-agent-plugins-market';
14
22
  export const inject = ['skills', 'commands'];
15
- export function apply(ctx, config = {}) {
23
+ export async function apply(ctx, config = {}) {
16
24
  const userRoot = resolveUserRoot(config.userRoot);
17
25
  const dataRoot = resolveDataRoot(config.dataRoot, userRoot);
18
- // The pre-0.5.4 layout kept a sibling `agent-plugins-data` root; fold it in.
19
- void migrateLegacyDataRoot(join(resolveDshHome(), 'agent-plugins-data'), dataRoot).catch(error => {
20
- ctx.logger?.warn?.(`[dsh-agent-plugins-market] legacy data-root migration failed: ${String(error)}`);
21
- });
26
+ const migration = await migratePluginStorage(config);
27
+ if (migration.conflicts.length > 0)
28
+ throw new Error(`Plugin storage migration conflicts (original files retained): ${migration.conflicts.join(', ')}`);
22
29
  let providerControl;
30
+ let userPanelControl;
23
31
  // Host runtime copy resolves from the harness `locale.preference` setting;
24
32
  // the async settings read lands before the first session starts in practice.
25
33
  const t = bindHostLocale(undefined);
26
34
  void loadHostLocale().then(locale => {
27
35
  hostLocale.t = locale.t;
28
36
  providerControl?.invalidate();
37
+ userPanelControl?.invalidate();
29
38
  });
30
39
  const hostLocale = { t };
40
+ /** Settings watchers to release at teardown (backend watch + feedback switch). */
41
+ const settingsWatchers = [];
31
42
  const runtime = new RuntimeReconciler(ctx, dataRoot, key => hostLocale.t(key));
32
- const reconcileMounts = async () => {
43
+ // User panel stores (skills / commands / agent personas) and their runtime
44
+ // contributions: one extra skill provider plus one command mount registry.
45
+ const panels = createUserPanelStores(userRoot);
46
+ const userCommands = new UserCommandMountRegistry(ctx, panels.commands, key => hostLocale.t(key));
47
+ let disposed = false;
48
+ let projectCommands;
49
+ let projectMcp;
50
+ let projectHooks;
51
+ let suitePrompts;
52
+ let reconcileRequested = false;
53
+ let reconciliation;
54
+ const reconcileOnce = async () => {
33
55
  try {
34
- const snapshot = await catalog.readUserCatalog();
35
- const diagnostics = await runtime.reconcile(snapshot.enabledSuites);
56
+ const suites = await catalog.enabledUserSuites();
57
+ if (disposed)
58
+ return;
59
+ const diagnostics = await runtime.reconcile(suites);
36
60
  catalog.mcpDiagnostics = diagnostics.mcp;
37
61
  for (const diagnostic of diagnostics.mcp) {
38
62
  ctx.logger?.warn(`[dsh-agent-plugins-market] suite "${diagnostic.suiteId}" mcp server "${diagnostic.serverKey}": ${diagnostic.reason}`);
@@ -56,6 +80,26 @@ export function apply(ctx, config = {}) {
56
80
  throw error;
57
81
  }
58
82
  };
83
+ // Settings, credential and catalog events can arrive during a slow MCP
84
+ // connection. Keep one current pass and one fresh follow-up, not a queue
85
+ // of stale snapshots that all reconnect the same servers.
86
+ const reconcileMounts = () => {
87
+ if (disposed)
88
+ return Promise.resolve();
89
+ reconcileRequested = true;
90
+ reconciliation ??= (async () => {
91
+ try {
92
+ do {
93
+ reconcileRequested = false;
94
+ await reconcileOnce();
95
+ } while (reconcileRequested && !disposed);
96
+ }
97
+ finally {
98
+ reconciliation = undefined;
99
+ }
100
+ })();
101
+ return reconciliation;
102
+ };
59
103
  /**
60
104
  * Coalesce credential updates.
61
105
  *
@@ -78,7 +122,17 @@ export function apply(ctx, config = {}) {
78
122
  }
79
123
  };
80
124
  const onChanged = async () => {
125
+ if (disposed)
126
+ return;
81
127
  providerControl?.invalidate();
128
+ userPanelControl?.invalidate();
129
+ await projectCommands?.refresh();
130
+ await suitePrompts?.refresh();
131
+ await Promise.all([projectMcp?.refresh(), projectHooks?.refresh()]);
132
+ const userCommandDiagnostics = await userCommands.reconcile().catch(() => []);
133
+ for (const reason of userCommandDiagnostics) {
134
+ ctx.logger?.warn(`[dsh-agent-plugins-market] user commands: ${reason}`);
135
+ }
82
136
  await reconcileMounts();
83
137
  };
84
138
  // The credential store powers the MCP re-authorize action (dropping a grant
@@ -87,6 +141,9 @@ export function apply(ctx, config = {}) {
87
141
  // credentials plugin provisions, and a snapshot taken here would be
88
142
  // permanently undefined even after the service is live.
89
143
  const catalog = new Catalog({ userRoot, dataRoot, onChanged, ...(config.git === undefined ? {} : { git: config.git }) });
144
+ await catalog.load();
145
+ await catalog.mergeSources(config.sources ?? []);
146
+ const resources = createPanelResources(catalog, panels);
90
147
  // Mirror oauth.ts's `credentialIdFor`: the record is stored under the folded
91
148
  // serverName, so the delete must fold the same way or it misses the record.
92
149
  const credentialIdFor = (serverName) => serverName
@@ -101,35 +158,87 @@ export function apply(ctx, config = {}) {
101
158
  await store.deleteRecord(`mcp-auth/${credentialIdFor(serverName)}`);
102
159
  }
103
160
  });
104
- runtime.setMcpOverridesProvider(() => catalog.allMcpOverrides());
161
+ runtime.setMcpOverridesProvider(async () => catalog.allMcpOverrides(await catalog.enabledUserSuites()));
105
162
  // Re-authorize must rebuild the live bridge, not just drop the grant: the
106
163
  // registry flags the mount and the following reconcile tears it down and
107
164
  // remounts, so the server's 401 restarts the browser authorization.
108
165
  catalog.setMcpRemountHook((suiteId, serverKey) => runtime.forceMcpRemount(suiteId, serverKey), serverName => runtime.mcpServerOwner(serverName));
109
166
  catalog.setLspStatusSource(runtime.lsp);
110
167
  runtime.lsp.setDirectProvider(async () => (await loadLspServers(dataRoot)).servers);
168
+ runtime.lsp.setDisabledProvider(() => loadDisabledLspServers(dataRoot));
111
169
  // The MCP backend choice is a host settings namespace (the registration is
112
170
  // also what makes the plugin-config tab serve our card). The node half owns
113
171
  // it: the provider derives the mount backend from the scope, the writer
114
- // flips the switch, and the watcher remounts servers when it flips.
172
+ // flips the switch, and the watcher remounts servers when it flips. The
173
+ // same single registration also gates the experience-feedback tool —
174
+ // `settings.register` throws on a duplicate namespace, so there is exactly
175
+ // one inject block and one `register` call.
176
+ //
177
+ // The whole callback is failure-contained on purpose: a throw here is
178
+ // INVISIBLE in the UI (the inject resolves asynchronously and cordis only
179
+ // logs), and it silently removes the namespace from settings.describe —
180
+ // which is exactly what hides the plugin-config card. Every failure mode
181
+ // lands in the logger with a loud prefix instead.
182
+ let feedbackDisposer;
115
183
  ctx.inject(['settings'], settingsCtx => {
116
- const settings = settingsCtx.settings;
117
- const scope = settings.register(MCP_SETTINGS_NAMESPACE, MarketSettingsSchema, {
118
- base: { mcpEnhanced: true, downloadRegion: 'auto' }
119
- });
120
- catalog.setMcpBackendProvider(async () => (scope.get().mcpEnhanced === false ? 'host' : 'builtin'));
121
- catalog.setMcpBackendWriter(async (backend) => {
122
- await scope.update({ mcpEnhanced: backend !== 'host' });
123
- });
124
- catalog.setDownloadRegionProvider(async () => narrowDownloadRegion(scope.get().downloadRegion));
125
- // One-time migration from the earlier data-root settings.json choice.
126
- void readMcpBackend(dataRoot).then(backend => {
127
- if (backend === 'host')
128
- void scope.update({ mcpEnhanced: false }).catch(() => { });
129
- });
130
- scope.watch(() => {
131
- void reconcileMounts().catch(() => { });
132
- });
184
+ try {
185
+ ctx.logger?.info?.('[dsh-agent-plugins-market] settings inject resolved — registering namespace');
186
+ const settings = settingsCtx.settings;
187
+ const scope = settings.register(MCP_SETTINGS_NAMESPACE, MarketSettingsSchema, {
188
+ base: { mcpEnhanced: true, downloadRegion: 'auto', feedbackEnabled: true, scanProjectLayouts: true }
189
+ });
190
+ ctx.logger?.info?.('[dsh-agent-plugins-market] settings namespace registered — plugin-config card will serve');
191
+ catalog.setMcpBackendProvider(async () => (scope.get().mcpEnhanced === false ? 'host' : 'builtin'));
192
+ catalog.setMcpBackendWriter(async (backend) => {
193
+ await scope.update({ mcpEnhanced: backend !== 'host' });
194
+ });
195
+ catalog.setDownloadRegionProvider(async () => narrowDownloadRegion(scope.get().downloadRegion));
196
+ const syncProjectLayouts = () => {
197
+ void catalog.setScanProjectLayouts(scope.get().scanProjectLayouts !== false).catch(error => {
198
+ ctx.logger?.error?.(`[dsh-agent-plugins-market] project layout reconciliation failed: ${String(error)}`);
199
+ });
200
+ };
201
+ syncProjectLayouts();
202
+ settingsWatchers.push(scope.watch(syncProjectLayouts));
203
+ // One-time migration from the earlier data-root settings.json choice.
204
+ void readMcpBackend(dataRoot).then(backend => {
205
+ if (backend === 'host')
206
+ void scope.update({ mcpEnhanced: false }).catch(() => { });
207
+ });
208
+ let previousBackend = scope.get().mcpEnhanced !== false;
209
+ settingsWatchers.push(scope.watch(() => {
210
+ const backend = scope.get().mcpEnhanced !== false;
211
+ if (backend === previousBackend)
212
+ return;
213
+ previousBackend = backend;
214
+ void Promise.all([reconcileMounts(), projectMcp?.refresh()]).catch(() => { });
215
+ }));
216
+ // The experience-feedback model tool: gated by the namespace's
217
+ // `feedbackEnabled` field (default on); the switch unregisters it. The
218
+ // tool mount is doubly contained — its failure must never take the
219
+ // settings namespace (and with it the config card) down with it.
220
+ const syncFeedbackTool = () => {
221
+ try {
222
+ const state = scope.get();
223
+ const wanted = state.feedbackEnabled !== false;
224
+ if (wanted && feedbackDisposer === undefined) {
225
+ feedbackDisposer = mountFeedbackTool(ctx, dataRoot, (key, params) => hostLocale.t(key, params)) ?? undefined;
226
+ }
227
+ else if (!wanted && feedbackDisposer !== undefined) {
228
+ feedbackDisposer();
229
+ feedbackDisposer = undefined;
230
+ }
231
+ }
232
+ catch (error) {
233
+ ctx.logger?.error?.(`[dsh-agent-plugins-market] feedback tool mount failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
234
+ }
235
+ };
236
+ syncFeedbackTool();
237
+ settingsWatchers.push(scope.watch(() => syncFeedbackTool()));
238
+ }
239
+ catch (error) {
240
+ ctx.logger?.error?.(`[dsh-agent-plugins-market] settings namespace registration failed — the plugin-config card will be hidden this boot: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
241
+ }
133
242
  });
134
243
  runtime.setMcpBackendProvider(() => catalog.mcpBackend());
135
244
  const eventHost = ctx;
@@ -148,26 +257,69 @@ export function apply(ctx, config = {}) {
148
257
  // own server with a clear diagnostic instead of failing mid-registration.
149
258
  runtime.setMcpToolNamesProvider(() => inspectToolRegistry(registry).map(tool => tool.name));
150
259
  });
151
- void catalog
152
- .load()
260
+ void Promise.resolve()
153
261
  .then(async () => {
154
- await catalog.mergeSources(config.sources ?? []);
262
+ const userCommandDiagnostics = await userCommands.reconcile().catch(() => []);
263
+ for (const reason of userCommandDiagnostics) {
264
+ ctx.logger?.warn(`[dsh-agent-plugins-market] user commands: ${reason}`);
265
+ }
155
266
  await reconcileMounts();
156
267
  })
157
268
  .catch(() => { });
158
269
  ctx.skills.registerProvider(control => {
159
270
  providerControl = control;
160
- return new SuiteSkillProvider(catalog, key => hostLocale.t(key));
271
+ return new SuiteSkillProvider(catalog);
272
+ });
273
+ // User panel skills ride a second provider so a panel
274
+ // edit invalidates only its own catalog contribution.
275
+ ctx.skills.registerProvider(control => {
276
+ userPanelControl = control;
277
+ return new UserPanelSkillProvider(panels.skills, key => hostLocale.t(key));
278
+ });
279
+ ctx.inject(['tools', 'llm', 'subagents', 'agents'], hostCtx => {
280
+ hostCtx.effect(() => mountAgentRoleTool(hostCtx, async (parent) => [
281
+ ...(await resources.agents.list(true)).map(entry => ({ ...entry, title: entry.name, name: entry.id ?? entry.name })),
282
+ ...(await projectAgentRoles(catalog, parent))
283
+ ], (key, params) => hostLocale.t(key, params)), 'dsh-agent-plugins-market: agent role routing');
284
+ });
285
+ ctx.inject(['agents'], hostCtx => {
286
+ hostCtx.effect(() => {
287
+ const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key));
288
+ const mcp = mountProjectMcp(hostCtx, catalog, dataRoot);
289
+ const hooks = mountProjectHooks(hostCtx, catalog);
290
+ const prompts = mountSuiteInstructions(hostCtx, catalog);
291
+ projectCommands = mounted;
292
+ projectMcp = mcp;
293
+ projectHooks = hooks;
294
+ suitePrompts = prompts;
295
+ return async () => {
296
+ if (projectCommands === mounted)
297
+ projectCommands = undefined;
298
+ if (projectMcp === mcp)
299
+ projectMcp = undefined;
300
+ if (projectHooks === hooks)
301
+ projectHooks = undefined;
302
+ if (suitePrompts === prompts)
303
+ suitePrompts = undefined;
304
+ await Promise.all([mounted.dispose(), mcp.dispose(), hooks.dispose(), prompts.dispose()]);
305
+ };
306
+ }, 'dsh-agent-plugins-market: project command lifecycle');
161
307
  });
162
308
  ctx.inject(['webServer', 'loader'], hostCtx => {
163
- hostCtx.effect(() => mountSuiteRoutes(hostCtx, catalog), 'dsh-agent-plugins-market: http routes');
309
+ hostCtx.effect(() => mountSuiteRoutes(hostCtx, catalog, resources), 'dsh-agent-plugins-market: http routes');
164
310
  });
165
311
  ctx.effect(() => () => {
312
+ disposed = true;
166
313
  disposeCredentialUpdates();
167
314
  if (credentialFlush !== undefined)
168
315
  clearTimeout(credentialFlush);
169
316
  credentialFlush = undefined;
170
317
  pendingCredentialRefs.clear();
318
+ for (const dispose of settingsWatchers.splice(0))
319
+ dispose();
320
+ feedbackDisposer?.();
321
+ feedbackDisposer = undefined;
322
+ userCommands.disposeAll();
171
323
  void runtime.dispose();
172
324
  }, 'dsh-agent-plugins-market: lifecycle');
173
325
  }
@@ -0,0 +1,49 @@
1
+ /** Ordered layout declarations shared by manifest and project discovery. */
2
+ export const PLUGIN_LAYOUTS = [
3
+ { kind: 'agent-plugin-v1', manifest: 'plugin.json', marketplaces: [] },
4
+ { kind: 'universal', manifest: '.plugin/plugin.json', marketplaces: ['.plugin/marketplace.json'] },
5
+ { kind: 'claude-code', manifest: '.claude-plugin/plugin.json', marketplaces: ['.claude-plugin/marketplace.json'] },
6
+ { kind: 'cursor', manifest: '.cursor-plugin/plugin.json', marketplaces: ['.cursor-plugin/marketplace.json'] },
7
+ { kind: 'kimi', manifest: '.kimi-plugin/plugin.json', marketplaces: ['.kimi-plugin/marketplace.json'] },
8
+ { kind: 'codex', manifest: '.codex-plugin/plugin.json', marketplaces: ['.agents/plugins/marketplace.json', '.agents/plugins/api_marketplace.json'] },
9
+ { kind: 'zcode', manifest: '.zcode-plugin/plugin.json', marketplaces: [] },
10
+ { kind: 'qoder', manifest: '.qoder-plugin/plugin.json', marketplaces: ['.qoder-plugin/marketplace.json'] },
11
+ { kind: 'github-copilot', manifest: '.github/plugin/plugin.json', marketplaces: ['.github/plugin/marketplace.json'] }
12
+ ];
13
+ /** Kimi Code's primary spelling precedes its compatibility spelling within that dialect. */
14
+ export const MANIFEST_ALIASES = { kimi: ['kimi.plugin.json'] };
15
+ /** Equivalent plugin-owned path variables; these never resolve as credentials. */
16
+ export const PLUGIN_ROOT_VARIABLES = new Set(['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'CODEX_PLUGIN_ROOT', 'ZCODE_PLUGIN_ROOT', 'QODER_PLUGIN_ROOT']);
17
+ export const PLUGIN_DATA_VARIABLES = new Set(['PLUGIN_DATA', 'CLAUDE_PLUGIN_DATA', 'ZCODE_PLUGIN_DATA', 'QODER_PLUGIN_DATA']);
18
+ export const PROJECT_LAYOUTS = [
19
+ {
20
+ dirName: '.claude',
21
+ label: 'Claude Code',
22
+ subdirs: ['skills', 'agents', 'commands'],
23
+ mcpFiles: ['.mcp.json'],
24
+ hookFiles: ['.claude/settings.json', '.claude/settings.local.json']
25
+ },
26
+ { dirName: '.agents', label: 'agents', subdirs: ['skills', 'agents', 'commands'] },
27
+ { dirName: '.codex', label: 'Codex', subdirs: ['skills'], mcpFiles: ['.codex/config.toml'], mcpFormat: 'codex' },
28
+ { dirName: '.cursor', label: 'Cursor', subdirs: ['skills', 'agents', 'commands'], mcpFiles: ['.cursor/mcp.json'] },
29
+ { dirName: '.kimi', label: 'Kimi', subdirs: ['skills'] },
30
+ {
31
+ dirName: '.zcode',
32
+ label: 'ZCode',
33
+ subdirs: ['skills', 'agents', 'commands'],
34
+ mcpFiles: ['zcode.json', '.zcode/config.json'],
35
+ mcpFormat: 'zcode',
36
+ hookFiles: ['zcode.json', '.zcode/config.json'],
37
+ hookFormat: 'zcode'
38
+ },
39
+ {
40
+ dirName: '.qoder',
41
+ label: 'Qoder CLI',
42
+ subdirs: ['skills', 'agents', 'commands'],
43
+ mcpFiles: ['.qoder/settings.json', '.qoder/settings.local.json'],
44
+ hookFiles: ['.qoder/settings.json', '.qoder/settings.local.json']
45
+ },
46
+ { dirName: '.github', label: 'GitHub Copilot', subdirs: ['skills', 'agents'] }
47
+ ];
48
+ /** Marketplace catalogs follow manifest layout order; the shared root catalog is the fallback. */
49
+ export const MARKETPLACE_PATHS = [...PLUGIN_LAYOUTS.flatMap(layout => layout.marketplaces), 'marketplace.json'];