dsh-agent-plugins-market 0.7.0 → 0.7.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 +31 -26
- package/README.zh.md +31 -26
- package/client/client.js +29164 -1136
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +8 -1
- package/lib/application/catalog.js +27 -2
- package/lib/application/details.js +13 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +191 -19
- package/lib/application/panel-resources.js +16 -4
- package/lib/application/project-agent-roles.js +7 -1
- package/lib/catalog/archive.js +16 -1
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/paths.js +8 -0
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-hooks.js +14 -1
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/skills-parse.js +47 -0
- package/lib/catalog/source-catalog.js +8 -3
- package/lib/catalog/suite-components.js +2 -2
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +3 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +32 -13
- package/lib/model/layouts.js +44 -1
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +43 -2
- package/lib/runtime/agent-role-router.js +203 -56
- package/lib/runtime/commands-mounts.js +50 -9
- package/lib/runtime/deadline.js +32 -6
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +7 -3
- package/lib/runtime/host-locale.js +45 -15
- package/lib/runtime/lsp-mounts.js +23 -2
- package/lib/runtime/mcp-backend.js +14 -15
- package/lib/runtime/mcp-client/config.js +8 -0
- package/lib/runtime/mcp-client/connection.js +9 -8
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +182 -35
- package/lib/runtime/mcp-direct-config.js +49 -6
- package/lib/runtime/mcp-mounts.js +15 -3
- package/lib/runtime/mcp-overrides.js +131 -1
- package/lib/runtime/mcp-redaction.js +8 -0
- package/lib/runtime/mcp-status.js +21 -5
- package/lib/runtime/project-runtime.js +23 -9
- package/lib/runtime/reconcile-scheduler.js +6 -8
- package/lib/runtime/reconciler.js +2 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/settings-namespace.js +21 -13
- package/lib/runtime/skills-provider.js +33 -11
- package/lib/runtime/source-auto-update.js +10 -14
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +24 -20
- package/lib/runtime/user-commands.js +5 -1
- package/lib/runtime/user-panels.js +110 -41
- package/lib/runtime/user-store.js +114 -58
- package/lib/types/application/catalog-context.d.ts +6 -0
- package/lib/types/application/catalog.d.ts +19 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +38 -3
- package/lib/types/application/ports.d.ts +1 -1
- package/lib/types/application/queries.d.ts +8 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/paths.d.ts +6 -0
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/skills-parse.d.ts +13 -0
- package/lib/types/catalog/source-catalog.d.ts +7 -2
- package/lib/types/catalog/surfaces.d.ts +14 -4
- package/lib/types/catalog/validate.d.ts +72 -11
- package/lib/types/contracts/market.d.ts +77 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +30 -0
- package/lib/types/model/preset-source.d.ts +18 -0
- package/lib/types/model/types.d.ts +37 -1
- package/lib/types/runtime/agent-role-router.d.ts +96 -18
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +10 -11
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-mounts.d.ts +7 -0
- package/lib/types/runtime/mcp-backend.d.ts +8 -15
- package/lib/types/runtime/mcp-client/config.d.ts +8 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
- package/lib/types/runtime/mcp-config.d.ts +75 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
- package/lib/types/runtime/mcp-overrides.d.ts +44 -0
- package/lib/types/runtime/mcp-redaction.d.ts +6 -0
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/settings-namespace.d.ts +10 -10
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +3 -4
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
- package/lib/types/runtime/user-panels.d.ts +65 -12
- package/lib/types/runtime/user-store.d.ts +65 -16
- package/package.json +23 -6
- package/schemas/1.0.0/mcp.schema.json +0 -22
- package/schemas/1.1.0/mcp.schema.json +120 -0
- package/schemas/1.1.0/plugin.schema.json +65 -0
- package/schemas/README.md +25 -19
- package/schemas/agent-plugins/spec.md +23 -9
- package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
- package/schemas/com.deepseek.harness/spec.md +52 -0
package/lib/catalog/manifests.js
CHANGED
|
@@ -23,7 +23,8 @@ import { join } from 'node:path';
|
|
|
23
23
|
import { sanitizeId } from './paths.js';
|
|
24
24
|
import { isFile } from './fs-probes.js';
|
|
25
25
|
import { isRecord } from './component-files.js';
|
|
26
|
-
import { isRecognizedSchema, validatePluginManifest } from './validate.js';
|
|
26
|
+
import { formatSchemaErrors, isRecognizedSchema, validatePluginManifest } from './validate.js';
|
|
27
|
+
import { EXTENSION_NAMESPACE, NAMESPACE_SCHEMA_VERSIONS, V1_IGNORED_MANIFEST_KEYS } from '../model/layouts.js';
|
|
27
28
|
import { PLUGIN_LAYOUTS, MANIFEST_ALIASES, MARKETPLACE_PATHS } from '../model/layouts.js';
|
|
28
29
|
/**
|
|
29
30
|
* Every manifest file a directory carries, in selection order: highest
|
|
@@ -65,14 +66,20 @@ export function componentDeclarations(record) {
|
|
|
65
66
|
* go to `fallbacks`, so the winner stays usable while the caller can still
|
|
66
67
|
* report why the higher-priority declaration was ignored. The winner's own
|
|
67
68
|
* diagnostics stay in `errors` and remain fatal.
|
|
69
|
+
*
|
|
70
|
+
* `notes` carries the winner's non-fatal diagnostics — §5.2 unknown
|
|
71
|
+
* top-level fields, §8.1 extension data problems. Callers surface them
|
|
72
|
+
* without failing the suite.
|
|
68
73
|
*/
|
|
69
|
-
export async function readManifest(root, errors, hint, fallbacks = []) {
|
|
74
|
+
export async function readManifest(root, errors, hint, fallbacks = [], notes = []) {
|
|
70
75
|
const candidates = await detectManifests(root);
|
|
71
76
|
for (const [index, candidate] of candidates.entries()) {
|
|
72
77
|
const attempt = [];
|
|
73
|
-
const
|
|
78
|
+
const attemptNotes = [];
|
|
79
|
+
const manifest = await readOneManifest(root, candidate, attempt, hint, attemptNotes);
|
|
74
80
|
if (manifest !== undefined) {
|
|
75
81
|
errors.push(...attempt);
|
|
82
|
+
notes.push(...attemptNotes);
|
|
76
83
|
return manifest;
|
|
77
84
|
}
|
|
78
85
|
if (index === candidates.length - 1)
|
|
@@ -83,7 +90,7 @@ export async function readManifest(root, errors, hint, fallbacks = []) {
|
|
|
83
90
|
return undefined;
|
|
84
91
|
}
|
|
85
92
|
/** Parse one manifest candidate; undefined when this candidate is unusable. */
|
|
86
|
-
async function readOneManifest(root, candidate, errors, hint) {
|
|
93
|
+
async function readOneManifest(root, candidate, errors, hint, notes = []) {
|
|
87
94
|
let raw;
|
|
88
95
|
try {
|
|
89
96
|
raw = JSON.parse(await readFile(candidate.path, 'utf8'));
|
|
@@ -130,21 +137,47 @@ async function readOneManifest(root, candidate, errors, hint) {
|
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
// v1 validation verdicts split per §5.2: fatal violations reject this
|
|
141
|
+
// candidate (the next one by priority is tried), ignored violations —
|
|
142
|
+
// unknown top-level fields and the whole `extensions` subtree — are
|
|
143
|
+
// reported through `notes` and the manifest keeps loading.
|
|
144
|
+
const verdict = kind === 'agent-plugin-v1' ? await validatePluginManifest(raw) : undefined;
|
|
145
|
+
if (verdict !== undefined && verdict.fatal.length > 0) {
|
|
146
|
+
errors.push(...formatSchemaErrors(verdict.fatal).map(problem => `${candidate.path}: ${problem}`));
|
|
136
147
|
return undefined;
|
|
137
148
|
}
|
|
149
|
+
const ignored = verdict === undefined ? [] : formatSchemaErrors(verdict.ignored);
|
|
150
|
+
if (kind === 'agent-plugin-v1') {
|
|
151
|
+
// §5.2 / §6.1: the portable manifest carries no component configuration
|
|
152
|
+
// and no client semantics beyond `extensions`; every inline key this
|
|
153
|
+
// manager would otherwise read is an unknown top-level field here.
|
|
154
|
+
for (const key of V1_IGNORED_MANIFEST_KEYS) {
|
|
155
|
+
if (raw[key] !== undefined)
|
|
156
|
+
ignored.push(`${key}: not part of the portable manifest; ignored`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
notes.push(...ignored.map(problem => `${candidate.path}: ${problem}`));
|
|
160
|
+
const harness = kind === 'agent-plugin-v1' ? parseHarnessExtension(record['extensions'], notes) : undefined;
|
|
138
161
|
const name = pickString(record.name) ?? hint?.name ?? syntheticManifestName(root);
|
|
139
162
|
const version = pickString(record.version) ?? hint?.version;
|
|
140
163
|
const description = pickString(record.description) ?? hint?.description;
|
|
141
164
|
const author = record.author;
|
|
142
165
|
return {
|
|
143
|
-
components: {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
components: {
|
|
167
|
+
...(kind === 'agent-plugin-v1' ? {} : componentDeclarations(hint ?? {})),
|
|
168
|
+
...fallbackComponents,
|
|
169
|
+
...(kind === 'agent-plugin-v1' ? {} : componentDeclarations(raw))
|
|
170
|
+
},
|
|
171
|
+
...(typeof raw.skillInstructions === 'string' && kind !== 'agent-plugin-v1'
|
|
172
|
+
? { skillInstructions: raw.skillInstructions }
|
|
173
|
+
: {}),
|
|
174
|
+
...(typeof raw.systemPrompt === 'string' && kind !== 'agent-plugin-v1'
|
|
175
|
+
? { systemPrompt: raw.systemPrompt }
|
|
176
|
+
: {}),
|
|
177
|
+
...(typeof raw.systemPromptPath === 'string' && kind !== 'agent-plugin-v1'
|
|
178
|
+
? { systemPromptPath: raw.systemPromptPath }
|
|
179
|
+
: {}),
|
|
180
|
+
...(kind !== 'agent-plugin-v1' && typeof raw.sessionStart?.skill === 'string'
|
|
148
181
|
? { startupSkill: raw.sessionStart.skill }
|
|
149
182
|
: {}),
|
|
150
183
|
layout: kind,
|
|
@@ -161,9 +194,87 @@ async function readOneManifest(root, candidate, errors, hint) {
|
|
|
161
194
|
? { author: pickString(record.homepage) }
|
|
162
195
|
: {}),
|
|
163
196
|
keywords: Array.isArray(record.keywords) ? record.keywords.filter((entry) => typeof entry === 'string') : [],
|
|
164
|
-
...(isRecognizedSchema(record.$schema) ? { schemaVersion: record.$schema } : {})
|
|
197
|
+
...(isRecognizedSchema(record.$schema) ? { schemaVersion: record.$schema } : {}),
|
|
198
|
+
...(harness === undefined ? {} : { harness })
|
|
165
199
|
};
|
|
166
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Read this client's §8.1 extension data from a v1 manifest's `extensions`
|
|
203
|
+
* object. Unknown namespaces are ignored without validating their contents
|
|
204
|
+
* (§8.1); our own namespace is validated against the namespace contract, and
|
|
205
|
+
* an unusable value is reported through `notes` instead of failing the
|
|
206
|
+
* plugin.
|
|
207
|
+
*
|
|
208
|
+
* @returns the parsed namespace data, or undefined when the suite declares
|
|
209
|
+
* none; diagnostics land in `notes`.
|
|
210
|
+
*/
|
|
211
|
+
function parseHarnessExtension(extensions, notes) {
|
|
212
|
+
if (!isRecord(extensions) || extensions[EXTENSION_NAMESPACE] === undefined)
|
|
213
|
+
return undefined;
|
|
214
|
+
return parseHarnessNamespace(extensions[EXTENSION_NAMESPACE], notes);
|
|
215
|
+
}
|
|
216
|
+
/** Validate one namespace value against the namespace contract; diagnostics go to `notes`. */
|
|
217
|
+
function parseHarnessNamespace(value, notes) {
|
|
218
|
+
if (!isRecord(value)) {
|
|
219
|
+
notes.push(`extensions.${EXTENSION_NAMESPACE}: value must be an object; ignored`);
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const schemaVersion = value['schemaVersion'];
|
|
223
|
+
if (typeof schemaVersion !== 'string' || !NAMESPACE_SCHEMA_VERSIONS.has(schemaVersion)) {
|
|
224
|
+
notes.push(`extensions.${EXTENSION_NAMESPACE}: unsupported or missing schemaVersion ${JSON.stringify(schemaVersion)}; supported: ${[...NAMESPACE_SCHEMA_VERSIONS].join(', ')}; namespace extensions are ignored`);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const policies = Object.create(null);
|
|
228
|
+
const rawPolicies = value['mcpServers'];
|
|
229
|
+
if (rawPolicies !== undefined) {
|
|
230
|
+
if (!isRecord(rawPolicies)) {
|
|
231
|
+
notes.push(`extensions.${EXTENSION_NAMESPACE}.mcpServers: value must be an object; ignored`);
|
|
232
|
+
return { schemaVersion, mcpServers: policies };
|
|
233
|
+
}
|
|
234
|
+
for (const [name, policy] of Object.entries(rawPolicies)) {
|
|
235
|
+
if (!isRecord(policy)) {
|
|
236
|
+
notes.push(`extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}: policy must be an object; ignored`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const auth = policy['auth'];
|
|
240
|
+
if (auth !== undefined &&
|
|
241
|
+
(!isRecord(auth) || (auth['enabled'] !== undefined && typeof auth['enabled'] !== 'boolean') || (auth['scope'] !== undefined && typeof auth['scope'] !== 'string'))) {
|
|
242
|
+
notes.push(`extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.auth: invalid auth declaration; ignored`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const enabledTools = optionalStringArray(policy['enabledTools'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.enabledTools`, notes);
|
|
246
|
+
const disabledTools = optionalStringArray(policy['disabledTools'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.disabledTools`, notes);
|
|
247
|
+
const startupTimeoutMs = optionalPositiveNumber(policy['startupTimeoutMs'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.startupTimeoutMs`, notes);
|
|
248
|
+
const toolCallTimeoutMs = optionalPositiveNumber(policy['toolCallTimeoutMs'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.toolCallTimeoutMs`, notes);
|
|
249
|
+
policies[name] = {
|
|
250
|
+
...(enabledTools === undefined ? {} : { enabledTools }),
|
|
251
|
+
...(disabledTools === undefined ? {} : { disabledTools }),
|
|
252
|
+
...(startupTimeoutMs === undefined ? {} : { startupTimeoutMs }),
|
|
253
|
+
...(toolCallTimeoutMs === undefined ? {} : { toolCallTimeoutMs }),
|
|
254
|
+
...(auth === undefined || !isRecord(auth) ? {} : { auth: { enabled: auth['enabled'] !== false, ...(typeof auth['scope'] === 'string' ? { scope: auth['scope'] } : {}) } })
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { schemaVersion, mcpServers: policies };
|
|
259
|
+
}
|
|
260
|
+
function optionalStringArray(value, label, notes) {
|
|
261
|
+
if (value === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
if (!Array.isArray(value) || !value.every(entry => typeof entry === 'string')) {
|
|
264
|
+
notes.push(`${label}: must be an array of strings; ignored`);
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
function optionalPositiveNumber(value, label, notes) {
|
|
270
|
+
if (value === undefined)
|
|
271
|
+
return undefined;
|
|
272
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
273
|
+
notes.push(`${label}: must be a positive number; ignored`);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
167
278
|
function pickString(value) {
|
|
168
279
|
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
169
280
|
}
|
package/lib/catalog/paths.js
CHANGED
|
@@ -93,6 +93,14 @@ export function sourceCheckoutDir(dimensionRoot, sourceId) {
|
|
|
93
93
|
export function qualifiedSuiteId(sourceId, suiteId) {
|
|
94
94
|
return `${sourceId}/${suiteId}`;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* The data directory `${PLUGIN_DATA}` resolves to for one suite: a
|
|
98
|
+
* source-qualified directory under the plugin storage root, shared by every
|
|
99
|
+
* surface of that suite so MCP servers, skills and commands see one location.
|
|
100
|
+
*/
|
|
101
|
+
export function suiteDataDir(dataRoot, sourceId, suiteId) {
|
|
102
|
+
return `${dataRoot.replace(/[\\/]$/, '')}/${qualifiedSuiteId(sourceId, suiteId)}`;
|
|
103
|
+
}
|
|
96
104
|
/**
|
|
97
105
|
* Whether `candidate` is `root` itself or a path below it, compared on whole
|
|
98
106
|
* path segments by `flavor`'s rules so a sibling whose name merely starts with
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-variable expansion for author-written suite text.
|
|
3
|
+
*
|
|
4
|
+
* A suite authored for Claude Code or its dialects writes `${CLAUDE_PLUGIN_ROOT}`
|
|
5
|
+
* (or the dialect's own spelling) into command bodies, agent personas, hook
|
|
6
|
+
* commands, LSP declarations, startup instructions and skill prose, and uses
|
|
7
|
+
* `${CLAUDE_PLUGIN_DATA}`, `${CLAUDE_PROJECT_DIR}` and `${CLAUDE_SKILL_DIR}` the
|
|
8
|
+
* same way. Its home ecosystem replaces those variables before the text reaches
|
|
9
|
+
* the model or a process; every surface here has to do the same, or the command
|
|
10
|
+
* runs with an empty path. One implementation keeps the surfaces agreeing on
|
|
11
|
+
* what each spelling means.
|
|
12
|
+
*
|
|
13
|
+
* A variable whose value the calling layer does not hold is left exactly as
|
|
14
|
+
* written: an author's `${NAME:-default}`-style intent is never silently
|
|
15
|
+
* emptied, and a surface that cannot supply a session directory does not
|
|
16
|
+
* invent one.
|
|
17
|
+
*/
|
|
18
|
+
import { PLUGIN_DATA_VARIABLES, PLUGIN_ROOT_VARIABLES, PROJECT_DIR_VARIABLES, SKILL_DIR_VARIABLE } from '../model/layouts.js';
|
|
19
|
+
/** The suite root `${PLUGIN_ROOT}` and its dialect aliases resolve to. */
|
|
20
|
+
export function pluginRootOf(suite) {
|
|
21
|
+
// Project-native directories are the repository's own files, read in place;
|
|
22
|
+
// no plugin root exists for them, so the variable stays as the author wrote it.
|
|
23
|
+
return suite.manifest.layout === 'project-native' ? undefined : suite.root;
|
|
24
|
+
}
|
|
25
|
+
/** Replace every path variable the context can resolve; other text stays exactly as written. */
|
|
26
|
+
export function expandPluginPaths(text, context) {
|
|
27
|
+
return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => {
|
|
28
|
+
if (PLUGIN_ROOT_VARIABLES.has(name))
|
|
29
|
+
return context.root ?? match;
|
|
30
|
+
if (PLUGIN_DATA_VARIABLES.has(name))
|
|
31
|
+
return context.data ?? match;
|
|
32
|
+
if (name === SKILL_DIR_VARIABLE)
|
|
33
|
+
return context.skillDir ?? match;
|
|
34
|
+
if (PROJECT_DIR_VARIABLES.has(name))
|
|
35
|
+
return context.projectDir ?? match;
|
|
36
|
+
return match;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -11,6 +11,19 @@ function object(value) {
|
|
|
11
11
|
function declarationText(value) {
|
|
12
12
|
return String(value);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Present one read document as the settings shape the shared validator reads:
|
|
16
|
+
* a standalone hook file may carry the bare event table instead of a `hooks`
|
|
17
|
+
* key. A recognized top-level event name is the marker — a document with no
|
|
18
|
+
* event name at the top stays verbatim, so an ordinary settings file missing
|
|
19
|
+
* `hooks` is still not accepted as a bare event table.
|
|
20
|
+
*/
|
|
21
|
+
function settingsDocument(settings, format) {
|
|
22
|
+
if (format !== 'claude' || settings.hooks !== undefined)
|
|
23
|
+
return settings;
|
|
24
|
+
const isEventTable = Object.keys(settings).some(key => EVENTS.has(key));
|
|
25
|
+
return isEventTable ? { ...settings, hooks: settings } : settings;
|
|
26
|
+
}
|
|
14
27
|
/** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
|
|
15
28
|
export async function discoverProjectHooks(projectRoot, files, errors, format = 'claude') {
|
|
16
29
|
const documents = [];
|
|
@@ -19,7 +32,7 @@ export async function discoverProjectHooks(projectRoot, files, errors, format =
|
|
|
19
32
|
if (settings === null)
|
|
20
33
|
return undefined;
|
|
21
34
|
if (settings !== undefined)
|
|
22
|
-
documents.push({ file, settings });
|
|
35
|
+
documents.push({ file, settings: settingsDocument(settings, format) });
|
|
23
36
|
}
|
|
24
37
|
return normalizeHookDocuments(projectRoot, documents, errors, format);
|
|
25
38
|
}
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
* 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
|
|
19
19
|
* `<root>/<name>/SKILL.md` collections.
|
|
20
20
|
*/
|
|
21
|
-
import { realpath } from 'node:fs/promises';
|
|
21
|
+
import { realpath, readFile } from 'node:fs/promises';
|
|
22
22
|
import { join, resolve } from 'node:path';
|
|
23
23
|
import { isWithin, sanitizeId } from './paths.js';
|
|
24
24
|
import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
|
|
25
|
+
import { EXTENSION_NAMESPACE, NAMESPACE_DIR, NAMESPACE_HOOKS_FILE, NAMESPACE_LSP_FILE } from '../model/layouts.js';
|
|
25
26
|
import { componentDeclarations, hasSuiteManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
|
|
26
27
|
import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
|
|
27
28
|
import { discoverMarkdownResources, isUnknownArray } from './component-files.js';
|
|
28
29
|
import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
|
|
30
|
+
import { parseLspServers } from './lsp-spec.js';
|
|
31
|
+
import { normalizeHookDocuments } from './project-hooks.js';
|
|
29
32
|
import { runScanChain } from './scan-pipeline.js';
|
|
30
33
|
const CONTAINER_DIRS = ['plugins', 'external_plugins', 'skills'];
|
|
31
34
|
/**
|
|
@@ -327,10 +330,11 @@ export async function hasSkillFiles(dir) {
|
|
|
327
330
|
/** Read one suite root into the normalized shape, or undefined when no manifest parses. */
|
|
328
331
|
export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
329
332
|
const errors = [];
|
|
333
|
+
const manifestNotes = [];
|
|
330
334
|
const fallbacks = [];
|
|
331
335
|
const declaredManifest = await hasSuiteManifest(root);
|
|
332
|
-
let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks) : await syntheticManifest(root);
|
|
333
|
-
if (declaredManifest &&
|
|
336
|
+
let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks, manifestNotes) : await syntheticManifest(root);
|
|
337
|
+
if (declaredManifest && manifest === undefined) {
|
|
334
338
|
notes.push(...errors, ...fallbacks, `suite ${root}: rejected declared manifest`);
|
|
335
339
|
return undefined;
|
|
336
340
|
}
|
|
@@ -338,6 +342,7 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
|
338
342
|
// higher-priority declaration was skipped visible without failing the suite.
|
|
339
343
|
if (fallbacks.length > 0)
|
|
340
344
|
notes.push(`suite ${root}: a higher-priority manifest was rejected`, ...fallbacks);
|
|
345
|
+
notes.push(...manifestNotes);
|
|
341
346
|
// A declaration-only suite (official CC lsp plugins ship just a README):
|
|
342
347
|
// the marketplace entry's inline lspServers are its manifest.
|
|
343
348
|
if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
|
|
@@ -354,24 +359,41 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
|
354
359
|
}
|
|
355
360
|
if (manifest === undefined)
|
|
356
361
|
return undefined;
|
|
362
|
+
// The portable v1 dialect reads only the spec's fixed locations plus this
|
|
363
|
+
// client's extension namespace (§6.1, §8.2): skills and MCP from their
|
|
364
|
+
// fixed locations, extension surfaces from `com.deepseek.harness/`.
|
|
365
|
+
// Marketplace-entry and hint declarations carry no inline components for
|
|
366
|
+
// this dialect, so `components` stays empty and every discovery call below
|
|
367
|
+
// takes its namespace path.
|
|
368
|
+
const portable = manifest.layout === 'agent-plugin-v1';
|
|
369
|
+
// The namespace seats (manifest data and directory) open only when the
|
|
370
|
+
// suite declares our namespace with a supported contract version; an
|
|
371
|
+
// unsupported or missing declaration reads the portable core alone.
|
|
372
|
+
const namespaceOpen = portable && (manifest.harness !== undefined || manifest.schemaVersion !== undefined) && namespaceReadable(manifest);
|
|
357
373
|
let declared = manifest.components?.skills;
|
|
358
374
|
if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
|
|
359
375
|
declared = ['skills', ...(isUnknownArray(declared) ? declared : [declared])];
|
|
360
376
|
}
|
|
361
|
-
|
|
377
|
+
if (portable)
|
|
378
|
+
await noteUnreadRootComponents(root, notes);
|
|
379
|
+
const skills = await discoverSkills(root, errors, declared, portable);
|
|
362
380
|
if (manifest.layout === 'skill-collection' && skills.length === 0) {
|
|
363
381
|
notes.push(...errors);
|
|
364
382
|
return undefined;
|
|
365
383
|
}
|
|
366
|
-
const mcp = await discoverMcp(root, errors, manifest);
|
|
384
|
+
const mcp = await discoverMcp(root, errors, manifest, portable);
|
|
367
385
|
// Effective component declarations include marketplace fallbacks and manifest overrides.
|
|
368
|
-
const lsp = await discoverSuiteLsp(root, manifest, errors);
|
|
369
|
-
const hooks = await discoverSuiteHooks(root, manifest, errors);
|
|
370
|
-
const resources =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
386
|
+
const lsp = namespaceOpen ? await discoverNamespaceLsp(root, manifest, errors, notes) : await discoverSuiteLsp(root, manifest, errors);
|
|
387
|
+
const hooks = namespaceOpen ? await discoverNamespaceHooks(root, manifest, errors, notes) : await discoverSuiteHooks(root, manifest, errors);
|
|
388
|
+
const resources = namespaceOpen
|
|
389
|
+
? await discoverNamespaceMarkdown(root, notes)
|
|
390
|
+
: portable
|
|
391
|
+
? { commands: [], agents: [] }
|
|
392
|
+
: {
|
|
393
|
+
commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
|
|
394
|
+
agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
|
|
395
|
+
};
|
|
396
|
+
const systemPrompt = portable ? undefined : await discoverSystemPrompt(root, manifest, errors);
|
|
375
397
|
const surfaces = await countSurfaces(root, skills, mcp, lsp);
|
|
376
398
|
surfaces.commands = resources.commands.length;
|
|
377
399
|
surfaces.agents = resources.agents.length;
|
|
@@ -428,3 +450,86 @@ async function syntheticManifest(root) {
|
|
|
428
450
|
name
|
|
429
451
|
};
|
|
430
452
|
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Extension namespace surfaces (Agent Plugins §8.2)
|
|
455
|
+
//
|
|
456
|
+
// A portable v1 suite carries this client's extension surfaces in the
|
|
457
|
+
// top-level directory named after the namespace. Contents outside it — the
|
|
458
|
+
// dialect root directories other layouts read — are package files this
|
|
459
|
+
// manager does not mount for the portable dialect, and their presence is
|
|
460
|
+
// reported through scan notes rather than read silently.
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
/** Root component locations other dialects read; under v1 their presence is reported, never read. */
|
|
463
|
+
const ROOT_COMPONENT_DIRS = ['commands', 'agents', 'hooks'];
|
|
464
|
+
/** `.mcp.json` is an alternative MCP path the portable format forbids; `mcp.json` itself is the fixed location. */
|
|
465
|
+
const ROOT_COMPONENT_FILES = ['hooks.json', 'lsp.json', '.lsp.json', '.mcp.json'];
|
|
466
|
+
/**
|
|
467
|
+
* Whether the namespace contract allows reading the extension seats: the
|
|
468
|
+
* manifest must carry our namespace with a supported `schemaVersion`. A
|
|
469
|
+
* directory without manifest data stays closed until the suite declares the
|
|
470
|
+
* contract, so a name collision on the filesystem cannot silently activate
|
|
471
|
+
* extension reading.
|
|
472
|
+
*/
|
|
473
|
+
function namespaceReadable(manifest) {
|
|
474
|
+
return manifest.harness !== undefined;
|
|
475
|
+
}
|
|
476
|
+
/** Report portable-suite content this manager does not mount, so nothing fails silently. */
|
|
477
|
+
async function noteUnreadRootComponents(root, notes) {
|
|
478
|
+
for (const dir of ROOT_COMPONENT_DIRS) {
|
|
479
|
+
if (await isDirectory(join(root, dir)))
|
|
480
|
+
notes.push(`suite ${root}: "${dir}/" is outside the portable layout and the ${EXTENSION_NAMESPACE} namespace; not read for this dialect`);
|
|
481
|
+
}
|
|
482
|
+
for (const file of ROOT_COMPONENT_FILES) {
|
|
483
|
+
if (await isFile(join(root, file)))
|
|
484
|
+
notes.push(`suite ${root}: "${file}" is outside the portable layout; not read for this dialect`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/** Commands and agents from `<namespace>/commands/` and `<namespace>/agents/`. */
|
|
488
|
+
async function discoverNamespaceMarkdown(root, notes) {
|
|
489
|
+
const namespaceRoot = join(root, NAMESPACE_DIR);
|
|
490
|
+
if (!(await isDirectory(namespaceRoot)))
|
|
491
|
+
return { commands: [], agents: [] };
|
|
492
|
+
const errors = [];
|
|
493
|
+
const commands = await discoverMarkdownResources(namespaceRoot, 'commands', undefined, join(namespaceRoot, 'plugin.json'), errors);
|
|
494
|
+
const agents = await discoverMarkdownResources(namespaceRoot, 'agents', undefined, join(namespaceRoot, 'plugin.json'), errors);
|
|
495
|
+
notes.push(...errors.map(error => `${EXTENSION_NAMESPACE}: ${error}`));
|
|
496
|
+
return { commands, agents };
|
|
497
|
+
}
|
|
498
|
+
/** Hooks from `<namespace>/hooks/hooks.json`, normalized through the command-hook bridge. */
|
|
499
|
+
async function discoverNamespaceHooks(root, manifest, errors, notes) {
|
|
500
|
+
const file = join(root, NAMESPACE_HOOKS_FILE);
|
|
501
|
+
if (!(await isFile(file)))
|
|
502
|
+
return undefined;
|
|
503
|
+
let value;
|
|
504
|
+
try {
|
|
505
|
+
value = JSON.parse(await readFile(file, 'utf8'));
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
notes.push(`${NAMESPACE_HOOKS_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
const events = value?.hooks ?? value;
|
|
512
|
+
if (typeof events !== 'object' || events === null) {
|
|
513
|
+
notes.push(`${NAMESPACE_HOOKS_FILE}: must contain an event table`);
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
return normalizeHookDocuments(root, [{ file: NAMESPACE_HOOKS_FILE, settings: { hooks: events } }], errors);
|
|
517
|
+
}
|
|
518
|
+
/** LSP servers from `<namespace>/lsp.json`. */
|
|
519
|
+
async function discoverNamespaceLsp(root, manifest, errors, notes) {
|
|
520
|
+
const file = join(root, NAMESPACE_LSP_FILE);
|
|
521
|
+
if (!(await isFile(file)))
|
|
522
|
+
return undefined;
|
|
523
|
+
let value;
|
|
524
|
+
try {
|
|
525
|
+
value = JSON.parse(await readFile(file, 'utf8'));
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
notes.push(`${NAMESPACE_LSP_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
const servers = Object.create(null);
|
|
532
|
+
const document = value.lspServers ?? value;
|
|
533
|
+
Object.assign(servers, parseLspServers(document, notes));
|
|
534
|
+
return Object.keys(servers).length > 0 ? { servers } : undefined;
|
|
535
|
+
}
|
|
@@ -99,6 +99,53 @@ export function parseSkillFrontmatter(text, expectedName) {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
/** Frontmatter record for the panel's host-parity check, or the reason the YAML is unusable. */
|
|
103
|
+
function strictSkillRecord(text) {
|
|
104
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
105
|
+
if (frontmatter === undefined)
|
|
106
|
+
return 'missing YAML frontmatter';
|
|
107
|
+
let raw;
|
|
108
|
+
try {
|
|
109
|
+
raw = parseYaml(frontmatter);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return 'invalid YAML frontmatter';
|
|
113
|
+
}
|
|
114
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
115
|
+
return 'frontmatter is not an object';
|
|
116
|
+
return raw;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Rejection for a user skill document the harness's own `dsh-skill-filesystem`
|
|
120
|
+
* reader would drop from `~/.agents/skills`, or `undefined` when that reader
|
|
121
|
+
* accepts it.
|
|
122
|
+
*
|
|
123
|
+
* The two readers share the directory, so the panel publishes only documents
|
|
124
|
+
* both accept: a listed entry never becomes a skill that loads in a
|
|
125
|
+
* market-equipped session alone. The checks mirror the shipped reader rather
|
|
126
|
+
* than the lenient suite scan — strict YAML, no display-name normalization,
|
|
127
|
+
* and a `name` the reader compares only against its kebab grammar.
|
|
128
|
+
* @returns the drop reason, phrased as the reader's own diagnostic.
|
|
129
|
+
*/
|
|
130
|
+
export function skillEntryRejection(text) {
|
|
131
|
+
const record = strictSkillRecord(text);
|
|
132
|
+
if (typeof record === 'string')
|
|
133
|
+
return record;
|
|
134
|
+
const name = record['name'];
|
|
135
|
+
const description = record['description'];
|
|
136
|
+
if (typeof name !== 'string' || name === '' || typeof description !== 'string' || description.trim() === '') {
|
|
137
|
+
return 'frontmatter requires name and description';
|
|
138
|
+
}
|
|
139
|
+
if (!isSkillName(name))
|
|
140
|
+
return `invalid skill name "${name}"`;
|
|
141
|
+
const disableModel = parseBoolean(record['disable-model-invocation']);
|
|
142
|
+
if (disableModel === undefined && record['disable-model-invocation'] !== undefined)
|
|
143
|
+
return 'invalid disable-model-invocation value';
|
|
144
|
+
const userInvocable = parseBoolean(record['user-invocable']);
|
|
145
|
+
if (userInvocable === undefined && record['user-invocable'] !== undefined)
|
|
146
|
+
return 'invalid user-invocable value';
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
102
149
|
/** Lenient frontmatter recovery for prose fields with embedded `: `. */
|
|
103
150
|
function lenientFrontmatter(body) {
|
|
104
151
|
const record = {};
|
|
@@ -15,10 +15,15 @@ import { scanSource as scanSourceWithNotes } from './suite-scanner.js';
|
|
|
15
15
|
import { discoverNativeProjectSuites } from './native-project.js';
|
|
16
16
|
/** Discover suites from the selected configured or project checkouts. */
|
|
17
17
|
export async function discoverSourceList(sources, dimension, dimensionRoot) {
|
|
18
|
-
return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot)).suites;
|
|
18
|
+
return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot, true)).suites;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Discover suites plus per-source scan diagnostics for one dimension.
|
|
22
|
+
* @param scanProjectLayouts - whether the project's own native Agent directories
|
|
23
|
+
* join the scan. Required on purpose: the switch has one owner (the host
|
|
24
|
+
* settings document), so a default here would be a second copy of it.
|
|
25
|
+
*/
|
|
26
|
+
export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts) {
|
|
22
27
|
const checkoutRoot = sourcesDir(dimensionRoot);
|
|
23
28
|
const listed = new Set(sources.map(source => source.id));
|
|
24
29
|
const checkouts = sources.map(source => ({
|
|
@@ -4,7 +4,7 @@ import { relative } from 'node:path';
|
|
|
4
4
|
import { componentDocuments, componentPath, firstComponentFile, isRecord, isStringArray, isUnknownArray } from './component-files.js';
|
|
5
5
|
import { parseLspServers } from './lsp-spec.js';
|
|
6
6
|
import { normalizeHookDocuments } from './project-hooks.js';
|
|
7
|
-
import {
|
|
7
|
+
import { expandPluginPaths } from './plugin-variables.js';
|
|
8
8
|
export async function discoverSuiteLsp(root, manifest, errors) {
|
|
9
9
|
const declaration = manifest.components?.lspServers;
|
|
10
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']);
|
|
@@ -22,7 +22,7 @@ function quote(argument) {
|
|
|
22
22
|
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
23
23
|
}
|
|
24
24
|
function rootVariables(text, root) {
|
|
25
|
-
return text
|
|
25
|
+
return expandPluginPaths(text, { root });
|
|
26
26
|
}
|
|
27
27
|
const EVENT_ALIASES = {
|
|
28
28
|
sessionStart: 'SessionStart',
|