@magpie-community/opencode-cursor-auth 0.2.17 → 0.2.19
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/README.md +7 -5
- package/index.mjs +53 -13
- package/native-session.mjs +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,11 +13,13 @@ An account is a Cursor access token. There are three ways to get one:
|
|
|
13
13
|
plugin polls `api2.cursor.sh/auth/poll` until the browser has signed in.
|
|
14
14
|
The account is named by its email (`DashboardService/GetMe`).
|
|
15
15
|
- **cursor-agent's sign-in**: takes the account `cursor-agent login`
|
|
16
|
-
signed in to. Its token is read
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
signed in to. Its token is read from where the CLI keeps it: the macOS
|
|
17
|
+
keychain (`cursor-access-token`), else its `auth.json` (`~/.cursor/`,
|
|
18
|
+
`$XDG_CONFIG_HOME/cursor/` or `%APPDATA%\Cursor\`). A keychain read is
|
|
19
|
+
kept for 10 seconds, and one the keychain refused (the user said no to
|
|
20
|
+
its dialog) is not asked again for 10 minutes; signing in reads it
|
|
21
|
+
afresh. The plugin never writes it; when it has less than 5 minutes
|
|
22
|
+
left, `cursor-agent status` is run, which renews it.
|
|
21
23
|
- **Cursor API key**: a key from cursor.com/dashboard → Integrations. It is
|
|
22
24
|
exchanged for a token at `/auth/exchange_user_api_key`, as the CLI does
|
|
23
25
|
with `CURSOR_API_KEY`, and exchanged again when the token runs out.
|
package/index.mjs
CHANGED
|
@@ -63,7 +63,7 @@ const exists = (p) => stat(p).then((s) => !s.isDirectory(), () => false)
|
|
|
63
63
|
function run(file, args, { timeout = 10_000, env } = {}) {
|
|
64
64
|
return new Promise((resolve) => {
|
|
65
65
|
execFile(file, args, { timeout, env: env ?? process.env, maxBuffer: 8 << 20 }, (err, stdout) =>
|
|
66
|
-
resolve({ ok: !err, out: String(stdout ?? "") }))
|
|
66
|
+
resolve({ ok: !err, out: String(stdout ?? ""), code: typeof err?.code === "number" ? err.code : err ? -1 : 0 }))
|
|
67
67
|
})
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -117,12 +117,48 @@ function authFile() {
|
|
|
117
117
|
return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "cursor", "auth.json")
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// TOKEN_TTL is how long a token read from the Keychain is taken as it: a
|
|
121
|
+
// sign-in reads it afresh, one made in cursor-agent is seen within it.
|
|
122
|
+
// KEYCHAIN_DENIED is how long a read the user may have been asked about,
|
|
123
|
+
// and answered no to, is not asked again: with the item's trust list
|
|
124
|
+
// holding no `security`, every ask can show the dialog again. Both as the
|
|
125
|
+
// built-in's cursorKeychainToken (magpie #746, #2408).
|
|
126
|
+
const TOKEN_TTL = 10_000
|
|
127
|
+
const KEYCHAIN_DENIED = 10 * 60_000
|
|
128
|
+
const keychain = { tok: "", at: 0, ask: 0, busy: null, now: () => Date.now(), read: (args) => run("security", args) }
|
|
129
|
+
|
|
130
|
+
// keychainToken is cursor-agent's token in the Keychain, "" for none; fresh
|
|
131
|
+
// reads it whatever was read last. Reads go one at a time, so callers at
|
|
132
|
+
// once share one `security` run (and one dialog).
|
|
133
|
+
function keychainToken(fresh = false) {
|
|
134
|
+
const go = async () => {
|
|
135
|
+
const now = keychain.now()
|
|
136
|
+
const ttl = !keychain.tok && now - keychain.ask < KEYCHAIN_DENIED ? KEYCHAIN_DENIED : TOKEN_TTL
|
|
137
|
+
if (!fresh && now - keychain.at < ttl) return keychain.tok
|
|
138
|
+
const r = await keychain.read(["find-generic-password", "-s", "cursor-access-token", "-a", "cursor-user", "-w"])
|
|
139
|
+
keychain.tok = ""
|
|
140
|
+
keychain.at = keychain.now()
|
|
141
|
+
if (r.ok) {
|
|
142
|
+
keychain.tok = r.out.trim()
|
|
143
|
+
keychain.ask = 0
|
|
144
|
+
} else if (r.code !== 44) {
|
|
145
|
+
// 44 is "no such item"; anything else may have been a dialog the
|
|
146
|
+
// user said no to, or a keychain that refused
|
|
147
|
+
keychain.ask = keychain.at
|
|
148
|
+
} else keychain.ask = 0
|
|
149
|
+
return keychain.tok
|
|
150
|
+
}
|
|
151
|
+
const p = (keychain.busy ?? Promise.resolve()).then(go, go)
|
|
152
|
+
keychain.busy = p.catch(() => {})
|
|
153
|
+
return p
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
// cliToken is the access token cursor-agent signed in with; read, never
|
|
121
|
-
// written.
|
|
122
|
-
async function cliToken() {
|
|
157
|
+
// written. fresh reads the Keychain whatever was read last.
|
|
158
|
+
async function cliToken(fresh = false) {
|
|
123
159
|
if (GOOS === "darwin") {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
160
|
+
const t = await keychainToken(fresh)
|
|
161
|
+
if (t) return t
|
|
126
162
|
}
|
|
127
163
|
try {
|
|
128
164
|
return JSON.parse(await readFile(authFile(), "utf8"))?.accessToken ?? ""
|
|
@@ -163,7 +199,7 @@ async function cliTokenNow() {
|
|
|
163
199
|
if (exe) {
|
|
164
200
|
cliRefresh ??= run(exe, ["status"], { timeout: 30_000 }).finally(() => (cliRefresh = null))
|
|
165
201
|
await cliRefresh
|
|
166
|
-
tok = await cliToken()
|
|
202
|
+
tok = await cliToken(true)
|
|
167
203
|
}
|
|
168
204
|
// the built-in's words (provider.CursorToken)
|
|
169
205
|
if (!tok) throw new AuthError("Cursor isn't signed in; sign in from magpie's Providers page or run `cursor-agent login`")
|
|
@@ -332,7 +368,7 @@ async function cliSignIn() {
|
|
|
332
368
|
instructions: "Uses the account `cursor-agent login` signed in to.",
|
|
333
369
|
method: "auto",
|
|
334
370
|
async callback() {
|
|
335
|
-
const tok = await cliToken()
|
|
371
|
+
const tok = await cliToken(true)
|
|
336
372
|
const exp = expiry(tok)
|
|
337
373
|
if (!tok) return { type: "failed", error: "cursor-agent isn't signed in: run `cursor-agent login`" }
|
|
338
374
|
if (exp && exp <= Date.now()) return { type: "failed", error: "cursor-agent's sign-in has run out: run `cursor-agent login` again" }
|
|
@@ -1209,7 +1245,9 @@ function conversation(chat, tools) {
|
|
|
1209
1245
|
pending.splice(i, 1)
|
|
1210
1246
|
const text = textOf(m.content)
|
|
1211
1247
|
size += text.length
|
|
1212
|
-
|
|
1248
|
+
// a failed tool's result says so, as the built-in's cursorResult
|
|
1249
|
+
// told Cursor isError
|
|
1250
|
+
results.push(result(id, text, m.is_error === true || m.isError === true))
|
|
1213
1251
|
}
|
|
1214
1252
|
continue
|
|
1215
1253
|
}
|
|
@@ -2618,12 +2656,12 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2618
2656
|
let response = native ? await runNative({ ...args, native }) : await runOnce(args)
|
|
2619
2657
|
const rebuild = async () => {
|
|
2620
2658
|
const fallback = await runOnce(args)
|
|
2621
|
-
fallback.diagnostic = "stateless; bootstrap=imported-history; reason=missing-batch-count"
|
|
2659
|
+
fallback.diagnostic = "stateless; bootstrap=imported-history; reason=" + (response.error?.continuityReason || "missing-batch-count")
|
|
2622
2660
|
return fallback
|
|
2623
2661
|
}
|
|
2624
|
-
// A missing native batch count may be retried once,
|
|
2625
|
-
// output is exposed or any native result was submitted.
|
|
2626
|
-
// uncertain tool
|
|
2662
|
+
// A missing or conflicting native batch count may be retried once,
|
|
2663
|
+
// only before Chat output is exposed or any native result was submitted.
|
|
2664
|
+
// Never repeat an uncertain tool write or progressive stream output.
|
|
2627
2665
|
if (response.error?.retentionIneligible && !signal?.aborted) response = await rebuild()
|
|
2628
2666
|
else if (native) response.rebuild = rebuild
|
|
2629
2667
|
response.diagnostic ??= "stateless; bootstrap=imported-history; reason=" + (prepared.fallback || "retention capacity or reservation unavailable")
|
|
@@ -2681,8 +2719,10 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2681
2719
|
for await (const e of events) {
|
|
2682
2720
|
if (e.error) {
|
|
2683
2721
|
if (!e.error.retentionIneligible || !r.rebuild || signal?.aborted) return errorResponse(e.error)
|
|
2722
|
+
const reason = e.error.continuityReason
|
|
2684
2723
|
r = await r.rebuild()
|
|
2685
2724
|
if (r.error) return errorResponse(r.error)
|
|
2725
|
+
if (reason) r.diagnostic = "stateless; bootstrap=imported-history; reason=" + reason
|
|
2686
2726
|
events = r.events
|
|
2687
2727
|
responseHeaders["X-Magpie-Cursor-Continuity"] = r.diagnostic
|
|
2688
2728
|
retry = true
|
|
@@ -2828,4 +2868,4 @@ export async function CursorAuthPlugin() {
|
|
|
2828
2868
|
}
|
|
2829
2869
|
|
|
2830
2870
|
// for tests
|
|
2831
|
-
export const _internal = { nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
|
|
2871
|
+
export const _internal = { keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
|
package/native-session.mjs
CHANGED
|
@@ -675,7 +675,13 @@ class NativeSession {
|
|
|
675
675
|
}
|
|
676
676
|
listed(count) {
|
|
677
677
|
if (!Number.isSafeInteger(count) || count < 1) invalid("invalid native tool count", 502)
|
|
678
|
-
if (this.segment.expected != null && this.segment.expected !== count)
|
|
678
|
+
if (this.segment.expected != null && this.segment.expected !== count) {
|
|
679
|
+
// Conflicting counts leave the native step boundary ambiguous. Retire
|
|
680
|
+
// this owner; Chat may rebuild from history only if no native result
|
|
681
|
+
// was submitted to this Run.
|
|
682
|
+
this.fail({ status: 502, message: "inconsistent native tool count", retentionIneligible: !this.resultSubmitted, continuityReason: "inconsistent-batch-count" })
|
|
683
|
+
return
|
|
684
|
+
}
|
|
679
685
|
this.segment.expected = count
|
|
680
686
|
if (this.state === "awaiting-results") return // an identical retransmission
|
|
681
687
|
this.waitForBatch()
|
|
@@ -876,7 +882,10 @@ class NativeSession {
|
|
|
876
882
|
} else if (u.num === 14) {
|
|
877
883
|
if (!this.segment.content && !this.segment.calls.length) invalid("an empty reply (Cursor ended the turn without text or a tool call)", 502)
|
|
878
884
|
this.terminalSeen(uf)
|
|
879
|
-
} else if (u.num === 27)
|
|
885
|
+
} else if (u.num === 27) {
|
|
886
|
+
this.listed(num(uf, 1))
|
|
887
|
+
if (this.error) return
|
|
888
|
+
}
|
|
880
889
|
}
|
|
881
890
|
break
|
|
882
891
|
case 2: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"description": "Cursor subscriptions (Pro, Pro+, Ultra, Teams) in OpenCode and magpie, through the API cursor-agent talks to",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|