@oh-my-pi/pi-coding-agent 16.3.4 → 16.3.5
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 +18 -0
- package/dist/cli.js +3039 -3016
- package/dist/types/hindsight/content.d.ts +4 -0
- package/dist/types/internal-urls/artifact-protocol.d.ts +8 -0
- package/dist/types/internal-urls/types.d.ts +10 -0
- package/dist/types/lsp/config.d.ts +4 -0
- package/dist/types/lsp/edits.d.ts +2 -0
- package/dist/types/mcp/oauth-discovery.d.ts +30 -0
- package/dist/types/modes/components/login-dialog.d.ts +10 -2
- package/dist/types/modes/components/transcript-container.d.ts +0 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +20 -3
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -3
- package/dist/types/modes/rpc/rpc-types.d.ts +6 -0
- package/dist/types/subprocess/worker-runtime.d.ts +11 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/utils/git.d.ts +16 -0
- package/package.json +12 -12
- package/src/cli/auth-broker-cli.ts +13 -2
- package/src/cli/tiny-models-cli.ts +12 -5
- package/src/hindsight/content.ts +31 -0
- package/src/internal-urls/artifact-protocol.ts +97 -53
- package/src/internal-urls/types.ts +10 -0
- package/src/lsp/config.ts +15 -0
- package/src/lsp/edits.ts +28 -7
- package/src/lsp/index.ts +46 -4
- package/src/mcp/oauth-discovery.ts +88 -18
- package/src/mnemopi/state.ts +26 -2
- package/src/modes/components/login-dialog.ts +16 -2
- package/src/modes/components/mcp-add-wizard.ts +9 -1
- package/src/modes/components/transcript-container.ts +0 -26
- package/src/modes/controllers/mcp-command-controller.ts +106 -29
- package/src/modes/controllers/selector-controller.ts +9 -1
- package/src/modes/rpc/rpc-client.ts +8 -4
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/modes/rpc/rpc-types.ts +13 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +18 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/subprocess/worker-runtime.ts +219 -2
- package/src/task/worktree.ts +28 -6
- package/src/tiny/worker.ts +14 -4
- package/src/tools/bash-skill-urls.ts +3 -3
- package/src/tools/grep.ts +19 -2
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +198 -1
- package/src/utils/git.ts +20 -0
- package/src/utils/open.ts +51 -6
|
@@ -8,7 +8,13 @@ import { type Component, replaceTabs, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { SourceMeta } from "../../capability/types";
|
|
10
10
|
import { expandEnvVarsDeep } from "../../discovery/helpers";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
analyzeAuthError,
|
|
13
|
+
discoverOAuthEndpoints,
|
|
14
|
+
fetchResourceMetadataScopes,
|
|
15
|
+
loadAllMCPConfigs,
|
|
16
|
+
MCPManager,
|
|
17
|
+
} from "../../mcp";
|
|
12
18
|
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
|
|
13
19
|
import {
|
|
14
20
|
addMCPServer,
|
|
@@ -43,6 +49,7 @@ import {
|
|
|
43
49
|
import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
44
50
|
import { shortenPath } from "../../tools/render-utils";
|
|
45
51
|
import { urlHyperlinkAlways } from "../../tui";
|
|
52
|
+
import { copyToClipboard } from "../../utils/clipboard";
|
|
46
53
|
import { openPath } from "../../utils/open";
|
|
47
54
|
import { ChatBlock } from "../components/chat-block";
|
|
48
55
|
import { MCPAddWizard } from "../components/mcp-add-wizard";
|
|
@@ -73,23 +80,77 @@ function raceAbortSignal<T>(promise: Promise<T>, signal: AbortSignal, createErro
|
|
|
73
80
|
});
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Minimum column budget for URL wrapping. Below this the terminal is
|
|
85
|
+
* effectively unusable, but we still emit chunks so no character is silently
|
|
86
|
+
* dropped and the user can widen and reflow.
|
|
87
|
+
*/
|
|
88
|
+
const MCP_AUTH_MIN_WRAP_WIDTH = 16;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wrap `url` into rows that each fit inside `width`, prefixed by a shared
|
|
92
|
+
* single-column indent so nested composition doesn't touch column 0. When the
|
|
93
|
+
* label + URL fit on one line, returns a single row; otherwise puts the label
|
|
94
|
+
* on its own row and slices the URL into fixed-width chunks. URL chunks are
|
|
95
|
+
* plain code points — browsers strip whitespace when pasted into the address
|
|
96
|
+
* bar, so a multi-row selection copies back to the intact URL.
|
|
97
|
+
*/
|
|
98
|
+
function wrapUrlRows(label: string, url: string, width: number): string[] {
|
|
99
|
+
const indent = " ";
|
|
100
|
+
const sanitized = replaceTabs(url);
|
|
101
|
+
const effective = Math.max(MCP_AUTH_MIN_WRAP_WIDTH, Math.trunc(width));
|
|
102
|
+
const inlineWidth = indent.length + label.length + 1 + sanitized.length;
|
|
103
|
+
if (inlineWidth <= effective) {
|
|
104
|
+
return [`${indent}${theme.fg("muted", `${label} ${sanitized}`)}`];
|
|
105
|
+
}
|
|
106
|
+
const chunkWidth = Math.max(1, effective - indent.length);
|
|
107
|
+
const rows: string[] = [`${indent}${theme.fg("muted", label)}`];
|
|
108
|
+
for (let i = 0; i < sanitized.length; i += chunkWidth) {
|
|
109
|
+
rows.push(`${indent}${theme.fg("muted", sanitized.slice(i, i + chunkWidth))}`);
|
|
110
|
+
}
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Renders the MCP OAuth fallback URL. Always shows the full authorization URL
|
|
116
|
+
* as the primary `Copy URL:` target — that works from any machine, including
|
|
117
|
+
* SSH/WSL/headless sessions where the OMP-hosted `/launch` loopback URL would
|
|
118
|
+
* resolve against the user's local browser and fail.
|
|
119
|
+
*
|
|
120
|
+
* The render is `width`-aware: on any viewport narrower than the composed row
|
|
121
|
+
* ({@link TUI#prepareLine} truncates anything wider with `Ellipsis.Omit`, no
|
|
122
|
+
* marker), the URL is hard-wrapped into width-fitted rows so the primary copy
|
|
123
|
+
* target can never silently lose trailing OAuth parameters — the failure mode
|
|
124
|
+
* that motivated #4418 in the first place. Browsers strip whitespace when a
|
|
125
|
+
* multi-row selection is pasted into the address bar, so the reassembled URL
|
|
126
|
+
* is byte-identical to what we rendered.
|
|
127
|
+
*
|
|
128
|
+
* When the flow's callback server hosts a short `launchUrl`, it is offered
|
|
129
|
+
* as an additional local shortcut for wide-terminal local users. The OSC 8
|
|
130
|
+
* hyperlink continues to carry the full URL for terminals that support it.
|
|
131
|
+
*/
|
|
77
132
|
export class MCPAuthorizationLinkPrompt implements Component {
|
|
78
|
-
readonly #
|
|
133
|
+
readonly #fullUrl: string;
|
|
134
|
+
readonly #launchUrl: string | undefined;
|
|
79
135
|
|
|
80
|
-
constructor(url: string) {
|
|
81
|
-
this.#
|
|
136
|
+
constructor(url: string, launchUrl?: string) {
|
|
137
|
+
this.#fullUrl = url;
|
|
138
|
+
this.#launchUrl = launchUrl && launchUrl !== url ? launchUrl : undefined;
|
|
82
139
|
}
|
|
83
140
|
|
|
84
141
|
invalidate(): void {}
|
|
85
142
|
|
|
86
|
-
render(
|
|
87
|
-
const link = urlHyperlinkAlways(this.#
|
|
88
|
-
|
|
143
|
+
render(width: number): readonly string[] {
|
|
144
|
+
const link = urlHyperlinkAlways(this.#fullUrl, "Click here to authorize");
|
|
145
|
+
const lines: string[] = [
|
|
89
146
|
` ${theme.fg("success", "Open authorization URL:")}`,
|
|
90
147
|
` ${theme.fg("accent", link)}`,
|
|
91
|
-
|
|
148
|
+
...wrapUrlRows("Copy URL:", this.#fullUrl, width),
|
|
92
149
|
];
|
|
150
|
+
if (this.#launchUrl) {
|
|
151
|
+
lines.push(...wrapUrlRows("Local shortcut (this machine only):", this.#launchUrl, width));
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
93
154
|
}
|
|
94
155
|
}
|
|
95
156
|
|
|
@@ -506,11 +567,18 @@ export class MCPCommandController {
|
|
|
506
567
|
finalConfig.url,
|
|
507
568
|
authResult.authServerUrl,
|
|
508
569
|
authResult.resourceMetadataUrl,
|
|
570
|
+
{ protectedScopes: authResult.scopes },
|
|
509
571
|
);
|
|
510
572
|
} catch {
|
|
511
573
|
// Ignore discovery error and handle below.
|
|
512
574
|
}
|
|
513
575
|
}
|
|
576
|
+
if (oauth && !oauth.scopes && authResult.resourceMetadataUrl) {
|
|
577
|
+
// JSON-error-body path skips `discoverOAuthEndpoints`; fetch the
|
|
578
|
+
// advertised protected-resource metadata for the required scopes.
|
|
579
|
+
const scopes = await fetchResourceMetadataScopes(authResult.resourceMetadataUrl);
|
|
580
|
+
if (scopes) oauth = { ...oauth, scopes };
|
|
581
|
+
}
|
|
514
582
|
|
|
515
583
|
if (!oauth) {
|
|
516
584
|
this.ctx.showError(
|
|
@@ -689,7 +757,7 @@ export class MCPCommandController {
|
|
|
689
757
|
stripSameOriginResource: opts?.stripSameOriginResource,
|
|
690
758
|
},
|
|
691
759
|
{
|
|
692
|
-
onAuth: (info: { url: string; instructions?: string }) => {
|
|
760
|
+
onAuth: (info: { url: string; launchUrl?: string; instructions?: string }) => {
|
|
693
761
|
// Show auth URL prominently in chat as one block
|
|
694
762
|
const block = new TranscriptBlock();
|
|
695
763
|
this.ctx.present(block);
|
|
@@ -707,24 +775,25 @@ export class MCPCommandController {
|
|
|
707
775
|
block.addChild(new Text(theme.fg("muted", MCP_MANUAL_LOGIN_TIP), 1, 0));
|
|
708
776
|
block.addChild(new Spacer(1));
|
|
709
777
|
block.addChild(new Text(theme.fg("accent", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), 1, 0));
|
|
710
|
-
//
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
778
|
+
// `openPath` is best-effort — it logs spawn failures but never
|
|
779
|
+
// throws, so we always render the copy-URL fallback beneath the
|
|
780
|
+
// "attempting to open browser" line and no earlier try/catch is
|
|
781
|
+
// worth keeping.
|
|
782
|
+
openPath(info.url);
|
|
783
|
+
// Stage the FULL authorization URL on the clipboard via OSC 52.
|
|
784
|
+
// The full URL works from any machine (unlike `launchUrl`, which
|
|
785
|
+
// only resolves against the OMP host), and OSC 52 is a
|
|
786
|
+
// wire-level protocol — the terminal writes it to the user's
|
|
787
|
+
// LOCAL clipboard even when OMP is on a remote SSH box.
|
|
788
|
+
// Best-effort: falls back to the visible copy-URL rows below
|
|
789
|
+
// whether or not the terminal honors OSC 52.
|
|
790
|
+
void copyToClipboard(info.url).catch(() => {});
|
|
791
|
+
block.addChild(new Spacer(1));
|
|
792
|
+
block.addChild(new Text(theme.fg("success", "→ Attempting to open browser..."), 1, 0));
|
|
793
|
+
block.addChild(new Spacer(1));
|
|
794
|
+
block.addChild(new Text(theme.fg("muted", "Alternative if browser did not open:"), 1, 0));
|
|
795
|
+
block.addChild(new MCPAuthorizationLinkPrompt(info.url, info.launchUrl));
|
|
796
|
+
this.ctx.ui.requestRender();
|
|
728
797
|
},
|
|
729
798
|
onProgress: (message: string) => {
|
|
730
799
|
this.ctx.present([new Spacer(1), new Text(theme.fg("muted", message), 1, 0)]);
|
|
@@ -1010,7 +1079,15 @@ export class MCPCommandController {
|
|
|
1010
1079
|
let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
|
|
1011
1080
|
|
|
1012
1081
|
if (!oauth && (config.type === "http" || config.type === "sse") && config.url) {
|
|
1013
|
-
oauth = await discoverOAuthEndpoints(config.url, authResult.authServerUrl, authResult.resourceMetadataUrl
|
|
1082
|
+
oauth = await discoverOAuthEndpoints(config.url, authResult.authServerUrl, authResult.resourceMetadataUrl, {
|
|
1083
|
+
protectedScopes: authResult.scopes,
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
if (oauth && !oauth.scopes && authResult.resourceMetadataUrl) {
|
|
1087
|
+
// JSON-error-body path skips `discoverOAuthEndpoints`; fetch the
|
|
1088
|
+
// advertised protected-resource metadata for the required scopes.
|
|
1089
|
+
const scopes = await fetchResourceMetadataScopes(authResult.resourceMetadataUrl);
|
|
1090
|
+
if (scopes) oauth = { ...oauth, scopes };
|
|
1014
1091
|
}
|
|
1015
1092
|
|
|
1016
1093
|
if (!oauth) {
|
|
@@ -1117,11 +1117,19 @@ export class SelectorController {
|
|
|
1117
1117
|
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
|
|
1118
1118
|
try {
|
|
1119
1119
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
1120
|
-
onAuth: (info: { url: string; instructions?: string }) => {
|
|
1120
|
+
onAuth: (info: { url: string; launchUrl?: string; instructions?: string }) => {
|
|
1121
1121
|
const block = new TranscriptBlock();
|
|
1122
|
+
// Full URL first: works from any machine, including SSH boxes
|
|
1123
|
+
// where the OMP-hosted `launchUrl` would resolve against the
|
|
1124
|
+
// user's local browser and fail.
|
|
1122
1125
|
block.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
1123
1126
|
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
1124
1127
|
block.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
1128
|
+
if (info.launchUrl && info.launchUrl !== info.url) {
|
|
1129
|
+
block.addChild(
|
|
1130
|
+
new Text(theme.fg("dim", `Local shortcut (this machine only): ${info.launchUrl}`), 1, 0),
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1125
1133
|
if (info.instructions) {
|
|
1126
1134
|
block.addChild(new Spacer(1));
|
|
1127
1135
|
block.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
@@ -724,17 +724,21 @@ export class RpcClient {
|
|
|
724
724
|
* The server will emit an `open_url` extension_ui_request for the auth URL.
|
|
725
725
|
* Resolves when login completes or rejects on failure.
|
|
726
726
|
*
|
|
727
|
-
* @param onOpenUrl Called when the server emits the auth URL. The host must
|
|
728
|
-
*
|
|
727
|
+
* @param onOpenUrl Called when the server emits the auth URL. The host must
|
|
728
|
+
* open `url` in a browser for the callback-server OAuth flow to complete.
|
|
729
|
+
* When the flow's callback server hosts a `/launch` redirect, `launchUrl`
|
|
730
|
+
* is a short loopback URL that 302s to `url` — hosts SHOULD surface it as
|
|
731
|
+
* the truncation-safe copy target so terminal viewport clipping cannot
|
|
732
|
+
* corrupt trailing OAuth query parameters (e.g. `code_challenge_method=S256`).
|
|
729
733
|
*/
|
|
730
734
|
async login(
|
|
731
735
|
providerId: string,
|
|
732
|
-
options?: { onOpenUrl?: (url: string, instructions?: string) => void },
|
|
736
|
+
options?: { onOpenUrl?: (url: string, instructions?: string, launchUrl?: string) => void },
|
|
733
737
|
): Promise<{ providerId: string }> {
|
|
734
738
|
const { onOpenUrl } = options ?? {};
|
|
735
739
|
const listener = onOpenUrl
|
|
736
740
|
? (req: RpcExtensionUIRequest) => {
|
|
737
|
-
if (req.method === "open_url") onOpenUrl(req.url, req.instructions);
|
|
741
|
+
if (req.method === "open_url") onOpenUrl(req.url, req.instructions, req.launchUrl);
|
|
738
742
|
}
|
|
739
743
|
: undefined;
|
|
740
744
|
if (listener) this.#extensionUiListeners.add(listener);
|
|
@@ -370,7 +370,19 @@ export type RpcExtensionUIRequest =
|
|
|
370
370
|
}
|
|
371
371
|
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
|
|
372
372
|
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
|
|
373
|
-
| {
|
|
373
|
+
| {
|
|
374
|
+
type: "extension_ui_request";
|
|
375
|
+
id: string;
|
|
376
|
+
method: "open_url";
|
|
377
|
+
url: string;
|
|
378
|
+
/**
|
|
379
|
+
* Short loopback URL that 302-redirects to {@link url}. When present,
|
|
380
|
+
* hosts SHOULD surface it as the copy target so terminal viewport
|
|
381
|
+
* truncation cannot corrupt OAuth query parameters on the full URL.
|
|
382
|
+
*/
|
|
383
|
+
launchUrl?: string;
|
|
384
|
+
instructions?: string;
|
|
385
|
+
};
|
|
374
386
|
|
|
375
387
|
// ============================================================================
|
|
376
388
|
// Host Tool Frames (bidirectional)
|
|
@@ -80,6 +80,7 @@ export class SignInTab implements SetupTab {
|
|
|
80
80
|
#selector: OAuthSelectorComponent;
|
|
81
81
|
#statusLines: string[] = [];
|
|
82
82
|
#authUrl: string | undefined;
|
|
83
|
+
#authLaunchUrl: string | undefined;
|
|
83
84
|
#prompt: PromptState | undefined;
|
|
84
85
|
#promptResolve: ((value: string) => void) | undefined;
|
|
85
86
|
#loginAbort: AbortController | undefined;
|
|
@@ -146,6 +147,9 @@ export class SignInTab implements SetupTab {
|
|
|
146
147
|
theme.fg("accent", `Browser login: ${loginUrlLink(this.#authUrl)} ${loginCopyHint()}`),
|
|
147
148
|
...urlLines.slice(0, 2),
|
|
148
149
|
);
|
|
150
|
+
if (this.#authLaunchUrl) {
|
|
151
|
+
lines.push(theme.fg("dim", `Local shortcut (this machine only): ${this.#authLaunchUrl}`));
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
154
|
if (this.#prompt) {
|
|
151
155
|
lines.push(theme.fg("warning", this.#prompt.message));
|
|
@@ -182,6 +186,7 @@ export class SignInTab implements SetupTab {
|
|
|
182
186
|
this.#loggingInProvider = providerId;
|
|
183
187
|
this.#statusLines = [theme.fg("dim", "Starting OAuth flow…")];
|
|
184
188
|
this.#authUrl = undefined;
|
|
189
|
+
this.#authLaunchUrl = undefined;
|
|
185
190
|
this.#loginAbort = new AbortController();
|
|
186
191
|
this.host.restoreFocus();
|
|
187
192
|
this.host.requestRender();
|
|
@@ -189,7 +194,17 @@ export class SignInTab implements SetupTab {
|
|
|
189
194
|
await this.#authStorage.login(providerId as OAuthProvider, {
|
|
190
195
|
signal: this.#loginAbort.signal,
|
|
191
196
|
onAuth: info => {
|
|
197
|
+
// Store the full authorization URL as the primary copy/display
|
|
198
|
+
// target: it works from any machine, including SSH boxes where
|
|
199
|
+
// the OMP-hosted `launchUrl` would resolve against the user's
|
|
200
|
+
// local browser and fail. The wizard render uses
|
|
201
|
+
// `wrapTextWithAnsi`, so long URLs wrap across lines rather
|
|
202
|
+
// than getting truncated — the RFC 7636 §4.3 PKCE-downgrade
|
|
203
|
+
// bug that motivated `launchUrl` is unreachable through this
|
|
204
|
+
// surface. `launchUrl` is still surfaced as an optional local
|
|
205
|
+
// shortcut for wide-terminal local users.
|
|
192
206
|
this.#authUrl = info.url;
|
|
207
|
+
this.#authLaunchUrl = info.launchUrl && info.launchUrl !== info.url ? info.launchUrl : undefined;
|
|
193
208
|
this.#statusLines = [];
|
|
194
209
|
if (info.instructions) {
|
|
195
210
|
this.#statusLines.push(theme.fg("warning", info.instructions));
|
|
@@ -216,6 +231,7 @@ export class SignInTab implements SetupTab {
|
|
|
216
231
|
theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`),
|
|
217
232
|
];
|
|
218
233
|
this.#authUrl = undefined;
|
|
234
|
+
this.#authLaunchUrl = undefined;
|
|
219
235
|
this.#loggingInProvider = undefined;
|
|
220
236
|
this.#loginAbort = undefined;
|
|
221
237
|
this.#selector.stopValidation();
|
|
@@ -227,6 +243,7 @@ export class SignInTab implements SetupTab {
|
|
|
227
243
|
if (this.#loginAbort?.signal.aborted) {
|
|
228
244
|
this.#statusLines = [theme.fg("dim", "Login cancelled.")];
|
|
229
245
|
this.#authUrl = undefined;
|
|
246
|
+
this.#authLaunchUrl = undefined;
|
|
230
247
|
} else {
|
|
231
248
|
const message = error instanceof Error ? error.message : String(error);
|
|
232
249
|
this.#statusLines = [
|
|
@@ -234,6 +251,7 @@ export class SignInTab implements SetupTab {
|
|
|
234
251
|
theme.fg("dim", "Choose another provider or press Esc to continue."),
|
|
235
252
|
];
|
|
236
253
|
this.#authUrl = undefined;
|
|
254
|
+
this.#authLaunchUrl = undefined;
|
|
237
255
|
}
|
|
238
256
|
this.#loggingInProvider = undefined;
|
|
239
257
|
this.#loginAbort = undefined;
|
|
@@ -67,7 +67,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
67
67
|
|
|
68
68
|
# Internal URIs
|
|
69
69
|
|
|
70
|
-
All URI schemes take the same line selectors. `artifact://<id>` recovers
|
|
70
|
+
All URI schemes take the same line selectors. `artifact://<id>` recovers spilled output; large artifacts block unbounded `:raw`, so page with `artifact://<id>:N-M` / `artifact://<id>:raw:N-M` and use the reported artifact file path for search/copy workflows.
|
|
71
71
|
|
|
72
72
|
`ssh://host/<absolute-path>` reads a remote text file (UTF-8, ≤1 MiB) or lists a directory one level deep, on a pre-configured SSH host or `~/.ssh/config` alias; `ssh://host/` lists the remote root and bare `ssh://` lists the configured hosts. Files are also writable via `write` and searchable via `search`; a directory only lists (`search` refuses a directory, `write` refuses to overwrite one). A literal `:`, `?`, or `#` in the remote path must be percent-encoded (`%3A`/`%3F`/`%23`) — a trailing `:sel` is read as a line selector, and `?`/`#` start a URL query/fragment. Requires a POSIX login shell (`sh`/`bash`/`zsh`); a Windows host or a non-POSIX shell (fish, csh/tcsh) is rejected — use the `ssh` tool there.
|
|
73
73
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fsp from "node:fs/promises";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ProgressInfo } from "@huggingface/transformers";
|
|
@@ -27,6 +28,15 @@ import packageJson from "../../package.json" with { type: "json" };
|
|
|
27
28
|
|
|
28
29
|
export const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
29
30
|
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
|
|
31
|
+
const ONNX_RUNTIME_NODE_PACKAGE = "onnxruntime-node";
|
|
32
|
+
const ONNX_RUNTIME_CUDA_INSTALL = "cuda12";
|
|
33
|
+
const ONNX_RUNTIME_CUDA_PROVIDER_FILES = [
|
|
34
|
+
"libonnxruntime_providers_cuda.so",
|
|
35
|
+
"libonnxruntime_providers_shared.so",
|
|
36
|
+
"libonnxruntime_providers_tensorrt.so",
|
|
37
|
+
] as const;
|
|
38
|
+
const LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR = path.join("bin", "napi-v6", "linux", "x64");
|
|
39
|
+
|
|
30
40
|
const sourceRequire = createRequire(import.meta.url);
|
|
31
41
|
|
|
32
42
|
// ── Error serialization ─────────────────────────────────────────────
|
|
@@ -162,6 +172,87 @@ export async function installSharpStubResolver(runtimeDir: string): Promise<stri
|
|
|
162
172
|
return nodeModules;
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
function shouldInstallOnnxRuntimeCudaProviders(device: string | undefined): boolean {
|
|
176
|
+
const normalized = device?.trim().toLowerCase();
|
|
177
|
+
return (
|
|
178
|
+
process.platform === "linux" &&
|
|
179
|
+
process.arch === "x64" &&
|
|
180
|
+
(normalized === "cuda" || normalized === "gpu" || normalized === "auto")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function missingOnnxRuntimeCudaProviderFiles(binDir: string): Promise<string[]> {
|
|
185
|
+
const missing: string[] = [];
|
|
186
|
+
for (const file of ONNX_RUNTIME_CUDA_PROVIDER_FILES) {
|
|
187
|
+
try {
|
|
188
|
+
await fsp.access(path.join(binDir, file));
|
|
189
|
+
} catch {
|
|
190
|
+
missing.push(file);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return missing;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
197
|
+
if (!stream) return "";
|
|
198
|
+
return new Response(stream).text();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function installOnnxRuntimeCudaProviders(packageDir: string, runtimeDir: string, binDir: string): Promise<void> {
|
|
202
|
+
const script = path.join(packageDir, "script", "install.js");
|
|
203
|
+
try {
|
|
204
|
+
await fsp.access(script);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`ONNX Runtime CUDA provider binaries are missing from ${binDir}, and ${script} is unavailable. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const proc = Bun.spawn([process.execPath, script], {
|
|
212
|
+
cwd: runtimeDir,
|
|
213
|
+
env: { ...Bun.env, BUN_BE_BUN: "1", ONNXRUNTIME_NODE_INSTALL: ONNX_RUNTIME_CUDA_INSTALL },
|
|
214
|
+
stdout: "pipe",
|
|
215
|
+
stderr: "pipe",
|
|
216
|
+
});
|
|
217
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
218
|
+
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
219
|
+
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
220
|
+
proc.exited,
|
|
221
|
+
]);
|
|
222
|
+
if (exitCode !== 0) {
|
|
223
|
+
const output = `${stdout}\n${stderr}`.trim();
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Failed to install ONNX Runtime CUDA provider binaries into ${binDir} with ${process.execPath} ${script} (exit ${exitCode}). Remove the tiny-model side runtime cache at ${runtimeDir} and retry with network access. ${output}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Repairs the compiled Transformers side runtime when CUDA was requested and
|
|
232
|
+
* Bun skipped `onnxruntime-node`'s NuGet sidecar install.
|
|
233
|
+
*/
|
|
234
|
+
export async function ensureOnnxRuntimeCudaProviders(
|
|
235
|
+
runtimeDir: string,
|
|
236
|
+
device = process.env.PI_TINY_DEVICE,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
if (!shouldInstallOnnxRuntimeCudaProviders(device)) return;
|
|
239
|
+
const nodeModules = path.join(runtimeDir, "node_modules");
|
|
240
|
+
const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
|
|
241
|
+
if (!manifest)
|
|
242
|
+
throw new Error(`Unable to resolve ${ONNX_RUNTIME_NODE_PACKAGE} in compiled runtime at ${nodeModules}`);
|
|
243
|
+
const packageDir = path.dirname(manifest);
|
|
244
|
+
const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
|
|
245
|
+
const missing = await missingOnnxRuntimeCudaProviderFiles(binDir);
|
|
246
|
+
if (missing.length === 0) return;
|
|
247
|
+
|
|
248
|
+
await installOnnxRuntimeCudaProviders(packageDir, runtimeDir, binDir);
|
|
249
|
+
const stillMissing = await missingOnnxRuntimeCudaProviderFiles(binDir);
|
|
250
|
+
if (stillMissing.length === 0) return;
|
|
251
|
+
throw new Error(
|
|
252
|
+
`ONNX Runtime CUDA provider install completed but ${stillMissing.join(", ")} are still missing from ${binDir}. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
165
256
|
/**
|
|
166
257
|
* Prepare a freshly-installed compiled runtime for loading and return the
|
|
167
258
|
* absolute entrypoint of `packageName` to `require`.
|
|
@@ -211,6 +302,114 @@ interface ConfigurableTransformers {
|
|
|
211
302
|
LogLevel: { ERROR: unknown };
|
|
212
303
|
}
|
|
213
304
|
|
|
305
|
+
export interface TransformersRuntimeMetadata {
|
|
306
|
+
__ompRuntimeNodeModules?: string;
|
|
307
|
+
__ompTransformersEntry?: string;
|
|
308
|
+
__ompCudaRepairError?: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function attachTransformersRuntimeMetadata<T extends ConfigurableTransformers>(
|
|
312
|
+
transformers: T,
|
|
313
|
+
metadata: TransformersRuntimeMetadata,
|
|
314
|
+
): T {
|
|
315
|
+
const runtime = transformers as T & TransformersRuntimeMetadata;
|
|
316
|
+
runtime.__ompRuntimeNodeModules = metadata.__ompRuntimeNodeModules;
|
|
317
|
+
runtime.__ompTransformersEntry = metadata.__ompTransformersEntry;
|
|
318
|
+
runtime.__ompCudaRepairError = metadata.__ompCudaRepairError;
|
|
319
|
+
return runtime;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const TRANSITIVE_CUDA_LIBRARY_RE =
|
|
323
|
+
/\b(lib(?:cu|nv)[A-Za-z0-9_.+-]*\.so(?:\.[0-9]+)*)\b[^:\n]*:\s*cannot open shared object file/iu;
|
|
324
|
+
const CUDA_DEVICE_UNAVAILABLE_RE = /\bCUDA failure 100\b|no CUDA-capable device is detected|cudaSetDevice|GPU=-1/iu;
|
|
325
|
+
|
|
326
|
+
function cudaDeviceUnavailable(error: unknown): boolean {
|
|
327
|
+
return CUDA_DEVICE_UNAVAILABLE_RE.test(errorText(error));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function missingCudaLibrary(error: unknown): string | undefined {
|
|
331
|
+
return TRANSITIVE_CUDA_LIBRARY_RE.exec(errorText(error))?.[1];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function cudaFailureCause(
|
|
335
|
+
metadata: TransformersRuntimeMetadata,
|
|
336
|
+
error: unknown,
|
|
337
|
+
missingFiles: readonly string[],
|
|
338
|
+
): string {
|
|
339
|
+
if (metadata.__ompCudaRepairError) {
|
|
340
|
+
return `ONNX Runtime CUDA provider install failed: ${metadata.__ompCudaRepairError}`;
|
|
341
|
+
}
|
|
342
|
+
if (missingFiles.length > 0) return `missing ONNX Runtime CUDA provider file(s): ${missingFiles.join(", ")}`;
|
|
343
|
+
const missingLibrary = missingCudaLibrary(error);
|
|
344
|
+
if (missingLibrary) return `${missingLibrary}: cannot open shared object file`;
|
|
345
|
+
if (cudaDeviceUnavailable(error)) {
|
|
346
|
+
return "CUDA provider files are present; CUDA runtime reports no CUDA-capable device";
|
|
347
|
+
}
|
|
348
|
+
return "CUDA provider files are present; inspect the original ONNX Runtime CUDA error";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function cudaFailureHint(
|
|
352
|
+
metadata: TransformersRuntimeMetadata,
|
|
353
|
+
error: unknown,
|
|
354
|
+
missingFiles: readonly string[],
|
|
355
|
+
): string {
|
|
356
|
+
if (metadata.__ompCudaRepairError) {
|
|
357
|
+
return "restore network access to nuget.org (or pre-populate the tiny side runtime) and rerun; CPU inference remained available";
|
|
358
|
+
}
|
|
359
|
+
if (missingFiles.length > 0) return "reinstall the tiny side runtime with ONNX Runtime postinstall enabled";
|
|
360
|
+
if (missingCudaLibrary(error)) {
|
|
361
|
+
return "install the matching CUDA/cuDNN shared libraries and expose them on the dynamic loader path";
|
|
362
|
+
}
|
|
363
|
+
if (cudaDeviceUnavailable(error)) {
|
|
364
|
+
return "make the NVIDIA GPU visible to this process/session, or use providers.tinyModelDevice=default/cpu";
|
|
365
|
+
}
|
|
366
|
+
return "check the host CUDA driver, device visibility, and ONNX Runtime CUDA compatibility";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function resolveOnnxRuntimePackageDir(metadata: TransformersRuntimeMetadata): string | null {
|
|
370
|
+
const entry = metadata.__ompTransformersEntry;
|
|
371
|
+
if (entry) {
|
|
372
|
+
try {
|
|
373
|
+
return path.dirname(createRequire(entry).resolve(`${ONNX_RUNTIME_NODE_PACKAGE}/package.json`));
|
|
374
|
+
} catch {
|
|
375
|
+
// Fall through to the side-runtime resolver below.
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const nodeModules = metadata.__ompRuntimeNodeModules;
|
|
379
|
+
if (!nodeModules) return null;
|
|
380
|
+
const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
|
|
381
|
+
return manifest ? path.dirname(manifest) : null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function formatOnnxRuntimeCudaDiagnostics(
|
|
385
|
+
metadata: TransformersRuntimeMetadata,
|
|
386
|
+
requestedDevice: string,
|
|
387
|
+
error: unknown,
|
|
388
|
+
): Promise<string | null> {
|
|
389
|
+
const device = requestedDevice.trim().toLowerCase();
|
|
390
|
+
if (device !== "cuda" && device !== "gpu" && device !== "auto") return null;
|
|
391
|
+
if (process.platform !== "linux" || process.arch !== "x64") return null;
|
|
392
|
+
const packageDir = resolveOnnxRuntimePackageDir(metadata);
|
|
393
|
+
if (!packageDir) {
|
|
394
|
+
return [
|
|
395
|
+
"ONNX Runtime CUDA diagnostics:",
|
|
396
|
+
` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
|
|
397
|
+
" cause: unable to resolve onnxruntime-node in the tiny-model runtime",
|
|
398
|
+
].join("\n");
|
|
399
|
+
}
|
|
400
|
+
const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
|
|
401
|
+
const missingFiles = await missingOnnxRuntimeCudaProviderFiles(binDir);
|
|
402
|
+
const sideRuntime = metadata.__ompRuntimeNodeModules;
|
|
403
|
+
const lines = [
|
|
404
|
+
"ONNX Runtime CUDA diagnostics:",
|
|
405
|
+
` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
|
|
406
|
+
sideRuntime ? ` side runtime: ${sideRuntime}` : ` onnxruntime-node: ${packageDir}`,
|
|
407
|
+
` cause: ${cudaFailureCause(metadata, error, missingFiles)}`,
|
|
408
|
+
];
|
|
409
|
+
lines.push(` hint: ${cudaFailureHint(metadata, error, missingFiles)}`);
|
|
410
|
+
return lines.join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
214
413
|
function configureTransformers<T extends ConfigurableTransformers>(transformers: T): T {
|
|
215
414
|
transformers.env.cacheDir = getTinyModelsCacheDir();
|
|
216
415
|
transformers.env.allowLocalModels = false;
|
|
@@ -251,7 +450,12 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
|
|
|
251
450
|
runtimeDir: () => string,
|
|
252
451
|
): Promise<T> {
|
|
253
452
|
return holder.load(async () => {
|
|
254
|
-
if (!isCompiledBinary())
|
|
453
|
+
if (!isCompiledBinary()) {
|
|
454
|
+
const entry = sourceRequire.resolve(TRANSFORMERS_PACKAGE);
|
|
455
|
+
return attachTransformersRuntimeMetadata(configureTransformers(sourceRequire(entry) as T), {
|
|
456
|
+
__ompTransformersEntry: entry,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
255
459
|
const installedDir = await ensureRuntimeInstalled({
|
|
256
460
|
runtimeDir: runtimeDir(),
|
|
257
461
|
install: {
|
|
@@ -270,8 +474,21 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
|
|
|
270
474
|
},
|
|
271
475
|
}),
|
|
272
476
|
});
|
|
477
|
+
let cudaRepairError: string | undefined;
|
|
478
|
+
try {
|
|
479
|
+
await ensureOnnxRuntimeCudaProviders(installedDir);
|
|
480
|
+
} catch (repairError) {
|
|
481
|
+
// Deferred failure: keep loading Transformers so `loadPipelineWithDeviceFallback`
|
|
482
|
+
// still gets its CUDA→CPU retry. The error is surfaced through the CUDA
|
|
483
|
+
// diagnostics attached to the runtime metadata.
|
|
484
|
+
cudaRepairError = errorMessage(repairError);
|
|
485
|
+
}
|
|
273
486
|
const entry = await prepareCompiledRuntime(installedDir, TRANSFORMERS_PACKAGE);
|
|
274
487
|
const require_ = createRequire(entry);
|
|
275
|
-
return configureTransformers(require_(entry) as T)
|
|
488
|
+
return attachTransformersRuntimeMetadata(configureTransformers(require_(entry) as T), {
|
|
489
|
+
__ompRuntimeNodeModules: path.join(installedDir, "node_modules"),
|
|
490
|
+
__ompTransformersEntry: entry,
|
|
491
|
+
__ompCudaRepairError: cudaRepairError,
|
|
492
|
+
});
|
|
276
493
|
});
|
|
277
494
|
}
|
package/src/task/worktree.ts
CHANGED
|
@@ -855,18 +855,40 @@ export async function mergeTaskBranches(
|
|
|
855
855
|
try {
|
|
856
856
|
const target = baseSha ? `${baseSha}..${branchName}` : branchName;
|
|
857
857
|
await git.cherryPick(repoRoot, target);
|
|
858
|
-
} catch (
|
|
858
|
+
} catch (initialErr) {
|
|
859
|
+
// Empty cherry-picks are not conflicts: a commit whose net
|
|
860
|
+
// effect is already on HEAD (redundant change, or 3-way
|
|
861
|
+
// merge auto-resolved to HEAD) leaves the sequencer stopped
|
|
862
|
+
// with a "The previous cherry-pick is now empty" message.
|
|
863
|
+
// Advance past every consecutive empty with `--skip` so the
|
|
864
|
+
// remaining non-redundant commits in the range still land.
|
|
865
|
+
// A genuine conflict (unmerged files, no "now empty"
|
|
866
|
+
// message) falls through to the abort path below.
|
|
867
|
+
let cursor: unknown = initialErr;
|
|
868
|
+
while (git.cherryPick.isEmptyError(cursor)) {
|
|
869
|
+
try {
|
|
870
|
+
await git.cherryPick.skip(repoRoot);
|
|
871
|
+
cursor = undefined;
|
|
872
|
+
break;
|
|
873
|
+
} catch (skipErr) {
|
|
874
|
+
cursor = skipErr;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (cursor === undefined) {
|
|
878
|
+
merged.push(branchName);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
859
881
|
try {
|
|
860
882
|
await git.cherryPick.abort(repoRoot);
|
|
861
883
|
} catch {
|
|
862
884
|
/* no state to abort */
|
|
863
885
|
}
|
|
864
886
|
const stderr =
|
|
865
|
-
|
|
866
|
-
?
|
|
867
|
-
:
|
|
868
|
-
?
|
|
869
|
-
: String(
|
|
887
|
+
cursor instanceof git.GitCommandError
|
|
888
|
+
? cursor.result.stderr.trim()
|
|
889
|
+
: cursor instanceof Error
|
|
890
|
+
? cursor.message
|
|
891
|
+
: String(cursor);
|
|
870
892
|
failed.push(branchName);
|
|
871
893
|
conflictResult = {
|
|
872
894
|
merged,
|