ocx-cursor 0.5.1 → 1.0.0
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 +102 -43
- package/bin/ocx-cursor.mjs +19 -6
- package/package.json +2 -2
- package/src/cursor-patch.mjs +41 -10
- package/src/cursor-state.mjs +24 -3
- package/src/gateway.mjs +29 -10
- package/src/install.mjs +8 -2
- package/src/paths.mjs +1 -1
- package/src/service.mjs +2 -2
- package/src/setup.mjs +1 -1
package/README.md
CHANGED
|
@@ -3,16 +3,29 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/ocx-cursor)
|
|
4
4
|
[](https://github.com/hiddenest/opencodex-cursor-bridge/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
|
-
Use your active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through
|
|
6
|
+
Use your active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through an authenticated HTTPS endpoint on your Mac.
|
|
7
7
|
|
|
8
8
|
> [!WARNING]
|
|
9
|
-
>
|
|
9
|
+
> The installer edits two private workbench files inside `Cursor.app`. This invalidates Cursor's vendor signature. Cursor may report a corrupt installation. The bridge saves the original files under `~/.opencodex/cursor-bridge/cursor-app-backups`. Reinstall Cursor to restore a signed app.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Requirements
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- macOS with Cursor installed at `/Applications/Cursor.app`
|
|
14
|
+
- Node.js 22.5 or newer
|
|
15
|
+
- OpenCodex with at least one active model
|
|
16
|
+
- An HTTPS hostname that forwards to `http://127.0.0.1:10101`
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
Use [Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/) for the HTTPS hostname, or choose another reverse proxy that preserves the `Authorization` header.
|
|
19
|
+
|
|
20
|
+
## Upgrading from 0.5.x
|
|
21
|
+
|
|
22
|
+
Version 0.5.x enabled Cursor's local-agent mode and edited ten app files. Version 1.0.0 edits two workbench files and leaves local-agent mode disabled.
|
|
23
|
+
|
|
24
|
+
Reinstall Cursor before upgrading so macOS replaces the old app files. Quit the new Cursor installation, then follow the setup below. Your Cursor settings remain under `~/Library/Application Support/Cursor`.
|
|
25
|
+
|
|
26
|
+
## Getting started
|
|
27
|
+
|
|
28
|
+
Install OpenCodex and start its login service:
|
|
16
29
|
|
|
17
30
|
```bash
|
|
18
31
|
npm install --global @bitkyc08/opencodex
|
|
@@ -21,96 +34,142 @@ ocx service install
|
|
|
21
34
|
ocx service status
|
|
22
35
|
```
|
|
23
36
|
|
|
24
|
-
Check
|
|
37
|
+
OpenCodex listens on `http://127.0.0.1:10100`. Check its active models:
|
|
25
38
|
|
|
26
39
|
```bash
|
|
27
40
|
ocx models --json
|
|
28
41
|
```
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
Install `cloudflared`, sign in, and create a named tunnel:
|
|
31
44
|
|
|
32
45
|
```bash
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
brew install cloudflared
|
|
47
|
+
cloudflared tunnel login
|
|
48
|
+
cloudflared tunnel create ocx-cursor
|
|
49
|
+
cloudflared tunnel list
|
|
35
50
|
```
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
Create `~/.cloudflared/config.yml`. Replace the UUID, username, and hostname:
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
```yaml
|
|
55
|
+
tunnel: YOUR_TUNNEL_UUID
|
|
56
|
+
credentials-file: /Users/YOUR_USERNAME/.cloudflared/YOUR_TUNNEL_UUID.json
|
|
57
|
+
|
|
58
|
+
ingress:
|
|
59
|
+
- hostname: cursor-api.example.com
|
|
60
|
+
service: http://127.0.0.1:10101
|
|
61
|
+
- service: http_status:404
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Create the DNS record and check the ingress rule:
|
|
40
65
|
|
|
41
66
|
```bash
|
|
42
|
-
|
|
67
|
+
cloudflared tunnel route dns ocx-cursor cursor-api.example.com
|
|
68
|
+
cloudflared tunnel ingress validate
|
|
69
|
+
cloudflared tunnel ingress rule https://cursor-api.example.com
|
|
43
70
|
```
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
Start the tunnel at login with Cloudflare's [macOS service command](https://developers.cloudflare.com/tunnel/advanced/local-management/as-a-service/macos/):
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| Cursor model state | Adds active models under `opencodex/*` with names, effort choices, and Fast metadata. |
|
|
51
|
-
| `Cursor.app` | Enables local-agent mode in eight bundles and patches two local-agent runtimes. |
|
|
52
|
-
| Login service | Installs `com.opencodex.cursor-bridge`, refreshes models, and reapplies patches after Cursor updates. |
|
|
74
|
+
```bash
|
|
75
|
+
cloudflared service install
|
|
76
|
+
```
|
|
53
77
|
|
|
54
|
-
|
|
78
|
+
This command reads `~/.cloudflared/config.yml`. `sudo cloudflared service install` starts the tunnel at boot and reads `/etc/cloudflared/config.yml` instead.
|
|
79
|
+
|
|
80
|
+
Launch Cursor once and sign in, then quit it. Install the bridge:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx ocx-cursor init --base-url https://cursor-api.example.com/v1
|
|
84
|
+
~/.local/bin/ocx-cursor status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Open Cursor after `status` reports a running service and a healthy gateway.
|
|
88
|
+
|
|
89
|
+
## How requests move
|
|
55
90
|
|
|
56
91
|
```text
|
|
57
|
-
Cursor
|
|
92
|
+
Cursor
|
|
93
|
+
-> https://cursor-api.example.com/v1
|
|
94
|
+
-> Cloudflare Tunnel
|
|
95
|
+
-> OpenCodex Cursor Bridge on 127.0.0.1:10101
|
|
96
|
+
-> OpenCodex on 127.0.0.1:10100
|
|
97
|
+
-> your provider
|
|
58
98
|
```
|
|
59
99
|
|
|
60
|
-
The
|
|
100
|
+
The bridge generates a bearer token during `init`, stores it in Cursor with macOS Safe Storage, and checks it on each `/v1/*` request. The gateway binds to `127.0.0.1` unless you set `OCX_CURSOR_HOST`.
|
|
61
101
|
|
|
62
|
-
|
|
102
|
+
The package does not create, edit, or remove your Cloudflare Tunnel.
|
|
63
103
|
|
|
64
|
-
|
|
104
|
+
## Models and Cursor metadata
|
|
105
|
+
|
|
106
|
+
The bridge reads the active OpenCodex catalog and adds stable aliases to Cursor:
|
|
65
107
|
|
|
66
108
|
```text
|
|
67
109
|
anthropic/claude-sonnet-5 -> opencodex/claude-sonnet-5
|
|
68
110
|
gpt-5.6-sol -> opencodex/gpt-5.6-sol
|
|
69
111
|
```
|
|
70
112
|
|
|
71
|
-
Cursor
|
|
113
|
+
Cursor displays provider prefixes where model names overlap. Examples include `Cursor Kimi K3` and `OpenCode Go Kimi K3`. Claude and GPT names stay short.
|
|
114
|
+
|
|
115
|
+
Reasoning models receive an effort selector. Models that advertise OpenCodex's `priority` service tier receive a Fast toggle. The gateway maps those choices to `reasoning_effort` and `service_tier: priority`.
|
|
116
|
+
|
|
117
|
+
The installer patches these files:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
/Applications/Cursor.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js
|
|
121
|
+
/Applications/Cursor.app/Contents/Resources/app/out/vs/workbench/workbench.glass.main.js
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The patch keeps the model metadata that Cursor would discard. It leaves Cursor's local-agent mode disabled and does not edit either local-agent runtime.
|
|
72
125
|
|
|
73
|
-
|
|
126
|
+
The login service refreshes the catalog every 15 seconds. It waits for Cursor to quit before writing the state database or reapplying a patch after a Cursor update.
|
|
74
127
|
|
|
75
|
-
## Commands
|
|
128
|
+
## Commands
|
|
76
129
|
|
|
77
|
-
| Command |
|
|
130
|
+
| Command | Action |
|
|
78
131
|
| --- | --- |
|
|
79
|
-
| `ocx-cursor init` | Install the service,
|
|
80
|
-
| `ocx-cursor install` | Reinstall the service and check
|
|
81
|
-
| `ocx-cursor update` | Install the
|
|
82
|
-
| `ocx-cursor sync` |
|
|
83
|
-
| `ocx-cursor status` |
|
|
132
|
+
| `ocx-cursor init --base-url URL` | Install the service, test the HTTPS endpoint, configure Cursor, and sync models. |
|
|
133
|
+
| `ocx-cursor install` | Reinstall the service and check the two metadata patches. |
|
|
134
|
+
| `ocx-cursor update` | Install the current npm release and restart the service. |
|
|
135
|
+
| `ocx-cursor sync` | Read the active OpenCodex catalog and update Cursor. |
|
|
136
|
+
| `ocx-cursor status` | Print service, gateway, catalog, and pending-sync status. |
|
|
84
137
|
| `ocx-cursor uninstall` | Remove the service, command link, and bridge state. |
|
|
85
138
|
|
|
86
|
-
`uninstall`
|
|
139
|
+
`uninstall` leaves Cursor's endpoint setting and patched workbench files in place. Reinstall Cursor if you want a signed app without the metadata patch.
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
87
142
|
|
|
88
143
|
| Variable | Default |
|
|
89
144
|
| --- | --- |
|
|
90
|
-
| `OCX_CURSOR_BASE_URL` |
|
|
145
|
+
| `OCX_CURSOR_BASE_URL` | Cursor's stored endpoint |
|
|
91
146
|
| `OCX_CURSOR_HOME` | `~/.opencodex/cursor-bridge` |
|
|
92
147
|
| `OCX_CURSOR_HOST` | `127.0.0.1` |
|
|
93
148
|
| `OCX_CURSOR_PORT` | `10101` |
|
|
94
149
|
| `OCX_BIN` | `~/.local/bin/ocx` |
|
|
95
150
|
|
|
96
|
-
The
|
|
151
|
+
The gateway exposes `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/responses/compact`, and `POST /v1/messages`.
|
|
152
|
+
|
|
153
|
+
The bridge reads `~/.opencodex/service-api-token` when OpenCodex uses service authentication.
|
|
97
154
|
|
|
98
155
|
## Troubleshooting
|
|
99
156
|
|
|
100
|
-
| Symptom |
|
|
157
|
+
| Symptom | Check |
|
|
101
158
|
| --- | --- |
|
|
102
|
-
| Cursor returns 401 | Quit Cursor and rerun `npx ocx-cursor init`. |
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
159
|
+
| Cursor returns 401 | Quit Cursor and rerun `npx ocx-cursor init --base-url https://YOUR_HOST/v1`. |
|
|
160
|
+
| The hostname returns 502 | Run `ocx-cursor status`, then check that the tunnel points to port `10101`. |
|
|
161
|
+
| Cloudflare returns 1016 | Run `cloudflared tunnel info ocx-cursor` and check the service. |
|
|
162
|
+
| Models or controls are missing | Quit Cursor and run `ocx-cursor sync`. |
|
|
163
|
+
| A Cursor update removed the patch | Quit Cursor and run `ocx-cursor install`. |
|
|
164
|
+
|
|
165
|
+
Read the service logs:
|
|
107
166
|
|
|
108
167
|
```bash
|
|
109
168
|
tail -n 100 ~/.opencodex/cursor-bridge/service.log
|
|
110
169
|
tail -n 100 ~/.opencodex/cursor-bridge/service.error.log
|
|
111
170
|
```
|
|
112
171
|
|
|
113
|
-
The patcher stops
|
|
172
|
+
The patcher stops if a Cursor update removes its known catalog hook. It records the file and error without replacing that file.
|
|
114
173
|
|
|
115
174
|
## Development
|
|
116
175
|
|
package/bin/ocx-cursor.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import {
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { configureCursorOpenAI, storedCursorOpenAIBaseUrl } from "../src/cursor-config.mjs";
|
|
6
7
|
import { cursorIsRunning } from "../src/cursor-state.mjs";
|
|
7
8
|
import { installService, prepareInstallSecret, serviceStatus, uninstallService, updateService } from "../src/install.mjs";
|
|
8
|
-
import {
|
|
9
|
+
import { cursorOpenAIBaseUrl, pendingFile } from "../src/paths.mjs";
|
|
9
10
|
import { runService } from "../src/service.mjs";
|
|
10
11
|
import { normalizeBaseUrl, testEndpoint } from "../src/setup.mjs";
|
|
11
12
|
import { loadCatalogSnapshot, syncNow } from "../src/sync.mjs";
|
|
@@ -36,7 +37,19 @@ function argumentValue(name) {
|
|
|
36
37
|
|
|
37
38
|
async function requestedBaseUrl() {
|
|
38
39
|
const supplied = argumentValue("--base-url");
|
|
39
|
-
|
|
40
|
+
const fallback = cursorOpenAIBaseUrl || storedCursorOpenAIBaseUrl();
|
|
41
|
+
if (supplied) return normalizeBaseUrl(supplied);
|
|
42
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return normalizeBaseUrl(fallback);
|
|
43
|
+
|
|
44
|
+
const prompt = fallback
|
|
45
|
+
? `HTTPS endpoint [${fallback}]: `
|
|
46
|
+
: "HTTPS endpoint (for example, https://cursor-api.example.com): ";
|
|
47
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
48
|
+
try {
|
|
49
|
+
return normalizeBaseUrl((await readline.question(prompt)).trim() || fallback);
|
|
50
|
+
} finally {
|
|
51
|
+
readline.close();
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
async function pendingCount() {
|
|
@@ -63,7 +76,7 @@ function printCursorPatches(result) {
|
|
|
63
76
|
return;
|
|
64
77
|
}
|
|
65
78
|
const changed = result.cursorPatches.filter(({ status }) => status === "patched").length;
|
|
66
|
-
process.stdout.write(`Verified ${result.cursorPatches.length} Cursor
|
|
79
|
+
process.stdout.write(`Verified ${result.cursorPatches.length} Cursor metadata patches (${changed} changed).\n`);
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
async function main() {
|
|
@@ -83,7 +96,7 @@ async function main() {
|
|
|
83
96
|
process.stdout.write(`Installed ${installed.launchAgentFile} (API key ${installed.secretStatus}).\nCLI: ${installed.cliLinkFile}\n`);
|
|
84
97
|
printCursorPatches(installed);
|
|
85
98
|
printSync(await syncNow());
|
|
86
|
-
process.stdout.write("
|
|
99
|
+
process.stdout.write("Local gateway: http://127.0.0.1:10101/v1\n");
|
|
87
100
|
return;
|
|
88
101
|
}
|
|
89
102
|
if (command === "init") {
|
|
@@ -100,7 +113,7 @@ async function main() {
|
|
|
100
113
|
try {
|
|
101
114
|
endpoint = await testEndpoint(baseUrl, prepared.secret);
|
|
102
115
|
} catch (error) {
|
|
103
|
-
throw new Error(`${error.message}\nThe bridge service is running locally. Fix the endpoint and rerun ocx-cursor init.`);
|
|
116
|
+
throw new Error(`${error.message}\nThe bridge service is running locally. Fix the HTTPS endpoint and rerun ocx-cursor init.`);
|
|
104
117
|
}
|
|
105
118
|
process.stdout.write(`Endpoint is ready (${endpoint.modelCount} models reachable).\n`);
|
|
106
119
|
const configured = await configureCursorOpenAI({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocx-cursor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenCodex
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Use OpenCodex models in Cursor through an authenticated HTTPS gateway",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencodex",
|
|
7
7
|
"cursor",
|
package/src/cursor-patch.mjs
CHANGED
|
@@ -5,15 +5,17 @@ import {
|
|
|
5
5
|
cursorBundleFiles,
|
|
6
6
|
cursorGlassWorkbenchFile,
|
|
7
7
|
cursorLocalRuntimeFiles,
|
|
8
|
+
cursorModelMetadataFiles,
|
|
8
9
|
cursorPatchBackupDirectory,
|
|
9
10
|
cursorWorkbenchFile,
|
|
10
11
|
} from "./paths.mjs";
|
|
11
12
|
|
|
12
|
-
export const cursorPatchMarker = "/*ocx-cursor-model-metadata-
|
|
13
|
+
export const cursorPatchMarker = "/*ocx-cursor-model-metadata-v5*/";
|
|
13
14
|
export const legacyCursorPatchMarker = "/*ocx-cursor-model-metadata*/";
|
|
14
15
|
export const cursorLocalModeDisabled = "localMode:!1";
|
|
15
16
|
export const cursorLocalModeEnabled = "localMode:!0";
|
|
16
|
-
export const cursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display*/";
|
|
17
|
+
export const cursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display-v2*/";
|
|
18
|
+
export const legacyCursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display*/";
|
|
17
19
|
export const cursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v3*/";
|
|
18
20
|
export const legacyCursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v2*/";
|
|
19
21
|
|
|
@@ -22,7 +24,7 @@ export function isCursorModelMetadataBundle(file) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const catalogNormalization = /(?<normalization>\b(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\((?<item>[A-Za-z_$][\w$]*)=>(?<plain>[A-Za-z_$][\w$]*)\(\k<item>\)\)),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
|
|
25
|
-
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-v[
|
|
27
|
+
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-v[234])?\*\/(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\(ocxCursorModel=>\{.*?\}\),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
|
|
26
28
|
const localModelConstructor = /function (?<functionName>[A-Za-z_$][\w$]*)\(e,t\)\{return new (?<modelType>[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*)\(\{modelId:e,displayModelId:e,displayName:null!=t\?t:e,displayNameShort:null!=t\?t:e,aliases:\[\]\}\)\}/g;
|
|
27
29
|
const localRuntimeVisionCapability = /"boolean"==typeof (?<object>[A-Za-z_$][\w$]*)\.supports_vision\?\{supports_vision:\k<object>\.supports_vision\}:\{\}\)/g;
|
|
28
30
|
const localRuntimePickerInput = /toPickerInput\((?<model>[A-Za-z_$][\w$]*)\)\{var [A-Za-z_$][\w$]*;const [A-Za-z_$][\w$]*=this\.modelMetadataById\.get\(\k<model>\.modelId\),(?<capabilities>[A-Za-z_$][\w$]*)=.*?;return\{.*?supportsReasoning:[^,]+,supportsVision:/g;
|
|
@@ -40,12 +42,12 @@ function replaceSingleMatch(source, pattern, replacement, label) {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function metadataInjection(catalog) {
|
|
43
|
-
return `${cursorPatchMarker}${catalog}=${catalog}.map(ocxCursorModel=>{if(!ocxCursorModel.name.startsWith("opencodex/"))return ocxCursorModel;const ocxCursorStored=(this._reactiveStorageService.applicationUserPersistentStorage.availableDefaultModels2??[]).find(ocxCursorCandidate=>ocxCursorCandidate?.name===ocxCursorModel.name);const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=ocxCursorModel.name.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" ");const ocxCursorPrettyLabel=ocxCursorValue=>typeof ocxCursorValue==="string"?ocxCursorValue.split(ocxCursorModel.name).join(ocxCursorDisplayName):ocxCursorValue;const ocxCursorDefinitions=Array.isArray(ocxCursorModel.parameterDefinitions)&&ocxCursorModel.parameterDefinitions.length>0?ocxCursorModel.parameterDefinitions:ocxCursorStored?.parameterDefinitions??[];const ocxCursorVariantSource=Array.isArray(ocxCursorModel.variants)&&ocxCursorModel.variants.length>0?ocxCursorModel.variants:ocxCursorStored?.variants??[];const ocxCursorVariants=ocxCursorVariantSource.map(ocxCursorVariant=>({...ocxCursorVariant,displayName:ocxCursorPrettyLabel(ocxCursorVariant.displayName),displayNameOutsidePicker:ocxCursorPrettyLabel(ocxCursorVariant.displayNameOutsidePicker)}));const ocxCursorLegacySlugs=Array.isArray(ocxCursorModel.legacySlugs)&&ocxCursorModel.legacySlugs.length>0?ocxCursorModel.legacySlugs:ocxCursorStored?.legacySlugs??[];return{...ocxCursorModel,clientDisplayName:ocxCursorDisplayName,inputboxShortModelName:ocxCursorDisplayName,parameterDefinitions:ocxCursorDefinitions,variants:ocxCursorVariants,legacySlugs:ocxCursorLegacySlugs,supportsThinking:void 0!==ocxCursorModel.supportsThinking?ocxCursorModel.supportsThinking:ocxCursorStored?.supportsThinking}}),`;
|
|
45
|
+
return `${cursorPatchMarker}${catalog}=${catalog}.map(ocxCursorModel=>{if(!ocxCursorModel.name.startsWith("opencodex/"))return ocxCursorModel;const ocxCursorStored=(this._reactiveStorageService.applicationUserPersistentStorage.availableDefaultModels2??[]).find(ocxCursorCandidate=>ocxCursorCandidate?.name===ocxCursorModel.name);const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=ocxCursorStored?.clientDisplayName??((ocxCursorModel.name.startsWith("opencodex/cursor/")?"Cursor ":"")+ocxCursorModel.name.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" "));const ocxCursorPrettyLabel=ocxCursorValue=>typeof ocxCursorValue==="string"?ocxCursorValue.split(ocxCursorModel.name).join(ocxCursorDisplayName):ocxCursorValue;const ocxCursorDefinitions=Array.isArray(ocxCursorModel.parameterDefinitions)&&ocxCursorModel.parameterDefinitions.length>0?ocxCursorModel.parameterDefinitions:ocxCursorStored?.parameterDefinitions??[];const ocxCursorVariantSource=Array.isArray(ocxCursorModel.variants)&&ocxCursorModel.variants.length>0?ocxCursorModel.variants:ocxCursorStored?.variants??[];const ocxCursorVariants=ocxCursorVariantSource.map(ocxCursorVariant=>({...ocxCursorVariant,displayName:ocxCursorPrettyLabel(ocxCursorVariant.displayName),displayNameOutsidePicker:ocxCursorPrettyLabel(ocxCursorVariant.displayNameOutsidePicker)}));const ocxCursorLegacySlugs=Array.isArray(ocxCursorModel.legacySlugs)&&ocxCursorModel.legacySlugs.length>0?ocxCursorModel.legacySlugs:ocxCursorStored?.legacySlugs??[];return{...ocxCursorModel,clientDisplayName:ocxCursorDisplayName,inputboxShortModelName:ocxCursorDisplayName,parameterDefinitions:ocxCursorDefinitions,variants:ocxCursorVariants,legacySlugs:ocxCursorLegacySlugs,supportsThinking:void 0!==ocxCursorModel.supportsThinking?ocxCursorModel.supportsThinking:ocxCursorStored?.supportsThinking}}),`;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export function patchCursorWorkbenchSource(source) {
|
|
47
49
|
if (source.includes(cursorPatchMarker)) return { status: "already-patched", source };
|
|
48
|
-
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/") || source.includes("/*ocx-cursor-model-metadata-v3*/")) {
|
|
50
|
+
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/") || source.includes("/*ocx-cursor-model-metadata-v3*/") || source.includes("/*ocx-cursor-model-metadata-v4*/")) {
|
|
49
51
|
const matches = [...source.matchAll(previousCatalogInjection)];
|
|
50
52
|
if (matches.length !== 1) {
|
|
51
53
|
throw new Error(`Expected one legacy Cursor model metadata hook, found ${matches.length}`);
|
|
@@ -98,13 +100,18 @@ export function patchCursorBundleSource(source, options = {}) {
|
|
|
98
100
|
|
|
99
101
|
function patchCursorLocalRuntimeDisplayName(source) {
|
|
100
102
|
if (source.includes(cursorLocalRuntimePatchMarker)) return source;
|
|
103
|
+
if (source.includes(legacyCursorLocalRuntimePatchMarker)) {
|
|
104
|
+
return source
|
|
105
|
+
.replace(legacyCursorLocalRuntimePatchMarker, cursorLocalRuntimePatchMarker)
|
|
106
|
+
.replace('e.startsWith("opencodex/")?e.split("/")', 'e.startsWith("opencodex/")?(e.startsWith("opencodex/cursor/")?"Cursor ":"")+e.split("/")');
|
|
107
|
+
}
|
|
101
108
|
const matches = [...source.matchAll(localModelConstructor)];
|
|
102
109
|
if (matches.length !== 1) {
|
|
103
110
|
throw new Error(`Expected one Cursor local model constructor, found ${matches.length}`);
|
|
104
111
|
}
|
|
105
112
|
const match = matches[0];
|
|
106
113
|
const { functionName, modelType } = match.groups;
|
|
107
|
-
const replacement = `function ${functionName}(e,t){${cursorLocalRuntimePatchMarker}const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=null!=t?t:e.startsWith("opencodex/")?e.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" "):e;return new ${modelType}({modelId:e,displayModelId:e,displayName:ocxCursorDisplayName,displayNameShort:ocxCursorDisplayName,aliases:[]})}`;
|
|
114
|
+
const replacement = `function ${functionName}(e,t){${cursorLocalRuntimePatchMarker}const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=null!=t?t:e.startsWith("opencodex/")?(e.startsWith("opencodex/cursor/")?"Cursor ":"")+e.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" "):e;return new ${modelType}({modelId:e,displayModelId:e,displayName:ocxCursorDisplayName,displayNameShort:ocxCursorDisplayName,aliases:[]})}`;
|
|
108
115
|
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -176,15 +183,17 @@ function patchCursorLocalRuntimeProviderCapabilities(source) {
|
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
export function patchCursorLocalRuntimeSource(source) {
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
186
|
+
const displayPatched = patchCursorLocalRuntimeDisplayName(source);
|
|
187
|
+
if (displayPatched.includes(cursorLocalRuntimeCapabilitiesPatchMarker)) {
|
|
188
|
+
return { status: displayPatched === source ? "already-patched" : "patched", source: displayPatched };
|
|
189
|
+
}
|
|
190
|
+
if (displayPatched.includes(legacyCursorLocalRuntimeCapabilitiesPatchMarker)) {
|
|
191
|
+
const upgraded = patchCursorLocalRuntimeProviderCapabilities(displayPatched).replace(
|
|
182
192
|
legacyCursorLocalRuntimeCapabilitiesPatchMarker,
|
|
183
193
|
cursorLocalRuntimeCapabilitiesPatchMarker,
|
|
184
194
|
);
|
|
185
195
|
return { status: "patched", source: upgraded };
|
|
186
196
|
}
|
|
187
|
-
const displayPatched = patchCursorLocalRuntimeDisplayName(source);
|
|
188
197
|
return { status: "patched", source: patchCursorLocalRuntimeCapabilities(displayPatched) };
|
|
189
198
|
}
|
|
190
199
|
|
|
@@ -221,6 +230,13 @@ export async function ensureCursorWorkbenchPatched(options = {}) {
|
|
|
221
230
|
return await writePatchedFile(file, source, patched, backupDirectory);
|
|
222
231
|
}
|
|
223
232
|
|
|
233
|
+
export async function ensureCursorModelMetadataPatched(options = {}) {
|
|
234
|
+
const file = options.file || cursorWorkbenchFile;
|
|
235
|
+
const backupDirectory = options.backupDirectory || cursorPatchBackupDirectory;
|
|
236
|
+
const source = await readFile(file, "utf8");
|
|
237
|
+
return await writePatchedFile(file, source, patchCursorWorkbenchSource(source), backupDirectory);
|
|
238
|
+
}
|
|
239
|
+
|
|
224
240
|
export async function ensureCursorLocalRuntimesPatched(options = {}) {
|
|
225
241
|
const files = options.files || cursorLocalRuntimeFiles;
|
|
226
242
|
const backupDirectory = options.backupDirectory || cursorPatchBackupDirectory;
|
|
@@ -260,6 +276,12 @@ export async function ensureCursorAppPatched(options = {}) {
|
|
|
260
276
|
}
|
|
261
277
|
|
|
262
278
|
async function ensureCursorPatchFile(file, localRuntimeFiles, options) {
|
|
279
|
+
if (options.metadataOnly) {
|
|
280
|
+
return {
|
|
281
|
+
file,
|
|
282
|
+
...await ensureCursorModelMetadataPatched({ ...options, file }),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
263
285
|
if (localRuntimeFiles.has(file)) {
|
|
264
286
|
const [result] = await ensureCursorLocalRuntimesPatched({
|
|
265
287
|
...options,
|
|
@@ -277,6 +299,15 @@ async function ensureCursorPatchFile(file, localRuntimeFiles, options) {
|
|
|
277
299
|
};
|
|
278
300
|
}
|
|
279
301
|
|
|
302
|
+
export function startCursorModelMetadataPatchMonitor(options = {}) {
|
|
303
|
+
return startCursorPatchMonitor({
|
|
304
|
+
...options,
|
|
305
|
+
bundleFiles: options.bundleFiles || cursorModelMetadataFiles,
|
|
306
|
+
localRuntimeFiles: [],
|
|
307
|
+
metadataOnly: true,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
export function startCursorPatchMonitor(options = {}) {
|
|
281
312
|
const intervalMs = options.intervalMs || 250;
|
|
282
313
|
const bundleFiles = options.file ? [options.file] : (options.bundleFiles || options.files || cursorBundleFiles);
|
package/src/cursor-state.mjs
CHANGED
|
@@ -29,6 +29,11 @@ const displayWords = new Map([
|
|
|
29
29
|
["spark", "Spark"],
|
|
30
30
|
["terra", "Terra"],
|
|
31
31
|
]);
|
|
32
|
+
const providerDisplayNames = new Map([
|
|
33
|
+
["cursor", "Cursor"],
|
|
34
|
+
["opencode-go", "OpenCode Go"],
|
|
35
|
+
]);
|
|
36
|
+
const unprefixedProviders = new Set(["anthropic", "openai"]);
|
|
32
37
|
|
|
33
38
|
function displayWord(value) {
|
|
34
39
|
const known = displayWords.get(value);
|
|
@@ -42,12 +47,17 @@ function displayWord(value) {
|
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export function displayNameFor(model) {
|
|
45
|
-
|
|
50
|
+
const provider = model.provider || (model.sourceId.includes("/") ? model.sourceId.split("/", 1)[0] : "openai");
|
|
51
|
+
const providerDisplayName = providerDisplayNames.get(provider)
|
|
52
|
+
|| provider.split(/[-_]/).map(displayWord).join(" ");
|
|
53
|
+
const displayName = model.sourceId.split("/").at(-1).split("-").map(displayWord).join(" ");
|
|
54
|
+
return unprefixedProviders.has(provider) ? displayName : `${providerDisplayName} ${displayName}`;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
export function cursorIsRunning() {
|
|
49
58
|
try {
|
|
50
|
-
|
|
59
|
+
// Match the main binary only: orphaned crashpad helpers under Contents/ outlive the app.
|
|
60
|
+
execFileSync("pgrep", ["-f", "/Applications/Cursor.app/Contents/MacOS/Cursor"], { stdio: "ignore" });
|
|
51
61
|
return true;
|
|
52
62
|
} catch {
|
|
53
63
|
return false;
|
|
@@ -165,6 +175,9 @@ function syncSelectedModel(state, catalog) {
|
|
|
165
175
|
const composer = state.aiSettings?.modelConfig?.composer;
|
|
166
176
|
if (!composer || !Array.isArray(composer.selectedModels)) return;
|
|
167
177
|
const byAlias = new Map(catalog.map((model) => [model.alias, model]));
|
|
178
|
+
composer.selectedModels = composer.selectedModels.filter((selected) => (
|
|
179
|
+
typeof selected?.modelId !== "string" || !selected.modelId.startsWith(managedPrefix) || byAlias.has(selected.modelId)
|
|
180
|
+
));
|
|
168
181
|
for (const selected of composer.selectedModels) {
|
|
169
182
|
const model = byAlias.get(selected.modelId);
|
|
170
183
|
if (!model || (model.reasoningEfforts.length === 0 && !model.supportsFast)) continue;
|
|
@@ -187,7 +200,7 @@ export function applyCatalogToState(state, catalog) {
|
|
|
187
200
|
const keepUnmanaged = (value) => typeof value !== "string" || !value.startsWith(managedPrefix);
|
|
188
201
|
const existingModels = Array.isArray(state.availableDefaultModels2) ? state.availableDefaultModels2 : [];
|
|
189
202
|
state.availableDefaultModels2 = [
|
|
190
|
-
...existingModels.filter((model) => !(
|
|
203
|
+
...existingModels.filter((model) => !(typeof model?.name === "string" && model.name.startsWith(managedPrefix))),
|
|
191
204
|
...catalog.map(cursorModel),
|
|
192
205
|
];
|
|
193
206
|
state.aiSettings ||= {};
|
|
@@ -199,6 +212,14 @@ export function applyCatalogToState(state, catalog) {
|
|
|
199
212
|
...(Array.isArray(state.aiSettings.modelOverrideEnabled) ? state.aiSettings.modelOverrideEnabled.filter(keepUnmanaged) : []),
|
|
200
213
|
...aliases,
|
|
201
214
|
];
|
|
215
|
+
if (state.aiSettings.modelParameterPreferences && typeof state.aiSettings.modelParameterPreferences === "object") {
|
|
216
|
+
const activeAliases = new Set(aliases);
|
|
217
|
+
for (const alias of Object.keys(state.aiSettings.modelParameterPreferences)) {
|
|
218
|
+
if (alias.startsWith(managedPrefix) && !activeAliases.has(alias)) {
|
|
219
|
+
delete state.aiSettings.modelParameterPreferences[alias];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
202
223
|
syncSelectedModel(state, catalog);
|
|
203
224
|
return state;
|
|
204
225
|
}
|
package/src/gateway.mjs
CHANGED
|
@@ -118,10 +118,15 @@ async function serviceToken() {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function authorized(req, expected) {
|
|
122
|
-
const supplied =
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
export function authorized(req, expected) {
|
|
122
|
+
const supplied = [
|
|
123
|
+
String(req.headers.authorization || "").replace(/^Bearer\s+/i, "").trim(),
|
|
124
|
+
String(req.headers["x-api-key"] || "").trim(),
|
|
125
|
+
];
|
|
126
|
+
return supplied.some((value) => {
|
|
127
|
+
const actual = Buffer.from(value);
|
|
128
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
129
|
+
});
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
function json(res, status, body) {
|
|
@@ -161,7 +166,7 @@ function upstreamHeaders(req, body, token, upstream) {
|
|
|
161
166
|
|
|
162
167
|
export function startGateway(options) {
|
|
163
168
|
const host = options.host || gatewayHost;
|
|
164
|
-
const port = options.port
|
|
169
|
+
const port = options.port ?? gatewayPort;
|
|
165
170
|
const expected = Buffer.from(options.secret);
|
|
166
171
|
let activeRequests = 0;
|
|
167
172
|
|
|
@@ -179,16 +184,29 @@ export function startGateway(options) {
|
|
|
179
184
|
if (activeRequests >= 8) return json(res, 429, { error: { message: "bridge concurrency limit reached", type: "rate_limit_error" } });
|
|
180
185
|
|
|
181
186
|
activeRequests += 1;
|
|
182
|
-
|
|
187
|
+
let upstreamReq;
|
|
188
|
+
let downstreamClosed = false;
|
|
189
|
+
const cancelUpstream = () => {
|
|
190
|
+
downstreamClosed = true;
|
|
191
|
+
if (upstreamReq && !upstreamReq.destroyed) upstreamReq.destroy();
|
|
192
|
+
};
|
|
193
|
+
req.once("aborted", cancelUpstream);
|
|
194
|
+
res.once("close", () => {
|
|
195
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
196
|
+
if (!res.writableEnded) cancelUpstream();
|
|
197
|
+
});
|
|
183
198
|
try {
|
|
184
199
|
if (route === "GET /v1/models") {
|
|
185
200
|
return json(res, 200, enrichModelList(await activeModels(), catalog));
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
const upstream = opencodexEndpoint();
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
203
|
+
const upstream = options.upstream || opencodexEndpoint();
|
|
204
|
+
const receivedBody = await readBody(req);
|
|
205
|
+
if (downstreamClosed) return;
|
|
206
|
+
const body = rewriteModelAliasBody(receivedBody, catalog);
|
|
207
|
+
const token = options.serviceToken !== undefined ? options.serviceToken : await serviceToken();
|
|
208
|
+
if (downstreamClosed) return;
|
|
209
|
+
upstreamReq = http.request({
|
|
192
210
|
hostname: upstream.host,
|
|
193
211
|
port: upstream.port,
|
|
194
212
|
method: req.method,
|
|
@@ -204,6 +222,7 @@ export function startGateway(options) {
|
|
|
204
222
|
});
|
|
205
223
|
});
|
|
206
224
|
upstreamReq.on("error", (error) => {
|
|
225
|
+
if (downstreamClosed) return;
|
|
207
226
|
if (!res.headersSent) json(res, 502, { error: { message: "OpenCodex upstream unavailable", type: "upstream_error" } });
|
|
208
227
|
else res.destroy(error);
|
|
209
228
|
});
|
package/src/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { access, chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readlink,
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { ensureCursorModelMetadataPatched } from "./cursor-patch.mjs";
|
|
8
8
|
import { cursorIsRunning } from "./cursor-state.mjs";
|
|
9
9
|
import {
|
|
10
10
|
cliLinkFile,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
launchAgentFile,
|
|
14
14
|
legacyLaunchAgentFile,
|
|
15
15
|
legacyServiceLabel,
|
|
16
|
+
cursorModelMetadataFiles,
|
|
16
17
|
secretFile,
|
|
17
18
|
serviceLabel,
|
|
18
19
|
stderrFile,
|
|
@@ -167,7 +168,12 @@ function launchAgent(nodePath) {
|
|
|
167
168
|
export async function installService() {
|
|
168
169
|
await mkdir(dirname(launchAgentFile), { recursive: true });
|
|
169
170
|
const { legacyPlist, secretStatus } = await prepareInstallSecret();
|
|
170
|
-
const cursorPatches = cursorIsRunning()
|
|
171
|
+
const cursorPatches = cursorIsRunning()
|
|
172
|
+
? null
|
|
173
|
+
: await Promise.all(cursorModelMetadataFiles.map(async (file) => ({
|
|
174
|
+
file,
|
|
175
|
+
...await ensureCursorModelMetadataPatched({ file }),
|
|
176
|
+
})));
|
|
171
177
|
await copyPackage();
|
|
172
178
|
await installCliLink();
|
|
173
179
|
|
package/src/paths.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export const cursorDatabaseFile = join(homedir(), "Library", "Application Suppor
|
|
|
17
17
|
export const cursorAppOutDirectory = "/Applications/Cursor.app/Contents/Resources/app/out";
|
|
18
18
|
export const cursorWorkbenchFile = join(cursorAppOutDirectory, "vs", "workbench", "workbench.desktop.main.js");
|
|
19
19
|
export const cursorGlassWorkbenchFile = join(cursorAppOutDirectory, "vs", "workbench", "workbench.glass.main.js");
|
|
20
|
+
export const cursorModelMetadataFiles = [cursorWorkbenchFile, cursorGlassWorkbenchFile];
|
|
20
21
|
export const cursorLocalRuntimeFiles = [
|
|
21
22
|
"/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-local-agent-runtime/dist/main.js",
|
|
22
23
|
"/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js",
|
|
@@ -39,6 +40,5 @@ export const opencodexConfigFile = join(homedir(), ".opencodex", "config.json");
|
|
|
39
40
|
export const opencodexServiceTokenFile = join(homedir(), ".opencodex", "service-api-token");
|
|
40
41
|
export const gatewayPort = Number(process.env.OCX_CURSOR_PORT || "10101");
|
|
41
42
|
export const gatewayHost = process.env.OCX_CURSOR_HOST || "127.0.0.1";
|
|
42
|
-
export const cursorLocalBaseUrl = `http://127.0.0.1:${gatewayPort}/v1`;
|
|
43
43
|
export const cursorOpenAIBaseUrl = process.env.OCX_CURSOR_BASE_URL || "";
|
|
44
44
|
export const managedPrefix = "opencodex/";
|
package/src/service.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { buildActiveCatalog } from "./catalog.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { startCursorModelMetadataPatchMonitor } from "./cursor-patch.mjs";
|
|
5
5
|
import {
|
|
6
6
|
cursorIsRunning,
|
|
7
7
|
readPendingCatalog,
|
|
@@ -59,7 +59,7 @@ export async function runService(options = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const server = startGateway({ secret, getCatalog: () => catalog, host: options.host, port: options.port });
|
|
62
|
-
const patchMonitor =
|
|
62
|
+
const patchMonitor = startCursorModelMetadataPatchMonitor({
|
|
63
63
|
intervalMs: options.cursorPatchIntervalMs,
|
|
64
64
|
shouldPatch: () => !cursorIsRunning(),
|
|
65
65
|
onResult: (result) => {
|
package/src/setup.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function normalizeBaseUrl(value) {
|
|
2
2
|
if (!value) {
|
|
3
|
-
throw new Error("Enter an endpoint
|
|
3
|
+
throw new Error("Enter an HTTPS endpoint or pass --base-url https://cursor-api.example.com/v1");
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
const trimmed = value.trim();
|