cursor-opencode-provider 0.7.6 → 0.8.0
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 +9 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +42 -5
- package/dist/context/build.js +96 -92
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +10 -160
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +37 -14
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +7 -4
- package/dist/context/paths.js +13 -9
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.js +2 -2
- package/dist/image-save.js +3 -3
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +41 -8
- package/dist/language-model.js +391 -98
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/plan-execution-kickoff.js +2 -2
- package/dist/plugin-opencode2.js +10 -16
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +138 -109
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.js +2 -1
- package/dist/protocol/messages.js +22 -4
- package/dist/protocol/request.d.ts +11 -9
- package/dist/protocol/request.js +15 -25
- package/dist/protocol/tools.d.ts +17 -10
- package/dist/protocol/tools.js +53 -93
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/session.d.ts +9 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/package.json +1 -1
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/context/skills.d.ts +0 -11
- package/dist/context/skills.js +0 -110
package/dist/context/paths.js
CHANGED
|
@@ -68,9 +68,18 @@ export function getHostCacheDirOverride() {
|
|
|
68
68
|
export function resolveHostCacheDir(env = process.env) {
|
|
69
69
|
return bridgeGlobalCacheDir() ?? openCodeGlobalCacheDir(env);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
function xdgConfigHome(env = process.env) {
|
|
72
|
+
if (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0)
|
|
73
|
+
return env.XDG_CONFIG_HOME;
|
|
74
|
+
return path.join(resolveHome(env), ".config");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Native OpenCode global config dir: `$XDG_CONFIG_HOME/opencode`, otherwise
|
|
78
|
+
* `~/.config/opencode` — OpenCode's `Global.Path.config` (`xdg-basedir`
|
|
79
|
+
* `xdgConfig`, `packages/core/src/global.ts`), the same in 1.x and 2.0.
|
|
80
|
+
*/
|
|
81
|
+
export function opencodeGlobalConfigDir(env = process.env) {
|
|
82
|
+
return path.join(xdgConfigHome(env), "opencode");
|
|
74
83
|
}
|
|
75
84
|
/**
|
|
76
85
|
* Host global cache dir for Cursor project metadata + model/version caches.
|
|
@@ -116,7 +125,7 @@ export function hostPlansDir(_workspaceRoot) {
|
|
|
116
125
|
return path.join(hostGlobalDataDir(), "plans");
|
|
117
126
|
}
|
|
118
127
|
/**
|
|
119
|
-
* Cursor-compatible path slug (`/
|
|
128
|
+
* Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
|
|
120
129
|
* Used for per-workspace metadata under the host cache.
|
|
121
130
|
*/
|
|
122
131
|
export function slugifyWorkspacePath(workspaceRoot) {
|
|
@@ -157,8 +166,3 @@ export function ensureOpencodeProjectDir(workspaceRoot) {
|
|
|
157
166
|
`xdg_cache_home=${process.env.XDG_CACHE_HOME ?? "(unset)"}`);
|
|
158
167
|
return dir;
|
|
159
168
|
}
|
|
160
|
-
export function resolveHomeRelative(p) {
|
|
161
|
-
if (p.startsWith("~/"))
|
|
162
|
-
return path.join(homedir(), p.slice(2));
|
|
163
|
-
return p;
|
|
164
|
-
}
|
package/dist/context/rules.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export type CollectedRule = {
|
|
2
|
-
fullPath: string;
|
|
3
|
-
content: string;
|
|
4
|
-
};
|
|
5
1
|
export type OpencodeJson = {
|
|
6
2
|
instructions?: string[];
|
|
7
3
|
permission?: unknown;
|
|
@@ -9,18 +5,10 @@ export type OpencodeJson = {
|
|
|
9
5
|
plugins?: string[];
|
|
10
6
|
mcp?: Record<string, unknown>;
|
|
11
7
|
};
|
|
12
|
-
export declare function findGitWorktree(start: string): Promise<string>;
|
|
13
8
|
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
14
9
|
export declare function isProjectConfigDisabled(): boolean;
|
|
15
|
-
/** Fetch a remote instruction with one deadline covering headers and body. */
|
|
16
|
-
export declare function fetchRemoteInstruction(url: string, timeoutMs?: number): Promise<string | undefined>;
|
|
17
|
-
export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
|
|
18
10
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
11
|
+
* Merged `opencode.json` / `opencode.jsonc` for MCP server ids, plugin lists,
|
|
12
|
+
* and interaction guidance. Instruction file bodies are not collected here.
|
|
21
13
|
*/
|
|
22
|
-
export declare function
|
|
23
|
-
rules: CollectedRule[];
|
|
24
|
-
config: OpencodeJson;
|
|
25
|
-
worktree: string;
|
|
26
|
-
}>;
|
|
14
|
+
export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
|
package/dist/context/rules.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readFile,
|
|
2
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs
|
|
3
|
+
import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
|
|
5
4
|
async function exists(file) {
|
|
6
5
|
try {
|
|
7
6
|
await stat(file);
|
|
@@ -18,7 +17,10 @@ async function readJsonConfig(dir) {
|
|
|
18
17
|
continue;
|
|
19
18
|
try {
|
|
20
19
|
const raw = await readFile(file, "utf-8");
|
|
21
|
-
|
|
20
|
+
// Match strings first so comment markers inside URLs/plugin specifiers
|
|
21
|
+
// survive. Remove comments before trailing commas (which may precede one).
|
|
22
|
+
const uncommented = raw.replace(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, token => token.startsWith('"') ? token : " ");
|
|
23
|
+
const stripped = uncommented.replace(/"(?:\\.|[^"\\])*"|,\s*(?=[}\]])/g, token => token.startsWith('"') ? token : "");
|
|
22
24
|
return JSON.parse(stripped);
|
|
23
25
|
}
|
|
24
26
|
catch {
|
|
@@ -27,131 +29,11 @@ async function readJsonConfig(dir) {
|
|
|
27
29
|
}
|
|
28
30
|
return {};
|
|
29
31
|
}
|
|
30
|
-
export async function findGitWorktree(start) {
|
|
31
|
-
let dir = path.resolve(start);
|
|
32
|
-
for (;;) {
|
|
33
|
-
if (await exists(path.join(dir, ".git")))
|
|
34
|
-
return dir;
|
|
35
|
-
const parent = path.dirname(dir);
|
|
36
|
-
if (parent === dir)
|
|
37
|
-
return path.resolve(start);
|
|
38
|
-
dir = parent;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async function findUp(name, start, stop) {
|
|
42
|
-
let dir = path.resolve(start);
|
|
43
|
-
const root = path.resolve(stop);
|
|
44
|
-
for (;;) {
|
|
45
|
-
const candidate = path.join(dir, name);
|
|
46
|
-
if (await exists(candidate))
|
|
47
|
-
return candidate;
|
|
48
|
-
if (dir === root)
|
|
49
|
-
return undefined;
|
|
50
|
-
const parent = path.dirname(dir);
|
|
51
|
-
if (parent === dir)
|
|
52
|
-
return undefined;
|
|
53
|
-
dir = parent;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function readRule(file) {
|
|
57
|
-
if (!(await exists(file)))
|
|
58
|
-
return undefined;
|
|
59
|
-
try {
|
|
60
|
-
const content = await readFile(file, "utf-8");
|
|
61
|
-
if (!content.trim())
|
|
62
|
-
return undefined;
|
|
63
|
-
return { fullPath: path.resolve(file), content };
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function globToRegExp(glob) {
|
|
70
|
-
const norm = glob.replace(/\\/g, "/");
|
|
71
|
-
let re = "^";
|
|
72
|
-
for (let i = 0; i < norm.length; i++) {
|
|
73
|
-
const c = norm[i];
|
|
74
|
-
if (c === "*") {
|
|
75
|
-
if (norm[i + 1] === "*") {
|
|
76
|
-
re += ".*";
|
|
77
|
-
i++;
|
|
78
|
-
if (norm[i + 1] === "/")
|
|
79
|
-
i++;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
re += "[^/]*";
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else if (".$^+?()[]{}|".includes(c) || c === "\\") {
|
|
86
|
-
re += "\\" + c;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
re += c;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return new RegExp(re + "$");
|
|
93
|
-
}
|
|
94
|
-
async function expandGlob(pattern, workspaceRoot) {
|
|
95
|
-
const abs = path.isAbsolute(pattern) ? pattern : path.join(workspaceRoot, pattern);
|
|
96
|
-
if (!abs.includes("*"))
|
|
97
|
-
return (await exists(abs)) ? [abs] : [];
|
|
98
|
-
const out = [];
|
|
99
|
-
const base = abs.split("*")[0] || workspaceRoot;
|
|
100
|
-
const startDir = path.dirname(base.endsWith("/") ? base : base);
|
|
101
|
-
const regex = globToRegExp(abs);
|
|
102
|
-
async function walk(dir, depth) {
|
|
103
|
-
if (depth > 8)
|
|
104
|
-
return;
|
|
105
|
-
let entries;
|
|
106
|
-
try {
|
|
107
|
-
entries = await readdir(dir);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
entries.sort();
|
|
113
|
-
for (const name of entries) {
|
|
114
|
-
if (name === "node_modules" || name === ".git")
|
|
115
|
-
continue;
|
|
116
|
-
const full = path.join(dir, name);
|
|
117
|
-
let st;
|
|
118
|
-
try {
|
|
119
|
-
st = await stat(full);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (st.isDirectory())
|
|
125
|
-
await walk(full, depth + 1);
|
|
126
|
-
else if (regex.test(full.replace(/\\/g, "/")))
|
|
127
|
-
out.push(full);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
await walk(startDir, 0);
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
32
|
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
134
33
|
export function isProjectConfigDisabled() {
|
|
135
34
|
const value = process.env.OPENCODE_DISABLE_PROJECT_CONFIG?.toLowerCase();
|
|
136
35
|
return value === "true" || value === "1";
|
|
137
36
|
}
|
|
138
|
-
/** Fetch a remote instruction with one deadline covering headers and body. */
|
|
139
|
-
export async function fetchRemoteInstruction(url, timeoutMs = 5000) {
|
|
140
|
-
const ctrl = new AbortController();
|
|
141
|
-
const timer = setTimeout(() => ctrl.abort(), Math.max(1, timeoutMs));
|
|
142
|
-
try {
|
|
143
|
-
const res = await fetch(url, { signal: ctrl.signal });
|
|
144
|
-
if (!res.ok)
|
|
145
|
-
return undefined;
|
|
146
|
-
return await res.text();
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
clearTimeout(timer);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
37
|
function mergeConfig(base, overlay) {
|
|
156
38
|
return {
|
|
157
39
|
...base,
|
|
@@ -163,6 +45,10 @@ function mergeConfig(base, overlay) {
|
|
|
163
45
|
permission: overlay.permission ?? base.permission,
|
|
164
46
|
};
|
|
165
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Merged `opencode.json` / `opencode.jsonc` for MCP server ids, plugin lists,
|
|
50
|
+
* and interaction guidance. Instruction file bodies are not collected here.
|
|
51
|
+
*/
|
|
166
52
|
export async function loadMergedConfig(workspaceRoot) {
|
|
167
53
|
const globalConfig = await readJsonConfig(opencodeGlobalConfigDirs()[0] ?? "");
|
|
168
54
|
if (isProjectConfigDisabled())
|
|
@@ -176,65 +62,3 @@ export async function loadMergedConfig(workspaceRoot) {
|
|
|
176
62
|
}
|
|
177
63
|
return mergeConfig(globalConfig, projectConfig);
|
|
178
64
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Collect OpenCode instruction files.
|
|
181
|
-
* `preloadedConfig` reuses a merged config already loaded on this Run.
|
|
182
|
-
*/
|
|
183
|
-
export async function collectRules(workspaceRoot, preloadedConfig) {
|
|
184
|
-
const worktree = await findGitWorktree(workspaceRoot);
|
|
185
|
-
const rules = [];
|
|
186
|
-
const seen = new Set();
|
|
187
|
-
const config = preloadedConfig ?? await loadMergedConfig(workspaceRoot);
|
|
188
|
-
const add = async (file) => {
|
|
189
|
-
if (!file)
|
|
190
|
-
return;
|
|
191
|
-
const resolved = path.resolve(file);
|
|
192
|
-
if (seen.has(resolved))
|
|
193
|
-
return;
|
|
194
|
-
const rule = await readRule(resolved);
|
|
195
|
-
if (!rule)
|
|
196
|
-
return;
|
|
197
|
-
seen.add(resolved);
|
|
198
|
-
rules.push(rule);
|
|
199
|
-
};
|
|
200
|
-
// Match OpenCode: OPENCODE_DISABLE_PROJECT_CONFIG skips project AGENTS/CLAUDE/CONTEXT
|
|
201
|
-
// discovery and project opencode.json (see loadMergedConfig).
|
|
202
|
-
if (!isProjectConfigDisabled()) {
|
|
203
|
-
for (const name of ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"]) {
|
|
204
|
-
const hit = await findUp(name, workspaceRoot, worktree);
|
|
205
|
-
if (hit) {
|
|
206
|
-
await add(hit);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
for (const globalDir of opencodeGlobalConfigDirs()) {
|
|
212
|
-
await add(path.join(globalDir, "AGENTS.md"));
|
|
213
|
-
}
|
|
214
|
-
await add(path.join(homedir(), ".claude", "CLAUDE.md"));
|
|
215
|
-
for (const raw of config.instructions ?? []) {
|
|
216
|
-
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
|
217
|
-
// HTTPS-only. Redirects (incl. to a local proxy) are intentional — do not
|
|
218
|
-
// disable follow-redirects or reject localhost/private/metadata hosts.
|
|
219
|
-
let remoteUrl;
|
|
220
|
-
try {
|
|
221
|
-
remoteUrl = new URL(raw);
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (remoteUrl.protocol !== "https:")
|
|
227
|
-
continue;
|
|
228
|
-
const content = await fetchRemoteInstruction(remoteUrl.href);
|
|
229
|
-
if (!content?.trim() || seen.has(raw))
|
|
230
|
-
continue;
|
|
231
|
-
seen.add(raw);
|
|
232
|
-
rules.push({ fullPath: raw, content });
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
const expanded = resolveHomeRelative(raw);
|
|
236
|
-
for (const m of await expandGlob(expanded, workspaceRoot))
|
|
237
|
-
await add(m);
|
|
238
|
-
}
|
|
239
|
-
return { rules, config, worktree };
|
|
240
|
-
}
|
package/dist/debug.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Soft cap for the debug log file; exceeded size triggers truncate + new header. */
|
|
2
2
|
export declare const DEBUG_LOG_MAX_BYTES: number;
|
|
3
|
+
/** Message of an unknown thrown value, for log lines and error text. */
|
|
4
|
+
export declare function errorMessage(error: unknown): string;
|
|
3
5
|
/** Whether `CURSOR_PROVIDER_DEBUG` is enabled for this process. */
|
|
4
6
|
export declare function isDebugEnabled(): boolean;
|
|
5
7
|
/** Resolve the debug log path (env override or per-uid tmpdir default). */
|
package/dist/debug.js
CHANGED
|
@@ -18,6 +18,10 @@ let _traceInitialized = false;
|
|
|
18
18
|
let _debugFile;
|
|
19
19
|
let _debugFileUsesManagedDirectory = false;
|
|
20
20
|
let _announcedLogPath = false;
|
|
21
|
+
/** Message of an unknown thrown value, for log lines and error text. */
|
|
22
|
+
export function errorMessage(error) {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
21
25
|
/** Whether `CURSOR_PROVIDER_DEBUG` is enabled for this process. */
|
|
22
26
|
export function isDebugEnabled() {
|
|
23
27
|
return DEBUG_ENABLED;
|
package/dist/errors.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export declare class CursorProviderError extends Error {
|
|
|
17
17
|
readonly origin: CursorErrorOrigin;
|
|
18
18
|
readonly transient: boolean;
|
|
19
19
|
replaySafe: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The Run failed because Cursor could not restore the resumed checkpoint
|
|
22
|
+
* (it requested blobs this client does not hold) before producing anything.
|
|
23
|
+
* Recovery must reseed a new conversation instead of resuming that checkpoint.
|
|
24
|
+
*/
|
|
25
|
+
checkpointUnusable?: boolean;
|
|
20
26
|
readonly statusCode?: number;
|
|
21
27
|
readonly grpcStatus?: number | string;
|
|
22
28
|
readonly rstCode?: number;
|
|
@@ -41,8 +47,15 @@ export declare class CursorProtocolError extends CursorProviderError {
|
|
|
41
47
|
export declare class CursorAuthError extends CursorProviderError {
|
|
42
48
|
constructor(message?: string, options?: Partial<CursorErrorDiagnostics> & {
|
|
43
49
|
cause?: unknown;
|
|
50
|
+
replaySafe?: boolean;
|
|
44
51
|
});
|
|
45
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Cursor refused the request's credential (HTTP 401 / gRPC unauthenticated),
|
|
55
|
+
* as opposed to refusing the account an action (403 / permission_denied).
|
|
56
|
+
* Only this case is worth one renewed-token retry.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isRejectedCredentialError(error: unknown): error is CursorAuthError;
|
|
46
59
|
/**
|
|
47
60
|
* OpenCode's SessionRetry treats several substrings in provider error messages
|
|
48
61
|
* as always-retryable — notably bare "unavailable" and "exhausted", plus a
|
package/dist/errors.js
CHANGED
|
@@ -3,6 +3,12 @@ export class CursorProviderError extends Error {
|
|
|
3
3
|
origin;
|
|
4
4
|
transient;
|
|
5
5
|
replaySafe;
|
|
6
|
+
/**
|
|
7
|
+
* The Run failed because Cursor could not restore the resumed checkpoint
|
|
8
|
+
* (it requested blobs this client does not hold) before producing anything.
|
|
9
|
+
* Recovery must reseed a new conversation instead of resuming that checkpoint.
|
|
10
|
+
*/
|
|
11
|
+
checkpointUnusable;
|
|
6
12
|
statusCode;
|
|
7
13
|
grpcStatus;
|
|
8
14
|
rstCode;
|
|
@@ -64,11 +70,27 @@ export class CursorAuthError extends CursorProviderError {
|
|
|
64
70
|
...options,
|
|
65
71
|
origin: "auth",
|
|
66
72
|
transient: false,
|
|
67
|
-
replaySafe: false,
|
|
73
|
+
replaySafe: options.replaySafe ?? false,
|
|
68
74
|
});
|
|
69
75
|
this.name = "CursorAuthError";
|
|
70
76
|
}
|
|
71
77
|
}
|
|
78
|
+
function isUnauthenticatedStatus(status) {
|
|
79
|
+
if (status === undefined)
|
|
80
|
+
return false;
|
|
81
|
+
const normalized = String(status).toLowerCase().replaceAll("-", "_");
|
|
82
|
+
return normalized === "16" || normalized === "unauthenticated";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Cursor refused the request's credential (HTTP 401 / gRPC unauthenticated),
|
|
86
|
+
* as opposed to refusing the account an action (403 / permission_denied).
|
|
87
|
+
* Only this case is worth one renewed-token retry.
|
|
88
|
+
*/
|
|
89
|
+
export function isRejectedCredentialError(error) {
|
|
90
|
+
return error instanceof CursorAuthError && (error.statusCode === 401 ||
|
|
91
|
+
isUnauthenticatedStatus(error.grpcStatus) ||
|
|
92
|
+
isUnauthenticatedStatus(error.code));
|
|
93
|
+
}
|
|
72
94
|
/**
|
|
73
95
|
* OpenCode's SessionRetry treats several substrings in provider error messages
|
|
74
96
|
* as always-retryable — notably bare "unavailable" and "exhausted", plus a
|
|
@@ -141,7 +163,9 @@ export function isAuthGrpcStatus(status) {
|
|
|
141
163
|
}
|
|
142
164
|
export function cursorHttpError(operation, statusCode, diagnostics = {}) {
|
|
143
165
|
if (statusCode === 401 || statusCode === 403) {
|
|
144
|
-
|
|
166
|
+
// A rejected credential means the server did no work: replaying with a
|
|
167
|
+
// renewed token is safe unless the attempt already crossed a barrier.
|
|
168
|
+
return new CursorAuthError(`Cursor authentication failed (HTTP ${statusCode}); reauthenticate with Cursor`, { ...diagnostics, statusCode, replaySafe: statusCode === 401 });
|
|
145
169
|
}
|
|
146
170
|
return new CursorServerError(`${operation} HTTP ${statusCode}`, {
|
|
147
171
|
...diagnostics,
|
|
@@ -152,7 +176,7 @@ export function cursorHttpError(operation, statusCode, diagnostics = {}) {
|
|
|
152
176
|
}
|
|
153
177
|
export function cursorGrpcError(operation, grpcStatus, diagnostics = {}) {
|
|
154
178
|
if (isAuthGrpcStatus(grpcStatus)) {
|
|
155
|
-
return new CursorAuthError(`Cursor authentication failed (gRPC ${grpcStatus}); reauthenticate with Cursor`, { ...diagnostics, grpcStatus });
|
|
179
|
+
return new CursorAuthError(`Cursor authentication failed (gRPC ${grpcStatus}); reauthenticate with Cursor`, { ...diagnostics, grpcStatus, replaySafe: isUnauthenticatedStatus(grpcStatus) });
|
|
156
180
|
}
|
|
157
181
|
return new CursorServerError(`${operation} gRPC status ${grpcStatus}`, {
|
|
158
182
|
...diagnostics,
|
package/dist/host-agent-mode.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents;
|
|
8
8
|
* OpenCode 1.x continues to use advertised `plan_enter` / `plan_exit` tools.
|
|
9
9
|
*/
|
|
10
|
-
import { trace } from "./debug.js";
|
|
10
|
+
import { errorMessage, trace } from "./debug.js";
|
|
11
11
|
let switchHostAgent;
|
|
12
12
|
const pending = new Map();
|
|
13
13
|
const MAX_PENDING_HOST_AGENT_SWITCHES = 256;
|
|
@@ -82,7 +82,7 @@ export async function flushHostAgentModeSwitch(sessionID, options = {}) {
|
|
|
82
82
|
// behavioral fallback until the native host switch succeeds.
|
|
83
83
|
delete state.cursorSessionID;
|
|
84
84
|
trace(`host-agent-mode: FAILED sessionID=${key} target=${state.targetModeID} ` +
|
|
85
|
-
`attempts=${state.attempts} err=${
|
|
85
|
+
`attempts=${state.attempts} err=${errorMessage(error)}`);
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
88
88
|
}
|
package/dist/image-save.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import { trace } from "./debug.js";
|
|
11
|
+
import { errorMessage, trace } from "./debug.js";
|
|
12
12
|
import { takePendingCursorImage } from "./image-staging.js";
|
|
13
13
|
/**
|
|
14
14
|
* Marks a refusal that must reach Cursor as `WriteResult.permission_denied`
|
|
@@ -150,7 +150,7 @@ export async function executeCursorImageSave(args, ctx) {
|
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
catch (error) {
|
|
153
|
-
const reason =
|
|
153
|
+
const reason = errorMessage(error);
|
|
154
154
|
trace(`image save: permission refused path=${JSON.stringify(contained.path)} reason=${reason}`);
|
|
155
155
|
throw new Error(`${IMAGE_PERMISSION_DENIED_PREFIX} ${reason}`);
|
|
156
156
|
}
|
|
@@ -165,7 +165,7 @@ export async function executeCursorImageSave(args, ctx) {
|
|
|
165
165
|
catch (error) {
|
|
166
166
|
// Most often something already occupies a path component (EEXIST/ENOTDIR).
|
|
167
167
|
// Say which path is blocked; the raw errno alone tells the model nothing.
|
|
168
|
-
const reason =
|
|
168
|
+
const reason = errorMessage(error);
|
|
169
169
|
trace(`image save: write failed path=${JSON.stringify(contained.path)} reason=${reason}`);
|
|
170
170
|
throw new Error(`Could not save the generated image to ${contained.path}: ${reason}. `
|
|
171
171
|
+ `Check that ${directory} is a writable directory.`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CursorPlugin } from "./plugin.js";
|
|
2
2
|
import type { CursorContinuationOptions } from "./session.js";
|
|
3
3
|
import type { HostToolDialect } from "./protocol/tools.js";
|
|
4
|
+
import type { AccessTokenProvider } from "./auth-renewal.js";
|
|
4
5
|
export type CursorRetryOptions = {
|
|
5
6
|
/** Total attempts including the initial request. Default: 3. */
|
|
6
7
|
maxAttempts?: number;
|
|
@@ -11,7 +12,16 @@ export type CursorRetryOptions = {
|
|
|
11
12
|
};
|
|
12
13
|
export type CreateCursorOptions = {
|
|
13
14
|
name: string;
|
|
15
|
+
/**
|
|
16
|
+
* Current Cursor access token, asked for every time a Run opens. Takes
|
|
17
|
+
* precedence over `accessToken` and `apiKey`; hosts use it to renew the
|
|
18
|
+
* login without exposing a secret in serializable options. `forceRefresh`
|
|
19
|
+
* is set once after Cursor rejected the previous token.
|
|
20
|
+
*/
|
|
21
|
+
getAccessToken?: AccessTokenProvider;
|
|
22
|
+
/** Access token sent as-is (no renewal). Used when `getAccessToken` is absent. */
|
|
14
23
|
accessToken?: string;
|
|
24
|
+
/** Raw `crsr_` API key, exchanged and renewed. Used only when neither of the above is given. */
|
|
15
25
|
apiKey?: string;
|
|
16
26
|
/** API base for auth, model discovery, and GetServerConfig. */
|
|
17
27
|
apiBaseURL?: string;
|
package/dist/language-model.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { cursorContextUsageMetadata, type CursorContextUsageSource, type CursorC
|
|
|
5
5
|
import { type ConversationBlobGraphStats } from "./protocol/blob-store.js";
|
|
6
6
|
import { type CursorSession } from "./session.js";
|
|
7
7
|
import { CursorProviderError } from "./errors.js";
|
|
8
|
+
import { type ModelInfo } from "./models.js";
|
|
8
9
|
import type { SeedHistoryMessage } from "./protocol/request.js";
|
|
9
10
|
type PromptIdentity = {
|
|
10
11
|
hostAgent?: string;
|
|
@@ -21,6 +22,13 @@ export declare const MAX_CHECKPOINT_BLOB_GRAPH_BYTES: number;
|
|
|
21
22
|
export declare function checkpointBlobGraphRequiresRebase(_stats: ConversationBlobGraphStats, _maxBytes?: number): boolean;
|
|
22
23
|
/** Warn-only concern for incomplete / oversized checkpoint graphs (no remint). */
|
|
23
24
|
export declare function checkpointBlobGraphConcern(stats: ConversationBlobGraphStats, maxBytes?: number): "incomplete-checkpoint-graph" | "oversized-checkpoint-graph" | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* True when the raw frame holds exactly one control message: a KV request, a
|
|
27
|
+
* checkpoint update, or an interaction update that is only a heartbeat. Extra
|
|
28
|
+
* fields inside a KV request are tolerated (Cursor has sent them live); extra
|
|
29
|
+
* top-level fields are not.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSoleControlFrame(payload: Uint8Array): boolean;
|
|
24
32
|
export type CursorRetryPolicy = {
|
|
25
33
|
maxAttempts: number;
|
|
26
34
|
baseDelayMs: number;
|
|
@@ -50,9 +58,17 @@ export declare function pumpWithRecovery(input: {
|
|
|
50
58
|
recover: (recovery: CursorRunRecovery) => Promise<CursorSession>;
|
|
51
59
|
onSession?: (session: CursorSession) => void;
|
|
52
60
|
maxRecoveries?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Set when the credential source can renew: called once per turn after
|
|
63
|
+
* Cursor rejects the token (401 / unauthenticated) before the attempt did
|
|
64
|
+
* anything, so the reopened Run uses a force-renewed token. This retry does
|
|
65
|
+
* not count against the transient-failure budget.
|
|
66
|
+
*/
|
|
67
|
+
renewRejectedCredential?: () => void;
|
|
53
68
|
}): Promise<CursorSession>;
|
|
54
69
|
export type CursorRunRecovery = {
|
|
55
70
|
kind: "rebase";
|
|
71
|
+
reason?: "checkpoint-unusable";
|
|
56
72
|
} | {
|
|
57
73
|
kind: "resume";
|
|
58
74
|
conversationId: string;
|
|
@@ -145,18 +161,12 @@ type ExtractedToolResult = {
|
|
|
145
161
|
output: string;
|
|
146
162
|
error?: string;
|
|
147
163
|
};
|
|
148
|
-
/**
|
|
149
|
-
* Tool results that form a live continuation: only the trailing run of `tool`
|
|
150
|
-
* messages after the last non-tool message. Mid-prompt historical tool results
|
|
151
|
-
* are ignored — they are conversation history, not replies for a held-open Run.
|
|
152
|
-
*/
|
|
153
164
|
export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOptions["prompt"]): ExtractedToolResult[];
|
|
154
165
|
/** Detect a host-owned canonical plan review, excluding Cursor exec replies. */
|
|
155
166
|
export declare function hasApprovedUncorrelatedPlanStageResult(prompt: LanguageModelV3CallOptions["prompt"]): boolean;
|
|
156
167
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* the host's file-tool dialect.
|
|
168
|
+
* Keep the workspace root on a checkpointed turn's live user message too, and
|
|
169
|
+
* require absolute `path` arguments when that is the host's file-tool dialect.
|
|
160
170
|
*/
|
|
161
171
|
export declare function groundCheckpointTurnText(userText: string, checkpoint: boolean, workspaceRoot: string, tools: readonly {
|
|
162
172
|
name?: string;
|
|
@@ -196,6 +206,21 @@ export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions[
|
|
|
196
206
|
preserveTrailingUser?: boolean;
|
|
197
207
|
toolResults?: "omit" | "all" | "trailing";
|
|
198
208
|
}): SeedHistoryMessage[];
|
|
209
|
+
/** Share of the target context a foreign-history rebase may fill before compaction. */
|
|
210
|
+
export declare const FOREIGN_HISTORY_REBASE_CONTEXT_SHARE = 0.8;
|
|
211
|
+
/**
|
|
212
|
+
* A foreign-history rebase replays the full host history. When that cannot fit,
|
|
213
|
+
* fail before opening a Run with an error hosts classify as context overflow
|
|
214
|
+
* (HTTP 413 + "prompt is too long"), so the host compacts and retries.
|
|
215
|
+
*/
|
|
216
|
+
export declare function assertForeignHistoryRebaseFits(input: {
|
|
217
|
+
modelInfo: ModelInfo | undefined;
|
|
218
|
+
cursorModelId: string;
|
|
219
|
+
maxMode: boolean;
|
|
220
|
+
history: SeedHistoryMessage[];
|
|
221
|
+
systemPrompt: string | undefined;
|
|
222
|
+
userText: string;
|
|
223
|
+
}): void;
|
|
199
224
|
/** OpenCode session id header, if present. */
|
|
200
225
|
export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptions): string | undefined;
|
|
201
226
|
/**
|
|
@@ -205,6 +230,14 @@ export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptio
|
|
|
205
230
|
*/
|
|
206
231
|
export declare function resolveConversationId(callOptions: LanguageModelV3CallOptions): string;
|
|
207
232
|
export { sessionIdToUuid } from "./protocol/conversation-bind.js";
|
|
233
|
+
/**
|
|
234
|
+
* Grow the held Run's advertised + permitted catalog from this `doStream`
|
|
235
|
+
* call. Continuation skips `startSession`, so without this, exec #36 and
|
|
236
|
+
* permission keep the freeze from Run open. MCP server ids stay those of the
|
|
237
|
+
* Run's one merged-config load (`startSession`); a tool that connects later
|
|
238
|
+
* still gets its identity from that set.
|
|
239
|
+
*/
|
|
240
|
+
export declare function refreshHeldSessionToolCatalog(session: CursorSession, callOptions: LanguageModelV3CallOptions): Promise<void>;
|
|
208
241
|
/** Exported for tests — AI SDK V3 span ends that must precede finish / tool-call. */
|
|
209
242
|
export declare function spanEndParts(opts: {
|
|
210
243
|
textStarted: boolean;
|