* Add OpenCode Go usage API support
* Preserve OpenCode Go Zen balance with API usage
* Update OpenCode Go capability gate
* Format OpenCode Go API overlay
* Document custom-pricing overlays and the current Usage & Spend surface.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Clarify custom-pricing overlay scope and cost JSON shapes.
The overlay only affects native Codex scans, and CLI cost JSON is not the Usage & Spend export schema.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Document Linux custom-pricing path and bare-key precedence.
The overlay lives in Application Support on every platform, and a bare model key wins over provider/model when both exist.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add the opt-in Claude statusLine usage feed
Implements the owner ruling on #2733: Claude Code publishes rate_limits to
whatever statusLine command the user configured, and reading what it publishes
does not cross the credential-ownership boundary the Keychain rules protect.
Off by default. When enabled, a .statusline step joins the app Auto order
behind .oauth and ahead of .cli, so it fills the gap while the polled sources
cool down and never pre-empts a successful OAuth read. When disabled the plan
is byte-identical to today: the step is inserted, not present-but-unavailable,
because planner order is asserted directly by tests and shown in debug output.
The source is never user-selectable. Pinning a feed that goes silent whenever
the user is not running Claude Code would strand the card, so it is excluded
from the picker and persists as Auto if it ever reaches settings.
Fail soft throughout: bad JSON, an unknown envelope version, wrong types, an
out-of-range percentage or a drifted schema all read as absence, which leaves
the polled sources in place rather than surfacing an error. The status line is
the user's own configuration and Claude Code owns the payload schema.
Observations are matched to the reporting profile by CLAUDE_CONFIG_DIR and
bounded to 15 minutes, so one account's numbers cannot render under another's
card and a stale live value cannot outrank a fresh poll. The snapshot asserts
no identity or plan, and the card labels the numbers as coming from the user's
own statusLine config.
* Document the statusLine feed and its drop-file contract
Describes the envelope, a minimal shim, and the profile/freshness rules, so the
feed is usable by hand while the managed-install question is open.
* Make the statusLine feed toggle actually reachable
The opt-in flag was read by the planner but nothing ever populated it: there
was no SettingsStore property, no defaults key, and no UI control, so it was
false in every build and the feed could not be switched on at all.
Wires it end to end — stored default (off), settings snapshot, menu
observation, and a Claude provider toggle — and adds a test that asserts the
user-facing switch reaches the snapshot field the planner reads.
The existing planner tests could not catch this: they construct
ClaudeSourcePlanningInput directly, so they pass whether or not anything
carries the user's choice into it. Removing the snapshot line again fails the
new test with the exact symptom that shipped.
* Require a capture time on statusLine observations
The parser defaulted a missing capturedAt to the parse time, so a drop file
that had sat on disk for hours read as current and the staleness bound stopped
protecting anything — a dead session could outrank a live poll indefinitely.
An observation that cannot be aged is now absence. The previous test asserted
the fallback as intended behaviour, which is why the suite stayed green; it now
asserts the drop instead.
* Cap how far ahead a statusLine capture time may sit
Future timestamps were accepted without bound to tolerate clock skew, so an
observation dated arbitrarily far ahead stayed fresh forever — the same defect
as a missing capture time, in the opposite direction. Skew is now tolerated up
to five minutes and rejected beyond it.
The previous test asserted the unbounded behaviour as intended, so the suite
stayed green; it now covers both the tolerated and the rejected case.
* Compose the statusLine feed over the last poll instead of replacing it
Addresses both P1 findings on the draft, now that #2675 has landed.
The feed carries only the 5h/7d windows. Publishing its result whole blanked
identity, plan, model-scoped weekly, Daily Routines, extra usage and cost —
the opposite of the "composes with, never replaces" constraint in the owner
ruling. Its windows are now merged over the last polled Claude snapshot, and a
window the observation omits means "no update" rather than "cleared".
Composition also carries the account guard the feed cannot supply itself.
Matching CLAUDE_CONFIG_DIR cannot distinguish an account switch within one
profile, so an observation is discarded outright when the active account has
changed since the snapshot it would compose over. Publishing it there would
render one account's windows beneath another account's identity.
Follows the existing shape of this apply path, which already reconciles
results against previous state for Codex, CommandCode, DeepSeek and reset
backfill. The helper lives in its own file and the call site replaces an
existing binding, so the refresh function stays within its length budget.
The card note keeps precedence over the dataConfidence note added by #2675: a
composed snapshot inherits its confidence from the previous poll, so a prior
CLI scrape would otherwise label live statusLine windows as CLI-sourced.
* Address the three Codex findings on the statusLine feed
Persist the opt-in toggle. The setter only updated the in-memory defaults
state, so the feed silently switched itself off on the next launch. It now
writes the key and notifies the background-work path, matching the adjacent
provider toggles.
Stop promoting a weekly-only observation into the session lane. This snapshot
is composed over a previous one where primary is the session row, so a
promoted 7-day figure rendered as the 5-hour limit while the real weekly row
survived beside it. A weekly-only payload is now absence rather than a
lane-shifted guess.
Verify ownership of the rows being composed over. The active-account
observation only proves the account held still during a fetch; if the user
switches accounts and the next refresh is served by the statusLine file,
nothing in that fetch re-read the account, so stability alone would let the
new account's windows sit beneath the old account's identity. The account
behind each polled snapshot is now recorded and must still be active for the
feed to compose over it, and a snapshot the feed itself produced never becomes
that evidence.
* Keep the statusLine opt-in through launch
The launch reset that clears Claude web extras outside CLI mode was also clearing
the statusLine feed. The planner emits the feed's step only under `.auto`, which is
exactly the branch that reset covers, so an enabled user lost the opt-in on every
launch and the feature was unreachable after a restart.
The reset is CLI-scoped by design; the statusLine line never belonged in it.
No test caught this because the reset lives in the non-test branch of an
`isRunningTests` gate, so the suite never executed it. Adding
`writesLaunchResetsToRawState` — mirroring the existing
`performInitialProviderDetection` seam — lets a test take the production path.
Verified by reintroducing the defect and watching the new test fail on both
assertions before it passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Restore the provider architecture gatekeeper on this branch
`cross provider case clusters are derived or specifically justified` passes on
main and failed on this branch. The allowlist pins each guarded construct to an
exact line with a text anchor, and earlier commits here inserted lines into
UsageStore+Refresh.swift, UsageStore.swift and MenuCardView+ModelHelpers.swift,
so 44 entries drifted off their anchors and every orphaned entry then cascaded
into an unjustified-construct failure.
- Recompute the 44 drifted line numbers against their anchors.
- Justify the two Claude constructs in the composition file, which were added
with no marker at all. The first reads better hoisted into a named local than
as a comment buried in an argument list.
- Update the MenuCardView cluster fingerprint: the statusLine note sits between
the .kiro and .kilo checks, so the gatekeeper now sees one three-provider
cluster where the entry expected two. The maintainer's reason text still
describes it accurately and is left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Keep the Claude chain moving when a feed observation cannot be attributed
Composition returned nil when a statusLine observation could not be attributed to
the stored rows. By then the fetch pipeline had already accepted the statusLine
result, so nothing fell through to the CLI probe: the refresh published nothing
and the card kept whatever it was showing.
Three things combined to make that permanent rather than transient:
- Feed-stored snapshots recorded no owner at all, so the first feed-only publish
created rows that no later feed refresh could ever attribute. The account UUID
is read from the environment, not from the observation, so recording it for
feed snapshots is evidence about when the rows were stored — exactly what the
next composition needs to ask about.
- The record lived only in memory while the snapshots it describes are persisted,
so every relaunch began with an unknown owner against restored rows.
- Rejection was a decision about whether to publish rather than about which rows
to trust.
Ownership is now three-state. "Cannot tell" is not "belongs to someone else":
only the latter justifies discarding rows the user can currently see, so an
unreadable account republishes the previous snapshot rather than blanking
identity, plan and extra rows on a guess.
This does not gate the step at plan time as the review suggested, which would
mean plumbing composability from UsageStore through the settings snapshot into
the Core planner. The stall it protects against no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Make statusLine ownership a condition of the source, not a decision after it
An unowned observation was being resolved after the fetch had already committed
to it, where every remaining option was wrong: publish rows that cannot be
attributed, publish nothing and stall the chain, or blank verified rows on a
guess. Ownership is now part of whether the step is offered at all. When the
stored rows are not provably owned by the active account the source is not
planned, OAuth -> CLI -> web runs as it did before the feed existed, and a poll
that carries its own identity re-establishes ownership on its own.
Two findings this closes:
- A feed observation no longer records who owns the rows it produced. The account
UUID is read from the environment, so it is evidence about when the rows were
stored, not about whose usage they count. The drop file is scoped to the
profile rather than the account and its freshness window is minutes wide, so a
file written by the previous account's session was being stamped as the new
account's and composed beneath its identity on the next refresh.
- Composition reports whether the published rows came from the fetched result,
and the source label only moves when they did. A discarded observation
republishes the previous rows, and claiming its label made the card attribute
an old OAuth or CLI reading to the user's status line.
Worth recording for whoever owns this next: the payload carries no account
identity of any kind, and its drop file is shared by every account on a profile.
The feed cannot attribute itself. It can only ever supplement rows whose owner is
already established, which is what the planning gate now enforces structurally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: add Keychain-free Claude statusLine feed
Co-authored-by: Luis Gonzalez <luis@getduckbill.com>
* test: stabilize subprocess timing under load
* fix: import CoreFoundation for Linux statusLine build
* test: register statusLine localization fallbacks
* style: wrap statusLine localization fallback
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The serve dashboard ignored the app's "Hide personal information"
toggle and always served full account emails unless --identity was
passed. Resolve the identity mode per request when the flag is
absent, so the toggle applies without a serve restart, and fold the
resolved mode into the response cache key so a body cached before a
toggle cannot be replayed after it. An explicit --identity still
wins. codexbar dashboard keeps its full default.
Redaction was the dashboard-v1 default at every layer; the --identity
flag already existed on both the one-shot command and serve. Flip the
default to full identity and keep --identity redacted as the opt-in
privacy setting for snapshots that cross untrusted networks. The web
UI stops re-redacting rows before persisting them to localStorage so
cached paints match what the server serves in either mode.
Three layers, one goal: never make the browser wait on a snapshot build
(measured 46.5s cold vs 22ms warm on the portal host).
- Stale-while-revalidate: expired cache entries within staleTTL answer
immediately from the last-good response while a coalesced background
rebuild commits fresh data. Applies to /usage, /cost, and the
dashboard snapshot; failure-fallback rules are unchanged.
- Snapshot query params (schema stays v1): ?provider=<id> builds and
caches one provider row independently; ?detail=shell returns
config-only rows with no provider fetches or cost scans.
- Web UI: renders the last snapshot from localStorage instantly on
revisit, paints skeleton cards from the shell on cold start, fills
each card as its provider resolves, then hands off to normal polling.
Static-webroot publishers currently shell-wrap the one-shot command
(codexbar dashboard > tmp && install tmp target) to avoid readers seeing
a partial document. --output writes the snapshot atomically instead:
staged temp file in the destination directory, fsync, rename(2) over the
target, 0644. Stdout stays untouched when the flag is absent and silent
on success when writing to a file. Empty paths are an args error and a
missing parent directory fails with a clear message; directories are
deliberately not created.
When a serve operation outlived its request deadline, the coordinator
marked the slot timed out and threw away the eventually-produced value:
it never reached the accept commit (response cache) and the queued
same-config successor restarted the source from zero. On machines where
the dashboard snapshot build outlasts --request-timeout, GET
/dashboard/v1/snapshot 504ed forever while each retry burned another
full cost-history scan.
Timed-out sources now still commit through accept and hand the fresh
result to a same-fingerprint successor without rerunning the source;
changed-config successors promote as before. The web UI shows a
friendlier 504 message noting it retries automatically.
GET / now serves a self-contained HTML page that polls
/dashboard/v1/snapshot and renders provider cards with usage windows,
credits, and cost. The static UI stays unauthenticated (it carries no
account data); tokens are entered in-browser, kept in localStorage, and
sent as a bearer header. No visibility gating on polling so embedded
panes that report document.hidden permanently still refresh.
* refactor: migrate provider details batch one
* refactor: migrate provider details batch two
* refactor: migrate provider details batch three
* fix: preserve provider detail rendering parity
* fix: migrate Copilot credits into provider details
* fix: pin Poe details to UTC
Poe timestamps are UTC and the daily buckets already used it, but the
recent-activity labels and Today bucket used the local zone in both the
Swift and JS paths, flaking the golden on non-Pacific runners. Golden
proven invariant under TZ=UTC, America/Los_Angeles, Asia/Tokyo.
* fix: normalize OpenRouter reset detail
* test: pin Poe menu fixture to UTC
* fix: preserve OpenRouter key details
---------
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
* feat(cli): add `hooks watch` so hooks fire without the macOS app
`HookRunner.dispatch` was only ever called from `Sources/CodexBar/UsageStore+Hooks.swift`,
so hook rules could be configured on Linux/headless but could never fire. `serve`'s
refresh interval is a cache TTL, not a poller, and `hooks test` only fires a synthetic
event on demand.
Add `HookTransitionDetector` to CodexBarCore: platform-neutral, side-effect free, and
built on the primitives Core already ships (`QuotaLowHookThreshold.crossedRules`,
`QuotaWarningWindow`, `HookRule`, `HookRateLimiter`). It preserves the app's semantics:
the first sample of a lane establishes a baseline without firing, `unknown`/`maintenance`
never flip provider status, a rule change clears baselines, and disappearing lanes are
pruned. All events are edge-triggered, which is what `HookRunner` already assumes when it
declines to rate-limit quota events.
Add `codexbar hooks watch` to drive it: `--interval` (default 300s, minimum 60s, rejected
rather than clamped), `--provider`, `--once`, and JSON output. Runs read-only like
`guard`, stops on SIGINT/SIGTERM/SIGHUP, and reports refresh failures with a coarse
category so raw provider errors are never forwarded.
The macOS app is intentionally untouched.
* fix: address review — filtered quota_low dispatch, session-only reached, interruptible sleep
Three bugs from automated PR review:
- quota_low dispatched against the full hooks config, so HookRunner re-evaluated
every enabled rule's `matches()` against the current reading, re-firing any rule
whose threshold sat below the one that actually crossed. `HookDispatch` now
carries the pre-computed crossed subset for quota_low, mirroring the app's
`HooksConfig(events: crossed)` pattern; other events still use the full config
since `matches()` doesn't threshold-recheck them.
- quota_reached fired for every lane, including weekly. Documented and the app's
own call site (`emitQuotaReachedHook(sessionWindow:)`) restrict it to session;
gated accordingly.
- SIGINT/SIGTERM/SIGHUP during the poll sleep only flipped a flag; the running
Task.sleep wasn't cancelled, so `hooks watch` could appear hung for up to the
full interval. Replaced with `sleepInterruptibly`, a 200ms-tick loop that checks
the stop flag between ticks.
Each fix has a regression test proving the bug and the fix.
* fix: validate watch arguments before loading configuration
`runHooksWatch` called `loadConfig` before decoding `--interval` and `--provider`.
`loadConfig` exits the process on a malformed config, so a broken config file
masked the documented argument errors: `hooks watch --interval 5` reported a
config decode failure instead of the interval floor error.
Reordering alone would leave the coupling in place, so the decoders no longer take
a config at all. `decodeHooksWatchProviderNames` validates explicit `--provider`
names and returns nil to mean "fall back to the enabled set"; the new
`hooksWatchProviders` resolves that fallback once the config is available. Both
command-only decoders now run before `loadConfig`, which makes the ordering defect
structurally impossible rather than merely corrected.
Verified against a malformed config: `--interval 5`, `--interval abc`, and an
unknown provider all report their own errors, while valid arguments still surface
the config decode error.
* fix(cli): require continuous hooks watch
---------
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
docs/cli.md gains the new serve flags and endpoint; docs/dashboard-api.md
describes the bearer-token auth, the plain-HTTP threat model (the token
crosses the network in cleartext on every request), the loopback default,
the --allow-plain-http acceptance, a Caddy reverse-proxy recipe for TLS,
token hygiene (openssl rand -hex 32, prefer CODEXBAR_DASHBOARD_TOKEN,
rotate by restart), and the snapshot payload contract.
Both files state plainly that on a non-loopback bind the token gates ALL
data routes (/usage, /cost, /dashboard/v1/snapshot) and that /health is
always open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Group local Codex usage and costs by canonical project and worktree source across menus, CLI text, and JSON output. Preserve the existing cache artifact while migrating project metadata safely, and avoid rendering legacy cache rows before migration.
Co-authored-by: Clemens Peters <c.peters@conventic.com>
Add session pace to text output, expose derived pace data in JSON, and respect the configured weekly work-day baseline.
Co-authored-by: Kentoku Matsunami <kentoku.matsunami@bizreach.co.jp>
* serve: report build version on /health for stale-binary detection
Expose the running build version in the `codexbar serve` /health
response so polling clients (e.g. showy-quota, glean) can compare it
against `codexbar --version` and recycle a serve process still running
an older binary after an app update. Omitted when unresolvable.
Add CLIServeRouterTests coverage and document the field in docs/cli.md.
* serve: capture /health version at startup, not lazily
Resolve the running build version once when `serve` starts (via a
ServeRuntime context) instead of a lazily-initialized static let. A
static let resolves on the first /health request, which can land after
an in-place update replaced the on-disk binary — making a stale serve
report the newly installed version and defeating the client
stale-process detection this field exists for.
Addresses Codex review feedback on PR #1703.
* chore: polish health version reporting
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>