cursor-opencode-provider 0.2.1 → 0.2.2
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/DISCLAIMER.md +9 -0
- package/README.md +9 -4
- package/dist/context/build.js +3 -2
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -0
- package/dist/language-model.d.ts +52 -6
- package/dist/language-model.js +414 -56
- package/dist/plugin.js +17 -2
- package/dist/protocol/blob-store.d.ts +2 -0
- package/dist/protocol/blob-store.js +4 -0
- package/dist/protocol/conversation-bind.d.ts +18 -0
- package/dist/protocol/conversation-bind.js +66 -0
- package/dist/protocol/interactions.d.ts +23 -0
- package/dist/protocol/interactions.js +136 -0
- package/dist/protocol/messages.js +421 -13
- package/dist/protocol/request.d.ts +30 -0
- package/dist/protocol/request.js +23 -8
- package/dist/protocol/stream.js +9 -3
- package/dist/protocol/tool-call-bridge.d.ts +44 -0
- package/dist/protocol/tool-call-bridge.js +480 -0
- package/dist/protocol/tools.d.ts +35 -12
- package/dist/protocol/tools.js +179 -74
- package/dist/session.d.ts +26 -1
- package/dist/session.js +2 -2
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/package.json +2 -1
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
|
|
1
|
+
import { CURSOR_COMPACTION_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
|
|
2
2
|
import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtPayload } from "./auth.js";
|
|
3
3
|
import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, readCache, discoverModels, isCacheFresh } from "./models.js";
|
|
4
4
|
import { opencodeGlobalCacheDir } from "./context/paths.js";
|
|
@@ -129,6 +129,11 @@ export function modelInfoToConfig(mi, options = {}) {
|
|
|
129
129
|
const context = contextTier === "long"
|
|
130
130
|
? (mi.maxContextForMaxMode ?? 1_000_000)
|
|
131
131
|
: (mi.maxContext ?? 200_000);
|
|
132
|
+
// OpenCode's overflow/compaction/UI use limit.context; generation and
|
|
133
|
+
// thinking budgets use limit.output. models.dev 1M peers advertise
|
|
134
|
+
// 64k–128k output — a tiny cap makes long-context sessions feel broken
|
|
135
|
+
// even when the 1M input window is correct.
|
|
136
|
+
const output = contextTier === "long" ? 128_000 : 32_000;
|
|
132
137
|
const config = {
|
|
133
138
|
name,
|
|
134
139
|
reasoning: mi.supportsThinking ?? false,
|
|
@@ -136,7 +141,7 @@ export function modelInfoToConfig(mi, options = {}) {
|
|
|
136
141
|
temperature: false,
|
|
137
142
|
limit: {
|
|
138
143
|
context,
|
|
139
|
-
output
|
|
144
|
+
output,
|
|
140
145
|
},
|
|
141
146
|
};
|
|
142
147
|
const variantConfig = modelInfoVariants(mi, variants);
|
|
@@ -306,6 +311,16 @@ export async function CursorPlugin(input) {
|
|
|
306
311
|
return cached?.models.length ? modelsToConfig(cached.models) : {};
|
|
307
312
|
}
|
|
308
313
|
return {
|
|
314
|
+
async "chat.params"(hookInput, output) {
|
|
315
|
+
if (hookInput.model.providerID !== CURSOR_PROVIDER_ID)
|
|
316
|
+
return;
|
|
317
|
+
// OpenCode's compaction pipeline invokes the LLM with agent="compaction".
|
|
318
|
+
// Carry that stable runtime fact into LanguageModelV3 providerOptions so
|
|
319
|
+
// the provider never has to guess from an empty tool list.
|
|
320
|
+
if (hookInput.agent === "compaction") {
|
|
321
|
+
output.options[CURSOR_COMPACTION_OPTION] = true;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
309
324
|
async config(cfg) {
|
|
310
325
|
cfg.provider ??= {};
|
|
311
326
|
const models = await loadModels();
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
export declare function setConversationBlob(conversationId: string, blobId: Uint8Array, blobData: Uint8Array): string;
|
|
11
11
|
export declare function getConversationBlob(conversationId: string, blobId: Uint8Array): Uint8Array | undefined;
|
|
12
12
|
export declare function conversationBlobCount(conversationId: string): number;
|
|
13
|
+
/** Drop all blobs for a conversation (compaction conversation reset). */
|
|
14
|
+
export declare function clearConversationBlobs(conversationId: string): void;
|
|
13
15
|
/** SHA-256 content hashes are 32 non-text bytes — never echo those as content. */
|
|
14
16
|
export declare function isBlobIdHash(blobId: Uint8Array): boolean;
|
|
15
17
|
export declare function resetConversationBlobsForTests(): void;
|
|
@@ -34,6 +34,10 @@ export function getConversationBlob(conversationId, blobId) {
|
|
|
34
34
|
export function conversationBlobCount(conversationId) {
|
|
35
35
|
return byConversation.get(conversationId)?.size ?? 0;
|
|
36
36
|
}
|
|
37
|
+
/** Drop all blobs for a conversation (compaction conversation reset). */
|
|
38
|
+
export function clearConversationBlobs(conversationId) {
|
|
39
|
+
byConversation.delete(conversationId);
|
|
40
|
+
}
|
|
37
41
|
/** SHA-256 content hashes are 32 non-text bytes — never echo those as content. */
|
|
38
42
|
export function isBlobIdHash(blobId) {
|
|
39
43
|
if (blobId.length !== 32)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const MAX_ACTIVE_CONVERSATION_BINDINGS = 256;
|
|
2
|
+
export declare function resetConversationBindingsForTests(): void;
|
|
3
|
+
/** Deterministic UUID (version-4 shape) from an arbitrary session key. */
|
|
4
|
+
export declare function sessionIdToUuid(sessionId: string): string;
|
|
5
|
+
/** Current Cursor conversation_id for an OpenCode session key, creating the default binding if needed. */
|
|
6
|
+
export declare function peekConversationId(sessionKey: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the Cursor conversation_id for this OpenCode session.
|
|
9
|
+
* When `reset` is true (compaction/summary), discard prior Cursor state and
|
|
10
|
+
* mint a new id so TurnEnded cache_read cannot keep overflowing OpenCode.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bindConversationId(sessionKey: string | undefined, opts?: {
|
|
13
|
+
reset?: boolean;
|
|
14
|
+
}): {
|
|
15
|
+
conversationId: string;
|
|
16
|
+
reset: boolean;
|
|
17
|
+
previousId?: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { clearCheckpoint } from "./checkpoint.js";
|
|
3
|
+
import { clearConversationBlobs } from "./blob-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* OpenCode session key → active Cursor conversation_id.
|
|
6
|
+
*
|
|
7
|
+
* Normally the binding is the deterministic UUID from the OpenCode session id
|
|
8
|
+
* (CLI-shaped sticky conversation). Compaction/summary turns mint a fresh id
|
|
9
|
+
* and drop the prior checkpoint + blobs so Cursor no longer bills the old
|
|
10
|
+
* cached conversation against OpenCode's newly compacted prompt.
|
|
11
|
+
*/
|
|
12
|
+
const activeBySession = new Map();
|
|
13
|
+
export const MAX_ACTIVE_CONVERSATION_BINDINGS = 256;
|
|
14
|
+
function rememberConversation(sessionKey, conversationId) {
|
|
15
|
+
// Map insertion order is the LRU order. Refresh existing sessions on use.
|
|
16
|
+
activeBySession.delete(sessionKey);
|
|
17
|
+
activeBySession.set(sessionKey, conversationId);
|
|
18
|
+
while (activeBySession.size > MAX_ACTIVE_CONVERSATION_BINDINGS) {
|
|
19
|
+
const oldest = activeBySession.entries().next().value;
|
|
20
|
+
if (!oldest)
|
|
21
|
+
break;
|
|
22
|
+
activeBySession.delete(oldest[0]);
|
|
23
|
+
clearCheckpoint(oldest[1]);
|
|
24
|
+
clearConversationBlobs(oldest[1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function resetConversationBindingsForTests() {
|
|
28
|
+
activeBySession.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Deterministic UUID (version-4 shape) from an arbitrary session key. */
|
|
31
|
+
export function sessionIdToUuid(sessionId) {
|
|
32
|
+
const hash = createHash("sha256").update(`cursor-opencode-provider:conv:${sessionId}`).digest();
|
|
33
|
+
const bytes = Buffer.from(hash.subarray(0, 16));
|
|
34
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
35
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
36
|
+
const hex = bytes.toString("hex");
|
|
37
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
38
|
+
}
|
|
39
|
+
/** Current Cursor conversation_id for an OpenCode session key, creating the default binding if needed. */
|
|
40
|
+
export function peekConversationId(sessionKey) {
|
|
41
|
+
let id = activeBySession.get(sessionKey);
|
|
42
|
+
if (!id) {
|
|
43
|
+
id = sessionIdToUuid(sessionKey);
|
|
44
|
+
}
|
|
45
|
+
rememberConversation(sessionKey, id);
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the Cursor conversation_id for this OpenCode session.
|
|
50
|
+
* When `reset` is true (compaction/summary), discard prior Cursor state and
|
|
51
|
+
* mint a new id so TurnEnded cache_read cannot keep overflowing OpenCode.
|
|
52
|
+
*/
|
|
53
|
+
export function bindConversationId(sessionKey, opts) {
|
|
54
|
+
if (!sessionKey) {
|
|
55
|
+
return { conversationId: crypto.randomUUID(), reset: !!opts?.reset };
|
|
56
|
+
}
|
|
57
|
+
if (opts?.reset) {
|
|
58
|
+
const previousId = activeBySession.get(sessionKey) ?? sessionIdToUuid(sessionKey);
|
|
59
|
+
clearCheckpoint(previousId);
|
|
60
|
+
clearConversationBlobs(previousId);
|
|
61
|
+
const conversationId = crypto.randomUUID();
|
|
62
|
+
rememberConversation(sessionKey, conversationId);
|
|
63
|
+
return { conversationId, reset: true, previousId };
|
|
64
|
+
}
|
|
65
|
+
return { conversationId: peekConversationId(sessionKey), reset: false };
|
|
66
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type InteractionQueryWireInfo = {
|
|
2
|
+
id?: number;
|
|
3
|
+
variantField?: number;
|
|
4
|
+
variantName?: string;
|
|
5
|
+
};
|
|
6
|
+
export type HandledInteraction = {
|
|
7
|
+
id: number;
|
|
8
|
+
variantField: number;
|
|
9
|
+
variantName: string;
|
|
10
|
+
outcome: "rejected" | "acknowledged" | "failed";
|
|
11
|
+
reply: Uint8Array;
|
|
12
|
+
};
|
|
13
|
+
export declare class UnsupportedInteractionQueryError extends Error {
|
|
14
|
+
constructor(info: InteractionQueryWireInfo);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Inspect the raw wrapper as well as the decoded object. protobufjs drops
|
|
18
|
+
* fields introduced by newer Cursor schemas; raw inspection lets us fail fast
|
|
19
|
+
* instead of accidentally restoring the heartbeat-only deadlock.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Array): InteractionQueryWireInfo;
|
|
22
|
+
/** Build the immediate typed response required by Cursor's Run RPC. */
|
|
23
|
+
export declare function handleInteractionQuery(query: Record<string, unknown>, agentServerPayload: Uint8Array): HandledInteraction;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { encodeMessage } from "./messages.js";
|
|
2
|
+
import { readAllFields } from "./struct.js";
|
|
3
|
+
const HEADLESS_REASON = "This Cursor interaction requires UI approval and is not available through the OpenCode provider.";
|
|
4
|
+
const variantNames = {
|
|
5
|
+
2: "web_search_request_query",
|
|
6
|
+
3: "ask_question_interaction_query",
|
|
7
|
+
4: "switch_mode_request_query",
|
|
8
|
+
7: "create_plan_request_query",
|
|
9
|
+
8: "setup_vm_environment_args",
|
|
10
|
+
9: "web_fetch_request_query",
|
|
11
|
+
10: "pr_management_request_query",
|
|
12
|
+
11: "mcp_auth_request_query",
|
|
13
|
+
12: "generate_image_request_query",
|
|
14
|
+
13: "replace_env_args",
|
|
15
|
+
14: "connect_scm_request_query",
|
|
16
|
+
};
|
|
17
|
+
export class UnsupportedInteractionQueryError extends Error {
|
|
18
|
+
constructor(info) {
|
|
19
|
+
const variant = info.variantField === undefined
|
|
20
|
+
? "missing variant"
|
|
21
|
+
: `unsupported variant field #${info.variantField}`;
|
|
22
|
+
const id = info.id === undefined ? "missing id" : `id=${info.id}`;
|
|
23
|
+
super(`Cursor interaction query cannot be handled (${id}, ${variant})`);
|
|
24
|
+
this.name = "UnsupportedInteractionQueryError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Inspect the raw wrapper as well as the decoded object. protobufjs drops
|
|
29
|
+
* fields introduced by newer Cursor schemas; raw inspection lets us fail fast
|
|
30
|
+
* instead of accidentally restoring the heartbeat-only deadlock.
|
|
31
|
+
*/
|
|
32
|
+
export function inspectInteractionQueryWire(agentServerPayload) {
|
|
33
|
+
const queryBytes = readAllFields(agentServerPayload)
|
|
34
|
+
.find((field) => field.fn === 7 && field.wt === 2)?.bytes;
|
|
35
|
+
if (!queryBytes)
|
|
36
|
+
return {};
|
|
37
|
+
// InteractionQuery.id is a proto3 uint32. Cursor commonly uses id=0, whose
|
|
38
|
+
// default scalar value is omitted from the wire; absence therefore means
|
|
39
|
+
// zero, not a malformed/missing correlation id.
|
|
40
|
+
let id = 0;
|
|
41
|
+
let variantField;
|
|
42
|
+
for (const field of readAllFields(queryBytes)) {
|
|
43
|
+
if (field.fn === 1 && field.wt === 0)
|
|
44
|
+
id = field.varint;
|
|
45
|
+
else if (field.wt === 2 && variantField === undefined)
|
|
46
|
+
variantField = field.fn;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
variantField,
|
|
51
|
+
variantName: variantField === undefined ? undefined : variantNames[variantField],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Build the immediate typed response required by Cursor's Run RPC. */
|
|
55
|
+
export function handleInteractionQuery(query, agentServerPayload) {
|
|
56
|
+
const info = inspectInteractionQueryWire(agentServerPayload);
|
|
57
|
+
if (info.id === undefined || info.variantField === undefined || !info.variantName) {
|
|
58
|
+
throw new UnsupportedInteractionQueryError(info);
|
|
59
|
+
}
|
|
60
|
+
if (typeof query.id === "number" && query.id !== info.id) {
|
|
61
|
+
throw new Error(`Cursor interaction query id mismatch: decoded=${query.id} wire=${info.id}`);
|
|
62
|
+
}
|
|
63
|
+
let response;
|
|
64
|
+
let outcome;
|
|
65
|
+
switch (info.variantField) {
|
|
66
|
+
case 2:
|
|
67
|
+
response = { web_search_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
68
|
+
outcome = "rejected";
|
|
69
|
+
break;
|
|
70
|
+
case 3:
|
|
71
|
+
response = {
|
|
72
|
+
ask_question_interaction_response: {
|
|
73
|
+
result: { rejected: { reason: "Questions must be asked through OpenCode's question tool." } },
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
outcome = "rejected";
|
|
77
|
+
break;
|
|
78
|
+
case 4:
|
|
79
|
+
response = { switch_mode_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
80
|
+
outcome = "rejected";
|
|
81
|
+
break;
|
|
82
|
+
case 7:
|
|
83
|
+
// Cursor CLI's headless fallback acknowledges plan creation without a
|
|
84
|
+
// client-side URI; the plan remains in conversation state/checkpoints.
|
|
85
|
+
response = { create_plan_request_response: { result: { success: {}, plan_uri: "" } } };
|
|
86
|
+
outcome = "acknowledged";
|
|
87
|
+
break;
|
|
88
|
+
case 8:
|
|
89
|
+
// OpenCode owns the local environment; there is no Cursor VM to create.
|
|
90
|
+
response = { setup_vm_environment_result: { success: {} } };
|
|
91
|
+
outcome = "acknowledged";
|
|
92
|
+
break;
|
|
93
|
+
case 9:
|
|
94
|
+
response = { web_fetch_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
95
|
+
outcome = "rejected";
|
|
96
|
+
break;
|
|
97
|
+
case 10:
|
|
98
|
+
response = { pr_management_result: { rejected: { reason: HEADLESS_REASON } } };
|
|
99
|
+
outcome = "rejected";
|
|
100
|
+
break;
|
|
101
|
+
case 11:
|
|
102
|
+
response = { mcp_auth_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
103
|
+
outcome = "rejected";
|
|
104
|
+
break;
|
|
105
|
+
case 12:
|
|
106
|
+
response = { generate_image_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
107
|
+
outcome = "rejected";
|
|
108
|
+
break;
|
|
109
|
+
case 13:
|
|
110
|
+
response = {
|
|
111
|
+
replace_env_result: {
|
|
112
|
+
failure: {
|
|
113
|
+
error_message: "Environment replacement is not supported by the OpenCode provider.",
|
|
114
|
+
setup_logs: "",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
outcome = "failed";
|
|
119
|
+
break;
|
|
120
|
+
case 14:
|
|
121
|
+
response = { connect_scm_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
122
|
+
outcome = "rejected";
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
throw new UnsupportedInteractionQueryError(info);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
id: info.id,
|
|
129
|
+
variantField: info.variantField,
|
|
130
|
+
variantName: info.variantName,
|
|
131
|
+
outcome,
|
|
132
|
+
reply: encodeMessage("AgentClientMessage", {
|
|
133
|
+
interaction_response: { id: info.id, ...response },
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
}
|