@yhong91/cpac 0.1.26 → 0.1.28
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 +19 -19
- package/dist/agents.js +60 -396
- package/dist/config.js +40 -1
- package/dist/cpac.js +95 -33
- package/dist/proxy.js +2 -2
- package/dist/{claude.js → targets/claude.js} +3 -3
- package/dist/{codex.js → targets/codex.js} +64 -11
- package/dist/targets/grok.js +109 -0
- package/dist/targets/kimi.js +175 -0
- package/dist/targets/opencode.js +59 -0
- package/dist/targets/pi.js +32 -0
- package/dist/util.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CPAC 将 Codex 和 Claude Code 接入远端 CLIProxyAPI(CPA):
|
|
4
4
|
|
|
5
|
-
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider
|
|
5
|
+
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider。无用户改动时逐字节恢复原配置;注入期间改过文件则只撤掉 CPAC 字段。
|
|
6
6
|
- **Claude Code**:通过临时环境变量启动,不修改 Claude Code 配置。
|
|
7
7
|
|
|
8
8
|
实现基于 TypeScript + Node.js,运行时零依赖。
|
|
@@ -41,15 +41,15 @@ cpac install --all # 注入所有检测到的目标(codex/
|
|
|
41
41
|
cpac install --target codex # 只注入 Codex(默认开启 multi-agent v2)
|
|
42
42
|
cpac install --target codex --v2_models # 先多选 spawn_agent 模型再注入
|
|
43
43
|
cpac install --target codex --max_context # 注入时把模型上下文提升到实测上限(opt-in,仅限 codex)
|
|
44
|
-
cpac
|
|
44
|
+
cpac restore --target 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 setup
|
|
52
|
-
cpac claude setup
|
|
51
|
+
cpac codex --setup # Codex 配置向导 → 注入 → 启动
|
|
52
|
+
cpac claude --setup # 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 setup` 交互式配置。codex spawn subagent 取三个 GPT + gemini + grok 五席(顺序即优先级,Codex 只广告 catalog 前 5),可通过 `cpac codex setup` 交互式配置。候选替换:`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)。
|
|
83
|
+
Claude 槽位(opus/sonnet/haiku)通常不用配:cpac 启动时自动选择——同家族 claude 模型优先,否则 gemini → grok → luna → 目录第一个;opus 无匹配则不设。这个链条在常见目录上正好给出理想结果:sonnet 落在 claude-sonnet-*(它同时充当 Auto mode 分类器的替身,避免分类请求失败后回退到昂贵的会话主模型),haiku 落在快的 gemini flash。只有目录奇怪时才用 `cpac claude --setup` 交互式配置。codex spawn subagent 取三个 GPT + gemini + grok 五席(顺序即优先级,Codex 只广告 catalog 前 5),可通过 `cpac codex --setup` 交互式配置。候选替换:`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,7 +91,7 @@ cpac status # 验证:各 target 显示 ready/injected
|
|
|
91
91
|
|
|
92
92
|
1. **统一启动与配置**:
|
|
93
93
|
- 启动:`cpac <codex|claude|kimi|grok|pi|opencode> [args...]`(所有原生参数如 `-c` 等原样透明传递)
|
|
94
|
-
-
|
|
94
|
+
- 配置并启动:`cpac <codex|claude> --setup`(向导结束后注入并拉起)
|
|
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`。
|
|
@@ -112,7 +112,7 @@ Commands:
|
|
|
112
112
|
cpac claude [args...] Launch Claude Code through CPA
|
|
113
113
|
cpac inject Inject CPA into Codex
|
|
114
114
|
cpac status Show Codex injection status
|
|
115
|
-
cpac restore
|
|
115
|
+
cpac restore --target <id> | --all Detach injected agents
|
|
116
116
|
cpac --help Show command usage
|
|
117
117
|
```
|
|
118
118
|
|
|
@@ -158,7 +158,7 @@ cpac
|
|
|
158
158
|
cpac --help
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
### 统一管理(detect / install / sync / uninstall / upgrade)
|
|
161
|
+
### 统一管理(detect / install / sync / restore / uninstall / upgrade)
|
|
162
162
|
|
|
163
163
|
参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi` 三个持久注入目标统一管理(claude/opencode 是临时接管,用 `cpac claude` / `cpac opencode` 启动,不属于 install 目标):
|
|
164
164
|
|
|
@@ -166,9 +166,10 @@ cpac --help
|
|
|
166
166
|
cpac detect [--json] [--home PATH]
|
|
167
167
|
cpac install [--target codex,pi,kimi] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context] [--home PATH]
|
|
168
168
|
cpac sync [--target codex,pi,kimi] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context] [--home PATH]
|
|
169
|
-
cpac
|
|
169
|
+
cpac restore --target <id> | --all [--dry-run] [--home PATH]
|
|
170
|
+
cpac uninstall [--dry-run]
|
|
170
171
|
cpac upgrade [--check]
|
|
171
|
-
cpac version
|
|
172
|
+
cpac -v | --version
|
|
172
173
|
```
|
|
173
174
|
|
|
174
175
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
@@ -176,7 +177,8 @@ cpac version
|
|
|
176
177
|
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi 扩展重写、Kimi 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `--target codex` 有效。
|
|
177
178
|
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`);`install --target codex --v2_off` 写入 `enabled = false`。服务端还需在 CPA 配置中开启 `codex.optimize-multi-agent-v2`。
|
|
178
179
|
- `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
|
|
179
|
-
- `
|
|
180
|
+
- `restore` 必须带 `--target` 或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
|
|
181
|
+
- `uninstall` 先用和 `cpac codex --setup` 相同的方向键选择器确认(默认 Cancel),再 `restore --all`,最后 `npm uninstall -g @yhong91/cpac`。不要用 `--target`。非终端取消。`--dry-run` 跳过确认。
|
|
180
182
|
- `--home PATH` 临时把 `HOME` 指向给定目录,便于在隔离环境中试验。
|
|
181
183
|
- `upgrade` 是发布后更新本机的一次性入口:查 npm → 需要时 `npm install -g` → 对**已接入**的 Codex / Pi / Kimi 执行 `sync`。已是最新也会 sync。不要先手动 `npm install -g` 再 `cpac sync`。`--check` 只报告版本,不安装也不 sync。没有已安装目标时跳过 sync,不报错。只刷新配置、不升 CLI 时用 `cpac sync`。
|
|
182
184
|
|
|
@@ -194,7 +196,7 @@ cpac claude -- -p "检查当前项目"
|
|
|
194
196
|
#### 配置与清除
|
|
195
197
|
|
|
196
198
|
```bash
|
|
197
|
-
cpac claude setup
|
|
199
|
+
cpac claude --setup # 交互式依次配置 Sonnet / Haiku / Opus,然后启动
|
|
198
200
|
cpac claude clear # 重置所有覆盖,恢复自动选择
|
|
199
201
|
```
|
|
200
202
|
|
|
@@ -223,7 +225,7 @@ cpac codex exec "帮我写一个测试"
|
|
|
223
225
|
#### 配置与清除
|
|
224
226
|
|
|
225
227
|
```bash
|
|
226
|
-
cpac codex setup
|
|
228
|
+
cpac codex --setup # 交互式配置后注入并启动 Codex
|
|
227
229
|
cpac codex clear # 还原 Codex 原生配置并关闭代理
|
|
228
230
|
```
|
|
229
231
|
|
|
@@ -231,7 +233,7 @@ cpac codex clear # 还原 Codex 原生配置并关闭代
|
|
|
231
233
|
|
|
232
234
|
```bash
|
|
233
235
|
cpac inject [--v2_off] [--v2_models] [--max_context] [--config PATH]
|
|
234
|
-
cpac restore
|
|
236
|
+
cpac restore --target codex
|
|
235
237
|
```
|
|
236
238
|
|
|
237
239
|
`inject` 从远端 `/v1/models?client_version=1` 获取 Codex rich catalog,并启动只监听 `127.0.0.1` 的轻量转发代理。Codex 配置保留内置 `openai` provider,只写入根级 `model_catalog_json` 和:
|
|
@@ -246,7 +248,7 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。`inject` 会删除 `mod
|
|
|
246
248
|
|
|
247
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 组合使用会直接报错。
|
|
248
250
|
|
|
249
|
-
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore`
|
|
251
|
+
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore`:若 `config.toml` 相对首次注入未改,按原 mode 逐字节还原备份;若用户在注入期间改过文件,只撤掉 CPAC 写入的 catalog、url 和标记。原先没有 `config.toml` 且撤掉后为空则删除文件。关闭 CPAC loopback 代理。
|
|
250
252
|
|
|
251
253
|
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac inject` 即可恢复。
|
|
252
254
|
|
|
@@ -286,8 +288,7 @@ cpac pi install
|
|
|
286
288
|
它将生成 `cpac.ts` 写入 `~/.pi/agent/extensions/`,Pi 启动时自动加载并从 CPA 动态注册模型目录。卸载:
|
|
287
289
|
|
|
288
290
|
```bash
|
|
289
|
-
cpac pi
|
|
290
|
-
cpac pi status
|
|
291
|
+
cpac restore --target pi
|
|
291
292
|
```
|
|
292
293
|
|
|
293
294
|
Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
|
|
@@ -303,8 +304,7 @@ cpac kimi install
|
|
|
303
304
|
它在 `~/.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` 指向 Codex 注入使用的 loopback 代理,因此密钥不落盘;代理未运行时 install 会提示先执行 `cpac inject`。卸载与状态:
|
|
304
305
|
|
|
305
306
|
```bash
|
|
306
|
-
cpac kimi
|
|
307
|
-
cpac kimi status
|
|
307
|
+
cpac restore --target kimi
|
|
308
308
|
```
|
|
309
309
|
|
|
310
310
|
Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若 `config.toml` 已存在,会在同目录保留一份原始快照 `config.toml.cpac-backup`(只存最早版本),万一文件损坏可手动复制回去。
|
package/dist/agents.js
CHANGED
|
@@ -1,68 +1,15 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync } 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 {
|
|
7
|
-
import { MANAGED_MARKER, inject, restore } from "./codex.js";
|
|
6
|
+
import { fetchCatalog, injectedOwnership, pickSpawnModels, proxyFingerprint, readState, saveSpawnModels, stateBytes, stateProxy, } from "./config.js";
|
|
8
7
|
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "./proxy.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const apiKey = process.env[config.api_key_env]?.trim();
|
|
15
|
-
if (!apiKey)
|
|
16
|
-
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
17
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
18
|
-
let document;
|
|
19
|
-
try {
|
|
20
|
-
document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
throw new CPACError("invalid CPA catalog");
|
|
24
|
-
}
|
|
25
|
-
const models = {};
|
|
26
|
-
for (const row of document.models) {
|
|
27
|
-
const id = catalogModelId(row);
|
|
28
|
-
if (!id)
|
|
29
|
-
continue;
|
|
30
|
-
const window = typeof row.context_window === "number" && row.context_window > 0
|
|
31
|
-
? Math.floor(row.context_window)
|
|
32
|
-
: 0;
|
|
33
|
-
models[id] =
|
|
34
|
-
window > 0
|
|
35
|
-
? {
|
|
36
|
-
limit: {
|
|
37
|
-
context: window,
|
|
38
|
-
output: Math.min(OPENCODE_OUTPUT_BUDGET, window),
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
: {};
|
|
42
|
-
}
|
|
43
|
-
const content = JSON.stringify({
|
|
44
|
-
provider: {
|
|
45
|
-
cpac: {
|
|
46
|
-
npm: "@ai-sdk/openai-compatible",
|
|
47
|
-
options: { baseURL: apiBase(config.cpa_url), apiKey },
|
|
48
|
-
models,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
const env = {
|
|
53
|
-
...process.env,
|
|
54
|
-
OPENCODE_CONFIG_CONTENT: content,
|
|
55
|
-
};
|
|
56
|
-
return await new Promise((resolve, reject) => {
|
|
57
|
-
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
58
|
-
child.once("error", (error) => {
|
|
59
|
-
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
60
|
-
? `${executable} not found`
|
|
61
|
-
: `cannot start ${executable}: ${error.message}`));
|
|
62
|
-
});
|
|
63
|
-
child.once("close", (code) => resolve(code ?? 1));
|
|
64
|
-
});
|
|
65
|
-
}
|
|
8
|
+
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
9
|
+
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
10
|
+
import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
11
|
+
import { installPiExtension, isPiExtensionInstalled, piExtensionsDir, uninstallPiExtension, } from "./targets/pi.js";
|
|
12
|
+
import { CPACError, atomicWrite, checkboxPicker, expandUserPath, objectValue, resolveApiKey, } from "./util.js";
|
|
66
13
|
export async function runCodexConfig(config, configPath, options = {}) {
|
|
67
14
|
if (options.reset) {
|
|
68
15
|
let raw = {};
|
|
@@ -129,13 +76,16 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
129
76
|
if (["kimi", "grok"].includes(targetId)) {
|
|
130
77
|
const state = readState(config.state_dir);
|
|
131
78
|
const proxy = state ? stateProxy(state) : null;
|
|
132
|
-
|
|
79
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
80
|
+
const fingerprint = proxyFingerprint(config, apiKey);
|
|
81
|
+
const reuse = proxy !== null &&
|
|
82
|
+
state?.proxy_fingerprint === fingerprint &&
|
|
83
|
+
(await proxyIsHealthy(proxy));
|
|
84
|
+
if (!reuse) {
|
|
133
85
|
if (proxy)
|
|
134
86
|
await stopProxyProcess(proxy);
|
|
135
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
136
87
|
const started = await startProxyProcess(config, apiKey);
|
|
137
88
|
try {
|
|
138
|
-
const fingerprint = proxyFingerprint(config, apiKey);
|
|
139
89
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
140
90
|
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
141
91
|
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
@@ -143,7 +93,7 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
143
93
|
const originalMode = existsSync(config.codex_config)
|
|
144
94
|
? statSync(config.codex_config).mode & 0o7777
|
|
145
95
|
: 0o600;
|
|
146
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint));
|
|
96
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint, injectedOwnership(state)));
|
|
147
97
|
}
|
|
148
98
|
catch (error) {
|
|
149
99
|
await stopProxyProcess(started);
|
|
@@ -151,22 +101,24 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
151
101
|
}
|
|
152
102
|
}
|
|
153
103
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
104
|
+
if (!options.skipSync) {
|
|
105
|
+
if (target.installed(config)) {
|
|
106
|
+
try {
|
|
107
|
+
await runSync(config, [targetId], {
|
|
108
|
+
all: false,
|
|
109
|
+
dryRun: false,
|
|
110
|
+
v2Off: options.v2Off,
|
|
111
|
+
maxContext: options.maxContext,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.warn(`warning: auto-sync failed (${error instanceof Error ? error.message : String(error)}); launching with existing config`);
|
|
116
|
+
}
|
|
162
117
|
}
|
|
163
|
-
|
|
164
|
-
|
|
118
|
+
else {
|
|
119
|
+
await target.install(config, false, options.v2Off, options.maxContext);
|
|
165
120
|
}
|
|
166
121
|
}
|
|
167
|
-
else {
|
|
168
|
-
await target.install(config, false, options.v2Off, options.maxContext);
|
|
169
|
-
}
|
|
170
122
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
171
123
|
const child = spawn(executable, args, { stdio: "inherit" });
|
|
172
124
|
child.once("error", (error) => {
|
|
@@ -177,315 +129,6 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
177
129
|
child.once("close", (code) => resolvePromise(code ?? 1));
|
|
178
130
|
});
|
|
179
131
|
}
|
|
180
|
-
function piExtensionsDir() {
|
|
181
|
-
return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
|
|
182
|
-
}
|
|
183
|
-
export function isPiExtensionInstalled() {
|
|
184
|
-
return existsSync(join(piExtensionsDir(), "cpac.ts"));
|
|
185
|
-
}
|
|
186
|
-
function piTemplatePath() {
|
|
187
|
-
return join(dirname(fileURLToPath(import.meta.url)), "pi-extension.template");
|
|
188
|
-
}
|
|
189
|
-
function piExtensionContent(cpaUrl) {
|
|
190
|
-
return readFileSync(piTemplatePath(), "utf8").replace("__CPA_URL__", cpaUrl);
|
|
191
|
-
}
|
|
192
|
-
export async function installPiExtension(config) {
|
|
193
|
-
const dir = piExtensionsDir();
|
|
194
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
195
|
-
const target = join(dir, "cpac.ts");
|
|
196
|
-
ensureCpacBackup(target);
|
|
197
|
-
atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
|
|
198
|
-
console.log(`Installed Pi extension: ${target}`);
|
|
199
|
-
}
|
|
200
|
-
export async function uninstallPiExtension() {
|
|
201
|
-
const target = join(piExtensionsDir(), "cpac.ts");
|
|
202
|
-
if (!existsSync(target))
|
|
203
|
-
throw new CPACError("Pi extension is not installed");
|
|
204
|
-
unlinkSync(target);
|
|
205
|
-
console.log(`Removed Pi extension: ${target}`);
|
|
206
|
-
}
|
|
207
|
-
// Pre-write snapshot kept at <target>.cpac-backup (first version only) so a
|
|
208
|
-
// damaged config can be restored by hand even if uninstall cannot parse it.
|
|
209
|
-
function ensureCpacBackup(target) {
|
|
210
|
-
if (!existsSync(target))
|
|
211
|
-
return;
|
|
212
|
-
const backup = `${target}.cpac-backup`;
|
|
213
|
-
if (existsSync(backup))
|
|
214
|
-
return;
|
|
215
|
-
copyFileSync(target, backup);
|
|
216
|
-
}
|
|
217
|
-
function kimiConfigPath() {
|
|
218
|
-
const home = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
|
|
219
|
-
return join(expandUserPath(home), "config.toml");
|
|
220
|
-
}
|
|
221
|
-
const KIMI_BLOCK_START = "# >>> CPAC Kimi >>>";
|
|
222
|
-
const KIMI_BLOCK_END = "# <<< CPAC Kimi <<<";
|
|
223
|
-
const kimiBlockRegex = new RegExp(`${KIMI_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${KIMI_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
224
|
-
export function isKimiConfigInstalled() {
|
|
225
|
-
try {
|
|
226
|
-
const content = readFileSync(kimiConfigPath(), "utf8");
|
|
227
|
-
return (kimiBlockRegex.test(content) || /^\s*\[providers\.cpac\]\s*$/m.test(content));
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function isCpacTableHeader(line) {
|
|
234
|
-
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
235
|
-
if (!header)
|
|
236
|
-
return false;
|
|
237
|
-
const name = header[1];
|
|
238
|
-
return (name === "providers.cpac" ||
|
|
239
|
-
name.startsWith("providers.cpac.") ||
|
|
240
|
-
/^models\.(?:"cpac\/|'cpac\/)/.test(name));
|
|
241
|
-
}
|
|
242
|
-
// Kimi may rewrite config.toml and drop our comment markers, leaving the
|
|
243
|
-
// [providers.cpac] / [models."cpac/..."] tables behind. A later install would
|
|
244
|
-
// then append a second copy and make the file invalid TOML.
|
|
245
|
-
function stripOrphanCpacTables(content) {
|
|
246
|
-
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
247
|
-
const out = [];
|
|
248
|
-
let skipping = false;
|
|
249
|
-
for (const line of content.split(/\r?\n/)) {
|
|
250
|
-
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
251
|
-
skipping = isCpacTableHeader(line);
|
|
252
|
-
if (!skipping)
|
|
253
|
-
out.push(line);
|
|
254
|
-
}
|
|
255
|
-
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
256
|
-
}
|
|
257
|
-
function writeKimiBlock(path, block) {
|
|
258
|
-
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
259
|
-
content = content.replace(kimiBlockRegex, "");
|
|
260
|
-
// A truncated managed block (start marker without the end marker) leaves its
|
|
261
|
-
// tables behind; appending again would duplicate [providers.cpac], make the
|
|
262
|
-
// file invalid TOML, and block `kimi login`. The block is always appended
|
|
263
|
-
// last, so dropping from an orphaned start marker to EOF is safe.
|
|
264
|
-
const orphan = content.indexOf(KIMI_BLOCK_START);
|
|
265
|
-
if (orphan !== -1)
|
|
266
|
-
content = content.slice(0, orphan);
|
|
267
|
-
content = stripOrphanCpacTables(content);
|
|
268
|
-
if (block) {
|
|
269
|
-
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
270
|
-
}
|
|
271
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
272
|
-
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
273
|
-
atomicWrite(path, Buffer.from(content), mode);
|
|
274
|
-
}
|
|
275
|
-
const KIMI_REASONING_EFFORTS = new Set([
|
|
276
|
-
"minimal",
|
|
277
|
-
"low",
|
|
278
|
-
"medium",
|
|
279
|
-
"high",
|
|
280
|
-
"xhigh",
|
|
281
|
-
"max",
|
|
282
|
-
"ultra",
|
|
283
|
-
]);
|
|
284
|
-
function kimiSupportEfforts(row) {
|
|
285
|
-
const levels = row.supported_reasoning_levels ??
|
|
286
|
-
row.reasoning_effort_levels ??
|
|
287
|
-
row.reasoning_levels;
|
|
288
|
-
if (!Array.isArray(levels))
|
|
289
|
-
return [];
|
|
290
|
-
const seen = new Set();
|
|
291
|
-
const efforts = [];
|
|
292
|
-
for (const level of levels) {
|
|
293
|
-
const effort = typeof level === "string"
|
|
294
|
-
? level.toLowerCase()
|
|
295
|
-
: objectValue(level) && typeof level.effort === "string"
|
|
296
|
-
? level.effort.toLowerCase()
|
|
297
|
-
: undefined;
|
|
298
|
-
if (!effort || !KIMI_REASONING_EFFORTS.has(effort) || seen.has(effort))
|
|
299
|
-
continue;
|
|
300
|
-
seen.add(effort);
|
|
301
|
-
efforts.push(effort);
|
|
302
|
-
}
|
|
303
|
-
return efforts;
|
|
304
|
-
}
|
|
305
|
-
function kimiInputHasImage(row) {
|
|
306
|
-
const modalities = row.input_modalities;
|
|
307
|
-
if (!Array.isArray(modalities))
|
|
308
|
-
return true;
|
|
309
|
-
return modalities.some((value) => value === "image");
|
|
310
|
-
}
|
|
311
|
-
export async function installKimiConfig(config) {
|
|
312
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
313
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
314
|
-
let document;
|
|
315
|
-
try {
|
|
316
|
-
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
throw new CPACError("invalid CPA catalog");
|
|
320
|
-
}
|
|
321
|
-
const rows = catalogModelRows(document) ?? [];
|
|
322
|
-
if (rows.length === 0)
|
|
323
|
-
throw new CPACError("CPA catalog contains no models");
|
|
324
|
-
const state = readState(config.state_dir);
|
|
325
|
-
const recorded = state ? stateProxy(state) : null;
|
|
326
|
-
const port = recorded?.port ?? config.codex_proxy_port;
|
|
327
|
-
if (!port) {
|
|
328
|
-
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
329
|
-
}
|
|
330
|
-
const lines = [
|
|
331
|
-
KIMI_BLOCK_START,
|
|
332
|
-
"[providers.cpac]",
|
|
333
|
-
'type = "openai"',
|
|
334
|
-
`base_url = "http://127.0.0.1:${port}/v1"`,
|
|
335
|
-
'# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.',
|
|
336
|
-
'api_key = "cpac-loopback"',
|
|
337
|
-
];
|
|
338
|
-
for (const row of rows) {
|
|
339
|
-
const slug = catalogModelId(row);
|
|
340
|
-
if (!slug)
|
|
341
|
-
continue;
|
|
342
|
-
// ponytail: Kimi requires max_context_size; default 200000 when the catalog omits it.
|
|
343
|
-
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
344
|
-
? Math.floor(row.context_window)
|
|
345
|
-
: 200000;
|
|
346
|
-
const efforts = kimiSupportEfforts(row);
|
|
347
|
-
const capabilities = [
|
|
348
|
-
...(efforts.length > 0 ? ["thinking"] : []),
|
|
349
|
-
"tool_use",
|
|
350
|
-
...(kimiInputHasImage(row) ? ["image_in"] : []),
|
|
351
|
-
];
|
|
352
|
-
lines.push("", `[models."cpac/${slug}"]`, 'provider = "cpac"', `model = ${tomlString(slug)}`, `max_context_size = ${context}`, `capabilities = ${tomlStringArray(capabilities)}`);
|
|
353
|
-
if (typeof row.display_name === "string" && row.display_name.trim()) {
|
|
354
|
-
lines.push(`display_name = ${tomlString(row.display_name)}`);
|
|
355
|
-
}
|
|
356
|
-
if (efforts.length > 0) {
|
|
357
|
-
lines.push(`support_efforts = ${tomlStringArray(efforts)}`);
|
|
358
|
-
const rawDefaultEffort = (typeof row.default_reasoning_level === "string" &&
|
|
359
|
-
row.default_reasoning_level) ||
|
|
360
|
-
(typeof row.default_reasoning_effort === "string" &&
|
|
361
|
-
row.default_reasoning_effort) ||
|
|
362
|
-
(typeof row.default_effort === "string" && row.default_effort);
|
|
363
|
-
const defaultEffort = rawDefaultEffort && efforts.includes(rawDefaultEffort.toLowerCase())
|
|
364
|
-
? rawDefaultEffort.toLowerCase()
|
|
365
|
-
: undefined;
|
|
366
|
-
if (defaultEffort)
|
|
367
|
-
lines.push(`default_effort = ${tomlString(defaultEffort)}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
lines.push(KIMI_BLOCK_END);
|
|
371
|
-
const target = kimiConfigPath();
|
|
372
|
-
ensureCpacBackup(target);
|
|
373
|
-
writeKimiBlock(target, lines.join("\n"));
|
|
374
|
-
console.log(`Installed Kimi Code provider config: ${target}`);
|
|
375
|
-
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
376
|
-
console.log("Loopback proxy is not running; run: cpac inject");
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
export async function uninstallKimiConfig() {
|
|
380
|
-
const target = kimiConfigPath();
|
|
381
|
-
if (!isKimiConfigInstalled())
|
|
382
|
-
throw new CPACError("Kimi Code config is not installed");
|
|
383
|
-
writeKimiBlock(target, null);
|
|
384
|
-
console.log(`Removed Kimi Code provider config: ${target}`);
|
|
385
|
-
}
|
|
386
|
-
function grokHome() {
|
|
387
|
-
const home = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
388
|
-
return expandUserPath(home);
|
|
389
|
-
}
|
|
390
|
-
function grokConfigPath() {
|
|
391
|
-
return join(grokHome(), "config.toml");
|
|
392
|
-
}
|
|
393
|
-
const GROK_BLOCK_START = "# >>> CPAC Grok >>>";
|
|
394
|
-
const GROK_BLOCK_END = "# <<< CPAC Grok <<<";
|
|
395
|
-
const grokBlockRegex = new RegExp(`${GROK_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GROK_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
396
|
-
export function isGrokConfigInstalled() {
|
|
397
|
-
try {
|
|
398
|
-
const content = readFileSync(grokConfigPath(), "utf8");
|
|
399
|
-
return (grokBlockRegex.test(content) ||
|
|
400
|
-
/^\s*\[model\.(?:"cpac\/|'cpac\/|cpac-)/m.test(content));
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
function isCpacGrokTableHeader(line) {
|
|
407
|
-
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
408
|
-
if (!header)
|
|
409
|
-
return false;
|
|
410
|
-
const name = header[1];
|
|
411
|
-
return /^model\.(?:"cpac\/|'cpac\/|cpac-)/.test(name);
|
|
412
|
-
}
|
|
413
|
-
function stripOrphanCpacGrokTables(content) {
|
|
414
|
-
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
415
|
-
const out = [];
|
|
416
|
-
let skipping = false;
|
|
417
|
-
for (const line of content.split(/\r?\n/)) {
|
|
418
|
-
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
419
|
-
skipping = isCpacGrokTableHeader(line);
|
|
420
|
-
if (!skipping)
|
|
421
|
-
out.push(line);
|
|
422
|
-
}
|
|
423
|
-
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
424
|
-
}
|
|
425
|
-
function writeGrokBlock(path, block) {
|
|
426
|
-
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
427
|
-
content = content.replace(grokBlockRegex, "");
|
|
428
|
-
const orphan = content.indexOf(GROK_BLOCK_START);
|
|
429
|
-
if (orphan !== -1)
|
|
430
|
-
content = content.slice(0, orphan);
|
|
431
|
-
content = stripOrphanCpacGrokTables(content);
|
|
432
|
-
if (block) {
|
|
433
|
-
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
434
|
-
}
|
|
435
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
436
|
-
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
437
|
-
atomicWrite(path, Buffer.from(content), mode);
|
|
438
|
-
}
|
|
439
|
-
export async function installGrokConfig(config) {
|
|
440
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
441
|
-
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
442
|
-
let document;
|
|
443
|
-
try {
|
|
444
|
-
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
throw new CPACError("invalid CPA catalog");
|
|
448
|
-
}
|
|
449
|
-
const rows = catalogModelRows(document) ?? [];
|
|
450
|
-
if (rows.length === 0)
|
|
451
|
-
throw new CPACError("CPA catalog contains no models");
|
|
452
|
-
const state = readState(config.state_dir);
|
|
453
|
-
const recorded = state ? stateProxy(state) : null;
|
|
454
|
-
const port = recorded?.port ?? config.codex_proxy_port;
|
|
455
|
-
if (!port) {
|
|
456
|
-
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
457
|
-
}
|
|
458
|
-
const lines = [GROK_BLOCK_START];
|
|
459
|
-
for (const row of rows) {
|
|
460
|
-
const slug = catalogModelId(row);
|
|
461
|
-
if (!slug)
|
|
462
|
-
continue;
|
|
463
|
-
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
464
|
-
? Math.floor(row.context_window)
|
|
465
|
-
: 200000;
|
|
466
|
-
const name = typeof row.display_name === "string" && row.display_name.trim()
|
|
467
|
-
? `${row.display_name.trim()} (CPAC)`
|
|
468
|
-
: `CPAC ${slug}`;
|
|
469
|
-
lines.push(`[model."cpac/${slug}"]`, `model = ${tomlString(slug)}`, `base_url = "http://127.0.0.1:${port}/v1"`, 'api_backend = "responses"', '# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.', 'api_key = "cpac-loopback"', `name = ${tomlString(name)}`, `context_window = ${context}`, "");
|
|
470
|
-
}
|
|
471
|
-
if (lines[lines.length - 1] === "")
|
|
472
|
-
lines.pop();
|
|
473
|
-
lines.push(GROK_BLOCK_END);
|
|
474
|
-
const target = grokConfigPath();
|
|
475
|
-
ensureCpacBackup(target);
|
|
476
|
-
writeGrokBlock(target, lines.join("\n"));
|
|
477
|
-
console.log(`Installed Grok Build provider config: ${target}`);
|
|
478
|
-
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
479
|
-
console.log("Loopback proxy is not running; run: cpac inject");
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
export async function uninstallGrokConfig() {
|
|
483
|
-
const target = grokConfigPath();
|
|
484
|
-
if (!isGrokConfigInstalled())
|
|
485
|
-
throw new CPACError("Grok Build config is not installed");
|
|
486
|
-
writeGrokBlock(target, null);
|
|
487
|
-
console.log(`Removed Grok Build provider config: ${target}`);
|
|
488
|
-
}
|
|
489
132
|
function readPackageVersion() {
|
|
490
133
|
try {
|
|
491
134
|
const raw = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8");
|
|
@@ -696,30 +339,51 @@ export async function runSync(config, requested, options) {
|
|
|
696
339
|
maxContext: options.maxContext,
|
|
697
340
|
});
|
|
698
341
|
}
|
|
699
|
-
export async function
|
|
342
|
+
export async function runRestore(config, requested, options) {
|
|
343
|
+
if (requested.length === 0 && !options.all) {
|
|
344
|
+
throw new CPACError("restore requires --target <id> or --all");
|
|
345
|
+
}
|
|
700
346
|
const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
701
347
|
const selected = selectTargets(requested, options.all, eligible);
|
|
702
|
-
if (selected.length === 0) {
|
|
703
|
-
throw new CPACError("no installed CPAC integrations found; use --target <id> or --all");
|
|
704
|
-
}
|
|
705
348
|
for (const target of selected) {
|
|
706
349
|
if (!target.installed(config)) {
|
|
707
|
-
|
|
350
|
+
if (requested.length > 0)
|
|
351
|
+
console.log(`${target.id}: not installed`);
|
|
708
352
|
continue;
|
|
709
353
|
}
|
|
710
354
|
await target.uninstall(config, options.dryRun);
|
|
711
355
|
}
|
|
712
356
|
return 0;
|
|
713
357
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
358
|
+
async function confirmUninstall() {
|
|
359
|
+
let choice;
|
|
360
|
+
try {
|
|
361
|
+
[choice] = await checkboxPicker("This will restore all agents and run npm uninstall -g @yhong91/cpac.", ["Cancel", "Uninstall"], 1);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (error instanceof CPACError)
|
|
365
|
+
throw new CPACError("uninstall cancelled");
|
|
366
|
+
throw error;
|
|
717
367
|
}
|
|
368
|
+
if (choice !== "Uninstall")
|
|
369
|
+
throw new CPACError("uninstall cancelled");
|
|
370
|
+
}
|
|
371
|
+
export async function runUninstall(config, options) {
|
|
372
|
+
if (!options.dryRun)
|
|
373
|
+
await confirmUninstall();
|
|
374
|
+
await runRestore(config, [], { all: true, dryRun: options.dryRun });
|
|
718
375
|
if (options.dryRun) {
|
|
719
|
-
console.log("would
|
|
376
|
+
console.log("would uninstall @yhong91/cpac");
|
|
720
377
|
return 0;
|
|
721
378
|
}
|
|
722
|
-
|
|
379
|
+
console.log("Uninstalling @yhong91/cpac");
|
|
380
|
+
const result = spawnSync("npm", ["uninstall", "-g", "@yhong91/cpac"], {
|
|
381
|
+
stdio: "inherit",
|
|
382
|
+
});
|
|
383
|
+
if (result.status !== 0) {
|
|
384
|
+
console.error("npm uninstall failed");
|
|
385
|
+
return result.status ?? 1;
|
|
386
|
+
}
|
|
723
387
|
return 0;
|
|
724
388
|
}
|
|
725
389
|
function compareVersions(a, b) {
|