cursor-opencode-provider 0.2.8 → 0.3.0

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 (51) hide show
  1. package/README.md +143 -11
  2. package/dist/auth.d.ts +7 -0
  3. package/dist/auth.js +9 -0
  4. package/dist/compaction-marker.d.ts +14 -0
  5. package/dist/compaction-marker.js +36 -0
  6. package/dist/context/agents.d.ts +5 -1
  7. package/dist/context/agents.js +90 -30
  8. package/dist/context/build.js +57 -8
  9. package/dist/context/env.js +6 -2
  10. package/dist/context/paths.d.ts +10 -0
  11. package/dist/context/paths.js +24 -0
  12. package/dist/context/plugins.js +16 -12
  13. package/dist/context/rules.js +26 -23
  14. package/dist/context/skills.js +8 -3
  15. package/dist/index.d.ts +1 -3
  16. package/dist/index.js +2 -8
  17. package/dist/language-model.js +121 -35
  18. package/dist/model-config.d.ts +15 -0
  19. package/dist/model-config.js +188 -0
  20. package/dist/opencode2/catalog.d.ts +33 -0
  21. package/dist/opencode2/catalog.js +85 -0
  22. package/dist/opencode2/integration.d.ts +23 -0
  23. package/dist/opencode2/integration.js +124 -0
  24. package/dist/opencode2/types.d.ts +316 -0
  25. package/dist/opencode2/types.js +18 -0
  26. package/dist/plugin-core.d.ts +27 -0
  27. package/dist/plugin-core.js +31 -0
  28. package/dist/plugin-opencode2.d.ts +3 -0
  29. package/dist/plugin-opencode2.js +293 -0
  30. package/dist/plugin-v2.d.ts +7 -0
  31. package/dist/plugin-v2.js +1 -15
  32. package/dist/plugin.d.ts +1 -15
  33. package/dist/plugin.js +11 -191
  34. package/dist/protocol/messages.d.ts +10 -0
  35. package/dist/protocol/messages.js +76 -1
  36. package/dist/protocol/tool-call-bridge.js +22 -2
  37. package/dist/protocol/tools.d.ts +57 -1
  38. package/dist/protocol/tools.js +277 -17
  39. package/dist/session-directory.d.ts +17 -0
  40. package/dist/session-directory.js +35 -0
  41. package/dist/session.d.ts +16 -2
  42. package/dist/session.js +70 -1
  43. package/dist/shell-timeout.d.ts +3 -1
  44. package/dist/shell-timeout.js +6 -2
  45. package/dist/usage.d.ts +30 -0
  46. package/dist/usage.js +77 -0
  47. package/dist/web-search-tool.d.ts +33 -0
  48. package/dist/web-search-tool.js +22 -0
  49. package/dist/web-tools.d.ts +9 -25
  50. package/dist/web-tools.js +34 -36
  51. package/package.json +7 -2
@@ -1,2 +1,9 @@
1
+ /**
2
+ * OpenCode Effect / Promise v2 plugin.
3
+ *
4
+ * Wires the Cursor provider through `ctx.aisdk.sdk` / `ctx.aisdk.language`
5
+ * (the V2 extension points). Auth still lives in the classic Hooks plugin
6
+ * (`plugin.ts`) until OpenCode integrations fully replace provider OAuth.
7
+ */
1
8
  declare const _default: import("@opencode-ai/plugin/v2/promise").Plugin;
2
9
  export default _default;
package/dist/plugin-v2.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { define } from "@opencode-ai/plugin/v2/promise";
2
2
  import { CURSOR_PROVIDER_ID } from "./shared.js";
3
- import { createCursorLanguageModel } from "./language-model.js";
3
+ import { createSdk, isCursorPackage } from "./plugin-core.js";
4
4
  import { adoptCompatHostCacheDir } from "./context/paths.js";
5
5
  /**
6
6
  * OpenCode Effect / Promise v2 plugin.
@@ -9,20 +9,6 @@ import { adoptCompatHostCacheDir } from "./context/paths.js";
9
9
  * (the V2 extension points). Auth still lives in the classic Hooks plugin
10
10
  * (`plugin.ts`) until OpenCode integrations fully replace provider OAuth.
11
11
  */
12
- function isCursorPackage(pkg, providerID) {
13
- if (providerID === CURSOR_PROVIDER_ID)
14
- return true;
15
- return (pkg.includes("cursor-opencode-provider") ||
16
- /cursor-opencode-provider[/\\]dist[/\\]index\.js/.test(pkg));
17
- }
18
- function createSdk(options) {
19
- const providerId = options.name || CURSOR_PROVIDER_ID;
20
- return {
21
- languageModel(modelId) {
22
- return createCursorLanguageModel(modelId, providerId, options);
23
- },
24
- };
25
- }
26
12
  export default define({
27
13
  id: "cursor.provider",
28
14
  setup: async (ctx) => {
package/dist/plugin.d.ts CHANGED
@@ -1,17 +1,3 @@
1
1
  import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
- import { type ModelInfo } from "./models.js";
3
- /**
4
- * Display names shared by both a thinking and a non-thinking model. A thinking
5
- * model with such a name needs a "Thinking" tag to disambiguate it from its
6
- * non-thinking twin (Cursor's Claude/Fable/Sonnet families). Models whose names
7
- * are already unique — including Cursor's GPT family, where the reasoning tier
8
- * ("None"/"Low"/"High"…) is baked into the name — are excluded, so they aren't
9
- * tagged redundantly.
10
- */
11
- export declare function thinkingSuffixBaseNames(models: ModelInfo[]): Set<string>;
12
- export declare function modelInfoToConfig(mi: ModelInfo, options?: {
13
- thinkingSuffix?: boolean;
14
- contextTier?: "base" | "long";
15
- }): Record<string, any>;
16
- export declare function modelsToConfig(models: ModelInfo[]): Record<string, any>;
2
+ export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js";
17
3
  export declare function CursorPlugin(input: PluginInput): Promise<Hooks>;
package/dist/plugin.js CHANGED
@@ -1,199 +1,19 @@
1
1
  import { CURSOR_COMPACTION_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
2
+ import { cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled } from "./plugin-core.js";
2
3
  import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs } from "./auth.js";
3
- import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, readCache, discoverModels, isCacheFresh, parseCursorContextLimit } from "./models.js";
4
+ import { readCache, discoverModels, isCacheFresh } from "./models.js";
5
+ // Re-exported for API compatibility: tests and downstream code import these
6
+ // from "./plugin.js". Canonical home is ./model-config.ts, which stays free of
7
+ // host plugin imports so the OpenCode 2.0 entrypoint can share it.
8
+ import { modelsToConfig } from "./model-config.js";
9
+ export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js";
4
10
  import { adoptCompatHostCacheDir, opencodeGlobalCacheDir } from "./context/paths.js";
5
11
  import { readStoredAuth } from "./context/auth-store.js";
6
12
  import { resolveAgentUrl } from "./agent-url.js";
7
13
  import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, } from "./shell-timeout.js";
8
14
  import { sessionActivity } from "./activity.js";
9
- import { openCodeWebSearchTool } from "./web-tools.js";
15
+ import { openCodeWebSearchTool } from "./web-search-tool.js";
10
16
  const MODULE_URL = new URL("./index.js", import.meta.url).href;
11
- /**
12
- * Strip characters and markup that break rendering in the OpenCode TUI/GUI from
13
- * a model or variant display name:
14
- * • HTML tags (e.g. `<span style="…">Medium</span>`) — the IDE colours variant
15
- * suffixes with a CSS var that doesn't exist in OpenCode, so the raw markup
16
- * would show as literal text. We drop the tags and keep the inner text
17
- * ("Medium").
18
- * • HTML/markup chars (`< > & " ' \``), parentheses.
19
- * • Tabs/newlines collapse to single spaces.
20
- * Dots and unicode letters are preserved so names stay readable.
21
- * Fixes https://github.com/oakimov/cursor-opencode-provider/issues/2.
22
- */
23
- function safeLabel(value) {
24
- return (value
25
- .replace(/<[^>]+>/g, "")
26
- .replace(/[()<>&"'`]/g, "")
27
- .replace(/\s+/g, " ")
28
- .trim() || "default");
29
- }
30
- function baseName(mi) {
31
- return safeLabel(mi.displayName ?? mi.id);
32
- }
33
- function modelInfoVariants(mi, variants) {
34
- if (variants.length === 0)
35
- return undefined;
36
- const entries = {};
37
- const usedKeys = new Set();
38
- const baseName = safeLabel(mi.displayName ?? mi.id);
39
- // Each variant's key is `safeLabel(displayName)` (the IDE's own label, e.g.
40
- // "Opus 4.8 1M High Fast Thinking") so the picker matches what the user
41
- // sees in Cursor. Two variants can sanitize to the same name when the IDE
42
- // wraps a differentiator in `<span>…</span>` (e.g. Composer's "Fast"
43
- // suffix collapses to the bare model name after stripping). To guarantee
44
- // every variant stays pickable:
45
- // 1. If the sanitized displayName matches the model name itself, suffix
46
- // it with distinguishing params (or "default") so it never collides
47
- // with the model entry in the variant panel.
48
- // 2. If two variants still collide, tag the later one with its params.
49
- // Suffixes must stay free of `()` / markup chars — same constraint as
50
- // safeLabel (issue #2); use spaced tokens, not parenthetical tags.
51
- const tagDims = (p) => {
52
- const labels = [];
53
- for (const d of p) {
54
- if (d.id === "fast" && d.value === "true")
55
- labels.push("Fast");
56
- else if (d.id === "thinking" && d.value === "true")
57
- labels.push("Thinking");
58
- else if (d.id === "context")
59
- labels.push(d.value);
60
- }
61
- if (labels.length > 0)
62
- return ` ${labels.join(" ")}`;
63
- // No params at all — still disambiguate from the model name itself.
64
- if (p.length === 0)
65
- return "";
66
- return " default";
67
- };
68
- for (const v of variants) {
69
- const sanitized = safeLabel(v.displayName || v.key || "default");
70
- let key = sanitized;
71
- // Never let a variant key equal the model name — that would make the
72
- // variant entry indistinguishable from the model entry in pickers that
73
- // collapse them.
74
- if (key === baseName && !usedKeys.has(key)) {
75
- key = `${baseName}${tagDims(v.parameterValues)}` || `${baseName} default`;
76
- }
77
- else if (usedKeys.has(key)) {
78
- key = `${sanitized}${tagDims(v.parameterValues)}`;
79
- }
80
- let n = 2;
81
- while (usedKeys.has(key))
82
- key = `${sanitized}${tagDims(v.parameterValues)} ${n++}`;
83
- usedKeys.add(key);
84
- entries[key] = {
85
- [CURSOR_VARIANT_PARAMETERS_KEY]: v.parameterValues.map((p) => ({ ...p })),
86
- };
87
- }
88
- return entries;
89
- }
90
- function isLongContextVariant(v) {
91
- return v.parameterValues.some((p) => p.id === "context" && parseCursorContextLimit(p.value) === 1_000_000);
92
- }
93
- function variantsForTier(mi, tier) {
94
- return mi.variants.filter((v) => isLongContextVariant(v) === (tier === "long"));
95
- }
96
- /**
97
- * Display names shared by both a thinking and a non-thinking model. A thinking
98
- * model with such a name needs a "Thinking" tag to disambiguate it from its
99
- * non-thinking twin (Cursor's Claude/Fable/Sonnet families). Models whose names
100
- * are already unique — including Cursor's GPT family, where the reasoning tier
101
- * ("None"/"Low"/"High"…) is baked into the name — are excluded, so they aren't
102
- * tagged redundantly.
103
- */
104
- export function thinkingSuffixBaseNames(models) {
105
- const flags = new Map();
106
- for (const m of models) {
107
- const base = baseName(m);
108
- const entry = flags.get(base) ?? { hasThinking: false, hasNonThinking: false };
109
- if (m.supportsThinking)
110
- entry.hasThinking = true;
111
- else
112
- entry.hasNonThinking = true;
113
- flags.set(base, entry);
114
- }
115
- const ambiguous = new Set();
116
- for (const [base, f] of flags)
117
- if (f.hasThinking && f.hasNonThinking)
118
- ambiguous.add(base);
119
- return ambiguous;
120
- }
121
- export function modelInfoToConfig(mi, options = {}) {
122
- const contextTier = options.contextTier ?? "base";
123
- const variants = variantsForTier(mi, contextTier);
124
- let name = baseName(mi);
125
- if (options.thinkingSuffix)
126
- name += " Thinking";
127
- if (contextTier === "long")
128
- name += " 1M";
129
- // OpenCode's context limit is static per model entry, while Cursor's context
130
- // tier is a variant parameter. Long-context choices are therefore emitted as
131
- // separate OpenCode entries by modelsToConfig.
132
- const context = contextTier === "long"
133
- ? (mi.maxContextForMaxMode ?? 1_000_000)
134
- : (mi.maxContext ?? 200_000);
135
- // OpenCode's overflow/compaction/UI use limit.context; generation and
136
- // thinking budgets use limit.output. models.dev 1M peers advertise
137
- // 64k–128k output — a tiny cap makes long-context sessions feel broken
138
- // even when the 1M input window is correct.
139
- const output = contextTier === "long" ? 128_000 : 32_000;
140
- const config = {
141
- name,
142
- reasoning: mi.supportsThinking ?? false,
143
- tool_call: mi.supportsAgent ?? true,
144
- temperature: false,
145
- limit: {
146
- context,
147
- output,
148
- },
149
- };
150
- const variantConfig = modelInfoVariants(mi, variants);
151
- if (variantConfig)
152
- config.variants = variantConfig;
153
- if (contextTier === "long") {
154
- const defaultVariant = variants.find((v) => v.isDefaultMax) ?? variants[0];
155
- if (defaultVariant) {
156
- config.options = {
157
- [CURSOR_WIRE_MODEL_ID_KEY]: mi.id,
158
- [CURSOR_VARIANT_PARAMETERS_KEY]: defaultVariant.parameterValues.map((p) => ({ ...p })),
159
- };
160
- }
161
- }
162
- return config;
163
- }
164
- export function modelsToConfig(models) {
165
- const ambiguous = thinkingSuffixBaseNames(models);
166
- const out = {};
167
- const usedIds = new Set(models.map((m) => m.id));
168
- for (const m of models) {
169
- const thinkingSuffix = !!m.supportsThinking && ambiguous.has(baseName(m));
170
- const baseVariants = variantsForTier(m, "base");
171
- const longVariants = variantsForTier(m, "long");
172
- if (baseVariants.length > 0 || longVariants.length === 0) {
173
- out[m.id] = modelInfoToConfig(m, { thinkingSuffix, contextTier: "base" });
174
- }
175
- if (longVariants.length === 0)
176
- continue;
177
- if (baseVariants.length === 0) {
178
- out[m.id] = modelInfoToConfig(m, { thinkingSuffix, contextTier: "long" });
179
- continue;
180
- }
181
- let longId = `${m.id}-1m`;
182
- let suffix = 2;
183
- while (usedIds.has(longId))
184
- longId = `${m.id}-1m-${suffix++}`;
185
- usedIds.add(longId);
186
- out[longId] = modelInfoToConfig(m, { thinkingSuffix, contextTier: "long" });
187
- }
188
- return out;
189
- }
190
- function cursorApiBaseURL() {
191
- return process.env.CURSOR_API_BASE_URL ?? `https://${CURSOR_API_HOST}`;
192
- }
193
- function cursorGetServerConfigTelemetryEnabled() {
194
- return (process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "1" ||
195
- process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "true");
196
- }
197
17
  export async function CursorPlugin(input) {
198
18
  // Prefer strong OCP host identity when available; otherwise resolve from explicit host signals/install path.
199
19
  await adoptCompatHostCacheDir();
@@ -443,10 +263,10 @@ export async function CursorPlugin(input) {
443
263
  type: "text",
444
264
  key: "apiKey",
445
265
  message: "Cursor API key",
446
- placeholder: "sk-...",
266
+ placeholder: "crsr_...",
447
267
  validate(value) {
448
- if (!value.startsWith("sk-"))
449
- return "API key should start with sk-";
268
+ if (!value.startsWith("crsr_"))
269
+ return "API key should start with crsr_";
450
270
  return undefined;
451
271
  },
452
272
  },
@@ -3,6 +3,16 @@ export declare function createMessageTypes(): protobuf.Root;
3
3
  export declare function getMessageTypes(): protobuf.Root;
4
4
  export declare function encodeMessage(typeName: string, message: Record<string, unknown>): Uint8Array;
5
5
  export declare function decodeMessage<T = Record<string, unknown>>(typeName: string, data: Uint8Array): T;
6
+ /**
7
+ * Diagnostic-only: walk AgentServerMessage.interaction_update(1).turn_ended(14)
8
+ * without going through our hand-written TurnEnded schema, so a `cache_write`/
9
+ * `reasoning_tokens` question can be checked against the raw wire instead of
10
+ * guessed. Confirmed field ids 1-5 (input/output/cache_read/cache_write/
11
+ * reasoning) decode correctly. Live captures routinely include `f4` explicitly
12
+ * as `0` while `f3` (cache_read) and `f5` (reasoning) are non-zero — Cursor's
13
+ * agent TurnEnded populates the field but does not report write counts here.
14
+ */
15
+ export declare function debugWalkTurnEnded(payload: Uint8Array): string;
6
16
  /**
7
17
  * Decode a protobuf sub-message from a frame payload that wraps it in
8
18
  * a top-level field key + length varint. Skips the outer wrapper and
@@ -366,10 +366,29 @@ export function createMessageTypes() {
366
366
  { id: 1, name: "path", type: "string" },
367
367
  { id: 2, name: "error", type: "string" },
368
368
  ]);
369
+ // Cursor's LocalReadExecutor validates the target before execution and, for a
370
+ // missing / non-regular path, returns one of these typed ReadResult cases
371
+ // instead of running the tool. Mirroring the exact field ids/shapes lets this
372
+ // provider reply the same way so the model sees a structured file-not-found
373
+ // observation and the host never surfaces a permission prompt.
374
+ addType(root, "ReadFileNotFound", [{ id: 1, name: "path", type: "string" }]);
375
+ addType(root, "ReadPermissionDenied", [{ id: 1, name: "path", type: "string" }]);
376
+ addType(root, "ReadInvalidFile", [
377
+ { id: 1, name: "path", type: "string" },
378
+ { id: 2, name: "reason", type: "string" },
379
+ ]);
369
380
  addType(root, "ReadResult", [
370
381
  { id: 1, name: "success", type: "ReadSuccess" },
371
382
  { id: 2, name: "error", type: "ReadError" },
372
- ], [{ name: "result", fields: ["success", "error"] }]);
383
+ { id: 4, name: "file_not_found", type: "ReadFileNotFound" },
384
+ { id: 5, name: "permission_denied", type: "ReadPermissionDenied" },
385
+ { id: 6, name: "invalid_file", type: "ReadInvalidFile" },
386
+ ], [
387
+ {
388
+ name: "result",
389
+ fields: ["success", "error", "file_not_found", "permission_denied", "invalid_file"],
390
+ },
391
+ ]);
373
392
  addType(root, "GrepArgs", [
374
393
  { id: 1, name: "pattern", type: "string" },
375
394
  { id: 2, name: "path", type: "string" },
@@ -1190,6 +1209,62 @@ export function decodeMessage(typeName, data) {
1190
1209
  const decoded = type.decode(data);
1191
1210
  return type.toObject(decoded, { defaults: true, json: true, longs: Number });
1192
1211
  }
1212
+ /** Schema-free walk of one message's top-level wire fields (no nested descent). */
1213
+ function readRawFields(buf) {
1214
+ const reader = protobuf.Reader.create(buf);
1215
+ const fields = [];
1216
+ while (reader.pos < reader.len) {
1217
+ const tag = reader.uint32();
1218
+ const field = tag >>> 3;
1219
+ const wireType = tag & 7;
1220
+ switch (wireType) {
1221
+ case 0:
1222
+ fields.push({ field, wireType, varint: reader.uint64().toString() });
1223
+ break;
1224
+ case 1:
1225
+ fields.push({ field, wireType, fixed: reader.fixed64().toString() });
1226
+ break;
1227
+ case 2:
1228
+ fields.push({ field, wireType, bytes: reader.bytes() });
1229
+ break;
1230
+ case 5:
1231
+ fields.push({ field, wireType, fixed: reader.fixed32() });
1232
+ break;
1233
+ default:
1234
+ // Groups (3/4) aren't used on this wire; bail rather than desync the reader.
1235
+ return fields;
1236
+ }
1237
+ }
1238
+ return fields;
1239
+ }
1240
+ /**
1241
+ * Diagnostic-only: walk AgentServerMessage.interaction_update(1).turn_ended(14)
1242
+ * without going through our hand-written TurnEnded schema, so a `cache_write`/
1243
+ * `reasoning_tokens` question can be checked against the raw wire instead of
1244
+ * guessed. Confirmed field ids 1-5 (input/output/cache_read/cache_write/
1245
+ * reasoning) decode correctly. Live captures routinely include `f4` explicitly
1246
+ * as `0` while `f3` (cache_read) and `f5` (reasoning) are non-zero — Cursor's
1247
+ * agent TurnEnded populates the field but does not report write counts here.
1248
+ */
1249
+ export function debugWalkTurnEnded(payload) {
1250
+ try {
1251
+ const top = readRawFields(payload);
1252
+ const iuField = top.find((f) => f.field === 1 && f.wireType === 2);
1253
+ if (!iuField?.bytes)
1254
+ return "(no interaction_update field)";
1255
+ const iu = readRawFields(iuField.bytes);
1256
+ const teField = iu.find((f) => f.field === 14 && f.wireType === 2);
1257
+ if (!teField?.bytes)
1258
+ return "(no turn_ended field in interaction_update)";
1259
+ const te = readRawFields(teField.bytes);
1260
+ return te
1261
+ .map((f) => `f${f.field}:wt${f.wireType}=${f.varint ?? f.fixed ?? (f.bytes ? `bytes[${f.bytes.length}]` : "?")}`)
1262
+ .join(" ");
1263
+ }
1264
+ catch (e) {
1265
+ return `(walk failed: ${e.message})`;
1266
+ }
1267
+ }
1193
1268
  /**
1194
1269
  * Decode a protobuf sub-message from a frame payload that wraps it in
1195
1270
  * a top-level field key + length varint. Skips the outer wrapper and
@@ -1,4 +1,4 @@
1
- import { mapCursorArgsToOpencode, mcpRealToolName } from "./tools.js";
1
+ import { mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName } from "./tools.js";
2
2
  import { decodeStructEntriesToJson } from "./struct.js";
3
3
  const TODO_STATUS = {
4
4
  0: "pending",
@@ -334,7 +334,27 @@ function openCodeSubagentType(raw) {
334
334
  if (typeof custom?.name === "string" && custom.name.length > 0)
335
335
  return custom.name;
336
336
  if (subtype.explore != null)
337
- return "explore";
337
+ return mapCursorSubagentTypeToOpenCode("explore");
338
+ if (subtype.cursor_guide != null)
339
+ return mapCursorSubagentTypeToOpenCode("cursor_guide");
340
+ if (subtype.bash != null)
341
+ return mapCursorSubagentTypeToOpenCode("bash");
342
+ if (subtype.shell != null)
343
+ return mapCursorSubagentTypeToOpenCode("shell");
344
+ if (subtype.debug != null)
345
+ return mapCursorSubagentTypeToOpenCode("debug");
346
+ if (subtype.computer_use != null)
347
+ return mapCursorSubagentTypeToOpenCode("computer_use");
348
+ if (subtype.browser_use != null)
349
+ return mapCursorSubagentTypeToOpenCode("browser_use");
350
+ if (subtype.media_review != null)
351
+ return mapCursorSubagentTypeToOpenCode("media_review");
352
+ if (subtype.watch_video != null)
353
+ return mapCursorSubagentTypeToOpenCode("watch_video");
354
+ if (subtype.vm_setup_helper != null)
355
+ return mapCursorSubagentTypeToOpenCode("vm_setup_helper");
356
+ if (subtype.unspecified != null)
357
+ return mapCursorSubagentTypeToOpenCode("unspecified");
338
358
  return undefined;
339
359
  }
340
360
  /** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */
@@ -64,6 +64,29 @@ export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], provider
64
64
  export declare function buildLiveRequestContext(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>): Record<string, unknown>;
65
65
  export declare function mapExecServerToToolName(execField: string): string | undefined;
66
66
  export declare function mapToolNameToExecField(toolName: string): string | undefined;
67
+ export type HostSubagentDefinition = {
68
+ name: string;
69
+ description?: string;
70
+ };
71
+ export type HostSubagentCatalog = {
72
+ executor?: "task" | "actor";
73
+ agents: HostSubagentDefinition[];
74
+ /** True when the host tool supplied its complete, permission-filtered catalog. */
75
+ complete: boolean;
76
+ };
77
+ /** Extract the current host's permission-filtered Task/Actor recipient catalog. */
78
+ export declare function extractHostSubagentCatalog(tools: OpencodeToolDef[]): HostSubagentCatalog;
79
+ /** Resolve a Cursor subtype against the agents the host can spawn this turn. */
80
+ export declare function resolveCursorSubagentType(subagentType: string, catalog?: HostSubagentCatalog): string | undefined;
81
+ /**
82
+ * Cursor's native Task/subagent protocol uses Cursor-owned subtype names while
83
+ * OpenCode-family hosts execute named agents. Map known Cursor built-ins to
84
+ * the closest host agent and preserve unknown values so user-defined OpenCode
85
+ * agents can still be called by exact name.
86
+ */
87
+ export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string;
88
+ /** Resolve a native Cursor subagent request to this turn's executor and catalog. */
89
+ export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, catalog?: HostSubagentCatalog): void;
67
90
  export type ParsedExecRequest = {
68
91
  id: number;
69
92
  execId: string;
@@ -92,6 +115,32 @@ export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<st
92
115
  * → github_create_pull_request). Builtins under the default server stay bare.
93
116
  */
94
117
  export declare function mcpRealToolName(mcpArgs: Record<string, unknown>, defaultServer?: string): string;
118
+ /**
119
+ * Resolve a read target the way Cursor's LocalReadExecutor does before it
120
+ * stats the file: untildify, then resolve a relative path against the workspace
121
+ * root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with
122
+ * agent utils `resolvePath(path, workspaceRoot)`.
123
+ */
124
+ export declare function resolveReadTargetPath(requested: string, workspaceRoot: string): string;
125
+ /**
126
+ * A typed ReadResult oneof case for a read target that fails Cursor's
127
+ * pre-execution validation, or undefined when the path is a readable file the
128
+ * tool should actually read. Mirrors LocalReadExecutor exactly:
129
+ * - missing path (ENOENT/ENOTDIR) → file_not_found
130
+ * - directory → invalid_file "Path is a directory, not a file"
131
+ * - socket/fifo/etc (not a file) → invalid_file "Path is neither a file nor a directory"
132
+ * EACCES/EPERM (exists but unreadable) and any other stat error return undefined
133
+ * so the call proceeds to OpenCode and a genuine permission decision is never
134
+ * masked. Never rejects a non-existent *write/edit* target — this is read-only.
135
+ */
136
+ export declare function classifyMissingReadTarget(absolutePath: string): Record<string, unknown> | undefined;
137
+ /**
138
+ * Encode a pre-execution read rejection as the exact frames Cursor's client
139
+ * emits: the ExecClientMessage carrying the typed ReadResult oneof case, then
140
+ * the ACM #5 stream_close for that id. `readResult` is the case object from
141
+ * classifyMissingReadTarget (e.g. `{ file_not_found: { path } }`).
142
+ */
143
+ export declare function buildReadRejectionMessages(execId: number, readResult: Record<string, unknown>): Uint8Array[];
95
144
  export type ToolResultInput = {
96
145
  execId: number;
97
146
  /** ExecClientMessage result field (from ParsedExecRequest.resultField). */
@@ -109,6 +158,13 @@ export type ToolResultInput = {
109
158
  resultMetadata?: Record<string, unknown>;
110
159
  /** Structured shell completion captured by the OpenCode plugin hook. */
111
160
  shellOutcome?: CursorShellOutcome;
161
+ /**
162
+ * Workspace root for result shapes that must name a directory
163
+ * (grep_result workspace_results keys, ls_result directory_tree_root).
164
+ * Must not fall back to process.cwd() — the OpenCode 2.0 daemon's cwd is
165
+ * often $HOME and would re-advertise the wrong folder to the model.
166
+ */
167
+ workspaceRoot?: string;
112
168
  };
113
169
  /**
114
170
  * Build one or more ExecClientMessage frames for a tool result.
@@ -146,7 +202,7 @@ export declare function unwrapReadOutput(output: string): string;
146
202
  * OpenCode returns free-form text; we wrap it in the minimal success shape the
147
203
  * server accepts (verified against agent.v1 wire captures).
148
204
  */
149
- export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome): Record<string, unknown>;
205
+ export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome, workspaceRoot?: string): Record<string, unknown>;
150
206
  export declare function buildToolCallPart(execMsg: ParsedExecRequest, sessionId: string): {
151
207
  toolCallId: string;
152
208
  toolName: string;