dsh-agent-plugins-market 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -169
- package/README.zh.md +136 -170
- package/client/client.js +8192 -691
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +236 -47
- package/lib/application/details.js +19 -10
- package/lib/application/panel-resources.js +95 -0
- package/lib/application/project-agent-roles.js +39 -0
- package/lib/catalog/codex-mcp.js +114 -0
- package/lib/catalog/component-files.js +151 -0
- package/lib/catalog/manifests.js +105 -44
- package/lib/catalog/native-project.js +44 -23
- package/lib/catalog/paths.js +6 -6
- package/lib/catalog/project-config.js +96 -0
- package/lib/catalog/project-hooks.js +108 -0
- package/lib/catalog/scan-pipeline.js +6 -1
- package/lib/catalog/scan-resolvers.js +70 -26
- package/lib/catalog/source-catalog.js +16 -3
- package/lib/catalog/suite-components.js +125 -0
- package/lib/catalog/surfaces.js +60 -46
- package/lib/catalog/validate.js +16 -11
- package/lib/contracts/market.js +16 -1
- package/lib/index.js +187 -35
- package/lib/model/layouts.js +49 -0
- package/lib/routes.js +106 -6
- package/lib/runtime/agent-role-router.js +226 -0
- package/lib/runtime/commands-mounts.js +27 -67
- package/lib/runtime/feedback-tool.js +181 -0
- package/lib/runtime/hooks-mounts.js +66 -13
- package/lib/runtime/host-locale.js +23 -7
- package/lib/runtime/legacy-root-migration.js +12 -0
- package/lib/runtime/lsp-direct-config.js +6 -3
- package/lib/runtime/lsp-mounts.js +33 -19
- package/lib/runtime/lsp-server-state.js +19 -0
- package/lib/runtime/lsp-status.js +3 -2
- package/lib/runtime/mcp-backend.js +6 -2
- package/lib/runtime/mcp-client/config.js +6 -0
- package/lib/runtime/mcp-client/connection.js +8 -2
- package/lib/runtime/mcp-client/tools.js +6 -3
- package/lib/runtime/mcp-config.js +16 -7
- package/lib/runtime/mcp-direct-config.js +56 -0
- package/lib/runtime/mcp-mounts.js +12 -4
- package/lib/runtime/mcp-overrides.js +11 -5
- package/lib/runtime/mcp-status.js +8 -4
- package/lib/runtime/model-catalog.js +48 -0
- package/lib/runtime/project-runtime.js +136 -0
- package/lib/runtime/reconciler.js +38 -28
- package/lib/runtime/server-config.js +75 -0
- package/lib/runtime/skills-provider.js +31 -77
- package/lib/runtime/storage-migration.js +169 -0
- package/lib/runtime/subagent-catalog.js +129 -0
- package/lib/runtime/user-commands.js +86 -0
- package/lib/runtime/user-panels.js +176 -0
- package/lib/runtime/user-store.js +142 -0
- package/lib/types/application/catalog.d.ts +39 -9
- package/lib/types/application/panel-resources.d.ts +13 -0
- package/lib/types/application/project-agent-roles.d.ts +3 -0
- package/lib/types/application/queries.d.ts +17 -1
- package/lib/types/catalog/codex-mcp.d.ts +4 -0
- package/lib/types/catalog/component-files.d.ts +17 -0
- package/lib/types/catalog/manifests.d.ts +14 -9
- package/lib/types/catalog/native-project.d.ts +2 -5
- package/lib/types/catalog/paths.d.ts +4 -4
- package/lib/types/catalog/project-config.d.ts +6 -0
- package/lib/types/catalog/project-hooks.d.ts +10 -0
- package/lib/types/catalog/scan-pipeline.d.ts +2 -0
- package/lib/types/catalog/scan-resolvers.d.ts +4 -3
- package/lib/types/catalog/source-catalog.d.ts +1 -1
- package/lib/types/catalog/suite-components.d.ts +5 -0
- package/lib/types/catalog/surfaces.d.ts +2 -2
- package/lib/types/catalog/validate.d.ts +2 -0
- package/lib/types/contracts/market.d.ts +60 -0
- package/lib/types/contracts/mcp-status.d.ts +4 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/model/layouts.d.ts +102 -0
- package/lib/types/model/types.d.ts +51 -6
- package/lib/types/routes.d.ts +6 -1
- package/lib/types/runtime/agent-role-router.d.ts +95 -0
- package/lib/types/runtime/commands-mounts.d.ts +16 -7
- package/lib/types/runtime/feedback-tool.d.ts +49 -0
- package/lib/types/runtime/hooks-mounts.d.ts +4 -0
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/legacy-root-migration.d.ts +2 -0
- package/lib/types/runtime/lsp-mounts.d.ts +4 -0
- package/lib/types/runtime/lsp-server-state.d.ts +3 -0
- package/lib/types/runtime/lsp-status.d.ts +1 -0
- package/lib/types/runtime/mcp-backend.d.ts +9 -1
- package/lib/types/runtime/mcp-client/config.d.ts +4 -3
- package/lib/types/runtime/mcp-client/tools.d.ts +3 -0
- package/lib/types/runtime/mcp-direct-config.d.ts +7 -0
- package/lib/types/runtime/mcp-mounts.d.ts +2 -1
- package/lib/types/runtime/mcp-overrides.d.ts +2 -0
- package/lib/types/runtime/model-catalog.d.ts +7 -0
- package/lib/types/runtime/project-runtime.d.ts +32 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/server-config.d.ts +10 -0
- package/lib/types/runtime/skills-provider.d.ts +1 -5
- package/lib/types/runtime/storage-migration.d.ts +15 -0
- package/lib/types/runtime/subagent-catalog.d.ts +52 -0
- package/lib/types/runtime/user-commands.d.ts +24 -0
- package/lib/types/runtime/user-panels.d.ts +91 -0
- package/lib/types/runtime/user-store.d.ts +51 -0
- package/package.json +9 -2
- package/schemas/README.md +69 -7
- package/schemas/agent-plugins/spec.md +48 -0
- package/schemas/claude-code/marketplace.schema.json +143 -0
- package/schemas/claude-code/plugin.schema.json +105 -0
- package/schemas/claude-code/spec.md +108 -0
- package/schemas/codex/marketplace.schema.json +78 -0
- package/schemas/codex/plugin.schema.json +77 -0
- package/schemas/codex/spec.md +91 -0
- package/schemas/cursor/marketplace.schema.json +82 -0
- package/schemas/cursor/plugin.schema.json +58 -0
- package/schemas/cursor/spec.md +92 -0
- package/schemas/github-copilot/marketplace.schema.json +107 -0
- package/schemas/github-copilot/plugin.schema.json +89 -0
- package/schemas/github-copilot/spec.md +105 -0
- package/schemas/kimi/marketplace.schema.json +38 -0
- package/schemas/kimi/plugin.schema.json +84 -0
- package/schemas/kimi/spec.md +94 -0
- package/schemas/qoder/marketplace.schema.json +115 -0
- package/schemas/qoder/plugin.schema.json +127 -0
- package/schemas/qoder/spec.md +111 -0
- package/schemas/skill-collection/spec.md +29 -0
- package/schemas/universal/marketplace.schema.json +98 -0
- package/schemas/universal/plugin.schema.json +49 -0
- package/schemas/universal/spec.md +53 -0
- package/schemas/zcode/marketplace.schema.json +161 -0
- package/schemas/zcode/plugin.schema.json +89 -0
- package/schemas/zcode/spec.md +102 -0
- package/lib/catalog/legacy-root-migration.js +0 -51
- package/lib/types/catalog/legacy-root-migration.d.ts +0 -14
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Read native project configuration without treating unrelated settings as server definitions. */
|
|
2
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { validateMcpJson } from './validate.js';
|
|
5
|
+
import { parse as parseToml } from 'smol-toml';
|
|
6
|
+
import { normalizeCodexMcp } from './codex-mcp.js';
|
|
7
|
+
/** Later files override earlier keys; a malformed layer invalidates the whole layout's table. */
|
|
8
|
+
export async function discoverProjectMcp(projectRoot, files, errors, format = 'mcpServers') {
|
|
9
|
+
const servers = Object.create(null);
|
|
10
|
+
let found = false;
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
const raw = await readProjectDocument(projectRoot, file, errors, format === 'codex' ? 'TOML' : 'JSON');
|
|
13
|
+
if (raw === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
if (format === 'codex') {
|
|
18
|
+
if (raw.mcp_servers === undefined)
|
|
19
|
+
continue;
|
|
20
|
+
if (typeof raw.mcp_servers !== 'object' || raw.mcp_servers === null || Array.isArray(raw.mcp_servers)) {
|
|
21
|
+
errors.push(`${file}: mcp_servers must be a table`);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
found = true;
|
|
25
|
+
Object.assign(servers, normalizeCodexMcp(raw.mcp_servers, errors));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const mcp = raw.mcp;
|
|
29
|
+
if (format === 'zcode' && mcp === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
if (format === 'zcode' && (typeof mcp !== 'object' || mcp === null || Array.isArray(mcp))) {
|
|
32
|
+
errors.push(`${file}: mcp must be an object`);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (format === 'zcode' && mcp.enabled === false)
|
|
36
|
+
return undefined;
|
|
37
|
+
const table = format === 'zcode' ? mcp.servers : raw.mcpServers;
|
|
38
|
+
if (format === 'zcode' && table === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
if (table === undefined && /settings(?:\.local)?\.json$/.test(file))
|
|
41
|
+
continue;
|
|
42
|
+
if (typeof table !== 'object' || table === null || Array.isArray(table)) {
|
|
43
|
+
errors.push(`${file}: mcpServers must be an object`);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
found = true;
|
|
47
|
+
Object.assign(servers, table);
|
|
48
|
+
}
|
|
49
|
+
if (format === 'zcode' && Object.keys(servers).length === 0)
|
|
50
|
+
return discoverProjectMcp(projectRoot, ['.agents/mcp.json'], errors);
|
|
51
|
+
if (!found)
|
|
52
|
+
return undefined;
|
|
53
|
+
for (const [key, server] of Object.entries(servers)) {
|
|
54
|
+
if (typeof server === 'object' && server !== null && server.enabled === false)
|
|
55
|
+
delete servers[key];
|
|
56
|
+
}
|
|
57
|
+
const result = await validateMcpJson(projectRoot, { mcpServers: servers }, { strict: false, pathMode: 'project' });
|
|
58
|
+
errors.push(...result.errors.map(error => `project MCP: ${error}`));
|
|
59
|
+
return result.config === undefined ? undefined : { ...result.config, root: projectRoot };
|
|
60
|
+
}
|
|
61
|
+
/** Missing is undefined; malformed/unreadable is null and carries a diagnostic. */
|
|
62
|
+
export async function readProjectDocument(projectRoot, file, errors, format = 'JSON') {
|
|
63
|
+
const path = join(projectRoot, file);
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
const [base, target] = await Promise.all([realpath(projectRoot), realpath(path)]);
|
|
67
|
+
if (!target.startsWith(`${base}/`)) {
|
|
68
|
+
errors.push(`${file}: project configuration escapes the project root`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
text = await readFile(path, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error.code === 'ENOENT')
|
|
75
|
+
return undefined;
|
|
76
|
+
errors.push(`${file}: project configuration is unreadable`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = format === 'TOML' ? parseToml(text) : JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
errors.push(`${file}: project configuration is invalid ${format}`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
88
|
+
errors.push(`${file}: project configuration must be an object`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const record = raw;
|
|
92
|
+
if (record.lspServers !== undefined || record.lsp_servers !== undefined) {
|
|
93
|
+
errors.push(`${file}: project LSP declarations are not mounted; the host LSP registry does not isolate projects`);
|
|
94
|
+
}
|
|
95
|
+
return record;
|
|
96
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readProjectDocument } from './project-config.js';
|
|
2
|
+
const EVENTS = new Set(['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'SubagentStart', 'SubagentStop']);
|
|
3
|
+
function object(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
|
|
7
|
+
export async function discoverProjectHooks(projectRoot, files, errors, format = 'claude') {
|
|
8
|
+
const documents = [];
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
const settings = await readProjectDocument(projectRoot, file, errors);
|
|
11
|
+
if (settings === null)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (settings !== undefined)
|
|
14
|
+
documents.push({ file, settings });
|
|
15
|
+
}
|
|
16
|
+
return normalizeHookDocuments(projectRoot, documents, errors, format);
|
|
17
|
+
}
|
|
18
|
+
/** Pure validation shared by native settings and plugin file/inline declarations. */
|
|
19
|
+
export function normalizeHookDocuments(projectRoot, documents, errors, format = 'claude') {
|
|
20
|
+
const events = Object.create(null);
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
let disabled = false;
|
|
23
|
+
for (const { file, settings } of documents) {
|
|
24
|
+
if (settings?.disableAllHooks !== undefined) {
|
|
25
|
+
if (typeof settings.disableAllHooks !== 'boolean') {
|
|
26
|
+
errors.push(`${file}: disableAllHooks must be a boolean`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
disabled = settings.disableAllHooks;
|
|
30
|
+
}
|
|
31
|
+
if (settings?.hooks === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
if (!object(settings.hooks)) {
|
|
34
|
+
errors.push(`${file}: hooks must be an event table`);
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (format === 'zcode' && settings.hooks.enabled !== true)
|
|
38
|
+
continue;
|
|
39
|
+
const eventTable = format === 'zcode' ? settings.hooks.events : settings.hooks;
|
|
40
|
+
if (!object(eventTable)) {
|
|
41
|
+
errors.push(`${file}: hooks.events must be an event table`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
for (const [event, groups] of Object.entries(eventTable)) {
|
|
45
|
+
if (!EVENTS.has(event)) {
|
|
46
|
+
errors.push(`${file}: unsupported project hook event ${event}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(groups)) {
|
|
50
|
+
errors.push(`${file}: ${event} hook groups must be an array`);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
for (const group of groups) {
|
|
54
|
+
if (!object(group) || !Array.isArray(group.hooks) || (group.matcher !== undefined && typeof group.matcher !== 'string')) {
|
|
55
|
+
errors.push(`${file}: invalid ${event} hook group`);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const matcher = event === 'Stop' || event === 'UserPromptSubmit' ? undefined : group.matcher;
|
|
59
|
+
if (matcher !== undefined && matcher !== '*') {
|
|
60
|
+
try {
|
|
61
|
+
new RegExp(matcher);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
errors.push(`${file}: invalid ${event} hook matcher`);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const hooks = [];
|
|
69
|
+
for (const hook of group.hooks) {
|
|
70
|
+
if (!object(hook)) {
|
|
71
|
+
errors.push(`${file}: invalid ${event} hook`);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (hook.enabled === false)
|
|
75
|
+
continue;
|
|
76
|
+
if (hook.type !== undefined && hook.type !== 'command') {
|
|
77
|
+
errors.push(`${file}: unsupported ${event} hook type ${String(hook.type)}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof hook.command !== 'string' ||
|
|
81
|
+
hook.command.trim() === '' ||
|
|
82
|
+
(hook.timeout !== undefined && (typeof hook.timeout !== 'number' || !Number.isFinite(hook.timeout) || hook.timeout <= 0))) {
|
|
83
|
+
errors.push(`${file}: invalid ${event} command hook`);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (hook.async === true || hook.asyncRewake === true) {
|
|
87
|
+
errors.push(`${file}: asynchronous project hooks are unsupported`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const key = JSON.stringify([event, matcher, hook.command]);
|
|
91
|
+
if (seen.has(key))
|
|
92
|
+
continue;
|
|
93
|
+
seen.add(key);
|
|
94
|
+
const timeoutMs = hook.timeoutMs ?? settings.hooks.timeoutMs;
|
|
95
|
+
if (format === 'zcode' && timeoutMs !== undefined && (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
96
|
+
errors.push(`${file}: invalid ${event} hook timeoutMs`);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const timeout = format === 'zcode' && typeof timeoutMs === 'number' ? timeoutMs / 1000 : hook.timeout;
|
|
100
|
+
hooks.push({ type: 'command', command: hook.command, ...(typeof timeout === 'number' ? { timeout } : {}) });
|
|
101
|
+
}
|
|
102
|
+
if (hooks.length > 0)
|
|
103
|
+
(events[event] ??= []).push({ ...(matcher === undefined ? {} : { matcher }), hooks });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return !disabled && Object.keys(events).length > 0 ? { projectRoot, events } : undefined;
|
|
108
|
+
}
|
|
@@ -46,5 +46,10 @@ export async function runScanChain(filters, context) {
|
|
|
46
46
|
return resolution;
|
|
47
47
|
};
|
|
48
48
|
const resolution = await run(0);
|
|
49
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
suites: resolution.kind === 'resolved' ? resolution.suites : [],
|
|
51
|
+
attempts,
|
|
52
|
+
notes,
|
|
53
|
+
...(context.marketplacePath === undefined ? {} : { marketplacePath: context.marketplacePath })
|
|
54
|
+
};
|
|
50
55
|
}
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
* 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
|
|
19
19
|
* `<root>/<name>/SKILL.md` collections.
|
|
20
20
|
*/
|
|
21
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
21
|
+
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
22
22
|
import { join, resolve } from 'node:path';
|
|
23
23
|
import { isDirectory, sanitizeId } from './paths.js';
|
|
24
|
-
import {
|
|
25
|
-
import { countSurfaces, discoverMcp, discoverSkills } from './surfaces.js';
|
|
26
|
-
import {
|
|
24
|
+
import { componentDeclarations, detectManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
|
|
25
|
+
import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
|
|
26
|
+
import { discoverMarkdownResources } from './component-files.js';
|
|
27
|
+
import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
|
|
27
28
|
import { runScanChain } from './scan-pipeline.js';
|
|
28
29
|
export { repoName } from './manifests.js';
|
|
29
30
|
export { listMdFiles, discoverLspEntries } from './surfaces.js';
|
|
@@ -71,7 +72,7 @@ export function entryRemoteUrl(entry) {
|
|
|
71
72
|
return record['url'];
|
|
72
73
|
return githubRepoUrl(record);
|
|
73
74
|
}
|
|
74
|
-
function claimLocal(checkout, declaredPath) {
|
|
75
|
+
async function claimLocal(checkout, declaredPath) {
|
|
75
76
|
const dir = resolve(checkout, declaredPath);
|
|
76
77
|
// Containment guard: a relative entry must stay inside the checkout.
|
|
77
78
|
if (dir !== checkout && !dir.startsWith(`${checkout}/`)) {
|
|
@@ -80,6 +81,15 @@ function claimLocal(checkout, declaredPath) {
|
|
|
80
81
|
// handlers must not reinterpret it as something else.
|
|
81
82
|
return { kind: 'rejected', reason: `path "${declaredPath}" escapes the checkout` };
|
|
82
83
|
}
|
|
84
|
+
try {
|
|
85
|
+
const [realCheckout, realDir] = await Promise.all([realpath(checkout), realpath(dir)]);
|
|
86
|
+
if (realDir !== realCheckout && !realDir.startsWith(`${realCheckout}/`)) {
|
|
87
|
+
return { kind: 'rejected', reason: `path "${declaredPath}" escapes the checkout through a symlink` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { kind: 'rejected', reason: `path "${declaredPath}" is missing or unreadable` };
|
|
92
|
+
}
|
|
83
93
|
return { kind: 'local', dir };
|
|
84
94
|
}
|
|
85
95
|
function unclaimed(reason) {
|
|
@@ -88,7 +98,7 @@ function unclaimed(reason) {
|
|
|
88
98
|
/** String source: a checkout-relative path (Claude Code `"./skills/x"` form). */
|
|
89
99
|
const STRING_PATH_HANDLER = {
|
|
90
100
|
name: 'local-string-path',
|
|
91
|
-
handle(context) {
|
|
101
|
+
async handle(context) {
|
|
92
102
|
const source = context.entry.source;
|
|
93
103
|
if (typeof source !== 'string')
|
|
94
104
|
return unclaimed('source is not a path string');
|
|
@@ -98,11 +108,13 @@ const STRING_PATH_HANDLER = {
|
|
|
98
108
|
/** Object source with `path`: Codex `{ source: 'local', path }` and Claude `{ path }`. */
|
|
99
109
|
const OBJECT_PATH_HANDLER = {
|
|
100
110
|
name: 'local-object-path',
|
|
101
|
-
handle(context) {
|
|
111
|
+
async handle(context) {
|
|
102
112
|
const source = context.entry.source;
|
|
103
113
|
if (source === null || typeof source !== 'object')
|
|
104
114
|
return unclaimed('source is not an object');
|
|
105
115
|
const record = source;
|
|
116
|
+
if (entryRemoteUrl(context.entry) !== undefined)
|
|
117
|
+
return unclaimed('path belongs to a remote source');
|
|
106
118
|
if (typeof record['path'] !== 'string' || record['path'] === '')
|
|
107
119
|
return unclaimed('object source carries no path');
|
|
108
120
|
return claimLocal(context.checkout, record['path']);
|
|
@@ -128,7 +140,8 @@ const ENTRY_HANDLERS = [STRING_PATH_HANDLER, OBJECT_PATH_HANDLER, REMOTE_URL_HAN
|
|
|
128
140
|
export async function resolveMarketplaceEntry(checkout, entry, sourceUrl) {
|
|
129
141
|
const remoteUrl = entryRemoteUrl(entry);
|
|
130
142
|
if (sourceUrl !== undefined && remoteUrl !== undefined && canonicalGitUrl(remoteUrl) === canonicalGitUrl(sourceUrl) && (await isDirectory(checkout))) {
|
|
131
|
-
|
|
143
|
+
const source = entry.source;
|
|
144
|
+
return typeof source === 'object' && source !== null && typeof source.path === 'string' ? claimLocal(checkout, source.path) : { kind: 'local', dir: checkout };
|
|
132
145
|
}
|
|
133
146
|
let lastReason = 'no handler claimed the entry';
|
|
134
147
|
for (const handler of ENTRY_HANDLERS) {
|
|
@@ -148,20 +161,21 @@ export async function resolveMarketplaceEntry(checkout, entry, sourceUrl) {
|
|
|
148
161
|
export class MarketplaceStrategy {
|
|
149
162
|
name = 'marketplace';
|
|
150
163
|
async doScan(context, chain) {
|
|
151
|
-
const marketplaces = await readMarketplaces(context.checkout);
|
|
164
|
+
const marketplaces = await readMarketplaces(context.checkout, context.notes);
|
|
152
165
|
for (const marketplace of marketplaces) {
|
|
153
166
|
const roots = await this.marketplaceRoots(context, marketplace.entries);
|
|
154
167
|
if (roots.length === 0) {
|
|
155
168
|
context.notes.push(`marketplace ${marketplace.path}: no entry resolved (all ${marketplace.entries.length} dropped or remote)`);
|
|
156
169
|
continue;
|
|
157
170
|
}
|
|
158
|
-
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)));
|
|
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)));
|
|
159
172
|
const resolved = suites.filter((suite) => suite !== undefined);
|
|
160
173
|
if (resolved.length === 0) {
|
|
161
174
|
context.notes.push(`marketplace ${marketplace.path}: every resolved entry failed to parse a manifest`);
|
|
162
175
|
continue;
|
|
163
176
|
}
|
|
164
177
|
// A productive dialect wins; supplement with containers it did not list.
|
|
178
|
+
context.marketplacePath = marketplace.path;
|
|
165
179
|
for (const container of CONTAINER_DIRS) {
|
|
166
180
|
const containerDir = join(context.checkout, container);
|
|
167
181
|
if (!(await isDirectory(containerDir)))
|
|
@@ -169,7 +183,7 @@ export class MarketplaceStrategy {
|
|
|
169
183
|
for (const child of await listChildDirs(containerDir)) {
|
|
170
184
|
if (roots.some(root => root.dir === child) || !(await hasSuiteManifest(child)))
|
|
171
185
|
continue;
|
|
172
|
-
const extra = await readSuite(child, context.sourceId, context.dimension, undefined);
|
|
186
|
+
const extra = await readSuite(child, context.sourceId, context.dimension, undefined, context.notes);
|
|
173
187
|
if (extra !== undefined)
|
|
174
188
|
resolved.push(extra);
|
|
175
189
|
}
|
|
@@ -186,6 +200,8 @@ export class MarketplaceStrategy {
|
|
|
186
200
|
const seen = new Set();
|
|
187
201
|
for (const entry of entries) {
|
|
188
202
|
const hint = {
|
|
203
|
+
...componentDeclarations(entry),
|
|
204
|
+
layout: entry.layout,
|
|
189
205
|
name: entry.name,
|
|
190
206
|
version: entry.version,
|
|
191
207
|
description: entry.description,
|
|
@@ -221,13 +237,13 @@ export class RootedStrategy {
|
|
|
221
237
|
name = 'rooted';
|
|
222
238
|
async doScan(context, chain) {
|
|
223
239
|
if (await hasSuiteManifest(context.checkout)) {
|
|
224
|
-
const suite = await readSuite(context.checkout, context.sourceId, context.dimension, undefined);
|
|
240
|
+
const suite = await readSuite(context.checkout, context.sourceId, context.dimension, undefined, context.notes);
|
|
225
241
|
return suite === undefined ? chain.next(context) : { kind: 'resolved', suites: [suite] };
|
|
226
242
|
}
|
|
227
243
|
const found = await collectRoots(context.checkout, undefined, new Set());
|
|
228
244
|
if (found.length === 0)
|
|
229
245
|
return chain.next(context);
|
|
230
|
-
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)));
|
|
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)));
|
|
231
247
|
const resolved = suites.filter((suite) => suite !== undefined);
|
|
232
248
|
return resolved.length === 0 ? chain.next(context) : { kind: 'resolved', suites: resolved };
|
|
233
249
|
}
|
|
@@ -236,6 +252,10 @@ export class RootedStrategy {
|
|
|
236
252
|
export class FlatCollectionsStrategy {
|
|
237
253
|
name = 'flat-collections';
|
|
238
254
|
async doScan(context) {
|
|
255
|
+
// A declared suite has already been evaluated by rooted discovery.
|
|
256
|
+
// Reinterpreting its children would bypass a rejected manifest.
|
|
257
|
+
if (await hasSuiteManifest(context.checkout))
|
|
258
|
+
return { kind: 'resolved', suites: [] };
|
|
239
259
|
const collection = [];
|
|
240
260
|
for (const child of await listChildDirs(context.checkout)) {
|
|
241
261
|
if (await hasSkillFiles(child))
|
|
@@ -245,7 +265,7 @@ export class FlatCollectionsStrategy {
|
|
|
245
265
|
context.notes.push('checkout declares no recognizable plugin shape');
|
|
246
266
|
return { kind: 'resolved', suites: [] };
|
|
247
267
|
}
|
|
248
|
-
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)));
|
|
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)));
|
|
249
269
|
return { kind: 'resolved', suites: suites.filter((suite) => suite !== undefined) };
|
|
250
270
|
}
|
|
251
271
|
}
|
|
@@ -280,7 +300,7 @@ async function collectRoots(dir, hint, seen, depth = 0) {
|
|
|
280
300
|
// An entry carrying inline LSP declarations is a suite by declaration alone
|
|
281
301
|
// (official CC lsp plugins ship only a README), but only at the marketplace
|
|
282
302
|
// entry root — never deeper, so containers cannot self-declare.
|
|
283
|
-
if ((await hasSuiteManifest(dir)) || (await hasSkillFiles(dir)) || (hint
|
|
303
|
+
if ((await hasSuiteManifest(dir)) || (await hasSkillFiles(dir)) || (hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0 && depth === 0)) {
|
|
284
304
|
seen.add(dir);
|
|
285
305
|
return [{ dir, hint }];
|
|
286
306
|
}
|
|
@@ -299,6 +319,8 @@ export async function hasSkillFiles(dir) {
|
|
|
299
319
|
const skillsDir = join(dir, 'skills');
|
|
300
320
|
if (!(await isDirectory(skillsDir)))
|
|
301
321
|
return false;
|
|
322
|
+
if ((await listMdFiles(skillsDir)).length > 0)
|
|
323
|
+
return true;
|
|
302
324
|
for (const child of await listChildDirs(skillsDir)) {
|
|
303
325
|
if (await isFile(join(child, 'SKILL.md')))
|
|
304
326
|
return true;
|
|
@@ -316,15 +338,21 @@ async function listChildDirs(dir) {
|
|
|
316
338
|
return entries.filter(entry => entry.isDirectory() && !DOT_DIRS.has(entry.name) && !entry.name.startsWith('.')).map(entry => join(dir, entry.name));
|
|
317
339
|
}
|
|
318
340
|
/** Read one suite root into the normalized shape, or undefined when no manifest parses. */
|
|
319
|
-
export async function readSuite(root, sourceId, dimension, hint) {
|
|
341
|
+
export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
320
342
|
const errors = [];
|
|
321
|
-
|
|
343
|
+
const declaredManifest = await hasSuiteManifest(root);
|
|
344
|
+
let manifest = declaredManifest ? await readManifest(root, errors, hint) : await syntheticManifest(root);
|
|
345
|
+
if (declaredManifest && (manifest === undefined || errors.length > 0)) {
|
|
346
|
+
notes.push(...errors, `suite ${root}: rejected declared manifest`);
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
322
349
|
// A declaration-only suite (official CC lsp plugins ship just a README):
|
|
323
350
|
// the marketplace entry's inline lspServers are its manifest.
|
|
324
|
-
if (manifest === undefined && hint
|
|
351
|
+
if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
|
|
325
352
|
const name = hint.name ?? syntheticManifestName(root);
|
|
326
353
|
manifest = {
|
|
327
|
-
layout: 'claude-code',
|
|
354
|
+
layout: hint.layout ?? 'claude-code',
|
|
355
|
+
components: componentDeclarations(hint),
|
|
328
356
|
path: '',
|
|
329
357
|
id: sanitizeId(name),
|
|
330
358
|
name,
|
|
@@ -334,16 +362,32 @@ export async function readSuite(root, sourceId, dimension, hint) {
|
|
|
334
362
|
}
|
|
335
363
|
if (manifest === undefined)
|
|
336
364
|
return undefined;
|
|
337
|
-
|
|
365
|
+
let declared = manifest.components?.skills;
|
|
366
|
+
if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
|
|
367
|
+
declared = ['skills', ...(Array.isArray(declared) ? declared : [declared])];
|
|
368
|
+
}
|
|
338
369
|
const skills = await discoverSkills(root, errors, declared);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
370
|
+
if (manifest.layout === 'skill-collection' && skills.length === 0) {
|
|
371
|
+
notes.push(...errors);
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
const mcp = await discoverMcp(root, errors, manifest);
|
|
375
|
+
// Effective component declarations include marketplace fallbacks and manifest overrides.
|
|
376
|
+
const lsp = await discoverSuiteLsp(root, manifest, errors);
|
|
377
|
+
const hooks = await discoverSuiteHooks(root, manifest, errors);
|
|
378
|
+
const resources = {
|
|
379
|
+
commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
|
|
380
|
+
agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
|
|
381
|
+
};
|
|
382
|
+
const systemPrompt = await discoverSystemPrompt(root, manifest, errors);
|
|
345
383
|
const surfaces = await countSurfaces(root, skills, mcp, lsp);
|
|
384
|
+
surfaces.commands = resources.commands.length;
|
|
385
|
+
surfaces.agents = resources.agents.length;
|
|
386
|
+
surfaces.hooks = Object.values(hooks?.events ?? {}).reduce((count, groups) => count + groups.reduce((sum, group) => sum + group.hooks.length, 0), 0);
|
|
346
387
|
return {
|
|
388
|
+
resources,
|
|
389
|
+
...(hooks === undefined ? {} : { hooks }),
|
|
390
|
+
...(systemPrompt === undefined ? {} : { systemPrompt }),
|
|
347
391
|
sourceId,
|
|
348
392
|
id: manifest.id,
|
|
349
393
|
root,
|
|
@@ -17,7 +17,7 @@ export async function discoverSourceList(sources, dimension, dimensionRoot) {
|
|
|
17
17
|
return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot)).suites;
|
|
18
18
|
}
|
|
19
19
|
/** Discover suites plus per-source scan diagnostics for one dimension. */
|
|
20
|
-
export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot) {
|
|
20
|
+
export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts = true) {
|
|
21
21
|
const checkoutRoot = sourcesDir(dimensionRoot);
|
|
22
22
|
const listed = new Set(sources.map(source => source.id));
|
|
23
23
|
const checkouts = sources.map(source => ({
|
|
@@ -52,10 +52,23 @@ export async function discoverSourceListWithNotes(sources, dimension, dimensionR
|
|
|
52
52
|
if (entry.notes.length > 0)
|
|
53
53
|
scanNotes[entry.sourceId] = entry.notes;
|
|
54
54
|
}
|
|
55
|
-
if (dimension === 'project') {
|
|
55
|
+
if (dimension === 'project' && scanProjectLayouts) {
|
|
56
56
|
// Native project directories live two levels above the dimension root
|
|
57
57
|
// (`<projectRoot>/.dsh/agent-plugins`); read them in place.
|
|
58
|
-
|
|
58
|
+
const native = await discoverNativeProjectSuites(dirname(dirname(dimensionRoot)), dimension);
|
|
59
|
+
suites.push(...native);
|
|
60
|
+
const errors = [...new Set(native.flatMap(suite => suite.errors))];
|
|
61
|
+
if (errors.length > 0)
|
|
62
|
+
scanNotes.native = errors;
|
|
63
|
+
}
|
|
64
|
+
if (dimension === 'project') {
|
|
65
|
+
for (const suite of suites) {
|
|
66
|
+
if (suite.lsp === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const note = `suite ${suite.id}: project LSP declarations are not mounted; the host LSP registry does not isolate projects`;
|
|
69
|
+
suite.errors.push(note);
|
|
70
|
+
(scanNotes[suite.sourceId] ??= []).push(note);
|
|
71
|
+
}
|
|
59
72
|
}
|
|
60
73
|
return { suites, scanNotes };
|
|
61
74
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Resolve all plugin component declarations into the same normalized resources used by runtime consumers. */
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { relative } from 'node:path';
|
|
4
|
+
import { componentDocuments, componentPath, firstComponentFile, isRecord } from './component-files.js';
|
|
5
|
+
import { parseLspServers } from './lsp-spec.js';
|
|
6
|
+
import { normalizeHookDocuments } from './project-hooks.js';
|
|
7
|
+
import { PLUGIN_ROOT_VARIABLES } from '../model/layouts.js';
|
|
8
|
+
export async function discoverSuiteLsp(root, manifest, errors) {
|
|
9
|
+
const declaration = manifest.components?.lspServers;
|
|
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']);
|
|
11
|
+
if (declaration === undefined && fallback === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const documents = await componentDocuments(root, declaration ?? fallback, errors);
|
|
14
|
+
if (documents === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const servers = Object.create(null);
|
|
17
|
+
for (const { value } of documents)
|
|
18
|
+
Object.assign(servers, parseLspServers(value.lspServers ?? value, errors));
|
|
19
|
+
return Object.keys(servers).length > 0 ? { servers } : undefined;
|
|
20
|
+
}
|
|
21
|
+
function quote(argument) {
|
|
22
|
+
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
23
|
+
}
|
|
24
|
+
function rootVariables(text, root) {
|
|
25
|
+
return text.replace(/\$\{([A-Z_]+)\}/g, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? root : match));
|
|
26
|
+
}
|
|
27
|
+
const EVENT_ALIASES = {
|
|
28
|
+
sessionStart: 'SessionStart',
|
|
29
|
+
userPromptSubmitted: 'UserPromptSubmit',
|
|
30
|
+
beforeSubmitPrompt: 'UserPromptSubmit',
|
|
31
|
+
preToolUse: 'PreToolUse',
|
|
32
|
+
postToolUse: 'PostToolUse',
|
|
33
|
+
stop: 'Stop',
|
|
34
|
+
subagentStart: 'SubagentStart',
|
|
35
|
+
subagentStop: 'SubagentStop'
|
|
36
|
+
};
|
|
37
|
+
/** File configs and Kimi's inline array converge on the established command-hook bridge shape. */
|
|
38
|
+
export async function discoverSuiteHooks(root, manifest, errors) {
|
|
39
|
+
let declaration = manifest.components?.hooks;
|
|
40
|
+
if (manifest.layout === 'kimi' && Array.isArray(declaration)) {
|
|
41
|
+
const events = Object.create(null);
|
|
42
|
+
for (const hook of declaration) {
|
|
43
|
+
if (!isRecord(hook) || typeof hook.event !== 'string' || typeof hook.command !== 'string') {
|
|
44
|
+
errors.push('invalid Kimi inline hook');
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
;
|
|
48
|
+
(events[hook.event] ??= []).push({
|
|
49
|
+
...(hook.matcher === undefined ? {} : { matcher: hook.matcher }),
|
|
50
|
+
hooks: [{ type: 'command', command: hook.command, ...(hook.timeout === undefined ? {} : { timeout: hook.timeout }) }]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
declaration = { hooks: events };
|
|
54
|
+
}
|
|
55
|
+
const fallback = manifest.layout === 'kimi' ? undefined : await firstComponentFile(root, ['hooks/hooks.json', 'hooks.json']);
|
|
56
|
+
if (declaration === undefined && fallback === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const documents = await componentDocuments(root, declaration ?? fallback, errors, true);
|
|
59
|
+
if (documents === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const normalized = [];
|
|
62
|
+
for (const { path, value } of documents) {
|
|
63
|
+
const events = value.hooks ?? value;
|
|
64
|
+
if (!isRecord(events)) {
|
|
65
|
+
errors.push('hooks must contain an event table');
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const output = {};
|
|
69
|
+
for (const [event, groups] of Object.entries(events)) {
|
|
70
|
+
const canonicalEvent = EVENT_ALIASES[event] ?? event;
|
|
71
|
+
if (!Array.isArray(groups)) {
|
|
72
|
+
output[canonicalEvent] = groups;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
output[canonicalEvent] = groups.map(rawGroup => {
|
|
76
|
+
const group = isRecord(rawGroup) && !Array.isArray(rawGroup.hooks) && (typeof rawGroup.command === 'string' || typeof rawGroup.bash === 'string')
|
|
77
|
+
? {
|
|
78
|
+
matcher: rawGroup.matcher,
|
|
79
|
+
hooks: [{ ...rawGroup, type: rawGroup.type ?? 'command', command: rawGroup.command ?? rawGroup.bash, timeout: rawGroup.timeout ?? rawGroup.timeoutSec }]
|
|
80
|
+
}
|
|
81
|
+
: rawGroup;
|
|
82
|
+
if (!isRecord(group) || !Array.isArray(group.hooks))
|
|
83
|
+
return group;
|
|
84
|
+
return {
|
|
85
|
+
...group,
|
|
86
|
+
hooks: group.hooks.map(hook => {
|
|
87
|
+
if (!isRecord(hook))
|
|
88
|
+
return hook;
|
|
89
|
+
if (hook.type !== 'process')
|
|
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'))))
|
|
92
|
+
return { ...hook, type: 'command', command: undefined };
|
|
93
|
+
const argv = [hook.command, ...(hook.args ?? [])].map(arg => quote(rootVariables(arg, root)));
|
|
94
|
+
return { ...hook, type: 'command', command: argv.join(' '), ...(typeof hook.timeoutMs === 'number' ? { timeout: hook.timeoutMs / 1000 } : {}) };
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
normalized.push({ file: path === undefined ? manifest.path : relative(root, path), settings: { hooks: output } });
|
|
100
|
+
}
|
|
101
|
+
return normalizeHookDocuments(root, normalized, errors);
|
|
102
|
+
}
|
|
103
|
+
export async function discoverSystemPrompt(root, manifest, errors) {
|
|
104
|
+
const parts = [];
|
|
105
|
+
if (manifest.systemPrompt !== undefined)
|
|
106
|
+
parts.push(manifest.systemPrompt);
|
|
107
|
+
if (manifest.systemPromptPath !== undefined) {
|
|
108
|
+
const path = await componentPath(root, manifest.systemPromptPath, errors);
|
|
109
|
+
if (path === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
parts.push(await readFile(path, 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
errors.push('systemPromptPath is unreadable');
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const content = parts.join('\n\n');
|
|
120
|
+
if (Buffer.byteLength(content) > 32768) {
|
|
121
|
+
errors.push('system prompt exceeds 32 KiB');
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return content === '' ? undefined : content;
|
|
125
|
+
}
|