@yhong91/cpac 0.1.40 → 0.1.42

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`
13
+ - 已安装需要使用的 agent:`codex`、`claude`、`pi`、`kimi`、`grok`、`zed` 或 `hermes`
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)
40
+ cpac install --all # 注入所有检测到的目标(codex/pi/kimi/grok/zed/hermes
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` 在 PATH 上)。向用户索取 CPA API key。
64
+ 1. **检查前置**:`node --version` ≥ 20;确认用户需要哪些 agent(`codex`/`claude`/`pi`/`kimi`/`grok`/`zed`/`hermes` 在 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> [args...]`(所有原生参数如 `-c` 等原样透明传递)
93
+ - 启动:`cpac <codex|claude|kimi|grok|pi|zed|hermes> [args...]`(所有原生参数如 `-c` 等原样透明传递)
94
94
  - `setup` / `--setup`:配置 Codex/Claude,然后启动
95
95
  - `clear`:还原原生配置
96
96
  - 只写配置、不启动:`cpac setup [claude|codex]`;查看当前:`cpac setup`
97
97
 
98
- 2. **已安装后升级**:npm 发布新版本后,等 publish 成功再执行 `cpac upgrade`。它会按需 `npm install -g`,再对已接入的 Codex / Pi / Kimi / Grok / Zed 做 `sync`。不要拆成 `npm install -g @yhong91/cpac` 再手动 `cpac sync`。只刷新配置、不升 CLI 时才用 `cpac sync`。
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>` 恢复原配置。
100
+ 失败处理:任何一步报错时先跑 `cpac status`;密钥未配置回到步骤 2;注入后异常用 `cpac <agent> clear` 或 `cpac restore <codex|pi|kimi|grok|zed|hermes>` 恢复原配置。
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> | --all Detach injected agents
117
+ cpac restore <codex|pi|kimi|grok|zed|hermes> | --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` 五个持久注入目标统一管理(claude 是临时接管,用 `cpac claude` 启动,不属于 install 目标):
165
+ 参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok`、`zed`、`hermes` 六个持久注入目标统一管理(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...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]
171
- cpac sync [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]
172
- cpac restore <codex|pi|kimi|grok|zed...> | --all [--dry-run]
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]
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` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
181
- - `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi 按 CLI 版本决定写入 packages / `pi update` / 跳过,并清理旧的 `extensions/pi-cpac.ts`、Kimi / Grok / Zed 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `cpac install codex` 有效。
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 按 CLI 版本决定写入 packages / `pi update` / 跳过,并清理旧的 `extensions/pi-cpac.ts`、Kimi / Grok / Zed / Hermes 配置重写),用来在 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 执行 `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 执行 `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/pi 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。所有 target 的 restore 都按 Codex 规则:注入后记下文件 hash;相对注入未改则逐字节还原备份(没有备份则删除我们创建的文件);注入后改过则只剥 CPAC 字段,用户改动留下。没有其它需要代理的 agent 时才关闭代理。
256
+ `config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok/zed/hermes/pi 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。所有 target 的 restore 都按 Codex 规则:注入后记下文件 hash;相对注入未改则逐字节还原备份(没有备份则删除我们创建的文件);注入后改过则只剥 CPAC 字段,用户改动留下。没有其它需要代理的 agent 时才关闭代理。
257
257
 
258
- loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac kimi` / `cpac grok` / `cpac zed` 即可恢复。
258
+ loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` 即可恢复。
259
259
 
260
260
  默认路径:
261
261
 
@@ -267,6 +267,7 @@ CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录
267
267
  Kimi ~/.cpac/kimi/config.toml
268
268
  Grok ~/.cpac/grok/config.toml
269
269
  Zed ~/.cpac/zed/settings.json
270
+ Hermes ~/.cpac/hermes/config.yaml
270
271
  Pi ~/.cpac/pi/settings.json
271
272
  restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
272
273
  ```
@@ -309,7 +310,7 @@ cpac install zed
309
310
  cpac zed
310
311
  ```
311
312
 
312
- 它在 `~/.config/zed/settings.json` 的 `language_models.openai_compatible.cpac` 写入 `api_url`(指向共享 loopback 代理)和 CPA 目录里的 `available_models`。模型走 Responses API(`chat_completions: false`),与 Pi / Grok 一致。密钥不写进 settings:Zed 读取 `CPAC_API_KEY`(任意非空即可,loopback 会换成真正的 `CPA_API_KEY`),也可在 Agent Settings 里为 `cpac` provider 填一次。卸载:
313
+ 它在 `~/.config/zed/settings.json` 的 `language_models.openai_compatible.cpac` 写入 `api_url`(指向共享 loopback 代理)和 CPA 目录里的 `available_models`。模型走 Responses API(`chat_completions: false`),与 Pi / Grok 一致。密钥不写进 settings:`cpac zed` 会注入占位 `CPAC_API_KEY`;从 Dock 打开 Zed 时在 Agent Settings 里给 `cpac` provider 填任意非空值(loopback 会换成真正的 `CPA_API_KEY`)。这是 Zed 内置 Agent 的 OpenAI-compatible provider,不是 Codex ACP(`codex-acp`)。卸载:
313
314
 
314
315
  ```bash
315
316
  cpac restore zed
@@ -317,6 +318,23 @@ cpac restore zed
317
318
 
318
319
  配置目录尊重 `ZED_CONFIG_DIR`(否则 `$XDG_CONFIG_HOME/zed` 或 `~/.config/zed`)。首次写入前若 `settings.json` 已存在,会在 `state_dir/zed/settings.json` 保留原始快照。
319
320
 
321
+ ### Hermes Agent
322
+
323
+ 写入 CPA 的自定义 OpenAI-compatible provider 到 Hermes:
324
+
325
+ ```bash
326
+ cpac install hermes
327
+ cpac hermes
328
+ ```
329
+
330
+ 它在 `~/.hermes/config.yaml` 的 `providers.cpac` 写入 loopback `base_url`/`api`、占位 `api_key`、`api_mode: responses`,以及 CPA catalog 的 slug 列表(`discover_models: false`)。不改当前默认模型;切换用 `hermes model` 或 `/model`。密钥由 loopback 注入,不写进 `.env`。卸载:
331
+
332
+ ```bash
333
+ cpac restore hermes
334
+ ```
335
+
336
+ 配置目录尊重 `HERMES_HOME`(Windows 默认 `%LOCALAPPDATA%\hermes`)。首次写入前若 `config.yaml` 已存在,会在 `state_dir/hermes/config.yaml` 保留原始快照。
337
+
320
338
  ## 环境变量
321
339
 
322
340
  | 变量 | 默认值 | 作用 |
@@ -326,6 +344,7 @@ cpac restore zed
326
344
  | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `settings.json` 写入位置) |
327
345
  | `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
328
346
  | `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
347
+ | `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
329
348
  | `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
330
349
  | `CODEX_HOME` | `~/.codex` | Codex home 目录 |
331
350
 
package/dist/agents.js CHANGED
@@ -9,6 +9,7 @@ import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uni
9
9
  import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
10
10
  import { PI_PACKAGE, ensurePiExtension, isPiExtensionInstalled, piAgentDir, piSettingsPath, uninstallPiExtension, } from "./targets/pi.js";
11
11
  import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
12
+ import { hermesConfigPath, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
12
13
  import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, tabWizard, } from "./util.js";
13
14
  const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
14
15
  const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
@@ -133,7 +134,10 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
133
134
  }
134
135
  }
135
136
  return await new Promise((resolvePromise, rejectPromise) => {
136
- const child = spawn(executable, args, { stdio: "inherit" });
137
+ const env = targetId === "zed"
138
+ ? { ...process.env, CPAC_API_KEY: process.env.CPAC_API_KEY || "cpac" }
139
+ : undefined;
140
+ const child = spawn(executable, args, { stdio: "inherit", env });
137
141
  child.once("error", (error) => {
138
142
  rejectPromise(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
139
143
  ? `${executable} not found`
@@ -294,6 +298,27 @@ const TARGETS = [
294
298
  await uninstallZedConfig(config);
295
299
  },
296
300
  },
301
+ {
302
+ id: "hermes",
303
+ home: () => hermesHome(),
304
+ detected: () => binaryOnPath("hermes") || existsSync(hermesHome()),
305
+ installed: () => isHermesConfigInstalled(),
306
+ version: () => detectClientVersion("hermes"),
307
+ install: async (config, dryRun) => {
308
+ if (dryRun) {
309
+ console.log(`would write CPA provider: ${hermesConfigPath()}`);
310
+ return;
311
+ }
312
+ await installHermesConfig(config);
313
+ },
314
+ uninstall: async (config, dryRun) => {
315
+ if (dryRun) {
316
+ console.log(`would remove CPA provider: ${hermesConfigPath()}`);
317
+ return;
318
+ }
319
+ await uninstallHermesConfig(config);
320
+ },
321
+ },
297
322
  ];
298
323
  export async function detectTargets(config) {
299
324
  return TARGETS.map((target) => ({
package/dist/config.js CHANGED
@@ -22,7 +22,7 @@ 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"];
25
+ export const AGENT_IDS = ["codex", "pi", "kimi", "grok", "zed", "hermes"];
26
26
  const AGENT_ID_SET = new Set(AGENT_IDS);
27
27
  const STATE_KEYS = new Set([
28
28
  "config_path",
@@ -525,7 +525,7 @@ export function needsProxy(state) {
525
525
  if (state.config_path)
526
526
  return true;
527
527
  const agents = state.agents ?? {};
528
- return Boolean(agents.codex || agents.kimi || agents.grok || agents.zed);
528
+ return Boolean(agents.codex || agents.kimi || agents.grok || agents.zed || agents.hermes);
529
529
  }
530
530
  export function saveState(stateDir, state) {
531
531
  atomicWrite(join(stateDir, "state.json"), Buffer.from(`${JSON.stringify(state, null, 2)}\n`));
package/dist/cpac.js CHANGED
@@ -10,6 +10,7 @@ import { isGrokConfigInstalled } from "./targets/grok.js";
10
10
  import { isKimiConfigInstalled } from "./targets/kimi.js";
11
11
  import { isPiExtensionInstalled } from "./targets/pi.js";
12
12
  import { isZedConfigInstalled } from "./targets/zed.js";
13
+ import { isHermesConfigInstalled } from "./targets/hermes.js";
13
14
  import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, } from "./config.js";
14
15
  import { proxyIsHealthy, runProxyChild } from "./proxy.js";
15
16
  import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
@@ -23,6 +24,7 @@ export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "
23
24
  export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
24
25
  export { ensurePiExtension, installPiExtension, isPiExtensionInstalled, piPackageAction, piPackageVersion, uninstallPiExtension, } from "./targets/pi.js";
25
26
  export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
27
+ export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
26
28
  export async function status(config) {
27
29
  const apiKey = process.env[config.api_key_env]?.trim();
28
30
  const keyConfigured = !!apiKey;
@@ -76,6 +78,8 @@ export async function status(config) {
76
78
  console.log(`grok: ${keyConfigured ? (grokInstalled ? "ready" : "config not installed; run: cpac install grok") : "not configured"}`);
77
79
  const zedInstalled = isZedConfigInstalled();
78
80
  console.log(`zed: ${keyConfigured ? (zedInstalled ? "ready" : "config not installed; run: cpac install zed") : "not configured"}`);
81
+ const hermesInstalled = isHermesConfigInstalled();
82
+ console.log(`hermes: ${keyConfigured ? (hermesInstalled ? "ready" : "config not installed; run: cpac install hermes") : "not configured"}`);
79
83
  if (!state)
80
84
  return 1;
81
85
  const proxy = stateProxy(state);
@@ -114,10 +118,10 @@ async function listModels(config, json) {
114
118
  return 0;
115
119
  }
116
120
  async function guide(config) {
117
- 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, 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)\n cpac restore <codex|pi|kimi|grok|zed> | --all Detach injected agents
121
+ 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
118
122
  cpac detect Show supported targets and install status
119
- cpac install [codex|pi|kimi|grok|zed...] Install CPA into detected targets, or named agents
120
- cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed injections from CPA
123
+ cpac install [codex|pi|kimi|grok|zed|hermes...] Install CPA into detected targets, or named agents
124
+ cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed/Hermes injections from CPA
121
125
  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`);
122
126
  if (process.env[config.api_key_env]?.trim())
123
127
  return 0;
@@ -128,42 +132,77 @@ async function guide(config) {
128
132
  console.log(`\nSaved ${config.api_key_env} to ${profile}. Open a new terminal or run:\n source ${profile}`);
129
133
  return 0;
130
134
  }
135
+ function helpRow(left, desc, tag = "", col = 34) {
136
+ const gap = Math.max(2, col - left.length);
137
+ const body = `${left}${" ".repeat(gap)}${desc}`;
138
+ if (!tag)
139
+ return ` ${body}`;
140
+ return ` ${body}${" ".repeat(Math.max(2, 70 - body.length))}${tag}`;
141
+ }
131
142
  function usage() {
143
+ const cmd = (left, desc, tag = "") => helpRow(left, desc, tag);
144
+ const opt = (left, desc, tag = "") => helpRow(left, desc, tag, 18);
132
145
  return [
133
- "Usage: cpac",
134
- " cpac <codex|kimi|grok|pi|zed|claude> [args...]",
135
- " setup|--setup configure Codex/Claude, then launch",
136
- " clear restore native config",
137
- " cpac setup Show current Codex/Claude setup",
138
- " claude [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset]",
139
- " codex [--v2_off] [--v2_models] [--max_context]",
140
- " cpac restore <codex|pi|kimi|grok|zed...> | --all [--dry-run]",
141
- " cpac status",
142
- " cpac models [--json]",
143
- " cpac detect [--json]",
144
- " cpac install [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]",
145
- " cpac sync [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]",
146
- " cpac uninstall [--dry-run]",
147
- " cpac upgrade [--check]",
148
- " cpac -v | --version",
146
+ "Usage: cpac [command]",
147
+ "",
148
+ "Commands:",
149
+ cmd("cpac", "first-run guide and key setup", "[default]"),
150
+ cmd("cpac <agent> [args...]", "launch an agent through CPA"),
151
+ cmd("cpac <agent> setup", "configure Codex/Claude, then launch"),
152
+ cmd("cpac <agent> clear", "restore native config"),
153
+ cmd("cpac setup [claude|codex]", "show or write Codex/Claude setup"),
154
+ cmd("cpac status", "show agent support status"),
155
+ cmd("cpac models", "list remote CPA catalog models"),
156
+ cmd("cpac detect", "show supported targets and install status"),
157
+ cmd("cpac install [agent...]", "install CPA into detected or named agents"),
158
+ cmd("cpac sync [agent...]", "refresh installed injections from CPA"),
159
+ cmd("cpac restore <agent...>", "detach injected agents"),
160
+ cmd("cpac uninstall", "detach all agents, then remove the cpac package"),
161
+ cmd("cpac upgrade", "update cpac from npm and sync installed agents"),
162
+ "",
163
+ "Positionals:",
164
+ helpRow("agent", "codex, kimi, grok, pi, zed, hermes, or claude (claude is launch-only)", "", 18),
165
+ "",
166
+ "Options:",
167
+ opt("-h, --help", "show help", "[boolean]"),
168
+ opt("-v, --version", "show version number", "[boolean]"),
169
+ opt(" --config", "path to cpac config.json", "[string]"),
149
170
  "",
150
- "launch an agent with `cpac <agent> [args...]` (remaining args forwarded).",
151
- "`cpac <agent> setup` configures then launches; `cpac setup [claude|codex]` writes settings only.",
171
+ "Command options:",
172
+ opt("--json", "JSON output", "[models, detect]"),
173
+ opt("--all", "every detected or installed agent", "[install, sync, restore]"),
174
+ opt("--dry-run", "print actions without writing", "[install, sync, restore, uninstall]"),
175
+ opt("--force", "allow reinstall of already-injected agents", "[install]"),
176
+ opt("--check", "report upgrade status without installing", "[upgrade]"),
177
+ opt("--v2_off", "disable Codex multi-agent v2", "[install, sync, setup codex]"),
178
+ opt("--v2_models", "pick spawn_agent models interactively", "[install, sync, setup codex]"),
179
+ opt("--max_context", "lift Codex context windows to measured max", "[install, sync, setup codex]"),
152
180
  ].join("\n");
153
181
  }
154
182
  function setupUsage() {
183
+ const cmd = (left, desc, tag = "") => helpRow(left, desc, tag);
184
+ const opt = (left, desc, tag = "") => helpRow(left, desc, tag, 18);
155
185
  return [
156
- "Usage: cpac setup Show current Codex/Claude setup",
157
- " claude [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset]",
158
- " codex [--v2_off] [--v2_models] [--max_context]",
186
+ "Usage: cpac setup [claude|codex]",
159
187
  "",
160
- "Claude slots (saved to config, applied on every `cpac claude`):",
161
- " --opus heavy reasoning / planning fallback (ANTHROPIC_DEFAULT_OPUS_MODEL)",
162
- " --sonnet main conversation default (ANTHROPIC_DEFAULT_SONNET_MODEL)",
163
- " --haiku high-frequency small tasks (ANTHROPIC_DEFAULT_HAIKU_MODEL)",
188
+ "Commands:",
189
+ cmd("cpac setup", "show current Codex/Claude setup", "[default]"),
190
+ cmd("cpac setup claude", "configure Claude model slots"),
191
+ cmd("cpac setup codex", "configure Codex injection"),
164
192
  "",
165
- "A slot without a value opens an arrow-key picker; --reset clears overrides.",
166
- "No flags on a TTY opens the wizard. Does not launch; use `cpac claude setup` / `cpac codex setup` to start.",
193
+ "Claude options:",
194
+ opt("--opus [M]", "opus slot (omit M to pick)", "[string]"),
195
+ opt("--sonnet [M]", "sonnet slot (omit M to pick)", "[string]"),
196
+ opt("--haiku [M]", "haiku slot (omit M to pick)", "[string]"),
197
+ opt("--reset", "clear Claude slot overrides", "[boolean]"),
198
+ "",
199
+ "Codex options:",
200
+ opt("--v2_off", "disable Codex multi-agent v2", "[boolean]"),
201
+ opt("--v2_models", "pick spawn_agent models interactively", "[boolean]"),
202
+ opt("--max_context", "lift Codex context windows to measured max", "[boolean]"),
203
+ "",
204
+ "A slot without a value opens an arrow-key picker. No flags on a TTY opens the wizard.",
205
+ "Does not launch; use `cpac claude setup` / `cpac codex setup` to start.",
167
206
  ].join("\n");
168
207
  }
169
208
  function printSetup(config, configPath) {
@@ -205,6 +244,10 @@ function parseArgs(args) {
205
244
  };
206
245
  for (let index = 1; index < args.length; index++) {
207
246
  const arg = args[index];
247
+ if (arg === "-h" || arg === "--help") {
248
+ console.log(usage());
249
+ return null;
250
+ }
208
251
  if (arg === "--json")
209
252
  parsed.json = true;
210
253
  else if (arg === "--all")
@@ -257,7 +300,7 @@ function parseArgs(args) {
257
300
  }
258
301
  return parsed;
259
302
  }
260
- if (["codex", "kimi", "grok", "pi", "zed", "claude"].includes(args[0])) {
303
+ if (["codex", "kimi", "grok", "pi", "zed", "hermes", "claude"].includes(args[0])) {
261
304
  const targetId = args[0];
262
305
  let configPath = defaultConfigPath();
263
306
  const raw = args.slice(1);
@@ -605,7 +648,7 @@ export async function main(args = process.argv.slice(2)) {
605
648
  });
606
649
  if (parsed.command === "uninstall") {
607
650
  if (parsed.targets.length || parsed.all) {
608
- throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed> | --all");
651
+ throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed|hermes> | --all");
609
652
  }
610
653
  return await runUninstall(config, { dryRun: parsed.dryRun });
611
654
  }
@@ -657,7 +700,8 @@ export async function main(args = process.argv.slice(2)) {
657
700
  if (parsed.command === "kimi" ||
658
701
  parsed.command === "grok" ||
659
702
  parsed.command === "pi" ||
660
- parsed.command === "zed") {
703
+ parsed.command === "zed" ||
704
+ parsed.command === "hermes") {
661
705
  return await runTargetLauncher(config, parsed.command, parsed.args);
662
706
  }
663
707
  return await status(config);
package/dist/proxy.js CHANGED
@@ -329,7 +329,7 @@ export async function runProxy(config) {
329
329
  throw new CPACError("no active CPAC state; run cpac inject or install an agent first");
330
330
  const recorded = stateProxy(state);
331
331
  if (!recorded) {
332
- throw new CPACError("state has no loopback proxy; run cpac inject or install kimi/grok/zed first");
332
+ throw new CPACError("state has no loopback proxy; run cpac inject or install kimi/grok/zed/hermes first");
333
333
  }
334
334
  if (await proxyIsHealthy(recorded)) {
335
335
  throw new CPACError(`loopback proxy is already running on 127.0.0.1:${recorded.port}`);
@@ -0,0 +1,237 @@
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, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
5
+ import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
6
+ import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, resolveApiKey, } from "../util.js";
7
+ export const HERMES_PROVIDER_ID = "cpac";
8
+ export function hermesHome() {
9
+ const override = process.env.HERMES_HOME?.trim();
10
+ if (override)
11
+ return expandUserPath(override);
12
+ if (process.platform === "win32") {
13
+ const local = process.env.LOCALAPPDATA?.trim();
14
+ return join(local || join(homedir(), "AppData", "Local"), "hermes");
15
+ }
16
+ return join(homedir(), ".hermes");
17
+ }
18
+ export function hermesConfigPath() {
19
+ return join(hermesHome(), "config.yaml");
20
+ }
21
+ function lineIndent(line) {
22
+ const match = line.match(/^( *)/);
23
+ return match ? match[1].length : 0;
24
+ }
25
+ function mappingEnd(lines, startIdx, keyIndent) {
26
+ let i = startIdx + 1;
27
+ while (i < lines.length) {
28
+ const line = lines[i];
29
+ if (line.trim() === "") {
30
+ i += 1;
31
+ continue;
32
+ }
33
+ const indent = lineIndent(line);
34
+ if (indent > keyIndent) {
35
+ i += 1;
36
+ continue;
37
+ }
38
+ break;
39
+ }
40
+ while (i > startIdx + 1 && lines[i - 1].trim() === "")
41
+ i -= 1;
42
+ return i;
43
+ }
44
+ function providersRange(lines) {
45
+ const idx = lines.findIndex((line) => /^providers:\s*(?:#.*)?$/.test(line));
46
+ if (idx === -1)
47
+ return undefined;
48
+ return { idx, end: mappingEnd(lines, idx, 0) };
49
+ }
50
+ function childIndent(lines, mapStart, mapEnd, parentIndent) {
51
+ for (let i = mapStart + 1; i < mapEnd; i++) {
52
+ const line = lines[i];
53
+ if (!line.trim() || line.trimStart().startsWith("#"))
54
+ continue;
55
+ const indent = lineIndent(line);
56
+ if (indent > parentIndent)
57
+ return indent;
58
+ }
59
+ return parentIndent + 2;
60
+ }
61
+ function findCpacChild(lines, mapStart, mapEnd, indent) {
62
+ const prefix = `${" ".repeat(indent)}${HERMES_PROVIDER_ID}:`;
63
+ for (let i = mapStart + 1; i < mapEnd; i++) {
64
+ if (lines[i] === prefix ||
65
+ lines[i].startsWith(`${prefix} `) ||
66
+ lines[i].startsWith(`${prefix}\t`)) {
67
+ return { idx: i, end: mappingEnd(lines, i, indent) };
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ function yamlKey(key) {
73
+ if (/^[A-Za-z_][\w.-]*$/.test(key))
74
+ return key;
75
+ return JSON.stringify(key);
76
+ }
77
+ function cpacChildLines(port, rows, indent) {
78
+ const pad = (n) => " ".repeat(n);
79
+ const lines = [
80
+ `${pad(indent)}${HERMES_PROVIDER_ID}:`,
81
+ `${pad(indent + 2)}base_url: ${JSON.stringify(`http://[IP]:${port}/v1`)}`,
82
+ `${pad(indent + 2)}api: ${JSON.stringify(`http://[IP]:${port}/v1`)}`,
83
+ `${pad(indent + 2)}api_key: cpac`,
84
+ `${pad(indent + 2)}api_mode: responses`,
85
+ `${pad(indent + 2)}discover_models: false`,
86
+ `${pad(indent + 2)}models:`,
87
+ ];
88
+ let count = 0;
89
+ for (const row of rows) {
90
+ const slug = catalogModelId(row);
91
+ if (!slug)
92
+ continue;
93
+ const context = typeof row.context_window === "number" && row.context_window > 0
94
+ ? Math.floor(row.context_window)
95
+ : 200000;
96
+ lines.push(`${pad(indent + 4)}${yamlKey(slug)}:`);
97
+ lines.push(`${pad(indent + 6)}context_length: ${context}`);
98
+ count += 1;
99
+ }
100
+ if (count === 0)
101
+ throw new CPACError("CPA catalog contains no models");
102
+ return lines;
103
+ }
104
+ function mappingIsEmpty(lines, start, end, keyIndent) {
105
+ for (let i = start + 1; i < end; i++) {
106
+ const line = lines[i];
107
+ if (!line.trim() || line.trimStart().startsWith("#"))
108
+ continue;
109
+ if (lineIndent(line) > keyIndent)
110
+ return false;
111
+ }
112
+ return true;
113
+ }
114
+ function hasProvidersCpac(content) {
115
+ const lines = content.split(/\r?\n/);
116
+ const range = providersRange(lines);
117
+ if (!range)
118
+ return false;
119
+ const indent = childIndent(lines, range.idx, range.end, 0);
120
+ return Boolean(findCpacChild(lines, range.idx, range.end, indent));
121
+ }
122
+ export function isHermesConfigInstalled() {
123
+ try {
124
+ return hasProvidersCpac(readFileSync(hermesConfigPath(), "utf8"));
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ function stripProvidersCpac(content) {
131
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
132
+ const lines = content.split(/\r?\n/);
133
+ const range = providersRange(lines);
134
+ if (!range)
135
+ return content;
136
+ const indent = childIndent(lines, range.idx, range.end, 0);
137
+ const child = findCpacChild(lines, range.idx, range.end, indent);
138
+ if (!child)
139
+ return content;
140
+ const next = [...lines.slice(0, child.idx), ...lines.slice(child.end)];
141
+ const providersEnd = mappingEnd(next, range.idx, 0);
142
+ if (mappingIsEmpty(next, range.idx, providersEnd, 0)) {
143
+ next.splice(range.idx, providersEnd - range.idx);
144
+ }
145
+ return next.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
146
+ }
147
+ function upsertProvidersCpac(content, port, rows) {
148
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
149
+ const stripped = stripProvidersCpac(content);
150
+ const lines = stripped === "" ? [] : stripped.split(/\r?\n/);
151
+ while (lines.length > 0 && lines[lines.length - 1] === "")
152
+ lines.pop();
153
+ const range = providersRange(lines);
154
+ if (!range) {
155
+ const block = ["providers:", ...cpacChildLines(port, rows, 2)];
156
+ if (lines.length === 0)
157
+ return `${block.join(eol)}${eol}`;
158
+ return `${lines.join(eol)}${eol}${block.join(eol)}${eol}`;
159
+ }
160
+ const inline = lines[range.idx]
161
+ .replace(/^providers:\s*/, "")
162
+ .replace(/#.*$/, "")
163
+ .trim();
164
+ if (inline && inline !== "{}" && inline !== "null") {
165
+ throw new CPACError("Hermes providers: must be a block mapping");
166
+ }
167
+ if (inline === "{}" || inline === "null")
168
+ lines[range.idx] = "providers:";
169
+ const end = mappingEnd(lines, range.idx, 0);
170
+ const indent = childIndent(lines, range.idx, end, 0);
171
+ const child = findCpacChild(lines, range.idx, end, indent);
172
+ const replacement = cpacChildLines(port, rows, indent);
173
+ if (child) {
174
+ return [
175
+ ...lines.slice(0, child.idx),
176
+ ...replacement,
177
+ ...lines.slice(child.end),
178
+ ].join(eol);
179
+ }
180
+ return [
181
+ ...lines.slice(0, range.idx + 1),
182
+ ...replacement,
183
+ ...lines.slice(range.idx + 1),
184
+ ].join(eol);
185
+ }
186
+ function writeHermesProvider(path, port, rows) {
187
+ let content = existsSync(path) ? readFileSync(path, "utf8") : "";
188
+ content = upsertProvidersCpac(content, port, rows);
189
+ if (!content.endsWith("\n"))
190
+ content += "\n";
191
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
192
+ const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
193
+ atomicWrite(path, Buffer.from(content), mode);
194
+ }
195
+ export async function installHermesConfig(config) {
196
+ const apiKey = await resolveApiKey(config.api_key_env);
197
+ const catalog = await fetchCatalog(config.cpa_url, apiKey);
198
+ let document;
199
+ try {
200
+ document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
201
+ }
202
+ catch {
203
+ throw new CPACError("invalid CPA catalog");
204
+ }
205
+ const rows = catalogModelRows(document) ?? [];
206
+ if (rows.length === 0)
207
+ throw new CPACError("CPA catalog contains no models");
208
+ const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
209
+ const target = hermesConfigPath();
210
+ const hadFile = existsSync(target);
211
+ try {
212
+ ensureCpacBackup(config.state_dir, "hermes", target, !isHermesConfigInstalled());
213
+ writeHermesProvider(target, proxy.port, rows);
214
+ recordWrittenConfig(config.state_dir, "hermes", target, {
215
+ id: proxy.id,
216
+ pid: proxy.pid,
217
+ port: proxy.port,
218
+ fingerprint,
219
+ }, hadFile);
220
+ }
221
+ catch (error) {
222
+ if (started)
223
+ await stopProxyProcess(proxy);
224
+ throw error;
225
+ }
226
+ console.log(`Installed Hermes provider "${HERMES_PROVIDER_ID}": ${target}`);
227
+ }
228
+ export async function uninstallHermesConfig(config) {
229
+ const target = hermesConfigPath();
230
+ if (!isHermesConfigInstalled())
231
+ throw new CPACError("Hermes CPAC provider is not installed");
232
+ restoreOwnedConfig(config.state_dir, "hermes", target, (current) => Buffer.from(stripProvidersCpac(new TextDecoder("utf-8").decode(current))));
233
+ const { stopProxy } = dropAgent(config.state_dir, "hermes");
234
+ if (stopProxy)
235
+ await stopProxyProcess(stopProxy);
236
+ console.log(`Removed Hermes provider "${HERMES_PROVIDER_ID}": ${target}`);
237
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {