cursor-opencode-provider 0.6.8 → 0.7.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 (41) hide show
  1. package/README.md +51 -181
  2. package/dist/compaction-marker.d.ts +3 -2
  3. package/dist/compaction-marker.js +3 -2
  4. package/dist/host-agent-mode.d.ts +32 -0
  5. package/dist/host-agent-mode.js +92 -0
  6. package/dist/language-model.d.ts +15 -1
  7. package/dist/language-model.js +190 -36
  8. package/dist/models.d.ts +12 -1
  9. package/dist/models.js +16 -6
  10. package/dist/opencode2/catalog.d.ts +46 -11
  11. package/dist/opencode2/catalog.js +60 -45
  12. package/dist/opencode2/todo-tools.d.ts +64 -0
  13. package/dist/opencode2/todo-tools.js +165 -0
  14. package/dist/opencode2/types.d.ts +138 -83
  15. package/dist/opencode2/types.js +8 -10
  16. package/dist/plan-execution-kickoff.d.ts +2 -0
  17. package/dist/plan-execution-kickoff.js +25 -1
  18. package/dist/plugin-opencode2.d.ts +4 -1
  19. package/dist/plugin-opencode2.js +335 -154
  20. package/dist/plugin.js +5 -1
  21. package/dist/protocol/conversation-persistence.d.ts +2 -0
  22. package/dist/protocol/conversation-persistence.js +19 -1
  23. package/dist/protocol/conversation-state.d.ts +4 -0
  24. package/dist/protocol/conversation-state.js +4 -0
  25. package/dist/protocol/switch-mode.d.ts +5 -4
  26. package/dist/protocol/switch-mode.js +12 -4
  27. package/dist/protocol/tool-call-bridge.js +1 -1
  28. package/dist/protocol/tools.d.ts +36 -11
  29. package/dist/protocol/tools.js +747 -117
  30. package/dist/protocol/workspace-grounding.d.ts +17 -1
  31. package/dist/protocol/workspace-grounding.js +34 -5
  32. package/dist/session.d.ts +7 -1
  33. package/dist/shared.d.ts +3 -1
  34. package/dist/shared.js +3 -1
  35. package/dist/shell-timeout.d.ts +13 -0
  36. package/dist/shell-timeout.js +38 -0
  37. package/dist/todo-store.d.ts +29 -0
  38. package/dist/todo-store.js +78 -0
  39. package/dist/web-tools.d.ts +13 -0
  40. package/dist/web-tools.js +83 -5
  41. package/package.json +6 -3
@@ -1 +1,17 @@
1
- export declare function appendWorkspaceRootGrounding(reason: string, workspaceRoot: string | undefined): string;
1
+ export type WorkspaceGroundingOptions = {
2
+ /** OpenCode 2 file tools take `path` and do not say it must be absolute. */
3
+ requireAbsolutePathArg?: boolean;
4
+ };
5
+ /**
6
+ * Attach the exact workspace root to a model-visible string.
7
+ *
8
+ * Idempotent: a second call does not stack another root line. The absolute-path
9
+ * sentence is added only when requested, and only once.
10
+ */
11
+ export declare function appendWorkspaceRootGrounding(reason: string, workspaceRoot: string | undefined, options?: WorkspaceGroundingOptions): string;
12
+ /**
13
+ * Checkpointed Runs omit the system prompt, which is where the root normally
14
+ * lives. Put the same reminder on the live user message so a later turn cannot
15
+ * invent an absolute prefix before any tool result arrives.
16
+ */
17
+ export declare function appendCheckpointUserGrounding(userText: string, workspaceRoot: string | undefined, options?: WorkspaceGroundingOptions): string;
@@ -1,8 +1,37 @@
1
- export function appendWorkspaceRootGrounding(reason, workspaceRoot) {
1
+ const ABSOLUTE_PATH_ARG = "OpenCode file tools take `path` as an absolute path under this root. Do not pass a project-relative path, and do not invent a different absolute prefix.";
2
+ /**
3
+ * Attach the exact workspace root to a model-visible string.
4
+ *
5
+ * Idempotent: a second call does not stack another root line. The absolute-path
6
+ * sentence is added only when requested, and only once.
7
+ */
8
+ export function appendWorkspaceRootGrounding(reason, workspaceRoot, options) {
2
9
  if (!workspaceRoot || !workspaceRoot.trim())
3
10
  return reason;
4
- if (reason.includes("Workspace root:"))
5
- return reason;
6
- return (`${reason}\nWorkspace root: ${JSON.stringify(workspaceRoot)}. ` +
7
- "Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.");
11
+ const pathSentence = options?.requireAbsolutePathArg ? ` ${ABSOLUTE_PATH_ARG}` : "";
12
+ if (reason.includes("Workspace root:")) {
13
+ if (!pathSentence || reason.includes("take `path` as an absolute path"))
14
+ return reason;
15
+ return `${reason}${pathSentence}`;
16
+ }
17
+ const note = `Workspace root: ${JSON.stringify(workspaceRoot)}. ` +
18
+ "Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them." +
19
+ pathSentence;
20
+ if (!reason)
21
+ return note;
22
+ return `${reason}\n${note}`;
23
+ }
24
+ /**
25
+ * Checkpointed Runs omit the system prompt, which is where the root normally
26
+ * lives. Put the same reminder on the live user message so a later turn cannot
27
+ * invent an absolute prefix before any tool result arrives.
28
+ */
29
+ export function appendCheckpointUserGrounding(userText, workspaceRoot, options) {
30
+ const note = appendWorkspaceRootGrounding("", workspaceRoot, options);
31
+ if (!note)
32
+ return userText;
33
+ if (userText.includes("Workspace root:")) {
34
+ return appendWorkspaceRootGrounding(userText, workspaceRoot, options);
35
+ }
36
+ return userText ? `${userText}\n\n${note}` : note;
8
37
  }
package/dist/session.d.ts CHANGED
@@ -1,7 +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, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
4
+ import type { HostSubagentCatalog, HostToolDialect, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
5
5
  import type { CursorConversationTokenDetails } from "./protocol/token-details.js";
6
6
  export type Frame = {
7
7
  flags: number;
@@ -134,6 +134,10 @@ export type CursorSession = {
134
134
  };
135
135
  /** OpenCode session whose own or descendant activity renews tool leases. */
136
136
  openCodeSessionId?: string;
137
+ /** Host primary agent whose prompt/permissions this Run was seeded with. */
138
+ hostAgent?: string;
139
+ /** Stable host-system + provider-guidance identity for restart validation. */
140
+ stableSystemPromptHash?: string;
137
141
  /** Completed compaction must rebase once before resuming a normal agent. */
138
142
  postCompactionRebase?: boolean;
139
143
  /** Last real host catalog, retained only as a lifecycle-turn fallback. */
@@ -170,6 +174,8 @@ export type CursorSession = {
170
174
  toolDescriptors: Array<Record<string, unknown>>;
171
175
  /** Cursor-facing web alias → exact executable host tool for this Run. */
172
176
  toolAliases?: ToolAliasRegistry;
177
+ /** OpenCode 1.x `filePath`/`bash` vs 2.x `path`/`shell`, from advertised schemas. */
178
+ hostToolDialect?: HostToolDialect;
173
179
  /** Spawnable host agents extracted from this turn's Task/Actor definition. */
174
180
  subagentCatalog?: HostSubagentCatalog;
175
181
  /** Full RequestContext for exec #10 replies. */
package/dist/shared.d.ts CHANGED
@@ -4,6 +4,8 @@ export declare const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
4
4
  export declare const CURSOR_PROVIDER_ID = "cursor";
5
5
  /** Private provider option injected by the OpenCode plugin for summary turns. */
6
6
  export declare const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
7
+ /** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */
8
+ export declare const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent";
7
9
  export declare const TOKEN_EXPIRY_THRESHOLD_S = 300;
8
10
  export declare const RUN_PATH = "/agent.v1.AgentService/Run";
9
11
  export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
@@ -13,7 +15,7 @@ export declare const MODEL_CACHE_FILE = "cursor-models.json";
13
15
  export declare const MODEL_CACHE_SCHEMA_VERSION = 3;
14
16
  export declare const MODEL_CACHE_TTL_MS = 86400000;
15
17
  export declare const CONVERSATION_CACHE_DIR = "cursor-conversations";
16
- export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 3;
18
+ export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 4;
17
19
  export declare const CONVERSATION_CACHE_TTL_MS = 86400000;
18
20
  export declare const VERSION_CACHE_FILE = "cursor-client-version.json";
19
21
  export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
package/dist/shared.js CHANGED
@@ -4,6 +4,8 @@ export const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
4
4
  export const CURSOR_PROVIDER_ID = "cursor";
5
5
  /** Private provider option injected by the OpenCode plugin for summary turns. */
6
6
  export const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
7
+ /** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */
8
+ export const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent";
7
9
  export const TOKEN_EXPIRY_THRESHOLD_S = 300;
8
10
  export const RUN_PATH = "/agent.v1.AgentService/Run";
9
11
  export const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
@@ -13,7 +15,7 @@ export const MODEL_CACHE_FILE = "cursor-models.json";
13
15
  export const MODEL_CACHE_SCHEMA_VERSION = 3;
14
16
  export const MODEL_CACHE_TTL_MS = 86_400_000;
15
17
  export const CONVERSATION_CACHE_DIR = "cursor-conversations";
16
- export const CONVERSATION_CACHE_SCHEMA_VERSION = 3;
18
+ export const CONVERSATION_CACHE_SCHEMA_VERSION = 4;
17
19
  export const CONVERSATION_CACHE_TTL_MS = 86_400_000;
18
20
  export const VERSION_CACHE_FILE = "cursor-client-version.json";
19
21
  export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
@@ -85,9 +85,19 @@ export declare function releaseCursorShellEnv(toolCallId: string): void;
85
85
  * same materialized wrapper backs the direct-command sh/dash fallback.
86
86
  */
87
87
  export declare function cursorShellEnvForCall(toolCallId: string | undefined): Record<string, string> | undefined;
88
+ /**
89
+ * OpenCode 2.0 `shell.create.before` has no tool-call id. Correlate the pending
90
+ * wrap by command + working directory when possible, then fall back to the
91
+ * original command for hosts that omit the directory.
92
+ */
93
+ export declare function cursorShellEnvForCommand(command: string | undefined, workingDirectory?: string): Record<string, string> | undefined;
88
94
  /**
89
95
  * Strip private wrapper sentinels / OpenCode timeout envelopes for display.
90
96
  * Does not record outcomes — use {@link captureCursorShellResult} for that.
97
+ *
98
+ * OpenCode 2.0 `Tool.Result.output` is structured (an object for shell), not
99
+ * a string. Non-string input is returned unchanged so the 2.0 after-hook can
100
+ * pass structured output through safely.
91
101
  */
92
102
  export declare function sanitizeCursorShellDisplayOutput(output: string, policy?: CursorShellPolicy): string;
93
103
  /** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
@@ -95,6 +105,9 @@ export declare function sanitizeRegisteredCursorShellOutput(toolCallId: string,
95
105
  /**
96
106
  * Capture Bash completion in the classic plugin's after hook. Returns the
97
107
  * sanitized output that OpenCode should store and render.
108
+ *
109
+ * Guards non-string output (OpenCode 2.0 structured `Tool.Result.output`)
110
+ * by returning it unchanged.
98
111
  */
99
112
  export declare function captureCursorShellResult(toolCallId: string, output: string, metadata?: Record<string, unknown>): string;
100
113
  /** Consume the structured result, with an inline fallback when no plugin hook ran. */
@@ -269,6 +269,31 @@ export function cursorShellEnvForCall(toolCallId) {
269
269
  pendingEnvWraps.delete(toolCallId);
270
270
  return wrap.env;
271
271
  }
272
+ /**
273
+ * OpenCode 2.0 `shell.create.before` has no tool-call id. Correlate the pending
274
+ * wrap by command + working directory when possible, then fall back to the
275
+ * original command for hosts that omit the directory.
276
+ */
277
+ export function cursorShellEnvForCommand(command, workingDirectory) {
278
+ if (typeof command !== "string" || !command)
279
+ return undefined;
280
+ if (workingDirectory) {
281
+ for (const [id, policy] of policies) {
282
+ if (!pendingEnvWraps.has(id))
283
+ continue;
284
+ if (policy.command === command && policy.workingDirectory === workingDirectory) {
285
+ return cursorShellEnvForCall(id);
286
+ }
287
+ }
288
+ }
289
+ for (const [id, policy] of policies) {
290
+ if (!pendingEnvWraps.has(id))
291
+ continue;
292
+ if (policy.command === command)
293
+ return cursorShellEnvForCall(id);
294
+ }
295
+ return undefined;
296
+ }
272
297
  function withoutMarker(output, index) {
273
298
  let clean = output.slice(0, index).replace(/[\t ]+$/gm, "").replace(/\n{2,}$/, "\n");
274
299
  if (clean.trim() === "" || clean.trim() === "(no output)")
@@ -390,8 +415,14 @@ function parseBackgroundSpawnOutcome(output, policy) {
390
415
  /**
391
416
  * Strip private wrapper sentinels / OpenCode timeout envelopes for display.
392
417
  * Does not record outcomes — use {@link captureCursorShellResult} for that.
418
+ *
419
+ * OpenCode 2.0 `Tool.Result.output` is structured (an object for shell), not
420
+ * a string. Non-string input is returned unchanged so the 2.0 after-hook can
421
+ * pass structured output through safely.
393
422
  */
394
423
  export function sanitizeCursorShellDisplayOutput(output, policy) {
424
+ if (typeof output !== "string")
425
+ return output;
395
426
  if (policy?.backgroundSpawn) {
396
427
  const spawn = parseBackgroundSpawnOutcome(output, policy);
397
428
  if (spawn)
@@ -413,6 +444,8 @@ export function sanitizeCursorShellDisplayOutput(output, policy) {
413
444
  }
414
445
  /** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
415
446
  export function sanitizeRegisteredCursorShellOutput(toolCallId, output) {
447
+ if (typeof output !== "string")
448
+ return output;
416
449
  if (typeof toolCallId !== "string" || !toolCallId)
417
450
  return output;
418
451
  return sanitizeCursorShellDisplayOutput(output, policies.get(toolCallId));
@@ -420,8 +453,13 @@ export function sanitizeRegisteredCursorShellOutput(toolCallId, output) {
420
453
  /**
421
454
  * Capture Bash completion in the classic plugin's after hook. Returns the
422
455
  * sanitized output that OpenCode should store and render.
456
+ *
457
+ * Guards non-string output (OpenCode 2.0 structured `Tool.Result.output`)
458
+ * by returning it unchanged.
423
459
  */
424
460
  export function captureCursorShellResult(toolCallId, output, metadata) {
461
+ if (typeof output !== "string")
462
+ return output;
425
463
  if (typeof toolCallId !== "string" || !toolCallId.startsWith("cursor_"))
426
464
  return output;
427
465
  const policy = policies.get(toolCallId);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Per-session todo list used when the host does not ship `todowrite` /
3
+ * `todoread` (OpenCode 2 dropped those builtins; OpenCode 1.x still has
4
+ * `todowrite`).
5
+ *
6
+ * In-memory only — same lifetime as the provider process. A restart clears
7
+ * the list until the next write/read, matching the mirrored Cursor-todo
8
+ * snapshot in `language-model.ts`.
9
+ */
10
+ export declare const TODOWRITE_TOOL = "todowrite";
11
+ export declare const TODOREAD_TOOL = "todoread";
12
+ export declare const TODO_STATUSES: readonly ["pending", "in_progress", "completed", "cancelled"];
13
+ export type TodoStatus = (typeof TODO_STATUSES)[number];
14
+ export declare const TODO_PRIORITIES: readonly ["high", "medium", "low"];
15
+ export type TodoPriority = (typeof TODO_PRIORITIES)[number];
16
+ export type SessionTodo = {
17
+ id: string;
18
+ content: string;
19
+ status: TodoStatus;
20
+ priority: TodoPriority;
21
+ };
22
+ /** Normalize one host/Cursor todo item. Drops entries with empty content. */
23
+ export declare function normalizeSessionTodo(value: unknown, index: number): SessionTodo | undefined;
24
+ export declare function normalizeSessionTodos(value: unknown): SessionTodo[];
25
+ export declare function getSessionTodos(sessionID: string): SessionTodo[];
26
+ export declare function setSessionTodos(sessionID: string, todos: unknown): SessionTodo[];
27
+ export declare function clearSessionTodos(sessionID: string): void;
28
+ /** Test-only: drop every session list. */
29
+ export declare function clearAllSessionTodos(): void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Per-session todo list used when the host does not ship `todowrite` /
3
+ * `todoread` (OpenCode 2 dropped those builtins; OpenCode 1.x still has
4
+ * `todowrite`).
5
+ *
6
+ * In-memory only — same lifetime as the provider process. A restart clears
7
+ * the list until the next write/read, matching the mirrored Cursor-todo
8
+ * snapshot in `language-model.ts`.
9
+ */
10
+ export const TODOWRITE_TOOL = "todowrite";
11
+ export const TODOREAD_TOOL = "todoread";
12
+ export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"];
13
+ export const TODO_PRIORITIES = ["high", "medium", "low"];
14
+ const lists = new Map();
15
+ const MAX_TRACKED_SESSIONS = 256;
16
+ function isStatus(value) {
17
+ return typeof value === "string" && TODO_STATUSES.includes(value);
18
+ }
19
+ function isPriority(value) {
20
+ return typeof value === "string" && TODO_PRIORITIES.includes(value);
21
+ }
22
+ /** Normalize one host/Cursor todo item. Drops entries with empty content. */
23
+ export function normalizeSessionTodo(value, index) {
24
+ if (!value || typeof value !== "object" || Array.isArray(value))
25
+ return undefined;
26
+ const record = value;
27
+ const content = typeof record.content === "string" ? record.content : "";
28
+ if (!content.trim())
29
+ return undefined;
30
+ const id = typeof record.id === "string" && record.id.trim()
31
+ ? record.id.trim()
32
+ : String(index + 1);
33
+ return {
34
+ id,
35
+ content,
36
+ status: isStatus(record.status) ? record.status : "pending",
37
+ priority: isPriority(record.priority) ? record.priority : "medium",
38
+ };
39
+ }
40
+ export function normalizeSessionTodos(value) {
41
+ if (!Array.isArray(value))
42
+ return [];
43
+ const out = [];
44
+ const seen = new Set();
45
+ for (const item of value) {
46
+ const todo = normalizeSessionTodo(item, out.length);
47
+ if (!todo)
48
+ continue;
49
+ let id = todo.id;
50
+ if (seen.has(id))
51
+ id = `${id}-${out.length + 1}`;
52
+ seen.add(id);
53
+ out.push(id === todo.id ? todo : { ...todo, id });
54
+ }
55
+ return out;
56
+ }
57
+ export function getSessionTodos(sessionID) {
58
+ return lists.get(sessionID)?.map((todo) => ({ ...todo })) ?? [];
59
+ }
60
+ export function setSessionTodos(sessionID, todos) {
61
+ const normalized = normalizeSessionTodos(todos);
62
+ lists.delete(sessionID);
63
+ lists.set(sessionID, normalized.map((todo) => ({ ...todo })));
64
+ while (lists.size > MAX_TRACKED_SESSIONS) {
65
+ const oldest = lists.keys().next().value;
66
+ if (oldest === undefined)
67
+ break;
68
+ lists.delete(oldest);
69
+ }
70
+ return getSessionTodos(sessionID);
71
+ }
72
+ export function clearSessionTodos(sessionID) {
73
+ lists.delete(sessionID);
74
+ }
75
+ /** Test-only: drop every session list. */
76
+ export function clearAllSessionTodos() {
77
+ lists.clear();
78
+ }
@@ -14,5 +14,18 @@ export declare function parseOpenCodeWebSearchResponse(raw: string): string | un
14
14
  * OpenCode 2.0 plugin's tool registration share one implementation — 2.0's
15
15
  * ToolContext has no `ask`, permissions being handled by the host instead.
16
16
  */
17
+ export type OpenCode2WebSearchResult = {
18
+ url: string;
19
+ title?: string;
20
+ content?: string;
21
+ time: {
22
+ published?: number;
23
+ };
24
+ };
25
+ /**
26
+ * Turn an Exa MCP text blob into OpenCode 2.0 `websearch` results
27
+ * (`{ url, title?, content?, time }`). Unknown shapes yield an empty list.
28
+ */
29
+ export declare function parseExaWebSearchResults(raw: string): OpenCode2WebSearchResult[];
17
30
  export declare function fetchOpenCodeWebSearchText(args: OpenCodeWebSearchArgs, signal: AbortSignal | undefined, fetchImpl?: typeof fetch): Promise<string>;
18
31
  export declare function executeOpenCodeWebSearch(args: OpenCodeWebSearchArgs, context: ToolContext, fetchImpl?: typeof fetch): Promise<ToolResult>;
package/dist/web-tools.js CHANGED
@@ -53,13 +53,91 @@ export function parseOpenCodeWebSearchResponse(raw) {
53
53
  }
54
54
  return undefined;
55
55
  }
56
+ function tryParseJson(raw) {
57
+ try {
58
+ return JSON.parse(raw);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ function publishedMs(value) {
65
+ if (typeof value === "number" && Number.isFinite(value)) {
66
+ return value < 1e12 ? Math.floor(value * 1000) : Math.floor(value);
67
+ }
68
+ if (typeof value === "string" && value.trim()) {
69
+ const parsed = Date.parse(value);
70
+ return Number.isFinite(parsed) ? parsed : undefined;
71
+ }
72
+ return undefined;
73
+ }
74
+ function asWebSearchResult(row) {
75
+ if (!row || typeof row !== "object")
76
+ return undefined;
77
+ const record = row;
78
+ const url = typeof record.url === "string" ? record.url
79
+ : typeof record.href === "string" ? record.href
80
+ : undefined;
81
+ if (!url)
82
+ return undefined;
83
+ const title = typeof record.title === "string" ? record.title : undefined;
84
+ const content = typeof record.content === "string" ? record.content
85
+ : typeof record.text === "string" ? record.text
86
+ : typeof record.snippet === "string" ? record.snippet
87
+ : undefined;
88
+ const published = publishedMs(record.publishedDate ?? record.published ?? record.date);
89
+ return {
90
+ url,
91
+ ...(title ? { title } : {}),
92
+ ...(content ? { content } : {}),
93
+ time: published !== undefined ? { published } : {},
94
+ };
95
+ }
96
+ function parseExaTextResults(text) {
97
+ return text.split(/\n\n---\n\n/).flatMap((block) => {
98
+ const url = block.match(/^URL:\s*(.+)$/m)?.[1]?.trim();
99
+ if (!url)
100
+ return [];
101
+ const title = block.match(/^Title:\s*(.+)$/m)?.[1]?.trim();
102
+ const publishedText = block.match(/^Published:\s*(.+)$/m)?.[1]?.trim();
103
+ const published = publishedText && publishedText !== "N/A"
104
+ ? publishedMs(publishedText)
105
+ : undefined;
106
+ const content = block.match(/^(?:Highlights|Text):\s*\n?([\s\S]*)$/m)?.[1]?.trim();
107
+ return [{
108
+ url,
109
+ ...(title && title !== "N/A" ? { title } : {}),
110
+ ...(content ? { content } : {}),
111
+ time: published === undefined ? {} : { published },
112
+ }];
113
+ });
114
+ }
56
115
  /**
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.
116
+ * Turn an Exa MCP text blob into OpenCode 2.0 `websearch` results
117
+ * (`{ url, title?, content?, time }`). Unknown shapes yield an empty list.
62
118
  */
119
+ export function parseExaWebSearchResults(raw) {
120
+ const text = parseOpenCodeWebSearchResponse(raw) ?? raw.trim();
121
+ if (!text)
122
+ return [];
123
+ const parsed = tryParseJson(text);
124
+ const rows = Array.isArray(parsed)
125
+ ? parsed
126
+ : parsed && typeof parsed === "object"
127
+ ? Array.isArray(parsed.results)
128
+ ? parsed.results
129
+ : Array.isArray(parsed.data)
130
+ ? parsed.data
131
+ : []
132
+ : [];
133
+ if (rows.length > 0) {
134
+ return rows.flatMap((row) => {
135
+ const result = asWebSearchResult(row);
136
+ return result ? [result] : [];
137
+ });
138
+ }
139
+ return parseExaTextResults(text);
140
+ }
63
141
  export async function fetchOpenCodeWebSearchText(args, signal, fetchImpl = fetch) {
64
142
  const controller = new AbortController();
65
143
  const abort = () => controller.abort(signal?.reason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.6.8",
3
+ "version": "0.7.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",
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/plugin-opencode2.d.ts",
41
41
  "import": "./dist/plugin-opencode2.js"
42
42
  },
43
+ "./server": {
44
+ "types": "./dist/plugin-opencode2.d.ts",
45
+ "import": "./dist/plugin-opencode2.js"
46
+ },
43
47
  "./errors": {
44
48
  "types": "./dist/errors.d.ts",
45
49
  "import": "./dist/errors.js"
@@ -70,7 +74,7 @@
70
74
  ],
71
75
  "scripts": {
72
76
  "build": "tsc",
73
- "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
77
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
74
78
  "test": "bun test",
75
79
  "test:node-http2": "bun run build && node test/node-http2-cleanup.mjs",
76
80
  "test:watch": "bun test --watch",
@@ -93,7 +97,6 @@
93
97
  },
94
98
  "devDependencies": {
95
99
  "@opencode-ai/plugin": "^1.18.16",
96
- "@opencode-ai/plugin-next": "npm:@opencode-ai/plugin@0.0.0-next-17155",
97
100
  "@tsconfig/node22": "^22.0.5",
98
101
  "@types/bun": "^1.3.0",
99
102
  "@types/node": "^22.20.1",