@magpie-community/opencode-cursor-auth 0.1.0 → 0.1.2
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/index.mjs +93 -7
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// OpenCode speaks chat completions to it (@ai-sdk/openai-compatible); the
|
|
26
26
|
// fetch here answers them.
|
|
27
27
|
import http2 from "node:http2"
|
|
28
|
+
import { STATUS_CODES } from "node:http"
|
|
28
29
|
import { gunzipSync } from "node:zlib"
|
|
29
30
|
import { createHash, randomBytes, randomUUID } from "node:crypto"
|
|
30
31
|
import { execFile } from "node:child_process"
|
|
@@ -267,7 +268,7 @@ async function browserSignIn() {
|
|
|
267
268
|
try {
|
|
268
269
|
res = await fetch(`${API}/auth/poll?uuid=${uuid}&verifier=${verifier}`, { headers, signal: AbortSignal.timeout(15_000) })
|
|
269
270
|
} catch {
|
|
270
|
-
if (++errors >= 3) return { type: "failed" }
|
|
271
|
+
if (++errors >= 3) return { type: "failed", error: "couldn't reach Cursor" }
|
|
271
272
|
await sleep(wait)
|
|
272
273
|
continue
|
|
273
274
|
}
|
|
@@ -277,12 +278,12 @@ async function browserSignIn() {
|
|
|
277
278
|
continue
|
|
278
279
|
}
|
|
279
280
|
if (!res.ok) {
|
|
280
|
-
if (res.status === 403 || ++errors >= 3) return { type: "failed" }
|
|
281
|
+
if (res.status === 403 || ++errors >= 3) return { type: "failed", error: res.status === 403 ? "Cursor refused the sign-in (403)" : `Cursor's sign-in answered HTTP ${res.status}` }
|
|
281
282
|
await sleep(wait)
|
|
282
283
|
continue
|
|
283
284
|
}
|
|
284
285
|
const j = await res.json().catch(() => null)
|
|
285
|
-
if (!j?.accessToken || !("refreshToken" in j)) return { type: "failed" }
|
|
286
|
+
if (!j?.accessToken || !("refreshToken" in j)) return { type: "failed", error: "Cursor sent back no token" }
|
|
286
287
|
const who = await whoIs(j.accessToken)
|
|
287
288
|
return {
|
|
288
289
|
type: "success",
|
|
@@ -293,7 +294,7 @@ async function browserSignIn() {
|
|
|
293
294
|
...(who.plan ? { plan: who.plan } : {}),
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
|
-
return { type: "failed" }
|
|
297
|
+
return { type: "failed", error: "the sign-in timed out" }
|
|
297
298
|
},
|
|
298
299
|
}
|
|
299
300
|
}
|
|
@@ -308,9 +309,10 @@ async function cliSignIn() {
|
|
|
308
309
|
async callback() {
|
|
309
310
|
const tok = await cliToken()
|
|
310
311
|
const exp = expiry(tok)
|
|
311
|
-
if (!tok
|
|
312
|
+
if (!tok) return { type: "failed", error: "cursor-agent isn't signed in: run `cursor-agent login`" }
|
|
313
|
+
if (exp && exp <= Date.now()) return { type: "failed", error: "cursor-agent's sign-in has run out: run `cursor-agent login` again" }
|
|
312
314
|
const who = await whoIs(tok)
|
|
313
|
-
if (!who.email) return { type: "failed" }
|
|
315
|
+
if (!who.email) return { type: "failed", error: "Cursor couldn't say which account is signed in" }
|
|
314
316
|
return { type: "success", access: "", refresh: CLI_MARK, expires: 0, accountId: who.email, ...(who.plan ? { plan: who.plan } : {}) }
|
|
315
317
|
},
|
|
316
318
|
}
|
|
@@ -318,6 +320,79 @@ async function cliSignIn() {
|
|
|
318
320
|
|
|
319
321
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
320
322
|
|
|
323
|
+
// ---- usage ------------------------------------------------------------------------
|
|
324
|
+
//
|
|
325
|
+
// How much of the plan's included usage is gone, as magpie's built-in Cursor
|
|
326
|
+
// account shows it (internal/provider/cursor_usage.go): the dashboard's
|
|
327
|
+
// current period, split into the Cursor Models and Other Models pools.
|
|
328
|
+
|
|
329
|
+
// statusText is a refused request's status as magpie says it (Go's
|
|
330
|
+
// http.StatusText).
|
|
331
|
+
const statusText = (s) =>
|
|
332
|
+
({ 413: "Request Entity Too Large", 414: "Request URI Too Long", 416: "Requested Range Not Satisfiable", 418: "I'm a teapot", 509: "" })[s] ?? STATUS_CODES[s] ?? ""
|
|
333
|
+
|
|
334
|
+
const POOL_VARIANT = /-(fast|none|low|medium|high|xhigh|extra-high|max|thinking)$/
|
|
335
|
+
|
|
336
|
+
// poolBase names a model's family: lower case, without cursor- and the
|
|
337
|
+
// effort and speed the CLI adds to it.
|
|
338
|
+
function poolBase(model) {
|
|
339
|
+
model = String(model).trim().toLowerCase()
|
|
340
|
+
if (model.startsWith("cursor-")) model = model.slice("cursor-".length)
|
|
341
|
+
for (;;) {
|
|
342
|
+
const b = model.replace(POOL_VARIANT, "")
|
|
343
|
+
if (b === model) return model
|
|
344
|
+
model = b
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Cursor's autoBucketModels can lag model releases: it still omitted Grok
|
|
349
|
+
// 4.6/4.7 when the published Cursor Models pool already included them.
|
|
350
|
+
// Keep those documented families alongside the server's exact model list.
|
|
351
|
+
// See https://cursor.com/docs/models-and-pricing.
|
|
352
|
+
function firstParty(model) {
|
|
353
|
+
if (model.startsWith("cursor-")) model = model.slice("cursor-".length)
|
|
354
|
+
if (model === "default" || model === "composer" || model.startsWith("composer-")) return true
|
|
355
|
+
return ["grok-4.5", "grok-4.6", "grok-4.7"].some((b) => model === b || model.startsWith(b + "-"))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// usage is the account's windows this billing period; an enterprise plan
|
|
359
|
+
// reports spend instead, and gets none. ids are the models it can be
|
|
360
|
+
// asked for, each counted by the pool Cursor bills it to.
|
|
361
|
+
async function usage(tok, ids) {
|
|
362
|
+
const res = await fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
|
|
365
|
+
body: "{}",
|
|
366
|
+
signal: AbortSignal.timeout(15_000),
|
|
367
|
+
})
|
|
368
|
+
if (!res.ok) return { error: statusText(res.status), windows: [] }
|
|
369
|
+
const data = await res.json()
|
|
370
|
+
const u = data?.planUsage
|
|
371
|
+
if (!u) return { windows: [] }
|
|
372
|
+
const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
|
|
373
|
+
const resets = ms > 0 ? { resetsAt: new Date(ms).toISOString() } : {}
|
|
374
|
+
const bucket = new Set((data.autoBucketModels ?? []).map(poolBase))
|
|
375
|
+
const inPool = (model) => {
|
|
376
|
+
model = poolBase(model)
|
|
377
|
+
if (model === "auto") model = "default" // the CLI's Auto is default in Cursor's API
|
|
378
|
+
// the server names a family (grok-4.8); the CLI asks for one at an
|
|
379
|
+
// effort or speed (grok-4.8-high-fast)
|
|
380
|
+
return bucket.has(model) || firstParty(model)
|
|
381
|
+
}
|
|
382
|
+
// Auto is always one, so the pool's list is never empty (which would
|
|
383
|
+
// count every model)
|
|
384
|
+
const pool = [...new Set(["auto", ...ids, ...(data.autoBucketModels ?? [])])].filter(inPool)
|
|
385
|
+
const num = (v) => (typeof v === "number" ? v : 0)
|
|
386
|
+
// the two pools fit the line; the total goes in its tooltip
|
|
387
|
+
return {
|
|
388
|
+
windows: [
|
|
389
|
+
{ name: "Cursor Models", used: num(u.autoPercentUsed), ...resets, models: pool },
|
|
390
|
+
{ name: "Other Models", used: num(u.apiPercentUsed), ...resets, notModels: pool },
|
|
391
|
+
{ name: "Total", used: num(u.totalPercentUsed), ...resets, aside: true },
|
|
392
|
+
],
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
321
396
|
// ---- models ----------------------------------------------------------------------
|
|
322
397
|
//
|
|
323
398
|
// Cursor lists a model once for each effort and speed it serves it at
|
|
@@ -1381,6 +1456,17 @@ export async function CursorAuthPlugin() {
|
|
|
1381
1456
|
{ type: "oauth", label: "cursor-agent's sign-in", authorize: cliSignIn },
|
|
1382
1457
|
{ type: "api", label: "Cursor API key (cursor.com/dashboard → Integrations)" },
|
|
1383
1458
|
],
|
|
1459
|
+
// magpie's: how much of the plan's included usage is gone (the plan
|
|
1460
|
+
// is the one the sign-in read)
|
|
1461
|
+
async usage(getAuth, provider) {
|
|
1462
|
+
let tok
|
|
1463
|
+
try {
|
|
1464
|
+
tok = await tokenOf(await getAuth())
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
return { error: e.message, windows: [] }
|
|
1467
|
+
}
|
|
1468
|
+
return usage(tok, Object.keys(provider?.models ?? {}))
|
|
1469
|
+
},
|
|
1384
1470
|
},
|
|
1385
1471
|
async config(config) {
|
|
1386
1472
|
config.provider ??= {}
|
|
@@ -1414,4 +1500,4 @@ export async function CursorAuthPlugin() {
|
|
|
1414
1500
|
}
|
|
1415
1501
|
|
|
1416
1502
|
// for tests
|
|
1417
|
-
export const _internal = { splitID, families, byEffort, offered, modelID, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
|
|
1503
|
+
export const _internal = { poolBase, splitID, families, byEffort, offered, modelID, 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.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|