Skip to content

feat(cursor): Cursor sessions via the Cursor API (token set in the UI) - #29

Open
daimonbot wants to merge 3 commits into
mainfrom
feat/cursor-provider
Open

daimonbot wants to merge 3 commits into
mainfrom
feat/cursor-provider

Conversation

@daimonbot

@daimonbot daimonbot commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Closes #27.

Do not merge before operator review. It depends on an API whose real responses I could not call (no key), and on a key type an individual account may not be able to get.

What it does

  • New provider cursor (src/providers/cursor.mjs) next to Claude and Codex; sessions show a provider badge in the list.
  • Settings panel on the sessions page: paste key, Test connection, Sync now, Remove key. Status shows only "key set" and the last 4 characters.
  • src/cursor-api.mjs: token store, HTTP client, event cache. Sync runs on save, hourly and on demand, in the server (not in discovery), so providers stay synchronous and offline.

API used

Cursor Admin API, POST https://api.cursor.com/teams/filtered-usage-events (per-request events: model, conversationId, tokenUsage with input/output/cacheWrite/cacheRead tokens and totalCents). Auth: HTTP Basic, key as username, empty password.

Individual accounts: per the docs, Admin API keys are created by team administrators (API Keys page in the dashboard) and the API is listed for Enterprise teams. I found no documented personal usage endpoint. The other key types (Analytics, Cloud Agents user key, Origin personal key) are not usage-event APIs. So a Pro/individual account probably cannot use this. If the operator has no team plan, the panel will report 401/403 and nothing else works. This is the main open point; an undocumented dashboard-cookie endpoint exists but I did not use it (unofficial, session cookie as secret).

Session mapping

Cursor has no session id. Decision for the operator: sessions group by conversationId when present; otherwise per user, a gap of more than 30 minutes starts a new session. Each event is one request = one turn with one call. No prompts are available (humanMsgs 0, no first prompt); title is "Cursor conversation " or "Cursor · email". An admin key returns the whole team's events, so a team shows every member's sessions.

Cost

Not priced through the Claude/Codex tables (Cursor model names differ and Cursor already reports money). Cost is tokenUsage.totalCents with confidence reported. Request-based events (isTokenBasedCall false) have no tokens or amount, so such sessions are partial. Per-token-class cost parts are zero (Cursor does not give them), so the in/out/cache $ sub-figures show $0 for Cursor rows while the total is right. Once #28 lands, tokens could be re-priced through its entry point; not done here to avoid its files.

Token handling

  • ~/.config/agent-atlas/cursor-token (dir 0700, file 0600; AGENT_ATLAS_CONFIG_DIR overrides). Removing the key also deletes the cached events.
  • Never logged, never returned by any endpoint. Sent only to the constant https://api.cursor.com (no env override, redirect: "error"). Error messages carry the HTTP status only, never the response body.
  • Mutating endpoints require application/json and a same-origin Origin, so another web page cannot drive a local server.

Tests

test/cursor.test.mjs (8 tests, mocked HTTP, fixture test/fixtures/cursor-usage-events.json modelled on the documented response shape): token storage/permissions/redaction, Basic auth and host, error mapping, pagination and merge, grouping, tree tokens/cost, and the server flow. typecheck and build pass. npm test: the same 4 failures as main (browser test and 3 querySessions date tests) remain; I left them alone.

Unverified

Field names and shapes come from the docs, not from a real response. Event time range limits for this endpoint are unstated (I request 30 days). A real key may reveal differences.

Review fixes (second push)

  • DNS rebinding: every /api/* route only accepts Host 127.0.0.1:<port>, localhost:<port>, [::1]:<port> (plus serve({ allowedHosts })). Changes (PUT/POST/DELETE) also need a matching Origin, application/json, and a per-launch CSRF token (injected into the served page as a meta tag, sent as x-atlas-csrf).
  • Key files: random exclusive (wx, 0600) temp file, so a planted symlink is never followed; config dir must be owned by the user and is tightened to 0700.
  • Removing or replacing the key: a credential generation counter drops the result of a sync started under an older key; a different key clears the cache, and the cache carries a key fingerprint, so teams never merge.
  • Cost now uses chargedCents, falling back to totalCents; each tree records costBasis (chargedCents, totalCents or mixed). This supersedes the Cost section above: request-based events get their charged cost instead of making the session partial. Included usage may show $0 charged.
  • If the 200-page cap is hit with more pages pending, the sync result, the settings panel and the session titles say the data is incomplete.
  • The settings panel states that all team members' usage is downloaded, cached locally and included in exports.
  • Tests: 12 in test/cursor.test.mjs (rebound Host on reads and writes, missing Origin/CSRF, symlink, in-flight sync, charged vs total, truncation). Same 4 pre-existing failures on main elsewhere.

Last round

  • Host is validated on every route (shell and assets too), so a rebound Host no longer gets the CSRF token.
  • Before writing the cache a sync re-reads the key file and compares fingerprints, so a second server that removed or replaced the key wins.
  • Truncation is sticky: incomplete time ranges are stored and cleared only by a later full sync that covers them, or by removing the key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Cursor provider via the Cursor API (user-supplied token in the UI)

1 participant