cursor-usage 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cursor-usage 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.
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: cursor-usage
3
+ Version: 0.1.0
4
+ Summary: Cross-platform CLI to read your Cursor (cursor.com) usage, spend, and per-event logs.
5
+ Author: cursor-usage contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/javaisbetterthanpython/cursor-usage
8
+ Project-URL: Issues, https://github.com/javaisbetterthanpython/cursor-usage/issues
9
+ Keywords: cursor,usage,spend,cli,ai,tokens,llm
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: keyring
21
+ Requires-Dist: keyring>=24; extra == "keyring"
22
+ Dynamic: license-file
23
+
24
+ <h1 align="center">๐Ÿ“Š cursor-usage</h1>
25
+
26
+ <p align="center">
27
+ <b>See your <a href="https://cursor.com">Cursor</a> usage, spend, and per-event logs โ€” right from your terminal.</b>
28
+ </p>
29
+
30
+ <p align="center">
31
+ <img alt="Python" src="https://img.shields.io/badge/python-3.8%2B-blue">
32
+ <img alt="Platforms" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey">
33
+ <img alt="Dependencies" src="https://img.shields.io/badge/dependencies-zero-brightgreen">
34
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-green">
35
+ </p>
36
+
37
+ ---
38
+
39
+ Cursor shows your usage on its web dashboard, but there's no official way to get
40
+ it from the command line. **`cursor-usage` gives you that** โ€” a clean summary, a
41
+ per-day breakdown, and a full per-event CSV export โ€” using the session your
42
+ Cursor app already has. No API key to manage, nothing to configure.
43
+
44
+ ```text
45
+ ==============================================================================
46
+ CURSOR USAGE | you@example.com | 2026-06-04 -> 2026-06-09
47
+ ==============================================================================
48
+ Included value used : $875.81 (compute consumed; included in plan)
49
+ Tokens in=126,033,860 out=18,989,068
50
+ cacheRead=2,492,781,450 cacheWrite=8,322,520
51
+ ------------------------------------------------------------------------------
52
+ model $ value in tok out tok
53
+ ------------------------------------------------------------------------------
54
+ composer-2.5 460.14 95,638,983 13,315,470
55
+ claude-4.6-opus-high 233.71 786,434 1,059,451
56
+ gemini-3.5-flash 45.77 16,097,065 1,075,904
57
+ gpt-5.4-high 43.65 4,069,043 1,041,967
58
+ ...
59
+ ```
60
+
61
+ ## โœจ Features
62
+
63
+ - **One command, real numbers** โ€” per-model tokens and compute value for the
64
+ current billing month.
65
+ - **๐Ÿ“… Per-day breakdown** โ€” `--by-day` shows how much you burned each day.
66
+ - **๐Ÿงพ CSV export** โ€” `--csv` dumps every usage event (timestamp, model, tokens,
67
+ cost) for your own spreadsheets and charts.
68
+ - **๐ŸŒ Cross-platform** โ€” macOS, Linux, and Windows.
69
+ - **๐Ÿ”‹ Zero dependencies** โ€” pure Python standard library.
70
+ - **๐Ÿ”’ Local & private** โ€” reads the session your Cursor app already stored;
71
+ talks only to `cursor.com`. No telemetry, no third parties.
72
+
73
+ ## ๐Ÿš€ Quickstart
74
+
75
+ ```bash
76
+ pip install cursor-usage # or: pip install . from a clone
77
+ cursor-usage # summary for the current billing month
78
+ ```
79
+
80
+ That's it โ€” if you're signed in to Cursor on this machine, it just works.
81
+
82
+ ## ๐Ÿง‘โ€๐Ÿ’ป Usage
83
+
84
+ | Command | What it does |
85
+ |---|---|
86
+ | `cursor-usage` | Summary for the current billing month |
87
+ | `cursor-usage --by-day` | Add a per-day breakdown |
88
+ | `cursor-usage --csv usage.csv` | Export every usage event to CSV |
89
+ | `cursor-usage --days 7` | Window: the last 7 days |
90
+ | `cursor-usage --month 2026-05` | Window: a specific month |
91
+ | `cursor-usage --start 2026-06-01 --end 2026-06-07` | Window: an explicit range |
92
+ | `cursor-usage --json` | Raw aggregated JSON (for scripting) |
93
+ | `cursor-usage -v` | Also print which session source was used |
94
+
95
+ Flags combine โ€” e.g. `cursor-usage --by-day --csv june.csv --month 2026-06`.
96
+
97
+ <details>
98
+ <summary><b>๐Ÿ“… Example: <code>--by-day</code></b></summary>
99
+
100
+ ```text
101
+ ==============================================================================
102
+ CURSOR USAGE BY DAY | you@example.com | 2026-06-02 -> 2026-06-09
103
+ ==============================================================================
104
+ date events $ value in tok out tok
105
+ ------------------------------------------------------------------------------
106
+ 2026-06-04 433 324.46 38,602,275 6,080,617
107
+ 2026-06-05 368 252.58 40,616,543 6,469,309
108
+ 2026-06-06 416 121.06 29,686,247 4,497,010
109
+ ------------------------------------------------------------------------------
110
+ TOTAL 1,661 929.46 128,631,599 20,101,078
111
+ ```
112
+ </details>
113
+
114
+ <details>
115
+ <summary><b>๐Ÿงพ CSV columns</b></summary>
116
+
117
+ `datetime_local, timestamp_ms, date, model, kind, input_tokens, output_tokens,
118
+ cache_read_tokens, cache_write_tokens, value_cents, charged_cents,
119
+ requests_costs, is_headless, owning_user` โ€” one row per usage event, sorted by
120
+ time.
121
+ </details>
122
+
123
+ ## ๐Ÿค” How it works
124
+
125
+ A Cursor **API key** (`crsr_โ€ฆ`) can't read usage โ€” that data lives behind your
126
+ web **session**, the same one your browser/app uses on `cursor.com`. This tool
127
+ finds that session locally and asks Cursor's dashboard API for your numbers.
128
+
129
+ It looks for the session in this order (all **local-only**):
130
+
131
+ 1. `CURSOR_SESSION_TOKEN` environment variable (manual override)
132
+ 2. macOS Keychain (written by the `cursor-agent` CLI)
133
+ 3. Your OS keyring, if the optional `keyring` package is installed
134
+ 4. The Cursor app's local state database (works on every OS)
135
+
136
+ If it can't find one, sign in to the Cursor app and run it again.
137
+
138
+ <details>
139
+ <summary><b>๐Ÿ” Where exactly the session lives (per OS)</b></summary>
140
+
141
+ The Cursor IDE stores the session token in a small SQLite file
142
+ (`state.vscdb` โ†’ key `cursorAuth/accessToken`), in the same place on every OS:
143
+
144
+ | OS | Path |
145
+ |---|---|
146
+ | macOS | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` |
147
+ | Linux | `~/.config/Cursor/User/globalStorage/state.vscdb` |
148
+ | Windows | `%APPDATA%\Cursor\User\globalStorage\state.vscdb` |
149
+
150
+ Want the full reverse-engineering story (and a recipe to rebuild this tool)? See
151
+ **[docs/HOW_THIS_WAS_BUILT.md](docs/HOW_THIS_WAS_BUILT.md)**.
152
+ </details>
153
+
154
+ ### Manual override
155
+
156
+ On any OS you can skip auto-detection entirely:
157
+
158
+ ```bash
159
+ # cursor.com โ†’ DevTools โ†’ Application โ†’ Cookies โ†’ copy WorkosCursorSessionToken
160
+ export CURSOR_SESSION_TOKEN='user_โ€ฆ::eyJhbGciโ€ฆ'
161
+ cursor-usage
162
+ ```
163
+
164
+ ## ๐Ÿ”’ Privacy & security
165
+
166
+ - The tool only **reads** your existing local session โ€” it never writes,
167
+ refreshes, or sends it anywhere except `cursor.com`.
168
+ - **No telemetry. No third-party calls.**
169
+ - CSV exports contain your own usage data; they're git-ignored by default so you
170
+ don't commit them by accident.
171
+
172
+ ## โš ๏ธ Good to know
173
+
174
+ - **`$ value` is compute consumed, not money owed.** On plans where usage-based
175
+ pricing is off, your bill is just the flat subscription โ€” these figures show
176
+ the value of the compute included in your plan.
177
+ - This uses Cursor's **internal, undocumented** dashboard API. It works great
178
+ today, but Cursor could change it at any time. If something breaks, please open
179
+ an issue.
180
+ - If your session has expired, sign back in to Cursor and run the command again.
181
+
182
+ ## ๐Ÿ› ๏ธ Install options
183
+
184
+ ```bash
185
+ pip install cursor-usage # from PyPI (once published)
186
+ pip install . # from a local clone
187
+ pip install "cursor-usage[keyring]" # + OS-keyring lookup on Linux/Windows
188
+ ```
189
+
190
+ Requires Python 3.8+.
191
+
192
+ ## ๐Ÿค Contributing
193
+
194
+ Issues and PRs are welcome. Run the tests with:
195
+
196
+ ```bash
197
+ pip install pytest && pytest -q
198
+ ```
199
+
200
+ ## ๐Ÿ“„ License
201
+
202
+ [MIT](LICENSE) โ€” do whatever you like.
@@ -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 # 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 # from PyPI (once published)
163
+ pip install . # from a local clone
164
+ pip install "cursor-usage[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.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cursor-usage"
7
+ version = "0.1.0"
8
+ description = "Cross-platform CLI to read your Cursor (cursor.com) usage, spend, and per-event logs."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ authors = [{ name = "cursor-usage contributors" }]
13
+ keywords = ["cursor", "usage", "spend", "cli", "ai", "tokens", "llm"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Utilities",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.optional-dependencies]
26
+ # Optional: lets the tool read the session token from the OS keyring on
27
+ # Linux/Windows. macOS works without it (via the `security` CLI), and the
28
+ # Cursor IDE SQLite state DB works everywhere with no extra dependency.
29
+ keyring = ["keyring>=24"]
30
+
31
+ [project.scripts]
32
+ cursor-usage = "cursor_usage.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/javaisbetterthanpython/cursor-usage"
36
+ Issues = "https://github.com/javaisbetterthanpython/cursor-usage/issues"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """cursor-usage: read your Cursor (cursor.com) usage, spend, and per-event logs."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,97 @@
1
+ """Minimal client for the Cursor dashboard usage API (stdlib only).
2
+
3
+ Endpoints used (all on ``https://cursor.com``):
4
+ GET /api/auth/me -> {email, id, sub, ...}
5
+ GET /api/usage?user=<id> -> legacy counter + startOfMonth
6
+ POST /api/dashboard/get-aggregated-usage-events -> per-model tokens + cents
7
+ POST /api/dashboard/get-filtered-usage-events -> per-event log (paginated)
8
+
9
+ State-changing POSTs require an ``Origin: https://cursor.com`` header (CSRF guard).
10
+ Auth is the ``WorkosCursorSessionToken`` cookie, value ``<sub>::<jwt>`` (the ``::``
11
+ is sent URL-encoded as ``%3A%3A``).
12
+ """
13
+
14
+ import json
15
+ import urllib.error
16
+ import urllib.request
17
+
18
+ from . import __version__
19
+
20
+ BASE = "https://cursor.com"
21
+ USER_AGENT = "cursor-usage/%s" % __version__
22
+
23
+
24
+ class CursorAPIError(RuntimeError):
25
+ def __init__(self, status, body):
26
+ self.status = status
27
+ self.body = body
28
+ super().__init__("HTTP %s: %s" % (status, body[:300]))
29
+
30
+
31
+ class CursorClient:
32
+ def __init__(self, cookie_value, timeout=30):
33
+ self._cookie = "WorkosCursorSessionToken=" + cookie_value.replace("::", "%3A%3A")
34
+ self._timeout = timeout
35
+
36
+ def _request(self, path, method="GET", body=None):
37
+ headers = {
38
+ "Cookie": self._cookie,
39
+ "Accept": "application/json",
40
+ "User-Agent": USER_AGENT,
41
+ }
42
+ data = None
43
+ if body is not None:
44
+ data = json.dumps(body).encode("utf-8")
45
+ headers["Content-Type"] = "application/json"
46
+ headers["Origin"] = BASE # required: dashboard CSRF check
47
+ req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
48
+ try:
49
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
50
+ raw = resp.read().decode("utf-8", "ignore")
51
+ except urllib.error.HTTPError as exc:
52
+ raise CursorAPIError(exc.code, exc.read().decode("utf-8", "ignore"))
53
+ return json.loads(raw) if raw else {}
54
+
55
+ # -- endpoints ---------------------------------------------------------
56
+ def me(self):
57
+ return self._request("/api/auth/me")
58
+
59
+ def usage(self, user_id):
60
+ return self._request("/api/usage?user=%s" % user_id)
61
+
62
+ def aggregated_usage(self, user_id, start_ms, end_ms):
63
+ return self._request(
64
+ "/api/dashboard/get-aggregated-usage-events", "POST",
65
+ {"teamId": 0, "startDate": str(start_ms), "endDate": str(end_ms),
66
+ "userId": user_id},
67
+ )
68
+
69
+ def _events_page(self, user_id, start_ms, end_ms, page, page_size):
70
+ return self._request(
71
+ "/api/dashboard/get-filtered-usage-events", "POST",
72
+ {"teamId": 0, "startDate": str(start_ms), "endDate": str(end_ms),
73
+ "userId": user_id, "page": page, "pageSize": page_size},
74
+ )
75
+
76
+ def all_events(self, user_id, start_ms, end_ms, page_size=1000, progress=None):
77
+ """Fetch every usage event in the window by paginating.
78
+
79
+ Returns ``(events, total_reported)``. ``progress(fetched, total)`` is
80
+ called after each page if provided.
81
+ """
82
+ first = self._events_page(user_id, start_ms, end_ms, 1, page_size)
83
+ total = int(first.get("totalUsageEventsCount", 0) or 0)
84
+ events = list(first.get("usageEventsDisplay", []))
85
+ if progress:
86
+ progress(len(events), total)
87
+ page = 2
88
+ while len(events) < total and page <= 1000: # 1000-page safety cap
89
+ chunk = self._events_page(user_id, start_ms, end_ms, page, page_size)
90
+ rows = chunk.get("usageEventsDisplay", [])
91
+ if not rows:
92
+ break
93
+ events.extend(rows)
94
+ if progress:
95
+ progress(len(events), total)
96
+ page += 1
97
+ return events, total
@@ -0,0 +1,194 @@
1
+ """Cross-platform resolution of the Cursor web session token.
2
+
3
+ Why this exists
4
+ ---------------
5
+ A personal Cursor API key (``crsr_...``) only reaches the Agent API
6
+ (``api.cursor.com/v1/*``). It CANNOT read usage/spend -- those endpoints reject
7
+ it with "Invalid Team API Key". The data the web dashboard shows comes from
8
+ ``cursor.com/api/*`` and is gated by a WorkOS *session* cookie
9
+ (``WorkosCursorSessionToken``), not the API key.
10
+
11
+ The Cursor app/CLI stores that session JWT locally. We read it from whichever of
12
+ these is available, in priority order (all are local-only; nothing is sent
13
+ anywhere except cursor.com):
14
+
15
+ 1. ``$CURSOR_SESSION_TOKEN`` -- explicit override (raw JWT, or full ``sub::jwt``).
16
+ 2. macOS Keychain service ``cursor-access-token`` (used by ``cursor-agent``).
17
+ 3. OS keyring via the optional ``keyring`` package (Linux Secret Service /
18
+ Windows Credential Locker / macOS Keychain).
19
+ 4. The Cursor IDE SQLite state DB (``state.vscdb`` -> ``cursorAuth/accessToken``),
20
+ whose layout is identical on macOS, Linux and Windows.
21
+
22
+ The dashboard cookie value is ``<workos_sub>::<jwt>``; ``sub`` is the ``sub``
23
+ claim of the JWT, so we can derive the whole cookie from just the token.
24
+ """
25
+
26
+ import base64
27
+ import json
28
+ import os
29
+ import sqlite3
30
+ import subprocess
31
+ import sys
32
+ from pathlib import Path
33
+
34
+ KEYCHAIN_SERVICE = "cursor-access-token"
35
+ KEYCHAIN_ACCOUNT = "cursor-user"
36
+
37
+
38
+ class SessionNotFound(RuntimeError):
39
+ """Raised when no Cursor session token can be located."""
40
+
41
+
42
+ def _jwt_claims(token):
43
+ """Decode a JWT payload to a dict, or ``{}`` if it can't be parsed."""
44
+ try:
45
+ payload = token.split(".")[1]
46
+ payload += "=" * (-len(payload) % 4) # restore base64url padding
47
+ return json.loads(base64.urlsafe_b64decode(payload))
48
+ except Exception:
49
+ return {}
50
+
51
+
52
+ def _cookie_id(claims):
53
+ """Return the cookie id from a token's ``sub`` claim, or ``None``.
54
+
55
+ WorkOS subjects look like ``<connection>|<user_id>`` (e.g.
56
+ ``github|user_01ABC...``). The dashboard cookie wants the bare ``user_...``
57
+ part (what ``/api/auth/me`` reports as ``sub``), so strip any connection
58
+ prefix before the last ``|``.
59
+ """
60
+ sub = claims.get("sub")
61
+ return sub.split("|")[-1] if sub else None
62
+
63
+
64
+ def _from_env():
65
+ return os.environ.get("CURSOR_SESSION_TOKEN")
66
+
67
+
68
+ def _from_macos_keychain():
69
+ if sys.platform != "darwin":
70
+ return None
71
+ try:
72
+ out = subprocess.run(
73
+ ["security", "find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"],
74
+ capture_output=True, text=True, timeout=15,
75
+ )
76
+ return out.stdout.strip() or None
77
+ except Exception:
78
+ return None
79
+
80
+
81
+ def _from_keyring():
82
+ try:
83
+ import keyring # optional dependency
84
+ except Exception:
85
+ return None
86
+ try:
87
+ return keyring.get_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
88
+ except Exception:
89
+ return None
90
+
91
+
92
+ def _state_db_candidates():
93
+ """Yield possible Cursor IDE ``state.vscdb`` paths for the current OS."""
94
+ home = Path.home()
95
+ if sys.platform == "darwin":
96
+ base = home / "Library" / "Application Support"
97
+ elif sys.platform.startswith("win"):
98
+ base = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
99
+ else: # linux / other unix
100
+ base = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config"))
101
+ for app in ("Cursor", "Cursor Nightly"):
102
+ yield base / app / "User" / "globalStorage" / "state.vscdb"
103
+
104
+
105
+ def _from_state_db():
106
+ for path in _state_db_candidates():
107
+ if not path.exists():
108
+ continue
109
+ try:
110
+ con = sqlite3.connect("file:%s?mode=ro" % path, uri=True)
111
+ try:
112
+ row = con.execute(
113
+ "SELECT value FROM ItemTable WHERE key = ?",
114
+ ("cursorAuth/accessToken",),
115
+ ).fetchone()
116
+ finally:
117
+ con.close()
118
+ except Exception:
119
+ continue
120
+ if row and row[0]:
121
+ value = row[0]
122
+ if isinstance(value, bytes):
123
+ value = value.decode("utf-8", "ignore")
124
+ return value.strip().strip('"')
125
+ return None
126
+
127
+
128
+ # Local credential stores, tried in order (env override is handled separately).
129
+ _LOCAL_SOURCES = (
130
+ ("macOS keychain", _from_macos_keychain),
131
+ ("OS keyring", _from_keyring),
132
+ ("Cursor state DB", _from_state_db),
133
+ )
134
+
135
+
136
+ def _log(verbose, name):
137
+ if verbose:
138
+ print("[auth] using session from %s" % name, file=sys.stderr)
139
+
140
+
141
+ def resolve_cookie_value(verbose=False):
142
+ """Return ``"<id>::<jwt>"`` suitable for the dashboard cookie.
143
+
144
+ Only a WorkOS *session* token authenticates the dashboard; an
145
+ ``api_key_token`` (which ``cursor-agent`` may store in the keychain) returns
146
+ HTTP 204 and is skipped. Raises :class:`SessionNotFound` if none is found.
147
+ """
148
+ # 1. Explicit override always wins (raw JWT, or full ``id::jwt`` cookie).
149
+ env = _from_env()
150
+ if env:
151
+ env = env.strip().replace("%3A%3A", "::")
152
+ if "::" in env:
153
+ _log(verbose, "$CURSOR_SESSION_TOKEN")
154
+ return env
155
+ cid = _cookie_id(_jwt_claims(env))
156
+ if cid:
157
+ _log(verbose, "$CURSOR_SESSION_TOKEN")
158
+ return "%s::%s" % (cid, env)
159
+
160
+ # 2. Local stores -- require a session token; skip api_key_token.
161
+ tried = ["$CURSOR_SESSION_TOKEN"]
162
+ saw_api_key = False
163
+ for name, source in _LOCAL_SOURCES:
164
+ tried.append(name)
165
+ token = source()
166
+ if not token:
167
+ continue
168
+ token = token.strip().replace("%3A%3A", "::")
169
+ if "::" in token: # already a full "id::jwt" cookie value
170
+ _log(verbose, name)
171
+ return token
172
+ claims = _jwt_claims(token)
173
+ cid = _cookie_id(claims)
174
+ if not cid:
175
+ continue
176
+ if claims.get("type") == "api_key_token":
177
+ saw_api_key = True # valid token, but not a *web* session
178
+ continue
179
+ _log(verbose, name)
180
+ return "%s::%s" % (cid, token)
181
+
182
+ hint = ""
183
+ if saw_api_key:
184
+ hint = ("\nNote: found an api_key_token (Agent API), but that does not "
185
+ "authenticate the\nusage dashboard. A *web session* is needed.")
186
+ raise SessionNotFound(
187
+ "Could not find a usable Cursor session token.\n"
188
+ "Tried: %s.%s\n\n"
189
+ "Fix one of:\n"
190
+ " - Sign in via the Cursor app (this stores a web session), or\n"
191
+ " - Set CURSOR_SESSION_TOKEN to your WorkosCursorSessionToken cookie\n"
192
+ " (cursor.com -> DevTools -> Application -> Cookies)."
193
+ % (", ".join(tried), hint)
194
+ )
@@ -0,0 +1,127 @@
1
+ """Command-line entry point for cursor-usage."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from datetime import datetime, timedelta, timezone
7
+
8
+ from . import __version__
9
+ from .api import CursorAPIError, CursorClient
10
+ from .auth import SessionNotFound, resolve_cookie_value
11
+ from .report import render_by_day, render_summary, write_csv
12
+
13
+
14
+ def _ms(dt):
15
+ return int(dt.timestamp() * 1000)
16
+
17
+
18
+ def _parse_date(s):
19
+ return datetime.strptime(s, "%Y-%m-%d").replace(tzinfo=timezone.utc)
20
+
21
+
22
+ def _resolve_window(args, client, user_id):
23
+ """Return (start_ms, end_ms, start_label, end_label)."""
24
+ now = datetime.now(timezone.utc)
25
+ end = _parse_date(args.end) + timedelta(days=1) if args.end else now
26
+ if args.start:
27
+ start = _parse_date(args.start)
28
+ elif args.days:
29
+ start = now - timedelta(days=args.days)
30
+ elif args.month:
31
+ y, m = (int(x) for x in args.month.split("-"))
32
+ start = datetime(y, m, 1, tzinfo=timezone.utc)
33
+ if not args.end:
34
+ nm = datetime(y + (m == 12), (m % 12) + 1, 1, tzinfo=timezone.utc)
35
+ end = min(now, nm)
36
+ else:
37
+ # default: current billing month, per /api/usage startOfMonth
38
+ som = client.usage(user_id).get("startOfMonth")
39
+ start = (datetime.fromisoformat(som.replace("Z", "+00:00"))
40
+ if som else now.replace(day=1, hour=0, minute=0, second=0, microsecond=0))
41
+ return _ms(start), _ms(end), start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d")
42
+
43
+
44
+ def _progress(fetched, total):
45
+ if total:
46
+ sys.stderr.write("\r[events] %d/%d" % (fetched, total))
47
+ sys.stderr.flush()
48
+ if fetched >= total:
49
+ sys.stderr.write("\n")
50
+
51
+
52
+ def build_parser():
53
+ p = argparse.ArgumentParser(
54
+ prog="cursor-usage",
55
+ description="Show Cursor (cursor.com) usage, spend and per-event logs "
56
+ "for the locally signed-in account.",
57
+ )
58
+ p.add_argument("--by-day", action="store_true",
59
+ help="break usage down by calendar day (fetches per-event data)")
60
+ p.add_argument("--csv", metavar="FILE",
61
+ help="write the per-event log to FILE as CSV")
62
+ p.add_argument("--json", action="store_true",
63
+ help="print the raw aggregated-usage JSON and exit")
64
+ g = p.add_argument_group("time window (default: current billing month)")
65
+ g.add_argument("--start", metavar="YYYY-MM-DD", help="window start (inclusive)")
66
+ g.add_argument("--end", metavar="YYYY-MM-DD", help="window end (inclusive)")
67
+ g.add_argument("--days", type=int, metavar="N", help="last N days")
68
+ g.add_argument("--month", metavar="YYYY-MM", help="a specific calendar month")
69
+ p.add_argument("--page-size", type=int, default=1000,
70
+ help="events per API page when paginating (default: 1000)")
71
+ p.add_argument("-v", "--verbose", action="store_true", help="log the token source")
72
+ p.add_argument("-V", "--version", action="version",
73
+ version="cursor-usage %s" % __version__)
74
+ return p
75
+
76
+
77
+ def main(argv=None):
78
+ args = build_parser().parse_args(argv)
79
+ try:
80
+ cookie = resolve_cookie_value(verbose=args.verbose)
81
+ except SessionNotFound as exc:
82
+ print(str(exc), file=sys.stderr)
83
+ return 2
84
+
85
+ client = CursorClient(cookie)
86
+ try:
87
+ me = client.me()
88
+ user_id = me.get("id")
89
+ email = me.get("email", "unknown")
90
+ if not user_id:
91
+ print("Session token is invalid or expired (auth/me returned no id).\n"
92
+ "Re-authenticate: cursor-agent login", file=sys.stderr)
93
+ return 2
94
+
95
+ start_ms, end_ms, start_label, end_label = _resolve_window(args, client, user_id)
96
+ meta = {"email": email, "start": start_label, "end": end_label}
97
+
98
+ if args.json:
99
+ print(json.dumps(client.aggregated_usage(user_id, start_ms, end_ms), indent=2))
100
+ return 0
101
+
102
+ # The aggregated endpoint powers the summary (one cheap call).
103
+ print(render_summary(client.aggregated_usage(user_id, start_ms, end_ms), meta))
104
+
105
+ # Per-event data powers --by-day and --csv; fetch it once if needed.
106
+ if args.by_day or args.csv:
107
+ events, total = client.all_events(
108
+ user_id, start_ms, end_ms, page_size=args.page_size, progress=_progress)
109
+ if args.by_day:
110
+ print()
111
+ print(render_by_day(events, meta))
112
+ if args.csv:
113
+ n = write_csv(events, args.csv)
114
+ print("\nWrote %d events to %s" % (n, args.csv))
115
+ except CursorAPIError as exc:
116
+ print("\nCursor API error (HTTP %s): %s" % (exc.status, exc.body[:300]), file=sys.stderr)
117
+ if exc.status in (401, 403):
118
+ print("Session may be expired. Re-authenticate: cursor-agent login", file=sys.stderr)
119
+ return 1
120
+ except OSError as exc:
121
+ print("\nNetwork error talking to cursor.com: %s" % exc, file=sys.stderr)
122
+ return 1
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main())
@@ -0,0 +1,134 @@
1
+ """Turn raw API payloads into summaries, day breakdowns and CSV rows."""
2
+
3
+ import csv
4
+ from collections import OrderedDict
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ # --- field extraction -----------------------------------------------------
9
+ def _i(d, k):
10
+ try:
11
+ return int(d.get(k, 0) or 0)
12
+ except (TypeError, ValueError):
13
+ return 0
14
+
15
+
16
+ def _f(d, k):
17
+ try:
18
+ return float(d.get(k, 0) or 0)
19
+ except (TypeError, ValueError):
20
+ return 0.0
21
+
22
+
23
+ def event_row(ev):
24
+ """Flatten one ``usageEventsDisplay`` entry into a plain dict."""
25
+ tu = ev.get("tokenUsage") or {}
26
+ ts_ms = _i(ev, "timestamp")
27
+ local = datetime.fromtimestamp(ts_ms / 1000).astimezone() if ts_ms else None
28
+ return OrderedDict([
29
+ ("datetime_local", local.isoformat() if local else ""),
30
+ ("timestamp_ms", ts_ms),
31
+ ("date", local.strftime("%Y-%m-%d") if local else ""),
32
+ ("model", ev.get("model", "")),
33
+ ("kind", ev.get("kind", "")),
34
+ ("input_tokens", _i(tu, "inputTokens")),
35
+ ("output_tokens", _i(tu, "outputTokens")),
36
+ ("cache_read_tokens", _i(tu, "cacheReadTokens")),
37
+ ("cache_write_tokens", _i(tu, "cacheWriteTokens")),
38
+ ("value_cents", round(_f(tu, "totalCents"), 6)),
39
+ ("charged_cents", round(_f(ev, "chargedCents"), 6)),
40
+ ("requests_costs", ev.get("requestsCosts", 0)),
41
+ ("is_headless", bool(ev.get("isHeadless", False))),
42
+ ("owning_user", ev.get("owningUser", "")),
43
+ ])
44
+
45
+
46
+ # --- formatting helpers ---------------------------------------------------
47
+ def _money(cents):
48
+ return "$%s" % format(cents / 100.0, ",.2f")
49
+
50
+
51
+ def _n(x):
52
+ return format(int(x), ",")
53
+
54
+
55
+ def _rule(width=78):
56
+ return "-" * width
57
+
58
+
59
+ # --- summary (from aggregated endpoint) -----------------------------------
60
+ def render_summary(agg, meta):
61
+ rows = agg.get("aggregations", []) or []
62
+ cents = lambda r: _f(r, "totalCents")
63
+ total = sum(cents(r) for r in rows)
64
+ ti = sum(_i(r, "inputTokens") for r in rows)
65
+ to = sum(_i(r, "outputTokens") for r in rows)
66
+ tcr = sum(_i(r, "cacheReadTokens") for r in rows)
67
+ tcw = sum(_i(r, "cacheWriteTokens") for r in rows)
68
+
69
+ out = ["=" * 78]
70
+ out.append("CURSOR USAGE | %s | %s -> %s"
71
+ % (meta["email"], meta["start"], meta["end"]))
72
+ out.append("=" * 78)
73
+ out.append("Included value used : %s (compute consumed; included in plan)" % _money(total))
74
+ out.append("Tokens in=%s out=%s" % (_n(ti), _n(to)))
75
+ out.append(" cacheRead=%s cacheWrite=%s" % (_n(tcr), _n(tcw)))
76
+ out.append(_rule())
77
+ out.append("%-36s%10s%14s%12s" % ("model", "$ value", "in tok", "out tok"))
78
+ out.append(_rule())
79
+ for r in sorted(rows, key=lambda x: -cents(x)):
80
+ out.append("%-36s%10s%14s%12s" % (
81
+ r.get("modelIntent", "?"),
82
+ format(cents(r) / 100.0, ",.2f"),
83
+ _n(_i(r, "inputTokens")),
84
+ _n(_i(r, "outputTokens")),
85
+ ))
86
+ return "\n".join(out)
87
+
88
+
89
+ # --- per-day breakdown (from events) --------------------------------------
90
+ def render_by_day(events, meta):
91
+ days = OrderedDict()
92
+ for ev in events:
93
+ row = event_row(ev)
94
+ d = row["date"] or "unknown"
95
+ b = days.setdefault(d, {"n": 0, "in": 0, "out": 0, "cents": 0.0})
96
+ b["n"] += 1
97
+ b["in"] += row["input_tokens"]
98
+ b["out"] += row["output_tokens"]
99
+ b["cents"] += row["value_cents"]
100
+
101
+ out = ["=" * 78]
102
+ out.append("CURSOR USAGE BY DAY | %s | %s -> %s"
103
+ % (meta["email"], meta["start"], meta["end"]))
104
+ out.append("=" * 78)
105
+ out.append("%-12s%9s%12s%15s%13s" % ("date", "events", "$ value", "in tok", "out tok"))
106
+ out.append(_rule())
107
+ tot = {"n": 0, "in": 0, "out": 0, "cents": 0.0}
108
+ for d in sorted(days):
109
+ b = days[d]
110
+ out.append("%-12s%9s%12s%15s%13s" % (
111
+ d, _n(b["n"]), format(b["cents"] / 100.0, ",.2f"), _n(b["in"]), _n(b["out"])))
112
+ for k in tot:
113
+ tot[k] += b[k]
114
+ out.append(_rule())
115
+ out.append("%-12s%9s%12s%15s%13s" % (
116
+ "TOTAL", _n(tot["n"]), format(tot["cents"] / 100.0, ",.2f"),
117
+ _n(tot["in"]), _n(tot["out"])))
118
+ return "\n".join(out)
119
+
120
+
121
+ # --- CSV ------------------------------------------------------------------
122
+ def write_csv(events, path):
123
+ rows = [event_row(e) for e in events]
124
+ rows.sort(key=lambda r: r["timestamp_ms"])
125
+ fields = list(event_row({}).keys())
126
+ with open(path, "w", newline="", encoding="utf-8") as fh:
127
+ writer = csv.DictWriter(fh, fieldnames=fields)
128
+ writer.writeheader()
129
+ writer.writerows(rows)
130
+ return len(rows)
131
+
132
+
133
+ def to_iso(ms):
134
+ return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d")
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: cursor-usage
3
+ Version: 0.1.0
4
+ Summary: Cross-platform CLI to read your Cursor (cursor.com) usage, spend, and per-event logs.
5
+ Author: cursor-usage contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/javaisbetterthanpython/cursor-usage
8
+ Project-URL: Issues, https://github.com/javaisbetterthanpython/cursor-usage/issues
9
+ Keywords: cursor,usage,spend,cli,ai,tokens,llm
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: keyring
21
+ Requires-Dist: keyring>=24; extra == "keyring"
22
+ Dynamic: license-file
23
+
24
+ <h1 align="center">๐Ÿ“Š cursor-usage</h1>
25
+
26
+ <p align="center">
27
+ <b>See your <a href="https://cursor.com">Cursor</a> usage, spend, and per-event logs โ€” right from your terminal.</b>
28
+ </p>
29
+
30
+ <p align="center">
31
+ <img alt="Python" src="https://img.shields.io/badge/python-3.8%2B-blue">
32
+ <img alt="Platforms" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey">
33
+ <img alt="Dependencies" src="https://img.shields.io/badge/dependencies-zero-brightgreen">
34
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-green">
35
+ </p>
36
+
37
+ ---
38
+
39
+ Cursor shows your usage on its web dashboard, but there's no official way to get
40
+ it from the command line. **`cursor-usage` gives you that** โ€” a clean summary, a
41
+ per-day breakdown, and a full per-event CSV export โ€” using the session your
42
+ Cursor app already has. No API key to manage, nothing to configure.
43
+
44
+ ```text
45
+ ==============================================================================
46
+ CURSOR USAGE | you@example.com | 2026-06-04 -> 2026-06-09
47
+ ==============================================================================
48
+ Included value used : $875.81 (compute consumed; included in plan)
49
+ Tokens in=126,033,860 out=18,989,068
50
+ cacheRead=2,492,781,450 cacheWrite=8,322,520
51
+ ------------------------------------------------------------------------------
52
+ model $ value in tok out tok
53
+ ------------------------------------------------------------------------------
54
+ composer-2.5 460.14 95,638,983 13,315,470
55
+ claude-4.6-opus-high 233.71 786,434 1,059,451
56
+ gemini-3.5-flash 45.77 16,097,065 1,075,904
57
+ gpt-5.4-high 43.65 4,069,043 1,041,967
58
+ ...
59
+ ```
60
+
61
+ ## โœจ Features
62
+
63
+ - **One command, real numbers** โ€” per-model tokens and compute value for the
64
+ current billing month.
65
+ - **๐Ÿ“… Per-day breakdown** โ€” `--by-day` shows how much you burned each day.
66
+ - **๐Ÿงพ CSV export** โ€” `--csv` dumps every usage event (timestamp, model, tokens,
67
+ cost) for your own spreadsheets and charts.
68
+ - **๐ŸŒ Cross-platform** โ€” macOS, Linux, and Windows.
69
+ - **๐Ÿ”‹ Zero dependencies** โ€” pure Python standard library.
70
+ - **๐Ÿ”’ Local & private** โ€” reads the session your Cursor app already stored;
71
+ talks only to `cursor.com`. No telemetry, no third parties.
72
+
73
+ ## ๐Ÿš€ Quickstart
74
+
75
+ ```bash
76
+ pip install cursor-usage # or: pip install . from a clone
77
+ cursor-usage # summary for the current billing month
78
+ ```
79
+
80
+ That's it โ€” if you're signed in to Cursor on this machine, it just works.
81
+
82
+ ## ๐Ÿง‘โ€๐Ÿ’ป Usage
83
+
84
+ | Command | What it does |
85
+ |---|---|
86
+ | `cursor-usage` | Summary for the current billing month |
87
+ | `cursor-usage --by-day` | Add a per-day breakdown |
88
+ | `cursor-usage --csv usage.csv` | Export every usage event to CSV |
89
+ | `cursor-usage --days 7` | Window: the last 7 days |
90
+ | `cursor-usage --month 2026-05` | Window: a specific month |
91
+ | `cursor-usage --start 2026-06-01 --end 2026-06-07` | Window: an explicit range |
92
+ | `cursor-usage --json` | Raw aggregated JSON (for scripting) |
93
+ | `cursor-usage -v` | Also print which session source was used |
94
+
95
+ Flags combine โ€” e.g. `cursor-usage --by-day --csv june.csv --month 2026-06`.
96
+
97
+ <details>
98
+ <summary><b>๐Ÿ“… Example: <code>--by-day</code></b></summary>
99
+
100
+ ```text
101
+ ==============================================================================
102
+ CURSOR USAGE BY DAY | you@example.com | 2026-06-02 -> 2026-06-09
103
+ ==============================================================================
104
+ date events $ value in tok out tok
105
+ ------------------------------------------------------------------------------
106
+ 2026-06-04 433 324.46 38,602,275 6,080,617
107
+ 2026-06-05 368 252.58 40,616,543 6,469,309
108
+ 2026-06-06 416 121.06 29,686,247 4,497,010
109
+ ------------------------------------------------------------------------------
110
+ TOTAL 1,661 929.46 128,631,599 20,101,078
111
+ ```
112
+ </details>
113
+
114
+ <details>
115
+ <summary><b>๐Ÿงพ CSV columns</b></summary>
116
+
117
+ `datetime_local, timestamp_ms, date, model, kind, input_tokens, output_tokens,
118
+ cache_read_tokens, cache_write_tokens, value_cents, charged_cents,
119
+ requests_costs, is_headless, owning_user` โ€” one row per usage event, sorted by
120
+ time.
121
+ </details>
122
+
123
+ ## ๐Ÿค” How it works
124
+
125
+ A Cursor **API key** (`crsr_โ€ฆ`) can't read usage โ€” that data lives behind your
126
+ web **session**, the same one your browser/app uses on `cursor.com`. This tool
127
+ finds that session locally and asks Cursor's dashboard API for your numbers.
128
+
129
+ It looks for the session in this order (all **local-only**):
130
+
131
+ 1. `CURSOR_SESSION_TOKEN` environment variable (manual override)
132
+ 2. macOS Keychain (written by the `cursor-agent` CLI)
133
+ 3. Your OS keyring, if the optional `keyring` package is installed
134
+ 4. The Cursor app's local state database (works on every OS)
135
+
136
+ If it can't find one, sign in to the Cursor app and run it again.
137
+
138
+ <details>
139
+ <summary><b>๐Ÿ” Where exactly the session lives (per OS)</b></summary>
140
+
141
+ The Cursor IDE stores the session token in a small SQLite file
142
+ (`state.vscdb` โ†’ key `cursorAuth/accessToken`), in the same place on every OS:
143
+
144
+ | OS | Path |
145
+ |---|---|
146
+ | macOS | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` |
147
+ | Linux | `~/.config/Cursor/User/globalStorage/state.vscdb` |
148
+ | Windows | `%APPDATA%\Cursor\User\globalStorage\state.vscdb` |
149
+
150
+ Want the full reverse-engineering story (and a recipe to rebuild this tool)? See
151
+ **[docs/HOW_THIS_WAS_BUILT.md](docs/HOW_THIS_WAS_BUILT.md)**.
152
+ </details>
153
+
154
+ ### Manual override
155
+
156
+ On any OS you can skip auto-detection entirely:
157
+
158
+ ```bash
159
+ # cursor.com โ†’ DevTools โ†’ Application โ†’ Cookies โ†’ copy WorkosCursorSessionToken
160
+ export CURSOR_SESSION_TOKEN='user_โ€ฆ::eyJhbGciโ€ฆ'
161
+ cursor-usage
162
+ ```
163
+
164
+ ## ๐Ÿ”’ Privacy & security
165
+
166
+ - The tool only **reads** your existing local session โ€” it never writes,
167
+ refreshes, or sends it anywhere except `cursor.com`.
168
+ - **No telemetry. No third-party calls.**
169
+ - CSV exports contain your own usage data; they're git-ignored by default so you
170
+ don't commit them by accident.
171
+
172
+ ## โš ๏ธ Good to know
173
+
174
+ - **`$ value` is compute consumed, not money owed.** On plans where usage-based
175
+ pricing is off, your bill is just the flat subscription โ€” these figures show
176
+ the value of the compute included in your plan.
177
+ - This uses Cursor's **internal, undocumented** dashboard API. It works great
178
+ today, but Cursor could change it at any time. If something breaks, please open
179
+ an issue.
180
+ - If your session has expired, sign back in to Cursor and run the command again.
181
+
182
+ ## ๐Ÿ› ๏ธ Install options
183
+
184
+ ```bash
185
+ pip install cursor-usage # from PyPI (once published)
186
+ pip install . # from a local clone
187
+ pip install "cursor-usage[keyring]" # + OS-keyring lookup on Linux/Windows
188
+ ```
189
+
190
+ Requires Python 3.8+.
191
+
192
+ ## ๐Ÿค Contributing
193
+
194
+ Issues and PRs are welcome. Run the tests with:
195
+
196
+ ```bash
197
+ pip install pytest && pytest -q
198
+ ```
199
+
200
+ ## ๐Ÿ“„ License
201
+
202
+ [MIT](LICENSE) โ€” do whatever you like.
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/cursor_usage/__init__.py
5
+ src/cursor_usage/__main__.py
6
+ src/cursor_usage/api.py
7
+ src/cursor_usage/auth.py
8
+ src/cursor_usage/cli.py
9
+ src/cursor_usage/report.py
10
+ src/cursor_usage.egg-info/PKG-INFO
11
+ src/cursor_usage.egg-info/SOURCES.txt
12
+ src/cursor_usage.egg-info/dependency_links.txt
13
+ src/cursor_usage.egg-info/entry_points.txt
14
+ src/cursor_usage.egg-info/requires.txt
15
+ src/cursor_usage.egg-info/top_level.txt
16
+ tests/test_auth.py
17
+ tests/test_report.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cursor-usage = cursor_usage.cli:main
@@ -0,0 +1,3 @@
1
+
2
+ [keyring]
3
+ keyring>=24
@@ -0,0 +1 @@
1
+ cursor_usage
@@ -0,0 +1,27 @@
1
+ import base64
2
+ import json
3
+
4
+ from cursor_usage.auth import _cookie_id, _jwt_claims
5
+
6
+
7
+ def _make_jwt(claims):
8
+ seg = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
9
+ return "header.%s.signature" % seg
10
+
11
+
12
+ def test_cookie_id_strips_connection_prefix():
13
+ assert _cookie_id({"sub": "github|user_01ABC"}) == "user_01ABC"
14
+ assert _cookie_id({"sub": "user_01ABC"}) == "user_01ABC"
15
+ assert _cookie_id({}) is None
16
+
17
+
18
+ def test_jwt_claims_roundtrip():
19
+ tok = _make_jwt({"sub": "github|user_01XYZ", "type": "session"})
20
+ claims = _jwt_claims(tok)
21
+ assert claims["type"] == "session"
22
+ assert _cookie_id(claims) == "user_01XYZ"
23
+
24
+
25
+ def test_jwt_claims_handles_garbage():
26
+ assert _jwt_claims("not-a-jwt") == {}
27
+ assert _jwt_claims("") == {}
@@ -0,0 +1,55 @@
1
+ from cursor_usage.report import event_row, render_by_day, render_summary, write_csv
2
+
3
+ SAMPLE = {
4
+ "timestamp": "1780526996230",
5
+ "model": "composer-2.5",
6
+ "kind": "USAGE_EVENT_KIND_INCLUDED_IN_PRO",
7
+ "tokenUsage": {
8
+ "inputTokens": 100, "outputTokens": 20,
9
+ "cacheReadTokens": 5, "cacheWriteTokens": 1, "totalCents": 12.5,
10
+ },
11
+ "chargedCents": 12.5, "requestsCosts": 1, "isHeadless": False, "owningUser": "42",
12
+ }
13
+ META = {"email": "you@example.com", "start": "2026-06-01", "end": "2026-06-08"}
14
+
15
+
16
+ def test_event_row_extracts_fields():
17
+ r = event_row(SAMPLE)
18
+ assert r["model"] == "composer-2.5"
19
+ assert r["input_tokens"] == 100
20
+ assert r["output_tokens"] == 20
21
+ assert r["value_cents"] == 12.5
22
+ assert r["date"].count("-") == 2 # YYYY-MM-DD
23
+
24
+
25
+ def test_event_row_tolerates_empty():
26
+ r = event_row({})
27
+ assert r["input_tokens"] == 0
28
+ assert r["model"] == ""
29
+ assert r["timestamp_ms"] == 0
30
+
31
+
32
+ def test_render_by_day_totals():
33
+ out = render_by_day([SAMPLE, SAMPLE], META)
34
+ assert "BY DAY" in out
35
+ assert "TOTAL" in out
36
+ assert "200" in out # 100 in-tokens * 2 events
37
+
38
+
39
+ def test_render_summary_money():
40
+ agg = {"aggregations": [
41
+ {"modelIntent": "composer-2.5", "inputTokens": "1",
42
+ "outputTokens": "2", "totalCents": 100},
43
+ ]}
44
+ out = render_summary(agg, META)
45
+ assert "$1.00" in out
46
+ assert "composer-2.5" in out
47
+
48
+
49
+ def test_write_csv(tmp_path):
50
+ path = tmp_path / "out.csv"
51
+ n = write_csv([SAMPLE], str(path))
52
+ assert n == 1
53
+ text = path.read_text()
54
+ assert "input_tokens" in text # header
55
+ assert "composer-2.5" in text # row