@magpie-community/opencode-cursor-auth 0.2.16 → 0.2.18
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 +11 -6
- package/index.mjs +47 -9
- package/native-session.mjs +16 -1
- 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.
|
|
@@ -94,7 +96,10 @@ Tool results can continue a retained Run in the same plugin process. The owner
|
|
|
94
96
|
uses the same session/cache key and first user message as `conversation_id`, so
|
|
95
97
|
Task subagents sharing their parent's session can run independently. Retention
|
|
96
98
|
waits at most 120 seconds for a result batch and 15 minutes for a Run's total
|
|
97
|
-
lifetime.
|
|
99
|
+
lifetime. A continuation arriving in the last minute of that lifetime rebuilds
|
|
100
|
+
from supplied history before submitting results to the old Run. The absolute
|
|
101
|
+
deadline still applies to an active segment; it is never extended by activity.
|
|
102
|
+
The pool holds at most 8 owners per account and 32 per process, with
|
|
98
103
|
bounded history, blobs, arguments, and pending writes. When pending writes
|
|
99
104
|
reach their bound (a long history asks for its blobs at once, and a slow link
|
|
100
105
|
drains them slowly), the Run waits for the link to drain, and fails only if it
|
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
|
}
|
|
@@ -2828,4 +2866,4 @@ export async function CursorAuthPlugin() {
|
|
|
2828
2866
|
}
|
|
2829
2867
|
|
|
2830
2868
|
// 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 }
|
|
2869
|
+
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
|
@@ -381,6 +381,14 @@ export class NativeSessionPool {
|
|
|
381
381
|
return { owner, batch, replay: true, segment: batch.next }
|
|
382
382
|
}
|
|
383
383
|
if (owner.state !== "awaiting-results" || owner.segment !== batch || !batch.clean) return fallback("owner busy")
|
|
384
|
+
// A retained Run's absolute timer spans all tool roundtrips. Rebuild
|
|
385
|
+
// near its deadline before writing this batch: otherwise a new Chat
|
|
386
|
+
// segment can lose its transport after already exposing output.
|
|
387
|
+
const reserve = Math.min(60_000, owner.limits.lifetime / 10)
|
|
388
|
+
if (owner.expiresAt - Date.now() <= reserve) {
|
|
389
|
+
owner.fail({ status: 410, message: "native Run retired before lifetime deadline" }, "expired")
|
|
390
|
+
return fallback("owner near lifetime deadline")
|
|
391
|
+
}
|
|
384
392
|
return { owner, batch, results, encoded, digest, chat }
|
|
385
393
|
} catch (e) {
|
|
386
394
|
if (!(e instanceof NativeSessionError)) throw e
|
|
@@ -463,7 +471,14 @@ class NativeSession {
|
|
|
463
471
|
this.terminal = false
|
|
464
472
|
this.segment = new Segment(this, prepared.chat)
|
|
465
473
|
this.retain(bytes(prepared.chat))
|
|
466
|
-
this.
|
|
474
|
+
this.expireIn(this.limits.lifetime)
|
|
475
|
+
}
|
|
476
|
+
// expireIn sets the Run's absolute deadline ms from now: from its start,
|
|
477
|
+
// the whole lifetime; a test brings it in after the first answer
|
|
478
|
+
expireIn(ms) {
|
|
479
|
+
clearTimeout(this.lifetimeTimer)
|
|
480
|
+
this.expiresAt = Date.now() + ms
|
|
481
|
+
this.lifetimeTimer = later(() => this.fail({ status: 410, message: "native Run lifetime expired" }, "expired"), ms)
|
|
467
482
|
}
|
|
468
483
|
retain(n) {
|
|
469
484
|
if (this.retainedBytes + n > this.limits.outputBytes) invalid("native output/history retention limit reached", 503)
|
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.18",
|
|
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",
|