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.
- package/README.md +143 -169
- package/README.zh.md +136 -170
- package/client/client.js +8202 -684
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +236 -47
- package/lib/application/details.js +19 -10
- package/lib/application/panel-resources.js +95 -0
- package/lib/application/project-agent-roles.js +39 -0
- package/lib/catalog/codex-mcp.js +114 -0
- package/lib/catalog/component-files.js +151 -0
- package/lib/catalog/manifests.js +105 -44
- package/lib/catalog/native-project.js +44 -23
- package/lib/catalog/paths.js +6 -6
- package/lib/catalog/project-config.js +96 -0
- package/lib/catalog/project-hooks.js +108 -0
- package/lib/catalog/scan-pipeline.js +6 -1
- package/lib/catalog/scan-resolvers.js +70 -26
- package/lib/catalog/source-catalog.js +16 -3
- package/lib/catalog/suite-components.js +125 -0
- package/lib/catalog/surfaces.js +60 -46
- package/lib/catalog/validate.js +16 -11
- package/lib/contracts/market.js +16 -1
- package/lib/index.js +187 -35
- package/lib/model/layouts.js +49 -0
- package/lib/routes.js +106 -6
- package/lib/runtime/agent-role-router.js +226 -0
- package/lib/runtime/commands-mounts.js +27 -67
- package/lib/runtime/feedback-tool.js +181 -0
- package/lib/runtime/hooks-mounts.js +66 -13
- package/lib/runtime/host-locale.js +23 -7
- package/lib/runtime/legacy-root-migration.js +12 -0
- package/lib/runtime/lsp-direct-config.js +6 -3
- package/lib/runtime/lsp-mounts.js +38 -24
- package/lib/runtime/lsp-server-state.js +19 -0
- package/lib/runtime/lsp-status.js +3 -2
- package/lib/runtime/mcp-backend.js +6 -2
- package/lib/runtime/mcp-client/config.js +6 -0
- package/lib/runtime/mcp-client/connection.js +8 -2
- package/lib/runtime/mcp-client/tools.js +6 -3
- package/lib/runtime/mcp-config.js +26 -13
- package/lib/runtime/mcp-direct-config.js +56 -0
- package/lib/runtime/mcp-mounts.js +25 -9
- package/lib/runtime/mcp-overrides.js +11 -5
- package/lib/runtime/mcp-status.js +10 -7
- package/lib/runtime/model-catalog.js +48 -0
- package/lib/runtime/project-runtime.js +136 -0
- package/lib/runtime/reconciler.js +38 -28
- package/lib/runtime/server-config.js +75 -0
- package/lib/runtime/skills-provider.js +31 -77
- package/lib/runtime/storage-migration.js +169 -0
- package/lib/runtime/subagent-catalog.js +129 -0
- package/lib/runtime/user-commands.js +86 -0
- package/lib/runtime/user-panels.js +176 -0
- package/lib/runtime/user-store.js +142 -0
- package/lib/types/application/catalog.d.ts +39 -9
- package/lib/types/application/panel-resources.d.ts +13 -0
- package/lib/types/application/project-agent-roles.d.ts +3 -0
- package/lib/types/application/queries.d.ts +17 -1
- package/lib/types/catalog/codex-mcp.d.ts +4 -0
- package/lib/types/catalog/component-files.d.ts +17 -0
- package/lib/types/catalog/manifests.d.ts +14 -9
- package/lib/types/catalog/native-project.d.ts +2 -5
- package/lib/types/catalog/paths.d.ts +4 -4
- package/lib/types/catalog/project-config.d.ts +6 -0
- package/lib/types/catalog/project-hooks.d.ts +10 -0
- package/lib/types/catalog/scan-pipeline.d.ts +2 -0
- package/lib/types/catalog/scan-resolvers.d.ts +4 -3
- package/lib/types/catalog/source-catalog.d.ts +1 -1
- package/lib/types/catalog/suite-components.d.ts +5 -0
- package/lib/types/catalog/surfaces.d.ts +2 -2
- package/lib/types/catalog/validate.d.ts +2 -0
- package/lib/types/contracts/market.d.ts +60 -0
- package/lib/types/contracts/mcp-status.d.ts +5 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/model/layouts.d.ts +102 -0
- package/lib/types/model/types.d.ts +51 -6
- package/lib/types/routes.d.ts +6 -1
- package/lib/types/runtime/agent-role-router.d.ts +95 -0
- package/lib/types/runtime/commands-mounts.d.ts +16 -7
- package/lib/types/runtime/feedback-tool.d.ts +49 -0
- package/lib/types/runtime/hooks-mounts.d.ts +4 -0
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/legacy-root-migration.d.ts +2 -0
- package/lib/types/runtime/lsp-mounts.d.ts +4 -0
- package/lib/types/runtime/lsp-server-state.d.ts +3 -0
- package/lib/types/runtime/lsp-status.d.ts +1 -0
- package/lib/types/runtime/mcp-backend.d.ts +9 -1
- package/lib/types/runtime/mcp-client/config.d.ts +4 -3
- package/lib/types/runtime/mcp-client/tools.d.ts +3 -0
- package/lib/types/runtime/mcp-config.d.ts +11 -5
- package/lib/types/runtime/mcp-direct-config.d.ts +7 -0
- package/lib/types/runtime/mcp-mounts.d.ts +2 -1
- package/lib/types/runtime/mcp-overrides.d.ts +2 -0
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/model-catalog.d.ts +7 -0
- package/lib/types/runtime/project-runtime.d.ts +32 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/server-config.d.ts +10 -0
- package/lib/types/runtime/skills-provider.d.ts +1 -5
- package/lib/types/runtime/storage-migration.d.ts +15 -0
- package/lib/types/runtime/subagent-catalog.d.ts +52 -0
- package/lib/types/runtime/user-commands.d.ts +24 -0
- package/lib/types/runtime/user-panels.d.ts +91 -0
- package/lib/types/runtime/user-store.d.ts +51 -0
- package/package.json +10 -3
- package/schemas/README.md +69 -7
- package/schemas/agent-plugins/spec.md +48 -0
- package/schemas/claude-code/marketplace.schema.json +143 -0
- package/schemas/claude-code/plugin.schema.json +105 -0
- package/schemas/claude-code/spec.md +108 -0
- package/schemas/codex/marketplace.schema.json +78 -0
- package/schemas/codex/plugin.schema.json +77 -0
- package/schemas/codex/spec.md +91 -0
- package/schemas/cursor/marketplace.schema.json +82 -0
- package/schemas/cursor/plugin.schema.json +58 -0
- package/schemas/cursor/spec.md +92 -0
- package/schemas/github-copilot/marketplace.schema.json +107 -0
- package/schemas/github-copilot/plugin.schema.json +89 -0
- package/schemas/github-copilot/spec.md +105 -0
- package/schemas/kimi/marketplace.schema.json +38 -0
- package/schemas/kimi/plugin.schema.json +84 -0
- package/schemas/kimi/spec.md +94 -0
- package/schemas/qoder/marketplace.schema.json +115 -0
- package/schemas/qoder/plugin.schema.json +127 -0
- package/schemas/qoder/spec.md +111 -0
- package/schemas/skill-collection/spec.md +29 -0
- package/schemas/universal/marketplace.schema.json +98 -0
- package/schemas/universal/plugin.schema.json +49 -0
- package/schemas/universal/spec.md +53 -0
- package/schemas/zcode/marketplace.schema.json +161 -0
- package/schemas/zcode/plugin.schema.json +89 -0
- package/schemas/zcode/spec.md +102 -0
- package/lib/catalog/legacy-root-migration.js +0 -51
- 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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
22
|
+
userCommandSourceLabel: '用户命令',
|
|
23
|
+
userCommandForwardTitle: '[用户快捷命令 /{command}]',
|
|
24
|
+
userCommandAcknowledged: '/{command} 已转交模型执行',
|
|
25
|
+
userSkillDescription: '[用户技能] {description}',
|
|
26
|
+
feedbackToolCardTitle: '提交市场体验反馈'
|
|
19
27
|
};
|
|
20
28
|
const en = {
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
113
|
+
this.lastDiagnostics.delete(key);
|
|
106
114
|
if (reason !== undefined)
|
|
107
|
-
diagnostics.push({ suiteId:
|
|
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:
|
|
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:
|
|
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:
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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);
|