@vidit19sharma/opencode-cursor-auth 0.1.0 → 0.3.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ ## 0.3.1 — 2026-10-16
6
+
7
+ ### Fixed
8
+
9
+ - opencode crashed at startup on a fresh machine (`undefined is not an object (evaluating 'C.provider')`, and `C.auth` on `opencode auth login`) as soon as the plugin was enabled. opencode invokes **every named export** of a plugin module as a plugin; the entry module also exported `CursorAuthPlugin`, `keyProblem` and `parseContext`, so opencode called the helpers as plugins and tried to read hooks off their return values. The entry now exposes only `default`; helpers moved to `src/plugin.ts`. Affects 0.1.0–0.3.0. A test guards the export shape.
10
+
11
+ ## 0.3.0 — 2026-10-16
12
+
13
+ ### Breaking
14
+
15
+ - The suffixed variant ids from 0.2.0 (`cursor/gpt-5.5-low`, `cursor/claude-opus-5-5-1m-max`, …) no longer exist; Cursor rejects them with 400 "Cannot use this model". Migrate `cursor/gpt-5.5-low` to model `cursor/gpt-5.5` + variant `low` (Ctrl+T in the TUI, or `variant:` in config).
16
+
17
+ ### Added
18
+
19
+ - Cursor variants on opencode's native variant picker (Ctrl+T): the model list is the 47 base ids only, and every other Cursor variant is attached as an opencode model variant, named by the params that differ from Cursor's default (e.g. `low`, `272k-none`, `no-thinking`, `xhigh-fast`).
20
+ - Variants are selectable in config with `variant: "<name>"` next to `model` in an agent entry.
21
+ - `installation.md`: step-by-step install guide for AI coding agents (prerequisites, config edit, auth, verification, troubleshooting), shipped in the npm tarball; the README has a one-line prompt to hand to any LLM agent.
22
+ - `spike/probe-models.ts`: probes every base model once and reports AVAILABLE / BLOCKED / OTHER for the logged-in account.
23
+
24
+ ### Changed
25
+
26
+ - A base id now means Cursor's own default variant (`isDefault`), sends its params explicitly, and gets that variant's context limit; previously it implied the first-listed param values and under-reported context (`gpt-5.5`: 272k → 1M).
27
+ - Models cache bumped to v4 `{name, context, params, variants, allowed}`; older cache files are refetched once.
28
+
29
+ ### Fixed
30
+
31
+ - Blocked-model flags learned from 403s persist across the 24h catalog refresh (carried over by base id), so admin-blocked models stay hidden instead of reappearing; delete `models.json` to re-check.
32
+ - An unknown variant param id or value (sent as `cursor_params`) returns 400 `invalid_request_error` before any Cursor agent is created or concurrency slot taken.
33
+
34
+ ## 0.2.0 — 2026-10-16
35
+
36
+ ### Added
37
+
38
+ - Model variants: every variant Cursor publishes (reasoning effort, fast, 1M context) is a selectable id, e.g. `cursor/gpt-5.5-low-fast`, `cursor/claude-opus-5-5-1m-max`. Ids are the base id plus non-default param values and map back to Cursor `ModelSelection` params; unknown ids still pass through to Cursor's own 400.
39
+ - Model context limits: each model gets `limit.context` from Cursor's `context` parameter (default 200k) and `limit.output` 32k. Without a limit opencode 1.19 sets context to 0 and silently disables auto-compaction and the context indicator.
40
+ - SDK key check on `/chat/completions` (60s cache, skipped for pasted/env keys): a missing or expired key returns 401 telling the user to run `opencode auth login`; one-time stderr warning within 7 days of expiry.
41
+ - Concurrency cap: a FIFO semaphore limits concurrently active Cursor runs (default 4, `OPENCODE_CURSOR_MAX_ACTIVE` to override). Extra requests queue; tool-result continuations never take a slot.
42
+ - Retry: `Agent.create` / `Agent.resume` / `send` are attempted up to 3 times with jittered backoff on 429, 5xx, and network errors; never after the first byte.
43
+ - Usage reports cache-read tokens (`prompt_tokens_details.cached_tokens`) and reasoning tokens (`completion_tokens_details.reasoning_tokens`).
44
+
45
+ ### Changed
46
+
47
+ - Models cache bumped to v3 `{name, context, params}`; older cache files are refetched once. Catalog code moved to `src/models.ts`.
48
+ - The reply is opened only on content deltas (text/thinking), so run errors before any content become real JSON error responses instead of a 200 stream with an error frame.
49
+
50
+ ### Fixed
51
+
52
+ - Real HTTP error statuses: failures before the first byte return a JSON error with a real status (400 bad model/argument, 401 auth, 403 blocked, 429 rate limit, 502 otherwise); mid-stream failures emit an OpenAI error frame and close without `[DONE]`.
53
+ - Admin-blocked models: "Model Blocked" / `permission_denied` map to 403 `permission_error` and are never retried. A 403 marks the model and its variants blocked in the models cache; they are hidden from the model list until the next 24h catalog refresh and still resolve if used explicitly.
54
+ - Abort propagation: the request AbortSignal and stream `cancel()` cancel the Cursor run, close the agent, and release registries (no more zombie runs burning usage).
55
+ - agentId race: the held run is registered before `send()`, so a tool call firing immediately can never miss its continuation lookup.
56
+ - Session resume is scoped by opencode session id (via the `chat.headers` hook), so byte-identical histories from different sessions (parallel sub-agents) never resume each other's Cursor agent. Unscoped callers and existing disk entries hash exactly as before.
57
+ - Per-session temp workspaces are removed once no session, disk, or held entry references them.
58
+
59
+ ## 0.1.0 — 2026-10-15
60
+
61
+ - Initial release: in-process Cursor SDK bridge, auth, tool loop, session resume, AGENTS.md system prompt, dynamic model list.
package/README.md CHANGED
@@ -10,9 +10,14 @@ The plugin bridges opencode's OpenAI-style chat requests to the official [`@curs
10
10
  - **In-process bridge** — no local HTTP proxy, no port to collide on, no extra daemon
11
11
  - **Full tool loop** — opencode's native tools (read, edit, bash, MCP, …) work end-to-end via a held-run bridge: one Cursor run spans the whole tool exchange
12
12
  - **Thinking blocks** — Cursor reasoning streams into opencode as proper thinking blocks (`reasoning_content`)
13
- - **Session resume** — conversations map to native Cursor agents (`Agent.resume`), in-memory and persisted to disk for 7 days, so multi-turn chats don't replay history
13
+ - **Session resume** — conversations map to native Cursor agents (`Agent.resume`), in-memory and persisted to disk for 7 days, scoped per opencode session so parallel sub-agents never share an agent
14
14
  - **System prompts** — delivered via `AGENTS.md` in a per-session workspace (the `--system-prompt` flag is server-gated by Cursor)
15
- - **Dynamic model list** — fetched live from `Cursor.models.list()`, cached 24h
15
+ - **Dynamic model list with variants** — fetched live from `Cursor.models.list()`, cached 24h; one id per model, with every reasoning-effort / thinking / fast / context variant on opencode's Ctrl+T variant picker
16
+ - **Context limits** — each model publishes the context window of Cursor's default variant, so opencode's auto-compaction and context indicator work
17
+ - **Real error statuses** — 400 / 401 / 403 / 429 / 502 instead of a 200 stream carrying an error
18
+ - **Cancellation** — Esc or a sub-agent timeout cancels the Cursor run and closes the agent
19
+ - **Concurrency cap and retry** — at most 4 Cursor runs at once (configurable); transient failures retried before the first byte
20
+ - **Usage** — prompt, completion, cached-read, and reasoning tokens reported to opencode
16
21
  - **Zero cost display** — models are marked cost-free since you're on subscription billing
17
22
 
18
23
  ## Requirements
@@ -23,6 +28,14 @@ The plugin bridges opencode's OpenAI-style chat requests to the official [`@curs
23
28
 
24
29
  ## Install
25
30
 
31
+ For AI agents — paste this into any LLM agent:
32
+
33
+ ```
34
+ Install the @vidit19sharma/opencode-cursor-auth plugin and configure it by following: https://raw.githubusercontent.com/vidit19sharma/opencode-cursor-auth/main/installation.md
35
+ ```
36
+
37
+ See [installation.md](installation.md) for the step-by-step agent instructions.
38
+
26
39
  Add the plugin to `~/.config/opencode/opencode.json`:
27
40
 
28
41
  ```json
@@ -46,6 +59,8 @@ Pick **Cursor**, then either:
46
59
 
47
60
  Alternatively set `CURSOR_API_KEY` in your environment.
48
61
 
62
+ When the browser-login key is used, the plugin checks it before each request (cached 60s): a missing or expired key returns a 401 telling you to run `opencode auth login`, and a one-time warning is printed to stderr within 7 days of expiry. The check is skipped for pasted keys and `CURSOR_API_KEY`.
63
+
49
64
  ## Use
50
65
 
51
66
  ```sh
@@ -56,6 +71,47 @@ opencode run -m cursor/auto "hello"
56
71
 
57
72
  Or pick any `cursor/*` model from the opencode model selector.
58
73
 
74
+ ## Models
75
+
76
+ ```sh
77
+ opencode models | grep cursor/
78
+ ```
79
+
80
+ - **Base ids only** — one id per Cursor model (47 today: `cursor/auto`, `cursor/gpt-5.5`, `cursor/claude-opus-5-5`, …). A base id is Cursor's own default variant (`isDefault`) and sends its params explicitly. Cursor's `default` model is exposed as `cursor/auto`.
81
+ - **Variants** — every other Cursor variant (reasoning effort, thinking, fast, context size) is an opencode model variant. In the TUI, select the model and press **Ctrl+T** to cycle them. Models with no variants (e.g. `gemini-3.1-pro`) show "No variants available".
82
+ - **Variant names** are the params that differ from Cursor's default, joined by `-`: values as-is (`low`, `xhigh`, `272k`), a boolean switched on is the param name (`fast`, `thinking`), switched off is `no-<param>` (`no-fast`, `no-thinking`). Examples: gpt-5.5 → `low`, `extra-high`, `272k-high-fast`; claude-opus-5 → `no-thinking`, `max-fast`; grok-4.7 → `256k-low-no-fast`.
83
+ - **In config**, set `variant` next to `model`:
84
+
85
+ ```json
86
+ { "agent": { "build": { "model": "cursor/gpt-5.5", "variant": "low" } } }
87
+ ```
88
+
89
+ - **Limits**: `limit.context` is the default variant's context value, else the model's first listed context value, else 200k (gpt-5.5 and claude-opus-5-5 → 1M). `limit.output` is a fixed 32k accounting reserve (Cursor publishes no output cap and the bridge sends no `max_tokens`). Limits you set yourself in opencode config are kept.
90
+ - **Unknown ids** are passed through to Cursor, which answers with its own 400. An unknown variant param returns 400 `invalid_request_error` before any Cursor agent is created.
91
+ - **Blocked models**: Cursor's catalog has no per-account entitlement info. If Cursor rejects a model as "Model Blocked" (e.g. a team admin disabled it), the request fails with 403 `permission_error` carrying Cursor's message, and that model and all its variants are hidden from the list. Using a hidden id explicitly still sends it to Cursor. Blocked flags persist across catalog refreshes; delete `~/.cache/opencode-cursor-auth/models.json` to re-check.
92
+ - **Offline / logged out**: the stale cache is used if present, otherwise only `cursor/auto`.
93
+
94
+ The catalog is cached for 24h at `~/.cache/opencode-cursor-auth/models.json`; delete it to force a refresh.
95
+
96
+ ### Migrating from 0.2.0
97
+
98
+ The suffixed ids (`cursor/gpt-5.5-low`, `cursor/claude-opus-5-5-1m-max`, …) no longer exist; Cursor rejects them with 400 "Cannot use this model".
99
+ Replace each with the base model plus a variant: `cursor/gpt-5.5-low` → `"model": "cursor/gpt-5.5", "variant": "low"`.
100
+ The models cache is refetched once on upgrade.
101
+
102
+ ## Configuration
103
+
104
+ | Setting | Default | Purpose |
105
+ | --- | --- | --- |
106
+ | `OPENCODE_CURSOR_MAX_ACTIVE` | `4` | Max Cursor runs active at once; extra requests queue FIFO |
107
+ | `CURSOR_API_KEY` | unset | Use this key instead of the stored SDK key |
108
+
109
+ | Path | Contents |
110
+ | --- | --- |
111
+ | `~/.cursor/sdk/auth.json` | 90-day SDK key from browser login (written by `@cursor/sdk`) |
112
+ | `~/.cache/opencode-cursor-auth/models.json` | Model catalog, 24h TTL, blocked-model flags |
113
+ | `~/.cache/opencode-cursor-auth/sessions.json` | Conversation → Cursor agent map, 7-day TTL |
114
+
59
115
  ## How it works
60
116
 
61
117
  ```
@@ -65,25 +121,44 @@ opencode ──(OpenAI chat JSON)──> plugin fetch() ──> @cursor/sdk Agen
65
121
 
66
122
  - opencode calls the provider's `fetch` — the plugin intercepts it in-process; `http://cursor.local/v1` is a dummy URL that never resolves.
67
123
  - Each opencode tool is registered as a Cursor **custom tool** whose `execute()` parks a promise, emits an OpenAI `tool_calls` chunk, and resumes the *same* Cursor run when opencode sends the tool result back.
68
- - Conversation prefixes are hashed → mapped to Cursor agent IDs, so follow-up turns use `Agent.resume` instead of replaying history. The map persists at `~/.cache/opencode-cursor-auth/sessions.json`.
69
- - System messages are written to `AGENTS.md` in a per-session temp workspace and loaded via Cursor's project rules (`settingSources: ["project"]`).
124
+ - Conversation prefixes, plus the opencode session id (sent via the `chat.headers` hook), are hashed → mapped to Cursor agent IDs, so follow-up turns use `Agent.resume` instead of replaying history. The map persists at `~/.cache/opencode-cursor-auth/sessions.json`.
125
+ - System messages are written to `AGENTS.md` in a per-session temp workspace and loaded via Cursor's project rules (`settingSources: ["project"]`). Workspaces are removed once no session references them.
126
+ - The selected variant reaches the plugin as `providerOptions.cursor`; `@ai-sdk/openai-compatible` puts it in the request body as top-level `cursor_params: [{ id, value }, …]`, which the bridge validates and maps to Cursor `ModelSelection.params`.
127
+ - New turns take a slot from a FIFO semaphore (`OPENCODE_CURSOR_MAX_ACTIVE`); tool-result continuations reuse their run's slot. `Agent.create` / `Agent.resume` / `send` are attempted up to 3 times with jittered exponential backoff on 429, 5xx, and network errors, never after the first byte.
128
+ - The HTTP reply opens on the first text or thinking delta, so failures before any content become real HTTP errors.
129
+ - The request's abort signal and stream cancel cancel the Cursor run, close the agent, and release its slot.
130
+
131
+ ### Errors
132
+
133
+ | Status | When |
134
+ | --- | --- |
135
+ | 400 | Unknown model or bad argument (Cursor's message), unknown variant param; also an expired tool-call continuation |
136
+ | 401 | SDK key missing/expired, or Cursor rejects the key |
137
+ | 403 | Model blocked for your account (`permission_error`) |
138
+ | 429 | Cursor rate limit |
139
+ | 502 | Anything else |
140
+
141
+ These are JSON error responses when the failure happens before the first byte; mid-stream failures emit an SSE `error` frame and close the stream without `[DONE]`.
70
142
 
71
143
  ## Development
72
144
 
73
145
  ```sh
74
146
  bun install
75
147
  bun run build # tsc → dist/
76
- bun run test # end-to-end bridge tests (requires Cursor login)
148
+ bun run test # end-to-end bridge tests (requires Cursor login, uses real Cursor usage)
77
149
  bun run spike # raw @cursor/sdk validation
78
150
  ```
79
151
 
80
- The test suite exercises: plain streaming, a full tool loop on one held run, in-memory session resume, AGENTS.md system-prompt delivery, and disk-persisted resume.
152
+ The test suite runs 13 scenarios: plain streaming, a full tool loop on one held run, in-memory and disk-persisted session resume, AGENTS.md system-prompt delivery, mid-stream abort, error statuses, session scoping, context limits and key checks, model variants, the concurrency cap, retry semantics, and blocked models.
81
153
 
82
154
  ## Known limitations
83
155
 
84
- - `systemPrompt` as a true harness replacement is feature-gated server-side by Cursor; system text is applied as a project rule (merged under Cursor's harness prompt).
156
+ - `systemPrompt` as a true harness replacement is feature-gated server-side by Cursor; system text is applied via `AGENTS.md` as a project rule (merged under Cursor's harness prompt).
85
157
  - The SDK spawns a `cursor-agent` worker per turn (~1–2 s startup on resume).
86
- - Usage/rate limits are whatever your Cursor plan enforces — the plugin adds none.
158
+ - Cursor's catalog has no entitlement info, so models your plan or team blocks only disappear after the first 403.
159
+ - `limit.context` is per model, not per variant: picking a smaller-context variant (e.g. gpt-5.5 `272k`) keeps the base model's 1M limit in opencode's context indicator and auto-compaction.
160
+ - A run parked waiting for a tool result keeps its slot. Cursor-model sessions that spawn Cursor-model sub-agents nested deeper than `OPENCODE_CURSOR_MAX_ACTIVE` will stall; raise the limit if you fan out.
161
+ - Usage/rate limits are whatever your Cursor plan enforces — the plugin adds none beyond the concurrency cap.
87
162
 
88
163
  ## License
89
164
 
package/dist/bridge.d.ts CHANGED
@@ -27,7 +27,20 @@ type OaBody = {
27
27
  messages: OaMessage[];
28
28
  tools?: OaTool[];
29
29
  stream?: boolean;
30
+ cursor_params?: unknown;
30
31
  };
32
+ export declare function isTransient(err: unknown): boolean;
33
+ export declare function withRetry<T>(fn: () => Promise<T>, signal?: AbortSignal, baseDelayMs?: number, retryable?: typeof isTransient): Promise<T>;
34
+ export declare function _setMaxActive(n: number): void;
35
+ export declare function errorResponse(err: unknown, status?: number): Response;
31
36
  export declare function _resetForTest(): void;
32
- export declare function handleChatCompletion(body: OaBody, apiKey?: string): Promise<Response>;
37
+ export declare function _stats(): {
38
+ held: number;
39
+ sessions: number;
40
+ disk: number;
41
+ agentIds: string[];
42
+ active: number;
43
+ waiting: number;
44
+ };
45
+ export declare function handleChatCompletion(body: OaBody, apiKey?: string, signal?: AbortSignal, scope?: string): Promise<Response>;
33
46
  export {};