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 CHANGED
@@ -3,244 +3,114 @@
3
3
  [![npm version](https://img.shields.io/npm/v/ocx-cursor.svg)](https://www.npmjs.com/package/ocx-cursor)
4
4
  [![CI](https://github.com/hiddenest/opencodex-cursor-bridge/actions/workflows/ci.yml/badge.svg)](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 its custom OpenAI endpoint. The package runs a loopback-only gateway, registers it in Cursor, and keeps Cursor's custom model list in sync.
6
+ Use your active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through a local OpenAI-compatible gateway.
7
7
 
8
- ## Requirements
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
- ### 1. Install OpenCodex
11
+ ## Quick start
19
12
 
20
- [OpenCodex](https://github.com/lidge-jun/opencodex) requires Node.js 18 or newer and bundles its own Bun runtime. This bridge requires Node.js 22.5 or newer, so install Node.js 22 or later before continuing.
13
+ You need macOS, Node.js 22.5 or newer, and Cursor at `/Applications/Cursor.app`.
21
14
 
22
- Install OpenCodex without `sudo` from a user-owned Node.js installation:
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
- If you configured OpenCodex with a service API token, include it in the request:
24
+ Check the configured models:
54
25
 
55
26
  ```bash
56
- curl \
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
- See the [OpenCodex documentation](https://lidge-jun.github.io/opencodex/) for provider-specific setup.
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
- No tunnel or public domain is required. `init` uses `http://127.0.0.1:10101/v1` and performs these actions:
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
- Original files are copied to `~/.opencodex/cursor-bridge/cursor-app-backups` before they are changed. The LaunchAgent watches the same files and reapplies the patches after a Cursor update, once Cursor has quit.
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
- Check the service before opening Cursor:
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
- ### Fast mode
114
-
115
- OpenAI models whose OpenCodex catalog advertises the `priority` service tier show a Fast toggle in Cursor. Fast is off by default. When enabled, the bridge sends `service_tier: "priority"` to OpenCodex, which increases generation speed and consumes more usage.
116
-
117
- Fast requires an OpenCodex version that preserves `service_tier` on its Chat Completions compatibility endpoint. Models without the `priority` tier, including Anthropic subscription models, do not receive the toggle.
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
- ## How requests are routed
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 API key is generated during `init` and stored in Cursor with macOS Safe Storage encryption. The bridge requires this bearer token on every `/v1/*` request and binds to `127.0.0.1` by default.
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
- `update` preserves the existing gateway API key and Cursor endpoint. If Cursor is running, app patches and model sync are queued until it quits. The command updates only this companion package; update OpenCodex separately with your package manager.
62
+ ## Model behavior
145
63
 
146
- ## Model mapping
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 -> opencodex/claude-sonnet-5
152
- gpt-5.6-sol -> opencodex/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
- The catalog includes every model returned by OpenCodex's active `/v1/models` endpoint, including models from the `cursor/*` provider. Cursor-backed aliases use the `opencodex/cursor/*` prefix so they remain distinct from Cursor's built-in entries.
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
- `init`, `install`, and `update` also patch Cursor's local model runtimes so provider refreshes keep human-readable names such as `GPT 5.6 Sol` while requests continue using stable `opencodex/*` model IDs.
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
- Cursor sends some built-in function tools with `strict: true` even when their JSON schema is not valid for strict mode. The gateway changes that flag to `false` for OpenCodex requests, preventing errors such as `additionalProperties is required to be supplied and to be false` without changing tool arguments.
75
+ ## Commands and configuration
162
76
 
163
- ### Experimental live model metadata
164
-
165
- `ocx-cursor launch` starts Cursor with a random loopback-only debugging port and keeps `opencodex/*` effort and Fast metadata in the live model catalog:
166
-
167
- ```bash
168
- ocx-cursor launch
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
- | Variable | Default | Purpose |
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
- The gateway accepts these routes:
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
- - `GET /v1/models`
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
- ### Cursor returns 401
193
-
194
- Quit Cursor and run `init` again. It preserves the bridge key, tests the local endpoint, and writes the matching encrypted value back to Cursor:
195
-
196
- ```bash
197
- npx ocx-cursor init
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 without modifying a file when a Cursor release no longer matches its known code structure. Check `service.error.log` in that case.
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 code uses `node:sqlite`, so development requires Node.js 22.5 or newer.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocx-cursor",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "OpenCodex companion service for Cursor custom models",
5
5
  "keywords": [
6
6
  "opencodex",
@@ -9,28 +9,43 @@ import {
9
9
  cursorWorkbenchFile,
10
10
  } from "./paths.mjs";
11
11
 
12
- export const cursorPatchMarker = "/*ocx-cursor-model-metadata-v3*/";
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(?:-v2)?\*\/(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\(ocxCursorModel=>\{.*?\}\),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
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 ocxCursorVariants=(ocxCursorStored?.variants??ocxCursorModel.variants??[]).map(ocxCursorVariant=>({...ocxCursorVariant,displayName:ocxCursorPrettyLabel(ocxCursorVariant.displayName),displayNameOutsidePicker:ocxCursorPrettyLabel(ocxCursorVariant.displayNameOutsidePicker)}));return{...ocxCursorModel,clientDisplayName:ocxCursorDisplayName,inputboxShortModelName:ocxCursorDisplayName,parameterDefinitions:ocxCursorStored?.parameterDefinitions??ocxCursorModel.parameterDefinitions??[],variants:ocxCursorVariants,legacySlugs:ocxCursorStored?.legacySlugs??ocxCursorModel.legacySlugs??[],supportsThinking:ocxCursorStored?.supportsThinking??ocxCursorModel.supportsThinking}}),`;
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
- export function patchCursorLocalRuntimeSource(source) {
85
- if (source.includes(cursorLocalRuntimePatchMarker)) return { status: "already-patched", source };
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
- status: "patched",
95
- source: `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`,
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) {