@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.0",
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.0",
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 Bun.sleep(delay);
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 Bun.sleep(delay);
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
- const error = await tokenResponse.text();
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
- this.callbackResolve?.({ code: resultState.code, state: resultState.state });
166
+ resolve?.({ code: resultState.code, state: resultState.state });
165
167
  } else {
166
- this.callbackReject?.(resultState.error ?? "Unknown error");
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 [, payload] = token.split(".");
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
- class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
44
- private verifier: string = "";
45
- private challenge: string = "";
43
+ interface PKCE {
44
+ verifier: string;
45
+ challenge: string;
46
+ }
46
47
 
47
- constructor(ctrl: OAuthController) {
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
- const tokenResponse = await fetch(TOKEN_URL, {
78
- method: "POST",
79
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
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
- if (!tokenResponse.ok) {
90
- throw new Error(`Token exchange failed: ${tokenResponse.status}`);
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
- const tokenData = (await tokenResponse.json()) as {
94
- access_token?: string;
95
- refresh_token?: string;
96
- expires_in?: number;
97
- };
95
+ if (!tokenResponse.ok) {
96
+ throw new Error(`Token exchange failed: ${tokenResponse.status}`);
97
+ }
98
98
 
99
- if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
100
- throw new Error("Token response missing required fields");
101
- }
99
+ const tokenData = (await tokenResponse.json()) as {
100
+ access_token?: string;
101
+ refresh_token?: string;
102
+ expires_in?: number;
103
+ };
102
104
 
103
- const accountId = getAccountId(tokenData.access_token);
104
- if (!accountId) {
105
- throw new Error("Failed to extract accountId from token");
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
- return {
109
- access: tokenData.access_token,
110
- refresh: tokenData.refresh_token,
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 flow = new OpenAICodexOAuthFlow(ctrl);
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
- const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
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