cursor-opencode-provider 0.6.0 → 0.6.1

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
@@ -368,8 +368,12 @@ bun run typecheck # type-check without emit
368
368
  bun test # run unit tests
369
369
  bun run test:node-http2 # Node-specific HTTP/2 detach regression
370
370
  bun run test:watch # watch mode
371
+ bun run generate:pricing # refresh src/pricing-data.ts from Cursor docs
372
+ bun run check:pricing # fixture coverage for known model ids
371
373
  ```
372
374
 
375
+ Before any version bump or `v*` tag, run `generate:pricing` and `check:pricing` and commit mapping/`pricing-data.ts` updates. Publish CI regenerates rates from the live docs; an unmapped Cursor display name fails the job.
376
+
373
377
  ## Architecture
374
378
 
375
379
  ```
@@ -16,6 +16,7 @@ async function listLocalPlugins(dir) {
16
16
  catch {
17
17
  return [];
18
18
  }
19
+ entries.sort();
19
20
  for (const name of entries) {
20
21
  if (!/\.(m?[jt]s)$/.test(name))
21
22
  continue;
@@ -109,6 +109,7 @@ async function expandGlob(pattern, workspaceRoot) {
109
109
  catch {
110
110
  return;
111
111
  }
112
+ entries.sort();
112
113
  for (const name of entries) {
113
114
  if (name === "node_modules" || name === ".git")
114
115
  continue;
@@ -63,6 +63,7 @@ async function scanSkillsRoot(root, out) {
63
63
  catch {
64
64
  return;
65
65
  }
66
+ entries.sort();
66
67
  for (const name of entries) {
67
68
  const skillMd = path.join(root, name, "SKILL.md");
68
69
  if (!(await exists(skillMd)))
@@ -625,12 +625,18 @@ async function startSession(modelId, token, callOptions, options, startOptions)
625
625
  // Compaction must not reuse the prior conversation; its first normal turn
626
626
  // must also rebase so the summary-agent checkpoint cannot replace the normal
627
627
  // system prompt and OpenCode's newly compacted history.
628
+ const lifecycle = !allowTools && !isCompaction && !recovery;
628
629
  let bound = resuming
629
630
  ? { conversationId: resumeRecovery.conversationId, reset: false, previousId: undefined }
630
- : bindConversationId(sessionKey, { reset: resetState.reset || recovery?.kind === "rebase" });
631
- let conversationState = resuming
632
- ? resumeRecovery.checkpoint
633
- : (bound.reset ? undefined : getCheckpoint(bound.conversationId));
631
+ : bindConversationId(sessionKey, {
632
+ reset: resetState.reset || recovery?.kind === "rebase",
633
+ ephemeral: lifecycle,
634
+ });
635
+ let conversationState = lifecycle
636
+ ? undefined
637
+ : resuming
638
+ ? resumeRecovery.checkpoint
639
+ : (bound.reset ? undefined : getCheckpoint(bound.conversationId));
634
640
  let checkpointGraph = conversationState
635
641
  ? inspectConversationBlobGraph(bound.conversationId, conversationState)
636
642
  : { count: 0, bytes: 0, complete: true };
@@ -685,12 +691,13 @@ async function startSession(modelId, token, callOptions, options, startOptions)
685
691
  const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot);
686
692
  // After an approved SwitchMode, inject the Cursor CLI-shaped mode reminder
687
693
  // (same <system_reminder> contract the CLI uses after flipping unifiedMode).
688
- const modeReminder = isCompaction
694
+ const startedWithCheckpoint = !!conversationState;
695
+ const modeReminder = isCompaction || startedWithCheckpoint || lifecycle
689
696
  ? undefined
690
697
  : takeActiveCursorModeReminder(sessionKey, {
691
698
  advertisedTools: cursorTools.map((tool) => tool.name),
692
699
  });
693
- const kickoffWarning = isCompaction
700
+ const kickoffWarning = isCompaction || startedWithCheckpoint || lifecycle
694
701
  ? undefined
695
702
  : takePlanExecutionKickoffWarning(sessionKey);
696
703
  const systemPrompt = [
@@ -898,7 +905,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
898
905
  displayToolCalls: 0,
899
906
  execRequests: 0,
900
907
  },
901
- openCodeSessionId: sessionKey,
908
+ openCodeSessionId: lifecycle ? undefined : sessionKey,
902
909
  postCompactionRebase: isCompaction,
903
910
  toolCatalog: snapshotToolCatalog(sessionKey),
904
911
  stream,
@@ -2790,6 +2797,9 @@ export function spanEndParts(opts) {
2790
2797
  out.push({ type: "text-end", id: opts.textId });
2791
2798
  return out;
2792
2799
  }
2800
+ function toolsInFixedOrder(tools) {
2801
+ return tools.map((tool) => ({ ...tool })).sort((left, right) => (left.name ?? "").localeCompare(right.name ?? ""));
2802
+ }
2793
2803
  /** Exported for tests — false for compaction/summary (no tools) and toolChoice none. */
2794
2804
  export function computeAllowTools(toolCount, toolChoice) {
2795
2805
  return toolCount > 0 && toolChoice?.type !== "none";
@@ -2797,7 +2807,7 @@ export function computeAllowTools(toolCount, toolChoice) {
2797
2807
  export async function resolveTurnToolState(input) {
2798
2808
  const { sessionKey, incomingTools, isCompaction } = input;
2799
2809
  if (sessionKey && incomingTools.length > 0) {
2800
- rememberToolCatalog(sessionKey, incomingTools.map((tool) => ({ ...tool })));
2810
+ rememberToolCatalog(sessionKey, toolsInFixedOrder(incomingTools));
2801
2811
  }
2802
2812
  // Advertisement and permission are deliberately independent.
2803
2813
  //
@@ -2818,12 +2828,12 @@ export async function resolveTurnToolState(input) {
2818
2828
  // still refuse execution and cannot duplicate the real turn's side effects.
2819
2829
  let advertisedTools;
2820
2830
  if (incomingTools.length > 0) {
2821
- advertisedTools = incomingTools;
2831
+ advertisedTools = toolsInFixedOrder(incomingTools);
2822
2832
  }
2823
2833
  else if (sessionKey) {
2824
2834
  const cached = toolCatalogBySession.get(sessionKey)
2825
2835
  ?? await waitForSiblingToolCatalog(sessionKey, input.abortSignal);
2826
- advertisedTools = cached.map((tool) => ({ ...tool }));
2836
+ advertisedTools = toolsInFixedOrder(cached);
2827
2837
  }
2828
2838
  else {
2829
2839
  // Without a session key there is no safe sibling-correlation key. Preserve
@@ -24,6 +24,7 @@ export declare function peekConversationId(sessionKey: string): string;
24
24
  */
25
25
  export declare function bindConversationId(sessionKey: string | undefined, opts?: {
26
26
  reset?: boolean;
27
+ ephemeral?: boolean;
27
28
  }): {
28
29
  conversationId: string;
29
30
  reset: boolean;
@@ -76,6 +76,9 @@ export function peekConversationId(sessionKey) {
76
76
  * mint a new id so TurnEnded cache_read cannot keep overflowing OpenCode.
77
77
  */
78
78
  export function bindConversationId(sessionKey, opts) {
79
+ if (opts?.ephemeral) {
80
+ return { conversationId: crypto.randomUUID(), reset: false };
81
+ }
79
82
  if (!sessionKey) {
80
83
  return { conversationId: crypto.randomUUID(), reset: !!opts?.reset };
81
84
  }
@@ -249,7 +249,7 @@ export function encodeJsonAsValue(v) {
249
249
  }
250
250
  else if (typeof v === "object") {
251
251
  const st = [];
252
- for (const [key, val] of Object.entries(v)) {
252
+ for (const [key, val] of Object.entries(v).sort(([left], [right]) => left.localeCompare(right))) {
253
253
  if (val === undefined)
254
254
  continue;
255
255
  const entry = [];
@@ -57,7 +57,9 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
57
57
  * reconstructed in `mcpRealToolName`.
58
58
  */
59
59
  export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
60
- return tools.map((t) => {
60
+ return [...tools]
61
+ .sort((left, right) => left.name.localeCompare(right.name))
62
+ .map((t) => {
61
63
  const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
62
64
  const collisionSafeAlias = COLLISION_SAFE_ALIASES.has(t.name);
63
65
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
5
5
  "type": "module",
6
6
  "license": "MIT",