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 +12 -3
- package/package.json +3 -2
- package/src/apps.js +42 -35
- package/src/cliProfiles.js +36 -33
- package/src/commands/apps.js +35 -28
- package/src/commands/auth.js +6 -4
- package/src/commands/launch.js +12 -8
- package/src/commands/pat.js +46 -37
- package/src/commands/setup.js +11 -8
- package/src/commands/status.js +7 -5
- package/src/commands/token.js +3 -2
- package/src/config.js +14 -10
- package/src/extension/index.js +81 -0
- package/src/provisioning.js +5 -4
- package/src/runtimeBrand.js +140 -0
- package/src/selfInvocation.js +7 -3
- package/src/shellEntries.js +5 -2
- package/src/stableEntrypoint.js +18 -12
- package/src/tenants.js +15 -7
- package/src/windowsApps.js +7 -6
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
|
|
313
|
-
|
|
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.
|
|
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 =
|
|
21
|
-
const CHATGPT_CONFIG_END =
|
|
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:
|
|
28
|
-
bundleIdentifier:
|
|
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:
|
|
34
|
-
bundleIdentifier:
|
|
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
|
-
?
|
|
149
|
-
:
|
|
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
|
|
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
|
|
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",
|
|
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",
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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") !==
|
|
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 =
|
|
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
|
|
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 '
|
|
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
|
|
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
|
|
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 '
|
|
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",
|
|
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(
|
|
2417
|
+
&& entry.name.startsWith(`${RUNTIME_BRAND.apps.displayPrefix} Claude`)
|
|
2411
2418
|
&& entry.name.includes(" Helper")
|
|
2412
2419
|
&& entry.name.endsWith(".app")
|
|
2413
2420
|
));
|
package/src/cliProfiles.js
CHANGED
|
@@ -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 =
|
|
30
|
-
const CODEX_END_MARK =
|
|
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 ===
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
|
164
|
+
const providerId = RUNTIME_BRAND.cli.providerId;
|
|
162
165
|
const lines = [
|
|
163
166
|
CODEX_START_MARK,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
}
|
package/src/commands/apps.js
CHANGED
|
@@ -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:
|
|
67
|
-
chatgpt: Object.freeze({ label:
|
|
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
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1179
|
-
profiles
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
}
|
package/src/commands/auth.js
CHANGED
|
@@ -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(
|
|
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(
|
|
34
|
+
if (!pat.startsWith(RUNTIME_BRAND.auth.patPrefix)) {
|
|
33
35
|
console.warn(
|
|
34
|
-
|
|
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(
|
|
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)}`);
|