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
@@ -0,0 +1,114 @@
1
+ import { PLUGIN_DATA_VARIABLES, PLUGIN_ROOT_VARIABLES } from '../model/layouts.js';
2
+ const FIELDS = new Set([
3
+ 'command',
4
+ 'args',
5
+ 'env',
6
+ 'env_vars',
7
+ 'cwd',
8
+ 'url',
9
+ 'http_headers',
10
+ 'env_http_headers',
11
+ 'bearer_token_env_var',
12
+ 'enabled',
13
+ 'enabled_tools',
14
+ 'disabled_tools',
15
+ 'startup_timeout_sec',
16
+ 'startup_timeout_ms',
17
+ 'tool_timeout_sec'
18
+ ]);
19
+ function record(value) {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
21
+ }
22
+ function text(value, field) {
23
+ if (typeof value !== 'string' || value.trim() === '')
24
+ throw new Error(`${field} must be a non-empty string`);
25
+ return value;
26
+ }
27
+ function strings(value, field) {
28
+ if (value === undefined)
29
+ return undefined;
30
+ if (!Array.isArray(value) || !value.every(item => typeof item === 'string'))
31
+ throw new Error(`${field} must be an array of strings`);
32
+ return value;
33
+ }
34
+ function table(value, field) {
35
+ if (value === undefined)
36
+ return {};
37
+ if (!record(value) || !Object.values(value).every(item => typeof item === 'string'))
38
+ throw new Error(`${field} must be a string table`);
39
+ return value;
40
+ }
41
+ function reference(value, field) {
42
+ const name = text(value, field);
43
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || PLUGIN_ROOT_VARIABLES.has(name) || PLUGIN_DATA_VARIABLES.has(name))
44
+ throw new Error(`${field} must name a non-reserved environment variable`);
45
+ return `\${${name}}`;
46
+ }
47
+ function timeout(value, field, factor) {
48
+ if (value === undefined)
49
+ return undefined;
50
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value * factor > 2_147_483_647)
51
+ throw new Error(`${field} must be a positive finite timeout within the timer range`);
52
+ return Math.max(1, Math.ceil(value * factor));
53
+ }
54
+ /** Unknown or malformed server options reject that server; other servers stay independently usable. */
55
+ export function normalizeCodexMcp(raw, errors) {
56
+ const servers = Object.create(null);
57
+ for (const [name, value] of Object.entries(raw)) {
58
+ try {
59
+ if (!record(value))
60
+ throw new Error('server must be a table');
61
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean')
62
+ throw new Error('enabled must be a boolean');
63
+ if (value.enabled === false)
64
+ continue;
65
+ const unsupported = Object.keys(value).filter(key => !FIELDS.has(key));
66
+ if (unsupported.length > 0)
67
+ throw new Error(`unsupported fields: ${unsupported.join(', ')}`);
68
+ const enabledTools = strings(value.enabled_tools, 'enabled_tools');
69
+ const disabledTools = strings(value.disabled_tools, 'disabled_tools');
70
+ const startupTimeoutMs = value.startup_timeout_sec === undefined ? timeout(value.startup_timeout_ms, 'startup_timeout_ms', 1) : timeout(value.startup_timeout_sec, 'startup_timeout_sec', 1000);
71
+ const toolCallTimeoutMs = timeout(value.tool_timeout_sec, 'tool_timeout_sec', 1000);
72
+ const policy = {
73
+ ...(enabledTools === undefined ? {} : { enabledTools }),
74
+ ...(disabledTools === undefined ? {} : { disabledTools }),
75
+ ...(startupTimeoutMs === undefined ? {} : { startupTimeoutMs }),
76
+ ...(toolCallTimeoutMs === undefined ? {} : { toolCallTimeoutMs })
77
+ };
78
+ if (value.command !== undefined) {
79
+ if (['url', 'http_headers', 'env_http_headers', 'bearer_token_env_var'].some(key => value[key] !== undefined))
80
+ throw new Error('HTTP options cannot be combined with a stdio command');
81
+ const inherited = Object.fromEntries((strings(value.env_vars, 'env_vars') ?? []).map(key => [key, reference(key, 'env_vars')]));
82
+ servers[name] = {
83
+ ...policy,
84
+ type: 'stdio',
85
+ command: text(value.command, 'command'),
86
+ args: strings(value.args, 'args') ?? [],
87
+ env: { ...inherited, ...table(value.env, 'env') },
88
+ ...(value.cwd === undefined ? {} : { cwd: text(value.cwd, 'cwd') })
89
+ };
90
+ }
91
+ else {
92
+ if (['args', 'env', 'env_vars', 'cwd'].some(key => value[key] !== undefined))
93
+ throw new Error('stdio options cannot be combined with an HTTP endpoint');
94
+ const url = text(value.url, 'url');
95
+ if (!['https:', 'http:'].includes(new URL(url).protocol))
96
+ throw new Error('url must use HTTP or HTTPS');
97
+ const headers = {
98
+ ...table(value.http_headers, 'http_headers'),
99
+ ...Object.fromEntries(Object.entries(table(value.env_http_headers, 'env_http_headers')).map(([key, variable]) => [key, reference(variable, 'env_http_headers')]))
100
+ };
101
+ if (value.bearer_token_env_var !== undefined) {
102
+ if (Object.keys(headers).some(key => key.toLowerCase() === 'authorization'))
103
+ throw new Error('bearer_token_env_var conflicts with an Authorization header');
104
+ headers.Authorization = `Bearer ${reference(value.bearer_token_env_var, 'bearer_token_env_var')}`;
105
+ }
106
+ servers[name] = { ...policy, type: 'streamable-http', url, headers };
107
+ }
108
+ }
109
+ catch (error) {
110
+ errors.push(`Codex MCP server "${name}": ${error instanceof Error ? error.message : 'invalid configuration'}`);
111
+ }
112
+ }
113
+ return servers;
114
+ }
@@ -0,0 +1,151 @@
1
+ /** Resolve declared component files once; all consumers use the same contained resource paths. */
2
+ import { readFile, readdir, realpath, stat } from 'node:fs/promises';
3
+ import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { PLUGIN_ROOT_VARIABLES } from '../model/layouts.js';
5
+ export function isRecord(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+ /** Reject both lexical and symlink escapes. Optional defaults may be absent without a diagnostic. */
9
+ export async function componentPath(root, value, errors, required = true) {
10
+ const clean = value.replace(/^\$\{([A-Z_]+)\}\//, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? './' : match));
11
+ const path = resolve(root, clean);
12
+ const rel = relative(resolve(root), path);
13
+ if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) {
14
+ errors.push(`component path ${value}: escapes the suite root`);
15
+ return undefined;
16
+ }
17
+ try {
18
+ const [base, target] = await Promise.all([realpath(root), realpath(path)]);
19
+ const actual = relative(base, target);
20
+ if (isAbsolute(actual) || actual === '..' || actual.startsWith(`..${sep}`)) {
21
+ errors.push(`component path ${value}: escapes the suite root through a symlink`);
22
+ return undefined;
23
+ }
24
+ return path;
25
+ }
26
+ catch {
27
+ if (required)
28
+ errors.push(`component path ${value}: missing or unreadable`);
29
+ return undefined;
30
+ }
31
+ }
32
+ /** Commands and agents may declare one file, several files, or recursive directories. */
33
+ export async function discoverMarkdownResources(root, component, declaration, manifestPath, errors, extensions = ['.md']) {
34
+ const resources = [];
35
+ const seen = new Set();
36
+ const add = (resource) => {
37
+ const identity = `${resource.file}\0${resource.name}`;
38
+ if (!seen.has(identity)) {
39
+ seen.add(identity);
40
+ resources.push(resource);
41
+ }
42
+ };
43
+ async function visit(path, container) {
44
+ const valid = await componentPath(root, relative(root, path), errors);
45
+ if (valid === undefined)
46
+ return;
47
+ const info = await stat(valid);
48
+ if (info.isFile()) {
49
+ if (extensions.includes(extname(path)))
50
+ add({ name: relative(container, path).replace(/\\/g, '/').slice(0, -extname(path).length), file: path });
51
+ return;
52
+ }
53
+ if (!info.isDirectory())
54
+ return;
55
+ for (const entry of (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
56
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.isSymbolicLink())
57
+ continue;
58
+ await visit(join(path, entry.name), container);
59
+ }
60
+ }
61
+ const declared = declaration === undefined ? [component] : Array.isArray(declaration) ? declaration : [declaration];
62
+ for (const item of declared) {
63
+ if (typeof item === 'string') {
64
+ const path = await componentPath(root, item, errors, declaration !== undefined);
65
+ if (path === undefined)
66
+ continue;
67
+ const info = await stat(path);
68
+ await visit(path, info.isDirectory() ? path : resolve(path, '..'));
69
+ }
70
+ else if (component === 'commands' && isRecord(item)) {
71
+ for (const [name, spec] of Object.entries(item)) {
72
+ if (!isRecord(spec) || (typeof spec.source !== 'string' && typeof spec.content !== 'string') || (spec.source !== undefined && spec.content !== undefined)) {
73
+ errors.push(`commands.${name}: requires exactly one source or content`);
74
+ continue;
75
+ }
76
+ if (typeof spec.source === 'string') {
77
+ const file = await componentPath(root, spec.source, errors);
78
+ if (file !== undefined)
79
+ add({ name, file });
80
+ }
81
+ else {
82
+ const metadata = Object.fromEntries(Object.entries(spec).filter(([key]) => !['source', 'content'].includes(key)));
83
+ // JSON is a YAML subset, preserving command metadata without string-built YAML scalars.
84
+ add({ name, file: manifestPath, content: `---\n${JSON.stringify(metadata)}\n---\n${spec.content}` });
85
+ }
86
+ }
87
+ }
88
+ else {
89
+ errors.push(`${component}: invalid component declaration`);
90
+ }
91
+ }
92
+ return resources;
93
+ }
94
+ /** A malformed declared file invalidates that configuration surface instead of falling back. */
95
+ export async function componentDocuments(root, declaration, errors, allowDirectory = false) {
96
+ const result = [];
97
+ for (const item of Array.isArray(declaration) ? declaration : [declaration]) {
98
+ if (typeof item === 'string') {
99
+ const path = await componentPath(root, item, errors);
100
+ if (path === undefined)
101
+ return undefined;
102
+ if (allowDirectory && (await stat(path)).isDirectory()) {
103
+ const files = (await readdir(path))
104
+ .filter(file => file.endsWith('.json'))
105
+ .sort()
106
+ .map(file => relative(root, join(path, file)));
107
+ const nested = await componentDocuments(root, files, errors);
108
+ if (nested === undefined)
109
+ return undefined;
110
+ result.push(...nested);
111
+ continue;
112
+ }
113
+ try {
114
+ const value = JSON.parse(await readFile(path, 'utf8'));
115
+ if (!isRecord(value)) {
116
+ errors.push(`${item}: configuration must be an object`);
117
+ return undefined;
118
+ }
119
+ result.push({ path, value });
120
+ }
121
+ catch {
122
+ errors.push(`${item}: invalid JSON configuration`);
123
+ return undefined;
124
+ }
125
+ }
126
+ else if (isRecord(item)) {
127
+ result.push({ value: item });
128
+ }
129
+ else {
130
+ errors.push('invalid JSON component declaration');
131
+ return undefined;
132
+ }
133
+ }
134
+ return result;
135
+ }
136
+ export async function firstComponentFile(root, candidates) {
137
+ for (const path of candidates)
138
+ if ((await stat(join(root, path)).catch(() => undefined))?.isFile())
139
+ return path;
140
+ return undefined;
141
+ }
142
+ /** Legacy/native suites without catalog resource lists keep their conventional directory behavior. */
143
+ export async function defaultMarkdownResources(root, component) {
144
+ return discoverMarkdownResources(root, component, undefined, '', []);
145
+ }
146
+ export async function resourceText(resource) {
147
+ return resource.content ?? (await readFile(resource.file, 'utf8'));
148
+ }
149
+ export function resourceCommandName(name) {
150
+ return name.replaceAll('/', '-').replaceAll('\\', '-').toLowerCase();
151
+ }
@@ -8,7 +8,10 @@
8
8
  * - claude-code: `.claude-plugin/plugin.json`;
9
9
  * - cursor: `.cursor-plugin/plugin.json`;
10
10
  * - kimi: `.kimi-plugin/plugin.json`;
11
- * - codex: `.codex-plugin/plugin.json`.
11
+ * - codex: `.codex-plugin/plugin.json`;
12
+ * - zcode: `.zcode-plugin/plugin.json`;
13
+ * - qoder: `.qoder-plugin/plugin.json`;
14
+ * - github-copilot: `.github/plugin/plugin.json`.
12
15
  *
13
16
  * The same repo can declare several dialects (e.g. vercel/vercel-plugin ships
14
17
  * all of them); a suite's identity comes from the highest-precedence dialect
@@ -19,26 +22,19 @@ import { readFile, stat } from 'node:fs/promises';
19
22
  import { join, resolve } from 'node:path';
20
23
  import { sanitizeId } from './paths.js';
21
24
  import { isRecognizedSchema, validatePluginManifest } from './validate.js';
22
- /** Dialect precedence: v1 wins over universal over claude over cursor over kimi over codex. */
23
- const KIND_PRECEDENCE = ['agent-plugin-v1', 'universal', 'claude-code', 'cursor', 'kimi', 'codex'];
24
- const MANIFEST_PATHS = {
25
- 'agent-plugin-v1': 'plugin.json',
26
- universal: join('.plugin', 'plugin.json'),
27
- 'claude-code': join('.claude-plugin', 'plugin.json'),
28
- cursor: join('.cursor-plugin', 'plugin.json'),
29
- kimi: join('.kimi-plugin', 'plugin.json'),
30
- codex: join('.codex-plugin', 'plugin.json')
31
- };
25
+ import { PLUGIN_LAYOUTS, MANIFEST_ALIASES, MARKETPLACE_PATHS } from '../model/layouts.js';
32
26
  /** The highest-precedence manifest file a directory carries, if any. */
33
27
  export async function detectManifest(dir) {
34
- for (const kind of KIND_PRECEDENCE) {
35
- const path = join(dir, MANIFEST_PATHS[kind]);
36
- try {
37
- if ((await stat(path)).isFile())
38
- return { kind, path };
39
- }
40
- catch {
41
- // try the next dialect
28
+ for (const { kind, manifest } of PLUGIN_LAYOUTS) {
29
+ for (const relative of [...(MANIFEST_ALIASES[kind] ?? []), manifest]) {
30
+ const path = join(dir, relative);
31
+ try {
32
+ if ((await stat(path)).isFile())
33
+ return { kind, path };
34
+ }
35
+ catch {
36
+ // try the next candidate
37
+ }
42
38
  }
43
39
  }
44
40
  return undefined;
@@ -51,6 +47,10 @@ export async function hasSuiteManifest(dir) {
51
47
  export function syntheticManifestName(root) {
52
48
  return root.split(/[\\/]/).at(-1) ?? 'plugin';
53
49
  }
50
+ export function componentDeclarations(record) {
51
+ const value = record;
52
+ return Object.fromEntries(['skills', 'commands', 'agents', 'hooks', 'mcpServers', 'lspServers'].filter(key => value[key] !== undefined).map(key => [key, value[key]]));
53
+ }
54
54
  /**
55
55
  * Parse one manifest document into a normalized SuiteManifest. The v1 dialect
56
56
  * is schema-validated (fail-closed); the others are structurally read with
@@ -68,7 +68,7 @@ export async function readManifest(root, errors, hint) {
68
68
  errors.push(`${candidate.path} unparsable: ${error instanceof Error ? error.message : String(error)}`);
69
69
  return undefined;
70
70
  }
71
- if (typeof raw !== 'object' || raw === null) {
71
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
72
72
  errors.push(`${candidate.path}: manifest is not a JSON object`);
73
73
  return undefined;
74
74
  }
@@ -82,6 +82,30 @@ export async function readManifest(root, errors, hint) {
82
82
  // layout instead — strict validation only ever applies to manifests
83
83
  // that actually declare the v1 schema.
84
84
  const kind = candidate.kind === 'agent-plugin-v1' && !isRecognizedSchema(record.$schema) ? 'claude-code' : candidate.kind;
85
+ let fallbackComponents = {};
86
+ if (candidate.kind === 'agent-plugin-v1' && kind === 'claude-code') {
87
+ const path = join(root, '.claude-plugin/plugin.json');
88
+ let text;
89
+ try {
90
+ text = await readFile(path, 'utf8');
91
+ }
92
+ catch (error) {
93
+ if (error.code !== 'ENOENT')
94
+ errors.push(`${path}: component fallback is unreadable`);
95
+ }
96
+ if (text !== undefined) {
97
+ try {
98
+ const fallback = JSON.parse(text);
99
+ if (typeof fallback !== 'object' || fallback === null || Array.isArray(fallback))
100
+ errors.push(`${path}: component fallback must be an object`);
101
+ else
102
+ fallbackComponents = componentDeclarations(fallback);
103
+ }
104
+ catch {
105
+ errors.push(`${path}: invalid component fallback JSON`);
106
+ }
107
+ }
108
+ }
85
109
  const problems = kind === 'agent-plugin-v1' ? await validatePluginManifest(raw) : [];
86
110
  errors.push(...problems.map(problem => `${candidate.path}: ${problem}`));
87
111
  const name = pickString(record.name) ?? hint?.name ?? syntheticManifestName(root);
@@ -89,23 +113,26 @@ export async function readManifest(root, errors, hint) {
89
113
  const description = pickString(record.description) ?? hint?.description;
90
114
  const author = record.author;
91
115
  return {
92
- layout: kind === 'agent-plugin-v1'
93
- ? 'agent-plugin-v1'
94
- : kind === 'universal'
95
- ? 'universal'
96
- : kind === 'claude-code'
97
- ? 'claude-code'
98
- : kind === 'cursor'
99
- ? 'cursor'
100
- : kind === 'kimi'
101
- ? 'kimi'
102
- : 'codex',
116
+ components: { ...componentDeclarations(hint ?? {}), ...fallbackComponents, ...componentDeclarations(raw) },
117
+ ...(typeof raw.skillInstructions === 'string' ? { skillInstructions: raw.skillInstructions } : {}),
118
+ ...(typeof raw.systemPrompt === 'string' ? { systemPrompt: raw.systemPrompt } : {}),
119
+ ...(typeof raw.systemPromptPath === 'string' ? { systemPromptPath: raw.systemPromptPath } : {}),
120
+ ...(typeof raw.sessionStart?.skill === 'string'
121
+ ? { startupSkill: raw.sessionStart.skill }
122
+ : {}),
123
+ layout: kind,
103
124
  path: candidate.path,
104
125
  id: sanitizeId(name),
105
126
  name,
106
127
  ...(version === undefined ? {} : { version }),
107
128
  ...(description === undefined ? {} : { description }),
108
- ...(author?.name !== undefined ? { author: author.name } : pickString(record.homepage) !== undefined ? { author: pickString(record.homepage) } : {}),
129
+ ...(typeof record.author === 'string'
130
+ ? { author: record.author }
131
+ : author?.name !== undefined
132
+ ? { author: author.name }
133
+ : pickString(record.homepage) !== undefined
134
+ ? { author: pickString(record.homepage) }
135
+ : {}),
109
136
  keywords: Array.isArray(record.keywords) ? record.keywords.filter((entry) => typeof entry === 'string') : [],
110
137
  ...(isRecognizedSchema(record.$schema) ? { schemaVersion: record.$schema } : {})
111
138
  };
@@ -113,8 +140,6 @@ export async function readManifest(root, errors, hint) {
113
140
  function pickString(value) {
114
141
  return typeof value === 'string' && value !== '' ? value : undefined;
115
142
  }
116
- /** Marketplace manifest locations per dialect (Claude Code, Codex). */
117
- const MARKETPLACE_PATHS = ['.claude-plugin/marketplace.json', '.agents/plugins/marketplace.json'];
118
143
  /** Read one marketplace manifest file; malformed documents are diagnosed, not thrown. */
119
144
  async function readOneMarketplace(path, errors) {
120
145
  let text;
@@ -132,35 +157,71 @@ async function readOneMarketplace(path, errors) {
132
157
  errors.push(`marketplace ${path} unparsable: ${error instanceof Error ? error.message : String(error)}`);
133
158
  return undefined;
134
159
  }
135
- if (typeof parsed !== 'object' || parsed === null) {
160
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
136
161
  errors.push(`marketplace ${path}: manifest is not a JSON object`);
137
162
  return undefined;
138
163
  }
139
164
  const record = parsed;
140
- const plugins = record['plugins'];
141
- if (!Array.isArray(plugins)) {
165
+ const rawPlugins = record['plugins'];
166
+ const plugins = Array.isArray(rawPlugins)
167
+ ? rawPlugins
168
+ : typeof rawPlugins === 'object' && rawPlugins !== null
169
+ ? Object.entries(rawPlugins).map(([name, entry]) => (typeof entry === 'object' && entry !== null && !Array.isArray(entry) ? { ...entry, name } : entry))
170
+ : undefined;
171
+ if (plugins === undefined) {
142
172
  errors.push(`marketplace ${path}: "plugins" is not an array`);
143
173
  return undefined;
144
174
  }
145
175
  return {
176
+ ...(typeof record.metadata?.pluginRoot === 'string' ? { pluginRoot: record.metadata.pluginRoot } : {}),
146
177
  ...(typeof record['name'] === 'string' ? { name: record['name'] } : {}),
147
- entries: plugins,
178
+ entries: plugins
179
+ .map(entry => {
180
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
181
+ return entry;
182
+ const value = entry;
183
+ if (typeof value.id !== 'string')
184
+ return value;
185
+ const source = [value.source, value.url, value.downloadUrl].find(candidate => typeof candidate === 'string' && candidate.trim() !== '') ?? value.source;
186
+ return { ...value, name: value.id, description: value.description ?? value.shortDescription, source, layout: 'kimi' };
187
+ })
188
+ .filter((entry) => {
189
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry) || !('source' in entry)) {
190
+ errors.push(`marketplace ${path}: invalid plugin entry`);
191
+ return false;
192
+ }
193
+ const metadata = entry;
194
+ if (['name', 'version', 'description'].some(key => metadata[key] !== undefined && typeof metadata[key] !== 'string') ||
195
+ (typeof metadata.name === 'string' && metadata.name.trim() === '')) {
196
+ errors.push(`marketplace ${path}: invalid plugin entry metadata`);
197
+ return false;
198
+ }
199
+ return true;
200
+ }),
148
201
  path
149
202
  };
150
203
  }
151
204
  /**
152
205
  * Read every marketplace manifest the checkout carries, in dialect
153
- * precedence order (Claude Code before Codex). Both dialects can coexist
154
- * (e.g. a repo shipping one marketplace per agent); callers decide which
155
- * dialect's entries win, so no dialect is silently shadowed.
206
+ * precedence order shared with suite manifests, then the shared root catalog.
207
+ * The scan strategy selects the first catalog that produces suites; an empty
208
+ * or invalid catalog allows the next candidate to be tried.
156
209
  */
157
- export async function readMarketplaces(checkoutDir) {
210
+ export async function readMarketplaces(checkoutDir, errors = []) {
158
211
  const results = [];
159
- const errors = [];
160
212
  for (const relative of MARKETPLACE_PATHS) {
161
213
  const result = await readOneMarketplace(join(checkoutDir, relative), errors);
162
- if (result !== undefined)
214
+ if (result !== undefined) {
215
+ const kind = PLUGIN_LAYOUTS.find(layout => layout.marketplaces.some(path => path === relative))?.kind;
216
+ for (const entry of result.entries) {
217
+ entry.layout ??= kind;
218
+ if (typeof entry.source === 'string' && /^https?:\/\//.test(entry.source))
219
+ entry.source = { source: 'url', url: entry.source };
220
+ if (typeof entry.source === 'string' && result.pluginRoot !== undefined && !entry.source.startsWith('.'))
221
+ entry.source = join(result.pluginRoot, entry.source);
222
+ }
163
223
  results.push(result);
224
+ }
164
225
  }
165
226
  return results;
166
227
  }
@@ -1,35 +1,27 @@
1
1
  /**
2
2
  * Native project-layout discovery: read a coding agent's own project-local
3
- * directories (`.claude/`, `.agents/`) in place as read-only suites.
3
+ * directories from the shared layout registry in place as read-only suites.
4
4
  *
5
5
  * Migrating a repository from Claude Code / Codex / Cursor should not
6
6
  * require copying files into `.dsh/agent-plugins/`. For the layouts with a
7
- * project-local convention (Claude Code `.claude/`, agent-plugins
8
- * `.agents/`), the project dimension discovers each content-carrying child
7
+ * project-local convention, the project dimension discovers each content-carrying directory
9
8
  * as one synthetic suite so skills and agents reuse the ordinary injection
10
9
  * pipeline. The directories are the repository's own: they are never
11
10
  * installed, uninstalled, or mutated by this manager.
12
11
  */
13
12
  import { join } from 'node:path';
14
- import { isDirectory, sanitizeId } from './paths.js';
13
+ import { stat } from 'node:fs/promises';
14
+ import { sanitizeId } from './paths.js';
15
15
  import { countSurfaces, discoverSkills } from './surfaces.js';
16
+ import { PROJECT_LAYOUTS } from '../model/layouts.js';
17
+ import { discoverProjectMcp } from './project-config.js';
18
+ import { discoverProjectHooks } from './project-hooks.js';
19
+ import { discoverMarkdownResources } from './component-files.js';
16
20
  /**
17
21
  * Project-local directory conventions, in precedence order. Each entry maps
18
22
  * one agent convention onto the content subdirectories it owns.
19
23
  */
20
- export const NATIVE_PROJECT_DIRS = [
21
- { dirName: '.claude', label: 'Claude Code', subdirs: ['skills', 'agents', 'commands'] },
22
- { dirName: '.agents', label: 'agents', subdirs: ['skills', 'agents', 'commands'] }
23
- ];
24
- /** A native project directory that carries no relevant content is skipped. */
25
- async function carriesContent(dir, projectRoot) {
26
- for (const subdir of dir.subdirs) {
27
- const path = join(projectRoot, dir.dirName, subdir);
28
- if (await isDirectory(path))
29
- return true;
30
- }
31
- return false;
32
- }
24
+ export const NATIVE_PROJECT_DIRS = PROJECT_LAYOUTS;
33
25
  /**
34
26
  * Discover native project-layout suites under a project root. Each
35
27
  * convention directory that carries content becomes one synthetic suite;
@@ -38,10 +30,6 @@ async function carriesContent(dir, projectRoot) {
38
30
  export async function discoverNativeProjectSuites(projectRoot, dimension) {
39
31
  const suites = [];
40
32
  for (const native of NATIVE_PROJECT_DIRS) {
41
- if (!(await isDirectory(join(projectRoot, native.dirName))))
42
- continue;
43
- if (!(await carriesContent(native, projectRoot)))
44
- continue;
45
33
  const root = join(projectRoot, native.dirName);
46
34
  const id = sanitizeId(`${native.dirName}-native`);
47
35
  const manifest = {
@@ -52,20 +40,53 @@ export async function discoverNativeProjectSuites(projectRoot, dimension) {
52
40
  description: `Skills and agents read in place from the project's ${native.dirName}/ directory.`
53
41
  };
54
42
  const errors = [];
43
+ const lspFiles = [join(native.dirName, 'lsp.json'), join(native.dirName, '.lsp.json')];
44
+ if (native.dirName === '.claude')
45
+ lspFiles.push('.lsp.json');
46
+ if (native.dirName === '.github')
47
+ lspFiles.push('lsp.json');
48
+ for (const path of lspFiles) {
49
+ if ((await stat(join(projectRoot, path)).catch(() => undefined))?.isFile()) {
50
+ errors.push(`${path}: project LSP configuration is not mounted; the host LSP registry does not isolate projects`);
51
+ }
52
+ }
55
53
  // Skills live in `<dir>/skills/<name>/SKILL.md` — the same shape as the
56
54
  // ordinary skills container; agents/commands only surface as counts here
57
55
  // and are read by the runtime providers directly from the same files.
58
- const skills = await discoverSkills(root, errors, 'skills');
59
- const surfaces = await countSurfaces(root, skills, undefined);
56
+ const skills = await discoverSkills(root, errors);
57
+ const mcp = await discoverProjectMcp(projectRoot, native.mcpFiles ?? [], errors, native.mcpFormat);
58
+ const hooks = await discoverProjectHooks(projectRoot, native.hookFiles ?? [], errors, native.hookFormat);
59
+ const resources = {
60
+ commands: native.subdirs.includes('commands') ? await discoverMarkdownResources(root, 'commands', undefined, manifest.path, errors) : [],
61
+ agents: native.subdirs.includes('agents') ? await discoverMarkdownResources(root, 'agents', undefined, manifest.path, errors) : []
62
+ };
63
+ const surfaces = await countSurfaces(root, skills, mcp);
64
+ surfaces.commands = resources.commands.length;
65
+ surfaces.agents = resources.agents.length;
66
+ surfaces.hooks = Object.values(hooks?.events ?? {}).reduce((total, groups) => total + groups.reduce((count, group) => count + group.hooks.length, 0), 0);
67
+ surfaces.lsp = 0;
68
+ if (Object.values(surfaces).every(count => count === 0) && errors.length === 0)
69
+ continue;
60
70
  suites.push({
61
71
  sourceId: 'native',
62
72
  id,
63
73
  root,
64
74
  manifest,
65
75
  skills,
76
+ resources,
77
+ ...(mcp === undefined ? {} : { mcp }),
78
+ ...(hooks === undefined ? {} : { hooks }),
66
79
  surfaces,
67
80
  dimension,
68
81
  enabled: true,
82
+ activeSurfaces: {
83
+ skills: true,
84
+ commands: native.subdirs.includes('commands'),
85
+ agents: native.subdirs.includes('agents'),
86
+ mcp: mcp !== undefined,
87
+ hooks: hooks !== undefined,
88
+ lsp: false
89
+ },
69
90
  errors
70
91
  });
71
92
  }
@@ -29,17 +29,17 @@ export function expandHome(path) {
29
29
  export function resolveDshHome() {
30
30
  return process.env.DSH_HOME === undefined ? join(homedir(), '.dsh') : resolve(process.env.DSH_HOME);
31
31
  }
32
- /** Resolve the user-dimension suite root. */
33
- export function resolveUserRoot(configUserRoot) {
34
- return resolve(expandHome(configUserRoot ?? join(resolveDshHome(), 'agent-plugins')));
32
+ /** Resolve the canonical user-dimension root. Legacy overrides are migration inputs only. */
33
+ export function resolveUserRoot(_configUserRoot) {
34
+ return join(resolveDshHome(), 'agent-plugins');
35
35
  }
36
36
  /**
37
37
  * Resolve the suite data root hosting `${PLUGIN_DATA}` directories and the
38
38
  * MCP overrides. Defaults under the user root so the whole plugin persists
39
- * into one directory; an explicit `dataRoot` config still wins.
39
+ * into one directory. Legacy overrides are migration inputs only.
40
40
  */
41
- export function resolveDataRoot(configDataRoot, configUserRoot) {
42
- return resolve(expandHome(configDataRoot ?? join(configUserRoot ?? resolveUserRoot(), 'data')));
41
+ export function resolveDataRoot(_configDataRoot, _configUserRoot) {
42
+ return join(resolveUserRoot(), 'data');
43
43
  }
44
44
  /** Resolve a project root from a workspace cwd: nearest ancestor with `.git`. */
45
45
  export async function findProjectRoot(cwd) {