@vidit19sharma/opencode-cursor-auth 0.3.2 → 0.3.3

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
4
 
5
+ ## 0.3.3 — 2026-10-16
6
+
7
+ ### Added
8
+
9
+ - `npx @vidit19sharma/opencode-cursor-auth update` — removes opencode's cached copy of the plugin so the next opencode start installs the current release. opencode resolves `@latest` once at first install and never re-checks, so users were silently stuck on the version they first installed (`--dry-run` supported).
10
+ - Mocked plugin unit tests (`bun run test:plugin`) covering key handling, expiry, headless login URL passthrough and auth-entry seeding.
11
+
12
+ ### Fixed
13
+
14
+ - Pressing Esc before the first byte (during agent setup) returned HTTP 502 "aborted"; it now propagates as a proper abort.
15
+ - `@cursor/sdk` INFO/WARN log lines leaked into the opencode TUI: the lines are ANSI-coloured when stdout is a TTY, so the filter never matched there. Colours are stripped before matching and the filter is re-asserted per request.
16
+ - An empty or whitespace API key stored via the paste method silently fell back to the SDK key store; it now returns 401 `invalid_api_key`.
17
+ - A failure after content has already streamed emits a single SSE `error` frame and closes without `[DONE]` (covered by a test).
18
+
5
19
  ## 0.3.2 — 2026-10-16
6
20
 
7
21
  ### Added
package/README.md CHANGED
@@ -85,6 +85,18 @@ opencode run -m cursor/auto "hello"
85
85
 
86
86
  Or pick any `cursor/*` model from the opencode model selector.
87
87
 
88
+ ## Upgrading
89
+
90
+ ```sh
91
+ npx @vidit19sharma/opencode-cursor-auth update
92
+ ```
93
+
94
+ Then restart opencode. opencode pins `@latest` to the version it first installed and never re-checks npm, so this deletes that cached copy and opencode installs the current release on next start. Tell-tale sign you need it: Ctrl+T shows no variants, or features from the changelog are missing. Manual equivalent:
95
+
96
+ ```sh
97
+ rm -rf ~/.cache/opencode/packages/@vidit19sharma/opencode-cursor-auth@latest
98
+ ```
99
+
88
100
  ## Models
89
101
 
90
102
  ```sh
package/dist/bridge.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function ensureQuietConsole(): void;
1
2
  type ContentPart = {
2
3
  type: string;
3
4
  text?: string;
@@ -34,6 +35,7 @@ export declare function withRetry<T>(fn: () => Promise<T>, signal?: AbortSignal,
34
35
  export declare function _setMaxActive(n: number): void;
35
36
  export declare function errorResponse(err: unknown, status?: number): Response;
36
37
  export declare function _resetForTest(): void;
38
+ export declare function _failAfterFirstDelta(message: string | null): void;
37
39
  export declare function _stats(): {
38
40
  held: number;
39
41
  sessions: number;
package/dist/bridge.js CHANGED
@@ -15,16 +15,28 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
15
15
  import { homedir, tmpdir } from "node:os";
16
16
  import { join } from "node:path";
17
17
  import { markBlocked, resolveVariant } from "./models.js";
18
- // @cursor/sdk's internal logger writes "HH:MM:SS.mmm INFO ..." lines via
19
- // console.log unconditionally (no config/env to silence it, verified against
20
- // the esm bundle). Filter exactly those lines, pass everything else through.
21
- const SDK_LOG_LINE = /^\d{2}:\d{2}:\d{2}\.\d{3} (TRACE|DEBUG|INFO|WARN|ERROR)\s/;
22
- const originalConsoleLog = console.log.bind(console);
23
- console.log = (...args) => {
24
- if (typeof args[0] === "string" && SDK_LOG_LINE.test(args[0]))
25
- return;
26
- originalConsoleLog(...args);
27
- };
18
+ // @cursor/sdk has no log-level switch: it prints "HH:MM:SS.mmm INFO ..." through
19
+ // whatever console.* is installed at call time, ANSI-coloured when stdout is a
20
+ // TTY (the opencode TUI). Match after stripping colours; re-wrap per request so
21
+ // a host that replaces console.* after plugin load cannot bypass the filter.
22
+ const SDK_LOG_LINE = /^\d{2}:\d{2}:\d{2}\.\d{3} (TRACE|DEBUG|INFO|WARN)\s/;
23
+ const ANSI = /\x1b\[[0-9;]*m/g;
24
+ const QUIET = Symbol.for("opencode-cursor-auth.quiet");
25
+ export function ensureQuietConsole() {
26
+ for (const m of ["log", "info", "warn", "debug"]) {
27
+ const current = console[m];
28
+ if (current[QUIET])
29
+ continue;
30
+ const wrapped = (...args) => {
31
+ if (typeof args[0] === "string" && SDK_LOG_LINE.test(args[0].replace(ANSI, "")))
32
+ return;
33
+ current(...args);
34
+ };
35
+ wrapped[QUIET] = true;
36
+ console[m] = wrapped;
37
+ }
38
+ }
39
+ ensureQuietConsole();
28
40
  function textOf(content) {
29
41
  if (typeof content === "string")
30
42
  return content;
@@ -63,6 +75,7 @@ function errorBody(err, status = statusFor(err)) {
63
75
  };
64
76
  }
65
77
  const abortError = () => new DOMException("aborted", "AbortError");
78
+ const isAbort = (err) => err?.name === "AbortError";
66
79
  export function isTransient(err) {
67
80
  const e = (typeof err === "object" && err !== null ? err : {});
68
81
  if (e.name === "AbortError")
@@ -84,7 +97,9 @@ export async function withRetry(fn, signal, baseDelayMs = 1000, retryable = isTr
84
97
  return await fn();
85
98
  }
86
99
  catch (err) {
87
- if (attempt >= 2 || signal?.aborted || !retryable(err))
100
+ if (signal?.aborted || isAbort(err))
101
+ throw abortError();
102
+ if (attempt >= 2 || !retryable(err))
88
103
  throw err;
89
104
  const delay = Math.min(baseDelayMs * 2 ** attempt * (0.75 + Math.random() * 0.5), 30_000);
90
105
  await new Promise((r) => {
@@ -438,6 +453,9 @@ function attachSignal(held, signal) {
438
453
  held.detachSignal = null;
439
454
  if (!signal)
440
455
  return;
456
+ // listeners never fire on an already-aborted signal (abort during Agent.create/resume)
457
+ if (signal.aborted)
458
+ return abortHeld(held);
441
459
  const onAbort = () => abortHeld(held);
442
460
  signal.addEventListener("abort", onAbort, { once: true });
443
461
  held.detachSignal = () => signal.removeEventListener("abort", onAbort);
@@ -465,7 +483,10 @@ function teardownHeld(held) {
465
483
  heldByAgent.delete(held.agent.agentId);
466
484
  }
467
485
  function failHeld(held, err) {
486
+ if (isAbort(err))
487
+ return abortHeld(held);
468
488
  teardownHeld(held);
489
+ held.run?.cancel().catch(() => { });
469
490
  if (statusFor(err) === 403)
470
491
  markBlocked(held.model);
471
492
  if (held.response && !held.opened) {
@@ -544,6 +565,10 @@ export function _resetForTest() {
544
565
  agentByToolCallId.clear();
545
566
  sessionsLoaded = false;
546
567
  }
568
+ let failAfterFirstDelta = null;
569
+ export function _failAfterFirstDelta(message) {
570
+ failAfterFirstDelta = message;
571
+ }
547
572
  export function _stats() {
548
573
  return {
549
574
  held: heldByAgent.size,
@@ -557,6 +582,7 @@ export function _stats() {
557
582
  export async function handleChatCompletion(body, apiKey, signal, scope) {
558
583
  if (signal?.aborted)
559
584
  throw new DOMException("aborted", "AbortError");
585
+ ensureQuietConsole();
560
586
  await loadSessions();
561
587
  const streaming = body.stream !== false;
562
588
  const reply = deferred();
@@ -579,6 +605,8 @@ export async function handleChatCompletion(body, apiKey, signal, scope) {
579
605
  held.response = sse?.response ?? null;
580
606
  held.opened = false;
581
607
  attachSignal(held, signal);
608
+ if (held.done)
609
+ return reply.promise;
582
610
  attachSink(held, sink);
583
611
  for (const m of body.messages) {
584
612
  if (m.role !== "tool" || !m.tool_call_id)
@@ -630,7 +658,7 @@ export async function handleChatCompletion(body, apiKey, signal, scope) {
630
658
  catch (err) {
631
659
  releaseSlot();
632
660
  releaseCwd(cwd);
633
- if (signal?.aborted)
661
+ if (signal?.aborted || isAbort(err))
634
662
  throw abortError();
635
663
  return errorResponse(err);
636
664
  }
@@ -660,6 +688,8 @@ export async function handleChatCompletion(body, apiKey, signal, scope) {
660
688
  const self = held;
661
689
  heldByAgent.set(agent.agentId, held);
662
690
  attachSignal(held, signal);
691
+ if (held.done)
692
+ return reply.promise;
663
693
  attachSink(held, sink);
664
694
  const customTools = {};
665
695
  for (const t of body.tools ?? []) {
@@ -676,7 +706,12 @@ export async function handleChatCompletion(body, apiKey, signal, scope) {
676
706
  },
677
707
  };
678
708
  }
679
- await syncAgentsMd(cwd, body.messages);
709
+ await syncAgentsMd(cwd, body.messages).catch((err) => {
710
+ if (!self.done)
711
+ throw err;
712
+ });
713
+ if (self.done)
714
+ return reply.promise;
680
715
  const prompt = newSuffix
681
716
  .filter((m) => m.role !== "system")
682
717
  .map((m) => (m.role === "user" ? textOf(m.content) : `${m.role.toUpperCase()}: ${textOf(m.content)}`))
@@ -692,6 +727,11 @@ export async function handleChatCompletion(body, apiKey, signal, scope) {
692
727
  openReply(self);
693
728
  self.textAcc += update.text;
694
729
  emit(self, "text", update.text);
730
+ if (failAfterFirstDelta !== null) {
731
+ const message = failAfterFirstDelta;
732
+ failAfterFirstDelta = null;
733
+ failHeld(self, new Error(message));
734
+ }
695
735
  }
696
736
  else if (update.type === "thinking-delta" && update.text) {
697
737
  openReply(self);
package/dist/cli.js CHANGED
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import process from "node:process";
6
6
  const PKG = "@vidit19sharma/opencode-cursor-auth";
7
7
  const ENTRY = `${PKG}@latest`;
8
8
  const HELP = `Usage: npx ${PKG} [setup] [--dry-run] [--config <path>]
9
+ npx ${PKG} update [--dry-run]
9
10
 
10
- Adds "${ENTRY}" to the "plugin" array of your opencode config.
11
+ setup (default): adds "${ENTRY}" to the "plugin" array of your opencode config.
11
12
  Idempotent: does nothing if any ${PKG} entry is already present.
12
13
 
13
- --dry-run print what would change, write nothing
14
+ update: deletes opencode's cached copy of ${PKG} (opencode never re-resolves
15
+ "@latest" once installed), so the next opencode start installs the latest version.
16
+ Cache: $XDG_CACHE_HOME/opencode or ~/.cache/opencode.
17
+
18
+ --dry-run print what would change, write/delete nothing
14
19
  --config <path> edit this file instead of the auto-detected one
15
20
  --help show this help
16
21
 
@@ -36,9 +41,45 @@ function tryParse(s) {
36
41
  return undefined;
37
42
  }
38
43
  }
44
+ // opencode caches npm plugins in <cache>/packages/<name>@<spec>/ (older: <cache>/npm/) and never re-resolves @latest.
45
+ function update(dryRun) {
46
+ const cache = join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "opencode");
47
+ const [scope, name] = PKG.split("/");
48
+ const dirs = ["packages", "npm"].flatMap((p) => {
49
+ const parent = join(cache, p, scope);
50
+ if (!existsSync(parent))
51
+ return [];
52
+ return readdirSync(parent)
53
+ .filter((d) => d.startsWith(`${name}@`))
54
+ .map((d) => join(parent, d));
55
+ });
56
+ if (!dirs.length) {
57
+ console.log(`nothing to update: opencode has no cached copy of ${PKG} (it will install the latest version on next start)`);
58
+ return 0;
59
+ }
60
+ for (const d of dirs) {
61
+ const pkg = tryParse(readFileSafe(join(d, "node_modules", PKG, "package.json")));
62
+ const version = typeof pkg?.version === "string" ? pkg.version : "unknown version";
63
+ if (!dryRun)
64
+ rmSync(d, { recursive: true, force: true });
65
+ console.log(`${dryRun ? "would remove" : "removed"} cached ${version} at ${d}`);
66
+ }
67
+ if (!dryRun)
68
+ console.log(`restart opencode — it will install the latest ${PKG} from npm on start`);
69
+ return 0;
70
+ }
71
+ function readFileSafe(f) {
72
+ try {
73
+ return readFileSync(f, "utf8");
74
+ }
75
+ catch {
76
+ return "";
77
+ }
78
+ }
39
79
  function main(argv) {
40
80
  let dryRun = false;
41
81
  let explicit;
82
+ let cmd = "setup";
42
83
  for (let i = 0; i < argv.length; i++) {
43
84
  const a = argv[i];
44
85
  if (a === "--help" || a === "-h")
@@ -47,11 +88,15 @@ function main(argv) {
47
88
  dryRun = true;
48
89
  else if (a === "--config")
49
90
  explicit = argv[++i];
50
- else if (a !== "setup")
91
+ else if (a === "setup" || a === "update")
92
+ cmd = a;
93
+ else
51
94
  return console.error(`Unknown argument: ${a}\n\n${HELP}`), 1;
52
95
  }
53
96
  if (argv.includes("--config") && !explicit)
54
97
  return console.error("--config needs a path"), 1;
98
+ if (cmd === "update")
99
+ return update(dryRun);
55
100
  const env = process.env;
56
101
  const dir = env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
57
102
  const file = resolve(explicit ||
package/dist/plugin.d.ts CHANGED
@@ -2,4 +2,5 @@ import type { Plugin } from "@opencode-ai/plugin";
2
2
  import { type SdkAuthStatus } from "@cursor/sdk";
3
3
  export { parseContext } from "./models.js";
4
4
  export declare function keyProblem(status: SdkAuthStatus, nowMs: number): string | undefined;
5
+ export declare function _resetKeyCache(): void;
5
6
  export declare const CursorAuthPlugin: Plugin;
package/dist/plugin.js CHANGED
@@ -48,10 +48,17 @@ async function bodyText(input, init) {
48
48
  return input.text();
49
49
  return "";
50
50
  }
51
+ export function _resetKeyCache() {
52
+ keyStatus = undefined;
53
+ warnedExpiry = false;
54
+ }
51
55
  let pastedApiKey;
56
+ const EMPTY_KEY = "Cursor API key is empty — run `opencode auth login` and pick Cursor, or paste a key from cursor.com/dashboard";
52
57
  async function cursorFetch(input, init) {
53
58
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
54
59
  if (url.includes("/chat/completions")) {
60
+ if (pastedApiKey === "")
61
+ return errorResponse({ message: EMPTY_KEY, code: "invalid_api_key" }, 401);
55
62
  const problem = await checkKey();
56
63
  if (problem)
57
64
  return errorResponse(problem, 401);
@@ -106,8 +113,7 @@ export const CursorAuthPlugin = async (input) => {
106
113
  provider: "cursor",
107
114
  loader: async (getAuth) => {
108
115
  const auth = await getAuth().catch(() => undefined);
109
- if (auth && auth.type === "api" && auth.key !== "cursor-sdk-store")
110
- pastedApiKey = auth.key;
116
+ pastedApiKey = auth?.type === "api" && auth.key !== "cursor-sdk-store" ? (auth.key ?? "").trim() : undefined;
111
117
  return {
112
118
  apiKey: "cursor-managed",
113
119
  baseURL: BASE_URL,
package/installation.md CHANGED
@@ -101,7 +101,13 @@ Optional: to see which models the account is actually allowed to use, clone the
101
101
 
102
102
  ## Upgrading
103
103
 
104
- 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:
104
+ opencode resolves `@latest` once, at first install, and keeps that cached version forever; restarting does not upgrade it. To upgrade, run:
105
+
106
+ ```bash
107
+ npx @vidit19sharma/opencode-cursor-auth update
108
+ ```
109
+
110
+ Then tell the user to restart opencode; it installs the current release from npm on start. `update --dry-run` shows what would be removed. Manual equivalent (also delete any other `@vidit19sharma/opencode-cursor-auth@*` dir there, and under `~/.cache/opencode/npm/` on older opencode):
105
111
 
106
112
  ```bash
107
113
  rm -rf ~/.cache/opencode/packages/@vidit19sharma/opencode-cursor-auth@latest
@@ -118,6 +124,7 @@ The plugin is installed. Every model the user's Cursor plan includes is availabl
118
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.
119
125
  - **400 `Cannot use this model`**: the model id is not in Cursor's catalog for this account.
120
126
  - **Request hangs with no error**: `opencode auth login` was never run for Cursor; run it (0.3.2+ seeds the entry automatically so you get a 401 message instead).
127
+ - **Features missing / variants not showing on Ctrl+T**: opencode is running a stale cached version. Run `npx @vidit19sharma/opencode-cursor-auth update`, then restart opencode.
121
128
  - **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.
122
129
 
123
130
  ### Paths
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vidit19sharma/opencode-cursor-auth",
3
- "version": "0.3.2",
4
- "description": "OpenCode plugin that uses your Cursor subscription via the official @cursor/sdk \u2014 no proxy server, no separate login.",
3
+ "version": "0.3.3",
4
+ "description": "OpenCode plugin that uses your Cursor subscription via the official @cursor/sdk — no proxy server, no separate login.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -39,7 +39,8 @@
39
39
  "spike": "bun spike/validate-sdk.ts",
40
40
  "smoke": "bash scripts/fresh-home-smoke.sh",
41
41
  "prepublishOnly": "npm run build",
42
- "test:cli": "npm run build && bash spike/cli-setup.test.sh"
42
+ "test:cli": "npm run build && bash spike/cli-setup.test.sh",
43
+ "test:plugin": "bun test spike/plugin.test.ts"
43
44
  },
44
45
  "dependencies": {
45
46
  "@cursor/sdk": "^1.0.36"