impel-cli 0.18.4 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -309,9 +309,18 @@ normal help.
309
309
 
310
310
  ## Library export
311
311
 
312
- The package continues to expose `impel-cli/gateway` for approved branded
313
- vendor integrations. Its CLI allowlist is deliberately narrow and does not
314
- expose control-plane administration commands.
312
+ The package continues to expose `impel-cli/gateway` for approved legacy
313
+ gateway-only integrations. Its CLI allowlist is deliberately narrow and does
314
+ not expose control-plane administration commands.
315
+
316
+ `impel-cli/extension` is the canonical full-implementation extension API for
317
+ reviewed first-party brands. The adapter supplies a validated declarative
318
+ brand before import and receives an explicit command allowlist over the same
319
+ setup, PAT, managed-app, and Claude/Codex implementations used by `impel`.
320
+ Brand-specific config directories, credential prefixes, provider identifiers,
321
+ app identities, environment variables, and control-plane origins remain
322
+ isolated. Optional Sessions, MCP, skills, agents, and agent PATs are fail-closed
323
+ feature flags; an extension cannot add an upstream command family.
315
324
 
316
325
  ## Development
317
326
 
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "impel": "bin/impel.js"
8
8
  },
9
9
  "exports": {
10
- "./gateway": "./src/gateway/index.js"
10
+ "./gateway": "./src/gateway/index.js",
11
+ "./extension": "./src/extension/index.js"
11
12
  },
12
13
  "files": [
13
14
  "bin",
package/src/apps.js CHANGED
@@ -10,28 +10,29 @@ import {
10
10
  secureManagedCodexHome,
11
11
  } from "./codexSecurity.js";
12
12
  import { normalizeTenantId } from "./tenants.js";
13
- import { impelCliInvocation } from "./selfInvocation.js";
13
+ import { IMPEL_CLI_ENTRYPOINT, impelCliInvocation } from "./selfInvocation.js";
14
14
  import { crossAppModelsEnabled, redactSecretText } from "./config.js";
15
15
  import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
16
16
  import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
17
17
  import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
18
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
18
19
 
19
20
  export const CLAUDE_CONFIG_ID = "1ced0000-0000-4000-8000-000000000001";
20
- const CHATGPT_CONFIG_START = "# >>> impel app managed gateway >>>";
21
- const CHATGPT_CONFIG_END = "# <<< impel app managed gateway <<<";
21
+ const CHATGPT_CONFIG_START = `# >>> ${RUNTIME_BRAND.cli.command} app managed gateway >>>`;
22
+ const CHATGPT_CONFIG_END = `# <<< ${RUNTIME_BRAND.cli.command} app managed gateway <<<`;
22
23
  const CLI_VERSION = JSON.parse(
23
24
  fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"),
24
25
  ).version;
25
26
  const APP_DEFINITIONS = {
26
27
  claude: {
27
- label: "Impel Claude",
28
- bundleIdentifier: "com.useimpel.claude",
28
+ label: `${RUNTIME_BRAND.apps.displayPrefix} Claude`,
29
+ bundleIdentifier: `${RUNTIME_BRAND.apps.bundleIdentifierPrefix}.claude`,
29
30
  names: ["Claude.app"],
30
31
  executableNames: ["Claude"],
31
32
  },
32
33
  chatgpt: {
33
- label: "Impel ChatGPT",
34
- bundleIdentifier: "com.useimpel.chatgpt",
34
+ label: `${RUNTIME_BRAND.apps.displayPrefix} ChatGPT`,
35
+ bundleIdentifier: `${RUNTIME_BRAND.apps.bundleIdentifierPrefix}.chatgpt`,
35
36
  names: ["ChatGPT.app", "Codex.app"],
36
37
  executableNames: ["ChatGPT", "Codex"],
37
38
  },
@@ -145,14 +146,15 @@ export function claudeAppNameKeepsDesktopMode(appName) {
145
146
  /** Tenant-scoped Safe Storage / app name that still ends in "Claude". */
146
147
  function impelClaudeSafeStorageName(tenantId) {
147
148
  return tenantId
148
- ? `Impel [${normalizeTenantId(tenantId)}] Claude`
149
- : "Impel Claude";
149
+ ? `${RUNTIME_BRAND.apps.displayPrefix} [${normalizeTenantId(tenantId)}] Claude`
150
+ : `${RUNTIME_BRAND.apps.displayPrefix} Claude`;
150
151
  }
151
152
 
152
153
  // The pre-0.17.9 format put the tenant AFTER "Claude" ("Impel Claude [tenant]"),
153
154
  // which breaks the desktop-mode UA match. Recognize exactly the names this CLI
154
155
  // used to write so we can heal them without touching an unrelated custom name.
155
- const LEGACY_TRAPPED_CLAUDE_NAME = /^Impel Claude( \[[^\]]+\])?$/u;
156
+ const escapedDisplayPrefix = RUNTIME_BRAND.apps.displayPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
157
+ const LEGACY_TRAPPED_CLAUDE_NAME = new RegExp(`^${escapedDisplayPrefix} Claude( \\[[^\\]]+\\])?$`, "u");
156
158
 
157
159
  export const FALLBACK_MODELS = [
158
160
  { id: "claude-opus-4-8", provider: "claude", display_name: "Claude Opus 4.8", family: "opus", family_default: true, default: true, context_window: 200000 },
@@ -502,7 +504,8 @@ export function appPaths(homeDir = os.homedir(), tenantId = null, {
502
504
  claudeUserData = null,
503
505
  tenantName = null,
504
506
  } = {}) {
505
- const root = process.env.IMPEL_APP_HOME || path.join(homeDir, ".config", "impel", "apps");
507
+ const root = process.env[brandedEnvironmentName("APP_HOME")]
508
+ || path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "apps");
506
509
  const tenantRoot = tenantId
507
510
  ? path.join(root, "tenants", normalizeTenantId(tenantId))
508
511
  : root;
@@ -765,7 +768,7 @@ export function installManagedAppFiles({
765
768
  writeClaudeNative3PSelection(paths);
766
769
  writeClaudeCodeSettings(paths);
767
770
  writeClaudeConfig(paths, config, models);
768
- ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
771
+ if (RUNTIME_BRAND.features.sessions) ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
769
772
  }
770
773
  else {
771
774
  writeChatGPTConfig(
@@ -776,7 +779,7 @@ export function installManagedAppFiles({
776
779
  chatgptInvocations,
777
780
  generatedAt,
778
781
  );
779
- ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
782
+ if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
780
783
  }
781
784
  installed.push({
782
785
  target,
@@ -808,7 +811,7 @@ export function installManagedAppFiles({
808
811
  function vendorSearchRoots(homeDir, target) {
809
812
  const roots = ["/Applications", path.join(homeDir, "Applications")];
810
813
  const pin = PINNED_VENDOR_APPS[target];
811
- if (pin) roots.push(path.join(homeDir, ".config", "impel", "vendor", target, pin.version));
814
+ if (pin) roots.push(path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version));
812
815
  return roots;
813
816
  }
814
817
 
@@ -938,7 +941,7 @@ function installPinnedVendorApp(target, homeDir) {
938
941
  }
939
942
 
940
943
  const destination = path.join(
941
- homeDir, ".config", "impel", "vendor", target, pin.version, pin.bundleName,
944
+ homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
942
945
  );
943
946
  fs.mkdirSync(path.dirname(destination), { recursive: true });
944
947
  sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
@@ -1054,7 +1057,7 @@ function writeClaudeConfig(paths, config, models) {
1054
1057
  writeAtomic(path.join(dir, `${CLAUDE_CONFIG_ID}.json`), JSON.stringify(body, null, 2) + "\n", 0o600);
1055
1058
  writeAtomic(path.join(dir, "_meta.json"), JSON.stringify({
1056
1059
  appliedId: CLAUDE_CONFIG_ID,
1057
- entries: [{ id: CLAUDE_CONFIG_ID, name: "Impel Gateway" }],
1060
+ entries: [{ id: CLAUDE_CONFIG_ID, name: `${RUNTIME_BRAND.product.displayName} Gateway` }],
1058
1061
  }, null, 2) + "\n", 0o600);
1059
1062
  }
1060
1063
 
@@ -1139,7 +1142,7 @@ function writeChatGPTConfig(
1139
1142
  : (configuredTier === "fast" && supportedTiers.includes("priority") ? "priority" : null);
1140
1143
  const catalog = {
1141
1144
  fetched_at: generatedAt,
1142
- client_version: "impel-managed-2",
1145
+ client_version: `${RUNTIME_BRAND.cli.command}-managed-2`,
1143
1146
  models: codexModels,
1144
1147
  };
1145
1148
  writeAtomic(paths.chatgpt.catalog, JSON.stringify(catalog, null, 2) + "\n", 0o600);
@@ -1160,10 +1163,12 @@ function writeChatGPTConfig(
1160
1163
 
1161
1164
  const managedToml = [
1162
1165
  CHATGPT_CONFIG_START,
1163
- "# Managed by `impel app update`. This profile is isolated from ~/.codex.",
1164
- "# Impel session-hook trust is pinned separately by exact handler hash.",
1166
+ `# Managed by \`${RUNTIME_BRAND.cli.command} app update\`. This profile is isolated from ~/.codex.`,
1167
+ ...(RUNTIME_BRAND.features.sessions
1168
+ ? [`# ${RUNTIME_BRAND.product.displayName} session-hook trust is pinned separately by exact handler hash.`]
1169
+ : []),
1165
1170
  `model = ${tomlString(selectedModel.slug)}`,
1166
- 'model_provider = "impel"',
1171
+ `model_provider = ${tomlString(RUNTIME_BRAND.cli.providerId)}`,
1167
1172
  `chatgpt_base_url = ${tomlString(`${config.gatewayUrl}/chatgpt_passthrough/backend-api`)}`,
1168
1173
  `model_catalog_json = ${tomlString(paths.chatgpt.catalog)}`,
1169
1174
  ...(selectedEffort ? [`model_reasoning_effort = ${tomlString(selectedEffort)}`] : []),
@@ -1172,8 +1177,8 @@ function writeChatGPTConfig(
1172
1177
  // The built-in ChatGPT provider derives its inference endpoint from
1173
1178
  // chatgpt.com even when chatgpt_base_url points at the gateway. Keep the
1174
1179
  // native account client for status/usage, but route inference explicitly.
1175
- "[model_providers.impel]",
1176
- 'name = "Impel Gateway"',
1180
+ `[model_providers.${RUNTIME_BRAND.cli.providerId}]`,
1181
+ `name = ${tomlString(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
1177
1182
  `base_url = ${tomlString(experimental
1178
1183
  ? `${config.gatewayUrl}/experimental/openai/v1`
1179
1184
  : `${config.gatewayUrl}/chatgpt_passthrough/backend-api/codex`)}`,
@@ -1183,7 +1188,7 @@ function writeChatGPTConfig(
1183
1188
  "stream_max_retries = 2",
1184
1189
  "stream_idle_timeout_ms = 600000",
1185
1190
  "",
1186
- "[model_providers.impel.auth]",
1191
+ `[model_providers.${RUNTIME_BRAND.cli.providerId}.auth]`,
1187
1192
  `command = ${tomlString(auth.command)}`,
1188
1193
  ...(Array.isArray(auth.args)
1189
1194
  ? [`args = [${auth.args.map((argument) => tomlString(argument)).join(", ")}]`]
@@ -1191,9 +1196,11 @@ function writeChatGPTConfig(
1191
1196
  "timeout_ms = 5000",
1192
1197
  "refresh_interval_ms = 300000",
1193
1198
  "",
1194
- "[mcp_servers.impel]",
1195
- `command = ${tomlString(mcp.command)}`,
1196
- `args = [${mcp.args.map((argument) => tomlString(argument)).join(", ")}]`,
1199
+ ...(RUNTIME_BRAND.features.mcp ? [
1200
+ `[mcp_servers.${RUNTIME_BRAND.cli.providerId}]`,
1201
+ `command = ${tomlString(mcp.command)}`,
1202
+ `args = [${mcp.args.map((argument) => tomlString(argument)).join(", ")}]`,
1203
+ ] : []),
1197
1204
  CHATGPT_CONFIG_END,
1198
1205
  ].join("\n");
1199
1206
  const mergedToml = mergeManagedChatGPTToml(currentToml, managedToml);
@@ -1399,7 +1406,7 @@ export function managedChatGPTConfigDrifted(paths) {
1399
1406
  return false; // No managed profile; install/update owns creating one.
1400
1407
  }
1401
1408
  return !current.includes(CHATGPT_CONFIG_START)
1402
- || readTopLevelTomlString(current, "model_provider") !== "impel"
1409
+ || readTopLevelTomlString(current, "model_provider") !== RUNTIME_BRAND.cli.providerId
1403
1410
  || !readTopLevelTomlString(current, "chatgpt_base_url");
1404
1411
  }
1405
1412
 
@@ -1427,7 +1434,7 @@ export function rewriteTenantTokenHelper(tenantId, homeDir = os.homedir()) {
1427
1434
  }
1428
1435
 
1429
1436
  function writeTokenHelper(target, tenantId) {
1430
- const cliPath = fileURLToPath(new URL("../bin/impel.js", import.meta.url));
1437
+ const cliPath = IMPEL_CLI_ENTRYPOINT;
1431
1438
  const node = shellQuote(process.execPath);
1432
1439
  const cli = shellQuote(cliPath);
1433
1440
  const tenantArgs = tenantId ? ` --tenant ${shellQuote(tenantId)}` : "";
@@ -1444,7 +1451,7 @@ function writeTokenHelper(target, tenantId) {
1444
1451
  // prefer the global npm install next to whichever node is found so the
1445
1452
  // refresh keeps rewriting this helper with current paths.
1446
1453
  const script = `#!/bin/sh
1447
- # Managed by impel-cli; regenerated by \`impel app refresh\`.
1454
+ # Managed by ${RUNTIME_BRAND.cli.command}-cli; regenerated by \`${RUNTIME_BRAND.cli.command} app refresh\`.
1448
1455
  NODE=${node}
1449
1456
  CLI=${cli}
1450
1457
  if ! [ -x "$NODE" ]; then
@@ -1456,17 +1463,17 @@ if ! [ -x "$NODE" ]; then
1456
1463
  done
1457
1464
  fi
1458
1465
  if ! [ -x "$NODE" ]; then
1459
- echo 'impel token helper: no usable node runtime found; re-run \`impel setup\`' >&2
1466
+ echo '${RUNTIME_BRAND.cli.command} token helper: no usable node runtime found; re-run \`${RUNTIME_BRAND.cli.command} setup\`' >&2
1460
1467
  exit 1
1461
1468
  fi
1462
1469
  if ! [ -f "$CLI" ]; then
1463
- CLI="$(dirname "$NODE")/../lib/node_modules/impel-cli/bin/impel.js"
1470
+ CLI="$(dirname "$NODE")/../lib/node_modules/${RUNTIME_BRAND.cli.packageName}/bin/${RUNTIME_BRAND.cli.command}.js"
1464
1471
  fi
1465
1472
  if ! [ -f "$CLI" ]; then
1466
- CLI="$(command -v impel 2>/dev/null || true)"
1473
+ CLI="$(command -v ${RUNTIME_BRAND.cli.command} 2>/dev/null || true)"
1467
1474
  fi
1468
1475
  if [ -z "$CLI" ] || ! [ -f "$CLI" ]; then
1469
- echo 'impel token helper: impel-cli not found; re-run \`impel setup\`' >&2
1476
+ echo '${RUNTIME_BRAND.cli.command} token helper: ${RUNTIME_BRAND.cli.packageName} not found; re-run \`${RUNTIME_BRAND.cli.command} setup\`' >&2
1470
1477
  exit 1
1471
1478
  fi
1472
1479
  ("$NODE" "$CLI" app refresh --stale-only${tenantArgs} </dev/null >/dev/null 2>&1 &)
@@ -2149,7 +2156,7 @@ export function ensureChatGPTLaunchHost({
2149
2156
  }
2150
2157
 
2151
2158
  export function chatGPTLaunchHostCacheDir(homeDir = os.homedir()) {
2152
- return path.join(homeDir, ".config", "impel", "cache", "chatgpt-launch-host");
2159
+ return path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "cache", "chatgpt-launch-host");
2153
2160
  }
2154
2161
 
2155
2162
  function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableName = "launch") {
@@ -2407,7 +2414,7 @@ function signVendoredClaudeCapabilities(bundle, identity = ADHOC_IDENTITY) {
2407
2414
  const helpers = fs.readdirSync(frameworks, { withFileTypes: true })
2408
2415
  .filter((entry) => (
2409
2416
  entry.isDirectory()
2410
- && entry.name.startsWith("Impel Claude")
2417
+ && entry.name.startsWith(`${RUNTIME_BRAND.apps.displayPrefix} Claude`)
2411
2418
  && entry.name.includes(" Helper")
2412
2419
  && entry.name.endsWith(".app")
2413
2420
  ));
@@ -14,6 +14,7 @@ import { impelCliInvocation, impelMcpInvocation } from "./selfInvocation.js";
14
14
  import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
15
15
  import { renameWithWindowsRetry } from "./windowsFs.js";
16
16
  import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
17
+ import { RUNTIME_BRAND } from "./runtimeBrand.js";
17
18
 
18
19
  export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
19
20
 
@@ -26,8 +27,8 @@ export function tenantCliProfilePaths(tenantId) {
26
27
  };
27
28
  }
28
29
 
29
- const CODEX_START_MARK = "# >>> impel-cli isolated profile block >>>";
30
- const CODEX_END_MARK = "# <<< impel-cli isolated profile block <<<";
30
+ const CODEX_START_MARK = `# >>> ${RUNTIME_BRAND.cli.managedMarker} isolated profile block >>>`;
31
+ const CODEX_END_MARK = `# <<< ${RUNTIME_BRAND.cli.managedMarker} isolated profile block <<<`;
31
32
  const CODEX_PROVIDER_LINE_RE = /^model_provider[ \t]*=[ \t]*"([^"]*)"[ \t]*$/m;
32
33
  const IMPEL_SPECIALIST_CODEX_APPROVAL_TOOLS = [
33
34
  "impel_specialists-list_specialist_runs",
@@ -95,7 +96,7 @@ export function ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels
95
96
  // The isolated launcher supplies the Impel PAT only to the child process.
96
97
  // Remove older launcher-generated helpers so Claude does not validate the
97
98
  // bearer token as an Anthropic-shaped API key before sending it upstream.
98
- if (settings.apiKeyHelper === "impel token") delete settings.apiKeyHelper;
99
+ if (settings.apiKeyHelper === `${RUNTIME_BRAND.cli.command} token`) delete settings.apiKeyHelper;
99
100
  if (typeof settings.model !== "string" || !settings.model.trim()) {
100
101
  settings.model = IMPEL_DEFAULT_CLAUDE_MODEL;
101
102
  }
@@ -122,18 +123,20 @@ export function ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels
122
123
  }
123
124
  settings.env = managedEnvironment;
124
125
  applyImpelClaudeSandbox(settings);
125
- userConfig.mcpServers = {
126
- ...(userConfig.mcpServers &&
127
- typeof userConfig.mcpServers === "object" &&
128
- !Array.isArray(userConfig.mcpServers)
129
- ? userConfig.mcpServers
130
- : {}),
131
- impel: impelMcpInvocation(["--tenant", tenantId]),
132
- };
126
+ if (RUNTIME_BRAND.features.mcp) {
127
+ userConfig.mcpServers = {
128
+ ...(userConfig.mcpServers &&
129
+ typeof userConfig.mcpServers === "object" &&
130
+ !Array.isArray(userConfig.mcpServers)
131
+ ? userConfig.mcpServers
132
+ : {}),
133
+ [RUNTIME_BRAND.cli.providerId]: impelMcpInvocation(["--tenant", tenantId]),
134
+ };
135
+ }
133
136
 
134
137
  writePrivateFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
135
138
  writePrivateFile(userConfigPath, `${JSON.stringify(userConfig, null, 2)}\n`);
136
- ensureClaudeSessionHooks(configDir, tenantId, "claude_cli");
139
+ if (RUNTIME_BRAND.features.sessions) ensureClaudeSessionHooks(configDir, tenantId, "claude_cli");
137
140
 
138
141
  return { configDir, settingsPath, userConfigPath };
139
142
  }
@@ -144,7 +147,7 @@ function stripCodexManagedBlock(text, configPath) {
144
147
  const end = text.indexOf(CODEX_END_MARK, start);
145
148
  if (end === -1) {
146
149
  throw new Error(
147
- `${configPath} has an incomplete Impel profile block. Fix or remove it, then re-run.`
150
+ `${configPath} has an incomplete ${RUNTIME_BRAND.product.displayName} profile block. Fix or remove it, then re-run.`
148
151
  );
149
152
  }
150
153
  return text.slice(0, start) + text.slice(end + CODEX_END_MARK.length);
@@ -158,32 +161,31 @@ function splitTomlPreamble(text) {
158
161
 
159
162
  function codexManagedBlock(gatewayUrl, tenantId) {
160
163
  const auth = impelCliInvocation(["token", "--tenant", tenantId]);
161
- const mcp = impelCliInvocation(["mcp", "--tenant", tenantId]);
164
+ const providerId = RUNTIME_BRAND.cli.providerId;
162
165
  const lines = [
163
166
  CODEX_START_MARK,
164
- "# Generated for `impel codex`. Other profile settings outside this block are preserved.",
165
- "[model_providers.impel]",
166
- 'name = "Impel Gateway"',
167
+ `# Generated for \`${RUNTIME_BRAND.cli.command} codex\`. Other profile settings outside this block are preserved.`,
168
+ `[model_providers.${providerId}]`,
169
+ `name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
167
170
  `base_url = ${JSON.stringify(impelCodexBaseUrl(gatewayUrl))}`,
168
171
  'wire_api = "responses"',
169
172
  "",
170
- "[model_providers.impel.auth]",
173
+ `[model_providers.${providerId}.auth]`,
171
174
  `command = ${JSON.stringify(auth.command)}`,
172
175
  `args = [${auth.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
173
176
  "timeout_ms = 5000",
174
177
  "refresh_interval_ms = 300000",
175
- "",
176
- "[mcp_servers.impel]",
177
- `command = ${JSON.stringify(mcp.command)}`,
178
- `args = [${mcp.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
179
- "",
180
178
  ];
181
- for (const tool of IMPEL_SPECIALIST_CODEX_APPROVAL_TOOLS) {
182
- lines.push(
183
- `[mcp_servers.impel.tools.${JSON.stringify(tool)}]`,
184
- 'approval_mode = "approve"',
185
- "",
186
- );
179
+ if (RUNTIME_BRAND.features.mcp) {
180
+ const mcp = impelCliInvocation(["mcp", "--tenant", tenantId]);
181
+ lines.push("", `[mcp_servers.${providerId}]`, `command = ${JSON.stringify(mcp.command)}`, `args = [${mcp.args.map((argument) => JSON.stringify(argument)).join(", ")}]`, "");
182
+ for (const tool of IMPEL_SPECIALIST_CODEX_APPROVAL_TOOLS) {
183
+ lines.push(
184
+ `[mcp_servers.${providerId}.tools.${JSON.stringify(tool)}]`,
185
+ 'approval_mode = "approve"',
186
+ "",
187
+ );
188
+ }
187
189
  }
188
190
  lines.push(CODEX_END_MARK);
189
191
  return lines.join("\n");
@@ -197,15 +199,16 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
197
199
  const original = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
198
200
  const withoutManagedBlock = stripCodexManagedBlock(original, configPath);
199
201
 
200
- if (/^(\[model_providers\.impel(?:\.|\])|\[mcp_servers\.impel\])/m.test(withoutManagedBlock)) {
202
+ const providerPattern = RUNTIME_BRAND.cli.providerId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
203
+ if (new RegExp(`^(\\[model_providers\\.${providerPattern}(?:\\.|\\])|\\[mcp_servers\\.${providerPattern}\\])`, "m").test(withoutManagedBlock)) {
201
204
  throw new Error(
202
- `${configPath} contains an Impel provider or MCP table outside the managed profile block. ` +
205
+ `${configPath} contains a ${RUNTIME_BRAND.product.displayName} provider or MCP table outside the managed profile block. ` +
203
206
  "Remove or rename that table, then re-run."
204
207
  );
205
208
  }
206
209
 
207
210
  const { preamble, rest } = splitTomlPreamble(withoutManagedBlock);
208
- const providerLine = 'model_provider = "impel"';
211
+ const providerLine = `model_provider = ${JSON.stringify(RUNTIME_BRAND.cli.providerId)}`;
209
212
  let nextPreamble = CODEX_PROVIDER_LINE_RE.test(preamble)
210
213
  ? preamble.replace(CODEX_PROVIDER_LINE_RE, providerLine)
211
214
  : `${preamble.trimEnd()}${preamble.trim() ? "\n" : ""}${providerLine}\n`;
@@ -217,7 +220,7 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
217
220
  .concat("\n");
218
221
 
219
222
  writePrivateFile(configPath, hardenManagedCodexToml(next, configPath));
220
- ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
223
+ if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
221
224
  secureManagedCodexHome(codexHome);
222
225
  return { codexHome, configPath };
223
226
  }
@@ -59,12 +59,13 @@ import {
59
59
  windowsClaudeUserData,
60
60
  windowsManagedChatGPTRoot,
61
61
  } from "../windowsApps.js";
62
+ import { RUNTIME_BRAND } from "../runtimeBrand.js";
62
63
 
63
64
  const CLAUDE_KEYCHAIN_NOTICE = "Claude Keychain: enter your Mac login password and choose Always Allow on the first prompt for this tenant; Allow is temporary.";
64
65
 
65
66
  const APP_CAPABILITIES = Object.freeze({
66
- claude: Object.freeze({ label: "Impel Claude", provider: "Claude" }),
67
- chatgpt: Object.freeze({ label: "Impel ChatGPT", provider: "Codex" }),
67
+ claude: Object.freeze({ label: `${RUNTIME_BRAND.apps.displayPrefix} Claude`, provider: "Claude" }),
68
+ chatgpt: Object.freeze({ label: `${RUNTIME_BRAND.apps.displayPrefix} ChatGPT`, provider: "Codex" }),
68
69
  });
69
70
 
70
71
  /**
@@ -446,14 +447,14 @@ export async function reconcileWindowsTenantApps({
446
447
  // Merge the sync env so an app-embedded binary (IMPEL_CODEX_BIN) satisfies
447
448
  // the availability check even when no standalone CLI is installed.
448
449
  if (io.findBinary(client, { ...environment, ...env }, "win32")) {
449
- await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label, homeDir: os.homedir() });
450
+ if (RUNTIME_BRAND.features.skills) await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label, homeDir: os.homedir() });
450
451
  agentTargets.push(target);
451
452
  } else {
452
453
  io.log(`Skipping skill/agent sync for ${label} — no ${client === "claude" ? "Claude Code" : "Codex"} binary is available to run plugin commands.`);
453
454
  }
454
455
  if (target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
455
456
  }
456
- if (agentTargets.length) {
457
+ if (RUNTIME_BRAND.features.agents && agentTargets.length) {
457
458
  await io.syncAgents({
458
459
  profiles: agentTargets.map((target) => appAgentProfile(target, paths)),
459
460
  gatewayUrl: config.gatewayUrl,
@@ -553,12 +554,12 @@ export async function reconcileMacTenantApps({
553
554
  for (const item of installed) {
554
555
  const { client, env, label } = appSkillTarget(item.target, paths);
555
556
  if (io.findBinary(client, environment, "darwin")) {
556
- await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label, homeDir: os.homedir() });
557
+ if (RUNTIME_BRAND.features.skills) await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label, homeDir: os.homedir() });
557
558
  agentItems.push(item);
558
559
  }
559
560
  if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
560
561
  }
561
- if (agentItems.length) {
562
+ if (RUNTIME_BRAND.features.agents && agentItems.length) {
562
563
  await io.syncAgents({
563
564
  profiles: agentItems.map((item) => appAgentProfile(item.target, paths)),
564
565
  gatewayUrl: config.gatewayUrl,
@@ -689,7 +690,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
689
690
  }
690
691
 
691
692
  try {
692
- if (action === "open") maybePrintUpdateNotice();
693
+ if (action === "open" && RUNTIME_BRAND.cli.packageName === "impel-cli") maybePrintUpdateNotice();
693
694
  // Background token-helper refreshes pass --stale-only; honor the manifest
694
695
  // TTL here like the darwin refresh path does, so every vendor token call
695
696
  // does not become a full catalog fetch + profile rewrite (and its child
@@ -790,7 +791,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
790
791
  const profile = target === "claude" ? tenantPaths.claude.userData : tenantPaths.chatgpt.root;
791
792
  console.log(`${verb} Impel ${target === "claude" ? "Claude" : "ChatGPT"} profile at ${profile}`);
792
793
  const { client, env, label } = appSkillTarget(target, tenantPaths, { platform: "win32" });
793
- if (!flags["skip-skills"]) {
794
+ if (RUNTIME_BRAND.features.skills && !flags["skip-skills"]) {
794
795
  await withProgress(`Syncing skills for ${label}`, (spinner) => (
795
796
  io.syncSkills({
796
797
  client,
@@ -804,7 +805,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
804
805
  }
805
806
  if (target === "chatgpt") secureManagedCodexHome(tenantPaths.chatgpt.codexHome);
806
807
  }
807
- if (!flags["skip-agents"]) {
808
+ if (RUNTIME_BRAND.features.agents && !flags["skip-agents"]) {
808
809
  await withProgress("Syncing app agent profiles", () => io.syncAgents({
809
810
  profiles: actionTargets.map((target) => appAgentProfile(target, tenantPaths)),
810
811
  gatewayUrl: config.gatewayUrl,
@@ -1038,14 +1039,14 @@ export async function cmdApps(argv, overrides = {}) {
1038
1039
  const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
1039
1040
  for (const item of installed) {
1040
1041
  const { client, env, label } = appSkillTarget(item.target, paths);
1041
- if (!flags["skip-skills"]) {
1042
+ if (RUNTIME_BRAND.features.skills && !flags["skip-skills"]) {
1042
1043
  await withProgress(`Syncing skills for ${label}`, (spinner) => (
1043
1044
  io.syncSkills({ client, gatewayUrl, env, label, homeDir: os.homedir(), logger: createProgressLogger(spinner) })
1044
1045
  ));
1045
1046
  }
1046
1047
  if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
1047
1048
  }
1048
- if (!flags["skip-agents"]) {
1049
+ if (RUNTIME_BRAND.features.agents && !flags["skip-agents"]) {
1049
1050
  await withProgress("Syncing app agent profiles", () => io.syncAgents({
1050
1051
  profiles: installed.map((item) => appAgentProfile(item.target, paths)),
1051
1052
  gatewayUrl: config.gatewayUrl,
@@ -1170,17 +1171,21 @@ export async function provisionAndOpenManagedApps({
1170
1171
  const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
1171
1172
  for (const item of installed) {
1172
1173
  const { client, env, label } = appSkillTarget(item.target, paths);
1173
- await withProgress(`Syncing skills for ${label}`, (spinner) => (
1174
- io.syncSkills({ client, gatewayUrl, env, label, homeDir: os.homedir(), logger: createProgressLogger(spinner) })
1175
- ));
1174
+ if (RUNTIME_BRAND.features.skills) {
1175
+ await withProgress(`Syncing skills for ${label}`, (spinner) => (
1176
+ io.syncSkills({ client, gatewayUrl, env, label, homeDir: os.homedir(), logger: createProgressLogger(spinner) })
1177
+ ));
1178
+ }
1176
1179
  if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
1177
1180
  }
1178
- await withProgress("Syncing app agent profiles", () => io.syncAgents({
1179
- profiles: installed.map((item) => appAgentProfile(item.target, paths)),
1180
- gatewayUrl: config.gatewayUrl,
1181
- credential: config.pat,
1182
- tenantId: config.tenantId,
1183
- }));
1181
+ if (RUNTIME_BRAND.features.agents) {
1182
+ await withProgress("Syncing app agent profiles", () => io.syncAgents({
1183
+ profiles: installed.map((item) => appAgentProfile(item.target, paths)),
1184
+ gatewayUrl: config.gatewayUrl,
1185
+ credential: config.pat,
1186
+ tenantId: config.tenantId,
1187
+ }));
1188
+ }
1184
1189
  for (const item of installed) await io.openLauncher(item.launcher);
1185
1190
  return installed;
1186
1191
  }
@@ -1335,18 +1340,20 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {},
1335
1340
  const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
1336
1341
  for (const status of supportedStatuses) {
1337
1342
  const { client, env, label } = appSkillTarget(status.target, tenantPaths);
1338
- await io.syncSkills({ client, gatewayUrl, env, label, homeDir: os.homedir() });
1343
+ if (RUNTIME_BRAND.features.skills) await io.syncSkills({ client, gatewayUrl, env, label, homeDir: os.homedir() });
1339
1344
  if (status.target === "chatgpt") io.secureCodexHome(tenantPaths.chatgpt.codexHome);
1340
1345
  }
1341
- await io.syncAgents({
1342
- profiles: supportedStatuses.map((status) => appAgentProfile(status.target, tenantPaths)),
1343
- gatewayUrl: config.gatewayUrl,
1344
- credential: config.pat,
1345
- tenantId: config.tenantId,
1346
- });
1346
+ if (RUNTIME_BRAND.features.agents) {
1347
+ await io.syncAgents({
1348
+ profiles: supportedStatuses.map((status) => appAgentProfile(status.target, tenantPaths)),
1349
+ gatewayUrl: config.gatewayUrl,
1350
+ credential: config.pat,
1351
+ tenantId: config.tenantId,
1352
+ });
1353
+ }
1347
1354
  // Keep the update-notice cache warm from the same background slot.
1348
1355
  try {
1349
- refreshUpdateCache();
1356
+ if (RUNTIME_BRAND.cli.packageName === "impel-cli") refreshUpdateCache();
1350
1357
  } catch {
1351
1358
  // opportunistic only
1352
1359
  }
@@ -1,6 +1,7 @@
1
1
  import { parseFlags } from "../args.js";
2
2
  import {
3
3
  loadConfig,
4
+ CONFIG_PATH,
4
5
  saveConfig,
5
6
  normalizeGatewayUrl,
6
7
  resolveDefaultGateway,
@@ -9,6 +10,7 @@ import {
9
10
  } from "../config.js";
10
11
  import { promptSecret } from "../prompt.js";
11
12
  import { fetchTenants, normalizeTenantId } from "../tenants.js";
13
+ import { RUNTIME_BRAND } from "../runtimeBrand.js";
12
14
 
13
15
  export async function cmdAuth(argv) {
14
16
  const { flags } = parseFlags(argv, {
@@ -22,16 +24,16 @@ export async function cmdAuth(argv) {
22
24
 
23
25
  let pat = flags.pat;
24
26
  if (!pat) {
25
- pat = await promptSecret("Impel Personal Access Token (impel_pat_...): ");
27
+ pat = await promptSecret(`${RUNTIME_BRAND.product.displayName} Personal Access Token (${RUNTIME_BRAND.auth.patPrefix}...): `);
26
28
  }
27
29
  if (!pat) {
28
30
  console.error("impel: no PAT provided, aborting.");
29
31
  process.exitCode = 1;
30
32
  return;
31
33
  }
32
- if (!pat.startsWith("impel_pat_")) {
34
+ if (!pat.startsWith(RUNTIME_BRAND.auth.patPrefix)) {
33
35
  console.warn(
34
- `impel: warning: that token doesn't start with "impel_pat_" — double check you copied the right value.`
36
+ `${RUNTIME_BRAND.cli.command}: warning: that token doesn't start with "${RUNTIME_BRAND.auth.patPrefix}" — double check you copied the right value.`
35
37
  );
36
38
  }
37
39
 
@@ -75,7 +77,7 @@ export async function cmdAuth(argv) {
75
77
  }
76
78
  saveConfig(config);
77
79
 
78
- console.log(`impel: stored credentials in ~/.config/impel/config.json (mode 0600)`);
80
+ console.log(`${RUNTIME_BRAND.cli.command}: stored credentials in ${CONFIG_PATH} (mode 0600)`);
79
81
  console.log(` gateway: ${gatewayUrl}`);
80
82
  console.log(` app: ${appUrl}`);
81
83
  console.log(` pat: ${maskSecret(pat)}`);