dsh-agent-plugins-market 0.4.4 → 0.4.6
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 +77 -45
- package/README.zh.md +77 -45
- package/client/client.js +810 -70
- package/client/client.js.map +1 -1
- package/lib/discovery.js +30 -28
- package/lib/index.js +5 -0
- package/lib/manager.js +96 -5
- package/lib/mcp-status.js +147 -0
- package/lib/routes.js +6 -0
- package/lib/types/discovery.d.ts +7 -4
- package/lib/types/manager.d.ts +37 -0
- package/lib/types/mcp-status.d.ts +51 -0
- package/lib/types/types.d.ts +2 -0
- package/package.json +9 -2
package/lib/discovery.js
CHANGED
|
@@ -23,15 +23,10 @@ const DOT_DIRS = new Set(['.git', '.github', '.claude', '.cursor', '.kimi', '.pl
|
|
|
23
23
|
/** Discover every suite under one cloned source checkout. */
|
|
24
24
|
export async function discoverSuitesInSource(checkoutDir, sourceId, dimension) {
|
|
25
25
|
const roots = await suiteRoots(checkoutDir);
|
|
26
|
-
const suites =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: await readSuite(root.dir, sourceId, dimension, root.hint);
|
|
31
|
-
if (suite !== undefined)
|
|
32
|
-
suites.push(suite);
|
|
33
|
-
}
|
|
34
|
-
return suites;
|
|
26
|
+
const suites = await Promise.all(roots.map(async (root) => root.dir === undefined
|
|
27
|
+
? remoteSuite(sourceId, dimension, root)
|
|
28
|
+
: readSuite(root.dir, sourceId, dimension, root.hint)));
|
|
29
|
+
return suites.filter((suite) => suite !== undefined);
|
|
35
30
|
}
|
|
36
31
|
/**
|
|
37
32
|
* Resolve the suite roots of one checkout. A marketplace manifest is the
|
|
@@ -205,30 +200,37 @@ export function isOutside(root, candidate) {
|
|
|
205
200
|
return isAbsolute(candidate) ? !candidate.startsWith(root) : false;
|
|
206
201
|
}
|
|
207
202
|
/**
|
|
208
|
-
* Discover
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
203
|
+
* Discover suites for one dimension. User catalogs only read configured
|
|
204
|
+
* sources: unmanaged user checkouts have no persisted identity and otherwise
|
|
205
|
+
* leak stale repositories into the catalog totals. Project catalogs retain
|
|
206
|
+
* unmanaged checkout ids because project state may authorize them through its
|
|
207
|
+
* installed map without duplicating source configuration. All selected
|
|
208
|
+
* checkouts are discovered concurrently so one large repository does not
|
|
209
|
+
* serialize every other repository's scan.
|
|
212
210
|
*/
|
|
213
211
|
export async function discoverSourceList(sources, dimension, dimensionRoot) {
|
|
214
212
|
const checkoutRoot = sourcesDir(dimensionRoot);
|
|
215
|
-
const bySource = new Map();
|
|
216
213
|
const listed = new Set(sources.map(source => source.id));
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
214
|
+
const checkouts = sources.map(source => ({
|
|
215
|
+
sourceId: source.id,
|
|
216
|
+
checkout: source.local === true ? expandHome(source.url) : join(checkoutRoot, source.id),
|
|
217
|
+
}));
|
|
218
|
+
if (dimension === 'project') {
|
|
219
|
+
try {
|
|
220
|
+
for (const entry of await readdir(checkoutRoot, { withFileTypes: true })) {
|
|
221
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || listed.has(entry.name))
|
|
222
|
+
continue;
|
|
223
|
+
checkouts.push({ sourceId: entry.name, checkout: join(checkoutRoot, entry.name) });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// A missing project checkout root has no unmanaged project sources.
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
|
-
|
|
225
|
-
// a missing checkout root simply has no manual checkouts
|
|
226
|
-
}
|
|
227
|
-
for (const source of sources) {
|
|
228
|
-
const checkout = source.local === true ? expandHome(source.url) : join(checkoutRoot, source.id);
|
|
230
|
+
const discovered = await Promise.all(checkouts.map(async ({ sourceId, checkout }) => {
|
|
229
231
|
if (!await isDirectory(checkout))
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
return
|
|
232
|
+
return [];
|
|
233
|
+
return discoverSuitesInSource(checkout, sourceId, dimension);
|
|
234
|
+
}));
|
|
235
|
+
return discovered.flat();
|
|
234
236
|
}
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { HooksMountRegistry } from './hooks-mounts.js';
|
|
|
3
3
|
import { mountSuiteContext } from './context.js';
|
|
4
4
|
import { SuiteManager } from './manager.js';
|
|
5
5
|
import { McpMountRegistry } from './mcp-mounts.js';
|
|
6
|
+
import { inspectToolRegistry } from './mcp-status.js';
|
|
6
7
|
import { resolveDataRoot, resolveUserRoot } from './paths.js';
|
|
7
8
|
import { mountSuiteRoutes } from './routes.js';
|
|
8
9
|
import { SuiteSkillProvider } from './skills-provider.js';
|
|
@@ -19,6 +20,7 @@ export function apply(ctx, config = {}) {
|
|
|
19
20
|
void (async () => {
|
|
20
21
|
try {
|
|
21
22
|
const diagnostics = await mounts.reconcile(await manager.enabledUserSuites());
|
|
23
|
+
manager.mcpDiagnostics = diagnostics;
|
|
22
24
|
for (const diagnostic of diagnostics) {
|
|
23
25
|
ctx.logger?.warn(`[dsh-agent-plugins-market] suite "${diagnostic.suiteId}" mcp server "${diagnostic.serverKey}": ${diagnostic.reason}`);
|
|
24
26
|
}
|
|
@@ -52,6 +54,9 @@ export function apply(ctx, config = {}) {
|
|
|
52
54
|
reconcileMounts();
|
|
53
55
|
};
|
|
54
56
|
const manager = new SuiteManager({ userRoot, dataRoot, onChanged });
|
|
57
|
+
ctx.inject(['tools'], (toolsCtx) => {
|
|
58
|
+
manager.setMcpToolSnapshotProvider(() => inspectToolRegistry(toolsCtx.tools));
|
|
59
|
+
});
|
|
55
60
|
void manager.load().then(async () => {
|
|
56
61
|
await manager.mergeSources(config.sources ?? []);
|
|
57
62
|
reconcileMounts();
|
package/lib/manager.js
CHANGED
|
@@ -13,6 +13,7 @@ import { mkdir, readFile } from 'node:fs/promises';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { discoverLspEntries, discoverSourceList, discoverSuitesInSource, listMdFiles, repoName } from './discovery.js';
|
|
15
15
|
import { gitClone, gitHead, gitPull, gitRemove } from './git.js';
|
|
16
|
+
import { buildMcpStatus } from './mcp-status.js';
|
|
16
17
|
import { deriveSourceId, expandHome, isDirectory, resolveProjectRoot, sanitizeId, sourceCheckoutDir, sourcesDir, STATE_FILE_NAME } from './paths.js';
|
|
17
18
|
import { loadState, saveState, EMPTY_STATE } from './state.js';
|
|
18
19
|
export class SuiteManager {
|
|
@@ -20,10 +21,24 @@ export class SuiteManager {
|
|
|
20
21
|
state = EMPTY_STATE;
|
|
21
22
|
mutationQueue = Promise.resolve();
|
|
22
23
|
statePath;
|
|
24
|
+
/** Cached `git rev-parse HEAD` per source id, so overview reads stay instant; cleared whenever a checkout may have moved. */
|
|
25
|
+
headCache = new Map();
|
|
26
|
+
/** Latest MCP mount diagnostics (suiteId -> reasons), fed by the host reconcile. */
|
|
27
|
+
mcpDiagnostics = [];
|
|
28
|
+
toolSnapshotProvider = () => [];
|
|
23
29
|
constructor(options) {
|
|
24
30
|
this.options = options;
|
|
25
31
|
this.statePath = join(options.userRoot, STATE_FILE_NAME);
|
|
26
32
|
}
|
|
33
|
+
/** Install the host tool snapshot provider used by the MCP status surface. */
|
|
34
|
+
setMcpToolSnapshotProvider(provider) {
|
|
35
|
+
this.toolSnapshotProvider = provider;
|
|
36
|
+
}
|
|
37
|
+
/** Build the flat MCP service inventory for the status surface. */
|
|
38
|
+
async mcpStatus() {
|
|
39
|
+
const suites = await this.discoverDimension('user', this.options.userRoot);
|
|
40
|
+
return buildMcpStatus(suites, this.mcpDiagnostics, this.toolSnapshotProvider());
|
|
41
|
+
}
|
|
27
42
|
/** Load persisted state once at plugin activation. */
|
|
28
43
|
async load() {
|
|
29
44
|
this.state = await loadState(this.statePath);
|
|
@@ -72,6 +87,7 @@ export class SuiteManager {
|
|
|
72
87
|
agents: remoteUrl === undefined ? await this.agentPreviews(`${suite.root}/agents`) : [],
|
|
73
88
|
lsp: remoteUrl === undefined ? await this.lspPreviews(suite.root) : [],
|
|
74
89
|
errors: suite.errors,
|
|
90
|
+
mcpErrors: this.mcpDiagnostics.filter(diagnostic => diagnostic.suiteId === suite.id).map(diagnostic => `${diagnostic.serverKey}: ${diagnostic.reason}`),
|
|
75
91
|
};
|
|
76
92
|
}
|
|
77
93
|
async readPreview(path, capBytes = 64 * 1024) {
|
|
@@ -101,6 +117,7 @@ export class SuiteManager {
|
|
|
101
117
|
const suites = await this.discoverDimension('user', this.options.userRoot);
|
|
102
118
|
const sourceRows = [];
|
|
103
119
|
for (const source of this.state.sources) {
|
|
120
|
+
const inFlight = this.currentSourceState?.sourceId === source.id ? this.currentSourceState : undefined;
|
|
104
121
|
const checkout = this.sourceCheckoutPath(source);
|
|
105
122
|
let cloned = false;
|
|
106
123
|
let lockCommit;
|
|
@@ -110,13 +127,27 @@ export class SuiteManager {
|
|
|
110
127
|
if (!cloned)
|
|
111
128
|
error = `local source directory ${checkout} is missing`;
|
|
112
129
|
}
|
|
130
|
+
else if (inFlight !== undefined) {
|
|
131
|
+
// A mutation owns this source right now (clone/pull in progress): report
|
|
132
|
+
// its snapshot instead of racing git against the transient directory.
|
|
133
|
+
cloned = inFlight.cloned || await isDirectory(checkout);
|
|
134
|
+
lockCommit = inFlight.head;
|
|
135
|
+
}
|
|
113
136
|
else {
|
|
114
|
-
|
|
115
|
-
|
|
137
|
+
const cachedHead = this.headCache.get(source.id);
|
|
138
|
+
if (cachedHead !== undefined && await isDirectory(checkout)) {
|
|
116
139
|
cloned = true;
|
|
140
|
+
lockCommit = cachedHead;
|
|
117
141
|
}
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
else {
|
|
143
|
+
try {
|
|
144
|
+
lockCommit = await gitHead(checkout);
|
|
145
|
+
cloned = true;
|
|
146
|
+
this.headCache.set(source.id, lockCommit);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// not cloned yet (or a broken checkout); the clone attempt records its own error on refresh
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
}
|
|
122
153
|
const sourceSuites = suites.filter(suite => suite.sourceId === source.id);
|
|
@@ -145,6 +176,7 @@ export class SuiteManager {
|
|
|
145
176
|
dimension: suite.dimension,
|
|
146
177
|
layout: suite.manifest.layout,
|
|
147
178
|
errors: suite.errors,
|
|
179
|
+
mcpErrors: this.mcpDiagnostics.filter(diagnostic => diagnostic.suiteId === suite.id).map(diagnostic => `${diagnostic.serverKey}: ${diagnostic.reason}`),
|
|
148
180
|
}));
|
|
149
181
|
return {
|
|
150
182
|
sources: sourceRows,
|
|
@@ -194,15 +226,59 @@ export class SuiteManager {
|
|
|
194
226
|
throw new Error(`local source directory ${checkout} is missing`);
|
|
195
227
|
}
|
|
196
228
|
else {
|
|
197
|
-
|
|
229
|
+
this.beginSourceState(baseId, 'cloning', false);
|
|
230
|
+
try {
|
|
231
|
+
await this.ensureClone(source);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
this.endSourceState();
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
this.updateSourceStep('reading');
|
|
198
238
|
}
|
|
199
239
|
source.id = this.uniqueSourceId(sanitizeId(await repoName(checkout)));
|
|
240
|
+
const head = await tryHead(checkout);
|
|
241
|
+
if (head !== undefined)
|
|
242
|
+
this.headCache.set(source.id, head);
|
|
243
|
+
this.endSourceState();
|
|
200
244
|
this.state = { ...this.state, sources: [...this.state.sources, source] };
|
|
201
245
|
await saveState(this.statePath, this.state);
|
|
202
246
|
this.options.onChanged();
|
|
203
247
|
return source;
|
|
204
248
|
});
|
|
205
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Progress snapshot of the source mutation currently in flight, if any.
|
|
252
|
+
* While present, `overview()` reports it instead of running git against
|
|
253
|
+
* the transient checkout directory.
|
|
254
|
+
*/
|
|
255
|
+
currentSourceState;
|
|
256
|
+
/**
|
|
257
|
+
* Begin reporting progress for a source mutation: `step` names the active
|
|
258
|
+
* phase and `cloned` whether the checkout directory exists yet. The client
|
|
259
|
+
* polls this through the `progress` route while its add-source modal is
|
|
260
|
+
* open, and `overview()` merges the snapshot into the source row.
|
|
261
|
+
*/
|
|
262
|
+
beginSourceState(sourceId, step, cloned) {
|
|
263
|
+
this.currentSourceState = { sourceId, step, cloned };
|
|
264
|
+
}
|
|
265
|
+
/** Advance the in-flight mutation's step (checkout state unchanged). */
|
|
266
|
+
updateSourceStep(step) {
|
|
267
|
+
if (this.currentSourceState !== undefined) {
|
|
268
|
+
this.currentSourceState = { ...this.currentSourceState, step };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Stop reporting progress; `overview()` falls back to on-disk inspection. */
|
|
272
|
+
endSourceState() {
|
|
273
|
+
this.currentSourceState = undefined;
|
|
274
|
+
}
|
|
275
|
+
/** Progress snapshot for the `progress` route (never throws). */
|
|
276
|
+
sourceProgress() {
|
|
277
|
+
const state = this.currentSourceState;
|
|
278
|
+
return state === undefined
|
|
279
|
+
? { active: false, sourceId: '', step: '' }
|
|
280
|
+
: { active: true, sourceId: state.sourceId, step: state.step };
|
|
281
|
+
}
|
|
206
282
|
uniqueSourceId(derived) {
|
|
207
283
|
if (!this.state.sources.some(source => source.id === derived))
|
|
208
284
|
return derived;
|
|
@@ -230,6 +306,7 @@ export class SuiteManager {
|
|
|
230
306
|
...(patch.local !== undefined ? { local: patch.local } : current.local === undefined ? {} : { local: current.local }),
|
|
231
307
|
};
|
|
232
308
|
if (current.local !== true && patch.url !== undefined && patch.url !== current.url) {
|
|
309
|
+
this.headCache.delete(sourceId);
|
|
233
310
|
await gitRemove(sourceCheckoutDir(this.options.userRoot, sourceId));
|
|
234
311
|
}
|
|
235
312
|
this.state = { ...this.state, sources: this.state.sources.map((source, i) => i === index ? next : source) };
|
|
@@ -247,6 +324,7 @@ export class SuiteManager {
|
|
|
247
324
|
};
|
|
248
325
|
await saveState(this.statePath, this.state);
|
|
249
326
|
const source = this.state.sources.find(entry => entry.id === sourceId);
|
|
327
|
+
this.headCache.delete(sourceId);
|
|
250
328
|
if (source === undefined || source.local !== true) {
|
|
251
329
|
await gitRemove(sourceCheckoutDir(this.options.userRoot, sourceId));
|
|
252
330
|
}
|
|
@@ -265,14 +343,27 @@ export class SuiteManager {
|
|
|
265
343
|
continue;
|
|
266
344
|
}
|
|
267
345
|
const checkout = sourceCheckoutDir(this.options.userRoot, source.id);
|
|
346
|
+
this.headCache.delete(source.id);
|
|
268
347
|
try {
|
|
269
348
|
await gitHead(checkout);
|
|
270
349
|
}
|
|
271
350
|
catch {
|
|
272
351
|
await this.ensureClone(source);
|
|
352
|
+
try {
|
|
353
|
+
this.headCache.set(source.id, await gitHead(checkout));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// a failed head read simply means the next overview re-probes
|
|
357
|
+
}
|
|
273
358
|
continue;
|
|
274
359
|
}
|
|
275
360
|
await gitPull(checkout);
|
|
361
|
+
try {
|
|
362
|
+
this.headCache.set(source.id, await gitHead(checkout));
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// a failed head read simply means the next overview re-probes
|
|
366
|
+
}
|
|
276
367
|
}
|
|
277
368
|
this.options.onChanged();
|
|
278
369
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { deriveServerName } from './mcp-config.js';
|
|
2
|
+
/** Build status rows from discovered plugin MCP definitions and observed tool names. */
|
|
3
|
+
export function buildMcpStatus(suites, diagnostics, observed) {
|
|
4
|
+
const entries = [];
|
|
5
|
+
const claimedServers = new Set();
|
|
6
|
+
const knownServerNames = new Set();
|
|
7
|
+
for (const suite of suites) {
|
|
8
|
+
if (suite.mcp === undefined || suite.installedAt === undefined || !suite.enabled)
|
|
9
|
+
continue;
|
|
10
|
+
for (const serverKey of Object.keys(suite.mcp.servers))
|
|
11
|
+
knownServerNames.add(deriveServerName(suite.id, serverKey));
|
|
12
|
+
}
|
|
13
|
+
const diagnosticsByKey = new Map(diagnostics.map(diagnostic => [`${diagnostic.suiteId}\u0000${diagnostic.serverKey}`, diagnostic]));
|
|
14
|
+
const observedByServer = groupObservedTools(observed, knownServerNames);
|
|
15
|
+
for (const suite of suites) {
|
|
16
|
+
// This is an operational inventory, not a configuration audit: suite MCP
|
|
17
|
+
// definitions only appear after their suite is both installed and enabled.
|
|
18
|
+
if (suite.mcp === undefined || suite.installedAt === undefined || !suite.enabled)
|
|
19
|
+
continue;
|
|
20
|
+
for (const [serverKey, server] of Object.entries(suite.mcp.servers)) {
|
|
21
|
+
const serverName = deriveServerName(suite.id, serverKey);
|
|
22
|
+
const tools = observedByServer.get(serverName) ?? [];
|
|
23
|
+
claimedServers.add(serverName);
|
|
24
|
+
const diagnostic = diagnosticsByKey.get(`${suite.id}\u0000${serverKey}`);
|
|
25
|
+
const state = diagnostic !== undefined
|
|
26
|
+
? 'failed'
|
|
27
|
+
: tools.length > 0
|
|
28
|
+
? 'connected'
|
|
29
|
+
: 'degraded';
|
|
30
|
+
entries.push({
|
|
31
|
+
id: `plugin:${suite.sourceId}/${suite.id}/${serverKey}`,
|
|
32
|
+
name: serverName,
|
|
33
|
+
kind: 'plugin',
|
|
34
|
+
state,
|
|
35
|
+
source: suite.manifest.name,
|
|
36
|
+
suiteId: suite.id,
|
|
37
|
+
serverKey,
|
|
38
|
+
transport: server.type,
|
|
39
|
+
endpoint: endpointOf(server),
|
|
40
|
+
config: redactConfig(server),
|
|
41
|
+
tools: tools.map(tool => ({ name: tool.name, ...tool.description === undefined ? {} : { description: tool.description } })),
|
|
42
|
+
...diagnostic === undefined ? {} : { reason: diagnostic.reason },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [serverName, tools] of observedByServer) {
|
|
47
|
+
if (claimedServers.has(serverName))
|
|
48
|
+
continue;
|
|
49
|
+
entries.push({
|
|
50
|
+
id: `direct:${serverName}`,
|
|
51
|
+
name: serverName,
|
|
52
|
+
kind: 'direct',
|
|
53
|
+
state: 'connected',
|
|
54
|
+
transport: 'observed',
|
|
55
|
+
tools: tools.map(tool => ({ name: tool.name, ...tool.description === undefined ? {} : { description: tool.description } })),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const totals = {
|
|
59
|
+
all: entries.length,
|
|
60
|
+
connected: entries.filter(entry => entry.state === 'connected').length,
|
|
61
|
+
degraded: entries.filter(entry => entry.state === 'degraded').length,
|
|
62
|
+
failed: entries.filter(entry => entry.state === 'failed').length,
|
|
63
|
+
disabled: entries.filter(entry => entry.state === 'disabled').length,
|
|
64
|
+
};
|
|
65
|
+
return { entries, observedAt: new Date().toISOString(), totals, directObservationOnly: true };
|
|
66
|
+
}
|
|
67
|
+
function groupObservedTools(observed, knownServerNames) {
|
|
68
|
+
const grouped = new Map();
|
|
69
|
+
for (const tool of observed) {
|
|
70
|
+
if (!tool.name.startsWith('mcp__'))
|
|
71
|
+
continue;
|
|
72
|
+
let serverName;
|
|
73
|
+
let rawName;
|
|
74
|
+
// Plugin server names contain `__` themselves (`suite__server`), so use
|
|
75
|
+
// known names first and choose the longest matching namespace.
|
|
76
|
+
for (const candidate of knownServerNames) {
|
|
77
|
+
const prefix = `mcp__${candidate}__`;
|
|
78
|
+
if (tool.name.startsWith(prefix) && (serverName === undefined || candidate.length > serverName.length)) {
|
|
79
|
+
serverName = candidate;
|
|
80
|
+
rawName = tool.name.slice(prefix.length);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (serverName === undefined) {
|
|
84
|
+
const parts = tool.name.split('__');
|
|
85
|
+
if (parts.length < 3)
|
|
86
|
+
continue;
|
|
87
|
+
serverName = parts[1];
|
|
88
|
+
rawName = parts.slice(2).join('__');
|
|
89
|
+
}
|
|
90
|
+
if (serverName === undefined || rawName === undefined || rawName === '')
|
|
91
|
+
continue;
|
|
92
|
+
const list = grouped.get(serverName) ?? [];
|
|
93
|
+
list.push({ name: rawName, ...tool.description === undefined ? {} : { description: tool.description } });
|
|
94
|
+
grouped.set(serverName, list);
|
|
95
|
+
}
|
|
96
|
+
return grouped;
|
|
97
|
+
}
|
|
98
|
+
function endpointOf(server) {
|
|
99
|
+
if (server.type === 'stdio')
|
|
100
|
+
return [server.command, ...(server.args ?? [])].join(' ');
|
|
101
|
+
return server.url;
|
|
102
|
+
}
|
|
103
|
+
const SENSITIVE_KEY = /(authorization|token|secret|password|credential|api[-_]?key)/i;
|
|
104
|
+
function redactConfig(value) {
|
|
105
|
+
return redactValue(value);
|
|
106
|
+
}
|
|
107
|
+
function redactValue(value, key = '') {
|
|
108
|
+
if (SENSITIVE_KEY.test(key))
|
|
109
|
+
return '[redacted]';
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return value.map(item => redactValue(item));
|
|
112
|
+
if (typeof value === 'object' && value !== null) {
|
|
113
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [childKey, redactValue(childValue, childKey)]));
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
/** Read model-facing MCP tool names from the dsh-tools runtime when available.
|
|
118
|
+
* The registry intentionally has no public listing API in rc.6; this adapter
|
|
119
|
+
* uses only the runtime's stable `layers.merge(...).tools.entries()` data shape
|
|
120
|
+
* and safely returns an empty observation when another host changes it. */
|
|
121
|
+
export function inspectToolRegistry(runtime) {
|
|
122
|
+
if (typeof runtime !== 'object' || runtime === null)
|
|
123
|
+
return [];
|
|
124
|
+
const layers = runtime.layers;
|
|
125
|
+
if (typeof layers !== 'object' || layers === null)
|
|
126
|
+
return [];
|
|
127
|
+
const merge = layers.merge;
|
|
128
|
+
if (typeof merge !== 'function')
|
|
129
|
+
return [];
|
|
130
|
+
const empty = { entries: () => [] };
|
|
131
|
+
try {
|
|
132
|
+
const visible = merge.call(layers, undefined, layer => layer.tools ?? empty);
|
|
133
|
+
const output = [];
|
|
134
|
+
for (const [name, definition] of visible.entries()) {
|
|
135
|
+
if (!name.startsWith('mcp__'))
|
|
136
|
+
continue;
|
|
137
|
+
const description = typeof definition === 'object' && definition !== null && typeof definition.description === 'string'
|
|
138
|
+
? definition.description
|
|
139
|
+
: undefined;
|
|
140
|
+
output.push({ name, ...description === undefined ? {} : { description } });
|
|
141
|
+
}
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -46,6 +46,12 @@ export function mountSuiteRoutes(hostCtx, manager) {
|
|
|
46
46
|
get(`${API_PREFIX}overview`, async (_request, response) => {
|
|
47
47
|
sendJson(response, 200, await manager.overview());
|
|
48
48
|
});
|
|
49
|
+
get(`${API_PREFIX}mcp-status`, async (_request, response) => {
|
|
50
|
+
sendJson(response, 200, await manager.mcpStatus());
|
|
51
|
+
});
|
|
52
|
+
get(`${API_PREFIX}progress`, async (_request, response) => {
|
|
53
|
+
sendJson(response, 200, manager.sourceProgress());
|
|
54
|
+
});
|
|
49
55
|
get(`${API_PREFIX}config`, async (_request, response) => {
|
|
50
56
|
sendJson(response, 200, { sources: manager.sources });
|
|
51
57
|
});
|
package/lib/types/discovery.d.ts
CHANGED
|
@@ -8,9 +8,12 @@ export declare function discoverSuitesInSource(checkoutDir: string, sourceId: st
|
|
|
8
8
|
/** Whether a suite root path lies outside the checkout (defense for malformed marketplace sources). */
|
|
9
9
|
export declare function isOutside(root: string, candidate: string): boolean;
|
|
10
10
|
/**
|
|
11
|
-
* Discover
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Discover suites for one dimension. User catalogs only read configured
|
|
12
|
+
* sources: unmanaged user checkouts have no persisted identity and otherwise
|
|
13
|
+
* leak stale repositories into the catalog totals. Project catalogs retain
|
|
14
|
+
* unmanaged checkout ids because project state may authorize them through its
|
|
15
|
+
* installed map without duplicating source configuration. All selected
|
|
16
|
+
* checkouts are discovered concurrently so one large repository does not
|
|
17
|
+
* serialize every other repository's scan.
|
|
15
18
|
*/
|
|
16
19
|
export declare function discoverSourceList(sources: SourceRef[], dimension: SuiteDimension, dimensionRoot: string): Promise<Suite[]>;
|
package/lib/types/manager.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type McpStatusPayload, type McpToolSnapshot } from './mcp-status.js';
|
|
1
2
|
import type { OverviewPayload, SourceRef, Suite, SuiteDimension } from './types.js';
|
|
2
3
|
export interface ManagerOptions {
|
|
3
4
|
userRoot: string;
|
|
@@ -9,7 +10,20 @@ export declare class SuiteManager {
|
|
|
9
10
|
private state;
|
|
10
11
|
private mutationQueue;
|
|
11
12
|
private readonly statePath;
|
|
13
|
+
/** Cached `git rev-parse HEAD` per source id, so overview reads stay instant; cleared whenever a checkout may have moved. */
|
|
14
|
+
private readonly headCache;
|
|
15
|
+
/** Latest MCP mount diagnostics (suiteId -> reasons), fed by the host reconcile. */
|
|
16
|
+
mcpDiagnostics: Array<{
|
|
17
|
+
suiteId: string;
|
|
18
|
+
serverKey: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}>;
|
|
21
|
+
private toolSnapshotProvider;
|
|
12
22
|
constructor(options: ManagerOptions);
|
|
23
|
+
/** Install the host tool snapshot provider used by the MCP status surface. */
|
|
24
|
+
setMcpToolSnapshotProvider(provider: () => readonly McpToolSnapshot[]): void;
|
|
25
|
+
/** Build the flat MCP service inventory for the status surface. */
|
|
26
|
+
mcpStatus(): Promise<McpStatusPayload>;
|
|
13
27
|
/** Load persisted state once at plugin activation. */
|
|
14
28
|
load(): Promise<void>;
|
|
15
29
|
get sources(): SourceRef[];
|
|
@@ -50,6 +64,29 @@ export declare class SuiteManager {
|
|
|
50
64
|
branch?: string;
|
|
51
65
|
local?: boolean;
|
|
52
66
|
}): Promise<SourceRef>;
|
|
67
|
+
/**
|
|
68
|
+
* Progress snapshot of the source mutation currently in flight, if any.
|
|
69
|
+
* While present, `overview()` reports it instead of running git against
|
|
70
|
+
* the transient checkout directory.
|
|
71
|
+
*/
|
|
72
|
+
private currentSourceState;
|
|
73
|
+
/**
|
|
74
|
+
* Begin reporting progress for a source mutation: `step` names the active
|
|
75
|
+
* phase and `cloned` whether the checkout directory exists yet. The client
|
|
76
|
+
* polls this through the `progress` route while its add-source modal is
|
|
77
|
+
* open, and `overview()` merges the snapshot into the source row.
|
|
78
|
+
*/
|
|
79
|
+
beginSourceState(sourceId: string, step: string, cloned: boolean): void;
|
|
80
|
+
/** Advance the in-flight mutation's step (checkout state unchanged). */
|
|
81
|
+
updateSourceStep(step: string): void;
|
|
82
|
+
/** Stop reporting progress; `overview()` falls back to on-disk inspection. */
|
|
83
|
+
endSourceState(): void;
|
|
84
|
+
/** Progress snapshot for the `progress` route (never throws). */
|
|
85
|
+
sourceProgress(): {
|
|
86
|
+
active: boolean;
|
|
87
|
+
sourceId: string;
|
|
88
|
+
step: string;
|
|
89
|
+
};
|
|
53
90
|
private uniqueSourceId;
|
|
54
91
|
/**
|
|
55
92
|
* Update one source's url / branch / local flag. A git source whose URL
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Suite } from './types.js';
|
|
2
|
+
/** A tool observed in the host tool registry. */
|
|
3
|
+
export interface McpToolSnapshot {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
export type McpStatusKind = 'plugin' | 'direct';
|
|
8
|
+
export type McpStatusState = 'connected' | 'degraded' | 'failed' | 'disabled';
|
|
9
|
+
/** One MCP service row for the status surface. */
|
|
10
|
+
export interface McpStatusEntry {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
kind: McpStatusKind;
|
|
14
|
+
state: McpStatusState;
|
|
15
|
+
source?: string;
|
|
16
|
+
suiteId?: string;
|
|
17
|
+
serverKey?: string;
|
|
18
|
+
transport: string;
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
config?: Record<string, unknown>;
|
|
21
|
+
tools: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}>;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface McpStatusPayload {
|
|
28
|
+
entries: McpStatusEntry[];
|
|
29
|
+
observedAt: string;
|
|
30
|
+
totals: {
|
|
31
|
+
all: number;
|
|
32
|
+
connected: number;
|
|
33
|
+
degraded: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
disabled: number;
|
|
36
|
+
};
|
|
37
|
+
directObservationOnly: boolean;
|
|
38
|
+
}
|
|
39
|
+
interface McpDiagnostic {
|
|
40
|
+
suiteId: string;
|
|
41
|
+
serverKey: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
}
|
|
44
|
+
/** Build status rows from discovered plugin MCP definitions and observed tool names. */
|
|
45
|
+
export declare function buildMcpStatus(suites: Suite[], diagnostics: McpDiagnostic[], observed: readonly McpToolSnapshot[]): McpStatusPayload;
|
|
46
|
+
/** Read model-facing MCP tool names from the dsh-tools runtime when available.
|
|
47
|
+
* The registry intentionally has no public listing API in rc.6; this adapter
|
|
48
|
+
* uses only the runtime's stable `layers.merge(...).tools.entries()` data shape
|
|
49
|
+
* and safely returns an empty observation when another host changes it. */
|
|
50
|
+
export declare function inspectToolRegistry(runtime: unknown): McpToolSnapshot[];
|
|
51
|
+
export {};
|
package/lib/types/types.d.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface SuiteOverviewCard {
|
|
|
137
137
|
dimension: SuiteDimension;
|
|
138
138
|
layout: SuiteLayoutKind;
|
|
139
139
|
errors: string[];
|
|
140
|
+
/** Live MCP mount failures for this suite (reason per failed server). */
|
|
141
|
+
mcpErrors: string[];
|
|
140
142
|
}
|
|
141
143
|
export interface SourceOverview {
|
|
142
144
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-plugins-market",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.4.
|
|
3
|
+
"description": "The standard way to install Claude Code / Codex / Cursor / Kimi plugin marketplace suites in DeepSeek Harness (DSH) — zero conversion, zero file copying. Skills, MCP servers, hooks and slash commands are injected into your dsh sessions at runtime.",
|
|
4
|
+
"version": "0.4.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -56,11 +56,18 @@
|
|
|
56
56
|
"harness",
|
|
57
57
|
"dsh",
|
|
58
58
|
"dsh-plugin",
|
|
59
|
+
"dsh-plugins",
|
|
60
|
+
"dsh-marketplace",
|
|
61
|
+
"dsh-plugin-marketplace",
|
|
62
|
+
"deepseek-plugin",
|
|
63
|
+
"deepseek-plugins",
|
|
59
64
|
"agent-plugins",
|
|
60
65
|
"agent-plugin",
|
|
61
66
|
"plugin-marketplace",
|
|
62
67
|
"marketplace",
|
|
63
68
|
"claude-code",
|
|
69
|
+
"claude-code-plugins",
|
|
70
|
+
"claude-code-plugin-marketplace",
|
|
64
71
|
"claude",
|
|
65
72
|
"codex",
|
|
66
73
|
"cursor",
|