@oh-my-pi/pi-ai 6.8.0 → 6.8.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.1",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "bun test"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@oh-my-pi/pi-utils": "6.8.
|
|
20
|
+
"@oh-my-pi/pi-utils": "6.8.1",
|
|
21
21
|
"@anthropic-ai/sdk": "0.71.2",
|
|
22
22
|
"@aws-sdk/client-bedrock-runtime": "^3.968.0",
|
|
23
23
|
"@bufbuild/protobuf": "^2.10.2",
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createInterface } from "readline";
|
|
|
3
3
|
import { CliAuthStorage } from "./storage";
|
|
4
4
|
import "./utils/migrate-env";
|
|
5
5
|
import { loginAnthropic } from "./utils/oauth/anthropic";
|
|
6
|
+
import { loginCursor } from "./utils/oauth/cursor";
|
|
6
7
|
import { loginGitHubCopilot } from "./utils/oauth/github-copilot";
|
|
7
8
|
import { loginAntigravity } from "./utils/oauth/google-antigravity";
|
|
8
9
|
import { loginGeminiCli } from "./utils/oauth/google-gemini-cli";
|
|
@@ -88,6 +89,17 @@ async function login(provider: OAuthProvider): Promise<void> {
|
|
|
88
89
|
});
|
|
89
90
|
break;
|
|
90
91
|
|
|
92
|
+
case "cursor":
|
|
93
|
+
credentials = await loginCursor(
|
|
94
|
+
(url) => {
|
|
95
|
+
console.log(`\nOpen this URL in your browser:\n${url}\n`);
|
|
96
|
+
},
|
|
97
|
+
() => {
|
|
98
|
+
console.log("Waiting for browser authentication...");
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
break;
|
|
102
|
+
|
|
91
103
|
default:
|
|
92
104
|
throw new Error(`Unknown provider: ${provider}`);
|
|
93
105
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
+
import { abortableSleep } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import type {
|
|
3
4
|
ResponseFunctionToolCall,
|
|
4
5
|
ResponseInput,
|
|
@@ -440,13 +441,13 @@ async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSign
|
|
|
440
441
|
}
|
|
441
442
|
if (signal?.aborted) return response;
|
|
442
443
|
const delay = getRetryDelayMs(response, attempt);
|
|
443
|
-
await
|
|
444
|
+
await abortableSleep(delay, signal);
|
|
444
445
|
} catch (error) {
|
|
445
446
|
if (attempt >= CODEX_MAX_RETRIES || signal?.aborted) {
|
|
446
447
|
throw error;
|
|
447
448
|
}
|
|
448
449
|
const delay = CODEX_RETRY_DELAY_MS * (attempt + 1);
|
|
449
|
-
await
|
|
450
|
+
await abortableSleep(delay, signal);
|
|
450
451
|
}
|
|
451
452
|
attempt += 1;
|
|
452
453
|
}
|
|
@@ -63,7 +63,12 @@ class AnthropicOAuthFlow extends OAuthCallbackFlow {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
if (!tokenResponse.ok) {
|
|
66
|
-
|
|
66
|
+
let error: string;
|
|
67
|
+
try {
|
|
68
|
+
error = await tokenResponse.text();
|
|
69
|
+
} catch {
|
|
70
|
+
error = `HTTP ${tokenResponse.status}`;
|
|
71
|
+
}
|
|
67
72
|
throw new Error(`Token exchange failed: ${error}`);
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -158,12 +158,14 @@ export abstract class OAuthCallbackFlow {
|
|
|
158
158
|
resultState = { ok: true, code, state };
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// Signal to waitForCallback
|
|
161
|
+
// Signal to waitForCallback - capture refs before they could be cleared
|
|
162
|
+
const resolve = this.callbackResolve;
|
|
163
|
+
const reject = this.callbackReject;
|
|
162
164
|
queueMicrotask(() => {
|
|
163
165
|
if (resultState.ok) {
|
|
164
|
-
|
|
166
|
+
resolve?.({ code: resultState.code, state: resultState.state });
|
|
165
167
|
} else {
|
|
166
|
-
|
|
168
|
+
reject?.(resultState.error ?? "Unknown error");
|
|
167
169
|
}
|
|
168
170
|
});
|
|
169
171
|
|
|
@@ -126,12 +126,16 @@ export async function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<
|
|
|
126
126
|
|
|
127
127
|
function getTokenExpiry(token: string): number {
|
|
128
128
|
try {
|
|
129
|
-
const
|
|
129
|
+
const parts = token.split(".");
|
|
130
|
+
if (parts.length !== 3) {
|
|
131
|
+
return Date.now() + 3600 * 1000;
|
|
132
|
+
}
|
|
133
|
+
const payload = parts[1];
|
|
130
134
|
if (!payload) {
|
|
131
135
|
return Date.now() + 3600 * 1000;
|
|
132
136
|
}
|
|
133
137
|
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
134
|
-
if (decoded.exp) {
|
|
138
|
+
if (decoded && typeof decoded === "object" && typeof decoded.exp === "number") {
|
|
135
139
|
return decoded.exp * 1000 - 5 * 60 * 1000;
|
|
136
140
|
}
|
|
137
141
|
} catch {
|
|
@@ -40,11 +40,16 @@ function getAccountId(accessToken: string): string | null {
|
|
|
40
40
|
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
interface PKCE {
|
|
44
|
+
verifier: string;
|
|
45
|
+
challenge: string;
|
|
46
|
+
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
49
|
+
constructor(
|
|
50
|
+
ctrl: OAuthController,
|
|
51
|
+
private readonly pkce: PKCE,
|
|
52
|
+
) {
|
|
48
53
|
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -52,16 +57,12 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
|
52
57
|
state: string,
|
|
53
58
|
redirectUri: string,
|
|
54
59
|
): Promise<{ url: string; instructions?: string }> {
|
|
55
|
-
const pkce = await generatePKCE();
|
|
56
|
-
this.verifier = pkce.verifier;
|
|
57
|
-
this.challenge = pkce.challenge;
|
|
58
|
-
|
|
59
60
|
const searchParams = new URLSearchParams({
|
|
60
61
|
response_type: "code",
|
|
61
62
|
client_id: CLIENT_ID,
|
|
62
63
|
redirect_uri: redirectUri,
|
|
63
64
|
scope: SCOPE,
|
|
64
|
-
code_challenge: this.challenge,
|
|
65
|
+
code_challenge: this.pkce.challenge,
|
|
65
66
|
code_challenge_method: "S256",
|
|
66
67
|
state,
|
|
67
68
|
id_token_add_organizations: "true",
|
|
@@ -74,56 +75,61 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
protected async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
body: new URLSearchParams({
|
|
81
|
-
grant_type: "authorization_code",
|
|
82
|
-
client_id: CLIENT_ID,
|
|
83
|
-
code,
|
|
84
|
-
code_verifier: this.verifier,
|
|
85
|
-
redirect_uri: redirectUri,
|
|
86
|
-
}),
|
|
87
|
-
});
|
|
78
|
+
return exchangeCodeForToken(code, this.pkce.verifier, redirectUri);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
88
81
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
async function exchangeCodeForToken(code: string, verifier: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
83
|
+
const tokenResponse = await fetch(TOKEN_URL, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
86
|
+
body: new URLSearchParams({
|
|
87
|
+
grant_type: "authorization_code",
|
|
88
|
+
client_id: CLIENT_ID,
|
|
89
|
+
code,
|
|
90
|
+
code_verifier: verifier,
|
|
91
|
+
redirect_uri: redirectUri,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expires_in?: number;
|
|
97
|
-
};
|
|
95
|
+
if (!tokenResponse.ok) {
|
|
96
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
97
|
+
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
100
|
+
access_token?: string;
|
|
101
|
+
refresh_token?: string;
|
|
102
|
+
expires_in?: number;
|
|
103
|
+
};
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
105
|
+
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
|
|
106
|
+
throw new Error("Token response missing required fields");
|
|
107
|
+
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expires: Date.now() + tokenData.expires_in * 1000,
|
|
112
|
-
accountId,
|
|
113
|
-
};
|
|
109
|
+
const accountId = getAccountId(tokenData.access_token);
|
|
110
|
+
if (!accountId) {
|
|
111
|
+
throw new Error("Failed to extract accountId from token");
|
|
114
112
|
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
access: tokenData.access_token,
|
|
116
|
+
refresh: tokenData.refresh_token,
|
|
117
|
+
expires: Date.now() + tokenData.expires_in * 1000,
|
|
118
|
+
accountId,
|
|
119
|
+
};
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
118
123
|
* Login with OpenAI Codex OAuth
|
|
119
124
|
*/
|
|
120
125
|
export async function loginOpenAICodex(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
121
|
-
const
|
|
126
|
+
const pkce = await generatePKCE();
|
|
127
|
+
const flow = new OpenAICodexOAuthFlow(ctrl, pkce);
|
|
128
|
+
const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
122
129
|
|
|
123
130
|
try {
|
|
124
131
|
return await flow.login();
|
|
125
132
|
} catch (error) {
|
|
126
|
-
// Callback failed - fall back to onPrompt if available
|
|
127
133
|
if (!ctrl.onPrompt) {
|
|
128
134
|
throw error;
|
|
129
135
|
}
|
|
@@ -139,47 +145,7 @@ export async function loginOpenAICodex(ctrl: OAuthController): Promise<OAuthCred
|
|
|
139
145
|
throw new Error("No authorization code found in input");
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Manual token exchange
|
|
145
|
-
const pkce = await generatePKCE();
|
|
146
|
-
const tokenResponse = await fetch(TOKEN_URL, {
|
|
147
|
-
method: "POST",
|
|
148
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
149
|
-
body: new URLSearchParams({
|
|
150
|
-
grant_type: "authorization_code",
|
|
151
|
-
client_id: CLIENT_ID,
|
|
152
|
-
code: parsed.code,
|
|
153
|
-
code_verifier: pkce.verifier,
|
|
154
|
-
redirect_uri: redirectUri,
|
|
155
|
-
}),
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (!tokenResponse.ok) {
|
|
159
|
-
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const tokenData = (await tokenResponse.json()) as {
|
|
163
|
-
access_token?: string;
|
|
164
|
-
refresh_token?: string;
|
|
165
|
-
expires_in?: number;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
|
|
169
|
-
throw new Error("Token response missing required fields");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const accountId = getAccountId(tokenData.access_token);
|
|
173
|
-
if (!accountId) {
|
|
174
|
-
throw new Error("Failed to extract accountId from token");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
access: tokenData.access_token,
|
|
179
|
-
refresh: tokenData.refresh_token,
|
|
180
|
-
expires: Date.now() + tokenData.expires_in * 1000,
|
|
181
|
-
accountId,
|
|
182
|
-
};
|
|
148
|
+
return exchangeCodeForToken(parsed.code, pkce.verifier, redirectUri);
|
|
183
149
|
}
|
|
184
150
|
}
|
|
185
151
|
|