dsh-agent-plugins-market 0.8.0 → 0.8.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 +2 -2
- package/README.zh.md +2 -2
- package/client/client.js +1594 -1732
- package/client/client.js.map +1 -1
- package/lib/application/agent-roles.js +53 -0
- package/lib/application/catalog-context.js +2 -2
- package/lib/application/catalog.js +4 -8
- package/lib/{runtime → application}/deadline.js +1 -1
- package/lib/application/details.js +3 -3
- package/lib/application/json-file.js +43 -0
- package/lib/{runtime → application/lsp}/lsp-direct-config.js +9 -7
- package/lib/application/lsp/lsp-server-state.js +17 -0
- package/lib/{runtime → application/lsp}/lsp-status.js +22 -7
- package/lib/{runtime → application/lsp}/profile-seam.js +1 -1
- package/lib/application/lsp-service.js +5 -5
- package/lib/{runtime → application/mcp}/mcp-backend.js +6 -21
- package/lib/{runtime → application/mcp}/mcp-config.js +4 -4
- package/lib/{runtime → application/mcp}/mcp-direct-config.js +8 -49
- package/lib/{runtime → application/mcp}/mcp-overrides.js +3 -5
- package/lib/{runtime → application/mcp}/mcp-status.js +13 -7
- package/lib/application/mcp-service.js +11 -35
- package/lib/application/panel-resources.js +48 -11
- package/lib/{runtime → application/panels}/user-hooks.js +2 -2
- package/lib/{runtime → application/panels}/user-store.js +2 -2
- package/lib/application/ports.js +1 -0
- package/lib/application/project-agent-roles.js +1 -1
- package/lib/{runtime → application}/server-config.js +7 -17
- package/lib/application/snapshot-cache.js +1 -1
- package/lib/application/source-store.js +3 -4
- package/lib/{runtime → application/state}/state-store.js +5 -7
- package/lib/{runtime → application/state}/storage-migration.js +4 -4
- package/lib/catalog/source-catalog.js +0 -4
- package/lib/contracts/lsp.js +1 -0
- package/lib/contracts/market.js +0 -1
- package/lib/contracts/mcp.js +1 -0
- package/lib/index.js +64 -34
- package/lib/routes.js +2 -9
- package/lib/runtime/{agent-role-router.js → agents/agent-role-router.js} +7 -53
- package/lib/runtime/{subagent-catalog.js → agents/subagent-catalog.js} +24 -16
- package/lib/runtime/core/context.js +31 -0
- package/lib/runtime/{reconciler.js → core/reconciler.js} +5 -5
- package/lib/runtime/host/failure-detail.js +58 -0
- package/lib/runtime/{feedback-tool.js → host/feedback-tool.js} +1 -1
- package/lib/runtime/host/host-locale.js +83 -0
- package/lib/runtime/{settings-namespace.js → host/settings-namespace.js} +44 -66
- package/lib/runtime/host/shell-path.js +196 -0
- package/lib/runtime/{tool-registry-observer.js → host/tool-registry-observer.js} +2 -1
- package/lib/runtime/{lsp-mounts.js → lsp/lsp-mounts.js} +69 -20
- package/lib/runtime/{mcp-client → mcp/bridge}/bridge.js +52 -22
- package/lib/runtime/{mcp-client → mcp/bridge}/connection.js +2 -2
- package/lib/runtime/{mcp-client → mcp/bridge}/oauth.js +1 -1
- package/lib/runtime/{mcp-client → mcp/bridge}/tools.js +2 -2
- package/lib/runtime/{mcp-auth-record.js → mcp/mcp-auth-record.js} +1 -1
- package/lib/runtime/{mcp-mounts.js → mcp/mcp-mounts.js} +14 -7
- package/lib/runtime/{user-commands.js → panels/user-commands.js} +3 -3
- package/lib/runtime/{user-panels.js → panels/user-panels.js} +2 -2
- package/lib/runtime/{commands-mounts.js → surfaces/commands-mounts.js} +7 -7
- package/lib/runtime/{hooks-mounts.js → surfaces/hooks-mounts.js} +2 -2
- package/lib/runtime/{project-runtime.js → surfaces/project-runtime.js} +5 -5
- package/lib/runtime/{skills-provider.js → surfaces/skills-provider.js} +4 -4
- package/lib/types/application/agent-roles.d.ts +30 -0
- package/lib/types/application/catalog.d.ts +4 -7
- package/lib/types/application/details.d.ts +1 -1
- package/lib/types/application/json-file.d.ts +12 -0
- package/lib/types/{runtime → application/lsp}/lsp-direct-config.d.ts +1 -1
- package/lib/types/{runtime → application/lsp}/lsp-status.d.ts +3 -3
- package/lib/types/{runtime → application/mcp}/mcp-backend.d.ts +24 -22
- package/lib/types/{runtime/mcp-client/config.d.ts → application/mcp/mcp-bridge-config.d.ts} +11 -1
- package/lib/types/{runtime → application/mcp}/mcp-config.d.ts +6 -7
- package/lib/types/application/mcp/mcp-direct-config.d.ts +17 -0
- package/lib/types/{runtime → application/mcp}/mcp-overrides.d.ts +1 -1
- package/lib/types/{runtime → application/mcp}/mcp-status.d.ts +7 -7
- package/lib/types/application/mcp-service.d.ts +4 -11
- package/lib/types/application/panel-resources.d.ts +9 -2
- package/lib/types/{runtime → application/panels}/user-hooks.d.ts +1 -1
- package/lib/types/{runtime → application/panels}/user-store.d.ts +1 -1
- package/lib/types/application/ports.d.ts +17 -3
- package/lib/types/application/project-agent-roles.d.ts +1 -1
- package/lib/types/application/queries.d.ts +3 -6
- package/lib/types/{runtime → application}/regions.d.ts +1 -1
- package/lib/types/{runtime → application/state}/state-store.d.ts +2 -2
- package/lib/types/catalog/source-catalog.d.ts +0 -2
- package/lib/types/contracts/lsp-status.d.ts +6 -0
- package/lib/types/contracts/lsp.d.ts +16 -0
- package/lib/types/contracts/market.d.ts +1 -4
- package/lib/types/contracts/mcp-status.d.ts +21 -2
- package/lib/types/contracts/mcp.d.ts +20 -0
- package/lib/types/index.d.ts +87 -1
- package/lib/types/model/types.d.ts +0 -14
- package/lib/types/runtime/{agent-role-router.d.ts → agents/agent-role-router.d.ts} +3 -28
- package/lib/types/runtime/{subagent-catalog.d.ts → agents/subagent-catalog.d.ts} +6 -6
- package/lib/types/runtime/core/context.d.ts +14 -0
- package/lib/types/runtime/{reconcile-scheduler.d.ts → core/reconcile-scheduler.d.ts} +2 -2
- package/lib/types/runtime/{reconciler.d.ts → core/reconciler.d.ts} +8 -8
- package/lib/types/runtime/host/failure-detail.d.ts +6 -0
- package/lib/types/runtime/host/host-locale.d.ts +66 -0
- package/lib/types/runtime/{model-catalog.d.ts → host/model-catalog.d.ts} +1 -1
- package/lib/types/runtime/{settings-namespace.d.ts → host/settings-namespace.d.ts} +42 -32
- package/lib/types/runtime/host/shell-path.d.ts +70 -0
- package/lib/types/runtime/{lsp-mounts.d.ts → lsp/lsp-mounts.d.ts} +17 -11
- package/lib/types/runtime/{mcp-client → mcp/bridge}/bridge.d.ts +1 -1
- package/lib/types/runtime/{mcp-client → mcp/bridge}/connection.d.ts +3 -11
- package/lib/types/runtime/{mcp-client → mcp/bridge}/host-contract.d.ts +1 -1
- package/lib/types/runtime/{mcp-client → mcp/bridge}/oauth.d.ts +1 -1
- package/lib/types/runtime/{mcp-client → mcp/bridge}/transport.d.ts +1 -1
- package/lib/types/runtime/{mcp-auth-record.d.ts → mcp/mcp-auth-record.d.ts} +1 -1
- package/lib/types/runtime/{mcp-credentials.d.ts → mcp/mcp-credentials.d.ts} +1 -1
- package/lib/types/runtime/{mcp-mounts.d.ts → mcp/mcp-mounts.d.ts} +5 -11
- package/lib/types/runtime/{user-commands.d.ts → panels/user-commands.d.ts} +1 -1
- package/lib/types/runtime/{user-panels.d.ts → panels/user-panels.d.ts} +1 -1
- package/lib/types/runtime/{commands-mounts.d.ts → surfaces/commands-mounts.d.ts} +2 -2
- package/lib/types/runtime/{hooks-mounts.d.ts → surfaces/hooks-mounts.d.ts} +1 -1
- package/lib/types/runtime/{project-runtime.d.ts → surfaces/project-runtime.d.ts} +3 -3
- package/lib/types/runtime/{skills-provider.d.ts → surfaces/skills-provider.d.ts} +1 -1
- package/package.json +39 -33
- package/lib/runtime/host-locale.js +0 -89
- package/lib/runtime/legacy-root-migration.js +0 -12
- package/lib/runtime/lsp-server-state.js +0 -19
- package/lib/runtime/mcp-client/json-schema-subset.js +0 -181
- package/lib/types/runtime/host-locale.d.ts +0 -31
- package/lib/types/runtime/legacy-root-migration.d.ts +0 -2
- package/lib/types/runtime/mcp-client/json-schema-subset.d.ts +0 -58
- package/lib/types/runtime/mcp-direct-config.d.ts +0 -39
- /package/lib/{runtime/mcp-client/config.js → application/mcp/mcp-bridge-config.js} +0 -0
- /package/lib/{runtime → application/mcp}/mcp-redaction.js +0 -0
- /package/lib/{runtime → application}/regions.js +0 -0
- /package/lib/runtime/{agent-role-names.js → agents/agent-role-names.js} +0 -0
- /package/lib/runtime/{mount-lifecycle.js → core/mount-lifecycle.js} +0 -0
- /package/lib/runtime/{reconcile-scheduler.js → core/reconcile-scheduler.js} +0 -0
- /package/lib/runtime/{source-auto-update.js → core/source-auto-update.js} +0 -0
- /package/lib/runtime/{timer-seat.js → core/timer-seat.js} +0 -0
- /package/lib/runtime/{model-catalog.js → host/model-catalog.js} +0 -0
- /package/lib/runtime/{plugin-message-source.js → host/plugin-message-source.js} +0 -0
- /package/lib/runtime/{mcp-client → mcp/bridge}/host-contract.js +0 -0
- /package/lib/runtime/{mcp-client → mcp/bridge}/host-seams.js +0 -0
- /package/lib/runtime/{mcp-client → mcp/bridge}/plugin-identity.js +0 -0
- /package/lib/runtime/{mcp-client → mcp/bridge}/projection.js +0 -0
- /package/lib/runtime/{mcp-client → mcp/bridge}/transport.js +0 -0
- /package/lib/runtime/{mcp-credentials.js → mcp/mcp-credentials.js} +0 -0
- /package/lib/runtime/{dynamic-context.js → surfaces/dynamic-context.js} +0 -0
- /package/lib/types/{runtime → application}/deadline.d.ts +0 -0
- /package/lib/types/{runtime → application/lsp}/lsp-server-state.d.ts +0 -0
- /package/lib/types/{runtime → application/lsp}/profile-seam.d.ts +0 -0
- /package/lib/types/{runtime → application/mcp}/mcp-redaction.d.ts +0 -0
- /package/lib/types/{runtime → application}/server-config.d.ts +0 -0
- /package/lib/types/{runtime → application/state}/storage-migration.d.ts +0 -0
- /package/lib/types/runtime/{agent-role-names.d.ts → agents/agent-role-names.d.ts} +0 -0
- /package/lib/types/runtime/{mount-lifecycle.d.ts → core/mount-lifecycle.d.ts} +0 -0
- /package/lib/types/runtime/{source-auto-update.d.ts → core/source-auto-update.d.ts} +0 -0
- /package/lib/types/runtime/{timer-seat.d.ts → core/timer-seat.d.ts} +0 -0
- /package/lib/types/runtime/{feedback-tool.d.ts → host/feedback-tool.d.ts} +0 -0
- /package/lib/types/runtime/{plugin-message-source.d.ts → host/plugin-message-source.d.ts} +0 -0
- /package/lib/types/runtime/{tool-registry-observer.d.ts → host/tool-registry-observer.d.ts} +0 -0
- /package/lib/types/runtime/{mcp-client → mcp/bridge}/host-seams.d.ts +0 -0
- /package/lib/types/runtime/{mcp-client → mcp/bridge}/plugin-identity.d.ts +0 -0
- /package/lib/types/runtime/{mcp-client → mcp/bridge}/projection.d.ts +0 -0
- /package/lib/types/runtime/{mcp-client → mcp/bridge}/tools.d.ts +0 -0
- /package/lib/types/runtime/{dynamic-context.d.ts → surfaces/dynamic-context.d.ts} +0 -0
|
@@ -61,26 +61,34 @@ const CATALOG_UPDATED = 'The available subagent roles changed. This complete lis
|
|
|
61
61
|
const CATALOG_PROMPT = 'Writing the prompt: brief the child like a colleague who just walked into the room — it has not seen this conversation, does not know what you tried, and does not know why the task matters. Explain what you are trying to accomplish, describe what you learned and ruled out, and give enough context for the child to make judgment calls. The child cannot see this conversation, so prompt must be self-contained: the task itself, the relevant files and known findings, the output you expect, and the boundaries of the task (research only, or may edit files). Terse command-style prompts produce shallow, generic work.';
|
|
62
62
|
const CATALOG_USAGE = [
|
|
63
63
|
'Usage notes:',
|
|
64
|
-
|
|
64
|
+
"- Call subagent_role with the exact catalog name as agent. A child has its own context: it starts without this conversation, so it suits self-contained work that one briefing can state in full. If no listed role matches, do not substitute a similarly named one — delegate through one of the host's general delegation channels instead, and do not load these roles through skill or slash commands.",
|
|
65
65
|
'- By default the call returns a durable subagent id immediately and runs in the background without blocking you. A child usually runs for minutes: spend that time advancing independent work that does not depend on it, rather than idling — and do not poll it or re-check its progress. While it runs, send_message adds an instruction or more material and list_agents reports its status.',
|
|
66
|
-
|
|
67
|
-
"- run_in_background false runs one foreground child and returns its report as this call's result instead of a settlement notice. Use it only when your next action depends on the result.",
|
|
66
|
+
"- run_in_background true runs the same child as a tracked background job and returns a job id instead: collect it with job_output and stop it with job_kill. run_in_background false runs one foreground child and returns its report as this call's result — use it when your next action depends on the result and no independent work remains.",
|
|
68
67
|
'- A question a child asks while it runs goes unanswered: nothing will reply on your behalf. So state the prompt in full the first time, and a child should decide for itself, keep going, and list in its final reply which choices it made alone and what information it still lacked.',
|
|
69
|
-
"- A background child's output is not visible to the user
|
|
70
|
-
"- The child's settlement notice arrives as a user message, not as a tool result. Until it arrives, do not assume or predict its findings, and do not deliver anything that depends on them; act on them once it lands, by verifying or delegating further as needed.",
|
|
68
|
+
"- The settlement notice arrives as a user message, not as a tool result. Until it arrives, do not assume or predict its findings, and do not deliver anything that depends on them. A background child's output is not visible to the user: when it finishes, summarize the result to the user yourself.",
|
|
71
69
|
"- The child's final reply is its own report: check its assertions against the files themselves, and treat its statements about its own configuration the same way — a child cannot see how its role instructions were installed.",
|
|
72
|
-
|
|
73
|
-
'- Do not duplicate work a child is already doing.',
|
|
74
|
-
'- When several children run at once, give each its own git worktree and name the files it owns in prompt.'
|
|
70
|
+
'- Do not duplicate work a child is already doing. When several children run at once, give each its own git worktree and name the files it owns in prompt.'
|
|
75
71
|
].join('\n');
|
|
76
|
-
const CATALOG_WHEN_NOT_TO_USE = "When NOT to use a role child: no listed role matches; your next action needs the result; or another child is already doing this work. When no role matches, do not substitute a similarly named one — delegate through one of the host's general delegation channels instead. Do not load these roles through skill or slash commands.";
|
|
77
72
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
* The positive half of the delegation decision, published with every catalog
|
|
74
|
+
* ahead of the role list so the model reads the triggers before the names.
|
|
75
|
+
* Delegation had only negative framing before this block: every other paragraph
|
|
76
|
+
* states what must not happen, and none states when delegating is the default.
|
|
77
|
+
*/
|
|
78
|
+
const CATALOG_WHEN_TO_DELEGATE = [
|
|
79
|
+
'Delegate proactively: hand self-contained work to a role child by default instead of doing it inline. Delegate when any of these holds:',
|
|
80
|
+
'- The task would burn many tool calls or file reads whose raw output would flood this conversation — codebase exploration, tracing behavior across files, digesting long logs or reports. The child reads everything and returns only the distilled result.',
|
|
81
|
+
'- The task is a complete unit one briefing can state: a scoped implementation, a review, an analysis, a document.',
|
|
82
|
+
'- Two or more such tasks are independent: start all the children in one message and let them run in parallel.',
|
|
83
|
+
'Decide by the briefing test: if you can state the task in full and it needs no further input from the user, delegate it and keep working.'
|
|
84
|
+
].join('\n');
|
|
85
|
+
/**
|
|
86
|
+
* Model-facing catalog text: the delegation triggers, then the role list, then
|
|
87
|
+
* the same guidance on first publication and on every update, so a replacement
|
|
88
|
+
* never arrives without the rules that govern it. The guidance covers when a
|
|
89
|
+
* role child is the default, how to brief one, and how to work with one while
|
|
90
|
+
* it runs; the per-channel arbitration between the host's delegation tools
|
|
91
|
+
* stays in the tool descriptions the harness owns.
|
|
84
92
|
*
|
|
85
93
|
* Role lines carry a name and a description only. The configured route stays in
|
|
86
94
|
* the durable entries for execution and change detection; publishing it would
|
|
@@ -91,12 +99,12 @@ export function renderCatalogText(entries, update) {
|
|
|
91
99
|
return [
|
|
92
100
|
'<system-reminder>',
|
|
93
101
|
update ? CATALOG_UPDATED : CATALOG_INTRO,
|
|
102
|
+
CATALOG_WHEN_TO_DELEGATE,
|
|
94
103
|
'<available_subagents>',
|
|
95
104
|
...lines,
|
|
96
105
|
'</available_subagents>',
|
|
97
106
|
CATALOG_PROMPT,
|
|
98
107
|
CATALOG_USAGE,
|
|
99
|
-
CATALOG_WHEN_NOT_TO_USE,
|
|
100
108
|
'</system-reminder>'
|
|
101
109
|
].join('\n');
|
|
102
110
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tolerant reads of optional services off a cordis context.
|
|
3
|
+
* @module runtime/core/context
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Read one optional service off a cordis context, tolerating every access
|
|
7
|
+
* style the host shows.
|
|
8
|
+
*
|
|
9
|
+
* Cordis strict mode throws instead of returning `undefined` for an
|
|
10
|
+
* unregistered service, and contexts vary in which access style they expose —
|
|
11
|
+
* some the bare property, some only a `get()` accessor — so each read is
|
|
12
|
+
* guarded and absence always comes back as `undefined`, never a throw.
|
|
13
|
+
*/
|
|
14
|
+
export function optionalService(ctx, name) {
|
|
15
|
+
try {
|
|
16
|
+
const direct = ctx?.[name];
|
|
17
|
+
if (direct !== undefined && direct !== null)
|
|
18
|
+
return direct;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Strict mode throws on the property read too; fall through to `get()`.
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const get = ctx?.get;
|
|
25
|
+
const service = typeof get === 'function' ? get.call(ctx, name) : undefined;
|
|
26
|
+
return service === null ? undefined : service;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { CommandMountRegistry } from '
|
|
2
|
-
import { HooksMountRegistry } from '
|
|
3
|
-
import { LspMountRegistry } from '
|
|
4
|
-
import { McpMountRegistry } from '
|
|
5
|
-
import { bindHostLocale } from '
|
|
1
|
+
import { CommandMountRegistry } from '../surfaces/commands-mounts.js';
|
|
2
|
+
import { HooksMountRegistry } from '../surfaces/hooks-mounts.js';
|
|
3
|
+
import { LspMountRegistry } from '../lsp/lsp-mounts.js';
|
|
4
|
+
import { McpMountRegistry } from '../mcp/mcp-mounts.js';
|
|
5
|
+
import { bindHostLocale } from '../host/host-locale.js';
|
|
6
6
|
export class RuntimeReconciler {
|
|
7
7
|
mcp;
|
|
8
8
|
commands;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The messages under one error's `cause` chain.
|
|
3
|
+
*
|
|
4
|
+
* The layers under the mount registries put the useful part of a failure in
|
|
5
|
+
* `cause`: a client wraps a startup failure in one fixed sentence and hands
|
|
6
|
+
* the spawn, transport, or handshake error beneath it. A status row keeps that
|
|
7
|
+
* chain so the panel can offer the detail behind a disclosure instead of
|
|
8
|
+
* showing the wrapper sentence alone.
|
|
9
|
+
*
|
|
10
|
+
* @module runtime/failure-detail
|
|
11
|
+
*/
|
|
12
|
+
import { redactErrorMessage } from '../../application/mcp/mcp-redaction.js';
|
|
13
|
+
/** How many cause messages one failure keeps; a mount failure's chain is short. */
|
|
14
|
+
const MAX_CAUSES = 4;
|
|
15
|
+
/** The message text of an error, a thrown string, or an object carrying one. */
|
|
16
|
+
function messageOf(value) {
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return value;
|
|
19
|
+
if (value instanceof Error)
|
|
20
|
+
return value.message;
|
|
21
|
+
if (typeof value === 'object' && value !== null && 'message' in value) {
|
|
22
|
+
const message = value.message;
|
|
23
|
+
if (typeof message === 'string')
|
|
24
|
+
return message;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/** The next link of a chain: `cause` on an error, or on an object carrying one. */
|
|
29
|
+
function causeOf(value) {
|
|
30
|
+
if (value instanceof Error)
|
|
31
|
+
return value.cause;
|
|
32
|
+
if (typeof value === 'object' && value !== null && 'cause' in value)
|
|
33
|
+
return value.cause;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The chain of messages below one error, outermost first, redacted and
|
|
38
|
+
* de-duplicated. A repeated link ends the walk, so a chain that points back at
|
|
39
|
+
* itself cannot loop.
|
|
40
|
+
*/
|
|
41
|
+
export function causeMessages(error) {
|
|
42
|
+
const messages = [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
let current = causeOf(error);
|
|
45
|
+
while (current !== undefined && current !== null && messages.length < MAX_CAUSES) {
|
|
46
|
+
if (seen.has(current))
|
|
47
|
+
break;
|
|
48
|
+
seen.add(current);
|
|
49
|
+
const message = messageOf(current);
|
|
50
|
+
if (message !== undefined) {
|
|
51
|
+
const redacted = redactErrorMessage(message);
|
|
52
|
+
if (redacted !== '' && !messages.includes(redacted))
|
|
53
|
+
messages.push(redacted);
|
|
54
|
+
}
|
|
55
|
+
current = causeOf(current);
|
|
56
|
+
}
|
|
57
|
+
return messages;
|
|
58
|
+
}
|
|
@@ -23,7 +23,7 @@ import { join } from 'node:path';
|
|
|
23
23
|
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
|
-
import { platformBrowserOpener } from '
|
|
26
|
+
import { platformBrowserOpener } from '../mcp/bridge/oauth.js';
|
|
27
27
|
import { toolsServiceOf } from './tool-registry-observer.js';
|
|
28
28
|
const execFileAsync = promisify(execFile);
|
|
29
29
|
/** The GitHub repository experience feedback files issues against. */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side runtime locale: bilingual copy for the host-facing strings a person
|
|
3
|
+
* reads, resolved from the harness's `locale.preference` setting.
|
|
4
|
+
*
|
|
5
|
+
* The model-facing subagent catalog is not here. It is fixed English, because it
|
|
6
|
+
* is a contract read alongside the harness's own English tool descriptions and
|
|
7
|
+
* must not change with the operator's interface language.
|
|
8
|
+
*
|
|
9
|
+
* The web client resolves locale through its own injected service. The host
|
|
10
|
+
* process reads the same setting off the harness settings service, which
|
|
11
|
+
* projects every active profile entry's live configuration: the `locale` entry
|
|
12
|
+
* carries `preference`. Every absence — no settings service, the entry not
|
|
13
|
+
* active, no preference written, a value that is not a string — means zh.
|
|
14
|
+
*/
|
|
15
|
+
const zh = {
|
|
16
|
+
commandAcknowledged: '/{command} 已转交模型执行',
|
|
17
|
+
userCommandSourceLabel: '用户命令',
|
|
18
|
+
feedbackToolCardTitle: '提交市场体验反馈'
|
|
19
|
+
};
|
|
20
|
+
const en = {
|
|
21
|
+
commandAcknowledged: '/{command} forwarded to the model for execution',
|
|
22
|
+
userCommandSourceLabel: 'user command',
|
|
23
|
+
feedbackToolCardTitle: 'File market feedback'
|
|
24
|
+
};
|
|
25
|
+
const DICTS = { zh, en };
|
|
26
|
+
/**
|
|
27
|
+
* The settings entry the harness locale plugin owns. The web bundle patch
|
|
28
|
+
* inserts it as `- id: locale` / `name: '@deepseek-ai/dsh-client-locale'`, and
|
|
29
|
+
* that package's host half declares `Config = { preference: volatile(string) }`.
|
|
30
|
+
*/
|
|
31
|
+
export const LOCALE_SETTINGS_ENTRY = 'locale';
|
|
32
|
+
/** Resolve the active host language from settings; unknown values default to zh. */
|
|
33
|
+
export function bindHostLocale(preference) {
|
|
34
|
+
const dict = preference !== undefined && preference.toLowerCase().startsWith('en') ? DICTS.en : DICTS.zh;
|
|
35
|
+
return (key, params) => {
|
|
36
|
+
let text = dict[key];
|
|
37
|
+
if (params !== undefined) {
|
|
38
|
+
for (const [name, value] of Object.entries(params))
|
|
39
|
+
text = text.replaceAll(`{${name}}`, value);
|
|
40
|
+
}
|
|
41
|
+
return text;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
let hostLocaleSource;
|
|
45
|
+
/**
|
|
46
|
+
* Wire the reader the host-facing copy resolves through. The caller registers
|
|
47
|
+
* the returned disposer with its own effects, so the wiring lives exactly as
|
|
48
|
+
* long as the fiber that installed it; the disposer clears this reader only, so
|
|
49
|
+
* a reload that installs a newer reader first keeps the newer one.
|
|
50
|
+
* @param read - reads the active `locale.preference`, or `undefined`.
|
|
51
|
+
* @returns the disposer that clears this wiring.
|
|
52
|
+
*/
|
|
53
|
+
export function setHostLocaleSource(read) {
|
|
54
|
+
hostLocaleSource = read;
|
|
55
|
+
return () => {
|
|
56
|
+
if (hostLocaleSource === read)
|
|
57
|
+
hostLocaleSource = undefined;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The persisted locale preference: what {@link setHostLocaleSource} wired, and
|
|
62
|
+
* `undefined` while the entry has not wired a reader.
|
|
63
|
+
*/
|
|
64
|
+
export function readLocalePreference() {
|
|
65
|
+
return hostLocaleSource?.();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read `locale.preference` off the settings service's entry projection.
|
|
69
|
+
*
|
|
70
|
+
* `ctx.get` resolves the service from the store, and the store answers for as
|
|
71
|
+
* long as the provider is active, so the caller can read it from the entry's
|
|
72
|
+
* own context. The property read this replaces walks the reading fiber's
|
|
73
|
+
* ancestors and throws `cannot get required service "settings" in inactive
|
|
74
|
+
* context` once that fiber unloads, the state a profile reload leaves an
|
|
75
|
+
* injection callback's context in.
|
|
76
|
+
* @param settings - the host settings service, when the profile mounts one.
|
|
77
|
+
* @returns the preference string, or `undefined` for every absence.
|
|
78
|
+
*/
|
|
79
|
+
export function readHostLocalePreference(settings) {
|
|
80
|
+
const locale = settings?.describe().find(entry => entry.ns === LOCALE_SETTINGS_ENTRY);
|
|
81
|
+
const preference = locale?.value?.preference;
|
|
82
|
+
return typeof preference === 'string' ? preference : undefined;
|
|
83
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveMarketSettings } from '../../contracts/settings.js';
|
|
2
2
|
import { FEEDBACK_TOOL_NAME, mountFeedbackTool } from './feedback-tool.js';
|
|
3
|
-
import { MarketSettingsSchema, readMcpBackend } from './mcp-backend.js';
|
|
4
3
|
export class MarketSettingsNamespace {
|
|
5
4
|
ctx;
|
|
5
|
+
refs;
|
|
6
6
|
dataRoot;
|
|
7
7
|
locale;
|
|
8
8
|
host;
|
|
9
|
-
scope;
|
|
10
9
|
watchers = [];
|
|
11
10
|
feedbackDisposer;
|
|
12
11
|
/**
|
|
@@ -16,87 +15,66 @@ export class MarketSettingsNamespace {
|
|
|
16
15
|
* switch, or skipped because the host has no tools registry to register on.
|
|
17
16
|
*/
|
|
18
17
|
feedbackMountState;
|
|
19
|
-
constructor(ctx, dataRoot, locale, host) {
|
|
18
|
+
constructor(ctx, refs, dataRoot, locale, host) {
|
|
20
19
|
this.ctx = ctx;
|
|
20
|
+
this.refs = refs;
|
|
21
21
|
this.dataRoot = dataRoot;
|
|
22
22
|
this.locale = locale;
|
|
23
23
|
this.host = host;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/** Subscribe the runtime reactions to live settings updates. */
|
|
26
26
|
mount() {
|
|
27
|
-
this.
|
|
27
|
+
this.syncProjectLayouts();
|
|
28
|
+
this.syncAutoUpdateSources();
|
|
29
|
+
this.syncFeedbackTool();
|
|
30
|
+
let previousBackend = this.settings().mcpEnhanced;
|
|
31
|
+
const watcher = this.ctx.on('loader/volatile-update', () => {
|
|
32
|
+
const backend = this.settings().mcpEnhanced;
|
|
33
|
+
if (backend !== previousBackend) {
|
|
34
|
+
previousBackend = backend;
|
|
35
|
+
this.host.refreshMcpMounts();
|
|
36
|
+
}
|
|
37
|
+
this.syncProjectLayouts();
|
|
38
|
+
this.syncAutoUpdateSources();
|
|
39
|
+
this.syncFeedbackTool();
|
|
40
|
+
});
|
|
41
|
+
this.watchers.push(watcher);
|
|
42
|
+
}
|
|
43
|
+
/** Release every watcher and unmount the feedback tool. */
|
|
44
|
+
dispose() {
|
|
45
|
+
for (const release of this.watchers.splice(0))
|
|
46
|
+
release();
|
|
47
|
+
this.feedbackDisposer?.();
|
|
48
|
+
this.feedbackDisposer = undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The resolved settings, from the volatile references the host updates in place.
|
|
52
|
+
*/
|
|
53
|
+
settings() {
|
|
54
|
+
return resolveMarketSettings({
|
|
55
|
+
mcpEnhanced: this.refs.mcpEnhanced.get(),
|
|
56
|
+
scanProjectLayouts: this.refs.scanProjectLayouts.get(),
|
|
57
|
+
downloadRegion: this.refs.downloadRegion.get(),
|
|
58
|
+
feedbackEnabled: this.refs.feedbackEnabled.get(),
|
|
59
|
+
autoUpdateSources: this.refs.autoUpdateSources.get()
|
|
60
|
+
});
|
|
28
61
|
}
|
|
29
|
-
/** The persisted MCP backend choice; the built-in client until
|
|
62
|
+
/** The persisted MCP backend choice; the built-in client until the host applies a value. */
|
|
30
63
|
async backend() {
|
|
31
64
|
return this.settings().mcpEnhanced ? 'builtin' : 'host';
|
|
32
65
|
}
|
|
33
66
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
67
|
+
* Legacy write path for the backend choice: the value now lives in the host
|
|
68
|
+
* settings document, edited from the Plugins panel's configuration page, so
|
|
69
|
+
* this accepts the request and lets the volatile reference decide.
|
|
36
70
|
*/
|
|
37
71
|
async setBackend(backend) {
|
|
38
|
-
|
|
39
|
-
throw new Error('the settings service is not mounted');
|
|
40
|
-
await this.scope.update({ mcpEnhanced: backend !== 'host' });
|
|
72
|
+
this.ctx.logger?.info?.(`[dsh-agent-plugins-market] legacy MCP backend write (${backend}) ignored — the value lives in the host settings document`);
|
|
41
73
|
}
|
|
42
74
|
/** The persisted download-region setting. */
|
|
43
75
|
async downloadRegion() {
|
|
44
76
|
return this.settings().downloadRegion;
|
|
45
77
|
}
|
|
46
|
-
/**
|
|
47
|
-
* The resolved settings, whether or not the namespace is registered yet.
|
|
48
|
-
*
|
|
49
|
-
* Every reader goes through here so "the field is absent" has exactly one
|
|
50
|
-
* answer — {@link resolveMarketSettings} — instead of each caller inventing
|
|
51
|
-
* its own fallback for the window before registration lands.
|
|
52
|
-
*/
|
|
53
|
-
settings() {
|
|
54
|
-
return resolveMarketSettings(this.scope?.get());
|
|
55
|
-
}
|
|
56
|
-
/** Release every watcher and unmount the feedback tool. */
|
|
57
|
-
dispose() {
|
|
58
|
-
for (const release of this.watchers.splice(0))
|
|
59
|
-
release();
|
|
60
|
-
this.feedbackDisposer?.();
|
|
61
|
-
this.feedbackDisposer = undefined;
|
|
62
|
-
}
|
|
63
|
-
register(settingsCtx) {
|
|
64
|
-
try {
|
|
65
|
-
this.ctx.logger?.info?.('[dsh-agent-plugins-market] settings inject resolved — registering namespace');
|
|
66
|
-
const settings = settingsCtx.settings;
|
|
67
|
-
const scope = settings.register(MARKET_SETTINGS_NAMESPACE, MarketSettingsSchema);
|
|
68
|
-
this.scope = scope;
|
|
69
|
-
this.ctx.logger?.info?.('[dsh-agent-plugins-market] settings namespace registered — plugin-config card will serve');
|
|
70
|
-
this.syncProjectLayouts();
|
|
71
|
-
this.watchers.push(scope.watch(() => this.syncProjectLayouts()));
|
|
72
|
-
this.syncAutoUpdateSources();
|
|
73
|
-
this.watchers.push(scope.watch(() => this.syncAutoUpdateSources()));
|
|
74
|
-
// One-time migration from the earlier data-root settings.json choice.
|
|
75
|
-
void readMcpBackend(this.dataRoot).then(backend => {
|
|
76
|
-
if (backend === 'host')
|
|
77
|
-
void scope.update({ mcpEnhanced: false }).catch(() => { });
|
|
78
|
-
});
|
|
79
|
-
let previousBackend = this.settings().mcpEnhanced;
|
|
80
|
-
this.watchers.push(scope.watch(() => {
|
|
81
|
-
const backend = this.settings().mcpEnhanced;
|
|
82
|
-
if (backend === previousBackend)
|
|
83
|
-
return;
|
|
84
|
-
previousBackend = backend;
|
|
85
|
-
this.host.refreshMcpMounts();
|
|
86
|
-
}));
|
|
87
|
-
// The experience-feedback model tool: gated by the namespace's
|
|
88
|
-
// `feedbackEnabled` field (default on); the switch unregisters it. The
|
|
89
|
-
// tool mount is doubly contained — its failure must never take the
|
|
90
|
-
// settings namespace (and with it the config card) down with it. The
|
|
91
|
-
// mount outcome is logged on every transition so an absent
|
|
92
|
-
// `report_market_issue` in a session is traceable to its cause.
|
|
93
|
-
this.syncFeedbackTool();
|
|
94
|
-
this.watchers.push(scope.watch(() => this.syncFeedbackTool()));
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
this.ctx.logger?.error?.(`[dsh-agent-plugins-market] settings namespace registration failed — the plugin-config card will be hidden this boot: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
78
|
/** Apply the background source-update switch. */
|
|
101
79
|
syncAutoUpdateSources() {
|
|
102
80
|
try {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login-shell command resolution for bare commands a suite declares
|
|
3
|
+
* (`npx`, `uvx`, a language-server bare name…).
|
|
4
|
+
*
|
|
5
|
+
* The desktop Host starts from Finder/Dock, so its `PATH` comes from launchd
|
|
6
|
+
* and holds only the system directories; the user's toolchain (`npx`, `uvx`,
|
|
7
|
+
* language servers) lives in directories a login shell adds. The plugin
|
|
8
|
+
* therefore probes the user's login shell once, lazily, and appends the
|
|
9
|
+
* directories the current `PATH` lacks — after the current entries, never
|
|
10
|
+
* before, so no existing priority changes. Nothing here rewrites a declared
|
|
11
|
+
* command: the bare name stays the command and the child inherits a `PATH`
|
|
12
|
+
* that can find it.
|
|
13
|
+
*
|
|
14
|
+
* The probe is deliberately narrow: only `darwin`, only the user's own login
|
|
15
|
+
* shell, and only when a first resolution against the current environment
|
|
16
|
+
* failed. A healthy environment therefore behaves exactly as before; only a
|
|
17
|
+
* command that would fail with `ENOENT` today takes the extra step. Every
|
|
18
|
+
* failure falls back to the unmodified environment and leaves a diagnostic
|
|
19
|
+
* line behind, so a resolution that still fails can explain what was searched.
|
|
20
|
+
*
|
|
21
|
+
* @module runtime/shell-path
|
|
22
|
+
*/
|
|
23
|
+
import { execFile } from 'node:child_process';
|
|
24
|
+
import { delimiter, isAbsolute } from 'node:path';
|
|
25
|
+
import { SubprocessExecutableNotFoundError, scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess';
|
|
26
|
+
import { optionalService } from '../core/context.js';
|
|
27
|
+
/** Unique line a login shell prints before the `PATH` it resolved. */
|
|
28
|
+
const PATH_MARKER = '__DSH_MARKET_LOGIN_PATH__';
|
|
29
|
+
/** How long the login-shell probe may take before it is abandoned. */
|
|
30
|
+
const PROBE_TIMEOUT_MS = 3_000;
|
|
31
|
+
/**
|
|
32
|
+
* In-process memo of the login-shell probe. A spawn resolution can run on
|
|
33
|
+
* every reconcile pass, but the user's shell environment does not change
|
|
34
|
+
* within one Host process, so the shell is asked at most once. The promise
|
|
35
|
+
* (not its value) is cached so concurrent first callers share one probe.
|
|
36
|
+
*/
|
|
37
|
+
let loginShellProbe;
|
|
38
|
+
/**
|
|
39
|
+
* Probe the user's login shell for the `PATH` it exports, once per process.
|
|
40
|
+
*
|
|
41
|
+
* `-i` is required, not incidental: Homebrew's `shellenv` line lives in
|
|
42
|
+
* `.zshrc`, which a non-interactive `zsh -lc` never reads. The probe prints
|
|
43
|
+
* one marker line and then the value, so a shell that writes banners or
|
|
44
|
+
* prompts to stdout cannot be mistaken for the value. Every failure —
|
|
45
|
+
* non-darwin, no shell, timeout, malformed output — resolves to `undefined`
|
|
46
|
+
* rather than throwing, because a caller must be able to fall back silently
|
|
47
|
+
* to today's behavior.
|
|
48
|
+
*
|
|
49
|
+
* @returns the absolute directories the login shell resolves, or `undefined`
|
|
50
|
+
* when the probe could not produce a usable `PATH`.
|
|
51
|
+
*/
|
|
52
|
+
export function loginShellPathEntries() {
|
|
53
|
+
loginShellProbe ??= probeLoginShellPath();
|
|
54
|
+
return loginShellProbe;
|
|
55
|
+
}
|
|
56
|
+
async function probeLoginShellPath() {
|
|
57
|
+
if (process.platform !== 'darwin')
|
|
58
|
+
return undefined;
|
|
59
|
+
const stdout = await new Promise(resolve => {
|
|
60
|
+
execFile('/bin/zsh', ['-ilc', `print -r -- ${PATH_MARKER} && print -r -- "$PATH"`], { timeout: PROBE_TIMEOUT_MS }, (error, output) => {
|
|
61
|
+
resolve(error === null ? output : undefined);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
if (stdout === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const markerIndex = stdout.lastIndexOf(PATH_MARKER);
|
|
67
|
+
if (markerIndex === -1)
|
|
68
|
+
return undefined;
|
|
69
|
+
const afterMarker = stdout.slice(markerIndex + PATH_MARKER.length);
|
|
70
|
+
const line = afterMarker
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map(part => part.trim())
|
|
73
|
+
.find(part => part !== '');
|
|
74
|
+
if (line === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const entry of line.split(delimiter)) {
|
|
78
|
+
if (entry !== '' && isAbsolute(entry) && !entries.includes(entry))
|
|
79
|
+
entries.push(entry);
|
|
80
|
+
}
|
|
81
|
+
return entries.length > 0 ? entries : undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the executable lookup path out of the environment the way the host
|
|
85
|
+
* subprocess provider does: a non-empty `PATH` entry, or the platform
|
|
86
|
+
* default when the variable is absent (as `child_process.spawn` itself
|
|
87
|
+
* resolves it).
|
|
88
|
+
*/
|
|
89
|
+
export function environmentPath(env) {
|
|
90
|
+
const value = env?.PATH;
|
|
91
|
+
if (value !== undefined && value !== '')
|
|
92
|
+
return value;
|
|
93
|
+
return process.env.PATH ?? '/usr/bin:/bin';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Append every directory the login shell has and `basePath` lacks, keeping
|
|
97
|
+
* `basePath`'s order and precedence intact.
|
|
98
|
+
*
|
|
99
|
+
* @returns the merged `PATH` and the appended directories, or `undefined`
|
|
100
|
+
* when the probe failed or added nothing.
|
|
101
|
+
*/
|
|
102
|
+
export async function augmentChildPath(basePath) {
|
|
103
|
+
const entries = await loginShellPathEntries();
|
|
104
|
+
if (entries === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const current = basePath === '' ? [] : basePath.split(delimiter);
|
|
107
|
+
const present = new Set(current);
|
|
108
|
+
const added = entries.filter(entry => !present.has(entry));
|
|
109
|
+
if (added.length === 0)
|
|
110
|
+
return undefined;
|
|
111
|
+
return { path: [...current, ...added].join(delimiter), added };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read the host's executable lookup seam from a cordis context, tolerantly.
|
|
115
|
+
* The service is optional — a context without a subprocess runtime resolves
|
|
116
|
+
* nothing and the caller keeps today's behavior.
|
|
117
|
+
*/
|
|
118
|
+
export function subprocessResolver(ctx) {
|
|
119
|
+
const subprocess = optionalService(ctx, 'subprocess');
|
|
120
|
+
const resolve = subprocess?.resolveExecutable;
|
|
121
|
+
if (typeof resolve !== 'function')
|
|
122
|
+
return undefined;
|
|
123
|
+
return (command, env) => resolve.call(subprocess, command, env);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Per-context memo of command resolutions, keyed by `${command}\u0000${basePath}`.
|
|
127
|
+
* The lsp-mounts and MCP-bridge reconciles ask on every pass, and a resolution
|
|
128
|
+
* walks a subprocess per attempt, so the promise (not its value) is cached —
|
|
129
|
+
* concurrent first callers share one walk. Keyed first by `ctx` so tests and
|
|
130
|
+
* fresh contexts stay isolated.
|
|
131
|
+
*/
|
|
132
|
+
const resolutionMemo = new WeakMap();
|
|
133
|
+
/**
|
|
134
|
+
* Resolve one command a suite or the user declared, against the environment a
|
|
135
|
+
* spawn would inherit, extending `PATH` from the login shell only when that
|
|
136
|
+
* environment cannot find the command.
|
|
137
|
+
*
|
|
138
|
+
* The rule the two consumers share: an explicit `env.PATH` is a declaration
|
|
139
|
+
* and is never touched, so the caller gets `undefined` and keeps it verbatim.
|
|
140
|
+
* The plugin never rewrites the command itself — the bare name stays the
|
|
141
|
+
* command, and only the child's `PATH` grows.
|
|
142
|
+
*
|
|
143
|
+
* @param ctx - the cordis context carrying the host subprocess seam.
|
|
144
|
+
* @param command - the declared command, exactly as written.
|
|
145
|
+
* @param declaredEnv - the environment the suite/user declared for the child.
|
|
146
|
+
* @returns the extension to apply and its fact, or `undefined` when there is
|
|
147
|
+
* nothing to do (no seam, or an explicit `PATH`).
|
|
148
|
+
*/
|
|
149
|
+
export function resolveDeclaredCommand(ctx, command, declaredEnv) {
|
|
150
|
+
const resolveExecutable = subprocessResolver(ctx);
|
|
151
|
+
// A context without the seam must not pin a stale no-seam result: the memo
|
|
152
|
+
// only exists once the seam is real. A declared `env.PATH` is a declaration
|
|
153
|
+
// and stays verbatim.
|
|
154
|
+
if (resolveExecutable === undefined || declaredEnv.PATH !== undefined)
|
|
155
|
+
return Promise.resolve(undefined);
|
|
156
|
+
if (!((typeof ctx === 'object' || typeof ctx === 'function') && ctx !== null))
|
|
157
|
+
return Promise.resolve(undefined);
|
|
158
|
+
const environment = { ...scrubbedParentEnv(), ...declaredEnv };
|
|
159
|
+
const basePath = environmentPath(environment);
|
|
160
|
+
let byCommand = resolutionMemo.get(ctx);
|
|
161
|
+
if (byCommand === undefined) {
|
|
162
|
+
byCommand = new Map();
|
|
163
|
+
resolutionMemo.set(ctx, byCommand);
|
|
164
|
+
}
|
|
165
|
+
const memoKey = `${command}\u0000${basePath}`;
|
|
166
|
+
const memoized = byCommand.get(memoKey);
|
|
167
|
+
if (memoized !== undefined)
|
|
168
|
+
return memoized;
|
|
169
|
+
const resolution = resolveOnce(resolveExecutable, command, environment, basePath);
|
|
170
|
+
byCommand.set(memoKey, resolution);
|
|
171
|
+
return resolution;
|
|
172
|
+
}
|
|
173
|
+
/** One full resolution walk: resolve, and extend `PATH` from the login shell only on a lookup miss. */
|
|
174
|
+
async function resolveOnce(resolveExecutable, command, environment, basePath) {
|
|
175
|
+
try {
|
|
176
|
+
await resolveExecutable(command, { ...environment, PATH: basePath });
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// Only a genuine lookup miss earns the extra probe; any other failure
|
|
181
|
+
// (a relative-path rejection, a provider fault) keeps today's behavior.
|
|
182
|
+
if (!(error instanceof SubprocessExecutableNotFoundError))
|
|
183
|
+
return undefined;
|
|
184
|
+
const augmented = await augmentChildPath(basePath);
|
|
185
|
+
if (augmented === undefined) {
|
|
186
|
+
return { diagnostic: `could not resolve "${command}" on the Host PATH (${basePath}); the login shell could not supply a usable PATH to extend it` };
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await resolveExecutable(command, { ...environment, PATH: augmented.path });
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return { diagnostic: `could not resolve "${command}" even after extending PATH from the login shell (searched: ${augmented.path})` };
|
|
193
|
+
}
|
|
194
|
+
return { path: augmented.path, diagnostic: `PATH extended from the login shell: ${augmented.added.join(', ')}` };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Reading the model-facing MCP tools the host registry currently exposes. */
|
|
2
|
+
import { optionalService } from '../core/context.js';
|
|
2
3
|
/**
|
|
3
4
|
* The MCP tools the host currently publishes to the model.
|
|
4
5
|
*
|
|
@@ -35,5 +36,5 @@ export function inspectToolRegistry(tools) {
|
|
|
35
36
|
}
|
|
36
37
|
/** Read the tools service off the plugin context, when the host mounts one. */
|
|
37
38
|
export function toolsServiceOf(ctx) {
|
|
38
|
-
return ctx
|
|
39
|
+
return optionalService(ctx, 'tools');
|
|
39
40
|
}
|