@oh-my-pi/pi-ai 3.20.0 → 3.34.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.
@@ -2,6 +2,8 @@
2
2
  * OpenAI Codex (ChatGPT OAuth) flow
3
3
  */
4
4
 
5
+ import { randomBytes } from "node:crypto";
6
+ import http from "node:http";
5
7
  import { generatePKCE } from "./pkce";
6
8
  import type { OAuthCredentials, OAuthPrompt } from "./types";
7
9
 
@@ -36,9 +38,7 @@ type JwtPayload = {
36
38
  };
37
39
 
38
40
  function createState(): string {
39
- const bytes = new Uint8Array(16);
40
- crypto.getRandomValues(bytes);
41
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
41
+ return randomBytes(16).toString("hex");
42
42
  }
43
43
 
44
44
  function parseAuthorizationInput(input: string): { code?: string; state?: string } {
@@ -187,64 +187,79 @@ async function createAuthorizationFlow(): Promise<{ verifier: string; state: str
187
187
 
188
188
  type OAuthServerInfo = {
189
189
  close: () => void;
190
+ cancelWait: () => void;
190
191
  waitForCode: () => Promise<{ code: string } | null>;
191
192
  };
192
193
 
193
194
  function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
194
195
  let lastCode: string | null = null;
196
+ let cancelled = false;
197
+ const server = http.createServer((req, res) => {
198
+ try {
199
+ const url = new URL(req.url || "", "http://localhost");
200
+ if (url.pathname !== "/auth/callback") {
201
+ res.statusCode = 404;
202
+ res.end("Not found");
203
+ return;
204
+ }
205
+ if (url.searchParams.get("state") !== state) {
206
+ res.statusCode = 400;
207
+ res.end("State mismatch");
208
+ return;
209
+ }
210
+ const code = url.searchParams.get("code");
211
+ if (!code) {
212
+ res.statusCode = 400;
213
+ res.end("Missing authorization code");
214
+ return;
215
+ }
216
+ res.statusCode = 200;
217
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
218
+ res.end(SUCCESS_HTML);
219
+ lastCode = code;
220
+ } catch {
221
+ res.statusCode = 500;
222
+ res.end("Internal error");
223
+ }
224
+ });
195
225
 
196
226
  return new Promise((resolve) => {
197
- try {
198
- const server = Bun.serve({
199
- port: 1455,
200
- hostname: "127.0.0.1",
201
- fetch(req) {
202
- try {
203
- const url = new URL(req.url);
204
- if (url.pathname !== "/auth/callback") {
205
- return new Response("Not found", { status: 404 });
206
- }
207
- if (url.searchParams.get("state") !== state) {
208
- return new Response("State mismatch", { status: 400 });
227
+ server
228
+ .listen(1455, "127.0.0.1", () => {
229
+ resolve({
230
+ close: () => server.close(),
231
+ cancelWait: () => {
232
+ cancelled = true;
233
+ },
234
+ waitForCode: async () => {
235
+ const sleep = () => new Promise((r) => setTimeout(r, 100));
236
+ for (let i = 0; i < 600; i += 1) {
237
+ if (lastCode) return { code: lastCode };
238
+ if (cancelled) return null;
239
+ await sleep();
209
240
  }
210
- const code = url.searchParams.get("code");
211
- if (!code) {
212
- return new Response("Missing authorization code", { status: 400 });
241
+ return null;
242
+ },
243
+ });
244
+ })
245
+ .on("error", (err: NodeJS.ErrnoException) => {
246
+ console.error(
247
+ "[openai-codex] Failed to bind http://127.0.0.1:1455 (",
248
+ err.code,
249
+ ") Falling back to manual paste.",
250
+ );
251
+ resolve({
252
+ close: () => {
253
+ try {
254
+ server.close();
255
+ } catch {
256
+ // ignore
213
257
  }
214
- lastCode = code;
215
- return new Response(SUCCESS_HTML, {
216
- status: 200,
217
- headers: { "Content-Type": "text/html; charset=utf-8" },
218
- });
219
- } catch {
220
- return new Response("Internal error", { status: 500 });
221
- }
222
- },
258
+ },
259
+ cancelWait: () => {},
260
+ waitForCode: async () => null,
261
+ });
223
262
  });
224
-
225
- resolve({
226
- close: () => server.stop(),
227
- waitForCode: async () => {
228
- const sleep = () => new Promise((r) => setTimeout(r, 100));
229
- for (let i = 0; i < 600; i += 1) {
230
- if (lastCode) return { code: lastCode };
231
- await sleep();
232
- }
233
- return null;
234
- },
235
- });
236
- } catch (err) {
237
- const code = (err as { code?: string }).code;
238
- console.error(
239
- "[openai-codex] Failed to bind http://127.0.0.1:1455 (",
240
- code,
241
- ") Falling back to manual paste.",
242
- );
243
- resolve({
244
- close: () => {},
245
- waitForCode: async () => null,
246
- });
247
- }
248
263
  });
249
264
  }
250
265
 
@@ -257,11 +272,19 @@ function getAccountId(accessToken: string): string | null {
257
272
 
258
273
  /**
259
274
  * Login with OpenAI Codex OAuth
275
+ *
276
+ * @param options.onAuth - Called with URL and instructions when auth starts
277
+ * @param options.onPrompt - Called to prompt user for manual code paste (fallback if no onManualCodeInput)
278
+ * @param options.onProgress - Optional progress messages
279
+ * @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.
280
+ * Races with browser callback - whichever completes first wins.
281
+ * Useful for showing paste input immediately alongside browser flow.
260
282
  */
261
283
  export async function loginOpenAICodex(options: {
262
284
  onAuth: (info: { url: string; instructions?: string }) => void;
263
285
  onPrompt: (prompt: OAuthPrompt) => Promise<string>;
264
286
  onProgress?: (message: string) => void;
287
+ onManualCodeInput?: () => Promise<string>;
265
288
  }): Promise<OAuthCredentials> {
266
289
  const { verifier, state, url } = await createAuthorizationFlow();
267
290
  const server = await startLocalOAuthServer(state);
@@ -270,11 +293,63 @@ export async function loginOpenAICodex(options: {
270
293
 
271
294
  let code: string | undefined;
272
295
  try {
273
- const result = await server.waitForCode();
274
- if (result?.code) {
275
- code = result.code;
296
+ if (options.onManualCodeInput) {
297
+ // Race between browser callback and manual input
298
+ let manualCode: string | undefined;
299
+ let manualError: Error | undefined;
300
+ const manualPromise = options
301
+ .onManualCodeInput()
302
+ .then((input) => {
303
+ manualCode = input;
304
+ server.cancelWait();
305
+ })
306
+ .catch((err) => {
307
+ manualError = err instanceof Error ? err : new Error(String(err));
308
+ server.cancelWait();
309
+ });
310
+
311
+ const result = await server.waitForCode();
312
+
313
+ // If manual input was cancelled, throw that error
314
+ if (manualError) {
315
+ throw manualError;
316
+ }
317
+
318
+ if (result?.code) {
319
+ // Browser callback won
320
+ code = result.code;
321
+ } else if (manualCode) {
322
+ // Manual input won (or callback timed out and user had entered code)
323
+ const parsed = parseAuthorizationInput(manualCode);
324
+ if (parsed.state && parsed.state !== state) {
325
+ throw new Error("State mismatch");
326
+ }
327
+ code = parsed.code;
328
+ }
329
+
330
+ // If still no code, wait for manual promise to complete and try that
331
+ if (!code) {
332
+ await manualPromise;
333
+ if (manualError) {
334
+ throw manualError;
335
+ }
336
+ if (manualCode) {
337
+ const parsed = parseAuthorizationInput(manualCode);
338
+ if (parsed.state && parsed.state !== state) {
339
+ throw new Error("State mismatch");
340
+ }
341
+ code = parsed.code;
342
+ }
343
+ }
344
+ } else {
345
+ // Original flow: wait for callback, then prompt if needed
346
+ const result = await server.waitForCode();
347
+ if (result?.code) {
348
+ code = result.code;
349
+ }
276
350
  }
277
351
 
352
+ // Fallback to onPrompt if still no code
278
353
  if (!code) {
279
354
  const input = await options.onPrompt({
280
355
  message: "Paste the authorization code (or full redirect URL):",
@@ -32,7 +32,7 @@ const OVERFLOW_PATTERNS = [
32
32
  /exceeds the limit of \d+/i, // GitHub Copilot
33
33
  /exceeds the available context size/i, // llama.cpp server
34
34
  /greater than the context length/i, // LM Studio
35
- /context length exceeded/i, // Generic fallback
35
+ /context[_ ]length[_ ]exceeded/i, // Generic fallback
36
36
  /too many tokens/i, // Generic fallback
37
37
  /token limit exceeded/i, // Generic fallback
38
38
  ];
@@ -1,14 +0,0 @@
1
- /**
2
- * Type declarations for Bun's import attributes.
3
- * These allow importing non-JS files as text at build time.
4
- */
5
-
6
- declare module "*.md" {
7
- const content: string;
8
- export default content;
9
- }
10
-
11
- declare module "*.txt" {
12
- const content: string;
13
- export default content;
14
- }