@yhong91/cpac 0.1.42 → 0.1.44

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
@@ -178,7 +178,7 @@ cpac -v | --version
178
178
  - `models` 从远端 CPA 拉取 rich catalog,默认一行一个 slug;`--json` 输出 `cpa_url` 和模型字段(`slug` / `display_name` / `context_window`)。用来核对 catalog 里有没有某个模型(例如 `*-fast`),不写本地配置。
179
179
  - `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
180
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` 有效。
181
+ - `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi / 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` 时未安装的目标静默跳过。
@@ -255,7 +255,7 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。注入会删除 `models
255
255
 
256
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` / `cpac hermes` 即可恢复。
258
+ loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac pi` / `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` 即可恢复。
259
259
 
260
260
  默认路径:
261
261
 
@@ -268,7 +268,7 @@ CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录
268
268
  Grok ~/.cpac/grok/config.toml
269
269
  Zed ~/.cpac/zed/settings.json
270
270
  Hermes ~/.cpac/hermes/config.yaml
271
- Pi ~/.cpac/pi/settings.json
271
+ Pi ~/.cpac/pi/models.json
272
272
  restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
273
273
  ```
274
274
 
@@ -281,7 +281,7 @@ cpac install pi
281
281
  cpac pi
282
282
  ```
283
283
 
284
- 未接入时把 `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
+ 它在 `~/.pi/agent/models.json` 的 `providers.cpac` 写入 loopback `baseUrl`、占位 `apiKey`、`api: openai-responses`,以及 CPA catalog 的模型列表。不改当前默认模型;切换用 `/model`。密钥由 loopback 注入。旧的 `npm:@yhong91/cpac` package `extensions/pi-cpac.ts` 会在 install / restore 时删掉。
285
285
 
286
286
  卸载:`cpac restore pi` 或 `cpac pi clear`。尊重 `PI_CODING_AGENT_DIR`。
287
287
 
@@ -341,7 +341,7 @@ cpac restore hermes
341
341
  | --- | --- | --- |
342
342
  | `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
343
343
  | `CPA_BASE_URL` | `http://124.223.178.52:8317` | 覆盖内置 CPA 地址 |
344
- | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `settings.json` 写入位置) |
344
+ | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `models.json` 写入位置) |
345
345
  | `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
346
346
  | `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
347
347
  | `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
package/dist/agents.js CHANGED
@@ -7,7 +7,7 @@ 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, ensurePiExtension, isPiExtensionInstalled, piAgentDir, piSettingsPath, uninstallPiExtension, } from "./targets/pi.js";
10
+ import { installPiConfig, isPiConfigInstalled, piAgentDir, piModelsPath, uninstallPiConfig, } from "./targets/pi.js";
11
11
  import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
12
12
  import { hermesConfigPath, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
13
13
  import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, tabWizard, } from "./util.js";
@@ -222,17 +222,21 @@ const TARGETS = [
222
222
  id: "pi",
223
223
  home: () => piAgentDir(),
224
224
  detected: () => binaryOnPath("pi") || existsSync(piAgentDir()),
225
- installed: () => isPiExtensionInstalled(),
225
+ installed: () => isPiConfigInstalled(),
226
226
  version: () => detectClientVersion("pi"),
227
227
  install: async (config, dryRun) => {
228
- await ensurePiExtension(config, CPAC_VERSION, dryRun);
228
+ if (dryRun) {
229
+ console.log(`would write CPA provider: ${piModelsPath()}`);
230
+ return;
231
+ }
232
+ await installPiConfig(config);
229
233
  },
230
234
  uninstall: async (config, dryRun) => {
231
235
  if (dryRun) {
232
- console.log(`would remove ${PI_PACKAGE} from ${piSettingsPath()}`);
236
+ console.log(`would remove CPA provider: ${piModelsPath()}`);
233
237
  return;
234
238
  }
235
- await uninstallPiExtension(config);
239
+ await uninstallPiConfig(config);
236
240
  },
237
241
  },
238
242
  {
package/dist/config.js CHANGED
@@ -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 || agents.hermes);
528
+ return Boolean(agents.codex || agents.pi || 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
@@ -8,7 +8,7 @@ import { runClaude, runClaudeConfig } from "./targets/claude.js";
8
8
  import { multiAgentV2Enabled } from "./targets/codex.js";
9
9
  import { isGrokConfigInstalled } from "./targets/grok.js";
10
10
  import { isKimiConfigInstalled } from "./targets/kimi.js";
11
- import { isPiExtensionInstalled } from "./targets/pi.js";
11
+ import { isPiConfigInstalled } from "./targets/pi.js";
12
12
  import { isZedConfigInstalled } from "./targets/zed.js";
13
13
  import { isHermesConfigInstalled } from "./targets/hermes.js";
14
14
  import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, } from "./config.js";
@@ -22,7 +22,7 @@ export { claudeTierSlots, createClaudeProxy, runClaude, } from "./targets/claude
22
22
  export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, runTargetLauncher, } from "./agents.js";
23
23
  export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
24
24
  export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
25
- export { ensurePiExtension, installPiExtension, isPiExtensionInstalled, piPackageAction, piPackageVersion, uninstallPiExtension, } from "./targets/pi.js";
25
+ export { installPiConfig, isPiConfigInstalled, uninstallPiConfig, PI_PROVIDER_ID, } from "./targets/pi.js";
26
26
  export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
27
27
  export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
28
28
  export async function status(config) {
@@ -70,8 +70,8 @@ export async function status(config) {
70
70
  console.log("codex: native");
71
71
  }
72
72
  console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
73
- const piInstalled = isPiExtensionInstalled();
74
- console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "extension not installed; run: cpac install pi") : "not configured"}`);
73
+ const piInstalled = isPiConfigInstalled();
74
+ console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "config not installed; run: cpac install pi") : "not configured"}`);
75
75
  const kimiInstalled = isKimiConfigInstalled();
76
76
  console.log(`kimi: ${keyConfigured ? (kimiInstalled ? "ready" : "config not installed; run: cpac install kimi") : "not configured"}`);
77
77
  const grokInstalled = isGrokConfigInstalled();
@@ -76,10 +76,12 @@ function yamlKey(key) {
76
76
  }
77
77
  function cpacChildLines(port, rows, indent) {
78
78
  const pad = (n) => " ".repeat(n);
79
+ const host = ["127", "0", "0", "1"].join(".");
80
+ const url = `http://${host}:${port}/v1`;
79
81
  const lines = [
80
82
  `${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)}base_url: ${JSON.stringify(url)}`,
84
+ `${pad(indent + 2)}api: ${JSON.stringify(url)}`,
83
85
  `${pad(indent + 2)}api_key: cpac`,
84
86
  `${pad(indent + 2)}api_mode: responses`,
85
87
  `${pad(indent + 2)}discover_models: false`,
@@ -1,16 +1,27 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { dropAgent, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
6
- import { stopProxyProcess } from "../proxy.js";
7
- import { CPACError, atomicWrite, compareVersions, ensureCpacBackup, expandUserPath, objectValue, } from "../util.js";
8
- export const PI_PACKAGE = "npm:@yhong91/cpac";
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 PI_PROVIDER_ID = "cpac";
9
8
  const PI_PACKAGE_NAME = "@yhong91/cpac";
10
9
  const LEGACY_EXTENSIONS = ["pi-cpac.ts", "cpac.ts", "pi-cpa.ts"];
10
+ const PI_REASONING_EFFORTS = new Set([
11
+ "minimal",
12
+ "low",
13
+ "medium",
14
+ "high",
15
+ "xhigh",
16
+ "max",
17
+ "ultra",
18
+ ]);
11
19
  export function piAgentDir() {
12
20
  return expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"));
13
21
  }
22
+ export function piModelsPath() {
23
+ return join(piAgentDir(), "models.json");
24
+ }
14
25
  export function piSettingsPath() {
15
26
  return join(piAgentDir(), "settings.json");
16
27
  }
@@ -37,38 +48,24 @@ function isCpacPackage(entry) {
37
48
  const source = packageSource(entry);
38
49
  return source !== undefined && specName(source) === PI_PACKAGE_NAME;
39
50
  }
40
- function isEnabledCpacPackage(entry) {
41
- if (!isCpacPackage(entry))
42
- return false;
43
- if (typeof entry === "string")
44
- return !entry.startsWith("-");
45
- return !objectValue(entry) || entry.enabled !== false;
46
- }
47
- function readSettings(path) {
51
+ function readJsonObject(path, label) {
52
+ if (!existsSync(path))
53
+ return {};
48
54
  let raw;
49
55
  try {
50
56
  raw = JSON.parse(readFileSync(path, "utf8"));
51
57
  }
52
- catch (error) {
53
- if (error.code === "ENOENT")
54
- return {};
55
- throw new CPACError(`cannot parse Pi settings: ${path}`);
56
- }
57
- if (!objectValue(raw)) {
58
- throw new CPACError(`Pi settings must be a JSON object: ${path}`);
58
+ catch {
59
+ throw new CPACError(`cannot parse Pi ${label}: ${path}`);
59
60
  }
61
+ if (!objectValue(raw))
62
+ throw new CPACError(`Pi ${label} must be a JSON object: ${path}`);
60
63
  return raw;
61
64
  }
62
- function settingsPackages(settings, path) {
63
- if (!Object.hasOwn(settings, "packages"))
64
- return undefined;
65
- if (!Array.isArray(settings.packages)) {
66
- throw new CPACError(`Pi settings packages must be an array: ${path}`);
67
- }
68
- return settings.packages;
69
- }
70
- function writeSettings(path, value) {
71
- atomicWrite(path, Buffer.from(`${JSON.stringify(value, null, 2)}\n`));
65
+ function writeJson(path, value) {
66
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
67
+ const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
68
+ atomicWrite(path, Buffer.from(`${JSON.stringify(value, null, 2)}\n`), mode);
72
69
  }
73
70
  function removeLeftovers() {
74
71
  const removed = [];
@@ -78,116 +75,206 @@ function removeLeftovers() {
78
75
  }
79
76
  return removed;
80
77
  }
81
- function isPiPackageEnabled() {
78
+ function hasLeftoverPackage() {
79
+ try {
80
+ const settings = readJsonObject(piSettingsPath(), "settings.json");
81
+ const packages = settings.packages;
82
+ return Array.isArray(packages) && packages.some(isCpacPackage);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function stripLeftoverPackage() {
82
89
  const path = piSettingsPath();
83
90
  if (!existsSync(path))
84
91
  return false;
92
+ const settings = readJsonObject(path, "settings.json");
93
+ const packages = settings.packages;
94
+ if (!Array.isArray(packages) || !packages.some(isCpacPackage))
95
+ return false;
96
+ settings.packages = packages.filter((entry) => !isCpacPackage(entry));
97
+ writeJson(path, settings);
98
+ return true;
99
+ }
100
+ function hasCpacModels() {
85
101
  try {
86
- const packages = settingsPackages(readSettings(path), path);
87
- return packages?.some(isEnabledCpacPackage) ?? false;
102
+ const doc = readJsonObject(piModelsPath(), "models.json");
103
+ const providers = objectValue(doc.providers) ? doc.providers : null;
104
+ return Boolean(providers && objectValue(providers[PI_PROVIDER_ID]));
88
105
  }
89
106
  catch {
90
107
  return false;
91
108
  }
92
109
  }
93
- export function isPiExtensionInstalled() {
94
- return leftoverExtensions().length > 0 || isPiPackageEnabled();
110
+ export function isPiConfigInstalled() {
111
+ return hasCpacModels() || leftoverExtensions().length > 0 || hasLeftoverPackage();
95
112
  }
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;
113
+ function loopbackUrl(port) {
114
+ const host = ["127", "0", "0", "1"].join(".");
115
+ return `http://${host}:${port}/v1`;
116
+ }
117
+ function effortsFor(row) {
118
+ const levels = row.supported_reasoning_levels;
119
+ if (!Array.isArray(levels))
120
+ return undefined;
121
+ const efforts = [];
122
+ const seen = new Set();
123
+ for (const level of levels) {
124
+ const effort = typeof level === "string"
125
+ ? level.toLowerCase()
126
+ : objectValue(level) && typeof level.effort === "string"
127
+ ? level.effort.toLowerCase()
128
+ : undefined;
129
+ if (!effort || !PI_REASONING_EFFORTS.has(effort) || seen.has(effort))
130
+ continue;
131
+ seen.add(effort);
132
+ efforts.push(effort);
101
133
  }
102
- catch {
134
+ return efforts.length > 0 ? efforts : undefined;
135
+ }
136
+ function thinkingLevelMapFor(efforts) {
137
+ if (!efforts)
103
138
  return undefined;
139
+ const available = new Set(efforts);
140
+ return {
141
+ minimal: available.has("minimal")
142
+ ? "minimal"
143
+ : available.has("low")
144
+ ? "low"
145
+ : null,
146
+ low: available.has("low") ? "low" : null,
147
+ medium: available.has("medium") ? "medium" : null,
148
+ high: available.has("high") ? "high" : null,
149
+ xhigh: available.has("xhigh") ? "xhigh" : null,
150
+ max: available.has("max") || available.has("ultra") ? "max" : null,
151
+ };
152
+ }
153
+ function inputFor(row) {
154
+ const modalities = row.input_modalities;
155
+ if (!Array.isArray(modalities))
156
+ return ["text", "image"];
157
+ return modalities.some((value) => value === "image")
158
+ ? ["text", "image"]
159
+ : ["text"];
160
+ }
161
+ function buildCpacProvider(port, rows) {
162
+ const models = [];
163
+ for (const row of rows) {
164
+ const slug = catalogModelId(row);
165
+ if (!slug)
166
+ continue;
167
+ const context = typeof row.context_window === "number" && row.context_window > 0
168
+ ? Math.floor(row.context_window)
169
+ : 200000;
170
+ const maxOut = typeof row.max_output_tokens === "number" && row.max_output_tokens > 0
171
+ ? Math.floor(row.max_output_tokens)
172
+ : 65536;
173
+ const efforts = effortsFor(row);
174
+ const thinkingLevelMap = thinkingLevelMapFor(efforts);
175
+ const model = {
176
+ id: slug,
177
+ name: typeof row.display_name === "string" && row.display_name.trim()
178
+ ? row.display_name.trim()
179
+ : slug,
180
+ reasoning: efforts === undefined || thinkingLevelMap !== undefined,
181
+ input: inputFor(row),
182
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
183
+ contextWindow: context,
184
+ maxTokens: maxOut,
185
+ compat: {
186
+ sendSessionAffinityHeaders: true,
187
+ sessionAffinityFormat: "openai-nosession",
188
+ },
189
+ };
190
+ if (thinkingLevelMap)
191
+ model.thinkingLevelMap = thinkingLevelMap;
192
+ models.push(model);
104
193
  }
194
+ if (models.length === 0)
195
+ throw new CPACError("CPA catalog contains no models");
196
+ return {
197
+ baseUrl: loopbackUrl(port),
198
+ api: "openai-responses",
199
+ apiKey: "cpac",
200
+ models,
201
+ };
202
+ }
203
+ function applyPiProvider(doc, provider) {
204
+ const providers = objectValue(doc.providers) ? { ...doc.providers } : {};
205
+ if (provider)
206
+ providers[PI_PROVIDER_ID] = provider;
207
+ else
208
+ delete providers[PI_PROVIDER_ID];
209
+ if (Object.keys(providers).length > 0)
210
+ doc.providers = providers;
211
+ else
212
+ delete doc.providers;
213
+ return doc;
214
+ }
215
+ function stripPiManaged(content) {
216
+ const doc = applyPiProvider(JSON.parse(new TextDecoder("utf-8").decode(content)), null);
217
+ return Buffer.from(`${JSON.stringify(doc, null, 2)}\n`);
218
+ }
219
+ function writePiProvider(path, provider) {
220
+ const doc = applyPiProvider(readJsonObject(path, "models.json"), provider);
221
+ writeJson(path, doc);
105
222
  }
106
- export function piPackageAction(desiredVersion) {
107
- if (leftoverExtensions().length > 0 || !isPiExtensionInstalled()) {
108
- return "install";
223
+ export async function installPiConfig(config) {
224
+ const apiKey = await resolveApiKey(config.api_key_env);
225
+ const catalog = await fetchCatalog(config.cpa_url, apiKey);
226
+ let document;
227
+ try {
228
+ document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
109
229
  }
110
- const version = piPackageVersion();
111
- if (version !== undefined && compareVersions(version, desiredVersion) < 0) {
112
- return "upgrade";
230
+ catch {
231
+ throw new CPACError("invalid CPA catalog");
113
232
  }
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;
233
+ const rows = catalogModelRows(document) ?? [];
234
+ if (rows.length === 0)
235
+ throw new CPACError("CPA catalog contains no models");
236
+ const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
237
+ const provider = buildCpacProvider(proxy.port, rows);
238
+ const target = piModelsPath();
239
+ const hadFile = existsSync(target);
240
+ try {
241
+ ensureCpacBackup(config.state_dir, "pi", target, !hasCpacModels());
242
+ writePiProvider(target, provider);
243
+ recordWrittenConfig(config.state_dir, "pi", target, {
244
+ id: proxy.id,
245
+ pid: proxy.pid,
246
+ port: proxy.port,
247
+ fingerprint,
248
+ }, hadFile);
125
249
  }
126
- if (action === "install") {
127
- await installPiExtension(config);
128
- return;
250
+ catch (error) {
251
+ if (started)
252
+ await stopProxyProcess(proxy);
253
+ throw error;
129
254
  }
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`);
255
+ if (stripLeftoverPackage()) {
256
+ console.log(`Removed leftover Pi package npm:${PI_PACKAGE_NAME} from ${piSettingsPath()}`);
136
257
  }
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
- }
148
- export async function installPiExtension(config) {
149
- const path = piSettingsPath();
150
- mkdirSync(piAgentDir(), { recursive: true, mode: 0o700 });
151
- const hadFile = existsSync(path);
152
- ensureCpacBackup(config.state_dir, "pi", path, !isPiPackageEnabled());
153
- const settings = hadFile ? readSettings(path) : {};
154
- const packages = settingsPackages(settings, path) ?? [];
155
- if (!packages.some(isEnabledCpacPackage)) {
156
- settings.packages = [
157
- ...packages.filter((entry) => !isCpacPackage(entry)),
158
- PI_PACKAGE,
159
- ];
160
- writeSettings(path, settings);
161
- }
162
- recordWrittenConfig(config.state_dir, "pi", path, undefined, hadFile);
163
258
  for (const file of removeLeftovers()) {
164
259
  console.log(`Removed leftover Pi extension: ${file}`);
165
260
  }
166
- console.log(`Installed Pi package ${PI_PACKAGE} in ${path}`);
261
+ console.log(`Installed Pi provider "${PI_PROVIDER_ID}": ${target}`);
167
262
  }
168
- export async function uninstallPiExtension(config) {
169
- const path = piSettingsPath();
170
- let removed = false;
263
+ export async function uninstallPiConfig(config) {
264
+ const target = piModelsPath();
265
+ if (!hasCpacModels() && leftoverExtensions().length === 0 && !hasLeftoverPackage())
266
+ throw new CPACError("Pi CPAC provider is not installed");
267
+ if (hasCpacModels()) {
268
+ restoreOwnedConfig(config.state_dir, "pi", target, stripPiManaged);
269
+ }
270
+ if (stripLeftoverPackage()) {
271
+ console.log(`Removed leftover Pi package npm:${PI_PACKAGE_NAME} from ${piSettingsPath()}`);
272
+ }
171
273
  for (const file of removeLeftovers()) {
172
274
  console.log(`Removed leftover Pi extension: ${file}`);
173
- removed = true;
174
- }
175
- if (isPiExtensionInstalled() || existsSync(path)) {
176
- try {
177
- if (existsSync(path)) {
178
- restoreOwnedConfig(config.state_dir, "pi", path, stripPiManaged);
179
- removed = true;
180
- console.log(`Removed Pi package ${PI_PACKAGE} from ${path}`);
181
- }
182
- }
183
- catch (error) {
184
- if (!removed)
185
- throw error;
186
- }
187
- }
188
- if (!removed)
189
- throw new CPACError("Pi extension is not installed");
275
+ }
190
276
  const { stopProxy } = dropAgent(config.state_dir, "pi");
191
277
  if (stopProxy)
192
278
  await stopProxyProcess(stopProxy);
279
+ console.log(`Removed Pi provider "${PI_PROVIDER_ID}": ${target}`);
193
280
  }
package/package.json CHANGED
@@ -1,22 +1,13 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cpac": "dist/cpac.js"
8
8
  },
9
- "keywords": [
10
- "pi-package"
11
- ],
12
- "pi": {
13
- "extensions": [
14
- "./extensions/pi-cpac.ts"
15
- ]
16
- },
17
9
  "files": [
18
10
  "dist",
19
- "extensions",
20
11
  "cpac.example.json",
21
12
  "README.md"
22
13
  ],
@@ -43,8 +34,6 @@
43
34
  "pack:check": "npm pack --dry-run"
44
35
  },
45
36
  "devDependencies": {
46
- "@earendil-works/pi-ai": "^0.84.1",
47
- "@earendil-works/pi-coding-agent": "^0.84.1",
48
37
  "@types/node": "^22.13.10",
49
38
  "typescript": "~5.8.2"
50
39
  },
@@ -1,205 +0,0 @@
1
- // Pi CPAC extension: register CPA models as Pi providers
2
- // Loaded via package.json "pi.extensions" when settings include npm:@yhong91/cpac
3
-
4
- // keep in sync with src/config.ts DEFAULT_CPA_URL
5
- const CPA = (process.env.CPA_BASE_URL || "http://124.223.178.52:8317").replace(
6
- /\/+$/,
7
- "",
8
- );
9
- const BUILTIN = new Set([
10
- "openai",
11
- "github-copilot",
12
- "xai",
13
- "deepseek",
14
- "anthropic",
15
- "google",
16
- ]);
17
- const VENDOR_PREFIXES = [
18
- ["gpt-", "openai"],
19
- ["o1-", "openai"],
20
- ["o3-", "openai"],
21
- ["o4-", "openai"],
22
- ["gpt-image", "openai"],
23
- ["claude-", "anthropic"],
24
- ["gemini-", "google"],
25
- ["grok-", "xai"],
26
- ["deepseek-", "deepseek"],
27
- ["glm-", "zhipu"],
28
- ["kimi-", "moonshot"],
29
- ["mimo-", "xiaomi"],
30
- ["doubao-", "volcengine"],
31
- ["ark-", "volcengine"],
32
- ["minimax-", "minimax"],
33
- ["step-", "stepfun"],
34
- ["qwen-", "alibaba"],
35
- ["hunyuan-", "tencent"],
36
- ];
37
- const MAX_TOKENS = {
38
- "gpt-5.6-sol": 128000,
39
- "gpt-5.6-terra": 128000,
40
- "gpt-5.6-luna": 128000,
41
- "claude-opus-4-6-thinking": 128000,
42
- "claude-sonnet-4-6": 64000,
43
- "gemini-3.6-flash": 65536,
44
- "gemini-3.6-flash-high": 65536,
45
- "deepseek-v4-pro": 128000,
46
- "deepseek-v4-flash": 128000,
47
- "glm-5.3": 128000,
48
- "kimi-k2.7-code": 128000,
49
- "minimax-m3": 128000,
50
- "mimo-v2.5": 131072,
51
- "mimo-v2.5-pro": 131072,
52
- "grok-4.5": 128000,
53
- "doubao-seed-2.0-lite": 128000,
54
- "doubao-seed-2.1-turbo": 128000,
55
- };
56
- const DEFAULT_MAX_TOKENS = 65536;
57
- const REASONING_EFFORTS = new Set([
58
- "minimal",
59
- "low",
60
- "medium",
61
- "high",
62
- "xhigh",
63
- "max",
64
- "ultra",
65
- ]);
66
-
67
- function vendorFor(slug) {
68
- for (const [prefix, vendor] of VENDOR_PREFIXES) {
69
- if (slug.startsWith(prefix)) return vendor;
70
- }
71
- return "misc";
72
- }
73
-
74
- function groupName(vendor) {
75
- return BUILTIN.has(vendor) ? vendor + "-cpa" : vendor;
76
- }
77
-
78
- function effortsFor(row) {
79
- const levels = row.supported_reasoning_levels;
80
- if (!Array.isArray(levels)) return undefined;
81
- const efforts = levels.flatMap((level) => {
82
- if (!level || typeof level !== "object") return [];
83
- const effort = level.effort;
84
- return typeof effort === "string" && REASONING_EFFORTS.has(effort)
85
- ? [effort]
86
- : [];
87
- });
88
- return efforts.length > 0 ? efforts : undefined;
89
- }
90
-
91
- function thinkingLevelMapFor(efforts) {
92
- if (!efforts || !efforts.some((effort) => REASONING_EFFORTS.has(effort))) {
93
- return undefined;
94
- }
95
- const available = new Set(efforts);
96
- return {
97
- minimal: available.has("minimal")
98
- ? "minimal"
99
- : available.has("low")
100
- ? "low"
101
- : null,
102
- low: available.has("low") ? "low" : null,
103
- medium: available.has("medium") ? "medium" : null,
104
- high: available.has("high") ? "high" : null,
105
- xhigh: available.has("xhigh") ? "xhigh" : null,
106
- max: available.has("max") || available.has("ultra") ? "max" : null,
107
- };
108
- }
109
-
110
- function intField(value) {
111
- return typeof value === "number" && Number.isFinite(value) && value > 0
112
- ? Math.floor(value)
113
- : undefined;
114
- }
115
-
116
- export default async function (pi) {
117
- const apiKey = (process.env.CPA_API_KEY || "").trim();
118
- if (!apiKey) {
119
- console.error("[pi-cpac] CPA_API_KEY not set; skipping CPA providers");
120
- return;
121
- }
122
- let payload;
123
- try {
124
- const res = await fetch(CPA + "/v1/models?client_version=1", {
125
- headers: { Authorization: "Bearer " + apiKey },
126
- signal: AbortSignal.timeout(10000),
127
- });
128
- if (!res.ok) {
129
- console.error("[pi-cpac] CPA models request failed: HTTP " + res.status);
130
- return;
131
- }
132
- payload = await res.json();
133
- } catch (error) {
134
- console.error(
135
- "[pi-cpac] CPA models request failed: " +
136
- ((error && error.message) || error),
137
- );
138
- return;
139
- }
140
- const source =
141
- payload && Array.isArray(payload.models)
142
- ? payload.models
143
- : payload && Array.isArray(payload.data)
144
- ? payload.data
145
- : null;
146
- if (!source) {
147
- console.error("[pi-cpac] CPA models response has no models list");
148
- return;
149
- }
150
- const rows = [];
151
- for (const m of source) {
152
- if (!m || typeof m !== "object") continue;
153
- const slug =
154
- typeof m.slug === "string" && m.slug.trim()
155
- ? m.slug.trim()
156
- : typeof m.id === "string" && m.id.trim()
157
- ? m.id.trim()
158
- : null;
159
- if (slug)
160
- rows.push({
161
- slug: slug,
162
- display_name: m.display_name,
163
- supported_reasoning_levels: m.supported_reasoning_levels,
164
- context_window: m.context_window,
165
- });
166
- }
167
- if (rows.length === 0) {
168
- console.error("[pi-cpac] CPA models response has no usable models");
169
- return;
170
- }
171
- const groups = new Map();
172
- for (const m of rows) {
173
- const name = groupName(vendorFor(m.slug));
174
- if (!groups.has(name)) groups.set(name, []);
175
- groups.get(name).push(m);
176
- }
177
- for (const [provName, models] of groups) {
178
- const vendor = provName.replace(/-cpa$/, "");
179
- pi.registerProvider(provName, {
180
- name: vendor + " (cpa)",
181
- baseUrl: CPA + "/v1",
182
- apiKey: apiKey,
183
- api: "openai-responses",
184
- models: models.map((m) => {
185
- const efforts = effortsFor(m);
186
- const thinkingLevelMap = thinkingLevelMapFor(efforts);
187
- const modelObj = {
188
- id: m.slug,
189
- name: typeof m.display_name === "string" ? m.display_name : m.slug,
190
- reasoning: efforts === undefined || thinkingLevelMap !== undefined,
191
- input: ["text", "image"],
192
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
193
- contextWindow: intField(m.context_window) || 200000,
194
- maxTokens: MAX_TOKENS[m.slug] || DEFAULT_MAX_TOKENS,
195
- compat: {
196
- sendSessionAffinityHeaders: true,
197
- sessionAffinityFormat: "openai-nosession",
198
- },
199
- };
200
- if (thinkingLevelMap) modelObj.thinkingLevelMap = thinkingLevelMap;
201
- return modelObj;
202
- }),
203
- });
204
- }
205
- }