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.
- package/CHANGELOG.md +174 -0
- package/README.md +230 -44
- package/SECURITY.md +60 -12
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +23 -4
- package/dist/context/build.js +65 -10
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/frozen.js +6 -2
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +45 -18
- package/dist/context/paths.js +72 -22
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +122 -30
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +149 -30
- package/dist/language-model.js +3127 -673
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +12 -1
- package/dist/model-config.js +78 -30
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +233 -65
- package/dist/plugin.js +97 -23
- package/dist/pricing-data.d.ts +40 -3
- package/dist/pricing-data.js +40 -3
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +164 -21
- package/dist/protocol/request.d.ts +40 -19
- package/dist/protocol/request.js +55 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +6 -0
- package/dist/protocol/tool-call-bridge.js +24 -0
- package/dist/protocol/tools.d.ts +90 -9
- package/dist/protocol/tools.js +543 -151
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +22 -4
- package/dist/replay-safety.js +86 -27
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +129 -10
- package/dist/session.js +198 -35
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +10 -1
- package/dist/transport/connect.js +124 -27
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +85 -34
- package/package.json +5 -3
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
|
@@ -17,18 +17,18 @@
|
|
|
17
17
|
* through the Question service and, on "Yes", injects a synthetic user message
|
|
18
18
|
* carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a
|
|
19
19
|
* `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
|
|
20
|
-
* `session/prompt.ts`).
|
|
21
|
-
* observable halves — the approval gate and the behavioural contract — are both
|
|
22
|
-
* reachable, so the protocol bridge degrades instead of refusing when the host
|
|
23
|
-
* tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its
|
|
20
|
+
* `session/prompt.ts`). OpenCode 2 exposes `session.switchAgent`; its
|
|
24
21
|
* entrypoint installs a structural callback that selects the native `plan` or
|
|
25
22
|
* `build` primary agent after the owning Cursor Run becomes terminal:
|
|
26
23
|
*
|
|
27
24
|
* - entering plan/spec needs no host tool at all — approve immediately and let
|
|
28
25
|
* the injected <system_reminder> carry the contract, exactly as Cursor CLI's
|
|
29
26
|
* own plan mode is prompt-enforced.
|
|
30
|
-
* - leaving
|
|
31
|
-
*
|
|
27
|
+
* - leaving the host's own `plan` agent without its `plan_exit` goes through
|
|
28
|
+
* CreatePlan, whose `question` prompt is the execution approval, so the
|
|
29
|
+
* request is rejected with a pointer there (or, with no `question`, to the
|
|
30
|
+
* user's agent switch); a Cursor-only plan mode has nothing on the host to
|
|
31
|
+
* leave and is approved.
|
|
32
32
|
*
|
|
33
33
|
* Resolution is keyed solely on the advertised catalog under canonical
|
|
34
34
|
* OpenCode tool names — never on any external identity or model id.
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* CLI reject reason for user declines: "Mode switch rejected by user"
|
|
37
37
|
* (chunk-7076/dist/ui.js onSwitchModeReject).
|
|
38
38
|
*/
|
|
39
|
-
import {
|
|
39
|
+
import { CURSOR_PLAN_STAGE_TOOL } from "./create-plan.js";
|
|
40
40
|
import { decodeMessageSparse } from "./messages.js";
|
|
41
41
|
/**
|
|
42
42
|
* `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
|
|
@@ -59,13 +59,18 @@ export const MISSING_TARGET_REASON = "Missing targetModeId";
|
|
|
59
59
|
* even though the next Run will succeed. CreatePlan already soft-acks the same
|
|
60
60
|
* case; SwitchMode must match.
|
|
61
61
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export const
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The host's plan agent is active and offers no `plan_exit`: on such a host
|
|
64
|
+
* the user ends plan mode by switching agents, so the model cannot.
|
|
65
|
+
*/
|
|
66
|
+
export const PLAN_EXIT_BY_USER_REASON = "This host leaves plan mode when the user switches from the plan agent to the build " +
|
|
67
|
+
"agent. Ask the user to switch agents when the plan is ready; do not implement before then.";
|
|
68
|
+
/**
|
|
69
|
+
* The host's plan agent is active, offers no `plan_exit`, but can ask: leaving
|
|
70
|
+
* plan mode goes through CreatePlan, whose `question` prompt is the approval.
|
|
71
|
+
*/
|
|
72
|
+
export const PLAN_EXIT_VIA_CREATE_PLAN_REASON = "Leave plan mode by recording the finished plan with Cursor CreatePlan: the user is then " +
|
|
73
|
+
"asked whether to switch to the build agent and start implementing. Do not implement before they approve.";
|
|
69
74
|
const activeCursorModeBySession = new Map();
|
|
70
75
|
const MAX_ACTIVE_CURSOR_MODES = 256;
|
|
71
76
|
function asRecord(value) {
|
|
@@ -97,46 +102,14 @@ export function mapSwitchModeTarget(targetModeId) {
|
|
|
97
102
|
}
|
|
98
103
|
return { ok: true, toolName: "plan_exit" };
|
|
99
104
|
}
|
|
100
|
-
/** The synthetic AskQuestion item backing a question-emulated plan exit. */
|
|
101
|
-
function switchModeExitQuestionItem() {
|
|
102
|
-
return {
|
|
103
|
-
id: "switch_mode_exit",
|
|
104
|
-
prompt: SWITCH_MODE_EXIT_QUESTION,
|
|
105
|
-
options: [
|
|
106
|
-
{ id: "yes", label: SWITCH_MODE_EXIT_YES },
|
|
107
|
-
{ id: "no", label: SWITCH_MODE_EXIT_NO },
|
|
108
|
-
],
|
|
109
|
-
allowMultiple: false,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
|
|
113
|
-
export function switchModeExitQuestionInput() {
|
|
114
|
-
return {
|
|
115
|
-
questions: [
|
|
116
|
-
{
|
|
117
|
-
question: SWITCH_MODE_EXIT_QUESTION,
|
|
118
|
-
header: SWITCH_MODE_EXIT_HEADER,
|
|
119
|
-
options: [
|
|
120
|
-
{
|
|
121
|
-
label: SWITCH_MODE_EXIT_YES,
|
|
122
|
-
description: "Switch to build agent and start implementing the plan",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
label: SWITCH_MODE_EXIT_NO,
|
|
126
|
-
description: "Stay with plan agent to continue refining the plan",
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
105
|
/**
|
|
134
106
|
* Resolve how this SwitchMode target is satisfied, keyed only on the advertised
|
|
135
107
|
* catalog under canonical tool names.
|
|
136
108
|
*
|
|
137
|
-
* A missing plan tool is not a refusal: entering plan mode needs no host tool
|
|
138
|
-
*
|
|
139
|
-
*
|
|
109
|
+
* A missing plan tool is not a refusal: entering plan mode needs no host tool.
|
|
110
|
+
* Leaving the host's own plan agent without `plan_exit` is approved through
|
|
111
|
+
* CreatePlan (or the user's agent switch when nothing can ask), and a
|
|
112
|
+
* Cursor-only plan mode (no host plan agent) has nothing on the host to leave.
|
|
140
113
|
*/
|
|
141
114
|
export function resolveSwitchModeBridge(targetModeId, options) {
|
|
142
115
|
const mapped = mapSwitchModeTarget(targetModeId);
|
|
@@ -162,11 +135,13 @@ export function resolveSwitchModeBridge(targetModeId, options) {
|
|
|
162
135
|
// whole contract, exactly as Cursor CLI's own plan mode is prompt-enforced.
|
|
163
136
|
if (mapped.toolName === "plan_enter")
|
|
164
137
|
return { kind: "approve" };
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
138
|
+
if (options.hostAgent === "plan") {
|
|
139
|
+
return {
|
|
140
|
+
kind: "reject",
|
|
141
|
+
reason: names.has("question") ? PLAN_EXIT_VIA_CREATE_PLAN_REASON : PLAN_EXIT_BY_USER_REASON,
|
|
142
|
+
};
|
|
168
143
|
}
|
|
169
|
-
return { kind: "
|
|
144
|
+
return { kind: "approve" };
|
|
170
145
|
}
|
|
171
146
|
/** Decode a `switch_mode_request_query` body, or undefined when unusable. */
|
|
172
147
|
export function decodeSwitchModeQuery(queryBytes) {
|
|
@@ -222,20 +197,6 @@ export function switchModeResultFromToolOutput(output, isError) {
|
|
|
222
197
|
}
|
|
223
198
|
return switchModeRejectedResult(trimmed);
|
|
224
199
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated
|
|
227
|
-
* plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt
|
|
228
|
-
* keeps the model in plan mode rather than silently starting execution.
|
|
229
|
-
*/
|
|
230
|
-
export function switchModeResultFromQuestionOutput(output, isError) {
|
|
231
|
-
if (isError)
|
|
232
|
-
return switchModeResultFromToolOutput(output, true);
|
|
233
|
-
const [segment] = parseAnswerSegments([switchModeExitQuestionItem()], output);
|
|
234
|
-
const answer = (segment ?? "").trim().toLowerCase();
|
|
235
|
-
if (answer === SWITCH_MODE_EXIT_YES.toLowerCase())
|
|
236
|
-
return switchModeApprovedResult();
|
|
237
|
-
return switchModeRejectedResult(USER_REJECTED_REASON);
|
|
238
|
-
}
|
|
239
200
|
/** Record the approved Cursor mode for later system-reminder injection. */
|
|
240
201
|
export function setActiveCursorMode(sessionKey, targetModeId, options = {}) {
|
|
241
202
|
if (!sessionKey)
|
|
@@ -265,13 +226,121 @@ export function isCursorPlanModeActive(sessionKey) {
|
|
|
265
226
|
const mode = getActiveCursorMode(sessionKey);
|
|
266
227
|
return mode === "plan" || mode === "spec";
|
|
267
228
|
}
|
|
268
|
-
/** True
|
|
229
|
+
/** True while Cursor plan/spec mode is one the host enforces (see `bridgedPlanEntered`). */
|
|
269
230
|
export function isBridgedCursorPlanModeActive(sessionKey) {
|
|
270
231
|
if (!sessionKey)
|
|
271
232
|
return false;
|
|
272
233
|
const state = activeCursorModeBySession.get(sessionKey);
|
|
273
234
|
return (state?.modeId === "plan" || state?.modeId === "spec") && state.bridgedPlanEntered;
|
|
274
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Cursor plan mode follows the host's `plan` primary agent, as Cursor CLI's
|
|
238
|
+
* mode follows its own mode switch (Shift+Tab, `/plan`).
|
|
239
|
+
*
|
|
240
|
+
* - A turn under the host plan agent puts Cursor in plan mode, however the
|
|
241
|
+
* host got there (its agent picker, a plan_enter approval).
|
|
242
|
+
* - The host's own exit from that agent (a plan_exit approval, or an agent
|
|
243
|
+
* switch in its UI) ends Cursor plan mode. Only an observed `plan` → other
|
|
244
|
+
* transition counts, so Cursor-only modes (debug, ask, …) under the host's
|
|
245
|
+
* ordinary agent are kept; the next Run then carries the agent-mode
|
|
246
|
+
* reminder instead of a stale "plan mode is still active" one.
|
|
247
|
+
*/
|
|
248
|
+
export function followHostPlanAgent(sessionKey, previousHostAgent, hostAgent) {
|
|
249
|
+
if (!sessionKey || !hostAgent)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (hostAgent === "plan") {
|
|
252
|
+
if (isBridgedCursorPlanModeActive(sessionKey))
|
|
253
|
+
return undefined;
|
|
254
|
+
// The host runs its own plan agent, so it enforces this plan mode.
|
|
255
|
+
setActiveCursorMode(sessionKey, "plan", { bridgedPlanEntered: true });
|
|
256
|
+
return "entered";
|
|
257
|
+
}
|
|
258
|
+
if (previousHostAgent !== "plan" || !isCursorPlanModeActive(sessionKey))
|
|
259
|
+
return undefined;
|
|
260
|
+
setActiveCursorMode(sessionKey, "agent");
|
|
261
|
+
return "left";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* `agent.v1.AgentMode` for a Cursor mode id, as Cursor CLI derives
|
|
265
|
+
* `UserMessage.mode` from its current mode: plan, ask, and debug are their own
|
|
266
|
+
* modes; every other mode (agent, spec, triage, …) runs as agent.
|
|
267
|
+
*/
|
|
268
|
+
export function cursorAgentModeWireValue(modeId) {
|
|
269
|
+
switch (normalizeSwitchModeId(modeId ?? "")) {
|
|
270
|
+
case "plan":
|
|
271
|
+
return 3;
|
|
272
|
+
case "chat":
|
|
273
|
+
case "ask":
|
|
274
|
+
case "search":
|
|
275
|
+
return 2;
|
|
276
|
+
case "debug":
|
|
277
|
+
return 4;
|
|
278
|
+
default:
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Conversation that already received the host plan-exit call shape, per session. */
|
|
283
|
+
const hostPlanExitNoteBySession = new Map();
|
|
284
|
+
function describeToolArguments(schema) {
|
|
285
|
+
const record = asRecord(schema);
|
|
286
|
+
const properties = asRecord(record?.properties);
|
|
287
|
+
const names = properties ? Object.keys(properties) : [];
|
|
288
|
+
if (names.length === 0)
|
|
289
|
+
return "It takes no arguments: call it with `{}`.";
|
|
290
|
+
const required = new Set(Array.isArray(record?.required) ? record.required.filter((name) => typeof name === "string") : []);
|
|
291
|
+
const lines = names.map((name) => {
|
|
292
|
+
const property = asRecord(properties[name]);
|
|
293
|
+
const type = str(property?.type) || "value";
|
|
294
|
+
const description = str(property?.description).trim();
|
|
295
|
+
return `- \`${name}\` (${type}, ${required.has(name) ? "required" : "optional"})` +
|
|
296
|
+
(description ? `: ${description}` : "");
|
|
297
|
+
});
|
|
298
|
+
return `Its arguments (use exactly these names):\n${lines.join("\n")}`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* One-shot translation of the host plan agent's workflow into Cursor terms,
|
|
302
|
+
* delivered once per Cursor conversation while that agent owns the turn.
|
|
303
|
+
*
|
|
304
|
+
* With the host's plan file known, Cursor's native CreatePlan carries out the
|
|
305
|
+
* host instruction "write the plan file, then call plan_exit" (see the `exit`
|
|
306
|
+
* CreatePlan bridge), so the model needs no host tool at all. Without it, the
|
|
307
|
+
* model is given `plan_exit`'s exact call shape from the live schema: the
|
|
308
|
+
* RequestContext overlay is names-only, and otherwise it must look the tool up
|
|
309
|
+
* (or guess its arguments) before it can submit the plan.
|
|
310
|
+
*/
|
|
311
|
+
export function takeHostPlanAgentNote(sessionKey, conversationId, hostAgent, tools, options = {}) {
|
|
312
|
+
if (!sessionKey)
|
|
313
|
+
return undefined;
|
|
314
|
+
if (hostAgent !== "plan") {
|
|
315
|
+
hostPlanExitNoteBySession.delete(sessionKey);
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const planExit = tools.find((tool) => tool.name === "plan_exit");
|
|
319
|
+
// A host plan-stage tool carries the plan to the host review itself.
|
|
320
|
+
if (tools.some((tool) => tool.name === CURSOR_PLAN_STAGE_TOOL))
|
|
321
|
+
return undefined;
|
|
322
|
+
if (!planExit || hostPlanExitNoteBySession.get(sessionKey) === conversationId)
|
|
323
|
+
return undefined;
|
|
324
|
+
hostPlanExitNoteBySession.delete(sessionKey);
|
|
325
|
+
hostPlanExitNoteBySession.set(sessionKey, conversationId);
|
|
326
|
+
while (hostPlanExitNoteBySession.size > MAX_ACTIVE_CURSOR_MODES) {
|
|
327
|
+
const oldest = hostPlanExitNoteBySession.keys().next().value;
|
|
328
|
+
if (!oldest)
|
|
329
|
+
break;
|
|
330
|
+
hostPlanExitNoteBySession.delete(oldest);
|
|
331
|
+
}
|
|
332
|
+
if (options.hostPlanFile) {
|
|
333
|
+
return wrapReminder("The host plan agent is active. When the plan is ready, record it with CreatePlan. " +
|
|
334
|
+
`CreatePlan saves it as the host's plan file (${options.hostPlanFile}) and submits it ` +
|
|
335
|
+
"to the host's plan review, which asks the user; this carries out the host " +
|
|
336
|
+
"instructions to write the plan file and call plan_exit, so do neither yourself. " +
|
|
337
|
+
"Do not implement until the user approves.");
|
|
338
|
+
}
|
|
339
|
+
return wrapReminder("The host plan agent is active. When the plan file is written and ready for review, " +
|
|
340
|
+
`submit it by calling MCP tool \`plan_exit\` on server \`${options.server ?? "opencode"}\` directly. ` +
|
|
341
|
+
"Its full definition is below, so do not look it up with GetMcpTools first. " +
|
|
342
|
+
describeToolArguments(planExit.inputSchema));
|
|
343
|
+
}
|
|
275
344
|
export function clearActiveCursorMode(sessionKey) {
|
|
276
345
|
if (!sessionKey)
|
|
277
346
|
return;
|
|
@@ -279,6 +348,7 @@ export function clearActiveCursorMode(sessionKey) {
|
|
|
279
348
|
}
|
|
280
349
|
export function resetActiveCursorModesForTests() {
|
|
281
350
|
activeCursorModeBySession.clear();
|
|
351
|
+
hostPlanExitNoteBySession.clear();
|
|
282
352
|
}
|
|
283
353
|
function wrapReminder(body) {
|
|
284
354
|
return `<system_reminder>\n${body.trim()}\n</system_reminder>`;
|
|
@@ -295,12 +365,15 @@ export function cursorModeSystemReminder(targetModeId, options = {}) {
|
|
|
295
365
|
const first = options.firstTurn !== false;
|
|
296
366
|
// A host plan-stage tool writes the artifact and its result names the
|
|
297
367
|
// follow-up that requests approval. plan_exit only leaves plan mode.
|
|
298
|
-
// Without either
|
|
299
|
-
//
|
|
368
|
+
// Without either, CreatePlan asks through `question` whether to implement;
|
|
369
|
+
// with no `question` either, the user leaves plan mode by switching agents.
|
|
370
|
+
// Naming an unavailable tool would strand the model.
|
|
300
371
|
const leavePlan = options.planStageAdvertised
|
|
301
372
|
? "record the finished plan with Cursor CreatePlan. The host stage tool waits for the host plan review and does not return until the user accepts or declines. Do not call `plan_exit` to submit or skip that review, and do not implement until the tool returns success"
|
|
302
373
|
: options.planExitAdvertised === false
|
|
303
|
-
?
|
|
374
|
+
? options.questionAdvertised
|
|
375
|
+
? "record the finished plan with Cursor CreatePlan. Writing it needs no approval; the user is then asked whether to switch to the build agent and start implementing. If they decline, refine the plan and record it again"
|
|
376
|
+
: "record the finished plan (Cursor CreatePlan), then tell the user it is ready: they switch to the build agent to have it implemented"
|
|
304
377
|
: "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode";
|
|
305
378
|
if (id === "plan" || id === "spec") {
|
|
306
379
|
return wrapReminder(first
|
|
@@ -413,7 +486,7 @@ export function takeActiveCursorModeReminder(sessionKey, options = {}) {
|
|
|
413
486
|
: new Set(options.advertisedTools)
|
|
414
487
|
: undefined;
|
|
415
488
|
const isPlanMode = state.modeId === "plan" || state.modeId === "spec";
|
|
416
|
-
if (isPlanMode && state.bridgedPlanEntered && advertised) {
|
|
489
|
+
if (isPlanMode && state.bridgedPlanEntered && advertised && !options.hostAgent) {
|
|
417
490
|
if (advertised.has("plan_enter")) {
|
|
418
491
|
activeCursorModeBySession.delete(sessionKey);
|
|
419
492
|
return cursorModeSystemReminder("agent", { firstTurn: true });
|
|
@@ -425,6 +498,7 @@ export function takeActiveCursorModeReminder(sessionKey, options = {}) {
|
|
|
425
498
|
? {
|
|
426
499
|
planExitAdvertised: advertised.has("plan_exit"),
|
|
427
500
|
planStageAdvertised: advertised.has("cursor_plan_stage"),
|
|
501
|
+
questionAdvertised: advertised.has("question"),
|
|
428
502
|
}
|
|
429
503
|
: {}),
|
|
430
504
|
});
|
|
@@ -15,6 +15,8 @@ export type CursorConversationTokenDetails = {
|
|
|
15
15
|
breakdown?: CursorPromptTokenBreakdown;
|
|
16
16
|
};
|
|
17
17
|
export type CursorContextUsageSource = "checkpoint-current-run" | "checkpoint-previous-turn";
|
|
18
|
+
/** Cursor can update occupancy while retaining categories from an older checkpoint. */
|
|
19
|
+
export declare function currentCursorTokenBreakdown(details: CursorConversationTokenDetails | undefined): CursorPromptTokenBreakdown | undefined;
|
|
18
20
|
/**
|
|
19
21
|
* Read field #5 (`ConversationTokenDetails`) from Cursor's opaque
|
|
20
22
|
* `ConversationStateStructure` checkpoint without decoding or re-encoding the
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import protobuf from "protobufjs";
|
|
2
|
+
/** Cursor can update occupancy while retaining categories from an older checkpoint. */
|
|
3
|
+
export function currentCursorTokenBreakdown(details) {
|
|
4
|
+
const breakdown = details?.breakdown;
|
|
5
|
+
return breakdown
|
|
6
|
+
&& breakdown.totalUsedTokens === details.usedTokens
|
|
7
|
+
&& breakdown.maxTokens === details.maxTokens
|
|
8
|
+
? breakdown
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
2
11
|
const MAX_BREAKDOWN_CATEGORIES = 128;
|
|
3
12
|
function requireWireType(actual, expected, field) {
|
|
4
13
|
if (actual !== expected)
|
|
@@ -142,6 +151,7 @@ export function cursorContextUsageMetadata(details, source = "checkpoint-current
|
|
|
142
151
|
const usedPercent = details.maxTokens > 0
|
|
143
152
|
? Math.max(0, Math.min(100, Math.round(details.usedTokens / details.maxTokens * 1_000) / 10))
|
|
144
153
|
: undefined;
|
|
154
|
+
const breakdown = currentCursorTokenBreakdown(details);
|
|
145
155
|
return {
|
|
146
156
|
contextUsageVersion: 2,
|
|
147
157
|
source,
|
|
@@ -150,6 +160,6 @@ export function cursorContextUsageMetadata(details, source = "checkpoint-current
|
|
|
150
160
|
maxTokens: details.maxTokens,
|
|
151
161
|
remainingTokens: Math.max(0, details.maxTokens - details.usedTokens),
|
|
152
162
|
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
153
|
-
...(
|
|
163
|
+
...(breakdown ? { breakdown } : {}),
|
|
154
164
|
};
|
|
155
165
|
}
|
|
@@ -22,6 +22,10 @@ export type BridgedOpenCodeToolCall = {
|
|
|
22
22
|
};
|
|
23
23
|
/** Display variants Cursor executes itself (mcp_state + optional write spill). */
|
|
24
24
|
export declare function isNativeDisplayToolCall(variant: string): boolean;
|
|
25
|
+
/** Server-side dynamic-call failures may complete without a started/exec frame. */
|
|
26
|
+
export declare function displayMcpToolError(toolCall: Record<string, unknown> | undefined): string | undefined;
|
|
27
|
+
/** Native discovery errors are server-side results, never host execution. */
|
|
28
|
+
export declare function displayNativeDiscoveryError(toolCall: Record<string, unknown> | undefined): string | undefined;
|
|
25
29
|
/**
|
|
26
30
|
* Normalize a full todo snapshot for `mirroredTodos` storage. Returns
|
|
27
31
|
* undefined when the input is not a todo array (nothing to learn). Drops the
|
|
@@ -73,3 +77,5 @@ export declare function extractProtobufSubmessage(buf: Uint8Array, path: number[
|
|
|
73
77
|
* can correlate display tool_call_started with the authoritative exec path.
|
|
74
78
|
*/
|
|
75
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[];
|
|
@@ -69,6 +69,20 @@ export function isNativeDisplayToolCall(variant) {
|
|
|
69
69
|
function asRecord(v) {
|
|
70
70
|
return v && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
71
71
|
}
|
|
72
|
+
/** Server-side dynamic-call failures may complete without a started/exec frame. */
|
|
73
|
+
export function displayMcpToolError(toolCall) {
|
|
74
|
+
const call = asRecord(toolCall?.mcp_tool_call);
|
|
75
|
+
const result = asRecord(call?.result);
|
|
76
|
+
const error = asRecord(result?.error);
|
|
77
|
+
const parts = [error?.error, error?.detail].filter((value) => typeof value === "string" && value.length > 0);
|
|
78
|
+
return parts.length > 0 ? parts.join(": ") : undefined;
|
|
79
|
+
}
|
|
80
|
+
/** Native discovery errors are server-side results, never host execution. */
|
|
81
|
+
export function displayNativeDiscoveryError(toolCall) {
|
|
82
|
+
const call = asRecord(toolCall?.get_mcp_tools_tool_call);
|
|
83
|
+
const error = asRecord(asRecord(call?.result)?.error)?.error;
|
|
84
|
+
return typeof error === "string" && error.length > 0 ? error : undefined;
|
|
85
|
+
}
|
|
72
86
|
function findToolVariant(toolCall) {
|
|
73
87
|
for (const key of TOOL_CALL_VARIANTS) {
|
|
74
88
|
if (toolCall[key] != null)
|
|
@@ -717,3 +731,13 @@ export function extractExecDisplayCallId(execMsg) {
|
|
|
717
731
|
}
|
|
718
732
|
return undefined;
|
|
719
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
|
+
}
|
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { HookAdditionalContext } from "./host-notes.js";
|
|
2
|
+
import type { CursorImageInput } from "../image-input.js";
|
|
1
3
|
import { type CursorExecVariant } from "./exec-variants.js";
|
|
2
4
|
import { type CursorShellOutcome } from "../shell-timeout.js";
|
|
3
5
|
export declare const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
@@ -8,6 +10,10 @@ export type OpencodeToolDef = {
|
|
|
8
10
|
/** Original flattened identity when `name` is a Cursor-facing alias. */
|
|
9
11
|
sourceName?: string;
|
|
10
12
|
};
|
|
13
|
+
/** Check explicit required keys after normalization, before host execution. */
|
|
14
|
+
export declare function missingRequiredToolArguments(tool: OpencodeToolDef | undefined, args: Record<string, unknown>): string[];
|
|
15
|
+
/** Canonical file/search tools must not silently ignore a misplaced shell command. */
|
|
16
|
+
export declare function misplacedShellCommand(tool: OpencodeToolDef | undefined, args: Record<string, unknown>): boolean;
|
|
11
17
|
/** Host file-tool argument key. OpenCode 1.x uses `filePath`; 2.0 uses `path`. */
|
|
12
18
|
export type HostFilePathKey = "path" | "filePath";
|
|
13
19
|
/** Host shell tool id. OpenCode 1.x uses `bash`; 2.0 uses `shell`. */
|
|
@@ -18,6 +24,7 @@ export type HostToolDialect = {
|
|
|
18
24
|
filePathKey: HostFilePathKey;
|
|
19
25
|
shellTool: HostShellTool;
|
|
20
26
|
skillArgKey: HostSkillArgKey;
|
|
27
|
+
shellDescription?: "optional" | "required";
|
|
21
28
|
};
|
|
22
29
|
export declare const OPENCODE_1_TOOL_DIALECT: HostToolDialect;
|
|
23
30
|
export declare const OPENCODE_2_TOOL_DIALECT: HostToolDialect;
|
|
@@ -118,6 +125,30 @@ export declare function resolveCursorSubagentType(subagentType: string, catalog?
|
|
|
118
125
|
export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string;
|
|
119
126
|
/** Resolve a native Cursor subagent request to this turn's executor and catalog. */
|
|
120
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;
|
|
121
152
|
/**
|
|
122
153
|
* Complete the private read inside Cursor's legacy edit transaction without
|
|
123
154
|
* routing it through OpenCode's user-facing `read` tool. The latter caps every
|
|
@@ -128,6 +159,28 @@ export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest,
|
|
|
128
159
|
* file named by an active `edit_tool_call` and is contained by the workspace.
|
|
129
160
|
*/
|
|
130
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;
|
|
131
184
|
/**
|
|
132
185
|
* Preserve a legacy Cursor edit's intent when its internal executor follows
|
|
133
186
|
* `edit_tool_call` with a whole-file `write_args` request.
|
|
@@ -137,19 +190,19 @@ export declare function buildCompleteEditReadMessages(execId: number, sourcePath
|
|
|
137
190
|
*/
|
|
138
191
|
export declare function remapCorrelatedEditWriteForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, editPath: string, workspaceRoot?: string): boolean;
|
|
139
192
|
/**
|
|
140
|
-
* Express a native Cursor write/edit as
|
|
193
|
+
* Express a native Cursor write/edit as a patch call when the host swapped the
|
|
141
194
|
* edit-tool family out from under us.
|
|
142
195
|
*
|
|
143
|
-
* OpenCode
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* using its native write/edit
|
|
147
|
-
*
|
|
148
|
-
*
|
|
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.
|
|
149
202
|
*
|
|
150
203
|
* Keyed purely off the advertised set: this is inert whenever the host offers
|
|
151
204
|
* `write`/`edit` normally, and equally inert when it offers neither those nor
|
|
152
|
-
*
|
|
205
|
+
* a patch tool (the caller's unavailable-tool rejection still applies).
|
|
153
206
|
*/
|
|
154
207
|
export declare function remapEditToolsForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, workspaceRoot?: string): void;
|
|
155
208
|
export type ParsedExecRequest = {
|
|
@@ -262,6 +315,8 @@ export type ToolResultInput = {
|
|
|
262
315
|
resultField: string;
|
|
263
316
|
output: string;
|
|
264
317
|
error?: string;
|
|
318
|
+
refusal?: "permission_denied" | "rejected";
|
|
319
|
+
interrupted?: boolean;
|
|
265
320
|
executionTimeMs?: number;
|
|
266
321
|
/**
|
|
267
322
|
* Resolved opencode tool name (read/write/grep/…). Gates read-envelope
|
|
@@ -280,7 +335,16 @@ export type ToolResultInput = {
|
|
|
280
335
|
* often $HOME and would re-advertise the wrong folder to the model.
|
|
281
336
|
*/
|
|
282
337
|
workspaceRoot?: string;
|
|
338
|
+
/** Images the host tool returned; see `execResultImages` for which results carry them. */
|
|
339
|
+
images?: readonly CursorImageInput[];
|
|
283
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* The leading tool-result images an exec result can carry on a held Run.
|
|
343
|
+
* Cursor's own read executor answers an image file with its bytes in
|
|
344
|
+
* `ReadSuccess.data` (#5) and no text; its MCP executor appends each MCP image
|
|
345
|
+
* as an `McpImageContent` item (#2). Other result shapes have no image field.
|
|
346
|
+
*/
|
|
347
|
+
export declare function execResultImages(resultField: string, images: readonly CursorImageInput[] | undefined): CursorImageInput[];
|
|
284
348
|
/**
|
|
285
349
|
* Build one or more ExecClientMessage frames for a tool result.
|
|
286
350
|
* Shell replies are a sequence of ShellStream oneofs under the same id —
|
|
@@ -289,6 +353,16 @@ export type ToolResultInput = {
|
|
|
289
353
|
* shell execs hang on heartbeats forever).
|
|
290
354
|
*/
|
|
291
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;
|
|
292
366
|
/** ACM #5 exec_client_control_message { stream_close { id } }. */
|
|
293
367
|
export declare function buildExecStreamClose(execId: number): Uint8Array;
|
|
294
368
|
export declare function buildUnsupportedExecDeny(input: {
|
|
@@ -316,12 +390,19 @@ export declare function buildUnsupportedExecDeny(input: {
|
|
|
316
390
|
* alone is not a tool-identity check.
|
|
317
391
|
*/
|
|
318
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;
|
|
319
400
|
/**
|
|
320
401
|
* Map OpenCode tool text into the agent.v1 result oneof for each exec variant.
|
|
321
402
|
* OpenCode returns free-form text; we wrap it in the minimal success shape the
|
|
322
403
|
* server accepts (verified against agent.v1 wire captures).
|
|
323
404
|
*/
|
|
324
|
-
export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome, workspaceRoot?: string): Record<string, unknown>;
|
|
405
|
+
export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome, workspaceRoot?: string, images?: readonly CursorImageInput[]): Record<string, unknown>;
|
|
325
406
|
export declare function buildToolCallPart(execMsg: ParsedExecRequest, sessionId: string): {
|
|
326
407
|
toolCallId: string;
|
|
327
408
|
toolName: string;
|