codexapp 0.1.33 → 0.1.34

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
@@ -1,33 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import "dotenv/config";
5
4
  import { createServer as createServer2 } from "http";
6
- import { existsSync as existsSync3 } from "fs";
7
- import { readFile as readFile2 } from "fs/promises";
8
- import { homedir as homedir2 } from "os";
9
- import { join as join3 } from "path";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
6
+ import { readFile as readFile3 } from "fs/promises";
7
+ import { homedir as homedir2, networkInterfaces } from "os";
8
+ import { join as join4 } from "path";
10
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
+ import { createInterface } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
- import { dirname as dirname2 } from "path";
12
+ import { dirname as dirname3 } from "path";
13
+ import { get as httpsGet } from "https";
13
14
  import { Command } from "commander";
14
15
  import qrcode from "qrcode-terminal";
15
16
 
16
17
  // src/server/httpServer.ts
17
18
  import { fileURLToPath } from "url";
18
- import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
19
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
19
20
  import { existsSync as existsSync2 } from "fs";
21
+ import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
20
22
  import express from "express";
21
23
 
22
24
  // src/server/codexAppServerBridge.ts
23
- import "dotenv/config";
24
25
  import { spawn } from "child_process";
26
+ import { randomBytes } from "crypto";
25
27
  import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
26
28
  import { existsSync } from "fs";
27
29
  import { request as httpsRequest } from "https";
28
30
  import { homedir } from "os";
29
31
  import { tmpdir } from "os";
30
- import { isAbsolute, join, resolve } from "path";
32
+ import { basename, isAbsolute, join, resolve } from "path";
31
33
  import { writeFile } from "fs/promises";
32
34
  function asRecord(value) {
33
35
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -51,6 +53,46 @@ function setJson(res, statusCode, payload) {
51
53
  res.setHeader("Content-Type", "application/json; charset=utf-8");
52
54
  res.end(JSON.stringify(payload));
53
55
  }
56
+ function extractThreadMessageText(threadReadPayload) {
57
+ const payload = asRecord(threadReadPayload);
58
+ const thread = asRecord(payload?.thread);
59
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
60
+ const parts = [];
61
+ for (const turn of turns) {
62
+ const turnRecord = asRecord(turn);
63
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
64
+ for (const item of items) {
65
+ const itemRecord = asRecord(item);
66
+ const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
67
+ if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
68
+ parts.push(itemRecord.text.trim());
69
+ continue;
70
+ }
71
+ if (type === "userMessage") {
72
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
73
+ for (const block of content) {
74
+ const blockRecord = asRecord(block);
75
+ if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
76
+ parts.push(blockRecord.text.trim());
77
+ }
78
+ }
79
+ continue;
80
+ }
81
+ if (type === "commandExecution") {
82
+ const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
83
+ const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
84
+ if (command) parts.push(command);
85
+ if (output) parts.push(output);
86
+ }
87
+ }
88
+ }
89
+ return parts.join("\n").trim();
90
+ }
91
+ function isExactPhraseMatch(query, doc) {
92
+ const q = query.trim().toLowerCase();
93
+ if (!q) return false;
94
+ return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
95
+ }
54
96
  function scoreFileCandidate(path, query) {
55
97
  if (!query) return 0;
56
98
  const lowerPath = path.toLowerCase();
@@ -124,6 +166,55 @@ async function runCommand(command, args, options = {}) {
124
166
  });
125
167
  });
126
168
  }
169
+ function isMissingHeadError(error) {
170
+ const message = getErrorMessage(error, "").toLowerCase();
171
+ return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
172
+ }
173
+ function isNotGitRepositoryError(error) {
174
+ const message = getErrorMessage(error, "").toLowerCase();
175
+ return message.includes("not a git repository") || message.includes("fatal: not a git repository");
176
+ }
177
+ async function ensureRepoHasInitialCommit(repoRoot) {
178
+ const agentsPath = join(repoRoot, "AGENTS.md");
179
+ try {
180
+ await stat(agentsPath);
181
+ } catch {
182
+ await writeFile(agentsPath, "", "utf8");
183
+ }
184
+ await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
185
+ await runCommand(
186
+ "git",
187
+ ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
188
+ { cwd: repoRoot }
189
+ );
190
+ }
191
+ async function runCommandCapture(command, args, options = {}) {
192
+ return await new Promise((resolve2, reject) => {
193
+ const proc = spawn(command, args, {
194
+ cwd: options.cwd,
195
+ env: process.env,
196
+ stdio: ["ignore", "pipe", "pipe"]
197
+ });
198
+ let stdout = "";
199
+ let stderr = "";
200
+ proc.stdout.on("data", (chunk) => {
201
+ stdout += chunk.toString();
202
+ });
203
+ proc.stderr.on("data", (chunk) => {
204
+ stderr += chunk.toString();
205
+ });
206
+ proc.on("error", reject);
207
+ proc.on("close", (code) => {
208
+ if (code === 0) {
209
+ resolve2(stdout.trim());
210
+ return;
211
+ }
212
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
213
+ const suffix = details.length > 0 ? `: ${details}` : "";
214
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
215
+ });
216
+ });
217
+ }
127
218
  async function runCommandWithOutput(command, args, options = {}) {
128
219
  return await new Promise((resolve2, reject) => {
129
220
  const proc = spawn(command, args, {
@@ -142,7 +233,7 @@ async function runCommandWithOutput(command, args, options = {}) {
142
233
  proc.on("error", reject);
143
234
  proc.on("close", (code) => {
144
235
  if (code === 0) {
145
- resolve2(stdout);
236
+ resolve2(stdout.trim());
146
237
  return;
147
238
  }
148
239
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -721,11 +812,29 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
721
812
  const codexHomeDir = getCodexHomeDir();
722
813
  const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
723
814
  const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
815
+ await mkdir(join(codexHomeDir, "skills"), { recursive: true });
816
+ let copiedFromCodex = false;
724
817
  try {
725
- const skillsAgentsStat = await stat(skillsAgentsPath);
726
- if (!skillsAgentsStat.isFile()) return;
818
+ const codexAgentsStat = await lstat(codexAgentsPath);
819
+ if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
820
+ const content = await readFile(codexAgentsPath, "utf8");
821
+ await writeFile(skillsAgentsPath, content, "utf8");
822
+ copiedFromCodex = true;
823
+ } else {
824
+ await rm(codexAgentsPath, { force: true, recursive: true });
825
+ }
727
826
  } catch {
728
- return;
827
+ }
828
+ if (!copiedFromCodex) {
829
+ try {
830
+ const skillsAgentsStat = await stat(skillsAgentsPath);
831
+ if (!skillsAgentsStat.isFile()) {
832
+ await rm(skillsAgentsPath, { force: true, recursive: true });
833
+ await writeFile(skillsAgentsPath, "", "utf8");
834
+ }
835
+ } catch {
836
+ await writeFile(skillsAgentsPath, "", "utf8");
837
+ }
729
838
  }
730
839
  const relativeTarget = join("skills", "AGENTS.md");
731
840
  try {
@@ -1402,8 +1511,82 @@ function getSharedBridgeState() {
1402
1511
  globalScope[SHARED_BRIDGE_KEY] = created;
1403
1512
  return created;
1404
1513
  }
1514
+ async function loadAllThreadsForSearch(appServer) {
1515
+ const threads = [];
1516
+ let cursor = null;
1517
+ do {
1518
+ const response = asRecord(await appServer.rpc("thread/list", {
1519
+ archived: false,
1520
+ limit: 100,
1521
+ sortKey: "updated_at",
1522
+ cursor
1523
+ }));
1524
+ const data = Array.isArray(response?.data) ? response.data : [];
1525
+ for (const row of data) {
1526
+ const record = asRecord(row);
1527
+ const id = typeof record?.id === "string" ? record.id : "";
1528
+ if (!id) continue;
1529
+ 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";
1530
+ const preview = typeof record?.preview === "string" ? record.preview : "";
1531
+ threads.push({ id, title, preview });
1532
+ }
1533
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
1534
+ } while (cursor);
1535
+ const docs = [];
1536
+ const concurrency = 4;
1537
+ for (let offset = 0; offset < threads.length; offset += concurrency) {
1538
+ const batch = threads.slice(offset, offset + concurrency);
1539
+ const loaded = await Promise.all(batch.map(async (thread) => {
1540
+ try {
1541
+ const readResponse = await appServer.rpc("thread/read", {
1542
+ threadId: thread.id,
1543
+ includeTurns: true
1544
+ });
1545
+ const messageText = extractThreadMessageText(readResponse);
1546
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
1547
+ return {
1548
+ id: thread.id,
1549
+ title: thread.title,
1550
+ preview: thread.preview,
1551
+ messageText,
1552
+ searchableText
1553
+ };
1554
+ } catch {
1555
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
1556
+ return {
1557
+ id: thread.id,
1558
+ title: thread.title,
1559
+ preview: thread.preview,
1560
+ messageText: "",
1561
+ searchableText
1562
+ };
1563
+ }
1564
+ }));
1565
+ docs.push(...loaded);
1566
+ }
1567
+ return docs;
1568
+ }
1569
+ async function buildThreadSearchIndex(appServer) {
1570
+ const docs = await loadAllThreadsForSearch(appServer);
1571
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
1572
+ return { docsById };
1573
+ }
1405
1574
  function createCodexBridgeMiddleware() {
1406
1575
  const { appServer, methodCatalog } = getSharedBridgeState();
1576
+ let threadSearchIndex = null;
1577
+ let threadSearchIndexPromise = null;
1578
+ async function getThreadSearchIndex() {
1579
+ if (threadSearchIndex) return threadSearchIndex;
1580
+ if (!threadSearchIndexPromise) {
1581
+ threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
1582
+ threadSearchIndex = index;
1583
+ return index;
1584
+ }).finally(() => {
1585
+ threadSearchIndexPromise = null;
1586
+ });
1587
+ }
1588
+ return threadSearchIndexPromise;
1589
+ }
1407
1590
  void initializeSkillsSyncOnStartup(appServer);
1408
1591
  const middleware = async (req, res, next) => {
1409
1592
  try {
@@ -1470,6 +1653,76 @@ function createCodexBridgeMiddleware() {
1470
1653
  setJson(res, 200, { data: { path: homedir() } });
1471
1654
  return;
1472
1655
  }
1656
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
1657
+ const payload = asRecord(await readJsonBody(req));
1658
+ const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
1659
+ if (!rawSourceCwd) {
1660
+ setJson(res, 400, { error: "Missing sourceCwd" });
1661
+ return;
1662
+ }
1663
+ const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
1664
+ try {
1665
+ const sourceInfo = await stat(sourceCwd);
1666
+ if (!sourceInfo.isDirectory()) {
1667
+ setJson(res, 400, { error: "sourceCwd is not a directory" });
1668
+ return;
1669
+ }
1670
+ } catch {
1671
+ setJson(res, 404, { error: "sourceCwd does not exist" });
1672
+ return;
1673
+ }
1674
+ try {
1675
+ let gitRoot = "";
1676
+ try {
1677
+ gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1678
+ } catch (error) {
1679
+ if (!isNotGitRepositoryError(error)) throw error;
1680
+ await runCommand("git", ["init"], { cwd: sourceCwd });
1681
+ gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1682
+ }
1683
+ const repoName = basename(gitRoot) || "repo";
1684
+ const worktreesRoot = join(getCodexHomeDir(), "worktrees");
1685
+ await mkdir(worktreesRoot, { recursive: true });
1686
+ let worktreeId = "";
1687
+ let worktreeParent = "";
1688
+ let worktreeCwd = "";
1689
+ for (let attempt = 0; attempt < 12; attempt += 1) {
1690
+ const candidate = randomBytes(2).toString("hex");
1691
+ const parent = join(worktreesRoot, candidate);
1692
+ try {
1693
+ await stat(parent);
1694
+ continue;
1695
+ } catch {
1696
+ worktreeId = candidate;
1697
+ worktreeParent = parent;
1698
+ worktreeCwd = join(parent, repoName);
1699
+ break;
1700
+ }
1701
+ }
1702
+ if (!worktreeId || !worktreeParent || !worktreeCwd) {
1703
+ throw new Error("Failed to allocate a unique worktree id");
1704
+ }
1705
+ const branch = `codex/${worktreeId}`;
1706
+ await mkdir(worktreeParent, { recursive: true });
1707
+ try {
1708
+ await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1709
+ } catch (error) {
1710
+ if (!isMissingHeadError(error)) throw error;
1711
+ await ensureRepoHasInitialCommit(gitRoot);
1712
+ await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1713
+ }
1714
+ setJson(res, 200, {
1715
+ data: {
1716
+ cwd: worktreeCwd,
1717
+ branch,
1718
+ gitRoot
1719
+ }
1720
+ });
1721
+ } catch (error) {
1722
+ setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
1723
+ }
1724
+ return;
1725
+ }
1473
1726
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1474
1727
  const payload = await readJsonBody(req);
1475
1728
  const record = asRecord(payload);
@@ -1595,6 +1848,20 @@ function createCodexBridgeMiddleware() {
1595
1848
  setJson(res, 200, { data: cache });
1596
1849
  return;
1597
1850
  }
1851
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1852
+ const payload = asRecord(await readJsonBody(req));
1853
+ const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1854
+ const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1855
+ const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1856
+ if (!query) {
1857
+ setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1858
+ return;
1859
+ }
1860
+ const index = await getThreadSearchIndex();
1861
+ const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
1862
+ setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1863
+ return;
1864
+ }
1598
1865
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1599
1866
  const payload = asRecord(await readJsonBody(req));
1600
1867
  const id = typeof payload?.id === "string" ? payload.id : "";
@@ -1761,7 +2028,13 @@ function createCodexBridgeMiddleware() {
1761
2028
  try {
1762
2029
  const state = await readSkillsSyncState();
1763
2030
  if (!state.githubToken || !state.repoOwner || !state.repoName) {
1764
- setJson(res, 400, { error: "Skills sync is not configured yet" });
2031
+ const localDir2 = await detectUserSkillsDir(appServer);
2032
+ await bootstrapSkillsFromUpstreamIntoLocal(localDir2);
2033
+ try {
2034
+ await appServer.rpc("skills/list", { forceReload: true });
2035
+ } catch {
2036
+ }
2037
+ setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
1765
2038
  return;
1766
2039
  }
1767
2040
  const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
@@ -1945,6 +2218,7 @@ data: ${JSON.stringify({ ok: true })}
1945
2218
  }
1946
2219
  };
1947
2220
  middleware.dispose = () => {
2221
+ threadSearchIndex = null;
1948
2222
  appServer.dispose();
1949
2223
  };
1950
2224
  middleware.subscribeNotifications = (listener) => {
@@ -1959,7 +2233,7 @@ data: ${JSON.stringify({ ok: true })}
1959
2233
  }
1960
2234
 
1961
2235
  // src/server/authMiddleware.ts
1962
- import { randomBytes, timingSafeEqual } from "crypto";
2236
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1963
2237
  var TOKEN_COOKIE = "codex_web_local_token";
1964
2238
  function constantTimeCompare(a, b) {
1965
2239
  const bufA = Buffer.from(a);
@@ -2057,7 +2331,7 @@ function createAuthSession(password) {
2057
2331
  res.status(401).json({ error: "Invalid password" });
2058
2332
  return;
2059
2333
  }
2060
- const token = randomBytes(32).toString("hex");
2334
+ const token = randomBytes2(32).toString("hex");
2061
2335
  validTokens.add(token);
2062
2336
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
2063
2337
  res.json({ ok: true });
@@ -2076,11 +2350,277 @@ function createAuthSession(password) {
2076
2350
  };
2077
2351
  }
2078
2352
 
2353
+ // src/server/localBrowseUi.ts
2354
+ import { dirname, extname, join as join2 } from "path";
2355
+ import { open, readFile as readFile2, readdir as readdir2, stat as stat2 } from "fs/promises";
2356
+ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2357
+ ".txt",
2358
+ ".md",
2359
+ ".json",
2360
+ ".js",
2361
+ ".ts",
2362
+ ".tsx",
2363
+ ".jsx",
2364
+ ".css",
2365
+ ".scss",
2366
+ ".html",
2367
+ ".htm",
2368
+ ".xml",
2369
+ ".yml",
2370
+ ".yaml",
2371
+ ".log",
2372
+ ".csv",
2373
+ ".env",
2374
+ ".py",
2375
+ ".sh",
2376
+ ".toml",
2377
+ ".ini",
2378
+ ".conf",
2379
+ ".sql",
2380
+ ".bat",
2381
+ ".cmd",
2382
+ ".ps1"
2383
+ ]);
2384
+ function languageForPath(pathValue) {
2385
+ const extension = extname(pathValue).toLowerCase();
2386
+ switch (extension) {
2387
+ case ".js":
2388
+ return "javascript";
2389
+ case ".ts":
2390
+ return "typescript";
2391
+ case ".jsx":
2392
+ return "javascript";
2393
+ case ".tsx":
2394
+ return "typescript";
2395
+ case ".py":
2396
+ return "python";
2397
+ case ".sh":
2398
+ return "sh";
2399
+ case ".css":
2400
+ case ".scss":
2401
+ return "css";
2402
+ case ".html":
2403
+ case ".htm":
2404
+ return "html";
2405
+ case ".json":
2406
+ return "json";
2407
+ case ".md":
2408
+ return "markdown";
2409
+ case ".yaml":
2410
+ case ".yml":
2411
+ return "yaml";
2412
+ case ".xml":
2413
+ return "xml";
2414
+ case ".sql":
2415
+ return "sql";
2416
+ case ".toml":
2417
+ return "ini";
2418
+ case ".ini":
2419
+ case ".conf":
2420
+ return "ini";
2421
+ default:
2422
+ return "plaintext";
2423
+ }
2424
+ }
2425
+ function normalizeLocalPath(rawPath) {
2426
+ const trimmed = rawPath.trim();
2427
+ if (!trimmed) return "";
2428
+ if (trimmed.startsWith("file://")) {
2429
+ try {
2430
+ return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
2431
+ } catch {
2432
+ return trimmed.replace(/^file:\/\//u, "");
2433
+ }
2434
+ }
2435
+ return trimmed;
2436
+ }
2437
+ function decodeBrowsePath(rawPath) {
2438
+ if (!rawPath) return "";
2439
+ try {
2440
+ return decodeURIComponent(rawPath);
2441
+ } catch {
2442
+ return rawPath;
2443
+ }
2444
+ }
2445
+ function isTextEditablePath(pathValue) {
2446
+ return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
2447
+ }
2448
+ function looksLikeTextBuffer(buffer) {
2449
+ if (buffer.length === 0) return true;
2450
+ for (const byte of buffer) {
2451
+ if (byte === 0) return false;
2452
+ }
2453
+ const decoded = buffer.toString("utf8");
2454
+ const replacementCount = (decoded.match(/\uFFFD/gu) ?? []).length;
2455
+ return replacementCount / decoded.length < 0.05;
2456
+ }
2457
+ async function probeFileIsText(localPath) {
2458
+ const handle = await open(localPath, "r");
2459
+ try {
2460
+ const sample = Buffer.allocUnsafe(4096);
2461
+ const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
2462
+ return looksLikeTextBuffer(sample.subarray(0, bytesRead));
2463
+ } finally {
2464
+ await handle.close();
2465
+ }
2466
+ }
2467
+ async function isTextEditableFile(localPath) {
2468
+ if (isTextEditablePath(localPath)) return true;
2469
+ try {
2470
+ const fileStat = await stat2(localPath);
2471
+ if (!fileStat.isFile()) return false;
2472
+ return await probeFileIsText(localPath);
2473
+ } catch {
2474
+ return false;
2475
+ }
2476
+ }
2477
+ function escapeHtml(value) {
2478
+ return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
2479
+ }
2480
+ function toBrowseHref(pathValue) {
2481
+ return `/codex-local-browse${encodeURI(pathValue)}`;
2482
+ }
2483
+ function toEditHref(pathValue) {
2484
+ return `/codex-local-edit${encodeURI(pathValue)}`;
2485
+ }
2486
+ function escapeForInlineScriptString(value) {
2487
+ return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
2488
+ }
2489
+ async function getDirectoryItems(localPath) {
2490
+ const entries = await readdir2(localPath, { withFileTypes: true });
2491
+ const withMeta = await Promise.all(entries.map(async (entry) => {
2492
+ const entryPath = join2(localPath, entry.name);
2493
+ const entryStat = await stat2(entryPath);
2494
+ const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2495
+ return {
2496
+ name: entry.name,
2497
+ path: entryPath,
2498
+ isDirectory: entry.isDirectory(),
2499
+ editable,
2500
+ mtimeMs: entryStat.mtimeMs
2501
+ };
2502
+ }));
2503
+ return withMeta.sort((a, b) => {
2504
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
2505
+ if (a.isDirectory && !b.isDirectory) return -1;
2506
+ if (!a.isDirectory && b.isDirectory) return 1;
2507
+ return a.name.localeCompare(b.name);
2508
+ });
2509
+ }
2510
+ async function createDirectoryListingHtml(localPath) {
2511
+ const items = await getDirectoryItems(localPath);
2512
+ const parentPath = dirname(localPath);
2513
+ const rows = items.map((item) => {
2514
+ const suffix = item.isDirectory ? "/" : "";
2515
+ const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
2516
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
2517
+ }).join("\n");
2518
+ const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
2519
+ return `<!doctype html>
2520
+ <html lang="en">
2521
+ <head>
2522
+ <meta charset="utf-8" />
2523
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2524
+ <title>Index of ${escapeHtml(localPath)}</title>
2525
+ <style>
2526
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
2527
+ a { color: #8cc2ff; text-decoration: none; }
2528
+ a:hover { text-decoration: underline; }
2529
+ ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
2530
+ .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
2531
+ .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
2532
+ .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; }
2533
+ .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
2534
+ h1 { font-size: 18px; margin: 0; word-break: break-all; }
2535
+ @media (max-width: 640px) {
2536
+ body { margin: 12px; }
2537
+ .file-row { gap: 8px; }
2538
+ .file-link { font-size: 15px; padding: 12px; }
2539
+ .icon-btn { width: 44px; height: 44px; }
2540
+ }
2541
+ </style>
2542
+ </head>
2543
+ <body>
2544
+ <h1>Index of ${escapeHtml(localPath)}</h1>
2545
+ ${parentLink}
2546
+ <ul>${rows}</ul>
2547
+ </body>
2548
+ </html>`;
2549
+ }
2550
+ async function createTextEditorHtml(localPath) {
2551
+ const content = await readFile2(localPath, "utf8");
2552
+ const parentPath = dirname(localPath);
2553
+ const language = languageForPath(localPath);
2554
+ const safeContentLiteral = escapeForInlineScriptString(content);
2555
+ return `<!doctype html>
2556
+ <html lang="en">
2557
+ <head>
2558
+ <meta charset="utf-8" />
2559
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2560
+ <title>Edit ${escapeHtml(localPath)}</title>
2561
+ <style>
2562
+ html, body { width: 100%; height: 100%; margin: 0; }
2563
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
2564
+ .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; }
2565
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
2566
+ button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
2567
+ button:hover, a:hover { filter: brightness(1.08); }
2568
+ #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
2569
+ #status { margin-left: 8px; color: #8cc2ff; }
2570
+ .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
2571
+ .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
2572
+ .ace_marker-layer .ace_active-line { background: #10213c !important; }
2573
+ .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
2574
+ .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
2575
+ </style>
2576
+ </head>
2577
+ <body>
2578
+ <div class="toolbar">
2579
+ <div class="row">
2580
+ <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
2581
+ <button id="saveBtn" type="button">Save</button>
2582
+ <span id="status"></span>
2583
+ </div>
2584
+ <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
2585
+ </div>
2586
+ <div id="editor"></div>
2587
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
2588
+ <script>
2589
+ const saveBtn = document.getElementById('saveBtn');
2590
+ const status = document.getElementById('status');
2591
+ const editor = ace.edit('editor');
2592
+ editor.setTheme('ace/theme/tomorrow_night');
2593
+ editor.session.setMode('ace/mode/${escapeHtml(language)}');
2594
+ editor.setValue(${safeContentLiteral}, -1);
2595
+ editor.setOptions({
2596
+ fontSize: '13px',
2597
+ wrap: true,
2598
+ showPrintMargin: false,
2599
+ useSoftTabs: true,
2600
+ tabSize: 2,
2601
+ behavioursEnabled: true,
2602
+ });
2603
+ editor.resize();
2604
+
2605
+ saveBtn.addEventListener('click', async () => {
2606
+ status.textContent = 'Saving...';
2607
+ const response = await fetch(location.pathname, {
2608
+ method: 'PUT',
2609
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
2610
+ body: editor.getValue(),
2611
+ });
2612
+ status.textContent = response.ok ? 'Saved' : 'Save failed';
2613
+ });
2614
+ </script>
2615
+ </body>
2616
+ </html>`;
2617
+ }
2618
+
2079
2619
  // src/server/httpServer.ts
2080
2620
  import { WebSocketServer } from "ws";
2081
- var __dirname = dirname(fileURLToPath(import.meta.url));
2082
- var distDir = join2(__dirname, "..", "dist");
2083
- var spaEntryFile = join2(distDir, "index.html");
2621
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
2622
+ var distDir = join3(__dirname, "..", "dist");
2623
+ var spaEntryFile = join3(distDir, "index.html");
2084
2624
  var IMAGE_CONTENT_TYPES = {
2085
2625
  ".avif": "image/avif",
2086
2626
  ".bmp": "image/bmp",
@@ -2103,6 +2643,11 @@ function normalizeLocalImagePath(rawPath) {
2103
2643
  }
2104
2644
  return trimmed;
2105
2645
  }
2646
+ function readWildcardPathParam(value) {
2647
+ if (typeof value === "string") return value;
2648
+ if (Array.isArray(value)) return value.join("/");
2649
+ return "";
2650
+ }
2106
2651
  function createServer(options = {}) {
2107
2652
  const app = express();
2108
2653
  const bridge = createCodexBridgeMiddleware();
@@ -2118,7 +2663,7 @@ function createServer(options = {}) {
2118
2663
  res.status(400).json({ error: "Expected absolute local file path." });
2119
2664
  return;
2120
2665
  }
2121
- const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
2666
+ const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
2122
2667
  if (!contentType) {
2123
2668
  res.status(415).json({ error: "Unsupported image type." });
2124
2669
  return;
@@ -2130,6 +2675,81 @@ function createServer(options = {}) {
2130
2675
  if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
2131
2676
  });
2132
2677
  });
2678
+ app.get("/codex-local-file", (req, res) => {
2679
+ const rawPath = typeof req.query.path === "string" ? req.query.path : "";
2680
+ const localPath = normalizeLocalPath(rawPath);
2681
+ if (!localPath || !isAbsolute2(localPath)) {
2682
+ res.status(400).json({ error: "Expected absolute local file path." });
2683
+ return;
2684
+ }
2685
+ res.setHeader("Cache-Control", "private, no-store");
2686
+ res.setHeader("Content-Disposition", "inline");
2687
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
2688
+ if (!error) return;
2689
+ if (!res.headersSent) res.status(404).json({ error: "File not found." });
2690
+ });
2691
+ });
2692
+ app.get("/codex-local-browse/*path", async (req, res) => {
2693
+ const rawPath = readWildcardPathParam(req.params.path);
2694
+ const localPath = decodeBrowsePath(`/${rawPath}`);
2695
+ if (!localPath || !isAbsolute2(localPath)) {
2696
+ res.status(400).json({ error: "Expected absolute local file path." });
2697
+ return;
2698
+ }
2699
+ try {
2700
+ const fileStat = await stat3(localPath);
2701
+ res.setHeader("Cache-Control", "private, no-store");
2702
+ if (fileStat.isDirectory()) {
2703
+ const html = await createDirectoryListingHtml(localPath);
2704
+ res.status(200).type("text/html; charset=utf-8").send(html);
2705
+ return;
2706
+ }
2707
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
2708
+ if (!error) return;
2709
+ if (!res.headersSent) res.status(404).json({ error: "File not found." });
2710
+ });
2711
+ } catch {
2712
+ res.status(404).json({ error: "File not found." });
2713
+ }
2714
+ });
2715
+ app.get("/codex-local-edit/*path", async (req, res) => {
2716
+ const rawPath = readWildcardPathParam(req.params.path);
2717
+ const localPath = decodeBrowsePath(`/${rawPath}`);
2718
+ if (!localPath || !isAbsolute2(localPath)) {
2719
+ res.status(400).json({ error: "Expected absolute local file path." });
2720
+ return;
2721
+ }
2722
+ try {
2723
+ const fileStat = await stat3(localPath);
2724
+ if (!fileStat.isFile()) {
2725
+ res.status(400).json({ error: "Expected file path." });
2726
+ return;
2727
+ }
2728
+ const html = await createTextEditorHtml(localPath);
2729
+ res.status(200).type("text/html; charset=utf-8").send(html);
2730
+ } catch {
2731
+ res.status(404).json({ error: "File not found." });
2732
+ }
2733
+ });
2734
+ app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
2735
+ const rawPath = readWildcardPathParam(req.params.path);
2736
+ const localPath = decodeBrowsePath(`/${rawPath}`);
2737
+ if (!localPath || !isAbsolute2(localPath)) {
2738
+ res.status(400).json({ error: "Expected absolute local file path." });
2739
+ return;
2740
+ }
2741
+ if (!await isTextEditableFile(localPath)) {
2742
+ res.status(415).json({ error: "Only text-like files are editable." });
2743
+ return;
2744
+ }
2745
+ const body = typeof req.body === "string" ? req.body : "";
2746
+ try {
2747
+ await writeFile2(localPath, body, "utf8");
2748
+ res.status(200).json({ ok: true });
2749
+ } catch {
2750
+ res.status(404).json({ error: "File not found." });
2751
+ }
2752
+ });
2133
2753
  const hasFrontendAssets = existsSync2(spaEntryFile);
2134
2754
  if (hasFrontendAssets) {
2135
2755
  app.use(express.static(distDir));
@@ -2201,11 +2821,11 @@ function generatePassword() {
2201
2821
 
2202
2822
  // src/cli/index.ts
2203
2823
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2204
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2824
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2205
2825
  async function readCliVersion() {
2206
2826
  try {
2207
- const packageJsonPath = join3(__dirname2, "..", "package.json");
2208
- const raw = await readFile2(packageJsonPath, "utf8");
2827
+ const packageJsonPath = join4(__dirname2, "..", "package.json");
2828
+ const raw = await readFile3(packageJsonPath, "utf8");
2209
2829
  const parsed = JSON.parse(raw);
2210
2830
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2211
2831
  } catch {
@@ -2230,13 +2850,13 @@ function runWithStatus(command, args) {
2230
2850
  return result.status ?? -1;
2231
2851
  }
2232
2852
  function getUserNpmPrefix() {
2233
- return join3(homedir2(), ".npm-global");
2853
+ return join4(homedir2(), ".npm-global");
2234
2854
  }
2235
2855
  function resolveCodexCommand() {
2236
2856
  if (canRun("codex", ["--version"])) {
2237
2857
  return "codex";
2238
2858
  }
2239
- const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
2859
+ const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
2240
2860
  if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
2241
2861
  return userCandidate;
2242
2862
  }
@@ -2244,15 +2864,113 @@ function resolveCodexCommand() {
2244
2864
  if (!prefix) {
2245
2865
  return null;
2246
2866
  }
2247
- const candidate = join3(prefix, "bin", "codex");
2867
+ const candidate = join4(prefix, "bin", "codex");
2248
2868
  if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
2249
2869
  return candidate;
2250
2870
  }
2251
2871
  return null;
2252
2872
  }
2873
+ function resolveCloudflaredCommand() {
2874
+ if (canRun("cloudflared", ["--version"])) {
2875
+ return "cloudflared";
2876
+ }
2877
+ const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
2878
+ if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
2879
+ return localCandidate;
2880
+ }
2881
+ return null;
2882
+ }
2883
+ function mapCloudflaredLinuxArch(arch) {
2884
+ if (arch === "x64") {
2885
+ return "amd64";
2886
+ }
2887
+ if (arch === "arm64") {
2888
+ return "arm64";
2889
+ }
2890
+ return null;
2891
+ }
2892
+ function downloadFile(url, destination) {
2893
+ return new Promise((resolve2, reject) => {
2894
+ const request = (currentUrl) => {
2895
+ httpsGet(currentUrl, (response) => {
2896
+ const code = response.statusCode ?? 0;
2897
+ if (code >= 300 && code < 400 && response.headers.location) {
2898
+ response.resume();
2899
+ request(response.headers.location);
2900
+ return;
2901
+ }
2902
+ if (code !== 200) {
2903
+ response.resume();
2904
+ reject(new Error(`Download failed with HTTP status ${String(code)}`));
2905
+ return;
2906
+ }
2907
+ const file = createWriteStream(destination, { mode: 493 });
2908
+ response.pipe(file);
2909
+ file.on("finish", () => {
2910
+ file.close();
2911
+ resolve2();
2912
+ });
2913
+ file.on("error", reject);
2914
+ }).on("error", reject);
2915
+ };
2916
+ request(url);
2917
+ });
2918
+ }
2919
+ async function ensureCloudflaredInstalledLinux() {
2920
+ const current = resolveCloudflaredCommand();
2921
+ if (current) {
2922
+ return current;
2923
+ }
2924
+ if (process.platform !== "linux") {
2925
+ return null;
2926
+ }
2927
+ const mappedArch = mapCloudflaredLinuxArch(process.arch);
2928
+ if (!mappedArch) {
2929
+ throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2930
+ }
2931
+ const userBinDir = join4(homedir2(), ".local", "bin");
2932
+ mkdirSync(userBinDir, { recursive: true });
2933
+ const destination = join4(userBinDir, "cloudflared");
2934
+ const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2935
+ console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
2936
+ await downloadFile(downloadUrl, destination);
2937
+ chmodSync(destination, 493);
2938
+ process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
2939
+ const installed = resolveCloudflaredCommand();
2940
+ if (!installed) {
2941
+ throw new Error("cloudflared download completed but executable is still not available");
2942
+ }
2943
+ console.log("\ncloudflared installed.\n");
2944
+ return installed;
2945
+ }
2946
+ async function shouldInstallCloudflaredInteractively() {
2947
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2948
+ console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
2949
+ return false;
2950
+ }
2951
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
2952
+ try {
2953
+ const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
2954
+ const normalized = answer.trim().toLowerCase();
2955
+ return normalized === "y" || normalized === "yes";
2956
+ } finally {
2957
+ prompt.close();
2958
+ }
2959
+ }
2960
+ async function resolveCloudflaredForTunnel() {
2961
+ const current = resolveCloudflaredCommand();
2962
+ if (current) {
2963
+ return current;
2964
+ }
2965
+ const installApproved = await shouldInstallCloudflaredInteractively();
2966
+ if (!installApproved) {
2967
+ return null;
2968
+ }
2969
+ return ensureCloudflaredInstalledLinux();
2970
+ }
2253
2971
  function hasCodexAuth() {
2254
- const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
2255
- return existsSync3(join3(codexHome, "auth.json"));
2972
+ const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
2973
+ return existsSync3(join4(codexHome, "auth.json"));
2256
2974
  }
2257
2975
  function ensureCodexInstalled() {
2258
2976
  let codexCommand = resolveCodexCommand();
@@ -2270,7 +2988,7 @@ function ensureCodexInstalled() {
2270
2988
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
2271
2989
  `);
2272
2990
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
2273
- process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2991
+ process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2274
2992
  };
2275
2993
  if (isTermuxRuntime()) {
2276
2994
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2331,9 +3049,27 @@ function parseCloudflaredUrl(chunk) {
2331
3049
  }
2332
3050
  return urlMatch[urlMatch.length - 1] ?? null;
2333
3051
  }
2334
- async function startCloudflaredTunnel(localPort) {
3052
+ function getAccessibleUrls(port) {
3053
+ const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
3054
+ const interfaces = networkInterfaces();
3055
+ for (const entries of Object.values(interfaces)) {
3056
+ if (!entries) {
3057
+ continue;
3058
+ }
3059
+ for (const entry of entries) {
3060
+ if (entry.internal) {
3061
+ continue;
3062
+ }
3063
+ if (entry.family === "IPv4") {
3064
+ urls.add(`http://${entry.address}:${String(port)}`);
3065
+ }
3066
+ }
3067
+ }
3068
+ return Array.from(urls);
3069
+ }
3070
+ async function startCloudflaredTunnel(command, localPort) {
2335
3071
  return new Promise((resolve2, reject) => {
2336
- const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
3072
+ const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2337
3073
  stdio: ["ignore", "pipe", "pipe"]
2338
3074
  });
2339
3075
  const timeout = setTimeout(() => {
@@ -2384,7 +3120,7 @@ function listenWithFallback(server, startPort) {
2384
3120
  };
2385
3121
  server.once("error", onError);
2386
3122
  server.once("listening", onListening);
2387
- server.listen(port);
3123
+ server.listen(port, "0.0.0.0");
2388
3124
  };
2389
3125
  attempt(startPort);
2390
3126
  });
@@ -2406,7 +3142,11 @@ async function startServer(options) {
2406
3142
  let tunnelUrl = null;
2407
3143
  if (options.tunnel) {
2408
3144
  try {
2409
- const tunnel = await startCloudflaredTunnel(port);
3145
+ const cloudflaredCommand = await resolveCloudflaredForTunnel();
3146
+ if (!cloudflaredCommand) {
3147
+ throw new Error("cloudflared is not installed");
3148
+ }
3149
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
2410
3150
  tunnelChild = tunnel.process;
2411
3151
  tunnelUrl = tunnel.url;
2412
3152
  } catch (error) {
@@ -2421,8 +3161,15 @@ async function startServer(options) {
2421
3161
  ` Version: ${version}`,
2422
3162
  " GitHub: https://github.com/friuns2/codexui",
2423
3163
  "",
2424
- ` Local: http://localhost:${String(port)}`
3164
+ ` Bind: http://0.0.0.0:${String(port)}`
2425
3165
  ];
3166
+ const accessUrls = getAccessibleUrls(port);
3167
+ if (accessUrls.length > 0) {
3168
+ lines.push(` Local: ${accessUrls[0]}`);
3169
+ for (const accessUrl of accessUrls.slice(1)) {
3170
+ lines.push(` Network: ${accessUrl}`);
3171
+ }
3172
+ }
2426
3173
  if (port !== requestedPort) {
2427
3174
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
2428
3175
  }
@@ -2431,9 +3178,7 @@ async function startServer(options) {
2431
3178
  }
2432
3179
  if (tunnelUrl) {
2433
3180
  lines.push(` Tunnel: ${tunnelUrl}`);
2434
- lines.push("");
2435
- lines.push(" Tunnel QR code:");
2436
- lines.push(` URL: ${tunnelUrl}`);
3181
+ lines.push(" Tunnel QR code below");
2437
3182
  }
2438
3183
  printTermuxKeepAlive(lines);
2439
3184
  lines.push("");