@yhong91/cpac 0.1.24 → 0.1.26
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 +28 -47
- package/dist/agents.js +784 -0
- package/dist/claude.js +373 -0
- package/dist/codex.js +378 -0
- package/dist/config.js +440 -0
- package/dist/cpac.js +157 -2252
- package/dist/proxy.js +341 -0
- package/dist/util.js +192 -0
- package/package.json +2 -3
package/dist/cpac.js
CHANGED
|
@@ -1,1364 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"api_key_env",
|
|
19
|
-
"codex_config",
|
|
20
|
-
"codex_proxy_port",
|
|
21
|
-
"state_dir",
|
|
22
|
-
"claude_models",
|
|
23
|
-
"spawn_models",
|
|
24
|
-
]);
|
|
25
|
-
// Codex advertises only the first 5 picker-visible catalog models as
|
|
26
|
-
// spawn_agent overrides (codex-rs MAX_SPAWN_AGENT_MODEL_OVERRIDES).
|
|
27
|
-
const MAX_SPAWN_MODELS = 5;
|
|
28
|
-
const CLAUDE_SLOT_KEYS = ["opus", "sonnet", "haiku"];
|
|
29
|
-
const REQUIRED_STATE_KEYS = new Set([
|
|
30
|
-
"config_path",
|
|
31
|
-
"config_existed",
|
|
32
|
-
"config_mode",
|
|
33
|
-
]);
|
|
34
|
-
const STATE_KEYS = new Set([
|
|
35
|
-
...REQUIRED_STATE_KEYS,
|
|
36
|
-
"proxy_id",
|
|
37
|
-
"proxy_fingerprint",
|
|
38
|
-
"proxy_pid",
|
|
39
|
-
"proxy_port",
|
|
40
|
-
]);
|
|
41
|
-
const STATE_FILES = ["state.json", "config.toml.backup", "codex-models.json"];
|
|
42
|
-
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
43
|
-
const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model_catalog_json')\s*=/;
|
|
44
|
-
const OPENAI_BASE_URL_KEY = /^\s*(?:openai_base_url|"openai_base_url"|'openai_base_url')\s*=/;
|
|
45
|
-
const ROOT_KEY = new RegExp(`(?:${MODEL_PROVIDER_KEY.source}|${MODEL_CATALOG_KEY.source}|${OPENAI_BASE_URL_KEY.source})`);
|
|
46
|
-
const MANAGED_MARKER = "# CPAC managed; run CPAC 'restore' to restore the original file.";
|
|
47
|
-
let atomicSequence = 0;
|
|
48
|
-
export class CPACError extends Error {
|
|
49
|
-
}
|
|
50
|
-
function objectValue(value) {
|
|
51
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
-
}
|
|
53
|
-
function expandUserPath(value) {
|
|
54
|
-
if (value === "~")
|
|
55
|
-
return homedir();
|
|
56
|
-
if (value.startsWith("~/") || value.startsWith("~\\"))
|
|
57
|
-
return join(homedir(), value.slice(2));
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
export function defaultConfigPath() {
|
|
61
|
-
return resolve(expandUserPath(process.env.CPAC_CONFIG || "~/.config/cpac/config.json"));
|
|
62
|
-
}
|
|
63
|
-
function normalizeCpaUrl(value) {
|
|
64
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
65
|
-
throw new CPACError("cpa_url must be a non-empty string");
|
|
66
|
-
}
|
|
67
|
-
let parsed;
|
|
68
|
-
try {
|
|
69
|
-
parsed = new URL(value);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
throw new CPACError("cpa_url must be an absolute http(s) URL");
|
|
73
|
-
}
|
|
74
|
-
if (!["http:", "https:"].includes(parsed.protocol) ||
|
|
75
|
-
!parsed.hostname ||
|
|
76
|
-
parsed.username ||
|
|
77
|
-
parsed.password ||
|
|
78
|
-
parsed.search ||
|
|
79
|
-
parsed.hash) {
|
|
80
|
-
throw new CPACError("cpa_url must be an absolute http(s) URL without credentials, query, or fragment");
|
|
81
|
-
}
|
|
82
|
-
return value.trim().replace(/\/+$/, "");
|
|
83
|
-
}
|
|
84
|
-
export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
85
|
-
let value;
|
|
86
|
-
try {
|
|
87
|
-
value = JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
if (error.code === "ENOENT" &&
|
|
91
|
-
useDefaultsIfMissing) {
|
|
92
|
-
value = {};
|
|
93
|
-
}
|
|
94
|
-
else if (error.code === "ENOENT") {
|
|
95
|
-
throw new CPACError(`config not found: ${path}`);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
throw new CPACError(`cannot read config: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (!objectValue(value))
|
|
102
|
-
throw new CPACError("config must be a JSON object");
|
|
103
|
-
const unknown = Object.keys(value)
|
|
104
|
-
.filter((key) => !CONFIG_KEYS.has(key))
|
|
105
|
-
.sort();
|
|
106
|
-
if (unknown.length)
|
|
107
|
-
throw new CPACError(`unknown config keys: ${unknown.join(", ")}`);
|
|
108
|
-
const cpaUrl = normalizeCpaUrl(value.cpa_url ?? process.env.CPA_BASE_URL?.trim() ?? DEFAULT_CPA_URL);
|
|
109
|
-
const apiKeyEnv = value.api_key_env ?? "CPA_API_KEY";
|
|
110
|
-
if (typeof apiKeyEnv !== "string" ||
|
|
111
|
-
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(apiKeyEnv)) {
|
|
112
|
-
throw new CPACError("api_key_env must be an environment variable name");
|
|
113
|
-
}
|
|
114
|
-
if (value.codex_config !== undefined &&
|
|
115
|
-
(typeof value.codex_config !== "string" || !value.codex_config.trim())) {
|
|
116
|
-
throw new CPACError("codex_config must be a non-empty string");
|
|
117
|
-
}
|
|
118
|
-
if (value.state_dir !== undefined &&
|
|
119
|
-
(typeof value.state_dir !== "string" || !value.state_dir.trim())) {
|
|
120
|
-
throw new CPACError("state_dir must be a non-empty string");
|
|
121
|
-
}
|
|
122
|
-
const codexProxyPort = value.codex_proxy_port ?? DEFAULT_CODEX_PROXY_PORT;
|
|
123
|
-
if (!Number.isInteger(codexProxyPort) ||
|
|
124
|
-
codexProxyPort < 0 ||
|
|
125
|
-
codexProxyPort > 65535) {
|
|
126
|
-
throw new CPACError("codex_proxy_port must be an integer from 0 to 65535");
|
|
127
|
-
}
|
|
128
|
-
const claudeModelsRaw = value.claude_models;
|
|
129
|
-
let claudeModels;
|
|
130
|
-
if (claudeModelsRaw !== undefined) {
|
|
131
|
-
if (!objectValue(claudeModelsRaw))
|
|
132
|
-
throw new CPACError("claude_models must be an object");
|
|
133
|
-
const unknownSlots = Object.keys(claudeModelsRaw)
|
|
134
|
-
// "classifier" was a shipped slot; its env var turned out unread by
|
|
135
|
-
// Claude Code, so the key is dropped on load instead of erroring.
|
|
136
|
-
.filter((key) => key !== "classifier" && !CLAUDE_SLOT_KEYS.includes(key))
|
|
137
|
-
.sort();
|
|
138
|
-
if (unknownSlots.length)
|
|
139
|
-
throw new CPACError(`unknown claude_models slots: ${unknownSlots.join(", ")}`);
|
|
140
|
-
claudeModels = {};
|
|
141
|
-
for (const slot of CLAUDE_SLOT_KEYS) {
|
|
142
|
-
const model = claudeModelsRaw[slot];
|
|
143
|
-
if (model === undefined)
|
|
144
|
-
continue;
|
|
145
|
-
if (typeof model !== "string" || !model.trim())
|
|
146
|
-
throw new CPACError(`claude_models.${slot} must be a non-empty string`);
|
|
147
|
-
claudeModels[slot] = model.trim();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const spawnModelsRaw = value.spawn_models;
|
|
151
|
-
let spawnModels;
|
|
152
|
-
if (spawnModelsRaw !== undefined) {
|
|
153
|
-
if (!Array.isArray(spawnModelsRaw))
|
|
154
|
-
throw new CPACError("spawn_models must be an array of model slugs");
|
|
155
|
-
if (spawnModelsRaw.length > MAX_SPAWN_MODELS)
|
|
156
|
-
throw new CPACError(`spawn_models accepts at most ${MAX_SPAWN_MODELS} models`);
|
|
157
|
-
spawnModels = spawnModelsRaw.map((model, index) => {
|
|
158
|
-
if (typeof model !== "string" || !model.trim())
|
|
159
|
-
throw new CPACError(`spawn_models[${index}] must be a non-empty string`);
|
|
160
|
-
return model.trim();
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
const codexHome = expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
164
|
-
const codexConfig = resolve(expandUserPath(value.codex_config || join(codexHome, "config.toml")));
|
|
165
|
-
const stateDir = resolve(expandUserPath(value.state_dir || join(homedir(), ".cpac")));
|
|
166
|
-
const stateToConfig = relative(stateDir, codexConfig);
|
|
167
|
-
if ([parse(stateDir).root, resolve(homedir()), resolve(tmpdir())].includes(stateDir) ||
|
|
168
|
-
stateToConfig === "" ||
|
|
169
|
-
(!stateToConfig.startsWith("..") && !isAbsolute(stateToConfig))) {
|
|
170
|
-
throw new CPACError("state_dir must not be a filesystem root, home/temp directory, or contain codex_config");
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
cpa_url: cpaUrl,
|
|
174
|
-
api_key_env: apiKeyEnv,
|
|
175
|
-
codex_config: codexConfig,
|
|
176
|
-
codex_proxy_port: codexProxyPort,
|
|
177
|
-
state_dir: stateDir,
|
|
178
|
-
claude_models: claudeModels,
|
|
179
|
-
spawn_models: spawnModels,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
export function apiBase(cpaUrl) {
|
|
183
|
-
try {
|
|
184
|
-
const url = new URL(cpaUrl);
|
|
185
|
-
return url.pathname.replace(/\/+$/, "").endsWith("/v1")
|
|
186
|
-
? cpaUrl
|
|
187
|
-
: `${cpaUrl}/v1`;
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
throw new CPACError("cpa_url must be an absolute http(s) URL");
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
export async function fetchCatalog(cpaUrl, apiKey) {
|
|
194
|
-
let response;
|
|
195
|
-
try {
|
|
196
|
-
response = await fetch(`${apiBase(cpaUrl)}/models?client_version=1`, {
|
|
197
|
-
headers: {
|
|
198
|
-
Authorization: `Bearer ${apiKey}`,
|
|
199
|
-
Accept: "application/json",
|
|
200
|
-
},
|
|
201
|
-
signal: AbortSignal.timeout(20_000),
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
throw new CPACError("CPA catalog request failed");
|
|
206
|
-
}
|
|
207
|
-
if (!response.ok)
|
|
208
|
-
throw new CPACError(`CPA catalog request failed: HTTP ${response.status}`);
|
|
209
|
-
const declaredLength = response.headers.get("content-length");
|
|
210
|
-
if (declaredLength &&
|
|
211
|
-
/^\d+$/.test(declaredLength) &&
|
|
212
|
-
Number(declaredLength) > MAX_CATALOG_BYTES) {
|
|
213
|
-
await response.body?.cancel();
|
|
214
|
-
throw new CPACError("CPA catalog exceeds 16 MiB");
|
|
215
|
-
}
|
|
216
|
-
const chunks = [];
|
|
217
|
-
let length = 0;
|
|
218
|
-
const reader = response.body?.getReader();
|
|
219
|
-
if (reader) {
|
|
220
|
-
while (true) {
|
|
221
|
-
let result;
|
|
222
|
-
try {
|
|
223
|
-
result = await reader.read();
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
throw new CPACError("CPA catalog request failed");
|
|
227
|
-
}
|
|
228
|
-
if (result.done)
|
|
229
|
-
break;
|
|
230
|
-
length += result.value.byteLength;
|
|
231
|
-
if (length > MAX_CATALOG_BYTES) {
|
|
232
|
-
await reader.cancel();
|
|
233
|
-
throw new CPACError("CPA catalog exceeds 16 MiB");
|
|
234
|
-
}
|
|
235
|
-
chunks.push(result.value);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const body = Buffer.concat(chunks, length);
|
|
239
|
-
let document;
|
|
240
|
-
try {
|
|
241
|
-
document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
throw new CPACError("CPA catalog is not valid JSON");
|
|
245
|
-
}
|
|
246
|
-
const models = objectValue(document) ? document.models : undefined;
|
|
247
|
-
if (!Array.isArray(models) || models.length === 0) {
|
|
248
|
-
throw new CPACError("CPA rich catalog must contain a non-empty models array");
|
|
249
|
-
}
|
|
250
|
-
if (models.some((model) => !objectValue(model) || typeof model.slug !== "string" || !model.slug.trim())) {
|
|
251
|
-
throw new CPACError("CPA catalog contains a model without a slug");
|
|
252
|
-
}
|
|
253
|
-
return {
|
|
254
|
-
bytes: Buffer.from(`${JSON.stringify(document, null, 2)}\n`),
|
|
255
|
-
modelCount: models.length,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
// Codex treats a catalog model's context_window as its input budget, not a
|
|
259
|
-
// display label; upstream keeps it a conservative operating cap while
|
|
260
|
-
// max_context_window holds the real ceiling (gpt-5.6: 272k vs ~921k, measured
|
|
261
|
-
// by opencodex). Opt-in lift: raise context_window to max_context_window and
|
|
262
|
-
// set auto_compact_token_limit at 90% (Codex's own convention), keeping
|
|
263
|
-
// compaction ahead of the hard ceiling.
|
|
264
|
-
export function liftContextWindows(bytes) {
|
|
265
|
-
const document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
266
|
-
for (const model of document.models) {
|
|
267
|
-
const maxWindow = typeof model.max_context_window === "number" && model.max_context_window > 0
|
|
268
|
-
? Math.floor(model.max_context_window)
|
|
269
|
-
: 0;
|
|
270
|
-
const window = typeof model.context_window === "number"
|
|
271
|
-
? Math.floor(model.context_window)
|
|
272
|
-
: 0;
|
|
273
|
-
if (maxWindow <= window)
|
|
274
|
-
continue;
|
|
275
|
-
model.context_window = maxWindow;
|
|
276
|
-
model.auto_compact_token_limit = Math.floor(maxWindow * 0.9);
|
|
277
|
-
}
|
|
278
|
-
return Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
279
|
-
}
|
|
280
|
-
// Move `order` slugs to the front of the rich catalog (stable for the rest),
|
|
281
|
-
// so the Codex client's first-5 spawn_agent advertisement picks them.
|
|
282
|
-
export function reorderCatalog(bytes, order) {
|
|
283
|
-
const document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
284
|
-
const rank = new Map(order.map((slug, index) => [slug, index]));
|
|
285
|
-
const models = document.models.map((model, index) => ({ model, index }));
|
|
286
|
-
models.sort((left, right) => {
|
|
287
|
-
const leftRank = rank.get(String(left.model.slug));
|
|
288
|
-
const rightRank = rank.get(String(right.model.slug));
|
|
289
|
-
if (leftRank === undefined && rightRank === undefined)
|
|
290
|
-
return left.index - right.index;
|
|
291
|
-
if (leftRank === undefined)
|
|
292
|
-
return 1;
|
|
293
|
-
if (rightRank === undefined)
|
|
294
|
-
return -1;
|
|
295
|
-
return leftRank - rightRank;
|
|
296
|
-
});
|
|
297
|
-
document.models = models.map((entry) => entry.model);
|
|
298
|
-
return Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
299
|
-
}
|
|
300
|
-
export function saveSpawnModels(configPath, models) {
|
|
301
|
-
let document = {};
|
|
302
|
-
if (existsSync(configPath)) {
|
|
303
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
304
|
-
if (!objectValue(parsed))
|
|
305
|
-
throw new CPACError("config must be a JSON object");
|
|
306
|
-
document = parsed;
|
|
307
|
-
}
|
|
308
|
-
document.spawn_models = models;
|
|
309
|
-
atomicWrite(configPath, Buffer.from(`${JSON.stringify(document, null, 2)}\n`));
|
|
310
|
-
}
|
|
311
|
-
// Arrow-key checkbox picker on raw stdin; resolves with the checked labels in
|
|
312
|
-
// check order (which becomes the spawn advertisement ranking).
|
|
313
|
-
async function checkboxPicker(title, items, max) {
|
|
314
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
315
|
-
throw new CPACError("model selection requires an interactive terminal");
|
|
316
|
-
}
|
|
317
|
-
const stdin = process.stdin;
|
|
318
|
-
const out = process.stderr;
|
|
319
|
-
const selected = new Set();
|
|
320
|
-
let cursor = 0;
|
|
321
|
-
const lines = items.length + 1;
|
|
322
|
-
const render = () => {
|
|
323
|
-
out.write(`${title} [space: toggle, up/down: move, enter: confirm, q: cancel, max ${max}]\n`);
|
|
324
|
-
items.forEach((item, index) => {
|
|
325
|
-
const box = selected.has(index) ? "[x]" : "[ ]";
|
|
326
|
-
out.write(`${index === cursor ? ">" : " "} ${box} ${item}\n`);
|
|
327
|
-
});
|
|
328
|
-
};
|
|
329
|
-
const redraw = () => {
|
|
330
|
-
out.write(`\x1b[${lines}F`);
|
|
331
|
-
for (let line = 0; line < lines; line += 1)
|
|
332
|
-
out.write("\x1b[2K\x1b[1E");
|
|
333
|
-
out.write(`\x1b[${lines}F`);
|
|
334
|
-
render();
|
|
335
|
-
};
|
|
336
|
-
return await new Promise((resolvePromise, rejectPromise) => {
|
|
337
|
-
const finish = (error) => {
|
|
338
|
-
stdin.removeListener("data", onData);
|
|
339
|
-
try {
|
|
340
|
-
stdin.setRawMode(false);
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Terminal already gone; nothing to restore.
|
|
344
|
-
}
|
|
345
|
-
stdin.pause();
|
|
346
|
-
out.write("\n");
|
|
347
|
-
if (error)
|
|
348
|
-
rejectPromise(error);
|
|
349
|
-
else
|
|
350
|
-
resolvePromise([...selected].map((index) => items[index]));
|
|
351
|
-
};
|
|
352
|
-
const onData = (chunk) => {
|
|
353
|
-
const key = chunk.toString("utf8");
|
|
354
|
-
if (key === "\x03" || key === "q" || key === "\x1b") {
|
|
355
|
-
finish(new CPACError("model selection cancelled"));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (key === "\r" || key === "\n") {
|
|
359
|
-
finish();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (key === "\x1b[A")
|
|
363
|
-
cursor = (cursor + items.length - 1) % items.length;
|
|
364
|
-
else if (key === "\x1b[B")
|
|
365
|
-
cursor = (cursor + 1) % items.length;
|
|
366
|
-
else if (key === " ") {
|
|
367
|
-
if (max === 1) {
|
|
368
|
-
selected.clear();
|
|
369
|
-
selected.add(cursor);
|
|
370
|
-
finish();
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (selected.has(cursor))
|
|
374
|
-
selected.delete(cursor);
|
|
375
|
-
else if (selected.size < max)
|
|
376
|
-
selected.add(cursor);
|
|
377
|
-
}
|
|
378
|
-
redraw();
|
|
379
|
-
};
|
|
380
|
-
stdin.setRawMode(true);
|
|
381
|
-
stdin.resume();
|
|
382
|
-
stdin.on("data", onData);
|
|
383
|
-
render();
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
export async function pickSpawnModels(config) {
|
|
387
|
-
const apiKey = process.env[config.api_key_env]?.trim();
|
|
388
|
-
if (!apiKey)
|
|
389
|
-
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
390
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
391
|
-
const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
392
|
-
const slugs = document.models.map((model) => model.slug);
|
|
393
|
-
const picked = await checkboxPicker("Select spawn_agent models", slugs, MAX_SPAWN_MODELS);
|
|
394
|
-
if (picked.length === 0)
|
|
395
|
-
throw new CPACError("no models selected");
|
|
396
|
-
return picked;
|
|
397
|
-
}
|
|
398
|
-
export function tomlString(value) {
|
|
399
|
-
return JSON.stringify(value);
|
|
400
|
-
}
|
|
401
|
-
function tomlStringArray(values) {
|
|
402
|
-
return `[ ${values.map(tomlString).join(", ")} ]`;
|
|
403
|
-
}
|
|
404
|
-
export function dominantEol(content) {
|
|
405
|
-
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
406
|
-
if (crlf === 0)
|
|
407
|
-
return "\n";
|
|
408
|
-
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
409
|
-
return crlf >= bareLf ? "\r\n" : "\n";
|
|
410
|
-
}
|
|
411
|
-
function providerPatterns(provider = PROVIDER) {
|
|
412
|
-
const escaped = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
413
|
-
const modelProviders = `(?:model_providers|"model_providers"|'model_providers')`;
|
|
414
|
-
const providerToken = `(?:${escaped}|"${escaped}"|'${escaped}')`;
|
|
415
|
-
return {
|
|
416
|
-
header: new RegExp(`^\\s*\\[\\s*${modelProviders}\\s*\\.\\s*${providerToken}\\s*\\]\\s*(?:#.*)?$`),
|
|
417
|
-
arrayHeader: new RegExp(`^\\s*\\[\\[\\s*${modelProviders}\\s*\\.\\s*${providerToken}\\s*\\]\\]\\s*(?:#.*)?$`),
|
|
418
|
-
parentHeader: new RegExp(`^\\s*\\[\\s*${modelProviders}\\s*\\]\\s*(?:#.*)?$`),
|
|
419
|
-
inlineKey: new RegExp(`^\\s*${providerToken}\\s*=`),
|
|
420
|
-
dottedKey: new RegExp(`^\\s*${modelProviders}\\s*\\.\\s*${providerToken}\\s*=`),
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
function providerHeader(provider = PROVIDER) {
|
|
424
|
-
return providerPatterns(provider).header;
|
|
425
|
-
}
|
|
426
|
-
export function hasProviderTable(content, provider = PROVIDER) {
|
|
427
|
-
const patterns = providerPatterns(provider);
|
|
428
|
-
let inRoot = true;
|
|
429
|
-
let inModelProviders = false;
|
|
430
|
-
for (const line of content.split(/\r?\n/)) {
|
|
431
|
-
if (patterns.header.test(line) || patterns.arrayHeader.test(line))
|
|
432
|
-
return true;
|
|
433
|
-
if (/^\s*\[/.test(line)) {
|
|
434
|
-
inRoot = false;
|
|
435
|
-
inModelProviders = patterns.parentHeader.test(line);
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if ((inModelProviders && patterns.inlineKey.test(line)) ||
|
|
439
|
-
(inRoot && patterns.dottedKey.test(line)))
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
function stripManagedConfig(content) {
|
|
445
|
-
let text;
|
|
446
|
-
try {
|
|
447
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
throw new CPACError("Codex config must be UTF-8");
|
|
451
|
-
}
|
|
452
|
-
const eol = dominantEol(text);
|
|
453
|
-
const header = providerHeader();
|
|
454
|
-
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
455
|
-
let inRoot = true;
|
|
456
|
-
let inProvider = false;
|
|
457
|
-
let managedRootKeys = 0;
|
|
458
|
-
const kept = [];
|
|
459
|
-
for (const line of lines) {
|
|
460
|
-
if (inProvider && /^\s*\[/.test(line))
|
|
461
|
-
inProvider = false;
|
|
462
|
-
if (!inProvider && header.test(line)) {
|
|
463
|
-
inProvider = true;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (inProvider)
|
|
467
|
-
continue;
|
|
468
|
-
if (/^\s*\[/.test(line))
|
|
469
|
-
inRoot = false;
|
|
470
|
-
if (inRoot && line === MANAGED_MARKER) {
|
|
471
|
-
managedRootKeys = 2;
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (inRoot && managedRootKeys > 0 && ROOT_KEY.test(line)) {
|
|
475
|
-
managedRootKeys -= 1;
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
kept.push(line);
|
|
479
|
-
}
|
|
480
|
-
const output = kept.join("\n");
|
|
481
|
-
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
482
|
-
}
|
|
483
|
-
export function buildCodexConfig(original, proxyPort, catalogPath) {
|
|
484
|
-
let text;
|
|
485
|
-
try {
|
|
486
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(original);
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
throw new CPACError("Codex config must be UTF-8");
|
|
490
|
-
}
|
|
491
|
-
if (hasProviderTable(text)) {
|
|
492
|
-
throw new CPACError(`Codex config already contains [model_providers.${PROVIDER}]`);
|
|
493
|
-
}
|
|
494
|
-
const normalizedLines = text.replace(/\r\n/g, "\n").split("\n");
|
|
495
|
-
const firstTable = normalizedLines.findIndex((line) => /^\s*\[/.test(line));
|
|
496
|
-
const rootLines = normalizedLines.slice(0, firstTable === -1 ? undefined : firstTable);
|
|
497
|
-
const providerLine = rootLines.find((line) => MODEL_PROVIDER_KEY.test(line));
|
|
498
|
-
if (providerLine) {
|
|
499
|
-
const match = /=\s*["']([^"']+)["']/.exec(providerLine);
|
|
500
|
-
if (!match || match[1] !== "openai") {
|
|
501
|
-
throw new CPACError("Codex config selects an external model_provider; restore it to openai before injecting CPAC");
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (rootLines.some((line) => OPENAI_BASE_URL_KEY.test(line))) {
|
|
505
|
-
throw new CPACError("Codex config already contains openai_base_url; remove that user-owned override before injecting CPAC");
|
|
506
|
-
}
|
|
507
|
-
const eol = dominantEol(text);
|
|
508
|
-
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
509
|
-
let inRoot = true;
|
|
510
|
-
const kept = lines.filter((line) => {
|
|
511
|
-
if (/^\s*\[/.test(line))
|
|
512
|
-
inRoot = false;
|
|
513
|
-
return !(inRoot && MODEL_CATALOG_KEY.test(line));
|
|
514
|
-
});
|
|
515
|
-
let body = kept.join("\n");
|
|
516
|
-
if (body && !body.endsWith("\n"))
|
|
517
|
-
body += "\n";
|
|
518
|
-
const root = [
|
|
519
|
-
MANAGED_MARKER,
|
|
520
|
-
`model_catalog_json = ${tomlString(catalogPath)}`,
|
|
521
|
-
`openai_base_url = ${tomlString(`http://127.0.0.1:${proxyPort}/v1`)}`,
|
|
522
|
-
"",
|
|
523
|
-
].join("\n");
|
|
524
|
-
const output = root + body;
|
|
525
|
-
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
526
|
-
}
|
|
527
|
-
// Codex multi-agent v2 (features.multi_agent_v2). Accepts the same TOML
|
|
528
|
-
// shapes codex-rs does: a [features.multi_agent_v2] table, an inline
|
|
529
|
-
// `multi_agent_v2 = ...` under [features], or dotted root keys.
|
|
530
|
-
const V2_TABLE_HEADER = /^\s*\[\s*(?:features|"features"|'features')\s*\.\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')\s*\]\s*(?:#.*)?$/;
|
|
531
|
-
const FEATURES_TABLE_HEADER = /^\s*\[\s*(?:features|"features"|'features')\s*\]\s*(?:#.*)?$/;
|
|
532
|
-
const V2_KEY = /^\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')\s*=/;
|
|
533
|
-
const V2_DOTTED_KEY = /^\s*(?:features|"features"|'features')\s*\.\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')(?:\s*\.\s*(?:enabled|"enabled"|'enabled'))?\s*=/;
|
|
534
|
-
function v2LineValue(line) {
|
|
535
|
-
const inline = line.match(/\{[^}]*\benabled\s*=\s*(true|false)/);
|
|
536
|
-
if (inline)
|
|
537
|
-
return inline[1] === "true";
|
|
538
|
-
const plain = line.match(/=\s*(true|false)(?![A-Za-z0-9_])/);
|
|
539
|
-
if (plain)
|
|
540
|
-
return plain[1] === "true";
|
|
541
|
-
return undefined;
|
|
542
|
-
}
|
|
543
|
-
export function multiAgentV2Enabled(content) {
|
|
544
|
-
let table = "root";
|
|
545
|
-
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
546
|
-
if (V2_TABLE_HEADER.test(line)) {
|
|
547
|
-
table = "v2";
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
if (FEATURES_TABLE_HEADER.test(line)) {
|
|
551
|
-
table = "features";
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
if (/^\s*\[/.test(line)) {
|
|
555
|
-
table = "other";
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
if (table === "v2" && /^\s*(?:enabled|"enabled"|'enabled')\s*=/.test(line)) {
|
|
559
|
-
return v2LineValue(line) ?? false;
|
|
560
|
-
}
|
|
561
|
-
if (table === "features" && V2_KEY.test(line))
|
|
562
|
-
return v2LineValue(line) ?? false;
|
|
563
|
-
if (table === "root" && V2_DOTTED_KEY.test(line))
|
|
564
|
-
return v2LineValue(line) ?? false;
|
|
565
|
-
}
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
function stripMultiAgentV2(content) {
|
|
569
|
-
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
570
|
-
const kept = [];
|
|
571
|
-
let skipTable = false;
|
|
572
|
-
let table = "root";
|
|
573
|
-
for (const line of lines) {
|
|
574
|
-
if (/^\s*\[/.test(line)) {
|
|
575
|
-
skipTable = V2_TABLE_HEADER.test(line);
|
|
576
|
-
if (skipTable) {
|
|
577
|
-
table = "other";
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
table = FEATURES_TABLE_HEADER.test(line) ? "features" : "other";
|
|
581
|
-
}
|
|
582
|
-
if (skipTable)
|
|
583
|
-
continue;
|
|
584
|
-
if (table === "features" && V2_KEY.test(line))
|
|
585
|
-
continue;
|
|
586
|
-
if (table === "root" && V2_DOTTED_KEY.test(line))
|
|
587
|
-
continue;
|
|
588
|
-
kept.push(line);
|
|
589
|
-
}
|
|
590
|
-
return kept.join("\n");
|
|
591
|
-
}
|
|
592
|
-
function hasAgentsMaxThreads(content) {
|
|
593
|
-
let table = "root";
|
|
594
|
-
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
595
|
-
if (/^\s*\[/.test(line)) {
|
|
596
|
-
table = /^\s*\[\s*(?:agents|"agents"|'agents')\s*\]\s*(?:#.*)?$/.test(line)
|
|
597
|
-
? "agents"
|
|
598
|
-
: "other";
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (table === "agents" && /^\s*max_threads\s*=/.test(line))
|
|
602
|
-
return true;
|
|
603
|
-
if (table === "root" &&
|
|
604
|
-
/^\s*(?:agents|"agents"|'agents')\s*\.\s*max_threads\s*=/.test(line))
|
|
605
|
-
return true;
|
|
606
|
-
}
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
export function withMultiAgentV2(content, enabled, eol) {
|
|
610
|
-
let body = stripMultiAgentV2(content);
|
|
611
|
-
if (body && !body.endsWith("\n"))
|
|
612
|
-
body += "\n";
|
|
613
|
-
const output = body + `\n[features.multi_agent_v2]\nenabled = ${enabled}\n`;
|
|
614
|
-
return eol === "\n" ? output : output.replace(/\n/g, "\r\n");
|
|
615
|
-
}
|
|
616
|
-
export function atomicWrite(path, data, mode = 0o600) {
|
|
617
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
618
|
-
const temporary = `${path}.cpac.${process.pid}.${Date.now()}.${++atomicSequence}.tmp`;
|
|
619
|
-
let descriptor;
|
|
620
|
-
try {
|
|
621
|
-
descriptor = openSync(temporary, "wx", 0o600);
|
|
622
|
-
writeFileSync(descriptor, data);
|
|
623
|
-
fsyncSync(descriptor);
|
|
624
|
-
closeSync(descriptor);
|
|
625
|
-
descriptor = undefined;
|
|
626
|
-
chmodSync(temporary, mode);
|
|
627
|
-
renameSync(temporary, path);
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
if (descriptor !== undefined) {
|
|
631
|
-
try {
|
|
632
|
-
closeSync(descriptor);
|
|
633
|
-
}
|
|
634
|
-
catch (closeError) {
|
|
635
|
-
void closeError;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
try {
|
|
639
|
-
unlinkSync(temporary);
|
|
640
|
-
}
|
|
641
|
-
catch (unlinkError) {
|
|
642
|
-
void unlinkError;
|
|
643
|
-
}
|
|
644
|
-
throw error;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
const HOP_BY_HOP_HEADERS = new Set([
|
|
648
|
-
"connection",
|
|
649
|
-
"keep-alive",
|
|
650
|
-
"proxy-authenticate",
|
|
651
|
-
"proxy-authorization",
|
|
652
|
-
"proxy-connection",
|
|
653
|
-
"te",
|
|
654
|
-
"trailer",
|
|
655
|
-
"transfer-encoding",
|
|
656
|
-
"upgrade",
|
|
657
|
-
]);
|
|
658
|
-
const CLIENT_CREDENTIAL_HEADERS = new Set([
|
|
659
|
-
"authorization",
|
|
660
|
-
"chatgpt-account-id",
|
|
661
|
-
"cookie",
|
|
662
|
-
"openai-organization",
|
|
663
|
-
"openai-project",
|
|
664
|
-
"x-api-key",
|
|
665
|
-
"x-opencodex-api-key",
|
|
666
|
-
]);
|
|
667
|
-
function localBrowserOrigin(value) {
|
|
668
|
-
if (!value)
|
|
669
|
-
return true;
|
|
670
|
-
try {
|
|
671
|
-
const hostname = new URL(value).hostname.toLowerCase();
|
|
672
|
-
return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1");
|
|
673
|
-
}
|
|
674
|
-
catch {
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
function proxyHeaders(headers, apiKey) {
|
|
679
|
-
const forwarded = {};
|
|
680
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
681
|
-
if (value === undefined ||
|
|
682
|
-
name === "host" ||
|
|
683
|
-
CLIENT_CREDENTIAL_HEADERS.has(name) ||
|
|
684
|
-
HOP_BY_HOP_HEADERS.has(name)) {
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
forwarded[name] = value;
|
|
688
|
-
}
|
|
689
|
-
forwarded.authorization = `Bearer ${apiKey}`;
|
|
690
|
-
return forwarded;
|
|
691
|
-
}
|
|
692
|
-
function responseHeaders(headers) {
|
|
693
|
-
const forwarded = {};
|
|
694
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
695
|
-
if (value === undefined || HOP_BY_HOP_HEADERS.has(name))
|
|
696
|
-
continue;
|
|
697
|
-
forwarded[name] = value;
|
|
698
|
-
}
|
|
699
|
-
return forwarded;
|
|
700
|
-
}
|
|
701
|
-
function upstreamUrl(cpaUrl, requestUrl) {
|
|
702
|
-
const incoming = new URL(requestUrl, "http://127.0.0.1");
|
|
703
|
-
const suffix = incoming.pathname === "/v1"
|
|
704
|
-
? ""
|
|
705
|
-
: incoming.pathname.startsWith("/v1/")
|
|
706
|
-
? incoming.pathname.slice(3)
|
|
707
|
-
: incoming.pathname;
|
|
708
|
-
return new URL(`${apiBase(cpaUrl)}${suffix}${incoming.search}`);
|
|
709
|
-
}
|
|
710
|
-
export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
711
|
-
const server = createServer((request, response) => {
|
|
712
|
-
const requestUrl = request.url ?? "/";
|
|
713
|
-
if (requestUrl === "/_cpac/health") {
|
|
714
|
-
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
715
|
-
response.writeHead(404).end();
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
response.writeHead(204).end();
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
if (requestUrl === "/_cpac/shutdown" && request.method === "POST") {
|
|
722
|
-
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
723
|
-
response.writeHead(404).end();
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
response.writeHead(204).end();
|
|
727
|
-
setImmediate(() => {
|
|
728
|
-
server.closeAllConnections?.();
|
|
729
|
-
server.close();
|
|
730
|
-
});
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (!localBrowserOrigin(request.headers.origin)) {
|
|
734
|
-
response.writeHead(403, { "content-type": "application/json" });
|
|
735
|
-
response.end(JSON.stringify({ error: "non-local origin rejected" }));
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
let target;
|
|
739
|
-
try {
|
|
740
|
-
target = upstreamUrl(cpaUrl, requestUrl);
|
|
741
|
-
}
|
|
742
|
-
catch {
|
|
743
|
-
response.writeHead(400).end("invalid request URL");
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
747
|
-
const upstream = send(target, {
|
|
748
|
-
method: request.method,
|
|
749
|
-
headers: proxyHeaders(request.headers, apiKey),
|
|
750
|
-
}, (upstreamResponse) => {
|
|
751
|
-
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
752
|
-
upstreamResponse.pipe(response);
|
|
753
|
-
});
|
|
754
|
-
upstream.once("error", () => {
|
|
755
|
-
if (!response.headersSent) {
|
|
756
|
-
response.writeHead(502, { "content-type": "application/json" });
|
|
757
|
-
}
|
|
758
|
-
if (!response.writableEnded)
|
|
759
|
-
response.end(JSON.stringify({ error: "CPA upstream request failed" }));
|
|
760
|
-
});
|
|
761
|
-
request.once("aborted", () => upstream.destroy());
|
|
762
|
-
response.once("close", () => {
|
|
763
|
-
if (!response.writableEnded)
|
|
764
|
-
upstream.destroy();
|
|
765
|
-
});
|
|
766
|
-
request.pipe(upstream);
|
|
767
|
-
});
|
|
768
|
-
server.on("upgrade", (_request, socket) => {
|
|
769
|
-
socket.end("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
770
|
-
});
|
|
771
|
-
server.on("clientError", (_error, socket) => socket.destroy());
|
|
772
|
-
await new Promise((resolveListen, rejectListen) => {
|
|
773
|
-
server.once("error", rejectListen);
|
|
774
|
-
server.listen(port, "127.0.0.1", () => {
|
|
775
|
-
server.off("error", rejectListen);
|
|
776
|
-
resolveListen();
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
const address = server.address();
|
|
780
|
-
if (!address || typeof address === "string") {
|
|
781
|
-
server.close();
|
|
782
|
-
throw new CPACError("cannot determine loopback proxy port");
|
|
783
|
-
}
|
|
784
|
-
return { server, port: address.port };
|
|
785
|
-
}
|
|
786
|
-
const CLAUDE_ALIAS_PREFIX = "claude-cpac--";
|
|
787
|
-
// Claude Code accepts CLAUDE_CODE_AUTO_COMPACT_WINDOW in 100K–1M (binary-verified).
|
|
788
|
-
// A single global window cannot be per-model; 350K is opencodex's user-approved
|
|
789
|
-
// default — high enough to mark mid-size models (372K/500K), low enough that
|
|
790
|
-
// marking never outgrows a model's real window.
|
|
791
|
-
const ONE_MILLION = 1_000_000;
|
|
792
|
-
const COMPACT_WINDOW_DEFAULT = 350_000;
|
|
793
|
-
function claudeCompactWindow(maxContext) {
|
|
794
|
-
return maxContext > 200_000 ? COMPACT_WINDOW_DEFAULT : undefined;
|
|
795
|
-
}
|
|
796
|
-
// [1m] marking predicate (opencodex rule): native claude models manage their own
|
|
797
|
-
// marker; aliases mark when the real window is >= 1M, or when it clears 200K and
|
|
798
|
-
// can host the compact window (marking below it would trip API limits mid-session).
|
|
799
|
-
function shouldMarkOneMillion(id, window, compactWindow) {
|
|
800
|
-
if (id.startsWith("claude"))
|
|
801
|
-
return false;
|
|
802
|
-
if (window >= ONE_MILLION)
|
|
803
|
-
return true;
|
|
804
|
-
return (compactWindow !== undefined && window > 200_000 && window >= compactWindow);
|
|
805
|
-
}
|
|
806
|
-
// Auto tier defaults. Within the claude-family pool (whole catalog when no
|
|
807
|
-
// claude model exists), opus matches its family name or stays unset; sonnet
|
|
808
|
-
// and haiku match their family name first, then fall back through
|
|
809
|
-
// gemini → grok → luna → first catalog row.
|
|
810
|
-
function defaultClaudeSlots(models) {
|
|
811
|
-
if (!models.length)
|
|
812
|
-
return undefined;
|
|
813
|
-
const claudeRows = models.filter((model) => model.id.startsWith("claude"));
|
|
814
|
-
const pool = claudeRows.length ? claudeRows : models;
|
|
815
|
-
const family = (keyword) => pool.find((model) => model.id.includes(keyword));
|
|
816
|
-
const fallback = (keyword) => family(keyword) ??
|
|
817
|
-
models.find((model) => model.id.includes("gemini")) ??
|
|
818
|
-
models.find((model) => model.id.includes("grok")) ??
|
|
819
|
-
models.find((model) => model.id.includes("luna")) ??
|
|
820
|
-
models[0];
|
|
821
|
-
const slots = {
|
|
822
|
-
sonnet: fallback("sonnet").id,
|
|
823
|
-
haiku: fallback("haiku").id,
|
|
824
|
-
};
|
|
825
|
-
const opus = family("opus");
|
|
826
|
-
if (opus)
|
|
827
|
-
slots.opus = opus.id;
|
|
828
|
-
return slots;
|
|
829
|
-
}
|
|
830
|
-
// Resolve the tier-model env values: config overrides win over catalog
|
|
831
|
-
// defaults; values are [1m]-marked per the predicate above (Claude Code
|
|
832
|
-
// strips the marker before the request leaves).
|
|
833
|
-
export function claudeTierSlots(override, models, compactWindow) {
|
|
834
|
-
const defaults = defaultClaudeSlots(models);
|
|
835
|
-
if (!defaults)
|
|
836
|
-
return undefined;
|
|
837
|
-
const windowById = new Map(models.map((model) => [model.id, model.window]));
|
|
838
|
-
const mark = (id) => !/\[1m\]$/i.test(id) &&
|
|
839
|
-
shouldMarkOneMillion(id, windowById.get(id) ?? 0, compactWindow)
|
|
840
|
-
? `${id}[1m]`
|
|
841
|
-
: id;
|
|
842
|
-
const slots = {
|
|
843
|
-
sonnet: mark(override?.sonnet ?? defaults.sonnet),
|
|
844
|
-
haiku: mark(override?.haiku ?? defaults.haiku),
|
|
845
|
-
};
|
|
846
|
-
const opus = override?.opus ?? defaults.opus;
|
|
847
|
-
if (opus)
|
|
848
|
-
slots.opus = mark(opus);
|
|
849
|
-
return slots;
|
|
850
|
-
}
|
|
851
|
-
function catalogModelRows(document) {
|
|
852
|
-
if (!objectValue(document))
|
|
853
|
-
return undefined;
|
|
854
|
-
const source = Array.isArray(document.models)
|
|
855
|
-
? document.models
|
|
856
|
-
: Array.isArray(document.data)
|
|
857
|
-
? document.data
|
|
858
|
-
: undefined;
|
|
859
|
-
if (!source)
|
|
860
|
-
return undefined;
|
|
861
|
-
return source.filter(objectValue);
|
|
862
|
-
}
|
|
863
|
-
function catalogModelId(model) {
|
|
864
|
-
if (typeof model.slug === "string" && model.slug.trim())
|
|
865
|
-
return model.slug.trim();
|
|
866
|
-
if (typeof model.id === "string" && model.id.trim())
|
|
867
|
-
return model.id.trim();
|
|
868
|
-
return undefined;
|
|
869
|
-
}
|
|
870
|
-
async function claudeModelList(cpaUrl, apiKey, response) {
|
|
871
|
-
let rows;
|
|
872
|
-
try {
|
|
873
|
-
const catalog = await fetch(`${apiBase(cpaUrl)}/models?client_version=1`, {
|
|
874
|
-
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
875
|
-
signal: AbortSignal.timeout(20_000),
|
|
876
|
-
});
|
|
877
|
-
if (catalog.ok)
|
|
878
|
-
rows = catalogModelRows(await catalog.json());
|
|
879
|
-
}
|
|
880
|
-
catch {
|
|
881
|
-
rows = undefined;
|
|
882
|
-
}
|
|
883
|
-
if (!rows) {
|
|
884
|
-
response.writeHead(502, { "content-type": "application/json" });
|
|
885
|
-
response.end(JSON.stringify({ error: "CPA catalog request failed" }));
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
// Claude Code's /model picker only lists ids starting with claude/anthropic;
|
|
889
|
-
// expose every other catalog model as claude-cpac--<id>. Append the [1m]
|
|
890
|
-
// marker when the model can host the auto-compact window — that marker is
|
|
891
|
-
// the only signal that lifts context accounting above the 200K default
|
|
892
|
-
// (Claude Code strips it before the request reaches the proxy).
|
|
893
|
-
const rowsWithWindow = [];
|
|
894
|
-
let maxContext = 0;
|
|
895
|
-
for (const model of rows) {
|
|
896
|
-
const id = catalogModelId(model);
|
|
897
|
-
if (!id)
|
|
898
|
-
continue;
|
|
899
|
-
const window = typeof model.context_window === "number" && model.context_window > 0
|
|
900
|
-
? Math.floor(model.context_window)
|
|
901
|
-
: 0;
|
|
902
|
-
if (window > maxContext)
|
|
903
|
-
maxContext = window;
|
|
904
|
-
rowsWithWindow.push({
|
|
905
|
-
id,
|
|
906
|
-
window,
|
|
907
|
-
alias: id.startsWith("claude") ? id : `${CLAUDE_ALIAS_PREFIX}${id}`,
|
|
908
|
-
display: typeof model.display_name === "string" && model.display_name.trim()
|
|
909
|
-
? model.display_name
|
|
910
|
-
: id,
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
const compactWindow = claudeCompactWindow(maxContext);
|
|
914
|
-
const data = rowsWithWindow.map((row) => ({
|
|
915
|
-
type: "model",
|
|
916
|
-
id: shouldMarkOneMillion(row.id, row.window, compactWindow)
|
|
917
|
-
? `${row.alias}[1m]`
|
|
918
|
-
: row.alias,
|
|
919
|
-
display_name: row.display,
|
|
920
|
-
}));
|
|
921
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
922
|
-
response.end(JSON.stringify({
|
|
923
|
-
data,
|
|
924
|
-
has_more: false,
|
|
925
|
-
first_id: data.length > 0 ? data[0].id : null,
|
|
926
|
-
last_id: data.length > 0 ? data[data.length - 1].id : null,
|
|
927
|
-
}));
|
|
928
|
-
}
|
|
929
|
-
export async function createClaudeProxy(cpaUrl, apiKey) {
|
|
930
|
-
const server = createServer((request, response) => {
|
|
931
|
-
const requestUrl = request.url ?? "/";
|
|
932
|
-
if (request.method === "GET" && requestUrl.startsWith("/v1/models")) {
|
|
933
|
-
void claudeModelList(cpaUrl, apiKey, response);
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
let target;
|
|
937
|
-
try {
|
|
938
|
-
target = upstreamUrl(cpaUrl, requestUrl);
|
|
939
|
-
}
|
|
940
|
-
catch {
|
|
941
|
-
response.writeHead(400).end("invalid request URL");
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const chunks = [];
|
|
945
|
-
request.on("data", (chunk) => chunks.push(chunk));
|
|
946
|
-
request.once("error", () => response.destroy());
|
|
947
|
-
request.once("end", () => {
|
|
948
|
-
let body = Buffer.concat(chunks);
|
|
949
|
-
if (body.length > 0) {
|
|
950
|
-
try {
|
|
951
|
-
const parsed = JSON.parse(body.toString("utf8"));
|
|
952
|
-
if (objectValue(parsed) &&
|
|
953
|
-
typeof parsed.model === "string" &&
|
|
954
|
-
parsed.model.startsWith(CLAUDE_ALIAS_PREFIX)) {
|
|
955
|
-
// Claude Code strips [1m] itself; keep the strip as a guard.
|
|
956
|
-
parsed.model = parsed.model
|
|
957
|
-
.slice(CLAUDE_ALIAS_PREFIX.length)
|
|
958
|
-
.replace(/\[(1|2)m\]$/i, "");
|
|
959
|
-
body = Buffer.from(JSON.stringify(parsed));
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
catch {
|
|
963
|
-
// not JSON; forward unchanged
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
const headers = proxyHeaders(request.headers, apiKey);
|
|
967
|
-
headers["content-length"] = String(body.length);
|
|
968
|
-
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
969
|
-
const upstream = send(target, { method: request.method, headers }, (upstreamResponse) => {
|
|
970
|
-
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
971
|
-
upstreamResponse.pipe(response);
|
|
972
|
-
});
|
|
973
|
-
upstream.once("error", () => {
|
|
974
|
-
if (!response.headersSent) {
|
|
975
|
-
response.writeHead(502, { "content-type": "application/json" });
|
|
976
|
-
}
|
|
977
|
-
if (!response.writableEnded)
|
|
978
|
-
response.end(JSON.stringify({ error: "CPA upstream request failed" }));
|
|
979
|
-
});
|
|
980
|
-
request.once("aborted", () => upstream.destroy());
|
|
981
|
-
response.once("close", () => {
|
|
982
|
-
if (!response.writableEnded)
|
|
983
|
-
upstream.destroy();
|
|
984
|
-
});
|
|
985
|
-
upstream.end(body);
|
|
986
|
-
});
|
|
987
|
-
});
|
|
988
|
-
server.on("clientError", (_error, socket) => socket.destroy());
|
|
989
|
-
await new Promise((resolveListen, rejectListen) => {
|
|
990
|
-
server.once("error", rejectListen);
|
|
991
|
-
server.listen(0, "127.0.0.1", () => {
|
|
992
|
-
server.off("error", rejectListen);
|
|
993
|
-
resolveListen();
|
|
994
|
-
});
|
|
995
|
-
});
|
|
996
|
-
const address = server.address();
|
|
997
|
-
if (!address || typeof address === "string") {
|
|
998
|
-
server.close();
|
|
999
|
-
throw new CPACError("cannot determine Claude proxy port");
|
|
1000
|
-
}
|
|
1001
|
-
return { server, port: address.port };
|
|
1002
|
-
}
|
|
1003
|
-
async function runProxyChild(port) {
|
|
1004
|
-
const cpaUrl = process.env.CPAC_PROXY_UPSTREAM;
|
|
1005
|
-
const apiKey = process.env.CPAC_PROXY_API_KEY;
|
|
1006
|
-
const proxyId = process.env.CPAC_PROXY_ID;
|
|
1007
|
-
if (!cpaUrl || !apiKey || !proxyId)
|
|
1008
|
-
return 1;
|
|
1009
|
-
try {
|
|
1010
|
-
const proxy = await createLoopbackProxy(cpaUrl, apiKey, proxyId, port);
|
|
1011
|
-
process.send?.({ ready: true, port: proxy.port, pid: process.pid });
|
|
1012
|
-
const close = () => {
|
|
1013
|
-
proxy.server.closeAllConnections?.();
|
|
1014
|
-
proxy.server.close();
|
|
1015
|
-
};
|
|
1016
|
-
process.once("SIGINT", close);
|
|
1017
|
-
process.once("SIGTERM", close);
|
|
1018
|
-
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
1019
|
-
return 0;
|
|
1020
|
-
}
|
|
1021
|
-
catch (error) {
|
|
1022
|
-
process.send?.({
|
|
1023
|
-
ready: false,
|
|
1024
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1025
|
-
});
|
|
1026
|
-
return 1;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
async function proxyIsHealthy(proxy) {
|
|
1030
|
-
try {
|
|
1031
|
-
const response = await fetch(`http://127.0.0.1:${proxy.port}/_cpac/health`, {
|
|
1032
|
-
headers: { "x-cpac-proxy-id": proxy.id },
|
|
1033
|
-
signal: AbortSignal.timeout(1_000),
|
|
1034
|
-
});
|
|
1035
|
-
return response.status === 204;
|
|
1036
|
-
}
|
|
1037
|
-
catch {
|
|
1038
|
-
return false;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
async function startProxyProcess(config, apiKey) {
|
|
1042
|
-
const id = randomBytes(24).toString("hex");
|
|
1043
|
-
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "_proxy", String(config.codex_proxy_port)], {
|
|
1044
|
-
detached: true,
|
|
1045
|
-
env: {
|
|
1046
|
-
...process.env,
|
|
1047
|
-
CPAC_PROXY_API_KEY: apiKey,
|
|
1048
|
-
CPAC_PROXY_ID: id,
|
|
1049
|
-
CPAC_PROXY_UPSTREAM: config.cpa_url,
|
|
1050
|
-
},
|
|
1051
|
-
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
1052
|
-
});
|
|
1053
|
-
return await new Promise((resolveStart, rejectStart) => {
|
|
1054
|
-
let settled = false;
|
|
1055
|
-
let timeout;
|
|
1056
|
-
const finish = (error, value) => {
|
|
1057
|
-
if (settled)
|
|
1058
|
-
return;
|
|
1059
|
-
settled = true;
|
|
1060
|
-
clearTimeout(timeout);
|
|
1061
|
-
child.removeAllListeners();
|
|
1062
|
-
if (child.connected)
|
|
1063
|
-
child.disconnect();
|
|
1064
|
-
child.unref();
|
|
1065
|
-
if (error)
|
|
1066
|
-
rejectStart(error);
|
|
1067
|
-
else
|
|
1068
|
-
resolveStart(value);
|
|
1069
|
-
};
|
|
1070
|
-
timeout = setTimeout(() => {
|
|
1071
|
-
child.kill();
|
|
1072
|
-
finish(new CPACError("loopback proxy did not become ready"));
|
|
1073
|
-
}, 5_000);
|
|
1074
|
-
child.once("error", (error) => finish(new CPACError(`cannot start loopback proxy: ${error.message}`)));
|
|
1075
|
-
child.once("exit", (code) => finish(new CPACError(`loopback proxy exited before ready (${code ?? 1})`)));
|
|
1076
|
-
child.once("message", (message) => {
|
|
1077
|
-
if (objectValue(message) &&
|
|
1078
|
-
message.ready === true &&
|
|
1079
|
-
typeof message.pid === "number" &&
|
|
1080
|
-
typeof message.port === "number") {
|
|
1081
|
-
finish(undefined, { id, pid: message.pid, port: message.port });
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
const detail = objectValue(message) && typeof message.error === "string"
|
|
1085
|
-
? `: ${message.error}`
|
|
1086
|
-
: "";
|
|
1087
|
-
finish(new CPACError(`cannot start loopback proxy${detail}`));
|
|
1088
|
-
});
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
async function stopProxyProcess(proxy) {
|
|
1092
|
-
if (!(await proxyIsHealthy(proxy)))
|
|
1093
|
-
return;
|
|
1094
|
-
try {
|
|
1095
|
-
await fetch(`http://127.0.0.1:${proxy.port}/_cpac/shutdown`, {
|
|
1096
|
-
method: "POST",
|
|
1097
|
-
headers: { "x-cpac-proxy-id": proxy.id },
|
|
1098
|
-
signal: AbortSignal.timeout(1_000),
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
catch {
|
|
1102
|
-
// The server may close the connection while shutting down.
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
function readState(stateDir) {
|
|
1106
|
-
const path = join(stateDir, "state.json");
|
|
1107
|
-
if (!existsSync(path))
|
|
1108
|
-
return null;
|
|
1109
|
-
let value;
|
|
1110
|
-
try {
|
|
1111
|
-
value = JSON.parse(readFileSync(path, "utf8"));
|
|
1112
|
-
}
|
|
1113
|
-
catch (error) {
|
|
1114
|
-
throw new CPACError(`cannot read state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1115
|
-
}
|
|
1116
|
-
if (!objectValue(value) ||
|
|
1117
|
-
Object.keys(value).some((key) => !STATE_KEYS.has(key)) ||
|
|
1118
|
-
[...REQUIRED_STATE_KEYS].some((key) => !(key in value))) {
|
|
1119
|
-
throw new CPACError("invalid state file");
|
|
1120
|
-
}
|
|
1121
|
-
if (typeof value.config_path !== "string" || !isAbsolute(value.config_path)) {
|
|
1122
|
-
throw new CPACError("invalid config path in state");
|
|
1123
|
-
}
|
|
1124
|
-
if (typeof value.config_existed !== "boolean")
|
|
1125
|
-
throw new CPACError("invalid config_existed value in state");
|
|
1126
|
-
if (!Number.isInteger(value.config_mode) ||
|
|
1127
|
-
value.config_mode < 0 ||
|
|
1128
|
-
value.config_mode > 0o7777) {
|
|
1129
|
-
throw new CPACError("invalid config_mode value in state");
|
|
1130
|
-
}
|
|
1131
|
-
const proxyKeys = ["proxy_id", "proxy_pid", "proxy_port"];
|
|
1132
|
-
const proxyKeyCount = proxyKeys.filter((key) => key in value).length;
|
|
1133
|
-
if (proxyKeyCount !== 0 &&
|
|
1134
|
-
(proxyKeyCount !== proxyKeys.length ||
|
|
1135
|
-
typeof value.proxy_id !== "string" ||
|
|
1136
|
-
!/^[a-f0-9]{48}$/.test(value.proxy_id) ||
|
|
1137
|
-
!Number.isInteger(value.proxy_pid) ||
|
|
1138
|
-
value.proxy_pid <= 0 ||
|
|
1139
|
-
!Number.isInteger(value.proxy_port) ||
|
|
1140
|
-
value.proxy_port <= 0 ||
|
|
1141
|
-
value.proxy_port > 65535)) {
|
|
1142
|
-
throw new CPACError("invalid loopback proxy state");
|
|
1143
|
-
}
|
|
1144
|
-
if (value.proxy_fingerprint !== undefined &&
|
|
1145
|
-
(typeof value.proxy_fingerprint !== "string" ||
|
|
1146
|
-
!/^[a-f0-9]{64}$/.test(value.proxy_fingerprint))) {
|
|
1147
|
-
throw new CPACError("invalid loopback proxy fingerprint");
|
|
1148
|
-
}
|
|
1149
|
-
return value;
|
|
1150
|
-
}
|
|
1151
|
-
function stateProxy(state) {
|
|
1152
|
-
return state.proxy_id && state.proxy_pid && state.proxy_port
|
|
1153
|
-
? { id: state.proxy_id, pid: state.proxy_pid, port: state.proxy_port }
|
|
1154
|
-
: null;
|
|
1155
|
-
}
|
|
1156
|
-
function stateBytes(config, existed, mode, proxy, proxyFingerprint) {
|
|
1157
|
-
return Buffer.from(`${JSON.stringify({
|
|
1158
|
-
config_path: config.codex_config,
|
|
1159
|
-
config_existed: existed,
|
|
1160
|
-
config_mode: mode,
|
|
1161
|
-
proxy_id: proxy.id,
|
|
1162
|
-
proxy_fingerprint: proxyFingerprint,
|
|
1163
|
-
proxy_pid: proxy.pid,
|
|
1164
|
-
proxy_port: proxy.port,
|
|
1165
|
-
}, null, 2)}\n`);
|
|
1166
|
-
}
|
|
1167
|
-
function originalBytes(stateDir, state, expectedPath) {
|
|
1168
|
-
if (state.config_path !== expectedPath) {
|
|
1169
|
-
throw new CPACError(`active injection belongs to ${state.config_path}; restore it first`);
|
|
1170
|
-
}
|
|
1171
|
-
if (!state.config_existed)
|
|
1172
|
-
return new Uint8Array();
|
|
1173
|
-
try {
|
|
1174
|
-
return readFileSync(join(stateDir, "config.toml.backup"));
|
|
1175
|
-
}
|
|
1176
|
-
catch (error) {
|
|
1177
|
-
throw new CPACError(`cannot read original config backup: ${error instanceof Error ? error.message : String(error)}`);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
function cleanupStateFiles(stateDir) {
|
|
1181
|
-
for (const name of STATE_FILES)
|
|
1182
|
-
rmSync(join(stateDir, name), { force: true });
|
|
1183
|
-
try {
|
|
1184
|
-
rmdirSync(stateDir);
|
|
1185
|
-
}
|
|
1186
|
-
catch (error) {
|
|
1187
|
-
const code = error.code;
|
|
1188
|
-
if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST")
|
|
1189
|
-
throw error;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
function proxyFingerprint(config, apiKey) {
|
|
1193
|
-
return createHash("sha256")
|
|
1194
|
-
.update(config.cpa_url)
|
|
1195
|
-
.update("\0")
|
|
1196
|
-
.update(apiKey)
|
|
1197
|
-
.digest("hex");
|
|
1198
|
-
}
|
|
1199
|
-
export async function inject(config, v2Off = false, maxContext = false) {
|
|
1200
|
-
const apiKey = process.env[config.api_key_env]?.trim();
|
|
1201
|
-
if (!apiKey)
|
|
1202
|
-
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
1203
|
-
const state = readState(config.state_dir);
|
|
1204
|
-
let existed;
|
|
1205
|
-
let original;
|
|
1206
|
-
let originalMode;
|
|
1207
|
-
if (state) {
|
|
1208
|
-
const backup = originalBytes(config.state_dir, state, config.codex_config);
|
|
1209
|
-
try {
|
|
1210
|
-
original = existsSync(config.codex_config)
|
|
1211
|
-
? stripManagedConfig(readFileSync(config.codex_config))
|
|
1212
|
-
: backup;
|
|
1213
|
-
}
|
|
1214
|
-
catch (error) {
|
|
1215
|
-
if (error instanceof CPACError)
|
|
1216
|
-
throw error;
|
|
1217
|
-
throw new CPACError(`cannot read Codex config: ${error instanceof Error ? error.message : String(error)}`);
|
|
1218
|
-
}
|
|
1219
|
-
existed = state.config_existed;
|
|
1220
|
-
originalMode = state.config_mode;
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
existed = existsSync(config.codex_config);
|
|
1224
|
-
try {
|
|
1225
|
-
original = existed ? readFileSync(config.codex_config) : new Uint8Array();
|
|
1226
|
-
originalMode = existed ? statSync(config.codex_config).mode & 0o7777 : 0o600;
|
|
1227
|
-
}
|
|
1228
|
-
catch (error) {
|
|
1229
|
-
throw new CPACError(`cannot read Codex config: ${error instanceof Error ? error.message : String(error)}`);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1233
|
-
if (config.spawn_models?.length)
|
|
1234
|
-
catalog.bytes = reorderCatalog(catalog.bytes, config.spawn_models);
|
|
1235
|
-
if (maxContext)
|
|
1236
|
-
catalog.bytes = liftContextWindows(catalog.bytes);
|
|
1237
|
-
const stateDirExisted = existsSync(config.state_dir);
|
|
1238
|
-
if (!state &&
|
|
1239
|
-
STATE_FILES.some((name) => existsSync(join(config.state_dir, name)))) {
|
|
1240
|
-
throw new CPACError("state_dir contains CPAC files without a valid state; refusing to overwrite them");
|
|
1241
|
-
}
|
|
1242
|
-
const fingerprint = proxyFingerprint(config, apiKey);
|
|
1243
|
-
const previousProxy = state ? stateProxy(state) : null;
|
|
1244
|
-
let proxy;
|
|
1245
|
-
let startedProxy = false;
|
|
1246
|
-
if (previousProxy &&
|
|
1247
|
-
state?.proxy_fingerprint === fingerprint &&
|
|
1248
|
-
(config.codex_proxy_port === 0 ||
|
|
1249
|
-
config.codex_proxy_port === previousProxy.port) &&
|
|
1250
|
-
(await proxyIsHealthy(previousProxy))) {
|
|
1251
|
-
proxy = previousProxy;
|
|
1252
|
-
}
|
|
1253
|
-
else {
|
|
1254
|
-
if (previousProxy)
|
|
1255
|
-
await stopProxyProcess(previousProxy);
|
|
1256
|
-
try {
|
|
1257
|
-
proxy = await startProxyProcess(config, apiKey);
|
|
1258
|
-
startedProxy = true;
|
|
1259
|
-
}
|
|
1260
|
-
catch (error) {
|
|
1261
|
-
throw new CPACError(`${error instanceof Error ? error.message : String(error)}; choose another codex_proxy_port if the port is occupied`);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
1265
|
-
let injected;
|
|
1266
|
-
try {
|
|
1267
|
-
injected = buildCodexConfig(original, proxy.port, catalogPath);
|
|
1268
|
-
const injectedText = new TextDecoder("utf-8").decode(injected);
|
|
1269
|
-
injected = Buffer.from(withMultiAgentV2(injectedText, !v2Off, dominantEol(injectedText)));
|
|
1270
|
-
if (!v2Off && hasAgentsMaxThreads(injectedText)) {
|
|
1271
|
-
console.warn("warning: [agents] max_threads is set; codex refuses to start with multi_agent_v2 enabled, remove it");
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
catch (error) {
|
|
1275
|
-
if (startedProxy)
|
|
1276
|
-
await stopProxyProcess(proxy);
|
|
1277
|
-
throw error;
|
|
1278
|
-
}
|
|
1279
|
-
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
1280
|
-
if (!stateDirExisted)
|
|
1281
|
-
chmodSync(config.state_dir, 0o700);
|
|
1282
|
-
if (state) {
|
|
1283
|
-
try {
|
|
1284
|
-
atomicWrite(catalogPath, catalog.bytes);
|
|
1285
|
-
atomicWrite(config.codex_config, injected, originalMode);
|
|
1286
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
1287
|
-
}
|
|
1288
|
-
catch (error) {
|
|
1289
|
-
if (startedProxy)
|
|
1290
|
-
await stopProxyProcess(proxy);
|
|
1291
|
-
throw error;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
else {
|
|
1295
|
-
let configWritten = false;
|
|
1296
|
-
try {
|
|
1297
|
-
if (existed)
|
|
1298
|
-
atomicWrite(join(config.state_dir, "config.toml.backup"), original);
|
|
1299
|
-
atomicWrite(catalogPath, catalog.bytes);
|
|
1300
|
-
atomicWrite(config.codex_config, injected, originalMode);
|
|
1301
|
-
configWritten = true;
|
|
1302
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
1303
|
-
}
|
|
1304
|
-
catch (error) {
|
|
1305
|
-
if (configWritten) {
|
|
1306
|
-
try {
|
|
1307
|
-
if (existed)
|
|
1308
|
-
atomicWrite(config.codex_config, original, originalMode);
|
|
1309
|
-
else
|
|
1310
|
-
rmSync(config.codex_config, { force: true });
|
|
1311
|
-
}
|
|
1312
|
-
catch (rollbackError) {
|
|
1313
|
-
throw new CPACError(`injection failed and config rollback failed; backup retained in ${config.state_dir}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
let cleanupError;
|
|
1317
|
-
try {
|
|
1318
|
-
cleanupStateFiles(config.state_dir);
|
|
1319
|
-
}
|
|
1320
|
-
catch (caught) {
|
|
1321
|
-
cleanupError = caught;
|
|
1322
|
-
}
|
|
1323
|
-
if (startedProxy)
|
|
1324
|
-
await stopProxyProcess(proxy);
|
|
1325
|
-
if (cleanupError) {
|
|
1326
|
-
throw new CPACError(`injection failed and state cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
1327
|
-
}
|
|
1328
|
-
throw error;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
try {
|
|
1332
|
-
rmSync(join(dirname(config.codex_config), "models_cache.json"), {
|
|
1333
|
-
force: true,
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
catch {
|
|
1337
|
-
console.warn("CPAC could not invalidate Codex models_cache.json; restart Codex App if its model list is stale.");
|
|
1338
|
-
}
|
|
1339
|
-
console.log(`Injected ${catalog.modelCount} CPA models into ${config.codex_config} via http://127.0.0.1:${proxy.port}/v1 (multi-agent v2: ${v2Off ? "off" : "on"})`);
|
|
1340
|
-
console.log("Restart Codex App if its running app-server still shows the old model list.");
|
|
1341
|
-
}
|
|
1342
|
-
export async function restore(config) {
|
|
1343
|
-
const state = readState(config.state_dir);
|
|
1344
|
-
if (!state)
|
|
1345
|
-
throw new CPACError("no active CPAC injection");
|
|
1346
|
-
const original = originalBytes(config.state_dir, state, config.codex_config);
|
|
1347
|
-
if (state.config_existed)
|
|
1348
|
-
atomicWrite(config.codex_config, original, state.config_mode);
|
|
1349
|
-
else
|
|
1350
|
-
rmSync(config.codex_config, { force: true });
|
|
1351
|
-
const proxy = stateProxy(state);
|
|
1352
|
-
if (proxy)
|
|
1353
|
-
await stopProxyProcess(proxy);
|
|
1354
|
-
try {
|
|
1355
|
-
cleanupStateFiles(config.state_dir);
|
|
1356
|
-
}
|
|
1357
|
-
catch (error) {
|
|
1358
|
-
throw new CPACError(`config restored, but state cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1359
|
-
}
|
|
1360
|
-
console.log(`Restored ${config.codex_config}`);
|
|
1361
|
-
}
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { CPAC_VERSION, isGrokConfigInstalled, isKimiConfigInstalled, isPiExtensionInstalled, runCodexConfig, runDetect, runInstall, runOpencode, runRestore, runSync, runTargetLauncher, runUpgrade, } from "./agents.js";
|
|
7
|
+
import { runClaude, runClaudeConfig, runClaudeModels } from "./claude.js";
|
|
8
|
+
import { inject, multiAgentV2Enabled } from "./codex.js";
|
|
9
|
+
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, } from "./config.js";
|
|
10
|
+
import { proxyIsHealthy, runProxy, runProxyChild } from "./proxy.js";
|
|
11
|
+
import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
|
|
12
|
+
export { PROVIDER, loadConfig, saveSpawnModels, liftContextWindows, reorderCatalog, readState, stateProxy, } from "./config.js";
|
|
13
|
+
export { saveApiKeyExport } from "./util.js";
|
|
14
|
+
export { stopProxyProcess } from "./proxy.js";
|
|
15
|
+
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, withMultiAgentV2, } from "./codex.js";
|
|
16
|
+
export { claudeTierSlots, createClaudeProxy, runClaude } from "./claude.js";
|
|
17
|
+
export { detectClientVersion, detectTargets, installGrokConfig, installKimiConfig, installPiExtension, isGrokConfigInstalled, isKimiConfigInstalled, isPiExtensionInstalled, runInstall, runOpencode, runRestore, runSync, runTargetLauncher, uninstallGrokConfig, uninstallKimiConfig, uninstallPiExtension, } from "./agents.js";
|
|
1362
18
|
export async function status(config) {
|
|
1363
19
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
1364
20
|
const keyConfigured = !!apiKey;
|
|
@@ -1405,62 +61,8 @@ export async function status(config) {
|
|
|
1405
61
|
return 2;
|
|
1406
62
|
return 0;
|
|
1407
63
|
}
|
|
1408
|
-
async function promptSecret(name) {
|
|
1409
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
1410
|
-
throw new CPACError(`environment variable ${name} is not set; run: export ${name}="..."`);
|
|
1411
|
-
}
|
|
1412
|
-
process.stderr.write(`Enter ${name}: `);
|
|
1413
|
-
const silent = new Writable({
|
|
1414
|
-
write(_chunk, _encoding, callback) {
|
|
1415
|
-
callback();
|
|
1416
|
-
},
|
|
1417
|
-
});
|
|
1418
|
-
const prompt = createInterface({
|
|
1419
|
-
input: process.stdin,
|
|
1420
|
-
output: silent,
|
|
1421
|
-
terminal: true,
|
|
1422
|
-
});
|
|
1423
|
-
try {
|
|
1424
|
-
const value = (await prompt.question("")).trim();
|
|
1425
|
-
process.stderr.write("\n");
|
|
1426
|
-
if (!value)
|
|
1427
|
-
throw new CPACError(`${name} must not be empty`);
|
|
1428
|
-
return value;
|
|
1429
|
-
}
|
|
1430
|
-
finally {
|
|
1431
|
-
prompt.close();
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
async function resolveApiKey(name) {
|
|
1435
|
-
return process.env[name]?.trim() || (await promptSecret(name));
|
|
1436
|
-
}
|
|
1437
|
-
function shellProfile() {
|
|
1438
|
-
const shell = parse(process.env.SHELL || "").base;
|
|
1439
|
-
if (shell === "zsh")
|
|
1440
|
-
return join(homedir(), ".zshrc");
|
|
1441
|
-
if (shell === "bash")
|
|
1442
|
-
return join(homedir(), process.platform === "darwin" ? ".bash_profile" : ".bashrc");
|
|
1443
|
-
if (["sh", "dash", "ksh"].includes(shell))
|
|
1444
|
-
return join(homedir(), ".profile");
|
|
1445
|
-
throw new CPACError(`unsupported shell ${shell || "unknown"}; run: export CPA_API_KEY="..."`);
|
|
1446
|
-
}
|
|
1447
|
-
function shellQuote(value) {
|
|
1448
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1449
|
-
}
|
|
1450
|
-
export function saveApiKeyExport(profile, name, apiKey) {
|
|
1451
|
-
const start = `# >>> CPAC ${name} >>>`;
|
|
1452
|
-
const end = `# <<< CPAC ${name} <<<`;
|
|
1453
|
-
const block = `${start}\nexport ${name}=${shellQuote(apiKey)}\n${end}`;
|
|
1454
|
-
let content = existsSync(profile) ? readFileSync(profile, "utf8") : "";
|
|
1455
|
-
const managed = new RegExp(`${start.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
1456
|
-
content = managed.test(content)
|
|
1457
|
-
? content.replace(managed, block)
|
|
1458
|
-
: `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
1459
|
-
const mode = existsSync(profile) ? statSync(profile).mode & 0o7777 : 0o600;
|
|
1460
|
-
atomicWrite(profile, Buffer.from(content), mode);
|
|
1461
|
-
}
|
|
1462
64
|
async function guide(config) {
|
|
1463
|
-
console.log(`CPA Companion\n\nCPA: ${config.cpa_url}\nCPA_API_KEY: ${process.env[config.api_key_env]?.trim() ? "configured" : "not configured"}\n\nCommands:\n cpac
|
|
65
|
+
console.log(`CPA Companion\n\nCPA: ${config.cpa_url}\nCPA_API_KEY: ${process.env[config.api_key_env]?.trim() ? "configured" : "not configured"}\n\nCommands:\n cpac <agent> [args...] Launch an agent (codex, kimi, grok, pi, claude, opencode) through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi, Kimi, Grok)\n cpac restore Restore the original Codex config\n cpac detect Show supported targets and install status\n cpac install Install CPA integration into detected targets\n cpac sync Refresh installed Codex/Pi/Kimi/Grok injections from CPA\n cpac uninstall Remove CPA integration from installed targets\n cpac upgrade Update cpac from npm and sync installed agents\n cpac --help Show command usage`);
|
|
1464
66
|
if (process.env[config.api_key_env]?.trim())
|
|
1465
67
|
return 0;
|
|
1466
68
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -1472,819 +74,24 @@ async function guide(config) {
|
|
|
1472
74
|
}
|
|
1473
75
|
// opencode's config schema rejects a limit block with context but no output;
|
|
1474
76
|
// the catalog has no output field, so a safe budget stands in, clamped to the
|
|
1475
|
-
// context window (same scheme as opencodex's SCHEMA_REQUIRED_OUTPUT_BUDGET).
|
|
1476
|
-
const OPENCODE_OUTPUT_BUDGET = 32_000;
|
|
1477
|
-
// Ephemeral takeover: inline a CPA provider via OPENCODE_CONFIG_CONTENT so no
|
|
1478
|
-
// opencode config file is touched; the session ends with zero residue.
|
|
1479
|
-
export async function runOpencode(config, args, executable = "opencode") {
|
|
1480
|
-
const apiKey = process.env[config.api_key_env]?.trim();
|
|
1481
|
-
if (!apiKey)
|
|
1482
|
-
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
1483
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1484
|
-
const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
1485
|
-
const models = {};
|
|
1486
|
-
for (const row of document.models) {
|
|
1487
|
-
const id = catalogModelId(row);
|
|
1488
|
-
if (!id)
|
|
1489
|
-
continue;
|
|
1490
|
-
const window = typeof row.context_window === "number" && row.context_window > 0
|
|
1491
|
-
? Math.floor(row.context_window)
|
|
1492
|
-
: 0;
|
|
1493
|
-
models[id] =
|
|
1494
|
-
window > 0
|
|
1495
|
-
? {
|
|
1496
|
-
limit: {
|
|
1497
|
-
context: window,
|
|
1498
|
-
output: Math.min(OPENCODE_OUTPUT_BUDGET, window),
|
|
1499
|
-
},
|
|
1500
|
-
}
|
|
1501
|
-
: {};
|
|
1502
|
-
}
|
|
1503
|
-
const content = JSON.stringify({
|
|
1504
|
-
provider: {
|
|
1505
|
-
cpac: {
|
|
1506
|
-
npm: "@ai-sdk/openai-compatible",
|
|
1507
|
-
options: { baseURL: apiBase(config.cpa_url), apiKey },
|
|
1508
|
-
models,
|
|
1509
|
-
},
|
|
1510
|
-
},
|
|
1511
|
-
});
|
|
1512
|
-
const env = {
|
|
1513
|
-
...process.env,
|
|
1514
|
-
OPENCODE_CONFIG_CONTENT: content,
|
|
1515
|
-
};
|
|
1516
|
-
return await new Promise((resolve, reject) => {
|
|
1517
|
-
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
1518
|
-
child.once("error", (error) => {
|
|
1519
|
-
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
1520
|
-
? `${executable} not found`
|
|
1521
|
-
: `cannot start ${executable}: ${error.message}`));
|
|
1522
|
-
});
|
|
1523
|
-
child.once("close", (code) => resolve(code ?? 1));
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
export async function runClaudeModels(parsed) {
|
|
1527
|
-
if (parsed.pick.length) {
|
|
1528
|
-
const config = loadConfig(parsed.configPath, true);
|
|
1529
|
-
const apiKey = process.env[config.api_key_env]?.trim();
|
|
1530
|
-
if (!apiKey)
|
|
1531
|
-
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
1532
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1533
|
-
const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
1534
|
-
const slugs = document.models.map((model) => model.slug);
|
|
1535
|
-
for (const slot of parsed.pick) {
|
|
1536
|
-
const [picked] = await checkboxPicker(`Select ${slot} model`, slugs, 1);
|
|
1537
|
-
if (!picked)
|
|
1538
|
-
throw new CPACError("no model selected");
|
|
1539
|
-
parsed.models[slot] = picked;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (!parsed.reset && !Object.keys(parsed.models).length) {
|
|
1543
|
-
const config = loadConfig(parsed.configPath, true);
|
|
1544
|
-
console.log(config.claude_models
|
|
1545
|
-
? JSON.stringify(config.claude_models, null, 2)
|
|
1546
|
-
: "claude_models not set; cpac claude auto-picks tier models from the CPA catalog");
|
|
1547
|
-
return 0;
|
|
1548
|
-
}
|
|
1549
|
-
let raw = {};
|
|
1550
|
-
try {
|
|
1551
|
-
const file = JSON.parse(readFileSync(parsed.configPath, "utf8"));
|
|
1552
|
-
if (!objectValue(file))
|
|
1553
|
-
throw new CPACError("config must be a JSON object");
|
|
1554
|
-
raw = file;
|
|
1555
|
-
}
|
|
1556
|
-
catch (error) {
|
|
1557
|
-
if (error.code !== "ENOENT") {
|
|
1558
|
-
if (error instanceof CPACError)
|
|
1559
|
-
throw error;
|
|
1560
|
-
throw new CPACError(`cannot read config: ${error instanceof Error ? error.message : String(error)}`);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
if (parsed.reset) {
|
|
1564
|
-
delete raw.claude_models;
|
|
1565
|
-
}
|
|
1566
|
-
else {
|
|
1567
|
-
const existing = objectValue(raw.claude_models)
|
|
1568
|
-
? { ...raw.claude_models }
|
|
1569
|
-
: {};
|
|
1570
|
-
Object.assign(existing, parsed.models);
|
|
1571
|
-
raw.claude_models = existing;
|
|
1572
|
-
}
|
|
1573
|
-
atomicWrite(parsed.configPath, Buffer.from(`${JSON.stringify(raw, null, 2)}\n`));
|
|
1574
|
-
loadConfig(parsed.configPath);
|
|
1575
|
-
console.log(parsed.reset
|
|
1576
|
-
? "claude_models cleared"
|
|
1577
|
-
: `claude_models updated: ${JSON.stringify(raw.claude_models)}`);
|
|
1578
|
-
return 0;
|
|
1579
|
-
}
|
|
1580
|
-
export async function runClaude(config, args, executable = "claude") {
|
|
1581
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1582
|
-
const proxy = await createClaudeProxy(config.cpa_url, apiKey);
|
|
1583
|
-
const stopProxy = () => {
|
|
1584
|
-
proxy.server.closeAllConnections?.();
|
|
1585
|
-
proxy.server.close();
|
|
1586
|
-
};
|
|
1587
|
-
// Gateway discovery only carries {id, display_name}; Claude Code accounts
|
|
1588
|
-
// claude-prefixed unknown models at 200K and ignores MAX_CONTEXT_TOKENS for
|
|
1589
|
-
// them. The [1m] marker on the alias (added by the models listing) plus the
|
|
1590
|
-
// auto-compact window is opencodex's working mechanism for bigger windows.
|
|
1591
|
-
let maxContext = 0;
|
|
1592
|
-
const catalogWindows = [];
|
|
1593
|
-
try {
|
|
1594
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1595
|
-
const rows = catalogModelRows(JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes))) ?? [];
|
|
1596
|
-
for (const row of rows) {
|
|
1597
|
-
const id = catalogModelId(row);
|
|
1598
|
-
const window = typeof row.context_window === "number" && row.context_window > 0
|
|
1599
|
-
? Math.floor(row.context_window)
|
|
1600
|
-
: 0;
|
|
1601
|
-
if (id)
|
|
1602
|
-
catalogWindows.push({ id, window });
|
|
1603
|
-
if (window > maxContext)
|
|
1604
|
-
maxContext = window;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
catch {
|
|
1608
|
-
// keep the default window when the catalog is unreachable
|
|
1609
|
-
}
|
|
1610
|
-
const compactWindow = claudeCompactWindow(maxContext);
|
|
1611
|
-
const slots = claudeTierSlots(config.claude_models, catalogWindows, compactWindow);
|
|
1612
|
-
const env = {
|
|
1613
|
-
...process.env,
|
|
1614
|
-
ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxy.port}`,
|
|
1615
|
-
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
1616
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
1617
|
-
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
|
|
1618
|
-
};
|
|
1619
|
-
if (compactWindow !== undefined &&
|
|
1620
|
-
!(Number(process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW) > 0)) {
|
|
1621
|
-
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(compactWindow);
|
|
1622
|
-
}
|
|
1623
|
-
const setSlot = (name, value) => {
|
|
1624
|
-
if (process.env[name] === undefined)
|
|
1625
|
-
env[name] = value;
|
|
1626
|
-
};
|
|
1627
|
-
if (slots) {
|
|
1628
|
-
// Route haiku/subagent tier traffic at catalog models instead of the
|
|
1629
|
-
// stock claude-haiku-* ids the gateway does not carry. The opus slot
|
|
1630
|
-
// stays unset when the catalog has no opus-family model.
|
|
1631
|
-
if (slots.opus)
|
|
1632
|
-
setSlot("ANTHROPIC_DEFAULT_OPUS_MODEL", slots.opus);
|
|
1633
|
-
setSlot("ANTHROPIC_DEFAULT_SONNET_MODEL", slots.sonnet);
|
|
1634
|
-
setSlot("ANTHROPIC_DEFAULT_HAIKU_MODEL", slots.haiku);
|
|
1635
|
-
setSlot("ANTHROPIC_SMALL_FAST_MODEL", slots.haiku);
|
|
1636
|
-
}
|
|
1637
|
-
delete env.ANTHROPIC_API_KEY;
|
|
1638
|
-
delete env.CLAUDE_CODE_USE_ANTHROPIC_AWS;
|
|
1639
|
-
delete env.CLAUDE_CODE_USE_BEDROCK;
|
|
1640
|
-
delete env.CLAUDE_CODE_USE_FOUNDRY;
|
|
1641
|
-
delete env.CLAUDE_CODE_USE_VERTEX;
|
|
1642
|
-
return await new Promise((resolve, reject) => {
|
|
1643
|
-
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
1644
|
-
child.once("error", (error) => {
|
|
1645
|
-
stopProxy();
|
|
1646
|
-
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
1647
|
-
? `${executable} not found`
|
|
1648
|
-
: `cannot start ${executable}: ${error.message}`));
|
|
1649
|
-
});
|
|
1650
|
-
child.once("close", (code) => {
|
|
1651
|
-
stopProxy();
|
|
1652
|
-
resolve(code ?? 1);
|
|
1653
|
-
});
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
export async function runProxy(config) {
|
|
1657
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1658
|
-
const state = readState(config.state_dir);
|
|
1659
|
-
if (!state)
|
|
1660
|
-
throw new CPACError("no active CPAC injection; run cpac inject first");
|
|
1661
|
-
originalBytes(config.state_dir, state, config.codex_config);
|
|
1662
|
-
const recorded = stateProxy(state);
|
|
1663
|
-
if (!recorded) {
|
|
1664
|
-
throw new CPACError("injection has no loopback proxy state; run cpac inject to migrate it");
|
|
1665
|
-
}
|
|
1666
|
-
if (await proxyIsHealthy(recorded)) {
|
|
1667
|
-
throw new CPACError(`loopback proxy is already running on 127.0.0.1:${recorded.port}`);
|
|
1668
|
-
}
|
|
1669
|
-
const proxy = await createLoopbackProxy(config.cpa_url, apiKey, recorded.id, recorded.port);
|
|
1670
|
-
try {
|
|
1671
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey)));
|
|
1672
|
-
}
|
|
1673
|
-
catch (error) {
|
|
1674
|
-
proxy.server.closeAllConnections?.();
|
|
1675
|
-
proxy.server.close();
|
|
1676
|
-
throw error;
|
|
1677
|
-
}
|
|
1678
|
-
console.log(`CPAC loopback proxy listening on http://127.0.0.1:${proxy.port}/v1`);
|
|
1679
|
-
const close = () => {
|
|
1680
|
-
proxy.server.closeAllConnections?.();
|
|
1681
|
-
proxy.server.close();
|
|
1682
|
-
};
|
|
1683
|
-
process.once("SIGINT", close);
|
|
1684
|
-
process.once("SIGTERM", close);
|
|
1685
|
-
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
1686
|
-
return 0;
|
|
1687
|
-
}
|
|
1688
|
-
function piExtensionsDir() {
|
|
1689
|
-
return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
|
|
1690
|
-
}
|
|
1691
|
-
export function isPiExtensionInstalled() {
|
|
1692
|
-
return existsSync(join(piExtensionsDir(), "cpac.ts"));
|
|
1693
|
-
}
|
|
1694
|
-
function piTemplatePath() {
|
|
1695
|
-
return join(dirname(fileURLToPath(import.meta.url)), "pi-extension.template");
|
|
1696
|
-
}
|
|
1697
|
-
function piExtensionContent(cpaUrl) {
|
|
1698
|
-
return readFileSync(piTemplatePath(), "utf8").replace("__CPA_URL__", cpaUrl);
|
|
1699
|
-
}
|
|
1700
|
-
export async function installPiExtension(config) {
|
|
1701
|
-
const dir = piExtensionsDir();
|
|
1702
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1703
|
-
const target = join(dir, "cpac.ts");
|
|
1704
|
-
ensureCpacBackup(target);
|
|
1705
|
-
atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
|
|
1706
|
-
console.log(`Installed Pi extension: ${target}`);
|
|
1707
|
-
}
|
|
1708
|
-
export async function uninstallPiExtension() {
|
|
1709
|
-
const target = join(piExtensionsDir(), "cpac.ts");
|
|
1710
|
-
if (!existsSync(target))
|
|
1711
|
-
throw new CPACError("Pi extension is not installed");
|
|
1712
|
-
unlinkSync(target);
|
|
1713
|
-
console.log(`Removed Pi extension: ${target}`);
|
|
1714
|
-
}
|
|
1715
|
-
// Pre-write snapshot kept at <target>.cpac-backup (first version only) so a
|
|
1716
|
-
// damaged config can be restored by hand even if uninstall cannot parse it.
|
|
1717
|
-
function ensureCpacBackup(target) {
|
|
1718
|
-
if (!existsSync(target))
|
|
1719
|
-
return;
|
|
1720
|
-
const backup = `${target}.cpac-backup`;
|
|
1721
|
-
if (existsSync(backup))
|
|
1722
|
-
return;
|
|
1723
|
-
copyFileSync(target, backup);
|
|
1724
|
-
}
|
|
1725
|
-
function kimiConfigPath() {
|
|
1726
|
-
const home = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
|
|
1727
|
-
return join(expandUserPath(home), "config.toml");
|
|
1728
|
-
}
|
|
1729
|
-
const KIMI_BLOCK_START = "# >>> CPAC Kimi >>>";
|
|
1730
|
-
const KIMI_BLOCK_END = "# <<< CPAC Kimi <<<";
|
|
1731
|
-
const kimiBlockRegex = new RegExp(`${KIMI_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${KIMI_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
1732
|
-
export function isKimiConfigInstalled() {
|
|
1733
|
-
try {
|
|
1734
|
-
const content = readFileSync(kimiConfigPath(), "utf8");
|
|
1735
|
-
return (kimiBlockRegex.test(content) || /^\s*\[providers\.cpac\]\s*$/m.test(content));
|
|
1736
|
-
}
|
|
1737
|
-
catch {
|
|
1738
|
-
return false;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
function isCpacTableHeader(line) {
|
|
1742
|
-
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
1743
|
-
if (!header)
|
|
1744
|
-
return false;
|
|
1745
|
-
const name = header[1];
|
|
1746
|
-
return (name === "providers.cpac" ||
|
|
1747
|
-
name.startsWith("providers.cpac.") ||
|
|
1748
|
-
/^models\.(?:"cpac\/|'cpac\/)/.test(name));
|
|
1749
|
-
}
|
|
1750
|
-
// Kimi may rewrite config.toml and drop our comment markers, leaving the
|
|
1751
|
-
// [providers.cpac] / [models."cpac/..."] tables behind. A later install would
|
|
1752
|
-
// then append a second copy and make the file invalid TOML.
|
|
1753
|
-
function stripOrphanCpacTables(content) {
|
|
1754
|
-
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
1755
|
-
const out = [];
|
|
1756
|
-
let skipping = false;
|
|
1757
|
-
for (const line of content.split(/\r?\n/)) {
|
|
1758
|
-
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
1759
|
-
skipping = isCpacTableHeader(line);
|
|
1760
|
-
if (!skipping)
|
|
1761
|
-
out.push(line);
|
|
1762
|
-
}
|
|
1763
|
-
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
1764
|
-
}
|
|
1765
|
-
function writeKimiBlock(path, block) {
|
|
1766
|
-
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
1767
|
-
content = content.replace(kimiBlockRegex, "");
|
|
1768
|
-
// A truncated managed block (start marker without the end marker) leaves its
|
|
1769
|
-
// tables behind; appending again would duplicate [providers.cpac], make the
|
|
1770
|
-
// file invalid TOML, and block `kimi login`. The block is always appended
|
|
1771
|
-
// last, so dropping from an orphaned start marker to EOF is safe.
|
|
1772
|
-
const orphan = content.indexOf(KIMI_BLOCK_START);
|
|
1773
|
-
if (orphan !== -1)
|
|
1774
|
-
content = content.slice(0, orphan);
|
|
1775
|
-
content = stripOrphanCpacTables(content);
|
|
1776
|
-
if (block) {
|
|
1777
|
-
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
1778
|
-
}
|
|
1779
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
1780
|
-
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
1781
|
-
atomicWrite(path, Buffer.from(content), mode);
|
|
1782
|
-
}
|
|
1783
|
-
const KIMI_REASONING_EFFORTS = new Set([
|
|
1784
|
-
"minimal",
|
|
1785
|
-
"low",
|
|
1786
|
-
"medium",
|
|
1787
|
-
"high",
|
|
1788
|
-
"xhigh",
|
|
1789
|
-
"max",
|
|
1790
|
-
"ultra",
|
|
1791
|
-
]);
|
|
1792
|
-
function kimiSupportEfforts(row) {
|
|
1793
|
-
const levels = row.supported_reasoning_levels ??
|
|
1794
|
-
row.reasoning_effort_levels ??
|
|
1795
|
-
row.reasoning_levels;
|
|
1796
|
-
if (!Array.isArray(levels))
|
|
1797
|
-
return [];
|
|
1798
|
-
const seen = new Set();
|
|
1799
|
-
const efforts = [];
|
|
1800
|
-
for (const level of levels) {
|
|
1801
|
-
const effort = typeof level === "string"
|
|
1802
|
-
? level.toLowerCase()
|
|
1803
|
-
: objectValue(level) && typeof level.effort === "string"
|
|
1804
|
-
? level.effort.toLowerCase()
|
|
1805
|
-
: undefined;
|
|
1806
|
-
if (!effort || !KIMI_REASONING_EFFORTS.has(effort) || seen.has(effort))
|
|
1807
|
-
continue;
|
|
1808
|
-
seen.add(effort);
|
|
1809
|
-
efforts.push(effort);
|
|
1810
|
-
}
|
|
1811
|
-
return efforts;
|
|
1812
|
-
}
|
|
1813
|
-
function kimiInputHasImage(row) {
|
|
1814
|
-
const modalities = row.input_modalities;
|
|
1815
|
-
if (!Array.isArray(modalities))
|
|
1816
|
-
return true;
|
|
1817
|
-
return modalities.some((value) => value === "image");
|
|
1818
|
-
}
|
|
1819
|
-
export async function installKimiConfig(config) {
|
|
1820
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1821
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1822
|
-
const document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
1823
|
-
const rows = catalogModelRows(document) ?? [];
|
|
1824
|
-
if (rows.length === 0)
|
|
1825
|
-
throw new CPACError("CPA catalog contains no models");
|
|
1826
|
-
const state = readState(config.state_dir);
|
|
1827
|
-
const recorded = state ? stateProxy(state) : null;
|
|
1828
|
-
const port = recorded?.port ?? config.codex_proxy_port;
|
|
1829
|
-
if (!port) {
|
|
1830
|
-
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
1831
|
-
}
|
|
1832
|
-
const lines = [
|
|
1833
|
-
KIMI_BLOCK_START,
|
|
1834
|
-
"[providers.cpac]",
|
|
1835
|
-
'type = "openai"',
|
|
1836
|
-
`base_url = "http://127.0.0.1:${port}/v1"`,
|
|
1837
|
-
'# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.',
|
|
1838
|
-
'api_key = "cpac-loopback"',
|
|
1839
|
-
];
|
|
1840
|
-
for (const row of rows) {
|
|
1841
|
-
const slug = catalogModelId(row);
|
|
1842
|
-
if (!slug)
|
|
1843
|
-
continue;
|
|
1844
|
-
// ponytail: Kimi requires max_context_size; default 200000 when the catalog omits it.
|
|
1845
|
-
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
1846
|
-
? Math.floor(row.context_window)
|
|
1847
|
-
: 200000;
|
|
1848
|
-
const efforts = kimiSupportEfforts(row);
|
|
1849
|
-
const capabilities = [
|
|
1850
|
-
...(efforts.length > 0 ? ["thinking"] : []),
|
|
1851
|
-
"tool_use",
|
|
1852
|
-
...(kimiInputHasImage(row) ? ["image_in"] : []),
|
|
1853
|
-
];
|
|
1854
|
-
lines.push("", `[models."cpac/${slug}"]`, 'provider = "cpac"', `model = ${tomlString(slug)}`, `max_context_size = ${context}`, `capabilities = ${tomlStringArray(capabilities)}`);
|
|
1855
|
-
if (typeof row.display_name === "string" && row.display_name.trim()) {
|
|
1856
|
-
lines.push(`display_name = ${tomlString(row.display_name)}`);
|
|
1857
|
-
}
|
|
1858
|
-
if (efforts.length > 0) {
|
|
1859
|
-
lines.push(`support_efforts = ${tomlStringArray(efforts)}`);
|
|
1860
|
-
const rawDefaultEffort = (typeof row.default_reasoning_level === "string" &&
|
|
1861
|
-
row.default_reasoning_level) ||
|
|
1862
|
-
(typeof row.default_reasoning_effort === "string" &&
|
|
1863
|
-
row.default_reasoning_effort) ||
|
|
1864
|
-
(typeof row.default_effort === "string" && row.default_effort);
|
|
1865
|
-
const defaultEffort = rawDefaultEffort && efforts.includes(rawDefaultEffort.toLowerCase())
|
|
1866
|
-
? rawDefaultEffort.toLowerCase()
|
|
1867
|
-
: undefined;
|
|
1868
|
-
if (defaultEffort)
|
|
1869
|
-
lines.push(`default_effort = ${tomlString(defaultEffort)}`);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
lines.push(KIMI_BLOCK_END);
|
|
1873
|
-
const target = kimiConfigPath();
|
|
1874
|
-
ensureCpacBackup(target);
|
|
1875
|
-
writeKimiBlock(target, lines.join("\n"));
|
|
1876
|
-
console.log(`Installed Kimi Code provider config: ${target}`);
|
|
1877
|
-
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
1878
|
-
console.log("Loopback proxy is not running; run: cpac inject");
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
export async function uninstallKimiConfig() {
|
|
1882
|
-
const target = kimiConfigPath();
|
|
1883
|
-
if (!isKimiConfigInstalled())
|
|
1884
|
-
throw new CPACError("Kimi Code config is not installed");
|
|
1885
|
-
writeKimiBlock(target, null);
|
|
1886
|
-
console.log(`Removed Kimi Code provider config: ${target}`);
|
|
1887
|
-
}
|
|
1888
|
-
function grokHome() {
|
|
1889
|
-
const home = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
1890
|
-
return expandUserPath(home);
|
|
1891
|
-
}
|
|
1892
|
-
function grokConfigPath() {
|
|
1893
|
-
return join(grokHome(), "config.toml");
|
|
1894
|
-
}
|
|
1895
|
-
const GROK_BLOCK_START = "# >>> CPAC Grok >>>";
|
|
1896
|
-
const GROK_BLOCK_END = "# <<< CPAC Grok <<<";
|
|
1897
|
-
const grokBlockRegex = new RegExp(`${GROK_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GROK_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
1898
|
-
export function isGrokConfigInstalled() {
|
|
1899
|
-
try {
|
|
1900
|
-
const content = readFileSync(grokConfigPath(), "utf8");
|
|
1901
|
-
return (grokBlockRegex.test(content) ||
|
|
1902
|
-
/^\s*\[model\.(?:"cpac\/|'cpac\/|cpac-)/m.test(content));
|
|
1903
|
-
}
|
|
1904
|
-
catch {
|
|
1905
|
-
return false;
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
function isCpacGrokTableHeader(line) {
|
|
1909
|
-
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
1910
|
-
if (!header)
|
|
1911
|
-
return false;
|
|
1912
|
-
const name = header[1];
|
|
1913
|
-
return /^model\.(?:"cpac\/|'cpac\/|cpac-)/.test(name);
|
|
1914
|
-
}
|
|
1915
|
-
function stripOrphanCpacGrokTables(content) {
|
|
1916
|
-
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
1917
|
-
const out = [];
|
|
1918
|
-
let skipping = false;
|
|
1919
|
-
for (const line of content.split(/\r?\n/)) {
|
|
1920
|
-
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
1921
|
-
skipping = isCpacGrokTableHeader(line);
|
|
1922
|
-
if (!skipping)
|
|
1923
|
-
out.push(line);
|
|
1924
|
-
}
|
|
1925
|
-
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
1926
|
-
}
|
|
1927
|
-
function writeGrokBlock(path, block) {
|
|
1928
|
-
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
1929
|
-
content = content.replace(grokBlockRegex, "");
|
|
1930
|
-
const orphan = content.indexOf(GROK_BLOCK_START);
|
|
1931
|
-
if (orphan !== -1)
|
|
1932
|
-
content = content.slice(0, orphan);
|
|
1933
|
-
content = stripOrphanCpacGrokTables(content);
|
|
1934
|
-
if (block) {
|
|
1935
|
-
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
1936
|
-
}
|
|
1937
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
1938
|
-
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
1939
|
-
atomicWrite(path, Buffer.from(content), mode);
|
|
1940
|
-
}
|
|
1941
|
-
export async function installGrokConfig(config) {
|
|
1942
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1943
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1944
|
-
const document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
1945
|
-
const rows = catalogModelRows(document) ?? [];
|
|
1946
|
-
if (rows.length === 0)
|
|
1947
|
-
throw new CPACError("CPA catalog contains no models");
|
|
1948
|
-
const state = readState(config.state_dir);
|
|
1949
|
-
const recorded = state ? stateProxy(state) : null;
|
|
1950
|
-
const port = recorded?.port ?? config.codex_proxy_port;
|
|
1951
|
-
if (!port) {
|
|
1952
|
-
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
1953
|
-
}
|
|
1954
|
-
const lines = [GROK_BLOCK_START];
|
|
1955
|
-
for (const row of rows) {
|
|
1956
|
-
const slug = catalogModelId(row);
|
|
1957
|
-
if (!slug)
|
|
1958
|
-
continue;
|
|
1959
|
-
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
1960
|
-
? Math.floor(row.context_window)
|
|
1961
|
-
: 200000;
|
|
1962
|
-
const name = typeof row.display_name === "string" && row.display_name.trim()
|
|
1963
|
-
? `${row.display_name.trim()} (CPAC)`
|
|
1964
|
-
: `CPAC ${slug}`;
|
|
1965
|
-
lines.push(`[model."cpac/${slug}"]`, `model = ${tomlString(slug)}`, `base_url = "http://127.0.0.1:${port}/v1"`, 'api_backend = "responses"', '# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.', 'api_key = "cpac-loopback"', `name = ${tomlString(name)}`, `context_window = ${context}`, "");
|
|
1966
|
-
}
|
|
1967
|
-
if (lines[lines.length - 1] === "")
|
|
1968
|
-
lines.pop();
|
|
1969
|
-
lines.push(GROK_BLOCK_END);
|
|
1970
|
-
const target = grokConfigPath();
|
|
1971
|
-
ensureCpacBackup(target);
|
|
1972
|
-
writeGrokBlock(target, lines.join("\n"));
|
|
1973
|
-
console.log(`Installed Grok Build provider config: ${target}`);
|
|
1974
|
-
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
1975
|
-
console.log("Loopback proxy is not running; run: cpac inject");
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
export async function uninstallGrokConfig() {
|
|
1979
|
-
const target = grokConfigPath();
|
|
1980
|
-
if (!isGrokConfigInstalled())
|
|
1981
|
-
throw new CPACError("Grok Build config is not installed");
|
|
1982
|
-
writeGrokBlock(target, null);
|
|
1983
|
-
console.log(`Removed Grok Build provider config: ${target}`);
|
|
1984
|
-
}
|
|
1985
|
-
function readPackageVersion() {
|
|
1986
|
-
try {
|
|
1987
|
-
const raw = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8");
|
|
1988
|
-
const version = JSON.parse(raw).version;
|
|
1989
|
-
return typeof version === "string" ? version : "0.0.0-dev";
|
|
1990
|
-
}
|
|
1991
|
-
catch {
|
|
1992
|
-
return "0.0.0-dev";
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
const CPAC_VERSION = readPackageVersion();
|
|
1996
|
-
export function detectClientVersion(binary) {
|
|
1997
|
-
if (!/^[\w.-]+$/.test(binary))
|
|
1998
|
-
return undefined;
|
|
1999
|
-
try {
|
|
2000
|
-
const res = spawnSync(binary, ["--version"], {
|
|
2001
|
-
encoding: "utf8",
|
|
2002
|
-
timeout: 2000,
|
|
2003
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2004
|
-
});
|
|
2005
|
-
if (res.status === 0 && typeof res.stdout === "string") {
|
|
2006
|
-
const match = res.stdout.match(/v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
|
|
2007
|
-
return match ? match[1] : res.stdout.trim().split(/\s+/)[0] || undefined;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
catch {
|
|
2011
|
-
// ignored
|
|
2012
|
-
}
|
|
2013
|
-
return undefined;
|
|
2014
|
-
}
|
|
2015
|
-
function binaryOnPath(name) {
|
|
2016
|
-
if (!/^[\w.-]+$/.test(name))
|
|
2017
|
-
return false;
|
|
2018
|
-
try {
|
|
2019
|
-
return (spawnSync(`command -v ${name}`, { stdio: "ignore", shell: true }).status ===
|
|
2020
|
-
0);
|
|
2021
|
-
}
|
|
2022
|
-
catch {
|
|
2023
|
-
return false;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
const TARGETS = [
|
|
2027
|
-
{
|
|
2028
|
-
id: "codex",
|
|
2029
|
-
home: () => expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex")),
|
|
2030
|
-
detected: function () {
|
|
2031
|
-
return existsSync(this.home());
|
|
2032
|
-
},
|
|
2033
|
-
installed: (config) => {
|
|
2034
|
-
try {
|
|
2035
|
-
return readFileSync(config.codex_config, "utf8").includes(MANAGED_MARKER);
|
|
2036
|
-
}
|
|
2037
|
-
catch {
|
|
2038
|
-
return false;
|
|
2039
|
-
}
|
|
2040
|
-
},
|
|
2041
|
-
version: () => detectClientVersion("codex"),
|
|
2042
|
-
install: async (config, dryRun, v2Off, maxContext) => {
|
|
2043
|
-
if (dryRun) {
|
|
2044
|
-
console.log("would inject CPA catalog and loopback proxy into Codex");
|
|
2045
|
-
return;
|
|
2046
|
-
}
|
|
2047
|
-
await inject(config, v2Off, maxContext);
|
|
2048
|
-
},
|
|
2049
|
-
uninstall: async (config, dryRun) => {
|
|
2050
|
-
if (dryRun) {
|
|
2051
|
-
console.log("would restore the original Codex config");
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
await restore(config);
|
|
2055
|
-
},
|
|
2056
|
-
},
|
|
2057
|
-
{
|
|
2058
|
-
id: "pi",
|
|
2059
|
-
home: () => piExtensionsDir(),
|
|
2060
|
-
detected: () => binaryOnPath("pi") || existsSync(piExtensionsDir()),
|
|
2061
|
-
installed: () => isPiExtensionInstalled(),
|
|
2062
|
-
version: () => detectClientVersion("pi"),
|
|
2063
|
-
install: async (config, dryRun) => {
|
|
2064
|
-
if (dryRun) {
|
|
2065
|
-
console.log(`would write Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
|
-
await installPiExtension(config);
|
|
2069
|
-
},
|
|
2070
|
-
uninstall: async (_config, dryRun) => {
|
|
2071
|
-
if (dryRun) {
|
|
2072
|
-
console.log(`would remove Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
2073
|
-
return;
|
|
2074
|
-
}
|
|
2075
|
-
await uninstallPiExtension();
|
|
2076
|
-
},
|
|
2077
|
-
},
|
|
2078
|
-
{
|
|
2079
|
-
id: "kimi",
|
|
2080
|
-
home: () => dirname(kimiConfigPath()),
|
|
2081
|
-
detected: () => binaryOnPath("kimi") || existsSync(dirname(kimiConfigPath())),
|
|
2082
|
-
installed: () => isKimiConfigInstalled(),
|
|
2083
|
-
version: () => detectClientVersion("kimi"),
|
|
2084
|
-
install: async (config, dryRun) => {
|
|
2085
|
-
if (dryRun) {
|
|
2086
|
-
console.log(`would write CPA provider block: ${kimiConfigPath()}`);
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
await installKimiConfig(config);
|
|
2090
|
-
},
|
|
2091
|
-
uninstall: async (_config, dryRun) => {
|
|
2092
|
-
if (dryRun) {
|
|
2093
|
-
console.log(`would remove CPA provider block: ${kimiConfigPath()}`);
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
await uninstallKimiConfig();
|
|
2097
|
-
},
|
|
2098
|
-
},
|
|
2099
|
-
{
|
|
2100
|
-
id: "grok",
|
|
2101
|
-
home: () => grokHome(),
|
|
2102
|
-
detected: () => binaryOnPath("grok") || existsSync(grokHome()),
|
|
2103
|
-
installed: () => isGrokConfigInstalled(),
|
|
2104
|
-
version: () => detectClientVersion("grok"),
|
|
2105
|
-
install: async (config, dryRun) => {
|
|
2106
|
-
if (dryRun) {
|
|
2107
|
-
console.log(`would write CPA provider block: ${grokConfigPath()}`);
|
|
2108
|
-
return;
|
|
2109
|
-
}
|
|
2110
|
-
await installGrokConfig(config);
|
|
2111
|
-
},
|
|
2112
|
-
uninstall: async (_config, dryRun) => {
|
|
2113
|
-
if (dryRun) {
|
|
2114
|
-
console.log(`would remove CPA provider block: ${grokConfigPath()}`);
|
|
2115
|
-
return;
|
|
2116
|
-
}
|
|
2117
|
-
await uninstallGrokConfig();
|
|
2118
|
-
},
|
|
2119
|
-
},
|
|
2120
|
-
];
|
|
2121
|
-
export async function detectTargets(config) {
|
|
2122
|
-
return TARGETS.map((target) => ({
|
|
2123
|
-
id: target.id,
|
|
2124
|
-
detected: target.detected(),
|
|
2125
|
-
installed: target.installed(config),
|
|
2126
|
-
path: target.home(),
|
|
2127
|
-
version: target.version?.(),
|
|
2128
|
-
}));
|
|
2129
|
-
}
|
|
2130
|
-
export async function runDetect(config, asJson) {
|
|
2131
|
-
const targets = await detectTargets(config);
|
|
2132
|
-
if (asJson) {
|
|
2133
|
-
console.log(JSON.stringify({ version: CPAC_VERSION, targets }, null, 2));
|
|
2134
|
-
return 0;
|
|
2135
|
-
}
|
|
2136
|
-
for (const target of targets) {
|
|
2137
|
-
console.log(`${target.id.padEnd(8)} ${(target.detected ? "detected" : "missing").padEnd(9)} ${(target.installed ? "installed" : "not installed").padEnd(14)} ${target.path}`);
|
|
2138
|
-
}
|
|
2139
|
-
return 0;
|
|
2140
|
-
}
|
|
2141
|
-
function selectTargets(requested, all, eligible) {
|
|
2142
|
-
const unknown = requested.filter((id) => !TARGETS.some((target) => target.id === id));
|
|
2143
|
-
if (unknown.length > 0) {
|
|
2144
|
-
throw new CPACError(`unknown target(s): ${unknown.join(", ")}; use ${TARGETS.map((target) => target.id).join(",")}`);
|
|
2145
|
-
}
|
|
2146
|
-
const ids = requested.length > 0
|
|
2147
|
-
? requested
|
|
2148
|
-
: all
|
|
2149
|
-
? TARGETS.map((target) => target.id)
|
|
2150
|
-
: eligible;
|
|
2151
|
-
return TARGETS.filter((target) => ids.includes(target.id));
|
|
2152
|
-
}
|
|
2153
|
-
export async function runInstall(config, requested, options) {
|
|
2154
|
-
const eligible = TARGETS.filter((target) => target.detected()).map((target) => target.id);
|
|
2155
|
-
const selected = selectTargets(requested, options.all, eligible);
|
|
2156
|
-
if (selected.length === 0) {
|
|
2157
|
-
throw new CPACError(`no supported targets detected; use --target ${TARGETS.map((target) => target.id).join(",")} or --all`);
|
|
2158
|
-
}
|
|
2159
|
-
// Catalog context lifting is Codex-specific; refuse to silently skip it on
|
|
2160
|
-
// other targets so the flag never gains cross-agent meaning.
|
|
2161
|
-
if (options.maxContext && selected.some((target) => target.id !== "codex")) {
|
|
2162
|
-
throw new CPACError("--max_context is only valid with --target codex");
|
|
2163
|
-
}
|
|
2164
|
-
for (const target of selected) {
|
|
2165
|
-
if (target.installed(config) && !options.force) {
|
|
2166
|
-
console.log(`${target.id}: already installed; use --force to reinstall`);
|
|
2167
|
-
continue;
|
|
2168
|
-
}
|
|
2169
|
-
await target.install(config, options.dryRun, options.v2Off, options.maxContext);
|
|
2170
|
-
}
|
|
2171
|
-
return 0;
|
|
2172
|
-
}
|
|
2173
|
-
export async function runSync(config, requested, options) {
|
|
2174
|
-
const installed = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
2175
|
-
if (requested.length === 0 && !options.all) {
|
|
2176
|
-
if (installed.length === 0) {
|
|
2177
|
-
throw new CPACError("no installed CPAC integrations found; run cpac install first");
|
|
2178
|
-
}
|
|
2179
|
-
return runInstall(config, installed, {
|
|
2180
|
-
all: false,
|
|
2181
|
-
dryRun: options.dryRun,
|
|
2182
|
-
force: true,
|
|
2183
|
-
v2Off: options.v2Off,
|
|
2184
|
-
maxContext: options.maxContext,
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
return runInstall(config, requested, {
|
|
2188
|
-
all: options.all,
|
|
2189
|
-
dryRun: options.dryRun,
|
|
2190
|
-
force: true,
|
|
2191
|
-
v2Off: options.v2Off,
|
|
2192
|
-
maxContext: options.maxContext,
|
|
2193
|
-
});
|
|
2194
|
-
}
|
|
2195
|
-
export async function runUninstall(config, requested, options) {
|
|
2196
|
-
const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
2197
|
-
const selected = selectTargets(requested, options.all, eligible);
|
|
2198
|
-
if (selected.length === 0) {
|
|
2199
|
-
throw new CPACError("no installed CPAC integrations found; use --target <id> or --all");
|
|
2200
|
-
}
|
|
2201
|
-
for (const target of selected) {
|
|
2202
|
-
if (!target.installed(config)) {
|
|
2203
|
-
console.log(`${target.id}: not installed`);
|
|
2204
|
-
continue;
|
|
2205
|
-
}
|
|
2206
|
-
await target.uninstall(config, options.dryRun);
|
|
2207
|
-
}
|
|
2208
|
-
return 0;
|
|
2209
|
-
}
|
|
2210
|
-
function compareVersions(a, b) {
|
|
2211
|
-
const left = a.split(".").map(Number);
|
|
2212
|
-
const right = b.split(".").map(Number);
|
|
2213
|
-
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
2214
|
-
const l = left[index] ?? 0;
|
|
2215
|
-
const r = right[index] ?? 0;
|
|
2216
|
-
if (l !== r)
|
|
2217
|
-
return l > r ? 1 : -1;
|
|
2218
|
-
}
|
|
2219
|
-
return 0;
|
|
2220
|
-
}
|
|
2221
|
-
export async function runUpgrade(config, checkOnly) {
|
|
2222
|
-
console.log(`Current version: ${CPAC_VERSION}`);
|
|
2223
|
-
let latest;
|
|
2224
|
-
try {
|
|
2225
|
-
const response = await fetch("https://registry.npmjs.org/@yhong91/cpac/latest", {
|
|
2226
|
-
headers: { Accept: "application/json" },
|
|
2227
|
-
signal: AbortSignal.timeout(10_000),
|
|
2228
|
-
});
|
|
2229
|
-
if (response.ok) {
|
|
2230
|
-
const data = (await response.json());
|
|
2231
|
-
if (typeof data.version === "string")
|
|
2232
|
-
latest = data.version;
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
catch {
|
|
2236
|
-
latest = undefined;
|
|
2237
|
-
}
|
|
2238
|
-
if (!latest) {
|
|
2239
|
-
console.error("Could not fetch latest version from npm. Check your network connection.");
|
|
2240
|
-
return 1;
|
|
2241
|
-
}
|
|
2242
|
-
console.log(`Latest version: ${latest}`);
|
|
2243
|
-
const upToDate = compareVersions(CPAC_VERSION, latest) >= 0;
|
|
2244
|
-
if (checkOnly) {
|
|
2245
|
-
if (upToDate)
|
|
2246
|
-
console.log("Already up to date.");
|
|
2247
|
-
else
|
|
2248
|
-
console.log("Update available. Run: cpac upgrade");
|
|
2249
|
-
return 0;
|
|
2250
|
-
}
|
|
2251
|
-
if (upToDate) {
|
|
2252
|
-
console.log("Already up to date.");
|
|
2253
|
-
}
|
|
2254
|
-
else {
|
|
2255
|
-
console.log(`Installing @yhong91/cpac@${latest}`);
|
|
2256
|
-
const result = spawnSync("npm", ["install", "-g", `@yhong91/cpac@${latest}`], { stdio: "inherit" });
|
|
2257
|
-
if (result.status !== 0) {
|
|
2258
|
-
console.error("npm install failed");
|
|
2259
|
-
return result.status ?? 1;
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
const installed = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
2263
|
-
if (installed.length === 0) {
|
|
2264
|
-
console.log("No installed agents to sync.");
|
|
2265
|
-
return 0;
|
|
2266
|
-
}
|
|
2267
|
-
console.log(`Syncing ${installed.join(", ")}`);
|
|
2268
|
-
return runSync(config, [], { all: false, dryRun: false });
|
|
2269
|
-
}
|
|
2270
77
|
function usage() {
|
|
2271
78
|
return [
|
|
2272
79
|
"Usage: cpac",
|
|
80
|
+
" cpac <codex|kimi|grok|pi|claude|opencode> [args...]",
|
|
81
|
+
" cpac <codex|claude> setup",
|
|
82
|
+
" cpac <codex|claude|kimi|grok|pi> clear",
|
|
2273
83
|
" cpac inject [--v2_off] [--v2_models] [--max_context] [--config PATH]",
|
|
2274
|
-
" cpac
|
|
84
|
+
" cpac restore [--target codex,pi,kimi,grok] [--all] [--dry-run] [--config PATH]",
|
|
85
|
+
" cpac status [--config PATH]",
|
|
2275
86
|
" cpac proxy [--config PATH]",
|
|
2276
|
-
" cpac claude [--config PATH] [--] [claude args...]",
|
|
2277
|
-
" cpac opencode [--config PATH] [--] [opencode args...]",
|
|
2278
|
-
" cpac claude-models [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset] [--config PATH]",
|
|
2279
87
|
" cpac detect [--json] [--home PATH]",
|
|
2280
88
|
" cpac install [--target codex,pi,kimi,grok] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context] [--home PATH]",
|
|
2281
89
|
" cpac sync [--target codex,pi,kimi,grok] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context] [--home PATH]",
|
|
2282
|
-
" cpac uninstall [--target codex,pi,kimi,grok] [--all] [--dry-run] [--home PATH]",
|
|
2283
90
|
" cpac upgrade [--check]",
|
|
2284
91
|
" cpac version",
|
|
2285
92
|
"",
|
|
2286
|
-
"
|
|
2287
|
-
"
|
|
93
|
+
"launch an agent directly with `cpac <agent> [args...]` (all args forwarded).",
|
|
94
|
+
"configure an agent with `cpac <agent> setup` or restore with `cpac <agent> clear`.",
|
|
2288
95
|
].join("\n");
|
|
2289
96
|
}
|
|
2290
97
|
function parseArgs(args) {
|
|
@@ -2295,6 +102,7 @@ function parseArgs(args) {
|
|
|
2295
102
|
args[0] === "install" ||
|
|
2296
103
|
args[0] === "sync" ||
|
|
2297
104
|
args[0] === "uninstall" ||
|
|
105
|
+
args[0] === "restore" ||
|
|
2298
106
|
args[0] === "upgrade" ||
|
|
2299
107
|
args[0] === "version") {
|
|
2300
108
|
const parsed = {
|
|
@@ -2358,10 +166,100 @@ function parseArgs(args) {
|
|
|
2358
166
|
}
|
|
2359
167
|
return parsed;
|
|
2360
168
|
}
|
|
169
|
+
if (["codex", "kimi", "grok", "pi", "claude", "opencode"].includes(args[0])) {
|
|
170
|
+
const targetId = args[0];
|
|
171
|
+
let configPath = defaultConfigPath();
|
|
172
|
+
if (args[1] === "clear" || args[1] === "--clear") {
|
|
173
|
+
for (let i = 2; i < args.length; i++) {
|
|
174
|
+
if (args[i] === "--config") {
|
|
175
|
+
const val = args[++i];
|
|
176
|
+
if (val)
|
|
177
|
+
configPath = resolve(expandUserPath(val));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (targetId === "claude") {
|
|
181
|
+
return {
|
|
182
|
+
command: "claude-config",
|
|
183
|
+
configPath,
|
|
184
|
+
models: {},
|
|
185
|
+
pick: [],
|
|
186
|
+
reset: true,
|
|
187
|
+
interactive: false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
command: "restore",
|
|
192
|
+
configPath,
|
|
193
|
+
json: false,
|
|
194
|
+
targets: [targetId],
|
|
195
|
+
all: false,
|
|
196
|
+
dryRun: false,
|
|
197
|
+
force: false,
|
|
198
|
+
check: false,
|
|
199
|
+
v2Off: false,
|
|
200
|
+
v2Models: false,
|
|
201
|
+
maxContext: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (args[1] === "setup" || args[1] === "--setup") {
|
|
205
|
+
for (let i = 2; i < args.length; i++) {
|
|
206
|
+
if (args[i] === "--config") {
|
|
207
|
+
const val = args[++i];
|
|
208
|
+
if (val)
|
|
209
|
+
configPath = resolve(expandUserPath(val));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (targetId === "codex") {
|
|
213
|
+
return {
|
|
214
|
+
command: "codex-config",
|
|
215
|
+
configPath,
|
|
216
|
+
v2Off: false,
|
|
217
|
+
v2Models: false,
|
|
218
|
+
maxContext: false,
|
|
219
|
+
reset: false,
|
|
220
|
+
interactive: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (targetId === "claude") {
|
|
224
|
+
return {
|
|
225
|
+
command: "claude-config",
|
|
226
|
+
configPath,
|
|
227
|
+
models: {},
|
|
228
|
+
pick: [],
|
|
229
|
+
reset: false,
|
|
230
|
+
interactive: true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
throw new CPACError(`${targetId} does not require setup (all CPA models are auto-synced). Run 'cpac ${targetId}' or 'cpac install --target ${targetId}'.`);
|
|
234
|
+
}
|
|
235
|
+
const passArgs = args.slice(1);
|
|
236
|
+
if (targetId === "codex") {
|
|
237
|
+
return {
|
|
238
|
+
command: "codex",
|
|
239
|
+
configPath,
|
|
240
|
+
args: passArgs,
|
|
241
|
+
v2Off: false,
|
|
242
|
+
v2Models: false,
|
|
243
|
+
maxContext: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (targetId === "claude") {
|
|
247
|
+
return {
|
|
248
|
+
command: "claude",
|
|
249
|
+
configPath,
|
|
250
|
+
args: passArgs,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
command: targetId,
|
|
255
|
+
configPath,
|
|
256
|
+
args: passArgs,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
2361
259
|
if (args[0] === "claude-models") {
|
|
2362
260
|
if (args.includes("-h") || args.includes("--help")) {
|
|
2363
261
|
console.log([
|
|
2364
|
-
"Usage: cpac claude
|
|
262
|
+
"Usage: cpac claude --config [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset] [--config PATH]",
|
|
2365
263
|
"",
|
|
2366
264
|
"Sets the model Claude Code uses per task tier. Saved to the config file",
|
|
2367
265
|
"and applied on every `cpac claude` launch:",
|
|
@@ -2410,35 +308,14 @@ function parseArgs(args) {
|
|
|
2410
308
|
throw new CPACError(`unknown option: ${arg}`);
|
|
2411
309
|
}
|
|
2412
310
|
}
|
|
2413
|
-
return {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
throw new CPACError("--config requires a path");
|
|
2422
|
-
configPath = resolve(expandUserPath(value));
|
|
2423
|
-
index += 1;
|
|
2424
|
-
}
|
|
2425
|
-
if (args[index] === "--")
|
|
2426
|
-
index += 1;
|
|
2427
|
-
return { command: "claude", configPath, args: args.slice(index) };
|
|
2428
|
-
}
|
|
2429
|
-
if (args[0] === "opencode") {
|
|
2430
|
-
let configPath = defaultConfigPath();
|
|
2431
|
-
let index = 1;
|
|
2432
|
-
if (args[index] === "--config") {
|
|
2433
|
-
const value = args[++index];
|
|
2434
|
-
if (!value)
|
|
2435
|
-
throw new CPACError("--config requires a path");
|
|
2436
|
-
configPath = resolve(expandUserPath(value));
|
|
2437
|
-
index += 1;
|
|
2438
|
-
}
|
|
2439
|
-
if (args[index] === "--")
|
|
2440
|
-
index += 1;
|
|
2441
|
-
return { command: "opencode", configPath, args: args.slice(index) };
|
|
311
|
+
return {
|
|
312
|
+
command: "claude-config",
|
|
313
|
+
configPath,
|
|
314
|
+
models,
|
|
315
|
+
pick,
|
|
316
|
+
reset,
|
|
317
|
+
interactive: !reset && Object.keys(models).length === 0 && pick.length === 0,
|
|
318
|
+
};
|
|
2442
319
|
}
|
|
2443
320
|
if (args.includes("-h") || args.includes("--help")) {
|
|
2444
321
|
console.log(usage());
|
|
@@ -2473,7 +350,7 @@ function parseArgs(args) {
|
|
|
2473
350
|
positional.push(args[index]);
|
|
2474
351
|
}
|
|
2475
352
|
if (positional.length !== 1 ||
|
|
2476
|
-
!["inject", "status", "
|
|
353
|
+
!["inject", "status", "proxy"].includes(positional[0])) {
|
|
2477
354
|
throw new CPACError(usage());
|
|
2478
355
|
}
|
|
2479
356
|
if (positional[0] !== "inject" && (v2Off || v2Models || maxContext)) {
|
|
@@ -2536,8 +413,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
2536
413
|
maxContext: parsed.maxContext,
|
|
2537
414
|
});
|
|
2538
415
|
}
|
|
2539
|
-
if (parsed.command === "uninstall")
|
|
2540
|
-
return await
|
|
416
|
+
if (parsed.command === "uninstall" || parsed.command === "restore")
|
|
417
|
+
return await runRestore(config, parsed.targets, {
|
|
2541
418
|
all: parsed.all,
|
|
2542
419
|
dryRun: parsed.dryRun,
|
|
2543
420
|
});
|
|
@@ -2549,8 +426,38 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
2549
426
|
}
|
|
2550
427
|
if (parsed.command === "claude")
|
|
2551
428
|
return await runClaude(config, parsed.args);
|
|
429
|
+
if (parsed.command === "claude-config")
|
|
430
|
+
return await runClaudeConfig(config, parsed.configPath, {
|
|
431
|
+
models: parsed.models,
|
|
432
|
+
pick: parsed.pick,
|
|
433
|
+
reset: parsed.reset,
|
|
434
|
+
interactive: parsed.interactive,
|
|
435
|
+
});
|
|
436
|
+
if (parsed.command === "codex-config")
|
|
437
|
+
return await runCodexConfig(config, parsed.configPath, {
|
|
438
|
+
v2Off: parsed.v2Off,
|
|
439
|
+
v2Models: parsed.v2Models,
|
|
440
|
+
maxContext: parsed.maxContext,
|
|
441
|
+
reset: parsed.reset,
|
|
442
|
+
interactive: parsed.interactive,
|
|
443
|
+
});
|
|
2552
444
|
if (parsed.command === "opencode")
|
|
2553
445
|
return await runOpencode(config, parsed.args);
|
|
446
|
+
if (parsed.command === "codex") {
|
|
447
|
+
let launcherConfig = config;
|
|
448
|
+
if (parsed.v2Models) {
|
|
449
|
+
const picked = await pickSpawnModels(config);
|
|
450
|
+
saveSpawnModels(parsed.configPath, picked);
|
|
451
|
+
console.log(`spawn_models saved to ${parsed.configPath}: ${picked.join(", ")}`);
|
|
452
|
+
launcherConfig = { ...config, spawn_models: picked };
|
|
453
|
+
}
|
|
454
|
+
return await runTargetLauncher(launcherConfig, "codex", parsed.args, "codex", { v2Off: parsed.v2Off, maxContext: parsed.maxContext });
|
|
455
|
+
}
|
|
456
|
+
if (parsed.command === "kimi" ||
|
|
457
|
+
parsed.command === "grok" ||
|
|
458
|
+
parsed.command === "pi") {
|
|
459
|
+
return await runTargetLauncher(config, parsed.command, parsed.args);
|
|
460
|
+
}
|
|
2554
461
|
if (parsed.command === "claude-models")
|
|
2555
462
|
return await runClaudeModels(parsed);
|
|
2556
463
|
if (parsed.command === "proxy")
|
|
@@ -2565,8 +472,6 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
2565
472
|
}
|
|
2566
473
|
await inject(injectConfig, parsed.v2Off, parsed.maxContext);
|
|
2567
474
|
}
|
|
2568
|
-
else if (parsed.command === "restore")
|
|
2569
|
-
await restore(config);
|
|
2570
475
|
else
|
|
2571
476
|
return await status(config);
|
|
2572
477
|
return 0;
|