opencode-dux 1.1.1 → 1.2.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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Codex quota API scraper.
3
+ *
4
+ * Fetches usage data from the Codex (chatgpt.com) WHAM usage API using Bearer
5
+ * token authentication. Returns structured quota data including usage windows
6
+ * and credit balance.
7
+ */
8
+
9
+ import type { CodexUsageEntry, UsageWindow } from './types';
10
+
11
+ const CODEX_WHAM_URL = 'https://chatgpt.com/backend-api/wham/usage';
12
+
13
+ const EMPTY_WINDOW: UsageWindow = {
14
+ usagePercent: 0,
15
+ percentRemaining: 100,
16
+ resetInSec: 0,
17
+ resetTimeIso: '',
18
+ };
19
+
20
+ const EMPTY_CREDITS = { hasCredits: false, unlimited: false, balance: 0 };
21
+
22
+ function windowFromApi(
23
+ w: { used_percent: number; reset_at: number; limit_window_seconds: number } | undefined,
24
+ ): UsageWindow {
25
+ if (!w) return { ...EMPTY_WINDOW };
26
+ const usagePercent = Math.max(0, Math.min(100, w.used_percent));
27
+ return {
28
+ usagePercent,
29
+ percentRemaining: 100 - usagePercent,
30
+ resetInSec: Math.max(0, w.limit_window_seconds),
31
+ resetTimeIso: w.reset_at ? new Date(w.reset_at * 1000).toISOString() : '',
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Fetch Codex quota data via the WHAM usage API.
37
+ */
38
+ export async function scrapeCodexQuota(
39
+ accessToken: string,
40
+ signal?: AbortSignal,
41
+ ): Promise<CodexUsageEntry> {
42
+ const accountName = ''; // filled by caller
43
+
44
+ try {
45
+ const res = await fetch(CODEX_WHAM_URL, {
46
+ headers: {
47
+ Authorization: `Bearer ${accessToken}`,
48
+ Accept: 'application/json',
49
+ },
50
+ signal,
51
+ });
52
+
53
+ if (!res.ok) {
54
+ return {
55
+ provider: 'codex',
56
+ accountName,
57
+ fetchedAt: Date.now(),
58
+ error: `Codex API returned ${res.status} ${res.statusText}`,
59
+ primaryWindow: { ...EMPTY_WINDOW },
60
+ secondaryWindow: { ...EMPTY_WINDOW },
61
+ credits: { ...EMPTY_CREDITS },
62
+ };
63
+ }
64
+
65
+ const data = await res.json() as {
66
+ plan_type?: string;
67
+ rate_limit?: {
68
+ primary_window?: { used_percent: number; reset_at: number; limit_window_seconds: number };
69
+ secondary_window?: { used_percent: number; reset_at: number; limit_window_seconds: number };
70
+ };
71
+ credits?: { has_credits: boolean; unlimited: boolean; balance: number };
72
+ };
73
+
74
+ return {
75
+ provider: 'codex',
76
+ accountName,
77
+ fetchedAt: Date.now(),
78
+ primaryWindow: windowFromApi(data.rate_limit?.primary_window),
79
+ secondaryWindow: windowFromApi(data.rate_limit?.secondary_window),
80
+ credits: {
81
+ hasCredits: data.credits?.has_credits ?? false,
82
+ unlimited: data.credits?.unlimited ?? false,
83
+ balance: data.credits?.balance ?? 0,
84
+ },
85
+ };
86
+ } catch (err: unknown) {
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ return {
89
+ provider: 'codex',
90
+ accountName,
91
+ fetchedAt: Date.now(),
92
+ error: `Codex fetch failed: ${message}`,
93
+ primaryWindow: { ...EMPTY_WINDOW },
94
+ secondaryWindow: { ...EMPTY_WINDOW },
95
+ credits: { ...EMPTY_CREDITS },
96
+ };
97
+ }
98
+ }
@@ -18,9 +18,11 @@ export {
18
18
  setAccountKey,
19
19
  updateAccountCookie,
20
20
  } from './accounts-store';
21
+ export { scrapeCodexQuota } from './codex-scraper';
21
22
  export { scrapeNeuralwattQuota } from './neuralwatt-scraper';
22
23
  export { scrapeQuota, scrapeUsagePage } from './opencode-go-scraper';
23
24
  export type {
25
+ CodexUsageEntry,
24
26
  NeuralwattUsageEntry,
25
27
  OpenCodeGoUsageEntry,
26
28
  SubscriptionUsageEntry,
@@ -1,145 +1,170 @@
1
- /**
2
- * Type definitions for multi-provider subscription usage tracking.
3
- *
4
- * Supports OpenCode Go (dashboard scraping) and Neuralwatt (REST API)
5
- * as discriminated unions on the `provider` field.
6
- */
7
-
8
- /** Provider discriminator. */
9
- export type SubscriptionProvider = 'opencode-go' | 'neuralwatt';
10
-
11
- // ── Account definitions (discriminated union) ──
12
-
13
- export interface OpenCodeGoAccount {
14
- provider: 'opencode-go';
15
- name: string;
16
- workspaceId: string;
17
- authCookie: string;
18
- apiKey?: string;
19
- }
20
-
21
- export interface NeuralwattAccount {
22
- provider: 'neuralwatt';
23
- name: string;
24
- apiKey: string;
25
- }
26
-
27
- export type StoredAccount = OpenCodeGoAccount | NeuralwattAccount;
28
-
29
- // ── Usage window (OpenCode Go) ──
30
-
31
- /** Per-time-window usage data scraped from the OpenCode Go dashboard. */
32
- export interface UsageWindow {
33
- /** Usage percentage [0..100] */
34
- usagePercent: number;
35
- /** Seconds until usage resets */
36
- resetInSec: number;
37
- /** Remaining percentage [0..100] */
38
- percentRemaining: number;
39
- /** ISO reset timestamp */
40
- resetTimeIso: string;
41
- }
42
-
43
- // ── OpenCode Go usage entry ──
44
-
45
- /** Snapshot entry per OpenCode Go account - stored in tui-state.json. */
46
- export interface OpenCodeGoUsageEntry {
47
- provider: 'opencode-go';
48
- /** Display name for this account (from config). */
49
- accountName: string;
50
- /** OpenCode Go workspace ID. */
51
- workspaceId: string;
52
- /** Rolling (~5h) usage window, when present. */
53
- rolling?: UsageWindow;
54
- /** Weekly usage window, when present. */
55
- weekly?: UsageWindow;
56
- /** Monthly usage window, when present. */
57
- monthly?: UsageWindow;
58
- /** Timestamp when data was fetched. */
59
- fetchedAt: number;
60
- /** Error message if the scrape failed for this account. */
61
- error?: string;
62
- }
63
-
64
- // ── Neuralwatt types ──
65
-
66
- export interface NeuralwattBalance {
67
- credits_remaining_usd: number;
68
- total_credits_usd: number;
69
- credits_used_usd: number;
70
- accounting_method: string;
71
- }
72
-
73
- export interface NeuralwattUsagePeriod {
74
- cost_usd: number;
75
- requests: number;
76
- tokens: number;
77
- energy_kwh: number;
78
- }
79
-
80
- export interface NeuralwattUsage {
81
- lifetime: NeuralwattUsagePeriod;
82
- current_month: NeuralwattUsagePeriod;
83
- }
84
-
85
- export interface NeuralwattSubscription {
86
- plan: string;
87
- status: 'active' | 'canceling' | 'past_due' | 'paused' | 'trialing';
88
- billing_interval: string | null;
89
- current_period_start: string | null;
90
- current_period_end: string | null;
91
- auto_renew: boolean | null;
92
- kwh_included: number | null;
93
- kwh_used: number | null;
94
- kwh_remaining: number | null;
95
- in_overage: boolean | null;
96
- }
97
-
98
- /** Snapshot entry per Neuralwatt account - stored in tui-state.json. */
99
- export interface NeuralwattUsageEntry {
100
- provider: 'neuralwatt';
101
- /** Display name for this account. */
102
- accountName: string;
103
- /** ISO timestamp from the API response. */
104
- snapshot_at: string;
105
- /** Credit balance. */
106
- balance: NeuralwattBalance;
107
- /** Usage data (lifetime + current month). */
108
- usage: NeuralwattUsage;
109
- /** Subscription details, null if no active subscription. */
110
- subscription: NeuralwattSubscription | null;
111
- /** Timestamp when data was fetched. */
112
- fetchedAt: number;
113
- /** Error message if the fetch failed. */
114
- error?: string;
115
- }
116
-
117
- // ── Unified usage entry ──
118
-
119
- export type SubscriptionUsageEntry =
120
- | OpenCodeGoUsageEntry
121
- | NeuralwattUsageEntry;
122
-
123
- // ── Detailed usage (OpenCode Go /usage page) ──
124
-
125
- /** Detailed usage data from the /usage page. */
126
- export interface UsageDetail {
127
- /** Total number of API calls. */
128
- totalCalls: number;
129
- /** Total estimated cost in USD. */
130
- totalCost: number;
131
- /** Per-model breakdown. */
132
- perModel: Array<{
133
- model: string;
134
- calls: number;
135
- cost: number;
136
- }>;
137
- }
138
-
139
- // ── Config ──
140
-
141
- /** Config for the subscription tracking feature. */
142
- export interface SubscriptionsConfig {
143
- /** Minimum interval between auto-refreshes in ms (default: 60000). */
144
- refreshIntervalMs: number;
145
- }
1
+ /**
2
+ * Type definitions for multi-provider subscription usage tracking.
3
+ *
4
+ * Supports OpenCode Go (dashboard scraping) and Neuralwatt (REST API)
5
+ * as discriminated unions on the `provider` field.
6
+ */
7
+
8
+ /** Provider discriminator. */
9
+ export type SubscriptionProvider = 'opencode-go' | 'neuralwatt' | 'codex';
10
+
11
+ // ── Account definitions (discriminated union) ──
12
+
13
+ export interface OpenCodeGoAccount {
14
+ provider: 'opencode-go';
15
+ name: string;
16
+ workspaceId: string;
17
+ authCookie: string;
18
+ apiKey?: string;
19
+ }
20
+
21
+ export interface NeuralwattAccount {
22
+ provider: 'neuralwatt';
23
+ name: string;
24
+ apiKey: string;
25
+ }
26
+
27
+ export interface CodexAccount {
28
+ provider: 'codex';
29
+ name: string;
30
+ accessToken: string;
31
+ refreshToken?: string;
32
+ }
33
+
34
+ export type StoredAccount = OpenCodeGoAccount | NeuralwattAccount | CodexAccount;
35
+
36
+ // ── Usage window (OpenCode Go) ──
37
+
38
+ /** Per-time-window usage data scraped from the OpenCode Go dashboard. */
39
+ export interface UsageWindow {
40
+ /** Usage percentage [0..100] */
41
+ usagePercent: number;
42
+ /** Seconds until usage resets */
43
+ resetInSec: number;
44
+ /** Remaining percentage [0..100] */
45
+ percentRemaining: number;
46
+ /** ISO reset timestamp */
47
+ resetTimeIso: string;
48
+ }
49
+
50
+ // ── OpenCode Go usage entry ──
51
+
52
+ /** Snapshot entry per OpenCode Go account - stored in tui-state.json. */
53
+ export interface OpenCodeGoUsageEntry {
54
+ provider: 'opencode-go';
55
+ /** Display name for this account (from config). */
56
+ accountName: string;
57
+ /** OpenCode Go workspace ID. */
58
+ workspaceId: string;
59
+ /** Rolling (~5h) usage window, when present. */
60
+ rolling?: UsageWindow;
61
+ /** Weekly usage window, when present. */
62
+ weekly?: UsageWindow;
63
+ /** Monthly usage window, when present. */
64
+ monthly?: UsageWindow;
65
+ /** Timestamp when data was fetched. */
66
+ fetchedAt: number;
67
+ /** Error message if the scrape failed for this account. */
68
+ error?: string;
69
+ }
70
+
71
+ // ── Neuralwatt types ──
72
+
73
+ export interface NeuralwattBalance {
74
+ credits_remaining_usd: number;
75
+ total_credits_usd: number;
76
+ credits_used_usd: number;
77
+ accounting_method: string;
78
+ }
79
+
80
+ export interface NeuralwattUsagePeriod {
81
+ cost_usd: number;
82
+ requests: number;
83
+ tokens: number;
84
+ energy_kwh: number;
85
+ }
86
+
87
+ export interface NeuralwattUsage {
88
+ lifetime: NeuralwattUsagePeriod;
89
+ current_month: NeuralwattUsagePeriod;
90
+ }
91
+
92
+ export interface NeuralwattSubscription {
93
+ plan: string;
94
+ status: 'active' | 'canceling' | 'past_due' | 'paused' | 'trialing';
95
+ billing_interval: string | null;
96
+ current_period_start: string | null;
97
+ current_period_end: string | null;
98
+ auto_renew: boolean | null;
99
+ kwh_included: number | null;
100
+ kwh_used: number | null;
101
+ kwh_remaining: number | null;
102
+ in_overage: boolean | null;
103
+ }
104
+
105
+ /** Snapshot entry per Neuralwatt account - stored in tui-state.json. */
106
+ export interface NeuralwattUsageEntry {
107
+ provider: 'neuralwatt';
108
+ /** Display name for this account. */
109
+ accountName: string;
110
+ /** ISO timestamp from the API response. */
111
+ snapshot_at: string;
112
+ /** Credit balance. */
113
+ balance: NeuralwattBalance;
114
+ /** Usage data (lifetime + current month). */
115
+ usage: NeuralwattUsage;
116
+ /** Subscription details, null if no active subscription. */
117
+ subscription: NeuralwattSubscription | null;
118
+ /** Timestamp when data was fetched. */
119
+ fetchedAt: number;
120
+ /** Error message if the fetch failed. */
121
+ error?: string;
122
+ }
123
+
124
+ // ── Codex usage entry ──
125
+
126
+ export interface CodexUsageEntry {
127
+ provider: 'codex';
128
+ accountName: string;
129
+ fetchedAt: number;
130
+ error?: string;
131
+ /** 5-hour rolling window (primary_window from API) */
132
+ primaryWindow: UsageWindow;
133
+ /** 7-day rolling window (secondary_window from API) */
134
+ secondaryWindow: UsageWindow;
135
+ /** Credit balance info */
136
+ credits: {
137
+ hasCredits: boolean;
138
+ unlimited: boolean;
139
+ balance: number;
140
+ };
141
+ }
142
+
143
+ export type SubscriptionUsageEntry =
144
+ | OpenCodeGoUsageEntry
145
+ | NeuralwattUsageEntry
146
+ | CodexUsageEntry;
147
+
148
+ // ── Detailed usage (OpenCode Go /usage page) ──
149
+
150
+ /** Detailed usage data from the /usage page. */
151
+ export interface UsageDetail {
152
+ /** Total number of API calls. */
153
+ totalCalls: number;
154
+ /** Total estimated cost in USD. */
155
+ totalCost: number;
156
+ /** Per-model breakdown. */
157
+ perModel: Array<{
158
+ model: string;
159
+ calls: number;
160
+ cost: number;
161
+ }>;
162
+ }
163
+
164
+ // ── Config ──
165
+
166
+ /** Config for the subscription tracking feature. */
167
+ export interface SubscriptionsConfig {
168
+ /** Minimum interval between auto-refreshes in ms (default: 60000). */
169
+ refreshIntervalMs: number;
170
+ }
@@ -29,6 +29,7 @@ import {
29
29
  setAccountKey,
30
30
  updateAccountCookie,
31
31
  } from './accounts-store';
32
+ import { scrapeCodexQuota } from './codex-scraper';
32
33
  import { scrapeNeuralwattQuota } from './neuralwatt-scraper';
33
34
  import { scrapeQuota } from './opencode-go-scraper';
34
35
  import type { SubscriptionProvider, SubscriptionUsageEntry } from './types';
@@ -36,12 +37,12 @@ import type { SubscriptionProvider, SubscriptionUsageEntry } from './types';
36
37
  const SUBSCRIPTIONS_COMMAND = 'subscriptions';
37
38
  const DEFAULT_REFRESH_INTERVAL_MS = 60_000;
38
39
  const DEFAULT_PERIODIC_INTERVAL_MS = 600_000; // 10 minutes
39
- const PROVIDERS: SubscriptionProvider[] = ['opencode-go', 'neuralwatt'];
40
+ const PROVIDERS: SubscriptionProvider[] = ['opencode-go', 'neuralwatt', 'codex'];
40
41
 
41
42
  function parseProvider(
42
43
  raw: string | undefined,
43
44
  ): SubscriptionProvider | undefined {
44
- if (raw === 'opencode-go' || raw === 'neuralwatt') return raw;
45
+ if (raw === 'opencode-go' || raw === 'neuralwatt' || raw === 'codex') return raw;
45
46
  return undefined;
46
47
  }
47
48
 
@@ -146,6 +147,35 @@ export class UsageService {
146
147
  );
147
148
  entry.accountName = account.name;
148
149
  return entry as SubscriptionUsageEntry;
150
+ } else if (account.provider === 'codex') {
151
+ if (!account.accessToken?.trim()) {
152
+ return {
153
+ provider: 'codex',
154
+ accountName: account.name,
155
+ fetchedAt: Date.now(),
156
+ error:
157
+ 'Missing Codex access token. Re-add with /subscriptions add-codex.',
158
+ primaryWindow: {
159
+ usagePercent: 0,
160
+ percentRemaining: 100,
161
+ resetInSec: 0,
162
+ resetTimeIso: '',
163
+ },
164
+ secondaryWindow: {
165
+ usagePercent: 0,
166
+ percentRemaining: 100,
167
+ resetInSec: 0,
168
+ resetTimeIso: '',
169
+ },
170
+ credits: { hasCredits: false, unlimited: false, balance: 0 },
171
+ } as SubscriptionUsageEntry;
172
+ }
173
+ const entry = await scrapeCodexQuota(
174
+ account.accessToken,
175
+ controller.signal,
176
+ );
177
+ entry.accountName = account.name;
178
+ return entry as SubscriptionUsageEntry;
149
179
  } else {
150
180
  // neuralwatt
151
181
  if (!account.apiKey?.trim()) {
@@ -235,10 +265,11 @@ export class UsageService {
235
265
  const key = auth[provider]?.key;
236
266
  const match =
237
267
  typeof key === 'string' && key.length > 0
238
- ? accounts.find(
239
- (account) =>
240
- account.provider === provider && account.apiKey === key,
241
- )
268
+ ? accounts.find((account): boolean => {
269
+ if (account.provider !== provider) return false;
270
+ if (account.provider === 'codex') return account.accessToken === key;
271
+ return account.apiKey === key;
272
+ })
242
273
  : undefined;
243
274
  if (match) {
244
275
  activeByProvider[provider] = match.name;
@@ -367,6 +398,26 @@ export class UsageService {
367
398
  break;
368
399
  }
369
400
 
401
+ case 'add-codex': {
402
+ const [_, name, ...tokenParts] = parts;
403
+ const accessToken = tokenParts.join(' ');
404
+ if (!name || !accessToken) {
405
+ output.parts.push(
406
+ createInternalAgentTextPart(
407
+ 'Usage: /subscriptions add-codex <name> <access-token>\n' +
408
+ 'Example: /subscriptions add-codex my-codex eyJhbGci...',
409
+ ),
410
+ );
411
+ return;
412
+ }
413
+ saveAccount({ provider: 'codex', name, accessToken });
414
+ this.refresh(true).catch(() => {});
415
+ output.parts.push(
416
+ createInternalAgentTextPart(`✅ Added Codex account "${name}".`),
417
+ );
418
+ break;
419
+ }
420
+
370
421
  case 'remove':
371
422
  case 'rm': {
372
423
  const [_, name] = parts;
@@ -439,7 +490,7 @@ export class UsageService {
439
490
  if (accounts.length === 0) {
440
491
  output.parts.push(
441
492
  createInternalAgentTextPart(
442
- 'No accounts configured. Use /subscriptions add-opencode-go or /subscriptions add-neuralwatt to add one.',
493
+ 'No accounts configured. Use /subscriptions add-opencode-go, /subscriptions add-neuralwatt, or /subscriptions add-codex to add one.',
443
494
  ),
444
495
  );
445
496
  return;
@@ -449,11 +500,17 @@ export class UsageService {
449
500
  const isActive = activeByProvider[acct.provider] === acct.name;
450
501
  const star = isActive ? '★ ' : ' ';
451
502
  const providerLabel =
452
- acct.provider === 'opencode-go' ? 'OpenCode Go' : 'Neuralwatt';
503
+ acct.provider === 'opencode-go'
504
+ ? 'OpenCode Go'
505
+ : acct.provider === 'codex'
506
+ ? 'Codex'
507
+ : 'Neuralwatt';
453
508
  lines.push(`${star}${acct.name} (${providerLabel})`);
454
509
  if (acct.provider === 'opencode-go') {
455
510
  lines.push(` workspace: ${acct.workspaceId}`);
456
511
  lines.push(` cookie: ${maskCookie(acct.authCookie)}`);
512
+ } else if (acct.provider === 'codex') {
513
+ lines.push(` access-token: ${maskCookie(acct.accessToken)}`);
457
514
  } else {
458
515
  lines.push(` api-key: ${maskCookie(acct.apiKey)}`);
459
516
  }
@@ -473,6 +530,7 @@ export class UsageService {
473
530
  ' /subscriptions add-opencode-go <name> <workspace-id> <auth-cookie>',
474
531
  );
475
532
  lines.push(' /subscriptions add-neuralwatt <name> <api-key>');
533
+ lines.push(' /subscriptions add-codex <name> <access-token>');
476
534
  lines.push(' /subscriptions remove <name>');
477
535
  lines.push(' /subscriptions edit <name> <new-auth-cookie>');
478
536
  lines.push(' /subscriptions set-key <name> <api-key>');
@@ -520,7 +578,7 @@ export class UsageService {
520
578
  output.parts.push(
521
579
  createInternalAgentTextPart(
522
580
  'Usage: /subscriptions switch <provider> <name>\n' +
523
- 'Providers: opencode-go, neuralwatt\n' +
581
+ 'Providers: opencode-go, neuralwatt, codex\n' +
524
582
  'Example: /subscriptions switch opencode-go personal',
525
583
  ),
526
584
  );
@@ -538,13 +596,24 @@ export class UsageService {
538
596
  );
539
597
  return;
540
598
  }
541
- if (!account.apiKey) {
542
- output.parts.push(
543
- createInternalAgentTextPart(
544
- `Account "${name}" has no API key set. Use /subscriptions set-key ${name} <api-key> first.`,
545
- ),
546
- );
547
- return;
599
+ if (account.provider === 'codex') {
600
+ if (!account.accessToken) {
601
+ output.parts.push(
602
+ createInternalAgentTextPart(
603
+ `Account "${name}" has no access token set. Use /subscriptions add-codex to re-add.`,
604
+ ),
605
+ );
606
+ return;
607
+ }
608
+ } else {
609
+ if (!account.apiKey) {
610
+ output.parts.push(
611
+ createInternalAgentTextPart(
612
+ `Account "${name}" has no API key set. Use /subscriptions set-key ${name} <api-key> first.`,
613
+ ),
614
+ );
615
+ return;
616
+ }
548
617
  }
549
618
  const activeByProvider = this.syncActiveAccounts();
550
619
  // No-op if already active for this provider
@@ -558,9 +627,13 @@ export class UsageService {
558
627
  }
559
628
  try {
560
629
  // Write the API key to OpenCode auth.json via SDK's auth.set()
630
+ const key: string =
631
+ account.provider === 'codex'
632
+ ? account.accessToken
633
+ : (account.apiKey ?? '');
561
634
  await this.client.auth.set({
562
635
  path: { id: account.provider },
563
- body: { type: 'api', key: account.apiKey },
636
+ body: { type: 'api', key },
564
637
  });
565
638
  } catch {
566
639
  output.parts.push(
@@ -605,6 +678,7 @@ export class UsageService {
605
678
  'Commands:\n' +
606
679
  ' /subscriptions add-opencode-go <name> <workspace-id> <auth-cookie> Add an OpenCode Go account\n' +
607
680
  ' /subscriptions add-neuralwatt <name> <api-key> Add a Neuralwatt account\n' +
681
+ ' /subscriptions add-codex <name> <access-token> Add a Codex account\n' +
608
682
  ' /subscriptions remove <name> Remove an account\n' +
609
683
  ' /subscriptions edit <name> <new-auth-cookie> Update auth cookie (OpenCode Go)\n' +
610
684
  ' /subscriptions set-key <name> <api-key> Set API key for switching\n' +
@@ -634,7 +708,7 @@ export class UsageService {
634
708
  SUBSCRIPTIONS_COMMAND
635
709
  ] = {
636
710
  template:
637
- 'Manage subscription accounts (add-opencode-go, add-neuralwatt, remove, list, edit, set-key, switch, refresh)',
711
+ 'Manage subscription accounts (add-opencode-go, add-neuralwatt, add-codex, remove, list, edit, set-key, switch, refresh)',
638
712
  description:
639
713
  'Add, remove, list, edit, set-key, switch, or refresh subscription accounts for usage tracking in the sidebar',
640
714
  };
package/src/tui-state.ts CHANGED
@@ -616,7 +616,7 @@ function parseSnapshot(value: string): TuiSnapshot | null {
616
616
  Record<SubscriptionProvider, string>
617
617
  > = {};
618
618
  if (parsed.activeSubscriptionByProvider) {
619
- for (const provider of ['opencode-go', 'neuralwatt'] as const) {
619
+ for (const provider of ['opencode-go', 'neuralwatt', 'codex'] as const) {
620
620
  const name = parsed.activeSubscriptionByProvider[provider];
621
621
  if (typeof name === 'string' && name.length > 0) {
622
622
  activeSubscriptionByProvider[provider] = name;