OpenCode rejects a dead auth cookie with HTTP 302 to /auth/authorize (or
401/403). The HTTP client now disables redirect following so this surfaces
as a FetchError::Unauthorized instead of a 200 login page that fails to parse.
ocd health probes each account's cookie against its default workspace Zen
page and reports alive/dead/errored/misconfigured with a total count. Exit is
0 only when all accounts are alive.
The main walk (pretty/raw/fetch) also classifies a dead cookie as the new
ReportStatus::CookieDead, labels it [cookie dead] in pretty output and
cookie_dead in raw output, and continues with the valid accounts rather than
aborting. Verified live: valid account surfaces [ok], dead account surfaces
[cookie dead], unfilled account surfaces [cookie dead].
The Zen page embedded record streams reloadAmount, reloadTrigger,
subscriptionPlan, monthlyLimit, and monthlyUsage, none of which appear in the
rendered DOM. Note as an open item for a future 'ocd billing' command.
OpenCode is React Router v7 with streaming SSR; loader data is inlined in the
HTML as JS resource objects (e.g. rollingUsage:$R[n]={status,resetInSec,
usagePercent}). There is no separate JSON HTTP endpoint. parse the embedded
records first (exact resetInSec integers + a status field), and fall back to the
data-slot DOM when the embedded path finds nothing.
Add optional status field to UsageFrame (serde default, schema-compatible).
embedded_usage_frame scans every key occurrence so a billing-record
'monthlyUsage:null' sibling does not shadow the real usage object. embedded_balance
scans past 'balance:null'. Verified live: all three frames parse, month no
longer blank, status captured, resets_at driven by resetInSec.
HttpClient.get_text now accepts etag/last_modified and sends
If-None-Match/If-Modified-Since; a 304 returns no body and the caller reuses
the cached parse. fetch_target recovers validators from the stale cache entry
(via new cache::read_raw, TTL-agnostic) and stores whatever the server emits.
Verified live: OpenCode/Cloudflare returns no ETag or Last-Modified on either
page, so the slots stay None, no headers go out, and 304 never fires. The
machinery is correct and ready for the day they add validators.
Client builds the Cookie header as 'auth=<value>; oc_locale=en' from the bare
auth value the config supplies, so oc_locale defaults to English and the
scrape selectors match. Legacy full-header configs pass through. Default
User-Agent is a desktop Chrome string; --user-agent still overrides. Fix
relative-time past tense ('1m ago' not 'ago1m'). Verified live against a real
OpenCode account: balance, three usage frames, and reset times all parse.
Go page (/workspace/{id}/go): parse three [data-slot=usage-item] frames
(Rolling/Weekly/Monthly) into usage percentages + computed resets_at from the
relative reset-duration text. Zen page (/workspace/{id}): parse
[data-slot=balance] b into remaining + currency unit. Add an offline unit test
against tmp/ captures. Fix pretty renderer (percent display, money format,
day-aware relative times). Update DESIGN.md, README, examples and config
template to the real wrk_ workspace ids.
Rust CLI with clap subcommands (pretty/raw json/raw plain/fetch/cache/config).
Per-workspace dual HTTP (Zen balance + Go limits) run with tokio::join,
500 ms workspace pacing, TTL-180s file cache, retry/backoff for transient
failures. scrape.rs holds the only OpenCode-coupled HTML selectors (placeholders
until the real Zen/Go pages are captured). Unicode blitter-block bars in
pretty mode; stable JSON/plain raw output for widgets and agent plugins.