@oh-my-pi/pi-ai 6.7.670 → 6.8.0

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/src/storage.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Simple auth storage for CLI using SQLite.
3
+ * Compatible with coding-agent's agent.db format.
4
+ */
5
+
6
+ import { Database } from "bun:sqlite";
7
+ import { existsSync, mkdirSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import type { OAuthCredentials } from "./utils/oauth/types";
11
+
12
+ type AuthCredential = { type: "api_key"; key: string } | ({ type: "oauth" } & OAuthCredentials);
13
+
14
+ type AuthRow = {
15
+ id: number;
16
+ provider: string;
17
+ credential_type: string;
18
+ data: string;
19
+ created_at: number;
20
+ updated_at: number;
21
+ };
22
+
23
+ /**
24
+ * Get the agent config directory (e.g., ~/.omp/agent/)
25
+ */
26
+ function getAgentDir(): string {
27
+ const configDir = process.env.OMP_CODING_AGENT_DIR || join(homedir(), ".omp", "agent");
28
+ return configDir;
29
+ }
30
+
31
+ /**
32
+ * Get path to agent.db
33
+ */
34
+ function getAgentDbPath(): string {
35
+ return join(getAgentDir(), "agent.db");
36
+ }
37
+
38
+ function serializeCredential(credential: AuthCredential): { credentialType: string; data: string } | null {
39
+ if (credential.type === "api_key") {
40
+ return {
41
+ credentialType: "api_key",
42
+ data: JSON.stringify({ key: credential.key }),
43
+ };
44
+ }
45
+ if (credential.type === "oauth") {
46
+ const { type: _type, ...rest } = credential;
47
+ return {
48
+ credentialType: "oauth",
49
+ data: JSON.stringify(rest),
50
+ };
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function deserializeCredential(row: AuthRow): AuthCredential | null {
56
+ let parsed: unknown;
57
+ try {
58
+ parsed = JSON.parse(row.data);
59
+ } catch {
60
+ return null;
61
+ }
62
+ if (!parsed || typeof parsed !== "object") {
63
+ return null;
64
+ }
65
+
66
+ if (row.credential_type === "api_key") {
67
+ const data = parsed as Record<string, unknown>;
68
+ if (typeof data.key === "string") {
69
+ return { type: "api_key", key: data.key };
70
+ }
71
+ }
72
+
73
+ if (row.credential_type === "oauth") {
74
+ return { type: "oauth", ...(parsed as Record<string, unknown>) } as AuthCredential;
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Simple storage class for CLI auth credentials.
82
+ */
83
+ export class CliAuthStorage {
84
+ private db: Database;
85
+ private insertStmt: ReturnType<Database["prepare"]>;
86
+ private listByProviderStmt: ReturnType<Database["prepare"]>;
87
+ private listAllStmt: ReturnType<Database["prepare"]>;
88
+ private deleteByProviderStmt: ReturnType<Database["prepare"]>;
89
+
90
+ constructor(dbPath: string = getAgentDbPath()) {
91
+ // Ensure directory exists
92
+ const dir = dirname(dbPath);
93
+ if (!existsSync(dir)) {
94
+ mkdirSync(dir, { recursive: true });
95
+ }
96
+
97
+ this.db = new Database(dbPath);
98
+ this.initializeSchema();
99
+
100
+ this.insertStmt = this.db.prepare(
101
+ "INSERT INTO auth_credentials (provider, credential_type, data) VALUES (?, ?, ?) RETURNING id",
102
+ );
103
+ this.listByProviderStmt = this.db.prepare("SELECT * FROM auth_credentials WHERE provider = ?");
104
+ this.listAllStmt = this.db.prepare("SELECT * FROM auth_credentials");
105
+ this.deleteByProviderStmt = this.db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
106
+ }
107
+
108
+ private initializeSchema(): void {
109
+ this.db.exec(`
110
+ PRAGMA journal_mode=WAL;
111
+ PRAGMA synchronous=NORMAL;
112
+ PRAGMA busy_timeout=5000;
113
+
114
+ CREATE TABLE IF NOT EXISTS auth_credentials (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ provider TEXT NOT NULL,
117
+ credential_type TEXT NOT NULL,
118
+ data TEXT NOT NULL,
119
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
120
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
121
+ );
122
+ CREATE INDEX IF NOT EXISTS idx_auth_provider ON auth_credentials(provider);
123
+ `);
124
+ }
125
+
126
+ /**
127
+ * Save OAuth credentials for a provider (replaces existing).
128
+ */
129
+ saveOAuth(provider: string, credentials: OAuthCredentials): void {
130
+ const credential: AuthCredential = { type: "oauth", ...credentials };
131
+ this.replaceForProvider(provider, credential);
132
+ }
133
+
134
+ /**
135
+ * Get OAuth credentials for a provider.
136
+ */
137
+ getOAuth(provider: string): OAuthCredentials | null {
138
+ const rows = this.listByProviderStmt.all(provider) as AuthRow[];
139
+ for (const row of rows) {
140
+ const credential = deserializeCredential(row);
141
+ if (credential && credential.type === "oauth") {
142
+ const { type: _type, ...oauth } = credential;
143
+ return oauth as OAuthCredentials;
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * List all providers with credentials.
151
+ */
152
+ listProviders(): string[] {
153
+ const rows = this.listAllStmt.all() as AuthRow[];
154
+ const providers = new Set<string>();
155
+ for (const row of rows) {
156
+ providers.add(row.provider);
157
+ }
158
+ return Array.from(providers);
159
+ }
160
+
161
+ /**
162
+ * Delete all credentials for a provider.
163
+ */
164
+ deleteProvider(provider: string): void {
165
+ this.deleteByProviderStmt.run(provider);
166
+ }
167
+
168
+ /**
169
+ * Replace all credentials for a provider with a single credential.
170
+ */
171
+ private replaceForProvider(provider: string, credential: AuthCredential): void {
172
+ const serialized = serializeCredential(credential);
173
+ if (!serialized) return;
174
+
175
+ const replace = this.db.transaction(() => {
176
+ this.deleteByProviderStmt.run(provider);
177
+ this.insertStmt.run(provider, serialized.credentialType, serialized.data);
178
+ });
179
+ replace();
180
+ }
181
+
182
+ close(): void {
183
+ this.db.close();
184
+ }
185
+ }
@@ -12,9 +12,9 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
12
12
  private isComplete: (event: T) => boolean,
13
13
  private extractResult: (event: T) => R,
14
14
  ) {
15
- this.finalResultPromise = new Promise((resolve) => {
16
- this.resolveFinalResult = resolve;
17
- });
15
+ const { promise, resolve } = Promise.withResolvers<R>();
16
+ this.finalResultPromise = promise;
17
+ this.resolveFinalResult = resolve;
18
18
  }
19
19
 
20
20
  push(event: T): void {
@@ -2,120 +2,91 @@
2
2
  * Anthropic OAuth flow (Claude Pro/Max)
3
3
  */
4
4
 
5
+ import { OAuthCallbackFlow } from "./callback-server";
5
6
  import { generatePKCE } from "./pkce";
6
- import type { OAuthCredentials } from "./types";
7
+ import type { OAuthController, OAuthCredentials } from "./types";
7
8
 
8
9
  const decode = (s: string) => atob(s);
9
10
  const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
10
11
  const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
11
12
  const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
12
- const REDIRECT_URI = "http://localhost:54545/callback";
13
+ const CALLBACK_PORT = 54545;
14
+ const CALLBACK_PATH = "/callback";
13
15
  const SCOPES = "org:create_api_key user:profile user:inference";
14
16
 
15
- function generateState(): string {
16
- const bytes = new Uint8Array(16);
17
- crypto.getRandomValues(bytes);
18
- return Array.from(bytes)
19
- .map((value) => value.toString(16).padStart(2, "0"))
20
- .join("");
21
- }
17
+ class AnthropicOAuthFlow extends OAuthCallbackFlow {
18
+ private verifier: string = "";
19
+ private challenge: string = "";
22
20
 
23
- function parseAuthCode(input: string): { code: string; state?: string } {
24
- const trimmed = input.trim();
25
- if (!trimmed) return { code: "" };
26
-
27
- try {
28
- const url = new URL(trimmed);
29
- const code = url.searchParams.get("code") ?? "";
30
- const state = url.searchParams.get("state") ?? undefined;
31
- if (code) return { code, state };
32
- } catch {
33
- // Ignore invalid URL parsing and fall back to manual parsing.
21
+ constructor(ctrl: OAuthController) {
22
+ super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
34
23
  }
35
24
 
36
- if (trimmed.includes("code=")) {
37
- const params = new URLSearchParams(trimmed.replace(/^[?#]/, ""));
38
- const code = params.get("code") ?? "";
39
- const state = params.get("state") ?? undefined;
40
- if (code) return { code, state };
25
+ protected async generateAuthUrl(
26
+ state: string,
27
+ redirectUri: string,
28
+ ): Promise<{ url: string; instructions?: string }> {
29
+ const pkce = await generatePKCE();
30
+ this.verifier = pkce.verifier;
31
+ this.challenge = pkce.challenge;
32
+
33
+ const authParams = new URLSearchParams({
34
+ code: "true",
35
+ client_id: CLIENT_ID,
36
+ response_type: "code",
37
+ redirect_uri: redirectUri,
38
+ scope: SCOPES,
39
+ code_challenge: this.challenge,
40
+ code_challenge_method: "S256",
41
+ state,
42
+ });
43
+
44
+ const url = `${AUTHORIZE_URL}?${authParams.toString()}`;
45
+ return { url };
41
46
  }
42
47
 
43
- const [code, state] = trimmed.split("#");
44
- return { code, state };
48
+ protected async exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials> {
49
+ const tokenResponse = await fetch(TOKEN_URL, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ Accept: "application/json",
54
+ },
55
+ body: JSON.stringify({
56
+ grant_type: "authorization_code",
57
+ client_id: CLIENT_ID,
58
+ code,
59
+ state,
60
+ redirect_uri: redirectUri,
61
+ code_verifier: this.verifier,
62
+ }),
63
+ });
64
+
65
+ if (!tokenResponse.ok) {
66
+ const error = await tokenResponse.text();
67
+ throw new Error(`Token exchange failed: ${error}`);
68
+ }
69
+
70
+ const tokenData = (await tokenResponse.json()) as {
71
+ access_token: string;
72
+ refresh_token: string;
73
+ expires_in: number;
74
+ };
75
+
76
+ return {
77
+ refresh: tokenData.refresh_token,
78
+ access: tokenData.access_token,
79
+ expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
80
+ };
81
+ }
45
82
  }
46
83
 
47
84
  /**
48
- * Login with Anthropic OAuth (device code flow)
49
- *
50
- * @param onAuthUrl - Callback to handle the authorization URL (e.g., open browser)
51
- * @param onPromptCode - Callback to prompt user for the authorization code
85
+ * Login with Anthropic OAuth
52
86
  */
53
- export async function loginAnthropic(
54
- onAuthUrl: (url: string) => void,
55
- onPromptCode: () => Promise<string>,
56
- ): Promise<OAuthCredentials> {
57
- const { verifier, challenge } = await generatePKCE();
58
- const state = generateState();
59
-
60
- // Build authorization URL
61
- const authParams = new URLSearchParams({
62
- code: "true",
63
- client_id: CLIENT_ID,
64
- response_type: "code",
65
- redirect_uri: REDIRECT_URI,
66
- scope: SCOPES,
67
- code_challenge: challenge,
68
- code_challenge_method: "S256",
69
- state,
70
- });
71
-
72
- const authUrl = `${AUTHORIZE_URL}?${authParams.toString()}`;
73
-
74
- // Notify caller with URL to open
75
- onAuthUrl(authUrl);
76
-
77
- // Wait for user to paste authorization code (format: code#state)
78
- const authCode = await onPromptCode();
79
- const { code, state: parsedState } = parseAuthCode(authCode);
80
- const requestState = parsedState ?? state;
81
-
82
- // Exchange code for tokens
83
- const tokenResponse = await fetch(TOKEN_URL, {
84
- method: "POST",
85
- headers: {
86
- "Content-Type": "application/json",
87
- Accept: "application/json",
88
- },
89
- body: JSON.stringify({
90
- grant_type: "authorization_code",
91
- client_id: CLIENT_ID,
92
- code,
93
- state: requestState,
94
- redirect_uri: REDIRECT_URI,
95
- code_verifier: verifier,
96
- }),
97
- });
98
-
99
- if (!tokenResponse.ok) {
100
- const error = await tokenResponse.text();
101
- throw new Error(`Token exchange failed: ${error}`);
102
- }
103
-
104
- const tokenData = (await tokenResponse.json()) as {
105
- access_token: string;
106
- refresh_token: string;
107
- expires_in: number;
108
- };
109
-
110
- // Calculate expiry time (current time + expires_in seconds - 5 min buffer)
111
- const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
112
-
113
- // Save credentials
114
- return {
115
- refresh: tokenData.refresh_token,
116
- access: tokenData.access_token,
117
- expires: expiresAt,
118
- };
87
+ export async function loginAnthropic(ctrl: OAuthController): Promise<OAuthCredentials> {
88
+ const flow = new AnthropicOAuthFlow(ctrl);
89
+ return flow.login();
119
90
  }
120
91
 
121
92
  /**
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Abstract base class for OAuth flows with local callback servers.
3
+ *
4
+ * Handles:
5
+ * - Port allocation (tries expected port, falls back to random)
6
+ * - Callback server setup and request handling
7
+ * - Common OAuth flow logic
8
+ *
9
+ * Providers extend this and implement:
10
+ * - generateAuthUrl(): Build provider-specific authorization URL
11
+ * - exchangeToken(): Exchange authorization code for tokens
12
+ */
13
+
14
+ import templateHtml from "./oauth.html" with { type: "text" };
15
+ import type { OAuthController, OAuthCredentials } from "./types";
16
+
17
+ const DEFAULT_TIMEOUT = 120;
18
+ const DEFAULT_HOSTNAME = "localhost";
19
+ const CALLBACK_PATH = "/callback";
20
+
21
+ export type CallbackResult = { code: string; state: string };
22
+
23
+ /**
24
+ * Abstract base class for OAuth flows with local callback servers.
25
+ */
26
+ export abstract class OAuthCallbackFlow {
27
+ protected ctrl: OAuthController;
28
+ protected preferredPort: number;
29
+ protected callbackPath: string;
30
+ private callbackResolve?: (result: CallbackResult) => void;
31
+ private callbackReject?: (error: string) => void;
32
+
33
+ constructor(ctrl: OAuthController, preferredPort: number, callbackPath: string = CALLBACK_PATH) {
34
+ this.ctrl = ctrl;
35
+ this.preferredPort = preferredPort;
36
+ this.callbackPath = callbackPath;
37
+ }
38
+
39
+ /**
40
+ * Generate provider-specific authorization URL.
41
+ * @param state - CSRF state token
42
+ * @param redirectUri - The actual redirect URI to use (may differ from expected if port fallback occurred)
43
+ * @returns Authorization URL and optional instructions
44
+ */
45
+ protected abstract generateAuthUrl(
46
+ state: string,
47
+ redirectUri: string,
48
+ ): Promise<{ url: string; instructions?: string }>;
49
+
50
+ /**
51
+ * Exchange authorization code for OAuth tokens.
52
+ * @param code - Authorization code from callback
53
+ * @param state - CSRF state token
54
+ * @param redirectUri - The actual redirect URI used (must match authorization request)
55
+ * @returns OAuth credentials
56
+ */
57
+ protected abstract exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
58
+
59
+ /**
60
+ * Generate CSRF state token. Override if provider needs custom state generation.
61
+ */
62
+ protected generateState(): string {
63
+ const bytes = new Uint8Array(16);
64
+ crypto.getRandomValues(bytes);
65
+ return Array.from(bytes)
66
+ .map((value) => value.toString(16).padStart(2, "0"))
67
+ .join("");
68
+ }
69
+
70
+ /**
71
+ * Execute the OAuth login flow.
72
+ */
73
+ async login(): Promise<OAuthCredentials> {
74
+ const state = this.generateState();
75
+
76
+ // Start callback server first to get actual redirect URI
77
+ const { server, redirectUri } = await this.startCallbackServer(state);
78
+
79
+ try {
80
+ // Generate auth URL with the ACTUAL redirect URI (may differ from expected if port was busy)
81
+ const { url: authUrl, instructions } = await this.generateAuthUrl(state, redirectUri);
82
+
83
+ // Notify controller that auth is ready
84
+ this.ctrl.onAuth?.({ url: authUrl, instructions });
85
+ this.ctrl.onProgress?.("Waiting for browser authentication...");
86
+
87
+ // Wait for callback or manual input
88
+ const { code } = await this.waitForCallback(state);
89
+
90
+ this.ctrl.onProgress?.("Exchanging authorization code for tokens...");
91
+
92
+ return await this.exchangeToken(code, state, redirectUri);
93
+ } finally {
94
+ server.stop();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Start callback server, trying preferred port first, falling back to random.
100
+ */
101
+ private async startCallbackServer(
102
+ expectedState: string,
103
+ ): Promise<{ server: Bun.Server<unknown>; redirectUri: string }> {
104
+ // Try preferred port first
105
+ try {
106
+ const redirectUri = `http://${DEFAULT_HOSTNAME}:${this.preferredPort}${this.callbackPath}`;
107
+ const server = this.createServer(this.preferredPort, expectedState);
108
+ return { server, redirectUri };
109
+ } catch {
110
+ // Port busy or unavailable, try random port
111
+ const randomPort = 0; // Let OS assign
112
+ const server = this.createServer(randomPort, expectedState);
113
+ const actualPort = server.port;
114
+ const redirectUri = `http://${DEFAULT_HOSTNAME}:${actualPort}${this.callbackPath}`;
115
+ this.ctrl.onProgress?.(`Preferred port ${this.preferredPort} unavailable, using port ${actualPort}`);
116
+ return { server, redirectUri };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Create HTTP server for OAuth callback.
122
+ */
123
+ private createServer(port: number, expectedState: string): Bun.Server<unknown> {
124
+ return Bun.serve({
125
+ hostname: DEFAULT_HOSTNAME,
126
+ port,
127
+ reusePort: false,
128
+ fetch: (req) => this.handleCallback(req, expectedState),
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Handle OAuth callback HTTP request.
134
+ */
135
+ private handleCallback(req: Request, expectedState: string): Response {
136
+ const url = new URL(req.url);
137
+
138
+ if (url.pathname !== this.callbackPath) {
139
+ return new Response("Not Found", { status: 404 });
140
+ }
141
+
142
+ const code = url.searchParams.get("code");
143
+ const state = url.searchParams.get("state") || "";
144
+ const error = url.searchParams.get("error") || "";
145
+ const errorDescription = url.searchParams.get("error_description") || error;
146
+
147
+ type OkState = { ok: true; code: string; state: string };
148
+ type ErrorState = { ok?: false; error?: string };
149
+ let resultState: OkState | ErrorState;
150
+
151
+ if (error) {
152
+ resultState = { ok: false, error: `Authorization failed: ${errorDescription}` };
153
+ } else if (!code) {
154
+ resultState = { ok: false, error: "Missing authorization code" };
155
+ } else if (expectedState && state !== expectedState) {
156
+ resultState = { ok: false, error: "State mismatch - possible CSRF attack" };
157
+ } else {
158
+ resultState = { ok: true, code, state };
159
+ }
160
+
161
+ // Signal to waitForCallback
162
+ queueMicrotask(() => {
163
+ if (resultState.ok) {
164
+ this.callbackResolve?.({ code: resultState.code, state: resultState.state });
165
+ } else {
166
+ this.callbackReject?.(resultState.error ?? "Unknown error");
167
+ }
168
+ });
169
+
170
+ return new Response(
171
+ (templateHtml as unknown as string).replaceAll("__OAUTH_STATE__", JSON.stringify(resultState)),
172
+ {
173
+ status: resultState.ok ? 200 : 500,
174
+ headers: { "Content-Type": "text/html" },
175
+ },
176
+ );
177
+ }
178
+
179
+ /**
180
+ * Wait for OAuth callback or manual input (whichever comes first).
181
+ */
182
+ private waitForCallback(expectedState: string): Promise<CallbackResult> {
183
+ const timeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT * 1000);
184
+ const signal = this.ctrl.signal ? AbortSignal.any([this.ctrl.signal, timeoutSignal]) : timeoutSignal;
185
+
186
+ const callbackPromise = new Promise<CallbackResult>((resolve, reject) => {
187
+ this.callbackResolve = resolve;
188
+ this.callbackReject = reject;
189
+
190
+ signal.addEventListener("abort", () => {
191
+ this.callbackResolve = undefined;
192
+ this.callbackReject = undefined;
193
+ reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
194
+ });
195
+ });
196
+
197
+ // Manual input race (if supported)
198
+ if (this.ctrl.onManualCodeInput) {
199
+ const manualPromise = this.ctrl.onManualCodeInput().then((input): CallbackResult => {
200
+ const parsed = parseCallbackInput(input);
201
+ if (!parsed.code) {
202
+ throw new Error("No authorization code found in input");
203
+ }
204
+ if (expectedState && parsed.state && parsed.state !== expectedState) {
205
+ throw new Error("State mismatch - possible CSRF attack");
206
+ }
207
+ return { code: parsed.code, state: parsed.state ?? "" };
208
+ });
209
+
210
+ return Promise.race([callbackPromise, manualPromise]);
211
+ }
212
+
213
+ return callbackPromise;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Parse a redirect URL or code string to extract code and state.
219
+ */
220
+ export function parseCallbackInput(input: string): { code?: string; state?: string } {
221
+ const value = input.trim();
222
+ if (!value) return {};
223
+
224
+ try {
225
+ const url = new URL(value);
226
+ return {
227
+ code: url.searchParams.get("code") ?? undefined,
228
+ state: url.searchParams.get("state") ?? undefined,
229
+ };
230
+ } catch {
231
+ // Not a URL - check for query string format
232
+ }
233
+
234
+ if (value.includes("code=")) {
235
+ const params = new URLSearchParams(value.replace(/^[?#]/, ""));
236
+ return {
237
+ code: params.get("code") ?? undefined,
238
+ state: params.get("state") ?? undefined,
239
+ };
240
+ }
241
+
242
+ // Assume raw code, possibly with state after #
243
+ const [code, state] = value.split("#", 2);
244
+ return { code, state };
245
+ }
@@ -10,10 +10,6 @@ const POLL_BASE_DELAY = 1000;
10
10
  const POLL_MAX_DELAY = 10000;
11
11
  const POLL_BACKOFF_MULTIPLIER = 1.2;
12
12
 
13
- function sleep(ms: number): Promise<void> {
14
- return new Promise((resolve) => setTimeout(resolve, ms));
15
- }
16
-
17
13
  export interface CursorAuthParams {
18
14
  verifier: string;
19
15
  challenge: string;
@@ -45,7 +41,7 @@ export async function pollCursorAuth(
45
41
  let consecutiveErrors = 0;
46
42
 
47
43
  for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
48
- await sleep(delay);
44
+ await Bun.sleep(delay);
49
45
 
50
46
  try {
51
47
  const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
@@ -2,6 +2,7 @@
2
2
  * GitHub Copilot OAuth flow
3
3
  */
4
4
 
5
+ import { abortableSleep } from "@oh-my-pi/pi-utils";
5
6
  import { getModels } from "../../models";
6
7
  import type { OAuthCredentials } from "./types";
7
8
 
@@ -136,29 +137,6 @@ async function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {
136
137
  };
137
138
  }
138
139
 
139
- /**
140
- * Sleep that can be interrupted by an AbortSignal
141
- */
142
- function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
143
- return new Promise((resolve, reject) => {
144
- if (signal?.aborted) {
145
- reject(new Error("Login cancelled"));
146
- return;
147
- }
148
-
149
- const timeout = setTimeout(resolve, ms);
150
-
151
- signal?.addEventListener(
152
- "abort",
153
- () => {
154
- clearTimeout(timeout);
155
- reject(new Error("Login cancelled"));
156
- },
157
- { once: true },
158
- );
159
- });
160
- }
161
-
162
140
  async function pollForGitHubAccessToken(
163
141
  domain: string,
164
142
  deviceCode: string,