# CLAUDE.md Guidance for Claude Code when working in this repository. ## What this is `staminai` is a **zero-dependency, zero-build browser extension** that draws a usage wheel on `claude.ai`. It ships as an MV3 extension (Firefox + Chromium) and as a Greasemonkey userscript, all from the same two source files. There is no npm, no bundler, no transpiler, and no test runner. `content.js` and `content.css` are shipped byte-for-byte as authored. Keep it that way — the "~200 lines JS, ~80 lines CSS, fully auditable" claim in the README is a feature, and every added dependency is one more thing a store reviewer has to vet. ## Layout ``` manifest.json MV3 manifest, shared by Firefox and Chromium. Sole source of the version number. content.js Everything: rendering, placement, drag, API access, backoff. content.css Styling. Uses Claude's own CSS custom properties with fallbacks. icons/ 16/32/48/128 PNGs (referenced by the manifest) + the SVG master. build.sh Packages the three artifacts. Pure bash + zip + awk. .github/workflows/build.yml Releases on a new manifest version on `main`, or a `v*` tag push. ``` ## Commands ```bash ./build.sh # all three artifacts into dist/ ./build.sh firefox # dist/staminai-firefox.xpi ./build.sh chromium # dist/staminai-chromium.zip ./build.sh userscript # dist/staminai.user.js ./build.sh clean # rm -rf dist/ node --check content.js # the only "lint" that always works python3 -c 'import json; json.load(open("manifest.json"))' node --check dist/staminai.user.js # catches CSS→JS-string escaping bugs npx web-ext lint # AMO's own linter, if network is available ``` ### Build system notes - **Version lives only in `manifest.json`.** `read_version()` scrapes it with `grep`/`sed` (no `jq` dependency) and the userscript header interpolates it. When bumping, also add a `CHANGELOG.md` entry — nothing enforces this. - **`preflight()` runs before every build.** It fails fast if a source file or a manifest-referenced icon is missing, because AMO and the Chrome Web Store reject those uploads only *after* you've waited in the review queue. - **The `.xpi` and the `.zip` have identical contents.** Only the extension differs; Firefox just wants `.xpi`. Don't "optimize" this into one artifact — the two file names are what the release workflow and the README point at. - **`to_js_string()` embeds `content.css` into the userscript** as a double-quoted JS string literal, escaping backslashes, double quotes and newlines. If you ever put a backtick, `${`, or a `` exists; if not, CI builds and publishes a release, creating the tag itself. Merging a version bump *is* the release — so bump only when you mean to ship. Pushing a `v*` tag by hand still works too. There is no PR validation job: if you change `build.sh`, run it locally — CI will not catch a break until release. ## Architecture - **Single IIFE, no globals.** `content.js` is one `(function () { "use strict"; ... })()`. - **Fixed SVG coordinate space.** The wheel's SVG has a constant `viewBox="0 0 100 100"` and is sized purely by CSS (`--csw-size`). Every radius, stroke width and font size in the JS is therefore a *percentage of the wheel* — never a pixel value. Changing the widget's size is a one-line CSS change; never reach for JS to resize the SVG. - **Two scale knobs, both in `content.css`:** `--csw-size` (the wheel) and `--csw-font` (the tooltip). Everything in the tooltip is expressed in `em`, so it follows `--csw-font`. Don't reintroduce fixed `px` font sizes. - **The widget floats free.** It is positioned in viewport pixels and anchored to nothing in Claude's DOM, so a Claude UI change can't strand it. The position is stored as a *fraction* of the free space (`{fx, fy}` in `localStorage`) so it survives resizes and restores sensibly on a different screen. Every write is wrapped in `try/catch` — `localStorage` throws in some private-browsing modes. - **Event-driven, never polling.** No `MutationObserver`, and nothing on a timer fetches. Refreshes come from page load, wheel hover, and document-delegated `focusin`/`click` on the chatbox — debounced 3 s and skipped when the tab is backgrounded. The one `setInterval` in the codebase (`setTick`) repaints the exhausted-state countdown, makes no network calls, is skipped while the tab is backgrounded, and clears itself the moment the wheel returns to rings. Don't add a second timer. - **Backoff is mandatory.** 429 climbs a 1/5/15-minute ladder; 5xx waits 30 s; `Retry-After` wins when it asks for longer. Never add a code path that hits `/api/organizations/*` without going through `triggerRefresh()`. - **Drag vs. click.** Pointer Events with `setPointerCapture`; movement under `DRAG_SLOP` px stays a click. `suppressClick` swallows the synthetic click that follows a real drag and is cleared on the next `pointerdown` so it can never go stale. - **The wheel carries no text in its normal state.** Exact numbers live in the tooltip; the rings are the whole reading. The only text it ever shows is the exhausted-state countdown. ### Ring geometry `rSession` must stay **at or below `S_SESSION`**. The session arc has a round end cap of radius `S_SESSION / 2`, so once the stroke is thicker than the ring's own radius the cap overruns the centre hole and the arc curls over itself into a comma instead of tapering to a round tip. The current constants land `rSession` exactly on `S_SESSION` — the thickest the inner ring can be. If you thicken it further, widen `GAP` or thin the outer rings to compensate, and re-render the `full` / `mixed` / `critical` states before believing it looks right. The **outer ring is dormant.** It used to show Claude Design, which no longer has its own allowance (Design now counts toward the shared limits). It is kept switched off behind `OUTER_ENABLED` / `OUTER_FIELD`, like credits below, for the next separate meter or a credit balance. Its space is still reserved in the geometry, so the inner rings don't change size when it's toggled. The **outer ring is dotted, and a dotted ring cannot carry progress in its own dash pattern** — putting a `stroke-dashoffset` on it just slides the dots around and shows no proportion at all. It is drawn as a full dotted ring masked by a plain arc, so the colour retracts around the ring exactly like the solid ones. The dot period is derived as `circumference / OUTER_DOTS` so the pattern closes with no seam at the 3 o'clock start, and the mask arc is snapped to end in a gap so no dot is ever sliced in half. ### Tolerating an unknown API shape The usage endpoint is undocumented. Lookups like `getOuterUtil` and `getCredits` read one named key, type-check the value, and return `null` otherwise; callers render `—` or nothing when it comes back empty. Keep new fields defensive in the same way — never index straight into a response. **Credits are dormant, and must stay that way.** The endpoint does not surface a credit balance. `getCredits` is gated behind `CREDITS_ENABLED` and reads exactly one key, `CREDITS_FIELD`, which is empty — so nothing reads the response for it at all. Do not restore the speculative multi-key probe: guessing at names the API has never returned finds nothing, and risks latching onto an unrelated field. When the endpoint does expose credits, set `CREDITS_FIELD` to the real key, flip `CREDITS_ENABLED`, and confirm against a real response before shipping. The tooltip row below it is already wired and needs no other change. ## Best practices for browser add-ons These are the rules this extension is held to. Apply them to any change here, and to any other add-on work in this repo. ### Manifest and permissions - **MV3 only.** MV2 is dead in Chrome and deprecated in Firefox. - **Ask for the narrowest possible match.** `https://claude.ai/*` in `content_scripts.matches` is all this needs. Never add ``, and never add a permission "in case we need it later" — each one is a review delay and a scarier install prompt. - **Prefer no `permissions` block at all.** Same-origin `fetch` with `credentials: "include"` from a content script reuses the user's session and needs neither `cookies` nor `host_permissions`. That's why this extension's permission table is one row long. - **Keep Firefox-specific keys in `browser_specific_settings.gecko`.** Chromium ignores the block, so one manifest serves both. `gecko.id` and `strict_min_version` are required for AMO; `data_collection_permissions` is now required for all new AMO submissions — `{"required": ["none"]}` when the add-on transmits nothing. - **Bump `version` for every submission.** Stores reject a re-upload of an existing version, and there is no way to overwrite one. ### Security - **Never assign untrusted data to `innerHTML`.** Build DOM with `createElement`/`createElementNS` and set text via `textContent`. A static template literal with no interpolation (like this extension's one `root.innerHTML` skeleton) is fine; anything carrying API or user data is not. AMO's linter flags these and they are the most common cause of a rejected review. - **No remote code, ever.** No `eval`, no `new Function`, no CDN `