cursor-opencode-provider 0.2.9 → 0.3.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 (46) hide show
  1. package/README.md +139 -10
  2. package/dist/agent-url.js +2 -0
  3. package/dist/auth.d.ts +7 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/compaction-marker.d.ts +14 -0
  6. package/dist/compaction-marker.js +36 -0
  7. package/dist/context/env.js +6 -2
  8. package/dist/context/paths.js +3 -2
  9. package/dist/index.d.ts +1 -3
  10. package/dist/index.js +2 -8
  11. package/dist/language-model.js +51 -35
  12. package/dist/model-config.d.ts +15 -0
  13. package/dist/model-config.js +206 -0
  14. package/dist/opencode2/catalog.d.ts +33 -0
  15. package/dist/opencode2/catalog.js +85 -0
  16. package/dist/opencode2/integration.d.ts +23 -0
  17. package/dist/opencode2/integration.js +124 -0
  18. package/dist/opencode2/types.d.ts +316 -0
  19. package/dist/opencode2/types.js +18 -0
  20. package/dist/plugin-core.d.ts +27 -0
  21. package/dist/plugin-core.js +31 -0
  22. package/dist/plugin-opencode2.d.ts +3 -0
  23. package/dist/plugin-opencode2.js +293 -0
  24. package/dist/plugin-v2.d.ts +7 -0
  25. package/dist/plugin-v2.js +1 -15
  26. package/dist/plugin.d.ts +1 -15
  27. package/dist/plugin.js +11 -191
  28. package/dist/protocol/apply-patch.d.ts +109 -0
  29. package/dist/protocol/apply-patch.js +163 -0
  30. package/dist/protocol/messages.d.ts +10 -0
  31. package/dist/protocol/messages.js +83 -1
  32. package/dist/protocol/tools.d.ts +24 -1
  33. package/dist/protocol/tools.js +262 -19
  34. package/dist/session-directory.d.ts +17 -0
  35. package/dist/session-directory.js +35 -0
  36. package/dist/session.d.ts +13 -1
  37. package/dist/session.js +70 -1
  38. package/dist/shell-timeout.d.ts +3 -1
  39. package/dist/shell-timeout.js +12 -4
  40. package/dist/usage.d.ts +30 -0
  41. package/dist/usage.js +77 -0
  42. package/dist/web-search-tool.d.ts +33 -0
  43. package/dist/web-search-tool.js +22 -0
  44. package/dist/web-tools.d.ts +9 -25
  45. package/dist/web-tools.js +34 -36
  46. package/package.json +14 -9
@@ -213,7 +213,7 @@ function ensureShellEnvWrap(toolCallId, policy) {
213
213
  * The classic hook replaces it with the original command (bash/zsh) or the
214
214
  * shorter wrapper-file command (sh/dash), avoiding duplicate execution.
215
215
  */
216
- export function prepareCursorShellArgs(toolCallId, args) {
216
+ export function prepareCursorShellArgs(toolCallId, args, options = {}) {
217
217
  const policy = policies.get(toolCallId);
218
218
  if (!policy)
219
219
  return;
@@ -226,7 +226,11 @@ export function prepareCursorShellArgs(toolCallId, args) {
226
226
  args.timeout = Math.max(OPENCODE_TIMEOUT_GRACE_MS, policy.timeoutMs + OPENCODE_TIMEOUT_GRACE_MS);
227
227
  }
228
228
  const shellKind = resolveCursorShellKind();
229
- if (process.platform === "win32" || shellKind === "bash" || shellKind === "zsh") {
229
+ // OpenCode 2.0 has no `shell.env` hook, so bash/zsh cannot be wrapped by
230
+ // sourcing an injector. Callers there opt into the wrapper-file command —
231
+ // the same mechanism sh/dash already use — which needs no env injection.
232
+ const envInjectable = !options.preferWrapperCommand && (shellKind === "bash" || shellKind === "zsh");
233
+ if (process.platform === "win32" || envInjectable) {
230
234
  // Native Windows PowerShell/cmd wrapping remains unsupported; do not emit
231
235
  // a POSIX /bin/sh command there. Git Bash still uses the env path above.
232
236
  args.command = policy.command;
@@ -318,8 +322,12 @@ function lastPrivateMarker(output, marker, valuePattern) {
318
322
  return last;
319
323
  }
320
324
  function parseOpenCodeTimeout(output) {
321
- const re = /<shell_metadata>\r?\nshell tool terminated command after exceeding timeout (\d+) ms\.[\s\S]*?<\/shell_metadata>\s*$/;
322
- const match = re.exec(output);
325
+ const closeTag = "</shell_metadata>";
326
+ const closeAt = output.lastIndexOf(closeTag);
327
+ if (closeAt === -1 || output.slice(closeAt + closeTag.length).trim() !== "")
328
+ return undefined;
329
+ const header = /<shell_metadata>\r?\nshell tool terminated command after exceeding timeout (\d+) ms\./;
330
+ const match = header.exec(output.slice(0, closeAt));
323
331
  if (!match || match.index === undefined)
324
332
  return undefined;
325
333
  return { output: withoutMarker(output, match.index), timeoutMs: Number(match[1]) };
@@ -0,0 +1,30 @@
1
+ import type { LanguageModelV3Usage } from "@ai-sdk/provider";
2
+ /** Non-negative integer counter from a Cursor `turn_ended` field. */
3
+ export declare function turnEndedCounter(te: Record<string, unknown>, key: string): number;
4
+ /**
5
+ * Map authoritative Cursor TurnEnded counters to AI SDK V3 usage (Kilo/OpenCode
6
+ * flatten via input total = noCache + cacheRead + cacheWrite).
7
+ *
8
+ * Contract: `input_tokens` is non-cached input; cache fields are separate.
9
+ * Cursor's agent wire currently sends `cache_write=0` even when later turns
10
+ * report large `cache_read`; we still forward the counter when non-zero.
11
+ * If production semantics differ (input_tokens already total), adjust here.
12
+ */
13
+ export declare function buildLanguageModelV3UsageFromTurnEnded(te: Record<string, unknown>): LanguageModelV3Usage;
14
+ export type CursorUsageEstimate = {
15
+ inputTokens: number;
16
+ outputTokens: number;
17
+ cacheRead: number;
18
+ cacheWrite: number;
19
+ reasoningTokens: number;
20
+ };
21
+ /** Mid-turn finish (e.g. tool-calls) before `turn_ended` — keep non-zero estimates. */
22
+ export declare function buildLanguageModelV3UsageFromEstimate(est: CursorUsageEstimate, promptTokens: number, outputChars: number, estimateCharsToTokens: (chars: number) => number): LanguageModelV3Usage;
23
+ /** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
24
+ export declare function kiloShapedUsageFromV3(usage: LanguageModelV3Usage): {
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ reasoningTokens: number;
28
+ cacheReadInputTokens: number;
29
+ cacheWriteInputTokens: number;
30
+ };
package/dist/usage.js ADDED
@@ -0,0 +1,77 @@
1
+ /** Non-negative integer counter from a Cursor `turn_ended` field. */
2
+ export function turnEndedCounter(te, key) {
3
+ const value = te[key];
4
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
5
+ ? Math.trunc(value)
6
+ : 0;
7
+ }
8
+ /**
9
+ * Map authoritative Cursor TurnEnded counters to AI SDK V3 usage (Kilo/OpenCode
10
+ * flatten via input total = noCache + cacheRead + cacheWrite).
11
+ *
12
+ * Contract: `input_tokens` is non-cached input; cache fields are separate.
13
+ * Cursor's agent wire currently sends `cache_write=0` even when later turns
14
+ * report large `cache_read`; we still forward the counter when non-zero.
15
+ * If production semantics differ (input_tokens already total), adjust here.
16
+ */
17
+ export function buildLanguageModelV3UsageFromTurnEnded(te) {
18
+ const input_tokens = turnEndedCounter(te, "input_tokens");
19
+ const output_tokens = turnEndedCounter(te, "output_tokens");
20
+ const cache_read = turnEndedCounter(te, "cache_read");
21
+ const cache_write = turnEndedCounter(te, "cache_write");
22
+ const reasoning_tokens = turnEndedCounter(te, "reasoning_tokens");
23
+ return {
24
+ inputTokens: {
25
+ total: input_tokens + cache_read + cache_write,
26
+ noCache: input_tokens,
27
+ cacheRead: cache_read,
28
+ cacheWrite: cache_write,
29
+ },
30
+ outputTokens: {
31
+ total: output_tokens + reasoning_tokens,
32
+ text: output_tokens,
33
+ reasoning: reasoning_tokens,
34
+ },
35
+ };
36
+ }
37
+ /** Mid-turn finish (e.g. tool-calls) before `turn_ended` — keep non-zero estimates. */
38
+ export function buildLanguageModelV3UsageFromEstimate(est, promptTokens, outputChars, estimateCharsToTokens) {
39
+ const hasInputBreakdown = est.cacheRead > 0 || est.cacheWrite > 0 || est.inputTokens > 0;
40
+ const inputNoCache = hasInputBreakdown ? est.inputTokens : promptTokens;
41
+ const cacheRead = est.cacheRead;
42
+ const cacheWrite = est.cacheWrite;
43
+ const outputFromChars = estimateCharsToTokens(outputChars);
44
+ const outputText = est.outputTokens > 0 ? est.outputTokens : outputFromChars;
45
+ const reasoning = est.reasoningTokens;
46
+ const outputTotal = outputText + reasoning;
47
+ return {
48
+ inputTokens: {
49
+ total: inputNoCache + cacheRead + cacheWrite,
50
+ noCache: inputNoCache,
51
+ cacheRead,
52
+ cacheWrite,
53
+ },
54
+ outputTokens: {
55
+ total: outputTotal,
56
+ text: outputText,
57
+ reasoning: reasoning > 0 ? reasoning : undefined,
58
+ },
59
+ };
60
+ }
61
+ /** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
62
+ export function kiloShapedUsageFromV3(usage) {
63
+ const input = usage.inputTokens;
64
+ const output = usage.outputTokens;
65
+ const noCache = input.noCache ?? 0;
66
+ const cacheRead = input.cacheRead ?? 0;
67
+ const cacheWrite = input.cacheWrite ?? 0;
68
+ const reasoning = output.reasoning ?? 0;
69
+ const text = output.text ?? Math.max(0, (output.total ?? 0) - reasoning);
70
+ return {
71
+ inputTokens: noCache,
72
+ outputTokens: text,
73
+ reasoningTokens: reasoning,
74
+ cacheReadInputTokens: cacheRead,
75
+ cacheWriteInputTokens: cacheWrite,
76
+ };
77
+ }
@@ -0,0 +1,33 @@
1
+ /**
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.
9
+ */
10
+ export declare const openCodeWebSearchTool: {
11
+ description: string;
12
+ 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>;
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>;
33
+ };
@@ -0,0 +1,22 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { executeOpenCodeWebSearch } from "./web-tools.js";
3
+ /**
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.
11
+ */
12
+ export const openCodeWebSearchTool = tool({
13
+ description: "Search the web for current information using OpenCode's web search backend.",
14
+ 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(),
20
+ },
21
+ execute: executeOpenCodeWebSearch,
22
+ });
@@ -1,4 +1,4 @@
1
- import { type ToolContext, type ToolResult } from "@opencode-ai/plugin";
1
+ import type { ToolContext, ToolResult } from "@opencode-ai/plugin";
2
2
  export type OpenCodeWebSearchArgs = {
3
3
  query: string;
4
4
  numResults?: number;
@@ -7,28 +7,12 @@ export type OpenCodeWebSearchArgs = {
7
7
  contextMaxCharacters?: number;
8
8
  };
9
9
  export declare function parseOpenCodeWebSearchResponse(raw: string): string | undefined;
10
+ /**
11
+ * Raw Exa web-search call: no host tool context, no permission prompt.
12
+ *
13
+ * Split out so both the classic plugin's `custom_websearch` tool and the
14
+ * OpenCode 2.0 plugin's tool registration share one implementation — 2.0's
15
+ * ToolContext has no `ask`, permissions being handled by the host instead.
16
+ */
17
+ export declare function fetchOpenCodeWebSearchText(args: OpenCodeWebSearchArgs, signal: AbortSignal | undefined, fetchImpl?: typeof fetch): Promise<string>;
10
18
  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
- };
package/dist/web-tools.js CHANGED
@@ -1,4 +1,3 @@
1
- import { tool } from "@opencode-ai/plugin";
2
1
  const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
3
2
  const WEB_SEARCH_TIMEOUT_MS = 25_000;
4
3
  function exaMcpUrl() {
@@ -54,26 +53,20 @@ export function parseOpenCodeWebSearchResponse(raw) {
54
53
  }
55
54
  return undefined;
56
55
  }
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
- });
56
+ /**
57
+ * Raw Exa web-search call: no host tool context, no permission prompt.
58
+ *
59
+ * Split out so both the classic plugin's `custom_websearch` tool and the
60
+ * OpenCode 2.0 plugin's tool registration share one implementation — 2.0's
61
+ * ToolContext has no `ask`, permissions being handled by the host instead.
62
+ */
63
+ export async function fetchOpenCodeWebSearchText(args, signal, fetchImpl = fetch) {
71
64
  const controller = new AbortController();
72
- const abort = () => controller.abort(context.abort.reason);
73
- if (context.abort.aborted)
65
+ const abort = () => controller.abort(signal?.reason);
66
+ if (signal?.aborted)
74
67
  abort();
75
68
  else
76
- context.abort.addEventListener("abort", abort, { once: true });
69
+ signal?.addEventListener("abort", abort, { once: true });
77
70
  const timeout = setTimeout(() => controller.abort(new Error("Web search timed out")), WEB_SEARCH_TIMEOUT_MS);
78
71
  try {
79
72
  const response = await fetchImpl(exaMcpUrl(), {
@@ -102,26 +95,31 @@ export async function executeOpenCodeWebSearch(args, context, fetchImpl = fetch)
102
95
  const raw = await response.text();
103
96
  if (!response.ok)
104
97
  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
- };
98
+ return parseOpenCodeWebSearchResponse(raw) ?? "No search results found. Please try a different query.";
111
99
  }
112
100
  finally {
113
101
  clearTimeout(timeout);
114
- context.abort.removeEventListener("abort", abort);
102
+ signal?.removeEventListener("abort", abort);
115
103
  }
116
104
  }
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
- });
105
+ export async function executeOpenCodeWebSearch(args, context, fetchImpl = fetch) {
106
+ await context.ask({
107
+ permission: "websearch",
108
+ patterns: [args.query],
109
+ always: ["*"],
110
+ metadata: {
111
+ query: args.query,
112
+ numResults: args.numResults,
113
+ livecrawl: args.livecrawl,
114
+ type: args.type,
115
+ contextMaxCharacters: args.contextMaxCharacters,
116
+ provider: "exa",
117
+ },
118
+ });
119
+ const output = await fetchOpenCodeWebSearchText(args, context.abort, fetchImpl);
120
+ return {
121
+ title: `Exa Web Search: ${args.query}`,
122
+ output,
123
+ metadata: { provider: "exa" },
124
+ };
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.2.9",
3
+ "version": "0.3.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",
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/plugin-v2.d.ts",
37
37
  "import": "./dist/plugin-v2.js"
38
38
  },
39
+ "./plugin/opencode2": {
40
+ "types": "./dist/plugin-opencode2.d.ts",
41
+ "import": "./dist/plugin-opencode2.js"
42
+ },
39
43
  "./errors": {
40
44
  "types": "./dist/errors.d.ts",
41
45
  "import": "./dist/errors.js"
@@ -50,15 +54,15 @@
50
54
  ],
51
55
  "scripts": {
52
56
  "build": "tsc",
53
- "typecheck": "tsc --noEmit",
57
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
54
58
  "test": "bun test",
55
59
  "test:node-http2": "bun run build && node test/node-http2-cleanup.mjs",
56
60
  "test:watch": "bun test --watch",
57
61
  "prepublishOnly": "bun run build"
58
62
  },
59
63
  "dependencies": {
60
- "@ai-sdk/provider": "3.0.8",
61
- "protobufjs": "^7.4.0"
64
+ "@ai-sdk/provider": "3.0.14",
65
+ "protobufjs": "^7.6.5"
62
66
  },
63
67
  "peerDependencies": {
64
68
  "@opencode-ai/plugin": "^1.17.13",
@@ -70,9 +74,10 @@
70
74
  }
71
75
  },
72
76
  "devDependencies": {
73
- "@opencode-ai/plugin": "^1.17.13",
74
- "@tsconfig/node22": "^22.0.1",
75
- "@types/node": "^22.15.3",
76
- "typescript": "^5.8.3"
77
+ "@opencode-ai/plugin": "^1.18.13",
78
+ "@opencode-ai/plugin-next": "npm:@opencode-ai/plugin@0.0.0-next-16281",
79
+ "@tsconfig/node22": "^22.0.5",
80
+ "@types/node": "^22.20.1",
81
+ "typescript": "^5.9.3"
77
82
  }
78
- }
83
+ }