codexapp 0.1.26 → 0.1.28

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,30 +2,32 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { existsSync as existsSync2 } from "fs";
6
- import { readFile as readFile3 } from "fs/promises";
7
- import { homedir as homedir2 } from "os";
8
- import { join as join4 } from "path";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
6
+ import { readFile as readFile2 } from "fs/promises";
7
+ import { homedir as homedir2, networkInterfaces } from "os";
8
+ import { join as join3 } from "path";
9
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
- import { dirname as dirname3 } from "path";
11
+ import { dirname as dirname2 } from "path";
12
+ import { get as httpsGet } from "https";
12
13
  import { Command } from "commander";
13
14
  import qrcode from "qrcode-terminal";
14
15
 
15
16
  // src/server/httpServer.ts
16
17
  import { fileURLToPath } from "url";
17
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
18
+ import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
18
19
  import { existsSync } from "fs";
19
- import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
20
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
20
21
  import express from "express";
21
22
 
22
23
  // src/server/codexAppServerBridge.ts
23
24
  import { spawn } from "child_process";
25
+ import { randomBytes } from "crypto";
24
26
  import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
25
27
  import { request as httpsRequest } from "https";
26
28
  import { homedir } from "os";
27
29
  import { tmpdir } from "os";
28
- import { isAbsolute, join, resolve } from "path";
30
+ import { basename, isAbsolute, join, resolve } from "path";
29
31
  import { writeFile } from "fs/promises";
30
32
  function asRecord(value) {
31
33
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -49,6 +51,46 @@ function setJson(res, statusCode, payload) {
49
51
  res.setHeader("Content-Type", "application/json; charset=utf-8");
50
52
  res.end(JSON.stringify(payload));
51
53
  }
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
+ }
52
94
  function scoreFileCandidate(path, query) {
53
95
  if (!query) return 0;
54
96
  const lowerPath = path.toLowerCase();
@@ -122,6 +164,55 @@ async function runCommand(command, args, options = {}) {
122
164
  });
123
165
  });
124
166
  }
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
+ }
125
216
  async function detectUserSkillsDir(appServer) {
126
217
  try {
127
218
  const result = await appServer.rpc("skills/list", {});
@@ -842,8 +933,82 @@ function getSharedBridgeState() {
842
933
  globalScope[SHARED_BRIDGE_KEY] = created;
843
934
  return created;
844
935
  }
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
+ }
845
996
  function createCodexBridgeMiddleware() {
846
997
  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
+ }
847
1012
  const middleware = async (req, res, next) => {
848
1013
  try {
849
1014
  if (!req.url) {
@@ -909,6 +1074,76 @@ function createCodexBridgeMiddleware() {
909
1074
  setJson(res, 200, { data: { path: homedir() } });
910
1075
  return;
911
1076
  }
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
+ }
912
1147
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
913
1148
  const payload = await readJsonBody(req);
914
1149
  const record = asRecord(payload);
@@ -1034,6 +1269,20 @@ function createCodexBridgeMiddleware() {
1034
1269
  setJson(res, 200, { data: cache });
1035
1270
  return;
1036
1271
  }
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
+ }
1037
1286
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1038
1287
  const payload = asRecord(await readJsonBody(req));
1039
1288
  const id = typeof payload?.id === "string" ? payload.id : "";
@@ -1197,6 +1446,7 @@ data: ${JSON.stringify({ ok: true })}
1197
1446
  }
1198
1447
  };
1199
1448
  middleware.dispose = () => {
1449
+ threadSearchIndex = null;
1200
1450
  appServer.dispose();
1201
1451
  };
1202
1452
  middleware.subscribeNotifications = (listener) => {
@@ -1211,7 +1461,7 @@ data: ${JSON.stringify({ ok: true })}
1211
1461
  }
1212
1462
 
1213
1463
  // src/server/authMiddleware.ts
1214
- import { randomBytes, timingSafeEqual } from "crypto";
1464
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1215
1465
  var TOKEN_COOKIE = "codex_web_local_token";
1216
1466
  function constantTimeCompare(a, b) {
1217
1467
  const bufA = Buffer.from(a);
@@ -1309,7 +1559,7 @@ function createAuthSession(password) {
1309
1559
  res.status(401).json({ error: "Invalid password" });
1310
1560
  return;
1311
1561
  }
1312
- const token = randomBytes(32).toString("hex");
1562
+ const token = randomBytes2(32).toString("hex");
1313
1563
  validTokens.add(token);
1314
1564
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1315
1565
  res.json({ ok: true });
@@ -1328,74 +1578,32 @@ function createAuthSession(password) {
1328
1578
  };
1329
1579
  }
1330
1580
 
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";
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
+ }
1398
1605
  }
1606
+ return trimmed;
1399
1607
  }
1400
1608
  function normalizeLocalPath(rawPath) {
1401
1609
  const trimmed = rawPath.trim();
@@ -1417,72 +1625,39 @@ function decodeBrowsePath(rawPath) {
1417
1625
  return rawPath;
1418
1626
  }
1419
1627
  }
1420
- function isTextEditablePath(pathValue) {
1421
- return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
1422
- }
1423
1628
  function escapeHtml(value) {
1424
1629
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
1425
1630
  }
1426
1631
  function toBrowseHref(pathValue) {
1427
1632
  return `/codex-local-browse${encodeURI(pathValue)}`;
1428
1633
  }
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) {
1634
+ async function renderDirectoryListing(res, localPath) {
1436
1635
  const entries = await readdir2(localPath, { withFileTypes: true });
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;
1636
+ const sorted = entries.slice().sort((a, b) => {
1637
+ if (a.isDirectory() && !b.isDirectory()) return -1;
1638
+ if (!a.isDirectory() && b.isDirectory()) return 1;
1452
1639
  return a.name.localeCompare(b.name);
1453
1640
  });
1454
- }
1455
- async function createDirectoryListingHtml(localPath) {
1456
- const items = await getDirectoryItems(localPath);
1457
1641
  const parentPath = dirname(localPath);
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>`;
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>`;
1462
1646
  }).join("\n");
1463
1647
  const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
1464
- return `<!doctype html>
1648
+ const html = `<!doctype html>
1465
1649
  <html lang="en">
1466
1650
  <head>
1467
1651
  <meta charset="utf-8" />
1468
1652
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1469
1653
  <title>Index of ${escapeHtml(localPath)}</title>
1470
1654
  <style>
1471
- body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
1655
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
1472
1656
  a { color: #8cc2ff; text-decoration: none; }
1473
1657
  a:hover { text-decoration: underline; }
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; }
1658
+ ul { list-style: none; padding: 0; margin: 12px 0 0; }
1659
+ li { padding: 3px 0; }
1479
1660
  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
- }
1486
1661
  </style>
1487
1662
  </head>
1488
1663
  <body>
@@ -1491,107 +1666,7 @@ async function createDirectoryListingHtml(localPath) {
1491
1666
  <ul>${rows}</ul>
1492
1667
  </body>
1493
1668
  </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 "";
1669
+ res.status(200).type("text/html; charset=utf-8").send(html);
1595
1670
  }
1596
1671
  function createServer(options = {}) {
1597
1672
  const app = express();
@@ -1608,7 +1683,7 @@ function createServer(options = {}) {
1608
1683
  res.status(400).json({ error: "Expected absolute local file path." });
1609
1684
  return;
1610
1685
  }
1611
- const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
1686
+ const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
1612
1687
  if (!contentType) {
1613
1688
  res.status(415).json({ error: "Unsupported image type." });
1614
1689
  return;
@@ -1635,18 +1710,17 @@ function createServer(options = {}) {
1635
1710
  });
1636
1711
  });
1637
1712
  app.get("/codex-local-browse/*path", async (req, res) => {
1638
- const rawPath = readWildcardPathParam(req.params.path);
1713
+ const rawPath = typeof req.params.path === "string" ? req.params.path : "";
1639
1714
  const localPath = decodeBrowsePath(`/${rawPath}`);
1640
1715
  if (!localPath || !isAbsolute2(localPath)) {
1641
1716
  res.status(400).json({ error: "Expected absolute local file path." });
1642
1717
  return;
1643
1718
  }
1644
1719
  try {
1645
- const fileStat = await stat3(localPath);
1720
+ const fileStat = await stat2(localPath);
1646
1721
  res.setHeader("Cache-Control", "private, no-store");
1647
1722
  if (fileStat.isDirectory()) {
1648
- const html = await createDirectoryListingHtml(localPath);
1649
- res.status(200).type("text/html; charset=utf-8").send(html);
1723
+ await renderDirectoryListing(res, localPath);
1650
1724
  return;
1651
1725
  }
1652
1726
  res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
@@ -1657,44 +1731,6 @@ function createServer(options = {}) {
1657
1731
  res.status(404).json({ error: "File not found." });
1658
1732
  }
1659
1733
  });
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
- });
1698
1734
  const hasFrontendAssets = existsSync(spaEntryFile);
1699
1735
  if (hasFrontendAssets) {
1700
1736
  app.use(express.static(distDir));
@@ -1766,11 +1802,11 @@ function generatePassword() {
1766
1802
 
1767
1803
  // src/cli/index.ts
1768
1804
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
1769
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1805
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1770
1806
  async function readCliVersion() {
1771
1807
  try {
1772
- const packageJsonPath = join4(__dirname2, "..", "package.json");
1773
- const raw = await readFile3(packageJsonPath, "utf8");
1808
+ const packageJsonPath = join3(__dirname2, "..", "package.json");
1809
+ const raw = await readFile2(packageJsonPath, "utf8");
1774
1810
  const parsed = JSON.parse(raw);
1775
1811
  return typeof parsed.version === "string" ? parsed.version : "unknown";
1776
1812
  } catch {
@@ -1795,13 +1831,13 @@ function runWithStatus(command, args) {
1795
1831
  return result.status ?? -1;
1796
1832
  }
1797
1833
  function getUserNpmPrefix() {
1798
- return join4(homedir2(), ".npm-global");
1834
+ return join3(homedir2(), ".npm-global");
1799
1835
  }
1800
1836
  function resolveCodexCommand() {
1801
1837
  if (canRun("codex", ["--version"])) {
1802
1838
  return "codex";
1803
1839
  }
1804
- const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
1840
+ const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
1805
1841
  if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
1806
1842
  return userCandidate;
1807
1843
  }
@@ -1809,15 +1845,88 @@ function resolveCodexCommand() {
1809
1845
  if (!prefix) {
1810
1846
  return null;
1811
1847
  }
1812
- const candidate = join4(prefix, "bin", "codex");
1848
+ const candidate = join3(prefix, "bin", "codex");
1813
1849
  if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
1814
1850
  return candidate;
1815
1851
  }
1816
1852
  return null;
1817
1853
  }
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
+ }
1818
1927
  function hasCodexAuth() {
1819
- const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
1820
- return existsSync2(join4(codexHome, "auth.json"));
1928
+ const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1929
+ return existsSync2(join3(codexHome, "auth.json"));
1821
1930
  }
1822
1931
  function ensureCodexInstalled() {
1823
1932
  let codexCommand = resolveCodexCommand();
@@ -1835,7 +1944,7 @@ function ensureCodexInstalled() {
1835
1944
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
1836
1945
  `);
1837
1946
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
1838
- process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1947
+ process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1839
1948
  };
1840
1949
  if (isTermuxRuntime()) {
1841
1950
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -1896,9 +2005,27 @@ function parseCloudflaredUrl(chunk) {
1896
2005
  }
1897
2006
  return urlMatch[urlMatch.length - 1] ?? null;
1898
2007
  }
1899
- async function startCloudflaredTunnel(localPort) {
2008
+ function getAccessibleUrls(port) {
2009
+ const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
2010
+ const interfaces = networkInterfaces();
2011
+ for (const entries of Object.values(interfaces)) {
2012
+ if (!entries) {
2013
+ continue;
2014
+ }
2015
+ for (const entry of entries) {
2016
+ if (entry.internal) {
2017
+ continue;
2018
+ }
2019
+ if (entry.family === "IPv4") {
2020
+ urls.add(`http://${entry.address}:${String(port)}`);
2021
+ }
2022
+ }
2023
+ }
2024
+ return Array.from(urls);
2025
+ }
2026
+ async function startCloudflaredTunnel(command, localPort) {
1900
2027
  return new Promise((resolve2, reject) => {
1901
- const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2028
+ const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
1902
2029
  stdio: ["ignore", "pipe", "pipe"]
1903
2030
  });
1904
2031
  const timeout = setTimeout(() => {
@@ -1949,7 +2076,7 @@ function listenWithFallback(server, startPort) {
1949
2076
  };
1950
2077
  server.once("error", onError);
1951
2078
  server.once("listening", onListening);
1952
- server.listen(port);
2079
+ server.listen(port, "0.0.0.0");
1953
2080
  };
1954
2081
  attempt(startPort);
1955
2082
  });
@@ -1971,7 +2098,8 @@ async function startServer(options) {
1971
2098
  let tunnelUrl = null;
1972
2099
  if (options.tunnel) {
1973
2100
  try {
1974
- const tunnel = await startCloudflaredTunnel(port);
2101
+ const cloudflaredCommand = await ensureCloudflaredInstalledLinux() ?? "cloudflared";
2102
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
1975
2103
  tunnelChild = tunnel.process;
1976
2104
  tunnelUrl = tunnel.url;
1977
2105
  } catch (error) {
@@ -1986,8 +2114,15 @@ async function startServer(options) {
1986
2114
  ` Version: ${version}`,
1987
2115
  " GitHub: https://github.com/friuns2/codexui",
1988
2116
  "",
1989
- ` Local: http://localhost:${String(port)}`
2117
+ ` Bind: http://0.0.0.0:${String(port)}`
1990
2118
  ];
2119
+ const accessUrls = getAccessibleUrls(port);
2120
+ if (accessUrls.length > 0) {
2121
+ lines.push(` Local: ${accessUrls[0]}`);
2122
+ for (const accessUrl of accessUrls.slice(1)) {
2123
+ lines.push(` Network: ${accessUrl}`);
2124
+ }
2125
+ }
1991
2126
  if (port !== requestedPort) {
1992
2127
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
1993
2128
  }
@@ -1996,9 +2131,7 @@ async function startServer(options) {
1996
2131
  }
1997
2132
  if (tunnelUrl) {
1998
2133
  lines.push(` Tunnel: ${tunnelUrl}`);
1999
- lines.push("");
2000
- lines.push(" Tunnel QR code:");
2001
- lines.push(` URL: ${tunnelUrl}`);
2134
+ lines.push(" Tunnel QR code below");
2002
2135
  }
2003
2136
  printTermuxKeepAlive(lines);
2004
2137
  lines.push("");