cursor-opencode-provider 0.8.1 → 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 (96) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +204 -37
  3. package/SECURITY.md +20 -8
  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 +1 -0
  14. package/dist/context/build.js +32 -5
  15. package/dist/context/env.js +27 -3
  16. package/dist/context/epoch.js +9 -5
  17. package/dist/context/git.d.ts +5 -2
  18. package/dist/context/git.js +17 -7
  19. package/dist/context/paths.d.ts +1 -1
  20. package/dist/context/paths.js +7 -4
  21. package/dist/debug.js +13 -2
  22. package/dist/early-tool-rows.d.ts +60 -0
  23. package/dist/early-tool-rows.js +96 -0
  24. package/dist/errors.d.ts +49 -7
  25. package/dist/errors.js +163 -8
  26. package/dist/host-interrupt.d.ts +12 -0
  27. package/dist/host-interrupt.js +18 -0
  28. package/dist/host-steer.d.ts +71 -0
  29. package/dist/host-steer.js +232 -0
  30. package/dist/image-input.js +4 -3
  31. package/dist/index.d.ts +2 -2
  32. package/dist/language-model.d.ts +114 -23
  33. package/dist/language-model.js +2290 -469
  34. package/dist/lost-request.d.ts +39 -0
  35. package/dist/lost-request.js +111 -0
  36. package/dist/model-config.d.ts +3 -1
  37. package/dist/model-config.js +50 -29
  38. package/dist/models.d.ts +26 -3
  39. package/dist/models.js +101 -45
  40. package/dist/opencode2/tool-results.d.ts +18 -0
  41. package/dist/opencode2/tool-results.js +66 -0
  42. package/dist/plugin-opencode2.js +185 -25
  43. package/dist/plugin.js +28 -5
  44. package/dist/pricing-data.d.ts +26 -2
  45. package/dist/pricing-data.js +26 -2
  46. package/dist/pricing.d.ts +1 -1
  47. package/dist/pricing.js +7 -4
  48. package/dist/process-shared.d.ts +14 -0
  49. package/dist/process-shared.js +18 -0
  50. package/dist/protocol/apply-patch.d.ts +25 -13
  51. package/dist/protocol/apply-patch.js +79 -30
  52. package/dist/protocol/blob-store.d.ts +5 -1
  53. package/dist/protocol/blob-store.js +10 -6
  54. package/dist/protocol/client-version.js +6 -4
  55. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  56. package/dist/protocol/conversation-cache-header.js +149 -0
  57. package/dist/protocol/conversation-persistence.d.ts +27 -1
  58. package/dist/protocol/conversation-persistence.js +628 -126
  59. package/dist/protocol/conversation-state.d.ts +8 -1
  60. package/dist/protocol/conversation-state.js +28 -7
  61. package/dist/protocol/host-notes.d.ts +31 -0
  62. package/dist/protocol/host-notes.js +85 -0
  63. package/dist/protocol/messages.js +91 -8
  64. package/dist/protocol/request.d.ts +35 -19
  65. package/dist/protocol/request.js +53 -28
  66. package/dist/protocol/sent-history-images.d.ts +49 -0
  67. package/dist/protocol/sent-history-images.js +102 -0
  68. package/dist/protocol/tool-call-bridge.d.ts +2 -0
  69. package/dist/protocol/tool-call-bridge.js +10 -0
  70. package/dist/protocol/tools.d.ts +74 -8
  71. package/dist/protocol/tools.js +431 -141
  72. package/dist/protocol/turn-provenance.d.ts +19 -4
  73. package/dist/protocol/turn-provenance.js +77 -20
  74. package/dist/replay-safety.d.ts +16 -4
  75. package/dist/replay-safety.js +34 -25
  76. package/dist/retry-budget.d.ts +21 -0
  77. package/dist/retry-budget.js +94 -0
  78. package/dist/runtime-files.d.ts +23 -0
  79. package/dist/runtime-files.js +581 -0
  80. package/dist/session.d.ts +94 -9
  81. package/dist/session.js +153 -34
  82. package/dist/shared.d.ts +1 -1
  83. package/dist/shared.js +1 -1
  84. package/dist/shell-timeout.d.ts +54 -10
  85. package/dist/shell-timeout.js +362 -92
  86. package/dist/stream-write.d.ts +20 -0
  87. package/dist/stream-write.js +87 -0
  88. package/dist/terminal-runner.d.ts +15 -0
  89. package/dist/terminal-runner.js +528 -0
  90. package/dist/transport/connect.d.ts +8 -1
  91. package/dist/transport/connect.js +103 -23
  92. package/dist/usage.d.ts +37 -17
  93. package/dist/usage.js +63 -29
  94. package/package.json +3 -2
  95. package/dist/protocol/thinking.d.ts +0 -15
  96. package/dist/protocol/thinking.js +0 -17
@@ -1,35 +1,42 @@
1
1
  import { encodeMessage, getMessageTypes } from "./messages.js";
2
2
  /**
3
- * Seed ConversationStateStructure for the first turn (no checkpoint yet).
3
+ * Empty ConversationStateStructure for a Run without a checkpoint (turn 1,
4
+ * compaction, rebase, reseed). After the first checkpoint arrives we stop
5
+ * inventing state and echo the server's opaque structure instead (CLI behavior).
4
6
  *
5
- * After the first checkpoint arrives we stop inventing state and echo the
6
- * server's opaque structure instead (CLI behavior). Compaction resets and
7
- * rebases also use this seed, with `history` carrying OpenCode's prompt turns
8
- * so Cursor can continue without the old checkpoint.
9
- *
10
- * No `system` entry is seeded: Cursor does not follow a client-authored
11
- * `system` root message, and neither Cursor client writes
12
- * `root_prompt_messages_json`. Host system context travels as the
13
- * system-instructions rule in RequestContext (`systemInstructionsRule`), so
14
- * `system` history entries are dropped here rather than duplicated.
7
+ * Nothing goes in `root_prompt_messages_json` (#1). Neither Cursor client
8
+ * writes it: Cursor checkpoints fill it with references to the conversation's
9
+ * own rendered root prompt. A client-written entry stands in for that prompt,
10
+ * so Cursor stops rendering RequestContext rules, skills and subagents — the
11
+ * host system-instructions rule included. Verified live 2026-10-09
12
+ * (claude-sonnet-5-5): two seeded history messages there gave checkpoint
13
+ * categories `rules:0, subagents:0` and a reply that ignored the rule; the same
14
+ * history in the user text kept both. History therefore travels in the user
15
+ * text ({@link seedHistoryUserText}).
15
16
  *
16
17
  * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
17
18
  * field is the internal `--system-prompt` CLI override and the server rejects
18
19
  * it for normal accounts.
19
20
  */
20
- export function buildSeedConversationState(input) {
21
- const root = getMessageTypes();
22
- const type = root.lookupType("ConversationStateStructure");
23
- const messages = [];
24
- for (const entry of input?.history ?? []) {
25
- if (!entry.content || entry.role === "system")
26
- continue;
27
- messages.push(JSON.stringify({ role: entry.role, content: entry.content }));
28
- }
29
- const obj = {};
30
- if (messages.length > 0)
31
- obj.root_prompt_messages_json = messages;
32
- return type.encode(type.fromObject(obj)).finish();
21
+ export function buildSeedConversationState() {
22
+ const type = getMessageTypes().lookupType("ConversationStateStructure");
23
+ return type.encode(type.fromObject({})).finish();
24
+ }
25
+ const SEED_HISTORY_TAGS = /<\/(conversation_history|user|assistant)>/gi;
26
+ /**
27
+ * Prior turns of a Run without a checkpoint, as a transcript ahead of the live
28
+ * user text. `system` entries are dropped: host system context travels as the
29
+ * system-instructions rule in RequestContext. Closing tags inside a message are
30
+ * escaped so a message cannot end its own block early.
31
+ */
32
+ export function seedHistoryUserText(text, history) {
33
+ const entries = (history ?? []).filter((entry) => entry.content && entry.role !== "system");
34
+ if (entries.length === 0)
35
+ return text;
36
+ const transcript = entries
37
+ .map((entry) => `<${entry.role}>\n${entry.content.replace(SEED_HISTORY_TAGS, "<\\/$1>")}\n</${entry.role}>`)
38
+ .join("\n\n");
39
+ return `<conversation_history>\n${transcript}\n</conversation_history>\n\n${text}`;
33
40
  }
34
41
  /**
35
42
  * Build an AgentClientMessage{run_request} for a conversation turn.
@@ -43,8 +50,9 @@ export function buildRunRequest(input) {
43
50
  // mcp_meta_tool_options. AgentRunRequest.mcp_tools (#4) stays empty on real
44
51
  // turns (CLI prewarm-only). Full defs are session.toolDescriptors + exec #36.
45
52
  const requestContext = input.requestContext;
53
+ const seeded = !(input.conversationState && input.conversationState.length > 0);
46
54
  const userMessage = {
47
- text: input.text,
55
+ text: seeded ? seedHistoryUserText(input.text, input.history) : input.text,
48
56
  message_id: msgId,
49
57
  };
50
58
  if (input.mode !== undefined)
@@ -65,9 +73,9 @@ export function buildRunRequest(input) {
65
73
  const action = input.action === "resume"
66
74
  ? { resume_action: {} }
67
75
  : { user_message_action: userMessageAction };
68
- const conversationState = input.conversationState && input.conversationState.length > 0
69
- ? input.conversationState
70
- : buildSeedConversationState({ history: input.history });
76
+ const conversationState = seeded
77
+ ? buildSeedConversationState()
78
+ : input.conversationState;
71
79
  const runRequest = {
72
80
  conversation_id: input.conversationId,
73
81
  conversation_group_id: input.conversationGroupId ?? input.conversationId,
@@ -98,3 +106,20 @@ export function buildHeartbeat() {
98
106
  client_heartbeat: {},
99
107
  });
100
108
  }
109
+ /**
110
+ * ACM #5 `exec_client_control_message.heartbeat{id}`: Cursor CLI sends it every
111
+ * 3 s for each exec it is still running, keyed by the exec message id. The CLI
112
+ * leaves a zero id out, as proto3 does for a default scalar; protobufjs would
113
+ * write it explicitly, so it is omitted here to send the CLI's exact bytes.
114
+ */
115
+ export function buildExecHeartbeat(execId) {
116
+ return encodeMessage("AgentClientMessage", {
117
+ exec_client_control_message: { heartbeat: execId === 0 ? {} : { id: execId } },
118
+ });
119
+ }
120
+ /** Stops the live Run, as Cursor CLI does on a user Stop before it closes the stream. */
121
+ export function buildCancelAction(reason) {
122
+ return encodeMessage("AgentClientMessage", {
123
+ conversation_action: { cancel_action: { reason } },
124
+ });
125
+ }
@@ -0,0 +1,49 @@
1
+ /** `hashes` is shared between entries and never mutated. */
2
+ type HeldImages = {
3
+ conversationId: string;
4
+ checkpoint: string;
5
+ hashes: readonly string[];
6
+ };
7
+ /**
8
+ * One Run's view: `held` by the checkpoint it last stood on, `checkpointed` sent
9
+ * before Cursor's latest checkpoint arrived, `sent` since, each with the last
10
+ * frame arrival at its send. Only a turn that ends on a checkpoint of its own
11
+ * moves `checkpointed` into `held`.
12
+ */
13
+ export type RunHistoryImages = {
14
+ held: readonly string[];
15
+ checkpointed: string[];
16
+ sent: Array<{
17
+ hash: string;
18
+ afterArrival: number;
19
+ }>;
20
+ };
21
+ export declare function checkpointFingerprint(checkpoint: Uint8Array): string;
22
+ /** The images `checkpoint` is known to hold. */
23
+ export declare function heldHistoryImageHashes(sessionKey: string, conversationId: string, checkpoint: Uint8Array): ReadonlySet<string> | undefined;
24
+ export declare function runHistoryImages(held?: ReadonlySet<string>): RunHistoryImages;
25
+ /** `lastArrival`: the last frame arrival once the images are written. */
26
+ export declare function noteHistoryImagesSent(run: RunHistoryImages, hashes: readonly string[], lastArrival?: number): void;
27
+ /**
28
+ * Cursor's checkpoints extend the one the Run started on, so each holds at least what that one held.
29
+ * A checkpoint that arrived before images were written (read only after a host tool's result) holds none of them.
30
+ */
31
+ export declare function noteHistoryImageCheckpoint(run: RunHistoryImages, sessionKey: string, conversationId: string, checkpoint: Uint8Array, arrival?: number): void;
32
+ /**
33
+ * The turn ended. On a checkpoint of its own, that checkpoint holds what the Run
34
+ * sent before it; without one, nothing the Run sent counts as held.
35
+ */
36
+ export declare function confirmHistoryImagesAtTurnEnd(run: RunHistoryImages, sessionKey: string, conversationId: string, checkpoint: Uint8Array | undefined): void;
37
+ /** For the restart snapshot: the set the checkpoint being saved holds. */
38
+ export declare function heldHistoryImagesSnapshot(sessionKey: string, conversationId: string, checkpoint: Uint8Array | undefined): {
39
+ checkpoint: string;
40
+ hashes: string[];
41
+ } | undefined;
42
+ export declare function restoreHeldHistoryImages(sessionKey: string, conversationId: string, saved: {
43
+ checkpoint?: string;
44
+ hashes?: readonly string[];
45
+ }): void;
46
+ export declare function forgetHeldHistoryImages(sessionKey: string): void;
47
+ export declare function snapshotHeldHistoryImagesForTests(sessionKey: string): HeldImages[];
48
+ export declare function resetHeldHistoryImagesForTests(): void;
49
+ export {};
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto";
2
+ // Content hashes of the history images a session's Cursor conversation holds, so
3
+ // growing history does not re-upload old screenshots. Each set belongs to one
4
+ // checkpoint: a Run skips an image only when it sends exactly that checkpoint.
5
+ /** Mirrors MAX_TURN_STATE_SESSIONS. */
6
+ const MAX_SESSIONS = 256;
7
+ const MAX_IMAGES_PER_SESSION = 256;
8
+ /** A stopped or failed turn leaves the next Run on one of its intermediate checkpoints. */
9
+ const MAX_CHECKPOINTS_PER_SESSION = 32;
10
+ /** Newest first. */
11
+ const heldBySession = new Map();
12
+ export function checkpointFingerprint(checkpoint) {
13
+ return createHash("sha256").update(checkpoint).digest("hex");
14
+ }
15
+ function newest(hashes) {
16
+ const unique = [...new Set([...hashes].reverse())].reverse();
17
+ return unique.slice(Math.max(0, unique.length - MAX_IMAGES_PER_SESSION));
18
+ }
19
+ function touch(sessionKey, entries) {
20
+ heldBySession.delete(sessionKey);
21
+ if (entries.length === 0)
22
+ return;
23
+ heldBySession.set(sessionKey, entries.slice(0, MAX_CHECKPOINTS_PER_SESSION));
24
+ while (heldBySession.size > MAX_SESSIONS) {
25
+ const oldest = heldBySession.keys().next().value;
26
+ if (!oldest)
27
+ break;
28
+ heldBySession.delete(oldest);
29
+ }
30
+ }
31
+ function find(sessionKey, conversationId, checkpoint) {
32
+ const entries = heldBySession.get(sessionKey);
33
+ if (!entries)
34
+ return undefined;
35
+ const fingerprint = checkpointFingerprint(checkpoint);
36
+ return entries.find((entry) => entry.conversationId === conversationId && entry.checkpoint === fingerprint);
37
+ }
38
+ /** The images `checkpoint` is known to hold. */
39
+ export function heldHistoryImageHashes(sessionKey, conversationId, checkpoint) {
40
+ const entry = find(sessionKey, conversationId, checkpoint);
41
+ if (!entry)
42
+ return undefined;
43
+ touch(sessionKey, heldBySession.get(sessionKey));
44
+ return new Set(entry.hashes);
45
+ }
46
+ function recordHeldHistoryImages(sessionKey, conversationId, checkpoint, hashes) {
47
+ if (hashes.length === 0)
48
+ return;
49
+ const entry = { conversationId, checkpoint: checkpointFingerprint(checkpoint), hashes };
50
+ const others = (heldBySession.get(sessionKey) ?? [])
51
+ .filter((other) => other.conversationId !== conversationId || other.checkpoint !== entry.checkpoint);
52
+ touch(sessionKey, [entry, ...others]);
53
+ }
54
+ export function runHistoryImages(held) {
55
+ return { held: [...(held ?? [])], checkpointed: [], sent: [] };
56
+ }
57
+ /** `lastArrival`: the last frame arrival once the images are written. */
58
+ export function noteHistoryImagesSent(run, hashes, lastArrival = 0) {
59
+ run.sent.push(...hashes.map((hash) => ({ hash, afterArrival: lastArrival })));
60
+ }
61
+ /**
62
+ * Cursor's checkpoints extend the one the Run started on, so each holds at least what that one held.
63
+ * A checkpoint that arrived before images were written (read only after a host tool's result) holds none of them.
64
+ */
65
+ export function noteHistoryImageCheckpoint(run, sessionKey, conversationId, checkpoint, arrival = Infinity) {
66
+ const later = run.sent.findIndex((image) => image.afterArrival >= arrival);
67
+ const earlier = later < 0 ? run.sent.length : later;
68
+ run.checkpointed.push(...run.sent.slice(0, earlier).map((image) => image.hash));
69
+ run.sent = run.sent.slice(earlier);
70
+ recordHeldHistoryImages(sessionKey, conversationId, checkpoint, run.held);
71
+ }
72
+ /**
73
+ * The turn ended. On a checkpoint of its own, that checkpoint holds what the Run
74
+ * sent before it; without one, nothing the Run sent counts as held.
75
+ */
76
+ export function confirmHistoryImagesAtTurnEnd(run, sessionKey, conversationId, checkpoint) {
77
+ if (checkpoint?.length) {
78
+ run.held = newest([...run.held, ...run.checkpointed]);
79
+ recordHeldHistoryImages(sessionKey, conversationId, checkpoint, run.held);
80
+ }
81
+ run.checkpointed = [];
82
+ run.sent = [];
83
+ }
84
+ /** For the restart snapshot: the set the checkpoint being saved holds. */
85
+ export function heldHistoryImagesSnapshot(sessionKey, conversationId, checkpoint) {
86
+ const entry = checkpoint?.length ? find(sessionKey, conversationId, checkpoint) : undefined;
87
+ return entry ? { checkpoint: entry.checkpoint, hashes: [...entry.hashes] } : undefined;
88
+ }
89
+ export function restoreHeldHistoryImages(sessionKey, conversationId, saved) {
90
+ touch(sessionKey, saved.checkpoint && saved.hashes?.length
91
+ ? [{ conversationId, checkpoint: saved.checkpoint, hashes: newest(saved.hashes) }]
92
+ : []);
93
+ }
94
+ export function forgetHeldHistoryImages(sessionKey) {
95
+ heldBySession.delete(sessionKey);
96
+ }
97
+ export function snapshotHeldHistoryImagesForTests(sessionKey) {
98
+ return (heldBySession.get(sessionKey) ?? []).map((entry) => ({ ...entry, hashes: [...entry.hashes] }));
99
+ }
100
+ export function resetHeldHistoryImagesForTests() {
101
+ heldBySession.clear();
102
+ }
@@ -77,3 +77,5 @@ export declare function extractProtobufSubmessage(buf: Uint8Array, path: number[
77
77
  * can correlate display tool_call_started with the authoritative exec path.
78
78
  */
79
79
  export declare function extractExecDisplayCallId(execMsg: Record<string, unknown>): string | undefined;
80
+ /** File paths a display TaskToolCall attaches; Cursor reads each one before its subagent request. */
81
+ export declare function taskToolCallAttachments(toolCall: Record<string, unknown> | undefined): string[];
@@ -731,3 +731,13 @@ export function extractExecDisplayCallId(execMsg) {
731
731
  }
732
732
  return undefined;
733
733
  }
734
+ /** File paths a display TaskToolCall attaches; Cursor reads each one before its subagent request. */
735
+ export function taskToolCallAttachments(toolCall) {
736
+ if (!toolCall || findToolVariant(toolCall) !== "task_tool_call")
737
+ return [];
738
+ const payload = asRecord(toolCall.task_tool_call);
739
+ const attachments = payload ? unwrapArgs(payload).attachments : undefined;
740
+ if (!Array.isArray(attachments))
741
+ return [];
742
+ return attachments.filter((item) => typeof item === "string" && item.trim().length > 0);
743
+ }
@@ -1,3 +1,4 @@
1
+ import type { HookAdditionalContext } from "./host-notes.js";
1
2
  import type { CursorImageInput } from "../image-input.js";
2
3
  import { type CursorExecVariant } from "./exec-variants.js";
3
4
  import { type CursorShellOutcome } from "../shell-timeout.js";
@@ -124,6 +125,30 @@ export declare function resolveCursorSubagentType(subagentType: string, catalog?
124
125
  export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string;
125
126
  /** Resolve a native Cursor subagent request to this turn's executor and catalog. */
126
127
  export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, catalog?: HostSubagentCatalog): void;
128
+ /**
129
+ * Who answers the private read Cursor sends under an edit's call id. Both the
130
+ * exec handler and the early tool row decide from this, so a row never opens
131
+ * for an edit whose read becomes its own host call.
132
+ *
133
+ * - `empty`: no target. The provider answers an empty file; creation continues.
134
+ * - `complete`: a readable regular file within Cursor's edit limit whose real
135
+ * path is inside the workspace. The provider answers its complete content.
136
+ * - `external`: an existing target not proven inside the workspace by real
137
+ * path, including through a symlink rooted in it. OpenCode's
138
+ * permission-aware `read` answers; once authorized, the continuation
139
+ * upgrades the result to the complete content.
140
+ * - `host`: any other existing target (not a regular file, too large,
141
+ * unreadable, or not stat-able). OpenCode's `read` answers as is.
142
+ */
143
+ export type CursorEditReadAnswer = "empty" | "complete" | "external" | "host";
144
+ export declare function cursorEditReadAnswer(absolutePath: string, workspaceRoot: string): CursorEditReadAnswer;
145
+ /**
146
+ * The edit's private read becomes its own host `read` call. A read the edit
147
+ * handshake leaves to the host can still be answered by the provider's
148
+ * missing-target validation (a dangling symlink, a directory); that read
149
+ * never reaches the host.
150
+ */
151
+ export declare function cursorEditReadReachesHost(absolutePath: string, workspaceRoot: string): boolean;
127
152
  /**
128
153
  * Complete the private read inside Cursor's legacy edit transaction without
129
154
  * routing it through OpenCode's user-facing `read` tool. The latter caps every
@@ -134,6 +159,28 @@ export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest,
134
159
  * file named by an active `edit_tool_call` and is contained by the workspace.
135
160
  */
136
161
  export declare function buildCompleteEditReadMessages(execId: number, sourcePath: string, resultPath?: string): Uint8Array[] | undefined;
162
+ /**
163
+ * Answer Cursor's read of a Task attachment without the file's bytes. Cursor
164
+ * puts them only in SubagentArgs.selected_context, which OpenCode's subagent
165
+ * tools cannot take, yet refuses the Task unless the read returns binary data.
166
+ * The path reaches the subagent prompt instead ({@link appendTaskAttachments})
167
+ * and the child reads it under its own permissions. A missing or non-regular
168
+ * target gets the same typed rejection as any native read.
169
+ */
170
+ export declare function buildTaskAttachmentReadMessages(execId: number, sourcePath: string, resultPath?: string): Uint8Array[];
171
+ /** List Cursor's Task attachments at the end of the subagent prompt as absolute paths. */
172
+ export declare function appendTaskAttachments(parsed: ParsedExecRequest, attachments: readonly string[], workspaceRoot: string): void;
173
+ /**
174
+ * Host tool a Cursor edit announcement will become, using the same file facts
175
+ * as `remapCorrelatedEditWriteForCatalog` and `remapEditToolsForCatalog`. An
176
+ * existing file becomes `edit`, else the patch tool. A missing file is a
177
+ * creation, which stays `write`, else becomes the patch tool's Add File; so
178
+ * does an empty file, which has no text for an `edit` to replace. A name is
179
+ * withheld when those remaps would refuse the file or not produce that tool:
180
+ * no workspace for a relative path, not a regular file, too large,
181
+ * unreadable, or `patch` with carriage returns.
182
+ */
183
+ export declare function hostToolForCursorEdit(filePath: string, catalog: ReadonlySet<string>, workspaceRoot?: string): string | undefined;
137
184
  /**
138
185
  * Preserve a legacy Cursor edit's intent when its internal executor follows
139
186
  * `edit_tool_call` with a whole-file `write_args` request.
@@ -143,19 +190,19 @@ export declare function buildCompleteEditReadMessages(execId: number, sourcePath
143
190
  */
144
191
  export declare function remapCorrelatedEditWriteForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, editPath: string, workspaceRoot?: string): boolean;
145
192
  /**
146
- * Express a native Cursor write/edit as `apply_patch` when the host swapped the
193
+ * Express a native Cursor write/edit as a patch call when the host swapped the
147
194
  * edit-tool family out from under us.
148
195
  *
149
- * OpenCode 1.x removes `edit` and `write` from the catalog for GPT models and
150
- * advertises `apply_patch` in their place — see the module comment in
151
- * `./apply-patch.ts` for the upstream reasoning and citations. Cursor keeps
152
- * using its native write/edit exec channel regardless, so without this the
153
- * request is refused as an unavailable tool and the model loses the ability to
154
- * change files at all.
196
+ * Both OpenCode majors remove `edit` and `write` from the catalog for GPT
197
+ * models and advertise a patch tool in their place (`apply_patch` on 1.x,
198
+ * `patch` on 2.0) — see the module comment in `./apply-patch.ts` for the
199
+ * upstream reasoning and citations. Cursor keeps using its native write/edit
200
+ * exec channel regardless, so without this the request is refused as an
201
+ * unavailable tool.
155
202
  *
156
203
  * Keyed purely off the advertised set: this is inert whenever the host offers
157
204
  * `write`/`edit` normally, and equally inert when it offers neither those nor
158
- * `apply_patch` (the caller's unavailable-tool rejection still applies).
205
+ * a patch tool (the caller's unavailable-tool rejection still applies).
159
206
  */
160
207
  export declare function remapEditToolsForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, workspaceRoot?: string): void;
161
208
  export type ParsedExecRequest = {
@@ -268,6 +315,8 @@ export type ToolResultInput = {
268
315
  resultField: string;
269
316
  output: string;
270
317
  error?: string;
318
+ refusal?: "permission_denied" | "rejected";
319
+ interrupted?: boolean;
271
320
  executionTimeMs?: number;
272
321
  /**
273
322
  * Resolved opencode tool name (read/write/grep/…). Gates read-envelope
@@ -304,6 +353,16 @@ export declare function execResultImages(resultField: string, images: readonly C
304
353
  * shell execs hang on heartbeats forever).
305
354
  */
306
355
  export declare function buildExecClientMessages(input: ToolResultInput): Uint8Array[];
356
+ /**
357
+ * Attach host notes to an exec result as Cursor CLI postToolUse hook context:
358
+ * `ExecClientMessage.hook_additional_contexts` (#45) on a unary result, or a
359
+ * `ShellStream.hook_context` (#8) event after the stream's own events and
360
+ * before `stream_close` (CLI agent-exec hook wrapper). Cursor renders it as a
361
+ * system reminder on that tool result, so it never mixes into file content.
362
+ * Returns undefined when the frames carry no exec result (bridged
363
+ * interaction replies have no such field).
364
+ */
365
+ export declare function attachHookAdditionalContexts(frames: readonly Uint8Array[], contexts: readonly HookAdditionalContext[]): Uint8Array[] | undefined;
307
366
  /** ACM #5 exec_client_control_message { stream_close { id } }. */
308
367
  export declare function buildExecStreamClose(execId: number): Uint8Array;
309
368
  export declare function buildUnsupportedExecDeny(input: {
@@ -331,6 +390,13 @@ export declare function buildUnsupportedExecDeny(input: {
331
390
  * alone is not a tool-identity check.
332
391
  */
333
392
  export declare function unwrapReadOutput(output: string): string;
393
+ /**
394
+ * OpenCode's read tool appends newly discovered instructions after the
395
+ * envelope's unnumbered `</content>`, in one `<system-reminder>` block.
396
+ * The numbered-body parser stops before that block, so callers must lift it
397
+ * out before the file text is encoded.
398
+ */
399
+ export declare function extractOpenCodeReadInstruction(output: string): string | undefined;
334
400
  /**
335
401
  * Map OpenCode tool text into the agent.v1 result oneof for each exec variant.
336
402
  * OpenCode returns free-form text; we wrap it in the minimal success shape the