dsh-agent-plugins-market 0.6.2 → 0.7.0
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 +117 -111
- package/README.zh.md +112 -98
- package/client/client.js +547 -540
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +181 -0
- package/lib/application/catalog.js +217 -1110
- package/lib/application/details.js +6 -6
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +246 -0
- package/lib/application/panel-resources.js +29 -12
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +1 -1
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +34 -29
- package/lib/catalog/component-files.js +14 -6
- package/lib/catalog/fs-probes.js +43 -0
- package/lib/catalog/manifests.js +53 -92
- package/lib/catalog/native-project.js +2 -2
- package/lib/catalog/paths.js +66 -55
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +9 -1
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +14 -10
- package/lib/catalog/source-catalog.js +3 -2
- package/lib/catalog/suite-components.js +5 -5
- package/lib/catalog/suite-scanner.js +3 -1
- package/lib/catalog/surfaces.js +5 -26
- package/lib/catalog/validate.js +7 -11
- package/lib/contracts/market.js +1 -0
- package/lib/index.js +91 -208
- package/lib/model/types.js +26 -3
- package/lib/routes.js +45 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +116 -102
- package/lib/runtime/commands-mounts.js +9 -6
- package/lib/runtime/deadline.js +28 -0
- package/lib/runtime/feedback-tool.js +94 -43
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +10 -21
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +159 -68
- package/lib/runtime/lsp-server-state.js +4 -4
- package/lib/runtime/lsp-status.js +1 -1
- package/lib/runtime/mcp-auth-record.js +42 -0
- package/lib/runtime/mcp-backend.js +6 -5
- package/lib/runtime/mcp-client/config.js +0 -1
- package/lib/runtime/mcp-client/connection.js +32 -37
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- package/lib/runtime/mcp-client/oauth.js +5 -7
- package/lib/runtime/mcp-client/plugin-identity.js +28 -0
- package/lib/runtime/mcp-client/projection.js +202 -0
- package/lib/runtime/mcp-client/tools.js +4 -189
- package/lib/runtime/mcp-config.js +29 -10
- package/lib/runtime/mcp-direct-config.js +33 -16
- package/lib/runtime/mcp-mounts.js +35 -75
- package/lib/runtime/mcp-overrides.js +5 -5
- package/lib/runtime/mcp-redaction.js +3 -2
- package/lib/runtime/mcp-status.js +7 -8
- package/lib/runtime/mount-lifecycle.js +73 -0
- package/lib/runtime/profile-seam.js +321 -0
- package/lib/runtime/project-runtime.js +11 -8
- package/lib/runtime/reconcile-scheduler.js +99 -0
- package/lib/runtime/reconciler.js +4 -0
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +136 -0
- package/lib/runtime/skills-provider.js +5 -3
- package/lib/runtime/source-auto-update.js +70 -0
- package/lib/{model/state.js → runtime/state-store.js} +9 -11
- package/lib/runtime/storage-migration.js +35 -18
- package/lib/runtime/subagent-catalog.js +5 -4
- package/lib/runtime/user-commands.js +2 -2
- package/lib/runtime/user-panels.js +31 -21
- package/lib/runtime/user-store.js +12 -13
- package/lib/types/application/catalog-context.d.ts +118 -0
- package/lib/types/application/catalog.d.ts +99 -304
- package/lib/types/application/install-store.d.ts +21 -0
- package/lib/types/application/lsp-service.d.ts +36 -0
- package/lib/types/application/mcp-service.d.ts +71 -0
- package/lib/types/application/panel-resources.d.ts +8 -0
- package/lib/types/application/ports.d.ts +72 -0
- package/lib/types/application/queries.d.ts +13 -26
- package/lib/types/application/snapshot-cache.d.ts +51 -0
- package/lib/types/application/source-store.d.ts +100 -0
- package/lib/types/catalog/archive.d.ts +3 -11
- package/lib/types/catalog/component-files.d.ts +4 -0
- package/lib/types/catalog/fs-probes.d.ts +10 -0
- package/lib/types/catalog/manifests.d.ts +17 -16
- package/lib/types/catalog/native-project.d.ts +2 -2
- package/lib/types/catalog/paths.d.ts +47 -15
- package/lib/types/catalog/scan-pipeline.d.ts +3 -3
- package/lib/types/catalog/scan-resolvers.d.ts +3 -9
- package/lib/types/catalog/skills-parse.d.ts +0 -7
- package/lib/types/catalog/source-catalog.d.ts +3 -3
- package/lib/types/catalog/suite-scanner.d.ts +8 -6
- package/lib/types/catalog/validate.d.ts +0 -2
- package/lib/types/contracts/lsp-status.d.ts +35 -0
- package/lib/types/contracts/market.d.ts +3 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -0
- package/lib/types/model/types.d.ts +29 -5
- package/lib/types/runtime/agent-role-names.d.ts +6 -0
- package/lib/types/runtime/agent-role-router.d.ts +54 -51
- package/lib/types/runtime/deadline.d.ts +18 -0
- package/lib/types/runtime/feedback-tool.d.ts +33 -11
- package/lib/types/runtime/hooks-mounts.d.ts +1 -1
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +62 -19
- package/lib/types/runtime/lsp-status.d.ts +2 -1
- package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
- package/lib/types/runtime/mcp-backend.d.ts +7 -4
- package/lib/types/runtime/mcp-client/config.d.ts +0 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
- package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
- package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
- package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
- package/lib/types/runtime/mcp-config.d.ts +25 -4
- package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
- package/lib/types/runtime/mcp-mounts.d.ts +10 -15
- package/lib/types/runtime/mcp-overrides.d.ts +1 -1
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
- package/lib/types/runtime/profile-seam.d.ts +64 -0
- package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/source-auto-update.d.ts +33 -0
- package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
- package/lib/types/runtime/storage-migration.d.ts +5 -3
- package/lib/types/runtime/subagent-catalog.d.ts +2 -0
- package/lib/types/runtime/user-panels.d.ts +8 -11
- package/lib/types/runtime/user-store.d.ts +2 -4
- package/package.json +48 -21
- package/schemas/README.md +1 -1
|
@@ -3,6 +3,14 @@ const EVENTS = new Set(['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostT
|
|
|
3
3
|
function object(value) {
|
|
4
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Render a declared hook field inside a diagnostic. Declarations are
|
|
8
|
+
* untrusted JSON, so the value is whatever the file carried — including an
|
|
9
|
+
* object, which reads as `[object Object]` in the message.
|
|
10
|
+
*/
|
|
11
|
+
function declarationText(value) {
|
|
12
|
+
return String(value);
|
|
13
|
+
}
|
|
6
14
|
/** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
|
|
7
15
|
export async function discoverProjectHooks(projectRoot, files, errors, format = 'claude') {
|
|
8
16
|
const documents = [];
|
|
@@ -74,7 +82,7 @@ export function normalizeHookDocuments(projectRoot, documents, errors, format =
|
|
|
74
82
|
if (hook.enabled === false)
|
|
75
83
|
continue;
|
|
76
84
|
if (hook.type !== undefined && hook.type !== 'command') {
|
|
77
|
-
errors.push(`${file}: unsupported ${event} hook type ${
|
|
85
|
+
errors.push(`${file}: unsupported ${event} hook type ${declarationText(hook.type)}`);
|
|
78
86
|
continue;
|
|
79
87
|
}
|
|
80
88
|
if (typeof hook.command !== 'string' ||
|
|
@@ -18,18 +18,16 @@
|
|
|
18
18
|
* 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
|
|
19
19
|
* `<root>/<name>/SKILL.md` collections.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
21
|
+
import { realpath } from 'node:fs/promises';
|
|
22
22
|
import { join, resolve } from 'node:path';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { isWithin, sanitizeId } from './paths.js';
|
|
24
|
+
import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
|
|
25
|
+
import { componentDeclarations, hasSuiteManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
|
|
25
26
|
import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
|
|
26
|
-
import { discoverMarkdownResources } from './component-files.js';
|
|
27
|
+
import { discoverMarkdownResources, isUnknownArray } from './component-files.js';
|
|
27
28
|
import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
|
|
28
29
|
import { runScanChain } from './scan-pipeline.js';
|
|
29
|
-
export { repoName } from './manifests.js';
|
|
30
|
-
export { listMdFiles, discoverLspEntries } from './surfaces.js';
|
|
31
30
|
const CONTAINER_DIRS = ['plugins', 'external_plugins', 'skills'];
|
|
32
|
-
const DOT_DIRS = new Set(['.git', '.github', '.claude', '.cursor', '.kimi', '.plugin', '.sources', 'node_modules']);
|
|
33
31
|
/**
|
|
34
32
|
* Canonical `https://host/owner/repo` form of a git URL, for equality
|
|
35
33
|
* comparisons between a source's configured URL and an entry's remote.
|
|
@@ -75,7 +73,7 @@ export function entryRemoteUrl(entry) {
|
|
|
75
73
|
async function claimLocal(checkout, declaredPath) {
|
|
76
74
|
const dir = resolve(checkout, declaredPath);
|
|
77
75
|
// Containment guard: a relative entry must stay inside the checkout.
|
|
78
|
-
if (
|
|
76
|
+
if (!isWithin(checkout, dir)) {
|
|
79
77
|
// `rejected`: the handler positively identified a local path, but the
|
|
80
78
|
// path escapes the checkout. Its verdict is authoritative — later
|
|
81
79
|
// handlers must not reinterpret it as something else.
|
|
@@ -83,7 +81,7 @@ async function claimLocal(checkout, declaredPath) {
|
|
|
83
81
|
}
|
|
84
82
|
try {
|
|
85
83
|
const [realCheckout, realDir] = await Promise.all([realpath(checkout), realpath(dir)]);
|
|
86
|
-
if (
|
|
84
|
+
if (!isWithin(realCheckout, realDir)) {
|
|
87
85
|
return { kind: 'rejected', reason: `path "${declaredPath}" escapes the checkout through a symlink` };
|
|
88
86
|
}
|
|
89
87
|
}
|
|
@@ -168,8 +166,7 @@ export class MarketplaceStrategy {
|
|
|
168
166
|
context.notes.push(`marketplace ${marketplace.path}: no entry resolved (all ${marketplace.entries.length} dropped or remote)`);
|
|
169
167
|
continue;
|
|
170
168
|
}
|
|
171
|
-
const
|
|
172
|
-
const resolved = suites.filter((suite) => suite !== undefined);
|
|
169
|
+
const resolved = await readSuites(roots, context);
|
|
173
170
|
if (resolved.length === 0) {
|
|
174
171
|
context.notes.push(`marketplace ${marketplace.path}: every resolved entry failed to parse a manifest`);
|
|
175
172
|
continue;
|
|
@@ -243,8 +240,7 @@ export class RootedStrategy {
|
|
|
243
240
|
const found = await collectRoots(context.checkout, undefined, new Set());
|
|
244
241
|
if (found.length === 0)
|
|
245
242
|
return chain.next(context);
|
|
246
|
-
const
|
|
247
|
-
const resolved = suites.filter((suite) => suite !== undefined);
|
|
243
|
+
const resolved = await readSuites(found, context);
|
|
248
244
|
return resolved.length === 0 ? chain.next(context) : { kind: 'resolved', suites: resolved };
|
|
249
245
|
}
|
|
250
246
|
}
|
|
@@ -265,8 +261,8 @@ export class FlatCollectionsStrategy {
|
|
|
265
261
|
context.notes.push('checkout declares no recognizable plugin shape');
|
|
266
262
|
return { kind: 'resolved', suites: [] };
|
|
267
263
|
}
|
|
268
|
-
const suites = await
|
|
269
|
-
return { kind: 'resolved', suites
|
|
264
|
+
const suites = await readSuites(collection, context);
|
|
265
|
+
return { kind: 'resolved', suites };
|
|
270
266
|
}
|
|
271
267
|
}
|
|
272
268
|
/** The default chain, most specific first with a terminal fallback. */
|
|
@@ -276,11 +272,6 @@ export function defaultScanFilters() {
|
|
|
276
272
|
// ---------------------------------------------------------------------------
|
|
277
273
|
// Public entry points
|
|
278
274
|
// ---------------------------------------------------------------------------
|
|
279
|
-
/** Discover every suite under one source checkout. */
|
|
280
|
-
export async function discoverSuitesInSource(checkoutDir, sourceId, dimension, sourceUrl) {
|
|
281
|
-
const result = await scanSource(checkoutDir, sourceId, dimension, sourceUrl);
|
|
282
|
-
return result.suites;
|
|
283
|
-
}
|
|
284
275
|
/** Scan one checkout through the filter chain with full diagnostics. */
|
|
285
276
|
export async function scanSource(checkoutDir, sourceId, dimension, sourceUrl) {
|
|
286
277
|
return runScanChain(defaultScanFilters(), { checkout: checkoutDir, sourceId, dimension, ...(sourceUrl !== undefined ? { sourceUrl } : {}), notes: [] });
|
|
@@ -288,6 +279,16 @@ export async function scanSource(checkoutDir, sourceId, dimension, sourceUrl) {
|
|
|
288
279
|
// ---------------------------------------------------------------------------
|
|
289
280
|
// Shared root/manifest plumbing
|
|
290
281
|
// ---------------------------------------------------------------------------
|
|
282
|
+
/**
|
|
283
|
+
* Materialize resolved suite roots into suites: a remote root becomes a
|
|
284
|
+
* metadata-only suite, a local root that fails to parse drops out.
|
|
285
|
+
*/
|
|
286
|
+
async function readSuites(roots, context) {
|
|
287
|
+
const suites = await Promise.all(roots.map(root => root.dir === undefined
|
|
288
|
+
? Promise.resolve(remoteSuite(context.sourceId, context.dimension, root))
|
|
289
|
+
: readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
|
|
290
|
+
return suites.filter((suite) => suite !== undefined);
|
|
291
|
+
}
|
|
291
292
|
/**
|
|
292
293
|
* Collect nested plugin roots up to four levels deep. Sibling subtrees are
|
|
293
294
|
* traversed concurrently (readdir/stat are I/O-bound); results concat in
|
|
@@ -308,10 +309,6 @@ async function collectRoots(dir, hint, seen, depth = 0) {
|
|
|
308
309
|
const nested = await Promise.all(children.map(child => collectRoots(child, hint, seen, depth + 1)));
|
|
309
310
|
return nested.flat();
|
|
310
311
|
}
|
|
311
|
-
/** Whether a directory carries any known suite manifest. */
|
|
312
|
-
export async function hasSuiteManifest(dir) {
|
|
313
|
-
return (await detectManifest(dir)) !== undefined;
|
|
314
|
-
}
|
|
315
312
|
/** Whether a directory carries skill files in the flat or bundled shape. */
|
|
316
313
|
export async function hasSkillFiles(dir) {
|
|
317
314
|
if (await isFile(join(dir, 'SKILL.md')))
|
|
@@ -327,25 +324,20 @@ export async function hasSkillFiles(dir) {
|
|
|
327
324
|
}
|
|
328
325
|
return false;
|
|
329
326
|
}
|
|
330
|
-
async function listChildDirs(dir) {
|
|
331
|
-
let entries;
|
|
332
|
-
try {
|
|
333
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
return [];
|
|
337
|
-
}
|
|
338
|
-
return entries.filter(entry => entry.isDirectory() && !DOT_DIRS.has(entry.name) && !entry.name.startsWith('.')).map(entry => join(dir, entry.name));
|
|
339
|
-
}
|
|
340
327
|
/** Read one suite root into the normalized shape, or undefined when no manifest parses. */
|
|
341
328
|
export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
342
329
|
const errors = [];
|
|
330
|
+
const fallbacks = [];
|
|
343
331
|
const declaredManifest = await hasSuiteManifest(root);
|
|
344
|
-
let manifest = declaredManifest ? await readManifest(root, errors, hint) : await syntheticManifest(root);
|
|
332
|
+
let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks) : await syntheticManifest(root);
|
|
345
333
|
if (declaredManifest && (manifest === undefined || errors.length > 0)) {
|
|
346
|
-
notes.push(...errors, `suite ${root}: rejected declared manifest`);
|
|
334
|
+
notes.push(...errors, ...fallbacks, `suite ${root}: rejected declared manifest`);
|
|
347
335
|
return undefined;
|
|
348
336
|
}
|
|
337
|
+
// A lower-priority manifest carried the suite: keep the reason the
|
|
338
|
+
// higher-priority declaration was skipped visible without failing the suite.
|
|
339
|
+
if (fallbacks.length > 0)
|
|
340
|
+
notes.push(`suite ${root}: a higher-priority manifest was rejected`, ...fallbacks);
|
|
349
341
|
// A declaration-only suite (official CC lsp plugins ship just a README):
|
|
350
342
|
// the marketplace entry's inline lspServers are its manifest.
|
|
351
343
|
if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
|
|
@@ -364,7 +356,7 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
|
364
356
|
return undefined;
|
|
365
357
|
let declared = manifest.components?.skills;
|
|
366
358
|
if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
|
|
367
|
-
declared = ['skills', ...(
|
|
359
|
+
declared = ['skills', ...(isUnknownArray(declared) ? declared : [declared])];
|
|
368
360
|
}
|
|
369
361
|
const skills = await discoverSkills(root, errors, declared);
|
|
370
362
|
if (manifest.layout === 'skill-collection' && skills.length === 0) {
|
|
@@ -436,11 +428,3 @@ async function syntheticManifest(root) {
|
|
|
436
428
|
name
|
|
437
429
|
};
|
|
438
430
|
}
|
|
439
|
-
async function isFile(path) {
|
|
440
|
-
try {
|
|
441
|
-
return (await stat(path)).isFile();
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { parse as parseYaml } from 'yaml';
|
|
11
11
|
/** Kebab-case skill names only, matching the shipped provider's rule. */
|
|
12
|
-
|
|
12
|
+
function isSkillName(name) {
|
|
13
13
|
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Normalize a display-style skill name into kebab-case (e.g. "Presentations"
|
|
17
17
|
* → "presentations"), or `undefined` when nothing usable remains.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
function normalizeSkillName(name) {
|
|
20
20
|
const normalized = name
|
|
21
21
|
.toLowerCase()
|
|
22
22
|
.replace(/[^a-z0-9]+/g, '-')
|
|
@@ -50,29 +50,31 @@ function parseBoolean(value) {
|
|
|
50
50
|
* skill must be dropped from discovery.
|
|
51
51
|
*/
|
|
52
52
|
export function parseSkillFrontmatter(text, expectedName) {
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
53
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
54
|
+
if (frontmatter === undefined)
|
|
55
55
|
return 'missing YAML frontmatter';
|
|
56
56
|
let raw;
|
|
57
57
|
try {
|
|
58
|
-
raw = parseYaml(
|
|
58
|
+
raw = parseYaml(frontmatter);
|
|
59
59
|
}
|
|
60
60
|
catch {
|
|
61
61
|
// Claude Code-authored frontmatter sometimes carries unquoted `: `
|
|
62
62
|
// sequences in prose fields, which strict YAML rejects. A lenient
|
|
63
63
|
// line-based fallback recovers the standard fields first-occurrence
|
|
64
64
|
// wins; values that still fail the field checks below drop the skill.
|
|
65
|
-
raw = lenientFrontmatter(
|
|
65
|
+
raw = lenientFrontmatter(frontmatter);
|
|
66
66
|
}
|
|
67
67
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
68
68
|
return 'frontmatter is not an object';
|
|
69
69
|
const record = raw;
|
|
70
70
|
const rawName = record['name'];
|
|
71
71
|
const description = record['description'];
|
|
72
|
+
if (typeof rawName !== 'string')
|
|
73
|
+
return 'frontmatter name is missing or not kebab-case';
|
|
72
74
|
// Codex plugins ship display names in `name` (e.g. "Presentations"); the
|
|
73
75
|
// skill identity is its kebab form, so normalize instead of dropping.
|
|
74
|
-
const name =
|
|
75
|
-
if (name === undefined
|
|
76
|
+
const name = isSkillName(rawName) ? rawName : normalizeSkillName(rawName);
|
|
77
|
+
if (name === undefined)
|
|
76
78
|
return 'frontmatter name is missing or not kebab-case';
|
|
77
79
|
if (expectedName !== undefined && name !== expectedName)
|
|
78
80
|
return `frontmatter name "${rawName}" does not match skill directory "${expectedName}"`;
|
|
@@ -104,9 +106,11 @@ function lenientFrontmatter(body) {
|
|
|
104
106
|
const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
|
|
105
107
|
if (match === null)
|
|
106
108
|
continue;
|
|
107
|
-
const key = match
|
|
109
|
+
const [, key, value] = match;
|
|
110
|
+
if (key === undefined || value === undefined)
|
|
111
|
+
continue;
|
|
108
112
|
if (record[key] === undefined)
|
|
109
|
-
record[key] =
|
|
113
|
+
record[key] = value.trim();
|
|
110
114
|
}
|
|
111
115
|
return record;
|
|
112
116
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readdir } from 'node:fs/promises';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
|
-
import { expandHome,
|
|
12
|
+
import { expandHome, sourcesDir } from './paths.js';
|
|
13
|
+
import { isDirectory } from './fs-probes.js';
|
|
13
14
|
import { scanSource as scanSourceWithNotes } from './suite-scanner.js';
|
|
14
15
|
import { discoverNativeProjectSuites } from './native-project.js';
|
|
15
16
|
/** Discover suites from the selected configured or project checkouts. */
|
|
@@ -17,7 +18,7 @@ export async function discoverSourceList(sources, dimension, dimensionRoot) {
|
|
|
17
18
|
return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot)).suites;
|
|
18
19
|
}
|
|
19
20
|
/** Discover suites plus per-source scan diagnostics for one dimension. */
|
|
20
|
-
export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts =
|
|
21
|
+
export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts = false) {
|
|
21
22
|
const checkoutRoot = sourcesDir(dimensionRoot);
|
|
22
23
|
const listed = new Set(sources.map(source => source.id));
|
|
23
24
|
const checkouts = sources.map(source => ({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Resolve all plugin component declarations into the same normalized resources used by runtime consumers. */
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { relative } from 'node:path';
|
|
4
|
-
import { componentDocuments, componentPath, firstComponentFile, isRecord } from './component-files.js';
|
|
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
7
|
import { PLUGIN_ROOT_VARIABLES } from '../model/layouts.js';
|
|
@@ -68,18 +68,18 @@ export async function discoverSuiteHooks(root, manifest, errors) {
|
|
|
68
68
|
const output = {};
|
|
69
69
|
for (const [event, groups] of Object.entries(events)) {
|
|
70
70
|
const canonicalEvent = EVENT_ALIASES[event] ?? event;
|
|
71
|
-
if (!
|
|
71
|
+
if (!isUnknownArray(groups)) {
|
|
72
72
|
output[canonicalEvent] = groups;
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
output[canonicalEvent] = groups.map(rawGroup => {
|
|
76
|
-
const group = isRecord(rawGroup) && !
|
|
76
|
+
const group = isRecord(rawGroup) && !isUnknownArray(rawGroup.hooks) && (typeof rawGroup.command === 'string' || typeof rawGroup.bash === 'string')
|
|
77
77
|
? {
|
|
78
78
|
matcher: rawGroup.matcher,
|
|
79
79
|
hooks: [{ ...rawGroup, type: rawGroup.type ?? 'command', command: rawGroup.command ?? rawGroup.bash, timeout: rawGroup.timeout ?? rawGroup.timeoutSec }]
|
|
80
80
|
}
|
|
81
81
|
: rawGroup;
|
|
82
|
-
if (!isRecord(group) || !
|
|
82
|
+
if (!isRecord(group) || !isUnknownArray(group.hooks))
|
|
83
83
|
return group;
|
|
84
84
|
return {
|
|
85
85
|
...group,
|
|
@@ -88,7 +88,7 @@ export async function discoverSuiteHooks(root, manifest, errors) {
|
|
|
88
88
|
return hook;
|
|
89
89
|
if (hook.type !== 'process')
|
|
90
90
|
return typeof hook.command === 'string' ? { ...hook, command: rootVariables(hook.command, root) } : hook;
|
|
91
|
-
if (typeof hook.command !== 'string' || (hook.args !== undefined &&
|
|
91
|
+
if (typeof hook.command !== 'string' || (hook.args !== undefined && !isStringArray(hook.args)))
|
|
92
92
|
return { ...hook, type: 'command', command: undefined };
|
|
93
93
|
const argv = [hook.command, ...(hook.args ?? [])].map(arg => quote(rootVariables(arg, root)));
|
|
94
94
|
return { ...hook, type: 'command', command: argv.join(' '), ...(typeof hook.timeoutMs === 'number' ? { timeout: hook.timeoutMs / 1000 } : {}) };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { scanSource } from './scan-resolvers.js';
|
|
2
|
-
export { repoName
|
|
2
|
+
export { repoName } from './manifests.js';
|
|
3
|
+
export { listMdFiles, discoverLspEntries } from './surfaces.js';
|
|
4
|
+
export { scanSource, canonicalGitUrl, resolveMarketplaceEntry, defaultScanFilters, MarketplaceStrategy, RootedStrategy, FlatCollectionsStrategy } from './scan-resolvers.js';
|
|
3
5
|
export { runScanChain } from './scan-pipeline.js';
|
|
4
6
|
/** Discover every suite under one source checkout. */
|
|
5
7
|
export async function discoverSuitesInSource(checkoutDir, sourceId, dimension, sourceUrl) {
|
package/lib/catalog/surfaces.js
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { parseSkillFrontmatter } from './skills-parse.js';
|
|
15
|
-
import { isDirectory } from './
|
|
15
|
+
import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
|
|
16
16
|
import { validateMcpJson } from './validate.js';
|
|
17
17
|
import { readManifest } from './manifests.js';
|
|
18
|
-
import { componentDocuments, componentPath, firstComponentFile } from './component-files.js';
|
|
19
|
-
const DOT_DIRS = new Set(['.git', '.github', '.claude', '.cursor', '.kimi', '.plugin', '.sources', 'node_modules']);
|
|
18
|
+
import { componentDocuments, componentPath, firstComponentFile, isUnknownArray } from './component-files.js';
|
|
20
19
|
/**
|
|
21
20
|
* Resolve a manifest-declared skills path into absolute directories (string or
|
|
22
21
|
* array form). Containment is checked on the *realpath* of both sides: the
|
|
@@ -168,7 +167,7 @@ export async function discoverMcp(root, errors, manifest) {
|
|
|
168
167
|
? fallback === undefined
|
|
169
168
|
? []
|
|
170
169
|
: [fallback]
|
|
171
|
-
: [...(additive && fallback !== undefined ? [fallback] : []), ...(
|
|
170
|
+
: [...(additive && fallback !== undefined ? [fallback] : []), ...(isUnknownArray(declared) ? declared : [declared])];
|
|
172
171
|
if (values.length === 0)
|
|
173
172
|
return undefined;
|
|
174
173
|
const documents = await componentDocuments(root, values, errors);
|
|
@@ -183,10 +182,8 @@ export async function discoverMcp(root, errors, manifest) {
|
|
|
183
182
|
errors.push(...result.errors);
|
|
184
183
|
if (result.config === undefined)
|
|
185
184
|
return undefined;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
schema = result.config.schema;
|
|
189
|
-
}
|
|
185
|
+
Object.assign(servers, result.config.servers);
|
|
186
|
+
schema = result.config.schema;
|
|
190
187
|
}
|
|
191
188
|
return { schema, servers };
|
|
192
189
|
}
|
|
@@ -274,21 +271,3 @@ export async function listMdFiles(dir) {
|
|
|
274
271
|
}
|
|
275
272
|
return entries.filter(name => name.endsWith('.md')).sort();
|
|
276
273
|
}
|
|
277
|
-
async function listChildDirs(dir) {
|
|
278
|
-
let entries;
|
|
279
|
-
try {
|
|
280
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
return entries.filter(entry => entry.isDirectory() && !DOT_DIRS.has(entry.name) && !entry.name.startsWith('.')).map(entry => join(dir, entry.name));
|
|
286
|
-
}
|
|
287
|
-
async function isFile(path) {
|
|
288
|
-
try {
|
|
289
|
-
return (await stat(path)).isFile();
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
}
|
package/lib/catalog/validate.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* the `${PLUGIN_ROOT}` / `${PLUGIN_DATA}` expansion the spec makes mandatory.
|
|
10
10
|
*/
|
|
11
11
|
import { readFile, realpath } from 'node:fs/promises';
|
|
12
|
-
import { dirname,
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
// The 2020-12 dist build's d.ts resolves to a CJS namespace under NodeNext;
|
|
15
15
|
// the runtime default export is the class itself (module.exports = Ajv2020).
|
|
16
16
|
import Ajv2020Default from 'ajv/dist/2020.js';
|
|
17
17
|
import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../model/layouts.js';
|
|
18
|
+
import { isWithin } from './paths.js';
|
|
18
19
|
const SCHEMAS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas', '1.0.0');
|
|
19
20
|
const Ajv2020Class = Ajv2020Default;
|
|
20
21
|
export const PLUGIN_SCHEMA_ID = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json';
|
|
@@ -63,7 +64,6 @@ export async function pathContainmentError(pluginRoot, value) {
|
|
|
63
64
|
catch {
|
|
64
65
|
rootResolved = resolve(pluginRoot);
|
|
65
66
|
}
|
|
66
|
-
const rootPrefix = rootResolved.endsWith(sep) ? rootResolved : `${rootResolved}${sep}`;
|
|
67
67
|
let candidateResolved;
|
|
68
68
|
try {
|
|
69
69
|
candidateResolved = await realpath(candidate);
|
|
@@ -71,7 +71,7 @@ export async function pathContainmentError(pluginRoot, value) {
|
|
|
71
71
|
catch {
|
|
72
72
|
candidateResolved = candidate;
|
|
73
73
|
}
|
|
74
|
-
if (
|
|
74
|
+
if (!isWithin(rootResolved, candidateResolved)) {
|
|
75
75
|
return `path "${value}" resolves outside the plugin root`;
|
|
76
76
|
}
|
|
77
77
|
return undefined;
|
|
@@ -158,12 +158,13 @@ export async function validateMcpJson(pluginRoot, raw, options) {
|
|
|
158
158
|
problems.push(reason);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
const declaredCwd = stdioServer.cwd;
|
|
162
|
+
if (declaredCwd !== undefined && typeof declaredCwd !== 'string') {
|
|
162
163
|
problems.push('cwd must be a string');
|
|
163
164
|
}
|
|
164
|
-
else if (options?.pathMode !== 'project' &&
|
|
165
|
+
else if (options?.pathMode !== 'project' && declaredCwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => declaredCwd.startsWith(`\${${name}}`))) {
|
|
165
166
|
// `.` is the Codex dialect spelling for the plugin root.
|
|
166
|
-
const cwd =
|
|
167
|
+
const cwd = declaredCwd === '.' ? './' : declaredCwd;
|
|
167
168
|
const reason = await pathContainmentError(pluginRoot, cwd);
|
|
168
169
|
if (reason !== undefined)
|
|
169
170
|
problems.push(reason);
|
|
@@ -197,8 +198,3 @@ export function resolveCwd(value, pluginRoot, pluginData) {
|
|
|
197
198
|
return resolve(pluginRoot, value.slice('${PLUGIN_ROOT}'.length).replace(/^\/+/, ''));
|
|
198
199
|
return resolve(pluginRoot, value.replace(/^\.\//, ''));
|
|
199
200
|
}
|
|
200
|
-
/** Whether one absolute path equals or lies under another absolute path. */
|
|
201
|
-
export function isWithin(root, candidate) {
|
|
202
|
-
const relativePath = relative(root, candidate);
|
|
203
|
-
return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
|
|
204
|
-
}
|
package/lib/contracts/market.js
CHANGED
|
@@ -9,6 +9,7 @@ export const MARKET_ROUTES = {
|
|
|
9
9
|
lspStatus: `${MARKET_API_PREFIX}lsp-status`,
|
|
10
10
|
lspServers: `${MARKET_API_PREFIX}lsp-servers`,
|
|
11
11
|
addLspServer: `${MARKET_API_PREFIX}lsp-servers/add`,
|
|
12
|
+
migrateLspSeam: `${MARKET_API_PREFIX}lsp-servers/migrate-seam`,
|
|
12
13
|
serverConfig: `${MARKET_API_PREFIX}server-config`,
|
|
13
14
|
saveServerConfig: `${MARKET_API_PREFIX}server-config/save`,
|
|
14
15
|
progress: `${MARKET_API_PREFIX}progress`,
|