cursor-opencode-provider 0.7.6 → 0.8.1
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/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/dist/pricing-data.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export declare const CURSOR_MODEL_COSTS: {
|
|
|
25
25
|
readonly cache_read: 0.1;
|
|
26
26
|
readonly cache_write: 1.25;
|
|
27
27
|
};
|
|
28
|
+
readonly "claude-haiku-5-5": {
|
|
29
|
+
readonly input: 0.1;
|
|
30
|
+
readonly output: 0.5;
|
|
31
|
+
readonly cache_read: 0.01;
|
|
32
|
+
readonly cache_write: 0.125;
|
|
33
|
+
};
|
|
28
34
|
readonly "claude-opus-4-5": {
|
|
29
35
|
readonly input: 5;
|
|
30
36
|
readonly output: 25;
|
|
@@ -94,7 +100,7 @@ export declare const CURSOR_MODEL_COSTS: {
|
|
|
94
100
|
readonly "claude-sonnet-5-5": {
|
|
95
101
|
readonly input: 2;
|
|
96
102
|
readonly output: 10;
|
|
97
|
-
readonly cache_read: 0.
|
|
103
|
+
readonly cache_read: 0.1;
|
|
98
104
|
readonly cache_write: 2.5;
|
|
99
105
|
};
|
|
100
106
|
readonly "composer-2.5": {
|
|
@@ -311,6 +317,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
|
|
|
311
317
|
readonly "claude-haiku-4-5": {
|
|
312
318
|
readonly maxContext: 200000;
|
|
313
319
|
};
|
|
320
|
+
readonly "claude-haiku-5-5": {
|
|
321
|
+
readonly maxContext: 300000;
|
|
322
|
+
readonly maxContextForMaxMode: 1000000;
|
|
323
|
+
};
|
|
314
324
|
readonly "claude-opus-4-5": {
|
|
315
325
|
readonly maxContext: 200000;
|
|
316
326
|
readonly maxContextForMaxMode: 200000;
|
|
@@ -465,6 +475,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
|
|
|
465
475
|
readonly "claude-haiku-4-5": {
|
|
466
476
|
readonly supportsImages: true;
|
|
467
477
|
};
|
|
478
|
+
readonly "claude-haiku-5-5": {
|
|
479
|
+
readonly supportsImages: true;
|
|
480
|
+
};
|
|
468
481
|
readonly "claude-opus-4-5": {
|
|
469
482
|
readonly supportsImages: true;
|
|
470
483
|
};
|
package/dist/pricing-data.js
CHANGED
|
@@ -25,6 +25,12 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
25
25
|
"cache_read": 0.1,
|
|
26
26
|
"cache_write": 1.25
|
|
27
27
|
},
|
|
28
|
+
"claude-haiku-5-5": {
|
|
29
|
+
"input": 0.1,
|
|
30
|
+
"output": 0.5,
|
|
31
|
+
"cache_read": 0.01,
|
|
32
|
+
"cache_write": 0.125
|
|
33
|
+
},
|
|
28
34
|
"claude-opus-4-5": {
|
|
29
35
|
"input": 5,
|
|
30
36
|
"output": 25,
|
|
@@ -94,7 +100,7 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
94
100
|
"claude-sonnet-5-5": {
|
|
95
101
|
"input": 2,
|
|
96
102
|
"output": 10,
|
|
97
|
-
"cache_read": 0.
|
|
103
|
+
"cache_read": 0.1,
|
|
98
104
|
"cache_write": 2.5
|
|
99
105
|
},
|
|
100
106
|
"composer-2.5": {
|
|
@@ -311,6 +317,10 @@ export const CURSOR_MODEL_CONTEXTS = {
|
|
|
311
317
|
"claude-haiku-4-5": {
|
|
312
318
|
"maxContext": 200000
|
|
313
319
|
},
|
|
320
|
+
"claude-haiku-5-5": {
|
|
321
|
+
"maxContext": 300000,
|
|
322
|
+
"maxContextForMaxMode": 1000000
|
|
323
|
+
},
|
|
314
324
|
"claude-opus-4-5": {
|
|
315
325
|
"maxContext": 200000,
|
|
316
326
|
"maxContextForMaxMode": 200000
|
|
@@ -465,6 +475,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
|
|
|
465
475
|
"claude-haiku-4-5": {
|
|
466
476
|
"supportsImages": true
|
|
467
477
|
},
|
|
478
|
+
"claude-haiku-5-5": {
|
|
479
|
+
"supportsImages": true
|
|
480
|
+
},
|
|
468
481
|
"claude-opus-4-5": {
|
|
469
482
|
"supportsImages": true
|
|
470
483
|
},
|
|
@@ -76,6 +76,13 @@ export declare function decodeAskQuestionQuery(queryBytes: Uint8Array): DecodedA
|
|
|
76
76
|
export declare function isCatchAllOptionLabel(label: string): boolean;
|
|
77
77
|
/** Options as Cursor would display them: trailing catch-all removed. */
|
|
78
78
|
export declare function displayOptions(question: CursorAskQuestionItem): CursorAskQuestionOption[];
|
|
79
|
+
/** OpenCode `Question.Prompt.header`: short label, max 30 chars. */
|
|
80
|
+
export declare function questionHeader(title: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Fill required OpenCode `question` fields when a model call omits them.
|
|
83
|
+
* Live OC1 failure: SchemaError Missing key at ["questions"][0]["header"].
|
|
84
|
+
*/
|
|
85
|
+
export declare function normalizeOpencodeQuestionArgs(args: Record<string, unknown>): Record<string, unknown>;
|
|
79
86
|
export type OpencodeQuestionInput = {
|
|
80
87
|
questions: Array<{
|
|
81
88
|
question: string;
|
|
@@ -100,12 +107,16 @@ export declare function asyncResult(): AskQuestionResultMessage;
|
|
|
100
107
|
/**
|
|
101
108
|
* Pull each question's answer text out of a host `question` tool output.
|
|
102
109
|
*
|
|
103
|
-
* OpenCode returns prose (`metadata.answers` does not cross the AI SDK
|
|
110
|
+
* OpenCode 1.x returns prose (`metadata.answers` does not cross the AI SDK
|
|
104
111
|
* boundary):
|
|
105
112
|
*
|
|
106
113
|
* User has answered your questions: "<q1>"="<a, b>", "<q2>"="Unanswered". You
|
|
107
114
|
* can now continue with the user's answers in mind.
|
|
108
115
|
*
|
|
116
|
+
* OpenCode 2.0 sends the tool's declared output as JSON
|
|
117
|
+
* (`{ "answers": [["Yes"], …] }`) and does not put that prose on the AI SDK
|
|
118
|
+
* tool-result. Answers are positional.
|
|
119
|
+
*
|
|
109
120
|
* The prose is located by its `"<question>"="` anchor — robust
|
|
110
121
|
* against commas, quotes and `"="` inside question or answer text, and against
|
|
111
122
|
* duplicate questions. Returns undefined for a question whose anchor is
|
|
@@ -183,7 +183,8 @@ export function displayOptions(question) {
|
|
|
183
183
|
return question.options.slice(0, -1);
|
|
184
184
|
return question.options.slice();
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
/** OpenCode `Question.Prompt.header`: short label, max 30 chars. */
|
|
187
|
+
export function questionHeader(title) {
|
|
187
188
|
const trimmed = title.trim();
|
|
188
189
|
if (!trimmed)
|
|
189
190
|
return "Question";
|
|
@@ -191,6 +192,40 @@ function header(title) {
|
|
|
191
192
|
? trimmed.slice(0, HEADER_MAX_LENGTH - 1).trimEnd() + "…"
|
|
192
193
|
: trimmed;
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Fill required OpenCode `question` fields when a model call omits them.
|
|
197
|
+
* Live OC1 failure: SchemaError Missing key at ["questions"][0]["header"].
|
|
198
|
+
*/
|
|
199
|
+
export function normalizeOpencodeQuestionArgs(args) {
|
|
200
|
+
if (!Array.isArray(args.questions))
|
|
201
|
+
return args;
|
|
202
|
+
return {
|
|
203
|
+
...args,
|
|
204
|
+
questions: args.questions.map((item) => {
|
|
205
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
206
|
+
return item;
|
|
207
|
+
const q = item;
|
|
208
|
+
const prompt = typeof q.question === "string" ? q.question : "";
|
|
209
|
+
const existing = typeof q.header === "string" ? q.header : "";
|
|
210
|
+
const options = Array.isArray(q.options)
|
|
211
|
+
? q.options.map((opt) => {
|
|
212
|
+
if (!opt || typeof opt !== "object" || Array.isArray(opt))
|
|
213
|
+
return opt;
|
|
214
|
+
const o = opt;
|
|
215
|
+
return {
|
|
216
|
+
...o,
|
|
217
|
+
...(o.description === undefined ? { description: "" } : {}),
|
|
218
|
+
};
|
|
219
|
+
})
|
|
220
|
+
: q.options;
|
|
221
|
+
return {
|
|
222
|
+
...q,
|
|
223
|
+
header: questionHeader(existing.trim() || prompt),
|
|
224
|
+
options,
|
|
225
|
+
};
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
194
229
|
/**
|
|
195
230
|
* Build the OpenCode `question` tool input. `Question.Prompt` requires
|
|
196
231
|
* `question`, `header`, and `options[{label, description}]`; Cursor options
|
|
@@ -200,7 +235,7 @@ export function askQuestionToolInput(args) {
|
|
|
200
235
|
return {
|
|
201
236
|
questions: args.questions.map((question) => ({
|
|
202
237
|
question: question.prompt,
|
|
203
|
-
header:
|
|
238
|
+
header: questionHeader(args.title),
|
|
204
239
|
options: displayOptions(question).map((option) => ({
|
|
205
240
|
label: option.label,
|
|
206
241
|
description: "",
|
|
@@ -220,18 +255,25 @@ export function asyncResult() {
|
|
|
220
255
|
/**
|
|
221
256
|
* Pull each question's answer text out of a host `question` tool output.
|
|
222
257
|
*
|
|
223
|
-
* OpenCode returns prose (`metadata.answers` does not cross the AI SDK
|
|
258
|
+
* OpenCode 1.x returns prose (`metadata.answers` does not cross the AI SDK
|
|
224
259
|
* boundary):
|
|
225
260
|
*
|
|
226
261
|
* User has answered your questions: "<q1>"="<a, b>", "<q2>"="Unanswered". You
|
|
227
262
|
* can now continue with the user's answers in mind.
|
|
228
263
|
*
|
|
264
|
+
* OpenCode 2.0 sends the tool's declared output as JSON
|
|
265
|
+
* (`{ "answers": [["Yes"], …] }`) and does not put that prose on the AI SDK
|
|
266
|
+
* tool-result. Answers are positional.
|
|
267
|
+
*
|
|
229
268
|
* The prose is located by its `"<question>"="` anchor — robust
|
|
230
269
|
* against commas, quotes and `"="` inside question or answer text, and against
|
|
231
270
|
* duplicate questions. Returns undefined for a question whose anchor is
|
|
232
271
|
* absent, which is treated as unanswered rather than guessed at.
|
|
233
272
|
*/
|
|
234
273
|
export function parseAnswerSegments(questions, output) {
|
|
274
|
+
const fromJson = parseJsonAnswerSegments(output, questions.length);
|
|
275
|
+
if (fromJson)
|
|
276
|
+
return fromJson;
|
|
235
277
|
const answers = [];
|
|
236
278
|
let cursor = 0;
|
|
237
279
|
for (const question of questions) {
|
|
@@ -260,6 +302,38 @@ export function parseAnswerSegments(questions, output) {
|
|
|
260
302
|
}
|
|
261
303
|
return answers;
|
|
262
304
|
}
|
|
305
|
+
function cellText(cell) {
|
|
306
|
+
if (typeof cell === "string")
|
|
307
|
+
return cell;
|
|
308
|
+
if (typeof cell === "number" || typeof cell === "boolean")
|
|
309
|
+
return String(cell);
|
|
310
|
+
if (Array.isArray(cell)) {
|
|
311
|
+
const parts = cell.map((item) => (typeof item === "string" ? item : "")).filter(Boolean);
|
|
312
|
+
return parts.length > 0 ? parts.join(", ") : undefined;
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
/** OpenCode 2 `question` output `{ answers: string[][] }`, or a JSON array of cells. */
|
|
317
|
+
function parseJsonAnswerSegments(output, count) {
|
|
318
|
+
const trimmed = output.trim();
|
|
319
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
320
|
+
return undefined;
|
|
321
|
+
let parsed;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(trimmed);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
const cells = Array.isArray(parsed)
|
|
329
|
+
? parsed
|
|
330
|
+
: parsed && typeof parsed === "object" && Array.isArray(parsed.answers)
|
|
331
|
+
? parsed.answers
|
|
332
|
+
: undefined;
|
|
333
|
+
if (!cells)
|
|
334
|
+
return undefined;
|
|
335
|
+
return Array.from({ length: count }, (_, index) => cellText(cells[index]));
|
|
336
|
+
}
|
|
263
337
|
/** OpenCode's placeholder for a question the user left blank. */
|
|
264
338
|
const UNANSWERED = "Unanswered";
|
|
265
339
|
/**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* At a successful TurnEnded it is compacted to the graph reachable from the
|
|
10
10
|
* latest checkpoint, matching Cursor CLI's conversation-export traversal.
|
|
11
11
|
*/
|
|
12
|
+
import { errorMessage } from "../debug.js";
|
|
12
13
|
import { collectReachableConversationBlobIds } from "./blob-reachability.js";
|
|
13
14
|
function hex(b) {
|
|
14
15
|
let s = "";
|
|
@@ -76,7 +77,7 @@ export function inspectConversationBlobGraph(conversationId, checkpoint) {
|
|
|
76
77
|
return { count, bytes, complete: true };
|
|
77
78
|
}
|
|
78
79
|
catch (error) {
|
|
79
|
-
return retainedStats(
|
|
80
|
+
return retainedStats(errorMessage(error));
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
/** Copy all durable blobs so a completed turn can be persisted atomically. */
|
|
@@ -131,7 +132,7 @@ export function compactConversationBlobs(conversationId, checkpoint) {
|
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
catch (error) {
|
|
134
|
-
return fallback(
|
|
135
|
+
return fallback(errorMessage(error));
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
/** Replace the in-memory blob bucket when restoring a persisted conversation. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { clearCheckpoint } from "./checkpoint.js";
|
|
3
3
|
import { clearConversationBlobs } from "./blob-store.js";
|
|
4
|
-
import { transferFrozenRequestContext
|
|
4
|
+
import { transferFrozenRequestContext } from "../context/frozen.js";
|
|
5
5
|
/**
|
|
6
6
|
* OpenCode session key → active Cursor conversation_id.
|
|
7
7
|
*
|
|
@@ -11,6 +11,8 @@ export type PersistedConversation = {
|
|
|
11
11
|
postCompactionRebase: boolean;
|
|
12
12
|
hostAgent?: string;
|
|
13
13
|
systemPromptHash?: string;
|
|
14
|
+
/** JSON-encoded TurnProvenance (what this provider emitted last). */
|
|
15
|
+
turnProvenance?: string;
|
|
14
16
|
};
|
|
15
17
|
export type ConversationLoadStatus = "restored" | "missing" | "invalid" | "expired";
|
|
16
18
|
export type PersistedConversationLoad = {
|
|
@@ -47,7 +47,8 @@ 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, host_agent=10, system_prompt_hash=11
|
|
50
|
+
* post_compaction_rebase=9, host_agent=10, system_prompt_hash=11,
|
|
51
|
+
* turn_provenance_json=12.
|
|
51
52
|
* Blob: id=1, data=2. Tool: name=1, description=2,
|
|
52
53
|
* input_schema_json=3, source_name=4.
|
|
53
54
|
*
|
|
@@ -176,6 +177,8 @@ function encodeCacheFile(value) {
|
|
|
176
177
|
writer.uint32(fieldTag(10, 2)).string(value.hostAgent);
|
|
177
178
|
if (value.systemPromptHash)
|
|
178
179
|
writer.uint32(fieldTag(11, 2)).string(value.systemPromptHash);
|
|
180
|
+
if (value.turnProvenance)
|
|
181
|
+
writer.uint32(fieldTag(12, 2)).string(value.turnProvenance);
|
|
179
182
|
return { protobufBytes: writer.finish(), requestContextBytes: requestContext.length };
|
|
180
183
|
}
|
|
181
184
|
function decodeProtobuf(data) {
|
|
@@ -191,6 +194,7 @@ function decodeProtobuf(data) {
|
|
|
191
194
|
let postCompactionRebase = false;
|
|
192
195
|
let hostAgent;
|
|
193
196
|
let systemPromptHash;
|
|
197
|
+
let turnProvenance;
|
|
194
198
|
while (reader.pos < reader.len) {
|
|
195
199
|
const tag = reader.uint32();
|
|
196
200
|
const wireType = tag & 7;
|
|
@@ -250,6 +254,11 @@ function decodeProtobuf(data) {
|
|
|
250
254
|
throw new Error("invalid system prompt hash");
|
|
251
255
|
systemPromptHash = reader.string();
|
|
252
256
|
break;
|
|
257
|
+
case 12:
|
|
258
|
+
if (wireType !== 2)
|
|
259
|
+
throw new Error("invalid turn provenance");
|
|
260
|
+
turnProvenance = reader.string();
|
|
261
|
+
break;
|
|
253
262
|
default:
|
|
254
263
|
reader.skipType(wireType);
|
|
255
264
|
}
|
|
@@ -270,6 +279,7 @@ function decodeProtobuf(data) {
|
|
|
270
279
|
postCompactionRebase,
|
|
271
280
|
...(hostAgent ? { hostAgent } : {}),
|
|
272
281
|
...(systemPromptHash ? { systemPromptHash } : {}),
|
|
282
|
+
...(turnProvenance ? { turnProvenance } : {}),
|
|
273
283
|
};
|
|
274
284
|
}
|
|
275
285
|
function decodeCacheFile(compressed, expectedSessionKey) {
|
|
@@ -7,6 +7,8 @@ export declare function hydrateConversationState(cacheDir: string, sessionKey: s
|
|
|
7
7
|
hostAgent?: string;
|
|
8
8
|
systemPromptHash?: string;
|
|
9
9
|
} | undefined>;
|
|
10
|
+
/** Restore only turn provenance when its in-memory entry was evicted. */
|
|
11
|
+
export declare function hydrateTurnProvenance(cacheDir: string, sessionKey: string): Promise<void>;
|
|
10
12
|
/** Persist the complete resumable state only after Cursor confirms TurnEnded. */
|
|
11
13
|
export declare function persistConversationState(cacheDir: string, input: {
|
|
12
14
|
sessionKey: string;
|
|
@@ -3,6 +3,7 @@ import { getFrozenRequestContext, setFrozenRequestContext, } from "../context/fr
|
|
|
3
3
|
import { hasConversationBinding, isActiveConversationBinding, restoreConversationBinding, } from "./conversation-bind.js";
|
|
4
4
|
import { compactConversationBlobs, restoreConversationBlobs, } from "./blob-store.js";
|
|
5
5
|
import { getCheckpoint, setCheckpoint } from "./checkpoint.js";
|
|
6
|
+
import { getTurnProvenance, parseTurnProvenance, restoreTurnProvenance, serializeTurnProvenance, } from "./turn-provenance.js";
|
|
6
7
|
import { deletePersistedConversation, loadPersistedConversation, persistConversation, } from "./conversation-persistence.js";
|
|
7
8
|
/** Restore one OpenCode session before its conversation binding is resolved. */
|
|
8
9
|
export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
@@ -20,6 +21,9 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
|
20
21
|
setCheckpoint(persisted.conversationId, persisted.checkpoint);
|
|
21
22
|
restoreConversationBlobs(persisted.conversationId, persisted.blobs);
|
|
22
23
|
setFrozenRequestContext(persisted.conversationId, persisted.requestContext);
|
|
24
|
+
const provenance = persisted.turnProvenance ? parseTurnProvenance(persisted.turnProvenance) : undefined;
|
|
25
|
+
if (provenance?.conversationId === persisted.conversationId)
|
|
26
|
+
restoreTurnProvenance(sessionKey, provenance);
|
|
23
27
|
trace(`conversation persistence: restored sessionKey=${sessionKey} ` +
|
|
24
28
|
`conversationId=${persisted.conversationId} checkpoint=${persisted.checkpoint?.length ?? 0}B ` +
|
|
25
29
|
`blobs=${persisted.blobs.length}`);
|
|
@@ -31,6 +35,17 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
|
31
35
|
...(persisted.systemPromptHash ? { systemPromptHash: persisted.systemPromptHash } : {}),
|
|
32
36
|
};
|
|
33
37
|
}
|
|
38
|
+
/** Restore only turn provenance when its in-memory entry was evicted. */
|
|
39
|
+
export async function hydrateTurnProvenance(cacheDir, sessionKey) {
|
|
40
|
+
if (getTurnProvenance(sessionKey))
|
|
41
|
+
return;
|
|
42
|
+
const persisted = (await loadPersistedConversation(cacheDir, sessionKey)).value;
|
|
43
|
+
if (!persisted?.turnProvenance)
|
|
44
|
+
return;
|
|
45
|
+
const provenance = parseTurnProvenance(persisted.turnProvenance);
|
|
46
|
+
if (provenance?.conversationId === persisted.conversationId)
|
|
47
|
+
restoreTurnProvenance(sessionKey, provenance);
|
|
48
|
+
}
|
|
34
49
|
/** Persist the complete resumable state only after Cursor confirms TurnEnded. */
|
|
35
50
|
export async function persistConversationState(cacheDir, input) {
|
|
36
51
|
// A newer Run may have reset/superseded this conversation while its final
|
|
@@ -44,6 +59,7 @@ export async function persistConversationState(cacheDir, input) {
|
|
|
44
59
|
const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint);
|
|
45
60
|
const blobs = blobCompaction.blobs;
|
|
46
61
|
const requestContext = getFrozenRequestContext(input.conversationId) ?? input.requestContext;
|
|
62
|
+
const provenance = getTurnProvenance(input.sessionKey);
|
|
47
63
|
await persistConversation(cacheDir, {
|
|
48
64
|
sessionKey: input.sessionKey,
|
|
49
65
|
conversationId: input.conversationId,
|
|
@@ -54,6 +70,9 @@ export async function persistConversationState(cacheDir, input) {
|
|
|
54
70
|
postCompactionRebase: input.postCompactionRebase,
|
|
55
71
|
hostAgent: input.hostAgent,
|
|
56
72
|
systemPromptHash: input.systemPromptHash,
|
|
73
|
+
...(provenance?.conversationId === input.conversationId
|
|
74
|
+
? { turnProvenance: serializeTurnProvenance(provenance) }
|
|
75
|
+
: {}),
|
|
57
76
|
});
|
|
58
77
|
trace(`conversation persistence: saved sessionKey=${input.sessionKey} ` +
|
|
59
78
|
`conversationId=${input.conversationId} checkpoint=${checkpoint?.length ?? 0}B ` +
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* does **not** — plans must stay host-portable so switching models does not
|
|
8
8
|
* strand users on Cursor-specific paths or frontmatter.
|
|
9
9
|
*
|
|
10
|
-
* Location
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Location is the host's own: the session plan file when the host defines one
|
|
11
|
+
* (`hostPlanFilePath`, OpenCode 1.x `Session.plan`), otherwise a new
|
|
12
|
+
* `<created>-<slug>.md` under {@link hostPlansDir} (OpenCode 2.0's Plan
|
|
13
|
+
* directory, OpenCode 1.x `<data>/plans`, or an installed path bridge).
|
|
14
14
|
*
|
|
15
15
|
* Body is plain markdown (the same shape a plan-mode model would write with
|
|
16
16
|
* `write`). Cursor YAML frontmatter is never emitted.
|
|
@@ -58,46 +58,66 @@ export type CursorPlanStageInput = {
|
|
|
58
58
|
* Its tool result is the approval outcome (success = approved,
|
|
59
59
|
* error = not approved). The result may name the host's own
|
|
60
60
|
* follow-up approval call. This provider does not invent that prompt.
|
|
61
|
-
* - `
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* - `
|
|
65
|
-
*
|
|
61
|
+
* - `ack` nothing to ask with (or a lifecycle turn / CreatePlan outside
|
|
62
|
+
* plan mode): write, or for a lifecycle turn skip the write, and
|
|
63
|
+
* acknowledge the CLI way.
|
|
64
|
+
* - `approve` plan mode is active and the host advertises `question` but not
|
|
65
|
+
* a plan review tool: write the plan, then ask with upstream
|
|
66
|
+
* `PlanExitTool`'s own prompt (OpenCode 1.x / 2.0 without
|
|
67
|
+
* `plan_exit`). Explicit "Yes" is execution approval.
|
|
68
|
+
* - `exit` the host's `plan` agent is active, advertises `plan_exit`, and
|
|
69
|
+
* its own plan file is known: write the plan there and run the
|
|
70
|
+
* host `plan_exit` review. The host owns approval and execution;
|
|
71
|
+
* the turn that carries the result leaves `plan` only on approval.
|
|
72
|
+
* - `defer` the host's `plan` agent owns planning but CreatePlan cannot be
|
|
73
|
+
* carried out yet: an approved switch into it is still pending (it
|
|
74
|
+
* applies after this Run), or its plan file is unknown. Nothing is
|
|
75
|
+
* written; the error tells the model which host workflow follows.
|
|
66
76
|
*
|
|
67
|
-
* The plan is always persisted before any
|
|
68
|
-
* executing does.
|
|
77
|
+
* The plan is always persisted before any review: writing needs no approval,
|
|
78
|
+
* executing does, and only the host asks for it.
|
|
69
79
|
*/
|
|
70
80
|
export type CreatePlanBridge = {
|
|
71
81
|
kind: "stage";
|
|
82
|
+
} | {
|
|
83
|
+
kind: "ack";
|
|
72
84
|
} | {
|
|
73
85
|
kind: "approve";
|
|
74
86
|
} | {
|
|
75
|
-
kind: "
|
|
87
|
+
kind: "defer";
|
|
88
|
+
reason: string;
|
|
89
|
+
} | {
|
|
90
|
+
kind: "exit";
|
|
91
|
+
planPath: string;
|
|
76
92
|
};
|
|
93
|
+
/** CreatePlan raised before an approved switch into the host plan agent took effect. */
|
|
94
|
+
export declare const CREATE_PLAN_HOST_PLAN_PENDING_REASON: string;
|
|
95
|
+
/** CreatePlan raised while the host's own plan agent owns the plan file and its review. */
|
|
96
|
+
export declare const CREATE_PLAN_HOST_PLAN_WORKFLOW_REASON: string;
|
|
97
|
+
/** Cursor-visible reason when the user wants the plan revised instead of run. */
|
|
98
|
+
export declare const CREATE_PLAN_NOT_APPROVED_REASON: string;
|
|
77
99
|
/** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */
|
|
78
100
|
export declare function createPlanApprovalQuestion(planLabel: string): string;
|
|
79
101
|
export declare const CREATE_PLAN_APPROVAL_HEADER = "Build Agent";
|
|
80
|
-
/**
|
|
81
|
-
export declare const CREATE_PLAN_NOT_APPROVED_REASON: string;
|
|
82
|
-
/**
|
|
83
|
-
* Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's
|
|
84
|
-
* own record of an approved Cursor plan/spec mode: the approval prompt guards
|
|
85
|
-
* the transition *out of* planning, so a CreatePlan raised outside plan mode is
|
|
86
|
-
* written and acknowledged without one.
|
|
87
|
-
*/
|
|
102
|
+
/** Resolve how this CreatePlan is satisfied (see {@link CreatePlanBridge}). */
|
|
88
103
|
export declare function resolveCreatePlanBridge(options: {
|
|
89
104
|
allowTools: boolean;
|
|
90
105
|
canStage?: boolean;
|
|
91
|
-
planModeActive?: boolean;
|
|
92
106
|
advertised: ReadonlySet<string> | Iterable<string>;
|
|
107
|
+
/** An approved switch into the host `plan` agent waits for this Run to end. */
|
|
108
|
+
hostPlanEntryPending?: boolean;
|
|
109
|
+
/** Host primary agent of this Run, when the host reported it. */
|
|
110
|
+
hostAgent?: string;
|
|
111
|
+
/** Provider-recorded Cursor plan/spec mode (the gate out of planning). */
|
|
112
|
+
planModeActive?: boolean;
|
|
113
|
+
/** The host's own plan file for this session, when the host defines one. */
|
|
114
|
+
hostPlanFile?: string;
|
|
93
115
|
}): CreatePlanBridge;
|
|
94
116
|
/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
|
|
95
117
|
export declare function createPlanApprovalQuestionInput(planLabel: string): OpencodeQuestionInput;
|
|
96
118
|
/**
|
|
97
119
|
* True when the emulated approval prompt came back as an explicit "Yes".
|
|
98
|
-
* An unanswered, dismissed, or failed prompt keeps the model planning
|
|
99
|
-
* than silently starting execution. OpenCode prose is anchored on the exact
|
|
100
|
-
* prompt text. Every other result shape fails closed.
|
|
120
|
+
* An unanswered, dismissed, or failed prompt keeps the model planning.
|
|
101
121
|
*/
|
|
102
122
|
export declare function createPlanApproved(output: string, isError: boolean, question: string): boolean;
|
|
103
123
|
/** Decode a `create_plan_request_query` body, or undefined when unusable. */
|
|
@@ -125,7 +145,9 @@ export declare function renderOpencodePlanMarkdown(args: CursorCreatePlanArgs):
|
|
|
125
145
|
* Write the plan under {@link hostPlansDir} and return a `file://` URI.
|
|
126
146
|
* Empty / missing content is the caller's responsibility (empty plan_uri ack).
|
|
127
147
|
*/
|
|
128
|
-
export declare function writeOpencodePlanFile(args: CursorCreatePlanArgs, workspaceRoot: string, created?: number
|
|
148
|
+
export declare function writeOpencodePlanFile(args: CursorCreatePlanArgs, workspaceRoot: string, created?: number,
|
|
149
|
+
/** Exact host-owned plan file; default is a new file under {@link hostPlansDir}. */
|
|
150
|
+
target?: string): CreatePlanWriteResult;
|
|
129
151
|
/**
|
|
130
152
|
* The plan as the user reads it before approving execution.
|
|
131
153
|
*
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* does **not** — plans must stay host-portable so switching models does not
|
|
8
8
|
* strand users on Cursor-specific paths or frontmatter.
|
|
9
9
|
*
|
|
10
|
-
* Location
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Location is the host's own: the session plan file when the host defines one
|
|
11
|
+
* (`hostPlanFilePath`, OpenCode 1.x `Session.plan`), otherwise a new
|
|
12
|
+
* `<created>-<slug>.md` under {@link hostPlansDir} (OpenCode 2.0's Plan
|
|
13
|
+
* directory, OpenCode 1.x `<data>/plans`, or an installed path bridge).
|
|
14
14
|
*
|
|
15
15
|
* Body is plain markdown (the same shape a plan-mode model would write with
|
|
16
16
|
* `write`). Cursor YAML frontmatter is never emitted.
|
|
@@ -19,8 +19,9 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import { hostPlansDir } from "../context/paths.js";
|
|
22
|
-
import { parseAnswerSegments, } from "./ask-question.js";
|
|
23
22
|
import { decodeMessageSparse } from "./messages.js";
|
|
23
|
+
import { errorMessage } from "../debug.js";
|
|
24
|
+
import { parseAnswerSegments, } from "./ask-question.js";
|
|
24
25
|
const PLAN_ADJECTIVES = [
|
|
25
26
|
"brave",
|
|
26
27
|
"calm",
|
|
@@ -89,6 +90,17 @@ const PLAN_NOUNS = [
|
|
|
89
90
|
export const CURSOR_PLAN_STAGE_TOOL = "cursor_plan_stage";
|
|
90
91
|
/** Held InteractionQuery continuation field for a native host plan stage. */
|
|
91
92
|
export const CREATE_PLAN_RESULT_FIELD = "create_plan_request_response";
|
|
93
|
+
/** CreatePlan raised before an approved switch into the host plan agent took effect. */
|
|
94
|
+
export const CREATE_PLAN_HOST_PLAN_PENDING_REASON = "Plan mode starts in the host's plan agent after this turn. Do not record the plan " +
|
|
95
|
+
"now and make no further tool calls: end this turn. Planning continues in the next " +
|
|
96
|
+
"turn under the host's plan-mode instructions.";
|
|
97
|
+
/** CreatePlan raised while the host's own plan agent owns the plan file and its review. */
|
|
98
|
+
export const CREATE_PLAN_HOST_PLAN_WORKFLOW_REASON = "This host's plan agent owns the plan file and its approval. Write the plan where the " +
|
|
99
|
+
"plan-mode instructions say, then call `plan_exit` to ask the user to approve it. Do " +
|
|
100
|
+
"not implement until it is approved.";
|
|
101
|
+
/** Cursor-visible reason when the user wants the plan revised instead of run. */
|
|
102
|
+
export const CREATE_PLAN_NOT_APPROVED_REASON = "The user did not approve executing this plan. Keep planning: refine the plan and "
|
|
103
|
+
+ "propose it again when it is ready.";
|
|
92
104
|
/** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */
|
|
93
105
|
export function createPlanApprovalQuestion(planLabel) {
|
|
94
106
|
const where = planLabel.trim();
|
|
@@ -99,28 +111,28 @@ export function createPlanApprovalQuestion(planLabel) {
|
|
|
99
111
|
export const CREATE_PLAN_APPROVAL_HEADER = "Build Agent";
|
|
100
112
|
const CREATE_PLAN_APPROVAL_YES = "Yes";
|
|
101
113
|
const CREATE_PLAN_APPROVAL_NO = "No";
|
|
102
|
-
/**
|
|
103
|
-
export const CREATE_PLAN_NOT_APPROVED_REASON = "The user did not approve executing this plan. Keep planning: refine the plan and "
|
|
104
|
-
+ "propose it again when it is ready.";
|
|
105
|
-
/**
|
|
106
|
-
* Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's
|
|
107
|
-
* own record of an approved Cursor plan/spec mode: the approval prompt guards
|
|
108
|
-
* the transition *out of* planning, so a CreatePlan raised outside plan mode is
|
|
109
|
-
* written and acknowledged without one.
|
|
110
|
-
*/
|
|
114
|
+
/** Resolve how this CreatePlan is satisfied (see {@link CreatePlanBridge}). */
|
|
111
115
|
export function resolveCreatePlanBridge(options) {
|
|
112
116
|
// A lifecycle turn (title generation, compaction) runs alongside the real one
|
|
113
117
|
// and must not write a second plan file or raise a second prompt.
|
|
114
118
|
if (!options.allowTools)
|
|
115
119
|
return { kind: "ack" };
|
|
120
|
+
if (options.hostPlanEntryPending) {
|
|
121
|
+
return { kind: "defer", reason: CREATE_PLAN_HOST_PLAN_PENDING_REASON };
|
|
122
|
+
}
|
|
116
123
|
if (options.canStage)
|
|
117
124
|
return { kind: "stage" };
|
|
118
125
|
const names = options.advertised instanceof Set ? options.advertised : new Set(options.advertised);
|
|
119
|
-
if (options.
|
|
126
|
+
if (options.hostAgent === "plan" && names.has("plan_exit")) {
|
|
127
|
+
return options.hostPlanFile
|
|
128
|
+
? { kind: "exit", planPath: options.hostPlanFile }
|
|
129
|
+
: { kind: "defer", reason: CREATE_PLAN_HOST_PLAN_WORKFLOW_REASON };
|
|
130
|
+
}
|
|
131
|
+
const planModeActive = options.planModeActive === true || options.hostAgent === "plan";
|
|
132
|
+
if (planModeActive && names.has("question"))
|
|
120
133
|
return { kind: "approve" };
|
|
121
134
|
return { kind: "ack" };
|
|
122
135
|
}
|
|
123
|
-
/** The synthetic AskQuestion item backing the emulated approval prompt. */
|
|
124
136
|
function createPlanApprovalItem(question) {
|
|
125
137
|
return {
|
|
126
138
|
id: "create_plan_approval",
|
|
@@ -155,16 +167,13 @@ export function createPlanApprovalQuestionInput(planLabel) {
|
|
|
155
167
|
}
|
|
156
168
|
/**
|
|
157
169
|
* True when the emulated approval prompt came back as an explicit "Yes".
|
|
158
|
-
* An unanswered, dismissed, or failed prompt keeps the model planning
|
|
159
|
-
* than silently starting execution. OpenCode prose is anchored on the exact
|
|
160
|
-
* prompt text. Every other result shape fails closed.
|
|
170
|
+
* An unanswered, dismissed, or failed prompt keeps the model planning.
|
|
161
171
|
*/
|
|
162
172
|
export function createPlanApproved(output, isError, question) {
|
|
163
173
|
if (isError)
|
|
164
174
|
return false;
|
|
165
175
|
const [segment] = parseAnswerSegments([createPlanApprovalItem(question)], output);
|
|
166
|
-
|
|
167
|
-
return answer === CREATE_PLAN_APPROVAL_YES.toLowerCase();
|
|
176
|
+
return (segment ?? "").trim().toLowerCase() === CREATE_PLAN_APPROVAL_YES.toLowerCase();
|
|
168
177
|
}
|
|
169
178
|
function asRecord(value) {
|
|
170
179
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -333,18 +342,20 @@ export function renderOpencodePlanMarkdown(args) {
|
|
|
333
342
|
* Write the plan under {@link hostPlansDir} and return a `file://` URI.
|
|
334
343
|
* Empty / missing content is the caller's responsibility (empty plan_uri ack).
|
|
335
344
|
*/
|
|
336
|
-
export function writeOpencodePlanFile(args, workspaceRoot, created = Date.now()
|
|
345
|
+
export function writeOpencodePlanFile(args, workspaceRoot, created = Date.now(),
|
|
346
|
+
/** Exact host-owned plan file; default is a new file under {@link hostPlansDir}. */
|
|
347
|
+
target) {
|
|
337
348
|
const markdown = renderOpencodePlanMarkdown(args);
|
|
338
349
|
if (!markdown.trim()) {
|
|
339
350
|
return { ok: false, error: "CreatePlan produced no plan content to write" };
|
|
340
351
|
}
|
|
341
|
-
const planPath = resolveHostPlanPath(workspaceRoot, args.name, created);
|
|
352
|
+
const planPath = target ?? resolveHostPlanPath(workspaceRoot, args.name, created);
|
|
342
353
|
try {
|
|
343
354
|
mkdirSync(path.dirname(planPath), { recursive: true });
|
|
344
355
|
writeFileSync(planPath, markdown, "utf-8");
|
|
345
356
|
}
|
|
346
357
|
catch (err) {
|
|
347
|
-
const message =
|
|
358
|
+
const message = errorMessage(err);
|
|
348
359
|
return { ok: false, error: `Failed to write plan file: ${message}` };
|
|
349
360
|
}
|
|
350
361
|
return {
|