@yhong91/cpac 0.1.49 → 0.2.1

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 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` 或 `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` 六个持久注入目标统一管理(claude 是临时接管,用 `cpac claude` 启动,不属于 install 目标):
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
  ```
@@ -335,16 +336,55 @@ 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
+
366
+ ### 手动打开 5h 窗口
367
+
368
+ 本机手动发一条最小请求,用来激活当前 5h 额度。不定时,也不查配额。定时仍在 VPS 的 `scripts/quota-heartbeat.py`。
369
+
370
+ ```bash
371
+ cpac ping codex # 目录里匹配 gpt-*-luna 的一条
372
+ cpac ping agy # gemini-3.8-flash-high 和 claude-sonnet-4-6,各一条
373
+ ```
374
+
375
+ 用 `CPA_API_KEY`。日志只打模型、HTTP 状态和不含 key 的 `x-cpa-*` trace。CPA 按账号轮询,一次只会落在一个账号上。
376
+
338
377
  ## 环境变量
339
378
 
340
379
  | 变量 | 默认值 | 作用 |
341
380
  | --- | --- | --- |
342
- | `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
381
+ | `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件;`cpac ping` 也用它 |
343
382
  | `CPA_BASE_URL` | `http://124.223.178.52:8317` | 覆盖内置 CPA 地址 |
344
383
  | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `models.json` 写入位置) |
345
384
  | `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
346
385
  | `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
347
386
  | `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
387
+ | `CLINE_DATA_DIR` | `~/.cline/data` | 覆盖 Cline 数据目录(影响 `cpac install cline` 写入位置;仅 CLI 生效,Desktop 读默认路径) |
348
388
  | `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
349
389
  | `CODEX_HOME` | `~/.codex` | Codex home 目录 |
350
390
 
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 = ["codex", "pi", "kimi", "grok", "zed", "hermes"];
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,7 +11,9 @@ 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";
16
+ import { runPing } from "./ping.js";
15
17
  import { proxyIsHealthy, runProxyChild } from "./proxy.js";
16
18
  import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
17
19
  export { PROVIDER, loadConfig, saveSpawnModels, saveCodexSetup, liftContextWindows, reorderCatalog, sanitizeCatalogModalities, stampMultiAgentVersion, catalogMaxContextLive, readState, stateProxy, } from "./config.js";
@@ -26,6 +28,7 @@ export { installPiConfig, isPiConfigInstalled, uninstallPiConfig, PI_PROVIDER_ID
26
28
  export { compactModelPrices, ensureModelPrices, lookupModelCost, resetModelPrices, } from "./pricing.js";
27
29
  export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
28
30
  export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
31
+ export { CLINE_PROVIDER_ID, installClineConfig, isClineConfigInstalled, uninstallClineConfig, } from "./targets/cline.js";
29
32
  export async function status(config) {
30
33
  const apiKey = process.env[config.api_key_env]?.trim();
31
34
  const keyConfigured = !!apiKey;
@@ -81,6 +84,8 @@ export async function status(config) {
81
84
  console.log(`zed: ${keyConfigured ? (zedInstalled ? "ready" : "config not installed; run: cpac install zed") : "not configured"}`);
82
85
  const hermesInstalled = isHermesConfigInstalled();
83
86
  console.log(`hermes: ${keyConfigured ? (hermesInstalled ? "ready" : "config not installed; run: cpac install hermes") : "not configured"}`);
87
+ const clineInstalled = isClineConfigInstalled();
88
+ console.log(`cline: ${keyConfigured ? (clineInstalled ? "ready" : "config not installed; run: cpac install cline") : "not configured"}`);
84
89
  if (!state)
85
90
  return 1;
86
91
  const proxy = stateProxy(state);
@@ -98,7 +103,13 @@ async function listModels(config, json) {
98
103
  console.log(slug);
99
104
  return 0;
100
105
  }
101
- const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
106
+ let document;
107
+ try {
108
+ document = JSON.parse(new TextDecoder().decode(catalog.bytes));
109
+ }
110
+ catch {
111
+ throw new CPACError("CPA catalog is not valid JSON");
112
+ }
102
113
  const models = (catalogModelRows(document) ?? []).flatMap((row) => {
103
114
  const slug = catalogModelId(row);
104
115
  if (!slug)
@@ -119,11 +130,11 @@ async function listModels(config, json) {
119
130
  return 0;
120
131
  }
121
132
  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
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, 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
134
  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
126
- 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`);
135
+ cpac install [codex|pi|kimi|grok|zed|hermes|cline...] Install CPA into detected targets, or named agents
136
+ cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed/Hermes/Cline injections from CPA
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`);
127
138
  if (process.env[config.api_key_env]?.trim())
128
139
  return 0;
129
140
  const apiKey = await promptSecret(config.api_key_env);
@@ -160,9 +171,10 @@ function usage() {
160
171
  cmd("cpac restore <agent...>", "detach injected agents"),
161
172
  cmd("cpac uninstall", "detach all agents, then remove the cpac package"),
162
173
  cmd("cpac upgrade", "update cpac from npm and sync installed agents"),
174
+ cmd("cpac ping <codex|agy>", "send one request to open that 5h window"),
163
175
  "",
164
176
  "Positionals:",
165
- helpRow("agent", "codex, kimi, grok, pi, zed, hermes, or claude (claude is launch-only)", "", 18),
177
+ helpRow("agent", "codex, kimi, grok, pi, zed, hermes, cline, or claude (claude is launch-only)", "", 18),
166
178
  "",
167
179
  "Options:",
168
180
  opt("-h, --help", "show help", "[boolean]"),
@@ -223,6 +235,31 @@ function parseArgs(args) {
223
235
  if (args.length === 0) {
224
236
  return { command: "guide", configPath: defaultConfigPath() };
225
237
  }
238
+ if (args[0] === "ping") {
239
+ let configPath = defaultConfigPath();
240
+ let target = "";
241
+ for (let index = 1; index < args.length; index++) {
242
+ const arg = args[index];
243
+ if (arg === "-h" || arg === "--help") {
244
+ console.log(usage());
245
+ return null;
246
+ }
247
+ if (arg === "--config") {
248
+ const value = args[++index];
249
+ if (!value)
250
+ throw new CPACError("--config requires a path");
251
+ configPath = resolve(expandUserPath(value));
252
+ }
253
+ else if (!arg.startsWith("-") && !target)
254
+ target = arg;
255
+ else
256
+ throw new CPACError(`unknown option: ${arg}`);
257
+ }
258
+ if (target !== "codex" && target !== "agy") {
259
+ throw new CPACError("cpac ping requires codex or agy");
260
+ }
261
+ return { command: "ping", configPath, target };
262
+ }
226
263
  if (args[0] === "detect" ||
227
264
  args[0] === "install" ||
228
265
  args[0] === "sync" ||
@@ -301,7 +338,7 @@ function parseArgs(args) {
301
338
  }
302
339
  return parsed;
303
340
  }
304
- if (["codex", "kimi", "grok", "pi", "zed", "hermes", "claude"].includes(args[0])) {
341
+ if (["codex", "kimi", "grok", "pi", "zed", "hermes", "cline", "claude"].includes(args[0])) {
305
342
  const targetId = args[0];
306
343
  let configPath = defaultConfigPath();
307
344
  const raw = args.slice(1);
@@ -649,7 +686,7 @@ export async function main(args = process.argv.slice(2)) {
649
686
  });
650
687
  if (parsed.command === "uninstall") {
651
688
  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");
689
+ throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed|hermes|cline> | --all");
653
690
  }
654
691
  return await runUninstall(config, { dryRun: parsed.dryRun });
655
692
  }
@@ -659,6 +696,8 @@ export async function main(args = process.argv.slice(2)) {
659
696
  }
660
697
  if (parsed.command === "upgrade")
661
698
  return await runUpgrade(config, parsed.check);
699
+ if (parsed.command === "ping")
700
+ return await runPing(config, parsed.target);
662
701
  if (parsed.command === "claude")
663
702
  return await runClaude(config, parsed.args);
664
703
  if (parsed.command === "claude-config") {
@@ -698,6 +737,14 @@ export async function main(args = process.argv.slice(2)) {
698
737
  }
699
738
  return await runTargetLauncher(launcherConfig, "codex", parsed.args, "codex", { v2Off: parsed.v2Off, maxContext: parsed.maxContext });
700
739
  }
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
+ }
701
748
  if (parsed.command === "kimi" ||
702
749
  parsed.command === "grok" ||
703
750
  parsed.command === "pi" ||
package/dist/ping.js ADDED
@@ -0,0 +1,66 @@
1
+ import { apiBase, catalogSlugs, fetchCatalog } from "./config.js";
2
+ import { CPACError } from "./util.js";
3
+ const LUNA = /^gpt-.+-luna$/;
4
+ export const PING_MODELS = {
5
+ agy: ["gemini-3.8-flash-high", "claude-sonnet-4-6"],
6
+ };
7
+ export function matchLuna(slugs) {
8
+ const model = slugs.filter((slug) => LUNA.test(slug)).sort().at(-1);
9
+ if (!model)
10
+ throw new CPACError("no catalog model matches gpt-*-luna");
11
+ return model;
12
+ }
13
+ const EMAIL_RE = /[A-Za-z0-9._%+\-]+@/g;
14
+ function redact(value) {
15
+ return String(value).replace(EMAIL_RE, "*@");
16
+ }
17
+ export async function runPing(config, target, deps = {}) {
18
+ const env = deps.env ?? process.env;
19
+ const log = deps.log ?? ((line) => console.log(line));
20
+ const fetchImpl = deps.fetch ?? fetch;
21
+ const apiKey = env[config.api_key_env]?.trim();
22
+ if (!apiKey)
23
+ throw new CPACError(`environment variable ${config.api_key_env} is not set`);
24
+ const url = `${apiBase(config.cpa_url)}/chat/completions`;
25
+ const models = target === "codex"
26
+ ? [matchLuna(catalogSlugs((await fetchCatalog(config.cpa_url, apiKey)).bytes))]
27
+ : PING_MODELS[target];
28
+ let failed = 0;
29
+ for (const model of models) {
30
+ let response;
31
+ try {
32
+ response = await fetchImpl(url, {
33
+ method: "POST",
34
+ headers: {
35
+ Accept: "application/json",
36
+ Authorization: `Bearer ${apiKey}`,
37
+ "Content-Type": "application/json",
38
+ },
39
+ body: JSON.stringify({
40
+ model,
41
+ messages: [{ role: "user", content: "hi" }],
42
+ max_tokens: 16,
43
+ stream: false,
44
+ }),
45
+ redirect: "error",
46
+ signal: AbortSignal.timeout(90_000),
47
+ });
48
+ }
49
+ catch (error) {
50
+ log(`${model} failed: ${error instanceof Error ? error.message : String(error)}`);
51
+ failed += 1;
52
+ continue;
53
+ }
54
+ await response.body?.cancel();
55
+ const trace = [];
56
+ response.headers.forEach((value, key) => {
57
+ const name = key.toLowerCase();
58
+ if (name.startsWith("x-cpa-") && !name.includes("key"))
59
+ trace.push(`${name}=${redact(value)}`);
60
+ });
61
+ log(`${model} HTTP ${response.status}${trace.length ? ` ${trace.join(" ")}` : ""}`);
62
+ if (response.status !== 200)
63
+ failed += 1;
64
+ }
65
+ return failed ? 1 : 0;
66
+ }
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) => {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.49",
3
+ "version": "0.2.1",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {