impel-cli 0.18.15 → 0.18.16

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.
@@ -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();
@@ -20,8 +20,9 @@ import { assertProviderScopes, ensureTenantSelection, tenantCredential } from ".
20
20
  import { maybePrintUpdateNotice } from "../updates.js";
21
21
  import {
22
22
  nativeSpawnInvocation,
23
- resolveNativeBinary,
24
23
  } from "../nativeProcess.js";
24
+ import { resolveReviewedVendorCliBinary } from "../vendorCliBinaries.js";
25
+ import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
25
26
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
26
27
  import {
27
28
  cacheProviderReadiness,
@@ -115,7 +116,15 @@ async function assertLiveProviderReadiness({ config, gatewayUrl, credential, ten
115
116
  }
116
117
 
117
118
  function runNativeCli(tool, argv, environment) {
118
- const binary = resolveNativeBinary(tool, environment);
119
+ const binary = resolveReviewedVendorCliBinary(tool, environment);
120
+ if (!binary) {
121
+ const version = PINNED_VENDOR_CLI_VERSIONS[tool];
122
+ console.error(
123
+ `impel ${tool}: reviewed ${tool === "claude" ? "Claude Code" : "Codex"} CLI `
124
+ + `v${version} is not installed. Run \`impel update\` to repair it.`,
125
+ );
126
+ return Promise.resolve(127);
127
+ }
119
128
  let invocation;
120
129
  try {
121
130
  invocation = nativeSpawnInvocation(binary, argv, environment);
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
 
5
5
  import { appPaths, CLAUDE_CONFIG_ID, readTenantManifest } from "../apps.js";
6
6
  import { crossAppModelsEnabled, loadConfig, maskSecret } from "../config.js";
7
- import { environmentValue, findNativeBinary } from "../nativeProcess.js";
7
+ import { environmentValue } from "../nativeProcess.js";
8
8
  import { tenantCliClientReadiness } from "../provisioning.js";
9
+ import { findReviewedVendorCliBinary } from "../vendorCliBinaries.js";
9
10
  import { windowsTenantShortcutName } from "../shellEntries.js";
10
11
  import {
11
12
  ensureTenantSelection,
@@ -86,7 +87,7 @@ export async function cmdStatus(overrides = {}) {
86
87
  cliReadiness: tenantCliClientReadiness,
87
88
  desktopReadiness,
88
89
  installedVersion,
89
- findNativeBinary,
90
+ findNativeBinary: findReviewedVendorCliBinary,
90
91
  maybePrintUpdateNotice,
91
92
  platform: process.platform,
92
93
  environment: process.env,
@@ -18,6 +18,7 @@ import {
18
18
  isNewerVersion,
19
19
  refreshUpdateCache,
20
20
  updateInstallSpec,
21
+ updateTagForVersion,
21
22
  writeUpdateCache,
22
23
  } from "../updates.js";
23
24
 
@@ -200,30 +201,41 @@ export async function cmdUpdate(argv, overrides = {}) {
200
201
  }
201
202
 
202
203
  const current = io.installedVersion();
203
- const remote = await io.progress("Checking npm for impel-cli updates", () => io.fetchRemoteVersion());
204
+ const updateTag = updateTagForVersion(current);
205
+ const installSpec = updateInstallSpec(updateTag);
206
+ const remote = await io.progress(
207
+ "Checking npm for impel-cli updates",
208
+ () => io.fetchRemoteVersion({ tag: updateTag }),
209
+ );
204
210
  if (remote) io.writeCache({ remoteVersion: remote, checkedAt: Date.now() });
205
211
 
206
212
  console.log(`impel-cli v${current ?? "?"}`);
207
- console.log(`npm latest: ${remote ? `v${remote}` : "unknown — registry check failed"}`);
213
+ console.log(`npm ${updateTag}: ${remote ? `v${remote}` : "unknown — registry check failed"}`);
208
214
 
209
215
  const updateAvailable = Boolean(current && remote && isNewerVersion(remote, current));
210
216
  const upToDate = Boolean(current && remote && !updateAvailable);
211
217
  if (flags.check) {
212
- console.log(upToDate ? "Up to date." : "Update available: run `impel update`.");
213
- if (!remote) process.exitCode = 1;
218
+ if (!remote) {
219
+ console.log("Update status unavailable; the installed CLI was not changed.");
220
+ process.exitCode = 1;
221
+ } else {
222
+ console.log(upToDate ? "Up to date." : "Update available: run `impel update`.");
223
+ }
214
224
  return;
215
225
  }
216
226
 
217
227
  // ── CLI ────────────────────────────────────────────────────────────────
218
- if (upToDate) {
228
+ if (!remote) {
229
+ console.warn("CLI: npm update check unavailable; keeping the installed build.");
230
+ } else if (upToDate) {
219
231
  console.log("CLI: already up to date.");
220
232
  } else {
221
- console.log("CLI: installing the latest build…");
222
- if (!await io.progress("Installing the latest impel-cli build", () => io.selfUpdate(updateInstallSpec()))) {
233
+ console.log(`CLI: installing the npm ${updateTag} build…`);
234
+ if (!await io.progress(`Installing the npm ${updateTag} impel-cli build`, () => io.selfUpdate(installSpec))) {
223
235
  console.error("impel update: `npm install -g` failed; the CLI was not updated.");
224
236
  if (io.platform === "win32") {
225
237
  console.error(" Verify `npm --version` in PowerShell, then retry `impel update`.");
226
- console.error(" Manual recovery: `npm install --global impel-cli@latest`.");
238
+ console.error(` Manual recovery: \`npm install --global ${installSpec}\`.`);
227
239
  }
228
240
  const config = io.loadConfig();
229
241
  let recovered = false;
@@ -239,7 +251,7 @@ export async function cmdUpdate(argv, overrides = {}) {
239
251
  platform: io.platform,
240
252
  architecture: process.arch,
241
253
  step: "install.impel_cli",
242
- command: `npm install --global ${RUNTIME_BRAND.cli.packageName}@latest`,
254
+ command: `npm install --global ${installSpec}`,
243
255
  message: `The npm-verified global ${RUNTIME_BRAND.cli.packageName} update failed.`,
244
256
  },
245
257
  config,
@@ -265,7 +277,7 @@ export async function cmdUpdate(argv, overrides = {}) {
265
277
  }
266
278
  },
267
279
  retryStep: async () => {
268
- updateState.ok = io.selfUpdate(updateInstallSpec()) === true;
280
+ updateState.ok = io.selfUpdate(installSpec) === true;
269
281
  return updateState.ok;
270
282
  },
271
283
  },
@@ -294,7 +306,7 @@ export async function cmdUpdate(argv, overrides = {}) {
294
306
  console.error(`impel update: npm reported success but the running CLI still resolves v${postInstall ?? "?"} (expected v${remote}).`);
295
307
  console.error(` Running CLI: ${CLI_BIN}`);
296
308
  console.error(" This usually means the `impel` on PATH belongs to a different npm prefix than `npm prefix -g`.");
297
- console.error(" Fix: run `npm prefix -g`, confirm it owns the `impel` shim on PATH, then `npm install --global impel-cli@latest` there.");
309
+ console.error(` Fix: run \`npm prefix -g\`, confirm it owns the \`impel\` shim on PATH, then \`npm install --global ${installSpec}\` there.`);
298
310
  process.exitCode = 1;
299
311
  return;
300
312
  }