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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +36 -15
  3. package/SECURITY.md +43 -7
  4. package/dist/agent-url.js +2 -2
  5. package/dist/auth-renewal.d.ts +85 -0
  6. package/dist/auth-renewal.js +284 -0
  7. package/dist/auth.d.ts +38 -22
  8. package/dist/auth.js +79 -96
  9. package/dist/classic-tools.d.ts +15 -0
  10. package/dist/classic-tools.js +45 -0
  11. package/dist/context/build.d.ts +62 -7
  12. package/dist/context/build.js +127 -95
  13. package/dist/context/dynamic-catalog.d.ts +2 -58
  14. package/dist/context/dynamic-catalog.js +16 -161
  15. package/dist/context/epoch.d.ts +6 -4
  16. package/dist/context/epoch.js +22 -10
  17. package/dist/context/frozen.js +42 -15
  18. package/dist/context/index.d.ts +3 -1
  19. package/dist/context/index.js +3 -1
  20. package/dist/context/overlay.d.ts +2 -23
  21. package/dist/context/overlay.js +6 -30
  22. package/dist/context/paths.d.ts +51 -21
  23. package/dist/context/paths.js +78 -27
  24. package/dist/context/rules.d.ts +3 -15
  25. package/dist/context/rules.js +10 -186
  26. package/dist/context/skills-bridge.d.ts +22 -0
  27. package/dist/context/skills-bridge.js +20 -0
  28. package/dist/context/skills.d.ts +31 -8
  29. package/dist/context/skills.js +187 -86
  30. package/dist/debug.d.ts +2 -0
  31. package/dist/debug.js +4 -0
  32. package/dist/errors.d.ts +13 -0
  33. package/dist/errors.js +27 -3
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +119 -15
  36. package/dist/host-plan-file.d.ts +28 -0
  37. package/dist/host-plan-file.js +76 -0
  38. package/dist/image-input.d.ts +18 -0
  39. package/dist/image-input.js +119 -28
  40. package/dist/image-save.d.ts +10 -2
  41. package/dist/image-save.js +35 -23
  42. package/dist/index.d.ts +10 -0
  43. package/dist/language-model.d.ts +86 -25
  44. package/dist/language-model.js +1295 -369
  45. package/dist/model-config.d.ts +9 -0
  46. package/dist/model-config.js +28 -1
  47. package/dist/opencode2/catalog.d.ts +1 -0
  48. package/dist/opencode2/catalog.js +2 -0
  49. package/dist/opencode2/image-save-tool.d.ts +8 -0
  50. package/dist/opencode2/image-save-tool.js +84 -0
  51. package/dist/opencode2/integration.d.ts +30 -7
  52. package/dist/opencode2/integration.js +75 -46
  53. package/dist/opencode2/types.d.ts +15 -0
  54. package/dist/parallel-step.d.ts +65 -0
  55. package/dist/parallel-step.js +102 -0
  56. package/dist/plugin-opencode2.js +58 -56
  57. package/dist/plugin.d.ts +0 -10
  58. package/dist/plugin.js +207 -127
  59. package/dist/pricing-data.d.ts +14 -1
  60. package/dist/pricing-data.js +14 -1
  61. package/dist/protocol/ask-question.d.ts +12 -1
  62. package/dist/protocol/ask-question.js +77 -3
  63. package/dist/protocol/blob-store.js +3 -2
  64. package/dist/protocol/conversation-bind.js +1 -1
  65. package/dist/protocol/conversation-persistence.d.ts +2 -0
  66. package/dist/protocol/conversation-persistence.js +11 -1
  67. package/dist/protocol/conversation-state.d.ts +2 -0
  68. package/dist/protocol/conversation-state.js +19 -0
  69. package/dist/protocol/create-plan.d.ts +47 -25
  70. package/dist/protocol/create-plan.js +35 -24
  71. package/dist/protocol/interactions.d.ts +26 -17
  72. package/dist/protocol/interactions.js +80 -31
  73. package/dist/protocol/messages.js +96 -18
  74. package/dist/protocol/request.d.ts +16 -9
  75. package/dist/protocol/request.js +17 -25
  76. package/dist/protocol/switch-mode.d.ts +66 -27
  77. package/dist/protocol/switch-mode.js +147 -73
  78. package/dist/protocol/token-details.d.ts +2 -0
  79. package/dist/protocol/token-details.js +11 -1
  80. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  81. package/dist/protocol/tool-call-bridge.js +14 -0
  82. package/dist/protocol/tools.d.ts +33 -11
  83. package/dist/protocol/tools.js +165 -103
  84. package/dist/protocol/turn-provenance.d.ts +52 -0
  85. package/dist/protocol/turn-provenance.js +174 -0
  86. package/dist/replay-safety.d.ts +6 -0
  87. package/dist/replay-safety.js +56 -6
  88. package/dist/session.d.ts +44 -2
  89. package/dist/session.js +45 -1
  90. package/dist/shared.d.ts +7 -0
  91. package/dist/shared.js +7 -0
  92. package/dist/todo-store.d.ts +2 -0
  93. package/dist/todo-store.js +19 -0
  94. package/dist/transport/connect.d.ts +2 -0
  95. package/dist/transport/connect.js +23 -6
  96. package/dist/usage.js +23 -6
  97. package/package.json +3 -2
  98. package/dist/context/agents.d.ts +0 -12
  99. package/dist/context/agents.js +0 -136
  100. package/dist/plan-execution-kickoff.d.ts +0 -57
  101. package/dist/plan-execution-kickoff.js +0 -186
@@ -1,110 +1,211 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
1
+ import { statSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { homedir } from "node:os";
4
- import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
5
- async function exists(p) {
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
- await stat(p);
8
- return true;
26
+ return statSync(file).isFile();
9
27
  }
10
28
  catch {
11
29
  return false;
12
30
  }
13
31
  }
14
- function parseFrontmatter(raw) {
15
- if (!raw.startsWith("---"))
16
- return { body: raw };
17
- const end = raw.indexOf("\n---", 3);
18
- if (end < 0)
19
- return { body: raw };
20
- const fm = raw.slice(3, end).trim();
21
- const body = raw.slice(end + 4).replace(/^\n/, "");
22
- let name;
23
- let description;
24
- for (const line of fm.split("\n")) {
25
- const m = line.match(/^(\w+):\s*(.*)$/);
26
- if (!m)
27
- continue;
28
- const key = m[1];
29
- let val = m[2].trim();
30
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
31
- val = val.slice(1, -1);
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
- if (key === "name")
34
- name = val;
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 { name, description, body };
59
+ return path.isAbsolute(value) && isFile(value);
39
60
  }
40
- async function loadSkillFile(file) {
41
- try {
42
- const raw = await readFile(file, "utf-8");
43
- const { name, description, body } = parseFrontmatter(raw);
44
- const dirName = path.basename(path.dirname(file));
45
- return {
46
- fullPath: path.resolve(file),
47
- name: name || dirName,
48
- description: description || "",
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
- async function scanSkillsRoot(root, out) {
57
- if (!(await exists(root)))
58
- return;
59
- let entries;
60
- try {
61
- entries = await readdir(root);
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
- catch {
64
- return;
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
- entries.sort();
67
- for (const name of entries) {
68
- const skillMd = path.join(root, name, "SKILL.md");
69
- if (!(await exists(skillMd)))
95
+ }
96
+ export function hostSkillFiles(directory) {
97
+ return directory ? byDirectory.get(path.resolve(directory)) : undefined;
98
+ }
99
+ /** Test helper. */
100
+ export function resetHostSkillFilesForTests() {
101
+ byDirectory.clear();
102
+ }
103
+ /** Undo OpenCode 1's escapeHtml once; literal entity text in paths must survive. */
104
+ function decodeCatalogLocation(value) {
105
+ const entities = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'" };
106
+ return value.replace(/&(?:amp|lt|gt|quot|#39);/g, (entity) => entities[entity]);
107
+ }
108
+ function parseCatalogEntries(systemText) {
109
+ const catalog = CATALOG.exec(systemText)?.[1];
110
+ if (!catalog)
111
+ return [];
112
+ const out = [];
113
+ const seen = new Set();
114
+ for (const match of catalog.matchAll(ENTRY_OC2)) {
115
+ const id = match[1]?.trim();
116
+ const description = match[2] ?? "";
117
+ if (!id || seen.has(id))
70
118
  continue;
71
- const skill = await loadSkillFile(skillMd);
72
- if (!skill)
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
- if (!out.has(skill.name))
75
- out.set(skill.name, skill);
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
- async function walkAncestorsFor(dir, rel, stop, out) {
79
- let cur = path.resolve(dir);
80
- const root = path.resolve(stop);
81
- for (;;) {
82
- await scanSkillsRoot(path.join(cur, rel), out);
83
- if (cur === root)
84
- break;
85
- const parent = path.dirname(cur);
86
- if (parent === cur)
87
- break;
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
- * Discover skills the way OpenCode does: `.opencode/skills`, global opencode,
93
- * plus `.claude/skills` and `.agents/skills` (project + home).
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 async function collectSkills(workspaceRoot, worktree) {
96
- const out = new Map();
97
- const home = homedir();
98
- for (const projectDir of opencodeProjectConfigDirs(workspaceRoot)) {
99
- const relative = path.relative(workspaceRoot, projectDir);
100
- await walkAncestorsFor(workspaceRoot, path.join(relative, "skills"), worktree, out);
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
- for (const globalDir of opencodeGlobalConfigDirs()) {
103
- await scanSkillsRoot(path.join(globalDir, "skills"), out);
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
- await walkAncestorsFor(workspaceRoot, path.join(".claude", "skills"), worktree, out);
106
- await scanSkillsRoot(path.join(home, ".claude", "skills"), out);
107
- await walkAncestorsFor(workspaceRoot, path.join(".agents", "skills"), worktree, out);
108
- await scanSkillsRoot(path.join(home, ".agents", "skills"), out);
109
- return [...out.values()];
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
- return new CursorAuthError(`Cursor authentication failed (HTTP ${statusCode}); reauthenticate with Cursor`, { ...diagnostics, statusCode });
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
- * OpenCode 1.x continues to use advertised `plan_enter` / `plan_exit` tools.
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 declare function setHostAgentModeSwitch(fn: HostAgentModeSwitchFn | undefined): void;
18
- /** Queue only when this host installed a native primary-agent switch. */
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