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/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
- for (const root of roots) {
28
- const suite = root.dir === undefined
29
- ? remoteSuite(sourceId, dimension, root)
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 every suite of one dimension's configured sources, plus manual
209
- * checkouts present under the dimension's `.sources/` that no source entry
210
- * names. Local sources read their directory directly; git sources read
211
- * their clone; a missing checkout contributes nothing.
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
- try {
218
- for (const entry of await readdir(checkoutRoot, { withFileTypes: true })) {
219
- if (!entry.isDirectory() || entry.name.startsWith('.') || listed.has(entry.name))
220
- continue;
221
- bySource.set(entry.name, await discoverSuitesInSource(join(checkoutRoot, entry.name), entry.name, dimension));
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
- catch {
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
- continue;
231
- bySource.set(source.id, await discoverSuitesInSource(checkout, source.id, dimension));
232
- }
233
- return [...bySource.values()].flat();
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
- try {
115
- lockCommit = await gitHead(checkout);
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
- catch {
119
- // not cloned yet (or a broken checkout); the clone attempt records its own error on refresh
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
- await this.ensureClone(source);
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
  });
@@ -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 every suite of one dimension's configured sources, plus manual
12
- * checkouts present under the dimension's `.sources/` that no source entry
13
- * names. Local sources read their directory directly; git sources read
14
- * their clone; a missing checkout contributes nothing.
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[]>;
@@ -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 {};
@@ -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": "Bring the Claude Code / Codex / Cursor plugin-marketplace ecosystem into DeepSeek Harness (DSH): install & inject agent plugins — skills, MCP servers, hooks, slash commands — from git marketplace repos (agent-plugins.org v1.0.0, .claude-plugin, .codex-plugin, .cursor-plugin, .kimi-plugin), with a Web GUI market page.",
4
- "version": "0.4.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",