@yhong91/cpac 0.1.25 → 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 +445 -0
- package/dist/config.js +440 -0
- package/dist/cpac.js +105 -2441
- package/dist/proxy.js +341 -0
- package/dist/targets/claude.js +373 -0
- package/dist/targets/codex.js +378 -0
- 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 +200 -0
- package/package.json +2 -3
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
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fetchCatalog, pickSpawnModels, proxyFingerprint, readState, saveSpawnModels, stateBytes, stateProxy, } from "./config.js";
|
|
7
|
+
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "./proxy.js";
|
|
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";
|
|
13
|
+
export async function runCodexConfig(config, configPath, options = {}) {
|
|
14
|
+
if (options.reset) {
|
|
15
|
+
let raw = {};
|
|
16
|
+
try {
|
|
17
|
+
const file = JSON.parse(readFileSync(configPath, "utf8"));
|
|
18
|
+
if (!objectValue(file))
|
|
19
|
+
throw new CPACError("config must be a JSON object");
|
|
20
|
+
raw = file;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== "ENOENT") {
|
|
24
|
+
if (error instanceof CPACError)
|
|
25
|
+
throw error;
|
|
26
|
+
throw new CPACError(`cannot read config: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
delete raw.spawn_models;
|
|
30
|
+
atomicWrite(configPath, Buffer.from(`${JSON.stringify(raw, null, 2)}\n`));
|
|
31
|
+
await runRestore(config, ["codex"], { all: false, dryRun: false });
|
|
32
|
+
console.log("Codex configuration reset to default");
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (options.interactive && (!process.stdin.isTTY || !process.stderr.isTTY)) {
|
|
36
|
+
console.log(config.spawn_models
|
|
37
|
+
? JSON.stringify(config.spawn_models, null, 2)
|
|
38
|
+
: "spawn_models not set");
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
let spawnModels = config.spawn_models;
|
|
42
|
+
let v2Off = options.v2Off ?? false;
|
|
43
|
+
let maxContext = options.maxContext ?? false;
|
|
44
|
+
if (options.interactive || options.v2Models) {
|
|
45
|
+
const picked = await pickSpawnModels(config);
|
|
46
|
+
if (picked.length > 0) {
|
|
47
|
+
spawnModels = picked;
|
|
48
|
+
saveSpawnModels(configPath, picked);
|
|
49
|
+
console.log(`spawn_models saved to ${configPath}: ${picked.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
if (options.interactive) {
|
|
52
|
+
const [maxCtxChoice] = await checkboxPicker("Codex Context Window Size:", [
|
|
53
|
+
"Standard (default input budget, e.g. 200K~272K)",
|
|
54
|
+
"Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)",
|
|
55
|
+
], 1);
|
|
56
|
+
maxContext = maxCtxChoice?.startsWith("Max Context") ?? false;
|
|
57
|
+
const [v2Choice] = await checkboxPicker("Codex Multi-Agent V2 mode:", ["Enabled (Default)", "Disabled (--v2_off)"], 1);
|
|
58
|
+
v2Off = v2Choice === "Disabled (--v2_off)";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const updatedConfig = { ...config, spawn_models: spawnModels };
|
|
62
|
+
await runInstall(updatedConfig, ["codex"], {
|
|
63
|
+
all: false,
|
|
64
|
+
dryRun: false,
|
|
65
|
+
force: true,
|
|
66
|
+
v2Off,
|
|
67
|
+
maxContext,
|
|
68
|
+
});
|
|
69
|
+
console.log("Codex configuration updated and synced.");
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
export async function runTargetLauncher(config, targetId, args, executable = targetId, options = {}) {
|
|
73
|
+
const target = TARGETS.find((t) => t.id === targetId);
|
|
74
|
+
if (!target)
|
|
75
|
+
throw new CPACError(`unknown target: ${targetId}`);
|
|
76
|
+
if (["kimi", "grok"].includes(targetId)) {
|
|
77
|
+
const state = readState(config.state_dir);
|
|
78
|
+
const proxy = state ? stateProxy(state) : null;
|
|
79
|
+
if (!proxy || !(await proxyIsHealthy(proxy))) {
|
|
80
|
+
if (proxy)
|
|
81
|
+
await stopProxyProcess(proxy);
|
|
82
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
83
|
+
const started = await startProxyProcess(config, apiKey);
|
|
84
|
+
try {
|
|
85
|
+
const fingerprint = proxyFingerprint(config, apiKey);
|
|
86
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
87
|
+
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
88
|
+
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
89
|
+
atomicWrite(catalogPath, catalog.bytes);
|
|
90
|
+
const originalMode = existsSync(config.codex_config)
|
|
91
|
+
? statSync(config.codex_config).mode & 0o7777
|
|
92
|
+
: 0o600;
|
|
93
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
await stopProxyProcess(started);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
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
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
await target.install(config, false, options.v2Off, options.maxContext);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
120
|
+
const child = spawn(executable, args, { stdio: "inherit" });
|
|
121
|
+
child.once("error", (error) => {
|
|
122
|
+
rejectPromise(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
123
|
+
? `${executable} not found`
|
|
124
|
+
: `cannot start ${executable}: ${error.message}`));
|
|
125
|
+
});
|
|
126
|
+
child.once("close", (code) => resolvePromise(code ?? 1));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function readPackageVersion() {
|
|
130
|
+
try {
|
|
131
|
+
const raw = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8");
|
|
132
|
+
const version = JSON.parse(raw).version;
|
|
133
|
+
return typeof version === "string" ? version : "0.0.0-dev";
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return "0.0.0-dev";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export const CPAC_VERSION = readPackageVersion();
|
|
140
|
+
export function detectClientVersion(binary) {
|
|
141
|
+
if (!/^[\w.-]+$/.test(binary))
|
|
142
|
+
return undefined;
|
|
143
|
+
try {
|
|
144
|
+
const res = spawnSync(binary, ["--version"], {
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
timeout: 2000,
|
|
147
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
148
|
+
});
|
|
149
|
+
if (res.status === 0 && typeof res.stdout === "string") {
|
|
150
|
+
const match = res.stdout.match(/v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
|
|
151
|
+
return match ? match[1] : res.stdout.trim().split(/\s+/)[0] || undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// ignored
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function binaryOnPath(name) {
|
|
160
|
+
if (!/^[\w.-]+$/.test(name))
|
|
161
|
+
return false;
|
|
162
|
+
try {
|
|
163
|
+
return (spawnSync(`command -v ${name}`, { stdio: "ignore", shell: true }).status ===
|
|
164
|
+
0);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const TARGETS = [
|
|
171
|
+
{
|
|
172
|
+
id: "codex",
|
|
173
|
+
home: () => expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex")),
|
|
174
|
+
detected: function () {
|
|
175
|
+
return existsSync(this.home());
|
|
176
|
+
},
|
|
177
|
+
installed: (config) => {
|
|
178
|
+
try {
|
|
179
|
+
return readFileSync(config.codex_config, "utf8").includes(MANAGED_MARKER);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
version: () => detectClientVersion("codex"),
|
|
186
|
+
install: async (config, dryRun, v2Off, maxContext) => {
|
|
187
|
+
if (dryRun) {
|
|
188
|
+
console.log("would inject CPA catalog and loopback proxy into Codex");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await inject(config, v2Off, maxContext);
|
|
192
|
+
},
|
|
193
|
+
uninstall: async (config, dryRun) => {
|
|
194
|
+
if (dryRun) {
|
|
195
|
+
console.log("would restore the original Codex config");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await restore(config);
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "pi",
|
|
203
|
+
home: () => piExtensionsDir(),
|
|
204
|
+
detected: () => binaryOnPath("pi") || existsSync(piExtensionsDir()),
|
|
205
|
+
installed: () => isPiExtensionInstalled(),
|
|
206
|
+
version: () => detectClientVersion("pi"),
|
|
207
|
+
install: async (config, dryRun) => {
|
|
208
|
+
if (dryRun) {
|
|
209
|
+
console.log(`would write Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await installPiExtension(config);
|
|
213
|
+
},
|
|
214
|
+
uninstall: async (_config, dryRun) => {
|
|
215
|
+
if (dryRun) {
|
|
216
|
+
console.log(`would remove Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await uninstallPiExtension();
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "kimi",
|
|
224
|
+
home: () => dirname(kimiConfigPath()),
|
|
225
|
+
detected: () => binaryOnPath("kimi") || existsSync(dirname(kimiConfigPath())),
|
|
226
|
+
installed: () => isKimiConfigInstalled(),
|
|
227
|
+
version: () => detectClientVersion("kimi"),
|
|
228
|
+
install: async (config, dryRun) => {
|
|
229
|
+
if (dryRun) {
|
|
230
|
+
console.log(`would write CPA provider block: ${kimiConfigPath()}`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await installKimiConfig(config);
|
|
234
|
+
},
|
|
235
|
+
uninstall: async (_config, dryRun) => {
|
|
236
|
+
if (dryRun) {
|
|
237
|
+
console.log(`would remove CPA provider block: ${kimiConfigPath()}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await uninstallKimiConfig();
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: "grok",
|
|
245
|
+
home: () => grokHome(),
|
|
246
|
+
detected: () => binaryOnPath("grok") || existsSync(grokHome()),
|
|
247
|
+
installed: () => isGrokConfigInstalled(),
|
|
248
|
+
version: () => detectClientVersion("grok"),
|
|
249
|
+
install: async (config, dryRun) => {
|
|
250
|
+
if (dryRun) {
|
|
251
|
+
console.log(`would write CPA provider block: ${grokConfigPath()}`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await installGrokConfig(config);
|
|
255
|
+
},
|
|
256
|
+
uninstall: async (_config, dryRun) => {
|
|
257
|
+
if (dryRun) {
|
|
258
|
+
console.log(`would remove CPA provider block: ${grokConfigPath()}`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await uninstallGrokConfig();
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
export async function detectTargets(config) {
|
|
266
|
+
return TARGETS.map((target) => ({
|
|
267
|
+
id: target.id,
|
|
268
|
+
detected: target.detected(),
|
|
269
|
+
installed: target.installed(config),
|
|
270
|
+
path: target.home(),
|
|
271
|
+
version: target.version?.(),
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
export async function runDetect(config, asJson) {
|
|
275
|
+
const targets = await detectTargets(config);
|
|
276
|
+
if (asJson) {
|
|
277
|
+
console.log(JSON.stringify({ version: CPAC_VERSION, targets }, null, 2));
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
for (const target of targets) {
|
|
281
|
+
console.log(`${target.id.padEnd(8)} ${(target.detected ? "detected" : "missing").padEnd(9)} ${(target.installed ? "installed" : "not installed").padEnd(14)} ${target.path}`);
|
|
282
|
+
}
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
function selectTargets(requested, all, eligible) {
|
|
286
|
+
const unknown = requested.filter((id) => !TARGETS.some((target) => target.id === id));
|
|
287
|
+
if (unknown.length > 0) {
|
|
288
|
+
throw new CPACError(`unknown target(s): ${unknown.join(", ")}; use ${TARGETS.map((target) => target.id).join(",")}`);
|
|
289
|
+
}
|
|
290
|
+
const ids = requested.length > 0
|
|
291
|
+
? requested
|
|
292
|
+
: all
|
|
293
|
+
? TARGETS.map((target) => target.id)
|
|
294
|
+
: eligible;
|
|
295
|
+
return TARGETS.filter((target) => ids.includes(target.id));
|
|
296
|
+
}
|
|
297
|
+
export async function runInstall(config, requested, options) {
|
|
298
|
+
const eligible = TARGETS.filter((target) => target.detected()).map((target) => target.id);
|
|
299
|
+
const selected = selectTargets(requested, options.all, eligible);
|
|
300
|
+
if (selected.length === 0) {
|
|
301
|
+
throw new CPACError(`no supported targets detected; use --target ${TARGETS.map((target) => target.id).join(",")} or --all`);
|
|
302
|
+
}
|
|
303
|
+
// Catalog context lifting is Codex-specific; refuse to silently skip it on
|
|
304
|
+
// other targets so the flag never gains cross-agent meaning.
|
|
305
|
+
if (options.maxContext && selected.some((target) => target.id !== "codex")) {
|
|
306
|
+
throw new CPACError("--max_context is only valid with --target codex");
|
|
307
|
+
}
|
|
308
|
+
for (const target of selected) {
|
|
309
|
+
if (target.installed(config) && !options.force) {
|
|
310
|
+
console.log(`${target.id}: already installed; use --force to reinstall`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
await target.install(config, options.dryRun, options.v2Off, options.maxContext);
|
|
314
|
+
}
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
export async function runSync(config, requested, options) {
|
|
318
|
+
const installed = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
319
|
+
if (requested.length === 0 && !options.all) {
|
|
320
|
+
if (installed.length === 0) {
|
|
321
|
+
throw new CPACError("no installed CPAC integrations found; run cpac install first");
|
|
322
|
+
}
|
|
323
|
+
return runInstall(config, installed, {
|
|
324
|
+
all: false,
|
|
325
|
+
dryRun: options.dryRun,
|
|
326
|
+
force: true,
|
|
327
|
+
v2Off: options.v2Off,
|
|
328
|
+
maxContext: options.maxContext,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return runInstall(config, requested, {
|
|
332
|
+
all: options.all,
|
|
333
|
+
dryRun: options.dryRun,
|
|
334
|
+
force: true,
|
|
335
|
+
v2Off: options.v2Off,
|
|
336
|
+
maxContext: options.maxContext,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
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
|
+
}
|
|
343
|
+
const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
344
|
+
const selected = selectTargets(requested, options.all, eligible);
|
|
345
|
+
for (const target of selected) {
|
|
346
|
+
if (!target.installed(config)) {
|
|
347
|
+
if (requested.length > 0)
|
|
348
|
+
console.log(`${target.id}: not installed`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
await target.uninstall(config, options.dryRun);
|
|
352
|
+
}
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
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;
|
|
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 });
|
|
372
|
+
if (options.dryRun) {
|
|
373
|
+
console.log("would uninstall @yhong91/cpac");
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
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
|
+
}
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
function compareVersions(a, b) {
|
|
387
|
+
const left = a.split(".").map(Number);
|
|
388
|
+
const right = b.split(".").map(Number);
|
|
389
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
390
|
+
const l = left[index] ?? 0;
|
|
391
|
+
const r = right[index] ?? 0;
|
|
392
|
+
if (l !== r)
|
|
393
|
+
return l > r ? 1 : -1;
|
|
394
|
+
}
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
export async function runUpgrade(config, checkOnly) {
|
|
398
|
+
console.log(`Current version: ${CPAC_VERSION}`);
|
|
399
|
+
let latest;
|
|
400
|
+
try {
|
|
401
|
+
const response = await fetch("https://registry.npmjs.org/@yhong91/cpac/latest", {
|
|
402
|
+
headers: { Accept: "application/json" },
|
|
403
|
+
signal: AbortSignal.timeout(10_000),
|
|
404
|
+
});
|
|
405
|
+
if (response.ok) {
|
|
406
|
+
const data = (await response.json());
|
|
407
|
+
if (typeof data.version === "string")
|
|
408
|
+
latest = data.version;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
latest = undefined;
|
|
413
|
+
}
|
|
414
|
+
if (!latest) {
|
|
415
|
+
console.error("Could not fetch latest version from npm. Check your network connection.");
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
console.log(`Latest version: ${latest}`);
|
|
419
|
+
const upToDate = compareVersions(CPAC_VERSION, latest) >= 0;
|
|
420
|
+
if (checkOnly) {
|
|
421
|
+
if (upToDate)
|
|
422
|
+
console.log("Already up to date.");
|
|
423
|
+
else
|
|
424
|
+
console.log("Update available. Run: cpac upgrade");
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
if (upToDate) {
|
|
428
|
+
console.log("Already up to date.");
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.log(`Installing @yhong91/cpac@${latest}`);
|
|
432
|
+
const result = spawnSync("npm", ["install", "-g", `@yhong91/cpac@${latest}`], { stdio: "inherit" });
|
|
433
|
+
if (result.status !== 0) {
|
|
434
|
+
console.error("npm install failed");
|
|
435
|
+
return result.status ?? 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const installed = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
439
|
+
if (installed.length === 0) {
|
|
440
|
+
console.log("No installed agents to sync.");
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
console.log(`Syncing ${installed.join(", ")}`);
|
|
444
|
+
return runSync(config, [], { all: false, dryRun: false });
|
|
445
|
+
}
|