@magpie-community/opencode-cursor-auth 0.2.38 → 0.2.40

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 CHANGED
@@ -99,7 +99,10 @@ plugin's `fetch` answers them on Cursor's agent API:
99
99
 
100
100
  Tool results can continue a retained Run in the same plugin process. The owner
101
101
  uses the same session/cache key and first user message as `conversation_id`, so
102
- Task subagents sharing their parent's session can run independently. Retention
102
+ Task subagents sharing their parent's session can run independently. A new
103
+ user turn whose earlier native calls all have their results gets a retained Run
104
+ of its own, with those calls as imported history; while any of them is still
105
+ pending, the turn goes stateless. Retention
103
106
  waits at most 120 seconds for a result batch and 15 minutes for a Run's total
104
107
  lifetime. A continuation arriving with less than 10 minutes of that lifetime
105
108
  left rebuilds from supplied history on a fresh Run before submitting results
@@ -208,17 +211,36 @@ minutes), the way pi-cursor-sdk lists Cursor's catalog:
208
211
  has. A model the usable list has nothing of is the picker's alone
209
212
  (GLM-5.3) and is kept whole; so is every model when Cursor can't give
210
213
  the usable list.
211
- - A family the usable list has and the picker doesn't (an account whose
212
- picker lacks Claude Opus 5.5 while `cursor-agent models` lists
213
- `claude-opus-5-5-low` to `-max`) is listed once under its family id
214
- (`claude-opus-5-5`), its efforts as variants, and goes as the usable id
215
- those parameters pick, never as the family id Cursor doesn't know. A
216
- picker model whose chosen variant isn't one the account can use goes as
217
- its family's nearest usable id the same way. Such a family is named
218
- without the tier Cursor's names carry ("Claude Opus 5 1M Medium
219
- Thinking" is listed as Claude Opus 5 Thinking): the tier is the
220
- reasoning level the agent asks for (low to max, as the account has
221
- them), fast the request's `service_tier`.
214
+ - The full catalog (`AvailableModels` with `scope: 3`, asked alongside the
215
+ other two; it also lists the models Cursor hides from an account, Claude
216
+ from a Pro one) supplies the Anthropic models the picker lacks: each live
217
+ one (not `isHidden`, not Auto, not chat-only or Tab-only) with a variant
218
+ the usable list has an id for is listed as the picker's models are, once
219
+ for each context size (`claude-opus-5-5@300k`, `claude-opus-5-5@1m`; name
220
+ "Claude Opus 5.5 @ 300k"), thinking a parameter, the efforts the account
221
+ is served, and the larger size in Max Mode with that `context`. A catalog
222
+ variant without its own id gets the one of the model's ids that says its
223
+ effort, thinking and fast; with none or several it is not offered. Only
224
+ the parameters the model defines are sent (the catalog's variants also
225
+ carry `cyber=false`, which the picker's don't and cursor-agent never
226
+ sends). With no effort asked, a model goes at Cursor's default variant
227
+ (Opus 5 and 4.8: high), as the picker's models do, not at medium as the
228
+ fallback entries did. The picker's models and every model that is not
229
+ Anthropic's don't change.
230
+ An id saved from an earlier list (`claude-opus-5-5`,
231
+ `claude-opus-4-8-thinking@1m`) runs as the model at the nearest size it
232
+ has, as a usable id.
233
+ - When Cursor can't give the catalog, a family the usable list has and the
234
+ picker doesn't (`claude-opus-5-5-low` to `-max`) is listed once under its
235
+ family id (`claude-opus-5-5`), its efforts as variants, and goes as the
236
+ usable id those parameters pick, never as the family id Cursor doesn't
237
+ know; it has no window in the usable list, so 200K is assumed. A picker
238
+ model whose chosen variant isn't one the account can use goes as its
239
+ family's nearest usable id the same way. Such a family is named without
240
+ the tier Cursor's names carry ("Claude Opus 5 1M Medium Thinking" is
241
+ listed as Claude Opus 5 Thinking): the tier is the reasoning level the
242
+ agent asks for (low to max, as the account has them), fast the request's
243
+ `service_tier`.
222
244
  - An id for a model with thinking that the account lists only without it
223
245
  (`claude-haiku-5-5-thinking` when the list has Claude Haiku 5.5 alone),
224
246
  or the other way round, runs as the model the account has. So does a
package/index.mjs CHANGED
@@ -1082,6 +1082,69 @@ function modelsOf(picker, ids, listed = []) {
1082
1082
  return out
1083
1083
  }
1084
1084
 
1085
+ // slugFor is the id of a variant the full catalog (AvailableModels with
1086
+ // scope 3) gives no legacySlug: the one of the model's legacySlugs that says
1087
+ // the variant's effort (any, when it has none), thinking and fast; an id
1088
+ // with no effort in it (gpt-5.3-codex) stands for the default's effort. With
1089
+ // none, or more than one, "": a guess could send an id Cursor refuses.
1090
+ function slugFor(m, v) {
1091
+ if (v.legacySlug) return v.legacySlug
1092
+ const have = valuesOf(v)
1093
+ const level = (p) => String(p[EFFORT_PARAMS.find((k) => p[k])] ?? "").replace("extra-high", "xhigh")
1094
+ const effort = level(have)
1095
+ const slugs = (Array.isArray(m.legacySlugs) ? m.legacySlugs : []).filter((id) => typeof id === "string" && id).map((id) => ({ id, ...familyOf(id) }))
1096
+ const says = (s, at) =>
1097
+ (at === null || s.effort === at) &&
1098
+ (have.thinking === undefined || s.family.endsWith("-thinking") === (have.thinking === "true")) &&
1099
+ (have.fast === undefined || s.fast === (have.fast === "true"))
1100
+ let hits = slugs.filter((s) => says(s, effort || null))
1101
+ const def = m.variants.find((x) => x?.isDefaultNonMaxConfig)
1102
+ if (!hits.length && effort && level(valuesOf(def)) === effort) hits = slugs.filter((s) => says(s, ""))
1103
+ return hits.length === 1 ? hits[0].id : ""
1104
+ }
1105
+
1106
+ // withSlugs is a catalog model with each variant's own id filled in, and
1107
+ // only the parameters the model defines: the full catalog is the admin
1108
+ // settings one, and carries what the picker's variants don't (cyber=false
1109
+ // on Opus 4.7 to 5.5, which cursor-agent never sends). A variant slugFor
1110
+ // can't name keeps no id of its own, but never goes without one: served()
1111
+ // leaves a variant with no legacySlug in, and one the account can't run
1112
+ // would be listed. So it gets a mark no usable id has, and modelsOf lists
1113
+ // it as it does a variant the account hasn't got. The variants are never
1114
+ // null.
1115
+ const NO_ID = "\u0000no-id"
1116
+ function withSlugs(m) {
1117
+ const defined = new Set((Array.isArray(m.parameterDefinitions) ? m.parameterDefinitions : []).map((d) => d?.id))
1118
+ const vs = (Array.isArray(m.variants) ? m.variants : [])
1119
+ .filter((v) => v && typeof v === "object" && Array.isArray(v.parameterValues ?? []))
1120
+ .map((v) => ({ ...v, parameterValues: (v.parameterValues ?? []).filter((p) => defined.has(p?.id)) }))
1121
+ const full = { ...m, variants: vs }
1122
+ return { ...full, variants: vs.map((v) => (v.legacySlug ? v : { ...v, legacySlug: slugFor(full, v) || NO_ID })) }
1123
+ }
1124
+
1125
+ // fromCatalog is the live models of Cursor's full catalog (AvailableModels
1126
+ // with scope 3, which also lists the models Cursor hides from some accounts:
1127
+ // Claude, from a Pro one) that the picker lacks, as picker models, with only
1128
+ // the variants the account can run (withSlugs). modelsOf lists them as it
1129
+ // does the picker's: once for each context size, thinking a parameter, the
1130
+ // efforts the account is served. Only Anthropic's are taken: the models the
1131
+ // picker lists, and every other id of the usable list, stay as they are.
1132
+ // Not offered: one the picker has or lists under another name, a hidden one
1133
+ // (isHidden: no longer offered), Auto, one the picker leaves out (chat-only,
1134
+ // Tab-only, no agent), and one with no variant the account has an id for.
1135
+ function fromCatalog(picker, catalog, ids) {
1136
+ const have = new Set(picker.map((m) => m?.name))
1137
+ const out = []
1138
+ for (const m of Array.isArray(catalog) ? catalog : []) {
1139
+ if (!m?.name || have.has(m.name) || m.isHidden || m.name === "default") continue
1140
+ if (m.vendor?.id !== "MODEL_VENDOR_ID_ANTHROPIC") continue
1141
+ if (m.isChatOnly || m.onlySupportsCmdK || m.supportsAgent === false || PICKER_SKIP.has(m.name)) continue
1142
+ const full = withSlugs(m)
1143
+ if (full.variants.some((v) => ids.has(v.legacySlug))) out.push(full)
1144
+ }
1145
+ return out
1146
+ }
1147
+
1085
1148
  // fastAt is whether Cursor has a fast variant the account is served at
1086
1149
  // the context size (any, for a model without sizes): what magpie offers
1087
1150
  // fast for, which 0.1.x said with a -fast model of its own.
@@ -1215,15 +1278,23 @@ function listing(tok, h) {
1215
1278
  let p = listings.get(h)
1216
1279
  if (p) return p
1217
1280
  p = (async () => {
1218
- const [j, listed] = await Promise.all([
1281
+ const [j, listed, catalog] = await Promise.all([
1219
1282
  unary(API, "aiserver.v1.AiService/AvailableModels", tok, { useModelParameters: true, doNotUseMarkdown: true }, 30_000),
1220
1283
  unary(API, "agent.v1.AgentService/GetUsableModels", tok, {}, 8_000).then(
1221
1284
  (u) => (Array.isArray(u?.models) ? u.models : []),
1222
1285
  () => [],
1223
1286
  ),
1287
+ // the full catalog, scope 3: the live models the picker lacks (Claude,
1288
+ // on an account Cursor blocks it for); without it the list is the
1289
+ // picker's and the usable list's, as before
1290
+ unary(API, "aiserver.v1.AiService/AvailableModels", tok, { useModelParameters: true, doNotUseMarkdown: true, scope: 3 }, 8_000).then(
1291
+ (c) => (Array.isArray(c?.models) ? c.models : []),
1292
+ () => [],
1293
+ ),
1224
1294
  ])
1225
1295
  const ids = new Set(listed.flatMap((m) => [m?.modelId, m?.displayModelId]).filter(Boolean))
1226
- const raw = modelsOf(Array.isArray(j?.models) ? j.models : [], ids.size ? ids : null, listed)
1296
+ const picker = Array.isArray(j?.models) ? j.models : []
1297
+ const raw = modelsOf(ids.size ? [...picker, ...fromCatalog(picker, catalog, ids)] : picker, ids.size ? ids : null, listed)
1227
1298
  if (!raw.length) throw new Error("Cursor listed no models")
1228
1299
  lists.set(h, { at: Date.now(), raw })
1229
1300
  listFailed.delete(h)
@@ -1330,7 +1401,16 @@ function named(raw, id) {
1330
1401
  return { entry: near, seed: undefined, effort: "", fast: false }
1331
1402
  }
1332
1403
  const old = named(raw, sized[1])
1333
- if (old) return { ...old, max: !old.entry.size && want > old.entry.context }
1404
+ if (old) {
1405
+ // the name's model at the nearest size it has (claude-opus-4-8-thinking@1m,
1406
+ // an id an earlier list had, is Opus 4.8 at 1m with thinking)
1407
+ const sizes = raw.filter((e) => e.size && e.model === old.entry.model)
1408
+ if (old.entry.size && sizes.length) {
1409
+ const near = sizes.reduce((a, b) => (Math.abs(tokensOf(b.size) - want) < Math.abs(tokensOf(a.size) - want) ? b : a))
1410
+ return { ...old, entry: near }
1411
+ }
1412
+ return { ...old, max: !old.entry.size && want > old.entry.context }
1413
+ }
1334
1414
  }
1335
1415
  return null
1336
1416
  }
@@ -3535,4 +3615,4 @@ export async function CursorAuthPlugin(input) {
3535
3615
  }
3536
3616
 
3537
3617
  // for tests
3538
- export const _internal = { credits, kvReply, listFor, lists, keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),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 }
3618
+ export const _internal = { slugFor, fromCatalog, credits, kvReply, listFor, lists, keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),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 }
@@ -796,7 +796,15 @@ export class NativeSessionPool {
796
796
  }
797
797
  // Pending ancestors, new input, and terminal old handles are ordinary
798
798
  // imported history, even while a parent Run is still awaiting its results.
799
- if (chat.messages.some((m) => m?.tool_calls?.some((c) => HANDLE.test(c?.id ?? "")))) return fallback("historical native calls")
799
+ // A fresh user turn whose old native calls all have results imports them
800
+ // into a NEW owner; those results are never submitted to their old exec.
801
+ const historical = chat.messages.flatMap((m) => (m?.tool_calls ?? []).filter((c) => HANDLE.test(c?.id ?? "")))
802
+ if (historical.length) {
803
+ const answered = new Set(chat.messages.filter((m) => m?.role === "tool").map((m) => m.tool_call_id))
804
+ const last = chat.messages.at(-1)
805
+ const freshTurn = last?.role === "user" && !toolAttachment(last)
806
+ if (!freshTurn || historical.some((c) => !answered.has(c.id))) return fallback("historical native calls")
807
+ }
800
808
  if (selector && this.selectors.has(account + ":" + selector)) return fallback("owner busy")
801
809
  return { account, selector, controls, selected: !!selector, chat }
802
810
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
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": "./opencode.mjs",