# Juicebox

A macOS menu bar app that shows how much **juice** you have left across Claude,
Codex, Muse and Kimi Code — and when each limit resets.

The menu bar always shows your *tightest* limit, so the number up there is the
one that will bite you first. Every meter fills to show what is **left**, never
what is used, so a full bar always means "plenty".

![panel](docs/panel.png)

## How it reads your usage

Juicebox never handles a credential. A Chrome extension reads each provider's
own usage endpoint **from inside your browser**, using the session you are
already signed in to, and hands the result to the menu bar app.

```
 Chrome extension                native messaging            menu bar app
 ────────────────                ────────────────            ────────────
 claude.ai   ─┐
              ├─ fetch with ──►  juicebox-bridge  ──►  ~/.juicebox/usage.json
 chatgpt.com ─┘   your session      (writes only)              │
                                                               └─► Juicebox.app
```

The app itself opens no sockets, reads no cookie jar, and holds no token. It
reads one JSON file. The bridge is a native messaging host that only ever
writes that file — Chrome will not launch it for any extension except the one
you explicitly link.

### The endpoints

| Provider | Endpoint | What it returns |
|---|---|---|
| Claude | `claude.ai/api/organizations` → `/{org}/usage` | `five_hour`, `seven_day`, `seven_day_opus`/`_sonnet`, each `{utilization, resets_at}` |
| Codex | `chatgpt.com/api/auth/session` → `chatgpt.com/backend-api/wham/usage` | `rate_limit.primary_window` / `secondary_window`, each `{used_percent, reset_at, limit_window_seconds}` |

These are the providers' own internal endpoints — the same ones their web apps
call to draw your usage page. They are undocumented and can change.

Two providers are read from local files instead, with no network or credentials
at all:

| Provider | Source | Notes |
|---|---|---|
| Codex *(fallback)* | `~/.codex/sessions/**/rollout-*.jsonl` | Used when the extension isn't running, so Codex still works with the browser closed. Labelled **LOCAL**. |
| Muse | `~/.config/muse` | Detected only — Muse records no quota data locally |
| Kimi Code | `~/.kimi-code/sessions` | Detected only, unless a quota block appears |

`CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `KIMI_CODE_HOME` are honoured if set.

## Install

```sh
./make.sh --install
```

That is the whole terminal step. Requires macOS 14+ and the Swift toolchain
(Xcode Command Line Tools is enough — no full Xcode).

Juicebox opens a setup window on first launch and walks you through the rest:

![setup](docs/setup.png)

1. **Connect the bridge** — one click. Juicebox stages the extension to
   `~/Juicebox/extension`, derives the ID Chrome will give it, and registers
   the native messaging host for that one ID in every Chromium browser you have.
2. **Load the extension** — the only manual step. `chrome://extensions` →
   Developer mode → Load unpacked → `~/Juicebox/extension` (there are
   *Reveal in Finder* and *Copy path* buttons). If Chrome ends up assigning a
   different ID — because you loaded the folder from somewhere else — open
   *"Loaded it but nothing is happening?"* and paste that ID instead.
3. **Sign in** — any provider that needs a signed-in tab is listed with a link.

Each step verifies itself by watching the filesystem, so the checklist ticks
along on its own — you never have to tell it you finished something, and you
never copy an extension ID by hand.

You can reopen it any time from **Browser setup…** in the panel's gear menu,
and the panel shows a *Finish setup* prompt until it is done.

Enable **Launch at login** from that same gear.

### Why you never paste an extension ID

Chrome derives an unpacked extension's ID deterministically: the first 32 hex
digits of the SHA-256 of the folder's absolute path, each digit mapped `0-f`
onto `a-p`. Juicebox computes the same value, which is why step 1 can register
the bridge before the extension has ever been loaded. Staging to a fixed path
outside the app bundle keeps that ID stable across reinstalls.

The staging folder is deliberately `~/Juicebox/extension` and not somewhere
tidier: Chrome's *Load unpacked* dialog is a standard macOS open panel, and that
panel hides dot-directories **and** `~/Library`, which carries the hidden flag.
An extension staged there cannot be selected without keyboard tricks.

### A cleaner route: the Chrome Web Store

*Load unpacked* is the only option that needs no infrastructure, but it is not
the nicest. Publishing the extension as **Unlisted** on the Chrome Web Store
gives a one-click install from a private link, a stable store-assigned ID, and
automatic updates — no Developer mode and no staged folder. It costs a one-time
$5 developer registration plus review time. That is what comparable tools do.

If you would rather do it from the terminal:

```sh
./make.sh --link-extension <ID>
```

### Verifying it

```sh
# What each provider reports, and why any of them aren't reporting
/Applications/Juicebox.app/Contents/MacOS/Juicebox --probe

# Render the real panel to a PNG (add --light for the light scheme)
/Applications/Juicebox.app/Contents/MacOS/Juicebox --render panel.png
```

`--probe` also prints a **Setup** section — whether the extension is staged, the
extension ID it expects, which browsers are linked, and how old the bridge file
is. That is usually enough to see which step has not landed.

If Claude says *"Extension not connected"*, step 1 or 2 hasn't taken effect yet.
If it says *"Sign in at claude.ai"*, the extension is working but that browser
has no signed-in session.

## Snapshots go stale, and the app knows it

A reading is only valid until its window rolls over. If a stored reading said
"100% used" but its reset time has since passed, the allowance is full again —
so Juicebox shows it as **full**, labelled *"window reset — full again"*,
instead of a stale and alarming 0%. This matters most for the local Codex
fallback, whose numbers can be hours old.

If the extension stops reporting for more than six minutes, the app says so
rather than presenting old numbers as current.

## Layout

```
extension/               Chrome extension (plain JS, no build step)
  manifest.json          permissions: alarms, storage, nativeMessaging
  endpoints.js           every network call, in one place
  usage.js               per-provider reads and normalisation
  background.js          refresh alarm, badge, native messaging push
  popup.html/.css/.js    standalone popup UI

Sources/JuiceboxBridge/  native messaging host: stdin → ~/.juicebox/usage.json

Sources/Juicebox/
  JuiceboxApp.swift      MenuBarExtra entry point, CLI flags
  Models/Quota.swift     LimitWindow / ProviderSnapshot, rollover logic
  Core/
    BridgeFile.swift     reads ~/.juicebox/usage.json
    ExtensionInstaller.swift  stages the extension, derives its Chrome ID,
                              registers the native messaging host
    OnboardingState.swift     self-verifying setup steps
    UsageStore.swift     refresh loop, tightest/next-reset logic
    FileTail.swift       backwards chunked reads of large JSONL transcripts
    Probe.swift          --probe
    PanelRenderer.swift  --render
  Providers/             one file per tool, all behind UsageProvider
  UI/                    panel, cards, meters, menu bar label, setup window
```

Adding a provider means writing one reader in `extension/usage.js` (for a web
session) or one `UsageProvider` (for local files), then listing it in
`UsageStore`.

## Uninstall

```sh
osascript -e 'quit app "Juicebox"'
rm -rf /Applications/Juicebox.app ~/.juicebox ~/Juicebox
find ~/Library/Application\ Support -name com.juicebox.bridge.json -delete
```

Then remove the extension at `chrome://extensions`. Juicebox writes nothing
else — no preferences, no caches, no login items unless you enabled one.


## Redesigned experience

The Mac app, extension, and website share a cream, citrus, and sage palette,
smiling carton mascot, light/dark appearances, and Reduce Motion support.
The website is in `website/`, with landing, setup, privacy, and 404 pages.
Run `python3 website/server.py` to serve the static HTML site at
http://127.0.0.1:8080. No npm install or build is needed for the website.

### Product screenshots

Run `./make.sh`, then render the actual app using synthetic allowances:

```sh
build/Juicebox.app/Contents/MacOS/Juicebox --render docs/panel.png --demo
build/Juicebox.app/Contents/MacOS/Juicebox --render docs/panel-light.png --demo --light
build/Juicebox.app/Contents/MacOS/Juicebox --render-setup docs/setup.png --demo --light
```

`node Tools/RenderExtension.mjs` captures the actual popup in both appearances
with synthetic data and verifies refresh, rollover, sign-in links, and retry.
Install website dependencies and run `npx playwright install chromium` in
`website/` first. The capture uses no real browser profile or provider requests.

Original artwork, all six candidates, and prompt records are in `docs/brand/`.
An extra carton draw is preserved as `A1-extra.png`. The canonical app artwork
is `Sources/Juicebox/Resources/Logo.png`; platform icons are generated from it.

The website’s source download is generated by `python3 Tools/PackageSource.py`.
Regenerate it after app changes before publishing.
