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.
Files changed (53) hide show
  1. package/README.md +22 -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/plugins.js +1 -0
  12. package/dist/context/rules.js +2 -1
  13. package/dist/context/skills.js +1 -0
  14. package/dist/image-save-tool.d.ts +19 -0
  15. package/dist/image-save-tool.js +25 -0
  16. package/dist/image-save.d.ts +58 -0
  17. package/dist/image-save.js +189 -0
  18. package/dist/image-staging.d.ts +59 -0
  19. package/dist/image-staging.js +91 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/language-model.d.ts +3 -2
  22. package/dist/language-model.js +589 -41
  23. package/dist/plan-execution-kickoff.d.ts +55 -0
  24. package/dist/plan-execution-kickoff.js +162 -0
  25. package/dist/plugin-opencode2.js +52 -3
  26. package/dist/plugin-v2.d.ts +11 -9
  27. package/dist/plugin-v2.js +6 -13
  28. package/dist/plugin.d.ts +9 -0
  29. package/dist/plugin.js +61 -5
  30. package/dist/pricing-data.d.ts +16 -4
  31. package/dist/pricing-data.js +16 -4
  32. package/dist/protocol/ask-question.d.ts +133 -0
  33. package/dist/protocol/ask-question.js +320 -0
  34. package/dist/protocol/conversation-bind.d.ts +1 -0
  35. package/dist/protocol/conversation-bind.js +3 -0
  36. package/dist/protocol/create-plan.d.ts +141 -0
  37. package/dist/protocol/create-plan.js +373 -0
  38. package/dist/protocol/generate-image.d.ts +54 -0
  39. package/dist/protocol/generate-image.js +101 -0
  40. package/dist/protocol/interactions.d.ts +110 -8
  41. package/dist/protocol/interactions.js +295 -35
  42. package/dist/protocol/messages.js +118 -6
  43. package/dist/protocol/struct.js +1 -1
  44. package/dist/protocol/switch-mode.d.ts +178 -0
  45. package/dist/protocol/switch-mode.js +417 -0
  46. package/dist/protocol/tool-call-bridge.js +10 -5
  47. package/dist/protocol/tools.d.ts +27 -11
  48. package/dist/protocol/tools.js +98 -42
  49. package/dist/usage.d.ts +2 -2
  50. package/dist/usage.js +2 -2
  51. package/dist/web-search-tool.d.ts +22 -26
  52. package/dist/web-search-tool.js +25 -15
  53. package/package.json +7 -4
@@ -71,9 +71,9 @@ export type HostSubagentDefinition = {
71
71
  description?: string;
72
72
  };
73
73
  export type HostSubagentCatalog = {
74
- executor?: "task" | "actor";
74
+ executor?: "task";
75
75
  agents: HostSubagentDefinition[];
76
- /** True when the host tool supplied its complete, permission-filtered catalog. */
76
+ /** True when the OpenCode task tool supplied its complete, permission-filtered catalog. */
77
77
  complete: boolean;
78
78
  };
79
79
  /** Extract the current host's permission-filtered Task/Actor recipient catalog. */
@@ -151,7 +151,28 @@ export declare function parseExecServerMessage(msg: Record<string, unknown>): Pa
151
151
  export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<string, unknown>, execVariant?: string): {
152
152
  toolName: string;
153
153
  args: Record<string, unknown>;
154
+ binaryBytes?: Uint8Array;
154
155
  };
156
+ /**
157
+ * Decode `WriteArgs.file_bytes`. `encoding_hint` names the file's original
158
+ * encoding; OpenCode's `write` takes a string, so an unknown encoding label
159
+ * falls back to UTF-8 rather than dropping the write.
160
+ *
161
+ * Returns undefined when the bytes are not text in that encoding. A non-fatal
162
+ * decode would replace every undecodable sequence with U+FFFD and hand the host
163
+ * a corrupted file that still reports success, so binary content must take the
164
+ * byte-preserving path instead (see `isBinaryWriteRequest`).
165
+ */
166
+ export declare function decodeWriteBytes(bytes: Uint8Array, encodingHint?: string): string | undefined;
167
+ /**
168
+ * True when a parsed `write` request carries bytes that are not text. Such a
169
+ * request cannot go to OpenCode's `write` tool at all — it takes a string, and
170
+ * additionally BOM-splits, diffs, and runs `Format.file()` on what it writes.
171
+ */
172
+ export declare function binaryWritePayload(parsed: ParsedExecRequest): {
173
+ path: string;
174
+ data: Uint8Array;
175
+ } | undefined;
155
176
  /**
156
177
  * Map Cursor McpArgs back to the OpenCode tool id.
157
178
  * Prefers provider_identifier + bare tool_name (github + create_pull_request
@@ -162,15 +183,10 @@ export declare function mcpRealToolName(mcpArgs: Record<string, unknown>, defaul
162
183
  * A read target addressed by URI scheme rather than by local filesystem path.
163
184
  *
164
185
  * Cursor's LocalReadExecutor only ever reads local files, so this provider
165
- * mirrors its pre-execution validation. Host read tools are not so limited:
166
- * oh-my-pi's `read` accepts "a path, URL, or internal URI", and mounts every
167
- * discoverable tool — including all MCP server tools — under `xd://<tool>`
168
- * device URLs that are read for their schema and written to for execution
169
- * (`tools.xdev`, on by default). Resolving those against the workspace root
170
- * mangles `xd://everything_echo` into `<root>/xd:/everything_echo`, which stats
171
- * as missing and gets refused as `file_not_found` — so the host's read tool is
172
- * never called and every xd://-mounted MCP tool is unreachable through this
173
- * provider. Such targets must be forwarded untouched and left to the host.
186
+ * mirrors its pre-execution validation. An advertised host read tool may also
187
+ * accept URLs or internal URI schemes. Resolving those against the workspace
188
+ * root mangles the URI and produces a false `file_not_found`, so URI targets
189
+ * are forwarded untouched and left to the advertised executor.
174
190
  *
175
191
  * Requires a scheme of two or more characters followed by `://`, so Windows
176
192
  * drive letters (`C:\src`, `C:/src`) stay local paths.
@@ -57,7 +57,9 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
57
57
  * reconstructed in `mcpRealToolName`.
58
58
  */
59
59
  export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
60
- return tools.map((t) => {
60
+ return [...tools]
61
+ .sort((left, right) => left.name.localeCompare(right.name))
62
+ .map((t) => {
61
63
  const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
62
64
  const collisionSafeAlias = COLLISION_SAFE_ALIASES.has(t.name);
63
65
  return {
@@ -299,6 +301,10 @@ const PRESERVE_EMPTY_STRING_KEYS = new Set([
299
301
  "file_text",
300
302
  "fileText",
301
303
  "stream_content",
304
+ // Some advertised edit schemas use a single textual `input` payload. Preserve
305
+ // an empty value so the executor reports the real edit error rather than a
306
+ // misleading missing-property error.
307
+ "input",
302
308
  "oldString",
303
309
  "old_string",
304
310
  "newString",
@@ -396,18 +402,16 @@ function subagentTypeEnumValues(schema) {
396
402
  }
397
403
  /** Extract the current host's permission-filtered Task/Actor recipient catalog. */
398
404
  export function extractHostSubagentCatalog(tools) {
399
- const executorTool = tools.find((tool) => tool.name === "actor") ??
400
- tools.find((tool) => tool.name === "task");
405
+ const executorTool = tools.find((tool) => tool.name === "task");
401
406
  if (!executorTool)
402
407
  return { agents: [], complete: true };
403
408
  const described = parseSubagentDescriptionCatalog(executorTool.description);
404
409
  const enumNames = subagentTypeEnumValues(executorTool.inputSchema);
405
410
  const descriptions = new Map(described.agents.map((agent) => [agent.name, agent.description]));
406
- // MiMo's Actor enum is stricter than its prose catalog (`mode: subagent`
407
- // rather than every non-primary agent), so structured names are authoritative.
411
+ // A structured subagent_type enum is stricter than prose, so it is authoritative.
408
412
  const names = new Set(enumNames.length > 0 ? enumNames : described.agents.map((agent) => agent.name));
409
413
  return {
410
- executor: executorTool.name,
414
+ executor: "task",
411
415
  agents: [...names].map((name) => ({
412
416
  name,
413
417
  ...(descriptions.get(name) ? { description: descriptions.get(name) } : {}),
@@ -467,9 +471,9 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
467
471
  if (parsed.resultField !== "subagent_result" || parsed.toolName !== "task")
468
472
  return;
469
473
  const advertised = new Set(advertisedToolNames);
470
- const executor = advertised.has("actor") ? "actor" : advertised.has("task") ? "task" : undefined;
471
- if (!executor)
474
+ if (!advertised.has("task"))
472
475
  return;
476
+ const executor = "task";
473
477
  const description = str(parsed.args.description) ?? "";
474
478
  const prompt = str(parsed.args.prompt) ?? "";
475
479
  const cursorSubagentType = str(parsed.resultMetadata?.cursor_subagent_type) ??
@@ -484,24 +488,12 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
484
488
  }
485
489
  const resumeAgentId = str(parsed.args.task_id);
486
490
  parsed.toolName = executor;
487
- if (executor === "task") {
488
- parsed.args = {
489
- description,
490
- prompt,
491
- subagent_type: subagentType,
492
- ...(resumeAgentId ? { task_id: resumeAgentId } : {}),
493
- ...(parsed.args.background === true ? { background: true } : {}),
494
- };
495
- return;
496
- }
497
491
  parsed.args = {
498
- operation: {
499
- action: parsed.args.background === true ? "spawn" : "run",
500
- description,
501
- prompt,
502
- subagent_type: subagentType,
503
- ...(resumeAgentId ? { actor_id: resumeAgentId } : {}),
504
- },
492
+ description,
493
+ prompt,
494
+ subagent_type: subagentType,
495
+ ...(resumeAgentId ? { task_id: resumeAgentId } : {}),
496
+ ...(parsed.args.background === true ? { background: true } : {}),
505
497
  };
506
498
  }
507
499
  /** Matches Cursor's own pre-write read threshold (`local-exec` 52428800). */
@@ -682,6 +674,12 @@ export function remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNames,
682
674
  export function remapEditToolsForCatalog(parsed, advertisedToolNames, workspaceRoot) {
683
675
  if (parsed.toolName !== "write" && parsed.toolName !== "edit")
684
676
  return;
677
+ // `apply_patch` envelopes are text. A binary write has no patch form at all,
678
+ // so it must stay a write and reach the byte-preserving path — converting it
679
+ // here would drop the bytes and report a patch-translation failure instead of
680
+ // the image Cursor asked to save.
681
+ if (binaryWritePayload(parsed))
682
+ return;
685
683
  const advertised = new Set(advertisedToolNames);
686
684
  if (advertised.has(parsed.toolName) || !advertised.has(APPLY_PATCH_TOOL))
687
685
  return;
@@ -940,7 +938,19 @@ export function parseExecServerMessage(msg) {
940
938
  toolName: mapped.toolName,
941
939
  args: mapped.args,
942
940
  resultField,
943
- ...(resultMetadata ? { resultMetadata } : {}),
941
+ ...(resultMetadata || mapped.binaryBytes
942
+ ? {
943
+ resultMetadata: {
944
+ ...resultMetadata,
945
+ ...(mapped.binaryBytes
946
+ ? {
947
+ binaryWriteBytes: mapped.binaryBytes,
948
+ path: str(rawArgs.path) ?? str(rawArgs.file_path) ?? "",
949
+ }
950
+ : {}),
951
+ },
952
+ }
953
+ : {}),
944
954
  };
945
955
  }
946
956
  function shellStreamResultMetadata(raw) {
@@ -1020,14 +1030,31 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
1020
1030
  // non-empty and only falls back to `file_text`; mirror that order so a
1021
1031
  // byte-encoded write is not silently seen as empty content.
1022
1032
  const bytes = bytesValue(cleaned.file_bytes) ?? bytesValue(cleaned.fileBytes);
1023
- const content = bytes !== undefined
1024
- ? decodeWriteBytes(bytes, str(cleaned.encoding_hint) ?? str(cleaned.encodingHint))
1025
- : stringValue(cleaned.content) ?? stringValue(cleaned.file_text) ?? stringValue(cleaned.fileText);
1033
+ let content;
1034
+ if (bytes !== undefined) {
1035
+ content = decodeWriteBytes(bytes, str(cleaned.encoding_hint) ?? str(cleaned.encodingHint));
1036
+ if (content === undefined) {
1037
+ // Binary. Surface the raw bytes to the caller instead of a lossy
1038
+ // decode; `binaryWritePayload` picks them up after parsing.
1039
+ return { toolName: "write", args, binaryBytes: bytes };
1040
+ }
1041
+ }
1042
+ else {
1043
+ content = stringValue(cleaned.content)
1044
+ ?? stringValue(cleaned.file_text)
1045
+ ?? stringValue(cleaned.fileText);
1046
+ }
1026
1047
  if (content !== undefined)
1027
1048
  args.content = content;
1028
1049
  return { toolName: "write", args };
1029
1050
  }
1030
1051
  case "edit": {
1052
+ // Preserve an opaque textual edit payload when the advertised executor
1053
+ // accepts that generic shape; rebuilding only OpenCode's targeted fields
1054
+ // would silently turn a valid call into `{}`.
1055
+ if (typeof cleaned.input === "string") {
1056
+ return { toolName: "edit", args: { input: cleaned.input } };
1057
+ }
1031
1058
  const args = {};
1032
1059
  const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
1033
1060
  if (filePath)
@@ -1105,22 +1132,56 @@ function bytesValue(v) {
1105
1132
  }
1106
1133
  return undefined;
1107
1134
  }
1135
+ /** UTF-16/32 embed NUL bytes in ordinary text; 8-bit encodings do not. */
1136
+ const WIDE_TEXT_ENCODING = /^utf-?(16|32)/;
1108
1137
  /**
1109
1138
  * Decode `WriteArgs.file_bytes`. `encoding_hint` names the file's original
1110
- * encoding; OpenCode's `write` takes a string, so anything Node cannot decode
1139
+ * encoding; OpenCode's `write` takes a string, so an unknown encoding label
1111
1140
  * falls back to UTF-8 rather than dropping the write.
1141
+ *
1142
+ * Returns undefined when the bytes are not text in that encoding. A non-fatal
1143
+ * decode would replace every undecodable sequence with U+FFFD and hand the host
1144
+ * a corrupted file that still reports success, so binary content must take the
1145
+ * byte-preserving path instead (see `isBinaryWriteRequest`).
1112
1146
  */
1113
- function decodeWriteBytes(bytes, encodingHint) {
1147
+ export function decodeWriteBytes(bytes, encodingHint) {
1114
1148
  const encoding = encodingHint?.trim().toLowerCase().replace(/[_ ]/g, "-");
1149
+ let decoder;
1150
+ let label = "utf-8";
1115
1151
  if (encoding && encoding !== "utf-8" && encoding !== "utf8") {
1116
1152
  try {
1117
- return new TextDecoder(encoding, { fatal: false }).decode(bytes);
1153
+ decoder = new TextDecoder(encoding, { fatal: true });
1154
+ label = encoding;
1118
1155
  }
1119
1156
  catch {
1120
1157
  trace(`write: unsupported encoding_hint ${JSON.stringify(encodingHint)} — decoding as utf-8`);
1121
1158
  }
1122
1159
  }
1123
- return new TextDecoder("utf-8").decode(bytes);
1160
+ if (!decoder)
1161
+ decoder = new TextDecoder("utf-8", { fatal: true });
1162
+ // Single-byte encodings decode any byte sequence without error, so a decode
1163
+ // that cannot fail is not on its own evidence of text.
1164
+ if (!WIDE_TEXT_ENCODING.test(label) && bytes.includes(0))
1165
+ return undefined;
1166
+ try {
1167
+ return decoder.decode(bytes);
1168
+ }
1169
+ catch {
1170
+ return undefined;
1171
+ }
1172
+ }
1173
+ /**
1174
+ * True when a parsed `write` request carries bytes that are not text. Such a
1175
+ * request cannot go to OpenCode's `write` tool at all — it takes a string, and
1176
+ * additionally BOM-splits, diffs, and runs `Format.file()` on what it writes.
1177
+ */
1178
+ export function binaryWritePayload(parsed) {
1179
+ if (parsed.toolName !== "write")
1180
+ return undefined;
1181
+ const data = parsed.resultMetadata?.binaryWriteBytes;
1182
+ if (!(data instanceof Uint8Array))
1183
+ return undefined;
1184
+ return { path: str(parsed.resultMetadata?.path) ?? "", data };
1124
1185
  }
1125
1186
  function num(v) {
1126
1187
  if (typeof v === "number" && Number.isFinite(v))
@@ -1203,15 +1264,10 @@ function untildify(input) {
1203
1264
  * A read target addressed by URI scheme rather than by local filesystem path.
1204
1265
  *
1205
1266
  * Cursor's LocalReadExecutor only ever reads local files, so this provider
1206
- * mirrors its pre-execution validation. Host read tools are not so limited:
1207
- * oh-my-pi's `read` accepts "a path, URL, or internal URI", and mounts every
1208
- * discoverable tool — including all MCP server tools — under `xd://<tool>`
1209
- * device URLs that are read for their schema and written to for execution
1210
- * (`tools.xdev`, on by default). Resolving those against the workspace root
1211
- * mangles `xd://everything_echo` into `<root>/xd:/everything_echo`, which stats
1212
- * as missing and gets refused as `file_not_found` — so the host's read tool is
1213
- * never called and every xd://-mounted MCP tool is unreachable through this
1214
- * provider. Such targets must be forwarded untouched and left to the host.
1267
+ * mirrors its pre-execution validation. An advertised host read tool may also
1268
+ * accept URLs or internal URI schemes. Resolving those against the workspace
1269
+ * root mangles the URI and produces a false `file_not_found`, so URI targets
1270
+ * are forwarded untouched and left to the advertised executor.
1215
1271
  *
1216
1272
  * Requires a scheme of two or more characters followed by `://`, so Windows
1217
1273
  * drive letters (`C:\src`, `C:/src`) stay local paths.
package/dist/usage.d.ts CHANGED
@@ -49,8 +49,8 @@ export declare function formatCursorCacheDiagnostics(counters: CursorUsageCounte
49
49
  export declare function formatTurnUsageValidation(counters: CursorUsageCounters, usage: LanguageModelV3Usage, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): string;
50
50
  /** OpenCode requires a usage object at every step boundary. */
51
51
  export declare function emptyLanguageModelV3Usage(): LanguageModelV3Usage;
52
- /** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
53
- export declare function kiloShapedUsageFromV3(usage: LanguageModelV3Usage): {
52
+ /** Project nested V3 usage into the common flat AI-SDK counter shape. */
53
+ export declare function flatUsageFromV3(usage: LanguageModelV3Usage): {
54
54
  inputTokens: number;
55
55
  outputTokens: number;
56
56
  reasoningTokens: number;
package/dist/usage.js CHANGED
@@ -217,8 +217,8 @@ export function emptyLanguageModelV3Usage() {
217
217
  reasoningTokens: 0,
218
218
  });
219
219
  }
220
- /** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
221
- export function kiloShapedUsageFromV3(usage) {
220
+ /** Project nested V3 usage into the common flat AI-SDK counter shape. */
221
+ export function flatUsageFromV3(usage) {
222
222
  const input = usage.inputTokens;
223
223
  const output = usage.outputTokens;
224
224
  const noCache = input.noCache ?? 0;
@@ -1,33 +1,29 @@
1
+ import { executeOpenCodeWebSearch } from "./web-tools.js";
2
+ type ToolFactory = {
3
+ tool: (input: Record<string, unknown>) => Record<string, unknown>;
4
+ schema: {
5
+ string: () => any;
6
+ number: () => any;
7
+ enum: (values: readonly string[]) => any;
8
+ };
9
+ };
10
+ /** Build the classic OpenCode web-search tool with the host's own Zod helper. */
11
+ export declare function createOpenCodeWebSearchTool(factory: ToolFactory): Record<string, unknown>;
1
12
  /**
2
- * Classic-plugin web search tool registration.
3
- *
4
- * Kept apart from `./web-tools.ts` on purpose: this is a *value* import from
5
- * `@opencode-ai/plugin`, and the OpenCode 2.0 entrypoint shares the search
6
- * implementation in that module. 2.0's package root exports no `tool`, so
7
- * importing it from a module on the 2.0 graph would throw during plugin load.
8
- * Only `plugin.ts` (classic hooks) may import this file.
13
+ * Host-neutral JSON-schema fallback when the classic helper is unavailable.
14
+ * OpenCode's legacy schema adapter marks every listed property required, so
15
+ * expose only the genuinely required query rather than turning four optional
16
+ * tuning fields into mandatory inputs.
9
17
  */
10
18
  export declare const openCodeWebSearchTool: {
11
19
  description: string;
12
20
  args: {
13
- query: import("zod").ZodString;
14
- numResults: import("zod").ZodOptional<import("zod").ZodNumber>;
15
- livecrawl: import("zod").ZodOptional<import("zod").ZodEnum<{
16
- fallback: "fallback";
17
- preferred: "preferred";
18
- }>>;
19
- type: import("zod").ZodOptional<import("zod").ZodEnum<{
20
- fast: "fast";
21
- auto: "auto";
22
- deep: "deep";
23
- }>>;
24
- contextMaxCharacters: import("zod").ZodOptional<import("zod").ZodNumber>;
21
+ query: {
22
+ type: string;
23
+ description: string;
24
+ };
25
25
  };
26
- execute(args: {
27
- query: string;
28
- numResults?: number | undefined;
29
- livecrawl?: "fallback" | "preferred" | undefined;
30
- type?: "fast" | "auto" | "deep" | undefined;
31
- contextMaxCharacters?: number | undefined;
32
- }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
26
+ execute: typeof executeOpenCodeWebSearch;
33
27
  };
28
+ export type { ToolFactory as OpenCodeToolFactory };
29
+ export declare function createOpenCodeWebSearchToolFromPlugin(pluginModule: any): Record<string, unknown>;
@@ -1,22 +1,32 @@
1
- import { tool } from "@opencode-ai/plugin";
2
1
  import { executeOpenCodeWebSearch } from "./web-tools.js";
2
+ /** Build the classic OpenCode web-search tool with the host's own Zod helper. */
3
+ export function createOpenCodeWebSearchTool(factory) {
4
+ const schema = factory.schema;
5
+ return factory.tool({
6
+ description: "Search the web for current information using OpenCode's web search backend.",
7
+ args: {
8
+ query: schema.string().describe("Web search query"),
9
+ numResults: schema.number().int().min(1).max(20).optional(),
10
+ livecrawl: schema.enum(["fallback", "preferred"]).optional(),
11
+ type: schema.enum(["auto", "fast", "deep"]).optional(),
12
+ contextMaxCharacters: schema.number().int().positive().optional(),
13
+ },
14
+ execute: executeOpenCodeWebSearch,
15
+ });
16
+ }
3
17
  /**
4
- * Classic-plugin web search tool registration.
5
- *
6
- * Kept apart from `./web-tools.ts` on purpose: this is a *value* import from
7
- * `@opencode-ai/plugin`, and the OpenCode 2.0 entrypoint shares the search
8
- * implementation in that module. 2.0's package root exports no `tool`, so
9
- * importing it from a module on the 2.0 graph would throw during plugin load.
10
- * Only `plugin.ts` (classic hooks) may import this file.
18
+ * Host-neutral JSON-schema fallback when the classic helper is unavailable.
19
+ * OpenCode's legacy schema adapter marks every listed property required, so
20
+ * expose only the genuinely required query rather than turning four optional
21
+ * tuning fields into mandatory inputs.
11
22
  */
12
- export const openCodeWebSearchTool = tool({
23
+ export const openCodeWebSearchTool = {
13
24
  description: "Search the web for current information using OpenCode's web search backend.",
14
25
  args: {
15
- query: tool.schema.string().describe("Web search query"),
16
- numResults: tool.schema.number().int().min(1).max(20).optional(),
17
- livecrawl: tool.schema.enum(["fallback", "preferred"]).optional(),
18
- type: tool.schema.enum(["auto", "fast", "deep"]).optional(),
19
- contextMaxCharacters: tool.schema.number().int().positive().optional(),
26
+ query: { type: "string", description: "Web search query" },
20
27
  },
21
28
  execute: executeOpenCodeWebSearch,
22
- });
29
+ };
30
+ export function createOpenCodeWebSearchToolFromPlugin(pluginModule) {
31
+ return createOpenCodeWebSearchTool({ tool: pluginModule.tool, schema: pluginModule.tool.schema });
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,6 +55,10 @@
55
55
  "./auth": {
56
56
  "types": "./dist/auth.d.ts",
57
57
  "import": "./dist/auth.js"
58
+ },
59
+ "./image-save": {
60
+ "types": "./dist/image-save.d.ts",
61
+ "import": "./dist/image-save.js"
58
62
  }
59
63
  },
60
64
  "files": [
@@ -80,11 +84,10 @@
80
84
  "protobufjs": "^7.6.5"
81
85
  },
82
86
  "peerDependencies": {
83
- "@opencode-ai/plugin": "^1.17.13",
84
- "@opencode-compat/profile": ">=0.1.0"
87
+ "@opencode-ai/plugin": "^1.17.13"
85
88
  },
86
89
  "peerDependenciesMeta": {
87
- "@opencode-compat/profile": {
90
+ "@opencode-ai/plugin": {
88
91
  "optional": true
89
92
  }
90
93
  },