@yhong91/cpac 0.1.24 → 0.1.26
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 +28 -47
- package/dist/agents.js +784 -0
- package/dist/claude.js +373 -0
- package/dist/codex.js +378 -0
- package/dist/config.js +440 -0
- package/dist/cpac.js +157 -2252
- package/dist/proxy.js +341 -0
- package/dist/util.js +192 -0
- package/package.json +2 -3
package/dist/codex.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, stateBytes, stateProxy, } from "./config.js";
|
|
4
|
+
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "./proxy.js";
|
|
5
|
+
import { CPACError, atomicWrite, dominantEol, tomlString } from "./util.js";
|
|
6
|
+
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
7
|
+
const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model_catalog_json')\s*=/;
|
|
8
|
+
const OPENAI_BASE_URL_KEY = /^\s*(?:openai_base_url|"openai_base_url"|'openai_base_url')\s*=/;
|
|
9
|
+
const ROOT_KEY = new RegExp(`(?:${MODEL_PROVIDER_KEY.source}|${MODEL_CATALOG_KEY.source}|${OPENAI_BASE_URL_KEY.source})`);
|
|
10
|
+
export const MANAGED_MARKER = "# CPAC managed; run CPAC 'restore' to restore the original file.";
|
|
11
|
+
function providerPatterns(provider = PROVIDER) {
|
|
12
|
+
const escaped = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
const modelProviders = `(?:model_providers|"model_providers"|'model_providers')`;
|
|
14
|
+
const providerToken = `(?:${escaped}|"${escaped}"|'${escaped}')`;
|
|
15
|
+
return {
|
|
16
|
+
header: new RegExp(`^\\s*\\[\\s*${modelProviders}\\s*\\.\\s*${providerToken}\\s*\\]\\s*(?:#.*)?$`),
|
|
17
|
+
arrayHeader: new RegExp(`^\\s*\\[\\[\\s*${modelProviders}\\s*\\.\\s*${providerToken}\\s*\\]\\]\\s*(?:#.*)?$`),
|
|
18
|
+
parentHeader: new RegExp(`^\\s*\\[\\s*${modelProviders}\\s*\\]\\s*(?:#.*)?$`),
|
|
19
|
+
inlineKey: new RegExp(`^\\s*${providerToken}\\s*=`),
|
|
20
|
+
dottedKey: new RegExp(`^\\s*${modelProviders}\\s*\\.\\s*${providerToken}\\s*=`),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function providerHeader(provider = PROVIDER) {
|
|
24
|
+
return providerPatterns(provider).header;
|
|
25
|
+
}
|
|
26
|
+
export function hasProviderTable(content, provider = PROVIDER) {
|
|
27
|
+
const patterns = providerPatterns(provider);
|
|
28
|
+
let inRoot = true;
|
|
29
|
+
let inModelProviders = false;
|
|
30
|
+
for (const line of content.split(/\r?\n/)) {
|
|
31
|
+
if (patterns.header.test(line) || patterns.arrayHeader.test(line))
|
|
32
|
+
return true;
|
|
33
|
+
if (/^\s*\[/.test(line)) {
|
|
34
|
+
inRoot = false;
|
|
35
|
+
inModelProviders = patterns.parentHeader.test(line);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if ((inModelProviders && patterns.inlineKey.test(line)) ||
|
|
39
|
+
(inRoot && patterns.dottedKey.test(line)))
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function stripManagedConfig(content) {
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new CPACError("Codex config must be UTF-8");
|
|
51
|
+
}
|
|
52
|
+
const eol = dominantEol(text);
|
|
53
|
+
const header = providerHeader();
|
|
54
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
55
|
+
let inRoot = true;
|
|
56
|
+
let inProvider = false;
|
|
57
|
+
let managedRootKeys = 0;
|
|
58
|
+
const kept = [];
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (inProvider && /^\s*\[/.test(line))
|
|
61
|
+
inProvider = false;
|
|
62
|
+
if (!inProvider && header.test(line)) {
|
|
63
|
+
inProvider = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inProvider)
|
|
67
|
+
continue;
|
|
68
|
+
if (/^\s*\[/.test(line))
|
|
69
|
+
inRoot = false;
|
|
70
|
+
if (inRoot && line === MANAGED_MARKER) {
|
|
71
|
+
managedRootKeys = 2;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inRoot && managedRootKeys > 0 && ROOT_KEY.test(line)) {
|
|
75
|
+
managedRootKeys -= 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
kept.push(line);
|
|
79
|
+
}
|
|
80
|
+
const output = kept.join("\n");
|
|
81
|
+
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
82
|
+
}
|
|
83
|
+
export function buildCodexConfig(original, proxyPort, catalogPath) {
|
|
84
|
+
let text;
|
|
85
|
+
try {
|
|
86
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(original);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new CPACError("Codex config must be UTF-8");
|
|
90
|
+
}
|
|
91
|
+
if (hasProviderTable(text)) {
|
|
92
|
+
throw new CPACError(`Codex config already contains [model_providers.${PROVIDER}]`);
|
|
93
|
+
}
|
|
94
|
+
const normalizedLines = text.replace(/\r\n/g, "\n").split("\n");
|
|
95
|
+
const firstTable = normalizedLines.findIndex((line) => /^\s*\[/.test(line));
|
|
96
|
+
const rootLines = normalizedLines.slice(0, firstTable === -1 ? undefined : firstTable);
|
|
97
|
+
const providerLine = rootLines.find((line) => MODEL_PROVIDER_KEY.test(line));
|
|
98
|
+
if (providerLine) {
|
|
99
|
+
const match = /=\s*["']([^"']+)["']/.exec(providerLine);
|
|
100
|
+
if (!match || match[1] !== "openai") {
|
|
101
|
+
throw new CPACError("Codex config selects an external model_provider; restore it to openai before injecting CPAC");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (rootLines.some((line) => OPENAI_BASE_URL_KEY.test(line))) {
|
|
105
|
+
throw new CPACError("Codex config already contains openai_base_url; remove that user-owned override before injecting CPAC");
|
|
106
|
+
}
|
|
107
|
+
const eol = dominantEol(text);
|
|
108
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
109
|
+
let inRoot = true;
|
|
110
|
+
const kept = lines.filter((line) => {
|
|
111
|
+
if (/^\s*\[/.test(line))
|
|
112
|
+
inRoot = false;
|
|
113
|
+
return !(inRoot && MODEL_CATALOG_KEY.test(line));
|
|
114
|
+
});
|
|
115
|
+
let body = kept.join("\n");
|
|
116
|
+
if (body && !body.endsWith("\n"))
|
|
117
|
+
body += "\n";
|
|
118
|
+
const root = [
|
|
119
|
+
MANAGED_MARKER,
|
|
120
|
+
`model_catalog_json = ${tomlString(catalogPath)}`,
|
|
121
|
+
`openai_base_url = ${tomlString(`http://127.0.0.1:${proxyPort}/v1`)}`,
|
|
122
|
+
"",
|
|
123
|
+
].join("\n");
|
|
124
|
+
const output = root + body;
|
|
125
|
+
return Buffer.from(eol === "\n" ? output : output.replace(/\n/g, "\r\n"));
|
|
126
|
+
}
|
|
127
|
+
// Codex multi-agent v2 (features.multi_agent_v2). Accepts the same TOML
|
|
128
|
+
// shapes codex-rs does: a [features.multi_agent_v2] table, an inline
|
|
129
|
+
// `multi_agent_v2 = ...` under [features], or dotted root keys.
|
|
130
|
+
const V2_TABLE_HEADER = /^\s*\[\s*(?:features|"features"|'features')\s*\.\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')\s*\]\s*(?:#.*)?$/;
|
|
131
|
+
const FEATURES_TABLE_HEADER = /^\s*\[\s*(?:features|"features"|'features')\s*\]\s*(?:#.*)?$/;
|
|
132
|
+
const V2_KEY = /^\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')\s*=/;
|
|
133
|
+
const V2_DOTTED_KEY = /^\s*(?:features|"features"|'features')\s*\.\s*(?:multi_agent_v2|"multi_agent_v2"|'multi_agent_v2')(?:\s*\.\s*(?:enabled|"enabled"|'enabled'))?\s*=/;
|
|
134
|
+
function v2LineValue(line) {
|
|
135
|
+
const inline = line.match(/\{[^}]*\benabled\s*=\s*(true|false)/);
|
|
136
|
+
if (inline)
|
|
137
|
+
return inline[1] === "true";
|
|
138
|
+
const plain = line.match(/=\s*(true|false)(?![A-Za-z0-9_])/);
|
|
139
|
+
if (plain)
|
|
140
|
+
return plain[1] === "true";
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
export function multiAgentV2Enabled(content) {
|
|
144
|
+
let table = "root";
|
|
145
|
+
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
146
|
+
if (V2_TABLE_HEADER.test(line)) {
|
|
147
|
+
table = "v2";
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (FEATURES_TABLE_HEADER.test(line)) {
|
|
151
|
+
table = "features";
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (/^\s*\[/.test(line)) {
|
|
155
|
+
table = "other";
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (table === "v2" && /^\s*(?:enabled|"enabled"|'enabled')\s*=/.test(line)) {
|
|
159
|
+
return v2LineValue(line) ?? false;
|
|
160
|
+
}
|
|
161
|
+
if (table === "features" && V2_KEY.test(line))
|
|
162
|
+
return v2LineValue(line) ?? false;
|
|
163
|
+
if (table === "root" && V2_DOTTED_KEY.test(line))
|
|
164
|
+
return v2LineValue(line) ?? false;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function stripMultiAgentV2(content) {
|
|
169
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
170
|
+
const kept = [];
|
|
171
|
+
let skipTable = false;
|
|
172
|
+
let table = "root";
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (/^\s*\[/.test(line)) {
|
|
175
|
+
skipTable = V2_TABLE_HEADER.test(line);
|
|
176
|
+
if (skipTable) {
|
|
177
|
+
table = "other";
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
table = FEATURES_TABLE_HEADER.test(line) ? "features" : "other";
|
|
181
|
+
}
|
|
182
|
+
if (skipTable)
|
|
183
|
+
continue;
|
|
184
|
+
if (table === "features" && V2_KEY.test(line))
|
|
185
|
+
continue;
|
|
186
|
+
if (table === "root" && V2_DOTTED_KEY.test(line))
|
|
187
|
+
continue;
|
|
188
|
+
kept.push(line);
|
|
189
|
+
}
|
|
190
|
+
return kept.join("\n");
|
|
191
|
+
}
|
|
192
|
+
function hasAgentsMaxThreads(content) {
|
|
193
|
+
let table = "root";
|
|
194
|
+
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
195
|
+
if (/^\s*\[/.test(line)) {
|
|
196
|
+
table = /^\s*\[\s*(?:agents|"agents"|'agents')\s*\]\s*(?:#.*)?$/.test(line)
|
|
197
|
+
? "agents"
|
|
198
|
+
: "other";
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (table === "agents" && /^\s*max_threads\s*=/.test(line))
|
|
202
|
+
return true;
|
|
203
|
+
if (table === "root" &&
|
|
204
|
+
/^\s*(?:agents|"agents"|'agents')\s*\.\s*max_threads\s*=/.test(line))
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
export function withMultiAgentV2(content, enabled, eol) {
|
|
210
|
+
let body = stripMultiAgentV2(content);
|
|
211
|
+
if (body && !body.endsWith("\n"))
|
|
212
|
+
body += "\n";
|
|
213
|
+
const output = body + `\n[features.multi_agent_v2]\nenabled = ${enabled}\n`;
|
|
214
|
+
return eol === "\n" ? output : output.replace(/\n/g, "\r\n");
|
|
215
|
+
}
|
|
216
|
+
export async function inject(config, v2Off = false, maxContext = false) {
|
|
217
|
+
const apiKey = process.env[config.api_key_env]?.trim();
|
|
218
|
+
if (!apiKey)
|
|
219
|
+
throw new CPACError(`environment variable ${config.api_key_env} is not set`);
|
|
220
|
+
const state = readState(config.state_dir);
|
|
221
|
+
let existed;
|
|
222
|
+
let original;
|
|
223
|
+
let originalMode;
|
|
224
|
+
if (state) {
|
|
225
|
+
const backup = originalBytes(config.state_dir, state, config.codex_config);
|
|
226
|
+
try {
|
|
227
|
+
original = existsSync(config.codex_config)
|
|
228
|
+
? stripManagedConfig(readFileSync(config.codex_config))
|
|
229
|
+
: backup;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error instanceof CPACError)
|
|
233
|
+
throw error;
|
|
234
|
+
throw new CPACError(`cannot read Codex config: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
}
|
|
236
|
+
existed = state.config_existed;
|
|
237
|
+
originalMode = state.config_mode;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
existed = existsSync(config.codex_config);
|
|
241
|
+
try {
|
|
242
|
+
original = existed ? readFileSync(config.codex_config) : new Uint8Array();
|
|
243
|
+
originalMode = existed ? statSync(config.codex_config).mode & 0o7777 : 0o600;
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
throw new CPACError(`cannot read Codex config: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
250
|
+
if (config.spawn_models?.length)
|
|
251
|
+
catalog.bytes = reorderCatalog(catalog.bytes, config.spawn_models);
|
|
252
|
+
if (maxContext)
|
|
253
|
+
catalog.bytes = liftContextWindows(catalog.bytes);
|
|
254
|
+
const stateDirExisted = existsSync(config.state_dir);
|
|
255
|
+
if (!state &&
|
|
256
|
+
STATE_FILES.some((name) => existsSync(join(config.state_dir, name)))) {
|
|
257
|
+
throw new CPACError("state_dir contains CPAC files without a valid state; refusing to overwrite them");
|
|
258
|
+
}
|
|
259
|
+
const fingerprint = proxyFingerprint(config, apiKey);
|
|
260
|
+
const previousProxy = state ? stateProxy(state) : null;
|
|
261
|
+
let proxy;
|
|
262
|
+
let startedProxy = false;
|
|
263
|
+
if (previousProxy &&
|
|
264
|
+
state?.proxy_fingerprint === fingerprint &&
|
|
265
|
+
(config.codex_proxy_port === 0 ||
|
|
266
|
+
config.codex_proxy_port === previousProxy.port) &&
|
|
267
|
+
(await proxyIsHealthy(previousProxy))) {
|
|
268
|
+
proxy = previousProxy;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
if (previousProxy)
|
|
272
|
+
await stopProxyProcess(previousProxy);
|
|
273
|
+
try {
|
|
274
|
+
proxy = await startProxyProcess(config, apiKey);
|
|
275
|
+
startedProxy = true;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
throw new CPACError(`${error instanceof Error ? error.message : String(error)}; choose another codex_proxy_port if the port is occupied`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
282
|
+
let injected;
|
|
283
|
+
try {
|
|
284
|
+
injected = buildCodexConfig(original, proxy.port, catalogPath);
|
|
285
|
+
const injectedText = new TextDecoder("utf-8").decode(injected);
|
|
286
|
+
injected = Buffer.from(withMultiAgentV2(injectedText, !v2Off, dominantEol(injectedText)));
|
|
287
|
+
if (!v2Off && hasAgentsMaxThreads(injectedText)) {
|
|
288
|
+
console.warn("warning: [agents] max_threads is set; codex refuses to start with multi_agent_v2 enabled, remove it");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (startedProxy)
|
|
293
|
+
await stopProxyProcess(proxy);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
297
|
+
if (!stateDirExisted)
|
|
298
|
+
chmodSync(config.state_dir, 0o700);
|
|
299
|
+
if (state) {
|
|
300
|
+
try {
|
|
301
|
+
atomicWrite(catalogPath, catalog.bytes);
|
|
302
|
+
atomicWrite(config.codex_config, injected, originalMode);
|
|
303
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (startedProxy)
|
|
307
|
+
await stopProxyProcess(proxy);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
let configWritten = false;
|
|
313
|
+
try {
|
|
314
|
+
if (existed)
|
|
315
|
+
atomicWrite(join(config.state_dir, "config.toml.backup"), original);
|
|
316
|
+
atomicWrite(catalogPath, catalog.bytes);
|
|
317
|
+
atomicWrite(config.codex_config, injected, originalMode);
|
|
318
|
+
configWritten = true;
|
|
319
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (configWritten) {
|
|
323
|
+
try {
|
|
324
|
+
if (existed)
|
|
325
|
+
atomicWrite(config.codex_config, original, originalMode);
|
|
326
|
+
else
|
|
327
|
+
rmSync(config.codex_config, { force: true });
|
|
328
|
+
}
|
|
329
|
+
catch (rollbackError) {
|
|
330
|
+
throw new CPACError(`injection failed and config rollback failed; backup retained in ${config.state_dir}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
let cleanupError;
|
|
334
|
+
try {
|
|
335
|
+
cleanupStateFiles(config.state_dir);
|
|
336
|
+
}
|
|
337
|
+
catch (caught) {
|
|
338
|
+
cleanupError = caught;
|
|
339
|
+
}
|
|
340
|
+
if (startedProxy)
|
|
341
|
+
await stopProxyProcess(proxy);
|
|
342
|
+
if (cleanupError) {
|
|
343
|
+
throw new CPACError(`injection failed and state cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
344
|
+
}
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
rmSync(join(dirname(config.codex_config), "models_cache.json"), {
|
|
350
|
+
force: true,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
console.warn("CPAC could not invalidate Codex models_cache.json; restart Codex App if its model list is stale.");
|
|
355
|
+
}
|
|
356
|
+
console.log(`Injected ${catalog.modelCount} CPA models into ${config.codex_config} via http://127.0.0.1:${proxy.port}/v1 (multi-agent v2: ${v2Off ? "off" : "on"})`);
|
|
357
|
+
console.log("Restart Codex App if its running app-server still shows the old model list.");
|
|
358
|
+
}
|
|
359
|
+
export async function restore(config) {
|
|
360
|
+
const state = readState(config.state_dir);
|
|
361
|
+
if (!state)
|
|
362
|
+
throw new CPACError("no active CPAC injection");
|
|
363
|
+
const original = originalBytes(config.state_dir, state, config.codex_config);
|
|
364
|
+
if (state.config_existed)
|
|
365
|
+
atomicWrite(config.codex_config, original, state.config_mode);
|
|
366
|
+
else
|
|
367
|
+
rmSync(config.codex_config, { force: true });
|
|
368
|
+
const proxy = stateProxy(state);
|
|
369
|
+
if (proxy)
|
|
370
|
+
await stopProxyProcess(proxy);
|
|
371
|
+
try {
|
|
372
|
+
cleanupStateFiles(config.state_dir);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
throw new CPACError(`config restored, but state cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
376
|
+
}
|
|
377
|
+
console.log(`Restored ${config.codex_config}`);
|
|
378
|
+
}
|