@magpie-community/opencode-cursor-auth 0.1.4 → 0.1.7
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 +20 -1
- package/index.mjs +237 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,8 +43,18 @@ plugin's `fetch` answers them on Cursor's agent API:
|
|
|
43
43
|
system prompt; the model calls them through Cursor's `CallDynamicTool`,
|
|
44
44
|
and the calls come back as chat completion tool calls. Cursor's own tools
|
|
45
45
|
(shell, file reads, edits) are never run.
|
|
46
|
+
- A conversation's Runs share one `conversation_id`, so Cursor sends them
|
|
47
|
+
to the machine that has the prompt cached (Grok on Cursor caches by
|
|
48
|
+
machine). It is made from what names the session — the request's
|
|
49
|
+
`prompt_cache_key` (Codex's thread id), else the session the
|
|
50
|
+
`chat.headers` hook is told — and the conversation's first user message,
|
|
51
|
+
so subagents under one session are conversations of their own. With
|
|
52
|
+
nothing naming the session, each Run has a new one.
|
|
46
53
|
- Text, thinking (`reasoning_content`) and token usage come back streamed
|
|
47
|
-
or not.
|
|
54
|
+
or not. Cursor's `input_tokens` counts the cached prompt too: the cache
|
|
55
|
+
read and written is taken out of it, and given as `cached_tokens` and
|
|
56
|
+
`cache_write_tokens`, so it is counted once; its reasoning tokens are
|
|
57
|
+
`reasoning_tokens`. Failures keep their statuses: 401 to sign in again, 429 at the
|
|
48
58
|
usage limit, 400 for a prompt too long, 403 for a region refusal.
|
|
49
59
|
|
|
50
60
|
## Models
|
|
@@ -60,6 +70,15 @@ The `config` hook declares `auto` (Cursor's pick). Once signed in, the
|
|
|
60
70
|
is. `-fast` and `-thinking` models stay models of their own; a
|
|
61
71
|
`service_tier` of `priority` picks the fast one.
|
|
62
72
|
- The context window is 1M for a model whose name says 1M, else 200K.
|
|
73
|
+
- Cursor's model picker (`AiService/AvailableModels`, as the CLI asks for
|
|
74
|
+
it) adds the models it offers that the usable list leaves out (GLM-5.3,
|
|
75
|
+
GLM-5.3 Flash), run with the picker variant's parameters; hidden and
|
|
76
|
+
Tab-only models stay out.
|
|
77
|
+
- A model Cursor serves only in Max Mode (the usable list's `maxMode`, a
|
|
78
|
+
picker model with no other mode, or a Max Mode variant) is asked for in
|
|
79
|
+
Max Mode, as the CLI turns it on for one. A model neither says it of is
|
|
80
|
+
asked again in Max Mode when Cursor answers "Max Mode Required", and so
|
|
81
|
+
from then on.
|
|
63
82
|
|
|
64
83
|
## Not included
|
|
65
84
|
|
package/index.mjs
CHANGED
|
@@ -158,9 +158,10 @@ async function cliTokenNow() {
|
|
|
158
158
|
await cliRefresh
|
|
159
159
|
tok = await cliToken()
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
// the built-in's words (provider.CursorToken)
|
|
162
|
+
if (!tok) throw new AuthError("Cursor isn't signed in; sign in from magpie's Providers page or run `cursor-agent login`")
|
|
162
163
|
const e = expiry(tok)
|
|
163
|
-
if (e && e <= Date.now()) throw new AuthError("Cursor's sign-in has run out
|
|
164
|
+
if (e && e <= Date.now()) throw new AuthError("Cursor's sign-in has run out; sign in again from magpie's Providers page or run `cursor-agent login`")
|
|
164
165
|
return tok
|
|
165
166
|
}
|
|
166
167
|
|
|
@@ -601,14 +602,19 @@ function modelID(raw, model, effort, fast) {
|
|
|
601
602
|
return vs[fitEffort(effort, levels)]
|
|
602
603
|
}
|
|
603
604
|
|
|
604
|
-
// usable is the account's list
|
|
605
|
-
// names and the id each is run by
|
|
605
|
+
// usable is the account's list: what `cursor-agent models` has
|
|
606
|
+
// (GetUsableModels: its ids, names and the id each is run by, and whether
|
|
607
|
+
// Cursor serves it in Max Mode), and the models Cursor's model picker
|
|
608
|
+
// offers that it leaves out (parameterized). Kept a while, by token.
|
|
606
609
|
const lists = new Map()
|
|
607
610
|
async function usable(tok) {
|
|
608
611
|
const h = createHash("sha256").update(tok).digest("hex")
|
|
609
612
|
const had = lists.get(h)
|
|
610
613
|
if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
|
|
611
|
-
const j = await
|
|
614
|
+
const [j, picker] = await Promise.all([
|
|
615
|
+
unary(API, "agent.v1.AgentService/GetUsableModels", tok, {}, 30_000),
|
|
616
|
+
parameterized(tok),
|
|
617
|
+
])
|
|
612
618
|
const raw = []
|
|
613
619
|
for (const m of j?.models ?? []) {
|
|
614
620
|
const id = m.displayModelId || m.modelId
|
|
@@ -616,13 +622,88 @@ async function usable(tok) {
|
|
|
616
622
|
// some names come with zero-width spaces and doubled ones
|
|
617
623
|
let name = String(m.displayName || id).replaceAll("", "").split(/\s+/).filter(Boolean).join(" ")
|
|
618
624
|
name = name.replace(/\(default\)$/, "").trim().replace(/\(current\)$/, "").trim()
|
|
619
|
-
|
|
625
|
+
const run = m.modelId || id
|
|
626
|
+
// Max Mode as the CLI sends it: the list's own say, else the picker's
|
|
627
|
+
// (a model with no other mode, or a variant that is Max Mode's)
|
|
628
|
+
const maxMode = m.maxMode === true || !!(picker.slugs.get(id)?.maxMode || picker.slugs.get(run)?.maxMode)
|
|
629
|
+
raw.push({ id, name, context: contextOf(name), run, ...(maxMode ? { maxMode } : {}) })
|
|
620
630
|
}
|
|
621
631
|
if (!raw.length) throw new Error("Cursor listed no models")
|
|
632
|
+
for (const m of picker.extra(raw)) raw.push(m)
|
|
622
633
|
lists.set(h, { at: Date.now(), raw })
|
|
623
634
|
return raw
|
|
624
635
|
}
|
|
625
636
|
|
|
637
|
+
// left out of the CLI's picker as it leaves them out (model-service.ts)
|
|
638
|
+
const PICKER_SKIP = new Set(["claude-4.5-haiku", "claude-4.5-haiku-thinking", "gemini-2.5-pro", "gemini-2.5-flash"])
|
|
639
|
+
|
|
640
|
+
// needsMax is whether a variant of a picker's model is run in Max Mode
|
|
641
|
+
// only, as the CLI reads it: a model with no other mode, or a Max Mode
|
|
642
|
+
// variant.
|
|
643
|
+
const needsMax = (m, v) => m?.supportsNonMaxMode === false || v?.isMaxMode === true
|
|
644
|
+
|
|
645
|
+
// parameterized is Cursor's model picker (AiService/AvailableModels, as
|
|
646
|
+
// the CLI asks for it): each id a model goes by (its name, legacy slugs,
|
|
647
|
+
// each variant's slug) with the model and variant it is, and extra(raw),
|
|
648
|
+
// the picker's models the usable list doesn't have, as entries of it (a
|
|
649
|
+
// model added since, such as GLM-5.3, or one the account hasn't turned
|
|
650
|
+
// on). A picker Cursor can't give is none.
|
|
651
|
+
async function parameterized(tok) {
|
|
652
|
+
let models = []
|
|
653
|
+
try {
|
|
654
|
+
const j = await unary(API, "aiserver.v1.AiService/AvailableModels", tok, { useModelParameters: true, doNotUseMarkdown: true }, 8_000)
|
|
655
|
+
models = Array.isArray(j?.models) ? j.models : []
|
|
656
|
+
} catch {}
|
|
657
|
+
const slugs = new Map()
|
|
658
|
+
const namesOf = (m) => {
|
|
659
|
+
const vs = m.variants ?? []
|
|
660
|
+
return [m.name, m.serverModelName, ...(m.legacySlugs ?? []), ...(m.idAliases ?? []), ...vs.flatMap((v) => [v.legacySlug, v.variantStringRepresentation])].filter(Boolean)
|
|
661
|
+
}
|
|
662
|
+
for (const m of models) {
|
|
663
|
+
const vs = m.variants ?? []
|
|
664
|
+
for (const v of vs) for (const s of [v.legacySlug, v.variantStringRepresentation]) if (s && !slugs.has(s)) slugs.set(s, { maxMode: needsMax(m, v) })
|
|
665
|
+
for (const s of [m.name, m.serverModelName, ...(m.legacySlugs ?? [])]) if (s && !slugs.has(s)) slugs.set(s, { maxMode: needsMax(m, defaultVariant(m)) })
|
|
666
|
+
}
|
|
667
|
+
const extra = (raw) => {
|
|
668
|
+
const have = new Set(raw.flatMap((r) => [r.id, r.run]))
|
|
669
|
+
const out = []
|
|
670
|
+
for (const m of models) {
|
|
671
|
+
if (!m?.name || m.isHidden || m.isChatOnly || m.onlySupportsCmdK || m.supportsAgent === false || PICKER_SKIP.has(m.name)) continue
|
|
672
|
+
if (namesOf(m).some((s) => have.has(s))) continue
|
|
673
|
+
const title = clean(m.clientDisplayName || m.name)
|
|
674
|
+
const params = (v) => (v?.parameterValues ?? []).map((p) => ({ id: String(p.id ?? ""), value: String(p.value ?? "") })).filter((p) => p.id)
|
|
675
|
+
const entry = (id, name, v) => {
|
|
676
|
+
const maxMode = needsMax(m, v)
|
|
677
|
+
const limit = (maxMode && m.contextTokenLimitForMaxMode) || m.contextTokenLimit
|
|
678
|
+
return { id, name, context: limit > 0 ? limit : contextOf(name), run: m.name, params: params(v), ...(maxMode ? { maxMode } : {}) }
|
|
679
|
+
}
|
|
680
|
+
const named = (m.variants ?? []).filter((v) => v.legacySlug)
|
|
681
|
+
if (named.length) {
|
|
682
|
+
for (const v of named) {
|
|
683
|
+
if (have.has(v.legacySlug)) continue
|
|
684
|
+
have.add(v.legacySlug)
|
|
685
|
+
const label = clean(v.displayNameOutsidePicker || [title, v.displayName].filter(Boolean).join(" "))
|
|
686
|
+
out.push(entry(v.legacySlug, label, v))
|
|
687
|
+
}
|
|
688
|
+
} else if (!have.has(m.name)) {
|
|
689
|
+
have.add(m.name)
|
|
690
|
+
out.push(entry(m.name, title, defaultVariant(m)))
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return out
|
|
694
|
+
}
|
|
695
|
+
return { slugs, extra }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// defaultVariant is the variant the CLI picks for a model asked for by
|
|
699
|
+
// name: its default without Max Mode, else with it, else the first.
|
|
700
|
+
const defaultVariant = (m) => {
|
|
701
|
+
const vs = m?.variants ?? []
|
|
702
|
+
return vs.find((v) => v.isDefaultNonMaxConfig) ?? vs.find((v) => v.isDefaultMaxConfig) ?? vs[0]
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const clean = (s) => String(s ?? "").replaceAll("", "").split(/\s+/).filter(Boolean).join(" ")
|
|
706
|
+
|
|
626
707
|
function runtimeModel(m) {
|
|
627
708
|
return {
|
|
628
709
|
id: m.id,
|
|
@@ -847,21 +928,17 @@ function failure(status, text) {
|
|
|
847
928
|
return statusOf(status, f.code ?? "", msg)
|
|
848
929
|
}
|
|
849
930
|
|
|
850
|
-
// signInExpired is a message of a token or session run out; a trial, a
|
|
851
|
-
// plan or a link run out is no lapsed sign-in, and magpie marks the
|
|
852
|
-
// account lapsed on a 401.
|
|
853
|
-
const signInExpired = (low) => low.includes("expired") && /token|session|sign[- ]?in|log[- ]?in|auth|credential/.test(low)
|
|
854
|
-
|
|
855
931
|
// statusOf is the status and words for Cursor's error of this code and
|
|
856
932
|
// message. A region the team isn't served in says so, not to sign in.
|
|
857
933
|
function statusOf(status, code, msg) {
|
|
858
|
-
msg ||= statusText(status)
|
|
934
|
+
msg ||= statusText(status)
|
|
859
935
|
const low = msg.toLowerCase()
|
|
860
936
|
// magpie names the provider before the message itself
|
|
861
937
|
const out = (status, message) => ({ status, message })
|
|
862
938
|
if (regional(msg)) return out(403, msg + " — Cursor serves your team only in some regions and turned this request away; signing in again won't change that")
|
|
863
939
|
if (code === "permission_denied") return out(403, msg)
|
|
864
|
-
if (code === "unauthenticated" || status === 401 ||
|
|
940
|
+
if (code === "unauthenticated" || status === 401 || low.includes("expired"))
|
|
941
|
+
return out(401, msg + " — sign in to Cursor again in magpie")
|
|
865
942
|
if (code === "resource_exhausted" || low.includes("quota") || low.includes("rate limit") || low.includes("usage limit"))
|
|
866
943
|
return out(429, "usage limit reached: " + msg)
|
|
867
944
|
if (low.includes("too long") || low.includes("context length") || low.includes("too many tokens"))
|
|
@@ -1023,10 +1100,75 @@ function toolDef(t) {
|
|
|
1023
1100
|
|
|
1024
1101
|
const env = () => pb().str(1, GOOS).str(2, tmpdir()).str(10, "UTC")
|
|
1025
1102
|
|
|
1103
|
+
// SESSION carries the session magpie (or OpenCode) names a request's
|
|
1104
|
+
// conversation by, from the chat.headers hook to the loader's fetch. It
|
|
1105
|
+
// goes no further: nothing of the request's headers is sent to Cursor.
|
|
1106
|
+
const SESSION = "x-magpie-cursor-session"
|
|
1107
|
+
|
|
1108
|
+
// sessionOf is the session a chat.headers hook is told, "" when it names
|
|
1109
|
+
// none. magpie, with no session header from the agent, makes one of the
|
|
1110
|
+
// conversation's first user message ("magpie-" and 24 hex digits): that
|
|
1111
|
+
// names no session, and is taken as none, as the built-in's
|
|
1112
|
+
// nativeSessionOf takes no such header as none.
|
|
1113
|
+
function sessionOf(input) {
|
|
1114
|
+
const s = String(input?.sessionID ?? "").trim()
|
|
1115
|
+
if (!s || /^magpie-[0-9a-f]{24}$/.test(s)) return ""
|
|
1116
|
+
return s.slice(0, 128)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// uuidOf is a version 4 UUID made of a hash's first 16 bytes.
|
|
1120
|
+
function uuidOf(b) {
|
|
1121
|
+
b = Buffer.from(b.subarray(0, 16))
|
|
1122
|
+
b[6] = (b[6] & 0x0f) | 0x40
|
|
1123
|
+
b[8] = (b[8] & 0x3f) | 0x80
|
|
1124
|
+
const h = b.toString("hex")
|
|
1125
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// conversationID is the AgentRunRequest's conversation_id for a
|
|
1129
|
+
// conversation, as the built-in's cursorConversation makes it (magpie
|
|
1130
|
+
// #498): the same on every request of it, so that Cursor's backend keeps
|
|
1131
|
+
// sending it to the machine that has its prompt cached — Grok on Cursor
|
|
1132
|
+
// caches by machine. Cursor's own client keeps one conversationId per
|
|
1133
|
+
// agent session, so it is made from what names the session — the
|
|
1134
|
+
// client's prompt_cache_key (Codex's thread id), else the session magpie
|
|
1135
|
+
// or OpenCode names — together with the conversation's first user
|
|
1136
|
+
// message, which every later request repeats: subagents running at once
|
|
1137
|
+
// under one session (Claude Code's Task agents share its session id) are
|
|
1138
|
+
// separate conversations to Cursor, as they are to its own client. With
|
|
1139
|
+
// nothing naming the session it is "", and each Run gets a new id as
|
|
1140
|
+
// before: a first message alone ("hi") would put strangers' conversations
|
|
1141
|
+
// under one id.
|
|
1142
|
+
function conversationID(chat, session) {
|
|
1143
|
+
const key = (typeof chat?.prompt_cache_key === "string" ? chat.prompt_cache_key.trim() : "") || session || ""
|
|
1144
|
+
if (!key) return ""
|
|
1145
|
+
const msgs = Array.isArray(chat?.messages) ? chat.messages : []
|
|
1146
|
+
const first = msgs.find((m) => m?.role === "user") ?? msgs[0]
|
|
1147
|
+
const sum = createHash("sha256")
|
|
1148
|
+
.update("cursor conversation\0" + key + "\0" + (first === undefined ? "" : JSON.stringify(first)))
|
|
1149
|
+
.digest()
|
|
1150
|
+
return uuidOf(sum)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// usageOf is a TurnEndedUpdate as magpie counts usage, as the built-in's
|
|
1154
|
+
// cursorUsage has it (magpie #498). Its input_tokens is the whole prompt,
|
|
1155
|
+
// what was read from the cache and written to it included, as Cursor's
|
|
1156
|
+
// own client has it (it takes both out to get the prompt's uncached
|
|
1157
|
+
// rest); input is that rest, the cache added back for prompt_tokens. Its
|
|
1158
|
+
// reasoning_tokens is kept as reasoning.
|
|
1159
|
+
function usageOf(uf) {
|
|
1160
|
+
const [inp, cr, cw] = [pbNum(uf, 1), pbNum(uf, 3), pbNum(uf, 4)]
|
|
1161
|
+
return { input: Math.max(inp - cr - cw, 0), output: pbNum(uf, 2), cacheRead: cr, cacheWrite: cw, reasoning: pbNum(uf, 5) }
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1026
1164
|
// buildRun is the Run's first message, an AgentClientMessage with its
|
|
1027
1165
|
// run_request, and the blobs it names. The conversation state is the
|
|
1028
1166
|
// messages and one turn, which the server wants there to sample at all.
|
|
1029
|
-
|
|
1167
|
+
// conv is the conversation_id (conversationID), a new one when "". The
|
|
1168
|
+
// model goes as the CLI sends it: in Max Mode when maxMode is set (Cursor
|
|
1169
|
+
// refuses a model it serves only that way otherwise: "Max Mode
|
|
1170
|
+
// Required"), with the picker variant's parameters when it has them.
|
|
1171
|
+
function buildRun(msgs, lastUser, tools, model, conv = "", { maxMode = false, params = [] } = {}) {
|
|
1030
1172
|
const blobs = new Map()
|
|
1031
1173
|
const put = (b) => {
|
|
1032
1174
|
const id = createHash("sha256").update(b).digest()
|
|
@@ -1047,13 +1189,22 @@ function buildRun(msgs, lastUser, tools, model) {
|
|
|
1047
1189
|
mcp.bytes(1, d)
|
|
1048
1190
|
}
|
|
1049
1191
|
const action = pb().bytes(2, pb().bytes(2, rc)) // resume_action
|
|
1192
|
+
// ModelDetails: model_id, display_model_id, display_name, max_mode (7);
|
|
1193
|
+
// RequestedModel: model_id, max_mode (2), parameters (3)
|
|
1194
|
+
const details = pb().str(1, model).str(3, model).str(4, model)
|
|
1195
|
+
const requested = pb().str(1, model)
|
|
1196
|
+
if (maxMode) {
|
|
1197
|
+
details.varint(7, 1)
|
|
1198
|
+
requested.varint(2, 1)
|
|
1199
|
+
}
|
|
1200
|
+
for (const p of params) requested.bytes(3, pb().str(1, p.id).str(2, p.value))
|
|
1050
1201
|
const rr = pb()
|
|
1051
1202
|
.bytes(1, state)
|
|
1052
1203
|
.bytes(2, action)
|
|
1053
|
-
.bytes(3,
|
|
1204
|
+
.bytes(3, details)
|
|
1054
1205
|
.bytes(4, mcp)
|
|
1055
|
-
.str(5, randomUUID())
|
|
1056
|
-
.bytes(9,
|
|
1206
|
+
.str(5, conv || randomUUID())
|
|
1207
|
+
.bytes(9, requested)
|
|
1057
1208
|
.varint(19, 1) // inline images
|
|
1058
1209
|
return { first: pb().bytes(1, rr).done(), blobs }
|
|
1059
1210
|
}
|
|
@@ -1120,8 +1271,8 @@ function open(base, headers, signal) {
|
|
|
1120
1271
|
|
|
1121
1272
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
1122
1273
|
// iterator of the answer's pieces, or the error it failed with before any.
|
|
1123
|
-
async function runOnce({ tok, base, id, tools, conv, signal }) {
|
|
1124
|
-
const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id)
|
|
1274
|
+
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
|
|
1275
|
+
const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id, convID, { maxMode, params })
|
|
1125
1276
|
const o = await open(
|
|
1126
1277
|
base,
|
|
1127
1278
|
{
|
|
@@ -1194,10 +1345,12 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1194
1345
|
let calls = 0
|
|
1195
1346
|
let listed = 0
|
|
1196
1347
|
let said = 0
|
|
1197
|
-
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
1348
|
+
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
1198
1349
|
const finish = () => {
|
|
1199
1350
|
if (!usage.output) usage.output = Math.floor((said + 3) / 4)
|
|
1200
|
-
|
|
1351
|
+
// a guess only when Cursor counted nothing: a prompt read whole from
|
|
1352
|
+
// the cache leaves no uncached rest, and is no reason to guess
|
|
1353
|
+
if (!usage.input && !usage.cacheRead && !usage.cacheWrite) usage.input = estimate
|
|
1201
1354
|
const prompt = usage.input + usage.cacheRead + usage.cacheWrite
|
|
1202
1355
|
return {
|
|
1203
1356
|
stop: calls > 0 ? "tool_calls" : "stop",
|
|
@@ -1206,6 +1359,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1206
1359
|
completion_tokens: usage.output,
|
|
1207
1360
|
total_tokens: prompt + usage.output,
|
|
1208
1361
|
prompt_tokens_details: { cached_tokens: usage.cacheRead, cache_write_tokens: usage.cacheWrite },
|
|
1362
|
+
...(usage.reasoning ? { completion_tokens_details: { reasoning_tokens: usage.reasoning } } : {}),
|
|
1209
1363
|
},
|
|
1210
1364
|
}
|
|
1211
1365
|
}
|
|
@@ -1244,7 +1398,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1244
1398
|
break
|
|
1245
1399
|
}
|
|
1246
1400
|
case 14: // turn ended, with what it used
|
|
1247
|
-
usage =
|
|
1401
|
+
usage = usageOf(uf)
|
|
1248
1402
|
if (calls === 0) {
|
|
1249
1403
|
yield finish()
|
|
1250
1404
|
return
|
|
@@ -1357,15 +1511,36 @@ function exec(msg, tools, send, closeExec) {
|
|
|
1357
1511
|
|
|
1358
1512
|
// ---- chat completions ------------------------------------------------------------------
|
|
1359
1513
|
|
|
1514
|
+
// Every answer says X-Magpie-Sign-In: kept. The built-in marked no Cursor
|
|
1515
|
+
// account lapsed, not for a 401 nor for any "expired" (a token, a session,
|
|
1516
|
+
// a trial), and cleared none on a success or a renewed token.
|
|
1360
1517
|
const errorResponse = ({ status, message }) =>
|
|
1361
1518
|
new Response(JSON.stringify({ error: { message, type: "cursor_error", code: status } }), {
|
|
1362
1519
|
status,
|
|
1363
|
-
headers: { "Content-Type": "application/json" },
|
|
1520
|
+
headers: { "Content-Type": "application/json", "X-Magpie-Sign-In": "kept" },
|
|
1364
1521
|
})
|
|
1365
1522
|
|
|
1366
|
-
|
|
1523
|
+
function kept(res) {
|
|
1524
|
+
if (res.headers.has("X-Magpie-Sign-In")) return res
|
|
1525
|
+
const headers = new Headers(res.headers)
|
|
1526
|
+
headers.set("X-Magpie-Sign-In", "kept")
|
|
1527
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// answer runs a chat completion on Cursor, the account kept (errorResponse).
|
|
1531
|
+
const answer = async (auth, chat, signal, session) => kept(await answerOf(auth, chat, signal, session))
|
|
1532
|
+
|
|
1533
|
+
// maxOnly are the ids Cursor said it serves in Max Mode only.
|
|
1534
|
+
const maxOnly = new Set()
|
|
1535
|
+
|
|
1536
|
+
// maxRequired is Cursor's refusal of a model asked for without Max Mode
|
|
1537
|
+
// (the CLI's MAX_MODE_REQUIRED): "Max Mode Required: The model "x"
|
|
1538
|
+
// requires Max Mode to be enabled. …"
|
|
1539
|
+
const maxRequired = (msg) => /max[ _]mode[ _]required|requires max mode/i.test(String(msg ?? ""))
|
|
1540
|
+
|
|
1541
|
+
// answerOf runs a chat completion on Cursor and answers it as one, streamed
|
|
1367
1542
|
// or not.
|
|
1368
|
-
async function
|
|
1543
|
+
async function answerOf(auth, chat, signal, session = "") {
|
|
1369
1544
|
let tok
|
|
1370
1545
|
try {
|
|
1371
1546
|
tok = await tokenOf(auth)
|
|
@@ -1379,17 +1554,34 @@ async function answer(auth, chat, signal) {
|
|
|
1379
1554
|
} catch {}
|
|
1380
1555
|
const fast = chat.service_tier === "priority" || chat.service_tier === "fast"
|
|
1381
1556
|
let id = modelID(raw, model, chat.reasoning_effort ?? "", fast)
|
|
1382
|
-
|
|
1557
|
+
const entry = raw.find((m) => m.id === id)
|
|
1558
|
+
id = entry?.run ?? id
|
|
1383
1559
|
if (id === "auto") id = "default" // Cursor's pick, which its API calls default
|
|
1560
|
+
const params = entry?.params ?? []
|
|
1561
|
+
let maxMode = !!entry?.maxMode || maxOnly.has(id)
|
|
1384
1562
|
const tools = toolsOf(chat)
|
|
1385
1563
|
const conv = conversation(chat, tools)
|
|
1386
|
-
const
|
|
1387
|
-
let
|
|
1564
|
+
const convID = conversationID(chat, session)
|
|
1565
|
+
let base = await agentURL(tok, false)
|
|
1566
|
+
const once = () => runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params })
|
|
1567
|
+
let r = await once()
|
|
1388
1568
|
if (r.error && regional(r.error.message)) {
|
|
1389
1569
|
// the team moved, or the config was kept from before: once more with
|
|
1390
1570
|
// what the config says now
|
|
1391
1571
|
const fresh = await agentURL(tok, true)
|
|
1392
|
-
if (fresh !== base)
|
|
1572
|
+
if (fresh !== base) {
|
|
1573
|
+
base = fresh
|
|
1574
|
+
r = await once()
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (r.error && !maxMode && maxRequired(r.error.message)) {
|
|
1578
|
+
// a model Cursor serves only in Max Mode that neither list said so of:
|
|
1579
|
+
// in Max Mode, as the CLI turns it on for such a model, and so from
|
|
1580
|
+
// now on. An account Max Mode isn't open to is answered as Cursor
|
|
1581
|
+
// answers that.
|
|
1582
|
+
maxMode = true
|
|
1583
|
+
maxOnly.add(id)
|
|
1584
|
+
r = await once()
|
|
1393
1585
|
}
|
|
1394
1586
|
if (r.error) return errorResponse(r.error)
|
|
1395
1587
|
const it = r.events
|
|
@@ -1470,7 +1662,8 @@ export async function CursorAuthPlugin() {
|
|
|
1470
1662
|
} catch {
|
|
1471
1663
|
return errorResponse({ status: 400, message: "a request that isn't JSON" })
|
|
1472
1664
|
}
|
|
1473
|
-
|
|
1665
|
+
const session = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined)).get(SESSION) ?? ""
|
|
1666
|
+
return answer((await getAuth()) ?? auth, chat, init.signal ?? (input instanceof Request ? input.signal : undefined), session)
|
|
1474
1667
|
},
|
|
1475
1668
|
}
|
|
1476
1669
|
},
|
|
@@ -1480,18 +1673,28 @@ export async function CursorAuthPlugin() {
|
|
|
1480
1673
|
{ type: "api", label: "Cursor API key (cursor.com/dashboard → Integrations)" },
|
|
1481
1674
|
],
|
|
1482
1675
|
// magpie's: how much of the plan's included usage is gone (the plan
|
|
1483
|
-
// is the one the sign-in read)
|
|
1676
|
+
// is the one the sign-in read). The built-in's read, clean or not,
|
|
1677
|
+
// neither marked the account nor cleared it.
|
|
1484
1678
|
async usage(getAuth, provider) {
|
|
1485
1679
|
let tok
|
|
1486
1680
|
try {
|
|
1487
1681
|
tok = await tokenOf(await getAuth())
|
|
1488
1682
|
} catch (e) {
|
|
1489
|
-
return { error: e.message, windows: [] }
|
|
1683
|
+
return { error: e.message, windows: [], signIn: "kept" }
|
|
1490
1684
|
}
|
|
1491
1685
|
const [u, plan] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok)])
|
|
1492
|
-
return
|
|
1686
|
+
return { ...u, ...(plan ? { plan } : {}), signIn: "kept" }
|
|
1493
1687
|
},
|
|
1494
1688
|
},
|
|
1689
|
+
// the session the request is part of, for the conversation_id
|
|
1690
|
+
// (conversationID): magpie and OpenCode name it here, and the loader's
|
|
1691
|
+
// fetch reads it back. Every plugin's hook sees every provider's
|
|
1692
|
+
// requests, so it is put on Cursor's alone.
|
|
1693
|
+
async "chat.headers"(input, output) {
|
|
1694
|
+
if (input?.model?.providerID !== ID && input?.provider?.info?.id !== ID) return
|
|
1695
|
+
const s = sessionOf(input)
|
|
1696
|
+
if (s) output.headers[SESSION] = s
|
|
1697
|
+
},
|
|
1495
1698
|
async config(config) {
|
|
1496
1699
|
config.provider ??= {}
|
|
1497
1700
|
const was = config.provider[ID] ?? {}
|
|
@@ -1522,4 +1725,4 @@ export async function CursorAuthPlugin() {
|
|
|
1522
1725
|
}
|
|
1523
1726
|
|
|
1524
1727
|
// for tests
|
|
1525
|
-
export const _internal = { poolBase, splitID, families, byEffort, offered, modelID, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
|
|
1728
|
+
export const _internal = { usable, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, 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.7",
|
|
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",
|