Skip to content

Commit bffbd5f

Browse files
cursor-usage-cliclaude
andcommitted
cursor-usage-cli: cross-platform Cursor usage CLI
Read Cursor (cursor.com) usage, spend, and per-event logs from the locally signed-in account. - Zero required deps (stdlib urllib/sqlite3); optional [keyring] extra - Token resolver: env override -> macOS keychain -> OS keyring -> Cursor IDE state.vscdb; skips api_key_token, requires a web session token - Dashboard client: auth/me, /api/usage, aggregated + paginated events, with the required CSRF Origin header - CLI: summary, --by-day breakdown, --csv per-event export, time windows - Docs: README, HOW_THIS_WAS_BUILT (reproducible recipe), PUBLISHING - Unit tests for parsing/formatting Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 parents  commit bffbd5f

14 files changed

Lines changed: 1127 additions & 0 deletions

File tree

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
build/
6+
dist/
7+
.eggs/
8+
.venv/
9+
venv/
10+
env/
11+
12+
# Tooling
13+
.mypy_cache/
14+
.pytest_cache/
15+
.ruff_cache/
16+
17+
# Editors / OS
18+
.DS_Store
19+
.idea/
20+
.vscode/
21+
22+
# Local exports (never commit anyone's usage data)
23+
*.csv

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 cursor-usage-cli contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<h1 align="center">📊 cursor-usage</h1>
2+
3+
<p align="center">
4+
<b>See your <a href="https://cursor.com">Cursor</a> usage, spend, and per-event logs — right from your terminal.</b>
5+
</p>
6+
7+
<p align="center">
8+
<img alt="Python" src="https://img.shields.io/badge/python-3.8%2B-blue">
9+
<img alt="Platforms" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey">
10+
<img alt="Dependencies" src="https://img.shields.io/badge/dependencies-zero-brightgreen">
11+
<img alt="License" src="https://img.shields.io/badge/license-MIT-green">
12+
</p>
13+
14+
---
15+
16+
Cursor shows your usage on its web dashboard, but there's no official way to get
17+
it from the command line. **`cursor-usage` gives you that** — a clean summary, a
18+
per-day breakdown, and a full per-event CSV export — using the session your
19+
Cursor app already has. No API key to manage, nothing to configure.
20+
21+
```text
22+
==============================================================================
23+
CURSOR USAGE | you@example.com | 2026-06-04 -> 2026-06-09
24+
==============================================================================
25+
Included value used : $875.81 (compute consumed; included in plan)
26+
Tokens in=126,033,860 out=18,989,068
27+
cacheRead=2,492,781,450 cacheWrite=8,322,520
28+
------------------------------------------------------------------------------
29+
model $ value in tok out tok
30+
------------------------------------------------------------------------------
31+
composer-2.5 460.14 95,638,983 13,315,470
32+
claude-4.6-opus-high 233.71 786,434 1,059,451
33+
gemini-3.5-flash 45.77 16,097,065 1,075,904
34+
gpt-5.4-high 43.65 4,069,043 1,041,967
35+
...
36+
```
37+
38+
## ✨ Features
39+
40+
- **One command, real numbers** — per-model tokens and compute value for the
41+
current billing month.
42+
- **📅 Per-day breakdown**`--by-day` shows how much you burned each day.
43+
- **🧾 CSV export**`--csv` dumps every usage event (timestamp, model, tokens,
44+
cost) for your own spreadsheets and charts.
45+
- **🌍 Cross-platform** — macOS, Linux, and Windows.
46+
- **🔋 Zero dependencies** — pure Python standard library.
47+
- **🔒 Local & private** — reads the session your Cursor app already stored;
48+
talks only to `cursor.com`. No telemetry, no third parties.
49+
50+
## 🚀 Quickstart
51+
52+
```bash
53+
pip install cursor-usage-cli # or: pip install . from a clone
54+
cursor-usage # summary for the current billing month
55+
```
56+
57+
That's it — if you're signed in to Cursor on this machine, it just works.
58+
59+
## 🧑‍💻 Usage
60+
61+
| Command | What it does |
62+
|---|---|
63+
| `cursor-usage` | Summary for the current billing month |
64+
| `cursor-usage --by-day` | Add a per-day breakdown |
65+
| `cursor-usage --csv usage.csv` | Export every usage event to CSV |
66+
| `cursor-usage --days 7` | Window: the last 7 days |
67+
| `cursor-usage --month 2026-05` | Window: a specific month |
68+
| `cursor-usage --start 2026-06-01 --end 2026-06-07` | Window: an explicit range |
69+
| `cursor-usage --json` | Raw aggregated JSON (for scripting) |
70+
| `cursor-usage -v` | Also print which session source was used |
71+
72+
Flags combine — e.g. `cursor-usage --by-day --csv june.csv --month 2026-06`.
73+
74+
<details>
75+
<summary><b>📅 Example: <code>--by-day</code></b></summary>
76+
77+
```text
78+
==============================================================================
79+
CURSOR USAGE BY DAY | you@example.com | 2026-06-02 -> 2026-06-09
80+
==============================================================================
81+
date events $ value in tok out tok
82+
------------------------------------------------------------------------------
83+
2026-06-04 433 324.46 38,602,275 6,080,617
84+
2026-06-05 368 252.58 40,616,543 6,469,309
85+
2026-06-06 416 121.06 29,686,247 4,497,010
86+
------------------------------------------------------------------------------
87+
TOTAL 1,661 929.46 128,631,599 20,101,078
88+
```
89+
</details>
90+
91+
<details>
92+
<summary><b>🧾 CSV columns</b></summary>
93+
94+
`datetime_local, timestamp_ms, date, model, kind, input_tokens, output_tokens,
95+
cache_read_tokens, cache_write_tokens, value_cents, charged_cents,
96+
requests_costs, is_headless, owning_user` — one row per usage event, sorted by
97+
time.
98+
</details>
99+
100+
## 🤔 How it works
101+
102+
A Cursor **API key** (`crsr_…`) can't read usage — that data lives behind your
103+
web **session**, the same one your browser/app uses on `cursor.com`. This tool
104+
finds that session locally and asks Cursor's dashboard API for your numbers.
105+
106+
It looks for the session in this order (all **local-only**):
107+
108+
1. `CURSOR_SESSION_TOKEN` environment variable (manual override)
109+
2. macOS Keychain (written by the `cursor-agent` CLI)
110+
3. Your OS keyring, if the optional `keyring` package is installed
111+
4. The Cursor app's local state database (works on every OS)
112+
113+
If it can't find one, sign in to the Cursor app and run it again.
114+
115+
<details>
116+
<summary><b>🔍 Where exactly the session lives (per OS)</b></summary>
117+
118+
The Cursor IDE stores the session token in a small SQLite file
119+
(`state.vscdb` → key `cursorAuth/accessToken`), in the same place on every OS:
120+
121+
| OS | Path |
122+
|---|---|
123+
| macOS | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` |
124+
| Linux | `~/.config/Cursor/User/globalStorage/state.vscdb` |
125+
| Windows | `%APPDATA%\Cursor\User\globalStorage\state.vscdb` |
126+
127+
Want the full reverse-engineering story (and a recipe to rebuild this tool)? See
128+
**[docs/HOW_THIS_WAS_BUILT.md](docs/HOW_THIS_WAS_BUILT.md)**.
129+
</details>
130+
131+
### Manual override
132+
133+
On any OS you can skip auto-detection entirely:
134+
135+
```bash
136+
# cursor.com → DevTools → Application → Cookies → copy WorkosCursorSessionToken
137+
export CURSOR_SESSION_TOKEN='user_…::eyJhbGci…'
138+
cursor-usage
139+
```
140+
141+
## 🔒 Privacy & security
142+
143+
- The tool only **reads** your existing local session — it never writes,
144+
refreshes, or sends it anywhere except `cursor.com`.
145+
- **No telemetry. No third-party calls.**
146+
- CSV exports contain your own usage data; they're git-ignored by default so you
147+
don't commit them by accident.
148+
149+
## ⚠️ Good to know
150+
151+
- **`$ value` is compute consumed, not money owed.** On plans where usage-based
152+
pricing is off, your bill is just the flat subscription — these figures show
153+
the value of the compute included in your plan.
154+
- This uses Cursor's **internal, undocumented** dashboard API. It works great
155+
today, but Cursor could change it at any time. If something breaks, please open
156+
an issue.
157+
- If your session has expired, sign back in to Cursor and run the command again.
158+
159+
## 🛠️ Install options
160+
161+
```bash
162+
pip install cursor-usage-cli # from PyPI (once published)
163+
pip install . # from a local clone
164+
pip install "cursor-usage-cli[keyring]" # + OS-keyring lookup on Linux/Windows
165+
```
166+
167+
Requires Python 3.8+.
168+
169+
## 🤝 Contributing
170+
171+
Issues and PRs are welcome. Run the tests with:
172+
173+
```bash
174+
pip install pytest && pytest -q
175+
```
176+
177+
## 📄 License
178+
179+
[MIT](LICENSE) — do whatever you like.

docs/HOW_THIS_WAS_BUILT.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# How this tool was built (a guide for the next LLM/engineer)
2+
3+
This document is a reproducible recipe for rebuilding `cursor-usage-cli` from
4+
scratch — including the dead-ends — so another agent can recreate or extend it
5+
without re-deriving everything. Nothing here is secret; it's all observable by
6+
probing Cursor's own endpoints with a session you already own.
7+
8+
> Scope/ethics: this reads **your own** local Cursor session to query **your own**
9+
> usage from Cursor's own servers. Don't use it against accounts you don't control.
10+
11+
## 1. The goal and the first wrong turn
12+
13+
Goal: "show my Cursor usage from the CLI." The obvious idea is to use a Cursor
14+
**API key** (`crsr_…`). That's a dead end for usage:
15+
16+
- `GET https://api.cursor.com/v1/me``200` (key is valid, it's a *User* key).
17+
- `GET https://api.cursor.com/v1/models``200`.
18+
- Anything usage/spend/analytics (`/teams/daily-usage-data`,
19+
`/teams/filtered-usage-events`, `/analytics/team/*`) → `401 Invalid Team API Key`.
20+
21+
Conclusion: there are **two** key types. A *User/Agent* key (`crsr_…`) drives the
22+
Agent/Background API only. Usage & spend live on the **Admin/Team API**, which
23+
needs a different (team admin) key. If you only have a personal account, there is
24+
**no API key** that returns your usage.
25+
26+
## 2. The insight: the dashboard uses a *session*, not an API key
27+
28+
The web dashboard at `cursor.com/dashboard` clearly shows usage, so an endpoint
29+
exists — it's just gated differently. Probing:
30+
31+
- `GET https://cursor.com/api/usage?user=<id>` with the API key as a Bearer or
32+
cookie → `401 not_authenticated`. So it wants a real **session**, not the key.
33+
34+
Cursor authenticates the web app with a WorkOS session cookie named
35+
`WorkosCursorSessionToken`. We need that token.
36+
37+
## 3. Finding the session token locally
38+
39+
The Cursor app and the `cursor-agent` CLI both store the session after login.
40+
Two reliable, cross-platform sources:
41+
42+
- **macOS Keychain** (written by `cursor-agent`):
43+
`security find-generic-password -s cursor-access-token -w` → a JWT.
44+
- **Cursor IDE SQLite state DB** (works on every OS, identical schema):
45+
`state.vscdb`, table `ItemTable`, key `cursorAuth/accessToken` → the same JWT.
46+
- macOS: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`
47+
- Linux: `~/.config/Cursor/User/globalStorage/state.vscdb`
48+
- Windows: `%APPDATA%\Cursor\User\globalStorage\state.vscdb`
49+
50+
Decode the JWT payload (middle segment, base64url) to confirm it's a session:
51+
`{"sub":"github|user_01…","aud":"https://cursor.com","type":"session", ...}`.
52+
The `sub` claim matters next.
53+
54+
## 4. Cracking the cookie format
55+
56+
The cookie value is **not** just the JWT. Trying `/api/auth/me` with different
57+
shapes:
58+
59+
| cookie value | result |
60+
|------------------------|-------------------------------------------|
61+
| `<jwt>` | `204` (no identity) |
62+
| `<numericUserId>::<jwt>` | `404 User not found` |
63+
| `<sub>::<jwt>` | **`200`** with `{email, id, sub, ...}` |
64+
65+
So the format is `WorkosCursorSessionToken=<sub>::<jwt>`, where `<sub>` is the
66+
JWT's `sub` claim (e.g. `user_01ABC…`). The `::` is sent URL-encoded as `%3A%3A`.
67+
68+
(Interesting wrinkle: `/api/usage` accepts both `numericUserId::jwt` and
69+
`sub::jwt`, but `/api/auth/me` only accepts `sub::jwt`. Use `sub::jwt` — it works
70+
for everything.)
71+
72+
## 5. The CSRF gotcha
73+
74+
The read endpoints (`GET /api/auth/me`, `GET /api/usage?user=…`) work with just
75+
the cookie. The dashboard data is behind **POST** endpoints that add a CSRF
76+
check:
77+
78+
- POST without an Origin header → `403 Invalid origin for state-changing request`.
79+
- Add `Origin: https://cursor.com` → works.
80+
81+
## 6. The endpoints that matter
82+
83+
All on `https://cursor.com`, cookie `WorkosCursorSessionToken=<sub>::<jwt>`:
84+
85+
| method | path | purpose |
86+
|--------|------|---------|
87+
| GET | `/api/auth/me` | identity: `email`, numeric `id`, `sub` |
88+
| GET | `/api/usage?user=<id>` | legacy request counter + `startOfMonth` (use for the billing-window start) |
89+
| POST | `/api/dashboard/get-aggregated-usage-events` | per-model totals: `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `totalCents` |
90+
| POST | `/api/dashboard/get-filtered-usage-events` | per-event log + `totalUsageEventsCount` (paginate with `page`/`pageSize`) |
91+
| POST | `/api/dashboard/get-hard-limit` | `{"noUsageBasedAllowed":true}` ⇒ overage off ⇒ everything is included |
92+
93+
POST body shape:
94+
```json
95+
{"teamId": 0, "startDate": "<epoch_ms>", "endDate": "<epoch_ms>", "userId": <id>}
96+
```
97+
`get-filtered-usage-events` additionally takes `"page": 1, "pageSize": 1000`.
98+
99+
Per-event entry (`usageEventsDisplay[]`) fields used here:
100+
`timestamp` (ms string), `model`, `kind` (e.g. `USAGE_EVENT_KIND_INCLUDED_IN_ULTRA`),
101+
`tokenUsage.{inputTokens,outputTokens,cacheReadTokens,cacheWriteTokens,totalCents}`,
102+
`chargedCents`, `requestsCosts`, `isHeadless`, `owningUser`.
103+
104+
## 7. Turning it into a tool
105+
106+
- **Token resolution** (`auth.py`): env var → macOS keychain → `keyring`
107+
state.vscdb. Derive `sub` from the JWT so the whole cookie comes from just the
108+
token. Accept a pre-formed `sub::jwt` (or `%3A%3A`) override too.
109+
- **API client** (`api.py`): stdlib `urllib`; always send `Origin` on POST;
110+
paginate `all_events` until `len >= totalUsageEventsCount`.
111+
- **Reporting** (`report.py`): summary from the aggregated endpoint; per-day
112+
buckets and CSV from the event log (bucket by local date).
113+
- **CLI** (`cli.py`): default window = current billing month from
114+
`startOfMonth`; flags `--by-day`, `--csv`, `--days/--month/--start/--end`,
115+
`--json`.
116+
117+
## 8. Cross-OS notes
118+
119+
- Token: keychain is macOS-only; `keyring` covers Linux/Windows; the SQLite
120+
state DB covers all three with no dependency — prefer it as the universal path.
121+
- Everything else (urllib, sqlite3, base64, csv) is stdlib and OS-agnostic.
122+
- Windows edge case: if a future Cursor build encrypts the state-DB token
123+
(DPAPI), fall back to `CURSOR_SESSION_TOKEN` copied from the browser.
124+
125+
## 9. How to re-verify quickly (one-liner, macOS)
126+
127+
```bash
128+
TOKEN=$(security find-generic-password -s cursor-access-token -w)
129+
SUB=$(python3 -c "import base64,json,sys;p=sys.argv[1].split('.')[1];p+='='*(-len(p)%4);print(json.loads(base64.urlsafe_b64decode(p))['sub'])" "$TOKEN")
130+
ID=$(curl -s -H "Cookie: WorkosCursorSessionToken=$SUB%3A%3A$TOKEN" https://cursor.com/api/auth/me | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
131+
curl -s -X POST -H "Cookie: WorkosCursorSessionToken=$SUB%3A%3A$TOKEN" \
132+
-H "Content-Type: application/json" -H "Origin: https://cursor.com" \
133+
-d "{\"teamId\":0,\"startDate\":\"0\",\"endDate\":\"9999999999999\",\"userId\":$ID}" \
134+
https://cursor.com/api/dashboard/get-aggregated-usage-events
135+
```
136+
137+
If that returns `aggregations`, the whole approach is intact and any breakage in
138+
the Python tool is local, not protocol-level.

0 commit comments

Comments
 (0)