@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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/CHANGELOG.md +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -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
|
-
|
|
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);
|
|
@@ -277,6 +356,32 @@ export class ExtensionRunner {
|
|
|
277
356
|
#managedTimers = new ManagedTimers((event, error, stack) =>
|
|
278
357
|
this.emitError({ extensionPath: "<timer>", event, error, stack }),
|
|
279
358
|
);
|
|
359
|
+
/**
|
|
360
|
+
* Dedup markers for `tool_call` emission, keyed `${toolCallId}:${toolName}`.
|
|
361
|
+
* The agent loop emits `tool_call` at arg-prep time (before scheduling and
|
|
362
|
+
* `tool_execution_start`) via the session's `beforeToolCall` wiring; the
|
|
363
|
+
* marker tells `ExtensionToolWrapper.execute` not to emit a second event for
|
|
364
|
+
* the same dispatch. Keyed by call id + tool name because a nested xd://
|
|
365
|
+
* device dispatch reuses the model's toolCallId under a different tool name
|
|
366
|
+
* and must still emit its own event. Bounded: markers for calls whose
|
|
367
|
+
* execute path never runs (policy deny, validation failure) would otherwise
|
|
368
|
+
* accumulate for the session's lifetime.
|
|
369
|
+
*/
|
|
370
|
+
#emittedToolCalls = new Set<string>();
|
|
371
|
+
|
|
372
|
+
/** Records that the loop already emitted `tool_call` for this dispatch. */
|
|
373
|
+
markToolCallEmitted(toolCallId: string, toolName: string): void {
|
|
374
|
+
if (this.#emittedToolCalls.size >= 512) {
|
|
375
|
+
const oldest = this.#emittedToolCalls.values().next().value;
|
|
376
|
+
if (oldest !== undefined) this.#emittedToolCalls.delete(oldest);
|
|
377
|
+
}
|
|
378
|
+
this.#emittedToolCalls.add(`${toolCallId}:${toolName}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Consumes a {@link markToolCallEmitted} marker; true when the loop already emitted. */
|
|
382
|
+
consumeToolCallEmitted(toolCallId: string, toolName: string): boolean {
|
|
383
|
+
return this.#emittedToolCalls.delete(`${toolCallId}:${toolName}`);
|
|
384
|
+
}
|
|
280
385
|
|
|
281
386
|
constructor(
|
|
282
387
|
private readonly extensions: Extension[],
|
|
@@ -637,7 +742,11 @@ export class ExtensionRunner {
|
|
|
637
742
|
: undefined;
|
|
638
743
|
if (signal?.aborted) return undefined;
|
|
639
744
|
try {
|
|
640
|
-
const handlerResult = await raceHandlerWithTimeout(
|
|
745
|
+
const handlerResult = await raceHandlerWithTimeout(
|
|
746
|
+
handlerSignal => handler(event, createHandlerContext(ctx, handlerSignal)),
|
|
747
|
+
timeoutMs,
|
|
748
|
+
signal,
|
|
749
|
+
);
|
|
641
750
|
if (handlerResult === EXTENSION_HANDLER_ABORTED) return undefined;
|
|
642
751
|
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
643
752
|
const error = `handler timed out after ${timeoutMs}ms`;
|
|
@@ -799,7 +908,10 @@ export class ExtensionRunner {
|
|
|
799
908
|
|
|
800
909
|
for (const handler of handlers) {
|
|
801
910
|
try {
|
|
802
|
-
const handlerResult = await raceHandlerWithTimeout(
|
|
911
|
+
const handlerResult = await raceHandlerWithTimeout(
|
|
912
|
+
handlerSignal => handler(event, createHandlerContext(ctx, handlerSignal)),
|
|
913
|
+
timeoutMs,
|
|
914
|
+
);
|
|
803
915
|
|
|
804
916
|
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
805
917
|
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. */
|
|
@@ -157,15 +157,70 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
157
157
|
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
|
|
158
158
|
context?: AgentToolContext,
|
|
159
159
|
): Promise<AgentToolResult<TDetails, TParameters>> {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
160
|
+
// The agent loop emits `tool_call` at arg-prep time (session
|
|
161
|
+
// `beforeToolCall` wiring) so a handler revision lands before concurrency
|
|
162
|
+
// scheduling and `tool_execution_start`. Consume the marker
|
|
163
|
+
// unconditionally so it cannot go stale; emit here only for dispatches
|
|
164
|
+
// the loop never saw — nested xd:// device dispatches and direct
|
|
165
|
+
// (non-loop) execution such as Cursor exec handlers.
|
|
166
|
+
const loopEmittedToolCall = this.runner.consumeToolCallEmitted(toolCallId, this.tool.name);
|
|
167
|
+
// Resolve approval settings up front. A `deny` on the original input short-circuits before the
|
|
168
|
+
// runner is touched — an already-denied tool never emits `tool_call` — while the full gate below
|
|
169
|
+
// re-resolves against the (possibly revised) input so a handler cannot rewrite into a denied or
|
|
170
|
+
// newly prompt-gated command and have it run unapproved.
|
|
163
171
|
const cliAutoApprove = context?.autoApprove === true;
|
|
164
172
|
const settings: Settings | undefined = context?.settings;
|
|
165
173
|
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
|
166
174
|
const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
|
|
167
175
|
const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
168
|
-
|
|
176
|
+
if (resolveApproval(this.tool, approvalArgs(params, context), approvalMode, userPolicies).policy === "deny") {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Tool "${this.tool.name}" is blocked by user policy.\n` +
|
|
179
|
+
`To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 1. Emit tool_call event first - extensions can block execution or revise the input the tool
|
|
184
|
+
// runs with. Doing this BEFORE the approval gate means approval (below) resolves against the
|
|
185
|
+
// input that actually executes, closing the "approve one thing, run another" gap: the prompt
|
|
186
|
+
// text, policy resolution, and provider safety checks all see `effectiveParams`.
|
|
187
|
+
let effectiveParams = params;
|
|
188
|
+
if (!loopEmittedToolCall && this.runner.hasHandlers("tool_call")) {
|
|
189
|
+
try {
|
|
190
|
+
const callResult = (await this.runner.emitToolCall({
|
|
191
|
+
type: "tool_call",
|
|
192
|
+
toolName: this.tool.name,
|
|
193
|
+
toolCallId,
|
|
194
|
+
input: normalizeToolEventInput(
|
|
195
|
+
this.tool.name,
|
|
196
|
+
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
197
|
+
),
|
|
198
|
+
})) as ToolCallEventResult | undefined;
|
|
199
|
+
|
|
200
|
+
if (callResult?.block) {
|
|
201
|
+
const reason = callResult.reason || "Tool execution was blocked by an extension";
|
|
202
|
+
throw new Error(reason);
|
|
203
|
+
}
|
|
204
|
+
// A non-blocking handler may replace the execution input. The returned object is the raw
|
|
205
|
+
// input passed to `execute` (handler-owned; not re-normalized). Skipped for `computer`
|
|
206
|
+
// tool calls, whose event input is a synthetic {actions,pendingSafetyChecks} view
|
|
207
|
+
// (see toolEventArgs) rather than the real execution params.
|
|
208
|
+
if (callResult?.input !== undefined && context?.toolCall?.providerMetadata?.type !== "computer") {
|
|
209
|
+
effectiveParams = callResult.input as typeof params;
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err instanceof Error) {
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Full approval gate against the (possibly revised) input that will actually run — resolves
|
|
220
|
+
// policy and prompts on `effectiveParams`, so the user approves exactly what executes. A revised
|
|
221
|
+
// input that newly resolves to `deny` is caught here even though the original passed the
|
|
222
|
+
// short-circuit above.
|
|
223
|
+
const resolvedArgs = approvalArgs(effectiveParams, context);
|
|
169
224
|
const resolved = resolveApproval(this.tool, resolvedArgs, approvalMode, userPolicies);
|
|
170
225
|
if (resolved.policy === "deny") {
|
|
171
226
|
throw new Error(
|
|
@@ -175,15 +230,17 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
175
230
|
}
|
|
176
231
|
const pendingSafetyChecks = computerSafetyChecks(context);
|
|
177
232
|
// An xd:// device dispatch already cleared the write tool's outer gate at
|
|
178
|
-
// this tool's tier — re-prompting would double-ask for one action.
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
233
|
+
// this tool's tier — re-prompting would double-ask for one action. The
|
|
234
|
+
// bypass only holds while the input is exactly what that outer gate
|
|
235
|
+
// approved: a handler revision here may have raised the tier, so revised
|
|
236
|
+
// input always faces the full gate. Explicit per-tool "prompt" policies
|
|
237
|
+
// and tool-demanded overrides still prompt. Provider safety checks are
|
|
238
|
+
// stronger: yolo, per-tool allow, and xdev approval never acknowledge
|
|
239
|
+
// them on the user's behalf.
|
|
182
240
|
const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, this.tool.name);
|
|
241
|
+
const xdevBypass = context?.xdevApproved === true && effectiveParams === params;
|
|
183
242
|
const approvalCheck = {
|
|
184
|
-
required:
|
|
185
|
-
pendingSafetyChecks.length > 0 ||
|
|
186
|
-
(resolved.policy === "prompt" && (explicitPrompt || context?.xdevApproved !== true)),
|
|
243
|
+
required: pendingSafetyChecks.length > 0 || (resolved.policy === "prompt" && (explicitPrompt || !xdevBypass)),
|
|
187
244
|
reason: resolved.reason,
|
|
188
245
|
};
|
|
189
246
|
|
|
@@ -202,7 +259,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
202
259
|
});
|
|
203
260
|
}
|
|
204
261
|
|
|
205
|
-
const
|
|
262
|
+
const emitApprovalResolved = async (approved: boolean, reason?: string) => {
|
|
206
263
|
if (!hasApprovalHandlers) return;
|
|
207
264
|
await this.runner.emit({
|
|
208
265
|
type: "tool_approval_resolved",
|
|
@@ -218,7 +275,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
218
275
|
// ordinary tier approval, no setting or yolo mode may bypass this gate.
|
|
219
276
|
if (!this.runner.hasUI()) {
|
|
220
277
|
const reason = "no interactive UI available";
|
|
221
|
-
await
|
|
278
|
+
await emitApprovalResolved(false, reason);
|
|
222
279
|
if (pendingSafetyChecks.length > 0) {
|
|
223
280
|
throw new Error(
|
|
224
281
|
`Tool "${this.tool.name}" has pending provider safety checks but no interactive UI is available.`,
|
|
@@ -243,11 +300,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
243
300
|
try {
|
|
244
301
|
choice = await uiContext.select(safetyPrompt, ["Approve", "Deny"]);
|
|
245
302
|
} catch (err) {
|
|
246
|
-
await
|
|
303
|
+
await emitApprovalResolved(false, err instanceof Error ? err.message : "approval aborted");
|
|
247
304
|
throw err;
|
|
248
305
|
}
|
|
249
306
|
const approved = choice === "Approve";
|
|
250
|
-
await
|
|
307
|
+
await emitApprovalResolved(approved, approved ? undefined : "denied by user");
|
|
251
308
|
if (!approved) {
|
|
252
309
|
throw new Error(`Tool call denied by user: ${this.tool.name}`);
|
|
253
310
|
}
|
|
@@ -257,37 +314,12 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
257
314
|
}
|
|
258
315
|
}
|
|
259
316
|
|
|
260
|
-
// 2. Emit tool_call event - extensions can block execution
|
|
261
|
-
if (this.runner.hasHandlers("tool_call")) {
|
|
262
|
-
try {
|
|
263
|
-
const callResult = (await this.runner.emitToolCall({
|
|
264
|
-
type: "tool_call",
|
|
265
|
-
toolName: this.tool.name,
|
|
266
|
-
toolCallId,
|
|
267
|
-
input: normalizeToolEventInput(
|
|
268
|
-
this.tool.name,
|
|
269
|
-
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
270
|
-
),
|
|
271
|
-
})) as ToolCallEventResult | undefined;
|
|
272
|
-
|
|
273
|
-
if (callResult?.block) {
|
|
274
|
-
const reason = callResult.reason || "Tool execution was blocked by an extension";
|
|
275
|
-
throw new Error(reason);
|
|
276
|
-
}
|
|
277
|
-
} catch (err) {
|
|
278
|
-
if (err instanceof Error) {
|
|
279
|
-
throw err;
|
|
280
|
-
}
|
|
281
|
-
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
317
|
// Execute the actual tool
|
|
286
318
|
let result: AgentToolResult<TDetails, TParameters>;
|
|
287
319
|
let executionError: Error | undefined;
|
|
288
320
|
|
|
289
321
|
try {
|
|
290
|
-
result = await this.tool.execute(toolCallId,
|
|
322
|
+
result = await this.tool.execute(toolCallId, effectiveParams, signal, onUpdate, context);
|
|
291
323
|
} catch (err) {
|
|
292
324
|
executionError = err instanceof Error ? err : new Error(String(err));
|
|
293
325
|
result = {
|
|
@@ -304,7 +336,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
304
336
|
toolCallId,
|
|
305
337
|
input: normalizeToolEventInput(
|
|
306
338
|
this.tool.name,
|
|
307
|
-
resolveToolEventInput(this.tool, toolEventArgs(
|
|
339
|
+
resolveToolEventInput(this.tool, toolEventArgs(effectiveParams, context)),
|
|
308
340
|
),
|
|
309
341
|
content: result.content,
|
|
310
342
|
details: result.details,
|
|
@@ -39,8 +39,9 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
39
39
|
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
|
|
40
40
|
context?: AgentToolContext,
|
|
41
41
|
) {
|
|
42
|
-
// Emit tool_call event - hooks can block execution
|
|
42
|
+
// Emit tool_call event - hooks can block execution or revise the input the tool runs with.
|
|
43
43
|
// If hook errors/times out, block by default (fail-safe)
|
|
44
|
+
let effectiveParams = params;
|
|
44
45
|
if (this.hookRunner.hasHandlers("tool_call")) {
|
|
45
46
|
try {
|
|
46
47
|
const callResult = (await this.hookRunner.emitToolCall({
|
|
@@ -57,6 +58,12 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
57
58
|
const reason = callResult.reason || "Tool execution was blocked by a hook";
|
|
58
59
|
throw new Error(reason);
|
|
59
60
|
}
|
|
61
|
+
// A non-blocking handler may replace the execution input. The returned object is the raw
|
|
62
|
+
// input the tool runs with (handler-owned); it is not re-normalized. Skipped for `computer`
|
|
63
|
+
// tool calls, whose real parameters are not represented by the event input.
|
|
64
|
+
if (callResult?.input !== undefined && context?.toolCall?.providerMetadata?.type !== "computer") {
|
|
65
|
+
effectiveParams = callResult.input as Static<TParameters>;
|
|
66
|
+
}
|
|
60
67
|
} catch (err) {
|
|
61
68
|
// Hook error or block - throw to mark as error
|
|
62
69
|
if (err instanceof Error) {
|
|
@@ -68,7 +75,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
68
75
|
|
|
69
76
|
// Execute the actual tool, forwarding onUpdate for progress streaming
|
|
70
77
|
try {
|
|
71
|
-
const result = await this.tool.execute(toolCallId,
|
|
78
|
+
const result = await this.tool.execute(toolCallId, effectiveParams, signal, onUpdate, context);
|
|
72
79
|
|
|
73
80
|
// Emit tool_result event - hooks can modify the result
|
|
74
81
|
if (this.hookRunner.hasHandlers("tool_result")) {
|
|
@@ -78,7 +85,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
78
85
|
toolCallId,
|
|
79
86
|
input: normalizeToolEventInput(
|
|
80
87
|
this.tool.name,
|
|
81
|
-
resolveToolEventInput(this.tool,
|
|
88
|
+
resolveToolEventInput(this.tool, effectiveParams as Record<string, unknown>),
|
|
82
89
|
),
|
|
83
90
|
content: result.content,
|
|
84
91
|
details: result.details,
|
|
@@ -104,7 +111,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
104
111
|
toolCallId,
|
|
105
112
|
input: normalizeToolEventInput(
|
|
106
113
|
this.tool.name,
|
|
107
|
-
resolveToolEventInput(this.tool,
|
|
114
|
+
resolveToolEventInput(this.tool, effectiveParams as Record<string, unknown>),
|
|
108
115
|
),
|
|
109
116
|
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
110
117
|
details: undefined,
|
|
@@ -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 {
|
|
@@ -289,13 +289,30 @@ export interface TodoReminderEvent {
|
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
291
|
* Return type for `tool_call` handlers.
|
|
292
|
-
* Allows handlers to block tool execution.
|
|
292
|
+
* Allows handlers to block tool execution or revise the input the tool runs with.
|
|
293
293
|
*/
|
|
294
294
|
export interface ToolCallEventResult {
|
|
295
295
|
/** If true, block the tool from executing */
|
|
296
296
|
block?: boolean;
|
|
297
297
|
/** Reason for blocking (returned to LLM as error) */
|
|
298
298
|
reason?: string;
|
|
299
|
+
/**
|
|
300
|
+
* Replacement input the tool executes with, instead of the original arguments. Ignored when
|
|
301
|
+
* `block` is true. This is the raw execution input passed to the tool's `execute` (the handler
|
|
302
|
+
* owns its correctness) — not the normalized `event.input` view, which may carry derived
|
|
303
|
+
* gate-only fields (e.g. hashline `edit` `path`/`paths`) that are not real parameters. When
|
|
304
|
+
* multiple handlers set `input`, the last one wins; handlers do not observe each other's
|
|
305
|
+
* revisions (each sees the original `event.input`). Not applied to `computer` tool calls.
|
|
306
|
+
*
|
|
307
|
+
* For model-issued tool calls the event fires at arg-prep time in the agent loop, before
|
|
308
|
+
* concurrency scheduling, `tool_execution_start`, and the approval gate: the revision is
|
|
309
|
+
* revalidated against the tool schema and becomes what the loop schedules, displays, persists,
|
|
310
|
+
* and executes — the user always approves what actually runs. For dispatches the loop never
|
|
311
|
+
* sees (nested `write xd://` device calls, Cursor direct execution) the tool wrapper applies
|
|
312
|
+
* the revision before its own approval gate; a revised nested xd:// input forfeits the outer
|
|
313
|
+
* write gate's approval and faces the full prompt again.
|
|
314
|
+
*/
|
|
315
|
+
input?: Record<string, unknown>;
|
|
299
316
|
}
|
|
300
317
|
|
|
301
318
|
/**
|
|
@@ -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
|
|
107
|
+
* Protocol handler for MCP resources.
|
|
99
108
|
*
|
|
100
|
-
* URL
|
|
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
|
-
|
|
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
|
-
#
|
|
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 =
|
|
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
|
/**
|
package/src/live/transport.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AuthStorage, isAuthRetryableError, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import {
|
|
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:
|
|
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";
|
package/src/lsp/client.ts
CHANGED
|
@@ -918,7 +918,7 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
|
|
|
918
918
|
if (isEnoent(err)) return;
|
|
919
919
|
throw err;
|
|
920
920
|
}
|
|
921
|
-
const languageId = detectLanguageId(filePath);
|
|
921
|
+
const languageId = client.config.languageId ?? detectLanguageId(filePath);
|
|
922
922
|
throwIfAborted(signal);
|
|
923
923
|
|
|
924
924
|
await sendNotification(
|
|
@@ -992,7 +992,7 @@ export async function syncContent(
|
|
|
992
992
|
|
|
993
993
|
if (!info) {
|
|
994
994
|
// Open file with provided content instead of reading from disk
|
|
995
|
-
const languageId = detectLanguageId(filePath);
|
|
995
|
+
const languageId = client.config.languageId ?? detectLanguageId(filePath);
|
|
996
996
|
throwIfAborted(signal);
|
|
997
997
|
await sendNotification(
|
|
998
998
|
client,
|