Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/ISSUE_TEMPLATE/adapter_broken.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Adapter broken (grey "!" on a provider)
description: A provider stopped reading — its endpoint probably changed. Your capture is usually a few-line fix.
labels: [adapter-broken]
body:
- type: dropdown
id: provider
attributes:
label: Provider
options:
- Claude (claude.ai)
- Codex (chatgpt.com)
- Other (say which below)
validations:
required: true
- type: input
id: error
attributes:
label: Error shown on the card
description: Hover or read the card's message, e.g. "schema_mismatch …" or "endpoint_not_verified".
placeholder: "Error: No known claude.ai usage endpoint matched …"
validations:
required: true
- type: checkboxes
id: loggedin
attributes:
label: Sanity check
options:
- label: I am logged into the provider's site in this browser profile
required: true
- type: textarea
id: capture
attributes:
label: Captured response shape (redact all ids!)
description: |
Log into the provider's site → DevTools → Network tab → open the provider's own
usage/settings page → find the usage request → copy its URL path and response JSON here.
**Redact every UUID, email, and account identifier** — we only need field names,
value types, and rough numbers.
render: json
- type: input
id: version
attributes:
label: Ration version
placeholder: "0.1.0"
35 changes: 35 additions & 0 deletions .github/ISSUE_TEMPLATE/adapter_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: New provider request
description: Ask for a provider to be supported — or better, volunteer to add it (single-file contribution!).
labels: [adapter-request]
body:
- type: input
id: provider
attributes:
label: Provider
placeholder: "Cursor / Gemini / Grok / …"
validations:
required: true
- type: dropdown
id: credential
attributes:
label: Where does its usage data live?
description: This decides how (and whether) an extension can read it.
options:
- Behind a website login I have in my browser (best case)
- Behind a local file / CLI token on disk (extensions can't read files)
- Behind an API key I'd have to paste
- Not sure
validations:
required: true
- type: input
id: dashboard
attributes:
label: Where do you check usage today?
placeholder: "https://…/settings/usage"
- type: checkboxes
id: volunteer
attributes:
label: Contribution
options:
- label: I'm willing to record fixtures from my account (redacted)
- label: I'd like to write the adapter myself with guidance
31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Bug report
description: Something in the extension misbehaves (not a provider reading failure — use "Adapter broken" for that).
labels: [bug]
body:
- type: textarea
id: what
attributes:
label: What happened?
description: What did you do, what did you expect, what happened instead?
validations:
required: true
- type: input
id: version
attributes:
label: Ration version
placeholder: "0.1.0"
validations:
required: true
- type: input
id: browser
attributes:
label: Browser and version
placeholder: "Chrome 129"
validations:
required: true
- type: textarea
id: console
attributes:
label: Console output (optional)
description: chrome://extensions → Ration → "Inspect views: service worker" — paste relevant errors. Redact anything personal.
render: text
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question / discussion
url: https://github.com/devudilip/ration/discussions
about: Ask questions or discuss ideas that aren't a bug or a request.
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Build
run: npm run build

- name: Manifest sanity
run: node -e "JSON.parse(require('fs').readFileSync('dist/manifest.json','utf8')); console.log('manifest OK')"

- name: No runtime dependencies (auditability commitment)
run: node -e "const d = require('./package.json').dependencies; if (d && Object.keys(d).length) { console.error('runtime deps found:', d); process.exit(1); } console.log('zero runtime deps OK')"

- name: Upload unpacked extension
uses: actions/upload-artifact@v4
with:
name: ration-unpacked
path: dist/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
*.zip
.DS_Store
136 changes: 136 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Contributing to Ration

Thanks for helping! The most valuable contribution is a **provider
adapter** — Ration's coverage is designed to grow through single-file
community contributions.

## Dev setup

```sh
npm install
npm run watch # rebuilds dist/ on save
```

Load `dist/` as an unpacked extension at `chrome://extensions` (Developer
mode → Load unpacked), and reload it there after changes. `npm run ci` runs
exactly what GitHub Actions runs: typecheck, tests, build.

## Adding a provider adapter

An adapter is five artifacts:

1. `src/adapters/<id>.ts` — the adapter itself
2. One line in `src/adapters/index.ts` — import it and append to the array
3. Its origin added to `optional_host_permissions` in `public/manifest.json`
4. `tests/fixtures/<id>/*.json` — recorded responses (redacted!)
5. `tests/<id>.test.ts` — fixture tests

### The contract (`src/types.ts`)

Your adapter implements `ProviderAdapter` and returns `ProviderSnapshot`s
made of `QuotaLane`s. Three rules are non-negotiable, and reviewers will
check them:

- **Never throw.** Every failure path — logged out, rate-limited, server
error, unparseable body — returns a snapshot with the matching `status`
(`unauthenticated` / `rate_limited` / `error`). One broken adapter must
never take down the popup.
- **Never coerce to zero.** Validate the response with `src/lib/validate.ts`
(see `matchVariant` for handling multiple known shapes). If the shape
doesn't match, return `status: 'error'` with code `schema_mismatch`. A
confidently wrong number is worse than no number.
- **Read raw field names, not inferred labels.** Codex, for example, has
been observed with both `primary_window`/`secondary_window` and
`five_hour_limit`/`weekly_limit`. Match each known shape literally as a
named variant and record which one matched in `snapshot.schemaVariant`.
Mislabeling which window is which produces a confidently wrong display.

`headroomPct` (0–100 remaining) is the universal comparator. A provider's
overall headroom is the **minimum** across its lanes — you're blocked by
whichever ceiling you hit first.

### Politeness rules

We read undocumented endpoints as guests:

- Set `minRefreshMs` to at least `60_000`. The scheduler additionally
enforces a hard floor of one request per provider per minute and
exponential backoff on failures — your adapter doesn't need to implement
backoff, just report honest statuses.
- Use `ctx.fetch(url, { credentials: 'include', headers: { Accept:
'application/json' } })`. Do not spoof another client's identity, add
fake client headers, or construct `Authorization` headers. Cookie-riding
Tier A adapters must never read credential values at all.
- **Anthropic-specific:** PRs that read, store, or transmit Claude Code /
claude.ai OAuth tokens will be declined — Anthropic's terms restrict
those tokens to their own products (see README).

### Recording a fixture

1. Log into the provider's site, open DevTools → Network.
2. Open the provider's own usage/settings page and find the usage request.
3. Copy the response JSON into `tests/fixtures/<id>/`.
4. **Redact every identifier** — org/user UUIDs, emails, plan names if
sensitive. Keep the shape and realistic numbers.

Also record (or hand-write) at least: a logged-out response, and a
deliberately wrong shape (for the `schema_mismatch` test).

### Worked example: a hypothetical Cursor adapter

```ts
// src/adapters/cursor.ts
import type { FetchContext, ProviderAdapter, ProviderSnapshot } from '../types';
import { clampPct } from '../lib/headroom';
import { matchVariant, num, obj, optional } from '../lib/validate';

const variants = {
usage_summary: obj({
included_usd_used: num,
included_usd_limit: num,
// ...match the real field names you observed, literally
}),
};

export const cursorAdapter: ProviderAdapter = {
id: 'cursor',
displayName: 'Cursor',
tier: 'A',
hostPermissions: ['https://cursor.com/*'],
dashboardUrl: 'https://cursor.com/settings',
minRefreshMs: 60_000,
async fetch(ctx: FetchContext): Promise<ProviderSnapshot> {
// 1. ctx.fetch the usage endpoint with credentials: 'include'
// 2. map 401/403 → 'unauthenticated', 429 → 'rate_limited'
// 3. matchVariant(variants, body) — mismatch → 'schema_mismatch' error
// 4. build QuotaLanes; for currency lanes:
// kind: 'currency', used, limit,
// headroomPct: clampPct(100 * (1 - used / limit))
// ...
},
};
```

Then in `src/adapters/index.ts`:

```ts
import { cursorAdapter } from './cursor';
export const adapters = [claudeAdapter, codexAdapter, cursorAdapter] as const;
```

Look at `src/adapters/codex.ts` (simpler) and `src/adapters/claude.ts`
(endpoint probing, per-provider cache) for complete, tested references, and
mirror `tests/codex.test.ts` for the test checklist: happy path per
variant, unauthenticated, rate-limited, unknown shape → `schema_mismatch`,
never-throws, and no `Authorization` header.

## Non-adapter contributions

Bug fixes and small improvements: open a PR. New surface area (options
pages, notifications, history) tends to be roadmap-sensitive — open an
issue first so we don't waste your time.

## Commit style

Conventional commits (`feat(adapter): …`, `fix: …`, `docs: …`). CI must be
green: `npm run ci` locally before pushing.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2026 Ration contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
Loading
Loading