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.
- package/README.md +143 -169
- package/README.zh.md +136 -170
- package/client/client.js +8202 -684
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +236 -47
- package/lib/application/details.js +19 -10
- package/lib/application/panel-resources.js +95 -0
- package/lib/application/project-agent-roles.js +39 -0
- package/lib/catalog/codex-mcp.js +114 -0
- package/lib/catalog/component-files.js +151 -0
- package/lib/catalog/manifests.js +105 -44
- package/lib/catalog/native-project.js +44 -23
- package/lib/catalog/paths.js +6 -6
- package/lib/catalog/project-config.js +96 -0
- package/lib/catalog/project-hooks.js +108 -0
- package/lib/catalog/scan-pipeline.js +6 -1
- package/lib/catalog/scan-resolvers.js +70 -26
- package/lib/catalog/source-catalog.js +16 -3
- package/lib/catalog/suite-components.js +125 -0
- package/lib/catalog/surfaces.js +60 -46
- package/lib/catalog/validate.js +16 -11
- package/lib/contracts/market.js +16 -1
- package/lib/index.js +187 -35
- package/lib/model/layouts.js +49 -0
- package/lib/routes.js +106 -6
- package/lib/runtime/agent-role-router.js +226 -0
- package/lib/runtime/commands-mounts.js +27 -67
- package/lib/runtime/feedback-tool.js +181 -0
- package/lib/runtime/hooks-mounts.js +66 -13
- package/lib/runtime/host-locale.js +23 -7
- package/lib/runtime/legacy-root-migration.js +12 -0
- package/lib/runtime/lsp-direct-config.js +6 -3
- package/lib/runtime/lsp-mounts.js +38 -24
- package/lib/runtime/lsp-server-state.js +19 -0
- package/lib/runtime/lsp-status.js +3 -2
- package/lib/runtime/mcp-backend.js +6 -2
- package/lib/runtime/mcp-client/config.js +6 -0
- package/lib/runtime/mcp-client/connection.js +8 -2
- package/lib/runtime/mcp-client/tools.js +6 -3
- package/lib/runtime/mcp-config.js +26 -13
- package/lib/runtime/mcp-direct-config.js +56 -0
- package/lib/runtime/mcp-mounts.js +25 -9
- package/lib/runtime/mcp-overrides.js +11 -5
- package/lib/runtime/mcp-status.js +10 -7
- package/lib/runtime/model-catalog.js +48 -0
- package/lib/runtime/project-runtime.js +136 -0
- package/lib/runtime/reconciler.js +38 -28
- package/lib/runtime/server-config.js +75 -0
- package/lib/runtime/skills-provider.js +31 -77
- package/lib/runtime/storage-migration.js +169 -0
- package/lib/runtime/subagent-catalog.js +129 -0
- package/lib/runtime/user-commands.js +86 -0
- package/lib/runtime/user-panels.js +176 -0
- package/lib/runtime/user-store.js +142 -0
- package/lib/types/application/catalog.d.ts +39 -9
- package/lib/types/application/panel-resources.d.ts +13 -0
- package/lib/types/application/project-agent-roles.d.ts +3 -0
- package/lib/types/application/queries.d.ts +17 -1
- package/lib/types/catalog/codex-mcp.d.ts +4 -0
- package/lib/types/catalog/component-files.d.ts +17 -0
- package/lib/types/catalog/manifests.d.ts +14 -9
- package/lib/types/catalog/native-project.d.ts +2 -5
- package/lib/types/catalog/paths.d.ts +4 -4
- package/lib/types/catalog/project-config.d.ts +6 -0
- package/lib/types/catalog/project-hooks.d.ts +10 -0
- package/lib/types/catalog/scan-pipeline.d.ts +2 -0
- package/lib/types/catalog/scan-resolvers.d.ts +4 -3
- package/lib/types/catalog/source-catalog.d.ts +1 -1
- package/lib/types/catalog/suite-components.d.ts +5 -0
- package/lib/types/catalog/surfaces.d.ts +2 -2
- package/lib/types/catalog/validate.d.ts +2 -0
- package/lib/types/contracts/market.d.ts +60 -0
- package/lib/types/contracts/mcp-status.d.ts +5 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/model/layouts.d.ts +102 -0
- package/lib/types/model/types.d.ts +51 -6
- package/lib/types/routes.d.ts +6 -1
- package/lib/types/runtime/agent-role-router.d.ts +95 -0
- package/lib/types/runtime/commands-mounts.d.ts +16 -7
- package/lib/types/runtime/feedback-tool.d.ts +49 -0
- package/lib/types/runtime/hooks-mounts.d.ts +4 -0
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/legacy-root-migration.d.ts +2 -0
- package/lib/types/runtime/lsp-mounts.d.ts +4 -0
- package/lib/types/runtime/lsp-server-state.d.ts +3 -0
- package/lib/types/runtime/lsp-status.d.ts +1 -0
- package/lib/types/runtime/mcp-backend.d.ts +9 -1
- package/lib/types/runtime/mcp-client/config.d.ts +4 -3
- package/lib/types/runtime/mcp-client/tools.d.ts +3 -0
- package/lib/types/runtime/mcp-config.d.ts +11 -5
- package/lib/types/runtime/mcp-direct-config.d.ts +7 -0
- package/lib/types/runtime/mcp-mounts.d.ts +2 -1
- package/lib/types/runtime/mcp-overrides.d.ts +2 -0
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/model-catalog.d.ts +7 -0
- package/lib/types/runtime/project-runtime.d.ts +32 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/server-config.d.ts +10 -0
- package/lib/types/runtime/skills-provider.d.ts +1 -5
- package/lib/types/runtime/storage-migration.d.ts +15 -0
- package/lib/types/runtime/subagent-catalog.d.ts +52 -0
- package/lib/types/runtime/user-commands.d.ts +24 -0
- package/lib/types/runtime/user-panels.d.ts +91 -0
- package/lib/types/runtime/user-store.d.ts +51 -0
- package/package.json +10 -3
- package/schemas/README.md +69 -7
- package/schemas/agent-plugins/spec.md +48 -0
- package/schemas/claude-code/marketplace.schema.json +143 -0
- package/schemas/claude-code/plugin.schema.json +105 -0
- package/schemas/claude-code/spec.md +108 -0
- package/schemas/codex/marketplace.schema.json +78 -0
- package/schemas/codex/plugin.schema.json +77 -0
- package/schemas/codex/spec.md +91 -0
- package/schemas/cursor/marketplace.schema.json +82 -0
- package/schemas/cursor/plugin.schema.json +58 -0
- package/schemas/cursor/spec.md +92 -0
- package/schemas/github-copilot/marketplace.schema.json +107 -0
- package/schemas/github-copilot/plugin.schema.json +89 -0
- package/schemas/github-copilot/spec.md +105 -0
- package/schemas/kimi/marketplace.schema.json +38 -0
- package/schemas/kimi/plugin.schema.json +84 -0
- package/schemas/kimi/spec.md +94 -0
- package/schemas/qoder/marketplace.schema.json +115 -0
- package/schemas/qoder/plugin.schema.json +127 -0
- package/schemas/qoder/spec.md +111 -0
- package/schemas/skill-collection/spec.md +29 -0
- package/schemas/universal/marketplace.schema.json +98 -0
- package/schemas/universal/plugin.schema.json +49 -0
- package/schemas/universal/spec.md +53 -0
- package/schemas/zcode/marketplace.schema.json +161 -0
- package/schemas/zcode/plugin.schema.json +89 -0
- package/schemas/zcode/spec.md +102 -0
- package/lib/catalog/legacy-root-migration.js +0 -51
- 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
|
+
}
|
package/lib/catalog/manifests.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
...(
|
|
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
|
|
141
|
-
|
|
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
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
59
|
-
const
|
|
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
|
}
|
package/lib/catalog/paths.js
CHANGED
|
@@ -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
|
|
33
|
-
export function resolveUserRoot(
|
|
34
|
-
return
|
|
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
|
|
39
|
+
* into one directory. Legacy overrides are migration inputs only.
|
|
40
40
|
*/
|
|
41
|
-
export function resolveDataRoot(
|
|
42
|
-
return
|
|
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) {
|