cursor-opencode-provider 0.4.3 → 0.6.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 +33 -23
- package/dist/context/agents.d.ts +1 -1
- package/dist/context/agents.js +1 -1
- package/dist/context/auth-store.js +2 -2
- package/dist/context/build.d.ts +11 -1
- package/dist/context/build.js +76 -25
- package/dist/context/frozen.d.ts +29 -0
- package/dist/context/frozen.js +168 -0
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -2
- package/dist/context/paths.d.ts +34 -53
- package/dist/context/paths.js +54 -118
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +2 -2
- package/dist/image-save-tool.d.ts +19 -0
- package/dist/image-save-tool.js +25 -0
- package/dist/image-save.d.ts +58 -0
- package/dist/image-save.js +189 -0
- package/dist/image-staging.d.ts +59 -0
- package/dist/image-staging.js +91 -0
- package/dist/index.d.ts +1 -1
- package/dist/language-model.d.ts +13 -11
- package/dist/language-model.js +957 -150
- package/dist/plan-execution-kickoff.d.ts +55 -0
- package/dist/plan-execution-kickoff.js +162 -0
- package/dist/plugin-opencode2.js +52 -3
- package/dist/plugin-v2.d.ts +11 -9
- package/dist/plugin-v2.js +6 -13
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +61 -5
- package/dist/pricing-data.d.ts +16 -4
- package/dist/pricing-data.js +16 -4
- package/dist/protocol/ask-question.d.ts +133 -0
- package/dist/protocol/ask-question.js +320 -0
- package/dist/protocol/blob-reachability.d.ts +15 -0
- package/dist/protocol/blob-reachability.js +140 -0
- package/dist/protocol/blob-store.d.ts +37 -0
- package/dist/protocol/blob-store.js +115 -0
- package/dist/protocol/conversation-bind.d.ts +13 -0
- package/dist/protocol/conversation-bind.js +27 -1
- package/dist/protocol/conversation-persistence.d.ts +32 -0
- package/dist/protocol/conversation-persistence.js +504 -0
- package/dist/protocol/conversation-state.d.ts +16 -0
- package/dist/protocol/conversation-state.js +62 -0
- package/dist/protocol/create-plan.d.ts +141 -0
- package/dist/protocol/create-plan.js +373 -0
- package/dist/protocol/generate-image.d.ts +54 -0
- package/dist/protocol/generate-image.js +101 -0
- package/dist/protocol/interactions.d.ts +110 -8
- package/dist/protocol/interactions.js +295 -35
- package/dist/protocol/kv.d.ts +1 -0
- package/dist/protocol/kv.js +2 -0
- package/dist/protocol/messages.d.ts +2 -0
- package/dist/protocol/messages.js +148 -8
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/switch-mode.d.ts +178 -0
- package/dist/protocol/switch-mode.js +417 -0
- package/dist/protocol/token-details.d.ts +33 -0
- package/dist/protocol/token-details.js +155 -0
- package/dist/protocol/tool-call-bridge.js +10 -5
- package/dist/protocol/tools.d.ts +44 -11
- package/dist/protocol/tools.js +171 -41
- package/dist/session.d.ts +39 -11
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +3 -0
- package/dist/usage.d.ts +42 -37
- package/dist/usage.js +201 -70
- package/dist/web-search-tool.d.ts +22 -26
- package/dist/web-search-tool.js +25 -15
- package/package.json +7 -4
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -71,9 +71,9 @@ export type HostSubagentDefinition = {
|
|
|
71
71
|
description?: string;
|
|
72
72
|
};
|
|
73
73
|
export type HostSubagentCatalog = {
|
|
74
|
-
executor?: "task"
|
|
74
|
+
executor?: "task";
|
|
75
75
|
agents: HostSubagentDefinition[];
|
|
76
|
-
/** True when the
|
|
76
|
+
/** True when the OpenCode task tool supplied its complete, permission-filtered catalog. */
|
|
77
77
|
complete: boolean;
|
|
78
78
|
};
|
|
79
79
|
/** Extract the current host's permission-filtered Task/Actor recipient catalog. */
|
|
@@ -89,6 +89,16 @@ export declare function resolveCursorSubagentType(subagentType: string, catalog?
|
|
|
89
89
|
export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string;
|
|
90
90
|
/** Resolve a native Cursor subagent request to this turn's executor and catalog. */
|
|
91
91
|
export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, catalog?: HostSubagentCatalog): void;
|
|
92
|
+
/**
|
|
93
|
+
* Complete the private read inside Cursor's legacy edit transaction without
|
|
94
|
+
* routing it through OpenCode's user-facing `read` tool. The latter caps every
|
|
95
|
+
* result at 50 KB, while Cursor's editor needs the complete file to calculate
|
|
96
|
+
* the correlated whole-file `write_args` that follows.
|
|
97
|
+
*
|
|
98
|
+
* The caller is responsible for proving that `filePath` is the same regular
|
|
99
|
+
* file named by an active `edit_tool_call` and is contained by the workspace.
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildCompleteEditReadMessages(execId: number, sourcePath: string, resultPath?: string): Uint8Array[] | undefined;
|
|
92
102
|
/**
|
|
93
103
|
* Preserve a legacy Cursor edit's intent when its internal executor follows
|
|
94
104
|
* `edit_tool_call` with a whole-file `write_args` request.
|
|
@@ -125,6 +135,13 @@ export type ParsedExecRequest = {
|
|
|
125
135
|
/** Original request values needed by typed Cursor result messages. */
|
|
126
136
|
resultMetadata?: Record<string, unknown>;
|
|
127
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* A partial-read notice is for Cursor's model, never for a file. Refuse only
|
|
140
|
+
* whole-file mutation forms that echo it. A targeted edit or Update File patch
|
|
141
|
+
* cannot truncate an unseen tail, and may legitimately edit source that quotes
|
|
142
|
+
* this provider's warning text.
|
|
143
|
+
*/
|
|
144
|
+
export declare function rejectPartialReadMutation(parsed: ParsedExecRequest): void;
|
|
128
145
|
export declare function parseExecServerMessage(msg: Record<string, unknown>): ParsedExecRequest | undefined;
|
|
129
146
|
/**
|
|
130
147
|
* Remap Cursor exec / MCP arg shapes onto OpenCode's Effect Schema keys.
|
|
@@ -134,7 +151,28 @@ export declare function parseExecServerMessage(msg: Record<string, unknown>): Pa
|
|
|
134
151
|
export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<string, unknown>, execVariant?: string): {
|
|
135
152
|
toolName: string;
|
|
136
153
|
args: Record<string, unknown>;
|
|
154
|
+
binaryBytes?: Uint8Array;
|
|
137
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* Decode `WriteArgs.file_bytes`. `encoding_hint` names the file's original
|
|
158
|
+
* encoding; OpenCode's `write` takes a string, so an unknown encoding label
|
|
159
|
+
* falls back to UTF-8 rather than dropping the write.
|
|
160
|
+
*
|
|
161
|
+
* Returns undefined when the bytes are not text in that encoding. A non-fatal
|
|
162
|
+
* decode would replace every undecodable sequence with U+FFFD and hand the host
|
|
163
|
+
* a corrupted file that still reports success, so binary content must take the
|
|
164
|
+
* byte-preserving path instead (see `isBinaryWriteRequest`).
|
|
165
|
+
*/
|
|
166
|
+
export declare function decodeWriteBytes(bytes: Uint8Array, encodingHint?: string): string | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* True when a parsed `write` request carries bytes that are not text. Such a
|
|
169
|
+
* request cannot go to OpenCode's `write` tool at all — it takes a string, and
|
|
170
|
+
* additionally BOM-splits, diffs, and runs `Format.file()` on what it writes.
|
|
171
|
+
*/
|
|
172
|
+
export declare function binaryWritePayload(parsed: ParsedExecRequest): {
|
|
173
|
+
path: string;
|
|
174
|
+
data: Uint8Array;
|
|
175
|
+
} | undefined;
|
|
138
176
|
/**
|
|
139
177
|
* Map Cursor McpArgs back to the OpenCode tool id.
|
|
140
178
|
* Prefers provider_identifier + bare tool_name (github + create_pull_request
|
|
@@ -145,15 +183,10 @@ export declare function mcpRealToolName(mcpArgs: Record<string, unknown>, defaul
|
|
|
145
183
|
* A read target addressed by URI scheme rather than by local filesystem path.
|
|
146
184
|
*
|
|
147
185
|
* Cursor's LocalReadExecutor only ever reads local files, so this provider
|
|
148
|
-
* mirrors its pre-execution validation.
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* (`tools.xdev`, on by default). Resolving those against the workspace root
|
|
153
|
-
* mangles `xd://everything_echo` into `<root>/xd:/everything_echo`, which stats
|
|
154
|
-
* as missing and gets refused as `file_not_found` — so the host's read tool is
|
|
155
|
-
* never called and every xd://-mounted MCP tool is unreachable through this
|
|
156
|
-
* provider. Such targets must be forwarded untouched and left to the host.
|
|
186
|
+
* mirrors its pre-execution validation. An advertised host read tool may also
|
|
187
|
+
* accept URLs or internal URI schemes. Resolving those against the workspace
|
|
188
|
+
* root mangles the URI and produces a false `file_not_found`, so URI targets
|
|
189
|
+
* are forwarded untouched and left to the advertised executor.
|
|
157
190
|
*
|
|
158
191
|
* Requires a scheme of two or more characters followed by `://`, so Windows
|
|
159
192
|
* drive letters (`C:\src`, `C:/src`) stay local paths.
|
package/dist/protocol/tools.js
CHANGED
|
@@ -299,6 +299,10 @@ const PRESERVE_EMPTY_STRING_KEYS = new Set([
|
|
|
299
299
|
"file_text",
|
|
300
300
|
"fileText",
|
|
301
301
|
"stream_content",
|
|
302
|
+
// Some advertised edit schemas use a single textual `input` payload. Preserve
|
|
303
|
+
// an empty value so the executor reports the real edit error rather than a
|
|
304
|
+
// misleading missing-property error.
|
|
305
|
+
"input",
|
|
302
306
|
"oldString",
|
|
303
307
|
"old_string",
|
|
304
308
|
"newString",
|
|
@@ -396,18 +400,16 @@ function subagentTypeEnumValues(schema) {
|
|
|
396
400
|
}
|
|
397
401
|
/** Extract the current host's permission-filtered Task/Actor recipient catalog. */
|
|
398
402
|
export function extractHostSubagentCatalog(tools) {
|
|
399
|
-
const executorTool = tools.find((tool) => tool.name === "
|
|
400
|
-
tools.find((tool) => tool.name === "task");
|
|
403
|
+
const executorTool = tools.find((tool) => tool.name === "task");
|
|
401
404
|
if (!executorTool)
|
|
402
405
|
return { agents: [], complete: true };
|
|
403
406
|
const described = parseSubagentDescriptionCatalog(executorTool.description);
|
|
404
407
|
const enumNames = subagentTypeEnumValues(executorTool.inputSchema);
|
|
405
408
|
const descriptions = new Map(described.agents.map((agent) => [agent.name, agent.description]));
|
|
406
|
-
//
|
|
407
|
-
// rather than every non-primary agent), so structured names are authoritative.
|
|
409
|
+
// A structured subagent_type enum is stricter than prose, so it is authoritative.
|
|
408
410
|
const names = new Set(enumNames.length > 0 ? enumNames : described.agents.map((agent) => agent.name));
|
|
409
411
|
return {
|
|
410
|
-
executor:
|
|
412
|
+
executor: "task",
|
|
411
413
|
agents: [...names].map((name) => ({
|
|
412
414
|
name,
|
|
413
415
|
...(descriptions.get(name) ? { description: descriptions.get(name) } : {}),
|
|
@@ -467,9 +469,9 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
|
|
|
467
469
|
if (parsed.resultField !== "subagent_result" || parsed.toolName !== "task")
|
|
468
470
|
return;
|
|
469
471
|
const advertised = new Set(advertisedToolNames);
|
|
470
|
-
|
|
471
|
-
if (!executor)
|
|
472
|
+
if (!advertised.has("task"))
|
|
472
473
|
return;
|
|
474
|
+
const executor = "task";
|
|
473
475
|
const description = str(parsed.args.description) ?? "";
|
|
474
476
|
const prompt = str(parsed.args.prompt) ?? "";
|
|
475
477
|
const cursorSubagentType = str(parsed.resultMetadata?.cursor_subagent_type) ??
|
|
@@ -484,28 +486,55 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
|
|
|
484
486
|
}
|
|
485
487
|
const resumeAgentId = str(parsed.args.task_id);
|
|
486
488
|
parsed.toolName = executor;
|
|
487
|
-
if (executor === "task") {
|
|
488
|
-
parsed.args = {
|
|
489
|
-
description,
|
|
490
|
-
prompt,
|
|
491
|
-
subagent_type: subagentType,
|
|
492
|
-
...(resumeAgentId ? { task_id: resumeAgentId } : {}),
|
|
493
|
-
...(parsed.args.background === true ? { background: true } : {}),
|
|
494
|
-
};
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
489
|
parsed.args = {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
...(resumeAgentId ? { actor_id: resumeAgentId } : {}),
|
|
504
|
-
},
|
|
490
|
+
description,
|
|
491
|
+
prompt,
|
|
492
|
+
subagent_type: subagentType,
|
|
493
|
+
...(resumeAgentId ? { task_id: resumeAgentId } : {}),
|
|
494
|
+
...(parsed.args.background === true ? { background: true } : {}),
|
|
505
495
|
};
|
|
506
496
|
}
|
|
507
497
|
/** Matches Cursor's own pre-write read threshold (`local-exec` 52428800). */
|
|
508
498
|
const MAX_EDIT_SOURCE_BYTES = 50 * 1024 * 1024;
|
|
499
|
+
/**
|
|
500
|
+
* Complete the private read inside Cursor's legacy edit transaction without
|
|
501
|
+
* routing it through OpenCode's user-facing `read` tool. The latter caps every
|
|
502
|
+
* result at 50 KB, while Cursor's editor needs the complete file to calculate
|
|
503
|
+
* the correlated whole-file `write_args` that follows.
|
|
504
|
+
*
|
|
505
|
+
* The caller is responsible for proving that `filePath` is the same regular
|
|
506
|
+
* file named by an active `edit_tool_call` and is contained by the workspace.
|
|
507
|
+
*/
|
|
508
|
+
export function buildCompleteEditReadMessages(execId, sourcePath, resultPath = sourcePath) {
|
|
509
|
+
try {
|
|
510
|
+
const stat = fs.statSync(sourcePath);
|
|
511
|
+
if (!stat.isFile() || stat.size > MAX_EDIT_SOURCE_BYTES)
|
|
512
|
+
return undefined;
|
|
513
|
+
const content = fs.readFileSync(sourcePath, "utf8");
|
|
514
|
+
return [
|
|
515
|
+
encodeMessage("AgentClientMessage", {
|
|
516
|
+
exec_client_message: {
|
|
517
|
+
id: execId,
|
|
518
|
+
local_execution_time_ms: 0,
|
|
519
|
+
read_result: {
|
|
520
|
+
success: {
|
|
521
|
+
path: resultPath,
|
|
522
|
+
content,
|
|
523
|
+
total_lines: countLines(content),
|
|
524
|
+
file_size: stat.size,
|
|
525
|
+
truncated: false,
|
|
526
|
+
range_applied: false,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
}),
|
|
531
|
+
buildExecStreamClose(execId),
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
509
538
|
/** Return the number of non-overlapping occurrences, stopping after ambiguity. */
|
|
510
539
|
function replacementOccurrenceCount(source, needle) {
|
|
511
540
|
if (!needle)
|
|
@@ -643,6 +672,12 @@ export function remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNames,
|
|
|
643
672
|
export function remapEditToolsForCatalog(parsed, advertisedToolNames, workspaceRoot) {
|
|
644
673
|
if (parsed.toolName !== "write" && parsed.toolName !== "edit")
|
|
645
674
|
return;
|
|
675
|
+
// `apply_patch` envelopes are text. A binary write has no patch form at all,
|
|
676
|
+
// so it must stay a write and reach the byte-preserving path — converting it
|
|
677
|
+
// here would drop the bytes and report a patch-translation failure instead of
|
|
678
|
+
// the image Cursor asked to save.
|
|
679
|
+
if (binaryWritePayload(parsed))
|
|
680
|
+
return;
|
|
646
681
|
const advertised = new Set(advertisedToolNames);
|
|
647
682
|
if (advertised.has(parsed.toolName) || !advertised.has(APPLY_PATCH_TOOL))
|
|
648
683
|
return;
|
|
@@ -709,6 +744,43 @@ export function remapEditToolsForCatalog(parsed, advertisedToolNames, workspaceR
|
|
|
709
744
|
// the requested path for the typed Cursor result.
|
|
710
745
|
parsed.resultMetadata = { ...parsed.resultMetadata, path: filePath };
|
|
711
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* A partial-read notice is for Cursor's model, never for a file. Refuse only
|
|
749
|
+
* whole-file mutation forms that echo it. A targeted edit or Update File patch
|
|
750
|
+
* cannot truncate an unseen tail, and may legitimately edit source that quotes
|
|
751
|
+
* this provider's warning text.
|
|
752
|
+
*/
|
|
753
|
+
export function rejectPartialReadMutation(parsed) {
|
|
754
|
+
if (parsed.localError)
|
|
755
|
+
return;
|
|
756
|
+
let content;
|
|
757
|
+
if (parsed.toolName === "write") {
|
|
758
|
+
content = parsed.args.content;
|
|
759
|
+
}
|
|
760
|
+
else if (parsed.toolName === APPLY_PATCH_TOOL) {
|
|
761
|
+
const patchText = parsed.args.patchText;
|
|
762
|
+
if (typeof patchText !== "string" || !patchText.includes("*** Add File:"))
|
|
763
|
+
return;
|
|
764
|
+
content = patchText;
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (typeof content !== "string"
|
|
770
|
+
|| !content.includes("[Partial read:")
|
|
771
|
+
|| !content.includes("It is NOT the complete file."))
|
|
772
|
+
return;
|
|
773
|
+
const filePath = typeof parsed.args.filePath === "string"
|
|
774
|
+
? parsed.args.filePath
|
|
775
|
+
: "the target file";
|
|
776
|
+
const nextOffset = /Continue with offset=(\d+)/.exec(content)?.[1];
|
|
777
|
+
parsed.localError =
|
|
778
|
+
`NO FILE CHANGE WAS MADE. Refusing a whole-file mutation of ${JSON.stringify(filePath)} ` +
|
|
779
|
+
"because it contains the provider's partial-read notice. Do not retry the same mutation. " +
|
|
780
|
+
(nextOffset
|
|
781
|
+
? `Read the file from offset=${nextOffset}, then use a targeted edit or Update File patch.`
|
|
782
|
+
: "Read the remaining file ranges, then use a targeted edit or Update File patch.");
|
|
783
|
+
}
|
|
712
784
|
export function parseExecServerMessage(msg) {
|
|
713
785
|
const id = msg.id;
|
|
714
786
|
if (id === undefined)
|
|
@@ -864,7 +936,19 @@ export function parseExecServerMessage(msg) {
|
|
|
864
936
|
toolName: mapped.toolName,
|
|
865
937
|
args: mapped.args,
|
|
866
938
|
resultField,
|
|
867
|
-
...(resultMetadata
|
|
939
|
+
...(resultMetadata || mapped.binaryBytes
|
|
940
|
+
? {
|
|
941
|
+
resultMetadata: {
|
|
942
|
+
...resultMetadata,
|
|
943
|
+
...(mapped.binaryBytes
|
|
944
|
+
? {
|
|
945
|
+
binaryWriteBytes: mapped.binaryBytes,
|
|
946
|
+
path: str(rawArgs.path) ?? str(rawArgs.file_path) ?? "",
|
|
947
|
+
}
|
|
948
|
+
: {}),
|
|
949
|
+
},
|
|
950
|
+
}
|
|
951
|
+
: {}),
|
|
868
952
|
};
|
|
869
953
|
}
|
|
870
954
|
function shellStreamResultMetadata(raw) {
|
|
@@ -944,14 +1028,31 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
944
1028
|
// non-empty and only falls back to `file_text`; mirror that order so a
|
|
945
1029
|
// byte-encoded write is not silently seen as empty content.
|
|
946
1030
|
const bytes = bytesValue(cleaned.file_bytes) ?? bytesValue(cleaned.fileBytes);
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1031
|
+
let content;
|
|
1032
|
+
if (bytes !== undefined) {
|
|
1033
|
+
content = decodeWriteBytes(bytes, str(cleaned.encoding_hint) ?? str(cleaned.encodingHint));
|
|
1034
|
+
if (content === undefined) {
|
|
1035
|
+
// Binary. Surface the raw bytes to the caller instead of a lossy
|
|
1036
|
+
// decode; `binaryWritePayload` picks them up after parsing.
|
|
1037
|
+
return { toolName: "write", args, binaryBytes: bytes };
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
content = stringValue(cleaned.content)
|
|
1042
|
+
?? stringValue(cleaned.file_text)
|
|
1043
|
+
?? stringValue(cleaned.fileText);
|
|
1044
|
+
}
|
|
950
1045
|
if (content !== undefined)
|
|
951
1046
|
args.content = content;
|
|
952
1047
|
return { toolName: "write", args };
|
|
953
1048
|
}
|
|
954
1049
|
case "edit": {
|
|
1050
|
+
// Preserve an opaque textual edit payload when the advertised executor
|
|
1051
|
+
// accepts that generic shape; rebuilding only OpenCode's targeted fields
|
|
1052
|
+
// would silently turn a valid call into `{}`.
|
|
1053
|
+
if (typeof cleaned.input === "string") {
|
|
1054
|
+
return { toolName: "edit", args: { input: cleaned.input } };
|
|
1055
|
+
}
|
|
955
1056
|
const args = {};
|
|
956
1057
|
const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
|
|
957
1058
|
if (filePath)
|
|
@@ -1029,22 +1130,56 @@ function bytesValue(v) {
|
|
|
1029
1130
|
}
|
|
1030
1131
|
return undefined;
|
|
1031
1132
|
}
|
|
1133
|
+
/** UTF-16/32 embed NUL bytes in ordinary text; 8-bit encodings do not. */
|
|
1134
|
+
const WIDE_TEXT_ENCODING = /^utf-?(16|32)/;
|
|
1032
1135
|
/**
|
|
1033
1136
|
* Decode `WriteArgs.file_bytes`. `encoding_hint` names the file's original
|
|
1034
|
-
* encoding; OpenCode's `write` takes a string, so
|
|
1137
|
+
* encoding; OpenCode's `write` takes a string, so an unknown encoding label
|
|
1035
1138
|
* falls back to UTF-8 rather than dropping the write.
|
|
1139
|
+
*
|
|
1140
|
+
* Returns undefined when the bytes are not text in that encoding. A non-fatal
|
|
1141
|
+
* decode would replace every undecodable sequence with U+FFFD and hand the host
|
|
1142
|
+
* a corrupted file that still reports success, so binary content must take the
|
|
1143
|
+
* byte-preserving path instead (see `isBinaryWriteRequest`).
|
|
1036
1144
|
*/
|
|
1037
|
-
function decodeWriteBytes(bytes, encodingHint) {
|
|
1145
|
+
export function decodeWriteBytes(bytes, encodingHint) {
|
|
1038
1146
|
const encoding = encodingHint?.trim().toLowerCase().replace(/[_ ]/g, "-");
|
|
1147
|
+
let decoder;
|
|
1148
|
+
let label = "utf-8";
|
|
1039
1149
|
if (encoding && encoding !== "utf-8" && encoding !== "utf8") {
|
|
1040
1150
|
try {
|
|
1041
|
-
|
|
1151
|
+
decoder = new TextDecoder(encoding, { fatal: true });
|
|
1152
|
+
label = encoding;
|
|
1042
1153
|
}
|
|
1043
1154
|
catch {
|
|
1044
1155
|
trace(`write: unsupported encoding_hint ${JSON.stringify(encodingHint)} — decoding as utf-8`);
|
|
1045
1156
|
}
|
|
1046
1157
|
}
|
|
1047
|
-
|
|
1158
|
+
if (!decoder)
|
|
1159
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1160
|
+
// Single-byte encodings decode any byte sequence without error, so a decode
|
|
1161
|
+
// that cannot fail is not on its own evidence of text.
|
|
1162
|
+
if (!WIDE_TEXT_ENCODING.test(label) && bytes.includes(0))
|
|
1163
|
+
return undefined;
|
|
1164
|
+
try {
|
|
1165
|
+
return decoder.decode(bytes);
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* True when a parsed `write` request carries bytes that are not text. Such a
|
|
1173
|
+
* request cannot go to OpenCode's `write` tool at all — it takes a string, and
|
|
1174
|
+
* additionally BOM-splits, diffs, and runs `Format.file()` on what it writes.
|
|
1175
|
+
*/
|
|
1176
|
+
export function binaryWritePayload(parsed) {
|
|
1177
|
+
if (parsed.toolName !== "write")
|
|
1178
|
+
return undefined;
|
|
1179
|
+
const data = parsed.resultMetadata?.binaryWriteBytes;
|
|
1180
|
+
if (!(data instanceof Uint8Array))
|
|
1181
|
+
return undefined;
|
|
1182
|
+
return { path: str(parsed.resultMetadata?.path) ?? "", data };
|
|
1048
1183
|
}
|
|
1049
1184
|
function num(v) {
|
|
1050
1185
|
if (typeof v === "number" && Number.isFinite(v))
|
|
@@ -1127,15 +1262,10 @@ function untildify(input) {
|
|
|
1127
1262
|
* A read target addressed by URI scheme rather than by local filesystem path.
|
|
1128
1263
|
*
|
|
1129
1264
|
* Cursor's LocalReadExecutor only ever reads local files, so this provider
|
|
1130
|
-
* mirrors its pre-execution validation.
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
* (`tools.xdev`, on by default). Resolving those against the workspace root
|
|
1135
|
-
* mangles `xd://everything_echo` into `<root>/xd:/everything_echo`, which stats
|
|
1136
|
-
* as missing and gets refused as `file_not_found` — so the host's read tool is
|
|
1137
|
-
* never called and every xd://-mounted MCP tool is unreachable through this
|
|
1138
|
-
* provider. Such targets must be forwarded untouched and left to the host.
|
|
1265
|
+
* mirrors its pre-execution validation. An advertised host read tool may also
|
|
1266
|
+
* accept URLs or internal URI schemes. Resolving those against the workspace
|
|
1267
|
+
* root mangles the URI and produces a false `file_not_found`, so URI targets
|
|
1268
|
+
* are forwarded untouched and left to the advertised executor.
|
|
1139
1269
|
*
|
|
1140
1270
|
* Requires a scheme of two or more characters followed by `://`, so Windows
|
|
1141
1271
|
* drive letters (`C:\src`, `C:/src`) stay local paths.
|
package/dist/session.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { BidiStream } from "./transport/connect.js";
|
|
2
2
|
import { type CursorProviderError } from "./errors.js";
|
|
3
3
|
import { type SessionActivitySource } from "./activity.js";
|
|
4
|
-
import type { HostSubagentCatalog, ToolAliasRegistry } from "./protocol/tools.js";
|
|
4
|
+
import type { HostSubagentCatalog, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
|
|
5
|
+
import type { CursorConversationTokenDetails } from "./protocol/token-details.js";
|
|
5
6
|
export type Frame = {
|
|
6
7
|
flags: number;
|
|
7
8
|
payload: Uint8Array;
|
|
@@ -99,10 +100,44 @@ export type CursorSession = {
|
|
|
99
100
|
* conversation_checkpoint_update (CLI parity).
|
|
100
101
|
*/
|
|
101
102
|
conversationId: string;
|
|
103
|
+
/** Cache root owning this conversation's durable restart state. */
|
|
104
|
+
cacheDir?: string;
|
|
102
105
|
/** Latest eligible checkpoint emitted by this Run attempt. */
|
|
103
106
|
resumeCheckpoint?: Uint8Array;
|
|
107
|
+
/** Last checkpoint-derived context snapshot, independent of retry eligibility. */
|
|
108
|
+
tokenDetails?: CursorConversationTokenDetails;
|
|
109
|
+
/** True only after this Run receives a checkpoint containing token details. */
|
|
110
|
+
tokenDetailsFresh?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Run-lifetime cache evidence. Cursor exposes only aggregate cache counters at
|
|
113
|
+
* TurnEnded, so retain the inputs and protocol activity needed to explain
|
|
114
|
+
* whether a low ratio came from a cold/rebased prefix, context growth, or a
|
|
115
|
+
* multi-step agent Run.
|
|
116
|
+
*/
|
|
117
|
+
cacheDiagnostics?: {
|
|
118
|
+
sessionKey?: string;
|
|
119
|
+
conversationId: string;
|
|
120
|
+
conversationGroupId?: string;
|
|
121
|
+
modelId?: string;
|
|
122
|
+
priorTokenDetails?: CursorConversationTokenDetails;
|
|
123
|
+
startedWithCheckpoint: boolean;
|
|
124
|
+
requestContextReused: boolean;
|
|
125
|
+
requestContextHash: string;
|
|
126
|
+
systemPromptHash?: string;
|
|
127
|
+
checkpointUpdates: number;
|
|
128
|
+
tokenDetailUpdates: number;
|
|
129
|
+
pumpPasses: number;
|
|
130
|
+
stepStarts: number;
|
|
131
|
+
stepCompletes: number;
|
|
132
|
+
displayToolCalls: number;
|
|
133
|
+
execRequests: number;
|
|
134
|
+
};
|
|
104
135
|
/** OpenCode session whose own or descendant activity renews tool leases. */
|
|
105
136
|
openCodeSessionId?: string;
|
|
137
|
+
/** Completed compaction must rebase once before resuming a normal agent. */
|
|
138
|
+
postCompactionRebase?: boolean;
|
|
139
|
+
/** Last real host catalog, retained only as a lifecycle-turn fallback. */
|
|
140
|
+
toolCatalog?: OpencodeToolDef[];
|
|
106
141
|
stream: BidiStream;
|
|
107
142
|
frames: AsyncIterator<Frame>;
|
|
108
143
|
pending: Map<number, PendingExec>;
|
|
@@ -119,6 +154,7 @@ export type CursorSession = {
|
|
|
119
154
|
*/
|
|
120
155
|
editToolCalls?: Map<string, {
|
|
121
156
|
path: string;
|
|
157
|
+
completeRead?: boolean;
|
|
122
158
|
}>;
|
|
123
159
|
/** Monotonic synthetic exec ids for bridged (display-only) OpenCode tool calls. */
|
|
124
160
|
nextBridgedExecId: number;
|
|
@@ -139,9 +175,8 @@ export type CursorSession = {
|
|
|
139
175
|
*/
|
|
140
176
|
allowTools: boolean;
|
|
141
177
|
/**
|
|
142
|
-
* Best-effort
|
|
143
|
-
*
|
|
144
|
-
* tool-call finishes so OpenCode does not store all-zero usage mid-loop.
|
|
178
|
+
* Best-effort held-Run activity counters used only for diagnostics. AI SDK
|
|
179
|
+
* accounting comes from the request-local final TurnEnded, never this estimate.
|
|
145
180
|
*/
|
|
146
181
|
usageEstimate: {
|
|
147
182
|
inputTokens: number;
|
|
@@ -150,13 +185,6 @@ export type CursorSession = {
|
|
|
150
185
|
cacheWrite: number;
|
|
151
186
|
reasoningTokens: number;
|
|
152
187
|
};
|
|
153
|
-
/**
|
|
154
|
-
* True after this held Run has already crossed an OpenCode tool-call
|
|
155
|
-
* boundary. Cursor's eventual TurnEnded counters then cover the whole Run,
|
|
156
|
-
* not only the final doStream request, so they cannot be used as that
|
|
157
|
-
* request's context usage.
|
|
158
|
-
*/
|
|
159
|
-
hadToolCallBoundary?: boolean;
|
|
160
188
|
/**
|
|
161
189
|
* True while a doStream pull() is actively reading this session's frames.
|
|
162
190
|
* Prevents a late cancel/abort from a prior ReadableStream from destroying
|
package/dist/shared.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export declare const MODEL_CACHE_FILE = "cursor-models.json";
|
|
|
12
12
|
/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
|
|
13
13
|
export declare const MODEL_CACHE_SCHEMA_VERSION = 3;
|
|
14
14
|
export declare const MODEL_CACHE_TTL_MS = 86400000;
|
|
15
|
+
export declare const CONVERSATION_CACHE_DIR = "cursor-conversations";
|
|
16
|
+
export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 3;
|
|
17
|
+
export declare const CONVERSATION_CACHE_TTL_MS = 86400000;
|
|
15
18
|
export declare const VERSION_CACHE_FILE = "cursor-client-version.json";
|
|
16
19
|
export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
|
|
17
20
|
export declare const CONNECT_PROTOCOL_VERSION = "1";
|
package/dist/shared.js
CHANGED
|
@@ -12,6 +12,9 @@ export const MODEL_CACHE_FILE = "cursor-models.json";
|
|
|
12
12
|
/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
|
|
13
13
|
export const MODEL_CACHE_SCHEMA_VERSION = 3;
|
|
14
14
|
export const MODEL_CACHE_TTL_MS = 86_400_000;
|
|
15
|
+
export const CONVERSATION_CACHE_DIR = "cursor-conversations";
|
|
16
|
+
export const CONVERSATION_CACHE_SCHEMA_VERSION = 3;
|
|
17
|
+
export const CONVERSATION_CACHE_TTL_MS = 86_400_000;
|
|
15
18
|
export const VERSION_CACHE_FILE = "cursor-client-version.json";
|
|
16
19
|
export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
|
|
17
20
|
export const CONNECT_PROTOCOL_VERSION = "1";
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,51 +1,56 @@
|
|
|
1
1
|
import type { LanguageModelV3Usage } from "@ai-sdk/provider";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
/**
|
|
5
|
-
* Map request-local Cursor TurnEnded counters to AI SDK V3 usage (Kilo/OpenCode
|
|
6
|
-
* flatten via input total = noCache + cacheRead + cacheWrite). Only use this
|
|
7
|
-
* for a Run with no prior tool boundary; multi-step TurnEnded is cumulative.
|
|
8
|
-
*
|
|
9
|
-
* Contract: `input_tokens` is non-cached input; cache fields are separate.
|
|
10
|
-
* Cursor's agent wire currently sends `cache_write=0` even when later turns
|
|
11
|
-
* report large `cache_read`; we still forward the counter when non-zero.
|
|
12
|
-
* If production semantics differ (input_tokens already total), adjust here.
|
|
13
|
-
*/
|
|
14
|
-
export declare function buildLanguageModelV3UsageFromTurnEnded(te: Record<string, unknown>): LanguageModelV3Usage;
|
|
15
|
-
export type CursorUsageEstimate = {
|
|
2
|
+
import type { CursorContextUsageSource, CursorConversationTokenDetails } from "./protocol/token-details.js";
|
|
3
|
+
export type CursorUsageCounters = {
|
|
16
4
|
inputTokens: number;
|
|
17
5
|
outputTokens: number;
|
|
18
6
|
cacheRead: number;
|
|
19
7
|
cacheWrite: number;
|
|
20
8
|
reasoningTokens: number;
|
|
21
9
|
};
|
|
10
|
+
export type CursorUsageOptions = {
|
|
11
|
+
/** Cursor checkpoint occupancy, including the current turn's output. */
|
|
12
|
+
contextTotalTokens?: number;
|
|
13
|
+
};
|
|
14
|
+
export type CursorCacheDiagnosticStats = {
|
|
15
|
+
sessionKey?: string;
|
|
16
|
+
conversationId: string;
|
|
17
|
+
conversationGroupId?: string;
|
|
18
|
+
modelId?: string;
|
|
19
|
+
startedWithCheckpoint: boolean;
|
|
20
|
+
requestContextReused: boolean;
|
|
21
|
+
requestContextHash: string;
|
|
22
|
+
systemPromptHash?: string;
|
|
23
|
+
checkpointUpdates: number;
|
|
24
|
+
tokenDetailUpdates: number;
|
|
25
|
+
pumpPasses: number;
|
|
26
|
+
stepStarts: number;
|
|
27
|
+
stepCompletes: number;
|
|
28
|
+
displayToolCalls: number;
|
|
29
|
+
execRequests: number;
|
|
30
|
+
};
|
|
31
|
+
/** Non-negative integer counter from a Cursor `turn_ended` field. */
|
|
32
|
+
export declare function turnEndedCounter(te: Record<string, unknown>, key: string): number;
|
|
33
|
+
export declare function cursorUsageCountersFromTurnEnded(te: Record<string, unknown>): CursorUsageCounters;
|
|
22
34
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* conversation/Run totals even on Runs that never crossed an OpenCode tool
|
|
26
|
-
* boundary: single-step follow-up turns and parallel no-tools side calls
|
|
27
|
-
* carry the whole conversation's `cache_read`. Treating those totals as
|
|
28
|
-
* request-local inflates OpenCode's context usage and triggers premature
|
|
29
|
-
* compaction (issue #20).
|
|
35
|
+
* Map Cursor counters to AI SDK V3. Cursor's `input_tokens` already includes
|
|
36
|
+
* cache reads/writes, and `output_tokens` already includes reasoning.
|
|
30
37
|
*/
|
|
31
|
-
export declare
|
|
32
|
-
export declare
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
outputTokens: number;
|
|
36
|
-
}): boolean;
|
|
38
|
+
export declare function buildLanguageModelV3UsageFromCounters(counters: CursorUsageCounters, options?: CursorUsageOptions): LanguageModelV3Usage;
|
|
39
|
+
export declare function buildLanguageModelV3UsageFromTurnEnded(te: Record<string, unknown>, options?: CursorUsageOptions): LanguageModelV3Usage;
|
|
40
|
+
/** Compact, parseable category snapshot for checkpoint-by-checkpoint traces. */
|
|
41
|
+
export declare function formatCursorTokenCategories(details: CursorConversationTokenDetails | undefined): string;
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `est.outputTokens` accumulates model output across earlier tool boundaries.
|
|
42
|
-
* Move prior output into the current input estimate and report only this
|
|
43
|
-
* doStream request's output. Cache counters are intentionally absent: the
|
|
44
|
-
* only counters Cursor exposes at TurnEnded are cumulative for the whole Run.
|
|
43
|
+
* Explain Cursor's aggregate cache counters using the state visible to this
|
|
44
|
+
* client. `stepStarts` is deliberately not called a model-call count: Cursor
|
|
45
|
+
* does not expose per-model-call cache accounting on the Run stream.
|
|
45
46
|
*/
|
|
46
|
-
export declare function
|
|
47
|
-
/**
|
|
48
|
-
export declare function
|
|
47
|
+
export declare function formatCursorCacheDiagnostics(counters: CursorUsageCounters, current: CursorConversationTokenDetails | undefined, prior: CursorConversationTokenDetails | undefined, stats: CursorCacheDiagnosticStats): string;
|
|
48
|
+
/** One-line proof that Cursor, AI SDK, and projected OpenCode totals agree. */
|
|
49
|
+
export declare function formatTurnUsageValidation(counters: CursorUsageCounters, usage: LanguageModelV3Usage, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): string;
|
|
50
|
+
/** OpenCode requires a usage object at every step boundary. */
|
|
51
|
+
export declare function emptyLanguageModelV3Usage(): LanguageModelV3Usage;
|
|
52
|
+
/** Project nested V3 usage into the common flat AI-SDK counter shape. */
|
|
53
|
+
export declare function flatUsageFromV3(usage: LanguageModelV3Usage): {
|
|
49
54
|
inputTokens: number;
|
|
50
55
|
outputTokens: number;
|
|
51
56
|
reasoningTokens: number;
|