dsh-agent-plugins-market 0.6.1 → 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 (132) hide show
  1. package/README.md +143 -169
  2. package/README.zh.md +136 -170
  3. package/client/client.js +8192 -691
  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 +33 -19
  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 +16 -7
  41. package/lib/runtime/mcp-direct-config.js +56 -0
  42. package/lib/runtime/mcp-mounts.js +12 -4
  43. package/lib/runtime/mcp-overrides.js +11 -5
  44. package/lib/runtime/mcp-status.js +8 -4
  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 +4 -0
  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-direct-config.d.ts +7 -0
  91. package/lib/types/runtime/mcp-mounts.d.ts +2 -1
  92. package/lib/types/runtime/mcp-overrides.d.ts +2 -0
  93. package/lib/types/runtime/model-catalog.d.ts +7 -0
  94. package/lib/types/runtime/project-runtime.d.ts +32 -0
  95. package/lib/types/runtime/reconciler.d.ts +2 -0
  96. package/lib/types/runtime/server-config.d.ts +10 -0
  97. package/lib/types/runtime/skills-provider.d.ts +1 -5
  98. package/lib/types/runtime/storage-migration.d.ts +15 -0
  99. package/lib/types/runtime/subagent-catalog.d.ts +52 -0
  100. package/lib/types/runtime/user-commands.d.ts +24 -0
  101. package/lib/types/runtime/user-panels.d.ts +91 -0
  102. package/lib/types/runtime/user-store.d.ts +51 -0
  103. package/package.json +9 -2
  104. package/schemas/README.md +69 -7
  105. package/schemas/agent-plugins/spec.md +48 -0
  106. package/schemas/claude-code/marketplace.schema.json +143 -0
  107. package/schemas/claude-code/plugin.schema.json +105 -0
  108. package/schemas/claude-code/spec.md +108 -0
  109. package/schemas/codex/marketplace.schema.json +78 -0
  110. package/schemas/codex/plugin.schema.json +77 -0
  111. package/schemas/codex/spec.md +91 -0
  112. package/schemas/cursor/marketplace.schema.json +82 -0
  113. package/schemas/cursor/plugin.schema.json +58 -0
  114. package/schemas/cursor/spec.md +92 -0
  115. package/schemas/github-copilot/marketplace.schema.json +107 -0
  116. package/schemas/github-copilot/plugin.schema.json +89 -0
  117. package/schemas/github-copilot/spec.md +105 -0
  118. package/schemas/kimi/marketplace.schema.json +38 -0
  119. package/schemas/kimi/plugin.schema.json +84 -0
  120. package/schemas/kimi/spec.md +94 -0
  121. package/schemas/qoder/marketplace.schema.json +115 -0
  122. package/schemas/qoder/plugin.schema.json +127 -0
  123. package/schemas/qoder/spec.md +111 -0
  124. package/schemas/skill-collection/spec.md +29 -0
  125. package/schemas/universal/marketplace.schema.json +98 -0
  126. package/schemas/universal/plugin.schema.json +49 -0
  127. package/schemas/universal/spec.md +53 -0
  128. package/schemas/zcode/marketplace.schema.json +161 -0
  129. package/schemas/zcode/plugin.schema.json +89 -0
  130. package/schemas/zcode/spec.md +102 -0
  131. package/lib/catalog/legacy-root-migration.js +0 -51
  132. package/lib/types/catalog/legacy-root-migration.d.ts +0 -14
@@ -0,0 +1,96 @@
1
+ /** Read native project configuration without treating unrelated settings as server definitions. */
2
+ import { readFile, realpath } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { validateMcpJson } from './validate.js';
5
+ import { parse as parseToml } from 'smol-toml';
6
+ import { normalizeCodexMcp } from './codex-mcp.js';
7
+ /** Later files override earlier keys; a malformed layer invalidates the whole layout's table. */
8
+ export async function discoverProjectMcp(projectRoot, files, errors, format = 'mcpServers') {
9
+ const servers = Object.create(null);
10
+ let found = false;
11
+ for (const file of files) {
12
+ const raw = await readProjectDocument(projectRoot, file, errors, format === 'codex' ? 'TOML' : 'JSON');
13
+ if (raw === null)
14
+ return undefined;
15
+ if (raw === undefined)
16
+ continue;
17
+ if (format === 'codex') {
18
+ if (raw.mcp_servers === undefined)
19
+ continue;
20
+ if (typeof raw.mcp_servers !== 'object' || raw.mcp_servers === null || Array.isArray(raw.mcp_servers)) {
21
+ errors.push(`${file}: mcp_servers must be a table`);
22
+ return undefined;
23
+ }
24
+ found = true;
25
+ Object.assign(servers, normalizeCodexMcp(raw.mcp_servers, errors));
26
+ continue;
27
+ }
28
+ const mcp = raw.mcp;
29
+ if (format === 'zcode' && mcp === undefined)
30
+ continue;
31
+ if (format === 'zcode' && (typeof mcp !== 'object' || mcp === null || Array.isArray(mcp))) {
32
+ errors.push(`${file}: mcp must be an object`);
33
+ return undefined;
34
+ }
35
+ if (format === 'zcode' && mcp.enabled === false)
36
+ return undefined;
37
+ const table = format === 'zcode' ? mcp.servers : raw.mcpServers;
38
+ if (format === 'zcode' && table === undefined)
39
+ continue;
40
+ if (table === undefined && /settings(?:\.local)?\.json$/.test(file))
41
+ continue;
42
+ if (typeof table !== 'object' || table === null || Array.isArray(table)) {
43
+ errors.push(`${file}: mcpServers must be an object`);
44
+ return undefined;
45
+ }
46
+ found = true;
47
+ Object.assign(servers, table);
48
+ }
49
+ if (format === 'zcode' && Object.keys(servers).length === 0)
50
+ return discoverProjectMcp(projectRoot, ['.agents/mcp.json'], errors);
51
+ if (!found)
52
+ return undefined;
53
+ for (const [key, server] of Object.entries(servers)) {
54
+ if (typeof server === 'object' && server !== null && server.enabled === false)
55
+ delete servers[key];
56
+ }
57
+ const result = await validateMcpJson(projectRoot, { mcpServers: servers }, { strict: false, pathMode: 'project' });
58
+ errors.push(...result.errors.map(error => `project MCP: ${error}`));
59
+ return result.config === undefined ? undefined : { ...result.config, root: projectRoot };
60
+ }
61
+ /** Missing is undefined; malformed/unreadable is null and carries a diagnostic. */
62
+ export async function readProjectDocument(projectRoot, file, errors, format = 'JSON') {
63
+ const path = join(projectRoot, file);
64
+ let text;
65
+ try {
66
+ const [base, target] = await Promise.all([realpath(projectRoot), realpath(path)]);
67
+ if (!target.startsWith(`${base}/`)) {
68
+ errors.push(`${file}: project configuration escapes the project root`);
69
+ return null;
70
+ }
71
+ text = await readFile(path, 'utf8');
72
+ }
73
+ catch (error) {
74
+ if (error.code === 'ENOENT')
75
+ return undefined;
76
+ errors.push(`${file}: project configuration is unreadable`);
77
+ return null;
78
+ }
79
+ let raw;
80
+ try {
81
+ raw = format === 'TOML' ? parseToml(text) : JSON.parse(text);
82
+ }
83
+ catch {
84
+ errors.push(`${file}: project configuration is invalid ${format}`);
85
+ return null;
86
+ }
87
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
88
+ errors.push(`${file}: project configuration must be an object`);
89
+ return null;
90
+ }
91
+ const record = raw;
92
+ if (record.lspServers !== undefined || record.lsp_servers !== undefined) {
93
+ errors.push(`${file}: project LSP declarations are not mounted; the host LSP registry does not isolate projects`);
94
+ }
95
+ return record;
96
+ }
@@ -0,0 +1,108 @@
1
+ import { readProjectDocument } from './project-config.js';
2
+ const EVENTS = new Set(['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'SubagentStart', 'SubagentStop']);
3
+ function object(value) {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ }
6
+ /** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
7
+ export async function discoverProjectHooks(projectRoot, files, errors, format = 'claude') {
8
+ const documents = [];
9
+ for (const file of files) {
10
+ const settings = await readProjectDocument(projectRoot, file, errors);
11
+ if (settings === null)
12
+ return undefined;
13
+ if (settings !== undefined)
14
+ documents.push({ file, settings });
15
+ }
16
+ return normalizeHookDocuments(projectRoot, documents, errors, format);
17
+ }
18
+ /** Pure validation shared by native settings and plugin file/inline declarations. */
19
+ export function normalizeHookDocuments(projectRoot, documents, errors, format = 'claude') {
20
+ const events = Object.create(null);
21
+ const seen = new Set();
22
+ let disabled = false;
23
+ for (const { file, settings } of documents) {
24
+ if (settings?.disableAllHooks !== undefined) {
25
+ if (typeof settings.disableAllHooks !== 'boolean') {
26
+ errors.push(`${file}: disableAllHooks must be a boolean`);
27
+ return undefined;
28
+ }
29
+ disabled = settings.disableAllHooks;
30
+ }
31
+ if (settings?.hooks === undefined)
32
+ continue;
33
+ if (!object(settings.hooks)) {
34
+ errors.push(`${file}: hooks must be an event table`);
35
+ return undefined;
36
+ }
37
+ if (format === 'zcode' && settings.hooks.enabled !== true)
38
+ continue;
39
+ const eventTable = format === 'zcode' ? settings.hooks.events : settings.hooks;
40
+ if (!object(eventTable)) {
41
+ errors.push(`${file}: hooks.events must be an event table`);
42
+ return undefined;
43
+ }
44
+ for (const [event, groups] of Object.entries(eventTable)) {
45
+ if (!EVENTS.has(event)) {
46
+ errors.push(`${file}: unsupported project hook event ${event}`);
47
+ continue;
48
+ }
49
+ if (!Array.isArray(groups)) {
50
+ errors.push(`${file}: ${event} hook groups must be an array`);
51
+ return undefined;
52
+ }
53
+ for (const group of groups) {
54
+ if (!object(group) || !Array.isArray(group.hooks) || (group.matcher !== undefined && typeof group.matcher !== 'string')) {
55
+ errors.push(`${file}: invalid ${event} hook group`);
56
+ return undefined;
57
+ }
58
+ const matcher = event === 'Stop' || event === 'UserPromptSubmit' ? undefined : group.matcher;
59
+ if (matcher !== undefined && matcher !== '*') {
60
+ try {
61
+ new RegExp(matcher);
62
+ }
63
+ catch {
64
+ errors.push(`${file}: invalid ${event} hook matcher`);
65
+ return undefined;
66
+ }
67
+ }
68
+ const hooks = [];
69
+ for (const hook of group.hooks) {
70
+ if (!object(hook)) {
71
+ errors.push(`${file}: invalid ${event} hook`);
72
+ return undefined;
73
+ }
74
+ if (hook.enabled === false)
75
+ continue;
76
+ if (hook.type !== undefined && hook.type !== 'command') {
77
+ errors.push(`${file}: unsupported ${event} hook type ${String(hook.type)}`);
78
+ continue;
79
+ }
80
+ if (typeof hook.command !== 'string' ||
81
+ hook.command.trim() === '' ||
82
+ (hook.timeout !== undefined && (typeof hook.timeout !== 'number' || !Number.isFinite(hook.timeout) || hook.timeout <= 0))) {
83
+ errors.push(`${file}: invalid ${event} command hook`);
84
+ return undefined;
85
+ }
86
+ if (hook.async === true || hook.asyncRewake === true) {
87
+ errors.push(`${file}: asynchronous project hooks are unsupported`);
88
+ continue;
89
+ }
90
+ const key = JSON.stringify([event, matcher, hook.command]);
91
+ if (seen.has(key))
92
+ continue;
93
+ seen.add(key);
94
+ const timeoutMs = hook.timeoutMs ?? settings.hooks.timeoutMs;
95
+ if (format === 'zcode' && timeoutMs !== undefined && (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
96
+ errors.push(`${file}: invalid ${event} hook timeoutMs`);
97
+ return undefined;
98
+ }
99
+ const timeout = format === 'zcode' && typeof timeoutMs === 'number' ? timeoutMs / 1000 : hook.timeout;
100
+ hooks.push({ type: 'command', command: hook.command, ...(typeof timeout === 'number' ? { timeout } : {}) });
101
+ }
102
+ if (hooks.length > 0)
103
+ (events[event] ??= []).push({ ...(matcher === undefined ? {} : { matcher }), hooks });
104
+ }
105
+ }
106
+ }
107
+ return !disabled && Object.keys(events).length > 0 ? { projectRoot, events } : undefined;
108
+ }
@@ -46,5 +46,10 @@ export async function runScanChain(filters, context) {
46
46
  return resolution;
47
47
  };
48
48
  const resolution = await run(0);
49
- return { suites: resolution.kind === 'resolved' ? resolution.suites : [], attempts, notes };
49
+ return {
50
+ suites: resolution.kind === 'resolved' ? resolution.suites : [],
51
+ attempts,
52
+ notes,
53
+ ...(context.marketplacePath === undefined ? {} : { marketplacePath: context.marketplacePath })
54
+ };
50
55
  }
@@ -18,12 +18,13 @@
18
18
  * 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
19
19
  * `<root>/<name>/SKILL.md` collections.
20
20
  */
21
- import { readdir, stat } from 'node:fs/promises';
21
+ import { readdir, realpath, stat } from 'node:fs/promises';
22
22
  import { join, resolve } from 'node:path';
23
23
  import { isDirectory, sanitizeId } from './paths.js';
24
- import { declaredSkillsPath, detectManifest, readManifest, readMarketplaces, syntheticManifestName, declaredLspServers } from './manifests.js';
25
- import { countSurfaces, discoverMcp, discoverSkills } from './surfaces.js';
26
- import { parseLspServers } from './lsp-spec.js';
24
+ import { componentDeclarations, detectManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
25
+ import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
26
+ import { discoverMarkdownResources } from './component-files.js';
27
+ import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
27
28
  import { runScanChain } from './scan-pipeline.js';
28
29
  export { repoName } from './manifests.js';
29
30
  export { listMdFiles, discoverLspEntries } from './surfaces.js';
@@ -71,7 +72,7 @@ export function entryRemoteUrl(entry) {
71
72
  return record['url'];
72
73
  return githubRepoUrl(record);
73
74
  }
74
- function claimLocal(checkout, declaredPath) {
75
+ async function claimLocal(checkout, declaredPath) {
75
76
  const dir = resolve(checkout, declaredPath);
76
77
  // Containment guard: a relative entry must stay inside the checkout.
77
78
  if (dir !== checkout && !dir.startsWith(`${checkout}/`)) {
@@ -80,6 +81,15 @@ function claimLocal(checkout, declaredPath) {
80
81
  // handlers must not reinterpret it as something else.
81
82
  return { kind: 'rejected', reason: `path "${declaredPath}" escapes the checkout` };
82
83
  }
84
+ try {
85
+ const [realCheckout, realDir] = await Promise.all([realpath(checkout), realpath(dir)]);
86
+ if (realDir !== realCheckout && !realDir.startsWith(`${realCheckout}/`)) {
87
+ return { kind: 'rejected', reason: `path "${declaredPath}" escapes the checkout through a symlink` };
88
+ }
89
+ }
90
+ catch {
91
+ return { kind: 'rejected', reason: `path "${declaredPath}" is missing or unreadable` };
92
+ }
83
93
  return { kind: 'local', dir };
84
94
  }
85
95
  function unclaimed(reason) {
@@ -88,7 +98,7 @@ function unclaimed(reason) {
88
98
  /** String source: a checkout-relative path (Claude Code `"./skills/x"` form). */
89
99
  const STRING_PATH_HANDLER = {
90
100
  name: 'local-string-path',
91
- handle(context) {
101
+ async handle(context) {
92
102
  const source = context.entry.source;
93
103
  if (typeof source !== 'string')
94
104
  return unclaimed('source is not a path string');
@@ -98,11 +108,13 @@ const STRING_PATH_HANDLER = {
98
108
  /** Object source with `path`: Codex `{ source: 'local', path }` and Claude `{ path }`. */
99
109
  const OBJECT_PATH_HANDLER = {
100
110
  name: 'local-object-path',
101
- handle(context) {
111
+ async handle(context) {
102
112
  const source = context.entry.source;
103
113
  if (source === null || typeof source !== 'object')
104
114
  return unclaimed('source is not an object');
105
115
  const record = source;
116
+ if (entryRemoteUrl(context.entry) !== undefined)
117
+ return unclaimed('path belongs to a remote source');
106
118
  if (typeof record['path'] !== 'string' || record['path'] === '')
107
119
  return unclaimed('object source carries no path');
108
120
  return claimLocal(context.checkout, record['path']);
@@ -128,7 +140,8 @@ const ENTRY_HANDLERS = [STRING_PATH_HANDLER, OBJECT_PATH_HANDLER, REMOTE_URL_HAN
128
140
  export async function resolveMarketplaceEntry(checkout, entry, sourceUrl) {
129
141
  const remoteUrl = entryRemoteUrl(entry);
130
142
  if (sourceUrl !== undefined && remoteUrl !== undefined && canonicalGitUrl(remoteUrl) === canonicalGitUrl(sourceUrl) && (await isDirectory(checkout))) {
131
- return { kind: 'local', dir: checkout };
143
+ const source = entry.source;
144
+ return typeof source === 'object' && source !== null && typeof source.path === 'string' ? claimLocal(checkout, source.path) : { kind: 'local', dir: checkout };
132
145
  }
133
146
  let lastReason = 'no handler claimed the entry';
134
147
  for (const handler of ENTRY_HANDLERS) {
@@ -148,20 +161,21 @@ export async function resolveMarketplaceEntry(checkout, entry, sourceUrl) {
148
161
  export class MarketplaceStrategy {
149
162
  name = 'marketplace';
150
163
  async doScan(context, chain) {
151
- const marketplaces = await readMarketplaces(context.checkout);
164
+ const marketplaces = await readMarketplaces(context.checkout, context.notes);
152
165
  for (const marketplace of marketplaces) {
153
166
  const roots = await this.marketplaceRoots(context, marketplace.entries);
154
167
  if (roots.length === 0) {
155
168
  context.notes.push(`marketplace ${marketplace.path}: no entry resolved (all ${marketplace.entries.length} dropped or remote)`);
156
169
  continue;
157
170
  }
158
- const suites = await Promise.all(roots.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint)));
171
+ const suites = await Promise.all(roots.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
159
172
  const resolved = suites.filter((suite) => suite !== undefined);
160
173
  if (resolved.length === 0) {
161
174
  context.notes.push(`marketplace ${marketplace.path}: every resolved entry failed to parse a manifest`);
162
175
  continue;
163
176
  }
164
177
  // A productive dialect wins; supplement with containers it did not list.
178
+ context.marketplacePath = marketplace.path;
165
179
  for (const container of CONTAINER_DIRS) {
166
180
  const containerDir = join(context.checkout, container);
167
181
  if (!(await isDirectory(containerDir)))
@@ -169,7 +183,7 @@ export class MarketplaceStrategy {
169
183
  for (const child of await listChildDirs(containerDir)) {
170
184
  if (roots.some(root => root.dir === child) || !(await hasSuiteManifest(child)))
171
185
  continue;
172
- const extra = await readSuite(child, context.sourceId, context.dimension, undefined);
186
+ const extra = await readSuite(child, context.sourceId, context.dimension, undefined, context.notes);
173
187
  if (extra !== undefined)
174
188
  resolved.push(extra);
175
189
  }
@@ -186,6 +200,8 @@ export class MarketplaceStrategy {
186
200
  const seen = new Set();
187
201
  for (const entry of entries) {
188
202
  const hint = {
203
+ ...componentDeclarations(entry),
204
+ layout: entry.layout,
189
205
  name: entry.name,
190
206
  version: entry.version,
191
207
  description: entry.description,
@@ -221,13 +237,13 @@ export class RootedStrategy {
221
237
  name = 'rooted';
222
238
  async doScan(context, chain) {
223
239
  if (await hasSuiteManifest(context.checkout)) {
224
- const suite = await readSuite(context.checkout, context.sourceId, context.dimension, undefined);
240
+ const suite = await readSuite(context.checkout, context.sourceId, context.dimension, undefined, context.notes);
225
241
  return suite === undefined ? chain.next(context) : { kind: 'resolved', suites: [suite] };
226
242
  }
227
243
  const found = await collectRoots(context.checkout, undefined, new Set());
228
244
  if (found.length === 0)
229
245
  return chain.next(context);
230
- const suites = await Promise.all(found.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint)));
246
+ const suites = await Promise.all(found.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
231
247
  const resolved = suites.filter((suite) => suite !== undefined);
232
248
  return resolved.length === 0 ? chain.next(context) : { kind: 'resolved', suites: resolved };
233
249
  }
@@ -236,6 +252,10 @@ export class RootedStrategy {
236
252
  export class FlatCollectionsStrategy {
237
253
  name = 'flat-collections';
238
254
  async doScan(context) {
255
+ // A declared suite has already been evaluated by rooted discovery.
256
+ // Reinterpreting its children would bypass a rejected manifest.
257
+ if (await hasSuiteManifest(context.checkout))
258
+ return { kind: 'resolved', suites: [] };
239
259
  const collection = [];
240
260
  for (const child of await listChildDirs(context.checkout)) {
241
261
  if (await hasSkillFiles(child))
@@ -245,7 +265,7 @@ export class FlatCollectionsStrategy {
245
265
  context.notes.push('checkout declares no recognizable plugin shape');
246
266
  return { kind: 'resolved', suites: [] };
247
267
  }
248
- const suites = await Promise.all(collection.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint)));
268
+ const suites = await Promise.all(collection.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
249
269
  return { kind: 'resolved', suites: suites.filter((suite) => suite !== undefined) };
250
270
  }
251
271
  }
@@ -280,7 +300,7 @@ async function collectRoots(dir, hint, seen, depth = 0) {
280
300
  // An entry carrying inline LSP declarations is a suite by declaration alone
281
301
  // (official CC lsp plugins ship only a README), but only at the marketplace
282
302
  // entry root — never deeper, so containers cannot self-declare.
283
- if ((await hasSuiteManifest(dir)) || (await hasSkillFiles(dir)) || (hint?.lspServers !== undefined && depth === 0)) {
303
+ if ((await hasSuiteManifest(dir)) || (await hasSkillFiles(dir)) || (hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0 && depth === 0)) {
284
304
  seen.add(dir);
285
305
  return [{ dir, hint }];
286
306
  }
@@ -299,6 +319,8 @@ export async function hasSkillFiles(dir) {
299
319
  const skillsDir = join(dir, 'skills');
300
320
  if (!(await isDirectory(skillsDir)))
301
321
  return false;
322
+ if ((await listMdFiles(skillsDir)).length > 0)
323
+ return true;
302
324
  for (const child of await listChildDirs(skillsDir)) {
303
325
  if (await isFile(join(child, 'SKILL.md')))
304
326
  return true;
@@ -316,15 +338,21 @@ async function listChildDirs(dir) {
316
338
  return entries.filter(entry => entry.isDirectory() && !DOT_DIRS.has(entry.name) && !entry.name.startsWith('.')).map(entry => join(dir, entry.name));
317
339
  }
318
340
  /** Read one suite root into the normalized shape, or undefined when no manifest parses. */
319
- export async function readSuite(root, sourceId, dimension, hint) {
341
+ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
320
342
  const errors = [];
321
- let manifest = (await readManifest(root, errors, hint)) ?? (await syntheticManifest(root));
343
+ const declaredManifest = await hasSuiteManifest(root);
344
+ let manifest = declaredManifest ? await readManifest(root, errors, hint) : await syntheticManifest(root);
345
+ if (declaredManifest && (manifest === undefined || errors.length > 0)) {
346
+ notes.push(...errors, `suite ${root}: rejected declared manifest`);
347
+ return undefined;
348
+ }
322
349
  // A declaration-only suite (official CC lsp plugins ship just a README):
323
350
  // the marketplace entry's inline lspServers are its manifest.
324
- if (manifest === undefined && hint?.lspServers !== undefined) {
351
+ if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
325
352
  const name = hint.name ?? syntheticManifestName(root);
326
353
  manifest = {
327
- layout: 'claude-code',
354
+ layout: hint.layout ?? 'claude-code',
355
+ components: componentDeclarations(hint),
328
356
  path: '',
329
357
  id: sanitizeId(name),
330
358
  name,
@@ -334,16 +362,32 @@ export async function readSuite(root, sourceId, dimension, hint) {
334
362
  }
335
363
  if (manifest === undefined)
336
364
  return undefined;
337
- const declared = await declaredSkillsPath(root);
365
+ let declared = manifest.components?.skills;
366
+ if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
367
+ declared = ['skills', ...(Array.isArray(declared) ? declared : [declared])];
368
+ }
338
369
  const skills = await discoverSkills(root, errors, declared);
339
- const mcp = await discoverMcp(root, errors);
340
- // Inline LSP declarations: the marketplace entry wins over the manifest
341
- // (Claude Code ships lspServers on the entry), and either is optional.
342
- const lspRaw = hint?.lspServers ?? (await declaredLspServers(root));
343
- const lspServers = parseLspServers(lspRaw, errors);
344
- const lsp = Object.keys(lspServers).length > 0 ? { servers: lspServers } : undefined;
370
+ if (manifest.layout === 'skill-collection' && skills.length === 0) {
371
+ notes.push(...errors);
372
+ return undefined;
373
+ }
374
+ const mcp = await discoverMcp(root, errors, manifest);
375
+ // Effective component declarations include marketplace fallbacks and manifest overrides.
376
+ const lsp = await discoverSuiteLsp(root, manifest, errors);
377
+ const hooks = await discoverSuiteHooks(root, manifest, errors);
378
+ const resources = {
379
+ commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
380
+ agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
381
+ };
382
+ const systemPrompt = await discoverSystemPrompt(root, manifest, errors);
345
383
  const surfaces = await countSurfaces(root, skills, mcp, lsp);
384
+ surfaces.commands = resources.commands.length;
385
+ surfaces.agents = resources.agents.length;
386
+ surfaces.hooks = Object.values(hooks?.events ?? {}).reduce((count, groups) => count + groups.reduce((sum, group) => sum + group.hooks.length, 0), 0);
346
387
  return {
388
+ resources,
389
+ ...(hooks === undefined ? {} : { hooks }),
390
+ ...(systemPrompt === undefined ? {} : { systemPrompt }),
347
391
  sourceId,
348
392
  id: manifest.id,
349
393
  root,
@@ -17,7 +17,7 @@ export async function discoverSourceList(sources, dimension, dimensionRoot) {
17
17
  return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot)).suites;
18
18
  }
19
19
  /** Discover suites plus per-source scan diagnostics for one dimension. */
20
- export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot) {
20
+ export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts = true) {
21
21
  const checkoutRoot = sourcesDir(dimensionRoot);
22
22
  const listed = new Set(sources.map(source => source.id));
23
23
  const checkouts = sources.map(source => ({
@@ -52,10 +52,23 @@ export async function discoverSourceListWithNotes(sources, dimension, dimensionR
52
52
  if (entry.notes.length > 0)
53
53
  scanNotes[entry.sourceId] = entry.notes;
54
54
  }
55
- if (dimension === 'project') {
55
+ if (dimension === 'project' && scanProjectLayouts) {
56
56
  // Native project directories live two levels above the dimension root
57
57
  // (`<projectRoot>/.dsh/agent-plugins`); read them in place.
58
- suites.push(...(await discoverNativeProjectSuites(dirname(dirname(dimensionRoot)), dimension)));
58
+ const native = await discoverNativeProjectSuites(dirname(dirname(dimensionRoot)), dimension);
59
+ suites.push(...native);
60
+ const errors = [...new Set(native.flatMap(suite => suite.errors))];
61
+ if (errors.length > 0)
62
+ scanNotes.native = errors;
63
+ }
64
+ if (dimension === 'project') {
65
+ for (const suite of suites) {
66
+ if (suite.lsp === undefined)
67
+ continue;
68
+ const note = `suite ${suite.id}: project LSP declarations are not mounted; the host LSP registry does not isolate projects`;
69
+ suite.errors.push(note);
70
+ (scanNotes[suite.sourceId] ??= []).push(note);
71
+ }
59
72
  }
60
73
  return { suites, scanNotes };
61
74
  }
@@ -0,0 +1,125 @@
1
+ /** Resolve all plugin component declarations into the same normalized resources used by runtime consumers. */
2
+ import { readFile } from 'node:fs/promises';
3
+ import { relative } from 'node:path';
4
+ import { componentDocuments, componentPath, firstComponentFile, isRecord } from './component-files.js';
5
+ import { parseLspServers } from './lsp-spec.js';
6
+ import { normalizeHookDocuments } from './project-hooks.js';
7
+ import { PLUGIN_ROOT_VARIABLES } from '../model/layouts.js';
8
+ export async function discoverSuiteLsp(root, manifest, errors) {
9
+ const declaration = manifest.components?.lspServers;
10
+ const fallback = await firstComponentFile(root, manifest.layout === 'github-copilot' || manifest.layout === 'universal' ? ['lsp.json', '.github/lsp.json', 'lsp-config/servers.json', '.lsp.json'] : ['.lsp.json', 'lsp.json']);
11
+ if (declaration === undefined && fallback === undefined)
12
+ return undefined;
13
+ const documents = await componentDocuments(root, declaration ?? fallback, errors);
14
+ if (documents === undefined)
15
+ return undefined;
16
+ const servers = Object.create(null);
17
+ for (const { value } of documents)
18
+ Object.assign(servers, parseLspServers(value.lspServers ?? value, errors));
19
+ return Object.keys(servers).length > 0 ? { servers } : undefined;
20
+ }
21
+ function quote(argument) {
22
+ return `'${argument.replaceAll("'", "'\\''")}'`;
23
+ }
24
+ function rootVariables(text, root) {
25
+ return text.replace(/\$\{([A-Z_]+)\}/g, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? root : match));
26
+ }
27
+ const EVENT_ALIASES = {
28
+ sessionStart: 'SessionStart',
29
+ userPromptSubmitted: 'UserPromptSubmit',
30
+ beforeSubmitPrompt: 'UserPromptSubmit',
31
+ preToolUse: 'PreToolUse',
32
+ postToolUse: 'PostToolUse',
33
+ stop: 'Stop',
34
+ subagentStart: 'SubagentStart',
35
+ subagentStop: 'SubagentStop'
36
+ };
37
+ /** File configs and Kimi's inline array converge on the established command-hook bridge shape. */
38
+ export async function discoverSuiteHooks(root, manifest, errors) {
39
+ let declaration = manifest.components?.hooks;
40
+ if (manifest.layout === 'kimi' && Array.isArray(declaration)) {
41
+ const events = Object.create(null);
42
+ for (const hook of declaration) {
43
+ if (!isRecord(hook) || typeof hook.event !== 'string' || typeof hook.command !== 'string') {
44
+ errors.push('invalid Kimi inline hook');
45
+ return undefined;
46
+ }
47
+ ;
48
+ (events[hook.event] ??= []).push({
49
+ ...(hook.matcher === undefined ? {} : { matcher: hook.matcher }),
50
+ hooks: [{ type: 'command', command: hook.command, ...(hook.timeout === undefined ? {} : { timeout: hook.timeout }) }]
51
+ });
52
+ }
53
+ declaration = { hooks: events };
54
+ }
55
+ const fallback = manifest.layout === 'kimi' ? undefined : await firstComponentFile(root, ['hooks/hooks.json', 'hooks.json']);
56
+ if (declaration === undefined && fallback === undefined)
57
+ return undefined;
58
+ const documents = await componentDocuments(root, declaration ?? fallback, errors, true);
59
+ if (documents === undefined)
60
+ return undefined;
61
+ const normalized = [];
62
+ for (const { path, value } of documents) {
63
+ const events = value.hooks ?? value;
64
+ if (!isRecord(events)) {
65
+ errors.push('hooks must contain an event table');
66
+ return undefined;
67
+ }
68
+ const output = {};
69
+ for (const [event, groups] of Object.entries(events)) {
70
+ const canonicalEvent = EVENT_ALIASES[event] ?? event;
71
+ if (!Array.isArray(groups)) {
72
+ output[canonicalEvent] = groups;
73
+ continue;
74
+ }
75
+ output[canonicalEvent] = groups.map(rawGroup => {
76
+ const group = isRecord(rawGroup) && !Array.isArray(rawGroup.hooks) && (typeof rawGroup.command === 'string' || typeof rawGroup.bash === 'string')
77
+ ? {
78
+ matcher: rawGroup.matcher,
79
+ hooks: [{ ...rawGroup, type: rawGroup.type ?? 'command', command: rawGroup.command ?? rawGroup.bash, timeout: rawGroup.timeout ?? rawGroup.timeoutSec }]
80
+ }
81
+ : rawGroup;
82
+ if (!isRecord(group) || !Array.isArray(group.hooks))
83
+ return group;
84
+ return {
85
+ ...group,
86
+ hooks: group.hooks.map(hook => {
87
+ if (!isRecord(hook))
88
+ return hook;
89
+ if (hook.type !== 'process')
90
+ return typeof hook.command === 'string' ? { ...hook, command: rootVariables(hook.command, root) } : hook;
91
+ if (typeof hook.command !== 'string' || (hook.args !== undefined && (!Array.isArray(hook.args) || !hook.args.every(arg => typeof arg === 'string'))))
92
+ return { ...hook, type: 'command', command: undefined };
93
+ const argv = [hook.command, ...(hook.args ?? [])].map(arg => quote(rootVariables(arg, root)));
94
+ return { ...hook, type: 'command', command: argv.join(' '), ...(typeof hook.timeoutMs === 'number' ? { timeout: hook.timeoutMs / 1000 } : {}) };
95
+ })
96
+ };
97
+ });
98
+ }
99
+ normalized.push({ file: path === undefined ? manifest.path : relative(root, path), settings: { hooks: output } });
100
+ }
101
+ return normalizeHookDocuments(root, normalized, errors);
102
+ }
103
+ export async function discoverSystemPrompt(root, manifest, errors) {
104
+ const parts = [];
105
+ if (manifest.systemPrompt !== undefined)
106
+ parts.push(manifest.systemPrompt);
107
+ if (manifest.systemPromptPath !== undefined) {
108
+ const path = await componentPath(root, manifest.systemPromptPath, errors);
109
+ if (path === undefined)
110
+ return undefined;
111
+ try {
112
+ parts.push(await readFile(path, 'utf8'));
113
+ }
114
+ catch {
115
+ errors.push('systemPromptPath is unreadable');
116
+ return undefined;
117
+ }
118
+ }
119
+ const content = parts.join('\n\n');
120
+ if (Buffer.byteLength(content) > 32768) {
121
+ errors.push('system prompt exceeds 32 KiB');
122
+ return undefined;
123
+ }
124
+ return content === '' ? undefined : content;
125
+ }