cursor-opencode-provider 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +230 -44
  3. package/SECURITY.md +60 -12
  4. package/dist/activity.d.ts +73 -1
  5. package/dist/activity.js +231 -5
  6. package/dist/agent-url.js +37 -9
  7. package/dist/auth-renewal.d.ts +1 -1
  8. package/dist/auth-renewal.js +58 -26
  9. package/dist/billing.d.ts +105 -0
  10. package/dist/billing.js +212 -0
  11. package/dist/classic-tool-results.d.ts +1 -0
  12. package/dist/classic-tool-results.js +42 -0
  13. package/dist/context/build.d.ts +23 -4
  14. package/dist/context/build.js +65 -10
  15. package/dist/context/dynamic-catalog.js +6 -1
  16. package/dist/context/env.js +27 -3
  17. package/dist/context/epoch.js +9 -5
  18. package/dist/context/frozen.js +6 -2
  19. package/dist/context/git.d.ts +5 -2
  20. package/dist/context/git.js +17 -7
  21. package/dist/context/index.d.ts +2 -0
  22. package/dist/context/index.js +2 -0
  23. package/dist/context/paths.d.ts +45 -18
  24. package/dist/context/paths.js +72 -22
  25. package/dist/context/skills-bridge.d.ts +22 -0
  26. package/dist/context/skills-bridge.js +20 -0
  27. package/dist/context/skills.d.ts +34 -0
  28. package/dist/context/skills.js +211 -0
  29. package/dist/debug.js +13 -2
  30. package/dist/early-tool-rows.d.ts +60 -0
  31. package/dist/early-tool-rows.js +96 -0
  32. package/dist/errors.d.ts +49 -7
  33. package/dist/errors.js +163 -8
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +117 -13
  36. package/dist/host-interrupt.d.ts +12 -0
  37. package/dist/host-interrupt.js +18 -0
  38. package/dist/host-plan-file.d.ts +28 -0
  39. package/dist/host-plan-file.js +76 -0
  40. package/dist/host-steer.d.ts +71 -0
  41. package/dist/host-steer.js +232 -0
  42. package/dist/image-input.d.ts +18 -0
  43. package/dist/image-input.js +122 -30
  44. package/dist/image-save.d.ts +10 -2
  45. package/dist/image-save.js +33 -21
  46. package/dist/index.d.ts +2 -2
  47. package/dist/language-model.d.ts +149 -30
  48. package/dist/language-model.js +3127 -673
  49. package/dist/lost-request.d.ts +39 -0
  50. package/dist/lost-request.js +111 -0
  51. package/dist/model-config.d.ts +12 -1
  52. package/dist/model-config.js +78 -30
  53. package/dist/models.d.ts +26 -3
  54. package/dist/models.js +101 -45
  55. package/dist/opencode2/catalog.d.ts +1 -0
  56. package/dist/opencode2/catalog.js +2 -0
  57. package/dist/opencode2/image-save-tool.d.ts +8 -0
  58. package/dist/opencode2/image-save-tool.js +84 -0
  59. package/dist/opencode2/tool-results.d.ts +18 -0
  60. package/dist/opencode2/tool-results.js +66 -0
  61. package/dist/opencode2/types.d.ts +15 -0
  62. package/dist/parallel-step.d.ts +65 -0
  63. package/dist/parallel-step.js +102 -0
  64. package/dist/plugin-opencode2.js +233 -65
  65. package/dist/plugin.js +97 -23
  66. package/dist/pricing-data.d.ts +40 -3
  67. package/dist/pricing-data.js +40 -3
  68. package/dist/pricing.d.ts +1 -1
  69. package/dist/pricing.js +7 -4
  70. package/dist/process-shared.d.ts +14 -0
  71. package/dist/process-shared.js +18 -0
  72. package/dist/protocol/apply-patch.d.ts +25 -13
  73. package/dist/protocol/apply-patch.js +79 -30
  74. package/dist/protocol/ask-question.d.ts +12 -1
  75. package/dist/protocol/ask-question.js +77 -3
  76. package/dist/protocol/blob-store.d.ts +5 -1
  77. package/dist/protocol/blob-store.js +10 -6
  78. package/dist/protocol/client-version.js +6 -4
  79. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  80. package/dist/protocol/conversation-cache-header.js +149 -0
  81. package/dist/protocol/conversation-persistence.d.ts +27 -1
  82. package/dist/protocol/conversation-persistence.js +628 -126
  83. package/dist/protocol/conversation-state.d.ts +8 -1
  84. package/dist/protocol/conversation-state.js +28 -7
  85. package/dist/protocol/create-plan.d.ts +47 -25
  86. package/dist/protocol/create-plan.js +33 -23
  87. package/dist/protocol/host-notes.d.ts +31 -0
  88. package/dist/protocol/host-notes.js +85 -0
  89. package/dist/protocol/interactions.d.ts +26 -17
  90. package/dist/protocol/interactions.js +80 -31
  91. package/dist/protocol/messages.js +164 -21
  92. package/dist/protocol/request.d.ts +40 -19
  93. package/dist/protocol/request.js +55 -28
  94. package/dist/protocol/sent-history-images.d.ts +49 -0
  95. package/dist/protocol/sent-history-images.js +102 -0
  96. package/dist/protocol/switch-mode.d.ts +66 -27
  97. package/dist/protocol/switch-mode.js +147 -73
  98. package/dist/protocol/token-details.d.ts +2 -0
  99. package/dist/protocol/token-details.js +11 -1
  100. package/dist/protocol/tool-call-bridge.d.ts +6 -0
  101. package/dist/protocol/tool-call-bridge.js +24 -0
  102. package/dist/protocol/tools.d.ts +90 -9
  103. package/dist/protocol/tools.js +543 -151
  104. package/dist/protocol/turn-provenance.d.ts +19 -4
  105. package/dist/protocol/turn-provenance.js +77 -20
  106. package/dist/replay-safety.d.ts +22 -4
  107. package/dist/replay-safety.js +86 -27
  108. package/dist/retry-budget.d.ts +21 -0
  109. package/dist/retry-budget.js +94 -0
  110. package/dist/runtime-files.d.ts +23 -0
  111. package/dist/runtime-files.js +581 -0
  112. package/dist/session.d.ts +129 -10
  113. package/dist/session.js +198 -35
  114. package/dist/shared.d.ts +1 -1
  115. package/dist/shared.js +1 -1
  116. package/dist/shell-timeout.d.ts +54 -10
  117. package/dist/shell-timeout.js +362 -92
  118. package/dist/stream-write.d.ts +20 -0
  119. package/dist/stream-write.js +87 -0
  120. package/dist/terminal-runner.d.ts +15 -0
  121. package/dist/terminal-runner.js +528 -0
  122. package/dist/todo-store.d.ts +2 -0
  123. package/dist/todo-store.js +19 -0
  124. package/dist/transport/connect.d.ts +10 -1
  125. package/dist/transport/connect.js +124 -27
  126. package/dist/usage.d.ts +37 -17
  127. package/dist/usage.js +85 -34
  128. package/package.json +5 -3
  129. package/dist/plan-execution-kickoff.d.ts +0 -57
  130. package/dist/plan-execution-kickoff.js +0 -186
  131. package/dist/protocol/thinking.d.ts +0 -15
  132. package/dist/protocol/thinking.js +0 -17
@@ -6,10 +6,15 @@ export declare function hydrateConversationState(cacheDir: string, sessionKey: s
6
6
  toolCatalog: OpencodeToolDef[];
7
7
  hostAgent?: string;
8
8
  systemPromptHash?: string;
9
+ hostNote?: string;
9
10
  } | undefined>;
10
11
  /** Restore only turn provenance when its in-memory entry was evicted. */
11
12
  export declare function hydrateTurnProvenance(cacheDir: string, sessionKey: string): Promise<void>;
12
- /** Persist the complete resumable state only after Cursor confirms TurnEnded. */
13
+ /**
14
+ * Persist the resumable state at Cursor's TurnEnded, or before it
15
+ * (`runInProgress`) so a Run the host interrupts, or a restart, resumes from
16
+ * the latest checkpoint and still recognizes the step in progress.
17
+ */
13
18
  export declare function persistConversationState(cacheDir: string, input: {
14
19
  sessionKey: string;
15
20
  conversationId: string;
@@ -18,5 +23,7 @@ export declare function persistConversationState(cacheDir: string, input: {
18
23
  postCompactionRebase?: boolean;
19
24
  hostAgent?: string;
20
25
  systemPromptHash?: string;
26
+ hostNote?: string;
27
+ runInProgress?: boolean;
21
28
  }): Promise<void>;
22
29
  export declare function clearPersistedConversationState(cacheDir: string, sessionKey: string, expectedConversationId?: string): Promise<void>;
@@ -1,10 +1,13 @@
1
1
  import { trace } from "../debug.js";
2
+ import { restoreEarlySteers, serializeEarlySteers } from "../host-steer.js";
3
+ import { restoreLostRequest, serializeLostRequest } from "../lost-request.js";
2
4
  import { getFrozenRequestContext, setFrozenRequestContext, } from "../context/frozen.js";
3
5
  import { hasConversationBinding, isActiveConversationBinding, restoreConversationBinding, } from "./conversation-bind.js";
4
6
  import { compactConversationBlobs, restoreConversationBlobs, } from "./blob-store.js";
5
7
  import { getCheckpoint, setCheckpoint } from "./checkpoint.js";
6
8
  import { getTurnProvenance, parseTurnProvenance, restoreTurnProvenance, serializeTurnProvenance, } from "./turn-provenance.js";
7
- import { deletePersistedConversation, loadPersistedConversation, persistConversation, } from "./conversation-persistence.js";
9
+ import { heldHistoryImagesSnapshot, restoreHeldHistoryImages } from "./sent-history-images.js";
10
+ import { deletePersistedConversation, getPersistedTurnProvenance, loadPersistedConversation, persistConversation, } from "./conversation-persistence.js";
8
11
  /** Restore one OpenCode session before its conversation binding is resolved. */
9
12
  export async function hydrateConversationState(cacheDir, sessionKey) {
10
13
  if (hasConversationBinding(sessionKey))
@@ -17,6 +20,8 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
17
20
  return undefined;
18
21
  }
19
22
  restoreConversationBinding(sessionKey, persisted.conversationId);
23
+ restoreEarlySteers(sessionKey, persisted.earlySteers);
24
+ restoreLostRequest(sessionKey, persisted.lostRequest);
20
25
  if (persisted.checkpoint)
21
26
  setCheckpoint(persisted.conversationId, persisted.checkpoint);
22
27
  restoreConversationBlobs(persisted.conversationId, persisted.blobs);
@@ -24,29 +29,38 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
24
29
  const provenance = persisted.turnProvenance ? parseTurnProvenance(persisted.turnProvenance) : undefined;
25
30
  if (provenance?.conversationId === persisted.conversationId)
26
31
  restoreTurnProvenance(sessionKey, provenance);
32
+ restoreHeldHistoryImages(sessionKey, persisted.conversationId, {
33
+ checkpoint: persisted.sentImagesCheckpoint,
34
+ hashes: persisted.sentImageHashes,
35
+ });
27
36
  trace(`conversation persistence: restored sessionKey=${sessionKey} ` +
28
37
  `conversationId=${persisted.conversationId} checkpoint=${persisted.checkpoint?.length ?? 0}B ` +
29
- `blobs=${persisted.blobs.length}`);
38
+ `blobs=${persisted.blobs.length} heldImages=${persisted.sentImageHashes?.length ?? 0}`);
30
39
  return {
31
40
  conversationId: persisted.conversationId,
32
41
  postCompactionRebase: persisted.postCompactionRebase,
33
42
  toolCatalog: structuredClone(persisted.toolCatalog),
34
43
  ...(persisted.hostAgent ? { hostAgent: persisted.hostAgent } : {}),
35
44
  ...(persisted.systemPromptHash ? { systemPromptHash: persisted.systemPromptHash } : {}),
45
+ ...(persisted.hostNote ? { hostNote: persisted.hostNote } : {}),
36
46
  };
37
47
  }
38
48
  /** Restore only turn provenance when its in-memory entry was evicted. */
39
49
  export async function hydrateTurnProvenance(cacheDir, sessionKey) {
40
50
  if (getTurnProvenance(sessionKey))
41
51
  return;
42
- const persisted = (await loadPersistedConversation(cacheDir, sessionKey)).value;
43
- if (!persisted?.turnProvenance)
52
+ const persisted = await getPersistedTurnProvenance(cacheDir, sessionKey);
53
+ if (!persisted)
44
54
  return;
45
55
  const provenance = parseTurnProvenance(persisted.turnProvenance);
46
56
  if (provenance?.conversationId === persisted.conversationId)
47
57
  restoreTurnProvenance(sessionKey, provenance);
48
58
  }
49
- /** Persist the complete resumable state only after Cursor confirms TurnEnded. */
59
+ /**
60
+ * Persist the resumable state at Cursor's TurnEnded, or before it
61
+ * (`runInProgress`) so a Run the host interrupts, or a restart, resumes from
62
+ * the latest checkpoint and still recognizes the step in progress.
63
+ */
50
64
  export async function persistConversationState(cacheDir, input) {
51
65
  // A newer Run may have reset/superseded this conversation while its final
52
66
  // frame was still in flight. Never let that late TurnEnded resurrect it.
@@ -56,10 +70,13 @@ export async function persistConversationState(cacheDir, input) {
56
70
  return;
57
71
  }
58
72
  const checkpoint = getCheckpoint(input.conversationId);
59
- const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint);
73
+ const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint, {
74
+ keepInMemory: input.runInProgress,
75
+ });
60
76
  const blobs = blobCompaction.blobs;
61
77
  const requestContext = getFrozenRequestContext(input.conversationId) ?? input.requestContext;
62
78
  const provenance = getTurnProvenance(input.sessionKey);
79
+ const heldImages = heldHistoryImagesSnapshot(input.sessionKey, input.conversationId, checkpoint);
63
80
  await persistConversation(cacheDir, {
64
81
  sessionKey: input.sessionKey,
65
82
  conversationId: input.conversationId,
@@ -70,11 +87,15 @@ export async function persistConversationState(cacheDir, input) {
70
87
  postCompactionRebase: input.postCompactionRebase,
71
88
  hostAgent: input.hostAgent,
72
89
  systemPromptHash: input.systemPromptHash,
90
+ hostNote: input.hostNote,
91
+ earlySteers: serializeEarlySteers(input.sessionKey),
92
+ lostRequest: serializeLostRequest(input.sessionKey),
93
+ ...(heldImages ? { sentImageHashes: heldImages.hashes, sentImagesCheckpoint: heldImages.checkpoint } : {}),
73
94
  ...(provenance?.conversationId === input.conversationId
74
95
  ? { turnProvenance: serializeTurnProvenance(provenance) }
75
96
  : {}),
76
97
  });
77
- trace(`conversation persistence: saved sessionKey=${input.sessionKey} ` +
98
+ trace(`conversation persistence: saved${input.runInProgress ? " in-progress" : ""} sessionKey=${input.sessionKey} ` +
78
99
  `conversationId=${input.conversationId} checkpoint=${checkpoint?.length ?? 0}B ` +
79
100
  `blobs=${blobCompaction.beforeCount}->${blobCompaction.afterCount} ` +
80
101
  `blobBytes=${blobCompaction.beforeBytes}->${blobCompaction.afterBytes}` +
@@ -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");
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Host notes (OpenCode `<system-update>` and read-tool instructions) in the
3
+ * shapes Cursor's own clients give model-visible context:
4
+ * - mid-turn, plain text in a `user_context` injection, as Cursor SDK
5
+ * `Run.steer` sends it (no wrapper: wrapped text was refused live);
6
+ * - on a later user turn, a `<system_reminder>` block in the message text;
7
+ * - provider notices about one exec result, as postToolUse hook context.
8
+ */
9
+ /** Cursor CLI rejects a hook additional_context longer than this. */
10
+ export declare const HOOK_ADDITIONAL_CONTEXT_MAX_CHARS = 10000;
11
+ /** Stable delivery order survives asynchronous acknowledgements and Run recovery. */
12
+ export type HostNoteBatch = {
13
+ notes: string[];
14
+ order: number;
15
+ };
16
+ export type HookAdditionalContext = {
17
+ hook_event_name: string;
18
+ content: string;
19
+ };
20
+ /** Plain note text for one mid-turn injection, in host order. */
21
+ export declare function hostNoteText(notes: readonly string[]): string;
22
+ /**
23
+ * One postToolUse hook context per note, trimmed like Cursor CLI's carrier.
24
+ * A note over the CLI limit is split rather than dropped, so no text is lost.
25
+ */
26
+ export declare function hostNoteHookContexts(notes: readonly string[]): HookAdditionalContext[];
27
+ /** One `<system_reminder>` block per note, for a user turn's text. */
28
+ export declare function wrapHostNoteForCursor(text: string): string;
29
+ export declare function wrapHostNotesForCursor(notes: readonly string[]): string[];
30
+ export declare function encodePersistedHostNotes(notes: readonly string[]): string | undefined;
31
+ export declare function decodePersistedHostNotes(blob: string | undefined): string[];
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Host notes (OpenCode `<system-update>` and read-tool instructions) in the
3
+ * shapes Cursor's own clients give model-visible context:
4
+ * - mid-turn, plain text in a `user_context` injection, as Cursor SDK
5
+ * `Run.steer` sends it (no wrapper: wrapped text was refused live);
6
+ * - on a later user turn, a `<system_reminder>` block in the message text;
7
+ * - provider notices about one exec result, as postToolUse hook context.
8
+ */
9
+ const SYSTEM_UPDATE_BLOCK = /<system-update>([\s\S]*?)<\/system-update>/g;
10
+ const SYSTEM_REMINDER_OPEN = "<system_reminder>";
11
+ const SYSTEM_REMINDER_CLOSE = "</system_reminder>";
12
+ /** Cursor CLI's hook event name for context attached to a tool result. */
13
+ const POST_TOOL_USE_HOOK_EVENT = "postToolUse";
14
+ /** Cursor CLI rejects a hook additional_context longer than this. */
15
+ export const HOOK_ADDITIONAL_CONTEXT_MAX_CHARS = 10_000;
16
+ /** Delimiter for ordered note bodies in the restart-snapshot `hostNote` string. */
17
+ const HOST_NOTE_PERSIST_SEP = "\n\u001e\n";
18
+ /** Plain note text for one mid-turn injection, in host order. */
19
+ export function hostNoteText(notes) {
20
+ return notes.map(hostNoteBody).filter(Boolean).join("\n\n");
21
+ }
22
+ /**
23
+ * One postToolUse hook context per note, trimmed like Cursor CLI's carrier.
24
+ * A note over the CLI limit is split rather than dropped, so no text is lost.
25
+ */
26
+ export function hostNoteHookContexts(notes) {
27
+ return notes.flatMap((note) => {
28
+ const body = hostNoteBody(note);
29
+ const chunks = [];
30
+ for (let start = 0; start < body.length;) {
31
+ let end = Math.min(body.length, start + HOOK_ADDITIONAL_CONTEXT_MAX_CHARS);
32
+ // Never split a surrogate pair.
33
+ if (end < body.length && /[\uD800-\uDBFF]/.test(body[end - 1]))
34
+ end--;
35
+ chunks.push(body.slice(start, end));
36
+ start = end;
37
+ }
38
+ return chunks.map((content) => ({ hook_event_name: POST_TOOL_USE_HOOK_EVENT, content }));
39
+ });
40
+ }
41
+ /** One `<system_reminder>` block per note, for a user turn's text. */
42
+ export function wrapHostNoteForCursor(text) {
43
+ return `${SYSTEM_REMINDER_OPEN}\n${escapeReminderTags(hostNoteBody(text))}\n${SYSTEM_REMINDER_CLOSE}`;
44
+ }
45
+ export function wrapHostNotesForCursor(notes) {
46
+ return notes.filter((note) => hostNoteBody(note).length > 0).map(wrapHostNoteForCursor);
47
+ }
48
+ export function encodePersistedHostNotes(notes) {
49
+ const present = notes.filter((note) => note.length > 0);
50
+ if (present.length === 0)
51
+ return undefined;
52
+ return present.join(HOST_NOTE_PERSIST_SEP);
53
+ }
54
+ export function decodePersistedHostNotes(blob) {
55
+ if (!blob)
56
+ return [];
57
+ return blob.split(HOST_NOTE_PERSIST_SEP).filter((note) => note.length > 0);
58
+ }
59
+ /**
60
+ * Unwrap OpenCode `<system-update>` blocks. One host message may hold several
61
+ * (joined by `hostTailNote`); text that is not only such blocks stays as is.
62
+ */
63
+ function hostNoteBody(text) {
64
+ const trimmed = text.trim();
65
+ const bodies = [];
66
+ let rest = trimmed;
67
+ for (const match of trimmed.matchAll(SYSTEM_UPDATE_BLOCK)) {
68
+ bodies.push(unescapeSystemUpdateText(match[1].trim()));
69
+ rest = rest.replace(match[0], "");
70
+ }
71
+ return bodies.length > 0 && rest.trim() === ""
72
+ ? bodies.filter(Boolean).join("\n\n")
73
+ : trimmed;
74
+ }
75
+ /**
76
+ * Inverse of OpenCode's `escapeSystemUpdateText` (`packages/llm/src/protocols/
77
+ * shared.ts`): it escapes `&`, `<`, `>` inside the wrapper. `&amp;` goes last.
78
+ */
79
+ function unescapeSystemUpdateText(text) {
80
+ return text.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
81
+ }
82
+ /** Same neutralization Cursor CLI applies to text placed inside its reminders. */
83
+ function escapeReminderTags(text) {
84
+ return text.replace(/system_reminder/gi, "system_reminder_");
85
+ }
@@ -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;