codexapp 0.1.44 → 0.1.46

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-cli/index.js CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
6
- import { readFile as readFile4 } from "fs/promises";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync4, mkdirSync } from "fs";
6
+ import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
7
7
  import { homedir as homedir3, networkInterfaces } from "os";
8
- import { join as join5 } from "path";
8
+ import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
9
9
  import { spawn as spawn3, spawnSync } from "child_process";
10
- import { createInterface } from "readline/promises";
10
+ import { createInterface as createInterface2 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
12
  import { dirname as dirname3 } from "path";
13
13
  import { get as httpsGet } from "https";
@@ -17,7 +17,7 @@ import qrcode from "qrcode-terminal";
17
17
  // src/server/httpServer.ts
18
18
  import { fileURLToPath } from "url";
19
19
  import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
20
- import { existsSync as existsSync2 } from "fs";
20
+ import { existsSync as existsSync3 } from "fs";
21
21
  import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
22
22
  import express from "express";
23
23
 
@@ -25,10 +25,13 @@ import express from "express";
25
25
  import { spawn as spawn2 } from "child_process";
26
26
  import { randomBytes } from "crypto";
27
27
  import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
28
+ import { createReadStream } from "fs";
29
+ import { request as httpRequest } from "http";
28
30
  import { request as httpsRequest } from "https";
29
31
  import { homedir as homedir2 } from "os";
30
32
  import { tmpdir as tmpdir2 } from "os";
31
33
  import { basename, isAbsolute, join as join2, resolve } from "path";
34
+ import { createInterface } from "readline";
32
35
  import { writeFile as writeFile2 } from "fs/promises";
33
36
 
34
37
  // src/server/skillsRoutes.ts
@@ -68,7 +71,7 @@ function getSkillsInstallDir() {
68
71
  return join(getCodexHomeDir(), "skills");
69
72
  }
70
73
  async function runCommand(command, args, options = {}) {
71
- await new Promise((resolve2, reject) => {
74
+ await new Promise((resolve3, reject) => {
72
75
  const proc = spawn(command, args, {
73
76
  cwd: options.cwd,
74
77
  env: process.env,
@@ -85,7 +88,7 @@ async function runCommand(command, args, options = {}) {
85
88
  proc.on("error", reject);
86
89
  proc.on("close", (code) => {
87
90
  if (code === 0) {
88
- resolve2();
91
+ resolve3();
89
92
  return;
90
93
  }
91
94
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -95,7 +98,7 @@ async function runCommand(command, args, options = {}) {
95
98
  });
96
99
  }
97
100
  async function runCommandWithOutput(command, args, options = {}) {
98
- return await new Promise((resolve2, reject) => {
101
+ return await new Promise((resolve3, reject) => {
99
102
  const proc = spawn(command, args, {
100
103
  cwd: options.cwd,
101
104
  env: process.env,
@@ -112,7 +115,7 @@ async function runCommandWithOutput(command, args, options = {}) {
112
115
  proc.on("error", reject);
113
116
  proc.on("close", (code) => {
114
117
  if (code === 0) {
115
- resolve2(stdout.trim());
118
+ resolve3(stdout.trim());
116
119
  return;
117
120
  }
118
121
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -157,9 +160,9 @@ async function getGhToken() {
157
160
  proc.stdout.on("data", (d) => {
158
161
  out += d.toString();
159
162
  });
160
- return new Promise((resolve2) => {
161
- proc.on("close", (code) => resolve2(code === 0 ? out.trim() : null));
162
- proc.on("error", () => resolve2(null));
163
+ return new Promise((resolve3) => {
164
+ proc.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
165
+ proc.on("error", () => resolve3(null));
163
166
  });
164
167
  } catch {
165
168
  return null;
@@ -398,7 +401,7 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
398
401
  ready = true;
399
402
  break;
400
403
  }
401
- await new Promise((resolve2) => setTimeout(resolve2, 1e3));
404
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
402
405
  }
403
406
  if (!ready) throw new Error("Private mirror repo was created but is not available yet");
404
407
  if (!created) return;
@@ -1193,7 +1196,7 @@ function scoreFileCandidate(path, query) {
1193
1196
  return 10;
1194
1197
  }
1195
1198
  async function listFilesWithRipgrep(cwd) {
1196
- return await new Promise((resolve2, reject) => {
1199
+ return await new Promise((resolve3, reject) => {
1197
1200
  const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1198
1201
  cwd,
1199
1202
  env: process.env,
@@ -1211,7 +1214,7 @@ async function listFilesWithRipgrep(cwd) {
1211
1214
  proc.on("close", (code) => {
1212
1215
  if (code === 0) {
1213
1216
  const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1214
- resolve2(rows);
1217
+ resolve3(rows);
1215
1218
  return;
1216
1219
  }
1217
1220
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -1224,7 +1227,7 @@ function getCodexHomeDir2() {
1224
1227
  return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1225
1228
  }
1226
1229
  async function runCommand2(command, args, options = {}) {
1227
- await new Promise((resolve2, reject) => {
1230
+ await new Promise((resolve3, reject) => {
1228
1231
  const proc = spawn2(command, args, {
1229
1232
  cwd: options.cwd,
1230
1233
  env: process.env,
@@ -1241,7 +1244,7 @@ async function runCommand2(command, args, options = {}) {
1241
1244
  proc.on("error", reject);
1242
1245
  proc.on("close", (code) => {
1243
1246
  if (code === 0) {
1244
- resolve2();
1247
+ resolve3();
1245
1248
  return;
1246
1249
  }
1247
1250
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -1273,7 +1276,7 @@ async function ensureRepoHasInitialCommit(repoRoot) {
1273
1276
  );
1274
1277
  }
1275
1278
  async function runCommandCapture(command, args, options = {}) {
1276
- return await new Promise((resolve2, reject) => {
1279
+ return await new Promise((resolve3, reject) => {
1277
1280
  const proc = spawn2(command, args, {
1278
1281
  cwd: options.cwd,
1279
1282
  env: process.env,
@@ -1290,7 +1293,7 @@ async function runCommandCapture(command, args, options = {}) {
1290
1293
  proc.on("error", reject);
1291
1294
  proc.on("close", (code) => {
1292
1295
  if (code === 0) {
1293
- resolve2(stdout.trim());
1296
+ resolve3(stdout.trim());
1294
1297
  return;
1295
1298
  }
1296
1299
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -1326,9 +1329,10 @@ async function readCodexAuth() {
1326
1329
  try {
1327
1330
  const raw = await readFile2(getCodexAuthPath(), "utf8");
1328
1331
  const auth = JSON.parse(raw);
1332
+ const apiKey = auth.OPENAI_API_KEY || process.env.OPENAI_API_KEY || void 0;
1329
1333
  const token = auth.tokens?.access_token;
1330
- if (!token) return null;
1331
- return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
1334
+ if (!token && !apiKey) return null;
1335
+ return { accessToken: token ?? "", accountId: auth.tokens?.account_id ?? void 0, apiKey };
1332
1336
  } catch {
1333
1337
  return null;
1334
1338
  }
@@ -1336,10 +1340,18 @@ async function readCodexAuth() {
1336
1340
  function getCodexGlobalStatePath() {
1337
1341
  return join2(getCodexHomeDir2(), ".codex-global-state.json");
1338
1342
  }
1343
+ function getCodexSessionIndexPath() {
1344
+ return join2(getCodexHomeDir2(), "session_index.jsonl");
1345
+ }
1339
1346
  var MAX_THREAD_TITLES = 500;
1347
+ var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
1348
+ var sessionIndexThreadTitleCacheState = {
1349
+ fileSignature: null,
1350
+ cache: EMPTY_THREAD_TITLE_CACHE
1351
+ };
1340
1352
  function normalizeThreadTitleCache(value) {
1341
1353
  const record = asRecord2(value);
1342
- if (!record) return { titles: {}, order: [] };
1354
+ if (!record) return EMPTY_THREAD_TITLE_CACHE;
1343
1355
  const rawTitles = asRecord2(record.titles);
1344
1356
  const titles = {};
1345
1357
  if (rawTitles) {
@@ -1363,6 +1375,47 @@ function removeFromThreadTitleCache(cache, id) {
1363
1375
  const { [id]: _, ...titles } = cache.titles;
1364
1376
  return { titles, order: cache.order.filter((o) => o !== id) };
1365
1377
  }
1378
+ function normalizeSessionIndexThreadTitle(value) {
1379
+ const record = asRecord2(value);
1380
+ if (!record) return null;
1381
+ const id = typeof record.id === "string" ? record.id.trim() : "";
1382
+ const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
1383
+ const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
1384
+ const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
1385
+ if (!id || !title) return null;
1386
+ return {
1387
+ id,
1388
+ title,
1389
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
1390
+ };
1391
+ }
1392
+ function trimThreadTitleCache(cache) {
1393
+ const titles = { ...cache.titles };
1394
+ const order = cache.order.filter((id) => {
1395
+ if (!titles[id]) return false;
1396
+ return true;
1397
+ }).slice(0, MAX_THREAD_TITLES);
1398
+ for (const id of Object.keys(titles)) {
1399
+ if (!order.includes(id)) {
1400
+ delete titles[id];
1401
+ }
1402
+ }
1403
+ return { titles, order };
1404
+ }
1405
+ function mergeThreadTitleCaches(base, overlay) {
1406
+ const titles = { ...base.titles, ...overlay.titles };
1407
+ const order = [];
1408
+ for (const id of [...overlay.order, ...base.order]) {
1409
+ if (!titles[id] || order.includes(id)) continue;
1410
+ order.push(id);
1411
+ }
1412
+ for (const id of Object.keys(titles)) {
1413
+ if (!order.includes(id)) {
1414
+ order.push(id);
1415
+ }
1416
+ }
1417
+ return trimThreadTitleCache({ titles, order });
1418
+ }
1366
1419
  async function readThreadTitleCache() {
1367
1420
  const statePath = getCodexGlobalStatePath();
1368
1421
  try {
@@ -1370,7 +1423,7 @@ async function readThreadTitleCache() {
1370
1423
  const payload = asRecord2(JSON.parse(raw)) ?? {};
1371
1424
  return normalizeThreadTitleCache(payload["thread-titles"]);
1372
1425
  } catch {
1373
- return { titles: {}, order: [] };
1426
+ return EMPTY_THREAD_TITLE_CACHE;
1374
1427
  }
1375
1428
  }
1376
1429
  async function writeThreadTitleCache(cache) {
@@ -1385,6 +1438,69 @@ async function writeThreadTitleCache(cache) {
1385
1438
  payload["thread-titles"] = cache;
1386
1439
  await writeFile2(statePath, JSON.stringify(payload), "utf8");
1387
1440
  }
1441
+ function getSessionIndexFileSignature(stats) {
1442
+ return `${String(stats.mtimeMs)}:${String(stats.size)}`;
1443
+ }
1444
+ async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
1445
+ const latestById = /* @__PURE__ */ new Map();
1446
+ const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
1447
+ const lines = createInterface({
1448
+ input,
1449
+ crlfDelay: Infinity
1450
+ });
1451
+ try {
1452
+ for await (const line of lines) {
1453
+ const trimmed = line.trim();
1454
+ if (!trimmed) continue;
1455
+ try {
1456
+ const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
1457
+ if (!entry) continue;
1458
+ const previous = latestById.get(entry.id);
1459
+ if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
1460
+ latestById.set(entry.id, entry);
1461
+ }
1462
+ } catch {
1463
+ }
1464
+ }
1465
+ } finally {
1466
+ lines.close();
1467
+ input.close();
1468
+ }
1469
+ const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
1470
+ const titles = {};
1471
+ const order = [];
1472
+ for (const entry of entries) {
1473
+ titles[entry.id] = entry.title;
1474
+ order.push(entry.id);
1475
+ }
1476
+ return trimThreadTitleCache({ titles, order });
1477
+ }
1478
+ async function readThreadTitlesFromSessionIndex() {
1479
+ const sessionIndexPath = getCodexSessionIndexPath();
1480
+ try {
1481
+ const stats = await stat2(sessionIndexPath);
1482
+ const fileSignature = getSessionIndexFileSignature(stats);
1483
+ if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
1484
+ return sessionIndexThreadTitleCacheState.cache;
1485
+ }
1486
+ const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
1487
+ sessionIndexThreadTitleCacheState = { fileSignature, cache };
1488
+ return cache;
1489
+ } catch {
1490
+ sessionIndexThreadTitleCacheState = {
1491
+ fileSignature: "missing",
1492
+ cache: EMPTY_THREAD_TITLE_CACHE
1493
+ };
1494
+ return sessionIndexThreadTitleCacheState.cache;
1495
+ }
1496
+ }
1497
+ async function readMergedThreadTitleCache() {
1498
+ const [sessionIndexCache, persistedCache] = await Promise.all([
1499
+ readThreadTitlesFromSessionIndex(),
1500
+ readThreadTitleCache()
1501
+ ]);
1502
+ return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
1503
+ }
1388
1504
  async function readWorkspaceRootsState() {
1389
1505
  const statePath = getCodexGlobalStatePath();
1390
1506
  let payload = {};
@@ -1498,32 +1614,93 @@ function handleFileUpload(req, res) {
1498
1614
  setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
1499
1615
  });
1500
1616
  }
1501
- async function proxyTranscribe(body, contentType, authToken, accountId) {
1502
- const headers = {
1617
+ function httpPost(url, headers, body) {
1618
+ const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
1619
+ return new Promise((resolve3, reject) => {
1620
+ const req = doRequest(url, { method: "POST", headers }, (res) => {
1621
+ const chunks = [];
1622
+ res.on("data", (c) => chunks.push(c));
1623
+ res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
1624
+ res.on("error", reject);
1625
+ });
1626
+ req.on("error", reject);
1627
+ req.write(body);
1628
+ req.end();
1629
+ });
1630
+ }
1631
+ var curlImpersonateAvailable = null;
1632
+ function curlImpersonatePost(url, headers, body) {
1633
+ return new Promise((resolve3, reject) => {
1634
+ const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
1635
+ for (const [k, v] of Object.entries(headers)) {
1636
+ if (k.toLowerCase() === "content-length") continue;
1637
+ args.push("-H", `${k}: ${String(v)}`);
1638
+ }
1639
+ args.push("--data-binary", "@-");
1640
+ const proc = spawn2("curl-impersonate-chrome", args, {
1641
+ env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
1642
+ stdio: ["pipe", "pipe", "pipe"]
1643
+ });
1644
+ const chunks = [];
1645
+ proc.stdout.on("data", (c) => chunks.push(c));
1646
+ proc.on("error", (e) => {
1647
+ curlImpersonateAvailable = false;
1648
+ reject(e);
1649
+ });
1650
+ proc.on("close", (code) => {
1651
+ const raw = Buffer.concat(chunks).toString("utf8");
1652
+ const lastNewline = raw.lastIndexOf("\n");
1653
+ const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
1654
+ const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
1655
+ const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
1656
+ curlImpersonateAvailable = true;
1657
+ resolve3({ status, body: responseBody });
1658
+ });
1659
+ proc.stdin.write(body);
1660
+ proc.stdin.end();
1661
+ });
1662
+ }
1663
+ var TRANSCRIBE_RELAY_URL = process.env.TRANSCRIBE_RELAY_URL || "http://127.0.0.1:1090/relay-transcribe";
1664
+ async function tryRelay(headers, body) {
1665
+ try {
1666
+ const resp = await httpPost(TRANSCRIBE_RELAY_URL, headers, body);
1667
+ if (resp.status !== 0) return resp;
1668
+ } catch {
1669
+ }
1670
+ return null;
1671
+ }
1672
+ async function proxyTranscribe(body, contentType, authToken, accountId, apiKey) {
1673
+ const chatgptHeaders = {
1503
1674
  "Content-Type": contentType,
1504
1675
  "Content-Length": body.length,
1505
- Authorization: `Bearer ${authToken}`,
1676
+ Authorization: `Bearer ${authToken || apiKey || ""}`,
1506
1677
  originator: "Codex Desktop",
1507
1678
  "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
1508
1679
  };
1509
- if (accountId) {
1510
- headers["ChatGPT-Account-Id"] = accountId;
1511
- }
1512
- return new Promise((resolve2, reject) => {
1513
- const req = httpsRequest(
1514
- "https://chatgpt.com/backend-api/transcribe",
1515
- { method: "POST", headers },
1516
- (res) => {
1517
- const chunks = [];
1518
- res.on("data", (c) => chunks.push(c));
1519
- res.on("end", () => resolve2({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
1520
- res.on("error", reject);
1521
- }
1522
- );
1523
- req.on("error", reject);
1524
- req.write(body);
1525
- req.end();
1526
- });
1680
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
1681
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
1682
+ let result;
1683
+ try {
1684
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1685
+ } catch {
1686
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1687
+ }
1688
+ if (result.status === 403 && result.body.includes("cf_chl")) {
1689
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
1690
+ try {
1691
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1692
+ if (ciResult.status !== 403) return ciResult;
1693
+ } catch {
1694
+ }
1695
+ }
1696
+ const relayed = await tryRelay(chatgptHeaders, body);
1697
+ if (relayed && relayed.status !== 403) return relayed;
1698
+ if (apiKey) {
1699
+ return httpPost("https://api.openai.com/v1/audio/transcriptions", { ...chatgptHeaders, Authorization: `Bearer ${apiKey}` }, body);
1700
+ }
1701
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome, start relay, or set OPENAI_API_KEY." }) };
1702
+ }
1703
+ return result;
1527
1704
  }
1528
1705
  var AppServerProcess = class {
1529
1706
  constructor() {
@@ -1670,8 +1847,8 @@ var AppServerProcess = class {
1670
1847
  async call(method, params) {
1671
1848
  this.start();
1672
1849
  const id = this.nextId++;
1673
- return new Promise((resolve2, reject) => {
1674
- this.pending.set(id, { resolve: resolve2, reject });
1850
+ return new Promise((resolve3, reject) => {
1851
+ this.pending.set(id, { resolve: resolve3, reject });
1675
1852
  this.sendLine({
1676
1853
  jsonrpc: "2.0",
1677
1854
  id,
@@ -1772,7 +1949,7 @@ var MethodCatalog = class {
1772
1949
  this.notificationCache = null;
1773
1950
  }
1774
1951
  async runGenerateSchemaCommand(outDir) {
1775
- await new Promise((resolve2, reject) => {
1952
+ await new Promise((resolve3, reject) => {
1776
1953
  const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
1777
1954
  stdio: ["ignore", "ignore", "pipe"]
1778
1955
  });
@@ -1784,7 +1961,7 @@ var MethodCatalog = class {
1784
1961
  process2.on("error", reject);
1785
1962
  process2.on("exit", (code) => {
1786
1963
  if (code === 0) {
1787
- resolve2();
1964
+ resolve3();
1788
1965
  return;
1789
1966
  }
1790
1967
  reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
@@ -1974,7 +2151,7 @@ function createCodexBridgeMiddleware() {
1974
2151
  }
1975
2152
  const rawBody = await readRawBody(req);
1976
2153
  const incomingCt = req.headers["content-type"] ?? "application/octet-stream";
1977
- const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId);
2154
+ const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId, auth.apiKey);
1978
2155
  res.statusCode = upstream.status;
1979
2156
  res.setHeader("Content-Type", "application/json; charset=utf-8");
1980
2157
  res.end(upstream.body);
@@ -2200,7 +2377,7 @@ function createCodexBridgeMiddleware() {
2200
2377
  return;
2201
2378
  }
2202
2379
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
2203
- const cache = await readThreadTitleCache();
2380
+ const cache = await readMergedThreadTitleCache();
2204
2381
  setJson2(res, 200, { data: cache });
2205
2382
  return;
2206
2383
  }
@@ -2815,7 +2992,7 @@ function createServer(options = {}) {
2815
2992
  res.status(404).json({ error: "File not found." });
2816
2993
  }
2817
2994
  });
2818
- const hasFrontendAssets = existsSync2(spaEntryFile);
2995
+ const hasFrontendAssets = existsSync3(spaEntryFile);
2819
2996
  if (hasFrontendAssets) {
2820
2997
  app.use(express.static(distDir));
2821
2998
  }
@@ -2921,7 +3098,7 @@ function resolveCodexCommand() {
2921
3098
  return "codex";
2922
3099
  }
2923
3100
  const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
2924
- if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
3101
+ if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
2925
3102
  return userCandidate;
2926
3103
  }
2927
3104
  const prefix = process.env.PREFIX?.trim();
@@ -2929,7 +3106,7 @@ function resolveCodexCommand() {
2929
3106
  return null;
2930
3107
  }
2931
3108
  const candidate = join5(prefix, "bin", "codex");
2932
- if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
3109
+ if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
2933
3110
  return candidate;
2934
3111
  }
2935
3112
  return null;
@@ -2939,7 +3116,7 @@ function resolveCloudflaredCommand() {
2939
3116
  return "cloudflared";
2940
3117
  }
2941
3118
  const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
2942
- if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
3119
+ if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
2943
3120
  return localCandidate;
2944
3121
  }
2945
3122
  return null;
@@ -2954,7 +3131,7 @@ function mapCloudflaredLinuxArch(arch) {
2954
3131
  return null;
2955
3132
  }
2956
3133
  function downloadFile(url, destination) {
2957
- return new Promise((resolve2, reject) => {
3134
+ return new Promise((resolve3, reject) => {
2958
3135
  const request = (currentUrl) => {
2959
3136
  httpsGet(currentUrl, (response) => {
2960
3137
  const code = response.statusCode ?? 0;
@@ -2972,7 +3149,7 @@ function downloadFile(url, destination) {
2972
3149
  response.pipe(file);
2973
3150
  file.on("finish", () => {
2974
3151
  file.close();
2975
- resolve2();
3152
+ resolve3();
2976
3153
  });
2977
3154
  file.on("error", reject);
2978
3155
  }).on("error", reject);
@@ -3012,7 +3189,7 @@ async function shouldInstallCloudflaredInteractively() {
3012
3189
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3013
3190
  return false;
3014
3191
  }
3015
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
3192
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
3016
3193
  try {
3017
3194
  const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
3018
3195
  const normalized = answer.trim().toLowerCase();
@@ -3034,7 +3211,7 @@ async function resolveCloudflaredForTunnel() {
3034
3211
  }
3035
3212
  function hasCodexAuth() {
3036
3213
  const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3037
- return existsSync3(join5(codexHome, "auth.json"));
3214
+ return existsSync4(join5(codexHome, "auth.json"));
3038
3215
  }
3039
3216
  function ensureCodexInstalled() {
3040
3217
  let codexCommand = resolveCodexCommand();
@@ -3115,24 +3292,27 @@ function parseCloudflaredUrl(chunk) {
3115
3292
  }
3116
3293
  function getAccessibleUrls(port) {
3117
3294
  const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
3118
- const interfaces = networkInterfaces();
3119
- for (const entries of Object.values(interfaces)) {
3120
- if (!entries) {
3121
- continue;
3122
- }
3123
- for (const entry of entries) {
3124
- if (entry.internal) {
3295
+ try {
3296
+ const interfaces = networkInterfaces();
3297
+ for (const entries of Object.values(interfaces)) {
3298
+ if (!entries) {
3125
3299
  continue;
3126
3300
  }
3127
- if (entry.family === "IPv4") {
3128
- urls.add(`http://${entry.address}:${String(port)}`);
3301
+ for (const entry of entries) {
3302
+ if (entry.internal) {
3303
+ continue;
3304
+ }
3305
+ if (entry.family === "IPv4") {
3306
+ urls.add(`http://${entry.address}:${String(port)}`);
3307
+ }
3129
3308
  }
3130
3309
  }
3310
+ } catch {
3131
3311
  }
3132
3312
  return Array.from(urls);
3133
3313
  }
3134
3314
  async function startCloudflaredTunnel(command, localPort) {
3135
- return new Promise((resolve2, reject) => {
3315
+ return new Promise((resolve3, reject) => {
3136
3316
  const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
3137
3317
  stdio: ["ignore", "pipe", "pipe"]
3138
3318
  });
@@ -3149,7 +3329,7 @@ async function startCloudflaredTunnel(command, localPort) {
3149
3329
  clearTimeout(timeout);
3150
3330
  child.stdout?.off("data", handleData);
3151
3331
  child.stderr?.off("data", handleData);
3152
- resolve2({ process: child, url: parsedUrl });
3332
+ resolve3({ process: child, url: parsedUrl });
3153
3333
  };
3154
3334
  const onError = (error) => {
3155
3335
  clearTimeout(timeout);
@@ -3168,7 +3348,7 @@ async function startCloudflaredTunnel(command, localPort) {
3168
3348
  });
3169
3349
  }
3170
3350
  function listenWithFallback(server, startPort) {
3171
- return new Promise((resolve2, reject) => {
3351
+ return new Promise((resolve3, reject) => {
3172
3352
  const attempt = (port) => {
3173
3353
  const onError = (error) => {
3174
3354
  server.off("listening", onListening);
@@ -3180,7 +3360,7 @@ function listenWithFallback(server, startPort) {
3180
3360
  };
3181
3361
  const onListening = () => {
3182
3362
  server.off("error", onError);
3183
- resolve2(port);
3363
+ resolve3(port);
3184
3364
  };
3185
3365
  server.once("error", onError);
3186
3366
  server.once("listening", onListening);
@@ -3189,8 +3369,72 @@ function listenWithFallback(server, startPort) {
3189
3369
  attempt(startPort);
3190
3370
  });
3191
3371
  }
3372
+ function getCodexGlobalStatePath2() {
3373
+ const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3374
+ return join5(codexHome, ".codex-global-state.json");
3375
+ }
3376
+ function normalizeUniqueStrings(value) {
3377
+ if (!Array.isArray(value)) return [];
3378
+ const next = [];
3379
+ for (const item of value) {
3380
+ if (typeof item !== "string") continue;
3381
+ const trimmed = item.trim();
3382
+ if (!trimmed || next.includes(trimmed)) continue;
3383
+ next.push(trimmed);
3384
+ }
3385
+ return next;
3386
+ }
3387
+ async function persistLaunchProject(projectPath) {
3388
+ const trimmed = projectPath.trim();
3389
+ if (!trimmed) return;
3390
+ const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
3391
+ const directoryInfo = await stat5(normalizedPath);
3392
+ if (!directoryInfo.isDirectory()) {
3393
+ throw new Error(`Not a directory: ${normalizedPath}`);
3394
+ }
3395
+ const statePath = getCodexGlobalStatePath2();
3396
+ let payload = {};
3397
+ try {
3398
+ const raw = await readFile4(statePath, "utf8");
3399
+ const parsed = JSON.parse(raw);
3400
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3401
+ payload = parsed;
3402
+ }
3403
+ } catch {
3404
+ payload = {};
3405
+ }
3406
+ const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
3407
+ const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
3408
+ payload["electron-saved-workspace-roots"] = [
3409
+ normalizedPath,
3410
+ ...roots.filter((value) => value !== normalizedPath)
3411
+ ];
3412
+ payload["active-workspace-roots"] = [
3413
+ normalizedPath,
3414
+ ...activeRoots.filter((value) => value !== normalizedPath)
3415
+ ];
3416
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
3417
+ }
3418
+ async function addProjectOnly(projectPath) {
3419
+ const trimmed = projectPath.trim();
3420
+ if (!trimmed) {
3421
+ throw new Error("Missing project path");
3422
+ }
3423
+ await persistLaunchProject(trimmed);
3424
+ }
3192
3425
  async function startServer(options) {
3193
3426
  const version = await readCliVersion();
3427
+ const projectPath = options.projectPath?.trim() ?? "";
3428
+ if (projectPath.length > 0) {
3429
+ try {
3430
+ await persistLaunchProject(projectPath);
3431
+ } catch (error) {
3432
+ const message = error instanceof Error ? error.message : String(error);
3433
+ console.warn(`
3434
+ [project] Could not open launch project: ${message}
3435
+ `);
3436
+ }
3437
+ }
3194
3438
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
3195
3439
  if (!hasCodexAuth() && codexCommand) {
3196
3440
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
@@ -3251,7 +3495,7 @@ async function startServer(options) {
3251
3495
  qrcode.generate(tunnelUrl, { small: true });
3252
3496
  console.log("");
3253
3497
  }
3254
- openBrowser(`http://localhost:${String(port)}`);
3498
+ if (options.open) openBrowser(`http://localhost:${String(port)}`);
3255
3499
  function shutdown() {
3256
3500
  console.log("\nShutting down...");
3257
3501
  if (tunnelChild && !tunnelChild.killed) {
@@ -3274,8 +3518,20 @@ async function runLogin() {
3274
3518
  console.log("\nStarting `codex login`...\n");
3275
3519
  runOrFail(codexCommand, ["login"], "Codex login");
3276
3520
  }
3277
- program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (opts) => {
3278
- await startServer(opts);
3521
+ program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").action(async (projectPath, opts) => {
3522
+ const rawArgv = process.argv.slice(2);
3523
+ const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
3524
+ let openProjectOnly = (opts.openProject ?? "").trim();
3525
+ if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
3526
+ openProjectOnly = projectPath.trim();
3527
+ }
3528
+ if (openProjectOnly.length > 0) {
3529
+ await addProjectOnly(openProjectOnly);
3530
+ console.log(`Added project: ${openProjectOnly}`);
3531
+ return;
3532
+ }
3533
+ const launchProject = (projectPath ?? "").trim();
3534
+ await startServer({ ...opts, projectPath: launchProject });
3279
3535
  });
3280
3536
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
3281
3537
  program.command("help").description("Show codexui command help").action(() => {