@magpie-community/opencode-cursor-auth 0.2.25 → 0.2.28
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 +14 -3
- package/index.mjs +95 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,8 +50,11 @@ plugin's `fetch` answers them on Cursor's agent API:
|
|
|
50
50
|
after it go straight to HTTP/1.1 for 10 minutes; a failed connection
|
|
51
51
|
(refused, no network) or a stream the server refused leaves the next Run
|
|
52
52
|
to try HTTP/2 again. A region error over HTTP/1.1 sends the Run back to
|
|
53
|
-
HTTP/2 at the region's agent host.
|
|
54
|
-
|
|
53
|
+
HTTP/2 at the region's agent host. BidiAppends go up numbered in order,
|
|
54
|
+
up to 16 at a time as cursor-agent sends them (one at a time for a
|
|
55
|
+
native session's Run, whose tool results must be certain). A BidiAppend
|
|
56
|
+
with no answer in 60 s (more for a large one) ends the Run, as
|
|
57
|
+
cursor-agent does.
|
|
55
58
|
- A new Run imports the whole conversation as AI SDK messages kept as blobs
|
|
56
59
|
the server asks for. The caller's tools are MCP tools, listed in the
|
|
57
60
|
system prompt; the model calls them through Cursor's `CallDynamicTool`,
|
|
@@ -210,7 +213,10 @@ minutes), the way pi-cursor-sdk lists Cursor's catalog:
|
|
|
210
213
|
them), fast the request's `service_tier`.
|
|
211
214
|
- An id for a model with thinking that the account lists only without it
|
|
212
215
|
(`claude-haiku-5-5-thinking` when the list has Claude Haiku 5.5 alone),
|
|
213
|
-
or the other way round, runs as the model the account has.
|
|
216
|
+
or the other way round, runs as the model the account has. So does a
|
|
217
|
+
picker model whose default thinks while the account lists it only
|
|
218
|
+
without thinking (`claude-haiku-5-5`), or the other way round: each of
|
|
219
|
+
its variant ids goes as the id the account lists.
|
|
214
220
|
- An `@<size>` id the list no longer has (`claude-opus-5-5@1m` when only
|
|
215
221
|
300k is listed, or the model is listed unsized) runs at the model's
|
|
216
222
|
nearest size, in Max Mode when the size asked for is over the model's
|
|
@@ -224,6 +230,11 @@ minutes), the way pi-cursor-sdk lists Cursor's catalog:
|
|
|
224
230
|
and `service_tier` counting as for a listed model. The list doesn't show
|
|
225
231
|
these ids: pick the `@<size>` model for a window that is the model's.
|
|
226
232
|
|
|
233
|
+
A chat request picks its variant from the same list without waiting for it
|
|
234
|
+
when it needn't: a list past its 10 minutes is used while a new one is
|
|
235
|
+
asked for, requests at once share one ask, and after an ask fails requests
|
|
236
|
+
go without the list for 5 minutes instead of each waiting for it again.
|
|
237
|
+
|
|
227
238
|
## Usage
|
|
228
239
|
|
|
229
240
|
magpie shows Cursor Models, Other Models and Total for the billing period.
|
package/index.mjs
CHANGED
|
@@ -827,7 +827,14 @@ function modelsOf(picker, ids, listed = []) {
|
|
|
827
827
|
const def = defaultVariant({ variants })
|
|
828
828
|
const { pid, levels: all } = effortsOf(defs)
|
|
829
829
|
const levels = Object.fromEntries(Object.entries(all).filter(([, value]) => has(pid, value)))
|
|
830
|
-
|
|
830
|
+
// the usable ids of its default's family, else of the same model with
|
|
831
|
+
// thinking or without it: a Team's picker can default Claude Haiku 5.5
|
|
832
|
+
// to thinking while cursor-agent lists it as claude-haiku-5-5 alone
|
|
833
|
+
// (ysm119 on Discord, after 0.2.25), and a variant id it lacks is then
|
|
834
|
+
// asked as that one, as named() takes such an id
|
|
835
|
+
const own = familyOf(def?.legacySlug || m.name).family
|
|
836
|
+
const bare = own.replace(/-thinking$/, "")
|
|
837
|
+
const fam = fams.get(own) ?? fams.get(bare) ?? fams.get(bare + "-thinking") ?? []
|
|
831
838
|
const base = { run: m.name, title: clean(m.clientDisplayName || m.name), pid, levels, fast: !!defs.fast?.length, variants, def, partial, model: m, fam, ids }
|
|
832
839
|
const contexts = (defs.context ?? []).filter((c) => has("context", c))
|
|
833
840
|
if (contexts.length) {
|
|
@@ -974,22 +981,66 @@ function fitEffort(want, levels) {
|
|
|
974
981
|
// can't give is a failure: magpie keeps the list it had. A usable list it
|
|
975
982
|
// can't give leaves the picker as it is.
|
|
976
983
|
const lists = new Map()
|
|
984
|
+
// listings are the lists being asked for now, by token: callers at once
|
|
985
|
+
// share one ask; listFailed when a token's last ask failed
|
|
986
|
+
const listings = new Map()
|
|
987
|
+
const listFailed = new Map()
|
|
977
988
|
async function usable(tok) {
|
|
978
989
|
const h = createHash("sha256").update(tok).digest("hex")
|
|
979
990
|
const had = lists.get(h)
|
|
980
991
|
if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
992
|
+
return listing(tok, h)
|
|
993
|
+
}
|
|
994
|
+
function listing(tok, h) {
|
|
995
|
+
let p = listings.get(h)
|
|
996
|
+
if (p) return p
|
|
997
|
+
p = (async () => {
|
|
998
|
+
const [j, listed] = await Promise.all([
|
|
999
|
+
unary(API, "aiserver.v1.AiService/AvailableModels", tok, { useModelParameters: true, doNotUseMarkdown: true }, 30_000),
|
|
1000
|
+
unary(API, "agent.v1.AgentService/GetUsableModels", tok, {}, 8_000).then(
|
|
1001
|
+
(u) => (Array.isArray(u?.models) ? u.models : []),
|
|
1002
|
+
() => [],
|
|
1003
|
+
),
|
|
1004
|
+
])
|
|
1005
|
+
const ids = new Set(listed.flatMap((m) => [m?.modelId, m?.displayModelId]).filter(Boolean))
|
|
1006
|
+
const raw = modelsOf(Array.isArray(j?.models) ? j.models : [], ids.size ? ids : null, listed)
|
|
1007
|
+
if (!raw.length) throw new Error("Cursor listed no models")
|
|
1008
|
+
lists.set(h, { at: Date.now(), raw })
|
|
1009
|
+
listFailed.delete(h)
|
|
1010
|
+
return raw
|
|
1011
|
+
})()
|
|
1012
|
+
.catch((e) => {
|
|
1013
|
+
listFailed.set(h, Date.now())
|
|
1014
|
+
throw e
|
|
1015
|
+
})
|
|
1016
|
+
.finally(() => listings.delete(h))
|
|
1017
|
+
listings.set(h, p)
|
|
1018
|
+
return p
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// LIST_RETRY is how long a request goes on without the list after Cursor
|
|
1022
|
+
// couldn't give it, before it waits for an ask again.
|
|
1023
|
+
const LIST_RETRY = 5 * 60_000
|
|
1024
|
+
|
|
1025
|
+
// listFor is the list a chat request picks its variant from. It is asked
|
|
1026
|
+
// before the Run, so a request never waits on it when it needn't: a list
|
|
1027
|
+
// past MODELS_KEEP is used as it is while a new one is asked for, and
|
|
1028
|
+
// after a failed ask the requests go without one for LIST_RETRY, as the
|
|
1029
|
+
// failed ask left them, instead of each waiting up to AvailableModels'
|
|
1030
|
+
// 30 s again before its Run (ysm119 on Discord: 33–36 s to the first
|
|
1031
|
+
// token on every Opus request). Only a token with no list at all, and no
|
|
1032
|
+
// failure lately, waits for it.
|
|
1033
|
+
async function listFor(tok) {
|
|
1034
|
+
const h = createHash("sha256").update(tok).digest("hex")
|
|
1035
|
+
const had = lists.get(h)
|
|
1036
|
+
if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
|
|
1037
|
+
if (had) {
|
|
1038
|
+
listing(tok, h).catch(() => {})
|
|
1039
|
+
return had.raw
|
|
1040
|
+
}
|
|
1041
|
+
const failed = listFailed.get(h)
|
|
1042
|
+
if (failed && Date.now() - failed < LIST_RETRY) return []
|
|
1043
|
+
return listing(tok, h).catch(() => [])
|
|
993
1044
|
}
|
|
994
1045
|
|
|
995
1046
|
// named is the model and variant an id the list doesn't have stands for,
|
|
@@ -1986,10 +2037,11 @@ async function agentURL(tok, fresh) {
|
|
|
1986
2037
|
// had here (h2Blocked), the Runs after it go straight to HTTP/1.1 for
|
|
1987
2038
|
// H2.retryAfter, or until a region error sends one back (answerOf); a
|
|
1988
2039
|
// connection that failed (refused, no network) or a stream the server
|
|
1989
|
-
// refused leaves the next Run to try HTTP/2 again.
|
|
2040
|
+
// refused leaves the next Run to try HTTP/2 again. atOnce is how many
|
|
2041
|
+
// BidiAppends openH1 may have on their way at once.
|
|
1990
2042
|
let h1Until = 0
|
|
1991
2043
|
const cancelledOpen = () => ({ error: { status: 410, message: "the request was cancelled" }, h2Failed: false })
|
|
1992
|
-
async function open(base, headers, signal) {
|
|
2044
|
+
async function open(base, headers, signal, atOnce = H1.atOnce) {
|
|
1993
2045
|
if (signal?.aborted) return cancelledOpen()
|
|
1994
2046
|
if (Date.now() >= h1Until) {
|
|
1995
2047
|
const o = await openH2(base, headers, signal)
|
|
@@ -2003,7 +2055,7 @@ async function open(base, headers, signal) {
|
|
|
2003
2055
|
if (o.blocked) h1Until = Date.now() + H2.retryAfter
|
|
2004
2056
|
}
|
|
2005
2057
|
if (signal?.aborted) return cancelledOpen()
|
|
2006
|
-
return openH1(headers, signal)
|
|
2058
|
+
return openH1(headers, signal, atOnce)
|
|
2007
2059
|
}
|
|
2008
2060
|
|
|
2009
2061
|
const H2 = {
|
|
@@ -2070,7 +2122,15 @@ function openH2(base, headers, signal) {
|
|
|
2070
2122
|
})
|
|
2071
2123
|
}
|
|
2072
2124
|
|
|
2073
|
-
const H1 = {
|
|
2125
|
+
const H1 = {
|
|
2126
|
+
appendWait: 60_000, // ms a BidiAppend may take before the Run fails, as cursor-agent's appendTimeoutMs
|
|
2127
|
+
// BidiAppends on their way at once, numbered in order, as cursor-agent
|
|
2128
|
+
// keeps them: one at a time, a new Run's history (a blob a message,
|
|
2129
|
+
// hundreds for a long conversation, all asked for at once) took a round
|
|
2130
|
+
// trip to Cursor each before the model could start. A native session's
|
|
2131
|
+
// Run keeps them one at a time (runNative).
|
|
2132
|
+
atOnce: 16,
|
|
2133
|
+
}
|
|
2074
2134
|
|
|
2075
2135
|
// connectCode is Connect's code for an HTTP status that came with no
|
|
2076
2136
|
// Connect error of its own
|
|
@@ -2085,7 +2145,7 @@ function connectCode(status) {
|
|
|
2085
2145
|
// is a 200 at once; an error Cursor says later, or a call that fails,
|
|
2086
2146
|
// ends the stream with Connect's end frame, which runOnce reads as the
|
|
2087
2147
|
// Run's own.
|
|
2088
|
-
function openH1(headers, signal) {
|
|
2148
|
+
function openH1(headers, signal, atOnce = H1.atOnce) {
|
|
2089
2149
|
if (signal?.aborted) return Promise.resolve(cancelledOpen())
|
|
2090
2150
|
const ac = new AbortController()
|
|
2091
2151
|
const id = headers["x-request-id"]
|
|
@@ -2093,15 +2153,23 @@ function openH1(headers, signal) {
|
|
|
2093
2153
|
let ended = false
|
|
2094
2154
|
let seq = 0
|
|
2095
2155
|
let reader
|
|
2156
|
+
const going = new Set()
|
|
2096
2157
|
const req = new Duplex({
|
|
2097
2158
|
read() {},
|
|
2098
2159
|
write(chunk, _enc, cb) {
|
|
2099
2160
|
// runOnce writes one whole frame each time
|
|
2100
2161
|
const msg = Buffer.from(chunk).subarray(5)
|
|
2101
2162
|
const n = seq++
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2163
|
+
const p = append(msg, n)
|
|
2164
|
+
going.add(p)
|
|
2165
|
+
const gone = () => going.delete(p)
|
|
2166
|
+
p.then(gone, gone)
|
|
2167
|
+
// Up to atOnce appends go at once; past that the write stays
|
|
2168
|
+
// buffered until one finishes, so callbacks and drain reflect the
|
|
2169
|
+
// pending upload rather than an unbounded promise chain. At one,
|
|
2170
|
+
// a write's callback is its own append answered.
|
|
2171
|
+
if (going.size < atOnce) return cb()
|
|
2172
|
+
Promise.race(going).then(() => cb(), (e) => cb(e))
|
|
2105
2173
|
},
|
|
2106
2174
|
})
|
|
2107
2175
|
const cleanup = () => {
|
|
@@ -2128,7 +2196,7 @@ function openH1(headers, signal) {
|
|
|
2128
2196
|
}
|
|
2129
2197
|
// append sends one message up. One that gets no answer by its deadline
|
|
2130
2198
|
// (more time for a large one) ends the Run, as cursor-agent's
|
|
2131
|
-
// bidi_append_deadline_exceeded does
|
|
2199
|
+
// bidi_append_deadline_exceeded does; so does one that fails.
|
|
2132
2200
|
const append = async (msg, n) => {
|
|
2133
2201
|
if (ended || ac.signal.aborted) return
|
|
2134
2202
|
const body = pb().str(1, msg.toString("hex")).bytes(2, pb().str(1, id).done()).varint(3, n).done()
|
|
@@ -2364,6 +2432,9 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
|
|
|
2364
2432
|
"x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
|
|
2365
2433
|
},
|
|
2366
2434
|
native.ac.signal,
|
|
2435
|
+
// one at a time: a native session counts a write as done only when
|
|
2436
|
+
// Cursor answered its own append, so a tool result is never uncertain
|
|
2437
|
+
1,
|
|
2367
2438
|
)
|
|
2368
2439
|
if (o.error) {
|
|
2369
2440
|
native.fail(o.error)
|
|
@@ -2865,10 +2936,7 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2865
2936
|
if (prepared.owner) r = prepared.owner.resume({ ...prepared, estimate: conversation(chat, tools).estimate }, signal)
|
|
2866
2937
|
const model = String(chat.model ?? "")
|
|
2867
2938
|
if (!r) {
|
|
2868
|
-
|
|
2869
|
-
try {
|
|
2870
|
-
raw = await usable(tok)
|
|
2871
|
-
} catch {}
|
|
2939
|
+
const raw = await listFor(tok)
|
|
2872
2940
|
// fast only when asked: a service_tier of priority or fast is on; any
|
|
2873
2941
|
// other tier, or none, is off, unless the id is a fast one of Cursor's own
|
|
2874
2942
|
const tier = chat.service_tier
|
|
@@ -3099,4 +3167,4 @@ export async function CursorAuthPlugin() {
|
|
|
3099
3167
|
}
|
|
3100
3168
|
|
|
3101
3169
|
// for tests
|
|
3102
|
-
export const _internal = { keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),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 }
|
|
3170
|
+
export const _internal = { listFor, lists, keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),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.28",
|
|
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",
|