@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/dist/index.d.ts CHANGED
@@ -1,3 +1 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
- export declare const CursorAuthPlugin: Plugin;
3
- export default CursorAuthPlugin;
1
+ export { CursorAuthPlugin as default } from "./plugin.js";
package/dist/index.js CHANGED
@@ -1,128 +1,3 @@
1
- import { Cursor } from "@cursor/sdk";
2
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { handleChatCompletion } from "./bridge.js";
6
- const BASE_URL = "http://cursor.local/v1";
7
- const CACHE_DIR = join(homedir(), ".cache", "opencode-cursor-auth");
8
- const MODELS_CACHE_PATH = join(CACHE_DIR, "models.json");
9
- const MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
10
- function loadCachedModels() {
11
- try {
12
- const raw = JSON.parse(readFileSync(MODELS_CACHE_PATH, "utf8"));
13
- if (raw?.version === 1 && raw.models && Object.keys(raw.models).length > 0)
14
- return raw;
15
- }
16
- catch {
17
- // no cache yet, or unreadable — fall back
18
- }
19
- return undefined;
20
- }
21
- // Live model list from Cursor, cached 24h to keep opencode startup fast.
22
- // Offline/logged-out: stale cache if present, else just "auto" (Cursor's universal alias).
23
- async function currentModels() {
24
- const cached = loadCachedModels();
25
- if (cached && Date.now() - cached.fetchedAt <= MODELS_CACHE_TTL_MS)
26
- return cached.models;
27
- try {
28
- const list = await Cursor.models.list();
29
- const models = {};
30
- for (const m of list)
31
- models[m.id === "default" ? "auto" : m.id] = m.displayName ?? m.id;
32
- mkdirSync(CACHE_DIR, { recursive: true });
33
- writeFileSync(MODELS_CACHE_PATH, JSON.stringify({ version: 1, fetchedAt: Date.now(), models }));
34
- return models;
35
- }
36
- catch {
37
- return cached?.models ?? { auto: "Auto" };
38
- }
39
- }
40
- async function bodyText(input, init) {
41
- const b = init?.body;
42
- if (typeof b === "string")
43
- return b;
44
- if (b instanceof Uint8Array)
45
- return new TextDecoder().decode(b);
46
- if (input instanceof Request)
47
- return input.text();
48
- return "";
49
- }
50
- let pastedApiKey;
51
- async function cursorFetch(input, init) {
52
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
53
- if (url.includes("/chat/completions")) {
54
- const body = JSON.parse(await bodyText(input, init));
55
- return handleChatCompletion(body, pastedApiKey);
56
- }
57
- if (url.includes("/models")) {
58
- return Response.json({
59
- object: "list",
60
- data: Object.keys(await currentModels()).map((id) => ({ id, object: "model" })),
61
- });
62
- }
63
- return Response.json({ error: { message: `unsupported endpoint: ${url}` } }, { status: 404 });
64
- }
65
- export const CursorAuthPlugin = async () => {
66
- return {
67
- config: async (config) => {
68
- const providers = (config.provider ??= {});
69
- const cursor = (providers["cursor"] ??= { models: {} });
70
- cursor.npm ??= "@ai-sdk/openai-compatible";
71
- cursor.name ??= "Cursor";
72
- cursor.options = { ...cursor.options, baseURL: BASE_URL };
73
- cursor.models ??= {};
74
- for (const [id, displayName] of Object.entries(await currentModels())) {
75
- cursor.models[id] ??= { name: displayName };
76
- const model = cursor.models[id];
77
- model.cost = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
78
- }
79
- },
80
- auth: {
81
- provider: "cursor",
82
- loader: async (getAuth) => {
83
- const auth = await getAuth().catch(() => undefined);
84
- if (auth && auth.type === "api" && auth.key !== "cursor-sdk-store")
85
- pastedApiKey = auth.key;
86
- return {
87
- apiKey: "cursor-managed",
88
- baseURL: BASE_URL,
89
- fetch: cursorFetch,
90
- };
91
- },
92
- methods: [
93
- {
94
- type: "oauth",
95
- label: "Log in with Cursor (browser)",
96
- authorize: async () => {
97
- let resolveUrl;
98
- const urlReady = new Promise((r) => (resolveUrl = r));
99
- const login = Cursor.auth
100
- .login({
101
- apiKeyName: "opencode-cursor-auth",
102
- openBrowser: false,
103
- onLoginUrl: (url) => resolveUrl(url),
104
- })
105
- .catch((err) => err);
106
- const url = await Promise.race([urlReady, login.then(() => "")]);
107
- return {
108
- url: typeof url === "string" && url ? url : "https://cursor.com/dashboard",
109
- instructions: "Complete the login in your browser. The key is stored in ~/.cursor/sdk/auth.json.",
110
- method: "auto",
111
- callback: async () => {
112
- const result = await login;
113
- if (result instanceof Error)
114
- return { type: "failed" };
115
- return { type: "success", key: "cursor-sdk-store" };
116
- },
117
- };
118
- },
119
- },
120
- {
121
- type: "api",
122
- label: "Paste a Cursor API key (cursor.com/dashboard)",
123
- },
124
- ],
125
- },
126
- };
127
- };
128
- export default CursorAuthPlugin;
1
+ // opencode calls EVERY named export of a plugin module as a plugin, so this
2
+ // entry must expose exactly one export; helpers live in plugin.ts.
3
+ export { CursorAuthPlugin as default } from "./plugin.js";
@@ -0,0 +1,20 @@
1
+ import { type ModelListItem, type ModelParameterValue, type ModelSelection } from "@cursor/sdk";
2
+ type Variant = {
3
+ params: ModelParameterValue[];
4
+ context?: number;
5
+ };
6
+ export type ModelInfo = {
7
+ name: string;
8
+ context?: number;
9
+ params?: ModelParameterValue[];
10
+ variants?: Record<string, Variant>;
11
+ allowed?: Record<string, string[]>;
12
+ blocked?: true;
13
+ };
14
+ export declare function parseContext(m: ModelListItem): number | undefined;
15
+ export declare function buildCatalog(list: ModelListItem[]): Record<string, ModelInfo>;
16
+ export declare function currentModels(): Promise<Record<string, ModelInfo>>;
17
+ export declare function resolveModel(opencodeId: string): ModelSelection | undefined;
18
+ export declare function resolveVariant(opencodeId: string, params?: unknown): ModelSelection | string | undefined;
19
+ export declare function markBlocked(opencodeId: string): void;
20
+ export {};
package/dist/models.js ADDED
@@ -0,0 +1,134 @@
1
+ import { Cursor } from "@cursor/sdk";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ const CACHE_DIR = join(homedir(), ".cache", "opencode-cursor-auth");
6
+ const MODELS_CACHE_PATH = join(CACHE_DIR, "models.json");
7
+ const MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
8
+ let catalog = {};
9
+ let fetchedAt = 0;
10
+ function saveCatalog() {
11
+ mkdirSync(CACHE_DIR, { recursive: true });
12
+ writeFileSync(MODELS_CACHE_PATH, JSON.stringify({ version: 4, fetchedAt, models: catalog }));
13
+ }
14
+ function contextValue(raw) {
15
+ const match = raw?.trim().match(/^(\d+)([km]?)$/i);
16
+ if (!match)
17
+ return undefined;
18
+ const unit = match[2].toLowerCase();
19
+ return Number(match[1]) * (unit === "k" ? 1_000 : unit === "m" ? 1_000_000 : 1);
20
+ }
21
+ export function parseContext(m) {
22
+ return contextValue(m.parameters?.find((p) => p.id === "context")?.values[0]?.value);
23
+ }
24
+ export function buildCatalog(list) {
25
+ const models = {};
26
+ for (const m of list) {
27
+ const def = m.variants?.find((v) => v.isDefault) ?? m.variants?.[0];
28
+ const ctxOf = (ps) => contextValue(ps?.find((p) => p.id === "context")?.value);
29
+ const context = ctxOf(def?.params) ?? parseContext(m);
30
+ const allowed = {};
31
+ const allow = (id, value) => {
32
+ if (!(allowed[id] ??= []).includes(value))
33
+ allowed[id].push(value);
34
+ };
35
+ for (const p of m.parameters ?? [])
36
+ for (const v of p.values)
37
+ allow(p.id, v.value);
38
+ const variants = {};
39
+ for (const v of m.variants ?? []) {
40
+ for (const p of v.params)
41
+ allow(p.id, p.value);
42
+ const name = v.params
43
+ .filter((p) => def?.params.find((d) => d.id === p.id)?.value !== p.value)
44
+ .map((p) => (p.value === "true" ? p.id : p.value === "false" ? `no-${p.id}` : p.value))
45
+ .join("-");
46
+ if (!name || variants[name])
47
+ continue;
48
+ const vctx = ctxOf(v.params) ?? context;
49
+ variants[name] = { params: v.params, ...(vctx ? { context: vctx } : {}) };
50
+ }
51
+ models[m.id === "default" ? "auto" : m.id] = {
52
+ name: m.displayName ?? m.id,
53
+ ...(context ? { context } : {}),
54
+ ...(def ? { params: def.params, allowed } : {}),
55
+ ...(Object.keys(variants).length ? { variants } : {}),
56
+ };
57
+ }
58
+ return models;
59
+ }
60
+ function loadCachedModels() {
61
+ try {
62
+ const raw = JSON.parse(readFileSync(MODELS_CACHE_PATH, "utf8"));
63
+ if (raw?.version === 4 && raw.models && Object.keys(raw.models).length > 0)
64
+ return raw;
65
+ }
66
+ catch {
67
+ // no cache yet, or unreadable — fall back
68
+ }
69
+ return undefined;
70
+ }
71
+ // Live model list from Cursor, cached 24h to keep opencode startup fast.
72
+ // Offline/logged-out: stale cache if present, else just "auto" (Cursor's universal alias).
73
+ // Models Cursor has rejected with "Model Blocked" for this account are hidden
74
+ // until the cache file is deleted (the catalog itself carries no entitlement info).
75
+ export async function currentModels() {
76
+ const cached = loadCachedModels();
77
+ if (cached && Date.now() - cached.fetchedAt <= MODELS_CACHE_TTL_MS) {
78
+ catalog = cached.models;
79
+ fetchedAt = cached.fetchedAt;
80
+ }
81
+ else {
82
+ try {
83
+ catalog = buildCatalog(await Cursor.models.list());
84
+ for (const [id, m] of Object.entries(cached?.models ?? {}))
85
+ if (m.blocked && catalog[id])
86
+ catalog[id].blocked = true;
87
+ fetchedAt = Date.now();
88
+ saveCatalog();
89
+ }
90
+ catch {
91
+ catalog = cached?.models ?? { auto: { name: "Auto" } };
92
+ fetchedAt = cached?.fetchedAt ?? 0;
93
+ }
94
+ }
95
+ return Object.fromEntries(Object.entries(catalog).filter(([, m]) => !m.blocked));
96
+ }
97
+ function loadCatalog() {
98
+ if (Object.keys(catalog).length)
99
+ return;
100
+ const cached = loadCachedModels();
101
+ catalog = cached?.models ?? {};
102
+ fetchedAt = cached?.fetchedAt ?? 0;
103
+ }
104
+ export function resolveModel(opencodeId) {
105
+ loadCatalog();
106
+ const m = catalog[opencodeId];
107
+ if (!m)
108
+ return undefined;
109
+ return m.params ? { id: opencodeId, params: m.params } : { id: opencodeId };
110
+ }
111
+ export function resolveVariant(opencodeId, params) {
112
+ if (!Array.isArray(params))
113
+ return resolveModel(opencodeId);
114
+ loadCatalog();
115
+ const allowed = catalog[opencodeId]?.allowed ?? {};
116
+ for (const p of params) {
117
+ if (!allowed[p?.id]?.includes(p?.value))
118
+ return `unknown variant param ${p?.id}=${p?.value} for model ${opencodeId}`;
119
+ }
120
+ return { id: opencodeId, params: params.map((p) => ({ id: p.id, value: p.value })) };
121
+ }
122
+ export function markBlocked(opencodeId) {
123
+ loadCatalog();
124
+ const m = catalog[opencodeId];
125
+ if (!m || m.blocked)
126
+ return;
127
+ m.blocked = true;
128
+ try {
129
+ saveCatalog();
130
+ }
131
+ catch {
132
+ // cache dir unwritable — the in-memory flag still hides it for this process
133
+ }
134
+ }
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { type SdkAuthStatus } from "@cursor/sdk";
3
+ export { parseContext } from "./models.js";
4
+ export declare function keyProblem(status: SdkAuthStatus, nowMs: number): string | undefined;
5
+ export declare const CursorAuthPlugin: Plugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,139 @@
1
+ import { Cursor } from "@cursor/sdk";
2
+ import { errorResponse, handleChatCompletion } from "./bridge.js";
3
+ import { currentModels } from "./models.js";
4
+ export { parseContext } from "./models.js";
5
+ const BASE_URL = "http://cursor.local/v1";
6
+ // Cursor publishes no output limit and the bridge never passes max_tokens to Cursor, so output is only an accounting reserve for opencode.
7
+ const DEFAULT_CONTEXT = 200_000;
8
+ const DEFAULT_OUTPUT = 32_000;
9
+ export function keyProblem(status, nowMs) {
10
+ if (status.status !== "logged-in")
11
+ return "Cursor SDK key missing — run `opencode auth login` and pick Cursor";
12
+ if (typeof status.apiKeyExpiresAtMs === "number" && status.apiKeyExpiresAtMs <= nowMs)
13
+ return `Cursor SDK key expired on ${new Date(status.apiKeyExpiresAtMs).toISOString()} — run \`opencode auth login\` and pick Cursor`;
14
+ return undefined;
15
+ }
16
+ const KEY_CHECK_TTL_MS = 60_000;
17
+ const EXPIRY_WARN_MS = 7 * 24 * 60 * 60 * 1000;
18
+ let keyStatus;
19
+ let warnedExpiry = false;
20
+ async function checkKey() {
21
+ if (pastedApiKey || process.env.CURSOR_API_KEY)
22
+ return undefined;
23
+ const now = Date.now();
24
+ if (!keyStatus || now - keyStatus.at > KEY_CHECK_TTL_MS) {
25
+ keyStatus = { at: now, value: Cursor.auth.status().catch(() => undefined) };
26
+ }
27
+ const status = await keyStatus.value;
28
+ if (!status)
29
+ return undefined;
30
+ const exp = status.status === "logged-in" ? status.apiKeyExpiresAtMs : undefined;
31
+ if (!warnedExpiry && typeof exp === "number" && exp > now && exp - now <= EXPIRY_WARN_MS) {
32
+ warnedExpiry = true;
33
+ const days = Math.ceil((exp - now) / (24 * 60 * 60 * 1000));
34
+ console.error(`[opencode-cursor-auth] Cursor SDK key expires in ${days} days — run \`opencode auth login\` to renew`);
35
+ }
36
+ return keyProblem(status, now);
37
+ }
38
+ async function bodyText(input, init) {
39
+ const b = init?.body;
40
+ if (typeof b === "string")
41
+ return b;
42
+ if (b instanceof Uint8Array)
43
+ return new TextDecoder().decode(b);
44
+ if (input instanceof Request)
45
+ return input.text();
46
+ return "";
47
+ }
48
+ let pastedApiKey;
49
+ async function cursorFetch(input, init) {
50
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
51
+ if (url.includes("/chat/completions")) {
52
+ const problem = await checkKey();
53
+ if (problem)
54
+ return errorResponse(problem, 401);
55
+ const body = JSON.parse(await bodyText(input, init));
56
+ const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
57
+ const scope = new Headers(init?.headers).get("x-opencode-session") ??
58
+ (input instanceof Request ? input.headers.get("x-opencode-session") : null) ??
59
+ undefined;
60
+ return handleChatCompletion(body, pastedApiKey, signal, scope);
61
+ }
62
+ if (url.includes("/models")) {
63
+ return Response.json({
64
+ object: "list",
65
+ data: Object.keys(await currentModels()).map((id) => ({ id, object: "model" })),
66
+ });
67
+ }
68
+ return Response.json({ error: { message: `unsupported endpoint: ${url}` } }, { status: 404 });
69
+ }
70
+ export const CursorAuthPlugin = async () => {
71
+ return {
72
+ config: async (config) => {
73
+ const providers = (config.provider ??= {});
74
+ const cursor = (providers["cursor"] ??= { models: {} });
75
+ cursor.npm ??= "@ai-sdk/openai-compatible";
76
+ cursor.name ??= "Cursor";
77
+ cursor.options = { ...cursor.options, baseURL: BASE_URL };
78
+ cursor.models ??= {};
79
+ for (const [id, { name, context, variants }] of Object.entries(await currentModels())) {
80
+ cursor.models[id] ??= { name };
81
+ const model = cursor.models[id];
82
+ model.cost = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
83
+ model.limit ??= { context: context ?? DEFAULT_CONTEXT, output: DEFAULT_OUTPUT };
84
+ if (variants)
85
+ model.variants ??= Object.fromEntries(Object.entries(variants).map(([n, v]) => [n, { cursor_params: v.params }]));
86
+ }
87
+ },
88
+ "chat.headers": async (input, output) => {
89
+ if (input.model.providerID === "cursor")
90
+ output.headers["x-opencode-session"] = input.sessionID;
91
+ },
92
+ auth: {
93
+ provider: "cursor",
94
+ loader: async (getAuth) => {
95
+ const auth = await getAuth().catch(() => undefined);
96
+ if (auth && auth.type === "api" && auth.key !== "cursor-sdk-store")
97
+ pastedApiKey = auth.key;
98
+ return {
99
+ apiKey: "cursor-managed",
100
+ baseURL: BASE_URL,
101
+ fetch: cursorFetch,
102
+ };
103
+ },
104
+ methods: [
105
+ {
106
+ type: "oauth",
107
+ label: "Log in with Cursor (browser)",
108
+ authorize: async () => {
109
+ let resolveUrl;
110
+ const urlReady = new Promise((r) => (resolveUrl = r));
111
+ const login = Cursor.auth
112
+ .login({
113
+ apiKeyName: "opencode-cursor-auth",
114
+ openBrowser: false,
115
+ onLoginUrl: (url) => resolveUrl(url),
116
+ })
117
+ .catch((err) => err);
118
+ const url = await Promise.race([urlReady, login.then(() => "")]);
119
+ return {
120
+ url: typeof url === "string" && url ? url : "https://cursor.com/dashboard",
121
+ instructions: "Complete the login in your browser. The key is stored in ~/.cursor/sdk/auth.json.",
122
+ method: "auto",
123
+ callback: async () => {
124
+ const result = await login;
125
+ if (result instanceof Error)
126
+ return { type: "failed" };
127
+ return { type: "success", key: "cursor-sdk-store" };
128
+ },
129
+ };
130
+ },
131
+ },
132
+ {
133
+ type: "api",
134
+ label: "Paste a Cursor API key (cursor.com/dashboard)",
135
+ },
136
+ ],
137
+ },
138
+ };
139
+ };
@@ -0,0 +1,139 @@
1
+ # Install @vidit19sharma/opencode-cursor-auth
2
+
3
+ These instructions are designed for AI coding agents.
4
+
5
+ The plugin lets opencode use a Cursor subscription through the official `@cursor/sdk`, in-process. Supported on macOS/Linux; Windows untested.
6
+
7
+ ## Prerequisites
8
+
9
+ ### Check opencode version
10
+
11
+ ```bash
12
+ opencode --version
13
+ ```
14
+
15
+ You should see `1.19` or newer. If older or missing, ask the user to install or upgrade opencode before continuing.
16
+
17
+ ### Check Node version
18
+
19
+ ```bash
20
+ node --version
21
+ ```
22
+
23
+ You should see `v22.13` or newer. opencode installs plugins with Bun, but `@cursor/sdk` spawns the `cursor-agent` binary and requires Node >= 22.13. If older, ask the user to upgrade Node.
24
+
25
+ ### Check the Cursor CLI
26
+
27
+ ```bash
28
+ cursor-agent --version
29
+ ```
30
+
31
+ If this prints a version, continue. If it fails with `command not found`, install it:
32
+
33
+ ```bash
34
+ curl https://cursor.com/install -fsS | bash
35
+ ```
36
+
37
+ Then open a new shell, or add `~/.local/bin` to `PATH`, and re-run `cursor-agent --version`.
38
+
39
+ A Cursor subscription (Pro, Business, or team) is required. `cursor-agent login` is not required (the plugin has its own login) but is harmless.
40
+
41
+ ## Installation
42
+
43
+ ### Step 1: Remove the old open-cursor setup (only if present)
44
+
45
+ If `~/.config/opencode/plugin/cursor-acp.js` exists or the opencode config has a `cursor-acp` provider, the user has the old `@rama_nigg/open-cursor` proxy setup. Remove it:
46
+
47
+ 1. Delete the `cursor-acp` block under `provider` in the opencode config.
48
+ 2. Delete the file: `rm ~/.config/opencode/plugin/cursor-acp.js`
49
+ 3. Uninstall it: `npm uninstall -g @rama_nigg/open-cursor`
50
+
51
+ Skip this step if none of these exist.
52
+
53
+ ### Step 2: Add the plugin to the opencode config
54
+
55
+ The config file is `~/.config/opencode/opencode.json`. Some users have `~/.config/opencode/opencode.jsonc` instead; edit whichever exists.
56
+
57
+ If neither exists, create `~/.config/opencode/opencode.json`:
58
+
59
+ ```json
60
+ {
61
+ "$schema": "https://opencode.ai/config.json",
62
+ "plugin": ["@vidit19sharma/opencode-cursor-auth@latest"]
63
+ }
64
+ ```
65
+
66
+ If the file exists and has no top-level `plugin` array, add one:
67
+
68
+ ```json
69
+ "plugin": ["@vidit19sharma/opencode-cursor-auth@latest"]
70
+ ```
71
+
72
+ If a `plugin` array already exists, append `"@vidit19sharma/opencode-cursor-auth@latest"` and keep every existing entry.
73
+
74
+ Do not add a `provider.cursor` block by hand. On startup the plugin registers the `cursor` provider and its model list automatically. No `npm install` is needed; opencode installs the package at startup.
75
+
76
+ ### Step 3: Authenticate
77
+
78
+ Pick one method.
79
+
80
+ **Browser login.** This step needs the user. Ask them to run:
81
+
82
+ ```bash
83
+ opencode auth login
84
+ ```
85
+
86
+ and choose provider **Cursor**, then **Log in with Cursor (browser)**. A browser opens; they sign in to Cursor. This mints a 90-day SDK key stored at `~/.cursor/sdk/auth.json` (written by `@cursor/sdk`, not by the plugin). When the key expires, requests return a 401 telling the user to run `opencode auth login` again; a warning is printed to stderr during the last 7 days.
87
+
88
+ **API key.** In `opencode auth login`, choose **Cursor**, then **Paste a Cursor API key**. Keys come from cursor.com/dashboard (Integrations / API keys). Alternatively, set `CURSOR_API_KEY` in the environment before starting opencode.
89
+
90
+ ## Verification
91
+
92
+ ```bash
93
+ opencode models | grep cursor/
94
+ ```
95
+
96
+ You should see `cursor/auto`, `cursor/gpt-5.5`, `cursor/claude-opus-5-5`, and more. The first run fetches the catalog from Cursor (about 3 s); it is cached for 24 h at `~/.cache/opencode-cursor-auth/models.json`.
97
+
98
+ Variants are not separate ids. In the TUI, press Ctrl+T on a cursor model to pick its effort, thinking, fast, or context variant.
99
+
100
+ ```bash
101
+ opencode run -m cursor/gpt-5.5 "Reply with exactly: CURSOR-AUTH-OK"
102
+ ```
103
+
104
+ You should see `CURSOR-AUTH-OK`. In some setups `opencode run` does not exit on its own because of other plugins; Ctrl-C after the reply is fine.
105
+
106
+ Optional: to see which models the account is actually allowed to use, clone the repo, run `bun install`, then `bun spike/probe-models.ts`. It sends one tiny request per model.
107
+
108
+ ## Upgrading
109
+
110
+ With `@latest` in the `plugin` array, restarting opencode picks up new versions. If it stays on an old version, clear the cached package and restart opencode:
111
+
112
+ ```bash
113
+ rm -rf ~/.cache/opencode/packages/@vidit19sharma/opencode-cursor-auth@latest
114
+ ```
115
+
116
+ ## Done
117
+
118
+ The plugin is installed. Every model the user's Cursor plan includes is available as `cursor/*` in opencode.
119
+
120
+ ## Troubleshooting
121
+
122
+ - **`Cannot connect to API`, or nothing happens on cursor models**: the auth entry is missing. Run `opencode auth login` and pick Cursor.
123
+ - **401 `Cursor SDK key missing/expired`**: run `opencode auth login`.
124
+ - **403 `permission_error` "Model Blocked"**: a team admin disabled that model for the account. It is hidden from `opencode models` from then on; delete `~/.cache/opencode-cursor-auth/models.json` to re-check.
125
+ - **400 `Cannot use this model`**: the model id is not in Cursor's catalog for this account.
126
+ - **`cursor-agent: command not found` at request time**: install the Cursor CLI (see Prerequisites) and make sure it is on `PATH` for the shell that launches opencode.
127
+ - **Model list empty or only `cursor/auto`**: the catalog fetch failed (offline or not logged in). Fix auth, delete `~/.cache/opencode-cursor-auth/models.json`, restart opencode.
128
+ - **Stray `cursor-agent` processes**: check with `pgrep -fl cursor-agent`. A single `worker-server` is the SDK's idle singleton and is normal.
129
+
130
+ ### Paths
131
+
132
+ | Path | Contents |
133
+ | --- | --- |
134
+ | `~/.cursor/sdk/auth.json` | SDK key from browser login |
135
+ | `~/.cache/opencode-cursor-auth/models.json` | Model catalog, 24 h |
136
+ | `~/.cache/opencode-cursor-auth/sessions.json` | Conversation to Cursor agent map, 7 days |
137
+ | `~/.local/share/opencode/auth.json` | opencode's auth registry; the Cursor entry is a sentinel, the real key lives in the SDK file |
138
+
139
+ See the [README](README.md) for more detail.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidit19sharma/opencode-cursor-auth",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenCode plugin that uses your Cursor subscription via the official @cursor/sdk \u2014 no proxy server, no separate login.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,8 @@
8
8
  "files": [
9
9
  "dist",
10
10
  "README.md",
11
+ "installation.md",
12
+ "CHANGELOG.md",
11
13
  "LICENSE"
12
14
  ],
13
15
  "author": "Vidit Sharma <vidit19sharma@gmail.com> (https://github.com/vidit19sharma)",