Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: CI

on:
push:
branches: [main, "port/**"]
pull_request:
branches: [main]

Expand All @@ -25,3 +27,24 @@ jobs:

- name: Run tests
run: bun run test

windows-rust:
name: Windows Rust tests
runs-on: windows-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87
with:
toolchain: stable
components: rustfmt
- uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: "./src-tauri -> target"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: "1.4.2"
- run: bun install --frozen-lockfile
- run: bun run bundle:plugins
- run: bun run build
- name: Test native plugin host
run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

### Bug Fixes
- Provider fixes adapted from upstream OpenUsage 0.7.14: Codex scans the selected login folder, keeps slow history scans in the background, and uses Pro 100 / Pro 200 / Pro 500 plan names
- Codex history waits at most 150 ms per refresh; completed scans are retained for the next refresh and scoped by home, account and date range. WSL history stays disabled
- Cursor prefers structured team usage pools over legacy dollar allowances; legacy accounts keep their dollar meter
- OpenCode Go keeps reset countdowns for active sessions rounded below 1%, and drops untouched rolling-window placeholder resets


## v0.7.3

Logins inside WSL get their own cards, Antigravity and Copilot work from their CLI logins, and the Codex logo follows the theme.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ Settings → **Tray icon** picks what sits next to the clock. Settings → **Tas
- [**Amp**](docs/providers/amp.md) / free tier, bonus, credits
- [**Antigravity**](docs/providers/antigravity.md) / Gemini and Claude models, session and weekly
- [**Claude**](docs/providers/claude.md) / session, weekly, Fable, extra usage, rate limit resets, local token usage (ccusage)
- [**Codex**](docs/providers/codex.md) / session, weekly, reviews, credits
- [**Codex**](docs/providers/codex.md) / session, weekly, reviews, credits, local spend per account
- [**Copilot**](docs/providers/copilot.md) / credits, extra usage, chat, completions
- [**Cursor**](docs/providers/cursor.md) / credits, total usage, requests, auto usage, API usage, Grok Bot, on-demand, bonus spend, CLI auth
- [**Cursor**](docs/providers/cursor.md) / credits, total usage, structured team pools, requests, auto usage, API usage, Grok Bot, on-demand, bonus spend, CLI auth
- [**Factory / Droid**](docs/providers/factory.md) / standard, premium tokens
- [**Grok**](docs/providers/grok.md) / weekly limit, credits used, pay-as-you-go cap
- [**JetBrains AI Assistant**](docs/providers/jetbrains-ai-assistant.md) / quota, remaining
Expand Down
8 changes: 8 additions & 0 deletions docs/plugins/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,14 @@ host.ccusage.query(opts: {
provider?: "claude" | "codex", // Optional; defaults to plugin id, then "claude"
since?: string, // Start date (YYYYMMDD or YYYY-MM-DD)
until?: string, // End date (YYYYMMDD or YYYY-MM-DD)
accountId?: string, // Codex retained-history scope; not sent to the CLI
homePath?: string, // Provider home override (CLAUDE_CONFIG_DIR or CODEX_HOME)
claudePath?: string, // Legacy Claude-only override (deprecated; use homePath)
}):
| { status: "ok", data: { daily: DailyUsage[] } }
| { status: "no_runner" }
| { status: "runner_failed" }
| { status: "pending" } // Codex only: quota can render before local history finishes
```

Queries local token usage via provider-focused ccusage commands:
Expand Down Expand Up @@ -596,3 +598,9 @@ if (result.status === "ok") {
## See Also

- [Plugin Schema](./schema.md) - Plugin structure, manifest format, and output schema

Codex history queries return `pending` while their background scan is running and no retained result
is available. They wait at most 150 ms; later queries retrieve the completed result. The worker has
its own bounded execution budget and is shared by home folder. The optional `accountId` and date
bounds invalidate retained results when the login or query range changes. Plugins should leave live
quota visible and omit unknown spend while pending. WSL account copies still return `no_runner`.
9 changes: 9 additions & 0 deletions docs/providers/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,12 @@ Response returns new `access_token`, and optionally new `refresh_token` and `id_
## WSL

A login inside a running WSL distro shows as its own `Codex · WSL` card when it's a different account than the ones already shown. A stopped distro is never started; its card keeps its last data.

### Upstream 0.7.14 compatibility

The plugin already refreshes OAuth tokens and saves them to the selected account's own `auth.json`.
Local spend now follows that same file's parent folder, including `~/.config/codex` and extra accounts.
The host starts one background history scan per home, waits at most 150 ms for it, and retains the
result for later probes. Home, account ID and query dates scope retained data. Pending history does
not hide live quota or invent zero spend. History in WSL remains disabled to avoid slow network paths.
Plans `prolite`, `pro`, and `promax` display as Pro 100, Pro 200, and Pro 500.
4 changes: 4 additions & 0 deletions docs/providers/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,7 @@ WorkosCursorSessionToken=<userId>%3A%3A<access_token>
```

`userId` is derived from JWT `sub` (e.g. `google-oauth2|user_abc` -> `user_abc`).

Modern team seats use structured Auto/API pool percentages, and the reported total percentage when
present. The old dollar allowance does not replace those pools or synthesize a missing total.
Zero-valued placeholders beside positive spend, and malformed/incomplete pools, retain legacy handling.
4 changes: 4 additions & 0 deletions docs/providers/opencode-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ Percents are clamped to 0–100.
## WSL

A login inside a running WSL distro shows as its own `OpenCode Go · WSL` card when it's a different account than the ones already shown. A stopped distro is never started; its card keeps its last data.

A zero-percent rolling session can already be active. Its real reset remains visible; only a reset
within two seconds of a full five-hour period at capture time is treated as an untouched placeholder.
The HTTP Date header supplies the reference clock when valid. Weekly and monthly resets are preserved.
27 changes: 27 additions & 0 deletions docs/upstream-0.7.14-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# OpenUsage 0.7.14 provider port

Compared OpenTokenUsage main `d5feca2bf6a062e0fd2faedef2c1fdd9987a4f6d` (v0.7.3)
with upstream tags v0.7.13 and v0.7.14 (`cb21465e3d88d8b2075ca48d38c936b82177fb78`).
[Upstream release](https://github.com/robinebers/openusage/releases/tag/v0.7.14)
was published on October 6, 2026. The fork's v0.7.0 changelog entry records provider ports through
0.7.13; this comparison checks the code present in v0.7.3 rather than assuming those ports are complete.

| Upstream change | Windows/Tauri decision |
| --- | --- |
| Refresh and save independent Codex home tokens (#1322) | Already supported by file-backed auth, guarded reload and account environment bindings. Add a regression for Windows paths, token rotation and preserving the default login. |
| Codex multi-account local spend (#1349) | Bind local history to the selected auth file's parent, including `~/.config/codex`. Each configured account scans its own home. Swift's discovered-home/account assembly does not match the fork's explicit account cards. |
| Slow Codex history (#1338) | Keep one host worker per home across probes. Wait at most 150 ms; scope retained results by account identity and query dates. A completed result is available on later refreshes; pending history leaves live quota intact. WSL history stays disabled. |
| Codex plan names (#1332) | Map `prolite`, `pro`, `promax` to Pro 100, Pro 200, Pro 500. Keep other plan names. Swift header layout changes are excluded. |
| Cursor structured team pools (#1337) | Prefer two valid model pools and the reported total percent. Omit absent totals instead of computing them from legacy dollars. Preserve legacy meters for zero placeholders beside positive spend or incomplete/invalid pools. Keep existing labels and translations. |
| OpenCode reset below 1% | The React card already displays zero-usage resets. Preserve active session resets; drop only untouched rolling placeholders within two seconds of a full period at capture time, using HTTP Date when valid. Keep weekly/monthly resets. |
| Report an Issue (#1343) | Existing Help navigation already opens the fork's issues page. |
| macOS panel anchoring/navigation (#1345, #1346) | Excluded: native Swift panel behavior does not apply to the Windows panel. |
| PostHog 3.85.3 (#1347) | Excluded: no applicable analytics dependency in this fork. |

No plugin identities, metric labels, translations, logos or UI layout are changed. Existing brand
colors and `currentColor` logos remain valid. The new history `accountId` is local scope metadata,
not a CLI argument or network parameter. The host's existing redaction covers it and OAuth tokens;
a regression checks nested tokens while preserving pool percentages and reset timestamps.

CI now runs before a PR on `port/**` branches, and tests the native host on Windows as well as
running the frontend type check, build and full Vitest suite on Linux.
57 changes: 57 additions & 0 deletions plugins/codex/plugin.accounts.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { beforeEach, expect, it, vi } from "vitest"
import { makeCtx } from "../test-helpers.js"

beforeEach(() => vi.resetModules())

async function setup(home, accountId = "work-account") {
const ctx = makeCtx()
ctx.host.env.get.mockImplementation((name) => name === "CODEX_HOME" ? home : null)
const path = (home || "~/.config/codex") + "/auth.json"
ctx.host.fs.writeText(path, JSON.stringify({ tokens: {
access_token: "old-token", refresh_token: "old-refresh", account_id: accountId,
}, last_refresh: "2000-01-01T00:00:00Z", extra: "preserve" }))
ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({ tokens: { access_token: "personal-token" } }))
const personal = ctx.host.fs.readText("~/.codex/auth.json")
ctx.host.http.request.mockImplementation(({ url, headers }) => {
if (url.includes("oauth/token")) return { status: 200, bodyText: JSON.stringify({
access_token: "fresh-token", refresh_token: "fresh-refresh", id_token: "fresh-id",
}) }
expect(headers.Authorization).toBe("Bearer fresh-token")
expect(headers["ChatGPT-Account-Id"]).toBe(accountId)
return { status: 200, headers: {}, bodyText: JSON.stringify({ plan_type: "promax", rate_limit: {
primary_window: { used_percent: 15, limit_window_seconds: 18000 },
} }) }
})
ctx.host.ccusage.query.mockReturnValue({ status: "pending" })
await import("./plugin.js")
const result = globalThis.__openusage_plugin.probe(ctx)
return { ctx, path, personal, result }
}

it.each(["C:/Users/test/extra-codex", null])("refreshes and scans the selected auth home %s", async (home) => {
const { ctx, path, personal, result } = await setup(home)
expect(JSON.parse(ctx.host.fs.readText(path))).toMatchObject({ extra: "preserve", tokens: {
access_token: "fresh-token", refresh_token: "fresh-refresh", account_id: "work-account",
} })
expect(ctx.host.fs.readText("~/.codex/auth.json")).toBe(personal)
expect(ctx.host.keychain.readGenericPassword).not.toHaveBeenCalled()
expect(ctx.host.ccusage.query).toHaveBeenCalledWith(expect.objectContaining({
homePath: home || "~/.config/codex", accountId: "work-account", provider: "codex",
}))
expect(result.lines.find((line) => line.label === "Session").used).toBe(15)
expect(result.lines.some((line) => line.label === "Today")).toBe(false)
expect(result.plan).toBe("Pro 500")
})

it.each([
["prolite", "Pro 100"], [" PRO ", "Pro 200"], [" ProMax ", "Pro 500"],
["self_serve_business_prolite", "Business Premium"], ["plus", "Plus"],
["future_plan", "Future_plan"], ["", null], [500, null],
])("maps upstream plan %s to %s", async (raw, expected) => {
const ctx = makeCtx()
ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({ tokens: { access_token: "token" } }))
ctx.host.http.request.mockReturnValue({ status: 200, headers: {}, bodyText: JSON.stringify({ plan_type: raw }) })
ctx.host.ccusage.query.mockReturnValue({ status: "pending" })
await import("./plugin.js")
expect(globalThis.__openusage_plugin.probe(ctx).plan).toBe(expected)
})
20 changes: 12 additions & 8 deletions plugins/codex/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,9 @@
function formatCodexPlan(ctx, planType) {
const rawPlan = typeof planType === "string" ? planType.trim() : ""
if (!rawPlan) return null
if (rawPlan.toLowerCase() === "prolite") return "Pro 5x"
if (rawPlan.toLowerCase() === "pro") return "Pro 20x"
if (rawPlan.toLowerCase() === "prolite") return "Pro 100"
if (rawPlan.toLowerCase() === "pro") return "Pro 200"
if (rawPlan.toLowerCase() === "promax") return "Pro 500"
if (rawPlan.toLowerCase() === "self_serve_business_prolite") return "Business Premium"
return ctx.fmt.planLabel(rawPlan) || null
}
Expand Down Expand Up @@ -408,7 +409,7 @@
}
}

function queryTokenUsage(ctx) {
function queryTokenUsage(ctx, authState) {
if (!ctx.host.ccusage || typeof ctx.host.ccusage.query !== "function") {
return { status: "no_runner", data: null }
}
Expand All @@ -421,10 +422,13 @@
const d = since.getDate()
const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d
const queryOpts = { provider: "codex", since: sinceStr }
const codexHome = readCodexHome(ctx)
if (codexHome) {
queryOpts.homePath = codexHome
}
const codexHome = authState.authPath
? authState.authPath.replace(/[\\/]auth\.json$/, "")
: readCodexHome(ctx)
if (codexHome) queryOpts.homePath = codexHome
// Separate retained history when the login in a home changes.
const accountId = authState.auth.tokens && authState.auth.tokens.account_id
queryOpts.accountId = accountId || ctx.host.crypto.sha256Hex(authState.auth.tokens.access_token)

const result = ctx.host.ccusage.query(queryOpts)
if (!result || typeof result !== "object" || typeof result.status !== "string") {
Expand Down Expand Up @@ -848,7 +852,7 @@
}
}

const tokenUsageResult = queryTokenUsage(ctx)
const tokenUsageResult = queryTokenUsage(ctx, authState)
if (tokenUsageResult.status === "ok") {
const tokenUsage = tokenUsageResult.data
const now = new Date()
Expand Down
6 changes: 3 additions & 3 deletions plugins/codex/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ describe("codex plugin", () => {

const plugin = await loadPlugin()
const result = plugin.probe(ctx)
expect(result.plan).toBe("Pro 20x")
expect(result.plan).toBe("Pro 200")
expect(result.lines.find((line) => line.label === "Session")).toBeTruthy()
expect(result.lines.find((line) => line.label === "Weekly")).toBeTruthy()
const credits = result.lines.find((line) => line.label === "Credits")
Expand Down Expand Up @@ -300,7 +300,7 @@ describe("codex plugin", () => {
expect(weekly.used).toBe(44)
})

it("maps prolite plan to Pro 5x", async () => {
it("maps prolite plan to Pro 100", async () => {
const ctx = makeCtx()
ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({
tokens: { access_token: "token" },
Expand All @@ -324,7 +324,7 @@ describe("codex plugin", () => {

const plugin = await loadPlugin()
const result = plugin.probe(ctx)
expect(result.plan).toBe("Pro 5x")
expect(result.plan).toBe("Pro 100")
expect(result.lines.find((line) => line.label === "Session")).toBeTruthy()
expect(result.lines.find((line) => line.label === "Weekly")).toBeTruthy()
const credits = result.lines.find((line) => line.label === "Credits")
Expand Down
29 changes: 22 additions & 7 deletions plugins/cursor/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@
}

const normalizedPlanName = typeof planName === "string"
? planName.toLowerCase()
? planName.trim().toLowerCase()
: ""

const hasPlanUsage = !!usage.planUsage
Expand All @@ -629,9 +629,16 @@
typeof usage.planUsage.totalPercentUsed === "number" &&
Number.isFinite(usage.planUsage.totalPercentUsed)

const poolUsage = usage.planUsage || {}
const spent = readNumber(poolUsage.totalSpend) ??
((readNumber(poolUsage.limit) ?? 0) - (readNumber(poolUsage.remaining) ?? readNumber(poolUsage.limit) ?? 0))
const hasModelPools = [poolUsage.autoPercentUsed, poolUsage.apiPercentUsed].every(
(value) => typeof value === "number" && Number.isFinite(value) && value >= 0
) && (poolUsage.autoPercentUsed > 0 || poolUsage.apiPercentUsed > 0 || spent === 0)

// Enterprise and some Team request-based accounts can return no planUsage
// or a planUsage object without limit from the Connect API.
const needsRequestBasedFallback = usage.enabled !== false && (!hasPlanUsage || planUsageLimitMissing) && (
const needsRequestBasedFallback = usage.enabled !== false && (!hasPlanUsage || (planUsageLimitMissing && !hasModelPools)) && (
normalizedPlanName === "enterprise" ||
normalizedPlanName === "team"
)
Expand All @@ -645,7 +652,7 @@
}

const needsFallbackWithoutPlanInfo = usage.enabled !== false &&
(!hasPlanUsage || planUsageLimitMissing) &&
(!hasPlanUsage || (planUsageLimitMissing && !hasModelPools)) &&
!hasTotalUsagePercent &&
!normalizedPlanName &&
planInfoUnavailable
Expand All @@ -654,7 +661,7 @@
return buildUnknownRequestBasedResult(ctx, accessToken, planName)
}

if (usage.enabled !== false && planUsageLimitMissing && !hasTotalUsagePercent) {
if (usage.enabled !== false && planUsageLimitMissing && !hasTotalUsagePercent && !hasModelPools) {
ctx.host.log.warn("planUsage.limit missing, attempting REST usage API fallback")
try {
return buildUnknownRequestBasedResult(ctx, accessToken, planName)
Expand Down Expand Up @@ -712,7 +719,7 @@
}

// Total usage (always present) - fallback primary metric
if (!hasPlanUsageLimit && !hasTotalUsagePercent) {
if (!hasPlanUsageLimit && !hasTotalUsagePercent && !hasModelPools) {
throw "Total usage limit missing from API response."
}
const planUsed = hasPlanUsageLimit
Expand All @@ -738,11 +745,19 @@
const su = usage.spendLimitUsage
const isTeamAccount = (
normalizedPlanName === "team" ||
(su && su.limitType === "team") ||
(su && typeof su.limitType === "string" && su.limitType.toLowerCase() === "team") ||
(su && typeof su.pooledLimit === "number" && su.pooledLimit > 0)
)

if (isTeamAccount) {
if (isTeamAccount && hasModelPools) {
if (hasTotalUsagePercent) {
lines.push(ctx.line.progress({
label: "Total usage", used: pu.totalPercentUsed, limit: 100,
format: { kind: "percent" }, resetsAt: ctx.util.toIso(usage.billingCycleEnd),
periodDurationMs: billingPeriodMs,
}))
}
} else if (isTeamAccount) {
if (!hasPlanUsageLimit) {
ctx.host.log.warn("team-inferred account missing planUsage.limit, attempting REST usage API fallback")
return buildUnknownRequestBasedResult(ctx, accessToken, planName)
Expand Down
Loading
Loading