@magpie-community/opencode-cursor-auth 0.1.7 → 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 +99 -14
- package/package.json +3 -2
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
|
@@ -981,11 +981,14 @@ function toolsOf(chat) {
|
|
|
981
981
|
// catalog lists the caller's tools for the model. It sees MCP tools only
|
|
982
982
|
// through Cursor's GetDynamicTools and CallDynamicTool, and Cursor names
|
|
983
983
|
// them in a prompt of its own, which this conversation goes without:
|
|
984
|
-
// 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).
|
|
985
988
|
function catalog(tools) {
|
|
986
989
|
if (!tools.length) return ""
|
|
987
|
-
let s = `\n\n<dynamic_tool_catalog>\nThe
|
|
988
|
-
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`
|
|
989
992
|
return s + "</dynamic_tool_catalog>"
|
|
990
993
|
}
|
|
991
994
|
|
|
@@ -1161,6 +1164,59 @@ function usageOf(uf) {
|
|
|
1161
1164
|
return { input: Math.max(inp - cr - cw, 0), output: pbNum(uf, 2), cacheRead: cr, cacheWrite: cw, reasoning: pbNum(uf, 5) }
|
|
1162
1165
|
}
|
|
1163
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
|
+
|
|
1164
1220
|
// buildRun is the Run's first message, an AgentClientMessage with its
|
|
1165
1221
|
// run_request, and the blobs it names. The conversation state is the
|
|
1166
1222
|
// messages and one turn, which the server wants there to sample at all.
|
|
@@ -1272,7 +1328,11 @@ function open(base, headers, signal) {
|
|
|
1272
1328
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
1273
1329
|
// iterator of the answer's pieces, or the error it failed with before any.
|
|
1274
1330
|
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
|
|
1275
|
-
|
|
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 })
|
|
1276
1336
|
const o = await open(
|
|
1277
1337
|
base,
|
|
1278
1338
|
{
|
|
@@ -1315,7 +1375,14 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
1315
1375
|
close()
|
|
1316
1376
|
return { error: failure(o.status, Buffer.concat(chunks).toString("utf8").slice(0, 1 << 20)) }
|
|
1317
1377
|
}
|
|
1318
|
-
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
|
+
})
|
|
1319
1386
|
const wrapped = (async function* () {
|
|
1320
1387
|
try {
|
|
1321
1388
|
yield* it
|
|
@@ -1341,10 +1408,14 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
1341
1408
|
|
|
1342
1409
|
// decode follows the server's messages into the answer's pieces until the
|
|
1343
1410
|
// turn ends, or the model has made its tool calls.
|
|
1344
|
-
async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
1411
|
+
async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage }) {
|
|
1345
1412
|
let calls = 0
|
|
1413
|
+
// ended: Cursor said what the turn used
|
|
1414
|
+
let ended = false
|
|
1346
1415
|
let listed = 0
|
|
1347
1416
|
let said = 0
|
|
1417
|
+
// wrote: the reply's text alone; a turn of thinking only says nothing
|
|
1418
|
+
let wrote = 0
|
|
1348
1419
|
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
1349
1420
|
const finish = () => {
|
|
1350
1421
|
if (!usage.output) usage.output = Math.floor((said + 3) / 4)
|
|
@@ -1363,14 +1434,24 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1363
1434
|
},
|
|
1364
1435
|
}
|
|
1365
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
|
+
}
|
|
1366
1447
|
const closeExec = (id) => send(pb().bytes(5, pb().bytes(1, pb().varint(1, id))).done())
|
|
1367
1448
|
try {
|
|
1368
1449
|
for await (const f of stream) {
|
|
1369
1450
|
if (f.end) {
|
|
1370
1451
|
const e = failure(200, f.data)
|
|
1371
1452
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
1372
|
-
else if (
|
|
1373
|
-
else yield
|
|
1453
|
+
else if (wrote === 0 && calls === 0) yield { error: { status: 502, message: "an empty reply" } }
|
|
1454
|
+
else yield await settled()
|
|
1374
1455
|
return
|
|
1375
1456
|
}
|
|
1376
1457
|
for (const m of fields(f.data)) {
|
|
@@ -1384,6 +1465,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1384
1465
|
const t = pbStr(uf, 1)
|
|
1385
1466
|
if (t) {
|
|
1386
1467
|
said += t.length
|
|
1468
|
+
wrote += t.length
|
|
1387
1469
|
yield { text: t }
|
|
1388
1470
|
}
|
|
1389
1471
|
break
|
|
@@ -1399,15 +1481,18 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1399
1481
|
}
|
|
1400
1482
|
case 14: // turn ended, with what it used
|
|
1401
1483
|
usage = usageOf(uf)
|
|
1484
|
+
ended = true
|
|
1402
1485
|
if (calls === 0) {
|
|
1403
|
-
|
|
1486
|
+
// nothing written and nothing called: an empty reply,
|
|
1487
|
+
// which the client asks again, not a finished turn
|
|
1488
|
+
yield wrote === 0 ? { error: { status: 502, message: "an empty reply" } } : finish()
|
|
1404
1489
|
return
|
|
1405
1490
|
}
|
|
1406
1491
|
break
|
|
1407
1492
|
case 27: // how many tool calls the step makes, sent before them
|
|
1408
1493
|
listed = pbNum(uf, 1)
|
|
1409
1494
|
if (listed > 0 && calls >= listed) {
|
|
1410
|
-
yield
|
|
1495
|
+
yield await settled()
|
|
1411
1496
|
return
|
|
1412
1497
|
}
|
|
1413
1498
|
break
|
|
@@ -1423,7 +1508,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1423
1508
|
}
|
|
1424
1509
|
if (listed > 0 && calls >= listed) {
|
|
1425
1510
|
// every call made: the caller runs them
|
|
1426
|
-
yield
|
|
1511
|
+
yield await settled()
|
|
1427
1512
|
return
|
|
1428
1513
|
}
|
|
1429
1514
|
break
|
|
@@ -1449,13 +1534,13 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1449
1534
|
}
|
|
1450
1535
|
}
|
|
1451
1536
|
if (calls > 0) {
|
|
1452
|
-
yield
|
|
1537
|
+
yield await settled()
|
|
1453
1538
|
return
|
|
1454
1539
|
}
|
|
1455
1540
|
yield { error: { status: 502, message: "the reply broke off: EOF" } }
|
|
1456
1541
|
} catch (e) {
|
|
1457
1542
|
if (calls > 0 && /EOF/.test(e?.message ?? "")) {
|
|
1458
|
-
yield
|
|
1543
|
+
yield await settled()
|
|
1459
1544
|
return
|
|
1460
1545
|
}
|
|
1461
1546
|
yield { error: { status: 502, message: "the reply broke off: " + (e?.message ?? e) } }
|
|
@@ -1725,4 +1810,4 @@ export async function CursorAuthPlugin() {
|
|
|
1725
1810
|
}
|
|
1726
1811
|
|
|
1727
1812
|
// for tests
|
|
1728
|
-
export const _internal = { 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,11 +1,12 @@
|
|
|
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",
|
|
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
|
}
|