impel-cli 0.18.15-beta.9 → 0.18.15

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.
@@ -11,10 +11,9 @@ import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
11
11
  import { normalizeTenantId } from "./tenants.js";
12
12
  import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
13
13
 
14
- const WINDOWS_CHATGPT_PIN = PINNED_VENDOR_APPS.chatgpt.windows;
15
- export const WINDOWS_CHATGPT_PACKAGE = WINDOWS_CHATGPT_PIN.storeProductId;
16
- export const WINDOWS_CHATGPT_PACKAGE_NAME = WINDOWS_CHATGPT_PIN.packageName;
17
- export const WINDOWS_CHATGPT_PUBLISHER_ID = WINDOWS_CHATGPT_PIN.publisherId;
14
+ export const WINDOWS_CHATGPT_PACKAGE = "9PLM9XGG6VKS";
15
+ export const WINDOWS_CHATGPT_PACKAGE_NAME = "OpenAI.Codex";
16
+ export const WINDOWS_CHATGPT_PUBLISHER_ID = "2p2nqsd0c76g0";
18
17
  const WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
19
18
  const WINDOWS_WINGET_UPDATE_NOT_APPLICABLE = 0x8A15002B;
20
19
 
@@ -61,26 +60,20 @@ function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
61
60
  }
62
61
  }
63
62
 
64
- function installedMsixChatGPT(environment, run = spawnSync, pin = null) {
63
+ function installedMsixChatGPT(environment, run = spawnSync) {
65
64
  const script = [
66
65
  `$package = Get-AppxPackage -Name '${WINDOWS_CHATGPT_PACKAGE_NAME}' -ErrorAction SilentlyContinue`,
67
- `$package = $package | Where-Object { $_.PublisherId -ceq '${WINDOWS_CHATGPT_PUBLISHER_ID}' -and (-not $env:IMPEL_CHATGPT_PACKAGE_VERSION -or $_.Version.ToString() -ceq $env:IMPEL_CHATGPT_PACKAGE_VERSION) } | Sort-Object Version -Descending | Select-Object -First 1`,
66
+ `$package = $package | Where-Object PublisherId -eq '${WINDOWS_CHATGPT_PUBLISHER_ID}' | Sort-Object Version -Descending | Select-Object -First 1`,
68
67
  "if ($package) {",
69
68
  " $manifest = Get-AppxPackageManifest -Package $package",
70
69
  " $relative = $manifest.Package.Applications.Application | Where-Object Id -eq 'App' | Select-Object -First 1 -ExpandProperty Executable",
71
- " $normalized = ([string]$relative).Replace('/', '\\')",
72
- " $expected = ([string]$env:IMPEL_CHATGPT_PACKAGE_EXECUTABLE).Replace('/', '\\')",
73
- " if ($relative -and (-not $expected -or $normalized -ieq $expected)) { Join-Path $package.InstallLocation $relative }",
70
+ " if ($relative) { Join-Path $package.InstallLocation $relative }",
74
71
  "}",
75
72
  ].join("; ");
76
73
  try {
77
74
  const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
78
75
  encoding: "utf8",
79
- env: {
80
- ...windowsPowerShellEnvironment(environment),
81
- IMPEL_CHATGPT_PACKAGE_VERSION: pin?.packageVersion || "",
82
- IMPEL_CHATGPT_PACKAGE_EXECUTABLE: pin?.executable || "",
83
- },
76
+ env: windowsPowerShellEnvironment(environment),
84
77
  stdio: ["ignore", "pipe", "ignore"],
85
78
  windowsHide: true,
86
79
  });
@@ -148,23 +141,9 @@ export function findWindowsChatGPTApp(environment = process.env, dependencies =
148
141
  return msix && exists(msix) ? path.win32.normalize(msix) : null;
149
142
  }
150
143
 
151
- function pinnedWindowsChatGPTApp(environment = process.env, dependencies = {}) {
152
- const exists = dependencies.isFile || isFile;
153
- const pin = dependencies.pin || WINDOWS_CHATGPT_PIN;
154
- const msix = (dependencies.queryMsix || installedMsixChatGPT)(environment, dependencies.run, pin);
155
- return msix && exists(msix) ? path.win32.normalize(msix) : null;
156
- }
157
-
158
- function wingetInvocation(action, environment, {
159
- packageId,
160
- source = "winget",
161
- scope = "user",
162
- }) {
144
+ function wingetInvocation(action, environment, { packageId, source = "winget", scope = "user" }) {
163
145
  const verb = action === "update" ? "upgrade" : "install";
164
146
  const scopeArgs = scope ? ["--scope", scope] : [];
165
- // Microsoft Store packages commonly report Version "Unknown" to winget.
166
- // Without this switch winget refuses to consider their available update.
167
- const updateArgs = action === "update" ? ["--include-unknown"] : [];
168
147
  return nativeCommandInvocation(
169
148
  "winget",
170
149
  [
@@ -175,7 +154,6 @@ function wingetInvocation(action, environment, {
175
154
  "--source",
176
155
  source,
177
156
  ...scopeArgs,
178
- ...updateArgs,
179
157
  "--silent",
180
158
  "--disable-interactivity",
181
159
  "--accept-package-agreements",
@@ -394,31 +372,21 @@ export async function ensureWindowsClaudeApp(_options = {}, dependencies = {}) {
394
372
 
395
373
  /** Install/update OpenAI's official ChatGPT/Codex Store package. */
396
374
  export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies = {}) {
397
- const find = dependencies.find || findWindowsChatGPTApp;
398
- const findPinned = dependencies.findPinned || dependencies.find || pinnedWindowsChatGPTApp;
399
375
  const io = {
400
376
  environment: process.env,
401
- find,
402
- findPinned,
377
+ find: findWindowsChatGPTApp,
403
378
  logger: console,
404
- pin: WINDOWS_CHATGPT_PIN,
405
379
  run: spawnSync,
406
380
  ...dependencies,
407
381
  };
408
382
  const before = io.find(io.environment);
409
- const pinnedBefore = io.findPinned(io.environment, { pin: io.pin, run: io.run });
410
- if (pinnedBefore && !update) {
411
- return { binary: pinnedBefore, action: "existing", result: null };
412
- }
383
+ if (before && !update) return { binary: before, action: "existing", result: null };
413
384
 
414
385
  // `impel update` also repairs missing surfaces. A missing Store package must
415
386
  // be installed, not sent to `winget upgrade` (which returns 0x8A150014).
416
387
  const shouldUpdate = Boolean(update && before);
417
- // The msstore source rejects its AppX package version as a winget --version
418
- // selector. Install the current Store package, then fail closed below unless
419
- // Windows registered the exact reviewed package version and executable.
420
388
  const invocation = wingetInvocation(shouldUpdate ? "update" : "install", io.environment, {
421
- packageId: io.pin.storeProductId,
389
+ packageId: WINDOWS_CHATGPT_PACKAGE,
422
390
  source: "msstore",
423
391
  scope: null,
424
392
  });
@@ -432,36 +400,26 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
432
400
  } catch (error) {
433
401
  result = { status: null, error };
434
402
  }
435
- let resultBinary = io.findPinned(io.environment, { pin: io.pin, run: io.run });
403
+ const binary = io.find(io.environment) || before;
436
404
  // winget uses an error HRESULT when `upgrade` finds an installed package
437
405
  // with no newer Store release. That is a healthy idempotent update result,
438
406
  // not an installation failure (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE).
439
407
  const alreadyUpToDate = Boolean(
440
408
  shouldUpdate
441
- && resultBinary
409
+ && binary
442
410
  && !result?.error
443
411
  && Number.isInteger(result?.status)
444
412
  && (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE,
445
413
  );
446
414
  if (alreadyUpToDate) {
447
- io.logger.log(`ChatGPT/Codex: Microsoft Store package ${io.pin.packageVersion} already up to date.`);
448
- }
449
- const commandSucceeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
450
- if (commandSucceeded && !resultBinary) {
451
- result = {
452
- ...result,
453
- error: new Error(
454
- `ChatGPT/Codex Store package ${io.pin.packageVersion} did not appear after winget completed`,
455
- ),
456
- };
415
+ io.logger.log("ChatGPT/Codex: Microsoft Store package already up to date.");
457
416
  }
458
- const succeeded = commandSucceeded && Boolean(resultBinary);
459
- if (!succeeded) resultBinary = null;
417
+ const succeeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
460
418
  return {
461
- binary: resultBinary,
419
+ binary,
462
420
  action: alreadyUpToDate
463
421
  ? "existing"
464
- : succeeded ? (shouldUpdate ? "updated" : "installed") : "install-failed",
422
+ : succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
465
423
  result,
466
424
  };
467
425
  }
@@ -659,7 +617,6 @@ export function launchWindowsChatGPTApp(binary, {
659
617
  "CODEX_AUTHAPI_BASE_URL",
660
618
  "CODEX_ELECTRON_USER_DATA_PATH",
661
619
  "CODEX_HOME",
662
- "CODEX_SPARKLE_ENABLED",
663
620
  "OPENAI_API_KEY",
664
621
  "OPENAI_BASE_URL",
665
622
  ]);
@@ -668,7 +625,6 @@ export function launchWindowsChatGPTApp(binary, {
668
625
  }
669
626
  environment.CODEX_HOME = codexHome;
670
627
  environment.CODEX_AUTHAPI_BASE_URL = `${gatewayUrl}/chatgpt_passthrough/backend-api`;
671
- environment.CODEX_SPARKLE_ENABLED = "false";
672
628
  // Codex Desktop resolves this before acquiring Electron's single-instance
673
629
  // lock. Keep --user-data-dir below as a compatibility fallback for builds
674
630
  // that predate the dedicated desktop override.
@@ -8,7 +8,6 @@ import {
8
8
  nativeSpawnInvocation,
9
9
  } from "./nativeProcess.js";
10
10
  import { syncSkillsSafe } from "./skills.js";
11
- import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
12
11
  import { provisionWindowsGit } from "./windowsGit.js";
13
12
 
14
13
  const POWERSHELL_ARGS = [
@@ -28,14 +27,12 @@ const POWERSHELL_ARGS = [
28
27
  */
29
28
  export const WINDOWS_CLI_INSTALLERS = Object.freeze({
30
29
  claude: Object.freeze({
31
- version: PINNED_VENDOR_CLI_VERSIONS.claude,
32
- command: `powershell -ExecutionPolicy Bypass -NoProfile -Command "& ([scriptblock]::Create((irm https://claude.ai/install.ps1))) '${PINNED_VENDOR_CLI_VERSIONS.claude}'"`,
33
- script: `& ([scriptblock]::Create((Invoke-RestMethod -Uri 'https://claude.ai/install.ps1'))) '${PINNED_VENDOR_CLI_VERSIONS.claude}'`,
30
+ command: "powershell -ExecutionPolicy Bypass -NoProfile -Command \"irm https://claude.ai/install.ps1 | iex\"",
31
+ script: "Invoke-RestMethod -Uri 'https://claude.ai/install.ps1' | Invoke-Expression",
34
32
  }),
35
33
  codex: Object.freeze({
36
- version: PINNED_VENDOR_CLI_VERSIONS.codex,
37
- command: `powershell -ExecutionPolicy Bypass -NoProfile -Command "[Environment]::SetEnvironmentVariable('CODEX_NON_INTERACTIVE','1','Process'); & ([scriptblock]::Create((irm https://chatgpt.com/codex/install.ps1))) -Release '${PINNED_VENDOR_CLI_VERSIONS.codex}'"`,
38
- script: `[Environment]::SetEnvironmentVariable('CODEX_NON_INTERACTIVE', '1', 'Process'); & ([scriptblock]::Create((Invoke-RestMethod -Uri 'https://chatgpt.com/codex/install.ps1'))) -Release '${PINNED_VENDOR_CLI_VERSIONS.codex}'`,
34
+ command: "powershell -ExecutionPolicy Bypass -NoProfile -Command \"$env:CODEX_NON_INTERACTIVE='1'; irm https://chatgpt.com/codex/install.ps1 | iex\"",
35
+ script: "$env:CODEX_NON_INTERACTIVE = '1'; Invoke-RestMethod -Uri 'https://chatgpt.com/codex/install.ps1' | Invoke-Expression",
39
36
  }),
40
37
  });
41
38
 
@@ -1,205 +0,0 @@
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.13.25 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.
@@ -1,192 +0,0 @@
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 };