@magpie-community/opencode-cursor-auth 0.2.26 → 0.2.29
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 +12 -2
- package/index.mjs +117 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,8 +50,13 @@ 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. A native session's Run
|
|
55
|
+
sends only its KV replies (the history's blobs) that way; a tool result
|
|
56
|
+
or anything else waits for the appends before it, goes alone, and is
|
|
57
|
+
written once Cursor answered it, so it is never uncertain. A BidiAppend
|
|
58
|
+
with no answer in 60 s (more for a large one) ends the Run, as
|
|
59
|
+
cursor-agent does.
|
|
55
60
|
- A new Run imports the whole conversation as AI SDK messages kept as blobs
|
|
56
61
|
the server asks for. The caller's tools are MCP tools, listed in the
|
|
57
62
|
system prompt; the model calls them through Cursor's `CallDynamicTool`,
|
|
@@ -227,6 +232,11 @@ minutes), the way pi-cursor-sdk lists Cursor's catalog:
|
|
|
227
232
|
and `service_tier` counting as for a listed model. The list doesn't show
|
|
228
233
|
these ids: pick the `@<size>` model for a window that is the model's.
|
|
229
234
|
|
|
235
|
+
A chat request picks its variant from the same list without waiting for it
|
|
236
|
+
when it needn't: a list past its 10 minutes is used while a new one is
|
|
237
|
+
asked for, requests at once share one ask, and after an ask fails requests
|
|
238
|
+
go without the list for 5 minutes instead of each waiting for it again.
|
|
239
|
+
|
|
230
240
|
## Usage
|
|
231
241
|
|
|
232
242
|
magpie shows Cursor Models, Other Models and Total for the billing period.
|
package/index.mjs
CHANGED
|
@@ -981,22 +981,66 @@ function fitEffort(want, levels) {
|
|
|
981
981
|
// can't give is a failure: magpie keeps the list it had. A usable list it
|
|
982
982
|
// can't give leaves the picker as it is.
|
|
983
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()
|
|
984
988
|
async function usable(tok) {
|
|
985
989
|
const h = createHash("sha256").update(tok).digest("hex")
|
|
986
990
|
const had = lists.get(h)
|
|
987
991
|
if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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(() => [])
|
|
1000
1044
|
}
|
|
1001
1045
|
|
|
1002
1046
|
// named is the model and variant an id the list doesn't have stands for,
|
|
@@ -1993,10 +2037,12 @@ async function agentURL(tok, fresh) {
|
|
|
1993
2037
|
// had here (h2Blocked), the Runs after it go straight to HTTP/1.1 for
|
|
1994
2038
|
// H2.retryAfter, or until a region error sends one back (answerOf); a
|
|
1995
2039
|
// connection that failed (refused, no network) or a stream the server
|
|
1996
|
-
// 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, overlaps which messages
|
|
2042
|
+
// may be among them (openH1).
|
|
1997
2043
|
let h1Until = 0
|
|
1998
2044
|
const cancelledOpen = () => ({ error: { status: 410, message: "the request was cancelled" }, h2Failed: false })
|
|
1999
|
-
async function open(base, headers, signal) {
|
|
2045
|
+
async function open(base, headers, signal, atOnce = H1.atOnce, overlaps) {
|
|
2000
2046
|
if (signal?.aborted) return cancelledOpen()
|
|
2001
2047
|
if (Date.now() >= h1Until) {
|
|
2002
2048
|
const o = await openH2(base, headers, signal)
|
|
@@ -2010,7 +2056,7 @@ async function open(base, headers, signal) {
|
|
|
2010
2056
|
if (o.blocked) h1Until = Date.now() + H2.retryAfter
|
|
2011
2057
|
}
|
|
2012
2058
|
if (signal?.aborted) return cancelledOpen()
|
|
2013
|
-
return openH1(headers, signal)
|
|
2059
|
+
return openH1(headers, signal, atOnce, overlaps)
|
|
2014
2060
|
}
|
|
2015
2061
|
|
|
2016
2062
|
const H2 = {
|
|
@@ -2077,7 +2123,24 @@ function openH2(base, headers, signal) {
|
|
|
2077
2123
|
})
|
|
2078
2124
|
}
|
|
2079
2125
|
|
|
2080
|
-
const H1 = {
|
|
2126
|
+
const H1 = {
|
|
2127
|
+
appendWait: 60_000, // ms a BidiAppend may take before the Run fails, as cursor-agent's appendTimeoutMs
|
|
2128
|
+
// BidiAppends on their way at once, numbered in order, as cursor-agent
|
|
2129
|
+
// keeps them: one at a time, a new Run's history (a blob a message,
|
|
2130
|
+
// hundreds for a long conversation, all asked for at once) took a round
|
|
2131
|
+
// trip to Cursor each before the model could start. A native session's
|
|
2132
|
+
// Run sends only its KV replies that way (kvReply); each of its other
|
|
2133
|
+
// messages waits for every append before it, and goes alone (runNative).
|
|
2134
|
+
atOnce: 16,
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// kvReply is whether a client message (an AgentClientMessage, unframed) is
|
|
2138
|
+
// a KV reply, kv_client_message: a blob Cursor asked for, which nothing
|
|
2139
|
+
// else waits on. Cursor asks for a new Run's whole history at once.
|
|
2140
|
+
function kvReply(msg) {
|
|
2141
|
+
const b = Buffer.isBuffer(msg) ? msg : Buffer.from(msg ?? EMPTY)
|
|
2142
|
+
return b.length > 0 && b[0] === 0x1a // field 3, length-delimited
|
|
2143
|
+
}
|
|
2081
2144
|
|
|
2082
2145
|
// connectCode is Connect's code for an HTTP status that came with no
|
|
2083
2146
|
// Connect error of its own
|
|
@@ -2091,8 +2154,11 @@ function connectCode(status) {
|
|
|
2091
2154
|
// the stream it reads, Connect's frames as the HTTP/2 Run's. Its response
|
|
2092
2155
|
// is a 200 at once; an error Cursor says later, or a call that fails,
|
|
2093
2156
|
// ends the stream with Connect's end frame, which runOnce reads as the
|
|
2094
|
-
// Run's own.
|
|
2095
|
-
|
|
2157
|
+
// Run's own. overlaps says of a message whether it may go up while appends
|
|
2158
|
+
// before it are on their way (up to atOnce of them); one that may not waits
|
|
2159
|
+
// for all of them, goes alone, and its write's callback is its own append
|
|
2160
|
+
// answered.
|
|
2161
|
+
function openH1(headers, signal, atOnce = H1.atOnce, overlaps = () => true) {
|
|
2096
2162
|
if (signal?.aborted) return Promise.resolve(cancelledOpen())
|
|
2097
2163
|
const ac = new AbortController()
|
|
2098
2164
|
const id = headers["x-request-id"]
|
|
@@ -2100,15 +2166,32 @@ function openH1(headers, signal) {
|
|
|
2100
2166
|
let ended = false
|
|
2101
2167
|
let seq = 0
|
|
2102
2168
|
let reader
|
|
2169
|
+
const going = new Set()
|
|
2103
2170
|
const req = new Duplex({
|
|
2104
2171
|
read() {},
|
|
2105
2172
|
write(chunk, _enc, cb) {
|
|
2106
2173
|
// runOnce writes one whole frame each time
|
|
2107
2174
|
const msg = Buffer.from(chunk).subarray(5)
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2175
|
+
const go = () => {
|
|
2176
|
+
const p = append(msg, seq++)
|
|
2177
|
+
going.add(p)
|
|
2178
|
+
const gone = () => going.delete(p)
|
|
2179
|
+
p.then(gone, gone)
|
|
2180
|
+
return p
|
|
2181
|
+
}
|
|
2182
|
+
if (!overlaps(msg)) {
|
|
2183
|
+
// alone: after every append before it is answered, and answered
|
|
2184
|
+
// itself before the next write
|
|
2185
|
+
Promise.allSettled([...going]).then(go).then(() => cb(), (e) => cb(e))
|
|
2186
|
+
return
|
|
2187
|
+
}
|
|
2188
|
+
go()
|
|
2189
|
+
// Up to atOnce appends go at once; past that the write stays
|
|
2190
|
+
// buffered until one finishes, so callbacks and drain reflect the
|
|
2191
|
+
// pending upload rather than an unbounded promise chain. At one,
|
|
2192
|
+
// a write's callback is its own append answered.
|
|
2193
|
+
if (going.size < atOnce) return cb()
|
|
2194
|
+
Promise.race(going).then(() => cb(), (e) => cb(e))
|
|
2112
2195
|
},
|
|
2113
2196
|
})
|
|
2114
2197
|
const cleanup = () => {
|
|
@@ -2135,7 +2218,7 @@ function openH1(headers, signal) {
|
|
|
2135
2218
|
}
|
|
2136
2219
|
// append sends one message up. One that gets no answer by its deadline
|
|
2137
2220
|
// (more time for a large one) ends the Run, as cursor-agent's
|
|
2138
|
-
// bidi_append_deadline_exceeded does
|
|
2221
|
+
// bidi_append_deadline_exceeded does; so does one that fails.
|
|
2139
2222
|
const append = async (msg, n) => {
|
|
2140
2223
|
if (ended || ac.signal.aborted) return
|
|
2141
2224
|
const body = pb().str(1, msg.toString("hex")).bytes(2, pb().str(1, id).done()).varint(3, n).done()
|
|
@@ -2371,6 +2454,15 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
|
|
|
2371
2454
|
"x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
|
|
2372
2455
|
},
|
|
2373
2456
|
native.ac.signal,
|
|
2457
|
+
// Over HTTP/1.1 the history's blobs go up H1.atOnce at a time, as
|
|
2458
|
+
// cursor-agent sends them; one at a time, a Run with a long history
|
|
2459
|
+
// waited a round trip to api2 a blob before its first token (ysm119 on
|
|
2460
|
+
// Discord: 135 blobs, 37 s against 3 s over HTTP/2). Everything else
|
|
2461
|
+
// goes alone, after the appends before it were answered, and counts as
|
|
2462
|
+
// written only once Cursor answered it, so a tool result is never
|
|
2463
|
+
// uncertain.
|
|
2464
|
+
H1.atOnce,
|
|
2465
|
+
kvReply,
|
|
2374
2466
|
)
|
|
2375
2467
|
if (o.error) {
|
|
2376
2468
|
native.fail(o.error)
|
|
@@ -2872,10 +2964,7 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2872
2964
|
if (prepared.owner) r = prepared.owner.resume({ ...prepared, estimate: conversation(chat, tools).estimate }, signal)
|
|
2873
2965
|
const model = String(chat.model ?? "")
|
|
2874
2966
|
if (!r) {
|
|
2875
|
-
|
|
2876
|
-
try {
|
|
2877
|
-
raw = await usable(tok)
|
|
2878
|
-
} catch {}
|
|
2967
|
+
const raw = await listFor(tok)
|
|
2879
2968
|
// fast only when asked: a service_tier of priority or fast is on; any
|
|
2880
2969
|
// other tier, or none, is off, unless the id is a fast one of Cursor's own
|
|
2881
2970
|
const tier = chat.service_tier
|
|
@@ -3106,4 +3195,4 @@ export async function CursorAuthPlugin() {
|
|
|
3106
3195
|
}
|
|
3107
3196
|
|
|
3108
3197
|
// for tests
|
|
3109
|
-
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 }
|
|
3198
|
+
export const _internal = { kvReply, 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.29",
|
|
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",
|