@jacobbd/relay-ai 0.9.5 → 0.9.7
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/dist/{chunk-SCW2TYSG.js → chunk-BJ4AE3PS.js} +122 -30
- package/dist/{chunk-SCW2TYSG.js.map → chunk-BJ4AE3PS.js.map} +1 -1
- package/dist/cli.js +659 -212
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/{ui-command-Q6LBKVM3.js → ui-command-NM4MNZLO.js} +2 -2
- package/package.json +1 -1
- /package/dist/{ui-command-Q6LBKVM3.js.map → ui-command-NM4MNZLO.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
getCodexProxyDebugLogPath,
|
|
85
85
|
getConfigPath,
|
|
86
86
|
getGeminiProxyDebugLogPath,
|
|
87
|
+
getLogsPath,
|
|
87
88
|
getProvidersPath,
|
|
88
89
|
getProxyDebugLogPath,
|
|
89
90
|
getReasoningCapabilities,
|
|
@@ -195,9 +196,10 @@ import {
|
|
|
195
196
|
updateCustomEndpointProvider,
|
|
196
197
|
upstreamHttpStatus,
|
|
197
198
|
validateCustomEndpointUrl,
|
|
199
|
+
waitForCodexAppQuit,
|
|
198
200
|
writeSecureLogLine,
|
|
199
201
|
zenRegistryStub
|
|
200
|
-
} from "./chunk-
|
|
202
|
+
} from "./chunk-BJ4AE3PS.js";
|
|
201
203
|
import {
|
|
202
204
|
filterTemplates,
|
|
203
205
|
getTemplateById,
|
|
@@ -1295,7 +1297,7 @@ ${pc4.bold("Subcommands:")}
|
|
|
1295
1297
|
(none) Provider hub wizard ${pc4.dim("[Phase 1.1]")}
|
|
1296
1298
|
add Add a provider (Groq, Mistral, Together AI, \u2026) ${pc4.dim("[Phase 1.1]")}
|
|
1297
1299
|
import Optional one-time import from OpenCode CLI ${pc4.dim("[Phase 1.0]")}
|
|
1298
|
-
auth Sign in with OAuth (GitHub Copilot, xAI, OpenAI, ClinePass)
|
|
1300
|
+
auth Sign in with OAuth (Antigravity, GitHub Copilot, xAI, OpenAI, ClinePass)
|
|
1299
1301
|
list Show configured providers ${pc4.dim("[Phase 1.0]")}
|
|
1300
1302
|
remove Remove a provider by id ${pc4.dim("[Phase 1.1]")}
|
|
1301
1303
|
refresh-models Update cached model lists ${pc4.dim("[Phase 1.2]")}`;
|
|
@@ -2233,7 +2235,7 @@ async function runProvidersCommand(args) {
|
|
|
2233
2235
|
// src/codex.ts
|
|
2234
2236
|
import pc7 from "picocolors";
|
|
2235
2237
|
import * as p8 from "@clack/prompts";
|
|
2236
|
-
import { join as
|
|
2238
|
+
import { join as join6 } from "path";
|
|
2237
2239
|
|
|
2238
2240
|
// src/codex-proxy.ts
|
|
2239
2241
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -3770,6 +3772,53 @@ async function resolveRoutedCollaborationInput(input, context) {
|
|
|
3770
3772
|
return normalizePlaintextCollaborationForExternal(out);
|
|
3771
3773
|
}
|
|
3772
3774
|
|
|
3775
|
+
// src/codex/route-audit.ts
|
|
3776
|
+
import { chmodSync, mkdirSync, writeFileSync } from "fs";
|
|
3777
|
+
import { join as join2 } from "path";
|
|
3778
|
+
var DIR_MODE = 448;
|
|
3779
|
+
var FILE_MODE = 384;
|
|
3780
|
+
var CODEX_ROUTE_AUDIT_LOG = "codex-route-audit.jsonl";
|
|
3781
|
+
function safeIdentifier(value) {
|
|
3782
|
+
if (value === void 0) return void 0;
|
|
3783
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, "_").slice(0, 300);
|
|
3784
|
+
}
|
|
3785
|
+
function sanitizeCodexRouteAuditEvent(event) {
|
|
3786
|
+
return {
|
|
3787
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3788
|
+
transport: event.transport,
|
|
3789
|
+
requestedModel: safeIdentifier(event.requestedModel),
|
|
3790
|
+
dispatch: event.dispatch,
|
|
3791
|
+
phase: event.phase,
|
|
3792
|
+
...event.provider ? { provider: safeIdentifier(event.provider) } : {},
|
|
3793
|
+
...event.routeModel ? { routeModel: safeIdentifier(event.routeModel) } : {},
|
|
3794
|
+
...event.upstreamModel ? { upstreamModel: safeIdentifier(event.upstreamModel) } : {},
|
|
3795
|
+
...event.outcome ? { outcome: event.outcome } : {},
|
|
3796
|
+
...event.status !== void 0 ? { status: typeof event.status === "string" ? safeIdentifier(event.status) : event.status } : {}
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function getCodexRouteAuditLogPath() {
|
|
3800
|
+
const dir = getLogsPath();
|
|
3801
|
+
mkdirSync(dir, { recursive: true, mode: DIR_MODE });
|
|
3802
|
+
try {
|
|
3803
|
+
chmodSync(dir, DIR_MODE);
|
|
3804
|
+
} catch {
|
|
3805
|
+
}
|
|
3806
|
+
return join2(dir, CODEX_ROUTE_AUDIT_LOG);
|
|
3807
|
+
}
|
|
3808
|
+
function prepareCodexRouteAuditLog(path3 = getCodexRouteAuditLogPath()) {
|
|
3809
|
+
writeFileSync(path3, "", { mode: FILE_MODE });
|
|
3810
|
+
chmodSync(path3, FILE_MODE);
|
|
3811
|
+
return path3;
|
|
3812
|
+
}
|
|
3813
|
+
function appendCodexRouteAudit(path3, event) {
|
|
3814
|
+
try {
|
|
3815
|
+
writeFileSync(path3, `${JSON.stringify(sanitizeCodexRouteAuditEvent(event))}
|
|
3816
|
+
`, { flag: "a", mode: FILE_MODE });
|
|
3817
|
+
chmodSync(path3, FILE_MODE);
|
|
3818
|
+
} catch {
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3773
3822
|
// src/codex-proxy.ts
|
|
3774
3823
|
function captureCompletedResponse(sseText) {
|
|
3775
3824
|
if (!sseText.includes("response.completed")) return void 0;
|
|
@@ -3777,11 +3826,34 @@ function captureCompletedResponse(sseText) {
|
|
|
3777
3826
|
if (!dataLine) return void 0;
|
|
3778
3827
|
try {
|
|
3779
3828
|
const obj = JSON.parse(dataLine.slice(5).trim());
|
|
3780
|
-
if (obj && obj.type === "response.completed"
|
|
3829
|
+
if (obj && obj.type === "response.completed" && obj.response && typeof obj.response === "object") {
|
|
3830
|
+
return obj.response;
|
|
3831
|
+
}
|
|
3781
3832
|
} catch {
|
|
3782
3833
|
}
|
|
3783
3834
|
return void 0;
|
|
3784
3835
|
}
|
|
3836
|
+
var MAX_EXTERNAL_RESPONSE_STATES = 8;
|
|
3837
|
+
var EXTERNAL_TOOL_OUTPUT_TYPES = /* @__PURE__ */ new Set([
|
|
3838
|
+
"function_call_output",
|
|
3839
|
+
"custom_tool_call_output",
|
|
3840
|
+
"tool_search_output"
|
|
3841
|
+
]);
|
|
3842
|
+
function responsesInputItems(input) {
|
|
3843
|
+
if (Array.isArray(input)) return input;
|
|
3844
|
+
if (typeof input === "string") {
|
|
3845
|
+
return [{ type: "message", role: "user", content: input }];
|
|
3846
|
+
}
|
|
3847
|
+
return [];
|
|
3848
|
+
}
|
|
3849
|
+
function isExternalToolOutputItem(item) {
|
|
3850
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
3851
|
+
const type = item.type;
|
|
3852
|
+
return typeof type === "string" && EXTERNAL_TOOL_OUTPUT_TYPES.has(type);
|
|
3853
|
+
}
|
|
3854
|
+
function isExternalToolContinuation(input) {
|
|
3855
|
+
return Array.isArray(input) && input.length > 0 && input.every(isExternalToolOutputItem);
|
|
3856
|
+
}
|
|
3785
3857
|
function estimateCodexRequestChars(params) {
|
|
3786
3858
|
let chars = (params.system ?? "").length;
|
|
3787
3859
|
for (const msg of params.messages) {
|
|
@@ -3980,11 +4052,32 @@ async function prepareExternalCodexBody(body, context) {
|
|
|
3980
4052
|
);
|
|
3981
4053
|
return { ...externalBody, input: resolvedInput };
|
|
3982
4054
|
}
|
|
4055
|
+
function applyExternalCodexRuntimeIdentity(params, route) {
|
|
4056
|
+
const selectedModel = route.auditUpstreamModelId ?? route.upstreamModelId ?? route.modelId;
|
|
4057
|
+
const provider = route.providerId ?? "relay";
|
|
4058
|
+
const identity = [
|
|
4059
|
+
"<external-model-identity>",
|
|
4060
|
+
`The selected model for this turn is ${JSON.stringify(selectedModel)} through provider ${JSON.stringify(provider)}.`,
|
|
4061
|
+
"Codex is the host application and agent environment, not the model identity.",
|
|
4062
|
+
"Follow Codex host and tool instructions normally, but do not infer that you are an OpenAI or GPT model from host names, tool names, documentation, or conversation context.",
|
|
4063
|
+
"If asked what model you are, report the selected model and provider above; do not use self-identification as evidence of the network route.",
|
|
4064
|
+
"</external-model-identity>"
|
|
4065
|
+
].join("\n");
|
|
4066
|
+
return {
|
|
4067
|
+
...params,
|
|
4068
|
+
system: params.system?.trim() ? `${identity}
|
|
4069
|
+
|
|
4070
|
+
${params.system}` : identity
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
3983
4073
|
async function startCodexProxy(routes, options = {}) {
|
|
3984
4074
|
const opts = typeof options === "boolean" ? { debug: options } : options;
|
|
3985
4075
|
const debug = opts.debug ?? false;
|
|
3986
4076
|
const requireAuth = opts.requireAuth ?? true;
|
|
3987
4077
|
const mixedNative = opts.mixedNative;
|
|
4078
|
+
const audit = (event) => {
|
|
4079
|
+
if (opts.routeAuditPath) appendCodexRouteAudit(opts.routeAuditPath, event);
|
|
4080
|
+
};
|
|
3988
4081
|
const nativePayloadRelay = mixedNative ? createNativePayloadRelay({}) : void 0;
|
|
3989
4082
|
silenceSdkWarnings();
|
|
3990
4083
|
const models = /* @__PURE__ */ new Map();
|
|
@@ -4158,6 +4251,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4158
4251
|
log14(`subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
|
|
4159
4252
|
}
|
|
4160
4253
|
if (mixedNative && markedSubagent && !subagentRoute) {
|
|
4254
|
+
audit({ transport: "http", requestedModel: modelId, dispatch: "relay-subagent", phase: "complete", outcome: "error", status: 503 });
|
|
4161
4255
|
sendJson(res, 503, {
|
|
4162
4256
|
error: {
|
|
4163
4257
|
message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
|
|
@@ -4170,10 +4264,20 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4170
4264
|
if (!markedSubagent) {
|
|
4171
4265
|
const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
|
|
4172
4266
|
if (dispatch.kind === "unknown") {
|
|
4267
|
+
audit({ transport: "http", requestedModel: modelId, dispatch: "unknown", phase: "complete", outcome: "error", status: 404 });
|
|
4173
4268
|
sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
|
|
4174
4269
|
return;
|
|
4175
4270
|
}
|
|
4176
4271
|
if (dispatch.kind === "native") {
|
|
4272
|
+
audit({
|
|
4273
|
+
transport: "http",
|
|
4274
|
+
requestedModel: modelId,
|
|
4275
|
+
dispatch: "native",
|
|
4276
|
+
phase: "dispatch",
|
|
4277
|
+
provider: "openai-native",
|
|
4278
|
+
routeModel: modelId,
|
|
4279
|
+
upstreamModel: modelId
|
|
4280
|
+
});
|
|
4177
4281
|
const controller = new AbortController();
|
|
4178
4282
|
req.once("aborted", () => controller.abort());
|
|
4179
4283
|
try {
|
|
@@ -4187,7 +4291,29 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4187
4291
|
const contentType = nativeResponse.headers.get("content-type");
|
|
4188
4292
|
res.writeHead(nativeResponse.status, contentType ? { "content-type": contentType } : void 0);
|
|
4189
4293
|
res.end(Buffer.from(await nativeResponse.arrayBuffer()));
|
|
4294
|
+
audit({
|
|
4295
|
+
transport: "http",
|
|
4296
|
+
requestedModel: modelId,
|
|
4297
|
+
dispatch: "native",
|
|
4298
|
+
phase: "complete",
|
|
4299
|
+
provider: "openai-native",
|
|
4300
|
+
routeModel: modelId,
|
|
4301
|
+
upstreamModel: modelId,
|
|
4302
|
+
outcome: nativeResponse.ok ? "ok" : "error",
|
|
4303
|
+
status: nativeResponse.status
|
|
4304
|
+
});
|
|
4190
4305
|
} catch (err) {
|
|
4306
|
+
audit({
|
|
4307
|
+
transport: "http",
|
|
4308
|
+
requestedModel: modelId,
|
|
4309
|
+
dispatch: "native",
|
|
4310
|
+
phase: "complete",
|
|
4311
|
+
provider: "openai-native",
|
|
4312
|
+
routeModel: modelId,
|
|
4313
|
+
upstreamModel: modelId,
|
|
4314
|
+
outcome: "error",
|
|
4315
|
+
status: "forward-failed"
|
|
4316
|
+
});
|
|
4191
4317
|
if (!res.writableEnded) sendJson(res, 502, { error: { message: "Native Codex request failed", type: "upstream_error" } });
|
|
4192
4318
|
}
|
|
4193
4319
|
return;
|
|
@@ -4212,13 +4338,23 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4212
4338
|
}
|
|
4213
4339
|
}
|
|
4214
4340
|
const { route, languageModel } = resolved;
|
|
4341
|
+
const relayDispatch = markedSubagent ? "relay-subagent" : "relay";
|
|
4342
|
+
audit({
|
|
4343
|
+
transport: "http",
|
|
4344
|
+
requestedModel: modelId,
|
|
4345
|
+
dispatch: relayDispatch,
|
|
4346
|
+
phase: "dispatch",
|
|
4347
|
+
provider: route.providerId ?? "relay",
|
|
4348
|
+
routeModel: route.modelId,
|
|
4349
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId
|
|
4350
|
+
});
|
|
4215
4351
|
try {
|
|
4216
4352
|
const routedBody = await prepareExternalCodexBody(body, {
|
|
4217
4353
|
relay: nativePayloadRelay,
|
|
4218
4354
|
mixedNative,
|
|
4219
4355
|
headers: req.headers
|
|
4220
4356
|
});
|
|
4221
|
-
let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
|
|
4357
|
+
let params = applyClaudeCodeOAuthIdentity(route, applyExternalCodexRuntimeIdentity(translateResponsesRequest(
|
|
4222
4358
|
routedBody,
|
|
4223
4359
|
route.npm,
|
|
4224
4360
|
{
|
|
@@ -4230,7 +4366,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4230
4366
|
upstreamModelId: route.upstreamModelId
|
|
4231
4367
|
},
|
|
4232
4368
|
{ maxTools: maxToolsForNpm(route.npm) }
|
|
4233
|
-
));
|
|
4369
|
+
), route));
|
|
4234
4370
|
if (route.contextWindow && route.contextWindow > 0) {
|
|
4235
4371
|
const before = params.messages.length;
|
|
4236
4372
|
const estimatedChars = estimateCodexRequestChars(params);
|
|
@@ -4285,9 +4421,31 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4285
4421
|
log14(`response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
|
|
4286
4422
|
}
|
|
4287
4423
|
});
|
|
4424
|
+
audit({
|
|
4425
|
+
transport: "http",
|
|
4426
|
+
requestedModel: modelId,
|
|
4427
|
+
dispatch: relayDispatch,
|
|
4428
|
+
phase: "complete",
|
|
4429
|
+
provider: route.providerId ?? "relay",
|
|
4430
|
+
routeModel: route.modelId,
|
|
4431
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
|
|
4432
|
+
outcome: "ok",
|
|
4433
|
+
status: 200
|
|
4434
|
+
});
|
|
4288
4435
|
} catch (err) {
|
|
4289
4436
|
const msg = formatUpstreamError(err);
|
|
4290
4437
|
const status = upstreamHttpStatus(err, msg);
|
|
4438
|
+
audit({
|
|
4439
|
+
transport: "http",
|
|
4440
|
+
requestedModel: modelId,
|
|
4441
|
+
dispatch: relayDispatch,
|
|
4442
|
+
phase: "complete",
|
|
4443
|
+
provider: route.providerId ?? "relay",
|
|
4444
|
+
routeModel: route.modelId,
|
|
4445
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
|
|
4446
|
+
outcome: "error",
|
|
4447
|
+
status
|
|
4448
|
+
});
|
|
4291
4449
|
if (debug) log14(`sdk error: ${route.modelId}: ${msg}`);
|
|
4292
4450
|
if (status === 429) {
|
|
4293
4451
|
writeResponsesRateLimitStream(modelId, msg, write);
|
|
@@ -4309,9 +4467,31 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
4309
4467
|
});
|
|
4310
4468
|
}
|
|
4311
4469
|
sendJson(res, 200, response);
|
|
4470
|
+
audit({
|
|
4471
|
+
transport: "http",
|
|
4472
|
+
requestedModel: modelId,
|
|
4473
|
+
dispatch: relayDispatch,
|
|
4474
|
+
phase: "complete",
|
|
4475
|
+
provider: route.providerId ?? "relay",
|
|
4476
|
+
routeModel: route.modelId,
|
|
4477
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
|
|
4478
|
+
outcome: "ok",
|
|
4479
|
+
status: 200
|
|
4480
|
+
});
|
|
4312
4481
|
} catch (err) {
|
|
4313
4482
|
const msg = formatUpstreamError(err);
|
|
4314
4483
|
const status = upstreamHttpStatus(err, msg);
|
|
4484
|
+
audit({
|
|
4485
|
+
transport: "http",
|
|
4486
|
+
requestedModel: modelId,
|
|
4487
|
+
dispatch: relayDispatch,
|
|
4488
|
+
phase: "complete",
|
|
4489
|
+
provider: route.providerId ?? "relay",
|
|
4490
|
+
routeModel: route.modelId,
|
|
4491
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
|
|
4492
|
+
outcome: "error",
|
|
4493
|
+
status
|
|
4494
|
+
});
|
|
4315
4495
|
if (debug) log14(`sdk error: ${route.modelId}: ${msg}`);
|
|
4316
4496
|
if (status === 429) {
|
|
4317
4497
|
sendJson(res, 200, responsesRateLimitBody(modelId, msg));
|
|
@@ -4436,21 +4616,53 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
4436
4616
|
`
|
|
4437
4617
|
);
|
|
4438
4618
|
let frameBuf = Buffer.alloc(0);
|
|
4439
|
-
let
|
|
4619
|
+
let externalActive = false;
|
|
4440
4620
|
let nativeActive = false;
|
|
4441
4621
|
let nativeUpstream;
|
|
4622
|
+
let nativeSendTurn;
|
|
4623
|
+
let socketClosing = false;
|
|
4624
|
+
const externalResponseStates = /* @__PURE__ */ new Map();
|
|
4625
|
+
let currentExternalCompletedResponse;
|
|
4626
|
+
let currentExternalStateInput;
|
|
4627
|
+
let currentExternalConsumedResponseId;
|
|
4442
4628
|
let currentRequestModel = "";
|
|
4443
|
-
const
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4629
|
+
const rememberExternalResponse = (response, input) => {
|
|
4630
|
+
const responseId = typeof response.id === "string" ? response.id : void 0;
|
|
4631
|
+
const output = Array.isArray(response.output) ? response.output : void 0;
|
|
4632
|
+
if (!responseId || !output || response.error) return;
|
|
4633
|
+
externalResponseStates.delete(responseId);
|
|
4634
|
+
externalResponseStates.set(responseId, { input: [...input], output: [...output] });
|
|
4635
|
+
while (externalResponseStates.size > MAX_EXTERNAL_RESPONSE_STATES) {
|
|
4636
|
+
const oldest = externalResponseStates.keys().next().value;
|
|
4637
|
+
if (!oldest) break;
|
|
4638
|
+
externalResponseStates.delete(oldest);
|
|
4447
4639
|
}
|
|
4448
4640
|
};
|
|
4641
|
+
const resolveExternalContinuation = (body) => {
|
|
4642
|
+
const previousResponseId = typeof body.previous_response_id === "string" ? body.previous_response_id : void 0;
|
|
4643
|
+
if (!previousResponseId || !isExternalToolContinuation(body.input)) return { body };
|
|
4644
|
+
const previous = externalResponseStates.get(previousResponseId);
|
|
4645
|
+
if (!previous) return { body, orphanedResponseId: previousResponseId };
|
|
4646
|
+
return {
|
|
4647
|
+
body: {
|
|
4648
|
+
...body,
|
|
4649
|
+
input: [...previous.input, ...previous.output, ...body.input]
|
|
4650
|
+
},
|
|
4651
|
+
consumedResponseId: previousResponseId
|
|
4652
|
+
};
|
|
4653
|
+
};
|
|
4654
|
+
const closeSocket = (code = 1e3) => {
|
|
4655
|
+
if (socketClosing || socket.destroyed) return;
|
|
4656
|
+
socketClosing = true;
|
|
4657
|
+
socket.write(wsCloseFrame(code));
|
|
4658
|
+
socket.end();
|
|
4659
|
+
};
|
|
4449
4660
|
const sendWsEvent = (sseChunk2) => {
|
|
4450
|
-
if (socket.destroyed) return;
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4661
|
+
if (socketClosing || socket.destroyed) return;
|
|
4662
|
+
const completed = captureCompletedResponse(sseChunk2);
|
|
4663
|
+
if (completed) {
|
|
4664
|
+
currentExternalCompletedResponse = completed;
|
|
4665
|
+
if (debug) {
|
|
4454
4666
|
appendCodexBodyDump({
|
|
4455
4667
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4456
4668
|
transport: "ws",
|
|
@@ -4468,7 +4680,6 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
4468
4680
|
};
|
|
4469
4681
|
const onData = (chunk) => {
|
|
4470
4682
|
frameBuf = Buffer.concat([frameBuf, chunk]);
|
|
4471
|
-
if (handled && !nativeActive) return;
|
|
4472
4683
|
const frame = wsDecodeFrame(frameBuf);
|
|
4473
4684
|
if (!frame) return;
|
|
4474
4685
|
frameBuf = Buffer.alloc(0);
|
|
@@ -4490,7 +4701,10 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
4490
4701
|
socket.end();
|
|
4491
4702
|
return;
|
|
4492
4703
|
}
|
|
4493
|
-
|
|
4704
|
+
if (externalActive) {
|
|
4705
|
+
closeSocket(1008);
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4494
4708
|
void (async () => {
|
|
4495
4709
|
let body;
|
|
4496
4710
|
try {
|
|
@@ -4531,6 +4745,7 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
|
|
|
4531
4745
|
log14(`WS subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
|
|
4532
4746
|
}
|
|
4533
4747
|
if (mixedNative && markedSubagent && !subagentRoute) {
|
|
4748
|
+
audit({ transport: "ws", requestedModel: modelId, dispatch: "relay-subagent", phase: "complete", outcome: "error", status: 503 });
|
|
4534
4749
|
sendWsEvent(`event: error
|
|
4535
4750
|
data: ${JSON.stringify({ error: {
|
|
4536
4751
|
message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
|
|
@@ -4545,6 +4760,7 @@ data: ${JSON.stringify({ error: {
|
|
|
4545
4760
|
if (!markedSubagent) {
|
|
4546
4761
|
const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
|
|
4547
4762
|
if (dispatch.kind === "unknown") {
|
|
4763
|
+
audit({ transport: "ws", requestedModel: modelId, dispatch: "unknown", phase: "complete", outcome: "error", status: 404 });
|
|
4548
4764
|
sendWsEvent(`event: error
|
|
4549
4765
|
data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } })}
|
|
4550
4766
|
|
|
@@ -4553,6 +4769,15 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4553
4769
|
return;
|
|
4554
4770
|
}
|
|
4555
4771
|
if (dispatch.kind === "native") {
|
|
4772
|
+
audit({
|
|
4773
|
+
transport: "ws",
|
|
4774
|
+
requestedModel: modelId,
|
|
4775
|
+
dispatch: "native",
|
|
4776
|
+
phase: "dispatch",
|
|
4777
|
+
provider: "openai-native",
|
|
4778
|
+
routeModel: modelId,
|
|
4779
|
+
upstreamModel: modelId
|
|
4780
|
+
});
|
|
4556
4781
|
const nativeBody = prepareNativeCodexBody(body);
|
|
4557
4782
|
if (debug && nativeBody !== body) {
|
|
4558
4783
|
log14(`WS native history normalized: model=${modelId} converted Relay compaction for native verification`);
|
|
@@ -4560,7 +4785,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4560
4785
|
if (nativeActive && nativeUpstream) {
|
|
4561
4786
|
if (nativeUpstream.readyState === WebSocket.OPEN) {
|
|
4562
4787
|
if (debug) log14(`WS native forwarding next turn: model=${modelId}`);
|
|
4563
|
-
|
|
4788
|
+
nativeSendTurn?.(nativeBody, modelId);
|
|
4564
4789
|
} else if (debug) {
|
|
4565
4790
|
log14(`WS native cannot forward next turn: upstream_state=${nativeUpstream.readyState}`);
|
|
4566
4791
|
}
|
|
@@ -4571,6 +4796,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4571
4796
|
let upstream;
|
|
4572
4797
|
let nativeOpened = false;
|
|
4573
4798
|
let nativeCompleted = false;
|
|
4799
|
+
let nativeTurnModelId = modelId;
|
|
4574
4800
|
let nativeFrameCount = 0;
|
|
4575
4801
|
let finished = false;
|
|
4576
4802
|
let connectTimer;
|
|
@@ -4590,10 +4816,24 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4590
4816
|
if (finished) return;
|
|
4591
4817
|
finished = true;
|
|
4592
4818
|
nativeActive = false;
|
|
4819
|
+
nativeSendTurn = void 0;
|
|
4593
4820
|
if (nativeUpstream === upstream) nativeUpstream = void 0;
|
|
4594
4821
|
clearTimers();
|
|
4595
4822
|
if (debug && message) {
|
|
4596
|
-
log14(`WS native upstream failed: model=${
|
|
4823
|
+
log14(`WS native upstream failed: model=${nativeTurnModelId} opened=${nativeOpened} frames=${nativeFrameCount} message=${message}`);
|
|
4824
|
+
}
|
|
4825
|
+
if (message && !nativeCompleted) {
|
|
4826
|
+
audit({
|
|
4827
|
+
transport: "ws",
|
|
4828
|
+
requestedModel: nativeTurnModelId,
|
|
4829
|
+
dispatch: "native",
|
|
4830
|
+
phase: "complete",
|
|
4831
|
+
provider: "openai-native",
|
|
4832
|
+
routeModel: nativeTurnModelId,
|
|
4833
|
+
upstreamModel: nativeTurnModelId,
|
|
4834
|
+
outcome: "error",
|
|
4835
|
+
status: "upstream-failed"
|
|
4836
|
+
});
|
|
4597
4837
|
}
|
|
4598
4838
|
if (message && !nativeCompleted) sendNativeError(message);
|
|
4599
4839
|
try {
|
|
@@ -4602,20 +4842,31 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4602
4842
|
}
|
|
4603
4843
|
closeSocket(closeCode);
|
|
4604
4844
|
};
|
|
4845
|
+
const sendNativeTurn = (turnBody, turnModelId) => {
|
|
4846
|
+
if (!upstream || upstream.readyState !== WebSocket.OPEN) {
|
|
4847
|
+
if (debug) log14(`WS native cannot send turn: model=${turnModelId} upstream_state=${upstream?.readyState ?? "missing"}`);
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
nativeTurnModelId = turnModelId;
|
|
4851
|
+
nativeCompleted = false;
|
|
4852
|
+
if (firstFrameTimer) clearTimeout(firstFrameTimer);
|
|
4853
|
+
upstream.send(JSON.stringify({ type: "response.create", ...turnBody }));
|
|
4854
|
+
firstFrameTimer = setTimeout(() => closeBoth("Native Codex WebSocket response timed out"), 6e4);
|
|
4855
|
+
};
|
|
4605
4856
|
try {
|
|
4606
4857
|
if (debug) {
|
|
4607
4858
|
log14(`WS native connecting: model=${modelId} url=${target.url} headers=[${Object.keys(target.headers).join(",")}]`);
|
|
4608
4859
|
}
|
|
4609
4860
|
upstream = new WebSocket(target.url, { headers: target.headers });
|
|
4610
4861
|
nativeUpstream = upstream;
|
|
4862
|
+
nativeSendTurn = sendNativeTurn;
|
|
4611
4863
|
nativeActive = true;
|
|
4612
4864
|
connectTimer = setTimeout(() => closeBoth("Native Codex WebSocket connection timed out"), 15e3);
|
|
4613
4865
|
upstream.once("open", () => {
|
|
4614
4866
|
nativeOpened = true;
|
|
4615
4867
|
if (connectTimer) clearTimeout(connectTimer);
|
|
4616
4868
|
if (debug) log14(`WS native upstream open: model=${modelId}`);
|
|
4617
|
-
|
|
4618
|
-
firstFrameTimer = setTimeout(() => closeBoth("Native Codex WebSocket response timed out"), 6e4);
|
|
4869
|
+
sendNativeTurn(nativeBody, modelId);
|
|
4619
4870
|
});
|
|
4620
4871
|
upstream.once("unexpected-response", (_request, response) => {
|
|
4621
4872
|
if (debug) log14(`WS native upstream HTTP rejection: model=${modelId} status=${response.statusCode}`);
|
|
@@ -4633,6 +4884,17 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4633
4884
|
if (typeof parsed.type === "string") eventType = parsed.type;
|
|
4634
4885
|
if (eventType === "response.completed" || eventType === "response.failed" || eventType === "response.incomplete") {
|
|
4635
4886
|
nativeCompleted = true;
|
|
4887
|
+
audit({
|
|
4888
|
+
transport: "ws",
|
|
4889
|
+
requestedModel: modelId,
|
|
4890
|
+
dispatch: "native",
|
|
4891
|
+
phase: "complete",
|
|
4892
|
+
provider: "openai-native",
|
|
4893
|
+
routeModel: modelId,
|
|
4894
|
+
upstreamModel: modelId,
|
|
4895
|
+
outcome: eventType === "response.completed" ? "ok" : "error",
|
|
4896
|
+
status: eventType
|
|
4897
|
+
});
|
|
4636
4898
|
}
|
|
4637
4899
|
} catch {
|
|
4638
4900
|
}
|
|
@@ -4653,6 +4915,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4653
4915
|
if (debug) log14(`WS native downstream close: model=${modelId} frames=${nativeFrameCount} completed=${nativeCompleted}`);
|
|
4654
4916
|
finished = true;
|
|
4655
4917
|
nativeActive = false;
|
|
4918
|
+
nativeSendTurn = void 0;
|
|
4656
4919
|
if (nativeUpstream === upstream) nativeUpstream = void 0;
|
|
4657
4920
|
clearTimers();
|
|
4658
4921
|
try {
|
|
@@ -4667,6 +4930,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
|
|
|
4667
4930
|
}
|
|
4668
4931
|
}
|
|
4669
4932
|
}
|
|
4933
|
+
externalActive = true;
|
|
4670
4934
|
let resolved = subagentRoute ? resolveModel(routes, models, subagentRoute.modelId) : resolveModel(routes, models, modelId);
|
|
4671
4935
|
if (!resolved) {
|
|
4672
4936
|
const fb = routes[0];
|
|
@@ -4685,13 +4949,35 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
4685
4949
|
}
|
|
4686
4950
|
}
|
|
4687
4951
|
const { route, languageModel } = resolved;
|
|
4952
|
+
const relayDispatch = markedSubagent ? "relay-subagent" : "relay";
|
|
4953
|
+
audit({
|
|
4954
|
+
transport: "ws",
|
|
4955
|
+
requestedModel: modelId,
|
|
4956
|
+
dispatch: relayDispatch,
|
|
4957
|
+
phase: "dispatch",
|
|
4958
|
+
provider: route.providerId ?? "relay",
|
|
4959
|
+
routeModel: route.modelId,
|
|
4960
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId
|
|
4961
|
+
});
|
|
4962
|
+
currentExternalCompletedResponse = void 0;
|
|
4963
|
+
currentExternalStateInput = void 0;
|
|
4964
|
+
currentExternalConsumedResponseId = void 0;
|
|
4965
|
+
const continuation = resolveExternalContinuation(body);
|
|
4966
|
+
if (continuation.orphanedResponseId) {
|
|
4967
|
+
if (debug) log14(`WS continuation rejected: unknown previous_response_id=${continuation.orphanedResponseId}`);
|
|
4968
|
+
writeResponsesErrorStream(modelId, "Unknown or expired previous_response_id", sendWsEvent, 400);
|
|
4969
|
+
externalActive = false;
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
4688
4972
|
try {
|
|
4689
|
-
const routedBody = await prepareExternalCodexBody(body, {
|
|
4973
|
+
const routedBody = await prepareExternalCodexBody(continuation.body, {
|
|
4690
4974
|
relay: nativePayloadRelay,
|
|
4691
4975
|
mixedNative,
|
|
4692
4976
|
headers: req.headers
|
|
4693
4977
|
});
|
|
4694
|
-
|
|
4978
|
+
currentExternalStateInput = responsesInputItems(routedBody.input);
|
|
4979
|
+
currentExternalConsumedResponseId = continuation.consumedResponseId;
|
|
4980
|
+
let params = applyClaudeCodeOAuthIdentity(route, applyExternalCodexRuntimeIdentity(translateResponsesRequest(
|
|
4695
4981
|
routedBody,
|
|
4696
4982
|
route.npm,
|
|
4697
4983
|
{
|
|
@@ -4703,7 +4989,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
4703
4989
|
upstreamModelId: route.upstreamModelId
|
|
4704
4990
|
},
|
|
4705
4991
|
{ maxTools: maxToolsForNpm(route.npm) }
|
|
4706
|
-
));
|
|
4992
|
+
), route));
|
|
4707
4993
|
if (route.contextWindow && route.contextWindow > 0) {
|
|
4708
4994
|
const before = params.messages.length;
|
|
4709
4995
|
const estimatedChars = estimateCodexRequestChars(params);
|
|
@@ -4736,9 +5022,37 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
4736
5022
|
log14(`WS response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
|
|
4737
5023
|
}
|
|
4738
5024
|
});
|
|
5025
|
+
if (currentExternalCompletedResponse && currentExternalStateInput) {
|
|
5026
|
+
if (currentExternalConsumedResponseId) {
|
|
5027
|
+
externalResponseStates.delete(currentExternalConsumedResponseId);
|
|
5028
|
+
}
|
|
5029
|
+
rememberExternalResponse(currentExternalCompletedResponse, currentExternalStateInput);
|
|
5030
|
+
}
|
|
5031
|
+
audit({
|
|
5032
|
+
transport: "ws",
|
|
5033
|
+
requestedModel: modelId,
|
|
5034
|
+
dispatch: relayDispatch,
|
|
5035
|
+
phase: "complete",
|
|
5036
|
+
provider: route.providerId ?? "relay",
|
|
5037
|
+
routeModel: route.modelId,
|
|
5038
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
|
|
5039
|
+
outcome: "ok",
|
|
5040
|
+
status: "response.completed"
|
|
5041
|
+
});
|
|
4739
5042
|
} catch (err) {
|
|
4740
5043
|
const msg = formatUpstreamError(err);
|
|
4741
5044
|
const status = upstreamHttpStatus(err, msg);
|
|
5045
|
+
audit({
|
|
5046
|
+
transport: "ws",
|
|
5047
|
+
requestedModel: modelId,
|
|
5048
|
+
dispatch: relayDispatch,
|
|
5049
|
+
phase: "complete",
|
|
5050
|
+
provider: route.providerId ?? "relay",
|
|
5051
|
+
routeModel: route.modelId,
|
|
5052
|
+
upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
|
|
5053
|
+
outcome: "error",
|
|
5054
|
+
status
|
|
5055
|
+
});
|
|
4742
5056
|
if (debug) log14(`WS sdk error: ${route.modelId}: ${msg}`);
|
|
4743
5057
|
if (status === 429) {
|
|
4744
5058
|
writeResponsesRateLimitStream(modelId, msg, sendWsEvent);
|
|
@@ -4746,10 +5060,11 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
4746
5060
|
writeResponsesErrorStream(modelId, msg, sendWsEvent, status);
|
|
4747
5061
|
}
|
|
4748
5062
|
}
|
|
4749
|
-
|
|
5063
|
+
externalActive = false;
|
|
4750
5064
|
})();
|
|
4751
5065
|
};
|
|
4752
5066
|
socket.on("error", () => socket.destroy());
|
|
5067
|
+
socket.once("close", () => externalResponseStates.clear());
|
|
4753
5068
|
socket.on("data", onData);
|
|
4754
5069
|
onData(head);
|
|
4755
5070
|
});
|
|
@@ -4774,44 +5089,44 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
4774
5089
|
}
|
|
4775
5090
|
|
|
4776
5091
|
// src/codex/profile.ts
|
|
4777
|
-
import { join as
|
|
5092
|
+
import { join as join4 } from "path";
|
|
4778
5093
|
|
|
4779
5094
|
// src/codex/session.ts
|
|
4780
5095
|
import {
|
|
4781
5096
|
copyFileSync,
|
|
4782
|
-
chmodSync,
|
|
5097
|
+
chmodSync as chmodSync2,
|
|
4783
5098
|
existsSync as existsSync3,
|
|
4784
|
-
mkdirSync,
|
|
5099
|
+
mkdirSync as mkdirSync2,
|
|
4785
5100
|
readdirSync,
|
|
4786
5101
|
readFileSync as readFileSync2,
|
|
4787
5102
|
renameSync,
|
|
4788
5103
|
rmSync,
|
|
4789
5104
|
statSync,
|
|
4790
5105
|
unlinkSync,
|
|
4791
|
-
writeFileSync
|
|
5106
|
+
writeFileSync as writeFileSync2
|
|
4792
5107
|
} from "fs";
|
|
4793
5108
|
import { homedir as homedir3 } from "os";
|
|
4794
|
-
import { basename, dirname, join as
|
|
5109
|
+
import { basename, dirname, join as join3 } from "path";
|
|
4795
5110
|
var CODEX_PROFILE_NAME = "relay-ai-launch";
|
|
4796
5111
|
var STALE_SESSION_MS = 5 * 60 * 1e3;
|
|
4797
5112
|
var MAX_BACKUPS = 5;
|
|
4798
5113
|
function getCodexHome(env = process.env) {
|
|
4799
|
-
return env["CODEX_HOME"] ||
|
|
5114
|
+
return env["CODEX_HOME"] || join3(homedir3(), ".codex");
|
|
4800
5115
|
}
|
|
4801
5116
|
function getCodexProfilePath() {
|
|
4802
|
-
return
|
|
5117
|
+
return join3(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
|
|
4803
5118
|
}
|
|
4804
5119
|
function getRelayAiCodexDir(env = process.env) {
|
|
4805
|
-
return
|
|
5120
|
+
return join3(getAppHome(env), "codex");
|
|
4806
5121
|
}
|
|
4807
5122
|
function getSessionLockPath(env = process.env) {
|
|
4808
|
-
return
|
|
5123
|
+
return join3(getRelayAiCodexDir(env), "session.json");
|
|
4809
5124
|
}
|
|
4810
5125
|
function getBackupsDir(env = process.env) {
|
|
4811
|
-
return
|
|
5126
|
+
return join3(getRelayAiCodexDir(env), "backups");
|
|
4812
5127
|
}
|
|
4813
5128
|
function getCatalogPath(providerId, env = process.env) {
|
|
4814
|
-
return
|
|
5129
|
+
return join3(getRelayAiCodexDir(env), `models-${providerId}.json`);
|
|
4815
5130
|
}
|
|
4816
5131
|
function ownedOverlayPaths(env = process.env) {
|
|
4817
5132
|
const paths = [getCodexProfilePath()];
|
|
@@ -4819,15 +5134,15 @@ function ownedOverlayPaths(env = process.env) {
|
|
|
4819
5134
|
if (existsSync3(codexDir)) {
|
|
4820
5135
|
for (const name of readdirSync(codexDir)) {
|
|
4821
5136
|
if (name.startsWith("models-") && name.endsWith(".json")) {
|
|
4822
|
-
paths.push(
|
|
5137
|
+
paths.push(join3(codexDir, name));
|
|
4823
5138
|
}
|
|
4824
5139
|
}
|
|
4825
5140
|
}
|
|
4826
|
-
const agentsDir =
|
|
5141
|
+
const agentsDir = join3(getCodexHome(env), "agents");
|
|
4827
5142
|
if (existsSync3(agentsDir)) {
|
|
4828
5143
|
for (const name of readdirSync(agentsDir)) {
|
|
4829
5144
|
if (/^relay-model-[a-z0-9-]+\.toml$/i.test(name)) {
|
|
4830
|
-
paths.push(
|
|
5145
|
+
paths.push(join3(agentsDir, name));
|
|
4831
5146
|
}
|
|
4832
5147
|
}
|
|
4833
5148
|
}
|
|
@@ -4835,27 +5150,27 @@ function ownedOverlayPaths(env = process.env) {
|
|
|
4835
5150
|
return paths;
|
|
4836
5151
|
}
|
|
4837
5152
|
function atomicWriteFile(path3, content) {
|
|
4838
|
-
|
|
5153
|
+
mkdirSync2(dirname(path3), { recursive: true });
|
|
4839
5154
|
const tmp = `${path3}.tmp.${process.pid}`;
|
|
4840
|
-
|
|
5155
|
+
writeFileSync2(tmp, content, { encoding: "utf8", mode: 384 });
|
|
4841
5156
|
renameSync(tmp, path3);
|
|
4842
5157
|
try {
|
|
4843
|
-
|
|
5158
|
+
chmodSync2(path3, 384);
|
|
4844
5159
|
} catch {
|
|
4845
5160
|
}
|
|
4846
5161
|
}
|
|
4847
5162
|
function rotateBackups(filePath, env = process.env) {
|
|
4848
5163
|
if (!existsSync3(filePath)) return;
|
|
4849
5164
|
const backupsDir = getBackupsDir(env);
|
|
4850
|
-
|
|
5165
|
+
mkdirSync2(backupsDir, { recursive: true });
|
|
4851
5166
|
const base = basename(filePath);
|
|
4852
5167
|
const stamp = Date.now();
|
|
4853
|
-
const backupPath =
|
|
5168
|
+
const backupPath = join3(backupsDir, `${base}.${stamp}.bak`);
|
|
4854
5169
|
copyFileSync(filePath, backupPath);
|
|
4855
|
-
const backups = readdirSync(backupsDir).filter((n) => n.startsWith(`${base}.`) && n.endsWith(".bak")).map((n) => ({ name: n, mtime: statSync(
|
|
5170
|
+
const backups = readdirSync(backupsDir).filter((n) => n.startsWith(`${base}.`) && n.endsWith(".bak")).map((n) => ({ name: n, mtime: statSync(join3(backupsDir, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
4856
5171
|
for (const old of backups.slice(MAX_BACKUPS)) {
|
|
4857
5172
|
try {
|
|
4858
|
-
unlinkSync(
|
|
5173
|
+
unlinkSync(join3(backupsDir, old.name));
|
|
4859
5174
|
} catch {
|
|
4860
5175
|
}
|
|
4861
5176
|
}
|
|
@@ -4876,7 +5191,7 @@ function readSessionLock(env = process.env) {
|
|
|
4876
5191
|
}
|
|
4877
5192
|
function writeSessionLock(lock, env = process.env) {
|
|
4878
5193
|
const path3 = getSessionLockPath(env);
|
|
4879
|
-
|
|
5194
|
+
mkdirSync2(getRelayAiCodexDir(env), { recursive: true });
|
|
4880
5195
|
atomicWriteFile(path3, `${JSON.stringify(lock, null, 2)}
|
|
4881
5196
|
`);
|
|
4882
5197
|
}
|
|
@@ -4999,10 +5314,10 @@ function getCatalogOutputPath(providerId) {
|
|
|
4999
5314
|
return getCatalogPath(providerId);
|
|
5000
5315
|
}
|
|
5001
5316
|
function getFavoritesCatalogPath() {
|
|
5002
|
-
return
|
|
5317
|
+
return join4(getRelayAiCodexDir(), "models-favorites.json");
|
|
5003
5318
|
}
|
|
5004
5319
|
function getFavoritesAppCatalogPath() {
|
|
5005
|
-
return
|
|
5320
|
+
return join4(getRelayAiCodexDir(), "app-models-favorites.json");
|
|
5006
5321
|
}
|
|
5007
5322
|
function profileName() {
|
|
5008
5323
|
return CODEX_PROFILE_NAME;
|
|
@@ -5013,7 +5328,7 @@ import { execSync as execSync2 } from "child_process";
|
|
|
5013
5328
|
import spawn2 from "cross-spawn";
|
|
5014
5329
|
import { existsSync as existsSync4 } from "fs";
|
|
5015
5330
|
import { homedir as homedir4 } from "os";
|
|
5016
|
-
import { join as
|
|
5331
|
+
import { join as join5 } from "path";
|
|
5017
5332
|
var isWindows2 = process.platform === "win32";
|
|
5018
5333
|
var CODEX_CI_ENV_VARS = [
|
|
5019
5334
|
"CI",
|
|
@@ -5034,11 +5349,11 @@ function stripCodexInheritedEnv(env) {
|
|
|
5034
5349
|
return out;
|
|
5035
5350
|
}
|
|
5036
5351
|
var CODEX_FALLBACK_PATHS = isWindows2 ? [
|
|
5037
|
-
|
|
5038
|
-
|
|
5352
|
+
join5(process.env["APPDATA"] ?? homedir4(), "npm", "codex.cmd"),
|
|
5353
|
+
join5(process.env["APPDATA"] ?? homedir4(), "npm", "codex")
|
|
5039
5354
|
] : [
|
|
5040
|
-
|
|
5041
|
-
|
|
5355
|
+
join5(homedir4(), ".local", "bin", "codex"),
|
|
5356
|
+
join5(homedir4(), ".npm", "bin", "codex"),
|
|
5042
5357
|
"/usr/local/bin/codex",
|
|
5043
5358
|
"/opt/homebrew/bin/codex"
|
|
5044
5359
|
];
|
|
@@ -5260,7 +5575,7 @@ function printCodexAppSessionPanel(opts) {
|
|
|
5260
5575
|
`${pc6.bold("Provider")} ${fmtProvider(opts.providerName)}`,
|
|
5261
5576
|
"",
|
|
5262
5577
|
`${pc6.yellow(pc6.bold("Keep this terminal open"))}${pc6.white(" while you use Codex.")}`,
|
|
5263
|
-
`${pc6.white("Press ")}${pc6.bold(pc6.red("Ctrl+C"))}${pc6.white(" to
|
|
5578
|
+
`${pc6.white("Press ")}${pc6.bold(pc6.red("Ctrl+C"))}${pc6.white(" to close ChatGPT Desktop, restore ")}${fmtCommand("~/.codex/config.toml")}${pc6.white(", and stop the proxy.")}`,
|
|
5264
5579
|
`${pc6.dim("Codex may show ")}${pc6.yellow('"Custom"')}${pc6.dim(" if the desktop picker cannot resolve registry models \u2014 check the terminal line above. After restart, pick your model from the picker if it appears.")}`,
|
|
5265
5580
|
`${pc6.dim("If Codex asks you to sign in after restart: choose API key and enter any character \u2014 that unlocks the model picker for registry providers.")}`,
|
|
5266
5581
|
`${pc6.dim("Stuck? Run ")}${fmtCommand(opts.restoreCommand)}${pc6.dim(".")}`
|
|
@@ -5541,7 +5856,8 @@ async function resolveCodexMixedModels(input) {
|
|
|
5541
5856
|
subagents: subagentResult.resolved,
|
|
5542
5857
|
all,
|
|
5543
5858
|
providersById: new Map(input.compatible.map((provider) => [provider.id, provider])),
|
|
5544
|
-
dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites]
|
|
5859
|
+
dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites],
|
|
5860
|
+
capacitySkipped: [...visibleResult.capacitySkippedFavorites, ...subagentResult.capacitySkippedFavorites]
|
|
5545
5861
|
};
|
|
5546
5862
|
}
|
|
5547
5863
|
|
|
@@ -5741,6 +6057,7 @@ async function prepareCodexMixedRelayRoutes(models, trace = false) {
|
|
|
5741
6057
|
apiKey: backend.token,
|
|
5742
6058
|
baseURL: `http://127.0.0.1:${backend.port}`,
|
|
5743
6059
|
upstreamModelId: proxyRoute.aliasId,
|
|
6060
|
+
auditUpstreamModelId: original.model.upstreamModelId || original.model.id,
|
|
5744
6061
|
providerId: original.providerId,
|
|
5745
6062
|
authType: "oauth",
|
|
5746
6063
|
oauthAccountId: original.oauthAccountId,
|
|
@@ -6071,7 +6388,7 @@ async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
|
|
|
6071
6388
|
return { profilePath, catalogPath };
|
|
6072
6389
|
}
|
|
6073
6390
|
async function writeMixedLaunchArtifacts(plan, proxyPort) {
|
|
6074
|
-
const catalogPath =
|
|
6391
|
+
const catalogPath = join6(getRelayAiCodexDir(), "models-mixed.json");
|
|
6075
6392
|
writeOverlayFile(catalogPath, serializeCatalog(plan.catalog));
|
|
6076
6393
|
const profilePath = getProfileOutputPath();
|
|
6077
6394
|
writeOverlayFile(profilePath, buildCodexMixedProfileToml({
|
|
@@ -6656,17 +6973,17 @@ import * as p10 from "@clack/prompts";
|
|
|
6656
6973
|
|
|
6657
6974
|
// src/gemini/launch.ts
|
|
6658
6975
|
import { spawn as spawn3 } from "child_process";
|
|
6659
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
6976
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
6660
6977
|
import { homedir as homedir5, tmpdir } from "os";
|
|
6661
|
-
import { join as
|
|
6978
|
+
import { join as join7 } from "path";
|
|
6662
6979
|
var isWindows3 = process.platform === "win32";
|
|
6663
6980
|
var GEMINI_API_KEY_AUTH_TYPE = "gemini-api-key";
|
|
6664
6981
|
var GEMINI_FALLBACK_PATHS = isWindows3 ? [
|
|
6665
|
-
|
|
6666
|
-
|
|
6982
|
+
join7(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
|
|
6983
|
+
join7(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
|
|
6667
6984
|
] : [
|
|
6668
|
-
|
|
6669
|
-
|
|
6985
|
+
join7(homedir5(), ".local", "bin", "gemini"),
|
|
6986
|
+
join7(homedir5(), ".npm", "bin", "gemini"),
|
|
6670
6987
|
"/usr/local/bin/gemini",
|
|
6671
6988
|
"/opt/homebrew/bin/gemini"
|
|
6672
6989
|
];
|
|
@@ -6687,7 +7004,7 @@ function buildGeminiChildEnv(proxyPort, proxyToken) {
|
|
|
6687
7004
|
return env;
|
|
6688
7005
|
}
|
|
6689
7006
|
function createGeminiCliHomeOverlay() {
|
|
6690
|
-
const cliHome = mkdtempSync(
|
|
7007
|
+
const cliHome = mkdtempSync(join7(tmpdir(), "relay-ai-gemini-"));
|
|
6691
7008
|
const settings = {
|
|
6692
7009
|
security: {
|
|
6693
7010
|
auth: {
|
|
@@ -6695,9 +7012,9 @@ function createGeminiCliHomeOverlay() {
|
|
|
6695
7012
|
}
|
|
6696
7013
|
}
|
|
6697
7014
|
};
|
|
6698
|
-
const geminiDir =
|
|
6699
|
-
|
|
6700
|
-
|
|
7015
|
+
const geminiDir = join7(cliHome, ".gemini");
|
|
7016
|
+
mkdirSync3(geminiDir);
|
|
7017
|
+
writeFileSync3(join7(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
|
|
6701
7018
|
`, {
|
|
6702
7019
|
encoding: "utf8",
|
|
6703
7020
|
mode: 384
|
|
@@ -9939,15 +10256,15 @@ import { execFileSync, execSync as execSync3 } from "child_process";
|
|
|
9939
10256
|
import spawn4 from "cross-spawn";
|
|
9940
10257
|
import { existsSync as existsSync6 } from "fs";
|
|
9941
10258
|
import { homedir as homedir6 } from "os";
|
|
9942
|
-
import { join as
|
|
10259
|
+
import { join as join8 } from "path";
|
|
9943
10260
|
var isWindows4 = process.platform === "win32";
|
|
9944
10261
|
var FALLBACK_PATHS = isWindows4 ? [
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
10262
|
+
join8(process.env["APPDATA"] ?? homedir6(), "npm", "agy.cmd"),
|
|
10263
|
+
join8(process.env["APPDATA"] ?? homedir6(), "npm", "agy"),
|
|
10264
|
+
join8(homedir6(), "AppData", "Roaming", "npm", "agy.cmd")
|
|
9948
10265
|
] : [
|
|
9949
|
-
|
|
9950
|
-
|
|
10266
|
+
join8(homedir6(), ".local", "bin", "agy"),
|
|
10267
|
+
join8(homedir6(), ".npm", "bin", "agy"),
|
|
9951
10268
|
"/usr/local/bin/agy",
|
|
9952
10269
|
"/opt/homebrew/bin/agy"
|
|
9953
10270
|
];
|
|
@@ -10025,7 +10342,7 @@ function launchAntigravityCli(env, extraArgs) {
|
|
|
10025
10342
|
import { execFileSync as execFileSync2, execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
10026
10343
|
import { existsSync as existsSync7 } from "fs";
|
|
10027
10344
|
import { homedir as homedir7 } from "os";
|
|
10028
|
-
import { join as
|
|
10345
|
+
import { join as join9 } from "path";
|
|
10029
10346
|
|
|
10030
10347
|
// src/antigravity/ide-profile.ts
|
|
10031
10348
|
import fs from "fs";
|
|
@@ -10059,8 +10376,8 @@ function prepareIdeProfile(profileDir, gatewayUrl) {
|
|
|
10059
10376
|
}
|
|
10060
10377
|
|
|
10061
10378
|
// src/antigravity/launch-ide.ts
|
|
10062
|
-
var LINUX_APP_PROFILE_DIR =
|
|
10063
|
-
var LINUX_IDE_PROFILE_DIR =
|
|
10379
|
+
var LINUX_APP_PROFILE_DIR = join9(homedir7(), ".relay-ai", "antigravity", "app-profile");
|
|
10380
|
+
var LINUX_IDE_PROFILE_DIR = join9(homedir7(), ".relay-ai", "antigravity", "profile");
|
|
10064
10381
|
function sleep(ms) {
|
|
10065
10382
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
10066
10383
|
}
|
|
@@ -10068,7 +10385,7 @@ function linuxAntigravityBinary() {
|
|
|
10068
10385
|
const candidates = [
|
|
10069
10386
|
"/usr/share/antigravity/antigravity",
|
|
10070
10387
|
"/opt/antigravity/antigravity",
|
|
10071
|
-
|
|
10388
|
+
join9(homedir7(), ".local", "share", "antigravity", "antigravity")
|
|
10072
10389
|
];
|
|
10073
10390
|
for (const candidate of candidates) {
|
|
10074
10391
|
if (existsSync7(candidate)) return candidate;
|
|
@@ -10223,15 +10540,15 @@ function findAntigravityAppBinary() {
|
|
|
10223
10540
|
const override = getAppPathOverride("antigravity");
|
|
10224
10541
|
if (override) return existsSync7(override) ? override : null;
|
|
10225
10542
|
if (process.platform === "win32") {
|
|
10226
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
10227
|
-
const winPath =
|
|
10543
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir7(), "AppData", "Local");
|
|
10544
|
+
const winPath = join9(localAppData, "Programs", "Antigravity", "Antigravity.exe");
|
|
10228
10545
|
return existsSync7(winPath) ? winPath : null;
|
|
10229
10546
|
}
|
|
10230
10547
|
if (process.platform === "linux") return linuxAntigravityBinary();
|
|
10231
10548
|
if (process.platform !== "darwin") return null;
|
|
10232
10549
|
const defaultPath = "/Applications/Antigravity.app/Contents/MacOS/Antigravity";
|
|
10233
10550
|
if (existsSync7(defaultPath)) return defaultPath;
|
|
10234
|
-
const homePath =
|
|
10551
|
+
const homePath = join9(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
|
|
10235
10552
|
if (existsSync7(homePath)) return homePath;
|
|
10236
10553
|
return null;
|
|
10237
10554
|
}
|
|
@@ -10239,15 +10556,15 @@ function findAntigravityIdeBinary() {
|
|
|
10239
10556
|
const override = getAppPathOverride("antigravity-ide");
|
|
10240
10557
|
if (override) return existsSync7(override) ? override : null;
|
|
10241
10558
|
if (process.platform === "win32") {
|
|
10242
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
10243
|
-
const winPath =
|
|
10559
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir7(), "AppData", "Local");
|
|
10560
|
+
const winPath = join9(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
|
|
10244
10561
|
return existsSync7(winPath) ? winPath : null;
|
|
10245
10562
|
}
|
|
10246
10563
|
if (process.platform === "linux") return linuxAntigravityBinary();
|
|
10247
10564
|
if (process.platform !== "darwin") return null;
|
|
10248
10565
|
const defaultPath = "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide";
|
|
10249
10566
|
if (existsSync7(defaultPath)) return defaultPath;
|
|
10250
|
-
const homePath =
|
|
10567
|
+
const homePath = join9(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
|
|
10251
10568
|
if (existsSync7(homePath)) return homePath;
|
|
10252
10569
|
return null;
|
|
10253
10570
|
}
|
|
@@ -10308,7 +10625,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
|
|
|
10308
10625
|
return;
|
|
10309
10626
|
}
|
|
10310
10627
|
prepareIdeProfile(profileDir, gatewayUrl);
|
|
10311
|
-
const relayExtensionsDir =
|
|
10628
|
+
const relayExtensionsDir = join9(homedir7(), ".relay-ai", "antigravity", "extensions");
|
|
10312
10629
|
const args = [
|
|
10313
10630
|
`--user-data-dir=${profileDir}`,
|
|
10314
10631
|
`--extensions-dir=${relayExtensionsDir}`,
|
|
@@ -10338,7 +10655,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
|
|
|
10338
10655
|
|
|
10339
10656
|
// src/antigravity.ts
|
|
10340
10657
|
import { homedir as homedir8 } from "os";
|
|
10341
|
-
import { join as
|
|
10658
|
+
import { join as join10 } from "path";
|
|
10342
10659
|
var SHUTDOWN_DRAIN_MS = 500;
|
|
10343
10660
|
var AGY_FAVORITES_PROVIDER_ID = "__relay_agy_favorites__";
|
|
10344
10661
|
var AGY_FAVORITES_PROVIDER_LABEL = "\u2605 Antigravity CLI Favorites";
|
|
@@ -10632,7 +10949,7 @@ async function runAntigravityAppCommand(childArgs, trace = false, boot) {
|
|
|
10632
10949
|
trace,
|
|
10633
10950
|
boot,
|
|
10634
10951
|
async (env, _routes, gatewayHandle) => {
|
|
10635
|
-
const profileDir =
|
|
10952
|
+
const profileDir = join10(homedir8(), ".relay-ai", "antigravity", "app-profile");
|
|
10636
10953
|
if (isAntigravityAppRunning(profileDir)) {
|
|
10637
10954
|
const restart = await p11.confirm({
|
|
10638
10955
|
message: "Restart Antigravity to apply this Relay gateway?",
|
|
@@ -10680,7 +10997,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
|
|
|
10680
10997
|
trace,
|
|
10681
10998
|
boot,
|
|
10682
10999
|
async (env, _routes, gatewayHandle) => {
|
|
10683
|
-
const profileDir =
|
|
11000
|
+
const profileDir = join10(homedir8(), ".relay-ai", "antigravity", "profile");
|
|
10684
11001
|
if (isAntigravityIdeRunning(profileDir)) {
|
|
10685
11002
|
const restart = await p11.confirm({
|
|
10686
11003
|
message: "Restart Antigravity IDE to apply this Relay gateway?",
|
|
@@ -10725,7 +11042,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
|
|
|
10725
11042
|
// src/codex-app.ts
|
|
10726
11043
|
import pc10 from "picocolors";
|
|
10727
11044
|
import * as p12 from "@clack/prompts";
|
|
10728
|
-
import { join as
|
|
11045
|
+
import { join as join13 } from "path";
|
|
10729
11046
|
|
|
10730
11047
|
// src/codex/app-provider-routes.ts
|
|
10731
11048
|
function codexRouteToProxyRoute(provider, model, apiKey) {
|
|
@@ -10814,14 +11131,14 @@ async function buildCodexAppProviderCatalogRoutes(provider, apiKey, selectedMode
|
|
|
10814
11131
|
}
|
|
10815
11132
|
|
|
10816
11133
|
// src/codex/app-config.ts
|
|
10817
|
-
import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as
|
|
10818
|
-
import { dirname as dirname2, join as
|
|
11134
|
+
import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
11135
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
10819
11136
|
import { parse, stringify } from "smol-toml";
|
|
10820
11137
|
function getCodexConfigPath() {
|
|
10821
|
-
return
|
|
11138
|
+
return join11(getCodexHome(), "config.toml");
|
|
10822
11139
|
}
|
|
10823
11140
|
function getCodexAppSidecarProfilePath() {
|
|
10824
|
-
return
|
|
11141
|
+
return join11(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
|
|
10825
11142
|
}
|
|
10826
11143
|
function asRecord(value) {
|
|
10827
11144
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -10996,8 +11313,13 @@ function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
|
|
|
10996
11313
|
const text5 = `${stringify(merged)}
|
|
10997
11314
|
`;
|
|
10998
11315
|
validateAppConfigText(text5, spec);
|
|
10999
|
-
|
|
11000
|
-
|
|
11316
|
+
mkdirSync4(dirname2(configPath), { recursive: true });
|
|
11317
|
+
atomicWriteFile(configPath, text5);
|
|
11318
|
+
const written = readCodexConfigText(configPath);
|
|
11319
|
+
if (written !== text5) {
|
|
11320
|
+
throw new Error(`Codex config readback mismatch at ${configPath}`);
|
|
11321
|
+
}
|
|
11322
|
+
validateAppConfigText(written, spec);
|
|
11001
11323
|
return text5;
|
|
11002
11324
|
}
|
|
11003
11325
|
function applyRestoreKey(config, key, had, value) {
|
|
@@ -11053,7 +11375,7 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
11053
11375
|
rmSync3(configPath, { force: true });
|
|
11054
11376
|
return true;
|
|
11055
11377
|
}
|
|
11056
|
-
|
|
11378
|
+
writeFileSync4(configPath, `${stringify(config)}
|
|
11057
11379
|
`, "utf8");
|
|
11058
11380
|
return true;
|
|
11059
11381
|
}
|
|
@@ -11064,31 +11386,69 @@ function previewAppConfigToml(spec) {
|
|
|
11064
11386
|
return text5;
|
|
11065
11387
|
}
|
|
11066
11388
|
|
|
11389
|
+
// src/codex/app-readiness.ts
|
|
11390
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
11391
|
+
function proxyRoot(spec) {
|
|
11392
|
+
const base = spec.proxyBaseUrl ?? `http://127.0.0.1:${spec.proxyPort}/v1`;
|
|
11393
|
+
if (!base.endsWith("/v1")) throw new Error("Codex App proxy base URL must end in /v1");
|
|
11394
|
+
return base.slice(0, -3);
|
|
11395
|
+
}
|
|
11396
|
+
async function checkedJson(url, fetchImpl) {
|
|
11397
|
+
const response = await fetchImpl(url);
|
|
11398
|
+
if (!response.ok) throw new Error(`Relay readiness check failed: GET ${url} returned HTTP ${response.status}`);
|
|
11399
|
+
return response.json();
|
|
11400
|
+
}
|
|
11401
|
+
async function verifyCodexAppReadiness(spec, options = {}) {
|
|
11402
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11403
|
+
const root = proxyRoot(spec);
|
|
11404
|
+
const health = await checkedJson(`${root}/health`, fetchImpl);
|
|
11405
|
+
if (health.ok !== true) throw new Error("Relay proxy health check did not report ready");
|
|
11406
|
+
const catalog = JSON.parse(readFileSync4(spec.catalogPath, "utf8"));
|
|
11407
|
+
if (!Array.isArray(catalog.models) || catalog.models.length === 0) {
|
|
11408
|
+
throw new Error("Relay Codex model catalog is empty or invalid");
|
|
11409
|
+
}
|
|
11410
|
+
const catalogIds = catalog.models.map((model) => model?.slug).filter((id) => typeof id === "string" && id.length > 0);
|
|
11411
|
+
if (catalogIds.length !== catalog.models.length) throw new Error("Relay Codex model catalog contains an invalid model slug");
|
|
11412
|
+
if (!catalogIds.includes(spec.route.modelId)) {
|
|
11413
|
+
throw new Error(`Relay Codex model catalog is missing selected model ${spec.route.modelId}`);
|
|
11414
|
+
}
|
|
11415
|
+
const advertised = await checkedJson(`${root}/v1/models`, fetchImpl);
|
|
11416
|
+
const advertisedIds = new Set((advertised.data ?? []).map((model) => model.id).filter((id) => typeof id === "string"));
|
|
11417
|
+
for (const id of catalogIds) {
|
|
11418
|
+
if (!advertisedIds.has(id)) throw new Error(`Relay proxy does not advertise catalog model ${id}`);
|
|
11419
|
+
}
|
|
11420
|
+
validateAppConfigText(readCodexConfigText(options.configPath), spec);
|
|
11421
|
+
}
|
|
11422
|
+
|
|
11067
11423
|
// src/codex/app-session.ts
|
|
11068
11424
|
import {
|
|
11069
11425
|
copyFileSync as copyFileSync2,
|
|
11070
11426
|
existsSync as existsSync9,
|
|
11071
|
-
mkdirSync as
|
|
11427
|
+
mkdirSync as mkdirSync5,
|
|
11072
11428
|
readdirSync as readdirSync2,
|
|
11073
|
-
readFileSync as
|
|
11429
|
+
readFileSync as readFileSync5,
|
|
11074
11430
|
rmSync as rmSync4,
|
|
11075
11431
|
statSync as statSync2
|
|
11076
11432
|
} from "fs";
|
|
11077
|
-
import { basename as basename2, join as
|
|
11433
|
+
import { basename as basename2, join as join12 } from "path";
|
|
11434
|
+
import { createHash as createHash3 } from "crypto";
|
|
11078
11435
|
function getAppSessionLockPath(env = process.env) {
|
|
11079
|
-
return
|
|
11436
|
+
return join12(getRelayAiCodexDir(env), "session-app.json");
|
|
11080
11437
|
}
|
|
11081
11438
|
function getAppRestoreStatePath(env = process.env) {
|
|
11082
|
-
return
|
|
11439
|
+
return join12(getRelayAiCodexDir(env), "app-restore-state.json");
|
|
11083
11440
|
}
|
|
11084
11441
|
function getAppCatalogPath(providerId, env = process.env) {
|
|
11085
|
-
return
|
|
11442
|
+
return join12(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
|
|
11443
|
+
}
|
|
11444
|
+
function fileSha256(path3) {
|
|
11445
|
+
return createHash3("sha256").update(readFileSync5(path3)).digest("hex");
|
|
11086
11446
|
}
|
|
11087
11447
|
function readAppSessionLock(env = process.env) {
|
|
11088
11448
|
const path3 = getAppSessionLockPath(env);
|
|
11089
11449
|
if (!existsSync9(path3)) return null;
|
|
11090
11450
|
try {
|
|
11091
|
-
const parsed = JSON.parse(
|
|
11451
|
+
const parsed = JSON.parse(readFileSync5(path3, "utf8"));
|
|
11092
11452
|
if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
|
|
11093
11453
|
} catch {
|
|
11094
11454
|
}
|
|
@@ -11106,7 +11466,7 @@ function readAppRestoreState(env = process.env) {
|
|
|
11106
11466
|
const path3 = getAppRestoreStatePath(env);
|
|
11107
11467
|
if (!existsSync9(path3)) return null;
|
|
11108
11468
|
try {
|
|
11109
|
-
return JSON.parse(
|
|
11469
|
+
return JSON.parse(readFileSync5(path3, "utf8"));
|
|
11110
11470
|
} catch {
|
|
11111
11471
|
return null;
|
|
11112
11472
|
}
|
|
@@ -11125,9 +11485,9 @@ function backupConfigToml(env = process.env) {
|
|
|
11125
11485
|
if (!existsSync9(configPath)) return void 0;
|
|
11126
11486
|
rotateBackups(configPath, env);
|
|
11127
11487
|
const backupsDir = getBackupsDir(env);
|
|
11128
|
-
|
|
11488
|
+
mkdirSync5(backupsDir, { recursive: true });
|
|
11129
11489
|
const base = basename2(configPath);
|
|
11130
|
-
const backupPath =
|
|
11490
|
+
const backupPath = join12(backupsDir, `${base}.${Date.now()}.bak`);
|
|
11131
11491
|
copyFileSync2(configPath, backupPath);
|
|
11132
11492
|
return backupPath;
|
|
11133
11493
|
}
|
|
@@ -11144,7 +11504,7 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
|
|
|
11144
11504
|
function ownedAppCatalogPaths(env = process.env) {
|
|
11145
11505
|
const codexDir = getRelayAiCodexDir(env);
|
|
11146
11506
|
if (!existsSync9(codexDir)) return [];
|
|
11147
|
-
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) =>
|
|
11507
|
+
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join12(codexDir, n));
|
|
11148
11508
|
}
|
|
11149
11509
|
function removeAppCatalogs(env = process.env) {
|
|
11150
11510
|
const removed = [];
|
|
@@ -11162,7 +11522,7 @@ function newestConfigBackup(env = process.env) {
|
|
|
11162
11522
|
if (!existsSync9(backupDir)) return null;
|
|
11163
11523
|
const configBase = basename2(getCodexConfigPath());
|
|
11164
11524
|
const candidates = readdirSync2(backupDir).filter((name) => name.startsWith(`${configBase}.`) && name.endsWith(".bak")).map((name) => {
|
|
11165
|
-
const path3 =
|
|
11525
|
+
const path3 = join12(backupDir, name);
|
|
11166
11526
|
try {
|
|
11167
11527
|
return { path: path3, mtimeMs: statSync2(path3).mtimeMs };
|
|
11168
11528
|
} catch {
|
|
@@ -11188,7 +11548,12 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
11188
11548
|
clearAppSessionLock(env);
|
|
11189
11549
|
return { restored: false, message: "Nothing to restore." };
|
|
11190
11550
|
}
|
|
11191
|
-
|
|
11551
|
+
const exactBackupIsSafe = Boolean(
|
|
11552
|
+
managed && lock?.backupPath && existsSync9(lock.backupPath) && lock.patchedConfigSha256 && lock.originalConfigSha256 && fileSha256(getCodexConfigPath()) === lock.patchedConfigSha256 && fileSha256(lock.backupPath) === lock.originalConfigSha256
|
|
11553
|
+
);
|
|
11554
|
+
if (exactBackupIsSafe) {
|
|
11555
|
+
copyFileSync2(lock.backupPath, getCodexConfigPath());
|
|
11556
|
+
} else if (restoreState) {
|
|
11192
11557
|
restoreConfigFromState(restoreState);
|
|
11193
11558
|
} else if (lock?.backupPath && existsSync9(lock.backupPath)) {
|
|
11194
11559
|
copyFileSync2(lock.backupPath, getCodexConfigPath());
|
|
@@ -11244,6 +11609,23 @@ function waitForShutdown2() {
|
|
|
11244
11609
|
});
|
|
11245
11610
|
}
|
|
11246
11611
|
|
|
11612
|
+
// src/codex/app-shutdown.ts
|
|
11613
|
+
async function shutdownCodexAppSession(dependencies) {
|
|
11614
|
+
if (dependencies.isAppRunning()) {
|
|
11615
|
+
dependencies.quitApp();
|
|
11616
|
+
const exited = await dependencies.waitForAppExit();
|
|
11617
|
+
if (!exited) {
|
|
11618
|
+
throw new Error(
|
|
11619
|
+
"ChatGPT Desktop did not exit after graceful shutdown; refusing to restore config until Desktop exits. Close Desktop, then run relay-ai codex-app --restore."
|
|
11620
|
+
);
|
|
11621
|
+
}
|
|
11622
|
+
}
|
|
11623
|
+
const result = dependencies.restoreOverlay();
|
|
11624
|
+
if (result.liveSession) throw new Error(result.message);
|
|
11625
|
+
dependencies.closeResources();
|
|
11626
|
+
return result;
|
|
11627
|
+
}
|
|
11628
|
+
|
|
11247
11629
|
// src/codex-app.ts
|
|
11248
11630
|
function codexProxyRouteToCodexRoute(route, fallbackProviderId) {
|
|
11249
11631
|
return {
|
|
@@ -11264,11 +11646,15 @@ function codexProxyRouteToCodexRoute(route, fallbackProviderId) {
|
|
|
11264
11646
|
refreshToken: route.refreshToken
|
|
11265
11647
|
};
|
|
11266
11648
|
}
|
|
11649
|
+
function codexAppUsesExplicitSelection(configOnly, launchProvider, launchModel) {
|
|
11650
|
+
void configOnly;
|
|
11651
|
+
return Boolean(launchProvider && launchModel);
|
|
11652
|
+
}
|
|
11267
11653
|
async function waitForShutdownWithConfirm(assumeYes = false) {
|
|
11268
11654
|
while (true) {
|
|
11269
11655
|
const signal = await waitForShutdown2();
|
|
11270
|
-
if (signal !== "sigint")
|
|
11271
|
-
if (assumeYes)
|
|
11656
|
+
if (signal !== "sigint") return signal;
|
|
11657
|
+
if (assumeYes) return signal;
|
|
11272
11658
|
console.log("");
|
|
11273
11659
|
const choice = await p12.select({
|
|
11274
11660
|
message: "Close ChatGPT Desktop and restore your Codex config?",
|
|
@@ -11277,20 +11663,7 @@ async function waitForShutdownWithConfirm(assumeYes = false) {
|
|
|
11277
11663
|
{ value: "no", label: "No, keep session running" }
|
|
11278
11664
|
]
|
|
11279
11665
|
});
|
|
11280
|
-
if (p12.isCancel(choice) || choice === "yes")
|
|
11281
|
-
}
|
|
11282
|
-
}
|
|
11283
|
-
async function maybeCloseRunningCodexApp(assumeYes = false) {
|
|
11284
|
-
if (!isCodexAppRunning()) return;
|
|
11285
|
-
if (assumeYes) {
|
|
11286
|
-
p12.log.step("Stopping ChatGPT Desktop...");
|
|
11287
|
-
quitCodexAppGracefully();
|
|
11288
|
-
return;
|
|
11289
|
-
}
|
|
11290
|
-
const shouldClose = await p12.confirm({ message: "ChatGPT Desktop is still running. Close it?" });
|
|
11291
|
-
if (shouldClose && !p12.isCancel(shouldClose)) {
|
|
11292
|
-
p12.log.step("Stopping ChatGPT Desktop...");
|
|
11293
|
-
quitCodexAppGracefully();
|
|
11666
|
+
if (p12.isCancel(choice) || choice === "yes") return signal;
|
|
11294
11667
|
}
|
|
11295
11668
|
}
|
|
11296
11669
|
function codexAppHelpText() {
|
|
@@ -11310,7 +11683,7 @@ ${pc10.bold("Options:")}
|
|
|
11310
11683
|
--vertex Use Claude models through Google Vertex AI
|
|
11311
11684
|
--with-native Load native Codex models beside Relay models for this launch
|
|
11312
11685
|
--relay-only Keep the current Relay-only launch behavior
|
|
11313
|
-
--yes, -y
|
|
11686
|
+
--yes, -y Approve a fully specified launch/restart without prompting
|
|
11314
11687
|
--restore Restore Codex config after an interrupted app session
|
|
11315
11688
|
--config Preview the generated Codex app configuration without launching
|
|
11316
11689
|
--trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
@@ -11326,7 +11699,7 @@ ${pc10.bold("Platforms:")}
|
|
|
11326
11699
|
macOS, Windows, and Linux (ChatGPT desktop app preview).
|
|
11327
11700
|
|
|
11328
11701
|
${pc10.bold("Cleanup:")}
|
|
11329
|
-
Ctrl+C
|
|
11702
|
+
Ctrl+C closes ChatGPT Desktop, restores your previous Codex config, and stops the proxy.
|
|
11330
11703
|
After crash: relay-ai codex-app --restore
|
|
11331
11704
|
|
|
11332
11705
|
${pc10.bold("Preview (no writes):")}
|
|
@@ -11424,6 +11797,25 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11424
11797
|
}
|
|
11425
11798
|
let proxyHandle = null;
|
|
11426
11799
|
let sessionActive = false;
|
|
11800
|
+
let shutdownFailed = false;
|
|
11801
|
+
let resourcesClosed = false;
|
|
11802
|
+
const closeResources = () => {
|
|
11803
|
+
if (resourcesClosed) return;
|
|
11804
|
+
resourcesClosed = true;
|
|
11805
|
+
proxyHandle?.close();
|
|
11806
|
+
};
|
|
11807
|
+
const restoreOverlay = () => {
|
|
11808
|
+
const result = restoreCodexAppOverlay();
|
|
11809
|
+
if (!result.liveSession) sessionActive = false;
|
|
11810
|
+
return result;
|
|
11811
|
+
};
|
|
11812
|
+
const restoreOverlaySafely = () => {
|
|
11813
|
+
try {
|
|
11814
|
+
restoreOverlay();
|
|
11815
|
+
} catch (err) {
|
|
11816
|
+
p12.log.error(String(err instanceof Error ? err.message : err));
|
|
11817
|
+
}
|
|
11818
|
+
};
|
|
11427
11819
|
try {
|
|
11428
11820
|
proxyHandle = await startCodexProxy(
|
|
11429
11821
|
vertexModels.map((m) => ({
|
|
@@ -11446,8 +11838,10 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11446
11838
|
catalogPath
|
|
11447
11839
|
};
|
|
11448
11840
|
saveAppRestoreStateBeforePatch();
|
|
11841
|
+
sessionActive = true;
|
|
11449
11842
|
const backupPath = backupConfigToml();
|
|
11450
11843
|
applyAppConfigPatch(spec);
|
|
11844
|
+
await verifyCodexAppReadiness(spec);
|
|
11451
11845
|
writeAppSessionLock({
|
|
11452
11846
|
pid: process.pid,
|
|
11453
11847
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -11455,9 +11849,10 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11455
11849
|
catalogPaths: [catalogPath],
|
|
11456
11850
|
restoreStatePath: getAppRestoreStatePath(),
|
|
11457
11851
|
backupPath,
|
|
11458
|
-
proxyPort
|
|
11852
|
+
proxyPort,
|
|
11853
|
+
patchedConfigSha256: fileSha256(getCodexConfigPath()),
|
|
11854
|
+
...backupPath ? { originalConfigSha256: fileSha256(backupPath) } : {}
|
|
11459
11855
|
});
|
|
11460
|
-
sessionActive = true;
|
|
11461
11856
|
p12.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
|
|
11462
11857
|
logProxy(proxyPort);
|
|
11463
11858
|
logActiveModel(selectedEntry.display_name, selectedEntry.id);
|
|
@@ -11466,6 +11861,7 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11466
11861
|
} catch (err) {
|
|
11467
11862
|
p12.log.warn(String(err instanceof Error ? err.message : err));
|
|
11468
11863
|
p12.log.info(codexAppInstallHint());
|
|
11864
|
+
throw err;
|
|
11469
11865
|
}
|
|
11470
11866
|
printCodexAppSessionPanel({
|
|
11471
11867
|
modelLabel: selectedEntry.display_name,
|
|
@@ -11476,15 +11872,27 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11476
11872
|
codexAppOutro(selectedEntry.display_name);
|
|
11477
11873
|
await waitForShutdownWithConfirm();
|
|
11478
11874
|
console.log("");
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11875
|
+
try {
|
|
11876
|
+
const result = await shutdownCodexAppSession({
|
|
11877
|
+
isAppRunning: isCodexAppRunning,
|
|
11878
|
+
quitApp: quitCodexAppGracefully,
|
|
11879
|
+
waitForAppExit: () => waitForCodexAppQuit(),
|
|
11880
|
+
restoreOverlay,
|
|
11881
|
+
closeResources
|
|
11882
|
+
});
|
|
11883
|
+
p12.log.success(result.message);
|
|
11884
|
+
return 0;
|
|
11885
|
+
} catch (err) {
|
|
11886
|
+
shutdownFailed = true;
|
|
11887
|
+
p12.log.error(String(err instanceof Error ? err.message : err));
|
|
11888
|
+
return 1;
|
|
11482
11889
|
}
|
|
11483
|
-
await maybeCloseRunningCodexApp();
|
|
11484
|
-
return 0;
|
|
11485
11890
|
} finally {
|
|
11486
|
-
|
|
11487
|
-
|
|
11891
|
+
if (sessionActive && !isCodexAppRunning()) restoreOverlaySafely();
|
|
11892
|
+
closeResources();
|
|
11893
|
+
if (sessionActive && !shutdownFailed) {
|
|
11894
|
+
p12.log.error("ChatGPT Desktop is still running; config restoration was skipped. Close Desktop, then run relay-ai codex-app --restore.");
|
|
11895
|
+
}
|
|
11488
11896
|
}
|
|
11489
11897
|
}
|
|
11490
11898
|
async function runCodexAppCommand(args, opts = {}) {
|
|
@@ -11576,7 +11984,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
11576
11984
|
compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
|
|
11577
11985
|
);
|
|
11578
11986
|
let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastCodexModel) ?? activeProvider.models[0];
|
|
11579
|
-
if (
|
|
11987
|
+
if (codexAppUsesExplicitSelection(configOnly, opts.launchProvider, opts.launchModel)) {
|
|
11580
11988
|
const bootSelection = resolveBootSelection(
|
|
11581
11989
|
compatible,
|
|
11582
11990
|
opts.launchProvider,
|
|
@@ -11655,6 +12063,11 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
11655
12063
|
generalFavorites: favorites,
|
|
11656
12064
|
subagentFavorites: prefs.codexSubagentModels ?? []
|
|
11657
12065
|
});
|
|
12066
|
+
if (mixedModels.capacitySkipped.length > 0) {
|
|
12067
|
+
p12.log.warn(
|
|
12068
|
+
`Skipped ${mixedModels.capacitySkipped.length} favorite(s) because the mixed catalog is full: ` + mixedModels.capacitySkipped.map((f) => `${f.providerId}:${f.modelId}`).join(", ")
|
|
12069
|
+
);
|
|
12070
|
+
}
|
|
11658
12071
|
assertConfiguredCodexSubagentsResolved(prefs.codexSubagentModels ?? [], mixedModels);
|
|
11659
12072
|
const multiAgentV2Supported = mixedModels.subagents.length === 0 || supportsMultiAgentV2(embeddedBinary);
|
|
11660
12073
|
if (!multiAgentV2Supported) {
|
|
@@ -11693,8 +12106,29 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11693
12106
|
}
|
|
11694
12107
|
let proxyHandle = null;
|
|
11695
12108
|
let sessionActive = false;
|
|
12109
|
+
let shutdownFailed = false;
|
|
12110
|
+
let resourcesClosed = false;
|
|
12111
|
+
const closeResources = () => {
|
|
12112
|
+
if (resourcesClosed) return;
|
|
12113
|
+
resourcesClosed = true;
|
|
12114
|
+
proxyHandle?.close();
|
|
12115
|
+
cloudCodeBackend?.handle.close();
|
|
12116
|
+
cloudCodeBackendFav?.handle.close();
|
|
12117
|
+
};
|
|
12118
|
+
const restoreOverlay = () => {
|
|
12119
|
+
const result = restoreCodexAppOverlay();
|
|
12120
|
+
if (!result.liveSession) sessionActive = false;
|
|
12121
|
+
return result;
|
|
12122
|
+
};
|
|
12123
|
+
const restoreOverlaySafely = () => {
|
|
12124
|
+
try {
|
|
12125
|
+
restoreOverlay();
|
|
12126
|
+
} catch (err) {
|
|
12127
|
+
p12.log.error(String(err instanceof Error ? err.message : err));
|
|
12128
|
+
}
|
|
12129
|
+
};
|
|
11696
12130
|
try {
|
|
11697
|
-
const catalogPath = mixedPlan ?
|
|
12131
|
+
const catalogPath = mixedPlan ? join13(getRelayAiCodexDir(), "app-models-mixed.json") : favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
|
|
11698
12132
|
const activeRoute = mixedPlan ? {
|
|
11699
12133
|
tier: "proxy",
|
|
11700
12134
|
modelId: mixedPlan.selectedSlug,
|
|
@@ -11757,10 +12191,12 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11757
12191
|
return 0;
|
|
11758
12192
|
}
|
|
11759
12193
|
let proxyPort;
|
|
12194
|
+
const routeAuditPath = mixedPlan ? prepareCodexRouteAuditLog() : void 0;
|
|
11760
12195
|
if (mixedPlan) {
|
|
11761
12196
|
proxyHandle = await startCodexProxy(mixedPlan.relayRoutes, {
|
|
11762
12197
|
requireAuth: false,
|
|
11763
12198
|
debug: trace,
|
|
12199
|
+
routeAuditPath,
|
|
11764
12200
|
mixedNative: {
|
|
11765
12201
|
nativeModelIds: mixedPlan.nativeModelIds,
|
|
11766
12202
|
subagentRouteModelId: mixedPlan.subagentRouteModelId,
|
|
@@ -11769,6 +12205,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11769
12205
|
}
|
|
11770
12206
|
});
|
|
11771
12207
|
proxyPort = proxyHandle.port;
|
|
12208
|
+
p12.log.info(`Route audit (metadata only): ${routeAuditPath}`);
|
|
11772
12209
|
} else if (favoritesActive && resolvedFavorites.length > 0) {
|
|
11773
12210
|
const needsBackend = (r) => {
|
|
11774
12211
|
const m = r.model;
|
|
@@ -11828,8 +12265,10 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11828
12265
|
...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(proxyPort, mixedPlan.capability)}/v1` } : {}
|
|
11829
12266
|
};
|
|
11830
12267
|
saveAppRestoreStateBeforePatch();
|
|
12268
|
+
sessionActive = true;
|
|
11831
12269
|
const backupPath = backupConfigToml();
|
|
11832
12270
|
applyAppConfigPatch(spec);
|
|
12271
|
+
await verifyCodexAppReadiness(spec);
|
|
11833
12272
|
writeAppSessionLock({
|
|
11834
12273
|
pid: process.pid,
|
|
11835
12274
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -11837,9 +12276,10 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11837
12276
|
catalogPaths: [catalogPath],
|
|
11838
12277
|
restoreStatePath: getAppRestoreStatePath(),
|
|
11839
12278
|
backupPath,
|
|
11840
|
-
proxyPort
|
|
12279
|
+
proxyPort,
|
|
12280
|
+
patchedConfigSha256: fileSha256(getCodexConfigPath()),
|
|
12281
|
+
...backupPath ? { originalConfigSha256: fileSha256(backupPath) } : {}
|
|
11841
12282
|
});
|
|
11842
|
-
sessionActive = true;
|
|
11843
12283
|
const prevRecent = prefs.recentModelsByProvider?.[activeProvider.id] ?? [];
|
|
11844
12284
|
const updatedRecent = [selectedModel.id, ...prevRecent.filter((id) => id !== selectedModel.id)].slice(0, 3);
|
|
11845
12285
|
savePreferences({
|
|
@@ -11854,6 +12294,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11854
12294
|
} catch (err) {
|
|
11855
12295
|
p12.log.warn(String(err instanceof Error ? err.message : err));
|
|
11856
12296
|
p12.log.info(codexAppInstallHint());
|
|
12297
|
+
throw err;
|
|
11857
12298
|
}
|
|
11858
12299
|
printCodexAppSessionPanel({
|
|
11859
12300
|
modelLabel,
|
|
@@ -11865,21 +12306,27 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
|
|
|
11865
12306
|
await waitForShutdownWithConfirm(opts.assumeYes);
|
|
11866
12307
|
if (trace) printTraceLog(debugLogPath);
|
|
11867
12308
|
console.log("");
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
12309
|
+
try {
|
|
12310
|
+
const result = await shutdownCodexAppSession({
|
|
12311
|
+
isAppRunning: isCodexAppRunning,
|
|
12312
|
+
quitApp: quitCodexAppGracefully,
|
|
12313
|
+
waitForAppExit: () => waitForCodexAppQuit(),
|
|
12314
|
+
restoreOverlay,
|
|
12315
|
+
closeResources
|
|
12316
|
+
});
|
|
12317
|
+
p12.log.success(result.message);
|
|
12318
|
+
return 0;
|
|
12319
|
+
} catch (err) {
|
|
12320
|
+
shutdownFailed = true;
|
|
12321
|
+
p12.log.error(String(err instanceof Error ? err.message : err));
|
|
12322
|
+
return 1;
|
|
11871
12323
|
}
|
|
11872
|
-
await maybeCloseRunningCodexApp(opts.assumeYes);
|
|
11873
|
-
return 0;
|
|
11874
12324
|
} finally {
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
12325
|
+
if (sessionActive && !isCodexAppRunning()) restoreOverlaySafely();
|
|
12326
|
+
closeResources();
|
|
12327
|
+
if (sessionActive && !shutdownFailed) {
|
|
12328
|
+
p12.log.error("ChatGPT Desktop is still running; config restoration was skipped. Close Desktop, then run relay-ai codex-app --restore.");
|
|
11878
12329
|
}
|
|
11879
|
-
if (cloudCodeBackendFav) {
|
|
11880
|
-
cloudCodeBackendFav.handle.close();
|
|
11881
|
-
}
|
|
11882
|
-
if (sessionActive) restoreCodexAppOverlay();
|
|
11883
12330
|
}
|
|
11884
12331
|
}
|
|
11885
12332
|
|
|
@@ -11888,38 +12335,38 @@ import pc11 from "picocolors";
|
|
|
11888
12335
|
import * as p13 from "@clack/prompts";
|
|
11889
12336
|
|
|
11890
12337
|
// src/claude-desktop/app-config.ts
|
|
11891
|
-
import { existsSync as existsSync10, readFileSync as
|
|
12338
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
11892
12339
|
import { homedir as homedir9 } from "os";
|
|
11893
|
-
import { join as
|
|
12340
|
+
import { join as join14, dirname as dirname3 } from "path";
|
|
11894
12341
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
11895
12342
|
function getClaudeDesktopHome() {
|
|
11896
12343
|
if (process.platform === "win32") {
|
|
11897
|
-
return
|
|
12344
|
+
return join14(process.env.LOCALAPPDATA || join14(homedir9(), "AppData", "Local"), "Claude-3p");
|
|
11898
12345
|
}
|
|
11899
12346
|
if (process.platform === "linux") {
|
|
11900
|
-
return
|
|
12347
|
+
return join14(process.env.XDG_CONFIG_HOME || join14(homedir9(), ".config"), "Claude-3p");
|
|
11901
12348
|
}
|
|
11902
|
-
return
|
|
12349
|
+
return join14(homedir9(), "Library", "Application Support", "Claude-3p");
|
|
11903
12350
|
}
|
|
11904
12351
|
function getConfigLibraryPath() {
|
|
11905
|
-
return
|
|
12352
|
+
return join14(getClaudeDesktopHome(), "configLibrary");
|
|
11906
12353
|
}
|
|
11907
12354
|
function getMetaJsonPath() {
|
|
11908
|
-
return
|
|
12355
|
+
return join14(getConfigLibraryPath(), "_meta.json");
|
|
11909
12356
|
}
|
|
11910
12357
|
function readMetaJson() {
|
|
11911
12358
|
const metaPath = getMetaJsonPath();
|
|
11912
12359
|
if (!existsSync10(metaPath)) return null;
|
|
11913
12360
|
try {
|
|
11914
|
-
return JSON.parse(
|
|
12361
|
+
return JSON.parse(readFileSync6(metaPath, "utf8"));
|
|
11915
12362
|
} catch {
|
|
11916
12363
|
return null;
|
|
11917
12364
|
}
|
|
11918
12365
|
}
|
|
11919
12366
|
function writeMetaJson(meta) {
|
|
11920
12367
|
const metaPath = getMetaJsonPath();
|
|
11921
|
-
|
|
11922
|
-
|
|
12368
|
+
mkdirSync6(dirname3(metaPath), { recursive: true });
|
|
12369
|
+
writeFileSync5(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
11923
12370
|
`, "utf8");
|
|
11924
12371
|
}
|
|
11925
12372
|
function buildRelayAiConfig(proxyPort) {
|
|
@@ -11933,10 +12380,10 @@ function buildRelayAiConfig(proxyPort) {
|
|
|
11933
12380
|
}
|
|
11934
12381
|
function writeRelayAiConfig(proxyPort) {
|
|
11935
12382
|
const uuid = randomUUID3();
|
|
11936
|
-
const configPath =
|
|
12383
|
+
const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
|
|
11937
12384
|
const config = buildRelayAiConfig(proxyPort);
|
|
11938
|
-
|
|
11939
|
-
|
|
12385
|
+
mkdirSync6(dirname3(configPath), { recursive: true });
|
|
12386
|
+
writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
|
|
11940
12387
|
`, "utf8");
|
|
11941
12388
|
const meta = readMetaJson() || { appliedId: "", entries: [] };
|
|
11942
12389
|
meta.appliedId = uuid;
|
|
@@ -12091,22 +12538,22 @@ async function buildClaudeAppServerCatalog(entries, providersById, trace) {
|
|
|
12091
12538
|
import {
|
|
12092
12539
|
copyFileSync as copyFileSync3,
|
|
12093
12540
|
existsSync as existsSync11,
|
|
12094
|
-
mkdirSync as
|
|
12095
|
-
readFileSync as
|
|
12541
|
+
mkdirSync as mkdirSync7,
|
|
12542
|
+
readFileSync as readFileSync7,
|
|
12096
12543
|
renameSync as renameSync2,
|
|
12097
12544
|
rmSync as rmSync5,
|
|
12098
12545
|
unlinkSync as unlinkSync2,
|
|
12099
|
-
writeFileSync as
|
|
12546
|
+
writeFileSync as writeFileSync6
|
|
12100
12547
|
} from "fs";
|
|
12101
|
-
import { dirname as dirname4, join as
|
|
12548
|
+
import { dirname as dirname4, join as join15 } from "path";
|
|
12102
12549
|
function getSessionLockPath2() {
|
|
12103
|
-
return
|
|
12550
|
+
return join15(getClaudeDesktopHome(), ".relay-ai.lock");
|
|
12104
12551
|
}
|
|
12105
12552
|
function inspectSessionLock() {
|
|
12106
12553
|
const path3 = getSessionLockPath2();
|
|
12107
12554
|
if (!existsSync11(path3)) return { status: "missing" };
|
|
12108
12555
|
try {
|
|
12109
|
-
const parsed = JSON.parse(
|
|
12556
|
+
const parsed = JSON.parse(readFileSync7(path3, "utf8"));
|
|
12110
12557
|
if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string" && typeof parsed.uuid === "string" && typeof parsed.proxyPort === "number") {
|
|
12111
12558
|
return { status: "valid", lock: parsed };
|
|
12112
12559
|
}
|
|
@@ -12117,9 +12564,9 @@ function inspectSessionLock() {
|
|
|
12117
12564
|
function writeSessionLock2(lock) {
|
|
12118
12565
|
const path3 = getSessionLockPath2();
|
|
12119
12566
|
const tempPath = `${path3}.tmp.${process.pid}`;
|
|
12120
|
-
|
|
12567
|
+
mkdirSync7(dirname4(path3), { recursive: true });
|
|
12121
12568
|
try {
|
|
12122
|
-
|
|
12569
|
+
writeFileSync6(tempPath, `${JSON.stringify(lock, null, 2)}
|
|
12123
12570
|
`, "utf8");
|
|
12124
12571
|
renameSync2(tempPath, path3);
|
|
12125
12572
|
} finally {
|
|
@@ -12154,7 +12601,7 @@ function restoreMetaJson() {
|
|
|
12154
12601
|
}
|
|
12155
12602
|
}
|
|
12156
12603
|
function removeRelayAiConfig(uuid) {
|
|
12157
|
-
const configPath =
|
|
12604
|
+
const configPath = join15(getConfigLibraryPath(), `${uuid}.json`);
|
|
12158
12605
|
if (existsSync11(configPath)) {
|
|
12159
12606
|
try {
|
|
12160
12607
|
rmSync5(configPath, { force: true });
|
|
@@ -12472,17 +12919,17 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
|
|
|
12472
12919
|
}
|
|
12473
12920
|
|
|
12474
12921
|
// src/ai-doc.ts
|
|
12475
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
12922
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
12476
12923
|
import { homedir as homedir10 } from "os";
|
|
12477
|
-
import { join as
|
|
12924
|
+
import { join as join16 } from "path";
|
|
12478
12925
|
var SKILL_DIR_NAME = "relay-ai-cli";
|
|
12479
12926
|
var SKILL_INSTALL_DIRS = [
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12927
|
+
join16(getAppHome(), "skills"),
|
|
12928
|
+
join16(homedir10(), ".claude", "skills"),
|
|
12929
|
+
join16(homedir10(), ".agents", "skills"),
|
|
12930
|
+
join16(homedir10(), ".codex", "skills"),
|
|
12931
|
+
join16(homedir10(), ".cursor", "skills"),
|
|
12932
|
+
join16(homedir10(), ".cursor", "skills-cursor")
|
|
12486
12933
|
];
|
|
12487
12934
|
function parseSkillVersion(content) {
|
|
12488
12935
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -12496,10 +12943,10 @@ function parseSkillVersion(content) {
|
|
|
12496
12943
|
return null;
|
|
12497
12944
|
}
|
|
12498
12945
|
function readInstalledSkillVersion(skillDir) {
|
|
12499
|
-
const skillPath =
|
|
12946
|
+
const skillPath = join16(skillDir, "SKILL.md");
|
|
12500
12947
|
if (!existsSync12(skillPath)) return null;
|
|
12501
12948
|
try {
|
|
12502
|
-
const head =
|
|
12949
|
+
const head = readFileSync8(skillPath, "utf-8").slice(0, 1024);
|
|
12503
12950
|
return parseSkillVersion(head.includes("---", 4) ? head : `${head}
|
|
12504
12951
|
---
|
|
12505
12952
|
`);
|
|
@@ -12509,8 +12956,8 @@ function readInstalledSkillVersion(skillDir) {
|
|
|
12509
12956
|
}
|
|
12510
12957
|
function skillInstallTargets() {
|
|
12511
12958
|
return SKILL_INSTALL_DIRS.map((dir) => {
|
|
12512
|
-
const skillDir =
|
|
12513
|
-
return { skillDir, skillPath:
|
|
12959
|
+
const skillDir = join16(dir, SKILL_DIR_NAME);
|
|
12960
|
+
return { skillDir, skillPath: join16(skillDir, "SKILL.md") };
|
|
12514
12961
|
});
|
|
12515
12962
|
}
|
|
12516
12963
|
function formatProviderModels(provider) {
|
|
@@ -13014,8 +13461,8 @@ function installAiDoc(opts = {}) {
|
|
|
13014
13461
|
result.skipped.push(skillPath);
|
|
13015
13462
|
continue;
|
|
13016
13463
|
}
|
|
13017
|
-
|
|
13018
|
-
|
|
13464
|
+
mkdirSync8(skillDir, { recursive: true });
|
|
13465
|
+
writeFileSync7(skillPath, doc, "utf-8");
|
|
13019
13466
|
if (previous) {
|
|
13020
13467
|
result.updated.push({ path: skillPath, fromVersion: previous });
|
|
13021
13468
|
} else {
|
|
@@ -13175,16 +13622,16 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
|
|
|
13175
13622
|
// src/http-proxy/ca.ts
|
|
13176
13623
|
import { randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
|
|
13177
13624
|
import {
|
|
13178
|
-
chmodSync as
|
|
13625
|
+
chmodSync as chmodSync3,
|
|
13179
13626
|
existsSync as existsSync13,
|
|
13180
|
-
mkdirSync as
|
|
13181
|
-
readFileSync as
|
|
13627
|
+
mkdirSync as mkdirSync10,
|
|
13628
|
+
readFileSync as readFileSync9,
|
|
13182
13629
|
readdirSync as readdirSync3,
|
|
13183
13630
|
rmSync as rmSync6,
|
|
13184
13631
|
statSync as statSync3,
|
|
13185
|
-
writeFileSync as
|
|
13632
|
+
writeFileSync as writeFileSync9
|
|
13186
13633
|
} from "fs";
|
|
13187
|
-
import { dirname as dirname6, join as
|
|
13634
|
+
import { dirname as dirname6, join as join18, resolve } from "path";
|
|
13188
13635
|
import forge from "node-forge";
|
|
13189
13636
|
var SESSION_ROOT = "http-proxy-sessions";
|
|
13190
13637
|
var OWNER_FILE = "owner.pid";
|
|
@@ -13204,22 +13651,22 @@ function processIsRunning(pid) {
|
|
|
13204
13651
|
}
|
|
13205
13652
|
}
|
|
13206
13653
|
function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
13207
|
-
const root =
|
|
13654
|
+
const root = join18(appHome, SESSION_ROOT);
|
|
13208
13655
|
if (!existsSync13(root)) return;
|
|
13209
13656
|
const now = Date.now();
|
|
13210
13657
|
for (const name of readdirSync3(root)) {
|
|
13211
|
-
const sessionDir =
|
|
13658
|
+
const sessionDir = join18(root, name);
|
|
13212
13659
|
try {
|
|
13213
13660
|
const stat = statSync3(sessionDir);
|
|
13214
13661
|
if (!stat.isDirectory()) continue;
|
|
13215
|
-
const ownerPath =
|
|
13662
|
+
const ownerPath = join18(sessionDir, OWNER_FILE);
|
|
13216
13663
|
if (!existsSync13(ownerPath)) {
|
|
13217
13664
|
if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
|
|
13218
13665
|
rmSync6(sessionDir, { recursive: true, force: true });
|
|
13219
13666
|
}
|
|
13220
13667
|
continue;
|
|
13221
13668
|
}
|
|
13222
|
-
const pid = Number(
|
|
13669
|
+
const pid = Number(readFileSync9(ownerPath, "utf8").trim());
|
|
13223
13670
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
13224
13671
|
const ownerStat = statSync3(ownerPath);
|
|
13225
13672
|
const newestMtimeMs = Math.max(stat.mtimeMs, ownerStat.mtimeMs);
|
|
@@ -13235,13 +13682,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
|
13235
13682
|
}
|
|
13236
13683
|
function createHttpProxyCertificates(appHome = getAppHome()) {
|
|
13237
13684
|
cleanupStaleHttpProxySessions(appHome);
|
|
13238
|
-
const root =
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
const sessionDir =
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13685
|
+
const root = join18(appHome, SESSION_ROOT);
|
|
13686
|
+
mkdirSync10(root, { recursive: true, mode: 448 });
|
|
13687
|
+
chmodSync3(root, 448);
|
|
13688
|
+
const sessionDir = join18(root, randomUUID4());
|
|
13689
|
+
mkdirSync10(sessionDir, { mode: 448 });
|
|
13690
|
+
chmodSync3(sessionDir, 448);
|
|
13691
|
+
writeFileSync9(join18(sessionDir, OWNER_FILE), `${process.pid}
|
|
13245
13692
|
`, { mode: 384 });
|
|
13246
13693
|
try {
|
|
13247
13694
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
@@ -13276,9 +13723,9 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
|
|
|
13276
13723
|
]);
|
|
13277
13724
|
server.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
13278
13725
|
const caCert = forge.pki.certificateToPem(ca);
|
|
13279
|
-
const caCertPath =
|
|
13280
|
-
|
|
13281
|
-
|
|
13726
|
+
const caCertPath = join18(sessionDir, "relay-ai-ca.pem");
|
|
13727
|
+
writeFileSync9(caCertPath, caCert, { encoding: "utf8", mode: 384 });
|
|
13728
|
+
chmodSync3(caCertPath, 384);
|
|
13282
13729
|
let cleaned = false;
|
|
13283
13730
|
const cleanupOnExit = () => {
|
|
13284
13731
|
if (cleaned) return;
|
|
@@ -13319,18 +13766,18 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
|
|
|
13319
13766
|
if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) {
|
|
13320
13767
|
return relayCaCertPath;
|
|
13321
13768
|
}
|
|
13322
|
-
const relayCa =
|
|
13323
|
-
const additionalCa =
|
|
13769
|
+
const relayCa = readFileSync9(relayCaCertPath, "utf8").trimEnd();
|
|
13770
|
+
const additionalCa = readFileSync9(additionalCaCertPath, "utf8").trim();
|
|
13324
13771
|
if (!additionalCa) return relayCaCertPath;
|
|
13325
|
-
const combinedPath =
|
|
13326
|
-
|
|
13772
|
+
const combinedPath = join18(dirname6(relayCaCertPath), "combined-ca.pem");
|
|
13773
|
+
writeFileSync9(
|
|
13327
13774
|
combinedPath,
|
|
13328
13775
|
`${relayCa}
|
|
13329
13776
|
${additionalCa}
|
|
13330
13777
|
`,
|
|
13331
13778
|
{ encoding: "utf8", mode: 384 }
|
|
13332
13779
|
);
|
|
13333
|
-
|
|
13780
|
+
chmodSync3(combinedPath, 384);
|
|
13334
13781
|
return combinedPath;
|
|
13335
13782
|
}
|
|
13336
13783
|
|
|
@@ -15345,7 +15792,7 @@ Options:
|
|
|
15345
15792
|
--trace Write debug logs under ~/.relay-ai/logs/`);
|
|
15346
15793
|
return 0;
|
|
15347
15794
|
}
|
|
15348
|
-
const { runUiCommand } = await import("./ui-command-
|
|
15795
|
+
const { runUiCommand } = await import("./ui-command-NM4MNZLO.js");
|
|
15349
15796
|
return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
|
|
15350
15797
|
}
|
|
15351
15798
|
if (parsed.command === "models") {
|