codexapp 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cli/index.js CHANGED
@@ -3,30 +3,29 @@
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
5
  import { existsSync as existsSync2 } from "fs";
6
- import { readFile as readFile2 } from "fs/promises";
6
+ import { readFile as readFile3 } from "fs/promises";
7
7
  import { homedir as homedir2 } from "os";
8
- import { join as join3 } from "path";
8
+ import { join as join4 } from "path";
9
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
- import { dirname as dirname2 } from "path";
11
+ import { dirname as dirname3 } from "path";
12
12
  import { Command } from "commander";
13
13
  import qrcode from "qrcode-terminal";
14
14
 
15
15
  // src/server/httpServer.ts
16
16
  import { fileURLToPath } from "url";
17
- import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
17
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
18
18
  import { existsSync } from "fs";
19
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
19
+ import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
20
20
  import express from "express";
21
21
 
22
22
  // src/server/codexAppServerBridge.ts
23
23
  import { spawn } from "child_process";
24
- import { randomBytes } from "crypto";
25
24
  import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
26
25
  import { request as httpsRequest } from "https";
27
26
  import { homedir } from "os";
28
27
  import { tmpdir } from "os";
29
- import { basename, isAbsolute, join, resolve } from "path";
28
+ import { isAbsolute, join, resolve } from "path";
30
29
  import { writeFile } from "fs/promises";
31
30
  function asRecord(value) {
32
31
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -123,55 +122,6 @@ async function runCommand(command, args, options = {}) {
123
122
  });
124
123
  });
125
124
  }
126
- function isMissingHeadError(error) {
127
- const message = getErrorMessage(error, "").toLowerCase();
128
- return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
129
- }
130
- function isNotGitRepositoryError(error) {
131
- const message = getErrorMessage(error, "").toLowerCase();
132
- return message.includes("not a git repository") || message.includes("fatal: not a git repository");
133
- }
134
- async function ensureRepoHasInitialCommit(repoRoot) {
135
- const agentsPath = join(repoRoot, "AGENTS.md");
136
- try {
137
- await stat(agentsPath);
138
- } catch {
139
- await writeFile(agentsPath, "", "utf8");
140
- }
141
- await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
142
- await runCommand(
143
- "git",
144
- ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
145
- { cwd: repoRoot }
146
- );
147
- }
148
- async function runCommandCapture(command, args, options = {}) {
149
- return await new Promise((resolveOutput, reject) => {
150
- const proc = spawn(command, args, {
151
- cwd: options.cwd,
152
- env: process.env,
153
- stdio: ["ignore", "pipe", "pipe"]
154
- });
155
- let stdout = "";
156
- let stderr = "";
157
- proc.stdout.on("data", (chunk) => {
158
- stdout += chunk.toString();
159
- });
160
- proc.stderr.on("data", (chunk) => {
161
- stderr += chunk.toString();
162
- });
163
- proc.on("error", reject);
164
- proc.on("close", (code) => {
165
- if (code === 0) {
166
- resolveOutput(stdout.trim());
167
- return;
168
- }
169
- const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
170
- const suffix = details.length > 0 ? `: ${details}` : "";
171
- reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
172
- });
173
- });
174
- }
175
125
  async function detectUserSkillsDir(appServer) {
176
126
  try {
177
127
  const result = await appServer.rpc("skills/list", {});
@@ -959,76 +909,6 @@ function createCodexBridgeMiddleware() {
959
909
  setJson(res, 200, { data: { path: homedir() } });
960
910
  return;
961
911
  }
962
- if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
963
- const payload = asRecord(await readJsonBody(req));
964
- const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
965
- if (!rawSourceCwd) {
966
- setJson(res, 400, { error: "Missing sourceCwd" });
967
- return;
968
- }
969
- const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
970
- try {
971
- const sourceInfo = await stat(sourceCwd);
972
- if (!sourceInfo.isDirectory()) {
973
- setJson(res, 400, { error: "sourceCwd is not a directory" });
974
- return;
975
- }
976
- } catch {
977
- setJson(res, 404, { error: "sourceCwd does not exist" });
978
- return;
979
- }
980
- try {
981
- let gitRoot = "";
982
- try {
983
- gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
984
- } catch (error) {
985
- if (!isNotGitRepositoryError(error)) throw error;
986
- await runCommand("git", ["init"], { cwd: sourceCwd });
987
- gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
988
- }
989
- const repoName = basename(gitRoot) || "repo";
990
- const worktreesRoot = join(getCodexHomeDir(), "worktrees");
991
- await mkdir(worktreesRoot, { recursive: true });
992
- let worktreeId = "";
993
- let worktreeParent = "";
994
- let worktreeCwd = "";
995
- for (let attempt = 0; attempt < 12; attempt += 1) {
996
- const candidate = randomBytes(2).toString("hex");
997
- const parent = join(worktreesRoot, candidate);
998
- try {
999
- await stat(parent);
1000
- continue;
1001
- } catch {
1002
- worktreeId = candidate;
1003
- worktreeParent = parent;
1004
- worktreeCwd = join(parent, repoName);
1005
- break;
1006
- }
1007
- }
1008
- if (!worktreeId || !worktreeParent || !worktreeCwd) {
1009
- throw new Error("Failed to allocate a unique worktree id");
1010
- }
1011
- const branch = `codex/${worktreeId}`;
1012
- await mkdir(worktreeParent, { recursive: true });
1013
- try {
1014
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1015
- } catch (error) {
1016
- if (!isMissingHeadError(error)) throw error;
1017
- await ensureRepoHasInitialCommit(gitRoot);
1018
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1019
- }
1020
- setJson(res, 200, {
1021
- data: {
1022
- cwd: worktreeCwd,
1023
- branch,
1024
- gitRoot
1025
- }
1026
- });
1027
- } catch (error) {
1028
- setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
1029
- }
1030
- return;
1031
- }
1032
912
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1033
913
  const payload = await readJsonBody(req);
1034
914
  const record = asRecord(payload);
@@ -1331,7 +1211,7 @@ data: ${JSON.stringify({ ok: true })}
1331
1211
  }
1332
1212
 
1333
1213
  // src/server/authMiddleware.ts
1334
- import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1214
+ import { randomBytes, timingSafeEqual } from "crypto";
1335
1215
  var TOKEN_COOKIE = "codex_web_local_token";
1336
1216
  function constantTimeCompare(a, b) {
1337
1217
  const bufA = Buffer.from(a);
@@ -1429,7 +1309,7 @@ function createAuthSession(password) {
1429
1309
  res.status(401).json({ error: "Invalid password" });
1430
1310
  return;
1431
1311
  }
1432
- const token = randomBytes2(32).toString("hex");
1312
+ const token = randomBytes(32).toString("hex");
1433
1313
  validTokens.add(token);
1434
1314
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1435
1315
  res.json({ ok: true });
@@ -1448,32 +1328,74 @@ function createAuthSession(password) {
1448
1328
  };
1449
1329
  }
1450
1330
 
1451
- // src/server/httpServer.ts
1452
- import { WebSocketServer } from "ws";
1453
- var __dirname = dirname(fileURLToPath(import.meta.url));
1454
- var distDir = join2(__dirname, "..", "dist");
1455
- var spaEntryFile = join2(distDir, "index.html");
1456
- var IMAGE_CONTENT_TYPES = {
1457
- ".avif": "image/avif",
1458
- ".bmp": "image/bmp",
1459
- ".gif": "image/gif",
1460
- ".jpeg": "image/jpeg",
1461
- ".jpg": "image/jpeg",
1462
- ".png": "image/png",
1463
- ".svg": "image/svg+xml",
1464
- ".webp": "image/webp"
1465
- };
1466
- function normalizeLocalImagePath(rawPath) {
1467
- const trimmed = rawPath.trim();
1468
- if (!trimmed) return "";
1469
- if (trimmed.startsWith("file://")) {
1470
- try {
1471
- return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1472
- } catch {
1473
- return trimmed.replace(/^file:\/\//u, "");
1474
- }
1331
+ // src/server/localBrowseUi.ts
1332
+ import { dirname, extname, join as join2 } from "path";
1333
+ import { readFile as readFile2, readdir as readdir2, stat as stat2 } from "fs/promises";
1334
+ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1335
+ ".txt",
1336
+ ".md",
1337
+ ".json",
1338
+ ".js",
1339
+ ".ts",
1340
+ ".tsx",
1341
+ ".jsx",
1342
+ ".css",
1343
+ ".scss",
1344
+ ".html",
1345
+ ".htm",
1346
+ ".xml",
1347
+ ".yml",
1348
+ ".yaml",
1349
+ ".log",
1350
+ ".csv",
1351
+ ".env",
1352
+ ".py",
1353
+ ".sh",
1354
+ ".toml",
1355
+ ".ini",
1356
+ ".conf",
1357
+ ".sql"
1358
+ ]);
1359
+ function languageForPath(pathValue) {
1360
+ const extension = extname(pathValue).toLowerCase();
1361
+ switch (extension) {
1362
+ case ".js":
1363
+ return "javascript";
1364
+ case ".ts":
1365
+ return "typescript";
1366
+ case ".jsx":
1367
+ return "javascript";
1368
+ case ".tsx":
1369
+ return "typescript";
1370
+ case ".py":
1371
+ return "python";
1372
+ case ".sh":
1373
+ return "sh";
1374
+ case ".css":
1375
+ case ".scss":
1376
+ return "css";
1377
+ case ".html":
1378
+ case ".htm":
1379
+ return "html";
1380
+ case ".json":
1381
+ return "json";
1382
+ case ".md":
1383
+ return "markdown";
1384
+ case ".yaml":
1385
+ case ".yml":
1386
+ return "yaml";
1387
+ case ".xml":
1388
+ return "xml";
1389
+ case ".sql":
1390
+ return "sql";
1391
+ case ".toml":
1392
+ return "ini";
1393
+ case ".ini":
1394
+ case ".conf":
1395
+ return "ini";
1396
+ default:
1397
+ return "plaintext";
1475
1398
  }
1476
- return trimmed;
1477
1399
  }
1478
1400
  function normalizeLocalPath(rawPath) {
1479
1401
  const trimmed = rawPath.trim();
@@ -1495,39 +1417,72 @@ function decodeBrowsePath(rawPath) {
1495
1417
  return rawPath;
1496
1418
  }
1497
1419
  }
1420
+ function isTextEditablePath(pathValue) {
1421
+ return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
1422
+ }
1498
1423
  function escapeHtml(value) {
1499
1424
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
1500
1425
  }
1501
1426
  function toBrowseHref(pathValue) {
1502
1427
  return `/codex-local-browse${encodeURI(pathValue)}`;
1503
1428
  }
1504
- async function renderDirectoryListing(res, localPath) {
1429
+ function toEditHref(pathValue) {
1430
+ return `/codex-local-edit${encodeURI(pathValue)}`;
1431
+ }
1432
+ function escapeForInlineScriptString(value) {
1433
+ return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
1434
+ }
1435
+ async function getDirectoryItems(localPath) {
1505
1436
  const entries = await readdir2(localPath, { withFileTypes: true });
1506
- const sorted = entries.slice().sort((a, b) => {
1507
- if (a.isDirectory() && !b.isDirectory()) return -1;
1508
- if (!a.isDirectory() && b.isDirectory()) return 1;
1437
+ const withMeta = await Promise.all(entries.map(async (entry) => {
1438
+ const entryPath = join2(localPath, entry.name);
1439
+ const entryStat = await stat2(entryPath);
1440
+ return {
1441
+ name: entry.name,
1442
+ path: entryPath,
1443
+ isDirectory: entry.isDirectory(),
1444
+ editable: !entry.isDirectory() && isTextEditablePath(entryPath),
1445
+ mtimeMs: entryStat.mtimeMs
1446
+ };
1447
+ }));
1448
+ return withMeta.sort((a, b) => {
1449
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
1450
+ if (a.isDirectory && !b.isDirectory) return -1;
1451
+ if (!a.isDirectory && b.isDirectory) return 1;
1509
1452
  return a.name.localeCompare(b.name);
1510
1453
  });
1454
+ }
1455
+ async function createDirectoryListingHtml(localPath) {
1456
+ const items = await getDirectoryItems(localPath);
1511
1457
  const parentPath = dirname(localPath);
1512
- const rows = sorted.map((entry) => {
1513
- const entryPath = join2(localPath, entry.name);
1514
- const suffix = entry.isDirectory() ? "/" : "";
1515
- return `<li><a href="${escapeHtml(toBrowseHref(entryPath))}">${escapeHtml(entry.name)}${suffix}</a></li>`;
1458
+ const rows = items.map((item) => {
1459
+ const suffix = item.isDirectory ? "/" : "";
1460
+ const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
1461
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
1516
1462
  }).join("\n");
1517
1463
  const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
1518
- const html = `<!doctype html>
1464
+ return `<!doctype html>
1519
1465
  <html lang="en">
1520
1466
  <head>
1521
1467
  <meta charset="utf-8" />
1522
1468
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1523
1469
  <title>Index of ${escapeHtml(localPath)}</title>
1524
1470
  <style>
1525
- body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
1471
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
1526
1472
  a { color: #8cc2ff; text-decoration: none; }
1527
1473
  a:hover { text-decoration: underline; }
1528
- ul { list-style: none; padding: 0; margin: 12px 0 0; }
1529
- li { padding: 3px 0; }
1474
+ ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
1475
+ .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
1476
+ .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
1477
+ .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; }
1478
+ .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
1530
1479
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
1480
+ @media (max-width: 640px) {
1481
+ body { margin: 12px; }
1482
+ .file-row { gap: 8px; }
1483
+ .file-link { font-size: 15px; padding: 12px; }
1484
+ .icon-btn { width: 44px; height: 44px; }
1485
+ }
1531
1486
  </style>
1532
1487
  </head>
1533
1488
  <body>
@@ -1536,7 +1491,107 @@ async function renderDirectoryListing(res, localPath) {
1536
1491
  <ul>${rows}</ul>
1537
1492
  </body>
1538
1493
  </html>`;
1539
- res.status(200).type("text/html; charset=utf-8").send(html);
1494
+ }
1495
+ async function createTextEditorHtml(localPath) {
1496
+ const content = await readFile2(localPath, "utf8");
1497
+ const parentPath = dirname(localPath);
1498
+ const language = languageForPath(localPath);
1499
+ const safeContentLiteral = escapeForInlineScriptString(content);
1500
+ return `<!doctype html>
1501
+ <html lang="en">
1502
+ <head>
1503
+ <meta charset="utf-8" />
1504
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1505
+ <title>Edit ${escapeHtml(localPath)}</title>
1506
+ <style>
1507
+ html, body { width: 100%; height: 100%; margin: 0; }
1508
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
1509
+ .toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: #0b1020; border-bottom: 1px solid #243a5a; }
1510
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
1511
+ button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
1512
+ button:hover, a:hover { filter: brightness(1.08); }
1513
+ #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
1514
+ #status { margin-left: 8px; color: #8cc2ff; }
1515
+ .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
1516
+ .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
1517
+ .ace_marker-layer .ace_active-line { background: #10213c !important; }
1518
+ .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
1519
+ .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
1520
+ </style>
1521
+ </head>
1522
+ <body>
1523
+ <div class="toolbar">
1524
+ <div class="row">
1525
+ <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
1526
+ <button id="saveBtn" type="button">Save</button>
1527
+ <span id="status"></span>
1528
+ </div>
1529
+ <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
1530
+ </div>
1531
+ <div id="editor"></div>
1532
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
1533
+ <script>
1534
+ const saveBtn = document.getElementById('saveBtn');
1535
+ const status = document.getElementById('status');
1536
+ const editor = ace.edit('editor');
1537
+ editor.setTheme('ace/theme/tomorrow_night');
1538
+ editor.session.setMode('ace/mode/${escapeHtml(language)}');
1539
+ editor.setValue(${safeContentLiteral}, -1);
1540
+ editor.setOptions({
1541
+ fontSize: '13px',
1542
+ wrap: true,
1543
+ showPrintMargin: false,
1544
+ useSoftTabs: true,
1545
+ tabSize: 2,
1546
+ behavioursEnabled: true,
1547
+ });
1548
+ editor.resize();
1549
+
1550
+ saveBtn.addEventListener('click', async () => {
1551
+ status.textContent = 'Saving...';
1552
+ const response = await fetch(location.pathname, {
1553
+ method: 'PUT',
1554
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
1555
+ body: editor.getValue(),
1556
+ });
1557
+ status.textContent = response.ok ? 'Saved' : 'Save failed';
1558
+ });
1559
+ </script>
1560
+ </body>
1561
+ </html>`;
1562
+ }
1563
+
1564
+ // src/server/httpServer.ts
1565
+ import { WebSocketServer } from "ws";
1566
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
1567
+ var distDir = join3(__dirname, "..", "dist");
1568
+ var spaEntryFile = join3(distDir, "index.html");
1569
+ var IMAGE_CONTENT_TYPES = {
1570
+ ".avif": "image/avif",
1571
+ ".bmp": "image/bmp",
1572
+ ".gif": "image/gif",
1573
+ ".jpeg": "image/jpeg",
1574
+ ".jpg": "image/jpeg",
1575
+ ".png": "image/png",
1576
+ ".svg": "image/svg+xml",
1577
+ ".webp": "image/webp"
1578
+ };
1579
+ function normalizeLocalImagePath(rawPath) {
1580
+ const trimmed = rawPath.trim();
1581
+ if (!trimmed) return "";
1582
+ if (trimmed.startsWith("file://")) {
1583
+ try {
1584
+ return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1585
+ } catch {
1586
+ return trimmed.replace(/^file:\/\//u, "");
1587
+ }
1588
+ }
1589
+ return trimmed;
1590
+ }
1591
+ function readWildcardPathParam(value) {
1592
+ if (typeof value === "string") return value;
1593
+ if (Array.isArray(value)) return value.join("/");
1594
+ return "";
1540
1595
  }
1541
1596
  function createServer(options = {}) {
1542
1597
  const app = express();
@@ -1553,7 +1608,7 @@ function createServer(options = {}) {
1553
1608
  res.status(400).json({ error: "Expected absolute local file path." });
1554
1609
  return;
1555
1610
  }
1556
- const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
1611
+ const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
1557
1612
  if (!contentType) {
1558
1613
  res.status(415).json({ error: "Unsupported image type." });
1559
1614
  return;
@@ -1580,17 +1635,18 @@ function createServer(options = {}) {
1580
1635
  });
1581
1636
  });
1582
1637
  app.get("/codex-local-browse/*path", async (req, res) => {
1583
- const rawPath = typeof req.params.path === "string" ? req.params.path : "";
1638
+ const rawPath = readWildcardPathParam(req.params.path);
1584
1639
  const localPath = decodeBrowsePath(`/${rawPath}`);
1585
1640
  if (!localPath || !isAbsolute2(localPath)) {
1586
1641
  res.status(400).json({ error: "Expected absolute local file path." });
1587
1642
  return;
1588
1643
  }
1589
1644
  try {
1590
- const fileStat = await stat2(localPath);
1645
+ const fileStat = await stat3(localPath);
1591
1646
  res.setHeader("Cache-Control", "private, no-store");
1592
1647
  if (fileStat.isDirectory()) {
1593
- await renderDirectoryListing(res, localPath);
1648
+ const html = await createDirectoryListingHtml(localPath);
1649
+ res.status(200).type("text/html; charset=utf-8").send(html);
1594
1650
  return;
1595
1651
  }
1596
1652
  res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
@@ -1601,6 +1657,44 @@ function createServer(options = {}) {
1601
1657
  res.status(404).json({ error: "File not found." });
1602
1658
  }
1603
1659
  });
1660
+ app.get("/codex-local-edit/*path", async (req, res) => {
1661
+ const rawPath = readWildcardPathParam(req.params.path);
1662
+ const localPath = decodeBrowsePath(`/${rawPath}`);
1663
+ if (!localPath || !isAbsolute2(localPath)) {
1664
+ res.status(400).json({ error: "Expected absolute local file path." });
1665
+ return;
1666
+ }
1667
+ try {
1668
+ const fileStat = await stat3(localPath);
1669
+ if (!fileStat.isFile()) {
1670
+ res.status(400).json({ error: "Expected file path." });
1671
+ return;
1672
+ }
1673
+ const html = await createTextEditorHtml(localPath);
1674
+ res.status(200).type("text/html; charset=utf-8").send(html);
1675
+ } catch {
1676
+ res.status(404).json({ error: "File not found." });
1677
+ }
1678
+ });
1679
+ app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
1680
+ const rawPath = readWildcardPathParam(req.params.path);
1681
+ const localPath = decodeBrowsePath(`/${rawPath}`);
1682
+ if (!localPath || !isAbsolute2(localPath)) {
1683
+ res.status(400).json({ error: "Expected absolute local file path." });
1684
+ return;
1685
+ }
1686
+ if (!isTextEditablePath(localPath)) {
1687
+ res.status(415).json({ error: "Only text-like files are editable." });
1688
+ return;
1689
+ }
1690
+ const body = typeof req.body === "string" ? req.body : "";
1691
+ try {
1692
+ await writeFile2(localPath, body, "utf8");
1693
+ res.status(200).json({ ok: true });
1694
+ } catch {
1695
+ res.status(404).json({ error: "File not found." });
1696
+ }
1697
+ });
1604
1698
  const hasFrontendAssets = existsSync(spaEntryFile);
1605
1699
  if (hasFrontendAssets) {
1606
1700
  app.use(express.static(distDir));
@@ -1672,11 +1766,11 @@ function generatePassword() {
1672
1766
 
1673
1767
  // src/cli/index.ts
1674
1768
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
1675
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1769
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1676
1770
  async function readCliVersion() {
1677
1771
  try {
1678
- const packageJsonPath = join3(__dirname2, "..", "package.json");
1679
- const raw = await readFile2(packageJsonPath, "utf8");
1772
+ const packageJsonPath = join4(__dirname2, "..", "package.json");
1773
+ const raw = await readFile3(packageJsonPath, "utf8");
1680
1774
  const parsed = JSON.parse(raw);
1681
1775
  return typeof parsed.version === "string" ? parsed.version : "unknown";
1682
1776
  } catch {
@@ -1701,13 +1795,13 @@ function runWithStatus(command, args) {
1701
1795
  return result.status ?? -1;
1702
1796
  }
1703
1797
  function getUserNpmPrefix() {
1704
- return join3(homedir2(), ".npm-global");
1798
+ return join4(homedir2(), ".npm-global");
1705
1799
  }
1706
1800
  function resolveCodexCommand() {
1707
1801
  if (canRun("codex", ["--version"])) {
1708
1802
  return "codex";
1709
1803
  }
1710
- const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
1804
+ const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
1711
1805
  if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
1712
1806
  return userCandidate;
1713
1807
  }
@@ -1715,15 +1809,15 @@ function resolveCodexCommand() {
1715
1809
  if (!prefix) {
1716
1810
  return null;
1717
1811
  }
1718
- const candidate = join3(prefix, "bin", "codex");
1812
+ const candidate = join4(prefix, "bin", "codex");
1719
1813
  if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
1720
1814
  return candidate;
1721
1815
  }
1722
1816
  return null;
1723
1817
  }
1724
1818
  function hasCodexAuth() {
1725
- const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1726
- return existsSync2(join3(codexHome, "auth.json"));
1819
+ const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
1820
+ return existsSync2(join4(codexHome, "auth.json"));
1727
1821
  }
1728
1822
  function ensureCodexInstalled() {
1729
1823
  let codexCommand = resolveCodexCommand();
@@ -1741,7 +1835,7 @@ function ensureCodexInstalled() {
1741
1835
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
1742
1836
  `);
1743
1837
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
1744
- process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1838
+ process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1745
1839
  };
1746
1840
  if (isTermuxRuntime()) {
1747
1841
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");