@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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/index.mjs +49 -6
  3. 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. Cursor's `input_tokens` counts the cached prompt too: the cache
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
- out[out.length - 1] = { ...prev, content, tool_calls: [...prev.tool_calls, ...(m.tool_calls ?? [])] }
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 { error: { status: 502, message: "an empty reply" } }
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 ? { error: { status: 502, message: "an empty reply" } } : await settled()
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.6",
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",