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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type HostSteer = {
|
|
2
|
+
sessionID: string;
|
|
3
|
+
inboxID: string;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
export type EarlySteer = HostSteer & {
|
|
7
|
+
injectionId: string;
|
|
8
|
+
/** The Cursor Run (`CursorSession.sessionId`) it was injected into, or the Run resumed from its checkpoint. */
|
|
9
|
+
run: string;
|
|
10
|
+
delivered: boolean;
|
|
11
|
+
/** A checkpoint arrived after Cursor delivered it, so a Run resumed from that checkpoint holds it. */
|
|
12
|
+
checkpointed: boolean;
|
|
13
|
+
/** Its Run reached `turn_ended` after delivering it, so the model has answered it. */
|
|
14
|
+
answered: boolean;
|
|
15
|
+
/** OpenCode's own copy came after the step's tool results and was left out of the continuation. */
|
|
16
|
+
promoted?: boolean;
|
|
17
|
+
/** Prompt boundary at which the host supplied its copy; distinct later replies do not match it. */
|
|
18
|
+
hostTurn?: string;
|
|
19
|
+
};
|
|
20
|
+
/** undefined when another plugin copy already announced the steer. */
|
|
21
|
+
export declare function announceHostSteer(steer: HostSteer): boolean | undefined;
|
|
22
|
+
/** Remember a queued message, so a later switch to `steer` can announce it. */
|
|
23
|
+
export declare function rememberQueuedSteer(steer: HostSteer): void;
|
|
24
|
+
export declare function takeQueuedSteer(inboxID: string): HostSteer | undefined;
|
|
25
|
+
export declare function listenForHostSteers(sessionID: string, listener: (steer: HostSteer) => boolean): () => void;
|
|
26
|
+
/** false when there is no room to track it; OpenCode's own copy then runs as usual. */
|
|
27
|
+
export declare function recordEarlySteer(steer: EarlySteer): boolean;
|
|
28
|
+
export declare function isEarlySteerTracked(sessionID: string, injectionId: string): boolean;
|
|
29
|
+
/** An injection Cursor never accepted. A promoted one stays, to be sent at `turn_ended`. */
|
|
30
|
+
export declare function forgetUndeliveredEarlySteer(sessionID: string, injectionId: string): void;
|
|
31
|
+
/** The host withdrew the message, so no copy of it will come. */
|
|
32
|
+
export declare function forgetEarlySteerOfInbox(sessionID: string, inboxID: string): void;
|
|
33
|
+
/** true when the injection is an early steer. */
|
|
34
|
+
export declare function markEarlySteerDelivered(sessionID: string | undefined, injectionId: string | undefined): boolean;
|
|
35
|
+
export declare function markEarlySteersCheckpointed(sessionID: string | undefined, run: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* At `turn_ended`: a delivered steer is answered and waits for OpenCode's copy; one Cursor never took is
|
|
38
|
+
* forgotten, so OpenCode's copy runs as an ordinary message. A promoted one has no copy left to come:
|
|
39
|
+
* one Cursor never took is returned for a follow-up Run and stays, as sent, until a checkpoint holds it.
|
|
40
|
+
* Answered receipts survive retries and are retired when a later Run finishes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function settleEarlySteers(sessionID: string | undefined, run: string): {
|
|
43
|
+
answered: number;
|
|
44
|
+
undelivered: EarlySteer[];
|
|
45
|
+
leftToHost: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A failed Run's steers: those its checkpoint holds go to the Run resumed from it (`to`), and so does a
|
|
49
|
+
* promoted one it does not hold, to be sent at that Run's `turn_ended`. The rest are forgotten, so
|
|
50
|
+
* OpenCode's own copy of each runs as an ordinary message. A rebase (`to` undefined) seeds from host
|
|
51
|
+
* history, which already holds a promoted copy.
|
|
52
|
+
*/
|
|
53
|
+
export declare function handOverEarlySteers(sessionID: string | undefined, from: string, to: string | undefined): void;
|
|
54
|
+
/** The host may add `<system-reminder>` parts to a promoted copy; any other difference is another message. */
|
|
55
|
+
export declare function carriesSteer(message: string, text: string): boolean;
|
|
56
|
+
/** The record each message carries, or nothing unless every message carries one. */
|
|
57
|
+
export declare function matchEarlySteers(sessionID: string | undefined, messages: readonly string[], accept: (record: EarlySteer) => boolean): EarlySteer[];
|
|
58
|
+
export declare function forgetEarlySteerRecords(sessionID: string, records: readonly EarlySteer[]): void;
|
|
59
|
+
/**
|
|
60
|
+
* A steer of a Run that ended without answering it (a stop, a failed recovery) that only host history
|
|
61
|
+
* holds reliably: OpenCode placed it in its history (a checkpoint may hold it too, and resuming would
|
|
62
|
+
* then send it twice), or a checkpoint holds it while OpenCode's copy is still to come.
|
|
63
|
+
*/
|
|
64
|
+
export declare function needsHostHistory(sessionID: string | undefined, openRun: string | undefined): boolean;
|
|
65
|
+
/** Keep answered receipts until the new turn succeeds, so a host retry still omits its copies. */
|
|
66
|
+
export declare function clearUnansweredEarlySteers(sessionID: string | undefined): void;
|
|
67
|
+
/** Private restart metadata, saved atomically with the checkpoint it describes. */
|
|
68
|
+
export declare function serializeEarlySteers(sessionID: string): string | undefined;
|
|
69
|
+
export declare function restoreEarlySteers(sessionID: string, encoded: string | undefined): void;
|
|
70
|
+
export declare function forgetEarlySteers(sessionID: string): void;
|
|
71
|
+
export declare function resetHostSteersForTests(): void;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { processShared } from "./process-shared.js";
|
|
2
|
+
const MAX_RECORDS_PER_SESSION = 32;
|
|
3
|
+
const MAX_REMEMBERED = 256;
|
|
4
|
+
// The plugin and the model can be separate copies of this module, and each plugin copy sees every event.
|
|
5
|
+
const state = processShared("host-steers.v1", () => ({
|
|
6
|
+
listeners: new Map(),
|
|
7
|
+
injected: new Map(),
|
|
8
|
+
queued: new Map(),
|
|
9
|
+
announced: new Set(),
|
|
10
|
+
}));
|
|
11
|
+
function remember(entries, key) {
|
|
12
|
+
while (entries.size > MAX_REMEMBERED) {
|
|
13
|
+
const oldest = entries.keys().next().value;
|
|
14
|
+
if (oldest === undefined || oldest === key)
|
|
15
|
+
break;
|
|
16
|
+
entries.delete(oldest);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** undefined when another plugin copy already announced the steer. */
|
|
20
|
+
export function announceHostSteer(steer) {
|
|
21
|
+
if (state.announced.has(steer.inboxID))
|
|
22
|
+
return undefined;
|
|
23
|
+
state.announced.add(steer.inboxID);
|
|
24
|
+
remember(state.announced, steer.inboxID);
|
|
25
|
+
const listener = state.listeners.get(steer.sessionID);
|
|
26
|
+
return listener ? listener(steer) : false;
|
|
27
|
+
}
|
|
28
|
+
/** Remember a queued message, so a later switch to `steer` can announce it. */
|
|
29
|
+
export function rememberQueuedSteer(steer) {
|
|
30
|
+
if (state.announced.has(steer.inboxID))
|
|
31
|
+
return;
|
|
32
|
+
state.queued.delete(steer.inboxID);
|
|
33
|
+
state.queued.set(steer.inboxID, steer);
|
|
34
|
+
remember(state.queued, steer.inboxID);
|
|
35
|
+
}
|
|
36
|
+
export function takeQueuedSteer(inboxID) {
|
|
37
|
+
const steer = state.queued.get(inboxID);
|
|
38
|
+
state.queued.delete(inboxID);
|
|
39
|
+
return steer;
|
|
40
|
+
}
|
|
41
|
+
export function listenForHostSteers(sessionID, listener) {
|
|
42
|
+
state.listeners.set(sessionID, listener);
|
|
43
|
+
return () => {
|
|
44
|
+
if (state.listeners.get(sessionID) === listener)
|
|
45
|
+
state.listeners.delete(sessionID);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** false when there is no room to track it; OpenCode's own copy then runs as usual. */
|
|
49
|
+
export function recordEarlySteer(steer) {
|
|
50
|
+
const records = state.injected.get(steer.sessionID);
|
|
51
|
+
if (records ? records.length >= MAX_RECORDS_PER_SESSION : state.injected.size >= MAX_REMEMBERED)
|
|
52
|
+
return false;
|
|
53
|
+
state.injected.set(steer.sessionID, [...records ?? [], steer]);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function keep(sessionID, records) {
|
|
57
|
+
if (records.length === 0)
|
|
58
|
+
state.injected.delete(sessionID);
|
|
59
|
+
else
|
|
60
|
+
state.injected.set(sessionID, records);
|
|
61
|
+
}
|
|
62
|
+
export function isEarlySteerTracked(sessionID, injectionId) {
|
|
63
|
+
return !!state.injected.get(sessionID)?.some((record) => record.injectionId === injectionId);
|
|
64
|
+
}
|
|
65
|
+
/** An injection Cursor never accepted. A promoted one stays, to be sent at `turn_ended`. */
|
|
66
|
+
export function forgetUndeliveredEarlySteer(sessionID, injectionId) {
|
|
67
|
+
keep(sessionID, (state.injected.get(sessionID) ?? [])
|
|
68
|
+
.filter((record) => record.injectionId !== injectionId || record.delivered || record.promoted));
|
|
69
|
+
}
|
|
70
|
+
/** The host withdrew the message, so no copy of it will come. */
|
|
71
|
+
export function forgetEarlySteerOfInbox(sessionID, inboxID) {
|
|
72
|
+
keep(sessionID, (state.injected.get(sessionID) ?? []).filter((record) => record.inboxID !== inboxID));
|
|
73
|
+
}
|
|
74
|
+
/** true when the injection is an early steer. */
|
|
75
|
+
export function markEarlySteerDelivered(sessionID, injectionId) {
|
|
76
|
+
const record = sessionID && injectionId
|
|
77
|
+
? state.injected.get(sessionID)?.find((candidate) => candidate.injectionId === injectionId)
|
|
78
|
+
: undefined;
|
|
79
|
+
if (record)
|
|
80
|
+
record.delivered = true;
|
|
81
|
+
return !!record;
|
|
82
|
+
}
|
|
83
|
+
export function markEarlySteersCheckpointed(sessionID, run) {
|
|
84
|
+
for (const record of (sessionID && state.injected.get(sessionID)) || []) {
|
|
85
|
+
if (record.run === run && record.delivered)
|
|
86
|
+
record.checkpointed = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* At `turn_ended`: a delivered steer is answered and waits for OpenCode's copy; one Cursor never took is
|
|
91
|
+
* forgotten, so OpenCode's copy runs as an ordinary message. A promoted one has no copy left to come:
|
|
92
|
+
* one Cursor never took is returned for a follow-up Run and stays, as sent, until a checkpoint holds it.
|
|
93
|
+
* Answered receipts survive retries and are retired when a later Run finishes.
|
|
94
|
+
*/
|
|
95
|
+
export function settleEarlySteers(sessionID, run) {
|
|
96
|
+
const records = sessionID ? state.injected.get(sessionID) : undefined;
|
|
97
|
+
if (!records)
|
|
98
|
+
return { answered: 0, undelivered: [], leftToHost: 0 };
|
|
99
|
+
let answered = 0;
|
|
100
|
+
let leftToHost = 0;
|
|
101
|
+
const undelivered = [];
|
|
102
|
+
const kept = records.filter((record) => {
|
|
103
|
+
if (record.run !== run)
|
|
104
|
+
return !record.answered;
|
|
105
|
+
if (record.promoted) {
|
|
106
|
+
if (record.delivered) {
|
|
107
|
+
if (!record.answered) {
|
|
108
|
+
record.answered = true;
|
|
109
|
+
answered++;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
undelivered.push(record);
|
|
114
|
+
record.delivered = true;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (!record.delivered) {
|
|
118
|
+
leftToHost++;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (!record.answered) {
|
|
122
|
+
record.answered = true;
|
|
123
|
+
answered++;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
keep(sessionID, kept);
|
|
128
|
+
return { answered, undelivered, leftToHost };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A failed Run's steers: those its checkpoint holds go to the Run resumed from it (`to`), and so does a
|
|
132
|
+
* promoted one it does not hold, to be sent at that Run's `turn_ended`. The rest are forgotten, so
|
|
133
|
+
* OpenCode's own copy of each runs as an ordinary message. A rebase (`to` undefined) seeds from host
|
|
134
|
+
* history, which already holds a promoted copy.
|
|
135
|
+
*/
|
|
136
|
+
export function handOverEarlySteers(sessionID, from, to) {
|
|
137
|
+
const records = sessionID ? state.injected.get(sessionID) : undefined;
|
|
138
|
+
if (!records)
|
|
139
|
+
return;
|
|
140
|
+
keep(sessionID, records.filter((record) => {
|
|
141
|
+
if (record.run !== from || record.answered)
|
|
142
|
+
return true;
|
|
143
|
+
if (!to || (!record.checkpointed && !record.promoted))
|
|
144
|
+
return false;
|
|
145
|
+
record.run = to;
|
|
146
|
+
if (!record.checkpointed)
|
|
147
|
+
record.delivered = false;
|
|
148
|
+
return true;
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
const SYSTEM_REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
152
|
+
/** The host may add `<system-reminder>` parts to a promoted copy; any other difference is another message. */
|
|
153
|
+
export function carriesSteer(message, text) {
|
|
154
|
+
return message.replace(SYSTEM_REMINDER, "").trim() === text.trim();
|
|
155
|
+
}
|
|
156
|
+
/** The record each message carries, or nothing unless every message carries one. */
|
|
157
|
+
export function matchEarlySteers(sessionID, messages, accept) {
|
|
158
|
+
const records = sessionID ? state.injected.get(sessionID) : undefined;
|
|
159
|
+
if (!records?.length || messages.length === 0)
|
|
160
|
+
return [];
|
|
161
|
+
const candidates = [...records];
|
|
162
|
+
const matched = [];
|
|
163
|
+
for (const message of messages) {
|
|
164
|
+
const index = candidates.findIndex((record) => accept(record) && carriesSteer(message, record.text));
|
|
165
|
+
if (index === -1)
|
|
166
|
+
return [];
|
|
167
|
+
matched.push(...candidates.splice(index, 1));
|
|
168
|
+
}
|
|
169
|
+
return matched;
|
|
170
|
+
}
|
|
171
|
+
export function forgetEarlySteerRecords(sessionID, records) {
|
|
172
|
+
keep(sessionID, (state.injected.get(sessionID) ?? []).filter((record) => !records.includes(record)));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A steer of a Run that ended without answering it (a stop, a failed recovery) that only host history
|
|
176
|
+
* holds reliably: OpenCode placed it in its history (a checkpoint may hold it too, and resuming would
|
|
177
|
+
* then send it twice), or a checkpoint holds it while OpenCode's copy is still to come.
|
|
178
|
+
*/
|
|
179
|
+
export function needsHostHistory(sessionID, openRun) {
|
|
180
|
+
return !!sessionID && !!state.injected.get(sessionID)?.some((record) => !record.answered && record.run !== openRun && (record.promoted || record.checkpointed));
|
|
181
|
+
}
|
|
182
|
+
/** Keep answered receipts until the new turn succeeds, so a host retry still omits its copies. */
|
|
183
|
+
export function clearUnansweredEarlySteers(sessionID) {
|
|
184
|
+
if (sessionID)
|
|
185
|
+
keep(sessionID, (state.injected.get(sessionID) ?? []).filter((record) => record.answered));
|
|
186
|
+
}
|
|
187
|
+
/** Private restart metadata, saved atomically with the checkpoint it describes. */
|
|
188
|
+
export function serializeEarlySteers(sessionID) {
|
|
189
|
+
const records = state.injected.get(sessionID);
|
|
190
|
+
return records?.length ? JSON.stringify({ version: 1, records }) : undefined;
|
|
191
|
+
}
|
|
192
|
+
export function restoreEarlySteers(sessionID, encoded) {
|
|
193
|
+
if (!encoded || state.injected.has(sessionID) || state.injected.size >= MAX_REMEMBERED)
|
|
194
|
+
return;
|
|
195
|
+
let value;
|
|
196
|
+
try {
|
|
197
|
+
value = JSON.parse(encoded);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!value || value.version !== 1 || !Array.isArray(value.records) || value.records.length > MAX_RECORDS_PER_SESSION)
|
|
203
|
+
return;
|
|
204
|
+
const records = [];
|
|
205
|
+
for (const record of value.records) {
|
|
206
|
+
if (!record || record.sessionID !== sessionID
|
|
207
|
+
|| ![record.inboxID, record.injectionId, record.run, record.text].every((part) => typeof part === "string" && part.length > 0)
|
|
208
|
+
|| ![record.delivered, record.checkpointed, record.answered].every((part) => typeof part === "boolean")
|
|
209
|
+
|| ((record.checkpointed || record.answered) && !record.delivered)
|
|
210
|
+
|| (record.promoted !== undefined && typeof record.promoted !== "boolean")
|
|
211
|
+
|| (record.hostTurn !== undefined && (typeof record.hostTurn !== "string" || !/^[a-f0-9]{64}$/.test(record.hostTurn))))
|
|
212
|
+
return;
|
|
213
|
+
records.push({
|
|
214
|
+
sessionID, inboxID: record.inboxID, injectionId: record.injectionId, run: record.run, text: record.text,
|
|
215
|
+
delivered: record.delivered, checkpointed: record.checkpointed, answered: record.answered,
|
|
216
|
+
...(record.promoted !== undefined ? { promoted: record.promoted } : {}),
|
|
217
|
+
...(record.hostTurn !== undefined ? { hostTurn: record.hostTurn } : {}),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (records.length)
|
|
221
|
+
state.injected.set(sessionID, records);
|
|
222
|
+
}
|
|
223
|
+
export function forgetEarlySteers(sessionID) {
|
|
224
|
+
state.injected.delete(sessionID);
|
|
225
|
+
state.listeners.delete(sessionID);
|
|
226
|
+
}
|
|
227
|
+
export function resetHostSteersForTests() {
|
|
228
|
+
state.listeners.clear();
|
|
229
|
+
state.injected.clear();
|
|
230
|
+
state.queued.clear();
|
|
231
|
+
state.announced.clear();
|
|
232
|
+
}
|
package/dist/image-input.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type CursorHistoryImageExtraction = {
|
|
|
9
9
|
hashes: string[];
|
|
10
10
|
candidateCount: number;
|
|
11
11
|
duplicateCount: number;
|
|
12
|
+
/** Unreadable optional tool media; explicit user attachments still fail loudly. */
|
|
13
|
+
omittedCount?: number;
|
|
12
14
|
};
|
|
13
15
|
export type CursorPromptImageExtraction = CursorHistoryImageExtraction & {
|
|
14
16
|
userImageCount: number;
|
|
@@ -23,6 +25,22 @@ export declare function extractCursorHistoryImages(prompt: readonly unknown[], o
|
|
|
23
25
|
maxBytes?: number;
|
|
24
26
|
filenameOffset?: number;
|
|
25
27
|
}): Promise<CursorHistoryImageExtraction>;
|
|
28
|
+
/**
|
|
29
|
+
* Decode the images a host tool returned (its own `file-data` / `image-data`
|
|
30
|
+
* parts, or the `file` parts OpenCode moves into the trailing
|
|
31
|
+
* `Attached media from tool result:` message) for a held-Run exec result.
|
|
32
|
+
* Non-image parts are skipped; a part that cannot be decoded is dropped so the
|
|
33
|
+
* text result is still delivered.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractCursorToolResultImages(parts: readonly unknown[], options?: {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
maxBytes?: number;
|
|
38
|
+
maxImages?: number;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
images: CursorImageInput[];
|
|
41
|
+
hashes: string[];
|
|
42
|
+
omittedCount: number;
|
|
43
|
+
}>;
|
|
26
44
|
export declare function extractCursorPromptImages(prompt: readonly unknown[], lastUser: Record<string, unknown> | undefined, options: {
|
|
27
45
|
supportsImages: boolean;
|
|
28
46
|
seenHistoryHashes?: ReadonlySet<string>;
|
package/dist/image-input.js
CHANGED
|
@@ -4,8 +4,14 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { UnsupportedFunctionalityError } from "@ai-sdk/provider";
|
|
6
6
|
export const MAX_CURSOR_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
7
|
+
function isToolMediaCaption(message) {
|
|
8
|
+
if (!message || !Array.isArray(message.content))
|
|
9
|
+
return false;
|
|
10
|
+
const first = message.content[0];
|
|
11
|
+
return first?.type === "text" && first.text === "Attached media from tool result:";
|
|
12
|
+
}
|
|
7
13
|
export function hasCursorUserImages(lastUser) {
|
|
8
|
-
return !!lastUser && Array.isArray(lastUser.content) && lastUser.content.some((part) => {
|
|
14
|
+
return !isToolMediaCaption(lastUser) && !!lastUser && Array.isArray(lastUser.content) && lastUser.content.some((part) => {
|
|
9
15
|
if (!part || typeof part !== "object")
|
|
10
16
|
return false;
|
|
11
17
|
const file = part;
|
|
@@ -20,18 +26,20 @@ export function assertCursorUserImageSupport(lastUser, supportsImages, modelId)
|
|
|
20
26
|
return;
|
|
21
27
|
unsupported("image input", `Cursor model ${JSON.stringify(modelId)} does not support image input`);
|
|
22
28
|
}
|
|
23
|
-
function decodeBase64(value) {
|
|
29
|
+
function decodeBase64(value, remaining) {
|
|
24
30
|
const normalized = value.replace(/\s/g, "");
|
|
25
31
|
if (!normalized || !/^[A-Za-z0-9+/_-]*={0,2}$/.test(normalized)) {
|
|
26
32
|
return unsupported("image input", "Cursor provider received invalid base64 image data");
|
|
27
33
|
}
|
|
34
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
35
|
+
assertImageSize(Math.floor((normalized.length - padding) * 3 / 4), remaining);
|
|
28
36
|
const data = Uint8Array.from(Buffer.from(normalized, "base64"));
|
|
29
37
|
if (data.length === 0) {
|
|
30
38
|
return unsupported("image input", "Cursor provider received an empty image");
|
|
31
39
|
}
|
|
32
40
|
return data;
|
|
33
41
|
}
|
|
34
|
-
function decodeDataUrl(value) {
|
|
42
|
+
function decodeDataUrl(value, remaining) {
|
|
35
43
|
if (!value.startsWith("data:")) {
|
|
36
44
|
return unsupported("image input", "Cursor provider supports base64-encoded image data URLs only");
|
|
37
45
|
}
|
|
@@ -47,7 +55,7 @@ function decodeDataUrl(value) {
|
|
|
47
55
|
const firstSeparator = metadata.indexOf(";");
|
|
48
56
|
const mimeType = metadata.slice(0, firstSeparator);
|
|
49
57
|
return {
|
|
50
|
-
data: decodeBase64(value.slice(commaIndex + 1)),
|
|
58
|
+
data: decodeBase64(value.slice(commaIndex + 1), remaining),
|
|
51
59
|
mimeType: mimeType || undefined,
|
|
52
60
|
};
|
|
53
61
|
}
|
|
@@ -121,13 +129,15 @@ async function readResponseBytes(response, remaining) {
|
|
|
121
129
|
return data;
|
|
122
130
|
}
|
|
123
131
|
async function resolveImageData(value, remaining, signal) {
|
|
124
|
-
if (value instanceof Uint8Array)
|
|
132
|
+
if (value instanceof Uint8Array) {
|
|
133
|
+
assertImageSize(value.length, remaining);
|
|
125
134
|
return { data: Uint8Array.from(value) };
|
|
135
|
+
}
|
|
126
136
|
if (typeof value === "string") {
|
|
127
|
-
return value.startsWith("data:") ? decodeDataUrl(value) : { data: decodeBase64(value) };
|
|
137
|
+
return value.startsWith("data:") ? decodeDataUrl(value, remaining) : { data: decodeBase64(value, remaining) };
|
|
128
138
|
}
|
|
129
139
|
if (value.protocol === "data:")
|
|
130
|
-
return decodeDataUrl(value.href);
|
|
140
|
+
return decodeDataUrl(value.href, remaining);
|
|
131
141
|
if (value.protocol === "file:") {
|
|
132
142
|
const filePath = fileURLToPath(value);
|
|
133
143
|
const info = await stat(filePath);
|
|
@@ -183,21 +193,33 @@ export async function extractCursorUserImages(lastUser, signal, maxBytes = MAX_C
|
|
|
183
193
|
const file = part;
|
|
184
194
|
if (file.type !== "file")
|
|
185
195
|
continue;
|
|
186
|
-
const image = await decodeCursorImagePart(file, byteBudget - totalBytes, signal, `image-${images.length + 1}`);
|
|
196
|
+
const image = await decodeCursorImagePart(toolImagePart(file) ?? file, byteBudget - totalBytes, signal, `image-${images.length + 1}`);
|
|
187
197
|
totalBytes += image.data.length;
|
|
188
198
|
images.push(image);
|
|
189
199
|
}
|
|
190
200
|
return images;
|
|
191
201
|
}
|
|
192
|
-
|
|
202
|
+
/** Normalize OpenCode / AI SDK attachment shapes before either delivery path. */
|
|
203
|
+
function toolImagePart(part) {
|
|
204
|
+
if (!part || typeof part !== "object")
|
|
205
|
+
return undefined;
|
|
206
|
+
const file = part;
|
|
207
|
+
if (!["file", "file-data", "image-data", "image-url", "media", "image"].includes(String(file.type)))
|
|
208
|
+
return undefined;
|
|
209
|
+
const mediaType = typeof file.mediaType === "string" ? file.mediaType
|
|
210
|
+
: file.type === "image-url" ? "image/*" : file.mime;
|
|
211
|
+
if (typeof mediaType !== "string" || !mediaType.startsWith("image/"))
|
|
212
|
+
return undefined;
|
|
213
|
+
const source = file.url ?? file.uri;
|
|
214
|
+
const data = file.data ?? (typeof source === "string" && /^(?:https?|file):/.test(source) && URL.canParse(source)
|
|
215
|
+
? new URL(source) : source);
|
|
216
|
+
return { ...file, mediaType, data };
|
|
217
|
+
}
|
|
218
|
+
function pushImageFileParts(parts, content, toolResult = false) {
|
|
193
219
|
for (const part of content) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (file.type === "file" &&
|
|
198
|
-
typeof file.mediaType === "string" &&
|
|
199
|
-
file.mediaType.startsWith("image/"))
|
|
200
|
-
parts.push(file);
|
|
220
|
+
const file = toolImagePart(part);
|
|
221
|
+
if (file?.type === "file")
|
|
222
|
+
parts.push({ file, toolResult });
|
|
201
223
|
}
|
|
202
224
|
}
|
|
203
225
|
function cursorHistoryImageParts(prompt) {
|
|
@@ -222,7 +244,7 @@ function cursorHistoryImageParts(prompt) {
|
|
|
222
244
|
if (record.role === "user") {
|
|
223
245
|
if (i === lastUserIndex)
|
|
224
246
|
continue;
|
|
225
|
-
pushImageFileParts(parts, record.content);
|
|
247
|
+
pushImageFileParts(parts, record.content, isToolMediaCaption(record));
|
|
226
248
|
continue;
|
|
227
249
|
}
|
|
228
250
|
if (record.role === "assistant") {
|
|
@@ -244,11 +266,9 @@ function cursorHistoryImageParts(prompt) {
|
|
|
244
266
|
for (const value of output.value) {
|
|
245
267
|
if (!value || typeof value !== "object")
|
|
246
268
|
continue;
|
|
247
|
-
const file = value;
|
|
248
|
-
if (file
|
|
249
|
-
|
|
250
|
-
file.mediaType.startsWith("image/"))
|
|
251
|
-
parts.push(file);
|
|
269
|
+
const file = toolImagePart(value);
|
|
270
|
+
if (file)
|
|
271
|
+
parts.push({ file, toolResult: true });
|
|
252
272
|
}
|
|
253
273
|
}
|
|
254
274
|
}
|
|
@@ -264,11 +284,22 @@ export async function extractCursorHistoryImages(prompt, options) {
|
|
|
264
284
|
const hashes = [];
|
|
265
285
|
const hashesThisTurn = new Set();
|
|
266
286
|
let duplicateCount = 0;
|
|
287
|
+
let omittedCount = 0;
|
|
267
288
|
let totalBytes = 0;
|
|
268
|
-
for (const file of candidates) {
|
|
289
|
+
for (const { file, toolResult } of candidates) {
|
|
269
290
|
// Resolve against the per-image cap first so a previously sent duplicate
|
|
270
291
|
// does not fail merely because little combined budget remains this turn.
|
|
271
|
-
|
|
292
|
+
let image;
|
|
293
|
+
try {
|
|
294
|
+
image = await decodeCursorImagePart(file, MAX_CURSOR_IMAGE_INPUT_BYTES, options.signal, `image-${(options.filenameOffset ?? 0) + images.length + 1}`);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
options.signal?.throwIfAborted();
|
|
298
|
+
if (!toolResult || (error instanceof Error && error.name === "AbortError"))
|
|
299
|
+
throw error;
|
|
300
|
+
omittedCount++;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
272
303
|
const hash = imageContentHash(image.data);
|
|
273
304
|
if (options.seenHashes?.has(hash) || hashesThisTurn.has(hash)) {
|
|
274
305
|
duplicateCount++;
|
|
@@ -280,17 +311,74 @@ export async function extractCursorHistoryImages(prompt, options) {
|
|
|
280
311
|
hashes.push(hash);
|
|
281
312
|
hashesThisTurn.add(hash);
|
|
282
313
|
}
|
|
283
|
-
return { images, hashes, candidateCount: candidates.length, duplicateCount };
|
|
314
|
+
return { images, hashes, candidateCount: candidates.length, duplicateCount, ...(omittedCount > 0 ? { omittedCount } : {}) };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Decode the images a host tool returned (its own `file-data` / `image-data`
|
|
318
|
+
* parts, or the `file` parts OpenCode moves into the trailing
|
|
319
|
+
* `Attached media from tool result:` message) for a held-Run exec result.
|
|
320
|
+
* Non-image parts are skipped; a part that cannot be decoded is dropped so the
|
|
321
|
+
* text result is still delivered.
|
|
322
|
+
*/
|
|
323
|
+
export async function extractCursorToolResultImages(parts, options = {}) {
|
|
324
|
+
const maxBytes = cursorImageBudget(options.maxBytes ?? MAX_CURSOR_IMAGE_INPUT_BYTES);
|
|
325
|
+
const images = [];
|
|
326
|
+
const hashes = [];
|
|
327
|
+
let totalBytes = 0;
|
|
328
|
+
let omittedCount = 0;
|
|
329
|
+
options.signal?.throwIfAborted();
|
|
330
|
+
for (const part of parts) {
|
|
331
|
+
options.signal?.throwIfAborted();
|
|
332
|
+
const file = toolImagePart(part);
|
|
333
|
+
if (!file)
|
|
334
|
+
continue;
|
|
335
|
+
if (images.length >= (options.maxImages ?? Infinity) || totalBytes >= maxBytes) {
|
|
336
|
+
omittedCount++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const image = await decodeCursorImagePart(file, maxBytes - totalBytes, options.signal, `image-${images.length + 1}`);
|
|
341
|
+
options.signal?.throwIfAborted();
|
|
342
|
+
totalBytes += image.data.length;
|
|
343
|
+
images.push(image);
|
|
344
|
+
hashes.push(imageContentHash(image.data));
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
// Tool media is optional: a missing file or failed download must not
|
|
348
|
+
// strand every pending exec. Cancellation still belongs to the caller.
|
|
349
|
+
options.signal?.throwIfAborted();
|
|
350
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
351
|
+
throw error;
|
|
352
|
+
omittedCount++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { images, hashes, omittedCount };
|
|
284
356
|
}
|
|
285
357
|
export async function extractCursorPromptImages(prompt, lastUser, options) {
|
|
286
358
|
const maxBytes = cursorImageBudget(options.maxBytes ?? MAX_CURSOR_IMAGE_INPUT_BYTES);
|
|
287
|
-
const
|
|
359
|
+
const toolCaption = isToolMediaCaption(lastUser);
|
|
360
|
+
const caption = toolCaption && options.supportsImages
|
|
361
|
+
? await extractCursorToolResultImages(lastUser.content, { signal: options.signal, maxBytes })
|
|
362
|
+
: undefined;
|
|
363
|
+
const captionHashes = [];
|
|
364
|
+
const seenHashes = new Set(options.seenHistoryHashes);
|
|
365
|
+
let captionDuplicates = 0;
|
|
366
|
+
const userImages = toolCaption ? (caption?.images ?? []).filter((_image, index) => {
|
|
367
|
+
const hash = caption.hashes[index];
|
|
368
|
+
if (seenHashes.has(hash)) {
|
|
369
|
+
captionDuplicates++;
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
seenHashes.add(hash);
|
|
373
|
+
captionHashes.push(hash);
|
|
374
|
+
return true;
|
|
375
|
+
}) : await extractCursorUserImages(lastUser, options.signal, maxBytes);
|
|
288
376
|
const userBytes = userImages.reduce((total, image) => total + image.data.length, 0);
|
|
377
|
+
const userHashes = toolCaption ? captionHashes : userImages.map((image) => imageContentHash(image.data));
|
|
289
378
|
// Seed history dedupe with this-turn last-user hashes so the same bytes on an
|
|
290
379
|
// earlier user/assistant/tool message are not attached twice in one Run.
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
seenHashes.add(imageContentHash(image.data));
|
|
380
|
+
for (const hash of userHashes)
|
|
381
|
+
seenHashes.add(hash);
|
|
294
382
|
const history = await extractCursorHistoryImages(prompt, {
|
|
295
383
|
supportsImages: options.supportsImages,
|
|
296
384
|
seenHashes,
|
|
@@ -300,7 +388,11 @@ export async function extractCursorPromptImages(prompt, lastUser, options) {
|
|
|
300
388
|
});
|
|
301
389
|
return {
|
|
302
390
|
...history,
|
|
391
|
+
hashes: [...userHashes, ...history.hashes],
|
|
392
|
+
duplicateCount: history.duplicateCount + captionDuplicates,
|
|
303
393
|
images: [...userImages, ...history.images],
|
|
304
|
-
userImageCount: userImages.length,
|
|
394
|
+
userImageCount: toolCaption ? 0 : userImages.length,
|
|
395
|
+
...((history.omittedCount ?? 0) + (caption?.omittedCount ?? 0) > 0
|
|
396
|
+
? { omittedCount: (history.omittedCount ?? 0) + (caption?.omittedCount ?? 0) } : {}),
|
|
305
397
|
};
|
|
306
398
|
}
|
package/dist/image-save.d.ts
CHANGED
|
@@ -17,16 +17,24 @@ export declare const IMAGE_PERMISSION_DENIED_PREFIX = "CursorImagePermissionDeni
|
|
|
17
17
|
export type ImageSaveToolContext = {
|
|
18
18
|
worktree: string;
|
|
19
19
|
directory: string;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Classic OpenCode 1.x `ToolContext.ask`. A host whose tool context has no
|
|
22
|
+
* permission prompt (OpenCode 2.0's public `ToolContext`) passes `null`
|
|
23
|
+
* explicitly: the commit then writes after containment, gated only by the
|
|
24
|
+
* tool's catalog permission. A missing `ask` is refused, never treated as
|
|
25
|
+
* permission.
|
|
26
|
+
*/
|
|
27
|
+
ask: ((input: {
|
|
21
28
|
permission: string;
|
|
22
29
|
patterns: string[];
|
|
23
30
|
always: string[];
|
|
24
31
|
metadata: Record<string, unknown>;
|
|
25
|
-
})
|
|
32
|
+
}) => Promise<void>) | null;
|
|
26
33
|
};
|
|
27
34
|
export type ImageSaveResult = {
|
|
28
35
|
title: string;
|
|
29
36
|
output: string;
|
|
37
|
+
bytes: number;
|
|
30
38
|
attachments?: Array<{
|
|
31
39
|
type: "file";
|
|
32
40
|
mime: string;
|