dsh-agent-plugins-market 0.4.5 → 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 +4 -4
- package/README.zh.md +4 -4
- package/client/client.js +697 -63
- package/client/client.js.map +1 -1
- package/lib/discovery.js +30 -28
- package/lib/index.js +4 -0
- package/lib/manager.js +11 -0
- package/lib/mcp-status.js +147 -0
- package/lib/routes.js +3 -0
- package/lib/types/discovery.d.ts +7 -4
- package/lib/types/manager.d.ts +6 -0
- package/lib/types/mcp-status.d.ts +51 -0
- package/package.json +1 -1
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';
|
|
@@ -53,6 +54,9 @@ export function apply(ctx, config = {}) {
|
|
|
53
54
|
reconcileMounts();
|
|
54
55
|
};
|
|
55
56
|
const manager = new SuiteManager({ userRoot, dataRoot, onChanged });
|
|
57
|
+
ctx.inject(['tools'], (toolsCtx) => {
|
|
58
|
+
manager.setMcpToolSnapshotProvider(() => inspectToolRegistry(toolsCtx.tools));
|
|
59
|
+
});
|
|
56
60
|
void manager.load().then(async () => {
|
|
57
61
|
await manager.mergeSources(config.sources ?? []);
|
|
58
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 {
|
|
@@ -24,10 +25,20 @@ export class SuiteManager {
|
|
|
24
25
|
headCache = new Map();
|
|
25
26
|
/** Latest MCP mount diagnostics (suiteId -> reasons), fed by the host reconcile. */
|
|
26
27
|
mcpDiagnostics = [];
|
|
28
|
+
toolSnapshotProvider = () => [];
|
|
27
29
|
constructor(options) {
|
|
28
30
|
this.options = options;
|
|
29
31
|
this.statePath = join(options.userRoot, STATE_FILE_NAME);
|
|
30
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
|
+
}
|
|
31
42
|
/** Load persisted state once at plugin activation. */
|
|
32
43
|
async load() {
|
|
33
44
|
this.state = await loadState(this.statePath);
|
|
@@ -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,9 @@ 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
|
+
});
|
|
49
52
|
get(`${API_PREFIX}progress`, async (_request, response) => {
|
|
50
53
|
sendJson(response, 200, manager.sourceProgress());
|
|
51
54
|
});
|
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;
|
|
@@ -17,7 +18,12 @@ export declare class SuiteManager {
|
|
|
17
18
|
serverKey: string;
|
|
18
19
|
reason: string;
|
|
19
20
|
}>;
|
|
21
|
+
private toolSnapshotProvider;
|
|
20
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>;
|
|
21
27
|
/** Load persisted state once at plugin activation. */
|
|
22
28
|
load(): Promise<void>;
|
|
23
29
|
get sources(): SourceRef[];
|
|
@@ -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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-plugins-market",
|
|
3
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.
|
|
4
|
+
"version": "0.4.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|