@yhong91/cpac 0.1.32 → 0.1.34
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 +37 -46
- package/cpac.example.json +3 -1
- package/dist/agents.js +67 -26
- package/dist/config.js +93 -11
- package/dist/cpac.js +120 -69
- 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
|
@@ -38,18 +38,18 @@ cpac status
|
|
|
38
38
|
|
|
39
39
|
# 4a. 持久注入(长期生效,写入 agent 配置,可完整恢复)
|
|
40
40
|
cpac install --all # 注入所有检测到的目标(codex/pi/kimi)
|
|
41
|
-
cpac install
|
|
42
|
-
cpac install
|
|
43
|
-
cpac install
|
|
44
|
-
cpac restore
|
|
41
|
+
cpac install codex # 只注入 Codex(默认开启 multi-agent v2)
|
|
42
|
+
cpac install codex --v2_models # 先多选 spawn_agent 模型再注入
|
|
43
|
+
cpac install codex --max_context # 注入时把模型上下文提升到实测上限(opt-in,仅限 codex)
|
|
44
|
+
cpac restore codex # 撤掉注入;注入期间的用户改动会留下
|
|
45
45
|
|
|
46
46
|
# 4b. 临时接管(退出无痕,不写任何配置文件)
|
|
47
47
|
cpac claude # Claude Code 走 CPA
|
|
48
48
|
cpac opencode # opencode 走 CPA(模型选 cpac/<模型名>)
|
|
49
49
|
|
|
50
50
|
# 5. 可选:Agent 配置向导与模型选择
|
|
51
|
-
cpac codex
|
|
52
|
-
cpac claude
|
|
51
|
+
cpac setup codex # Codex 配置向导 → 注入 → 启动
|
|
52
|
+
cpac setup claude # Claude 模型向导 → 启动
|
|
53
53
|
|
|
54
54
|
# 6. 升级(装上新版本后自动 sync 已接入的 agent)
|
|
55
55
|
cpac upgrade
|
|
@@ -80,7 +80,7 @@ cpac status # 验证:CPA_API_KEY 应显示 configured
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Claude 槽位(opus/sonnet/haiku)通常不用配:cpac 启动时自动选择——同家族 claude 模型优先,否则 gemini → grok → luna → 目录第一个;opus 无匹配则不设。这个链条在常见目录上正好给出理想结果:sonnet 落在 claude-sonnet-*(它同时充当 Auto mode 分类器的替身,避免分类请求失败后回退到昂贵的会话主模型),haiku 落在快的 gemini flash。只有目录奇怪时才用 `cpac claude
|
|
83
|
+
Claude 槽位(opus/sonnet/haiku)通常不用配:cpac 启动时自动选择——同家族 claude 模型优先,否则 gemini → grok → luna → 目录第一个;opus 无匹配则不设。这个链条在常见目录上正好给出理想结果:sonnet 落在 claude-sonnet-*(它同时充当 Auto mode 分类器的替身,避免分类请求失败后回退到昂贵的会话主模型),haiku 落在快的 gemini flash。只有目录奇怪时才用 `cpac setup claude` 交互式配置。codex spawn subagent 取三个 GPT + gemini + grok 五席(顺序即优先级,Codex 只广告 catalog 前 5),可通过 `cpac setup codex` 交互式配置。候选替换:`mimo-v2.5`(haiku)、`grok-4.5`(haiku)。若上述 slug 不在目录中(`curl -s -H "Authorization: Bearer $CPA_API_KEY" "$CPA_BASE_URL/v1/models"` 可查),按家族就近替换。分类器机制细节见 [docs/claude-auto-mode-classifier.md](docs/claude-auto-mode-classifier.md)。
|
|
84
84
|
|
|
85
85
|
1. **持久注入**(只对检测到的目标;codex 会应用步骤 3 的 spawn_models):
|
|
86
86
|
|
|
@@ -91,12 +91,12 @@ cpac status # 验证:各 target 显示 ready/injected
|
|
|
91
91
|
|
|
92
92
|
1. **统一启动与配置**:
|
|
93
93
|
- 启动:`cpac <codex|claude|kimi|grok|pi|opencode> [args...]`(所有原生参数如 `-c` 等原样透明传递)
|
|
94
|
-
- 配置并启动:`cpac <codex|claude
|
|
94
|
+
- 配置并启动:`cpac setup <codex|claude>`(向导结束后注入并拉起)
|
|
95
95
|
- 还原:`cpac <agent> clear`(还原或重置配置)
|
|
96
96
|
|
|
97
97
|
2. **已安装后升级**:npm 发布新版本后,等 publish 成功再执行 `cpac upgrade`。它会按需 `npm install -g`,再对已接入的 Codex / Pi / Kimi / Grok 做 `sync`。不要拆成 `npm install -g @yhong91/cpac` 再手动 `cpac sync`。只刷新配置、不升 CLI 时才用 `cpac sync`。
|
|
98
98
|
|
|
99
|
-
失败处理:任何一步报错时先跑 `cpac status`;密钥未配置回到步骤 2;注入后异常用 `cpac <agent> clear` 或 `cpac restore
|
|
99
|
+
失败处理:任何一步报错时先跑 `cpac status`;密钥未配置回到步骤 2;注入后异常用 `cpac <agent> clear` 或 `cpac restore <id>` 恢复原配置。
|
|
100
100
|
|
|
101
101
|
## 首次引导
|
|
102
102
|
|
|
@@ -110,9 +110,9 @@ CPA_API_KEY: configured / not configured
|
|
|
110
110
|
|
|
111
111
|
Commands:
|
|
112
112
|
cpac claude [args...] Launch Claude Code through CPA
|
|
113
|
-
cpac
|
|
113
|
+
cpac setup <codex|claude> Configure then launch
|
|
114
114
|
cpac status Show Codex injection status
|
|
115
|
-
cpac restore
|
|
115
|
+
cpac restore <id> | --all Detach injected agents
|
|
116
116
|
cpac --help Show command usage
|
|
117
117
|
```
|
|
118
118
|
|
|
@@ -164,21 +164,21 @@ cpac --help
|
|
|
164
164
|
|
|
165
165
|
```bash
|
|
166
166
|
cpac detect [--json] [--home PATH]
|
|
167
|
-
cpac install [
|
|
168
|
-
cpac sync [
|
|
169
|
-
cpac restore
|
|
167
|
+
cpac install [codex|pi|kimi|grok...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context] [--home PATH]
|
|
168
|
+
cpac sync [codex|pi|kimi|grok...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context] [--home PATH]
|
|
169
|
+
cpac restore <id...> | --all [--dry-run] [--home PATH]
|
|
170
170
|
cpac uninstall [--dry-run]
|
|
171
171
|
cpac upgrade [--check]
|
|
172
172
|
cpac -v | --version
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
176
|
-
- `install`
|
|
177
|
-
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi
|
|
178
|
-
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`);`install
|
|
176
|
+
- `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
177
|
+
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi 确认 `npm:@yhong91/cpac` 并清理旧的 `extensions/pi-cpac.ts`、Kimi 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `cpac install codex` 有效。
|
|
178
|
+
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`,catalog 行打 `multi_agent_version: "v2"`);`install 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
|
-
- `restore` 必须带
|
|
181
|
-
- `uninstall` 先用和 `cpac codex
|
|
180
|
+
- `restore` 必须带 agent 名或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
|
|
181
|
+
- `uninstall` 先用和 `cpac setup codex` 相同的方向键选择器确认(默认 Cancel),再 `restore --all`,最后 `npm uninstall -g @yhong91/cpac`。不要跟 agent 名;卸载的是 cpac 包本身。非终端取消。`--dry-run` 跳过确认。
|
|
182
182
|
- `--home PATH` 临时把 `HOME` 指向给定目录,便于在隔离环境中试验。
|
|
183
183
|
- `upgrade` 是发布后更新本机的一次性入口:查 npm → 需要时 `npm install -g` → 对**已接入**的 Codex / Pi / Kimi 执行 `sync`。已是最新也会 sync。不要先手动 `npm install -g` 再 `cpac sync`。`--check` 只报告版本,不安装也不 sync。没有已安装目标时跳过 sync,不报错。只刷新配置、不升 CLI 时用 `cpac sync`。
|
|
184
184
|
|
|
@@ -196,7 +196,7 @@ cpac claude -- -p "检查当前项目"
|
|
|
196
196
|
#### 配置与清除
|
|
197
197
|
|
|
198
198
|
```bash
|
|
199
|
-
cpac claude
|
|
199
|
+
cpac setup claude # 交互式依次配置 Sonnet / Haiku / Opus,然后启动
|
|
200
200
|
cpac claude clear # 重置所有覆盖,恢复自动选择
|
|
201
201
|
```
|
|
202
202
|
|
|
@@ -225,18 +225,18 @@ cpac codex exec "帮我写一个测试"
|
|
|
225
225
|
#### 配置与清除
|
|
226
226
|
|
|
227
227
|
```bash
|
|
228
|
-
cpac codex
|
|
228
|
+
cpac setup codex # 交互式配置后注入并启动 Codex
|
|
229
229
|
cpac codex clear # 还原 Codex 原生配置并关闭代理
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
####
|
|
232
|
+
#### 注入(`cpac install codex`)
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
cpac
|
|
236
|
-
cpac restore
|
|
235
|
+
cpac install codex [--v2_off] [--v2_models] [--max_context] [--config PATH]
|
|
236
|
+
cpac restore codex
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
`
|
|
239
|
+
`install codex` 从远端 `/v1/models?client_version=1` 获取 Codex rich catalog,并启动只监听 `127.0.0.1` 的轻量转发代理。Codex 配置保留内置 `openai` provider,只写入根级 `model_catalog_json` 和:
|
|
240
240
|
|
|
241
241
|
```toml
|
|
242
242
|
openai_base_url = "http://127.0.0.1:10101/v1"
|
|
@@ -244,21 +244,13 @@ openai_base_url = "http://127.0.0.1:10101/v1"
|
|
|
244
244
|
|
|
245
245
|
Codex App/CLI 发给本地代理的 ChatGPT bearer 不会转发到 CPA;代理会改用 `CPA_API_KEY`,并将请求和流式响应原样转发到远端 CPA。这保留了 Codex App 的原生 `openai` provider 身份、历史和账号相关界面。已有用户自定义根级 `openai_base_url` 或非 `openai` 的活动 `model_provider` 时,CPAC 会拒绝覆盖。
|
|
246
246
|
|
|
247
|
-
Codex App 的长驻 `app-server`
|
|
247
|
+
Codex App 的长驻 `app-server` 可能缓存旧目录。注入会删除 `models_cache.json`;如果选择器仍未更新,请重启 Codex App。
|
|
248
248
|
|
|
249
|
-
注入命令可选 `--max_context`(`cpac
|
|
249
|
+
注入命令可选 `--max_context`(`cpac install 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
|
|
251
|
+
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。Pi 只改 `settings.json` 的 `packages`,不备份。`restore codex`:若 `config.toml` 相对注入未改,按原 mode 逐字节还原备份;若用户在注入期间改过文件,只撤掉 CPAC 写入的 catalog、url 和标记。没有其它需要代理的 agent 时才关闭代理。
|
|
252
252
|
|
|
253
|
-
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac
|
|
254
|
-
|
|
255
|
-
需要由现有进程管理器监督时,可使用前台入口:
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
cpac proxy
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
它复用 `state.json` 里记录的端口和实例身份;没有 loopback 记录时会拒绝启动。
|
|
253
|
+
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac kimi` / `cpac grok` 即可恢复。
|
|
262
254
|
|
|
263
255
|
默认路径:
|
|
264
256
|
|
|
@@ -267,7 +259,6 @@ Codex 配置:$CODEX_HOME/config.toml,未设置 CODEX_HOME 时为 ~/.codex/co
|
|
|
267
259
|
CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录)
|
|
268
260
|
各 agent 配置备份:~/.cpac/<agent>/<原文件名>
|
|
269
261
|
Codex ~/.cpac/codex/config.toml
|
|
270
|
-
Pi ~/.cpac/pi/pi-cpac.ts
|
|
271
262
|
Kimi ~/.cpac/kimi/config.toml
|
|
272
263
|
Grok ~/.cpac/grok/config.toml
|
|
273
264
|
restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
|
|
@@ -291,13 +282,13 @@ cpac opencode [--] [opencode args...]
|
|
|
291
282
|
cpac pi install
|
|
292
283
|
```
|
|
293
284
|
|
|
294
|
-
|
|
285
|
+
在 `~/.pi/agent/settings.json` 的 `packages` 中加入 `npm:@yhong91/cpac`。Pi 启动时按 npm 包加载 `extensions/pi-cpac.ts`,从 CPA 动态注册模型。旧版写入的 `~/.pi/agent/extensions/pi-cpac.ts` 会在 install / restore 时删掉。卸载:
|
|
295
286
|
|
|
296
287
|
```bash
|
|
297
|
-
cpac restore
|
|
288
|
+
cpac restore pi
|
|
298
289
|
```
|
|
299
290
|
|
|
300
|
-
|
|
291
|
+
尊重 `PI_CODING_AGENT_DIR`。扩展代码随 npm 包更新,已装过的副本用 `pi update npm:@yhong91/cpac`(或 `pi update --extensions`)。
|
|
301
292
|
|
|
302
293
|
### Kimi Code
|
|
303
294
|
|
|
@@ -307,10 +298,10 @@ Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
|
|
|
307
298
|
cpac kimi install
|
|
308
299
|
```
|
|
309
300
|
|
|
310
|
-
它在 `~/.kimi-code/config.toml` 追加一个由 CPAC 管理的块,注册 `cpac` OpenAI 兼容 provider,并把 CPA 目录里的每个模型映射为一个 `cpac/<模型名>` 条目。带 `supported_reasoning_levels` 的模型会写入 `capabilities`、`support_efforts` 和 `default_effort`,否则 Kimi 只给 Claude 名套内置 thinking profile,其它模型会停在 thinking off 且无法改 effort。`base_url` 指向共享 loopback 代理,因此密钥不落盘。install 会写入 `state.json`
|
|
301
|
+
它在 `~/.kimi-code/config.toml` 追加一个由 CPAC 管理的块,注册 `cpac` OpenAI 兼容 provider,并把 CPA 目录里的每个模型映射为一个 `cpac/<模型名>` 条目。带 `supported_reasoning_levels` 的模型会写入 `capabilities`、`support_efforts` 和 `default_effort`,否则 Kimi 只给 Claude 名套内置 thinking profile,其它模型会停在 thinking off 且无法改 effort。`base_url` 指向共享 loopback 代理,因此密钥不落盘。install 会写入 `state.json` 并在需要时启动代理,不必先单独注入 Codex。卸载与状态:
|
|
311
302
|
|
|
312
303
|
```bash
|
|
313
|
-
cpac restore
|
|
304
|
+
cpac restore kimi
|
|
314
305
|
```
|
|
315
306
|
|
|
316
307
|
Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若 `config.toml` 已存在,会在 `state_dir/kimi/config.toml` 保留一份原始快照(只存最早版本),万一文件损坏可手动复制回去。
|
|
@@ -321,7 +312,7 @@ Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若
|
|
|
321
312
|
| --- | --- | --- |
|
|
322
313
|
| `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
|
|
323
314
|
| `CPA_BASE_URL` | `http://124.223.178.52:8317` | 覆盖内置 CPA 地址 |
|
|
324
|
-
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `
|
|
315
|
+
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `settings.json` 写入位置) |
|
|
325
316
|
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac kimi install` 写入位置) |
|
|
326
317
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
327
318
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
@@ -332,7 +323,7 @@ CPAC CLI 可临时覆盖 CPA 地址:
|
|
|
332
323
|
CPA_BASE_URL='https://cpa.example.com' cpac claude
|
|
333
324
|
```
|
|
334
325
|
|
|
335
|
-
|
|
326
|
+
Pi 扩展只从进程环境读取 `CPA_BASE_URL`(默认内置地址)和 `CPA_API_KEY`;不读取 `config.json` 的 `cpa_url`,也不读用户 home 中的私有文件。
|
|
336
327
|
|
|
337
328
|
## 可选 JSON 配置
|
|
338
329
|
|
|
@@ -359,7 +350,7 @@ CPA_BASE_URL='https://cpa.example.com' cpac claude
|
|
|
359
350
|
使用自定义配置:
|
|
360
351
|
|
|
361
352
|
```bash
|
|
362
|
-
cpac
|
|
353
|
+
cpac install codex --config ./cpac.json
|
|
363
354
|
cpac claude --config ./cpac.json -- --model claude-sonnet-4-5
|
|
364
355
|
```
|
|
365
356
|
|
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);
|
|
@@ -272,12 +313,12 @@ export async function runInstall(config, requested, options) {
|
|
|
272
313
|
const eligible = TARGETS.filter((target) => target.detected()).map((target) => target.id);
|
|
273
314
|
const selected = selectTargets(requested, options.all, eligible);
|
|
274
315
|
if (selected.length === 0) {
|
|
275
|
-
throw new CPACError(`no supported targets detected; use
|
|
316
|
+
throw new CPACError(`no supported targets detected; use: cpac install ${TARGETS.map((target) => target.id).join(" ")} or --all`);
|
|
276
317
|
}
|
|
277
318
|
// Catalog context lifting is Codex-specific; refuse to silently skip it on
|
|
278
319
|
// other targets so the flag never gains cross-agent meaning.
|
|
279
320
|
if (options.maxContext && selected.some((target) => target.id !== "codex")) {
|
|
280
|
-
throw new CPACError("--max_context is only valid with
|
|
321
|
+
throw new CPACError("--max_context is only valid with codex");
|
|
281
322
|
}
|
|
282
323
|
for (const target of selected) {
|
|
283
324
|
if (target.installed(config) && !options.force) {
|
|
@@ -312,7 +353,7 @@ export async function runSync(config, requested, options) {
|
|
|
312
353
|
}
|
|
313
354
|
export async function runRestore(config, requested, options) {
|
|
314
355
|
if (requested.length === 0 && !options.all) {
|
|
315
|
-
throw new CPACError("restore requires
|
|
356
|
+
throw new CPACError("restore requires an agent name or --all");
|
|
316
357
|
}
|
|
317
358
|
const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
318
359
|
const selected = selectTargets(requested, options.all, eligible);
|
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");
|