dsh-agent-plugins-market 0.6.0 → 0.6.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 (134) hide show
  1. package/README.md +143 -169
  2. package/README.zh.md +136 -170
  3. package/client/client.js +8202 -684
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog.js +236 -47
  6. package/lib/application/details.js +19 -10
  7. package/lib/application/panel-resources.js +95 -0
  8. package/lib/application/project-agent-roles.js +39 -0
  9. package/lib/catalog/codex-mcp.js +114 -0
  10. package/lib/catalog/component-files.js +151 -0
  11. package/lib/catalog/manifests.js +105 -44
  12. package/lib/catalog/native-project.js +44 -23
  13. package/lib/catalog/paths.js +6 -6
  14. package/lib/catalog/project-config.js +96 -0
  15. package/lib/catalog/project-hooks.js +108 -0
  16. package/lib/catalog/scan-pipeline.js +6 -1
  17. package/lib/catalog/scan-resolvers.js +70 -26
  18. package/lib/catalog/source-catalog.js +16 -3
  19. package/lib/catalog/suite-components.js +125 -0
  20. package/lib/catalog/surfaces.js +60 -46
  21. package/lib/catalog/validate.js +16 -11
  22. package/lib/contracts/market.js +16 -1
  23. package/lib/index.js +187 -35
  24. package/lib/model/layouts.js +49 -0
  25. package/lib/routes.js +106 -6
  26. package/lib/runtime/agent-role-router.js +226 -0
  27. package/lib/runtime/commands-mounts.js +27 -67
  28. package/lib/runtime/feedback-tool.js +181 -0
  29. package/lib/runtime/hooks-mounts.js +66 -13
  30. package/lib/runtime/host-locale.js +23 -7
  31. package/lib/runtime/legacy-root-migration.js +12 -0
  32. package/lib/runtime/lsp-direct-config.js +6 -3
  33. package/lib/runtime/lsp-mounts.js +38 -24
  34. package/lib/runtime/lsp-server-state.js +19 -0
  35. package/lib/runtime/lsp-status.js +3 -2
  36. package/lib/runtime/mcp-backend.js +6 -2
  37. package/lib/runtime/mcp-client/config.js +6 -0
  38. package/lib/runtime/mcp-client/connection.js +8 -2
  39. package/lib/runtime/mcp-client/tools.js +6 -3
  40. package/lib/runtime/mcp-config.js +26 -13
  41. package/lib/runtime/mcp-direct-config.js +56 -0
  42. package/lib/runtime/mcp-mounts.js +25 -9
  43. package/lib/runtime/mcp-overrides.js +11 -5
  44. package/lib/runtime/mcp-status.js +10 -7
  45. package/lib/runtime/model-catalog.js +48 -0
  46. package/lib/runtime/project-runtime.js +136 -0
  47. package/lib/runtime/reconciler.js +38 -28
  48. package/lib/runtime/server-config.js +75 -0
  49. package/lib/runtime/skills-provider.js +31 -77
  50. package/lib/runtime/storage-migration.js +169 -0
  51. package/lib/runtime/subagent-catalog.js +129 -0
  52. package/lib/runtime/user-commands.js +86 -0
  53. package/lib/runtime/user-panels.js +176 -0
  54. package/lib/runtime/user-store.js +142 -0
  55. package/lib/types/application/catalog.d.ts +39 -9
  56. package/lib/types/application/panel-resources.d.ts +13 -0
  57. package/lib/types/application/project-agent-roles.d.ts +3 -0
  58. package/lib/types/application/queries.d.ts +17 -1
  59. package/lib/types/catalog/codex-mcp.d.ts +4 -0
  60. package/lib/types/catalog/component-files.d.ts +17 -0
  61. package/lib/types/catalog/manifests.d.ts +14 -9
  62. package/lib/types/catalog/native-project.d.ts +2 -5
  63. package/lib/types/catalog/paths.d.ts +4 -4
  64. package/lib/types/catalog/project-config.d.ts +6 -0
  65. package/lib/types/catalog/project-hooks.d.ts +10 -0
  66. package/lib/types/catalog/scan-pipeline.d.ts +2 -0
  67. package/lib/types/catalog/scan-resolvers.d.ts +4 -3
  68. package/lib/types/catalog/source-catalog.d.ts +1 -1
  69. package/lib/types/catalog/suite-components.d.ts +5 -0
  70. package/lib/types/catalog/surfaces.d.ts +2 -2
  71. package/lib/types/catalog/validate.d.ts +2 -0
  72. package/lib/types/contracts/market.d.ts +60 -0
  73. package/lib/types/contracts/mcp-status.d.ts +5 -1
  74. package/lib/types/index.d.ts +2 -2
  75. package/lib/types/model/layouts.d.ts +102 -0
  76. package/lib/types/model/types.d.ts +51 -6
  77. package/lib/types/routes.d.ts +6 -1
  78. package/lib/types/runtime/agent-role-router.d.ts +95 -0
  79. package/lib/types/runtime/commands-mounts.d.ts +16 -7
  80. package/lib/types/runtime/feedback-tool.d.ts +49 -0
  81. package/lib/types/runtime/hooks-mounts.d.ts +4 -0
  82. package/lib/types/runtime/host-locale.d.ts +1 -1
  83. package/lib/types/runtime/legacy-root-migration.d.ts +2 -0
  84. package/lib/types/runtime/lsp-mounts.d.ts +4 -0
  85. package/lib/types/runtime/lsp-server-state.d.ts +3 -0
  86. package/lib/types/runtime/lsp-status.d.ts +1 -0
  87. package/lib/types/runtime/mcp-backend.d.ts +9 -1
  88. package/lib/types/runtime/mcp-client/config.d.ts +4 -3
  89. package/lib/types/runtime/mcp-client/tools.d.ts +3 -0
  90. package/lib/types/runtime/mcp-config.d.ts +11 -5
  91. package/lib/types/runtime/mcp-direct-config.d.ts +7 -0
  92. package/lib/types/runtime/mcp-mounts.d.ts +2 -1
  93. package/lib/types/runtime/mcp-overrides.d.ts +2 -0
  94. package/lib/types/runtime/mcp-status.d.ts +1 -1
  95. package/lib/types/runtime/model-catalog.d.ts +7 -0
  96. package/lib/types/runtime/project-runtime.d.ts +32 -0
  97. package/lib/types/runtime/reconciler.d.ts +2 -0
  98. package/lib/types/runtime/server-config.d.ts +10 -0
  99. package/lib/types/runtime/skills-provider.d.ts +1 -5
  100. package/lib/types/runtime/storage-migration.d.ts +15 -0
  101. package/lib/types/runtime/subagent-catalog.d.ts +52 -0
  102. package/lib/types/runtime/user-commands.d.ts +24 -0
  103. package/lib/types/runtime/user-panels.d.ts +91 -0
  104. package/lib/types/runtime/user-store.d.ts +51 -0
  105. package/package.json +10 -3
  106. package/schemas/README.md +69 -7
  107. package/schemas/agent-plugins/spec.md +48 -0
  108. package/schemas/claude-code/marketplace.schema.json +143 -0
  109. package/schemas/claude-code/plugin.schema.json +105 -0
  110. package/schemas/claude-code/spec.md +108 -0
  111. package/schemas/codex/marketplace.schema.json +78 -0
  112. package/schemas/codex/plugin.schema.json +77 -0
  113. package/schemas/codex/spec.md +91 -0
  114. package/schemas/cursor/marketplace.schema.json +82 -0
  115. package/schemas/cursor/plugin.schema.json +58 -0
  116. package/schemas/cursor/spec.md +92 -0
  117. package/schemas/github-copilot/marketplace.schema.json +107 -0
  118. package/schemas/github-copilot/plugin.schema.json +89 -0
  119. package/schemas/github-copilot/spec.md +105 -0
  120. package/schemas/kimi/marketplace.schema.json +38 -0
  121. package/schemas/kimi/plugin.schema.json +84 -0
  122. package/schemas/kimi/spec.md +94 -0
  123. package/schemas/qoder/marketplace.schema.json +115 -0
  124. package/schemas/qoder/plugin.schema.json +127 -0
  125. package/schemas/qoder/spec.md +111 -0
  126. package/schemas/skill-collection/spec.md +29 -0
  127. package/schemas/universal/marketplace.schema.json +98 -0
  128. package/schemas/universal/plugin.schema.json +49 -0
  129. package/schemas/universal/spec.md +53 -0
  130. package/schemas/zcode/marketplace.schema.json +161 -0
  131. package/schemas/zcode/plugin.schema.json +89 -0
  132. package/schemas/zcode/spec.md +102 -0
  133. package/lib/catalog/legacy-root-migration.js +0 -51
  134. package/lib/types/catalog/legacy-root-migration.d.ts +0 -14
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Optional model-facing experience-feedback tool.
3
+ *
4
+ * When enabled (the `feedbackEnabled` field of the market settings namespace,
5
+ * default true), the model can call `report_market_issue` when it concludes a
6
+ * problem it is looking at is caused by dsh-agent-plugins-market — a failed
7
+ * install, a misdetected source, a market UI defect the user just described.
8
+ * The tool files a GitHub issue on the plugin repository through the GitHub
9
+ * REST API, or falls back to appending a local JSONL record under the plugin
10
+ * data root when no repository host context is available.
11
+ *
12
+ * The tool is deliberately narrow: the title/body must describe the problem,
13
+ * submissions are rate-limited, and the setting switch unregisters the tool
14
+ * on the next reconcile pass.
15
+ * @module runtime/feedback-tool
16
+ */
17
+ import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
18
+ import { join } from 'node:path';
19
+ import { defineTool } from '@deepseek-ai/dsh-tools';
20
+ /** The GitHub repository experience feedback files issues against. */
21
+ export const FEEDBACK_REPO_OWNER = 'Sivan757';
22
+ export const FEEDBACK_REPO_NAME = 'dsh-agent-plugins-market';
23
+ /** Label applied to every tool-filed issue so they are filterable. */
24
+ export const FEEDBACK_LABEL = 'agent-feedback';
25
+ /** Minimum milliseconds between two accepted submissions (burst guard). */
26
+ const SUBMIT_COOLDOWN_MS = 60_000;
27
+ /**
28
+ * Submit one experience report. Tries the GitHub issue API first (works when
29
+ * the deployment carries a GITHUB_TOKEN / GH_TOKEN with repo scope), then
30
+ * falls back to the local JSONL spool under `<dataRoot>/feedback/`.
31
+ */
32
+ export async function submitFeedback(dataRoot, report, now = Date.now()) {
33
+ const title = report.title.trim();
34
+ if (title === '')
35
+ return { ok: false, reason: 'title is required' };
36
+ const body = renderFeedbackBody(report);
37
+ const lastAt = await readLastSubmitAt(dataRoot);
38
+ if (now - lastAt < SUBMIT_COOLDOWN_MS) {
39
+ return { ok: false, reason: 'a feedback report was submitted less than a minute ago; wait and try again' };
40
+ }
41
+ const token = process.env['GITHUB_TOKEN'] ?? process.env['GH_TOKEN'];
42
+ if (token !== undefined && token !== '') {
43
+ try {
44
+ const url = await createIssue(token, title, body);
45
+ await stampSubmitAt(dataRoot, now);
46
+ return { ok: true, location: url };
47
+ }
48
+ catch (error) {
49
+ // Network or auth failure: fall through to the local spool so the
50
+ // report is never lost, and say so in the outcome.
51
+ const local = await spoolLocally(dataRoot, title, body);
52
+ await stampSubmitAt(dataRoot, now);
53
+ return {
54
+ ok: true,
55
+ location: local,
56
+ reason: `GitHub issue failed (${error instanceof Error ? error.message : String(error)}); saved locally instead`
57
+ };
58
+ }
59
+ }
60
+ const local = await spoolLocally(dataRoot, title, body);
61
+ await stampSubmitAt(dataRoot, now);
62
+ return { ok: true, location: local };
63
+ }
64
+ /** POST one issue; returns its html_url. */
65
+ async function createIssue(token, title, body) {
66
+ const response = await fetch(`https://api.github.com/repos/${FEEDBACK_REPO_OWNER}/${FEEDBACK_REPO_NAME}/issues`, {
67
+ method: 'POST',
68
+ headers: {
69
+ authorization: `Bearer ${token}`,
70
+ accept: 'application/vnd.github+json',
71
+ 'content-type': 'application/json',
72
+ 'x-github-api-version': '2022-11-28',
73
+ 'user-agent': 'dsh-agent-plugins-market-feedback'
74
+ },
75
+ body: JSON.stringify({ title, body, labels: [FEEDBACK_LABEL] }),
76
+ signal: AbortSignal.timeout(15_000)
77
+ });
78
+ if (!response.ok) {
79
+ const detail = await response.text().catch(() => '');
80
+ throw new Error(`GitHub API ${response.status}${detail === '' ? '' : `: ${detail.slice(0, 200)}`}`);
81
+ }
82
+ const payload = (await response.json());
83
+ if (payload.html_url === undefined)
84
+ throw new Error('GitHub API returned no issue URL');
85
+ return payload.html_url;
86
+ }
87
+ /** Render the model's structured report into a deterministic issue body. */
88
+ export function renderFeedbackBody(report) {
89
+ const lines = ['## Experience feedback', '', `**Problem:** ${report.description.trim()}`, ''];
90
+ if (report.expected?.trim() !== undefined && report.expected.trim() !== '')
91
+ lines.push(`**Expected:** ${report.expected.trim()}`, '');
92
+ if (report.actual?.trim() !== undefined && report.actual.trim() !== '')
93
+ lines.push(`**Actual:** ${report.actual.trim()}`, '');
94
+ lines.push('---', `Filed automatically by the dsh-agent-plugins-market \`${FEEDBACK_TOOL_NAME}\` tool.`);
95
+ return lines.join('\n');
96
+ }
97
+ /** Append one report to the local spool; returns the spool file path. */
98
+ async function spoolLocally(dataRoot, title, body) {
99
+ const dir = join(dataRoot, 'feedback');
100
+ await mkdir(dir, { recursive: true });
101
+ const file = join(dir, 'reports.jsonl');
102
+ const record = JSON.stringify({ at: new Date().toISOString(), title, body });
103
+ await appendFile(file, `${record}\n`, 'utf8');
104
+ return file;
105
+ }
106
+ /** The last accepted submission's timestamp; 0 when the stamp file is absent. */
107
+ async function readLastSubmitAt(dataRoot) {
108
+ try {
109
+ const raw = await readFile(join(dataRoot, 'feedback', 'last-submit-at'), 'utf8');
110
+ const value = Number.parseInt(raw.trim(), 10);
111
+ return Number.isSafeInteger(value) ? value : 0;
112
+ }
113
+ catch {
114
+ return 0;
115
+ }
116
+ }
117
+ async function stampSubmitAt(dataRoot, now) {
118
+ const dir = join(dataRoot, 'feedback');
119
+ await mkdir(dir, { recursive: true });
120
+ // Overwrite, not append: an appending stamp concatenates timestamps and
121
+ // parseInt on the merged digits disables the cooldown permanently.
122
+ await writeFile(join(dir, 'last-submit-at'), String(now), 'utf8');
123
+ }
124
+ export const FEEDBACK_TOOL_NAME = 'report_market_issue';
125
+ const FEEDBACK_DESCRIPTION = 'File an experience-feedback issue for dsh-agent-plugins-market (the DeepSeek Harness agent plugin market). ' +
126
+ 'Call this ONLY when the human describes a problem caused by the agent plugins market itself — a failed or ' +
127
+ 'stuck install/uninstall, a market source that will not scan, wrong market UI behavior, or a runtime injection ' +
128
+ 'defect the user attributes to a market suite. Do not call it for problems in other plugins, in the harness, ' +
129
+ "or in the user's own repositories. Compose a specific title; describe what happened, what was expected, and " +
130
+ 'the exact error text when one appeared.';
131
+ /** Register the feedback tool; returns the disposer. Undefined when the host has no tools service. */
132
+ export function mountFeedbackTool(hostCtx, dataRoot, t) {
133
+ const host = hostCtx;
134
+ if (typeof host.tools?.register !== 'function')
135
+ return undefined;
136
+ return host.tools.register(defineTool({
137
+ name: FEEDBACK_TOOL_NAME,
138
+ description: FEEDBACK_DESCRIPTION,
139
+ parameters: {
140
+ title: { type: 'string', required: true, description: 'One-line problem summary for the issue title.' },
141
+ description: { type: 'string', required: true, description: 'What the problem is, with exact error text when one appeared.' },
142
+ expected: { type: 'string', description: 'What the user expected to happen.' },
143
+ actual: { type: 'string', description: 'What actually happened instead.' }
144
+ },
145
+ output: {
146
+ // The dsh-tools value-schema DSL rejects root-level `required`
147
+ // (verified against dsh-tools@0.1.2-rc.1); required-ness is declared
148
+ // per property instead.
149
+ schema: {
150
+ type: 'object',
151
+ additionalProperties: false,
152
+ properties: {
153
+ ok: { type: 'boolean', required: true },
154
+ location: { type: 'string' },
155
+ reason: { type: 'string' }
156
+ }
157
+ },
158
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }]
159
+ },
160
+ async execute(args) {
161
+ const input = args;
162
+ const report = {
163
+ title: String(input['title'] ?? ''),
164
+ description: String(input['description'] ?? ''),
165
+ ...(typeof input['expected'] === 'string' ? { expected: input['expected'] } : {}),
166
+ ...(typeof input['actual'] === 'string' ? { actual: input['actual'] } : {})
167
+ };
168
+ return await submitFeedback(dataRoot, report);
169
+ },
170
+ presentCall: (args) => {
171
+ const input = args;
172
+ const view = {
173
+ card: 'generic',
174
+ title: t('feedbackToolCardTitle'),
175
+ kind: 'other',
176
+ ...(typeof input['title'] === 'string' ? { rawInput: input['title'] } : {})
177
+ };
178
+ return view;
179
+ }
180
+ }));
181
+ }
@@ -10,19 +10,29 @@
10
10
  * on every enable/disable/install/uninstall; a missing bridge package, a
11
11
  * broken hook file, or a mount failure is contained per suite.
12
12
  */
13
- import { stat } from 'node:fs/promises';
13
+ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
14
+ import { tmpdir } from 'node:os';
15
+ import { createHash } from 'node:crypto';
14
16
  import { join } from 'node:path';
15
17
  import { qualifiedSuiteId } from '../catalog/paths.js';
16
18
  export class HooksMountRegistry {
17
19
  ctx;
18
20
  live = new Map();
21
+ fingerprints = new Map();
22
+ temporary = new Map();
23
+ queue = Promise.resolve();
19
24
  constructor(ctx) {
20
25
  this.ctx = ctx;
21
26
  }
22
27
  /** Mount/unmount one bridge per suite to match the enabled suites exactly. */
23
28
  async reconcile(enabledSuites) {
29
+ const run = this.queue.then(() => this.reconcileNow(enabledSuites));
30
+ this.queue = run.then(() => { }, () => { });
31
+ return run;
32
+ }
33
+ async reconcileNow(enabledSuites) {
24
34
  const diagnostics = [];
25
- const active = enabledSuites.filter(suite => suite.activeSurfaces?.hooks !== false);
35
+ const active = enabledSuites.filter(suite => suite.activeSurfaces?.hooks !== false && (suite.resources === undefined || suite.hooks !== undefined));
26
36
  // Keys are the qualified suite id: bare ids are unique per source only.
27
37
  const wanted = new Set(active.map(suite => qualifiedSuiteId(suite.sourceId, suite.id)));
28
38
  for (const [suiteId, handle] of [...this.live]) {
@@ -32,9 +42,25 @@ export class HooksMountRegistry {
32
42
  }
33
43
  for (const suite of active) {
34
44
  const key = qualifiedSuiteId(suite.sourceId, suite.id);
35
- if (this.live.has(key))
45
+ const originalPath = suite.hooks === undefined ? await hookConfigPath(suite.root) : undefined;
46
+ const content = suite.hooks === undefined
47
+ ? originalPath === undefined
48
+ ? undefined
49
+ : await readFile(originalPath, 'utf8').catch(() => undefined)
50
+ : JSON.stringify({ hooks: suite.hooks.events });
51
+ const fingerprint = content === undefined
52
+ ? undefined
53
+ : createHash('sha256')
54
+ .update(JSON.stringify([content, suite.root, suite.hooks?.projectRoot]))
55
+ .digest('hex');
56
+ const previous = this.live.get(key);
57
+ if (previous !== undefined && fingerprint === this.fingerprints.get(key))
58
+ continue;
59
+ if (previous !== undefined)
60
+ await this.unmount(key, previous);
61
+ if (content === undefined || fingerprint === undefined)
36
62
  continue;
37
- const reason = await this.mount(key, suite);
63
+ const reason = await this.mount(key, suite, content, fingerprint, originalPath);
38
64
  if (reason !== undefined)
39
65
  diagnostics.push({ suiteId: key, reason });
40
66
  }
@@ -42,14 +68,14 @@ export class HooksMountRegistry {
42
68
  }
43
69
  /** Dispose every live bridge; used at plugin teardown. */
44
70
  async disposeAll() {
45
- for (const [suiteId, handle] of [...this.live]) {
46
- await this.unmount(suiteId, handle);
47
- }
71
+ const run = this.queue.then(async () => {
72
+ for (const [suiteId, handle] of [...this.live])
73
+ await this.unmount(suiteId, handle);
74
+ });
75
+ this.queue = run.catch(() => { });
76
+ await run;
48
77
  }
49
- async mount(key, suite) {
50
- const configPath = await hookConfigPath(suite.root);
51
- if (configPath === undefined)
52
- return undefined;
78
+ async mount(key, suite, content, fingerprint, originalPath) {
53
79
  let bridge;
54
80
  try {
55
81
  bridge = await import('@deepseek-ai/dsh-hooks-claude-code');
@@ -60,27 +86,54 @@ export class HooksMountRegistry {
60
86
  const mountCtx = this.ctx;
61
87
  if (typeof mountCtx.plugin !== 'function')
62
88
  return 'the host context does not support dynamic plugin mounting';
89
+ let temporary;
90
+ let handle;
63
91
  try {
64
- const handle = mountCtx.plugin(bridge, {
92
+ let configPath = originalPath;
93
+ if (configPath === undefined) {
94
+ temporary = await mkdtemp(join(tmpdir(), 'dsh-project-hooks-'));
95
+ configPath = join(temporary, 'hooks.json');
96
+ await writeFile(configPath, content, { mode: 0o600 });
97
+ }
98
+ handle = mountCtx.plugin(bridge, {
65
99
  configPath,
66
- pluginRoot: suite.root
100
+ pluginRoot: suite.root,
101
+ ...(suite.hooks === undefined ? {} : { projectDir: suite.hooks.projectRoot })
67
102
  });
68
103
  await handle.await();
69
104
  this.live.set(key, handle);
105
+ this.fingerprints.set(key, fingerprint);
106
+ if (temporary !== undefined)
107
+ this.temporary.set(key, temporary);
70
108
  return undefined;
71
109
  }
72
110
  catch (error) {
111
+ try {
112
+ await handle?.dispose();
113
+ }
114
+ catch {
115
+ /* Preserve the startup failure. */
116
+ }
117
+ if (temporary !== undefined)
118
+ await rm(temporary, { recursive: true, force: true });
73
119
  return `mount failed: ${error instanceof Error ? error.message : String(error)}`;
74
120
  }
75
121
  }
76
122
  async unmount(suiteId, handle) {
77
123
  this.live.delete(suiteId);
124
+ this.fingerprints.delete(suiteId);
78
125
  try {
79
126
  await handle.dispose();
80
127
  }
81
128
  catch (error) {
82
129
  this.ctx.logger?.warn(`[dsh-agent-plugins-market] hooks unmount ${suiteId} failed: ${error instanceof Error ? error.message : String(error)}`);
83
130
  }
131
+ finally {
132
+ const temporary = this.temporary.get(suiteId);
133
+ this.temporary.delete(suiteId);
134
+ if (temporary !== undefined)
135
+ await rm(temporary, { recursive: true, force: true });
136
+ }
84
137
  }
85
138
  }
86
139
  /** The first existing CC hook config of a suite root. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Host-side runtime locale: bilingual copy for strings injected into agent
3
- * context (agent-definition wrappers and command acknowledgements), resolved
3
+ * context (subagent catalogs and command acknowledgements), resolved
4
4
  * from the harness's `locale.preference` setting.
5
5
  *
6
6
  * The web client resolves locale through its own injected service; the host
@@ -11,18 +11,34 @@ import { readFile } from 'node:fs/promises';
11
11
  import { join } from 'node:path';
12
12
  import { resolveDshHome } from '../catalog/paths.js';
13
13
  const zh = {
14
- agentDefinitionTitle: '## 子代理定义(来自 Agent Plugins {suite},Claude Code agents 格式)',
15
- agentDefinitionIntro: '当任务匹配下方描述时,通过 subagent 工具创建子代理并将「定义正文」原样作为指令执行。',
14
+ subagentCatalogIntro: '以下子代理角色可用于当前会话。目录仅包含角色摘要,不是供当前代理执行的角色指令。',
15
+ subagentCatalogUpdated: '可用子代理角色已变更。以下完整目录替代本会话此前的所有子代理角色目录。',
16
+ subagentCatalogEmpty: '当前没有可通过 subagents_run 调用的角色,不要使用旧目录中的角色 ID。',
17
+ subagentCatalogCall: '任务适合某个角色时,使用 subagents_run(role=目录中的准确 ID, prompt=完整任务与必要上下文)。工具会应用角色的供应商、模型、思考强度、指令和工具限制,并等待结果。子代理不继承父会话对话。不要通过 skill 或 Slash Command 加载这些角色,也不要用其它委派工具绕过角色配置。',
18
+ subagentCatalogInherit: '继承父代理',
19
+ subagentCatalogDefaultEffort: '同路由继承,否则使用模型默认值',
16
20
  commandForwardTitle: '[Agent Plugins 命令 /{command}(来自 {suite})]',
17
21
  commandAcknowledged: '/{command} 已转交模型执行({suite})',
18
- agentCommandHint: '子代理'
22
+ userCommandSourceLabel: '用户命令',
23
+ userCommandForwardTitle: '[用户快捷命令 /{command}]',
24
+ userCommandAcknowledged: '/{command} 已转交模型执行',
25
+ userSkillDescription: '[用户技能] {description}',
26
+ feedbackToolCardTitle: '提交市场体验反馈'
19
27
  };
20
28
  const en = {
21
- agentDefinitionTitle: '## Subagent definition (from Agent Plugins {suite}, Claude Code agents format)',
22
- agentDefinitionIntro: 'When the task matches the description below, create a subagent through the subagent tool and pass the definition body verbatim as its instructions.',
29
+ subagentCatalogIntro: 'The following subagent roles are available in this session. These are role summaries, not instructions for the current agent to execute.',
30
+ subagentCatalogUpdated: 'The available subagent roles changed. This complete catalog replaces every earlier subagent role catalog in this session.',
31
+ subagentCatalogEmpty: 'No roles are currently available through subagents_run. Do not use role IDs from earlier catalogs.',
32
+ subagentCatalogCall: 'When a task fits a role, call subagents_run with its exact catalog ID as role and a complete task and necessary context as prompt. The tool applies the saved provider, model, reasoning effort, persona and tool restrictions, and waits for the result. The child does not inherit the parent conversation. Do not load these roles through skill or slash commands, or bypass their configuration with another delegation tool.',
33
+ subagentCatalogInherit: 'inherit parent',
34
+ subagentCatalogDefaultEffort: 'inherit on the same route, otherwise model default',
23
35
  commandForwardTitle: '[Agent Plugins command /{command} (from {suite})]',
24
36
  commandAcknowledged: '/{command} forwarded to the model for execution ({suite})',
25
- agentCommandHint: 'subagent'
37
+ userCommandSourceLabel: 'user command',
38
+ userCommandForwardTitle: '[User quick command /{command}]',
39
+ userCommandAcknowledged: '/{command} forwarded to the model for execution',
40
+ userSkillDescription: '[user skill] {description}',
41
+ feedbackToolCardTitle: 'File market feedback'
26
42
  };
27
43
  const DICTS = { zh, en };
28
44
  /** Resolve the active host language from settings; unknown values default to zh. */
@@ -0,0 +1,12 @@
1
+ /** Compatibility migration for the pre-0.5.4 sibling data directory. */
2
+ import { readdir, rmdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { mergeStorageTree } from './storage-migration.js';
5
+ /** Move owned data subtrees without overwriting or deleting conflicting files. */
6
+ export async function migrateLegacyDataRoot(legacyRoot, dataRoot) {
7
+ const result = { conflicts: [] };
8
+ for (const subtree of ['data', 'overrides'])
9
+ await mergeStorageTree(join(legacyRoot, subtree), join(dataRoot, subtree), result);
10
+ if ((await readdir(legacyRoot).catch(() => ['keep'])).length === 0)
11
+ await rmdir(legacyRoot);
12
+ }
@@ -21,11 +21,13 @@ export async function loadLspServers(dataRoot) {
21
21
  try {
22
22
  raw = JSON.parse(await readFile(lspServersPath(dataRoot), 'utf8'));
23
23
  }
24
- catch {
25
- return { servers: {}, errors: [] };
24
+ catch (error) {
25
+ return { servers: {}, errors: error.code === 'ENOENT' ? [] : [`lsp-servers.json: ${error instanceof Error ? error.message : String(error)}`] };
26
26
  }
27
27
  const errors = [];
28
28
  const table = raw?.['lspServers'];
29
+ if (table === undefined || table === null || Array.isArray(table) || typeof table !== 'object')
30
+ return { servers: {}, errors: ['lsp-servers.json: lspServers must be an object'] };
29
31
  const servers = parseLspServers(table, errors);
30
32
  return { servers, errors: errors.map(error => `lsp-servers.json: ${error}`) };
31
33
  }
@@ -39,6 +41,7 @@ export async function saveLspServers(dataRoot, raw) {
39
41
  }
40
42
  const path = lspServersPath(dataRoot);
41
43
  await mkdir(dirname(path), { recursive: true });
42
- await writeFile(path, `${JSON.stringify({ lspServers: servers }, null, 2)}\n`, 'utf8');
44
+ const persisted = Object.fromEntries(Object.entries(servers).map(([name, { key: _key, ...config }]) => [name, config]));
45
+ await writeFile(path, `${JSON.stringify({ lspServers: persisted }, null, 2)}\n`, { mode: 0o600 });
43
46
  return { servers };
44
47
  }
@@ -37,6 +37,8 @@ export class LspMountRegistry {
37
37
  lastDiagnostics = new Map();
38
38
  /** Direct (user-configured) server provider; defaults to none. */
39
39
  directProvider = async () => ({});
40
+ disabledProvider = async () => new Set();
41
+ disabledSnapshot = new Set();
40
42
  constructor(ctx, loadHost) {
41
43
  this.ctx = ctx;
42
44
  this.loadHost =
@@ -62,6 +64,8 @@ export class LspMountRegistry {
62
64
  const active = enabledSuites.filter(suite => suite.activeSurfaces?.lsp !== false);
63
65
  const wanted = new Map();
64
66
  const diagnostics = [];
67
+ const disabled = await this.disabledProvider();
68
+ this.disabledSnapshot = new Set(disabled);
65
69
  for (const suite of active) {
66
70
  const servers = Object.values(suite.lsp?.servers ?? {});
67
71
  if (servers.length === 0)
@@ -72,8 +76,10 @@ export class LspMountRegistry {
72
76
  const key = qualifiedSuiteId(suite.sourceId, suite.id);
73
77
  const config = {};
74
78
  for (const spec of servers)
75
- config[`${key}/${spec.key}`] = toLspServerConfig(spec);
76
- wanted.set(key, { suite, config });
79
+ if (!disabled.has(`${key}/${spec.key}`))
80
+ config[`${key}/${spec.key}`] = toLspServerConfig(spec);
81
+ if (Object.keys(config).length > 0)
82
+ wanted.set(key, { suite, config });
77
83
  }
78
84
  // Direct user-configured servers ride the same mount path under the
79
85
  // sentinel suite id, so their lifecycle (retries, diagnostics, disposal)
@@ -83,28 +89,30 @@ export class LspMountRegistry {
83
89
  if (directKeys.length > 0) {
84
90
  const config = {};
85
91
  for (const [key, spec] of Object.entries(direct))
86
- config[`${DIRECT_LSP_SUITE_ID}/${key}`] = toLspServerConfig(spec);
87
- wanted.set(DIRECT_LSP_SUITE_ID, {
88
- suite: {
89
- sourceId: '',
90
- id: DIRECT_LSP_SUITE_ID,
91
- root: '',
92
- manifest: { layout: 'claude-code', path: '', id: DIRECT_LSP_SUITE_ID, name: DIRECT_LSP_SUITE_ID },
93
- skills: [],
94
- surfaces: { skills: 0, mcp: 0, hooks: 0, commands: 0, agents: 0, lsp: directKeys.length },
95
- dimension: 'user',
96
- enabled: true,
97
- errors: []
98
- },
99
- config
100
- });
92
+ if (!disabled.has(`${DIRECT_LSP_SUITE_ID}/${key}`))
93
+ config[`${DIRECT_LSP_SUITE_ID}/${key}`] = toLspServerConfig(spec);
94
+ if (Object.keys(config).length > 0)
95
+ wanted.set(DIRECT_LSP_SUITE_ID, {
96
+ suite: {
97
+ sourceId: '',
98
+ id: DIRECT_LSP_SUITE_ID,
99
+ root: '',
100
+ manifest: { layout: 'claude-code', path: '', id: DIRECT_LSP_SUITE_ID, name: DIRECT_LSP_SUITE_ID },
101
+ skills: [],
102
+ surfaces: { skills: 0, mcp: 0, hooks: 0, commands: 0, agents: 0, lsp: directKeys.length },
103
+ dimension: 'user',
104
+ enabled: true,
105
+ errors: []
106
+ },
107
+ config
108
+ });
101
109
  }
102
110
  for (const [key, live] of [...this.live]) {
103
- if (!wanted.has(key)) {
111
+ if (!wanted.has(key) || live.fingerprint !== JSON.stringify(wanted.get(key).config)) {
104
112
  const reason = await this.unmount(key, live);
105
- this.lastDiagnostics.delete(live.suiteId);
113
+ this.lastDiagnostics.delete(key);
106
114
  if (reason !== undefined)
107
- diagnostics.push({ suiteId: live.suiteId, serverKey: live.serverKeys.join(','), reason, code: 'unmount-failed' });
115
+ diagnostics.push({ suiteId: key, serverKey: live.serverKeys.join(','), reason, code: 'unmount-failed' });
108
116
  }
109
117
  }
110
118
  for (const [key, entry] of wanted) {
@@ -128,6 +136,12 @@ export class LspMountRegistry {
128
136
  setDirectProvider(provider) {
129
137
  this.directProvider = provider;
130
138
  }
139
+ setDisabledProvider(provider) {
140
+ this.disabledProvider = provider;
141
+ }
142
+ disabledServers() {
143
+ return new Set(this.disabledSnapshot);
144
+ }
131
145
  /** The latest mount diagnostic per suite, for the LSP status surface. */
132
146
  diagnosticsSnapshot() {
133
147
  return new Map(this.lastDiagnostics);
@@ -141,12 +155,12 @@ export class LspMountRegistry {
141
155
  const hostKeys = Object.keys(servers).join(',');
142
156
  const module = await this.loadHost();
143
157
  if (module === undefined) {
144
- return { suiteId: suite.id, serverKey: hostKeys, reason: HOST_MISSING_REASON, code: 'host-missing' };
158
+ return { suiteId: key, serverKey: hostKeys, reason: HOST_MISSING_REASON, code: 'host-missing' };
145
159
  }
146
160
  const plugin = module.default ?? module;
147
161
  const mountCtx = this.ctx;
148
162
  if (typeof mountCtx.plugin !== 'function') {
149
- return { suiteId: suite.id, serverKey: hostKeys, reason: 'the host context does not support dynamic plugin mounting', code: 'host-missing' };
163
+ return { suiteId: key, serverKey: hostKeys, reason: 'the host context does not support dynamic plugin mounting', code: 'host-missing' };
150
164
  }
151
165
  let handle;
152
166
  try {
@@ -167,13 +181,13 @@ export class LspMountRegistry {
167
181
  const message = error instanceof Error ? error.message : String(error);
168
182
  const conflict = /already handled by another LSP provider|already registered/.test(message);
169
183
  return {
170
- suiteId: suite.id,
184
+ suiteId: key,
171
185
  serverKey: hostKeys,
172
186
  reason: `mount failed: ${message}`,
173
187
  code: conflict ? 'seam-conflict' : 'mount-failed'
174
188
  };
175
189
  }
176
- this.live.set(key, { suiteId: suite.id, serverKeys: Object.keys(servers), disposer: () => handle.dispose() });
190
+ this.live.set(key, { fingerprint: JSON.stringify(servers), suiteId: key, serverKeys: Object.keys(servers), disposer: () => handle.dispose() });
177
191
  return undefined;
178
192
  }
179
193
  async unmount(key, live) {
@@ -0,0 +1,19 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ export function lspServerStatePath(dataRoot) {
4
+ return join(dataRoot, 'lsp-server-state.json');
5
+ }
6
+ export async function loadDisabledLspServers(dataRoot) {
7
+ try {
8
+ const value = JSON.parse(await readFile(lspServerStatePath(dataRoot), 'utf8'));
9
+ return new Set(Array.isArray(value.disabled) ? value.disabled.filter((id) => typeof id === 'string') : []);
10
+ }
11
+ catch {
12
+ return new Set();
13
+ }
14
+ }
15
+ export async function saveDisabledLspServers(dataRoot, disabled) {
16
+ const path = lspServerStatePath(dataRoot);
17
+ await mkdir(dirname(path), { recursive: true });
18
+ await writeFile(path, `${JSON.stringify({ disabled: [...new Set(disabled)].sort() }, null, 2)}\n`, 'utf8');
19
+ }
@@ -30,6 +30,7 @@ function deriveState(disabled, diagnostic, anyLive) {
30
30
  export function buildLspStatus(suites, registry, direct = { servers: {}, errors: [] }) {
31
31
  const diagnostics = registry.diagnosticsSnapshot();
32
32
  const anyLive = registry.hasLiveMounts();
33
+ const disabledServers = registry.disabledServers?.() ?? new Set();
33
34
  const entries = [];
34
35
  const mountedSuiteIds = new Set();
35
36
  for (const suite of suites) {
@@ -55,7 +56,7 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
55
56
  command: spec.command,
56
57
  args: spec.args,
57
58
  extensions: spec.extensionToLanguage,
58
- state,
59
+ state: disabled || disabledServers.has(`${suiteKey}/${spec.key}`) ? 'disabled' : state,
59
60
  ...(reason === undefined ? {} : { reason }),
60
61
  ...(retryable === undefined ? {} : { retryable })
61
62
  });
@@ -78,7 +79,7 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
78
79
  command: spec.command,
79
80
  args: spec.args,
80
81
  extensions: spec.extensionToLanguage,
81
- state: directState,
82
+ state: disabledServers.has(`${DIRECT_LSP_SUITE_ID}/${spec.key}`) ? 'disabled' : directState,
82
83
  ...(directReason === undefined ? {} : { reason: directReason }),
83
84
  ...(directRetryable === undefined ? {} : { retryable: directRetryable })
84
85
  });
@@ -20,12 +20,16 @@ import z from '@deepseek-ai/schemastery';
20
20
  export const MCP_BACKENDS = ['builtin', 'host'];
21
21
  /** Settings namespace this plugin registers; the key the plugin-config tab pairs our card by. */
22
22
  export const MCP_SETTINGS_NAMESPACE = 'dsh-agent-plugins-market';
23
- /** Schema of the market settings namespace: the MCP enhancement switch and the download region. */
23
+ /** Schema of the market settings namespace: the MCP enhancement switch, the download region, and the feedback tool switch. */
24
24
  export const MarketSettingsSchema = z.object({
25
25
  /** ON (default) = the built-in bridge with OAuth and SSE; OFF = host client compat mode. */
26
26
  mcpEnhanced: z.boolean().default(true),
27
+ /** Read native Agent layouts under the project root; default preserves existing discovery. */
28
+ scanProjectLayouts: z.boolean().default(true),
27
29
  /** Download region for GitHub acquisition; `auto` follows the interface language. */
28
- downloadRegion: z.union([z.const('auto'), z.const('global'), z.const('china')]).default('auto')
30
+ downloadRegion: z.union([z.const('auto'), z.const('global'), z.const('china')]).default('auto'),
31
+ /** ON (default) = the `report_market_issue` model tool is registered; OFF = unregistered. */
32
+ feedbackEnabled: z.boolean().default(true)
29
33
  });
30
34
  /** Path of the legacy persisted settings file under the plugin data root. */
31
35
  export function marketSettingsPath(dataRoot) {
@@ -23,6 +23,12 @@ export const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
23
23
  */
24
24
  export function validateConfig(config) {
25
25
  const fields = config;
26
+ for (const field of ['enabledTools', 'disabledTools']) {
27
+ if (fields[field] !== undefined && (!Array.isArray(fields[field]) || !fields[field].every(name => typeof name === 'string')))
28
+ throw new Error(`bridge config ${field} must be an array of strings`);
29
+ }
30
+ if (fields.startupTimeoutMs !== undefined && (!Number.isFinite(fields.startupTimeoutMs) || fields.startupTimeoutMs <= 0 || fields.startupTimeoutMs > 2_147_483_647))
31
+ throw new Error('bridge config startupTimeoutMs must be a positive finite timeout within the timer range');
26
32
  if (!SERVER_NAME_PATTERN.test(config.serverName)) {
27
33
  throw new Error(`bridge config serverName "${config.serverName}" must match ${String(SERVER_NAME_PATTERN)}`);
28
34
  }
@@ -87,7 +87,10 @@ export function startConnection(host, config, policy, credentials) {
87
87
  const opts = {
88
88
  registrationFailure: 'contain',
89
89
  serverName: config.serverName,
90
- toolCallTimeoutMs: config.toolCallTimeoutMs
90
+ toolCallTimeoutMs: config.toolCallTimeoutMs,
91
+ ...(config.enabledTools === undefined ? {} : { enabledTools: config.enabledTools }),
92
+ ...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools }),
93
+ ...(config.startupTimeoutMs === undefined ? {} : { startupTimeoutMs: config.startupTimeoutMs })
91
94
  };
92
95
  // The initial sync uses 'throw' when failOnStartupError is configured, so
93
96
  // a registration conflict propagates to the startup-await path. Re-syncs
@@ -229,7 +232,10 @@ export function startConnection(host, config, policy, credentials) {
229
232
  host.logger.info(message);
230
233
  });
231
234
  try {
232
- await generation.connect(transport);
235
+ if (config.startupTimeoutMs === undefined)
236
+ await generation.connect(transport);
237
+ else
238
+ await generation.connect(transport, { timeout: config.startupTimeoutMs });
233
239
  if (hasClosed()) {
234
240
  attemptSettled = true;
235
241
  generationDown(generation);