cursor-opencode-provider 0.8.1 → 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 (96) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +204 -37
  3. package/SECURITY.md +20 -8
  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 +1 -0
  14. package/dist/context/build.js +32 -5
  15. package/dist/context/env.js +27 -3
  16. package/dist/context/epoch.js +9 -5
  17. package/dist/context/git.d.ts +5 -2
  18. package/dist/context/git.js +17 -7
  19. package/dist/context/paths.d.ts +1 -1
  20. package/dist/context/paths.js +7 -4
  21. package/dist/debug.js +13 -2
  22. package/dist/early-tool-rows.d.ts +60 -0
  23. package/dist/early-tool-rows.js +96 -0
  24. package/dist/errors.d.ts +49 -7
  25. package/dist/errors.js +163 -8
  26. package/dist/host-interrupt.d.ts +12 -0
  27. package/dist/host-interrupt.js +18 -0
  28. package/dist/host-steer.d.ts +71 -0
  29. package/dist/host-steer.js +232 -0
  30. package/dist/image-input.js +4 -3
  31. package/dist/index.d.ts +2 -2
  32. package/dist/language-model.d.ts +114 -23
  33. package/dist/language-model.js +2290 -469
  34. package/dist/lost-request.d.ts +39 -0
  35. package/dist/lost-request.js +111 -0
  36. package/dist/model-config.d.ts +3 -1
  37. package/dist/model-config.js +50 -29
  38. package/dist/models.d.ts +26 -3
  39. package/dist/models.js +101 -45
  40. package/dist/opencode2/tool-results.d.ts +18 -0
  41. package/dist/opencode2/tool-results.js +66 -0
  42. package/dist/plugin-opencode2.js +185 -25
  43. package/dist/plugin.js +28 -5
  44. package/dist/pricing-data.d.ts +26 -2
  45. package/dist/pricing-data.js +26 -2
  46. package/dist/pricing.d.ts +1 -1
  47. package/dist/pricing.js +7 -4
  48. package/dist/process-shared.d.ts +14 -0
  49. package/dist/process-shared.js +18 -0
  50. package/dist/protocol/apply-patch.d.ts +25 -13
  51. package/dist/protocol/apply-patch.js +79 -30
  52. package/dist/protocol/blob-store.d.ts +5 -1
  53. package/dist/protocol/blob-store.js +10 -6
  54. package/dist/protocol/client-version.js +6 -4
  55. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  56. package/dist/protocol/conversation-cache-header.js +149 -0
  57. package/dist/protocol/conversation-persistence.d.ts +27 -1
  58. package/dist/protocol/conversation-persistence.js +628 -126
  59. package/dist/protocol/conversation-state.d.ts +8 -1
  60. package/dist/protocol/conversation-state.js +28 -7
  61. package/dist/protocol/host-notes.d.ts +31 -0
  62. package/dist/protocol/host-notes.js +85 -0
  63. package/dist/protocol/messages.js +91 -8
  64. package/dist/protocol/request.d.ts +35 -19
  65. package/dist/protocol/request.js +53 -28
  66. package/dist/protocol/sent-history-images.d.ts +49 -0
  67. package/dist/protocol/sent-history-images.js +102 -0
  68. package/dist/protocol/tool-call-bridge.d.ts +2 -0
  69. package/dist/protocol/tool-call-bridge.js +10 -0
  70. package/dist/protocol/tools.d.ts +74 -8
  71. package/dist/protocol/tools.js +431 -141
  72. package/dist/protocol/turn-provenance.d.ts +19 -4
  73. package/dist/protocol/turn-provenance.js +77 -20
  74. package/dist/replay-safety.d.ts +16 -4
  75. package/dist/replay-safety.js +34 -25
  76. package/dist/retry-budget.d.ts +21 -0
  77. package/dist/retry-budget.js +94 -0
  78. package/dist/runtime-files.d.ts +23 -0
  79. package/dist/runtime-files.js +581 -0
  80. package/dist/session.d.ts +94 -9
  81. package/dist/session.js +153 -34
  82. package/dist/shared.d.ts +1 -1
  83. package/dist/shared.js +1 -1
  84. package/dist/shell-timeout.d.ts +54 -10
  85. package/dist/shell-timeout.js +362 -92
  86. package/dist/stream-write.d.ts +20 -0
  87. package/dist/stream-write.js +87 -0
  88. package/dist/terminal-runner.d.ts +15 -0
  89. package/dist/terminal-runner.js +528 -0
  90. package/dist/transport/connect.d.ts +8 -1
  91. package/dist/transport/connect.js +103 -23
  92. package/dist/usage.d.ts +37 -17
  93. package/dist/usage.js +63 -29
  94. package/package.json +3 -2
  95. package/dist/protocol/thinking.d.ts +0 -15
  96. package/dist/protocol/thinking.js +0 -17
@@ -1,7 +1,21 @@
1
+ import { statSync } from "node:fs";
1
2
  import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
  import { trace } from "../debug.js";
4
5
  import { ensureOpencodeProjectDir } from "./paths.js";
6
+ import { maintainRuntimeFiles, runtimeLayout } from "../runtime-files.js";
7
+ function agentTranscriptsFolder(workspaceRoot) {
8
+ const configured = process.env.CURSOR_AGENT_TRANSCRIPTS_FOLDER;
9
+ if (!configured?.trim())
10
+ return undefined;
11
+ const folder = path.resolve(workspaceRoot, configured);
12
+ try {
13
+ return statSync(folder).isDirectory() ? folder : undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
5
19
  export function buildEnv(workspaceRoot) {
6
20
  const cwd = path.resolve(workspaceRoot);
7
21
  let timeZone = "UTC";
@@ -20,6 +34,16 @@ export function buildEnv(workspaceRoot) {
20
34
  // Cursor's project_folder is a metadata root (agent-tools, terminals, …),
21
35
  // not the git workspace. Keep dumps under OpenCode cache.
22
36
  const projectFolder = ensureOpencodeProjectDir(cwd);
37
+ let terminalsFolder;
38
+ try {
39
+ const layout = runtimeLayout(projectFolder, true);
40
+ terminalsFolder = layout.terminals;
41
+ maintainRuntimeFiles(layout);
42
+ }
43
+ catch (error) {
44
+ trace(`runtime folders unavailable: ${error instanceof Error ? error.message : String(error)}`);
45
+ }
46
+ const transcriptsFolder = agentTranscriptsFolder(cwd);
23
47
  // process_working_directory must match the workspace, not the host process
24
48
  // cwd. OpenCode 2.0's daemon is long-lived and often started from $HOME, so
25
49
  // process.cwd() would advertise the home folder to Cursor and the model would
@@ -32,15 +56,15 @@ export function buildEnv(workspaceRoot) {
32
56
  sandbox_supported: false,
33
57
  time_zone: timeZone,
34
58
  project_folder: projectFolder,
35
- terminals_folder: path.join(projectFolder, "terminals"),
36
- agent_transcripts_folder: path.join(projectFolder, "agent-transcripts"),
59
+ ...(terminalsFolder ? { terminals_folder: terminalsFolder } : {}),
60
+ ...(transcriptsFolder ? { agent_transcripts_folder: transcriptsFolder } : {}),
37
61
  process_working_directory: cwd,
38
62
  is_working_dir_home_dir: cwd === path.resolve(home),
39
63
  };
40
64
  trace(`buildEnv: workspace_paths=${JSON.stringify(env.workspace_paths)} ` +
41
65
  `project_folder=${env.project_folder} ` +
42
66
  `terminals_folder=${env.terminals_folder} ` +
43
- `agent_transcripts_folder=${env.agent_transcripts_folder} ` +
67
+ `agent_transcripts_folder=${env.agent_transcripts_folder ?? "(none)"} ` +
44
68
  `process_working_directory=${env.process_working_directory}`);
45
69
  return env;
46
70
  }
@@ -153,10 +153,13 @@ export function admitContextEpoch(input) {
153
153
  workspaceRoot: nextSnap.workspaceRoot || existing.snapshot.workspaceRoot,
154
154
  };
155
155
  const updates = renderSourceUpdates(existing.snapshot, observedSnap, input);
156
- const changed = existing.snapshot.hostSystemHash !== observedSnap.hostSystemHash
157
- || existing.snapshot.guidanceHash !== observedSnap.guidanceHash
158
- || existing.snapshot.hostAgent !== observedSnap.hostAgent
159
- || existing.snapshot.workspaceRoot !== observedSnap.workspaceRoot;
156
+ const changedSources = [
157
+ existing.snapshot.hostSystemHash !== observedSnap.hostSystemHash ? "hostSystem" : "",
158
+ existing.snapshot.guidanceHash !== observedSnap.guidanceHash ? "guidance" : "",
159
+ existing.snapshot.hostAgent !== observedSnap.hostAgent ? "hostAgent" : "",
160
+ existing.snapshot.workspaceRoot !== observedSnap.workspaceRoot ? "workspaceRoot" : "",
161
+ ].filter(Boolean);
162
+ const changed = changedSources.length > 0;
160
163
  if (changed) {
161
164
  existing.snapshot = observedSnap;
162
165
  }
@@ -165,7 +168,8 @@ export function admitContextEpoch(input) {
165
168
  const midConversationMessage = combineMessages([...updates, ...oneShots]);
166
169
  if (changed || oneShots.length) {
167
170
  trace(`context epoch: ${changed ? "updated" : "unchanged"} conversationId=${conversationId} ` +
168
- `checkpoint=1 updates=${updates.length} oneShots=${oneShots.length}`);
171
+ `checkpoint=1 updates=${updates.length} oneShots=${oneShots.length}` +
172
+ (changedSources.length ? ` changed=[${changedSources.join(",")}]` : ""));
169
173
  }
170
174
  return {
171
175
  action: changed ? "updated" : "unchanged",
@@ -14,7 +14,10 @@ export type GitRepoInfo = {
14
14
  branch_name: string;
15
15
  remote_url?: string;
16
16
  };
17
- export declare function collectGit(workspaceRoot: string): Promise<{
17
+ export type GitFacts = {
18
18
  repositoryInfo: RepoInfo[];
19
19
  gitRepos: GitRepoInfo[];
20
- }>;
20
+ /** False when `git status` failed or timed out, so the status is not the real one. */
21
+ statusComplete: boolean;
22
+ };
23
+ export declare function collectGit(workspaceRoot: string): Promise<GitFacts>;
@@ -1,21 +1,29 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import path from "node:path";
4
+ import { errorMessage, trace } from "../debug.js";
4
5
  const execFileAsync = promisify(execFile);
6
+ const GIT_TIMEOUT_MS = 5_000;
7
+ /** Trimmed stdout, or `undefined` when git failed or timed out. */
5
8
  async function git(cwd, args) {
9
+ const started = Date.now();
6
10
  try {
7
- const { stdout } = await execFileAsync("git", args, { cwd, encoding: "utf-8", timeout: 5000 });
11
+ const { stdout } = await execFileAsync("git", args, { cwd, encoding: "utf-8", timeout: GIT_TIMEOUT_MS });
8
12
  return stdout.trim();
9
13
  }
10
- catch {
11
- return "";
14
+ catch (error) {
15
+ const killed = error.killed === true;
16
+ trace(`git: \`${args.join(" ")}\` ${killed ? `timed out after ${GIT_TIMEOUT_MS}ms` : "failed"} ` +
17
+ `elapsedMs=${Date.now() - started}${killed ? "" : `: ${errorMessage(error).split("\n")[0]}`}`);
18
+ return undefined;
12
19
  }
13
20
  }
14
21
  export async function collectGit(workspaceRoot) {
22
+ const started = Date.now();
15
23
  const root = await git(workspaceRoot, ["rev-parse", "--show-toplevel"]);
16
24
  if (!root)
17
- return { repositoryInfo: [], gitRepos: [] };
18
- const remotesRaw = await git(root, ["remote", "-v"]);
25
+ return { repositoryInfo: [], gitRepos: [], statusComplete: true };
26
+ const remotesRaw = (await git(root, ["remote", "-v"])) ?? "";
19
27
  const remote_urls = [];
20
28
  const remote_names = [];
21
29
  for (const line of remotesRaw.split("\n")) {
@@ -34,7 +42,9 @@ export async function collectGit(workspaceRoot) {
34
42
  repo_name = gh[2];
35
43
  }
36
44
  const branch = (await git(root, ["rev-parse", "--abbrev-ref", "HEAD"])) || "HEAD";
45
+ const statusStarted = Date.now();
37
46
  const status = await git(root, ["status", "--porcelain", "-b"]);
47
+ trace(`git: discovery elapsedMs=${Date.now() - started} statusMs=${Date.now() - statusStarted} statusComplete=${status !== undefined}`);
38
48
  const repositoryInfo = [
39
49
  {
40
50
  relative_workspace_path: ".",
@@ -50,10 +60,10 @@ export async function collectGit(workspaceRoot) {
50
60
  const gitRepos = [
51
61
  {
52
62
  path: root,
53
- status: status.slice(0, 4000),
63
+ status: (status ?? "").slice(0, 4000),
54
64
  branch_name: branch,
55
65
  ...(primary ? { remote_url: primary } : {}),
56
66
  },
57
67
  ];
58
- return { repositoryInfo, gitRepos };
68
+ return { repositoryInfo, gitRepos, statusComplete: status !== undefined };
59
69
  }
@@ -97,7 +97,7 @@ export declare function slugifyWorkspacePath(workspaceRoot: string): string;
97
97
  * Lives at `<host-cache>/projects/<slug>/` under the resolved OpenCode/host cache root.
98
98
  *
99
99
  * This is what Cursor's RequestContextEnv.project_folder / MCP
100
- * workspace_project_dir point at — agent-tools, terminals, transcripts, etc.
100
+ * workspace_project_dir point at — agent-tools, terminals, assets, etc.
101
101
  * Must NOT be the git workspace, or those dumps land in the repo.
102
102
  */
103
103
  export declare function opencodeProjectDir(workspaceRoot: string): string;
@@ -188,17 +188,20 @@ export function slugifyWorkspacePath(workspaceRoot) {
188
188
  * Lives at `<host-cache>/projects/<slug>/` under the resolved OpenCode/host cache root.
189
189
  *
190
190
  * This is what Cursor's RequestContextEnv.project_folder / MCP
191
- * workspace_project_dir point at — agent-tools, terminals, transcripts, etc.
191
+ * workspace_project_dir point at — agent-tools, terminals, assets, etc.
192
192
  * Must NOT be the git workspace, or those dumps land in the repo.
193
193
  */
194
194
  export function opencodeProjectDir(workspaceRoot) {
195
195
  const projectsRoot = path.join(opencodeGlobalCacheDir(), "projects");
196
196
  const slug = slugifyWorkspacePath(workspaceRoot);
197
- let dir = path.join(projectsRoot, slug);
198
- // Mirror Cursor's long-path guard so nested agent-tools paths stay usable.
197
+ const dir = path.join(projectsRoot, slug);
198
+ // Keep nested agent-tools paths short by cutting the name to 84 characters.
199
+ // While that cut falls after `projects/`, it only shortens the slug, and it
200
+ // is the name existing folders already have. With a longer root it would
201
+ // fall inside the root, so the folder is named by the hash alone.
199
202
  if (dir.length > 92) {
200
203
  const hash = createHash("sha256").update(dir).digest("hex").slice(0, 7);
201
- dir = `${dir.slice(0, Math.min(84, dir.length))}-${hash}`;
204
+ return projectsRoot.length < 84 ? `${dir.slice(0, 84)}-${hash}` : path.join(projectsRoot, hash);
202
205
  }
203
206
  return dir;
204
207
  }
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;