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/context/paths.js
CHANGED
|
@@ -4,6 +4,30 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { trace } from "../debug.js";
|
|
7
|
+
/** Host-neutral bridge installed by OCP before an unchanged provider is loaded. */
|
|
8
|
+
export const OPENCODE_PATH_BRIDGE = Symbol.for("opencode.compat.path-bridge");
|
|
9
|
+
function pathBridge() {
|
|
10
|
+
const value = globalThis[OPENCODE_PATH_BRIDGE];
|
|
11
|
+
if (!value || typeof value !== "object")
|
|
12
|
+
return undefined;
|
|
13
|
+
const bridge = value;
|
|
14
|
+
return typeof bridge.projectConfigDirs === "function" && typeof bridge.globalConfigDirs === "function"
|
|
15
|
+
? bridge
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
export function opencodeProjectConfigDirs(workspaceRoot) {
|
|
19
|
+
return pathBridge()?.projectConfigDirs(path.resolve(workspaceRoot)) ?? [
|
|
20
|
+
path.join(path.resolve(workspaceRoot), ".opencode"),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
export function opencodeGlobalConfigDirs() {
|
|
24
|
+
return pathBridge()?.globalConfigDirs() ?? [opencodeGlobalConfigDir()];
|
|
25
|
+
}
|
|
26
|
+
export function opencodeConfigFileNames() {
|
|
27
|
+
return pathBridge()?.configFileNames?.length
|
|
28
|
+
? [...pathBridge().configFileNames]
|
|
29
|
+
: ["opencode.json", "opencode.jsonc"];
|
|
30
|
+
}
|
|
7
31
|
/** Explicit host cache root (e.g. Effect v2 `Path.cache`, or `createCursor({ cacheDir })`). */
|
|
8
32
|
let hostCacheDirOverride;
|
|
9
33
|
function resolveHome(env = process.env) {
|
package/dist/context/plugins.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
|
|
4
4
|
async function listLocalPlugins(dir) {
|
|
5
5
|
try {
|
|
6
6
|
await stat(dir);
|
|
@@ -34,17 +34,21 @@ export async function collectPlugins(workspaceRoot, config) {
|
|
|
34
34
|
seen.add(id);
|
|
35
35
|
out.push({ id, source: "npm" });
|
|
36
36
|
}
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) {
|
|
38
|
+
for (const p of await listLocalPlugins(path.join(configDir, "plugins"))) {
|
|
39
|
+
if (seen.has(p.id))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(p.id);
|
|
42
|
+
out.push(p);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const configDir of opencodeGlobalConfigDirs()) {
|
|
46
|
+
for (const p of await listLocalPlugins(path.join(configDir, "plugins"))) {
|
|
47
|
+
if (seen.has(p.id))
|
|
48
|
+
continue;
|
|
49
|
+
seen.add(p.id);
|
|
50
|
+
out.push(p);
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
return out;
|
|
50
54
|
}
|
package/dist/context/rules.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs, resolveHomeRelative } from "./paths.js";
|
|
5
5
|
async function exists(file) {
|
|
6
6
|
try {
|
|
7
7
|
await stat(file);
|
|
@@ -12,7 +12,7 @@ async function exists(file) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
async function readJsonConfig(dir) {
|
|
15
|
-
for (const name of
|
|
15
|
+
for (const name of opencodeConfigFileNames()) {
|
|
16
16
|
const file = path.join(dir, name);
|
|
17
17
|
if (!(await exists(file)))
|
|
18
18
|
continue;
|
|
@@ -151,29 +151,30 @@ export async function fetchRemoteInstruction(url, timeoutMs = 5000) {
|
|
|
151
151
|
clearTimeout(timer);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
const globalConfig = await readJsonConfig(opencodeGlobalConfigDir());
|
|
156
|
-
if (isProjectConfigDisabled()) {
|
|
157
|
-
return {
|
|
158
|
-
...globalConfig,
|
|
159
|
-
instructions: [...(globalConfig.instructions ?? [])],
|
|
160
|
-
plugin: [...(globalConfig.plugin ?? [])],
|
|
161
|
-
plugins: [...(globalConfig.plugins ?? [])],
|
|
162
|
-
mcp: { ...(globalConfig.mcp ?? {}) },
|
|
163
|
-
permission: globalConfig.permission,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
const projectConfig = await readJsonConfig(workspaceRoot);
|
|
154
|
+
function mergeConfig(base, overlay) {
|
|
167
155
|
return {
|
|
168
|
-
...
|
|
169
|
-
...
|
|
170
|
-
instructions: [...(
|
|
171
|
-
plugin: [...new Set([...(
|
|
172
|
-
plugins: [...new Set([...(
|
|
173
|
-
mcp: { ...(
|
|
174
|
-
permission:
|
|
156
|
+
...base,
|
|
157
|
+
...overlay,
|
|
158
|
+
instructions: [...(base.instructions ?? []), ...(overlay.instructions ?? [])],
|
|
159
|
+
plugin: [...new Set([...(base.plugin ?? []), ...(overlay.plugin ?? [])])],
|
|
160
|
+
plugins: [...new Set([...(base.plugins ?? []), ...(overlay.plugins ?? [])])],
|
|
161
|
+
mcp: { ...(base.mcp ?? {}), ...(overlay.mcp ?? {}) },
|
|
162
|
+
permission: overlay.permission ?? base.permission,
|
|
175
163
|
};
|
|
176
164
|
}
|
|
165
|
+
export async function loadMergedConfig(workspaceRoot) {
|
|
166
|
+
const globalConfig = await readJsonConfig(opencodeGlobalConfigDirs()[0] ?? "");
|
|
167
|
+
if (isProjectConfigDisabled())
|
|
168
|
+
return mergeConfig({}, globalConfig);
|
|
169
|
+
// The bridge supplies native project config roots for an unchanged plugin:
|
|
170
|
+
// .opencode on OpenCode, .mimocode on MiMo, and .kilo/.kilocode on Kilo.
|
|
171
|
+
// Later roots have higher precedence, matching the host's native ordering.
|
|
172
|
+
let projectConfig = await readJsonConfig(workspaceRoot);
|
|
173
|
+
for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) {
|
|
174
|
+
projectConfig = mergeConfig(projectConfig, await readJsonConfig(configDir));
|
|
175
|
+
}
|
|
176
|
+
return mergeConfig(globalConfig, projectConfig);
|
|
177
|
+
}
|
|
177
178
|
/**
|
|
178
179
|
* Collect OpenCode instruction files.
|
|
179
180
|
*/
|
|
@@ -205,7 +206,9 @@ export async function collectRules(workspaceRoot) {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
|
-
|
|
209
|
+
for (const globalDir of opencodeGlobalConfigDirs()) {
|
|
210
|
+
await add(path.join(globalDir, "AGENTS.md"));
|
|
211
|
+
}
|
|
209
212
|
await add(path.join(homedir(), ".claude", "CLAUDE.md"));
|
|
210
213
|
for (const raw of config.instructions ?? []) {
|
|
211
214
|
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
package/dist/context/skills.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
|
|
5
5
|
async function exists(p) {
|
|
6
6
|
try {
|
|
7
7
|
await stat(p);
|
|
@@ -94,8 +94,13 @@ async function walkAncestorsFor(dir, rel, stop, out) {
|
|
|
94
94
|
export async function collectSkills(workspaceRoot, worktree) {
|
|
95
95
|
const out = new Map();
|
|
96
96
|
const home = homedir();
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
for (const projectDir of opencodeProjectConfigDirs(workspaceRoot)) {
|
|
98
|
+
const relative = path.relative(workspaceRoot, projectDir);
|
|
99
|
+
await walkAncestorsFor(workspaceRoot, path.join(relative, "skills"), worktree, out);
|
|
100
|
+
}
|
|
101
|
+
for (const globalDir of opencodeGlobalConfigDirs()) {
|
|
102
|
+
await scanSkillsRoot(path.join(globalDir, "skills"), out);
|
|
103
|
+
}
|
|
99
104
|
await walkAncestorsFor(workspaceRoot, path.join(".claude", "skills"), worktree, out);
|
|
100
105
|
await scanSkillsRoot(path.join(home, ".claude", "skills"), out);
|
|
101
106
|
await walkAncestorsFor(workspaceRoot, path.join(".agents", "skills"), worktree, out);
|
package/dist/index.d.ts
CHANGED
|
@@ -34,9 +34,7 @@ export type CreateCursorOptions = {
|
|
|
34
34
|
/** Fresh-turn retry policy. Defaults: 3 attempts, 500ms base, 8000ms cap. */
|
|
35
35
|
retry?: CursorRetryOptions;
|
|
36
36
|
};
|
|
37
|
-
export declare function createCursor(options: CreateCursorOptions):
|
|
38
|
-
languageModel(modelId: string): import("@ai-sdk/provider").LanguageModelV3;
|
|
39
|
-
};
|
|
37
|
+
export declare function createCursor(options: CreateCursorOptions): import("./plugin-core.js").CursorSdk;
|
|
40
38
|
export { CursorPlugin };
|
|
41
39
|
export type { CursorContinuationOptions, CursorContinuationPolicy } from "./session.js";
|
|
42
40
|
export default CursorPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createCursorLanguageModel } from "./language-model.js";
|
|
1
|
+
import { createSdk } from "./plugin-core.js";
|
|
3
2
|
import { CursorPlugin } from "./plugin.js";
|
|
4
3
|
export function createCursor(options) {
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
languageModel(modelId) {
|
|
8
|
-
return createCursorLanguageModel(modelId, providerId, options);
|
|
9
|
-
},
|
|
10
|
-
};
|
|
4
|
+
return createSdk(options);
|
|
11
5
|
}
|
|
12
6
|
export { CursorPlugin };
|
|
13
7
|
export default CursorPlugin;
|
package/dist/language-model.js
CHANGED
|
@@ -5,8 +5,8 @@ import { bidiRunStream, CursorRunInterruptedError, normalizeAgentRunOrigin, } fr
|
|
|
5
5
|
import { trace, traceRequestContextPaths } from "./debug.js";
|
|
6
6
|
import { buildRunRequest, buildHeartbeat } from "./protocol/request.js";
|
|
7
7
|
import { decodeFramePayload } from "./protocol/framing.js";
|
|
8
|
-
import { decodeMessage } from "./protocol/messages.js";
|
|
9
|
-
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, resolveCustomWebToolAlias, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, } from "./protocol/tools.js";
|
|
8
|
+
import { debugWalkTurnEnded, decodeMessage } from "./protocol/messages.js";
|
|
9
|
+
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, } from "./protocol/tools.js";
|
|
10
10
|
import { describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
11
11
|
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
|
|
12
12
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
@@ -22,9 +22,12 @@ import { workspaceRootFromRequestContext } from "./context/env.js";
|
|
|
22
22
|
import { opencodeGlobalCacheDir, setHostCacheDirOverride } from "./context/paths.js";
|
|
23
23
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
24
24
|
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION } from "./shared.js";
|
|
25
|
+
import { isCompactionSession } from "./compaction-marker.js";
|
|
26
|
+
import { getSessionDirectory } from "./session-directory.js";
|
|
25
27
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
26
28
|
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
27
29
|
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
30
|
+
import { buildLanguageModelV3UsageFromEstimate, buildLanguageModelV3UsageFromTurnEnded, turnEndedCounter, } from "./usage.js";
|
|
28
31
|
let _availableModels;
|
|
29
32
|
// mtime of the cache file the last time we loaded it. Compared on each call
|
|
30
33
|
// so discoverModels' background refresh is picked up without a process restart.
|
|
@@ -275,7 +278,7 @@ export function createCursorLanguageModel(modelId, providerId, options) {
|
|
|
275
278
|
};
|
|
276
279
|
}
|
|
277
280
|
async function doStreamImpl(modelId, options, callOptions) {
|
|
278
|
-
// A raw `
|
|
281
|
+
// A raw `crsr_...` API key must be exchanged for a JWT before it can be used
|
|
279
282
|
// as a Bearer token (the plugin path does this in auth.ts). The accessToken
|
|
280
283
|
// path is already a JWT from OAuth/key-exchange, so we use it as-is.
|
|
281
284
|
// resolveBearerToken caches apiKey exchanges so we don't hit /auth/exchange
|
|
@@ -445,8 +448,10 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
445
448
|
const sessionKey = opencodeSessionKey(callOptions);
|
|
446
449
|
const providerOptions = callOptions.providerOptions?.cursor;
|
|
447
450
|
// The classic plugin marks OpenCode's agent="compaction" through chat.params.
|
|
451
|
+
// OpenCode 2.0 removed that hook, so its plugin records the same fact against
|
|
452
|
+
// the session id from session.hook("context") instead; consult both.
|
|
448
453
|
// Do not infer this from tools/toolChoice: standalone no-tool calls are valid.
|
|
449
|
-
const isCompaction = providerOptions?.[CURSOR_COMPACTION_OPTION] === true;
|
|
454
|
+
const isCompaction = providerOptions?.[CURSOR_COMPACTION_OPTION] === true || isCompactionSession(sessionKey);
|
|
450
455
|
const toolState = resolveTurnToolState({
|
|
451
456
|
sessionKey,
|
|
452
457
|
incomingTools,
|
|
@@ -463,6 +468,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
463
468
|
trace(`web tool alias: ${alias} -> ${original}`);
|
|
464
469
|
}
|
|
465
470
|
const allowTools = toolState.allowTools;
|
|
471
|
+
const discoveredSubagentCatalog = extractHostSubagentCatalog(cursorTools);
|
|
466
472
|
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
467
473
|
const recovery = startOptions?.recovery;
|
|
468
474
|
const resuming = recovery?.kind === "resume";
|
|
@@ -481,7 +487,12 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
481
487
|
const userText = recovery?.kind === "rebase"
|
|
482
488
|
? "Continue the interrupted turn from the conversation history above. Do not repeat completed work."
|
|
483
489
|
: (extractUserText([...prompt].reverse().find((m) => m.role === "user")) || ".");
|
|
484
|
-
|
|
490
|
+
// v1 sets `options.workspaceRoot` correctly per invocation (`input.directory`,
|
|
491
|
+
// one plugin instance per project). OpenCode 2.0 runs one daemon across many
|
|
492
|
+
// projects, so its static `options.workspaceRoot` is only a last-resort
|
|
493
|
+
// fallback; `getSessionDirectory` carries the real per-session directory
|
|
494
|
+
// recorded from `session.hook("context")` in `plugin-opencode2.ts`.
|
|
495
|
+
const workspaceRoot = path.resolve(getSessionDirectory(sessionKey) ?? (options.workspaceRoot || process.cwd()));
|
|
485
496
|
const baseSystemPrompt = extractSystemPrompt(prompt);
|
|
486
497
|
const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot);
|
|
487
498
|
const systemPrompt = interactionGuidance
|
|
@@ -526,6 +537,23 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
526
537
|
headers: options.headers,
|
|
527
538
|
});
|
|
528
539
|
const requestContext = await buildRequestContext({ workspaceRoot, tools: cursorTools });
|
|
540
|
+
const contextSubagents = Array.isArray(requestContext.custom_subagents)
|
|
541
|
+
? requestContext.custom_subagents
|
|
542
|
+
.map((agent) => agent && typeof agent === "object" && typeof agent.name === "string"
|
|
543
|
+
? {
|
|
544
|
+
name: agent.name,
|
|
545
|
+
description: typeof agent.description === "string"
|
|
546
|
+
? agent.description
|
|
547
|
+
: undefined,
|
|
548
|
+
}
|
|
549
|
+
: undefined)
|
|
550
|
+
.filter((agent) => !!agent)
|
|
551
|
+
: [];
|
|
552
|
+
const subagentCatalog = {
|
|
553
|
+
...discoveredSubagentCatalog,
|
|
554
|
+
agents: [...new Map([...discoveredSubagentCatalog.agents, ...contextSubagents]
|
|
555
|
+
.map((agent) => [agent.name, agent])).values()],
|
|
556
|
+
};
|
|
529
557
|
// Resolve descriptors once from the merged OpenCode config so MCP identity is
|
|
530
558
|
// consistent across AgentRunRequest and both request_context reply paths.
|
|
531
559
|
const toolDescriptors = Array.isArray(requestContext.tools)
|
|
@@ -565,6 +593,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
565
593
|
outputTokens: 0,
|
|
566
594
|
cacheRead: 0,
|
|
567
595
|
cacheWrite: 0,
|
|
596
|
+
reasoningTokens: 0,
|
|
568
597
|
};
|
|
569
598
|
trace(`outbound Run: model=${cursorModelId} opencodeModel=${modelId} conversationId=${conversationId} ` +
|
|
570
599
|
`params=${JSON.stringify(parameterValues ?? [])} ` +
|
|
@@ -603,6 +632,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
603
632
|
blobs: new Map(),
|
|
604
633
|
toolDescriptors,
|
|
605
634
|
toolAliases: webToolAliases.aliases,
|
|
635
|
+
subagentCatalog,
|
|
606
636
|
requestContext,
|
|
607
637
|
allowTools,
|
|
608
638
|
usageEstimate,
|
|
@@ -711,6 +741,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
711
741
|
toolName: pending.toolName ?? r.toolName,
|
|
712
742
|
resultMetadata: pending.resultMetadata,
|
|
713
743
|
shellOutcome: shellResult?.outcome,
|
|
744
|
+
workspaceRoot: workspaceRootFromRequestContext(session.requestContext),
|
|
714
745
|
});
|
|
715
746
|
}
|
|
716
747
|
catch (error) {
|
|
@@ -897,6 +928,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
897
928
|
error: reason,
|
|
898
929
|
toolName: parsed.toolName,
|
|
899
930
|
resultMetadata: parsed.resultMetadata,
|
|
931
|
+
workspaceRoot: workspaceRootFromRequestContext(session.requestContext),
|
|
900
932
|
})) {
|
|
901
933
|
session.stream.write(frame);
|
|
902
934
|
}
|
|
@@ -946,6 +978,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
946
978
|
output: "",
|
|
947
979
|
toolName: parsed.toolName,
|
|
948
980
|
resultMetadata: { path: requestedPath },
|
|
981
|
+
workspaceRoot,
|
|
949
982
|
})) {
|
|
950
983
|
session.stream.write(frame);
|
|
951
984
|
}
|
|
@@ -961,6 +994,49 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
961
994
|
return true;
|
|
962
995
|
}
|
|
963
996
|
};
|
|
997
|
+
/**
|
|
998
|
+
* A read of a path that does not exist would otherwise reach OpenCode as a
|
|
999
|
+
* real tool call and raise a permission prompt for a file the user never had.
|
|
1000
|
+
* Cursor's own LocalReadExecutor instead resolves the target against the
|
|
1001
|
+
* workspace, stats it, and returns a typed ReadResult (file_not_found /
|
|
1002
|
+
* invalid_file) before execution. Mirror that here: reply on the held-open
|
|
1003
|
+
* exec channel with the exact typed case so the model receives a structured
|
|
1004
|
+
* observation and the host surfaces no prompt.
|
|
1005
|
+
*
|
|
1006
|
+
* Read-only by construction — write/edit targets are never checked, so
|
|
1007
|
+
* legitimate file creation (path resolves, file absent) is never denied. Runs
|
|
1008
|
+
* after recoverMissingEditRead, which has already converted the new-file edit
|
|
1009
|
+
* handshake read into an empty-file success. EACCES/EPERM and other stat
|
|
1010
|
+
* errors fall through to OpenCode so a genuine permission decision stands.
|
|
1011
|
+
*/
|
|
1012
|
+
const rejectMissingReadTarget = (parsed) => {
|
|
1013
|
+
if (parsed.toolName !== "read")
|
|
1014
|
+
return false;
|
|
1015
|
+
const requested = typeof parsed.args.filePath === "string" ? parsed.args.filePath : "";
|
|
1016
|
+
if (!requested)
|
|
1017
|
+
return false;
|
|
1018
|
+
const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
|
|
1019
|
+
const absolutePath = resolveReadTargetPath(requested, workspaceRoot);
|
|
1020
|
+
const readResult = classifyMissingReadTarget(absolutePath);
|
|
1021
|
+
if (!readResult)
|
|
1022
|
+
return false;
|
|
1023
|
+
try {
|
|
1024
|
+
for (const frame of buildReadRejectionMessages(parsed.id, readResult)) {
|
|
1025
|
+
session.stream.write(frame);
|
|
1026
|
+
}
|
|
1027
|
+
const kind = Object.keys(readResult)[0];
|
|
1028
|
+
trace(`exec: rejected missing read target id=${parsed.id} kind=${kind} ` +
|
|
1029
|
+
`path=${JSON.stringify(absolutePath)}`);
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
catch (e) {
|
|
1033
|
+
const error = new Error(`Failed to reject Cursor read of a missing path: ${e.message}`);
|
|
1034
|
+
trace(`exec: missing read rejection FAILED ${error.message}`);
|
|
1035
|
+
safeError(error);
|
|
1036
|
+
sessionManager.close(session);
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
964
1040
|
/** AI SDK V3 requires text-end / reasoning-end before finish or tool-call. */
|
|
965
1041
|
const closeOpenSpans = () => {
|
|
966
1042
|
for (const part of spanEndParts({ textStarted, reasoningStarted, textId, reasoningId })) {
|
|
@@ -1005,32 +1081,27 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1005
1081
|
if (te) {
|
|
1006
1082
|
// Authoritative TurnEnded counts — replace the running estimate.
|
|
1007
1083
|
session.usageEstimate = {
|
|
1008
|
-
inputTokens:
|
|
1009
|
-
outputTokens:
|
|
1010
|
-
cacheRead:
|
|
1011
|
-
cacheWrite:
|
|
1084
|
+
inputTokens: turnEndedCounter(te, "input_tokens"),
|
|
1085
|
+
outputTokens: turnEndedCounter(te, "output_tokens"),
|
|
1086
|
+
cacheRead: turnEndedCounter(te, "cache_read"),
|
|
1087
|
+
cacheWrite: turnEndedCounter(te, "cache_write"),
|
|
1088
|
+
reasoningTokens: turnEndedCounter(te, "reasoning_tokens"),
|
|
1012
1089
|
};
|
|
1013
1090
|
}
|
|
1014
1091
|
const est = session.usageEstimate;
|
|
1015
|
-
const usage =
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
noCache: undefined,
|
|
1019
|
-
cacheRead: 0,
|
|
1020
|
-
cacheWrite: 0,
|
|
1021
|
-
},
|
|
1022
|
-
outputTokens: {
|
|
1023
|
-
total: estimateTokens(requestUsage.outputChars),
|
|
1024
|
-
text: undefined,
|
|
1025
|
-
reasoning: undefined,
|
|
1026
|
-
},
|
|
1027
|
-
};
|
|
1092
|
+
const usage = te
|
|
1093
|
+
? buildLanguageModelV3UsageFromTurnEnded(te)
|
|
1094
|
+
: buildLanguageModelV3UsageFromEstimate(est, promptTokens, requestUsage.outputChars, estimateTokens);
|
|
1028
1095
|
const providerMetadata = te ? cursorTurnEndedProviderMetadata(te) : undefined;
|
|
1029
1096
|
const reasonLabel = typeof reason === "object" && reason && "unified" in reason
|
|
1030
1097
|
? String(reason.unified ?? "unknown")
|
|
1031
1098
|
: String(reason);
|
|
1099
|
+
const inTotal = usage.inputTokens?.total ?? 0;
|
|
1100
|
+
const outTotal = usage.outputTokens?.total ?? 0;
|
|
1032
1101
|
trace(`finish: reason=${reasonLabel} ` +
|
|
1033
|
-
`
|
|
1102
|
+
`v3In=${inTotal} v3Out=${outTotal} ` +
|
|
1103
|
+
`v3CacheRead=${usage.inputTokens?.cacheRead ?? 0} v3CacheWrite=${usage.inputTokens?.cacheWrite ?? 0} ` +
|
|
1104
|
+
`v3Reasoning=${usage.outputTokens?.reasoning ?? 0} ` +
|
|
1034
1105
|
`rawIn=${est.inputTokens} rawOut=${est.outputTokens} ` +
|
|
1035
1106
|
`rawCacheRead=${est.cacheRead} rawCacheWrite=${est.cacheWrite} ` +
|
|
1036
1107
|
`source=${te ? "turn_ended" : "estimate"}`);
|
|
@@ -1176,6 +1247,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1176
1247
|
emitReasoning(iu.thinking_delta.text ?? "");
|
|
1177
1248
|
}
|
|
1178
1249
|
else if (iu?.turn_ended) {
|
|
1250
|
+
trace(`turn_ended raw wire fields: ${debugWalkTurnEnded(payload)}`);
|
|
1179
1251
|
emitFinish(iu.turn_ended, { unified: "stop", raw: undefined });
|
|
1180
1252
|
sessionManager.close(session);
|
|
1181
1253
|
return;
|
|
@@ -1304,6 +1376,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1304
1376
|
trace(`web tool alias resolved: ${parsed.toolName} -> ${executableToolName}`);
|
|
1305
1377
|
parsed.toolName = executableToolName;
|
|
1306
1378
|
}
|
|
1379
|
+
remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog);
|
|
1307
1380
|
}
|
|
1308
1381
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
1309
1382
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
@@ -1344,6 +1417,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1344
1417
|
}
|
|
1345
1418
|
if (recoverMissingEditRead(parsed, displayCallId))
|
|
1346
1419
|
continue;
|
|
1420
|
+
if (rejectMissingReadTarget(parsed)) {
|
|
1421
|
+
if (displayCallId)
|
|
1422
|
+
session.displayToolCalls.delete(displayCallId);
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1347
1425
|
if (displayCallId) {
|
|
1348
1426
|
session.displayToolCalls.delete(displayCallId);
|
|
1349
1427
|
trace(`exec: claimed display callId=${displayCallId}`);
|
|
@@ -1550,6 +1628,7 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1550
1628
|
if (names.size === 0)
|
|
1551
1629
|
return undefined;
|
|
1552
1630
|
const instructions = [];
|
|
1631
|
+
const subagents = extractHostSubagentCatalog(tools);
|
|
1553
1632
|
if (names.has("question")) {
|
|
1554
1633
|
instructions.push("- When user input is required, call the OpenCode `question` tool; do not use Cursor's native AskQuestion interaction.");
|
|
1555
1634
|
}
|
|
@@ -1568,6 +1647,17 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1568
1647
|
if (names.has(CUSTOM_WEBFETCH_TOOL)) {
|
|
1569
1648
|
instructions.push(`- To fetch a known URL, call \`${CUSTOM_WEBFETCH_TOOL}\`; do not use Cursor's native WebFetch interaction.`);
|
|
1570
1649
|
}
|
|
1650
|
+
if (names.has("task") || names.has("actor")) {
|
|
1651
|
+
const target = names.has("actor") ? "`actor`" : "`task`";
|
|
1652
|
+
const available = subagents.agents.map((agent) => `\`${agent.name}\``).join(", ");
|
|
1653
|
+
instructions.push(`- Native Cursor Task/subagent requests are executed through OpenCode ${target}. ` +
|
|
1654
|
+
"Advertised custom subagent names are used exactly; otherwise `unspecified` and `generalPurpose` select host `general`, " +
|
|
1655
|
+
"`bugbot`, `security-review`, and `explore` select host `explore` (then `general`), and other specialized Cursor types fall back to `general`." +
|
|
1656
|
+
(available ? ` Spawnable host agents this turn: ${available}.` : ""));
|
|
1657
|
+
if (subagents.agents.some((agent) => agent.name === "scout")) {
|
|
1658
|
+
instructions.push("- Host `scout` is available for external documentation and dependency-source research. Use Cursor `cursor-guide` for that use case; local repository discovery still uses `bugbot`/`explore`.");
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1571
1661
|
if (names.has("write")) {
|
|
1572
1662
|
instructions.push(names.has("edit")
|
|
1573
1663
|
? "- For file changes, use OpenCode `edit` for targeted changes to existing files and `write` to create files or intentionally replace complete contents; do not use shell, Python, or heredocs to change file content while these tools are available."
|
|
@@ -1576,7 +1666,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1576
1666
|
return [
|
|
1577
1667
|
`OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
|
|
1578
1668
|
`Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`,
|
|
1579
|
-
|
|
1669
|
+
subagents.executor
|
|
1670
|
+
? "Call only tools in that exact list. Cursor-native Task/subagent requests are permitted because a compatible host executor is listed; other unlisted Cursor-native tools remain unavailable."
|
|
1671
|
+
: "Call only tools in that exact list. Cursor-native tools that are not listed—including Task/subagents—are unavailable; do not invoke them. If a capability is absent, complete the work directly with the listed tools or explain the limitation.",
|
|
1580
1672
|
...(instructions.length > 0
|
|
1581
1673
|
? ["Use these OpenCode tools instead of equivalent Cursor-native UI interactions:"]
|
|
1582
1674
|
: []),
|
|
@@ -1601,20 +1693,14 @@ export function estimatePromptTokens(prompt) {
|
|
|
1601
1693
|
}
|
|
1602
1694
|
/** Preserve cumulative Cursor counters as diagnostics, never as AI SDK request usage. */
|
|
1603
1695
|
export function cursorTurnEndedProviderMetadata(te) {
|
|
1604
|
-
const counter = (key) => {
|
|
1605
|
-
const value = te[key];
|
|
1606
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
1607
|
-
? Math.trunc(value)
|
|
1608
|
-
: 0;
|
|
1609
|
-
};
|
|
1610
1696
|
return {
|
|
1611
1697
|
cursor: {
|
|
1612
1698
|
usageVersion: 2,
|
|
1613
|
-
inputTokensRaw:
|
|
1614
|
-
outputTokensRaw:
|
|
1615
|
-
cacheReadRaw:
|
|
1616
|
-
cacheWriteRaw:
|
|
1617
|
-
reasoningTokensRaw:
|
|
1699
|
+
inputTokensRaw: turnEndedCounter(te, "input_tokens"),
|
|
1700
|
+
outputTokensRaw: turnEndedCounter(te, "output_tokens"),
|
|
1701
|
+
cacheReadRaw: turnEndedCounter(te, "cache_read"),
|
|
1702
|
+
cacheWriteRaw: turnEndedCounter(te, "cache_write"),
|
|
1703
|
+
reasoningTokensRaw: turnEndedCounter(te, "reasoning_tokens"),
|
|
1618
1704
|
},
|
|
1619
1705
|
};
|
|
1620
1706
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ModelInfo } from "./models.js";
|
|
2
|
+
/**
|
|
3
|
+
* Display names shared by both a thinking and a non-thinking model. A thinking
|
|
4
|
+
* model with such a name needs a "Thinking" tag to disambiguate it from its
|
|
5
|
+
* non-thinking twin (Cursor's Claude/Fable/Sonnet families). Models whose names
|
|
6
|
+
* are already unique — including Cursor's GPT family, where the reasoning tier
|
|
7
|
+
* ("None"/"Low"/"High"…) is baked into the name — are excluded, so they aren't
|
|
8
|
+
* tagged redundantly.
|
|
9
|
+
*/
|
|
10
|
+
export declare function thinkingSuffixBaseNames(models: ModelInfo[]): Set<string>;
|
|
11
|
+
export declare function modelInfoToConfig(mi: ModelInfo, options?: {
|
|
12
|
+
thinkingSuffix?: boolean;
|
|
13
|
+
contextTier?: "base" | "long";
|
|
14
|
+
}): Record<string, any>;
|
|
15
|
+
export declare function modelsToConfig(models: ModelInfo[]): Record<string, any>;
|