@yhong91/cpac 0.1.39 → 0.1.41
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 +38 -16
- package/dist/agents.js +22 -0
- package/dist/config.js +2 -2
- package/dist/cpac.js +113 -35
- package/dist/proxy.js +1 -1
- package/dist/targets/hermes.js +236 -0
- package/package.json +1 -1
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` 或 `
|
|
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
|
|
|
@@ -113,7 +113,8 @@ Commands:
|
|
|
113
113
|
cpac <agent> [args...] Launch an agent through CPA
|
|
114
114
|
cpac setup Show current Codex/Claude setup
|
|
115
115
|
cpac status Show agent support status
|
|
116
|
-
cpac
|
|
116
|
+
cpac models List remote CPA catalog models
|
|
117
|
+
cpac restore <codex|pi|kimi|grok|zed|hermes> | --all Detach injected agents
|
|
117
118
|
cpac --help Show command usage
|
|
118
119
|
```
|
|
119
120
|
|
|
@@ -161,26 +162,28 @@ cpac --help
|
|
|
161
162
|
|
|
162
163
|
### 统一管理(detect / install / sync / restore / uninstall / upgrade)
|
|
163
164
|
|
|
164
|
-
参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok`、`zed`
|
|
165
|
+
参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok`、`zed`、`hermes` 六个持久注入目标统一管理(claude 是临时接管,用 `cpac claude` 启动,不属于 install 目标):
|
|
165
166
|
|
|
166
167
|
```bash
|
|
167
168
|
cpac detect [--json]
|
|
168
|
-
cpac
|
|
169
|
-
cpac
|
|
170
|
-
cpac
|
|
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]
|
|
171
173
|
cpac uninstall [--dry-run]
|
|
172
174
|
cpac upgrade [--check]
|
|
173
175
|
cpac -v | --version
|
|
174
176
|
```
|
|
175
177
|
|
|
178
|
+
- `models` 从远端 CPA 拉取 rich catalog,默认一行一个 slug;`--json` 输出 `cpa_url` 和模型字段(`slug` / `display_name` / `context_window`)。用来核对 catalog 里有没有某个模型(例如 `*-fast`),不写本地配置。
|
|
176
179
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
177
|
-
- `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok`/`zed` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
178
|
-
- `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` 有效。
|
|
179
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`。
|
|
180
183
|
- `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
|
|
181
184
|
- `restore` 必须带 agent 名或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
|
|
182
185
|
- `uninstall` 先用和 `cpac setup codex` 相同的方向键选择器确认(默认 Cancel),再 `restore --all`,最后 `npm uninstall -g @yhong91/cpac`。不要跟 agent 名;卸载的是 cpac 包本身。非终端取消。`--dry-run` 跳过确认。
|
|
183
|
-
- `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`。
|
|
184
187
|
|
|
185
188
|
### Claude Code
|
|
186
189
|
|
|
@@ -250,9 +253,9 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。注入会删除 `models
|
|
|
250
253
|
|
|
251
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 组合使用会直接报错。
|
|
252
255
|
|
|
253
|
-
`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 时才关闭代理。
|
|
254
257
|
|
|
255
|
-
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` 即可恢复。
|
|
256
259
|
|
|
257
260
|
默认路径:
|
|
258
261
|
|
|
@@ -264,6 +267,7 @@ CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录
|
|
|
264
267
|
Kimi ~/.cpac/kimi/config.toml
|
|
265
268
|
Grok ~/.cpac/grok/config.toml
|
|
266
269
|
Zed ~/.cpac/zed/settings.json
|
|
270
|
+
Hermes ~/.cpac/hermes/config.yaml
|
|
267
271
|
Pi ~/.cpac/pi/settings.json
|
|
268
272
|
restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
|
|
269
273
|
```
|
|
@@ -314,6 +318,23 @@ cpac restore zed
|
|
|
314
318
|
|
|
315
319
|
配置目录尊重 `ZED_CONFIG_DIR`(否则 `$XDG_CONFIG_HOME/zed` 或 `~/.config/zed`)。首次写入前若 `settings.json` 已存在,会在 `state_dir/zed/settings.json` 保留原始快照。
|
|
316
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 `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
|
+
|
|
317
338
|
## 环境变量
|
|
318
339
|
|
|
319
340
|
| 变量 | 默认值 | 作用 |
|
|
@@ -323,6 +344,7 @@ cpac restore zed
|
|
|
323
344
|
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `settings.json` 写入位置) |
|
|
324
345
|
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
|
|
325
346
|
| `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
|
|
347
|
+
| `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
|
|
326
348
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
327
349
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
328
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)";
|
|
@@ -294,6 +295,27 @@ const TARGETS = [
|
|
|
294
295
|
await uninstallZedConfig(config);
|
|
295
296
|
},
|
|
296
297
|
},
|
|
298
|
+
{
|
|
299
|
+
id: "hermes",
|
|
300
|
+
home: () => hermesHome(),
|
|
301
|
+
detected: () => binaryOnPath("hermes") || existsSync(hermesHome()),
|
|
302
|
+
installed: () => isHermesConfigInstalled(),
|
|
303
|
+
version: () => detectClientVersion("hermes"),
|
|
304
|
+
install: async (config, dryRun) => {
|
|
305
|
+
if (dryRun) {
|
|
306
|
+
console.log(`would write CPA provider: ${hermesConfigPath()}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await installHermesConfig(config);
|
|
310
|
+
},
|
|
311
|
+
uninstall: async (config, dryRun) => {
|
|
312
|
+
if (dryRun) {
|
|
313
|
+
console.log(`would remove CPA provider: ${hermesConfigPath()}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
await uninstallHermesConfig(config);
|
|
317
|
+
},
|
|
318
|
+
},
|
|
297
319
|
];
|
|
298
320
|
export async function detectTargets(config) {
|
|
299
321
|
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,7 +10,8 @@ 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 {
|
|
13
|
+
import { isHermesConfigInstalled } from "./targets/hermes.js";
|
|
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";
|
|
16
17
|
export { PROVIDER, loadConfig, saveSpawnModels, saveCodexSetup, liftContextWindows, reorderCatalog, sanitizeCatalogModalities, stampMultiAgentVersion, catalogMaxContextLive, readState, stateProxy, } from "./config.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);
|
|
@@ -83,12 +87,42 @@ export async function status(config) {
|
|
|
83
87
|
return 2;
|
|
84
88
|
return 0;
|
|
85
89
|
}
|
|
90
|
+
async function listModels(config, json) {
|
|
91
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
92
|
+
if (!apiKey)
|
|
93
|
+
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
94
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
95
|
+
if (!json) {
|
|
96
|
+
for (const slug of catalogSlugs(catalog.bytes))
|
|
97
|
+
console.log(slug);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const document = JSON.parse(new TextDecoder().decode(catalog.bytes));
|
|
101
|
+
const models = (catalogModelRows(document) ?? []).flatMap((row) => {
|
|
102
|
+
const slug = catalogModelId(row);
|
|
103
|
+
if (!slug)
|
|
104
|
+
return [];
|
|
105
|
+
const display = typeof row.display_name === "string" && row.display_name.trim()
|
|
106
|
+
? row.display_name.trim()
|
|
107
|
+
: slug;
|
|
108
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
109
|
+
? Math.floor(row.context_window)
|
|
110
|
+
: undefined;
|
|
111
|
+
return [
|
|
112
|
+
context !== undefined
|
|
113
|
+
? { slug, display_name: display, context_window: context }
|
|
114
|
+
: { slug, display_name: display },
|
|
115
|
+
];
|
|
116
|
+
});
|
|
117
|
+
console.log(JSON.stringify({ cpa_url: config.cpa_url, count: models.length, models }, null, 2));
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
86
120
|
async function guide(config) {
|
|
87
|
-
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
|
|
88
122
|
cpac detect Show supported targets and install status
|
|
89
|
-
cpac install [codex|pi|kimi|grok|zed...] Install CPA into detected targets, or named agents
|
|
90
|
-
cpac sync Refresh installed Codex/Pi/Kimi/Grok/Zed injections from CPA
|
|
91
|
-
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 --help Show command usage`);
|
|
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
|
|
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`);
|
|
92
126
|
if (process.env[config.api_key_env]?.trim())
|
|
93
127
|
return 0;
|
|
94
128
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -98,41 +132,77 @@ async function guide(config) {
|
|
|
98
132
|
console.log(`\nSaved ${config.api_key_env} to ${profile}. Open a new terminal or run:\n source ${profile}`);
|
|
99
133
|
return 0;
|
|
100
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
|
+
}
|
|
101
142
|
function usage() {
|
|
143
|
+
const cmd = (left, desc, tag = "") => helpRow(left, desc, tag);
|
|
144
|
+
const opt = (left, desc, tag = "") => helpRow(left, desc, tag, 18);
|
|
102
145
|
return [
|
|
103
|
-
"Usage: cpac",
|
|
104
|
-
" cpac <codex|kimi|grok|pi|zed|claude> [args...]",
|
|
105
|
-
" setup|--setup configure Codex/Claude, then launch",
|
|
106
|
-
" clear restore native config",
|
|
107
|
-
" cpac setup Show current Codex/Claude setup",
|
|
108
|
-
" claude [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset]",
|
|
109
|
-
" codex [--v2_off] [--v2_models] [--max_context]",
|
|
110
|
-
" cpac restore <codex|pi|kimi|grok|zed...> | --all [--dry-run]",
|
|
111
|
-
" cpac status",
|
|
112
|
-
" cpac detect [--json]",
|
|
113
|
-
" cpac install [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]",
|
|
114
|
-
" cpac sync [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]",
|
|
115
|
-
" cpac uninstall [--dry-run]",
|
|
116
|
-
" cpac upgrade [--check]",
|
|
117
|
-
" cpac -v | --version",
|
|
146
|
+
"Usage: cpac [command]",
|
|
118
147
|
"",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
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]"),
|
|
170
|
+
"",
|
|
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]"),
|
|
121
180
|
].join("\n");
|
|
122
181
|
}
|
|
123
182
|
function setupUsage() {
|
|
183
|
+
const cmd = (left, desc, tag = "") => helpRow(left, desc, tag);
|
|
184
|
+
const opt = (left, desc, tag = "") => helpRow(left, desc, tag, 18);
|
|
124
185
|
return [
|
|
125
|
-
"Usage: cpac setup
|
|
126
|
-
" claude [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset]",
|
|
127
|
-
" codex [--v2_off] [--v2_models] [--max_context]",
|
|
186
|
+
"Usage: cpac setup [claude|codex]",
|
|
128
187
|
"",
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
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"),
|
|
133
192
|
"",
|
|
134
|
-
"
|
|
135
|
-
"
|
|
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.",
|
|
136
206
|
].join("\n");
|
|
137
207
|
}
|
|
138
208
|
function printSetup(config, configPath) {
|
|
@@ -157,7 +227,8 @@ function parseArgs(args) {
|
|
|
157
227
|
args[0] === "sync" ||
|
|
158
228
|
args[0] === "uninstall" ||
|
|
159
229
|
args[0] === "restore" ||
|
|
160
|
-
args[0] === "upgrade"
|
|
230
|
+
args[0] === "upgrade" ||
|
|
231
|
+
args[0] === "models") {
|
|
161
232
|
const parsed = {
|
|
162
233
|
command: args[0],
|
|
163
234
|
configPath: defaultConfigPath(),
|
|
@@ -173,6 +244,10 @@ function parseArgs(args) {
|
|
|
173
244
|
};
|
|
174
245
|
for (let index = 1; index < args.length; index++) {
|
|
175
246
|
const arg = args[index];
|
|
247
|
+
if (arg === "-h" || arg === "--help") {
|
|
248
|
+
console.log(usage());
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
176
251
|
if (arg === "--json")
|
|
177
252
|
parsed.json = true;
|
|
178
253
|
else if (arg === "--all")
|
|
@@ -225,7 +300,7 @@ function parseArgs(args) {
|
|
|
225
300
|
}
|
|
226
301
|
return parsed;
|
|
227
302
|
}
|
|
228
|
-
if (["codex", "kimi", "grok", "pi", "zed", "claude"].includes(args[0])) {
|
|
303
|
+
if (["codex", "kimi", "grok", "pi", "zed", "hermes", "claude"].includes(args[0])) {
|
|
229
304
|
const targetId = args[0];
|
|
230
305
|
let configPath = defaultConfigPath();
|
|
231
306
|
const raw = args.slice(1);
|
|
@@ -531,6 +606,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
531
606
|
const config = loadConfig(parsed.configPath, true);
|
|
532
607
|
if (parsed.command === "guide")
|
|
533
608
|
return await guide(config);
|
|
609
|
+
if (parsed.command === "models")
|
|
610
|
+
return await listModels(config, parsed.json);
|
|
534
611
|
if (parsed.command === "detect")
|
|
535
612
|
return await runDetect(config, parsed.json);
|
|
536
613
|
if (parsed.command === "install") {
|
|
@@ -571,7 +648,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
571
648
|
});
|
|
572
649
|
if (parsed.command === "uninstall") {
|
|
573
650
|
if (parsed.targets.length || parsed.all) {
|
|
574
|
-
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");
|
|
575
652
|
}
|
|
576
653
|
return await runUninstall(config, { dryRun: parsed.dryRun });
|
|
577
654
|
}
|
|
@@ -623,7 +700,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
623
700
|
if (parsed.command === "kimi" ||
|
|
624
701
|
parsed.command === "grok" ||
|
|
625
702
|
parsed.command === "pi" ||
|
|
626
|
-
parsed.command === "zed"
|
|
703
|
+
parsed.command === "zed" ||
|
|
704
|
+
parsed.command === "hermes") {
|
|
627
705
|
return await runTargetLauncher(config, parsed.command, parsed.args);
|
|
628
706
|
}
|
|
629
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,236 @@
|
|
|
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)}api: ${JSON.stringify(`http://[IP]:${port}/v1`)}`,
|
|
82
|
+
`${pad(indent + 2)}api_key: cpac`,
|
|
83
|
+
`${pad(indent + 2)}api_mode: responses`,
|
|
84
|
+
`${pad(indent + 2)}discover_models: false`,
|
|
85
|
+
`${pad(indent + 2)}models:`,
|
|
86
|
+
];
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
const slug = catalogModelId(row);
|
|
90
|
+
if (!slug)
|
|
91
|
+
continue;
|
|
92
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
93
|
+
? Math.floor(row.context_window)
|
|
94
|
+
: 200000;
|
|
95
|
+
lines.push(`${pad(indent + 4)}${yamlKey(slug)}:`);
|
|
96
|
+
lines.push(`${pad(indent + 6)}context_length: ${context}`);
|
|
97
|
+
count += 1;
|
|
98
|
+
}
|
|
99
|
+
if (count === 0)
|
|
100
|
+
throw new CPACError("CPA catalog contains no models");
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
function mappingIsEmpty(lines, start, end, keyIndent) {
|
|
104
|
+
for (let i = start + 1; i < end; i++) {
|
|
105
|
+
const line = lines[i];
|
|
106
|
+
if (!line.trim() || line.trimStart().startsWith("#"))
|
|
107
|
+
continue;
|
|
108
|
+
if (lineIndent(line) > keyIndent)
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function hasProvidersCpac(content) {
|
|
114
|
+
const lines = content.split(/\r?\n/);
|
|
115
|
+
const range = providersRange(lines);
|
|
116
|
+
if (!range)
|
|
117
|
+
return false;
|
|
118
|
+
const indent = childIndent(lines, range.idx, range.end, 0);
|
|
119
|
+
return Boolean(findCpacChild(lines, range.idx, range.end, indent));
|
|
120
|
+
}
|
|
121
|
+
export function isHermesConfigInstalled() {
|
|
122
|
+
try {
|
|
123
|
+
return hasProvidersCpac(readFileSync(hermesConfigPath(), "utf8"));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function stripProvidersCpac(content) {
|
|
130
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
131
|
+
const lines = content.split(/\r?\n/);
|
|
132
|
+
const range = providersRange(lines);
|
|
133
|
+
if (!range)
|
|
134
|
+
return content;
|
|
135
|
+
const indent = childIndent(lines, range.idx, range.end, 0);
|
|
136
|
+
const child = findCpacChild(lines, range.idx, range.end, indent);
|
|
137
|
+
if (!child)
|
|
138
|
+
return content;
|
|
139
|
+
const next = [...lines.slice(0, child.idx), ...lines.slice(child.end)];
|
|
140
|
+
const providersEnd = mappingEnd(next, range.idx, 0);
|
|
141
|
+
if (mappingIsEmpty(next, range.idx, providersEnd, 0)) {
|
|
142
|
+
next.splice(range.idx, providersEnd - range.idx);
|
|
143
|
+
}
|
|
144
|
+
return next.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
145
|
+
}
|
|
146
|
+
function upsertProvidersCpac(content, port, rows) {
|
|
147
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
148
|
+
const stripped = stripProvidersCpac(content);
|
|
149
|
+
const lines = stripped === "" ? [] : stripped.split(/\r?\n/);
|
|
150
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
151
|
+
lines.pop();
|
|
152
|
+
const range = providersRange(lines);
|
|
153
|
+
if (!range) {
|
|
154
|
+
const block = ["providers:", ...cpacChildLines(port, rows, 2)];
|
|
155
|
+
if (lines.length === 0)
|
|
156
|
+
return `${block.join(eol)}${eol}`;
|
|
157
|
+
return `${lines.join(eol)}${eol}${block.join(eol)}${eol}`;
|
|
158
|
+
}
|
|
159
|
+
const inline = lines[range.idx]
|
|
160
|
+
.replace(/^providers:\s*/, "")
|
|
161
|
+
.replace(/#.*$/, "")
|
|
162
|
+
.trim();
|
|
163
|
+
if (inline && inline !== "{}" && inline !== "null") {
|
|
164
|
+
throw new CPACError("Hermes providers: must be a block mapping");
|
|
165
|
+
}
|
|
166
|
+
if (inline === "{}" || inline === "null")
|
|
167
|
+
lines[range.idx] = "providers:";
|
|
168
|
+
const end = mappingEnd(lines, range.idx, 0);
|
|
169
|
+
const indent = childIndent(lines, range.idx, end, 0);
|
|
170
|
+
const child = findCpacChild(lines, range.idx, end, indent);
|
|
171
|
+
const replacement = cpacChildLines(port, rows, indent);
|
|
172
|
+
if (child) {
|
|
173
|
+
return [
|
|
174
|
+
...lines.slice(0, child.idx),
|
|
175
|
+
...replacement,
|
|
176
|
+
...lines.slice(child.end),
|
|
177
|
+
].join(eol);
|
|
178
|
+
}
|
|
179
|
+
return [
|
|
180
|
+
...lines.slice(0, range.idx + 1),
|
|
181
|
+
...replacement,
|
|
182
|
+
...lines.slice(range.idx + 1),
|
|
183
|
+
].join(eol);
|
|
184
|
+
}
|
|
185
|
+
function writeHermesProvider(path, port, rows) {
|
|
186
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
187
|
+
content = upsertProvidersCpac(content, port, rows);
|
|
188
|
+
if (!content.endsWith("\n"))
|
|
189
|
+
content += "\n";
|
|
190
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
191
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
192
|
+
atomicWrite(path, Buffer.from(content), mode);
|
|
193
|
+
}
|
|
194
|
+
export async function installHermesConfig(config) {
|
|
195
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
196
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
197
|
+
let document;
|
|
198
|
+
try {
|
|
199
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new CPACError("invalid CPA catalog");
|
|
203
|
+
}
|
|
204
|
+
const rows = catalogModelRows(document) ?? [];
|
|
205
|
+
if (rows.length === 0)
|
|
206
|
+
throw new CPACError("CPA catalog contains no models");
|
|
207
|
+
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
208
|
+
const target = hermesConfigPath();
|
|
209
|
+
const hadFile = existsSync(target);
|
|
210
|
+
try {
|
|
211
|
+
ensureCpacBackup(config.state_dir, "hermes", target, !isHermesConfigInstalled());
|
|
212
|
+
writeHermesProvider(target, proxy.port, rows);
|
|
213
|
+
recordWrittenConfig(config.state_dir, "hermes", target, {
|
|
214
|
+
id: proxy.id,
|
|
215
|
+
pid: proxy.pid,
|
|
216
|
+
port: proxy.port,
|
|
217
|
+
fingerprint,
|
|
218
|
+
}, hadFile);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (started)
|
|
222
|
+
await stopProxyProcess(proxy);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Installed Hermes provider "${HERMES_PROVIDER_ID}": ${target}`);
|
|
226
|
+
}
|
|
227
|
+
export async function uninstallHermesConfig(config) {
|
|
228
|
+
const target = hermesConfigPath();
|
|
229
|
+
if (!isHermesConfigInstalled())
|
|
230
|
+
throw new CPACError("Hermes CPAC provider is not installed");
|
|
231
|
+
restoreOwnedConfig(config.state_dir, "hermes", target, (current) => Buffer.from(stripProvidersCpac(new TextDecoder("utf-8").decode(current))));
|
|
232
|
+
const { stopProxy } = dropAgent(config.state_dir, "hermes");
|
|
233
|
+
if (stopProxy)
|
|
234
|
+
await stopProxyProcess(stopProxy);
|
|
235
|
+
console.log(`Removed Hermes provider "${HERMES_PROVIDER_ID}": ${target}`);
|
|
236
|
+
}
|