@yhong91/cpac 0.2.3 → 0.2.4
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 +30 -36
- package/dist/agents.js +49 -27
- package/dist/config.js +18 -3
- package/dist/cpac.js +21 -19
- package/dist/models.js +145 -0
- package/dist/targets/codebuddy.js +204 -0
- package/package.json +1 -1
- package/dist/targets/cline.js +0 -197
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`、`hermes` 或 `
|
|
13
|
+
- 已安装需要使用的 agent:`codex`、`claude`、`pi`、`kimi`、`grok`、`zed`、`hermes` 或 `codebuddy`
|
|
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/codebuddy)
|
|
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`/`
|
|
64
|
+
1. **检查前置**:`node --version` ≥ 20;确认用户需要哪些 agent(`codex`/`claude`/`pi`/`kimi`/`grok`/`zed`/`hermes`/`codebuddy` 在 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|
|
|
93
|
+
- 启动:`cpac <codex|claude|kimi|grok|pi|zed|hermes|codebuddy> [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|codebuddy>` 恢复原配置。
|
|
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|
|
|
117
|
+
cpac restore <codex|pi|kimi|grok|zed|hermes|codebuddy> | --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`、`codebuddy` 七个持久注入目标统一管理(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|
|
|
171
|
-
cpac sync [codex|pi|kimi|grok|zed|hermes|
|
|
172
|
-
cpac restore <codex|pi|kimi|grok|zed|hermes|
|
|
170
|
+
cpac install [codex|pi|kimi|grok|zed|hermes|codebuddy...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]
|
|
171
|
+
cpac sync [codex|pi|kimi|grok|zed|hermes|codebuddy...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]
|
|
172
|
+
cpac restore <codex|pi|kimi|grok|zed|hermes|codebuddy...> | --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`/`
|
|
181
|
-
- `sync`
|
|
180
|
+
- `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok`/`zed`/`hermes`/`codebuddy` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
181
|
+
- `sync` 把 CPA 模型列表和更新时间写到 `state_dir/models.json`(默认 `~/.cpac/models.json`)。这个时间戳就是模型版本:列表没变则时间戳不变,目标上次同步到的版本相同就跳过,不同才重写。不带参数只看已安装目标;`--all` 还会装上检测到但还没接入的目标。`install` 仍总是重写。`spawn_models`、`v2_off`、`max_context` 和代理端口不改这个版本,要马上生效用 `cpac install`。`--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 /
|
|
186
|
+
- `upgrade` 是发布后更新本机的一次性入口:查 npm → 需要时 `npm install -g` → 对**已接入**的 Codex / Pi / Kimi / Grok / Zed / Hermes / CodeBuddy 执行 `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/
|
|
256
|
+
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok/zed/hermes/pi/codebuddy 同样:没有旧配置不写空备份,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` / `cpac
|
|
258
|
+
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac pi` / `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` / `cpac codebuddy` 即可恢复。
|
|
259
259
|
|
|
260
260
|
默认路径:
|
|
261
261
|
|
|
@@ -268,8 +268,8 @@ 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
|
|
272
271
|
Pi ~/.cpac/pi/models.json
|
|
272
|
+
CodeBuddy ~/.cpac/codebuddy/models.json
|
|
273
273
|
restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
|
|
274
274
|
```
|
|
275
275
|
|
|
@@ -336,32 +336,26 @@ cpac restore hermes
|
|
|
336
336
|
|
|
337
337
|
配置目录尊重 `HERMES_HOME`(Windows 默认 `%LOCALAPPDATA%\hermes`)。首次写入前若 default `config.yaml` 已存在,会在 `state_dir/hermes/config.yaml` 保留原始快照。profile 文件不单独备份,restore 只剥 `providers.cpac`。
|
|
338
338
|
|
|
339
|
-
###
|
|
339
|
+
### CodeBuddy
|
|
340
340
|
|
|
341
|
-
|
|
341
|
+
把 CPA 目录写成 CodeBuddy 的自定义模型:
|
|
342
342
|
|
|
343
343
|
```bash
|
|
344
|
-
cpac install
|
|
345
|
-
cpac
|
|
346
|
-
cpac
|
|
347
|
-
cpac cline -P cline "..." # 显式指定 provider 时不注入
|
|
344
|
+
cpac install codebuddy
|
|
345
|
+
cpac codebuddy # 启动;原生参数原样直通
|
|
346
|
+
cpac codebuddy -p "检查当前项目" # 非交互模式
|
|
348
347
|
```
|
|
349
348
|
|
|
350
|
-
|
|
349
|
+
`~/.codebuddy/models.json` 是 CodeBuddy 官方放置第三方模型的地方,CPAC 只做**合并**:为每个 catalog slug 写一条 `models[]` 条目(`vendor: "CPAC"`、占位 `apiKey`(`cpac`)、`url` 指向 loopback `/v1/chat/completions`),你在同一文件里自己加的模型条目原样保留。loopback 代理把入站 Authorization 换成真实 `CPA_API_KEY`。
|
|
351
350
|
|
|
352
|
-
-
|
|
353
|
-
-
|
|
354
|
-
-
|
|
355
|
-
-
|
|
356
|
-
-
|
|
351
|
+
- 字段映射:`maxInputTokens` 取 catalog 的 `context_window`(不是 `max_context_window`),`maxOutputTokens` 取 `max_output_tokens`,catalog 没给就不写这两个键;`supportsImages` / `supportsReasoning` 按 `input_modalities` / `supported_reasoning_levels` 推导,`supportsToolCall` 恒为 true。
|
|
352
|
+
- 不写 `relatedModels`:CodeBuddy 的 lite / reasoning 场景因此回落到主模型,而不是借用内置默认值。
|
|
353
|
+
- `availableModels` 只在你已经设置过时才改写(缺失即「显示全部」);改写只并入 CPA slug,不会删掉你列出的 id。
|
|
354
|
+
- 归属识别:`apiKey` 为 `cpac` 且 `url` 是 loopback `…/chat/completions` 的条目才算 CPAC 的,`cpac sync codebuddy` 只替换这些条目。
|
|
355
|
+
- 恢复:注入后未改 → 逐字节还原原文件(原本没有该文件则删除它);改过 → 只剥 CPAC 条目,你的模型和 `availableModels` 留下。
|
|
356
|
+
- 模型选择仍由 CodeBuddy 决定:用 `/model` 选 CPA 模型或用 `CODEBUDDY_MODEL` 指定默认模型,CPAC 不改 `settings.json`。
|
|
357
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` 保留原始快照。
|
|
358
|
+
配置目录尊重 `CODEBUDDY_CONFIG_DIR`(否则 `~/.codebuddy`)。首次写入前若 `models.json` 已存在,会在 `state_dir/codebuddy/models.json` 保留原始快照。
|
|
365
359
|
|
|
366
360
|
### 手动打开 5h 窗口
|
|
367
361
|
|
|
@@ -384,7 +378,7 @@ cpac ping agy # 目录里匹配 gemini-*-flash* 的一条,以及 claude-s
|
|
|
384
378
|
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
|
|
385
379
|
| `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
|
|
386
380
|
| `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
|
|
387
|
-
| `
|
|
381
|
+
| `CODEBUDDY_CONFIG_DIR` | `~/.codebuddy` | 覆盖 CodeBuddy 配置目录(影响 `cpac install codebuddy` 写入的 `models.json` 位置) |
|
|
388
382
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
389
383
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
390
384
|
|
package/dist/agents.js
CHANGED
|
@@ -3,15 +3,16 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { MAX_SPAWN_MODELS, loadCatalogSlugs, pickSpawnModels, saveCodexSetup, saveSpawnModels, } from "./config.js";
|
|
6
|
+
import { MAX_SPAWN_MODELS, fetchCatalog, loadCatalogSlugs, pickSpawnModels, saveCodexSetup, saveSpawnModels, } from "./config.js";
|
|
7
|
+
import { markModelsApplied, refreshModelIndex } from "./models.js";
|
|
7
8
|
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
8
9
|
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
9
10
|
import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
10
11
|
import { installPiConfig, isPiConfigInstalled, piAgentDir, piModelsPath, uninstallPiConfig, } from "./targets/pi.js";
|
|
11
12
|
import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
|
|
12
13
|
import { hermesConfigPaths, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
|
|
13
|
-
import {
|
|
14
|
-
import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, tabWizard, } from "./util.js";
|
|
14
|
+
import { codebuddyHome, codebuddyModelsPath, installCodebuddyConfig, isCodebuddyConfigInstalled, uninstallCodebuddyConfig, } from "./targets/codebuddy.js";
|
|
15
|
+
import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, resolveApiKey, tabWizard, } from "./util.js";
|
|
15
16
|
const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
|
|
16
17
|
const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
|
|
17
18
|
const CODEX_V2_ON = "Enabled (Default)";
|
|
@@ -327,27 +328,24 @@ const TARGETS = [
|
|
|
327
328
|
},
|
|
328
329
|
},
|
|
329
330
|
{
|
|
330
|
-
id: "
|
|
331
|
-
home: () =>
|
|
332
|
-
detected: () => binaryOnPath("
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
installed: () => isClineConfigInstalled(),
|
|
337
|
-
version: () => detectClientVersion("cline"),
|
|
338
|
-
install: async (config, dryRun, _v2Off, _maxContext, force) => {
|
|
331
|
+
id: "codebuddy",
|
|
332
|
+
home: () => codebuddyHome(),
|
|
333
|
+
detected: () => binaryOnPath("codebuddy") || existsSync(codebuddyHome()),
|
|
334
|
+
installed: () => isCodebuddyConfigInstalled(),
|
|
335
|
+
version: () => detectClientVersion("codebuddy"),
|
|
336
|
+
install: async (config, dryRun) => {
|
|
339
337
|
if (dryRun) {
|
|
340
|
-
console.log(`would write
|
|
338
|
+
console.log(`would write CPAC models into CodeBuddy: ${codebuddyModelsPath()}`);
|
|
341
339
|
return;
|
|
342
340
|
}
|
|
343
|
-
await
|
|
341
|
+
await installCodebuddyConfig(config);
|
|
344
342
|
},
|
|
345
343
|
uninstall: async (config, dryRun) => {
|
|
346
344
|
if (dryRun) {
|
|
347
|
-
console.log(`would remove
|
|
345
|
+
console.log(`would remove CPAC models from CodeBuddy: ${codebuddyModelsPath()}`);
|
|
348
346
|
return;
|
|
349
347
|
}
|
|
350
|
-
await
|
|
348
|
+
await uninstallCodebuddyConfig(config);
|
|
351
349
|
},
|
|
352
350
|
},
|
|
353
351
|
];
|
|
@@ -406,27 +404,51 @@ export async function runInstall(config, requested, options) {
|
|
|
406
404
|
}
|
|
407
405
|
return 0;
|
|
408
406
|
}
|
|
409
|
-
|
|
407
|
+
function syncTargetIds(config, requested, all) {
|
|
410
408
|
const installed = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
411
|
-
if (requested.length === 0 && !
|
|
409
|
+
if (requested.length === 0 && !all) {
|
|
412
410
|
if (installed.length === 0) {
|
|
413
411
|
throw new CPACError("no installed CPAC integrations found; run cpac install first");
|
|
414
412
|
}
|
|
415
|
-
return
|
|
416
|
-
all: false,
|
|
417
|
-
dryRun: options.dryRun,
|
|
418
|
-
force: true,
|
|
419
|
-
v2Off: options.v2Off,
|
|
420
|
-
maxContext: options.maxContext,
|
|
421
|
-
});
|
|
413
|
+
return installed;
|
|
422
414
|
}
|
|
423
|
-
|
|
424
|
-
|
|
415
|
+
const eligible = TARGETS.filter((target) => target.detected()).map((target) => target.id);
|
|
416
|
+
const selected = selectTargets(requested, all, eligible);
|
|
417
|
+
if (selected.length === 0) {
|
|
418
|
+
throw new CPACError(`no supported targets detected; use: cpac install ${TARGETS.map((target) => target.id).join(" ")} or --all`);
|
|
419
|
+
}
|
|
420
|
+
return selected.map((target) => target.id);
|
|
421
|
+
}
|
|
422
|
+
export async function runSync(config, requested, options) {
|
|
423
|
+
const ids = syncTargetIds(config, requested, options.all);
|
|
424
|
+
if (options.maxContext && ids.some((id) => id !== "codex")) {
|
|
425
|
+
throw new CPACError("--max_context is only valid with codex");
|
|
426
|
+
}
|
|
427
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
428
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
429
|
+
const { index, changed } = refreshModelIndex(config.state_dir, catalog.bytes, new Date().toISOString(), !options.dryRun);
|
|
430
|
+
console.log(`models ${index.updated_at}${changed ? (options.dryRun ? " would update" : " updated") : " unchanged"}`);
|
|
431
|
+
const pending = ids.filter((id) => index.applied[id] !== index.updated_at);
|
|
432
|
+
for (const id of ids) {
|
|
433
|
+
if (index.applied[id] === index.updated_at) {
|
|
434
|
+
console.log(`${id}: model version ${index.updated_at} unchanged`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (pending.length === 0)
|
|
438
|
+
return 0;
|
|
439
|
+
// ponytail: version is the model-list timestamp only. spawn_models, v2_off,
|
|
440
|
+
// max_context, and the proxy port do not bump it; cpac install still rewrites.
|
|
441
|
+
const code = await runInstall(config, pending, {
|
|
442
|
+
all: false,
|
|
425
443
|
dryRun: options.dryRun,
|
|
426
444
|
force: true,
|
|
427
445
|
v2Off: options.v2Off,
|
|
428
446
|
maxContext: options.maxContext,
|
|
429
447
|
});
|
|
448
|
+
if (!options.dryRun && code === 0) {
|
|
449
|
+
markModelsApplied(config.state_dir, pending, index.updated_at);
|
|
450
|
+
}
|
|
451
|
+
return code;
|
|
430
452
|
}
|
|
431
453
|
export async function runRestore(config, requested, options) {
|
|
432
454
|
if (requested.length === 0 && !options.all) {
|
package/dist/config.js
CHANGED
|
@@ -29,7 +29,7 @@ export const AGENT_IDS = [
|
|
|
29
29
|
"grok",
|
|
30
30
|
"zed",
|
|
31
31
|
"hermes",
|
|
32
|
-
"
|
|
32
|
+
"codebuddy",
|
|
33
33
|
];
|
|
34
34
|
const AGENT_ID_SET = new Set(AGENT_IDS);
|
|
35
35
|
const STATE_KEYS = new Set([
|
|
@@ -472,9 +472,13 @@ export function readState(stateDir) {
|
|
|
472
472
|
if (value.agents !== undefined) {
|
|
473
473
|
if (!objectValue(value.agents))
|
|
474
474
|
throw new CPACError("invalid agents in state");
|
|
475
|
+
// Records for targets that no longer exist (e.g. cline) are dropped on
|
|
476
|
+
// load instead of erroring, so a state file written by an older release
|
|
477
|
+
// keeps working after a target is removed.
|
|
478
|
+
const agents = {};
|
|
475
479
|
for (const [id, rec] of Object.entries(value.agents)) {
|
|
476
480
|
if (!AGENT_ID_SET.has(id))
|
|
477
|
-
|
|
481
|
+
continue;
|
|
478
482
|
if (!objectValue(rec) ||
|
|
479
483
|
typeof rec.path !== "string" ||
|
|
480
484
|
!rec.path ||
|
|
@@ -489,7 +493,12 @@ export function readState(stateDir) {
|
|
|
489
493
|
Object.keys(rec).some((key) => !AGENT_RECORD_KEYS.has(key))) {
|
|
490
494
|
throw new CPACError(`invalid agent record: ${id}`);
|
|
491
495
|
}
|
|
496
|
+
agents[id] = rec;
|
|
492
497
|
}
|
|
498
|
+
if (Object.keys(agents).length > 0)
|
|
499
|
+
value.agents = agents;
|
|
500
|
+
else
|
|
501
|
+
delete value.agents;
|
|
493
502
|
}
|
|
494
503
|
const proxyKeys = ["proxy_id", "proxy_pid", "proxy_port"];
|
|
495
504
|
const proxyKeyCount = proxyKeys.filter((key) => key in value).length;
|
|
@@ -535,7 +544,13 @@ export function needsProxy(state) {
|
|
|
535
544
|
if (state.config_path)
|
|
536
545
|
return true;
|
|
537
546
|
const agents = state.agents ?? {};
|
|
538
|
-
return Boolean(agents.codex ||
|
|
547
|
+
return Boolean(agents.codex ||
|
|
548
|
+
agents.pi ||
|
|
549
|
+
agents.kimi ||
|
|
550
|
+
agents.grok ||
|
|
551
|
+
agents.zed ||
|
|
552
|
+
agents.hermes ||
|
|
553
|
+
agents.codebuddy);
|
|
539
554
|
}
|
|
540
555
|
export function saveState(stateDir, state) {
|
|
541
556
|
atomicWrite(join(stateDir, "state.json"), Buffer.from(`${JSON.stringify(state, null, 2)}\n`));
|
package/dist/cpac.js
CHANGED
|
@@ -11,7 +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 {
|
|
14
|
+
import { isCodebuddyConfigInstalled } from "./targets/codebuddy.js";
|
|
15
15
|
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, } from "./config.js";
|
|
16
16
|
import { runPing } from "./ping.js";
|
|
17
17
|
import { proxyIsHealthy, runProxyChild } from "./proxy.js";
|
|
@@ -28,7 +28,7 @@ export { installPiConfig, isPiConfigInstalled, uninstallPiConfig, PI_PROVIDER_ID
|
|
|
28
28
|
export { compactModelPrices, ensureModelPrices, lookupModelCost, resetModelPrices, } from "./pricing.js";
|
|
29
29
|
export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
|
|
30
30
|
export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
|
|
31
|
-
export {
|
|
31
|
+
export { CODEBUDDY_API_KEY_PLACEHOLDER, codebuddyModelsPath, installCodebuddyConfig, isCodebuddyConfigInstalled, uninstallCodebuddyConfig, } from "./targets/codebuddy.js";
|
|
32
32
|
export async function status(config) {
|
|
33
33
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
34
34
|
const keyConfigured = !!apiKey;
|
|
@@ -84,8 +84,8 @@ export async function status(config) {
|
|
|
84
84
|
console.log(`zed: ${keyConfigured ? (zedInstalled ? "ready" : "config not installed; run: cpac install zed") : "not configured"}`);
|
|
85
85
|
const hermesInstalled = isHermesConfigInstalled();
|
|
86
86
|
console.log(`hermes: ${keyConfigured ? (hermesInstalled ? "ready" : "config not installed; run: cpac install hermes") : "not configured"}`);
|
|
87
|
-
const
|
|
88
|
-
console.log(`
|
|
87
|
+
const codebuddyInstalled = isCodebuddyConfigInstalled();
|
|
88
|
+
console.log(`codebuddy: ${keyConfigured ? (codebuddyInstalled ? "ready" : "config not installed; run: cpac install codebuddy") : "not configured"}`);
|
|
89
89
|
if (!state)
|
|
90
90
|
return 1;
|
|
91
91
|
const proxy = stateProxy(state);
|
|
@@ -130,10 +130,10 @@ async function listModels(config, json) {
|
|
|
130
130
|
return 0;
|
|
131
131
|
}
|
|
132
132
|
async function guide(config) {
|
|
133
|
-
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,
|
|
133
|
+
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, codebuddy, 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, CodeBuddy)\n cpac restore <codex|pi|kimi|grok|zed|hermes|codebuddy> | --all Detach injected agents
|
|
134
134
|
cpac detect Show supported targets and install status
|
|
135
|
-
cpac install [codex|pi|kimi|grok|zed|hermes|
|
|
136
|
-
cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed/Hermes/
|
|
135
|
+
cpac install [codex|pi|kimi|grok|zed|hermes|codebuddy...] Install CPA into detected targets, or named agents
|
|
136
|
+
cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed/Hermes/CodeBuddy injections from CPA
|
|
137
137
|
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 ping codex|agy Send one request to open that 5h window\n cpac --help Show command usage`);
|
|
138
138
|
if (process.env[config.api_key_env]?.trim())
|
|
139
139
|
return 0;
|
|
@@ -174,7 +174,7 @@ function usage() {
|
|
|
174
174
|
cmd("cpac ping <codex|agy>", "send one request to open that 5h window"),
|
|
175
175
|
"",
|
|
176
176
|
"Positionals:",
|
|
177
|
-
helpRow("agent", "codex, kimi, grok, pi, zed, hermes,
|
|
177
|
+
helpRow("agent", "codex, kimi, grok, pi, zed, hermes, codebuddy, or claude (claude is launch-only)", "", 18),
|
|
178
178
|
"",
|
|
179
179
|
"Options:",
|
|
180
180
|
opt("-h, --help", "show help", "[boolean]"),
|
|
@@ -338,7 +338,16 @@ function parseArgs(args) {
|
|
|
338
338
|
}
|
|
339
339
|
return parsed;
|
|
340
340
|
}
|
|
341
|
-
if ([
|
|
341
|
+
if ([
|
|
342
|
+
"codex",
|
|
343
|
+
"kimi",
|
|
344
|
+
"grok",
|
|
345
|
+
"pi",
|
|
346
|
+
"zed",
|
|
347
|
+
"hermes",
|
|
348
|
+
"codebuddy",
|
|
349
|
+
"claude",
|
|
350
|
+
].includes(args[0])) {
|
|
342
351
|
const targetId = args[0];
|
|
343
352
|
let configPath = defaultConfigPath();
|
|
344
353
|
const raw = args.slice(1);
|
|
@@ -686,7 +695,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
686
695
|
});
|
|
687
696
|
if (parsed.command === "uninstall") {
|
|
688
697
|
if (parsed.targets.length || parsed.all) {
|
|
689
|
-
throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed|hermes|
|
|
698
|
+
throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed|hermes|codebuddy> | --all");
|
|
690
699
|
}
|
|
691
700
|
return await runUninstall(config, { dryRun: parsed.dryRun });
|
|
692
701
|
}
|
|
@@ -737,19 +746,12 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
737
746
|
}
|
|
738
747
|
return await runTargetLauncher(launcherConfig, "codex", parsed.args, "codex", { v2Off: parsed.v2Off, maxContext: parsed.maxContext });
|
|
739
748
|
}
|
|
740
|
-
if (parsed.command === "cline") {
|
|
741
|
-
// Cline's default provider is its own hosted service; default the
|
|
742
|
-
// launch to the injected CPAC slot unless the user picked one.
|
|
743
|
-
const args = parsed.args.some((arg) => arg === "-P" || arg === "--provider")
|
|
744
|
-
? parsed.args
|
|
745
|
-
: ["-P", CLINE_PROVIDER_ID, ...parsed.args];
|
|
746
|
-
return await runTargetLauncher(config, "cline", args, "cline");
|
|
747
|
-
}
|
|
748
749
|
if (parsed.command === "kimi" ||
|
|
749
750
|
parsed.command === "grok" ||
|
|
750
751
|
parsed.command === "pi" ||
|
|
751
752
|
parsed.command === "zed" ||
|
|
752
|
-
parsed.command === "hermes"
|
|
753
|
+
parsed.command === "hermes" ||
|
|
754
|
+
parsed.command === "codebuddy") {
|
|
753
755
|
return await runTargetLauncher(config, parsed.command, parsed.args);
|
|
754
756
|
}
|
|
755
757
|
return await status(config);
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { AGENT_IDS, catalogModelRows } from "./config.js";
|
|
4
|
+
import { CPACError, atomicWrite, objectValue } from "./util.js";
|
|
5
|
+
const INDEX_KEYS = new Set(["updated_at", "models", "applied"]);
|
|
6
|
+
const AGENT_ID_SET = new Set(AGENT_IDS);
|
|
7
|
+
const VERSION = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
8
|
+
export function modelIndexPath(stateDir) {
|
|
9
|
+
return join(stateDir, "models.json");
|
|
10
|
+
}
|
|
11
|
+
function isJsonObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isModelRecord(value) {
|
|
15
|
+
return isJsonObject(value) && typeof value.slug === "string" && value.slug.trim() !== "";
|
|
16
|
+
}
|
|
17
|
+
function isAgentId(id) {
|
|
18
|
+
return AGENT_ID_SET.has(id);
|
|
19
|
+
}
|
|
20
|
+
function stableValue(value) {
|
|
21
|
+
if (value === null ||
|
|
22
|
+
typeof value === "boolean" ||
|
|
23
|
+
typeof value === "number" ||
|
|
24
|
+
typeof value === "string") {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value.map(stableValue);
|
|
29
|
+
if (!objectValue(value))
|
|
30
|
+
throw new CPACError("invalid CPA catalog");
|
|
31
|
+
const stable = {};
|
|
32
|
+
for (const key of Object.keys(value).sort())
|
|
33
|
+
stable[key] = stableValue(value[key]);
|
|
34
|
+
return stable;
|
|
35
|
+
}
|
|
36
|
+
export function canonicalModels(bytes) {
|
|
37
|
+
let document;
|
|
38
|
+
try {
|
|
39
|
+
document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new CPACError("invalid CPA catalog");
|
|
43
|
+
}
|
|
44
|
+
const rows = catalogModelRows(document);
|
|
45
|
+
if (!rows || rows.length === 0)
|
|
46
|
+
throw new CPACError("invalid CPA catalog");
|
|
47
|
+
return rows.map((row) => {
|
|
48
|
+
const stable = stableValue(row);
|
|
49
|
+
if (!isModelRecord(stable))
|
|
50
|
+
throw new CPACError("invalid CPA catalog");
|
|
51
|
+
return stable;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function parseApplied(value) {
|
|
55
|
+
if (!objectValue(value))
|
|
56
|
+
return null;
|
|
57
|
+
const applied = {};
|
|
58
|
+
for (const [id, version] of Object.entries(value)) {
|
|
59
|
+
// Entries for targets that no longer exist (e.g. cline) are dropped
|
|
60
|
+
// instead of rejecting the whole index, matching how readState treats
|
|
61
|
+
// agent records left over from an older release.
|
|
62
|
+
if (!isAgentId(id))
|
|
63
|
+
continue;
|
|
64
|
+
if (typeof version !== "string" || !VERSION.test(version))
|
|
65
|
+
return null;
|
|
66
|
+
applied[id] = version;
|
|
67
|
+
}
|
|
68
|
+
return applied;
|
|
69
|
+
}
|
|
70
|
+
function parseModelIndex(value) {
|
|
71
|
+
if (!objectValue(value) ||
|
|
72
|
+
Object.keys(value).some((key) => !INDEX_KEYS.has(key)) ||
|
|
73
|
+
typeof value.updated_at !== "string" ||
|
|
74
|
+
!VERSION.test(value.updated_at) ||
|
|
75
|
+
!Array.isArray(value.models) ||
|
|
76
|
+
value.models.length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const models = [];
|
|
80
|
+
for (const row of value.models) {
|
|
81
|
+
let stable;
|
|
82
|
+
try {
|
|
83
|
+
stable = stableValue(row);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!isModelRecord(stable))
|
|
89
|
+
return null;
|
|
90
|
+
models.push(stable);
|
|
91
|
+
}
|
|
92
|
+
const applied = parseApplied(value.applied);
|
|
93
|
+
if (!applied)
|
|
94
|
+
return null;
|
|
95
|
+
return { updated_at: value.updated_at, models, applied };
|
|
96
|
+
}
|
|
97
|
+
export function readModelIndex(stateDir) {
|
|
98
|
+
const path = modelIndexPath(stateDir);
|
|
99
|
+
if (!existsSync(path))
|
|
100
|
+
return null;
|
|
101
|
+
let value;
|
|
102
|
+
try {
|
|
103
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new CPACError(`cannot read model index: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
const index = parseModelIndex(value);
|
|
109
|
+
if (!index)
|
|
110
|
+
throw new CPACError("invalid model index");
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
function writeModelIndex(stateDir, index) {
|
|
114
|
+
atomicWrite(modelIndexPath(stateDir), Buffer.from(`${JSON.stringify(index, null, 2)}\n`));
|
|
115
|
+
}
|
|
116
|
+
export function refreshModelIndex(stateDir, catalogBytes, now = new Date().toISOString(), write = true) {
|
|
117
|
+
const models = canonicalModels(catalogBytes);
|
|
118
|
+
const current = readModelIndex(stateDir);
|
|
119
|
+
if (current && JSON.stringify(current.models) === JSON.stringify(models)) {
|
|
120
|
+
return { index: current, changed: false };
|
|
121
|
+
}
|
|
122
|
+
const index = {
|
|
123
|
+
updated_at: now,
|
|
124
|
+
models,
|
|
125
|
+
applied: current?.applied ?? {},
|
|
126
|
+
};
|
|
127
|
+
if (write)
|
|
128
|
+
writeModelIndex(stateDir, index);
|
|
129
|
+
return { index, changed: true };
|
|
130
|
+
}
|
|
131
|
+
export function markModelsApplied(stateDir, ids, updatedAt) {
|
|
132
|
+
const current = readModelIndex(stateDir);
|
|
133
|
+
if (!current)
|
|
134
|
+
throw new CPACError("model index missing");
|
|
135
|
+
let changed = false;
|
|
136
|
+
const applied = { ...current.applied };
|
|
137
|
+
for (const id of ids) {
|
|
138
|
+
if (applied[id] === updatedAt)
|
|
139
|
+
continue;
|
|
140
|
+
applied[id] = updatedAt;
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
if (changed)
|
|
144
|
+
writeModelIndex(stateDir, { ...current, applied });
|
|
145
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
* ~/.codebuddy/models.json is CodeBuddy's documented place for third-party
|
|
9
|
+
* models, so CPAC merges rather than owns the file: it writes one entry per
|
|
10
|
+
* catalog slug and leaves every model the user added there untouched. Entries
|
|
11
|
+
* CPAC wrote are recognizable by the placeholder key plus a loopback
|
|
12
|
+
* /chat/completions URL, and only those are replaced on re-sync and dropped on
|
|
13
|
+
* restore.
|
|
14
|
+
*/
|
|
15
|
+
export const CODEBUDDY_API_KEY_PLACEHOLDER = "cpac";
|
|
16
|
+
export const CODEBUDDY_VENDOR = "CPAC";
|
|
17
|
+
export function codebuddyHome() {
|
|
18
|
+
return expandUserPath(process.env.CODEBUDDY_CONFIG_DIR?.trim() || join(homedir(), ".codebuddy"));
|
|
19
|
+
}
|
|
20
|
+
export function codebuddyModelsPath() {
|
|
21
|
+
return join(codebuddyHome(), "models.json");
|
|
22
|
+
}
|
|
23
|
+
function readModelsDoc(path) {
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return {};
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new CPACError(`invalid CodeBuddy models.json: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
if (!objectValue(parsed))
|
|
34
|
+
throw new CPACError(`invalid CodeBuddy models.json: ${path}`);
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
function modelEntries(doc) {
|
|
38
|
+
return Array.isArray(doc.models) ? doc.models : [];
|
|
39
|
+
}
|
|
40
|
+
function loopbackChatCompletionsUrl(value) {
|
|
41
|
+
if (typeof value !== "string" || !value.trim())
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(value);
|
|
45
|
+
const host = url.hostname.toLowerCase();
|
|
46
|
+
return ((host === "127.0.0.1" || host === "localhost" || host === "::1") &&
|
|
47
|
+
url.pathname.endsWith("/chat/completions"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** True for entries CPAC wrote: placeholder key plus a loopback endpoint. */
|
|
54
|
+
function isCpacModel(entry) {
|
|
55
|
+
if (!objectValue(entry))
|
|
56
|
+
return false;
|
|
57
|
+
return (entry.apiKey === CODEBUDDY_API_KEY_PLACEHOLDER &&
|
|
58
|
+
loopbackChatCompletionsUrl(entry.url));
|
|
59
|
+
}
|
|
60
|
+
export function isCodebuddyConfigInstalled() {
|
|
61
|
+
try {
|
|
62
|
+
return modelEntries(readModelsDoc(codebuddyModelsPath())).some(isCpacModel);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function inputHasImage(row) {
|
|
69
|
+
const modalities = row.input_modalities;
|
|
70
|
+
return Array.isArray(modalities) && modalities.some((value) => value === "image");
|
|
71
|
+
}
|
|
72
|
+
function supportsReasoning(row) {
|
|
73
|
+
const levels = row.supported_reasoning_levels;
|
|
74
|
+
return Array.isArray(levels) && levels.length > 0;
|
|
75
|
+
}
|
|
76
|
+
function positiveInt(value) {
|
|
77
|
+
return typeof value === "number" && value > 0 ? Math.floor(value) : undefined;
|
|
78
|
+
}
|
|
79
|
+
function buildModel(row, slug, port) {
|
|
80
|
+
const model = {
|
|
81
|
+
id: slug,
|
|
82
|
+
name: typeof row.display_name === "string" && row.display_name.trim()
|
|
83
|
+
? row.display_name.trim()
|
|
84
|
+
: slug,
|
|
85
|
+
vendor: CODEBUDDY_VENDOR,
|
|
86
|
+
apiKey: CODEBUDDY_API_KEY_PLACEHOLDER,
|
|
87
|
+
url: `http://127.0.0.1:${port}/v1/chat/completions`,
|
|
88
|
+
supportsToolCall: true,
|
|
89
|
+
supportsImages: inputHasImage(row),
|
|
90
|
+
supportsReasoning: supportsReasoning(row),
|
|
91
|
+
};
|
|
92
|
+
const maxInput = positiveInt(row.context_window);
|
|
93
|
+
if (maxInput)
|
|
94
|
+
model.maxInputTokens = maxInput;
|
|
95
|
+
const maxOutput = positiveInt(row.max_output_tokens);
|
|
96
|
+
if (maxOutput)
|
|
97
|
+
model.maxOutputTokens = maxOutput;
|
|
98
|
+
// No relatedModels: CodeBuddy then falls back to the main model for the
|
|
99
|
+
// lite / reasoning slots instead of borrowing product built-in defaults.
|
|
100
|
+
return model;
|
|
101
|
+
}
|
|
102
|
+
/** Keep the user's models and the position CPAC's block already occupied. */
|
|
103
|
+
function mergeModels(existing, cpaModels) {
|
|
104
|
+
const userModels = existing.filter((entry) => !isCpacModel(entry));
|
|
105
|
+
const firstCpac = existing.findIndex(isCpacModel);
|
|
106
|
+
const insertAt = firstCpac === -1
|
|
107
|
+
? userModels.length
|
|
108
|
+
: existing.slice(0, firstCpac).filter((entry) => !isCpacModel(entry)).length;
|
|
109
|
+
return [
|
|
110
|
+
...userModels.slice(0, insertAt),
|
|
111
|
+
...cpaModels,
|
|
112
|
+
...userModels.slice(insertAt),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Only touch availableModels when the user already set it: an absent list means
|
|
117
|
+
* "show everything", but a present one would hide the CPA models. Slugs are
|
|
118
|
+
* added, never removed — the list cannot tell CPA ids from CodeBuddy built-ins.
|
|
119
|
+
*/
|
|
120
|
+
function mergedAvailableModels(doc, slugs) {
|
|
121
|
+
if (!Array.isArray(doc.availableModels))
|
|
122
|
+
return undefined;
|
|
123
|
+
const available = doc.availableModels.filter((id) => typeof id === "string");
|
|
124
|
+
for (const slug of slugs) {
|
|
125
|
+
if (!available.includes(slug))
|
|
126
|
+
available.push(slug);
|
|
127
|
+
}
|
|
128
|
+
return available;
|
|
129
|
+
}
|
|
130
|
+
export async function installCodebuddyConfig(config) {
|
|
131
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
132
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
133
|
+
let document;
|
|
134
|
+
try {
|
|
135
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new CPACError("invalid CPA catalog");
|
|
139
|
+
}
|
|
140
|
+
const rows = catalogModelRows(document) ?? [];
|
|
141
|
+
const catalogRows = rows.flatMap((row) => {
|
|
142
|
+
const slug = catalogModelId(row);
|
|
143
|
+
return slug ? [{ row, slug }] : [];
|
|
144
|
+
});
|
|
145
|
+
if (catalogRows.length === 0)
|
|
146
|
+
throw new CPACError("CPA catalog contains no models");
|
|
147
|
+
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
148
|
+
const models = catalogRows.map(({ row, slug }) => buildModel(row, slug, proxy.port));
|
|
149
|
+
const target = codebuddyModelsPath();
|
|
150
|
+
const hadFile = existsSync(target);
|
|
151
|
+
try {
|
|
152
|
+
const doc = readModelsDoc(target);
|
|
153
|
+
const available = mergedAvailableModels(doc, models.map((model) => String(model.id)));
|
|
154
|
+
ensureCpacBackup(config.state_dir, "codebuddy", target, !isCodebuddyConfigInstalled());
|
|
155
|
+
doc.models = mergeModels(modelEntries(doc), models);
|
|
156
|
+
if (available)
|
|
157
|
+
doc.availableModels = available;
|
|
158
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
159
|
+
const mode = existsSync(target) ? statSync(target).mode & 0o7777 : 0o600;
|
|
160
|
+
atomicWrite(target, Buffer.from(`${JSON.stringify(doc, null, 2)}\n`), mode);
|
|
161
|
+
recordWrittenConfig(config.state_dir, "codebuddy", target, {
|
|
162
|
+
id: proxy.id,
|
|
163
|
+
pid: proxy.pid,
|
|
164
|
+
port: proxy.port,
|
|
165
|
+
fingerprint,
|
|
166
|
+
}, hadFile);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (started)
|
|
170
|
+
await stopProxyProcess(proxy);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
console.log(`Installed ${models.length} CPAC models into CodeBuddy: ${target}`);
|
|
174
|
+
console.log(`Models keep the placeholder key "${CODEBUDDY_API_KEY_PLACEHOLDER}"; the loopback proxy injects ${config.api_key_env}.`);
|
|
175
|
+
}
|
|
176
|
+
function stripCodebuddyManaged(content) {
|
|
177
|
+
let parsed;
|
|
178
|
+
try {
|
|
179
|
+
parsed = JSON.parse(new TextDecoder("utf-8").decode(content));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return content;
|
|
183
|
+
}
|
|
184
|
+
if (!objectValue(parsed))
|
|
185
|
+
return content;
|
|
186
|
+
const doc = parsed;
|
|
187
|
+
const models = modelEntries(doc).filter((entry) => !isCpacModel(entry));
|
|
188
|
+
if (models.length > 0)
|
|
189
|
+
doc.models = models;
|
|
190
|
+
else
|
|
191
|
+
delete doc.models;
|
|
192
|
+
return Buffer.from(`${JSON.stringify(doc, null, 2)}\n`);
|
|
193
|
+
}
|
|
194
|
+
export async function uninstallCodebuddyConfig(config) {
|
|
195
|
+
const target = codebuddyModelsPath();
|
|
196
|
+
const recorded = readState(config.state_dir)?.agents?.codebuddy;
|
|
197
|
+
if (!recorded && !isCodebuddyConfigInstalled())
|
|
198
|
+
throw new CPACError("CodeBuddy CPAC models are not installed");
|
|
199
|
+
restoreOwnedConfig(config.state_dir, "codebuddy", target, stripCodebuddyManaged);
|
|
200
|
+
const { stopProxy } = dropAgent(config.state_dir, "codebuddy");
|
|
201
|
+
if (stopProxy)
|
|
202
|
+
await stopProxyProcess(stopProxy);
|
|
203
|
+
console.log(`Removed CPAC models from CodeBuddy: ${target}`);
|
|
204
|
+
}
|
package/package.json
CHANGED
package/dist/targets/cline.js
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
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
|
-
}
|