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.
Files changed (136) hide show
  1. package/README.md +117 -111
  2. package/README.zh.md +112 -98
  3. package/client/client.js +547 -540
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog-context.js +181 -0
  6. package/lib/application/catalog.js +217 -1110
  7. package/lib/application/details.js +6 -6
  8. package/lib/application/install-store.js +95 -0
  9. package/lib/application/lsp-service.js +97 -0
  10. package/lib/application/mcp-service.js +246 -0
  11. package/lib/application/panel-resources.js +29 -12
  12. package/lib/application/ports.js +21 -0
  13. package/lib/application/project-agent-roles.js +1 -1
  14. package/lib/application/snapshot-cache.js +160 -0
  15. package/lib/application/source-store.js +521 -0
  16. package/lib/catalog/archive.js +34 -29
  17. package/lib/catalog/component-files.js +14 -6
  18. package/lib/catalog/fs-probes.js +43 -0
  19. package/lib/catalog/manifests.js +53 -92
  20. package/lib/catalog/native-project.js +2 -2
  21. package/lib/catalog/paths.js +66 -55
  22. package/lib/catalog/project-config.js +2 -1
  23. package/lib/catalog/project-hooks.js +9 -1
  24. package/lib/catalog/scan-resolvers.js +29 -45
  25. package/lib/catalog/skills-parse.js +14 -10
  26. package/lib/catalog/source-catalog.js +3 -2
  27. package/lib/catalog/suite-components.js +5 -5
  28. package/lib/catalog/suite-scanner.js +3 -1
  29. package/lib/catalog/surfaces.js +5 -26
  30. package/lib/catalog/validate.js +7 -11
  31. package/lib/contracts/market.js +1 -0
  32. package/lib/index.js +91 -208
  33. package/lib/model/types.js +26 -3
  34. package/lib/routes.js +45 -15
  35. package/lib/runtime/agent-role-names.js +70 -0
  36. package/lib/runtime/agent-role-router.js +116 -102
  37. package/lib/runtime/commands-mounts.js +9 -6
  38. package/lib/runtime/deadline.js +28 -0
  39. package/lib/runtime/feedback-tool.js +94 -43
  40. package/lib/runtime/hooks-mounts.js +9 -11
  41. package/lib/runtime/host-locale.js +10 -21
  42. package/lib/runtime/lsp-direct-config.js +19 -19
  43. package/lib/runtime/lsp-mounts.js +159 -68
  44. package/lib/runtime/lsp-server-state.js +4 -4
  45. package/lib/runtime/lsp-status.js +1 -1
  46. package/lib/runtime/mcp-auth-record.js +42 -0
  47. package/lib/runtime/mcp-backend.js +6 -5
  48. package/lib/runtime/mcp-client/config.js +0 -1
  49. package/lib/runtime/mcp-client/connection.js +32 -37
  50. package/lib/runtime/mcp-client/host-contract.js +10 -0
  51. package/lib/runtime/mcp-client/oauth.js +5 -7
  52. package/lib/runtime/mcp-client/plugin-identity.js +28 -0
  53. package/lib/runtime/mcp-client/projection.js +202 -0
  54. package/lib/runtime/mcp-client/tools.js +4 -189
  55. package/lib/runtime/mcp-config.js +29 -10
  56. package/lib/runtime/mcp-direct-config.js +33 -16
  57. package/lib/runtime/mcp-mounts.js +35 -75
  58. package/lib/runtime/mcp-overrides.js +5 -5
  59. package/lib/runtime/mcp-redaction.js +3 -2
  60. package/lib/runtime/mcp-status.js +7 -8
  61. package/lib/runtime/mount-lifecycle.js +73 -0
  62. package/lib/runtime/profile-seam.js +321 -0
  63. package/lib/runtime/project-runtime.js +11 -8
  64. package/lib/runtime/reconcile-scheduler.js +99 -0
  65. package/lib/runtime/reconciler.js +4 -0
  66. package/lib/runtime/server-config.js +6 -4
  67. package/lib/runtime/settings-namespace.js +136 -0
  68. package/lib/runtime/skills-provider.js +5 -3
  69. package/lib/runtime/source-auto-update.js +70 -0
  70. package/lib/{model/state.js → runtime/state-store.js} +9 -11
  71. package/lib/runtime/storage-migration.js +35 -18
  72. package/lib/runtime/subagent-catalog.js +5 -4
  73. package/lib/runtime/user-commands.js +2 -2
  74. package/lib/runtime/user-panels.js +31 -21
  75. package/lib/runtime/user-store.js +12 -13
  76. package/lib/types/application/catalog-context.d.ts +118 -0
  77. package/lib/types/application/catalog.d.ts +99 -304
  78. package/lib/types/application/install-store.d.ts +21 -0
  79. package/lib/types/application/lsp-service.d.ts +36 -0
  80. package/lib/types/application/mcp-service.d.ts +71 -0
  81. package/lib/types/application/panel-resources.d.ts +8 -0
  82. package/lib/types/application/ports.d.ts +72 -0
  83. package/lib/types/application/queries.d.ts +13 -26
  84. package/lib/types/application/snapshot-cache.d.ts +51 -0
  85. package/lib/types/application/source-store.d.ts +100 -0
  86. package/lib/types/catalog/archive.d.ts +3 -11
  87. package/lib/types/catalog/component-files.d.ts +4 -0
  88. package/lib/types/catalog/fs-probes.d.ts +10 -0
  89. package/lib/types/catalog/manifests.d.ts +17 -16
  90. package/lib/types/catalog/native-project.d.ts +2 -2
  91. package/lib/types/catalog/paths.d.ts +47 -15
  92. package/lib/types/catalog/scan-pipeline.d.ts +3 -3
  93. package/lib/types/catalog/scan-resolvers.d.ts +3 -9
  94. package/lib/types/catalog/skills-parse.d.ts +0 -7
  95. package/lib/types/catalog/source-catalog.d.ts +3 -3
  96. package/lib/types/catalog/suite-scanner.d.ts +8 -6
  97. package/lib/types/catalog/validate.d.ts +0 -2
  98. package/lib/types/contracts/lsp-status.d.ts +35 -0
  99. package/lib/types/contracts/market.d.ts +3 -2
  100. package/lib/types/contracts/mcp-status.d.ts +4 -0
  101. package/lib/types/model/types.d.ts +29 -5
  102. package/lib/types/runtime/agent-role-names.d.ts +6 -0
  103. package/lib/types/runtime/agent-role-router.d.ts +54 -51
  104. package/lib/types/runtime/deadline.d.ts +18 -0
  105. package/lib/types/runtime/feedback-tool.d.ts +33 -11
  106. package/lib/types/runtime/hooks-mounts.d.ts +1 -1
  107. package/lib/types/runtime/host-locale.d.ts +1 -1
  108. package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
  109. package/lib/types/runtime/lsp-mounts.d.ts +62 -19
  110. package/lib/types/runtime/lsp-status.d.ts +2 -1
  111. package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
  112. package/lib/types/runtime/mcp-backend.d.ts +7 -4
  113. package/lib/types/runtime/mcp-client/config.d.ts +0 -2
  114. package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
  115. package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
  116. package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
  117. package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
  118. package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
  119. package/lib/types/runtime/mcp-config.d.ts +25 -4
  120. package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
  121. package/lib/types/runtime/mcp-mounts.d.ts +10 -15
  122. package/lib/types/runtime/mcp-overrides.d.ts +1 -1
  123. package/lib/types/runtime/mcp-status.d.ts +1 -1
  124. package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
  125. package/lib/types/runtime/profile-seam.d.ts +64 -0
  126. package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
  127. package/lib/types/runtime/reconciler.d.ts +2 -0
  128. package/lib/types/runtime/settings-namespace.d.ts +76 -0
  129. package/lib/types/runtime/source-auto-update.d.ts +33 -0
  130. package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
  131. package/lib/types/runtime/storage-migration.d.ts +5 -3
  132. package/lib/types/runtime/subagent-catalog.d.ts +2 -0
  133. package/lib/types/runtime/user-panels.d.ts +8 -11
  134. package/lib/types/runtime/user-store.d.ts +2 -4
  135. package/package.json +48 -21
  136. 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 ${String(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 { readdir, realpath, stat } from 'node:fs/promises';
21
+ import { realpath } from 'node:fs/promises';
22
22
  import { join, resolve } from 'node:path';
23
- import { isDirectory, sanitizeId } from './paths.js';
24
- import { componentDeclarations, detectManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
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 (dir !== checkout && !dir.startsWith(`${checkout}/`)) {
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 (realDir !== realCheckout && !realDir.startsWith(`${realCheckout}/`)) {
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 suites = await Promise.all(roots.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
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 suites = await Promise.all(found.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
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 Promise.all(collection.map(async (root) => root.dir === undefined ? remoteSuite(context.sourceId, context.dimension, root) : readSuite(root.dir, context.sourceId, context.dimension, root.hint, context.notes)));
269
- return { kind: 'resolved', suites: suites.filter((suite) => suite !== undefined) };
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', ...(Array.isArray(declared) ? declared : [declared])];
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
- export function isSkillName(name) {
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
- export function normalizeSkillName(name) {
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 match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
54
- if (match === null)
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(match[1]);
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(match[1]);
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 = typeof rawName === 'string' && !isSkillName(rawName) ? normalizeSkillName(rawName) : rawName;
75
- if (name === undefined || typeof name !== 'string')
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[1];
109
+ const [, key, value] = match;
110
+ if (key === undefined || value === undefined)
111
+ continue;
108
112
  if (record[key] === undefined)
109
- record[key] = match[2].trim();
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, isDirectory, sourcesDir } from './paths.js';
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 = true) {
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 (!Array.isArray(groups)) {
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) && !Array.isArray(rawGroup.hooks) && (typeof rawGroup.command === 'string' || typeof rawGroup.bash === 'string')
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) || !Array.isArray(group.hooks))
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 && (!Array.isArray(hook.args) || !hook.args.every(arg => typeof arg === 'string'))))
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, listMdFiles, discoverLspEntries, scanSource, canonicalGitUrl, resolveMarketplaceEntry, defaultScanFilters, MarketplaceStrategy, RootedStrategy, FlatCollectionsStrategy } from './scan-resolvers.js';
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) {
@@ -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 './paths.js';
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] : []), ...(Array.isArray(declared) ? declared : [declared])];
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
- if (result.config !== undefined) {
187
- Object.assign(servers, result.config.servers);
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
- }
@@ -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, isAbsolute, join, relative, resolve, sep } from 'node:path';
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 (candidateResolved !== rootResolved && !candidateResolved.startsWith(rootPrefix)) {
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
- if (stdioServer.cwd !== undefined && typeof stdioServer.cwd !== 'string') {
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' && stdioServer.cwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => stdioServer.cwd.startsWith(`\${${name}}`))) {
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 = stdioServer.cwd === '.' ? './' : stdioServer.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
- }
@@ -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`,