@magpie-community/opencode-cursor-auth 0.2.6 → 0.2.8
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 +5 -1
- package/index.mjs +49 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,11 @@ plugin's `fetch` answers them on Cursor's agent API:
|
|
|
63
63
|
so subagents under one session are conversations of their own. With
|
|
64
64
|
nothing naming the session, each Run has a new one.
|
|
65
65
|
- Text, thinking (`reasoning_content`) and token usage come back streamed
|
|
66
|
-
or not.
|
|
66
|
+
or not. Replayed assistant `reasoning_content` (or `reasoning`) is kept as
|
|
67
|
+
AI SDK reasoning parts in the next Run's transcript, including when a
|
|
68
|
+
caller splits parallel tool calls across assistant messages. It stays
|
|
69
|
+
separate from reply text; no thinking signature is invented.
|
|
70
|
+
Cursor's `input_tokens` counts the cached prompt too: the cache
|
|
67
71
|
read and written is taken out of it, and given as `cached_tokens` and
|
|
68
72
|
`cache_write_tokens`, so it is counted once; its reasoning tokens are
|
|
69
73
|
`reasoning_tokens`. A step that calls tools gets no usage in its Run
|
package/index.mjs
CHANGED
|
@@ -1081,6 +1081,10 @@ function statusOf(status, code, msg) {
|
|
|
1081
1081
|
const textOf = (c) =>
|
|
1082
1082
|
typeof c === "string" ? c : Array.isArray(c) ? c.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : ""
|
|
1083
1083
|
|
|
1084
|
+
// Chat clients replay thinking beside content. Cursor's transcript blobs
|
|
1085
|
+
// use AI SDK reasoning parts; putting it in text would change its role.
|
|
1086
|
+
const reasoningOf = (m) => [m.reasoning_content, m.reasoning].find((s) => typeof s === "string" && s.trim()) ?? ""
|
|
1087
|
+
|
|
1084
1088
|
// joinSplitCalls joins an assistant message onto the assistant message
|
|
1085
1089
|
// before it when that one made tool calls: Anthropic takes consecutive
|
|
1086
1090
|
// assistant messages as one turn, and an agent can send a turn's parallel
|
|
@@ -1093,7 +1097,8 @@ function joinSplitCalls(msgs) {
|
|
|
1093
1097
|
const prev = out.at(-1)
|
|
1094
1098
|
if (m?.role === "assistant" && prev?.role === "assistant" && prev.tool_calls?.length) {
|
|
1095
1099
|
const content = [textOf(prev.content), textOf(m.content)].filter(Boolean).join("\n\n")
|
|
1096
|
-
|
|
1100
|
+
const reasoning_content = [reasoningOf(prev), reasoningOf(m)].filter(Boolean).join("\n\n")
|
|
1101
|
+
out[out.length - 1] = { ...prev, content, reasoning_content, tool_calls: [...prev.tool_calls, ...(m.tool_calls ?? [])] }
|
|
1097
1102
|
continue
|
|
1098
1103
|
}
|
|
1099
1104
|
out.push(m)
|
|
@@ -1198,9 +1203,11 @@ function conversation(chat, tools) {
|
|
|
1198
1203
|
if (m.role === "assistant") {
|
|
1199
1204
|
answer()
|
|
1200
1205
|
const content = []
|
|
1206
|
+
const reasoning = reasoningOf(m)
|
|
1207
|
+
if (reasoning) content.push({ type: "reasoning", text: reasoning })
|
|
1201
1208
|
const text = textOf(m.content)
|
|
1202
1209
|
if (text) content.push({ type: "text", text })
|
|
1203
|
-
size += text.length
|
|
1210
|
+
size += reasoning.length + text.length
|
|
1204
1211
|
for (const c of m.tool_calls ?? []) {
|
|
1205
1212
|
if (c?.type && c.type !== "function") continue
|
|
1206
1213
|
const args = parseArgs(c.function?.arguments)
|
|
@@ -1889,6 +1896,12 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
1889
1896
|
let said = 0
|
|
1890
1897
|
// wrote: the reply's text alone; a turn of thinking only says nothing
|
|
1891
1898
|
let wrote = 0
|
|
1899
|
+
// what the Run sent, for an empty reply to name (#60): the thinking's
|
|
1900
|
+
// length, Cursor's own tools it asked for and was refused, and updates
|
|
1901
|
+
// and messages this doesn't read
|
|
1902
|
+
const seen = { thinking: 0, refused: new Map(), updates: new Map(), messages: new Map() }
|
|
1903
|
+
const count = (m, k) => m.set(k, (m.get(k) ?? 0) + 1)
|
|
1904
|
+
const empty = (how) => ({ error: { status: 502, message: "an empty reply" + emptyWhy(how, seen, ended ? usage : null) } })
|
|
1892
1905
|
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
1893
1906
|
// counted: the step's usage is the dashboard's events, its own counted
|
|
1894
1907
|
// with it or by a later step (ledger), never guessed
|
|
@@ -1934,7 +1947,7 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
1934
1947
|
if (f.end) {
|
|
1935
1948
|
const e = failure(200, f.data)
|
|
1936
1949
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
1937
|
-
else if (wrote === 0 && calls === 0) yield
|
|
1950
|
+
else if (wrote === 0 && calls === 0) yield empty(endFrame(f.data))
|
|
1938
1951
|
else yield await settled()
|
|
1939
1952
|
return
|
|
1940
1953
|
}
|
|
@@ -1959,6 +1972,7 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
1959
1972
|
const t = pbStr(uf, 1)
|
|
1960
1973
|
if (t) {
|
|
1961
1974
|
said += t.length
|
|
1975
|
+
seen.thinking += t.length
|
|
1962
1976
|
yield { reasoning: t }
|
|
1963
1977
|
}
|
|
1964
1978
|
break
|
|
@@ -1969,7 +1983,7 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
1969
1983
|
if (calls === 0) {
|
|
1970
1984
|
// nothing written and nothing called: an empty reply,
|
|
1971
1985
|
// which the client asks again, not a finished turn
|
|
1972
|
-
yield wrote === 0 ?
|
|
1986
|
+
yield wrote === 0 ? empty("Cursor ended the turn") : await settled()
|
|
1973
1987
|
return
|
|
1974
1988
|
}
|
|
1975
1989
|
break
|
|
@@ -1980,12 +1994,14 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
1980
1994
|
return
|
|
1981
1995
|
}
|
|
1982
1996
|
break
|
|
1997
|
+
default:
|
|
1998
|
+
count(seen.updates, u.num)
|
|
1983
1999
|
}
|
|
1984
2000
|
}
|
|
1985
2001
|
break
|
|
1986
2002
|
case 2: {
|
|
1987
2003
|
// exec_server_message: the server asks the client to run something
|
|
1988
|
-
const call = exec(m.data, tools, send, closeExec)
|
|
2004
|
+
const call = exec(m.data, tools, send, closeExec, seen.refused)
|
|
1989
2005
|
if (call) {
|
|
1990
2006
|
said += call.args.length
|
|
1991
2007
|
yield { tool: { index: calls++, ...call } }
|
|
@@ -2014,6 +2030,8 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2014
2030
|
}
|
|
2015
2031
|
break
|
|
2016
2032
|
}
|
|
2033
|
+
default:
|
|
2034
|
+
count(seen.messages, m.num)
|
|
2017
2035
|
}
|
|
2018
2036
|
}
|
|
2019
2037
|
}
|
|
@@ -2031,10 +2049,33 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2031
2049
|
}
|
|
2032
2050
|
}
|
|
2033
2051
|
|
|
2052
|
+
// endFrame is how a Run's Connect end frame that said no error reads in
|
|
2053
|
+
// an empty reply's words: its JSON, cut short, or that it was empty.
|
|
2054
|
+
function endFrame(data) {
|
|
2055
|
+
const t = Buffer.from(data ?? []).toString("utf8").trim()
|
|
2056
|
+
if (!t || t === "{}") return "the Run's stream ended (an empty end frame)"
|
|
2057
|
+
return `the Run's stream ended (end frame: ${t.length > 300 ? t.slice(0, 300) + "…" : t})`
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// emptyWhy is what an empty reply says of what came before it (#60): how
|
|
2061
|
+
// it ended, the thinking, the turn's own count of output, Cursor's own
|
|
2062
|
+
// tools refused, and what Cursor sent that isn't read. Field numbers are
|
|
2063
|
+
// agent.v1's, so a report names the message that wasn't understood.
|
|
2064
|
+
function emptyWhy(how, seen, usage) {
|
|
2065
|
+
const list = (m) => [...m].map(([k, n]) => (n > 1 ? `#${k} ×${n}` : `#${k}`)).join(", ")
|
|
2066
|
+
const parts = [how]
|
|
2067
|
+
parts.push(seen.thinking ? `after ${seen.thinking} chars of thinking, no text and no tool call` : "with no thinking, no text and no tool call")
|
|
2068
|
+
if (usage) parts.push(`Cursor counted ${usage.output} output tokens`)
|
|
2069
|
+
if (seen.refused.size) parts.push(`Cursor's own tools asked for and refused: ${list(seen.refused)}`)
|
|
2070
|
+
if (seen.updates.size) parts.push(`updates not read: ${list(seen.updates)}`)
|
|
2071
|
+
if (seen.messages.size) parts.push(`messages not read: ${list(seen.messages)}`)
|
|
2072
|
+
return ` (${parts.join("; ")}; try again)`
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2034
2075
|
// exec answers what the server asks the client to run: a call of the
|
|
2035
2076
|
// caller's tools is returned, the list of them is given, and anything else
|
|
2036
2077
|
// is refused.
|
|
2037
|
-
function exec(msg, tools, send, closeExec) {
|
|
2078
|
+
function exec(msg, tools, send, closeExec, refused) {
|
|
2038
2079
|
const es = fields(msg)
|
|
2039
2080
|
const id = pbNum(es, 1)
|
|
2040
2081
|
const execID = pbStr(es, 15)
|
|
@@ -2073,6 +2114,8 @@ function exec(msg, tools, send, closeExec) {
|
|
|
2073
2114
|
}
|
|
2074
2115
|
}
|
|
2075
2116
|
// a shell, a file read or an edit of Cursor's own: never run
|
|
2117
|
+
const kind = es.find((e) => e.num !== 1 && e.num !== 15)?.num ?? 0
|
|
2118
|
+
refused?.set(kind, (refused.get(kind) ?? 0) + 1)
|
|
2076
2119
|
send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
|
|
2077
2120
|
closeExec(id)
|
|
2078
2121
|
return null
|
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.8",
|
|
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",
|