@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 +61 -0
- package/README.md +83 -8
- package/dist/bridge.d.ts +14 -1
- package/dist/bridge.js +375 -112
- package/dist/index.d.ts +1 -3
- package/dist/index.js +3 -128
- package/dist/models.d.ts +20 -0
- package/dist/models.js +134 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +139 -0
- package/installation.md +139 -0
- package/package.json +3 -1
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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 {};
|