impel-cli 0.18.14 → 0.18.15-beta.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 +46 -2
- package/docs/experimental-managed-cursor.md +205 -0
- package/package.json +6 -5
- package/src/commands/cursorExperimental.js +192 -0
- package/src/commands/experimental.js +8 -2
- package/src/cursorLocal.js +1549 -0
- package/src/macSetup.js +11 -5
- package/src/vendorCliVersions.js +7 -0
- package/src/windowsSetup.js +7 -4
package/README.md
CHANGED
|
@@ -100,6 +100,50 @@ app opens. Tenant variants can remain installed side by side.
|
|
|
100
100
|
|
|
101
101
|
Linux does not have managed desktop apps. Use the isolated CLI commands there.
|
|
102
102
|
|
|
103
|
+
## Experimental managed Cursor
|
|
104
|
+
|
|
105
|
+
On macOS, a hidden experiment can prepare an isolated Cursor runtime for the
|
|
106
|
+
selected Impel tenant:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
impel experimental cursor prepare
|
|
110
|
+
impel experimental cursor open [workspace]
|
|
111
|
+
impel experimental cursor status
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The experiment never modifies `/Applications/Cursor.app` or the native Cursor
|
|
115
|
+
profile. It verifies the installed stable app's Developer ID signature, mirrors
|
|
116
|
+
it into the selected tenant's Impel-managed directory, enables Cursor's bundled
|
|
117
|
+
local-agent runtime with fail-closed fixed-width patches, verifies extension
|
|
118
|
+
integrity and signing, and prepares a tenant-only home, user-data directory,
|
|
119
|
+
and extension directory. Cursor's local-mode profile-import service is disabled
|
|
120
|
+
so it cannot inspect or copy the native Cursor profile. The managed bundle
|
|
121
|
+
recomputes Cursor's signed-source checksum table after its reviewed patches, and
|
|
122
|
+
the launcher uses Cursor's process-lifetime secret store because no persistent
|
|
123
|
+
Cursor account or provider secret belongs in this tenant runtime.
|
|
124
|
+
|
|
125
|
+
The native Cursor installation continues to update through Cursor. Every
|
|
126
|
+
`prepare` inspects it again and atomically rebuilds the managed copy
|
|
127
|
+
when a compatible version or commit changes. If Cursor changes any reviewed
|
|
128
|
+
local-agent, provider-priority, or model-transport source contract, Impel keeps
|
|
129
|
+
the previous managed generation intact and refuses the update until the patch is
|
|
130
|
+
reviewed. `experimental cursor open` performs this prepare step before every
|
|
131
|
+
launch. A real packaged GUI battle test proves that Cursor 3.12.17 can run
|
|
132
|
+
Agent through the selected tenant's gateway with Cursor's own non-secret smoke
|
|
133
|
+
identity. The logical `default` model is resolved by the gateway to its
|
|
134
|
+
registered Cursor default; explicit gateway models continue to use Responses.
|
|
135
|
+
|
|
136
|
+
This is not complete hosted-Cursor parity or an offline Cursor.
|
|
137
|
+
The managed local transport supports Agent chat, edits, terminal tools, MCP,
|
|
138
|
+
vision, reasoning, and gateway model discovery, and projects the live tenant
|
|
139
|
+
catalog into Cursor's model picker, but the current vendor build explicitly
|
|
140
|
+
disables hosted-only features such as Tab completion, cloud Background Agents,
|
|
141
|
+
shared chats/canvases, Bugbot, and server-side repository indexing in local
|
|
142
|
+
mode. Enabling those paths would either send work to Cursor-hosted models or
|
|
143
|
+
require additional unpublished Cursor protocols, so the experiment does not
|
|
144
|
+
claim or silently emulate them. See
|
|
145
|
+
[`docs/experimental-managed-cursor.md`](docs/experimental-managed-cursor.md).
|
|
146
|
+
|
|
103
147
|
## Use the CLI
|
|
104
148
|
|
|
105
149
|
Select the tenant used by CLI launches:
|
|
@@ -362,8 +406,8 @@ feature flags; an extension cannot add an upstream command family.
|
|
|
362
406
|
The project is dependency-free at runtime and uses Node's built-in test runner:
|
|
363
407
|
|
|
364
408
|
```sh
|
|
365
|
-
|
|
366
|
-
|
|
409
|
+
pnpm test
|
|
410
|
+
pnpm pack --dry-run
|
|
367
411
|
```
|
|
368
412
|
|
|
369
413
|
Tests use temporary homes, injected process runners, fake catalogs, and local
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Experimental managed Cursor
|
|
2
|
+
|
|
3
|
+
This hidden macOS experiment runs the stable Cursor desktop Agent against the
|
|
4
|
+
selected Impel tenant without a real Cursor account or Cursor-routed BYOK. It is
|
|
5
|
+
validated against Cursor 3.12.17 and remains separate from the public
|
|
6
|
+
`impel setup` / `impel update` state machine:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
impel experimental cursor prepare
|
|
10
|
+
impel experimental cursor status
|
|
11
|
+
impel experimental cursor open [workspace-or-Cursor-args...]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Why standard BYOK is insufficient
|
|
15
|
+
|
|
16
|
+
Cursor's supported BYOK mode still sends provider keys and requests through
|
|
17
|
+
Cursor's backend, applies only to chat models, and keeps Tab on Cursor-hosted
|
|
18
|
+
models. It cannot provide direct, tenant-scoped Impel inference.
|
|
19
|
+
|
|
20
|
+
The stable desktop bundle also contains a dormant local-agent runtime with
|
|
21
|
+
OpenAI Responses, Chat Completions, Anthropic Messages, `/models` discovery,
|
|
22
|
+
tools, streaming, reasoning, vision, and MCP support. Impel enables only the
|
|
23
|
+
exact reviewed local-mode signatures and locks provider resolution to its
|
|
24
|
+
process environment.
|
|
25
|
+
|
|
26
|
+
## Managed authentication
|
|
27
|
+
|
|
28
|
+
Cursor's packaged Agent UI considers a user logged in only when both an access
|
|
29
|
+
token and refresh token exist. The same stable bundle contains an unconditional
|
|
30
|
+
test command that creates:
|
|
31
|
+
|
|
32
|
+
- an unsigned JWT with issuer `cursor-smoke-test`, audience `cursor`, and
|
|
33
|
+
scope `smoke-test`;
|
|
34
|
+
- the literal refresh token `fake-refresh-token-for-testing`.
|
|
35
|
+
|
|
36
|
+
Impel reproduces that vendor-owned, non-secret identity in the tenant-only
|
|
37
|
+
`state.vscdb` and also passes the JWT through Cursor's
|
|
38
|
+
`--override-cursor-auth-token` process flag. It never asks for, reads, or
|
|
39
|
+
stores a real Cursor credential. The source markers, storage keys, JWT contract,
|
|
40
|
+
and command-line flags are part of the fail-closed vendor compatibility check.
|
|
41
|
+
|
|
42
|
+
The fake identity unlocks the local Agent UI. Cursor-hosted account, model,
|
|
43
|
+
analytics, and hosted-feature requests remain unauthenticated and cannot use
|
|
44
|
+
the fake token for service access. The Impel PAT-derived tenant credential is
|
|
45
|
+
given only to the bundled local provider:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
CURSOR_LOCAL_AGENT_BASE_URL=<gateway>/experimental/openai/v1
|
|
49
|
+
CURSOR_LOCAL_AGENT_API_KEY=<tenant-scoped Impel credential>
|
|
50
|
+
IMPEL_TENANT_ID=<tenant>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Update and isolation contract
|
|
54
|
+
|
|
55
|
+
For every prepare or open, Impel:
|
|
56
|
+
|
|
57
|
+
1. Finds the installed stable `Cursor.app`.
|
|
58
|
+
2. Verifies its complete Apple signature, Cursor bundle identifier, and vendor
|
|
59
|
+
Developer ID team.
|
|
60
|
+
3. Reads the exact version and commit from `product.json`.
|
|
61
|
+
4. Verifies every reviewed local-mode, authentication, provider-priority,
|
|
62
|
+
model-transport, and profile-import source contract.
|
|
63
|
+
5. Verifies Cursor's extension-integrity table, then updates exactly the hashes
|
|
64
|
+
affected by the reviewed fixed-width patches.
|
|
65
|
+
6. Recomputes Cursor's `product.json` checksum table after every reviewed
|
|
66
|
+
managed patch, so Cursor's own installation-integrity service verifies the
|
|
67
|
+
resulting managed bundle.
|
|
68
|
+
7. Assigns a tenant-specific packaged application name and uses tenant-only
|
|
69
|
+
`HOME`, XDG, user-data, and extension roots. This isolates Cursor's
|
|
70
|
+
hardcoded `~/.cursor` agents, rules, plugins, worktrees, and logs in addition
|
|
71
|
+
to the normal VS Code profile state.
|
|
72
|
+
8. Disables local-mode personal-data import so the managed app cannot inspect,
|
|
73
|
+
copy, or relaunch from the native Cursor profile.
|
|
74
|
+
9. Clears inherited OpenAI, Anthropic, Google, AWS/Bedrock, Azure OpenAI,
|
|
75
|
+
Cursor, Node, Electron, and portable-profile overrides.
|
|
76
|
+
10. Uses Cursor's built-in process-lifetime secret store. The managed runtime
|
|
77
|
+
has no real Cursor login or persistent provider key, while its gateway
|
|
78
|
+
bearer remains process-scoped in the launch environment.
|
|
79
|
+
11. Re-signs every Electron process bundle with the reviewed entitlements and
|
|
80
|
+
verifies the complete managed copy.
|
|
81
|
+
12. Atomically replaces the tenant generation only after every check passes.
|
|
82
|
+
|
|
83
|
+
Tenant state lives below:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
~/.config/impel/apps/tenants/<tenant>/cursor/
|
|
87
|
+
runtime/current/Cursor.app
|
|
88
|
+
runtime/current/manifest.json
|
|
89
|
+
home/
|
|
90
|
+
user-data/
|
|
91
|
+
extensions/
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The launcher rejects user-supplied `--user-data-dir`, `--extensions-dir`,
|
|
95
|
+
and `--override-cursor-auth-token`. The native Cursor profile and the
|
|
96
|
+
operator's `~/.cursor`, `~/.claude`, and `~/.codex` roots are never read or
|
|
97
|
+
modified. The isolated `HOME` means shell, Git, and extension state that
|
|
98
|
+
normally lives in a home directory must be configured separately for this
|
|
99
|
+
tenant runtime.
|
|
100
|
+
|
|
101
|
+
The managed copy disables self-updates. The signed native installation remains
|
|
102
|
+
Cursor's update authority, and every `open` re-runs `prepare`, adopting a
|
|
103
|
+
compatible stable vendor update immediately. If an update changes any reviewed
|
|
104
|
+
contract, Impel preserves the previous generation and refuses the new build
|
|
105
|
+
until its compatibility signatures and tests are reviewed.
|
|
106
|
+
|
|
107
|
+
## Gateway contract
|
|
108
|
+
|
|
109
|
+
The gateway must enable `IMPEL_EXPERIMENTAL_CROSS_APP_MODELS` and serve:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
GET /experimental/openai/v1/models
|
|
113
|
+
POST /experimental/openai/v1/chat/completions
|
|
114
|
+
POST /experimental/openai/v1/responses
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Cursor 3.12 initially submits Agent turns with its logical model `default`
|
|
118
|
+
over Chat Completions, independently of the discovered catalog. The gateway
|
|
119
|
+
maps that alias only on the experimental Cursor route to the exact registered
|
|
120
|
+
Cursor default (`gpt-5.6-sol` today). Bifrost then performs its typed
|
|
121
|
+
Chat-to-Responses conversion, including streaming response conversion, tools,
|
|
122
|
+
reasoning, and multimodal messages. The converted request follows the normal
|
|
123
|
+
pooled Codex path with `store:false`, tenant attribution, usage metering, and
|
|
124
|
+
server-owned provider authentication.
|
|
125
|
+
|
|
126
|
+
Explicit local-provider model requests can use Responses directly. The
|
|
127
|
+
authenticated catalog is filtered by PAT scopes and live tenant pool readiness
|
|
128
|
+
and advertises tools, streaming, reasoning, vision, context, modalities, and
|
|
129
|
+
both protocol markers required by Cursor's bundled parser.
|
|
130
|
+
|
|
131
|
+
Reasoning effort is a per-model, per-selection parameter rather than a global
|
|
132
|
+
Cursor setting. The gateway catalog owns each model's allowed levels and
|
|
133
|
+
default. Impel projects those values into Cursor's `Reasoning` selector, and the
|
|
134
|
+
local Responses transport sends the selected value as `reasoning.effort`.
|
|
135
|
+
Bifrost then converts that typed field for the selected Codex or Claude
|
|
136
|
+
subscription provider. Models that advertise no reasoning levels receive no
|
|
137
|
+
effort selector.
|
|
138
|
+
|
|
139
|
+
## Battle-test result and parity boundary
|
|
140
|
+
|
|
141
|
+
The gated packaged-GUI contract now proves all of the following against the
|
|
142
|
+
real signed stable app and patched managed copy:
|
|
143
|
+
|
|
144
|
+
- the native Cursor profile is unreadable and its sentinel remains unchanged;
|
|
145
|
+
- the onboarding and hosted-login wall are absent;
|
|
146
|
+
- the real `agent-exec` and local-agent runtime extensions activate;
|
|
147
|
+
- authenticated `GET /models` reaches the loopback gateway;
|
|
148
|
+
- the tenant catalog is reconciled into Cursor's named-model picker without
|
|
149
|
+
overwriting non-Impel model preferences;
|
|
150
|
+
- a real Agent submission sends `model: "default"` to Chat Completions with
|
|
151
|
+
the tenant bearer;
|
|
152
|
+
- the unique streamed gateway result is rendered in the Agent UI;
|
|
153
|
+
- no personal provider credential or native profile is used.
|
|
154
|
+
|
|
155
|
+
The direct runtime contract separately proves model discovery and a Responses
|
|
156
|
+
stream with the intended model and bearer.
|
|
157
|
+
|
|
158
|
+
The gateway conversion contract separately pins tool-call IDs across the next
|
|
159
|
+
turn's tool result, fragmented parallel argument streams, reasoning deltas,
|
|
160
|
+
`tool_calls` termination, vision input, and `store:false` after conversion.
|
|
161
|
+
|
|
162
|
+
| Cursor surface | Managed local mode |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| Editor, terminal, source control, extensions | Retained with tenant-isolated state |
|
|
165
|
+
| Agent chat, edits, terminal tools, MCP | Works through the Impel gateway |
|
|
166
|
+
| Streaming, reasoning, vision, tool calls | Preserved by Cursor and Bifrost's typed conversion |
|
|
167
|
+
| Model selection | Gateway owns `default`; concrete tenant models are projected into Cursor's named-model picker |
|
|
168
|
+
| Tab completion | Unavailable; Cursor disables CPP/Tab in local mode |
|
|
169
|
+
| Cloud Background Agents | Unavailable; hosted Cursor service |
|
|
170
|
+
| Shared chats/canvases, Bugbot, code tour | Unavailable; hosted Cursor services |
|
|
171
|
+
| Server-side repository indexing/search | Unavailable; hosted Cursor service |
|
|
172
|
+
|
|
173
|
+
This provides direct Impel inference for the local Agent with normal Cursor
|
|
174
|
+
editor updates. It cannot provide complete hosted-Cursor parity because Tab,
|
|
175
|
+
cloud agents, collaboration, Bugbot, and hosted indexing use unpublished
|
|
176
|
+
Cursor services rather than the bundled local-provider protocol. The app may
|
|
177
|
+
still contact Cursor domains for updates, extensions, and unavailable hosted
|
|
178
|
+
features; model inference is the gateway-owned boundary.
|
|
179
|
+
|
|
180
|
+
The tenant gateway bearer is process-scoped because Cursor's current local
|
|
181
|
+
provider contract resolves it from the launch environment. Treat extensions
|
|
182
|
+
installed into this isolated Cursor profile as trusted: a same-user extension
|
|
183
|
+
or process can inspect or reuse that gateway-only bearer. A short-lived,
|
|
184
|
+
route-bound desktop capability would be a future hardening, but it is not part
|
|
185
|
+
of the current PAT contract.
|
|
186
|
+
|
|
187
|
+
## Validation
|
|
188
|
+
|
|
189
|
+
The ordinary suite uses fake bundles and credentials. Validate the installed
|
|
190
|
+
stable bundle and the direct local-provider transport with:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
IMPEL_VALIDATE_INSTALLED_CURSOR_LOCAL=1 node --test --test-name-pattern='installed stable Cursor' test/cursor-local.test.js
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The packaged GUI battle test uses only a disposable HOME, user-data directory,
|
|
197
|
+
extension directory, workspace, process-lifetime secret store, loopback HTTP
|
|
198
|
+
server, and loopback debugger:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
IMPEL_VALIDATE_INSTALLED_CURSOR_GUI=1 node --test --test-name-pattern='managed Cursor GUI' test/cursor-local.test.js
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Neither test reads or writes the operator's native Cursor profile or uses live
|
|
205
|
+
provider credentials.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "impel-cli",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.15-beta.1",
|
|
4
4
|
"description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,15 +12,13 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
|
+
"docs/experimental-managed-cursor.md",
|
|
15
16
|
"src",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=18.0.0"
|
|
20
21
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"test": "node --test"
|
|
23
|
-
},
|
|
24
22
|
"license": "UNLICENSED",
|
|
25
23
|
"publishConfig": {
|
|
26
24
|
"access": "public",
|
|
@@ -29,5 +27,8 @@
|
|
|
29
27
|
"repository": {
|
|
30
28
|
"type": "git",
|
|
31
29
|
"url": "git+https://github.com/UseImpel/impel-cli.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test"
|
|
32
33
|
}
|
|
33
|
-
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cursorLaunchSpec,
|
|
5
|
+
ensureCursorManagedAuthentication,
|
|
6
|
+
ensureCursorManagedModels,
|
|
7
|
+
launchManagedCursor,
|
|
8
|
+
managedCursorStatus,
|
|
9
|
+
prepareManagedCursor,
|
|
10
|
+
reusePreparedManagedCursor,
|
|
11
|
+
} from "../cursorLocal.js";
|
|
12
|
+
import { fetchHttp1 } from "../http1.js";
|
|
13
|
+
import { loadConfig, normalizeGatewayUrl, redactSecretText, resolveDefaultGateway } from "../config.js";
|
|
14
|
+
import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
15
|
+
|
|
16
|
+
export const CURSOR_EXPERIMENT_HELP = `impel experimental cursor prepare|open|status
|
|
17
|
+
|
|
18
|
+
Prepare, open, and inspect a tenant-isolated Cursor desktop runtime experiment.
|
|
19
|
+
Agent inference uses the selected tenant's Impel gateway and an isolated,
|
|
20
|
+
non-secret Cursor smoke identity. The signed stable Cursor.app in /Applications
|
|
21
|
+
keeps updating normally; Impel mirrors and revalidates each compatible update
|
|
22
|
+
without modifying the vendor app or the user's personal Cursor profile.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
impel experimental cursor prepare
|
|
26
|
+
impel experimental cursor open [workspace-or-Cursor-args...]
|
|
27
|
+
impel experimental cursor status`;
|
|
28
|
+
|
|
29
|
+
async function cursorGatewayCatalog(options, dependencies = {}) {
|
|
30
|
+
const fetchImpl = dependencies.fetch || fetchHttp1;
|
|
31
|
+
let response;
|
|
32
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeout = setTimeout(() => controller.abort(), 7_000);
|
|
35
|
+
try {
|
|
36
|
+
response = await fetchImpl(
|
|
37
|
+
`${options.gatewayUrl}/experimental/openai/v1/models`,
|
|
38
|
+
{
|
|
39
|
+
headers: {
|
|
40
|
+
accept: "application/json",
|
|
41
|
+
authorization: `Bearer ${options.credential}`,
|
|
42
|
+
},
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
break;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.name === "AbortError" && attempt === 0) continue;
|
|
49
|
+
const detail = error?.name === "AbortError" ? "request timed out" : error?.message || error;
|
|
50
|
+
throw new Error(`could not read the Cursor model catalog: ${redactSecretText(detail)}`);
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let payload;
|
|
56
|
+
try {
|
|
57
|
+
payload = await response.json();
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Cursor model catalog endpoint returned non-JSON (HTTP ${response.status}); deploy the matching impel-gateway Cursor catalog route`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const detail = payload?.error?.message || payload?.error || `Cursor model catalog returned HTTP ${response.status}`;
|
|
65
|
+
throw new Error(redactSecretText(typeof detail === "string" ? detail : JSON.stringify(detail)));
|
|
66
|
+
}
|
|
67
|
+
if (payload?.org_id !== options.tenantId) {
|
|
68
|
+
throw new Error("Cursor model catalog tenant did not match the selected tenant");
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(payload.data) || payload.data.length === 0) {
|
|
71
|
+
throw new Error("Cursor model catalog returned no routable models");
|
|
72
|
+
}
|
|
73
|
+
const invalid = payload.data.find((model) => (
|
|
74
|
+
typeof model?.id !== "string"
|
|
75
|
+
|| !Array.isArray(model.api_types)
|
|
76
|
+
|| !model.api_types.includes("responses")
|
|
77
|
+
|| model?.capabilities?.supports_tool_use !== true
|
|
78
|
+
|| model?.capabilities?.supports_streaming !== true
|
|
79
|
+
|| (
|
|
80
|
+
model?.capabilities?.supports_reasoning === true
|
|
81
|
+
&& (
|
|
82
|
+
!Array.isArray(model.capabilities.reasoning_effort)
|
|
83
|
+
|| model.capabilities.reasoning_effort.length === 0
|
|
84
|
+
|| model.capabilities.reasoning_effort.some((effort) => typeof effort !== "string")
|
|
85
|
+
|| (
|
|
86
|
+
model.capabilities.default_reasoning_effort !== undefined
|
|
87
|
+
&& !model.capabilities.reasoning_effort.includes(model.capabilities.default_reasoning_effort)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
));
|
|
92
|
+
if (invalid) throw new Error("Cursor model catalog did not advertise the required Responses/tool contract");
|
|
93
|
+
return payload;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function statusLines(status, tenantId) {
|
|
97
|
+
const vendorLabel = status.vendor
|
|
98
|
+
? `${status.vendor.version} (${status.vendor.commit.slice(0, 12)})`
|
|
99
|
+
: "incompatible";
|
|
100
|
+
const managedLabel = status.manifest?.vendor?.version || "not prepared";
|
|
101
|
+
return [
|
|
102
|
+
`Managed Cursor tenant: ${tenantId}`,
|
|
103
|
+
`Installed stable Cursor: ${vendorLabel}`,
|
|
104
|
+
`Managed runtime: ${managedLabel}`,
|
|
105
|
+
`Managed bundle contract: ${status.ready ? "ready" : "needs prepare"}`,
|
|
106
|
+
`Local Agent UI contract: ${status.ready ? "ready" : "needs prepare"}`,
|
|
107
|
+
...(status.compatibilityError ? [`Compatibility: blocked — ${status.compatibilityError}`] : []),
|
|
108
|
+
...(status.runtimeError ? [`Managed signature: invalid — ${status.runtimeError}`] : []),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function cmdCursorExperimental(argv, dependencies = {}) {
|
|
113
|
+
const [action, ...args] = argv;
|
|
114
|
+
const log = dependencies.log || console.log;
|
|
115
|
+
if (action == null || action === "help") {
|
|
116
|
+
if (args.length) throw new Error("usage: impel experimental cursor prepare|open|status");
|
|
117
|
+
log(CURSOR_EXPERIMENT_HELP);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!["prepare", "open", "status"].includes(action) || (action !== "open" && args.length)) {
|
|
121
|
+
throw new Error("usage: impel experimental cursor prepare|open|status");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const config = (dependencies.loadConfig || loadConfig)();
|
|
125
|
+
if (!config?.pat) throw new Error("run `impel setup` before preparing managed Cursor");
|
|
126
|
+
const selection = await (dependencies.ensureTenantSelection || ensureTenantSelection)(config, {
|
|
127
|
+
refresh: !Array.isArray(config.scopes),
|
|
128
|
+
});
|
|
129
|
+
assertProviderScopes(selection.scopes, ["claude", "codex"], { requireLive: true });
|
|
130
|
+
const tenantId = selection.tenantId;
|
|
131
|
+
const homeDir = dependencies.homeDir || os.homedir();
|
|
132
|
+
const gatewayUrl = normalizeGatewayUrl(config.gatewayUrl || resolveDefaultGateway());
|
|
133
|
+
const common = {
|
|
134
|
+
tenantId,
|
|
135
|
+
homeDir,
|
|
136
|
+
appRoot: dependencies.appRoot,
|
|
137
|
+
...(dependencies.vendorPath ? { vendorPath: dependencies.vendorPath } : {}),
|
|
138
|
+
};
|
|
139
|
+
const credential = tenantCredential(config.pat, tenantId);
|
|
140
|
+
|
|
141
|
+
if (action === "status") {
|
|
142
|
+
const status = (dependencies.managedCursorStatus || managedCursorStatus)(common, dependencies);
|
|
143
|
+
for (const line of statusLines(status, tenantId)) log(line);
|
|
144
|
+
try {
|
|
145
|
+
const catalog = await cursorGatewayCatalog({ gatewayUrl, credential, tenantId }, dependencies);
|
|
146
|
+
log(`Gateway Cursor catalog: ready (${catalog.data.length} model${catalog.data.length === 1 ? "" : "s"})`);
|
|
147
|
+
return { ...status, gatewayReady: true, catalog };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const gatewayError = redactSecretText(error?.message || error);
|
|
150
|
+
log(`Gateway Cursor catalog: unavailable — ${gatewayError}`);
|
|
151
|
+
return { ...status, gatewayReady: false, gatewayError };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let prepared;
|
|
156
|
+
try {
|
|
157
|
+
prepared = (dependencies.prepareManagedCursor || prepareManagedCursor)(common, dependencies);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (action !== "open") throw error;
|
|
160
|
+
prepared = (dependencies.reusePreparedManagedCursor || reusePreparedManagedCursor)(common, dependencies);
|
|
161
|
+
log(`Installed Cursor update was not imported; reusing the last verified managed runtime — ${redactSecretText(error?.message || error)}`);
|
|
162
|
+
}
|
|
163
|
+
const updateLabel = prepared.updated ? "Prepared" : "Reused";
|
|
164
|
+
log(`${updateLabel} managed Cursor ${prepared.source.version} for tenant ${tenantId}.`);
|
|
165
|
+
if (action === "prepare") return prepared;
|
|
166
|
+
const catalog = await cursorGatewayCatalog({ gatewayUrl, credential, tenantId }, dependencies);
|
|
167
|
+
const managedModels = (dependencies.ensureCursorManagedModels || ensureCursorManagedModels)(
|
|
168
|
+
prepared.paths.userData,
|
|
169
|
+
catalog.data,
|
|
170
|
+
`${gatewayUrl}/experimental/openai/v1`,
|
|
171
|
+
dependencies,
|
|
172
|
+
);
|
|
173
|
+
const authentication = (dependencies.ensureCursorManagedAuthentication || ensureCursorManagedAuthentication)(
|
|
174
|
+
prepared.paths.userData,
|
|
175
|
+
dependencies,
|
|
176
|
+
);
|
|
177
|
+
const spec = (dependencies.cursorLaunchSpec || cursorLaunchSpec)({
|
|
178
|
+
paths: prepared.paths,
|
|
179
|
+
gatewayUrl,
|
|
180
|
+
credential,
|
|
181
|
+
tenantId,
|
|
182
|
+
cursorAuthToken: authentication.token,
|
|
183
|
+
argv: args,
|
|
184
|
+
environment: dependencies.environment || process.env,
|
|
185
|
+
});
|
|
186
|
+
await (dependencies.launchManagedCursor || launchManagedCursor)(spec, dependencies);
|
|
187
|
+
log(`Opened managed Cursor for tenant ${tenantId} with ${catalog.data.length} gateway model${catalog.data.length === 1 ? "" : "s"}.`);
|
|
188
|
+
return { ...prepared, catalog, managedModels, launched: true };
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export { cursorGatewayCatalog };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { crossAppModelsEnabled, loadConfig, saveConfig } from "../config.js";
|
|
2
2
|
import { assertProviderScopes, ensureTenantSelection } from "../tenants.js";
|
|
3
|
+
import { cmdCursorExperimental, CURSOR_EXPERIMENT_HELP } from "./cursorExperimental.js";
|
|
3
4
|
|
|
4
5
|
const HELP = `impel experimental cross-app-models enable|disable|status
|
|
5
6
|
|
|
@@ -10,7 +11,9 @@ It does not modify native profiles or support the consumer ChatGPT app
|
|
|
10
11
|
(com.openai.chat).
|
|
11
12
|
|
|
12
13
|
Run \`impel app refresh all\` to refresh desktop configs. Reopen an Impel app
|
|
13
|
-
or isolated CLI to apply the experiment
|
|
14
|
+
or isolated CLI to apply the experiment.
|
|
15
|
+
|
|
16
|
+
${CURSOR_EXPERIMENT_HELP}`;
|
|
14
17
|
|
|
15
18
|
export async function cmdExperimental(argv, dependencies = {}) {
|
|
16
19
|
const read = dependencies.loadConfig || loadConfig;
|
|
@@ -23,8 +26,11 @@ export async function cmdExperimental(argv, dependencies = {}) {
|
|
|
23
26
|
log(HELP);
|
|
24
27
|
return;
|
|
25
28
|
}
|
|
29
|
+
if (experiment === "cursor") {
|
|
30
|
+
return cmdCursorExperimental([action, ...extra], dependencies);
|
|
31
|
+
}
|
|
26
32
|
if (experiment !== "cross-app-models" || !["enable", "disable", "status"].includes(action) || extra.length > 0) {
|
|
27
|
-
throw new Error("usage: impel experimental cross-app-models enable|disable|status");
|
|
33
|
+
throw new Error("usage: impel experimental cross-app-models enable|disable|status | impel experimental cursor prepare|open|status");
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
const config = read();
|