@yhong91/cpac 0.1.4 → 0.1.6

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
@@ -91,6 +91,24 @@ cpac
91
91
  cpac --help
92
92
  ```
93
93
 
94
+ ### 统一管理(detect / install / uninstall / upgrade)
95
+
96
+ 参考 vibetime 的目标式命令,对 `codex`、`claude`、`pi`、`kimi` 四个目标统一管理:
97
+
98
+ ```bash
99
+ cpac detect [--json] [--home PATH]
100
+ cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--home PATH]
101
+ cpac uninstall [--target codex,claude,pi,kimi] [--all] [--dry-run] [--home PATH]
102
+ cpac upgrade [--check]
103
+ cpac version
104
+ ```
105
+
106
+ - `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
107
+ - `install` 默认作用于检测到的目标;`--target` 指定目标、`--all` 全部。各目标映射到既有逻辑:`codex` → `cpac inject`,`pi` → `cpac pi install`,`kimi` → `cpac kimi install`;`claude` 无持久化安装(用 `cpac claude` 启动)。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
108
+ - `uninstall` 默认作用于已安装目标,映射到 `cpac restore` / `cpac pi uninstall` / `cpac kimi uninstall`。
109
+ - `--home PATH` 临时把 `HOME` 指向给定目录,便于在隔离环境中试验。
110
+ - `upgrade` 检查 npm 上是否有新版本,`--check` 只报告不提示安装命令。
111
+
94
112
  ### Claude Code
95
113
 
96
114
  通过 CPA 启动 Claude Code,后续参数原样传递:
@@ -114,6 +132,8 @@ CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1
114
132
 
115
133
  代理开启 Claude Code 的 gateway model discovery:`/model` 选择器会列出 CPA 目录里的全部模型。claude 系模型直接使用原 id;其它模型以 `claude-cpac--<模型名>` 别名出现(例如 `claude-cpac--gpt-5.6-sol`),请求发出时由代理还原为真实模型名。
116
134
 
135
+ Claude Code 的发现协议不携带 context 信息,未知模型默认按 200K 处理。`cpac claude` 启动时会从 CPA 目录取最大 `context_window`,通过 `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 告知 Claude Code(对非 claude 名称的模型生效);你自己 export 该变量时以你为准。claude 系模型如需 1M 窗口,在模型名后加 `[1m]`(如 `cpac claude --model claude-sonnet-4-6[1m]`)。
136
+
117
137
  > CPA 服务端必须支持 Claude Code 使用的 Anthropic Messages API(`/v1/messages`)。
118
138
 
119
139
  ### Codex
@@ -196,6 +216,23 @@ cpac pi status
196
216
 
197
217
  Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
198
218
 
219
+ ### Kimi Code
220
+
221
+ 写入 CPA provider 配置到 Kimi Code:
222
+
223
+ ```bash
224
+ cpac kimi install
225
+ ```
226
+
227
+ 它在 `~/.kimi-code/config.toml` 追加一个由 CPAC 管理的块,注册 `cpac` OpenAI 兼容 provider,并把 CPA 目录里的每个模型映射为一个 `cpac/<模型名>` 条目。`base_url` 指向 Codex 注入使用的 loopback 代理,因此密钥不落盘;代理未运行时 install 会提示先执行 `cpac inject`。卸载与状态:
228
+
229
+ ```bash
230
+ cpac kimi uninstall
231
+ cpac kimi status
232
+ ```
233
+
234
+ Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若 `config.toml` 已存在,会在同目录保留一份原始快照 `config.toml.cpac-backup`(只存最早版本),万一文件损坏可手动复制回去。
235
+
199
236
  ## 环境变量
200
237
 
201
238
  | 变量 | 默认值 | 作用 |
@@ -203,6 +240,7 @@ Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
203
240
  | `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
204
241
  | `CPA_BASE_URL` | `https://cpa.vibetime.cc` | 覆盖内置 CPA 地址 |
205
242
  | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `cpac pi install` 写入位置) |
243
+ | `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac kimi install` 写入位置) |
206
244
  | `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
207
245
  | `CODEX_HOME` | `~/.codex` | Codex home 目录 |
208
246
 
package/dist/cpac.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import { spawn } from "node:child_process";
2
+ import { chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { spawn, spawnSync } from "node:child_process";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import { createServer, request as httpRequest, } from "node:http";
6
6
  import { request as httpsRequest } from "node:https";
@@ -1016,6 +1016,8 @@ export async function status(config) {
1016
1016
  console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
1017
1017
  const piInstalled = isPiExtensionInstalled();
1018
1018
  console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "extension not installed; run: cpac pi install") : "not configured"}`);
1019
+ const kimiInstalled = isKimiConfigInstalled();
1020
+ console.log(`kimi: ${keyConfigured ? (kimiInstalled ? "ready" : "config not installed; run: cpac kimi install") : "not configured"}`);
1019
1021
  if (!state)
1020
1022
  return 1;
1021
1023
  const proxy = stateProxy(state);
@@ -1078,7 +1080,7 @@ export function saveApiKeyExport(profile, name, apiKey) {
1078
1080
  atomicWrite(profile, Buffer.from(content), mode);
1079
1081
  }
1080
1082
  async function guide(config) {
1081
- 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`);
1083
+ 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, Kimi)\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 kimi install Write CPA provider config for Kimi Code\n cpac kimi uninstall Remove the Kimi Code CPA provider config\n cpac kimi status Show Kimi Code config install status\n cpac detect Show supported targets and install status\n cpac install Install CPA integration into detected targets\n cpac uninstall Remove CPA integration from installed targets\n cpac upgrade Check for cpac updates on npm\n cpac --help Show command usage`);
1082
1084
  if (process.env[config.api_key_env]?.trim())
1083
1085
  return 0;
1084
1086
  const apiKey = await promptSecret(config.api_key_env);
@@ -1095,6 +1097,22 @@ export async function runClaude(config, args, executable = "claude") {
1095
1097
  proxy.server.closeAllConnections?.();
1096
1098
  proxy.server.close();
1097
1099
  };
1100
+ // Gateway discovery only carries {id, display_name}; Claude Code defaults
1101
+ // unknown models to 200K. CLAUDE_CODE_MAX_CONTEXT_TOKENS applies to
1102
+ // non-claude model names, so feed it the largest catalog window.
1103
+ let maxContext = 0;
1104
+ try {
1105
+ const catalog = await fetchCatalog(config.cpa_url, apiKey);
1106
+ const rows = catalogModelRows(JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes))) ?? [];
1107
+ for (const row of rows) {
1108
+ if (typeof row.context_window === "number" && row.context_window > maxContext) {
1109
+ maxContext = row.context_window;
1110
+ }
1111
+ }
1112
+ }
1113
+ catch {
1114
+ // keep the default window when the catalog is unreachable
1115
+ }
1098
1116
  const env = {
1099
1117
  ...process.env,
1100
1118
  ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxy.port}`,
@@ -1102,6 +1120,9 @@ export async function runClaude(config, args, executable = "claude") {
1102
1120
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
1103
1121
  CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
1104
1122
  };
1123
+ if (maxContext > 0 && !(Number(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) > 0)) {
1124
+ env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(maxContext);
1125
+ }
1105
1126
  delete env.ANTHROPIC_API_KEY;
1106
1127
  delete env.CLAUDE_CODE_USE_ANTHROPIC_AWS;
1107
1128
  delete env.CLAUDE_CODE_USE_BEDROCK;
@@ -1169,6 +1190,7 @@ export async function installPiExtension(config) {
1169
1190
  const dir = piExtensionsDir();
1170
1191
  mkdirSync(dir, { recursive: true, mode: 0o700 });
1171
1192
  const target = join(dir, "cpac.ts");
1193
+ ensureCpacBackup(target);
1172
1194
  atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
1173
1195
  console.log(`Installed Pi extension: ${target}`);
1174
1196
  }
@@ -1195,6 +1217,314 @@ export async function runPi(config, action) {
1195
1217
  }
1196
1218
  throw new CPACError(`unknown pi action: ${action}; use install, uninstall, or status`);
1197
1219
  }
1220
+ // Pre-write snapshot kept at <target>.cpac-backup (first version only) so a
1221
+ // damaged config can be restored by hand even if uninstall cannot parse it.
1222
+ function ensureCpacBackup(target) {
1223
+ if (!existsSync(target))
1224
+ return;
1225
+ const backup = `${target}.cpac-backup`;
1226
+ if (existsSync(backup))
1227
+ return;
1228
+ copyFileSync(target, backup);
1229
+ }
1230
+ function kimiConfigPath() {
1231
+ const home = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
1232
+ return join(expandUserPath(home), "config.toml");
1233
+ }
1234
+ const KIMI_BLOCK_START = "# >>> CPAC Kimi >>>";
1235
+ const KIMI_BLOCK_END = "# <<< CPAC Kimi <<<";
1236
+ const kimiBlockRegex = new RegExp(`${KIMI_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${KIMI_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
1237
+ export function isKimiConfigInstalled() {
1238
+ try {
1239
+ return kimiBlockRegex.test(readFileSync(kimiConfigPath(), "utf8"));
1240
+ }
1241
+ catch {
1242
+ return false;
1243
+ }
1244
+ }
1245
+ function writeKimiBlock(path, block) {
1246
+ let content = existsSync(path) ? readFileSync(path, "utf8") : "";
1247
+ content = content.replace(kimiBlockRegex, "");
1248
+ if (block) {
1249
+ content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
1250
+ }
1251
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
1252
+ const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
1253
+ atomicWrite(path, Buffer.from(content), mode);
1254
+ }
1255
+ export async function installKimiConfig(config) {
1256
+ const apiKey = await resolveApiKey(config.api_key_env);
1257
+ const catalog = await fetchCatalog(config.cpa_url, apiKey);
1258
+ const document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
1259
+ const rows = catalogModelRows(document) ?? [];
1260
+ if (rows.length === 0)
1261
+ throw new CPACError("CPA catalog contains no models");
1262
+ const state = readState(config.state_dir);
1263
+ const recorded = state ? stateProxy(state) : null;
1264
+ const port = recorded?.port ?? config.codex_proxy_port;
1265
+ if (!port) {
1266
+ throw new CPACError("loopback proxy port unknown; run cpac inject first");
1267
+ }
1268
+ const lines = [
1269
+ KIMI_BLOCK_START,
1270
+ "[providers.cpac]",
1271
+ 'type = "openai"',
1272
+ `base_url = "http://127.0.0.1:${port}/v1"`,
1273
+ '# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.',
1274
+ 'api_key = "cpac-loopback"',
1275
+ ];
1276
+ for (const row of rows) {
1277
+ const slug = catalogModelId(row);
1278
+ if (!slug)
1279
+ continue;
1280
+ // ponytail: Kimi requires max_context_size; default 200000 when the catalog omits it.
1281
+ const context = typeof row.context_window === "number" && row.context_window > 0
1282
+ ? Math.floor(row.context_window)
1283
+ : 200000;
1284
+ lines.push("", `[models."cpac/${slug}"]`, 'provider = "cpac"', `model = ${tomlString(slug)}`, `max_context_size = ${context}`);
1285
+ if (typeof row.display_name === "string" && row.display_name.trim()) {
1286
+ lines.push(`display_name = ${tomlString(row.display_name)}`);
1287
+ }
1288
+ }
1289
+ lines.push(KIMI_BLOCK_END);
1290
+ const target = kimiConfigPath();
1291
+ ensureCpacBackup(target);
1292
+ writeKimiBlock(target, lines.join("\n"));
1293
+ console.log(`Installed Kimi Code provider config: ${target}`);
1294
+ if (!recorded || !(await proxyIsHealthy(recorded))) {
1295
+ console.log("Loopback proxy is not running; run: cpac inject");
1296
+ }
1297
+ }
1298
+ export async function uninstallKimiConfig() {
1299
+ const target = kimiConfigPath();
1300
+ if (!isKimiConfigInstalled())
1301
+ throw new CPACError("Kimi Code config is not installed");
1302
+ writeKimiBlock(target, null);
1303
+ console.log(`Removed Kimi Code provider config: ${target}`);
1304
+ }
1305
+ export async function runKimi(config, action) {
1306
+ if (action === "install") {
1307
+ await installKimiConfig(config);
1308
+ return 0;
1309
+ }
1310
+ if (action === "uninstall") {
1311
+ await uninstallKimiConfig();
1312
+ return 0;
1313
+ }
1314
+ if (action === "status") {
1315
+ const installed = isKimiConfigInstalled();
1316
+ console.log(`Kimi Code config: ${installed ? "installed" : "not installed"}`);
1317
+ return installed ? 0 : 1;
1318
+ }
1319
+ throw new CPACError(`unknown kimi action: ${action}; use install, uninstall, or status`);
1320
+ }
1321
+ function readPackageVersion() {
1322
+ try {
1323
+ const raw = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8");
1324
+ const version = JSON.parse(raw).version;
1325
+ return typeof version === "string" ? version : "0.0.0-dev";
1326
+ }
1327
+ catch {
1328
+ return "0.0.0-dev";
1329
+ }
1330
+ }
1331
+ const CPAC_VERSION = readPackageVersion();
1332
+ function binaryOnPath(name) {
1333
+ if (!/^[\w.-]+$/.test(name))
1334
+ return false;
1335
+ try {
1336
+ return spawnSync(`command -v ${name}`, { stdio: "ignore", shell: true }).status === 0;
1337
+ }
1338
+ catch {
1339
+ return false;
1340
+ }
1341
+ }
1342
+ const TARGETS = [
1343
+ {
1344
+ id: "codex",
1345
+ home: () => expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex")),
1346
+ detected: function () {
1347
+ return existsSync(this.home());
1348
+ },
1349
+ installed: (config) => {
1350
+ try {
1351
+ return hasProviderTable(readFileSync(config.codex_config, "utf8"));
1352
+ }
1353
+ catch {
1354
+ return false;
1355
+ }
1356
+ },
1357
+ install: async (config, dryRun) => {
1358
+ if (dryRun) {
1359
+ console.log("would inject CPA catalog and loopback proxy into Codex");
1360
+ return;
1361
+ }
1362
+ await inject(config);
1363
+ },
1364
+ uninstall: async (config, dryRun) => {
1365
+ if (dryRun) {
1366
+ console.log("would restore the original Codex config");
1367
+ return;
1368
+ }
1369
+ await restore(config);
1370
+ },
1371
+ },
1372
+ {
1373
+ id: "claude",
1374
+ home: () => expandUserPath(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude")),
1375
+ detected: () => binaryOnPath("claude") || existsSync(join(homedir(), ".claude")),
1376
+ installed: () => false,
1377
+ install: async (_config, dryRun) => {
1378
+ const message = "claude needs no install; launch it through CPA with: cpac claude";
1379
+ console.log(dryRun ? `would do nothing: ${message}` : message);
1380
+ },
1381
+ uninstall: async (_config, dryRun) => {
1382
+ const message = "claude keeps no persistent CPAC state; nothing to remove";
1383
+ console.log(dryRun ? `would do nothing: ${message}` : message);
1384
+ },
1385
+ },
1386
+ {
1387
+ id: "pi",
1388
+ home: () => piExtensionsDir(),
1389
+ detected: () => binaryOnPath("pi") || existsSync(piExtensionsDir()),
1390
+ installed: () => isPiExtensionInstalled(),
1391
+ install: async (config, dryRun) => {
1392
+ if (dryRun) {
1393
+ console.log(`would write Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
1394
+ return;
1395
+ }
1396
+ await installPiExtension(config);
1397
+ },
1398
+ uninstall: async (_config, dryRun) => {
1399
+ if (dryRun) {
1400
+ console.log(`would remove Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
1401
+ return;
1402
+ }
1403
+ await uninstallPiExtension();
1404
+ },
1405
+ },
1406
+ {
1407
+ id: "kimi",
1408
+ home: () => dirname(kimiConfigPath()),
1409
+ detected: () => binaryOnPath("kimi") || existsSync(dirname(kimiConfigPath())),
1410
+ installed: () => isKimiConfigInstalled(),
1411
+ install: async (config, dryRun) => {
1412
+ if (dryRun) {
1413
+ console.log(`would write CPA provider block: ${kimiConfigPath()}`);
1414
+ return;
1415
+ }
1416
+ await installKimiConfig(config);
1417
+ },
1418
+ uninstall: async (_config, dryRun) => {
1419
+ if (dryRun) {
1420
+ console.log(`would remove CPA provider block: ${kimiConfigPath()}`);
1421
+ return;
1422
+ }
1423
+ await uninstallKimiConfig();
1424
+ },
1425
+ },
1426
+ ];
1427
+ export async function detectTargets(config) {
1428
+ return TARGETS.map((target) => ({
1429
+ id: target.id,
1430
+ detected: target.detected(),
1431
+ installed: target.installed(config),
1432
+ path: target.home(),
1433
+ }));
1434
+ }
1435
+ export async function runDetect(config, asJson) {
1436
+ const targets = await detectTargets(config);
1437
+ if (asJson) {
1438
+ console.log(JSON.stringify({ version: CPAC_VERSION, targets }, null, 2));
1439
+ return 0;
1440
+ }
1441
+ for (const target of targets) {
1442
+ console.log(`${target.id.padEnd(8)} ${(target.detected ? "detected" : "missing").padEnd(9)} ${(target.installed ? "installed" : "not installed").padEnd(14)} ${target.path}`);
1443
+ }
1444
+ return 0;
1445
+ }
1446
+ function selectTargets(requested, all, eligible) {
1447
+ const unknown = requested.filter((id) => !TARGETS.some((target) => target.id === id));
1448
+ if (unknown.length > 0) {
1449
+ throw new CPACError(`unknown target(s): ${unknown.join(", ")}; use ${TARGETS.map((target) => target.id).join(",")}`);
1450
+ }
1451
+ const ids = requested.length > 0 ? requested : all ? TARGETS.map((target) => target.id) : eligible;
1452
+ return TARGETS.filter((target) => ids.includes(target.id));
1453
+ }
1454
+ export async function runInstall(config, requested, options) {
1455
+ const eligible = TARGETS.filter((target) => target.detected()).map((target) => target.id);
1456
+ const selected = selectTargets(requested, options.all, eligible);
1457
+ if (selected.length === 0) {
1458
+ throw new CPACError(`no supported targets detected; use --target ${TARGETS.map((target) => target.id).join(",")} or --all`);
1459
+ }
1460
+ for (const target of selected) {
1461
+ if (target.installed(config) && !options.force) {
1462
+ console.log(`${target.id}: already installed; use --force to reinstall`);
1463
+ continue;
1464
+ }
1465
+ await target.install(config, options.dryRun);
1466
+ }
1467
+ return 0;
1468
+ }
1469
+ export async function runUninstall(config, requested, options) {
1470
+ const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
1471
+ const selected = selectTargets(requested, options.all, eligible);
1472
+ if (selected.length === 0) {
1473
+ throw new CPACError("no installed CPAC integrations found; use --target <id> or --all");
1474
+ }
1475
+ for (const target of selected) {
1476
+ if (!target.installed(config)) {
1477
+ console.log(`${target.id}: not installed`);
1478
+ continue;
1479
+ }
1480
+ await target.uninstall(config, options.dryRun);
1481
+ }
1482
+ return 0;
1483
+ }
1484
+ function compareVersions(a, b) {
1485
+ const left = a.split(".").map(Number);
1486
+ const right = b.split(".").map(Number);
1487
+ for (let index = 0; index < Math.max(left.length, right.length); index++) {
1488
+ const l = left[index] ?? 0;
1489
+ const r = right[index] ?? 0;
1490
+ if (l !== r)
1491
+ return l > r ? 1 : -1;
1492
+ }
1493
+ return 0;
1494
+ }
1495
+ export async function runUpgrade(checkOnly) {
1496
+ console.log(`Current version: ${CPAC_VERSION}`);
1497
+ let latest;
1498
+ try {
1499
+ const response = await fetch("https://registry.npmjs.org/@yhong91/cpac/latest", {
1500
+ headers: { Accept: "application/json" },
1501
+ signal: AbortSignal.timeout(10_000),
1502
+ });
1503
+ if (response.ok) {
1504
+ const data = (await response.json());
1505
+ if (typeof data.version === "string")
1506
+ latest = data.version;
1507
+ }
1508
+ }
1509
+ catch {
1510
+ latest = undefined;
1511
+ }
1512
+ if (!latest) {
1513
+ console.error("Could not fetch latest version from npm. Check your network connection.");
1514
+ return 1;
1515
+ }
1516
+ console.log(`Latest version: ${latest}`);
1517
+ if (compareVersions(CPAC_VERSION, latest) >= 0) {
1518
+ console.log("Already up to date.");
1519
+ return 0;
1520
+ }
1521
+ if (checkOnly) {
1522
+ console.log(`Update available. Run: npm install -g @yhong91/cpac@latest`);
1523
+ return 0;
1524
+ }
1525
+ console.log(`Update available. Run: npm install -g @yhong91/cpac@latest`);
1526
+ return 0;
1527
+ }
1198
1528
  function usage() {
1199
1529
  return [
1200
1530
  "Usage: cpac",
@@ -1202,13 +1532,73 @@ function usage() {
1202
1532
  " cpac proxy [--config PATH]",
1203
1533
  " cpac claude [--config PATH] [--] [claude args...]",
1204
1534
  " cpac pi <install|uninstall|status> [--config PATH]",
1535
+ " cpac kimi <install|uninstall|status> [--config PATH]",
1536
+ " cpac detect [--json] [--home PATH]",
1537
+ " cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--home PATH]",
1538
+ " cpac uninstall [--target codex,claude,pi,kimi] [--all] [--dry-run] [--home PATH]",
1539
+ " cpac upgrade [--check]",
1540
+ " cpac version",
1205
1541
  ].join("\n");
1206
1542
  }
1207
1543
  function parseArgs(args) {
1208
1544
  if (args.length === 0) {
1209
1545
  return { command: "guide", configPath: defaultConfigPath() };
1210
1546
  }
1211
- if (args[0] === "pi") {
1547
+ if (args[0] === "detect" ||
1548
+ args[0] === "install" ||
1549
+ args[0] === "uninstall" ||
1550
+ args[0] === "upgrade" ||
1551
+ args[0] === "version") {
1552
+ const parsed = {
1553
+ command: args[0],
1554
+ configPath: defaultConfigPath(),
1555
+ json: false,
1556
+ targets: [],
1557
+ all: false,
1558
+ dryRun: false,
1559
+ force: false,
1560
+ check: false,
1561
+ };
1562
+ for (let index = 1; index < args.length; index++) {
1563
+ const arg = args[index];
1564
+ if (arg === "--json")
1565
+ parsed.json = true;
1566
+ else if (arg === "--all")
1567
+ parsed.all = true;
1568
+ else if (arg === "--dry-run")
1569
+ parsed.dryRun = true;
1570
+ else if (arg === "--force")
1571
+ parsed.force = true;
1572
+ else if (arg === "--check")
1573
+ parsed.check = true;
1574
+ else if (arg === "--target" || arg === "--targets") {
1575
+ const value = args[++index];
1576
+ if (!value)
1577
+ throw new CPACError(`${arg} requires a comma-separated list`);
1578
+ parsed.targets.push(...value
1579
+ .split(",")
1580
+ .map((entry) => entry.trim())
1581
+ .filter(Boolean));
1582
+ }
1583
+ else if (arg === "--home") {
1584
+ const value = args[++index];
1585
+ if (!value)
1586
+ throw new CPACError("--home requires a path");
1587
+ parsed.home = resolve(expandUserPath(value));
1588
+ }
1589
+ else if (arg === "--config") {
1590
+ const value = args[++index];
1591
+ if (!value)
1592
+ throw new CPACError("--config requires a path");
1593
+ parsed.configPath = resolve(expandUserPath(value));
1594
+ }
1595
+ else {
1596
+ throw new CPACError(`unknown option: ${arg}`);
1597
+ }
1598
+ }
1599
+ return parsed;
1600
+ }
1601
+ if (args[0] === "pi" || args[0] === "kimi") {
1212
1602
  let configPath = defaultConfigPath();
1213
1603
  let index = 1;
1214
1604
  if (args[index] === "--config") {
@@ -1219,7 +1609,7 @@ function parseArgs(args) {
1219
1609
  index += 1;
1220
1610
  }
1221
1611
  const action = args[index] || "status";
1222
- return { command: "pi", configPath, action };
1612
+ return { command: args[0], configPath, action };
1223
1613
  }
1224
1614
  if (args[0] === "claude") {
1225
1615
  let configPath = defaultConfigPath();
@@ -1274,15 +1664,38 @@ export async function main(args = process.argv.slice(2)) {
1274
1664
  const parsed = parseArgs(args);
1275
1665
  if (!parsed)
1276
1666
  return 0;
1667
+ if ("home" in parsed && parsed.home)
1668
+ process.env.HOME = parsed.home;
1277
1669
  const config = loadConfig(parsed.configPath, true);
1278
1670
  if (parsed.command === "guide")
1279
1671
  return await guide(config);
1672
+ if (parsed.command === "detect")
1673
+ return await runDetect(config, parsed.json);
1674
+ if (parsed.command === "install")
1675
+ return await runInstall(config, parsed.targets, {
1676
+ all: parsed.all,
1677
+ dryRun: parsed.dryRun,
1678
+ force: parsed.force,
1679
+ });
1680
+ if (parsed.command === "uninstall")
1681
+ return await runUninstall(config, parsed.targets, {
1682
+ all: parsed.all,
1683
+ dryRun: parsed.dryRun,
1684
+ });
1685
+ if (parsed.command === "upgrade")
1686
+ return await runUpgrade(parsed.check);
1687
+ if (parsed.command === "version") {
1688
+ console.log(CPAC_VERSION);
1689
+ return 0;
1690
+ }
1280
1691
  if (parsed.command === "claude")
1281
1692
  return await runClaude(config, parsed.args);
1282
1693
  if (parsed.command === "proxy")
1283
1694
  return await runProxy(config);
1284
1695
  if (parsed.command === "pi")
1285
1696
  return await runPi(config, parsed.action);
1697
+ if (parsed.command === "kimi")
1698
+ return await runKimi(config, parsed.action);
1286
1699
  if (parsed.command === "inject")
1287
1700
  await inject(config);
1288
1701
  else if (parsed.command === "restore")
@@ -16,7 +16,7 @@ const MAX_TOKENS = {
16
16
  "claude-opus-4-6-thinking": 128000, "claude-sonnet-4-6": 64000,
17
17
  "gemini-3.6-flash": 65536, "gemini-3.6-flash-high": 65536,
18
18
  "deepseek-v4-pro": 128000, "deepseek-v4-flash": 128000,
19
- "glm-5.2": 128000, "kimi-k2.7-code": 128000, "minimax-m3": 128000,
19
+ "glm-5.3": 128000, "kimi-k2.7-code": 128000, "minimax-m3": 128000,
20
20
  "mimo-v2.5": 131072, "mimo-v2.5-pro": 131072, "grok-4.5": 128000,
21
21
  "doubao-seed-2.0-lite": 128000, "doubao-seed-2.1-turbo": 128000,
22
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {