@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 +14 -0
- package/README.md +12 -0
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +53 -13
- package/dist/cli.js +49 -4
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +8 -2
- package/installation.md +8 -1
- package/package.json +4 -3
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
|
|
19
|
-
// console
|
|
20
|
-
// the
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "OpenCode plugin that uses your Cursor subscription via the official @cursor/sdk
|
|
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"
|