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,184 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const PRIVATE_ROOT_FILES = ["auth.json", "config.toml", "models.json"];
|
|
5
|
+
const MANAGED_COMMENT = "# Impel security policy: keep gateway credentials out of shell state.";
|
|
6
|
+
|
|
7
|
+
function lstatOrNull(target) {
|
|
8
|
+
try {
|
|
9
|
+
return fs.lstatSync(target);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error?.code === "ENOENT") return null;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function escapeRegex(value) {
|
|
17
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tableHeaderPattern(table) {
|
|
21
|
+
return new RegExp(`^\\s*\\[${escapeRegex(table)}\\]\\s*(?:#.*)?$`, "u");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assignmentPattern(key) {
|
|
25
|
+
return new RegExp(`^(\\s*)${escapeRegex(key)}\\s*=.*$`, "u");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function upsertManagedBoolean(toml, table, key, value, configPath) {
|
|
29
|
+
const lines = String(toml || "").replace(/\r\n/gu, "\n").split("\n");
|
|
30
|
+
const headerPattern = tableHeaderPattern(table);
|
|
31
|
+
const headerIndices = lines.flatMap((line, index) => (headerPattern.test(line) ? [index] : []));
|
|
32
|
+
if (headerIndices.length > 1) {
|
|
33
|
+
throw new Error(`${configPath} contains duplicate [${table}] tables; resolve them before re-running.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/u.test(line));
|
|
37
|
+
const preambleEnd = firstTable === -1 ? lines.length : firstTable;
|
|
38
|
+
const dottedPattern = new RegExp(`^(\\s*)${escapeRegex(table)}\\.${escapeRegex(key)}\\s*=.*$`, "u");
|
|
39
|
+
const dottedIndices = lines
|
|
40
|
+
.slice(0, preambleEnd)
|
|
41
|
+
.flatMap((line, index) => (dottedPattern.test(line) ? [index] : []));
|
|
42
|
+
if (dottedIndices.length > 1 || (dottedIndices.length === 1 && headerIndices.length === 1)) {
|
|
43
|
+
throw new Error(`${configPath} defines ${table}.${key} more than once; resolve it before re-running.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (dottedIndices.length === 1) {
|
|
47
|
+
const index = dottedIndices[0];
|
|
48
|
+
const indent = lines[index].match(dottedPattern)?.[1] || "";
|
|
49
|
+
lines[index] = `${indent}${table}.${key} = ${value}`;
|
|
50
|
+
return `${lines.join("\n").replace(/\s*$/u, "")}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (headerIndices.length === 1) {
|
|
54
|
+
const header = headerIndices[0];
|
|
55
|
+
const nextTableOffset = lines.slice(header + 1).findIndex((line) => /^\s*\[/u.test(line));
|
|
56
|
+
const end = nextTableOffset === -1 ? lines.length : header + 1 + nextTableOffset;
|
|
57
|
+
const keyPattern = assignmentPattern(key);
|
|
58
|
+
const keyIndices = lines
|
|
59
|
+
.slice(header + 1, end)
|
|
60
|
+
.flatMap((line, index) => (keyPattern.test(line) ? [header + 1 + index] : []));
|
|
61
|
+
if (keyIndices.length > 1) {
|
|
62
|
+
throw new Error(`${configPath} defines ${key} more than once in [${table}].`);
|
|
63
|
+
}
|
|
64
|
+
if (keyIndices.length === 1) {
|
|
65
|
+
const index = keyIndices[0];
|
|
66
|
+
const indent = lines[index].match(keyPattern)?.[1] || "";
|
|
67
|
+
lines[index] = `${indent}${key} = ${value}`;
|
|
68
|
+
} else {
|
|
69
|
+
lines.splice(header + 1, 0, `${key} = ${value}`);
|
|
70
|
+
}
|
|
71
|
+
return `${lines.join("\n").replace(/\s*$/u, "")}\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const inlineTablePattern = new RegExp(`^\\s*${escapeRegex(table)}\\s*=`, "u");
|
|
75
|
+
if (lines.slice(0, preambleEnd).some((line) => inlineTablePattern.test(line))) {
|
|
76
|
+
throw new Error(`${configPath} defines ${table} as an inline table; convert it to [${table}] before re-running.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const base = lines.join("\n").replace(/\s*$/u, "");
|
|
80
|
+
return `${base}${base ? "\n\n" : ""}${MANAGED_COMMENT}\n[${table}]\n${key} = ${value}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Disable Codex shell snapshots and keep its default KEY/SECRET/TOKEN filter
|
|
85
|
+
* enabled. Existing unrelated keys in either table are retained.
|
|
86
|
+
*/
|
|
87
|
+
export function hardenManagedCodexToml(toml, configPath = "managed Codex config") {
|
|
88
|
+
const withoutSnapshots = upsertManagedBoolean(toml, "features", "shell_snapshot", "false", configPath);
|
|
89
|
+
return upsertManagedBoolean(
|
|
90
|
+
withoutSnapshots,
|
|
91
|
+
"shell_environment_policy",
|
|
92
|
+
"ignore_default_excludes",
|
|
93
|
+
"false",
|
|
94
|
+
configPath,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertSafeManagedPath(target, expectedType) {
|
|
99
|
+
const stat = lstatOrNull(target);
|
|
100
|
+
if (!stat) return null;
|
|
101
|
+
if (stat.isSymbolicLink()) throw new Error(`${target} must not be a symbolic link.`);
|
|
102
|
+
if (expectedType === "directory" && !stat.isDirectory()) {
|
|
103
|
+
throw new Error(`${target} must be a directory.`);
|
|
104
|
+
}
|
|
105
|
+
if (expectedType === "file" && !stat.isFile()) {
|
|
106
|
+
throw new Error(`${target} must be a regular file.`);
|
|
107
|
+
}
|
|
108
|
+
return stat;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove all derived shell snapshots from an Impel-owned Codex home. Snapshot
|
|
113
|
+
* generation is disabled in config, so retaining any snapshot has no benefit
|
|
114
|
+
* and deleting the whole derived directory avoids reading a credential merely
|
|
115
|
+
* to decide whether the file contains one. Known credential/config files are
|
|
116
|
+
* owner-only. This function must never be called for the native ~/.codex home.
|
|
117
|
+
*/
|
|
118
|
+
export function secureManagedCodexHome(codexHome) {
|
|
119
|
+
const existing = assertSafeManagedPath(codexHome, "directory");
|
|
120
|
+
if (!existing) fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
121
|
+
fs.chmodSync(codexHome, 0o700);
|
|
122
|
+
|
|
123
|
+
const snapshots = path.join(codexHome, "shell_snapshots");
|
|
124
|
+
const snapshotStat = assertSafeManagedPath(snapshots, "directory");
|
|
125
|
+
if (snapshotStat) fs.rmSync(snapshots, { recursive: true, force: true });
|
|
126
|
+
for (const fileName of PRIVATE_ROOT_FILES) {
|
|
127
|
+
const filePath = path.join(codexHome, fileName);
|
|
128
|
+
const stat = assertSafeManagedPath(filePath, "file");
|
|
129
|
+
if (!stat) continue;
|
|
130
|
+
fs.chmodSync(filePath, 0o600);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function secureExistingManagedHome(root, relativeParts, secured) {
|
|
135
|
+
let current = root;
|
|
136
|
+
for (const part of relativeParts) {
|
|
137
|
+
current = path.join(current, part);
|
|
138
|
+
const stat = lstatOrNull(current);
|
|
139
|
+
if (!stat) return;
|
|
140
|
+
if (stat.isSymbolicLink()) throw new Error(`${current} must not be a symbolic link.`);
|
|
141
|
+
if (!stat.isDirectory()) throw new Error(`${current} must be a directory.`);
|
|
142
|
+
}
|
|
143
|
+
secureManagedCodexHome(current);
|
|
144
|
+
secured.push(current);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function secureTenantHomes(root, suffix, secured) {
|
|
148
|
+
const tenants = path.join(root, "tenants");
|
|
149
|
+
const tenantRootStat = lstatOrNull(tenants);
|
|
150
|
+
if (!tenantRootStat) return;
|
|
151
|
+
if (tenantRootStat.isSymbolicLink()) throw new Error(`${tenants} must not be a symbolic link.`);
|
|
152
|
+
if (!tenantRootStat.isDirectory()) throw new Error(`${tenants} must be a directory.`);
|
|
153
|
+
|
|
154
|
+
for (const entry of fs.readdirSync(tenants, { withFileTypes: true })) {
|
|
155
|
+
const tenantRoot = path.join(tenants, entry.name);
|
|
156
|
+
if (entry.isSymbolicLink()) throw new Error(`${tenantRoot} must not be a symbolic link.`);
|
|
157
|
+
if (!entry.isDirectory()) continue;
|
|
158
|
+
secureExistingManagedHome(tenantRoot, suffix, secured);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Secure every Impel-owned Codex profile, including inactive tenants and the
|
|
164
|
+
* pre-tenancy legacy locations. This deliberately accepts only the two Impel
|
|
165
|
+
* profile roots; it never scans or modifies the native ~/.codex directory.
|
|
166
|
+
*/
|
|
167
|
+
export function secureAllManagedCodexHomes({ appsRoot = null, cliRoot = null } = {}) {
|
|
168
|
+
const secured = [];
|
|
169
|
+
if (appsRoot) {
|
|
170
|
+
const appsStat = assertSafeManagedPath(appsRoot, "directory");
|
|
171
|
+
if (appsStat) {
|
|
172
|
+
secureExistingManagedHome(appsRoot, ["chatgpt", "codex-home"], secured);
|
|
173
|
+
secureTenantHomes(appsRoot, ["chatgpt", "codex-home"], secured);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (cliRoot) {
|
|
177
|
+
const cliStat = assertSafeManagedPath(cliRoot, "directory");
|
|
178
|
+
if (cliStat) {
|
|
179
|
+
secureExistingManagedHome(cliRoot, ["codex"], secured);
|
|
180
|
+
secureTenantHomes(cliRoot, ["codex"], secured);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return secured;
|
|
184
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Applies / reverts / detects the Impel-gateway config in ~/.codex/config.toml.
|
|
2
|
+
//
|
|
3
|
+
// Codex CLI supports a "command-backed" bearer token for custom
|
|
4
|
+
// model_providers (a `[model_providers.<id>.auth]` table with `command` +
|
|
5
|
+
// `args`), which Codex re-invokes on a timer and on auth retries. That's the
|
|
6
|
+
// same contract as Claude Code's `apiKeyHelper`. Managed Codex configs invoke
|
|
7
|
+
// this installation's Node + impel.js directly, which also works when the
|
|
8
|
+
// public `impel` command is an npm .cmd shim on Windows. The PAT itself is
|
|
9
|
+
// never written into config.toml or into ~/.codex/auth.json.
|
|
10
|
+
//
|
|
11
|
+
// Docs (fetched July 2026):
|
|
12
|
+
// https://developers.openai.com/codex/config-reference
|
|
13
|
+
// https://developers.openai.com/codex/config-advanced
|
|
14
|
+
// https://developers.openai.com/codex/auth
|
|
15
|
+
// https://developers.openai.com/codex/ide
|
|
16
|
+
//
|
|
17
|
+
// This file is NOT a general TOML parser/writer. Codex's config.toml can
|
|
18
|
+
// contain arbitrary user tables (sandbox policy, MCP servers, other
|
|
19
|
+
// providers, etc.) that we must never touch. Instead we do a narrow,
|
|
20
|
+
// targeted merge:
|
|
21
|
+
// 1. Find/replace a single root-level `model_provider = "..."` line
|
|
22
|
+
// (root-level = before the first `[table]` header). On revert we reset it
|
|
23
|
+
// to its backed-up prior value, or remove it so Codex uses its default.
|
|
24
|
+
// 2. Insert our own `[model_providers.impel]` + `[model_providers.impel.auth]`
|
|
25
|
+
// tables inside clearly-marked comment fences, so applying is idempotent
|
|
26
|
+
// and reverting is an exact, targeted removal (find our own fenced block).
|
|
27
|
+
// 3. Refuse to APPLY if a *foreign* (not ours) `[model_providers.impel*]`
|
|
28
|
+
// table already exists, rather than risk emitting duplicate TOML tables.
|
|
29
|
+
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import os from "node:os";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
|
|
34
|
+
import { impelCliInvocation } from "./selfInvocation.js";
|
|
35
|
+
|
|
36
|
+
export const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
37
|
+
export const CODEX_CONFIG_PATH = path.join(CODEX_HOME, "config.toml");
|
|
38
|
+
|
|
39
|
+
export const PROVIDER_ID = "impel";
|
|
40
|
+
|
|
41
|
+
// Genuine Codex CLI traffic has its own byte-preserving compatibility route.
|
|
42
|
+
// With wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this
|
|
43
|
+
// base must stop immediately before that suffix. Do not point the CLI at the
|
|
44
|
+
// separate `/v1/responses` SDK front door; the gateway may shape that body.
|
|
45
|
+
export const CODEX_CLI_BASE_PATH = "/chatgpt_passthrough/backend-api/codex";
|
|
46
|
+
export const impelCodexBaseUrl = (gatewayUrl) => `${gatewayUrl}${CODEX_CLI_BASE_PATH}`;
|
|
47
|
+
|
|
48
|
+
const START_MARK = `# >>> impel-cli managed block (model_providers.${PROVIDER_ID}) >>>`;
|
|
49
|
+
const END_MARK = `# <<< impel-cli managed block <<<`;
|
|
50
|
+
|
|
51
|
+
const PROVIDER_LINE_RE = /^model_provider[ \t]*=[ \t]*"([^"]*)"[ \t]*$/m;
|
|
52
|
+
|
|
53
|
+
function providerTablesBlock(gatewayUrl) {
|
|
54
|
+
const baseUrl = impelCodexBaseUrl(gatewayUrl);
|
|
55
|
+
const auth = impelCliInvocation(["token"]);
|
|
56
|
+
const mcp = impelCliInvocation(["mcp"]);
|
|
57
|
+
return [
|
|
58
|
+
START_MARK,
|
|
59
|
+
"# Generated by `impel use gateway codex`. Safe to re-run; do not hand-edit",
|
|
60
|
+
"# the lines between the markers above/below, they'll be overwritten.",
|
|
61
|
+
`[model_providers.${PROVIDER_ID}]`,
|
|
62
|
+
`name = "Impel Gateway"`,
|
|
63
|
+
`base_url = "${baseUrl}"`,
|
|
64
|
+
`wire_api = "responses"`,
|
|
65
|
+
"",
|
|
66
|
+
`[model_providers.${PROVIDER_ID}.auth]`,
|
|
67
|
+
`command = ${JSON.stringify(auth.command)}`,
|
|
68
|
+
`args = [${auth.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
69
|
+
`timeout_ms = 5000`,
|
|
70
|
+
`refresh_interval_ms = 300000`,
|
|
71
|
+
"",
|
|
72
|
+
`[mcp_servers.${PROVIDER_ID}]`,
|
|
73
|
+
`command = ${JSON.stringify(mcp.command)}`,
|
|
74
|
+
`args = [${mcp.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
75
|
+
END_MARK,
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readConfig() {
|
|
80
|
+
const exists = fs.existsSync(CODEX_CONFIG_PATH);
|
|
81
|
+
const text = exists ? fs.readFileSync(CODEX_CONFIG_PATH, "utf8") : "";
|
|
82
|
+
return { text, exists };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeConfig(text) {
|
|
86
|
+
fs.mkdirSync(CODEX_HOME, { recursive: true, mode: 0o700 });
|
|
87
|
+
fs.writeFileSync(CODEX_CONFIG_PATH, text, { mode: 0o600 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Splits `text` into the root preamble (keys before the first [table] header) and the rest. */
|
|
91
|
+
function splitPreamble(text) {
|
|
92
|
+
const match = text.match(/^\s*\[/m);
|
|
93
|
+
if (!match) return { preamble: text, rest: "" };
|
|
94
|
+
const idx = match.index;
|
|
95
|
+
return { preamble: text.slice(0, idx), rest: text.slice(idx) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Removes a previously-written impel-cli managed block, wherever it lives in the file. */
|
|
99
|
+
function stripManagedBlock(text) {
|
|
100
|
+
const startIdx = text.indexOf(START_MARK);
|
|
101
|
+
if (startIdx === -1) return text;
|
|
102
|
+
const endIdx = text.indexOf(END_MARK);
|
|
103
|
+
if (endIdx === -1) return text; // shouldn't happen; don't eat the rest of the file
|
|
104
|
+
return text.slice(0, startIdx) + text.slice(endIdx + END_MARK.length);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** True if a `[model_providers.impel` table exists outside of our own managed markers. */
|
|
108
|
+
function hasForeignImpelTable(textWithoutManagedBlock) {
|
|
109
|
+
return /^(\[model_providers\.impel(\.|\])|\[mcp_servers\.impel\])/m.test(
|
|
110
|
+
textWithoutManagedBlock
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reads the current root-level `model_provider` value, or null if unset. */
|
|
115
|
+
function currentProviderValue(text) {
|
|
116
|
+
return text.match(PROVIDER_LINE_RE)?.[1] ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Collapse 3+ consecutive newlines to 2, strip leading blank lines, ensure trailing newline. */
|
|
120
|
+
function tidy(text) {
|
|
121
|
+
let out = text.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
|
|
122
|
+
out = out.replace(/\s+$/, "");
|
|
123
|
+
return out ? out + "\n" : "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detects whether Codex is currently routed through Impel.
|
|
128
|
+
* Gateway mode = our managed block is present OR model_provider == "impel".
|
|
129
|
+
*/
|
|
130
|
+
export function detectCodexMode() {
|
|
131
|
+
const { text, exists } = readConfig();
|
|
132
|
+
const hasBlock = text.includes(START_MARK);
|
|
133
|
+
const providerIsImpel = currentProviderValue(text) === PROVIDER_ID;
|
|
134
|
+
return {
|
|
135
|
+
mode: hasBlock || providerIsImpel ? "gateway" : "account",
|
|
136
|
+
exists,
|
|
137
|
+
hasBlock,
|
|
138
|
+
providerIsImpel,
|
|
139
|
+
providerValue: currentProviderValue(text),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Turns ON gateway mode. Returns `priorProviderValue` (the model_provider value
|
|
145
|
+
* that was set before, or null) so the caller can back it up.
|
|
146
|
+
*/
|
|
147
|
+
export function applyCodexGateway(gatewayUrl) {
|
|
148
|
+
const { text: original } = readConfig();
|
|
149
|
+
|
|
150
|
+
const withoutOurBlock = stripManagedBlock(original);
|
|
151
|
+
if (hasForeignImpelTable(withoutOurBlock)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`${CODEX_CONFIG_PATH} already has an Impel model-provider or MCP table that wasn't written by impel-cli. ` +
|
|
154
|
+
`Remove or rename it, then re-run.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { preamble, rest } = splitPreamble(withoutOurBlock);
|
|
159
|
+
const priorProviderValue = preamble.match(PROVIDER_LINE_RE)?.[1] ?? null;
|
|
160
|
+
const newProviderLine = `model_provider = "${PROVIDER_ID}"`;
|
|
161
|
+
|
|
162
|
+
let newPreamble;
|
|
163
|
+
if (priorProviderValue != null) {
|
|
164
|
+
newPreamble = preamble.replace(PROVIDER_LINE_RE, newProviderLine);
|
|
165
|
+
} else {
|
|
166
|
+
const trimmed = preamble.trimEnd();
|
|
167
|
+
newPreamble = trimmed ? `${trimmed}\n${newProviderLine}\n` : `${newProviderLine}\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const block = providerTablesBlock(gatewayUrl);
|
|
171
|
+
const restTrimmed = rest.trim();
|
|
172
|
+
const newText = `${newPreamble.trimEnd()}\n\n${block}\n` + (restTrimmed ? `\n${restTrimmed}\n` : "");
|
|
173
|
+
|
|
174
|
+
writeConfig(newText);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
path: CODEX_CONFIG_PATH,
|
|
178
|
+
baseUrl: impelCodexBaseUrl(gatewayUrl),
|
|
179
|
+
priorProviderValue,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Turns OFF gateway mode: removes our marker-fenced block, and resets the
|
|
185
|
+
* root-level `model_provider` line — to the backed-up prior value if given,
|
|
186
|
+
* otherwise removes the line entirely so Codex falls back to its built-in
|
|
187
|
+
* default (`openai`). Only touches the line if it currently reads "impel";
|
|
188
|
+
* a value the user set by hand is left alone. All other tables are untouched.
|
|
189
|
+
*/
|
|
190
|
+
export function revertCodexGateway(backup = {}) {
|
|
191
|
+
const { text: original, exists } = readConfig();
|
|
192
|
+
if (!exists) {
|
|
193
|
+
return { path: CODEX_CONFIG_PATH, exists: false, changed: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const removedBlock = original.includes(START_MARK);
|
|
197
|
+
let text = stripManagedBlock(original);
|
|
198
|
+
|
|
199
|
+
let resetProvider = false;
|
|
200
|
+
let restoredProvider = null;
|
|
201
|
+
if (currentProviderValue(text) === PROVIDER_ID) {
|
|
202
|
+
resetProvider = true;
|
|
203
|
+
if (backup.model_provider != null) {
|
|
204
|
+
restoredProvider = backup.model_provider;
|
|
205
|
+
text = text.replace(PROVIDER_LINE_RE, `model_provider = "${backup.model_provider}"`);
|
|
206
|
+
} else {
|
|
207
|
+
// Remove the whole line (including its trailing newline).
|
|
208
|
+
text = text.replace(/^model_provider[ \t]*=[ \t]*"[^"]*"[ \t]*\n?/m, "");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const tidied = tidy(text);
|
|
213
|
+
const changed = tidied !== original;
|
|
214
|
+
if (changed) writeConfig(tidied);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
path: CODEX_CONFIG_PATH,
|
|
218
|
+
exists: true,
|
|
219
|
+
changed,
|
|
220
|
+
removedBlock,
|
|
221
|
+
resetProvider,
|
|
222
|
+
restoredProvider,
|
|
223
|
+
};
|
|
224
|
+
}
|