@yhong91/cpac 0.1.26 → 0.1.27
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 +17 -17
- package/dist/agents.js +53 -392
- package/dist/cpac.js +95 -33
- package/dist/{claude.js → targets/claude.js} +3 -3
- package/dist/{codex.js → targets/codex.js} +3 -3
- 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
|
@@ -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` 和:
|
|
@@ -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, 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 = {};
|
|
@@ -151,22 +98,24 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
151
98
|
}
|
|
152
99
|
}
|
|
153
100
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
101
|
+
if (!options.skipSync) {
|
|
102
|
+
if (target.installed(config)) {
|
|
103
|
+
try {
|
|
104
|
+
await runSync(config, [targetId], {
|
|
105
|
+
all: false,
|
|
106
|
+
dryRun: false,
|
|
107
|
+
v2Off: options.v2Off,
|
|
108
|
+
maxContext: options.maxContext,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.warn(`warning: auto-sync failed (${error instanceof Error ? error.message : String(error)}); launching with existing config`);
|
|
113
|
+
}
|
|
162
114
|
}
|
|
163
|
-
|
|
164
|
-
|
|
115
|
+
else {
|
|
116
|
+
await target.install(config, false, options.v2Off, options.maxContext);
|
|
165
117
|
}
|
|
166
118
|
}
|
|
167
|
-
else {
|
|
168
|
-
await target.install(config, false, options.v2Off, options.maxContext);
|
|
169
|
-
}
|
|
170
119
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
171
120
|
const child = spawn(executable, args, { stdio: "inherit" });
|
|
172
121
|
child.once("error", (error) => {
|
|
@@ -177,315 +126,6 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
177
126
|
child.once("close", (code) => resolvePromise(code ?? 1));
|
|
178
127
|
});
|
|
179
128
|
}
|
|
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
129
|
function readPackageVersion() {
|
|
490
130
|
try {
|
|
491
131
|
const raw = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8");
|
|
@@ -696,30 +336,51 @@ export async function runSync(config, requested, options) {
|
|
|
696
336
|
maxContext: options.maxContext,
|
|
697
337
|
});
|
|
698
338
|
}
|
|
699
|
-
export async function
|
|
339
|
+
export async function runRestore(config, requested, options) {
|
|
340
|
+
if (requested.length === 0 && !options.all) {
|
|
341
|
+
throw new CPACError("restore requires --target <id> or --all");
|
|
342
|
+
}
|
|
700
343
|
const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
701
344
|
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
345
|
for (const target of selected) {
|
|
706
346
|
if (!target.installed(config)) {
|
|
707
|
-
|
|
347
|
+
if (requested.length > 0)
|
|
348
|
+
console.log(`${target.id}: not installed`);
|
|
708
349
|
continue;
|
|
709
350
|
}
|
|
710
351
|
await target.uninstall(config, options.dryRun);
|
|
711
352
|
}
|
|
712
353
|
return 0;
|
|
713
354
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
355
|
+
async function confirmUninstall() {
|
|
356
|
+
let choice;
|
|
357
|
+
try {
|
|
358
|
+
[choice] = await checkboxPicker("This will restore all agents and run npm uninstall -g @yhong91/cpac.", ["Cancel", "Uninstall"], 1);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (error instanceof CPACError)
|
|
362
|
+
throw new CPACError("uninstall cancelled");
|
|
363
|
+
throw error;
|
|
717
364
|
}
|
|
365
|
+
if (choice !== "Uninstall")
|
|
366
|
+
throw new CPACError("uninstall cancelled");
|
|
367
|
+
}
|
|
368
|
+
export async function runUninstall(config, options) {
|
|
369
|
+
if (!options.dryRun)
|
|
370
|
+
await confirmUninstall();
|
|
371
|
+
await runRestore(config, [], { all: true, dryRun: options.dryRun });
|
|
718
372
|
if (options.dryRun) {
|
|
719
|
-
console.log("would
|
|
373
|
+
console.log("would uninstall @yhong91/cpac");
|
|
720
374
|
return 0;
|
|
721
375
|
}
|
|
722
|
-
|
|
376
|
+
console.log("Uninstalling @yhong91/cpac");
|
|
377
|
+
const result = spawnSync("npm", ["uninstall", "-g", "@yhong91/cpac"], {
|
|
378
|
+
stdio: "inherit",
|
|
379
|
+
});
|
|
380
|
+
if (result.status !== 0) {
|
|
381
|
+
console.error("npm uninstall failed");
|
|
382
|
+
return result.status ?? 1;
|
|
383
|
+
}
|
|
723
384
|
return 0;
|
|
724
385
|
}
|
|
725
386
|
function compareVersions(a, b) {
|
package/dist/cpac.js
CHANGED
|
@@ -3,18 +3,26 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { CPAC_VERSION,
|
|
7
|
-
import { runClaude, runClaudeConfig, runClaudeModels } from "./claude.js";
|
|
8
|
-
import { inject, multiAgentV2Enabled } from "./codex.js";
|
|
6
|
+
import { CPAC_VERSION, runCodexConfig, runDetect, runInstall, runRestore, runSync, runTargetLauncher, runUninstall, runUpgrade, } from "./agents.js";
|
|
7
|
+
import { runClaude, runClaudeConfig, runClaudeModels, } from "./targets/claude.js";
|
|
8
|
+
import { inject, multiAgentV2Enabled } from "./targets/codex.js";
|
|
9
|
+
import { isGrokConfigInstalled } from "./targets/grok.js";
|
|
10
|
+
import { isKimiConfigInstalled } from "./targets/kimi.js";
|
|
11
|
+
import { runOpencode } from "./targets/opencode.js";
|
|
12
|
+
import { isPiExtensionInstalled } from "./targets/pi.js";
|
|
9
13
|
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, } from "./config.js";
|
|
10
14
|
import { proxyIsHealthy, runProxy, runProxyChild } from "./proxy.js";
|
|
11
15
|
import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
|
|
12
16
|
export { PROVIDER, loadConfig, saveSpawnModels, liftContextWindows, reorderCatalog, readState, stateProxy, } from "./config.js";
|
|
13
17
|
export { saveApiKeyExport } from "./util.js";
|
|
14
18
|
export { stopProxyProcess } from "./proxy.js";
|
|
15
|
-
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, withMultiAgentV2, } from "./codex.js";
|
|
16
|
-
export { claudeTierSlots, createClaudeProxy, runClaude } from "./claude.js";
|
|
17
|
-
export { detectClientVersion, detectTargets,
|
|
19
|
+
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, withMultiAgentV2, } from "./targets/codex.js";
|
|
20
|
+
export { claudeTierSlots, createClaudeProxy, runClaude, } from "./targets/claude.js";
|
|
21
|
+
export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, runTargetLauncher, } from "./agents.js";
|
|
22
|
+
export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
23
|
+
export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
24
|
+
export { runOpencode } from "./targets/opencode.js";
|
|
25
|
+
export { installPiExtension, isPiExtensionInstalled, uninstallPiExtension, } from "./targets/pi.js";
|
|
18
26
|
export async function status(config) {
|
|
19
27
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
20
28
|
const keyConfigured = !!apiKey;
|
|
@@ -62,7 +70,11 @@ export async function status(config) {
|
|
|
62
70
|
return 0;
|
|
63
71
|
}
|
|
64
72
|
async function guide(config) {
|
|
65
|
-
console.log(`CPA Companion\n\nCPA: ${config.cpa_url}\nCPA_API_KEY: ${process.env[config.api_key_env]?.trim() ? "configured" : "not configured"}\n\nCommands:\n cpac <agent> [args...] Launch an agent (codex, kimi, grok, pi, claude, opencode) through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi, Kimi, Grok)\n cpac restore
|
|
73
|
+
console.log(`CPA Companion\n\nCPA: ${config.cpa_url}\nCPA_API_KEY: ${process.env[config.api_key_env]?.trim() ? "configured" : "not configured"}\n\nCommands:\n cpac <agent> [args...] Launch an agent (codex, kimi, grok, pi, claude, opencode) through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi, Kimi, Grok)\n cpac restore --target <id> | --all Detach injected agents
|
|
74
|
+
cpac detect Show supported targets and install status
|
|
75
|
+
cpac install Install CPA integration into detected targets
|
|
76
|
+
cpac sync Refresh installed Codex/Pi/Kimi/Grok injections from CPA
|
|
77
|
+
cpac uninstall Detach all agents, then remove the cpac package (confirms first)\n cpac upgrade Update cpac from npm and sync installed agents\n cpac --help Show command usage`);
|
|
66
78
|
if (process.env[config.api_key_env]?.trim())
|
|
67
79
|
return 0;
|
|
68
80
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -78,20 +90,21 @@ function usage() {
|
|
|
78
90
|
return [
|
|
79
91
|
"Usage: cpac",
|
|
80
92
|
" cpac <codex|kimi|grok|pi|claude|opencode> [args...]",
|
|
81
|
-
" cpac <codex|claude> setup",
|
|
93
|
+
" cpac <codex|claude> --setup [args...]",
|
|
82
94
|
" cpac <codex|claude|kimi|grok|pi> clear",
|
|
83
95
|
" cpac inject [--v2_off] [--v2_models] [--max_context] [--config PATH]",
|
|
84
|
-
" cpac restore
|
|
96
|
+
" cpac restore --target <id> | --all [--dry-run] [--config PATH]",
|
|
85
97
|
" cpac status [--config PATH]",
|
|
86
98
|
" cpac proxy [--config PATH]",
|
|
87
99
|
" cpac detect [--json] [--home PATH]",
|
|
88
100
|
" cpac install [--target codex,pi,kimi,grok] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context] [--home PATH]",
|
|
89
101
|
" cpac sync [--target codex,pi,kimi,grok] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context] [--home PATH]",
|
|
102
|
+
" cpac uninstall [--dry-run]",
|
|
90
103
|
" cpac upgrade [--check]",
|
|
91
|
-
" cpac version",
|
|
104
|
+
" cpac -v | --version",
|
|
92
105
|
"",
|
|
93
106
|
"launch an agent directly with `cpac <agent> [args...]` (all args forwarded).",
|
|
94
|
-
"configure
|
|
107
|
+
"configure then launch with `cpac <codex|claude> --setup`; restore with `cpac <agent> clear`.",
|
|
95
108
|
].join("\n");
|
|
96
109
|
}
|
|
97
110
|
function parseArgs(args) {
|
|
@@ -103,8 +116,7 @@ function parseArgs(args) {
|
|
|
103
116
|
args[0] === "sync" ||
|
|
104
117
|
args[0] === "uninstall" ||
|
|
105
118
|
args[0] === "restore" ||
|
|
106
|
-
args[0] === "upgrade"
|
|
107
|
-
args[0] === "version") {
|
|
119
|
+
args[0] === "upgrade") {
|
|
108
120
|
const parsed = {
|
|
109
121
|
command: args[0],
|
|
110
122
|
configPath: defaultConfigPath(),
|
|
@@ -169,10 +181,34 @@ function parseArgs(args) {
|
|
|
169
181
|
if (["codex", "kimi", "grok", "pi", "claude", "opencode"].includes(args[0])) {
|
|
170
182
|
const targetId = args[0];
|
|
171
183
|
let configPath = defaultConfigPath();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
const raw = args.slice(1);
|
|
185
|
+
let setup = false;
|
|
186
|
+
let clear = false;
|
|
187
|
+
const rest = [];
|
|
188
|
+
for (let i = 0; i < raw.length; i++) {
|
|
189
|
+
const arg = raw[i];
|
|
190
|
+
if (i === 0 && arg === "setup") {
|
|
191
|
+
setup = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (i === 0 && arg === "clear") {
|
|
195
|
+
clear = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg === "--setup") {
|
|
199
|
+
setup = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (arg === "--clear") {
|
|
203
|
+
clear = true;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
rest.push(arg);
|
|
207
|
+
}
|
|
208
|
+
if (clear) {
|
|
209
|
+
for (let i = 0; i < rest.length; i++) {
|
|
210
|
+
if (rest[i] === "--config") {
|
|
211
|
+
const val = rest[++i];
|
|
176
212
|
if (val)
|
|
177
213
|
configPath = resolve(expandUserPath(val));
|
|
178
214
|
}
|
|
@@ -201,13 +237,17 @@ function parseArgs(args) {
|
|
|
201
237
|
maxContext: false,
|
|
202
238
|
};
|
|
203
239
|
}
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
if (setup) {
|
|
241
|
+
const launchArgs = [];
|
|
242
|
+
for (let i = 0; i < rest.length; i++) {
|
|
243
|
+
if (rest[i] === "--config") {
|
|
244
|
+
const val = rest[++i];
|
|
245
|
+
if (!val)
|
|
246
|
+
throw new CPACError("--config requires a path");
|
|
247
|
+
configPath = resolve(expandUserPath(val));
|
|
248
|
+
continue;
|
|
210
249
|
}
|
|
250
|
+
launchArgs.push(rest[i]);
|
|
211
251
|
}
|
|
212
252
|
if (targetId === "codex") {
|
|
213
253
|
return {
|
|
@@ -218,6 +258,8 @@ function parseArgs(args) {
|
|
|
218
258
|
maxContext: false,
|
|
219
259
|
reset: false,
|
|
220
260
|
interactive: true,
|
|
261
|
+
launch: true,
|
|
262
|
+
args: launchArgs,
|
|
221
263
|
};
|
|
222
264
|
}
|
|
223
265
|
if (targetId === "claude") {
|
|
@@ -228,11 +270,13 @@ function parseArgs(args) {
|
|
|
228
270
|
pick: [],
|
|
229
271
|
reset: false,
|
|
230
272
|
interactive: true,
|
|
273
|
+
launch: true,
|
|
274
|
+
args: launchArgs,
|
|
231
275
|
};
|
|
232
276
|
}
|
|
233
277
|
throw new CPACError(`${targetId} does not require setup (all CPA models are auto-synced). Run 'cpac ${targetId}' or 'cpac install --target ${targetId}'.`);
|
|
234
278
|
}
|
|
235
|
-
const passArgs =
|
|
279
|
+
const passArgs = rest;
|
|
236
280
|
if (targetId === "codex") {
|
|
237
281
|
return {
|
|
238
282
|
command: "codex",
|
|
@@ -372,6 +416,10 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
372
416
|
return 1;
|
|
373
417
|
return await runProxyChild(port);
|
|
374
418
|
}
|
|
419
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
420
|
+
console.log(CPAC_VERSION);
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
375
423
|
const parsed = parseArgs(args);
|
|
376
424
|
if (!parsed)
|
|
377
425
|
return 0;
|
|
@@ -413,34 +461,48 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
413
461
|
maxContext: parsed.maxContext,
|
|
414
462
|
});
|
|
415
463
|
}
|
|
416
|
-
if (parsed.command === "
|
|
464
|
+
if (parsed.command === "restore")
|
|
417
465
|
return await runRestore(config, parsed.targets, {
|
|
418
466
|
all: parsed.all,
|
|
419
467
|
dryRun: parsed.dryRun,
|
|
420
468
|
});
|
|
469
|
+
if (parsed.command === "uninstall") {
|
|
470
|
+
if (parsed.targets.length || parsed.all) {
|
|
471
|
+
throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore --target <id> | --all");
|
|
472
|
+
}
|
|
473
|
+
return await runUninstall(config, { dryRun: parsed.dryRun });
|
|
474
|
+
}
|
|
421
475
|
if (parsed.command === "upgrade")
|
|
422
476
|
return await runUpgrade(config, parsed.check);
|
|
423
|
-
if (parsed.command === "version") {
|
|
424
|
-
console.log(CPAC_VERSION);
|
|
425
|
-
return 0;
|
|
426
|
-
}
|
|
427
477
|
if (parsed.command === "claude")
|
|
428
478
|
return await runClaude(config, parsed.args);
|
|
429
|
-
if (parsed.command === "claude-config")
|
|
430
|
-
|
|
479
|
+
if (parsed.command === "claude-config") {
|
|
480
|
+
const code = await runClaudeConfig(config, parsed.configPath, {
|
|
431
481
|
models: parsed.models,
|
|
432
482
|
pick: parsed.pick,
|
|
433
483
|
reset: parsed.reset,
|
|
434
484
|
interactive: parsed.interactive,
|
|
435
485
|
});
|
|
436
|
-
|
|
437
|
-
|
|
486
|
+
if (code !== 0 || !parsed.launch)
|
|
487
|
+
return code;
|
|
488
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
489
|
+
return code;
|
|
490
|
+
return await runClaude(loadConfig(parsed.configPath, true), parsed.args ?? []);
|
|
491
|
+
}
|
|
492
|
+
if (parsed.command === "codex-config") {
|
|
493
|
+
const code = await runCodexConfig(config, parsed.configPath, {
|
|
438
494
|
v2Off: parsed.v2Off,
|
|
439
495
|
v2Models: parsed.v2Models,
|
|
440
496
|
maxContext: parsed.maxContext,
|
|
441
497
|
reset: parsed.reset,
|
|
442
498
|
interactive: parsed.interactive,
|
|
443
499
|
});
|
|
500
|
+
if (code !== 0 || !parsed.launch)
|
|
501
|
+
return code;
|
|
502
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
503
|
+
return code;
|
|
504
|
+
return await runTargetLauncher(loadConfig(parsed.configPath, true), "codex", parsed.args ?? [], "codex", { skipSync: true });
|
|
505
|
+
}
|
|
444
506
|
if (parsed.command === "opencode")
|
|
445
507
|
return await runOpencode(config, parsed.args);
|
|
446
508
|
if (parsed.command === "codex") {
|
|
@@ -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 "
|
|
6
|
-
import { proxyHeaders, responseHeaders, upstreamUrl } from "
|
|
7
|
-
import { CPACError, atomicWrite, checkboxPicker, objectValue, resolveApiKey, } from "
|
|
5
|
+
import { apiBase, catalogModelId, catalogModelRows, fetchCatalog, loadConfig, } from "../config.js";
|
|
6
|
+
import { proxyHeaders, responseHeaders, upstreamUrl } from "../proxy.js";
|
|
7
|
+
import { CPACError, atomicWrite, checkboxPicker, objectValue, resolveApiKey, } 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
|
|
@@ -1,8 +1,8 @@
|
|
|
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, fetchCatalog, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, stateBytes, stateProxy, } from "
|
|
4
|
-
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "
|
|
5
|
-
import { CPACError, atomicWrite, dominantEol, tomlString } from "
|
|
3
|
+
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, stateBytes, stateProxy, } from "../config.js";
|
|
4
|
+
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "../proxy.js";
|
|
5
|
+
import { CPACError, atomicWrite, dominantEol, tomlString } from "../util.js";
|
|
6
6
|
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
7
7
|
const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model_catalog_json')\s*=/;
|
|
8
8
|
const OPENAI_BASE_URL_KEY = /^\s*(?:openai_base_url|"openai_base_url"|'openai_base_url')\s*=/;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { catalogModelId, catalogModelRows, fetchCatalog, readState, stateProxy, } from "../config.js";
|
|
5
|
+
import { proxyIsHealthy } from "../proxy.js";
|
|
6
|
+
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, resolveApiKey, tomlString, } from "../util.js";
|
|
7
|
+
export function grokHome() {
|
|
8
|
+
const home = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
9
|
+
return expandUserPath(home);
|
|
10
|
+
}
|
|
11
|
+
export function grokConfigPath() {
|
|
12
|
+
return join(grokHome(), "config.toml");
|
|
13
|
+
}
|
|
14
|
+
const GROK_BLOCK_START = "# >>> CPAC Grok >>>";
|
|
15
|
+
const GROK_BLOCK_END = "# <<< CPAC Grok <<<";
|
|
16
|
+
const grokBlockRegex = new RegExp(`${GROK_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GROK_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
17
|
+
export function isGrokConfigInstalled() {
|
|
18
|
+
try {
|
|
19
|
+
const content = readFileSync(grokConfigPath(), "utf8");
|
|
20
|
+
return (grokBlockRegex.test(content) ||
|
|
21
|
+
/^\s*\[model\.(?:"cpac\/|'cpac\/|cpac-)/m.test(content));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isCpacGrokTableHeader(line) {
|
|
28
|
+
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
29
|
+
if (!header)
|
|
30
|
+
return false;
|
|
31
|
+
const name = header[1];
|
|
32
|
+
return /^model\.(?:"cpac\/|'cpac\/|cpac-)/.test(name);
|
|
33
|
+
}
|
|
34
|
+
function stripOrphanCpacGrokTables(content) {
|
|
35
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
36
|
+
const out = [];
|
|
37
|
+
let skipping = false;
|
|
38
|
+
for (const line of content.split(/\r?\n/)) {
|
|
39
|
+
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
40
|
+
skipping = isCpacGrokTableHeader(line);
|
|
41
|
+
if (!skipping)
|
|
42
|
+
out.push(line);
|
|
43
|
+
}
|
|
44
|
+
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
45
|
+
}
|
|
46
|
+
function writeGrokBlock(path, block) {
|
|
47
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
48
|
+
content = content.replace(grokBlockRegex, "");
|
|
49
|
+
const orphan = content.indexOf(GROK_BLOCK_START);
|
|
50
|
+
if (orphan !== -1)
|
|
51
|
+
content = content.slice(0, orphan);
|
|
52
|
+
content = stripOrphanCpacGrokTables(content);
|
|
53
|
+
if (block) {
|
|
54
|
+
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
57
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
58
|
+
atomicWrite(path, Buffer.from(content), mode);
|
|
59
|
+
}
|
|
60
|
+
export async function installGrokConfig(config) {
|
|
61
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
62
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
63
|
+
let document;
|
|
64
|
+
try {
|
|
65
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new CPACError("invalid CPA catalog");
|
|
69
|
+
}
|
|
70
|
+
const rows = catalogModelRows(document) ?? [];
|
|
71
|
+
if (rows.length === 0)
|
|
72
|
+
throw new CPACError("CPA catalog contains no models");
|
|
73
|
+
const state = readState(config.state_dir);
|
|
74
|
+
const recorded = state ? stateProxy(state) : null;
|
|
75
|
+
const port = recorded?.port ?? config.codex_proxy_port;
|
|
76
|
+
if (!port) {
|
|
77
|
+
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
78
|
+
}
|
|
79
|
+
const lines = [GROK_BLOCK_START];
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const slug = catalogModelId(row);
|
|
82
|
+
if (!slug)
|
|
83
|
+
continue;
|
|
84
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
85
|
+
? Math.floor(row.context_window)
|
|
86
|
+
: 200000;
|
|
87
|
+
const name = typeof row.display_name === "string" && row.display_name.trim()
|
|
88
|
+
? `${row.display_name.trim()} (CPAC)`
|
|
89
|
+
: `CPAC ${slug}`;
|
|
90
|
+
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}`, "");
|
|
91
|
+
}
|
|
92
|
+
if (lines[lines.length - 1] === "")
|
|
93
|
+
lines.pop();
|
|
94
|
+
lines.push(GROK_BLOCK_END);
|
|
95
|
+
const target = grokConfigPath();
|
|
96
|
+
ensureCpacBackup(target);
|
|
97
|
+
writeGrokBlock(target, lines.join("\n"));
|
|
98
|
+
console.log(`Installed Grok Build provider config: ${target}`);
|
|
99
|
+
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
100
|
+
console.log("Loopback proxy is not running; run: cpac inject");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function uninstallGrokConfig() {
|
|
104
|
+
const target = grokConfigPath();
|
|
105
|
+
if (!isGrokConfigInstalled())
|
|
106
|
+
throw new CPACError("Grok Build config is not installed");
|
|
107
|
+
writeGrokBlock(target, null);
|
|
108
|
+
console.log(`Removed Grok Build provider config: ${target}`);
|
|
109
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { catalogModelId, catalogModelRows, fetchCatalog, readState, stateProxy, } from "../config.js";
|
|
5
|
+
import { proxyIsHealthy } from "../proxy.js";
|
|
6
|
+
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, objectValue, resolveApiKey, tomlString, tomlStringArray, } from "../util.js";
|
|
7
|
+
export function kimiConfigPath() {
|
|
8
|
+
const home = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
|
|
9
|
+
return join(expandUserPath(home), "config.toml");
|
|
10
|
+
}
|
|
11
|
+
const KIMI_BLOCK_START = "# >>> CPAC Kimi >>>";
|
|
12
|
+
const KIMI_BLOCK_END = "# <<< CPAC Kimi <<<";
|
|
13
|
+
const kimiBlockRegex = new RegExp(`${KIMI_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${KIMI_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
14
|
+
export function isKimiConfigInstalled() {
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(kimiConfigPath(), "utf8");
|
|
17
|
+
return (kimiBlockRegex.test(content) || /^\s*\[providers\.cpac\]\s*$/m.test(content));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isCpacTableHeader(line) {
|
|
24
|
+
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
25
|
+
if (!header)
|
|
26
|
+
return false;
|
|
27
|
+
const name = header[1];
|
|
28
|
+
return (name === "providers.cpac" ||
|
|
29
|
+
name.startsWith("providers.cpac.") ||
|
|
30
|
+
/^models\.(?:"cpac\/|'cpac\/)/.test(name));
|
|
31
|
+
}
|
|
32
|
+
// Kimi may rewrite config.toml and drop our comment markers, leaving the
|
|
33
|
+
// [providers.cpac] / [models."cpac/..."] tables behind. A later install would
|
|
34
|
+
// then append a second copy and make the file invalid TOML.
|
|
35
|
+
function stripOrphanCpacTables(content) {
|
|
36
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
37
|
+
const out = [];
|
|
38
|
+
let skipping = false;
|
|
39
|
+
for (const line of content.split(/\r?\n/)) {
|
|
40
|
+
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
41
|
+
skipping = isCpacTableHeader(line);
|
|
42
|
+
if (!skipping)
|
|
43
|
+
out.push(line);
|
|
44
|
+
}
|
|
45
|
+
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
46
|
+
}
|
|
47
|
+
function writeKimiBlock(path, block) {
|
|
48
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
49
|
+
content = content.replace(kimiBlockRegex, "");
|
|
50
|
+
// A truncated managed block (start marker without the end marker) leaves its
|
|
51
|
+
// tables behind; appending again would duplicate [providers.cpac], make the
|
|
52
|
+
// file invalid TOML, and block `kimi login`. The block is always appended
|
|
53
|
+
// last, so dropping from an orphaned start marker to EOF is safe.
|
|
54
|
+
const orphan = content.indexOf(KIMI_BLOCK_START);
|
|
55
|
+
if (orphan !== -1)
|
|
56
|
+
content = content.slice(0, orphan);
|
|
57
|
+
content = stripOrphanCpacTables(content);
|
|
58
|
+
if (block) {
|
|
59
|
+
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
60
|
+
}
|
|
61
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
62
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
63
|
+
atomicWrite(path, Buffer.from(content), mode);
|
|
64
|
+
}
|
|
65
|
+
const KIMI_REASONING_EFFORTS = new Set([
|
|
66
|
+
"minimal",
|
|
67
|
+
"low",
|
|
68
|
+
"medium",
|
|
69
|
+
"high",
|
|
70
|
+
"xhigh",
|
|
71
|
+
"max",
|
|
72
|
+
"ultra",
|
|
73
|
+
]);
|
|
74
|
+
function kimiSupportEfforts(row) {
|
|
75
|
+
const levels = row.supported_reasoning_levels ??
|
|
76
|
+
row.reasoning_effort_levels ??
|
|
77
|
+
row.reasoning_levels;
|
|
78
|
+
if (!Array.isArray(levels))
|
|
79
|
+
return [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const efforts = [];
|
|
82
|
+
for (const level of levels) {
|
|
83
|
+
const effort = typeof level === "string"
|
|
84
|
+
? level.toLowerCase()
|
|
85
|
+
: objectValue(level) && typeof level.effort === "string"
|
|
86
|
+
? level.effort.toLowerCase()
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!effort || !KIMI_REASONING_EFFORTS.has(effort) || seen.has(effort))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(effort);
|
|
91
|
+
efforts.push(effort);
|
|
92
|
+
}
|
|
93
|
+
return efforts;
|
|
94
|
+
}
|
|
95
|
+
function kimiInputHasImage(row) {
|
|
96
|
+
const modalities = row.input_modalities;
|
|
97
|
+
if (!Array.isArray(modalities))
|
|
98
|
+
return true;
|
|
99
|
+
return modalities.some((value) => value === "image");
|
|
100
|
+
}
|
|
101
|
+
export async function installKimiConfig(config) {
|
|
102
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
103
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
104
|
+
let document;
|
|
105
|
+
try {
|
|
106
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new CPACError("invalid CPA catalog");
|
|
110
|
+
}
|
|
111
|
+
const rows = catalogModelRows(document) ?? [];
|
|
112
|
+
if (rows.length === 0)
|
|
113
|
+
throw new CPACError("CPA catalog contains no models");
|
|
114
|
+
const state = readState(config.state_dir);
|
|
115
|
+
const recorded = state ? stateProxy(state) : null;
|
|
116
|
+
const port = recorded?.port ?? config.codex_proxy_port;
|
|
117
|
+
if (!port) {
|
|
118
|
+
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
119
|
+
}
|
|
120
|
+
const lines = [
|
|
121
|
+
KIMI_BLOCK_START,
|
|
122
|
+
"[providers.cpac]",
|
|
123
|
+
'type = "openai"',
|
|
124
|
+
`base_url = "http://127.0.0.1:${port}/v1"`,
|
|
125
|
+
'# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.',
|
|
126
|
+
'api_key = "cpac-loopback"',
|
|
127
|
+
];
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
const slug = catalogModelId(row);
|
|
130
|
+
if (!slug)
|
|
131
|
+
continue;
|
|
132
|
+
// ponytail: Kimi requires max_context_size; default 200000 when the catalog omits it.
|
|
133
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
134
|
+
? Math.floor(row.context_window)
|
|
135
|
+
: 200000;
|
|
136
|
+
const efforts = kimiSupportEfforts(row);
|
|
137
|
+
const capabilities = [
|
|
138
|
+
...(efforts.length > 0 ? ["thinking"] : []),
|
|
139
|
+
"tool_use",
|
|
140
|
+
...(kimiInputHasImage(row) ? ["image_in"] : []),
|
|
141
|
+
];
|
|
142
|
+
lines.push("", `[models."cpac/${slug}"]`, 'provider = "cpac"', `model = ${tomlString(slug)}`, `max_context_size = ${context}`, `capabilities = ${tomlStringArray(capabilities)}`);
|
|
143
|
+
if (typeof row.display_name === "string" && row.display_name.trim()) {
|
|
144
|
+
lines.push(`display_name = ${tomlString(row.display_name)}`);
|
|
145
|
+
}
|
|
146
|
+
if (efforts.length > 0) {
|
|
147
|
+
lines.push(`support_efforts = ${tomlStringArray(efforts)}`);
|
|
148
|
+
const rawDefaultEffort = (typeof row.default_reasoning_level === "string" &&
|
|
149
|
+
row.default_reasoning_level) ||
|
|
150
|
+
(typeof row.default_reasoning_effort === "string" &&
|
|
151
|
+
row.default_reasoning_effort) ||
|
|
152
|
+
(typeof row.default_effort === "string" && row.default_effort);
|
|
153
|
+
const defaultEffort = rawDefaultEffort && efforts.includes(rawDefaultEffort.toLowerCase())
|
|
154
|
+
? rawDefaultEffort.toLowerCase()
|
|
155
|
+
: undefined;
|
|
156
|
+
if (defaultEffort)
|
|
157
|
+
lines.push(`default_effort = ${tomlString(defaultEffort)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
lines.push(KIMI_BLOCK_END);
|
|
161
|
+
const target = kimiConfigPath();
|
|
162
|
+
ensureCpacBackup(target);
|
|
163
|
+
writeKimiBlock(target, lines.join("\n"));
|
|
164
|
+
console.log(`Installed Kimi Code provider config: ${target}`);
|
|
165
|
+
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
166
|
+
console.log("Loopback proxy is not running; run: cpac inject");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function uninstallKimiConfig() {
|
|
170
|
+
const target = kimiConfigPath();
|
|
171
|
+
if (!isKimiConfigInstalled())
|
|
172
|
+
throw new CPACError("Kimi Code config is not installed");
|
|
173
|
+
writeKimiBlock(target, null);
|
|
174
|
+
console.log(`Removed Kimi Code provider config: ${target}`);
|
|
175
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { apiBase, catalogModelId, fetchCatalog, } from "../config.js";
|
|
3
|
+
import { CPACError } from "../util.js";
|
|
4
|
+
const OPENCODE_OUTPUT_BUDGET = 32_000;
|
|
5
|
+
// Ephemeral takeover: inline a CPA provider via OPENCODE_CONFIG_CONTENT so no
|
|
6
|
+
// opencode config file is touched; the session ends with zero residue.
|
|
7
|
+
export async function runOpencode(config, args, executable = "opencode") {
|
|
8
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
9
|
+
if (!apiKey)
|
|
10
|
+
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
11
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
12
|
+
let document;
|
|
13
|
+
try {
|
|
14
|
+
document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new CPACError("invalid CPA catalog");
|
|
18
|
+
}
|
|
19
|
+
const models = {};
|
|
20
|
+
for (const row of document.models) {
|
|
21
|
+
const id = catalogModelId(row);
|
|
22
|
+
if (!id)
|
|
23
|
+
continue;
|
|
24
|
+
const window = typeof row.context_window === "number" && row.context_window > 0
|
|
25
|
+
? Math.floor(row.context_window)
|
|
26
|
+
: 0;
|
|
27
|
+
models[id] =
|
|
28
|
+
window > 0
|
|
29
|
+
? {
|
|
30
|
+
limit: {
|
|
31
|
+
context: window,
|
|
32
|
+
output: Math.min(OPENCODE_OUTPUT_BUDGET, window),
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
: {};
|
|
36
|
+
}
|
|
37
|
+
const content = JSON.stringify({
|
|
38
|
+
provider: {
|
|
39
|
+
cpac: {
|
|
40
|
+
npm: "@ai-sdk/openai-compatible",
|
|
41
|
+
options: { baseURL: apiBase(config.cpa_url), apiKey },
|
|
42
|
+
models,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const env = {
|
|
47
|
+
...process.env,
|
|
48
|
+
OPENCODE_CONFIG_CONTENT: content,
|
|
49
|
+
};
|
|
50
|
+
return await new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
52
|
+
child.once("error", (error) => {
|
|
53
|
+
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
54
|
+
? `${executable} not found`
|
|
55
|
+
: `cannot start ${executable}: ${error.message}`));
|
|
56
|
+
});
|
|
57
|
+
child.once("close", (code) => resolve(code ?? 1));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, } from "../util.js";
|
|
6
|
+
export function piExtensionsDir() {
|
|
7
|
+
return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
|
|
8
|
+
}
|
|
9
|
+
export function isPiExtensionInstalled() {
|
|
10
|
+
return existsSync(join(piExtensionsDir(), "cpac.ts"));
|
|
11
|
+
}
|
|
12
|
+
function piTemplatePath() {
|
|
13
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "pi-extension.template");
|
|
14
|
+
}
|
|
15
|
+
function piExtensionContent(cpaUrl) {
|
|
16
|
+
return readFileSync(piTemplatePath(), "utf8").replace("__CPA_URL__", cpaUrl);
|
|
17
|
+
}
|
|
18
|
+
export async function installPiExtension(config) {
|
|
19
|
+
const dir = piExtensionsDir();
|
|
20
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
21
|
+
const target = join(dir, "cpac.ts");
|
|
22
|
+
ensureCpacBackup(target);
|
|
23
|
+
atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
|
|
24
|
+
console.log(`Installed Pi extension: ${target}`);
|
|
25
|
+
}
|
|
26
|
+
export async function uninstallPiExtension() {
|
|
27
|
+
const target = join(piExtensionsDir(), "cpac.ts");
|
|
28
|
+
if (!existsSync(target))
|
|
29
|
+
throw new CPACError("Pi extension is not installed");
|
|
30
|
+
unlinkSync(target);
|
|
31
|
+
console.log(`Removed Pi extension: ${target}`);
|
|
32
|
+
}
|
package/dist/util.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, parse } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
@@ -9,6 +9,14 @@ export class CPACError extends Error {
|
|
|
9
9
|
export function objectValue(value) {
|
|
10
10
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
11
|
}
|
|
12
|
+
export function ensureCpacBackup(target) {
|
|
13
|
+
if (!existsSync(target))
|
|
14
|
+
return;
|
|
15
|
+
const backup = `${target}.cpac-backup`;
|
|
16
|
+
if (existsSync(backup))
|
|
17
|
+
return;
|
|
18
|
+
copyFileSync(target, backup);
|
|
19
|
+
}
|
|
12
20
|
export function expandUserPath(value) {
|
|
13
21
|
if (value === "~")
|
|
14
22
|
return homedir();
|