cursor-opencode-provider 0.8.0 → 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 (67) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +28 -9
  3. package/SECURITY.md +43 -7
  4. package/dist/context/build.d.ts +22 -4
  5. package/dist/context/build.js +33 -5
  6. package/dist/context/dynamic-catalog.js +6 -1
  7. package/dist/context/frozen.js +6 -2
  8. package/dist/context/index.d.ts +2 -0
  9. package/dist/context/index.js +2 -0
  10. package/dist/context/paths.d.ts +44 -17
  11. package/dist/context/paths.js +65 -18
  12. package/dist/context/skills-bridge.d.ts +22 -0
  13. package/dist/context/skills-bridge.js +20 -0
  14. package/dist/context/skills.d.ts +34 -0
  15. package/dist/context/skills.js +211 -0
  16. package/dist/host-agent-mode.d.ts +53 -4
  17. package/dist/host-agent-mode.js +117 -13
  18. package/dist/host-plan-file.d.ts +28 -0
  19. package/dist/host-plan-file.js +76 -0
  20. package/dist/image-input.d.ts +18 -0
  21. package/dist/image-input.js +119 -28
  22. package/dist/image-save.d.ts +10 -2
  23. package/dist/image-save.js +33 -21
  24. package/dist/language-model.d.ts +45 -17
  25. package/dist/language-model.js +925 -292
  26. package/dist/model-config.d.ts +9 -0
  27. package/dist/model-config.js +28 -1
  28. package/dist/opencode2/catalog.d.ts +1 -0
  29. package/dist/opencode2/catalog.js +2 -0
  30. package/dist/opencode2/image-save-tool.d.ts +8 -0
  31. package/dist/opencode2/image-save-tool.js +84 -0
  32. package/dist/opencode2/types.d.ts +15 -0
  33. package/dist/parallel-step.d.ts +65 -0
  34. package/dist/parallel-step.js +102 -0
  35. package/dist/plugin-opencode2.js +48 -40
  36. package/dist/plugin.js +69 -18
  37. package/dist/pricing-data.d.ts +14 -1
  38. package/dist/pricing-data.js +14 -1
  39. package/dist/protocol/ask-question.d.ts +12 -1
  40. package/dist/protocol/ask-question.js +77 -3
  41. package/dist/protocol/create-plan.d.ts +47 -25
  42. package/dist/protocol/create-plan.js +33 -23
  43. package/dist/protocol/interactions.d.ts +26 -17
  44. package/dist/protocol/interactions.js +80 -31
  45. package/dist/protocol/messages.js +74 -14
  46. package/dist/protocol/request.d.ts +5 -0
  47. package/dist/protocol/request.js +2 -0
  48. package/dist/protocol/switch-mode.d.ts +66 -27
  49. package/dist/protocol/switch-mode.js +147 -73
  50. package/dist/protocol/token-details.d.ts +2 -0
  51. package/dist/protocol/token-details.js +11 -1
  52. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  53. package/dist/protocol/tool-call-bridge.js +14 -0
  54. package/dist/protocol/tools.d.ts +16 -1
  55. package/dist/protocol/tools.js +113 -11
  56. package/dist/replay-safety.d.ts +6 -0
  57. package/dist/replay-safety.js +56 -6
  58. package/dist/session.d.ts +35 -1
  59. package/dist/session.js +45 -1
  60. package/dist/todo-store.d.ts +2 -0
  61. package/dist/todo-store.js +19 -0
  62. package/dist/transport/connect.d.ts +2 -0
  63. package/dist/transport/connect.js +23 -6
  64. package/dist/usage.js +23 -6
  65. package/package.json +3 -2
  66. package/dist/plan-execution-kickoff.d.ts +0 -57
  67. package/dist/plan-execution-kickoff.js +0 -186
@@ -25,6 +25,12 @@ export declare const CURSOR_MODEL_COSTS: {
25
25
  readonly cache_read: 0.1;
26
26
  readonly cache_write: 1.25;
27
27
  };
28
+ readonly "claude-haiku-5-5": {
29
+ readonly input: 0.1;
30
+ readonly output: 0.5;
31
+ readonly cache_read: 0.01;
32
+ readonly cache_write: 0.125;
33
+ };
28
34
  readonly "claude-opus-4-5": {
29
35
  readonly input: 5;
30
36
  readonly output: 25;
@@ -94,7 +100,7 @@ export declare const CURSOR_MODEL_COSTS: {
94
100
  readonly "claude-sonnet-5-5": {
95
101
  readonly input: 2;
96
102
  readonly output: 10;
97
- readonly cache_read: 0.2;
103
+ readonly cache_read: 0.1;
98
104
  readonly cache_write: 2.5;
99
105
  };
100
106
  readonly "composer-2.5": {
@@ -311,6 +317,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
311
317
  readonly "claude-haiku-4-5": {
312
318
  readonly maxContext: 200000;
313
319
  };
320
+ readonly "claude-haiku-5-5": {
321
+ readonly maxContext: 300000;
322
+ readonly maxContextForMaxMode: 1000000;
323
+ };
314
324
  readonly "claude-opus-4-5": {
315
325
  readonly maxContext: 200000;
316
326
  readonly maxContextForMaxMode: 200000;
@@ -465,6 +475,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
465
475
  readonly "claude-haiku-4-5": {
466
476
  readonly supportsImages: true;
467
477
  };
478
+ readonly "claude-haiku-5-5": {
479
+ readonly supportsImages: true;
480
+ };
468
481
  readonly "claude-opus-4-5": {
469
482
  readonly supportsImages: true;
470
483
  };
@@ -25,6 +25,12 @@ export const CURSOR_MODEL_COSTS = {
25
25
  "cache_read": 0.1,
26
26
  "cache_write": 1.25
27
27
  },
28
+ "claude-haiku-5-5": {
29
+ "input": 0.1,
30
+ "output": 0.5,
31
+ "cache_read": 0.01,
32
+ "cache_write": 0.125
33
+ },
28
34
  "claude-opus-4-5": {
29
35
  "input": 5,
30
36
  "output": 25,
@@ -94,7 +100,7 @@ export const CURSOR_MODEL_COSTS = {
94
100
  "claude-sonnet-5-5": {
95
101
  "input": 2,
96
102
  "output": 10,
97
- "cache_read": 0.2,
103
+ "cache_read": 0.1,
98
104
  "cache_write": 2.5
99
105
  },
100
106
  "composer-2.5": {
@@ -311,6 +317,10 @@ export const CURSOR_MODEL_CONTEXTS = {
311
317
  "claude-haiku-4-5": {
312
318
  "maxContext": 200000
313
319
  },
320
+ "claude-haiku-5-5": {
321
+ "maxContext": 300000,
322
+ "maxContextForMaxMode": 1000000
323
+ },
314
324
  "claude-opus-4-5": {
315
325
  "maxContext": 200000,
316
326
  "maxContextForMaxMode": 200000
@@ -465,6 +475,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
465
475
  "claude-haiku-4-5": {
466
476
  "supportsImages": true
467
477
  },
478
+ "claude-haiku-5-5": {
479
+ "supportsImages": true
480
+ },
468
481
  "claude-opus-4-5": {
469
482
  "supportsImages": true
470
483
  },
@@ -76,6 +76,13 @@ export declare function decodeAskQuestionQuery(queryBytes: Uint8Array): DecodedA
76
76
  export declare function isCatchAllOptionLabel(label: string): boolean;
77
77
  /** Options as Cursor would display them: trailing catch-all removed. */
78
78
  export declare function displayOptions(question: CursorAskQuestionItem): CursorAskQuestionOption[];
79
+ /** OpenCode `Question.Prompt.header`: short label, max 30 chars. */
80
+ export declare function questionHeader(title: string): string;
81
+ /**
82
+ * Fill required OpenCode `question` fields when a model call omits them.
83
+ * Live OC1 failure: SchemaError Missing key at ["questions"][0]["header"].
84
+ */
85
+ export declare function normalizeOpencodeQuestionArgs(args: Record<string, unknown>): Record<string, unknown>;
79
86
  export type OpencodeQuestionInput = {
80
87
  questions: Array<{
81
88
  question: string;
@@ -100,12 +107,16 @@ export declare function asyncResult(): AskQuestionResultMessage;
100
107
  /**
101
108
  * Pull each question's answer text out of a host `question` tool output.
102
109
  *
103
- * OpenCode returns prose (`metadata.answers` does not cross the AI SDK
110
+ * OpenCode 1.x returns prose (`metadata.answers` does not cross the AI SDK
104
111
  * boundary):
105
112
  *
106
113
  * User has answered your questions: "<q1>"="<a, b>", "<q2>"="Unanswered". You
107
114
  * can now continue with the user's answers in mind.
108
115
  *
116
+ * OpenCode 2.0 sends the tool's declared output as JSON
117
+ * (`{ "answers": [["Yes"], …] }`) and does not put that prose on the AI SDK
118
+ * tool-result. Answers are positional.
119
+ *
109
120
  * The prose is located by its `"<question>"="` anchor — robust
110
121
  * against commas, quotes and `"="` inside question or answer text, and against
111
122
  * duplicate questions. Returns undefined for a question whose anchor is
@@ -183,7 +183,8 @@ export function displayOptions(question) {
183
183
  return question.options.slice(0, -1);
184
184
  return question.options.slice();
185
185
  }
186
- function header(title) {
186
+ /** OpenCode `Question.Prompt.header`: short label, max 30 chars. */
187
+ export function questionHeader(title) {
187
188
  const trimmed = title.trim();
188
189
  if (!trimmed)
189
190
  return "Question";
@@ -191,6 +192,40 @@ function header(title) {
191
192
  ? trimmed.slice(0, HEADER_MAX_LENGTH - 1).trimEnd() + "…"
192
193
  : trimmed;
193
194
  }
195
+ /**
196
+ * Fill required OpenCode `question` fields when a model call omits them.
197
+ * Live OC1 failure: SchemaError Missing key at ["questions"][0]["header"].
198
+ */
199
+ export function normalizeOpencodeQuestionArgs(args) {
200
+ if (!Array.isArray(args.questions))
201
+ return args;
202
+ return {
203
+ ...args,
204
+ questions: args.questions.map((item) => {
205
+ if (!item || typeof item !== "object" || Array.isArray(item))
206
+ return item;
207
+ const q = item;
208
+ const prompt = typeof q.question === "string" ? q.question : "";
209
+ const existing = typeof q.header === "string" ? q.header : "";
210
+ const options = Array.isArray(q.options)
211
+ ? q.options.map((opt) => {
212
+ if (!opt || typeof opt !== "object" || Array.isArray(opt))
213
+ return opt;
214
+ const o = opt;
215
+ return {
216
+ ...o,
217
+ ...(o.description === undefined ? { description: "" } : {}),
218
+ };
219
+ })
220
+ : q.options;
221
+ return {
222
+ ...q,
223
+ header: questionHeader(existing.trim() || prompt),
224
+ options,
225
+ };
226
+ }),
227
+ };
228
+ }
194
229
  /**
195
230
  * Build the OpenCode `question` tool input. `Question.Prompt` requires
196
231
  * `question`, `header`, and `options[{label, description}]`; Cursor options
@@ -200,7 +235,7 @@ export function askQuestionToolInput(args) {
200
235
  return {
201
236
  questions: args.questions.map((question) => ({
202
237
  question: question.prompt,
203
- header: header(args.title),
238
+ header: questionHeader(args.title),
204
239
  options: displayOptions(question).map((option) => ({
205
240
  label: option.label,
206
241
  description: "",
@@ -220,18 +255,25 @@ export function asyncResult() {
220
255
  /**
221
256
  * Pull each question's answer text out of a host `question` tool output.
222
257
  *
223
- * OpenCode returns prose (`metadata.answers` does not cross the AI SDK
258
+ * OpenCode 1.x returns prose (`metadata.answers` does not cross the AI SDK
224
259
  * boundary):
225
260
  *
226
261
  * User has answered your questions: "<q1>"="<a, b>", "<q2>"="Unanswered". You
227
262
  * can now continue with the user's answers in mind.
228
263
  *
264
+ * OpenCode 2.0 sends the tool's declared output as JSON
265
+ * (`{ "answers": [["Yes"], …] }`) and does not put that prose on the AI SDK
266
+ * tool-result. Answers are positional.
267
+ *
229
268
  * The prose is located by its `"<question>"="` anchor — robust
230
269
  * against commas, quotes and `"="` inside question or answer text, and against
231
270
  * duplicate questions. Returns undefined for a question whose anchor is
232
271
  * absent, which is treated as unanswered rather than guessed at.
233
272
  */
234
273
  export function parseAnswerSegments(questions, output) {
274
+ const fromJson = parseJsonAnswerSegments(output, questions.length);
275
+ if (fromJson)
276
+ return fromJson;
235
277
  const answers = [];
236
278
  let cursor = 0;
237
279
  for (const question of questions) {
@@ -260,6 +302,38 @@ export function parseAnswerSegments(questions, output) {
260
302
  }
261
303
  return answers;
262
304
  }
305
+ function cellText(cell) {
306
+ if (typeof cell === "string")
307
+ return cell;
308
+ if (typeof cell === "number" || typeof cell === "boolean")
309
+ return String(cell);
310
+ if (Array.isArray(cell)) {
311
+ const parts = cell.map((item) => (typeof item === "string" ? item : "")).filter(Boolean);
312
+ return parts.length > 0 ? parts.join(", ") : undefined;
313
+ }
314
+ return undefined;
315
+ }
316
+ /** OpenCode 2 `question` output `{ answers: string[][] }`, or a JSON array of cells. */
317
+ function parseJsonAnswerSegments(output, count) {
318
+ const trimmed = output.trim();
319
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
320
+ return undefined;
321
+ let parsed;
322
+ try {
323
+ parsed = JSON.parse(trimmed);
324
+ }
325
+ catch {
326
+ return undefined;
327
+ }
328
+ const cells = Array.isArray(parsed)
329
+ ? parsed
330
+ : parsed && typeof parsed === "object" && Array.isArray(parsed.answers)
331
+ ? parsed.answers
332
+ : undefined;
333
+ if (!cells)
334
+ return undefined;
335
+ return Array.from({ length: count }, (_, index) => cellText(cells[index]));
336
+ }
263
337
  /** OpenCode's placeholder for a question the user left blank. */
264
338
  const UNANSWERED = "Unanswered";
265
339
  /**
@@ -7,10 +7,10 @@
7
7
  * does **not** — plans must stay host-portable so switching models does not
8
8
  * strand users on Cursor-specific paths or frontmatter.
9
9
  *
10
- * Location mirrors OpenCode Session.plan shape, but the project-config segment
11
- * comes from {@link hostPlansDir} / {@link opencodeProjectConfigDirs}
12
- * (the active host's project config dir via the path bridge), never a
13
- * hardcoded OpenCode-only directory name.
10
+ * Location is the host's own: the session plan file when the host defines one
11
+ * (`hostPlanFilePath`, OpenCode 1.x `Session.plan`), otherwise a new
12
+ * `<created>-<slug>.md` under {@link hostPlansDir} (OpenCode 2.0's Plan
13
+ * directory, OpenCode 1.x `<data>/plans`, or an installed path bridge).
14
14
  *
15
15
  * Body is plain markdown (the same shape a plan-mode model would write with
16
16
  * `write`). Cursor YAML frontmatter is never emitted.
@@ -58,46 +58,66 @@ export type CursorPlanStageInput = {
58
58
  * Its tool result is the approval outcome (success = approved,
59
59
  * error = not approved). The result may name the host's own
60
60
  * follow-up approval call. This provider does not invent that prompt.
61
- * - `approve` no such tool, but the host advertises `question`: the provider
62
- * writes the plan itself and then asks the same thing with upstream
63
- * `PlanExitTool`'s own prompt.
64
- * - `ack` nothing to ask with (or a lifecycle turn): write, or for a
65
- * lifecycle turn skip the write, and acknowledge the CLI way.
61
+ * - `ack` nothing to ask with (or a lifecycle turn / CreatePlan outside
62
+ * plan mode): write, or for a lifecycle turn skip the write, and
63
+ * acknowledge the CLI way.
64
+ * - `approve` plan mode is active and the host advertises `question` but not
65
+ * a plan review tool: write the plan, then ask with upstream
66
+ * `PlanExitTool`'s own prompt (OpenCode 1.x / 2.0 without
67
+ * `plan_exit`). Explicit "Yes" is execution approval.
68
+ * - `exit` the host's `plan` agent is active, advertises `plan_exit`, and
69
+ * its own plan file is known: write the plan there and run the
70
+ * host `plan_exit` review. The host owns approval and execution;
71
+ * the turn that carries the result leaves `plan` only on approval.
72
+ * - `defer` the host's `plan` agent owns planning but CreatePlan cannot be
73
+ * carried out yet: an approved switch into it is still pending (it
74
+ * applies after this Run), or its plan file is unknown. Nothing is
75
+ * written; the error tells the model which host workflow follows.
66
76
  *
67
- * The plan is always persisted before any prompt: writing needs no approval,
68
- * executing does.
77
+ * The plan is always persisted before any review: writing needs no approval,
78
+ * executing does, and only the host asks for it.
69
79
  */
70
80
  export type CreatePlanBridge = {
71
81
  kind: "stage";
82
+ } | {
83
+ kind: "ack";
72
84
  } | {
73
85
  kind: "approve";
74
86
  } | {
75
- kind: "ack";
87
+ kind: "defer";
88
+ reason: string;
89
+ } | {
90
+ kind: "exit";
91
+ planPath: string;
76
92
  };
93
+ /** CreatePlan raised before an approved switch into the host plan agent took effect. */
94
+ export declare const CREATE_PLAN_HOST_PLAN_PENDING_REASON: string;
95
+ /** CreatePlan raised while the host's own plan agent owns the plan file and its review. */
96
+ export declare const CREATE_PLAN_HOST_PLAN_WORKFLOW_REASON: string;
97
+ /** Cursor-visible reason when the user wants the plan revised instead of run. */
98
+ export declare const CREATE_PLAN_NOT_APPROVED_REASON: string;
77
99
  /** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */
78
100
  export declare function createPlanApprovalQuestion(planLabel: string): string;
79
101
  export declare const CREATE_PLAN_APPROVAL_HEADER = "Build Agent";
80
- /** Cursor-visible reason when the user wants the plan revised instead of run. */
81
- export declare const CREATE_PLAN_NOT_APPROVED_REASON: string;
82
- /**
83
- * Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's
84
- * own record of an approved Cursor plan/spec mode: the approval prompt guards
85
- * the transition *out of* planning, so a CreatePlan raised outside plan mode is
86
- * written and acknowledged without one.
87
- */
102
+ /** Resolve how this CreatePlan is satisfied (see {@link CreatePlanBridge}). */
88
103
  export declare function resolveCreatePlanBridge(options: {
89
104
  allowTools: boolean;
90
105
  canStage?: boolean;
91
- planModeActive?: boolean;
92
106
  advertised: ReadonlySet<string> | Iterable<string>;
107
+ /** An approved switch into the host `plan` agent waits for this Run to end. */
108
+ hostPlanEntryPending?: boolean;
109
+ /** Host primary agent of this Run, when the host reported it. */
110
+ hostAgent?: string;
111
+ /** Provider-recorded Cursor plan/spec mode (the gate out of planning). */
112
+ planModeActive?: boolean;
113
+ /** The host's own plan file for this session, when the host defines one. */
114
+ hostPlanFile?: string;
93
115
  }): CreatePlanBridge;
94
116
  /** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
95
117
  export declare function createPlanApprovalQuestionInput(planLabel: string): OpencodeQuestionInput;
96
118
  /**
97
119
  * True when the emulated approval prompt came back as an explicit "Yes".
98
- * An unanswered, dismissed, or failed prompt keeps the model planning rather
99
- * than silently starting execution. OpenCode prose is anchored on the exact
100
- * prompt text. Every other result shape fails closed.
120
+ * An unanswered, dismissed, or failed prompt keeps the model planning.
101
121
  */
102
122
  export declare function createPlanApproved(output: string, isError: boolean, question: string): boolean;
103
123
  /** Decode a `create_plan_request_query` body, or undefined when unusable. */
@@ -125,7 +145,9 @@ export declare function renderOpencodePlanMarkdown(args: CursorCreatePlanArgs):
125
145
  * Write the plan under {@link hostPlansDir} and return a `file://` URI.
126
146
  * Empty / missing content is the caller's responsibility (empty plan_uri ack).
127
147
  */
128
- export declare function writeOpencodePlanFile(args: CursorCreatePlanArgs, workspaceRoot: string, created?: number): CreatePlanWriteResult;
148
+ export declare function writeOpencodePlanFile(args: CursorCreatePlanArgs, workspaceRoot: string, created?: number,
149
+ /** Exact host-owned plan file; default is a new file under {@link hostPlansDir}. */
150
+ target?: string): CreatePlanWriteResult;
129
151
  /**
130
152
  * The plan as the user reads it before approving execution.
131
153
  *
@@ -7,10 +7,10 @@
7
7
  * does **not** — plans must stay host-portable so switching models does not
8
8
  * strand users on Cursor-specific paths or frontmatter.
9
9
  *
10
- * Location mirrors OpenCode Session.plan shape, but the project-config segment
11
- * comes from {@link hostPlansDir} / {@link opencodeProjectConfigDirs}
12
- * (the active host's project config dir via the path bridge), never a
13
- * hardcoded OpenCode-only directory name.
10
+ * Location is the host's own: the session plan file when the host defines one
11
+ * (`hostPlanFilePath`, OpenCode 1.x `Session.plan`), otherwise a new
12
+ * `<created>-<slug>.md` under {@link hostPlansDir} (OpenCode 2.0's Plan
13
+ * directory, OpenCode 1.x `<data>/plans`, or an installed path bridge).
14
14
  *
15
15
  * Body is plain markdown (the same shape a plan-mode model would write with
16
16
  * `write`). Cursor YAML frontmatter is never emitted.
@@ -19,9 +19,9 @@ import { mkdirSync, writeFileSync } from "node:fs";
19
19
  import path from "node:path";
20
20
  import { pathToFileURL } from "node:url";
21
21
  import { hostPlansDir } from "../context/paths.js";
22
- import { parseAnswerSegments, } from "./ask-question.js";
23
22
  import { decodeMessageSparse } from "./messages.js";
24
23
  import { errorMessage } from "../debug.js";
24
+ import { parseAnswerSegments, } from "./ask-question.js";
25
25
  const PLAN_ADJECTIVES = [
26
26
  "brave",
27
27
  "calm",
@@ -90,6 +90,17 @@ const PLAN_NOUNS = [
90
90
  export const CURSOR_PLAN_STAGE_TOOL = "cursor_plan_stage";
91
91
  /** Held InteractionQuery continuation field for a native host plan stage. */
92
92
  export const CREATE_PLAN_RESULT_FIELD = "create_plan_request_response";
93
+ /** CreatePlan raised before an approved switch into the host plan agent took effect. */
94
+ export const CREATE_PLAN_HOST_PLAN_PENDING_REASON = "Plan mode starts in the host's plan agent after this turn. Do not record the plan " +
95
+ "now and make no further tool calls: end this turn. Planning continues in the next " +
96
+ "turn under the host's plan-mode instructions.";
97
+ /** CreatePlan raised while the host's own plan agent owns the plan file and its review. */
98
+ export const CREATE_PLAN_HOST_PLAN_WORKFLOW_REASON = "This host's plan agent owns the plan file and its approval. Write the plan where the " +
99
+ "plan-mode instructions say, then call `plan_exit` to ask the user to approve it. Do " +
100
+ "not implement until it is approved.";
101
+ /** Cursor-visible reason when the user wants the plan revised instead of run. */
102
+ export const CREATE_PLAN_NOT_APPROVED_REASON = "The user did not approve executing this plan. Keep planning: refine the plan and "
103
+ + "propose it again when it is ready.";
93
104
  /** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */
94
105
  export function createPlanApprovalQuestion(planLabel) {
95
106
  const where = planLabel.trim();
@@ -100,28 +111,28 @@ export function createPlanApprovalQuestion(planLabel) {
100
111
  export const CREATE_PLAN_APPROVAL_HEADER = "Build Agent";
101
112
  const CREATE_PLAN_APPROVAL_YES = "Yes";
102
113
  const CREATE_PLAN_APPROVAL_NO = "No";
103
- /** Cursor-visible reason when the user wants the plan revised instead of run. */
104
- export const CREATE_PLAN_NOT_APPROVED_REASON = "The user did not approve executing this plan. Keep planning: refine the plan and "
105
- + "propose it again when it is ready.";
106
- /**
107
- * Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's
108
- * own record of an approved Cursor plan/spec mode: the approval prompt guards
109
- * the transition *out of* planning, so a CreatePlan raised outside plan mode is
110
- * written and acknowledged without one.
111
- */
114
+ /** Resolve how this CreatePlan is satisfied (see {@link CreatePlanBridge}). */
112
115
  export function resolveCreatePlanBridge(options) {
113
116
  // A lifecycle turn (title generation, compaction) runs alongside the real one
114
117
  // and must not write a second plan file or raise a second prompt.
115
118
  if (!options.allowTools)
116
119
  return { kind: "ack" };
120
+ if (options.hostPlanEntryPending) {
121
+ return { kind: "defer", reason: CREATE_PLAN_HOST_PLAN_PENDING_REASON };
122
+ }
117
123
  if (options.canStage)
118
124
  return { kind: "stage" };
119
125
  const names = options.advertised instanceof Set ? options.advertised : new Set(options.advertised);
120
- if (options.planModeActive && names.has("question"))
126
+ if (options.hostAgent === "plan" && names.has("plan_exit")) {
127
+ return options.hostPlanFile
128
+ ? { kind: "exit", planPath: options.hostPlanFile }
129
+ : { kind: "defer", reason: CREATE_PLAN_HOST_PLAN_WORKFLOW_REASON };
130
+ }
131
+ const planModeActive = options.planModeActive === true || options.hostAgent === "plan";
132
+ if (planModeActive && names.has("question"))
121
133
  return { kind: "approve" };
122
134
  return { kind: "ack" };
123
135
  }
124
- /** The synthetic AskQuestion item backing the emulated approval prompt. */
125
136
  function createPlanApprovalItem(question) {
126
137
  return {
127
138
  id: "create_plan_approval",
@@ -156,16 +167,13 @@ export function createPlanApprovalQuestionInput(planLabel) {
156
167
  }
157
168
  /**
158
169
  * True when the emulated approval prompt came back as an explicit "Yes".
159
- * An unanswered, dismissed, or failed prompt keeps the model planning rather
160
- * than silently starting execution. OpenCode prose is anchored on the exact
161
- * prompt text. Every other result shape fails closed.
170
+ * An unanswered, dismissed, or failed prompt keeps the model planning.
162
171
  */
163
172
  export function createPlanApproved(output, isError, question) {
164
173
  if (isError)
165
174
  return false;
166
175
  const [segment] = parseAnswerSegments([createPlanApprovalItem(question)], output);
167
- const answer = (segment ?? "").trim().toLowerCase();
168
- return answer === CREATE_PLAN_APPROVAL_YES.toLowerCase();
176
+ return (segment ?? "").trim().toLowerCase() === CREATE_PLAN_APPROVAL_YES.toLowerCase();
169
177
  }
170
178
  function asRecord(value) {
171
179
  return value && typeof value === "object" && !Array.isArray(value)
@@ -334,12 +342,14 @@ export function renderOpencodePlanMarkdown(args) {
334
342
  * Write the plan under {@link hostPlansDir} and return a `file://` URI.
335
343
  * Empty / missing content is the caller's responsibility (empty plan_uri ack).
336
344
  */
337
- export function writeOpencodePlanFile(args, workspaceRoot, created = Date.now()) {
345
+ export function writeOpencodePlanFile(args, workspaceRoot, created = Date.now(),
346
+ /** Exact host-owned plan file; default is a new file under {@link hostPlansDir}. */
347
+ target) {
338
348
  const markdown = renderOpencodePlanMarkdown(args);
339
349
  if (!markdown.trim()) {
340
350
  return { ok: false, error: "CreatePlan produced no plan content to write" };
341
351
  }
342
- const planPath = resolveHostPlanPath(workspaceRoot, args.name, created);
352
+ const planPath = target ?? resolveHostPlanPath(workspaceRoot, args.name, created);
343
353
  try {
344
354
  mkdirSync(path.dirname(planPath), { recursive: true });
345
355
  writeFileSync(planPath, markdown, "utf-8");
@@ -14,6 +14,8 @@ export type HandledInteraction = {
14
14
  variantField: number;
15
15
  variantName: string;
16
16
  outcome: "rejected" | "acknowledged" | "failed" | "bridged" | "approved";
17
+ /** Decoded model call id, including refusals and lifecycle acknowledgments. */
18
+ toolCallId?: string;
17
19
  /**
18
20
  * Immediate reply for this query. Absent for bridged *synchronous* AskQuestion
19
21
  * or SwitchMode: Cursor blocks until the host tool returns, exactly as its own
@@ -31,24 +33,29 @@ export type HandledInteraction = {
31
33
  */
32
34
  switchMode?: DecodedSwitchModeQuery & {
33
35
  bridge: SwitchModeBridge;
34
- toolName?: SwitchModeHostTool | "question";
36
+ toolName?: SwitchModeHostTool;
35
37
  };
36
38
  /**
37
- * Present when CreatePlan needs a host tool: either a native plan stage that
38
- * owns the write *and* the approval, or the emulated `question` prompt the
39
- * provider raises after writing the plan itself. `planUri` is set only for
40
- * the emulated path, where the provider already knows the file it wrote.
39
+ * Present when CreatePlan needs a host tool: a host plan-stage tool that
40
+ * owns the write *and* the review, the host's `plan_exit` review of the
41
+ * plan file the provider just wrote (`planUri` / `planPath` set), or the
42
+ * emulated `question` prompt after that write.
41
43
  */
42
44
  createPlan?: DecodedCreatePlanQuery & {
43
45
  bridge: CreatePlanBridge;
44
- toolName: typeof CURSOR_PLAN_STAGE_TOOL | "question";
46
+ toolName: typeof CURSOR_PLAN_STAGE_TOOL | "plan_exit" | "question";
45
47
  planUri?: string;
46
- /** Filesystem path of the plan the provider just wrote (emulated path). */
48
+ /** Filesystem path of the plan the provider just wrote. */
47
49
  planPath?: string;
48
50
  questionInput?: ReturnType<typeof createPlanApprovalQuestionInput>;
49
- /** The plan to show the user before they approve it (emulated path only). */
51
+ /** The plan to show the user before the host review. */
50
52
  planReview?: string;
51
53
  };
54
+ /**
55
+ * Cursor tool call id of a CreatePlan the host plan agent will record
56
+ * instead. Nothing was written, so its display must not be mirrored as a plan.
57
+ */
58
+ deferredCreatePlanToolCallId?: string;
52
59
  /** Present when an image generation was approved: the target to expect. */
53
60
  generateImage?: DecodedGenerateImageQuery;
54
61
  };
@@ -81,16 +88,18 @@ export type HandleInteractionQueryOptions = {
81
88
  workspaceRoot?: string;
82
89
  /** True when the advertised host plan-stage tool is available. */
83
90
  canBridgeCreatePlan?: boolean;
84
- /**
85
- * True when the provider has recorded an approved Cursor plan/spec mode for
86
- * this session. A written plan then ends with an execution-approval prompt,
87
- * which is the transition out of planning; outside plan mode there is none.
88
- */
91
+ /** An approved switch into the host `plan` agent waits for this Run to end. */
92
+ hostPlanEntryPending?: boolean;
93
+ /** Host primary agent this Run executes under, when the host reported it. */
94
+ hostAgent?: string;
95
+ /** Provider-recorded Cursor plan/spec mode for the CreatePlan execution gate. */
89
96
  planModeActive?: boolean;
97
+ /** The host's own plan file for this session, when the host defines one. */
98
+ hostPlanFile?: string;
90
99
  /**
91
100
  * Cursor unified mode currently recorded for this session. A SwitchMode to
92
101
  * the mode already in effect needs no approval, matching Cursor's own IDE
93
- * handler and keeping the plan-approval prompt from firing twice.
102
+ * handler and keeping the host's plan review from running twice.
94
103
  */
95
104
  activeCursorModeId?: string;
96
105
  };
@@ -112,9 +121,9 @@ export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Arr
112
121
  * Bridged / persisted exceptions:
113
122
  * - AskQuestion (#3) → OpenCode `question` tool
114
123
  * - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised
115
- * - CreatePlan (#7) → host-calculated plan file via hostPlansDir (project-config
116
- * `plans/` in a git worktree, else host global data/plans); empty args still
117
- * get the CLI empty-`plan_uri` success ack
124
+ * - CreatePlan (#7) → the host's plan file (session `Session.plan` file when
125
+ * known, else a new file under hostPlansDir); empty args still get the CLI
126
+ * empty-`plan_uri` success ack
118
127
  * - GenerateImage (#12) → approve when `cursor_image_save` is advertised
119
128
  */
120
129
  export declare function handleInteractionQuery(query: Record<string, unknown>, agentServerPayload: Uint8Array, options?: HandleInteractionQueryOptions): HandledInteraction;