cursor-opencode-provider 0.2.7 → 0.2.8
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 +10 -6
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/build.js +6 -0
- package/dist/context/paths.d.ts +21 -6
- package/dist/context/paths.js +32 -18
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/language-model.js +123 -69
- package/dist/models.d.ts +2 -0
- package/dist/plugin.js +8 -1
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +4 -0
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tools.d.ts +18 -0
- package/dist/protocol/tools.js +74 -5
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +3 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +106 -82
- package/dist/web-tools.d.ts +34 -0
- package/dist/web-tools.js +127 -0
- package/package.json +1 -1
package/dist/protocol/tools.js
CHANGED
|
@@ -10,6 +10,8 @@ import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-
|
|
|
10
10
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
11
11
|
// field number for every exec variant, so this is also the result field.
|
|
12
12
|
export const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
13
|
+
export const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
|
|
14
|
+
export const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
|
|
13
15
|
/** Match OpenCode's McpCatalog.sanitize for config server ids. */
|
|
14
16
|
export function sanitizeMcpServerId(value) {
|
|
15
17
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -51,13 +53,17 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
|
|
|
51
53
|
*/
|
|
52
54
|
export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
53
55
|
return tools.map((t) => {
|
|
54
|
-
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
56
|
+
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
57
|
+
const collisionSafeWebAlias = t.name === CUSTOM_WEBSEARCH_TOOL || t.name === CUSTOM_WEBFETCH_TOOL;
|
|
55
58
|
return {
|
|
56
|
-
name
|
|
59
|
+
// Keep the collision-safe public name exact. Prefixing it with the
|
|
60
|
+
// synthetic default server weakens the distinction from Cursor-native
|
|
61
|
+
// web tools in the model-visible catalog.
|
|
62
|
+
name: collisionSafeWebAlias ? t.name : `${id.server}-${id.toolName}`,
|
|
57
63
|
description: t.description ?? "",
|
|
58
64
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
59
65
|
provider_identifier: id.server,
|
|
60
|
-
tool_name: id.toolName,
|
|
66
|
+
tool_name: t.sourceName ? t.name : id.toolName,
|
|
61
67
|
};
|
|
62
68
|
});
|
|
63
69
|
}
|
|
@@ -67,6 +73,67 @@ function normalizeInputSchema(schema) {
|
|
|
67
73
|
}
|
|
68
74
|
return { type: "object", properties: {} };
|
|
69
75
|
}
|
|
76
|
+
const WEB_ALIAS_RULES = [
|
|
77
|
+
{
|
|
78
|
+
alias: CUSTOM_WEBSEARCH_TOOL,
|
|
79
|
+
exact: ["websearch", "web_search"],
|
|
80
|
+
suffixes: ["_web_search", "-web_search", "_websearch", "-websearch"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
alias: CUSTOM_WEBFETCH_TOOL,
|
|
84
|
+
exact: ["webfetch", "web_fetch"],
|
|
85
|
+
suffixes: ["_web_fetch", "-web_fetch", "_webfetch", "-webfetch"],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* Give collision-prone web capabilities names Cursor will not confuse with its
|
|
90
|
+
* native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a
|
|
91
|
+
* flattened MCP suffix is accepted only when it identifies one unique tool.
|
|
92
|
+
*/
|
|
93
|
+
export function buildCustomWebToolAliases(tools) {
|
|
94
|
+
const aliases = new Map();
|
|
95
|
+
const ambiguous = new Map();
|
|
96
|
+
const replacements = new Map();
|
|
97
|
+
for (const rule of WEB_ALIAS_RULES) {
|
|
98
|
+
// A host/plugin may already expose the collision-safe public name. Keep it
|
|
99
|
+
// authoritative instead of hiding it behind a second mapping.
|
|
100
|
+
if (tools.some((tool) => tool.name === rule.alias))
|
|
101
|
+
continue;
|
|
102
|
+
const exact = tools.filter((tool) => rule.exact.includes(tool.name.toLowerCase()));
|
|
103
|
+
const candidates = exact.length > 0
|
|
104
|
+
? exact
|
|
105
|
+
: tools.filter((tool) => {
|
|
106
|
+
const name = tool.name.toLowerCase();
|
|
107
|
+
return rule.suffixes.some((suffix) => name.endsWith(suffix));
|
|
108
|
+
});
|
|
109
|
+
if (candidates.length !== 1) {
|
|
110
|
+
if (candidates.length > 1)
|
|
111
|
+
ambiguous.set(rule.alias, candidates.map((tool) => tool.name));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const original = candidates[0].name;
|
|
115
|
+
aliases.set(rule.alias, original);
|
|
116
|
+
replacements.set(original, rule.alias);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
advertisedTools: tools.map((tool) => {
|
|
120
|
+
const alias = replacements.get(tool.name);
|
|
121
|
+
return alias ? { ...tool, name: alias, sourceName: tool.name } : { ...tool };
|
|
122
|
+
}),
|
|
123
|
+
aliases,
|
|
124
|
+
ambiguous,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function resolveCustomWebToolAlias(toolName, aliases) {
|
|
128
|
+
const direct = aliases?.get(toolName);
|
|
129
|
+
if (direct)
|
|
130
|
+
return direct;
|
|
131
|
+
for (const [alias, original] of aliases ?? []) {
|
|
132
|
+
if (toolName.endsWith(`_${alias}`))
|
|
133
|
+
return original;
|
|
134
|
+
}
|
|
135
|
+
return toolName;
|
|
136
|
+
}
|
|
70
137
|
/**
|
|
71
138
|
* Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
|
|
72
139
|
* requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
|
|
@@ -79,7 +146,7 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
79
146
|
const order = [];
|
|
80
147
|
const byServer = new Map();
|
|
81
148
|
for (const t of tools) {
|
|
82
|
-
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
149
|
+
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
83
150
|
let list = byServer.get(id.server);
|
|
84
151
|
if (!list) {
|
|
85
152
|
list = [];
|
|
@@ -87,7 +154,7 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
87
154
|
order.push(id.server);
|
|
88
155
|
}
|
|
89
156
|
list.push({
|
|
90
|
-
tool_name: id.toolName,
|
|
157
|
+
tool_name: t.sourceName ? t.name : id.toolName,
|
|
91
158
|
description: t.description ?? "",
|
|
92
159
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
93
160
|
});
|
|
@@ -118,6 +185,8 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
118
185
|
enabled: true,
|
|
119
186
|
mcp_descriptors: nested,
|
|
120
187
|
},
|
|
188
|
+
web_search_enabled: false,
|
|
189
|
+
web_fetch_enabled: false,
|
|
121
190
|
rules_info_complete: true,
|
|
122
191
|
env_info_complete: true,
|
|
123
192
|
repository_info_complete: true,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CursorProviderError } from "./errors.js";
|
|
2
|
+
export type ReplayBarrierReason = "visible-text" | "visible-reasoning" | "display-tool-lifecycle" | "non-control-exec" | "stateful-interaction" | "unknown-or-malformed-frame";
|
|
3
|
+
export declare class AttemptReplaySafety {
|
|
4
|
+
private readonly sessionId;
|
|
5
|
+
private barrierReason;
|
|
6
|
+
constructor(sessionId: string);
|
|
7
|
+
markBarrier(reason: ReplayBarrierReason): void;
|
|
8
|
+
applyTo(failure: CursorProviderError): CursorProviderError;
|
|
9
|
+
}
|
|
10
|
+
export type DecodedReplayFrame = {
|
|
11
|
+
interactionUpdate?: Record<string, unknown>;
|
|
12
|
+
exec?: Record<string, unknown>;
|
|
13
|
+
kv?: Record<string, unknown>;
|
|
14
|
+
execControl?: Record<string, unknown>;
|
|
15
|
+
interactionQuery?: Record<string, unknown>;
|
|
16
|
+
checkpointBytes?: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
export type ReplayFrameAnalysis = {
|
|
19
|
+
semanticProgress: boolean;
|
|
20
|
+
barrier?: ReplayBarrierReason;
|
|
21
|
+
};
|
|
22
|
+
/** Classify one decoded server frame without performing any protocol side effects. */
|
|
23
|
+
export declare function analyzeReplayFrame(payload: Uint8Array, decoded: DecodedReplayFrame): ReplayFrameAnalysis;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { trace } from "./debug.js";
|
|
2
|
+
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
3
|
+
export class AttemptReplaySafety {
|
|
4
|
+
sessionId;
|
|
5
|
+
barrierReason;
|
|
6
|
+
constructor(sessionId) {
|
|
7
|
+
this.sessionId = sessionId;
|
|
8
|
+
}
|
|
9
|
+
markBarrier(reason) {
|
|
10
|
+
if (this.barrierReason)
|
|
11
|
+
return;
|
|
12
|
+
this.barrierReason = reason;
|
|
13
|
+
trace(`replay barrier: reason=${reason} sessionId=${this.sessionId}`);
|
|
14
|
+
}
|
|
15
|
+
applyTo(failure) {
|
|
16
|
+
failure.replaySafe = this.barrierReason === undefined && failure.replaySafe;
|
|
17
|
+
if (this.barrierReason) {
|
|
18
|
+
trace(`replay suppressed: reason=${this.barrierReason} sessionId=${this.sessionId}`);
|
|
19
|
+
}
|
|
20
|
+
return failure;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const INTERACTION_UPDATE_FIELDS = new Set([1, 2, 3, 4, 7, 13, 14, 16, 17]);
|
|
24
|
+
const INTERACTION_QUERY_FIELDS = new Set([2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14]);
|
|
25
|
+
const TOP_LEVEL_FIELDS = new Set([1, 2, 3, 4, 5, 7]);
|
|
26
|
+
function nestedFields(topLevel, field) {
|
|
27
|
+
if (topLevel?.fn !== field || topLevel.wt !== 2 || !topLevel.bytes)
|
|
28
|
+
return [];
|
|
29
|
+
return readAllFieldsStrict(topLevel.bytes) ?? [];
|
|
30
|
+
}
|
|
31
|
+
function analyzeExecWire(topLevel) {
|
|
32
|
+
const variants = nestedFields(topLevel, 2)
|
|
33
|
+
.filter((field) => ![1, 15, 19].includes(field.fn));
|
|
34
|
+
const exactVariant = (field) => variants.length === 1 && variants[0].fn === field && variants[0].wt === 2;
|
|
35
|
+
return {
|
|
36
|
+
exactRequestContext: exactVariant(10),
|
|
37
|
+
exactMcpState: exactVariant(36),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function validKvWire(topLevel) {
|
|
41
|
+
const variants = nestedFields(topLevel, 4).filter((field) => field.fn !== 1);
|
|
42
|
+
const variant = variants.length === 1 ? variants[0] : undefined;
|
|
43
|
+
const args = variant?.wt === 2 && variant.bytes
|
|
44
|
+
? (readAllFieldsStrict(variant.bytes) ?? [])
|
|
45
|
+
: [];
|
|
46
|
+
const blobIds = args.filter((field) => field.fn === 1 && field.wt === 2 && (field.bytes?.length ?? 0) > 0);
|
|
47
|
+
return !!variant
|
|
48
|
+
&& [2, 3].includes(variant.fn)
|
|
49
|
+
&& variant.wt === 2
|
|
50
|
+
&& blobIds.length === 1
|
|
51
|
+
&& args.every((field) => field.wt === 2 && (field.fn === 1 || (variant.fn === 3 && field.fn === 2)));
|
|
52
|
+
}
|
|
53
|
+
function validInteractionUpdateWire(topLevel, decoded) {
|
|
54
|
+
if (!decoded)
|
|
55
|
+
return true;
|
|
56
|
+
const fields = nestedFields(topLevel, 1);
|
|
57
|
+
const update = fields.length === 1 ? fields[0] : undefined;
|
|
58
|
+
if (!update || update.wt !== 2 || !INTERACTION_UPDATE_FIELDS.has(update.fn))
|
|
59
|
+
return false;
|
|
60
|
+
if (![1, 4].includes(update.fn))
|
|
61
|
+
return true;
|
|
62
|
+
const delta = update.bytes ? (readAllFieldsStrict(update.bytes) ?? []) : [];
|
|
63
|
+
return delta.length === 1 && delta[0].fn === 1 && delta[0].wt === 2;
|
|
64
|
+
}
|
|
65
|
+
function validInteractionQueryWire(topLevel, decoded) {
|
|
66
|
+
if (!decoded)
|
|
67
|
+
return true;
|
|
68
|
+
const fields = nestedFields(topLevel, 7);
|
|
69
|
+
const ids = fields.filter((field) => field.fn === 1);
|
|
70
|
+
const variants = fields.filter((field) => field.fn !== 1);
|
|
71
|
+
return ids.length <= 1
|
|
72
|
+
&& ids.every((field) => field.wt === 0)
|
|
73
|
+
&& variants.length === 1
|
|
74
|
+
&& variants[0].wt === 2
|
|
75
|
+
&& INTERACTION_QUERY_FIELDS.has(variants[0].fn);
|
|
76
|
+
}
|
|
77
|
+
function decodedMatchesWire(topLevel, decoded) {
|
|
78
|
+
return !((topLevel?.fn === 1 && !decoded.interactionUpdate)
|
|
79
|
+
|| (topLevel?.fn === 2 && !decoded.exec)
|
|
80
|
+
|| (topLevel?.fn === 4 && !decoded.kv)
|
|
81
|
+
|| (topLevel?.fn === 5 && !decoded.execControl)
|
|
82
|
+
|| (topLevel?.fn === 7 && !decoded.interactionQuery));
|
|
83
|
+
}
|
|
84
|
+
function hasSemanticProgress(decoded) {
|
|
85
|
+
const update = decoded.interactionUpdate;
|
|
86
|
+
const text = update?.text_delta?.text;
|
|
87
|
+
const thinking = update?.thinking_delta?.text;
|
|
88
|
+
return (typeof text === "string" && text.length > 0)
|
|
89
|
+
|| (typeof thinking === "string" && thinking.length > 0)
|
|
90
|
+
|| !!update?.turn_ended
|
|
91
|
+
|| !!update?.tool_call_started
|
|
92
|
+
|| !!update?.tool_call_completed
|
|
93
|
+
|| !!decoded.exec
|
|
94
|
+
|| !!decoded.kv
|
|
95
|
+
|| !!decoded.execControl
|
|
96
|
+
|| !!decoded.interactionQuery
|
|
97
|
+
|| !!decoded.checkpointBytes?.length;
|
|
98
|
+
}
|
|
99
|
+
/** Classify one decoded server frame without performing any protocol side effects. */
|
|
100
|
+
export function analyzeReplayFrame(payload, decoded) {
|
|
101
|
+
const topLevelFields = readAllFieldsStrict(payload) ?? [];
|
|
102
|
+
const topLevel = topLevelFields.length === 1 ? topLevelFields[0] : undefined;
|
|
103
|
+
const exec = analyzeExecWire(topLevel);
|
|
104
|
+
const validKv = validKvWire(topLevel);
|
|
105
|
+
const malformed = !topLevel
|
|
106
|
+
|| topLevel.wt !== 2
|
|
107
|
+
|| !TOP_LEVEL_FIELDS.has(topLevel.fn)
|
|
108
|
+
|| !validInteractionUpdateWire(topLevel, decoded.interactionUpdate)
|
|
109
|
+
|| !validInteractionQueryWire(topLevel, decoded.interactionQuery)
|
|
110
|
+
|| !decodedMatchesWire(topLevel, decoded)
|
|
111
|
+
|| !!decoded.execControl;
|
|
112
|
+
let barrier;
|
|
113
|
+
if (decoded.interactionUpdate?.tool_call_started || decoded.interactionUpdate?.tool_call_completed) {
|
|
114
|
+
barrier = "display-tool-lifecycle";
|
|
115
|
+
}
|
|
116
|
+
else if (malformed || (topLevel.fn === 4 && !validKv)) {
|
|
117
|
+
barrier = "unknown-or-malformed-frame";
|
|
118
|
+
}
|
|
119
|
+
else if (topLevel.fn === 2 && !exec.exactRequestContext && !exec.exactMcpState) {
|
|
120
|
+
barrier = "non-control-exec";
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
semanticProgress: hasSemanticProgress(decoded),
|
|
124
|
+
barrier,
|
|
125
|
+
};
|
|
126
|
+
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BidiStream } from "./transport/connect.js";
|
|
2
2
|
import { type CursorProviderError } from "./errors.js";
|
|
3
3
|
import { type SessionActivitySource } from "./activity.js";
|
|
4
|
+
import type { ToolAliasRegistry } from "./protocol/tools.js";
|
|
4
5
|
export type Frame = {
|
|
5
6
|
flags: number;
|
|
6
7
|
payload: Uint8Array;
|
|
@@ -117,6 +118,8 @@ export type CursorSession = {
|
|
|
117
118
|
blobs: Map<string, Uint8Array>;
|
|
118
119
|
/** McpToolDefinition list advertised this turn — echoed into the request_context reply. */
|
|
119
120
|
toolDescriptors: Array<Record<string, unknown>>;
|
|
121
|
+
/** Cursor-facing web alias → exact executable host tool for this Run. */
|
|
122
|
+
toolAliases?: ToolAliasRegistry;
|
|
120
123
|
/** Full RequestContext for exec #10 replies. */
|
|
121
124
|
requestContext: Record<string, unknown>;
|
|
122
125
|
/**
|
|
@@ -5,6 +5,7 @@ export declare function unaryAvailableModels(token: string, options?: {
|
|
|
5
5
|
apiBaseURL?: string;
|
|
6
6
|
baseURL?: string;
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
|
+
timeoutMs?: number;
|
|
8
9
|
}): Promise<Record<string, unknown>>;
|
|
9
10
|
/**
|
|
10
11
|
* Cursor Run stream hosts from GetServerConfig / agentBaseURL.
|
|
@@ -30,6 +31,7 @@ export declare function fetchAgentUrl(token: string, options?: {
|
|
|
30
31
|
apiBaseURL?: string;
|
|
31
32
|
baseURL?: string;
|
|
32
33
|
telemetryEnabled?: boolean;
|
|
34
|
+
timeoutMs?: number;
|
|
33
35
|
}): Promise<string>;
|
|
34
36
|
export type BidiStream = {
|
|
35
37
|
write(msg: Uint8Array): boolean | void;
|
|
@@ -5,8 +5,25 @@ import { getDeviceIds } from "../protocol/device-id.js";
|
|
|
5
5
|
import { resolveClientVersion } from "../protocol/client-version.js";
|
|
6
6
|
import { trace } from "../debug.js";
|
|
7
7
|
import { CursorLocalCancellationError, CursorProtocolError, CursorTransportError, cursorGrpcError, cursorHttpError, errorCode, CursorProviderError, } from "../errors.js";
|
|
8
|
+
import { withAbortDeadline } from "../deadline.js";
|
|
8
9
|
import http2 from "node:http2";
|
|
9
10
|
const API_BASE = `https://${CURSOR_API_HOST}`;
|
|
11
|
+
const DEFAULT_UNARY_TIMEOUT_MS = 5_000;
|
|
12
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
13
|
+
function unaryTimeoutMs(operation, value) {
|
|
14
|
+
const timeoutMs = value ?? DEFAULT_UNARY_TIMEOUT_MS;
|
|
15
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_MS) {
|
|
16
|
+
throw new CursorProtocolError(`${operation} timeout must be a positive integer no greater than ${MAX_TIMER_MS}`, { code: "CURSOR_INVALID_TIMEOUT" });
|
|
17
|
+
}
|
|
18
|
+
return timeoutMs;
|
|
19
|
+
}
|
|
20
|
+
async function withUnaryDeadline(operation, timeoutMs, timeoutCode, run) {
|
|
21
|
+
return withAbortDeadline(timeoutMs, () => new CursorTransportError(`${operation} timed out after ${timeoutMs}ms`, {
|
|
22
|
+
transient: true,
|
|
23
|
+
replaySafe: true,
|
|
24
|
+
code: timeoutCode,
|
|
25
|
+
}), run);
|
|
26
|
+
}
|
|
10
27
|
function resolveApiBaseURL(options) {
|
|
11
28
|
return new URL(options.apiBaseURL ?? options.baseURL ?? API_BASE).origin;
|
|
12
29
|
}
|
|
@@ -33,42 +50,46 @@ export function buildBaseHeaders(token, clientVersion, extra) {
|
|
|
33
50
|
export async function unaryAvailableModels(token, options = {}) {
|
|
34
51
|
const base = resolveApiBaseURL(options);
|
|
35
52
|
const url = `${base}/aiserver.v1.AiService/AvailableModels`;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
res
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
const timeoutMs = unaryTimeoutMs("AvailableModels", options.timeoutMs);
|
|
54
|
+
return withUnaryDeadline("AvailableModels", timeoutMs, "CURSOR_AVAILABLE_MODELS_TIMEOUT", async (signal) => {
|
|
55
|
+
const clientVersion = await resolveClientVersion();
|
|
56
|
+
const headers = buildBaseHeaders(token, clientVersion, options.headers);
|
|
57
|
+
let res;
|
|
58
|
+
try {
|
|
59
|
+
res = await fetch(url, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
...headers,
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
accept: "application/json",
|
|
65
|
+
},
|
|
66
|
+
// Request parameterized effort/context/fast variants like Cursor IDE.
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
includeLongContextModels: true,
|
|
69
|
+
useModelParameters: true,
|
|
70
|
+
useCloudAgentEffortModes: true,
|
|
71
|
+
}),
|
|
72
|
+
signal,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
throw new CursorTransportError("AvailableModels network request failed", {
|
|
77
|
+
transient: true,
|
|
78
|
+
replaySafe: true,
|
|
79
|
+
code: errorCode(cause),
|
|
80
|
+
cause,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw await cursorHttpResponseError("AvailableModels failed:", res);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return (await res.json());
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
throw new CursorProtocolError("AvailableModels returned malformed JSON", { cause });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
72
93
|
}
|
|
73
94
|
// ── Unary (GetServerConfig) ──
|
|
74
95
|
/**
|
|
@@ -116,51 +137,55 @@ export function normalizeAgentRunOrigin(raw) {
|
|
|
116
137
|
export async function fetchAgentUrl(token, options = {}) {
|
|
117
138
|
const base = resolveApiBaseURL(options);
|
|
118
139
|
const url = `${base}${SERVER_CONFIG_PATH}`;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
res
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
140
|
+
const timeoutMs = unaryTimeoutMs("GetServerConfig", options.timeoutMs);
|
|
141
|
+
return withUnaryDeadline("GetServerConfig", timeoutMs, "CURSOR_AGENT_URL_TIMEOUT", async (signal) => {
|
|
142
|
+
const clientVersion = await resolveClientVersion();
|
|
143
|
+
// Match Cursor CLI: GetServerConfig uses base headers only — session headers belong on Run.
|
|
144
|
+
const headers = buildBaseHeaders(token, clientVersion);
|
|
145
|
+
trace(`GetServerConfig POST ${url}`);
|
|
146
|
+
let res;
|
|
147
|
+
try {
|
|
148
|
+
res = await fetch(url, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
...headers,
|
|
152
|
+
"content-type": "application/json",
|
|
153
|
+
accept: "application/json",
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ telem_enabled: options.telemetryEnabled ?? false }),
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (cause) {
|
|
160
|
+
throw new CursorTransportError("GetServerConfig network request failed", {
|
|
161
|
+
transient: true,
|
|
162
|
+
replaySafe: true,
|
|
163
|
+
code: errorCode(cause),
|
|
164
|
+
cause,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw await cursorHttpResponseError("GetServerConfig failed:", res);
|
|
169
|
+
}
|
|
170
|
+
let body;
|
|
171
|
+
try {
|
|
172
|
+
body = (await res.json());
|
|
173
|
+
}
|
|
174
|
+
catch (cause) {
|
|
175
|
+
throw new CursorProtocolError("GetServerConfig returned malformed JSON", { cause });
|
|
176
|
+
}
|
|
177
|
+
const cfg = body.agentUrlConfig;
|
|
178
|
+
const raw = cfg?.agentnUrl || cfg?.agentUrl;
|
|
179
|
+
const normalized = normalizeAgentRunOrigin(raw);
|
|
180
|
+
trace(`GetServerConfig reply: agentnUrl=${cfg?.agentnUrl ?? "<missing>"} ` +
|
|
181
|
+
`agentUrl=${cfg?.agentUrl ?? "<missing>"} → ${normalized ?? "<invalid>"}`);
|
|
182
|
+
if (!normalized) {
|
|
183
|
+
throw new CursorProtocolError(raw
|
|
184
|
+
? "GetServerConfig returned an invalid Cursor agent URL"
|
|
185
|
+
: "GetServerConfig response missing agentUrlConfig.agentnUrl");
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
});
|
|
164
189
|
}
|
|
165
190
|
export class CursorRunInterruptedError extends CursorTransportError {
|
|
166
191
|
constructor(message = "Cursor Run ended before turn_ended", options) {
|
|
@@ -217,7 +242,6 @@ const CONNECT_TIMEOUT_MS = 15_000;
|
|
|
217
242
|
const DEFAULT_SESSION_PING_TIMEOUT_MS = 5_000;
|
|
218
243
|
const DEFAULT_READ_IDLE_MS = 120_000;
|
|
219
244
|
const WRITE_DRAIN_TIMEOUT_CODE = "CURSOR_WRITE_DRAIN_TIMEOUT";
|
|
220
|
-
const MAX_TIMER_MS = 2_147_483_647;
|
|
221
245
|
export const HTTP2_SESSION_MAX_AGE_MS = 15 * 60_000;
|
|
222
246
|
export function shouldReuseHttp2Session(state, createdAt, now = Date.now()) {
|
|
223
247
|
return !state.destroyed && !state.closed && now - createdAt < HTTP2_SESSION_MAX_AGE_MS;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ToolContext, type ToolResult } from "@opencode-ai/plugin";
|
|
2
|
+
export type OpenCodeWebSearchArgs = {
|
|
3
|
+
query: string;
|
|
4
|
+
numResults?: number;
|
|
5
|
+
livecrawl?: "fallback" | "preferred";
|
|
6
|
+
type?: "auto" | "fast" | "deep";
|
|
7
|
+
contextMaxCharacters?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseOpenCodeWebSearchResponse(raw: string): string | undefined;
|
|
10
|
+
export declare function executeOpenCodeWebSearch(args: OpenCodeWebSearchArgs, context: ToolContext, fetchImpl?: typeof fetch): Promise<ToolResult>;
|
|
11
|
+
export declare const openCodeWebSearchTool: {
|
|
12
|
+
description: string;
|
|
13
|
+
args: {
|
|
14
|
+
query: import("zod").ZodString;
|
|
15
|
+
numResults: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
16
|
+
livecrawl: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
17
|
+
fallback: "fallback";
|
|
18
|
+
preferred: "preferred";
|
|
19
|
+
}>>;
|
|
20
|
+
type: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
21
|
+
fast: "fast";
|
|
22
|
+
auto: "auto";
|
|
23
|
+
deep: "deep";
|
|
24
|
+
}>>;
|
|
25
|
+
contextMaxCharacters: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
26
|
+
};
|
|
27
|
+
execute(args: {
|
|
28
|
+
query: string;
|
|
29
|
+
numResults?: number | undefined;
|
|
30
|
+
livecrawl?: "fallback" | "preferred" | undefined;
|
|
31
|
+
type?: "fast" | "auto" | "deep" | undefined;
|
|
32
|
+
contextMaxCharacters?: number | undefined;
|
|
33
|
+
}, context: ToolContext): Promise<ToolResult>;
|
|
34
|
+
};
|