This page is for readers who want to understand or change the code: the data flow, the components and the pitfalls met along the way.
Data flow
claude.ai · Anthropic Console · chatgpt.com · agy · Google Cloud · ~/.claude · ~/.codex · remote host (SSH)
|
fetch_one.sh <source> (each source runs independently, in parallel)
| atomic write, file lock
~/.ai_usage_cache.json
| |
AIUsage.app (SwiftUI) AIUsageWidget (WidgetKit)
The app and the widget only read the cache. The refresh button only writes a trigger file and a LaunchAgent does the rest.
Components
fetch_one.shtakes one source (claude,codex,codexhist,agy,gemini,insights) and merges the result into the cache. Parser errors go to a log file instead of being swallowed.ClaudeHelperis a small WKWebView app that keeps the sign-in session and prints the page content for the scripts to parse. Its modes arelogin,fetch,login-aistudio,fetch-aistudio,login-consoleandfetch-console.remote_insights.pyruns on the remote host, reads the session logs incrementally and returns compressed JSON.Shared.swiftis compiled into both the app and the widget: the logo parser, the data model and the shared forecast logic.
Data rules
- Each source writes to its own key in the cache with a
fetched_at. The_freshflag marks a genuinely successful fetch; only then does the shared clock advance. - A transient fetch failure keeps the previous numbers and never overwrites them with empty values.
- When a session expires the old numbers are kept and
needs_loginis set so the card shows a sign-in button.
Pitfalls met
- The helper's background window must be on screen. An off-screen window makes WebKit report
visibilityState=hidden, parksrequestAnimationFrame, and Next.js pages such as the Anthropic Console never render. The helper uses a 99% transparent window placed on screen that ignores the mouse. - Cookies are tied to the helper's bundle id. A loose executable run outside the
.appbundle does not see the sign-in session. - The claude.ai usage page has a button whose label ends in an icon glyph, so a regular expression must not anchor on
$; the Console text also contains icon glyphs on their own lines between a label and its number. - The widget extension process survives for a long time across deploys. After every deploy that changes the widget, terminate the
AIUsageWidgetExtensionprocess so the system starts the new build. - A broken build copied by mistake. Never pipe
xcodebuildthroughgrepand skip checking forBUILD SUCCEEDED. - Ad-hoc signing inside a synced folder fails with "detritus not allowed"; sign a copy in a temporary directory.
- The widget has no real
NSHomeDirectory()because it runs in a sandbox; the cache path is built from the user name.
Illustrations
The app has two environment variables for producing promotional screenshots: AIUSAGE_HOME points the cache and scripts at a folder of sample data, and AIUSAGE_APPEARANCE (light or dark) pins the appearance. This way public images never contain real figures.