cursor-opencode-provider 0.6.7 → 0.7.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 +52 -180
- package/dist/compaction-marker.d.ts +3 -2
- package/dist/compaction-marker.js +3 -2
- package/dist/host-agent-mode.d.ts +32 -0
- package/dist/host-agent-mode.js +92 -0
- package/dist/language-model.d.ts +10 -1
- package/dist/language-model.js +253 -41
- package/dist/models.d.ts +12 -1
- package/dist/models.js +16 -6
- package/dist/opencode2/catalog.d.ts +46 -11
- package/dist/opencode2/catalog.js +60 -45
- package/dist/opencode2/todo-tools.d.ts +64 -0
- package/dist/opencode2/todo-tools.js +165 -0
- package/dist/opencode2/types.d.ts +138 -83
- package/dist/opencode2/types.js +8 -10
- package/dist/plan-execution-kickoff.d.ts +2 -0
- package/dist/plan-execution-kickoff.js +25 -1
- package/dist/plugin-opencode2.d.ts +4 -1
- package/dist/plugin-opencode2.js +335 -154
- package/dist/plugin.js +5 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +19 -1
- package/dist/protocol/conversation-state.d.ts +4 -0
- package/dist/protocol/conversation-state.js +4 -0
- package/dist/protocol/messages.js +14 -1
- package/dist/protocol/switch-mode.d.ts +5 -4
- package/dist/protocol/switch-mode.js +12 -4
- package/dist/protocol/tool-call-bridge.d.ts +29 -1
- package/dist/protocol/tool-call-bridge.js +187 -7
- package/dist/protocol/tools.d.ts +30 -4
- package/dist/protocol/tools.js +185 -58
- package/dist/session.d.ts +13 -1
- package/dist/session.js +1 -0
- package/dist/shared.d.ts +3 -1
- package/dist/shared.js +3 -1
- package/dist/shell-timeout.d.ts +13 -0
- package/dist/shell-timeout.js +38 -0
- package/dist/todo-store.d.ts +29 -0
- package/dist/todo-store.js +78 -0
- package/dist/web-tools.d.ts +13 -0
- package/dist/web-tools.js +83 -5
- package/package.json +6 -3
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { CURSOR_COMPACTION_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST,
|
|
3
|
+
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION, CURSOR_HOST_AGENT_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, } from "./shared.js";
|
|
4
4
|
import { cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled } from "./plugin-core.js";
|
|
5
5
|
import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs } from "./auth.js";
|
|
6
6
|
import { readCache, discoverModels, isCacheFresh } from "./models.js";
|
|
@@ -254,6 +254,10 @@ export async function CursorPlugin(input) {
|
|
|
254
254
|
async "chat.params"(hookInput, output) {
|
|
255
255
|
if (hookInput.model.providerID !== CURSOR_PROVIDER_ID)
|
|
256
256
|
return;
|
|
257
|
+
// Agent changes can replace the host system prompt and mode contract.
|
|
258
|
+
// Carry the canonical OpenCode id so an incompatible Cursor checkpoint
|
|
259
|
+
// is rotated instead of resuming the prior agent's prompt.
|
|
260
|
+
output.options[CURSOR_HOST_AGENT_OPTION] = hookInput.agent;
|
|
257
261
|
// OpenCode's compaction pipeline invokes the LLM with agent="compaction".
|
|
258
262
|
// Carry that stable runtime fact into LanguageModelV3 providerOptions so
|
|
259
263
|
// the provider never has to guess from an empty tool list.
|
|
@@ -9,6 +9,8 @@ export type PersistedConversation = {
|
|
|
9
9
|
requestContext: Record<string, unknown>;
|
|
10
10
|
toolCatalog: OpencodeToolDef[];
|
|
11
11
|
postCompactionRebase: boolean;
|
|
12
|
+
hostAgent?: string;
|
|
13
|
+
systemPromptHash?: string;
|
|
12
14
|
};
|
|
13
15
|
export type ConversationLoadStatus = "restored" | "missing" | "invalid" | "expired";
|
|
14
16
|
export type PersistedConversationLoad = {
|
|
@@ -47,7 +47,7 @@ function cloneConversation(value) {
|
|
|
47
47
|
*
|
|
48
48
|
* ConversationCache: schema_version=1, session_key=2, conversation_id=3,
|
|
49
49
|
* updated_at=4, checkpoint=5, blobs=6, request_context=7, tool_catalog=8,
|
|
50
|
-
* post_compaction_rebase=9.
|
|
50
|
+
* post_compaction_rebase=9, host_agent=10, system_prompt_hash=11.
|
|
51
51
|
* Blob: id=1, data=2. Tool: name=1, description=2,
|
|
52
52
|
* input_schema_json=3, source_name=4.
|
|
53
53
|
*
|
|
@@ -172,6 +172,10 @@ function encodeCacheFile(value) {
|
|
|
172
172
|
}
|
|
173
173
|
if (value.postCompactionRebase)
|
|
174
174
|
writer.uint32(fieldTag(9, 0)).bool(true);
|
|
175
|
+
if (value.hostAgent)
|
|
176
|
+
writer.uint32(fieldTag(10, 2)).string(value.hostAgent);
|
|
177
|
+
if (value.systemPromptHash)
|
|
178
|
+
writer.uint32(fieldTag(11, 2)).string(value.systemPromptHash);
|
|
175
179
|
return { protobufBytes: writer.finish(), requestContextBytes: requestContext.length };
|
|
176
180
|
}
|
|
177
181
|
function decodeProtobuf(data) {
|
|
@@ -185,6 +189,8 @@ function decodeProtobuf(data) {
|
|
|
185
189
|
let requestContextBytes;
|
|
186
190
|
const toolCatalog = [];
|
|
187
191
|
let postCompactionRebase = false;
|
|
192
|
+
let hostAgent;
|
|
193
|
+
let systemPromptHash;
|
|
188
194
|
while (reader.pos < reader.len) {
|
|
189
195
|
const tag = reader.uint32();
|
|
190
196
|
const wireType = tag & 7;
|
|
@@ -234,6 +240,16 @@ function decodeProtobuf(data) {
|
|
|
234
240
|
throw new Error("invalid compaction marker");
|
|
235
241
|
postCompactionRebase = reader.bool();
|
|
236
242
|
break;
|
|
243
|
+
case 10:
|
|
244
|
+
if (wireType !== 2)
|
|
245
|
+
throw new Error("invalid host agent");
|
|
246
|
+
hostAgent = reader.string();
|
|
247
|
+
break;
|
|
248
|
+
case 11:
|
|
249
|
+
if (wireType !== 2)
|
|
250
|
+
throw new Error("invalid system prompt hash");
|
|
251
|
+
systemPromptHash = reader.string();
|
|
252
|
+
break;
|
|
237
253
|
default:
|
|
238
254
|
reader.skipType(wireType);
|
|
239
255
|
}
|
|
@@ -252,6 +268,8 @@ function decodeProtobuf(data) {
|
|
|
252
268
|
requestContext,
|
|
253
269
|
toolCatalog,
|
|
254
270
|
postCompactionRebase,
|
|
271
|
+
...(hostAgent ? { hostAgent } : {}),
|
|
272
|
+
...(systemPromptHash ? { systemPromptHash } : {}),
|
|
255
273
|
};
|
|
256
274
|
}
|
|
257
275
|
function decodeCacheFile(compressed, expectedSessionKey) {
|
|
@@ -4,6 +4,8 @@ export declare function hydrateConversationState(cacheDir: string, sessionKey: s
|
|
|
4
4
|
conversationId: string;
|
|
5
5
|
postCompactionRebase: boolean;
|
|
6
6
|
toolCatalog: OpencodeToolDef[];
|
|
7
|
+
hostAgent?: string;
|
|
8
|
+
systemPromptHash?: string;
|
|
7
9
|
} | undefined>;
|
|
8
10
|
/** Persist the complete resumable state only after Cursor confirms TurnEnded. */
|
|
9
11
|
export declare function persistConversationState(cacheDir: string, input: {
|
|
@@ -12,5 +14,7 @@ export declare function persistConversationState(cacheDir: string, input: {
|
|
|
12
14
|
requestContext: Record<string, unknown>;
|
|
13
15
|
toolCatalog?: OpencodeToolDef[];
|
|
14
16
|
postCompactionRebase?: boolean;
|
|
17
|
+
hostAgent?: string;
|
|
18
|
+
systemPromptHash?: string;
|
|
15
19
|
}): Promise<void>;
|
|
16
20
|
export declare function clearPersistedConversationState(cacheDir: string, sessionKey: string, expectedConversationId?: string): Promise<void>;
|
|
@@ -27,6 +27,8 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
|
27
27
|
conversationId: persisted.conversationId,
|
|
28
28
|
postCompactionRebase: persisted.postCompactionRebase,
|
|
29
29
|
toolCatalog: structuredClone(persisted.toolCatalog),
|
|
30
|
+
...(persisted.hostAgent ? { hostAgent: persisted.hostAgent } : {}),
|
|
31
|
+
...(persisted.systemPromptHash ? { systemPromptHash: persisted.systemPromptHash } : {}),
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
/** Persist the complete resumable state only after Cursor confirms TurnEnded. */
|
|
@@ -50,6 +52,8 @@ export async function persistConversationState(cacheDir, input) {
|
|
|
50
52
|
requestContext,
|
|
51
53
|
toolCatalog: structuredClone(input.toolCatalog ?? []),
|
|
52
54
|
postCompactionRebase: input.postCompactionRebase,
|
|
55
|
+
hostAgent: input.hostAgent,
|
|
56
|
+
systemPromptHash: input.systemPromptHash,
|
|
53
57
|
});
|
|
54
58
|
trace(`conversation persistence: saved sessionKey=${input.sessionKey} ` +
|
|
55
59
|
`conversationId=${input.conversationId} checkpoint=${checkpoint?.length ?? 0}B ` +
|
|
@@ -257,11 +257,24 @@ export function createMessageTypes() {
|
|
|
257
257
|
addType(root, "PiFindToolCall", [{ id: 1, name: "args", type: "PiFindToolArgs" }]);
|
|
258
258
|
addType(root, "PiLsToolCall", [{ id: 1, name: "args", type: "PiLsToolArgs" }]);
|
|
259
259
|
// Display-only native tools Cursor may complete without an ExecServerMessage.
|
|
260
|
+
// ReadTodos mirrors UpdateTodos: args + result (see agent.v1 ReadTodosToolCall).
|
|
260
261
|
addType(root, "ReadTodosArgs", [
|
|
261
262
|
{ id: 1, name: "status_filter", type: "TodoStatus", repeated: true },
|
|
262
263
|
{ id: 2, name: "id_filter", type: "string", repeated: true },
|
|
263
264
|
]);
|
|
264
|
-
addType(root, "
|
|
265
|
+
addType(root, "ReadTodosSuccess", [
|
|
266
|
+
{ id: 1, name: "todos", type: "TodoItem", repeated: true },
|
|
267
|
+
{ id: 2, name: "total_count", type: "int32" },
|
|
268
|
+
]);
|
|
269
|
+
addType(root, "ReadTodosError", [{ id: 1, name: "error", type: "string" }]);
|
|
270
|
+
addType(root, "ReadTodosResult", [
|
|
271
|
+
{ id: 1, name: "success", type: "ReadTodosSuccess" },
|
|
272
|
+
{ id: 2, name: "error", type: "ReadTodosError" },
|
|
273
|
+
], [{ name: "result", fields: ["success", "error"] }]);
|
|
274
|
+
addType(root, "ReadTodosToolCall", [
|
|
275
|
+
{ id: 1, name: "args", type: "ReadTodosArgs" },
|
|
276
|
+
{ id: 2, name: "result", type: "ReadTodosResult" },
|
|
277
|
+
]);
|
|
265
278
|
addType(root, "AwaitArgs", [
|
|
266
279
|
{ id: 1, name: "task_id", type: "string" },
|
|
267
280
|
{ id: 2, name: "block_until_ms", type: "uint32" },
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Display `switch_mode_tool_call` (#25) is a separate transcript record and is
|
|
9
9
|
* not replayed as a host tool.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* OpenCode 1.x mapping (advertisement-gated):
|
|
12
12
|
* - plan, spec → plan_enter
|
|
13
13
|
* - every other non-empty target → plan_exit (leave plan for OpenCode build),
|
|
14
14
|
* then inject the Cursor CLI-shaped mode reminder for that target
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
* `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
|
|
20
20
|
* `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
|
|
21
21
|
* observable halves — the approval gate and the behavioural contract — are both
|
|
22
|
-
* reachable, so the bridge degrades instead of refusing when the host
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* reachable, so the protocol bridge degrades instead of refusing when the host
|
|
23
|
+
* tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its
|
|
24
|
+
* entrypoint installs a structural callback that selects the native `plan` or
|
|
25
|
+
* `build` primary agent after the owning Cursor Run becomes terminal:
|
|
25
26
|
*
|
|
26
27
|
* - entering plan/spec needs no host tool at all — approve immediately and let
|
|
27
28
|
* the injected <system_reminder> carry the contract, exactly as Cursor CLI's
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Display `switch_mode_tool_call` (#25) is a separate transcript record and is
|
|
9
9
|
* not replayed as a host tool.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* OpenCode 1.x mapping (advertisement-gated):
|
|
12
12
|
* - plan, spec → plan_enter
|
|
13
13
|
* - every other non-empty target → plan_exit (leave plan for OpenCode build),
|
|
14
14
|
* then inject the Cursor CLI-shaped mode reminder for that target
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
* `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
|
|
20
20
|
* `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
|
|
21
21
|
* observable halves — the approval gate and the behavioural contract — are both
|
|
22
|
-
* reachable, so the bridge degrades instead of refusing when the host
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* reachable, so the protocol bridge degrades instead of refusing when the host
|
|
23
|
+
* tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its
|
|
24
|
+
* entrypoint installs a structural callback that selects the native `plan` or
|
|
25
|
+
* `build` primary agent after the owning Cursor Run becomes terminal:
|
|
25
26
|
*
|
|
26
27
|
* - entering plan/spec needs no host tool at all — approve immediately and let
|
|
27
28
|
* the injected <system_reminder> carry the contract, exactly as Cursor CLI's
|
|
@@ -68,6 +69,7 @@ export const SWITCH_MODE_EXIT_HEADER = "Build Agent";
|
|
|
68
69
|
const SWITCH_MODE_EXIT_YES = "Yes";
|
|
69
70
|
const SWITCH_MODE_EXIT_NO = "No";
|
|
70
71
|
const activeCursorModeBySession = new Map();
|
|
72
|
+
const MAX_ACTIVE_CURSOR_MODES = 256;
|
|
71
73
|
function asRecord(value) {
|
|
72
74
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
73
75
|
? value
|
|
@@ -248,6 +250,12 @@ export function setActiveCursorMode(sessionKey, targetModeId, options = {}) {
|
|
|
248
250
|
firstTurn: true,
|
|
249
251
|
bridgedPlanEntered: options.bridgedPlanEntered === true,
|
|
250
252
|
});
|
|
253
|
+
while (activeCursorModeBySession.size > MAX_ACTIVE_CURSOR_MODES) {
|
|
254
|
+
const oldest = activeCursorModeBySession.keys().next().value;
|
|
255
|
+
if (!oldest)
|
|
256
|
+
break;
|
|
257
|
+
activeCursorModeBySession.delete(oldest);
|
|
258
|
+
}
|
|
251
259
|
}
|
|
252
260
|
export function getActiveCursorMode(sessionKey) {
|
|
253
261
|
if (!sessionKey)
|
|
@@ -19,10 +19,38 @@ export type BridgedOpenCodeToolCall = {
|
|
|
19
19
|
callId: string;
|
|
20
20
|
variant: string;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a full todo snapshot for `mirroredTodos` storage. Returns
|
|
24
|
+
* undefined when the input is not a todo array (nothing to learn). Drops the
|
|
25
|
+
* synthetic "plan" id minted for create_plan mirrors so later merges never
|
|
26
|
+
* resurrect it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function snapshotMirroredTodos(todos: unknown): Array<Record<string, unknown>> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Refresh the mirrored snapshot from a host `todoread` tool result. The host
|
|
31
|
+
* list is authoritative; returns undefined when the output is not recognizable
|
|
32
|
+
* todo JSON (prose, caps, errors) so a stale-but-useful snapshot survives.
|
|
33
|
+
*/
|
|
34
|
+
export declare function snapshotMirroredTodosFromReadOutput(output: string): Array<Record<string, unknown>> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Overlay Cursor merge patches onto a previously mirrored full todo list.
|
|
37
|
+
* Preserves prior order; appends newly introduced ids in patch order.
|
|
38
|
+
*
|
|
39
|
+
* Matching is by id first, then by exact (trimmed) content. The content
|
|
40
|
+
* fallback covers mixed flows where the prior snapshot came from a direct
|
|
41
|
+
* host `todowrite` (positional ids) while the patch references Cursor-side
|
|
42
|
+
* ids for the same tasks. Patch items with neither an id/content match nor
|
|
43
|
+
* their own content are skipped — a status-only update for an unknown item
|
|
44
|
+
* cannot be mapped onto the host list.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyTodoMerge(prior: ReadonlyArray<Record<string, unknown>>, patch: unknown): Array<Record<string, unknown>>;
|
|
22
47
|
/**
|
|
23
48
|
* Decode a Cursor ToolCall oneof into a display tool call we can bridge.
|
|
49
|
+
*
|
|
50
|
+
* @param priorMirroredTodos Last full todo snapshot mirrored this Run; used to
|
|
51
|
+
* expand Cursor `merge: true` updates that omit ToolCallCompleted.success.todos.
|
|
24
52
|
*/
|
|
25
|
-
export declare function parseDisplayToolCall(callId: string, toolCall: Record<string, unknown> | undefined): DisplayToolCall | undefined;
|
|
53
|
+
export declare function parseDisplayToolCall(callId: string, toolCall: Record<string, unknown> | undefined, priorMirroredTodos?: ReadonlyArray<Record<string, unknown>>): DisplayToolCall | undefined;
|
|
26
54
|
/**
|
|
27
55
|
* Pick an advertised OpenCode tool for a display call, remapping args as needed.
|
|
28
56
|
* Returns undefined when nothing compatible is advertised this turn.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName } from "./tools.js";
|
|
1
|
+
import { mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName, } from "./tools.js";
|
|
2
2
|
import { decodeStructEntriesToJson } from "./struct.js";
|
|
3
3
|
import { mapSwitchModeTarget } from "./switch-mode.js";
|
|
4
4
|
const TODO_STATUS = {
|
|
@@ -46,11 +46,13 @@ const VARIANT_TO_OPENCODE = {
|
|
|
46
46
|
const TOOL_CALL_VARIANTS = Object.keys(VARIANT_TO_OPENCODE);
|
|
47
47
|
// ToolCallCompleted is a notification, not an execution request. Only mirror
|
|
48
48
|
// client-visible state whose authoritative final value is already present in
|
|
49
|
-
// the completed payload
|
|
49
|
+
// the completed payload (or reconstructable from a prior mirrored snapshot for
|
|
50
|
+
// Cursor todo merges). Data-returning, interactive, and side-effecting calls
|
|
50
51
|
// must use an exec/interaction request channel where Cursor can receive their
|
|
51
52
|
// actual result.
|
|
52
53
|
const DISPLAY_STATE_MIRROR_VARIANTS = new Set([
|
|
53
54
|
"update_todos_tool_call",
|
|
55
|
+
"read_todos_tool_call",
|
|
54
56
|
"create_plan_tool_call",
|
|
55
57
|
]);
|
|
56
58
|
function asRecord(v) {
|
|
@@ -97,14 +99,169 @@ function mapTodos(raw) {
|
|
|
97
99
|
};
|
|
98
100
|
});
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Normalize a full todo snapshot for `mirroredTodos` storage. Returns
|
|
104
|
+
* undefined when the input is not a todo array (nothing to learn). Drops the
|
|
105
|
+
* synthetic "plan" id minted for create_plan mirrors so later merges never
|
|
106
|
+
* resurrect it.
|
|
107
|
+
*/
|
|
108
|
+
export function snapshotMirroredTodos(todos) {
|
|
109
|
+
if (!Array.isArray(todos))
|
|
110
|
+
return undefined;
|
|
111
|
+
return mapTodos(todos).filter((t) => {
|
|
112
|
+
const id = typeof t.id === "string" ? t.id : "";
|
|
113
|
+
return id.length > 0 && id !== "plan";
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Refresh the mirrored snapshot from a host `todoread` tool result. The host
|
|
118
|
+
* list is authoritative; returns undefined when the output is not recognizable
|
|
119
|
+
* todo JSON (prose, caps, errors) so a stale-but-useful snapshot survives.
|
|
120
|
+
*/
|
|
121
|
+
export function snapshotMirroredTodosFromReadOutput(output) {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(output);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const list = Array.isArray(parsed) ? parsed : (asRecord(parsed)?.todos ?? undefined);
|
|
130
|
+
if (!Array.isArray(list))
|
|
131
|
+
return undefined;
|
|
132
|
+
return snapshotMirroredTodos(list);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Overlay Cursor merge patches onto a previously mirrored full todo list.
|
|
136
|
+
* Preserves prior order; appends newly introduced ids in patch order.
|
|
137
|
+
*
|
|
138
|
+
* Matching is by id first, then by exact (trimmed) content. The content
|
|
139
|
+
* fallback covers mixed flows where the prior snapshot came from a direct
|
|
140
|
+
* host `todowrite` (positional ids) while the patch references Cursor-side
|
|
141
|
+
* ids for the same tasks. Patch items with neither an id/content match nor
|
|
142
|
+
* their own content are skipped — a status-only update for an unknown item
|
|
143
|
+
* cannot be mapped onto the host list.
|
|
144
|
+
*/
|
|
145
|
+
export function applyTodoMerge(prior, patch) {
|
|
146
|
+
const rawPatch = Array.isArray(patch) ? patch : [];
|
|
147
|
+
const mappedPatch = mapTodos(patch);
|
|
148
|
+
const byId = new Map();
|
|
149
|
+
const byContent = new Map();
|
|
150
|
+
// Patch ids already folded into a prior record via content match: never
|
|
151
|
+
// re-emit them as new items in the output phase.
|
|
152
|
+
const consumedPatchIds = new Set();
|
|
153
|
+
// Patch ids accepted as genuinely new content: the only patch ids the
|
|
154
|
+
// output phase may append. Orphans skipped above (unknown id, no content)
|
|
155
|
+
// must not leak back in here.
|
|
156
|
+
const appendedPatchIds = new Set();
|
|
157
|
+
for (const item of prior) {
|
|
158
|
+
const id = typeof item.id === "string" && item.id.length > 0 ? item.id : "";
|
|
159
|
+
if (!id)
|
|
160
|
+
continue;
|
|
161
|
+
const normalized = {
|
|
162
|
+
id,
|
|
163
|
+
content: typeof item.content === "string" ? item.content : "",
|
|
164
|
+
status: typeof item.status === "string" ? item.status : mapTodoStatus(item.status),
|
|
165
|
+
priority: typeof item.priority === "string" && item.priority ? item.priority : "medium",
|
|
166
|
+
};
|
|
167
|
+
byId.set(id, normalized);
|
|
168
|
+
const key = normalized.content.trim();
|
|
169
|
+
if (key && !byContent.has(key))
|
|
170
|
+
byContent.set(key, normalized);
|
|
171
|
+
}
|
|
172
|
+
const overlay = (target, item, raw) => {
|
|
173
|
+
// Only overwrite fields the patch actually carries. mapTodos fills
|
|
174
|
+
// defaults ("" content, "pending" status, "medium" priority) that must not
|
|
175
|
+
// clobber a prior item when Cursor sends an id+status-only patch.
|
|
176
|
+
const next = { ...target, id: target.id };
|
|
177
|
+
if (typeof raw.content === "string" && raw.content.length > 0) {
|
|
178
|
+
next.content = raw.content;
|
|
179
|
+
}
|
|
180
|
+
if (raw.status !== undefined && raw.status !== null) {
|
|
181
|
+
next.status = String(item.status);
|
|
182
|
+
}
|
|
183
|
+
if (typeof raw.priority === "string" && raw.priority) {
|
|
184
|
+
next.priority = raw.priority;
|
|
185
|
+
}
|
|
186
|
+
return next;
|
|
187
|
+
};
|
|
188
|
+
for (let index = 0; index < mappedPatch.length; index++) {
|
|
189
|
+
const item = mappedPatch[index];
|
|
190
|
+
const raw = asRecord(rawPatch[index]) ?? {};
|
|
191
|
+
const id = String(item.id);
|
|
192
|
+
const byIdMatch = byId.get(id);
|
|
193
|
+
if (byIdMatch) {
|
|
194
|
+
const merged = overlay(byIdMatch, item, raw);
|
|
195
|
+
byId.set(id, merged);
|
|
196
|
+
// Keep the content index pointing at the fresh record so later
|
|
197
|
+
// content-fallback matches never merge onto a stale copy.
|
|
198
|
+
const oldKey = typeof byIdMatch.content === "string" ? byIdMatch.content.trim() : "";
|
|
199
|
+
const newKey = typeof merged.content === "string" ? merged.content.trim() : "";
|
|
200
|
+
if (oldKey && byContent.get(oldKey) === byIdMatch)
|
|
201
|
+
byContent.delete(oldKey);
|
|
202
|
+
if (newKey && !byContent.has(newKey))
|
|
203
|
+
byContent.set(newKey, merged);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const contentKey = typeof raw.content === "string" && raw.content.trim().length > 0
|
|
207
|
+
? raw.content.trim()
|
|
208
|
+
: "";
|
|
209
|
+
const byContentMatch = contentKey ? byContent.get(contentKey) : undefined;
|
|
210
|
+
if (byContentMatch) {
|
|
211
|
+
// Same task, different id space (host positional ids vs Cursor ids).
|
|
212
|
+
// Keep the prior id so snapshot identity stays stable and order is
|
|
213
|
+
// preserved; adopt the patch's status/priority/content.
|
|
214
|
+
const merged = overlay(byContentMatch, item, raw);
|
|
215
|
+
byId.set(String(byContentMatch.id), merged);
|
|
216
|
+
if (contentKey)
|
|
217
|
+
byContent.set(contentKey, merged);
|
|
218
|
+
consumedPatchIds.add(id);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!contentKey)
|
|
222
|
+
continue;
|
|
223
|
+
byId.set(id, item);
|
|
224
|
+
byContent.set(contentKey, item);
|
|
225
|
+
appendedPatchIds.add(id);
|
|
226
|
+
}
|
|
227
|
+
const out = [];
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
for (const item of prior) {
|
|
230
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
231
|
+
if (!id || seen.has(id))
|
|
232
|
+
continue;
|
|
233
|
+
const next = byId.get(id);
|
|
234
|
+
if (next) {
|
|
235
|
+
out.push(next);
|
|
236
|
+
seen.add(id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const item of mappedPatch) {
|
|
240
|
+
const id = String(item.id);
|
|
241
|
+
// Id-matched items are already emitted under the prior id; content-matched
|
|
242
|
+
// items were folded into the prior record (not appended under the foreign
|
|
243
|
+
// patch id); orphans were skipped outright. Only genuinely new content
|
|
244
|
+
// reaches the host list.
|
|
245
|
+
if (seen.has(id) || consumedPatchIds.has(id))
|
|
246
|
+
continue;
|
|
247
|
+
if (!appendedPatchIds.has(id))
|
|
248
|
+
continue;
|
|
249
|
+
out.push(item);
|
|
250
|
+
seen.add(id);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
100
254
|
function unwrapArgs(variantPayload) {
|
|
101
255
|
const nested = asRecord(variantPayload.args);
|
|
102
256
|
return nested ?? variantPayload;
|
|
103
257
|
}
|
|
104
258
|
/**
|
|
105
259
|
* Decode a Cursor ToolCall oneof into a display tool call we can bridge.
|
|
260
|
+
*
|
|
261
|
+
* @param priorMirroredTodos Last full todo snapshot mirrored this Run; used to
|
|
262
|
+
* expand Cursor `merge: true` updates that omit ToolCallCompleted.success.todos.
|
|
106
263
|
*/
|
|
107
|
-
export function parseDisplayToolCall(callId, toolCall) {
|
|
264
|
+
export function parseDisplayToolCall(callId, toolCall, priorMirroredTodos) {
|
|
108
265
|
if (!toolCall || !callId)
|
|
109
266
|
return undefined;
|
|
110
267
|
const variant = findToolVariant(toolCall);
|
|
@@ -136,10 +293,16 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
136
293
|
const success = asRecord(result?.success);
|
|
137
294
|
const completedTodos = Array.isArray(success?.todos) ? success.todos : undefined;
|
|
138
295
|
const isMerge = variant === "update_todos_tool_call" && args.merge === true;
|
|
139
|
-
// OpenCode todowrite replaces the whole list.
|
|
140
|
-
//
|
|
296
|
+
// OpenCode todowrite replaces the whole list. Prefer the completed payload;
|
|
297
|
+
// otherwise a non-merge args list; otherwise merge against the Run snapshot.
|
|
141
298
|
const sourceTodos = completedTodos ?? (!isMerge ? args.todos : undefined);
|
|
142
|
-
const
|
|
299
|
+
const canMergeFromPrior = sourceTodos === undefined &&
|
|
300
|
+
isMerge &&
|
|
301
|
+
Array.isArray(priorMirroredTodos) &&
|
|
302
|
+
priorMirroredTodos.length > 0;
|
|
303
|
+
const todos = canMergeFromPrior
|
|
304
|
+
? applyTodoMerge(priorMirroredTodos, args.todos)
|
|
305
|
+
: mapTodos(sourceTodos);
|
|
143
306
|
if (variant === "create_plan_tool_call") {
|
|
144
307
|
const overview = typeof args.overview === "string" ? args.overview.trim() : "";
|
|
145
308
|
const plan = typeof args.plan === "string" ? args.plan.trim() : "";
|
|
@@ -158,7 +321,21 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
158
321
|
variant,
|
|
159
322
|
preferredToolName: "todowrite",
|
|
160
323
|
args: { todos },
|
|
161
|
-
bridgeable: variant === "create_plan_tool_call"
|
|
324
|
+
bridgeable: variant === "create_plan_tool_call"
|
|
325
|
+
? todos.length > 0
|
|
326
|
+
: Array.isArray(sourceTodos) || canMergeFromPrior,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// Cursor TodoRead is the same display-only channel as TodoWrite (agent.v1
|
|
330
|
+
// read_todos_tool_call). OpenCode todoread takes an empty body; host filters
|
|
331
|
+
// are not part of the canonical schema, so drop status_filter / id_filter.
|
|
332
|
+
if (variant === "read_todos_tool_call") {
|
|
333
|
+
return {
|
|
334
|
+
callId,
|
|
335
|
+
variant,
|
|
336
|
+
preferredToolName: "todoread",
|
|
337
|
+
args: {},
|
|
338
|
+
bridgeable: true,
|
|
162
339
|
};
|
|
163
340
|
}
|
|
164
341
|
if (variant === "edit_tool_call") {
|
|
@@ -295,6 +472,9 @@ export function resolveBridgedOpenCodeToolCall(display, advertised) {
|
|
|
295
472
|
todos: mapTodos(display.args.todos ?? mapped.args.todos),
|
|
296
473
|
};
|
|
297
474
|
}
|
|
475
|
+
if (toolName === "todoread") {
|
|
476
|
+
mapped.args = {};
|
|
477
|
+
}
|
|
298
478
|
return {
|
|
299
479
|
toolName: mapped.toolName,
|
|
300
480
|
args: mapped.args,
|
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -8,6 +8,25 @@ export type OpencodeToolDef = {
|
|
|
8
8
|
/** Original flattened identity when `name` is a Cursor-facing alias. */
|
|
9
9
|
sourceName?: string;
|
|
10
10
|
};
|
|
11
|
+
/** Host file-tool argument key. OpenCode 1.x uses `filePath`; 2.0 uses `path`. */
|
|
12
|
+
export type HostFilePathKey = "path" | "filePath";
|
|
13
|
+
/** Host shell tool id. OpenCode 1.x uses `bash`; 2.0 uses `shell`. */
|
|
14
|
+
export type HostShellTool = "bash" | "shell";
|
|
15
|
+
export type HostToolDialect = {
|
|
16
|
+
filePathKey: HostFilePathKey;
|
|
17
|
+
shellTool: HostShellTool;
|
|
18
|
+
};
|
|
19
|
+
export declare const OPENCODE_1_TOOL_DIALECT: HostToolDialect;
|
|
20
|
+
/** Prefer `filePath`, then `path` / `file_path` — Cursor and both OpenCode majors. */
|
|
21
|
+
export declare function opencodePathArg(args: Record<string, unknown> | undefined): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Infer the host tool dialect from advertised AI SDK schemas.
|
|
24
|
+
* OpenCode 2.0 read/edit/write require `path` and rename bash → `shell`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hostToolDialectFromTools(tools: readonly {
|
|
27
|
+
name?: string;
|
|
28
|
+
inputSchema?: unknown;
|
|
29
|
+
}[]): HostToolDialect;
|
|
11
30
|
export declare const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
|
|
12
31
|
export declare const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
|
|
13
32
|
export declare const CUSTOM_LIST_MCP_RESOURCES_TOOL = "custom_list_mcp_resources";
|
|
@@ -72,9 +91,9 @@ export type HostSubagentDefinition = {
|
|
|
72
91
|
description?: string;
|
|
73
92
|
};
|
|
74
93
|
export type HostSubagentCatalog = {
|
|
75
|
-
executor?: "task";
|
|
94
|
+
executor?: "task" | "subagent";
|
|
76
95
|
agents: HostSubagentDefinition[];
|
|
77
|
-
/** True when the OpenCode task tool supplied its complete, permission-filtered catalog. */
|
|
96
|
+
/** True when the OpenCode task/subagent tool supplied its complete, permission-filtered catalog. */
|
|
78
97
|
complete: boolean;
|
|
79
98
|
};
|
|
80
99
|
/** Extract the current host's permission-filtered Task/Actor recipient catalog. */
|
|
@@ -143,13 +162,13 @@ export type ParsedExecRequest = {
|
|
|
143
162
|
* this provider's warning text.
|
|
144
163
|
*/
|
|
145
164
|
export declare function rejectPartialReadMutation(parsed: ParsedExecRequest): void;
|
|
146
|
-
export declare function parseExecServerMessage(msg: Record<string, unknown
|
|
165
|
+
export declare function parseExecServerMessage(msg: Record<string, unknown>, dialect?: HostToolDialect): ParsedExecRequest | undefined;
|
|
147
166
|
/**
|
|
148
167
|
* Remap Cursor exec / MCP arg shapes onto OpenCode's Effect Schema keys.
|
|
149
168
|
* Without this, OpenCode rejects calls with InvalidArgumentsError
|
|
150
169
|
* (e.g. `path` instead of `filePath`, `file_text` instead of `content`).
|
|
151
170
|
*/
|
|
152
|
-
export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<string, unknown>, execVariant?: string): {
|
|
171
|
+
export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<string, unknown>, execVariant?: string, dialect?: HostToolDialect): {
|
|
153
172
|
toolName: string;
|
|
154
173
|
args: Record<string, unknown>;
|
|
155
174
|
binaryBytes?: Uint8Array;
|
|
@@ -174,6 +193,13 @@ export declare function binaryWritePayload(parsed: ParsedExecRequest): {
|
|
|
174
193
|
path: string;
|
|
175
194
|
data: Uint8Array;
|
|
176
195
|
} | undefined;
|
|
196
|
+
/**
|
|
197
|
+
* Replace the prompt-derived task title with Cursor's TaskToolCall description
|
|
198
|
+
* when the display call is correlated via tool_call_id. SubagentArgs itself has
|
|
199
|
+
* no description field, so without this OpenCode shows a cut-off first-five-words
|
|
200
|
+
* slice of the prompt.
|
|
201
|
+
*/
|
|
202
|
+
export declare function preferCorrelatedTaskDescription(parsed: ParsedExecRequest, description: string | undefined): void;
|
|
177
203
|
/**
|
|
178
204
|
* Map Cursor McpArgs back to the OpenCode tool id.
|
|
179
205
|
* Prefers provider_identifier + bare tool_name (github + create_pull_request
|