codexapp 0.1.46 → 0.1.48

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/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Codex Web Local</title>
7
- <script type="module" crossorigin src="/assets/index-ol-gi5Ys.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-D2tyBrlG.css">
7
+ <script type="module" crossorigin src="/assets/index-DhcIXLYw.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-C55jtDWV.css">
9
9
  </head>
10
10
  <body class="bg-slate-950">
11
11
  <div id="app"></div>
package/dist-cli/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync, createWriteStream, existsSync as existsSync4, mkdirSync } from "fs";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync } from "fs";
6
6
  import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
7
- import { homedir as homedir3, networkInterfaces } from "os";
8
- import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
9
- import { spawn as spawn3, spawnSync } from "child_process";
7
+ import { homedir as homedir4, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "path";
9
+ import { spawn as spawn3 } from "child_process";
10
10
  import { createInterface as createInterface2 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
12
  import { dirname as dirname3 } from "path";
@@ -16,8 +16,8 @@ import qrcode from "qrcode-terminal";
16
16
 
17
17
  // src/server/httpServer.ts
18
18
  import { fileURLToPath } from "url";
19
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
20
- import { existsSync as existsSync3 } from "fs";
19
+ import { dirname as dirname2, extname as extname3, isAbsolute as isAbsolute2, join as join5 } from "path";
20
+ import { existsSync as existsSync4 } from "fs";
21
21
  import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
22
22
  import express from "express";
23
23
 
@@ -28,9 +28,9 @@ import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as st
28
28
  import { createReadStream } from "fs";
29
29
  import { request as httpRequest } from "http";
30
30
  import { request as httpsRequest } from "https";
31
- import { homedir as homedir2 } from "os";
31
+ import { homedir as homedir3 } from "os";
32
32
  import { tmpdir as tmpdir2 } from "os";
33
- import { basename, isAbsolute, join as join2, resolve } from "path";
33
+ import { basename as basename2, isAbsolute, join as join3, resolve } from "path";
34
34
  import { createInterface } from "readline";
35
35
  import { writeFile as writeFile2 } from "fs/promises";
36
36
 
@@ -1120,6 +1120,84 @@ async function handleSkillsRoutes(req, res, url, context) {
1120
1120
  return false;
1121
1121
  }
1122
1122
 
1123
+ // src/utils/commandInvocation.ts
1124
+ import { spawnSync } from "child_process";
1125
+ import { existsSync as existsSync2 } from "fs";
1126
+ import { homedir as homedir2 } from "os";
1127
+ import { basename, extname, join as join2 } from "path";
1128
+ var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
1129
+ function quoteCmdExeArg(value) {
1130
+ const normalized = value.replace(/"/g, '""');
1131
+ if (!/[\s"]/u.test(normalized)) {
1132
+ return normalized;
1133
+ }
1134
+ return `"${normalized}"`;
1135
+ }
1136
+ function needsCmdExeWrapper(command) {
1137
+ if (process.platform !== "win32") {
1138
+ return false;
1139
+ }
1140
+ const lowerCommand = command.toLowerCase();
1141
+ const baseName = basename(lowerCommand);
1142
+ if (/\.(cmd|bat)$/i.test(baseName)) {
1143
+ return true;
1144
+ }
1145
+ if (extname(baseName)) {
1146
+ return false;
1147
+ }
1148
+ return WINDOWS_CMD_NAMES.has(baseName);
1149
+ }
1150
+ function getSpawnInvocation(command, args = []) {
1151
+ if (needsCmdExeWrapper(command)) {
1152
+ return {
1153
+ command: "cmd.exe",
1154
+ args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
1155
+ };
1156
+ }
1157
+ return { command, args };
1158
+ }
1159
+ function spawnSyncCommand(command, args = [], options = {}) {
1160
+ const invocation = getSpawnInvocation(command, args);
1161
+ return spawnSync(invocation.command, invocation.args, options);
1162
+ }
1163
+ function canRunCommand(command, args = []) {
1164
+ const result = spawnSyncCommand(command, args, { stdio: "ignore" });
1165
+ return result.status === 0;
1166
+ }
1167
+ function getUserNpmPrefix() {
1168
+ return join2(homedir2(), ".npm-global");
1169
+ }
1170
+ function resolveCodexCommand() {
1171
+ if (canRunCommand("codex", ["--version"])) {
1172
+ return "codex";
1173
+ }
1174
+ if (process.platform === "win32") {
1175
+ const windowsCandidates = [
1176
+ process.env.APPDATA ? join2(process.env.APPDATA, "npm", "codex.cmd") : "",
1177
+ join2(homedir2(), ".local", "bin", "codex.cmd"),
1178
+ join2(getUserNpmPrefix(), "bin", "codex.cmd")
1179
+ ].filter(Boolean);
1180
+ for (const candidate2 of windowsCandidates) {
1181
+ if (existsSync2(candidate2) && canRunCommand(candidate2, ["--version"])) {
1182
+ return candidate2;
1183
+ }
1184
+ }
1185
+ }
1186
+ const userCandidate = join2(getUserNpmPrefix(), "bin", "codex");
1187
+ if (existsSync2(userCandidate) && canRunCommand(userCandidate, ["--version"])) {
1188
+ return userCandidate;
1189
+ }
1190
+ const prefix = process.env.PREFIX?.trim();
1191
+ if (!prefix) {
1192
+ return null;
1193
+ }
1194
+ const candidate = join2(prefix, "bin", "codex");
1195
+ if (existsSync2(candidate) && canRunCommand(candidate, ["--version"])) {
1196
+ return candidate;
1197
+ }
1198
+ return null;
1199
+ }
1200
+
1123
1201
  // src/server/codexAppServerBridge.ts
1124
1202
  function asRecord2(value) {
1125
1203
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -1224,7 +1302,7 @@ async function listFilesWithRipgrep(cwd) {
1224
1302
  }
1225
1303
  function getCodexHomeDir2() {
1226
1304
  const codexHome = process.env.CODEX_HOME?.trim();
1227
- return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1305
+ return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
1228
1306
  }
1229
1307
  async function runCommand2(command, args, options = {}) {
1230
1308
  await new Promise((resolve3, reject) => {
@@ -1262,7 +1340,7 @@ function isNotGitRepositoryError(error) {
1262
1340
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
1263
1341
  }
1264
1342
  async function ensureRepoHasInitialCommit(repoRoot) {
1265
- const agentsPath = join2(repoRoot, "AGENTS.md");
1343
+ const agentsPath = join3(repoRoot, "AGENTS.md");
1266
1344
  try {
1267
1345
  await stat2(agentsPath);
1268
1346
  } catch {
@@ -1323,25 +1401,24 @@ function normalizeStringRecord(value) {
1323
1401
  return next;
1324
1402
  }
1325
1403
  function getCodexAuthPath() {
1326
- return join2(getCodexHomeDir2(), "auth.json");
1404
+ return join3(getCodexHomeDir2(), "auth.json");
1327
1405
  }
1328
1406
  async function readCodexAuth() {
1329
1407
  try {
1330
1408
  const raw = await readFile2(getCodexAuthPath(), "utf8");
1331
1409
  const auth = JSON.parse(raw);
1332
- const apiKey = auth.OPENAI_API_KEY || process.env.OPENAI_API_KEY || void 0;
1333
1410
  const token = auth.tokens?.access_token;
1334
- if (!token && !apiKey) return null;
1335
- return { accessToken: token ?? "", accountId: auth.tokens?.account_id ?? void 0, apiKey };
1411
+ if (!token) return null;
1412
+ return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
1336
1413
  } catch {
1337
1414
  return null;
1338
1415
  }
1339
1416
  }
1340
1417
  function getCodexGlobalStatePath() {
1341
- return join2(getCodexHomeDir2(), ".codex-global-state.json");
1418
+ return join3(getCodexHomeDir2(), ".codex-global-state.json");
1342
1419
  }
1343
1420
  function getCodexSessionIndexPath() {
1344
- return join2(getCodexHomeDir2(), "session_index.jsonl");
1421
+ return join3(getCodexHomeDir2(), "session_index.jsonl");
1345
1422
  }
1346
1423
  var MAX_THREAD_TITLES = 500;
1347
1424
  var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
@@ -1600,10 +1677,10 @@ function handleFileUpload(req, res) {
1600
1677
  setJson2(res, 400, { error: "No file in request" });
1601
1678
  return;
1602
1679
  }
1603
- const uploadDir = join2(tmpdir2(), "codex-web-uploads");
1680
+ const uploadDir = join3(tmpdir2(), "codex-web-uploads");
1604
1681
  await mkdir2(uploadDir, { recursive: true });
1605
- const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1606
- const destPath = join2(destDir, fileName);
1682
+ const destDir = await mkdtemp2(join3(uploadDir, "f-"));
1683
+ const destPath = join3(destDir, fileName);
1607
1684
  await writeFile2(destPath, fileData);
1608
1685
  setJson2(res, 200, { path: destPath });
1609
1686
  } catch (err) {
@@ -1660,20 +1737,11 @@ function curlImpersonatePost(url, headers, body) {
1660
1737
  proc.stdin.end();
1661
1738
  });
1662
1739
  }
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) {
1740
+ async function proxyTranscribe(body, contentType, authToken, accountId) {
1673
1741
  const chatgptHeaders = {
1674
1742
  "Content-Type": contentType,
1675
1743
  "Content-Length": body.length,
1676
- Authorization: `Bearer ${authToken || apiKey || ""}`,
1744
+ Authorization: `Bearer ${authToken}`,
1677
1745
  originator: "Codex Desktop",
1678
1746
  "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
1679
1747
  };
@@ -1693,12 +1761,7 @@ async function proxyTranscribe(body, contentType, authToken, accountId, apiKey)
1693
1761
  } catch {
1694
1762
  }
1695
1763
  }
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." }) };
1764
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
1702
1765
  }
1703
1766
  return result;
1704
1767
  }
@@ -1724,7 +1787,8 @@ var AppServerProcess = class {
1724
1787
  start() {
1725
1788
  if (this.process) return;
1726
1789
  this.stopping = false;
1727
- const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1790
+ const invocation = getSpawnInvocation(resolveCodexCommand() ?? "codex", this.appServerArgs);
1791
+ const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
1728
1792
  this.process = proc;
1729
1793
  proc.stdout.setEncoding("utf8");
1730
1794
  proc.stdout.on("data", (chunk) => {
@@ -1950,7 +2014,8 @@ var MethodCatalog = class {
1950
2014
  }
1951
2015
  async runGenerateSchemaCommand(outDir) {
1952
2016
  await new Promise((resolve3, reject) => {
1953
- const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
2017
+ const invocation = getSpawnInvocation(resolveCodexCommand() ?? "codex", ["app-server", "generate-json-schema", "--out", outDir]);
2018
+ const process2 = spawn2(invocation.command, invocation.args, {
1954
2019
  stdio: ["ignore", "ignore", "pipe"]
1955
2020
  });
1956
2021
  let stderr = "";
@@ -2006,9 +2071,9 @@ var MethodCatalog = class {
2006
2071
  if (this.methodCache) {
2007
2072
  return this.methodCache;
2008
2073
  }
2009
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2074
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
2010
2075
  await this.runGenerateSchemaCommand(outDir);
2011
- const clientRequestPath = join2(outDir, "ClientRequest.json");
2076
+ const clientRequestPath = join3(outDir, "ClientRequest.json");
2012
2077
  const raw = await readFile2(clientRequestPath, "utf8");
2013
2078
  const parsed = JSON.parse(raw);
2014
2079
  const methods = this.extractMethodsFromClientRequest(parsed);
@@ -2019,9 +2084,9 @@ var MethodCatalog = class {
2019
2084
  if (this.notificationCache) {
2020
2085
  return this.notificationCache;
2021
2086
  }
2022
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2087
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
2023
2088
  await this.runGenerateSchemaCommand(outDir);
2024
- const serverNotificationPath = join2(outDir, "ServerNotification.json");
2089
+ const serverNotificationPath = join3(outDir, "ServerNotification.json");
2025
2090
  const raw = await readFile2(serverNotificationPath, "utf8");
2026
2091
  const parsed = JSON.parse(raw);
2027
2092
  const methods = this.extractMethodsFromServerNotification(parsed);
@@ -2151,7 +2216,7 @@ function createCodexBridgeMiddleware() {
2151
2216
  }
2152
2217
  const rawBody = await readRawBody(req);
2153
2218
  const incomingCt = req.headers["content-type"] ?? "application/octet-stream";
2154
- const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId, auth.apiKey);
2219
+ const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId);
2155
2220
  res.statusCode = upstream.status;
2156
2221
  res.setHeader("Content-Type", "application/json; charset=utf-8");
2157
2222
  res.end(upstream.body);
@@ -2183,7 +2248,7 @@ function createCodexBridgeMiddleware() {
2183
2248
  return;
2184
2249
  }
2185
2250
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
2186
- setJson2(res, 200, { data: { path: homedir2() } });
2251
+ setJson2(res, 200, { data: { path: homedir3() } });
2187
2252
  return;
2188
2253
  }
2189
2254
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
@@ -2213,22 +2278,22 @@ function createCodexBridgeMiddleware() {
2213
2278
  await runCommand2("git", ["init"], { cwd: sourceCwd });
2214
2279
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2215
2280
  }
2216
- const repoName = basename(gitRoot) || "repo";
2217
- const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
2281
+ const repoName = basename2(gitRoot) || "repo";
2282
+ const worktreesRoot = join3(getCodexHomeDir2(), "worktrees");
2218
2283
  await mkdir2(worktreesRoot, { recursive: true });
2219
2284
  let worktreeId = "";
2220
2285
  let worktreeParent = "";
2221
2286
  let worktreeCwd = "";
2222
2287
  for (let attempt = 0; attempt < 12; attempt += 1) {
2223
2288
  const candidate = randomBytes(2).toString("hex");
2224
- const parent = join2(worktreesRoot, candidate);
2289
+ const parent = join3(worktreesRoot, candidate);
2225
2290
  try {
2226
2291
  await stat2(parent);
2227
2292
  continue;
2228
2293
  } catch {
2229
2294
  worktreeId = candidate;
2230
2295
  worktreeParent = parent;
2231
- worktreeCwd = join2(parent, repoName);
2296
+ worktreeCwd = join3(parent, repoName);
2232
2297
  break;
2233
2298
  }
2234
2299
  }
@@ -2333,7 +2398,7 @@ function createCodexBridgeMiddleware() {
2333
2398
  let index = 1;
2334
2399
  while (index < 1e5) {
2335
2400
  const candidateName = `New Project (${String(index)})`;
2336
- const candidatePath = join2(normalizedBasePath, candidateName);
2401
+ const candidatePath = join3(normalizedBasePath, candidateName);
2337
2402
  try {
2338
2403
  await stat2(candidatePath);
2339
2404
  index += 1;
@@ -2579,7 +2644,7 @@ function createAuthSession(password) {
2579
2644
  }
2580
2645
 
2581
2646
  // src/server/localBrowseUi.ts
2582
- import { dirname, extname, join as join3 } from "path";
2647
+ import { dirname, extname as extname2, join as join4 } from "path";
2583
2648
  import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
2584
2649
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2585
2650
  ".txt",
@@ -2610,7 +2675,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2610
2675
  ".ps1"
2611
2676
  ]);
2612
2677
  function languageForPath(pathValue) {
2613
- const extension = extname(pathValue).toLowerCase();
2678
+ const extension = extname2(pathValue).toLowerCase();
2614
2679
  switch (extension) {
2615
2680
  case ".js":
2616
2681
  return "javascript";
@@ -2671,7 +2736,7 @@ function decodeBrowsePath(rawPath) {
2671
2736
  }
2672
2737
  }
2673
2738
  function isTextEditablePath(pathValue) {
2674
- return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
2739
+ return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
2675
2740
  }
2676
2741
  function looksLikeTextBuffer(buffer) {
2677
2742
  if (buffer.length === 0) return true;
@@ -2717,7 +2782,7 @@ function escapeForInlineScriptString(value) {
2717
2782
  async function getDirectoryItems(localPath) {
2718
2783
  const entries = await readdir3(localPath, { withFileTypes: true });
2719
2784
  const withMeta = await Promise.all(entries.map(async (entry) => {
2720
- const entryPath = join3(localPath, entry.name);
2785
+ const entryPath = join4(localPath, entry.name);
2721
2786
  const entryStat = await stat3(entryPath);
2722
2787
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2723
2788
  return {
@@ -2741,9 +2806,9 @@ async function createDirectoryListingHtml(localPath) {
2741
2806
  const rows = items.map((item) => {
2742
2807
  const suffix = item.isDirectory ? "/" : "";
2743
2808
  const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
2744
- return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
2809
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
2745
2810
  }).join("\n");
2746
- const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
2811
+ const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
2747
2812
  return `<!doctype html>
2748
2813
  <html lang="en">
2749
2814
  <head>
@@ -2757,8 +2822,27 @@ async function createDirectoryListingHtml(localPath) {
2757
2822
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
2758
2823
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
2759
2824
  .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
2760
- .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
2825
+ .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
2826
+ .header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
2827
+ .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
2828
+ .header-open-btn {
2829
+ height: 42px;
2830
+ padding: 0 14px;
2831
+ border: 1px solid #4f8de0;
2832
+ border-radius: 10px;
2833
+ background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
2834
+ color: #eef6ff;
2835
+ font-weight: 700;
2836
+ letter-spacing: 0.01em;
2837
+ cursor: pointer;
2838
+ box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
2839
+ }
2840
+ .header-open-btn:hover { filter: brightness(1.08); }
2841
+ .header-open-btn:disabled { opacity: 0.6; cursor: default; }
2842
+ .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
2843
+ .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
2761
2844
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
2845
+ .status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
2762
2846
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
2763
2847
  @media (max-width: 640px) {
2764
2848
  body { margin: 12px; }
@@ -2770,8 +2854,46 @@ async function createDirectoryListingHtml(localPath) {
2770
2854
  </head>
2771
2855
  <body>
2772
2856
  <h1>Index of ${escapeHtml(localPath)}</h1>
2773
- ${parentLink}
2857
+ <div class="header-actions">
2858
+ ${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
2859
+ <button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
2860
+ </div>
2861
+ <p id="status" class="status"></p>
2774
2862
  <ul>${rows}</ul>
2863
+ <script>
2864
+ const status = document.getElementById('status');
2865
+ document.addEventListener('click', async (event) => {
2866
+ const target = event.target;
2867
+ if (!(target instanceof Element)) return;
2868
+ const button = target.closest('.open-folder-btn');
2869
+ if (!(button instanceof HTMLButtonElement)) return;
2870
+
2871
+ const path = button.getAttribute('data-path') || '';
2872
+ if (!path) return;
2873
+ button.disabled = true;
2874
+ status.textContent = 'Opening folder in Codex...';
2875
+ try {
2876
+ const response = await fetch('/codex-api/project-root', {
2877
+ method: 'POST',
2878
+ headers: { 'Content-Type': 'application/json' },
2879
+ body: JSON.stringify({
2880
+ path,
2881
+ createIfMissing: false,
2882
+ label: '',
2883
+ }),
2884
+ });
2885
+ if (!response.ok) {
2886
+ status.textContent = 'Failed to open folder.';
2887
+ button.disabled = false;
2888
+ return;
2889
+ }
2890
+ window.location.assign('/#/');
2891
+ } catch {
2892
+ status.textContent = 'Failed to open folder.';
2893
+ button.disabled = false;
2894
+ }
2895
+ });
2896
+ </script>
2775
2897
  </body>
2776
2898
  </html>`;
2777
2899
  }
@@ -2847,8 +2969,8 @@ async function createTextEditorHtml(localPath) {
2847
2969
  // src/server/httpServer.ts
2848
2970
  import { WebSocketServer } from "ws";
2849
2971
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2850
- var distDir = join4(__dirname, "..", "dist");
2851
- var spaEntryFile = join4(distDir, "index.html");
2972
+ var distDir = join5(__dirname, "..", "dist");
2973
+ var spaEntryFile = join5(distDir, "index.html");
2852
2974
  var IMAGE_CONTENT_TYPES = {
2853
2975
  ".avif": "image/avif",
2854
2976
  ".bmp": "image/bmp",
@@ -2905,7 +3027,7 @@ function createServer(options = {}) {
2905
3027
  res.status(400).json({ error: "Expected absolute local file path." });
2906
3028
  return;
2907
3029
  }
2908
- const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
3030
+ const contentType = IMAGE_CONTENT_TYPES[extname3(localPath).toLowerCase()];
2909
3031
  if (!contentType) {
2910
3032
  res.status(415).json({ error: "Unsupported image type." });
2911
3033
  return;
@@ -2992,7 +3114,7 @@ function createServer(options = {}) {
2992
3114
  res.status(404).json({ error: "File not found." });
2993
3115
  }
2994
3116
  });
2995
- const hasFrontendAssets = existsSync3(spaEntryFile);
3117
+ const hasFrontendAssets = existsSync4(spaEntryFile);
2996
3118
  if (hasFrontendAssets) {
2997
3119
  app.use(express.static(distDir));
2998
3120
  }
@@ -3065,7 +3187,7 @@ var program = new Command().name("codexui").description("Web interface for Codex
3065
3187
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3066
3188
  async function readCliVersion() {
3067
3189
  try {
3068
- const packageJsonPath = join5(__dirname2, "..", "package.json");
3190
+ const packageJsonPath = join6(__dirname2, "..", "package.json");
3069
3191
  const raw = await readFile4(packageJsonPath, "utf8");
3070
3192
  const parsed = JSON.parse(raw);
3071
3193
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -3077,46 +3199,25 @@ function isTermuxRuntime() {
3077
3199
  return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
3078
3200
  }
3079
3201
  function canRun(command, args = []) {
3080
- const result = spawnSync(command, args, { stdio: "ignore" });
3081
- return result.status === 0;
3202
+ const result = canRunCommand(command, args);
3203
+ return result;
3082
3204
  }
3083
3205
  function runOrFail(command, args, label) {
3084
- const result = spawnSync(command, args, { stdio: "inherit" });
3206
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
3085
3207
  if (result.status !== 0) {
3086
3208
  throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
3087
3209
  }
3088
3210
  }
3089
3211
  function runWithStatus(command, args) {
3090
- const result = spawnSync(command, args, { stdio: "inherit" });
3212
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
3091
3213
  return result.status ?? -1;
3092
3214
  }
3093
- function getUserNpmPrefix() {
3094
- return join5(homedir3(), ".npm-global");
3095
- }
3096
- function resolveCodexCommand() {
3097
- if (canRun("codex", ["--version"])) {
3098
- return "codex";
3099
- }
3100
- const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
3101
- if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
3102
- return userCandidate;
3103
- }
3104
- const prefix = process.env.PREFIX?.trim();
3105
- if (!prefix) {
3106
- return null;
3107
- }
3108
- const candidate = join5(prefix, "bin", "codex");
3109
- if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
3110
- return candidate;
3111
- }
3112
- return null;
3113
- }
3114
3215
  function resolveCloudflaredCommand() {
3115
3216
  if (canRun("cloudflared", ["--version"])) {
3116
3217
  return "cloudflared";
3117
3218
  }
3118
- const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
3119
- if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
3219
+ const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
3220
+ if (existsSync5(localCandidate) && canRun(localCandidate, ["--version"])) {
3120
3221
  return localCandidate;
3121
3222
  }
3122
3223
  return null;
@@ -3169,9 +3270,9 @@ async function ensureCloudflaredInstalledLinux() {
3169
3270
  if (!mappedArch) {
3170
3271
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
3171
3272
  }
3172
- const userBinDir = join5(homedir3(), ".local", "bin");
3273
+ const userBinDir = join6(homedir4(), ".local", "bin");
3173
3274
  mkdirSync(userBinDir, { recursive: true });
3174
- const destination = join5(userBinDir, "cloudflared");
3275
+ const destination = join6(userBinDir, "cloudflared");
3175
3276
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
3176
3277
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
3177
3278
  await downloadFile(downloadUrl, destination);
@@ -3210,8 +3311,8 @@ async function resolveCloudflaredForTunnel() {
3210
3311
  return ensureCloudflaredInstalledLinux();
3211
3312
  }
3212
3313
  function hasCodexAuth() {
3213
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3214
- return existsSync4(join5(codexHome, "auth.json"));
3314
+ const codexHome = process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
3315
+ return existsSync5(join6(codexHome, "auth.json"));
3215
3316
  }
3216
3317
  function ensureCodexInstalled() {
3217
3318
  let codexCommand = resolveCodexCommand();
@@ -3229,7 +3330,7 @@ function ensureCodexInstalled() {
3229
3330
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
3230
3331
  `);
3231
3332
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
3232
- process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3333
+ process.env.PATH = `${join6(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3233
3334
  };
3234
3335
  if (isTermuxRuntime()) {
3235
3336
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -3370,8 +3471,8 @@ function listenWithFallback(server, startPort) {
3370
3471
  });
3371
3472
  }
3372
3473
  function getCodexGlobalStatePath2() {
3373
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3374
- return join5(codexHome, ".codex-global-state.json");
3474
+ const codexHome = process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
3475
+ return join6(codexHome, ".codex-global-state.json");
3375
3476
  }
3376
3477
  function normalizeUniqueStrings(value) {
3377
3478
  if (!Array.isArray(value)) return [];