@yhong91/cpac 0.1.48 → 0.2.0
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 +45 -16
- package/dist/agents.js +26 -1
- package/dist/config.js +11 -1
- package/dist/cpac.js +19 -7
- package/dist/pricing.js +202 -20
- package/dist/proxy.js +1 -0
- package/dist/targets/claude.js +1 -1
- package/dist/targets/cline.js +197 -0
- package/dist/targets/pi.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ CPAC 将 Codex 和 Claude Code 接入远端 CLIProxyAPI(CPA):
|
|
|
10
10
|
## 要求
|
|
11
11
|
|
|
12
12
|
- [Node.js](https://nodejs.org/) 20 或更新版本(npm 随 Node 安装)
|
|
13
|
-
- 已安装需要使用的 agent:`codex`、`claude`、`pi`、`kimi`、`grok`、`zed` 或 `
|
|
13
|
+
- 已安装需要使用的 agent:`codex`、`claude`、`pi`、`kimi`、`grok`、`zed`、`hermes` 或 `cline`
|
|
14
14
|
- 有权访问的 CPA API key
|
|
15
15
|
|
|
16
16
|
## 安装
|
|
@@ -37,7 +37,7 @@ echo 'export CPA_API_KEY="<你的key>"' >> ~/.zshrc && source ~/.zshrc
|
|
|
37
37
|
cpac status
|
|
38
38
|
|
|
39
39
|
# 4a. 持久注入(长期生效,写入 agent 配置,可完整恢复)
|
|
40
|
-
cpac install --all # 注入所有检测到的目标(codex/pi/kimi/grok/zed/hermes)
|
|
40
|
+
cpac install --all # 注入所有检测到的目标(codex/pi/kimi/grok/zed/hermes/cline)
|
|
41
41
|
cpac install codex # 只注入 Codex(默认开启 multi-agent v2)
|
|
42
42
|
cpac install codex --v2_models # 先多选 spawn_agent 模型再注入
|
|
43
43
|
cpac install codex --max_context # 注入时把模型上下文提升到实测上限(opt-in,仅限 codex)
|
|
@@ -61,7 +61,7 @@ cpac upgrade
|
|
|
61
61
|
|
|
62
62
|
本节面向 AI agent:按顺序执行即可完成安装与配置,**全程无交互**。注意:`cpac codex --config` 和 `cpac claude --config` 的交互向导是 TTY-only,agent 必须走下方直接写配置 JSON 的路径。
|
|
63
63
|
|
|
64
|
-
1. **检查前置**:`node --version` ≥ 20;确认用户需要哪些 agent(`codex`/`claude`/`pi`/`kimi`/`grok`/`zed`/`hermes` 在 PATH 上)。向用户索取 CPA API key。
|
|
64
|
+
1. **检查前置**:`node --version` ≥ 20;确认用户需要哪些 agent(`codex`/`claude`/`pi`/`kimi`/`grok`/`zed`/`hermes`/`cline` 在 PATH 上)。向用户索取 CPA API key。
|
|
65
65
|
|
|
66
66
|
2. **安装与密钥**:
|
|
67
67
|
|
|
@@ -90,14 +90,14 @@ cpac status # 验证:各 target 显示 ready/injected
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
1. **统一启动与配置**:
|
|
93
|
-
- 启动:`cpac <codex|claude|kimi|grok|pi|zed|hermes> [args...]`(所有原生参数如 `-c` 等原样透明传递)
|
|
93
|
+
- 启动:`cpac <codex|claude|kimi|grok|pi|zed|hermes|cline> [args...]`(所有原生参数如 `-c` 等原样透明传递)
|
|
94
94
|
- `setup` / `--setup`:配置 Codex/Claude,然后启动
|
|
95
95
|
- `clear`:还原原生配置
|
|
96
96
|
- 只写配置、不启动:`cpac setup [claude|codex]`;查看当前:`cpac setup`
|
|
97
97
|
|
|
98
98
|
2. **已安装后升级**:npm 发布新版本后,等 publish 成功再执行 `cpac upgrade`。它会按需 `npm install -g`,再对已接入的 Codex / Pi / Kimi / Grok / Zed / Hermes 做 `sync`。不要拆成 `npm install -g @yhong91/cpac` 再手动 `cpac sync`。只刷新配置、不升 CLI 时才用 `cpac sync`。
|
|
99
99
|
|
|
100
|
-
失败处理:任何一步报错时先跑 `cpac status`;密钥未配置回到步骤 2;注入后异常用 `cpac <agent> clear` 或 `cpac restore <codex|pi|kimi|grok|zed|hermes>` 恢复原配置。
|
|
100
|
+
失败处理:任何一步报错时先跑 `cpac status`;密钥未配置回到步骤 2;注入后异常用 `cpac <agent> clear` 或 `cpac restore <codex|pi|kimi|grok|zed|hermes|cline>` 恢复原配置。
|
|
101
101
|
|
|
102
102
|
## 首次引导
|
|
103
103
|
|
|
@@ -114,7 +114,7 @@ Commands:
|
|
|
114
114
|
cpac setup Show current Codex/Claude setup
|
|
115
115
|
cpac status Show agent support status
|
|
116
116
|
cpac models List remote CPA catalog models
|
|
117
|
-
cpac restore <codex|pi|kimi|grok|zed|hermes> | --all Detach injected agents
|
|
117
|
+
cpac restore <codex|pi|kimi|grok|zed|hermes|cline> | --all Detach injected agents
|
|
118
118
|
cpac --help Show command usage
|
|
119
119
|
```
|
|
120
120
|
|
|
@@ -162,14 +162,14 @@ cpac --help
|
|
|
162
162
|
|
|
163
163
|
### 统一管理(detect / install / sync / restore / uninstall / upgrade)
|
|
164
164
|
|
|
165
|
-
参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok`、`zed`、`hermes`
|
|
165
|
+
参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok`、`zed`、`hermes`、`cline` 七个持久注入目标统一管理(claude 是临时接管,用 `cpac claude` 启动,不属于 install 目标):
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
168
|
cpac detect [--json]
|
|
169
169
|
cpac models [--json]
|
|
170
|
-
cpac install [codex|pi|kimi|grok|zed|hermes...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]
|
|
171
|
-
cpac sync [codex|pi|kimi|grok|zed|hermes...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]
|
|
172
|
-
cpac restore <codex|pi|kimi|grok|zed|hermes...> | --all [--dry-run]
|
|
170
|
+
cpac install [codex|pi|kimi|grok|zed|hermes|cline...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]
|
|
171
|
+
cpac sync [codex|pi|kimi|grok|zed|hermes|cline...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]
|
|
172
|
+
cpac restore <codex|pi|kimi|grok|zed|hermes|cline...> | --all [--dry-run]
|
|
173
173
|
cpac uninstall [--dry-run]
|
|
174
174
|
cpac upgrade [--check]
|
|
175
175
|
cpac -v | --version
|
|
@@ -177,13 +177,13 @@ cpac -v | --version
|
|
|
177
177
|
|
|
178
178
|
- `models` 从远端 CPA 拉取 rich catalog,默认一行一个 slug;`--json` 输出 `cpa_url` 和模型字段(`slug` / `display_name` / `context_window`)。用来核对 catalog 里有没有某个模型(例如 `*-fast`),不写本地配置。
|
|
179
179
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
180
|
-
- `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok`/`zed`/`hermes` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
181
|
-
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi / Kimi / Grok / Zed / Hermes 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `cpac install codex` 有效。
|
|
180
|
+
- `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok`/`zed`/`hermes`/`cline` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
181
|
+
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi / Kimi / Grok / Zed / Hermes / Cline 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `cpac install codex` 有效。
|
|
182
182
|
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`,catalog 行打 `multi_agent_version: "v2"`);`install codex --v2_off` 关掉 toml 并去掉 catalog 戳。v2 开着时会剥掉 `[agents] max_threads`(Codex 否则拒启动)。`cpac status` 对照 config 里的 `v2_off` / `max_context` 和 live toml / catalog。服务端还需在 CPA 配置中开启 `codex.optimize-multi-agent-v2`。
|
|
183
183
|
- `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
|
|
184
184
|
- `restore` 必须带 agent 名或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
|
|
185
185
|
- `uninstall` 先用和 `cpac setup codex` 相同的方向键选择器确认(默认 Cancel),再 `restore --all`,最后 `npm uninstall -g @yhong91/cpac`。不要跟 agent 名;卸载的是 cpac 包本身。非终端取消。`--dry-run` 跳过确认。
|
|
186
|
-
- `upgrade` 是发布后更新本机的一次性入口:查 npm → 需要时 `npm install -g` → 对**已接入**的 Codex / Pi / Kimi / Grok / Zed / Hermes 执行 `sync`。已是最新也会 sync。不要先手动 `npm install -g` 再 `cpac sync`。`--check` 只报告版本,不安装也不 sync。没有已安装目标时跳过 sync,不报错。只刷新配置、不升 CLI 时用 `cpac sync`。
|
|
186
|
+
- `upgrade` 是发布后更新本机的一次性入口:查 npm → 需要时 `npm install -g` → 对**已接入**的 Codex / Pi / Kimi / Grok / Zed / Hermes / Cline 执行 `sync`。已是最新也会 sync。不要先手动 `npm install -g` 再 `cpac sync`。`--check` 只报告版本,不安装也不 sync。没有已安装目标时跳过 sync,不报错。只刷新配置、不升 CLI 时用 `cpac sync`。
|
|
187
187
|
|
|
188
188
|
### Claude Code
|
|
189
189
|
|
|
@@ -253,9 +253,9 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。注入会删除 `models
|
|
|
253
253
|
|
|
254
254
|
注入命令可选 `--max_context`(`cpac install codex --max_context`,默认不带):把 catalog 中 `max_context_window` 高于 `context_window` 的模型提升到上限,并把 `auto_compact_token_limit` 设为上限的 90%。Codex 把 `context_window` 当输入预算而非展示标签,上游默认保留保守运营值(如 GPT-5.6 家族 272k),带上该参数后可用到实测上限(约 921k,opencodex 实测);90% 压缩线确保在硬上限前触发 auto-compact。不带参数注入的仍是原本上下文的目录。该参数仅限 codex,与其他 target 组合使用会直接报错。
|
|
255
255
|
|
|
256
|
-
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok/zed/hermes/pi 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。所有 target 的 restore 都按 Codex 规则:注入后记下文件 hash;相对注入未改则逐字节还原备份(没有备份则删除我们创建的文件);注入后改过则只剥 CPAC 字段,用户改动留下。没有其它需要代理的 agent 时才关闭代理。
|
|
256
|
+
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok/zed/hermes/pi/cline 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。所有 target 的 restore 都按 Codex 规则:注入后记下文件 hash;相对注入未改则逐字节还原备份(没有备份则删除我们创建的文件);注入后改过则只剥 CPAC 字段,用户改动留下。没有其它需要代理的 agent 时才关闭代理。
|
|
257
257
|
|
|
258
|
-
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac pi` / `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` 即可恢复。
|
|
258
|
+
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac pi` / `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` / `cpac cline` 即可恢复。
|
|
259
259
|
|
|
260
260
|
默认路径:
|
|
261
261
|
|
|
@@ -268,6 +268,7 @@ CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录
|
|
|
268
268
|
Grok ~/.cpac/grok/config.toml
|
|
269
269
|
Zed ~/.cpac/zed/settings.json
|
|
270
270
|
Hermes ~/.cpac/hermes/config.yaml
|
|
271
|
+
Cline ~/.cpac/cline/providers.json
|
|
271
272
|
Pi ~/.cpac/pi/models.json
|
|
272
273
|
restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
|
|
273
274
|
```
|
|
@@ -281,7 +282,7 @@ cpac install pi
|
|
|
281
282
|
cpac pi
|
|
282
283
|
```
|
|
283
284
|
|
|
284
|
-
它在 `~/.pi/agent/models.json` 的 `providers.cpac` 写入 loopback `baseUrl`、占位 `apiKey`、`api: openai-responses`,以及 CPA catalog 的模型列表。每个模型的 `cost` 是 USD / 百万 token(input / output / cacheRead / cacheWrite
|
|
285
|
+
它在 `~/.pi/agent/models.json` 的 `providers.cpac` 写入 loopback `baseUrl`、占位 `apiKey`、`api: openai-responses`,以及 CPA catalog 的模型列表。每个模型的 `cost` 是 USD / 百万 token(input / output / cacheRead / cacheWrite)。注入时若 `~/.cpac/model-prices.json` 超过 24 小时会从 [models.dev](https://models.dev) 刷新(官方价优先,否则 OpenRouter;失败则用打包快照);未知模型为 0。不改当前默认模型;切换用 `/model`。密钥由 loopback 注入。旧的 `npm:@yhong91/cpac` package 和 `extensions/pi-cpac.ts` 会在 install / restore 时删掉。
|
|
285
286
|
|
|
286
287
|
卸载:`cpac restore pi` 或 `cpac pi clear`。尊重 `PI_CODING_AGENT_DIR`。
|
|
287
288
|
|
|
@@ -335,6 +336,33 @@ cpac restore hermes
|
|
|
335
336
|
|
|
336
337
|
配置目录尊重 `HERMES_HOME`(Windows 默认 `%LOCALAPPDATA%\hermes`)。首次写入前若 default `config.yaml` 已存在,会在 `state_dir/hermes/config.yaml` 保留原始快照。profile 文件不单独备份,restore 只剥 `providers.cpac`。
|
|
337
338
|
|
|
339
|
+
### Cline(CLI 与 Desktop)
|
|
340
|
+
|
|
341
|
+
写入 CPA 的 OpenAI-compatible provider 到 Cline:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
cpac install cline
|
|
345
|
+
cpac cline # 未显式传 -P/--provider 时自动注入 -P openai-compatible
|
|
346
|
+
cpac cline "帮我写一个测试" # 原生参数原样直通
|
|
347
|
+
cpac cline -P cline "..." # 显式指定 provider 时不注入
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Cline CLI 与 Cline Desktop 共享同一份 `~/.cline/data/settings/providers.json`(并共用同一本地 hub)。CPAC 在 `providers["openai-compatible"]` 槽位写入 loopback `baseUrl`、占位 `apiKey`(`cpac`)和默认模型——这是 Cline 唯一接受自定义 OpenAI 兼容端点的 provider 槽位(Cline 拒绝未知 provider id),因此一次注入对 CLI 和 Desktop 同时生效。两者的请求都是 `POST {baseUrl}/chat/completions`,loopback 代理会把入站 Authorization 换成真实 `CPA_API_KEY`。
|
|
351
|
+
|
|
352
|
+
- 模型:首次写入 catalog 第一个 slug;之后你在 Cline 里改过模型,`cpac sync cline` 会保留你的选择(只要该 slug 仍在目录里)。切换模型用 CLI 的 `-m <model>` 或 Desktop/IDE 的模型选择器。
|
|
353
|
+
- provider 选择:`cpac cline` 默认注入 `-P openai-compatible`,避免走 Cline 官方托管 provider;Desktop 没有 CLI 参数,需要在 provider 列表里手动选 "OpenAI Compatible"。
|
|
354
|
+
- 槽位冲突:如果该槽位已配置为其它端点,`cpac install cline` 会拒绝;用 `cpac install cline --force` 覆盖(原文件备份在 `state_dir/cline/providers.json`)。
|
|
355
|
+
- 并发与恢复:Cline(尤其是常驻的 Desktop)会随时重写 `providers.json`。`restore` 按「注入后未改 → 逐字节还原备份;改过 → 只剥 CPAC 槽位、其余(包括你在 Cline 里改的 provider)留下」处理。Cline 侧改掉该槽位的 apiKey/baseUrl 后,注入即视为失效,重跑 `cpac install cline` / `cpac sync cline` 恢复。
|
|
356
|
+
- Desktop 从 GUI 启动不继承 shell 环境变量,`CLINE_DATA_DIR` 覆盖只影响 CLI;两侧使用默认路径时注入对两边同时生效。
|
|
357
|
+
|
|
358
|
+
卸载:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
cpac restore cline
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
配置目录尊重 `CLINE_DATA_DIR`(否则 `~/.cline/data`,CLI 与 Desktop 共享)。首次写入前若 `providers.json` 已存在,会在 `state_dir/cline/providers.json` 保留原始快照。
|
|
365
|
+
|
|
338
366
|
## 环境变量
|
|
339
367
|
|
|
340
368
|
| 变量 | 默认值 | 作用 |
|
|
@@ -345,6 +373,7 @@ cpac restore hermes
|
|
|
345
373
|
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
|
|
346
374
|
| `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
|
|
347
375
|
| `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
|
|
376
|
+
| `CLINE_DATA_DIR` | `~/.cline/data` | 覆盖 Cline 数据目录(影响 `cpac install cline` 写入位置;仅 CLI 生效,Desktop 读默认路径) |
|
|
348
377
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
349
378
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
350
379
|
|
package/dist/agents.js
CHANGED
|
@@ -10,6 +10,7 @@ import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimi
|
|
|
10
10
|
import { installPiConfig, isPiConfigInstalled, piAgentDir, piModelsPath, uninstallPiConfig, } from "./targets/pi.js";
|
|
11
11
|
import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
|
|
12
12
|
import { hermesConfigPaths, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
|
|
13
|
+
import { CLINE_PROVIDER_ID, clineDataDir, clineProvidersPath, installClineConfig, isClineConfigInstalled, uninstallClineConfig, } from "./targets/cline.js";
|
|
13
14
|
import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, tabWizard, } from "./util.js";
|
|
14
15
|
const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
|
|
15
16
|
const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
|
|
@@ -325,6 +326,30 @@ const TARGETS = [
|
|
|
325
326
|
await uninstallHermesConfig(config);
|
|
326
327
|
},
|
|
327
328
|
},
|
|
329
|
+
{
|
|
330
|
+
id: "cline",
|
|
331
|
+
home: () => clineDataDir(),
|
|
332
|
+
detected: () => binaryOnPath("cline") ||
|
|
333
|
+
existsSync(join(homedir(), ".cline")) ||
|
|
334
|
+
(process.platform === "darwin" &&
|
|
335
|
+
existsSync("/Applications/Cline.app")),
|
|
336
|
+
installed: () => isClineConfigInstalled(),
|
|
337
|
+
version: () => detectClientVersion("cline"),
|
|
338
|
+
install: async (config, dryRun, _v2Off, _maxContext, force) => {
|
|
339
|
+
if (dryRun) {
|
|
340
|
+
console.log(`would write Cline ${CLINE_PROVIDER_ID} provider: ${clineProvidersPath()}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await installClineConfig(config, force);
|
|
344
|
+
},
|
|
345
|
+
uninstall: async (config, dryRun) => {
|
|
346
|
+
if (dryRun) {
|
|
347
|
+
console.log(`would remove Cline ${CLINE_PROVIDER_ID} provider: ${clineProvidersPath()}`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
await uninstallClineConfig(config);
|
|
351
|
+
},
|
|
352
|
+
},
|
|
328
353
|
];
|
|
329
354
|
export async function detectTargets(config) {
|
|
330
355
|
return TARGETS.map((target) => ({
|
|
@@ -377,7 +402,7 @@ export async function runInstall(config, requested, options) {
|
|
|
377
402
|
console.log(`${target.id}: already installed; use --force to reinstall`);
|
|
378
403
|
continue;
|
|
379
404
|
}
|
|
380
|
-
await target.install(config, options.dryRun, options.v2Off, options.maxContext);
|
|
405
|
+
await target.install(config, options.dryRun, options.v2Off, options.maxContext, options.force);
|
|
381
406
|
}
|
|
382
407
|
return 0;
|
|
383
408
|
}
|
package/dist/config.js
CHANGED
|
@@ -22,7 +22,15 @@ const CONFIG_KEYS = new Set([
|
|
|
22
22
|
// spawn_agent overrides (codex-rs MAX_SPAWN_AGENT_MODEL_OVERRIDES).
|
|
23
23
|
export const MAX_SPAWN_MODELS = 5;
|
|
24
24
|
export const CLAUDE_SLOT_KEYS = ["opus", "sonnet", "haiku"];
|
|
25
|
-
export const AGENT_IDS = [
|
|
25
|
+
export const AGENT_IDS = [
|
|
26
|
+
"codex",
|
|
27
|
+
"pi",
|
|
28
|
+
"kimi",
|
|
29
|
+
"grok",
|
|
30
|
+
"zed",
|
|
31
|
+
"hermes",
|
|
32
|
+
"cline",
|
|
33
|
+
];
|
|
26
34
|
const AGENT_ID_SET = new Set(AGENT_IDS);
|
|
27
35
|
const STATE_KEYS = new Set([
|
|
28
36
|
"config_path",
|
|
@@ -742,5 +750,7 @@ export function proxyFingerprint(config, apiKey) {
|
|
|
742
750
|
.update(config.cpa_url)
|
|
743
751
|
.update("\0")
|
|
744
752
|
.update(apiKey)
|
|
753
|
+
.update("\0")
|
|
754
|
+
.update("agent:false")
|
|
745
755
|
.digest("hex");
|
|
746
756
|
}
|
package/dist/cpac.js
CHANGED
|
@@ -11,6 +11,7 @@ import { isKimiConfigInstalled } from "./targets/kimi.js";
|
|
|
11
11
|
import { isPiConfigInstalled } from "./targets/pi.js";
|
|
12
12
|
import { isZedConfigInstalled } from "./targets/zed.js";
|
|
13
13
|
import { isHermesConfigInstalled } from "./targets/hermes.js";
|
|
14
|
+
import { CLINE_PROVIDER_ID, isClineConfigInstalled } from "./targets/cline.js";
|
|
14
15
|
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, } from "./config.js";
|
|
15
16
|
import { proxyIsHealthy, runProxyChild } from "./proxy.js";
|
|
16
17
|
import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
|
|
@@ -23,9 +24,10 @@ export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, ru
|
|
|
23
24
|
export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
24
25
|
export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
25
26
|
export { installPiConfig, isPiConfigInstalled, uninstallPiConfig, PI_PROVIDER_ID, } from "./targets/pi.js";
|
|
26
|
-
export { lookupModelCost } from "./pricing.js";
|
|
27
|
+
export { compactModelPrices, ensureModelPrices, lookupModelCost, resetModelPrices, } from "./pricing.js";
|
|
27
28
|
export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
|
|
28
29
|
export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
|
|
30
|
+
export { CLINE_PROVIDER_ID, installClineConfig, isClineConfigInstalled, uninstallClineConfig, } from "./targets/cline.js";
|
|
29
31
|
export async function status(config) {
|
|
30
32
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
31
33
|
const keyConfigured = !!apiKey;
|
|
@@ -81,6 +83,8 @@ export async function status(config) {
|
|
|
81
83
|
console.log(`zed: ${keyConfigured ? (zedInstalled ? "ready" : "config not installed; run: cpac install zed") : "not configured"}`);
|
|
82
84
|
const hermesInstalled = isHermesConfigInstalled();
|
|
83
85
|
console.log(`hermes: ${keyConfigured ? (hermesInstalled ? "ready" : "config not installed; run: cpac install hermes") : "not configured"}`);
|
|
86
|
+
const clineInstalled = isClineConfigInstalled();
|
|
87
|
+
console.log(`cline: ${keyConfigured ? (clineInstalled ? "ready" : "config not installed; run: cpac install cline") : "not configured"}`);
|
|
84
88
|
if (!state)
|
|
85
89
|
return 1;
|
|
86
90
|
const proxy = stateProxy(state);
|
|
@@ -119,10 +123,10 @@ async function listModels(config, json) {
|
|
|
119
123
|
return 0;
|
|
120
124
|
}
|
|
121
125
|
async function guide(config) {
|
|
122
|
-
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, zed, hermes, claude) through CPA\n cpac setup Show current Codex/Claude setup; cpac setup <codex|claude> to change\n cpac status Show agent support status (Codex, Claude, Pi, Kimi, Grok, Zed, Hermes)\n cpac restore <codex|pi|kimi|grok|zed|hermes> | --all Detach injected agents
|
|
126
|
+
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, zed, hermes, cline, claude) through CPA\n cpac setup Show current Codex/Claude setup; cpac setup <codex|claude> to change\n cpac status Show agent support status (Codex, Claude, Pi, Kimi, Grok, Zed, Hermes, Cline)\n cpac restore <codex|pi|kimi|grok|zed|hermes|cline> | --all Detach injected agents
|
|
123
127
|
cpac detect Show supported targets and install status
|
|
124
|
-
cpac install [codex|pi|kimi|grok|zed|hermes...] Install CPA into detected targets, or named agents
|
|
125
|
-
cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed/Hermes injections from CPA
|
|
128
|
+
cpac install [codex|pi|kimi|grok|zed|hermes|cline...] Install CPA into detected targets, or named agents
|
|
129
|
+
cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed/Hermes/Cline injections from CPA
|
|
126
130
|
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 models List remote CPA catalog models\n cpac --help Show command usage`);
|
|
127
131
|
if (process.env[config.api_key_env]?.trim())
|
|
128
132
|
return 0;
|
|
@@ -162,7 +166,7 @@ function usage() {
|
|
|
162
166
|
cmd("cpac upgrade", "update cpac from npm and sync installed agents"),
|
|
163
167
|
"",
|
|
164
168
|
"Positionals:",
|
|
165
|
-
helpRow("agent", "codex, kimi, grok, pi, zed, hermes, or claude (claude is launch-only)", "", 18),
|
|
169
|
+
helpRow("agent", "codex, kimi, grok, pi, zed, hermes, cline, or claude (claude is launch-only)", "", 18),
|
|
166
170
|
"",
|
|
167
171
|
"Options:",
|
|
168
172
|
opt("-h, --help", "show help", "[boolean]"),
|
|
@@ -301,7 +305,7 @@ function parseArgs(args) {
|
|
|
301
305
|
}
|
|
302
306
|
return parsed;
|
|
303
307
|
}
|
|
304
|
-
if (["codex", "kimi", "grok", "pi", "zed", "hermes", "claude"].includes(args[0])) {
|
|
308
|
+
if (["codex", "kimi", "grok", "pi", "zed", "hermes", "cline", "claude"].includes(args[0])) {
|
|
305
309
|
const targetId = args[0];
|
|
306
310
|
let configPath = defaultConfigPath();
|
|
307
311
|
const raw = args.slice(1);
|
|
@@ -649,7 +653,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
649
653
|
});
|
|
650
654
|
if (parsed.command === "uninstall") {
|
|
651
655
|
if (parsed.targets.length || parsed.all) {
|
|
652
|
-
throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed|hermes> | --all");
|
|
656
|
+
throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed|hermes|cline> | --all");
|
|
653
657
|
}
|
|
654
658
|
return await runUninstall(config, { dryRun: parsed.dryRun });
|
|
655
659
|
}
|
|
@@ -698,6 +702,14 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
698
702
|
}
|
|
699
703
|
return await runTargetLauncher(launcherConfig, "codex", parsed.args, "codex", { v2Off: parsed.v2Off, maxContext: parsed.maxContext });
|
|
700
704
|
}
|
|
705
|
+
if (parsed.command === "cline") {
|
|
706
|
+
// Cline's default provider is its own hosted service; default the
|
|
707
|
+
// launch to the injected CPAC slot unless the user picked one.
|
|
708
|
+
const args = parsed.args.some((arg) => arg === "-P" || arg === "--provider")
|
|
709
|
+
? parsed.args
|
|
710
|
+
: ["-P", CLINE_PROVIDER_ID, ...parsed.args];
|
|
711
|
+
return await runTargetLauncher(config, "cline", args, "cline");
|
|
712
|
+
}
|
|
701
713
|
if (parsed.command === "kimi" ||
|
|
702
714
|
parsed.command === "grok" ||
|
|
703
715
|
parsed.command === "pi" ||
|
package/dist/pricing.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { objectValue } from "./util.js";
|
|
5
|
+
import { atomicWrite, objectValue } from "./util.js";
|
|
5
6
|
const ZERO_COST = {
|
|
6
7
|
input: 0,
|
|
7
8
|
output: 0,
|
|
@@ -9,29 +10,29 @@ const ZERO_COST = {
|
|
|
9
10
|
cacheWrite: 0,
|
|
10
11
|
};
|
|
11
12
|
const VARIANT_SUFFIX = /[-_\s]+(?:minimal|low|medium|high|xhigh|max|ultra|thinking|preview|build|fast|free)$/i;
|
|
13
|
+
const REFRESH_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
const FETCH_MS = 10_000;
|
|
15
|
+
const MODELS_DEV_API = "https://models.dev/api.json";
|
|
16
|
+
const MODELS_DEV_MODELS = "https://models.dev/models.json";
|
|
17
|
+
const MODELS_DEV_PROXY_API = "https://models-proxy.vibetime.cc/api.json";
|
|
18
|
+
const MODELS_DEV_PROXY_MODELS = "https://models-proxy.vibetime.cc/models.json";
|
|
12
19
|
let table;
|
|
13
20
|
function foldToken(value) {
|
|
14
21
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
15
22
|
}
|
|
16
|
-
function
|
|
23
|
+
function bundledPath() {
|
|
17
24
|
return join(dirname(fileURLToPath(import.meta.url)), "data", "model-prices.json");
|
|
18
25
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return table;
|
|
32
|
-
}
|
|
26
|
+
function cachePath() {
|
|
27
|
+
const override = process.env.CPAC_PRICES_CACHE?.trim();
|
|
28
|
+
if (override)
|
|
29
|
+
return override;
|
|
30
|
+
return join(homedir(), ".cpac", "model-prices.json");
|
|
31
|
+
}
|
|
32
|
+
function parseTable(raw) {
|
|
33
|
+
const next = new Map();
|
|
33
34
|
if (!objectValue(raw))
|
|
34
|
-
return
|
|
35
|
+
return next;
|
|
35
36
|
for (const [key, value] of Object.entries(raw)) {
|
|
36
37
|
if (!Array.isArray(value) || value.length < 4)
|
|
37
38
|
continue;
|
|
@@ -42,8 +43,186 @@ function loadTable() {
|
|
|
42
43
|
if (![input, output, cacheRead, cacheWrite].every((n) => Number.isFinite(n) && n >= 0)) {
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
next.set(key, { input, output, cacheRead, cacheWrite });
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
function readTableFile(path) {
|
|
51
|
+
if (!existsSync(path))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
return parseTable(JSON.parse(readFileSync(path, "utf8")));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function loadBundled() {
|
|
61
|
+
return readTableFile(bundledPath()) ?? new Map();
|
|
62
|
+
}
|
|
63
|
+
function tokens(raw) {
|
|
64
|
+
const trimmed = raw.trim();
|
|
65
|
+
if (!trimmed)
|
|
66
|
+
return [];
|
|
67
|
+
const lower = trimmed.toLowerCase();
|
|
68
|
+
const slash = lower.lastIndexOf("/");
|
|
69
|
+
const tail = slash === -1 ? lower : lower.slice(slash + 1);
|
|
70
|
+
const hyphenated = tail.replace(/\s+/g, "-");
|
|
71
|
+
const dotted = hyphenated.replace(/(\d+)-(\d+)/g, "$1.$2");
|
|
72
|
+
const dashed = hyphenated.replace(/(\d+)\.(\d+)/g, "$1-$2");
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const t of [lower, tail, hyphenated, dotted, dashed, foldToken(tail)]) {
|
|
75
|
+
if (t && !out.includes(t))
|
|
76
|
+
out.push(t);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function toCost(cost) {
|
|
81
|
+
if (!objectValue(cost))
|
|
82
|
+
return undefined;
|
|
83
|
+
const input = Number(cost.input);
|
|
84
|
+
const output = Number(cost.output);
|
|
85
|
+
if (!Number.isFinite(input) || !Number.isFinite(output))
|
|
86
|
+
return undefined;
|
|
87
|
+
if (input < 0 || output < 0 || (input === 0 && output === 0))
|
|
88
|
+
return undefined;
|
|
89
|
+
const cacheRead = Number.isFinite(Number(cost.cache_read))
|
|
90
|
+
? Number(cost.cache_read)
|
|
91
|
+
: input;
|
|
92
|
+
const cacheWrite = Number.isFinite(Number(cost.cache_write))
|
|
93
|
+
? Number(cost.cache_write)
|
|
94
|
+
: cacheRead;
|
|
95
|
+
return [input, output, cacheRead, cacheWrite];
|
|
96
|
+
}
|
|
97
|
+
function officialFor(lab, modelId) {
|
|
98
|
+
return foldToken(modelId.split("/").pop() ?? "").startsWith("glm") ? "zai" : lab;
|
|
99
|
+
}
|
|
100
|
+
function pick(index, modelId, lab) {
|
|
101
|
+
const official = lab ? officialFor(lab, modelId) : undefined;
|
|
102
|
+
const cands = tokens(modelId);
|
|
103
|
+
if (official) {
|
|
104
|
+
for (const cand of cands) {
|
|
105
|
+
for (const [p, c] of index.get(cand) ?? []) {
|
|
106
|
+
if (p === official)
|
|
107
|
+
return c;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const cand of cands) {
|
|
112
|
+
for (const [p, c] of index.get(cand) ?? []) {
|
|
113
|
+
if (p === "openrouter")
|
|
114
|
+
return c;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
export function compactModelPrices(api, models) {
|
|
120
|
+
const index = new Map();
|
|
121
|
+
if (objectValue(api)) {
|
|
122
|
+
for (const [pid, prov] of Object.entries(api)) {
|
|
123
|
+
if (!objectValue(prov) || !objectValue(prov.models))
|
|
124
|
+
continue;
|
|
125
|
+
const p = pid.toLowerCase();
|
|
126
|
+
for (const [mid, model] of Object.entries(prov.models)) {
|
|
127
|
+
const c = objectValue(model) ? toCost(model.cost) : undefined;
|
|
128
|
+
if (!c)
|
|
129
|
+
continue;
|
|
130
|
+
for (const tok of tokens(mid)) {
|
|
131
|
+
const bucket = index.get(tok);
|
|
132
|
+
if (bucket)
|
|
133
|
+
bucket.push([p, c]);
|
|
134
|
+
else
|
|
135
|
+
index.set(tok, [[p, c]]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const tableJson = {};
|
|
141
|
+
if (objectValue(models)) {
|
|
142
|
+
for (const key of Object.keys(models)) {
|
|
143
|
+
if (!key.includes("/"))
|
|
144
|
+
continue;
|
|
145
|
+
const lab = key.slice(0, key.indexOf("/")).toLowerCase();
|
|
146
|
+
const tail = key.slice(key.indexOf("/") + 1);
|
|
147
|
+
const cost = pick(index, key, lab);
|
|
148
|
+
const f = foldToken(tail);
|
|
149
|
+
if (cost && f && tableJson[f] === undefined)
|
|
150
|
+
tableJson[f] = cost;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [tok, bucket] of index) {
|
|
154
|
+
const f = foldToken(tok.includes("/") ? tok.slice(tok.lastIndexOf("/") + 1) : tok);
|
|
155
|
+
if (!f || tableJson[f] !== undefined)
|
|
156
|
+
continue;
|
|
157
|
+
tableJson[f] = bucket.find(([p]) => p === "openrouter")?.[1] ?? bucket[0][1];
|
|
158
|
+
}
|
|
159
|
+
return Object.fromEntries(Object.entries(tableJson).sort(([a], [b]) => a.localeCompare(b)));
|
|
160
|
+
}
|
|
161
|
+
async function fetchJson(url) {
|
|
162
|
+
const response = await fetch(url, {
|
|
163
|
+
headers: { accept: "application/json" },
|
|
164
|
+
signal: AbortSignal.timeout(FETCH_MS),
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok)
|
|
167
|
+
throw new Error(`HTTP ${response.status}`);
|
|
168
|
+
return response.json();
|
|
169
|
+
}
|
|
170
|
+
export async function fetchModelPriceTable() {
|
|
171
|
+
let api;
|
|
172
|
+
let models;
|
|
173
|
+
try {
|
|
174
|
+
[api, models] = await Promise.all([
|
|
175
|
+
fetchJson(MODELS_DEV_API),
|
|
176
|
+
fetchJson(MODELS_DEV_MODELS),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
[api, models] = await Promise.all([
|
|
181
|
+
fetchJson(MODELS_DEV_PROXY_API),
|
|
182
|
+
fetchJson(MODELS_DEV_PROXY_MODELS),
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
return compactModelPrices(api, models);
|
|
186
|
+
}
|
|
187
|
+
function cacheIsFresh(path) {
|
|
188
|
+
try {
|
|
189
|
+
return Date.now() - statSync(path).mtimeMs < REFRESH_MS;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function applyTable(next) {
|
|
196
|
+
table = next;
|
|
197
|
+
}
|
|
198
|
+
function writeCache(path, prices) {
|
|
199
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
200
|
+
atomicWrite(path, Buffer.from(`${JSON.stringify(prices)}\n`), 0o600);
|
|
201
|
+
}
|
|
202
|
+
/** Refresh from models.dev when the local cache is older than 24h; fall back to bundled snapshot. */
|
|
203
|
+
export async function ensureModelPrices() {
|
|
204
|
+
const cache = cachePath();
|
|
205
|
+
if (cacheIsFresh(cache)) {
|
|
206
|
+
applyTable(readTableFile(cache) ?? loadBundled());
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const prices = await fetchModelPriceTable();
|
|
211
|
+
if (Object.keys(prices).length > 0) {
|
|
212
|
+
writeCache(cache, prices);
|
|
213
|
+
applyTable(parseTable(prices));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
46
216
|
}
|
|
217
|
+
catch {
|
|
218
|
+
// Keep cache or bundled snapshot.
|
|
219
|
+
}
|
|
220
|
+
applyTable(readTableFile(cache) ?? loadBundled());
|
|
221
|
+
}
|
|
222
|
+
function loadTable() {
|
|
223
|
+
if (table)
|
|
224
|
+
return table;
|
|
225
|
+
table = loadBundled();
|
|
47
226
|
return table;
|
|
48
227
|
}
|
|
49
228
|
function lookupKeys(slug) {
|
|
@@ -55,7 +234,7 @@ function lookupKeys(slug) {
|
|
|
55
234
|
keys.push(stripped);
|
|
56
235
|
return keys;
|
|
57
236
|
}
|
|
58
|
-
/** USD per million tokens
|
|
237
|
+
/** USD per million tokens; zeros if unknown. */
|
|
59
238
|
export function lookupModelCost(slug) {
|
|
60
239
|
const prices = loadTable();
|
|
61
240
|
for (const key of lookupKeys(slug)) {
|
|
@@ -65,3 +244,6 @@ export function lookupModelCost(slug) {
|
|
|
65
244
|
}
|
|
66
245
|
return ZERO_COST;
|
|
67
246
|
}
|
|
247
|
+
export function resetModelPrices() {
|
|
248
|
+
table = undefined;
|
|
249
|
+
}
|
package/dist/proxy.js
CHANGED
|
@@ -112,6 +112,7 @@ export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
|
112
112
|
}
|
|
113
113
|
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
114
114
|
const upstream = send(target, {
|
|
115
|
+
agent: false,
|
|
115
116
|
method: request.method,
|
|
116
117
|
headers: proxyHeaders(request.headers, apiKey),
|
|
117
118
|
}, (upstreamResponse) => {
|
package/dist/targets/claude.js
CHANGED
|
@@ -169,7 +169,7 @@ export async function createClaudeProxy(cpaUrl, apiKey) {
|
|
|
169
169
|
const headers = proxyHeaders(request.headers, apiKey);
|
|
170
170
|
headers["content-length"] = String(body.length);
|
|
171
171
|
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
172
|
-
const upstream = send(target, { method: request.method, headers }, (upstreamResponse) => {
|
|
172
|
+
const upstream = send(target, { method: request.method, headers, agent: false }, (upstreamResponse) => {
|
|
173
173
|
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
174
174
|
upstreamResponse.pipe(response);
|
|
175
175
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
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, dropAgent, fetchCatalog, readState, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
|
|
5
|
+
import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
|
|
6
|
+
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, objectValue, resolveApiKey, } from "../util.js";
|
|
7
|
+
/**
|
|
8
|
+
* Cline only accepts its built-in provider ids; `openai-compatible` is the one
|
|
9
|
+
* slot meant for a user-defined OpenAI-compatible endpoint. The CLI (`cline`)
|
|
10
|
+
* and Cline Desktop share that slot through ~/.cline/data/settings/providers.json
|
|
11
|
+
* (both talk to the same local hub daemon on 127.0.0.1:25463), so one injection
|
|
12
|
+
* covers both apps.
|
|
13
|
+
*/
|
|
14
|
+
export const CLINE_PROVIDER_ID = "openai-compatible";
|
|
15
|
+
/** Cline sends this placeholder key; the loopback proxy swaps in the CPA key. */
|
|
16
|
+
export const CLINE_API_KEY_PLACEHOLDER = "cpac";
|
|
17
|
+
export function clineDataDir() {
|
|
18
|
+
const override = process.env.CLINE_DATA_DIR?.trim();
|
|
19
|
+
if (override)
|
|
20
|
+
return expandUserPath(override);
|
|
21
|
+
return join(homedir(), ".cline", "data");
|
|
22
|
+
}
|
|
23
|
+
export function clineProvidersPath() {
|
|
24
|
+
return join(clineDataDir(), "settings", "providers.json");
|
|
25
|
+
}
|
|
26
|
+
function readProviders(path) {
|
|
27
|
+
if (!existsSync(path))
|
|
28
|
+
return {};
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new CPACError(`invalid Cline providers.json: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
if (!objectValue(parsed))
|
|
37
|
+
throw new CPACError(`invalid Cline providers.json: ${path}`);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function providerEntry(doc) {
|
|
41
|
+
if (!objectValue(doc.providers))
|
|
42
|
+
return null;
|
|
43
|
+
const entry = doc.providers[CLINE_PROVIDER_ID];
|
|
44
|
+
return objectValue(entry) ? entry : null;
|
|
45
|
+
}
|
|
46
|
+
function loopbackBaseUrl(value) {
|
|
47
|
+
if (typeof value !== "string" || !value.trim())
|
|
48
|
+
return false;
|
|
49
|
+
try {
|
|
50
|
+
const host = new URL(value).hostname.toLowerCase();
|
|
51
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** True while the slot still carries the CPAC placeholder key and loopback URL. */
|
|
58
|
+
function isCpacEntry(entry) {
|
|
59
|
+
if (!objectValue(entry) || !objectValue(entry.settings))
|
|
60
|
+
return false;
|
|
61
|
+
const settings = entry.settings;
|
|
62
|
+
return (settings.apiKey === CLINE_API_KEY_PLACEHOLDER &&
|
|
63
|
+
loopbackBaseUrl(settings.baseUrl));
|
|
64
|
+
}
|
|
65
|
+
export function isClineConfigInstalled() {
|
|
66
|
+
try {
|
|
67
|
+
return isCpacEntry(providerEntry(readProviders(clineProvidersPath())));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function clineModelSlugs(rows) {
|
|
74
|
+
const slugs = [];
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
const slug = catalogModelId(row);
|
|
77
|
+
if (slug)
|
|
78
|
+
slugs.push(slug);
|
|
79
|
+
}
|
|
80
|
+
return slugs;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Keep Cline's slot shape (settings/updatedAt/tokenSource) and merge over the
|
|
84
|
+
* fields CPAC owns, so extra user tuning inside the slot survives re-injection.
|
|
85
|
+
*/
|
|
86
|
+
function clineProviderEntry(port, model, previous) {
|
|
87
|
+
const previousSettings = previous && objectValue(previous.settings) ? previous.settings : {};
|
|
88
|
+
return {
|
|
89
|
+
...(previous ?? {}),
|
|
90
|
+
settings: {
|
|
91
|
+
...previousSettings,
|
|
92
|
+
provider: CLINE_PROVIDER_ID,
|
|
93
|
+
apiKey: CLINE_API_KEY_PLACEHOLDER,
|
|
94
|
+
model,
|
|
95
|
+
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
96
|
+
},
|
|
97
|
+
updatedAt: new Date().toISOString(),
|
|
98
|
+
tokenSource: "manual",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function writeClineProvider(path, port, model) {
|
|
102
|
+
const doc = readProviders(path);
|
|
103
|
+
const previous = providerEntry(doc);
|
|
104
|
+
const providers = objectValue(doc.providers) ? { ...doc.providers } : {};
|
|
105
|
+
providers[CLINE_PROVIDER_ID] = clineProviderEntry(port, model, previous);
|
|
106
|
+
doc.providers = providers;
|
|
107
|
+
if (typeof doc.version !== "number")
|
|
108
|
+
doc.version = 1;
|
|
109
|
+
if (!objectValue(doc.modes))
|
|
110
|
+
doc.modes = {};
|
|
111
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
112
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
113
|
+
atomicWrite(path, Buffer.from(`${JSON.stringify(doc, null, 2)}\n`), mode);
|
|
114
|
+
}
|
|
115
|
+
export async function installClineConfig(config, force = false) {
|
|
116
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
117
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
118
|
+
let document;
|
|
119
|
+
try {
|
|
120
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new CPACError("invalid CPA catalog");
|
|
124
|
+
}
|
|
125
|
+
const rows = catalogModelRows(document) ?? [];
|
|
126
|
+
const slugs = clineModelSlugs(rows);
|
|
127
|
+
if (slugs.length === 0)
|
|
128
|
+
throw new CPACError("CPA catalog contains no models");
|
|
129
|
+
const target = clineProvidersPath();
|
|
130
|
+
const existing = providerEntry(readProviders(target));
|
|
131
|
+
if (existing && !isCpacEntry(existing) && !force) {
|
|
132
|
+
throw new CPACError(`Cline provider "${CLINE_PROVIDER_ID}" already holds a non-CPAC configuration (${target}); ` +
|
|
133
|
+
`re-run with --force to replace it (the original file is backed up in the cpac state dir)`);
|
|
134
|
+
}
|
|
135
|
+
// Keep the model the user picked in Cline when it still exists in the catalog.
|
|
136
|
+
const previousModel = existing && objectValue(existing.settings) ? existing.settings.model : undefined;
|
|
137
|
+
const model = typeof previousModel === "string" && slugs.includes(previousModel)
|
|
138
|
+
? previousModel
|
|
139
|
+
: slugs[0];
|
|
140
|
+
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
141
|
+
const hadFile = existsSync(target);
|
|
142
|
+
try {
|
|
143
|
+
ensureCpacBackup(config.state_dir, "cline", target, !isClineConfigInstalled());
|
|
144
|
+
writeClineProvider(target, proxy.port, model);
|
|
145
|
+
recordWrittenConfig(config.state_dir, "cline", target, {
|
|
146
|
+
id: proxy.id,
|
|
147
|
+
pid: proxy.pid,
|
|
148
|
+
port: proxy.port,
|
|
149
|
+
fingerprint,
|
|
150
|
+
}, hadFile);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (started)
|
|
154
|
+
await stopProxyProcess(proxy);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
console.log(`Installed Cline provider "${CLINE_PROVIDER_ID}" (model ${model}): ${target}`);
|
|
158
|
+
console.log(`Cline CLI: cline -P ${CLINE_PROVIDER_ID} -m <model> (or cpac cline); Cline Desktop: pick "OpenAI Compatible" in the provider list.`);
|
|
159
|
+
console.log(`The stored key stays "${CLINE_API_KEY_PLACEHOLDER}"; the loopback proxy injects ${config.api_key_env}.`);
|
|
160
|
+
}
|
|
161
|
+
/** Drop the CPAC slot; keep the file untouched when Cline now owns the slot. */
|
|
162
|
+
function stripClineProvider(current) {
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(new TextDecoder("utf-8").decode(current));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return current;
|
|
169
|
+
}
|
|
170
|
+
if (!objectValue(parsed))
|
|
171
|
+
return current;
|
|
172
|
+
const doc = parsed;
|
|
173
|
+
const providers = doc.providers;
|
|
174
|
+
if (!objectValue(providers))
|
|
175
|
+
return current;
|
|
176
|
+
const entry = providers[CLINE_PROVIDER_ID];
|
|
177
|
+
if (!isCpacEntry(entry))
|
|
178
|
+
return current;
|
|
179
|
+
const nextProviders = { ...providers };
|
|
180
|
+
delete nextProviders[CLINE_PROVIDER_ID];
|
|
181
|
+
if (Object.keys(nextProviders).length > 0)
|
|
182
|
+
doc.providers = nextProviders;
|
|
183
|
+
else
|
|
184
|
+
delete doc.providers;
|
|
185
|
+
return Buffer.from(`${JSON.stringify(doc, null, 2)}\n`);
|
|
186
|
+
}
|
|
187
|
+
export async function uninstallClineConfig(config) {
|
|
188
|
+
const target = clineProvidersPath();
|
|
189
|
+
const recorded = readState(config.state_dir)?.agents?.cline;
|
|
190
|
+
if (!recorded && !isClineConfigInstalled())
|
|
191
|
+
throw new CPACError("Cline CPAC provider is not installed");
|
|
192
|
+
restoreOwnedConfig(config.state_dir, "cline", target, stripClineProvider);
|
|
193
|
+
const { stopProxy } = dropAgent(config.state_dir, "cline");
|
|
194
|
+
if (stopProxy)
|
|
195
|
+
await stopProxyProcess(stopProxy);
|
|
196
|
+
console.log(`Removed Cline provider "${CLINE_PROVIDER_ID}": ${target}`);
|
|
197
|
+
}
|
package/dist/targets/pi.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync } from "node:
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
|
|
5
|
-
import { lookupModelCost } from "../pricing.js";
|
|
5
|
+
import { ensureModelPrices, lookupModelCost } from "../pricing.js";
|
|
6
6
|
import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
|
|
7
7
|
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, objectValue, resolveApiKey, } from "../util.js";
|
|
8
8
|
export const PI_PROVIDER_ID = "cpac";
|
|
@@ -234,6 +234,7 @@ export async function installPiConfig(config) {
|
|
|
234
234
|
const rows = catalogModelRows(document) ?? [];
|
|
235
235
|
if (rows.length === 0)
|
|
236
236
|
throw new CPACError("CPA catalog contains no models");
|
|
237
|
+
await ensureModelPrices();
|
|
237
238
|
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
238
239
|
const provider = buildCpacProvider(proxy.port, rows);
|
|
239
240
|
const target = piModelsPath();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"test": "npm run clean && tsc -p tsconfig.test.json && npm run copy-data && node --test --test-reporter=spec dist-test/cpac.test.js",
|
|
34
34
|
"pack:check": "npm pack --dry-run",
|
|
35
35
|
"copy-data": "node -e \"const fs=require('fs');const path=require('path');for (const d of ['dist/data','dist-test/src/data']) {fs.mkdirSync(d,{recursive:true});fs.copyFileSync('src/data/model-prices.json', path.join(d,'model-prices.json'))}\"",
|
|
36
|
-
"refresh-prices": "node scripts/refresh-model-prices.mjs"
|
|
36
|
+
"refresh-prices": "npm run build && node scripts/refresh-model-prices.mjs"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^22.13.10",
|