codexapp 0.1.25 → 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
@@ -2,32 +2,30 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
6
- import { readFile as readFile2 } from "fs/promises";
5
+ import { existsSync as existsSync2 } from "fs";
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";
12
- import { get as httpsGet } from "https";
11
+ import { dirname as dirname3 } from "path";
13
12
  import { Command } from "commander";
14
13
  import qrcode from "qrcode-terminal";
15
14
 
16
15
  // src/server/httpServer.ts
17
16
  import { fileURLToPath } from "url";
18
- 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";
19
18
  import { existsSync } from "fs";
20
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
19
+ import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
21
20
  import express from "express";
22
21
 
23
22
  // src/server/codexAppServerBridge.ts
24
23
  import { spawn } from "child_process";
25
- import { randomBytes } from "crypto";
26
24
  import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
27
25
  import { request as httpsRequest } from "https";
28
26
  import { homedir } from "os";
29
27
  import { tmpdir } from "os";
30
- import { basename, isAbsolute, join, resolve } from "path";
28
+ import { isAbsolute, join, resolve } from "path";
31
29
  import { writeFile } from "fs/promises";
32
30
  function asRecord(value) {
33
31
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -51,46 +49,6 @@ function setJson(res, statusCode, payload) {
51
49
  res.setHeader("Content-Type", "application/json; charset=utf-8");
52
50
  res.end(JSON.stringify(payload));
53
51
  }
54
- function extractThreadMessageText(threadReadPayload) {
55
- const payload = asRecord(threadReadPayload);
56
- const thread = asRecord(payload?.thread);
57
- const turns = Array.isArray(thread?.turns) ? thread.turns : [];
58
- const parts = [];
59
- for (const turn of turns) {
60
- const turnRecord = asRecord(turn);
61
- const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
62
- for (const item of items) {
63
- const itemRecord = asRecord(item);
64
- const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
65
- if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
66
- parts.push(itemRecord.text.trim());
67
- continue;
68
- }
69
- if (type === "userMessage") {
70
- const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
71
- for (const block of content) {
72
- const blockRecord = asRecord(block);
73
- if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
74
- parts.push(blockRecord.text.trim());
75
- }
76
- }
77
- continue;
78
- }
79
- if (type === "commandExecution") {
80
- const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
81
- const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
82
- if (command) parts.push(command);
83
- if (output) parts.push(output);
84
- }
85
- }
86
- }
87
- return parts.join("\n").trim();
88
- }
89
- function isExactPhraseMatch(query, doc) {
90
- const q = query.trim().toLowerCase();
91
- if (!q) return false;
92
- return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
93
- }
94
52
  function scoreFileCandidate(path, query) {
95
53
  if (!query) return 0;
96
54
  const lowerPath = path.toLowerCase();
@@ -164,55 +122,6 @@ async function runCommand(command, args, options = {}) {
164
122
  });
165
123
  });
166
124
  }
167
- function isMissingHeadError(error) {
168
- const message = getErrorMessage(error, "").toLowerCase();
169
- return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
170
- }
171
- function isNotGitRepositoryError(error) {
172
- const message = getErrorMessage(error, "").toLowerCase();
173
- return message.includes("not a git repository") || message.includes("fatal: not a git repository");
174
- }
175
- async function ensureRepoHasInitialCommit(repoRoot) {
176
- const agentsPath = join(repoRoot, "AGENTS.md");
177
- try {
178
- await stat(agentsPath);
179
- } catch {
180
- await writeFile(agentsPath, "", "utf8");
181
- }
182
- await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
183
- await runCommand(
184
- "git",
185
- ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
186
- { cwd: repoRoot }
187
- );
188
- }
189
- async function runCommandCapture(command, args, options = {}) {
190
- return await new Promise((resolveOutput, reject) => {
191
- const proc = spawn(command, args, {
192
- cwd: options.cwd,
193
- env: process.env,
194
- stdio: ["ignore", "pipe", "pipe"]
195
- });
196
- let stdout = "";
197
- let stderr = "";
198
- proc.stdout.on("data", (chunk) => {
199
- stdout += chunk.toString();
200
- });
201
- proc.stderr.on("data", (chunk) => {
202
- stderr += chunk.toString();
203
- });
204
- proc.on("error", reject);
205
- proc.on("close", (code) => {
206
- if (code === 0) {
207
- resolveOutput(stdout.trim());
208
- return;
209
- }
210
- const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
211
- const suffix = details.length > 0 ? `: ${details}` : "";
212
- reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
213
- });
214
- });
215
- }
216
125
  async function detectUserSkillsDir(appServer) {
217
126
  try {
218
127
  const result = await appServer.rpc("skills/list", {});
@@ -933,82 +842,8 @@ function getSharedBridgeState() {
933
842
  globalScope[SHARED_BRIDGE_KEY] = created;
934
843
  return created;
935
844
  }
936
- async function loadAllThreadsForSearch(appServer) {
937
- const threads = [];
938
- let cursor = null;
939
- do {
940
- const response = asRecord(await appServer.rpc("thread/list", {
941
- archived: false,
942
- limit: 100,
943
- sortKey: "updated_at",
944
- cursor
945
- }));
946
- const data = Array.isArray(response?.data) ? response.data : [];
947
- for (const row of data) {
948
- const record = asRecord(row);
949
- const id = typeof record?.id === "string" ? record.id : "";
950
- if (!id) continue;
951
- const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
952
- const preview = typeof record?.preview === "string" ? record.preview : "";
953
- threads.push({ id, title, preview });
954
- }
955
- cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
956
- } while (cursor);
957
- const docs = [];
958
- const concurrency = 4;
959
- for (let offset = 0; offset < threads.length; offset += concurrency) {
960
- const batch = threads.slice(offset, offset + concurrency);
961
- const loaded = await Promise.all(batch.map(async (thread) => {
962
- try {
963
- const readResponse = await appServer.rpc("thread/read", {
964
- threadId: thread.id,
965
- includeTurns: true
966
- });
967
- const messageText = extractThreadMessageText(readResponse);
968
- const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
969
- return {
970
- id: thread.id,
971
- title: thread.title,
972
- preview: thread.preview,
973
- messageText,
974
- searchableText
975
- };
976
- } catch {
977
- const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
978
- return {
979
- id: thread.id,
980
- title: thread.title,
981
- preview: thread.preview,
982
- messageText: "",
983
- searchableText
984
- };
985
- }
986
- }));
987
- docs.push(...loaded);
988
- }
989
- return docs;
990
- }
991
- async function buildThreadSearchIndex(appServer) {
992
- const docs = await loadAllThreadsForSearch(appServer);
993
- const docsById = new Map(docs.map((doc) => [doc.id, doc]));
994
- return { docsById };
995
- }
996
845
  function createCodexBridgeMiddleware() {
997
846
  const { appServer, methodCatalog } = getSharedBridgeState();
998
- let threadSearchIndex = null;
999
- let threadSearchIndexPromise = null;
1000
- async function getThreadSearchIndex() {
1001
- if (threadSearchIndex) return threadSearchIndex;
1002
- if (!threadSearchIndexPromise) {
1003
- threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
1004
- threadSearchIndex = index;
1005
- return index;
1006
- }).finally(() => {
1007
- threadSearchIndexPromise = null;
1008
- });
1009
- }
1010
- return threadSearchIndexPromise;
1011
- }
1012
847
  const middleware = async (req, res, next) => {
1013
848
  try {
1014
849
  if (!req.url) {
@@ -1074,76 +909,6 @@ function createCodexBridgeMiddleware() {
1074
909
  setJson(res, 200, { data: { path: homedir() } });
1075
910
  return;
1076
911
  }
1077
- if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
1078
- const payload = asRecord(await readJsonBody(req));
1079
- const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
1080
- if (!rawSourceCwd) {
1081
- setJson(res, 400, { error: "Missing sourceCwd" });
1082
- return;
1083
- }
1084
- const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
1085
- try {
1086
- const sourceInfo = await stat(sourceCwd);
1087
- if (!sourceInfo.isDirectory()) {
1088
- setJson(res, 400, { error: "sourceCwd is not a directory" });
1089
- return;
1090
- }
1091
- } catch {
1092
- setJson(res, 404, { error: "sourceCwd does not exist" });
1093
- return;
1094
- }
1095
- try {
1096
- let gitRoot = "";
1097
- try {
1098
- gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1099
- } catch (error) {
1100
- if (!isNotGitRepositoryError(error)) throw error;
1101
- await runCommand("git", ["init"], { cwd: sourceCwd });
1102
- gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1103
- }
1104
- const repoName = basename(gitRoot) || "repo";
1105
- const worktreesRoot = join(getCodexHomeDir(), "worktrees");
1106
- await mkdir(worktreesRoot, { recursive: true });
1107
- let worktreeId = "";
1108
- let worktreeParent = "";
1109
- let worktreeCwd = "";
1110
- for (let attempt = 0; attempt < 12; attempt += 1) {
1111
- const candidate = randomBytes(2).toString("hex");
1112
- const parent = join(worktreesRoot, candidate);
1113
- try {
1114
- await stat(parent);
1115
- continue;
1116
- } catch {
1117
- worktreeId = candidate;
1118
- worktreeParent = parent;
1119
- worktreeCwd = join(parent, repoName);
1120
- break;
1121
- }
1122
- }
1123
- if (!worktreeId || !worktreeParent || !worktreeCwd) {
1124
- throw new Error("Failed to allocate a unique worktree id");
1125
- }
1126
- const branch = `codex/${worktreeId}`;
1127
- await mkdir(worktreeParent, { recursive: true });
1128
- try {
1129
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1130
- } catch (error) {
1131
- if (!isMissingHeadError(error)) throw error;
1132
- await ensureRepoHasInitialCommit(gitRoot);
1133
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1134
- }
1135
- setJson(res, 200, {
1136
- data: {
1137
- cwd: worktreeCwd,
1138
- branch,
1139
- gitRoot
1140
- }
1141
- });
1142
- } catch (error) {
1143
- setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
1144
- }
1145
- return;
1146
- }
1147
912
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1148
913
  const payload = await readJsonBody(req);
1149
914
  const record = asRecord(payload);
@@ -1269,20 +1034,6 @@ function createCodexBridgeMiddleware() {
1269
1034
  setJson(res, 200, { data: cache });
1270
1035
  return;
1271
1036
  }
1272
- if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1273
- const payload = asRecord(await readJsonBody(req));
1274
- const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1275
- const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1276
- const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1277
- if (!query) {
1278
- setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1279
- return;
1280
- }
1281
- const index = await getThreadSearchIndex();
1282
- const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
1283
- setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1284
- return;
1285
- }
1286
1037
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1287
1038
  const payload = asRecord(await readJsonBody(req));
1288
1039
  const id = typeof payload?.id === "string" ? payload.id : "";
@@ -1446,7 +1197,6 @@ data: ${JSON.stringify({ ok: true })}
1446
1197
  }
1447
1198
  };
1448
1199
  middleware.dispose = () => {
1449
- threadSearchIndex = null;
1450
1200
  appServer.dispose();
1451
1201
  };
1452
1202
  middleware.subscribeNotifications = (listener) => {
@@ -1461,7 +1211,7 @@ data: ${JSON.stringify({ ok: true })}
1461
1211
  }
1462
1212
 
1463
1213
  // src/server/authMiddleware.ts
1464
- import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1214
+ import { randomBytes, timingSafeEqual } from "crypto";
1465
1215
  var TOKEN_COOKIE = "codex_web_local_token";
1466
1216
  function constantTimeCompare(a, b) {
1467
1217
  const bufA = Buffer.from(a);
@@ -1559,7 +1309,7 @@ function createAuthSession(password) {
1559
1309
  res.status(401).json({ error: "Invalid password" });
1560
1310
  return;
1561
1311
  }
1562
- const token = randomBytes2(32).toString("hex");
1312
+ const token = randomBytes(32).toString("hex");
1563
1313
  validTokens.add(token);
1564
1314
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1565
1315
  res.json({ ok: true });
@@ -1578,32 +1328,74 @@ function createAuthSession(password) {
1578
1328
  };
1579
1329
  }
1580
1330
 
1581
- // src/server/httpServer.ts
1582
- import { WebSocketServer } from "ws";
1583
- var __dirname = dirname(fileURLToPath(import.meta.url));
1584
- var distDir = join2(__dirname, "..", "dist");
1585
- var spaEntryFile = join2(distDir, "index.html");
1586
- var IMAGE_CONTENT_TYPES = {
1587
- ".avif": "image/avif",
1588
- ".bmp": "image/bmp",
1589
- ".gif": "image/gif",
1590
- ".jpeg": "image/jpeg",
1591
- ".jpg": "image/jpeg",
1592
- ".png": "image/png",
1593
- ".svg": "image/svg+xml",
1594
- ".webp": "image/webp"
1595
- };
1596
- function normalizeLocalImagePath(rawPath) {
1597
- const trimmed = rawPath.trim();
1598
- if (!trimmed) return "";
1599
- if (trimmed.startsWith("file://")) {
1600
- try {
1601
- return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1602
- } catch {
1603
- return trimmed.replace(/^file:\/\//u, "");
1604
- }
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";
1605
1398
  }
1606
- return trimmed;
1607
1399
  }
1608
1400
  function normalizeLocalPath(rawPath) {
1609
1401
  const trimmed = rawPath.trim();
@@ -1625,39 +1417,72 @@ function decodeBrowsePath(rawPath) {
1625
1417
  return rawPath;
1626
1418
  }
1627
1419
  }
1420
+ function isTextEditablePath(pathValue) {
1421
+ return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
1422
+ }
1628
1423
  function escapeHtml(value) {
1629
1424
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
1630
1425
  }
1631
1426
  function toBrowseHref(pathValue) {
1632
1427
  return `/codex-local-browse${encodeURI(pathValue)}`;
1633
1428
  }
1634
- 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) {
1635
1436
  const entries = await readdir2(localPath, { withFileTypes: true });
1636
- const sorted = entries.slice().sort((a, b) => {
1637
- if (a.isDirectory() && !b.isDirectory()) return -1;
1638
- 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;
1639
1452
  return a.name.localeCompare(b.name);
1640
1453
  });
1454
+ }
1455
+ async function createDirectoryListingHtml(localPath) {
1456
+ const items = await getDirectoryItems(localPath);
1641
1457
  const parentPath = dirname(localPath);
1642
- const rows = sorted.map((entry) => {
1643
- const entryPath = join2(localPath, entry.name);
1644
- const suffix = entry.isDirectory() ? "/" : "";
1645
- 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>`;
1646
1462
  }).join("\n");
1647
1463
  const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
1648
- const html = `<!doctype html>
1464
+ return `<!doctype html>
1649
1465
  <html lang="en">
1650
1466
  <head>
1651
1467
  <meta charset="utf-8" />
1652
1468
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1653
1469
  <title>Index of ${escapeHtml(localPath)}</title>
1654
1470
  <style>
1655
- 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; }
1656
1472
  a { color: #8cc2ff; text-decoration: none; }
1657
1473
  a:hover { text-decoration: underline; }
1658
- ul { list-style: none; padding: 0; margin: 12px 0 0; }
1659
- 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; }
1660
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
+ }
1661
1486
  </style>
1662
1487
  </head>
1663
1488
  <body>
@@ -1666,7 +1491,107 @@ async function renderDirectoryListing(res, localPath) {
1666
1491
  <ul>${rows}</ul>
1667
1492
  </body>
1668
1493
  </html>`;
1669
- 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 "";
1670
1595
  }
1671
1596
  function createServer(options = {}) {
1672
1597
  const app = express();
@@ -1683,7 +1608,7 @@ function createServer(options = {}) {
1683
1608
  res.status(400).json({ error: "Expected absolute local file path." });
1684
1609
  return;
1685
1610
  }
1686
- const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
1611
+ const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
1687
1612
  if (!contentType) {
1688
1613
  res.status(415).json({ error: "Unsupported image type." });
1689
1614
  return;
@@ -1710,17 +1635,18 @@ function createServer(options = {}) {
1710
1635
  });
1711
1636
  });
1712
1637
  app.get("/codex-local-browse/*path", async (req, res) => {
1713
- const rawPath = typeof req.params.path === "string" ? req.params.path : "";
1638
+ const rawPath = readWildcardPathParam(req.params.path);
1714
1639
  const localPath = decodeBrowsePath(`/${rawPath}`);
1715
1640
  if (!localPath || !isAbsolute2(localPath)) {
1716
1641
  res.status(400).json({ error: "Expected absolute local file path." });
1717
1642
  return;
1718
1643
  }
1719
1644
  try {
1720
- const fileStat = await stat2(localPath);
1645
+ const fileStat = await stat3(localPath);
1721
1646
  res.setHeader("Cache-Control", "private, no-store");
1722
1647
  if (fileStat.isDirectory()) {
1723
- await renderDirectoryListing(res, localPath);
1648
+ const html = await createDirectoryListingHtml(localPath);
1649
+ res.status(200).type("text/html; charset=utf-8").send(html);
1724
1650
  return;
1725
1651
  }
1726
1652
  res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
@@ -1731,6 +1657,44 @@ function createServer(options = {}) {
1731
1657
  res.status(404).json({ error: "File not found." });
1732
1658
  }
1733
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
+ });
1734
1698
  const hasFrontendAssets = existsSync(spaEntryFile);
1735
1699
  if (hasFrontendAssets) {
1736
1700
  app.use(express.static(distDir));
@@ -1802,11 +1766,11 @@ function generatePassword() {
1802
1766
 
1803
1767
  // src/cli/index.ts
1804
1768
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
1805
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1769
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1806
1770
  async function readCliVersion() {
1807
1771
  try {
1808
- const packageJsonPath = join3(__dirname2, "..", "package.json");
1809
- const raw = await readFile2(packageJsonPath, "utf8");
1772
+ const packageJsonPath = join4(__dirname2, "..", "package.json");
1773
+ const raw = await readFile3(packageJsonPath, "utf8");
1810
1774
  const parsed = JSON.parse(raw);
1811
1775
  return typeof parsed.version === "string" ? parsed.version : "unknown";
1812
1776
  } catch {
@@ -1831,13 +1795,13 @@ function runWithStatus(command, args) {
1831
1795
  return result.status ?? -1;
1832
1796
  }
1833
1797
  function getUserNpmPrefix() {
1834
- return join3(homedir2(), ".npm-global");
1798
+ return join4(homedir2(), ".npm-global");
1835
1799
  }
1836
1800
  function resolveCodexCommand() {
1837
1801
  if (canRun("codex", ["--version"])) {
1838
1802
  return "codex";
1839
1803
  }
1840
- const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
1804
+ const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
1841
1805
  if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
1842
1806
  return userCandidate;
1843
1807
  }
@@ -1845,88 +1809,15 @@ function resolveCodexCommand() {
1845
1809
  if (!prefix) {
1846
1810
  return null;
1847
1811
  }
1848
- const candidate = join3(prefix, "bin", "codex");
1812
+ const candidate = join4(prefix, "bin", "codex");
1849
1813
  if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
1850
1814
  return candidate;
1851
1815
  }
1852
1816
  return null;
1853
1817
  }
1854
- function resolveCloudflaredCommand() {
1855
- if (canRun("cloudflared", ["--version"])) {
1856
- return "cloudflared";
1857
- }
1858
- const localCandidate = join3(homedir2(), ".local", "bin", "cloudflared");
1859
- if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
1860
- return localCandidate;
1861
- }
1862
- return null;
1863
- }
1864
- function mapCloudflaredLinuxArch(arch) {
1865
- if (arch === "x64") {
1866
- return "amd64";
1867
- }
1868
- if (arch === "arm64") {
1869
- return "arm64";
1870
- }
1871
- return null;
1872
- }
1873
- function downloadFile(url, destination) {
1874
- return new Promise((resolve2, reject) => {
1875
- const request = (currentUrl) => {
1876
- httpsGet(currentUrl, (response) => {
1877
- const code = response.statusCode ?? 0;
1878
- if (code >= 300 && code < 400 && response.headers.location) {
1879
- response.resume();
1880
- request(response.headers.location);
1881
- return;
1882
- }
1883
- if (code !== 200) {
1884
- response.resume();
1885
- reject(new Error(`Download failed with HTTP status ${String(code)}`));
1886
- return;
1887
- }
1888
- const file = createWriteStream(destination, { mode: 493 });
1889
- response.pipe(file);
1890
- file.on("finish", () => {
1891
- file.close();
1892
- resolve2();
1893
- });
1894
- file.on("error", reject);
1895
- }).on("error", reject);
1896
- };
1897
- request(url);
1898
- });
1899
- }
1900
- async function ensureCloudflaredInstalledLinux() {
1901
- const current = resolveCloudflaredCommand();
1902
- if (current) {
1903
- return current;
1904
- }
1905
- if (process.platform !== "linux") {
1906
- return null;
1907
- }
1908
- const mappedArch = mapCloudflaredLinuxArch(process.arch);
1909
- if (!mappedArch) {
1910
- throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
1911
- }
1912
- const userBinDir = join3(homedir2(), ".local", "bin");
1913
- mkdirSync(userBinDir, { recursive: true });
1914
- const destination = join3(userBinDir, "cloudflared");
1915
- const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
1916
- console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
1917
- await downloadFile(downloadUrl, destination);
1918
- chmodSync(destination, 493);
1919
- process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
1920
- const installed = resolveCloudflaredCommand();
1921
- if (!installed) {
1922
- throw new Error("cloudflared download completed but executable is still not available");
1923
- }
1924
- console.log("\ncloudflared installed.\n");
1925
- return installed;
1926
- }
1927
1818
  function hasCodexAuth() {
1928
- const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1929
- return existsSync2(join3(codexHome, "auth.json"));
1819
+ const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
1820
+ return existsSync2(join4(codexHome, "auth.json"));
1930
1821
  }
1931
1822
  function ensureCodexInstalled() {
1932
1823
  let codexCommand = resolveCodexCommand();
@@ -1944,7 +1835,7 @@ function ensureCodexInstalled() {
1944
1835
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
1945
1836
  `);
1946
1837
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
1947
- process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1838
+ process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1948
1839
  };
1949
1840
  if (isTermuxRuntime()) {
1950
1841
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2005,9 +1896,9 @@ function parseCloudflaredUrl(chunk) {
2005
1896
  }
2006
1897
  return urlMatch[urlMatch.length - 1] ?? null;
2007
1898
  }
2008
- async function startCloudflaredTunnel(command, localPort) {
1899
+ async function startCloudflaredTunnel(localPort) {
2009
1900
  return new Promise((resolve2, reject) => {
2010
- const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
1901
+ const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2011
1902
  stdio: ["ignore", "pipe", "pipe"]
2012
1903
  });
2013
1904
  const timeout = setTimeout(() => {
@@ -2080,8 +1971,7 @@ async function startServer(options) {
2080
1971
  let tunnelUrl = null;
2081
1972
  if (options.tunnel) {
2082
1973
  try {
2083
- const cloudflaredCommand = await ensureCloudflaredInstalledLinux() ?? "cloudflared";
2084
- const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
1974
+ const tunnel = await startCloudflaredTunnel(port);
2085
1975
  tunnelChild = tunnel.process;
2086
1976
  tunnelUrl = tunnel.url;
2087
1977
  } catch (error) {