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.
Files changed (41) hide show
  1. package/README.md +4 -2
  2. package/README.zh.md +4 -2
  3. package/client/client.js +310 -17
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog.js +101 -4
  6. package/lib/application/details.js +4 -1
  7. package/lib/catalog/native-project.js +73 -0
  8. package/lib/catalog/source-catalog.js +12 -3
  9. package/lib/contracts/market.js +4 -1
  10. package/lib/index.js +12 -5
  11. package/lib/model/state.js +19 -1
  12. package/lib/model/types.js +12 -1
  13. package/lib/routes.js +37 -0
  14. package/lib/runtime/commands-mounts.js +15 -6
  15. package/lib/runtime/hooks-mounts.js +3 -2
  16. package/lib/runtime/host-locale.js +54 -0
  17. package/lib/runtime/mcp-config.js +11 -4
  18. package/lib/runtime/mcp-mounts.js +33 -17
  19. package/lib/runtime/mcp-overrides.js +113 -0
  20. package/lib/runtime/mcp-status.js +8 -4
  21. package/lib/runtime/reconciler.js +7 -2
  22. package/lib/runtime/skills-provider.js +22 -13
  23. package/lib/types/application/catalog.d.ts +30 -2
  24. package/lib/types/application/details.d.ts +2 -2
  25. package/lib/types/application/queries.d.ts +5 -1
  26. package/lib/types/catalog/native-project.d.ts +17 -0
  27. package/lib/types/contracts/market.d.ts +26 -0
  28. package/lib/types/index.d.ts +4 -3
  29. package/lib/types/model/state.d.ts +1 -1
  30. package/lib/types/model/types.d.ts +13 -1
  31. package/lib/types/runtime/commands-mounts.d.ts +4 -2
  32. package/lib/types/runtime/host-locale.d.ts +13 -0
  33. package/lib/types/runtime/mcp-config.d.ts +5 -1
  34. package/lib/types/runtime/mcp-mounts.d.ts +12 -3
  35. package/lib/types/runtime/mcp-overrides.d.ts +37 -0
  36. package/lib/types/runtime/mcp-status.d.ts +2 -1
  37. package/lib/types/runtime/reconciler.d.ts +4 -1
  38. package/lib/types/runtime/skills-provider.d.ts +3 -1
  39. package/package.json +1 -1
  40. package/lib/context.js +0 -78
  41. 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
- /** Mount/unmount MCP servers to match the enabled suites exactly. */
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 enabledSuites) {
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.mount(entry.suite, entry.serverKey);
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; used at plugin teardown. */
54
+ /** Dispose every live mount after queued reconciliation passes settle. */
40
55
  async disposeAll() {
41
- for (const [key, live] of [...this.live]) {
42
- await this.unmount(key, live);
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
- async mount(suite, serverKey) {
46
- const { mounts } = toMcpMounts(suite, this.pluginDataRoot);
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(suite.id, serverKey);
67
- this.live.set(key, { suiteId: suite.id, serverKey, serverName: request.config.serverName, disposer: () => handle.dispose() });
68
- this.names.set(request.config.serverName, `${suite.id}/${serverKey}`);
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 state = diagnostic !== undefined ? 'failed' : tools.length > 0 ? 'connected' : 'degraded';
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
- ...(diagnostic === undefined ? {} : { reason: diagnostic.reason })
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
- return located.map(entry => this.candidateFor(entry));
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 { SourceRef, Suite, SuiteDimension } from '../model/types.js';
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 { InstalledEntry, Suite } from '../model/types.js';
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: {
@@ -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, injects the
7
- * enabled-suite catalog at session start, registers the `agent_plugins`
8
- * tool, and mounts the market page's HTTP routes on the web server.
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 { SuiteState } from './types.js';
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 {};