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
@@ -0,0 +1,66 @@
1
+ export const TOOL_RESULT_STATUS_KEY = "opencodeToolResult";
2
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
3
+ ? value : undefined;
4
+ export function nativeToolFailure(value, refusal) {
5
+ const envelope = record(value);
6
+ const error = record(envelope?.error);
7
+ const type = typeof error?.type === "string" && error.type ? error.type : "tool.execution";
8
+ const message = typeof error?.message === "string" && error.message
9
+ ? error.message : typeof value === "string" && value ? value : `OpenCode tool failed (${type})`;
10
+ const content = Array.isArray(envelope?.content) ? envelope.content.flatMap(item => {
11
+ const part = record(item);
12
+ return part?.type === "text" && typeof part.text === "string" && part.text ? [part.text] : [];
13
+ }).join("\n") : "";
14
+ return { type, text: content ? `${message}\n\n${content}` : message,
15
+ ...(refusal ? { refusal } : type === "permission.rejected" ? { refusal: "rejected" } : {}) };
16
+ }
17
+ /** The native tool hook preserves the underlying typed permission cause. */
18
+ export function nativeToolRefusal(error) {
19
+ const seen = new Set();
20
+ for (let depth = 0; depth < 16 && !seen.has(error); depth++) {
21
+ seen.add(error);
22
+ const cause = record(error);
23
+ if (!cause)
24
+ return undefined;
25
+ if (cause._tag === "Permission.BlockedError")
26
+ return "permission_denied";
27
+ if (cause._tag === "Permission.CorrectedError" || cause._tag === "Permission.DeclinedError")
28
+ return "rejected";
29
+ error = cause.error;
30
+ }
31
+ return undefined;
32
+ }
33
+ /** Metadata travels with each part, including history and independently loaded SDK copies. */
34
+ export function preserveNativeToolResults(messages, refusals = new Map()) {
35
+ if (!Array.isArray(messages))
36
+ return [];
37
+ return messages.map(message => {
38
+ const msg = record(message);
39
+ if (msg?.role !== "tool" || !Array.isArray(msg.content))
40
+ return message;
41
+ return { ...msg, content: msg.content.map(item => {
42
+ const part = record(item);
43
+ const result = record(part?.result);
44
+ if (part?.type !== "tool-result" || typeof part.id !== "string" || !result
45
+ || typeof result.type !== "string" || !["error", "text", "json", "content"].includes(result.type))
46
+ return item;
47
+ const metadata = record(part.providerMetadata);
48
+ const cursor = record(metadata?.cursor);
49
+ const status = result.type === "error"
50
+ ? { callID: part.id, status: "error", failure: nativeToolFailure(result.value, refusals.get(part.id)) }
51
+ : { callID: part.id, status: "completed" };
52
+ return { ...part, providerMetadata: { ...metadata, cursor: { ...cursor, [TOOL_RESULT_STATUS_KEY]: status } } };
53
+ }) };
54
+ });
55
+ }
56
+ export function toolResultFailure(options, callID) {
57
+ const status = record(record(record(options)?.cursor)?.[TOOL_RESULT_STATUS_KEY]);
58
+ if (status?.callID !== callID || status.status !== "error")
59
+ return undefined;
60
+ const failure = record(status.failure);
61
+ if (!failure || typeof failure.type !== "string" || typeof failure.text !== "string")
62
+ return undefined;
63
+ const refusal = failure.refusal === "permission_denied" || failure.refusal === "rejected" ? failure.refusal : undefined;
64
+ return { type: failure.type, text: failure.text || `OpenCode tool failed (${failure.type || "tool.execution"})`,
65
+ ...(refusal ? { refusal } : {}) };
66
+ }
@@ -3,9 +3,11 @@ import { createSdk, cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled, isC
3
3
  import { opencode2PlanDir, opencodeGlobalCacheDir, setNativePlansDir } from "./context/paths.js";
4
4
  import { discoverModels, isCacheFresh, readCache } from "./models.js";
5
5
  import { resolveAgentUrl } from "./agent-url.js";
6
- import { sessionActivity } from "./activity.js";
6
+ import { applyHostPromptEvent, applyHostToolEvent, sessionActivity } from "./activity.js";
7
+ import { notifyHostInterrupt } from "./host-interrupt.js";
8
+ import { announceHostSteer, forgetEarlySteerOfInbox, forgetEarlySteers, rememberQueuedSteer, takeQueuedSteer, } from "./host-steer.js";
7
9
  import { fetchOpenCodeWebSearchText, parseExaWebSearchResults, } from "./web-tools.js";
8
- import { captureCursorShellResult, cursorShellEnvForCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
10
+ import { armCursorShellCreate, captureCursorShellResult, captureCursorShellFailure, prepareCursorShellCreate, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
9
11
  import { applyCursorProviderInventory, CURSOR_INTEGRATION_ID } from "./opencode2/catalog.js";
10
12
  import { applyCursorIntegration, requireCursorAccessToken, resolveCursorAccessToken, } from "./opencode2/integration.js";
11
13
  import { exposeDirectMcpTools, rememberDirectMcpNamespaces } from "./opencode2/mcp-direct.js";
@@ -20,6 +22,7 @@ import { trace } from "./debug.js";
20
22
  import { cancelHostAgentModeSwitch, hostAgentSwitchPromptText, setHostAgentModeSwitch, } from "./host-agent-mode.js";
21
23
  import { clearActiveCursorMode, getActiveCursorMode, normalizeSwitchModeId, setActiveCursorMode, } from "./protocol/switch-mode.js";
22
24
  import { CursorPlugin } from "./plugin.js";
25
+ import { nativeToolRefusal, preserveNativeToolResults } from "./opencode2/tool-results.js";
23
26
  /**
24
27
  * OpenCode 2.0 plugin.
25
28
  *
@@ -101,6 +104,60 @@ const plugin = {
101
104
  let disposeHostAgentModeSwitch;
102
105
  trace(`opencode2 plugin: setup directory=${workspaceRoot}`);
103
106
  const hasShellEnvHook = typeof ctx.shell?.hook === "function";
107
+ const refusals = new Map();
108
+ const permissionCalls = new Map();
109
+ const rememberRefusal = (id, refusal) => {
110
+ refusals.delete(id);
111
+ refusals.set(id, refusal);
112
+ while (refusals.size > 512)
113
+ refusals.delete(refusals.keys().next().value);
114
+ };
115
+ const releasePermissionCalls = () => {
116
+ for (const call of permissionCalls.values())
117
+ releaseCursorShellEnv(call.callID);
118
+ permissionCalls.clear();
119
+ };
120
+ const toolLifecycle = (event) => {
121
+ const payload = eventPayload(event);
122
+ if (event?.type === "permission.asked" && payload?.source?.type === "tool"
123
+ && typeof payload.source.id === "string" && payload.source.id.startsWith("cursor_")
124
+ && typeof payload.id === "string" && typeof payload.sessionID === "string") {
125
+ permissionCalls.set(payload.id, { sessionID: payload.sessionID, callID: payload.source.id });
126
+ }
127
+ else if (event?.type === "permission.replied") {
128
+ const call = permissionCalls.get(payload?.requestID);
129
+ if (!call || call.sessionID !== payload?.sessionID)
130
+ return;
131
+ permissionCalls.delete(payload.requestID);
132
+ if (payload.reply === "reject") {
133
+ rememberRefusal(call.callID, "rejected");
134
+ releaseCursorShellEnv(call.callID);
135
+ // Native rejection also rejects the session's other pending requests.
136
+ for (const [id, pending] of permissionCalls)
137
+ if (pending.sessionID === call.sessionID) {
138
+ rememberRefusal(pending.callID, "rejected");
139
+ releaseCursorShellEnv(pending.callID);
140
+ permissionCalls.delete(id);
141
+ }
142
+ }
143
+ }
144
+ else if ((event?.type === "session.tool.failed" || event?.type === "session.tool.success")
145
+ && typeof payload?.id === "string") {
146
+ if (event.type === "session.tool.failed")
147
+ releaseCursorShellEnv(payload.id);
148
+ for (const [id, call] of permissionCalls)
149
+ if (call.callID === payload.id && call.sessionID === payload.sessionID) {
150
+ permissionCalls.delete(id);
151
+ }
152
+ }
153
+ else if (event?.type === "session.deleted") {
154
+ for (const [id, call] of permissionCalls)
155
+ if (call.sessionID === payload?.sessionID) {
156
+ releaseCursorShellEnv(call.callID);
157
+ permissionCalls.delete(id);
158
+ }
159
+ }
160
+ };
104
161
  if (typeof ctx.session.switchAgent === "function") {
105
162
  const switchAgent = ctx.session.switchAgent;
106
163
  const continueTurn = typeof ctx.session.synthetic === "function"
@@ -192,15 +249,13 @@ const plugin = {
192
249
  };
193
250
  // ── AI SDK wiring ────────────────────────────────────────
194
251
  await track(ctx.aisdk.hook("sdk", async (event) => {
195
- if (event.sdk)
196
- return;
197
252
  if (!isCursorPackage(event.package, event.model.providerID))
198
253
  return;
254
+ // The host's generic loader runs before plugin hooks and may already
255
+ // have created a Cursor SDK with a captured token. Own our package's
256
+ // SDK so current integration credentials are resolved on every Run.
199
257
  event.sdk = createSdk({
200
258
  name: event.model.providerID || CURSOR_PROVIDER_ID,
201
- // Resolved per Run open: this SDK lives as long as the daemon, so
202
- // a token captured here would outlive its expiry.
203
- getAccessToken: (request) => requireCursorAccessToken(ctx.integration, request),
204
259
  // Static fallback only. This hook fires once per model/package, not
205
260
  // per session, and 2.0 runs one daemon across many projects — the
206
261
  // real per-request directory comes from `x-opencode-directory`
@@ -208,6 +263,9 @@ const plugin = {
208
263
  workspaceRoot,
209
264
  cacheDir,
210
265
  ...event.options,
266
+ // Keep after host options: a captured resolver/token must not bypass
267
+ // the integration's current credential and renewal path.
268
+ getAccessToken: (request) => requireCursorAccessToken(ctx.integration, request),
211
269
  // Keep after `event.options` so the OC2 plugin always selects the
212
270
  // `path`/`shell` dialect when advertised schemas are opaque.
213
271
  defaultDialect: OPENCODE_2_TOOL_DIALECT,
@@ -262,22 +320,35 @@ const plugin = {
262
320
  exposeDirectMcpTools(draft, directMcpNamespaces);
263
321
  }));
264
322
  // ── Shell timeout wrapper ────────────────────────────────
265
- await track(ctx.tool.hook("execute.before", (event) => {
266
- if (!isShellTool(event.tool))
323
+ await track(ctx.tool.hook("execute.before", async (event) => {
324
+ if (!isShellTool(event.tool) || typeof event.input !== "object" || event.input === null)
267
325
  return;
268
326
  const executionID = toolExecutionID(event);
269
- prepareCursorShellArgs(executionID, event.input, {
270
- // OpenCode 2.0 `shell.create.before` can inject env for bash/zsh.
327
+ const input = event.input;
328
+ prepareCursorShellArgs(executionID, input, {
329
+ deferToShellHook: hasShellEnvHook,
271
330
  preferWrapperCommand: !hasShellEnvHook,
272
331
  });
332
+ if (hasShellEnvHook)
333
+ await armCursorShellCreate(executionID, event);
273
334
  }));
274
335
  await track(ctx.tool.hook("execute.after", (event) => {
336
+ const executionID = toolExecutionID(event);
337
+ if (event.status === "error") {
338
+ const refusal = nativeToolRefusal(event.error);
339
+ if (refusal)
340
+ rememberRefusal(executionID, refusal);
341
+ }
342
+ else
343
+ refusals.delete(executionID);
275
344
  if (!isShellTool(event.tool))
276
345
  return;
277
- const executionID = toolExecutionID(event);
278
346
  try {
279
- if (event.status !== "completed")
347
+ if (event.status !== "completed") {
348
+ if (event.status === "error")
349
+ captureCursorShellFailure(executionID, event.error);
280
350
  return;
351
+ }
281
352
  const result = event.result;
282
353
  // V1: `output` is the model-facing string. V2: `output` is structured
283
354
  // per-tool output (shell: `{ output: string, ... }`) and the
@@ -324,10 +395,7 @@ const plugin = {
324
395
  }));
325
396
  if (ctx.shell) {
326
397
  await track(ctx.shell.hook("create.before", (event) => {
327
- const env = cursorShellEnvForCommand(event.command, event.cwd);
328
- if (!env)
329
- return;
330
- event.env = { ...event.env, ...env };
398
+ prepareCursorShellCreate(event);
331
399
  }));
332
400
  }
333
401
  const rememberSessionDirectory = async (sessionID) => {
@@ -341,6 +409,7 @@ const plugin = {
341
409
  }
342
410
  };
343
411
  await track(ctx.session.hook("context", async (event) => {
412
+ event.messages = preserveNativeToolResults(event.messages, refusals);
344
413
  markCompactionAndOptions(event, event.agent === "compaction");
345
414
  event.options ??= {};
346
415
  event.options[CURSOR_HOST_AGENT_OPTION] = event.agent;
@@ -401,14 +470,17 @@ const plugin = {
401
470
  };
402
471
  }, { providerID: CURSOR_PROVIDER_ID }));
403
472
  await track(ctx.session.hook("compaction", async (event) => {
473
+ event.messages = preserveNativeToolResults(event.messages, refusals);
404
474
  markCompactionAndOptions(event, true);
405
475
  await rememberSessionDirectory(event.sessionID);
406
476
  }));
407
477
  await track(ctx.session.hook("generate", async (event) => {
478
+ event.messages = preserveNativeToolResults(event.messages, refusals);
408
479
  markCompactionAndOptions(event, false);
409
480
  await rememberSessionDirectory(event.sessionID);
410
481
  }));
411
482
  await track(ctx.session.hook("title", async (event) => {
483
+ event.messages = preserveNativeToolResults(event.messages, refusals);
412
484
  markCompactionAndOptions(event, false);
413
485
  await rememberSessionDirectory(event.sessionID);
414
486
  }));
@@ -473,10 +545,13 @@ const plugin = {
473
545
  modelsLoaded = false;
474
546
  credentialGeneration++;
475
547
  };
476
- const unsubscribe = subscribeSessionActivity(ctx, ensureModels, onCredentialSwitch);
548
+ const unsubscribe = subscribeSessionActivity(ctx, ensureModels, onCredentialSwitch, toolLifecycle, releasePermissionCalls);
477
549
  return async () => {
478
550
  clearInterval(retry);
479
551
  unsubscribe?.();
552
+ releasePermissionCalls();
553
+ refusals.clear();
554
+ await saveOpenTurnsForShutdown();
480
555
  disposeHostAgentModeSwitch?.();
481
556
  disposeNativePlansDir();
482
557
  trace(`opencode2 plugin: cleanup directory=${workspaceRoot}`);
@@ -486,22 +561,45 @@ const plugin = {
486
561
  };
487
562
  },
488
563
  };
489
- function subscribeSessionActivity(ctx, onEvent, onCredentialSwitch) {
564
+ const SHUTDOWN_SAVE_TIMEOUT_MS = 2_000;
565
+ /** Save every open turn for the next server start, without holding shutdown up for long. */
566
+ async function saveOpenTurnsForShutdown() {
567
+ let timer;
568
+ const timedOut = new Promise((resolve) => {
569
+ timer = setTimeout(resolve, SHUTDOWN_SAVE_TIMEOUT_MS);
570
+ timer.unref?.();
571
+ });
572
+ try {
573
+ await Promise.race([notifyHostInterrupt(undefined, "shutdown"), timedOut]);
574
+ }
575
+ finally {
576
+ clearTimeout(timer);
577
+ }
578
+ }
579
+ function subscribeSessionActivity(ctx, onEvent, onCredentialSwitch, onToolEvent, onStreamClosed) {
490
580
  try {
491
- const stream = ctx.event.subscribe();
581
+ const controller = new AbortController();
582
+ const stream = ctx.event.subscribe({ signal: controller.signal });
492
583
  if (!stream || typeof stream[Symbol.asyncIterator] !== "function")
493
584
  return undefined;
494
585
  let stopped = false;
495
586
  void (async () => {
496
- for await (const event of stream) {
497
- if (stopped)
498
- break;
499
- applySessionActivity(event, onCredentialSwitch);
500
- onEvent?.();
587
+ try {
588
+ for await (const event of stream) {
589
+ if (stopped)
590
+ break;
591
+ applySessionActivity(event, onCredentialSwitch);
592
+ onToolEvent?.(event);
593
+ onEvent?.();
594
+ }
595
+ }
596
+ finally {
597
+ onStreamClosed?.();
501
598
  }
502
599
  })().catch(() => { });
503
600
  return () => {
504
601
  stopped = true;
602
+ controller.abort();
505
603
  };
506
604
  }
507
605
  catch {
@@ -511,6 +609,8 @@ function subscribeSessionActivity(ctx, onEvent, onCredentialSwitch) {
511
609
  function applySessionActivity(event, onCredentialSwitch) {
512
610
  const payload = eventPayload(event);
513
611
  const info = payload?.info;
612
+ applyHostPromptEvent(sessionActivity, event?.type, payload);
613
+ applyHostToolEvent(sessionActivity, event?.type, payload);
514
614
  switch (event?.type) {
515
615
  case "credential.switched":
516
616
  case "credential.updated": {
@@ -537,6 +637,7 @@ function applySessionActivity(event, onCredentialSwitch) {
537
637
  if (id) {
538
638
  sessionActivity.removeSession(id);
539
639
  clearSessionTodos(id);
640
+ forgetEarlySteers(id);
540
641
  clearActiveCursorMode(id);
541
642
  cancelHostAgentModeSwitch(id);
542
643
  }
@@ -561,6 +662,42 @@ function applySessionActivity(event, onCredentialSwitch) {
561
662
  sessionActivity.recordActivity(id);
562
663
  break;
563
664
  }
665
+ case "session.inbox.enqueued": {
666
+ const id = payload?.sessionID;
667
+ if (id)
668
+ sessionActivity.recordActivity(id);
669
+ const steer = inboxSteer(id, payload?.inboxID, payload?.item);
670
+ if (!steer)
671
+ break;
672
+ if (payload.item.delivery === "steer")
673
+ announceInboxSteer(steer);
674
+ else
675
+ rememberQueuedSteer(steer);
676
+ break;
677
+ }
678
+ case "session.inbox.delivery.changed": {
679
+ const id = payload?.sessionID;
680
+ if (id)
681
+ sessionActivity.recordActivity(id);
682
+ if (payload?.delivery !== "steer" || typeof payload?.inboxID !== "string")
683
+ break;
684
+ const steer = takeQueuedSteer(payload.inboxID);
685
+ if (steer)
686
+ announceInboxSteer(steer);
687
+ break;
688
+ }
689
+ case "session.inbox.delivered":
690
+ case "session.inbox.cancelled": {
691
+ const id = payload?.sessionID;
692
+ if (id)
693
+ sessionActivity.recordActivity(id);
694
+ if (typeof payload?.inboxID !== "string")
695
+ break;
696
+ takeQueuedSteer(payload.inboxID);
697
+ if (event.type === "session.inbox.cancelled" && typeof id === "string")
698
+ forgetEarlySteerOfInbox(id, payload.inboxID);
699
+ break;
700
+ }
564
701
  default: {
565
702
  // OpenCode 2.0 emits granular `session.*` progress events instead of
566
703
  // the V1 `message.updated` family (`session.tool.called/success/failed`,
@@ -571,9 +708,32 @@ function applySessionActivity(event, onCredentialSwitch) {
571
708
  const id = payload?.sessionID ?? info?.sessionID ?? info?.id;
572
709
  if (id)
573
710
  sessionActivity.recordActivity(id);
711
+ if (id && type === "session.execution.interrupted") {
712
+ void notifyHostInterrupt(id, typeof payload?.reason === "string" ? payload.reason : "user", typeof event.created === "number" ? event.created : Date.now());
713
+ }
574
714
  }
575
715
  break;
576
716
  }
577
717
  }
578
718
  }
719
+ /** A plain-text user message; one with files or skills reaches the prompt in another shape. */
720
+ function inboxSteer(sessionID, inboxID, item) {
721
+ if (typeof sessionID !== "string" || !sessionID || typeof inboxID !== "string" || !inboxID)
722
+ return undefined;
723
+ if (item?.type !== "user" || (item.delivery !== "steer" && item.delivery !== "queue"))
724
+ return undefined;
725
+ const prompt = item.payload;
726
+ if (typeof prompt?.text !== "string" || !prompt.text.trim())
727
+ return undefined;
728
+ if (prompt.files?.length || prompt.skills?.length)
729
+ return undefined;
730
+ return { sessionID, inboxID, text: prompt.text };
731
+ }
732
+ function announceInboxSteer(steer) {
733
+ const taken = announceHostSteer(steer);
734
+ if (taken === undefined)
735
+ return;
736
+ trace(`steer: OpenCode enqueued a mid-step message sessionID=${steer.sessionID} inboxID=${steer.inboxID} ` +
737
+ `chars=${steer.text.length} ${taken ? "injected into the pumping Run" : "left for the next step"}`);
738
+ }
579
739
  export default plugin;
package/dist/plugin.js CHANGED
@@ -4,14 +4,15 @@ import { pollForTokens, exchangeApiKey, isExpiringSoon, generatePkceParams, gene
4
4
  import { renewSessionIfDue, resolveApiKeyToken, } from "./auth-renewal.js";
5
5
  import { CursorAuthError } from "./errors.js";
6
6
  import { errorMessage, trace } from "./debug.js";
7
+ import { normalizeClassicInterruptedResults } from "./classic-tool-results.js";
7
8
  import { readCache, discoverModels, isCacheFresh } from "./models.js";
8
9
  import { modelsToConfig } from "./model-config.js";
9
10
  import { loadClassicTools } from "./classic-tools.js";
10
11
  import { opencodeGlobalCacheDir } from "./context/paths.js";
11
12
  import { readStoredAuth } from "./context/auth-store.js";
12
13
  import { resolveAgentUrl } from "./agent-url.js";
13
- import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, } from "./shell-timeout.js";
14
- import { sessionActivity } from "./activity.js";
14
+ import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, cursorShellTimeoutMs, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, resolveCursorShellPath, } from "./shell-timeout.js";
15
+ import { applyHostPromptEvent, applyHostToolEvent, sessionActivity } from "./activity.js";
15
16
  import { dispatchHostEventBridge } from "./host-event-bridge.js";
16
17
  import { createPromptHostAgentModeSwitch, setHostAgentModeSwitch } from "./host-agent-mode.js";
17
18
  import { isHostPlanFileResolved, resolveHostPlanFile } from "./host-plan-file.js";
@@ -250,6 +251,9 @@ export async function CursorPlugin(input) {
250
251
  cursor_image_save: classicTools.imageSave,
251
252
  },
252
253
  async event({ event }) {
254
+ // The SDK's event union predates these prompt events; read them untyped.
255
+ applyHostPromptEvent(sessionActivity, event.type, event.properties);
256
+ applyHostToolEvent(sessionActivity, event.type, event.properties);
253
257
  switch (event.type) {
254
258
  case "session.created":
255
259
  sessionActivity.linkSession(event.properties.info.id, event.properties.info.parentID);
@@ -266,6 +270,11 @@ export async function CursorPlugin(input) {
266
270
  break;
267
271
  case "message.part.updated":
268
272
  sessionActivity.recordActivity(event.properties.part.sessionID);
273
+ // Rejection/abort can stop the classic host before a continuation or
274
+ // execute.after. Its persisted tool state is still authoritative.
275
+ if (event.properties.part.type === "tool" && event.properties.part.state.status === "error") {
276
+ releaseCursorShellEnv(event.properties.part.callID);
277
+ }
269
278
  break;
270
279
  }
271
280
  await dispatchHostEventBridge({
@@ -275,13 +284,22 @@ export async function CursorPlugin(input) {
275
284
  serverUrl: input.serverUrl,
276
285
  });
277
286
  },
287
+ async "experimental.chat.messages.transform"(_hookInput, output) {
288
+ // The classic host retains the original array and discards hook output.
289
+ // Replace entries with request-only clones, preserving array identity.
290
+ const normalized = normalizeClassicInterruptedResults(output.messages);
291
+ for (let index = 0; index < output.messages.length; index++)
292
+ output.messages[index] = normalized[index];
293
+ },
278
294
  async "tool.execute.before"(hookInput, output) {
279
295
  if (hookInput.tool !== "bash")
280
296
  return;
281
297
  // bash/zsh retain the original display/permission command and wrap via
282
- // shell.env. sh/dash need a short wrapper-file command because their
298
+ // shell.env. sh/dash disclose the managed launcher inline because their
283
299
  // non-interactive `-c` path ignores BASH_ENV / ZDOTDIR.
284
- prepareCursorShellArgs(hookInput.callID, output.args);
300
+ prepareCursorShellArgs(hookInput.callID, output.args, {
301
+ shellPath: resolveCursorShellPath(undefined, input.directory),
302
+ });
285
303
  },
286
304
  async "shell.env"(hookInput, output) {
287
305
  const env = cursorShellEnvForCall(hookInput.callID);
@@ -302,6 +320,11 @@ export async function CursorPlugin(input) {
302
320
  if (typeof metadata.output === "string") {
303
321
  metadata.output = sanitizeRegisteredCursorShellOutput(hookInput.callID, metadata.output);
304
322
  }
323
+ // The stored output keeps only a display notice, and the saved
324
+ // outcome does not survive consumption or a restart.
325
+ const timeoutMs = cursorShellTimeoutMs(hookInput.callID);
326
+ if (timeoutMs !== undefined)
327
+ metadata.cursorShellTimeoutMs = timeoutMs;
305
328
  }
306
329
  }
307
330
  finally {
@@ -339,7 +362,7 @@ export async function CursorPlugin(input) {
339
362
  }
340
363
  },
341
364
  async config(cfg) {
342
- setCursorShellPath(cfg.shell);
365
+ setCursorShellPath(cfg.shell, input.directory);
343
366
  cfg.provider ??= {};
344
367
  const models = await loadModels();
345
368
  const existing = cfg.provider[CURSOR_PROVIDER_ID];
@@ -140,14 +140,19 @@ export declare const CURSOR_MODEL_COSTS: {
140
140
  };
141
141
  readonly "gemini-3.7-flash": {
142
142
  readonly input: 0.75;
143
- readonly output: 3.5;
143
+ readonly output: 3.75;
144
144
  readonly cache_read: 0.075;
145
145
  };
146
146
  readonly "gemini-3.8-flash": {
147
147
  readonly input: 0.75;
148
- readonly output: 3.5;
148
+ readonly output: 3.75;
149
149
  readonly cache_read: 0.075;
150
150
  };
151
+ readonly "gemini-4-argon": {
152
+ readonly input: 2;
153
+ readonly output: 10;
154
+ readonly cache_read: 0.1;
155
+ };
151
156
  readonly "glm-5.2": {
152
157
  readonly input: 1.4;
153
158
  readonly output: 4.4;
@@ -304,6 +309,11 @@ export declare const CURSOR_MODEL_COSTS: {
304
309
  readonly output: 4.25;
305
310
  readonly cache_read: 0.15;
306
311
  };
312
+ readonly "muse-wonder-1.0": {
313
+ readonly input: 4;
314
+ readonly output: 20;
315
+ readonly cache_read: 0.2;
316
+ };
307
317
  };
308
318
  export declare const CURSOR_MODEL_CONTEXTS: {
309
319
  readonly "claude-fable-5": {
@@ -396,6 +406,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
396
406
  readonly maxContext: 200000;
397
407
  readonly maxContextForMaxMode: 1000000;
398
408
  };
409
+ readonly "gemini-4-argon": {
410
+ readonly maxContext: 200000;
411
+ readonly maxContextForMaxMode: 1000000;
412
+ };
399
413
  readonly "glm-5.2": {
400
414
  readonly maxContext: 200000;
401
415
  };
@@ -464,6 +478,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
464
478
  readonly maxContext: 300000;
465
479
  readonly maxContextForMaxMode: 1000000;
466
480
  };
481
+ readonly "muse-wonder-1.0": {
482
+ readonly maxContext: 300000;
483
+ readonly maxContextForMaxMode: 1000000;
484
+ };
467
485
  };
468
486
  export declare const CURSOR_MODEL_CAPABILITIES: {
469
487
  readonly "claude-fable-5": {
@@ -535,6 +553,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
535
553
  readonly "gemini-3.8-flash": {
536
554
  readonly supportsImages: true;
537
555
  };
556
+ readonly "gemini-4-argon": {
557
+ readonly supportsImages: true;
558
+ };
538
559
  readonly "glm-5.2": {
539
560
  readonly supportsImages: false;
540
561
  };
@@ -595,4 +616,7 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
595
616
  readonly "muse-spark-1.3": {
596
617
  readonly supportsImages: true;
597
618
  };
619
+ readonly "muse-wonder-1.0": {
620
+ readonly supportsImages: true;
621
+ };
598
622
  };
@@ -140,14 +140,19 @@ export const CURSOR_MODEL_COSTS = {
140
140
  },
141
141
  "gemini-3.7-flash": {
142
142
  "input": 0.75,
143
- "output": 3.5,
143
+ "output": 3.75,
144
144
  "cache_read": 0.075
145
145
  },
146
146
  "gemini-3.8-flash": {
147
147
  "input": 0.75,
148
- "output": 3.5,
148
+ "output": 3.75,
149
149
  "cache_read": 0.075
150
150
  },
151
+ "gemini-4-argon": {
152
+ "input": 2,
153
+ "output": 10,
154
+ "cache_read": 0.1
155
+ },
151
156
  "glm-5.2": {
152
157
  "input": 1.4,
153
158
  "output": 4.4,
@@ -303,6 +308,11 @@ export const CURSOR_MODEL_COSTS = {
303
308
  "input": 1.25,
304
309
  "output": 4.25,
305
310
  "cache_read": 0.15
311
+ },
312
+ "muse-wonder-1.0": {
313
+ "input": 4,
314
+ "output": 20,
315
+ "cache_read": 0.2
306
316
  }
307
317
  };
308
318
  export const CURSOR_MODEL_CONTEXTS = {
@@ -396,6 +406,10 @@ export const CURSOR_MODEL_CONTEXTS = {
396
406
  "maxContext": 200000,
397
407
  "maxContextForMaxMode": 1000000
398
408
  },
409
+ "gemini-4-argon": {
410
+ "maxContext": 200000,
411
+ "maxContextForMaxMode": 1000000
412
+ },
399
413
  "glm-5.2": {
400
414
  "maxContext": 200000
401
415
  },
@@ -463,6 +477,10 @@ export const CURSOR_MODEL_CONTEXTS = {
463
477
  "muse-spark-1.3": {
464
478
  "maxContext": 300000,
465
479
  "maxContextForMaxMode": 1000000
480
+ },
481
+ "muse-wonder-1.0": {
482
+ "maxContext": 300000,
483
+ "maxContextForMaxMode": 1000000
466
484
  }
467
485
  };
468
486
  export const CURSOR_MODEL_CAPABILITIES = {
@@ -535,6 +553,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
535
553
  "gemini-3.8-flash": {
536
554
  "supportsImages": true
537
555
  },
556
+ "gemini-4-argon": {
557
+ "supportsImages": true
558
+ },
538
559
  "glm-5.2": {
539
560
  "supportsImages": false
540
561
  },
@@ -594,5 +615,8 @@ export const CURSOR_MODEL_CAPABILITIES = {
594
615
  },
595
616
  "muse-spark-1.3": {
596
617
  "supportsImages": true
618
+ },
619
+ "muse-wonder-1.0": {
620
+ "supportsImages": true
597
621
  }
598
622
  };
package/dist/pricing.d.ts CHANGED
@@ -40,7 +40,7 @@ export declare function validateOpenCodeModelCost(value: unknown, path?: string)
40
40
  errors: string[];
41
41
  };
42
42
  export declare function isOpenCodeModelCost(value: unknown): value is OpenCodeModelCost;
43
- /** Strip synthetic OpenCode suffixes (`…-1m`, `…-1m-2`, `…-1m-fast`, …). */
43
+ /** Strip synthetic context suffixes (`…-1m`, `…-500k`, `…-500k-2-fast`, …). */
44
44
  export declare function wireModelIdForPricing(modelId: string): {
45
45
  baseId: string;
46
46
  longContextEntry: boolean;