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.
- package/README.md +143 -11
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +9 -0
- package/dist/compaction-marker.d.ts +14 -0
- package/dist/compaction-marker.js +36 -0
- package/dist/context/agents.d.ts +5 -1
- package/dist/context/agents.js +90 -30
- package/dist/context/build.js +57 -8
- package/dist/context/env.js +6 -2
- package/dist/context/paths.d.ts +10 -0
- package/dist/context/paths.js +24 -0
- package/dist/context/plugins.js +16 -12
- package/dist/context/rules.js +26 -23
- package/dist/context/skills.js +8 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +2 -8
- package/dist/language-model.js +121 -35
- package/dist/model-config.d.ts +15 -0
- package/dist/model-config.js +188 -0
- package/dist/opencode2/catalog.d.ts +33 -0
- package/dist/opencode2/catalog.js +85 -0
- package/dist/opencode2/integration.d.ts +23 -0
- package/dist/opencode2/integration.js +124 -0
- package/dist/opencode2/types.d.ts +316 -0
- package/dist/opencode2/types.js +18 -0
- package/dist/plugin-core.d.ts +27 -0
- package/dist/plugin-core.js +31 -0
- package/dist/plugin-opencode2.d.ts +3 -0
- package/dist/plugin-opencode2.js +293 -0
- package/dist/plugin-v2.d.ts +7 -0
- package/dist/plugin-v2.js +1 -15
- package/dist/plugin.d.ts +1 -15
- package/dist/plugin.js +11 -191
- package/dist/protocol/messages.d.ts +10 -0
- package/dist/protocol/messages.js +76 -1
- package/dist/protocol/tool-call-bridge.js +22 -2
- package/dist/protocol/tools.d.ts +57 -1
- package/dist/protocol/tools.js +277 -17
- package/dist/session-directory.d.ts +17 -0
- package/dist/session-directory.js +35 -0
- package/dist/session.d.ts +16 -2
- package/dist/session.js +70 -1
- package/dist/shell-timeout.d.ts +3 -1
- package/dist/shell-timeout.js +6 -2
- package/dist/usage.d.ts +30 -0
- package/dist/usage.js +77 -0
- package/dist/web-search-tool.d.ts +33 -0
- package/dist/web-search-tool.js +22 -0
- package/dist/web-tools.d.ts +9 -25
- package/dist/web-tools.js +34 -36
- package/package.json +7 -2
package/dist/plugin-v2.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
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: "
|
|
266
|
+
placeholder: "crsr_...",
|
|
447
267
|
validate(value) {
|
|
448
|
-
if (!value.startsWith("
|
|
449
|
-
return "API key should start with
|
|
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
|
-
|
|
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. */
|
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -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;
|