cursor-opencode-provider 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -17
- package/SECURITY.md +42 -0
- package/dist/activity.d.ts +15 -0
- package/dist/activity.js +76 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +19 -13
- package/dist/context/build.js +0 -7
- package/dist/context/rules.d.ts +4 -0
- package/dist/context/rules.js +53 -17
- package/dist/debug.d.ts +9 -0
- package/dist/debug.js +44 -4
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +199 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4 -0
- package/dist/language-model.d.ts +54 -7
- package/dist/language-model.js +730 -197
- package/dist/models.d.ts +7 -0
- package/dist/models.js +259 -79
- package/dist/plugin.js +38 -11
- package/dist/protocol/exec-variants.d.ts +24 -0
- package/dist/protocol/exec-variants.js +55 -0
- package/dist/protocol/interactions.d.ts +8 -1
- package/dist/protocol/interactions.js +15 -2
- package/dist/protocol/messages.js +109 -26
- package/dist/protocol/request.d.ts +2 -4
- package/dist/protocol/request.js +0 -8
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/tool-call-bridge.js +13 -1
- package/dist/protocol/tools.d.ts +8 -1
- package/dist/protocol/tools.js +384 -39
- package/dist/session.d.ts +115 -8
- package/dist/session.js +362 -31
- package/dist/shell-timeout.d.ts +57 -0
- package/dist/shell-timeout.js +186 -0
- package/dist/transport/connect.d.ts +37 -1
- package/dist/transport/connect.js +679 -115
- package/package.json +8 -2
package/dist/models.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export type ModelCache = {
|
|
|
31
31
|
/** Absent or mismatched → treat cache as stale (forces AvailableModels refetch). */
|
|
32
32
|
schemaVersion?: number;
|
|
33
33
|
};
|
|
34
|
+
export declare class CursorVariantSelectionError extends Error {
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
|
37
|
+
export declare function normalizeModelParameterValues(value: unknown): ModelParameterValue[] | null;
|
|
38
|
+
export declare function normalizeModelCache(value: unknown): ModelCache | null;
|
|
34
39
|
/**
|
|
35
40
|
* Read only the dedicated variant payload generated by the plugin. OpenCode
|
|
36
41
|
* merges model, agent, and variant options into one providerOptions namespace;
|
|
@@ -41,6 +46,7 @@ export declare function extractCursorVariantParameters(providerOptions: Record<s
|
|
|
41
46
|
export declare function resolveCursorWireModelId(providerOptions: Record<string, unknown> | undefined, fallback: string): string;
|
|
42
47
|
/** True when resolved params select the long-context (1m) tier. */
|
|
43
48
|
export declare function paramsImplyMaxMode(params: ModelParameterValue[]): boolean;
|
|
49
|
+
export declare function parseCursorContextLimit(value: unknown): number | undefined;
|
|
44
50
|
export declare function isCacheFresh(cache: ModelCache, ttlMs?: number): boolean;
|
|
45
51
|
export declare function cacheFilePath(cacheDir: string): string;
|
|
46
52
|
export declare function readCache(cacheDir: string): Promise<ModelCache | null>;
|
|
@@ -68,6 +74,7 @@ export declare function fetchModels(token: string, options?: {
|
|
|
68
74
|
baseURL?: string;
|
|
69
75
|
headers?: Record<string, string>;
|
|
70
76
|
}): Promise<ModelInfo[]>;
|
|
77
|
+
export declare function refreshModelCache(cacheDir: string, fetcher: () => Promise<ModelInfo[]>): Promise<ModelInfo[]>;
|
|
71
78
|
export declare function discoverModels(token: string, cacheDir: string, options?: {
|
|
72
79
|
baseURL?: string;
|
|
73
80
|
headers?: Record<string, string>;
|
package/dist/models.js
CHANGED
|
@@ -1,13 +1,158 @@
|
|
|
1
1
|
import { MODEL_CACHE_FILE, MODEL_CACHE_SCHEMA_VERSION, MODEL_CACHE_TTL_MS } from "./shared.js";
|
|
2
2
|
import { unaryAvailableModels } from "./transport/connect.js";
|
|
3
3
|
import { buildRequestedModelParams } from "./protocol/thinking.js";
|
|
4
|
-
import { readFile,
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
/** Dedicated OpenCode provider-option key for a selected Cursor variant. */
|
|
8
7
|
export const CURSOR_VARIANT_PARAMETERS_KEY = "cursorVariantParameters";
|
|
9
8
|
/** Real Cursor model id used when an OpenCode entry has a synthetic id. */
|
|
10
9
|
export const CURSOR_WIRE_MODEL_ID_KEY = "cursorModelId";
|
|
10
|
+
export class CursorVariantSelectionError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(`Cursor variant selection ${message}`);
|
|
13
|
+
this.name = "CursorVariantSelectionError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isPlainRecord(value) {
|
|
17
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
18
|
+
return false;
|
|
19
|
+
const prototype = Object.getPrototypeOf(value);
|
|
20
|
+
return prototype === Object.prototype || prototype === null;
|
|
21
|
+
}
|
|
22
|
+
function isNonEmptyString(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
function optionalString(record, names) {
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
if (!Object.hasOwn(record, name))
|
|
28
|
+
continue;
|
|
29
|
+
if (record[name] === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
return typeof record[name] === "string" ? record[name] : null;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function optionalBoolean(record, names) {
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
if (!Object.hasOwn(record, name))
|
|
38
|
+
continue;
|
|
39
|
+
if (record[name] === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
return typeof record[name] === "boolean" ? record[name] : null;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function optionalPositiveNumber(record, names) {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
if (!Object.hasOwn(record, name))
|
|
48
|
+
continue;
|
|
49
|
+
const value = record[name];
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
53
|
+
? value
|
|
54
|
+
: null;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
export function normalizeModelParameterValues(value) {
|
|
59
|
+
if (!Array.isArray(value))
|
|
60
|
+
return null;
|
|
61
|
+
const parameters = [];
|
|
62
|
+
for (const parameter of value) {
|
|
63
|
+
if (!isPlainRecord(parameter))
|
|
64
|
+
return null;
|
|
65
|
+
if (!isNonEmptyString(parameter.id) || typeof parameter.value !== "string")
|
|
66
|
+
return null;
|
|
67
|
+
parameters.push({ id: parameter.id, value: parameter.value });
|
|
68
|
+
}
|
|
69
|
+
return parameters;
|
|
70
|
+
}
|
|
71
|
+
function normalizeVariant(value, fallbackName) {
|
|
72
|
+
if (!isPlainRecord(value))
|
|
73
|
+
return null;
|
|
74
|
+
const parameters = normalizeModelParameterValues(value.parameterValues ?? value.parameter_values ?? []);
|
|
75
|
+
if (!parameters)
|
|
76
|
+
return null;
|
|
77
|
+
const key = optionalString(value, ["key"]);
|
|
78
|
+
const displayName = optionalString(value, ["displayName", "display_name"]);
|
|
79
|
+
const isDefaultNonMax = optionalBoolean(value, ["isDefaultNonMax", "isDefaultNonMaxConfig", "is_default_non_max_config"]);
|
|
80
|
+
const isDefaultMax = optionalBoolean(value, ["isDefaultMax", "isDefaultMaxConfig", "is_default_max_config"]);
|
|
81
|
+
if (key === null ||
|
|
82
|
+
displayName === null ||
|
|
83
|
+
isDefaultNonMax === null ||
|
|
84
|
+
isDefaultMax === null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if ((key !== undefined && !isNonEmptyString(key)) ||
|
|
88
|
+
(displayName !== undefined && !isNonEmptyString(displayName))) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
key: key ?? fallbackName,
|
|
93
|
+
displayName: displayName ?? fallbackName,
|
|
94
|
+
parameterValues: parameters,
|
|
95
|
+
isDefaultNonMax: isDefaultNonMax ?? false,
|
|
96
|
+
isDefaultMax: isDefaultMax ?? false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function normalizeModelInfo(value) {
|
|
100
|
+
if (!isPlainRecord(value) || !isNonEmptyString(value.id))
|
|
101
|
+
return null;
|
|
102
|
+
const rawVariants = value.variants ?? [];
|
|
103
|
+
if (!Array.isArray(rawVariants))
|
|
104
|
+
return null;
|
|
105
|
+
const variants = rawVariants.map((variant) => normalizeVariant(variant, value.id));
|
|
106
|
+
if (variants.some((variant) => variant === null))
|
|
107
|
+
return null;
|
|
108
|
+
const displayName = optionalString(value, ["displayName", "display_name"]);
|
|
109
|
+
const family = optionalString(value, ["family"]);
|
|
110
|
+
const supportsThinking = optionalBoolean(value, ["supportsThinking", "supports_thinking"]);
|
|
111
|
+
const supportsAgent = optionalBoolean(value, ["supportsAgent", "supports_agent"]);
|
|
112
|
+
const supportsMaxMode = optionalBoolean(value, ["supportsMaxMode", "supports_max_mode"]);
|
|
113
|
+
const maxContext = optionalPositiveNumber(value, ["maxContext", "contextTokenLimit", "context_token_limit"]);
|
|
114
|
+
const maxContextForMaxMode = optionalPositiveNumber(value, ["maxContextForMaxMode", "contextTokenLimitForMaxMode", "context_token_limit_for_max_mode"]);
|
|
115
|
+
if (displayName === null ||
|
|
116
|
+
family === null ||
|
|
117
|
+
supportsThinking === null ||
|
|
118
|
+
supportsAgent === null ||
|
|
119
|
+
supportsMaxMode === null ||
|
|
120
|
+
maxContext === null ||
|
|
121
|
+
maxContextForMaxMode === null) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
id: value.id,
|
|
126
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
127
|
+
...(family === undefined ? {} : { family }),
|
|
128
|
+
...(supportsThinking === undefined ? {} : { supportsThinking }),
|
|
129
|
+
...(supportsAgent === undefined ? {} : { supportsAgent }),
|
|
130
|
+
...(maxContext === undefined ? {} : { maxContext }),
|
|
131
|
+
...(maxContextForMaxMode === undefined ? {} : { maxContextForMaxMode }),
|
|
132
|
+
...(supportsMaxMode === undefined ? {} : { supportsMaxMode }),
|
|
133
|
+
variants: variants,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function normalizeModelCache(value) {
|
|
137
|
+
if (!isPlainRecord(value) || !Array.isArray(value.models))
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof value.fetchedAt !== "number" || !Number.isFinite(value.fetchedAt))
|
|
140
|
+
return null;
|
|
141
|
+
if (value.schemaVersion !== undefined &&
|
|
142
|
+
(!Number.isSafeInteger(value.schemaVersion) || value.schemaVersion < 0)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const models = value.models.map(normalizeModelInfo);
|
|
146
|
+
if (models.some((model) => model === null))
|
|
147
|
+
return null;
|
|
148
|
+
return {
|
|
149
|
+
models: models,
|
|
150
|
+
fetchedAt: value.fetchedAt,
|
|
151
|
+
...(value.schemaVersion === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { schemaVersion: value.schemaVersion }),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
11
156
|
/**
|
|
12
157
|
* Read only the dedicated variant payload generated by the plugin. OpenCode
|
|
13
158
|
* merges model, agent, and variant options into one providerOptions namespace;
|
|
@@ -15,17 +160,12 @@ export const CURSOR_WIRE_MODEL_ID_KEY = "cursorModelId";
|
|
|
15
160
|
* onto the wire.
|
|
16
161
|
*/
|
|
17
162
|
export function extractCursorVariantParameters(providerOptions) {
|
|
18
|
-
|
|
19
|
-
if (!Array.isArray(raw) || raw.length === 0)
|
|
163
|
+
if (!providerOptions || !Object.hasOwn(providerOptions, CURSOR_VARIANT_PARAMETERS_KEY)) {
|
|
20
164
|
return undefined;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const { id, value } = item;
|
|
26
|
-
if (typeof id !== "string" || typeof value !== "string")
|
|
27
|
-
return undefined;
|
|
28
|
-
params.push({ id, value });
|
|
165
|
+
}
|
|
166
|
+
const params = normalizeModelParameterValues(providerOptions[CURSOR_VARIANT_PARAMETERS_KEY]);
|
|
167
|
+
if (!params) {
|
|
168
|
+
throw new CursorVariantSelectionError("is malformed: cursorVariantParameters must be a parameter array");
|
|
29
169
|
}
|
|
30
170
|
return params;
|
|
31
171
|
}
|
|
@@ -35,7 +175,8 @@ export function resolveCursorWireModelId(providerOptions, fallback) {
|
|
|
35
175
|
}
|
|
36
176
|
/** True when resolved params select the long-context (1m) tier. */
|
|
37
177
|
export function paramsImplyMaxMode(params) {
|
|
38
|
-
return params.some((
|
|
178
|
+
return params.some((parameter) => parameter.id === "context" &&
|
|
179
|
+
parseCursorContextLimit(parameter.value) === 1_000_000);
|
|
39
180
|
}
|
|
40
181
|
// Cursor encodes a model's context window as a variant parameter `id: "context"`
|
|
41
182
|
// whose value is a tier string — the same 200k / 272k / 300k / 1m the IDE's
|
|
@@ -44,21 +185,30 @@ export function paramsImplyMaxMode(params) {
|
|
|
44
185
|
// `context_token_limit` (#15) / `context_token_limit_for_max_mode` (#16) are
|
|
45
186
|
// also defined on AvailableModel but the server often leaves them empty — the
|
|
46
187
|
// variant param is the primary source (request flags may still populate them).
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
188
|
+
export function parseCursorContextLimit(value) {
|
|
189
|
+
if (typeof value !== "string")
|
|
190
|
+
return undefined;
|
|
191
|
+
const text = value.trim();
|
|
192
|
+
const match = /^(\d+(?:\.\d+)?)\s*([km])$/i.exec(text);
|
|
193
|
+
if (match) {
|
|
194
|
+
const multiplier = match[2].toLowerCase() === "k" ? 1_000 : 1_000_000;
|
|
195
|
+
const limit = Number(match[1]) * multiplier;
|
|
196
|
+
return Number.isSafeInteger(limit) && limit > 0 ? limit : undefined;
|
|
197
|
+
}
|
|
198
|
+
const numeric = Number(text);
|
|
199
|
+
return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : undefined;
|
|
200
|
+
}
|
|
53
201
|
function variantContextTokens(v) {
|
|
54
202
|
const raw = v?.parameterValues.find((p) => p.id === "context")?.value;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return Number.isFinite(
|
|
203
|
+
return parseCursorContextLimit(raw);
|
|
204
|
+
}
|
|
205
|
+
function isLongContextVariant(v) {
|
|
206
|
+
return variantContextTokens(v) === 1_000_000;
|
|
207
|
+
}
|
|
208
|
+
function positiveNumber(value) {
|
|
209
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
210
|
+
? value
|
|
211
|
+
: undefined;
|
|
62
212
|
}
|
|
63
213
|
// ── Cache helpers ──
|
|
64
214
|
export function isCacheFresh(cache, ttlMs = MODEL_CACHE_TTL_MS) {
|
|
@@ -72,43 +222,66 @@ export function cacheFilePath(cacheDir) {
|
|
|
72
222
|
export async function readCache(cacheDir) {
|
|
73
223
|
const filePath = cacheFilePath(cacheDir);
|
|
74
224
|
try {
|
|
75
|
-
if (!existsSync(filePath))
|
|
76
|
-
return null;
|
|
77
225
|
const data = await readFile(filePath, "utf-8");
|
|
78
|
-
return JSON.parse(data);
|
|
226
|
+
return normalizeModelCache(JSON.parse(data));
|
|
79
227
|
}
|
|
80
228
|
catch {
|
|
81
229
|
return null;
|
|
82
230
|
}
|
|
83
231
|
}
|
|
84
232
|
export async function writeCache(cacheDir, cache) {
|
|
233
|
+
const normalized = normalizeModelCache(cache);
|
|
234
|
+
if (!normalized)
|
|
235
|
+
throw new Error("Refusing to write an invalid Cursor model cache");
|
|
85
236
|
const filePath = cacheFilePath(cacheDir);
|
|
86
|
-
|
|
87
|
-
|
|
237
|
+
const directory = path.dirname(filePath);
|
|
238
|
+
const tempPath = path.join(directory, `.${MODEL_CACHE_FILE}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
239
|
+
await mkdir(directory, { recursive: true });
|
|
240
|
+
try {
|
|
241
|
+
await writeFile(tempPath, JSON.stringify(normalized, null, 2), "utf-8");
|
|
242
|
+
await rename(tempPath, filePath);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await unlink(tempPath).catch(() => { });
|
|
246
|
+
}
|
|
88
247
|
}
|
|
89
248
|
// ── Map live API response to ModelInfo[] ──
|
|
249
|
+
function apiBoolean(record, names) {
|
|
250
|
+
const value = optionalBoolean(record, names);
|
|
251
|
+
if (value === null)
|
|
252
|
+
throw new Error(`AvailableModels returned a non-boolean ${names[0]}`);
|
|
253
|
+
return value ?? false;
|
|
254
|
+
}
|
|
90
255
|
export function mapAvailableModelsResponse(raw) {
|
|
91
256
|
const entries = raw.models ?? [];
|
|
92
257
|
const models = [];
|
|
93
258
|
for (const entry of entries) {
|
|
259
|
+
if (!isPlainRecord(entry))
|
|
260
|
+
continue;
|
|
94
261
|
const e = entry;
|
|
95
262
|
const name = e.name;
|
|
96
|
-
if (!name)
|
|
263
|
+
if (!isNonEmptyString(name))
|
|
97
264
|
continue;
|
|
98
265
|
const variants = [];
|
|
99
|
-
const rawVariants =
|
|
266
|
+
const rawVariants = e.variants ?? [];
|
|
267
|
+
if (!Array.isArray(rawVariants)) {
|
|
268
|
+
throw new Error(`AvailableModels returned invalid variants for ${name}`);
|
|
269
|
+
}
|
|
100
270
|
for (const v of rawVariants) {
|
|
271
|
+
if (!isPlainRecord(v)) {
|
|
272
|
+
throw new Error(`AvailableModels returned an invalid variant for ${name}`);
|
|
273
|
+
}
|
|
101
274
|
const rawParams = (v.parameterValues ?? v.parameter_values ?? []);
|
|
102
|
-
const parameterValues = rawParams
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
275
|
+
const parameterValues = normalizeModelParameterValues(rawParams);
|
|
276
|
+
if (!parameterValues) {
|
|
277
|
+
throw new Error(`AvailableModels returned invalid parameter values for ${name}`);
|
|
278
|
+
}
|
|
106
279
|
variants.push({
|
|
107
280
|
key: name,
|
|
108
281
|
parameterValues,
|
|
109
282
|
displayName: (v.displayName ?? v.display_name ?? name),
|
|
110
|
-
isDefaultNonMax:
|
|
111
|
-
isDefaultMax:
|
|
283
|
+
isDefaultNonMax: apiBoolean(v, ["isDefaultNonMaxConfig", "is_default_non_max_config"]),
|
|
284
|
+
isDefaultMax: apiBoolean(v, ["isDefaultMaxConfig", "is_default_max_config"]),
|
|
112
285
|
});
|
|
113
286
|
}
|
|
114
287
|
// Derive the context window from the variant `context` param (primary
|
|
@@ -117,17 +290,18 @@ export function mapAvailableModelsResponse(raw) {
|
|
|
117
290
|
// Prefer a non-1m context when isDefaultNonMax is missing so a leading
|
|
118
291
|
// max-mode variant cannot inflate the base window.
|
|
119
292
|
const variantBaseContext = variantContextTokens(variants.find((v) => v.isDefaultNonMax)) ??
|
|
120
|
-
variantContextTokens(variants.find((v) => v
|
|
293
|
+
variantContextTokens(variants.find((v) => variantContextTokens(v) !== 1_000_000)) ??
|
|
121
294
|
variantContextTokens(variants.find((v) => v.parameterValues.some((p) => p.id === "context")));
|
|
122
295
|
const variantMaxContext = variantContextTokens(variants.find((v) => v.isDefaultMax));
|
|
123
296
|
models.push({
|
|
124
297
|
id: name,
|
|
125
298
|
displayName: (e.clientDisplayName ?? e.client_display_name ?? name),
|
|
126
|
-
supportsThinking:
|
|
127
|
-
supportsAgent:
|
|
128
|
-
maxContext:
|
|
129
|
-
maxContextForMaxMode:
|
|
130
|
-
|
|
299
|
+
supportsThinking: apiBoolean(e, ["supportsThinking", "supports_thinking"]),
|
|
300
|
+
supportsAgent: apiBoolean(e, ["supportsAgent", "supports_agent"]),
|
|
301
|
+
maxContext: variantBaseContext ?? positiveNumber(e.contextTokenLimit ?? e.context_token_limit),
|
|
302
|
+
maxContextForMaxMode: variantMaxContext ??
|
|
303
|
+
positiveNumber(e.contextTokenLimitForMaxMode ?? e.context_token_limit_for_max_mode),
|
|
304
|
+
supportsMaxMode: apiBoolean(e, ["supportsMaxMode", "supports_max_mode"]),
|
|
131
305
|
variants,
|
|
132
306
|
});
|
|
133
307
|
}
|
|
@@ -148,34 +322,34 @@ export function mapAvailableModelsResponse(raw) {
|
|
|
148
322
|
* so the picked values are already isolated and can be matched verbatim.
|
|
149
323
|
*/
|
|
150
324
|
export function resolveVariantParameters(model, opts = {}) {
|
|
151
|
-
const picked = opts.picked?.
|
|
325
|
+
const picked = opts.picked?.map((p) => ({ ...p }));
|
|
152
326
|
if (!model || model.variants.length === 0) {
|
|
153
|
-
if (picked
|
|
154
|
-
|
|
327
|
+
if (picked !== undefined) {
|
|
328
|
+
throw new CursorVariantSelectionError(`is stale: model ${JSON.stringify(model?.id ?? "unknown")} has no cached variants`);
|
|
329
|
+
}
|
|
155
330
|
return opts.reasoningEffort ? [{ id: "effort", value: opts.reasoningEffort }] : [];
|
|
156
331
|
}
|
|
157
332
|
const effortOf = (v) => v.parameterValues.find((p) => p.id === "effort" || p.id === "reasoning")?.value;
|
|
158
333
|
const isFast = (v) => v.parameterValues.find((p) => p.id === "fast")?.value === "true";
|
|
159
|
-
const
|
|
160
|
-
const isMaxVariant = (v) => v.isDefaultMax || contextOf(v) === "1m";
|
|
334
|
+
const isMaxVariant = (v) => v.isDefaultMax || isLongContextVariant(v);
|
|
161
335
|
const wantMax = opts.maxMode ?? false;
|
|
162
336
|
// Non-fast pool for hint-based resolution only. Exact `picked` matching
|
|
163
337
|
// searches all variants so Fast selections are not silently dropped.
|
|
164
338
|
const pool = model.variants.filter((v) => !isFast(v));
|
|
165
339
|
const scoped = pool.length > 0 ? pool : model.variants;
|
|
166
340
|
// 1. Variant explicitly picked by opencode (verbatim — preserves context, fast, …).
|
|
167
|
-
if (picked
|
|
168
|
-
const
|
|
169
|
-
const exact = model.variants.find((v) => v.parameterValues.
|
|
170
|
-
|
|
341
|
+
if (picked !== undefined) {
|
|
342
|
+
const pickedById = new Map(picked.map((parameter) => [parameter.id, parameter.value]));
|
|
343
|
+
const exact = model.variants.find((v) => v.parameterValues.length === picked.length &&
|
|
344
|
+
pickedById.size === picked.length &&
|
|
345
|
+
v.parameterValues.every((parameter) => pickedById.get(parameter.id) === parameter.value));
|
|
171
346
|
if (exact) {
|
|
172
347
|
return buildRequestedModelParams(exact.parameterValues, {
|
|
173
348
|
reasoningEffort: opts.reasoningEffort,
|
|
174
349
|
maxMode: wantMax,
|
|
175
350
|
});
|
|
176
351
|
}
|
|
177
|
-
|
|
178
|
-
return picked;
|
|
352
|
+
throw new CursorVariantSelectionError(`is stale for model ${JSON.stringify(model.id)}: its exact parameter tuple is unavailable`);
|
|
179
353
|
}
|
|
180
354
|
// When maxMode is requested, prefer max-tier variants for effort matching
|
|
181
355
|
// so "high + max" lands on 1m high rather than the first 300k high.
|
|
@@ -193,7 +367,7 @@ export function resolveVariantParameters(model, opts = {}) {
|
|
|
193
367
|
}
|
|
194
368
|
// 3. Max-mode hint → prefer the default max variant (1m context).
|
|
195
369
|
if (wantMax) {
|
|
196
|
-
const max = scoped.find((v) => v.isDefaultMax) ?? scoped.find(
|
|
370
|
+
const max = scoped.find((v) => v.isDefaultMax) ?? scoped.find(isLongContextVariant);
|
|
197
371
|
if (max)
|
|
198
372
|
return buildRequestedModelParams(max.parameterValues, { maxMode: true });
|
|
199
373
|
}
|
|
@@ -209,17 +383,37 @@ export async function fetchModels(token, options = {}) {
|
|
|
209
383
|
const raw = await unaryAvailableModels(token, options);
|
|
210
384
|
return mapAvailableModelsResponse(raw);
|
|
211
385
|
}
|
|
386
|
+
const refreshesByDirectory = new Map();
|
|
387
|
+
export async function refreshModelCache(cacheDir, fetcher) {
|
|
388
|
+
const key = path.resolve(cacheDir);
|
|
389
|
+
const existing = refreshesByDirectory.get(key);
|
|
390
|
+
if (existing)
|
|
391
|
+
return existing;
|
|
392
|
+
const refresh = (async () => {
|
|
393
|
+
const models = await fetcher();
|
|
394
|
+
await writeCache(cacheDir, {
|
|
395
|
+
models,
|
|
396
|
+
fetchedAt: Date.now(),
|
|
397
|
+
schemaVersion: MODEL_CACHE_SCHEMA_VERSION,
|
|
398
|
+
});
|
|
399
|
+
return models;
|
|
400
|
+
})();
|
|
401
|
+
refreshesByDirectory.set(key, refresh);
|
|
402
|
+
try {
|
|
403
|
+
return await refresh;
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
if (refreshesByDirectory.get(key) === refresh)
|
|
407
|
+
refreshesByDirectory.delete(key);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
212
410
|
export async function discoverModels(token, cacheDir, options = {}) {
|
|
213
411
|
const cached = await readCache(cacheDir);
|
|
412
|
+
const refresh = () => refreshModelCache(cacheDir, () => fetchModels(token, options));
|
|
214
413
|
// Cache is fresh → return it; refresh in background
|
|
215
414
|
if (cached && isCacheFresh(cached)) {
|
|
216
415
|
// Background refresh (fire and forget)
|
|
217
|
-
|
|
218
|
-
.then((models) => writeCache(cacheDir, {
|
|
219
|
-
models,
|
|
220
|
-
fetchedAt: Date.now(),
|
|
221
|
-
schemaVersion: MODEL_CACHE_SCHEMA_VERSION,
|
|
222
|
-
}))
|
|
416
|
+
void refresh()
|
|
223
417
|
.catch(() => {
|
|
224
418
|
/* background refresh failure is non-fatal */
|
|
225
419
|
});
|
|
@@ -228,26 +422,12 @@ export async function discoverModels(token, cacheDir, options = {}) {
|
|
|
228
422
|
// Cache exists but expired → try fetch, serve stale on failure
|
|
229
423
|
if (cached) {
|
|
230
424
|
try {
|
|
231
|
-
|
|
232
|
-
const newCache = {
|
|
233
|
-
models,
|
|
234
|
-
fetchedAt: Date.now(),
|
|
235
|
-
schemaVersion: MODEL_CACHE_SCHEMA_VERSION,
|
|
236
|
-
};
|
|
237
|
-
await writeCache(cacheDir, newCache);
|
|
238
|
-
return models;
|
|
425
|
+
return await refresh();
|
|
239
426
|
}
|
|
240
427
|
catch {
|
|
241
428
|
return cached.models;
|
|
242
429
|
}
|
|
243
430
|
}
|
|
244
431
|
// No cache → must fetch
|
|
245
|
-
|
|
246
|
-
const newCache = {
|
|
247
|
-
models,
|
|
248
|
-
fetchedAt: Date.now(),
|
|
249
|
-
schemaVersion: MODEL_CACHE_SCHEMA_VERSION,
|
|
250
|
-
};
|
|
251
|
-
await writeCache(cacheDir, newCache);
|
|
252
|
-
return models;
|
|
432
|
+
return refresh();
|
|
253
433
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { CURSOR_COMPACTION_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
|
|
2
|
-
import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl,
|
|
3
|
-
import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, readCache, discoverModels, isCacheFresh } from "./models.js";
|
|
2
|
+
import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs } from "./auth.js";
|
|
3
|
+
import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, readCache, discoverModels, isCacheFresh, parseCursorContextLimit } from "./models.js";
|
|
4
4
|
import { opencodeGlobalCacheDir } from "./context/paths.js";
|
|
5
5
|
import { readStoredAuth } from "./context/auth-store.js";
|
|
6
6
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
7
|
+
import { captureCursorShellResult, cursorShellOriginalCommand, prepareCursorShellArgs, } from "./shell-timeout.js";
|
|
8
|
+
import { sessionActivity } from "./activity.js";
|
|
7
9
|
const MODULE_URL = new URL("./index.js", import.meta.url).href;
|
|
8
10
|
/**
|
|
9
11
|
* Strip characters and markup that break rendering in the OpenCode TUI/GUI from
|
|
@@ -85,7 +87,7 @@ function modelInfoVariants(mi, variants) {
|
|
|
85
87
|
return entries;
|
|
86
88
|
}
|
|
87
89
|
function isLongContextVariant(v) {
|
|
88
|
-
return v.parameterValues.some((p) => p.id === "context" && p.value ===
|
|
90
|
+
return v.parameterValues.some((p) => p.id === "context" && parseCursorContextLimit(p.value) === 1_000_000);
|
|
89
91
|
}
|
|
90
92
|
function variantsForTier(mi, tier) {
|
|
91
93
|
return mi.variants.filter((v) => isLongContextVariant(v) === (tier === "long"));
|
|
@@ -273,7 +275,7 @@ export async function CursorPlugin(input) {
|
|
|
273
275
|
type: "oauth",
|
|
274
276
|
access: newTokens.accessToken,
|
|
275
277
|
refresh: newTokens.refreshToken,
|
|
276
|
-
expires:
|
|
278
|
+
expires: decodeJwtExpiryMs(newTokens.accessToken) ?? Date.now(),
|
|
277
279
|
...(extras.accountId !== undefined ? { accountId: extras.accountId } : {}),
|
|
278
280
|
...(extras.enterpriseUrl !== undefined ? { enterpriseUrl: extras.enterpriseUrl } : {}),
|
|
279
281
|
});
|
|
@@ -311,6 +313,37 @@ export async function CursorPlugin(input) {
|
|
|
311
313
|
return cached?.models.length ? modelsToConfig(cached.models) : {};
|
|
312
314
|
}
|
|
313
315
|
return {
|
|
316
|
+
async event({ event }) {
|
|
317
|
+
switch (event.type) {
|
|
318
|
+
case "session.created":
|
|
319
|
+
sessionActivity.linkSession(event.properties.info.id, event.properties.info.parentID);
|
|
320
|
+
sessionActivity.recordActivity(event.properties.info.id);
|
|
321
|
+
break;
|
|
322
|
+
case "session.updated":
|
|
323
|
+
sessionActivity.linkSession(event.properties.info.id, event.properties.info.parentID);
|
|
324
|
+
break;
|
|
325
|
+
case "session.deleted":
|
|
326
|
+
sessionActivity.removeSession(event.properties.info.id);
|
|
327
|
+
break;
|
|
328
|
+
case "message.updated":
|
|
329
|
+
sessionActivity.recordActivity(event.properties.info.sessionID);
|
|
330
|
+
break;
|
|
331
|
+
case "message.part.updated":
|
|
332
|
+
sessionActivity.recordActivity(event.properties.part.sessionID);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
async "tool.execute.before"(hookInput, output) {
|
|
337
|
+
if (hookInput.tool !== "bash")
|
|
338
|
+
return;
|
|
339
|
+
prepareCursorShellArgs(hookInput.callID, output.args);
|
|
340
|
+
},
|
|
341
|
+
async "tool.execute.after"(hookInput, output) {
|
|
342
|
+
if (hookInput.tool !== "bash")
|
|
343
|
+
return;
|
|
344
|
+
output.title = cursorShellOriginalCommand(hookInput.callID) ?? output.title;
|
|
345
|
+
output.output = captureCursorShellResult(hookInput.callID, output.output, output.metadata);
|
|
346
|
+
},
|
|
314
347
|
async "chat.params"(hookInput, output) {
|
|
315
348
|
if (hookInput.model.providerID !== CURSOR_PROVIDER_ID)
|
|
316
349
|
return;
|
|
@@ -364,7 +397,7 @@ export async function CursorPlugin(input) {
|
|
|
364
397
|
provider: CURSOR_PROVIDER_ID,
|
|
365
398
|
access: result.accessToken,
|
|
366
399
|
refresh: result.refreshToken,
|
|
367
|
-
expires:
|
|
400
|
+
expires: decodeJwtExpiryMs(result.accessToken) ?? Date.now(),
|
|
368
401
|
};
|
|
369
402
|
},
|
|
370
403
|
};
|
|
@@ -436,9 +469,3 @@ export async function CursorPlugin(input) {
|
|
|
436
469
|
},
|
|
437
470
|
};
|
|
438
471
|
}
|
|
439
|
-
function decodeExpFromJwt(jwt) {
|
|
440
|
-
const payload = decodeJwtPayload(jwt);
|
|
441
|
-
if (payload && typeof payload.exp === "number")
|
|
442
|
-
return payload.exp * 1000;
|
|
443
|
-
return Date.now() + 3600_000;
|
|
444
|
-
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Cursor agent.v1 exec request/result pairs.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth: Cursor CLI generated `agent/v1/exec_pb.js` plus the
|
|
5
|
+
* registrations in `agent-exec/dist/index.js`. Most request/result variants
|
|
6
|
+
* share a field number; Pi requests deliberately use result field +1.
|
|
7
|
+
*
|
|
8
|
+
* `handling` describes this provider, not the Cursor CLI:
|
|
9
|
+
* - opencode-tool: emitted through an advertised OpenCode tool
|
|
10
|
+
* - provider-control: answered directly on the held-open Run stream
|
|
11
|
+
* - unsupported: known Cursor-native capability with no safe AI SDK bridge
|
|
12
|
+
*/
|
|
13
|
+
export type CursorExecHandling = "opencode-tool" | "provider-control" | "unsupported";
|
|
14
|
+
export type CursorExecVariant = {
|
|
15
|
+
requestField: number;
|
|
16
|
+
requestName: string;
|
|
17
|
+
resultField: number;
|
|
18
|
+
resultName: string;
|
|
19
|
+
handling: CursorExecHandling;
|
|
20
|
+
};
|
|
21
|
+
export declare const CURSOR_EXEC_VARIANTS: readonly CursorExecVariant[];
|
|
22
|
+
export declare function cursorExecVariantByRequestField(field: number): CursorExecVariant | undefined;
|
|
23
|
+
export declare function cursorExecVariantByRequestName(name: string): CursorExecVariant | undefined;
|
|
24
|
+
export declare function describeCursorExecVariant(field: number | undefined): string;
|