impel-cli 0.7.0
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 +695 -0
- package/bin/impel.js +7 -0
- package/package.json +29 -0
- package/src/apps.js +1263 -0
- package/src/args.js +36 -0
- package/src/claudeSetup.js +207 -0
- package/src/cli.js +184 -0
- package/src/cliProfiles.js +216 -0
- package/src/codexSecurity.js +184 -0
- package/src/codexSetup.js +224 -0
- package/src/commands/apps.js +538 -0
- package/src/commands/auth.js +89 -0
- package/src/commands/doctor.js +215 -0
- package/src/commands/experimental.js +60 -0
- package/src/commands/launch.js +161 -0
- package/src/commands/mcp.js +94 -0
- package/src/commands/setup.js +350 -0
- package/src/commands/skills.js +108 -0
- package/src/commands/status.js +95 -0
- package/src/commands/tasks.js +359 -0
- package/src/commands/tenant.js +77 -0
- package/src/commands/token.js +25 -0
- package/src/commands/update.js +217 -0
- package/src/commands/use.js +208 -0
- package/src/config.js +98 -0
- package/src/doctor.js +546 -0
- package/src/nativeProcess.js +192 -0
- package/src/prompt.js +51 -0
- package/src/selfInvocation.js +21 -0
- package/src/skills.js +314 -0
- package/src/tenants.js +194 -0
- package/src/updates.js +181 -0
- package/src/windowsApps.js +439 -0
- package/src/windowsSetup.js +122 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// `impel use gateway|account [claude|codex|all]` and its aliases `impel on` /
|
|
2
|
+
// `impel off`. This is the reversible switch between:
|
|
3
|
+
// - gateway mode: Claude Code / Codex route through the Impel custom gateway
|
|
4
|
+
// - account mode: the developer's own Anthropic / OpenAI login
|
|
5
|
+
//
|
|
6
|
+
// The switch is per-tool and idempotent. When we turn gateway mode ON we back
|
|
7
|
+
// up whatever the tool had before (its own apiKeyHelper / base URL / Codex
|
|
8
|
+
// model_provider) into the impel config, so turning it OFF restores exactly
|
|
9
|
+
// that — never a guess.
|
|
10
|
+
|
|
11
|
+
import { loadConfig, saveConfig, resolveDefaultGateway } from "../config.js";
|
|
12
|
+
import {
|
|
13
|
+
applyClaudeGateway,
|
|
14
|
+
revertClaudeGateway,
|
|
15
|
+
isImpelApiKeyHelper,
|
|
16
|
+
isImpelClaudeBaseUrl,
|
|
17
|
+
} from "../claudeSetup.js";
|
|
18
|
+
import {
|
|
19
|
+
applyCodexGateway,
|
|
20
|
+
revertCodexGateway,
|
|
21
|
+
CODEX_HOME,
|
|
22
|
+
PROVIDER_ID,
|
|
23
|
+
} from "../codexSetup.js";
|
|
24
|
+
import { syncSkillsSafe } from "../skills.js";
|
|
25
|
+
|
|
26
|
+
const VALID_TARGETS = ["claude", "codex", "all"];
|
|
27
|
+
|
|
28
|
+
function wantsClaude(target) {
|
|
29
|
+
return target === "all" || target === "claude";
|
|
30
|
+
}
|
|
31
|
+
function wantsCodex(target) {
|
|
32
|
+
return target === "all" || target === "codex";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Store a fresh account-mode backup, but only when transitioning account -> gateway. */
|
|
36
|
+
function ensureBackup(config, tool) {
|
|
37
|
+
config.backups = config.backups || {};
|
|
38
|
+
config.backups[tool] = config.backups[tool] || {};
|
|
39
|
+
return config.backups[tool];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cmdUse({ mode, target = "all", app = false }) {
|
|
43
|
+
if (!VALID_TARGETS.includes(target)) {
|
|
44
|
+
console.error(`impel: unknown target "${target}". Use \`claude\`, \`codex\`, or \`all\`.`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mode === "gateway") return useGateway({ target, app });
|
|
50
|
+
if (mode === "account") return useAccount({ target, app });
|
|
51
|
+
|
|
52
|
+
console.error(`impel: unknown mode "${mode}". Use \`gateway\` or \`account\`.`);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function useGateway({ target, app }) {
|
|
57
|
+
const config = loadConfig();
|
|
58
|
+
if (!config?.pat) {
|
|
59
|
+
console.error("impel: not authenticated. Run `impel setup` (or `impel auth`) first.");
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const gatewayUrl = config.gatewayUrl || resolveDefaultGateway();
|
|
64
|
+
|
|
65
|
+
if (wantsClaude(target)) {
|
|
66
|
+
const result = applyClaudeGateway(gatewayUrl);
|
|
67
|
+
const backup = ensureBackup(config, "claude");
|
|
68
|
+
// Only capture the account-mode value; if the prior value was already
|
|
69
|
+
// Impel's own (re-apply), keep whatever we backed up the first time.
|
|
70
|
+
if (!isImpelApiKeyHelper(result.priorApiKeyHelper)) {
|
|
71
|
+
backup.apiKeyHelper = result.priorApiKeyHelper ?? null;
|
|
72
|
+
}
|
|
73
|
+
if (!isImpelClaudeBaseUrl(result.priorBaseUrl, gatewayUrl)) {
|
|
74
|
+
backup.ANTHROPIC_BASE_URL = result.priorBaseUrl ?? null;
|
|
75
|
+
}
|
|
76
|
+
console.log(`Claude Code -> GATEWAY (${result.path})`);
|
|
77
|
+
console.log(` apiKeyHelper = "${result.apiKeyHelper}"`);
|
|
78
|
+
console.log(` env.ANTHROPIC_BASE_URL = "${result.baseUrl}"`);
|
|
79
|
+
console.log(` MCP server = "impel" (direct Impel CLI invocation)`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (wantsCodex(target)) {
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = applyCodexGateway(gatewayUrl);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(`impel: ${err.message}`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const backup = ensureBackup(config, "codex");
|
|
92
|
+
if (result.priorProviderValue !== PROVIDER_ID) {
|
|
93
|
+
backup.model_provider = result.priorProviderValue ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`Codex ${app ? "app/IDE" : "CLI"} -> GATEWAY (${result.path})`);
|
|
97
|
+
console.log(` model_provider = "${PROVIDER_ID}"`);
|
|
98
|
+
console.log(` [model_providers.${PROVIDER_ID}] base_url = "${result.baseUrl}"`);
|
|
99
|
+
console.log(` [model_providers.${PROVIDER_ID}.auth] = direct Impel CLI invocation`);
|
|
100
|
+
console.log(` [mcp_servers.${PROVIDER_ID}] = direct Impel CLI invocation`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
saveConfig(config);
|
|
104
|
+
|
|
105
|
+
// Best-effort: pull the latest Bifrost shared skills into whichever managed
|
|
106
|
+
// profile we just pointed at the gateway. Never fails the setup.
|
|
107
|
+
if (wantsClaude(target)) {
|
|
108
|
+
await syncSkillsSafe({ client: "claude", gatewayUrl, env: {}, label: "Claude Code (gateway)" });
|
|
109
|
+
}
|
|
110
|
+
if (wantsCodex(target)) {
|
|
111
|
+
await syncSkillsSafe({ client: "codex", gatewayUrl, env: { CODEX_HOME }, label: "Codex CLI (gateway)" });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log("");
|
|
115
|
+
printGatewayNextSteps({ target, app });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function useAccount({ target, app }) {
|
|
119
|
+
const config = loadConfig();
|
|
120
|
+
const gatewayUrl = config?.gatewayUrl || resolveDefaultGateway();
|
|
121
|
+
const backups = config?.backups || {};
|
|
122
|
+
|
|
123
|
+
if (wantsClaude(target)) {
|
|
124
|
+
const result = revertClaudeGateway(gatewayUrl, backups.claude || {});
|
|
125
|
+
if (!result.exists) {
|
|
126
|
+
console.log(`Claude Code -> ACCOUNT (nothing to revert; ${result.path} doesn't exist)`);
|
|
127
|
+
} else if (!result.changed) {
|
|
128
|
+
console.log(`Claude Code -> ACCOUNT (already in account mode; no Impel keys found)`);
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`Claude Code -> ACCOUNT (${result.path})`);
|
|
131
|
+
if (result.removedHelper) {
|
|
132
|
+
console.log(
|
|
133
|
+
result.restoredApiKeyHelper != null
|
|
134
|
+
? ` restored apiKeyHelper = "${result.restoredApiKeyHelper}"`
|
|
135
|
+
: ` removed Impel apiKeyHelper`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (result.removedBaseUrl) {
|
|
139
|
+
console.log(
|
|
140
|
+
result.restoredBaseUrl != null
|
|
141
|
+
? ` restored env.ANTHROPIC_BASE_URL = "${result.restoredBaseUrl}"`
|
|
142
|
+
: ` removed Impel env.ANTHROPIC_BASE_URL`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (result.removedMcpServer) {
|
|
146
|
+
console.log(` removed Impel MCP server`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (config?.backups) delete config.backups.claude;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (wantsCodex(target)) {
|
|
153
|
+
const result = revertCodexGateway(backups.codex || {});
|
|
154
|
+
if (!result.exists) {
|
|
155
|
+
console.log(`Codex ${app ? "app/IDE" : "CLI"} -> ACCOUNT (nothing to revert; ${result.path} doesn't exist)`);
|
|
156
|
+
} else if (!result.changed) {
|
|
157
|
+
console.log(`Codex ${app ? "app/IDE" : "CLI"} -> ACCOUNT (already in account mode; no Impel block found)`);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`Codex ${app ? "app/IDE" : "CLI"} -> ACCOUNT (${result.path})`);
|
|
160
|
+
if (result.removedBlock) console.log(` removed [model_providers.${PROVIDER_ID}] block`);
|
|
161
|
+
if (result.resetProvider) {
|
|
162
|
+
console.log(
|
|
163
|
+
result.restoredProvider != null
|
|
164
|
+
? ` restored model_provider = "${result.restoredProvider}"`
|
|
165
|
+
: ` removed model_provider line (Codex falls back to its default)`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (config?.backups) delete config.backups.codex;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (config) saveConfig(config);
|
|
173
|
+
|
|
174
|
+
console.log("");
|
|
175
|
+
printAccountNextSteps({ target, app });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printGatewayNextSteps({ target, app }) {
|
|
179
|
+
console.log("Next steps:");
|
|
180
|
+
if (wantsClaude(target)) {
|
|
181
|
+
console.log(" Claude Code: restart it (or open a new terminal) so it re-reads ~/.claude/settings.json.");
|
|
182
|
+
}
|
|
183
|
+
if (wantsCodex(target)) {
|
|
184
|
+
if (app) {
|
|
185
|
+
console.log(
|
|
186
|
+
` Codex app/IDE: it shares ${CODEX_HOME} with the CLI — fully quit and reopen the app/IDE window so it re-reads config.toml.`
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
console.log(" Codex CLI: open a new terminal (config.toml is read at startup), then run `codex`.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
console.log(" Verify anytime with `impel status`. Flip back with `impel use account` (alias `impel off`).");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function printAccountNextSteps({ target, app }) {
|
|
196
|
+
console.log("Reverted to your own account login. Next steps:");
|
|
197
|
+
if (wantsClaude(target)) {
|
|
198
|
+
console.log(" Claude Code: restart it. If you aren't otherwise logged in, run `claude` and sign in / set ANTHROPIC_API_KEY.");
|
|
199
|
+
}
|
|
200
|
+
if (wantsCodex(target)) {
|
|
201
|
+
if (app) {
|
|
202
|
+
console.log(" Codex app/IDE: fully quit and reopen the window. Sign in with ChatGPT or your API key if needed.");
|
|
203
|
+
} else {
|
|
204
|
+
console.log(" Codex CLI: open a new terminal. Run `codex login` (or set OPENAI_API_KEY) if you aren't already signed in.");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.log(" Flip back to the gateway anytime with `impel use gateway` (alias `impel on`).");
|
|
208
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Reads/writes the impel-cli config file: ~/.config/impel/config.json
|
|
2
|
+
//
|
|
3
|
+
// Shape:
|
|
4
|
+
// {
|
|
5
|
+
// "pat": "impel_pat_...",
|
|
6
|
+
// "gatewayUrl": "https://gateway.useimpel.com",
|
|
7
|
+
// "appUrl": "https://www.useimpel.com",
|
|
8
|
+
// "tenantId": "impel",
|
|
9
|
+
// "experimental": { "crossAppModels": true },
|
|
10
|
+
// "updatedAt": "2026-07-08T00:00:00.000Z"
|
|
11
|
+
// }
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_GATEWAY_URL = "https://gateway.useimpel.com";
|
|
18
|
+
export const DEFAULT_APP_URL = "https://www.useimpel.com";
|
|
19
|
+
const LEGACY_GATEWAY_URLS = new Set(["https://gateway.useimpel.ai"]);
|
|
20
|
+
|
|
21
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".config", "impel");
|
|
22
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
23
|
+
|
|
24
|
+
/** Hidden, default-off experiment. Only the exact boolean true enables it. */
|
|
25
|
+
export function crossAppModelsEnabled(config) {
|
|
26
|
+
return config?.experimental?.crossAppModels === true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve the gateway URL to use when nothing is stored yet: env var, else the canonical gateway. */
|
|
30
|
+
export function resolveDefaultGateway() {
|
|
31
|
+
return normalizeGatewayUrl(process.env.IMPEL_GATEWAY_URL || DEFAULT_GATEWAY_URL);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Resolve the app/control-plane URL used by task APIs. */
|
|
35
|
+
export function resolveDefaultAppUrl() {
|
|
36
|
+
return normalizeGatewayUrl(process.env.IMPEL_APP_URL || DEFAULT_APP_URL);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Strip trailing slashes so `${gatewayUrl}/anthropic` never ends up with a double slash. */
|
|
40
|
+
export function normalizeGatewayUrl(url) {
|
|
41
|
+
const normalized = String(url).trim().replace(/\/+$/, "");
|
|
42
|
+
return LEGACY_GATEWAY_URLS.has(normalized) ? DEFAULT_GATEWAY_URL : normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns the parsed config object, or null if it doesn't exist yet. Throws on malformed JSON. */
|
|
46
|
+
export function loadConfig() {
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err.code === "ENOENT") return null;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const config = JSON.parse(raw);
|
|
56
|
+
if (config?.gatewayUrl) config.gatewayUrl = normalizeGatewayUrl(config.gatewayUrl);
|
|
57
|
+
return config;
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${CONFIG_PATH} exists but isn't valid JSON. Fix or delete it, then run \`impel auth\` again.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Writes the config atomically-ish and locks it down to 0600 (owner read/write only). */
|
|
66
|
+
export function saveConfig(config) {
|
|
67
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
68
|
+
const json = JSON.stringify(config, null, 2) + "\n";
|
|
69
|
+
const tmpPath = `${CONFIG_PATH}.tmp-${process.pid}`;
|
|
70
|
+
fs.writeFileSync(tmpPath, json, { mode: 0o600 });
|
|
71
|
+
fs.renameSync(tmpPath, CONFIG_PATH);
|
|
72
|
+
try {
|
|
73
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
74
|
+
} catch {
|
|
75
|
+
// best-effort on platforms (e.g. Windows) where chmod is a no-op
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Short, safe-to-print representation of a secret token, e.g. "impel_pat_ab12...wxyz". */
|
|
80
|
+
export function maskSecret(secret) {
|
|
81
|
+
if (!secret) return "(none)";
|
|
82
|
+
if (secret.length <= 12) return "*".repeat(secret.length);
|
|
83
|
+
return `${secret.slice(0, 10)}...${secret.slice(-4)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const TENANT_CREDENTIAL_RE = /impel_tenant_[A-Za-z0-9_-]+\.impel_pat_[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?/gu;
|
|
87
|
+
const PAT_RE = /impel_pat_[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?/gu;
|
|
88
|
+
const ANSI_ESCAPE_RE = /\u001B(?:\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/gu;
|
|
89
|
+
const TERMINAL_CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/gu;
|
|
90
|
+
|
|
91
|
+
/** Remove credentials and terminal control sequences from untrusted text. */
|
|
92
|
+
export function redactSecretText(value) {
|
|
93
|
+
return String(value ?? "")
|
|
94
|
+
.replace(TENANT_CREDENTIAL_RE, "[REDACTED IMPEL CREDENTIAL]")
|
|
95
|
+
.replace(PAT_RE, "[REDACTED IMPEL CREDENTIAL]")
|
|
96
|
+
.replace(ANSI_ESCAPE_RE, "")
|
|
97
|
+
.replace(TERMINAL_CONTROL_RE, " ");
|
|
98
|
+
}
|