@magpie-community/opencode-cursor-auth 0.2.17 → 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 +7 -5
- package/index.mjs +47 -9
- 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
|
}
|
|
@@ -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/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",
|