cursor-opencode-provider 0.8.0 → 0.8.3

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 (132) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +230 -44
  3. package/SECURITY.md +60 -12
  4. package/dist/activity.d.ts +73 -1
  5. package/dist/activity.js +231 -5
  6. package/dist/agent-url.js +37 -9
  7. package/dist/auth-renewal.d.ts +1 -1
  8. package/dist/auth-renewal.js +58 -26
  9. package/dist/billing.d.ts +105 -0
  10. package/dist/billing.js +212 -0
  11. package/dist/classic-tool-results.d.ts +1 -0
  12. package/dist/classic-tool-results.js +42 -0
  13. package/dist/context/build.d.ts +23 -4
  14. package/dist/context/build.js +65 -10
  15. package/dist/context/dynamic-catalog.js +6 -1
  16. package/dist/context/env.js +27 -3
  17. package/dist/context/epoch.js +9 -5
  18. package/dist/context/frozen.js +6 -2
  19. package/dist/context/git.d.ts +5 -2
  20. package/dist/context/git.js +17 -7
  21. package/dist/context/index.d.ts +2 -0
  22. package/dist/context/index.js +2 -0
  23. package/dist/context/paths.d.ts +45 -18
  24. package/dist/context/paths.js +72 -22
  25. package/dist/context/skills-bridge.d.ts +22 -0
  26. package/dist/context/skills-bridge.js +20 -0
  27. package/dist/context/skills.d.ts +34 -0
  28. package/dist/context/skills.js +211 -0
  29. package/dist/debug.js +13 -2
  30. package/dist/early-tool-rows.d.ts +60 -0
  31. package/dist/early-tool-rows.js +96 -0
  32. package/dist/errors.d.ts +49 -7
  33. package/dist/errors.js +163 -8
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +117 -13
  36. package/dist/host-interrupt.d.ts +12 -0
  37. package/dist/host-interrupt.js +18 -0
  38. package/dist/host-plan-file.d.ts +28 -0
  39. package/dist/host-plan-file.js +76 -0
  40. package/dist/host-steer.d.ts +71 -0
  41. package/dist/host-steer.js +232 -0
  42. package/dist/image-input.d.ts +18 -0
  43. package/dist/image-input.js +122 -30
  44. package/dist/image-save.d.ts +10 -2
  45. package/dist/image-save.js +33 -21
  46. package/dist/index.d.ts +2 -2
  47. package/dist/language-model.d.ts +149 -30
  48. package/dist/language-model.js +3127 -673
  49. package/dist/lost-request.d.ts +39 -0
  50. package/dist/lost-request.js +111 -0
  51. package/dist/model-config.d.ts +12 -1
  52. package/dist/model-config.js +78 -30
  53. package/dist/models.d.ts +26 -3
  54. package/dist/models.js +101 -45
  55. package/dist/opencode2/catalog.d.ts +1 -0
  56. package/dist/opencode2/catalog.js +2 -0
  57. package/dist/opencode2/image-save-tool.d.ts +8 -0
  58. package/dist/opencode2/image-save-tool.js +84 -0
  59. package/dist/opencode2/tool-results.d.ts +18 -0
  60. package/dist/opencode2/tool-results.js +66 -0
  61. package/dist/opencode2/types.d.ts +15 -0
  62. package/dist/parallel-step.d.ts +65 -0
  63. package/dist/parallel-step.js +102 -0
  64. package/dist/plugin-opencode2.js +233 -65
  65. package/dist/plugin.js +97 -23
  66. package/dist/pricing-data.d.ts +40 -3
  67. package/dist/pricing-data.js +40 -3
  68. package/dist/pricing.d.ts +1 -1
  69. package/dist/pricing.js +7 -4
  70. package/dist/process-shared.d.ts +14 -0
  71. package/dist/process-shared.js +18 -0
  72. package/dist/protocol/apply-patch.d.ts +25 -13
  73. package/dist/protocol/apply-patch.js +79 -30
  74. package/dist/protocol/ask-question.d.ts +12 -1
  75. package/dist/protocol/ask-question.js +77 -3
  76. package/dist/protocol/blob-store.d.ts +5 -1
  77. package/dist/protocol/blob-store.js +10 -6
  78. package/dist/protocol/client-version.js +6 -4
  79. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  80. package/dist/protocol/conversation-cache-header.js +149 -0
  81. package/dist/protocol/conversation-persistence.d.ts +27 -1
  82. package/dist/protocol/conversation-persistence.js +628 -126
  83. package/dist/protocol/conversation-state.d.ts +8 -1
  84. package/dist/protocol/conversation-state.js +28 -7
  85. package/dist/protocol/create-plan.d.ts +47 -25
  86. package/dist/protocol/create-plan.js +33 -23
  87. package/dist/protocol/host-notes.d.ts +31 -0
  88. package/dist/protocol/host-notes.js +85 -0
  89. package/dist/protocol/interactions.d.ts +26 -17
  90. package/dist/protocol/interactions.js +80 -31
  91. package/dist/protocol/messages.js +164 -21
  92. package/dist/protocol/request.d.ts +40 -19
  93. package/dist/protocol/request.js +55 -28
  94. package/dist/protocol/sent-history-images.d.ts +49 -0
  95. package/dist/protocol/sent-history-images.js +102 -0
  96. package/dist/protocol/switch-mode.d.ts +66 -27
  97. package/dist/protocol/switch-mode.js +147 -73
  98. package/dist/protocol/token-details.d.ts +2 -0
  99. package/dist/protocol/token-details.js +11 -1
  100. package/dist/protocol/tool-call-bridge.d.ts +6 -0
  101. package/dist/protocol/tool-call-bridge.js +24 -0
  102. package/dist/protocol/tools.d.ts +90 -9
  103. package/dist/protocol/tools.js +543 -151
  104. package/dist/protocol/turn-provenance.d.ts +19 -4
  105. package/dist/protocol/turn-provenance.js +77 -20
  106. package/dist/replay-safety.d.ts +22 -4
  107. package/dist/replay-safety.js +86 -27
  108. package/dist/retry-budget.d.ts +21 -0
  109. package/dist/retry-budget.js +94 -0
  110. package/dist/runtime-files.d.ts +23 -0
  111. package/dist/runtime-files.js +581 -0
  112. package/dist/session.d.ts +129 -10
  113. package/dist/session.js +198 -35
  114. package/dist/shared.d.ts +1 -1
  115. package/dist/shared.js +1 -1
  116. package/dist/shell-timeout.d.ts +54 -10
  117. package/dist/shell-timeout.js +362 -92
  118. package/dist/stream-write.d.ts +20 -0
  119. package/dist/stream-write.js +87 -0
  120. package/dist/terminal-runner.d.ts +15 -0
  121. package/dist/terminal-runner.js +528 -0
  122. package/dist/todo-store.d.ts +2 -0
  123. package/dist/todo-store.js +19 -0
  124. package/dist/transport/connect.d.ts +10 -1
  125. package/dist/transport/connect.js +124 -27
  126. package/dist/usage.d.ts +37 -17
  127. package/dist/usage.js +85 -34
  128. package/package.json +5 -3
  129. package/dist/plan-execution-kickoff.d.ts +0 -57
  130. package/dist/plan-execution-kickoff.js +0 -186
  131. package/dist/protocol/thinking.d.ts +0 -15
  132. package/dist/protocol/thinking.js +0 -17
@@ -0,0 +1,211 @@
1
+ import { statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { skillsBridge, } from "./skills-bridge.js";
5
+ /**
6
+ * Cursor path-desc `agent_skills`: `{ full_path, description }` only, plus
7
+ * `agent_skills_info_complete: true`. Catalog text in the frozen system-
8
+ * instructions rule is the permission filter and order; locations come from
9
+ * the structural `opencode.host.skills` bridge, else OpenCode 2 `skill.list()`
10
+ * files remembered by the plugin, else OpenCode 1 catalog `<location>`.
11
+ *
12
+ * No content on the wire. Built-in / marker locations stay on the host `skill`
13
+ * tool only.
14
+ */
15
+ // Plugin and language-model can load separate module graphs (see image-staging).
16
+ const HOST_SKILL_FILES = Symbol.for("cursor-opencode-provider.host-skill-files");
17
+ const globals = globalThis;
18
+ const byDirectory = globals[HOST_SKILL_FILES] ??= new Map();
19
+ const MAX_DIRECTORIES = 64;
20
+ const CATALOG = /<available_skills>([\s\S]*?)<\/available_skills>/;
21
+ // Names and descriptions are unescaped; OpenCode 1 escapes locations only.
22
+ const ENTRY_OC2 = /<skill>\s*<id>([\s\S]*?)<\/id>\s*<name>[\s\S]*?<\/name>\s*<description>([\s\S]*?)<\/description>\s*<\/skill>/g;
23
+ const ENTRY_OC1 = /<skill>\s*<name>([\s\S]*?)<\/name>\s*<description>([\s\S]*?)<\/description>\s*<location>([\s\S]*?)<\/location>\s*<\/skill>/g;
24
+ function isFile(file) {
25
+ try {
26
+ return statSync(file).isFile();
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ function hasUriScheme(value) {
33
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
34
+ }
35
+ /** Locations Cursor can advertise; markers / builtins are omitted. */
36
+ export function isUsableSkillLocation(location) {
37
+ if (typeof location !== "string")
38
+ return false;
39
+ const value = location.trim();
40
+ if (!value)
41
+ return false;
42
+ if (value === "<built-in>" || value === "builtin")
43
+ return false;
44
+ if (value.startsWith("/builtin/"))
45
+ return false;
46
+ if (hasUriScheme(value)) {
47
+ if (value.startsWith("file:")) {
48
+ try {
49
+ const filePath = fileURLToPath(value);
50
+ return path.isAbsolute(filePath) && isFile(filePath);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ // Host URIs such as skill://name — keep for bridge hosts; no disk check.
57
+ return true;
58
+ }
59
+ return path.isAbsolute(value) && isFile(value);
60
+ }
61
+ export function normalizeSkillLocation(location) {
62
+ const value = location.trim();
63
+ if (value.startsWith("file:")) {
64
+ try {
65
+ return fileURLToPath(value);
66
+ }
67
+ catch {
68
+ return value;
69
+ }
70
+ }
71
+ return value;
72
+ }
73
+ export function skillToolAdvertised(tools) {
74
+ return (tools ?? []).some((tool) => tool?.name === "skill");
75
+ }
76
+ /** Record skill files a Location's host currently registers (OpenCode 2 plugin). */
77
+ export function rememberHostSkillFiles(directory, skills) {
78
+ const key = path.resolve(directory);
79
+ const files = new Map();
80
+ for (const skill of skills) {
81
+ if (typeof skill?.id !== "string" || typeof skill.path !== "string")
82
+ continue;
83
+ if (!path.isAbsolute(skill.path) || !isFile(skill.path))
84
+ continue;
85
+ files.set(skill.id, skill.path);
86
+ }
87
+ byDirectory.delete(key);
88
+ byDirectory.set(key, files);
89
+ while (byDirectory.size > MAX_DIRECTORIES) {
90
+ const oldest = byDirectory.keys().next().value;
91
+ if (oldest === undefined)
92
+ break;
93
+ byDirectory.delete(oldest);
94
+ }
95
+ }
96
+ export function hostSkillFiles(directory) {
97
+ return directory ? byDirectory.get(path.resolve(directory)) : undefined;
98
+ }
99
+ /** Test helper. */
100
+ export function resetHostSkillFilesForTests() {
101
+ byDirectory.clear();
102
+ }
103
+ /** Undo OpenCode 1's escapeHtml once; literal entity text in paths must survive. */
104
+ function decodeCatalogLocation(value) {
105
+ const entities = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'" };
106
+ return value.replace(/&(?:amp|lt|gt|quot|#39);/g, (entity) => entities[entity]);
107
+ }
108
+ function parseCatalogEntries(systemText) {
109
+ const catalog = CATALOG.exec(systemText)?.[1];
110
+ if (!catalog)
111
+ return [];
112
+ const out = [];
113
+ const seen = new Set();
114
+ for (const match of catalog.matchAll(ENTRY_OC2)) {
115
+ const id = match[1]?.trim();
116
+ const description = match[2] ?? "";
117
+ if (!id || seen.has(id))
118
+ continue;
119
+ seen.add(id);
120
+ out.push({ key: id, description: description.trim() });
121
+ }
122
+ if (out.length > 0)
123
+ return out;
124
+ for (const match of catalog.matchAll(ENTRY_OC1)) {
125
+ const name = match[1]?.trim();
126
+ const description = match[2] ?? "";
127
+ const location = match[3] ? decodeCatalogLocation(match[3].trim()) : undefined;
128
+ if (!name || seen.has(name))
129
+ continue;
130
+ seen.add(name);
131
+ out.push({
132
+ key: name,
133
+ description: description.trim(),
134
+ location: location || undefined,
135
+ });
136
+ }
137
+ return out;
138
+ }
139
+ function locationFromBridge(entry, bridgeSkills) {
140
+ if (!bridgeSkills?.length)
141
+ return undefined;
142
+ const byId = bridgeSkills.find((skill) => skill.id === entry.key && isUsableSkillLocation(skill.location));
143
+ if (byId?.location)
144
+ return normalizeSkillLocation(byId.location);
145
+ const byName = bridgeSkills.find((skill) => skill.name === entry.key && isUsableSkillLocation(skill.location));
146
+ if (byName?.location)
147
+ return normalizeSkillLocation(byName.location);
148
+ return undefined;
149
+ }
150
+ /**
151
+ * path-desc `agent_skills` for cataloged skills that have a usable location,
152
+ * in catalog order. Descriptions come from the catalog (epoch-stable).
153
+ */
154
+ export function agentSkillsForCursor(systemText, options) {
155
+ if (!options.skillToolAdvertised || !systemText)
156
+ return [];
157
+ const entries = parseCatalogEntries(systemText);
158
+ if (entries.length === 0)
159
+ return [];
160
+ const out = [];
161
+ const seenPaths = new Set();
162
+ for (const entry of entries) {
163
+ let location = locationFromBridge(entry, options.bridgeSkills);
164
+ if (!location) {
165
+ const file = options.skillFiles?.get(entry.key);
166
+ if (file && isUsableSkillLocation(file))
167
+ location = file;
168
+ }
169
+ if (!location && entry.location && isUsableSkillLocation(entry.location)) {
170
+ location = normalizeSkillLocation(entry.location);
171
+ }
172
+ if (!location || seenPaths.has(location))
173
+ continue;
174
+ seenPaths.add(location);
175
+ out.push({ full_path: location, description: entry.description });
176
+ }
177
+ return out;
178
+ }
179
+ /** Fetch locations from the structural bridge when installed. */
180
+ export async function loadBridgeSkills(input) {
181
+ const bridge = skillsBridge();
182
+ if (!bridge)
183
+ return undefined;
184
+ try {
185
+ const listed = await bridge.list({
186
+ directory: path.resolve(input.workspaceRoot),
187
+ sessionID: input.sessionID,
188
+ });
189
+ if (!Array.isArray(listed))
190
+ return undefined;
191
+ return listed.filter((skill) => !!skill
192
+ && typeof skill === "object"
193
+ && typeof skill.name === "string"
194
+ && typeof skill.description === "string");
195
+ }
196
+ catch {
197
+ return undefined;
198
+ }
199
+ }
200
+ export function applyAgentSkillsToContext(context, skills) {
201
+ if (skills.length === 0) {
202
+ delete context.agent_skills;
203
+ delete context.agent_skills_info_complete;
204
+ return;
205
+ }
206
+ context.agent_skills = skills.map((skill) => ({
207
+ full_path: skill.full_path,
208
+ description: skill.description,
209
+ }));
210
+ context.agent_skills_info_complete = true;
211
+ }
package/dist/debug.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { processShared } from "./process-shared.js";
4
5
  // Wire-level diagnostics. Opt in with CURSOR_PROVIDER_DEBUG=1 (or "true").
5
6
  // Default path mirrors Cursor CLI: $TMPDIR/cursor-provider-logs-<uid>/debug-<pid>.log
6
7
  // with directory mode 0o700 and file mode 0o600. Override with CURSOR_PROVIDER_DEBUG_FILE.
@@ -14,6 +15,11 @@ const DEBUG_ENABLED = process.env.CURSOR_PROVIDER_DEBUG === "1" ||
14
15
  process.env.CURSOR_PROVIDER_DEBUG === "true";
15
16
  /** Soft cap for the debug log file; exceeded size triggers truncate + new header. */
16
17
  export const DEBUG_LOG_MAX_BYTES = 10 * 1024 * 1024;
18
+ // A host can load this package more than once in one process (a plugin and an
19
+ // AI SDK package import, or one plugin import per location). Count the copies so
20
+ // each banner names its copy and, once there are several, every line says which.
21
+ const moduleCopies = processShared("debug.module-copies.v1", () => ({ count: 0 }));
22
+ const MODULE_COPY = ++moduleCopies.count;
17
23
  let _traceInitialized = false;
18
24
  let _debugFile;
19
25
  let _debugFileUsesManagedDirectory = false;
@@ -73,7 +79,12 @@ function announceLogPath(filePath) {
73
79
  }
74
80
  }
75
81
  function debugBannerLine() {
76
- return `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ---\n`;
82
+ return `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ` +
83
+ `copy=#${MODULE_COPY} module=${import.meta.url} ---\n`;
84
+ }
85
+ /** `#N` on each line once a second copy of this module exists in the process. */
86
+ function copyTag() {
87
+ return moduleCopies.count > 1 ? `#${MODULE_COPY} ` : "";
77
88
  }
78
89
  /**
79
90
  * If `filePath` is at least `maxBytes`, truncate it and write a size-cap header.
@@ -120,7 +131,7 @@ export function trace(msg) {
120
131
  `\n`);
121
132
  }
122
133
  truncateDebugLogIfOversized(_debugFile);
123
- fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] ${msg}\n`);
134
+ fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] ${copyTag()}${msg}\n`);
124
135
  }
125
136
  catch {
126
137
  /* ignore */
@@ -0,0 +1,60 @@
1
+ /** Shown when an announced edit never becomes a host tool call. */
2
+ export declare const EARLY_ROW_CLOSED = "Cursor did not finish this tool call.";
3
+ /**
4
+ * Shown when the announced call runs as a host tool the row did not predict,
5
+ * such as a rewrite with unchanged content, which stays a `write`. The row's
6
+ * tool name cannot change once started (OpenCode 2 rejects a renamed call),
7
+ * so it completes and names the call that carries on.
8
+ */
9
+ export declare function earlyRowHandoff(toolName: string): string;
10
+ export type EarlyToolRow = {
11
+ cursorCallId: string;
12
+ toolCallId: string;
13
+ toolName: string;
14
+ path: string;
15
+ };
16
+ /**
17
+ * Does not match `cursor_<session>_<execId>`. Continuation finds a bound row
18
+ * through the session route table; a row that was closed never gets a route.
19
+ * Cursor's call id is digested, not embedded: it can hold a newline and run to
20
+ * ~90 characters, and the host keeps this id in history for every provider.
21
+ */
22
+ export declare function earlyRowToolCallId(sessionId: string, cursorCallId: string): string;
23
+ /** No exec has this id: the result is this provider's, but its exec is unknown. */
24
+ export declare const UNBOUND_EXEC_ID = -1;
25
+ /**
26
+ * An early-row id whose route is gone (process restart, evicted route) still
27
+ * belongs to this provider. Its result proves the Run lost a completed call,
28
+ * so it must rebuild from host history like any other orphaned result.
29
+ * The session id is a UUID, so it ends at the first underscore.
30
+ */
31
+ export declare function parseEarlyRowToolCallId(toolCallId: string): {
32
+ sessionId: string;
33
+ execId: number;
34
+ } | undefined;
35
+ /** Edit path from a `partial_tool_call`, once Cursor has included one. */
36
+ export declare function cursorEditAnnouncement(partial: unknown): {
37
+ callId: string;
38
+ path: string;
39
+ } | undefined;
40
+ /**
41
+ * Open only while this step cannot end before the exec arrives. A parallel
42
+ * step that is holding can take another row. Otherwise the step ends on the
43
+ * first host call, so a row is useful only before any call or owed exec.
44
+ */
45
+ export declare function shouldOpenEarlyRow(input: {
46
+ allowTools: boolean;
47
+ hostInterrupted: boolean;
48
+ holdingParallelStep: boolean;
49
+ emittedHostTools: number;
50
+ owedPending: number;
51
+ }): boolean;
52
+ export declare class EarlyToolRows {
53
+ private readonly open;
54
+ private readonly skipped;
55
+ seen(callId: string): boolean;
56
+ openRow(row: EarlyToolRow): void;
57
+ skip(callId: string): void;
58
+ claim(callId: string | undefined): EarlyToolRow | undefined;
59
+ drain(): EarlyToolRow[];
60
+ }
@@ -0,0 +1,96 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Shown when an announced edit never becomes a host tool call. */
3
+ export const EARLY_ROW_CLOSED = "Cursor did not finish this tool call.";
4
+ /**
5
+ * Shown when the announced call runs as a host tool the row did not predict,
6
+ * such as a rewrite with unchanged content, which stays a `write`. The row's
7
+ * tool name cannot change once started (OpenCode 2 rejects a renamed call),
8
+ * so it completes and names the call that carries on.
9
+ */
10
+ export function earlyRowHandoff(toolName) {
11
+ return `Cursor continued this call as \`${toolName}\`.`;
12
+ }
13
+ const EARLY_ROW_PREFIX = "cursor-early_";
14
+ /**
15
+ * Does not match `cursor_<session>_<execId>`. Continuation finds a bound row
16
+ * through the session route table; a row that was closed never gets a route.
17
+ * Cursor's call id is digested, not embedded: it can hold a newline and run to
18
+ * ~90 characters, and the host keeps this id in history for every provider.
19
+ */
20
+ export function earlyRowToolCallId(sessionId, cursorCallId) {
21
+ const digest = createHash("sha256").update(cursorCallId).digest("hex").slice(0, 12);
22
+ return `${EARLY_ROW_PREFIX}${sessionId}_${digest}`;
23
+ }
24
+ /** No exec has this id: the result is this provider's, but its exec is unknown. */
25
+ export const UNBOUND_EXEC_ID = -1;
26
+ /**
27
+ * An early-row id whose route is gone (process restart, evicted route) still
28
+ * belongs to this provider. Its result proves the Run lost a completed call,
29
+ * so it must rebuild from host history like any other orphaned result.
30
+ * The session id is a UUID, so it ends at the first underscore.
31
+ */
32
+ export function parseEarlyRowToolCallId(toolCallId) {
33
+ if (!toolCallId.startsWith(EARLY_ROW_PREFIX))
34
+ return undefined;
35
+ const rest = toolCallId.slice(EARLY_ROW_PREFIX.length);
36
+ const separator = rest.indexOf("_");
37
+ if (separator <= 0 || separator === rest.length - 1)
38
+ return undefined;
39
+ return { sessionId: rest.slice(0, separator), execId: UNBOUND_EXEC_ID };
40
+ }
41
+ /** Edit path from a `partial_tool_call`, once Cursor has included one. */
42
+ export function cursorEditAnnouncement(partial) {
43
+ if (!partial || typeof partial !== "object")
44
+ return undefined;
45
+ const callId = partial.call_id;
46
+ const toolCall = partial.tool_call;
47
+ const edit = toolCall && typeof toolCall === "object"
48
+ ? toolCall.edit_tool_call
49
+ : undefined;
50
+ const args = edit && typeof edit === "object" ? edit.args : undefined;
51
+ const filePath = args && typeof args === "object" ? args.path : undefined;
52
+ if (typeof callId !== "string" || callId.length === 0)
53
+ return undefined;
54
+ if (typeof filePath !== "string" || filePath.trim().length === 0)
55
+ return undefined;
56
+ return { callId, path: filePath };
57
+ }
58
+ /**
59
+ * Open only while this step cannot end before the exec arrives. A parallel
60
+ * step that is holding can take another row. Otherwise the step ends on the
61
+ * first host call, so a row is useful only before any call or owed exec.
62
+ */
63
+ export function shouldOpenEarlyRow(input) {
64
+ if (!input.allowTools || input.hostInterrupted)
65
+ return false;
66
+ if (input.holdingParallelStep)
67
+ return true;
68
+ return input.emittedHostTools === 0 && input.owedPending === 0;
69
+ }
70
+ export class EarlyToolRows {
71
+ open = new Map();
72
+ skipped = new Set();
73
+ seen(callId) {
74
+ return this.open.has(callId) || this.skipped.has(callId);
75
+ }
76
+ openRow(row) {
77
+ this.open.set(row.cursorCallId, row);
78
+ }
79
+ skip(callId) {
80
+ this.skipped.add(callId);
81
+ }
82
+ claim(callId) {
83
+ if (!callId)
84
+ return undefined;
85
+ const row = this.open.get(callId);
86
+ if (!row)
87
+ return undefined;
88
+ this.open.delete(callId);
89
+ return row;
90
+ }
91
+ drain() {
92
+ const rows = [...this.open.values()];
93
+ this.open.clear();
94
+ return rows;
95
+ }
96
+ }
package/dist/errors.d.ts CHANGED
@@ -10,6 +10,8 @@ export type CursorProviderErrorOptions = CursorErrorDiagnostics & {
10
10
  origin: CursorErrorOrigin;
11
11
  transient: boolean;
12
12
  replaySafe: boolean;
13
+ retryable?: boolean;
14
+ hostRetryable?: boolean;
13
15
  cause?: unknown;
14
16
  };
15
17
  /** Structured provider failure with retry and transport diagnostics. */
@@ -28,8 +30,37 @@ export declare class CursorProviderError extends Error {
28
30
  readonly rstCode?: number;
29
31
  readonly code?: string;
30
32
  readonly retryAfterMs?: number;
33
+ /** Cursor's classified verdict, or false once provider retries must stop. */
34
+ readonly retryable?: boolean;
35
+ /**
36
+ * The provider stopped, but the host may send the turn again: the failed
37
+ * attempt only showed text or reasoning, before any checkpoint. Cursor CLI
38
+ * resends the same action in this case.
39
+ */
40
+ readonly hostRetryable?: boolean;
41
+ /** What the failed attempt did that a replay inside the Run would repeat. */
42
+ replayBarrier?: "output-only" | "stateful";
31
43
  constructor(message: string, options: CursorProviderErrorOptions);
32
44
  }
45
+ /**
46
+ * The error the host receives. Like Cursor CLI, a request Cursor declared not
47
+ * retryable (see `cursorCliRetryable`), a spent/unsafe provider retry, or an
48
+ * authentication failure ends the turn: it becomes an AI SDK `APICallError`
49
+ * with `isRetryable: false` and the
50
+ * conventional `x-should-retry: false` header, which OpenCode 2 checks first
51
+ * (it retries a failure with no status or recognized code). OpenCode 1.x
52
+ * ignores both and retries any status >= 500 or a message matching its retry
53
+ * patterns, so the status is the Connect code's HTTP status capped below 500
54
+ * (Cursor CLI decides from the verdict, never the code) and the message is
55
+ * sanitized. The original code stays on `cause`.
56
+ *
57
+ * A `hostRetryable` failure is the opposite: Cursor CLI would send the same
58
+ * action again, but output the host already shows stops the provider from
59
+ * doing so. It becomes an `APICallError` with `isRetryable: true`, which
60
+ * OpenCode 1.x retries as the same step, and `x-should-retry: true` without a
61
+ * status, which OpenCode 2 continues after the shown output.
62
+ */
63
+ export declare function hostFacingError(error: unknown): Error;
33
64
  export declare class CursorLocalCancellationError extends CursorProviderError {
34
65
  constructor(message?: string, cause?: unknown);
35
66
  }
@@ -57,13 +88,12 @@ export declare class CursorAuthError extends CursorProviderError {
57
88
  */
58
89
  export declare function isRejectedCredentialError(error: unknown): error is CursorAuthError;
59
90
  /**
60
- * OpenCode's SessionRetry treats several substrings in provider error messages
61
- * as always-retryable — notably bare "unavailable" and "exhausted", plus a
62
- * broader regex set (resource_exhausted, service unavailable, …).
63
- *
64
- * When this provider has already spent its Run retry budget, or has decided a
65
- * replay would be unsafe, the host-facing message must not re-arm that loop.
66
- * Keep the real gRPC/HTTP code on structured fields (`code`, `grpcStatus`).
91
+ * When this provider has already spent its Run retry budget, decided a replay
92
+ * would be unsafe, or Cursor declared the request not retryable, the
93
+ * host-facing message must not re-arm OpenCode's SessionRetry loop. Common
94
+ * trigger words get readable replacements; any remaining match (status-like
95
+ * digits, Cursor's own wording) is split with a word joiner. Keep the real
96
+ * gRPC/HTTP code on structured fields (`code`, `grpcStatus`).
67
97
  */
68
98
  export declare function sanitizeHostTerminalMessage(message: string): string;
69
99
  export declare class CursorRetryExhaustedError extends CursorProviderError {
@@ -79,4 +109,16 @@ export declare function toCursorProviderError(error: unknown, options?: {
79
109
  replaySafe: boolean;
80
110
  fallback?: string;
81
111
  }): CursorProviderError;
112
+ /**
113
+ * A transient failure after the attempt showed only text or reasoning, before
114
+ * any checkpoint of the turn. Cursor CLI sends the same action again; the host
115
+ * does that here (see `hostFacingError`), and the lost-request record carries
116
+ * the user's request once.
117
+ */
118
+ export declare function hostRetryError(cause: CursorProviderError, beforeOutput?: boolean): CursorProviderError;
119
+ /**
120
+ * Cursor CLI stops automatic resumes after two resumed attempts that streamed
121
+ * but saved no checkpoint, so the turn does not repeat the same work forever.
122
+ */
123
+ export declare function noProgressResumeError(cause: CursorProviderError, resumes: number): CursorProviderError;
82
124
  export declare function retrySuppressedError(cause: CursorProviderError, reason: string, attempt: number, maxAttempts: number): CursorProviderError;