cursor-opencode-provider 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/context/agents.d.ts +1 -1
- package/dist/context/agents.js +1 -1
- package/dist/context/auth-store.js +2 -2
- package/dist/context/build.d.ts +3 -1
- package/dist/context/build.js +4 -2
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/paths.d.ts +34 -53
- package/dist/context/paths.js +54 -118
- package/dist/context/plugins.js +1 -0
- package/dist/context/rules.js +2 -1
- package/dist/context/skills.js +1 -0
- package/dist/image-save-tool.d.ts +19 -0
- package/dist/image-save-tool.js +25 -0
- package/dist/image-save.d.ts +58 -0
- package/dist/image-save.js +189 -0
- package/dist/image-staging.d.ts +59 -0
- package/dist/image-staging.js +91 -0
- package/dist/index.d.ts +1 -1
- package/dist/language-model.d.ts +3 -2
- package/dist/language-model.js +589 -41
- package/dist/plan-execution-kickoff.d.ts +55 -0
- package/dist/plan-execution-kickoff.js +162 -0
- package/dist/plugin-opencode2.js +52 -3
- package/dist/plugin-v2.d.ts +11 -9
- package/dist/plugin-v2.js +6 -13
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +61 -5
- package/dist/pricing-data.d.ts +16 -4
- package/dist/pricing-data.js +16 -4
- package/dist/protocol/ask-question.d.ts +133 -0
- package/dist/protocol/ask-question.js +320 -0
- package/dist/protocol/conversation-bind.d.ts +1 -0
- package/dist/protocol/conversation-bind.js +3 -0
- package/dist/protocol/create-plan.d.ts +141 -0
- package/dist/protocol/create-plan.js +373 -0
- package/dist/protocol/generate-image.d.ts +54 -0
- package/dist/protocol/generate-image.js +101 -0
- package/dist/protocol/interactions.d.ts +110 -8
- package/dist/protocol/interactions.js +295 -35
- package/dist/protocol/messages.js +118 -6
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/switch-mode.d.ts +178 -0
- package/dist/protocol/switch-mode.js +417 -0
- package/dist/protocol/tool-call-bridge.js +10 -5
- package/dist/protocol/tools.d.ts +27 -11
- package/dist/protocol/tools.js +98 -42
- package/dist/usage.d.ts +2 -2
- package/dist/usage.js +2 -2
- package/dist/web-search-tool.d.ts +22 -26
- package/dist/web-search-tool.js +25 -15
- 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
|
+
}
|
package/dist/plugin-opencode2.js
CHANGED
|
@@ -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 {
|
|
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
|
}
|
package/dist/plugin-v2.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
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:
|
|
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) {
|
package/dist/pricing-data.d.ts
CHANGED
|
@@ -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:
|
|
78
|
-
readonly output:
|
|
79
|
-
readonly cache_read: 0.
|
|
80
|
-
readonly cache_write:
|
|
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
|
};
|
package/dist/pricing-data.js
CHANGED
|
@@ -74,10 +74,10 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
74
74
|
"cache_write": 3.75
|
|
75
75
|
},
|
|
76
76
|
"claude-sonnet-5": {
|
|
77
|
-
"input":
|
|
78
|
-
"output":
|
|
79
|
-
"cache_read": 0.
|
|
80
|
-
"cache_write":
|
|
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
|
},
|