impel-cli 0.18.7 → 0.18.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.7",
3
+ "version": "0.18.8",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@ import os from "node:os";
10
10
  import path from "node:path";
11
11
 
12
12
  import { IMPEL_MANAGED_MCP_ENV, impelMcpInvocation } from "./selfInvocation.js";
13
+ import { RUNTIME_BRAND } from "./runtimeBrand.js";
13
14
  import {
14
15
  applyImpelClaudeSandbox,
15
16
  captureClaudeSandbox,
@@ -20,9 +21,13 @@ export const CLAUDE_DIR = path.join(os.homedir(), ".claude");
20
21
  export const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
21
22
  export const CLAUDE_USER_CONFIG_PATH = path.join(os.homedir(), ".claude.json");
22
23
 
23
- export const IMPEL_API_KEY_HELPER = "impel token";
24
- export const IMPEL_MCP_SERVER_NAME = "impel";
25
- const LEGACY_IMPEL_MCP_SERVER = { type: "stdio", command: "impel", args: ["mcp"] };
24
+ export const IMPEL_API_KEY_HELPER = `${RUNTIME_BRAND.cli.command} token`;
25
+ export const IMPEL_MCP_SERVER_NAME = RUNTIME_BRAND.cli.providerId;
26
+ const LEGACY_IMPEL_MCP_SERVER = {
27
+ type: "stdio",
28
+ command: RUNTIME_BRAND.cli.command,
29
+ args: ["mcp"],
30
+ };
26
31
 
27
32
  function impelMcpServer() {
28
33
  return impelMcpInvocation();
package/src/codexSetup.js CHANGED
@@ -32,11 +32,12 @@ import os from "node:os";
32
32
  import path from "node:path";
33
33
 
34
34
  import { impelCliInvocation } from "./selfInvocation.js";
35
+ import { RUNTIME_BRAND } from "./runtimeBrand.js";
35
36
 
36
37
  export const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
37
38
  export const CODEX_CONFIG_PATH = path.join(CODEX_HOME, "config.toml");
38
39
 
39
- export const PROVIDER_ID = "impel";
40
+ export const PROVIDER_ID = RUNTIME_BRAND.cli.providerId;
40
41
 
41
42
  // Genuine Codex CLI traffic has its own byte-preserving compatibility route.
42
43
  // With wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this
@@ -45,8 +46,13 @@ export const PROVIDER_ID = "impel";
45
46
  export const CODEX_CLI_BASE_PATH = "/chatgpt_passthrough/backend-api/codex";
46
47
  export const impelCodexBaseUrl = (gatewayUrl) => `${gatewayUrl}${CODEX_CLI_BASE_PATH}`;
47
48
 
48
- const START_MARK = `# >>> impel-cli managed block (model_providers.${PROVIDER_ID}) >>>`;
49
- const END_MARK = `# <<< impel-cli managed block <<<`;
49
+ const START_MARK = `# >>> ${RUNTIME_BRAND.cli.managedMarker} managed block (model_providers.${PROVIDER_ID}) >>>`;
50
+ const END_MARK = `# <<< ${RUNTIME_BRAND.cli.managedMarker} managed block <<<`;
51
+ const escapedProviderId = PROVIDER_ID.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
52
+ const FOREIGN_PROVIDER_TABLE_RE = new RegExp(
53
+ `^(\\[model_providers\\.${escapedProviderId}(\\.|\\])|\\[mcp_servers\\.${escapedProviderId}\\])`,
54
+ "m",
55
+ );
50
56
 
51
57
  const PROVIDER_LINE_RE = /^model_provider[ \t]*=[ \t]*"([^"]*)"[ \t]*$/m;
52
58
  const NETWORK_TABLE = "sandbox_workspace_write";
@@ -65,12 +71,12 @@ function providerTablesBlock(
65
71
  const mcp = impelCliInvocation(["mcp"]);
66
72
  return [
67
73
  START_MARK,
68
- "# Generated by `impel use gateway codex`. Safe to re-run; do not hand-edit",
74
+ `# Generated by \`${RUNTIME_BRAND.cli.command} use gateway codex\`. Safe to re-run; do not hand-edit`,
69
75
  "# the lines between the markers above/below, they'll be overwritten.",
70
76
  ...(includeSandboxMode ? ['sandbox_mode = "workspace-write"'] : []),
71
77
  ...(includeNetworkAccess ? [`${NETWORK_TABLE}.${NETWORK_KEY} = true`, ""] : []),
72
78
  `[model_providers.${PROVIDER_ID}]`,
73
- `name = "Impel Gateway"`,
79
+ `name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
74
80
  `base_url = "${baseUrl}"`,
75
81
  `wire_api = "responses"`,
76
82
  "",
@@ -234,9 +240,7 @@ function stripManagedBlock(text) {
234
240
 
235
241
  /** True if a `[model_providers.impel` table exists outside of our own managed markers. */
236
242
  function hasForeignImpelTable(textWithoutManagedBlock) {
237
- return /^(\[model_providers\.impel(\.|\])|\[mcp_servers\.impel\])/m.test(
238
- textWithoutManagedBlock
239
- );
243
+ return FOREIGN_PROVIDER_TABLE_RE.test(textWithoutManagedBlock);
240
244
  }
241
245
 
242
246
  /** Reads the current root-level `model_provider` value, or null if unset. */
@@ -12,6 +12,7 @@ import {
12
12
  normalizeTenantId,
13
13
  productAccessLabel,
14
14
  } from "../tenants.js";
15
+ import { brandedEnvironmentName } from "../runtimeBrand.js";
15
16
 
16
17
  const HELP = `impel doctor - run synthetic, billable end-to-end gateway checks
17
18
 
@@ -159,7 +160,10 @@ export async function cmdDoctor(argv) {
159
160
  const gatewayUrl = doctorGatewayUrl(
160
161
  flags.gateway !== undefined
161
162
  ? flags.gateway
162
- : process.env.IMPEL_GATEWAY_URL || config.gatewayUrl || resolveDefaultGateway(),
163
+ : process.env[brandedEnvironmentName("GATEWAY_URL")]
164
+ || process.env.IMPEL_GATEWAY_URL
165
+ || config.gatewayUrl
166
+ || resolveDefaultGateway(),
163
167
  );
164
168
  const doctorConfig = { ...config, gatewayUrl };
165
169
  const listing = await fetchTenants(config);
@@ -18,16 +18,18 @@ import { promptText } from "../prompt.js";
18
18
  import { appProcessPattern } from "../apps.js";
19
19
  import { windowsStableEntrypointRoot } from "../selfInvocation.js";
20
20
  import { windowsClaudeUserData } from "../windowsApps.js";
21
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "../runtimeBrand.js";
21
22
 
22
23
  // Launcher artifacts the CLI may have written into ~/Applications: the
23
24
  // tenant-scoped bundles, the pre-v0.8 global bundles, and any interrupted
24
25
  // `.tmp-<pid>` staging or `.previous-<pid>` rotation directories they left
25
26
  // behind. Anchored so unrelated apps that merely start with "Impel" survive.
26
- const MANAGED_LAUNCHER_RE = /^Impel (Claude|ChatGPT)(\.app| \()/u;
27
+ const escapedDisplayPrefix = RUNTIME_BRAND.apps.displayPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
28
+ const MANAGED_LAUNCHER_RE = new RegExp(`^${escapedDisplayPrefix} (Claude|ChatGPT)(\\.app| \\()`, "u");
27
29
 
28
30
  // Every Impel-managed bundle identifier starts with this prefix; the vendor
29
31
  // apps use com.anthropic.* / com.openai.* and are never matched.
30
- const MANAGED_BUNDLE_PREFIX = "com.useimpel.";
32
+ const MANAGED_BUNDLE_PREFIX = `${RUNTIME_BRAND.apps.bundleIdentifierPrefix}.`;
31
33
 
32
34
  // macOS per-app state locations that accumulate entries keyed by bundle id.
33
35
  const MAC_LIBRARY_LOCATIONS = [
@@ -79,9 +81,9 @@ function keychainCandidates(appsRoot) {
79
81
  // Cover both app-name eras: the current UA-safe "Impel [tenant] Claude"
80
82
  // and the pre-0.17.9 "Impel Claude [tenant]" that may have left an orphaned
81
83
  // Keychain item behind.
82
- names.add(`Impel [${tenantId}] Claude Safe Storage`);
83
- names.add(`Impel Claude [${tenantId}] Safe Storage`);
84
- names.add(`Impel ChatGPT [${tenantId}] Safe Storage`);
84
+ names.add(`${RUNTIME_BRAND.apps.displayPrefix} [${tenantId}] Claude Safe Storage`);
85
+ names.add(`${RUNTIME_BRAND.apps.displayPrefix} Claude [${tenantId}] Safe Storage`);
86
+ names.add(`${RUNTIME_BRAND.apps.displayPrefix} ChatGPT [${tenantId}] Safe Storage`);
85
87
  const metadataPath = path.join(tenantsRoot, tenantId, "claude", "safe-storage.json");
86
88
  try {
87
89
  const appName = JSON.parse(fs.readFileSync(metadataPath, "utf8"))?.appName;
@@ -166,7 +168,7 @@ export async function cmdNuke(argv = [], overrides = {}) {
166
168
  ...overrides,
167
169
  };
168
170
 
169
- const appsRoot = io.environment.IMPEL_APP_HOME || path.join(io.configDir, "apps");
171
+ const appsRoot = io.environment[brandedEnvironmentName("APP_HOME")] || path.join(io.configDir, "apps");
170
172
  const darwin = io.platform === "darwin";
171
173
  const launchers = darwin ? managedLauncherEntries(io.homeDir) : [];
172
174
  const libraryRemnants = darwin ? macLibraryRemnants(io.homeDir) : [];
@@ -222,7 +224,7 @@ export async function cmdNuke(argv = [], overrides = {}) {
222
224
  // Missing items exit non-zero; that just means there is nothing to remove.
223
225
  io.run("/usr/bin/security", ["delete-generic-password", "-s", item]);
224
226
  }
225
- if (keychainItems.length) io.log(`Cleared ${keychainItems.length} Impel Safe Storage Keychain item${keychainItems.length === 1 ? "" : "s"}.`);
227
+ if (keychainItems.length) io.log(`Cleared ${keychainItems.length} ${RUNTIME_BRAND.product.displayName} Safe Storage Keychain item${keychainItems.length === 1 ? "" : "s"}.`);
226
228
 
227
229
  for (const remnant of windowsRemnants) {
228
230
  fs.rmSync(remnant, { recursive: true, force: true });
@@ -16,13 +16,20 @@ import {
16
16
  import { loadConfig, redactSecretText } from "../config.js";
17
17
  import { impelCliInvocation } from "../selfInvocation.js";
18
18
  import { spawnDetachedAppRefresh } from "../updates.js";
19
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "../runtimeBrand.js";
20
+
21
+ const MANAGED_SESSION_HOOK_FLAG = `${RUNTIME_BRAND.cli.command}-managed-session-hook-v1`;
22
+
23
+ function environmentValue(suffix) {
24
+ return process.env[brandedEnvironmentName(suffix)] ?? process.env[`IMPEL_${suffix}`];
25
+ }
19
26
 
20
27
  const SPEC = {
21
28
  provider: { type: "string" },
22
29
  surface: { type: "string" },
23
30
  tenant: { type: "string" },
24
31
  session: { type: "string" },
25
- "impel-managed-session-hook-v1": { type: "boolean" },
32
+ [MANAGED_SESSION_HOOK_FLAG]: { type: "boolean" },
26
33
  };
27
34
 
28
35
  /**
@@ -60,12 +67,12 @@ function startDetachedFlush({ provider, tenant, session }) {
60
67
  tenant,
61
68
  "--session",
62
69
  session,
63
- "--impel-managed-session-hook-v1",
70
+ `--${MANAGED_SESSION_HOOK_FLAG}`,
64
71
  ]);
65
72
  const child = spawn(invocation.command, invocation.args, {
66
73
  detached: true,
67
74
  stdio: "ignore",
68
- env: { ...process.env, IMPEL_SESSIONS_FLUSH_CHILD: "1" },
75
+ env: { ...process.env, [brandedEnvironmentName("SESSIONS_FLUSH_CHILD")]: "1" },
69
76
  windowsHide: true,
70
77
  });
71
78
  child.once("error", () => {});
@@ -94,7 +101,7 @@ export async function cmdSessions(argv) {
94
101
  flush: false,
95
102
  });
96
103
  if (flags.provider === "codex") maybeRepairManagedCodexApp(flags.tenant);
97
- if (config && (config.tenantId === flags.tenant || process.env.IMPEL_SESSIONS_DEV_ORG_ID)) {
104
+ if (config && (config.tenantId === flags.tenant || environmentValue("SESSIONS_DEV_ORG_ID"))) {
98
105
  // Hooks fire on every session event; only spawn a flush child when no
99
106
  // live one is already polling this session's outbox (heartbeat lock).
100
107
  const lock = sessionFlushLockPath({
@@ -114,7 +121,7 @@ export async function cmdSessions(argv) {
114
121
  } catch (error) {
115
122
  // Session persistence is observational. A collector outage must never
116
123
  // block a model turn or change a provider hook's decision semantics.
117
- if (process.env.IMPEL_SESSIONS_DEBUG === "1") {
124
+ if (environmentValue("SESSIONS_DEBUG") === "1") {
118
125
  console.error(`impel sessions hook: ${redactSecretText(error?.message || error)}`);
119
126
  }
120
127
  }
@@ -130,7 +137,7 @@ export async function cmdSessions(argv) {
130
137
  if (action === "flush") {
131
138
  if (!flags.provider || !flags.session || !flags.tenant) return;
132
139
  if (!["claude_code", "codex"].includes(flags.provider)) return;
133
- if (!config || flags.tenant !== config.tenantId && !process.env.IMPEL_SESSIONS_DEV_ORG_ID) return;
140
+ if (!config || flags.tenant !== config.tenantId && !environmentValue("SESSIONS_DEV_ORG_ID")) return;
134
141
  const lock = sessionFlushLockPath({
135
142
  tenantId: flags.tenant,
136
143
  provider: flags.provider,
@@ -2,14 +2,16 @@
2
2
  // reconcile every tenant and managed surface from the live control-plane list.
3
3
 
4
4
  import fs from "node:fs";
5
+ import path from "node:path";
5
6
  import { spawnSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
7
 
8
8
  import { parseFlags } from "../args.js";
9
9
  import { loadConfig, redactSecretText } from "../config.js";
10
10
  import { nativeCommandInvocation } from "../nativeProcess.js";
11
11
  import { withProgress } from "../progress.js";
12
12
  import { runInstallRecovery } from "../installRecovery/engine.js";
13
+ import { brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
14
+ import { IMPEL_CLI_ENTRYPOINT } from "../selfInvocation.js";
13
15
  import {
14
16
  fetchRemoteVersion,
15
17
  installedVersion,
@@ -19,7 +21,7 @@ import {
19
21
  writeUpdateCache,
20
22
  } from "../updates.js";
21
23
 
22
- const CLI_BIN = fileURLToPath(new URL("../../bin/impel.js", import.meta.url));
24
+ const CLI_BIN = IMPEL_CLI_ENTRYPOINT;
23
25
 
24
26
  /**
25
27
  * The version now on disk at the package that owns CLI_BIN, read FRESH (never
@@ -28,7 +30,7 @@ const CLI_BIN = fileURLToPath(new URL("../../bin/impel.js", import.meta.url));
28
30
  */
29
31
  export function postInstallCliVersion() {
30
32
  try {
31
- const packagePath = fileURLToPath(new URL("../../package.json", import.meta.url));
33
+ const packagePath = path.resolve(path.dirname(CLI_BIN), "..", "package.json");
32
34
  const version = JSON.parse(fs.readFileSync(packagePath, "utf8"))?.version;
33
35
  return typeof version === "string" && version.trim() ? version.trim() : null;
34
36
  } catch {
@@ -36,7 +38,7 @@ export function postInstallCliVersion() {
36
38
  }
37
39
  }
38
40
 
39
- const HELP = `impel update - update everything Impel in one command
41
+ const HELP = brandedText(`impel update - update everything Impel in one command
40
42
 
41
43
  Reinstalls impel-cli from npm, then uses the new build to discover every
42
44
  accessible tenant. Missing tenants are installed; existing tenant profiles and
@@ -48,7 +50,7 @@ Usage:
48
50
  impel update --skip-apps Reconcile only isolated CLI profiles
49
51
  impel update --skip-clis Do not install missing vendor CLIs
50
52
  impel update --no-recovery Disable local and hosted recovery for this run
51
- `;
53
+ `);
52
54
 
53
55
  function reportProcessFailure(stage, result) {
54
56
  if (result?.error) {
@@ -237,14 +239,14 @@ export async function cmdUpdate(argv, overrides = {}) {
237
239
  platform: io.platform,
238
240
  architecture: process.arch,
239
241
  step: "install.impel_cli",
240
- command: "npm install --global impel-cli@latest",
241
- message: "The npm-verified global impel-cli update failed.",
242
+ command: `npm install --global ${RUNTIME_BRAND.cli.packageName}@latest`,
243
+ message: `The npm-verified global ${RUNTIME_BRAND.cli.packageName} update failed.`,
242
244
  },
243
245
  config,
244
246
  goals: [
245
247
  {
246
248
  id: "cli-update",
247
- description: "The global impel-cli npm update completed successfully",
249
+ description: `The global ${RUNTIME_BRAND.cli.packageName} npm update completed successfully`,
248
250
  run: () => updateState.ok === true,
249
251
  },
250
252
  ],
@@ -3,7 +3,25 @@ import path from "node:path";
3
3
  import { main as upstreamMain } from "../cli.js";
4
4
  import { brandedText, RUNTIME_BRAND, validateRuntimeBrand } from "../runtimeBrand.js";
5
5
 
6
- const ALIASES = Object.freeze({ apps: "app", pats: "pat" });
6
+ const ALIASES = Object.freeze({
7
+ apps: "app",
8
+ pats: "pat",
9
+ task: "tasks",
10
+ tickets: "tasks",
11
+ ticket: "tasks",
12
+ tenants: "tenant",
13
+ org: "tenant",
14
+ skill: "skills",
15
+ agent: "agents",
16
+ upgrade: "update",
17
+ on: "use",
18
+ off: "use",
19
+ });
20
+
21
+ const HIDDEN_COMMAND_CAPABILITIES = Object.freeze({
22
+ _converge: "update",
23
+ "_app-launch": "app",
24
+ });
7
25
 
8
26
  function help(version) {
9
27
  const command = RUNTIME_BRAND.cli.command;
@@ -13,10 +31,20 @@ function help(version) {
13
31
  if (enabled.has("setup")) lines.push(` ${command} setup Configure and prepare ${product} CLI/app profiles`);
14
32
  if (enabled.has("auth")) lines.push(` ${command} auth Store an existing ${product} PAT`);
15
33
  if (enabled.has("pat")) lines.push(` ${command} pat create|revoke Mint or revoke a ${product} PAT`);
34
+ if (enabled.has("tasks")) lines.push(` ${command} tasks list|get|create|... Work with ${product} tickets`);
35
+ if (enabled.has("tenant")) lines.push(` ${command} tenant list|current|use Inspect or select an organization`);
16
36
  if (enabled.has("app")) lines.push(` ${command} app install|update|open ... Manage isolated desktop apps`);
17
37
  if (enabled.has("claude")) lines.push(` ${command} claude [args...] Launch isolated Claude Code`);
18
38
  if (enabled.has("codex")) lines.push(` ${command} codex [args...] Launch isolated Codex`);
39
+ if (enabled.has("mcp")) lines.push(` ${command} mcp Run the authenticated MCP transport`);
40
+ if (enabled.has("sessions")) lines.push(` ${command} sessions ... Run managed session lifecycle hooks`);
41
+ if (enabled.has("skills")) lines.push(` ${command} skills sync [...] Sync gateway skills into managed clients`);
42
+ if (enabled.has("agents")) lines.push(` ${command} agents sync [...] Sync tenant agents into managed clients`);
19
43
  if (enabled.has("status")) lines.push(` ${command} status Show local readiness`);
44
+ if (enabled.has("doctor")) lines.push(` ${command} doctor [...] Run gateway and provider diagnostics`);
45
+ if (enabled.has("update")) lines.push(` ${command} update [...] Update the CLI and reconcile tenants`);
46
+ if (enabled.has("nuke")) lines.push(` ${command} nuke [--yes] Erase all ${product}-managed local state`);
47
+ if (enabled.has("experimental")) lines.push(` ${command} experimental ... Manage gated experimental features`);
20
48
  lines.push("", `Version: ${version}`);
21
49
  return `${lines.join("\n")}\n`;
22
50
  }
@@ -69,8 +97,8 @@ export function createImpelCliExtension({ brand, entrypoint, version }) {
69
97
  return;
70
98
  }
71
99
  const command = ALIASES[rawCommand] || rawCommand;
72
- const hiddenAppCommand = rawCommand === "_app-launch";
73
- if ((!hiddenAppCommand && !allowed.has(command)) || (hiddenAppCommand && !allowed.has("app"))) {
100
+ const requiredCapability = HIDDEN_COMMAND_CAPABILITIES[rawCommand] || command;
101
+ if (!allowed.has(requiredCapability)) {
74
102
  process.stderr.write(`${RUNTIME_BRAND.cli.command}: command ${JSON.stringify(rawCommand)} is not available\n`);
75
103
  process.exitCode = 1;
76
104
  return;
@@ -8,7 +8,11 @@ const SAFE_TENANT = /^[A-Za-z0-9_.-]{1,128}$/u;
8
8
  const SAFE_PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]{0,62}\/)?[a-z0-9][a-z0-9._-]{0,126}$/u;
9
9
  const SAFE_BUNDLE_PREFIX = /^[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)+$/u;
10
10
  const CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/u;
11
- const SUPPORTED_COMMANDS = new Set(["setup", "auth", "pat", "app", "claude", "codex", "status", "token"]);
11
+ const SUPPORTED_COMMANDS = new Set([
12
+ "setup", "auth", "pat", "token", "mcp", "sessions", "claude", "codex",
13
+ "status", "doctor", "tasks", "tenant", "app", "nuke", "skills", "agents",
14
+ "update", "use", "experimental",
15
+ ]);
12
16
 
13
17
  const DEFAULT = Object.freeze({
14
18
  schemaVersion: 1,
@@ -24,7 +28,9 @@ const DEFAULT = Object.freeze({
24
28
  auth: Object.freeze({ patPrefix: "impel_pat_", tenantPrefix: "impel_tenant_" }),
25
29
  tenant: Object.freeze({ defaultId: null, displayName: null }),
26
30
  gateway: Object.freeze({ defaultOrigin: "https://gateway.useimpel.com" }),
31
+ sessions: Object.freeze({ defaultOrigin: "https://sessions.useimpel.com" }),
27
32
  controlPlane: Object.freeze({ defaultOrigin: "https://www.useimpel.com" }),
33
+ updates: Object.freeze({ registry: null }),
28
34
  apps: Object.freeze({
29
35
  displayPrefix: "Impel",
30
36
  bundleIdentifierPrefix: "com.useimpel",
@@ -51,6 +57,15 @@ function origin(name, value) {
51
57
  return parsed.toString().replace(/\/$/u, "");
52
58
  }
53
59
 
60
+ function registryUrl(name, value) {
61
+ const parsed = new URL(text(name, value));
62
+ const local = parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
63
+ if ((parsed.protocol !== "https:" && !local) || parsed.username || parsed.password || parsed.search || parsed.hash) {
64
+ throw new Error(`impel-cli runtime brand ${name} must be an HTTPS registry URL`);
65
+ }
66
+ return parsed.toString().replace(/\/+$/u, "");
67
+ }
68
+
54
69
  function pathSegment(name, value) {
55
70
  const result = text(name, value);
56
71
  if (result === "." || result === ".." || /[\\/:]/u.test(result)) {
@@ -98,7 +113,18 @@ export function validateRuntimeBrand(input) {
98
113
  displayName: input.tenant?.displayName == null ? defaultTenant : text("tenant.displayName", input.tenant.displayName),
99
114
  }),
100
115
  gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
116
+ sessions: Object.freeze({
117
+ defaultOrigin: origin(
118
+ "sessions.defaultOrigin",
119
+ input.sessions?.defaultOrigin || input.gateway?.defaultOrigin,
120
+ ),
121
+ }),
101
122
  controlPlane: Object.freeze({ defaultOrigin: origin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin) }),
123
+ updates: Object.freeze({
124
+ registry: input.updates?.registry == null
125
+ ? null
126
+ : registryUrl("updates.registry", input.updates.registry),
127
+ }),
102
128
  apps: Object.freeze({
103
129
  displayPrefix: pathSegment("apps.displayPrefix", input.apps?.displayPrefix || input.product?.displayName),
104
130
  bundleIdentifierPrefix: text("apps.bundleIdentifierPrefix", input.apps?.bundleIdentifierPrefix, SAFE_BUNDLE_PREFIX),
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
 
4
4
  import { environmentValue } from "./nativeProcess.js";
5
- import { RUNTIME_BRAND } from "./runtimeBrand.js";
5
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
6
6
 
7
7
  /** The running package's own bin script, used directly for live child spawns. */
8
8
  export const IMPEL_CLI_ENTRYPOINT = process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT
@@ -64,7 +64,7 @@ export function impelCliInvocation(args = [], options = {}) {
64
64
  };
65
65
  }
66
66
 
67
- export const IMPEL_MANAGED_MCP_ENV = "IMPEL_MANAGED_MCP";
67
+ export const IMPEL_MANAGED_MCP_ENV = brandedEnvironmentName("MANAGED_MCP");
68
68
 
69
69
  export function impelMcpInvocation(args = [], options = {}) {
70
70
  return {
@@ -6,8 +6,9 @@ import { spawnSync } from "node:child_process";
6
6
  import * as zlib from "node:zlib";
7
7
 
8
8
  import { CONFIG_DIR, loadConfig, redactCredentialText, redactSecretText } from "./config.js";
9
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
9
10
 
10
- export const DEFAULT_SESSIONS_URL = "https://sessions.useimpel.com";
11
+ export const DEFAULT_SESSIONS_URL = RUNTIME_BRAND.sessions.defaultOrigin;
11
12
  const MAX_HOOK_INPUT_BYTES = 2 * 1024 * 1024;
12
13
  const MAX_LEDGER_PAYLOAD_BYTES = 256 * 1024;
13
14
  const TRANSCRIPT_CHUNK_BYTES = 512 * 1024;
@@ -22,8 +23,14 @@ const DIRECT_UPLOAD_TIMEOUT_MS = 30_000;
22
23
  const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
23
24
  const TASK_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
24
25
 
26
+ function brandedEnvironmentValue(suffix) {
27
+ const branded = process.env[brandedEnvironmentName(suffix)];
28
+ if (branded !== undefined) return branded;
29
+ return process.env[`IMPEL_${suffix}`];
30
+ }
31
+
25
32
  function stateRoot() {
26
- return process.env.IMPEL_SESSIONS_STATE_DIR || path.join(CONFIG_DIR, "sessions");
33
+ return brandedEnvironmentValue("SESSIONS_STATE_DIR") || path.join(CONFIG_DIR, "sessions");
27
34
  }
28
35
 
29
36
  function sha256(value) {
@@ -195,7 +202,7 @@ function acquireLock(root, name, staleMs = 60_000) {
195
202
  }
196
203
 
197
204
  function providerRoots(provider) {
198
- const roots = [process.env.IMPEL_SESSION_TRANSCRIPT_ROOT];
205
+ const roots = [brandedEnvironmentValue("SESSION_TRANSCRIPT_ROOT")];
199
206
  if (provider === "claude_code") roots.push(process.env.CLAUDE_CONFIG_DIR, path.join(os.homedir(), ".claude"));
200
207
  else roots.push(process.env.CODEX_HOME, path.join(os.homedir(), ".codex"));
201
208
  return roots.filter(Boolean).map((candidate) => {
@@ -372,8 +379,8 @@ function enforceOutboxLimits(root) {
372
379
  return { dropped: 0, pending: pendingDirectories(root).length, bytes: null, deferred: true };
373
380
  }
374
381
  try {
375
- const maximumBytes = positiveIntegerEnvironment("IMPEL_SESSIONS_MAX_OUTBOX_BYTES", DEFAULT_MAX_OUTBOX_BYTES);
376
- const maximumBatches = positiveIntegerEnvironment("IMPEL_SESSIONS_MAX_OUTBOX_BATCHES", DEFAULT_MAX_OUTBOX_BATCHES);
382
+ const maximumBytes = positiveIntegerEnvironment(brandedEnvironmentName("SESSIONS_MAX_OUTBOX_BYTES"), DEFAULT_MAX_OUTBOX_BYTES);
383
+ const maximumBatches = positiveIntegerEnvironment(brandedEnvironmentName("SESSIONS_MAX_OUTBOX_BATCHES"), DEFAULT_MAX_OUTBOX_BATCHES);
377
384
  const directories = pendingDirectories(root);
378
385
  let bytes = directories.reduce((total, directory) => total + batchDirectoryBytes(directory), 0);
379
386
  let count = directories.length;
@@ -503,7 +510,11 @@ function repositoryMetadata(cwd) {
503
510
  }
504
511
 
505
512
  function sessionsUrl(config) {
506
- return String(process.env.IMPEL_SESSIONS_URL || config?.sessionsUrl || DEFAULT_SESSIONS_URL).trim().replace(/\/+$/u, "");
513
+ return String(
514
+ brandedEnvironmentValue("SESSIONS_URL")
515
+ || config?.sessionsUrl
516
+ || DEFAULT_SESSIONS_URL,
517
+ ).trim().replace(/\/+$/u, "");
507
518
  }
508
519
 
509
520
  function problemCode(body) {
@@ -516,16 +527,16 @@ function problemMessage(body) {
516
527
 
517
528
  function requestHeaders(config, tenantId, extra = {}) {
518
529
  const headers = { accept: "application/json", ...extra };
519
- const devOrg = process.env.IMPEL_SESSIONS_DEV_ORG_ID;
530
+ const devOrg = brandedEnvironmentValue("SESSIONS_DEV_ORG_ID");
520
531
  if (devOrg) {
521
532
  headers["x-impel-dev-org-id"] = devOrg;
522
- headers["x-impel-dev-user-id"] = process.env.IMPEL_SESSIONS_DEV_USER_ID || "impel-cli-smoke";
533
+ headers["x-impel-dev-user-id"] = brandedEnvironmentValue("SESSIONS_DEV_USER_ID") || `${RUNTIME_BRAND.cli.command}-cli-smoke`;
523
534
  } else {
524
535
  if (!config?.pat) throw new Error("Impel authentication is unavailable");
525
536
  headers.authorization = `Bearer ${config.pat}`;
526
537
  headers["x-impel-org-id"] = tenantId;
527
538
  }
528
- const bypass = process.env.IMPEL_SESSIONS_VERCEL_BYPASS_TOKEN;
539
+ const bypass = brandedEnvironmentValue("SESSIONS_VERCEL_BYPASS_TOKEN");
529
540
  if (bypass) headers["x-vercel-protection-bypass"] = bypass;
530
541
  return headers;
531
542
  }
@@ -558,7 +569,7 @@ async function apiRequest(config, tenantId, route, options = {}) {
558
569
  }
559
570
 
560
571
  function compressZstd(payload) {
561
- if (process.env.IMPEL_SESSIONS_DISABLE_DIRECT_UPLOAD === "1") return null;
572
+ if (brandedEnvironmentValue("SESSIONS_DISABLE_DIRECT_UPLOAD") === "1") return null;
562
573
  if (typeof zlib.zstdCompressSync !== "function") return null;
563
574
  const level = zlib.constants?.ZSTD_c_compressionLevel;
564
575
  const windowLog = zlib.constants?.ZSTD_c_windowLog;
@@ -938,8 +949,9 @@ export async function collectSessionHook({ provider, surface, tenantId, input, c
938
949
  if (!SAFE_ID_RE.test(surface)) throw new Error("Invalid session surface");
939
950
  const sessionKey = String(input?.session_id || "").trim();
940
951
  if (!sessionKey || sessionKey.length > 512 || /[\r\n\0]/u.test(sessionKey)) throw new Error("Hook input has no valid session_id");
941
- const taskId = String(process.env.IMPEL_TASK_ID || "").trim();
942
- if (taskId && !TASK_ID_RE.test(taskId)) throw new Error("IMPEL_TASK_ID is invalid");
952
+ const taskIdEnvironment = brandedEnvironmentName("TASK_ID");
953
+ const taskId = String(brandedEnvironmentValue("TASK_ID") || "").trim();
954
+ if (taskId && !TASK_ID_RE.test(taskId)) throw new Error(`${taskIdEnvironment} is invalid`);
943
955
  const sessionMeta = {
944
956
  provider,
945
957
  surface,
@@ -960,7 +972,7 @@ export async function collectSessionHook({ provider, surface, tenantId, input, c
960
972
  }
961
973
  }
962
974
  const limits = enforceOutboxLimits(root);
963
- const selectedTenant = process.env.IMPEL_SESSIONS_DEV_ORG_ID || config?.tenantId === tenantId;
975
+ const selectedTenant = brandedEnvironmentValue("SESSIONS_DEV_ORG_ID") || config?.tenantId === tenantId;
964
976
  if (flush && config && selectedTenant) return flushCollectedSession({ tenantId, provider, sessionKey, config });
965
977
  return {
966
978
  flushed: 0,
@@ -3,10 +3,11 @@ import crypto from "node:crypto";
3
3
  import path from "node:path";
4
4
 
5
5
  import { impelCliInvocation } from "./selfInvocation.js";
6
+ import { RUNTIME_BRAND } from "./runtimeBrand.js";
6
7
 
7
- const MANAGED_MARKER = "--impel-managed-session-hook-v1";
8
- const CODEX_TRUST_START = "# >>> impel managed session hook trust >>>";
9
- const CODEX_TRUST_END = "# <<< impel managed session hook trust <<<";
8
+ const MANAGED_MARKER = `--${RUNTIME_BRAND.cli.command}-managed-session-hook-v1`;
9
+ const CODEX_TRUST_START = `# >>> ${RUNTIME_BRAND.product.id} managed session hook trust >>>`;
10
+ const CODEX_TRUST_END = `# <<< ${RUNTIME_BRAND.product.id} managed session hook trust <<<`;
10
11
 
11
12
  export const CLAUDE_SESSION_EVENTS = Object.freeze([
12
13
  "SessionStart",
package/src/updates.js CHANGED
@@ -9,26 +9,31 @@
9
9
 
10
10
  import fs from "node:fs";
11
11
  import path from "node:path";
12
- import { spawn } from "node:child_process";
13
- import { fileURLToPath } from "node:url";
12
+ import { spawn, spawnSync } from "node:child_process";
14
13
 
15
14
  import { CONFIG_DIR } from "./config.js";
15
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
16
+ import { IMPEL_CLI_ENTRYPOINT } from "./selfInvocation.js";
17
+ import { nativeCommandInvocation } from "./nativeProcess.js";
16
18
  import { renameWithWindowsRetry } from "./windowsFs.js";
17
19
 
18
20
  export const UPDATE_CACHE_PATH = path.join(CONFIG_DIR, "update-check.json");
19
21
  export const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
20
22
 
21
- const CLI_ROOT = fileURLToPath(new URL("..", import.meta.url));
22
- const DEFAULT_UPDATE_PACKAGE = "impel-cli";
23
+ const CLI_ROOT = path.resolve(path.dirname(IMPEL_CLI_ENTRYPOINT), "..");
23
24
  const DEFAULT_UPDATE_REGISTRY = "https://registry.npmjs.org";
24
25
  const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
25
26
 
26
27
  export function updatePackage() {
27
- return process.env.IMPEL_UPDATE_PACKAGE || DEFAULT_UPDATE_PACKAGE;
28
+ return process.env[brandedEnvironmentName("UPDATE_PACKAGE")] || RUNTIME_BRAND.cli.packageName;
28
29
  }
29
30
 
30
31
  export function updateRegistry() {
31
- return String(process.env.IMPEL_UPDATE_REGISTRY || DEFAULT_UPDATE_REGISTRY).replace(/\/+$/u, "");
32
+ return String(
33
+ process.env[brandedEnvironmentName("UPDATE_REGISTRY")]
34
+ || RUNTIME_BRAND.updates.registry
35
+ || DEFAULT_UPDATE_REGISTRY,
36
+ ).replace(/\/+$/u, "");
32
37
  }
33
38
 
34
39
  export function updateInstallSpec() {
@@ -80,18 +85,65 @@ export function isNewerVersion(candidate, current) {
80
85
  return false;
81
86
  }
82
87
 
83
- /** Read the latest published version from npm's registry metadata. */
88
+ function fetchRemoteVersionWithNpm({
89
+ packageName,
90
+ registry,
91
+ timeoutMs,
92
+ spawnSyncImpl = spawnSync,
93
+ environment = process.env,
94
+ platform = process.platform,
95
+ }) {
96
+ try {
97
+ const invocation = nativeCommandInvocation(
98
+ "npm",
99
+ ["view", packageName, "version", "--json", "--registry", registry],
100
+ environment,
101
+ platform,
102
+ );
103
+ const result = spawnSyncImpl(invocation.command, invocation.args, {
104
+ encoding: "utf8",
105
+ env: environment,
106
+ timeout: timeoutMs,
107
+ windowsHide: true,
108
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
109
+ });
110
+ if (result.status !== 0 || result.error) return null;
111
+ const raw = String(result.stdout || "").trim();
112
+ let version;
113
+ try { version = JSON.parse(raw); }
114
+ catch { version = raw; }
115
+ return validVersion(version) ? version : null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /** Read the latest published version from npm registry metadata. */
84
122
  export async function fetchRemoteVersion({
85
123
  packageName = updatePackage(),
86
124
  registry = updateRegistry(),
87
125
  timeoutMs = 10_000,
88
- fetchImpl = globalThis.fetch,
126
+ fetchImpl = null,
127
+ spawnSyncImpl = spawnSync,
128
+ environment = process.env,
129
+ platform = process.platform,
89
130
  } = {}) {
131
+ const normalizedRegistry = String(registry).replace(/\/+$/u, "");
132
+ if (!fetchImpl && normalizedRegistry !== DEFAULT_UPDATE_REGISTRY) {
133
+ return fetchRemoteVersionWithNpm({
134
+ packageName,
135
+ registry: normalizedRegistry,
136
+ timeoutMs,
137
+ spawnSyncImpl,
138
+ environment,
139
+ platform,
140
+ });
141
+ }
90
142
  const controller = new AbortController();
91
143
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
92
144
  try {
93
145
  const encodedName = encodeURIComponent(packageName);
94
- const response = await fetchImpl(`${String(registry).replace(/\/+$/u, "")}/${encodedName}/latest`, {
146
+ const response = await (fetchImpl || globalThis.fetch)(`${normalizedRegistry}/${encodedName}/latest`, {
95
147
  headers: { accept: "application/json" },
96
148
  signal: controller.signal,
97
149
  });
@@ -162,7 +214,7 @@ export async function refreshUpdateCache(dependencies = {}) {
162
214
  export function updateNoticeLine({ cache = readUpdateCache(), current = installedVersion() } = {}) {
163
215
  const remote = cache?.remoteVersion;
164
216
  if (!isNewerVersion(remote, current)) return null;
165
- return `impel-cli update available (v${current} → v${remote}): run \`impel update\``;
217
+ return `${RUNTIME_BRAND.cli.packageName} update available (v${current} → v${remote}): run \`${RUNTIME_BRAND.cli.command} update\``;
166
218
  }
167
219
 
168
220
  /**
@@ -198,7 +250,7 @@ export function spawnDetachedAppRefresh(tenantId = null) {
198
250
 
199
251
  function spawnDetached(args) {
200
252
  try {
201
- const child = spawn(process.execPath, [path.join(CLI_ROOT, "bin", "impel.js"), ...args], {
253
+ const child = spawn(process.execPath, [IMPEL_CLI_ENTRYPOINT, ...args], {
202
254
  detached: true,
203
255
  stdio: "ignore",
204
256
  // Windows: a detached console-subsystem child gets its OWN console — a