cursor-opencode-provider 0.2.7 → 0.2.9

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.
@@ -0,0 +1,126 @@
1
+ import { trace } from "./debug.js";
2
+ import { readAllFieldsStrict } from "./protocol/struct.js";
3
+ export class AttemptReplaySafety {
4
+ sessionId;
5
+ barrierReason;
6
+ constructor(sessionId) {
7
+ this.sessionId = sessionId;
8
+ }
9
+ markBarrier(reason) {
10
+ if (this.barrierReason)
11
+ return;
12
+ this.barrierReason = reason;
13
+ trace(`replay barrier: reason=${reason} sessionId=${this.sessionId}`);
14
+ }
15
+ applyTo(failure) {
16
+ failure.replaySafe = this.barrierReason === undefined && failure.replaySafe;
17
+ if (this.barrierReason) {
18
+ trace(`replay suppressed: reason=${this.barrierReason} sessionId=${this.sessionId}`);
19
+ }
20
+ return failure;
21
+ }
22
+ }
23
+ const INTERACTION_UPDATE_FIELDS = new Set([1, 2, 3, 4, 7, 13, 14, 16, 17]);
24
+ const INTERACTION_QUERY_FIELDS = new Set([2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14]);
25
+ const TOP_LEVEL_FIELDS = new Set([1, 2, 3, 4, 5, 7]);
26
+ function nestedFields(topLevel, field) {
27
+ if (topLevel?.fn !== field || topLevel.wt !== 2 || !topLevel.bytes)
28
+ return [];
29
+ return readAllFieldsStrict(topLevel.bytes) ?? [];
30
+ }
31
+ function analyzeExecWire(topLevel) {
32
+ const variants = nestedFields(topLevel, 2)
33
+ .filter((field) => ![1, 15, 19].includes(field.fn));
34
+ const exactVariant = (field) => variants.length === 1 && variants[0].fn === field && variants[0].wt === 2;
35
+ return {
36
+ exactRequestContext: exactVariant(10),
37
+ exactMcpState: exactVariant(36),
38
+ };
39
+ }
40
+ function validKvWire(topLevel) {
41
+ const variants = nestedFields(topLevel, 4).filter((field) => field.fn !== 1);
42
+ const variant = variants.length === 1 ? variants[0] : undefined;
43
+ const args = variant?.wt === 2 && variant.bytes
44
+ ? (readAllFieldsStrict(variant.bytes) ?? [])
45
+ : [];
46
+ const blobIds = args.filter((field) => field.fn === 1 && field.wt === 2 && (field.bytes?.length ?? 0) > 0);
47
+ return !!variant
48
+ && [2, 3].includes(variant.fn)
49
+ && variant.wt === 2
50
+ && blobIds.length === 1
51
+ && args.every((field) => field.wt === 2 && (field.fn === 1 || (variant.fn === 3 && field.fn === 2)));
52
+ }
53
+ function validInteractionUpdateWire(topLevel, decoded) {
54
+ if (!decoded)
55
+ return true;
56
+ const fields = nestedFields(topLevel, 1);
57
+ const update = fields.length === 1 ? fields[0] : undefined;
58
+ if (!update || update.wt !== 2 || !INTERACTION_UPDATE_FIELDS.has(update.fn))
59
+ return false;
60
+ if (![1, 4].includes(update.fn))
61
+ return true;
62
+ const delta = update.bytes ? (readAllFieldsStrict(update.bytes) ?? []) : [];
63
+ return delta.length === 1 && delta[0].fn === 1 && delta[0].wt === 2;
64
+ }
65
+ function validInteractionQueryWire(topLevel, decoded) {
66
+ if (!decoded)
67
+ return true;
68
+ const fields = nestedFields(topLevel, 7);
69
+ const ids = fields.filter((field) => field.fn === 1);
70
+ const variants = fields.filter((field) => field.fn !== 1);
71
+ return ids.length <= 1
72
+ && ids.every((field) => field.wt === 0)
73
+ && variants.length === 1
74
+ && variants[0].wt === 2
75
+ && INTERACTION_QUERY_FIELDS.has(variants[0].fn);
76
+ }
77
+ function decodedMatchesWire(topLevel, decoded) {
78
+ return !((topLevel?.fn === 1 && !decoded.interactionUpdate)
79
+ || (topLevel?.fn === 2 && !decoded.exec)
80
+ || (topLevel?.fn === 4 && !decoded.kv)
81
+ || (topLevel?.fn === 5 && !decoded.execControl)
82
+ || (topLevel?.fn === 7 && !decoded.interactionQuery));
83
+ }
84
+ function hasSemanticProgress(decoded) {
85
+ const update = decoded.interactionUpdate;
86
+ const text = update?.text_delta?.text;
87
+ const thinking = update?.thinking_delta?.text;
88
+ return (typeof text === "string" && text.length > 0)
89
+ || (typeof thinking === "string" && thinking.length > 0)
90
+ || !!update?.turn_ended
91
+ || !!update?.tool_call_started
92
+ || !!update?.tool_call_completed
93
+ || !!decoded.exec
94
+ || !!decoded.kv
95
+ || !!decoded.execControl
96
+ || !!decoded.interactionQuery
97
+ || !!decoded.checkpointBytes?.length;
98
+ }
99
+ /** Classify one decoded server frame without performing any protocol side effects. */
100
+ export function analyzeReplayFrame(payload, decoded) {
101
+ const topLevelFields = readAllFieldsStrict(payload) ?? [];
102
+ const topLevel = topLevelFields.length === 1 ? topLevelFields[0] : undefined;
103
+ const exec = analyzeExecWire(topLevel);
104
+ const validKv = validKvWire(topLevel);
105
+ const malformed = !topLevel
106
+ || topLevel.wt !== 2
107
+ || !TOP_LEVEL_FIELDS.has(topLevel.fn)
108
+ || !validInteractionUpdateWire(topLevel, decoded.interactionUpdate)
109
+ || !validInteractionQueryWire(topLevel, decoded.interactionQuery)
110
+ || !decodedMatchesWire(topLevel, decoded)
111
+ || !!decoded.execControl;
112
+ let barrier;
113
+ if (decoded.interactionUpdate?.tool_call_started || decoded.interactionUpdate?.tool_call_completed) {
114
+ barrier = "display-tool-lifecycle";
115
+ }
116
+ else if (malformed || (topLevel.fn === 4 && !validKv)) {
117
+ barrier = "unknown-or-malformed-frame";
118
+ }
119
+ else if (topLevel.fn === 2 && !exec.exactRequestContext && !exec.exactMcpState) {
120
+ barrier = "non-control-exec";
121
+ }
122
+ return {
123
+ semanticProgress: hasSemanticProgress(decoded),
124
+ barrier,
125
+ };
126
+ }
package/dist/session.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { BidiStream } from "./transport/connect.js";
2
2
  import { type CursorProviderError } from "./errors.js";
3
3
  import { type SessionActivitySource } from "./activity.js";
4
+ import type { HostSubagentCatalog, ToolAliasRegistry } from "./protocol/tools.js";
4
5
  export type Frame = {
5
6
  flags: number;
6
7
  payload: Uint8Array;
@@ -117,6 +118,10 @@ export type CursorSession = {
117
118
  blobs: Map<string, Uint8Array>;
118
119
  /** McpToolDefinition list advertised this turn — echoed into the request_context reply. */
119
120
  toolDescriptors: Array<Record<string, unknown>>;
121
+ /** Cursor-facing web alias → exact executable host tool for this Run. */
122
+ toolAliases?: ToolAliasRegistry;
123
+ /** Spawnable host agents extracted from this turn's Task/Actor definition. */
124
+ subagentCatalog?: HostSubagentCatalog;
120
125
  /** Full RequestContext for exec #10 replies. */
121
126
  requestContext: Record<string, unknown>;
122
127
  /**
@@ -5,6 +5,7 @@ export declare function unaryAvailableModels(token: string, options?: {
5
5
  apiBaseURL?: string;
6
6
  baseURL?: string;
7
7
  headers?: Record<string, string>;
8
+ timeoutMs?: number;
8
9
  }): Promise<Record<string, unknown>>;
9
10
  /**
10
11
  * Cursor Run stream hosts from GetServerConfig / agentBaseURL.
@@ -30,6 +31,7 @@ export declare function fetchAgentUrl(token: string, options?: {
30
31
  apiBaseURL?: string;
31
32
  baseURL?: string;
32
33
  telemetryEnabled?: boolean;
34
+ timeoutMs?: number;
33
35
  }): Promise<string>;
34
36
  export type BidiStream = {
35
37
  write(msg: Uint8Array): boolean | void;
@@ -5,8 +5,25 @@ import { getDeviceIds } from "../protocol/device-id.js";
5
5
  import { resolveClientVersion } from "../protocol/client-version.js";
6
6
  import { trace } from "../debug.js";
7
7
  import { CursorLocalCancellationError, CursorProtocolError, CursorTransportError, cursorGrpcError, cursorHttpError, errorCode, CursorProviderError, } from "../errors.js";
8
+ import { withAbortDeadline } from "../deadline.js";
8
9
  import http2 from "node:http2";
9
10
  const API_BASE = `https://${CURSOR_API_HOST}`;
11
+ const DEFAULT_UNARY_TIMEOUT_MS = 5_000;
12
+ const MAX_TIMER_MS = 2_147_483_647;
13
+ function unaryTimeoutMs(operation, value) {
14
+ const timeoutMs = value ?? DEFAULT_UNARY_TIMEOUT_MS;
15
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_MS) {
16
+ throw new CursorProtocolError(`${operation} timeout must be a positive integer no greater than ${MAX_TIMER_MS}`, { code: "CURSOR_INVALID_TIMEOUT" });
17
+ }
18
+ return timeoutMs;
19
+ }
20
+ async function withUnaryDeadline(operation, timeoutMs, timeoutCode, run) {
21
+ return withAbortDeadline(timeoutMs, () => new CursorTransportError(`${operation} timed out after ${timeoutMs}ms`, {
22
+ transient: true,
23
+ replaySafe: true,
24
+ code: timeoutCode,
25
+ }), run);
26
+ }
10
27
  function resolveApiBaseURL(options) {
11
28
  return new URL(options.apiBaseURL ?? options.baseURL ?? API_BASE).origin;
12
29
  }
@@ -33,42 +50,46 @@ export function buildBaseHeaders(token, clientVersion, extra) {
33
50
  export async function unaryAvailableModels(token, options = {}) {
34
51
  const base = resolveApiBaseURL(options);
35
52
  const url = `${base}/aiserver.v1.AiService/AvailableModels`;
36
- const clientVersion = await resolveClientVersion();
37
- const headers = buildBaseHeaders(token, clientVersion, options.headers);
38
- let res;
39
- try {
40
- res = await fetch(url, {
41
- method: "POST",
42
- headers: {
43
- ...headers,
44
- "content-type": "application/json",
45
- accept: "application/json",
46
- },
47
- // Request parameterized effort/context/fast variants like Cursor IDE.
48
- body: JSON.stringify({
49
- includeLongContextModels: true,
50
- useModelParameters: true,
51
- useCloudAgentEffortModes: true,
52
- }),
53
- });
54
- }
55
- catch (cause) {
56
- throw new CursorTransportError("AvailableModels network request failed", {
57
- transient: true,
58
- replaySafe: true,
59
- code: errorCode(cause),
60
- cause,
61
- });
62
- }
63
- if (!res.ok) {
64
- throw await cursorHttpResponseError("AvailableModels failed:", res);
65
- }
66
- try {
67
- return (await res.json());
68
- }
69
- catch (cause) {
70
- throw new CursorProtocolError("AvailableModels returned malformed JSON", { cause });
71
- }
53
+ const timeoutMs = unaryTimeoutMs("AvailableModels", options.timeoutMs);
54
+ return withUnaryDeadline("AvailableModels", timeoutMs, "CURSOR_AVAILABLE_MODELS_TIMEOUT", async (signal) => {
55
+ const clientVersion = await resolveClientVersion();
56
+ const headers = buildBaseHeaders(token, clientVersion, options.headers);
57
+ let res;
58
+ try {
59
+ res = await fetch(url, {
60
+ method: "POST",
61
+ headers: {
62
+ ...headers,
63
+ "content-type": "application/json",
64
+ accept: "application/json",
65
+ },
66
+ // Request parameterized effort/context/fast variants like Cursor IDE.
67
+ body: JSON.stringify({
68
+ includeLongContextModels: true,
69
+ useModelParameters: true,
70
+ useCloudAgentEffortModes: true,
71
+ }),
72
+ signal,
73
+ });
74
+ }
75
+ catch (cause) {
76
+ throw new CursorTransportError("AvailableModels network request failed", {
77
+ transient: true,
78
+ replaySafe: true,
79
+ code: errorCode(cause),
80
+ cause,
81
+ });
82
+ }
83
+ if (!res.ok) {
84
+ throw await cursorHttpResponseError("AvailableModels failed:", res);
85
+ }
86
+ try {
87
+ return (await res.json());
88
+ }
89
+ catch (cause) {
90
+ throw new CursorProtocolError("AvailableModels returned malformed JSON", { cause });
91
+ }
92
+ });
72
93
  }
73
94
  // ── Unary (GetServerConfig) ──
74
95
  /**
@@ -116,51 +137,55 @@ export function normalizeAgentRunOrigin(raw) {
116
137
  export async function fetchAgentUrl(token, options = {}) {
117
138
  const base = resolveApiBaseURL(options);
118
139
  const url = `${base}${SERVER_CONFIG_PATH}`;
119
- const clientVersion = await resolveClientVersion();
120
- // Match Cursor CLI: GetServerConfig uses base headers only session headers belong on Run.
121
- const headers = buildBaseHeaders(token, clientVersion);
122
- trace(`GetServerConfig POST ${url}`);
123
- let res;
124
- try {
125
- res = await fetch(url, {
126
- method: "POST",
127
- headers: {
128
- ...headers,
129
- "content-type": "application/json",
130
- accept: "application/json",
131
- },
132
- body: JSON.stringify({ telem_enabled: options.telemetryEnabled ?? false }),
133
- });
134
- }
135
- catch (cause) {
136
- throw new CursorTransportError("GetServerConfig network request failed", {
137
- transient: true,
138
- replaySafe: true,
139
- code: errorCode(cause),
140
- cause,
141
- });
142
- }
143
- if (!res.ok) {
144
- throw await cursorHttpResponseError("GetServerConfig failed:", res);
145
- }
146
- let body;
147
- try {
148
- body = (await res.json());
149
- }
150
- catch (cause) {
151
- throw new CursorProtocolError("GetServerConfig returned malformed JSON", { cause });
152
- }
153
- const cfg = body.agentUrlConfig;
154
- const raw = cfg?.agentnUrl || cfg?.agentUrl;
155
- const normalized = normalizeAgentRunOrigin(raw);
156
- trace(`GetServerConfig reply: agentnUrl=${cfg?.agentnUrl ?? "<missing>"} ` +
157
- `agentUrl=${cfg?.agentUrl ?? "<missing>"} ${normalized ?? "<invalid>"}`);
158
- if (!normalized) {
159
- throw new CursorProtocolError(raw
160
- ? "GetServerConfig returned an invalid Cursor agent URL"
161
- : "GetServerConfig response missing agentUrlConfig.agentnUrl");
162
- }
163
- return normalized;
140
+ const timeoutMs = unaryTimeoutMs("GetServerConfig", options.timeoutMs);
141
+ return withUnaryDeadline("GetServerConfig", timeoutMs, "CURSOR_AGENT_URL_TIMEOUT", async (signal) => {
142
+ const clientVersion = await resolveClientVersion();
143
+ // Match Cursor CLI: GetServerConfig uses base headers only — session headers belong on Run.
144
+ const headers = buildBaseHeaders(token, clientVersion);
145
+ trace(`GetServerConfig POST ${url}`);
146
+ let res;
147
+ try {
148
+ res = await fetch(url, {
149
+ method: "POST",
150
+ headers: {
151
+ ...headers,
152
+ "content-type": "application/json",
153
+ accept: "application/json",
154
+ },
155
+ body: JSON.stringify({ telem_enabled: options.telemetryEnabled ?? false }),
156
+ signal,
157
+ });
158
+ }
159
+ catch (cause) {
160
+ throw new CursorTransportError("GetServerConfig network request failed", {
161
+ transient: true,
162
+ replaySafe: true,
163
+ code: errorCode(cause),
164
+ cause,
165
+ });
166
+ }
167
+ if (!res.ok) {
168
+ throw await cursorHttpResponseError("GetServerConfig failed:", res);
169
+ }
170
+ let body;
171
+ try {
172
+ body = (await res.json());
173
+ }
174
+ catch (cause) {
175
+ throw new CursorProtocolError("GetServerConfig returned malformed JSON", { cause });
176
+ }
177
+ const cfg = body.agentUrlConfig;
178
+ const raw = cfg?.agentnUrl || cfg?.agentUrl;
179
+ const normalized = normalizeAgentRunOrigin(raw);
180
+ trace(`GetServerConfig reply: agentnUrl=${cfg?.agentnUrl ?? "<missing>"} ` +
181
+ `agentUrl=${cfg?.agentUrl ?? "<missing>"} ${normalized ?? "<invalid>"}`);
182
+ if (!normalized) {
183
+ throw new CursorProtocolError(raw
184
+ ? "GetServerConfig returned an invalid Cursor agent URL"
185
+ : "GetServerConfig response missing agentUrlConfig.agentnUrl");
186
+ }
187
+ return normalized;
188
+ });
164
189
  }
165
190
  export class CursorRunInterruptedError extends CursorTransportError {
166
191
  constructor(message = "Cursor Run ended before turn_ended", options) {
@@ -217,7 +242,6 @@ const CONNECT_TIMEOUT_MS = 15_000;
217
242
  const DEFAULT_SESSION_PING_TIMEOUT_MS = 5_000;
218
243
  const DEFAULT_READ_IDLE_MS = 120_000;
219
244
  const WRITE_DRAIN_TIMEOUT_CODE = "CURSOR_WRITE_DRAIN_TIMEOUT";
220
- const MAX_TIMER_MS = 2_147_483_647;
221
245
  export const HTTP2_SESSION_MAX_AGE_MS = 15 * 60_000;
222
246
  export function shouldReuseHttp2Session(state, createdAt, now = Date.now()) {
223
247
  return !state.destroyed && !state.closed && now - createdAt < HTTP2_SESSION_MAX_AGE_MS;
@@ -0,0 +1,34 @@
1
+ import { type ToolContext, type ToolResult } from "@opencode-ai/plugin";
2
+ export type OpenCodeWebSearchArgs = {
3
+ query: string;
4
+ numResults?: number;
5
+ livecrawl?: "fallback" | "preferred";
6
+ type?: "auto" | "fast" | "deep";
7
+ contextMaxCharacters?: number;
8
+ };
9
+ export declare function parseOpenCodeWebSearchResponse(raw: string): string | undefined;
10
+ export declare function executeOpenCodeWebSearch(args: OpenCodeWebSearchArgs, context: ToolContext, fetchImpl?: typeof fetch): Promise<ToolResult>;
11
+ export declare const openCodeWebSearchTool: {
12
+ description: string;
13
+ args: {
14
+ query: import("zod").ZodString;
15
+ numResults: import("zod").ZodOptional<import("zod").ZodNumber>;
16
+ livecrawl: import("zod").ZodOptional<import("zod").ZodEnum<{
17
+ fallback: "fallback";
18
+ preferred: "preferred";
19
+ }>>;
20
+ type: import("zod").ZodOptional<import("zod").ZodEnum<{
21
+ fast: "fast";
22
+ auto: "auto";
23
+ deep: "deep";
24
+ }>>;
25
+ contextMaxCharacters: import("zod").ZodOptional<import("zod").ZodNumber>;
26
+ };
27
+ execute(args: {
28
+ query: string;
29
+ numResults?: number | undefined;
30
+ livecrawl?: "fallback" | "preferred" | undefined;
31
+ type?: "fast" | "auto" | "deep" | undefined;
32
+ contextMaxCharacters?: number | undefined;
33
+ }, context: ToolContext): Promise<ToolResult>;
34
+ };
@@ -0,0 +1,127 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
3
+ const WEB_SEARCH_TIMEOUT_MS = 25_000;
4
+ function exaMcpUrl() {
5
+ const apiKey = process.env.EXA_API_KEY;
6
+ if (!apiKey)
7
+ return EXA_MCP_URL;
8
+ const url = new URL(EXA_MCP_URL);
9
+ url.searchParams.set("exaApiKey", apiKey);
10
+ return url.href;
11
+ }
12
+ function mcpText(value) {
13
+ if (!value || typeof value !== "object")
14
+ return undefined;
15
+ const result = value.result;
16
+ if (!result || typeof result !== "object")
17
+ return undefined;
18
+ const content = result.content;
19
+ if (!Array.isArray(content))
20
+ return undefined;
21
+ for (const item of content) {
22
+ if (item &&
23
+ typeof item === "object" &&
24
+ item.type === "text" &&
25
+ typeof item.text === "string") {
26
+ return item.text;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+ export function parseOpenCodeWebSearchResponse(raw) {
32
+ const trimmed = raw.trim();
33
+ if (!trimmed)
34
+ return undefined;
35
+ try {
36
+ const text = mcpText(JSON.parse(trimmed));
37
+ if (text)
38
+ return text;
39
+ }
40
+ catch {
41
+ // MCP may respond as an SSE stream instead of one JSON object.
42
+ }
43
+ for (const line of raw.split("\n")) {
44
+ if (!line.startsWith("data: "))
45
+ continue;
46
+ try {
47
+ const text = mcpText(JSON.parse(line.slice(6)));
48
+ if (text)
49
+ return text;
50
+ }
51
+ catch {
52
+ // Ignore non-JSON SSE events.
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ export async function executeOpenCodeWebSearch(args, context, fetchImpl = fetch) {
58
+ await context.ask({
59
+ permission: "websearch",
60
+ patterns: [args.query],
61
+ always: ["*"],
62
+ metadata: {
63
+ query: args.query,
64
+ numResults: args.numResults,
65
+ livecrawl: args.livecrawl,
66
+ type: args.type,
67
+ contextMaxCharacters: args.contextMaxCharacters,
68
+ provider: "exa",
69
+ },
70
+ });
71
+ const controller = new AbortController();
72
+ const abort = () => controller.abort(context.abort.reason);
73
+ if (context.abort.aborted)
74
+ abort();
75
+ else
76
+ context.abort.addEventListener("abort", abort, { once: true });
77
+ const timeout = setTimeout(() => controller.abort(new Error("Web search timed out")), WEB_SEARCH_TIMEOUT_MS);
78
+ try {
79
+ const response = await fetchImpl(exaMcpUrl(), {
80
+ method: "POST",
81
+ headers: {
82
+ accept: "application/json, text/event-stream",
83
+ "content-type": "application/json",
84
+ },
85
+ body: JSON.stringify({
86
+ jsonrpc: "2.0",
87
+ id: 1,
88
+ method: "tools/call",
89
+ params: {
90
+ name: "web_search_exa",
91
+ arguments: {
92
+ query: args.query,
93
+ type: args.type ?? "auto",
94
+ numResults: args.numResults ?? 8,
95
+ livecrawl: args.livecrawl ?? "fallback",
96
+ contextMaxCharacters: args.contextMaxCharacters,
97
+ },
98
+ },
99
+ }),
100
+ signal: controller.signal,
101
+ });
102
+ const raw = await response.text();
103
+ if (!response.ok)
104
+ throw new Error(`Web search failed (${response.status}): ${raw.slice(0, 500)}`);
105
+ const output = parseOpenCodeWebSearchResponse(raw) ?? "No search results found. Please try a different query.";
106
+ return {
107
+ title: `Exa Web Search: ${args.query}`,
108
+ output,
109
+ metadata: { provider: "exa" },
110
+ };
111
+ }
112
+ finally {
113
+ clearTimeout(timeout);
114
+ context.abort.removeEventListener("abort", abort);
115
+ }
116
+ }
117
+ export const openCodeWebSearchTool = tool({
118
+ description: "Search the web for current information using OpenCode's web search backend.",
119
+ args: {
120
+ query: tool.schema.string().describe("Web search query"),
121
+ numResults: tool.schema.number().int().min(1).max(20).optional(),
122
+ livecrawl: tool.schema.enum(["fallback", "preferred"]).optional(),
123
+ type: tool.schema.enum(["auto", "fast", "deep"]).optional(),
124
+ contextMaxCharacters: tool.schema.number().int().positive().optional(),
125
+ },
126
+ execute: executeOpenCodeWebSearch,
127
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -75,4 +75,4 @@
75
75
  "@types/node": "^22.15.3",
76
76
  "typescript": "^5.8.3"
77
77
  }
78
- }
78
+ }