Skip to content

Releases: thevibeworks/ccpace

v0.10.0 - Accounts and Codex

Choose a tag to compare

@github-actions github-actions released this 08 Sep 09:32

Claude and Codex accounts now share one usage overview. Bare ccpace opens
Accounts when several accounts are available, or the calendar for one account.
Select an account to inspect its usage without changing an agent's login.

  • Read-only Codex OAuth collection from CODEX_HOME/auth*.json or explicit
    --codex-file paths. Usage is routed by workspace and stored separately
    per provider and account. Native auth files are never rewritten.
  • Codex windows use reported durations, including weekly-only and scoped
    payloads. Banked-reset inventory and paid credit balances stay separate.
    No reset is redeemed or purchased by ccpace.
  • Inactive windows remain visible as No active window. Calendar rows are
    local clock ticks (00:00, 06:00, 12:00, 18:00), independent of
    whether a 5h quota window has started.
  • Single click selects, double-click inspects, and date headings select a
    day. Horizontal wheel events and Shift+wheel pan weeks with momentum
    coalescing. 0 opens Accounts; Ctrl+PageUp/PageDown cycles views.
  • --watch now opens the calendar. --once and versioned --json output
    provide noninteractive snapshots; --raw retains the legacy Claude JSON.
    Piped output stays noninteractive. Scripts using the old --json alias
    should switch to --raw or adopt the new schema.

Live Codex collection was checked against a staged subscription login,
including reset inventory and unchanged auth-file contents. Synthetic tests
cover account isolation, cache fallback, source deduplication, missing reset
times, and mouse/keyboard navigation. Expired Codex credentials require a
fresh login through their owning CLI or account manager.

uvx --refresh ccpace --demo

v0.9.0 - Usage calendar

Choose a tag to compare

@github-actions github-actions released this 08 Sep 07:07

Your 5h allowance can run out while much of the weekly pool goes unused.
ccpace --calendar puts both conditions on one screen, with a browsable
week, hourly usage patterns, quota-period history, and alerts.

  • Spectrum, Quiet, and Paper palettes. Change the theme from the picker,
    with Ctrl+t, or through --theme / CCPACE_THEME.
  • Account-wide and model-scoped limits stay separate. Forecasts use the
    shared history model and stop at the current quota or access boundary.
  • Missing observations stay blank; recorded zero remains 0.0. Long
    observation gaps are not assigned to individual hours. Select an interval
    for its evidence, or press Enter for hourly detail.
  • Persistent warning transitions, acknowledgement, and JSON notifier hooks.
    Forecast warnings require two distinct observations. Stale data and
    escalation into a cap cannot announce recovery. No execution control.
  • ccpace --calendar --demo runs synthetic scenarios without credentials,
    provider requests, usage writes, or notifications.

The calendar uses Textual and requires the package or a full checkout.
The compact ccpace and --watch views remain available. Supported on
macOS and Linux with Python 3.11+. Weekly underuse thresholds are
experimental; forecasts are estimates, not promised capacity.

uvx --from ccpace==0.9.0 ccpace --calendar --demo

v0.8.0 — name the wall

Choose a tag to compare

@lroolle lroolle released this 02 Sep 08:44
6c1785d

Claude Code can now offer /low-priority at a spent 5h session window, but
the gated offer, active mode, and separate allowance do not exist in
/api/oauth/usage. ccpace now says what its source can prove:
5h capped · 47% of 7d left · back @Tue 2 04:00. It distinguishes the
shorter wall from the weekly pool without claiming a session-only escape is
available for an account it cannot see into.

Notifications now carry the same facts. Their producers and formatter had
drifted onto different key names, causing real threshold and delta messages
to say 0%; the payload now has canonical window, utilization,
reset_at, and reset_time fields while retaining the old aliases for
custom hooks. Custom-notifier envelopes also gain a stable event id, such
as full:work:5h:<reset>, for dedupe and tracing across restarts.

The old watch cache used the maximum of every counter as an account-level
stop: 5h at 100, or one scoped model at 100, froze the whole account until a
reset. That is false once lower-priority service can bypass 5h, and it was
already false for another model. Worse, the early return ignored a newer
shared cache from statusline and even r. The cache is now reserved for
one genuinely terminal state — aggregate 7d spent with no paid path — and
both newer shared evidence and manual refresh evict it.

85 tests.

v0.7.0 — a guess may not delete a window

Choose a tag to compare

@lroolle lroolle released this 02 Sep 06:03

The × cell is retired. The ledger used to overwrite dry-projected
ahead-cells as red ×, and drawn out in a run they read as deleted
windows
— measured live the day statusline's unfold exposed the same
run on its row (▮▯▯▯×××: six windows to the reset, counted as three
by the person the row exists for). A future cell is a slot, never a
verdict. The wall already has an owner with better gates and an exact
time: the 7d dry ~... advice row directly under this ledger. Every
cell ahead now draws hollow — dim where your learned hours say you
sleep — to the grid's edge.

Ships with statusline v0.39.0 ("a guess may not delete a window"),
which retires its future × cells the same way and keeps the mark only
in its folded token, where no per-cell shape exists to say it. No
forecast.cache change of any kind.

v0.6.0 — the night on the ledger

Choose a tag to compare

@lroolle lroolle released this 02 Sep 06:03

v0.4.0 taught the forecast that you sleep. The ledger still did not know:

▂▃▅▁▂▄█▃▁▁▂▅▄▃▁▂▃▅▄▂▁▃▄▅▃▂▮▯▯▯▯▯▯▯

Nine hollow cells, all drawn the same, and three of them are the middle of
two nights. The row said "nine slots ahead" while the budget line beside it
said ~6 awake — one surface counting clock, the other already counting
yours.

The future is a shape, not a count

An ahead-cell whose 5h slot has under REST_SLOT_AWAKE_MIN_SECS (9000 —
half a window) of waking seconds now draws DIM. Same ▯: the glyph is the
fact, the tint is the refinement, and a reader who cannot see the tint
loses nothing they could have acted on — a dim ▯ is still a window ahead,
it is just one the capacity is unlikely to reach. Waking hours are the
v0.4.0 arithmetic exactly (mult >= REST_MULT_MAX over the slot's real
wall span), so nothing here is a second opinion about your day.

The wall span is the whole rule. 20:00–01:00 and 05:00–10:00 straddle the
same night's two edges and the hour a slot OPENS in gets both of them
wrong: the first is four waking hours and a window you can spend, the
second is two and a night. On the 5h grid every slot straddles something.

Gated on the evidence the walk already needs — a valid hour_profile and
FORECAST_MIN_DAYS of history. Unlearned, the row is byte-for-byte the
row it was in v0.5.0, tints and all, and the tests hold it there against
every way of not knowing: no field, a truncated one, a nonsense one, a
real one with a fortnight of history missing behind it.

× beats rest — a slot the pool will not cover is unreachable for a
stronger reason than sleep, and drawing it as a night would hide that. ▮
and every cell of the record are untouched.

Same rule on both surfaces

REST_SLOT_AWAKE_MIN_SECS is a shared READING RULE, not a cache field:
statusline v0.38.0 dims the same slots on its own 7d strip (and each rest
hour on the 5h one) off the same forecast.cache. schema stays 2; no
field moved. Documented in docs/statusline-interop.md. The dim cells and
the budget's ~N awake may differ by one — the strip is a grid on the
period start, the count comes from real clocks — the same tolerance the
window count itself already carries.

Internally the two now share one awake_seconds(); the budget's count and
the ledger's nights were never allowed to be two implementations.

v0.5.0 — two pools, one wall

Choose a tag to compare

@lroolle lroolle released this 02 Sep 06:03

An account at 81% of its week with a model-scoped pool at 63% has two
counters heading for the same wall at different speeds, and nothing on the
block said which cap binds or what it costs:

7d     81% ████████▓░  2d 3h   @Wed 19 09:00       1.2x
fable  63% ██████▒▒▒░  2d 3h   @Wed 19 09:00       0.9x

The 7d cap ends the week for every model, so those 37 fable points are not
37 points of headroom — at this week's mix, 22 of them expire untouched.

The ratio is the estimator

Both counters start at the same reset instant, which makes the live ratio
between them this week's MIX RATE — scoped points per 7d point — with no
history behind it at all:

mix       = scope / seven
reachable = round((100 - seven) * scope / seven)
strand    = round(100 * (seven - scope) / seven)   == (100 - scope) - reachable

81/63 gives mix 0.78, reachable 15, strand 22. Mining the corpus for the
same week's dF/dS put it at 0.77, so this is not an approximation of the
measurement, it is the measurement — available in the payload already on
screen. No new forecast.cache field, no schema question, nothing to
learn and nothing to wait two weeks for.

What is NOT published: the pure-scope coupling, what a scoped point costs
the account when only that model runs. n=22, and the band is wide enough
to be fluent and wrong.

One row, above the budget

fable: ~15% of its 37% left reachable at this mix · heavier fable extracts more
budget: ~9 windows left · ~6 awake · 9.3%/window stays even · lands ~91% on your pattern

An info row immediately before the budget line, because it qualifies the
very headroom the budget then rations. It states the reachable half rather
than the strand: 22 wasted points is not something a reader can act on,
and running that model heavier is. Where the payload names no running
model the deepest scoped pool answers, unless one declares itself active —
depth is a guess at which pool the reader cares about, is_active is the
account saying it outright.

Gated so the ratio stays honest, and statusline v0.37.0 gates its own
notice on the same five: one wall (both resets within 120 s — Anthropic
could split them someday), SCOPE_MIX_MIN_7D = 60 (which cap binds is a
question only near the end), SCOPE_MIX_MIN_SCOPE = 5 (an untouched model
is the underuse question, not a mix), neither pool capped (that is its own
notice), SCOPE_STRAND_MIN_PCT = 10 (under that it is rounding wearing
advice), and the existing young-week guard. The constants are shared
READING RULES, documented in docs/statusline-interop.md; statusline adds
two mutes ccpace has no mechanism for and did not invent.

v0.4.0 — the hours you keep

Choose a tag to compare

@lroolle lroolle released this 02 Sep 06:03

Claude Code can work 24/7. You cannot, and the forecast did not know the
difference: it learned a WEEKDAY profile and then burned it flat through
the night. So a week that really ran out on Thursday morning printed

7d dry ~Thu 03:00, 30h before reset; then hard stop until reset
budget: ~9 windows left · 6.2%/window stays even

A wall placed mid-sleep is a false alarm at 11pm and a missed warning at
09:00, and a ration divided across windows you sleep through asks you to
hit a number lower than the one you can actually spend. Both are the same
missing fact. The corpus already held it: burn credited by the envelope
pass carries a timestamp, and hours that never burn across weeks are the
hours you rest.

hour_profile: the shape of your day, in the shared cache

24 multipliers by local hour, mean 1.0, so the rate at hour h is
weekday_rate * mult[h] and a whole day still burns its weekday total —
only the shape inside the day changes. Built on the same pass and the same
constants as the weekdays: each envelope delta is credited to its local
(day, hour), today is excluded (partial, never a training day), the rest
are EWMA-weighted at the 14-day half-life, and each hour's share of the
week becomes its multiplier.

Floored at 0.1 and renormalized to a mean of exactly 1, in that order, at
BUILD time. The floor is the hedge for the occasional overnight autonomous
run — a rest hour projects a tenth of a uniform hour, never zero — and the
order matters, since flooring after the normalization would publish a
shape whose mean is no longer 1. Build-time rounding matters because the
cache is SHARED: statusline computes the identical field off the same log,
and two writers rounding their own way is two answers to one week.
schema stays 2 — the model of the existing fields did not change, and a
reader that has never heard of the field keeps working. Contract in
docs/statusline-interop.md and docs/data.md.

Read defensively and never fatally: all 24 keys, every value numeric in
[0, 24], mean in [0.9, 1.1], or the walk takes flat and carries on. A bad
hour shape decides only whether the forecast knows when you sleep; the
weekday guards still decide whether it speaks at all.

The walk steps by the hour

project_week now walks local hour boundaries instead of local days — at
most 169 segments for a week — and multiplies each segment's weekday rate
by that hour's shape. With no learned shape every multiplier is 1 and the
numbers are the day walk's to thirteen decimal places, which the suite
asserts. The dry warnings needed no copy change: the shaped walk moves the
dry TIME out of the night by itself, and that is the early-warning fix.

Two behaviours moved, both deliberately:

  • The 24h blend (max(weekday, recent_24h) over the first day) is tested
    at the start of each segment, and a segment used to be a calendar day —
    so a blend that began 15h out ran to 39h. It now ends at 24h.
  • A spring-forward day is 23 hours long and now burns 23 hours of quota.
    The day walk sized its segments by subtracting two datetimes that shared
    one tzinfo, which Python does on the WALL clock, so the skipped hour was
    credited anyway — twice a year, in every zone that moves. Segments are
    measured in absolute seconds off the local clock's own minute.

~6 awake: the ration you can actually spend

budget: ~9 windows left · ~6 awake · 9.3%/window stays even · lands ~52% on your pattern

An hour whose multiplier is under REST_MULT_MAX (0.25, a shared
constant) is rest. Count the waking seconds between the end of the window
you are in and the end of the week, ceil them into 5h windows the same way
windows_ahead ceils — a partial window is still spendable — and clamp to
the window count itself. The clause appears only when the shape is learned
on at least two weeks of history and the two counts differ, and it names
the ration's denominator by sitting beside it. Nothing awake ahead is not
a rate: the line states the count and stops rather than divide by zero or
quote a number nobody can spend. When paid access ends before the reset,
the awake count is measured over the truncated span too — one horizon per
block, as the runway, the ledger's and the landing already were.

windows_ahead itself is untouched. The countdown invariant stays; the
budget line owns the refinement.

v0.3.1 — one uuid rule, and the corpus the forecast stands on

Choose a tag to compare

@lroolle lroolle released this 27 Aug 07:59

Two readers, one rule. load_account_history partitioned by uuid and
dropped rows that carried none; weekday_burn_forecast let those same
rows through, on the theory that alias-scoped directories are
single-account. They are not — one real store held twelve uuids — and
two filters that disagree are a leak waiting for the first caller that
skips the loader. The forecast now applies the loader's rule and nothing
else.

The drop is counted. A row without a uuid is refused, never guessed
(thirteen of ninety-three in one store carry an email that would
identify them, which is exactly the temptation to resist on a log that
has already interleaved accounts), but a reader that discards
identifiable observations silently will discard a larger number just as
quietly. load_account_corpus returns the samples and a Corpus: files
read, rows kept, rows dropped for no uuid, rows of other accounts, and
the oldest kept timestamp.

That corpus is stamped into forecast.cache. schema versions the
MODEL and cannot say which samples it ran over: statusline reads one
directory, ccpace reads every store under the root, both count burn the
same way, both pass the gate — and the same account reads
days_history: 28 or 301 depending on which binary rendered last.
corpus: {uuid, files, samples, dropped_no_uuid, oldest} makes that
visible in one jq. Informative, not a gate; docs/data.md has the
contract.

v0.2.0 — no lockouts, the account is the identity, the ledger reads as days

Choose a tag to compare

@lroolle lroolle released this 19 Aug 07:37

Why upgrade

If ccpace ever sat on rate limited (429) (retry in 59m) while /usage in Claude Code worked fine, or its 5h-window ledger went blank in a deva container: both are gone.

  • A failed fetch is a badge, not a lockout. Last good numbers stay on screen with (stale 12m · !429); the next poll is the retry. ccpace never writes or gates on statusline's usage.err.
  • The account is the identity, not the directory. The default .credentials.json follows the statusline's own rule (STATUSLINE_ACCOUNTDEVA_AUTH_TAG › root); history is read from every store and partitioned by account uuid — the ledger cannot vanish because of where a sample landed. .credentials.work.json no longer collides with the default account.
  • Watch asks only when the answer can have changed: no Claude Code activity since the last fetch and no reset → no request; r overrides. has_command no longer crashes the loop on Linux; notifications are fenced.
  • --bark reads the bark CLI's env (BARK_KEY/BARK_SERVER/BARK_GROUP/BARK_ICON).
  • The ledger reads as days: ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ … ▆▆ˍ▂▮▯▯ — day gaps in history, ˍ negligible, ahead; the first window of the week is no longer lost to a jittered resets_at. Same grammar as claude-code-statusline's new week row.
  • DESIGN.md: the language on one page.

Full rationale in CHANGELOG.md. uvx ccpace (PyPI upload follows).

v0.1.1

Choose a tag to compare

@lroolle lroolle released this 06 Aug 08:27

Shared fetch pool with claude-code-statusline + forecast honors the access boundary. See CHANGELOG.md.