dsh-agent-plugins-market 0.7.0 → 0.7.1
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 +2 -2
- package/README.zh.md +2 -2
- package/client/client.js +28048 -1033
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +8 -1
- package/lib/application/catalog.js +13 -0
- package/lib/application/details.js +13 -2
- package/lib/application/mcp-service.js +17 -0
- package/lib/application/panel-resources.js +16 -4
- package/lib/application/project-agent-roles.js +7 -1
- package/lib/catalog/archive.js +1 -1
- package/lib/catalog/paths.js +8 -0
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-hooks.js +14 -1
- package/lib/catalog/skills-parse.js +47 -0
- package/lib/catalog/source-catalog.js +8 -3
- package/lib/catalog/suite-components.js +2 -2
- package/lib/contracts/market.js +1 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +26 -11
- package/lib/model/layouts.js +10 -1
- package/lib/routes.js +12 -0
- package/lib/runtime/agent-role-router.js +28 -6
- package/lib/runtime/commands-mounts.js +50 -9
- package/lib/runtime/deadline.js +32 -6
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +7 -3
- package/lib/runtime/host-locale.js +39 -3
- package/lib/runtime/lsp-mounts.js +23 -2
- package/lib/runtime/mcp-backend.js +14 -15
- package/lib/runtime/mcp-client/config.js +8 -0
- package/lib/runtime/mcp-client/connection.js +6 -6
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-config.js +7 -4
- package/lib/runtime/project-runtime.js +23 -9
- package/lib/runtime/reconcile-scheduler.js +6 -8
- package/lib/runtime/reconciler.js +2 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/settings-namespace.js +21 -13
- package/lib/runtime/skills-provider.js +33 -11
- package/lib/runtime/source-auto-update.js +10 -14
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +20 -20
- package/lib/runtime/user-commands.js +5 -1
- package/lib/runtime/user-panels.js +110 -41
- package/lib/runtime/user-store.js +114 -58
- package/lib/types/application/catalog-context.d.ts +6 -0
- package/lib/types/application/catalog.d.ts +9 -0
- package/lib/types/application/mcp-service.d.ts +6 -0
- package/lib/types/application/ports.d.ts +1 -1
- package/lib/types/application/queries.d.ts +2 -0
- package/lib/types/catalog/paths.d.ts +6 -0
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/skills-parse.d.ts +13 -0
- package/lib/types/catalog/source-catalog.d.ts +7 -2
- package/lib/types/contracts/market.d.ts +10 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +10 -0
- package/lib/types/runtime/agent-role-router.d.ts +6 -2
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +10 -11
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/host-locale.d.ts +19 -1
- package/lib/types/runtime/lsp-mounts.d.ts +7 -0
- package/lib/types/runtime/mcp-backend.d.ts +8 -15
- package/lib/types/runtime/mcp-client/config.d.ts +8 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/settings-namespace.d.ts +10 -10
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +3 -4
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +12 -6
- package/lib/types/runtime/user-panels.d.ts +65 -12
- package/lib/types/runtime/user-store.d.ts +65 -16
- package/package.json +21 -4
|
@@ -5,6 +5,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
5
5
|
import { mountSubagentCatalog } from './subagent-catalog.js';
|
|
6
6
|
import { bindHostLocale } from './host-locale.js';
|
|
7
7
|
import { namedAgentRoles } from './agent-role-names.js';
|
|
8
|
+
import { expandPluginPaths } from '../catalog/plugin-variables.js';
|
|
8
9
|
export const AGENT_ROLE_TOOL_NAME = 'subagent_run';
|
|
9
10
|
/**
|
|
10
11
|
* Absolute delegation-depth cap applied to every role child. The host default is
|
|
@@ -113,7 +114,7 @@ export async function resolveAgentOptions(policy, parent, llm, signal, subject,
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
/** Stable summaries; invalid declarations are excluded and transient read failures abort publication. */
|
|
116
|
-
export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
117
|
+
export async function agentRoleCatalog(entries, signal, diagnose = () => { }, projectDir) {
|
|
117
118
|
const summaries = [];
|
|
118
119
|
const counts = new Map();
|
|
119
120
|
for (const entry of entries)
|
|
@@ -137,7 +138,7 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
137
138
|
}
|
|
138
139
|
let policy;
|
|
139
140
|
try {
|
|
140
|
-
policy = parseAgentRole(text);
|
|
141
|
+
policy = expandPolicyPaths(parseAgentRole(text), entry, projectDir);
|
|
141
142
|
}
|
|
142
143
|
catch (error) {
|
|
143
144
|
diagnose(`${entry.path}: ${String(error)}`);
|
|
@@ -160,9 +161,30 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
160
161
|
signal.throwIfAborted();
|
|
161
162
|
return summaries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
162
163
|
}
|
|
164
|
+
/** A suite card carries the same path variables as every other surface its author wrote for. */
|
|
165
|
+
function expandPolicyPaths(policy, entry, projectDir) {
|
|
166
|
+
if (entry.suiteRoot === undefined && entry.suiteData === undefined && projectDir === undefined)
|
|
167
|
+
return policy;
|
|
168
|
+
const context = {
|
|
169
|
+
...(entry.suiteRoot === undefined ? {} : { root: entry.suiteRoot }),
|
|
170
|
+
...(entry.suiteData === undefined ? {} : { data: entry.suiteData }),
|
|
171
|
+
...(projectDir === undefined ? {} : { projectDir })
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
...policy,
|
|
175
|
+
content: expandPluginPaths(policy.content, context),
|
|
176
|
+
...(policy.title === undefined ? {} : { title: expandPluginPaths(policy.title, context) }),
|
|
177
|
+
...(policy.description === undefined ? {} : { description: expandPluginPaths(policy.description, context) })
|
|
178
|
+
};
|
|
179
|
+
}
|
|
163
180
|
/** Read the same declaration for catalog summaries and execution, including inline resources. */
|
|
164
|
-
export async function readAgentRole(entry) {
|
|
165
|
-
return parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8')));
|
|
181
|
+
export async function readAgentRole(entry, projectDir) {
|
|
182
|
+
return expandPolicyPaths(parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8'))), entry, projectDir);
|
|
183
|
+
}
|
|
184
|
+
/** The calling session's directory, which is what a card's `${CLAUDE_PROJECT_DIR}` names. */
|
|
185
|
+
function sessionCwd(agent) {
|
|
186
|
+
const cwd = agent?.session?.header?.cwd;
|
|
187
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
|
|
166
188
|
}
|
|
167
189
|
/**
|
|
168
190
|
* Start one role child and return its durable id without waiting for the result.
|
|
@@ -180,7 +202,7 @@ export async function executeAgentRole(host, listRoles, agentName, prompt, paren
|
|
|
180
202
|
throw new Error(`agent "${agentName}" is unavailable or ambiguous`);
|
|
181
203
|
if (entry.disabled)
|
|
182
204
|
throw new Error(`agent "${agentName}" is disabled`);
|
|
183
|
-
const policy = await readAgentRole(entry);
|
|
205
|
+
const policy = await readAgentRole(entry, sessionCwd(parent));
|
|
184
206
|
if (policy.disabled)
|
|
185
207
|
throw new Error(`agent "${agentName}" is disabled`);
|
|
186
208
|
const agentOptions = await resolveAgentOptions(policy, parent, host.llm, signal, agentName, diagnose);
|
|
@@ -227,7 +249,7 @@ export function mountAgentRoleTool(ctx, listRoles, t = bindHostLocale(undefined)
|
|
|
227
249
|
const disposeTool = host.tools.register(tool);
|
|
228
250
|
let disposeCatalog;
|
|
229
251
|
try {
|
|
230
|
-
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message)), t);
|
|
252
|
+
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message), sessionCwd(agent)), t);
|
|
231
253
|
}
|
|
232
254
|
catch (error) {
|
|
233
255
|
disposeTool();
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
1
2
|
import { parse as parseYaml } from 'yaml';
|
|
2
3
|
import { stripFrontmatter } from '../catalog/skills-parse.js';
|
|
3
4
|
import { parseFrontmatterRecord } from './user-store.js';
|
|
4
|
-
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
5
|
+
import { qualifiedSuiteId, suiteDataDir } from '../catalog/paths.js';
|
|
6
|
+
import { expandPluginPaths, pluginRootOf } from '../catalog/plugin-variables.js';
|
|
5
7
|
import { defaultMarkdownResources, resourceText, resourceCommandName } from '../catalog/component-files.js';
|
|
6
8
|
import { bindHostLocale } from './host-locale.js';
|
|
9
|
+
import { injectDynamicContext, shellSeamOf } from './dynamic-context.js';
|
|
7
10
|
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
8
11
|
export class CommandMountRegistry {
|
|
9
12
|
ctx;
|
|
10
13
|
t;
|
|
14
|
+
dataRoot;
|
|
11
15
|
fingerprints = new Map();
|
|
12
16
|
live = new Map();
|
|
13
|
-
constructor(ctx, t = bindHostLocale(undefined)) {
|
|
17
|
+
constructor(ctx, t = bindHostLocale(undefined), dataRoot) {
|
|
14
18
|
this.ctx = ctx;
|
|
15
19
|
this.t = t;
|
|
20
|
+
this.dataRoot = dataRoot;
|
|
21
|
+
}
|
|
22
|
+
/** The live shell seam, when the profile has one; dynamic context stays literal without it. */
|
|
23
|
+
shell() {
|
|
24
|
+
return shellSeamOf(this.ctx);
|
|
16
25
|
}
|
|
17
26
|
/** Register/unregister suite commands to match the enabled suites exactly. */
|
|
18
27
|
async reconcile(enabledSuites) {
|
|
@@ -20,6 +29,8 @@ export class CommandMountRegistry {
|
|
|
20
29
|
const wanted = new Map();
|
|
21
30
|
for (const suite of enabledSuites) {
|
|
22
31
|
const specs = suite.activeSurfaces.commands === false ? [] : await readCommands(suite.root, suite.resources?.commands);
|
|
32
|
+
const suiteRoot = pluginRootOf(suite);
|
|
33
|
+
const suiteData = suiteRoot === undefined || this.dataRoot === undefined ? undefined : suiteDataDir(this.dataRoot, suite.sourceId, suite.id);
|
|
23
34
|
for (const spec of specs) {
|
|
24
35
|
// The registry key is source-qualified: bare suite ids are unique per
|
|
25
36
|
// source only, so two sources' same-named suites would collide.
|
|
@@ -29,7 +40,13 @@ export class CommandMountRegistry {
|
|
|
29
40
|
diagnostics.push({ suiteId: suiteKey, command: spec.name, reason: 'duplicate normalized command name' });
|
|
30
41
|
continue;
|
|
31
42
|
}
|
|
32
|
-
wanted.set(key, {
|
|
43
|
+
wanted.set(key, {
|
|
44
|
+
...spec,
|
|
45
|
+
suiteId: suiteKey,
|
|
46
|
+
suiteName: suite.manifest.name,
|
|
47
|
+
...(suiteRoot === undefined ? {} : { suiteRoot }),
|
|
48
|
+
...(suiteData === undefined ? {} : { suiteData })
|
|
49
|
+
});
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
52
|
for (const [key, disposer] of [...this.live]) {
|
|
@@ -56,13 +73,37 @@ export class CommandMountRegistry {
|
|
|
56
73
|
name: spec.name,
|
|
57
74
|
description: `[${spec.suiteName}] ${spec.description}`,
|
|
58
75
|
...(spec.hint === undefined ? {} : { input: { hint: spec.hint } }),
|
|
59
|
-
handler: invocation => {
|
|
76
|
+
handler: async (invocation) => {
|
|
60
77
|
const agent = invocation.agent;
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
// wrote
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
const workdir = agent.session?.header?.cwd;
|
|
79
|
+
// The template rides the agent verbatim apart from the placeholders
|
|
80
|
+
// its author wrote: a decorator line naming this plugin or the
|
|
81
|
+
// suite would be text the command author never wrote, and the
|
|
82
|
+
// follow-up's `source` already records provenance.
|
|
83
|
+
const body = expandPluginPaths(spec.body, {
|
|
84
|
+
...(spec.suiteRoot === undefined ? {} : { root: spec.suiteRoot }),
|
|
85
|
+
...(spec.suiteData === undefined ? {} : { data: spec.suiteData }),
|
|
86
|
+
...(workdir === undefined ? {} : { projectDir: workdir })
|
|
87
|
+
}).replaceAll('$ARGUMENTS', invocation.rawInput.trim());
|
|
88
|
+
const shell = this.shell();
|
|
89
|
+
if (shell === undefined) {
|
|
90
|
+
agent.followup(createUserMessage({
|
|
91
|
+
content: [{ type: 'text', text: body }],
|
|
92
|
+
source: { kind: 'plugin', plugin: 'dsh-agent-plugins-market' }
|
|
93
|
+
}));
|
|
94
|
+
return { kind: 'success', text: this.t('commandAcknowledged', { command: spec.name }) };
|
|
95
|
+
}
|
|
96
|
+
let text;
|
|
97
|
+
try {
|
|
98
|
+
text = await injectDynamicContext(body, { shell, ...(workdir === undefined ? {} : { workdir }) });
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return { kind: 'error', text: error instanceof Error ? error.message : String(error) };
|
|
102
|
+
}
|
|
103
|
+
agent.followup(createUserMessage({
|
|
104
|
+
content: [{ type: 'text', text }],
|
|
105
|
+
source: { kind: 'plugin', plugin: 'dsh-agent-plugins-market' }
|
|
106
|
+
}));
|
|
66
107
|
return { kind: 'success', text: this.t('commandAcknowledged', { command: spec.name }) };
|
|
67
108
|
}
|
|
68
109
|
});
|
package/lib/runtime/deadline.js
CHANGED
|
@@ -7,22 +7,48 @@
|
|
|
7
7
|
* browser authorization the user is completing must not be abandoned just
|
|
8
8
|
* because nobody is waiting for it any more.
|
|
9
9
|
*
|
|
10
|
+
* The timer and its reason come from the host's own deadline primitive, which
|
|
11
|
+
* only notifies. This module adds the one thing that primitive does not answer
|
|
12
|
+
* and these callers need: whether the work settled at all.
|
|
13
|
+
*
|
|
10
14
|
* @module runtime/deadline
|
|
11
15
|
*/
|
|
16
|
+
import { deadline } from '@deepseek-ai/dsh-timeout';
|
|
17
|
+
/** Identifies this plugin's bounded waits inside a timeout reason. */
|
|
18
|
+
export const MARKET_DEADLINE_CODE = 'MARKET_DEADLINE';
|
|
12
19
|
/**
|
|
13
20
|
* Resolve `true` once `work` settles, or `false` when `deadlineMs` elapses.
|
|
14
21
|
*
|
|
15
22
|
* A rejection counts as settled: this helper only reports timing, so callers
|
|
16
23
|
* that need the failure attach their own handler to `work`.
|
|
24
|
+
*
|
|
25
|
+
* A non-positive wait means "do not wait": the answer is simply whether the
|
|
26
|
+
* work had already settled. That is deliberately not the host primitive's own
|
|
27
|
+
* reading of a non-positive timeout, which is "arm no timer at all" — the floor
|
|
28
|
+
* below keeps the two from collapsing into opposite behaviors.
|
|
29
|
+
* @param work - the work to wait for; it is never cancelled, only stopped being awaited.
|
|
30
|
+
* @param deadlineMs - how long to wait, in milliseconds.
|
|
31
|
+
* @returns whether the work settled before the deadline.
|
|
17
32
|
*/
|
|
18
33
|
export function settlesWithin(work, deadlineMs) {
|
|
34
|
+
const bound = deadline(undefined, Math.max(1, deadlineMs), MARKET_DEADLINE_CODE);
|
|
35
|
+
let settled = false;
|
|
19
36
|
return new Promise(resolve => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
resolve(
|
|
37
|
+
const settle = (value) => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
settled = true;
|
|
41
|
+
resolve(value);
|
|
25
42
|
};
|
|
26
|
-
|
|
43
|
+
bound.signal.addEventListener('abort', () => {
|
|
44
|
+
settle(false);
|
|
45
|
+
}, { once: true });
|
|
46
|
+
void work.then(() => {
|
|
47
|
+
settle(true);
|
|
48
|
+
}, () => {
|
|
49
|
+
settle(true);
|
|
50
|
+
});
|
|
51
|
+
}).finally(() => {
|
|
52
|
+
bound[Symbol.dispose]();
|
|
27
53
|
});
|
|
28
54
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** How long one injected command may run; the executor caps a larger request. */
|
|
2
|
+
export const DYNAMIC_CONTEXT_TIMEOUT_MS = 120_000;
|
|
3
|
+
/** The requested forward cap for one injected command's stdout, in bytes. */
|
|
4
|
+
export const DYNAMIC_CONTEXT_MAX_OUTPUT_BYTES = 32 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the host shell service from any context, whether or not the reading
|
|
7
|
+
* fiber injects `shell`.
|
|
8
|
+
*
|
|
9
|
+
* Cordis resolves `ctx.shell` by walking the reading fiber's ancestors, so a
|
|
10
|
+
* scope that does not inject the service throws `cannot get property "shell"
|
|
11
|
+
* without inject` even when a sibling fiber provides it. `ctx.get` reads the
|
|
12
|
+
* global service store instead and answers `undefined` in a profile that has
|
|
13
|
+
* no shell, which is what keeps the seam optional.
|
|
14
|
+
*
|
|
15
|
+
* @param ctx - the context the dynamic-context pass runs under.
|
|
16
|
+
* @returns the shell seam, or `undefined` when the profile provides none.
|
|
17
|
+
*/
|
|
18
|
+
export function shellSeamOf(ctx) {
|
|
19
|
+
return ctx.get('shell');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Commands whose exit code 1 is a result rather than a failure, mirroring the
|
|
23
|
+
* shell consumers the harness already treats this way.
|
|
24
|
+
*/
|
|
25
|
+
const BENIGN_EXIT_ONE = new Set(['grep', 'rg', 'egrep', 'fgrep', 'find', 'diff', 'test', '[']);
|
|
26
|
+
const BENIGN_EXIT_ONE_SUBCOMMANDS = new Set(['git diff', 'git grep']);
|
|
27
|
+
const INJECTION = /(^|[ \t])!`([^`\n]+)`|^```!\r?\n([\s\S]*?)^```[ \t]*$/gm;
|
|
28
|
+
/**
|
|
29
|
+
* Run every placeholder's command and return the text the model receives.
|
|
30
|
+
*
|
|
31
|
+
* @param text - author text whose `${...}` variables and arguments are already resolved.
|
|
32
|
+
* @param options - the resolved shell seam, session directory and cancellation signal.
|
|
33
|
+
* @returns the text with each placeholder replaced by its command's output.
|
|
34
|
+
* @throws when a command fails, times out or is aborted, carrying the command and its output.
|
|
35
|
+
*/
|
|
36
|
+
export async function injectDynamicContext(text, options) {
|
|
37
|
+
const injections = findInjections(text);
|
|
38
|
+
if (injections.length === 0)
|
|
39
|
+
return text;
|
|
40
|
+
const parts = [];
|
|
41
|
+
let cursor = 0;
|
|
42
|
+
for (const injection of injections) {
|
|
43
|
+
parts.push(text.slice(cursor, injection.start), await runInjection(injection.command, options));
|
|
44
|
+
cursor = injection.end;
|
|
45
|
+
}
|
|
46
|
+
parts.push(text.slice(cursor));
|
|
47
|
+
return parts.join('');
|
|
48
|
+
}
|
|
49
|
+
/** Locate every placeholder in one pass so an inserted output is never re-scanned. */
|
|
50
|
+
function findInjections(text) {
|
|
51
|
+
const found = [];
|
|
52
|
+
for (const match of text.matchAll(INJECTION)) {
|
|
53
|
+
const index = match.index ?? 0;
|
|
54
|
+
const script = match[3];
|
|
55
|
+
if (script !== undefined) {
|
|
56
|
+
// The capture runs to the closing fence line; the block's own framing
|
|
57
|
+
// whitespace is not part of the script.
|
|
58
|
+
found.push({ start: index, end: index + match[0].length, command: script.trim() });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const command = match[2];
|
|
62
|
+
if (command === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
// The captured prefix stays in the text, so the replacement keeps the
|
|
65
|
+
// author's own leading whitespace.
|
|
66
|
+
const prefix = match[1] ?? '';
|
|
67
|
+
found.push({ start: index + prefix.length, end: index + match[0].length, command });
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
/** Run one placeholder's command and return its output, or throw with the command's own words. */
|
|
72
|
+
async function runInjection(command, options) {
|
|
73
|
+
const spec = options.shell.resolve({
|
|
74
|
+
command,
|
|
75
|
+
...(options.workdir === undefined ? {} : { workdir: options.workdir }),
|
|
76
|
+
timeoutMs: DYNAMIC_CONTEXT_TIMEOUT_MS,
|
|
77
|
+
stdoutMaxBytes: DYNAMIC_CONTEXT_MAX_OUTPUT_BYTES,
|
|
78
|
+
...(options.signal === undefined ? {} : { signal: options.signal })
|
|
79
|
+
});
|
|
80
|
+
const result = await options.shell.run(spec);
|
|
81
|
+
if (!benignExit(result.exitCode, command))
|
|
82
|
+
throw injectionFailure(command, result);
|
|
83
|
+
const body = stripTrailingNewline(mergeOutputs(result.stdout.text, result.stderr.text));
|
|
84
|
+
if (!result.stdout.truncated && !result.stderr.truncated)
|
|
85
|
+
return body;
|
|
86
|
+
const notice = truncationNotice(result);
|
|
87
|
+
return body === '' ? notice : `${body}\n${notice}`;
|
|
88
|
+
}
|
|
89
|
+
/** Whether this command's exit status is a result rather than a failure. */
|
|
90
|
+
export function benignExit(exitCode, command) {
|
|
91
|
+
if (exitCode === 0)
|
|
92
|
+
return true;
|
|
93
|
+
if (exitCode !== 1)
|
|
94
|
+
return false;
|
|
95
|
+
const [program = '', subcommand = ''] = stripEnvironmentPrefix(command.trim().replace(/\s+/g, ' ')).split(' ');
|
|
96
|
+
// A path-qualified invocation names the same command as its bare spelling.
|
|
97
|
+
if (BENIGN_EXIT_ONE.has(program.split('/').pop() ?? program))
|
|
98
|
+
return true;
|
|
99
|
+
return program === 'git' && BENIGN_EXIT_ONE_SUBCOMMANDS.has(`git ${subcommand}`);
|
|
100
|
+
}
|
|
101
|
+
/** Skip leading `NAME=value` assignments so `FOO=1 grep x` keeps its benign status. */
|
|
102
|
+
function stripEnvironmentPrefix(command) {
|
|
103
|
+
let rest = command;
|
|
104
|
+
for (;;) {
|
|
105
|
+
const match = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/.exec(rest);
|
|
106
|
+
if (match === null)
|
|
107
|
+
return rest;
|
|
108
|
+
rest = rest.slice(match[0].length);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Merge the two captured streams the way the default shell surfaces them. */
|
|
112
|
+
function mergeOutputs(stdout, stderr) {
|
|
113
|
+
if (stderr === '')
|
|
114
|
+
return stdout;
|
|
115
|
+
if (stdout === '')
|
|
116
|
+
return stderr;
|
|
117
|
+
return stdout.endsWith('\n') ? stdout + stderr : `${stdout}\n${stderr}`;
|
|
118
|
+
}
|
|
119
|
+
/** One trailing newline is dropped so the output joins the surrounding text. */
|
|
120
|
+
function stripTrailingNewline(value) {
|
|
121
|
+
return value.endsWith('\n') ? value.slice(0, -1) : value;
|
|
122
|
+
}
|
|
123
|
+
/** What the caller reports when a placeholder's command does not produce a result. */
|
|
124
|
+
function injectionFailure(command, result) {
|
|
125
|
+
const cause = result.timedOut
|
|
126
|
+
? `timed out after ${DYNAMIC_CONTEXT_TIMEOUT_MS}ms`
|
|
127
|
+
: result.aborted
|
|
128
|
+
? 'was aborted'
|
|
129
|
+
: `exited with ${result.exitCode === null ? 'a signal' : `code ${result.exitCode}`}`;
|
|
130
|
+
return new Error([
|
|
131
|
+
`Shell command failed for pattern "!\`${command}\`" (${cause})`,
|
|
132
|
+
...(result.stdout.text === '' ? [] : ['[stdout]', result.stdout.text.trimEnd()]),
|
|
133
|
+
...(result.stderr.text === '' ? [] : ['[stderr]', result.stderr.text.trimEnd()])
|
|
134
|
+
].join('\n'));
|
|
135
|
+
}
|
|
136
|
+
/** Tell the model the injected text is only part of the command's output. */
|
|
137
|
+
function truncationNotice(result) {
|
|
138
|
+
const spill = result.stdout.spillPath ?? result.stderr.spillPath;
|
|
139
|
+
return `[output truncated${spill === undefined ? '' : `; full output: ${spill}`}]`;
|
|
140
|
+
}
|
|
@@ -24,6 +24,7 @@ import { promisify } from 'node:util';
|
|
|
24
24
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
25
25
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
26
26
|
import { platformBrowserOpener } from './mcp-client/oauth.js';
|
|
27
|
+
import { toolsServiceOf } from './tool-registry-observer.js';
|
|
27
28
|
const execFileAsync = promisify(execFile);
|
|
28
29
|
/** The GitHub repository experience feedback files issues against. */
|
|
29
30
|
export const FEEDBACK_REPO_OWNER = 'Sivan757';
|
|
@@ -180,10 +181,13 @@ const FEEDBACK_DESCRIPTION = 'File an experience-feedback issue for dsh-agent-pl
|
|
|
180
181
|
* on this host.
|
|
181
182
|
*/
|
|
182
183
|
export function mountFeedbackTool(hostCtx, dataRoot, t) {
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
// Read the service store rather than the context property: this plugin's own
|
|
185
|
+
// fiber injects `skills` and `commands` only, and the host provides `tools`
|
|
186
|
+
// from a sibling fiber, so a property read throws on every profile.
|
|
187
|
+
const tools = toolsServiceOf(hostCtx);
|
|
188
|
+
if (typeof tools?.register !== 'function')
|
|
185
189
|
return undefined;
|
|
186
|
-
return
|
|
190
|
+
return tools.register(defineTool({
|
|
187
191
|
name: FEEDBACK_TOOL_NAME,
|
|
188
192
|
description: FEEDBACK_DESCRIPTION,
|
|
189
193
|
parameters: {
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* `locale.preference` setting.
|
|
6
6
|
*
|
|
7
7
|
* The web client resolves locale through its own injected service; the host
|
|
8
|
-
* process
|
|
9
|
-
*
|
|
8
|
+
* process reads the same setting through the host settings service once the
|
|
9
|
+
* plugin wires it, and falls back to parsing `$DSH_HOME/settings.yaml` (the
|
|
10
|
+
* document that service serves, and which exists even when the service is not
|
|
11
|
+
* composed). Neither source means zh.
|
|
10
12
|
*/
|
|
11
13
|
import { readFile } from 'node:fs/promises';
|
|
12
14
|
import { join } from 'node:path';
|
|
@@ -42,8 +44,28 @@ export function bindHostLocale(preference) {
|
|
|
42
44
|
return text;
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The active language, read from the host settings service when the plugin has
|
|
49
|
+
* wired one. The service owns the resolved document in memory, so it answers
|
|
50
|
+
* without a file read and without racing a pending write.
|
|
51
|
+
*/
|
|
52
|
+
let hostPreference;
|
|
53
|
+
/**
|
|
54
|
+
* Wire the host settings service as the primary locale source, replacing the
|
|
55
|
+
* `settings.yaml` fallback.
|
|
56
|
+
* @param read - reads `locale.preference` from the settings service.
|
|
57
|
+
*/
|
|
58
|
+
export function setHostLocaleSource(read) {
|
|
59
|
+
hostPreference = read;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The persisted locale preference: the host settings document when the service
|
|
63
|
+
* is composed, else `$DSH_HOME/settings.yaml` (best effort).
|
|
64
|
+
*/
|
|
46
65
|
export async function readLocalePreference() {
|
|
66
|
+
const fromHost = hostPreference?.();
|
|
67
|
+
if (fromHost !== undefined)
|
|
68
|
+
return fromHost;
|
|
47
69
|
try {
|
|
48
70
|
const text = await readFile(join(resolveDshHome(), 'settings.yaml'), 'utf8');
|
|
49
71
|
const match = /^locale:\s*\n(?:[ \t]+preference:\s*'?([^'"\s#]+)'?)/m.exec(text);
|
|
@@ -57,3 +79,17 @@ export async function readLocalePreference() {
|
|
|
57
79
|
export async function loadHostLocale() {
|
|
58
80
|
return { t: bindHostLocale(await readLocalePreference()) };
|
|
59
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Read `locale.preference` from the host settings service.
|
|
84
|
+
*
|
|
85
|
+
* The locale namespace is registered by the host's own client-locale plugin, so
|
|
86
|
+
* this reads `undefined` until that plugin mounts — which is why the caller
|
|
87
|
+
* re-binds when the service arrives rather than capturing the value once.
|
|
88
|
+
* @param settingsCtx - a context carrying the `settings` service.
|
|
89
|
+
*/
|
|
90
|
+
export function readHostLocalePreference(settingsCtx) {
|
|
91
|
+
const settings = settingsCtx.settings;
|
|
92
|
+
const section = settings?.get?.('locale');
|
|
93
|
+
const preference = section?.preference;
|
|
94
|
+
return typeof preference === 'string' ? preference : undefined;
|
|
95
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DIRECT_LSP_SUITE_ID } from './lsp-status.js';
|
|
2
2
|
import { SerialPassQueue, RetryScheduler } from './mount-lifecycle.js';
|
|
3
3
|
import { describeLegacySeam, findLegacyLspSeams } from './profile-seam.js';
|
|
4
|
-
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
4
|
+
import { qualifiedSuiteId, suiteDataDir } from '../catalog/paths.js';
|
|
5
|
+
import { expandPluginPaths, pluginRootOf } from '../catalog/plugin-variables.js';
|
|
5
6
|
import { effectiveSurfaces } from '../model/types.js';
|
|
6
7
|
/** Build one dsh-lsp-stdio Config.servers entry from a normalized spec. */
|
|
7
8
|
export function toLspServerConfig(spec) {
|
|
@@ -14,6 +15,15 @@ export function toLspServerConfig(spec) {
|
|
|
14
15
|
...(spec.configuration === undefined ? {} : { configuration: spec.configuration })
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
/** Resolve author-written paths in one declaration before the host spawns the server. */
|
|
19
|
+
export function expandLspServerConfig(config, context) {
|
|
20
|
+
return {
|
|
21
|
+
...config,
|
|
22
|
+
command: expandPluginPaths(config.command, context),
|
|
23
|
+
args: config.args.map(argument => expandPluginPaths(argument, context)),
|
|
24
|
+
...(config.env === undefined ? {} : { env: Object.fromEntries(Object.entries(config.env).map(([key, value]) => [key, expandPluginPaths(value, context)])) })
|
|
25
|
+
};
|
|
26
|
+
}
|
|
17
27
|
/** Import specifiers kept as string literals so the host packages stay dynamically loaded. */
|
|
18
28
|
const LSP_STDIO_IMPORT = '@deepseek-ai/dsh-lsp-stdio';
|
|
19
29
|
const LSP_SERVICE_IMPORT = '@deepseek-ai/dsh-lsp';
|
|
@@ -62,6 +72,8 @@ export class LspMountRegistry {
|
|
|
62
72
|
capabilityFailure;
|
|
63
73
|
/** Latest diagnostic per suite id; cleared when its suite mounts or leaves the wanted set. */
|
|
64
74
|
lastDiagnostics = new Map();
|
|
75
|
+
/** Plugin storage root holding each suite's `${PLUGIN_DATA}` directory; unset leaves the variable verbatim. */
|
|
76
|
+
pluginDataRoot;
|
|
65
77
|
/** Direct (user-configured) server provider; defaults to none. */
|
|
66
78
|
directProvider = async () => ({});
|
|
67
79
|
disabledProvider = async () => new Set();
|
|
@@ -98,9 +110,14 @@ export class LspMountRegistry {
|
|
|
98
110
|
// otherwise shadow each other's mount and diagnostics.
|
|
99
111
|
const key = qualifiedSuiteId(suite.sourceId, suite.id);
|
|
100
112
|
const config = {};
|
|
113
|
+
const root = pluginRootOf(suite);
|
|
114
|
+
const context = {
|
|
115
|
+
...(root === undefined ? {} : { root }),
|
|
116
|
+
...(root === undefined || this.pluginDataRoot === undefined ? {} : { data: suiteDataDir(this.pluginDataRoot, suite.sourceId, suite.id) })
|
|
117
|
+
};
|
|
101
118
|
for (const spec of servers)
|
|
102
119
|
if (!disabled.has(`${key}/${spec.key}`))
|
|
103
|
-
config[`${key}/${spec.key}`] = toLspServerConfig(spec);
|
|
120
|
+
config[`${key}/${spec.key}`] = expandLspServerConfig(toLspServerConfig(spec), context);
|
|
104
121
|
if (Object.keys(config).length > 0)
|
|
105
122
|
wanted.set(key, { suite, config });
|
|
106
123
|
}
|
|
@@ -257,6 +274,10 @@ export class LspMountRegistry {
|
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
276
|
}
|
|
277
|
+
/** Install the plugin storage root per-suite data directories resolve against. */
|
|
278
|
+
setPluginDataRoot(root) {
|
|
279
|
+
this.pluginDataRoot = root;
|
|
280
|
+
}
|
|
260
281
|
/** Install the direct (user-configured) server provider used at reconcile time. */
|
|
261
282
|
setDirectProvider(provider) {
|
|
262
283
|
this.directProvider = provider;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* and legacy SSE) or through the host's `@deepseek-ai/dsh-mcp-client`
|
|
5
5
|
* (compatibility mode: no OAuth, no SSE, but the host-native implementation).
|
|
6
6
|
*
|
|
7
|
-
* The choice is
|
|
8
|
-
*
|
|
7
|
+
* The choice is the `mcpEnhanced` field of the market's host settings
|
|
8
|
+
* namespace (see `contracts/settings.ts`) registered by the plugin's node half:
|
|
9
9
|
* the registration is what makes the host 插件配置 tab serve our card, the
|
|
10
10
|
* client card binds it for state, and the node half watches it to remount
|
|
11
11
|
* servers when the switch flips. `readMcpBackend` remains only as the
|
|
@@ -17,20 +17,19 @@ import { readFile } from 'node:fs/promises';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import z from '@deepseek-ai/schemastery';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
import { MARKET_SETTINGS_DEFAULTS } from '../contracts/settings.js';
|
|
21
|
+
/**
|
|
22
|
+
* Schema of the market settings namespace: the switches this plugin's config
|
|
23
|
+
* card serves, with the defaults declared to the host. The values themselves
|
|
24
|
+
* belong to {@link MARKET_SETTINGS_DEFAULTS}; this schema states them, it does
|
|
25
|
+
* not own them.
|
|
26
|
+
*/
|
|
23
27
|
export const MarketSettingsSchema = z.object({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
downloadRegion: z.union([z.const('auto'), z.const('global'), z.const('china')]).default('auto'),
|
|
30
|
-
/** ON (default) = the `report_market_issue` model tool is registered; OFF = unregistered. */
|
|
31
|
-
feedbackEnabled: z.boolean().default(true),
|
|
32
|
-
/** ON = refresh every configured source in the background; OFF (default) = never. */
|
|
33
|
-
autoUpdateSources: z.boolean().default(false)
|
|
28
|
+
mcpEnhanced: z.boolean().default(MARKET_SETTINGS_DEFAULTS.mcpEnhanced),
|
|
29
|
+
scanProjectLayouts: z.boolean().default(MARKET_SETTINGS_DEFAULTS.scanProjectLayouts),
|
|
30
|
+
downloadRegion: z.union([z.const('auto'), z.const('global'), z.const('china')]).default(MARKET_SETTINGS_DEFAULTS.downloadRegion),
|
|
31
|
+
feedbackEnabled: z.boolean().default(MARKET_SETTINGS_DEFAULTS.feedbackEnabled),
|
|
32
|
+
autoUpdateSources: z.boolean().default(MARKET_SETTINGS_DEFAULTS.autoUpdateSources)
|
|
34
33
|
});
|
|
35
34
|
/** Path of the legacy persisted settings file under the plugin data root. */
|
|
36
35
|
export function marketSettingsPath(dataRoot) {
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** Default timeout for individual MCP tool calls (ms). */
|
|
12
12
|
export const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* How long one connection attempt may spend initializing (ms). A server that
|
|
15
|
+
* never answers would otherwise hold its process, transport and browser leg
|
|
16
|
+
* until the SDK's own 60-second request default expired; a working server
|
|
17
|
+
* handshakes well under a second, and a genuinely slow one can declare its own
|
|
18
|
+
* `startupTimeoutMs`.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
|
|
13
21
|
/** Valid `serverName`, kept below the public tool-name budget. */
|
|
14
22
|
export const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
|
|
15
23
|
/**
|
|
@@ -26,6 +26,7 @@ import { MAX_TIMER_DELAY_MS } from './host-seams.js';
|
|
|
26
26
|
import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
27
27
|
import { createTransport } from './transport.js';
|
|
28
28
|
import { syncTools } from './tools.js';
|
|
29
|
+
import { DEFAULT_STARTUP_TIMEOUT_MS } from './config.js';
|
|
29
30
|
/** Defaults shared by the config and {@link resolveReconnectPolicy}. */
|
|
30
31
|
export const RECONNECT_DEFAULTS = Object.freeze({
|
|
31
32
|
enabled: true,
|
|
@@ -89,9 +90,11 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
89
90
|
registrationFailure: 'contain',
|
|
90
91
|
serverName: config.serverName,
|
|
91
92
|
toolCallTimeoutMs: config.toolCallTimeoutMs,
|
|
93
|
+
// Every attempt is bounded: an unreachable server must not hold its
|
|
94
|
+
// process, transport and browser leg while nothing can arrive.
|
|
95
|
+
startupTimeoutMs: config.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS,
|
|
92
96
|
...(config.enabledTools === undefined ? {} : { enabledTools: config.enabledTools }),
|
|
93
|
-
...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools })
|
|
94
|
-
...(config.startupTimeoutMs === undefined ? {} : { startupTimeoutMs: config.startupTimeoutMs })
|
|
97
|
+
...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools })
|
|
95
98
|
};
|
|
96
99
|
// The initial sync uses 'throw' when failOnStartupError is configured, so
|
|
97
100
|
// a registration conflict propagates to the startup-await path. Re-syncs
|
|
@@ -258,10 +261,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
258
261
|
host.logger.info(message);
|
|
259
262
|
});
|
|
260
263
|
try {
|
|
261
|
-
|
|
262
|
-
await generation.connect(transport);
|
|
263
|
-
else
|
|
264
|
-
await generation.connect(transport, { timeout: config.startupTimeoutMs });
|
|
264
|
+
await generation.connect(transport, { timeout: opts.startupTimeoutMs });
|
|
265
265
|
if (hasClosed()) {
|
|
266
266
|
attemptSettled = true;
|
|
267
267
|
generationDown(generation);
|