@magpie-community/opencode-cursor-auth 0.2.13 → 0.2.14
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 +45 -2
- package/index.mjs +420 -84
- package/native-session.mjs +845 -0
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
// a conversation, the caller's system prompt at its head; the Run only
|
|
22
22
|
// resumes it. The caller's tools are MCP tools, which the model calls
|
|
23
23
|
// through Cursor's CallDynamicTool: the server hands each call to the
|
|
24
|
-
// client to run, and once it has said how many a step made, the
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// read) is run.
|
|
24
|
+
// client to run, and once it has said how many a step made, the calls
|
|
25
|
+
// go back to OpenCode. When the owner is available their results continue
|
|
26
|
+
// that Run; otherwise the caller's history starts a stateless Run. Nothing
|
|
27
|
+
// else the server asks the client to run (a shell, a file read) is run.
|
|
28
28
|
//
|
|
29
29
|
// OpenCode speaks chat completions to it (@ai-sdk/openai-compatible); the
|
|
30
30
|
// fetch here answers them.
|
|
31
|
+
import { NATIVE_LIMITS, nativeSessions, NativeSessionError, nativeFields, nativeMcpArgs } from "./native-session.mjs"
|
|
32
|
+
import { isDeepStrictEqual } from "node:util"
|
|
31
33
|
import http2 from "node:http2"
|
|
32
34
|
import { STATUS_CODES } from "node:http"
|
|
33
35
|
import { gunzipSync } from "node:zlib"
|
|
@@ -1396,14 +1398,43 @@ function addUsage(a, b) {
|
|
|
1396
1398
|
return a
|
|
1397
1399
|
}
|
|
1398
1400
|
|
|
1399
|
-
// owe adds to the account/conversation's debt.
|
|
1400
|
-
|
|
1401
|
-
|
|
1401
|
+
// owe adds to the account/conversation's debt. A native Run bounds older
|
|
1402
|
+
// event timestamps before its start: its authoritative final must never be
|
|
1403
|
+
// collected as an earlier Run. Keep that bound when a later stateless Run
|
|
1404
|
+
// adds unbounded debt. Ordinary stateless debt retains its original shape.
|
|
1405
|
+
const DEBT_GROUP_LIMIT = 64
|
|
1406
|
+
function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null, before }) {
|
|
1407
|
+
for (const [k, d] of owed) {
|
|
1408
|
+
if (d.groups) {
|
|
1409
|
+
d.groups = d.groups.filter((g) => Date.now() - g.since <= STEP_WAIT.owedFor)
|
|
1410
|
+
d.n = d.groups.reduce((sum, g) => sum + g.n, 0)
|
|
1411
|
+
if (Date.now() - d.since > STEP_WAIT.owedFor) d.carry = null
|
|
1412
|
+
if (!d.n && !d.carry) owed.delete(k)
|
|
1413
|
+
else if (d.groups.length) d.since = Math.min(...d.groups.map((g) => g.since))
|
|
1414
|
+
} else if (Date.now() - d.since > STEP_WAIT.owedFor) owed.delete(k)
|
|
1415
|
+
}
|
|
1402
1416
|
if (n <= 0 && !carry) return
|
|
1403
1417
|
const d = owed.get(key)
|
|
1404
|
-
if (!d) return void owed.set(key, { since, n, ready, carry: carry && { ...carry } })
|
|
1405
|
-
d.
|
|
1406
|
-
|
|
1418
|
+
if (!d) return void owed.set(key, { since, n, ready, carry: carry && { ...carry }, ...(before !== undefined ? { before } : {}) })
|
|
1419
|
+
if (n > 0 && (d.groups || d.before !== undefined || before !== undefined)) {
|
|
1420
|
+
const groups = d.groups ?? (d.n > 0 ? [{ since: d.since, n: d.n, ready: d.ready, before: d.before }] : [])
|
|
1421
|
+
const same = groups.find((g) => g.before === before)
|
|
1422
|
+
if (same) {
|
|
1423
|
+
same.since = Math.min(same.since, since)
|
|
1424
|
+
same.n += n
|
|
1425
|
+
same.ready = Math.min(same.ready, ready)
|
|
1426
|
+
} else groups.push({ since, n, ready, before })
|
|
1427
|
+
// As with expired dashboard debt, overflow stays unknown, never estimated
|
|
1428
|
+
// or merged across bounds into an authoritative native Run's event.
|
|
1429
|
+
groups.sort((a, b) => a.since - b.since)
|
|
1430
|
+
d.groups = groups.slice(-DEBT_GROUP_LIMIT)
|
|
1431
|
+
delete d.before
|
|
1432
|
+
d.n = d.groups.reduce((sum, g) => sum + g.n, 0)
|
|
1433
|
+
d.since = Math.min(...d.groups.map((g) => g.since))
|
|
1434
|
+
} else {
|
|
1435
|
+
d.since = Math.min(d.since, since)
|
|
1436
|
+
d.n += n
|
|
1437
|
+
}
|
|
1407
1438
|
d.ready = Math.min(d.ready, ready)
|
|
1408
1439
|
d.carry = addUsage(d.carry, carry)
|
|
1409
1440
|
}
|
|
@@ -1414,7 +1445,7 @@ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null
|
|
|
1414
1445
|
// when the account can't read them.
|
|
1415
1446
|
// Polling, they are looked for as STEP_WAIT says, from `first` on, until
|
|
1416
1447
|
// all show; else asked for once.
|
|
1417
|
-
async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_WAIT.first, signal } = {}) {
|
|
1448
|
+
async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_WAIT.first, signal, before = () => Infinity, groups, abortFetch = false } = {}) {
|
|
1418
1449
|
const key = usageKey(tok, conv)
|
|
1419
1450
|
const end = Date.now() + STEP_WAIT.until
|
|
1420
1451
|
const wait = (ms) => new Promise((r) => {
|
|
@@ -1426,6 +1457,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
|
|
|
1426
1457
|
let usage = null
|
|
1427
1458
|
let got = 0
|
|
1428
1459
|
const events = []
|
|
1460
|
+
const counts = groups?.map(() => 0)
|
|
1429
1461
|
if (poll) await wait(first)
|
|
1430
1462
|
while (!signal?.aborted) {
|
|
1431
1463
|
try {
|
|
@@ -1433,19 +1465,26 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
|
|
|
1433
1465
|
method: "POST",
|
|
1434
1466
|
headers: await headersFor(tok),
|
|
1435
1467
|
body: JSON.stringify({ teamId: 0, startDate: String(since - 60_000), endDate: String(Date.now() + 60_000), page: 1, pageSize: 50 }),
|
|
1436
|
-
signal: AbortSignal.timeout(STEP_WAIT.ask),
|
|
1468
|
+
signal: abortFetch && signal ? AbortSignal.any([signal, AbortSignal.timeout(STEP_WAIT.ask)]) : AbortSignal.timeout(STEP_WAIT.ask),
|
|
1437
1469
|
})
|
|
1438
1470
|
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
1439
1471
|
// not this account's to read
|
|
1440
1472
|
unreadable.set(tokKey(tok), Date.now())
|
|
1441
|
-
return { usage, events, got, refused: true }
|
|
1473
|
+
return { usage, events, got, counts, refused: true }
|
|
1442
1474
|
}
|
|
1443
1475
|
const evs = res.ok ? ((await res.json())?.usageEventsDisplay ?? []) : []
|
|
1444
1476
|
const found = evs
|
|
1445
1477
|
.filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(key + "\0" + e.timestamp))
|
|
1446
1478
|
.sort((a, b) => num(a.timestamp) - num(b.timestamp))
|
|
1447
|
-
.slice(0, want - got)
|
|
1448
1479
|
for (const ev of found) {
|
|
1480
|
+
if (got >= want) break
|
|
1481
|
+
const at = num(ev.timestamp)
|
|
1482
|
+
// found already excludes timestamps claimed by earlier fetches. Two
|
|
1483
|
+
// distinct events in this response may share a millisecond; count both.
|
|
1484
|
+
if (at >= before()) continue
|
|
1485
|
+
const group = groups?.findIndex((g, i) => counts[i] < g.n && at >= g.since - 1000 && at < (g.before ?? Infinity))
|
|
1486
|
+
if (groups && group === -1) continue
|
|
1487
|
+
if (groups) counts[group]++
|
|
1449
1488
|
claimed.set(key + "\0" + ev.timestamp, Date.now())
|
|
1450
1489
|
const t = ev.tokenUsage
|
|
1451
1490
|
const u = { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
|
|
@@ -1458,7 +1497,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
|
|
|
1458
1497
|
if (!poll || Date.now() + STEP_WAIT.every > end) break
|
|
1459
1498
|
await wait(STEP_WAIT.every)
|
|
1460
1499
|
}
|
|
1461
|
-
return { usage, events, got, refused: false }
|
|
1500
|
+
return { usage, events, got, counts, refused: false }
|
|
1462
1501
|
}
|
|
1463
1502
|
|
|
1464
1503
|
// stepUsage is the usage of the Run of conversation conv started at since,
|
|
@@ -1472,22 +1511,35 @@ async function stepUsage(tok, conv, since, signal) {
|
|
|
1472
1511
|
// what was carried alone (carry) and the events found (events), and whether any is
|
|
1473
1512
|
// still owed (short), which is owed on. What comes back once the Run that
|
|
1474
1513
|
// asked is over (over() true) is carried to the next instead.
|
|
1475
|
-
function collect(tok, conv, { poll = false, first, signal, over = () => false } = {}) {
|
|
1514
|
+
function collect(tok, conv, { poll = false, first, signal, over = () => false, abortFetch = false } = {}) {
|
|
1476
1515
|
const key = usageKey(tok, conv)
|
|
1477
1516
|
const d = owed.get(key)
|
|
1478
1517
|
if (!d) return Promise.resolve({ usage: null, carry: null, events: [], short: false })
|
|
1479
1518
|
owed.delete(key)
|
|
1480
1519
|
const p = (async () => {
|
|
1520
|
+
const groups = (d.groups ?? [d]).filter((g) => (g.before === undefined && !d.groups) || Date.now() - g.since <= STEP_WAIT.owedFor)
|
|
1521
|
+
const want = groups.reduce((sum, g) => sum + g.n, 0)
|
|
1481
1522
|
let r = { usage: null, events: [], got: 0 }
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1484
|
-
|
|
1523
|
+
if (want > 0 && canRead(tok)) r = await usageEvents(tok, conv, Math.min(...groups.map((g) => g.since)), want, {
|
|
1524
|
+
poll, first, signal, abortFetch, before: () => d.before ?? Infinity, ...(d.groups ? { groups } : {}),
|
|
1525
|
+
}).catch(() => r)
|
|
1526
|
+
let short = false
|
|
1527
|
+
for (const [i, g] of groups.entries()) {
|
|
1528
|
+
const remaining = g.n - (d.groups ? r.counts?.[i] ?? 0 : r.got)
|
|
1529
|
+
if (remaining > 0 && canRead(tok)) {
|
|
1530
|
+
short = true
|
|
1531
|
+
owe(key, { since: g.since, n: remaining, ready: g.ready, before: g.before })
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1485
1534
|
const carry = d.carry && { ...d.carry }
|
|
1486
1535
|
const usage = addUsage(d.carry, r.usage)
|
|
1487
1536
|
if (!over()) return { usage, carry, events: r.events ?? [], short }
|
|
1488
1537
|
owe(key, { carry: usage })
|
|
1489
1538
|
return { usage: null, carry: null, events: [], short }
|
|
1490
1539
|
})()
|
|
1540
|
+
// Native admission may happen while an older collector is still in flight.
|
|
1541
|
+
// Its dynamic event bound must apply before that response is claimed too.
|
|
1542
|
+
p.debt = d
|
|
1491
1543
|
const set = flights.get(key) ?? new Set()
|
|
1492
1544
|
flights.set(key, set.add(p))
|
|
1493
1545
|
p.finally(() => {
|
|
@@ -1517,14 +1569,18 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false }
|
|
|
1517
1569
|
// owed(), apart. A Run of its conversation begins only once the one
|
|
1518
1570
|
// before has ended, so the two never meet; were the time off, the step's
|
|
1519
1571
|
// count would be told in the wrong field, never twice nor lost.
|
|
1520
|
-
function ledger(tok, conv, since, signal, kept) {
|
|
1572
|
+
function ledger(tok, conv, since, signal, kept, { before } = {}) {
|
|
1521
1573
|
const key = tokKey(tok)
|
|
1522
1574
|
const debtKey = usageKey(tok, conv)
|
|
1575
|
+
if (kept && before !== undefined) {
|
|
1576
|
+
const debts = [owed.get(debtKey), ...[...(flights.get(debtKey) ?? [])].map((p) => p.debt)]
|
|
1577
|
+
for (const d of debts) for (const g of d?.groups ?? (d ? [d] : [])) g.before = Math.min(g.before ?? Infinity, before)
|
|
1578
|
+
}
|
|
1523
1579
|
let mine = null // this Run's own, collected, not given out yet
|
|
1524
1580
|
let theirs = null // earlier Runs', collected, not given out yet
|
|
1525
1581
|
let over = false
|
|
1526
1582
|
const gather = (opts) =>
|
|
1527
|
-
collect(tok, conv, { ...opts, over: () => over }).then((r) => {
|
|
1583
|
+
collect(tok, conv, { ...opts, over: () => over, abortFetch: before !== undefined }).then((r) => {
|
|
1528
1584
|
theirs = addUsage(theirs, r.carry)
|
|
1529
1585
|
for (const e of r.events ?? []) {
|
|
1530
1586
|
if (e.at >= since - 1000) mine = addUsage(mine, e.usage)
|
|
@@ -1876,6 +1932,206 @@ function openH1(headers, signal) {
|
|
|
1876
1932
|
return Promise.resolve(signal?.aborted ? cancelledOpen() : { session: { close, destroy: close }, req: Object.assign(req, { close }), status: 200, h1: true })
|
|
1877
1933
|
}
|
|
1878
1934
|
|
|
1935
|
+
// Every outbound frame, including KV replies and heartbeats, uses the same
|
|
1936
|
+
// bounded FIFO. Admission is synchronous; a result is reserved only once even
|
|
1937
|
+
// when a later write fails. Count bytes until both callback and drain complete.
|
|
1938
|
+
function nativeTransport(req, close, fail, limits = NATIVE_LIMITS) {
|
|
1939
|
+
const outbound = { bytes: 0, messages: 0 }
|
|
1940
|
+
const queue = []
|
|
1941
|
+
let closed = false, flushing = false, rejectWrite
|
|
1942
|
+
const flush = async () => {
|
|
1943
|
+
if (closed || flushing) return
|
|
1944
|
+
flushing = true
|
|
1945
|
+
try {
|
|
1946
|
+
while (!closed && queue.length) {
|
|
1947
|
+
const packet = queue.shift()
|
|
1948
|
+
await new Promise((resolve, reject) => {
|
|
1949
|
+
let written = false, drained = false, started = false, settled = false
|
|
1950
|
+
const finish = (error) => {
|
|
1951
|
+
if (settled) return
|
|
1952
|
+
settled = true
|
|
1953
|
+
rejectWrite = undefined
|
|
1954
|
+
req.removeListener("drain", drain)
|
|
1955
|
+
req.removeListener("error", failed)
|
|
1956
|
+
req.removeListener("close", lost)
|
|
1957
|
+
error ? reject(error) : resolve()
|
|
1958
|
+
}
|
|
1959
|
+
const check = () => { if (started && written && drained) finish() }
|
|
1960
|
+
const drain = () => { drained = true; check() }
|
|
1961
|
+
const failed = (e) => finish(e)
|
|
1962
|
+
const lost = () => finish(new NativeSessionError(502, "native transport closed during a write; its outcome is uncertain"))
|
|
1963
|
+
rejectWrite = finish
|
|
1964
|
+
req.once("drain", drain)
|
|
1965
|
+
req.once("error", failed)
|
|
1966
|
+
req.once("close", lost)
|
|
1967
|
+
try {
|
|
1968
|
+
drained = req.write(packet, (e) => {
|
|
1969
|
+
if (e) return finish(e)
|
|
1970
|
+
written = true
|
|
1971
|
+
check()
|
|
1972
|
+
}) || drained
|
|
1973
|
+
started = true
|
|
1974
|
+
if (drained) req.removeListener("drain", drain)
|
|
1975
|
+
check()
|
|
1976
|
+
} catch (e) { finish(e) }
|
|
1977
|
+
})
|
|
1978
|
+
if (!closed) { outbound.bytes -= packet.length; outbound.messages-- }
|
|
1979
|
+
}
|
|
1980
|
+
} catch (e) {
|
|
1981
|
+
if (!closed) fail(e)
|
|
1982
|
+
} finally { flushing = false }
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
outbound,
|
|
1986
|
+
send(message) {
|
|
1987
|
+
if (closed || req.destroyed || !req.writable) throw new NativeSessionError(502, "native transport is no longer writable")
|
|
1988
|
+
const size = message.length + 5
|
|
1989
|
+
if (outbound.bytes + size > limits.outboundBytes) throw new NativeSessionError(503, "native outbound bytes limit reached")
|
|
1990
|
+
if (outbound.messages + 1 > limits.outboundMessages) throw new NativeSessionError(503, "native outbound messages limit reached")
|
|
1991
|
+
outbound.bytes += size
|
|
1992
|
+
outbound.messages++
|
|
1993
|
+
queue.push(frame(message))
|
|
1994
|
+
void flush()
|
|
1995
|
+
},
|
|
1996
|
+
close() {
|
|
1997
|
+
if (closed) return
|
|
1998
|
+
closed = true
|
|
1999
|
+
queue.length = 0
|
|
2000
|
+
outbound.bytes = 0
|
|
2001
|
+
outbound.messages = 0
|
|
2002
|
+
rejectWrite?.(new NativeSessionError(410, "native transport was cancelled"))
|
|
2003
|
+
close()
|
|
2004
|
+
},
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// runNative opens a session-owned Run. Its background decoder survives the end
|
|
2009
|
+
// of each Chat segment; only terminal completion, failure, or cancellation
|
|
2010
|
+
// releases the transport.
|
|
2011
|
+
async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode, params, native }) {
|
|
2012
|
+
const cid = convID || randomUUID()
|
|
2013
|
+
const since = Date.now()
|
|
2014
|
+
// Transport shutdown precedes final debt collection. Keep its cancellation
|
|
2015
|
+
// separate, and return any undelivered collection through the ledger's carry.
|
|
2016
|
+
const accounting = new AbortController()
|
|
2017
|
+
const book = ledger(tok, cid, since, accounting.signal, !!convID, { before: since })
|
|
2018
|
+
const debtKey = usageKey(tok, cid)
|
|
2019
|
+
let started = false, settled = false, deferred = null
|
|
2020
|
+
native.accounting = {
|
|
2021
|
+
started() { started = true },
|
|
2022
|
+
async settle() {
|
|
2023
|
+
const more = await book.owed(true)
|
|
2024
|
+
if (settled) {
|
|
2025
|
+
if (more) owe(debtKey, { carry: more })
|
|
2026
|
+
return null
|
|
2027
|
+
}
|
|
2028
|
+
deferred = more
|
|
2029
|
+
return more
|
|
2030
|
+
},
|
|
2031
|
+
delivered() {
|
|
2032
|
+
if (settled) return
|
|
2033
|
+
settled = true
|
|
2034
|
+
deferred = null
|
|
2035
|
+
book.done()
|
|
2036
|
+
accounting.abort()
|
|
2037
|
+
},
|
|
2038
|
+
abandon() {
|
|
2039
|
+
if (settled) return
|
|
2040
|
+
settled = true
|
|
2041
|
+
book.done()
|
|
2042
|
+
accounting.abort()
|
|
2043
|
+
if (deferred) owe(debtKey, { carry: deferred })
|
|
2044
|
+
deferred = null
|
|
2045
|
+
// Every handoff of this owner belongs to one Run. Only an abandoned
|
|
2046
|
+
// started/pending Run is owed; a final snapshot (including zero) is not.
|
|
2047
|
+
if (started && native.segments > 0 && !native.finalUsage && convID && canRead(tok)) {
|
|
2048
|
+
owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
}
|
|
2052
|
+
const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id, cid, { maxMode, params })
|
|
2053
|
+
native.chatTools = tools
|
|
2054
|
+
native.attach(signal)
|
|
2055
|
+
if (native.error) {
|
|
2056
|
+
nativeSessions.forget(native)
|
|
2057
|
+
return { error: native.error }
|
|
2058
|
+
}
|
|
2059
|
+
const o = await open(
|
|
2060
|
+
base,
|
|
2061
|
+
{
|
|
2062
|
+
"content-type": "application/connect+proto",
|
|
2063
|
+
"connect-protocol-version": "1",
|
|
2064
|
+
authorization: `Bearer ${tok}`,
|
|
2065
|
+
"x-cursor-client-version": await clientVersion(),
|
|
2066
|
+
"x-cursor-client-type": "cli", // else Cursor adds a prompt of its own
|
|
2067
|
+
"x-ghost-mode": "true", // privacy mode: nothing kept for training
|
|
2068
|
+
"x-request-id": randomUUID(),
|
|
2069
|
+
// none of Cursor's own tools, only the caller's
|
|
2070
|
+
"x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
|
|
2071
|
+
},
|
|
2072
|
+
native.ac.signal,
|
|
2073
|
+
)
|
|
2074
|
+
if (o.error) {
|
|
2075
|
+
native.fail(o.error)
|
|
2076
|
+
nativeSessions.forget(native)
|
|
2077
|
+
return o
|
|
2078
|
+
}
|
|
2079
|
+
const { session, req } = o
|
|
2080
|
+
const close = () => {
|
|
2081
|
+
try { req.close(http2.constants.NGHTTP2_CANCEL) } catch {}
|
|
2082
|
+
session.close()
|
|
2083
|
+
session.destroy()
|
|
2084
|
+
}
|
|
2085
|
+
const transport = nativeTransport(req, close, (e) => native.fail({ status: e.status ?? 502, message: e.message }), native.limits)
|
|
2086
|
+
try {
|
|
2087
|
+
native.bind(transport, blobs, conv.estimate)
|
|
2088
|
+
if (native.error) {
|
|
2089
|
+
nativeSessions.forget(native)
|
|
2090
|
+
return { error: native.error }
|
|
2091
|
+
}
|
|
2092
|
+
native.send(first)
|
|
2093
|
+
native.accounting.started()
|
|
2094
|
+
if (o.status < 200 || o.status > 299) {
|
|
2095
|
+
const chunks = []
|
|
2096
|
+
for await (const c of req) chunks.push(c)
|
|
2097
|
+
native.fail(failure(o.status, Buffer.concat(chunks).toString("utf8").slice(0, 1 << 20)))
|
|
2098
|
+
nativeSessions.forget(native)
|
|
2099
|
+
return { error: native.error, h1: !!o.h1 }
|
|
2100
|
+
}
|
|
2101
|
+
native.pumpTask = (async () => {
|
|
2102
|
+
await native.pump(frames(req))
|
|
2103
|
+
})()
|
|
2104
|
+
const response = native.response()
|
|
2105
|
+
const it = response.events
|
|
2106
|
+
const head = await it.next()
|
|
2107
|
+
if (head.done || head.value.error) {
|
|
2108
|
+
const error = head.value?.error ?? { status: 502, message: "an empty reply" }
|
|
2109
|
+
await native.cancel(error.message)
|
|
2110
|
+
nativeSessions.forget(native)
|
|
2111
|
+
return { error, h1: !!o.h1 }
|
|
2112
|
+
}
|
|
2113
|
+
let firstEvent = head
|
|
2114
|
+
response.events = {
|
|
2115
|
+
next() {
|
|
2116
|
+
if (firstEvent) {
|
|
2117
|
+
const r = firstEvent
|
|
2118
|
+
firstEvent = null
|
|
2119
|
+
return Promise.resolve(r)
|
|
2120
|
+
}
|
|
2121
|
+
return it.next()
|
|
2122
|
+
},
|
|
2123
|
+
return: () => it.return(),
|
|
2124
|
+
[Symbol.asyncIterator]() { return this },
|
|
2125
|
+
}
|
|
2126
|
+
return response
|
|
2127
|
+
} catch (e) {
|
|
2128
|
+
native.fail({ status: e.status ?? 502, message: e.message })
|
|
2129
|
+
await native.pumpTask
|
|
2130
|
+
nativeSessions.forget(native)
|
|
2131
|
+
return { error: native.error, h1: !!o.h1 }
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1879
2135
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
1880
2136
|
// iterator of the answer's pieces, or the error it failed with before any.
|
|
1881
2137
|
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
|
|
@@ -2174,18 +2430,50 @@ function emptyWhy(how, seen, usage) {
|
|
|
2174
2430
|
// exec answers what the server asks the client to run: a call of the
|
|
2175
2431
|
// caller's tools is returned, the list of them is given, and anything else
|
|
2176
2432
|
// is refused.
|
|
2177
|
-
function exec(msg, tools, send, closeExec, refused) {
|
|
2178
|
-
const es = fields(msg)
|
|
2433
|
+
function exec(msg, tools, send, closeExec, refused, native = false) {
|
|
2434
|
+
const es = native ? nativeFields(msg) : fields(msg)
|
|
2435
|
+
// ExecServerMessage.id is a proto3 uint32: an omitted field means zero.
|
|
2436
|
+
// Explicit malformed fields must not acquire that valid default.
|
|
2437
|
+
if (native && es.some((e) => e.num === 1 && (e.wire !== 0 || !Number.isInteger(e.n) || e.n < 0 || e.n > 0xffffffff)))
|
|
2438
|
+
throw new NativeSessionError(502, "invalid native exec request ID")
|
|
2179
2439
|
const id = pbNum(es, 1)
|
|
2180
2440
|
const execID = pbStr(es, 15)
|
|
2441
|
+
// Validate every MCP envelope before any exec can publish a call or send a
|
|
2442
|
+
// response. Differing duplicates have no supported selection/merge contract.
|
|
2443
|
+
let nativeCall, nativeUnavailable = false
|
|
2444
|
+
if (native) for (const e of es) if (e.num === 11) {
|
|
2445
|
+
if (e.wire !== 2) throw new NativeSessionError(502, "invalid native MCP argument envelope")
|
|
2446
|
+
const { fields: a, args } = nativeMcpArgs(e.data)
|
|
2447
|
+
const name = pbStr(a, 5) || pbStr(a, 1).replace(/^magpie-/, "")
|
|
2448
|
+
const provider = pbStr(a, 4)
|
|
2449
|
+
const server = pbStr(a, 9)
|
|
2450
|
+
if (provider && provider !== "magpie" || server && server !== "magpie") nativeUnavailable = true
|
|
2451
|
+
if (pbNum(a, 7) || field(a, 6)) nativeUnavailable = true
|
|
2452
|
+
if (!tools.some((t) => t.name === name)) nativeUnavailable = true
|
|
2453
|
+
if (!pbStr(a, 5) && pbStr(a, 1) !== "magpie-" + name) nativeUnavailable = true
|
|
2454
|
+
const call = { requestID: id, execID, nativeID: pbStr(a, 3), name, args }
|
|
2455
|
+
if (nativeCall && !isDeepStrictEqual(nativeCall, call)) throw new NativeSessionError(502, "ambiguous repeated native MCP argument envelopes")
|
|
2456
|
+
nativeCall ??= call
|
|
2457
|
+
}
|
|
2181
2458
|
const answer = (num, res) => {
|
|
2182
2459
|
send(pb().bytes(2, pb().varint(1, id).str(15, execID).bytes(num, res)).done())
|
|
2183
2460
|
closeExec(id)
|
|
2184
2461
|
}
|
|
2462
|
+
const unavailable = () => {
|
|
2463
|
+
const kind = es.find((e) => ![1, 15, 19, 55].includes(e.num))?.num ?? 0
|
|
2464
|
+
refused?.set(kind, (refused.get(kind) ?? 0) + 1)
|
|
2465
|
+
send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
|
|
2466
|
+
closeExec(id)
|
|
2467
|
+
return null
|
|
2468
|
+
}
|
|
2469
|
+
// A foreign/unregistered MCP or approval flow is unavailable to this bridge.
|
|
2470
|
+
// Refuse it like a native shell request, then let the model continue.
|
|
2471
|
+
if (nativeUnavailable) return unavailable()
|
|
2185
2472
|
for (const e of es) {
|
|
2186
2473
|
switch (e.num) {
|
|
2187
2474
|
case 11: {
|
|
2188
2475
|
// mcp_args: a call of the caller's tools
|
|
2476
|
+
if (native) return { ...nativeCall, args: JSON.stringify(nativeCall.args) }
|
|
2189
2477
|
const a = fields(e.data)
|
|
2190
2478
|
const args = {}
|
|
2191
2479
|
for (const kv of a) {
|
|
@@ -2213,11 +2501,7 @@ function exec(msg, tools, send, closeExec, refused) {
|
|
|
2213
2501
|
}
|
|
2214
2502
|
}
|
|
2215
2503
|
// a shell, a file read or an edit of Cursor's own: never run
|
|
2216
|
-
|
|
2217
|
-
refused?.set(kind, (refused.get(kind) ?? 0) + 1)
|
|
2218
|
-
send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
|
|
2219
|
-
closeExec(id)
|
|
2220
|
-
return null
|
|
2504
|
+
return unavailable()
|
|
2221
2505
|
}
|
|
2222
2506
|
|
|
2223
2507
|
// ---- chat completions ------------------------------------------------------------------
|
|
@@ -2258,67 +2542,117 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2258
2542
|
} catch (e) {
|
|
2259
2543
|
return errorResponse({ status: e.status ?? 401, message: e.message })
|
|
2260
2544
|
}
|
|
2261
|
-
|
|
2262
|
-
let raw = []
|
|
2263
|
-
try {
|
|
2264
|
-
raw = await usable(tok)
|
|
2265
|
-
} catch {}
|
|
2266
|
-
// fast only when asked: a service_tier of priority or fast is on; any
|
|
2267
|
-
// other tier, or none, is off, unless the id is a fast one of Cursor's own
|
|
2268
|
-
const tier = chat.service_tier
|
|
2269
|
-
const fast = tier === "priority" || tier === "fast" ? true : tier ? false : undefined
|
|
2270
|
-
const req = request(raw, model, chat.reasoning_effort ?? "", fast)
|
|
2271
|
-
const id = req.id
|
|
2272
|
-
const params = req.params
|
|
2273
|
-
let maxMode = req.maxMode || maxOnly.has(id)
|
|
2545
|
+
if (signal?.aborted) return errorResponse({ status: 410, message: "the request was cancelled" })
|
|
2274
2546
|
const tools = toolsOf(chat)
|
|
2275
|
-
const conv = conversation(chat, tools)
|
|
2276
2547
|
const convID = conversationID(chat, session)
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2548
|
+
const prepared = nativeSessions.prepare(tok, chat, convID, tools)
|
|
2549
|
+
let r
|
|
2550
|
+
if (prepared.owner) r = prepared.owner.resume(prepared, signal)
|
|
2551
|
+
const model = String(chat.model ?? "")
|
|
2552
|
+
if (!r) {
|
|
2553
|
+
let raw = []
|
|
2554
|
+
try {
|
|
2555
|
+
raw = await usable(tok)
|
|
2556
|
+
} catch {}
|
|
2557
|
+
// fast only when asked: a service_tier of priority or fast is on; any
|
|
2558
|
+
// other tier, or none, is off, unless the id is a fast one of Cursor's own
|
|
2559
|
+
const tier = chat.service_tier
|
|
2560
|
+
const fast = tier === "priority" || tier === "fast" ? true : tier ? false : undefined
|
|
2561
|
+
const req = request(raw, model, chat.reasoning_effort ?? "", fast)
|
|
2562
|
+
const id = req.id
|
|
2563
|
+
const params = req.params
|
|
2564
|
+
let maxMode = req.maxMode || maxOnly.has(id)
|
|
2565
|
+
const conv = conversation(chat, tools)
|
|
2566
|
+
let base = await agentURL(tok, false)
|
|
2567
|
+
const once = async () => {
|
|
2568
|
+
// Retention is an optimization. Admission pressure or unavailable ownership
|
|
2569
|
+
// uses the original history builder/decoder without cancelling a live parent.
|
|
2570
|
+
const native = prepared.fallback ? null : nativeSessions.create(prepared, { id, maxMode, params }, { pb, exec, usageOf, failure })
|
|
2571
|
+
const args = { tok, base, id, tools, conv, convID, signal, maxMode, params }
|
|
2572
|
+
let response = native ? await runNative({ ...args, native }) : await runOnce(args)
|
|
2573
|
+
const rebuild = async () => {
|
|
2574
|
+
const fallback = await runOnce(args)
|
|
2575
|
+
fallback.diagnostic = "stateless; bootstrap=imported-history; reason=missing-batch-count"
|
|
2576
|
+
return fallback
|
|
2577
|
+
}
|
|
2578
|
+
// A missing native batch count may be retried once, only before Chat
|
|
2579
|
+
// output is exposed or any native result was submitted. Never repeat an
|
|
2580
|
+
// uncertain tool result write or duplicate a progressive stream's output.
|
|
2581
|
+
if (response.error?.retentionIneligible && !signal?.aborted) response = await rebuild()
|
|
2582
|
+
else if (native) response.rebuild = rebuild
|
|
2583
|
+
response.diagnostic ??= "stateless; bootstrap=imported-history; reason=" + (prepared.fallback || "retention capacity or reservation unavailable")
|
|
2584
|
+
return response
|
|
2290
2585
|
}
|
|
2291
|
-
}
|
|
2292
|
-
if (r.error && !maxMode && maxRequired(r.error.message)) {
|
|
2293
|
-
// a model Cursor serves only in Max Mode that neither list said so of:
|
|
2294
|
-
// in Max Mode, as the CLI turns it on for such a model, and so from
|
|
2295
|
-
// now on. An account Max Mode isn't open to is answered as Cursor
|
|
2296
|
-
// answers that.
|
|
2297
|
-
maxMode = true
|
|
2298
|
-
maxOnly.add(id)
|
|
2299
2586
|
r = await once()
|
|
2587
|
+
if (r.error && regional(r.error.message)) {
|
|
2588
|
+
// the team moved, or the config was kept from before: once more with
|
|
2589
|
+
// what the config says now. A Run that went over HTTP/1.1 went to api2,
|
|
2590
|
+
// not the region's agent host: once more over HTTP/2 there.
|
|
2591
|
+
const viaH1 = !!r.h1
|
|
2592
|
+
h1Until = 0
|
|
2593
|
+
const fresh = await agentURL(tok, true)
|
|
2594
|
+
if (fresh !== base || viaH1) {
|
|
2595
|
+
base = fresh
|
|
2596
|
+
r = await once()
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
if (r.error && !maxMode && maxRequired(r.error.message)) {
|
|
2600
|
+
// a model Cursor serves only in Max Mode that neither list said so of:
|
|
2601
|
+
// in Max Mode, as the CLI turns it on for such a model, and so from
|
|
2602
|
+
// now on. An account Max Mode isn't open to is answered as Cursor
|
|
2603
|
+
// answers that.
|
|
2604
|
+
maxMode = true
|
|
2605
|
+
maxOnly.add(id)
|
|
2606
|
+
r = await once()
|
|
2607
|
+
}
|
|
2300
2608
|
}
|
|
2301
2609
|
if (r.error) return errorResponse(r.error)
|
|
2610
|
+
// A resumed owner may fail while submitting results. Surface that uncertain
|
|
2611
|
+
// write once; later history rebuilds never submit it again on that owner.
|
|
2302
2612
|
const it = r.events
|
|
2613
|
+
let head = await it.next()
|
|
2614
|
+
if (head.value?.error) return errorResponse(head.value.error)
|
|
2615
|
+
let events = {
|
|
2616
|
+
next() {
|
|
2617
|
+
if (head) { const value = head; head = null; return Promise.resolve(value) }
|
|
2618
|
+
return it.next()
|
|
2619
|
+
},
|
|
2620
|
+
return: () => it.return?.(),
|
|
2621
|
+
[Symbol.asyncIterator]() { return this },
|
|
2622
|
+
}
|
|
2623
|
+
const responseHeaders = { "X-Magpie-Cursor-Continuity": r.diagnostic }
|
|
2303
2624
|
const cid = "chatcmpl-" + randomBytes(12).toString("hex")
|
|
2304
2625
|
const created = Math.floor(Date.now() / 1000)
|
|
2305
2626
|
|
|
2306
2627
|
if (!chat.stream) {
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2628
|
+
while (true) {
|
|
2629
|
+
let retry = false
|
|
2630
|
+
const msg = { role: "assistant", content: "" }
|
|
2631
|
+
let reasoning = ""
|
|
2632
|
+
const calls = []
|
|
2633
|
+
let stop = "stop"
|
|
2634
|
+
let usage
|
|
2635
|
+
for await (const e of events) {
|
|
2636
|
+
if (e.error) {
|
|
2637
|
+
if (!e.error.retentionIneligible || !r.rebuild || signal?.aborted) return errorResponse(e.error)
|
|
2638
|
+
r = await r.rebuild()
|
|
2639
|
+
if (r.error) return errorResponse(r.error)
|
|
2640
|
+
events = r.events
|
|
2641
|
+
responseHeaders["X-Magpie-Cursor-Continuity"] = r.diagnostic
|
|
2642
|
+
retry = true
|
|
2643
|
+
break
|
|
2644
|
+
}
|
|
2645
|
+
if (e.text) msg.content += e.text
|
|
2646
|
+
if (e.reasoning) reasoning += e.reasoning
|
|
2647
|
+
if (e.tool) calls.push({ id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } })
|
|
2648
|
+
if (e.stop) [stop, usage] = [e.stop, e.usage]
|
|
2649
|
+
}
|
|
2650
|
+
if (retry) continue
|
|
2651
|
+
if (reasoning) msg.reasoning_content = reasoning
|
|
2652
|
+
if (calls.length) msg.tool_calls = calls
|
|
2653
|
+
r.delivered?.()
|
|
2654
|
+
return Response.json({ id: cid, object: "chat.completion", created, model, choices: [{ index: 0, message: msg, finish_reason: stop }], ...(usage ? { usage } : {}) }, { headers: responseHeaders })
|
|
2318
2655
|
}
|
|
2319
|
-
if (reasoning) msg.reasoning_content = reasoning
|
|
2320
|
-
if (calls.length) msg.tool_calls = calls
|
|
2321
|
-
return Response.json({ id: cid, object: "chat.completion", created, model, choices: [{ index: 0, message: msg, finish_reason: stop }], ...(usage ? { usage } : {}) })
|
|
2322
2656
|
}
|
|
2323
2657
|
|
|
2324
2658
|
const chunk = (delta, finish_reason = null, extra = {}) =>
|
|
@@ -2326,16 +2660,17 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2326
2660
|
let began = false
|
|
2327
2661
|
const stream = new ReadableStream({
|
|
2328
2662
|
async pull(ctl) {
|
|
2329
|
-
const
|
|
2663
|
+
const next = await events.next()
|
|
2330
2664
|
if (!began) {
|
|
2331
2665
|
began = true
|
|
2332
2666
|
ctl.enqueue(chunk({ role: "assistant", content: "" }))
|
|
2333
2667
|
}
|
|
2334
|
-
if (
|
|
2668
|
+
if (next.done) {
|
|
2669
|
+
r.delivered?.()
|
|
2335
2670
|
ctl.enqueue(enc.encode("data: [DONE]\n\n"))
|
|
2336
2671
|
return ctl.close()
|
|
2337
2672
|
}
|
|
2338
|
-
const e =
|
|
2673
|
+
const e = next.value
|
|
2339
2674
|
if (e.text) ctl.enqueue(chunk({ content: e.text }))
|
|
2340
2675
|
else if (e.reasoning) ctl.enqueue(chunk({ reasoning_content: e.reasoning }))
|
|
2341
2676
|
else if (e.tool) ctl.enqueue(chunk({ tool_calls: [{ index: e.tool.index, id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } }] }))
|
|
@@ -2345,11 +2680,12 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2345
2680
|
ctl.close()
|
|
2346
2681
|
}
|
|
2347
2682
|
},
|
|
2348
|
-
cancel() {
|
|
2349
|
-
|
|
2683
|
+
async cancel() {
|
|
2684
|
+
await r.cancel?.()
|
|
2685
|
+
await events.return()
|
|
2350
2686
|
},
|
|
2351
2687
|
})
|
|
2352
|
-
return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } })
|
|
2688
|
+
return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...responseHeaders } })
|
|
2353
2689
|
}
|
|
2354
2690
|
|
|
2355
2691
|
// ---- the plugin ---------------------------------------------------------------------------
|
|
@@ -2446,4 +2782,4 @@ export async function CursorAuthPlugin() {
|
|
|
2446
2782
|
}
|
|
2447
2783
|
|
|
2448
2784
|
// for tests
|
|
2449
|
-
export const _internal = { onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, 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 }
|
|
2785
|
+
export const _internal = { nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, 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 }
|