codex-team 0.0.9 → 0.0.11
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 +2 -0
- package/dist/cli.cjs +434 -37
- package/dist/main.cjs +434 -37
- package/dist/main.js +432 -35
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -35,7 +35,9 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
35
35
|
__webpack_require__.d(__webpack_exports__, {
|
|
36
36
|
runCli: ()=>runCli
|
|
37
37
|
});
|
|
38
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
38
39
|
const external_node_process_namespaceObject = require("node:process");
|
|
40
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
39
41
|
const external_dayjs_namespaceObject = require("dayjs");
|
|
40
42
|
var external_dayjs_default = /*#__PURE__*/ __webpack_require__.n(external_dayjs_namespaceObject);
|
|
41
43
|
const timezone_js_namespaceObject = require("dayjs/plugin/timezone.js");
|
|
@@ -43,10 +45,9 @@ var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namesp
|
|
|
43
45
|
const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
|
|
44
46
|
var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
|
|
45
47
|
var package_namespaceObject = {
|
|
46
|
-
rE: "0.0.
|
|
48
|
+
rE: "0.0.11"
|
|
47
49
|
};
|
|
48
50
|
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
49
|
-
const promises_namespaceObject = require("node:fs/promises");
|
|
50
51
|
function isRecord(value) {
|
|
51
52
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
52
53
|
}
|
|
@@ -251,11 +252,11 @@ function decodeJwtPayload(token) {
|
|
|
251
252
|
return parsed;
|
|
252
253
|
}
|
|
253
254
|
const external_node_os_namespaceObject = require("node:os");
|
|
254
|
-
const external_node_path_namespaceObject = require("node:path");
|
|
255
|
-
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
256
|
-
const external_node_util_namespaceObject = require("node:util");
|
|
257
255
|
const DEFAULT_CHATGPT_BASE_URL = "https://chatgpt.com";
|
|
258
256
|
const USER_AGENT = "codexm/0.1";
|
|
257
|
+
const USAGE_FETCH_ATTEMPTS = 3;
|
|
258
|
+
const USAGE_FETCH_RETRY_DELAY_MS = 250;
|
|
259
|
+
const USAGE_FETCH_TIMEOUT_MS = 15000;
|
|
259
260
|
function quota_client_isRecord(value) {
|
|
260
261
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
261
262
|
}
|
|
@@ -353,8 +354,6 @@ async function resolveUsageUrls(homeDir) {
|
|
|
353
354
|
const normalizedBaseUrl = baseUrl.replace(/\/+$/u, "");
|
|
354
355
|
const candidates = [
|
|
355
356
|
`${normalizedBaseUrl}/backend-api/wham/usage`,
|
|
356
|
-
`${normalizedBaseUrl}/wham/usage`,
|
|
357
|
-
`${normalizedBaseUrl}/api/codex/usage`,
|
|
358
357
|
"https://chatgpt.com/backend-api/wham/usage"
|
|
359
358
|
];
|
|
360
359
|
return [
|
|
@@ -364,6 +363,19 @@ async function resolveUsageUrls(homeDir) {
|
|
|
364
363
|
function normalizeFetchError(error) {
|
|
365
364
|
return error instanceof Error ? error.message : String(error);
|
|
366
365
|
}
|
|
366
|
+
async function delay(milliseconds) {
|
|
367
|
+
await new Promise((resolve)=>setTimeout(resolve, milliseconds));
|
|
368
|
+
}
|
|
369
|
+
function isTransientUsageStatus(status) {
|
|
370
|
+
return 408 === status || 429 === status || status >= 500 && status <= 599;
|
|
371
|
+
}
|
|
372
|
+
function isLastAttempt(attempt) {
|
|
373
|
+
return attempt >= USAGE_FETCH_ATTEMPTS;
|
|
374
|
+
}
|
|
375
|
+
function formatUsageAttemptError(url, attempt, message) {
|
|
376
|
+
const retryContext = USAGE_FETCH_ATTEMPTS > 1 ? ` attempt ${attempt}/${USAGE_FETCH_ATTEMPTS}` : "";
|
|
377
|
+
return `${url}${retryContext} -> ${message}`;
|
|
378
|
+
}
|
|
367
379
|
function shouldRetryWithTokenRefresh(message) {
|
|
368
380
|
const normalized = message.toLowerCase();
|
|
369
381
|
return normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("invalid_token") || normalized.includes("deactivated_workspace");
|
|
@@ -419,7 +431,9 @@ async function requestUsage(snapshot, options) {
|
|
|
419
431
|
const urls = await resolveUsageUrls(options.homeDir);
|
|
420
432
|
const now = (options.now ?? new Date()).toISOString();
|
|
421
433
|
const errors = [];
|
|
422
|
-
for (const url of urls){
|
|
434
|
+
for (const url of urls)for(let attempt = 1; attempt <= USAGE_FETCH_ATTEMPTS; attempt += 1){
|
|
435
|
+
const abortController = new AbortController();
|
|
436
|
+
const timeout = setTimeout(()=>abortController.abort(), USAGE_FETCH_TIMEOUT_MS);
|
|
423
437
|
let response;
|
|
424
438
|
try {
|
|
425
439
|
response = await fetchImpl(url, {
|
|
@@ -429,23 +443,35 @@ async function requestUsage(snapshot, options) {
|
|
|
429
443
|
"ChatGPT-Account-Id": extracted.accountId,
|
|
430
444
|
Accept: "application/json",
|
|
431
445
|
"User-Agent": USER_AGENT
|
|
432
|
-
}
|
|
446
|
+
},
|
|
447
|
+
signal: abortController.signal
|
|
433
448
|
});
|
|
434
449
|
} catch (error) {
|
|
435
|
-
|
|
436
|
-
|
|
450
|
+
clearTimeout(timeout);
|
|
451
|
+
const message = abortController.signal.aborted ? `timed out after ${USAGE_FETCH_TIMEOUT_MS}ms` : normalizeFetchError(error);
|
|
452
|
+
errors.push(formatUsageAttemptError(url, attempt, message));
|
|
453
|
+
if (!isLastAttempt(attempt)) {
|
|
454
|
+
await delay(USAGE_FETCH_RETRY_DELAY_MS * attempt);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
437
458
|
}
|
|
459
|
+
clearTimeout(timeout);
|
|
438
460
|
if (!response.ok) {
|
|
439
461
|
const body = await response.text();
|
|
440
|
-
errors.push(
|
|
441
|
-
|
|
462
|
+
errors.push(formatUsageAttemptError(url, attempt, `${response.status}: ${body.slice(0, 140).replace(/\s+/gu, " ").trim()}`));
|
|
463
|
+
if (isTransientUsageStatus(response.status) && !isLastAttempt(attempt)) {
|
|
464
|
+
await delay(USAGE_FETCH_RETRY_DELAY_MS * attempt);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
442
468
|
}
|
|
443
469
|
let payload;
|
|
444
470
|
try {
|
|
445
471
|
payload = await response.json();
|
|
446
472
|
} catch (error) {
|
|
447
|
-
errors.push(
|
|
448
|
-
|
|
473
|
+
errors.push(formatUsageAttemptError(url, attempt, `failed to parse JSON: ${normalizeFetchError(error)}`));
|
|
474
|
+
break;
|
|
449
475
|
}
|
|
450
476
|
return mapUsagePayload(payload, extracted.planType, now);
|
|
451
477
|
}
|
|
@@ -514,7 +540,6 @@ async function fetchQuotaSnapshot(snapshot, options = {}) {
|
|
|
514
540
|
};
|
|
515
541
|
}
|
|
516
542
|
}
|
|
517
|
-
const execFile = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
518
543
|
const DIRECTORY_MODE = 448;
|
|
519
544
|
const FILE_MODE = 384;
|
|
520
545
|
const SCHEMA_VERSION = 1;
|
|
@@ -587,25 +612,6 @@ function canAutoMigrateLegacyChatGPTMeta(meta, snapshot) {
|
|
|
587
612
|
if (!snapshotUserId) return false;
|
|
588
613
|
return meta.account_id === getSnapshotAccountId(snapshot);
|
|
589
614
|
}
|
|
590
|
-
async function detectRunningCodexProcesses() {
|
|
591
|
-
try {
|
|
592
|
-
const { stdout } = await execFile("ps", [
|
|
593
|
-
"-Ao",
|
|
594
|
-
"pid=,command="
|
|
595
|
-
]);
|
|
596
|
-
const pids = [];
|
|
597
|
-
for (const line of stdout.split("\n")){
|
|
598
|
-
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
599
|
-
if (!match) continue;
|
|
600
|
-
const pid = Number(match[1]);
|
|
601
|
-
const command = match[2];
|
|
602
|
-
if (pid !== process.pid && /(^|\s|\/)codex(\s|$)/.test(command) && !command.includes("codex-team")) pids.push(pid);
|
|
603
|
-
}
|
|
604
|
-
return pids;
|
|
605
|
-
} catch {
|
|
606
|
-
return [];
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
615
|
class AccountStore {
|
|
610
616
|
paths;
|
|
611
617
|
fetchImpl;
|
|
@@ -901,8 +907,6 @@ class AccountStore {
|
|
|
901
907
|
last_switched_account: name,
|
|
902
908
|
last_backup_path: backupPath
|
|
903
909
|
});
|
|
904
|
-
const runningCodexPids = await detectRunningCodexProcesses();
|
|
905
|
-
if (runningCodexPids.length > 0) warnings.push(`Detected running codex processes (${runningCodexPids.join(", ")}). Existing sessions may still hold the previous login state.`);
|
|
906
910
|
return {
|
|
907
911
|
account: await this.readManagedAccount(name),
|
|
908
912
|
warnings,
|
|
@@ -1076,6 +1080,252 @@ function createAccountStore(homeDir, options) {
|
|
|
1076
1080
|
fetchImpl: options?.fetchImpl
|
|
1077
1081
|
});
|
|
1078
1082
|
}
|
|
1083
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
1084
|
+
const external_node_util_namespaceObject = require("node:util");
|
|
1085
|
+
const execFile = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
1086
|
+
const DEFAULT_CODEX_REMOTE_DEBUGGING_PORT = 9223;
|
|
1087
|
+
const DEFAULT_CODEX_DESKTOP_STATE_PATH = (0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.homedir)(), ".codex-team", "desktop-state.json");
|
|
1088
|
+
const CODEX_BINARY_SUFFIX = "/Contents/MacOS/Codex";
|
|
1089
|
+
const CODEX_APP_NAME = "Codex";
|
|
1090
|
+
const CODEX_LOCAL_HOST_ID = "local";
|
|
1091
|
+
const CODEX_APP_SERVER_RESTART_EXPRESSION = 'window.electronBridge.sendMessageFromView({ type: "codex-app-server-restart", hostId: "local" })';
|
|
1092
|
+
const DEVTOOLS_REQUEST_TIMEOUT_MS = 5000;
|
|
1093
|
+
function codex_desktop_launch_isRecord(value) {
|
|
1094
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
1095
|
+
}
|
|
1096
|
+
function isNonEmptyString(value) {
|
|
1097
|
+
return "string" == typeof value && "" !== value.trim();
|
|
1098
|
+
}
|
|
1099
|
+
async function codex_desktop_launch_delay(ms) {
|
|
1100
|
+
await new Promise((resolve)=>setTimeout(resolve, ms));
|
|
1101
|
+
}
|
|
1102
|
+
async function pathExistsViaStat(execFileImpl, path) {
|
|
1103
|
+
try {
|
|
1104
|
+
await execFileImpl("stat", [
|
|
1105
|
+
"-f",
|
|
1106
|
+
"%N",
|
|
1107
|
+
path
|
|
1108
|
+
]);
|
|
1109
|
+
return true;
|
|
1110
|
+
} catch {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
function parseManagedState(raw) {
|
|
1115
|
+
if ("" === raw.trim()) return null;
|
|
1116
|
+
let parsed;
|
|
1117
|
+
try {
|
|
1118
|
+
parsed = JSON.parse(raw);
|
|
1119
|
+
} catch {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
if (!codex_desktop_launch_isRecord(parsed)) return null;
|
|
1123
|
+
const pid = parsed.pid;
|
|
1124
|
+
const appPath = parsed.app_path;
|
|
1125
|
+
const remoteDebuggingPort = parsed.remote_debugging_port;
|
|
1126
|
+
const managedByCodexm = parsed.managed_by_codexm;
|
|
1127
|
+
const startedAt = parsed.started_at;
|
|
1128
|
+
if ("number" != typeof pid || !Number.isInteger(pid) || pid <= 0 || !isNonEmptyString(appPath) || "number" != typeof remoteDebuggingPort || !Number.isInteger(remoteDebuggingPort) || remoteDebuggingPort <= 0 || true !== managedByCodexm || !isNonEmptyString(startedAt)) return null;
|
|
1129
|
+
return {
|
|
1130
|
+
pid,
|
|
1131
|
+
app_path: appPath,
|
|
1132
|
+
remote_debugging_port: remoteDebuggingPort,
|
|
1133
|
+
managed_by_codexm: true,
|
|
1134
|
+
started_at: startedAt
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
async function ensureStateDirectory(statePath) {
|
|
1138
|
+
await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(statePath), {
|
|
1139
|
+
recursive: true,
|
|
1140
|
+
mode: 448
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
function createDefaultWebSocket(url) {
|
|
1144
|
+
return new WebSocket(url);
|
|
1145
|
+
}
|
|
1146
|
+
function isDevtoolsTarget(value) {
|
|
1147
|
+
return codex_desktop_launch_isRecord(value);
|
|
1148
|
+
}
|
|
1149
|
+
function isManagedDesktopProcess(runningApps, state) {
|
|
1150
|
+
const expectedBinaryPath = `${state.app_path}${CODEX_BINARY_SUFFIX}`;
|
|
1151
|
+
const expectedPort = `--remote-debugging-port=${state.remote_debugging_port}`;
|
|
1152
|
+
return runningApps.some((entry)=>entry.pid === state.pid && entry.command.includes(expectedBinaryPath) && entry.command.includes(expectedPort));
|
|
1153
|
+
}
|
|
1154
|
+
async function evaluateDevtoolsExpression(createWebSocketImpl, webSocketDebuggerUrl, expression) {
|
|
1155
|
+
const socket = createWebSocketImpl(webSocketDebuggerUrl);
|
|
1156
|
+
await new Promise((resolve, reject)=>{
|
|
1157
|
+
const requestId = 1;
|
|
1158
|
+
const timeout = setTimeout(()=>{
|
|
1159
|
+
cleanup();
|
|
1160
|
+
reject(new Error("Timed out waiting for Codex Desktop devtools response."));
|
|
1161
|
+
}, DEVTOOLS_REQUEST_TIMEOUT_MS);
|
|
1162
|
+
const cleanup = ()=>{
|
|
1163
|
+
clearTimeout(timeout);
|
|
1164
|
+
socket.onopen = null;
|
|
1165
|
+
socket.onmessage = null;
|
|
1166
|
+
socket.onerror = null;
|
|
1167
|
+
socket.onclose = null;
|
|
1168
|
+
socket.close();
|
|
1169
|
+
};
|
|
1170
|
+
socket.onopen = ()=>{
|
|
1171
|
+
socket.send(JSON.stringify({
|
|
1172
|
+
id: requestId,
|
|
1173
|
+
method: "Runtime.evaluate",
|
|
1174
|
+
params: {
|
|
1175
|
+
expression,
|
|
1176
|
+
awaitPromise: true
|
|
1177
|
+
}
|
|
1178
|
+
}));
|
|
1179
|
+
};
|
|
1180
|
+
socket.onmessage = (event)=>{
|
|
1181
|
+
if ("string" != typeof event.data) return;
|
|
1182
|
+
let payload;
|
|
1183
|
+
try {
|
|
1184
|
+
payload = JSON.parse(event.data);
|
|
1185
|
+
} catch {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (!codex_desktop_launch_isRecord(payload) || payload.id !== requestId) return;
|
|
1189
|
+
if (codex_desktop_launch_isRecord(payload.error)) {
|
|
1190
|
+
cleanup();
|
|
1191
|
+
reject(new Error(String(payload.error.message ?? "Codex Desktop devtools request failed.")));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const result = codex_desktop_launch_isRecord(payload.result) ? payload.result : null;
|
|
1195
|
+
if (result && codex_desktop_launch_isRecord(result.exceptionDetails)) {
|
|
1196
|
+
cleanup();
|
|
1197
|
+
reject(new Error("Codex Desktop rejected the app-server restart request."));
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
cleanup();
|
|
1201
|
+
resolve();
|
|
1202
|
+
};
|
|
1203
|
+
socket.onerror = ()=>{
|
|
1204
|
+
cleanup();
|
|
1205
|
+
reject(new Error("Failed to communicate with Codex Desktop devtools."));
|
|
1206
|
+
};
|
|
1207
|
+
socket.onclose = ()=>{
|
|
1208
|
+
cleanup();
|
|
1209
|
+
reject(new Error("Codex Desktop devtools connection closed before replying."));
|
|
1210
|
+
};
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function createCodexDesktopLauncher(options = {}) {
|
|
1214
|
+
const execFileImpl = options.execFileImpl ?? execFile;
|
|
1215
|
+
const statePath = options.statePath ?? DEFAULT_CODEX_DESKTOP_STATE_PATH;
|
|
1216
|
+
const readFileImpl = options.readFileImpl ?? (async (path)=>(0, promises_namespaceObject.readFile)(path, "utf8"));
|
|
1217
|
+
const writeFileImpl = options.writeFileImpl ?? promises_namespaceObject.writeFile;
|
|
1218
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1219
|
+
const createWebSocketImpl = options.createWebSocketImpl ?? createDefaultWebSocket;
|
|
1220
|
+
async function findInstalledApp() {
|
|
1221
|
+
const candidates = [
|
|
1222
|
+
"/Applications/Codex.app",
|
|
1223
|
+
(0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.homedir)(), "Applications", "Codex.app")
|
|
1224
|
+
];
|
|
1225
|
+
for (const candidate of candidates)if (await pathExistsViaStat(execFileImpl, candidate)) return candidate;
|
|
1226
|
+
try {
|
|
1227
|
+
const { stdout } = await execFileImpl("mdfind", [
|
|
1228
|
+
'kMDItemFSName == "Codex.app"'
|
|
1229
|
+
]);
|
|
1230
|
+
for (const line of stdout.split("\n")){
|
|
1231
|
+
const candidate = line.trim();
|
|
1232
|
+
if ("" !== candidate) {
|
|
1233
|
+
if (await pathExistsViaStat(execFileImpl, candidate)) return candidate;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} catch {}
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
async function listRunningApps() {
|
|
1240
|
+
const { stdout } = await execFileImpl("ps", [
|
|
1241
|
+
"-Ao",
|
|
1242
|
+
"pid=,command="
|
|
1243
|
+
]);
|
|
1244
|
+
const running = [];
|
|
1245
|
+
for (const line of stdout.split("\n")){
|
|
1246
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
1247
|
+
if (!match) continue;
|
|
1248
|
+
const pid = Number(match[1]);
|
|
1249
|
+
const command = match[2];
|
|
1250
|
+
if (pid !== process.pid && command.includes(CODEX_BINARY_SUFFIX)) running.push({
|
|
1251
|
+
pid,
|
|
1252
|
+
command
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
return running;
|
|
1256
|
+
}
|
|
1257
|
+
async function quitRunningApps() {
|
|
1258
|
+
const running = await listRunningApps();
|
|
1259
|
+
if (0 === running.length) return;
|
|
1260
|
+
await execFileImpl("osascript", [
|
|
1261
|
+
"-e",
|
|
1262
|
+
`tell application "${CODEX_APP_NAME}" to quit`
|
|
1263
|
+
]);
|
|
1264
|
+
for(let attempt = 0; attempt < 10; attempt += 1){
|
|
1265
|
+
const remaining = await listRunningApps();
|
|
1266
|
+
if (0 === remaining.length) return;
|
|
1267
|
+
await codex_desktop_launch_delay(300);
|
|
1268
|
+
}
|
|
1269
|
+
throw new Error("Timed out waiting for Codex Desktop to quit.");
|
|
1270
|
+
}
|
|
1271
|
+
async function launch(appPath) {
|
|
1272
|
+
await execFileImpl("open", [
|
|
1273
|
+
"-na",
|
|
1274
|
+
appPath,
|
|
1275
|
+
"--args",
|
|
1276
|
+
`--remote-debugging-port=${DEFAULT_CODEX_REMOTE_DEBUGGING_PORT}`
|
|
1277
|
+
]);
|
|
1278
|
+
}
|
|
1279
|
+
async function readManagedState() {
|
|
1280
|
+
try {
|
|
1281
|
+
return parseManagedState(await readFileImpl(statePath));
|
|
1282
|
+
} catch {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async function writeManagedState(state) {
|
|
1287
|
+
await ensureStateDirectory(statePath);
|
|
1288
|
+
await writeFileImpl(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1289
|
+
}
|
|
1290
|
+
async function clearManagedState() {
|
|
1291
|
+
await ensureStateDirectory(statePath);
|
|
1292
|
+
await writeFileImpl(statePath, "");
|
|
1293
|
+
}
|
|
1294
|
+
async function isManagedDesktopRunning() {
|
|
1295
|
+
const state = await readManagedState();
|
|
1296
|
+
if (!state) return false;
|
|
1297
|
+
const runningApps = await listRunningApps();
|
|
1298
|
+
return isManagedDesktopProcess(runningApps, state);
|
|
1299
|
+
}
|
|
1300
|
+
async function restartManagedAppServer() {
|
|
1301
|
+
const state = await readManagedState();
|
|
1302
|
+
if (!state) return false;
|
|
1303
|
+
const runningApps = await listRunningApps();
|
|
1304
|
+
if (!isManagedDesktopProcess(runningApps, state)) return false;
|
|
1305
|
+
const response = await fetchImpl(`http://127.0.0.1:${state.remote_debugging_port}/json/list`);
|
|
1306
|
+
if (!response.ok) throw new Error(`Failed to query Codex Desktop devtools targets (HTTP ${response.status}).`);
|
|
1307
|
+
const targets = await response.json();
|
|
1308
|
+
if (!Array.isArray(targets)) throw new Error("Codex Desktop devtools target list was not an array.");
|
|
1309
|
+
const localTarget = targets.find((target)=>{
|
|
1310
|
+
if (!isDevtoolsTarget(target)) return false;
|
|
1311
|
+
return "page" === target.type && target.url === `app://-/index.html?hostId=${CODEX_LOCAL_HOST_ID}` && isNonEmptyString(target.webSocketDebuggerUrl);
|
|
1312
|
+
});
|
|
1313
|
+
if (!localTarget || !isNonEmptyString(localTarget.webSocketDebuggerUrl)) throw new Error("Could not find the local Codex Desktop devtools target.");
|
|
1314
|
+
await evaluateDevtoolsExpression(createWebSocketImpl, localTarget.webSocketDebuggerUrl, CODEX_APP_SERVER_RESTART_EXPRESSION);
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
findInstalledApp,
|
|
1319
|
+
listRunningApps,
|
|
1320
|
+
quitRunningApps,
|
|
1321
|
+
launch,
|
|
1322
|
+
readManagedState,
|
|
1323
|
+
writeManagedState,
|
|
1324
|
+
clearManagedState,
|
|
1325
|
+
isManagedDesktopRunning,
|
|
1326
|
+
restartManagedAppServer
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1079
1329
|
external_dayjs_default().extend(utc_js_default());
|
|
1080
1330
|
external_dayjs_default().extend(timezone_js_default());
|
|
1081
1331
|
function parseArgs(argv) {
|
|
@@ -1119,12 +1369,29 @@ Usage:
|
|
|
1119
1369
|
codexm update [--json]
|
|
1120
1370
|
codexm switch <name> [--json]
|
|
1121
1371
|
codexm switch --auto [--dry-run] [--json]
|
|
1372
|
+
codexm launch [name] [--json]
|
|
1122
1373
|
codexm remove <name> [--yes] [--json]
|
|
1123
1374
|
codexm rename <old> <new> [--json]
|
|
1124
1375
|
|
|
1125
1376
|
Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
1126
1377
|
`);
|
|
1127
1378
|
}
|
|
1379
|
+
function stripManagedDesktopWarning(warnings) {
|
|
1380
|
+
return warnings.filter((warning)=>!warning.startsWith("Detected running codex processes (") || !warning.endsWith("Existing sessions may still hold the previous login state."));
|
|
1381
|
+
}
|
|
1382
|
+
async function refreshManagedDesktopAfterSwitch(warnings, desktopLauncher) {
|
|
1383
|
+
try {
|
|
1384
|
+
if (await desktopLauncher.restartManagedAppServer()) return;
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
warnings.push(`Failed to refresh the running codexm-managed Codex Desktop session: ${error.message}`);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const runningApps = await desktopLauncher.listRunningApps();
|
|
1391
|
+
if (0 === runningApps.length) return;
|
|
1392
|
+
warnings.push(`Detected running codex processes (${runningApps.map((app)=>app.pid).join(", ")}). Existing sessions may still hold the previous login state.`);
|
|
1393
|
+
} catch {}
|
|
1394
|
+
}
|
|
1128
1395
|
function describeCurrentStatus(status) {
|
|
1129
1396
|
const lines = [];
|
|
1130
1397
|
if (status.exists) {
|
|
@@ -1319,6 +1586,67 @@ async function confirmRemoval(name, streams) {
|
|
|
1319
1586
|
streams.stdin.on("data", onData);
|
|
1320
1587
|
});
|
|
1321
1588
|
}
|
|
1589
|
+
async function confirmDesktopRelaunch(streams) {
|
|
1590
|
+
if (!streams.stdin.isTTY) throw new Error("Refusing to relaunch Codex Desktop in a non-interactive terminal.");
|
|
1591
|
+
streams.stdout.write("Codex Desktop is already running. Close it and relaunch with the selected auth? [y/N] ");
|
|
1592
|
+
return await new Promise((resolve)=>{
|
|
1593
|
+
const cleanup = ()=>{
|
|
1594
|
+
streams.stdin.off("data", onData);
|
|
1595
|
+
streams.stdin.pause();
|
|
1596
|
+
};
|
|
1597
|
+
const onData = (buffer)=>{
|
|
1598
|
+
const answer = buffer.toString("utf8").trim().toLowerCase();
|
|
1599
|
+
cleanup();
|
|
1600
|
+
streams.stdout.write("\n");
|
|
1601
|
+
resolve("y" === answer || "yes" === answer);
|
|
1602
|
+
};
|
|
1603
|
+
streams.stdin.resume();
|
|
1604
|
+
streams.stdin.on("data", onData);
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
async function sleep(ms) {
|
|
1608
|
+
await new Promise((resolve)=>setTimeout(resolve, ms));
|
|
1609
|
+
}
|
|
1610
|
+
async function main_pathExists(path) {
|
|
1611
|
+
try {
|
|
1612
|
+
await (0, promises_namespaceObject.stat)(path);
|
|
1613
|
+
return true;
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
const nodeError = error;
|
|
1616
|
+
if ("ENOENT" === nodeError.code) return false;
|
|
1617
|
+
throw error;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
function isRunningDesktopFromApp(app, appPath) {
|
|
1621
|
+
return app.command.includes(`${appPath}/Contents/MacOS/Codex`);
|
|
1622
|
+
}
|
|
1623
|
+
async function resolveManagedDesktopState(desktopLauncher, appPath, existingApps) {
|
|
1624
|
+
const existingPids = new Set(existingApps.map((app)=>app.pid));
|
|
1625
|
+
for(let attempt = 0; attempt < 10; attempt += 1){
|
|
1626
|
+
const runningApps = await desktopLauncher.listRunningApps();
|
|
1627
|
+
const launchedApp = runningApps.filter((app)=>isRunningDesktopFromApp(app, appPath) && !existingPids.has(app.pid)).sort((left, right)=>right.pid - left.pid)[0] ?? runningApps.filter((app)=>isRunningDesktopFromApp(app, appPath)).sort((left, right)=>right.pid - left.pid)[0] ?? null;
|
|
1628
|
+
if (launchedApp) return {
|
|
1629
|
+
pid: launchedApp.pid,
|
|
1630
|
+
app_path: appPath,
|
|
1631
|
+
remote_debugging_port: DEFAULT_CODEX_REMOTE_DEBUGGING_PORT,
|
|
1632
|
+
managed_by_codexm: true,
|
|
1633
|
+
started_at: new Date().toISOString()
|
|
1634
|
+
};
|
|
1635
|
+
await sleep(300);
|
|
1636
|
+
}
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
async function restoreLaunchBackup(store, backupPath) {
|
|
1640
|
+
if (backupPath && await main_pathExists(backupPath)) await (0, promises_namespaceObject.copyFile)(backupPath, store.paths.currentAuthPath);
|
|
1641
|
+
else await (0, promises_namespaceObject.rm)(store.paths.currentAuthPath, {
|
|
1642
|
+
force: true
|
|
1643
|
+
});
|
|
1644
|
+
const configBackupPath = (0, external_node_path_namespaceObject.join)(store.paths.backupsDir, "last-active-config.toml");
|
|
1645
|
+
if (await main_pathExists(configBackupPath)) await (0, promises_namespaceObject.copyFile)(configBackupPath, store.paths.currentConfigPath);
|
|
1646
|
+
else await (0, promises_namespaceObject.rm)(store.paths.currentConfigPath, {
|
|
1647
|
+
force: true
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1322
1650
|
async function runCli(argv, options = {}) {
|
|
1323
1651
|
const streams = {
|
|
1324
1652
|
stdin: options.stdin ?? external_node_process_namespaceObject.stdin,
|
|
@@ -1326,6 +1654,7 @@ async function runCli(argv, options = {}) {
|
|
|
1326
1654
|
stderr: options.stderr ?? external_node_process_namespaceObject.stderr
|
|
1327
1655
|
};
|
|
1328
1656
|
const store = options.store ?? createAccountStore();
|
|
1657
|
+
const desktopLauncher = options.desktopLauncher ?? createCodexDesktopLauncher();
|
|
1329
1658
|
const parsed = parseArgs(argv);
|
|
1330
1659
|
const json = parsed.flags.has("--json");
|
|
1331
1660
|
try {
|
|
@@ -1458,6 +1787,8 @@ async function runCli(argv, options = {}) {
|
|
|
1458
1787
|
}
|
|
1459
1788
|
const result = await store.switchAccount(selected.name);
|
|
1460
1789
|
for (const warning of warnings)result.warnings.push(warning);
|
|
1790
|
+
result.warnings = stripManagedDesktopWarning(result.warnings);
|
|
1791
|
+
await refreshManagedDesktopAfterSwitch(result.warnings, desktopLauncher);
|
|
1461
1792
|
const payload = {
|
|
1462
1793
|
ok: true,
|
|
1463
1794
|
action: "switch",
|
|
@@ -1481,6 +1812,8 @@ async function runCli(argv, options = {}) {
|
|
|
1481
1812
|
}
|
|
1482
1813
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1483
1814
|
const result = await store.switchAccount(name);
|
|
1815
|
+
result.warnings = stripManagedDesktopWarning(result.warnings);
|
|
1816
|
+
await refreshManagedDesktopAfterSwitch(result.warnings, desktopLauncher);
|
|
1484
1817
|
let quota = null;
|
|
1485
1818
|
try {
|
|
1486
1819
|
await store.refreshQuotaForAccount(result.account.name);
|
|
@@ -1512,6 +1845,70 @@ async function runCli(argv, options = {}) {
|
|
|
1512
1845
|
}
|
|
1513
1846
|
return 0;
|
|
1514
1847
|
}
|
|
1848
|
+
case "launch":
|
|
1849
|
+
{
|
|
1850
|
+
const name = parsed.positionals[0] ?? null;
|
|
1851
|
+
if (parsed.positionals.length > 1) throw new Error("Usage: codexm launch [name] [--json]");
|
|
1852
|
+
const warnings = [];
|
|
1853
|
+
const appPath = await desktopLauncher.findInstalledApp();
|
|
1854
|
+
if (!appPath) throw new Error("Codex Desktop not found at /Applications/Codex.app.");
|
|
1855
|
+
const runningApps = await desktopLauncher.listRunningApps();
|
|
1856
|
+
if (runningApps.length > 0) {
|
|
1857
|
+
const confirmed = await confirmDesktopRelaunch(streams);
|
|
1858
|
+
if (!confirmed) {
|
|
1859
|
+
if (json) writeJson(streams.stdout, {
|
|
1860
|
+
ok: false,
|
|
1861
|
+
action: "launch",
|
|
1862
|
+
cancelled: true
|
|
1863
|
+
});
|
|
1864
|
+
else streams.stdout.write("Aborted.\n");
|
|
1865
|
+
return 1;
|
|
1866
|
+
}
|
|
1867
|
+
await desktopLauncher.quitRunningApps();
|
|
1868
|
+
}
|
|
1869
|
+
let switchedAccount = null;
|
|
1870
|
+
let switchBackupPath = null;
|
|
1871
|
+
if (name) {
|
|
1872
|
+
const switchResult = await store.switchAccount(name);
|
|
1873
|
+
warnings.push(...stripManagedDesktopWarning(switchResult.warnings));
|
|
1874
|
+
switchedAccount = switchResult.account;
|
|
1875
|
+
switchBackupPath = switchResult.backup_path;
|
|
1876
|
+
}
|
|
1877
|
+
try {
|
|
1878
|
+
await desktopLauncher.launch(appPath);
|
|
1879
|
+
const managedState = await resolveManagedDesktopState(desktopLauncher, appPath, runningApps);
|
|
1880
|
+
if (!managedState) {
|
|
1881
|
+
await desktopLauncher.clearManagedState().catch(()=>void 0);
|
|
1882
|
+
throw new Error("Failed to confirm the newly launched Codex Desktop process for managed-session tracking.");
|
|
1883
|
+
}
|
|
1884
|
+
await desktopLauncher.writeManagedState(managedState);
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
if (switchedAccount) await restoreLaunchBackup(store, switchBackupPath).catch(()=>void 0);
|
|
1887
|
+
throw error;
|
|
1888
|
+
}
|
|
1889
|
+
if (json) writeJson(streams.stdout, {
|
|
1890
|
+
ok: true,
|
|
1891
|
+
action: "launch",
|
|
1892
|
+
account: switchedAccount ? {
|
|
1893
|
+
name: switchedAccount.name,
|
|
1894
|
+
account_id: switchedAccount.account_id,
|
|
1895
|
+
user_id: switchedAccount.user_id ?? null,
|
|
1896
|
+
identity: switchedAccount.identity,
|
|
1897
|
+
auth_mode: switchedAccount.auth_mode
|
|
1898
|
+
} : null,
|
|
1899
|
+
launched_with_current_auth: null === switchedAccount,
|
|
1900
|
+
app_path: appPath,
|
|
1901
|
+
relaunched: runningApps.length > 0,
|
|
1902
|
+
warnings
|
|
1903
|
+
});
|
|
1904
|
+
else {
|
|
1905
|
+
if (switchedAccount) streams.stdout.write(`Switched to "${switchedAccount.name}" (${maskAccountId(switchedAccount.identity)}).\n`);
|
|
1906
|
+
if (runningApps.length > 0) streams.stdout.write("Closed existing Codex Desktop instance and launched a new one.\n");
|
|
1907
|
+
streams.stdout.write(switchedAccount ? `Launched Codex Desktop with "${switchedAccount.name}" (${maskAccountId(switchedAccount.identity)}).\n` : "Launched Codex Desktop with current auth.\n");
|
|
1908
|
+
for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
|
|
1909
|
+
}
|
|
1910
|
+
return 0;
|
|
1911
|
+
}
|
|
1515
1912
|
case "remove":
|
|
1516
1913
|
{
|
|
1517
1914
|
const name = parsed.positionals[0];
|