cursor-opencode-provider 0.4.3 → 0.6.0

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 (71) hide show
  1. package/README.md +33 -23
  2. package/dist/context/agents.d.ts +1 -1
  3. package/dist/context/agents.js +1 -1
  4. package/dist/context/auth-store.js +2 -2
  5. package/dist/context/build.d.ts +11 -1
  6. package/dist/context/build.js +76 -25
  7. package/dist/context/frozen.d.ts +29 -0
  8. package/dist/context/frozen.js +168 -0
  9. package/dist/context/index.d.ts +3 -2
  10. package/dist/context/index.js +3 -2
  11. package/dist/context/paths.d.ts +34 -53
  12. package/dist/context/paths.js +54 -118
  13. package/dist/context/rules.d.ts +1 -0
  14. package/dist/context/rules.js +2 -2
  15. package/dist/image-save-tool.d.ts +19 -0
  16. package/dist/image-save-tool.js +25 -0
  17. package/dist/image-save.d.ts +58 -0
  18. package/dist/image-save.js +189 -0
  19. package/dist/image-staging.d.ts +59 -0
  20. package/dist/image-staging.js +91 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/language-model.d.ts +13 -11
  23. package/dist/language-model.js +957 -150
  24. package/dist/plan-execution-kickoff.d.ts +55 -0
  25. package/dist/plan-execution-kickoff.js +162 -0
  26. package/dist/plugin-opencode2.js +52 -3
  27. package/dist/plugin-v2.d.ts +11 -9
  28. package/dist/plugin-v2.js +6 -13
  29. package/dist/plugin.d.ts +9 -0
  30. package/dist/plugin.js +61 -5
  31. package/dist/pricing-data.d.ts +16 -4
  32. package/dist/pricing-data.js +16 -4
  33. package/dist/protocol/ask-question.d.ts +133 -0
  34. package/dist/protocol/ask-question.js +320 -0
  35. package/dist/protocol/blob-reachability.d.ts +15 -0
  36. package/dist/protocol/blob-reachability.js +140 -0
  37. package/dist/protocol/blob-store.d.ts +37 -0
  38. package/dist/protocol/blob-store.js +115 -0
  39. package/dist/protocol/conversation-bind.d.ts +13 -0
  40. package/dist/protocol/conversation-bind.js +27 -1
  41. package/dist/protocol/conversation-persistence.d.ts +32 -0
  42. package/dist/protocol/conversation-persistence.js +504 -0
  43. package/dist/protocol/conversation-state.d.ts +16 -0
  44. package/dist/protocol/conversation-state.js +62 -0
  45. package/dist/protocol/create-plan.d.ts +141 -0
  46. package/dist/protocol/create-plan.js +373 -0
  47. package/dist/protocol/generate-image.d.ts +54 -0
  48. package/dist/protocol/generate-image.js +101 -0
  49. package/dist/protocol/interactions.d.ts +110 -8
  50. package/dist/protocol/interactions.js +295 -35
  51. package/dist/protocol/kv.d.ts +1 -0
  52. package/dist/protocol/kv.js +2 -0
  53. package/dist/protocol/messages.d.ts +2 -0
  54. package/dist/protocol/messages.js +148 -8
  55. package/dist/protocol/request.d.ts +2 -0
  56. package/dist/protocol/request.js +2 -0
  57. package/dist/protocol/switch-mode.d.ts +178 -0
  58. package/dist/protocol/switch-mode.js +417 -0
  59. package/dist/protocol/token-details.d.ts +33 -0
  60. package/dist/protocol/token-details.js +155 -0
  61. package/dist/protocol/tool-call-bridge.js +10 -5
  62. package/dist/protocol/tools.d.ts +44 -11
  63. package/dist/protocol/tools.js +171 -41
  64. package/dist/session.d.ts +39 -11
  65. package/dist/shared.d.ts +3 -0
  66. package/dist/shared.js +3 -0
  67. package/dist/usage.d.ts +42 -37
  68. package/dist/usage.js +201 -70
  69. package/dist/web-search-tool.d.ts +22 -26
  70. package/dist/web-search-tool.js +25 -15
  71. package/package.json +7 -4
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Cursor-native SwitchMode ⇄ OpenCode `plan_enter` / `plan_exit`.
3
+ *
4
+ * Cursor raises `switch_mode_request_query` (#4) with SwitchModeArgs and blocks
5
+ * until the client replies approved{} or rejected{reason} (no async variant).
6
+ * Cursor CLI prompts the user, then flips unifiedMode and injects a
7
+ * `<system_reminder>` describing how to behave in that mode.
8
+ * Display `switch_mode_tool_call` (#25) is a separate transcript record and is
9
+ * not replayed as a host tool.
10
+ *
11
+ * Host mapping (advertisement-gated):
12
+ * - plan, spec → plan_enter
13
+ * - every other non-empty target → plan_exit (leave plan for OpenCode build),
14
+ * then inject the Cursor CLI-shaped mode reminder for that target
15
+ *
16
+ * Upstream OpenCode's plan tools are not mode flags. Each one asks the user
17
+ * through the Question service and, on "Yes", injects a synthetic user message
18
+ * carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a
19
+ * `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
20
+ * `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
21
+ * observable halves — the approval gate and the behavioural contract — are both
22
+ * reachable, so the bridge degrades instead of refusing when the host tool is
23
+ * missing (`plan_enter` is commented out upstream and `plan_exit` is gated
24
+ * behind OPENCODE_EXPERIMENTAL_PLAN_MODE + CLI client):
25
+ *
26
+ * - entering plan/spec needs no host tool at all — approve immediately and let
27
+ * the injected <system_reminder> carry the contract, exactly as Cursor CLI's
28
+ * own plan mode is prompt-enforced.
29
+ * - leaving plan mode is the gate the user must actually see, so it falls back
30
+ * to the host `question` tool and only rejects when that is absent too.
31
+ *
32
+ * Resolution is keyed solely on the advertised catalog under canonical tool
33
+ * names — never on host identity or model id. A compatibility layer may restate
34
+ * alternate host vocabulary under canonical names before the provider sees the
35
+ * catalog, so the bridge itself remains OpenCode-shaped.
36
+ *
37
+ * CLI reject reason for user declines: "Mode switch rejected by user"
38
+ * (chunk-7076/dist/ui.js onSwitchModeReject).
39
+ */
40
+ import { parseAnswerSegments, } from "./ask-question.js";
41
+ import { decodeMessageSparse } from "./messages.js";
42
+ /**
43
+ * `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
44
+ * Continuation writes an InteractionResponse, not an ExecClientMessage.
45
+ */
46
+ export const SWITCH_MODE_RESULT_FIELD = "switch_mode_request_response";
47
+ /** CLI verbatim when the user rejects the mode switch. */
48
+ export const USER_REJECTED_REASON = "Mode switch rejected by user";
49
+ export const MISSING_QUERY_REASON = "Missing switch-mode query";
50
+ export const MISSING_ARGS_REASON = "Missing switch-mode arguments";
51
+ export const MISSING_TARGET_REASON = "Missing targetModeId";
52
+ /**
53
+ * A turn that cannot call host tools is a lifecycle turn (title generation,
54
+ * compaction, summarization). OpenCode opens one alongside the real Run, and it
55
+ * must not mutate session state — approving there flips plan mode twice and
56
+ * lets the throwaway turn write its own plan file.
57
+ *
58
+ * Soft-ack with `approved{}` instead of `rejected{reason}`: a hard reject lands
59
+ * in the transcript and the real turn narrates "mode switches are blocked"
60
+ * even though the next Run will succeed. CreatePlan already soft-acks the same
61
+ * case; SwitchMode must match.
62
+ */
63
+ export const PLAN_EXIT_UNAVAILABLE_REASON = "Neither the OpenCode `plan_exit` tool nor the `question` tool is available to the " +
64
+ "current agent, so leaving plan mode cannot be approved this turn.";
65
+ /** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */
66
+ export const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?";
67
+ export const SWITCH_MODE_EXIT_HEADER = "Build Agent";
68
+ const SWITCH_MODE_EXIT_YES = "Yes";
69
+ const SWITCH_MODE_EXIT_NO = "No";
70
+ const activeCursorModeBySession = new Map();
71
+ function asRecord(value) {
72
+ return value && typeof value === "object" && !Array.isArray(value)
73
+ ? value
74
+ : undefined;
75
+ }
76
+ function str(value) {
77
+ return typeof value === "string" ? value : "";
78
+ }
79
+ /** Normalize Cursor mode ids for comparison. */
80
+ export function normalizeSwitchModeId(targetModeId) {
81
+ return targetModeId.trim().toLowerCase();
82
+ }
83
+ /**
84
+ * Map a Cursor unified-mode id onto an OpenCode plan enter/exit tool.
85
+ * Does not check advertisement — callers gate on the host catalog.
86
+ *
87
+ * plan/spec enter plan; every other non-empty target leaves plan via plan_exit
88
+ * (host-portable primary switch) and relies on {@link cursorModeSystemReminder}
89
+ * for CLI-shaped behavioral guidance.
90
+ */
91
+ export function mapSwitchModeTarget(targetModeId) {
92
+ const id = normalizeSwitchModeId(targetModeId);
93
+ if (!id)
94
+ return { ok: false, reason: MISSING_TARGET_REASON };
95
+ if (id === "plan" || id === "spec") {
96
+ return { ok: true, toolName: "plan_enter" };
97
+ }
98
+ return { ok: true, toolName: "plan_exit" };
99
+ }
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
+ /**
134
+ * Resolve how this SwitchMode target is satisfied, keyed only on the advertised
135
+ * catalog under canonical tool names.
136
+ *
137
+ * A missing plan tool is not a refusal: entering plan mode needs no host tool,
138
+ * and leaving it degrades to the `question` prompt so the user still approves
139
+ * before any execution starts.
140
+ */
141
+ export function resolveSwitchModeBridge(targetModeId, options) {
142
+ const mapped = mapSwitchModeTarget(targetModeId);
143
+ if (!mapped.ok)
144
+ return { kind: "reject", reason: mapped.reason };
145
+ // Lifecycle turn: soft-ack on the wire, no session mutation. A hard reject
146
+ // would enter the transcript and make the real turn narrate a blocked switch.
147
+ if (!options.allowTools)
148
+ return { kind: "ack" };
149
+ // Already in the target mode: nothing to approve. Cursor's own IDE handler
150
+ // auto-approves here too, and without this a model that asks to leave plan
151
+ // mode after the plan write already prompted would ask the user twice.
152
+ if (options.activeModeId
153
+ && normalizeSwitchModeId(options.activeModeId) === normalizeSwitchModeId(targetModeId)) {
154
+ return { kind: "approve" };
155
+ }
156
+ const names = options.advertised instanceof Set
157
+ ? options.advertised
158
+ : new Set(options.advertised);
159
+ if (names.has(mapped.toolName))
160
+ return { kind: "native", toolName: mapped.toolName };
161
+ // Entering plan mode is provider-owned: the injected <system_reminder> is the
162
+ // whole contract, exactly as Cursor CLI's own plan mode is prompt-enforced.
163
+ if (mapped.toolName === "plan_enter")
164
+ return { kind: "approve" };
165
+ // Leaving plan mode starts real work, so it keeps a user-visible gate.
166
+ if (names.has("question")) {
167
+ return { kind: "question", input: switchModeExitQuestionInput() };
168
+ }
169
+ return { kind: "reject", reason: PLAN_EXIT_UNAVAILABLE_REASON };
170
+ }
171
+ /** Decode a `switch_mode_request_query` body, or undefined when unusable. */
172
+ export function decodeSwitchModeQuery(queryBytes) {
173
+ let decoded;
174
+ try {
175
+ decoded = decodeMessageSparse("SwitchModeRequestQuery", queryBytes);
176
+ }
177
+ catch {
178
+ return undefined;
179
+ }
180
+ const argsRecord = asRecord(decoded.args);
181
+ if (!argsRecord)
182
+ return undefined;
183
+ const targetModeId = str(argsRecord.target_mode_id);
184
+ if (!targetModeId.trim())
185
+ return undefined;
186
+ const toolCallId = str(argsRecord.tool_call_id) || str(decoded.tool_call_id);
187
+ return {
188
+ args: {
189
+ targetModeId,
190
+ explanation: str(argsRecord.explanation),
191
+ toolCallId,
192
+ },
193
+ toolCallId,
194
+ };
195
+ }
196
+ /** OpenCode plan_enter / plan_exit advertise empty input. */
197
+ export function switchModeToolInput() {
198
+ return {};
199
+ }
200
+ /** Shape `approved{}` / `rejected{reason}` for encodeMessage. */
201
+ export function switchModeApprovedResult() {
202
+ return { approved: {} };
203
+ }
204
+ export function switchModeRejectedResult(reason) {
205
+ return { rejected: { reason } };
206
+ }
207
+ /**
208
+ * Host tool outcome → Cursor SwitchModeRequestResponse.
209
+ * Question/permission declines and empty failures use the CLI user-reject string.
210
+ */
211
+ export function switchModeResultFromToolOutput(output, isError) {
212
+ if (!isError)
213
+ return switchModeApprovedResult();
214
+ const trimmed = output.trim();
215
+ // OpenCode plan tools throw Question.RejectedError on "No" — treat as user reject.
216
+ if (!trimmed
217
+ || /reject/i.test(trimmed)
218
+ || /denied/i.test(trimmed)
219
+ || /permission/i.test(trimmed)
220
+ || /cancelled|canceled|dismissed/i.test(trimmed)) {
221
+ return switchModeRejectedResult(USER_REJECTED_REASON);
222
+ }
223
+ return switchModeRejectedResult(trimmed);
224
+ }
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
+ /** Record the approved Cursor mode for later system-reminder injection. */
240
+ export function setActiveCursorMode(sessionKey, targetModeId, options = {}) {
241
+ if (!sessionKey)
242
+ return;
243
+ const modeId = normalizeSwitchModeId(targetModeId);
244
+ if (!modeId)
245
+ return;
246
+ activeCursorModeBySession.set(sessionKey, {
247
+ modeId,
248
+ firstTurn: true,
249
+ bridgedPlanEntered: options.bridgedPlanEntered === true,
250
+ });
251
+ }
252
+ export function getActiveCursorMode(sessionKey) {
253
+ if (!sessionKey)
254
+ return undefined;
255
+ return activeCursorModeBySession.get(sessionKey)?.modeId;
256
+ }
257
+ /** True while the provider has an approved Cursor plan/spec mode for this session. */
258
+ export function isCursorPlanModeActive(sessionKey) {
259
+ const mode = getActiveCursorMode(sessionKey);
260
+ return mode === "plan" || mode === "spec";
261
+ }
262
+ /** True after a bridged plan/spec Run has observed plan_enter removed by the host. */
263
+ export function isBridgedCursorPlanModeActive(sessionKey) {
264
+ if (!sessionKey)
265
+ return false;
266
+ const state = activeCursorModeBySession.get(sessionKey);
267
+ return (state?.modeId === "plan" || state?.modeId === "spec") && state.bridgedPlanEntered;
268
+ }
269
+ export function clearActiveCursorMode(sessionKey) {
270
+ if (!sessionKey)
271
+ return;
272
+ activeCursorModeBySession.delete(sessionKey);
273
+ }
274
+ export function resetActiveCursorModesForTests() {
275
+ activeCursorModeBySession.clear();
276
+ }
277
+ function wrapReminder(body) {
278
+ return `<system_reminder>\n${body.trim()}\n</system_reminder>`;
279
+ }
280
+ /**
281
+ * Cursor CLI-shaped mode reminder for the active unified mode.
282
+ * Tool names are adapted to OpenCode (`task`, `question`) where the CLI names
283
+ * Cursor-only tools; the behavioral contract matches the CLI reminders.
284
+ */
285
+ export function cursorModeSystemReminder(targetModeId, options = {}) {
286
+ const id = normalizeSwitchModeId(targetModeId);
287
+ if (!id)
288
+ return undefined;
289
+ const first = options.firstTurn !== false;
290
+ // Without a host plan_exit, recording the plan is itself the gate: the
291
+ // provider persists it and then asks the user whether to start implementing.
292
+ // Naming an unavailable tool would strand the model.
293
+ const leavePlan = options.planExitAdvertised === false
294
+ ? "record the finished plan (Cursor CreatePlan). Writing it needs no approval, and the user is then asked whether to start implementing; if they decline, refine the plan and record it again"
295
+ : "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode";
296
+ if (id === "plan" || id === "spec") {
297
+ return wrapReminder(first
298
+ ? `Plan mode is active. The user does not want execution yet -- you MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction.
299
+
300
+ 1. Research enough to make an accurate plan.
301
+ 2. Before finishing, resolve decisions that would materially change the implementation path, touched files, architecture, user-visible behavior, data model, or validation strategy. If investigation cannot resolve one, ask clarifying questions in small batches (use the OpenCode \`question\` tool when available).
302
+ 3. Do not put choices in the plan for the user to resolve. The plan must present one recommended approach, not unresolved questions or "choose A or B" options.
303
+ 4. When ready, ${leavePlan}.
304
+ 5. Do not execute the plan until the user confirms it.`
305
+ : `Plan mode is still active. You MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction.`);
306
+ }
307
+ if (id === "chat") {
308
+ return wrapReminder(first
309
+ ? `Ask mode is active. The user wants you to answer questions about their codebase or coding in general. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
310
+
311
+ Your role in Ask mode:
312
+
313
+ 1. Answer the user's questions comprehensively and accurately. Focus on providing clear, detailed explanations.
314
+ 2. Use readonly tools to explore the codebase and gather information needed to answer the user's questions.
315
+ 3. Provide code examples and references when helpful, citing specific file paths and line numbers.
316
+ 4. If you need more information, ask the user for clarification (OpenCode \`question\` when available).
317
+ 5. You may provide suggestions about how to implement something, but you MUST NOT actually implement it yourself.
318
+ 6. If the user asks you to make changes or implement something, politely remind them that you're in Ask mode and can only provide information and guidance. Suggest they switch to Agent mode (OpenCode \`plan_exit\` / SwitchMode target agent) if they want you to make changes.`
319
+ : `Ask mode is still active. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).`);
320
+ }
321
+ if (id === "debug") {
322
+ return wrapReminder(first
323
+ ? `You are now in **DEBUG MODE**. You must debug with **runtime evidence**.
324
+
325
+ **Why this approach:** Traditional AI agents jump to fixes claiming high confidence, but fail due to lacking runtime information. You cannot and must NOT fix bugs from code-only guesses — you need actual runtime data.
326
+
327
+ **Your systematic workflow:**
328
+ 1. Generate 3-5 precise hypotheses about WHY the bug occurs.
329
+ 2. Instrument code with logs to test hypotheses in parallel.
330
+ 3. Ask the user to reproduce the bug. Conclude with a <reproduction_steps>...</reproduction_steps> block when the user must run something (mandatory unless the issue is fully confirmed fixed).
331
+ 4. Analyze logs, accept/reject hypotheses, and only then implement a fix.
332
+ 5. Verify the fix with runtime evidence before claiming success.
333
+
334
+ Prefer reproduction, runtime logs, and end-to-end verification over speculative refactors.`
335
+ : `Debug mode is still active. You must debug with **runtime evidence**.
336
+
337
+ **During fixes:** Do NOT remove instrumentation until post-fix verification logs prove success or the user explicitly asks you to remove it.
338
+ **Testing:** Prefer reproduction, runtime logs, and end-to-end verification; run tests when they directly exercise a hypothesis or confirm the final fix.
339
+ **Reproduction steps (MANDATORY):** Unless the issue is fully confirmed fixed, conclude with a <reproduction_steps>...</reproduction_steps> block when the user must reproduce.
340
+ **If fix failed:** Generate NEW hypotheses from different subsystems and add more instrumentation.`);
341
+ }
342
+ if (id === "multitask") {
343
+ return wrapReminder(`Multitask Mode is active. You are a coordinator who pushes meaningful work to asynchronous workers through the OpenCode \`task\` tool (prefer background=true when the host supports it).
344
+
345
+ - For most non-trivial requests, launch or resume one coherent worker and let that worker handle the investigation/implementation.
346
+ - After delegating the only coherent worker task, do not redo the same work in the foreground. Only coordinate, answer a new independent question, or synthesize after multiple workers return.
347
+ - NEVER await or sleep while waiting for a running subagent — end your response; you will be notified when it completes.
348
+ - Do NOT aggressively decompose small or medium tasks into many sibling agents. Multitask Mode is about moving substantial work out of the foreground, not maximizing parallel agents.
349
+ - For trivial requests (zero or one tool call), fulfill them directly and disregard these Multitask instructions.`);
350
+ }
351
+ if (id === "triage") {
352
+ return wrapReminder(first
353
+ ? `Triage mode is active. Your job is to coordinate long-horizon, multi-step work by delegating to subagents and integrating their progress.
354
+
355
+ 1. Break the user's task into well-scoped subtasks and launch subagents with the OpenCode \`task\` tool. Provide clear objectives and context so each subagent can make measurable progress.
356
+ 2. Routinely inspect subagent output and synthesize results.
357
+ 3. Decide next steps and iterate: launch additional agents, request revisions, or merge work when ready.
358
+ 4. Throughout triage mode, maintain a global plan, document your decisions, and ensure the combined work moves the user toward their goal.`
359
+ : `Triage mode is still active. You must continue to coordinate long-horizon, multi-step work by delegating to subagents and integrating their progress.`);
360
+ }
361
+ if (id === "project") {
362
+ return wrapReminder(`You are Project Agent Mode: a long-running, high-level planner and orchestrator for complex software projects.
363
+
364
+ Your mandate is to convert user intent into a correct, high-quality implementation by delegating nearly all work to subagents and coordinating them safely over long horizons.
365
+
366
+ ## Non-Negotiable Rules
367
+ 1) Orchestrate, don't execute — you are NOT an implementer. Workspace modifications MUST be performed by OpenCode \`task\` subagents.
368
+ 2) Task-first for everything — default to spawning subagents for research, exploration, implementation, validation, and review. Use your own read-only tools only for quick triage and synthesizing plans.
369
+ 3) No work without clarity — if ambiguity remains, stop and ask clarifying questions (OpenCode \`question\` when available).
370
+ 4) Phase-gated workflow — Clarify → Research → Plan → User Review → Implement → Review → Iterate → Finalize. Do not skip phases.
371
+ 5) Externalize state — assume chat context may be condensed; keep durable progress notes so work can resume from written artifacts.`);
372
+ }
373
+ if (id === "background") {
374
+ return wrapReminder(`Cloud / background mode intent is active. Prefer long-running and environment/browser automation work via OpenCode \`task\` subagents (background=true when advertised) rather than blocking the foreground turn.
375
+
376
+ - Useful when the task needs browser automation, multi-service environments, or should keep running if the local session is interrupted.
377
+ - After launching background work, end your response promptly; do not busy-wait.
378
+ - For short local unit/lint/typecheck work, stay in ordinary agent execution instead.`);
379
+ }
380
+ // agent / build / edit / unknown leave-plan targets: implementation mode.
381
+ return wrapReminder(first
382
+ ? `Agent mode is active (OpenCode build / agents). You have left plan mode and may implement: edit files, run tools, and execute the agreed approach.
383
+
384
+ - Prefer making progress with the advertised host tools.
385
+ - If the task is large or ambiguous again, switch back to plan mode (OpenCode \`plan_enter\` / SwitchMode target plan) before a large rewrite.`
386
+ : `Agent mode is still active. Continue implementing with the advertised host tools. Switch back to plan mode only when a new large/ambiguous design decision appears.`);
387
+ }
388
+ /**
389
+ * Consume the active-mode reminder for this OpenCode session.
390
+ * Marks the mode as no longer first-turn after the first successful read.
391
+ */
392
+ export function takeActiveCursorModeReminder(sessionKey, options = {}) {
393
+ if (!sessionKey)
394
+ return undefined;
395
+ const state = activeCursorModeBySession.get(sessionKey);
396
+ if (!state)
397
+ return undefined;
398
+ const advertised = options.advertisedTools
399
+ ? options.advertisedTools instanceof Set
400
+ ? options.advertisedTools
401
+ : new Set(options.advertisedTools)
402
+ : undefined;
403
+ const isPlanMode = state.modeId === "plan" || state.modeId === "spec";
404
+ if (isPlanMode && state.bridgedPlanEntered && advertised) {
405
+ if (advertised.has("plan_enter")) {
406
+ activeCursorModeBySession.delete(sessionKey);
407
+ return cursorModeSystemReminder("agent", { firstTurn: true });
408
+ }
409
+ }
410
+ const reminder = cursorModeSystemReminder(state.modeId, {
411
+ firstTurn: state.firstTurn,
412
+ ...(advertised ? { planExitAdvertised: advertised.has("plan_exit") } : {}),
413
+ });
414
+ if (state.firstTurn)
415
+ state.firstTurn = false;
416
+ return reminder;
417
+ }
@@ -0,0 +1,33 @@
1
+ export type CursorPromptTokenCategory = {
2
+ id: string;
3
+ label: string;
4
+ estimatedTokens: number;
5
+ characterCount?: number;
6
+ };
7
+ export type CursorPromptTokenBreakdown = {
8
+ totalUsedTokens: number;
9
+ maxTokens: number;
10
+ categories: CursorPromptTokenCategory[];
11
+ };
12
+ export type CursorConversationTokenDetails = {
13
+ usedTokens: number;
14
+ maxTokens: number;
15
+ breakdown?: CursorPromptTokenBreakdown;
16
+ };
17
+ export type CursorContextUsageSource = "checkpoint-current-run" | "checkpoint-previous-turn";
18
+ /**
19
+ * Read field #5 (`ConversationTokenDetails`) from Cursor's opaque
20
+ * `ConversationStateStructure` checkpoint without decoding or re-encoding the
21
+ * rest of the state. Unknown checkpoint fields therefore remain byte-exact.
22
+ */
23
+ export declare function decodeConversationTokenDetails(checkpoint: Uint8Array | undefined): CursorConversationTokenDetails | undefined;
24
+ export declare function cursorContextUsageMetadata(details: CursorConversationTokenDetails, source?: CursorContextUsageSource): {
25
+ contextUsageVersion: number;
26
+ source: CursorContextUsageSource;
27
+ stale: boolean;
28
+ usedTokens: number;
29
+ maxTokens: number;
30
+ remainingTokens: number;
31
+ usedPercent?: number;
32
+ breakdown?: CursorPromptTokenBreakdown;
33
+ };
@@ -0,0 +1,155 @@
1
+ import protobuf from "protobufjs";
2
+ const MAX_BREAKDOWN_CATEGORIES = 128;
3
+ function requireWireType(actual, expected, field) {
4
+ if (actual !== expected)
5
+ throw new Error(`invalid ${field} wire type`);
6
+ }
7
+ function decodeCategory(data) {
8
+ const reader = protobuf.Reader.create(data);
9
+ let id = "";
10
+ let label = "";
11
+ let estimatedTokens = 0;
12
+ let characterCount;
13
+ while (reader.pos < reader.len) {
14
+ const tag = reader.uint32();
15
+ const wireType = tag & 7;
16
+ switch (tag >>> 3) {
17
+ case 1:
18
+ requireWireType(wireType, 2, "category id");
19
+ id = reader.string();
20
+ break;
21
+ case 2:
22
+ requireWireType(wireType, 2, "category label");
23
+ label = reader.string();
24
+ break;
25
+ case 3:
26
+ requireWireType(wireType, 0, "category token count");
27
+ estimatedTokens = reader.uint32();
28
+ break;
29
+ case 4:
30
+ requireWireType(wireType, 0, "category character count");
31
+ characterCount = reader.uint32();
32
+ break;
33
+ default:
34
+ reader.skipType(wireType);
35
+ }
36
+ }
37
+ return {
38
+ id,
39
+ label,
40
+ estimatedTokens,
41
+ ...(characterCount === undefined ? {} : { characterCount }),
42
+ };
43
+ }
44
+ function decodeBreakdown(data) {
45
+ const reader = protobuf.Reader.create(data);
46
+ let totalUsedTokens = 0;
47
+ let maxTokens = 0;
48
+ const categories = [];
49
+ while (reader.pos < reader.len) {
50
+ const tag = reader.uint32();
51
+ const wireType = tag & 7;
52
+ switch (tag >>> 3) {
53
+ case 1:
54
+ requireWireType(wireType, 0, "breakdown used tokens");
55
+ totalUsedTokens = reader.uint32();
56
+ break;
57
+ case 2:
58
+ requireWireType(wireType, 0, "breakdown max tokens");
59
+ maxTokens = reader.uint32();
60
+ break;
61
+ case 3:
62
+ requireWireType(wireType, 2, "breakdown category");
63
+ if (categories.length < MAX_BREAKDOWN_CATEGORIES) {
64
+ categories.push(decodeCategory(reader.bytes()));
65
+ }
66
+ else {
67
+ reader.bytes();
68
+ }
69
+ break;
70
+ default:
71
+ reader.skipType(wireType);
72
+ }
73
+ }
74
+ return { totalUsedTokens, maxTokens, categories };
75
+ }
76
+ function decodeTokenDetails(data) {
77
+ const reader = protobuf.Reader.create(data);
78
+ let usedTokens = 0;
79
+ let maxTokens = 0;
80
+ let sawUsedTokens = false;
81
+ let sawMaxTokens = false;
82
+ let breakdown;
83
+ while (reader.pos < reader.len) {
84
+ const tag = reader.uint32();
85
+ const wireType = tag & 7;
86
+ switch (tag >>> 3) {
87
+ case 1:
88
+ requireWireType(wireType, 0, "used tokens");
89
+ usedTokens = reader.uint32();
90
+ sawUsedTokens = true;
91
+ break;
92
+ case 2:
93
+ requireWireType(wireType, 0, "max tokens");
94
+ maxTokens = reader.uint32();
95
+ sawMaxTokens = true;
96
+ break;
97
+ case 3:
98
+ requireWireType(wireType, 2, "token breakdown");
99
+ breakdown = decodeBreakdown(reader.bytes());
100
+ break;
101
+ default:
102
+ reader.skipType(wireType);
103
+ }
104
+ }
105
+ if (!sawUsedTokens && !sawMaxTokens && !breakdown)
106
+ return undefined;
107
+ return {
108
+ usedTokens,
109
+ maxTokens,
110
+ ...(breakdown ? { breakdown } : {}),
111
+ };
112
+ }
113
+ /**
114
+ * Read field #5 (`ConversationTokenDetails`) from Cursor's opaque
115
+ * `ConversationStateStructure` checkpoint without decoding or re-encoding the
116
+ * rest of the state. Unknown checkpoint fields therefore remain byte-exact.
117
+ */
118
+ export function decodeConversationTokenDetails(checkpoint) {
119
+ if (!checkpoint?.length)
120
+ return undefined;
121
+ try {
122
+ const reader = protobuf.Reader.create(checkpoint);
123
+ let details;
124
+ while (reader.pos < reader.len) {
125
+ const tag = reader.uint32();
126
+ const wireType = tag & 7;
127
+ if ((tag >>> 3) === 5) {
128
+ requireWireType(wireType, 2, "conversation token details");
129
+ details = decodeTokenDetails(reader.bytes());
130
+ }
131
+ else {
132
+ reader.skipType(wireType);
133
+ }
134
+ }
135
+ return details;
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ }
141
+ export function cursorContextUsageMetadata(details, source = "checkpoint-current-run") {
142
+ const usedPercent = details.maxTokens > 0
143
+ ? Math.max(0, Math.min(100, Math.round(details.usedTokens / details.maxTokens * 1_000) / 10))
144
+ : undefined;
145
+ return {
146
+ contextUsageVersion: 2,
147
+ source,
148
+ stale: source === "checkpoint-previous-turn",
149
+ usedTokens: details.usedTokens,
150
+ maxTokens: details.maxTokens,
151
+ remainingTokens: Math.max(0, details.maxTokens - details.usedTokens),
152
+ ...(usedPercent === undefined ? {} : { usedPercent }),
153
+ ...(details.breakdown ? { breakdown: details.breakdown } : {}),
154
+ };
155
+ }
@@ -1,5 +1,6 @@
1
1
  import { mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName } from "./tools.js";
2
2
  import { decodeStructEntriesToJson } from "./struct.js";
3
+ import { mapSwitchModeTarget } from "./switch-mode.js";
3
4
  const TODO_STATUS = {
4
5
  0: "pending",
5
6
  1: "pending",
@@ -127,8 +128,8 @@ export function parseDisplayToolCall(callId, toolCall) {
127
128
  };
128
129
  }
129
130
  // create_plan_tool_call here is display-state mirroring into todowrite.
130
- // Separately, InteractionQuery create_plan_request_query is auto-acked in
131
- // protocol/interactions.ts (F14 / CLI headless parity) — that ack is not an
131
+ // Separately, InteractionQuery create_plan_request_query writes a host plan
132
+ // file via protocol/create-plan.ts (hostPlansDir) — that write is not an
132
133
  // execution of this display payload.
133
134
  if (variant === "update_todos_tool_call" || variant === "create_plan_tool_call") {
134
135
  const result = asRecord(payload.result);
@@ -237,14 +238,18 @@ export function parseDisplayToolCall(callId, toolCall) {
237
238
  };
238
239
  }
239
240
  if (variant === "switch_mode_tool_call") {
240
- const target = typeof args.target_mode_id === "string" ? args.target_mode_id.toLowerCase() : "";
241
- const preferred = target.includes("plan") || target === "plan" ? "plan_enter" : "plan_exit";
241
+ // Display-path mirror of InteractionQuery #4. Same mapping as switch-mode.ts:
242
+ // plan/spec → plan_enter; every other non-empty target → plan_exit.
243
+ // Display completions stay non-replayed regardless of mapping.
244
+ const target = typeof args.target_mode_id === "string" ? args.target_mode_id : "";
245
+ const mapped = mapSwitchModeTarget(target);
242
246
  return {
243
247
  callId,
244
248
  variant,
245
- preferredToolName: preferred,
249
+ preferredToolName: mapped.ok ? mapped.toolName : "plan_exit",
246
250
  // OpenCode plan_enter / plan_exit both advertise an empty input schema.
247
251
  args: {},
252
+ bridgeable: mapped.ok,
248
253
  };
249
254
  }
250
255
  // F11: display-path delete → bash `rm -f` (OpenCode has no delete tool).