dsh-agent-plugins-market 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -169
- package/README.zh.md +136 -170
- package/client/client.js +8202 -684
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +236 -47
- package/lib/application/details.js +19 -10
- package/lib/application/panel-resources.js +95 -0
- package/lib/application/project-agent-roles.js +39 -0
- package/lib/catalog/codex-mcp.js +114 -0
- package/lib/catalog/component-files.js +151 -0
- package/lib/catalog/manifests.js +105 -44
- package/lib/catalog/native-project.js +44 -23
- package/lib/catalog/paths.js +6 -6
- package/lib/catalog/project-config.js +96 -0
- package/lib/catalog/project-hooks.js +108 -0
- package/lib/catalog/scan-pipeline.js +6 -1
- package/lib/catalog/scan-resolvers.js +70 -26
- package/lib/catalog/source-catalog.js +16 -3
- package/lib/catalog/suite-components.js +125 -0
- package/lib/catalog/surfaces.js +60 -46
- package/lib/catalog/validate.js +16 -11
- package/lib/contracts/market.js +16 -1
- package/lib/index.js +187 -35
- package/lib/model/layouts.js +49 -0
- package/lib/routes.js +106 -6
- package/lib/runtime/agent-role-router.js +226 -0
- package/lib/runtime/commands-mounts.js +27 -67
- package/lib/runtime/feedback-tool.js +181 -0
- package/lib/runtime/hooks-mounts.js +66 -13
- package/lib/runtime/host-locale.js +23 -7
- package/lib/runtime/legacy-root-migration.js +12 -0
- package/lib/runtime/lsp-direct-config.js +6 -3
- package/lib/runtime/lsp-mounts.js +38 -24
- package/lib/runtime/lsp-server-state.js +19 -0
- package/lib/runtime/lsp-status.js +3 -2
- package/lib/runtime/mcp-backend.js +6 -2
- package/lib/runtime/mcp-client/config.js +6 -0
- package/lib/runtime/mcp-client/connection.js +8 -2
- package/lib/runtime/mcp-client/tools.js +6 -3
- package/lib/runtime/mcp-config.js +26 -13
- package/lib/runtime/mcp-direct-config.js +56 -0
- package/lib/runtime/mcp-mounts.js +25 -9
- package/lib/runtime/mcp-overrides.js +11 -5
- package/lib/runtime/mcp-status.js +10 -7
- package/lib/runtime/model-catalog.js +48 -0
- package/lib/runtime/project-runtime.js +136 -0
- package/lib/runtime/reconciler.js +38 -28
- package/lib/runtime/server-config.js +75 -0
- package/lib/runtime/skills-provider.js +31 -77
- package/lib/runtime/storage-migration.js +169 -0
- package/lib/runtime/subagent-catalog.js +129 -0
- package/lib/runtime/user-commands.js +86 -0
- package/lib/runtime/user-panels.js +176 -0
- package/lib/runtime/user-store.js +142 -0
- package/lib/types/application/catalog.d.ts +39 -9
- package/lib/types/application/panel-resources.d.ts +13 -0
- package/lib/types/application/project-agent-roles.d.ts +3 -0
- package/lib/types/application/queries.d.ts +17 -1
- package/lib/types/catalog/codex-mcp.d.ts +4 -0
- package/lib/types/catalog/component-files.d.ts +17 -0
- package/lib/types/catalog/manifests.d.ts +14 -9
- package/lib/types/catalog/native-project.d.ts +2 -5
- package/lib/types/catalog/paths.d.ts +4 -4
- package/lib/types/catalog/project-config.d.ts +6 -0
- package/lib/types/catalog/project-hooks.d.ts +10 -0
- package/lib/types/catalog/scan-pipeline.d.ts +2 -0
- package/lib/types/catalog/scan-resolvers.d.ts +4 -3
- package/lib/types/catalog/source-catalog.d.ts +1 -1
- package/lib/types/catalog/suite-components.d.ts +5 -0
- package/lib/types/catalog/surfaces.d.ts +2 -2
- package/lib/types/catalog/validate.d.ts +2 -0
- package/lib/types/contracts/market.d.ts +60 -0
- package/lib/types/contracts/mcp-status.d.ts +5 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/model/layouts.d.ts +102 -0
- package/lib/types/model/types.d.ts +51 -6
- package/lib/types/routes.d.ts +6 -1
- package/lib/types/runtime/agent-role-router.d.ts +95 -0
- package/lib/types/runtime/commands-mounts.d.ts +16 -7
- package/lib/types/runtime/feedback-tool.d.ts +49 -0
- package/lib/types/runtime/hooks-mounts.d.ts +4 -0
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/legacy-root-migration.d.ts +2 -0
- package/lib/types/runtime/lsp-mounts.d.ts +4 -0
- package/lib/types/runtime/lsp-server-state.d.ts +3 -0
- package/lib/types/runtime/lsp-status.d.ts +1 -0
- package/lib/types/runtime/mcp-backend.d.ts +9 -1
- package/lib/types/runtime/mcp-client/config.d.ts +4 -3
- package/lib/types/runtime/mcp-client/tools.d.ts +3 -0
- package/lib/types/runtime/mcp-config.d.ts +11 -5
- package/lib/types/runtime/mcp-direct-config.d.ts +7 -0
- package/lib/types/runtime/mcp-mounts.d.ts +2 -1
- package/lib/types/runtime/mcp-overrides.d.ts +2 -0
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/model-catalog.d.ts +7 -0
- package/lib/types/runtime/project-runtime.d.ts +32 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/server-config.d.ts +10 -0
- package/lib/types/runtime/skills-provider.d.ts +1 -5
- package/lib/types/runtime/storage-migration.d.ts +15 -0
- package/lib/types/runtime/subagent-catalog.d.ts +52 -0
- package/lib/types/runtime/user-commands.d.ts +24 -0
- package/lib/types/runtime/user-panels.d.ts +91 -0
- package/lib/types/runtime/user-store.d.ts +51 -0
- package/package.json +10 -3
- package/schemas/README.md +69 -7
- package/schemas/agent-plugins/spec.md +48 -0
- package/schemas/claude-code/marketplace.schema.json +143 -0
- package/schemas/claude-code/plugin.schema.json +105 -0
- package/schemas/claude-code/spec.md +108 -0
- package/schemas/codex/marketplace.schema.json +78 -0
- package/schemas/codex/plugin.schema.json +77 -0
- package/schemas/codex/spec.md +91 -0
- package/schemas/cursor/marketplace.schema.json +82 -0
- package/schemas/cursor/plugin.schema.json +58 -0
- package/schemas/cursor/spec.md +92 -0
- package/schemas/github-copilot/marketplace.schema.json +107 -0
- package/schemas/github-copilot/plugin.schema.json +89 -0
- package/schemas/github-copilot/spec.md +105 -0
- package/schemas/kimi/marketplace.schema.json +38 -0
- package/schemas/kimi/plugin.schema.json +84 -0
- package/schemas/kimi/spec.md +94 -0
- package/schemas/qoder/marketplace.schema.json +115 -0
- package/schemas/qoder/plugin.schema.json +127 -0
- package/schemas/qoder/spec.md +111 -0
- package/schemas/skill-collection/spec.md +29 -0
- package/schemas/universal/marketplace.schema.json +98 -0
- package/schemas/universal/plugin.schema.json +49 -0
- package/schemas/universal/spec.md +53 -0
- package/schemas/zcode/marketplace.schema.json +161 -0
- package/schemas/zcode/plugin.schema.json +89 -0
- package/schemas/zcode/spec.md +102 -0
- package/lib/catalog/legacy-root-migration.js +0 -51
- package/lib/types/catalog/legacy-root-migration.d.ts +0 -14
|
@@ -16,13 +16,16 @@ import { discoverSuitesInSource } from '../catalog/suite-scanner.js';
|
|
|
16
16
|
import { archiveInstall } from '../catalog/archive.js';
|
|
17
17
|
import { gitClone, gitCurrentBranch, gitHead, gitRemoteUrl, gitRemove, gitSetRemoteUrl, gitSync } from '../catalog/git.js';
|
|
18
18
|
import { buildMcpStatus } from '../runtime/mcp-status.js';
|
|
19
|
+
import { addUserMcpServer, loadUserMcpSuite, USER_MCP_SOURCE, USER_MCP_SUITE } from '../runtime/mcp-direct-config.js';
|
|
19
20
|
import { buildLspStatus } from '../runtime/lsp-status.js';
|
|
20
21
|
import { loadLspServers, saveLspServers } from '../runtime/lsp-direct-config.js';
|
|
21
|
-
import {
|
|
22
|
+
import { loadDisabledLspServers, saveDisabledLspServers } from '../runtime/lsp-server-state.js';
|
|
23
|
+
import { applyOverride, loadSuiteOverrides, mergeOverridePatch, saveSuiteOverrides } from '../runtime/mcp-overrides.js';
|
|
24
|
+
import { applyLspOverrides, lspConfig, restoreRedactedConfig, saveLspOverride, validateServerLsp, validateServerMcp } from '../runtime/server-config.js';
|
|
22
25
|
import { probeHostMcpClient } from '../runtime/mcp-backend.js';
|
|
23
26
|
import { githubCloneUrl, resolveRegion } from '../runtime/regions.js';
|
|
24
27
|
import { readLocalePreference } from '../runtime/host-locale.js';
|
|
25
|
-
import { redactMcpOverrides } from '../runtime/mcp-redaction.js';
|
|
28
|
+
import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
|
|
26
29
|
import { buildSuiteDetail, readSkillContent } from './details.js';
|
|
27
30
|
import { deriveSourceIdCandidates, expandHome, isDirectory, pathExists, qualifiedSuiteId, resolveProjectRoot, sanitizeId, sourceCheckoutDir, sourcesDir, STATE_FILE_NAME } from '../catalog/paths.js';
|
|
28
31
|
import { loadState, saveState, EMPTY_STATE } from '../model/state.js';
|
|
@@ -44,12 +47,13 @@ export class Catalog {
|
|
|
44
47
|
* discovery (cheap mapping) instead of re-walking every checkout — a full
|
|
45
48
|
* rescan of a 2,500-suite catalog costs ~1s and UI mutations happen in
|
|
46
49
|
* bursts. Content-changing mutations (add / update / remove / adopt /
|
|
47
|
-
* refresh / acquire)
|
|
48
|
-
*
|
|
50
|
+
* refresh / acquire) clear every cached discovery generation; the TTL
|
|
51
|
+
* bounds staleness for in-place working-tree
|
|
49
52
|
* edits of local sources.
|
|
50
53
|
*/
|
|
51
54
|
scanCache = new Map();
|
|
52
|
-
|
|
55
|
+
scanGeneration = 0;
|
|
56
|
+
scanPromises = new Map();
|
|
53
57
|
static SCAN_CACHE_TTL_MS = 30_000;
|
|
54
58
|
static SCAN_CACHE_MAX_ENTRIES = 8;
|
|
55
59
|
/**
|
|
@@ -83,6 +87,14 @@ export class Catalog {
|
|
|
83
87
|
};
|
|
84
88
|
/** Download-region source (the host settings namespace scope). */
|
|
85
89
|
downloadRegionProvider = async () => 'auto';
|
|
90
|
+
scanProjectLayouts = true;
|
|
91
|
+
/** Apply the host's project-layout switch and invalidate every project snapshot. */
|
|
92
|
+
async setScanProjectLayouts(enabled) {
|
|
93
|
+
if (this.scanProjectLayouts === enabled)
|
|
94
|
+
return;
|
|
95
|
+
this.scanProjectLayouts = enabled;
|
|
96
|
+
await this.notifyChanged();
|
|
97
|
+
}
|
|
86
98
|
constructor(options) {
|
|
87
99
|
this.options = options;
|
|
88
100
|
this.statePath = join(options.userRoot, STATE_FILE_NAME);
|
|
@@ -140,18 +152,119 @@ export class Catalog {
|
|
|
140
152
|
/** Build the flat MCP service inventory for the status surface. */
|
|
141
153
|
async mcpStatus() {
|
|
142
154
|
const snapshot = await this.readUserCatalog();
|
|
143
|
-
|
|
155
|
+
const suites = [...snapshot.suites, await loadUserMcpSuite(this.options.dataRoot)];
|
|
156
|
+
const payload = buildMcpStatus(suites, this.mcpDiagnostics, this.toolSnapshotProvider(), await this.allMcpOverrides(suites));
|
|
157
|
+
for (const entry of payload.entries)
|
|
158
|
+
if (entry.suiteId === `${USER_MCP_SOURCE}/${USER_MCP_SUITE}`) {
|
|
159
|
+
entry.kind = 'direct';
|
|
160
|
+
entry.managed = true;
|
|
161
|
+
}
|
|
162
|
+
const oauthBackend = (await this.mcpBackend()) === 'builtin' && this.mcpReauthorizeAvailable();
|
|
163
|
+
for (const entry of payload.entries) {
|
|
164
|
+
const auth = entry.config?.auth;
|
|
165
|
+
entry.canReauthorize =
|
|
166
|
+
oauthBackend &&
|
|
167
|
+
(entry.kind === 'plugin' || entry.managed === true) &&
|
|
168
|
+
(entry.transport === 'sse' || entry.transport === 'streamable-http') &&
|
|
169
|
+
auth?.enabled !== false &&
|
|
170
|
+
!['disabled', 'foreign', 'orphaned'].includes(entry.state);
|
|
171
|
+
}
|
|
172
|
+
return payload;
|
|
173
|
+
}
|
|
174
|
+
/** Persist a user-owned MCP service and reconcile its bridge mount. */
|
|
175
|
+
async addMcpServer(name, server) {
|
|
176
|
+
return this.enqueue(async () => {
|
|
177
|
+
await addUserMcpServer(this.options.dataRoot, name, server);
|
|
178
|
+
await this.notifyChanged(true);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Read effective service configuration without exposing credential literals. */
|
|
182
|
+
async serverConfig(kind, id) {
|
|
183
|
+
const config = await this.resolveServerConfig(kind, id);
|
|
184
|
+
return { kind, id, editable: true, config: redactMcpConfig(config.value) };
|
|
185
|
+
}
|
|
186
|
+
async resolveServerConfig(kind, id) {
|
|
187
|
+
if (kind === 'lsp' && id.startsWith('direct/')) {
|
|
188
|
+
const key = id.slice(7);
|
|
189
|
+
const server = (await loadLspServers(this.options.dataRoot)).servers[key];
|
|
190
|
+
if (server === undefined)
|
|
191
|
+
throw new Error('LSP server not found');
|
|
192
|
+
return { key, suiteKey: 'direct', root: this.options.dataRoot, value: lspConfig(server) };
|
|
193
|
+
}
|
|
194
|
+
const suites = [...(await this.readUserCatalog()).suites, await loadUserMcpSuite(this.options.dataRoot)];
|
|
195
|
+
for (const suite of await applyLspOverrides(this.options.dataRoot, suites)) {
|
|
196
|
+
const suiteKey = qualifiedSuiteId(suite.sourceId, suite.id);
|
|
197
|
+
if (kind === 'mcp') {
|
|
198
|
+
for (const [key, server] of Object.entries(suite.mcp?.servers ?? {})) {
|
|
199
|
+
if (id !== `plugin:${suiteKey}/${key}`)
|
|
200
|
+
continue;
|
|
201
|
+
const overrides = await loadSuiteOverrides(this.options.dataRoot, suiteKey);
|
|
202
|
+
return { key, suiteKey, root: suite.root, value: { ...applyOverride(server, overrides[key]) } };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
for (const [key, server] of Object.entries(suite.lsp?.servers ?? {})) {
|
|
207
|
+
if (id === `${suiteKey}/${key}`)
|
|
208
|
+
return { key, suiteKey, root: suite.root, value: lspConfig(server) };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new Error('service configuration is not managed by this plugin');
|
|
213
|
+
}
|
|
214
|
+
/** Validate a complete replacement before writing; plugin checkouts remain untouched. */
|
|
215
|
+
async saveServerConfig(kind, id, config) {
|
|
216
|
+
return this.enqueue(async () => {
|
|
217
|
+
const current = await this.resolveServerConfig(kind, id);
|
|
218
|
+
const value = restoreRedactedConfig(config, current.value);
|
|
219
|
+
if (kind === 'mcp') {
|
|
220
|
+
const server = await validateServerMcp(current.root, current.key, value);
|
|
221
|
+
const overrides = await loadSuiteOverrides(this.options.dataRoot, current.suiteKey);
|
|
222
|
+
const enabled = overrides[current.key]?.enabled;
|
|
223
|
+
overrides[current.key] = { config: server, ...(enabled === undefined ? {} : { enabled }) };
|
|
224
|
+
await saveSuiteOverrides(this.options.dataRoot, current.suiteKey, overrides);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const server = validateServerLsp(current.key, value);
|
|
228
|
+
if (current.suiteKey === 'direct') {
|
|
229
|
+
const direct = await loadLspServers(this.options.dataRoot);
|
|
230
|
+
if (direct.errors.length > 0)
|
|
231
|
+
throw new Error(direct.errors.join('; '));
|
|
232
|
+
direct.servers[current.key] = server;
|
|
233
|
+
await saveLspServers(this.options.dataRoot, { lspServers: Object.fromEntries(Object.entries(direct.servers).map(([key, spec]) => [key, lspConfig(spec)])) });
|
|
234
|
+
}
|
|
235
|
+
else
|
|
236
|
+
await saveLspOverride(this.options.dataRoot, id, server);
|
|
237
|
+
}
|
|
238
|
+
await this.notifyChanged(true);
|
|
239
|
+
});
|
|
144
240
|
}
|
|
145
241
|
/** The LSP status surface: declared servers merged with mount diagnostics. */
|
|
146
242
|
async lspStatus() {
|
|
147
243
|
const snapshot = await this.readUserCatalog();
|
|
148
244
|
const direct = await loadLspServers(this.options.dataRoot);
|
|
149
|
-
return buildLspStatus(snapshot.suites, this.lspStatusSource ?? { diagnosticsSnapshot: () => new Map(), hasLiveMounts: () => false }, direct);
|
|
245
|
+
return buildLspStatus(await applyLspOverrides(this.options.dataRoot, snapshot.suites), this.lspStatusSource ?? { diagnosticsSnapshot: () => new Map(), hasLiveMounts: () => false }, direct);
|
|
150
246
|
}
|
|
151
247
|
/** The user's direct LSP server table (normalized specs). */
|
|
152
248
|
async lspServers() {
|
|
153
249
|
return (await loadLspServers(this.options.dataRoot)).servers;
|
|
154
250
|
}
|
|
251
|
+
/** Create one direct LSP declaration without replacing other user servers. */
|
|
252
|
+
async addLspServer(name, config) {
|
|
253
|
+
return this.enqueue(async () => {
|
|
254
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name))
|
|
255
|
+
throw new Error('invalid LSP server name');
|
|
256
|
+
const direct = await loadLspServers(this.options.dataRoot);
|
|
257
|
+
if (direct.errors.length > 0)
|
|
258
|
+
throw new Error(direct.errors.join('; '));
|
|
259
|
+
if (Object.hasOwn(direct.servers, name))
|
|
260
|
+
throw new Error('LSP server already exists');
|
|
261
|
+
const spec = validateServerLsp(name, config);
|
|
262
|
+
await saveLspServers(this.options.dataRoot, {
|
|
263
|
+
lspServers: { ...Object.fromEntries(Object.entries(direct.servers).map(([key, value]) => [key, lspConfig(value)])), [name]: lspConfig(spec) }
|
|
264
|
+
});
|
|
265
|
+
await this.notifyChanged(true);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
155
268
|
/** Validate and persist the user's direct LSP server table. */
|
|
156
269
|
async setLspServers(raw) {
|
|
157
270
|
return this.enqueue(async () => {
|
|
@@ -160,6 +273,17 @@ export class Catalog {
|
|
|
160
273
|
return servers;
|
|
161
274
|
});
|
|
162
275
|
}
|
|
276
|
+
async setLspServerEnabled(id, enabled) {
|
|
277
|
+
return this.enqueue(async () => {
|
|
278
|
+
const disabled = await loadDisabledLspServers(this.options.dataRoot);
|
|
279
|
+
if (enabled)
|
|
280
|
+
disabled.delete(id);
|
|
281
|
+
else
|
|
282
|
+
disabled.add(id);
|
|
283
|
+
await saveDisabledLspServers(this.options.dataRoot, disabled);
|
|
284
|
+
await this.notifyChanged(true);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
163
287
|
/** One suite's persisted MCP overrides, addressed by qualified suite id. */
|
|
164
288
|
async mcpOverrides(sourceId, suiteId) {
|
|
165
289
|
const suiteKey = qualifiedSuiteId(sourceId, suiteId);
|
|
@@ -177,12 +301,18 @@ export class Catalog {
|
|
|
177
301
|
return this.enqueue(async () => {
|
|
178
302
|
const suiteKey = qualifiedSuiteId(sourceId, suiteId);
|
|
179
303
|
const source = this.state.sources.find(entry => entry.id === sourceId);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
304
|
+
let suites;
|
|
305
|
+
if (sourceId === USER_MCP_SOURCE && suiteId === USER_MCP_SUITE) {
|
|
306
|
+
suites = [await loadUserMcpSuite(this.options.dataRoot)];
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
if (source === undefined)
|
|
310
|
+
throw new Error(`unknown source "${sourceId}"`);
|
|
311
|
+
const checkout = this.sourceCheckoutPath(source);
|
|
312
|
+
if (!(await isDirectory(checkout)))
|
|
313
|
+
await this.acquire(source);
|
|
314
|
+
suites = await discoverSuitesInSource(checkout, sourceId, 'user', source.url);
|
|
315
|
+
}
|
|
186
316
|
const suite = suites.find(entry => entry.id === suiteId);
|
|
187
317
|
if (suite === undefined)
|
|
188
318
|
throw new Error(`suite "${suiteId}" not found in source "${sourceId}"`);
|
|
@@ -246,11 +376,10 @@ export class Catalog {
|
|
|
246
376
|
setDownloadRegionProvider(provider) {
|
|
247
377
|
this.downloadRegionProvider = provider;
|
|
248
378
|
}
|
|
249
|
-
/**
|
|
250
|
-
async allMcpOverrides() {
|
|
251
|
-
const snapshot = await this.readUserCatalog();
|
|
379
|
+
/** MCP overrides for a supplied runtime selection, or the full market catalog when omitted. */
|
|
380
|
+
async allMcpOverrides(suites) {
|
|
252
381
|
const map = new Map();
|
|
253
|
-
for (const suite of
|
|
382
|
+
for (const suite of suites ?? (await this.readUserCatalog()).suites) {
|
|
254
383
|
const suiteKey = qualifiedSuiteId(suite.sourceId, suite.id);
|
|
255
384
|
const overrides = await loadSuiteOverrides(this.options.dataRoot, suiteKey);
|
|
256
385
|
if (Object.keys(overrides).length > 0)
|
|
@@ -261,7 +390,7 @@ export class Catalog {
|
|
|
261
390
|
/** Load persisted user state once at plugin activation. */
|
|
262
391
|
async load() {
|
|
263
392
|
this.state = await loadState(this.statePath);
|
|
264
|
-
this.
|
|
393
|
+
this.invalidateScans();
|
|
265
394
|
this.invalidateSnapshot(false);
|
|
266
395
|
}
|
|
267
396
|
get sources() {
|
|
@@ -279,16 +408,24 @@ export class Catalog {
|
|
|
279
408
|
return this.buildSnapshot(this.state, 'user', this.options.userRoot);
|
|
280
409
|
if (this.userSnapshot !== undefined && Date.now() < this.userSnapshotExpiresAt)
|
|
281
410
|
return this.userSnapshot;
|
|
282
|
-
|
|
411
|
+
if (this.userSnapshotPromise !== undefined)
|
|
412
|
+
return this.userSnapshotPromise;
|
|
413
|
+
const revision = this.revision;
|
|
414
|
+
const generation = this.scanGeneration;
|
|
415
|
+
const promise = this.buildSnapshot(this.state, 'user', this.options.userRoot)
|
|
283
416
|
.then(snapshot => {
|
|
284
|
-
this.
|
|
285
|
-
|
|
417
|
+
if (revision === this.revision && generation === this.scanGeneration) {
|
|
418
|
+
this.userSnapshot = snapshot;
|
|
419
|
+
this.userSnapshotExpiresAt = Date.now() + this.userSnapshotTtlMs;
|
|
420
|
+
}
|
|
286
421
|
return snapshot;
|
|
287
422
|
})
|
|
288
423
|
.finally(() => {
|
|
289
|
-
this.userSnapshotPromise
|
|
424
|
+
if (this.userSnapshotPromise === promise)
|
|
425
|
+
this.userSnapshotPromise = undefined;
|
|
290
426
|
});
|
|
291
|
-
|
|
427
|
+
this.userSnapshotPromise = promise;
|
|
428
|
+
return promise;
|
|
292
429
|
}
|
|
293
430
|
/** Read one coherent project-dimension snapshot for a workspace cwd. */
|
|
294
431
|
async readProjectCatalog(cwd) {
|
|
@@ -301,13 +438,18 @@ export class Catalog {
|
|
|
301
438
|
const inFlight = this.projectSnapshotPromises.get(projectRoot);
|
|
302
439
|
if (inFlight !== undefined)
|
|
303
440
|
return inFlight;
|
|
441
|
+
const revision = this.revision;
|
|
442
|
+
const generation = this.scanGeneration;
|
|
304
443
|
const promise = this.buildProjectSnapshot(projectRoot)
|
|
305
444
|
.then(snapshot => {
|
|
306
|
-
|
|
445
|
+
if (revision === this.revision && generation === this.scanGeneration) {
|
|
446
|
+
this.projectSnapshots.set(projectRoot, { snapshot, expiresAt: Date.now() + this.projectSnapshotTtlMs });
|
|
447
|
+
}
|
|
307
448
|
return snapshot;
|
|
308
449
|
})
|
|
309
450
|
.finally(() => {
|
|
310
|
-
this.projectSnapshotPromises.
|
|
451
|
+
if (this.projectSnapshotPromises.get(projectRoot) === promise)
|
|
452
|
+
this.projectSnapshotPromises.delete(projectRoot);
|
|
311
453
|
});
|
|
312
454
|
this.projectSnapshotPromises.set(projectRoot, promise);
|
|
313
455
|
return promise;
|
|
@@ -423,14 +565,20 @@ export class Catalog {
|
|
|
423
565
|
unmanaged: await this.unmanagedSources()
|
|
424
566
|
};
|
|
425
567
|
}
|
|
426
|
-
/**
|
|
568
|
+
/** Runtime discovery scans only sources containing an enabled install. No acquisition or network access. */
|
|
427
569
|
async enabledUserSuites() {
|
|
428
|
-
|
|
570
|
+
const enabledSources = new Set(Object.entries(this.state.installed)
|
|
571
|
+
.filter(([, entry]) => entry.enabled)
|
|
572
|
+
.map(([key]) => key.slice(0, key.indexOf('/'))));
|
|
573
|
+
const sources = this.state.sources.filter(source => enabledSources.has(source.id));
|
|
574
|
+
const suites = sources.length === 0 ? [] : (await this.buildSnapshot({ ...this.state, sources }, 'user', this.options.userRoot)).enabledSuites;
|
|
575
|
+
const direct = await loadUserMcpSuite(this.options.dataRoot);
|
|
576
|
+
return applyLspOverrides(this.options.dataRoot, Object.keys(direct.mcp.servers).length === 0 ? suites : [...suites, direct]);
|
|
429
577
|
}
|
|
430
578
|
/** Enabled user- and project-dimension suites for a workspace cwd. */
|
|
431
579
|
async enabledSuitesForCwd(cwd) {
|
|
432
|
-
const [user, project] = await Promise.all([this.
|
|
433
|
-
return { user
|
|
580
|
+
const [user, project] = await Promise.all([this.enabledUserSuites(), this.readProjectCatalog(cwd)]);
|
|
581
|
+
return { user, project: project.enabledSuites };
|
|
434
582
|
}
|
|
435
583
|
/** All suites of one dimension. */
|
|
436
584
|
async suitesForDimension(dimension, dimensionRoot) {
|
|
@@ -681,8 +829,16 @@ export class Catalog {
|
|
|
681
829
|
await this.notifyChanged();
|
|
682
830
|
});
|
|
683
831
|
}
|
|
684
|
-
/**
|
|
685
|
-
|
|
832
|
+
/**
|
|
833
|
+
* Remove a source and its install entries. `deleteCheckout` (the UI's
|
|
834
|
+
* "also delete the managed market directory" option) physically removes the
|
|
835
|
+
* `.sources/<id>` checkout — including an adopted one, since a directory
|
|
836
|
+
* under `.sources` is manager-owned storage. A `local` source whose URL
|
|
837
|
+
* points outside `.sources` is registration-only: its directory is never
|
|
838
|
+
* touched. Deleting the checkout is what keeps a removed source from
|
|
839
|
+
* reappearing as an unmanaged entry.
|
|
840
|
+
*/
|
|
841
|
+
async removeSource(sourceId, deleteCheckout = false) {
|
|
686
842
|
return this.enqueue(async () => {
|
|
687
843
|
const source = this.state.sources.find(entry => entry.id === sourceId);
|
|
688
844
|
this.state = {
|
|
@@ -692,10 +848,11 @@ export class Catalog {
|
|
|
692
848
|
};
|
|
693
849
|
await saveState(this.statePath, this.state);
|
|
694
850
|
this.headCache.delete(sourceId);
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
851
|
+
// A checkout under `.sources` is manager-owned even when it was first
|
|
852
|
+
// discovered/adopted. Only an explicit local path points outside the
|
|
853
|
+
// manager's storage and must remain untouched.
|
|
854
|
+
const externalLocal = source?.local === true && source.url !== sourceCheckoutDir(this.options.userRoot, sourceId);
|
|
855
|
+
if (!externalLocal && deleteCheckout)
|
|
699
856
|
await gitRemove(sourceCheckoutDir(this.options.userRoot, sourceId));
|
|
700
857
|
await this.notifyChanged();
|
|
701
858
|
});
|
|
@@ -853,9 +1010,18 @@ export class Catalog {
|
|
|
853
1010
|
await saveState(this.statePath, this.state);
|
|
854
1011
|
// New sources change the fingerprint, but their content arrives through
|
|
855
1012
|
// an acquire, so mark the scan cache dirty conservatively.
|
|
856
|
-
this.
|
|
1013
|
+
this.invalidateScans();
|
|
857
1014
|
this.invalidateSnapshot(true);
|
|
858
1015
|
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Panel changed hook: the HTTP layer notifies after a user-panel mutation
|
|
1018
|
+
* (skills / commands / agent personas). Catalog state is untouched — this
|
|
1019
|
+
* only runs the change pipeline so the runtime remounts commands and the
|
|
1020
|
+
* skill providers re-read their catalogs.
|
|
1021
|
+
*/
|
|
1022
|
+
async notifyPanelsChanged() {
|
|
1023
|
+
await this.notifyChanged();
|
|
1024
|
+
}
|
|
859
1025
|
async setInstalled(sourceId, suiteId, entry) {
|
|
860
1026
|
this.state = { ...this.state, installed: { ...this.state.installed, [installKey(sourceId, suiteId)]: entry } };
|
|
861
1027
|
await saveState(this.statePath, this.state);
|
|
@@ -910,12 +1076,12 @@ export class Catalog {
|
|
|
910
1076
|
return source.local === true ? expandHome(source.url) : sourceCheckoutDir(this.options.userRoot, source.id);
|
|
911
1077
|
}
|
|
912
1078
|
async buildSnapshot(state, dimension, dimensionRoot, skipScanCache = false) {
|
|
913
|
-
const fingerprint = JSON.stringify([dimension, dimensionRoot, state.sources]);
|
|
1079
|
+
const fingerprint = JSON.stringify([dimension, dimensionRoot, state.sources, this.scanProjectLayouts]);
|
|
914
1080
|
const cached = this.scanCache.get(fingerprint);
|
|
915
1081
|
// The user snapshot's TTL also bounds scan reuse: a snapshot rebuild that
|
|
916
1082
|
// replays a 30s scan cache would silently outlive its own staleness bound.
|
|
917
1083
|
const scanCacheTtl = dimension === 'user' ? Math.min(this.userSnapshotTtlMs, Catalog.SCAN_CACHE_TTL_MS) : Catalog.SCAN_CACHE_TTL_MS;
|
|
918
|
-
const cacheFresh = !skipScanCache &&
|
|
1084
|
+
const cacheFresh = !skipScanCache && cached !== undefined && Date.now() - cached.at < scanCacheTtl;
|
|
919
1085
|
let discovered;
|
|
920
1086
|
let scanNotes;
|
|
921
1087
|
if (cacheFresh && cached !== undefined) {
|
|
@@ -923,15 +1089,28 @@ export class Catalog {
|
|
|
923
1089
|
scanNotes = cached.scanNotes;
|
|
924
1090
|
}
|
|
925
1091
|
else {
|
|
926
|
-
const
|
|
1092
|
+
const generation = this.scanGeneration;
|
|
1093
|
+
let pending = this.scanPromises.get(fingerprint);
|
|
1094
|
+
if (pending === undefined) {
|
|
1095
|
+
pending = discoverSourceListWithNotes(state.sources, dimension, dimensionRoot, this.scanProjectLayouts);
|
|
1096
|
+
this.scanPromises.set(fingerprint, pending);
|
|
1097
|
+
}
|
|
1098
|
+
let result;
|
|
1099
|
+
try {
|
|
1100
|
+
result = await pending;
|
|
1101
|
+
}
|
|
1102
|
+
finally {
|
|
1103
|
+
if (this.scanPromises.get(fingerprint) === pending)
|
|
1104
|
+
this.scanPromises.delete(fingerprint);
|
|
1105
|
+
}
|
|
927
1106
|
discovered = result.suites;
|
|
928
1107
|
scanNotes = result.scanNotes;
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
this.scanCache.
|
|
934
|
-
|
|
1108
|
+
// A scan started before a content mutation must not repopulate its cache.
|
|
1109
|
+
if (generation === this.scanGeneration) {
|
|
1110
|
+
if (this.scanCache.size >= Catalog.SCAN_CACHE_MAX_ENTRIES)
|
|
1111
|
+
this.scanCache.clear();
|
|
1112
|
+
this.scanCache.set(fingerprint, { at: Date.now(), discovered, scanNotes });
|
|
1113
|
+
}
|
|
935
1114
|
}
|
|
936
1115
|
const suites = discovered.map(suite => {
|
|
937
1116
|
const installed = state.installed[installKey(suite.sourceId, suite.id)];
|
|
@@ -941,7 +1120,11 @@ export class Catalog {
|
|
|
941
1120
|
return {
|
|
942
1121
|
...suite,
|
|
943
1122
|
enabled,
|
|
944
|
-
activeSurfaces:
|
|
1123
|
+
activeSurfaces: {
|
|
1124
|
+
...effectiveSurfaces(installed?.surfaces),
|
|
1125
|
+
...(suite.manifest.layout === 'project-native' ? suite.activeSurfaces : {}),
|
|
1126
|
+
...(dimension === 'project' ? { lsp: false } : {})
|
|
1127
|
+
},
|
|
945
1128
|
...(installed?.lockCommit === undefined ? {} : { lockCommit: installed.lockCommit }),
|
|
946
1129
|
...(installed?.installedAt === undefined ? {} : { installedAt: installed.installedAt })
|
|
947
1130
|
};
|
|
@@ -961,6 +1144,12 @@ export class Catalog {
|
|
|
961
1144
|
this.userSnapshotExpiresAt = 0;
|
|
962
1145
|
this.userSnapshotPromise = undefined;
|
|
963
1146
|
this.projectSnapshots.clear();
|
|
1147
|
+
this.projectSnapshotPromises.clear();
|
|
1148
|
+
}
|
|
1149
|
+
invalidateScans() {
|
|
1150
|
+
this.scanGeneration++;
|
|
1151
|
+
this.scanCache.clear();
|
|
1152
|
+
this.scanPromises.clear();
|
|
964
1153
|
}
|
|
965
1154
|
/**
|
|
966
1155
|
* Invalidate snapshots and run the change pipeline. `keepScanCache` marks
|
|
@@ -970,7 +1159,7 @@ export class Catalog {
|
|
|
970
1159
|
*/
|
|
971
1160
|
async notifyChanged(keepScanCache = false) {
|
|
972
1161
|
if (!keepScanCache)
|
|
973
|
-
this.
|
|
1162
|
+
this.invalidateScans();
|
|
974
1163
|
this.invalidateSnapshot(true);
|
|
975
1164
|
await this.options.onChanged();
|
|
976
1165
|
for (const listener of this.listeners)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Suite detail and preview projections owned by the application layer. */
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
|
-
import { discoverLspEntries
|
|
4
|
+
import { discoverLspEntries } from '../catalog/surfaces.js';
|
|
5
|
+
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
5
6
|
import { credentialRefsInServer } from '../runtime/mcp-config.js';
|
|
6
7
|
import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
|
|
7
8
|
import { applyOverride } from '../runtime/mcp-overrides.js';
|
|
@@ -41,9 +42,15 @@ export async function buildSuiteDetail(suite, installed, diagnostics, mcpOverrid
|
|
|
41
42
|
credentialRefs: credentialRefsInServer(effective)
|
|
42
43
|
};
|
|
43
44
|
}),
|
|
44
|
-
hooks: remoteUrl === undefined
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
hooks: remoteUrl === undefined
|
|
46
|
+
? suite.hooks === undefined
|
|
47
|
+
? suite.resources === undefined
|
|
48
|
+
? await hooksPreviews(suite.root)
|
|
49
|
+
: { count: 0, entries: [] }
|
|
50
|
+
: normalizedHookPreviews(suite)
|
|
51
|
+
: { count: 0, entries: [] },
|
|
52
|
+
commands: remoteUrl === undefined ? await markdownPreviews(suite.resources?.commands ?? (await defaultMarkdownResources(suite.root, 'commands'))) : [],
|
|
53
|
+
agents: remoteUrl === undefined ? await markdownPreviews(suite.resources?.agents ?? (await defaultMarkdownResources(suite.root, 'agents'))) : [],
|
|
47
54
|
lsp: remoteUrl === undefined ? await lspDetail(suite) : { servers: [], raw: [] },
|
|
48
55
|
errors: suite.errors,
|
|
49
56
|
mcpErrors: diagnostics.filter(diagnostic => diagnostic.suiteId === suite.id).map(diagnostic => `${diagnostic.serverKey}: ${diagnostic.reason}`)
|
|
@@ -67,13 +74,11 @@ async function readPreview(path, capBytes = 64 * 1024) {
|
|
|
67
74
|
const text = await readFile(path, 'utf8');
|
|
68
75
|
return text.length > capBytes ? `${text.slice(0, capBytes)}\n… (truncated)` : text;
|
|
69
76
|
}
|
|
70
|
-
async function markdownPreviews(
|
|
71
|
-
const names = await listMdFiles(dir);
|
|
77
|
+
async function markdownPreviews(resources) {
|
|
72
78
|
const previews = [];
|
|
73
|
-
for (const
|
|
74
|
-
const file = `${dir}/${name}`;
|
|
79
|
+
for (const resource of resources) {
|
|
75
80
|
try {
|
|
76
|
-
const content = await
|
|
81
|
+
const content = (await resourceText(resource)).slice(0, 64 * 1024);
|
|
77
82
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
78
83
|
let description;
|
|
79
84
|
if (match !== null) {
|
|
@@ -84,7 +89,7 @@ async function markdownPreviews(dir) {
|
|
|
84
89
|
description = desc;
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
|
-
previews.push({ name: name
|
|
92
|
+
previews.push({ name: resource.name, ...(description === undefined ? {} : { description }), content });
|
|
88
93
|
}
|
|
89
94
|
catch {
|
|
90
95
|
// Unreadable preview files are omitted from the detail response.
|
|
@@ -92,6 +97,10 @@ async function markdownPreviews(dir) {
|
|
|
92
97
|
}
|
|
93
98
|
return previews;
|
|
94
99
|
}
|
|
100
|
+
function normalizedHookPreviews(suite) {
|
|
101
|
+
const entries = Object.entries(suite.hooks.events).flatMap(([event, groups]) => groups.flatMap(group => group.hooks.map(hook => ({ event, ...(group.matcher === undefined ? {} : { matcher: group.matcher }), command: hook.command }))));
|
|
102
|
+
return { count: entries.length, entries };
|
|
103
|
+
}
|
|
95
104
|
async function hooksPreviews(root) {
|
|
96
105
|
for (const relative of ['hooks/hooks.json', 'hooks.json']) {
|
|
97
106
|
let text;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Installed suite and user resources share one inventory; paths never come from HTTP callers. */
|
|
2
|
+
import { realpath, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, relative } from 'node:path';
|
|
4
|
+
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
5
|
+
import { stripFrontmatter } from '../catalog/skills-parse.js';
|
|
6
|
+
import { parseFrontmatterRecord } from '../runtime/user-store.js';
|
|
7
|
+
export function createPanelResources(catalog, users) {
|
|
8
|
+
return {
|
|
9
|
+
skills: new PanelResources(catalog, users.skills, 'skills'),
|
|
10
|
+
commands: new PanelResources(catalog, users.commands, 'commands'),
|
|
11
|
+
agents: new PanelResources(catalog, users.agents, 'agents')
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
class PanelResources {
|
|
15
|
+
catalog;
|
|
16
|
+
users;
|
|
17
|
+
kind;
|
|
18
|
+
constructor(catalog, users, kind) {
|
|
19
|
+
this.catalog = catalog;
|
|
20
|
+
this.users = users;
|
|
21
|
+
this.kind = kind;
|
|
22
|
+
}
|
|
23
|
+
async list(strict = false) {
|
|
24
|
+
const entries = await this.users.list(strict);
|
|
25
|
+
const snapshot = await this.catalog.readUserCatalog();
|
|
26
|
+
const installed = new Set((await this.catalog.overview()).suites.filter(suite => suite.installed).map(suite => JSON.stringify([suite.sourceId, suite.suiteId])));
|
|
27
|
+
for (const suite of snapshot.suites) {
|
|
28
|
+
if (!installed.has(JSON.stringify([suite.sourceId, suite.id])) || suite.remote !== undefined)
|
|
29
|
+
continue;
|
|
30
|
+
const files = this.kind === 'skills'
|
|
31
|
+
? suite.skills.map(skill => ({ name: skill.name, file: skill.file }))
|
|
32
|
+
: (suite.resources?.[this.kind] ?? (await defaultMarkdownResources(suite.root, this.kind)));
|
|
33
|
+
for (const resource of files) {
|
|
34
|
+
const { name, file } = resource;
|
|
35
|
+
const rawText = await resourceText(resource).catch(error => {
|
|
36
|
+
if (strict && error.code !== 'ENOENT')
|
|
37
|
+
throw error;
|
|
38
|
+
return undefined;
|
|
39
|
+
});
|
|
40
|
+
if (rawText === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
let metadata;
|
|
43
|
+
try {
|
|
44
|
+
metadata = parseFrontmatterRecord(rawText);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
metadata = { disabled: true, validationError: String(error) };
|
|
48
|
+
}
|
|
49
|
+
entries.push({
|
|
50
|
+
id: JSON.stringify([suite.sourceId, suite.id, this.kind, name]),
|
|
51
|
+
name,
|
|
52
|
+
origin: 'plugin',
|
|
53
|
+
suiteName: suite.manifest.name,
|
|
54
|
+
disabled: !suite.enabled || suite.activeSurfaces?.[this.kind] === false || metadata.disabled === true,
|
|
55
|
+
metadata,
|
|
56
|
+
rawText,
|
|
57
|
+
content: stripFrontmatter(rawText),
|
|
58
|
+
path: file,
|
|
59
|
+
description: typeof metadata.description === 'string' ? metadata.description : ''
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
async get(id) {
|
|
66
|
+
return (await this.list()).find(entry => (entry.id ?? entry.name) === id);
|
|
67
|
+
}
|
|
68
|
+
create(name, text) {
|
|
69
|
+
return this.users.create(name, text);
|
|
70
|
+
}
|
|
71
|
+
async pluginPath(id) {
|
|
72
|
+
const entry = await this.get(id);
|
|
73
|
+
if (entry?.origin !== 'plugin')
|
|
74
|
+
throw new Error('Unknown installed plugin resource');
|
|
75
|
+
if (entry.path.endsWith('.json'))
|
|
76
|
+
throw new Error('Inline manifest resources are read-only; edit their source manifest');
|
|
77
|
+
const root = await realpath(this.users.root());
|
|
78
|
+
const path = await realpath(entry.path);
|
|
79
|
+
const rel = relative(root, path);
|
|
80
|
+
if (rel === '..' || rel.startsWith('../') || isAbsolute(rel))
|
|
81
|
+
throw new Error('External source files are read-only; create a user resource to customize them');
|
|
82
|
+
return path;
|
|
83
|
+
}
|
|
84
|
+
async update(id, text) {
|
|
85
|
+
if (!id.startsWith('['))
|
|
86
|
+
return this.users.update(id, text);
|
|
87
|
+
parseFrontmatterRecord(text);
|
|
88
|
+
await writeFile(await this.pluginPath(id), text, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
async remove(id) {
|
|
91
|
+
if (!id.startsWith('['))
|
|
92
|
+
return this.users.remove(id);
|
|
93
|
+
await unlink(await this.pluginPath(id));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Resolve read-only project roles from the calling session, never from tool-supplied paths. */
|
|
2
|
+
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
3
|
+
import { parseAgentRole } from '../runtime/agent-role-router.js';
|
|
4
|
+
export async function projectAgentRoles(catalog, parent) {
|
|
5
|
+
const cwd = parent?.session?.header?.cwd;
|
|
6
|
+
if (typeof cwd !== 'string' || cwd === '')
|
|
7
|
+
return [];
|
|
8
|
+
const snapshot = await catalog.readProjectCatalog(cwd);
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const suite of snapshot.enabledSuites) {
|
|
11
|
+
if (suite.activeSurfaces?.agents === false)
|
|
12
|
+
continue;
|
|
13
|
+
for (const resource of suite.resources?.agents ?? (await defaultMarkdownResources(suite.root, 'agents'))) {
|
|
14
|
+
const path = resource.file;
|
|
15
|
+
const rawText = await resourceText(resource).catch(error => {
|
|
16
|
+
if (error.code !== 'ENOENT')
|
|
17
|
+
throw error;
|
|
18
|
+
return undefined;
|
|
19
|
+
});
|
|
20
|
+
if (rawText === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
const policy = parseAgentRole(rawText);
|
|
24
|
+
entries.push({
|
|
25
|
+
name: JSON.stringify([suite.sourceId, suite.id, 'agents', resource.name]),
|
|
26
|
+
path,
|
|
27
|
+
rawText,
|
|
28
|
+
title: policy.title ?? resource.name.replace(/\.agent$/, ''),
|
|
29
|
+
description: policy.description ?? `${suite.manifest.name}: ${resource.name}`,
|
|
30
|
+
disabled: policy.disabled
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Malformed routing metadata cannot authorize a project role.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return entries;
|
|
39
|
+
}
|