@yhong91/cpac 0.1.27 → 0.1.29
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 +5 -5
- package/dist/agents.js +10 -7
- package/dist/config.js +40 -1
- package/dist/pi-extension.template +6 -6
- package/dist/proxy.js +2 -2
- package/dist/targets/codex.js +62 -9
- package/dist/targets/pi.js +24 -6
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CPAC 将 Codex 和 Claude Code 接入远端 CLIProxyAPI(CPA):
|
|
4
4
|
|
|
5
|
-
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider
|
|
5
|
+
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider。无用户改动时逐字节恢复原配置;注入期间改过文件则只撤掉 CPAC 字段。
|
|
6
6
|
- **Claude Code**:通过临时环境变量启动,不修改 Claude Code 配置。
|
|
7
7
|
|
|
8
8
|
实现基于 TypeScript + Node.js,运行时零依赖。
|
|
@@ -41,7 +41,7 @@ cpac install --all # 注入所有检测到的目标(codex/
|
|
|
41
41
|
cpac install --target codex # 只注入 Codex(默认开启 multi-agent v2)
|
|
42
42
|
cpac install --target codex --v2_models # 先多选 spawn_agent 模型再注入
|
|
43
43
|
cpac install --target codex --max_context # 注入时把模型上下文提升到实测上限(opt-in,仅限 codex)
|
|
44
|
-
cpac restore --target codex #
|
|
44
|
+
cpac restore --target codex # 撤掉注入;注入期间的用户改动会留下
|
|
45
45
|
|
|
46
46
|
# 4b. 临时接管(退出无痕,不写任何配置文件)
|
|
47
47
|
cpac claude # Claude Code 走 CPA
|
|
@@ -248,7 +248,7 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。`inject` 会删除 `mod
|
|
|
248
248
|
|
|
249
249
|
注入命令可选 `--max_context`(`cpac inject --max_context` 或 `cpac install --target 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 组合使用会直接报错。
|
|
250
250
|
|
|
251
|
-
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore`
|
|
251
|
+
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore`:若 `config.toml` 相对首次注入未改,按原 mode 逐字节还原备份;若用户在注入期间改过文件,只撤掉 CPAC 写入的 catalog、url 和标记。原先没有 `config.toml` 且撤掉后为空则删除文件。关闭 CPAC loopback 代理。
|
|
252
252
|
|
|
253
253
|
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac inject` 即可恢复。
|
|
254
254
|
|
|
@@ -285,7 +285,7 @@ cpac opencode [--] [opencode args...]
|
|
|
285
285
|
cpac pi install
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
-
它将生成 `cpac.ts` 写入 `~/.pi/agent/extensions/`,Pi 启动时自动加载并从 CPA 动态注册模型目录。卸载:
|
|
288
|
+
它将生成 `pi-cpac.ts` 写入 `~/.pi/agent/extensions/`,Pi 启动时自动加载并从 CPA 动态注册模型目录。卸载:
|
|
289
289
|
|
|
290
290
|
```bash
|
|
291
291
|
cpac restore --target pi
|
|
@@ -326,7 +326,7 @@ CPAC CLI 可临时覆盖 CPA 地址:
|
|
|
326
326
|
CPA_BASE_URL='https://cpa.example.com' cpac claude
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
-
`pi-
|
|
329
|
+
`pi-cpac.ts` 插件固定连接配置指定的 CPA 地址,并且只从 Pi 进程环境读取 `CPA_API_KEY`;不读取用户 home 中的私有文件。
|
|
330
330
|
|
|
331
331
|
## 可选 JSON 配置
|
|
332
332
|
|
package/dist/agents.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { fetchCatalog, pickSpawnModels, proxyFingerprint, readState, saveSpawnModels, stateBytes, stateProxy, } from "./config.js";
|
|
6
|
+
import { fetchCatalog, injectedOwnership, pickSpawnModels, proxyFingerprint, readState, saveSpawnModels, stateBytes, stateProxy, } from "./config.js";
|
|
7
7
|
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "./proxy.js";
|
|
8
8
|
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
9
9
|
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
@@ -76,13 +76,16 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
76
76
|
if (["kimi", "grok"].includes(targetId)) {
|
|
77
77
|
const state = readState(config.state_dir);
|
|
78
78
|
const proxy = state ? stateProxy(state) : null;
|
|
79
|
-
|
|
79
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
80
|
+
const fingerprint = proxyFingerprint(config, apiKey);
|
|
81
|
+
const reuse = proxy !== null &&
|
|
82
|
+
state?.proxy_fingerprint === fingerprint &&
|
|
83
|
+
(await proxyIsHealthy(proxy));
|
|
84
|
+
if (!reuse) {
|
|
80
85
|
if (proxy)
|
|
81
86
|
await stopProxyProcess(proxy);
|
|
82
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
83
87
|
const started = await startProxyProcess(config, apiKey);
|
|
84
88
|
try {
|
|
85
|
-
const fingerprint = proxyFingerprint(config, apiKey);
|
|
86
89
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
87
90
|
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
88
91
|
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
@@ -90,7 +93,7 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
90
93
|
const originalMode = existsSync(config.codex_config)
|
|
91
94
|
? statSync(config.codex_config).mode & 0o7777
|
|
92
95
|
: 0o600;
|
|
93
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint));
|
|
96
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint, injectedOwnership(state)));
|
|
94
97
|
}
|
|
95
98
|
catch (error) {
|
|
96
99
|
await stopProxyProcess(started);
|
|
@@ -206,14 +209,14 @@ const TARGETS = [
|
|
|
206
209
|
version: () => detectClientVersion("pi"),
|
|
207
210
|
install: async (config, dryRun) => {
|
|
208
211
|
if (dryRun) {
|
|
209
|
-
console.log(`would write Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
212
|
+
console.log(`would write Pi extension: ${join(piExtensionsDir(), "pi-cpac.ts")}`);
|
|
210
213
|
return;
|
|
211
214
|
}
|
|
212
215
|
await installPiExtension(config);
|
|
213
216
|
},
|
|
214
217
|
uninstall: async (_config, dryRun) => {
|
|
215
218
|
if (dryRun) {
|
|
216
|
-
console.log(`would remove Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
219
|
+
console.log(`would remove Pi extension: ${join(piExtensionsDir(), "pi-cpac.ts")}`);
|
|
217
220
|
return;
|
|
218
221
|
}
|
|
219
222
|
await uninstallPiExtension();
|
package/dist/config.js
CHANGED
|
@@ -31,6 +31,9 @@ const STATE_KEYS = new Set([
|
|
|
31
31
|
"proxy_fingerprint",
|
|
32
32
|
"proxy_pid",
|
|
33
33
|
"proxy_port",
|
|
34
|
+
"injected_config_hash",
|
|
35
|
+
"injected_openai_base_url",
|
|
36
|
+
"injected_catalog_path",
|
|
34
37
|
]);
|
|
35
38
|
export const STATE_FILES = [
|
|
36
39
|
"state.json",
|
|
@@ -388,6 +391,21 @@ export function readState(stateDir) {
|
|
|
388
391
|
!/^[a-f0-9]{64}$/.test(value.proxy_fingerprint))) {
|
|
389
392
|
throw new CPACError("invalid loopback proxy fingerprint");
|
|
390
393
|
}
|
|
394
|
+
if (value.injected_config_hash !== undefined &&
|
|
395
|
+
(typeof value.injected_config_hash !== "string" ||
|
|
396
|
+
!/^[a-f0-9]{64}$/.test(value.injected_config_hash))) {
|
|
397
|
+
throw new CPACError("invalid injected_config_hash in state");
|
|
398
|
+
}
|
|
399
|
+
if (value.injected_openai_base_url !== undefined &&
|
|
400
|
+
(typeof value.injected_openai_base_url !== "string" ||
|
|
401
|
+
!value.injected_openai_base_url.trim())) {
|
|
402
|
+
throw new CPACError("invalid injected_openai_base_url in state");
|
|
403
|
+
}
|
|
404
|
+
if (value.injected_catalog_path !== undefined &&
|
|
405
|
+
(typeof value.injected_catalog_path !== "string" ||
|
|
406
|
+
!value.injected_catalog_path.trim())) {
|
|
407
|
+
throw new CPACError("invalid injected_catalog_path in state");
|
|
408
|
+
}
|
|
391
409
|
return value;
|
|
392
410
|
}
|
|
393
411
|
export function stateProxy(state) {
|
|
@@ -395,7 +413,21 @@ export function stateProxy(state) {
|
|
|
395
413
|
? { id: state.proxy_id, pid: state.proxy_pid, port: state.proxy_port }
|
|
396
414
|
: null;
|
|
397
415
|
}
|
|
398
|
-
export function
|
|
416
|
+
export function injectedOwnership(state) {
|
|
417
|
+
if (!state?.injected_config_hash ||
|
|
418
|
+
!state.injected_openai_base_url ||
|
|
419
|
+
!state.injected_catalog_path)
|
|
420
|
+
return undefined;
|
|
421
|
+
return {
|
|
422
|
+
hash: state.injected_config_hash,
|
|
423
|
+
openaiBaseUrl: state.injected_openai_base_url,
|
|
424
|
+
catalogPath: state.injected_catalog_path,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function sha256Hex(bytes) {
|
|
428
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
429
|
+
}
|
|
430
|
+
export function stateBytes(config, existed, mode, proxy, proxyFingerprint, injected) {
|
|
399
431
|
return Buffer.from(`${JSON.stringify({
|
|
400
432
|
config_path: config.codex_config,
|
|
401
433
|
config_existed: existed,
|
|
@@ -404,6 +436,13 @@ export function stateBytes(config, existed, mode, proxy, proxyFingerprint) {
|
|
|
404
436
|
proxy_fingerprint: proxyFingerprint,
|
|
405
437
|
proxy_pid: proxy.pid,
|
|
406
438
|
proxy_port: proxy.port,
|
|
439
|
+
...(injected
|
|
440
|
+
? {
|
|
441
|
+
injected_config_hash: injected.hash,
|
|
442
|
+
injected_openai_base_url: injected.openaiBaseUrl,
|
|
443
|
+
injected_catalog_path: injected.catalogPath,
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
407
446
|
}, null, 2)}\n`);
|
|
408
447
|
}
|
|
409
448
|
export function originalBytes(stateDir, state, expectedPath) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// CPAC
|
|
1
|
+
// Pi CPAC extension: register CPA models as Pi providers
|
|
2
2
|
// Installed by: cpac pi install
|
|
3
3
|
|
|
4
4
|
const CPA = "__CPA_URL__";
|
|
@@ -71,7 +71,7 @@ function intField(value) {
|
|
|
71
71
|
export default async function (pi) {
|
|
72
72
|
const apiKey = (process.env.CPA_API_KEY || "").trim();
|
|
73
73
|
if (!apiKey) {
|
|
74
|
-
console.error("[cpac] CPA_API_KEY not set; skipping CPA providers");
|
|
74
|
+
console.error("[pi-cpac] CPA_API_KEY not set; skipping CPA providers");
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
let payload;
|
|
@@ -81,19 +81,19 @@ export default async function (pi) {
|
|
|
81
81
|
signal: AbortSignal.timeout(10000),
|
|
82
82
|
});
|
|
83
83
|
if (!res.ok) {
|
|
84
|
-
console.error("[cpac] CPA models request failed: HTTP " + res.status);
|
|
84
|
+
console.error("[pi-cpac] CPA models request failed: HTTP " + res.status);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
payload = await res.json();
|
|
88
88
|
} catch (error) {
|
|
89
|
-
console.error("[cpac] CPA models request failed: " + (error && error.message || error));
|
|
89
|
+
console.error("[pi-cpac] CPA models request failed: " + (error && error.message || error));
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
const source = payload && Array.isArray(payload.models)
|
|
93
93
|
? payload.models
|
|
94
94
|
: payload && Array.isArray(payload.data) ? payload.data : null;
|
|
95
95
|
if (!source) {
|
|
96
|
-
console.error("[cpac] CPA models response has no models list");
|
|
96
|
+
console.error("[pi-cpac] CPA models response has no models list");
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
const rows = [];
|
|
@@ -110,7 +110,7 @@ export default async function (pi) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
if (rows.length === 0) {
|
|
113
|
-
console.error("[cpac] CPA models response has no usable models");
|
|
113
|
+
console.error("[pi-cpac] CPA models response has no usable models");
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
const groups = new Map();
|
package/dist/proxy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer, request as httpRequest, } from "node:http";
|
|
|
5
5
|
import { request as httpsRequest } from "node:https";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { apiBase, originalBytes, proxyFingerprint, readState, stateBytes, stateProxy, } from "./config.js";
|
|
8
|
+
import { apiBase, injectedOwnership, originalBytes, proxyFingerprint, readState, stateBytes, stateProxy, } from "./config.js";
|
|
9
9
|
import { CPACError, atomicWrite, objectValue, resolveApiKey } from "./util.js";
|
|
10
10
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
11
11
|
"connection",
|
|
@@ -322,7 +322,7 @@ export async function runProxy(config) {
|
|
|
322
322
|
}
|
|
323
323
|
const proxy = await createLoopbackProxy(config.cpa_url, apiKey, recorded.id, recorded.port);
|
|
324
324
|
try {
|
|
325
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey)));
|
|
325
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey), injectedOwnership(state)));
|
|
326
326
|
}
|
|
327
327
|
catch (error) {
|
|
328
328
|
proxy.server.closeAllConnections?.();
|
package/dist/targets/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, stateBytes, stateProxy, } from "../config.js";
|
|
3
|
+
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, injectedOwnership, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, sha256Hex, stateBytes, stateProxy, } from "../config.js";
|
|
4
4
|
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "../proxy.js";
|
|
5
5
|
import { CPACError, atomicWrite, dominantEol, tomlString } from "../util.js";
|
|
6
6
|
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
@@ -8,6 +8,37 @@ const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model
|
|
|
8
8
|
const OPENAI_BASE_URL_KEY = /^\s*(?:openai_base_url|"openai_base_url"|'openai_base_url')\s*=/;
|
|
9
9
|
const ROOT_KEY = new RegExp(`(?:${MODEL_PROVIDER_KEY.source}|${MODEL_CATALOG_KEY.source}|${OPENAI_BASE_URL_KEY.source})`);
|
|
10
10
|
export const MANAGED_MARKER = "# CPAC managed; run CPAC 'restore' to restore the original file.";
|
|
11
|
+
function tomlQuotedValue(line) {
|
|
12
|
+
const eq = line.indexOf("=");
|
|
13
|
+
if (eq === -1)
|
|
14
|
+
return undefined;
|
|
15
|
+
const rest = line.slice(eq + 1).trimStart();
|
|
16
|
+
if (rest.startsWith('"')) {
|
|
17
|
+
const match = /^(?:"(?:\\.|[^"\\])*")/.exec(rest);
|
|
18
|
+
if (!match)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(match[0]);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (rest.startsWith("'")) {
|
|
28
|
+
const end = rest.indexOf("'", 1);
|
|
29
|
+
return end === -1 ? undefined : rest.slice(1, end);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function ownedRootLine(line, owned) {
|
|
34
|
+
if (!owned)
|
|
35
|
+
return false;
|
|
36
|
+
const value = tomlQuotedValue(line);
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return false;
|
|
39
|
+
return ((MODEL_CATALOG_KEY.test(line) && value === owned.catalogPath) ||
|
|
40
|
+
(OPENAI_BASE_URL_KEY.test(line) && value === owned.openaiBaseUrl));
|
|
41
|
+
}
|
|
11
42
|
function providerPatterns(provider = PROVIDER) {
|
|
12
43
|
const escaped = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
44
|
const modelProviders = `(?:model_providers|"model_providers"|'model_providers')`;
|
|
@@ -41,7 +72,7 @@ export function hasProviderTable(content, provider = PROVIDER) {
|
|
|
41
72
|
}
|
|
42
73
|
return false;
|
|
43
74
|
}
|
|
44
|
-
function stripManagedConfig(content) {
|
|
75
|
+
function stripManagedConfig(content, owned) {
|
|
45
76
|
let text;
|
|
46
77
|
try {
|
|
47
78
|
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
@@ -75,6 +106,8 @@ function stripManagedConfig(content) {
|
|
|
75
106
|
managedRootKeys -= 1;
|
|
76
107
|
continue;
|
|
77
108
|
}
|
|
109
|
+
if (inRoot && ownedRootLine(line, owned))
|
|
110
|
+
continue;
|
|
78
111
|
kept.push(line);
|
|
79
112
|
}
|
|
80
113
|
const output = kept.join("\n");
|
|
@@ -225,7 +258,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
225
258
|
const backup = originalBytes(config.state_dir, state, config.codex_config);
|
|
226
259
|
try {
|
|
227
260
|
original = existsSync(config.codex_config)
|
|
228
|
-
? stripManagedConfig(readFileSync(config.codex_config))
|
|
261
|
+
? stripManagedConfig(readFileSync(config.codex_config), injectedOwnership(state))
|
|
229
262
|
: backup;
|
|
230
263
|
}
|
|
231
264
|
catch (error) {
|
|
@@ -293,6 +326,11 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
293
326
|
await stopProxyProcess(proxy);
|
|
294
327
|
throw error;
|
|
295
328
|
}
|
|
329
|
+
const ownership = {
|
|
330
|
+
hash: state?.injected_config_hash ?? sha256Hex(injected),
|
|
331
|
+
openaiBaseUrl: `http://127.0.0.1:${proxy.port}/v1`,
|
|
332
|
+
catalogPath,
|
|
333
|
+
};
|
|
296
334
|
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
297
335
|
if (!stateDirExisted)
|
|
298
336
|
chmodSync(config.state_dir, 0o700);
|
|
@@ -300,7 +338,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
300
338
|
try {
|
|
301
339
|
atomicWrite(catalogPath, catalog.bytes);
|
|
302
340
|
atomicWrite(config.codex_config, injected, originalMode);
|
|
303
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
341
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint, ownership));
|
|
304
342
|
}
|
|
305
343
|
catch (error) {
|
|
306
344
|
if (startedProxy)
|
|
@@ -316,7 +354,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
316
354
|
atomicWrite(catalogPath, catalog.bytes);
|
|
317
355
|
atomicWrite(config.codex_config, injected, originalMode);
|
|
318
356
|
configWritten = true;
|
|
319
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
357
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint, ownership));
|
|
320
358
|
}
|
|
321
359
|
catch (error) {
|
|
322
360
|
if (configWritten) {
|
|
@@ -361,10 +399,25 @@ export async function restore(config) {
|
|
|
361
399
|
if (!state)
|
|
362
400
|
throw new CPACError("no active CPAC injection");
|
|
363
401
|
const original = originalBytes(config.state_dir, state, config.codex_config);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
402
|
+
const current = existsSync(config.codex_config)
|
|
403
|
+
? readFileSync(config.codex_config)
|
|
404
|
+
: null;
|
|
405
|
+
const unchanged = !state.injected_config_hash ||
|
|
406
|
+
(current !== null && sha256Hex(current) === state.injected_config_hash);
|
|
407
|
+
if (!current || unchanged) {
|
|
408
|
+
if (state.config_existed)
|
|
409
|
+
atomicWrite(config.codex_config, original, state.config_mode);
|
|
410
|
+
else
|
|
411
|
+
rmSync(config.codex_config, { force: true });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const stripped = stripManagedConfig(current, injectedOwnership(state));
|
|
415
|
+
const empty = new TextDecoder("utf-8").decode(stripped).trim() === "";
|
|
416
|
+
if (!state.config_existed && empty)
|
|
417
|
+
rmSync(config.codex_config, { force: true });
|
|
418
|
+
else
|
|
419
|
+
atomicWrite(config.codex_config, stripped, state.config_mode);
|
|
420
|
+
}
|
|
368
421
|
const proxy = stateProxy(state);
|
|
369
422
|
if (proxy)
|
|
370
423
|
await stopProxyProcess(proxy);
|
package/dist/targets/pi.js
CHANGED
|
@@ -7,7 +7,8 @@ export function piExtensionsDir() {
|
|
|
7
7
|
return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
|
|
8
8
|
}
|
|
9
9
|
export function isPiExtensionInstalled() {
|
|
10
|
-
|
|
10
|
+
const dir = piExtensionsDir();
|
|
11
|
+
return existsSync(join(dir, "pi-cpac.ts")) || existsSync(join(dir, "cpac.ts"));
|
|
11
12
|
}
|
|
12
13
|
function piTemplatePath() {
|
|
13
14
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "pi-extension.template");
|
|
@@ -18,15 +19,32 @@ function piExtensionContent(cpaUrl) {
|
|
|
18
19
|
export async function installPiExtension(config) {
|
|
19
20
|
const dir = piExtensionsDir();
|
|
20
21
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
21
|
-
const target = join(dir, "cpac.ts");
|
|
22
|
+
const target = join(dir, "pi-cpac.ts");
|
|
22
23
|
ensureCpacBackup(target);
|
|
23
24
|
atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
|
|
25
|
+
for (const legacy of ["cpac.ts", "pi-cpa.ts"]) {
|
|
26
|
+
const legacyPath = join(dir, legacy);
|
|
27
|
+
if (existsSync(legacyPath) && legacyPath !== target) {
|
|
28
|
+
try {
|
|
29
|
+
unlinkSync(legacyPath);
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
24
34
|
console.log(`Installed Pi extension: ${target}`);
|
|
25
35
|
}
|
|
26
36
|
export async function uninstallPiExtension() {
|
|
27
|
-
const
|
|
28
|
-
|
|
37
|
+
const dir = piExtensionsDir();
|
|
38
|
+
const target = join(dir, "pi-cpac.ts");
|
|
39
|
+
const legacyTargets = [join(dir, "cpac.ts"), join(dir, "pi-cpa.ts")];
|
|
40
|
+
let removed = false;
|
|
41
|
+
for (const file of [target, ...legacyTargets]) {
|
|
42
|
+
if (existsSync(file)) {
|
|
43
|
+
unlinkSync(file);
|
|
44
|
+
console.log(`Removed Pi extension: ${file}`);
|
|
45
|
+
removed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!removed)
|
|
29
49
|
throw new CPACError("Pi extension is not installed");
|
|
30
|
-
unlinkSync(target);
|
|
31
|
-
console.log(`Removed Pi extension: ${target}`);
|
|
32
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"clean": "node -e \"for (const d of ['dist','dist-test']) require('fs').rmSync(d,{recursive:true,force:true})\"",
|
|
31
31
|
"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
32
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
33
|
-
"check:pi": "tsc -p tsconfig.pi.json --noEmit",
|
|
34
33
|
"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
34
|
"pack:check": "npm pack --dry-run"
|
|
36
35
|
},
|