@yhong91/cpac 0.1.37 → 0.1.39

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`
13
+ - 已安装需要使用的 agent:`codex`、`claude`、`pi`、`kimi`、`grok` 或 `zed`
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)
40
+ cpac install --all # 注入所有检测到的目标(codex/pi/kimi/grok/zed
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` 在 PATH 上)。向用户索取 CPA API key。
64
+ 1. **检查前置**:`node --version` ≥ 20;确认用户需要哪些 agent(`codex`/`claude`/`pi`/`kimi`/`grok`/`zed` 在 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> [args...]`(所有原生参数如 `-c` 等原样透明传递)
93
+ - 启动:`cpac <codex|claude|kimi|grok|pi|zed> [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 做 `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 做 `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>` 恢复原配置。
100
+ 失败处理:任何一步报错时先跑 `cpac status`;密钥未配置回到步骤 2;注入后异常用 `cpac <agent> clear` 或 `cpac restore <codex|pi|kimi|grok|zed>` 恢复原配置。
101
101
 
102
102
  ## 首次引导
103
103
 
@@ -113,7 +113,7 @@ 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 restore <codex|pi|kimi|grok> | --all Detach injected agents
116
+ cpac restore <codex|pi|kimi|grok|zed> | --all Detach injected agents
117
117
  cpac --help Show command usage
118
118
  ```
119
119
 
@@ -161,26 +161,26 @@ cpac --help
161
161
 
162
162
  ### 统一管理(detect / install / sync / restore / uninstall / upgrade)
163
163
 
164
- 参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok` 四个持久注入目标统一管理(claude 是临时接管,用 `cpac claude` 启动,不属于 install 目标):
164
+ 参考 vibetime 的目标式命令,对 `codex`、`pi`、`kimi`、`grok`、`zed` 五个持久注入目标统一管理(claude 是临时接管,用 `cpac claude` 启动,不属于 install 目标):
165
165
 
166
166
  ```bash
167
167
  cpac detect [--json]
168
- cpac install [codex|pi|kimi|grok...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]
169
- cpac sync [codex|pi|kimi|grok...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]
170
- cpac restore <codex|pi|kimi|grok...> | --all [--dry-run]
168
+ cpac install [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]
169
+ cpac sync [codex|pi|kimi|grok|zed...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]
170
+ cpac restore <codex|pi|kimi|grok|zed...> | --all [--dry-run]
171
171
  cpac uninstall [--dry-run]
172
172
  cpac upgrade [--check]
173
173
  cpac -v | --version
174
174
  ```
175
175
 
176
176
  - `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
177
- - `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
178
- - `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi 确认 `npm:@yhong91/cpac` 并清理旧的 `extensions/pi-cpac.ts`、Kimi 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `cpac install codex` 有效。
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` 有效。
179
179
  - 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
180
  - `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
181
181
  - `restore` 必须带 agent 名或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
182
182
  - `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 执行 `sync`。已是最新也会 sync。不要先手动 `npm install -g` 再 `cpac sync`。`--check` 只报告版本,不安装也不 sync。没有已安装目标时跳过 sync,不报错。只刷新配置、不升 CLI 时用 `cpac sync`。
183
+ - `upgrade` 是发布后更新本机的一次性入口:查 npm → 需要时 `npm install -g` → 对**已接入**的 Codex / Pi / Kimi / Grok / Zed 执行 `sync`。已是最新也会 sync。不要先手动 `npm install -g` 再 `cpac sync`。`--check` 只报告版本,不安装也不 sync。没有已安装目标时跳过 sync,不报错。只刷新配置、不升 CLI 时用 `cpac sync`。
184
184
 
185
185
  ### Claude Code
186
186
 
@@ -250,9 +250,9 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。注入会删除 `models
250
250
 
251
251
  注入命令可选 `--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
252
 
253
- `config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。Pi 只改 `settings.json` `packages`,不备份。`restore codex`:若 `config.toml` 相对注入未改,按原 mode 逐字节还原备份;若用户在注入期间改过文件,只撤掉 CPAC 写入的 catalog、url 和标记。没有其它需要代理的 agent 时才关闭代理。
253
+ `config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok/zed/pi 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。所有 target 的 restore 都按 Codex 规则:注入后记下文件 hash;相对注入未改则逐字节还原备份(没有备份则删除我们创建的文件);注入后改过则只剥 CPAC 字段,用户改动留下。没有其它需要代理的 agent 时才关闭代理。
254
254
 
255
- loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac kimi` / `cpac grok` 即可恢复。
255
+ loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac kimi` / `cpac grok` / `cpac zed` 即可恢复。
256
256
 
257
257
  默认路径:
258
258
 
@@ -263,6 +263,8 @@ CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录
263
263
  Codex ~/.cpac/codex/config.toml
264
264
  Kimi ~/.cpac/kimi/config.toml
265
265
  Grok ~/.cpac/grok/config.toml
266
+ Zed ~/.cpac/zed/settings.json
267
+ Pi ~/.cpac/pi/settings.json
266
268
  restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
267
269
  ```
268
270
 
@@ -270,19 +272,14 @@ restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里
270
272
 
271
273
  ### Pi
272
274
 
273
- 安装 CPA provider 扩展到 Pi:
274
-
275
275
  ```bash
276
- cpac pi install
276
+ cpac install pi
277
+ cpac pi
277
278
  ```
278
279
 
279
- `~/.pi/agent/settings.json` 的 `packages` 中加入 `npm:@yhong91/cpac`。Pi 启动时按 npm 包加载 `extensions/pi-cpac.ts`,从 CPA 动态注册模型。旧版写入的 `~/.pi/agent/extensions/pi-cpac.ts` 会在 install / restore 时删掉。卸载:
280
-
281
- ```bash
282
- cpac restore pi
283
- ```
280
+ 未接入时把 `npm:@yhong91/cpac` 写入 `~/.pi/agent/settings.json` 的 `packages`。已接入则比较 `~/.pi/agent/npm/node_modules/@yhong91/cpac` 与当前 CLI 版本:落后则 `pi update npm:@yhong91/cpac`,已是最新(或尚未下载副本)则跳过。`cpac pi` 启动前走同一套判断。Pi 启动时加载 `extensions/pi-cpac.ts` CPA 动态注册模型。旧版 `~/.pi/agent/extensions/pi-cpac.ts` 会在 install / restore 时删掉。
284
281
 
285
- 尊重 `PI_CODING_AGENT_DIR`。扩展代码随 npm 包更新,已装过的副本用 `pi update npm:@yhong91/cpac`(或 `pi update --extensions`)。
282
+ 卸载:`cpac restore pi` `cpac pi clear`。尊重 `PI_CODING_AGENT_DIR`。
286
283
 
287
284
  ### Kimi Code
288
285
 
@@ -292,7 +289,7 @@ cpac restore pi
292
289
  cpac kimi install
293
290
  ```
294
291
 
295
- 它在 `~/.kimi-code/config.toml` 追加一个由 CPAC 管理的块,注册 `cpac` OpenAI 兼容 provider,并把 CPA 目录里的每个模型映射为一个 `cpac/<模型名>` 条目。带 `supported_reasoning_levels` 的模型会写入 `capabilities`、`support_efforts` 和 `default_effort`,否则 Kimi 只给 Claude 名套内置 thinking profile,其它模型会停在 thinking off 且无法改 effort。`base_url` 指向共享 loopback 代理,因此密钥不落盘。install 会写入 `state.json` 并在需要时启动代理,不必先单独注入 Codex。卸载与状态:
292
+ 它在 `~/.kimi-code/config.toml` 追加一个由 CPAC 管理的块,注册 `cpac` OpenAI 兼容 provider,并把 CPA 目录里的每个模型映射为一个 `cpac/<模型名>` 条目。带 `supported_reasoning_levels` 的模型会写入 `capabilities`、`support_efforts` 和 `default_effort`,否则 Kimi 只给 Claude 名套内置 thinking profile,其它模型会停在 thinking off 且无法改 effort。`base_url` 指向共享 loopback 代理,因此密钥不落盘。install 会写入 `state.json` 并在需要时启动代理,不必先单独注入 Codex。`restore` 未改过注入文件则还原备份,改过则只剥 CPAC 块。卸载与状态:
296
293
 
297
294
  ```bash
298
295
  cpac restore kimi
@@ -300,6 +297,23 @@ cpac restore kimi
300
297
 
301
298
  Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若 `config.toml` 已存在,会在 `state_dir/kimi/config.toml` 保留一份原始快照(只存最早版本),万一文件损坏可手动复制回去。
302
299
 
300
+ ### Zed
301
+
302
+ 写入 CPA 的 OpenAI-compatible provider 到 Zed Agent:
303
+
304
+ ```bash
305
+ cpac install zed
306
+ cpac zed
307
+ ```
308
+
309
+ 它在 `~/.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 填一次。卸载:
310
+
311
+ ```bash
312
+ cpac restore zed
313
+ ```
314
+
315
+ 配置目录尊重 `ZED_CONFIG_DIR`(否则 `$XDG_CONFIG_HOME/zed` 或 `~/.config/zed`)。首次写入前若 `settings.json` 已存在,会在 `state_dir/zed/settings.json` 保留原始快照。
316
+
303
317
  ## 环境变量
304
318
 
305
319
  | 变量 | 默认值 | 作用 |
@@ -307,7 +321,8 @@ Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若
307
321
  | `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
308
322
  | `CPA_BASE_URL` | `http://124.223.178.52:8317` | 覆盖内置 CPA 地址 |
309
323
  | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `settings.json` 写入位置) |
310
- | `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac kimi install` 写入位置) |
324
+ | `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
325
+ | `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
311
326
  | `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
312
327
  | `CODEX_HOME` | `~/.codex` | Codex home 目录 |
313
328
 
package/dist/agents.js CHANGED
@@ -7,8 +7,9 @@ import { MAX_SPAWN_MODELS, loadCatalogSlugs, pickSpawnModels, saveCodexSetup, sa
7
7
  import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
8
8
  import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
9
9
  import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
10
- import { PI_PACKAGE, installPiExtension, isPiExtensionInstalled, piAgentDir, piSettingsPath, uninstallPiExtension, } from "./targets/pi.js";
11
- import { CPACError, atomicWrite, checkboxPicker, expandUserPath, objectValue, tabWizard, } from "./util.js";
10
+ import { PI_PACKAGE, ensurePiExtension, isPiExtensionInstalled, piAgentDir, piSettingsPath, uninstallPiExtension, } from "./targets/pi.js";
11
+ import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
12
+ import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, tabWizard, } from "./util.js";
12
13
  const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
13
14
  const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
14
15
  const CODEX_V2_ON = "Enabled (Default)";
@@ -220,11 +221,7 @@ const TARGETS = [
220
221
  installed: () => isPiExtensionInstalled(),
221
222
  version: () => detectClientVersion("pi"),
222
223
  install: async (config, dryRun) => {
223
- if (dryRun) {
224
- console.log(`would add ${PI_PACKAGE} to ${piSettingsPath()}`);
225
- return;
226
- }
227
- await installPiExtension(config);
224
+ await ensurePiExtension(config, CPAC_VERSION, dryRun);
228
225
  },
229
226
  uninstall: async (config, dryRun) => {
230
227
  if (dryRun) {
@@ -276,6 +273,27 @@ const TARGETS = [
276
273
  await uninstallGrokConfig(config);
277
274
  },
278
275
  },
276
+ {
277
+ id: "zed",
278
+ home: () => zedHome(),
279
+ detected: () => binaryOnPath("zed") || existsSync(zedHome()),
280
+ installed: () => isZedConfigInstalled(),
281
+ version: () => detectClientVersion("zed"),
282
+ install: async (config, dryRun) => {
283
+ if (dryRun) {
284
+ console.log(`would write CPA openai_compatible provider: ${zedConfigPath()}`);
285
+ return;
286
+ }
287
+ await installZedConfig(config);
288
+ },
289
+ uninstall: async (config, dryRun) => {
290
+ if (dryRun) {
291
+ console.log(`would remove CPA openai_compatible provider: ${zedConfigPath()}`);
292
+ return;
293
+ }
294
+ await uninstallZedConfig(config);
295
+ },
296
+ },
279
297
  ];
280
298
  export async function detectTargets(config) {
281
299
  return TARGETS.map((target) => ({
@@ -321,7 +339,7 @@ export async function runInstall(config, requested, options) {
321
339
  throw new CPACError("--max_context is only valid with codex");
322
340
  }
323
341
  for (const target of selected) {
324
- if (target.installed(config) && !options.force) {
342
+ if (target.installed(config) && !options.force && target.id !== "pi") {
325
343
  console.log(`${target.id}: already installed; use --force to reinstall`);
326
344
  continue;
327
345
  }
@@ -398,17 +416,6 @@ export async function runUninstall(config, options) {
398
416
  }
399
417
  return 0;
400
418
  }
401
- function compareVersions(a, b) {
402
- const left = a.split(".").map(Number);
403
- const right = b.split(".").map(Number);
404
- for (let index = 0; index < Math.max(left.length, right.length); index++) {
405
- const l = left[index] ?? 0;
406
- const r = right[index] ?? 0;
407
- if (l !== r)
408
- return l > r ? 1 : -1;
409
- }
410
- return 0;
411
- }
412
419
  export async function runUpgrade(config, checkOnly) {
413
420
  console.log(`Current version: ${CPAC_VERSION}`);
414
421
  let latest;
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync, rmdirSync, rmSync } from "node:fs";
2
+ import { existsSync, readFileSync, rmdirSync, rmSync, statSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { isAbsolute, join, parse, relative, resolve } from "node:path";
4
+ import { basename, isAbsolute, join, parse, relative, resolve } from "node:path";
5
5
  import { CPACError, atomicWrite, checkboxPicker, expandUserPath, objectValue, } from "./util.js";
6
6
  export const PROVIDER = "cpac_cpa";
7
7
  export const DEFAULT_CPA_URL = "http://124.223.178.52:8317";
@@ -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"];
25
+ export const AGENT_IDS = ["codex", "pi", "kimi", "grok", "zed"];
26
26
  const AGENT_ID_SET = new Set(AGENT_IDS);
27
27
  const STATE_KEYS = new Set([
28
28
  "config_path",
@@ -42,6 +42,7 @@ export const STATE_FILES = [
42
42
  "config.toml.backup",
43
43
  "codex-models.json",
44
44
  ];
45
+ const AGENT_RECORD_KEYS = new Set(["path", "existed", "mode", "injected_hash"]);
45
46
  export function defaultConfigPath() {
46
47
  return resolve(expandUserPath(process.env.CPAC_CONFIG || "~/.config/cpac/config.json"));
47
48
  }
@@ -468,7 +469,14 @@ export function readState(stateDir) {
468
469
  typeof rec.path !== "string" ||
469
470
  !rec.path ||
470
471
  (rec.existed !== undefined && typeof rec.existed !== "boolean") ||
471
- Object.keys(rec).some((key) => key !== "path" && key !== "existed")) {
472
+ (rec.mode !== undefined &&
473
+ (!Number.isInteger(rec.mode) ||
474
+ rec.mode < 0 ||
475
+ rec.mode > 0o7777)) ||
476
+ (rec.injected_hash !== undefined &&
477
+ (typeof rec.injected_hash !== "string" ||
478
+ !/^[a-f0-9]{64}$/.test(rec.injected_hash))) ||
479
+ Object.keys(rec).some((key) => !AGENT_RECORD_KEYS.has(key))) {
472
480
  throw new CPACError(`invalid agent record: ${id}`);
473
481
  }
474
482
  }
@@ -517,17 +525,27 @@ export function needsProxy(state) {
517
525
  if (state.config_path)
518
526
  return true;
519
527
  const agents = state.agents ?? {};
520
- return Boolean(agents.codex || agents.kimi || agents.grok);
528
+ return Boolean(agents.codex || agents.kimi || agents.grok || agents.zed);
521
529
  }
522
530
  export function saveState(stateDir, state) {
523
531
  atomicWrite(join(stateDir, "state.json"), Buffer.from(`${JSON.stringify(state, null, 2)}\n`));
524
532
  }
525
- export function recordAgent(stateDir, id, path, proxy, existed) {
533
+ export function recordAgent(stateDir, id, path, proxy, existed, injected) {
526
534
  const current = readState(stateDir) ?? {};
527
535
  const prev = current.agents?.[id];
528
536
  const record = { path };
529
537
  if (!(prev?.existed ?? existed ?? true))
530
538
  record.existed = false;
539
+ if (injected) {
540
+ record.injected_hash = injected.hash;
541
+ record.mode = injected.mode;
542
+ }
543
+ else {
544
+ if (prev?.injected_hash)
545
+ record.injected_hash = prev.injected_hash;
546
+ if (prev?.mode !== undefined)
547
+ record.mode = prev.mode;
548
+ }
531
549
  const next = {
532
550
  ...current,
533
551
  agents: { ...current.agents, [id]: record },
@@ -541,10 +559,52 @@ export function recordAgent(stateDir, id, path, proxy, existed) {
541
559
  saveState(stateDir, next);
542
560
  return next;
543
561
  }
562
+ export function recordWrittenConfig(stateDir, id, path, proxy, existed) {
563
+ const bytes = readFileSync(path);
564
+ return recordAgent(stateDir, id, path, proxy, existed, {
565
+ hash: sha256Hex(bytes),
566
+ mode: statSync(path).mode & 0o7777,
567
+ });
568
+ }
544
569
  export function removeCreatedTarget(stateDir, id, target) {
545
570
  if (readState(stateDir)?.agents?.[id]?.existed === false)
546
571
  rmSync(target, { force: true });
547
572
  }
573
+ function restoredIsEmpty(bytes) {
574
+ const text = new TextDecoder("utf-8").decode(bytes).trim();
575
+ if (text === "")
576
+ return true;
577
+ try {
578
+ const value = JSON.parse(text);
579
+ return objectValue(value) && Object.keys(value).length === 0;
580
+ }
581
+ catch {
582
+ return false;
583
+ }
584
+ }
585
+ /** Codex restore: unchanged → backup bytes; edited → strip owned fields. */
586
+ export function restoreOwnedConfig(stateDir, id, target, strip) {
587
+ const rec = readState(stateDir)?.agents?.[id];
588
+ const existed = rec?.existed !== false;
589
+ const mode = rec?.mode ??
590
+ (existsSync(target) ? statSync(target).mode & 0o7777 : 0o600);
591
+ const current = existsSync(target) ? readFileSync(target) : null;
592
+ const unchanged = !rec?.injected_hash ||
593
+ (current !== null && sha256Hex(current) === rec.injected_hash);
594
+ if (!current || unchanged) {
595
+ const backup = join(stateDir, id, basename(target));
596
+ if (existsSync(backup))
597
+ atomicWrite(target, readFileSync(backup), mode);
598
+ else
599
+ rmSync(target, { force: true });
600
+ return;
601
+ }
602
+ const stripped = strip(current);
603
+ if (!existed && restoredIsEmpty(stripped))
604
+ rmSync(target, { force: true });
605
+ else
606
+ atomicWrite(target, stripped, mode);
607
+ }
548
608
  export function dropAgent(stateDir, id) {
549
609
  rmSync(join(stateDir, id), { recursive: true, force: true });
550
610
  const current = readState(stateDir);
package/dist/cpac.js CHANGED
@@ -9,6 +9,7 @@ import { multiAgentV2Enabled } from "./targets/codex.js";
9
9
  import { isGrokConfigInstalled } from "./targets/grok.js";
10
10
  import { isKimiConfigInstalled } from "./targets/kimi.js";
11
11
  import { isPiExtensionInstalled } from "./targets/pi.js";
12
+ import { isZedConfigInstalled } from "./targets/zed.js";
12
13
  import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, } from "./config.js";
13
14
  import { proxyIsHealthy, runProxyChild } from "./proxy.js";
14
15
  import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
@@ -20,7 +21,8 @@ export { claudeTierSlots, createClaudeProxy, runClaude, } from "./targets/claude
20
21
  export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, runTargetLauncher, } from "./agents.js";
21
22
  export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
22
23
  export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
23
- export { installPiExtension, isPiExtensionInstalled, uninstallPiExtension, } from "./targets/pi.js";
24
+ export { ensurePiExtension, installPiExtension, isPiExtensionInstalled, piPackageAction, piPackageVersion, uninstallPiExtension, } from "./targets/pi.js";
25
+ export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
24
26
  export async function status(config) {
25
27
  const apiKey = process.env[config.api_key_env]?.trim();
26
28
  const keyConfigured = !!apiKey;
@@ -72,6 +74,8 @@ export async function status(config) {
72
74
  console.log(`kimi: ${keyConfigured ? (kimiInstalled ? "ready" : "config not installed; run: cpac install kimi") : "not configured"}`);
73
75
  const grokInstalled = isGrokConfigInstalled();
74
76
  console.log(`grok: ${keyConfigured ? (grokInstalled ? "ready" : "config not installed; run: cpac install grok") : "not configured"}`);
77
+ const zedInstalled = isZedConfigInstalled();
78
+ console.log(`zed: ${keyConfigured ? (zedInstalled ? "ready" : "config not installed; run: cpac install zed") : "not configured"}`);
75
79
  if (!state)
76
80
  return 1;
77
81
  const proxy = stateProxy(state);
@@ -80,10 +84,10 @@ export async function status(config) {
80
84
  return 0;
81
85
  }
82
86
  async function guide(config) {
83
- 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, 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)\n cpac restore <codex|pi|kimi|grok> | --all Detach injected agents
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
84
88
  cpac detect Show supported targets and install status
85
- cpac install [codex|pi|kimi|grok...] Install CPA into detected targets, or named agents
86
- cpac sync Refresh installed Codex/Pi/Kimi/Grok injections from CPA
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
87
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`);
88
92
  if (process.env[config.api_key_env]?.trim())
89
93
  return 0;
@@ -97,17 +101,17 @@ async function guide(config) {
97
101
  function usage() {
98
102
  return [
99
103
  "Usage: cpac",
100
- " cpac <codex|kimi|grok|pi|claude> [args...]",
104
+ " cpac <codex|kimi|grok|pi|zed|claude> [args...]",
101
105
  " setup|--setup configure Codex/Claude, then launch",
102
106
  " clear restore native config",
103
107
  " cpac setup Show current Codex/Claude setup",
104
108
  " claude [--opus [M]] [--sonnet [M]] [--haiku [M]] [--reset]",
105
109
  " codex [--v2_off] [--v2_models] [--max_context]",
106
- " cpac restore <codex|pi|kimi|grok...> | --all [--dry-run]",
110
+ " cpac restore <codex|pi|kimi|grok|zed...> | --all [--dry-run]",
107
111
  " cpac status",
108
112
  " cpac detect [--json]",
109
- " cpac install [codex|pi|kimi|grok...] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context]",
110
- " cpac sync [codex|pi|kimi|grok...] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context]",
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]",
111
115
  " cpac uninstall [--dry-run]",
112
116
  " cpac upgrade [--check]",
113
117
  " cpac -v | --version",
@@ -221,7 +225,7 @@ function parseArgs(args) {
221
225
  }
222
226
  return parsed;
223
227
  }
224
- if (["codex", "kimi", "grok", "pi", "claude"].includes(args[0])) {
228
+ if (["codex", "kimi", "grok", "pi", "zed", "claude"].includes(args[0])) {
225
229
  const targetId = args[0];
226
230
  let configPath = defaultConfigPath();
227
231
  const raw = args.slice(1);
@@ -567,7 +571,7 @@ export async function main(args = process.argv.slice(2)) {
567
571
  });
568
572
  if (parsed.command === "uninstall") {
569
573
  if (parsed.targets.length || parsed.all) {
570
- throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok> | --all");
574
+ throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore <codex|pi|kimi|grok|zed> | --all");
571
575
  }
572
576
  return await runUninstall(config, { dryRun: parsed.dryRun });
573
577
  }
@@ -618,7 +622,8 @@ export async function main(args = process.argv.slice(2)) {
618
622
  }
619
623
  if (parsed.command === "kimi" ||
620
624
  parsed.command === "grok" ||
621
- parsed.command === "pi") {
625
+ parsed.command === "pi" ||
626
+ parsed.command === "zed") {
622
627
  return await runTargetLauncher(config, parsed.command, parsed.args);
623
628
  }
624
629
  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 first");
332
+ throw new CPACError("state has no loopback proxy; run cpac inject or install kimi/grok/zed 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}`);
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordAgent, removeCreatedTarget, } from "../config.js";
4
+ import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
5
5
  import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
6
6
  import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, resolveApiKey, tomlString, } from "../util.js";
7
7
  export function grokHome() {
@@ -43,16 +43,16 @@ function stripOrphanCpacGrokTables(content) {
43
43
  }
44
44
  return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
45
45
  }
46
- function writeGrokBlock(path, block) {
47
- let content = existsSync(path) ? readFileSync(path, "utf8") : "";
46
+ function stripGrokManaged(content) {
48
47
  content = content.replace(grokBlockRegex, "");
49
48
  const orphan = content.indexOf(GROK_BLOCK_START);
50
49
  if (orphan !== -1)
51
50
  content = content.slice(0, orphan);
52
- content = stripOrphanCpacGrokTables(content);
53
- if (block) {
54
- content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
55
- }
51
+ return stripOrphanCpacGrokTables(content);
52
+ }
53
+ function writeGrokBlock(path, block) {
54
+ let content = stripGrokManaged(existsSync(path) ? readFileSync(path, "utf8") : "");
55
+ content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
56
56
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
57
57
  const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
58
58
  atomicWrite(path, Buffer.from(content), mode);
@@ -93,7 +93,7 @@ export async function installGrokConfig(config) {
93
93
  try {
94
94
  ensureCpacBackup(config.state_dir, "grok", target, !isGrokConfigInstalled());
95
95
  writeGrokBlock(target, lines.join("\n"));
96
- recordAgent(config.state_dir, "grok", target, {
96
+ recordWrittenConfig(config.state_dir, "grok", target, {
97
97
  id: proxy.id,
98
98
  pid: proxy.pid,
99
99
  port: proxy.port,
@@ -111,8 +111,7 @@ export async function uninstallGrokConfig(config) {
111
111
  const target = grokConfigPath();
112
112
  if (!isGrokConfigInstalled())
113
113
  throw new CPACError("Grok Build config is not installed");
114
- writeGrokBlock(target, null);
115
- removeCreatedTarget(config.state_dir, "grok", target);
114
+ restoreOwnedConfig(config.state_dir, "grok", target, (current) => Buffer.from(stripGrokManaged(new TextDecoder("utf-8").decode(current))));
116
115
  const { stopProxy } = dropAgent(config.state_dir, "grok");
117
116
  if (stopProxy)
118
117
  await stopProxyProcess(stopProxy);
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordAgent, removeCreatedTarget, } from "../config.js";
4
+ import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
5
5
  import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
6
6
  import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, objectValue, resolveApiKey, tomlString, tomlStringArray, } from "../util.js";
7
7
  export function kimiConfigPath() {
@@ -44,8 +44,7 @@ function stripOrphanCpacTables(content) {
44
44
  }
45
45
  return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
46
46
  }
47
- function writeKimiBlock(path, block) {
48
- let content = existsSync(path) ? readFileSync(path, "utf8") : "";
47
+ function stripKimiManaged(content) {
49
48
  content = content.replace(kimiBlockRegex, "");
50
49
  // A truncated managed block (start marker without the end marker) leaves its
51
50
  // tables behind; appending again would duplicate [providers.cpac], make the
@@ -54,10 +53,11 @@ function writeKimiBlock(path, block) {
54
53
  const orphan = content.indexOf(KIMI_BLOCK_START);
55
54
  if (orphan !== -1)
56
55
  content = content.slice(0, orphan);
57
- content = stripOrphanCpacTables(content);
58
- if (block) {
59
- content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
60
- }
56
+ return stripOrphanCpacTables(content);
57
+ }
58
+ function writeKimiBlock(path, block) {
59
+ let content = stripKimiManaged(existsSync(path) ? readFileSync(path, "utf8") : "");
60
+ content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
61
61
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
62
62
  const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
63
63
  atomicWrite(path, Buffer.from(content), mode);
@@ -159,7 +159,7 @@ export async function installKimiConfig(config) {
159
159
  try {
160
160
  ensureCpacBackup(config.state_dir, "kimi", target, !isKimiConfigInstalled());
161
161
  writeKimiBlock(target, lines.join("\n"));
162
- recordAgent(config.state_dir, "kimi", target, {
162
+ recordWrittenConfig(config.state_dir, "kimi", target, {
163
163
  id: proxy.id,
164
164
  pid: proxy.pid,
165
165
  port: proxy.port,
@@ -177,8 +177,7 @@ export async function uninstallKimiConfig(config) {
177
177
  const target = kimiConfigPath();
178
178
  if (!isKimiConfigInstalled())
179
179
  throw new CPACError("Kimi Code config is not installed");
180
- writeKimiBlock(target, null);
181
- removeCreatedTarget(config.state_dir, "kimi", target);
180
+ restoreOwnedConfig(config.state_dir, "kimi", target, (current) => Buffer.from(stripKimiManaged(new TextDecoder("utf-8").decode(current))));
182
181
  const { stopProxy } = dropAgent(config.state_dir, "kimi");
183
182
  if (stopProxy)
184
183
  await stopProxyProcess(stopProxy);
@@ -1,9 +1,10 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
- import { dropAgent, recordAgent } from "../config.js";
5
+ import { dropAgent, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
5
6
  import { stopProxyProcess } from "../proxy.js";
6
- import { CPACError, atomicWrite, expandUserPath, objectValue, } from "../util.js";
7
+ import { CPACError, atomicWrite, compareVersions, ensureCpacBackup, expandUserPath, objectValue, } from "../util.js";
7
8
  export const PI_PACKAGE = "npm:@yhong91/cpac";
8
9
  const PI_PACKAGE_NAME = "@yhong91/cpac";
9
10
  const LEGACY_EXTENSIONS = ["pi-cpac.ts", "cpac.ts", "pi-cpa.ts"];
@@ -77,9 +78,7 @@ function removeLeftovers() {
77
78
  }
78
79
  return removed;
79
80
  }
80
- export function isPiExtensionInstalled() {
81
- if (leftoverExtensions().length > 0)
82
- return true;
81
+ function isPiPackageEnabled() {
83
82
  const path = piSettingsPath();
84
83
  if (!existsSync(path))
85
84
  return false;
@@ -91,10 +90,67 @@ export function isPiExtensionInstalled() {
91
90
  return false;
92
91
  }
93
92
  }
93
+ export function isPiExtensionInstalled() {
94
+ return leftoverExtensions().length > 0 || isPiPackageEnabled();
95
+ }
96
+ export function piPackageVersion() {
97
+ const pkg = join(piAgentDir(), "npm", "node_modules", "@yhong91", "cpac", "package.json");
98
+ try {
99
+ const version = JSON.parse(readFileSync(pkg, "utf8")).version;
100
+ return typeof version === "string" ? version : undefined;
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ }
106
+ export function piPackageAction(desiredVersion) {
107
+ if (leftoverExtensions().length > 0 || !isPiExtensionInstalled()) {
108
+ return "install";
109
+ }
110
+ const version = piPackageVersion();
111
+ if (version !== undefined && compareVersions(version, desiredVersion) < 0) {
112
+ return "upgrade";
113
+ }
114
+ return "none";
115
+ }
116
+ export async function ensurePiExtension(config, desiredVersion, dryRun = false) {
117
+ const action = piPackageAction(desiredVersion);
118
+ if (action === "none")
119
+ return;
120
+ if (dryRun) {
121
+ console.log(action === "install"
122
+ ? `would add ${PI_PACKAGE} to ${piSettingsPath()}`
123
+ : `would update ${PI_PACKAGE}`);
124
+ return;
125
+ }
126
+ if (action === "install") {
127
+ await installPiExtension(config);
128
+ return;
129
+ }
130
+ const result = spawnSync("pi", ["update", PI_PACKAGE], {
131
+ stdio: "inherit",
132
+ env: { ...process.env, PI_CODING_AGENT_DIR: piAgentDir() },
133
+ });
134
+ if (result.error || (result.status ?? 1) !== 0) {
135
+ console.warn(`warning: pi update ${PI_PACKAGE} failed`);
136
+ }
137
+ }
138
+ function stripPiManaged(content) {
139
+ const settings = JSON.parse(new TextDecoder("utf-8").decode(content));
140
+ if (!objectValue(settings))
141
+ return content;
142
+ const packages = settings.packages;
143
+ if (!Array.isArray(packages) || !packages.some(isCpacPackage))
144
+ return Buffer.from(`${JSON.stringify(settings, null, 2)}\n`);
145
+ settings.packages = packages.filter((entry) => !isCpacPackage(entry));
146
+ return Buffer.from(`${JSON.stringify(settings, null, 2)}\n`);
147
+ }
94
148
  export async function installPiExtension(config) {
95
149
  const path = piSettingsPath();
96
150
  mkdirSync(piAgentDir(), { recursive: true, mode: 0o700 });
97
- const settings = existsSync(path) ? readSettings(path) : {};
151
+ const hadFile = existsSync(path);
152
+ ensureCpacBackup(config.state_dir, "pi", path, !isPiPackageEnabled());
153
+ const settings = hadFile ? readSettings(path) : {};
98
154
  const packages = settingsPackages(settings, path) ?? [];
99
155
  if (!packages.some(isEnabledCpacPackage)) {
100
156
  settings.packages = [
@@ -103,7 +159,7 @@ export async function installPiExtension(config) {
103
159
  ];
104
160
  writeSettings(path, settings);
105
161
  }
106
- recordAgent(config.state_dir, "pi", path);
162
+ recordWrittenConfig(config.state_dir, "pi", path, undefined, hadFile);
107
163
  for (const file of removeLeftovers()) {
108
164
  console.log(`Removed leftover Pi extension: ${file}`);
109
165
  }
@@ -116,15 +172,12 @@ export async function uninstallPiExtension(config) {
116
172
  console.log(`Removed leftover Pi extension: ${file}`);
117
173
  removed = true;
118
174
  }
119
- if (existsSync(path)) {
175
+ if (isPiExtensionInstalled() || existsSync(path)) {
120
176
  try {
121
- const settings = readSettings(path);
122
- const packages = settingsPackages(settings, path);
123
- if (packages?.some(isCpacPackage)) {
124
- settings.packages = packages.filter((entry) => !isCpacPackage(entry));
125
- writeSettings(path, settings);
126
- console.log(`Removed Pi package ${PI_PACKAGE} from ${path}`);
177
+ if (existsSync(path)) {
178
+ restoreOwnedConfig(config.state_dir, "pi", path, stripPiManaged);
127
179
  removed = true;
180
+ console.log(`Removed Pi package ${PI_PACKAGE} from ${path}`);
128
181
  }
129
182
  }
130
183
  catch (error) {
@@ -0,0 +1,306 @@
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, objectValue, resolveApiKey, } from "../util.js";
7
+ export const ZED_PROVIDER_ID = "cpac";
8
+ const ZED_REASONING_EFFORTS = new Set([
9
+ "none",
10
+ "minimal",
11
+ "low",
12
+ "medium",
13
+ "high",
14
+ "xhigh",
15
+ "max",
16
+ ]);
17
+ export function zedHome() {
18
+ const override = process.env.ZED_CONFIG_DIR?.trim();
19
+ if (override)
20
+ return expandUserPath(override);
21
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
22
+ const base = xdg ? expandUserPath(xdg) : join(homedir(), ".config");
23
+ return join(base, "zed");
24
+ }
25
+ export function zedConfigPath() {
26
+ return join(zedHome(), "settings.json");
27
+ }
28
+ /** Strip // and /* *\/ comments outside strings for Zed JSONC settings. */
29
+ export function stripJsonc(text) {
30
+ let out = "";
31
+ let i = 0;
32
+ let inString = false;
33
+ let escape = false;
34
+ while (i < text.length) {
35
+ const c = text[i];
36
+ if (inString) {
37
+ out += c;
38
+ if (escape)
39
+ escape = false;
40
+ else if (c === "\\")
41
+ escape = true;
42
+ else if (c === '"')
43
+ inString = false;
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (c === '"') {
48
+ inString = true;
49
+ out += c;
50
+ i += 1;
51
+ continue;
52
+ }
53
+ if (c === "/" && text[i + 1] === "/") {
54
+ i += 2;
55
+ while (i < text.length && text[i] !== "\n")
56
+ i += 1;
57
+ continue;
58
+ }
59
+ if (c === "/" && text[i + 1] === "*") {
60
+ i += 2;
61
+ while (i + 1 < text.length && !(text[i] === "*" && text[i + 1] === "/"))
62
+ i += 1;
63
+ i += 2;
64
+ continue;
65
+ }
66
+ out += c;
67
+ i += 1;
68
+ }
69
+ return out;
70
+ }
71
+ /** Drop trailing commas before } or ] (Zed settings.json is JSONC). */
72
+ export function stripTrailingCommas(text) {
73
+ let out = "";
74
+ let i = 0;
75
+ let inString = false;
76
+ let escape = false;
77
+ while (i < text.length) {
78
+ const c = text[i];
79
+ if (inString) {
80
+ out += c;
81
+ if (escape)
82
+ escape = false;
83
+ else if (c === "\\")
84
+ escape = true;
85
+ else if (c === '"')
86
+ inString = false;
87
+ i += 1;
88
+ continue;
89
+ }
90
+ if (c === '"') {
91
+ inString = true;
92
+ out += c;
93
+ i += 1;
94
+ continue;
95
+ }
96
+ if (c === ",") {
97
+ let j = i + 1;
98
+ while (j < text.length && /\s/.test(text[j]))
99
+ j += 1;
100
+ if (text[j] === "}" || text[j] === "]") {
101
+ i += 1;
102
+ continue;
103
+ }
104
+ }
105
+ out += c;
106
+ i += 1;
107
+ }
108
+ return out;
109
+ }
110
+ function parseSettings(text) {
111
+ const parsed = JSON.parse(stripTrailingCommas(stripJsonc(text)));
112
+ if (!objectValue(parsed))
113
+ throw new CPACError("invalid Zed settings.json");
114
+ return parsed;
115
+ }
116
+ function readSettings(path) {
117
+ if (!existsSync(path))
118
+ return {};
119
+ return parseSettings(readFileSync(path, "utf8"));
120
+ }
121
+ export function isZedConfigInstalled() {
122
+ try {
123
+ const doc = readSettings(zedConfigPath());
124
+ const languageModels = objectValue(doc.language_models)
125
+ ? doc.language_models
126
+ : null;
127
+ const openaiCompatible = languageModels && objectValue(languageModels.openai_compatible)
128
+ ? languageModels.openai_compatible
129
+ : null;
130
+ return Boolean(openaiCompatible && objectValue(openaiCompatible[ZED_PROVIDER_ID]));
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ function zedSupportEffort(row) {
137
+ const levels = row.supported_reasoning_levels ??
138
+ row.reasoning_effort_levels ??
139
+ row.reasoning_levels;
140
+ if (!Array.isArray(levels))
141
+ return undefined;
142
+ const efforts = [];
143
+ const seen = new Set();
144
+ for (const level of levels) {
145
+ const effort = typeof level === "string"
146
+ ? level.toLowerCase()
147
+ : objectValue(level) && typeof level.effort === "string"
148
+ ? level.effort.toLowerCase()
149
+ : undefined;
150
+ if (!effort || !ZED_REASONING_EFFORTS.has(effort) || seen.has(effort))
151
+ continue;
152
+ if (effort === "none")
153
+ continue;
154
+ seen.add(effort);
155
+ efforts.push(effort);
156
+ }
157
+ if (efforts.length === 0)
158
+ return undefined;
159
+ const rawDefaultEffort = (typeof row.default_reasoning_level === "string" &&
160
+ row.default_reasoning_level) ||
161
+ (typeof row.default_reasoning_effort === "string" &&
162
+ row.default_reasoning_effort) ||
163
+ (typeof row.default_effort === "string" && row.default_effort) ||
164
+ undefined;
165
+ const preferred = typeof rawDefaultEffort === "string"
166
+ ? rawDefaultEffort.toLowerCase()
167
+ : undefined;
168
+ if (preferred && efforts.includes(preferred))
169
+ return preferred;
170
+ for (const candidate of ["high", "medium", "xhigh", "max", "low", "minimal"]) {
171
+ if (efforts.includes(candidate))
172
+ return candidate;
173
+ }
174
+ return efforts[0];
175
+ }
176
+ function zedInputHasImage(row) {
177
+ const modalities = row.input_modalities;
178
+ if (!Array.isArray(modalities))
179
+ return false;
180
+ return modalities.some((value) => value === "image");
181
+ }
182
+ function buildCpacProvider(port, rows) {
183
+ const available_models = [];
184
+ for (const row of rows) {
185
+ const slug = catalogModelId(row);
186
+ if (!slug)
187
+ continue;
188
+ const context = typeof row.context_window === "number" && row.context_window > 0
189
+ ? Math.floor(row.context_window)
190
+ : 200000;
191
+ const model = {
192
+ name: slug,
193
+ max_tokens: context,
194
+ capabilities: {
195
+ tools: true,
196
+ images: zedInputHasImage(row),
197
+ parallel_tool_calls: false,
198
+ prompt_cache_key: false,
199
+ // CPA speaks OpenAI Responses (same as Pi / Grok).
200
+ chat_completions: false,
201
+ interleaved_reasoning: false,
202
+ max_tokens_parameter: false,
203
+ },
204
+ };
205
+ if (typeof row.display_name === "string" && row.display_name.trim()) {
206
+ model.display_name = row.display_name.trim();
207
+ }
208
+ const maxOut = typeof row.max_output_tokens === "number" && row.max_output_tokens > 0
209
+ ? Math.floor(row.max_output_tokens)
210
+ : undefined;
211
+ if (maxOut)
212
+ model.max_output_tokens = maxOut;
213
+ const effort = zedSupportEffort(row);
214
+ if (effort)
215
+ model.reasoning_effort = effort;
216
+ available_models.push(model);
217
+ }
218
+ if (available_models.length === 0)
219
+ throw new CPACError("CPA catalog contains no models");
220
+ return {
221
+ api_url: `http://127.0.0.1:${port}/v1`,
222
+ available_models,
223
+ };
224
+ }
225
+ function applyZedProvider(doc, provider) {
226
+ const languageModels = objectValue(doc.language_models)
227
+ ? { ...doc.language_models }
228
+ : {};
229
+ const openaiCompatible = objectValue(languageModels.openai_compatible)
230
+ ? { ...languageModels.openai_compatible }
231
+ : {};
232
+ if (provider) {
233
+ openaiCompatible[ZED_PROVIDER_ID] = provider;
234
+ }
235
+ else {
236
+ delete openaiCompatible[ZED_PROVIDER_ID];
237
+ }
238
+ if (Object.keys(openaiCompatible).length > 0) {
239
+ languageModels.openai_compatible = openaiCompatible;
240
+ }
241
+ else {
242
+ delete languageModels.openai_compatible;
243
+ }
244
+ if (Object.keys(languageModels).length > 0) {
245
+ doc.language_models = languageModels;
246
+ }
247
+ else {
248
+ delete doc.language_models;
249
+ }
250
+ return doc;
251
+ }
252
+ function stripZedManaged(content) {
253
+ const doc = applyZedProvider(parseSettings(new TextDecoder("utf-8").decode(content)), null);
254
+ return Buffer.from(`${JSON.stringify(doc, null, 2)}\n`);
255
+ }
256
+ function writeZedProvider(path, provider) {
257
+ const doc = applyZedProvider(readSettings(path), provider);
258
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
259
+ const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
260
+ atomicWrite(path, Buffer.from(`${JSON.stringify(doc, null, 2)}\n`), mode);
261
+ }
262
+ export async function installZedConfig(config) {
263
+ const apiKey = await resolveApiKey(config.api_key_env);
264
+ const catalog = await fetchCatalog(config.cpa_url, apiKey);
265
+ let document;
266
+ try {
267
+ document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
268
+ }
269
+ catch {
270
+ throw new CPACError("invalid CPA catalog");
271
+ }
272
+ const rows = catalogModelRows(document) ?? [];
273
+ if (rows.length === 0)
274
+ throw new CPACError("CPA catalog contains no models");
275
+ const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
276
+ const provider = buildCpacProvider(proxy.port, rows);
277
+ const target = zedConfigPath();
278
+ const hadFile = existsSync(target);
279
+ try {
280
+ ensureCpacBackup(config.state_dir, "zed", target, !isZedConfigInstalled());
281
+ writeZedProvider(target, provider);
282
+ recordWrittenConfig(config.state_dir, "zed", target, {
283
+ id: proxy.id,
284
+ pid: proxy.pid,
285
+ port: proxy.port,
286
+ fingerprint,
287
+ }, hadFile);
288
+ }
289
+ catch (error) {
290
+ if (started)
291
+ await stopProxyProcess(proxy);
292
+ throw error;
293
+ }
294
+ console.log(`Installed Zed openai_compatible provider "${ZED_PROVIDER_ID}": ${target}`);
295
+ console.log(`Zed reads CPAC_API_KEY for this provider (any non-empty value; loopback injects ${config.api_key_env}).`);
296
+ }
297
+ export async function uninstallZedConfig(config) {
298
+ const target = zedConfigPath();
299
+ if (!isZedConfigInstalled())
300
+ throw new CPACError("Zed CPAC provider is not installed");
301
+ restoreOwnedConfig(config.state_dir, "zed", target, stripZedManaged);
302
+ const { stopProxy } = dropAgent(config.state_dir, "zed");
303
+ if (stopProxy)
304
+ await stopProxyProcess(stopProxy);
305
+ console.log(`Removed Zed openai_compatible provider "${ZED_PROVIDER_ID}": ${target}`);
306
+ }
package/dist/util.js CHANGED
@@ -205,9 +205,7 @@ export async function tabWizard(tabs) {
205
205
  };
206
206
  const resultOf = () => Object.fromEntries(state.map((tab) => [
207
207
  tab.id,
208
- [...tab.selected]
209
- .sort((a, b) => a - b)
210
- .map((i) => tab.items[i]),
208
+ [...tab.selected].sort((a, b) => a - b).map((i) => tab.items[i]),
211
209
  ]));
212
210
  const goNext = () => {
213
211
  if (index < state.length && state[index].selected.size === 0)
@@ -385,3 +383,14 @@ export function saveApiKeyExport(profile, name, apiKey) {
385
383
  const mode = existsSync(profile) ? statSync(profile).mode & 0o7777 : 0o600;
386
384
  atomicWrite(profile, Buffer.from(content), mode);
387
385
  }
386
+ export function compareVersions(a, b) {
387
+ const left = a.split(".").map(Number);
388
+ const right = b.split(".").map(Number);
389
+ for (let index = 0; index < Math.max(left.length, right.length); index++) {
390
+ const l = left[index] ?? 0;
391
+ const r = right[index] ?? 0;
392
+ if (l !== r)
393
+ return l > r ? 1 : -1;
394
+ }
395
+ return 0;
396
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {