openai-oauth 2.0.0-beta.2 → 2.0.0-beta.3

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/dist/cli.js CHANGED
@@ -4,17 +4,15 @@ import {
4
4
  resolveOpenAIOAuthModels,
5
5
  runOpenAIOAuthLogin,
6
6
  startOpenAIOAuthServer
7
- } from "./chunk-UZ3AHAWX.js";
7
+ } from "./chunk-KROVQ42X.js";
8
8
 
9
9
  // src/cli-app.ts
10
10
  import { access } from "fs/promises";
11
11
  import { createInterface } from "readline/promises";
12
12
  import {
13
- createCodexOAuthClient,
14
13
  resolveAuthFileCandidates,
15
14
  resolveCodexAuthFilePath
16
15
  } from "@openai-oauth/core";
17
- import { openaiCredentials } from "@openai-oauth/local";
18
16
  import yargs from "yargs";
19
17
  import { hideBin } from "yargs/helpers";
20
18
 
@@ -61,6 +59,53 @@ var toStartupMessage = (baseUrl, availableModels, options) => [
61
59
  "",
62
60
  `Available Models: ${availableModels.join(", ")}`
63
61
  ].join("\n");
62
+ var notRunning = "OpenAI OAuth is not running.";
63
+ var cliMessages = {
64
+ foregroundControls: "[d] Run in background [q] Quit",
65
+ backgroundActions: "Stop with `npx openai-oauth stop` or see logs with `npx openai-oauth logs --follow`",
66
+ notRunning,
67
+ notRunningWithStart: [notRunning, "", "Start with `npx openai-oauth`"].join(
68
+ "\n"
69
+ ),
70
+ noLogs: "No OpenAI OAuth logs found.",
71
+ followingLogs: "Following OpenAI OAuth logs. Press Ctrl+C to stop following.",
72
+ stopped: "OpenAI OAuth stopped.",
73
+ stoppedRemotely: "OpenAI OAuth was stopped from another terminal.",
74
+ couldNotStop: "Could not stop OpenAI OAuth.",
75
+ couldNotDetach: (details) => {
76
+ if (details?.startsWith("Could not move OpenAI OAuth to the background")) {
77
+ return details;
78
+ }
79
+ return details ? `Could not move OpenAI OAuth to the background: ${details}` : "Could not move OpenAI OAuth to the background.";
80
+ },
81
+ stoppedUnexpectedly: (details) => `OpenAI OAuth stopped unexpectedly (${details}).`,
82
+ workerStarted: (baseUrl) => `OpenAI OAuth started at ${baseUrl}`
83
+ };
84
+ var toForegroundStartupMessage = (baseUrl, availableModels, options) => [
85
+ toStartupMessage(baseUrl, availableModels, options),
86
+ "",
87
+ dim(cliMessages.foregroundControls, options)
88
+ ].join("\n");
89
+ var toBackgroundStartupMessage = (baseUrl, availableModels, options) => [
90
+ toStartupMessage(baseUrl, availableModels, options),
91
+ "",
92
+ cliMessages.backgroundActions
93
+ ].join("\n");
94
+ var toDetachedMessage = (baseUrl) => [
95
+ `OpenAI OAuth is now running in the background at ${baseUrl}`,
96
+ "",
97
+ cliMessages.backgroundActions
98
+ ].join("\n");
99
+ var toAlreadyRunningMessage = (baseUrl) => [
100
+ `OpenAI OAuth is already running at ${baseUrl}`,
101
+ "",
102
+ cliMessages.backgroundActions
103
+ ].join("\n");
104
+ var toRunningMessage = (baseUrl) => [
105
+ `OpenAI OAuth is running at ${baseUrl}`,
106
+ "",
107
+ cliMessages.backgroundActions
108
+ ].join("\n");
64
109
  var installCliWarningLogger = () => {
65
110
  let hasLoggedWarningSystemMessage = false;
66
111
  globalThis.AI_SDK_LOG_WARNINGS = ({ warnings, provider, model }) => {
@@ -80,6 +125,642 @@ var installCliWarningLogger = () => {
80
125
  };
81
126
  };
82
127
 
128
+ // src/cli-runtime.ts
129
+ import { spawn } from "child_process";
130
+ import { randomBytes } from "crypto";
131
+ import { promises as fs } from "fs";
132
+ import { createServer } from "http";
133
+ import os from "os";
134
+ import path from "path";
135
+ import { setTimeout as delay } from "timers/promises";
136
+ var runtimeSchemaVersion = 1;
137
+ var controlHost = "127.0.0.1";
138
+ var internalRuntimeDirectoryVariable = "OPENAI_OAUTH_INTERNAL_RUNTIME_DIR";
139
+ var internalDetachedFlag = "--internal-detached-child";
140
+ var waitForChildExit = (child, timeoutMs) => {
141
+ if (child.exitCode != null || child.signalCode != null) {
142
+ return Promise.resolve(true);
143
+ }
144
+ return new Promise((resolve) => {
145
+ const onExit = () => {
146
+ clearTimeout(timeout);
147
+ resolve(true);
148
+ };
149
+ const timeout = setTimeout(() => {
150
+ child.off("exit", onExit);
151
+ resolve(false);
152
+ }, timeoutMs);
153
+ child.once("exit", onExit);
154
+ });
155
+ };
156
+ var isNodeError = (error) => error instanceof Error;
157
+ var resolveRuntimeDirectory = () => {
158
+ const override = process.env[internalRuntimeDirectoryVariable];
159
+ if (override) {
160
+ return override;
161
+ }
162
+ if (process.platform === "darwin") {
163
+ return path.join(
164
+ os.homedir(),
165
+ "Library",
166
+ "Application Support",
167
+ "openai-oauth"
168
+ );
169
+ }
170
+ if (process.platform === "win32") {
171
+ return path.join(
172
+ process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"),
173
+ "openai-oauth"
174
+ );
175
+ }
176
+ return path.join(
177
+ process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state"),
178
+ "openai-oauth"
179
+ );
180
+ };
181
+ var resolveCliRuntimePaths = (directory = resolveRuntimeDirectory()) => ({
182
+ directory,
183
+ state: path.join(directory, "runtime.json"),
184
+ lock: path.join(directory, "runtime.lock"),
185
+ log: path.join(directory, "server.log")
186
+ });
187
+ var ensureRuntimeDirectory = async (paths) => {
188
+ await fs.mkdir(paths.directory, { recursive: true, mode: 448 });
189
+ try {
190
+ await fs.chmod(paths.directory, 448);
191
+ } catch {
192
+ }
193
+ };
194
+ var isRuntimeState = (value) => {
195
+ if (typeof value !== "object" || value == null) {
196
+ return false;
197
+ }
198
+ const state = value;
199
+ return state.schemaVersion === runtimeSchemaVersion && typeof state.instanceId === "string" && typeof state.pid === "number" && (state.mode === "foreground" || state.mode === "background") && typeof state.url === "string" && typeof state.startedAt === "string" && typeof state.version === "string" && typeof state.controlPort === "number" && typeof state.controlToken === "string";
200
+ };
201
+ var readJson = async (filePath) => {
202
+ try {
203
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
204
+ } catch {
205
+ return void 0;
206
+ }
207
+ };
208
+ var readRuntimeState = async (paths) => {
209
+ const parsed = await readJson(paths.state);
210
+ return isRuntimeState(parsed) ? parsed : void 0;
211
+ };
212
+ var writeRuntimeState = async (paths, state) => {
213
+ const temporaryPath = `${paths.state}.${process.pid}.${randomBytes(4).toString("hex")}`;
214
+ await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
215
+ `, {
216
+ mode: 384
217
+ });
218
+ await fs.rename(temporaryPath, paths.state);
219
+ try {
220
+ await fs.chmod(paths.state, 384);
221
+ } catch {
222
+ }
223
+ };
224
+ var isProcessAlive = (pid) => {
225
+ try {
226
+ process.kill(pid, 0);
227
+ return true;
228
+ } catch (error) {
229
+ return isNodeError(error) && error.code === "EPERM";
230
+ }
231
+ };
232
+ var readLockPid = async (paths) => {
233
+ const parsed = await readJson(paths.lock);
234
+ if (typeof parsed !== "object" || parsed == null) {
235
+ return void 0;
236
+ }
237
+ const pid = parsed.pid;
238
+ return typeof pid === "number" ? pid : void 0;
239
+ };
240
+ var removeIfPresent = async (filePath) => {
241
+ await fs.rm(filePath, { force: true }).catch(() => void 0);
242
+ };
243
+ var fetchWithTimeout = async (input, init, timeoutMs = 750) => {
244
+ const controller = new AbortController();
245
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
246
+ if (typeof timeout === "object" && "unref" in timeout) {
247
+ timeout.unref();
248
+ }
249
+ try {
250
+ return await fetch(input, { ...init, signal: controller.signal });
251
+ } finally {
252
+ clearTimeout(timeout);
253
+ }
254
+ };
255
+ var probeRuntime = async (state) => {
256
+ try {
257
+ const response = await fetchWithTimeout(
258
+ `http://${controlHost}:${state.controlPort}/state`,
259
+ {
260
+ headers: { authorization: `Bearer ${state.controlToken}` }
261
+ }
262
+ );
263
+ if (!response.ok) {
264
+ return false;
265
+ }
266
+ const body = await response.json();
267
+ return body.instanceId === state.instanceId;
268
+ } catch {
269
+ return false;
270
+ }
271
+ };
272
+ var findRunningCliRuntime = async (directory) => {
273
+ const paths = resolveCliRuntimePaths(directory);
274
+ const state = await readRuntimeState(paths);
275
+ if (!state) {
276
+ return void 0;
277
+ }
278
+ for (let attempt = 0; attempt < 3; attempt += 1) {
279
+ if (await probeRuntime(state)) {
280
+ return state;
281
+ }
282
+ await delay(50);
283
+ }
284
+ await removeIfPresent(paths.state);
285
+ await removeIfPresent(paths.lock);
286
+ return void 0;
287
+ };
288
+ var CliRuntimeAlreadyRunningError = class extends Error {
289
+ state;
290
+ constructor(state) {
291
+ super(toAlreadyRunningMessage(state.url));
292
+ this.name = "CliRuntimeAlreadyRunningError";
293
+ this.state = state;
294
+ }
295
+ };
296
+ var acquireCliRuntimeLock = async (directory) => {
297
+ const paths = resolveCliRuntimePaths(directory);
298
+ await ensureRuntimeDirectory(paths);
299
+ for (let attempt = 0; attempt < 50; attempt += 1) {
300
+ try {
301
+ const handle = await fs.open(paths.lock, "wx", 384);
302
+ await handle.writeFile(
303
+ `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
304
+ `
305
+ );
306
+ await handle.close();
307
+ let released = false;
308
+ return {
309
+ paths,
310
+ release: async () => {
311
+ if (released) {
312
+ return;
313
+ }
314
+ released = true;
315
+ await removeIfPresent(paths.lock);
316
+ }
317
+ };
318
+ } catch (error) {
319
+ if (!isNodeError(error) || error.code !== "EEXIST") {
320
+ throw error;
321
+ }
322
+ const running = await findRunningCliRuntime(directory);
323
+ if (running) {
324
+ throw new CliRuntimeAlreadyRunningError(running);
325
+ }
326
+ const ownerPid = await readLockPid(paths);
327
+ if (ownerPid == null || !isProcessAlive(ownerPid)) {
328
+ await removeIfPresent(paths.lock);
329
+ continue;
330
+ }
331
+ await delay(100);
332
+ }
333
+ }
334
+ throw new Error("OpenAI OAuth is already starting in another process.");
335
+ };
336
+ var closeServer = (server) => new Promise((resolve, reject) => {
337
+ if (!server.listening) {
338
+ resolve();
339
+ return;
340
+ }
341
+ server.close((error) => {
342
+ if (error) {
343
+ reject(error);
344
+ return;
345
+ }
346
+ resolve();
347
+ });
348
+ });
349
+ var activateCliRuntime = async (lock, options) => {
350
+ const instanceId = randomBytes(16).toString("hex");
351
+ const controlToken = randomBytes(32).toString("hex");
352
+ let state;
353
+ const controlServer = createServer((request, response) => {
354
+ if (request.headers.authorization !== `Bearer ${controlToken}`) {
355
+ response.writeHead(401).end();
356
+ return;
357
+ }
358
+ if (request.method === "GET" && request.url === "/state") {
359
+ response.writeHead(200, { "content-type": "application/json" });
360
+ response.end(JSON.stringify({ instanceId: state.instanceId }));
361
+ return;
362
+ }
363
+ if (request.method === "POST" && request.url === "/stop") {
364
+ response.writeHead(202).end();
365
+ setImmediate(() => void options.onStop());
366
+ return;
367
+ }
368
+ response.writeHead(404).end();
369
+ });
370
+ try {
371
+ await new Promise((resolve, reject) => {
372
+ controlServer.once("error", reject);
373
+ controlServer.listen(0, controlHost, () => {
374
+ controlServer.off("error", reject);
375
+ resolve();
376
+ });
377
+ });
378
+ const address = controlServer.address();
379
+ state = {
380
+ schemaVersion: runtimeSchemaVersion,
381
+ instanceId,
382
+ pid: process.pid,
383
+ mode: options.mode,
384
+ url: options.url,
385
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
386
+ version: options.version,
387
+ controlPort: address.port,
388
+ controlToken
389
+ };
390
+ await writeRuntimeState(lock.paths, state);
391
+ } catch (error) {
392
+ await closeServer(controlServer).catch(() => void 0);
393
+ await lock.release();
394
+ throw error;
395
+ }
396
+ let closed = false;
397
+ return {
398
+ state,
399
+ logPath: lock.paths.log,
400
+ setMode: async (mode) => {
401
+ if (state.mode === mode) {
402
+ return;
403
+ }
404
+ state = { ...state, mode };
405
+ await writeRuntimeState(lock.paths, state);
406
+ },
407
+ close: async () => {
408
+ if (closed) {
409
+ return;
410
+ }
411
+ closed = true;
412
+ const current = await readRuntimeState(lock.paths);
413
+ if (current?.instanceId === state.instanceId) {
414
+ await removeIfPresent(lock.paths.state);
415
+ }
416
+ await closeServer(controlServer).catch(() => void 0);
417
+ await lock.release();
418
+ }
419
+ };
420
+ };
421
+ var stopCliRuntime = async (state, directory) => {
422
+ try {
423
+ const response = await fetchWithTimeout(
424
+ `http://${controlHost}:${state.controlPort}/stop`,
425
+ {
426
+ method: "POST",
427
+ headers: { authorization: `Bearer ${state.controlToken}` }
428
+ }
429
+ );
430
+ if (!response.ok) {
431
+ return false;
432
+ }
433
+ } catch {
434
+ return false;
435
+ }
436
+ for (let attempt = 0; attempt < 50; attempt += 1) {
437
+ if (!await findRunningCliRuntime(directory)) {
438
+ return true;
439
+ }
440
+ await delay(100);
441
+ }
442
+ return false;
443
+ };
444
+ var isCliWorkerMessage = (value) => {
445
+ if (typeof value !== "object" || value == null) {
446
+ return false;
447
+ }
448
+ const message = value;
449
+ return message.type === "openai-oauth:ready" && typeof message.url === "string" && Array.isArray(message.models) || message.type === "openai-oauth:error" && typeof message.message === "string" || message.type === "openai-oauth:detached" || message.type === "openai-oauth:stopping" && message.source === "remote";
450
+ };
451
+ var waitForWorkerMessage = (child, predicate, timeoutMs) => new Promise((resolve, reject) => {
452
+ let settled = false;
453
+ const timeout = setTimeout(() => {
454
+ finish(new Error(`OpenAI OAuth did not respond within ${timeoutMs}ms.`));
455
+ }, timeoutMs);
456
+ const cleanup = () => {
457
+ clearTimeout(timeout);
458
+ child.off("message", onMessage);
459
+ child.off("error", onError);
460
+ child.off("exit", onExit);
461
+ };
462
+ const finish = (error, message) => {
463
+ if (settled) {
464
+ return;
465
+ }
466
+ settled = true;
467
+ cleanup();
468
+ if (error) {
469
+ reject(error);
470
+ return;
471
+ }
472
+ if (!message) {
473
+ reject(new Error("OpenAI OAuth worker returned an empty response."));
474
+ return;
475
+ }
476
+ resolve(message);
477
+ };
478
+ const onMessage = (value) => {
479
+ if (!isCliWorkerMessage(value)) {
480
+ return;
481
+ }
482
+ if (value.type === "openai-oauth:error") {
483
+ finish(new Error(value.message));
484
+ return;
485
+ }
486
+ if (predicate(value)) {
487
+ finish(void 0, value);
488
+ }
489
+ };
490
+ const onError = (error) => finish(error);
491
+ const onExit = (code, signal) => finish(
492
+ new Error(
493
+ `OpenAI OAuth exited unexpectedly (${signal ?? code ?? "unknown"}).`
494
+ )
495
+ );
496
+ child.on("message", onMessage);
497
+ child.once("error", onError);
498
+ child.once("exit", onExit);
499
+ });
500
+ var startCliWorker = async (argv, options = {}) => {
501
+ const paths = resolveCliRuntimePaths(options.runtimeDirectory);
502
+ await ensureRuntimeDirectory(paths);
503
+ const logHandle = await fs.open(paths.log, "a", 384);
504
+ let child;
505
+ const entryPath = options.entryPath ?? process.argv[1];
506
+ if (!entryPath) {
507
+ await logHandle.close();
508
+ throw new Error("Could not resolve the OpenAI OAuth CLI entrypoint.");
509
+ }
510
+ try {
511
+ child = spawn(
512
+ process.execPath,
513
+ [
514
+ entryPath,
515
+ ...argv.filter((argument) => argument !== internalDetachedFlag),
516
+ internalDetachedFlag
517
+ ],
518
+ {
519
+ detached: true,
520
+ windowsHide: true,
521
+ stdio: ["ignore", logHandle.fd, logHandle.fd, "ipc"],
522
+ env: {
523
+ ...process.env,
524
+ ...options.runtimeDirectory ? {
525
+ [internalRuntimeDirectoryVariable]: options.runtimeDirectory
526
+ } : {}
527
+ }
528
+ }
529
+ );
530
+ } finally {
531
+ await logHandle.close();
532
+ }
533
+ let ready;
534
+ try {
535
+ ready = await waitForWorkerMessage(
536
+ child,
537
+ (message) => message.type === "openai-oauth:ready",
538
+ options.timeoutMs ?? 15e3
539
+ );
540
+ } catch (error) {
541
+ if (child.pid) {
542
+ try {
543
+ process.kill(child.pid, "SIGTERM");
544
+ } catch {
545
+ }
546
+ }
547
+ if (child.connected) {
548
+ child.disconnect();
549
+ }
550
+ child.unref();
551
+ throw error;
552
+ }
553
+ return {
554
+ child,
555
+ url: ready.url,
556
+ models: ready.models,
557
+ logPath: paths.log,
558
+ detach: async () => {
559
+ const acknowledged = waitForWorkerMessage(
560
+ child,
561
+ (message) => message.type === "openai-oauth:detached",
562
+ 5e3
563
+ );
564
+ child.send?.({ type: "openai-oauth:detach" });
565
+ await acknowledged;
566
+ if (child.connected) {
567
+ child.disconnect();
568
+ }
569
+ child.unref();
570
+ },
571
+ stop: async () => {
572
+ child.send?.({ type: "openai-oauth:stop" });
573
+ if (await waitForChildExit(child, 5e3)) {
574
+ return;
575
+ }
576
+ child.kill("SIGTERM");
577
+ if (await waitForChildExit(child, 1e3)) {
578
+ return;
579
+ }
580
+ if (child.connected) {
581
+ child.disconnect();
582
+ }
583
+ child.unref();
584
+ throw new Error("OpenAI OAuth did not stop within 6000ms.");
585
+ }
586
+ };
587
+ };
588
+ var readLogSlice = async (filePath, start) => {
589
+ try {
590
+ const stat = await fs.stat(filePath);
591
+ const safeStart = stat.size < start ? 0 : start;
592
+ if (stat.size === safeStart) {
593
+ return { text: "", size: stat.size };
594
+ }
595
+ const handle = await fs.open(filePath, "r");
596
+ try {
597
+ const buffer = Buffer.alloc(stat.size - safeStart);
598
+ await handle.read(buffer, 0, buffer.length, safeStart);
599
+ return { text: buffer.toString("utf8"), size: stat.size };
600
+ } finally {
601
+ await handle.close();
602
+ }
603
+ } catch (error) {
604
+ if (isNodeError(error) && error.code === "ENOENT") {
605
+ return { text: "", size: 0 };
606
+ }
607
+ throw error;
608
+ }
609
+ };
610
+ var readRecentCliLogs = async (directory, lineCount = 50) => {
611
+ const { log } = resolveCliRuntimePaths(directory);
612
+ try {
613
+ const stat = await fs.stat(log);
614
+ const start = Math.max(0, stat.size - 64 * 1024);
615
+ const { text } = await readLogSlice(log, start);
616
+ return text.split("\n").slice(-lineCount - 1).join("\n").trimEnd();
617
+ } catch (error) {
618
+ if (isNodeError(error) && error.code === "ENOENT") {
619
+ return "";
620
+ }
621
+ throw error;
622
+ }
623
+ };
624
+ var followCliLogs = async (write, signal, directory, fromStart = false) => {
625
+ const { log } = resolveCliRuntimePaths(directory);
626
+ let offset = 0;
627
+ if (!fromStart) {
628
+ try {
629
+ offset = (await fs.stat(log)).size;
630
+ } catch {
631
+ }
632
+ }
633
+ while (!signal.aborted) {
634
+ try {
635
+ await delay(250, void 0, { signal });
636
+ } catch {
637
+ break;
638
+ }
639
+ const next = await readLogSlice(log, offset);
640
+ offset = next.size;
641
+ if (next.text) {
642
+ write(next.text);
643
+ }
644
+ }
645
+ };
646
+ var sendDetachedReady = (url, models) => {
647
+ process.send?.({ type: "openai-oauth:ready", url, models });
648
+ };
649
+ var sendDetachedAcknowledgement = () => {
650
+ process.send?.({ type: "openai-oauth:detached" });
651
+ };
652
+ var sendRemoteStopping = () => new Promise((resolve) => {
653
+ if (!process.send) {
654
+ resolve();
655
+ return;
656
+ }
657
+ process.send(
658
+ { type: "openai-oauth:stopping", source: "remote" },
659
+ () => resolve()
660
+ );
661
+ });
662
+ var sendDetachedError = (message) => new Promise((resolve) => {
663
+ if (!process.send) {
664
+ resolve();
665
+ return;
666
+ }
667
+ process.send({ type: "openai-oauth:error", message }, () => resolve());
668
+ });
669
+
670
+ // src/cli-worker.ts
671
+ import { createCodexOAuthClient } from "@openai-oauth/core";
672
+ import { openaiCredentials } from "@openai-oauth/local";
673
+ var isWorkerCommand = (value) => {
674
+ if (typeof value !== "object" || value == null) {
675
+ return false;
676
+ }
677
+ const type = value.type;
678
+ return type === "openai-oauth:detach" || type === "openai-oauth:stop";
679
+ };
680
+ var startCliInstance = async (options, mode, version, onRemoteStop) => {
681
+ const lock = await acquireCliRuntimeLock();
682
+ let server;
683
+ try {
684
+ const auth = openaiCredentials(options);
685
+ const client = createCodexOAuthClient({
686
+ ...options,
687
+ auth: () => auth.getSession(),
688
+ responsesState: false
689
+ });
690
+ const models = await resolveOpenAIOAuthModels(client, options.models);
691
+ server = await startOpenAIOAuthServer(options);
692
+ const runtime = await activateCliRuntime(lock, {
693
+ mode,
694
+ url: server.url,
695
+ version,
696
+ onStop: onRemoteStop
697
+ });
698
+ return { server, runtime, models };
699
+ } catch (error) {
700
+ await server?.close().catch(() => void 0);
701
+ await lock.release();
702
+ throw error;
703
+ }
704
+ };
705
+ var runCliWorker = async (options, initialMode, version) => {
706
+ if (typeof process.send !== "function") {
707
+ throw new Error("OpenAI OAuth worker requires an IPC channel.");
708
+ }
709
+ let detached = false;
710
+ let stopping = false;
711
+ let instance;
712
+ const shutdown = async (source) => {
713
+ if (stopping) {
714
+ return;
715
+ }
716
+ stopping = true;
717
+ if (source === "remote") {
718
+ await sendRemoteStopping();
719
+ }
720
+ await instance?.server.close().catch(() => void 0);
721
+ await instance?.runtime.close().catch(() => void 0);
722
+ if (process.connected) {
723
+ process.disconnect();
724
+ }
725
+ process.exit(0);
726
+ };
727
+ instance = await startCliInstance(options, initialMode, version, () => {
728
+ void shutdown("remote");
729
+ });
730
+ const onMessage = (value) => {
731
+ if (!isWorkerCommand(value) || stopping) {
732
+ return;
733
+ }
734
+ if (value.type === "openai-oauth:stop") {
735
+ void shutdown("parent");
736
+ return;
737
+ }
738
+ void (async () => {
739
+ try {
740
+ await instance?.runtime.setMode("background");
741
+ detached = true;
742
+ sendDetachedAcknowledgement();
743
+ } catch (error) {
744
+ await sendDetachedError(
745
+ error instanceof Error ? error.message : cliMessages.couldNotDetach()
746
+ );
747
+ }
748
+ })();
749
+ };
750
+ const onDisconnect = () => {
751
+ if (!detached) {
752
+ void shutdown("disconnect");
753
+ }
754
+ };
755
+ const onSignal = () => void shutdown("signal");
756
+ process.on("message", onMessage);
757
+ process.once("disconnect", onDisconnect);
758
+ process.once("SIGINT", onSignal);
759
+ process.once("SIGTERM", onSignal);
760
+ console.log(cliMessages.workerStarted(instance.server.url));
761
+ sendDetachedReady(instance.server.url, instance.models);
762
+ };
763
+
83
764
  // src/update-check.ts
84
765
  var REGISTRY_URL = "https://registry.npmjs.org/openai-oauth/latest";
85
766
  var normalizeVersion = (value) => {
@@ -154,7 +835,7 @@ Run \`npx openai-oauth@latest\` to use the newest version.`
154
835
  };
155
836
 
156
837
  // src/version.ts
157
- var packageVersion = "2.0.0-beta.2";
838
+ var packageVersion = "2.0.0-beta.3";
158
839
 
159
840
  // src/cli-app.ts
160
841
  var defaultUpdateCheckWarning = (message) => {
@@ -172,6 +853,10 @@ var helpLines = [
172
853
  "",
173
854
  "Usage",
174
855
  " npx openai-oauth@latest [options]",
856
+ " npx openai-oauth@latest --detach [options]",
857
+ " npx openai-oauth@latest status",
858
+ " npx openai-oauth@latest logs [--follow]",
859
+ " npx openai-oauth@latest stop",
175
860
  " npx openai-oauth@latest login [options]",
176
861
  "",
177
862
  "Options",
@@ -187,6 +872,8 @@ var helpLines = [
187
872
  " --login-timeout-ms <ms> Login timeout. Default: 300000",
188
873
  "",
189
874
  "Flags",
875
+ " -d, --detach Run in the background",
876
+ " -f, --follow Follow logs",
190
877
  " --help Show help",
191
878
  ` --version Show version (${packageVersion})`,
192
879
  "",
@@ -226,14 +913,31 @@ var createCliParser = (argv) => yargs(argv).scriptName("openai-oauth").strict().
226
913
  }).option("login-timeout-ms", {
227
914
  type: "number",
228
915
  describe: "Login timeout in milliseconds. Default: 300000"
229
- });
916
+ }).option("detach", {
917
+ alias: "d",
918
+ type: "boolean",
919
+ default: false,
920
+ describe: "Run in the background."
921
+ }).option("follow", {
922
+ alias: "f",
923
+ type: "boolean",
924
+ default: false,
925
+ describe: "Follow logs."
926
+ }).option("internal-detached-child", {
927
+ type: "boolean",
928
+ default: false
929
+ }).hide("internal-detached-child");
230
930
  var isHelpFlag = (argv) => argv.includes("--help") || argv.includes("-h");
231
931
  var isVersionFlag = (argv) => argv.includes("--version");
232
932
  var toHelpMessage = () => helpLines.join("\n");
233
933
  var parseCliArgs = (argv) => {
234
- const command = argv[0] === "login" ? "login" : "serve";
934
+ const first = argv[0];
935
+ if (first && !first.startsWith("-") && first !== "serve" && first !== "login" && first !== "logs" && first !== "status" && first !== "stop") {
936
+ throw new Error(`Unknown command: ${first}`);
937
+ }
938
+ const command = first === "login" || first === "logs" || first === "status" || first === "stop" ? first : "serve";
235
939
  const parsed = createCliParser(
236
- command === "login" ? argv.slice(1) : argv
940
+ command === "serve" && first !== "serve" ? argv : argv.slice(1)
237
941
  ).parseSync();
238
942
  return {
239
943
  command,
@@ -246,7 +950,10 @@ var parseCliArgs = (argv) => {
246
950
  tokenUrl: parsed.oauthTokenUrl,
247
951
  authFilePath: parsed.oauthFile,
248
952
  openBrowser: parsed.open,
249
- loginTimeoutMs: parsed.loginTimeoutMs
953
+ loginTimeoutMs: parsed.loginTimeoutMs,
954
+ detach: parsed.detach,
955
+ follow: parsed.follow,
956
+ internalDetachedChild: parsed.internalDetachedChild
250
957
  };
251
958
  };
252
959
  var toServerOptions = (args) => ({
@@ -313,8 +1020,9 @@ var toMissingAuthFilePrompt = (authFilePath) => {
313
1020
  `Sign in with ChatGPT now? This will write credentials to ${writePath}.`
314
1021
  ].join("\n");
315
1022
  };
1023
+ var toExistingAuthFileMessage = (authFilePath) => `OpenAI OAuth credentials already exist at ${authFilePath}.`;
316
1024
  var toOverwriteAuthFilePrompt = (authFilePath) => [
317
- `OpenAI OAuth credentials already exist at ${authFilePath}.`,
1025
+ toExistingAuthFileMessage(authFilePath),
318
1026
  "Sign in with ChatGPT again and overwrite them?"
319
1027
  ].join("\n");
320
1028
  var toMissingAuthFileMessage = (authFilePath) => {
@@ -373,6 +1081,200 @@ var runLoginWithCancellation = async (options) => {
373
1081
  cancellation.dispose();
374
1082
  }
375
1083
  };
1084
+ var runLogsCommand = async (follow) => {
1085
+ const recent = await readRecentCliLogs();
1086
+ const running = await findRunningCliRuntime();
1087
+ if (!running) {
1088
+ console.log(cliMessages.notRunningWithStart);
1089
+ return;
1090
+ }
1091
+ if (!follow) {
1092
+ console.log(recent || cliMessages.noLogs);
1093
+ return;
1094
+ }
1095
+ console.log(cliMessages.followingLogs);
1096
+ if (recent) {
1097
+ console.log(recent);
1098
+ }
1099
+ const controller = new AbortController();
1100
+ const abort = () => controller.abort();
1101
+ process.once("SIGINT", abort);
1102
+ process.once("SIGTERM", abort);
1103
+ try {
1104
+ await followCliLogs((text) => process.stdout.write(text), controller.signal);
1105
+ } finally {
1106
+ process.off("SIGINT", abort);
1107
+ process.off("SIGTERM", abort);
1108
+ }
1109
+ };
1110
+ var runStopCommand = async () => {
1111
+ const running = await findRunningCliRuntime();
1112
+ if (!running) {
1113
+ console.log(cliMessages.notRunning);
1114
+ return;
1115
+ }
1116
+ if (!await stopCliRuntime(running)) {
1117
+ throw new Error(cliMessages.couldNotStop);
1118
+ }
1119
+ console.log(cliMessages.stopped);
1120
+ };
1121
+ var runStatusCommand = async () => {
1122
+ const running = await findRunningCliRuntime();
1123
+ if (!running) {
1124
+ console.log(cliMessages.notRunningWithStart);
1125
+ process.exitCode = 1;
1126
+ return;
1127
+ }
1128
+ console.log(toRunningMessage(running.url));
1129
+ };
1130
+ var runForegroundController = async (worker) => new Promise((resolve, reject) => {
1131
+ const interactive = canPrompt();
1132
+ const stdin = process.stdin;
1133
+ const logController = new AbortController();
1134
+ const previousRawMode = stdin.isRaw;
1135
+ let menuShown = false;
1136
+ let completing = false;
1137
+ let remoteStopping = false;
1138
+ let rawModeEnabled = false;
1139
+ const cleanup = () => {
1140
+ logController.abort();
1141
+ stdin.off("data", onData);
1142
+ process.off("SIGINT", onSigint);
1143
+ process.off("SIGTERM", onSigterm);
1144
+ worker.child.off("message", onWorkerMessage);
1145
+ worker.child.off("error", onWorkerError);
1146
+ worker.child.off("exit", onWorkerExit);
1147
+ if (rawModeEnabled && stdin.setRawMode) {
1148
+ stdin.setRawMode(previousRawMode ?? false);
1149
+ }
1150
+ if (interactive) {
1151
+ stdin.pause();
1152
+ }
1153
+ };
1154
+ const finish = (error) => {
1155
+ cleanup();
1156
+ if (error) {
1157
+ reject(error);
1158
+ return;
1159
+ }
1160
+ resolve();
1161
+ };
1162
+ const quit = async (showMessage) => {
1163
+ if (completing) {
1164
+ return;
1165
+ }
1166
+ completing = true;
1167
+ try {
1168
+ await worker.stop();
1169
+ if (showMessage) {
1170
+ console.log(`
1171
+ ${cliMessages.stopped}`);
1172
+ }
1173
+ finish();
1174
+ } catch (error) {
1175
+ finish(
1176
+ error instanceof Error ? error : new Error(cliMessages.couldNotStop)
1177
+ );
1178
+ }
1179
+ };
1180
+ const detach = async () => {
1181
+ if (completing) {
1182
+ return;
1183
+ }
1184
+ completing = true;
1185
+ try {
1186
+ await worker.detach();
1187
+ console.log(`
1188
+ ${toDetachedMessage(worker.url)}`);
1189
+ finish();
1190
+ } catch (error) {
1191
+ completing = false;
1192
+ console.error(
1193
+ `
1194
+ ${cliMessages.couldNotDetach(
1195
+ error instanceof Error ? error.message : void 0
1196
+ )}`
1197
+ );
1198
+ }
1199
+ };
1200
+ const handleInterrupt = () => {
1201
+ if (!interactive || menuShown) {
1202
+ void quit(interactive);
1203
+ return;
1204
+ }
1205
+ menuShown = true;
1206
+ console.log(`
1207
+ ${cliMessages.foregroundControls}`);
1208
+ };
1209
+ function onData(value) {
1210
+ const input = value.toString().toLowerCase();
1211
+ if (input.includes("")) {
1212
+ handleInterrupt();
1213
+ return;
1214
+ }
1215
+ if (input.includes("d")) {
1216
+ void detach();
1217
+ return;
1218
+ }
1219
+ if (input.includes("q")) {
1220
+ void quit(true);
1221
+ }
1222
+ }
1223
+ function onSigint() {
1224
+ handleInterrupt();
1225
+ }
1226
+ function onSigterm() {
1227
+ void quit(false);
1228
+ }
1229
+ function onWorkerMessage(value) {
1230
+ const message = value;
1231
+ if (message.type === "openai-oauth:stopping" && message.source === "remote") {
1232
+ remoteStopping = true;
1233
+ }
1234
+ }
1235
+ function onWorkerError(error) {
1236
+ if (!completing) {
1237
+ finish(error);
1238
+ }
1239
+ }
1240
+ function onWorkerExit(code, signal) {
1241
+ if (completing) {
1242
+ return;
1243
+ }
1244
+ if (remoteStopping) {
1245
+ console.log(`
1246
+ ${cliMessages.stoppedRemotely}`);
1247
+ finish();
1248
+ return;
1249
+ }
1250
+ finish(
1251
+ new Error(
1252
+ cliMessages.stoppedUnexpectedly(signal ?? `${code ?? "unknown"}`)
1253
+ )
1254
+ );
1255
+ }
1256
+ if (interactive) {
1257
+ if (stdin.setRawMode) {
1258
+ stdin.setRawMode(true);
1259
+ rawModeEnabled = true;
1260
+ }
1261
+ stdin.resume();
1262
+ stdin.on("data", onData);
1263
+ }
1264
+ process.on("SIGINT", onSigint);
1265
+ process.on("SIGTERM", onSigterm);
1266
+ worker.child.on("message", onWorkerMessage);
1267
+ worker.child.once("error", onWorkerError);
1268
+ worker.child.once("exit", onWorkerExit);
1269
+ void followCliLogs(
1270
+ (text) => process.stdout.write(text),
1271
+ logController.signal
1272
+ ).catch((error) => {
1273
+ if (!logController.signal.aborted) {
1274
+ console.error(error instanceof Error ? error.message : error);
1275
+ }
1276
+ });
1277
+ });
376
1278
  var runCli = async (argv = hideBin(process.argv)) => {
377
1279
  if (isHelpFlag(argv)) {
378
1280
  console.log(toHelpMessage());
@@ -384,9 +1286,36 @@ var runCli = async (argv = hideBin(process.argv)) => {
384
1286
  }
385
1287
  installCliWarningLogger();
386
1288
  const args = parseCliArgs(argv);
387
- const updateCheck = runUpdateCheck();
1289
+ if (args.internalDetachedChild) {
1290
+ try {
1291
+ await runCliWorker(
1292
+ toServerOptions(args),
1293
+ args.detach ? "background" : "foreground",
1294
+ packageVersion
1295
+ );
1296
+ } catch (error) {
1297
+ await sendDetachedError(
1298
+ error instanceof Error ? error.message : "Failed to start OpenAI OAuth."
1299
+ );
1300
+ throw error;
1301
+ }
1302
+ return;
1303
+ }
1304
+ if (args.command === "logs") {
1305
+ await runLogsCommand(Boolean(args.follow));
1306
+ return;
1307
+ }
1308
+ if (args.command === "stop") {
1309
+ await runStopCommand();
1310
+ return;
1311
+ }
1312
+ if (args.command === "status") {
1313
+ await runStatusCommand();
1314
+ return;
1315
+ }
388
1316
  if (args.command === "login") {
389
- await updateCheck;
1317
+ const updateCheck2 = runUpdateCheck();
1318
+ await updateCheck2;
390
1319
  const loginOptions = toLoginOptions(args);
391
1320
  const existingAuthFile2 = await findExistingCodexAuthFile(
392
1321
  loginOptions.authFilePath
@@ -395,7 +1324,7 @@ var runCli = async (argv = hideBin(process.argv)) => {
395
1324
  if (!canPrompt()) {
396
1325
  throw new Error(
397
1326
  [
398
- `OpenAI OAuth credentials already exist at ${existingAuthFile2}.`,
1327
+ toExistingAuthFileMessage(existingAuthFile2),
399
1328
  "Run `npx openai-oauth login` in an interactive terminal to confirm overwrite."
400
1329
  ].join("\n")
401
1330
  );
@@ -412,6 +1341,12 @@ var runCli = async (argv = hideBin(process.argv)) => {
412
1341
  await runLoginWithCancellation(loginOptions);
413
1342
  return;
414
1343
  }
1344
+ const running = await findRunningCliRuntime();
1345
+ if (running) {
1346
+ console.log(toAlreadyRunningMessage(running.url));
1347
+ return;
1348
+ }
1349
+ const updateCheck = runUpdateCheck();
415
1350
  const options = toServerOptions(args);
416
1351
  const existingAuthFile = await findExistingAuthFile(options.authFilePath);
417
1352
  if (!existingAuthFile) {
@@ -432,34 +1367,23 @@ var runCli = async (argv = hideBin(process.argv)) => {
432
1367
  return;
433
1368
  }
434
1369
  }
435
- const auth = openaiCredentials(options);
436
- const client = createCodexOAuthClient({
437
- ...options,
438
- auth: () => auth.getSession(),
439
- responsesState: false
440
- });
441
- const availableModels = await resolveOpenAIOAuthModels(client, options.models);
442
- const server = await startOpenAIOAuthServer(options);
443
- console.log(
444
- toStartupMessage(
445
- `http://${server.host}:${server.port}/v1`,
446
- availableModels,
447
- {
1370
+ const worker = await startCliWorker(argv);
1371
+ await updateCheck;
1372
+ if (args.detach) {
1373
+ await worker.detach();
1374
+ console.log(
1375
+ toBackgroundStartupMessage(worker.url, worker.models, {
448
1376
  useColor: process.stdout.isTTY
449
- }
450
- )
1377
+ })
1378
+ );
1379
+ return;
1380
+ }
1381
+ console.log(
1382
+ toForegroundStartupMessage(worker.url, worker.models, {
1383
+ useColor: process.stdout.isTTY
1384
+ })
451
1385
  );
452
- void updateCheck;
453
- const shutdown = async () => {
454
- await server.close();
455
- process.exit(0);
456
- };
457
- process.on("SIGINT", () => {
458
- void shutdown();
459
- });
460
- process.on("SIGTERM", () => {
461
- void shutdown();
462
- });
1386
+ await runForegroundController(worker);
463
1387
  };
464
1388
 
465
1389
  // src/cli.ts