cursor-opencode-provider 0.5.0 → 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 (48) hide show
  1. package/README.md +18 -13
  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 +3 -1
  6. package/dist/context/build.js +4 -2
  7. package/dist/context/index.d.ts +1 -1
  8. package/dist/context/index.js +1 -1
  9. package/dist/context/paths.d.ts +34 -53
  10. package/dist/context/paths.js +54 -118
  11. package/dist/context/rules.js +1 -1
  12. package/dist/image-save-tool.d.ts +19 -0
  13. package/dist/image-save-tool.js +25 -0
  14. package/dist/image-save.d.ts +58 -0
  15. package/dist/image-save.js +189 -0
  16. package/dist/image-staging.d.ts +59 -0
  17. package/dist/image-staging.js +91 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/language-model.d.ts +3 -2
  20. package/dist/language-model.js +572 -34
  21. package/dist/plan-execution-kickoff.d.ts +55 -0
  22. package/dist/plan-execution-kickoff.js +162 -0
  23. package/dist/plugin-opencode2.js +52 -3
  24. package/dist/plugin-v2.d.ts +11 -9
  25. package/dist/plugin-v2.js +6 -13
  26. package/dist/plugin.d.ts +9 -0
  27. package/dist/plugin.js +61 -5
  28. package/dist/pricing-data.d.ts +16 -4
  29. package/dist/pricing-data.js +16 -4
  30. package/dist/protocol/ask-question.d.ts +133 -0
  31. package/dist/protocol/ask-question.js +320 -0
  32. package/dist/protocol/create-plan.d.ts +141 -0
  33. package/dist/protocol/create-plan.js +373 -0
  34. package/dist/protocol/generate-image.d.ts +54 -0
  35. package/dist/protocol/generate-image.js +101 -0
  36. package/dist/protocol/interactions.d.ts +110 -8
  37. package/dist/protocol/interactions.js +295 -35
  38. package/dist/protocol/messages.js +118 -6
  39. package/dist/protocol/switch-mode.d.ts +178 -0
  40. package/dist/protocol/switch-mode.js +417 -0
  41. package/dist/protocol/tool-call-bridge.js +10 -5
  42. package/dist/protocol/tools.d.ts +27 -11
  43. package/dist/protocol/tools.js +95 -41
  44. package/dist/usage.d.ts +2 -2
  45. package/dist/usage.js +2 -2
  46. package/dist/web-search-tool.d.ts +22 -26
  47. package/dist/web-search-tool.js +25 -15
  48. package/package.json +7 -4
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Queue OpenCode's post-plan-exit kickoff after CreatePlan approval.
3
+ *
4
+ * Native `plan_exit` asks the user, then injects a synthetic user message with
5
+ * `agent: "build"` so a new turn starts implementing. Our CreatePlan → question
6
+ * path already asks; this module is the missing second half. A host plugin
7
+ * installs the prompt handler; the language model records a request after Yes,
8
+ * and the outer stream wrapper flushes it only after the current `doStream`
9
+ * result stream has settled. This prevents a second generation from starting
10
+ * while the continuation Run is still live.
11
+ */
12
+ export type PlanExecutionKickoffInput = {
13
+ sessionID: string;
14
+ /** Filesystem path (or relative display path) of the approved plan. */
15
+ planPath: string;
16
+ /** Concrete Cursor Run that owns the approval. Guards recovery/supersession. */
17
+ cursorSessionID?: string;
18
+ };
19
+ export type PlanExecutionKickoffFn = (input: PlanExecutionKickoffInput) => void | Promise<void>;
20
+ /** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */
21
+ export declare function createPlanExecutionKickoffText(planPath: string): string;
22
+ /**
23
+ * Prefer a worktree-relative label when the plan sits under `workspaceRoot`,
24
+ * matching OpenCode's `path.relative(instance.worktree, Session.plan(...))`.
25
+ * Otherwise keep the absolute path (global data `plans/` is outside the tree).
26
+ */
27
+ export declare function formatPlanKickoffPath(planPath: string, workspaceRoot?: string): string;
28
+ /** Decode a `file://` plan URI to a filesystem path; pass other strings through. */
29
+ export declare function planPathFromUri(planUri: string): string;
30
+ export type PlanExecutionKickoffState = PlanExecutionKickoffInput & {
31
+ status: "pending" | "failed";
32
+ attempts: number;
33
+ lastError?: string;
34
+ };
35
+ /** Install (or clear) the host kickoff. */
36
+ export declare function setPlanExecutionKickoff(fn: PlanExecutionKickoffFn | undefined): void;
37
+ export declare function hasPlanExecutionKickoff(): boolean;
38
+ /** Record the approved plan; execution is flushed after the stream settles. */
39
+ export declare function queuePlanExecutionKickoff(input: PlanExecutionKickoffInput): boolean;
40
+ /**
41
+ * Run one pending kickoff only after the owning Cursor Run reached a terminal,
42
+ * idle state. Returns true only after successful host handoff.
43
+ */
44
+ export declare function flushPlanExecutionKickoff(sessionID: string | undefined, options?: {
45
+ cursorSessionID?: string;
46
+ terminal?: boolean;
47
+ pumpActive?: boolean;
48
+ pendingExecs?: number;
49
+ }): Promise<boolean>;
50
+ /** Consume a user-visible warning on the next provider turn. */
51
+ export declare function takePlanExecutionKickoffWarning(sessionID: string | undefined): string | undefined;
52
+ /** Retry state remains explicit; no timer or automatic loop fires it. */
53
+ export declare function planExecutionKickoffState(sessionID: string | undefined): PlanExecutionKickoffState | undefined;
54
+ /** Test helper: clear the registered kickoff and pending approvals. */
55
+ export declare function resetPlanExecutionKickoffForTests(): void;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Queue OpenCode's post-plan-exit kickoff after CreatePlan approval.
3
+ *
4
+ * Native `plan_exit` asks the user, then injects a synthetic user message with
5
+ * `agent: "build"` so a new turn starts implementing. Our CreatePlan → question
6
+ * path already asks; this module is the missing second half. A host plugin
7
+ * installs the prompt handler; the language model records a request after Yes,
8
+ * and the outer stream wrapper flushes it only after the current `doStream`
9
+ * result stream has settled. This prevents a second generation from starting
10
+ * while the continuation Run is still live.
11
+ */
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { trace } from "./debug.js";
15
+ /** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */
16
+ export function createPlanExecutionKickoffText(planPath) {
17
+ const where = planPath.trim() || "the plan";
18
+ return `The plan at ${where} has been approved, you can now edit files. Execute the plan`;
19
+ }
20
+ /**
21
+ * Prefer a worktree-relative label when the plan sits under `workspaceRoot`,
22
+ * matching OpenCode's `path.relative(instance.worktree, Session.plan(...))`.
23
+ * Otherwise keep the absolute path (global data `plans/` is outside the tree).
24
+ */
25
+ export function formatPlanKickoffPath(planPath, workspaceRoot) {
26
+ const absolute = planPath.trim();
27
+ if (!absolute)
28
+ return absolute;
29
+ const root = workspaceRoot?.trim();
30
+ if (!root)
31
+ return absolute;
32
+ const relative = path.relative(root, absolute);
33
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
34
+ return absolute;
35
+ }
36
+ return relative;
37
+ }
38
+ /** Decode a `file://` plan URI to a filesystem path; pass other strings through. */
39
+ export function planPathFromUri(planUri) {
40
+ const raw = planUri.trim();
41
+ if (!raw)
42
+ return "";
43
+ if (raw.startsWith("file:")) {
44
+ try {
45
+ return fileURLToPath(raw);
46
+ }
47
+ catch {
48
+ return raw;
49
+ }
50
+ }
51
+ return raw;
52
+ }
53
+ let kickoff;
54
+ const pending = new Map();
55
+ const warnings = new Map();
56
+ /** Install (or clear) the host kickoff. */
57
+ export function setPlanExecutionKickoff(fn) {
58
+ kickoff = fn;
59
+ }
60
+ export function hasPlanExecutionKickoff() {
61
+ return kickoff !== undefined;
62
+ }
63
+ /** Record the approved plan; execution is flushed after the stream settles. */
64
+ export function queuePlanExecutionKickoff(input) {
65
+ const sessionID = input.sessionID.trim();
66
+ const planPath = input.planPath.trim();
67
+ if (!sessionID || !planPath || !kickoff) {
68
+ trace(`plan-execution-kickoff: not queued sessionID=${JSON.stringify(sessionID)} ` +
69
+ `planPath=${JSON.stringify(planPath)} handler=${Boolean(kickoff)}`);
70
+ return false;
71
+ }
72
+ pending.set(sessionID, {
73
+ sessionID,
74
+ planPath,
75
+ ...(input.cursorSessionID ? { cursorSessionID: input.cursorSessionID } : {}),
76
+ status: "pending",
77
+ attempts: 0,
78
+ });
79
+ warnings.delete(sessionID);
80
+ trace(`plan-execution-kickoff: pending sessionID=${sessionID} ` +
81
+ `cursorSessionID=${input.cursorSessionID ?? ""} planPath=${planPath}`);
82
+ return true;
83
+ }
84
+ /**
85
+ * Run one pending kickoff only after the owning Cursor Run reached a terminal,
86
+ * idle state. Returns true only after successful host handoff.
87
+ */
88
+ export async function flushPlanExecutionKickoff(sessionID, options = {}) {
89
+ const key = sessionID?.trim();
90
+ if (!key)
91
+ return false;
92
+ const state = pending.get(key);
93
+ if (!state)
94
+ return false;
95
+ if (options.terminal !== true || options.pumpActive || (options.pendingExecs ?? 0) > 0) {
96
+ trace(`plan-execution-kickoff: deferred sessionID=${key} terminal=${Boolean(options.terminal)} ` +
97
+ `pumpActive=${Boolean(options.pumpActive)} pending=${options.pendingExecs ?? 0}`);
98
+ return false;
99
+ }
100
+ if (state.cursorSessionID
101
+ && options.cursorSessionID
102
+ && state.cursorSessionID !== options.cursorSessionID) {
103
+ trace(`plan-execution-kickoff: skipped stale Run sessionID=${key} ` +
104
+ `owner=${state.cursorSessionID} terminal=${options.cursorSessionID}`);
105
+ return false;
106
+ }
107
+ const run = kickoff;
108
+ if (!run) {
109
+ const message = "The plan was approved, but this host cannot start its execution turn.";
110
+ state.status = "failed";
111
+ state.lastError = message;
112
+ warnings.set(key, message);
113
+ return false;
114
+ }
115
+ state.attempts += 1;
116
+ try {
117
+ await run({
118
+ sessionID: state.sessionID,
119
+ planPath: state.planPath,
120
+ ...(state.cursorSessionID ? { cursorSessionID: state.cursorSessionID } : {}),
121
+ });
122
+ pending.delete(key);
123
+ warnings.delete(key);
124
+ trace(`plan-execution-kickoff: queued sessionID=${key} planPath=${state.planPath}`);
125
+ return true;
126
+ }
127
+ catch (error) {
128
+ const detail = error instanceof Error ? error.message : String(error);
129
+ const message = `The approved plan could not start execution: ${detail}. ` +
130
+ "The plan remains active and can be retried.";
131
+ state.status = "failed";
132
+ state.lastError = detail;
133
+ // A later explicit provider turn may retry after its own terminal boundary;
134
+ // do not keep pinning the retry to the now-finished Cursor Run.
135
+ delete state.cursorSessionID;
136
+ warnings.set(key, message);
137
+ trace(`plan-execution-kickoff: FAILED sessionID=${key} attempts=${state.attempts} err=${detail}`);
138
+ return false;
139
+ }
140
+ }
141
+ /** Consume a user-visible warning on the next provider turn. */
142
+ export function takePlanExecutionKickoffWarning(sessionID) {
143
+ const key = sessionID?.trim();
144
+ if (!key)
145
+ return undefined;
146
+ const message = warnings.get(key);
147
+ if (message)
148
+ warnings.delete(key);
149
+ return message;
150
+ }
151
+ /** Retry state remains explicit; no timer or automatic loop fires it. */
152
+ export function planExecutionKickoffState(sessionID) {
153
+ const key = sessionID?.trim();
154
+ const state = key ? pending.get(key) : undefined;
155
+ return state ? { ...state } : undefined;
156
+ }
157
+ /** Test helper: clear the registered kickoff and pending approvals. */
158
+ export function resetPlanExecutionKickoffForTests() {
159
+ kickoff = undefined;
160
+ pending.clear();
161
+ warnings.clear();
162
+ }
@@ -1,15 +1,18 @@
1
1
  import { CURSOR_PROVIDER_ID } from "./shared.js";
2
2
  import { createSdk, cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled, isCursorPackage } from "./plugin-core.js";
3
- import { adoptCompatHostCacheDir, opencodeGlobalCacheDir } from "./context/paths.js";
3
+ import { opencodeGlobalCacheDir } from "./context/paths.js";
4
4
  import { discoverModels, isCacheFresh, readCache } from "./models.js";
5
5
  import { resolveAgentUrl } from "./agent-url.js";
6
6
  import { sessionActivity } from "./activity.js";
7
7
  import { fetchOpenCodeWebSearchText } from "./web-tools.js";
8
+ import { executeCursorImageSave, } from "./image-save.js";
9
+ import { CURSOR_IMAGE_SAVE_TOOL } from "./protocol/generate-image.js";
8
10
  import { captureCursorShellResult, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
9
11
  import { applyCursorModels, applyCursorProvider } from "./opencode2/catalog.js";
10
12
  import { applyCursorIntegration, resolveCursorAccessToken } from "./opencode2/integration.js";
11
13
  import { markCompactionSession } from "./compaction-marker.js";
12
14
  import { markSessionDirectory } from "./session-directory.js";
15
+ import { setPlanExecutionKickoff } from "./plan-execution-kickoff.js";
13
16
  /**
14
17
  * OpenCode 2.0 beta plugin.
15
18
  *
@@ -44,9 +47,12 @@ function toolExecutionID(event) {
44
47
  const plugin = {
45
48
  id: "cursor.provider",
46
49
  setup: async (ctx) => {
47
- // Prefer OCP HostProfile.cacheDir when present; else XDG host heuristic.
48
- await adoptCompatHostCacheDir();
49
50
  const cacheDir = opencodeGlobalCacheDir();
51
+ // The 2.0 SessionDomain has `prompt` but no agent-selection method, so it
52
+ // cannot faithfully reproduce classic plan_exit's `agent: "build"` kickoff.
53
+ // Leave the handler absent: the continuation reports an execution error and
54
+ // retains plan mode instead of returning false success.
55
+ setPlanExecutionKickoff(undefined);
50
56
  const registrations = [];
51
57
  const track = async (p) => {
52
58
  registrations.push(await p);
@@ -143,6 +149,48 @@ const plugin = {
143
149
  };
144
150
  },
145
151
  });
152
+ // Same handle-only commit tool as classic plugin.ts. Uses host-neutral
153
+ // executeCursorImageSave (not image-save-tool.ts) so this entrypoint never
154
+ // imports `@opencode-ai/plugin`'s classic `tool()` helper.
155
+ draft.add({
156
+ name: CURSOR_IMAGE_SAVE_TOOL,
157
+ description: "Save an image that Cursor generated during this session to its target path. "
158
+ + "Takes only the id of an already-generated image — it cannot write arbitrary "
159
+ + "files, and it is not a general-purpose file writer. You do not normally call "
160
+ + "this: the Cursor provider issues it after an image is generated.",
161
+ input: {
162
+ type: "object",
163
+ properties: {
164
+ image_id: {
165
+ type: "string",
166
+ description: "Id of the pending Cursor-generated image to save",
167
+ },
168
+ },
169
+ required: ["image_id"],
170
+ additionalProperties: false,
171
+ },
172
+ execute: async (input, context) => {
173
+ const saveCtx = {
174
+ worktree: typeof context?.worktree === "string" ? context.worktree : "",
175
+ directory: typeof context?.directory === "string" ? context.directory : "",
176
+ ask: typeof context?.ask === "function"
177
+ ? context.ask.bind(context)
178
+ : async () => {
179
+ throw new Error("OpenCode 2.0 tool context did not provide ask(); cannot gate image save");
180
+ },
181
+ };
182
+ const result = await executeCursorImageSave(input, saveCtx);
183
+ if (typeof result === "string") {
184
+ return { title: CURSOR_IMAGE_SAVE_TOOL, output: result, metadata: {} };
185
+ }
186
+ const typed = result;
187
+ return {
188
+ title: typed.title,
189
+ output: typed.output,
190
+ metadata: {},
191
+ };
192
+ },
193
+ });
146
194
  }));
147
195
  // ── Shell timeout wrapper ────────────────────────────────────────────────
148
196
  await track(ctx.tool.hook("execute.before", (event) => {
@@ -249,6 +297,7 @@ const plugin = {
249
297
  return async () => {
250
298
  clearInterval(retry);
251
299
  unsubscribe?.();
300
+ setPlanExecutionKickoff(undefined);
252
301
  for (const registration of registrations.reverse()) {
253
302
  await registration.dispose().catch(() => { });
254
303
  }
@@ -1,9 +1,11 @@
1
- /**
2
- * OpenCode Effect / Promise v2 plugin.
3
- *
4
- * Wires the Cursor provider through `ctx.aisdk.sdk` / `ctx.aisdk.language`
5
- * (the V2 extension points). Auth still lives in the classic Hooks plugin
6
- * (`plugin.ts`) until OpenCode integrations fully replace provider OAuth.
7
- */
8
- declare const _default: import("@opencode-ai/plugin/v2/promise").Plugin;
9
- export default _default;
1
+ type PromiseV2Plugin = {
2
+ id: string;
3
+ setup: (ctx: {
4
+ aisdk: {
5
+ sdk: (callback: (event: any) => void) => Promise<void>;
6
+ language: (callback: (event: any) => void) => Promise<void>;
7
+ };
8
+ }) => Promise<void>;
9
+ };
10
+ declare const plugin: PromiseV2Plugin;
11
+ export default plugin;
package/dist/plugin-v2.js CHANGED
@@ -1,20 +1,12 @@
1
- import { define } from "@opencode-ai/plugin/v2/promise";
2
1
  import { CURSOR_PROVIDER_ID } from "./shared.js";
3
2
  import { createSdk, isCursorPackage } from "./plugin-core.js";
4
- import { adoptCompatHostCacheDir } from "./context/paths.js";
5
- /**
6
- * OpenCode Effect / Promise v2 plugin.
7
- *
8
- * Wires the Cursor provider through `ctx.aisdk.sdk` / `ctx.aisdk.language`
9
- * (the V2 extension points). Auth still lives in the classic Hooks plugin
10
- * (`plugin.ts`) until OpenCode integrations fully replace provider OAuth.
11
- */
12
- export default define({
3
+ /** OpenCode's v2/promise `define` is an identity function. */
4
+ function define(plugin) {
5
+ return plugin;
6
+ }
7
+ const plugin = define({
13
8
  id: "cursor.provider",
14
9
  setup: async (ctx) => {
15
- // Prefer OCP HostProfile.cacheDir when present; else XDG host heuristic.
16
- // Explicit createCursor({ cacheDir }) / event.options.cacheDir still wins via LM.
17
- await adoptCompatHostCacheDir();
18
10
  await ctx.aisdk.sdk((event) => {
19
11
  if (event.sdk)
20
12
  return;
@@ -36,3 +28,4 @@ export default define({
36
28
  });
37
29
  },
38
30
  });
31
+ export default plugin;
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,12 @@
1
1
  import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
2
  export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js";
3
+ export declare function loadClassicTools(options?: {
4
+ importModule?: (specifier: string) => Promise<{
5
+ tool?: any;
6
+ }>;
7
+ configDirs?: string[];
8
+ }): Promise<{
9
+ webSearch: any;
10
+ imageSave: any;
11
+ }>;
3
12
  export declare function CursorPlugin(input: PluginInput): Promise<Hooks>;
package/dist/plugin.js CHANGED
@@ -1,3 +1,5 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
1
3
  import { CURSOR_COMPACTION_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
2
4
  import { cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled } from "./plugin-core.js";
3
5
  import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs } from "./auth.js";
@@ -7,18 +9,68 @@ import { readCache, discoverModels, isCacheFresh } from "./models.js";
7
9
  // host plugin imports so the OpenCode 2.0 entrypoint can share it.
8
10
  import { modelsToConfig } from "./model-config.js";
9
11
  export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js";
10
- import { adoptCompatHostCacheDir, opencodeGlobalCacheDir } from "./context/paths.js";
12
+ import { opencodeGlobalCacheDir, opencodeGlobalConfigDirs } from "./context/paths.js";
11
13
  import { readStoredAuth } from "./context/auth-store.js";
12
14
  import { resolveAgentUrl } from "./agent-url.js";
13
15
  import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, } from "./shell-timeout.js";
14
16
  import { sessionActivity } from "./activity.js";
15
- import { openCodeWebSearchTool } from "./web-search-tool.js";
17
+ import { createOpenCodeWebSearchTool, openCodeWebSearchTool } from "./web-search-tool.js";
18
+ import { createCursorImageSaveTool, cursorImageSaveTool } from "./image-save-tool.js";
19
+ import { createPlanExecutionKickoffText, setPlanExecutionKickoff, } from "./plan-execution-kickoff.js";
16
20
  const MODULE_URL = new URL("./index.js", import.meta.url).href;
21
+ export async function loadClassicTools(options = {}) {
22
+ const configDir = options.configDirs?.[0] ?? opencodeGlobalConfigDirs()[0];
23
+ const candidates = [
24
+ ...(configDir
25
+ ? [path.join(configDir, "node_modules", "@opencode-ai", "plugin", "dist", "index.js")]
26
+ : []),
27
+ "@opencode-ai/plugin",
28
+ ];
29
+ const importModule = options.importModule ?? ((specifier) => import(specifier));
30
+ // The global config copy is the host-owned installation; bare import is a fallback.
31
+ for (const candidate of candidates) {
32
+ try {
33
+ const specifier = path.win32.isAbsolute(candidate) && !path.isAbsolute(candidate)
34
+ ? new URL(`file:///${candidate.replaceAll("\\", "/")}`).href
35
+ : path.isAbsolute(candidate)
36
+ ? pathToFileURL(candidate).href
37
+ : candidate;
38
+ const module = await importModule(specifier);
39
+ if (typeof module.tool === "function" && module.tool.schema) {
40
+ const factory = { tool: module.tool, schema: module.tool.schema };
41
+ return {
42
+ webSearch: createOpenCodeWebSearchTool(factory),
43
+ imageSave: createCursorImageSaveTool(factory),
44
+ };
45
+ }
46
+ }
47
+ catch {
48
+ // Try the next host-owned/normal resolution location.
49
+ }
50
+ }
51
+ // `tool()` is an identity helper; the fallback definitions use plain JSON
52
+ // Schema accepted by OpenCode's legacy registry when no Zod helper is present.
53
+ return { webSearch: openCodeWebSearchTool, imageSave: cursorImageSaveTool };
54
+ }
17
55
  export async function CursorPlugin(input) {
18
- // Prefer strong OCP host identity when available; otherwise resolve from explicit host signals/install path.
19
- await adoptCompatHostCacheDir();
20
56
  const cacheDir = opencodeGlobalCacheDir();
21
57
  const apiBaseURL = cursorApiBaseURL();
58
+ const classicTools = await loadClassicTools();
59
+ // After CreatePlan Yes, queue OpenCode's plan_exit-shaped synthetic turn so
60
+ // implementation starts. Failures stay in the kickoff module (trace only).
61
+ setPlanExecutionKickoff(async ({ sessionID, planPath }) => {
62
+ await input.client.session.promptAsync({
63
+ path: { id: sessionID },
64
+ body: {
65
+ agent: "build",
66
+ parts: [{
67
+ type: "text",
68
+ text: createPlanExecutionKickoffText(planPath),
69
+ synthetic: true,
70
+ }],
71
+ },
72
+ });
73
+ });
22
74
  // Last access token successfully resolved in this plugin instance. Config's
23
75
  // loadModels can only read OpenCode's durable store (auth.json /
24
76
  // OPENCODE_AUTH_CONTENT); auth.loader gets live credentials via getAuth().
@@ -140,7 +192,11 @@ export async function CursorPlugin(input) {
140
192
  // `websearch` is a reserved OpenCode id and is filtered for third-party
141
193
  // providers after plugin tools are merged. Use the collision-safe id
142
194
  // Cursor already sees so this host-side fallback survives that filter.
143
- custom_websearch: openCodeWebSearchTool,
195
+ custom_websearch: classicTools.webSearch,
196
+ // Commits Cursor-generated image bytes, which cannot travel through the
197
+ // host's text `write`. Handle-only, so its presence in the catalog does
198
+ // not give any model a way to write arbitrary files — see image-save.ts.
199
+ cursor_image_save: classicTools.imageSave,
144
200
  },
145
201
  async event({ event }) {
146
202
  switch (event.type) {
@@ -74,10 +74,10 @@ export declare const CURSOR_MODEL_COSTS: {
74
74
  readonly cache_write: 3.75;
75
75
  };
76
76
  readonly "claude-sonnet-5": {
77
- readonly input: 3;
78
- readonly output: 15;
79
- readonly cache_read: 0.3;
80
- readonly cache_write: 3.75;
77
+ readonly input: 2;
78
+ readonly output: 10;
79
+ readonly cache_read: 0.2;
80
+ readonly cache_write: 2.5;
81
81
  };
82
82
  readonly "gemini-2.5-flash": {
83
83
  readonly input: 0.3;
@@ -104,6 +104,11 @@ export declare const CURSOR_MODEL_COSTS: {
104
104
  readonly output: 7.5;
105
105
  readonly cache_read: 0.15;
106
106
  };
107
+ readonly "gemini-3.7-flash": {
108
+ readonly input: 0.75;
109
+ readonly output: 3.5;
110
+ readonly cache_read: 0.075;
111
+ };
107
112
  readonly "glm-5.2": {
108
113
  readonly input: 1.4;
109
114
  readonly output: 4.4;
@@ -261,6 +266,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
261
266
  readonly maxContext: 200000;
262
267
  readonly maxContextForMaxMode: 1000000;
263
268
  };
269
+ readonly "gemini-3.7-flash": {
270
+ readonly maxContext: 200000;
271
+ readonly maxContextForMaxMode: 1000000;
272
+ };
264
273
  readonly "glm-5.2": {
265
274
  readonly maxContext: 200000;
266
275
  };
@@ -369,6 +378,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
369
378
  readonly "gemini-3.6-flash": {
370
379
  readonly supportsImages: true;
371
380
  };
381
+ readonly "gemini-3.7-flash": {
382
+ readonly supportsImages: true;
383
+ };
372
384
  readonly "glm-5.2": {
373
385
  readonly supportsImages: false;
374
386
  };
@@ -74,10 +74,10 @@ export const CURSOR_MODEL_COSTS = {
74
74
  "cache_write": 3.75
75
75
  },
76
76
  "claude-sonnet-5": {
77
- "input": 3,
78
- "output": 15,
79
- "cache_read": 0.3,
80
- "cache_write": 3.75
77
+ "input": 2,
78
+ "output": 10,
79
+ "cache_read": 0.2,
80
+ "cache_write": 2.5
81
81
  },
82
82
  "gemini-2.5-flash": {
83
83
  "input": 0.3,
@@ -104,6 +104,11 @@ export const CURSOR_MODEL_COSTS = {
104
104
  "output": 7.5,
105
105
  "cache_read": 0.15
106
106
  },
107
+ "gemini-3.7-flash": {
108
+ "input": 0.75,
109
+ "output": 3.5,
110
+ "cache_read": 0.075
111
+ },
107
112
  "glm-5.2": {
108
113
  "input": 1.4,
109
114
  "output": 4.4,
@@ -261,6 +266,10 @@ export const CURSOR_MODEL_CONTEXTS = {
261
266
  "maxContext": 200000,
262
267
  "maxContextForMaxMode": 1000000
263
268
  },
269
+ "gemini-3.7-flash": {
270
+ "maxContext": 200000,
271
+ "maxContextForMaxMode": 1000000
272
+ },
264
273
  "glm-5.2": {
265
274
  "maxContext": 200000
266
275
  },
@@ -369,6 +378,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
369
378
  "gemini-3.6-flash": {
370
379
  "supportsImages": true
371
380
  },
381
+ "gemini-3.7-flash": {
382
+ "supportsImages": true
383
+ },
372
384
  "glm-5.2": {
373
385
  "supportsImages": false
374
386
  },