@yhong91/cpac 0.1.32 → 0.1.33
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 +7 -8
- package/cpac.example.json +3 -1
- package/dist/agents.js +64 -23
- package/dist/config.js +93 -11
- package/dist/cpac.js +28 -5
- package/dist/targets/claude.js +39 -18
- package/dist/targets/codex.js +31 -4
- package/dist/targets/pi.js +119 -35
- package/dist/util.js +173 -0
- package/{dist/pi-extension.template → extensions/pi-cpac.ts} +91 -36
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -174,8 +174,8 @@ cpac -v | --version
|
|
|
174
174
|
|
|
175
175
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
176
176
|
- `install` 默认作用于检测到的目标;`--target` 指定目标、`--all` 全部。各目标映射到既有逻辑:`codex` → `cpac inject`,`pi` → `cpac pi install`,`kimi` → `cpac kimi install`。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
177
|
-
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi
|
|
178
|
-
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`);`install --target codex --v2_off`
|
|
177
|
+
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi 确认 `npm:@yhong91/cpac` 并清理旧的 `extensions/pi-cpac.ts`、Kimi 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `--target codex` 有效。
|
|
178
|
+
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`,catalog 行打 `multi_agent_version: "v2"`);`install --target codex --v2_off` 关掉 toml 并去掉 catalog 戳。v2 开着时会剥掉 `[agents] max_threads`(Codex 否则拒启动)。`cpac status` 对照 config 里的 `v2_off` / `max_context` 和 live toml / catalog。服务端还需在 CPA 配置中开启 `codex.optimize-multi-agent-v2`。
|
|
179
179
|
- `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
|
|
180
180
|
- `restore` 必须带 `--target` 或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
|
|
181
181
|
- `uninstall` 先用和 `cpac codex --setup` 相同的方向键选择器确认(默认 Cancel),再 `restore --all`,最后 `npm uninstall -g @yhong91/cpac`。不要用 `--target`。非终端取消。`--dry-run` 跳过确认。
|
|
@@ -248,7 +248,7 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。`inject` 会删除 `mod
|
|
|
248
248
|
|
|
249
249
|
注入命令可选 `--max_context`(`cpac inject --max_context` 或 `cpac install --target codex --max_context`,默认不带):把 catalog 中 `max_context_window` 高于 `context_window` 的模型提升到上限,并把 `auto_compact_token_limit` 设为上限的 90%。Codex 把 `context_window` 当输入预算而非展示标签,上游默认保留保守运营值(如 GPT-5.6 家族 272k),带上该参数后可用到实测上限(约 921k,opencodex 实测);90% 压缩线确保在硬上限前触发 auto-compact。不带参数注入的仍是原本上下文的目录。该参数仅限 codex,与其他 target 组合使用会直接报错。
|
|
250
250
|
|
|
251
|
-
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再 inject 不覆盖备份。kimi/grok
|
|
251
|
+
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再 inject 不覆盖备份。kimi/grok 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。Pi 只改 `settings.json` 的 `packages`,不备份。`restore --target codex`:若 `config.toml` 相对注入未改,按原 mode 逐字节还原备份;若用户在注入期间改过文件,只撤掉 CPAC 写入的 catalog、url 和标记。没有其它需要代理的 agent 时才关闭代理。
|
|
252
252
|
|
|
253
253
|
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac inject` 或 `cpac kimi` / `cpac grok` 即可恢复。
|
|
254
254
|
|
|
@@ -267,7 +267,6 @@ Codex 配置:$CODEX_HOME/config.toml,未设置 CODEX_HOME 时为 ~/.codex/co
|
|
|
267
267
|
CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录)
|
|
268
268
|
各 agent 配置备份:~/.cpac/<agent>/<原文件名>
|
|
269
269
|
Codex ~/.cpac/codex/config.toml
|
|
270
|
-
Pi ~/.cpac/pi/pi-cpac.ts
|
|
271
270
|
Kimi ~/.cpac/kimi/config.toml
|
|
272
271
|
Grok ~/.cpac/grok/config.toml
|
|
273
272
|
restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
|
|
@@ -291,13 +290,13 @@ cpac opencode [--] [opencode args...]
|
|
|
291
290
|
cpac pi install
|
|
292
291
|
```
|
|
293
292
|
|
|
294
|
-
|
|
293
|
+
在 `~/.pi/agent/settings.json` 的 `packages` 中加入 `npm:@yhong91/cpac`。Pi 启动时按 npm 包加载 `extensions/pi-cpac.ts`,从 CPA 动态注册模型。旧版写入的 `~/.pi/agent/extensions/pi-cpac.ts` 会在 install / restore 时删掉。卸载:
|
|
295
294
|
|
|
296
295
|
```bash
|
|
297
296
|
cpac restore --target pi
|
|
298
297
|
```
|
|
299
298
|
|
|
300
|
-
|
|
299
|
+
尊重 `PI_CODING_AGENT_DIR`。扩展代码随 npm 包更新,已装过的副本用 `pi update npm:@yhong91/cpac`(或 `pi update --extensions`)。
|
|
301
300
|
|
|
302
301
|
### Kimi Code
|
|
303
302
|
|
|
@@ -321,7 +320,7 @@ Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若
|
|
|
321
320
|
| --- | --- | --- |
|
|
322
321
|
| `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
|
|
323
322
|
| `CPA_BASE_URL` | `http://124.223.178.52:8317` | 覆盖内置 CPA 地址 |
|
|
324
|
-
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `
|
|
323
|
+
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `settings.json` 写入位置) |
|
|
325
324
|
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac kimi install` 写入位置) |
|
|
326
325
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
327
326
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
@@ -332,7 +331,7 @@ CPAC CLI 可临时覆盖 CPA 地址:
|
|
|
332
331
|
CPA_BASE_URL='https://cpa.example.com' cpac claude
|
|
333
332
|
```
|
|
334
333
|
|
|
335
|
-
|
|
334
|
+
Pi 扩展只从进程环境读取 `CPA_BASE_URL`(默认内置地址)和 `CPA_API_KEY`;不读取 `config.json` 的 `cpa_url`,也不读用户 home 中的私有文件。
|
|
336
335
|
|
|
337
336
|
## 可选 JSON 配置
|
|
338
337
|
|
package/cpac.example.json
CHANGED
package/dist/agents.js
CHANGED
|
@@ -3,12 +3,16 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { pickSpawnModels, saveSpawnModels } from "./config.js";
|
|
6
|
+
import { MAX_SPAWN_MODELS, loadCatalogSlugs, pickSpawnModels, saveCodexSetup, saveSpawnModels, } from "./config.js";
|
|
7
7
|
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
8
8
|
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
9
9
|
import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
10
|
-
import { installPiExtension, isPiExtensionInstalled,
|
|
11
|
-
import { CPACError, atomicWrite, checkboxPicker, expandUserPath, objectValue, } from "./util.js";
|
|
10
|
+
import { PI_PACKAGE, installPiExtension, isPiExtensionInstalled, piAgentDir, piSettingsPath, uninstallPiExtension, } from "./targets/pi.js";
|
|
11
|
+
import { CPACError, atomicWrite, checkboxPicker, expandUserPath, objectValue, tabWizard, } from "./util.js";
|
|
12
|
+
const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
|
|
13
|
+
const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
|
|
14
|
+
const CODEX_V2_ON = "Enabled (Default)";
|
|
15
|
+
const CODEX_V2_OFF = "Disabled (--v2_off)";
|
|
12
16
|
export async function runCodexConfig(config, configPath, options = {}) {
|
|
13
17
|
if (options.reset) {
|
|
14
18
|
let raw = {};
|
|
@@ -26,38 +30,75 @@ export async function runCodexConfig(config, configPath, options = {}) {
|
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
delete raw.spawn_models;
|
|
33
|
+
delete raw.max_context;
|
|
34
|
+
delete raw.v2_off;
|
|
29
35
|
atomicWrite(configPath, Buffer.from(`${JSON.stringify(raw, null, 2)}\n`));
|
|
30
36
|
await runRestore(config, ["codex"], { all: false, dryRun: false });
|
|
31
37
|
console.log("Codex configuration reset to default");
|
|
32
38
|
return 0;
|
|
33
39
|
}
|
|
34
40
|
if (options.interactive && (!process.stdin.isTTY || !process.stderr.isTTY)) {
|
|
35
|
-
console.log(
|
|
36
|
-
|
|
37
|
-
:
|
|
41
|
+
console.log(JSON.stringify({
|
|
42
|
+
spawn_models: config.spawn_models ?? [],
|
|
43
|
+
max_context: !!config.max_context,
|
|
44
|
+
v2_off: !!config.v2_off,
|
|
45
|
+
}, null, 2));
|
|
38
46
|
return 0;
|
|
39
47
|
}
|
|
40
48
|
let spawnModels = config.spawn_models;
|
|
41
|
-
let v2Off = options.v2Off
|
|
42
|
-
let maxContext = options.maxContext
|
|
43
|
-
if (options.interactive
|
|
49
|
+
let v2Off = options.v2Off || !!config.v2_off;
|
|
50
|
+
let maxContext = options.maxContext || !!config.max_context;
|
|
51
|
+
if (options.interactive) {
|
|
52
|
+
const slugs = await loadCatalogSlugs(config);
|
|
53
|
+
const picked = await tabWizard([
|
|
54
|
+
{
|
|
55
|
+
id: "models",
|
|
56
|
+
label: "Models",
|
|
57
|
+
items: slugs,
|
|
58
|
+
max: MAX_SPAWN_MODELS,
|
|
59
|
+
selected: config.spawn_models?.filter((slug) => slugs.includes(slug)),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "context",
|
|
63
|
+
label: "Context",
|
|
64
|
+
items: [CODEX_CTX_STD, CODEX_CTX_MAX],
|
|
65
|
+
max: 1,
|
|
66
|
+
selected: [maxContext ? CODEX_CTX_MAX : CODEX_CTX_STD],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "v2",
|
|
70
|
+
label: "V2",
|
|
71
|
+
items: [CODEX_V2_ON, CODEX_V2_OFF],
|
|
72
|
+
max: 1,
|
|
73
|
+
selected: [v2Off ? CODEX_V2_OFF : CODEX_V2_ON],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
if (picked.models.length === 0)
|
|
77
|
+
throw new CPACError("no models selected");
|
|
78
|
+
spawnModels = picked.models;
|
|
79
|
+
maxContext = picked.context[0] === CODEX_CTX_MAX;
|
|
80
|
+
v2Off = picked.v2[0] === CODEX_V2_OFF;
|
|
81
|
+
saveCodexSetup(configPath, {
|
|
82
|
+
spawn_models: picked.models,
|
|
83
|
+
max_context: maxContext,
|
|
84
|
+
v2_off: v2Off,
|
|
85
|
+
});
|
|
86
|
+
console.log(`codex setup saved to ${configPath}: models=${picked.models.join(", ")} max_context=${maxContext} v2_off=${v2Off}`);
|
|
87
|
+
}
|
|
88
|
+
else if (options.v2Models) {
|
|
44
89
|
const picked = await pickSpawnModels(config);
|
|
45
90
|
if (picked.length > 0) {
|
|
46
91
|
spawnModels = picked;
|
|
47
92
|
saveSpawnModels(configPath, picked);
|
|
48
93
|
console.log(`spawn_models saved to ${configPath}: ${picked.join(", ")}`);
|
|
49
94
|
}
|
|
50
|
-
if (options.interactive) {
|
|
51
|
-
const [maxCtxChoice] = await checkboxPicker("Codex Context Window Size:", [
|
|
52
|
-
"Standard (default input budget, e.g. 200K~272K)",
|
|
53
|
-
"Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)",
|
|
54
|
-
], 1);
|
|
55
|
-
maxContext = maxCtxChoice?.startsWith("Max Context") ?? false;
|
|
56
|
-
const [v2Choice] = await checkboxPicker("Codex Multi-Agent V2 mode:", ["Enabled (Default)", "Disabled (--v2_off)"], 1);
|
|
57
|
-
v2Off = v2Choice === "Disabled (--v2_off)";
|
|
58
|
-
}
|
|
59
95
|
}
|
|
60
|
-
const updatedConfig = {
|
|
96
|
+
const updatedConfig = {
|
|
97
|
+
...config,
|
|
98
|
+
spawn_models: spawnModels,
|
|
99
|
+
max_context: maxContext || undefined,
|
|
100
|
+
v2_off: v2Off || undefined,
|
|
101
|
+
};
|
|
61
102
|
await runInstall(updatedConfig, ["codex"], {
|
|
62
103
|
all: false,
|
|
63
104
|
dryRun: false,
|
|
@@ -174,20 +215,20 @@ const TARGETS = [
|
|
|
174
215
|
},
|
|
175
216
|
{
|
|
176
217
|
id: "pi",
|
|
177
|
-
home: () =>
|
|
178
|
-
detected: () => binaryOnPath("pi") || existsSync(
|
|
218
|
+
home: () => piAgentDir(),
|
|
219
|
+
detected: () => binaryOnPath("pi") || existsSync(piAgentDir()),
|
|
179
220
|
installed: () => isPiExtensionInstalled(),
|
|
180
221
|
version: () => detectClientVersion("pi"),
|
|
181
222
|
install: async (config, dryRun) => {
|
|
182
223
|
if (dryRun) {
|
|
183
|
-
console.log(`would
|
|
224
|
+
console.log(`would add ${PI_PACKAGE} to ${piSettingsPath()}`);
|
|
184
225
|
return;
|
|
185
226
|
}
|
|
186
227
|
await installPiExtension(config);
|
|
187
228
|
},
|
|
188
229
|
uninstall: async (config, dryRun) => {
|
|
189
230
|
if (dryRun) {
|
|
190
|
-
console.log(`would remove
|
|
231
|
+
console.log(`would remove ${PI_PACKAGE} from ${piSettingsPath()}`);
|
|
191
232
|
return;
|
|
192
233
|
}
|
|
193
234
|
await uninstallPiExtension(config);
|
package/dist/config.js
CHANGED
|
@@ -15,10 +15,12 @@ const CONFIG_KEYS = new Set([
|
|
|
15
15
|
"state_dir",
|
|
16
16
|
"claude_models",
|
|
17
17
|
"spawn_models",
|
|
18
|
+
"max_context",
|
|
19
|
+
"v2_off",
|
|
18
20
|
]);
|
|
19
21
|
// Codex advertises only the first 5 picker-visible catalog models as
|
|
20
22
|
// spawn_agent overrides (codex-rs MAX_SPAWN_AGENT_MODEL_OVERRIDES).
|
|
21
|
-
const MAX_SPAWN_MODELS = 5;
|
|
23
|
+
export const MAX_SPAWN_MODELS = 5;
|
|
22
24
|
export const CLAUDE_SLOT_KEYS = ["opus", "sonnet", "haiku"];
|
|
23
25
|
export const AGENT_IDS = ["codex", "pi", "kimi", "grok"];
|
|
24
26
|
const AGENT_ID_SET = new Set(AGENT_IDS);
|
|
@@ -143,6 +145,12 @@ export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
|
143
145
|
return model.trim();
|
|
144
146
|
});
|
|
145
147
|
}
|
|
148
|
+
const maxContextRaw = value.max_context;
|
|
149
|
+
if (maxContextRaw !== undefined && typeof maxContextRaw !== "boolean")
|
|
150
|
+
throw new CPACError("max_context must be a boolean");
|
|
151
|
+
const v2OffRaw = value.v2_off;
|
|
152
|
+
if (v2OffRaw !== undefined && typeof v2OffRaw !== "boolean")
|
|
153
|
+
throw new CPACError("v2_off must be a boolean");
|
|
146
154
|
const codexHome = expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
147
155
|
const codexConfig = resolve(expandUserPath(value.codex_config || join(codexHome, "config.toml")));
|
|
148
156
|
const stateDir = resolve(expandUserPath(value.state_dir || join(homedir(), ".cpac")));
|
|
@@ -160,6 +168,8 @@ export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
|
160
168
|
state_dir: stateDir,
|
|
161
169
|
claude_models: claudeModels,
|
|
162
170
|
spawn_models: spawnModels,
|
|
171
|
+
max_context: maxContextRaw === true ? true : undefined,
|
|
172
|
+
v2_off: v2OffRaw === true ? true : undefined,
|
|
163
173
|
};
|
|
164
174
|
}
|
|
165
175
|
export function apiBase(cpaUrl) {
|
|
@@ -233,6 +243,10 @@ export async function fetchCatalog(cpaUrl, apiKey) {
|
|
|
233
243
|
if (models.some((model) => !objectValue(model) || typeof model.slug !== "string" || !model.slug.trim())) {
|
|
234
244
|
throw new CPACError("CPA catalog contains a model without a slug");
|
|
235
245
|
}
|
|
246
|
+
for (const model of models) {
|
|
247
|
+
if (objectValue(model))
|
|
248
|
+
sanitizeModelModalities(model);
|
|
249
|
+
}
|
|
236
250
|
return {
|
|
237
251
|
bytes: Buffer.from(`${JSON.stringify(document, null, 2)}\n`),
|
|
238
252
|
modelCount: models.length,
|
|
@@ -292,7 +306,65 @@ export function reorderCatalog(bytes, order) {
|
|
|
292
306
|
document.models = models.map((entry) => entry.model);
|
|
293
307
|
return Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
294
308
|
}
|
|
295
|
-
|
|
309
|
+
function catalogDocument(bytes) {
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
throw new CPACError("invalid CPA catalog");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Codex parses input_modalities as a closed enum. One extra value (e.g. "video")
|
|
318
|
+
// rejects the entire catalog. Keep unknown fields, but coerce this one.
|
|
319
|
+
function sanitizeModelModalities(model) {
|
|
320
|
+
if (!("input_modalities" in model))
|
|
321
|
+
return;
|
|
322
|
+
const raw = model.input_modalities;
|
|
323
|
+
const accepted = Array.isArray(raw)
|
|
324
|
+
? raw.filter((value) => value === "text" || value === "image" || value === "audio")
|
|
325
|
+
: [];
|
|
326
|
+
model.input_modalities = accepted.length > 0 ? accepted : ["text"];
|
|
327
|
+
}
|
|
328
|
+
export function sanitizeCatalogModalities(bytes) {
|
|
329
|
+
const document = catalogDocument(bytes);
|
|
330
|
+
for (const model of document.models) {
|
|
331
|
+
if (objectValue(model))
|
|
332
|
+
sanitizeModelModalities(model);
|
|
333
|
+
}
|
|
334
|
+
return Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
335
|
+
}
|
|
336
|
+
export function stampMultiAgentVersion(bytes, enabled) {
|
|
337
|
+
const document = catalogDocument(bytes);
|
|
338
|
+
for (const model of document.models) {
|
|
339
|
+
if (!objectValue(model))
|
|
340
|
+
continue;
|
|
341
|
+
if (enabled)
|
|
342
|
+
model.multi_agent_version = "v2";
|
|
343
|
+
else
|
|
344
|
+
delete model.multi_agent_version;
|
|
345
|
+
}
|
|
346
|
+
return Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
347
|
+
}
|
|
348
|
+
export function catalogMaxContextLive(bytes) {
|
|
349
|
+
const document = catalogDocument(bytes);
|
|
350
|
+
for (const model of document.models) {
|
|
351
|
+
if (!objectValue(model))
|
|
352
|
+
continue;
|
|
353
|
+
const maxWindow = typeof model.max_context_window === "number" && model.max_context_window > 0
|
|
354
|
+
? Math.floor(model.max_context_window)
|
|
355
|
+
: 0;
|
|
356
|
+
const window = typeof model.context_window === "number"
|
|
357
|
+
? Math.floor(model.context_window)
|
|
358
|
+
: 0;
|
|
359
|
+
if (maxWindow > 0 &&
|
|
360
|
+
window === maxWindow &&
|
|
361
|
+
model.auto_compact_token_limit === Math.floor(maxWindow * 0.9)) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
function patchConfig(configPath, fields) {
|
|
296
368
|
let document = {};
|
|
297
369
|
if (existsSync(configPath)) {
|
|
298
370
|
let parsed;
|
|
@@ -306,23 +378,33 @@ export function saveSpawnModels(configPath, models) {
|
|
|
306
378
|
throw new CPACError("config must be a JSON object");
|
|
307
379
|
document = parsed;
|
|
308
380
|
}
|
|
309
|
-
document
|
|
381
|
+
Object.assign(document, fields);
|
|
310
382
|
atomicWrite(configPath, Buffer.from(`${JSON.stringify(document, null, 2)}\n`));
|
|
311
383
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
384
|
+
export function saveSpawnModels(configPath, models) {
|
|
385
|
+
patchConfig(configPath, { spawn_models: models });
|
|
386
|
+
}
|
|
387
|
+
export function saveCodexSetup(configPath, setup) {
|
|
388
|
+
patchConfig(configPath, setup);
|
|
389
|
+
}
|
|
390
|
+
export function catalogSlugs(bytes) {
|
|
318
391
|
let document;
|
|
319
392
|
try {
|
|
320
|
-
document = JSON.parse(new TextDecoder().decode(
|
|
393
|
+
document = JSON.parse(new TextDecoder().decode(bytes));
|
|
321
394
|
}
|
|
322
395
|
catch {
|
|
323
396
|
throw new CPACError("invalid CPA catalog");
|
|
324
397
|
}
|
|
325
|
-
|
|
398
|
+
return document.models.map((model) => model.slug);
|
|
399
|
+
}
|
|
400
|
+
export async function loadCatalogSlugs(config) {
|
|
401
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
402
|
+
if (!apiKey)
|
|
403
|
+
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
404
|
+
return catalogSlugs((await fetchCatalog(config.cpa_url, apiKey)).bytes);
|
|
405
|
+
}
|
|
406
|
+
export async function pickSpawnModels(config) {
|
|
407
|
+
const slugs = await loadCatalogSlugs(config);
|
|
326
408
|
const picked = await checkboxPicker("Select spawn_agent models", slugs, MAX_SPAWN_MODELS);
|
|
327
409
|
if (picked.length === 0)
|
|
328
410
|
throw new CPACError("no models selected");
|
package/dist/cpac.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { CPAC_VERSION, runCodexConfig, runDetect, runInstall, runRestore, runSync, runTargetLauncher, runUninstall, runUpgrade, } from "./agents.js";
|
|
7
7
|
import { runClaude, runClaudeConfig, runClaudeModels, } from "./targets/claude.js";
|
|
@@ -10,13 +10,13 @@ import { isGrokConfigInstalled } from "./targets/grok.js";
|
|
|
10
10
|
import { isKimiConfigInstalled } from "./targets/kimi.js";
|
|
11
11
|
import { runOpencode } from "./targets/opencode.js";
|
|
12
12
|
import { isPiExtensionInstalled } from "./targets/pi.js";
|
|
13
|
-
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, } from "./config.js";
|
|
13
|
+
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, } from "./config.js";
|
|
14
14
|
import { proxyIsHealthy, runProxy, runProxyChild } from "./proxy.js";
|
|
15
15
|
import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
|
|
16
|
-
export { PROVIDER, loadConfig, saveSpawnModels, liftContextWindows, reorderCatalog, readState, stateProxy, } from "./config.js";
|
|
16
|
+
export { PROVIDER, loadConfig, saveSpawnModels, saveCodexSetup, liftContextWindows, reorderCatalog, sanitizeCatalogModalities, stampMultiAgentVersion, catalogMaxContextLive, readState, stateProxy, } from "./config.js";
|
|
17
17
|
export { saveApiKeyExport } from "./util.js";
|
|
18
18
|
export { stopProxyProcess } from "./proxy.js";
|
|
19
|
-
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, withMultiAgentV2, } from "./targets/codex.js";
|
|
19
|
+
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, stripAgentsMaxThreads, withMultiAgentV2, } from "./targets/codex.js";
|
|
20
20
|
export { claudeTierSlots, createClaudeProxy, runClaude, } from "./targets/claude.js";
|
|
21
21
|
export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, runTargetLauncher, } from "./agents.js";
|
|
22
22
|
export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
@@ -42,11 +42,23 @@ export async function status(config) {
|
|
|
42
42
|
console.log(`codex: injected proxy=http://127.0.0.1:${proxy.port}/v1 sha256=${digest}`);
|
|
43
43
|
}
|
|
44
44
|
try {
|
|
45
|
-
|
|
45
|
+
const liveV2 = multiAgentV2Enabled(readFileSync(config.codex_config, "utf8"));
|
|
46
|
+
console.log(`codex v2: expected=${config.v2_off ? "off" : "on"} live=${liveV2 ? "on" : "off"}`);
|
|
46
47
|
}
|
|
47
48
|
catch {
|
|
48
49
|
// Unreadable config: the injected line above already reports it.
|
|
49
50
|
}
|
|
51
|
+
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
52
|
+
let liveMax = false;
|
|
53
|
+
if (existsSync(catalogPath)) {
|
|
54
|
+
try {
|
|
55
|
+
liveMax = catalogMaxContextLive(readFileSync(catalogPath));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Unreadable catalog: report live=off.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log(`codex max_context: expected=${config.max_context ? "on" : "off"} live=${liveMax ? "on" : "off"}`);
|
|
50
62
|
}
|
|
51
63
|
else {
|
|
52
64
|
console.log(`codex: injected (config missing: ${config.codex_config})`);
|
|
@@ -223,6 +235,17 @@ function parseArgs(args) {
|
|
|
223
235
|
interactive: false,
|
|
224
236
|
};
|
|
225
237
|
}
|
|
238
|
+
if (targetId === "codex") {
|
|
239
|
+
return {
|
|
240
|
+
command: "codex-config",
|
|
241
|
+
configPath,
|
|
242
|
+
v2Off: false,
|
|
243
|
+
v2Models: false,
|
|
244
|
+
maxContext: false,
|
|
245
|
+
reset: true,
|
|
246
|
+
interactive: false,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
226
249
|
return {
|
|
227
250
|
command: "restore",
|
|
228
251
|
configPath,
|
package/dist/targets/claude.js
CHANGED
|
@@ -2,9 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { createServer, request as httpRequest, } from "node:http";
|
|
4
4
|
import { request as httpsRequest } from "node:https";
|
|
5
|
-
import { apiBase, catalogModelId, catalogModelRows, fetchCatalog, loadConfig, } from "../config.js";
|
|
5
|
+
import { apiBase, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, loadConfig, } from "../config.js";
|
|
6
6
|
import { proxyHeaders, responseHeaders, upstreamUrl } from "../proxy.js";
|
|
7
|
-
import { CPACError, atomicWrite,
|
|
7
|
+
import { CPACError, atomicWrite, objectValue, resolveApiKey, tabWizard, } from "../util.js";
|
|
8
8
|
const CLAUDE_ALIAS_PREFIX = "claude-cpac--";
|
|
9
9
|
// Claude Code accepts CLAUDE_CODE_AUTO_COMPACT_WINDOW in 100K–1M (binary-verified).
|
|
10
10
|
// A single global window cannot be per-model; 350K is opencodex's user-approved
|
|
@@ -235,25 +235,34 @@ export async function runClaudeConfig(config, configPath, options = {}) {
|
|
|
235
235
|
if (shouldPick) {
|
|
236
236
|
const apiKey = await resolveApiKey(config.api_key_env);
|
|
237
237
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
throw new CPACError("CPA catalog response is not valid JSON");
|
|
244
|
-
}
|
|
245
|
-
const slugs = (document.models ?? [])
|
|
246
|
-
.map((model) => (typeof model?.slug === "string" ? model.slug : ""))
|
|
247
|
-
.filter(Boolean);
|
|
238
|
+
const slugs = catalogSlugs(catalog.bytes);
|
|
239
|
+
const auto = "(auto-select from catalog)";
|
|
248
240
|
const slotsToPick = options.pick && options.pick.length > 0
|
|
249
241
|
? options.pick
|
|
250
242
|
: ["sonnet", "haiku", "opus"];
|
|
243
|
+
const current = { ...config.claude_models, ...models };
|
|
244
|
+
const picked = await tabWizard(slotsToPick.map((slot) => {
|
|
245
|
+
const items = slot === "sonnet" ? slugs : [auto, ...slugs];
|
|
246
|
+
const preset = current[slot];
|
|
247
|
+
const selected = preset && slugs.includes(preset)
|
|
248
|
+
? [preset]
|
|
249
|
+
: slot === "sonnet"
|
|
250
|
+
? undefined
|
|
251
|
+
: [auto];
|
|
252
|
+
return {
|
|
253
|
+
id: slot,
|
|
254
|
+
label: slot.toUpperCase(),
|
|
255
|
+
items,
|
|
256
|
+
max: 1,
|
|
257
|
+
selected,
|
|
258
|
+
};
|
|
259
|
+
}));
|
|
251
260
|
for (const slot of slotsToPick) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
const choice = picked[slot]?.[0];
|
|
262
|
+
if (choice && choice !== auto)
|
|
263
|
+
models[slot] = choice;
|
|
264
|
+
else
|
|
265
|
+
delete models[slot];
|
|
257
266
|
}
|
|
258
267
|
}
|
|
259
268
|
if (!Object.keys(models).length && !shouldPick) {
|
|
@@ -280,7 +289,19 @@ export async function runClaudeConfig(config, configPath, options = {}) {
|
|
|
280
289
|
? { ...raw.claude_models }
|
|
281
290
|
: {};
|
|
282
291
|
Object.assign(existing, models);
|
|
283
|
-
|
|
292
|
+
if (shouldPick) {
|
|
293
|
+
const slots = options.pick && options.pick.length > 0
|
|
294
|
+
? options.pick
|
|
295
|
+
: ["sonnet", "haiku", "opus"];
|
|
296
|
+
for (const slot of slots) {
|
|
297
|
+
if (!models[slot])
|
|
298
|
+
delete existing[slot];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (Object.keys(existing).length === 0)
|
|
302
|
+
delete raw.claude_models;
|
|
303
|
+
else
|
|
304
|
+
raw.claude_models = existing;
|
|
284
305
|
atomicWrite(configPath, Buffer.from(`${JSON.stringify(raw, null, 2)}\n`));
|
|
285
306
|
loadConfig(configPath);
|
|
286
307
|
console.log(`Claude Code tier models updated in ${configPath}:\n${JSON.stringify(raw.claude_models, null, 2)}`);
|
package/dist/targets/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { PROVIDER, STATE_FILES, cleanupStateFiles, dropAgent, fetchCatalog, injectedOwnership, liftContextWindows, originalBytes, readState, reorderCatalog, saveState, sha256Hex, stateBytes, } from "../config.js";
|
|
3
|
+
import { PROVIDER, STATE_FILES, cleanupStateFiles, dropAgent, fetchCatalog, injectedOwnership, liftContextWindows, originalBytes, readState, reorderCatalog, stampMultiAgentVersion, saveState, sha256Hex, stateBytes, } from "../config.js";
|
|
4
4
|
import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
|
|
5
5
|
import { CPACError, atomicWrite, dominantEol, tomlString } from "../util.js";
|
|
6
6
|
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
@@ -239,6 +239,27 @@ function hasAgentsMaxThreads(content) {
|
|
|
239
239
|
}
|
|
240
240
|
return false;
|
|
241
241
|
}
|
|
242
|
+
export function stripAgentsMaxThreads(content) {
|
|
243
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
244
|
+
const kept = [];
|
|
245
|
+
let table = "root";
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (/^\s*\[/.test(line)) {
|
|
248
|
+
table = /^\s*\[\s*(?:agents|"agents"|'agents')\s*\]\s*(?:#.*)?$/.test(line)
|
|
249
|
+
? "agents"
|
|
250
|
+
: "other";
|
|
251
|
+
kept.push(line);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (table === "agents" && /^\s*max_threads\s*=/.test(line))
|
|
255
|
+
continue;
|
|
256
|
+
if (table === "root" &&
|
|
257
|
+
/^\s*(?:agents|"agents"|'agents')\s*\.\s*max_threads\s*=/.test(line))
|
|
258
|
+
continue;
|
|
259
|
+
kept.push(line);
|
|
260
|
+
}
|
|
261
|
+
return kept.join("\n");
|
|
262
|
+
}
|
|
242
263
|
export function withMultiAgentV2(content, enabled, eol) {
|
|
243
264
|
let body = stripMultiAgentV2(content);
|
|
244
265
|
if (body && !body.endsWith("\n"))
|
|
@@ -247,6 +268,8 @@ export function withMultiAgentV2(content, enabled, eol) {
|
|
|
247
268
|
return eol === "\n" ? output : output.replace(/\n/g, "\r\n");
|
|
248
269
|
}
|
|
249
270
|
export async function inject(config, v2Off = false, maxContext = false) {
|
|
271
|
+
v2Off = v2Off || !!config.v2_off;
|
|
272
|
+
maxContext = maxContext || !!config.max_context;
|
|
250
273
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
251
274
|
if (!apiKey)
|
|
252
275
|
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
@@ -288,6 +311,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
288
311
|
catalog.bytes = reorderCatalog(catalog.bytes, config.spawn_models);
|
|
289
312
|
if (maxContext)
|
|
290
313
|
catalog.bytes = liftContextWindows(catalog.bytes);
|
|
314
|
+
catalog.bytes = stampMultiAgentVersion(catalog.bytes, !v2Off);
|
|
291
315
|
const stateDirExisted = existsSync(config.state_dir);
|
|
292
316
|
if (!state &&
|
|
293
317
|
STATE_FILES.some((name) => existsSync(join(config.state_dir, name)))) {
|
|
@@ -309,11 +333,14 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
309
333
|
let injected;
|
|
310
334
|
try {
|
|
311
335
|
injected = buildCodexConfig(original, proxy.port, catalogPath);
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
let injectedText = new TextDecoder("utf-8").decode(injected);
|
|
337
|
+
const eol = dominantEol(injectedText);
|
|
314
338
|
if (!v2Off && hasAgentsMaxThreads(injectedText)) {
|
|
315
|
-
|
|
339
|
+
const stripped = stripAgentsMaxThreads(injectedText);
|
|
340
|
+
injectedText = eol === "\n" ? stripped : stripped.replace(/\n/g, "\r\n");
|
|
341
|
+
console.warn("stripped [agents] max_threads (codex refuses it with multi_agent_v2)");
|
|
316
342
|
}
|
|
343
|
+
injected = Buffer.from(withMultiAgentV2(injectedText, !v2Off, eol));
|
|
317
344
|
}
|
|
318
345
|
catch (error) {
|
|
319
346
|
if (startedProxy)
|
package/dist/targets/pi.js
CHANGED
|
@@ -1,51 +1,135 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { join } from "node:path";
|
|
5
4
|
import { dropAgent, recordAgent } from "../config.js";
|
|
6
5
|
import { stopProxyProcess } from "../proxy.js";
|
|
7
|
-
import { CPACError, atomicWrite,
|
|
8
|
-
export
|
|
9
|
-
|
|
6
|
+
import { CPACError, atomicWrite, expandUserPath, objectValue, } from "../util.js";
|
|
7
|
+
export const PI_PACKAGE = "npm:@yhong91/cpac";
|
|
8
|
+
const PI_PACKAGE_NAME = "@yhong91/cpac";
|
|
9
|
+
const LEGACY_EXTENSIONS = ["pi-cpac.ts", "cpac.ts", "pi-cpa.ts"];
|
|
10
|
+
export function piAgentDir() {
|
|
11
|
+
return expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"));
|
|
10
12
|
}
|
|
11
|
-
export function
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export function piSettingsPath() {
|
|
14
|
+
return join(piAgentDir(), "settings.json");
|
|
15
|
+
}
|
|
16
|
+
function leftoverExtensions() {
|
|
17
|
+
const dir = join(piAgentDir(), "extensions");
|
|
18
|
+
return LEGACY_EXTENSIONS.map((name) => join(dir, name)).filter(existsSync);
|
|
19
|
+
}
|
|
20
|
+
function specName(source) {
|
|
21
|
+
const rest = source.startsWith("npm:") ? source.slice(4) : source;
|
|
22
|
+
if (rest.startsWith("@")) {
|
|
23
|
+
const cut = rest.lastIndexOf("@");
|
|
24
|
+
return cut > 0 ? rest.slice(0, cut) : rest;
|
|
25
|
+
}
|
|
26
|
+
const cut = rest.lastIndexOf("@");
|
|
27
|
+
return cut === -1 ? rest : rest.slice(0, cut);
|
|
28
|
+
}
|
|
29
|
+
function packageSource(entry) {
|
|
30
|
+
if (typeof entry === "string")
|
|
31
|
+
return entry.replace(/^-/, "");
|
|
32
|
+
if (objectValue(entry) && typeof entry.source === "string")
|
|
33
|
+
return entry.source;
|
|
34
|
+
}
|
|
35
|
+
function isCpacPackage(entry) {
|
|
36
|
+
const source = packageSource(entry);
|
|
37
|
+
return source !== undefined && specName(source) === PI_PACKAGE_NAME;
|
|
38
|
+
}
|
|
39
|
+
function isEnabledCpacPackage(entry) {
|
|
40
|
+
if (!isCpacPackage(entry))
|
|
41
|
+
return false;
|
|
42
|
+
if (typeof entry === "string")
|
|
43
|
+
return !entry.startsWith("-");
|
|
44
|
+
return !objectValue(entry) || entry.enabled !== false;
|
|
45
|
+
}
|
|
46
|
+
function readSettings(path) {
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === "ENOENT")
|
|
53
|
+
return {};
|
|
54
|
+
throw new CPACError(`cannot parse Pi settings: ${path}`);
|
|
55
|
+
}
|
|
56
|
+
if (!objectValue(raw)) {
|
|
57
|
+
throw new CPACError(`Pi settings must be a JSON object: ${path}`);
|
|
58
|
+
}
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
function settingsPackages(settings, path) {
|
|
62
|
+
if (!Object.hasOwn(settings, "packages"))
|
|
63
|
+
return undefined;
|
|
64
|
+
if (!Array.isArray(settings.packages)) {
|
|
65
|
+
throw new CPACError(`Pi settings packages must be an array: ${path}`);
|
|
66
|
+
}
|
|
67
|
+
return settings.packages;
|
|
68
|
+
}
|
|
69
|
+
function writeSettings(path, value) {
|
|
70
|
+
atomicWrite(path, Buffer.from(`${JSON.stringify(value, null, 2)}\n`));
|
|
14
71
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
72
|
+
function removeLeftovers() {
|
|
73
|
+
const removed = [];
|
|
74
|
+
for (const file of leftoverExtensions()) {
|
|
75
|
+
unlinkSync(file);
|
|
76
|
+
removed.push(file);
|
|
77
|
+
}
|
|
78
|
+
return removed;
|
|
17
79
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
80
|
+
export function isPiExtensionInstalled() {
|
|
81
|
+
if (leftoverExtensions().length > 0)
|
|
82
|
+
return true;
|
|
83
|
+
const path = piSettingsPath();
|
|
84
|
+
if (!existsSync(path))
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
const packages = settingsPackages(readSettings(path), path);
|
|
88
|
+
return packages?.some(isEnabledCpacPackage) ?? false;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
20
93
|
}
|
|
21
94
|
export async function installPiExtension(config) {
|
|
22
|
-
const
|
|
23
|
-
mkdirSync(
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
95
|
+
const path = piSettingsPath();
|
|
96
|
+
mkdirSync(piAgentDir(), { recursive: true, mode: 0o700 });
|
|
97
|
+
const settings = existsSync(path) ? readSettings(path) : {};
|
|
98
|
+
const packages = settingsPackages(settings, path) ?? [];
|
|
99
|
+
if (!packages.some(isEnabledCpacPackage)) {
|
|
100
|
+
settings.packages = [
|
|
101
|
+
...packages.filter((entry) => !isCpacPackage(entry)),
|
|
102
|
+
PI_PACKAGE,
|
|
103
|
+
];
|
|
104
|
+
writeSettings(path, settings);
|
|
105
|
+
}
|
|
106
|
+
recordAgent(config.state_dir, "pi", path);
|
|
107
|
+
for (const file of removeLeftovers()) {
|
|
108
|
+
console.log(`Removed leftover Pi extension: ${file}`);
|
|
36
109
|
}
|
|
37
|
-
console.log(`Installed Pi
|
|
110
|
+
console.log(`Installed Pi package ${PI_PACKAGE} in ${path}`);
|
|
38
111
|
}
|
|
39
112
|
export async function uninstallPiExtension(config) {
|
|
40
|
-
const
|
|
41
|
-
const target = join(dir, "pi-cpac.ts");
|
|
42
|
-
const legacyTargets = [join(dir, "cpac.ts"), join(dir, "pi-cpa.ts")];
|
|
113
|
+
const path = piSettingsPath();
|
|
43
114
|
let removed = false;
|
|
44
|
-
for (const file of
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
115
|
+
for (const file of removeLeftovers()) {
|
|
116
|
+
console.log(`Removed leftover Pi extension: ${file}`);
|
|
117
|
+
removed = true;
|
|
118
|
+
}
|
|
119
|
+
if (existsSync(path)) {
|
|
120
|
+
try {
|
|
121
|
+
const settings = readSettings(path);
|
|
122
|
+
const packages = settingsPackages(settings, path);
|
|
123
|
+
if (packages?.some(isCpacPackage)) {
|
|
124
|
+
settings.packages = packages.filter((entry) => !isCpacPackage(entry));
|
|
125
|
+
writeSettings(path, settings);
|
|
126
|
+
console.log(`Removed Pi package ${PI_PACKAGE} from ${path}`);
|
|
127
|
+
removed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!removed)
|
|
132
|
+
throw error;
|
|
49
133
|
}
|
|
50
134
|
}
|
|
51
135
|
if (!removed)
|
package/dist/util.js
CHANGED
|
@@ -114,6 +114,179 @@ export async function checkboxPicker(title, items, max) {
|
|
|
114
114
|
render();
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
+
export function wizardFrame(tabs, index) {
|
|
118
|
+
const confirming = index >= tabs.length;
|
|
119
|
+
const bar = tabs
|
|
120
|
+
.map((tab, i) => (i === index && !confirming ? `[${tab.label}]` : tab.label))
|
|
121
|
+
.concat(confirming ? ["[Confirm]"] : [])
|
|
122
|
+
.join(" ");
|
|
123
|
+
if (confirming) {
|
|
124
|
+
const width = Math.max(...tabs.map((tab) => tab.label.length));
|
|
125
|
+
return [
|
|
126
|
+
bar,
|
|
127
|
+
"",
|
|
128
|
+
...tabs.map((tab) => {
|
|
129
|
+
const values = [...tab.selected]
|
|
130
|
+
.sort((a, b) => a - b)
|
|
131
|
+
.map((i) => tab.items[i])
|
|
132
|
+
.join(", ");
|
|
133
|
+
return ` ${tab.label.padEnd(width)} ${values || "(none)"}`;
|
|
134
|
+
}),
|
|
135
|
+
"",
|
|
136
|
+
"enter: apply ←: back q: cancel",
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
const tab = tabs[index];
|
|
140
|
+
const hint = tab.max === 1
|
|
141
|
+
? "space/enter: select ←/→: tab q: cancel"
|
|
142
|
+
: `space: toggle enter: next ←/→: tab q: cancel max ${tab.max}`;
|
|
143
|
+
return [
|
|
144
|
+
bar,
|
|
145
|
+
hint,
|
|
146
|
+
...tab.items.map((item, i) => {
|
|
147
|
+
const box = tab.selected.has(i) ? "[x]" : "[ ]";
|
|
148
|
+
return `${i === tab.cursor ? ">" : " "} ${box} ${item}`;
|
|
149
|
+
}),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
function initWizardTab(tab) {
|
|
153
|
+
if (tab.items.length === 0)
|
|
154
|
+
throw new CPACError(`${tab.label} has no options`);
|
|
155
|
+
const selected = new Set();
|
|
156
|
+
for (const label of tab.selected ?? []) {
|
|
157
|
+
const found = tab.items.indexOf(label);
|
|
158
|
+
if (found < 0)
|
|
159
|
+
continue;
|
|
160
|
+
selected.add(found);
|
|
161
|
+
if (selected.size >= tab.max)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (tab.max === 1 && selected.size === 0)
|
|
165
|
+
selected.add(0);
|
|
166
|
+
return {
|
|
167
|
+
id: tab.id,
|
|
168
|
+
label: tab.label,
|
|
169
|
+
items: tab.items,
|
|
170
|
+
max: tab.max,
|
|
171
|
+
selected,
|
|
172
|
+
cursor: selected.size > 0 ? Math.min(...selected) : 0,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export async function tabWizard(tabs) {
|
|
176
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
177
|
+
throw new CPACError("model selection requires an interactive terminal");
|
|
178
|
+
}
|
|
179
|
+
if (tabs.length === 0)
|
|
180
|
+
throw new CPACError("setup has no options");
|
|
181
|
+
const state = tabs.map(initWizardTab);
|
|
182
|
+
let index = 0;
|
|
183
|
+
const stdin = process.stdin;
|
|
184
|
+
const out = process.stderr;
|
|
185
|
+
let painted = 0;
|
|
186
|
+
const paint = (lines) => {
|
|
187
|
+
if (painted > 0) {
|
|
188
|
+
out.write(`\x1b[${painted}F`);
|
|
189
|
+
for (let line = 0; line < painted; line += 1)
|
|
190
|
+
out.write("\x1b[2K\x1b[1E");
|
|
191
|
+
out.write(`\x1b[${painted}F`);
|
|
192
|
+
}
|
|
193
|
+
out.write(`${lines.join("\n")}\n`);
|
|
194
|
+
painted = lines.length;
|
|
195
|
+
};
|
|
196
|
+
const clear = () => {
|
|
197
|
+
if (painted > 0) {
|
|
198
|
+
out.write(`\x1b[${painted}F`);
|
|
199
|
+
for (let line = 0; line < painted; line += 1)
|
|
200
|
+
out.write("\x1b[2K\x1b[1E");
|
|
201
|
+
out.write(`\x1b[${painted}F`);
|
|
202
|
+
painted = 0;
|
|
203
|
+
}
|
|
204
|
+
out.write("\x1b[?25h");
|
|
205
|
+
};
|
|
206
|
+
const resultOf = () => Object.fromEntries(state.map((tab) => [
|
|
207
|
+
tab.id,
|
|
208
|
+
[...tab.selected]
|
|
209
|
+
.sort((a, b) => a - b)
|
|
210
|
+
.map((i) => tab.items[i]),
|
|
211
|
+
]));
|
|
212
|
+
const goNext = () => {
|
|
213
|
+
if (index < state.length && state[index].selected.size === 0)
|
|
214
|
+
return;
|
|
215
|
+
if (index < state.length)
|
|
216
|
+
index += 1;
|
|
217
|
+
};
|
|
218
|
+
const goPrev = () => {
|
|
219
|
+
if (index > 0)
|
|
220
|
+
index -= 1;
|
|
221
|
+
};
|
|
222
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
223
|
+
const finish = (error) => {
|
|
224
|
+
stdin.removeListener("data", onData);
|
|
225
|
+
try {
|
|
226
|
+
stdin.setRawMode(false);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Terminal already gone; nothing to restore.
|
|
230
|
+
}
|
|
231
|
+
stdin.pause();
|
|
232
|
+
clear();
|
|
233
|
+
if (error)
|
|
234
|
+
rejectPromise(error);
|
|
235
|
+
else
|
|
236
|
+
resolvePromise(resultOf());
|
|
237
|
+
};
|
|
238
|
+
const onData = (chunk) => {
|
|
239
|
+
const key = chunk.toString("utf8");
|
|
240
|
+
if (key === "\x03" || key === "q" || key === "\x1b") {
|
|
241
|
+
finish(new CPACError("model selection cancelled"));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (index >= state.length) {
|
|
245
|
+
if (key === "\r" || key === "\n") {
|
|
246
|
+
finish();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (key === "\x1b[D")
|
|
250
|
+
goPrev();
|
|
251
|
+
paint(wizardFrame(state, index));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const tab = state[index];
|
|
255
|
+
if (key === "\x1b[A")
|
|
256
|
+
tab.cursor = (tab.cursor + tab.items.length - 1) % tab.items.length;
|
|
257
|
+
else if (key === "\x1b[B")
|
|
258
|
+
tab.cursor = (tab.cursor + 1) % tab.items.length;
|
|
259
|
+
else if (key === "\x1b[C")
|
|
260
|
+
goNext();
|
|
261
|
+
else if (key === "\x1b[D")
|
|
262
|
+
goPrev();
|
|
263
|
+
else if (key === " ") {
|
|
264
|
+
if (tab.max === 1) {
|
|
265
|
+
tab.selected.clear();
|
|
266
|
+
tab.selected.add(tab.cursor);
|
|
267
|
+
goNext();
|
|
268
|
+
}
|
|
269
|
+
else if (tab.selected.has(tab.cursor))
|
|
270
|
+
tab.selected.delete(tab.cursor);
|
|
271
|
+
else if (tab.selected.size < tab.max)
|
|
272
|
+
tab.selected.add(tab.cursor);
|
|
273
|
+
}
|
|
274
|
+
else if (key === "\r" || key === "\n") {
|
|
275
|
+
if (tab.max === 1) {
|
|
276
|
+
tab.selected.clear();
|
|
277
|
+
tab.selected.add(tab.cursor);
|
|
278
|
+
}
|
|
279
|
+
goNext();
|
|
280
|
+
}
|
|
281
|
+
paint(wizardFrame(state, index));
|
|
282
|
+
};
|
|
283
|
+
stdin.setRawMode(true);
|
|
284
|
+
stdin.resume();
|
|
285
|
+
stdin.on("data", onData);
|
|
286
|
+
out.write("\x1b[?25l");
|
|
287
|
+
paint(wizardFrame(state, index));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
117
290
|
export function tomlString(value) {
|
|
118
291
|
return JSON.stringify(value);
|
|
119
292
|
}
|
|
@@ -1,29 +1,67 @@
|
|
|
1
1
|
// Pi CPAC extension: register CPA models as Pi providers
|
|
2
|
-
//
|
|
2
|
+
// Loaded via package.json "pi.extensions" when settings include npm:@yhong91/cpac
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
// keep in sync with src/config.ts DEFAULT_CPA_URL
|
|
5
|
+
const CPA = (process.env.CPA_BASE_URL || "http://124.223.178.52:8317").replace(
|
|
6
|
+
/\/+$/,
|
|
7
|
+
"",
|
|
8
|
+
);
|
|
9
|
+
const BUILTIN = new Set([
|
|
10
|
+
"openai",
|
|
11
|
+
"github-copilot",
|
|
12
|
+
"xai",
|
|
13
|
+
"deepseek",
|
|
14
|
+
"anthropic",
|
|
15
|
+
"google",
|
|
16
|
+
]);
|
|
6
17
|
const VENDOR_PREFIXES = [
|
|
7
|
-
["gpt-", "openai"],
|
|
18
|
+
["gpt-", "openai"],
|
|
19
|
+
["o1-", "openai"],
|
|
20
|
+
["o3-", "openai"],
|
|
21
|
+
["o4-", "openai"],
|
|
8
22
|
["gpt-image", "openai"],
|
|
9
|
-
["claude-", "anthropic"],
|
|
10
|
-
["
|
|
11
|
-
["
|
|
12
|
-
["
|
|
23
|
+
["claude-", "anthropic"],
|
|
24
|
+
["gemini-", "google"],
|
|
25
|
+
["grok-", "xai"],
|
|
26
|
+
["deepseek-", "deepseek"],
|
|
27
|
+
["glm-", "zhipu"],
|
|
28
|
+
["kimi-", "moonshot"],
|
|
29
|
+
["mimo-", "xiaomi"],
|
|
30
|
+
["doubao-", "volcengine"],
|
|
31
|
+
["ark-", "volcengine"],
|
|
32
|
+
["minimax-", "minimax"],
|
|
33
|
+
["step-", "stepfun"],
|
|
34
|
+
["qwen-", "alibaba"],
|
|
13
35
|
["hunyuan-", "tencent"],
|
|
14
36
|
];
|
|
15
37
|
const MAX_TOKENS = {
|
|
16
|
-
"gpt-5.6-sol": 128000,
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
38
|
+
"gpt-5.6-sol": 128000,
|
|
39
|
+
"gpt-5.6-terra": 128000,
|
|
40
|
+
"gpt-5.6-luna": 128000,
|
|
41
|
+
"claude-opus-4-6-thinking": 128000,
|
|
42
|
+
"claude-sonnet-4-6": 64000,
|
|
43
|
+
"gemini-3.6-flash": 65536,
|
|
44
|
+
"gemini-3.6-flash-high": 65536,
|
|
45
|
+
"deepseek-v4-pro": 128000,
|
|
46
|
+
"deepseek-v4-flash": 128000,
|
|
47
|
+
"glm-5.3": 128000,
|
|
48
|
+
"kimi-k2.7-code": 128000,
|
|
49
|
+
"minimax-m3": 128000,
|
|
50
|
+
"mimo-v2.5": 131072,
|
|
51
|
+
"mimo-v2.5-pro": 131072,
|
|
52
|
+
"grok-4.5": 128000,
|
|
53
|
+
"doubao-seed-2.0-lite": 128000,
|
|
54
|
+
"doubao-seed-2.1-turbo": 128000,
|
|
23
55
|
};
|
|
24
56
|
const DEFAULT_MAX_TOKENS = 65536;
|
|
25
57
|
const REASONING_EFFORTS = new Set([
|
|
26
|
-
"minimal",
|
|
58
|
+
"minimal",
|
|
59
|
+
"low",
|
|
60
|
+
"medium",
|
|
61
|
+
"high",
|
|
62
|
+
"xhigh",
|
|
63
|
+
"max",
|
|
64
|
+
"ultra",
|
|
27
65
|
]);
|
|
28
66
|
|
|
29
67
|
function vendorFor(slug) {
|
|
@@ -40,21 +78,27 @@ function groupName(vendor) {
|
|
|
40
78
|
function effortsFor(row) {
|
|
41
79
|
const levels = row.supported_reasoning_levels;
|
|
42
80
|
if (!Array.isArray(levels)) return undefined;
|
|
43
|
-
const efforts = levels.flatMap(
|
|
81
|
+
const efforts = levels.flatMap((level) => {
|
|
44
82
|
if (!level || typeof level !== "object") return [];
|
|
45
83
|
const effort = level.effort;
|
|
46
|
-
return typeof effort === "string" && REASONING_EFFORTS.has(effort)
|
|
84
|
+
return typeof effort === "string" && REASONING_EFFORTS.has(effort)
|
|
85
|
+
? [effort]
|
|
86
|
+
: [];
|
|
47
87
|
});
|
|
48
88
|
return efforts.length > 0 ? efforts : undefined;
|
|
49
89
|
}
|
|
50
90
|
|
|
51
91
|
function thinkingLevelMapFor(efforts) {
|
|
52
|
-
if (!efforts || !efforts.some(
|
|
92
|
+
if (!efforts || !efforts.some((effort) => REASONING_EFFORTS.has(effort))) {
|
|
53
93
|
return undefined;
|
|
54
94
|
}
|
|
55
95
|
const available = new Set(efforts);
|
|
56
96
|
return {
|
|
57
|
-
minimal: available.has("minimal")
|
|
97
|
+
minimal: available.has("minimal")
|
|
98
|
+
? "minimal"
|
|
99
|
+
: available.has("low")
|
|
100
|
+
? "low"
|
|
101
|
+
: null,
|
|
58
102
|
low: available.has("low") ? "low" : null,
|
|
59
103
|
medium: available.has("medium") ? "medium" : null,
|
|
60
104
|
high: available.has("high") ? "high" : null,
|
|
@@ -65,7 +109,8 @@ function thinkingLevelMapFor(efforts) {
|
|
|
65
109
|
|
|
66
110
|
function intField(value) {
|
|
67
111
|
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
68
|
-
? Math.floor(value)
|
|
112
|
+
? Math.floor(value)
|
|
113
|
+
: undefined;
|
|
69
114
|
}
|
|
70
115
|
|
|
71
116
|
export default async function (pi) {
|
|
@@ -86,12 +131,18 @@ export default async function (pi) {
|
|
|
86
131
|
}
|
|
87
132
|
payload = await res.json();
|
|
88
133
|
} catch (error) {
|
|
89
|
-
console.error(
|
|
134
|
+
console.error(
|
|
135
|
+
"[pi-cpac] CPA models request failed: " +
|
|
136
|
+
((error && error.message) || error),
|
|
137
|
+
);
|
|
90
138
|
return;
|
|
91
139
|
}
|
|
92
|
-
const source =
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
const source =
|
|
141
|
+
payload && Array.isArray(payload.models)
|
|
142
|
+
? payload.models
|
|
143
|
+
: payload && Array.isArray(payload.data)
|
|
144
|
+
? payload.data
|
|
145
|
+
: null;
|
|
95
146
|
if (!source) {
|
|
96
147
|
console.error("[pi-cpac] CPA models response has no models list");
|
|
97
148
|
return;
|
|
@@ -99,15 +150,19 @@ export default async function (pi) {
|
|
|
99
150
|
const rows = [];
|
|
100
151
|
for (const m of source) {
|
|
101
152
|
if (!m || typeof m !== "object") continue;
|
|
102
|
-
const slug =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
const slug =
|
|
154
|
+
typeof m.slug === "string" && m.slug.trim()
|
|
155
|
+
? m.slug.trim()
|
|
156
|
+
: typeof m.id === "string" && m.id.trim()
|
|
157
|
+
? m.id.trim()
|
|
158
|
+
: null;
|
|
159
|
+
if (slug)
|
|
160
|
+
rows.push({
|
|
161
|
+
slug: slug,
|
|
162
|
+
display_name: m.display_name,
|
|
163
|
+
supported_reasoning_levels: m.supported_reasoning_levels,
|
|
164
|
+
context_window: m.context_window,
|
|
165
|
+
});
|
|
111
166
|
}
|
|
112
167
|
if (rows.length === 0) {
|
|
113
168
|
console.error("[pi-cpac] CPA models response has no usable models");
|
|
@@ -126,7 +181,7 @@ export default async function (pi) {
|
|
|
126
181
|
baseUrl: CPA + "/v1",
|
|
127
182
|
apiKey: apiKey,
|
|
128
183
|
api: "openai-responses",
|
|
129
|
-
models: models.map(
|
|
184
|
+
models: models.map((m) => {
|
|
130
185
|
const efforts = effortsFor(m);
|
|
131
186
|
const thinkingLevelMap = thinkingLevelMapFor(efforts);
|
|
132
187
|
const modelObj = {
|
|
@@ -139,7 +194,7 @@ export default async function (pi) {
|
|
|
139
194
|
maxTokens: MAX_TOKENS[m.slug] || DEFAULT_MAX_TOKENS,
|
|
140
195
|
compat: {
|
|
141
196
|
sendSessionAffinityHeaders: true,
|
|
142
|
-
sessionAffinityFormat: "openai",
|
|
197
|
+
sessionAffinityFormat: "openai-nosession",
|
|
143
198
|
},
|
|
144
199
|
};
|
|
145
200
|
if (thinkingLevelMap) modelObj.thinkingLevelMap = thinkingLevelMap;
|
package/package.json
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cpac": "dist/cpac.js"
|
|
8
8
|
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pi-package"
|
|
11
|
+
],
|
|
12
|
+
"pi": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"./extensions/pi-cpac.ts"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
9
17
|
"files": [
|
|
10
18
|
"dist",
|
|
19
|
+
"extensions",
|
|
11
20
|
"cpac.example.json",
|
|
12
21
|
"README.md"
|
|
13
22
|
],
|
|
@@ -28,9 +37,9 @@
|
|
|
28
37
|
},
|
|
29
38
|
"scripts": {
|
|
30
39
|
"clean": "node -e \"for (const d of ['dist','dist-test']) require('fs').rmSync(d,{recursive:true,force:true})\"",
|
|
31
|
-
"build": "npm run clean --if-present && tsc -p tsconfig.build.json &&
|
|
40
|
+
"build": "npm run clean --if-present && tsc -p tsconfig.build.json && node -e \"const fs=require('fs');const p='dist/cpac.js';const s=fs.readFileSync(p,'utf8');if(!s.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+s);try{fs.chmodSync(p,0o755)}catch{}\"",
|
|
32
41
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
33
|
-
"test": "npm run clean && tsc -p tsconfig.test.json &&
|
|
42
|
+
"test": "npm run clean && tsc -p tsconfig.test.json && node --test --test-reporter=spec dist-test/cpac.test.js",
|
|
34
43
|
"pack:check": "npm pack --dry-run"
|
|
35
44
|
},
|
|
36
45
|
"devDependencies": {
|