@arandevcode/opencode-quota-extended 0.2.1 → 0.2.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.
Files changed (42) hide show
  1. package/README-es.md +5 -5
  2. package/README.md +5 -5
  3. package/dist/lib/balance.d.ts +14 -0
  4. package/dist/lib/balance.d.ts.map +1 -0
  5. package/dist/lib/balance.js +23 -0
  6. package/dist/lib/balance.js.map +1 -0
  7. package/dist/lib/pace.d.ts +28 -0
  8. package/dist/lib/pace.d.ts.map +1 -0
  9. package/dist/lib/pace.js +46 -0
  10. package/dist/lib/pace.js.map +1 -0
  11. package/dist/lib/refresh-scheduler.d.ts +20 -0
  12. package/dist/lib/refresh-scheduler.d.ts.map +1 -0
  13. package/dist/lib/refresh-scheduler.js +57 -0
  14. package/dist/lib/refresh-scheduler.js.map +1 -0
  15. package/dist/lib/types.d.ts +15 -0
  16. package/dist/lib/types.d.ts.map +1 -1
  17. package/dist/providers/opencode-go.d.ts +5 -2
  18. package/dist/providers/opencode-go.d.ts.map +1 -1
  19. package/dist/providers/opencode-go.js +156 -42
  20. package/dist/providers/opencode-go.js.map +1 -1
  21. package/dist/tui.d.ts.map +1 -1
  22. package/dist/tui.tsx +58 -83
  23. package/dist/ui/SidebarPanel.d.ts.map +1 -1
  24. package/dist/ui/SidebarPanel.tsx +11 -3
  25. package/dist/ui/cards.d.ts.map +1 -1
  26. package/dist/ui/cards.tsx +13 -1
  27. package/package.json +11 -7
  28. package/scripts/clean.mjs +8 -0
  29. package/scripts/postbuild.mjs +56 -0
  30. package/scripts/setup.mjs +45 -0
  31. package/dist/lib/sanitize.d.ts +0 -10
  32. package/dist/lib/sanitize.d.ts.map +0 -1
  33. package/dist/lib/sanitize.js +0 -14
  34. package/dist/lib/sanitize.js.map +0 -1
  35. package/dist/lib/throttle.d.ts +0 -18
  36. package/dist/lib/throttle.d.ts.map +0 -1
  37. package/dist/lib/throttle.js +0 -26
  38. package/dist/lib/throttle.js.map +0 -1
  39. package/dist/lib/toast-format.d.ts +0 -24
  40. package/dist/lib/toast-format.d.ts.map +0 -1
  41. package/dist/lib/toast-format.js +0 -79
  42. package/dist/lib/toast-format.js.map +0 -1
package/README-es.md CHANGED
@@ -129,21 +129,21 @@ Colores de semáforo: ≥50% `theme.success` (verde), 25-49% `theme.warning` (na
129
129
 
130
130
  Lee credenciales del archivo de autenticación de OpenCode (`~/.local/share/opencode/auth.json`). **No** almacena, solicita ni distribuye secretos.
131
131
 
132
- **OpenCode Go** requiere configuración adicional — es un poco tricky porque no hay API oficial todavía.
132
+ **OpenCode Go** requiere configuración adicional — es un poco tricky porque todavía no hay API pública, así que el plugin reutiliza la cookie de sesión de tu navegador para consultar el mismo endpoint del console que usa el dashboard. La cookie caduca, por lo que puede que tengas que renovarla periódicamente.
133
133
 
134
134
  Crea `~/.config/opencode/opencode-quota/opencode-go.json`:
135
135
 
136
136
  ```json
137
137
  {
138
138
  "workspaceId": "wrk_abc123...",
139
- "authCookie": "Fe26.2..."
139
+ "authCookie": "st_..."
140
140
  }
141
141
  ```
142
142
 
143
143
  | Campo | Descripción | Cómo obtenerlo |
144
144
  |---|---|---|
145
145
  | `workspaceId` | ID de tu workspace en OpenCode | Entra en [opencode.ai/go](https://opencode.ai/go). La URL es `https://opencode.ai/workspace/wrk_XXX/go` — copia la parte `wrk_XXX`. |
146
- | `authCookie` | Cookie de sesión para autenticación | Abre DevTools (F12) → Application → Cookies → `opencode.ai` → busca la cookie `auth` (empieza con `Fe26.2...`). O ejecuta en la consola: `document.cookie.match(/(?:^\|;\s*)auth=([^;]+)/)?.[1]` |
146
+ | `authCookie` | Cookie de sesión del console | Abre DevTools (F12) → Application → Cookies → `https://opencode.ai` → busca `__Host-console_session` (el valor empieza por `st_...`). Es `HttpOnly`, así que solo se puede leer desde DevTools (no con `document.cookie`). |
147
147
 
148
148
  **Múltiples workspaces/cuentas:** cualquier archivo `opencode-go-<sufijo>.json` en el mismo directorio añade una sección extra `Opencode GO (<sufijo>)` (p. ej. `opencode-go-work.json` → **Opencode GO (work)**). El `opencode-go.json` base siempre se muestra como **OpenCode Go**.
149
149
 
@@ -158,7 +158,7 @@ cp ~/.config/opencode/opencode-quota/opencode-go.json \
158
158
  ```sh
159
159
  npm run setup
160
160
  ```
161
- Te solicita ambos valores y crea el archivo automáticamente. También muestra un bookmarklet que puedes arrastrar a la barra de favoritos y pinchar en opencode.ai para extraer la auth cookie con un clic.
161
+ Te solicita ambos valores y crea el archivo automáticamente. También muestra instrucciones paso a paso con DevTools para copiar la cookie `__Host-console_session`.
162
162
 
163
163
  **Consejo:** mantén una pestaña del navegador con sesión iniciada en opencode.ai mientras usas este plugin — la auth cookie puede expirar.
164
164
 
@@ -170,7 +170,7 @@ Te solicita ambos valores y crea el archivo automáticamente. También muestra u
170
170
 
171
171
  - **El panel no aparece**: `grep '@arandevcode/opencode-quota-extended' ~/.local/share/opencode/log/*.log`
172
172
  - **OpenCode Go muestra "skipped"**: confirma que el archivo sidecar o vars de entorno están configurados
173
- - **Bug antiguo de pinning `@opentui/solid`**: actualiza a `^0.4.1` y reinstala limpio (`rm -rf node_modules && npm ci`)
173
+ - **Bug antiguo de pinning `@opentui/solid`**: usa `^0.4.1 || ^0.5.0` y reinstala limpio (`rm -rf node_modules && npm ci`)
174
174
 
175
175
  ### Desarrollo
176
176
 
package/README.md CHANGED
@@ -130,21 +130,21 @@ Traffic-light colors: ≥50% `theme.success` (green), 25-49% `theme.warning` (or
130
130
 
131
131
  Reads credentials from OpenCode's runtime auth file (`~/.local/share/opencode/auth.json`). Does **not** store, prompt for, or distribute secrets.
132
132
 
133
- **OpenCode Go** requires additional setup — it's a bit tricky because there's no official API yet.
133
+ **OpenCode Go** requires additional setup — it's a bit tricky because there's no public API yet, so the plugin reuses your browser session cookie to query the same console endpoint the dashboard uses. The cookie expires, so you may need to refresh it periodically.
134
134
 
135
135
  Create `~/.config/opencode/opencode-quota/opencode-go.json`:
136
136
 
137
137
  ```json
138
138
  {
139
139
  "workspaceId": "wrk_abc123...",
140
- "authCookie": "Fe26.2..."
140
+ "authCookie": "st_..."
141
141
  }
142
142
  ```
143
143
 
144
144
  | Field | Description | How to get it |
145
145
  |---|---|---|
146
146
  | `workspaceId` | Your OpenCode workspace ID | Log in at [opencode.ai/go](https://opencode.ai/go). The URL is `https://opencode.ai/workspace/wrk_XXX/go` — copy the `wrk_XXX` part. |
147
- | `authCookie` | Session cookie for auth | Open DevTools (F12) → Application → Cookies → `opencode.ai` → find the `auth` cookie (starts with `Fe26.2...`). Or run this in the console: `document.cookie.match(/(?:^\|;\s*)auth=([^;]+)/)?.[1]` |
147
+ | `authCookie` | Console session cookie | Open DevTools (F12) → Application → Cookies → `https://opencode.ai` → find `__Host-console_session` (value starts with `st_...`). It is `HttpOnly`, so it can only be read from DevTools (not `document.cookie`). |
148
148
 
149
149
  **Multiple workspaces/accounts:** any `opencode-go-<suffix>.json` file in the same directory adds an extra `Opencode GO (<suffix>)` section (e.g. `opencode-go-work.json` → **Opencode GO (work)**). The base `opencode-go.json` always renders as **OpenCode Go**.
150
150
 
@@ -159,7 +159,7 @@ cp ~/.config/opencode/opencode-quota/opencode-go.json \
159
159
  ```sh
160
160
  npm run setup
161
161
  ```
162
- This prompts for both values and creates the file for you. It also prints a bookmarklet you can drag to your bookmarks bar and click while on opencode.ai to extract the auth cookie with one click.
162
+ This prompts for both values and creates the file for you. It also prints step-by-step DevTools instructions to grab the `__Host-console_session` cookie.
163
163
 
164
164
  **Tip:** keep a browser tab logged into opencode.ai while using this plugin — the auth cookie may expire.
165
165
 
@@ -171,7 +171,7 @@ This prompts for both values and creates the file for you. It also prints a book
171
171
 
172
172
  - **Panel doesn't appear**: `grep '@arandevcode/opencode-quota-extended' ~/.local/share/opencode/log/*.log`
173
173
  - **OpenCode Go shows "skipped"**: confirm sidecar file or env vars are set
174
- - **Old `@opentui/solid` pinning bug**: update to `^0.4.1` and clean-install (`rm -rf node_modules && npm ci`)
174
+ - **Old `@opentui/solid` pinning bug**: use `^0.4.1 || ^0.5.0` and clean-install (`rm -rf node_modules && npm ci`)
175
175
 
176
176
  ### Development
177
177
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prepaid credit balance formatting (OpenCode Go).
3
+ *
4
+ * The provider reports `availableMicroCents` as a decimal string because it can
5
+ * exceed the safe integer range; 1e8 microcents make one US dollar. Only the
6
+ * dollar amount and the auto-recharge flag are ever rendered — never the raw
7
+ * microcents, and never anything that came from the account credentials.
8
+ */
9
+ import type { ProviderBalance } from "./types.js";
10
+ /** Available credit in dollars (2 decimals), or null when unreadable. */
11
+ export declare function balanceUsd(balance: ProviderBalance | undefined): number | null;
12
+ /** "$9.18 credit", plus a note when credits are drawn or auto-recharge is on. */
13
+ export declare function formatBalance(balance: ProviderBalance | undefined): string;
14
+ //# sourceMappingURL=balance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../../src/lib/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAM9E;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,CAO1E"}
@@ -0,0 +1,23 @@
1
+ /** Available credit in dollars (2 decimals), or null when unreadable. */
2
+ export function balanceUsd(balance) {
3
+ if (!balance)
4
+ return null;
5
+ const raw = balance.availableMicroCents;
6
+ const micro = typeof raw === "string" ? Number(raw.trim()) : raw;
7
+ if (typeof micro !== "number" || !Number.isFinite(micro))
8
+ return null;
9
+ return Math.round(micro / 1e6) / 100;
10
+ }
11
+ /** "$9.18 credit", plus a note when credits are drawn or auto-recharge is on. */
12
+ export function formatBalance(balance) {
13
+ const usd = balanceUsd(balance);
14
+ if (usd === null || !balance)
15
+ return "";
16
+ const parts = [`$${usd.toFixed(2)} credit`];
17
+ if (balance.useBalance === true)
18
+ parts.push("draws on credits");
19
+ if (balance.autoRecharge?.enabled)
20
+ parts.push("auto-recharge");
21
+ return parts.join(" · ");
22
+ }
23
+ //# sourceMappingURL=balance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"balance.js","sourceRoot":"","sources":["../../src/lib/balance.ts"],"names":[],"mappings":"AAUA,yEAAyE;AACzE,MAAM,UAAU,UAAU,CAAC,OAAoC;IAC7D,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,mBAAmB,CAAC;IACxC,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACjE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACvC,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,OAAoC;IAChE,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAChE,IAAI,OAAO,CAAC,YAAY,EAAE,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Quota pace — how usage compares to the passage of time in a window.
3
+ *
4
+ * Every window resets on a known schedule (5h, weekly, monthly). When a window
5
+ * carries its reset time we can say whether the remaining share is ahead of or
6
+ * behind the clock: spending slower than time passes is margin, faster is
7
+ * "ahead". This is the same reading Jarvis shows, but text-only — no overlay on
8
+ * the progress bar, so it works at any terminal width.
9
+ */
10
+ import type { QuotaWindow } from "./types.js";
11
+ export type QuotaPace = "buffer" | "on-pace" | "burn";
12
+ export interface QuotaPaceReading {
13
+ pace: QuotaPace;
14
+ /** Percentage-point gap between remaining quota and time left (rounded). */
15
+ deltaPercent: number;
16
+ /** The same gap expressed as time, in ms. */
17
+ deltaMs: number;
18
+ }
19
+ /** Window length when the id/label names a known schedule, else null. */
20
+ export declare function windowDurationMs(window: Pick<QuotaWindow, "id" | "label">): number | null;
21
+ /**
22
+ * Compare remaining quota to the time left in the window. Returns null when the
23
+ * schedule or reset time is unknown, or there is no reading to compare.
24
+ */
25
+ export declare function quotaPace(window: QuotaWindow, now?: number): QuotaPaceReading | null;
26
+ /** One short line: "margin +6% · 3h 12m", "ahead -6% · 1d 2h" or "on pace". */
27
+ export declare function formatPace(window: QuotaWindow, now?: number): string;
28
+ //# sourceMappingURL=pace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pace.d.ts","sourceRoot":"","sources":["../../src/lib/pace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAMzF;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,GAAE,MAAmB,GAAG,gBAAgB,GAAG,IAAI,CAUhG;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAWhF"}
@@ -0,0 +1,46 @@
1
+ import { clampPercent, formatDuration } from "./reset-format.js";
2
+ const HOUR_MS = 60 * 60 * 1000;
3
+ const DAY_MS = 24 * HOUR_MS;
4
+ /** Window length when the id/label names a known schedule, else null. */
5
+ export function windowDurationMs(window) {
6
+ const key = `${window.id} ${window.label}`.toLowerCase();
7
+ if (/\b5h\b|five[_ -]?hour/.test(key))
8
+ return 5 * HOUR_MS;
9
+ if (/weekly|seven[_ -]?day|\bweek\b/.test(key))
10
+ return 7 * DAY_MS;
11
+ if (/monthly|\bmonth\b/.test(key))
12
+ return 30 * DAY_MS;
13
+ return null;
14
+ }
15
+ /**
16
+ * Compare remaining quota to the time left in the window. Returns null when the
17
+ * schedule or reset time is unknown, or there is no reading to compare.
18
+ */
19
+ export function quotaPace(window, now = Date.now()) {
20
+ const duration = windowDurationMs(window);
21
+ const resetAt = window.resetTimeIso ? Date.parse(window.resetTimeIso) : Number.NaN;
22
+ const remaining = window.percentRemaining;
23
+ if (duration == null || !Number.isFinite(resetAt) || !Number.isFinite(remaining))
24
+ return null;
25
+ const exactTimeLeft = Math.min(100, Math.max(0, ((resetAt - now) / duration) * 100));
26
+ const deltaPercent = clampPercent(remaining) - clampPercent(exactTimeLeft);
27
+ const pace = deltaPercent > 1 ? "buffer" : deltaPercent < -1 ? "burn" : "on-pace";
28
+ const deltaMs = Math.round((Math.abs(Math.min(100, Math.max(0, remaining)) - exactTimeLeft) / 100) * duration);
29
+ return { pace, deltaPercent, deltaMs };
30
+ }
31
+ /** One short line: "margin +6% · 3h 12m", "ahead -6% · 1d 2h" or "on pace". */
32
+ export function formatPace(window, now = Date.now()) {
33
+ const reading = quotaPace(window, now);
34
+ if (!reading)
35
+ return "";
36
+ const { pace, deltaPercent, deltaMs } = reading;
37
+ if (pace === "on-pace")
38
+ return "on pace";
39
+ const word = pace === "buffer" ? "margin" : "ahead";
40
+ const sign = deltaPercent > 0 ? "+" : "";
41
+ const gap = formatDuration(deltaMs);
42
+ return gap
43
+ ? `${word} ${sign}${deltaPercent}% · ${gap}`
44
+ : `${word} ${sign}${deltaPercent}%`;
45
+ }
46
+ //# sourceMappingURL=pace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pace.js","sourceRoot":"","sources":["../../src/lib/pace.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEjE,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC/B,MAAM,MAAM,GAAG,EAAE,GAAG,OAAO,CAAC;AAY5B,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,MAAyC;IACxE,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC;IACzD,IAAI,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IAC1D,IAAI,gCAAgC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IAClE,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IACtD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,MAAmB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACrE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;IACnF,MAAM,SAAS,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAC1C,IAAI,QAAQ,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9F,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACrF,MAAM,YAAY,GAAG,YAAY,CAAC,SAAS,CAAC,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAc,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7F,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,GAAG,aAAa,CAAC,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC/G,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;AACzC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CAAC,MAAmB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACtE,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;IACpD,MAAM,IAAI,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACpC,OAAO,GAAG;QACR,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,GAAG,YAAY,OAAO,GAAG,EAAE;QAC5C,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,GAAG,YAAY,GAAG,CAAC;AACxC,CAAC"}
@@ -0,0 +1,20 @@
1
+ export type RefreshSchedulerOptions = {
2
+ refresh: () => void;
3
+ intervalMs: number;
4
+ activityDelayMs?: number;
5
+ minGapMs?: number;
6
+ now?: () => number;
7
+ };
8
+ export type RefreshScheduler = {
9
+ start: () => void;
10
+ schedule: (delay?: number, force?: boolean) => void;
11
+ dispose: () => void;
12
+ };
13
+ /**
14
+ * Coalesces bursts of usage events into a single refresh and enforces a
15
+ * minimum gap between attempts. A streaming message can emit many
16
+ * `message.updated` events per second; without this every one of them would
17
+ * fan out to the provider APIs.
18
+ */
19
+ export declare function createRefreshScheduler(options: RefreshSchedulerOptions): RefreshScheduler;
20
+ //# sourceMappingURL=refresh-scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-scheduler.d.ts","sourceRoot":"","sources":["../../src/lib/refresh-scheduler.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAoDzF"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Coalesces bursts of usage events into a single refresh and enforces a
3
+ * minimum gap between attempts. A streaming message can emit many
4
+ * `message.updated` events per second; without this every one of them would
5
+ * fan out to the provider APIs.
6
+ */
7
+ export function createRefreshScheduler(options) {
8
+ const { refresh, intervalMs, activityDelayMs = 1_500, minGapMs = 20_000, now = Date.now, } = options;
9
+ let interval = null;
10
+ let timer = null;
11
+ let lastRefreshAt = 0;
12
+ let disposed = false;
13
+ const run = () => {
14
+ if (disposed)
15
+ return;
16
+ lastRefreshAt = now();
17
+ refresh();
18
+ };
19
+ const schedule = (delay = activityDelayMs, force = false) => {
20
+ if (disposed || timer !== null)
21
+ return;
22
+ const wait = force
23
+ ? delay
24
+ : Math.max(delay, minGapMs - (now() - lastRefreshAt));
25
+ timer = setTimeout(() => {
26
+ timer = null;
27
+ if (disposed)
28
+ return;
29
+ if (!force && now() - lastRefreshAt < minGapMs) {
30
+ schedule(0);
31
+ return;
32
+ }
33
+ run();
34
+ }, wait);
35
+ };
36
+ return {
37
+ start() {
38
+ if (disposed || interval !== null)
39
+ return;
40
+ interval = setInterval(() => {
41
+ if (now() - lastRefreshAt >= minGapMs)
42
+ run();
43
+ }, intervalMs);
44
+ },
45
+ schedule,
46
+ dispose() {
47
+ disposed = true;
48
+ if (interval !== null)
49
+ clearInterval(interval);
50
+ if (timer !== null)
51
+ clearTimeout(timer);
52
+ interval = null;
53
+ timer = null;
54
+ },
55
+ };
56
+ }
57
+ //# sourceMappingURL=refresh-scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-scheduler.js","sourceRoot":"","sources":["../../src/lib/refresh-scheduler.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAgC;IACrE,MAAM,EACJ,OAAO,EACP,UAAU,EACV,eAAe,GAAG,KAAK,EACvB,QAAQ,GAAG,MAAM,EACjB,GAAG,GAAG,IAAI,CAAC,GAAG,GACf,GAAG,OAAO,CAAC;IAEZ,IAAI,QAAQ,GAA0C,IAAI,CAAC;IAC3D,IAAI,KAAK,GAAyC,IAAI,CAAC;IACvD,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,GAAG,GAAG,GAAG,EAAE;QACf,IAAI,QAAQ;YAAE,OAAO;QACrB,aAAa,GAAG,GAAG,EAAE,CAAC;QACtB,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,KAAK,GAAG,eAAe,EAAE,KAAK,GAAG,KAAK,EAAE,EAAE;QAC1D,IAAI,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO;QACvC,MAAM,IAAI,GAAG,KAAK;YAChB,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC;QACxD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,KAAK,GAAG,IAAI,CAAC;YACb,IAAI,QAAQ;gBAAE,OAAO;YACrB,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,GAAG,aAAa,GAAG,QAAQ,EAAE,CAAC;gBAC/C,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACZ,OAAO;YACT,CAAC;YACD,GAAG,EAAE,CAAC;QACR,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC,CAAC;IAEF,OAAO;QACL,KAAK;YACH,IAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI;gBAAE,OAAO;YAC1C,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE;gBAC1B,IAAI,GAAG,EAAE,GAAG,aAAa,IAAI,QAAQ;oBAAE,GAAG,EAAE,CAAC;YAC/C,CAAC,EAAE,UAAU,CAAC,CAAC;QACjB,CAAC;QACD,QAAQ;QACR,OAAO;YACL,QAAQ,GAAG,IAAI,CAAC;YAChB,IAAI,QAAQ,KAAK,IAAI;gBAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,IAAI;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YACxC,QAAQ,GAAG,IAAI,CAAC;YAChB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -27,6 +27,19 @@ export interface QuotaWindow {
27
27
  }
28
28
  /** Status of a single provider fetch. */
29
29
  export type ProviderStatus = "ok" | "no-auth" | "error" | "skipped" | "loading";
30
+ /** Prepaid credit balance of an account, when the provider exposes it (OpenCode Go). */
31
+ export interface ProviderBalance {
32
+ /** Available credit in microcents (1e8 microcents = 1 USD), as a decimal string. */
33
+ availableMicroCents: string;
34
+ /** Whether usage may draw on credits once a plan limit is reached; null if unknown. */
35
+ useBalance: boolean | null;
36
+ /** Auto-recharge settings, when readable. */
37
+ autoRecharge?: {
38
+ enabled: boolean;
39
+ thresholdDollars?: number;
40
+ rechargeAmountDollars?: number;
41
+ };
42
+ }
30
43
  /** A single provider's quota result. */
31
44
  export interface ProviderQuota {
32
45
  /** Stable provider id, e.g. "openai", "github-copilot", "opencode-go". */
@@ -41,6 +54,8 @@ export interface ProviderQuota {
41
54
  windows: QuotaWindow[];
42
55
  /** Timestamp the snapshot was taken. */
43
56
  fetchedAt: string;
57
+ /** Credit balance, when the provider exposes it and the lookup succeeded. */
58
+ balance?: ProviderBalance;
44
59
  }
45
60
  /** Aggregate snapshot used by the TUI sidebar and the server tool. */
46
61
  export interface CardsSnapshot {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAChB,IAAI,GACJ,QAAQ,GACR,SAAS,GACT,aAAa,GACb,SAAS,CAAC;AAEd,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,wBAAwB;IACxB,EAAE,EAAE,QAAQ,CAAC;IACb,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,yCAAyC;AACzC,MAAM,MAAM,cAAc,GACtB,IAAI,GACJ,SAAS,GACT,OAAO,GACP,SAAS,GACT,SAAS,CAAC;AAEd,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAChB,IAAI,GACJ,QAAQ,GACR,SAAS,GACT,aAAa,GACb,SAAS,CAAC;AAEd,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,wBAAwB;IACxB,EAAE,EAAE,QAAQ,CAAC;IACb,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,yCAAyC;AACzC,MAAM,MAAM,cAAc,GACtB,IAAI,GACJ,SAAS,GACT,OAAO,GACP,SAAS,GACT,SAAS,CAAC;AAEd,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uFAAuF;IACvF,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,6CAA6C;IAC7C,YAAY,CAAC,EAAE;QACb,OAAO,EAAE,OAAO,CAAC;QACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B"}
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * OpenCode Go quota provider.
3
3
  *
4
- * Fetches usage data by scraping the opencode.ai workspace dashboard.
5
- * Requires a workspace ID and auth cookie (either via env vars
4
+ * Fetches usage data from the opencode.ai console API:
5
+ * GET https://opencode.ai/console/api/go/status
6
+ * Requires a workspace ID and session cookie (either via env vars
6
7
  * OPENCODE_GO_WORKSPACE_ID / OPENCODE_GO_AUTH_COOKIE, or a sidecar file at
7
8
  * ~/.config/opencode/opencode-quota/opencode-go.json).
9
+ * The session cookie is the browser's `__Host-console_session` (value `st_...`)
10
+ * sent together with the `x-org-id` header (the `wrk_...` workspace id).
8
11
  * Additional instances can be configured via opencode-go-<suffix>.json files,
9
12
  * each rendered as its own "Opencode Go <suffix>" section.
10
13
  * Returns up to 3 windows: 5h (rolling), Weekly, Monthly.
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-go.d.ts","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,YAAY,CAAC;AAyC5D,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CAgFlF;AAED,eAAO,MAAM,kBAAkB,EAAE,QAG/B,CAAC"}
1
+ {"version":3,"file":"opencode-go.d.ts","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,YAAY,CAAC;AAuK5D,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CAkFlF;AAED,eAAO,MAAM,kBAAkB,EAAE,QAG/B,CAAC"}
@@ -1,27 +1,140 @@
1
1
  import { resolveOpenCodeGoInstanceCached } from "../lib/opencode-go-config.js";
2
2
  import { fetchWithTimeout } from "../lib/http.js";
3
3
  import { clampPercent } from "../lib/reset-format.js";
4
- const DASHBOARD_URL_PREFIX = "https://opencode.ai/workspace/";
5
- const DASHBOARD_URL_SUFFIX = "/go";
4
+ const GO_STATUS_URL = "https://opencode.ai/console/api/go/status";
5
+ const BILLING_STATUS_URL = "https://opencode.ai/console/api/billing/status";
6
+ const AUTO_RECHARGE_URL = "https://opencode.ai/console/api/billing/auto-recharge";
6
7
  const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0";
7
- const WINDOWS = [
8
- { field: "rollingUsage", id: "5h", label: "5h" },
9
- { field: "weeklyUsage", id: "weekly", label: "Weekly" },
10
- { field: "monthlyUsage", id: "monthly", label: "Monthly" },
11
- ];
12
- const RE_FIELD_PCT_FIRST = (field) => new RegExp(`${field}[^=]*=\\s*\\{[^}]*?usagePercent\\s*:\\s*(\\d+)[^}]*?resetInSec\\s*:\\s*(\\d+)`);
13
- const RE_FIELD_RESET_FIRST = (field) => new RegExp(`${field}[^=]*=\\s*\\{[^}]*?resetInSec\\s*:\\s*(\\d+)[^}]*?usagePercent\\s*:\\s*(\\d+)`);
14
- function parseWindow(html, field) {
15
- const m1 = html.match(RE_FIELD_PCT_FIRST(field));
16
- if (m1 && m1[1] !== undefined && m1[2] !== undefined) {
17
- return { usagePercent: Number(m1[1]), resetInSec: Number(m1[2]) };
8
+ /** Parse a microCents value (string or number) into a bigint. */
9
+ function toBigInt(value) {
10
+ if (typeof value === "bigint")
11
+ return value;
12
+ if (typeof value === "number") {
13
+ if (!Number.isFinite(value))
14
+ return null;
15
+ return BigInt(Math.trunc(value));
18
16
  }
19
- const m2 = html.match(RE_FIELD_RESET_FIRST(field));
20
- if (m2 && m2[1] !== undefined && m2[2] !== undefined) {
21
- return { usagePercent: Number(m2[2]), resetInSec: Number(m2[1]) };
17
+ if (typeof value === "string") {
18
+ const trimmed = value.trim();
19
+ if (trimmed === "")
20
+ return null;
21
+ try {
22
+ return BigInt(trimmed);
23
+ }
24
+ catch {
25
+ const n = Number(trimmed);
26
+ return Number.isFinite(n) ? BigInt(Math.trunc(n)) : null;
27
+ }
22
28
  }
23
29
  return null;
24
30
  }
31
+ /** Percent used, computed exactly as the opencode.ai dashboard does. */
32
+ function percentUsed(used, limit) {
33
+ if (limit <= 0n)
34
+ return 0;
35
+ return Number((used * 200n + limit) / (limit * 2n));
36
+ }
37
+ /**
38
+ * Build the Cookie header for the console API. The console authenticates with
39
+ * the `__Host-console_session` cookie (value `st_...`); older setups stored the
40
+ * site `auth` cookie (`Fe26....`), so pick the name by value shape.
41
+ */
42
+ function sessionCookieHeader(value) {
43
+ const name = value.startsWith("st_") ? "__Host-console_session" : "auth";
44
+ return `${name}=${value}`;
45
+ }
46
+ /** GET a console JSON endpoint. Errors never carry the response body or headers. */
47
+ async function getConsoleJson(url, headers, timeoutMs) {
48
+ const res = await fetchWithTimeout(url, { method: "GET", headers }, timeoutMs);
49
+ if (!res.ok)
50
+ throw new Error(`HTTP ${res.status}`);
51
+ return JSON.parse(await res.text());
52
+ }
53
+ /** Parse the `billing/status` body; null when the available credit is unreadable. */
54
+ function parseAvailableMicroCents(data) {
55
+ if (!data || typeof data !== "object")
56
+ return null;
57
+ const value = toBigInt(data.availableMicroCents);
58
+ return value === null || value < 0n ? null : value.toString();
59
+ }
60
+ /** Parse the `billing/auto-recharge` body; null when it does not carry `enabled`. */
61
+ function parseAutoRecharge(data) {
62
+ if (!data || typeof data !== "object")
63
+ return null;
64
+ const raw = data;
65
+ if (typeof raw.enabled !== "boolean")
66
+ return null;
67
+ const out = { enabled: raw.enabled };
68
+ if (typeof raw.thresholdDollars === "number" && Number.isFinite(raw.thresholdDollars)) {
69
+ out.thresholdDollars = raw.thresholdDollars;
70
+ }
71
+ if (typeof raw.rechargeAmountDollars === "number" && Number.isFinite(raw.rechargeAmountDollars)) {
72
+ out.rechargeAmountDollars = raw.rechargeAmountDollars;
73
+ }
74
+ return out;
75
+ }
76
+ /**
77
+ * Credit balance for the account. Both lookups are best-effort: when the
78
+ * balance itself is missing the result is undefined, and a failed auto-recharge
79
+ * lookup only drops that sub-field.
80
+ */
81
+ async function fetchBalance(headers, timeoutMs, useBalance) {
82
+ const [billing, recharge] = await Promise.allSettled([
83
+ getConsoleJson(BILLING_STATUS_URL, headers, timeoutMs),
84
+ getConsoleJson(AUTO_RECHARGE_URL, headers, timeoutMs),
85
+ ]);
86
+ if (billing.status !== "fulfilled")
87
+ return undefined;
88
+ const availableMicroCents = parseAvailableMicroCents(billing.value);
89
+ if (availableMicroCents === null)
90
+ return undefined;
91
+ const balance = { availableMicroCents, useBalance };
92
+ if (recharge.status === "fulfilled") {
93
+ const autoRecharge = parseAutoRecharge(recharge.value);
94
+ if (autoRecharge)
95
+ balance.autoRecharge = autoRecharge;
96
+ }
97
+ return balance;
98
+ }
99
+ function toIso(value) {
100
+ if (typeof value !== "string" || value.trim() === "")
101
+ return undefined;
102
+ const d = new Date(value);
103
+ if (Number.isNaN(d.getTime()))
104
+ return undefined;
105
+ return d.toISOString();
106
+ }
107
+ function buildWindows(status) {
108
+ const access = status.access;
109
+ if (!access || typeof access !== "object")
110
+ return [];
111
+ const meters = access.meters ?? {};
112
+ const specs = [
113
+ { meter: meters.fiveHour, id: "5h", label: "5h", resetsAt: meters.fiveHour?.resetsAt },
114
+ { meter: meters.week, id: "weekly", label: "Weekly", resetsAt: meters.week?.resetsAt },
115
+ { meter: meters.month, id: "monthly", label: "Monthly", resetsAt: access.endsAt },
116
+ ];
117
+ const windows = [];
118
+ for (const spec of specs) {
119
+ const meter = spec.meter;
120
+ if (!meter)
121
+ continue;
122
+ const limit = toBigInt(meter.limitMicroCents);
123
+ const used = toBigInt(meter.usedMicroCents);
124
+ if (limit === null || used === null)
125
+ continue;
126
+ const window = {
127
+ id: spec.id,
128
+ label: spec.label,
129
+ percentRemaining: clampPercent(100 - percentUsed(used, limit)),
130
+ };
131
+ const resetTimeIso = toIso(spec.resetsAt);
132
+ if (resetTimeIso)
133
+ window.resetTimeIso = resetTimeIso;
134
+ windows.push(window);
135
+ }
136
+ return windows;
137
+ }
25
138
  export function createOpenCodeGoProvider(opts) {
26
139
  const { id, label } = opts;
27
140
  return {
@@ -54,46 +167,47 @@ export function createOpenCodeGoProvider(opts) {
54
167
  return { ...base, status: "error", error: `Invalid config: ${r.error}` };
55
168
  }
56
169
  const { workspaceId, authCookie } = r.config;
170
+ const headers = {
171
+ "User-Agent": USER_AGENT,
172
+ Accept: "application/json",
173
+ Cookie: sessionCookieHeader(authCookie),
174
+ "x-org-id": workspaceId,
175
+ };
57
176
  try {
58
- const url = `${DASHBOARD_URL_PREFIX}${encodeURIComponent(workspaceId)}${DASHBOARD_URL_SUFFIX}`;
59
- const res = await fetchWithTimeout(url, {
60
- method: "GET",
61
- headers: {
62
- "User-Agent": USER_AGENT,
63
- Accept: "text/html",
64
- Cookie: `auth=${authCookie}`,
65
- },
66
- }, ctx.requestTimeoutMs);
177
+ const res = await fetchWithTimeout(GO_STATUS_URL, { method: "GET", headers }, ctx.requestTimeoutMs);
178
+ const bodyText = await res.text();
67
179
  if (!res.ok) {
68
- const text = (await res.text()).slice(0, 120);
69
180
  return {
70
181
  ...base,
71
182
  status: "error",
72
- error: `OpenCode Go dashboard error ${res.status}: ${text}`,
183
+ error: `OpenCode Go API error ${res.status}: ${bodyText.slice(0, 120)}`,
73
184
  };
74
185
  }
75
- const html = await res.text();
76
- const now = Date.now();
77
- const windows = [];
78
- for (const spec of WINDOWS) {
79
- const parsed = parseWindow(html, spec.field);
80
- if (!parsed)
81
- continue;
82
- windows.push({
83
- id: spec.id,
84
- label: spec.label,
85
- percentRemaining: clampPercent(100 - parsed.usagePercent),
86
- resetTimeIso: new Date(now + parsed.resetInSec * 1000).toISOString(),
87
- });
186
+ let data;
187
+ try {
188
+ data = JSON.parse(bodyText);
189
+ }
190
+ catch {
191
+ return {
192
+ ...base,
193
+ status: "error",
194
+ error: "OpenCode Go API returned invalid JSON",
195
+ };
88
196
  }
197
+ const status = (data ?? {});
198
+ const windows = buildWindows(status);
89
199
  if (windows.length === 0) {
90
200
  return {
91
201
  ...base,
92
202
  status: "error",
93
- error: "Could not parse any known OpenCode Go dashboard usage windows",
203
+ error: status.access
204
+ ? "Could not parse any known OpenCode Go usage windows"
205
+ : "No active OpenCode Go subscription",
94
206
  };
95
207
  }
96
- return { ...base, status: "ok", windows };
208
+ const useBalance = typeof status.useBalance === "boolean" ? status.useBalance : null;
209
+ const balance = await fetchBalance(headers, ctx.requestTimeoutMs, useBalance);
210
+ return { ...base, status: "ok", windows, ...(balance ? { balance } : {}) };
97
211
  }
98
212
  catch (err) {
99
213
  const message = err instanceof Error ? err.message : String(err);