cursor-opencode-provider 0.8.0 → 0.8.3
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 +174 -0
- package/README.md +230 -44
- package/SECURITY.md +60 -12
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +23 -4
- package/dist/context/build.js +65 -10
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/frozen.js +6 -2
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +45 -18
- package/dist/context/paths.js +72 -22
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +122 -30
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +149 -30
- package/dist/language-model.js +3127 -673
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +12 -1
- package/dist/model-config.js +78 -30
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- 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/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- 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 +233 -65
- package/dist/plugin.js +97 -23
- package/dist/pricing-data.d.ts +40 -3
- package/dist/pricing-data.js +40 -3
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +164 -21
- package/dist/protocol/request.d.ts +40 -19
- package/dist/protocol/request.js +55 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- 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 +6 -0
- package/dist/protocol/tool-call-bridge.js +24 -0
- package/dist/protocol/tools.d.ts +90 -9
- package/dist/protocol/tools.js +543 -151
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +22 -4
- package/dist/replay-safety.js +86 -27
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +129 -10
- package/dist/session.js +198 -35
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +10 -1
- package/dist/transport/connect.js +124 -27
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +85 -34
- package/package.json +5 -3
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
|
@@ -6,10 +6,15 @@ export declare function hydrateConversationState(cacheDir: string, sessionKey: s
|
|
|
6
6
|
toolCatalog: OpencodeToolDef[];
|
|
7
7
|
hostAgent?: string;
|
|
8
8
|
systemPromptHash?: string;
|
|
9
|
+
hostNote?: string;
|
|
9
10
|
} | undefined>;
|
|
10
11
|
/** Restore only turn provenance when its in-memory entry was evicted. */
|
|
11
12
|
export declare function hydrateTurnProvenance(cacheDir: string, sessionKey: string): Promise<void>;
|
|
12
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Persist the resumable state at Cursor's TurnEnded, or before it
|
|
15
|
+
* (`runInProgress`) so a Run the host interrupts, or a restart, resumes from
|
|
16
|
+
* the latest checkpoint and still recognizes the step in progress.
|
|
17
|
+
*/
|
|
13
18
|
export declare function persistConversationState(cacheDir: string, input: {
|
|
14
19
|
sessionKey: string;
|
|
15
20
|
conversationId: string;
|
|
@@ -18,5 +23,7 @@ export declare function persistConversationState(cacheDir: string, input: {
|
|
|
18
23
|
postCompactionRebase?: boolean;
|
|
19
24
|
hostAgent?: string;
|
|
20
25
|
systemPromptHash?: string;
|
|
26
|
+
hostNote?: string;
|
|
27
|
+
runInProgress?: boolean;
|
|
21
28
|
}): Promise<void>;
|
|
22
29
|
export declare function clearPersistedConversationState(cacheDir: string, sessionKey: string, expectedConversationId?: string): Promise<void>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { trace } from "../debug.js";
|
|
2
|
+
import { restoreEarlySteers, serializeEarlySteers } from "../host-steer.js";
|
|
3
|
+
import { restoreLostRequest, serializeLostRequest } from "../lost-request.js";
|
|
2
4
|
import { getFrozenRequestContext, setFrozenRequestContext, } from "../context/frozen.js";
|
|
3
5
|
import { hasConversationBinding, isActiveConversationBinding, restoreConversationBinding, } from "./conversation-bind.js";
|
|
4
6
|
import { compactConversationBlobs, restoreConversationBlobs, } from "./blob-store.js";
|
|
5
7
|
import { getCheckpoint, setCheckpoint } from "./checkpoint.js";
|
|
6
8
|
import { getTurnProvenance, parseTurnProvenance, restoreTurnProvenance, serializeTurnProvenance, } from "./turn-provenance.js";
|
|
7
|
-
import {
|
|
9
|
+
import { heldHistoryImagesSnapshot, restoreHeldHistoryImages } from "./sent-history-images.js";
|
|
10
|
+
import { deletePersistedConversation, getPersistedTurnProvenance, loadPersistedConversation, persistConversation, } from "./conversation-persistence.js";
|
|
8
11
|
/** Restore one OpenCode session before its conversation binding is resolved. */
|
|
9
12
|
export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
10
13
|
if (hasConversationBinding(sessionKey))
|
|
@@ -17,6 +20,8 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
|
17
20
|
return undefined;
|
|
18
21
|
}
|
|
19
22
|
restoreConversationBinding(sessionKey, persisted.conversationId);
|
|
23
|
+
restoreEarlySteers(sessionKey, persisted.earlySteers);
|
|
24
|
+
restoreLostRequest(sessionKey, persisted.lostRequest);
|
|
20
25
|
if (persisted.checkpoint)
|
|
21
26
|
setCheckpoint(persisted.conversationId, persisted.checkpoint);
|
|
22
27
|
restoreConversationBlobs(persisted.conversationId, persisted.blobs);
|
|
@@ -24,29 +29,38 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
|
|
|
24
29
|
const provenance = persisted.turnProvenance ? parseTurnProvenance(persisted.turnProvenance) : undefined;
|
|
25
30
|
if (provenance?.conversationId === persisted.conversationId)
|
|
26
31
|
restoreTurnProvenance(sessionKey, provenance);
|
|
32
|
+
restoreHeldHistoryImages(sessionKey, persisted.conversationId, {
|
|
33
|
+
checkpoint: persisted.sentImagesCheckpoint,
|
|
34
|
+
hashes: persisted.sentImageHashes,
|
|
35
|
+
});
|
|
27
36
|
trace(`conversation persistence: restored sessionKey=${sessionKey} ` +
|
|
28
37
|
`conversationId=${persisted.conversationId} checkpoint=${persisted.checkpoint?.length ?? 0}B ` +
|
|
29
|
-
`blobs=${persisted.blobs.length}`);
|
|
38
|
+
`blobs=${persisted.blobs.length} heldImages=${persisted.sentImageHashes?.length ?? 0}`);
|
|
30
39
|
return {
|
|
31
40
|
conversationId: persisted.conversationId,
|
|
32
41
|
postCompactionRebase: persisted.postCompactionRebase,
|
|
33
42
|
toolCatalog: structuredClone(persisted.toolCatalog),
|
|
34
43
|
...(persisted.hostAgent ? { hostAgent: persisted.hostAgent } : {}),
|
|
35
44
|
...(persisted.systemPromptHash ? { systemPromptHash: persisted.systemPromptHash } : {}),
|
|
45
|
+
...(persisted.hostNote ? { hostNote: persisted.hostNote } : {}),
|
|
36
46
|
};
|
|
37
47
|
}
|
|
38
48
|
/** Restore only turn provenance when its in-memory entry was evicted. */
|
|
39
49
|
export async function hydrateTurnProvenance(cacheDir, sessionKey) {
|
|
40
50
|
if (getTurnProvenance(sessionKey))
|
|
41
51
|
return;
|
|
42
|
-
const persisted =
|
|
43
|
-
if (!persisted
|
|
52
|
+
const persisted = await getPersistedTurnProvenance(cacheDir, sessionKey);
|
|
53
|
+
if (!persisted)
|
|
44
54
|
return;
|
|
45
55
|
const provenance = parseTurnProvenance(persisted.turnProvenance);
|
|
46
56
|
if (provenance?.conversationId === persisted.conversationId)
|
|
47
57
|
restoreTurnProvenance(sessionKey, provenance);
|
|
48
58
|
}
|
|
49
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Persist the resumable state at Cursor's TurnEnded, or before it
|
|
61
|
+
* (`runInProgress`) so a Run the host interrupts, or a restart, resumes from
|
|
62
|
+
* the latest checkpoint and still recognizes the step in progress.
|
|
63
|
+
*/
|
|
50
64
|
export async function persistConversationState(cacheDir, input) {
|
|
51
65
|
// A newer Run may have reset/superseded this conversation while its final
|
|
52
66
|
// frame was still in flight. Never let that late TurnEnded resurrect it.
|
|
@@ -56,10 +70,13 @@ export async function persistConversationState(cacheDir, input) {
|
|
|
56
70
|
return;
|
|
57
71
|
}
|
|
58
72
|
const checkpoint = getCheckpoint(input.conversationId);
|
|
59
|
-
const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint
|
|
73
|
+
const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint, {
|
|
74
|
+
keepInMemory: input.runInProgress,
|
|
75
|
+
});
|
|
60
76
|
const blobs = blobCompaction.blobs;
|
|
61
77
|
const requestContext = getFrozenRequestContext(input.conversationId) ?? input.requestContext;
|
|
62
78
|
const provenance = getTurnProvenance(input.sessionKey);
|
|
79
|
+
const heldImages = heldHistoryImagesSnapshot(input.sessionKey, input.conversationId, checkpoint);
|
|
63
80
|
await persistConversation(cacheDir, {
|
|
64
81
|
sessionKey: input.sessionKey,
|
|
65
82
|
conversationId: input.conversationId,
|
|
@@ -70,11 +87,15 @@ export async function persistConversationState(cacheDir, input) {
|
|
|
70
87
|
postCompactionRebase: input.postCompactionRebase,
|
|
71
88
|
hostAgent: input.hostAgent,
|
|
72
89
|
systemPromptHash: input.systemPromptHash,
|
|
90
|
+
hostNote: input.hostNote,
|
|
91
|
+
earlySteers: serializeEarlySteers(input.sessionKey),
|
|
92
|
+
lostRequest: serializeLostRequest(input.sessionKey),
|
|
93
|
+
...(heldImages ? { sentImageHashes: heldImages.hashes, sentImagesCheckpoint: heldImages.checkpoint } : {}),
|
|
73
94
|
...(provenance?.conversationId === input.conversationId
|
|
74
95
|
? { turnProvenance: serializeTurnProvenance(provenance) }
|
|
75
96
|
: {}),
|
|
76
97
|
});
|
|
77
|
-
trace(`conversation persistence: saved sessionKey=${input.sessionKey} ` +
|
|
98
|
+
trace(`conversation persistence: saved${input.runInProgress ? " in-progress" : ""} sessionKey=${input.sessionKey} ` +
|
|
78
99
|
`conversationId=${input.conversationId} checkpoint=${checkpoint?.length ?? 0}B ` +
|
|
79
100
|
`blobs=${blobCompaction.beforeCount}->${blobCompaction.afterCount} ` +
|
|
80
101
|
`blobBytes=${blobCompaction.beforeBytes}->${blobCompaction.afterBytes}` +
|
|
@@ -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,9 +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";
|
|
24
23
|
import { errorMessage } from "../debug.js";
|
|
24
|
+
import { parseAnswerSegments, } from "./ask-question.js";
|
|
25
25
|
const PLAN_ADJECTIVES = [
|
|
26
26
|
"brave",
|
|
27
27
|
"calm",
|
|
@@ -90,6 +90,17 @@ const PLAN_NOUNS = [
|
|
|
90
90
|
export const CURSOR_PLAN_STAGE_TOOL = "cursor_plan_stage";
|
|
91
91
|
/** Held InteractionQuery continuation field for a native host plan stage. */
|
|
92
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.";
|
|
93
104
|
/** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */
|
|
94
105
|
export function createPlanApprovalQuestion(planLabel) {
|
|
95
106
|
const where = planLabel.trim();
|
|
@@ -100,28 +111,28 @@ export function createPlanApprovalQuestion(planLabel) {
|
|
|
100
111
|
export const CREATE_PLAN_APPROVAL_HEADER = "Build Agent";
|
|
101
112
|
const CREATE_PLAN_APPROVAL_YES = "Yes";
|
|
102
113
|
const CREATE_PLAN_APPROVAL_NO = "No";
|
|
103
|
-
/**
|
|
104
|
-
export const CREATE_PLAN_NOT_APPROVED_REASON = "The user did not approve executing this plan. Keep planning: refine the plan and "
|
|
105
|
-
+ "propose it again when it is ready.";
|
|
106
|
-
/**
|
|
107
|
-
* Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's
|
|
108
|
-
* own record of an approved Cursor plan/spec mode: the approval prompt guards
|
|
109
|
-
* the transition *out of* planning, so a CreatePlan raised outside plan mode is
|
|
110
|
-
* written and acknowledged without one.
|
|
111
|
-
*/
|
|
114
|
+
/** Resolve how this CreatePlan is satisfied (see {@link CreatePlanBridge}). */
|
|
112
115
|
export function resolveCreatePlanBridge(options) {
|
|
113
116
|
// A lifecycle turn (title generation, compaction) runs alongside the real one
|
|
114
117
|
// and must not write a second plan file or raise a second prompt.
|
|
115
118
|
if (!options.allowTools)
|
|
116
119
|
return { kind: "ack" };
|
|
120
|
+
if (options.hostPlanEntryPending) {
|
|
121
|
+
return { kind: "defer", reason: CREATE_PLAN_HOST_PLAN_PENDING_REASON };
|
|
122
|
+
}
|
|
117
123
|
if (options.canStage)
|
|
118
124
|
return { kind: "stage" };
|
|
119
125
|
const names = options.advertised instanceof Set ? options.advertised : new Set(options.advertised);
|
|
120
|
-
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"))
|
|
121
133
|
return { kind: "approve" };
|
|
122
134
|
return { kind: "ack" };
|
|
123
135
|
}
|
|
124
|
-
/** The synthetic AskQuestion item backing the emulated approval prompt. */
|
|
125
136
|
function createPlanApprovalItem(question) {
|
|
126
137
|
return {
|
|
127
138
|
id: "create_plan_approval",
|
|
@@ -156,16 +167,13 @@ export function createPlanApprovalQuestionInput(planLabel) {
|
|
|
156
167
|
}
|
|
157
168
|
/**
|
|
158
169
|
* True when the emulated approval prompt came back as an explicit "Yes".
|
|
159
|
-
* An unanswered, dismissed, or failed prompt keeps the model planning
|
|
160
|
-
* than silently starting execution. OpenCode prose is anchored on the exact
|
|
161
|
-
* prompt text. Every other result shape fails closed.
|
|
170
|
+
* An unanswered, dismissed, or failed prompt keeps the model planning.
|
|
162
171
|
*/
|
|
163
172
|
export function createPlanApproved(output, isError, question) {
|
|
164
173
|
if (isError)
|
|
165
174
|
return false;
|
|
166
175
|
const [segment] = parseAnswerSegments([createPlanApprovalItem(question)], output);
|
|
167
|
-
|
|
168
|
-
return answer === CREATE_PLAN_APPROVAL_YES.toLowerCase();
|
|
176
|
+
return (segment ?? "").trim().toLowerCase() === CREATE_PLAN_APPROVAL_YES.toLowerCase();
|
|
169
177
|
}
|
|
170
178
|
function asRecord(value) {
|
|
171
179
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -334,12 +342,14 @@ export function renderOpencodePlanMarkdown(args) {
|
|
|
334
342
|
* Write the plan under {@link hostPlansDir} and return a `file://` URI.
|
|
335
343
|
* Empty / missing content is the caller's responsibility (empty plan_uri ack).
|
|
336
344
|
*/
|
|
337
|
-
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) {
|
|
338
348
|
const markdown = renderOpencodePlanMarkdown(args);
|
|
339
349
|
if (!markdown.trim()) {
|
|
340
350
|
return { ok: false, error: "CreatePlan produced no plan content to write" };
|
|
341
351
|
}
|
|
342
|
-
const planPath = resolveHostPlanPath(workspaceRoot, args.name, created);
|
|
352
|
+
const planPath = target ?? resolveHostPlanPath(workspaceRoot, args.name, created);
|
|
343
353
|
try {
|
|
344
354
|
mkdirSync(path.dirname(planPath), { recursive: true });
|
|
345
355
|
writeFileSync(planPath, markdown, "utf-8");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host notes (OpenCode `<system-update>` and read-tool instructions) in the
|
|
3
|
+
* shapes Cursor's own clients give model-visible context:
|
|
4
|
+
* - mid-turn, plain text in a `user_context` injection, as Cursor SDK
|
|
5
|
+
* `Run.steer` sends it (no wrapper: wrapped text was refused live);
|
|
6
|
+
* - on a later user turn, a `<system_reminder>` block in the message text;
|
|
7
|
+
* - provider notices about one exec result, as postToolUse hook context.
|
|
8
|
+
*/
|
|
9
|
+
/** Cursor CLI rejects a hook additional_context longer than this. */
|
|
10
|
+
export declare const HOOK_ADDITIONAL_CONTEXT_MAX_CHARS = 10000;
|
|
11
|
+
/** Stable delivery order survives asynchronous acknowledgements and Run recovery. */
|
|
12
|
+
export type HostNoteBatch = {
|
|
13
|
+
notes: string[];
|
|
14
|
+
order: number;
|
|
15
|
+
};
|
|
16
|
+
export type HookAdditionalContext = {
|
|
17
|
+
hook_event_name: string;
|
|
18
|
+
content: string;
|
|
19
|
+
};
|
|
20
|
+
/** Plain note text for one mid-turn injection, in host order. */
|
|
21
|
+
export declare function hostNoteText(notes: readonly string[]): string;
|
|
22
|
+
/**
|
|
23
|
+
* One postToolUse hook context per note, trimmed like Cursor CLI's carrier.
|
|
24
|
+
* A note over the CLI limit is split rather than dropped, so no text is lost.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hostNoteHookContexts(notes: readonly string[]): HookAdditionalContext[];
|
|
27
|
+
/** One `<system_reminder>` block per note, for a user turn's text. */
|
|
28
|
+
export declare function wrapHostNoteForCursor(text: string): string;
|
|
29
|
+
export declare function wrapHostNotesForCursor(notes: readonly string[]): string[];
|
|
30
|
+
export declare function encodePersistedHostNotes(notes: readonly string[]): string | undefined;
|
|
31
|
+
export declare function decodePersistedHostNotes(blob: string | undefined): string[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host notes (OpenCode `<system-update>` and read-tool instructions) in the
|
|
3
|
+
* shapes Cursor's own clients give model-visible context:
|
|
4
|
+
* - mid-turn, plain text in a `user_context` injection, as Cursor SDK
|
|
5
|
+
* `Run.steer` sends it (no wrapper: wrapped text was refused live);
|
|
6
|
+
* - on a later user turn, a `<system_reminder>` block in the message text;
|
|
7
|
+
* - provider notices about one exec result, as postToolUse hook context.
|
|
8
|
+
*/
|
|
9
|
+
const SYSTEM_UPDATE_BLOCK = /<system-update>([\s\S]*?)<\/system-update>/g;
|
|
10
|
+
const SYSTEM_REMINDER_OPEN = "<system_reminder>";
|
|
11
|
+
const SYSTEM_REMINDER_CLOSE = "</system_reminder>";
|
|
12
|
+
/** Cursor CLI's hook event name for context attached to a tool result. */
|
|
13
|
+
const POST_TOOL_USE_HOOK_EVENT = "postToolUse";
|
|
14
|
+
/** Cursor CLI rejects a hook additional_context longer than this. */
|
|
15
|
+
export const HOOK_ADDITIONAL_CONTEXT_MAX_CHARS = 10_000;
|
|
16
|
+
/** Delimiter for ordered note bodies in the restart-snapshot `hostNote` string. */
|
|
17
|
+
const HOST_NOTE_PERSIST_SEP = "\n\u001e\n";
|
|
18
|
+
/** Plain note text for one mid-turn injection, in host order. */
|
|
19
|
+
export function hostNoteText(notes) {
|
|
20
|
+
return notes.map(hostNoteBody).filter(Boolean).join("\n\n");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One postToolUse hook context per note, trimmed like Cursor CLI's carrier.
|
|
24
|
+
* A note over the CLI limit is split rather than dropped, so no text is lost.
|
|
25
|
+
*/
|
|
26
|
+
export function hostNoteHookContexts(notes) {
|
|
27
|
+
return notes.flatMap((note) => {
|
|
28
|
+
const body = hostNoteBody(note);
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for (let start = 0; start < body.length;) {
|
|
31
|
+
let end = Math.min(body.length, start + HOOK_ADDITIONAL_CONTEXT_MAX_CHARS);
|
|
32
|
+
// Never split a surrogate pair.
|
|
33
|
+
if (end < body.length && /[\uD800-\uDBFF]/.test(body[end - 1]))
|
|
34
|
+
end--;
|
|
35
|
+
chunks.push(body.slice(start, end));
|
|
36
|
+
start = end;
|
|
37
|
+
}
|
|
38
|
+
return chunks.map((content) => ({ hook_event_name: POST_TOOL_USE_HOOK_EVENT, content }));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** One `<system_reminder>` block per note, for a user turn's text. */
|
|
42
|
+
export function wrapHostNoteForCursor(text) {
|
|
43
|
+
return `${SYSTEM_REMINDER_OPEN}\n${escapeReminderTags(hostNoteBody(text))}\n${SYSTEM_REMINDER_CLOSE}`;
|
|
44
|
+
}
|
|
45
|
+
export function wrapHostNotesForCursor(notes) {
|
|
46
|
+
return notes.filter((note) => hostNoteBody(note).length > 0).map(wrapHostNoteForCursor);
|
|
47
|
+
}
|
|
48
|
+
export function encodePersistedHostNotes(notes) {
|
|
49
|
+
const present = notes.filter((note) => note.length > 0);
|
|
50
|
+
if (present.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
return present.join(HOST_NOTE_PERSIST_SEP);
|
|
53
|
+
}
|
|
54
|
+
export function decodePersistedHostNotes(blob) {
|
|
55
|
+
if (!blob)
|
|
56
|
+
return [];
|
|
57
|
+
return blob.split(HOST_NOTE_PERSIST_SEP).filter((note) => note.length > 0);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Unwrap OpenCode `<system-update>` blocks. One host message may hold several
|
|
61
|
+
* (joined by `hostTailNote`); text that is not only such blocks stays as is.
|
|
62
|
+
*/
|
|
63
|
+
function hostNoteBody(text) {
|
|
64
|
+
const trimmed = text.trim();
|
|
65
|
+
const bodies = [];
|
|
66
|
+
let rest = trimmed;
|
|
67
|
+
for (const match of trimmed.matchAll(SYSTEM_UPDATE_BLOCK)) {
|
|
68
|
+
bodies.push(unescapeSystemUpdateText(match[1].trim()));
|
|
69
|
+
rest = rest.replace(match[0], "");
|
|
70
|
+
}
|
|
71
|
+
return bodies.length > 0 && rest.trim() === ""
|
|
72
|
+
? bodies.filter(Boolean).join("\n\n")
|
|
73
|
+
: trimmed;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Inverse of OpenCode's `escapeSystemUpdateText` (`packages/llm/src/protocols/
|
|
77
|
+
* shared.ts`): it escapes `&`, `<`, `>` inside the wrapper. `&` goes last.
|
|
78
|
+
*/
|
|
79
|
+
function unescapeSystemUpdateText(text) {
|
|
80
|
+
return text.replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
81
|
+
}
|
|
82
|
+
/** Same neutralization Cursor CLI applies to text placed inside its reminders. */
|
|
83
|
+
function escapeReminderTags(text) {
|
|
84
|
+
return text.replace(/system_reminder/gi, "system_reminder_");
|
|
85
|
+
}
|
|
@@ -14,6 +14,8 @@ export type HandledInteraction = {
|
|
|
14
14
|
variantField: number;
|
|
15
15
|
variantName: string;
|
|
16
16
|
outcome: "rejected" | "acknowledged" | "failed" | "bridged" | "approved";
|
|
17
|
+
/** Decoded model call id, including refusals and lifecycle acknowledgments. */
|
|
18
|
+
toolCallId?: string;
|
|
17
19
|
/**
|
|
18
20
|
* Immediate reply for this query. Absent for bridged *synchronous* AskQuestion
|
|
19
21
|
* or SwitchMode: Cursor blocks until the host tool returns, exactly as its own
|
|
@@ -31,24 +33,29 @@ export type HandledInteraction = {
|
|
|
31
33
|
*/
|
|
32
34
|
switchMode?: DecodedSwitchModeQuery & {
|
|
33
35
|
bridge: SwitchModeBridge;
|
|
34
|
-
toolName?: SwitchModeHostTool
|
|
36
|
+
toolName?: SwitchModeHostTool;
|
|
35
37
|
};
|
|
36
38
|
/**
|
|
37
|
-
* Present when CreatePlan needs a host tool:
|
|
38
|
-
* owns the write *and* the
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* Present when CreatePlan needs a host tool: a host plan-stage tool that
|
|
40
|
+
* owns the write *and* the review, the host's `plan_exit` review of the
|
|
41
|
+
* plan file the provider just wrote (`planUri` / `planPath` set), or the
|
|
42
|
+
* emulated `question` prompt after that write.
|
|
41
43
|
*/
|
|
42
44
|
createPlan?: DecodedCreatePlanQuery & {
|
|
43
45
|
bridge: CreatePlanBridge;
|
|
44
|
-
toolName: typeof CURSOR_PLAN_STAGE_TOOL | "question";
|
|
46
|
+
toolName: typeof CURSOR_PLAN_STAGE_TOOL | "plan_exit" | "question";
|
|
45
47
|
planUri?: string;
|
|
46
|
-
/** Filesystem path of the plan the provider just wrote
|
|
48
|
+
/** Filesystem path of the plan the provider just wrote. */
|
|
47
49
|
planPath?: string;
|
|
48
50
|
questionInput?: ReturnType<typeof createPlanApprovalQuestionInput>;
|
|
49
|
-
/** The plan to show the user before
|
|
51
|
+
/** The plan to show the user before the host review. */
|
|
50
52
|
planReview?: string;
|
|
51
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Cursor tool call id of a CreatePlan the host plan agent will record
|
|
56
|
+
* instead. Nothing was written, so its display must not be mirrored as a plan.
|
|
57
|
+
*/
|
|
58
|
+
deferredCreatePlanToolCallId?: string;
|
|
52
59
|
/** Present when an image generation was approved: the target to expect. */
|
|
53
60
|
generateImage?: DecodedGenerateImageQuery;
|
|
54
61
|
};
|
|
@@ -81,16 +88,18 @@ export type HandleInteractionQueryOptions = {
|
|
|
81
88
|
workspaceRoot?: string;
|
|
82
89
|
/** True when the advertised host plan-stage tool is available. */
|
|
83
90
|
canBridgeCreatePlan?: boolean;
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
/** An approved switch into the host `plan` agent waits for this Run to end. */
|
|
92
|
+
hostPlanEntryPending?: boolean;
|
|
93
|
+
/** Host primary agent this Run executes under, when the host reported it. */
|
|
94
|
+
hostAgent?: string;
|
|
95
|
+
/** Provider-recorded Cursor plan/spec mode for the CreatePlan execution gate. */
|
|
89
96
|
planModeActive?: boolean;
|
|
97
|
+
/** The host's own plan file for this session, when the host defines one. */
|
|
98
|
+
hostPlanFile?: string;
|
|
90
99
|
/**
|
|
91
100
|
* Cursor unified mode currently recorded for this session. A SwitchMode to
|
|
92
101
|
* the mode already in effect needs no approval, matching Cursor's own IDE
|
|
93
|
-
* handler and keeping the plan
|
|
102
|
+
* handler and keeping the host's plan review from running twice.
|
|
94
103
|
*/
|
|
95
104
|
activeCursorModeId?: string;
|
|
96
105
|
};
|
|
@@ -112,9 +121,9 @@ export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Arr
|
|
|
112
121
|
* Bridged / persisted exceptions:
|
|
113
122
|
* - AskQuestion (#3) → OpenCode `question` tool
|
|
114
123
|
* - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised
|
|
115
|
-
* - CreatePlan (#7) → host
|
|
116
|
-
*
|
|
117
|
-
*
|
|
124
|
+
* - CreatePlan (#7) → the host's plan file (session `Session.plan` file when
|
|
125
|
+
* known, else a new file under hostPlansDir); empty args still get the CLI
|
|
126
|
+
* empty-`plan_uri` success ack
|
|
118
127
|
* - GenerateImage (#12) → approve when `cursor_image_save` is advertised
|
|
119
128
|
*/
|
|
120
129
|
export declare function handleInteractionQuery(query: Record<string, unknown>, agentServerPayload: Uint8Array, options?: HandleInteractionQueryOptions): HandledInteraction;
|