@yhong91/cpac 0.1.2 → 0.1.3

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
@@ -175,12 +175,30 @@ CPAC state:~/.cpac
175
175
 
176
176
  为防止误删或覆盖,`state_dir` 不能是文件系统根目录、用户 home、系统临时目录,也不能包含 Codex 配置文件。
177
177
 
178
+ ### Pi
179
+
180
+ 安装 CPA provider 扩展到 Pi:
181
+
182
+ ```bash
183
+ cpac pi install
184
+ ```
185
+
186
+ 它将生成 `cpac.ts` 写入 `~/.pi/agent/extensions/`,Pi 启动时自动加载并从 CPA 动态注册模型目录。卸载:
187
+
188
+ ```bash
189
+ cpac pi uninstall
190
+ cpac pi status
191
+ ```
192
+
193
+ Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
194
+
178
195
  ## 环境变量
179
196
 
180
197
  | 变量 | 默认值 | 作用 |
181
198
  | --- | --- | --- |
182
199
  | `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
183
200
  | `CPA_BASE_URL` | `https://cpa.vibetime.cc` | 覆盖内置 CPA 地址 |
201
+ | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `cpac pi install` 写入位置) |
184
202
  | `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
185
203
  | `CODEX_HOME` | `~/.codex` | Codex home 目录 |
186
204
 
package/dist/cpac.js CHANGED
@@ -886,7 +886,8 @@ export async function status(config) {
886
886
  }
887
887
  }
888
888
  console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
889
- console.log(`pi: ${keyConfigured ? "ready" : "not configured"}`);
889
+ const piInstalled = isPiExtensionInstalled();
890
+ console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "extension not installed; run: cpac pi install") : "not configured"}`);
890
891
  if (!state)
891
892
  return 1;
892
893
  const proxy = stateProxy(state);
@@ -952,7 +953,7 @@ export function saveApiKeyExport(profile, name, apiKey) {
952
953
  atomicWrite(profile, Buffer.from(content), mode);
953
954
  }
954
955
  async function guide(config) {
955
- 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 claude [args...] Launch Claude Code through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi)\n cpac restore Restore the original Codex config\n cpac --help Show command usage`);
956
+ 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 claude [args...] Launch Claude Code through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi)\n cpac restore Restore the original Codex config\n cpac pi install Install CPA provider extension for Pi\n cpac pi uninstall Remove the Pi CPA provider extension\n cpac pi status Show Pi extension install status\n cpac --help Show command usage`);
956
957
  if (process.env[config.api_key_env]?.trim())
957
958
  return 0;
958
959
  const apiKey = await promptSecret(config.api_key_env);
@@ -1016,18 +1017,74 @@ export async function runProxy(config) {
1016
1017
  await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
1017
1018
  return 0;
1018
1019
  }
1020
+ function piExtensionsDir() {
1021
+ return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
1022
+ }
1023
+ export function isPiExtensionInstalled() {
1024
+ return existsSync(join(piExtensionsDir(), "cpac.ts"));
1025
+ }
1026
+ function piTemplatePath() {
1027
+ return join(dirname(fileURLToPath(import.meta.url)), "pi-extension.template");
1028
+ }
1029
+ function piExtensionContent(cpaUrl) {
1030
+ return readFileSync(piTemplatePath(), "utf8").replace("__CPA_URL__", cpaUrl);
1031
+ }
1032
+ export async function installPiExtension(config) {
1033
+ const dir = piExtensionsDir();
1034
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
1035
+ const target = join(dir, "cpac.ts");
1036
+ atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
1037
+ console.log(`Installed Pi extension: ${target}`);
1038
+ }
1039
+ export async function uninstallPiExtension() {
1040
+ const target = join(piExtensionsDir(), "cpac.ts");
1041
+ if (!existsSync(target))
1042
+ throw new CPACError("Pi extension is not installed");
1043
+ unlinkSync(target);
1044
+ console.log(`Removed Pi extension: ${target}`);
1045
+ }
1046
+ export async function runPi(config, action) {
1047
+ if (action === "install") {
1048
+ await installPiExtension(config);
1049
+ return 0;
1050
+ }
1051
+ if (action === "uninstall") {
1052
+ await uninstallPiExtension();
1053
+ return 0;
1054
+ }
1055
+ if (action === "status") {
1056
+ const installed = isPiExtensionInstalled();
1057
+ console.log(`Pi extension: ${installed ? "installed" : "not installed"}`);
1058
+ return installed ? 0 : 1;
1059
+ }
1060
+ throw new CPACError(`unknown pi action: ${action}; use install, uninstall, or status`);
1061
+ }
1019
1062
  function usage() {
1020
1063
  return [
1021
1064
  "Usage: cpac",
1022
1065
  " cpac <inject|status|restore> [--config PATH]",
1023
1066
  " cpac proxy [--config PATH]",
1024
1067
  " cpac claude [--config PATH] [--] [claude args...]",
1068
+ " cpac pi <install|uninstall|status> [--config PATH]",
1025
1069
  ].join("\n");
1026
1070
  }
1027
1071
  function parseArgs(args) {
1028
1072
  if (args.length === 0) {
1029
1073
  return { command: "guide", configPath: defaultConfigPath() };
1030
1074
  }
1075
+ if (args[0] === "pi") {
1076
+ let configPath = defaultConfigPath();
1077
+ let index = 1;
1078
+ if (args[index] === "--config") {
1079
+ const value = args[++index];
1080
+ if (!value)
1081
+ throw new CPACError("--config requires a path");
1082
+ configPath = resolve(expandUserPath(value));
1083
+ index += 1;
1084
+ }
1085
+ const action = args[index] || "status";
1086
+ return { command: "pi", configPath, action };
1087
+ }
1031
1088
  if (args[0] === "claude") {
1032
1089
  let configPath = defaultConfigPath();
1033
1090
  let index = 1;
@@ -1088,6 +1145,8 @@ export async function main(args = process.argv.slice(2)) {
1088
1145
  return await runClaude(config, parsed.args);
1089
1146
  if (parsed.command === "proxy")
1090
1147
  return await runProxy(config);
1148
+ if (parsed.command === "pi")
1149
+ return await runPi(config, parsed.action);
1091
1150
  if (parsed.command === "inject")
1092
1151
  await inject(config);
1093
1152
  else if (parsed.command === "restore")
@@ -0,0 +1,83 @@
1
+ // CPAC Pi extension: register CPA models as Pi providers
2
+ // Installed by: cpac pi install
3
+
4
+ const CPA = "__CPA_URL__";
5
+ const BUILTIN = new Set(["openai", "github-copilot", "xai", "deepseek", "anthropic", "google"]);
6
+ const VENDOR_PREFIXES = [
7
+ ["gpt-", "openai"], ["o1-", "openai"], ["o3-", "openai"], ["o4-", "openai"],
8
+ ["claude-", "anthropic"], ["gemini-", "google"], ["grok-", "xai"],
9
+ ["deepseek-", "deepseek"], ["glm-", "zhipu"], ["kimi-", "moonshot"],
10
+ ["mimo-", "xiaomi"], ["doubao-", "volcengine"], ["ark-", "volcengine"],
11
+ ["minimax-", "minimax"], ["step-", "stepfun"], ["qwen-", "alibaba"],
12
+ ["hunyuan-", "tencent"],
13
+ ];
14
+ const MAX_TOKENS = {
15
+ "gpt-5.6-sol": 128000, "gpt-5.6-terra": 128000, "gpt-5.6-luna": 128000,
16
+ "claude-opus-4-6-thinking": 128000, "claude-sonnet-4-6": 64000,
17
+ "gemini-3.6-flash": 65536, "gemini-3.6-flash-high": 65536,
18
+ "deepseek-v4-pro": 128000, "deepseek-v4-flash": 128000,
19
+ "glm-5.2": 128000, "kimi-k2.7-code": 128000, "minimax-m3": 128000,
20
+ "mimo-v2.5": 131072, "mimo-v2.5-pro": 131072, "grok-4.5": 128000,
21
+ "doubao-seed-2.0-lite": 128000, "doubao-seed-2.1-turbo": 128000,
22
+ };
23
+ const DEFAULT_MAX_TOKENS = 65536;
24
+
25
+ function vendorFor(slug) {
26
+ for (const [prefix, vendor] of VENDOR_PREFIXES) {
27
+ if (slug.startsWith(prefix)) return vendor;
28
+ }
29
+ return "misc";
30
+ }
31
+
32
+ function groupName(vendor) {
33
+ return BUILTIN.has(vendor) ? vendor + "-cpa" : vendor;
34
+ }
35
+
36
+ function intField(value) {
37
+ return typeof value === "number" && Number.isFinite(value) && value > 0
38
+ ? Math.floor(value) : undefined;
39
+ }
40
+
41
+ export default async function (pi) {
42
+ const apiKey = (process.env.CPA_API_KEY || "").trim();
43
+ if (!apiKey) return;
44
+ let payload;
45
+ try {
46
+ const res = await fetch(CPA + "/v1/models?client_version=1", {
47
+ headers: { Authorization: "Bearer " + apiKey },
48
+ signal: AbortSignal.timeout(10000),
49
+ });
50
+ if (!res.ok) return;
51
+ payload = await res.json();
52
+ } catch { return; }
53
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.models)) return;
54
+ const rows = payload.models.filter(function (m) {
55
+ return m && typeof m === "object" && typeof m.slug === "string";
56
+ });
57
+ const groups = new Map();
58
+ for (const m of rows) {
59
+ const name = groupName(vendorFor(m.slug));
60
+ if (!groups.has(name)) groups.set(name, []);
61
+ groups.get(name).push(m);
62
+ }
63
+ for (const [provName, models] of groups) {
64
+ const vendor = provName.replace(/-cpa$/, "");
65
+ pi.registerProvider(provName, {
66
+ name: vendor + " (cpa)",
67
+ baseUrl: CPA + "/v1",
68
+ apiKey: apiKey,
69
+ api: "openai-responses",
70
+ models: models.map(function (m) {
71
+ return {
72
+ id: m.slug,
73
+ name: typeof m.display_name === "string" ? m.display_name : m.slug,
74
+ reasoning: Array.isArray(m.supported_reasoning_levels) && m.supported_reasoning_levels.length > 0,
75
+ input: ["text", "image"],
76
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
77
+ contextWindow: intField(m.context_window) || 200000,
78
+ maxTokens: MAX_TOKENS[m.slug] || DEFAULT_MAX_TOKENS,
79
+ };
80
+ }),
81
+ });
82
+ }
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist/cpac.js",
11
+ "dist/pi-extension.template",
11
12
  "cpac.example.json",
12
13
  "README.md"
13
14
  ],
@@ -28,10 +29,10 @@
28
29
  },
29
30
  "scripts": {
30
31
  "clean": "node -e \"for (const d of ['dist','dist-test']) require('fs').rmSync(d,{recursive:true,force:true})\"",
31
- "build": "npm run clean --if-present && tsc -p tsconfig.build.json && node -e \"const fs=require('fs');const p='dist/cpac.js';const s=fs.readFileSync(p,'utf8');if(!s.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+s);try{fs.chmodSync(p,0o755)}catch{}\"",
32
+ "build": "npm run clean --if-present && tsc -p tsconfig.build.json && cp src/pi-extension.template dist/ && node -e \"const fs=require('fs');const p='dist/cpac.js';const s=fs.readFileSync(p,'utf8');if(!s.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+s);try{fs.chmodSync(p,0o755)}catch{}\"",
32
33
  "check": "tsc -p tsconfig.json --noEmit",
33
34
  "check:pi": "tsc -p tsconfig.pi.json --noEmit",
34
- "test": "npm run clean && tsc -p tsconfig.test.json && node --test --test-reporter=spec dist-test/cpac.test.js",
35
+ "test": "npm run clean && tsc -p tsconfig.test.json && cp src/pi-extension.template dist-test/src/ && node --test --test-reporter=spec dist-test/cpac.test.js",
35
36
  "pack:check": "npm pack --dry-run"
36
37
  },
37
38
  "devDependencies": {