@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6

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 (207) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
  3. package/dist/cli.js +2989 -2978
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +23 -1
  7. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  8. package/dist/types/internal-urls/parse.d.ts +12 -0
  9. package/dist/types/internal-urls/router.d.ts +6 -0
  10. package/dist/types/internal-urls/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +1 -0
  12. package/dist/types/lsp/types.d.ts +2 -0
  13. package/dist/types/mcp/manager.d.ts +5 -0
  14. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  16. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  17. package/dist/types/sdk.d.ts +3 -1
  18. package/dist/types/session/agent-session-types.d.ts +3 -1
  19. package/dist/types/session/agent-session.d.ts +2 -0
  20. package/dist/types/session/model-controls.d.ts +3 -0
  21. package/dist/types/session/session-advisors.d.ts +7 -1
  22. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  23. package/dist/types/session/turn-recovery.d.ts +4 -2
  24. package/dist/types/task/index.d.ts +1 -1
  25. package/dist/types/task/types.d.ts +3 -11
  26. package/dist/types/thinking.d.ts +21 -2
  27. package/dist/types/utils/title-generator.d.ts +17 -16
  28. package/package.json +12 -12
  29. package/src/capability/index.ts +43 -12
  30. package/src/capability/mcp.ts +21 -0
  31. package/src/capability/types.ts +20 -1
  32. package/src/cli/read-cli.ts +44 -2
  33. package/src/config/settings-schema.ts +27 -1
  34. package/src/eval/py/runner.py +16 -2
  35. package/src/extensibility/extensions/runner.ts +91 -5
  36. package/src/extensibility/extensions/types.ts +0 -1
  37. package/src/extensibility/hooks/types.ts +0 -1
  38. package/src/internal-urls/mcp-protocol.ts +17 -3
  39. package/src/internal-urls/parse.ts +31 -0
  40. package/src/internal-urls/router.ts +24 -4
  41. package/src/internal-urls/types.ts +6 -0
  42. package/src/live/transport.ts +2 -2
  43. package/src/lsp/client.ts +2 -2
  44. package/src/lsp/config.ts +4 -0
  45. package/src/lsp/types.ts +2 -0
  46. package/src/mcp/config.ts +26 -14
  47. package/src/mcp/manager.ts +26 -9
  48. package/src/mcp/tool-bridge.ts +52 -1
  49. package/src/memories/index.ts +25 -6
  50. package/src/modes/components/status-line/segments.ts +3 -1
  51. package/src/modes/controllers/command-controller.ts +10 -10
  52. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  53. package/src/modes/rpc/rpc-mode.ts +60 -47
  54. package/src/prompts/steering/user-interjection.md +2 -5
  55. package/src/prompts/tools/task.md +4 -2
  56. package/src/sdk.ts +17 -8
  57. package/src/session/agent-session-types.ts +3 -0
  58. package/src/session/agent-session.ts +21 -4
  59. package/src/session/model-controls.ts +43 -7
  60. package/src/session/session-advisors.ts +30 -14
  61. package/src/session/session-tools.ts +4 -2
  62. package/src/session/tool-choice-queue.ts +19 -4
  63. package/src/session/turn-recovery.ts +21 -2
  64. package/src/task/executor.ts +11 -3
  65. package/src/task/index.ts +43 -32
  66. package/src/task/types.ts +12 -17
  67. package/src/thinking.ts +68 -5
  68. package/src/tools/read.ts +2 -2
  69. package/src/utils/title-generator.ts +88 -34
  70. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  71. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  72. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  73. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  74. package/dist/types/cli/update-cli.test.d.ts +0 -1
  75. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  76. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  77. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  78. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  79. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  80. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  81. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  82. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  83. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  84. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  85. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  86. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  87. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  88. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  89. package/dist/types/hindsight/client.test.d.ts +0 -1
  90. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  91. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  92. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  93. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  94. package/dist/types/launch/protocol.test.d.ts +0 -1
  95. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  96. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  97. package/dist/types/live/protocol.test.d.ts +0 -1
  98. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  99. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  100. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  101. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  102. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  103. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  104. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  105. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  106. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  107. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  108. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  109. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  110. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  111. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  112. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  113. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  114. package/dist/types/modes/print-mode.test.d.ts +0 -1
  115. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  116. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  117. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  118. package/dist/types/modes/warp-events.test.d.ts +0 -1
  119. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  120. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  121. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  122. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  123. package/dist/types/session/blob-store.test.d.ts +0 -1
  124. package/dist/types/session/messages.test.d.ts +0 -1
  125. package/dist/types/session/session-context.test.d.ts +0 -1
  126. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  127. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  128. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  129. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  130. package/dist/types/system-prompt.test.d.ts +0 -1
  131. package/dist/types/task/render.test.d.ts +0 -1
  132. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  133. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  134. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  135. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  136. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  137. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  138. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  139. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  140. package/src/advisor/__tests__/config.test.ts +0 -349
  141. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  142. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  143. package/src/cli/update-cli.test.ts +0 -28
  144. package/src/config/__tests__/model-registry.test.ts +0 -182
  145. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  146. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  147. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  148. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  149. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  150. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  151. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  152. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  153. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  154. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  155. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  156. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  157. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  158. package/src/hindsight/client.test.ts +0 -75
  159. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  160. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  161. package/src/launch/broker-list-order.test.ts +0 -89
  162. package/src/launch/broker-output-snapshot.test.ts +0 -126
  163. package/src/launch/protocol.test.ts +0 -59
  164. package/src/launch/spawn-options.test.ts +0 -31
  165. package/src/launch/terminal-output.test.ts +0 -107
  166. package/src/live/protocol.test.ts +0 -140
  167. package/src/mcp/config-writer.test.ts +0 -43
  168. package/src/mcp/smithery-auth.test.ts +0 -29
  169. package/src/mcp/smithery-registry.test.ts +0 -51
  170. package/src/mcp/transports/stdio.test.ts +0 -427
  171. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  172. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  173. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  174. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  175. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  176. package/src/modes/components/custom-editor.test.ts +0 -510
  177. package/src/modes/components/login-dialog.test.ts +0 -56
  178. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  179. package/src/modes/components/status-line/component.test.ts +0 -84
  180. package/src/modes/components/tool-execution.test.ts +0 -162
  181. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  182. package/src/modes/noninteractive-dispose.test.ts +0 -73
  183. package/src/modes/print-mode.test.ts +0 -71
  184. package/src/modes/session-teardown.test.ts +0 -219
  185. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  186. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  187. package/src/modes/warp-events.test.ts +0 -794
  188. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  189. package/src/plan-mode/model-transition.test.ts +0 -60
  190. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  191. package/src/session/agent-session-error-log.test.ts +0 -59
  192. package/src/session/blob-store.test.ts +0 -56
  193. package/src/session/messages.test.ts +0 -282
  194. package/src/session/session-context.test.ts +0 -384
  195. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  196. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  197. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  198. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  199. package/src/system-prompt.test.ts +0 -236
  200. package/src/task/render.test.ts +0 -290
  201. package/src/task/spawn-policy.test.ts +0 -62
  202. package/src/tools/__tests__/eval-description.test.ts +0 -18
  203. package/src/tools/__tests__/glob.test.ts +0 -37
  204. package/src/tools/__tests__/json-tree.test.ts +0 -35
  205. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  206. package/src/tools/hub/launch-compat.test.ts +0 -40
  207. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -102,9 +102,10 @@ async function loadImpl<T>(
102
102
  capability: Capability<T>,
103
103
  providers: Provider<T>[],
104
104
  ctx: LoadContext,
105
- options: LoadOptions,
105
+ options: LoadOptions<T>,
106
106
  ): Promise<CapabilityResult<T>> {
107
107
  const allItems: Array<T & { _source: SourceMeta; _shadowed?: boolean }> = [];
108
+ const suppressedItems = new Set<T & { _source: SourceMeta; _shadowed?: boolean }>();
108
109
  const allWarnings: string[] = [];
109
110
  const contributingProviders: string[] = [];
110
111
  const disabledExtensionIds = options.includeDisabled
@@ -154,6 +155,21 @@ async function loadImpl<T>(
154
155
  continue;
155
156
  }
156
157
 
158
+ if (options.filter && !options.filter(itemWithSource)) {
159
+ continue;
160
+ }
161
+
162
+ if (options.suppress?.(itemWithSource)) {
163
+ // Suppressed items still claim their dedupe key below, so a
164
+ // suppressed higher-priority item shadows same-key lower-priority
165
+ // ones, but they never survive or equivalence-shadow survivors.
166
+ itemWithSource._source.providerName = provider.displayName;
167
+ const suppressed = itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean };
168
+ suppressedItems.add(suppressed);
169
+ allItems.push(suppressed);
170
+ continue;
171
+ }
172
+
157
173
  itemWithSource._source.providerName = provider.displayName;
158
174
  allItems.push(itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean });
159
175
  contributedItemCount += 1;
@@ -164,21 +180,33 @@ async function loadImpl<T>(
164
180
  }
165
181
  }
166
182
 
167
- // Deduplicate by key (first wins = highest priority)
168
- const seen = new Map<string, number>();
183
+ // Deduplicate by key or semantic equivalence (first wins = highest priority)
184
+ const seen = new Set<string>();
169
185
  const deduped: Array<T & { _source: SourceMeta }> = [];
186
+ const equivalent = capability.equivalent;
170
187
 
171
- for (let i = 0; i < allItems.length; i++) {
172
- const item = allItems[i];
188
+ for (const item of allItems) {
173
189
  const key = capability.key(item);
174
190
 
191
+ if (suppressedItems.has(item)) {
192
+ // Claim key ownership (same-name precedence, including disabled
193
+ // state) without surviving or equivalence-shadowing survivors.
194
+ if (key !== undefined) seen.add(key);
195
+ continue;
196
+ }
197
+
175
198
  if (key === undefined) {
176
199
  deduped.push(item);
177
- } else if (!seen.has(key)) {
178
- seen.set(key, i);
179
- deduped.push(item);
180
- } else {
200
+ continue;
201
+ }
202
+
203
+ const keySeen = seen.has(key);
204
+ seen.add(key);
205
+ const aliasSeen = !keySeen && equivalent !== undefined && deduped.some(existing => equivalent(existing, item));
206
+ if (keySeen || aliasSeen) {
181
207
  item._shadowed = true;
208
+ } else {
209
+ deduped.push(item);
182
210
  }
183
211
  }
184
212
 
@@ -198,7 +226,7 @@ async function loadImpl<T>(
198
226
 
199
227
  return {
200
228
  items: deduped,
201
- all: allItems,
229
+ all: suppressedItems.size > 0 ? allItems.filter(item => !suppressedItems.has(item)) : allItems,
202
230
  warnings: allWarnings,
203
231
  providers: contributingProviders,
204
232
  };
@@ -207,7 +235,7 @@ async function loadImpl<T>(
207
235
  /**
208
236
  * Filter providers based on options and disabled state.
209
237
  */
210
- function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Provider<T>[] {
238
+ function filterProviders<T>(capability: Capability<T>, options: LoadOptions<T>): Provider<T>[] {
211
239
  let providers = (capability.providers as Provider<T>[]).filter(p => !disabledProviders.has(p.id));
212
240
 
213
241
  if (options.providers) {
@@ -225,7 +253,10 @@ function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Pr
225
253
  /**
226
254
  * Load a capability by ID.
227
255
  */
228
- export async function loadCapability<T>(capabilityId: string, options: LoadOptions = {}): Promise<CapabilityResult<T>> {
256
+ export async function loadCapability<T>(
257
+ capabilityId: string,
258
+ options: LoadOptions<T> = {},
259
+ ): Promise<CapabilityResult<T>> {
229
260
  const capability = capabilities.get(capabilityId) as Capability<T> | undefined;
230
261
  if (!capability) {
231
262
  throw new Error(`Unknown capability: "${capabilityId}"`);
@@ -53,11 +53,32 @@ export interface MCPServer {
53
53
  _source: SourceMeta;
54
54
  }
55
55
 
56
+ /** Compare the transport inputs that determine which MCP endpoint gets connected. */
57
+ function isSameMCPConnection(left: MCPServer, right: MCPServer): boolean {
58
+ if (!Bun.deepEquals(left.auth, right.auth) || !Bun.deepEquals(left.oauth, right.oauth)) return false;
59
+
60
+ const leftTransport = left.transport ?? (left.command ? "stdio" : left.url ? "http" : "stdio");
61
+ const rightTransport = right.transport ?? (right.command ? "stdio" : right.url ? "http" : "stdio");
62
+ if (leftTransport !== rightTransport) return false;
63
+
64
+ if (leftTransport === "stdio") {
65
+ return (
66
+ left.command === right.command &&
67
+ Bun.deepEquals(left.args, right.args) &&
68
+ Bun.deepEquals(left.env, right.env) &&
69
+ left.cwd === right.cwd
70
+ );
71
+ }
72
+
73
+ return left.url === right.url && Bun.deepEquals(left.headers, right.headers);
74
+ }
75
+
56
76
  export const mcpCapability = defineCapability<MCPServer>({
57
77
  id: "mcps",
58
78
  displayName: "MCP Servers",
59
79
  description: "Model Context Protocol server configurations for external tool integrations",
60
80
  key: server => server.name,
81
+ equivalent: isSameMCPConnection,
61
82
  toExtensionId: server => `mcp:${server.name}`,
62
83
  validate: server => {
63
84
  if (!server.name) return "Missing server name";
@@ -59,7 +59,7 @@ export interface Provider<T> {
59
59
  /**
60
60
  * Options for loading a capability.
61
61
  */
62
- export interface LoadOptions {
62
+ export interface LoadOptions<T = unknown> {
63
63
  /** Only use these providers (by ID). Default: all registered */
64
64
  providers?: string[];
65
65
  /** Exclude these providers (by ID). Default: none */
@@ -72,6 +72,22 @@ export interface LoadOptions {
72
72
  includeDisabled?: boolean;
73
73
  /** Explicit disabled extension IDs to apply instead of settings. */
74
74
  disabledExtensions?: string[];
75
+ /**
76
+ * Drop items before deduplication as if they never existed (e.g. scope
77
+ * exclusions). A dropped item neither survives nor claims its dedupe key,
78
+ * so it cannot shadow anything. Receives the item with its attached
79
+ * `_source`.
80
+ */
81
+ filter?(item: T & { _source: SourceMeta }): boolean;
82
+ /**
83
+ * Exclude items from the results while letting them claim their dedupe key.
84
+ * A suppressed higher-priority item still shadows same-key lower-priority
85
+ * items (a disabled project server keeps the same-named user server off),
86
+ * but never equivalence-shadows a differently-keyed survivor — so a
87
+ * suppressed alias cannot starve an enabled equivalent connection.
88
+ * Return `true` to suppress. Receives the item with its attached `_source`.
89
+ */
90
+ suppress?(item: T & { _source: SourceMeta }): boolean;
75
91
  }
76
92
 
77
93
  /**
@@ -122,6 +138,9 @@ export interface Capability<T> {
122
138
  */
123
139
  key(item: T): string | undefined;
124
140
 
141
+ /** Treat items with different keys as aliases; the first equivalent item wins. */
142
+ equivalent?(left: T, right: T): boolean;
143
+
125
144
  /**
126
145
  * Optional validation. Return error message if invalid, undefined if valid.
127
146
  */
@@ -8,6 +8,12 @@
8
8
  import { getProjectDir } from "@oh-my-pi/pi-utils";
9
9
  import chalk from "chalk";
10
10
  import { Settings } from "../config/settings";
11
+ import { extractUriScheme } from "../internal-urls/parse";
12
+ import { InternalUrlRouter } from "../internal-urls/router";
13
+ import { discoverAndLoadMCPTools } from "../mcp/loader";
14
+ import { MCPManager } from "../mcp/manager";
15
+ import { discoverAuthStorage } from "../session/auth-broker-config";
16
+ import type { AuthStorage } from "../session/auth-storage";
11
17
  import type { ToolSession } from "../tools";
12
18
  import { wrapToolWithMetaNotice } from "../tools/output-meta";
13
19
  import { ReadTool } from "../tools/read";
@@ -17,6 +23,17 @@ export interface ReadCommandArgs {
17
23
  path: string;
18
24
  }
19
25
 
26
+ function shouldDiscoverMcp(path: string): boolean {
27
+ // MCP resource URIs may be hierarchical (`test://notes`) or opaque
28
+ // (`urn:example:document`); `extractUriScheme` recognizes both while
29
+ // rejecting Windows drive paths and selector-shaped filesystem inputs.
30
+ const scheme = extractUriScheme(path);
31
+ if (!scheme) return false;
32
+ if (scheme === "mcp") return true;
33
+ if (["conflict", "file", "http", "https"].includes(scheme)) return false;
34
+ return InternalUrlRouter.instance().getHandler(scheme) === undefined;
35
+ }
36
+
20
37
  export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
21
38
  if (!cmd.path) {
22
39
  process.stderr.write(chalk.red("error: path is required\n"));
@@ -34,9 +51,26 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
34
51
  getSessionSpawns: () => "*",
35
52
  };
36
53
 
37
- const tool = wrapToolWithMetaNotice(new ReadTool(session));
54
+ let authStorage: AuthStorage | undefined;
55
+ let mcpManager: MCPManager | undefined;
56
+ let failed = false;
38
57
 
39
58
  try {
59
+ if (shouldDiscoverMcp(cmd.path)) {
60
+ authStorage = await discoverAuthStorage();
61
+ const result = await discoverAndLoadMCPTools(cwd, {
62
+ enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
63
+ filterExa: true,
64
+ filterBrowser: settings.get("browser.enabled") ?? false,
65
+ cacheStorage: settings.getStorage(),
66
+ authStorage,
67
+ });
68
+ mcpManager = result.manager;
69
+ session.mcpManager = mcpManager;
70
+ MCPManager.setInstance(mcpManager);
71
+ }
72
+
73
+ const tool = wrapToolWithMetaNotice(new ReadTool(session));
40
74
  const result = await tool.execute("omp-read", { path: cmd.path });
41
75
 
42
76
  for (const block of result.content) {
@@ -52,6 +86,14 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
52
86
  }
53
87
  } catch (err) {
54
88
  process.stderr.write(`${chalk.red(renderError(err))}\n`);
55
- process.exit(1);
89
+ failed = true;
90
+ } finally {
91
+ if (mcpManager) {
92
+ await mcpManager.disconnectAll();
93
+ if (MCPManager.instance() === mcpManager) MCPManager.setInstance(undefined);
94
+ }
95
+ authStorage?.close();
56
96
  }
97
+
98
+ if (failed) process.exit(1);
57
99
  }
@@ -935,7 +935,7 @@ export const SETTINGS_SCHEMA = {
935
935
  group: "Display",
936
936
  label: "Terminal Title Run State",
937
937
  description:
938
- "Show the agent run state in the terminal title's separator — an animated spinner while working, '>' when it's your turn, '!' when the agent is waiting on you",
938
+ "Show the agent run state in the terminal title's separator — an animated spinner while working (a static ':' on Windows), '>' when it's your turn, '!' when the agent is waiting on you",
939
939
  },
940
940
  },
941
941
 
@@ -4476,6 +4476,18 @@ export const SETTINGS_SCHEMA = {
4476
4476
  },
4477
4477
  },
4478
4478
 
4479
+ "task.enableEffort": {
4480
+ type: "boolean",
4481
+ default: false,
4482
+ ui: {
4483
+ tab: "tasks",
4484
+ group: "Subagents",
4485
+ label: "Per-Task Effort",
4486
+ description:
4487
+ "Expose the optional effort parameter on task spawns, allowing callers to override each subagent's thinking level",
4488
+ },
4489
+ },
4490
+
4479
4491
  "task.maxConcurrency": {
4480
4492
  type: "number",
4481
4493
  default: 32,
@@ -4588,6 +4600,20 @@ export const SETTINGS_SCHEMA = {
4588
4600
  },
4589
4601
  },
4590
4602
 
4603
+ "task.maxEffort": {
4604
+ type: "enum",
4605
+ values: THINKING_EFFORTS,
4606
+ default: "max",
4607
+ ui: {
4608
+ tab: "tasks",
4609
+ group: "Subagents",
4610
+ label: "Maximum Per-Spawn Effort",
4611
+ description:
4612
+ "Maximum reasoning effort allowed for the task tool's per-spawn effort hint. Lower values prevent callers from escalating subagents above this ceiling; the default preserves the model's full range.",
4613
+ options: THINKING_EFFORTS.map(getThinkingLevelMetadata),
4614
+ },
4615
+ },
4616
+
4591
4617
  "task.disabledAgents": {
4592
4618
  type: "array",
4593
4619
  default: [] as string[],
@@ -1057,7 +1057,7 @@ async def _run_compiled_async(code, ns: dict, *, want_value: bool) -> Any:
1057
1057
 
1058
1058
 
1059
1059
  def _compile_source(source: str) -> tuple[Any, Any | None, bool]:
1060
- module = ast.parse(source, mode="exec")
1060
+ module = ast.parse(source, "<cell>", "exec")
1061
1061
  if not module.body:
1062
1062
  return None, None, False
1063
1063
 
@@ -1279,7 +1279,21 @@ async def _handle_request_async(req: dict) -> None:
1279
1279
 
1280
1280
 
1281
1281
  def _emit_error(rid: str, exc: BaseException) -> None:
1282
- tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
1282
+ if isinstance(exc, SyntaxError) and exc.filename == "<cell>":
1283
+ # Syntax error in the cell source itself: every stack frame is runner
1284
+ # machinery, so emit only the caret display, like a REPL.
1285
+ tb_lines = traceback.format_exception_only(type(exc), exc)
1286
+ else:
1287
+ # Drop the leading runner-internal frames (_handle_request_async ->
1288
+ # _exec_source_async -> _run_compiled_*) so tracebacks start at user
1289
+ # code. If the exception never reached user code it is a runner bug;
1290
+ # keep the full traceback because those frames are the diagnosis.
1291
+ tb = exc.__traceback__
1292
+ while tb is not None and tb.tb_frame.f_code.co_filename == __file__:
1293
+ tb = tb.tb_next
1294
+ tb_lines = traceback.format_exception(
1295
+ type(exc), exc, tb if tb is not None else exc.__traceback__
1296
+ )
1283
1297
  _emit(
1284
1298
  {
1285
1299
  "type": "error",
@@ -37,6 +37,7 @@ import type {
37
37
  ExtensionRuntime,
38
38
  ExtensionShortcut,
39
39
  ExtensionUIContext,
40
+ ExtensionUIDialogOptions,
40
41
  InputEvent,
41
42
  InputEventResult,
42
43
  MessageRenderer,
@@ -105,6 +106,66 @@ function handlerTimeoutForEvent(eventType: string): number {
105
106
  const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
106
107
  const EXTENSION_HANDLER_ABORTED = Symbol("extensionHandlerAborted");
107
108
 
109
+ function attachHandlerSignal(
110
+ dialogOptions: ExtensionUIDialogOptions | undefined,
111
+ handlerSignal: AbortSignal,
112
+ ): ExtensionUIDialogOptions {
113
+ if (!dialogOptions) return { signal: handlerSignal };
114
+ if (!dialogOptions.signal) return { ...dialogOptions, signal: handlerSignal };
115
+ if (dialogOptions.signal === handlerSignal) return dialogOptions;
116
+ return { ...dialogOptions, signal: AbortSignal.any([dialogOptions.signal, handlerSignal]) };
117
+ }
118
+
119
+ function createHandlerUIContext(ui: ExtensionUIContext, handlerSignal: AbortSignal): ExtensionUIContext {
120
+ const askDialog = ui.askDialog;
121
+ const dialogMethods = {
122
+ select: (title, options, dialogOptions) =>
123
+ ui.select(title, options, attachHandlerSignal(dialogOptions, handlerSignal)),
124
+ confirm: (title, message, dialogOptions) =>
125
+ ui.confirm(title, message, attachHandlerSignal(dialogOptions, handlerSignal)),
126
+ input: (title, placeholder, dialogOptions) =>
127
+ ui.input(title, placeholder, attachHandlerSignal(dialogOptions, handlerSignal)),
128
+ askDialog: askDialog
129
+ ? (questions, dialogOptions) =>
130
+ askDialog.call(ui, questions, attachHandlerSignal(dialogOptions, handlerSignal))
131
+ : undefined,
132
+ editor: (title, prefill, dialogOptions, editorOptions) =>
133
+ ui.editor(title, prefill, attachHandlerSignal(dialogOptions, handlerSignal), editorOptions),
134
+ } satisfies Pick<ExtensionUIContext, "select" | "confirm" | "input" | "askDialog" | "editor">;
135
+ const delegatedMethods = new Map<PropertyKey, unknown>();
136
+
137
+ return new Proxy(ui, {
138
+ get(target, property) {
139
+ if (Object.hasOwn(dialogMethods, property)) {
140
+ return Reflect.get(dialogMethods, property, dialogMethods);
141
+ }
142
+ const cached = delegatedMethods.get(property);
143
+ if (cached) return cached;
144
+ const value: unknown = Reflect.get(target, property, target);
145
+ if (typeof value !== "function") return value;
146
+ const delegated: unknown = value.bind(target);
147
+ delegatedMethods.set(property, delegated);
148
+ return delegated;
149
+ },
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Scope `ctx` to a single handler run without spreading it: `{ ...ctx }` would
155
+ * snapshot live accessors (notably the `model` getter), so a handler calling
156
+ * `pi.setModel()` and then reading `ctx.model` would see a stale model.
157
+ * Prototype delegation keeps every getter live while overriding `ui`.
158
+ */
159
+ function createHandlerContext(ctx: ExtensionContext, handlerSignal: AbortSignal): ExtensionContext {
160
+ const scoped: ExtensionContext = Object.create(ctx);
161
+ Object.defineProperty(scoped, "ui", {
162
+ value: createHandlerUIContext(ctx.ui, handlerSignal),
163
+ enumerable: true,
164
+ configurable: true,
165
+ });
166
+ return scoped;
167
+ }
168
+
108
169
  /**
109
170
  * Race `work` against a `timeoutMs` budget and optional cancellation signal,
110
171
  * clearing the timer and abort listener as soon as one branch settles.
@@ -118,19 +179,37 @@ const EXTENSION_HANDLER_ABORTED = Symbol("extensionHandlerAborted");
118
179
  * can `clearTimeout` on the winning branch.
119
180
  */
120
181
  async function raceHandlerWithTimeout<T>(
121
- work: Promise<T>,
182
+ work: (handlerSignal: AbortSignal) => Promise<T> | T,
122
183
  timeoutMs: number,
123
184
  signal?: AbortSignal,
124
185
  ): Promise<T | typeof EXTENSION_HANDLER_TIMEOUT | typeof EXTENSION_HANDLER_ABORTED> {
125
186
  if (signal?.aborted) return EXTENSION_HANDLER_ABORTED;
187
+
188
+ const timeoutController = new AbortController();
189
+ const handlerSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
126
190
  const { promise: interruptPromise, resolve: resolveInterrupt } = Promise.withResolvers<
127
191
  typeof EXTENSION_HANDLER_TIMEOUT | typeof EXTENSION_HANDLER_ABORTED
128
192
  >();
129
- const timer = setTimeout(() => resolveInterrupt(EXTENSION_HANDLER_TIMEOUT), timeoutMs);
130
193
  const onAbort = () => resolveInterrupt(EXTENSION_HANDLER_ABORTED);
131
194
  signal?.addEventListener("abort", onAbort, { once: true });
195
+ const timer = setTimeout(() => {
196
+ timeoutController.abort(new DOMException(`Handler timed out after ${timeoutMs}ms`, "TimeoutError"));
197
+ resolveInterrupt(EXTENSION_HANDLER_TIMEOUT);
198
+ }, timeoutMs);
132
199
  try {
133
- return await Promise.race([work, interruptPromise]);
200
+ if (signal?.aborted) return EXTENSION_HANDLER_ABORTED;
201
+ const workPromise = Promise.resolve(work(handlerSignal));
202
+ const result = await Promise.race([workPromise, interruptPromise]);
203
+ if (result === EXTENSION_HANDLER_TIMEOUT) {
204
+ await Promise.race([
205
+ workPromise.then(
206
+ () => undefined,
207
+ () => undefined,
208
+ ),
209
+ Bun.sleep(0),
210
+ ]);
211
+ }
212
+ return result;
134
213
  } finally {
135
214
  clearTimeout(timer);
136
215
  signal?.removeEventListener("abort", onAbort);
@@ -637,7 +716,11 @@ export class ExtensionRunner {
637
716
  : undefined;
638
717
  if (signal?.aborted) return undefined;
639
718
  try {
640
- const handlerResult = await raceHandlerWithTimeout(Promise.resolve(handler(event, ctx)), timeoutMs, signal);
719
+ const handlerResult = await raceHandlerWithTimeout(
720
+ handlerSignal => handler(event, createHandlerContext(ctx, handlerSignal)),
721
+ timeoutMs,
722
+ signal,
723
+ );
641
724
  if (handlerResult === EXTENSION_HANDLER_ABORTED) return undefined;
642
725
  if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
643
726
  const error = `handler timed out after ${timeoutMs}ms`;
@@ -799,7 +882,10 @@ export class ExtensionRunner {
799
882
 
800
883
  for (const handler of handlers) {
801
884
  try {
802
- const handlerResult = await raceHandlerWithTimeout(Promise.resolve(handler(event, ctx)), timeoutMs);
885
+ const handlerResult = await raceHandlerWithTimeout(
886
+ handlerSignal => handler(event, createHandlerContext(ctx, handlerSignal)),
887
+ timeoutMs,
888
+ );
803
889
 
804
890
  if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
805
891
  const error = `handler timed out after ${timeoutMs}ms`;
@@ -1044,7 +1044,6 @@ export interface RegisteredCommand {
1044
1044
  // ============================================================================
1045
1045
 
1046
1046
  /** Handler function type for events */
1047
- // biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements
1048
1047
  export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
1049
1048
 
1050
1049
  /** Service tiers accepted by each provider family. */
@@ -444,7 +444,6 @@ export type {
444
444
  * Handler function type for each event.
445
445
  * Handlers can return R, undefined, or void (bare return statements).
446
446
  */
447
- // biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements in handlers
448
447
  export type HookHandler<E, R = undefined> = (event: E, ctx: HookContext) => Promise<R | void> | R | void;
449
448
 
450
449
  export interface HookMessageRenderOptions {
@@ -21,6 +21,15 @@ function getUriTemplateMatchScore(
21
21
  }
22
22
 
23
23
  function extractResourceUri(url: InternalUrl): string {
24
+ const scheme = url.protocol.replace(/:$/, "").toLowerCase();
25
+ if (scheme !== "mcp") {
26
+ // Server-advertised native URI (hierarchical or opaque). Preserve the
27
+ // input byte-for-byte: `resolveTargetServer` matches by exact string
28
+ // equality, so e.g. `catalog://root/` must keep its trailing slash.
29
+ return url.rawHref ?? url.href;
30
+ }
31
+ // Legacy `mcp://<resource-uri>` wrapper: reconstruct the wrapped URI and
32
+ // elide a bare trailing `/` that URL parsing adds to host-only forms.
24
33
  const host = url.rawHost || url.hostname;
25
34
  const rawPathname = url.rawPathname ?? url.pathname;
26
35
  const hasPath = rawPathname && rawPathname !== "/";
@@ -95,10 +104,11 @@ function formatAvailableResources(mcpManager: MCPManager): string {
95
104
  }
96
105
 
97
106
  /**
98
- * Protocol handler for mcp:// URLs.
107
+ * Protocol handler for MCP resources.
99
108
  *
100
- * URL form:
109
+ * URL forms:
101
110
  * - mcp://<resource-uri> (e.g. mcp://test://notes, mcp://ibkr://portfolio/positions)
111
+ * - A resource's native URI when its scheme has no OMP handler (e.g. ags://capabilities/current-host)
102
112
  */
103
113
  export class McpProtocolHandler implements ProtocolHandler {
104
114
  readonly scheme = "mcp";
@@ -111,7 +121,11 @@ export class McpProtocolHandler implements ProtocolHandler {
111
121
  }
112
122
 
113
123
  const uri = extractResourceUri(url);
114
- const targetServer = resolveTargetServer(mcpManager, uri);
124
+ let targetServer = resolveTargetServer(mcpManager, uri);
125
+ if (!targetServer) {
126
+ await Promise.allSettled(mcpManager.getConnectedServers().map(name => mcpManager.ensureServerResources(name)));
127
+ targetServer = resolveTargetServer(mcpManager, uri);
128
+ }
115
129
  if (!targetServer) {
116
130
  throw new Error(
117
131
  `No MCP server has resource "${uri}".\n\nAvailable resources:\n${formatAvailableResources(mcpManager)}`,
@@ -13,6 +13,36 @@ import type { InternalUrl } from "./types";
13
13
 
14
14
  const SCHEME_HOST_RE = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)/i;
15
15
  const PATHNAME_RE = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]*(\/[^?#]*)?/i;
16
+ // Opaque URI form (`urn:example:document`, `custom:item`) — an RFC 3986 scheme
17
+ // followed by `:` without `//`. Guarded separately in `extractUriScheme`.
18
+ const OPAQUE_URI_RE = /^([a-z][a-z0-9+.-]*):(.+)$/is;
19
+ // A read-tool selector chain (`12`, `1-20,30+5`, `raw`, `raw:2-4`, `conflicts`)
20
+ // so `Makefile:12` or `notes:raw` is not mistaken for an opaque URI.
21
+ const SELECTOR_CHUNK_SRC = String.raw`(?:raw|conflicts|-?\d+(?:[-+]\d+)?(?:,\d+(?:[-+]\d+)?)*)`;
22
+ const SELECTOR_CHAIN_RE = new RegExp(`^${SELECTOR_CHUNK_SRC}(?::${SELECTOR_CHUNK_SRC})*$`, "i");
23
+
24
+ /**
25
+ * Extract the lowercased scheme from a URI-shaped input, or `undefined` when
26
+ * the input does not look like a URI.
27
+ *
28
+ * Accepts both hierarchical (`scheme://…`) and opaque (`scheme:rest`) forms —
29
+ * MCP resource URIs may be opaque (`urn:example:document`, `custom:item`).
30
+ * The opaque form is guarded against path-like false positives:
31
+ * - Windows drive paths (`C:\…`, `C:/…`, `C:foo`) — single-letter scheme.
32
+ * - Filenames with extensions (`foo.ts:50`) — dot in the scheme segment.
33
+ * - Read-tool selector tails (`Makefile:12`, `README:raw:1-20`).
34
+ */
35
+ export function extractUriScheme(input: string): string | undefined {
36
+ const hierarchical = input.match(SCHEME_HOST_RE);
37
+ if (hierarchical) return hierarchical[1].toLowerCase();
38
+ const opaque = input.match(OPAQUE_URI_RE);
39
+ if (!opaque) return undefined;
40
+ const [, scheme, rest] = opaque;
41
+ if (scheme.length === 1) return undefined;
42
+ if (scheme.includes(".")) return undefined;
43
+ if (SELECTOR_CHAIN_RE.test(rest)) return undefined;
44
+ return scheme.toLowerCase();
45
+ }
16
46
 
17
47
  /**
18
48
  * Parse an internal URL into an InternalUrl.
@@ -68,5 +98,6 @@ export function parseInternalUrl(input: string): InternalUrl {
68
98
  const result = parsed as InternalUrl;
69
99
  result.rawHost = rawHost;
70
100
  result.rawPathname = pathMatch?.[1] ?? parsed.pathname;
101
+ result.rawHref = input;
71
102
  return result;
72
103
  }
@@ -13,7 +13,7 @@ import { LocalProtocolHandler } from "./local-protocol";
13
13
  import { McpProtocolHandler } from "./mcp-protocol";
14
14
  import { MemoryProtocolHandler } from "./memory-protocol";
15
15
  import { OmpProtocolHandler } from "./omp-protocol";
16
- import { parseInternalUrl } from "./parse";
16
+ import { extractUriScheme, parseInternalUrl } from "./parse";
17
17
  import { RuleProtocolHandler } from "./rule-protocol";
18
18
  import { SkillProtocolHandler } from "./skill-protocol";
19
19
  import { SshProtocolHandler } from "./ssh-protocol";
@@ -79,6 +79,20 @@ export class InternalUrlRouter {
79
79
  return this.#handlers.has(match[1].toLowerCase());
80
80
  }
81
81
 
82
+ /**
83
+ * Whether read can resolve this URL through either a native handler or the
84
+ * MCP resource fallback. MCP resources may use arbitrary custom schemes and
85
+ * may be opaque (`urn:example:document`) rather than hierarchical.
86
+ */
87
+ canResolve(input: string): boolean {
88
+ const scheme = extractUriScheme(input);
89
+ if (!scheme) return false;
90
+ // Registered handlers only accept the hierarchical `scheme://` form;
91
+ // opaque inputs reach the MCP resource fallback alone.
92
+ if (this.#handlers.has(scheme)) return this.canHandle(input);
93
+ return this.#isMcpResourceScheme(scheme);
94
+ }
95
+
82
96
  /** Schemes whose handler supports host/path autocomplete. */
83
97
  completionSchemes(): string[] {
84
98
  const schemes: string[] = [];
@@ -98,10 +112,16 @@ export class InternalUrlRouter {
98
112
  return handler.complete(query, context);
99
113
  }
100
114
 
101
- #route(input: string): { parsed: InternalUrl; handler: ProtocolHandler } {
115
+ #isMcpResourceScheme(scheme: string): boolean {
116
+ return !["file", "http", "https"].includes(scheme) && this.#handlers.has("mcp");
117
+ }
118
+
119
+ #route(input: string, allowMcpResource = false): { parsed: InternalUrl; handler: ProtocolHandler } {
102
120
  const parsed = parseInternalUrl(input);
103
121
  const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
104
- const handler = this.#handlers.get(scheme);
122
+ const handler =
123
+ this.#handlers.get(scheme) ??
124
+ (allowMcpResource && this.#isMcpResourceScheme(scheme) ? this.#handlers.get("mcp") : undefined);
105
125
  if (!handler) {
106
126
  const available = Array.from(this.#handlers.keys())
107
127
  .map(candidate => `${candidate}://`)
@@ -113,7 +133,7 @@ export class InternalUrlRouter {
113
133
 
114
134
  /** Resolve an internal URL through its registered protocol handler. */
115
135
  async resolve(input: string, context?: ResolveContext): Promise<InternalResource> {
116
- const { parsed, handler } = this.#route(input);
136
+ const { parsed, handler } = this.#route(input, true);
117
137
  const resource = await handler.resolve(parsed, context);
118
138
  return { ...resource, immutable: resource.immutable ?? handler.immutable };
119
139
  }
@@ -70,6 +70,12 @@ export interface InternalUrl extends URL {
70
70
  * Raw pathname extracted from input, preserving traversal markers before URL normalization.
71
71
  */
72
72
  rawPathname?: string;
73
+ /**
74
+ * Exact input string this URL was parsed from, before any normalization.
75
+ * Set by `parseInternalUrl`; used where byte-exact URI matching matters
76
+ * (e.g. MCP resource URIs compared by string equality).
77
+ */
78
+ rawHref?: string;
73
79
  }
74
80
 
75
81
  /**
@@ -1,5 +1,5 @@
1
1
  import { type AuthStorage, isAuthRetryableError, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
2
- import { getProxyForProvider, wrapFetchForProxy } from "@oh-my-pi/pi-ai/utils/proxy";
2
+ import { getProxyForUrl, wrapFetchForProxy } from "@oh-my-pi/pi-ai/utils/proxy";
3
3
  import {
4
4
  CODEX_BASE_URL,
5
5
  CODEX_CLIENT_VERSION,
@@ -252,7 +252,7 @@ export class CodexLiveTransport {
252
252
  const url = buildLiveSidebandUrl(callId);
253
253
  const options = {
254
254
  headers: liveSessionHeaders(access, this.#options.sessionId, this.#realtimeSessionId, attestation),
255
- proxy: getProxyForProvider(LIVE_PROVIDER),
255
+ proxy: getProxyForUrl(LIVE_PROVIDER, new URL(url)),
256
256
  } satisfies Bun.WebSocketOptions;
257
257
  const socket: Bun.WebSocket = Reflect.construct(WebSocket, [url, options]);
258
258
  socket.binaryType = "nodebuffer";