@magpie-community/opencode-cursor-auth 0.1.8 → 0.1.9
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 +6 -1
- package/index.mjs +86 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,12 @@ plugin's `fetch` answers them on Cursor's agent API:
|
|
|
54
54
|
or not. Cursor's `input_tokens` counts the cached prompt too: the cache
|
|
55
55
|
read and written is taken out of it, and given as `cached_tokens` and
|
|
56
56
|
`cache_write_tokens`, so it is counted once; its reasoning tokens are
|
|
57
|
-
`reasoning_tokens`.
|
|
57
|
+
`reasoning_tokens`. A step that calls tools gets no usage in its Run
|
|
58
|
+
(Cursor tells it only once the tools' results come back in the same
|
|
59
|
+
Run), so once the Run is closed its usage is read from the dashboard's
|
|
60
|
+
usage events, by the Run's conversation id: it shows in about 2.5 s,
|
|
61
|
+
and is looked for up to 6 s, after which it is estimated as before.
|
|
62
|
+
Failures keep their statuses: 401 to sign in again, 429 at the
|
|
58
63
|
usage limit, 400 for a prompt too long, 403 for a region refusal.
|
|
59
64
|
|
|
60
65
|
## Models
|
package/index.mjs
CHANGED
|
@@ -1164,6 +1164,59 @@ function usageOf(uf) {
|
|
|
1164
1164
|
return { input: Math.max(inp - cr - cw, 0), output: pbNum(uf, 2), cacheRead: cr, cacheWrite: cw, reasoning: pbNum(uf, 5) }
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
+
// A step that calls the caller's tools has no TurnEndedUpdate: Cursor ends
|
|
1168
|
+
// the turn, and tells what it used, only once the client has run the tools
|
|
1169
|
+
// and sent their results in the same Run, and the caller runs them after
|
|
1170
|
+
// the Run is closed (magpie #676). Before that it sends nothing more than
|
|
1171
|
+
// the conversation's checkpoint and heartbeats, and a cancelled Run ends
|
|
1172
|
+
// with an error and no usage either. The dashboard's usage events count
|
|
1173
|
+
// every Run, by its conversation_id, the uncached input apart from what
|
|
1174
|
+
// was read from the cache and written to it, as TurnEndedUpdate does; one
|
|
1175
|
+
// shows about 2.5 s after the Run is closed. STEP_WAIT is how long a step's
|
|
1176
|
+
// is looked for: first after that long, then every so often until the
|
|
1177
|
+
// end, after which it is guessed as before.
|
|
1178
|
+
const STEP_WAIT = { first: 1500, every: 500, until: 6000 }
|
|
1179
|
+
|
|
1180
|
+
// claimed are the usage events already counted, by conversation and time,
|
|
1181
|
+
// so that two steps of one conversation never count the same one.
|
|
1182
|
+
const claimed = new Map()
|
|
1183
|
+
|
|
1184
|
+
// stepUsage is the usage of the Run of conversation conv started at since,
|
|
1185
|
+
// as the dashboard's usage events count it; null when none shows in time.
|
|
1186
|
+
async function stepUsage(tok, conv, since, signal) {
|
|
1187
|
+
const end = Date.now() + STEP_WAIT.until
|
|
1188
|
+
const wait = (ms) => new Promise((r) => {
|
|
1189
|
+
const t = setTimeout(r, ms)
|
|
1190
|
+
signal?.addEventListener("abort", () => (clearTimeout(t), r()), { once: true })
|
|
1191
|
+
})
|
|
1192
|
+
for (const [k, at] of claimed) if (Date.now() - at > 600_000) claimed.delete(k)
|
|
1193
|
+
await wait(STEP_WAIT.first)
|
|
1194
|
+
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0)
|
|
1195
|
+
while (!signal?.aborted) {
|
|
1196
|
+
try {
|
|
1197
|
+
const res = await fetch(API + "/aiserver.v1.DashboardService/GetFilteredUsageEvents", {
|
|
1198
|
+
method: "POST",
|
|
1199
|
+
headers: await headersFor(tok),
|
|
1200
|
+
body: JSON.stringify({ teamId: 0, startDate: String(since - 60_000), endDate: String(Date.now() + 60_000), page: 1, pageSize: 50 }),
|
|
1201
|
+
signal: AbortSignal.timeout(5000),
|
|
1202
|
+
})
|
|
1203
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429) return null // not this account's to read
|
|
1204
|
+
const evs = res.ok ? ((await res.json())?.usageEventsDisplay ?? []) : []
|
|
1205
|
+
const ev = evs
|
|
1206
|
+
.filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(conv + "\0" + e.timestamp))
|
|
1207
|
+
.sort((a, b) => num(a.timestamp) - num(b.timestamp))[0]
|
|
1208
|
+
if (ev) {
|
|
1209
|
+
claimed.set(conv + "\0" + ev.timestamp, Date.now())
|
|
1210
|
+
const t = ev.tokenUsage
|
|
1211
|
+
return { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
|
|
1212
|
+
}
|
|
1213
|
+
} catch {}
|
|
1214
|
+
if (Date.now() + STEP_WAIT.every > end) return null
|
|
1215
|
+
await wait(STEP_WAIT.every)
|
|
1216
|
+
}
|
|
1217
|
+
return null
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1167
1220
|
// buildRun is the Run's first message, an AgentClientMessage with its
|
|
1168
1221
|
// run_request, and the blobs it names. The conversation state is the
|
|
1169
1222
|
// messages and one turn, which the server wants there to sample at all.
|
|
@@ -1275,7 +1328,11 @@ function open(base, headers, signal) {
|
|
|
1275
1328
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
1276
1329
|
// iterator of the answer's pieces, or the error it failed with before any.
|
|
1277
1330
|
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
|
|
1278
|
-
|
|
1331
|
+
// the Run's conversation_id, a new one when nothing names the session:
|
|
1332
|
+
// its usage event is found by it
|
|
1333
|
+
const cid = convID || randomUUID()
|
|
1334
|
+
const since = Date.now()
|
|
1335
|
+
const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id, cid, { maxMode, params })
|
|
1279
1336
|
const o = await open(
|
|
1280
1337
|
base,
|
|
1281
1338
|
{
|
|
@@ -1318,7 +1375,14 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
1318
1375
|
close()
|
|
1319
1376
|
return { error: failure(o.status, Buffer.concat(chunks).toString("utf8").slice(0, 1 << 20)) }
|
|
1320
1377
|
}
|
|
1321
|
-
const it = decode(frames(req), {
|
|
1378
|
+
const it = decode(frames(req), {
|
|
1379
|
+
send,
|
|
1380
|
+
blobs,
|
|
1381
|
+
tools,
|
|
1382
|
+
estimate: conv.estimate,
|
|
1383
|
+
close,
|
|
1384
|
+
stepUsage: () => stepUsage(tok, cid, since, signal),
|
|
1385
|
+
})
|
|
1322
1386
|
const wrapped = (async function* () {
|
|
1323
1387
|
try {
|
|
1324
1388
|
yield* it
|
|
@@ -1344,8 +1408,10 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
1344
1408
|
|
|
1345
1409
|
// decode follows the server's messages into the answer's pieces until the
|
|
1346
1410
|
// turn ends, or the model has made its tool calls.
|
|
1347
|
-
async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
1411
|
+
async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage }) {
|
|
1348
1412
|
let calls = 0
|
|
1413
|
+
// ended: Cursor said what the turn used
|
|
1414
|
+
let ended = false
|
|
1349
1415
|
let listed = 0
|
|
1350
1416
|
let said = 0
|
|
1351
1417
|
// wrote: the reply's text alone; a turn of thinking only says nothing
|
|
@@ -1368,6 +1434,16 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1368
1434
|
},
|
|
1369
1435
|
}
|
|
1370
1436
|
}
|
|
1437
|
+
// settled is finish once a step that called tools has its usage: the
|
|
1438
|
+
// dashboard's count, the Run closed first, when the turn didn't end
|
|
1439
|
+
const settled = async () => {
|
|
1440
|
+
if (calls > 0 && !ended && stepUsage) {
|
|
1441
|
+
close?.()
|
|
1442
|
+
const u = await stepUsage().catch(() => null)
|
|
1443
|
+
if (u) usage = u
|
|
1444
|
+
}
|
|
1445
|
+
return finish()
|
|
1446
|
+
}
|
|
1371
1447
|
const closeExec = (id) => send(pb().bytes(5, pb().bytes(1, pb().varint(1, id))).done())
|
|
1372
1448
|
try {
|
|
1373
1449
|
for await (const f of stream) {
|
|
@@ -1375,7 +1451,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1375
1451
|
const e = failure(200, f.data)
|
|
1376
1452
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
1377
1453
|
else if (wrote === 0 && calls === 0) yield { error: { status: 502, message: "an empty reply" } }
|
|
1378
|
-
else yield
|
|
1454
|
+
else yield await settled()
|
|
1379
1455
|
return
|
|
1380
1456
|
}
|
|
1381
1457
|
for (const m of fields(f.data)) {
|
|
@@ -1405,6 +1481,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1405
1481
|
}
|
|
1406
1482
|
case 14: // turn ended, with what it used
|
|
1407
1483
|
usage = usageOf(uf)
|
|
1484
|
+
ended = true
|
|
1408
1485
|
if (calls === 0) {
|
|
1409
1486
|
// nothing written and nothing called: an empty reply,
|
|
1410
1487
|
// which the client asks again, not a finished turn
|
|
@@ -1415,7 +1492,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1415
1492
|
case 27: // how many tool calls the step makes, sent before them
|
|
1416
1493
|
listed = pbNum(uf, 1)
|
|
1417
1494
|
if (listed > 0 && calls >= listed) {
|
|
1418
|
-
yield
|
|
1495
|
+
yield await settled()
|
|
1419
1496
|
return
|
|
1420
1497
|
}
|
|
1421
1498
|
break
|
|
@@ -1431,7 +1508,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1431
1508
|
}
|
|
1432
1509
|
if (listed > 0 && calls >= listed) {
|
|
1433
1510
|
// every call made: the caller runs them
|
|
1434
|
-
yield
|
|
1511
|
+
yield await settled()
|
|
1435
1512
|
return
|
|
1436
1513
|
}
|
|
1437
1514
|
break
|
|
@@ -1457,13 +1534,13 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1457
1534
|
}
|
|
1458
1535
|
}
|
|
1459
1536
|
if (calls > 0) {
|
|
1460
|
-
yield
|
|
1537
|
+
yield await settled()
|
|
1461
1538
|
return
|
|
1462
1539
|
}
|
|
1463
1540
|
yield { error: { status: 502, message: "the reply broke off: EOF" } }
|
|
1464
1541
|
} catch (e) {
|
|
1465
1542
|
if (calls > 0 && /EOF/.test(e?.message ?? "")) {
|
|
1466
|
-
yield
|
|
1543
|
+
yield await settled()
|
|
1467
1544
|
return
|
|
1468
1545
|
}
|
|
1469
1546
|
yield { error: { status: 502, message: "the reply broke off: " + (e?.message ?? e) } }
|
|
@@ -1733,4 +1810,4 @@ export async function CursorAuthPlugin() {
|
|
|
1733
1810
|
}
|
|
1734
1811
|
|
|
1735
1812
|
// for tests
|
|
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 }
|
|
1813
|
+
export const _internal = { STEP_WAIT, stepUsage, 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|