@yhong91/cpac 0.1.7 → 0.1.9
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 +18 -2
- package/cpac.example.json +8 -1
- package/dist/cpac.js +515 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ cpac --help
|
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
99
|
cpac detect [--json] [--home PATH]
|
|
100
|
-
cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--home PATH]
|
|
100
|
+
cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--home PATH]
|
|
101
101
|
cpac uninstall [--target codex,claude,pi,kimi] [--all] [--dry-run] [--home PATH]
|
|
102
102
|
cpac upgrade [--check]
|
|
103
103
|
cpac version
|
|
@@ -105,6 +105,8 @@ cpac version
|
|
|
105
105
|
|
|
106
106
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
107
107
|
- `install` 默认作用于检测到的目标;`--target` 指定目标、`--all` 全部。各目标映射到既有逻辑:`codex` → `cpac inject`,`pi` → `cpac pi install`,`kimi` → `cpac kimi install`;`claude` 无持久化安装(用 `cpac claude` 启动)。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
108
|
+
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`);`install --target codex --v2_off` 写入 `enabled = false`。服务端还需在 CPA 配置中开启 `codex.optimize-multi-agent-v2`。
|
|
109
|
+
- `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
|
|
108
110
|
- `uninstall` 默认作用于已安装目标,映射到 `cpac restore` / `cpac pi uninstall` / `cpac kimi uninstall`。
|
|
109
111
|
- `--home PATH` 临时把 `HOME` 指向给定目录,便于在隔离环境中试验。
|
|
110
112
|
- `upgrade` 检查 npm 上是否有新版本,`--check` 只报告不提示安装命令。
|
|
@@ -132,7 +134,21 @@ CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1
|
|
|
132
134
|
|
|
133
135
|
代理开启 Claude Code 的 gateway model discovery:`/model` 选择器会列出 CPA 目录里的全部模型。claude 系模型直接使用原 id;其它模型以 `claude-cpac--<模型名>` 别名出现(例如 `claude-cpac--gpt-5.6-sol`),请求发出时由代理还原为真实模型名。
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
启动时还会把模型槽位(`ANTHROPIC_DEFAULT_OPUS/SONNET/HAIKU_MODEL` 及 `ANTHROPIC_SMALL_FAST_MODEL`)指向目录里的模型,避免 haiku/subagent 任务打到网关不认识的默认 id。默认自动选择(claude 系优先:按家族名匹配 opus/sonnet/haiku,否则最大窗口给 opus/sonnet、最小窗口给 haiku),可用命令覆盖:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
cpac claude-models # 查看当前覆盖
|
|
141
|
+
cpac claude-models --opus claude-opus-4-8 --haiku glm-5.3 # 设置/合并
|
|
142
|
+
cpac claude-models --haiku # 不带值:arrow-key 下拉选择
|
|
143
|
+
cpac claude-models --classifier claude-sonnet-4-6 # Auto mode 审批分类器模型
|
|
144
|
+
cpac claude-models --reset # 恢复自动选择
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
所有设定写入配置文件持久缓存,每次 `cpac claude` 启动自动生效。`--classifier` 对应 `CLAUDE_CODE_AUTO_MODE_MODEL`(Auto mode 的审批分类器,默认跟随 Claude Code 内置选择);不设置则不注入。
|
|
148
|
+
|
|
149
|
+
你自己 export 同名环境变量时以你为准。
|
|
150
|
+
|
|
151
|
+
Claude Code 的发现协议不携带 context 信息,claude 前缀的未知模型默认按 200K 记账,且 `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 对 claude 前缀的名字不生效。cpac 采用与 opencodex 相同的机制:目录里窗口足够大的别名模型会带 `[1m]` 标记出现在选择器中(Claude Code 按 1M 记账,发请求前自己剥掉标记),同时 `cpac claude` 注入 `CLAUDE_CODE_AUTO_COMPACT_WINDOW`(默认 350K,与 opencodex 一致,接受范围 100K–1M)作为压缩阈值。你自己 export 该变量时以你为准。claude 系模型如需 1M 窗口,在模型名后加 `[1m]`(如 `cpac claude --model claude-sonnet-4-6[1m]`)。
|
|
136
152
|
|
|
137
153
|
> CPA 服务端必须支持 Claude Code 使用的 Anthropic Messages API(`/v1/messages`)。
|
|
138
154
|
|
package/cpac.example.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cpa_url": "https://cpa.example.com",
|
|
3
3
|
"api_key_env": "CPA_API_KEY",
|
|
4
|
-
"codex_proxy_port": 10101
|
|
4
|
+
"codex_proxy_port": 10101,
|
|
5
|
+
"claude_models": {
|
|
6
|
+
"opus": "claude-opus-4-8",
|
|
7
|
+
"sonnet": "claude-sonnet-4-6",
|
|
8
|
+
"haiku": "glm-5.3",
|
|
9
|
+
"classifier": "claude-sonnet-4-6"
|
|
10
|
+
},
|
|
11
|
+
"spawn_models": ["gpt-5.5", "claude-opus-4-8"]
|
|
5
12
|
}
|
package/dist/cpac.js
CHANGED
|
@@ -19,7 +19,13 @@ const CONFIG_KEYS = new Set([
|
|
|
19
19
|
"codex_config",
|
|
20
20
|
"codex_proxy_port",
|
|
21
21
|
"state_dir",
|
|
22
|
+
"claude_models",
|
|
23
|
+
"spawn_models",
|
|
22
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", "classifier"];
|
|
23
29
|
const REQUIRED_STATE_KEYS = new Set([
|
|
24
30
|
"config_path",
|
|
25
31
|
"config_existed",
|
|
@@ -119,6 +125,39 @@ export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
|
119
125
|
codexProxyPort > 65535) {
|
|
120
126
|
throw new CPACError("codex_proxy_port must be an integer from 0 to 65535");
|
|
121
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
|
+
.filter((key) => !CLAUDE_SLOT_KEYS.includes(key))
|
|
135
|
+
.sort();
|
|
136
|
+
if (unknownSlots.length)
|
|
137
|
+
throw new CPACError(`unknown claude_models slots: ${unknownSlots.join(", ")}`);
|
|
138
|
+
claudeModels = {};
|
|
139
|
+
for (const slot of CLAUDE_SLOT_KEYS) {
|
|
140
|
+
const model = claudeModelsRaw[slot];
|
|
141
|
+
if (model === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
if (typeof model !== "string" || !model.trim())
|
|
144
|
+
throw new CPACError(`claude_models.${slot} must be a non-empty string`);
|
|
145
|
+
claudeModels[slot] = model.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const spawnModelsRaw = value.spawn_models;
|
|
149
|
+
let spawnModels;
|
|
150
|
+
if (spawnModelsRaw !== undefined) {
|
|
151
|
+
if (!Array.isArray(spawnModelsRaw))
|
|
152
|
+
throw new CPACError("spawn_models must be an array of model slugs");
|
|
153
|
+
if (spawnModelsRaw.length > MAX_SPAWN_MODELS)
|
|
154
|
+
throw new CPACError(`spawn_models accepts at most ${MAX_SPAWN_MODELS} models`);
|
|
155
|
+
spawnModels = spawnModelsRaw.map((model, index) => {
|
|
156
|
+
if (typeof model !== "string" || !model.trim())
|
|
157
|
+
throw new CPACError(`spawn_models[${index}] must be a non-empty string`);
|
|
158
|
+
return model.trim();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
122
161
|
const codexHome = expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
123
162
|
const codexConfig = resolve(expandUserPath(value.codex_config ||
|
|
124
163
|
join(codexHome, "config.toml")));
|
|
@@ -135,6 +174,8 @@ export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
|
135
174
|
codex_config: codexConfig,
|
|
136
175
|
codex_proxy_port: codexProxyPort,
|
|
137
176
|
state_dir: stateDir,
|
|
177
|
+
claude_models: claudeModels,
|
|
178
|
+
spawn_models: spawnModels,
|
|
138
179
|
};
|
|
139
180
|
}
|
|
140
181
|
export function apiBase(cpaUrl) {
|
|
@@ -215,6 +256,124 @@ export async function fetchCatalog(cpaUrl, apiKey) {
|
|
|
215
256
|
modelCount: models.length,
|
|
216
257
|
};
|
|
217
258
|
}
|
|
259
|
+
// Move `order` slugs to the front of the rich catalog (stable for the rest),
|
|
260
|
+
// so the Codex client's first-5 spawn_agent advertisement picks them.
|
|
261
|
+
export function reorderCatalog(bytes, order) {
|
|
262
|
+
const document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
263
|
+
const rank = new Map(order.map((slug, index) => [slug, index]));
|
|
264
|
+
const models = document.models.map((model, index) => ({ model, index }));
|
|
265
|
+
models.sort((left, right) => {
|
|
266
|
+
const leftRank = rank.get(String(left.model.slug));
|
|
267
|
+
const rightRank = rank.get(String(right.model.slug));
|
|
268
|
+
if (leftRank === undefined && rightRank === undefined)
|
|
269
|
+
return left.index - right.index;
|
|
270
|
+
if (leftRank === undefined)
|
|
271
|
+
return 1;
|
|
272
|
+
if (rightRank === undefined)
|
|
273
|
+
return -1;
|
|
274
|
+
return leftRank - rightRank;
|
|
275
|
+
});
|
|
276
|
+
document.models = models.map((entry) => entry.model);
|
|
277
|
+
return Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
278
|
+
}
|
|
279
|
+
export function saveSpawnModels(configPath, models) {
|
|
280
|
+
let document = {};
|
|
281
|
+
if (existsSync(configPath)) {
|
|
282
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
283
|
+
if (!objectValue(parsed))
|
|
284
|
+
throw new CPACError("config must be a JSON object");
|
|
285
|
+
document = parsed;
|
|
286
|
+
}
|
|
287
|
+
document.spawn_models = models;
|
|
288
|
+
atomicWrite(configPath, Buffer.from(`${JSON.stringify(document, null, 2)}\n`));
|
|
289
|
+
}
|
|
290
|
+
// Arrow-key checkbox picker on raw stdin; resolves with the checked labels in
|
|
291
|
+
// check order (which becomes the spawn advertisement ranking).
|
|
292
|
+
async function checkboxPicker(title, items, max) {
|
|
293
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
294
|
+
throw new CPACError("model selection requires an interactive terminal");
|
|
295
|
+
}
|
|
296
|
+
const stdin = process.stdin;
|
|
297
|
+
const out = process.stderr;
|
|
298
|
+
const selected = new Set();
|
|
299
|
+
let cursor = 0;
|
|
300
|
+
const lines = items.length + 1;
|
|
301
|
+
const render = () => {
|
|
302
|
+
out.write(`${title} [space: toggle, up/down: move, enter: confirm, q: cancel, max ${max}]\n`);
|
|
303
|
+
items.forEach((item, index) => {
|
|
304
|
+
const box = selected.has(index) ? "[x]" : "[ ]";
|
|
305
|
+
out.write(`${index === cursor ? ">" : " "} ${box} ${item}\n`);
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
const redraw = () => {
|
|
309
|
+
out.write(`\x1b[${lines}F`);
|
|
310
|
+
for (let line = 0; line < lines; line += 1)
|
|
311
|
+
out.write("\x1b[2K\x1b[1E");
|
|
312
|
+
out.write(`\x1b[${lines}F`);
|
|
313
|
+
render();
|
|
314
|
+
};
|
|
315
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
316
|
+
const finish = (error) => {
|
|
317
|
+
stdin.removeListener("data", onData);
|
|
318
|
+
try {
|
|
319
|
+
stdin.setRawMode(false);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Terminal already gone; nothing to restore.
|
|
323
|
+
}
|
|
324
|
+
stdin.pause();
|
|
325
|
+
out.write("\n");
|
|
326
|
+
if (error)
|
|
327
|
+
rejectPromise(error);
|
|
328
|
+
else
|
|
329
|
+
resolvePromise([...selected].map((index) => items[index]));
|
|
330
|
+
};
|
|
331
|
+
const onData = (chunk) => {
|
|
332
|
+
const key = chunk.toString("utf8");
|
|
333
|
+
if (key === "\x03" || key === "q" || key === "\x1b") {
|
|
334
|
+
finish(new CPACError("model selection cancelled"));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (key === "\r" || key === "\n") {
|
|
338
|
+
finish();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (key === "\x1b[A")
|
|
342
|
+
cursor = (cursor + items.length - 1) % items.length;
|
|
343
|
+
else if (key === "\x1b[B")
|
|
344
|
+
cursor = (cursor + 1) % items.length;
|
|
345
|
+
else if (key === " ") {
|
|
346
|
+
if (max === 1) {
|
|
347
|
+
selected.clear();
|
|
348
|
+
selected.add(cursor);
|
|
349
|
+
finish();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (selected.has(cursor))
|
|
353
|
+
selected.delete(cursor);
|
|
354
|
+
else if (selected.size < max)
|
|
355
|
+
selected.add(cursor);
|
|
356
|
+
}
|
|
357
|
+
redraw();
|
|
358
|
+
};
|
|
359
|
+
stdin.setRawMode(true);
|
|
360
|
+
stdin.resume();
|
|
361
|
+
stdin.on("data", onData);
|
|
362
|
+
render();
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
export async function pickSpawnModels(config) {
|
|
366
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
367
|
+
if (!apiKey)
|
|
368
|
+
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
369
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
370
|
+
const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
371
|
+
const slugs = document.models.map((model) => model.slug);
|
|
372
|
+
const picked = await checkboxPicker("Select spawn_agent models", slugs, MAX_SPAWN_MODELS);
|
|
373
|
+
if (picked.length === 0)
|
|
374
|
+
throw new CPACError("no models selected");
|
|
375
|
+
return picked;
|
|
376
|
+
}
|
|
218
377
|
export function tomlString(value) {
|
|
219
378
|
return JSON.stringify(value);
|
|
220
379
|
}
|
|
@@ -341,6 +500,94 @@ export function buildCodexConfig(original, proxyPort, catalogPath) {
|
|
|
341
500
|
const output = root + body;
|
|
342
501
|
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
343
502
|
}
|
|
503
|
+
// Codex multi-agent v2 (features.multi_agent_v2). Accepts the same TOML
|
|
504
|
+
// shapes codex-rs does: a [features.multi_agent_v2] table, an inline
|
|
505
|
+
// `multi_agent_v2 = ...` under [features], or dotted root keys.
|
|
506
|
+
const V2_TABLE_HEADER = /^\s*\[\s*(?:features|"features"|'features')\s*\.\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')\s*\]\s*(?:#.*)?$/;
|
|
507
|
+
const FEATURES_TABLE_HEADER = /^\s*\[\s*(?:features|"features"|'features')\s*\]\s*(?:#.*)?$/;
|
|
508
|
+
const V2_KEY = /^\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')\s*=/;
|
|
509
|
+
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*=/;
|
|
510
|
+
function v2LineValue(line) {
|
|
511
|
+
const inline = line.match(/\{[^}]*\benabled\s*=\s*(true|false)/);
|
|
512
|
+
if (inline)
|
|
513
|
+
return inline[1] === "true";
|
|
514
|
+
const plain = line.match(/=\s*(true|false)(?![A-Za-z0-9_])/);
|
|
515
|
+
if (plain)
|
|
516
|
+
return plain[1] === "true";
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
export function multiAgentV2Enabled(content) {
|
|
520
|
+
let table = "root";
|
|
521
|
+
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
522
|
+
if (V2_TABLE_HEADER.test(line)) {
|
|
523
|
+
table = "v2";
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (FEATURES_TABLE_HEADER.test(line)) {
|
|
527
|
+
table = "features";
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (/^\s*\[/.test(line)) {
|
|
531
|
+
table = "other";
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (table === "v2" && /^\s*(?:enabled|"enabled"|'enabled')\s*=/.test(line)) {
|
|
535
|
+
return v2LineValue(line) ?? false;
|
|
536
|
+
}
|
|
537
|
+
if (table === "features" && V2_KEY.test(line))
|
|
538
|
+
return v2LineValue(line) ?? false;
|
|
539
|
+
if (table === "root" && V2_DOTTED_KEY.test(line))
|
|
540
|
+
return v2LineValue(line) ?? false;
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
function stripMultiAgentV2(content) {
|
|
545
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
546
|
+
const kept = [];
|
|
547
|
+
let skipTable = false;
|
|
548
|
+
let table = "root";
|
|
549
|
+
for (const line of lines) {
|
|
550
|
+
if (/^\s*\[/.test(line)) {
|
|
551
|
+
skipTable = V2_TABLE_HEADER.test(line);
|
|
552
|
+
if (skipTable) {
|
|
553
|
+
table = "other";
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
table = FEATURES_TABLE_HEADER.test(line) ? "features" : "other";
|
|
557
|
+
}
|
|
558
|
+
if (skipTable)
|
|
559
|
+
continue;
|
|
560
|
+
if (table === "features" && V2_KEY.test(line))
|
|
561
|
+
continue;
|
|
562
|
+
if (table === "root" && V2_DOTTED_KEY.test(line))
|
|
563
|
+
continue;
|
|
564
|
+
kept.push(line);
|
|
565
|
+
}
|
|
566
|
+
return kept.join("\n");
|
|
567
|
+
}
|
|
568
|
+
function hasAgentsMaxThreads(content) {
|
|
569
|
+
let table = "root";
|
|
570
|
+
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
571
|
+
if (/^\s*\[/.test(line)) {
|
|
572
|
+
table = /^\s*\[\s*(?:agents|"agents"|'agents')\s*\]\s*(?:#.*)?$/.test(line)
|
|
573
|
+
? "agents"
|
|
574
|
+
: "other";
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (table === "agents" && /^\s*max_threads\s*=/.test(line))
|
|
578
|
+
return true;
|
|
579
|
+
if (table === "root" && /^\s*(?:agents|"agents"|'agents')\s*\.\s*max_threads\s*=/.test(line))
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
export function withMultiAgentV2(content, enabled, eol) {
|
|
585
|
+
let body = stripMultiAgentV2(content);
|
|
586
|
+
if (body && !body.endsWith("\n"))
|
|
587
|
+
body += "\n";
|
|
588
|
+
const output = body + `\n[features.multi_agent_v2]\nenabled = ${enabled}\n`;
|
|
589
|
+
return eol === "\n" ? output : output.replace(/\n/g, "\r\n");
|
|
590
|
+
}
|
|
344
591
|
export function atomicWrite(path, data, mode = 0o600) {
|
|
345
592
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
346
593
|
const temporary = `${path}.cpac.${process.pid}.${Date.now()}.${++atomicSequence}.tmp`;
|
|
@@ -508,6 +755,63 @@ export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
|
508
755
|
return { server, port: address.port };
|
|
509
756
|
}
|
|
510
757
|
const CLAUDE_ALIAS_PREFIX = "claude-cpac--";
|
|
758
|
+
// Claude Code accepts CLAUDE_CODE_AUTO_COMPACT_WINDOW in 100K–1M (binary-verified).
|
|
759
|
+
// A single global window cannot be per-model; 350K is opencodex's user-approved
|
|
760
|
+
// default — high enough to mark mid-size models (372K/500K), low enough that
|
|
761
|
+
// marking never outgrows a model's real window.
|
|
762
|
+
const ONE_MILLION = 1_000_000;
|
|
763
|
+
const COMPACT_WINDOW_DEFAULT = 350_000;
|
|
764
|
+
function claudeCompactWindow(maxContext) {
|
|
765
|
+
return maxContext > 200_000 ? COMPACT_WINDOW_DEFAULT : undefined;
|
|
766
|
+
}
|
|
767
|
+
// [1m] marking predicate (opencodex rule): native claude models manage their own
|
|
768
|
+
// marker; aliases mark when the real window is >= 1M, or when it clears 200K and
|
|
769
|
+
// can host the compact window (marking below it would trip API limits mid-session).
|
|
770
|
+
function shouldMarkOneMillion(id, window, compactWindow) {
|
|
771
|
+
if (id.startsWith("claude"))
|
|
772
|
+
return false;
|
|
773
|
+
if (window >= ONE_MILLION)
|
|
774
|
+
return true;
|
|
775
|
+
return compactWindow !== undefined && window > 200_000 && window >= compactWindow;
|
|
776
|
+
}
|
|
777
|
+
// Auto tier defaults: prefer claude-family models; within the pool match by
|
|
778
|
+
// family keyword first (opus/sonnet/haiku) so equal context windows don't
|
|
779
|
+
// collapse every slot onto the first catalog entry, then fall back to
|
|
780
|
+
// biggest-window (opus/sonnet) and smallest-window (haiku).
|
|
781
|
+
function defaultClaudeSlots(models) {
|
|
782
|
+
if (!models.length)
|
|
783
|
+
return undefined;
|
|
784
|
+
const claudeRows = models.filter((model) => model.id.startsWith("claude"));
|
|
785
|
+
const pool = claudeRows.length ? claudeRows : models;
|
|
786
|
+
const biggest = [...pool].sort((a, b) => b.window - a.window)[0];
|
|
787
|
+
const smallest = pool
|
|
788
|
+
.filter((model) => model.window > 0)
|
|
789
|
+
.sort((a, b) => a.window - b.window)[0];
|
|
790
|
+
const family = (keyword) => pool.find((model) => model.id.includes(keyword));
|
|
791
|
+
return {
|
|
792
|
+
opus: (family("opus") ?? biggest).id,
|
|
793
|
+
sonnet: (family("sonnet") ?? biggest).id,
|
|
794
|
+
haiku: (family("haiku") ?? smallest ?? biggest).id,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
// Resolve the tier-model env values: config overrides win over catalog
|
|
798
|
+
// defaults; values are [1m]-marked per the predicate above (Claude Code
|
|
799
|
+
// strips the marker before the request leaves).
|
|
800
|
+
export function claudeTierSlots(override, models, compactWindow) {
|
|
801
|
+
const defaults = defaultClaudeSlots(models);
|
|
802
|
+
if (!defaults)
|
|
803
|
+
return undefined;
|
|
804
|
+
const windowById = new Map(models.map((model) => [model.id, model.window]));
|
|
805
|
+
const mark = (id) => !/\[1m\]$/i.test(id) &&
|
|
806
|
+
shouldMarkOneMillion(id, windowById.get(id) ?? 0, compactWindow)
|
|
807
|
+
? `${id}[1m]`
|
|
808
|
+
: id;
|
|
809
|
+
return {
|
|
810
|
+
opus: mark(override?.opus ?? defaults.opus),
|
|
811
|
+
sonnet: mark(override?.sonnet ?? defaults.sonnet),
|
|
812
|
+
haiku: mark(override?.haiku ?? defaults.haiku),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
511
815
|
function catalogModelRows(document) {
|
|
512
816
|
if (!objectValue(document))
|
|
513
817
|
return undefined;
|
|
@@ -546,21 +850,38 @@ async function claudeModelList(cpaUrl, apiKey, response) {
|
|
|
546
850
|
return;
|
|
547
851
|
}
|
|
548
852
|
// Claude Code's /model picker only lists ids starting with claude/anthropic;
|
|
549
|
-
// expose every other catalog model as claude-cpac--<id>.
|
|
550
|
-
|
|
853
|
+
// expose every other catalog model as claude-cpac--<id>. Append the [1m]
|
|
854
|
+
// marker when the model can host the auto-compact window — that marker is
|
|
855
|
+
// the only signal that lifts context accounting above the 200K default
|
|
856
|
+
// (Claude Code strips it before the request reaches the proxy).
|
|
857
|
+
const rowsWithWindow = [];
|
|
858
|
+
let maxContext = 0;
|
|
551
859
|
for (const model of rows) {
|
|
552
860
|
const id = catalogModelId(model);
|
|
553
861
|
if (!id)
|
|
554
862
|
continue;
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
863
|
+
const window = typeof model.context_window === "number" && model.context_window > 0
|
|
864
|
+
? Math.floor(model.context_window)
|
|
865
|
+
: 0;
|
|
866
|
+
if (window > maxContext)
|
|
867
|
+
maxContext = window;
|
|
868
|
+
rowsWithWindow.push({
|
|
869
|
+
id,
|
|
870
|
+
window,
|
|
871
|
+
alias: id.startsWith("claude") ? id : `${CLAUDE_ALIAS_PREFIX}${id}`,
|
|
872
|
+
display: typeof model.display_name === "string" && model.display_name.trim()
|
|
560
873
|
? model.display_name
|
|
561
874
|
: id,
|
|
562
875
|
});
|
|
563
876
|
}
|
|
877
|
+
const compactWindow = claudeCompactWindow(maxContext);
|
|
878
|
+
const data = rowsWithWindow.map((row) => ({
|
|
879
|
+
type: "model",
|
|
880
|
+
id: shouldMarkOneMillion(row.id, row.window, compactWindow)
|
|
881
|
+
? `${row.alias}[1m]`
|
|
882
|
+
: row.alias,
|
|
883
|
+
display_name: row.display,
|
|
884
|
+
}));
|
|
564
885
|
response.writeHead(200, { "content-type": "application/json" });
|
|
565
886
|
response.end(JSON.stringify({
|
|
566
887
|
data,
|
|
@@ -595,7 +916,10 @@ export async function createClaudeProxy(cpaUrl, apiKey) {
|
|
|
595
916
|
if (objectValue(parsed) &&
|
|
596
917
|
typeof parsed.model === "string" &&
|
|
597
918
|
parsed.model.startsWith(CLAUDE_ALIAS_PREFIX)) {
|
|
598
|
-
|
|
919
|
+
// Claude Code strips [1m] itself; keep the strip as a guard.
|
|
920
|
+
parsed.model = parsed.model
|
|
921
|
+
.slice(CLAUDE_ALIAS_PREFIX.length)
|
|
922
|
+
.replace(/\[(1|2)m\]$/i, "");
|
|
599
923
|
body = Buffer.from(JSON.stringify(parsed));
|
|
600
924
|
}
|
|
601
925
|
}
|
|
@@ -831,7 +1155,7 @@ function proxyFingerprint(config, apiKey) {
|
|
|
831
1155
|
.update(apiKey)
|
|
832
1156
|
.digest("hex");
|
|
833
1157
|
}
|
|
834
|
-
export async function inject(config) {
|
|
1158
|
+
export async function inject(config, v2Off = false) {
|
|
835
1159
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
836
1160
|
if (!apiKey)
|
|
837
1161
|
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
@@ -867,6 +1191,8 @@ export async function inject(config) {
|
|
|
867
1191
|
}
|
|
868
1192
|
}
|
|
869
1193
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1194
|
+
if (config.spawn_models?.length)
|
|
1195
|
+
catalog.bytes = reorderCatalog(catalog.bytes, config.spawn_models);
|
|
870
1196
|
const stateDirExisted = existsSync(config.state_dir);
|
|
871
1197
|
if (!state &&
|
|
872
1198
|
STATE_FILES.some((name) => existsSync(join(config.state_dir, name)))) {
|
|
@@ -898,6 +1224,11 @@ export async function inject(config) {
|
|
|
898
1224
|
let injected;
|
|
899
1225
|
try {
|
|
900
1226
|
injected = buildCodexConfig(original, proxy.port, catalogPath);
|
|
1227
|
+
const injectedText = new TextDecoder("utf-8").decode(injected);
|
|
1228
|
+
injected = Buffer.from(withMultiAgentV2(injectedText, !v2Off, dominantEol(injectedText)));
|
|
1229
|
+
if (!v2Off && hasAgentsMaxThreads(injectedText)) {
|
|
1230
|
+
console.warn("warning: [agents] max_threads is set; codex refuses to start with multi_agent_v2 enabled, remove it");
|
|
1231
|
+
}
|
|
901
1232
|
}
|
|
902
1233
|
catch (error) {
|
|
903
1234
|
if (startedProxy)
|
|
@@ -964,7 +1295,7 @@ export async function inject(config) {
|
|
|
964
1295
|
catch {
|
|
965
1296
|
console.warn("CPAC could not invalidate Codex models_cache.json; restart Codex App if its model list is stale.");
|
|
966
1297
|
}
|
|
967
|
-
console.log(`Injected ${catalog.modelCount} CPA models into ${config.codex_config} via http://127.0.0.1:${proxy.port}/v1`);
|
|
1298
|
+
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"})`);
|
|
968
1299
|
console.log("Restart Codex App if its running app-server still shows the old model list.");
|
|
969
1300
|
}
|
|
970
1301
|
export async function restore(config) {
|
|
@@ -1011,6 +1342,12 @@ export async function status(config) {
|
|
|
1011
1342
|
.slice(0, 12);
|
|
1012
1343
|
console.log(`codex: injected proxy=http://127.0.0.1:${proxy.port}/v1 sha256=${digest}`);
|
|
1013
1344
|
}
|
|
1345
|
+
try {
|
|
1346
|
+
console.log(`codex multi-agent v2: ${multiAgentV2Enabled(readFileSync(config.codex_config, "utf8")) ? "on" : "off"}`);
|
|
1347
|
+
}
|
|
1348
|
+
catch {
|
|
1349
|
+
// Unreadable config: the injected line above already reports it.
|
|
1350
|
+
}
|
|
1014
1351
|
}
|
|
1015
1352
|
}
|
|
1016
1353
|
console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
|
|
@@ -1090,6 +1427,60 @@ async function guide(config) {
|
|
|
1090
1427
|
console.log(`\nSaved ${config.api_key_env} to ${profile}. Open a new terminal or run:\n source ${profile}`);
|
|
1091
1428
|
return 0;
|
|
1092
1429
|
}
|
|
1430
|
+
export async function runClaudeModels(parsed) {
|
|
1431
|
+
if (parsed.pick.length) {
|
|
1432
|
+
const config = loadConfig(parsed.configPath);
|
|
1433
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
1434
|
+
if (!apiKey)
|
|
1435
|
+
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
1436
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1437
|
+
const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
1438
|
+
const slugs = document.models.map((model) => model.slug);
|
|
1439
|
+
for (const slot of parsed.pick) {
|
|
1440
|
+
const [picked] = await checkboxPicker(`Select ${slot} model`, slugs, 1);
|
|
1441
|
+
if (!picked)
|
|
1442
|
+
throw new CPACError("no model selected");
|
|
1443
|
+
parsed.models[slot] = picked;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (!parsed.reset && !Object.keys(parsed.models).length) {
|
|
1447
|
+
const config = loadConfig(parsed.configPath);
|
|
1448
|
+
console.log(config.claude_models
|
|
1449
|
+
? JSON.stringify(config.claude_models, null, 2)
|
|
1450
|
+
: "claude_models not set; cpac claude auto-picks tier models from the CPA catalog");
|
|
1451
|
+
return 0;
|
|
1452
|
+
}
|
|
1453
|
+
let raw = {};
|
|
1454
|
+
try {
|
|
1455
|
+
const file = JSON.parse(readFileSync(parsed.configPath, "utf8"));
|
|
1456
|
+
if (!objectValue(file))
|
|
1457
|
+
throw new CPACError("config must be a JSON object");
|
|
1458
|
+
raw = file;
|
|
1459
|
+
}
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
if (error.code !== "ENOENT") {
|
|
1462
|
+
if (error instanceof CPACError)
|
|
1463
|
+
throw error;
|
|
1464
|
+
throw new CPACError(`cannot read config: ${error instanceof Error ? error.message : String(error)}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (parsed.reset) {
|
|
1468
|
+
delete raw.claude_models;
|
|
1469
|
+
}
|
|
1470
|
+
else {
|
|
1471
|
+
const existing = objectValue(raw.claude_models)
|
|
1472
|
+
? { ...raw.claude_models }
|
|
1473
|
+
: {};
|
|
1474
|
+
Object.assign(existing, parsed.models);
|
|
1475
|
+
raw.claude_models = existing;
|
|
1476
|
+
}
|
|
1477
|
+
atomicWrite(parsed.configPath, Buffer.from(`${JSON.stringify(raw, null, 2)}\n`));
|
|
1478
|
+
loadConfig(parsed.configPath);
|
|
1479
|
+
console.log(parsed.reset
|
|
1480
|
+
? "claude_models cleared"
|
|
1481
|
+
: `claude_models updated: ${JSON.stringify(raw.claude_models)}`);
|
|
1482
|
+
return 0;
|
|
1483
|
+
}
|
|
1093
1484
|
export async function runClaude(config, args, executable = "claude") {
|
|
1094
1485
|
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1095
1486
|
const proxy = await createClaudeProxy(config.cpa_url, apiKey);
|
|
@@ -1097,22 +1488,31 @@ export async function runClaude(config, args, executable = "claude") {
|
|
|
1097
1488
|
proxy.server.closeAllConnections?.();
|
|
1098
1489
|
proxy.server.close();
|
|
1099
1490
|
};
|
|
1100
|
-
// Gateway discovery only carries {id, display_name}; Claude Code
|
|
1101
|
-
// unknown models
|
|
1102
|
-
//
|
|
1491
|
+
// Gateway discovery only carries {id, display_name}; Claude Code accounts
|
|
1492
|
+
// claude-prefixed unknown models at 200K and ignores MAX_CONTEXT_TOKENS for
|
|
1493
|
+
// them. The [1m] marker on the alias (added by the models listing) plus the
|
|
1494
|
+
// auto-compact window is opencodex's working mechanism for bigger windows.
|
|
1103
1495
|
let maxContext = 0;
|
|
1496
|
+
const catalogWindows = [];
|
|
1104
1497
|
try {
|
|
1105
1498
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1106
1499
|
const rows = catalogModelRows(JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes))) ?? [];
|
|
1107
1500
|
for (const row of rows) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1501
|
+
const id = catalogModelId(row);
|
|
1502
|
+
const window = typeof row.context_window === "number" && row.context_window > 0
|
|
1503
|
+
? Math.floor(row.context_window)
|
|
1504
|
+
: 0;
|
|
1505
|
+
if (id)
|
|
1506
|
+
catalogWindows.push({ id, window });
|
|
1507
|
+
if (window > maxContext)
|
|
1508
|
+
maxContext = window;
|
|
1111
1509
|
}
|
|
1112
1510
|
}
|
|
1113
1511
|
catch {
|
|
1114
1512
|
// keep the default window when the catalog is unreachable
|
|
1115
1513
|
}
|
|
1514
|
+
const compactWindow = claudeCompactWindow(maxContext);
|
|
1515
|
+
const slots = claudeTierSlots(config.claude_models, catalogWindows, compactWindow);
|
|
1116
1516
|
const env = {
|
|
1117
1517
|
...process.env,
|
|
1118
1518
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxy.port}`,
|
|
@@ -1120,9 +1520,23 @@ export async function runClaude(config, args, executable = "claude") {
|
|
|
1120
1520
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
1121
1521
|
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
|
|
1122
1522
|
};
|
|
1123
|
-
if (
|
|
1124
|
-
env.
|
|
1523
|
+
if (compactWindow !== undefined && !(Number(process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW) > 0)) {
|
|
1524
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(compactWindow);
|
|
1125
1525
|
}
|
|
1526
|
+
const setSlot = (name, value) => {
|
|
1527
|
+
if (process.env[name] === undefined)
|
|
1528
|
+
env[name] = value;
|
|
1529
|
+
};
|
|
1530
|
+
if (slots) {
|
|
1531
|
+
// Route haiku/subagent tier traffic at catalog models instead of the
|
|
1532
|
+
// stock claude-haiku-* ids the gateway does not carry.
|
|
1533
|
+
setSlot("ANTHROPIC_DEFAULT_OPUS_MODEL", slots.opus);
|
|
1534
|
+
setSlot("ANTHROPIC_DEFAULT_SONNET_MODEL", slots.sonnet);
|
|
1535
|
+
setSlot("ANTHROPIC_DEFAULT_HAIKU_MODEL", slots.haiku);
|
|
1536
|
+
setSlot("ANTHROPIC_SMALL_FAST_MODEL", slots.haiku);
|
|
1537
|
+
}
|
|
1538
|
+
if (config.claude_models?.classifier)
|
|
1539
|
+
setSlot("CLAUDE_CODE_AUTO_MODE_MODEL", config.claude_models.classifier);
|
|
1126
1540
|
delete env.ANTHROPIC_API_KEY;
|
|
1127
1541
|
delete env.CLAUDE_CODE_USE_ANTHROPIC_AWS;
|
|
1128
1542
|
delete env.CLAUDE_CODE_USE_BEDROCK;
|
|
@@ -1348,18 +1762,18 @@ const TARGETS = [
|
|
|
1348
1762
|
},
|
|
1349
1763
|
installed: (config) => {
|
|
1350
1764
|
try {
|
|
1351
|
-
return
|
|
1765
|
+
return readFileSync(config.codex_config, "utf8").includes(MANAGED_MARKER);
|
|
1352
1766
|
}
|
|
1353
1767
|
catch {
|
|
1354
1768
|
return false;
|
|
1355
1769
|
}
|
|
1356
1770
|
},
|
|
1357
|
-
install: async (config, dryRun) => {
|
|
1771
|
+
install: async (config, dryRun, v2Off) => {
|
|
1358
1772
|
if (dryRun) {
|
|
1359
1773
|
console.log("would inject CPA catalog and loopback proxy into Codex");
|
|
1360
1774
|
return;
|
|
1361
1775
|
}
|
|
1362
|
-
await inject(config);
|
|
1776
|
+
await inject(config, v2Off);
|
|
1363
1777
|
},
|
|
1364
1778
|
uninstall: async (config, dryRun) => {
|
|
1365
1779
|
if (dryRun) {
|
|
@@ -1462,7 +1876,7 @@ export async function runInstall(config, requested, options) {
|
|
|
1462
1876
|
console.log(`${target.id}: already installed; use --force to reinstall`);
|
|
1463
1877
|
continue;
|
|
1464
1878
|
}
|
|
1465
|
-
await target.install(config, options.dryRun);
|
|
1879
|
+
await target.install(config, options.dryRun, options.v2Off);
|
|
1466
1880
|
}
|
|
1467
1881
|
return 0;
|
|
1468
1882
|
}
|
|
@@ -1531,10 +1945,11 @@ function usage() {
|
|
|
1531
1945
|
" cpac <inject|status|restore> [--config PATH]",
|
|
1532
1946
|
" cpac proxy [--config PATH]",
|
|
1533
1947
|
" cpac claude [--config PATH] [--] [claude args...]",
|
|
1948
|
+
" cpac claude-models [--opus [M]] [--sonnet [M]] [--haiku [M]] [--classifier [M]] [--reset] [--config PATH]",
|
|
1534
1949
|
" cpac pi <install|uninstall|status> [--config PATH]",
|
|
1535
1950
|
" cpac kimi <install|uninstall|status> [--config PATH]",
|
|
1536
1951
|
" cpac detect [--json] [--home PATH]",
|
|
1537
|
-
" cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--home PATH]",
|
|
1952
|
+
" cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--home PATH]",
|
|
1538
1953
|
" cpac uninstall [--target codex,claude,pi,kimi] [--all] [--dry-run] [--home PATH]",
|
|
1539
1954
|
" cpac upgrade [--check]",
|
|
1540
1955
|
" cpac version",
|
|
@@ -1558,6 +1973,8 @@ function parseArgs(args) {
|
|
|
1558
1973
|
dryRun: false,
|
|
1559
1974
|
force: false,
|
|
1560
1975
|
check: false,
|
|
1976
|
+
v2Off: false,
|
|
1977
|
+
v2Models: false,
|
|
1561
1978
|
};
|
|
1562
1979
|
for (let index = 1; index < args.length; index++) {
|
|
1563
1980
|
const arg = args[index];
|
|
@@ -1592,6 +2009,12 @@ function parseArgs(args) {
|
|
|
1592
2009
|
throw new CPACError("--config requires a path");
|
|
1593
2010
|
parsed.configPath = resolve(expandUserPath(value));
|
|
1594
2011
|
}
|
|
2012
|
+
else if (arg === "--v2_off" || arg === "--v2-off") {
|
|
2013
|
+
parsed.v2Off = true;
|
|
2014
|
+
}
|
|
2015
|
+
else if (arg === "--v2_models" || arg === "--v2-models") {
|
|
2016
|
+
parsed.v2Models = true;
|
|
2017
|
+
}
|
|
1595
2018
|
else {
|
|
1596
2019
|
throw new CPACError(`unknown option: ${arg}`);
|
|
1597
2020
|
}
|
|
@@ -1611,6 +2034,62 @@ function parseArgs(args) {
|
|
|
1611
2034
|
const action = args[index] || "status";
|
|
1612
2035
|
return { command: args[0], configPath, action };
|
|
1613
2036
|
}
|
|
2037
|
+
if (args[0] === "claude-models") {
|
|
2038
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2039
|
+
console.log([
|
|
2040
|
+
"Usage: cpac claude-models [--opus [M]] [--sonnet [M]] [--haiku [M]] [--classifier [M]] [--reset] [--config PATH]",
|
|
2041
|
+
"",
|
|
2042
|
+
"Sets the model Claude Code uses per task tier. Saved to the config file",
|
|
2043
|
+
"and applied on every `cpac claude` launch:",
|
|
2044
|
+
" --opus heavy reasoning / planning fallback tier (ANTHROPIC_DEFAULT_OPUS_MODEL)",
|
|
2045
|
+
" --sonnet main conversation default tier (ANTHROPIC_DEFAULT_SONNET_MODEL)",
|
|
2046
|
+
" --haiku high-frequency small tasks: titles, summaries, background",
|
|
2047
|
+
" (ANTHROPIC_DEFAULT_HAIKU_MODEL + ANTHROPIC_SMALL_FAST_MODEL)",
|
|
2048
|
+
" --classifier auto-mode approval classifier (CLAUDE_CODE_AUTO_MODE_MODEL);",
|
|
2049
|
+
" unset follows Claude Code's built-in default",
|
|
2050
|
+
"",
|
|
2051
|
+
"A slot without a value opens an arrow-key picker over the CPA catalog;",
|
|
2052
|
+
"--reset clears all overrides back to catalog auto-selection.",
|
|
2053
|
+
].join("\n"));
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
let configPath = defaultConfigPath();
|
|
2057
|
+
const models = {};
|
|
2058
|
+
const pick = [];
|
|
2059
|
+
let reset = false;
|
|
2060
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
2061
|
+
const arg = args[index];
|
|
2062
|
+
if (arg === "--config") {
|
|
2063
|
+
const value = args[++index];
|
|
2064
|
+
if (!value)
|
|
2065
|
+
throw new CPACError("--config requires a path");
|
|
2066
|
+
configPath = resolve(expandUserPath(value));
|
|
2067
|
+
}
|
|
2068
|
+
else if (arg === "--reset") {
|
|
2069
|
+
reset = true;
|
|
2070
|
+
}
|
|
2071
|
+
else if (arg === "--opus" ||
|
|
2072
|
+
arg === "--sonnet" ||
|
|
2073
|
+
arg === "--haiku" ||
|
|
2074
|
+
arg === "--classifier") {
|
|
2075
|
+
const slot = arg.slice(2);
|
|
2076
|
+
const value = args[index + 1];
|
|
2077
|
+
if (value !== undefined && !value.startsWith("-")) {
|
|
2078
|
+
index += 1;
|
|
2079
|
+
if (!value.trim())
|
|
2080
|
+
throw new CPACError(`${arg} requires a model id`);
|
|
2081
|
+
models[slot] = value.trim();
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
pick.push(slot);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
else {
|
|
2088
|
+
throw new CPACError(`unknown option: ${arg}`);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return { command: "claude-models", configPath, models, pick, reset };
|
|
2092
|
+
}
|
|
1614
2093
|
if (args[0] === "claude") {
|
|
1615
2094
|
let configPath = defaultConfigPath();
|
|
1616
2095
|
let index = 1;
|
|
@@ -1671,12 +2150,21 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1671
2150
|
return await guide(config);
|
|
1672
2151
|
if (parsed.command === "detect")
|
|
1673
2152
|
return await runDetect(config, parsed.json);
|
|
1674
|
-
if (parsed.command === "install")
|
|
1675
|
-
|
|
2153
|
+
if (parsed.command === "install") {
|
|
2154
|
+
let installConfig = config;
|
|
2155
|
+
if (parsed.v2Models) {
|
|
2156
|
+
const picked = await pickSpawnModels(config);
|
|
2157
|
+
saveSpawnModels(parsed.configPath, picked);
|
|
2158
|
+
console.log(`spawn_models saved to ${parsed.configPath}: ${picked.join(", ")}`);
|
|
2159
|
+
installConfig = { ...config, spawn_models: picked };
|
|
2160
|
+
}
|
|
2161
|
+
return await runInstall(installConfig, parsed.targets, {
|
|
1676
2162
|
all: parsed.all,
|
|
1677
2163
|
dryRun: parsed.dryRun,
|
|
1678
2164
|
force: parsed.force,
|
|
2165
|
+
v2Off: parsed.v2Off,
|
|
1679
2166
|
});
|
|
2167
|
+
}
|
|
1680
2168
|
if (parsed.command === "uninstall")
|
|
1681
2169
|
return await runUninstall(config, parsed.targets, {
|
|
1682
2170
|
all: parsed.all,
|
|
@@ -1690,6 +2178,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1690
2178
|
}
|
|
1691
2179
|
if (parsed.command === "claude")
|
|
1692
2180
|
return await runClaude(config, parsed.args);
|
|
2181
|
+
if (parsed.command === "claude-models")
|
|
2182
|
+
return await runClaudeModels(parsed);
|
|
1693
2183
|
if (parsed.command === "proxy")
|
|
1694
2184
|
return await runProxy(config);
|
|
1695
2185
|
if (parsed.command === "pi")
|