on 2026.07.11.1840 help sign in
usage-menubar/README.md
179 lines 11 kB history raw
1# UsageMenubar
3A macOS menu bar application that displays your [Hyper (Charm)](https://hyper.charm.land) credit balance — and, if you use the Claude Code CLI, your Claude subscription limits alongside it.
5## Features
7- ⚡ **Menu bar display** — shows `⚡{balance}` (e.g. `⚡42`) right in your menu bar
8- 🤖 **Claude Code limits** — your Pro/Max 5-hour and 7-day usage windows in the popover, with the percentage used and a countdown to the next reset. No setup: if you have signed in with the Claude Code CLI, it just appears. If you haven't, the section doesn't
9- 🔄 **Auto-refresh** — fetches your balance on a configurable interval (1m / 5m / 15m / 30m), and again on wake from sleep
10- 📊 **Balance sparkline** — a subtle mini-chart in the popover shows your balance trend over recent fetches
11- 📋 **Click-to-copy** — click the balance number in the popover to copy it to the clipboard
12- 🖱️ **Right-click menu** — right-click the menu bar icon for quick access to Refresh, Open hyper.charm.land, and Quit
13- 🔔 **Low-balance notifications** — alerts you when your balance drops below 10 credits, including on the very first fetch if you are already below the threshold
14- 🔒 **Keychain storage** — your API key is stored securely in macOS Keychain
15- 🎨 **Color-coded balance** — green (≥100), yellow (10–99), red (<10)
16- 🔐 **Secure input** — API key field is masked
17- 🚀 **Launch at login** — optional, using `SMAppService`
18- 🔑 **Get key link** — quick link to [hyper.charm.land](https://hyper.charm.land) right next to the API key field
19- 🫥 **Agent app** — no dock icon, no main window; lives entirely in the menu bar
21## Installation
23### Homebrew (recommended)
25```bash
26brew tap ilyakooo0/tap
27brew install --cask usage-menubar
28```
30Or in one command:
32```bash
33brew install --cask ilyakooo0/tap/usage-menubar
34```
36### Download the pre-built release
381. Go to the [Releases page](https://github.com/ilyakooo0/usage-menubar/releases)
392. Download the latest `UsageMenubar-*.zip`
403. Unzip and move `UsageMenubar.app` to `/Applications`
414. Launch the app — it will appear in your menu bar as ⚡
43> **Note:** Releases are **ad-hoc signed** — the CI build applies `codesign -s -` to the app, which gives it a stable code identity but is *not* an Apple Developer ID signature. macOS will still warn you on first launch: right-click the app → **Open**, or allow it in **System Settings → Privacy & Security**. Removing that prompt entirely requires a paid Developer ID certificate and notarization.
45### Set your API key
471. Click the ⚡ icon in your menu bar
482. Enter your Hyper API key (starts with `sk-...`) in the **API Key** field
493. Click **Save**
504. Your balance will appear immediately
52You can get your API key from [hyper.charm.land](https://hyper.charm.land).
54### Claude Code limits (optional, no setup)
56If you use the [Claude Code CLI](https://claude.com/claude-code), the popover also shows how much of your Pro/Max plan you have used: the limit currently in effect as a headline percentage, the 5-hour and 7-day windows underneath, and how long until the next reset.
58There is nothing to configure. The app reads the OAuth credentials Claude Code already stores when you run `claude` and sign in — first from its login-Keychain item, then from `~/.claude/.credentials.json` if the Keychain has nothing. If neither exists, the Claude section simply doesn't appear; it is not treated as an error, and the Hyper balance is unaffected.
60Two things worth knowing:
62- **macOS will ask once.** Those credentials belong to Claude Code, not to this app, so the first read prompts you to allow access to the `Claude Code-credentials` Keychain item. Choose **Always Allow** and you won't be asked again.
63- **The credentials are read-only, with one exception.** When the access token has expired, the app refreshes it against Anthropic's token endpoint and writes the result back to wherever it read it from — exactly as Claude Code itself would. Nothing else about them is ever modified, and the fields the app doesn't model are preserved verbatim, so your CLI session keeps working.
65## Settings
67Everything is configured from the popover — click the ⚡ icon in the menu bar.
69| Setting | Where | Notes |
70| ------------------- | ------------------------------ | ------------------------------------------------------------------------------------ |
71| **API key** | **API Key** field → **Save** | Stored in the macOS Keychain. Clearing the field and saving deletes the stored key. |
72| **Launch at login** | **Launch at Login** toggle | Registers the app with `SMAppService`. |
73| **Notifications** | System permission prompt | macOS asks on first launch (only if not already decided). Required for low-balance alerts. |
74| **Refresh now** | **Refresh** button | Forces an immediate fetch, on top of the automatic refresh. |
75| **Refresh interval**| **Refresh every** picker | Choose 1m, 5m, 15m, or 30m. Defaults to 5m. Stored in `UserDefaults` and persisted across launches. |
77The app also refreshes automatically when your Mac wakes from sleep.
79## Build from source
81### Prerequisites
83- macOS 14.0+ (Sonoma)
84- Xcode 15+
85- [xcodegen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`)
87### Steps
89```bash
90# Clone the repository
91git clone https://github.com/ilyakooo0/usage-menubar.git
92cd usage-menubar
94# Generate the Xcode project
95xcodegen generate
97# Open in Xcode
98open UsageMenubar.xcodeproj
100# Or build from the command line
101xcodebuild \
102 -scheme UsageMenubar \
103 -configuration Release \
104 -archivePath build/UsageMenubar.xcarchive \
105 archive \
106 CODE_SIGNING_ALLOWED=NO
107```
109### Run tests
111```bash
112xcodebuild test \
113 -scheme UsageMenubar \
114 -configuration Debug \
115 -destination "platform=macOS"
116```
118## Versioning
120Versions follow the format `YYYY.MM.DD.HHHH` where `HHHH` is the UTC hour and minute (24-hour).
122| Commit time (UTC) | Version |
123| ------------------------ | ----------------- |
124| 2026-07-09 14:23 | `2026.07.09.1423` |
125| 2026-01-01 00:00 | `2026.01.01.0000` |
126| 2026-12-31 23:59 | `2026.12.31.2359` |
128The version is derived automatically from the commit timestamp in the GitHub Actions release workflow.
130## Project structure
132```
133usage-menubar/
134├── project.yml # xcodegen project definition
135├── UsageMenubar/
136│ ├── UsageMenubarApp.swift # @main App, NSStatusBar, timer, sleep/wake
137│ ├── ViewModel.swift # Observable state: balance, Claude usage, refresh, notifications
138│ ├── MenuView.swift # SwiftUI popover view
139│ ├── CreditsChecker.swift # API client (GET /v1/credits)
140│ ├── ClaudeUsageClient.swift # Claude OAuth client (GET /api/oauth/usage) + models
141│ ├── KeychainHelper.swift # Keychain wrapper for API key
142│ ├── VersionFormatter.swift # YYYY.MM.DD.HHHH formatting
143│ ├── Info.plist # LSUIElement=true (agent app)
144│ ├── UsageMenubar.entitlements # Network client; deliberately not sandboxed
145│ └── Assets.xcassets/ # App icon, accent color
146├── UsageMenubarTests/
147│ ├── CreditsCheckerTests.swift # API, retry, fractional balance, keychain tests
148│ ├── ClaudeUsageClientTests.swift # Usage decoding, token refresh, retry, credential store
149│ └── VersionFormatterTests.swift # Version formatting tests
150├── .github/workflows/
151│ └── release.yml # Build + release on push to master
152├── README.md
153├── LICENSE
154└── .gitignore
155```
157## How it works
1591. **Menu bar item**: The app creates an `NSStatusItem` with a variable-length button showing `⚡{balance}` or `⚡?`. Left-click toggles the popover; right-click opens a context menu (Refresh, Open hyper.charm.land, Quit).
1602. **Popover**: Clicking the menu bar item shows an `NSPopover` containing a SwiftUI `MenuView` with the balance, sparkline, API key field, and settings.
1613. **API client**: `CreditsChecker` performs `GET https://hyper.charm.land/v1/credits` with `Authorization: Bearer <key>`, retries transient failures (5xx, timeouts) with exponential backoff, and decodes `{"balance": <int>}` (tolerating fractional values by rounding).
1624. **Keychain**: The API key is stored/retrieved via `KeychainHelper` using the macOS Security framework. `save()` returns a `Bool` indicating success.
1635. **Timer**: A `Timer` fires at the user-selected interval (1m / 5m / 15m / 30m, default 5m) to refresh the balance; the app also refreshes on wake from sleep. Changing the interval in the popover restarts the timer immediately.
1646. **State**: `ViewModel` owns the balance, loading/error state, balance history (for the sparkline), refresh task (cancellable to prevent races), and the refresh interval, and is shared between the menu bar item and the popover.
1657. **Notifications**: When the balance crosses below 10 credits — or is already below 10 on the first successful fetch — `ViewModel` posts a local `UNUserNotification`. Permission is requested only once.
1668. **Agent app**: `LSUIElement=true` in `Info.plist` makes the app a background agent — no dock icon, no main window.
1679. **Claude usage**: `ClaudeUsageClient` reads Claude Code's OAuth credentials (its `Claude Code-credentials` Keychain item via `/usr/bin/security`, falling back to `~/.claude/.credentials.json`), refreshes the access token when it has expired, and fetches `GET https://api.anthropic.com/api/oauth/usage` with `Authorization: Bearer …` and `anthropic-beta: oauth-2025-04-20`. It is an actor so that overlapping refreshes (timer, manual click, wake) join a single in-flight token exchange — the endpoint rotates the refresh token, and rotating it twice would sign Claude Code out. Missing credentials are reported as "not configured" rather than as an error, and the popover hides the section.
16810. **Independent fetches**: `ViewModel.refresh()` runs the Hyper and Claude requests concurrently with `async let`. Neither can fail, stall, or be unconfigured in a way that affects the other.
16911. **No sandbox**: Reading credentials that belong to another app means spawning `/usr/bin/security` and reading a file in the user's home directory, neither of which the App Sandbox permits. The app is distributed ad-hoc signed via Homebrew rather than through the App Store, where the sandbox would be mandatory.
171## Credits
173- **Author**: Ilya Koo ([@ilyakooo0](https://github.com/ilyakooo0))
174- **API**: [Hyper by Charm](https://hyper.charm.land)
175- **Built with**: Swift 5.9, SwiftUI, macOS 14+
177## License
179MIT — see [LICENSE](LICENSE).