@yhong91/cpac 0.1.26 → 0.1.28
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 +19 -19
- package/dist/agents.js +60 -396
- package/dist/config.js +40 -1
- package/dist/cpac.js +95 -33
- package/dist/proxy.js +2 -2
- package/dist/{claude.js → targets/claude.js} +3 -3
- package/dist/{codex.js → targets/codex.js} +64 -11
- package/dist/targets/grok.js +109 -0
- package/dist/targets/kimi.js +175 -0
- package/dist/targets/opencode.js +59 -0
- package/dist/targets/pi.js +32 -0
- package/dist/util.js +9 -1
- package/package.json +1 -1
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) {
|
package/dist/cpac.js
CHANGED
|
@@ -3,18 +3,26 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { CPAC_VERSION,
|
|
7
|
-
import { runClaude, runClaudeConfig, runClaudeModels } from "./claude.js";
|
|
8
|
-
import { inject, multiAgentV2Enabled } from "./codex.js";
|
|
6
|
+
import { CPAC_VERSION, runCodexConfig, runDetect, runInstall, runRestore, runSync, runTargetLauncher, runUninstall, runUpgrade, } from "./agents.js";
|
|
7
|
+
import { runClaude, runClaudeConfig, runClaudeModels, } from "./targets/claude.js";
|
|
8
|
+
import { inject, multiAgentV2Enabled } from "./targets/codex.js";
|
|
9
|
+
import { isGrokConfigInstalled } from "./targets/grok.js";
|
|
10
|
+
import { isKimiConfigInstalled } from "./targets/kimi.js";
|
|
11
|
+
import { runOpencode } from "./targets/opencode.js";
|
|
12
|
+
import { isPiExtensionInstalled } from "./targets/pi.js";
|
|
9
13
|
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, } from "./config.js";
|
|
10
14
|
import { proxyIsHealthy, runProxy, runProxyChild } from "./proxy.js";
|
|
11
15
|
import { CPACError, expandUserPath, promptSecret, saveApiKeyExport, shellProfile, } from "./util.js";
|
|
12
16
|
export { PROVIDER, loadConfig, saveSpawnModels, liftContextWindows, reorderCatalog, readState, stateProxy, } from "./config.js";
|
|
13
17
|
export { saveApiKeyExport } from "./util.js";
|
|
14
18
|
export { stopProxyProcess } from "./proxy.js";
|
|
15
|
-
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, withMultiAgentV2, } from "./codex.js";
|
|
16
|
-
export { claudeTierSlots, createClaudeProxy, runClaude } from "./claude.js";
|
|
17
|
-
export { detectClientVersion, detectTargets,
|
|
19
|
+
export { buildCodexConfig, hasProviderTable, inject, multiAgentV2Enabled, restore, withMultiAgentV2, } from "./targets/codex.js";
|
|
20
|
+
export { claudeTierSlots, createClaudeProxy, runClaude, } from "./targets/claude.js";
|
|
21
|
+
export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, runTargetLauncher, } from "./agents.js";
|
|
22
|
+
export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
23
|
+
export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
24
|
+
export { runOpencode } from "./targets/opencode.js";
|
|
25
|
+
export { installPiExtension, isPiExtensionInstalled, uninstallPiExtension, } from "./targets/pi.js";
|
|
18
26
|
export async function status(config) {
|
|
19
27
|
const apiKey = process.env[config.api_key_env]?.trim();
|
|
20
28
|
const keyConfigured = !!apiKey;
|
|
@@ -62,7 +70,11 @@ export async function status(config) {
|
|
|
62
70
|
return 0;
|
|
63
71
|
}
|
|
64
72
|
async function guide(config) {
|
|
65
|
-
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 <agent> [args...] Launch an agent (codex, kimi, grok, pi, claude, opencode) 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, Grok)\n cpac restore
|
|
73
|
+
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 <agent> [args...] Launch an agent (codex, kimi, grok, pi, claude, opencode) 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, Grok)\n cpac restore --target <id> | --all Detach injected agents
|
|
74
|
+
cpac detect Show supported targets and install status
|
|
75
|
+
cpac install Install CPA integration into detected targets
|
|
76
|
+
cpac sync Refresh installed Codex/Pi/Kimi/Grok injections from CPA
|
|
77
|
+
cpac uninstall Detach all agents, then remove the cpac package (confirms first)\n cpac upgrade Update cpac from npm and sync installed agents\n cpac --help Show command usage`);
|
|
66
78
|
if (process.env[config.api_key_env]?.trim())
|
|
67
79
|
return 0;
|
|
68
80
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -78,20 +90,21 @@ function usage() {
|
|
|
78
90
|
return [
|
|
79
91
|
"Usage: cpac",
|
|
80
92
|
" cpac <codex|kimi|grok|pi|claude|opencode> [args...]",
|
|
81
|
-
" cpac <codex|claude> setup",
|
|
93
|
+
" cpac <codex|claude> --setup [args...]",
|
|
82
94
|
" cpac <codex|claude|kimi|grok|pi> clear",
|
|
83
95
|
" cpac inject [--v2_off] [--v2_models] [--max_context] [--config PATH]",
|
|
84
|
-
" cpac restore
|
|
96
|
+
" cpac restore --target <id> | --all [--dry-run] [--config PATH]",
|
|
85
97
|
" cpac status [--config PATH]",
|
|
86
98
|
" cpac proxy [--config PATH]",
|
|
87
99
|
" cpac detect [--json] [--home PATH]",
|
|
88
100
|
" cpac install [--target codex,pi,kimi,grok] [--all] [--dry-run] [--force] [--v2_off] [--v2_models] [--max_context] [--home PATH]",
|
|
89
101
|
" cpac sync [--target codex,pi,kimi,grok] [--all] [--dry-run] [--v2_off] [--v2_models] [--max_context] [--home PATH]",
|
|
102
|
+
" cpac uninstall [--dry-run]",
|
|
90
103
|
" cpac upgrade [--check]",
|
|
91
|
-
" cpac version",
|
|
104
|
+
" cpac -v | --version",
|
|
92
105
|
"",
|
|
93
106
|
"launch an agent directly with `cpac <agent> [args...]` (all args forwarded).",
|
|
94
|
-
"configure
|
|
107
|
+
"configure then launch with `cpac <codex|claude> --setup`; restore with `cpac <agent> clear`.",
|
|
95
108
|
].join("\n");
|
|
96
109
|
}
|
|
97
110
|
function parseArgs(args) {
|
|
@@ -103,8 +116,7 @@ function parseArgs(args) {
|
|
|
103
116
|
args[0] === "sync" ||
|
|
104
117
|
args[0] === "uninstall" ||
|
|
105
118
|
args[0] === "restore" ||
|
|
106
|
-
args[0] === "upgrade"
|
|
107
|
-
args[0] === "version") {
|
|
119
|
+
args[0] === "upgrade") {
|
|
108
120
|
const parsed = {
|
|
109
121
|
command: args[0],
|
|
110
122
|
configPath: defaultConfigPath(),
|
|
@@ -169,10 +181,34 @@ function parseArgs(args) {
|
|
|
169
181
|
if (["codex", "kimi", "grok", "pi", "claude", "opencode"].includes(args[0])) {
|
|
170
182
|
const targetId = args[0];
|
|
171
183
|
let configPath = defaultConfigPath();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
const raw = args.slice(1);
|
|
185
|
+
let setup = false;
|
|
186
|
+
let clear = false;
|
|
187
|
+
const rest = [];
|
|
188
|
+
for (let i = 0; i < raw.length; i++) {
|
|
189
|
+
const arg = raw[i];
|
|
190
|
+
if (i === 0 && arg === "setup") {
|
|
191
|
+
setup = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (i === 0 && arg === "clear") {
|
|
195
|
+
clear = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg === "--setup") {
|
|
199
|
+
setup = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (arg === "--clear") {
|
|
203
|
+
clear = true;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
rest.push(arg);
|
|
207
|
+
}
|
|
208
|
+
if (clear) {
|
|
209
|
+
for (let i = 0; i < rest.length; i++) {
|
|
210
|
+
if (rest[i] === "--config") {
|
|
211
|
+
const val = rest[++i];
|
|
176
212
|
if (val)
|
|
177
213
|
configPath = resolve(expandUserPath(val));
|
|
178
214
|
}
|
|
@@ -201,13 +237,17 @@ function parseArgs(args) {
|
|
|
201
237
|
maxContext: false,
|
|
202
238
|
};
|
|
203
239
|
}
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
if (setup) {
|
|
241
|
+
const launchArgs = [];
|
|
242
|
+
for (let i = 0; i < rest.length; i++) {
|
|
243
|
+
if (rest[i] === "--config") {
|
|
244
|
+
const val = rest[++i];
|
|
245
|
+
if (!val)
|
|
246
|
+
throw new CPACError("--config requires a path");
|
|
247
|
+
configPath = resolve(expandUserPath(val));
|
|
248
|
+
continue;
|
|
210
249
|
}
|
|
250
|
+
launchArgs.push(rest[i]);
|
|
211
251
|
}
|
|
212
252
|
if (targetId === "codex") {
|
|
213
253
|
return {
|
|
@@ -218,6 +258,8 @@ function parseArgs(args) {
|
|
|
218
258
|
maxContext: false,
|
|
219
259
|
reset: false,
|
|
220
260
|
interactive: true,
|
|
261
|
+
launch: true,
|
|
262
|
+
args: launchArgs,
|
|
221
263
|
};
|
|
222
264
|
}
|
|
223
265
|
if (targetId === "claude") {
|
|
@@ -228,11 +270,13 @@ function parseArgs(args) {
|
|
|
228
270
|
pick: [],
|
|
229
271
|
reset: false,
|
|
230
272
|
interactive: true,
|
|
273
|
+
launch: true,
|
|
274
|
+
args: launchArgs,
|
|
231
275
|
};
|
|
232
276
|
}
|
|
233
277
|
throw new CPACError(`${targetId} does not require setup (all CPA models are auto-synced). Run 'cpac ${targetId}' or 'cpac install --target ${targetId}'.`);
|
|
234
278
|
}
|
|
235
|
-
const passArgs =
|
|
279
|
+
const passArgs = rest;
|
|
236
280
|
if (targetId === "codex") {
|
|
237
281
|
return {
|
|
238
282
|
command: "codex",
|
|
@@ -372,6 +416,10 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
372
416
|
return 1;
|
|
373
417
|
return await runProxyChild(port);
|
|
374
418
|
}
|
|
419
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
420
|
+
console.log(CPAC_VERSION);
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
375
423
|
const parsed = parseArgs(args);
|
|
376
424
|
if (!parsed)
|
|
377
425
|
return 0;
|
|
@@ -413,34 +461,48 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
413
461
|
maxContext: parsed.maxContext,
|
|
414
462
|
});
|
|
415
463
|
}
|
|
416
|
-
if (parsed.command === "
|
|
464
|
+
if (parsed.command === "restore")
|
|
417
465
|
return await runRestore(config, parsed.targets, {
|
|
418
466
|
all: parsed.all,
|
|
419
467
|
dryRun: parsed.dryRun,
|
|
420
468
|
});
|
|
469
|
+
if (parsed.command === "uninstall") {
|
|
470
|
+
if (parsed.targets.length || parsed.all) {
|
|
471
|
+
throw new CPACError("uninstall removes the cpac package; detach agents with: cpac restore --target <id> | --all");
|
|
472
|
+
}
|
|
473
|
+
return await runUninstall(config, { dryRun: parsed.dryRun });
|
|
474
|
+
}
|
|
421
475
|
if (parsed.command === "upgrade")
|
|
422
476
|
return await runUpgrade(config, parsed.check);
|
|
423
|
-
if (parsed.command === "version") {
|
|
424
|
-
console.log(CPAC_VERSION);
|
|
425
|
-
return 0;
|
|
426
|
-
}
|
|
427
477
|
if (parsed.command === "claude")
|
|
428
478
|
return await runClaude(config, parsed.args);
|
|
429
|
-
if (parsed.command === "claude-config")
|
|
430
|
-
|
|
479
|
+
if (parsed.command === "claude-config") {
|
|
480
|
+
const code = await runClaudeConfig(config, parsed.configPath, {
|
|
431
481
|
models: parsed.models,
|
|
432
482
|
pick: parsed.pick,
|
|
433
483
|
reset: parsed.reset,
|
|
434
484
|
interactive: parsed.interactive,
|
|
435
485
|
});
|
|
436
|
-
|
|
437
|
-
|
|
486
|
+
if (code !== 0 || !parsed.launch)
|
|
487
|
+
return code;
|
|
488
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
489
|
+
return code;
|
|
490
|
+
return await runClaude(loadConfig(parsed.configPath, true), parsed.args ?? []);
|
|
491
|
+
}
|
|
492
|
+
if (parsed.command === "codex-config") {
|
|
493
|
+
const code = await runCodexConfig(config, parsed.configPath, {
|
|
438
494
|
v2Off: parsed.v2Off,
|
|
439
495
|
v2Models: parsed.v2Models,
|
|
440
496
|
maxContext: parsed.maxContext,
|
|
441
497
|
reset: parsed.reset,
|
|
442
498
|
interactive: parsed.interactive,
|
|
443
499
|
});
|
|
500
|
+
if (code !== 0 || !parsed.launch)
|
|
501
|
+
return code;
|
|
502
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
503
|
+
return code;
|
|
504
|
+
return await runTargetLauncher(loadConfig(parsed.configPath, true), "codex", parsed.args ?? [], "codex", { skipSync: true });
|
|
505
|
+
}
|
|
444
506
|
if (parsed.command === "opencode")
|
|
445
507
|
return await runOpencode(config, parsed.args);
|
|
446
508
|
if (parsed.command === "codex") {
|
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?.();
|
|
@@ -2,9 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { createServer, request as httpRequest, } from "node:http";
|
|
4
4
|
import { request as httpsRequest } from "node:https";
|
|
5
|
-
import { apiBase, catalogModelId, catalogModelRows, fetchCatalog, loadConfig, } from "
|
|
6
|
-
import { proxyHeaders, responseHeaders, upstreamUrl } from "
|
|
7
|
-
import { CPACError, atomicWrite, checkboxPicker, objectValue, resolveApiKey, } from "
|
|
5
|
+
import { apiBase, catalogModelId, catalogModelRows, fetchCatalog, loadConfig, } from "../config.js";
|
|
6
|
+
import { proxyHeaders, responseHeaders, upstreamUrl } from "../proxy.js";
|
|
7
|
+
import { CPACError, atomicWrite, checkboxPicker, objectValue, resolveApiKey, } from "../util.js";
|
|
8
8
|
const CLAUDE_ALIAS_PREFIX = "claude-cpac--";
|
|
9
9
|
// Claude Code accepts CLAUDE_CODE_AUTO_COMPACT_WINDOW in 100K–1M (binary-verified).
|
|
10
10
|
// A single global window cannot be per-model; 350K is opencodex's user-approved
|
|
@@ -1,13 +1,44 @@
|
|
|
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 "
|
|
4
|
-
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "
|
|
5
|
-
import { CPACError, atomicWrite, dominantEol, tomlString } from "
|
|
3
|
+
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, injectedOwnership, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, sha256Hex, stateBytes, stateProxy, } from "../config.js";
|
|
4
|
+
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "../proxy.js";
|
|
5
|
+
import { CPACError, atomicWrite, dominantEol, tomlString } from "../util.js";
|
|
6
6
|
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
7
7
|
const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model_catalog_json')\s*=/;
|
|
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);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { catalogModelId, catalogModelRows, fetchCatalog, readState, stateProxy, } from "../config.js";
|
|
5
|
+
import { proxyIsHealthy } from "../proxy.js";
|
|
6
|
+
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, resolveApiKey, tomlString, } from "../util.js";
|
|
7
|
+
export function grokHome() {
|
|
8
|
+
const home = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
9
|
+
return expandUserPath(home);
|
|
10
|
+
}
|
|
11
|
+
export function grokConfigPath() {
|
|
12
|
+
return join(grokHome(), "config.toml");
|
|
13
|
+
}
|
|
14
|
+
const GROK_BLOCK_START = "# >>> CPAC Grok >>>";
|
|
15
|
+
const GROK_BLOCK_END = "# <<< CPAC Grok <<<";
|
|
16
|
+
const grokBlockRegex = new RegExp(`${GROK_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GROK_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
17
|
+
export function isGrokConfigInstalled() {
|
|
18
|
+
try {
|
|
19
|
+
const content = readFileSync(grokConfigPath(), "utf8");
|
|
20
|
+
return (grokBlockRegex.test(content) ||
|
|
21
|
+
/^\s*\[model\.(?:"cpac\/|'cpac\/|cpac-)/m.test(content));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isCpacGrokTableHeader(line) {
|
|
28
|
+
const header = line.match(/^\[([^\]]+)\]\s*$/);
|
|
29
|
+
if (!header)
|
|
30
|
+
return false;
|
|
31
|
+
const name = header[1];
|
|
32
|
+
return /^model\.(?:"cpac\/|'cpac\/|cpac-)/.test(name);
|
|
33
|
+
}
|
|
34
|
+
function stripOrphanCpacGrokTables(content) {
|
|
35
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
36
|
+
const out = [];
|
|
37
|
+
let skipping = false;
|
|
38
|
+
for (const line of content.split(/\r?\n/)) {
|
|
39
|
+
if (/^\[[^\]]+\]\s*$/.test(line))
|
|
40
|
+
skipping = isCpacGrokTableHeader(line);
|
|
41
|
+
if (!skipping)
|
|
42
|
+
out.push(line);
|
|
43
|
+
}
|
|
44
|
+
return out.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`);
|
|
45
|
+
}
|
|
46
|
+
function writeGrokBlock(path, block) {
|
|
47
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
48
|
+
content = content.replace(grokBlockRegex, "");
|
|
49
|
+
const orphan = content.indexOf(GROK_BLOCK_START);
|
|
50
|
+
if (orphan !== -1)
|
|
51
|
+
content = content.slice(0, orphan);
|
|
52
|
+
content = stripOrphanCpacGrokTables(content);
|
|
53
|
+
if (block) {
|
|
54
|
+
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
57
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
58
|
+
atomicWrite(path, Buffer.from(content), mode);
|
|
59
|
+
}
|
|
60
|
+
export async function installGrokConfig(config) {
|
|
61
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
62
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
63
|
+
let document;
|
|
64
|
+
try {
|
|
65
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new CPACError("invalid CPA catalog");
|
|
69
|
+
}
|
|
70
|
+
const rows = catalogModelRows(document) ?? [];
|
|
71
|
+
if (rows.length === 0)
|
|
72
|
+
throw new CPACError("CPA catalog contains no models");
|
|
73
|
+
const state = readState(config.state_dir);
|
|
74
|
+
const recorded = state ? stateProxy(state) : null;
|
|
75
|
+
const port = recorded?.port ?? config.codex_proxy_port;
|
|
76
|
+
if (!port) {
|
|
77
|
+
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
78
|
+
}
|
|
79
|
+
const lines = [GROK_BLOCK_START];
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const slug = catalogModelId(row);
|
|
82
|
+
if (!slug)
|
|
83
|
+
continue;
|
|
84
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
85
|
+
? Math.floor(row.context_window)
|
|
86
|
+
: 200000;
|
|
87
|
+
const name = typeof row.display_name === "string" && row.display_name.trim()
|
|
88
|
+
? `${row.display_name.trim()} (CPAC)`
|
|
89
|
+
: `CPAC ${slug}`;
|
|
90
|
+
lines.push(`[model."cpac/${slug}"]`, `model = ${tomlString(slug)}`, `base_url = "http://127.0.0.1:${port}/v1"`, 'api_backend = "responses"', '# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.', 'api_key = "cpac-loopback"', `name = ${tomlString(name)}`, `context_window = ${context}`, "");
|
|
91
|
+
}
|
|
92
|
+
if (lines[lines.length - 1] === "")
|
|
93
|
+
lines.pop();
|
|
94
|
+
lines.push(GROK_BLOCK_END);
|
|
95
|
+
const target = grokConfigPath();
|
|
96
|
+
ensureCpacBackup(target);
|
|
97
|
+
writeGrokBlock(target, lines.join("\n"));
|
|
98
|
+
console.log(`Installed Grok Build provider config: ${target}`);
|
|
99
|
+
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
100
|
+
console.log("Loopback proxy is not running; run: cpac inject");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function uninstallGrokConfig() {
|
|
104
|
+
const target = grokConfigPath();
|
|
105
|
+
if (!isGrokConfigInstalled())
|
|
106
|
+
throw new CPACError("Grok Build config is not installed");
|
|
107
|
+
writeGrokBlock(target, null);
|
|
108
|
+
console.log(`Removed Grok Build provider config: ${target}`);
|
|
109
|
+
}
|