opencode-usage 0.5.3 → 0.5.5

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.
@@ -4,4 +4,9 @@
4
4
  * Each command is registered via `registerCommand` so the command-runner can
5
5
  * execute them as background jobs.
6
6
  */
7
- export {};
7
+ /**
8
+ * Proactively refresh Codex tokens that haven't been refreshed in 24+ hours.
9
+ * Keeps tokens fresh so they never silently expire.
10
+ * Safe to call frequently — skips accounts refreshed recently.
11
+ */
12
+ export declare function proactiveRefreshCodexTokens(): Promise<void>;
package/dist/index.js CHANGED
@@ -1,5 +1,15 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
3
13
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
4
14
 
5
15
  // src/commander/services/command-runner.ts
@@ -232,6 +242,9 @@ var init_config_service = __esm(() => {
232
242
 
233
243
  // src/commander/services/plugin-adapters.ts
234
244
  var exports_plugin_adapters = {};
245
+ __export(exports_plugin_adapters, {
246
+ proactiveRefreshCodexTokens: () => proactiveRefreshCodexTokens
247
+ });
235
248
  import { homedir as homedir6, tmpdir } from "os";
236
249
  import { join as join8 } from "path";
237
250
  function resolveSource2(provider) {
@@ -241,36 +254,215 @@ function resolveSource2(provider) {
241
254
  }
242
255
  return source;
243
256
  }
257
+ async function directCodexPing(alias) {
258
+ console.log(`[directCodexPing] pinging "${alias}"\u2026`);
259
+ const STORE_PATHS = [
260
+ join8(homedir6(), ".config", "opencode", "codex-multi-account-accounts.json"),
261
+ join8(homedir6(), ".config", "opencode", "codex-multi-accounts.json"),
262
+ join8(homedir6(), ".config", "oc-codex-multi-account", "accounts.json")
263
+ ];
264
+ let store = null;
265
+ for (const p of STORE_PATHS) {
266
+ try {
267
+ store = JSON.parse(await Bun.file(p).text());
268
+ break;
269
+ } catch {
270
+ continue;
271
+ }
272
+ }
273
+ if (!store)
274
+ return { status: "error", error: "No codex store found" };
275
+ console.log(`[directCodexPing] found account "${alias}", calling ChatGPT Codex API\u2026`);
276
+ const accounts = store.accounts ?? {};
277
+ const account = accounts[alias];
278
+ if (!account)
279
+ return { status: "error", error: `Account "${alias}" not found` };
280
+ const token = account.accessToken;
281
+ const accountId = account.accountId;
282
+ if (typeof token !== "string" || !token) {
283
+ return { status: "error", error: "Missing access token" };
284
+ }
285
+ if (typeof accountId !== "string" || !accountId) {
286
+ return { status: "error", error: "Missing accountId" };
287
+ }
288
+ try {
289
+ const res = await fetch("https://chatgpt.com/backend-api/codex/responses", {
290
+ method: "POST",
291
+ headers: {
292
+ Authorization: `Bearer ${token}`,
293
+ "Content-Type": "application/json",
294
+ "chatgpt-account-id": accountId,
295
+ "OpenAI-Beta": "responses=experimental",
296
+ originator: "codex_cli_rs",
297
+ accept: "text/event-stream"
298
+ },
299
+ body: JSON.stringify({
300
+ model: "gpt-5.3-codex",
301
+ instructions: "reply ok",
302
+ input: [{ type: "message", role: "user", content: "hi" }],
303
+ store: false,
304
+ stream: true
305
+ })
306
+ });
307
+ if (res.ok || res.status === 429) {
308
+ console.log(`[directCodexPing] "${alias}" \u2192 ok (HTTP ${res.status})`);
309
+ return { status: "ok" };
310
+ }
311
+ if (res.status === 401 || res.status === 403) {
312
+ console.log(`[directCodexPing] "${alias}" \u2192 expired (HTTP ${res.status})`);
313
+ return { status: "expired", error: `HTTP ${res.status}` };
314
+ }
315
+ console.log(`[directCodexPing] "${alias}" \u2192 error (HTTP ${res.status})`);
316
+ return { status: "error", error: `HTTP ${res.status}` };
317
+ } catch (err) {
318
+ return {
319
+ status: "error",
320
+ error: err instanceof Error ? err.message : String(err)
321
+ };
322
+ }
323
+ }
324
+ function decodeJwtPayload(token) {
325
+ try {
326
+ const parts = token.split(".");
327
+ if (parts.length !== 3)
328
+ return null;
329
+ const payload = Buffer.from(parts[1], "base64").toString("utf-8");
330
+ return JSON.parse(payload);
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+ function getExpiryFromJwt(claims) {
336
+ if (!claims || typeof claims.exp !== "number")
337
+ return null;
338
+ return claims.exp * 1000;
339
+ }
340
+ function getAccountIdFromJwt(claims) {
341
+ const auth = claims?.["https://api.openai.com/auth"];
342
+ return auth?.chatgpt_account_id ?? null;
343
+ }
344
+ async function refreshSingleCodexToken(alias, account, storePath, store) {
345
+ const refreshToken = account.refreshToken;
346
+ if (typeof refreshToken !== "string" || !refreshToken) {
347
+ console.log(`[proactiveRefresh] ${alias}: no refresh token, skipping`);
348
+ return false;
349
+ }
350
+ try {
351
+ const t0 = Date.now();
352
+ const res = await fetch(CODEX_TOKEN_URL, {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
355
+ body: new URLSearchParams({
356
+ grant_type: "refresh_token",
357
+ client_id: CODEX_CLIENT_ID,
358
+ refresh_token: refreshToken
359
+ })
360
+ });
361
+ if (!res.ok) {
362
+ console.log(`[proactiveRefresh] ${alias}: refresh failed HTTP ${res.status} in ${Date.now() - t0}ms`);
363
+ return false;
364
+ }
365
+ const tokens = await res.json();
366
+ const accessClaims = decodeJwtPayload(tokens.access_token);
367
+ const idClaims = tokens.id_token ? decodeJwtPayload(tokens.id_token) : null;
368
+ const expiresAt = getExpiryFromJwt(accessClaims) ?? getExpiryFromJwt(idClaims) ?? Date.now() + tokens.expires_in * 1000;
369
+ account.accessToken = tokens.access_token;
370
+ if (tokens.refresh_token)
371
+ account.refreshToken = tokens.refresh_token;
372
+ if (tokens.id_token)
373
+ account.idToken = tokens.id_token;
374
+ account.expiresAt = expiresAt;
375
+ account.lastRefresh = new Date().toISOString();
376
+ account.accountId = getAccountIdFromJwt(idClaims) ?? getAccountIdFromJwt(accessClaims) ?? account.accountId;
377
+ account.authInvalid = false;
378
+ await Bun.write(storePath, JSON.stringify(store, null, 2));
379
+ const daysLeft = ((expiresAt - Date.now()) / (24 * 60 * 60 * 1000)).toFixed(1);
380
+ console.log(`[proactiveRefresh] ${alias}: refreshed in ${Date.now() - t0}ms, new expiry in ${daysLeft}d`);
381
+ return true;
382
+ } catch (err) {
383
+ console.log(`[proactiveRefresh] ${alias}: error \u2014 ${err instanceof Error ? err.message : String(err)}`);
384
+ return false;
385
+ }
386
+ }
387
+ async function proactiveRefreshCodexTokens() {
388
+ const STORE_PATHS = [
389
+ join8(homedir6(), ".config", "opencode", "codex-multi-account-accounts.json"),
390
+ join8(homedir6(), ".config", "opencode", "codex-multi-accounts.json"),
391
+ join8(homedir6(), ".config", "oc-codex-multi-account", "accounts.json")
392
+ ];
393
+ let storePath = null;
394
+ let store = null;
395
+ for (const p of STORE_PATHS) {
396
+ try {
397
+ store = JSON.parse(await Bun.file(p).text());
398
+ storePath = p;
399
+ break;
400
+ } catch {
401
+ continue;
402
+ }
403
+ }
404
+ if (!store || !storePath)
405
+ return;
406
+ const accounts = store.accounts ?? {};
407
+ const now = Date.now();
408
+ let refreshed = 0;
409
+ for (const [alias, account] of Object.entries(accounts)) {
410
+ const expiresAt = typeof account.expiresAt === "number" ? account.expiresAt : 0;
411
+ const lastRefresh = typeof account.lastRefresh === "string" ? new Date(account.lastRefresh).getTime() : 0;
412
+ const timeSinceRefresh = now - lastRefresh;
413
+ const timeToExpiry = expiresAt - now;
414
+ if (timeToExpiry <= 0) {
415
+ console.log(`[proactiveRefresh] ${alias}: token expired, needs reauth`);
416
+ continue;
417
+ }
418
+ if (timeSinceRefresh < REFRESH_COOLDOWN_MS) {
419
+ const hoursAgo = (timeSinceRefresh / (60 * 60 * 1000)).toFixed(1);
420
+ console.log(`[proactiveRefresh] ${alias}: refreshed ${hoursAgo}h ago, skipping`);
421
+ continue;
422
+ }
423
+ const hoursStale = (timeSinceRefresh / (60 * 60 * 1000)).toFixed(1);
424
+ console.log(`[proactiveRefresh] ${alias}: ${hoursStale}h since refresh, refreshing\u2026`);
425
+ const ok = await refreshSingleCodexToken(alias, account, storePath, store);
426
+ if (ok)
427
+ refreshed++;
428
+ }
429
+ if (refreshed > 0) {
430
+ console.log(`[proactiveRefresh] refreshed ${refreshed} codex token(s)`);
431
+ }
432
+ }
244
433
  async function readCredentialFile(filename) {
245
434
  const filePath = join8(homedir6(), ".config", "opencode", filename);
246
435
  const text = await Bun.file(filePath).text();
247
436
  return JSON.parse(text);
248
437
  }
249
438
  async function spawnPluginCli(command, args, timeoutMs = 15000) {
250
- const localBin = `./node_modules/${command}/dist/cli.js`;
251
- const useLocal = await Bun.file(localBin).exists();
252
- if (!useLocal) {
253
- const existing = bunxBarrier.get(command);
254
- if (existing) {
255
- await existing.catch(() => {
256
- });
257
- return runPluginCli(command, args, timeoutMs, false);
439
+ const candidates = [
440
+ `./node_modules/${command}/dist/cli.js`,
441
+ join8(homedir6(), ".config", "opencode", "node_modules", command, "dist", "cli.js")
442
+ ];
443
+ let localBin = null;
444
+ for (const c of candidates) {
445
+ if (await Bun.file(c).exists()) {
446
+ localBin = c;
447
+ break;
258
448
  }
259
- const warmup = runPluginCli(command, args, timeoutMs, false);
260
- const barrier = warmup.then(() => {
449
+ }
450
+ const useLocal = localBin !== null;
451
+ if (!useLocal) {
452
+ const prev = bunxQueue.get(command) ?? Promise.resolve();
453
+ const run = prev.catch(() => {
454
+ }).then(() => runPluginCli(command, args, timeoutMs, null));
455
+ bunxQueue.set(command, run.then(() => {
261
456
  }, () => {
262
- });
263
- bunxBarrier.set(command, barrier);
264
- barrier.finally(() => bunxBarrier.delete(command));
265
- return warmup;
457
+ }));
458
+ return run;
266
459
  }
267
- return runPluginCli(command, args, timeoutMs, true);
460
+ return runPluginCli(command, args, timeoutMs, localBin);
268
461
  }
269
- async function runPluginCli(command, args, timeoutMs, useLocal) {
462
+ async function runPluginCli(command, args, timeoutMs, localBin) {
270
463
  const t0 = Date.now();
271
- const localBin = `./node_modules/${command}/dist/cli.js`;
272
- const cmd = useLocal ? ["bun", localBin, ...args] : ["bunx", `${command}@latest`, ...args];
273
- const cwd = useLocal ? undefined : join8(tmpdir(), `bunx-${crypto.randomUUID()}`);
464
+ const cmd = localBin ? ["bun", localBin, ...args] : ["bunx", `${command}@latest`, ...args];
465
+ const cwd = localBin ? undefined : join8(tmpdir(), `bunx-${crypto.randomUUID()}`);
274
466
  if (cwd)
275
467
  await Bun.$`mkdir -p ${cwd}`.quiet();
276
468
  console.log(`[spawnPluginCli] starting: ${cmd.join(" ")}`);
@@ -391,7 +583,7 @@ function reauthCliCommand(provider) {
391
583
  throw new Error(`Re-auth not supported for provider: ${provider}`);
392
584
  return cmd;
393
585
  }
394
- var isBun4, PROVIDER_SOURCE, bunxBarrier, REAUTH_PROVIDERS;
586
+ var isBun4, PROVIDER_SOURCE, CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token", CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", REFRESH_COOLDOWN_MS, bunxQueue, REAUTH_PROVIDERS;
395
587
  var init_plugin_adapters = __esm(() => {
396
588
  init_command_runner();
397
589
  init_config_service();
@@ -492,7 +684,8 @@ var init_plugin_adapters = __esm(() => {
492
684
  return { ok: true };
493
685
  }
494
686
  });
495
- bunxBarrier = new Map;
687
+ REFRESH_COOLDOWN_MS = 24 * 60 * 60 * 1000;
688
+ bunxQueue = new Map;
496
689
  registerCommand({
497
690
  id: "accounts.ping",
498
691
  timeoutMs: 30000,
@@ -527,19 +720,43 @@ var init_plugin_adapters = __esm(() => {
527
720
  };
528
721
  }
529
722
  case "codex": {
530
- ctx.log("info", "Calling oc-codex-multi-account ping\u2026");
531
- const result = await spawnPluginCli("oc-codex-multi-account", [
532
- "ping",
533
- input.alias
534
- ]);
535
- const status = String(result.status ?? "error");
536
- ctx.log("info", `Result: ${status}`);
537
- if (status === "ok") {
723
+ ctx.log("info", `Direct-pinging codex account "${input.alias}"\u2026`);
724
+ const direct = await directCodexPing(input.alias);
725
+ ctx.log("info", `Direct ping result: ${direct.status}`);
726
+ if (direct.status === "ok") {
538
727
  await clearStaleMetrics(input.provider, input.alias);
728
+ proactiveRefreshCodexTokens().catch(() => {
729
+ });
730
+ return { status: "ok", message: "pong" };
731
+ }
732
+ if (direct.status === "expired") {
733
+ ctx.log("info", "Token expired \u2014 trying plugin CLI for refresh\u2026");
734
+ try {
735
+ const result = await spawnPluginCli("oc-codex-multi-account", [
736
+ "ping",
737
+ input.alias
738
+ ]);
739
+ const cliStatus = String(result.status ?? "error");
740
+ ctx.log("info", `Plugin CLI result: ${cliStatus}`);
741
+ if (cliStatus === "ok") {
742
+ await clearStaleMetrics(input.provider, input.alias);
743
+ return { status: "ok", message: "pong (token refreshed)" };
744
+ }
745
+ return {
746
+ status: cliStatus,
747
+ message: String(result.error ?? "Token refresh failed")
748
+ };
749
+ } catch (cliErr) {
750
+ ctx.log("info", `Plugin CLI failed: ${cliErr instanceof Error ? cliErr.message : String(cliErr)}`);
751
+ return {
752
+ status: "error",
753
+ message: direct.error ?? "Token expired and plugin refresh failed"
754
+ };
755
+ }
539
756
  }
540
757
  return {
541
- status,
542
- message: status === "ok" ? "pong" : String(result.error ?? "unknown error")
758
+ status: "error",
759
+ message: direct.error ?? "unknown error"
543
760
  };
544
761
  }
545
762
  case "antigravity": {
@@ -4880,10 +5097,10 @@ async function createCliRenderer(config = {}) {
4880
5097
  await renderer.setupTerminal();
4881
5098
  return renderer;
4882
5099
  }
4883
- var __defProp = Object.defineProperty;
4884
- var __export = (target, all) => {
5100
+ var __defProp2 = Object.defineProperty;
5101
+ var __export2 = (target, all) => {
4885
5102
  for (var name in all)
4886
- __defProp(target, name, {
5103
+ __defProp2(target, name, {
4887
5104
  get: all[name],
4888
5105
  enumerable: true,
4889
5106
  configurable: true,
@@ -4892,7 +5109,7 @@ var __export = (target, all) => {
4892
5109
  };
4893
5110
  var __require = import.meta.require;
4894
5111
  var exports_src = {};
4895
- __export(exports_src, {
5112
+ __export2(exports_src, {
4896
5113
  default: () => src_default,
4897
5114
  Wrap: () => Wrap,
4898
5115
  Unit: () => Unit,
@@ -30219,6 +30436,9 @@ function ensureActionsRegistered() {
30219
30436
  var isBun5 = typeof globalThis.Bun !== "undefined";
30220
30437
  var registered = false;
30221
30438
 
30439
+ // src/commander/server.ts
30440
+ init_plugin_adapters();
30441
+
30222
30442
  // src/commander/services/app-init-service.ts
30223
30443
  init_command_runner();
30224
30444
  import { homedir as homedir7 } from "os";
@@ -30742,6 +30962,8 @@ async function runCommanderServer(args) {
30742
30962
  });
30743
30963
  const serverUrl = `http://${hostname}:${port}`;
30744
30964
  console.log(`Commander ready at ${serverUrl}`);
30965
+ proactiveRefreshCodexTokens().catch(() => {
30966
+ });
30745
30967
  if (isBun7 && !process.env.NO_OPEN) {
30746
30968
  const cmd = process.platform === "win32" ? ["cmd", "/c", "start", serverUrl] : process.platform === "darwin" ? ["open", serverUrl] : ["xdg-open", serverUrl];
30747
30969
  Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] });
@@ -30887,4 +31109,4 @@ async function main2() {
30887
31109
  var WATCH_INTERVAL_MS = 5 * 60 * 1000;
30888
31110
  main2().catch(console.error);
30889
31111
 
30890
- //# debugId=2B2A0EC747EBB71864756E2164756E21
31112
+ //# debugId=DF21C38AB467E5FE64756E2164756E21