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.
- package/README.md +5 -3
- package/dist/index.js +145 -18
- package/dist/subscriptions/codex-scraper.d.ts +12 -0
- package/dist/subscriptions/index.d.ts +2 -1
- package/dist/subscriptions/types.d.ts +25 -3
- package/dist/tui.js +42 -2
- package/package.json +1 -1
- package/src/hooks/auto-update-checker/index.test.ts +8 -10
- package/src/hooks/auto-update-checker/index.ts +10 -10
- package/src/subscriptions/accounts-store.ts +12 -2
- package/src/subscriptions/codex-scraper.ts +98 -0
- package/src/subscriptions/index.ts +2 -0
- package/src/subscriptions/types.ts +170 -145
- package/src/subscriptions/usage-service.ts +92 -18
- package/src/tui-state.ts +1 -1
- package/src/tui.ts +62 -1
package/README.md
CHANGED
|
@@ -120,9 +120,10 @@ Merged from two locations (project overrides user):
|
|
|
120
120
|
Manage API accounts directly from the OpenCode prompt via `/subscriptions`:
|
|
121
121
|
|
|
122
122
|
- `/subscriptions list` — View all accounts and their usage
|
|
123
|
-
- `/subscriptions add
|
|
124
|
-
- `/subscriptions add
|
|
125
|
-
- `/subscriptions
|
|
123
|
+
- `/subscriptions add-opencode-go <name> <workspace-id>` — Add OpenCode Go account
|
|
124
|
+
- `/subscriptions add-neuralwatt <name> <api-key>` — Add Neuralwatt account
|
|
125
|
+
- `/subscriptions add-codex <name> <access-token>` — Add Codex (OpenAI) account
|
|
126
|
+
- `/subscriptions switch <provider> <name>` — Activate an account for a provider
|
|
126
127
|
- `/subscriptions remove <name>` — Delete an account
|
|
127
128
|
- `/subscriptions refresh` — Force refresh usage data
|
|
128
129
|
|
|
@@ -132,6 +133,7 @@ Manage API accounts directly from the OpenCode prompt via `/subscriptions`:
|
|
|
132
133
|
| --------------- | ----------------------------------------------------- | -------------------------- |
|
|
133
134
|
| **OpenCode Go** | Dashboard scraping (rolling, weekly, monthly windows) | Workspace ID + auth cookie |
|
|
134
135
|
| **Neuralwatt** | REST API (credits, kWh, token usage) | API key |
|
|
136
|
+
| **Codex** | REST API (5H/7D rate limits, credits) | OAuth access token |
|
|
135
137
|
|
|
136
138
|
Usage data appears in the TUI sidebar under **API Usage**.
|
|
137
139
|
|
package/dist/index.js
CHANGED
|
@@ -23595,29 +23595,29 @@ async function runBackgroundUpdateCheck(ctx, autoUpdate) {
|
|
|
23595
23595
|
}
|
|
23596
23596
|
log(`[auto-update-checker] Update available (${channel}): ${currentVersion} → ${latestVersion}`);
|
|
23597
23597
|
if (pluginInfo.isPinned) {
|
|
23598
|
-
showToast(ctx, `
|
|
23598
|
+
showToast(ctx, `Opencode Dux ${latestVersion}`, `Update from v${currentVersion} to v${latestVersion} available.
|
|
23599
23599
|
Version is pinned. Update your plugin config to apply.`, "info", 8000);
|
|
23600
23600
|
log(`[auto-update-checker] Version is pinned; skipping auto-update.`);
|
|
23601
23601
|
return;
|
|
23602
23602
|
}
|
|
23603
23603
|
if (!autoUpdate) {
|
|
23604
|
-
showToast(ctx, `
|
|
23604
|
+
showToast(ctx, `Opencode Dux ${latestVersion}`, `Update from v${currentVersion} to v${latestVersion} available. Auto-update is disabled.`, "info", 8000);
|
|
23605
23605
|
log("[auto-update-checker] Auto-update disabled, notification only");
|
|
23606
23606
|
return;
|
|
23607
23607
|
}
|
|
23608
23608
|
const installDir = preparePackageUpdate(latestVersion, PACKAGE_NAME);
|
|
23609
23609
|
if (!installDir) {
|
|
23610
|
-
showToast(ctx, `
|
|
23610
|
+
showToast(ctx, `Opencode Dux ${latestVersion}`, `Update from v${currentVersion} to v${latestVersion} available. Auto-update could not prepare the active install.`, "info", 8000);
|
|
23611
23611
|
log("[auto-update-checker] Failed to prepare install root for auto-update");
|
|
23612
23612
|
return;
|
|
23613
23613
|
}
|
|
23614
23614
|
const installSuccess = await runBunInstallSafe(installDir);
|
|
23615
23615
|
if (installSuccess) {
|
|
23616
|
-
showToast(ctx, "
|
|
23616
|
+
showToast(ctx, "Opencode Dux Updated!", `Updated from v${currentVersion} to v${latestVersion}
|
|
23617
23617
|
Restart OpenCode to apply.`, "success", 8000);
|
|
23618
23618
|
log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`);
|
|
23619
23619
|
} else {
|
|
23620
|
-
showToast(ctx, `
|
|
23620
|
+
showToast(ctx, `Opencode Dux ${latestVersion}`, `Update from v${currentVersion} to v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`, "error", 8000);
|
|
23621
23621
|
log("[auto-update-checker] bun install failed; update not installed");
|
|
23622
23622
|
}
|
|
23623
23623
|
}
|
|
@@ -24677,7 +24677,7 @@ function parseSnapshot(value) {
|
|
|
24677
24677
|
return null;
|
|
24678
24678
|
const activeSubscriptionByProvider = {};
|
|
24679
24679
|
if (parsed.activeSubscriptionByProvider) {
|
|
24680
|
-
for (const provider of ["opencode-go", "neuralwatt"]) {
|
|
24680
|
+
for (const provider of ["opencode-go", "neuralwatt", "codex"]) {
|
|
24681
24681
|
const name = parsed.activeSubscriptionByProvider[provider];
|
|
24682
24682
|
if (typeof name === "string" && name.length > 0) {
|
|
24683
24683
|
activeSubscriptionByProvider[provider] = name;
|
|
@@ -26978,10 +26978,81 @@ function setAccountKey(name, provider, apiKey) {
|
|
|
26978
26978
|
if (!account)
|
|
26979
26979
|
return false;
|
|
26980
26980
|
account.provider = provider;
|
|
26981
|
-
account.
|
|
26981
|
+
if (account.provider === "codex") {
|
|
26982
|
+
account.accessToken = apiKey;
|
|
26983
|
+
} else {
|
|
26984
|
+
account.apiKey = apiKey;
|
|
26985
|
+
}
|
|
26982
26986
|
writeAccountsFile(file);
|
|
26983
26987
|
return true;
|
|
26984
26988
|
}
|
|
26989
|
+
// src/subscriptions/codex-scraper.ts
|
|
26990
|
+
var CODEX_WHAM_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
26991
|
+
var EMPTY_WINDOW = {
|
|
26992
|
+
usagePercent: 0,
|
|
26993
|
+
percentRemaining: 100,
|
|
26994
|
+
resetInSec: 0,
|
|
26995
|
+
resetTimeIso: ""
|
|
26996
|
+
};
|
|
26997
|
+
var EMPTY_CREDITS = { hasCredits: false, unlimited: false, balance: 0 };
|
|
26998
|
+
function windowFromApi(w) {
|
|
26999
|
+
if (!w)
|
|
27000
|
+
return { ...EMPTY_WINDOW };
|
|
27001
|
+
const usagePercent = Math.max(0, Math.min(100, w.used_percent));
|
|
27002
|
+
return {
|
|
27003
|
+
usagePercent,
|
|
27004
|
+
percentRemaining: 100 - usagePercent,
|
|
27005
|
+
resetInSec: Math.max(0, w.limit_window_seconds),
|
|
27006
|
+
resetTimeIso: w.reset_at ? new Date(w.reset_at * 1000).toISOString() : ""
|
|
27007
|
+
};
|
|
27008
|
+
}
|
|
27009
|
+
async function scrapeCodexQuota(accessToken, signal) {
|
|
27010
|
+
const accountName = "";
|
|
27011
|
+
try {
|
|
27012
|
+
const res = await fetch(CODEX_WHAM_URL, {
|
|
27013
|
+
headers: {
|
|
27014
|
+
Authorization: `Bearer ${accessToken}`,
|
|
27015
|
+
Accept: "application/json"
|
|
27016
|
+
},
|
|
27017
|
+
signal
|
|
27018
|
+
});
|
|
27019
|
+
if (!res.ok) {
|
|
27020
|
+
return {
|
|
27021
|
+
provider: "codex",
|
|
27022
|
+
accountName,
|
|
27023
|
+
fetchedAt: Date.now(),
|
|
27024
|
+
error: `Codex API returned ${res.status} ${res.statusText}`,
|
|
27025
|
+
primaryWindow: { ...EMPTY_WINDOW },
|
|
27026
|
+
secondaryWindow: { ...EMPTY_WINDOW },
|
|
27027
|
+
credits: { ...EMPTY_CREDITS }
|
|
27028
|
+
};
|
|
27029
|
+
}
|
|
27030
|
+
const data = await res.json();
|
|
27031
|
+
return {
|
|
27032
|
+
provider: "codex",
|
|
27033
|
+
accountName,
|
|
27034
|
+
fetchedAt: Date.now(),
|
|
27035
|
+
primaryWindow: windowFromApi(data.rate_limit?.primary_window),
|
|
27036
|
+
secondaryWindow: windowFromApi(data.rate_limit?.secondary_window),
|
|
27037
|
+
credits: {
|
|
27038
|
+
hasCredits: data.credits?.has_credits ?? false,
|
|
27039
|
+
unlimited: data.credits?.unlimited ?? false,
|
|
27040
|
+
balance: data.credits?.balance ?? 0
|
|
27041
|
+
}
|
|
27042
|
+
};
|
|
27043
|
+
} catch (err) {
|
|
27044
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27045
|
+
return {
|
|
27046
|
+
provider: "codex",
|
|
27047
|
+
accountName,
|
|
27048
|
+
fetchedAt: Date.now(),
|
|
27049
|
+
error: `Codex fetch failed: ${message}`,
|
|
27050
|
+
primaryWindow: { ...EMPTY_WINDOW },
|
|
27051
|
+
secondaryWindow: { ...EMPTY_WINDOW },
|
|
27052
|
+
credits: { ...EMPTY_CREDITS }
|
|
27053
|
+
};
|
|
27054
|
+
}
|
|
27055
|
+
}
|
|
26985
27056
|
// src/subscriptions/neuralwatt-scraper.ts
|
|
26986
27057
|
var NEURALWATT_QUOTA_URL = "https://api.neuralwatt.com/v1/quota";
|
|
26987
27058
|
var EMPTY_BALANCE = {
|
|
@@ -27166,9 +27237,9 @@ import * as path14 from "node:path";
|
|
|
27166
27237
|
var SUBSCRIPTIONS_COMMAND = "subscriptions";
|
|
27167
27238
|
var DEFAULT_REFRESH_INTERVAL_MS = 60000;
|
|
27168
27239
|
var DEFAULT_PERIODIC_INTERVAL_MS = 600000;
|
|
27169
|
-
var PROVIDERS = ["opencode-go", "neuralwatt"];
|
|
27240
|
+
var PROVIDERS = ["opencode-go", "neuralwatt", "codex"];
|
|
27170
27241
|
function parseProvider(raw) {
|
|
27171
|
-
if (raw === "opencode-go" || raw === "neuralwatt")
|
|
27242
|
+
if (raw === "opencode-go" || raw === "neuralwatt" || raw === "codex")
|
|
27172
27243
|
return raw;
|
|
27173
27244
|
return;
|
|
27174
27245
|
}
|
|
@@ -27247,6 +27318,31 @@ class UsageService {
|
|
|
27247
27318
|
const entry = await scrapeQuota(account.workspaceId, account.authCookie, controller.signal);
|
|
27248
27319
|
entry.accountName = account.name;
|
|
27249
27320
|
return entry;
|
|
27321
|
+
} else if (account.provider === "codex") {
|
|
27322
|
+
if (!account.accessToken?.trim()) {
|
|
27323
|
+
return {
|
|
27324
|
+
provider: "codex",
|
|
27325
|
+
accountName: account.name,
|
|
27326
|
+
fetchedAt: Date.now(),
|
|
27327
|
+
error: "Missing Codex access token. Re-add with /subscriptions add-codex.",
|
|
27328
|
+
primaryWindow: {
|
|
27329
|
+
usagePercent: 0,
|
|
27330
|
+
percentRemaining: 100,
|
|
27331
|
+
resetInSec: 0,
|
|
27332
|
+
resetTimeIso: ""
|
|
27333
|
+
},
|
|
27334
|
+
secondaryWindow: {
|
|
27335
|
+
usagePercent: 0,
|
|
27336
|
+
percentRemaining: 100,
|
|
27337
|
+
resetInSec: 0,
|
|
27338
|
+
resetTimeIso: ""
|
|
27339
|
+
},
|
|
27340
|
+
credits: { hasCredits: false, unlimited: false, balance: 0 }
|
|
27341
|
+
};
|
|
27342
|
+
}
|
|
27343
|
+
const entry = await scrapeCodexQuota(account.accessToken, controller.signal);
|
|
27344
|
+
entry.accountName = account.name;
|
|
27345
|
+
return entry;
|
|
27250
27346
|
} else {
|
|
27251
27347
|
if (!account.apiKey?.trim()) {
|
|
27252
27348
|
return {
|
|
@@ -27299,7 +27395,13 @@ class UsageService {
|
|
|
27299
27395
|
const accounts = this.getAccounts();
|
|
27300
27396
|
for (const provider of PROVIDERS) {
|
|
27301
27397
|
const key = auth[provider]?.key;
|
|
27302
|
-
const match = typeof key === "string" && key.length > 0 ? accounts.find((account) =>
|
|
27398
|
+
const match = typeof key === "string" && key.length > 0 ? accounts.find((account) => {
|
|
27399
|
+
if (account.provider !== provider)
|
|
27400
|
+
return false;
|
|
27401
|
+
if (account.provider === "codex")
|
|
27402
|
+
return account.accessToken === key;
|
|
27403
|
+
return account.apiKey === key;
|
|
27404
|
+
}) : undefined;
|
|
27303
27405
|
if (match) {
|
|
27304
27406
|
activeByProvider[provider] = match.name;
|
|
27305
27407
|
recordActiveSubscriptionForProvider(provider, match.name);
|
|
@@ -27372,6 +27474,19 @@ class UsageService {
|
|
|
27372
27474
|
output.parts.push(createInternalAgentTextPart(`✅ Added Neuralwatt account "${name}".`));
|
|
27373
27475
|
break;
|
|
27374
27476
|
}
|
|
27477
|
+
case "add-codex": {
|
|
27478
|
+
const [_, name, ...tokenParts] = parts;
|
|
27479
|
+
const accessToken = tokenParts.join(" ");
|
|
27480
|
+
if (!name || !accessToken) {
|
|
27481
|
+
output.parts.push(createInternalAgentTextPart(`Usage: /subscriptions add-codex <name> <access-token>
|
|
27482
|
+
` + "Example: /subscriptions add-codex my-codex eyJhbGci..."));
|
|
27483
|
+
return;
|
|
27484
|
+
}
|
|
27485
|
+
saveAccount({ provider: "codex", name, accessToken });
|
|
27486
|
+
this.refresh(true).catch(() => {});
|
|
27487
|
+
output.parts.push(createInternalAgentTextPart(`✅ Added Codex account "${name}".`));
|
|
27488
|
+
break;
|
|
27489
|
+
}
|
|
27375
27490
|
case "remove":
|
|
27376
27491
|
case "rm": {
|
|
27377
27492
|
const [_, name] = parts;
|
|
@@ -27417,18 +27532,20 @@ class UsageService {
|
|
|
27417
27532
|
const accounts = this.getAccounts();
|
|
27418
27533
|
const activeByProvider = this.syncActiveAccounts();
|
|
27419
27534
|
if (accounts.length === 0) {
|
|
27420
|
-
output.parts.push(createInternalAgentTextPart("No accounts configured. Use /subscriptions add-opencode-go or /subscriptions add-
|
|
27535
|
+
output.parts.push(createInternalAgentTextPart("No accounts configured. Use /subscriptions add-opencode-go, /subscriptions add-neuralwatt, or /subscriptions add-codex to add one."));
|
|
27421
27536
|
return;
|
|
27422
27537
|
}
|
|
27423
27538
|
const lines = ["### Subscription Accounts", ""];
|
|
27424
27539
|
for (const acct of accounts) {
|
|
27425
27540
|
const isActive = activeByProvider[acct.provider] === acct.name;
|
|
27426
27541
|
const star = isActive ? "★ " : " ";
|
|
27427
|
-
const providerLabel = acct.provider === "opencode-go" ? "OpenCode Go" : "Neuralwatt";
|
|
27542
|
+
const providerLabel = acct.provider === "opencode-go" ? "OpenCode Go" : acct.provider === "codex" ? "Codex" : "Neuralwatt";
|
|
27428
27543
|
lines.push(`${star}${acct.name} (${providerLabel})`);
|
|
27429
27544
|
if (acct.provider === "opencode-go") {
|
|
27430
27545
|
lines.push(` workspace: ${acct.workspaceId}`);
|
|
27431
27546
|
lines.push(` cookie: ${maskCookie(acct.authCookie)}`);
|
|
27547
|
+
} else if (acct.provider === "codex") {
|
|
27548
|
+
lines.push(` access-token: ${maskCookie(acct.accessToken)}`);
|
|
27432
27549
|
} else {
|
|
27433
27550
|
lines.push(` api-key: ${maskCookie(acct.apiKey)}`);
|
|
27434
27551
|
}
|
|
@@ -27446,6 +27563,7 @@ class UsageService {
|
|
|
27446
27563
|
lines.push("Commands:");
|
|
27447
27564
|
lines.push(" /subscriptions add-opencode-go <name> <workspace-id> <auth-cookie>");
|
|
27448
27565
|
lines.push(" /subscriptions add-neuralwatt <name> <api-key>");
|
|
27566
|
+
lines.push(" /subscriptions add-codex <name> <access-token>");
|
|
27449
27567
|
lines.push(" /subscriptions remove <name>");
|
|
27450
27568
|
lines.push(" /subscriptions edit <name> <new-auth-cookie>");
|
|
27451
27569
|
lines.push(" /subscriptions set-key <name> <api-key>");
|
|
@@ -27480,7 +27598,7 @@ class UsageService {
|
|
|
27480
27598
|
const provider = parseProvider(providerRaw);
|
|
27481
27599
|
if (!provider || !name) {
|
|
27482
27600
|
output.parts.push(createInternalAgentTextPart(`Usage: /subscriptions switch <provider> <name>
|
|
27483
|
-
` + `Providers: opencode-go, neuralwatt
|
|
27601
|
+
` + `Providers: opencode-go, neuralwatt, codex
|
|
27484
27602
|
` + "Example: /subscriptions switch opencode-go personal"));
|
|
27485
27603
|
return;
|
|
27486
27604
|
}
|
|
@@ -27489,9 +27607,16 @@ class UsageService {
|
|
|
27489
27607
|
output.parts.push(createInternalAgentTextPart(`Account "${name}" not found for provider "${provider}".`));
|
|
27490
27608
|
return;
|
|
27491
27609
|
}
|
|
27492
|
-
if (
|
|
27493
|
-
|
|
27494
|
-
|
|
27610
|
+
if (account.provider === "codex") {
|
|
27611
|
+
if (!account.accessToken) {
|
|
27612
|
+
output.parts.push(createInternalAgentTextPart(`Account "${name}" has no access token set. Use /subscriptions add-codex to re-add.`));
|
|
27613
|
+
return;
|
|
27614
|
+
}
|
|
27615
|
+
} else {
|
|
27616
|
+
if (!account.apiKey) {
|
|
27617
|
+
output.parts.push(createInternalAgentTextPart(`Account "${name}" has no API key set. Use /subscriptions set-key ${name} <api-key> first.`));
|
|
27618
|
+
return;
|
|
27619
|
+
}
|
|
27495
27620
|
}
|
|
27496
27621
|
const activeByProvider = this.syncActiveAccounts();
|
|
27497
27622
|
if (activeByProvider[account.provider] === name) {
|
|
@@ -27499,9 +27624,10 @@ class UsageService {
|
|
|
27499
27624
|
return;
|
|
27500
27625
|
}
|
|
27501
27626
|
try {
|
|
27627
|
+
const key = account.provider === "codex" ? account.accessToken : account.apiKey ?? "";
|
|
27502
27628
|
await this.client.auth.set({
|
|
27503
27629
|
path: { id: account.provider },
|
|
27504
|
-
body: { type: "api", key
|
|
27630
|
+
body: { type: "api", key }
|
|
27505
27631
|
});
|
|
27506
27632
|
} catch {
|
|
27507
27633
|
output.parts.push(createInternalAgentTextPart("⚠ Failed to update auth. The key was not applied."));
|
|
@@ -27530,6 +27656,7 @@ class UsageService {
|
|
|
27530
27656
|
` + `Commands:
|
|
27531
27657
|
` + ` /subscriptions add-opencode-go <name> <workspace-id> <auth-cookie> Add an OpenCode Go account
|
|
27532
27658
|
` + ` /subscriptions add-neuralwatt <name> <api-key> Add a Neuralwatt account
|
|
27659
|
+
` + ` /subscriptions add-codex <name> <access-token> Add a Codex account
|
|
27533
27660
|
` + ` /subscriptions remove <name> Remove an account
|
|
27534
27661
|
` + ` /subscriptions edit <name> <new-auth-cookie> Update auth cookie (OpenCode Go)
|
|
27535
27662
|
` + ` /subscriptions set-key <name> <api-key> Set API key for switching
|
|
@@ -27547,7 +27674,7 @@ class UsageService {
|
|
|
27547
27674
|
}
|
|
27548
27675
|
if (!configCommand?.[SUBSCRIPTIONS_COMMAND]) {
|
|
27549
27676
|
opencodeConfig.command[SUBSCRIPTIONS_COMMAND] = {
|
|
27550
|
-
template: "Manage subscription accounts (add-opencode-go, add-neuralwatt, remove, list, edit, set-key, switch, refresh)",
|
|
27677
|
+
template: "Manage subscription accounts (add-opencode-go, add-neuralwatt, add-codex, remove, list, edit, set-key, switch, refresh)",
|
|
27551
27678
|
description: "Add, remove, list, edit, set-key, switch, or refresh subscription accounts for usage tracking in the sidebar"
|
|
27552
27679
|
};
|
|
27553
27680
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
import type { CodexUsageEntry } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Fetch Codex quota data via the WHAM usage API.
|
|
11
|
+
*/
|
|
12
|
+
export declare function scrapeCodexQuota(accessToken: string, signal?: AbortSignal): Promise<CodexUsageEntry>;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export type { StoredAccount } from './accounts-store';
|
|
9
9
|
export { getAccount, getAccountsByProvider, loadAccounts, loadAccountsResult, maskCookie, removeAccount, saveAccount, setAccountKey, updateAccountCookie, } from './accounts-store';
|
|
10
|
+
export { scrapeCodexQuota } from './codex-scraper';
|
|
10
11
|
export { scrapeNeuralwattQuota } from './neuralwatt-scraper';
|
|
11
12
|
export { scrapeQuota, scrapeUsagePage } from './opencode-go-scraper';
|
|
12
|
-
export type { NeuralwattUsageEntry, OpenCodeGoUsageEntry, SubscriptionUsageEntry, UsageDetail, UsageWindow, } from './types';
|
|
13
|
+
export type { CodexUsageEntry, NeuralwattUsageEntry, OpenCodeGoUsageEntry, SubscriptionUsageEntry, UsageDetail, UsageWindow, } from './types';
|
|
13
14
|
export { createUsageService, UsageService } from './usage-service';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* as discriminated unions on the `provider` field.
|
|
6
6
|
*/
|
|
7
7
|
/** Provider discriminator. */
|
|
8
|
-
export type SubscriptionProvider = 'opencode-go' | 'neuralwatt';
|
|
8
|
+
export type SubscriptionProvider = 'opencode-go' | 'neuralwatt' | 'codex';
|
|
9
9
|
export interface OpenCodeGoAccount {
|
|
10
10
|
provider: 'opencode-go';
|
|
11
11
|
name: string;
|
|
@@ -18,7 +18,13 @@ export interface NeuralwattAccount {
|
|
|
18
18
|
name: string;
|
|
19
19
|
apiKey: string;
|
|
20
20
|
}
|
|
21
|
-
export
|
|
21
|
+
export interface CodexAccount {
|
|
22
|
+
provider: 'codex';
|
|
23
|
+
name: string;
|
|
24
|
+
accessToken: string;
|
|
25
|
+
refreshToken?: string;
|
|
26
|
+
}
|
|
27
|
+
export type StoredAccount = OpenCodeGoAccount | NeuralwattAccount | CodexAccount;
|
|
22
28
|
/** Per-time-window usage data scraped from the OpenCode Go dashboard. */
|
|
23
29
|
export interface UsageWindow {
|
|
24
30
|
/** Usage percentage [0..100] */
|
|
@@ -94,7 +100,23 @@ export interface NeuralwattUsageEntry {
|
|
|
94
100
|
/** Error message if the fetch failed. */
|
|
95
101
|
error?: string;
|
|
96
102
|
}
|
|
97
|
-
export
|
|
103
|
+
export interface CodexUsageEntry {
|
|
104
|
+
provider: 'codex';
|
|
105
|
+
accountName: string;
|
|
106
|
+
fetchedAt: number;
|
|
107
|
+
error?: string;
|
|
108
|
+
/** 5-hour rolling window (primary_window from API) */
|
|
109
|
+
primaryWindow: UsageWindow;
|
|
110
|
+
/** 7-day rolling window (secondary_window from API) */
|
|
111
|
+
secondaryWindow: UsageWindow;
|
|
112
|
+
/** Credit balance info */
|
|
113
|
+
credits: {
|
|
114
|
+
hasCredits: boolean;
|
|
115
|
+
unlimited: boolean;
|
|
116
|
+
balance: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export type SubscriptionUsageEntry = OpenCodeGoUsageEntry | NeuralwattUsageEntry | CodexUsageEntry;
|
|
98
120
|
/** Detailed usage data from the /usage page. */
|
|
99
121
|
export interface UsageDetail {
|
|
100
122
|
/** Total number of API calls. */
|
package/dist/tui.js
CHANGED
|
@@ -456,7 +456,7 @@ function parseSnapshot(value) {
|
|
|
456
456
|
return null;
|
|
457
457
|
const activeSubscriptionByProvider = {};
|
|
458
458
|
if (parsed.activeSubscriptionByProvider) {
|
|
459
|
-
for (const provider of ["opencode-go", "neuralwatt"]) {
|
|
459
|
+
for (const provider of ["opencode-go", "neuralwatt", "codex"]) {
|
|
460
460
|
const name = parsed.activeSubscriptionByProvider[provider];
|
|
461
461
|
if (typeof name === "string" && name.length > 0) {
|
|
462
462
|
activeSubscriptionByProvider[provider] = name;
|
|
@@ -1291,6 +1291,44 @@ function renderNeuralwattUsage(entry, rows, theme) {
|
|
|
1291
1291
|
pushNeuralwattMonthlyTokensRow(rows, theme, u);
|
|
1292
1292
|
}
|
|
1293
1293
|
}
|
|
1294
|
+
function renderCodexUsage(entry, rows, theme) {
|
|
1295
|
+
if (entry.primaryWindow) {
|
|
1296
|
+
const w = entry.primaryWindow;
|
|
1297
|
+
const usageColor = getUsageColor(w.percentRemaining);
|
|
1298
|
+
const bar = renderUsageBar(w.percentRemaining);
|
|
1299
|
+
const pct = w.percentRemaining.toFixed(0).padStart(3);
|
|
1300
|
+
const timeLeft = formatUsageTime(w.resetTimeIso);
|
|
1301
|
+
rows.push(box({ width: "100%", flexDirection: "row", justifyContent: "space-between" }, [
|
|
1302
|
+
box({ flexDirection: "row" }, [
|
|
1303
|
+
text({ fg: theme.accent }, ["5H "]),
|
|
1304
|
+
text({ fg: usageColor || theme.text }, [bar]),
|
|
1305
|
+
text({ fg: usageColor || theme.textMuted }, [` ${pct}%`])
|
|
1306
|
+
]),
|
|
1307
|
+
text({ fg: theme.textMuted }, [timeLeft])
|
|
1308
|
+
]));
|
|
1309
|
+
}
|
|
1310
|
+
if (entry.secondaryWindow) {
|
|
1311
|
+
const w = entry.secondaryWindow;
|
|
1312
|
+
const usageColor = getUsageColor(w.percentRemaining);
|
|
1313
|
+
const bar = renderUsageBar(w.percentRemaining);
|
|
1314
|
+
const pct = w.percentRemaining.toFixed(0).padStart(3);
|
|
1315
|
+
const timeLeft = formatUsageTime(w.resetTimeIso);
|
|
1316
|
+
rows.push(box({ width: "100%", flexDirection: "row", justifyContent: "space-between" }, [
|
|
1317
|
+
box({ flexDirection: "row" }, [
|
|
1318
|
+
text({ fg: theme.accent }, ["7D "]),
|
|
1319
|
+
text({ fg: usageColor || theme.text }, [bar]),
|
|
1320
|
+
text({ fg: usageColor || theme.textMuted }, [` ${pct}%`])
|
|
1321
|
+
]),
|
|
1322
|
+
text({ fg: theme.textMuted }, [timeLeft])
|
|
1323
|
+
]));
|
|
1324
|
+
}
|
|
1325
|
+
const balance = entry.credits.balance;
|
|
1326
|
+
rows.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1327
|
+
text({ fg: theme.text }, [
|
|
1328
|
+
entry.credits.unlimited ? "\uD83D\uDCB0 Unlimited credits" : `\uD83D\uDCB0 $${balance.toFixed(2)} credits`
|
|
1329
|
+
])
|
|
1330
|
+
]));
|
|
1331
|
+
}
|
|
1294
1332
|
function renderSubscriptionPanel(snapshot, theme) {
|
|
1295
1333
|
const usage = snapshot.subscriptionUsage ?? {};
|
|
1296
1334
|
const usageEntries = Object.entries(usage).sort(([, a], [, b]) => {
|
|
@@ -1306,7 +1344,7 @@ function renderSubscriptionPanel(snapshot, theme) {
|
|
|
1306
1344
|
const name = entry.accountName;
|
|
1307
1345
|
const activeName = snapshot.activeSubscriptionByProvider?.[entry.provider];
|
|
1308
1346
|
const isActive = activeName === name;
|
|
1309
|
-
const providerLabel = entry.provider === "neuralwatt" ? " [nw]" : " [go]";
|
|
1347
|
+
const providerLabel = entry.provider === "neuralwatt" ? " [nw]" : entry.provider === "codex" ? " [cx]" : " [go]";
|
|
1310
1348
|
if (!isFirstAccount) {
|
|
1311
1349
|
rows.push(box({ width: "100%", height: 1 }));
|
|
1312
1350
|
}
|
|
@@ -1331,6 +1369,8 @@ function renderSubscriptionPanel(snapshot, theme) {
|
|
|
1331
1369
|
renderOpenCodeGoBars(entry, rows, theme);
|
|
1332
1370
|
} else if (entry.provider === "neuralwatt") {
|
|
1333
1371
|
renderNeuralwattUsage(entry, rows, theme);
|
|
1372
|
+
} else if (entry.provider === "codex") {
|
|
1373
|
+
renderCodexUsage(entry, rows, theme);
|
|
1334
1374
|
} else {
|
|
1335
1375
|
rows.push(text({ fg: "#F39C12" }, [
|
|
1336
1376
|
" ⚠️ Provider field missing - re-add account with /subscriptions"
|
package/package.json
CHANGED
|
@@ -170,8 +170,8 @@ describe('auto-update-checker/index', () => {
|
|
|
170
170
|
);
|
|
171
171
|
expect(showToast).toHaveBeenCalledWith({
|
|
172
172
|
body: {
|
|
173
|
-
title: '
|
|
174
|
-
message: 'v0.9.1
|
|
173
|
+
title: 'Opencode Dux Updated!',
|
|
174
|
+
message: 'Updated from v0.9.1 to v0.9.11\nRestart OpenCode to apply.',
|
|
175
175
|
variant: 'success',
|
|
176
176
|
duration: 8000,
|
|
177
177
|
},
|
|
@@ -199,8 +199,8 @@ describe('auto-update-checker/index', () => {
|
|
|
199
199
|
|
|
200
200
|
expect(showToast).toHaveBeenCalledWith({
|
|
201
201
|
body: {
|
|
202
|
-
title: '
|
|
203
|
-
message: 'v0.9.11 available. Auto-update is disabled.',
|
|
202
|
+
title: 'Opencode Dux 0.9.11',
|
|
203
|
+
message: 'Update from v0.9.1 to v0.9.11 available. Auto-update is disabled.',
|
|
204
204
|
variant: 'info',
|
|
205
205
|
duration: 8000,
|
|
206
206
|
},
|
|
@@ -230,9 +230,8 @@ describe('auto-update-checker/index', () => {
|
|
|
230
230
|
expect(crossSpawnMock).not.toHaveBeenCalled();
|
|
231
231
|
expect(showToast).toHaveBeenCalledWith({
|
|
232
232
|
body: {
|
|
233
|
-
title: '
|
|
234
|
-
message:
|
|
235
|
-
'v0.9.11 available. Auto-update could not prepare the active install.',
|
|
233
|
+
title: 'Opencode Dux 0.9.11',
|
|
234
|
+
message: 'Update from v0.9.1 to v0.9.11 available. Auto-update could not prepare the active install.',
|
|
236
235
|
variant: 'info',
|
|
237
236
|
duration: 8000,
|
|
238
237
|
},
|
|
@@ -271,9 +270,8 @@ describe('auto-update-checker/index', () => {
|
|
|
271
270
|
);
|
|
272
271
|
expect(showToast).toHaveBeenCalledWith({
|
|
273
272
|
body: {
|
|
274
|
-
title: '
|
|
275
|
-
message:
|
|
276
|
-
'v0.9.11 available, but auto-update failed to install it. Check logs or retry manually.',
|
|
273
|
+
title: 'Opencode Dux 0.9.11',
|
|
274
|
+
message: 'Update from v0.9.1 to v0.9.11 available, but auto-update failed to install it. Check logs or retry manually.',
|
|
277
275
|
variant: 'error',
|
|
278
276
|
duration: 8000,
|
|
279
277
|
},
|
|
@@ -101,8 +101,8 @@ async function runBackgroundUpdateCheck(
|
|
|
101
101
|
if (pluginInfo.isPinned) {
|
|
102
102
|
showToast(
|
|
103
103
|
ctx,
|
|
104
|
-
`
|
|
105
|
-
`v${latestVersion} available.\nVersion is pinned. Update your plugin config to apply.`,
|
|
104
|
+
`Opencode Dux ${latestVersion}`,
|
|
105
|
+
`Update from v${currentVersion} to v${latestVersion} available.\nVersion is pinned. Update your plugin config to apply.`,
|
|
106
106
|
'info',
|
|
107
107
|
8000,
|
|
108
108
|
);
|
|
@@ -113,8 +113,8 @@ async function runBackgroundUpdateCheck(
|
|
|
113
113
|
if (!autoUpdate) {
|
|
114
114
|
showToast(
|
|
115
115
|
ctx,
|
|
116
|
-
`
|
|
117
|
-
`v${latestVersion} available. Auto-update is disabled.`,
|
|
116
|
+
`Opencode Dux ${latestVersion}`,
|
|
117
|
+
`Update from v${currentVersion} to v${latestVersion} available. Auto-update is disabled.`,
|
|
118
118
|
'info',
|
|
119
119
|
8000,
|
|
120
120
|
);
|
|
@@ -126,8 +126,8 @@ async function runBackgroundUpdateCheck(
|
|
|
126
126
|
if (!installDir) {
|
|
127
127
|
showToast(
|
|
128
128
|
ctx,
|
|
129
|
-
`
|
|
130
|
-
`v${latestVersion} available. Auto-update could not prepare the active install.`,
|
|
129
|
+
`Opencode Dux ${latestVersion}`,
|
|
130
|
+
`Update from v${currentVersion} to v${latestVersion} available. Auto-update could not prepare the active install.`,
|
|
131
131
|
'info',
|
|
132
132
|
8000,
|
|
133
133
|
);
|
|
@@ -140,8 +140,8 @@ async function runBackgroundUpdateCheck(
|
|
|
140
140
|
if (installSuccess) {
|
|
141
141
|
showToast(
|
|
142
142
|
ctx,
|
|
143
|
-
'
|
|
144
|
-
`v${currentVersion}
|
|
143
|
+
'Opencode Dux Updated!',
|
|
144
|
+
`Updated from v${currentVersion} to v${latestVersion}\nRestart OpenCode to apply.`,
|
|
145
145
|
'success',
|
|
146
146
|
8000,
|
|
147
147
|
);
|
|
@@ -151,8 +151,8 @@ async function runBackgroundUpdateCheck(
|
|
|
151
151
|
} else {
|
|
152
152
|
showToast(
|
|
153
153
|
ctx,
|
|
154
|
-
`
|
|
155
|
-
`v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`,
|
|
154
|
+
`Opencode Dux ${latestVersion}`,
|
|
155
|
+
`Update from v${currentVersion} to v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`,
|
|
156
156
|
'error',
|
|
157
157
|
8000,
|
|
158
158
|
);
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
import * as fs from 'node:fs';
|
|
14
14
|
import * as os from 'node:os';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
CodexAccount,
|
|
18
|
+
NeuralwattAccount,
|
|
19
|
+
OpenCodeGoAccount,
|
|
20
|
+
StoredAccount,
|
|
21
|
+
SubscriptionProvider,
|
|
22
|
+
} from './types';
|
|
17
23
|
|
|
18
24
|
// Re-export for consumers
|
|
19
25
|
export type { StoredAccount };
|
|
@@ -178,7 +184,11 @@ export function setAccountKey(
|
|
|
178
184
|
const account = file.accounts.find((a) => a.name === name);
|
|
179
185
|
if (!account) return false;
|
|
180
186
|
account.provider = provider as SubscriptionProvider;
|
|
181
|
-
account.
|
|
187
|
+
if (account.provider === 'codex') {
|
|
188
|
+
(account as CodexAccount).accessToken = apiKey;
|
|
189
|
+
} else {
|
|
190
|
+
(account as OpenCodeGoAccount | NeuralwattAccount).apiKey = apiKey;
|
|
191
|
+
}
|
|
182
192
|
writeAccountsFile(file);
|
|
183
193
|
return true;
|
|
184
194
|
}
|