@yhong91/cpac 0.1.1 → 0.1.2
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 +37 -4
- package/cpac.example.json +2 -1
- package/dist/cpac.js +473 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CPAC 将 Codex 和 Claude Code 接入远端 CLIProxyAPI(CPA):
|
|
4
4
|
|
|
5
|
-
- **Codex**:注入模型目录和 provider,并可逐字节恢复原配置。
|
|
5
|
+
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider,并可逐字节恢复原配置。
|
|
6
6
|
- **Claude Code**:通过临时环境变量启动,不修改 Claude Code 配置。
|
|
7
7
|
|
|
8
8
|
实现基于 TypeScript + Node.js,运行时零依赖。
|
|
@@ -110,7 +110,7 @@ ANTHROPIC_AUTH_TOKEN=<CPA_API_KEY>
|
|
|
110
110
|
|
|
111
111
|
同时清除可能覆盖网关选择的 `ANTHROPIC_API_KEY` 和 Claude Code 云 provider 环境变量。它不会修改 `~/.claude/`,并原样返回 Claude Code 的退出码。
|
|
112
112
|
|
|
113
|
-
> CPA 服务端必须支持 Claude Code 使用的 Anthropic Messages API(`/v1/messages`)。CPAC
|
|
113
|
+
> CPA 服务端必须支持 Claude Code 使用的 Anthropic Messages API(`/v1/messages`)。Claude 路径直接连接远端 CPA;CPAC 不负责协议转换。
|
|
114
114
|
|
|
115
115
|
### Codex
|
|
116
116
|
|
|
@@ -126,15 +126,45 @@ cpac inject
|
|
|
126
126
|
cpac status
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
`status` 列出所有支持的 agent 及当前状态:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
codex: injected proxy=http://127.0.0.1:10101/v1 sha256=...
|
|
133
|
+
claude: ready
|
|
134
|
+
pi: ready
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- **codex**:`native`(未注入)、`injected`(已注入,附代理地址和配置摘要)、`injected (loopback proxy stopped)` 或 `injected (config missing)`
|
|
138
|
+
- **claude**:`ready`(密钥已配置)或 `not configured`
|
|
139
|
+
- **pi**:`ready`(密钥已配置)或 `not configured`
|
|
140
|
+
|
|
129
141
|
恢复注入前的原配置:
|
|
130
142
|
|
|
131
143
|
```bash
|
|
132
144
|
cpac restore
|
|
133
145
|
```
|
|
134
146
|
|
|
135
|
-
`inject`
|
|
147
|
+
`inject` 从远端 `/v1/models?client_version=1` 获取 Codex rich catalog,并启动只监听 `127.0.0.1` 的轻量转发代理。Codex 配置保留内置 `openai` provider,只写入根级 `model_catalog_json` 和:
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
openai_base_url = "http://127.0.0.1:10101/v1"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Codex App/CLI 发给本地代理的 ChatGPT bearer 不会转发到 CPA;代理会改用 `CPA_API_KEY`,并将请求和流式响应原样转发到远端 CPA。这保留了 Codex App 的原生 `openai` provider 身份、历史和账号相关界面。已有用户自定义根级 `openai_base_url` 或非 `openai` 的活动 `model_provider` 时,CPAC 会拒绝覆盖。
|
|
154
|
+
|
|
155
|
+
Codex App 的长驻 `app-server` 可能缓存旧目录。`inject` 会删除 `models_cache.json`;如果选择器仍未更新,请重启 Codex App。
|
|
156
|
+
|
|
157
|
+
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore` 按原 mode 逐字节恢复首次备份、关闭 CPAC loopback 代理;若原文件不存在,则删除注入创建的 `config.toml`。
|
|
158
|
+
|
|
159
|
+
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac inject` 即可恢复。
|
|
160
|
+
|
|
161
|
+
需要由现有进程管理器监督时,可使用前台入口:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
cpac proxy
|
|
165
|
+
```
|
|
136
166
|
|
|
137
|
-
|
|
167
|
+
它复用当前注入记录的端口和实例身份;没有活动注入时会拒绝启动。
|
|
138
168
|
|
|
139
169
|
默认路径:
|
|
140
170
|
|
|
@@ -171,6 +201,7 @@ CPA_BASE_URL='https://cpa.example.com' cpac claude
|
|
|
171
201
|
"cpa_url": "https://cpa.example.com",
|
|
172
202
|
"api_key_env": "CPA_API_KEY",
|
|
173
203
|
"codex_config": "~/.codex/config.toml",
|
|
204
|
+
"codex_proxy_port": 10101,
|
|
174
205
|
"state_dir": "~/.cpac"
|
|
175
206
|
}
|
|
176
207
|
```
|
|
@@ -180,6 +211,7 @@ CPA_BASE_URL='https://cpa.example.com' cpac claude
|
|
|
180
211
|
- `cpa_url`:可选,绝对 `http(s)` URL,可带或不带 `/v1`
|
|
181
212
|
- `api_key_env`:可选,密钥环境变量名,默认 `CPA_API_KEY`
|
|
182
213
|
- `codex_config`:可选,Codex 配置路径
|
|
214
|
+
- `codex_proxy_port`:可选,本地 Codex 转发端口,默认 `10101`;设为 `0` 时自动选择空闲端口
|
|
183
215
|
- `state_dir`:可选,CPAC state 路径
|
|
184
216
|
|
|
185
217
|
使用自定义配置:
|
|
@@ -198,6 +230,7 @@ export CPAC_CONFIG=/path/to/cpac.json
|
|
|
198
230
|
## 安全行为
|
|
199
231
|
|
|
200
232
|
- API key 不写入 CPAC JSON、Codex 配置、备份、state、catalog 或日志。
|
|
233
|
+
- Codex loopback 代理仅监听 `127.0.0.1`,拒绝非本地浏览器 Origin,并在内存中用 CPA key 替换入站 Authorization。
|
|
201
234
|
- 引导输入不回显,写入 shell 时会正确引用特殊字符。
|
|
202
235
|
- shell 启动文件和 Codex 配置均通过同目录临时文件原子替换。
|
|
203
236
|
- CPA 请求失败、catalog 无效或 key 缺失时,首次注入不会修改 Codex 配置。
|
package/cpac.example.json
CHANGED
package/dist/cpac.js
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
4
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
import { createServer, request as httpRequest, } from "node:http";
|
|
6
|
+
import { request as httpsRequest } from "node:https";
|
|
5
7
|
import { homedir, tmpdir } from "node:os";
|
|
6
8
|
import { dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
10
|
import { createInterface } from "node:readline/promises";
|
|
9
11
|
import { Writable } from "node:stream";
|
|
10
12
|
export const PROVIDER = "cpac_cpa";
|
|
11
13
|
const DEFAULT_CPA_URL = "https://cpa.vibetime.cc";
|
|
14
|
+
const DEFAULT_CODEX_PROXY_PORT = 10101;
|
|
12
15
|
const MAX_CATALOG_BYTES = 16 * 1024 * 1024;
|
|
13
16
|
const CONFIG_KEYS = new Set([
|
|
14
17
|
"cpa_url",
|
|
15
18
|
"api_key_env",
|
|
16
19
|
"codex_config",
|
|
20
|
+
"codex_proxy_port",
|
|
17
21
|
"state_dir",
|
|
18
22
|
]);
|
|
19
|
-
const
|
|
23
|
+
const REQUIRED_STATE_KEYS = new Set([
|
|
24
|
+
"config_path",
|
|
25
|
+
"config_existed",
|
|
26
|
+
"config_mode",
|
|
27
|
+
]);
|
|
28
|
+
const STATE_KEYS = new Set([
|
|
29
|
+
...REQUIRED_STATE_KEYS,
|
|
30
|
+
"proxy_id",
|
|
31
|
+
"proxy_fingerprint",
|
|
32
|
+
"proxy_pid",
|
|
33
|
+
"proxy_port",
|
|
34
|
+
]);
|
|
20
35
|
const STATE_FILES = ["state.json", "config.toml.backup", "codex-models.json"];
|
|
21
|
-
const
|
|
36
|
+
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
37
|
+
const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model_catalog_json')\s*=/;
|
|
38
|
+
const OPENAI_BASE_URL_KEY = /^\s*(?:openai_base_url|"openai_base_url"|'openai_base_url')\s*=/;
|
|
39
|
+
const ROOT_KEY = new RegExp(`(?:${MODEL_PROVIDER_KEY.source}|${MODEL_CATALOG_KEY.source}|${OPENAI_BASE_URL_KEY.source})`);
|
|
40
|
+
const MANAGED_MARKER = "# CPAC managed; run CPAC 'restore' to restore the original file.";
|
|
22
41
|
let atomicSequence = 0;
|
|
23
42
|
export class CPACError extends Error {
|
|
24
43
|
}
|
|
@@ -94,6 +113,12 @@ export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
|
94
113
|
(typeof value.state_dir !== "string" || !value.state_dir.trim())) {
|
|
95
114
|
throw new CPACError("state_dir must be a non-empty string");
|
|
96
115
|
}
|
|
116
|
+
const codexProxyPort = value.codex_proxy_port ?? DEFAULT_CODEX_PROXY_PORT;
|
|
117
|
+
if (!Number.isInteger(codexProxyPort) ||
|
|
118
|
+
codexProxyPort < 0 ||
|
|
119
|
+
codexProxyPort > 65535) {
|
|
120
|
+
throw new CPACError("codex_proxy_port must be an integer from 0 to 65535");
|
|
121
|
+
}
|
|
97
122
|
const codexHome = expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
98
123
|
const codexConfig = resolve(expandUserPath(value.codex_config ||
|
|
99
124
|
join(codexHome, "config.toml")));
|
|
@@ -108,6 +133,7 @@ export function loadConfig(path, useDefaultsIfMissing = false) {
|
|
|
108
133
|
cpa_url: cpaUrl,
|
|
109
134
|
api_key_env: apiKeyEnv,
|
|
110
135
|
codex_config: codexConfig,
|
|
136
|
+
codex_proxy_port: codexProxyPort,
|
|
111
137
|
state_dir: stateDir,
|
|
112
138
|
};
|
|
113
139
|
}
|
|
@@ -245,6 +271,7 @@ function stripManagedConfig(content) {
|
|
|
245
271
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
246
272
|
let inRoot = true;
|
|
247
273
|
let inProvider = false;
|
|
274
|
+
let managedRootKeys = 0;
|
|
248
275
|
const kept = [];
|
|
249
276
|
for (const line of lines) {
|
|
250
277
|
if (inProvider && /^\s*\[/.test(line))
|
|
@@ -257,14 +284,20 @@ function stripManagedConfig(content) {
|
|
|
257
284
|
continue;
|
|
258
285
|
if (/^\s*\[/.test(line))
|
|
259
286
|
inRoot = false;
|
|
260
|
-
if (inRoot &&
|
|
287
|
+
if (inRoot && line === MANAGED_MARKER) {
|
|
288
|
+
managedRootKeys = 2;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (inRoot && managedRootKeys > 0 && ROOT_KEY.test(line)) {
|
|
292
|
+
managedRootKeys -= 1;
|
|
261
293
|
continue;
|
|
294
|
+
}
|
|
262
295
|
kept.push(line);
|
|
263
296
|
}
|
|
264
297
|
const output = kept.join("\n");
|
|
265
298
|
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
266
299
|
}
|
|
267
|
-
export function buildCodexConfig(original,
|
|
300
|
+
export function buildCodexConfig(original, proxyPort, catalogPath) {
|
|
268
301
|
let text;
|
|
269
302
|
try {
|
|
270
303
|
text = new TextDecoder("utf-8", { fatal: true }).decode(original);
|
|
@@ -275,33 +308,37 @@ export function buildCodexConfig(original, cpaUrl, apiKeyEnv, catalogPath) {
|
|
|
275
308
|
if (hasProviderTable(text)) {
|
|
276
309
|
throw new CPACError(`Codex config already contains [model_providers.${PROVIDER}]`);
|
|
277
310
|
}
|
|
311
|
+
const normalizedLines = text.replace(/\r\n/g, "\n").split("\n");
|
|
312
|
+
const firstTable = normalizedLines.findIndex((line) => /^\s*\[/.test(line));
|
|
313
|
+
const rootLines = normalizedLines.slice(0, firstTable === -1 ? undefined : firstTable);
|
|
314
|
+
const providerLine = rootLines.find((line) => MODEL_PROVIDER_KEY.test(line));
|
|
315
|
+
if (providerLine) {
|
|
316
|
+
const match = /=\s*["']([^"']+)["']/.exec(providerLine);
|
|
317
|
+
if (!match || match[1] !== "openai") {
|
|
318
|
+
throw new CPACError("Codex config selects an external model_provider; restore it to openai before injecting CPAC");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (rootLines.some((line) => OPENAI_BASE_URL_KEY.test(line))) {
|
|
322
|
+
throw new CPACError("Codex config already contains openai_base_url; remove that user-owned override before injecting CPAC");
|
|
323
|
+
}
|
|
278
324
|
const eol = dominantEol(text);
|
|
279
325
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
280
326
|
let inRoot = true;
|
|
281
327
|
const kept = lines.filter((line) => {
|
|
282
328
|
if (/^\s*\[/.test(line))
|
|
283
329
|
inRoot = false;
|
|
284
|
-
return !(inRoot &&
|
|
330
|
+
return !(inRoot && MODEL_CATALOG_KEY.test(line));
|
|
285
331
|
});
|
|
286
332
|
let body = kept.join("\n");
|
|
287
333
|
if (body && !body.endsWith("\n"))
|
|
288
334
|
body += "\n";
|
|
289
335
|
const root = [
|
|
290
|
-
|
|
291
|
-
`model_provider = ${tomlString(PROVIDER)}`,
|
|
336
|
+
MANAGED_MARKER,
|
|
292
337
|
`model_catalog_json = ${tomlString(catalogPath)}`,
|
|
338
|
+
`openai_base_url = ${tomlString(`http://127.0.0.1:${proxyPort}/v1`)}`,
|
|
293
339
|
"",
|
|
294
340
|
].join("\n");
|
|
295
|
-
const
|
|
296
|
-
"",
|
|
297
|
-
`[model_providers.${PROVIDER}]`,
|
|
298
|
-
'name = "CPA"',
|
|
299
|
-
`base_url = ${tomlString(apiBase(cpaUrl))}`,
|
|
300
|
-
`env_key = ${tomlString(apiKeyEnv)}`,
|
|
301
|
-
'wire_api = "responses"',
|
|
302
|
-
"",
|
|
303
|
-
].join("\n");
|
|
304
|
-
const output = root + body + provider;
|
|
341
|
+
const output = root + body;
|
|
305
342
|
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
306
343
|
}
|
|
307
344
|
export function atomicWrite(path, data, mode = 0o600) {
|
|
@@ -335,6 +372,243 @@ export function atomicWrite(path, data, mode = 0o600) {
|
|
|
335
372
|
throw error;
|
|
336
373
|
}
|
|
337
374
|
}
|
|
375
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
376
|
+
"connection",
|
|
377
|
+
"keep-alive",
|
|
378
|
+
"proxy-authenticate",
|
|
379
|
+
"proxy-authorization",
|
|
380
|
+
"proxy-connection",
|
|
381
|
+
"te",
|
|
382
|
+
"trailer",
|
|
383
|
+
"transfer-encoding",
|
|
384
|
+
"upgrade",
|
|
385
|
+
]);
|
|
386
|
+
const CLIENT_CREDENTIAL_HEADERS = new Set([
|
|
387
|
+
"authorization",
|
|
388
|
+
"chatgpt-account-id",
|
|
389
|
+
"cookie",
|
|
390
|
+
"openai-organization",
|
|
391
|
+
"openai-project",
|
|
392
|
+
"x-api-key",
|
|
393
|
+
"x-opencodex-api-key",
|
|
394
|
+
]);
|
|
395
|
+
function localBrowserOrigin(value) {
|
|
396
|
+
if (!value)
|
|
397
|
+
return true;
|
|
398
|
+
try {
|
|
399
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
400
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function proxyHeaders(headers, apiKey) {
|
|
407
|
+
const forwarded = {};
|
|
408
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
409
|
+
if (value === undefined ||
|
|
410
|
+
name === "host" ||
|
|
411
|
+
CLIENT_CREDENTIAL_HEADERS.has(name) ||
|
|
412
|
+
HOP_BY_HOP_HEADERS.has(name)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
forwarded[name] = value;
|
|
416
|
+
}
|
|
417
|
+
forwarded.authorization = `Bearer ${apiKey}`;
|
|
418
|
+
return forwarded;
|
|
419
|
+
}
|
|
420
|
+
function responseHeaders(headers) {
|
|
421
|
+
const forwarded = {};
|
|
422
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
423
|
+
if (value === undefined || HOP_BY_HOP_HEADERS.has(name))
|
|
424
|
+
continue;
|
|
425
|
+
forwarded[name] = value;
|
|
426
|
+
}
|
|
427
|
+
return forwarded;
|
|
428
|
+
}
|
|
429
|
+
function upstreamUrl(cpaUrl, requestUrl) {
|
|
430
|
+
const incoming = new URL(requestUrl, "http://127.0.0.1");
|
|
431
|
+
const suffix = incoming.pathname === "/v1"
|
|
432
|
+
? ""
|
|
433
|
+
: incoming.pathname.startsWith("/v1/")
|
|
434
|
+
? incoming.pathname.slice(3)
|
|
435
|
+
: incoming.pathname;
|
|
436
|
+
return new URL(`${apiBase(cpaUrl)}${suffix}${incoming.search}`);
|
|
437
|
+
}
|
|
438
|
+
export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
439
|
+
const server = createServer((request, response) => {
|
|
440
|
+
const requestUrl = request.url ?? "/";
|
|
441
|
+
if (requestUrl === "/_cpac/health") {
|
|
442
|
+
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
443
|
+
response.writeHead(404).end();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
response.writeHead(204).end();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (requestUrl === "/_cpac/shutdown" && request.method === "POST") {
|
|
450
|
+
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
451
|
+
response.writeHead(404).end();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
response.writeHead(204).end();
|
|
455
|
+
setImmediate(() => {
|
|
456
|
+
server.closeAllConnections?.();
|
|
457
|
+
server.close();
|
|
458
|
+
});
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!localBrowserOrigin(request.headers.origin)) {
|
|
462
|
+
response.writeHead(403, { "content-type": "application/json" });
|
|
463
|
+
response.end(JSON.stringify({ error: "non-local origin rejected" }));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let target;
|
|
467
|
+
try {
|
|
468
|
+
target = upstreamUrl(cpaUrl, requestUrl);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
response.writeHead(400).end("invalid request URL");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
475
|
+
const upstream = send(target, {
|
|
476
|
+
method: request.method,
|
|
477
|
+
headers: proxyHeaders(request.headers, apiKey),
|
|
478
|
+
}, (upstreamResponse) => {
|
|
479
|
+
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
480
|
+
upstreamResponse.pipe(response);
|
|
481
|
+
});
|
|
482
|
+
upstream.once("error", () => {
|
|
483
|
+
if (!response.headersSent) {
|
|
484
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
485
|
+
}
|
|
486
|
+
if (!response.writableEnded)
|
|
487
|
+
response.end(JSON.stringify({ error: "CPA upstream request failed" }));
|
|
488
|
+
});
|
|
489
|
+
request.once("aborted", () => upstream.destroy());
|
|
490
|
+
request.pipe(upstream);
|
|
491
|
+
});
|
|
492
|
+
server.on("upgrade", (_request, socket) => {
|
|
493
|
+
socket.end("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
494
|
+
});
|
|
495
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
496
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
497
|
+
server.once("error", rejectListen);
|
|
498
|
+
server.listen(port, "127.0.0.1", () => {
|
|
499
|
+
server.off("error", rejectListen);
|
|
500
|
+
resolveListen();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
const address = server.address();
|
|
504
|
+
if (!address || typeof address === "string") {
|
|
505
|
+
server.close();
|
|
506
|
+
throw new CPACError("cannot determine loopback proxy port");
|
|
507
|
+
}
|
|
508
|
+
return { server, port: address.port };
|
|
509
|
+
}
|
|
510
|
+
async function runProxyChild(port) {
|
|
511
|
+
const cpaUrl = process.env.CPAC_PROXY_UPSTREAM;
|
|
512
|
+
const apiKey = process.env.CPAC_PROXY_API_KEY;
|
|
513
|
+
const proxyId = process.env.CPAC_PROXY_ID;
|
|
514
|
+
if (!cpaUrl || !apiKey || !proxyId)
|
|
515
|
+
return 1;
|
|
516
|
+
try {
|
|
517
|
+
const proxy = await createLoopbackProxy(cpaUrl, apiKey, proxyId, port);
|
|
518
|
+
process.send?.({ ready: true, port: proxy.port, pid: process.pid });
|
|
519
|
+
const close = () => {
|
|
520
|
+
proxy.server.closeAllConnections?.();
|
|
521
|
+
proxy.server.close();
|
|
522
|
+
};
|
|
523
|
+
process.once("SIGINT", close);
|
|
524
|
+
process.once("SIGTERM", close);
|
|
525
|
+
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
process.send?.({
|
|
530
|
+
ready: false,
|
|
531
|
+
error: error instanceof Error ? error.message : String(error),
|
|
532
|
+
});
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function proxyIsHealthy(proxy) {
|
|
537
|
+
try {
|
|
538
|
+
const response = await fetch(`http://127.0.0.1:${proxy.port}/_cpac/health`, {
|
|
539
|
+
headers: { "x-cpac-proxy-id": proxy.id },
|
|
540
|
+
signal: AbortSignal.timeout(1_000),
|
|
541
|
+
});
|
|
542
|
+
return response.status === 204;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function startProxyProcess(config, apiKey) {
|
|
549
|
+
const id = randomBytes(24).toString("hex");
|
|
550
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "_proxy", String(config.codex_proxy_port)], {
|
|
551
|
+
detached: true,
|
|
552
|
+
env: {
|
|
553
|
+
...process.env,
|
|
554
|
+
CPAC_PROXY_API_KEY: apiKey,
|
|
555
|
+
CPAC_PROXY_ID: id,
|
|
556
|
+
CPAC_PROXY_UPSTREAM: config.cpa_url,
|
|
557
|
+
},
|
|
558
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
559
|
+
});
|
|
560
|
+
return await new Promise((resolveStart, rejectStart) => {
|
|
561
|
+
let settled = false;
|
|
562
|
+
let timeout;
|
|
563
|
+
const finish = (error, value) => {
|
|
564
|
+
if (settled)
|
|
565
|
+
return;
|
|
566
|
+
settled = true;
|
|
567
|
+
clearTimeout(timeout);
|
|
568
|
+
child.removeAllListeners();
|
|
569
|
+
if (child.connected)
|
|
570
|
+
child.disconnect();
|
|
571
|
+
child.unref();
|
|
572
|
+
if (error)
|
|
573
|
+
rejectStart(error);
|
|
574
|
+
else
|
|
575
|
+
resolveStart(value);
|
|
576
|
+
};
|
|
577
|
+
timeout = setTimeout(() => {
|
|
578
|
+
child.kill();
|
|
579
|
+
finish(new CPACError("loopback proxy did not become ready"));
|
|
580
|
+
}, 5_000);
|
|
581
|
+
child.once("error", (error) => finish(new CPACError(`cannot start loopback proxy: ${error.message}`)));
|
|
582
|
+
child.once("exit", (code) => finish(new CPACError(`loopback proxy exited before ready (${code ?? 1})`)));
|
|
583
|
+
child.once("message", (message) => {
|
|
584
|
+
if (objectValue(message) &&
|
|
585
|
+
message.ready === true &&
|
|
586
|
+
typeof message.pid === "number" &&
|
|
587
|
+
typeof message.port === "number") {
|
|
588
|
+
finish(undefined, { id, pid: message.pid, port: message.port });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const detail = objectValue(message) && typeof message.error === "string"
|
|
592
|
+
? `: ${message.error}`
|
|
593
|
+
: "";
|
|
594
|
+
finish(new CPACError(`cannot start loopback proxy${detail}`));
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
async function stopProxyProcess(proxy) {
|
|
599
|
+
if (!(await proxyIsHealthy(proxy)))
|
|
600
|
+
return;
|
|
601
|
+
try {
|
|
602
|
+
await fetch(`http://127.0.0.1:${proxy.port}/_cpac/shutdown`, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "x-cpac-proxy-id": proxy.id },
|
|
605
|
+
signal: AbortSignal.timeout(1_000),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
// The server may close the connection while shutting down.
|
|
610
|
+
}
|
|
611
|
+
}
|
|
338
612
|
function readState(stateDir) {
|
|
339
613
|
const path = join(stateDir, "state.json");
|
|
340
614
|
if (!existsSync(path))
|
|
@@ -348,7 +622,7 @@ function readState(stateDir) {
|
|
|
348
622
|
}
|
|
349
623
|
if (!objectValue(value) ||
|
|
350
624
|
Object.keys(value).some((key) => !STATE_KEYS.has(key)) ||
|
|
351
|
-
|
|
625
|
+
[...REQUIRED_STATE_KEYS].some((key) => !(key in value))) {
|
|
352
626
|
throw new CPACError("invalid state file");
|
|
353
627
|
}
|
|
354
628
|
if (typeof value.config_path !== "string" || !isAbsolute(value.config_path)) {
|
|
@@ -361,8 +635,42 @@ function readState(stateDir) {
|
|
|
361
635
|
value.config_mode > 0o7777) {
|
|
362
636
|
throw new CPACError("invalid config_mode value in state");
|
|
363
637
|
}
|
|
638
|
+
const proxyKeys = ["proxy_id", "proxy_pid", "proxy_port"];
|
|
639
|
+
const proxyKeyCount = proxyKeys.filter((key) => key in value).length;
|
|
640
|
+
if (proxyKeyCount !== 0 &&
|
|
641
|
+
(proxyKeyCount !== proxyKeys.length ||
|
|
642
|
+
typeof value.proxy_id !== "string" ||
|
|
643
|
+
!/^[a-f0-9]{48}$/.test(value.proxy_id) ||
|
|
644
|
+
!Number.isInteger(value.proxy_pid) ||
|
|
645
|
+
value.proxy_pid <= 0 ||
|
|
646
|
+
!Number.isInteger(value.proxy_port) ||
|
|
647
|
+
value.proxy_port <= 0 ||
|
|
648
|
+
value.proxy_port > 65535)) {
|
|
649
|
+
throw new CPACError("invalid loopback proxy state");
|
|
650
|
+
}
|
|
651
|
+
if (value.proxy_fingerprint !== undefined &&
|
|
652
|
+
(typeof value.proxy_fingerprint !== "string" ||
|
|
653
|
+
!/^[a-f0-9]{64}$/.test(value.proxy_fingerprint))) {
|
|
654
|
+
throw new CPACError("invalid loopback proxy fingerprint");
|
|
655
|
+
}
|
|
364
656
|
return value;
|
|
365
657
|
}
|
|
658
|
+
function stateProxy(state) {
|
|
659
|
+
return state.proxy_id && state.proxy_pid && state.proxy_port
|
|
660
|
+
? { id: state.proxy_id, pid: state.proxy_pid, port: state.proxy_port }
|
|
661
|
+
: null;
|
|
662
|
+
}
|
|
663
|
+
function stateBytes(config, existed, mode, proxy, proxyFingerprint) {
|
|
664
|
+
return Buffer.from(`${JSON.stringify({
|
|
665
|
+
config_path: config.codex_config,
|
|
666
|
+
config_existed: existed,
|
|
667
|
+
config_mode: mode,
|
|
668
|
+
proxy_id: proxy.id,
|
|
669
|
+
proxy_fingerprint: proxyFingerprint,
|
|
670
|
+
proxy_pid: proxy.pid,
|
|
671
|
+
proxy_port: proxy.port,
|
|
672
|
+
}, null, 2)}\n`);
|
|
673
|
+
}
|
|
366
674
|
function originalBytes(stateDir, state, expectedPath) {
|
|
367
675
|
if (state.config_path !== expectedPath) {
|
|
368
676
|
throw new CPACError(`active injection belongs to ${state.config_path}; restore it first`);
|
|
@@ -388,6 +696,13 @@ function cleanupStateFiles(stateDir) {
|
|
|
388
696
|
throw error;
|
|
389
697
|
}
|
|
390
698
|
}
|
|
699
|
+
function proxyFingerprint(config, apiKey) {
|
|
700
|
+
return createHash("sha256")
|
|
701
|
+
.update(config.cpa_url)
|
|
702
|
+
.update("\0")
|
|
703
|
+
.update(apiKey)
|
|
704
|
+
.digest("hex");
|
|
705
|
+
}
|
|
391
706
|
export async function inject(config) {
|
|
392
707
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
393
708
|
if (!apiKey)
|
|
@@ -423,20 +738,58 @@ export async function inject(config) {
|
|
|
423
738
|
throw new CPACError(`cannot read Codex config: ${error instanceof Error ? error.message : String(error)}`);
|
|
424
739
|
}
|
|
425
740
|
}
|
|
426
|
-
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
427
|
-
const injected = buildCodexConfig(original, config.cpa_url, config.api_key_env, catalogPath);
|
|
428
741
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
429
742
|
const stateDirExisted = existsSync(config.state_dir);
|
|
430
743
|
if (!state &&
|
|
431
744
|
STATE_FILES.some((name) => existsSync(join(config.state_dir, name)))) {
|
|
432
745
|
throw new CPACError("state_dir contains CPAC files without a valid state; refusing to overwrite them");
|
|
433
746
|
}
|
|
747
|
+
const fingerprint = proxyFingerprint(config, apiKey);
|
|
748
|
+
const previousProxy = state ? stateProxy(state) : null;
|
|
749
|
+
let proxy;
|
|
750
|
+
let startedProxy = false;
|
|
751
|
+
if (previousProxy &&
|
|
752
|
+
state?.proxy_fingerprint === fingerprint &&
|
|
753
|
+
(config.codex_proxy_port === 0 ||
|
|
754
|
+
config.codex_proxy_port === previousProxy.port) &&
|
|
755
|
+
(await proxyIsHealthy(previousProxy))) {
|
|
756
|
+
proxy = previousProxy;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
if (previousProxy)
|
|
760
|
+
await stopProxyProcess(previousProxy);
|
|
761
|
+
try {
|
|
762
|
+
proxy = await startProxyProcess(config, apiKey);
|
|
763
|
+
startedProxy = true;
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
throw new CPACError(`${error instanceof Error ? error.message : String(error)}; choose another codex_proxy_port if the port is occupied`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
770
|
+
let injected;
|
|
771
|
+
try {
|
|
772
|
+
injected = buildCodexConfig(original, proxy.port, catalogPath);
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
if (startedProxy)
|
|
776
|
+
await stopProxyProcess(proxy);
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
434
779
|
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
435
780
|
if (!stateDirExisted)
|
|
436
781
|
chmodSync(config.state_dir, 0o700);
|
|
437
782
|
if (state) {
|
|
438
|
-
|
|
439
|
-
|
|
783
|
+
try {
|
|
784
|
+
atomicWrite(catalogPath, catalog.bytes);
|
|
785
|
+
atomicWrite(config.codex_config, injected, originalMode);
|
|
786
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (startedProxy)
|
|
790
|
+
await stopProxyProcess(proxy);
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
440
793
|
}
|
|
441
794
|
else {
|
|
442
795
|
let configWritten = false;
|
|
@@ -446,11 +799,7 @@ export async function inject(config) {
|
|
|
446
799
|
atomicWrite(catalogPath, catalog.bytes);
|
|
447
800
|
atomicWrite(config.codex_config, injected, originalMode);
|
|
448
801
|
configWritten = true;
|
|
449
|
-
atomicWrite(join(config.state_dir, "state.json"),
|
|
450
|
-
config_path: config.codex_config,
|
|
451
|
-
config_existed: existed,
|
|
452
|
-
config_mode: originalMode,
|
|
453
|
-
}, null, 2)}\n`));
|
|
802
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
454
803
|
}
|
|
455
804
|
catch (error) {
|
|
456
805
|
if (configWritten) {
|
|
@@ -464,18 +813,33 @@ export async function inject(config) {
|
|
|
464
813
|
throw new CPACError(`injection failed and config rollback failed; backup retained in ${config.state_dir}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
465
814
|
}
|
|
466
815
|
}
|
|
816
|
+
let cleanupError;
|
|
467
817
|
try {
|
|
468
818
|
cleanupStateFiles(config.state_dir);
|
|
469
819
|
}
|
|
470
|
-
catch (
|
|
820
|
+
catch (caught) {
|
|
821
|
+
cleanupError = caught;
|
|
822
|
+
}
|
|
823
|
+
if (startedProxy)
|
|
824
|
+
await stopProxyProcess(proxy);
|
|
825
|
+
if (cleanupError) {
|
|
471
826
|
throw new CPACError(`injection failed and state cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
472
827
|
}
|
|
473
828
|
throw error;
|
|
474
829
|
}
|
|
475
830
|
}
|
|
476
|
-
|
|
831
|
+
try {
|
|
832
|
+
rmSync(join(dirname(config.codex_config), "models_cache.json"), {
|
|
833
|
+
force: true,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
console.warn("CPAC could not invalidate Codex models_cache.json; restart Codex App if its model list is stale.");
|
|
838
|
+
}
|
|
839
|
+
console.log(`Injected ${catalog.modelCount} CPA models into ${config.codex_config} via http://127.0.0.1:${proxy.port}/v1`);
|
|
840
|
+
console.log("Restart Codex App if its running app-server still shows the old model list.");
|
|
477
841
|
}
|
|
478
|
-
export function restore(config) {
|
|
842
|
+
export async function restore(config) {
|
|
479
843
|
const state = readState(config.state_dir);
|
|
480
844
|
if (!state)
|
|
481
845
|
throw new CPACError("no active CPAC injection");
|
|
@@ -484,6 +848,9 @@ export function restore(config) {
|
|
|
484
848
|
atomicWrite(config.codex_config, original, state.config_mode);
|
|
485
849
|
else
|
|
486
850
|
rmSync(config.codex_config, { force: true });
|
|
851
|
+
const proxy = stateProxy(state);
|
|
852
|
+
if (proxy)
|
|
853
|
+
await stopProxyProcess(proxy);
|
|
487
854
|
try {
|
|
488
855
|
cleanupStateFiles(config.state_dir);
|
|
489
856
|
}
|
|
@@ -492,22 +859,39 @@ export function restore(config) {
|
|
|
492
859
|
}
|
|
493
860
|
console.log(`Restored ${config.codex_config}`);
|
|
494
861
|
}
|
|
495
|
-
export function status(config) {
|
|
862
|
+
export async function status(config) {
|
|
863
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
864
|
+
const keyConfigured = !!apiKey;
|
|
496
865
|
const state = readState(config.state_dir);
|
|
497
866
|
if (!state) {
|
|
498
|
-
console.log("native");
|
|
499
|
-
return 1;
|
|
867
|
+
console.log("codex: native");
|
|
500
868
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
869
|
+
else {
|
|
870
|
+
originalBytes(config.state_dir, state, config.codex_config);
|
|
871
|
+
if (!existsSync(config.codex_config)) {
|
|
872
|
+
console.log(`codex: injected (config missing: ${config.codex_config})`);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
const proxy = stateProxy(state);
|
|
876
|
+
if (!proxy || !(await proxyIsHealthy(proxy))) {
|
|
877
|
+
console.log(`codex: injected (loopback proxy stopped: ${config.codex_config})`);
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
const digest = createHash("sha256")
|
|
881
|
+
.update(readFileSync(config.codex_config))
|
|
882
|
+
.digest("hex")
|
|
883
|
+
.slice(0, 12);
|
|
884
|
+
console.log(`codex: injected proxy=http://127.0.0.1:${proxy.port}/v1 sha256=${digest}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
505
887
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
888
|
+
console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
|
|
889
|
+
console.log(`pi: ${keyConfigured ? "ready" : "not configured"}`);
|
|
890
|
+
if (!state)
|
|
891
|
+
return 1;
|
|
892
|
+
const proxy = stateProxy(state);
|
|
893
|
+
if (!proxy || !(await proxyIsHealthy(proxy)))
|
|
894
|
+
return 2;
|
|
511
895
|
return 0;
|
|
512
896
|
}
|
|
513
897
|
function claudeBaseUrl(cpaUrl) {
|
|
@@ -568,7 +952,7 @@ export function saveApiKeyExport(profile, name, apiKey) {
|
|
|
568
952
|
atomicWrite(profile, Buffer.from(content), mode);
|
|
569
953
|
}
|
|
570
954
|
async function guide(config) {
|
|
571
|
-
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\n cpac status Show Codex
|
|
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`);
|
|
572
956
|
if (process.env[config.api_key_env]?.trim())
|
|
573
957
|
return 0;
|
|
574
958
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -600,10 +984,43 @@ export async function runClaude(config, args, executable = "claude") {
|
|
|
600
984
|
child.once("close", (code) => resolve(code ?? 1));
|
|
601
985
|
});
|
|
602
986
|
}
|
|
987
|
+
export async function runProxy(config) {
|
|
988
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
989
|
+
const state = readState(config.state_dir);
|
|
990
|
+
if (!state)
|
|
991
|
+
throw new CPACError("no active CPAC injection; run cpac inject first");
|
|
992
|
+
originalBytes(config.state_dir, state, config.codex_config);
|
|
993
|
+
const recorded = stateProxy(state);
|
|
994
|
+
if (!recorded) {
|
|
995
|
+
throw new CPACError("injection has no loopback proxy state; run cpac inject to migrate it");
|
|
996
|
+
}
|
|
997
|
+
if (await proxyIsHealthy(recorded)) {
|
|
998
|
+
throw new CPACError(`loopback proxy is already running on 127.0.0.1:${recorded.port}`);
|
|
999
|
+
}
|
|
1000
|
+
const proxy = await createLoopbackProxy(config.cpa_url, apiKey, recorded.id, recorded.port);
|
|
1001
|
+
try {
|
|
1002
|
+
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)));
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
proxy.server.closeAllConnections?.();
|
|
1006
|
+
proxy.server.close();
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
console.log(`CPAC loopback proxy listening on http://127.0.0.1:${proxy.port}/v1`);
|
|
1010
|
+
const close = () => {
|
|
1011
|
+
proxy.server.closeAllConnections?.();
|
|
1012
|
+
proxy.server.close();
|
|
1013
|
+
};
|
|
1014
|
+
process.once("SIGINT", close);
|
|
1015
|
+
process.once("SIGTERM", close);
|
|
1016
|
+
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
1017
|
+
return 0;
|
|
1018
|
+
}
|
|
603
1019
|
function usage() {
|
|
604
1020
|
return [
|
|
605
1021
|
"Usage: cpac",
|
|
606
1022
|
" cpac <inject|status|restore> [--config PATH]",
|
|
1023
|
+
" cpac proxy [--config PATH]",
|
|
607
1024
|
" cpac claude [--config PATH] [--] [claude args...]",
|
|
608
1025
|
].join("\n");
|
|
609
1026
|
}
|
|
@@ -645,7 +1062,7 @@ function parseArgs(args) {
|
|
|
645
1062
|
positional.push(args[index]);
|
|
646
1063
|
}
|
|
647
1064
|
if (positional.length !== 1 ||
|
|
648
|
-
!["inject", "status", "restore"].includes(positional[0])) {
|
|
1065
|
+
!["inject", "status", "restore", "proxy"].includes(positional[0])) {
|
|
649
1066
|
throw new CPACError(usage());
|
|
650
1067
|
}
|
|
651
1068
|
return {
|
|
@@ -655,6 +1072,12 @@ function parseArgs(args) {
|
|
|
655
1072
|
}
|
|
656
1073
|
export async function main(args = process.argv.slice(2)) {
|
|
657
1074
|
try {
|
|
1075
|
+
if (args[0] === "_proxy") {
|
|
1076
|
+
const port = Number(args[1]);
|
|
1077
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
1078
|
+
return 1;
|
|
1079
|
+
return await runProxyChild(port);
|
|
1080
|
+
}
|
|
658
1081
|
const parsed = parseArgs(args);
|
|
659
1082
|
if (!parsed)
|
|
660
1083
|
return 0;
|
|
@@ -663,12 +1086,14 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
663
1086
|
return await guide(config);
|
|
664
1087
|
if (parsed.command === "claude")
|
|
665
1088
|
return await runClaude(config, parsed.args);
|
|
1089
|
+
if (parsed.command === "proxy")
|
|
1090
|
+
return await runProxy(config);
|
|
666
1091
|
if (parsed.command === "inject")
|
|
667
1092
|
await inject(config);
|
|
668
1093
|
else if (parsed.command === "restore")
|
|
669
|
-
restore(config);
|
|
1094
|
+
await restore(config);
|
|
670
1095
|
else
|
|
671
|
-
return status(config);
|
|
1096
|
+
return await status(config);
|
|
672
1097
|
return 0;
|
|
673
1098
|
}
|
|
674
1099
|
catch (error) {
|