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.
Files changed (158) hide show
  1. package/README.md +2 -2
  2. package/README.zh.md +2 -2
  3. package/client/client.js +1594 -1732
  4. package/client/client.js.map +1 -1
  5. package/lib/application/agent-roles.js +53 -0
  6. package/lib/application/catalog-context.js +2 -2
  7. package/lib/application/catalog.js +4 -8
  8. package/lib/{runtime → application}/deadline.js +1 -1
  9. package/lib/application/details.js +3 -3
  10. package/lib/application/json-file.js +43 -0
  11. package/lib/{runtime → application/lsp}/lsp-direct-config.js +9 -7
  12. package/lib/application/lsp/lsp-server-state.js +17 -0
  13. package/lib/{runtime → application/lsp}/lsp-status.js +22 -7
  14. package/lib/{runtime → application/lsp}/profile-seam.js +1 -1
  15. package/lib/application/lsp-service.js +5 -5
  16. package/lib/{runtime → application/mcp}/mcp-backend.js +6 -21
  17. package/lib/{runtime → application/mcp}/mcp-config.js +4 -4
  18. package/lib/{runtime → application/mcp}/mcp-direct-config.js +8 -49
  19. package/lib/{runtime → application/mcp}/mcp-overrides.js +3 -5
  20. package/lib/{runtime → application/mcp}/mcp-status.js +13 -7
  21. package/lib/application/mcp-service.js +11 -35
  22. package/lib/application/panel-resources.js +48 -11
  23. package/lib/{runtime → application/panels}/user-hooks.js +2 -2
  24. package/lib/{runtime → application/panels}/user-store.js +2 -2
  25. package/lib/application/ports.js +1 -0
  26. package/lib/application/project-agent-roles.js +1 -1
  27. package/lib/{runtime → application}/server-config.js +7 -17
  28. package/lib/application/snapshot-cache.js +1 -1
  29. package/lib/application/source-store.js +3 -4
  30. package/lib/{runtime → application/state}/state-store.js +5 -7
  31. package/lib/{runtime → application/state}/storage-migration.js +4 -4
  32. package/lib/catalog/source-catalog.js +0 -4
  33. package/lib/contracts/lsp.js +1 -0
  34. package/lib/contracts/market.js +0 -1
  35. package/lib/contracts/mcp.js +1 -0
  36. package/lib/index.js +64 -34
  37. package/lib/routes.js +2 -9
  38. package/lib/runtime/{agent-role-router.js → agents/agent-role-router.js} +7 -53
  39. package/lib/runtime/{subagent-catalog.js → agents/subagent-catalog.js} +24 -16
  40. package/lib/runtime/core/context.js +31 -0
  41. package/lib/runtime/{reconciler.js → core/reconciler.js} +5 -5
  42. package/lib/runtime/host/failure-detail.js +58 -0
  43. package/lib/runtime/{feedback-tool.js → host/feedback-tool.js} +1 -1
  44. package/lib/runtime/host/host-locale.js +83 -0
  45. package/lib/runtime/{settings-namespace.js → host/settings-namespace.js} +44 -66
  46. package/lib/runtime/host/shell-path.js +196 -0
  47. package/lib/runtime/{tool-registry-observer.js → host/tool-registry-observer.js} +2 -1
  48. package/lib/runtime/{lsp-mounts.js → lsp/lsp-mounts.js} +69 -20
  49. package/lib/runtime/{mcp-client → mcp/bridge}/bridge.js +52 -22
  50. package/lib/runtime/{mcp-client → mcp/bridge}/connection.js +2 -2
  51. package/lib/runtime/{mcp-client → mcp/bridge}/oauth.js +1 -1
  52. package/lib/runtime/{mcp-client → mcp/bridge}/tools.js +2 -2
  53. package/lib/runtime/{mcp-auth-record.js → mcp/mcp-auth-record.js} +1 -1
  54. package/lib/runtime/{mcp-mounts.js → mcp/mcp-mounts.js} +14 -7
  55. package/lib/runtime/{user-commands.js → panels/user-commands.js} +3 -3
  56. package/lib/runtime/{user-panels.js → panels/user-panels.js} +2 -2
  57. package/lib/runtime/{commands-mounts.js → surfaces/commands-mounts.js} +7 -7
  58. package/lib/runtime/{hooks-mounts.js → surfaces/hooks-mounts.js} +2 -2
  59. package/lib/runtime/{project-runtime.js → surfaces/project-runtime.js} +5 -5
  60. package/lib/runtime/{skills-provider.js → surfaces/skills-provider.js} +4 -4
  61. package/lib/types/application/agent-roles.d.ts +30 -0
  62. package/lib/types/application/catalog.d.ts +4 -7
  63. package/lib/types/application/details.d.ts +1 -1
  64. package/lib/types/application/json-file.d.ts +12 -0
  65. package/lib/types/{runtime → application/lsp}/lsp-direct-config.d.ts +1 -1
  66. package/lib/types/{runtime → application/lsp}/lsp-status.d.ts +3 -3
  67. package/lib/types/{runtime → application/mcp}/mcp-backend.d.ts +24 -22
  68. package/lib/types/{runtime/mcp-client/config.d.ts → application/mcp/mcp-bridge-config.d.ts} +11 -1
  69. package/lib/types/{runtime → application/mcp}/mcp-config.d.ts +6 -7
  70. package/lib/types/application/mcp/mcp-direct-config.d.ts +17 -0
  71. package/lib/types/{runtime → application/mcp}/mcp-overrides.d.ts +1 -1
  72. package/lib/types/{runtime → application/mcp}/mcp-status.d.ts +7 -7
  73. package/lib/types/application/mcp-service.d.ts +4 -11
  74. package/lib/types/application/panel-resources.d.ts +9 -2
  75. package/lib/types/{runtime → application/panels}/user-hooks.d.ts +1 -1
  76. package/lib/types/{runtime → application/panels}/user-store.d.ts +1 -1
  77. package/lib/types/application/ports.d.ts +17 -3
  78. package/lib/types/application/project-agent-roles.d.ts +1 -1
  79. package/lib/types/application/queries.d.ts +3 -6
  80. package/lib/types/{runtime → application}/regions.d.ts +1 -1
  81. package/lib/types/{runtime → application/state}/state-store.d.ts +2 -2
  82. package/lib/types/catalog/source-catalog.d.ts +0 -2
  83. package/lib/types/contracts/lsp-status.d.ts +6 -0
  84. package/lib/types/contracts/lsp.d.ts +16 -0
  85. package/lib/types/contracts/market.d.ts +1 -4
  86. package/lib/types/contracts/mcp-status.d.ts +21 -2
  87. package/lib/types/contracts/mcp.d.ts +20 -0
  88. package/lib/types/index.d.ts +87 -1
  89. package/lib/types/model/types.d.ts +0 -14
  90. package/lib/types/runtime/{agent-role-router.d.ts → agents/agent-role-router.d.ts} +3 -28
  91. package/lib/types/runtime/{subagent-catalog.d.ts → agents/subagent-catalog.d.ts} +6 -6
  92. package/lib/types/runtime/core/context.d.ts +14 -0
  93. package/lib/types/runtime/{reconcile-scheduler.d.ts → core/reconcile-scheduler.d.ts} +2 -2
  94. package/lib/types/runtime/{reconciler.d.ts → core/reconciler.d.ts} +8 -8
  95. package/lib/types/runtime/host/failure-detail.d.ts +6 -0
  96. package/lib/types/runtime/host/host-locale.d.ts +66 -0
  97. package/lib/types/runtime/{model-catalog.d.ts → host/model-catalog.d.ts} +1 -1
  98. package/lib/types/runtime/{settings-namespace.d.ts → host/settings-namespace.d.ts} +42 -32
  99. package/lib/types/runtime/host/shell-path.d.ts +70 -0
  100. package/lib/types/runtime/{lsp-mounts.d.ts → lsp/lsp-mounts.d.ts} +17 -11
  101. package/lib/types/runtime/{mcp-client → mcp/bridge}/bridge.d.ts +1 -1
  102. package/lib/types/runtime/{mcp-client → mcp/bridge}/connection.d.ts +3 -11
  103. package/lib/types/runtime/{mcp-client → mcp/bridge}/host-contract.d.ts +1 -1
  104. package/lib/types/runtime/{mcp-client → mcp/bridge}/oauth.d.ts +1 -1
  105. package/lib/types/runtime/{mcp-client → mcp/bridge}/transport.d.ts +1 -1
  106. package/lib/types/runtime/{mcp-auth-record.d.ts → mcp/mcp-auth-record.d.ts} +1 -1
  107. package/lib/types/runtime/{mcp-credentials.d.ts → mcp/mcp-credentials.d.ts} +1 -1
  108. package/lib/types/runtime/{mcp-mounts.d.ts → mcp/mcp-mounts.d.ts} +5 -11
  109. package/lib/types/runtime/{user-commands.d.ts → panels/user-commands.d.ts} +1 -1
  110. package/lib/types/runtime/{user-panels.d.ts → panels/user-panels.d.ts} +1 -1
  111. package/lib/types/runtime/{commands-mounts.d.ts → surfaces/commands-mounts.d.ts} +2 -2
  112. package/lib/types/runtime/{hooks-mounts.d.ts → surfaces/hooks-mounts.d.ts} +1 -1
  113. package/lib/types/runtime/{project-runtime.d.ts → surfaces/project-runtime.d.ts} +3 -3
  114. package/lib/types/runtime/{skills-provider.d.ts → surfaces/skills-provider.d.ts} +1 -1
  115. package/package.json +39 -33
  116. package/lib/runtime/host-locale.js +0 -89
  117. package/lib/runtime/legacy-root-migration.js +0 -12
  118. package/lib/runtime/lsp-server-state.js +0 -19
  119. package/lib/runtime/mcp-client/json-schema-subset.js +0 -181
  120. package/lib/types/runtime/host-locale.d.ts +0 -31
  121. package/lib/types/runtime/legacy-root-migration.d.ts +0 -2
  122. package/lib/types/runtime/mcp-client/json-schema-subset.d.ts +0 -58
  123. package/lib/types/runtime/mcp-direct-config.d.ts +0 -39
  124. /package/lib/{runtime/mcp-client/config.js → application/mcp/mcp-bridge-config.js} +0 -0
  125. /package/lib/{runtime → application/mcp}/mcp-redaction.js +0 -0
  126. /package/lib/{runtime → application}/regions.js +0 -0
  127. /package/lib/runtime/{agent-role-names.js → agents/agent-role-names.js} +0 -0
  128. /package/lib/runtime/{mount-lifecycle.js → core/mount-lifecycle.js} +0 -0
  129. /package/lib/runtime/{reconcile-scheduler.js → core/reconcile-scheduler.js} +0 -0
  130. /package/lib/runtime/{source-auto-update.js → core/source-auto-update.js} +0 -0
  131. /package/lib/runtime/{timer-seat.js → core/timer-seat.js} +0 -0
  132. /package/lib/runtime/{model-catalog.js → host/model-catalog.js} +0 -0
  133. /package/lib/runtime/{plugin-message-source.js → host/plugin-message-source.js} +0 -0
  134. /package/lib/runtime/{mcp-client → mcp/bridge}/host-contract.js +0 -0
  135. /package/lib/runtime/{mcp-client → mcp/bridge}/host-seams.js +0 -0
  136. /package/lib/runtime/{mcp-client → mcp/bridge}/plugin-identity.js +0 -0
  137. /package/lib/runtime/{mcp-client → mcp/bridge}/projection.js +0 -0
  138. /package/lib/runtime/{mcp-client → mcp/bridge}/transport.js +0 -0
  139. /package/lib/runtime/{mcp-credentials.js → mcp/mcp-credentials.js} +0 -0
  140. /package/lib/runtime/{dynamic-context.js → surfaces/dynamic-context.js} +0 -0
  141. /package/lib/types/{runtime → application}/deadline.d.ts +0 -0
  142. /package/lib/types/{runtime → application/lsp}/lsp-server-state.d.ts +0 -0
  143. /package/lib/types/{runtime → application/lsp}/profile-seam.d.ts +0 -0
  144. /package/lib/types/{runtime → application/mcp}/mcp-redaction.d.ts +0 -0
  145. /package/lib/types/{runtime → application}/server-config.d.ts +0 -0
  146. /package/lib/types/{runtime → application/state}/storage-migration.d.ts +0 -0
  147. /package/lib/types/runtime/{agent-role-names.d.ts → agents/agent-role-names.d.ts} +0 -0
  148. /package/lib/types/runtime/{mount-lifecycle.d.ts → core/mount-lifecycle.d.ts} +0 -0
  149. /package/lib/types/runtime/{source-auto-update.d.ts → core/source-auto-update.d.ts} +0 -0
  150. /package/lib/types/runtime/{timer-seat.d.ts → core/timer-seat.d.ts} +0 -0
  151. /package/lib/types/runtime/{feedback-tool.d.ts → host/feedback-tool.d.ts} +0 -0
  152. /package/lib/types/runtime/{plugin-message-source.d.ts → host/plugin-message-source.d.ts} +0 -0
  153. /package/lib/types/runtime/{tool-registry-observer.d.ts → host/tool-registry-observer.d.ts} +0 -0
  154. /package/lib/types/runtime/{mcp-client → mcp/bridge}/host-seams.d.ts +0 -0
  155. /package/lib/types/runtime/{mcp-client → mcp/bridge}/plugin-identity.d.ts +0 -0
  156. /package/lib/types/runtime/{mcp-client → mcp/bridge}/projection.d.ts +0 -0
  157. /package/lib/types/runtime/{mcp-client → mcp/bridge}/tools.d.ts +0 -0
  158. /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
- '- 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.',
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
- '- 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. Use it when the result should be collected later through the job tools; leave the parameter out when you want the child reachable with send_message and list_agents.',
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. When it finishes, summarize the result to the user yourself.",
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
- "- The route parameters override the route the role card declares; omit them to use the card's route, or the parent route when the card declares none.",
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
- * Model-facing catalog text: the role list followed by the same guidance on
79
- * first publication and on every update, so a replacement never arrives without
80
- * the rules that govern it. The guidance covers how to brief a child, how to
81
- * work with one while it runs, and when a role child is the wrong answer; the
82
- * per-channel arbitration between the host's delegation tools stays in the tool
83
- * descriptions the harness owns.
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 './commands-mounts.js';
2
- import { HooksMountRegistry } from './hooks-mounts.js';
3
- import { LspMountRegistry } from './lsp-mounts.js';
4
- import { McpMountRegistry } from './mcp-mounts.js';
5
- import { bindHostLocale } from './host-locale.js';
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 './mcp-client/oauth.js';
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 { MARKET_SETTINGS_NAMESPACE, resolveMarketSettings } from '../contracts/settings.js';
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
- /** Register the namespace; the host resolves it whenever the settings service mounts. */
25
+ /** Subscribe the runtime reactions to live settings updates. */
26
26
  mount() {
27
- this.ctx.inject(['settings'], settingsCtx => this.register(settingsCtx));
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 registration lands. */
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
- * Persist a backend choice.
35
- * @throws when the settings service is not mounted.
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
- if (this.scope === undefined)
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.get?.('tools');
39
+ return optionalService(ctx, 'tools');
39
40
  }