@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.
- package/CHANGELOG.md +31 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
- package/dist/cli.js +2989 -2978
- 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 +23 -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/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 +3 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/model-controls.d.ts +3 -0
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +4 -2
- 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/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 +27 -1
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +91 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/hooks/types.ts +0 -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/status-line/segments.ts +3 -1
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +3 -3
- 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 +17 -8
- package/src/session/agent-session-types.ts +3 -0
- package/src/session/agent-session.ts +21 -4
- package/src/session/model-controls.ts +43 -7
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-tools.ts +4 -2
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +21 -2
- 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/read.ts +2 -2
- 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
package/src/capability/index.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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>(
|
|
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}"`);
|
package/src/capability/mcp.ts
CHANGED
|
@@ -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";
|
package/src/capability/types.ts
CHANGED
|
@@ -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
|
*/
|
package/src/cli/read-cli.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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[],
|
package/src/eval/py/runner.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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";
|