dsh-agent-plugins-market 0.5.0 → 0.5.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 +4 -2
- package/README.zh.md +4 -2
- package/client/client.js +310 -17
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +101 -4
- package/lib/application/details.js +4 -1
- package/lib/catalog/native-project.js +73 -0
- package/lib/catalog/source-catalog.js +12 -3
- package/lib/contracts/market.js +4 -1
- package/lib/index.js +12 -5
- package/lib/model/state.js +19 -1
- package/lib/model/types.js +12 -1
- package/lib/routes.js +37 -0
- package/lib/runtime/commands-mounts.js +15 -6
- package/lib/runtime/hooks-mounts.js +3 -2
- package/lib/runtime/host-locale.js +54 -0
- package/lib/runtime/mcp-config.js +11 -4
- package/lib/runtime/mcp-mounts.js +33 -17
- package/lib/runtime/mcp-overrides.js +113 -0
- package/lib/runtime/mcp-status.js +8 -4
- package/lib/runtime/reconciler.js +7 -2
- package/lib/runtime/skills-provider.js +22 -13
- package/lib/types/application/catalog.d.ts +30 -2
- package/lib/types/application/details.d.ts +2 -2
- package/lib/types/application/queries.d.ts +5 -1
- package/lib/types/catalog/native-project.d.ts +17 -0
- package/lib/types/contracts/market.d.ts +26 -0
- package/lib/types/index.d.ts +4 -3
- package/lib/types/model/state.d.ts +1 -1
- package/lib/types/model/types.d.ts +13 -1
- package/lib/types/runtime/commands-mounts.d.ts +4 -2
- package/lib/types/runtime/host-locale.d.ts +13 -0
- package/lib/types/runtime/mcp-config.d.ts +5 -1
- package/lib/types/runtime/mcp-mounts.d.ts +12 -3
- package/lib/types/runtime/mcp-overrides.d.ts +37 -0
- package/lib/types/runtime/mcp-status.d.ts +2 -1
- package/lib/types/runtime/reconciler.d.ts +4 -1
- package/lib/types/runtime/skills-provider.d.ts +3 -1
- package/package.json +1 -1
- package/lib/context.js +0 -78
- package/lib/types/context.d.ts +0 -13
|
@@ -4,21 +4,36 @@ export class McpMountRegistry {
|
|
|
4
4
|
pluginDataRoot;
|
|
5
5
|
live = new Map();
|
|
6
6
|
names = new Map();
|
|
7
|
+
overridesProvider = async () => new Map();
|
|
8
|
+
/** Serialize mount and unmount passes so a disable cannot race an in-flight spawn. */
|
|
9
|
+
reconcileQueue = Promise.resolve();
|
|
7
10
|
constructor(ctx, pluginDataRoot) {
|
|
8
11
|
this.ctx = ctx;
|
|
9
12
|
this.pluginDataRoot = pluginDataRoot;
|
|
10
13
|
}
|
|
11
|
-
/**
|
|
14
|
+
/** Install the per-suite overrides provider (suiteId -> overrides). */
|
|
15
|
+
setOverridesProvider(provider) {
|
|
16
|
+
this.overridesProvider = provider;
|
|
17
|
+
}
|
|
18
|
+
/** Queue one reconciliation behind any in-flight mount/unmount pass. */
|
|
12
19
|
async reconcile(enabledSuites) {
|
|
20
|
+
const run = this.reconcileQueue.then(() => this.reconcileNow(enabledSuites));
|
|
21
|
+
this.reconcileQueue = run.then(() => undefined, () => undefined);
|
|
22
|
+
return run;
|
|
23
|
+
}
|
|
24
|
+
/** Mount/unmount MCP servers to match the enabled suites exactly. */
|
|
25
|
+
async reconcileNow(enabledSuites) {
|
|
26
|
+
const active = enabledSuites.filter(suite => suite.activeSurfaces?.mcp !== false);
|
|
27
|
+
const overrides = await this.overridesProvider();
|
|
13
28
|
const wanted = new Map();
|
|
14
29
|
const diagnostics = [];
|
|
15
|
-
for (const suite of
|
|
16
|
-
const { mounts, failures } = toMcpMounts(suite, this.pluginDataRoot);
|
|
30
|
+
for (const suite of active) {
|
|
31
|
+
const { mounts, failures } = toMcpMounts(suite, this.pluginDataRoot, overrides.get(suite.id));
|
|
17
32
|
for (const failure of failures) {
|
|
18
33
|
diagnostics.push({ suiteId: suite.id, serverKey: failure.serverKey, reason: failure.reason });
|
|
19
34
|
}
|
|
20
35
|
for (const mount of mounts) {
|
|
21
|
-
wanted.set(mountKey(mount.suiteId, mount.serverKey), { suite, serverKey: mount.serverKey });
|
|
36
|
+
wanted.set(mountKey(mount.suiteId, mount.serverKey), { suite, serverKey: mount.serverKey, request: mount });
|
|
22
37
|
}
|
|
23
38
|
}
|
|
24
39
|
for (const [key, live] of [...this.live]) {
|
|
@@ -30,23 +45,24 @@ export class McpMountRegistry {
|
|
|
30
45
|
for (const [key, entry] of wanted) {
|
|
31
46
|
if (this.live.has(key))
|
|
32
47
|
continue;
|
|
33
|
-
const reason = await this.
|
|
48
|
+
const reason = await this.mountWith(entry.request);
|
|
34
49
|
if (reason !== undefined)
|
|
35
50
|
diagnostics.push({ suiteId: entry.suite.id, serverKey: entry.serverKey, reason });
|
|
36
51
|
}
|
|
37
52
|
return diagnostics.filter(diagnostic => diagnostic.reason !== 'unmounted');
|
|
38
53
|
}
|
|
39
|
-
/** Dispose every live mount
|
|
54
|
+
/** Dispose every live mount after queued reconciliation passes settle. */
|
|
40
55
|
async disposeAll() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
const run = this.reconcileQueue.then(async () => {
|
|
57
|
+
for (const [key, live] of [...this.live]) {
|
|
58
|
+
await this.unmount(key, live);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.reconcileQueue = run.then(() => undefined, () => undefined);
|
|
62
|
+
await run;
|
|
44
63
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const request = mounts.find(mount => mount.serverKey === serverKey);
|
|
48
|
-
if (request === undefined)
|
|
49
|
-
return 'server no longer present in mcp.json';
|
|
64
|
+
/** Mount one precomputed request (source config merged with overrides). */
|
|
65
|
+
async mountWith(request) {
|
|
50
66
|
const owner = this.names.get(request.config.serverName);
|
|
51
67
|
if (owner !== undefined)
|
|
52
68
|
return `derived serverName "${request.config.serverName}" already mounted by ${owner}`;
|
|
@@ -63,9 +79,9 @@ export class McpMountRegistry {
|
|
|
63
79
|
try {
|
|
64
80
|
const handle = mountCtx.plugin(mcpClient, request.config);
|
|
65
81
|
await handle.await();
|
|
66
|
-
const key = mountKey(
|
|
67
|
-
this.live.set(key, { suiteId:
|
|
68
|
-
this.names.set(request.config.serverName, `${
|
|
82
|
+
const key = mountKey(request.suiteId, request.serverKey);
|
|
83
|
+
this.live.set(key, { suiteId: request.suiteId, serverKey: request.serverKey, serverName: request.config.serverName, disposer: () => handle.dispose() });
|
|
84
|
+
this.names.set(request.config.serverName, `${request.suiteId}/${request.serverKey}`);
|
|
69
85
|
return undefined;
|
|
70
86
|
}
|
|
71
87
|
catch (error) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server overrides: user-owned edits layered over a suite's read-only
|
|
3
|
+
* `mcp.json`.
|
|
4
|
+
*
|
|
5
|
+
* The suite's mcp.json is source-controlled content — editing it in place
|
|
6
|
+
* would be clobbered by the next refresh and would blur authorship. Overrides
|
|
7
|
+
* live in `${dataRoot}/overrides/<suiteId>.json` and win at mount time.
|
|
8
|
+
* Editable scope is deliberate: connection inputs (`url`, `headers`, `env`,
|
|
9
|
+
* `args`) and enable/disable are local-adaptation concerns; transport type,
|
|
10
|
+
* command, and the derived serverName stay source-owned so mounts stay
|
|
11
|
+
* coherent across refreshes. Secret values should reference the process
|
|
12
|
+
* environment through `${NAME}` placeholders so keys never persist in plain
|
|
13
|
+
* text.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
/** The overrides directory under the plugin data root. */
|
|
18
|
+
export function overridesDir(dataRoot) {
|
|
19
|
+
return join(dataRoot, 'overrides');
|
|
20
|
+
}
|
|
21
|
+
/** One suite's override file path. */
|
|
22
|
+
export function suiteOverridePath(dataRoot, suiteId) {
|
|
23
|
+
return join(overridesDir(dataRoot), `${sanitizeFileName(suiteId)}.json`);
|
|
24
|
+
}
|
|
25
|
+
/** Load one suite's persisted overrides; unreadable files yield none. */
|
|
26
|
+
export async function loadSuiteOverrides(dataRoot, suiteId) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(await readFile(suiteOverridePath(dataRoot, suiteId), 'utf8'));
|
|
29
|
+
if (typeof raw !== 'object' || raw === null)
|
|
30
|
+
return {};
|
|
31
|
+
const parsed = sanitizeOverrides(raw);
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Persist one suite's overrides atomically-ish (single writer, small file). */
|
|
39
|
+
export async function saveSuiteOverrides(dataRoot, suiteId, overrides) {
|
|
40
|
+
const path = suiteOverridePath(dataRoot, suiteId);
|
|
41
|
+
await mkdir(dirname(path), { recursive: true });
|
|
42
|
+
await writeFile(path, `${JSON.stringify(overrides, null, 2)}\n`, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
/** Keep only recognized fields with correct shapes; drop everything else. */
|
|
45
|
+
export function sanitizeOverrides(raw) {
|
|
46
|
+
if (typeof raw !== 'object' || raw === null)
|
|
47
|
+
return {};
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [serverKey, value] of Object.entries(raw)) {
|
|
50
|
+
if (typeof value !== 'object' || value === null)
|
|
51
|
+
continue;
|
|
52
|
+
const record = value;
|
|
53
|
+
const override = {};
|
|
54
|
+
if (typeof record['enabled'] === 'boolean')
|
|
55
|
+
override.enabled = record['enabled'];
|
|
56
|
+
if (typeof record['url'] === 'string' && record['url'] !== '')
|
|
57
|
+
override.url = record['url'];
|
|
58
|
+
const headers = stringMap(record['headers']);
|
|
59
|
+
if (headers !== undefined)
|
|
60
|
+
override.headers = headers;
|
|
61
|
+
const env = stringMap(record['env']);
|
|
62
|
+
if (env !== undefined)
|
|
63
|
+
override.env = env;
|
|
64
|
+
if (Array.isArray(record['args']) && record['args'].every(entry => typeof entry === 'string')) {
|
|
65
|
+
override.args = record['args'];
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(override).length > 0)
|
|
68
|
+
result[serverKey] = override;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
/** Validate a client-supplied patch for one server into an override value. */
|
|
73
|
+
export function sanitizeOverridePatch(patch) {
|
|
74
|
+
return sanitizeOverrides({ server: patch })['server'];
|
|
75
|
+
}
|
|
76
|
+
function stringMap(value) {
|
|
77
|
+
if (typeof value !== 'object' || value === null)
|
|
78
|
+
return undefined;
|
|
79
|
+
const result = {};
|
|
80
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
81
|
+
if (typeof entry !== 'string')
|
|
82
|
+
continue;
|
|
83
|
+
result[key] = entry;
|
|
84
|
+
}
|
|
85
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Apply one server's overrides onto its source shape. Field replacement is
|
|
89
|
+
* whole-map (headers/env/args), never per-key merge, so removing a key in the
|
|
90
|
+
* UI actually removes it at mount time. Values pass through the ordinary
|
|
91
|
+
* mount-time placeholder pipeline afterwards, so override authors may
|
|
92
|
+
* reference secrets as `${NAME}` or `${NAME:-fallback}` (resolved from the
|
|
93
|
+
* process environment in memory) instead of persisting them here.
|
|
94
|
+
*/
|
|
95
|
+
export function applyOverride(server, override) {
|
|
96
|
+
if (override === undefined)
|
|
97
|
+
return server;
|
|
98
|
+
if (server.type === 'stdio') {
|
|
99
|
+
return {
|
|
100
|
+
...server,
|
|
101
|
+
...(override.args !== undefined ? { args: override.args } : {}),
|
|
102
|
+
...(override.env !== undefined ? { env: override.env } : {})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...server,
|
|
107
|
+
...(override.url !== undefined ? { url: override.url } : {}),
|
|
108
|
+
...(override.headers !== undefined ? { headers: override.headers } : {})
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function sanitizeFileName(raw) {
|
|
112
|
+
return raw.replace(/[^A-Za-z0-9_-]+/g, '_');
|
|
113
|
+
}
|
|
@@ -2,7 +2,7 @@ import { deriveServerName } from './mcp-config.js';
|
|
|
2
2
|
import { inspectToolRegistry } from './tool-registry-observer.js';
|
|
3
3
|
export { inspectToolRegistry };
|
|
4
4
|
/** Build status rows from discovered plugin MCP definitions and observed tool names. */
|
|
5
|
-
export function buildMcpStatus(suites, diagnostics, observed) {
|
|
5
|
+
export function buildMcpStatus(suites, diagnostics, observed, overrides = new Map()) {
|
|
6
6
|
const entries = [];
|
|
7
7
|
const claimedServers = new Set();
|
|
8
8
|
const knownServerNames = new Set();
|
|
@@ -19,12 +19,16 @@ export function buildMcpStatus(suites, diagnostics, observed) {
|
|
|
19
19
|
// definitions only appear after their suite is both installed and enabled.
|
|
20
20
|
if (suite.mcp === undefined || suite.installedAt === undefined || !suite.enabled)
|
|
21
21
|
continue;
|
|
22
|
+
const suiteOverrides = overrides.get(suite.id);
|
|
22
23
|
for (const [serverKey, server] of Object.entries(suite.mcp.servers)) {
|
|
24
|
+
const override = suiteOverrides?.[serverKey];
|
|
23
25
|
const serverName = deriveServerName(suite.id, serverKey);
|
|
24
26
|
const tools = observedByServer.get(serverName) ?? [];
|
|
25
27
|
claimedServers.add(serverName);
|
|
26
28
|
const diagnostic = diagnosticsByKey.get(`${suite.id}\u0000${serverKey}`);
|
|
27
|
-
const
|
|
29
|
+
const disabled = override?.enabled === false;
|
|
30
|
+
const state = diagnostic !== undefined ? 'failed' : disabled ? 'disabled' : tools.length > 0 ? 'connected' : 'degraded';
|
|
31
|
+
const reason = diagnostic?.reason ?? (override === undefined ? undefined : disabled ? 'disabled by override' : 'modified by override');
|
|
28
32
|
entries.push({
|
|
29
33
|
id: `plugin:${suite.sourceId}/${suite.id}/${serverKey}`,
|
|
30
34
|
name: serverName,
|
|
@@ -36,8 +40,8 @@ export function buildMcpStatus(suites, diagnostics, observed) {
|
|
|
36
40
|
transport: server.type,
|
|
37
41
|
endpoint: endpointOf(server),
|
|
38
42
|
config: redactConfig(server),
|
|
39
|
-
tools: tools.map(tool => ({ name: tool.name, ...(tool.description === undefined ? {} : { description: tool.description }) })),
|
|
40
|
-
...(
|
|
43
|
+
tools: disabled ? [] : tools.map(tool => ({ name: tool.name, ...(tool.description === undefined ? {} : { description: tool.description }) })),
|
|
44
|
+
...(reason === undefined ? {} : { reason })
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
47
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { CommandMountRegistry } from './commands-mounts.js';
|
|
2
2
|
import { HooksMountRegistry } from './hooks-mounts.js';
|
|
3
3
|
import { McpMountRegistry } from './mcp-mounts.js';
|
|
4
|
+
import { bindHostLocale } from './host-locale.js';
|
|
4
5
|
export class RuntimeReconciler {
|
|
5
6
|
mcp;
|
|
6
7
|
commands;
|
|
7
8
|
hooks;
|
|
8
|
-
constructor(ctx, dataRoot) {
|
|
9
|
+
constructor(ctx, dataRoot, t = bindHostLocale(undefined)) {
|
|
9
10
|
this.mcp = new McpMountRegistry(ctx, dataRoot);
|
|
10
|
-
this.commands = new CommandMountRegistry(ctx);
|
|
11
|
+
this.commands = new CommandMountRegistry(ctx, t);
|
|
11
12
|
this.hooks = new HooksMountRegistry(ctx);
|
|
12
13
|
}
|
|
14
|
+
/** Install the per-suite MCP overrides provider used at mount time. */
|
|
15
|
+
setMcpOverridesProvider(provider) {
|
|
16
|
+
this.mcp.setOverridesProvider(provider);
|
|
17
|
+
}
|
|
13
18
|
/** Reconcile all runtime surfaces against one enabled-suite snapshot. */
|
|
14
19
|
async reconcile(enabledSuites) {
|
|
15
20
|
const suites = [...enabledSuites];
|
|
@@ -15,23 +15,39 @@
|
|
|
15
15
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { parseSkillFrontmatter, stripFrontmatter } from '../catalog/skills-parse.js';
|
|
18
|
+
import { bindHostLocale } from './host-locale.js';
|
|
18
19
|
export const SUITE_PROJECT_SOURCE = 'agent-plugin-project';
|
|
19
20
|
export const SUITE_USER_SOURCE = 'agent-plugin-user';
|
|
20
21
|
const PROJECT_RANK = 250;
|
|
21
22
|
const USER_RANK = 450;
|
|
22
23
|
export class SuiteSkillProvider {
|
|
23
24
|
manager;
|
|
25
|
+
t;
|
|
24
26
|
name = 'agent-plugin';
|
|
25
|
-
constructor(manager) {
|
|
27
|
+
constructor(manager, t = bindHostLocale(undefined)) {
|
|
26
28
|
this.manager = manager;
|
|
29
|
+
this.t = t;
|
|
27
30
|
}
|
|
28
31
|
async list(options) {
|
|
29
32
|
const located = await this.locate(options.cwd);
|
|
30
33
|
located.sort((a, b) => a.rank - b.rank || a.suite.id.localeCompare(b.suite.id) || a.skill.name.localeCompare(b.skill.name));
|
|
31
|
-
|
|
34
|
+
// Dedupe by skill name across dimensions: the sort above puts project
|
|
35
|
+
// suites (250) ahead of user suites (450), so a project's own skill
|
|
36
|
+
// always wins over an installed suite shipping a skill of the same name.
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const unique = [];
|
|
39
|
+
for (const entry of located) {
|
|
40
|
+
if (seen.has(entry.skill.name))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(entry.skill.name);
|
|
43
|
+
unique.push(entry);
|
|
44
|
+
}
|
|
45
|
+
return unique.map(entry => this.candidateFor(entry));
|
|
32
46
|
}
|
|
33
47
|
/** Parse `agents/*.md` of one suite into agent-definition candidates. */
|
|
34
48
|
async agentsOf(suite) {
|
|
49
|
+
if (suite.activeSurfaces?.agents === false)
|
|
50
|
+
return [];
|
|
35
51
|
const dir = join(suite.root, 'agents');
|
|
36
52
|
let entries;
|
|
37
53
|
try {
|
|
@@ -91,15 +107,7 @@ export class SuiteSkillProvider {
|
|
|
91
107
|
}
|
|
92
108
|
if (locator.kind === 'agent') {
|
|
93
109
|
const suiteName = locator.suiteRoot.split(/[\\/]/).at(-1) ?? 'suite';
|
|
94
|
-
const content = [
|
|
95
|
-
`## 子代理定义(来自 Agent Plugins ${suiteName},Claude Code agents 格式)`,
|
|
96
|
-
'',
|
|
97
|
-
'当任务匹配下方描述时,通过 subagent 工具创建子代理并将「定义正文」原样作为指令执行。',
|
|
98
|
-
'',
|
|
99
|
-
'```markdown',
|
|
100
|
-
text,
|
|
101
|
-
'```'
|
|
102
|
-
].join('\n');
|
|
110
|
+
const content = [this.t('agentDefinitionTitle', { suite: suiteName }), '', this.t('agentDefinitionIntro'), '', '```markdown', text, '```'].join('\n');
|
|
103
111
|
return {
|
|
104
112
|
name: candidate.name,
|
|
105
113
|
description: candidate.description,
|
|
@@ -150,7 +158,7 @@ export class SuiteSkillProvider {
|
|
|
150
158
|
const located = [];
|
|
151
159
|
const userSuites = await this.manager.enabledUserSuites();
|
|
152
160
|
for (const suite of userSuites) {
|
|
153
|
-
for (const skill of suite.skills) {
|
|
161
|
+
for (const skill of suite.activeSurfaces?.skills === false ? [] : suite.skills) {
|
|
154
162
|
located.push({ rank: USER_RANK, source: SUITE_USER_SOURCE, suite, skill });
|
|
155
163
|
}
|
|
156
164
|
located.push(...(await this.agentsOf(suite)));
|
|
@@ -164,9 +172,10 @@ export class SuiteSkillProvider {
|
|
|
164
172
|
const snapshot = await this.manager.readProjectCatalog(cwd);
|
|
165
173
|
const located = [];
|
|
166
174
|
for (const suite of snapshot.enabledSuites) {
|
|
167
|
-
for (const skill of suite.skills) {
|
|
175
|
+
for (const skill of suite.activeSurfaces?.skills === false ? [] : suite.skills) {
|
|
168
176
|
located.push({ rank: PROJECT_RANK, source: SUITE_PROJECT_SOURCE, suite, skill });
|
|
169
177
|
}
|
|
178
|
+
located.push(...(await this.agentsOf(suite)));
|
|
170
179
|
}
|
|
171
180
|
return located;
|
|
172
181
|
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { type McpToolSnapshot } from '../runtime/mcp-status.js';
|
|
2
|
+
import { type McpServerOverride, type McpSuiteOverrides } from '../runtime/mcp-overrides.js';
|
|
2
3
|
import type { McpStatusPayload } from '../contracts/mcp-status.js';
|
|
3
4
|
import type { OverviewPayload, SkillContent, SuiteDetail } from '../contracts/market.js';
|
|
4
|
-
import type
|
|
5
|
+
import { type SourceRef, type Suite, type SuiteDimension, type SuiteSurfaceKey } from '../model/types.js';
|
|
5
6
|
/** Dependencies and host callback used by the catalog application module. */
|
|
6
7
|
export interface CatalogOptions {
|
|
7
8
|
userRoot: string;
|
|
8
9
|
dataRoot: string;
|
|
9
10
|
onChanged: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Freshness window for cached project-dimension snapshots. The project
|
|
13
|
+
* catalog sits on the skill-list hot path (every provider `list()` call),
|
|
14
|
+
* and its inputs — the project's own files — change only through editor
|
|
15
|
+
* saves; a short window keeps listing cheap without going stale. Defaults
|
|
16
|
+
* to 5 seconds; 0 disables caching.
|
|
17
|
+
*/
|
|
18
|
+
projectSnapshotTtlMs?: number;
|
|
10
19
|
}
|
|
11
20
|
/** A coherent discovered-and-installed view for one catalog dimension. */
|
|
12
21
|
export interface CatalogSnapshot {
|
|
@@ -25,6 +34,10 @@ export declare class Catalog {
|
|
|
25
34
|
private revision;
|
|
26
35
|
private userSnapshot;
|
|
27
36
|
private userSnapshotPromise;
|
|
37
|
+
/** Project-dimension snapshots keyed by project root, with TTL freshness. */
|
|
38
|
+
private readonly projectSnapshots;
|
|
39
|
+
private readonly projectSnapshotPromises;
|
|
40
|
+
private readonly projectSnapshotTtlMs;
|
|
28
41
|
/** Latest MCP mount diagnostics (suiteId -> reasons), fed by host reconcile. */
|
|
29
42
|
mcpDiagnostics: Array<{
|
|
30
43
|
suiteId: string;
|
|
@@ -39,6 +52,16 @@ export declare class Catalog {
|
|
|
39
52
|
subscribe(listener: () => void): () => void;
|
|
40
53
|
/** Build the flat MCP service inventory for the status surface. */
|
|
41
54
|
mcpStatus(): Promise<McpStatusPayload>;
|
|
55
|
+
/** One suite's persisted MCP overrides. */
|
|
56
|
+
mcpOverrides(suiteId: string): Promise<McpSuiteOverrides>;
|
|
57
|
+
/**
|
|
58
|
+
* Set or clear one server's MCP override and remount it. `override === null`
|
|
59
|
+
* clears the override (back to the source config). The serverKey must exist
|
|
60
|
+
* in the suite's parsed mcp.json.
|
|
61
|
+
*/
|
|
62
|
+
setMcpOverride(sourceId: string, suiteId: string, serverKey: string, override: McpServerOverride | null): Promise<void>;
|
|
63
|
+
/** All persisted MCP overrides keyed by suite id (mount-time provider). */
|
|
64
|
+
allMcpOverrides(): Promise<Map<string, McpSuiteOverrides>>;
|
|
42
65
|
/** Load persisted user state once at plugin activation. */
|
|
43
66
|
load(): Promise<void>;
|
|
44
67
|
get sources(): SourceRef[];
|
|
@@ -48,6 +71,7 @@ export declare class Catalog {
|
|
|
48
71
|
readUserCatalog(): Promise<CatalogSnapshot>;
|
|
49
72
|
/** Read one coherent project-dimension snapshot for a workspace cwd. */
|
|
50
73
|
readProjectCatalog(cwd: string): Promise<CatalogSnapshot>;
|
|
74
|
+
private buildProjectSnapshot;
|
|
51
75
|
/** One suite's full detail for the market detail modal. */
|
|
52
76
|
suiteDetail(sourceId: string, suiteId: string): Promise<SuiteDetail>;
|
|
53
77
|
/** One skill's full SKILL.md text for the market detail modal. */
|
|
@@ -94,12 +118,16 @@ export declare class Catalog {
|
|
|
94
118
|
removeSource(sourceId: string): Promise<void>;
|
|
95
119
|
/** Refresh one source checkout, or every source when sourceId is omitted. */
|
|
96
120
|
refreshSource(sourceId?: string): Promise<void>;
|
|
97
|
-
/** Install a suite from a source and enable it.
|
|
121
|
+
/** Install a suite from a source and enable it. The client confirms the
|
|
122
|
+
* injected surfaces (skills / MCP / hooks / commands) in a pre-install
|
|
123
|
+
* dialog before this runs; disabling afterwards is always available. */
|
|
98
124
|
install(sourceId: string, suiteId: string): Promise<void>;
|
|
99
125
|
/** Uninstall a suite while retaining the source checkout. */
|
|
100
126
|
uninstall(sourceId: string, suiteId: string): Promise<void>;
|
|
101
127
|
/** Enable or disable an installed suite. */
|
|
102
128
|
setEnabled(sourceId: string, suiteId: string, enabled: boolean): Promise<void>;
|
|
129
|
+
/** Enable or disable one runtime surface of an installed suite. */
|
|
130
|
+
setSurface(sourceId: string, suiteId: string, surface: SuiteSurfaceKey, enabled: boolean): Promise<void>;
|
|
103
131
|
/** Append config-seeded sources missing from user state and persist them. */
|
|
104
132
|
mergeSources(sources: SourceRef[]): Promise<void>;
|
|
105
133
|
private setInstalled;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { SkillContent, SuiteDetail } from '../contracts/market.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type InstalledEntry, type Suite } from '../model/types.js';
|
|
3
3
|
interface McpDiagnostic {
|
|
4
4
|
suiteId: string;
|
|
5
5
|
serverKey: string;
|
|
6
6
|
reason: string;
|
|
7
7
|
}
|
|
8
8
|
/** Build the detail response for one normalized suite. */
|
|
9
|
-
export declare function buildSuiteDetail(suite: Suite, installed: InstalledEntry | undefined, diagnostics: readonly McpDiagnostic[]): Promise<SuiteDetail>;
|
|
9
|
+
export declare function buildSuiteDetail(suite: Suite, installed: InstalledEntry | undefined, diagnostics: readonly McpDiagnostic[], mcpOverrides?: Record<string, Record<string, unknown>>): Promise<SuiteDetail>;
|
|
10
10
|
/** Read one skill body from a normalized suite. */
|
|
11
11
|
export declare function readSkillContent(suite: Suite, skillName: string): Promise<SkillContent>;
|
|
12
12
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Narrow application interfaces consumed by the HTTP transport adapter. */
|
|
2
2
|
import type { McpStatusPayload } from '../contracts/mcp-status.js';
|
|
3
3
|
import type { OverviewPayload, SkillContent, SourceProgress, SuiteDetail } from '../contracts/market.js';
|
|
4
|
-
import type { SourceRef } from '../model/types.js';
|
|
4
|
+
import type { SourceRef, SuiteSurfaceKey } from '../model/types.js';
|
|
5
|
+
import type { McpServerOverride, McpSuiteOverrides } from '../runtime/mcp-overrides.js';
|
|
5
6
|
/** Read-only market operations required by HTTP routes. */
|
|
6
7
|
export interface MarketQueries {
|
|
7
8
|
readonly sources: SourceRef[];
|
|
@@ -10,6 +11,7 @@ export interface MarketQueries {
|
|
|
10
11
|
sourceProgress(): SourceProgress;
|
|
11
12
|
suiteDetail(sourceId: string, suiteId: string): Promise<SuiteDetail>;
|
|
12
13
|
skillContent(sourceId: string, suiteId: string, skillName: string): Promise<SkillContent>;
|
|
14
|
+
mcpOverrides(suiteId: string): Promise<McpSuiteOverrides>;
|
|
13
15
|
}
|
|
14
16
|
/** Mutating market operations required by HTTP routes. */
|
|
15
17
|
export interface MarketMutations {
|
|
@@ -28,6 +30,8 @@ export interface MarketMutations {
|
|
|
28
30
|
install(sourceId: string, suiteId: string): Promise<void>;
|
|
29
31
|
uninstall(sourceId: string, suiteId: string): Promise<void>;
|
|
30
32
|
setEnabled(sourceId: string, suiteId: string, enabled: boolean): Promise<void>;
|
|
33
|
+
setSurface(sourceId: string, suiteId: string, surface: SuiteSurfaceKey, enabled: boolean): Promise<void>;
|
|
34
|
+
setMcpOverride(sourceId: string, suiteId: string, serverKey: string, override: McpServerOverride | null): Promise<void>;
|
|
31
35
|
}
|
|
32
36
|
/** Complete application surface required by the HTTP routes. */
|
|
33
37
|
export type MarketService = MarketQueries & MarketMutations;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Suite, SuiteDimension } from '../model/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Project-local directory conventions, in precedence order. Each entry maps
|
|
4
|
+
* one agent convention onto the content subdirectories it owns.
|
|
5
|
+
*/
|
|
6
|
+
export declare const NATIVE_PROJECT_DIRS: readonly NativeProjectDir[];
|
|
7
|
+
export interface NativeProjectDir {
|
|
8
|
+
dirName: string;
|
|
9
|
+
label: string;
|
|
10
|
+
subdirs: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Discover native project-layout suites under a project root. Each
|
|
14
|
+
* convention directory that carries content becomes one synthetic suite;
|
|
15
|
+
* empty or absent directories contribute nothing.
|
|
16
|
+
*/
|
|
17
|
+
export declare function discoverNativeProjectSuites(projectRoot: string, dimension: SuiteDimension): Promise<Suite[]>;
|
|
@@ -16,6 +16,9 @@ export declare const MARKET_ROUTES: {
|
|
|
16
16
|
readonly install: "/api/agent-plugins/install";
|
|
17
17
|
readonly uninstall: "/api/agent-plugins/uninstall";
|
|
18
18
|
readonly setEnabled: "/api/agent-plugins/set-enabled";
|
|
19
|
+
readonly setSurface: "/api/agent-plugins/set-surface";
|
|
20
|
+
readonly mcpOverrides: "/api/agent-plugins/mcp-overrides";
|
|
21
|
+
readonly setMcpOverride: "/api/agent-plugins/set-mcp-override";
|
|
19
22
|
};
|
|
20
23
|
/** A configured source row returned to the market client. */
|
|
21
24
|
export interface SourceOverview {
|
|
@@ -37,6 +40,24 @@ export interface SuiteSurfaceCounts {
|
|
|
37
40
|
agents: number;
|
|
38
41
|
lsp: number;
|
|
39
42
|
}
|
|
43
|
+
/** Effective per-surface enablement of an installed suite. */
|
|
44
|
+
export interface SuiteSurfaceToggles {
|
|
45
|
+
skills: boolean;
|
|
46
|
+
mcp: boolean;
|
|
47
|
+
hooks: boolean;
|
|
48
|
+
commands: boolean;
|
|
49
|
+
agents: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** One server's persisted MCP override (wire shape mirrors runtime types). */
|
|
52
|
+
export type McpServerOverrideWire = {
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
url?: string;
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
env?: Record<string, string>;
|
|
57
|
+
args?: string[];
|
|
58
|
+
};
|
|
59
|
+
/** Overrides for one suite keyed by mcp.json server key. */
|
|
60
|
+
export type McpSuiteOverridesWire = Record<string, McpServerOverrideWire>;
|
|
40
61
|
/** A normalized suite card returned by the overview route. */
|
|
41
62
|
export interface SuiteOverviewCard {
|
|
42
63
|
sourceId: string;
|
|
@@ -48,6 +69,8 @@ export interface SuiteOverviewCard {
|
|
|
48
69
|
surfaces: SuiteSurfaceCounts;
|
|
49
70
|
enabled: boolean;
|
|
50
71
|
installed: boolean;
|
|
72
|
+
/** Per-surface toggles; present on installed suites only. */
|
|
73
|
+
surfaceToggles?: SuiteSurfaceToggles;
|
|
51
74
|
dimension: string;
|
|
52
75
|
layout: string;
|
|
53
76
|
remoteUrl?: string;
|
|
@@ -128,6 +151,9 @@ export interface SuiteDetail {
|
|
|
128
151
|
remoteUrl: string | null;
|
|
129
152
|
installed: boolean;
|
|
130
153
|
enabled: boolean;
|
|
154
|
+
surfaceToggles: SuiteSurfaceToggles | null;
|
|
155
|
+
/** Persisted per-server MCP overrides (suite's mcp.json stays source-owned). */
|
|
156
|
+
mcpOverrides?: McpSuiteOverridesWire;
|
|
131
157
|
skills: SuiteSkillMeta[];
|
|
132
158
|
mcpServers: McpServerDetail[];
|
|
133
159
|
hooks: {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Function plugin (named exports, no default export). It registers one skill
|
|
5
5
|
* provider feeding enabled suites into `ctx.skills`, reconciles enabled
|
|
6
|
-
* suites' `mcp.json` servers into live `dsh-mcp-client` mounts,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* suites' `mcp.json` servers into live `dsh-mcp-client` mounts, and mounts the
|
|
7
|
+
* market page's HTTP routes on the web server. Skills and MCP tools are
|
|
8
|
+
* exposed through the host's native model surfaces; this plugin does not
|
|
9
|
+
* register a redundant suite-inventory model tool.
|
|
9
10
|
*
|
|
10
11
|
* Requires `ctx.skills` (the dsh skill registry). The MCP client package is
|
|
11
12
|
* optional at runtime: without it, suites still load their skills and the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SuiteState } from './types.js';
|
|
2
2
|
export declare const EMPTY_STATE: SuiteState;
|
|
3
3
|
/** Parse persisted state; unreadable or wrong-version files yield a contained empty state. */
|
|
4
4
|
export declare function loadState(statePath: string): Promise<SuiteState>;
|
|
@@ -19,7 +19,7 @@ export interface SourceRef {
|
|
|
19
19
|
local?: boolean;
|
|
20
20
|
}
|
|
21
21
|
/** The manifest layout a suite root was discovered under. */
|
|
22
|
-
export type SuiteLayoutKind = 'agent-plugin-v1' | 'universal' | 'claude-code' | 'cursor' | 'kimi' | 'codex' | 'skill-collection' | 'remote';
|
|
22
|
+
export type SuiteLayoutKind = 'agent-plugin-v1' | 'universal' | 'claude-code' | 'cursor' | 'kimi' | 'codex' | 'skill-collection' | 'remote' | 'project-native';
|
|
23
23
|
/** Normalized suite manifest fields. */
|
|
24
24
|
export interface SuiteManifest {
|
|
25
25
|
layout: SuiteLayoutKind;
|
|
@@ -100,6 +100,8 @@ export interface Suite {
|
|
|
100
100
|
surfaces: SuiteSurfaceCounts;
|
|
101
101
|
dimension: SuiteDimension;
|
|
102
102
|
enabled: boolean;
|
|
103
|
+
/** Effective per-surface enablement (overrides merged over enabled defaults). */
|
|
104
|
+
activeSurfaces?: Record<SuiteSurfaceKey, boolean>;
|
|
103
105
|
lockCommit?: string;
|
|
104
106
|
installedAt?: string;
|
|
105
107
|
/** Remote marketplace reference (not cloned): the source URL plus the
|
|
@@ -111,11 +113,21 @@ export interface Suite {
|
|
|
111
113
|
* broken manifest are still exposed when they parse, per spec §7.1. */
|
|
112
114
|
errors: string[];
|
|
113
115
|
}
|
|
116
|
+
/** Runtime surfaces that can be selectively enabled per installed suite. */
|
|
117
|
+
export type SuiteSurfaceKey = 'skills' | 'mcp' | 'hooks' | 'commands' | 'agents';
|
|
118
|
+
/** Per-surface user overrides; absent keys default to enabled. */
|
|
119
|
+
export type SurfaceOverrides = Partial<Record<SuiteSurfaceKey, boolean>>;
|
|
120
|
+
/** The full set of toggleable surfaces, in display order. */
|
|
121
|
+
export declare const SUITE_SURFACE_KEYS: readonly SuiteSurfaceKey[];
|
|
122
|
+
/** Merge user overrides over the enabled default into the effective surface set. */
|
|
123
|
+
export declare function effectiveSurfaces(overrides: SurfaceOverrides | undefined): Record<SuiteSurfaceKey, boolean>;
|
|
114
124
|
/** Persisted install entry, keyed `${sourceId}/${suiteId}`. */
|
|
115
125
|
export interface InstalledEntry {
|
|
116
126
|
enabled: boolean;
|
|
117
127
|
lockCommit?: string;
|
|
118
128
|
installedAt: string;
|
|
129
|
+
/** Per-surface enable overrides; absent keys default to enabled. */
|
|
130
|
+
surfaces?: SurfaceOverrides;
|
|
119
131
|
}
|
|
120
132
|
/** The persisted suite state file (`<dimensionRoot>/state.json`). */
|
|
121
133
|
export interface SuiteState {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { Suite } from '../model/types.js';
|
|
3
|
+
import { type HostTranslate } from './host-locale.js';
|
|
3
4
|
export interface CommandMountDiagnostic {
|
|
4
5
|
suiteId: string;
|
|
5
6
|
command: string;
|
|
@@ -13,8 +14,9 @@ interface CommandSpec {
|
|
|
13
14
|
}
|
|
14
15
|
export declare class CommandMountRegistry {
|
|
15
16
|
private readonly ctx;
|
|
17
|
+
private readonly t;
|
|
16
18
|
private readonly live;
|
|
17
|
-
constructor(ctx: Context);
|
|
19
|
+
constructor(ctx: Context, t?: HostTranslate);
|
|
18
20
|
/** Register/unregister suite commands and agent-commands to match the enabled suites exactly. */
|
|
19
21
|
reconcile(enabledSuites: Suite[]): Promise<CommandMountDiagnostic[]>;
|
|
20
22
|
/** Dispose every registered command; used at plugin teardown. */
|
|
@@ -25,5 +27,5 @@ export declare function readCommands(root: string): Promise<CommandSpec[]>;
|
|
|
25
27
|
/** Parse `agents/*.md` of one suite root into `agent-<name>` commands so
|
|
26
28
|
* subagents are selectable from the slash-command menu, grouped by the
|
|
27
29
|
* `agent-` prefix (the harness command UI has no group headers). */
|
|
28
|
-
export declare function readAgents(root: string): Promise<CommandSpec[]>;
|
|
30
|
+
export declare function readAgents(root: string, t: HostTranslate): Promise<CommandSpec[]>;
|
|
29
31
|
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Host runtime dictionary keys (mirrored for zh and en). */
|
|
2
|
+
export type HostLocaleKey = 'agentDefinitionTitle' | 'agentDefinitionIntro' | 'commandForwardTitle' | 'commandAcknowledged' | 'agentCommandHint';
|
|
3
|
+
export type HostTranslate = (key: HostLocaleKey, params?: Record<string, string>) => string;
|
|
4
|
+
interface ResolvedLocale {
|
|
5
|
+
t: HostTranslate;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve the active host language from settings; unknown values default to zh. */
|
|
8
|
+
export declare function bindHostLocale(preference: string | undefined): HostTranslate;
|
|
9
|
+
/** Read `locale.preference` from `$DSH_HOME/settings.yaml` (best effort). */
|
|
10
|
+
export declare function readLocalePreference(): Promise<string | undefined>;
|
|
11
|
+
/** Bind a host translator against the persisted locale preference. */
|
|
12
|
+
export declare function loadHostLocale(): Promise<ResolvedLocale>;
|
|
13
|
+
export {};
|