cursor-openai-byok 1.0.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/README.md +252 -0
- package/TROUBLESHOOTING.md +84 -0
- package/bin/cursor-openai-byok.js +9 -0
- package/config.example.json +48 -0
- package/lib/cli.js +1888 -0
- package/lib/daemon-entry.js +1128 -0
- package/lib/extension.js +302 -0
- package/package.json +77 -0
- package/scripts/install.ps1 +3 -0
- package/scripts/install.sh +4 -0
- package/scripts/uninstall.ps1 +3 -0
- package/scripts/uninstall.sh +4 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,1888 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const __bundlePath = require("path");
|
|
4
|
+
const __bundleRoot = __bundlePath.resolve(__dirname, "..");
|
|
5
|
+
const __bundleModules = {
|
|
6
|
+
"src/cli/index.js": function(module, exports, require, __filename, __dirname) {
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const { configPath } = require("../paths");
|
|
13
|
+
const { DEFAULT_CONFIG, writeDefaultConfig, loadConfig, listModels, redactConfig } = require("../config");
|
|
14
|
+
const { syncCursorStorage } = require("../cursor-models");
|
|
15
|
+
const { tailLog } = require("../logger");
|
|
16
|
+
const service = require("./service");
|
|
17
|
+
const patcher = require("../patcher");
|
|
18
|
+
|
|
19
|
+
async function main(args) {
|
|
20
|
+
const command = args[0] || "help";
|
|
21
|
+
const options = parseOptions(args.slice(1));
|
|
22
|
+
switch (command) {
|
|
23
|
+
case "help": return help();
|
|
24
|
+
case "configure": return configure(options);
|
|
25
|
+
case "daemon": return service.startForeground();
|
|
26
|
+
case "start": return print(service.startBackground());
|
|
27
|
+
case "stop": return print(service.stopBackground());
|
|
28
|
+
case "restart":
|
|
29
|
+
service.stopBackground();
|
|
30
|
+
return print(service.startBackground());
|
|
31
|
+
case "install": return install(options);
|
|
32
|
+
case "uninstall": return uninstall(options);
|
|
33
|
+
case "doctor": return doctor(options);
|
|
34
|
+
case "logs": return process.stdout.write(tailLog(Number(options.lines || 160)) + "\n");
|
|
35
|
+
case "models": return print({ models: listModels(loadConfig()) });
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unknown command ${command}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseOptions(args) {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const arg of args) {
|
|
44
|
+
if (arg === "--force") out.force = true;
|
|
45
|
+
else if (arg === "--import-ccursor") out.importCcursor = true;
|
|
46
|
+
else if (arg.startsWith("--app-root=")) out.appRoot = arg.slice("--app-root=".length);
|
|
47
|
+
else if (arg.startsWith("--lines=")) out.lines = arg.slice("--lines=".length);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function help() {
|
|
53
|
+
console.log(`cursor-openai-byok
|
|
54
|
+
|
|
55
|
+
Commands:
|
|
56
|
+
install Install Cursor patch and lifecycle extension
|
|
57
|
+
uninstall Remove extension and restore Cursor backup
|
|
58
|
+
configure Create default config if missing
|
|
59
|
+
configure --import-ccursor
|
|
60
|
+
Import ~/.ccursor/providers.json into this config
|
|
61
|
+
start|stop|restart Manage local daemon manually
|
|
62
|
+
daemon Run daemon in foreground
|
|
63
|
+
doctor Check config, daemon, Cursor patch, and provider routing
|
|
64
|
+
models Print configured models
|
|
65
|
+
logs [--lines=N] Print daemon log tail
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--app-root=PATH Cursor resources/app path
|
|
69
|
+
--force Allow unsupported Cursor version or legacy patch replacement
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function configure(options = {}) {
|
|
74
|
+
const file = options.importCcursor ? importCcursorConfig() : writeDefaultConfig();
|
|
75
|
+
console.log(`Config ready: ${file}`);
|
|
76
|
+
console.log("Edit apiKey/baseUrl/models before installing if this is a new setup.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function importCcursorConfig() {
|
|
80
|
+
const source = path.join(os.homedir(), ".ccursor", "providers.json");
|
|
81
|
+
if (!fs.existsSync(source)) throw new Error(`${source} not found`);
|
|
82
|
+
const ccursor = JSON.parse(fs.readFileSync(source, "utf8"));
|
|
83
|
+
const providers = (ccursor.providers || []).map((provider) => ({
|
|
84
|
+
id: provider.name || provider.id,
|
|
85
|
+
name: provider.name || provider.id,
|
|
86
|
+
type: provider.type === "openai-chat-completions" ? "openai-chat-completions" : "openai-responses",
|
|
87
|
+
baseUrl: provider.baseUrl,
|
|
88
|
+
apiKey: provider.auth?.value || provider.apiKey,
|
|
89
|
+
models: (provider.models || []).map((model) => ({
|
|
90
|
+
displayName: model.displayName || model.id || model.apiModel,
|
|
91
|
+
apiModel: model.apiModel || model.displayName || model.id,
|
|
92
|
+
supportsTools: model.supportsTools !== false,
|
|
93
|
+
supportsStreaming: model.supportsStreaming !== false,
|
|
94
|
+
contextTokenLimit: model.contextTokenLimit || model.contextTokenLimitForMaxMode || 128000,
|
|
95
|
+
maxOutputTokens: model.maxOutputTokens || 8192,
|
|
96
|
+
})),
|
|
97
|
+
})).filter((provider) => provider.id && provider.baseUrl && provider.models.length > 0);
|
|
98
|
+
if (providers.length === 0) throw new Error(`${source} does not contain usable providers`);
|
|
99
|
+
const config = {
|
|
100
|
+
...DEFAULT_CONFIG,
|
|
101
|
+
defaults: { model: providers[0].models[0].displayName },
|
|
102
|
+
providers,
|
|
103
|
+
};
|
|
104
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
105
|
+
fs.writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
106
|
+
return configPath();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function install(options) {
|
|
110
|
+
writeDefaultConfig();
|
|
111
|
+
const patch = patcher.installPatch(options);
|
|
112
|
+
const extension = service.installExtension();
|
|
113
|
+
const storage = syncCursorStorage(listModels(loadConfig()));
|
|
114
|
+
print({ config: configPath(), patch, extension, storage });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function uninstall(options) {
|
|
118
|
+
const stopped = service.stopBackground();
|
|
119
|
+
const extension = service.uninstallExtension();
|
|
120
|
+
const patch = patcher.uninstallPatch(options);
|
|
121
|
+
print({ stopped, extension, patch });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function doctor(options) {
|
|
125
|
+
const checks = [];
|
|
126
|
+
let config;
|
|
127
|
+
try {
|
|
128
|
+
config = loadConfig();
|
|
129
|
+
checks.push({ name: "config", ok: true, path: configPath(), models: listModels(config).map((m) => m.displayName) });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
checks.push({ name: "config", ok: false, error: err.message });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (config) {
|
|
135
|
+
checks.push({ name: "config-redacted", ok: true, config: redactConfig(config) });
|
|
136
|
+
try {
|
|
137
|
+
checks.push({ name: "cursor-model-storage", ok: true, ...syncCursorStorage(listModels(config)) });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
checks.push({ name: "cursor-model-storage", ok: false, error: err.message });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const st = patcher.status(options);
|
|
145
|
+
checks.push({ name: "cursor", ok: st.supportedVersion, ...st });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
checks.push({ name: "cursor", ok: false, error: err.message });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
checks.push({ name: "extension", ok: service.extensionStatus().installed, ...service.extensionStatus() });
|
|
151
|
+
const runningPid = service.isRunning();
|
|
152
|
+
checks.push({
|
|
153
|
+
name: "daemon-process",
|
|
154
|
+
ok: true,
|
|
155
|
+
running: !!runningPid,
|
|
156
|
+
pid: runningPid || undefined,
|
|
157
|
+
note: runningPid ? "server is running" : "server is expected to run only while Cursor extension is active",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const cfg = config || loadConfig();
|
|
162
|
+
const host = cfg.server?.host || "127.0.0.1";
|
|
163
|
+
const port = cfg.server?.port || 39832;
|
|
164
|
+
const res = await fetch(`http://${host}:${port}/health`);
|
|
165
|
+
const json = await res.json();
|
|
166
|
+
checks.push({ name: "daemon-health", ok: res.ok && json.ok, status: res.status, health: json });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
checks.push({
|
|
169
|
+
name: "daemon-health",
|
|
170
|
+
ok: !runningPid,
|
|
171
|
+
running: false,
|
|
172
|
+
error: err.message,
|
|
173
|
+
note: runningPid ? "server pid exists but health check failed" : "health is skipped when Cursor is not running",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
print({ ok: checks.every((c) => c.ok), checks });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function print(value) {
|
|
181
|
+
console.log(JSON.stringify(value, null, 2));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { main };
|
|
185
|
+
|
|
186
|
+
},
|
|
187
|
+
"src/paths.js": function(module, exports, require, __filename, __dirname) {
|
|
188
|
+
"use strict";
|
|
189
|
+
|
|
190
|
+
const os = require("os");
|
|
191
|
+
const path = require("path");
|
|
192
|
+
const fs = require("fs");
|
|
193
|
+
|
|
194
|
+
const APP_NAME = "cursor-openai-byok";
|
|
195
|
+
const DEFAULT_PORT = 39832;
|
|
196
|
+
|
|
197
|
+
function homeDir() {
|
|
198
|
+
return os.homedir();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function configDir() {
|
|
202
|
+
return path.join(homeDir(), `.${APP_NAME}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function configPath() {
|
|
206
|
+
return path.join(configDir(), "config.json");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function logPath() {
|
|
210
|
+
return path.join(configDir(), "daemon.log");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function pidPath() {
|
|
214
|
+
return path.join(configDir(), "daemon.pid");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function runtimePath(name) {
|
|
218
|
+
return path.join(configDir(), name);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function platformName() {
|
|
222
|
+
if (process.platform === "darwin") return "macos";
|
|
223
|
+
if (process.platform === "win32") return "windows";
|
|
224
|
+
return process.platform;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cursorExtensionsDir() {
|
|
228
|
+
return path.join(homeDir(), ".cursor", "extensions");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function installedExtensionDir() {
|
|
232
|
+
const version = require("../package.json").version;
|
|
233
|
+
return path.join(cursorExtensionsDir(), `cursor-openai-byok.cursor-openai-byok-${version}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ensureDir(dir) {
|
|
237
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
APP_NAME,
|
|
242
|
+
DEFAULT_PORT,
|
|
243
|
+
homeDir,
|
|
244
|
+
configDir,
|
|
245
|
+
configPath,
|
|
246
|
+
logPath,
|
|
247
|
+
pidPath,
|
|
248
|
+
runtimePath,
|
|
249
|
+
platformName,
|
|
250
|
+
cursorExtensionsDir,
|
|
251
|
+
installedExtensionDir,
|
|
252
|
+
ensureDir,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
},
|
|
256
|
+
"package.json": function(module, exports, require, __filename, __dirname) {
|
|
257
|
+
module.exports = {
|
|
258
|
+
"name": "cursor-openai-byok",
|
|
259
|
+
"version": "1.0.2",
|
|
260
|
+
"description": "Local BYOK bridge for Cursor using OpenAI-compatible providers.",
|
|
261
|
+
"displayName": "Cursor OpenAI BYOK",
|
|
262
|
+
"publisher": "cursor-openai-byok",
|
|
263
|
+
"type": "commonjs",
|
|
264
|
+
"main": "./lib/extension.js",
|
|
265
|
+
"repository": {
|
|
266
|
+
"type": "git",
|
|
267
|
+
"url": "git+https://git.3weijia.com/base/ai/cursor-openai-byok.git"
|
|
268
|
+
},
|
|
269
|
+
"keywords": [
|
|
270
|
+
"cursor",
|
|
271
|
+
"byok",
|
|
272
|
+
"openai",
|
|
273
|
+
"openai-compatible",
|
|
274
|
+
"local-bridge"
|
|
275
|
+
],
|
|
276
|
+
"files": [
|
|
277
|
+
"bin/",
|
|
278
|
+
"lib/",
|
|
279
|
+
"scripts/install.sh",
|
|
280
|
+
"scripts/uninstall.sh",
|
|
281
|
+
"scripts/install.ps1",
|
|
282
|
+
"scripts/uninstall.ps1",
|
|
283
|
+
"config.example.json",
|
|
284
|
+
"README.md",
|
|
285
|
+
"TROUBLESHOOTING.md"
|
|
286
|
+
],
|
|
287
|
+
"publishConfig": {
|
|
288
|
+
"access": "public",
|
|
289
|
+
"registry": "https://registry.npmjs.org/"
|
|
290
|
+
},
|
|
291
|
+
"activationEvents": [
|
|
292
|
+
"*"
|
|
293
|
+
],
|
|
294
|
+
"contributes": {
|
|
295
|
+
"commands": [
|
|
296
|
+
{
|
|
297
|
+
"command": "cursorOpenaiByok.openSettings",
|
|
298
|
+
"title": "Cursor OpenAI BYOK: Open Settings"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"command": "cursorOpenaiByok.startServer",
|
|
302
|
+
"title": "Cursor OpenAI BYOK: Start Server"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"command": "cursorOpenaiByok.stopServer",
|
|
306
|
+
"title": "Cursor OpenAI BYOK: Stop Server"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"command": "cursorOpenaiByok.restartServer",
|
|
310
|
+
"title": "Cursor OpenAI BYOK: Restart Server"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"command": "cursorOpenaiByok.showStatus",
|
|
314
|
+
"title": "Cursor OpenAI BYOK: Show Status"
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
},
|
|
318
|
+
"bin": {
|
|
319
|
+
"cursor-openai-byok": "bin/cursor-openai-byok.js"
|
|
320
|
+
},
|
|
321
|
+
"scripts": {
|
|
322
|
+
"build:npm": "node scripts/build-npm.js",
|
|
323
|
+
"test": "node --test",
|
|
324
|
+
"doctor": "node bin/cursor-openai-byok.js doctor",
|
|
325
|
+
"prepack": "npm run build:npm",
|
|
326
|
+
"package": "npm run build:npm && node scripts/package.js"
|
|
327
|
+
},
|
|
328
|
+
"engines": {
|
|
329
|
+
"vscode": "^1.99.0",
|
|
330
|
+
"node": ">=20"
|
|
331
|
+
},
|
|
332
|
+
"license": "UNLICENSED"
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
"src/config.js": function(module, exports, require, __filename, __dirname) {
|
|
336
|
+
"use strict";
|
|
337
|
+
|
|
338
|
+
const fs = require("fs");
|
|
339
|
+
const path = require("path");
|
|
340
|
+
const { configDir, configPath, DEFAULT_PORT } = require("./paths");
|
|
341
|
+
|
|
342
|
+
const DEFAULT_CONFIG = {
|
|
343
|
+
$schemaVersion: 1,
|
|
344
|
+
server: {
|
|
345
|
+
host: "127.0.0.1",
|
|
346
|
+
port: DEFAULT_PORT,
|
|
347
|
+
},
|
|
348
|
+
defaults: {
|
|
349
|
+
model: "fastskills-gpt-5.5",
|
|
350
|
+
},
|
|
351
|
+
providers: [
|
|
352
|
+
{
|
|
353
|
+
id: "fastskills",
|
|
354
|
+
name: "FastSkills",
|
|
355
|
+
type: "openai-responses",
|
|
356
|
+
baseUrl: "https://api-hw.fastskills.ai/v1",
|
|
357
|
+
apiKey: "sk-REPLACE_ME",
|
|
358
|
+
models: [
|
|
359
|
+
{
|
|
360
|
+
displayName: "fastskills-gpt-5.5",
|
|
361
|
+
apiModel: "gpt-5.5",
|
|
362
|
+
supportsTools: true,
|
|
363
|
+
supportsStreaming: true,
|
|
364
|
+
supportsImages: true,
|
|
365
|
+
contextTokenLimit: 258000,
|
|
366
|
+
maxOutputTokens: 32000,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
function ensureConfigDir() {
|
|
374
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function writeDefaultConfig(target = configPath()) {
|
|
378
|
+
ensureConfigDir();
|
|
379
|
+
if (!fs.existsSync(target)) {
|
|
380
|
+
fs.writeFileSync(target, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", { mode: 0o600 });
|
|
381
|
+
}
|
|
382
|
+
return target;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function readJsonFile(file) {
|
|
386
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function loadConfig(file = configPath()) {
|
|
390
|
+
if (!fs.existsSync(file)) writeDefaultConfig(file);
|
|
391
|
+
const config = readJsonFile(file);
|
|
392
|
+
validateConfig(config, file);
|
|
393
|
+
return config;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateConfig(config, file = configPath()) {
|
|
397
|
+
if (!config || typeof config !== "object") throw new Error(`${file}: config must be an object`);
|
|
398
|
+
if (!Array.isArray(config.providers) || config.providers.length === 0) {
|
|
399
|
+
throw new Error(`${file}: providers must be a non-empty array`);
|
|
400
|
+
}
|
|
401
|
+
const modelNames = new Set();
|
|
402
|
+
for (const provider of config.providers) {
|
|
403
|
+
if (!provider.id || typeof provider.id !== "string") throw new Error(`${file}: provider.id is required`);
|
|
404
|
+
if (!["openai-responses", "openai-chat-completions"].includes(provider.type)) {
|
|
405
|
+
throw new Error(`${file}: provider ${provider.id} type must be openai-responses or openai-chat-completions`);
|
|
406
|
+
}
|
|
407
|
+
if (!provider.baseUrl || typeof provider.baseUrl !== "string") {
|
|
408
|
+
throw new Error(`${file}: provider ${provider.id} baseUrl is required`);
|
|
409
|
+
}
|
|
410
|
+
const key = resolveApiKey(provider, false);
|
|
411
|
+
if (!key || key === "sk-REPLACE_ME") {
|
|
412
|
+
throw new Error(`${file}: provider ${provider.id} apiKey or apiKeyEnv must be configured`);
|
|
413
|
+
}
|
|
414
|
+
if (!Array.isArray(provider.models) || provider.models.length === 0) {
|
|
415
|
+
throw new Error(`${file}: provider ${provider.id} models must be a non-empty array`);
|
|
416
|
+
}
|
|
417
|
+
for (const model of provider.models) {
|
|
418
|
+
if (!model.displayName || typeof model.displayName !== "string") {
|
|
419
|
+
throw new Error(`${file}: provider ${provider.id} model.displayName is required`);
|
|
420
|
+
}
|
|
421
|
+
if (modelNames.has(model.displayName)) {
|
|
422
|
+
throw new Error(`${file}: duplicate model displayName ${model.displayName}`);
|
|
423
|
+
}
|
|
424
|
+
modelNames.add(model.displayName);
|
|
425
|
+
if (model.apiModel !== undefined && typeof model.apiModel !== "string") {
|
|
426
|
+
throw new Error(`${file}: model ${model.displayName} apiModel must be a string`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function resolveApiKey(provider, throwOnMissing = true) {
|
|
433
|
+
const key = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : provider.apiKey;
|
|
434
|
+
if ((!key || key === "sk-REPLACE_ME") && throwOnMissing) {
|
|
435
|
+
throw new Error(`Missing API key for provider ${provider.id}`);
|
|
436
|
+
}
|
|
437
|
+
return key;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function listModels(config) {
|
|
441
|
+
const models = [];
|
|
442
|
+
for (const provider of config.providers || []) {
|
|
443
|
+
for (const model of provider.models || []) {
|
|
444
|
+
models.push({
|
|
445
|
+
providerId: provider.id,
|
|
446
|
+
providerName: provider.name || provider.id,
|
|
447
|
+
providerType: provider.type,
|
|
448
|
+
displayName: model.displayName,
|
|
449
|
+
apiModel: model.apiModel || model.displayName,
|
|
450
|
+
supportsTools: model.supportsTools !== false,
|
|
451
|
+
supportsStreaming: model.supportsStreaming !== false,
|
|
452
|
+
supportsImages: model.supportsImages !== false,
|
|
453
|
+
contextTokenLimit: model.contextTokenLimit || 128000,
|
|
454
|
+
maxOutputTokens: model.maxOutputTokens || 8192,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return models;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function resolveModel(config, requestedModel) {
|
|
462
|
+
const models = listModels(config);
|
|
463
|
+
const fallback = config.defaults && config.defaults.model;
|
|
464
|
+
const target = requestedModel || fallback || (models[0] && models[0].displayName);
|
|
465
|
+
const found = models.find((m) => m.displayName === target || m.apiModel === target);
|
|
466
|
+
if (!found) {
|
|
467
|
+
throw new Error(`Model ${target || "<empty>"} is not configured`);
|
|
468
|
+
}
|
|
469
|
+
const provider = config.providers.find((p) => p.id === found.providerId);
|
|
470
|
+
return { provider, model: found };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function maskSecret(value) {
|
|
474
|
+
if (!value || typeof value !== "string") return value;
|
|
475
|
+
if (value.length <= 8) return "***";
|
|
476
|
+
return `${value.slice(0, 3)}***${value.slice(-4)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function redactConfig(config) {
|
|
480
|
+
return {
|
|
481
|
+
...config,
|
|
482
|
+
providers: (config.providers || []).map((provider) => ({
|
|
483
|
+
...provider,
|
|
484
|
+
apiKey: provider.apiKey ? maskSecret(provider.apiKey) : undefined,
|
|
485
|
+
})),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function bundledTemplatePath() {
|
|
490
|
+
return path.resolve(__dirname, "..", "config.example.json");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
module.exports = {
|
|
494
|
+
DEFAULT_CONFIG,
|
|
495
|
+
ensureConfigDir,
|
|
496
|
+
writeDefaultConfig,
|
|
497
|
+
loadConfig,
|
|
498
|
+
validateConfig,
|
|
499
|
+
resolveApiKey,
|
|
500
|
+
resolveModel,
|
|
501
|
+
listModels,
|
|
502
|
+
redactConfig,
|
|
503
|
+
bundledTemplatePath,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
},
|
|
507
|
+
"src/cursor-models.js": function(module, exports, require, __filename, __dirname) {
|
|
508
|
+
"use strict";
|
|
509
|
+
|
|
510
|
+
const os = require("os");
|
|
511
|
+
const path = require("path");
|
|
512
|
+
const childProcess = require("child_process");
|
|
513
|
+
|
|
514
|
+
const STORAGE_KEY = "src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser";
|
|
515
|
+
|
|
516
|
+
function toCursorModel(model) {
|
|
517
|
+
const name = model.displayName;
|
|
518
|
+
const providerName = model.providerName || model.providerId || "BYOK";
|
|
519
|
+
const contextLimit = model.contextTokenLimit || 128000;
|
|
520
|
+
const tooltipMarkdown = `**${escapeMarkdown(name)}**<br />OpenAI-compatible BYOK model via ${escapeMarkdown(providerName)}.<br /><br />${contextLimit.toLocaleString()} context window`;
|
|
521
|
+
return {
|
|
522
|
+
name,
|
|
523
|
+
defaultOn: true,
|
|
524
|
+
parameterDefinitions: [],
|
|
525
|
+
variants: [
|
|
526
|
+
{
|
|
527
|
+
parameterValues: [],
|
|
528
|
+
displayName: name,
|
|
529
|
+
isMaxMode: false,
|
|
530
|
+
isDefaultMaxConfig: true,
|
|
531
|
+
isDefaultNonMaxConfig: true,
|
|
532
|
+
tooltipData: tooltip(tooltipMarkdown),
|
|
533
|
+
displayNameOutsidePicker: name,
|
|
534
|
+
variantStringRepresentation: `${name}[]`,
|
|
535
|
+
legacySlug: name,
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
legacySlugs: [name],
|
|
539
|
+
idAliases: [name],
|
|
540
|
+
cloudAgentEffortModes: [],
|
|
541
|
+
supportsAgent: true,
|
|
542
|
+
degradationStatus: 0,
|
|
543
|
+
tooltipData: tooltip(tooltipMarkdown),
|
|
544
|
+
supportsThinking: false,
|
|
545
|
+
supportsImages: model.supportsImages !== false,
|
|
546
|
+
supportsMaxMode: true,
|
|
547
|
+
clientDisplayName: name,
|
|
548
|
+
serverModelName: name,
|
|
549
|
+
supportsNonMaxMode: true,
|
|
550
|
+
tooltipDataForMaxMode: tooltip(tooltipMarkdown),
|
|
551
|
+
isRecommendedForBackgroundComposer: false,
|
|
552
|
+
supportsPlanMode: true,
|
|
553
|
+
inputboxShortModelName: name,
|
|
554
|
+
supportsSandboxing: true,
|
|
555
|
+
supportsCmdK: true,
|
|
556
|
+
contextTokenLimit: contextLimit,
|
|
557
|
+
contextTokenLimitForMaxMode: contextLimit,
|
|
558
|
+
maxOutputTokens: model.maxOutputTokens || 8192,
|
|
559
|
+
namedModelSectionIndex: 1,
|
|
560
|
+
vendorName: providerName,
|
|
561
|
+
vendor: { id: 9000, displayName: providerName },
|
|
562
|
+
tagline: "OpenAI-compatible BYOK",
|
|
563
|
+
visibleInRoutedModelView: true,
|
|
564
|
+
isUserAdded: true,
|
|
565
|
+
byokModel: true,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function enrichCursorModels(existing, models) {
|
|
570
|
+
const out = Array.isArray(existing) ? existing.slice() : [];
|
|
571
|
+
for (const model of models || []) {
|
|
572
|
+
const cursorModel = toCursorModel(model);
|
|
573
|
+
const index = out.findIndex((item) => sameModel(item, cursorModel.name));
|
|
574
|
+
if (index >= 0) out[index] = { ...out[index], ...cursorModel };
|
|
575
|
+
else out.push(cursorModel);
|
|
576
|
+
}
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function syncCursorStorage(models, options = {}) {
|
|
581
|
+
if (!Array.isArray(models) || models.length === 0) return { skipped: true, reason: "no models" };
|
|
582
|
+
const dbPath = options.dbPath || cursorStateDbPath();
|
|
583
|
+
if (!dbPath) return { skipped: true, reason: "unsupported platform" };
|
|
584
|
+
const sqlite = options.sqlite || "sqlite3";
|
|
585
|
+
let raw;
|
|
586
|
+
try {
|
|
587
|
+
raw = childProcess.execFileSync(sqlite, [dbPath, `select value from ItemTable where key=${sqlString(STORAGE_KEY)};`], {
|
|
588
|
+
encoding: "utf8",
|
|
589
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
590
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
591
|
+
});
|
|
592
|
+
} catch (err) {
|
|
593
|
+
return { skipped: true, reason: `storage unavailable: ${err.message}` };
|
|
594
|
+
}
|
|
595
|
+
if (!raw.trim()) return { skipped: true, reason: "storage key not initialized" };
|
|
596
|
+
|
|
597
|
+
const data = JSON.parse(raw);
|
|
598
|
+
const before = Array.isArray(data.availableDefaultModels2) ? data.availableDefaultModels2.length : 0;
|
|
599
|
+
data.availableDefaultModels2 = enrichCursorModels(data.availableDefaultModels2, models);
|
|
600
|
+
data.aiSettings = data.aiSettings || {};
|
|
601
|
+
data.aiSettings.modelOverrideDisabled = removeNames(data.aiSettings.modelOverrideDisabled, models);
|
|
602
|
+
data.aiSettings.modelOverrideEnabled = addNames(data.aiSettings.modelOverrideEnabled, models);
|
|
603
|
+
|
|
604
|
+
const value = JSON.stringify(data);
|
|
605
|
+
childProcess.execFileSync(sqlite, [
|
|
606
|
+
dbPath,
|
|
607
|
+
`update ItemTable set value=${sqlString(value)} where key=${sqlString(STORAGE_KEY)};`,
|
|
608
|
+
], { stdio: ["ignore", "ignore", "pipe"], maxBuffer: 10 * 1024 * 1024 });
|
|
609
|
+
return { synced: true, dbPath, before, after: data.availableDefaultModels2.length };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function cursorStateDbPath() {
|
|
613
|
+
if (process.platform === "darwin") {
|
|
614
|
+
return path.join(os.homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
615
|
+
}
|
|
616
|
+
if (process.platform === "win32") {
|
|
617
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
618
|
+
return path.join(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function addNames(values, models) {
|
|
624
|
+
const out = new Set(Array.isArray(values) ? values : []);
|
|
625
|
+
for (const model of models) out.add(model.displayName);
|
|
626
|
+
return Array.from(out);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function removeNames(values, models) {
|
|
630
|
+
if (!Array.isArray(values)) return [];
|
|
631
|
+
const names = new Set(models.map((model) => model.displayName));
|
|
632
|
+
return values.filter((value) => !names.has(value));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function sameModel(item, name) {
|
|
636
|
+
return item && (item.name === name || item.clientDisplayName === name || item.serverModelName === name);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function sqlString(value) {
|
|
640
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function tooltip(markdownContent) {
|
|
644
|
+
return {
|
|
645
|
+
primaryText: "",
|
|
646
|
+
secondaryText: "",
|
|
647
|
+
secondaryWarningText: false,
|
|
648
|
+
icon: "",
|
|
649
|
+
tertiaryText: "",
|
|
650
|
+
tertiaryTextUrl: "",
|
|
651
|
+
markdownContent,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function escapeMarkdown(value) {
|
|
656
|
+
return String(value == null ? "" : value).replace(/[\\*_[\]`]/g, "\\$&");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
module.exports = {
|
|
660
|
+
STORAGE_KEY,
|
|
661
|
+
toCursorModel,
|
|
662
|
+
enrichCursorModels,
|
|
663
|
+
syncCursorStorage,
|
|
664
|
+
cursorStateDbPath,
|
|
665
|
+
sqlString,
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
},
|
|
669
|
+
"src/logger.js": function(module, exports, require, __filename, __dirname) {
|
|
670
|
+
"use strict";
|
|
671
|
+
|
|
672
|
+
const fs = require("fs");
|
|
673
|
+
const { ensureConfigDir } = require("./config");
|
|
674
|
+
const { logPath } = require("./paths");
|
|
675
|
+
|
|
676
|
+
function redactText(text) {
|
|
677
|
+
return String(text)
|
|
678
|
+
.replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-***")
|
|
679
|
+
.replace(/Bearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function log(event, meta) {
|
|
683
|
+
ensureConfigDir();
|
|
684
|
+
const suffix = meta === undefined ? "" : " " + redactText(JSON.stringify(meta));
|
|
685
|
+
fs.appendFileSync(logPath(), `[${new Date().toISOString()}] ${event}${suffix}\n`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function tailLog(lines = 120) {
|
|
689
|
+
if (!fs.existsSync(logPath())) return "";
|
|
690
|
+
const data = fs.readFileSync(logPath(), "utf8");
|
|
691
|
+
return data.split(/\r?\n/).slice(-lines).join("\n");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
module.exports = { log, tailLog, redactText };
|
|
695
|
+
|
|
696
|
+
},
|
|
697
|
+
"src/cli/service.js": function(module, exports, require, __filename, __dirname) {
|
|
698
|
+
"use strict";
|
|
699
|
+
|
|
700
|
+
const fs = require("fs");
|
|
701
|
+
const path = require("path");
|
|
702
|
+
const childProcess = require("child_process");
|
|
703
|
+
const {
|
|
704
|
+
pidPath,
|
|
705
|
+
logPath,
|
|
706
|
+
installedExtensionDir,
|
|
707
|
+
cursorExtensionsDir,
|
|
708
|
+
ensureDir,
|
|
709
|
+
} = require("../paths");
|
|
710
|
+
const { ensureConfigDir } = require("../config");
|
|
711
|
+
|
|
712
|
+
function nodeBin() {
|
|
713
|
+
return process.execPath;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function daemonScript(baseDir = path.resolve(__dirname, "..")) {
|
|
717
|
+
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
718
|
+
const bundled = path.join(packageRoot, "lib", "daemon-entry.js");
|
|
719
|
+
if (fs.existsSync(bundled)) return bundled;
|
|
720
|
+
return path.join(baseDir, "daemon-entry.js");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function isRunning() {
|
|
724
|
+
try {
|
|
725
|
+
const pid = Number(fs.readFileSync(pidPath(), "utf8"));
|
|
726
|
+
if (!pid) return false;
|
|
727
|
+
process.kill(pid, 0);
|
|
728
|
+
return pid;
|
|
729
|
+
} catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function startForeground() {
|
|
735
|
+
require("../daemon-entry");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function startBackground() {
|
|
739
|
+
ensureConfigDir();
|
|
740
|
+
const existing = isRunning();
|
|
741
|
+
if (existing) return { alreadyRunning: true, pid: existing };
|
|
742
|
+
const out = fs.openSync(logPath(), "a");
|
|
743
|
+
const err = fs.openSync(logPath(), "a");
|
|
744
|
+
const child = childProcess.spawn(nodeBin(), [daemonScript()], {
|
|
745
|
+
detached: false,
|
|
746
|
+
stdio: ["ignore", out, err],
|
|
747
|
+
env: { ...process.env },
|
|
748
|
+
});
|
|
749
|
+
child.unref();
|
|
750
|
+
fs.writeFileSync(pidPath(), String(child.pid));
|
|
751
|
+
return { started: true, pid: child.pid };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function stopBackground() {
|
|
755
|
+
const pid = isRunning();
|
|
756
|
+
if (!pid) return { stopped: false };
|
|
757
|
+
process.kill(pid, "SIGTERM");
|
|
758
|
+
try { fs.unlinkSync(pidPath()); } catch {}
|
|
759
|
+
return { stopped: true, pid };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function installExtension() {
|
|
763
|
+
const sourceRoot = path.resolve(__dirname, "..", "..");
|
|
764
|
+
const target = installedExtensionDir();
|
|
765
|
+
ensureDir(cursorExtensionsDir());
|
|
766
|
+
removeInstalledExtensionVersions(target);
|
|
767
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
768
|
+
copyTree(sourceRoot, target, {
|
|
769
|
+
exclude: new Set(["dist", "test", ".git", ".DS_Store"]),
|
|
770
|
+
});
|
|
771
|
+
writeCursorExtensionIndex(target);
|
|
772
|
+
clearObsoleteMarker(path.basename(target));
|
|
773
|
+
return { installed: true, extensionDir: target };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function uninstallExtension() {
|
|
777
|
+
const target = installedExtensionDir();
|
|
778
|
+
removeInstalledExtensionVersions();
|
|
779
|
+
removeFromCursorExtensionIndex();
|
|
780
|
+
markObsolete(path.basename(target));
|
|
781
|
+
return { uninstalled: true, extensionDir: target };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function extensionStatus() {
|
|
785
|
+
const target = installedExtensionDir();
|
|
786
|
+
return { installed: fs.existsSync(path.join(target, "extension", "extension.js")), extensionDir: target };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function copyTree(src, dst, options = {}) {
|
|
790
|
+
const base = path.basename(src);
|
|
791
|
+
if (options.exclude && options.exclude.has(base)) return;
|
|
792
|
+
const stat = fs.statSync(src);
|
|
793
|
+
if (stat.isDirectory()) {
|
|
794
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
795
|
+
for (const entry of fs.readdirSync(src)) copyTree(path.join(src, entry), path.join(dst, entry), options);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
799
|
+
fs.copyFileSync(src, dst);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function removeInstalledExtensionVersions(except) {
|
|
803
|
+
const dir = cursorExtensionsDir();
|
|
804
|
+
if (!fs.existsSync(dir)) return;
|
|
805
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
806
|
+
if (!entry.startsWith("cursor-openai-byok.cursor-openai-byok-")) continue;
|
|
807
|
+
const full = path.join(dir, entry);
|
|
808
|
+
if (except && full === except) continue;
|
|
809
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function writeCursorExtensionIndex(target) {
|
|
814
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(target, "package.json"), "utf8"));
|
|
815
|
+
const file = path.join(cursorExtensionsDir(), "extensions.json");
|
|
816
|
+
let entries = [];
|
|
817
|
+
try {
|
|
818
|
+
entries = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
819
|
+
if (!Array.isArray(entries)) entries = [];
|
|
820
|
+
} catch {}
|
|
821
|
+
entries = entries.filter((entry) => entry?.identifier?.id !== "cursor-openai-byok.cursor-openai-byok");
|
|
822
|
+
entries.push({
|
|
823
|
+
identifier: { id: "cursor-openai-byok.cursor-openai-byok" },
|
|
824
|
+
version: manifest.version,
|
|
825
|
+
location: {
|
|
826
|
+
$mid: 1,
|
|
827
|
+
fsPath: target,
|
|
828
|
+
external: `file://${target}`,
|
|
829
|
+
path: target,
|
|
830
|
+
scheme: "file",
|
|
831
|
+
},
|
|
832
|
+
relativeLocation: path.basename(target),
|
|
833
|
+
});
|
|
834
|
+
fs.writeFileSync(file, JSON.stringify(entries, null, 2));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function removeFromCursorExtensionIndex() {
|
|
838
|
+
const file = path.join(cursorExtensionsDir(), "extensions.json");
|
|
839
|
+
let entries = [];
|
|
840
|
+
try {
|
|
841
|
+
entries = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
842
|
+
if (!Array.isArray(entries)) entries = [];
|
|
843
|
+
} catch {}
|
|
844
|
+
entries = entries.filter((entry) => entry?.identifier?.id !== "cursor-openai-byok.cursor-openai-byok");
|
|
845
|
+
fs.writeFileSync(file, JSON.stringify(entries, null, 2));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function readObsolete() {
|
|
849
|
+
const file = path.join(cursorExtensionsDir(), ".obsolete");
|
|
850
|
+
try {
|
|
851
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
852
|
+
return value && typeof value === "object" ? value : {};
|
|
853
|
+
} catch {
|
|
854
|
+
return {};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function writeObsolete(value) {
|
|
859
|
+
fs.writeFileSync(path.join(cursorExtensionsDir(), ".obsolete"), JSON.stringify(value));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function clearObsoleteMarker(relativeLocation) {
|
|
863
|
+
const obsolete = readObsolete();
|
|
864
|
+
delete obsolete[relativeLocation];
|
|
865
|
+
writeObsolete(obsolete);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function markObsolete(relativeLocation) {
|
|
869
|
+
const obsolete = readObsolete();
|
|
870
|
+
obsolete[relativeLocation] = true;
|
|
871
|
+
writeObsolete(obsolete);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
module.exports = {
|
|
875
|
+
isRunning,
|
|
876
|
+
startForeground,
|
|
877
|
+
startBackground,
|
|
878
|
+
stopBackground,
|
|
879
|
+
installExtension,
|
|
880
|
+
uninstallExtension,
|
|
881
|
+
extensionStatus,
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
},
|
|
885
|
+
"src/daemon-entry.js": function(module, exports, require, __filename, __dirname) {
|
|
886
|
+
"use strict";
|
|
887
|
+
|
|
888
|
+
const { startServer } = require("./daemon/server");
|
|
889
|
+
const { pidPath } = require("./paths");
|
|
890
|
+
const fs = require("fs");
|
|
891
|
+
|
|
892
|
+
fs.writeFileSync(pidPath(), String(process.pid));
|
|
893
|
+
const server = startServer();
|
|
894
|
+
|
|
895
|
+
function shutdown() {
|
|
896
|
+
try { fs.unlinkSync(pidPath()); } catch {}
|
|
897
|
+
server.close(() => process.exit(0));
|
|
898
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
process.on("SIGTERM", shutdown);
|
|
902
|
+
process.on("SIGINT", shutdown);
|
|
903
|
+
process.on("exit", () => {
|
|
904
|
+
try { fs.unlinkSync(pidPath()); } catch {}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
},
|
|
908
|
+
"src/daemon/server.js": function(module, exports, require, __filename, __dirname) {
|
|
909
|
+
"use strict";
|
|
910
|
+
|
|
911
|
+
const http = require("http");
|
|
912
|
+
const { loadConfig, listModels, resolveModel } = require("../config");
|
|
913
|
+
const { log } = require("../logger");
|
|
914
|
+
const { DEFAULT_PORT } = require("../paths");
|
|
915
|
+
const { cursorPayloadToOpenAI } = require("../cursor-agent-adapter/messages");
|
|
916
|
+
const {
|
|
917
|
+
proxyOpenAIRequest,
|
|
918
|
+
streamProviderEvents,
|
|
919
|
+
callProviderOnce,
|
|
920
|
+
} = require("../provider-adapters/openai-compatible");
|
|
921
|
+
|
|
922
|
+
function readJson(req, limit = 30 * 1024 * 1024) {
|
|
923
|
+
return new Promise((resolve, reject) => {
|
|
924
|
+
let body = "";
|
|
925
|
+
req.setEncoding("utf8");
|
|
926
|
+
req.on("data", (chunk) => {
|
|
927
|
+
body += chunk;
|
|
928
|
+
if (body.length > limit) {
|
|
929
|
+
reject(new Error("request too large"));
|
|
930
|
+
req.destroy();
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
req.on("end", () => {
|
|
934
|
+
try {
|
|
935
|
+
resolve(body ? JSON.parse(body) : {});
|
|
936
|
+
} catch (err) {
|
|
937
|
+
reject(err);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
req.on("error", reject);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function sendJson(res, status, body) {
|
|
945
|
+
res.writeHead(status, corsHeaders({ "content-type": "application/json; charset=utf-8" }));
|
|
946
|
+
res.end(JSON.stringify(body));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function corsHeaders(extra = {}) {
|
|
950
|
+
return {
|
|
951
|
+
"access-control-allow-origin": "*",
|
|
952
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
953
|
+
"access-control-allow-headers": "content-type,authorization,openai-beta",
|
|
954
|
+
...extra,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function pipeProviderResponse(res, providerResponse) {
|
|
959
|
+
const headers = {
|
|
960
|
+
"content-type": providerResponse.headers.get("content-type") || "application/json; charset=utf-8",
|
|
961
|
+
};
|
|
962
|
+
res.writeHead(providerResponse.status, corsHeaders(headers));
|
|
963
|
+
if (!providerResponse.body) {
|
|
964
|
+
res.end(await providerResponse.text());
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
for await (const chunk of providerResponse.body) res.write(chunk);
|
|
968
|
+
res.end();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function sendNdjson(res, iterator) {
|
|
972
|
+
res.writeHead(200, corsHeaders({ "content-type": "application/x-ndjson; charset=utf-8" }));
|
|
973
|
+
try {
|
|
974
|
+
for await (const event of iterator) {
|
|
975
|
+
res.write(JSON.stringify(event) + "\n");
|
|
976
|
+
}
|
|
977
|
+
} catch (err) {
|
|
978
|
+
const message = err && err.message ? err.message : String(err);
|
|
979
|
+
log("stream-error", { error: err && err.stack ? err.stack : message });
|
|
980
|
+
res.write(JSON.stringify({ type: "error", error: message }) + "\n");
|
|
981
|
+
}
|
|
982
|
+
res.write(JSON.stringify({ type: "done" }) + "\n");
|
|
983
|
+
res.end();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function* runCursorAgentLoop(config, payload) {
|
|
987
|
+
const requestedModel = payload.model || payload.modelName || payload?.runRequest?.requestedModel?.modelId;
|
|
988
|
+
const route = resolveModel(config, requestedModel);
|
|
989
|
+
const openaiBody = cursorPayloadToOpenAI(payload, route);
|
|
990
|
+
log("cursor-run", {
|
|
991
|
+
model: route.model.displayName,
|
|
992
|
+
provider: route.provider.id,
|
|
993
|
+
conversationId: payload.conversationId,
|
|
994
|
+
generationUUID: payload.generationUUID,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
if (route.model.supportsStreaming === false) {
|
|
998
|
+
const text = await callProviderOnce(config, route, openaiBody);
|
|
999
|
+
if (text) yield { type: "text_delta", text };
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const pendingTools = new Map();
|
|
1004
|
+
for await (const event of streamProviderEvents(config, route, openaiBody)) {
|
|
1005
|
+
if (event.type === "tool_call_delta") {
|
|
1006
|
+
const key = event.id || String(event.index || pendingTools.size);
|
|
1007
|
+
const existing = pendingTools.get(key) || { id: key, name: event.name, arguments: "" };
|
|
1008
|
+
existing.name = event.name || existing.name;
|
|
1009
|
+
existing.arguments += event.argumentsDelta || "";
|
|
1010
|
+
pendingTools.set(key, existing);
|
|
1011
|
+
yield event;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (event.type === "tool_call") {
|
|
1015
|
+
pendingTools.set(event.id, event);
|
|
1016
|
+
yield event;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (event.type === "finish" && pendingTools.size > 0) {
|
|
1020
|
+
yield { type: "tool_calls_ready", toolCalls: [...pendingTools.values()] };
|
|
1021
|
+
pendingTools.clear();
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
yield event;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function createServer() {
|
|
1029
|
+
return http.createServer(async (req, res) => {
|
|
1030
|
+
try {
|
|
1031
|
+
if (req.method === "OPTIONS") return sendJson(res, 204, {});
|
|
1032
|
+
const config = loadConfig();
|
|
1033
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
1034
|
+
|
|
1035
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
1036
|
+
return sendJson(res, 200, {
|
|
1037
|
+
ok: true,
|
|
1038
|
+
app: "cursor-openai-byok",
|
|
1039
|
+
models: listModels(config).map((m) => m.displayName),
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (req.method === "GET" && url.pathname === "/cursor/v1/models") {
|
|
1044
|
+
return sendJson(res, 200, { ok: true, models: listModels(config) });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (req.method === "GET" && url.pathname === "/cursor/v1/should-handle") {
|
|
1048
|
+
const model = url.searchParams.get("model");
|
|
1049
|
+
try {
|
|
1050
|
+
const route = resolveModel(config, model);
|
|
1051
|
+
return sendJson(res, 200, { ok: true, handle: true, model: route.model });
|
|
1052
|
+
} catch {
|
|
1053
|
+
return sendJson(res, 200, { ok: true, handle: false });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (req.method === "POST" && url.pathname === "/cursor/v1/run-agent-loop") {
|
|
1058
|
+
const payload = await readJson(req);
|
|
1059
|
+
return sendNdjson(res, runCursorAgentLoop(config, payload));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (req.method === "POST" && url.pathname === "/cursor/v1/tool-result") {
|
|
1063
|
+
const payload = await readJson(req);
|
|
1064
|
+
log("tool-result", {
|
|
1065
|
+
conversationId: payload.conversationId,
|
|
1066
|
+
callId: payload.callId,
|
|
1067
|
+
ok: payload.ok !== false,
|
|
1068
|
+
});
|
|
1069
|
+
return sendJson(res, 200, { ok: true });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (req.method === "POST" && url.pathname === "/v1/responses") {
|
|
1073
|
+
const body = await readJson(req);
|
|
1074
|
+
return pipeProviderResponse(res, await proxyOpenAIRequest(config, "responses", body, req.headers));
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
|
|
1078
|
+
const body = await readJson(req);
|
|
1079
|
+
return pipeProviderResponse(res, await proxyOpenAIRequest(config, "chat", body, req.headers));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
sendJson(res, 404, { ok: false, error: "not found" });
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
log("error", { url: req.url, error: err && err.stack ? err.stack : String(err) });
|
|
1085
|
+
sendJson(res, 500, { ok: false, error: err && err.message ? err.message : String(err) });
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function startServer(options = {}) {
|
|
1091
|
+
const config = loadConfig();
|
|
1092
|
+
const host = options.host || config.server?.host || "127.0.0.1";
|
|
1093
|
+
const port = Number(options.port || config.server?.port || DEFAULT_PORT);
|
|
1094
|
+
const server = createServer();
|
|
1095
|
+
server.listen(port, host, () => {
|
|
1096
|
+
log("listening", { host, port });
|
|
1097
|
+
console.log(`cursor-openai-byok listening on http://${host}:${port}`);
|
|
1098
|
+
});
|
|
1099
|
+
return server;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
module.exports = { createServer, startServer, runCursorAgentLoop, readJson };
|
|
1103
|
+
|
|
1104
|
+
},
|
|
1105
|
+
"src/cursor-agent-adapter/messages.js": function(module, exports, require, __filename, __dirname) {
|
|
1106
|
+
"use strict";
|
|
1107
|
+
|
|
1108
|
+
const fs = require("fs");
|
|
1109
|
+
const path = require("path");
|
|
1110
|
+
|
|
1111
|
+
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
|
|
1112
|
+
const IMAGE_MIME_BY_EXT = {
|
|
1113
|
+
".png": "image/png",
|
|
1114
|
+
".jpg": "image/jpeg",
|
|
1115
|
+
".jpeg": "image/jpeg",
|
|
1116
|
+
".webp": "image/webp",
|
|
1117
|
+
".gif": "image/gif",
|
|
1118
|
+
};
|
|
1119
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
1120
|
+
|
|
1121
|
+
function looksLikeNoise(key, value) {
|
|
1122
|
+
const k = String(key || "").toLowerCase();
|
|
1123
|
+
if (k.includes("id") || k.includes("uuid") || k.includes("path") || k.includes("url")) return true;
|
|
1124
|
+
if (k.includes("model") || k.includes("token") || k.includes("hash")) return true;
|
|
1125
|
+
if (/^[a-z0-9_-]{24,}$/i.test(value) && !value.includes(" ")) return true;
|
|
1126
|
+
return value.length > 120000;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function collectStrings(value, out = [], pathParts = []) {
|
|
1130
|
+
if (value == null) return out;
|
|
1131
|
+
if (typeof value === "string") {
|
|
1132
|
+
const key = pathParts[pathParts.length - 1] || "";
|
|
1133
|
+
const text = value.trim();
|
|
1134
|
+
if (text && !looksLikeNoise(key, text)) {
|
|
1135
|
+
const lower = String(key).toLowerCase();
|
|
1136
|
+
let score = 1;
|
|
1137
|
+
if (["text", "content", "prompt", "userMessage", "message", "instruction", "instructions", "query"].includes(key)) score += 20;
|
|
1138
|
+
if (lower.includes("text") || lower.includes("content") || lower.includes("prompt") || lower.includes("message")) score += 10;
|
|
1139
|
+
if (/[\u4e00-\u9fff]/.test(text)) score += 2;
|
|
1140
|
+
out.push({ text, score, path: pathParts.join(".") });
|
|
1141
|
+
}
|
|
1142
|
+
return out;
|
|
1143
|
+
}
|
|
1144
|
+
if (Array.isArray(value)) {
|
|
1145
|
+
value.forEach((item, i) => collectStrings(item, out, pathParts.concat(String(i))));
|
|
1146
|
+
return out;
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof value === "object") {
|
|
1149
|
+
for (const [key, child] of Object.entries(value)) collectStrings(child, out, pathParts.concat(key));
|
|
1150
|
+
}
|
|
1151
|
+
return out;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function extractUserText(payload) {
|
|
1155
|
+
const direct =
|
|
1156
|
+
payload?.userText ||
|
|
1157
|
+
payload?.text ||
|
|
1158
|
+
payload?.runRequest?.action?.userMessageAction?.userMessage?.text ||
|
|
1159
|
+
payload?.runRequest?.action?.userMessageAction?.prependUserMessages?.at?.(-1)?.text;
|
|
1160
|
+
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
|
1161
|
+
const candidates = collectStrings(payload)
|
|
1162
|
+
.filter((candidate) => candidate.text.length <= 20000)
|
|
1163
|
+
.sort((a, b) => a.score - b.score);
|
|
1164
|
+
const chosen = candidates[candidates.length - 1];
|
|
1165
|
+
return chosen ? chosen.text : "";
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function parseJsonMaybe(value) {
|
|
1169
|
+
if (value == null) return undefined;
|
|
1170
|
+
if (typeof value === "object") return value;
|
|
1171
|
+
if (typeof value !== "string") return undefined;
|
|
1172
|
+
try {
|
|
1173
|
+
return JSON.parse(value);
|
|
1174
|
+
} catch {
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function extractConversationMessages(payload) {
|
|
1180
|
+
const messages = [];
|
|
1181
|
+
const turns = payload?.runRequest?.conversationState?.turns || payload?.conversationState?.turns || [];
|
|
1182
|
+
for (const turn of turns) {
|
|
1183
|
+
const parsed = parseJsonMaybe(turn);
|
|
1184
|
+
const text = extractUserText(parsed || turn);
|
|
1185
|
+
if (text) messages.push({ role: "user", content: text });
|
|
1186
|
+
}
|
|
1187
|
+
const current = extractUserText(payload);
|
|
1188
|
+
const images = extractImages(payload);
|
|
1189
|
+
if (current || images.length > 0) messages.push({ role: "user", content: current, images });
|
|
1190
|
+
if (messages.length === 0) messages.push({ role: "user", content: "Please reply briefly." });
|
|
1191
|
+
return compactMessages(messages);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function compactMessages(messages) {
|
|
1195
|
+
const out = [];
|
|
1196
|
+
for (const message of messages) {
|
|
1197
|
+
if (!message.content && !message.images?.length) continue;
|
|
1198
|
+
const prev = out[out.length - 1];
|
|
1199
|
+
if (prev && prev.role === message.role && prev.content === message.content) continue;
|
|
1200
|
+
const images = Array.isArray(message.images) && message.images.length > 0 ? message.images : undefined;
|
|
1201
|
+
out.push({
|
|
1202
|
+
role: message.role,
|
|
1203
|
+
content: message.content || "",
|
|
1204
|
+
...(images ? { images } : {}),
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return out.slice(-32);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function toResponsesInput(messages) {
|
|
1211
|
+
return messages.map((message) => ({
|
|
1212
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
1213
|
+
content: toResponsesContent(message),
|
|
1214
|
+
}));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function toChatMessages(messages) {
|
|
1218
|
+
return messages.map((message) => ({
|
|
1219
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
1220
|
+
content: toChatContent(message),
|
|
1221
|
+
}));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function toResponsesContent(message) {
|
|
1225
|
+
const content = [];
|
|
1226
|
+
const text = String(message.content || "");
|
|
1227
|
+
if (text || !message.images?.length) {
|
|
1228
|
+
content.push({
|
|
1229
|
+
type: message.role === "assistant" ? "output_text" : "input_text",
|
|
1230
|
+
text,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
if (message.role !== "assistant") {
|
|
1234
|
+
for (const image of message.images || []) {
|
|
1235
|
+
content.push({ type: "input_image", image_url: image.url });
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return content;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function toChatContent(message) {
|
|
1242
|
+
if (message.role === "assistant" || !message.images?.length) return String(message.content || "");
|
|
1243
|
+
const content = [];
|
|
1244
|
+
const text = String(message.content || "");
|
|
1245
|
+
if (text) content.push({ type: "text", text });
|
|
1246
|
+
for (const image of message.images) content.push({ type: "image_url", image_url: { url: image.url } });
|
|
1247
|
+
return content;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function cursorPayloadToOpenAI(payload, route) {
|
|
1251
|
+
const messages = extractConversationMessages(payload);
|
|
1252
|
+
const max = route.model.maxOutputTokens;
|
|
1253
|
+
if (route.provider.type === "openai-responses") {
|
|
1254
|
+
return {
|
|
1255
|
+
model: route.model.apiModel,
|
|
1256
|
+
stream: true,
|
|
1257
|
+
input: toResponsesInput(messages),
|
|
1258
|
+
tools: normalizeTools(payload?.tools || payload?.runRequest?.mcpTools),
|
|
1259
|
+
tool_choice: payload?.tool_choice || "auto",
|
|
1260
|
+
parallel_tool_calls: payload?.parallel_tool_calls !== false,
|
|
1261
|
+
max_output_tokens: payload?.max_output_tokens || max,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
model: route.model.apiModel,
|
|
1266
|
+
stream: true,
|
|
1267
|
+
messages: toChatMessages(messages),
|
|
1268
|
+
tools: normalizeTools(payload?.tools || payload?.runRequest?.mcpTools),
|
|
1269
|
+
tool_choice: payload?.tool_choice || "auto",
|
|
1270
|
+
parallel_tool_calls: payload?.parallel_tool_calls !== false,
|
|
1271
|
+
max_tokens: payload?.max_tokens || payload?.max_output_tokens || max,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function normalizeTools(tools) {
|
|
1276
|
+
if (!tools) return undefined;
|
|
1277
|
+
if (Array.isArray(tools)) return tools;
|
|
1278
|
+
const values = Object.values(tools);
|
|
1279
|
+
if (values.length === 0) return undefined;
|
|
1280
|
+
return values;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function extractImages(payload) {
|
|
1284
|
+
const images = [];
|
|
1285
|
+
const seen = new Set();
|
|
1286
|
+
collectImages(payload, images, seen, []);
|
|
1287
|
+
return images.slice(0, 8);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function collectImages(value, out, seen, pathParts) {
|
|
1291
|
+
if (value == null || out.length >= 8) return;
|
|
1292
|
+
if (typeof value === "string") {
|
|
1293
|
+
const image = imageFromString(value, pathParts);
|
|
1294
|
+
if (image && !seen.has(image.url)) {
|
|
1295
|
+
seen.add(image.url);
|
|
1296
|
+
out.push(image);
|
|
1297
|
+
}
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (Array.isArray(value)) {
|
|
1301
|
+
value.forEach((item, index) => collectImages(item, out, seen, pathParts.concat(String(index))));
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (typeof value !== "object") return;
|
|
1305
|
+
|
|
1306
|
+
const direct = imageFromObject(value, pathParts);
|
|
1307
|
+
if (direct && !seen.has(direct.url)) {
|
|
1308
|
+
seen.add(direct.url);
|
|
1309
|
+
out.push(direct);
|
|
1310
|
+
}
|
|
1311
|
+
for (const [key, child] of Object.entries(value)) collectImages(child, out, seen, pathParts.concat(key));
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function imageFromObject(value, pathParts) {
|
|
1315
|
+
const mime = value.mimeType || value.mimetype || value.contentType || value.type;
|
|
1316
|
+
if (typeof mime === "string" && mime.startsWith("image/")) {
|
|
1317
|
+
const data = value.data || value.base64 || value.bytes || value.content;
|
|
1318
|
+
if (typeof data === "string") {
|
|
1319
|
+
if (data.startsWith("data:image/")) return { url: data, source: pathParts.join(".") };
|
|
1320
|
+
if (looksLikeImageBase64(data)) return { url: `data:${mime};base64,${stripBase64(data)}`, source: pathParts.join(".") };
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const file = value.path || value.filePath || value.fsPath || value.uri;
|
|
1324
|
+
if (typeof file === "string") return imageFromString(file, pathParts);
|
|
1325
|
+
const url = value.url || value.imageUrl || value.image_url;
|
|
1326
|
+
if (typeof url === "string") return imageFromString(url, pathParts);
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function imageFromString(value, pathParts = []) {
|
|
1331
|
+
const text = value.trim();
|
|
1332
|
+
if (!text) return null;
|
|
1333
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(text)) return { url: text, source: pathParts.join(".") };
|
|
1334
|
+
if (/^https?:\/\//i.test(text) && looksLikeImagePath(text)) return { url: text, source: pathParts.join(".") };
|
|
1335
|
+
if (text.startsWith("file://")) return imageFromFilePath(decodeURIComponent(new URL(text).pathname), pathParts);
|
|
1336
|
+
if (looksLikeImagePath(text)) return imageFromFilePath(text, pathParts);
|
|
1337
|
+
if (pathParts.some((part) => /image|base64|bytes|data/i.test(part)) && looksLikeImageBase64(text)) {
|
|
1338
|
+
const mime = mimeFromImageBase64(text);
|
|
1339
|
+
if (mime) return { url: `data:${mime};base64,${stripBase64(text)}`, source: pathParts.join(".") };
|
|
1340
|
+
}
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function imageFromFilePath(file, pathParts) {
|
|
1345
|
+
try {
|
|
1346
|
+
const resolved = path.resolve(String(file).replace(/^~(?=\/|\\)/, process.env.HOME || ""));
|
|
1347
|
+
if (!looksLikeImagePath(resolved)) return null;
|
|
1348
|
+
const stat = fs.statSync(resolved);
|
|
1349
|
+
if (!stat.isFile() || stat.size > MAX_IMAGE_BYTES) return null;
|
|
1350
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
1351
|
+
const mime = IMAGE_MIME_BY_EXT[ext] || "image/png";
|
|
1352
|
+
return { url: `data:${mime};base64,${fs.readFileSync(resolved).toString("base64")}`, source: pathParts.join(".") };
|
|
1353
|
+
} catch {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function looksLikeImagePath(value) {
|
|
1359
|
+
try {
|
|
1360
|
+
const clean = String(value).split(/[?#]/)[0];
|
|
1361
|
+
return IMAGE_EXTENSIONS.has(path.extname(clean).toLowerCase());
|
|
1362
|
+
} catch {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function looksLikeImageBase64(value) {
|
|
1368
|
+
const stripped = stripBase64(value);
|
|
1369
|
+
if (stripped.length < 32 || stripped.length > MAX_IMAGE_BYTES * 2) return false;
|
|
1370
|
+
return !!mimeFromImageBase64(stripped);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function mimeFromImageBase64(value) {
|
|
1374
|
+
const stripped = stripBase64(value);
|
|
1375
|
+
if (stripped.startsWith("iVBORw0KGgo")) return "image/png";
|
|
1376
|
+
if (stripped.startsWith("/9j/")) return "image/jpeg";
|
|
1377
|
+
if (stripped.startsWith("UklGR")) return "image/webp";
|
|
1378
|
+
if (stripped.startsWith("R0lGOD")) return "image/gif";
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function stripBase64(value) {
|
|
1383
|
+
return String(value).replace(/^data:image\/[a-z0-9.+-]+;base64,/i, "").replace(/\s+/g, "");
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
module.exports = {
|
|
1387
|
+
extractUserText,
|
|
1388
|
+
extractConversationMessages,
|
|
1389
|
+
extractImages,
|
|
1390
|
+
cursorPayloadToOpenAI,
|
|
1391
|
+
toResponsesInput,
|
|
1392
|
+
toChatMessages,
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
},
|
|
1396
|
+
"src/provider-adapters/openai-compatible.js": function(module, exports, require, __filename, __dirname) {
|
|
1397
|
+
"use strict";
|
|
1398
|
+
|
|
1399
|
+
const { resolveApiKey, resolveModel } = require("../config");
|
|
1400
|
+
|
|
1401
|
+
function endpointFor(provider, kind) {
|
|
1402
|
+
const base = provider.baseUrl.replace(/\/+$/, "");
|
|
1403
|
+
if (kind === "responses") return `${base}/responses`;
|
|
1404
|
+
if (kind === "chat") return `${base}/chat/completions`;
|
|
1405
|
+
throw new Error(`Unknown endpoint kind ${kind}`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function routeRequestModel(config, body, preferredKind) {
|
|
1409
|
+
const route = resolveModel(config, body && body.model);
|
|
1410
|
+
const kind = preferredKind || (route.provider.type === "openai-responses" ? "responses" : "chat");
|
|
1411
|
+
const mapped = { ...(body || {}), model: route.model.apiModel };
|
|
1412
|
+
return { ...route, body: mapped, kind };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
async function proxyOpenAIRequest(config, kind, body, headers = {}) {
|
|
1416
|
+
const route = routeRequestModel(config, body, kind);
|
|
1417
|
+
return fetchProviderWithUnsupportedParamRetry(route.provider, route.kind, route.body, headers);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function outboundHeaders(provider, incoming = {}) {
|
|
1421
|
+
const headers = {
|
|
1422
|
+
"content-type": "application/json",
|
|
1423
|
+
authorization: `Bearer ${resolveApiKey(provider)}`,
|
|
1424
|
+
};
|
|
1425
|
+
for (const [key, value] of Object.entries(incoming || {})) {
|
|
1426
|
+
const lower = key.toLowerCase();
|
|
1427
|
+
if (lower.startsWith("x-") || lower === "openai-beta") headers[key] = value;
|
|
1428
|
+
}
|
|
1429
|
+
return headers;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async function* streamProviderEvents(config, route, openaiBody) {
|
|
1433
|
+
const kind = route.provider.type === "openai-responses" ? "responses" : "chat";
|
|
1434
|
+
const body = {
|
|
1435
|
+
...openaiBody,
|
|
1436
|
+
model: route.model.apiModel,
|
|
1437
|
+
stream: true,
|
|
1438
|
+
};
|
|
1439
|
+
const res = await fetchProviderWithUnsupportedParamRetry(route.provider, kind, body);
|
|
1440
|
+
if (!res.ok) {
|
|
1441
|
+
const text = await res.text();
|
|
1442
|
+
throw new Error(`Provider ${res.status}: ${text.slice(0, 1200)}`);
|
|
1443
|
+
}
|
|
1444
|
+
if (!res.body) throw new Error("Provider response is not streamable");
|
|
1445
|
+
for await (const event of parseSse(res.body)) {
|
|
1446
|
+
if (event === "[DONE]") break;
|
|
1447
|
+
let json;
|
|
1448
|
+
try {
|
|
1449
|
+
json = JSON.parse(event);
|
|
1450
|
+
} catch {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
yield* normalizeProviderEvent(kind, json);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
async function callProviderOnce(config, route, openaiBody) {
|
|
1458
|
+
const kind = route.provider.type === "openai-responses" ? "responses" : "chat";
|
|
1459
|
+
const body = { ...openaiBody, model: route.model.apiModel, stream: false };
|
|
1460
|
+
const res = await fetchProviderWithUnsupportedParamRetry(route.provider, kind, body);
|
|
1461
|
+
const text = await res.text();
|
|
1462
|
+
let json;
|
|
1463
|
+
try {
|
|
1464
|
+
json = JSON.parse(text);
|
|
1465
|
+
} catch {
|
|
1466
|
+
throw new Error(`Provider returned non-JSON ${res.status}: ${text.slice(0, 500)}`);
|
|
1467
|
+
}
|
|
1468
|
+
if (!res.ok) throw new Error(`Provider ${res.status}: ${JSON.stringify(json).slice(0, 1200)}`);
|
|
1469
|
+
return extractFinalText(kind, json);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
async function fetchProviderWithUnsupportedParamRetry(provider, kind, body, headers = {}) {
|
|
1473
|
+
const attempted = new Set();
|
|
1474
|
+
let current = sanitizeBodyForProvider(provider, body);
|
|
1475
|
+
for (;;) {
|
|
1476
|
+
const res = await fetch(endpointFor(provider, kind), {
|
|
1477
|
+
method: "POST",
|
|
1478
|
+
headers: outboundHeaders(provider, headers),
|
|
1479
|
+
body: JSON.stringify(current),
|
|
1480
|
+
});
|
|
1481
|
+
if (res.ok) return res;
|
|
1482
|
+
|
|
1483
|
+
const text = await res.text();
|
|
1484
|
+
const unsupported = parseUnsupportedParameter(text);
|
|
1485
|
+
if (!unsupported || attempted.has(unsupported) || !Object.prototype.hasOwnProperty.call(current, unsupported)) {
|
|
1486
|
+
return responseFromText(text, res);
|
|
1487
|
+
}
|
|
1488
|
+
attempted.add(unsupported);
|
|
1489
|
+
current = { ...current };
|
|
1490
|
+
delete current[unsupported];
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function sanitizeBodyForProvider(provider, body) {
|
|
1495
|
+
const out = { ...(body || {}) };
|
|
1496
|
+
for (const param of provider.omitParams || provider.unsupportedParams || []) {
|
|
1497
|
+
delete out[param];
|
|
1498
|
+
}
|
|
1499
|
+
return out;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function parseUnsupportedParameter(text) {
|
|
1503
|
+
if (!text) return null;
|
|
1504
|
+
try {
|
|
1505
|
+
const json = JSON.parse(text);
|
|
1506
|
+
const message = json?.error?.message || json?.message || "";
|
|
1507
|
+
const param = json?.error?.param || json?.param || "";
|
|
1508
|
+
if (param && /unsupported parameter/i.test(message)) return param;
|
|
1509
|
+
const fromMessage = message.match(/Unsupported parameter:\s*['"]?([A-Za-z0-9_.-]+)['"]?/i);
|
|
1510
|
+
if (fromMessage) return fromMessage[1];
|
|
1511
|
+
} catch {}
|
|
1512
|
+
const match = String(text).match(/Unsupported parameter:\s*['"]?([A-Za-z0-9_.-]+)['"]?/i);
|
|
1513
|
+
return match ? match[1] : null;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function responseFromText(text, res) {
|
|
1517
|
+
return new Response(text, {
|
|
1518
|
+
status: res.status,
|
|
1519
|
+
statusText: res.statusText,
|
|
1520
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function* parseSse(body) {
|
|
1525
|
+
const decoder = new TextDecoder();
|
|
1526
|
+
let buffer = "";
|
|
1527
|
+
for await (const chunk of body) {
|
|
1528
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1529
|
+
let idx;
|
|
1530
|
+
while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
1531
|
+
const raw = buffer.slice(0, idx);
|
|
1532
|
+
buffer = buffer.slice(idx + 2);
|
|
1533
|
+
const data = raw
|
|
1534
|
+
.split(/\r?\n/)
|
|
1535
|
+
.filter((line) => line.startsWith("data:"))
|
|
1536
|
+
.map((line) => line.slice(5).trimStart())
|
|
1537
|
+
.join("\n");
|
|
1538
|
+
if (data) yield data;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
buffer += decoder.decode();
|
|
1542
|
+
const data = buffer
|
|
1543
|
+
.split(/\r?\n/)
|
|
1544
|
+
.filter((line) => line.startsWith("data:"))
|
|
1545
|
+
.map((line) => line.slice(5).trimStart())
|
|
1546
|
+
.join("\n");
|
|
1547
|
+
if (data) yield data;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function* normalizeProviderEvent(kind, event) {
|
|
1551
|
+
if (kind === "chat") {
|
|
1552
|
+
const choice = event.choices && event.choices[0];
|
|
1553
|
+
const delta = choice && choice.delta;
|
|
1554
|
+
if (delta && typeof delta.content === "string" && delta.content) {
|
|
1555
|
+
yield { type: "text_delta", text: delta.content };
|
|
1556
|
+
}
|
|
1557
|
+
for (const toolCall of delta?.tool_calls || []) {
|
|
1558
|
+
yield {
|
|
1559
|
+
type: "tool_call_delta",
|
|
1560
|
+
id: toolCall.id,
|
|
1561
|
+
index: toolCall.index,
|
|
1562
|
+
name: toolCall.function && toolCall.function.name,
|
|
1563
|
+
argumentsDelta: toolCall.function && toolCall.function.arguments,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
if (choice && choice.finish_reason) yield { type: "finish", reason: choice.finish_reason };
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
1571
|
+
yield { type: "text_delta", text: event.delta };
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
1575
|
+
yield {
|
|
1576
|
+
type: "tool_call_delta",
|
|
1577
|
+
id: event.item_id,
|
|
1578
|
+
name: event.name,
|
|
1579
|
+
argumentsDelta: event.delta || "",
|
|
1580
|
+
};
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (event.type === "response.output_item.done" && event.item && event.item.type === "function_call") {
|
|
1584
|
+
yield {
|
|
1585
|
+
type: "tool_call",
|
|
1586
|
+
id: event.item.call_id || event.item.id,
|
|
1587
|
+
name: event.item.name,
|
|
1588
|
+
arguments: event.item.arguments || "{}",
|
|
1589
|
+
};
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (event.type === "response.completed") yield { type: "finish", reason: "completed" };
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function extractFinalText(kind, response) {
|
|
1596
|
+
if (kind === "chat") {
|
|
1597
|
+
return response?.choices?.[0]?.message?.content || "";
|
|
1598
|
+
}
|
|
1599
|
+
if (typeof response.output_text === "string") return response.output_text;
|
|
1600
|
+
const parts = [];
|
|
1601
|
+
for (const item of response.output || []) {
|
|
1602
|
+
for (const content of item.content || []) {
|
|
1603
|
+
if (typeof content.text === "string") parts.push(content.text);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return parts.join("");
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
module.exports = {
|
|
1610
|
+
endpointFor,
|
|
1611
|
+
routeRequestModel,
|
|
1612
|
+
proxyOpenAIRequest,
|
|
1613
|
+
streamProviderEvents,
|
|
1614
|
+
callProviderOnce,
|
|
1615
|
+
parseSse,
|
|
1616
|
+
normalizeProviderEvent,
|
|
1617
|
+
extractFinalText,
|
|
1618
|
+
fetchProviderWithUnsupportedParamRetry,
|
|
1619
|
+
parseUnsupportedParameter,
|
|
1620
|
+
sanitizeBodyForProvider,
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
},
|
|
1624
|
+
"src/patcher/index.js": function(module, exports, require, __filename, __dirname) {
|
|
1625
|
+
"use strict";
|
|
1626
|
+
|
|
1627
|
+
const fs = require("fs");
|
|
1628
|
+
const path = require("path");
|
|
1629
|
+
const { findCursorAppRoot, readCursorInfo } = require("./cursor-paths");
|
|
1630
|
+
const { MARKER, LEGACY_MARKER, helperSnippet, agentLoopSnippet } = require("./templates");
|
|
1631
|
+
|
|
1632
|
+
const RUN_LOOP_NEEDLE = `_e=SC(n.withName("AgentCompatService.agentClientService.run")),oe=new Q$t(this.instantiationService,z,re,f,ue);`;
|
|
1633
|
+
const CONNECT_HELPER_NEEDLE = `function E5_(n,e){return vp1(n,t=>{switch(t.kind){case rn.Unary:return wp1(e,n,t);case rn.ServerStreaming:return Sp1(e,n,t);case rn.ClientStreaming:return kp1(e,n,t);case rn.BiDiStreaming:return Cp1(e,n,t);default:return null}})}`;
|
|
1634
|
+
const UNARY_NEEDLE = `const s=await n.unary(e,t,r?.signal,r?.timeoutMs,r?.headers,i,r?.contextValues);return r?.onHeader?.(s.header),r?.onTrailer?.(s.trailer),s.message`;
|
|
1635
|
+
const MODEL_ENRICH_NEEDLE = `s=XI_(s,a.models);`;
|
|
1636
|
+
|
|
1637
|
+
function status(options = {}) {
|
|
1638
|
+
const appRoot = findCursorAppRoot(options.appRoot);
|
|
1639
|
+
const info = readCursorInfo(appRoot);
|
|
1640
|
+
const source = fs.readFileSync(info.workbenchPath, "utf8");
|
|
1641
|
+
return {
|
|
1642
|
+
...info,
|
|
1643
|
+
supportedVersion: isSupportedVersion(info.version),
|
|
1644
|
+
patched: source.includes(MARKER),
|
|
1645
|
+
legacyPatched: source.includes(LEGACY_MARKER),
|
|
1646
|
+
hasRunLoopNeedle: !!findRunLoopPatchPoint(source),
|
|
1647
|
+
hasConnectNeedle: !!findConnectHelperNeedle(source),
|
|
1648
|
+
hasModelEnrichNeedle: source.includes(MODEL_ENRICH_NEEDLE),
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function installPatch(options = {}) {
|
|
1653
|
+
const appRoot = findCursorAppRoot(options.appRoot);
|
|
1654
|
+
const info = readCursorInfo(appRoot);
|
|
1655
|
+
if (!isSupportedVersion(info.version) && !options.force) {
|
|
1656
|
+
throw new Error(`Unsupported Cursor version ${info.version}. Re-run with --force only after validating needles.`);
|
|
1657
|
+
}
|
|
1658
|
+
let source = fs.readFileSync(info.workbenchPath, "utf8");
|
|
1659
|
+
if (source.includes(MARKER)) return { ...info, changed: false, alreadyInstalled: true };
|
|
1660
|
+
if (source.includes(LEGACY_MARKER) && !options.force) {
|
|
1661
|
+
throw new Error("Legacy local BYOK patch is already present. Restore Cursor from backup or use --force after manual review.");
|
|
1662
|
+
}
|
|
1663
|
+
const connectNeedle = findConnectHelperNeedle(source);
|
|
1664
|
+
const runLoop = findRunLoopPatchPoint(source);
|
|
1665
|
+
if (!runLoop) throw new Error("AgentCompatService.runAgentLoop needle not found");
|
|
1666
|
+
if (!connectNeedle) throw new Error("Connect helper needle not found");
|
|
1667
|
+
if (!source.includes(UNARY_NEEDLE)) throw new Error("Unary transport needle not found");
|
|
1668
|
+
if (!source.includes(MODEL_ENRICH_NEEDLE)) throw new Error("Model enrichment needle not found");
|
|
1669
|
+
|
|
1670
|
+
const backupPath = backupWorkbench(info.workbenchPath);
|
|
1671
|
+
source = source.replace(connectNeedle, helperSnippet() + connectNeedle);
|
|
1672
|
+
source = source.replace(
|
|
1673
|
+
UNARY_NEEDLE,
|
|
1674
|
+
`const s=await n.unary(e,t,r?.signal,r?.timeoutMs,r?.headers,i,r?.contextValues);try{if(e?.typeName==="aiserver.v1.AiService"&&t?.name==="AvailableModels")await ${MARKER}().patchAvailableModels(s.message)}catch{}return r?.onHeader?.(s.header),r?.onTrailer?.(s.trailer),s.message`
|
|
1675
|
+
);
|
|
1676
|
+
source = source.replace(MODEL_ENRICH_NEEDLE, `s=XI_(s,a.models);s=await ${MARKER}().enrichModels(s);`);
|
|
1677
|
+
source = source.replace(runLoop.needle, runLoop.needle + agentLoopSnippet(runLoop.vars));
|
|
1678
|
+
fs.writeFileSync(info.workbenchPath, source);
|
|
1679
|
+
return { ...info, changed: true, backupPath };
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function uninstallPatch(options = {}) {
|
|
1683
|
+
const appRoot = findCursorAppRoot(options.appRoot);
|
|
1684
|
+
const info = readCursorInfo(appRoot);
|
|
1685
|
+
const backups = findBackups(info.workbenchPath).filter((file) => {
|
|
1686
|
+
const data = fs.readFileSync(file, "utf8");
|
|
1687
|
+
return !data.includes(MARKER);
|
|
1688
|
+
});
|
|
1689
|
+
if (backups.length === 0) throw new Error("No suitable cursor-openai-byok backup found");
|
|
1690
|
+
const latest = backups[backups.length - 1];
|
|
1691
|
+
const currentBackup = backupWorkbench(info.workbenchPath, "pre-uninstall");
|
|
1692
|
+
fs.copyFileSync(latest, info.workbenchPath);
|
|
1693
|
+
return { ...info, restoredFrom: latest, currentBackup };
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function backupWorkbench(workbenchPath, label = "backup") {
|
|
1697
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1698
|
+
const backup = `${workbenchPath}.${label}-cursor-openai-byok-${stamp}`;
|
|
1699
|
+
fs.copyFileSync(workbenchPath, backup);
|
|
1700
|
+
return backup;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function findBackups(workbenchPath) {
|
|
1704
|
+
const dir = path.dirname(workbenchPath);
|
|
1705
|
+
const base = path.basename(workbenchPath);
|
|
1706
|
+
return fs.readdirSync(dir)
|
|
1707
|
+
.filter((name) => name.startsWith(`${base}.backup-cursor-openai-byok-`) || name.includes("cursor-openai-byok"))
|
|
1708
|
+
.map((name) => path.join(dir, name))
|
|
1709
|
+
.sort();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function isSupportedVersion(version) {
|
|
1713
|
+
return /^3\.8\./.test(version);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function findConnectHelperNeedle(source) {
|
|
1717
|
+
if (source.includes(CONNECT_HELPER_NEEDLE)) return CONNECT_HELPER_NEEDLE;
|
|
1718
|
+
const match = source.match(/function\s+\w+\(n,e\)\{return\s+\w+\(n,t=>\{switch\(t\.kind\)\{case\s+\w+\.Unary:return\s+\w+\(e,n,t\);case\s+\w+\.ServerStreaming:return\s+\w+\(e,n,t\);case\s+\w+\.ClientStreaming:return\s+\w+\(e,n,t\);case\s+\w+\.BiDiStreaming:return\s+\w+\(e,n,t\);default:return null\}\}\)\}/);
|
|
1719
|
+
return match ? match[0] : null;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function findRunLoopPatchPoint(source) {
|
|
1723
|
+
if (source.includes(RUN_LOOP_NEEDLE)) {
|
|
1724
|
+
return {
|
|
1725
|
+
needle: RUN_LOOP_NEEDLE,
|
|
1726
|
+
vars: undefined,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
const re = /const\{userText:(\w+),richText:(\w+),mode:(\w+),selectedContext:(\w+),requestContext:(\w+),modelDetails:(\w+),generationUUID:(\w+),conversationId:(\w+),messageId:(\w+),[\s\S]{0,900}?conversationState:(\w+),[\s\S]{0,1600}?(\w+)=_b\(n\.withName\("AgentCompatService\.agentClientService\.run"\)\),(\w+)=new\s+\w+\(this\.instantiationService,[^;]+?\);/;
|
|
1730
|
+
const match = source.match(re);
|
|
1731
|
+
if (!match) return null;
|
|
1732
|
+
return {
|
|
1733
|
+
needle: match[0],
|
|
1734
|
+
vars: {
|
|
1735
|
+
userText: match[1],
|
|
1736
|
+
richText: match[2],
|
|
1737
|
+
mode: match[3],
|
|
1738
|
+
selectedContext: match[4],
|
|
1739
|
+
requestContext: match[5],
|
|
1740
|
+
model: match[6],
|
|
1741
|
+
generationUUID: match[7],
|
|
1742
|
+
conversationId: match[8],
|
|
1743
|
+
messageId: match[9],
|
|
1744
|
+
conversationState: match[10],
|
|
1745
|
+
ctx: match[11],
|
|
1746
|
+
adapter: match[12],
|
|
1747
|
+
event: "e",
|
|
1748
|
+
},
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
module.exports = { status, installPatch, uninstallPatch };
|
|
1753
|
+
|
|
1754
|
+
},
|
|
1755
|
+
"src/patcher/cursor-paths.js": function(module, exports, require, __filename, __dirname) {
|
|
1756
|
+
"use strict";
|
|
1757
|
+
|
|
1758
|
+
const fs = require("fs");
|
|
1759
|
+
const path = require("path");
|
|
1760
|
+
const os = require("os");
|
|
1761
|
+
|
|
1762
|
+
function candidateCursorAppRoots() {
|
|
1763
|
+
if (process.platform === "darwin") {
|
|
1764
|
+
return [
|
|
1765
|
+
"/Applications/Cursor.app/Contents/Resources/app",
|
|
1766
|
+
path.join(os.homedir(), "Applications/Cursor.app/Contents/Resources/app"),
|
|
1767
|
+
];
|
|
1768
|
+
}
|
|
1769
|
+
if (process.platform === "win32") {
|
|
1770
|
+
const local = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
1771
|
+
return [
|
|
1772
|
+
path.join(local, "Programs", "Cursor", "resources", "app"),
|
|
1773
|
+
path.join(local, "Cursor", "resources", "app"),
|
|
1774
|
+
];
|
|
1775
|
+
}
|
|
1776
|
+
return [];
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function findCursorAppRoot(explicitRoot) {
|
|
1780
|
+
const roots = explicitRoot ? [explicitRoot] : candidateCursorAppRoots();
|
|
1781
|
+
for (const root of roots) {
|
|
1782
|
+
const workbench = workbenchPath(root);
|
|
1783
|
+
if (fs.existsSync(workbench)) return root;
|
|
1784
|
+
}
|
|
1785
|
+
throw new Error(`Cursor app root not found. Checked: ${roots.join(", ")}`);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function workbenchPath(appRoot) {
|
|
1789
|
+
return path.join(appRoot, "out", "vs", "workbench", "workbench.desktop.main.js");
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function productJsonPath(appRoot) {
|
|
1793
|
+
return path.join(appRoot, "product.json");
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function packageJsonPath(appRoot) {
|
|
1797
|
+
return path.join(appRoot, "package.json");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function readCursorInfo(appRoot) {
|
|
1801
|
+
const productPath = productJsonPath(appRoot);
|
|
1802
|
+
const packagePath = packageJsonPath(appRoot);
|
|
1803
|
+
const product = fs.existsSync(productPath) ? JSON.parse(fs.readFileSync(productPath, "utf8")) : {};
|
|
1804
|
+
const pkg = fs.existsSync(packagePath) ? JSON.parse(fs.readFileSync(packagePath, "utf8")) : {};
|
|
1805
|
+
return {
|
|
1806
|
+
appRoot,
|
|
1807
|
+
version: product.version || pkg.version || "unknown",
|
|
1808
|
+
commit: product.commit || "unknown",
|
|
1809
|
+
workbenchPath: workbenchPath(appRoot),
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
module.exports = {
|
|
1814
|
+
candidateCursorAppRoots,
|
|
1815
|
+
findCursorAppRoot,
|
|
1816
|
+
workbenchPath,
|
|
1817
|
+
readCursorInfo,
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
},
|
|
1821
|
+
"src/patcher/templates.js": function(module, exports, require, __filename, __dirname) {
|
|
1822
|
+
"use strict";
|
|
1823
|
+
|
|
1824
|
+
const MARKER = "__CURSOR_OPENAI_BYOK_V1";
|
|
1825
|
+
const LEGACY_MARKER = "__LOCAL_CURSOR_BYOK_V0";
|
|
1826
|
+
const ENDPOINT = "http://127.0.0.1:39832";
|
|
1827
|
+
|
|
1828
|
+
function helperSnippet() {
|
|
1829
|
+
return `function ${MARKER}(){if(globalThis.${MARKER})return globalThis.${MARKER};const endpoint="${ENDPOINT}";const sleep=ms=>new Promise(r=>setTimeout(r,ms));const tip=t=>({primaryText:"",secondaryText:"",secondaryWarningText:false,icon:"",tertiaryText:"",tertiaryTextUrl:"",markdownContent:t});const esc=v=>String(v??"").replace(/[\\\\*_\\[\\]\`]/g,"\\\\$&");const byok={async models(){const started=Date.now();for(;;){try{const r=await fetch(endpoint+"/cursor/v1/models");const j=await r.json();return j&&j.ok?j.models||[]:[]}catch(e){if(Date.now()-started>2e3)return[];await sleep(120)}}},toCursorModel(m){const name=m.displayName,provider=m.providerName||m.providerId||"BYOK",ctx=m.contextTokenLimit||128000,md="**"+esc(name)+"**<br />OpenAI-compatible BYOK model via "+esc(provider)+".<br /><br />"+ctx.toLocaleString()+" context window";return{name,defaultOn:true,parameterDefinitions:[],variants:[{parameterValues:[],displayName:name,isMaxMode:false,isDefaultMaxConfig:true,isDefaultNonMaxConfig:true,tooltipData:tip(md),displayNameOutsidePicker:name,variantStringRepresentation:name+"[]",legacySlug:name}],legacySlugs:[name],idAliases:[name],cloudAgentEffortModes:[],supportsAgent:true,degradationStatus:0,tooltipData:tip(md),supportsThinking:false,supportsImages:m.supportsImages!==false,supportsMaxMode:true,clientDisplayName:name,serverModelName:name,supportsNonMaxMode:true,tooltipDataForMaxMode:tip(md),isRecommendedForBackgroundComposer:false,supportsPlanMode:true,inputboxShortModelName:name,supportsSandboxing:true,supportsCmdK:true,contextTokenLimit:ctx,contextTokenLimitForMaxMode:ctx,maxOutputTokens:m.maxOutputTokens||8192,namedModelSectionIndex:1,vendorName:provider,vendor:{id:9000,displayName:provider},tagline:"OpenAI-compatible BYOK",visibleInRoutedModelView:true,isUserAdded:true,byokModel:true}},async shouldHandle(model){try{if(!model)return false;const r=await fetch(endpoint+"/cursor/v1/should-handle?model="+encodeURIComponent(model));const j=await r.json();return !!(j&&j.ok&&j.handle)}catch{return false}},async enrichModels(list){try{const models=await this.models();if(!models.length)return list;const out=Array.isArray(list)?list:[];for(const m of models){const cm=this.toCursorModel(m),i=out.findIndex(x=>x&&(x.name===m.displayName||x.clientDisplayName===m.displayName||x.serverModelName===m.displayName));i>=0?out[i]={...out[i],...cm}:out.push(cm)}return out}catch(e){console.warn("cursor-openai-byok model enrich failed",e);return list}},async patchAvailableModels(msg){try{const models=await this.models();if(!models.length||!msg)return;msg.modelNames=msg.modelNames||[];msg.models=await this.enrichModels(msg.models||[]);for(const m of models)if(!msg.modelNames.includes(m.displayName))msg.modelNames.push(m.displayName)}catch(e){console.warn("cursor-openai-byok model patch failed",e)}},async runAgent(payload,onEvent){const res=await fetch(endpoint+"/cursor/v1/run-agent-loop",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload)});if(!res.ok)throw new Error("BYOK HTTP "+res.status);const reader=res.body.getReader();const decoder=new TextDecoder();let buf="";for(;;){const item=await reader.read();if(item.done)break;buf+=decoder.decode(item.value,{stream:true});let idx;while((idx=buf.indexOf("\\n"))>=0){const line=buf.slice(0,idx).trim();buf=buf.slice(idx+1);if(line){try{await onEvent(JSON.parse(line))}catch(e){console.warn("cursor-openai-byok event failed",e)}}}}}};globalThis.${MARKER}=byok;return byok}`;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function agentLoopSnippet(vars) {
|
|
1833
|
+
const v = vars || {
|
|
1834
|
+
model: "h",
|
|
1835
|
+
adapter: "oe",
|
|
1836
|
+
ctx: "_e",
|
|
1837
|
+
event: "e",
|
|
1838
|
+
userText: "o",
|
|
1839
|
+
richText: "a",
|
|
1840
|
+
mode: "c",
|
|
1841
|
+
selectedContext: "d",
|
|
1842
|
+
requestContext: "m",
|
|
1843
|
+
generationUUID: "f",
|
|
1844
|
+
conversationId: "v",
|
|
1845
|
+
messageId: "b",
|
|
1846
|
+
conversationState: "W",
|
|
1847
|
+
};
|
|
1848
|
+
return `if(!${v.event}.subagentTypeName&&await ${MARKER}().shouldHandle(${v.model}?.modelName)){const sendText=async text=>{if(text)await ${v.adapter}.sendUpdate(${v.ctx}.ctx,E0c.textDelta(text))};const sendDone=async()=>{await ${v.adapter}.sendUpdate(${v.ctx}.ctx,E0c.turnEnded())};try{await ${MARKER}().runAgent({model:${v.model}?.modelName,userText:${v.userText},richText:${v.richText},mode:${v.mode},selectedContext:${v.selectedContext},requestContext:${v.requestContext},generationUUID:${v.generationUUID},conversationId:${v.conversationId},messageId:${v.messageId},conversationState:${v.conversationState},event:${v.event},tools:${v.event}.tools},async ev=>{if(ev.type==="text_delta")await sendText(ev.text||"");else if(ev.type==="tool_calls_ready")await sendText("\\n[BYOK tool calls requested: "+(ev.toolCalls||[]).map(t=>t.name||t.id).join(", ")+"]\\n");else if(ev.type==="error")await sendText("Local BYOK error: "+(ev.error||"unknown"));});await sendDone();return}catch(err){await sendText("Local BYOK error: "+(err&&err.message?err.message:String(err)));await sendDone();return}}`;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
module.exports = { MARKER, LEGACY_MARKER, helperSnippet, agentLoopSnippet };
|
|
1852
|
+
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
const __bundleCache = Object.create(null);
|
|
1856
|
+
const __bundleNodeRequire = require;
|
|
1857
|
+
|
|
1858
|
+
function __bundleNormalize(id) {
|
|
1859
|
+
return id.split(__bundlePath.sep).join("/");
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function __bundleResolve(fromId, request) {
|
|
1863
|
+
if (!request.startsWith(".")) return null;
|
|
1864
|
+
const base = __bundlePath.resolve(__bundleRoot, __bundlePath.dirname(fromId), request);
|
|
1865
|
+
for (const candidate of [base, base + ".js", __bundlePath.join(base, "index.js")]) {
|
|
1866
|
+
const id = __bundleNormalize(__bundlePath.relative(__bundleRoot, candidate));
|
|
1867
|
+
if (__bundleModules[id]) return id;
|
|
1868
|
+
}
|
|
1869
|
+
throw new Error("Cannot resolve " + request + " from " + fromId);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function __bundleRequire(id) {
|
|
1873
|
+
if (__bundleCache[id]) return __bundleCache[id].exports;
|
|
1874
|
+
const factory = __bundleModules[id];
|
|
1875
|
+
if (!factory) return __bundleNodeRequire(id);
|
|
1876
|
+
const module = { exports: {} };
|
|
1877
|
+
__bundleCache[id] = module;
|
|
1878
|
+
const filename = __bundlePath.join(__bundleRoot, id);
|
|
1879
|
+
const dirname = __bundlePath.dirname(filename);
|
|
1880
|
+
function localRequire(request) {
|
|
1881
|
+
const localId = __bundleResolve(id, request);
|
|
1882
|
+
return localId ? __bundleRequire(localId) : __bundleNodeRequire(request);
|
|
1883
|
+
}
|
|
1884
|
+
factory(module, module.exports, localRequire, filename, dirname);
|
|
1885
|
+
return module.exports;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
module.exports = __bundleRequire("src/cli/index.js");
|