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.
- package/CHANGELOG.md +95 -0
- package/README.md +204 -37
- package/SECURITY.md +20 -8
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +1 -0
- package/dist/context/build.js +32 -5
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/paths.d.ts +1 -1
- package/dist/context/paths.js +7 -4
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.js +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +114 -23
- package/dist/language-model.js +2290 -469
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +50 -29
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/plugin-opencode2.js +185 -25
- package/dist/plugin.js +28 -5
- package/dist/pricing-data.d.ts +26 -2
- package/dist/pricing-data.js +26 -2
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/messages.js +91 -8
- package/dist/protocol/request.d.ts +35 -19
- package/dist/protocol/request.js +53 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/tool-call-bridge.d.ts +2 -0
- package/dist/protocol/tool-call-bridge.js +10 -0
- package/dist/protocol/tools.d.ts +74 -8
- package/dist/protocol/tools.js +431 -141
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +16 -4
- package/dist/replay-safety.js +34 -25
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +94 -9
- package/dist/session.js +153 -34
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/transport/connect.d.ts +8 -1
- package/dist/transport/connect.js +103 -23
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +63 -29
- package/package.json +3 -2
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
package/dist/context/env.js
CHANGED
|
@@ -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:
|
|
36
|
-
agent_transcripts_folder:
|
|
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
|
}
|
package/dist/context/epoch.js
CHANGED
|
@@ -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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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",
|
package/dist/context/git.d.ts
CHANGED
|
@@ -14,7 +14,10 @@ export type GitRepoInfo = {
|
|
|
14
14
|
branch_name: string;
|
|
15
15
|
remote_url?: string;
|
|
16
16
|
};
|
|
17
|
-
export
|
|
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>;
|
package/dist/context/git.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
}
|
package/dist/context/paths.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/context/paths.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
198
|
-
//
|
|
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
|
-
|
|
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()}
|
|
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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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;
|