@magpie-community/opencode-cursor-auth 0.1.5 → 0.1.8
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 +226 -26
- package/package.json +3 -2
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
|
@@ -602,14 +602,19 @@ function modelID(raw, model, effort, fast) {
|
|
|
602
602
|
return vs[fitEffort(effort, levels)]
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
// usable is the account's list
|
|
606
|
-
// 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.
|
|
607
609
|
const lists = new Map()
|
|
608
610
|
async function usable(tok) {
|
|
609
611
|
const h = createHash("sha256").update(tok).digest("hex")
|
|
610
612
|
const had = lists.get(h)
|
|
611
613
|
if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
|
|
612
|
-
const j = await
|
|
614
|
+
const [j, picker] = await Promise.all([
|
|
615
|
+
unary(API, "agent.v1.AgentService/GetUsableModels", tok, {}, 30_000),
|
|
616
|
+
parameterized(tok),
|
|
617
|
+
])
|
|
613
618
|
const raw = []
|
|
614
619
|
for (const m of j?.models ?? []) {
|
|
615
620
|
const id = m.displayModelId || m.modelId
|
|
@@ -617,13 +622,88 @@ async function usable(tok) {
|
|
|
617
622
|
// some names come with zero-width spaces and doubled ones
|
|
618
623
|
let name = String(m.displayName || id).replaceAll("", "").split(/\s+/).filter(Boolean).join(" ")
|
|
619
624
|
name = name.replace(/\(default\)$/, "").trim().replace(/\(current\)$/, "").trim()
|
|
620
|
-
|
|
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 } : {}) })
|
|
621
630
|
}
|
|
622
631
|
if (!raw.length) throw new Error("Cursor listed no models")
|
|
632
|
+
for (const m of picker.extra(raw)) raw.push(m)
|
|
623
633
|
lists.set(h, { at: Date.now(), raw })
|
|
624
634
|
return raw
|
|
625
635
|
}
|
|
626
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
|
+
|
|
627
707
|
function runtimeModel(m) {
|
|
628
708
|
return {
|
|
629
709
|
id: m.id,
|
|
@@ -901,11 +981,14 @@ function toolsOf(chat) {
|
|
|
901
981
|
// catalog lists the caller's tools for the model. It sees MCP tools only
|
|
902
982
|
// through Cursor's GetDynamicTools and CallDynamicTool, and Cursor names
|
|
903
983
|
// them in a prompt of its own, which this conversation goes without:
|
|
904
|
-
// without the list the model says it has no such tool.
|
|
984
|
+
// without the list the model says it has no such tool. Each entry is
|
|
985
|
+
// written as the CallDynamicTool call itself: listed as <tool name=...>,
|
|
986
|
+
// some models (GLM-5.3) called the names bare, as tools of their own, and
|
|
987
|
+
// those calls went nowhere (#11).
|
|
905
988
|
function catalog(tools) {
|
|
906
989
|
if (!tools.length) return ""
|
|
907
|
-
let s = `\n\n<dynamic_tool_catalog>\nThe
|
|
908
|
-
for (const t of tools) s += `<
|
|
990
|
+
let s = `\n\n<dynamic_tool_catalog>\nThe MCP namespace "magpie" has the tools below. They are reached only through \`${CALL}\`, never as tools of their own. Each entry is the exact call to make; its arguments must match the schema given. The schemas are complete, so there is no need to call \`GetDynamicTools\` first.\n`
|
|
991
|
+
for (const t of tools) s += `<call>${CALL}({"namespace":"magpie","toolName":"${t.name}","arguments":{...}})\n${t.description}\narguments schema: ${t.schemaText}\n</call>\n`
|
|
909
992
|
return s + "</dynamic_tool_catalog>"
|
|
910
993
|
}
|
|
911
994
|
|
|
@@ -1020,10 +1103,75 @@ function toolDef(t) {
|
|
|
1020
1103
|
|
|
1021
1104
|
const env = () => pb().str(1, GOOS).str(2, tmpdir()).str(10, "UTC")
|
|
1022
1105
|
|
|
1106
|
+
// SESSION carries the session magpie (or OpenCode) names a request's
|
|
1107
|
+
// conversation by, from the chat.headers hook to the loader's fetch. It
|
|
1108
|
+
// goes no further: nothing of the request's headers is sent to Cursor.
|
|
1109
|
+
const SESSION = "x-magpie-cursor-session"
|
|
1110
|
+
|
|
1111
|
+
// sessionOf is the session a chat.headers hook is told, "" when it names
|
|
1112
|
+
// none. magpie, with no session header from the agent, makes one of the
|
|
1113
|
+
// conversation's first user message ("magpie-" and 24 hex digits): that
|
|
1114
|
+
// names no session, and is taken as none, as the built-in's
|
|
1115
|
+
// nativeSessionOf takes no such header as none.
|
|
1116
|
+
function sessionOf(input) {
|
|
1117
|
+
const s = String(input?.sessionID ?? "").trim()
|
|
1118
|
+
if (!s || /^magpie-[0-9a-f]{24}$/.test(s)) return ""
|
|
1119
|
+
return s.slice(0, 128)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// uuidOf is a version 4 UUID made of a hash's first 16 bytes.
|
|
1123
|
+
function uuidOf(b) {
|
|
1124
|
+
b = Buffer.from(b.subarray(0, 16))
|
|
1125
|
+
b[6] = (b[6] & 0x0f) | 0x40
|
|
1126
|
+
b[8] = (b[8] & 0x3f) | 0x80
|
|
1127
|
+
const h = b.toString("hex")
|
|
1128
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// conversationID is the AgentRunRequest's conversation_id for a
|
|
1132
|
+
// conversation, as the built-in's cursorConversation makes it (magpie
|
|
1133
|
+
// #498): the same on every request of it, so that Cursor's backend keeps
|
|
1134
|
+
// sending it to the machine that has its prompt cached — Grok on Cursor
|
|
1135
|
+
// caches by machine. Cursor's own client keeps one conversationId per
|
|
1136
|
+
// agent session, so it is made from what names the session — the
|
|
1137
|
+
// client's prompt_cache_key (Codex's thread id), else the session magpie
|
|
1138
|
+
// or OpenCode names — together with the conversation's first user
|
|
1139
|
+
// message, which every later request repeats: subagents running at once
|
|
1140
|
+
// under one session (Claude Code's Task agents share its session id) are
|
|
1141
|
+
// separate conversations to Cursor, as they are to its own client. With
|
|
1142
|
+
// nothing naming the session it is "", and each Run gets a new id as
|
|
1143
|
+
// before: a first message alone ("hi") would put strangers' conversations
|
|
1144
|
+
// under one id.
|
|
1145
|
+
function conversationID(chat, session) {
|
|
1146
|
+
const key = (typeof chat?.prompt_cache_key === "string" ? chat.prompt_cache_key.trim() : "") || session || ""
|
|
1147
|
+
if (!key) return ""
|
|
1148
|
+
const msgs = Array.isArray(chat?.messages) ? chat.messages : []
|
|
1149
|
+
const first = msgs.find((m) => m?.role === "user") ?? msgs[0]
|
|
1150
|
+
const sum = createHash("sha256")
|
|
1151
|
+
.update("cursor conversation\0" + key + "\0" + (first === undefined ? "" : JSON.stringify(first)))
|
|
1152
|
+
.digest()
|
|
1153
|
+
return uuidOf(sum)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// usageOf is a TurnEndedUpdate as magpie counts usage, as the built-in's
|
|
1157
|
+
// cursorUsage has it (magpie #498). Its input_tokens is the whole prompt,
|
|
1158
|
+
// what was read from the cache and written to it included, as Cursor's
|
|
1159
|
+
// own client has it (it takes both out to get the prompt's uncached
|
|
1160
|
+
// rest); input is that rest, the cache added back for prompt_tokens. Its
|
|
1161
|
+
// reasoning_tokens is kept as reasoning.
|
|
1162
|
+
function usageOf(uf) {
|
|
1163
|
+
const [inp, cr, cw] = [pbNum(uf, 1), pbNum(uf, 3), pbNum(uf, 4)]
|
|
1164
|
+
return { input: Math.max(inp - cr - cw, 0), output: pbNum(uf, 2), cacheRead: cr, cacheWrite: cw, reasoning: pbNum(uf, 5) }
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1023
1167
|
// buildRun is the Run's first message, an AgentClientMessage with its
|
|
1024
1168
|
// run_request, and the blobs it names. The conversation state is the
|
|
1025
1169
|
// messages and one turn, which the server wants there to sample at all.
|
|
1026
|
-
|
|
1170
|
+
// conv is the conversation_id (conversationID), a new one when "". The
|
|
1171
|
+
// model goes as the CLI sends it: in Max Mode when maxMode is set (Cursor
|
|
1172
|
+
// refuses a model it serves only that way otherwise: "Max Mode
|
|
1173
|
+
// Required"), with the picker variant's parameters when it has them.
|
|
1174
|
+
function buildRun(msgs, lastUser, tools, model, conv = "", { maxMode = false, params = [] } = {}) {
|
|
1027
1175
|
const blobs = new Map()
|
|
1028
1176
|
const put = (b) => {
|
|
1029
1177
|
const id = createHash("sha256").update(b).digest()
|
|
@@ -1044,13 +1192,22 @@ function buildRun(msgs, lastUser, tools, model) {
|
|
|
1044
1192
|
mcp.bytes(1, d)
|
|
1045
1193
|
}
|
|
1046
1194
|
const action = pb().bytes(2, pb().bytes(2, rc)) // resume_action
|
|
1195
|
+
// ModelDetails: model_id, display_model_id, display_name, max_mode (7);
|
|
1196
|
+
// RequestedModel: model_id, max_mode (2), parameters (3)
|
|
1197
|
+
const details = pb().str(1, model).str(3, model).str(4, model)
|
|
1198
|
+
const requested = pb().str(1, model)
|
|
1199
|
+
if (maxMode) {
|
|
1200
|
+
details.varint(7, 1)
|
|
1201
|
+
requested.varint(2, 1)
|
|
1202
|
+
}
|
|
1203
|
+
for (const p of params) requested.bytes(3, pb().str(1, p.id).str(2, p.value))
|
|
1047
1204
|
const rr = pb()
|
|
1048
1205
|
.bytes(1, state)
|
|
1049
1206
|
.bytes(2, action)
|
|
1050
|
-
.bytes(3,
|
|
1207
|
+
.bytes(3, details)
|
|
1051
1208
|
.bytes(4, mcp)
|
|
1052
|
-
.str(5, randomUUID())
|
|
1053
|
-
.bytes(9,
|
|
1209
|
+
.str(5, conv || randomUUID())
|
|
1210
|
+
.bytes(9, requested)
|
|
1054
1211
|
.varint(19, 1) // inline images
|
|
1055
1212
|
return { first: pb().bytes(1, rr).done(), blobs }
|
|
1056
1213
|
}
|
|
@@ -1117,8 +1274,8 @@ function open(base, headers, signal) {
|
|
|
1117
1274
|
|
|
1118
1275
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
1119
1276
|
// iterator of the answer's pieces, or the error it failed with before any.
|
|
1120
|
-
async function runOnce({ tok, base, id, tools, conv, signal }) {
|
|
1121
|
-
const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id)
|
|
1277
|
+
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
|
|
1278
|
+
const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id, convID, { maxMode, params })
|
|
1122
1279
|
const o = await open(
|
|
1123
1280
|
base,
|
|
1124
1281
|
{
|
|
@@ -1191,10 +1348,14 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1191
1348
|
let calls = 0
|
|
1192
1349
|
let listed = 0
|
|
1193
1350
|
let said = 0
|
|
1194
|
-
|
|
1351
|
+
// wrote: the reply's text alone; a turn of thinking only says nothing
|
|
1352
|
+
let wrote = 0
|
|
1353
|
+
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
1195
1354
|
const finish = () => {
|
|
1196
1355
|
if (!usage.output) usage.output = Math.floor((said + 3) / 4)
|
|
1197
|
-
|
|
1356
|
+
// a guess only when Cursor counted nothing: a prompt read whole from
|
|
1357
|
+
// the cache leaves no uncached rest, and is no reason to guess
|
|
1358
|
+
if (!usage.input && !usage.cacheRead && !usage.cacheWrite) usage.input = estimate
|
|
1198
1359
|
const prompt = usage.input + usage.cacheRead + usage.cacheWrite
|
|
1199
1360
|
return {
|
|
1200
1361
|
stop: calls > 0 ? "tool_calls" : "stop",
|
|
@@ -1203,6 +1364,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1203
1364
|
completion_tokens: usage.output,
|
|
1204
1365
|
total_tokens: prompt + usage.output,
|
|
1205
1366
|
prompt_tokens_details: { cached_tokens: usage.cacheRead, cache_write_tokens: usage.cacheWrite },
|
|
1367
|
+
...(usage.reasoning ? { completion_tokens_details: { reasoning_tokens: usage.reasoning } } : {}),
|
|
1206
1368
|
},
|
|
1207
1369
|
}
|
|
1208
1370
|
}
|
|
@@ -1212,7 +1374,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1212
1374
|
if (f.end) {
|
|
1213
1375
|
const e = failure(200, f.data)
|
|
1214
1376
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
1215
|
-
else if (
|
|
1377
|
+
else if (wrote === 0 && calls === 0) yield { error: { status: 502, message: "an empty reply" } }
|
|
1216
1378
|
else yield finish()
|
|
1217
1379
|
return
|
|
1218
1380
|
}
|
|
@@ -1227,6 +1389,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1227
1389
|
const t = pbStr(uf, 1)
|
|
1228
1390
|
if (t) {
|
|
1229
1391
|
said += t.length
|
|
1392
|
+
wrote += t.length
|
|
1230
1393
|
yield { text: t }
|
|
1231
1394
|
}
|
|
1232
1395
|
break
|
|
@@ -1241,9 +1404,11 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1241
1404
|
break
|
|
1242
1405
|
}
|
|
1243
1406
|
case 14: // turn ended, with what it used
|
|
1244
|
-
usage =
|
|
1407
|
+
usage = usageOf(uf)
|
|
1245
1408
|
if (calls === 0) {
|
|
1246
|
-
|
|
1409
|
+
// nothing written and nothing called: an empty reply,
|
|
1410
|
+
// which the client asks again, not a finished turn
|
|
1411
|
+
yield wrote === 0 ? { error: { status: 502, message: "an empty reply" } } : finish()
|
|
1247
1412
|
return
|
|
1248
1413
|
}
|
|
1249
1414
|
break
|
|
@@ -1371,11 +1536,19 @@ function kept(res) {
|
|
|
1371
1536
|
}
|
|
1372
1537
|
|
|
1373
1538
|
// answer runs a chat completion on Cursor, the account kept (errorResponse).
|
|
1374
|
-
const answer = async (auth, chat, signal) => kept(await answerOf(auth, chat, signal))
|
|
1539
|
+
const answer = async (auth, chat, signal, session) => kept(await answerOf(auth, chat, signal, session))
|
|
1540
|
+
|
|
1541
|
+
// maxOnly are the ids Cursor said it serves in Max Mode only.
|
|
1542
|
+
const maxOnly = new Set()
|
|
1543
|
+
|
|
1544
|
+
// maxRequired is Cursor's refusal of a model asked for without Max Mode
|
|
1545
|
+
// (the CLI's MAX_MODE_REQUIRED): "Max Mode Required: The model "x"
|
|
1546
|
+
// requires Max Mode to be enabled. …"
|
|
1547
|
+
const maxRequired = (msg) => /max[ _]mode[ _]required|requires max mode/i.test(String(msg ?? ""))
|
|
1375
1548
|
|
|
1376
1549
|
// answerOf runs a chat completion on Cursor and answers it as one, streamed
|
|
1377
1550
|
// or not.
|
|
1378
|
-
async function answerOf(auth, chat, signal) {
|
|
1551
|
+
async function answerOf(auth, chat, signal, session = "") {
|
|
1379
1552
|
let tok
|
|
1380
1553
|
try {
|
|
1381
1554
|
tok = await tokenOf(auth)
|
|
@@ -1389,17 +1562,34 @@ async function answerOf(auth, chat, signal) {
|
|
|
1389
1562
|
} catch {}
|
|
1390
1563
|
const fast = chat.service_tier === "priority" || chat.service_tier === "fast"
|
|
1391
1564
|
let id = modelID(raw, model, chat.reasoning_effort ?? "", fast)
|
|
1392
|
-
|
|
1565
|
+
const entry = raw.find((m) => m.id === id)
|
|
1566
|
+
id = entry?.run ?? id
|
|
1393
1567
|
if (id === "auto") id = "default" // Cursor's pick, which its API calls default
|
|
1568
|
+
const params = entry?.params ?? []
|
|
1569
|
+
let maxMode = !!entry?.maxMode || maxOnly.has(id)
|
|
1394
1570
|
const tools = toolsOf(chat)
|
|
1395
1571
|
const conv = conversation(chat, tools)
|
|
1396
|
-
const
|
|
1397
|
-
let
|
|
1572
|
+
const convID = conversationID(chat, session)
|
|
1573
|
+
let base = await agentURL(tok, false)
|
|
1574
|
+
const once = () => runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params })
|
|
1575
|
+
let r = await once()
|
|
1398
1576
|
if (r.error && regional(r.error.message)) {
|
|
1399
1577
|
// the team moved, or the config was kept from before: once more with
|
|
1400
1578
|
// what the config says now
|
|
1401
1579
|
const fresh = await agentURL(tok, true)
|
|
1402
|
-
if (fresh !== base)
|
|
1580
|
+
if (fresh !== base) {
|
|
1581
|
+
base = fresh
|
|
1582
|
+
r = await once()
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (r.error && !maxMode && maxRequired(r.error.message)) {
|
|
1586
|
+
// a model Cursor serves only in Max Mode that neither list said so of:
|
|
1587
|
+
// in Max Mode, as the CLI turns it on for such a model, and so from
|
|
1588
|
+
// now on. An account Max Mode isn't open to is answered as Cursor
|
|
1589
|
+
// answers that.
|
|
1590
|
+
maxMode = true
|
|
1591
|
+
maxOnly.add(id)
|
|
1592
|
+
r = await once()
|
|
1403
1593
|
}
|
|
1404
1594
|
if (r.error) return errorResponse(r.error)
|
|
1405
1595
|
const it = r.events
|
|
@@ -1480,7 +1670,8 @@ export async function CursorAuthPlugin() {
|
|
|
1480
1670
|
} catch {
|
|
1481
1671
|
return errorResponse({ status: 400, message: "a request that isn't JSON" })
|
|
1482
1672
|
}
|
|
1483
|
-
|
|
1673
|
+
const session = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined)).get(SESSION) ?? ""
|
|
1674
|
+
return answer((await getAuth()) ?? auth, chat, init.signal ?? (input instanceof Request ? input.signal : undefined), session)
|
|
1484
1675
|
},
|
|
1485
1676
|
}
|
|
1486
1677
|
},
|
|
@@ -1503,6 +1694,15 @@ export async function CursorAuthPlugin() {
|
|
|
1503
1694
|
return { ...u, ...(plan ? { plan } : {}), signIn: "kept" }
|
|
1504
1695
|
},
|
|
1505
1696
|
},
|
|
1697
|
+
// the session the request is part of, for the conversation_id
|
|
1698
|
+
// (conversationID): magpie and OpenCode name it here, and the loader's
|
|
1699
|
+
// fetch reads it back. Every plugin's hook sees every provider's
|
|
1700
|
+
// requests, so it is put on Cursor's alone.
|
|
1701
|
+
async "chat.headers"(input, output) {
|
|
1702
|
+
if (input?.model?.providerID !== ID && input?.provider?.info?.id !== ID) return
|
|
1703
|
+
const s = sessionOf(input)
|
|
1704
|
+
if (s) output.headers[SESSION] = s
|
|
1705
|
+
},
|
|
1506
1706
|
async config(config) {
|
|
1507
1707
|
config.provider ??= {}
|
|
1508
1708
|
const was = config.provider[ID] ?? {}
|
|
@@ -1533,4 +1733,4 @@ export async function CursorAuthPlugin() {
|
|
|
1533
1733
|
}
|
|
1534
1734
|
|
|
1535
1735
|
// for tests
|
|
1536
|
-
export const _internal = { errorResponse, kept, poolBase, splitID, families, byEffort, offered, modelID, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
|
|
1736
|
+
export const _internal = { catalog, 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,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
8
|
"files": ["index.mjs", "README.md"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "magpie", "cursor"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/cursor" }
|
|
11
12
|
}
|