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
package/lib/routes.js CHANGED
@@ -7,12 +7,13 @@
7
7
  * enable/disable against a local profile.
8
8
  */
9
9
  import { isAbsolute } from 'node:path';
10
- import { MARKET_ROUTES } from './contracts/market.js';
10
+ import { MARKET_ROUTES, userPanelRoute } from './contracts/market.js';
11
11
  import { expandHome } from './catalog/paths.js';
12
12
  import { sanitizeOverridePatch } from './runtime/mcp-overrides.js';
13
+ import { readModelCatalog } from './runtime/model-catalog.js';
13
14
  const MAX_BODY_BYTES = 64 * 1024;
14
15
  /** Mount every route; returns the disposer releasing them all. */
15
- export function mountSuiteRoutes(hostCtx, manager) {
16
+ export function mountSuiteRoutes(hostCtx, manager, panels) {
16
17
  const host = hostCtx;
17
18
  const disposers = [];
18
19
  const get = (path, handler) => {
@@ -34,7 +35,7 @@ export function mountSuiteRoutes(hostCtx, manager) {
34
35
  return;
35
36
  }
36
37
  try {
37
- const value = await handler(body);
38
+ const value = await handler(body, request);
38
39
  sendJson(response, 200, { ok: true, ...value });
39
40
  }
40
41
  catch (error) {
@@ -47,9 +48,44 @@ export function mountSuiteRoutes(hostCtx, manager) {
47
48
  get(MARKET_ROUTES.overview, async (_request, response) => {
48
49
  sendJson(response, 200, await manager.overview());
49
50
  });
51
+ get(MARKET_ROUTES.modelCatalog, async (request, response) => {
52
+ try {
53
+ const query = queryOf(request);
54
+ sendJson(response, 200, await readModelCatalog(host, query.get('provider') || undefined, query.get('model') || undefined));
55
+ }
56
+ catch (error) {
57
+ sendJson(response, 503, { ok: false, error: error instanceof Error ? error.message : String(error) });
58
+ }
59
+ });
50
60
  get(MARKET_ROUTES.mcpStatus, async (_request, response) => {
51
61
  sendJson(response, 200, await manager.mcpStatus());
52
62
  });
63
+ get(MARKET_ROUTES.serverConfig, async (request, response) => {
64
+ try {
65
+ const query = queryOf(request);
66
+ const kind = query.get('kind');
67
+ if (kind !== 'mcp' && kind !== 'lsp')
68
+ throw new Error('invalid service kind');
69
+ sendJson(response, 200, await manager.serverConfig(kind, query.get('id') ?? ''));
70
+ }
71
+ catch (error) {
72
+ sendJson(response, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
73
+ }
74
+ });
75
+ post(MARKET_ROUTES.saveServerConfig, async (body) => {
76
+ if (body.kind !== 'mcp' && body.kind !== 'lsp')
77
+ throw new Error('invalid service kind');
78
+ if (typeof body.id !== 'string' || body.id === '')
79
+ throw new Error('missing service id');
80
+ await manager.saveServerConfig(body.kind, body.id, body.config);
81
+ return {};
82
+ });
83
+ post(MARKET_ROUTES.addMcpServer, async (body) => {
84
+ if (typeof body.name !== 'string')
85
+ throw new Error('MCP server name is required');
86
+ await manager.addMcpServer(body.name, body.config);
87
+ return {};
88
+ });
53
89
  get(MARKET_ROUTES.lspStatus, async (_request, response) => {
54
90
  sendJson(response, 200, await manager.lspStatus());
55
91
  });
@@ -157,7 +193,7 @@ export function mountSuiteRoutes(hostCtx, manager) {
157
193
  const id = body['id'];
158
194
  if (typeof id !== 'string' || id === '')
159
195
  throw new Error('missing source id');
160
- await manager.removeSource(id);
196
+ await manager.removeSource(id, body['deleteCheckout'] === true);
161
197
  return {};
162
198
  });
163
199
  post(MARKET_ROUTES.refreshSource, async (body) => {
@@ -249,11 +285,75 @@ export function mountSuiteRoutes(hostCtx, manager) {
249
285
  return await manager.mcpBackendInfo();
250
286
  });
251
287
  // Validate and persist the user's direct LSP server table; the reconcile
252
- // pass picks it up and mounts it alongside the suite declarations.
253
- post(MARKET_ROUTES.lspServers, async (body) => {
288
+ // pass picks it up and mounts it alongside the suite declarations. The
289
+ // mutation path is distinct from the read path: the webserver's exact table
290
+ // is keyed by pathname only, so a same-path GET/POST pair cannot coexist.
291
+ post(`${MARKET_ROUTES.lspServers}/save`, async (body) => {
254
292
  const servers = await manager.setLspServers(body['lspServers']);
255
293
  return { lspServers: servers };
256
294
  });
295
+ post(MARKET_ROUTES.addLspServer, async (body) => {
296
+ if (typeof body.name !== 'string')
297
+ throw new Error('missing LSP server name');
298
+ await manager.addLspServer(body.name.trim(), body.config);
299
+ return {};
300
+ });
301
+ post(`${MARKET_ROUTES.lspServers}/enabled`, async (body) => {
302
+ const id = String(body['id'] ?? '');
303
+ if (id === '')
304
+ throw new Error('missing LSP server id');
305
+ await manager.setLspServerEnabled(id, body['enabled'] !== false);
306
+ return {};
307
+ });
308
+ // User panel CRUD (skills / commands / agent personas). The host web
309
+ // server matches exact pathnames, so the entry name rides the `name` query
310
+ // parameter (GET read, POST replace, POST .../delete remove). The routes
311
+ // only parse and delegate; the stores own validation and persistence, and
312
+ // each mutation notifies the change pipeline so skills/commands remount.
313
+ if (panels !== undefined) {
314
+ const kinds = ['skills', 'commands', 'agents'];
315
+ const storeOf = (kind) => panels[kind];
316
+ for (const kind of kinds) {
317
+ get(userPanelRoute(kind), async (_request, response) => {
318
+ sendJson(response, 200, { entries: await storeOf(kind).list() });
319
+ });
320
+ get(`${userPanelRoute(kind)}/entry`, async (request, response) => {
321
+ const name = queryOf(request).get('name') ?? '';
322
+ const entry = await storeOf(kind).get(name);
323
+ if (entry === undefined) {
324
+ sendJson(response, 404, { ok: false, error: `no entry named "${name}"` });
325
+ return;
326
+ }
327
+ sendJson(response, 200, { entry });
328
+ });
329
+ post(`${userPanelRoute(kind)}/create`, async (body) => {
330
+ const name = String(body['name'] ?? '').trim();
331
+ const text = String(body['text'] ?? '');
332
+ if (name === '')
333
+ throw new Error('missing entry name');
334
+ const entry = await storeOf(kind).create(name, text);
335
+ await manager.notifyPanelsChanged();
336
+ return { entry };
337
+ });
338
+ post(`${userPanelRoute(kind)}/update`, async (body, request) => {
339
+ const name = queryOf(request).get('name') ?? '';
340
+ const text = String(body['text'] ?? '');
341
+ if (name === '')
342
+ throw new Error('missing entry name');
343
+ await storeOf(kind).update(name, text);
344
+ await manager.notifyPanelsChanged();
345
+ return {};
346
+ });
347
+ post(`${userPanelRoute(kind)}/delete`, async (body) => {
348
+ const name = String(body['name'] ?? '');
349
+ if (name === '')
350
+ throw new Error('missing entry name');
351
+ await storeOf(kind).remove(name);
352
+ await manager.notifyPanelsChanged();
353
+ return {};
354
+ });
355
+ }
356
+ }
257
357
  return () => {
258
358
  for (const dispose of disposers)
259
359
  dispose();
@@ -0,0 +1,226 @@
1
+ /** Execute user and installed-suite agent cards through the host subagent service. */
2
+ import { readFile } from 'node:fs/promises';
3
+ import { defineTool } from '@deepseek-ai/dsh-tools';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { mountSubagentCatalog } from './subagent-catalog.js';
6
+ import { bindHostLocale } from './host-locale.js';
7
+ export const AGENT_ROLE_TOOL_NAME = 'subagents_run';
8
+ function optionalText(value, key) {
9
+ if (value === undefined)
10
+ return undefined;
11
+ if (typeof value !== 'string' || value.trim() === '')
12
+ throw new Error(`agent metadata ${key} must be a non-empty string`);
13
+ return value.trim();
14
+ }
15
+ function toolList(value, key) {
16
+ if (value === undefined)
17
+ return undefined;
18
+ const entries = typeof value === 'string' ? value.split(',') : Array.isArray(value) ? value : [value];
19
+ return [
20
+ ...new Set(entries.map(entry => {
21
+ if (typeof entry !== 'string' || !/^[\w.:-]+$/.test(entry.trim()))
22
+ throw new Error(`agent metadata ${key} must contain tool names (comma-separated or a YAML array)`);
23
+ return entry.trim();
24
+ }))
25
+ ];
26
+ }
27
+ /** Strict YAML parsing prevents malformed routing/restriction metadata from silently inheriting wider privileges. */
28
+ export function parseAgentRole(text) {
29
+ const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
30
+ if (match === null) {
31
+ if (/^(?:\uFEFF)?---(?:\r?\n|$)/.test(text))
32
+ throw new Error('agent frontmatter is not closed');
33
+ return { content: text, disabled: false };
34
+ }
35
+ const parsed = parseYaml(match[1]);
36
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
37
+ throw new Error('agent frontmatter must be a YAML object');
38
+ const metadata = parsed;
39
+ if (metadata.disabled !== undefined && typeof metadata.disabled !== 'boolean')
40
+ throw new Error('agent metadata disabled must be a boolean');
41
+ const model = optionalText(metadata.model, 'model');
42
+ const provider = optionalText(metadata.provider, 'provider');
43
+ const effort = optionalText(metadata.reasoning_effort, 'reasoning_effort');
44
+ const camelEffort = optionalText(metadata.reasoningEffort, 'reasoningEffort');
45
+ if (effort !== undefined && camelEffort !== undefined && effort !== camelEffort)
46
+ throw new Error('agent metadata reasoning_effort and reasoningEffort conflict');
47
+ const reasoningEffort = effort ?? camelEffort;
48
+ const title = optionalText(metadata.name, 'name');
49
+ const description = optionalText(metadata.description, 'description');
50
+ if (model === 'inherit' && provider !== undefined)
51
+ throw new Error('agent model inherit cannot specify a provider');
52
+ return {
53
+ content: text.slice(match[0].length),
54
+ disabled: metadata.disabled === true,
55
+ ...(model === undefined || model === 'inherit' ? {} : { model }),
56
+ ...(provider === undefined ? {} : { provider }),
57
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
58
+ ...(title === undefined ? {} : { title }),
59
+ ...(description === undefined ? {} : { description }),
60
+ ...(metadata.tools === undefined ? {} : { tools: toolList(metadata.tools, 'tools') }),
61
+ ...(metadata.disallowedTools === undefined ? {} : { disallowedTools: toolList(metadata.disallowedTools, 'disallowedTools') })
62
+ };
63
+ }
64
+ /** Bare ids must resolve uniquely; Claude aliases are accepted only if actually advertised by a DSH provider. */
65
+ export async function resolveAgentModel(policy, catalog) {
66
+ if (policy.model === undefined)
67
+ return policy.provider === undefined ? {} : { provider: policy.provider };
68
+ const providers = catalog.listProviders();
69
+ if (policy.provider !== undefined) {
70
+ if (!providers.some(provider => provider.id === policy.provider))
71
+ throw new Error(`agent model provider "${policy.provider}" is not registered`);
72
+ return { provider: policy.provider, model: policy.model };
73
+ }
74
+ const qualified = providers.filter(provider => policy.model.startsWith(`${provider.id}/`)).sort((a, b) => b.id.length - a.id.length)[0];
75
+ if (qualified !== undefined) {
76
+ const model = policy.model.slice(qualified.id.length + 1);
77
+ if (model === '')
78
+ throw new Error('agent model id must not be empty');
79
+ return { provider: qualified.id, model };
80
+ }
81
+ const matches = (await Promise.all(providers.map(async (provider) => {
82
+ try {
83
+ return (await catalog.listModels(provider.id)).some(model => model.id === policy.model) ? provider.id : undefined;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }))).filter((provider) => provider !== undefined);
89
+ if (matches.length !== 1)
90
+ throw new Error(`agent model "${policy.model}" ${matches.length === 0 ? 'is not advertised' : 'is ambiguous'}; configure provider and model explicitly`);
91
+ return { provider: matches[0], model: policy.model };
92
+ }
93
+ /** Read the same declaration for catalog summaries and execution, including inline resources. */
94
+ export async function readAgentRole(entry) {
95
+ return parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8')));
96
+ }
97
+ /** Stable summaries; invalid declarations are excluded and transient read failures abort publication. */
98
+ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
99
+ const summaries = [];
100
+ const counts = new Map();
101
+ for (const entry of entries)
102
+ counts.set(entry.name, (counts.get(entry.name) ?? 0) + 1);
103
+ for (const entry of entries) {
104
+ signal.throwIfAborted();
105
+ if (entry.disabled)
106
+ continue;
107
+ if (counts.get(entry.name) !== 1) {
108
+ diagnose(`ambiguous subagent role: ${entry.name}`);
109
+ continue;
110
+ }
111
+ let text;
112
+ try {
113
+ text = entry.rawText ?? (await readFile(entry.path, 'utf8'));
114
+ }
115
+ catch (error) {
116
+ if (error.code !== 'ENOENT')
117
+ throw error;
118
+ continue;
119
+ }
120
+ let policy;
121
+ try {
122
+ policy = parseAgentRole(text);
123
+ }
124
+ catch (error) {
125
+ diagnose(`${entry.path}: ${String(error)}`);
126
+ continue;
127
+ }
128
+ if (policy.disabled)
129
+ continue;
130
+ const description = (policy.description ?? entry.description).replaceAll(/\s+/g, ' ').trim();
131
+ summaries.push({
132
+ name: entry.name,
133
+ title: policy.title ?? entry.title ?? entry.name,
134
+ description: description.length <= 500 ? description : `${description.slice(0, 497)}...`,
135
+ ...(policy.provider === undefined ? {} : { provider: policy.provider }),
136
+ ...(policy.model === undefined ? {} : { model: policy.model }),
137
+ ...(policy.reasoningEffort === undefined ? {} : { reasoningEffort: policy.reasoningEffort })
138
+ });
139
+ }
140
+ signal.throwIfAborted();
141
+ return summaries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
142
+ }
143
+ /** Resolve fresh panel state and raw Markdown on every call, including after edits, disabling and uninstall. */
144
+ export async function executeAgentRole(host, listRoles, name, prompt, parent, signal) {
145
+ if (parent === undefined)
146
+ throw new Error('subagents_run requires a calling agent');
147
+ if (prompt.trim() === '')
148
+ throw new Error('subagents_run requires a non-empty prompt');
149
+ signal.throwIfAborted();
150
+ const entries = (await listRoles(parent)).filter(entry => entry.name === name);
151
+ if (entries.length !== 1)
152
+ throw new Error(`agent role "${name}" is unavailable or ambiguous`);
153
+ const entry = entries[0];
154
+ if (entry.disabled)
155
+ throw new Error(`agent role "${name}" is disabled`);
156
+ const policy = await readAgentRole(entry);
157
+ if (policy.disabled)
158
+ throw new Error(`agent role "${name}" is disabled`);
159
+ const agentOptions = { ...(await resolveAgentModel(policy, host.llm)), ...(policy.reasoningEffort === undefined ? {} : { reasoningEffort: policy.reasoningEffort }) };
160
+ const parentAgent = parent;
161
+ const parentOptions = parentAgent.session?.requestHeader?.()?.config ?? parentAgent.options ?? {};
162
+ const provider = agentOptions.provider ?? parentOptions.provider;
163
+ const model = agentOptions.model ?? parentOptions.model;
164
+ if (provider === undefined || model === undefined)
165
+ throw new Error('subagents_run requires an effective provider and model');
166
+ const routeChanged = provider !== parentOptions.provider || model !== parentOptions.model;
167
+ const reasoningEffort = agentOptions.reasoningEffort ?? (routeChanged ? undefined : parentOptions.reasoningEffort);
168
+ await host.llm.resolveCallConfig({ provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) }, signal);
169
+ signal.throwIfAborted();
170
+ const run = await host.subagents.start('spawn', {
171
+ label: name,
172
+ prompt: [{ type: 'text', text: prompt }],
173
+ parent,
174
+ signal,
175
+ maxDepth: 3,
176
+ agentOptions,
177
+ persona: policy.content,
178
+ ...(policy.tools === undefined && policy.disallowedTools === undefined
179
+ ? {}
180
+ : {
181
+ toolFilter: {
182
+ ...(policy.tools === undefined ? {} : { allow: policy.tools }),
183
+ ...(policy.disallowedTools === undefined ? {} : { deny: policy.disallowedTools })
184
+ }
185
+ })
186
+ });
187
+ try {
188
+ const result = await run.result;
189
+ if (result.stopReason !== 'completed')
190
+ throw new Error(`agent role "${name}" stopped: ${result.stopReason}; partial output: ${JSON.stringify(result.output)}`);
191
+ return { runId: run.id, output: result.output };
192
+ }
193
+ finally {
194
+ await run.dispose();
195
+ }
196
+ }
197
+ /** Mount once after tools, llm and subagents are available; the returned disposer belongs to the plugin lifecycle. */
198
+ export function mountAgentRoleTool(ctx, listRoles, t = bindHostLocale(undefined)) {
199
+ const host = ctx;
200
+ const tool = defineTool({
201
+ name: AGENT_ROLE_TOOL_NAME,
202
+ description: 'Delegate a self-contained task to a role from the current subagent catalog and wait for its result. The child starts without the parent conversation. Its saved provider, model, reasoning effort, persona and tool restrictions are applied automatically.',
203
+ parameters: {
204
+ role: { type: 'string', required: true, description: 'Exact role ID from the current subagent catalog.' },
205
+ prompt: { type: 'string', required: true, description: 'Complete task and context for the isolated child.' }
206
+ },
207
+ output: { schema: { type: 'json' }, render: (_args, result) => [{ type: 'text', text: JSON.stringify(result) }] },
208
+ isConcurrencySafe: () => true,
209
+ async execute(args, exec) {
210
+ return executeAgentRole(host, listRoles, args.role, args.prompt, exec.agent, exec.signal);
211
+ }
212
+ });
213
+ const disposeTool = host.tools.register(tool);
214
+ let disposeCatalog;
215
+ try {
216
+ disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message)), t);
217
+ }
218
+ catch (error) {
219
+ disposeTool();
220
+ throw error;
221
+ }
222
+ return () => {
223
+ disposeCatalog();
224
+ disposeTool();
225
+ };
226
+ }
@@ -1,46 +1,34 @@
1
- /**
2
- * Claude Code command compatibility: `commands/*.md` of enabled suites
3
- * register as dsh slash commands.
4
- *
5
- * A CC command is a prompt template the model executes (its body carries
6
- * `$ARGUMENTS` and execution rules), so the handler maps it onto the
7
- * harness's follow-up mechanism: the template with `$ARGUMENTS` substituted
8
- * becomes one durable user-role follow-up message on the receiving agent.
9
- * Registrations reconcile on every enable/disable/install/uninstall; a
10
- * broken command file or an unavailable `ctx.commands` is contained per
11
- * command and reported as a diagnostic.
12
- */
13
- import { readFile, readdir } from 'node:fs/promises';
14
- import { join } from 'node:path';
15
1
  import { parse as parseYaml } from 'yaml';
16
- import { parseSkillFrontmatter, stripFrontmatter } from '../catalog/skills-parse.js';
2
+ import { stripFrontmatter } from '../catalog/skills-parse.js';
3
+ import { parseFrontmatterRecord } from './user-store.js';
17
4
  import { qualifiedSuiteId } from '../catalog/paths.js';
5
+ import { defaultMarkdownResources, resourceText, resourceCommandName } from '../catalog/component-files.js';
18
6
  import { bindHostLocale } from './host-locale.js';
19
7
  const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
20
8
  export class CommandMountRegistry {
21
9
  ctx;
22
10
  t;
11
+ fingerprints = new Map();
23
12
  live = new Map();
24
13
  constructor(ctx, t = bindHostLocale(undefined)) {
25
14
  this.ctx = ctx;
26
15
  this.t = t;
27
16
  }
28
- /** Register/unregister suite commands and agent-commands to match the enabled suites exactly. */
17
+ /** Register/unregister suite commands to match the enabled suites exactly. */
29
18
  async reconcile(enabledSuites) {
30
19
  const diagnostics = [];
31
20
  const wanted = new Map();
32
21
  for (const suite of enabledSuites) {
33
- const specs = suite.activeSurfaces?.commands === false && suite.activeSurfaces?.agents === false
34
- ? []
35
- : [
36
- ...(suite.activeSurfaces?.commands === false ? [] : await readCommands(suite.root)),
37
- ...(suite.activeSurfaces?.agents === false ? [] : await readAgents(suite.root, this.t))
38
- ];
22
+ const specs = suite.activeSurfaces?.commands === false ? [] : await readCommands(suite.root, suite.resources?.commands);
39
23
  for (const spec of specs) {
40
24
  // The registry key is source-qualified: bare suite ids are unique per
41
25
  // source only, so two sources' same-named suites would collide.
42
26
  const suiteKey = qualifiedSuiteId(suite.sourceId, suite.id);
43
27
  const key = `${suiteKey}/${spec.name}`;
28
+ if (wanted.has(key)) {
29
+ diagnostics.push({ suiteId: suiteKey, command: spec.name, reason: 'duplicate normalized command name' });
30
+ continue;
31
+ }
44
32
  wanted.set(key, { ...spec, suiteId: suiteKey, suiteName: suite.manifest.name });
45
33
  }
46
34
  }
@@ -57,8 +45,12 @@ export class CommandMountRegistry {
57
45
  return diagnostics;
58
46
  }
59
47
  for (const [key, spec] of wanted) {
60
- if (this.live.has(key))
48
+ const fingerprint = JSON.stringify(spec);
49
+ if (this.live.has(key) && this.fingerprints.get(key) === fingerprint)
61
50
  continue;
51
+ this.live.get(key)?.();
52
+ this.live.delete(key);
53
+ this.fingerprints.delete(key);
62
54
  try {
63
55
  const disposer = host.commands.register({
64
56
  name: spec.name,
@@ -72,6 +64,7 @@ export class CommandMountRegistry {
72
64
  }
73
65
  });
74
66
  this.live.set(key, disposer);
67
+ this.fingerprints.set(key, fingerprint);
75
68
  }
76
69
  catch (error) {
77
70
  diagnostics.push({ suiteId: spec.suiteId, command: spec.name, reason: error instanceof Error ? error.message : String(error) });
@@ -84,69 +77,36 @@ export class CommandMountRegistry {
84
77
  for (const disposer of [...this.live.values()])
85
78
  disposer();
86
79
  this.live.clear();
80
+ this.fingerprints.clear();
87
81
  }
88
82
  }
89
83
  /** Parse `commands/*.md` of one suite root (Claude Code format). */
90
- export async function readCommands(root) {
91
- let entries;
92
- try {
93
- entries = await readdir(join(root, 'commands'));
94
- }
95
- catch {
96
- return [];
97
- }
84
+ export async function readCommands(root, resources) {
85
+ const entries = resources ?? (await defaultMarkdownResources(root, 'commands'));
98
86
  const specs = [];
99
87
  for (const entry of entries) {
100
- if (!entry.endsWith('.md'))
101
- continue;
102
- const name = entry.slice(0, -3);
88
+ const name = resourceCommandName(entry.name);
103
89
  if (!COMMAND_NAME.test(name))
104
90
  continue;
105
91
  let text;
106
92
  try {
107
- text = await readFile(join(root, 'commands', entry), 'utf8');
93
+ text = await resourceText(entry);
108
94
  }
109
95
  catch {
110
96
  continue;
111
97
  }
112
- const meta = commandMeta(text);
113
- const description = meta?.description ?? firstLine(text);
114
- if (description === undefined)
115
- continue;
116
- specs.push({ name, description, hint: meta?.hint, body: stripFrontmatter(text) });
117
- }
118
- return specs;
119
- }
120
- /** Parse `agents/*.md` of one suite root into `agent-<name>` commands so
121
- * subagents are selectable from the slash-command menu, grouped by the
122
- * `agent-` prefix (the harness command UI has no group headers). */
123
- export async function readAgents(root, t) {
124
- let entries;
125
- try {
126
- entries = await readdir(join(root, 'agents'));
127
- }
128
- catch {
129
- return [];
130
- }
131
- const specs = [];
132
- for (const entry of entries) {
133
- if (!entry.endsWith('.md'))
134
- continue;
135
- const name = `agent-${entry.slice(0, -3)}`;
136
- if (!COMMAND_NAME.test(name))
137
- continue;
138
- let text;
139
98
  try {
140
- text = await readFile(join(root, 'agents', entry), 'utf8');
99
+ if (parseFrontmatterRecord(text).disabled === true)
100
+ continue;
141
101
  }
142
102
  catch {
143
103
  continue;
144
104
  }
145
- const parsed = parseSkillFrontmatter(text, name);
146
- const description = typeof parsed === 'string' ? parsed : parsed.description;
105
+ const meta = commandMeta(text);
106
+ const description = meta?.description ?? firstLine(text);
147
107
  if (description === undefined)
148
108
  continue;
149
- specs.push({ name, description, hint: t('agentCommandHint'), body: stripFrontmatter(text) });
109
+ specs.push({ name, description, hint: meta?.hint, body: stripFrontmatter(text) });
150
110
  }
151
111
  return specs;
152
112
  }
@@ -163,7 +123,7 @@ function commandMeta(text) {
163
123
  const description = record['description'];
164
124
  if (typeof description === 'string' && description.trim() !== '')
165
125
  meta.description = description.trim();
166
- const hint = record['argument-hint'];
126
+ const hint = record['argument-hint'] ?? record['argumentHint'];
167
127
  if (typeof hint === 'string' && hint.trim() !== '')
168
128
  meta.hint = hint.trim();
169
129
  return meta;