cursor-opencode-provider 0.7.6 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -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 +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- 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.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- 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.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/dist/context/skills.js
CHANGED
|
@@ -1,110 +1,211 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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) {
|
|
6
25
|
try {
|
|
7
|
-
|
|
8
|
-
return true;
|
|
26
|
+
return statSync(file).isFile();
|
|
9
27
|
}
|
|
10
28
|
catch {
|
|
11
29
|
return false;
|
|
12
30
|
}
|
|
13
31
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
|
32
55
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (key === "description")
|
|
36
|
-
description = val;
|
|
56
|
+
// Host URIs such as skill://name — keep for bridge hosts; no disk check.
|
|
57
|
+
return true;
|
|
37
58
|
}
|
|
38
|
-
return
|
|
59
|
+
return path.isAbsolute(value) && isFile(value);
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
content: body.slice(0, 20_000),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return undefined;
|
|
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
|
+
}
|
|
54
70
|
}
|
|
71
|
+
return value;
|
|
55
72
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
62
86
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
65
94
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'" };
|
|
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))
|
|
70
118
|
continue;
|
|
71
|
-
|
|
72
|
-
|
|
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))
|
|
73
129
|
continue;
|
|
74
|
-
|
|
75
|
-
|
|
130
|
+
seen.add(name);
|
|
131
|
+
out.push({
|
|
132
|
+
key: name,
|
|
133
|
+
description: description.trim(),
|
|
134
|
+
location: location || undefined,
|
|
135
|
+
});
|
|
76
136
|
}
|
|
137
|
+
return out;
|
|
77
138
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
cur = parent;
|
|
89
|
-
}
|
|
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;
|
|
90
149
|
}
|
|
91
150
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
151
|
+
* path-desc `agent_skills` for cataloged skills that have a usable location,
|
|
152
|
+
* in catalog order. Descriptions come from the catalog (epoch-stable).
|
|
94
153
|
*/
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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");
|
|
101
195
|
}
|
|
102
|
-
|
|
103
|
-
|
|
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;
|
|
104
205
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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;
|
|
110
211
|
}
|
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,
|
|
@@ -4,19 +4,68 @@
|
|
|
4
4
|
* The language model stays host-neutral: it records a canonical Cursor mode
|
|
5
5
|
* (`plan`, `spec`, `agent`, ...). A host entrypoint may install this structural
|
|
6
6
|
* callback when its public API can select the corresponding primary agent.
|
|
7
|
-
* OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents
|
|
8
|
-
*
|
|
7
|
+
* OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents
|
|
8
|
+
* and continues the turn via `session.synthetic` (OpenCode 2 `switchAgent`
|
|
9
|
+
* only publishes the agent and would otherwise idle).
|
|
10
|
+
* On OpenCode 1.x hosts, advertised `plan_enter` / `plan_exit` tools stay
|
|
11
|
+
* authoritative; without `plan_enter`, the classic entrypoint selects the
|
|
12
|
+
* `plan` agent the 1.x way, with a synthetic user message for that agent.
|
|
9
13
|
*/
|
|
10
14
|
export type HostAgentModeSwitchInput = {
|
|
11
15
|
sessionID: string;
|
|
12
16
|
targetModeID: string;
|
|
13
17
|
/** Concrete Cursor Run that owns the switch. */
|
|
14
18
|
cursorSessionID?: string;
|
|
19
|
+
/** Host primary agent the owning Run executed under, when the host reported it. */
|
|
20
|
+
hostAgent?: string;
|
|
15
21
|
};
|
|
16
22
|
export type HostAgentModeSwitchFn = (input: HostAgentModeSwitchInput) => void | Promise<void>;
|
|
17
|
-
export
|
|
18
|
-
/**
|
|
23
|
+
export type HostAgentModeSwitchOptions = {
|
|
24
|
+
/** The switch also starts the host turn that continues the conversation. */
|
|
25
|
+
resumesTurn?: boolean;
|
|
26
|
+
/** Queue-time filter: a request the switch would not apply stays provider-owned. */
|
|
27
|
+
accepts?: (input: HostAgentModeSwitchInput) => boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Synthetic user text that carries an OpenCode 1.x primary-agent switch. */
|
|
30
|
+
export declare function hostAgentSwitchPromptText(agent: "plan" | "build"): string;
|
|
31
|
+
/**
|
|
32
|
+
* OpenCode 1.x shape of the switch: a 1.x session runs each turn under the
|
|
33
|
+
* agent of its latest user message, which is how the host's own plan_enter /
|
|
34
|
+
* plan_exit change agents.
|
|
35
|
+
*
|
|
36
|
+
* It accepts only a real transition of a session running one of the host's
|
|
37
|
+
* own primary agents: entering an existing `plan` agent from another primary
|
|
38
|
+
* agent, or leaving the observed `plan` agent. Subagent and hidden internal
|
|
39
|
+
* sessions (title, summary, compaction, task children) never get a user turn
|
|
40
|
+
* injected.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createPromptHostAgentModeSwitch(prompt: (input: {
|
|
43
|
+
sessionID: string;
|
|
44
|
+
agent: "plan" | "build";
|
|
45
|
+
text: string;
|
|
46
|
+
}) => Promise<unknown>, primaryAgents: () => ReadonlySet<string> | undefined): {
|
|
47
|
+
apply: HostAgentModeSwitchFn;
|
|
48
|
+
accepts: (input: HostAgentModeSwitchInput) => boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Install this host's native switch; it is used until a newer one is
|
|
52
|
+
* installed. The returned disposer removes only this switch, so disposing an
|
|
53
|
+
* older setup never removes a newer setup's switch. `undefined` removes all.
|
|
54
|
+
*/
|
|
55
|
+
export declare function setHostAgentModeSwitch(fn: HostAgentModeSwitchFn | undefined, options?: HostAgentModeSwitchOptions): () => void;
|
|
56
|
+
/**
|
|
57
|
+
* How this host applies a Cursor mode switch to its primary agents: `resumes`
|
|
58
|
+
* when the switch also starts the next host turn, `next-turn` when the user's
|
|
59
|
+
* next message runs under the new agent, undefined without a native switch.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hostAgentModeSwitchKind(): "resumes" | "next-turn" | undefined;
|
|
62
|
+
/** Queue only when this host installed a native primary-agent switch that accepts the request. */
|
|
19
63
|
export declare function queueHostAgentModeSwitch(input: HostAgentModeSwitchInput): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* True while an approved switch into the host `plan` agent waits for its Run to
|
|
66
|
+
* end and will itself start the plan-agent turn that continues the work.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isHostPlanEntryPending(sessionID: string | undefined): boolean;
|
|
20
69
|
export declare function cancelHostAgentModeSwitch(sessionID: string | undefined): void;
|
|
21
70
|
/**
|
|
22
71
|
* Apply the switch only after its Cursor Run is terminal and owns no pending
|