ocx-cursor 0.5.0 → 0.5.1
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 +56 -196
- package/package.json +1 -1
- package/src/cursor-patch.mjs +102 -10
package/README.md
CHANGED
|
@@ -3,244 +3,114 @@
|
|
|
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 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 a local OpenAI-compatible gateway.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- macOS with Cursor installed at `/Applications/Cursor.app`
|
|
11
|
-
- Node.js 22.5 or newer
|
|
12
|
-
- OpenCodex installed and configured
|
|
13
|
-
|
|
14
|
-
Launch Cursor and sign in once before setup. Cursor creates the Safe Storage key that the installer uses to encrypt the gateway API key.
|
|
15
|
-
|
|
16
|
-
## Setup
|
|
8
|
+
> [!WARNING]
|
|
9
|
+
> This package edits private JavaScript files inside `Cursor.app`. The changes invalidate Cursor's vendor signature, so Cursor may report that the installation is corrupt. The installer saves original files under `~/.opencodex/cursor-bridge/cursor-app-backups`. Reinstall Cursor to restore a signed app.
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
## Quick start
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
You need macOS, Node.js 22.5 or newer, and Cursor at `/Applications/Cursor.app`.
|
|
21
14
|
|
|
22
|
-
Install
|
|
15
|
+
Install and configure OpenCodex:
|
|
23
16
|
|
|
24
17
|
```bash
|
|
25
18
|
npm install --global @bitkyc08/opencodex
|
|
26
19
|
ocx init
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
The bridge service looks for `ocx` at `~/.local/bin/ocx` and `/opt/homebrew/bin/ocx`. If `command -v ocx` prints another path, link it into `~/.local/bin`:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
mkdir -p ~/.local/bin
|
|
33
|
-
if [[ "$(command -v ocx)" != "$HOME/.local/bin/ocx" ]]; then
|
|
34
|
-
ln -sf "$(command -v ocx)" ~/.local/bin/ocx
|
|
35
|
-
fi
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`ocx init` opens the interactive provider setup. You can also configure providers through the dashboard or terminal:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
ocx gui
|
|
42
|
-
ocx login <provider>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Install OpenCodex as a login service, then verify it:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
20
|
ocx service install
|
|
49
21
|
ocx service status
|
|
50
|
-
curl http://127.0.0.1:10100/v1/models
|
|
51
22
|
```
|
|
52
23
|
|
|
53
|
-
|
|
24
|
+
Check the configured models:
|
|
54
25
|
|
|
55
26
|
```bash
|
|
56
|
-
|
|
57
|
-
--header "Authorization: Bearer $(cat ~/.opencodex/service-api-token)" \
|
|
58
|
-
http://127.0.0.1:10100/v1/models
|
|
27
|
+
ocx models --json
|
|
59
28
|
```
|
|
60
29
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
### 2. Install the Cursor bridge
|
|
64
|
-
|
|
65
|
-
Quit Cursor completely, confirm that OpenCodex is running, then initialize the bridge:
|
|
30
|
+
Launch Cursor and sign in once, then quit it. Run the bridge installer:
|
|
66
31
|
|
|
67
32
|
```bash
|
|
68
33
|
npx ocx-cursor init
|
|
34
|
+
~/.local/bin/ocx-cursor status
|
|
69
35
|
```
|
|
70
36
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
1. Generates a gateway API key in `~/.opencodex/cursor-bridge/secret`.
|
|
74
|
-
2. Installs the `com.opencodex.cursor-bridge` LaunchAgent.
|
|
75
|
-
3. Tests the local gateway and OpenCodex model endpoint.
|
|
76
|
-
4. Stores the key in Cursor with macOS Safe Storage encryption.
|
|
77
|
-
5. Registers the local gateway as Cursor's OpenAI base URL.
|
|
78
|
-
6. Adds active OpenCodex models to Cursor under `opencodex/*`.
|
|
79
|
-
7. Enables Cursor's local-agent mode and installs the model metadata patches described below.
|
|
80
|
-
|
|
81
|
-
`init` patches ten JavaScript files inside `Cursor.app`:
|
|
82
|
-
|
|
83
|
-
- Eight Cursor bundles get the local-mode build flag.
|
|
84
|
-
- The Desktop and Glass workbench bundles retain OpenCodex display names, effort choices, and Fast metadata when Cursor refreshes its server catalog.
|
|
85
|
-
- Both local-agent runtimes display names such as `GPT 5.6 Sol` while keeping `opencodex/gpt-5.6-sol` as the request ID.
|
|
37
|
+
`init` registers `http://127.0.0.1:10101/v1` in Cursor. You do not need a domain or tunnel. Open Cursor after the status command reports `Service: running` and `Gateway: healthy`.
|
|
86
38
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
> [!WARNING]
|
|
90
|
-
> Patching `Cursor.app` invalidates its vendor code signature. Cursor may show “Your Cursor installation appears to be corrupt.” This is expected for this integration. Reinstall Cursor to restore an unmodified, vendor-signed app.
|
|
91
|
-
|
|
92
|
-
The installer links `ocx-cursor` into `~/.local/bin`. Add that directory to `PATH` if needed:
|
|
39
|
+
Add the installed command to your shell path if needed:
|
|
93
40
|
|
|
94
41
|
```bash
|
|
95
42
|
export PATH="$HOME/.local/bin:$PATH"
|
|
96
43
|
```
|
|
97
44
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
ocx-cursor status
|
|
102
|
-
curl http://127.0.0.1:10101/healthz
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The status output should show `Service: running` and `Gateway: healthy`. Open Cursor after both checks pass.
|
|
106
|
-
|
|
107
|
-
To use an explicit HTTPS reverse proxy instead, pass its URL directly. This is optional and not used by the default setup:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
npx ocx-cursor init --base-url https://cursor-api.example.com/v1
|
|
111
|
-
```
|
|
45
|
+
## What init changes
|
|
112
46
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
47
|
+
| Area | Change |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Cursor settings | Stores the gateway key with macOS Safe Storage and sets the local base URL. |
|
|
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. |
|
|
118
53
|
|
|
119
|
-
|
|
54
|
+
Requests stay on loopback until OpenCodex sends them to your configured provider:
|
|
120
55
|
|
|
121
56
|
```text
|
|
122
|
-
Cursor
|
|
123
|
-
-> OpenCodex Cursor Bridge on http://127.0.0.1:10101/v1
|
|
124
|
-
-> OpenCodex on http://127.0.0.1:10100
|
|
125
|
-
-> configured provider
|
|
57
|
+
Cursor -> 127.0.0.1:10101 -> OpenCodex on 127.0.0.1:10100 -> provider
|
|
126
58
|
```
|
|
127
59
|
|
|
128
|
-
The gateway
|
|
129
|
-
|
|
130
|
-
## Commands
|
|
131
|
-
|
|
132
|
-
| Command | Purpose |
|
|
133
|
-
| --- | --- |
|
|
134
|
-
| `ocx-cursor init [--base-url URL]` | Install the service, test the endpoint, configure Cursor, and sync models. Cursor must be closed. |
|
|
135
|
-
| `ocx-cursor install` | Reinstall the LaunchAgent and verify app patches, or queue them until Cursor quits. |
|
|
136
|
-
| `ocx-cursor update` | Download the latest release from npm, reinstall the service, verify patches, and sync models. |
|
|
137
|
-
| `ocx-cursor sync` | Refresh the active model catalog. The service queues the update while Cursor runs. |
|
|
138
|
-
| `ocx-cursor launch` | Experimentally launch Cursor with live effort and Fast metadata injection. Keep the command running. |
|
|
139
|
-
| `ocx-cursor status` | Show service health, model count, and pending sync state. |
|
|
140
|
-
| `ocx-cursor uninstall` | Remove the LaunchAgent, command link, and bridge home directory. |
|
|
141
|
-
|
|
142
|
-
`uninstall` leaves Cursor's custom endpoint, model records, and app patches in place. Reinstall Cursor if you want to restore its original signed files.
|
|
60
|
+
The gateway requires its generated bearer token on every `/v1/*` request. It binds to `127.0.0.1` unless you change `OCX_CURSOR_HOST`.
|
|
143
61
|
|
|
144
|
-
|
|
62
|
+
## Model behavior
|
|
145
63
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
The bridge maps source model IDs to Cursor aliases:
|
|
64
|
+
The bridge reads OpenCodex's active `/v1/models` response and creates stable Cursor aliases:
|
|
149
65
|
|
|
150
66
|
```text
|
|
151
|
-
anthropic/claude-sonnet-5
|
|
152
|
-
gpt-5.6-sol
|
|
67
|
+
anthropic/claude-sonnet-5 -> opencodex/claude-sonnet-5
|
|
68
|
+
gpt-5.6-sol -> opencodex/gpt-5.6-sol
|
|
153
69
|
```
|
|
154
70
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Cursor removes custom effort metadata from its database during startup. The companion LaunchAgent patches the Desktop and Glass catalog storage steps so `opencodex/*` display names, effort, and Fast metadata survive server refreshes. It monitors the installed bundles and local-agent runtimes, then reapplies their patches when Cursor replaces the app during an update.
|
|
71
|
+
Cursor shows readable names such as `GPT 5.6 Sol`. Reasoning models receive an effort selector. Models with OpenCodex's `priority` service tier receive a Fast toggle.
|
|
158
72
|
|
|
159
|
-
|
|
73
|
+
Cursor can send `strict: true` with function schemas that fail strict validation. The gateway changes the flag to `false` before forwarding those requests. It leaves tool names, descriptions, and arguments intact.
|
|
160
74
|
|
|
161
|
-
|
|
75
|
+
## Commands and configuration
|
|
162
76
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
`ocx-cursor
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
ocx-cursor
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
Keep the command running for the lifetime of Cursor. This does not modify `Cursor.app`, but it depends on Cursor's private workbench code and supports only known Cursor builds.
|
|
172
|
-
|
|
173
|
-
## Configuration
|
|
77
|
+
| Command | Purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `ocx-cursor init` | Install the service, configure Cursor, patch the app, and sync models. Cursor must be closed. |
|
|
80
|
+
| `ocx-cursor install` | Reinstall the service and check app patches. Running Cursor defers app changes until exit. |
|
|
81
|
+
| `ocx-cursor update` | Install the newest npm release and run `install`. |
|
|
82
|
+
| `ocx-cursor sync` | Refresh the active model list. |
|
|
83
|
+
| `ocx-cursor status` | Show service health and pending model sync. |
|
|
84
|
+
| `ocx-cursor uninstall` | Remove the service, command link, and bridge state. |
|
|
174
85
|
|
|
175
|
-
|
|
176
|
-
| --- | --- | --- |
|
|
177
|
-
| `OCX_CURSOR_BASE_URL` | `http://127.0.0.1:10101/v1` | Endpoint used by `init` when `--base-url` is absent. |
|
|
178
|
-
| `OCX_CURSOR_HOME` | `~/.opencodex/cursor-bridge` | Service state, API key, catalog, and logs. |
|
|
179
|
-
| `OCX_CURSOR_HOST` | `127.0.0.1` | Local gateway bind address. |
|
|
180
|
-
| `OCX_CURSOR_PORT` | `10101` | Local gateway port. Also changes the default Cursor endpoint. |
|
|
181
|
-
| `OCX_BIN` | `~/.local/bin/ocx` | OpenCodex CLI path. |
|
|
86
|
+
`uninstall` does not restore Cursor settings or patched app files. Reinstall Cursor if you want a clean vendor build.
|
|
182
87
|
|
|
183
|
-
|
|
88
|
+
| Variable | Default |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `OCX_CURSOR_BASE_URL` | `http://127.0.0.1:10101/v1` |
|
|
91
|
+
| `OCX_CURSOR_HOME` | `~/.opencodex/cursor-bridge` |
|
|
92
|
+
| `OCX_CURSOR_HOST` | `127.0.0.1` |
|
|
93
|
+
| `OCX_CURSOR_PORT` | `10101` |
|
|
94
|
+
| `OCX_BIN` | `~/.local/bin/ocx` |
|
|
184
95
|
|
|
185
|
-
|
|
186
|
-
- `POST /v1/chat/completions`
|
|
187
|
-
- `POST /v1/responses` and `/v1/responses/compact`
|
|
188
|
-
- `POST /v1/messages`
|
|
96
|
+
The bridge reads an OpenCodex service token from `~/.opencodex/service-api-token` when that file exists.
|
|
189
97
|
|
|
190
98
|
## Troubleshooting
|
|
191
99
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
### The local gateway is unavailable
|
|
201
|
-
|
|
202
|
-
Check the service and logs, then reinstall it if needed:
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
ocx-cursor status
|
|
206
|
-
tail -n 100 ~/.opencodex/cursor-bridge/service.error.log
|
|
207
|
-
tail -n 100 ~/.opencodex/cursor-bridge/service.log
|
|
208
|
-
ocx-cursor install
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Models or effort options are missing
|
|
212
|
-
|
|
213
|
-
Quit Cursor and run:
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
ocx-cursor sync
|
|
217
|
-
ocx-cursor status
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
If the status shows a pending sync, wait until every Cursor Helper process has exited. The service applies the queued catalog automatically.
|
|
221
|
-
|
|
222
|
-
### Patches are missing after a Cursor update
|
|
223
|
-
|
|
224
|
-
Quit every Cursor window and wait for its Helper processes to stop. The LaunchAgent detects replaced app files and reapplies local mode, metadata, and display-name patches. Check the result with:
|
|
100
|
+
| Symptom | Fix |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| Cursor returns 401 | Quit Cursor and rerun `npx ocx-cursor init`. |
|
|
103
|
+
| Models or effort choices are missing | Quit Cursor, run `ocx-cursor sync`, then check `ocx-cursor status`. |
|
|
104
|
+
| A Cursor update removed the patches | Quit Cursor and run `ocx-cursor install`. |
|
|
105
|
+
| The gateway is unavailable | Run `ocx-cursor status` and inspect the logs below. |
|
|
106
|
+
| The bridge cannot find `ocx` | Run `ln -sf "$(command -v ocx)" ~/.local/bin/ocx`. |
|
|
225
107
|
|
|
226
108
|
```bash
|
|
227
|
-
ocx-cursor install
|
|
228
109
|
tail -n 100 ~/.opencodex/cursor-bridge/service.log
|
|
110
|
+
tail -n 100 ~/.opencodex/cursor-bridge/service.error.log
|
|
229
111
|
```
|
|
230
112
|
|
|
231
|
-
The patcher stops
|
|
232
|
-
|
|
233
|
-
### OpenCodex models cannot be loaded
|
|
234
|
-
|
|
235
|
-
Check OpenCodex first:
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
ocx status
|
|
239
|
-
ocx models --json
|
|
240
|
-
curl http://127.0.0.1:10100/v1/models
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
If `ocx models --json` works in your shell but fails in the bridge, check that `ocx` is available at `~/.local/bin/ocx` or `/opt/homebrew/bin/ocx`. The macOS LaunchAgent does not load your interactive shell profile.
|
|
113
|
+
The patcher stops when a Cursor update no longer matches its known code structure. It reports the file and mismatch in `service.error.log` without replacing that file.
|
|
244
114
|
|
|
245
115
|
## Development
|
|
246
116
|
|
|
@@ -250,14 +120,4 @@ npm run check
|
|
|
250
120
|
npm pack --dry-run
|
|
251
121
|
```
|
|
252
122
|
|
|
253
|
-
The
|
|
254
|
-
|
|
255
|
-
## Compatibility
|
|
256
|
-
|
|
257
|
-
The package writes Cursor's local state database, uses Cursor's Safe Storage format, and modifies private code inside `Cursor.app`. Cursor does not document these interfaces. A Cursor update can change them.
|
|
258
|
-
|
|
259
|
-
The current release supports macOS. It does not configure Windows Credential Manager, Linux keyrings, or system services outside launchd.
|
|
260
|
-
|
|
261
|
-
## License
|
|
262
|
-
|
|
263
|
-
MIT
|
|
123
|
+
The project uses the MIT license.
|
package/package.json
CHANGED
package/src/cursor-patch.mjs
CHANGED
|
@@ -9,28 +9,43 @@ import {
|
|
|
9
9
|
cursorWorkbenchFile,
|
|
10
10
|
} from "./paths.mjs";
|
|
11
11
|
|
|
12
|
-
export const cursorPatchMarker = "/*ocx-cursor-model-metadata-
|
|
12
|
+
export const cursorPatchMarker = "/*ocx-cursor-model-metadata-v4*/";
|
|
13
13
|
export const legacyCursorPatchMarker = "/*ocx-cursor-model-metadata*/";
|
|
14
14
|
export const cursorLocalModeDisabled = "localMode:!1";
|
|
15
15
|
export const cursorLocalModeEnabled = "localMode:!0";
|
|
16
16
|
export const cursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display*/";
|
|
17
|
+
export const cursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v3*/";
|
|
18
|
+
export const legacyCursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v2*/";
|
|
17
19
|
|
|
18
20
|
export function isCursorModelMetadataBundle(file) {
|
|
19
21
|
return file === cursorWorkbenchFile || file === cursorGlassWorkbenchFile;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
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;
|
|
23
|
-
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-
|
|
25
|
+
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-v[23])?\*\/(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\(ocxCursorModel=>\{.*?\}\),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
|
|
24
26
|
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
|
+
const localRuntimeVisionCapability = /"boolean"==typeof (?<object>[A-Za-z_$][\w$]*)\.supports_vision\?\{supports_vision:\k<object>\.supports_vision\}:\{\}\)/g;
|
|
28
|
+
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;
|
|
29
|
+
const localRuntimeProviderPickerInput = /supportsReasoning:(?<reasoning>!0===\(null===\((?<capability>[A-Za-z_$][\w$]*)=(?<model>[A-Za-z_$][\w$]*)\.capabilities\)\|\|void 0===\k<capability>\?void 0:\k<capability>\.supports_reasoning\)),supportsVision:/g;
|
|
30
|
+
const localRuntimeRequestParameters = /function\((?<payload>[A-Za-z_$][\w$]*),(?<model>[A-Za-z_$][\w$]*),(?<parameters>[A-Za-z_$][\w$]*),(?<apiType>[A-Za-z_$][\w$]*),(?<route>[A-Za-z_$][\w$]*),(?<extended>[A-Za-z_$][\w$]*)=!1\)\{(?=const [A-Za-z_$][\w$]*=function\([A-Za-z_$][\w$]*\)\{var [A-Za-z_$][\w$]*;const [A-Za-z_$][\w$]*=.*?\["reasoning","effort","thought_level"\]\.includes)/g;
|
|
31
|
+
const localRuntimeBuilderExport = /buildBottlerocketPickerModels:\(\)=>(?<builder>[A-Za-z_$][\w$]*)/g;
|
|
25
32
|
const transientPatchErrorCodes = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
26
33
|
|
|
34
|
+
function replaceSingleMatch(source, pattern, replacement, label) {
|
|
35
|
+
const matches = [...source.matchAll(pattern)];
|
|
36
|
+
if (matches.length !== 1) throw new Error(`Expected one Cursor ${label}, found ${matches.length}`);
|
|
37
|
+
const match = matches[0];
|
|
38
|
+
const value = typeof replacement === "function" ? replacement(match) : replacement;
|
|
39
|
+
return `${source.slice(0, match.index)}${value}${source.slice(match.index + match[0].length)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
function metadataInjection(catalog) {
|
|
28
|
-
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
|
|
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}}),`;
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
export function patchCursorWorkbenchSource(source) {
|
|
32
47
|
if (source.includes(cursorPatchMarker)) return { status: "already-patched", source };
|
|
33
|
-
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/")) {
|
|
48
|
+
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/") || source.includes("/*ocx-cursor-model-metadata-v3*/")) {
|
|
34
49
|
const matches = [...source.matchAll(previousCatalogInjection)];
|
|
35
50
|
if (matches.length !== 1) {
|
|
36
51
|
throw new Error(`Expected one legacy Cursor model metadata hook, found ${matches.length}`);
|
|
@@ -81,8 +96,8 @@ export function patchCursorBundleSource(source, options = {}) {
|
|
|
81
96
|
};
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
if (source.includes(cursorLocalRuntimePatchMarker)) return
|
|
99
|
+
function patchCursorLocalRuntimeDisplayName(source) {
|
|
100
|
+
if (source.includes(cursorLocalRuntimePatchMarker)) return source;
|
|
86
101
|
const matches = [...source.matchAll(localModelConstructor)];
|
|
87
102
|
if (matches.length !== 1) {
|
|
88
103
|
throw new Error(`Expected one Cursor local model constructor, found ${matches.length}`);
|
|
@@ -90,10 +105,87 @@ export function patchCursorLocalRuntimeSource(source) {
|
|
|
90
105
|
const match = matches[0];
|
|
91
106
|
const { functionName, modelType } = match.groups;
|
|
92
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:[]})}`;
|
|
93
|
-
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function patchCursorLocalRuntimeCapabilities(source) {
|
|
112
|
+
source = replaceSingleMatch(source, localRuntimeVisionCapability, (match) => {
|
|
113
|
+
const object = match.groups.object;
|
|
114
|
+
return `${match[0].slice(0, -1)},"boolean"==typeof ${object}.supports_fast?{supports_fast:${object}.supports_fast}:{})`;
|
|
115
|
+
}, "local model capability parser");
|
|
116
|
+
|
|
117
|
+
source = replaceSingleMatch(source, localRuntimePickerInput, (match) => {
|
|
118
|
+
const capabilities = match.groups.capabilities;
|
|
119
|
+
return match[0].replace(
|
|
120
|
+
",supportsVision:",
|
|
121
|
+
`,reasoningEfforts:Array.isArray(${capabilities}?.reasoning_effort)?${capabilities}.reasoning_effort:void 0,supportsFast:!0===${capabilities}?.supports_fast,supportsVision:`,
|
|
122
|
+
);
|
|
123
|
+
}, "local picker input mapper");
|
|
124
|
+
|
|
125
|
+
source = patchCursorLocalRuntimeProviderCapabilities(source);
|
|
126
|
+
|
|
127
|
+
source = replaceSingleMatch(source, localRuntimeRequestParameters, (match) => {
|
|
128
|
+
const { payload, parameters } = match.groups;
|
|
129
|
+
return `${match[0]}const ocxCursorFast=${parameters}?.find(ocxCursorParameter=>ocxCursorParameter.id==="fast")?.value;if(ocxCursorFast==="true")${payload}.service_tier="priority";else if(ocxCursorFast==="false")delete ${payload}.service_tier;`;
|
|
130
|
+
}, "local request parameter mapper");
|
|
131
|
+
|
|
132
|
+
const exportMatches = [...source.matchAll(localRuntimeBuilderExport)];
|
|
133
|
+
if (exportMatches.length !== 1) {
|
|
134
|
+
throw new Error(`Expected one Cursor local picker builder export, found ${exportMatches.length}`);
|
|
135
|
+
}
|
|
136
|
+
const builderName = exportMatches[0].groups.builder;
|
|
137
|
+
const builderStart = source.indexOf(`function ${builderName}(`, exportMatches[0].index);
|
|
138
|
+
if (builderStart === -1) throw new Error("Cursor local picker builder was not found");
|
|
139
|
+
const builderHead = source.slice(builderStart, builderStart + 1_000);
|
|
140
|
+
const modelBuilderName = /\.push\((?<name>[A-Za-z_$][\w$]*)\(/.exec(builderHead)?.groups.name;
|
|
141
|
+
if (!modelBuilderName) throw new Error("Cursor local picker model builder was not found");
|
|
142
|
+
|
|
143
|
+
const modelBuilderStart = source.indexOf(`function ${modelBuilderName}(`, builderStart);
|
|
144
|
+
const modelBuilderSource = source.slice(modelBuilderStart, modelBuilderStart + 6_000);
|
|
145
|
+
const signature = new RegExp(`function ${modelBuilderName}\\((?<model>[A-Za-z_$][\\w$]*),(?<tier>[A-Za-z_$][\\w$]*)\\)\\{var [^;]+;const (?<effort>[A-Za-z_$][\\w$]*)=function\\((?<inner>[A-Za-z_$][\\w$]*)\\)\\{`).exec(modelBuilderSource);
|
|
146
|
+
if (!signature) throw new Error("Cursor local picker model effort builder was not found");
|
|
147
|
+
const { model, effort } = signature.groups;
|
|
148
|
+
const effortPrefix = `const ${effort}=function(`;
|
|
149
|
+
const effortIndex = modelBuilderSource.indexOf(effortPrefix);
|
|
150
|
+
const effortReplacement = `const ${effort}=Array.isArray(${model}.reasoningEfforts)&&${model}.reasoningEfforts.length>0?{param:"reasoning_effort",values:${model}.reasoningEfforts,defaultValue:${model}.reasoningEfforts.includes("medium")?"medium":${model}.reasoningEfforts.includes("high")?"high":${model}.reasoningEfforts[0]}:function(`;
|
|
151
|
+
let patchedBuilder = `${modelBuilderSource.slice(0, effortIndex)}${effortReplacement}${modelBuilderSource.slice(effortIndex + effortPrefix.length)}`;
|
|
152
|
+
|
|
153
|
+
const definitions = new RegExp(`(?<name>[A-Za-z_$][\\w$]*)=\\[\\.\\.\\.void 0!==${effort}\\?`).exec(patchedBuilder)?.groups.name;
|
|
154
|
+
const parameterClass = /new (?<name>[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\(\{id:"reasoning"/.exec(patchedBuilder)?.groups.name;
|
|
155
|
+
if (!definitions || !parameterClass) throw new Error("Cursor local picker parameter definition builder was not found");
|
|
156
|
+
const definitionsEnd = patchedBuilder.indexOf("];var ", patchedBuilder.indexOf(`${definitions}=[`));
|
|
157
|
+
if (definitionsEnd === -1) throw new Error("Cursor local picker parameter definitions were not terminated");
|
|
158
|
+
const fastDefinition = `];${model}.supportsFast&&${definitions}.push(new ${parameterClass}({id:"fast",name:"Fast",markdownTooltip:"Use priority processing with increased usage.",parameterType:{booleanParameter:{values:[{value:"false"},{value:"true",displayName:"Fast",increasesModelCost:!0}]}},isCycleableByHotkey:!1}))`;
|
|
159
|
+
patchedBuilder = `${patchedBuilder.slice(0, definitionsEnd)}${fastDefinition}${patchedBuilder.slice(definitionsEnd + 1)}`;
|
|
160
|
+
|
|
161
|
+
const variantMatch = new RegExp(`variants:(?<call>[A-Za-z_$][\\w$]*\\(${effort},(?<context>[A-Za-z_$][\\w$]*),(?<display>[A-Za-z_$][\\w$]*),(?<tier>[A-Za-z_$][\\w$]*)\\))`).exec(patchedBuilder);
|
|
162
|
+
if (!variantMatch) throw new Error("Cursor local picker variant builder was not found");
|
|
163
|
+
const variantReplacement = `variants:ocxCursorFastVariants(${variantMatch.groups.call},${model}.supportsFast,${variantMatch.groups.display})`;
|
|
164
|
+
patchedBuilder = `${patchedBuilder.slice(0, variantMatch.index)}${variantReplacement}${patchedBuilder.slice(variantMatch.index + variantMatch[0].length)}`;
|
|
165
|
+
|
|
166
|
+
source = `${source.slice(0, modelBuilderStart)}${patchedBuilder}${source.slice(modelBuilderStart + modelBuilderSource.length)}`;
|
|
167
|
+
const helper = `${cursorLocalRuntimeCapabilitiesPatchMarker}function ocxCursorFastVariants(e,t,n){if(!t)return e;const r=e.length>0?e:[{parameterValues:[],displayName:n,displayNameOutsidePicker:n,isMaxMode:!1,isDefaultNonMaxConfig:!0,isDefaultMaxConfig:!0}];return r.flatMap(e=>[{...e,parameterValues:[...(e.parameterValues??[]),{id:"fast",value:"false"}]},{...e,parameterValues:[...(e.parameterValues??[]),{id:"fast",value:"true"}],displayName:e.displayName+" Fast",displayNameOutsidePicker:(e.displayNameOutsidePicker??e.displayName)+" Fast",isDefaultNonMaxConfig:!1,isDefaultMaxConfig:!1}])}`;
|
|
168
|
+
return `${source.slice(0, builderStart)}${helper}${source.slice(builderStart)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function patchCursorLocalRuntimeProviderCapabilities(source) {
|
|
172
|
+
return replaceSingleMatch(source, localRuntimeProviderPickerInput, (match) => {
|
|
173
|
+
const { model, reasoning } = match.groups;
|
|
174
|
+
return `supportsReasoning:${reasoning},reasoningEfforts:Array.isArray(${model}.capabilities?.reasoning_effort)?${model}.capabilities.reasoning_effort:void 0,supportsFast:!0===${model}.capabilities?.supports_fast,supportsVision:`;
|
|
175
|
+
}, "standalone local provider picker mapper");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function patchCursorLocalRuntimeSource(source) {
|
|
179
|
+
if (source.includes(cursorLocalRuntimeCapabilitiesPatchMarker)) return { status: "already-patched", source };
|
|
180
|
+
if (source.includes(legacyCursorLocalRuntimeCapabilitiesPatchMarker)) {
|
|
181
|
+
const upgraded = patchCursorLocalRuntimeProviderCapabilities(source).replace(
|
|
182
|
+
legacyCursorLocalRuntimeCapabilitiesPatchMarker,
|
|
183
|
+
cursorLocalRuntimeCapabilitiesPatchMarker,
|
|
184
|
+
);
|
|
185
|
+
return { status: "patched", source: upgraded };
|
|
186
|
+
}
|
|
187
|
+
const displayPatched = patchCursorLocalRuntimeDisplayName(source);
|
|
188
|
+
return { status: "patched", source: patchCursorLocalRuntimeCapabilities(displayPatched) };
|
|
97
189
|
}
|
|
98
190
|
|
|
99
191
|
export async function cursorWorkbenchSignature(file = cursorWorkbenchFile) {
|