codexapp 0.1.33 → 0.1.35

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,42 @@
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";
10
- import { spawn as spawn2, spawnSync } from "child_process";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
6
+ import { readFile as readFile4 } from "fs/promises";
7
+ import { homedir as homedir3, networkInterfaces } from "os";
8
+ import { join as join5 } from "path";
9
+ import { spawn as spawn3, 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 join4 } from "path";
19
20
  import { existsSync as existsSync2 } from "fs";
21
+ import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
20
22
  import express from "express";
21
23
 
22
24
  // src/server/codexAppServerBridge.ts
23
- import "dotenv/config";
25
+ import { spawn as spawn2 } from "child_process";
26
+ import { randomBytes } from "crypto";
27
+ import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
28
+ import { request as httpsRequest } from "https";
29
+ import { homedir as homedir2 } from "os";
30
+ import { tmpdir as tmpdir2 } from "os";
31
+ import { basename, isAbsolute, join as join2, resolve } from "path";
32
+ import { writeFile as writeFile2 } from "fs/promises";
33
+
34
+ // src/server/skillsRoutes.ts
24
35
  import { spawn } from "child_process";
25
36
  import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
26
37
  import { existsSync } from "fs";
27
- import { request as httpsRequest } from "https";
28
- import { homedir } from "os";
29
- import { tmpdir } from "os";
30
- import { isAbsolute, join, resolve } from "path";
38
+ import { homedir, tmpdir } from "os";
39
+ import { join } from "path";
31
40
  import { writeFile } from "fs/promises";
32
41
  function asRecord(value) {
33
42
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -51,45 +60,6 @@ function setJson(res, statusCode, payload) {
51
60
  res.setHeader("Content-Type", "application/json; charset=utf-8");
52
61
  res.end(JSON.stringify(payload));
53
62
  }
54
- function scoreFileCandidate(path, query) {
55
- if (!query) return 0;
56
- const lowerPath = path.toLowerCase();
57
- const lowerQuery = query.toLowerCase();
58
- const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
59
- if (baseName === lowerQuery) return 0;
60
- if (baseName.startsWith(lowerQuery)) return 1;
61
- if (baseName.includes(lowerQuery)) return 2;
62
- if (lowerPath.includes(`/${lowerQuery}`)) return 3;
63
- if (lowerPath.includes(lowerQuery)) return 4;
64
- return 10;
65
- }
66
- async function listFilesWithRipgrep(cwd) {
67
- return await new Promise((resolve2, reject) => {
68
- const proc = spawn("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
69
- cwd,
70
- env: process.env,
71
- stdio: ["ignore", "pipe", "pipe"]
72
- });
73
- let stdout = "";
74
- let stderr = "";
75
- proc.stdout.on("data", (chunk) => {
76
- stdout += chunk.toString();
77
- });
78
- proc.stderr.on("data", (chunk) => {
79
- stderr += chunk.toString();
80
- });
81
- proc.on("error", reject);
82
- proc.on("close", (code) => {
83
- if (code === 0) {
84
- const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
85
- resolve2(rows);
86
- return;
87
- }
88
- const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
89
- reject(new Error(details || "rg --files failed"));
90
- });
91
- });
92
- }
93
63
  function getCodexHomeDir() {
94
64
  const codexHome = process.env.CODEX_HOME?.trim();
95
65
  return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
@@ -142,7 +112,7 @@ async function runCommandWithOutput(command, args, options = {}) {
142
112
  proc.on("error", reject);
143
113
  proc.on("close", (code) => {
144
114
  if (code === 0) {
145
- resolve2(stdout);
115
+ resolve2(stdout.trim());
146
116
  return;
147
117
  }
148
118
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -234,7 +204,7 @@ async function fetchMetaBatch(entries) {
234
204
  const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
235
205
  if (toFetch.length === 0) return;
236
206
  const batch = toFetch.slice(0, 50);
237
- const results = await Promise.allSettled(
207
+ await Promise.allSettled(
238
208
  batch.map(async (e) => {
239
209
  const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
240
210
  const resp = await fetch(rawUrl);
@@ -247,7 +217,6 @@ async function fetchMetaBatch(entries) {
247
217
  });
248
218
  })
249
219
  );
250
- void results;
251
220
  }
252
221
  function buildHubEntry(e) {
253
222
  const cached = metaCache.get(`${e.owner}/${e.name}`);
@@ -376,8 +345,7 @@ function isAndroidLikeRuntime() {
376
345
  const prefix = process.env.PREFIX?.toLowerCase() ?? "";
377
346
  if (prefix.includes("/com.termux/")) return true;
378
347
  const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
379
- if (proot.length > 0) return true;
380
- return false;
348
+ return proot.length > 0;
381
349
  }
382
350
  function getPreferredSyncBranch() {
383
351
  return isAndroidLikeRuntime() ? "android" : "main";
@@ -641,23 +609,21 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
641
609
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
642
610
  const branch = getPreferredSyncBranch();
643
611
  const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
644
- const addPaths = ["."];
645
612
  void _installedMap;
646
613
  await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
647
614
  await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
648
- await runCommand("git", ["add", ...addPaths], { cwd: repoDir });
615
+ await runCommand("git", ["add", "."], { cwd: repoDir });
649
616
  const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
650
617
  if (!status) return;
651
618
  await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
652
619
  await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
653
620
  }
654
- async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName, _localSkillsDir) {
621
+ async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
655
622
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
656
623
  const branch = getPreferredSyncBranch();
657
624
  await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
658
625
  }
659
- async function bootstrapSkillsFromUpstreamIntoLocal(_localSkillsDir) {
660
- void _localSkillsDir;
626
+ async function bootstrapSkillsFromUpstreamIntoLocal() {
661
627
  const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
662
628
  const branch = getPreferredSyncBranch();
663
629
  await ensureSkillsWorkingTreeRepo(repoUrl, branch);
@@ -721,11 +687,29 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
721
687
  const codexHomeDir = getCodexHomeDir();
722
688
  const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
723
689
  const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
690
+ await mkdir(join(codexHomeDir, "skills"), { recursive: true });
691
+ let copiedFromCodex = false;
724
692
  try {
725
- const skillsAgentsStat = await stat(skillsAgentsPath);
726
- if (!skillsAgentsStat.isFile()) return;
693
+ const codexAgentsStat = await lstat(codexAgentsPath);
694
+ if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
695
+ const content = await readFile(codexAgentsPath, "utf8");
696
+ await writeFile(skillsAgentsPath, content, "utf8");
697
+ copiedFromCodex = true;
698
+ } else {
699
+ await rm(codexAgentsPath, { force: true, recursive: true });
700
+ }
727
701
  } catch {
728
- return;
702
+ }
703
+ if (!copiedFromCodex) {
704
+ try {
705
+ const skillsAgentsStat = await stat(skillsAgentsPath);
706
+ if (!skillsAgentsStat.isFile()) {
707
+ await rm(skillsAgentsPath, { force: true, recursive: true });
708
+ await writeFile(skillsAgentsPath, "", "utf8");
709
+ }
710
+ } catch {
711
+ await writeFile(skillsAgentsPath, "", "utf8");
712
+ }
729
713
  }
730
714
  const relativeTarget = join("skills", "AGENTS.md");
731
715
  try {
@@ -748,7 +732,6 @@ async function initializeSkillsSyncOnStartup(appServer) {
748
732
  startupSyncStatus.branch = getPreferredSyncBranch();
749
733
  try {
750
734
  const state = await readSkillsSyncState();
751
- const localSkillsDir = getSkillsInstallDir();
752
735
  if (!state.githubToken) {
753
736
  await ensureCodexAgentsSymlinkToSkillsAgents();
754
737
  if (!isAndroidLikeRuntime()) {
@@ -759,7 +742,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
759
742
  }
760
743
  startupSyncStatus.mode = "unauthenticated-bootstrap";
761
744
  startupSyncStatus.lastAction = "pull-upstream";
762
- await bootstrapSkillsFromUpstreamIntoLocal(localSkillsDir);
745
+ await bootstrapSkillsFromUpstreamIntoLocal();
763
746
  try {
764
747
  await appServer.rpc("skills/list", { forceReload: true });
765
748
  } catch {
@@ -773,10 +756,9 @@ async function initializeSkillsSyncOnStartup(appServer) {
773
756
  const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
774
757
  const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
775
758
  await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
776
- const nextState = { ...state, githubUsername: username, repoOwner: username, repoName };
777
- await writeSkillsSyncState(nextState);
759
+ await writeSkillsSyncState({ ...state, githubUsername: username, repoOwner: username, repoName });
778
760
  startupSyncStatus.lastAction = "pull-private-fork";
779
- await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName, localSkillsDir);
761
+ await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName);
780
762
  try {
781
763
  await appServer.rpc("skills/list", { forceReload: true });
782
764
  } catch {
@@ -796,15 +778,8 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
796
778
  const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
797
779
  await ensurePrivateForkFromUpstream(token, username, repoName);
798
780
  const current = await readSkillsSyncState();
799
- await writeSkillsSyncState({
800
- ...current,
801
- githubToken: token,
802
- githubUsername: username,
803
- repoOwner: username,
804
- repoName
805
- });
806
- const localDir = getSkillsInstallDir();
807
- await pullInstalledSkillsFolderFromRepo(token, username, repoName, localDir);
781
+ await writeSkillsSyncState({ ...current, githubToken: token, githubUsername: username, repoOwner: username, repoName });
782
+ await pullInstalledSkillsFolderFromRepo(token, username, repoName);
808
783
  try {
809
784
  await appServer.rpc("skills/list", { forceReload: true });
810
785
  } catch {
@@ -813,11 +788,10 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
813
788
  }
814
789
  async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
815
790
  const q = query.toLowerCase().trim();
816
- let filtered = q ? allEntries.filter((s) => {
791
+ const filtered = q ? allEntries.filter((s) => {
817
792
  if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
818
793
  const cached = metaCache.get(`${s.owner}/${s.name}`);
819
- if (cached?.displayName?.toLowerCase().includes(q)) return true;
820
- return false;
794
+ return Boolean(cached?.displayName?.toLowerCase().includes(q));
821
795
  }) : allEntries;
822
796
  const page = filtered.slice(0, Math.min(limit * 2, 200));
823
797
  await fetchMetaBatch(page);
@@ -837,204 +811,692 @@ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
837
811
  return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
838
812
  });
839
813
  }
840
- function normalizeStringArray(value) {
841
- if (!Array.isArray(value)) return [];
842
- const normalized = [];
843
- for (const item of value) {
844
- if (typeof item === "string" && item.length > 0 && !normalized.includes(item)) {
845
- normalized.push(item);
846
- }
847
- }
848
- return normalized;
849
- }
850
- function normalizeStringRecord(value) {
851
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
852
- const next = {};
853
- for (const [key, item] of Object.entries(value)) {
854
- if (typeof key === "string" && key.length > 0 && typeof item === "string") {
855
- next[key] = item;
814
+ async function handleSkillsRoutes(req, res, url, context) {
815
+ const { appServer, readJsonBody: readJsonBody2 } = context;
816
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
817
+ try {
818
+ const q = url.searchParams.get("q") || "";
819
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
820
+ const sort = url.searchParams.get("sort") || "date";
821
+ const allEntries = await fetchSkillsTree();
822
+ const installedMap = await scanInstalledSkillsFromDisk();
823
+ try {
824
+ const result = await appServer.rpc("skills/list", {});
825
+ for (const entry of result.data ?? []) {
826
+ for (const skill of entry.skills ?? []) {
827
+ if (skill.name) {
828
+ installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
829
+ }
830
+ }
831
+ }
832
+ } catch {
833
+ }
834
+ const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
835
+ await fetchMetaBatch(installedHubEntries);
836
+ const installed = [];
837
+ for (const [, info] of installedMap) {
838
+ const hubEntry = allEntries.find((e) => e.name === info.name);
839
+ const base = hubEntry ? buildHubEntry(hubEntry) : {
840
+ name: info.name,
841
+ owner: "local",
842
+ description: "",
843
+ displayName: "",
844
+ publishedAt: 0,
845
+ avatarUrl: "",
846
+ url: "",
847
+ installed: false
848
+ };
849
+ installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
850
+ }
851
+ const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
852
+ setJson(res, 200, { data: results, installed, total: allEntries.length });
853
+ } catch (error) {
854
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
856
855
  }
856
+ return true;
857
857
  }
858
- return next;
859
- }
860
- function getCodexAuthPath() {
861
- return join(getCodexHomeDir(), "auth.json");
862
- }
863
- async function readCodexAuth() {
864
- try {
865
- const raw = await readFile(getCodexAuthPath(), "utf8");
866
- const auth = JSON.parse(raw);
867
- const token = auth.tokens?.access_token;
868
- if (!token) return null;
869
- return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
870
- } catch {
871
- return null;
858
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
859
+ const state = await readSkillsSyncState();
860
+ setJson(res, 200, {
861
+ data: {
862
+ loggedIn: Boolean(state.githubToken),
863
+ githubUsername: state.githubUsername ?? "",
864
+ repoOwner: state.repoOwner ?? "",
865
+ repoName: state.repoName ?? "",
866
+ configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
867
+ startup: {
868
+ inProgress: startupSyncStatus.inProgress,
869
+ mode: startupSyncStatus.mode,
870
+ branch: startupSyncStatus.branch,
871
+ lastAction: startupSyncStatus.lastAction,
872
+ lastRunAtIso: startupSyncStatus.lastRunAtIso,
873
+ lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
874
+ lastError: startupSyncStatus.lastError
875
+ }
876
+ }
877
+ });
878
+ return true;
872
879
  }
873
- }
874
- function getCodexGlobalStatePath() {
875
- return join(getCodexHomeDir(), ".codex-global-state.json");
876
- }
877
- var MAX_THREAD_TITLES = 500;
878
- function normalizeThreadTitleCache(value) {
879
- const record = asRecord(value);
880
- if (!record) return { titles: {}, order: [] };
881
- const rawTitles = asRecord(record.titles);
882
- const titles = {};
883
- if (rawTitles) {
884
- for (const [k, v] of Object.entries(rawTitles)) {
885
- if (typeof v === "string" && v.length > 0) titles[k] = v;
880
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
881
+ try {
882
+ const started = await startGithubDeviceLogin();
883
+ setJson(res, 200, { data: started });
884
+ } catch (error) {
885
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
886
886
  }
887
+ return true;
887
888
  }
888
- const order = normalizeStringArray(record.order);
889
- return { titles, order };
890
- }
891
- function updateThreadTitleCache(cache, id, title) {
892
- const titles = { ...cache.titles, [id]: title };
893
- const order = [id, ...cache.order.filter((o) => o !== id)];
894
- while (order.length > MAX_THREAD_TITLES) {
895
- const removed = order.pop();
896
- if (removed) delete titles[removed];
897
- }
898
- return { titles, order };
899
- }
900
- function removeFromThreadTitleCache(cache, id) {
901
- const { [id]: _, ...titles } = cache.titles;
902
- return { titles, order: cache.order.filter((o) => o !== id) };
903
- }
904
- async function readThreadTitleCache() {
905
- const statePath = getCodexGlobalStatePath();
906
- try {
907
- const raw = await readFile(statePath, "utf8");
908
- const payload = asRecord(JSON.parse(raw)) ?? {};
909
- return normalizeThreadTitleCache(payload["thread-titles"]);
910
- } catch {
911
- return { titles: {}, order: [] };
912
- }
913
- }
914
- async function writeThreadTitleCache(cache) {
915
- const statePath = getCodexGlobalStatePath();
916
- let payload = {};
917
- try {
918
- const raw = await readFile(statePath, "utf8");
919
- payload = asRecord(JSON.parse(raw)) ?? {};
920
- } catch {
921
- payload = {};
922
- }
923
- payload["thread-titles"] = cache;
924
- await writeFile(statePath, JSON.stringify(payload), "utf8");
925
- }
926
- async function readWorkspaceRootsState() {
927
- const statePath = getCodexGlobalStatePath();
928
- let payload = {};
929
- try {
930
- const raw = await readFile(statePath, "utf8");
931
- const parsed = JSON.parse(raw);
932
- payload = asRecord(parsed) ?? {};
933
- } catch {
934
- payload = {};
935
- }
936
- return {
937
- order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
938
- labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
939
- active: normalizeStringArray(payload["active-workspace-roots"])
940
- };
941
- }
942
- async function writeWorkspaceRootsState(nextState) {
943
- const statePath = getCodexGlobalStatePath();
944
- let payload = {};
945
- try {
946
- const raw = await readFile(statePath, "utf8");
947
- payload = asRecord(JSON.parse(raw)) ?? {};
948
- } catch {
949
- payload = {};
889
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
890
+ try {
891
+ const payload = asRecord(await readJsonBody2(req));
892
+ const token = typeof payload?.token === "string" ? payload.token.trim() : "";
893
+ if (!token) {
894
+ setJson(res, 400, { error: "Missing GitHub token" });
895
+ return true;
896
+ }
897
+ const username = await resolveGithubUsername(token);
898
+ await finalizeGithubLoginAndSync(token, username, appServer);
899
+ setJson(res, 200, { ok: true, data: { githubUsername: username } });
900
+ } catch (error) {
901
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
902
+ }
903
+ return true;
950
904
  }
951
- payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
952
- payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
953
- payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
954
- await writeFile(statePath, JSON.stringify(payload), "utf8");
955
- }
956
- async function readJsonBody(req) {
957
- const raw = await readRawBody(req);
958
- if (raw.length === 0) return null;
959
- const text = raw.toString("utf8").trim();
960
- if (text.length === 0) return null;
961
- return JSON.parse(text);
962
- }
963
- async function readRawBody(req) {
964
- const chunks = [];
965
- for await (const chunk of req) {
966
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
905
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
906
+ try {
907
+ const state = await readSkillsSyncState();
908
+ await writeSkillsSyncState({
909
+ ...state,
910
+ githubToken: void 0,
911
+ githubUsername: void 0,
912
+ repoOwner: void 0,
913
+ repoName: void 0
914
+ });
915
+ setJson(res, 200, { ok: true });
916
+ } catch (error) {
917
+ setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
918
+ }
919
+ return true;
967
920
  }
968
- return Buffer.concat(chunks);
969
- }
970
- function bufferIndexOf(buf, needle, start = 0) {
971
- for (let i = start; i <= buf.length - needle.length; i++) {
972
- let match = true;
973
- for (let j = 0; j < needle.length; j++) {
974
- if (buf[i + j] !== needle[j]) {
975
- match = false;
976
- break;
921
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
922
+ try {
923
+ const payload = asRecord(await readJsonBody2(req));
924
+ const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
925
+ if (!deviceCode) {
926
+ setJson(res, 400, { error: "Missing deviceCode" });
927
+ return true;
977
928
  }
929
+ const result = await completeGithubDeviceLogin(deviceCode);
930
+ if (!result.token) {
931
+ setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
932
+ return true;
933
+ }
934
+ const token = result.token;
935
+ const username = await resolveGithubUsername(token);
936
+ await finalizeGithubLoginAndSync(token, username, appServer);
937
+ setJson(res, 200, { ok: true, data: { githubUsername: username } });
938
+ } catch (error) {
939
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
978
940
  }
979
- if (match) return i;
941
+ return true;
980
942
  }
981
- return -1;
982
- }
983
- function handleFileUpload(req, res) {
984
- const chunks = [];
985
- req.on("data", (chunk) => chunks.push(chunk));
986
- req.on("end", async () => {
943
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
987
944
  try {
988
- const body = Buffer.concat(chunks);
989
- const contentType = req.headers["content-type"] ?? "";
990
- const boundaryMatch = contentType.match(/boundary=(.+)/i);
991
- if (!boundaryMatch) {
992
- setJson(res, 400, { error: "Missing multipart boundary" });
993
- return;
945
+ const state = await readSkillsSyncState();
946
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
947
+ setJson(res, 400, { error: "Skills sync is not configured yet" });
948
+ return true;
994
949
  }
995
- const boundary = boundaryMatch[1];
996
- const boundaryBuf = Buffer.from(`--${boundary}`);
997
- const parts = [];
998
- let searchStart = 0;
999
- while (searchStart < body.length) {
1000
- const idx = body.indexOf(boundaryBuf, searchStart);
1001
- if (idx < 0) break;
1002
- if (searchStart > 0) parts.push(body.subarray(searchStart, idx));
1003
- searchStart = idx + boundaryBuf.length;
1004
- if (body[searchStart] === 13 && body[searchStart + 1] === 10) searchStart += 2;
950
+ if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
951
+ setJson(res, 400, { error: "Refusing to push to upstream repository" });
952
+ return true;
1005
953
  }
1006
- let fileName = "uploaded-file";
1007
- let fileData = null;
1008
- const headerSep = Buffer.from("\r\n\r\n");
1009
- for (const part of parts) {
1010
- const headerEnd = bufferIndexOf(part, headerSep);
1011
- if (headerEnd < 0) continue;
1012
- const headers = part.subarray(0, headerEnd).toString("utf8");
1013
- const fnMatch = headers.match(/filename="([^"]+)"/i);
1014
- if (!fnMatch) continue;
1015
- fileName = fnMatch[1].replace(/[/\\]/g, "_");
1016
- let end = part.length;
1017
- if (end >= 2 && part[end - 2] === 13 && part[end - 1] === 10) end -= 2;
1018
- fileData = part.subarray(headerEnd + 4, end);
1019
- break;
954
+ const local = await collectLocalSyncedSkills(appServer);
955
+ const installedMap = await scanInstalledSkillsFromDisk();
956
+ await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
957
+ await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
958
+ setJson(res, 200, { ok: true, data: { synced: local.length } });
959
+ } catch (error) {
960
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
961
+ }
962
+ return true;
963
+ }
964
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
965
+ try {
966
+ const state = await readSkillsSyncState();
967
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
968
+ await bootstrapSkillsFromUpstreamIntoLocal();
969
+ try {
970
+ await appServer.rpc("skills/list", { forceReload: true });
971
+ } catch {
972
+ }
973
+ setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
974
+ return true;
1020
975
  }
1021
- if (!fileData) {
1022
- setJson(res, 400, { error: "No file in request" });
1023
- return;
976
+ const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
977
+ const tree = await fetchSkillsTree();
978
+ const uniqueOwnerByName = /* @__PURE__ */ new Map();
979
+ const ambiguousNames = /* @__PURE__ */ new Set();
980
+ for (const entry of tree) {
981
+ if (ambiguousNames.has(entry.name)) continue;
982
+ const existingOwner = uniqueOwnerByName.get(entry.name);
983
+ if (!existingOwner) {
984
+ uniqueOwnerByName.set(entry.name, entry.owner);
985
+ continue;
986
+ }
987
+ if (existingOwner !== entry.owner) {
988
+ uniqueOwnerByName.delete(entry.name);
989
+ ambiguousNames.add(entry.name);
990
+ }
1024
991
  }
1025
- const uploadDir = join(tmpdir(), "codex-web-uploads");
1026
- await mkdir(uploadDir, { recursive: true });
1027
- const destDir = await mkdtemp(join(uploadDir, "f-"));
1028
- const destPath = join(destDir, fileName);
1029
- await writeFile(destPath, fileData);
1030
- setJson(res, 200, { path: destPath });
1031
- } catch (err) {
1032
- setJson(res, 500, { error: getErrorMessage(err, "Upload failed") });
1033
- }
1034
- });
1035
- req.on("error", (err) => {
1036
- setJson(res, 500, { error: getErrorMessage(err, "Upload stream error") });
1037
- });
992
+ const localDir = await detectUserSkillsDir(appServer);
993
+ await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
994
+ const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
995
+ const localSkills = await scanInstalledSkillsFromDisk();
996
+ for (const skill of remote) {
997
+ const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
998
+ if (!owner) continue;
999
+ if (!localSkills.has(skill.name)) {
1000
+ await runCommand("python3", [
1001
+ installerScript,
1002
+ "--repo",
1003
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1004
+ "--path",
1005
+ `skills/${owner}/${skill.name}`,
1006
+ "--dest",
1007
+ localDir,
1008
+ "--method",
1009
+ "git"
1010
+ ]);
1011
+ }
1012
+ const skillPath = join(localDir, skill.name);
1013
+ await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1014
+ }
1015
+ const remoteNames = new Set(remote.map((row) => row.name));
1016
+ for (const [name, localInfo] of localSkills.entries()) {
1017
+ if (!remoteNames.has(name)) {
1018
+ await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
1019
+ }
1020
+ }
1021
+ const nextOwners = {};
1022
+ for (const item of remote) {
1023
+ const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
1024
+ if (owner) nextOwners[item.name] = owner;
1025
+ }
1026
+ await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
1027
+ try {
1028
+ await appServer.rpc("skills/list", { forceReload: true });
1029
+ } catch {
1030
+ }
1031
+ setJson(res, 200, { ok: true, data: { synced: remote.length } });
1032
+ } catch (error) {
1033
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
1034
+ }
1035
+ return true;
1036
+ }
1037
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
1038
+ try {
1039
+ const owner = url.searchParams.get("owner") || "";
1040
+ const name = url.searchParams.get("name") || "";
1041
+ if (!owner || !name) {
1042
+ setJson(res, 400, { error: "Missing owner or name" });
1043
+ return true;
1044
+ }
1045
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1046
+ const resp = await fetch(rawUrl);
1047
+ if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1048
+ const content = await resp.text();
1049
+ setJson(res, 200, { content });
1050
+ } catch (error) {
1051
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
1052
+ }
1053
+ return true;
1054
+ }
1055
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
1056
+ try {
1057
+ const payload = asRecord(await readJsonBody2(req));
1058
+ const owner = typeof payload?.owner === "string" ? payload.owner : "";
1059
+ const name = typeof payload?.name === "string" ? payload.name : "";
1060
+ if (!owner || !name) {
1061
+ setJson(res, 400, { error: "Missing owner or name" });
1062
+ return true;
1063
+ }
1064
+ const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
1065
+ const installDest = await detectUserSkillsDir(appServer);
1066
+ await runCommand("python3", [
1067
+ installerScript,
1068
+ "--repo",
1069
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1070
+ "--path",
1071
+ `skills/${owner}/${name}`,
1072
+ "--dest",
1073
+ installDest,
1074
+ "--method",
1075
+ "git"
1076
+ ]);
1077
+ const skillDir = join(installDest, name);
1078
+ await ensureInstalledSkillIsValid(appServer, skillDir);
1079
+ const syncState = await readSkillsSyncState();
1080
+ const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1081
+ await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1082
+ await autoPushSyncedSkills(appServer);
1083
+ setJson(res, 200, { ok: true, path: skillDir });
1084
+ } catch (error) {
1085
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
1086
+ }
1087
+ return true;
1088
+ }
1089
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
1090
+ try {
1091
+ const payload = asRecord(await readJsonBody2(req));
1092
+ const name = typeof payload?.name === "string" ? payload.name : "";
1093
+ const path = typeof payload?.path === "string" ? payload.path : "";
1094
+ const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1095
+ if (!target) {
1096
+ setJson(res, 400, { error: "Missing name or path" });
1097
+ return true;
1098
+ }
1099
+ await rm(target, { recursive: true, force: true });
1100
+ if (name) {
1101
+ const syncState = await readSkillsSyncState();
1102
+ const nextOwners = { ...syncState.installedOwners ?? {} };
1103
+ delete nextOwners[name];
1104
+ await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1105
+ }
1106
+ await autoPushSyncedSkills(appServer);
1107
+ try {
1108
+ await appServer.rpc("skills/list", { forceReload: true });
1109
+ } catch {
1110
+ }
1111
+ setJson(res, 200, { ok: true, deletedPath: target });
1112
+ } catch (error) {
1113
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
1114
+ }
1115
+ return true;
1116
+ }
1117
+ return false;
1118
+ }
1119
+
1120
+ // src/server/codexAppServerBridge.ts
1121
+ function asRecord2(value) {
1122
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1123
+ }
1124
+ function getErrorMessage2(payload, fallback) {
1125
+ if (payload instanceof Error && payload.message.trim().length > 0) {
1126
+ return payload.message;
1127
+ }
1128
+ const record = asRecord2(payload);
1129
+ if (!record) return fallback;
1130
+ const error = record.error;
1131
+ if (typeof error === "string" && error.length > 0) return error;
1132
+ const nestedError = asRecord2(error);
1133
+ if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
1134
+ return nestedError.message;
1135
+ }
1136
+ return fallback;
1137
+ }
1138
+ function setJson2(res, statusCode, payload) {
1139
+ res.statusCode = statusCode;
1140
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1141
+ res.end(JSON.stringify(payload));
1142
+ }
1143
+ function extractThreadMessageText(threadReadPayload) {
1144
+ const payload = asRecord2(threadReadPayload);
1145
+ const thread = asRecord2(payload?.thread);
1146
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1147
+ const parts = [];
1148
+ for (const turn of turns) {
1149
+ const turnRecord = asRecord2(turn);
1150
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1151
+ for (const item of items) {
1152
+ const itemRecord = asRecord2(item);
1153
+ const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1154
+ if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
1155
+ parts.push(itemRecord.text.trim());
1156
+ continue;
1157
+ }
1158
+ if (type === "userMessage") {
1159
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1160
+ for (const block of content) {
1161
+ const blockRecord = asRecord2(block);
1162
+ if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
1163
+ parts.push(blockRecord.text.trim());
1164
+ }
1165
+ }
1166
+ continue;
1167
+ }
1168
+ if (type === "commandExecution") {
1169
+ const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
1170
+ const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
1171
+ if (command) parts.push(command);
1172
+ if (output) parts.push(output);
1173
+ }
1174
+ }
1175
+ }
1176
+ return parts.join("\n").trim();
1177
+ }
1178
+ function isExactPhraseMatch(query, doc) {
1179
+ const q = query.trim().toLowerCase();
1180
+ if (!q) return false;
1181
+ return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
1182
+ }
1183
+ function scoreFileCandidate(path, query) {
1184
+ if (!query) return 0;
1185
+ const lowerPath = path.toLowerCase();
1186
+ const lowerQuery = query.toLowerCase();
1187
+ const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
1188
+ if (baseName === lowerQuery) return 0;
1189
+ if (baseName.startsWith(lowerQuery)) return 1;
1190
+ if (baseName.includes(lowerQuery)) return 2;
1191
+ if (lowerPath.includes(`/${lowerQuery}`)) return 3;
1192
+ if (lowerPath.includes(lowerQuery)) return 4;
1193
+ return 10;
1194
+ }
1195
+ async function listFilesWithRipgrep(cwd) {
1196
+ return await new Promise((resolve2, reject) => {
1197
+ const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1198
+ cwd,
1199
+ env: process.env,
1200
+ stdio: ["ignore", "pipe", "pipe"]
1201
+ });
1202
+ let stdout = "";
1203
+ let stderr = "";
1204
+ proc.stdout.on("data", (chunk) => {
1205
+ stdout += chunk.toString();
1206
+ });
1207
+ proc.stderr.on("data", (chunk) => {
1208
+ stderr += chunk.toString();
1209
+ });
1210
+ proc.on("error", reject);
1211
+ proc.on("close", (code) => {
1212
+ if (code === 0) {
1213
+ const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1214
+ resolve2(rows);
1215
+ return;
1216
+ }
1217
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1218
+ reject(new Error(details || "rg --files failed"));
1219
+ });
1220
+ });
1221
+ }
1222
+ function getCodexHomeDir2() {
1223
+ const codexHome = process.env.CODEX_HOME?.trim();
1224
+ return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1225
+ }
1226
+ async function runCommand2(command, args, options = {}) {
1227
+ await new Promise((resolve2, reject) => {
1228
+ const proc = spawn2(command, args, {
1229
+ cwd: options.cwd,
1230
+ env: process.env,
1231
+ stdio: ["ignore", "pipe", "pipe"]
1232
+ });
1233
+ let stdout = "";
1234
+ let stderr = "";
1235
+ proc.stdout.on("data", (chunk) => {
1236
+ stdout += chunk.toString();
1237
+ });
1238
+ proc.stderr.on("data", (chunk) => {
1239
+ stderr += chunk.toString();
1240
+ });
1241
+ proc.on("error", reject);
1242
+ proc.on("close", (code) => {
1243
+ if (code === 0) {
1244
+ resolve2();
1245
+ return;
1246
+ }
1247
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1248
+ const suffix = details.length > 0 ? `: ${details}` : "";
1249
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
1250
+ });
1251
+ });
1252
+ }
1253
+ function isMissingHeadError(error) {
1254
+ const message = getErrorMessage2(error, "").toLowerCase();
1255
+ return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
1256
+ }
1257
+ function isNotGitRepositoryError(error) {
1258
+ const message = getErrorMessage2(error, "").toLowerCase();
1259
+ return message.includes("not a git repository") || message.includes("fatal: not a git repository");
1260
+ }
1261
+ async function ensureRepoHasInitialCommit(repoRoot) {
1262
+ const agentsPath = join2(repoRoot, "AGENTS.md");
1263
+ try {
1264
+ await stat2(agentsPath);
1265
+ } catch {
1266
+ await writeFile2(agentsPath, "", "utf8");
1267
+ }
1268
+ await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
1269
+ await runCommand2(
1270
+ "git",
1271
+ ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
1272
+ { cwd: repoRoot }
1273
+ );
1274
+ }
1275
+ async function runCommandCapture(command, args, options = {}) {
1276
+ return await new Promise((resolve2, reject) => {
1277
+ const proc = spawn2(command, args, {
1278
+ cwd: options.cwd,
1279
+ env: process.env,
1280
+ stdio: ["ignore", "pipe", "pipe"]
1281
+ });
1282
+ let stdout = "";
1283
+ let stderr = "";
1284
+ proc.stdout.on("data", (chunk) => {
1285
+ stdout += chunk.toString();
1286
+ });
1287
+ proc.stderr.on("data", (chunk) => {
1288
+ stderr += chunk.toString();
1289
+ });
1290
+ proc.on("error", reject);
1291
+ proc.on("close", (code) => {
1292
+ if (code === 0) {
1293
+ resolve2(stdout.trim());
1294
+ return;
1295
+ }
1296
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1297
+ const suffix = details.length > 0 ? `: ${details}` : "";
1298
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
1299
+ });
1300
+ });
1301
+ }
1302
+ function normalizeStringArray(value) {
1303
+ if (!Array.isArray(value)) return [];
1304
+ const normalized = [];
1305
+ for (const item of value) {
1306
+ if (typeof item === "string" && item.length > 0 && !normalized.includes(item)) {
1307
+ normalized.push(item);
1308
+ }
1309
+ }
1310
+ return normalized;
1311
+ }
1312
+ function normalizeStringRecord(value) {
1313
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1314
+ const next = {};
1315
+ for (const [key, item] of Object.entries(value)) {
1316
+ if (typeof key === "string" && key.length > 0 && typeof item === "string") {
1317
+ next[key] = item;
1318
+ }
1319
+ }
1320
+ return next;
1321
+ }
1322
+ function getCodexAuthPath() {
1323
+ return join2(getCodexHomeDir2(), "auth.json");
1324
+ }
1325
+ async function readCodexAuth() {
1326
+ try {
1327
+ const raw = await readFile2(getCodexAuthPath(), "utf8");
1328
+ const auth = JSON.parse(raw);
1329
+ const token = auth.tokens?.access_token;
1330
+ if (!token) return null;
1331
+ return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
1332
+ } catch {
1333
+ return null;
1334
+ }
1335
+ }
1336
+ function getCodexGlobalStatePath() {
1337
+ return join2(getCodexHomeDir2(), ".codex-global-state.json");
1338
+ }
1339
+ var MAX_THREAD_TITLES = 500;
1340
+ function normalizeThreadTitleCache(value) {
1341
+ const record = asRecord2(value);
1342
+ if (!record) return { titles: {}, order: [] };
1343
+ const rawTitles = asRecord2(record.titles);
1344
+ const titles = {};
1345
+ if (rawTitles) {
1346
+ for (const [k, v] of Object.entries(rawTitles)) {
1347
+ if (typeof v === "string" && v.length > 0) titles[k] = v;
1348
+ }
1349
+ }
1350
+ const order = normalizeStringArray(record.order);
1351
+ return { titles, order };
1352
+ }
1353
+ function updateThreadTitleCache(cache, id, title) {
1354
+ const titles = { ...cache.titles, [id]: title };
1355
+ const order = [id, ...cache.order.filter((o) => o !== id)];
1356
+ while (order.length > MAX_THREAD_TITLES) {
1357
+ const removed = order.pop();
1358
+ if (removed) delete titles[removed];
1359
+ }
1360
+ return { titles, order };
1361
+ }
1362
+ function removeFromThreadTitleCache(cache, id) {
1363
+ const { [id]: _, ...titles } = cache.titles;
1364
+ return { titles, order: cache.order.filter((o) => o !== id) };
1365
+ }
1366
+ async function readThreadTitleCache() {
1367
+ const statePath = getCodexGlobalStatePath();
1368
+ try {
1369
+ const raw = await readFile2(statePath, "utf8");
1370
+ const payload = asRecord2(JSON.parse(raw)) ?? {};
1371
+ return normalizeThreadTitleCache(payload["thread-titles"]);
1372
+ } catch {
1373
+ return { titles: {}, order: [] };
1374
+ }
1375
+ }
1376
+ async function writeThreadTitleCache(cache) {
1377
+ const statePath = getCodexGlobalStatePath();
1378
+ let payload = {};
1379
+ try {
1380
+ const raw = await readFile2(statePath, "utf8");
1381
+ payload = asRecord2(JSON.parse(raw)) ?? {};
1382
+ } catch {
1383
+ payload = {};
1384
+ }
1385
+ payload["thread-titles"] = cache;
1386
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
1387
+ }
1388
+ async function readWorkspaceRootsState() {
1389
+ const statePath = getCodexGlobalStatePath();
1390
+ let payload = {};
1391
+ try {
1392
+ const raw = await readFile2(statePath, "utf8");
1393
+ const parsed = JSON.parse(raw);
1394
+ payload = asRecord2(parsed) ?? {};
1395
+ } catch {
1396
+ payload = {};
1397
+ }
1398
+ return {
1399
+ order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
1400
+ labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
1401
+ active: normalizeStringArray(payload["active-workspace-roots"])
1402
+ };
1403
+ }
1404
+ async function writeWorkspaceRootsState(nextState) {
1405
+ const statePath = getCodexGlobalStatePath();
1406
+ let payload = {};
1407
+ try {
1408
+ const raw = await readFile2(statePath, "utf8");
1409
+ payload = asRecord2(JSON.parse(raw)) ?? {};
1410
+ } catch {
1411
+ payload = {};
1412
+ }
1413
+ payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
1414
+ payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
1415
+ payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
1416
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
1417
+ }
1418
+ async function readJsonBody(req) {
1419
+ const raw = await readRawBody(req);
1420
+ if (raw.length === 0) return null;
1421
+ const text = raw.toString("utf8").trim();
1422
+ if (text.length === 0) return null;
1423
+ return JSON.parse(text);
1424
+ }
1425
+ async function readRawBody(req) {
1426
+ const chunks = [];
1427
+ for await (const chunk of req) {
1428
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1429
+ }
1430
+ return Buffer.concat(chunks);
1431
+ }
1432
+ function bufferIndexOf(buf, needle, start = 0) {
1433
+ for (let i = start; i <= buf.length - needle.length; i++) {
1434
+ let match = true;
1435
+ for (let j = 0; j < needle.length; j++) {
1436
+ if (buf[i + j] !== needle[j]) {
1437
+ match = false;
1438
+ break;
1439
+ }
1440
+ }
1441
+ if (match) return i;
1442
+ }
1443
+ return -1;
1444
+ }
1445
+ function handleFileUpload(req, res) {
1446
+ const chunks = [];
1447
+ req.on("data", (chunk) => chunks.push(chunk));
1448
+ req.on("end", async () => {
1449
+ try {
1450
+ const body = Buffer.concat(chunks);
1451
+ const contentType = req.headers["content-type"] ?? "";
1452
+ const boundaryMatch = contentType.match(/boundary=(.+)/i);
1453
+ if (!boundaryMatch) {
1454
+ setJson2(res, 400, { error: "Missing multipart boundary" });
1455
+ return;
1456
+ }
1457
+ const boundary = boundaryMatch[1];
1458
+ const boundaryBuf = Buffer.from(`--${boundary}`);
1459
+ const parts = [];
1460
+ let searchStart = 0;
1461
+ while (searchStart < body.length) {
1462
+ const idx = body.indexOf(boundaryBuf, searchStart);
1463
+ if (idx < 0) break;
1464
+ if (searchStart > 0) parts.push(body.subarray(searchStart, idx));
1465
+ searchStart = idx + boundaryBuf.length;
1466
+ if (body[searchStart] === 13 && body[searchStart + 1] === 10) searchStart += 2;
1467
+ }
1468
+ let fileName = "uploaded-file";
1469
+ let fileData = null;
1470
+ const headerSep = Buffer.from("\r\n\r\n");
1471
+ for (const part of parts) {
1472
+ const headerEnd = bufferIndexOf(part, headerSep);
1473
+ if (headerEnd < 0) continue;
1474
+ const headers = part.subarray(0, headerEnd).toString("utf8");
1475
+ const fnMatch = headers.match(/filename="([^"]+)"/i);
1476
+ if (!fnMatch) continue;
1477
+ fileName = fnMatch[1].replace(/[/\\]/g, "_");
1478
+ let end = part.length;
1479
+ if (end >= 2 && part[end - 2] === 13 && part[end - 1] === 10) end -= 2;
1480
+ fileData = part.subarray(headerEnd + 4, end);
1481
+ break;
1482
+ }
1483
+ if (!fileData) {
1484
+ setJson2(res, 400, { error: "No file in request" });
1485
+ return;
1486
+ }
1487
+ const uploadDir = join2(tmpdir2(), "codex-web-uploads");
1488
+ await mkdir2(uploadDir, { recursive: true });
1489
+ const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1490
+ const destPath = join2(destDir, fileName);
1491
+ await writeFile2(destPath, fileData);
1492
+ setJson2(res, 200, { path: destPath });
1493
+ } catch (err) {
1494
+ setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
1495
+ }
1496
+ });
1497
+ req.on("error", (err) => {
1498
+ setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
1499
+ });
1038
1500
  }
1039
1501
  async function proxyTranscribe(body, contentType, authToken, accountId) {
1040
1502
  const headers = {
@@ -1085,7 +1547,7 @@ var AppServerProcess = class {
1085
1547
  start() {
1086
1548
  if (this.process) return;
1087
1549
  this.stopping = false;
1088
- const proc = spawn("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1550
+ const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1089
1551
  this.process = proc;
1090
1552
  proc.stdout.setEncoding("utf8");
1091
1553
  proc.stdout.on("data", (chunk) => {
@@ -1179,7 +1641,7 @@ var AppServerProcess = class {
1179
1641
  }
1180
1642
  this.pendingServerRequests.delete(requestId);
1181
1643
  this.sendServerRequestReply(requestId, reply);
1182
- const requestParams = asRecord(pendingRequest.params);
1644
+ const requestParams = asRecord2(pendingRequest.params);
1183
1645
  const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
1184
1646
  this.emitNotification({
1185
1647
  method: "server/request/resolved",
@@ -1248,7 +1710,7 @@ var AppServerProcess = class {
1248
1710
  }
1249
1711
  async respondToServerRequest(payload) {
1250
1712
  await this.ensureInitialized();
1251
- const body = asRecord(payload);
1713
+ const body = asRecord2(payload);
1252
1714
  if (!body) {
1253
1715
  throw new Error("Invalid response payload: expected object");
1254
1716
  }
@@ -1256,7 +1718,7 @@ var AppServerProcess = class {
1256
1718
  if (typeof id !== "number" || !Number.isInteger(id)) {
1257
1719
  throw new Error('Invalid response payload: "id" must be an integer');
1258
1720
  }
1259
- const rawError = asRecord(body.error);
1721
+ const rawError = asRecord2(body.error);
1260
1722
  if (rawError) {
1261
1723
  const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
1262
1724
  const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
@@ -1311,7 +1773,7 @@ var MethodCatalog = class {
1311
1773
  }
1312
1774
  async runGenerateSchemaCommand(outDir) {
1313
1775
  await new Promise((resolve2, reject) => {
1314
- const process2 = spawn("codex", ["app-server", "generate-json-schema", "--out", outDir], {
1776
+ const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
1315
1777
  stdio: ["ignore", "ignore", "pipe"]
1316
1778
  });
1317
1779
  let stderr = "";
@@ -1330,13 +1792,13 @@ var MethodCatalog = class {
1330
1792
  });
1331
1793
  }
1332
1794
  extractMethodsFromClientRequest(payload) {
1333
- const root = asRecord(payload);
1795
+ const root = asRecord2(payload);
1334
1796
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1335
1797
  const methods = /* @__PURE__ */ new Set();
1336
1798
  for (const entry of oneOf) {
1337
- const row = asRecord(entry);
1338
- const properties = asRecord(row?.properties);
1339
- const methodDef = asRecord(properties?.method);
1799
+ const row = asRecord2(entry);
1800
+ const properties = asRecord2(row?.properties);
1801
+ const methodDef = asRecord2(properties?.method);
1340
1802
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1341
1803
  for (const item of methodEnum) {
1342
1804
  if (typeof item === "string" && item.length > 0) {
@@ -1347,13 +1809,13 @@ var MethodCatalog = class {
1347
1809
  return Array.from(methods).sort((a, b) => a.localeCompare(b));
1348
1810
  }
1349
1811
  extractMethodsFromServerNotification(payload) {
1350
- const root = asRecord(payload);
1812
+ const root = asRecord2(payload);
1351
1813
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1352
1814
  const methods = /* @__PURE__ */ new Set();
1353
1815
  for (const entry of oneOf) {
1354
- const row = asRecord(entry);
1355
- const properties = asRecord(row?.properties);
1356
- const methodDef = asRecord(properties?.method);
1816
+ const row = asRecord2(entry);
1817
+ const properties = asRecord2(row?.properties);
1818
+ const methodDef = asRecord2(properties?.method);
1357
1819
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1358
1820
  for (const item of methodEnum) {
1359
1821
  if (typeof item === "string" && item.length > 0) {
@@ -1367,10 +1829,10 @@ var MethodCatalog = class {
1367
1829
  if (this.methodCache) {
1368
1830
  return this.methodCache;
1369
1831
  }
1370
- const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
1832
+ const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
1371
1833
  await this.runGenerateSchemaCommand(outDir);
1372
- const clientRequestPath = join(outDir, "ClientRequest.json");
1373
- const raw = await readFile(clientRequestPath, "utf8");
1834
+ const clientRequestPath = join2(outDir, "ClientRequest.json");
1835
+ const raw = await readFile2(clientRequestPath, "utf8");
1374
1836
  const parsed = JSON.parse(raw);
1375
1837
  const methods = this.extractMethodsFromClientRequest(parsed);
1376
1838
  this.methodCache = methods;
@@ -1380,10 +1842,10 @@ var MethodCatalog = class {
1380
1842
  if (this.notificationCache) {
1381
1843
  return this.notificationCache;
1382
1844
  }
1383
- const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
1845
+ const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
1384
1846
  await this.runGenerateSchemaCommand(outDir);
1385
- const serverNotificationPath = join(outDir, "ServerNotification.json");
1386
- const raw = await readFile(serverNotificationPath, "utf8");
1847
+ const serverNotificationPath = join2(outDir, "ServerNotification.json");
1848
+ const raw = await readFile2(serverNotificationPath, "utf8");
1387
1849
  const parsed = JSON.parse(raw);
1388
1850
  const methods = this.extractMethodsFromServerNotification(parsed);
1389
1851
  this.notificationCache = methods;
@@ -1402,8 +1864,82 @@ function getSharedBridgeState() {
1402
1864
  globalScope[SHARED_BRIDGE_KEY] = created;
1403
1865
  return created;
1404
1866
  }
1867
+ async function loadAllThreadsForSearch(appServer) {
1868
+ const threads = [];
1869
+ let cursor = null;
1870
+ do {
1871
+ const response = asRecord2(await appServer.rpc("thread/list", {
1872
+ archived: false,
1873
+ limit: 100,
1874
+ sortKey: "updated_at",
1875
+ cursor
1876
+ }));
1877
+ const data = Array.isArray(response?.data) ? response.data : [];
1878
+ for (const row of data) {
1879
+ const record = asRecord2(row);
1880
+ const id = typeof record?.id === "string" ? record.id : "";
1881
+ if (!id) continue;
1882
+ 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";
1883
+ const preview = typeof record?.preview === "string" ? record.preview : "";
1884
+ threads.push({ id, title, preview });
1885
+ }
1886
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
1887
+ } while (cursor);
1888
+ const docs = [];
1889
+ const concurrency = 4;
1890
+ for (let offset = 0; offset < threads.length; offset += concurrency) {
1891
+ const batch = threads.slice(offset, offset + concurrency);
1892
+ const loaded = await Promise.all(batch.map(async (thread) => {
1893
+ try {
1894
+ const readResponse = await appServer.rpc("thread/read", {
1895
+ threadId: thread.id,
1896
+ includeTurns: true
1897
+ });
1898
+ const messageText = extractThreadMessageText(readResponse);
1899
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
1900
+ return {
1901
+ id: thread.id,
1902
+ title: thread.title,
1903
+ preview: thread.preview,
1904
+ messageText,
1905
+ searchableText
1906
+ };
1907
+ } catch {
1908
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
1909
+ return {
1910
+ id: thread.id,
1911
+ title: thread.title,
1912
+ preview: thread.preview,
1913
+ messageText: "",
1914
+ searchableText
1915
+ };
1916
+ }
1917
+ }));
1918
+ docs.push(...loaded);
1919
+ }
1920
+ return docs;
1921
+ }
1922
+ async function buildThreadSearchIndex(appServer) {
1923
+ const docs = await loadAllThreadsForSearch(appServer);
1924
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
1925
+ return { docsById };
1926
+ }
1405
1927
  function createCodexBridgeMiddleware() {
1406
1928
  const { appServer, methodCatalog } = getSharedBridgeState();
1929
+ let threadSearchIndex = null;
1930
+ let threadSearchIndexPromise = null;
1931
+ async function getThreadSearchIndex() {
1932
+ if (threadSearchIndex) return threadSearchIndex;
1933
+ if (!threadSearchIndexPromise) {
1934
+ threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
1935
+ threadSearchIndex = index;
1936
+ return index;
1937
+ }).finally(() => {
1938
+ threadSearchIndexPromise = null;
1939
+ });
1940
+ }
1941
+ return threadSearchIndexPromise;
1942
+ }
1407
1943
  void initializeSkillsSyncOnStartup(appServer);
1408
1944
  const middleware = async (req, res, next) => {
1409
1945
  try {
@@ -1412,25 +1948,28 @@ function createCodexBridgeMiddleware() {
1412
1948
  return;
1413
1949
  }
1414
1950
  const url = new URL(req.url, "http://localhost");
1951
+ if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
1952
+ return;
1953
+ }
1415
1954
  if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
1416
1955
  handleFileUpload(req, res);
1417
1956
  return;
1418
1957
  }
1419
1958
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
1420
1959
  const payload = await readJsonBody(req);
1421
- const body = asRecord(payload);
1960
+ const body = asRecord2(payload);
1422
1961
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
1423
- setJson(res, 400, { error: "Invalid body: expected { method, params? }" });
1962
+ setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
1424
1963
  return;
1425
1964
  }
1426
1965
  const result = await appServer.rpc(body.method, body.params ?? null);
1427
- setJson(res, 200, { result });
1966
+ setJson2(res, 200, { result });
1428
1967
  return;
1429
1968
  }
1430
1969
  if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
1431
1970
  const auth = await readCodexAuth();
1432
1971
  if (!auth) {
1433
- setJson(res, 401, { error: "No auth token available for transcription" });
1972
+ setJson2(res, 401, { error: "No auth token available for transcription" });
1434
1973
  return;
1435
1974
  }
1436
1975
  const rawBody = await readRawBody(req);
@@ -1444,37 +1983,107 @@ function createCodexBridgeMiddleware() {
1444
1983
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
1445
1984
  const payload = await readJsonBody(req);
1446
1985
  await appServer.respondToServerRequest(payload);
1447
- setJson(res, 200, { ok: true });
1986
+ setJson2(res, 200, { ok: true });
1448
1987
  return;
1449
1988
  }
1450
1989
  if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
1451
- setJson(res, 200, { data: appServer.listPendingServerRequests() });
1990
+ setJson2(res, 200, { data: appServer.listPendingServerRequests() });
1452
1991
  return;
1453
1992
  }
1454
1993
  if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
1455
1994
  const methods = await methodCatalog.listMethods();
1456
- setJson(res, 200, { data: methods });
1995
+ setJson2(res, 200, { data: methods });
1457
1996
  return;
1458
1997
  }
1459
1998
  if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
1460
1999
  const methods = await methodCatalog.listNotificationMethods();
1461
- setJson(res, 200, { data: methods });
2000
+ setJson2(res, 200, { data: methods });
1462
2001
  return;
1463
2002
  }
1464
2003
  if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
1465
2004
  const state = await readWorkspaceRootsState();
1466
- setJson(res, 200, { data: state });
2005
+ setJson2(res, 200, { data: state });
1467
2006
  return;
1468
2007
  }
1469
2008
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
1470
- setJson(res, 200, { data: { path: homedir() } });
2009
+ setJson2(res, 200, { data: { path: homedir2() } });
2010
+ return;
2011
+ }
2012
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
2013
+ const payload = asRecord2(await readJsonBody(req));
2014
+ const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
2015
+ if (!rawSourceCwd) {
2016
+ setJson2(res, 400, { error: "Missing sourceCwd" });
2017
+ return;
2018
+ }
2019
+ const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
2020
+ try {
2021
+ const sourceInfo = await stat2(sourceCwd);
2022
+ if (!sourceInfo.isDirectory()) {
2023
+ setJson2(res, 400, { error: "sourceCwd is not a directory" });
2024
+ return;
2025
+ }
2026
+ } catch {
2027
+ setJson2(res, 404, { error: "sourceCwd does not exist" });
2028
+ return;
2029
+ }
2030
+ try {
2031
+ let gitRoot = "";
2032
+ try {
2033
+ gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2034
+ } catch (error) {
2035
+ if (!isNotGitRepositoryError(error)) throw error;
2036
+ await runCommand2("git", ["init"], { cwd: sourceCwd });
2037
+ gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2038
+ }
2039
+ const repoName = basename(gitRoot) || "repo";
2040
+ const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
2041
+ await mkdir2(worktreesRoot, { recursive: true });
2042
+ let worktreeId = "";
2043
+ let worktreeParent = "";
2044
+ let worktreeCwd = "";
2045
+ for (let attempt = 0; attempt < 12; attempt += 1) {
2046
+ const candidate = randomBytes(2).toString("hex");
2047
+ const parent = join2(worktreesRoot, candidate);
2048
+ try {
2049
+ await stat2(parent);
2050
+ continue;
2051
+ } catch {
2052
+ worktreeId = candidate;
2053
+ worktreeParent = parent;
2054
+ worktreeCwd = join2(parent, repoName);
2055
+ break;
2056
+ }
2057
+ }
2058
+ if (!worktreeId || !worktreeParent || !worktreeCwd) {
2059
+ throw new Error("Failed to allocate a unique worktree id");
2060
+ }
2061
+ const branch = `codex/${worktreeId}`;
2062
+ await mkdir2(worktreeParent, { recursive: true });
2063
+ try {
2064
+ await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2065
+ } catch (error) {
2066
+ if (!isMissingHeadError(error)) throw error;
2067
+ await ensureRepoHasInitialCommit(gitRoot);
2068
+ await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2069
+ }
2070
+ setJson2(res, 200, {
2071
+ data: {
2072
+ cwd: worktreeCwd,
2073
+ branch,
2074
+ gitRoot
2075
+ }
2076
+ });
2077
+ } catch (error) {
2078
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
2079
+ }
1471
2080
  return;
1472
2081
  }
1473
2082
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1474
2083
  const payload = await readJsonBody(req);
1475
- const record = asRecord(payload);
2084
+ const record = asRecord2(payload);
1476
2085
  if (!record) {
1477
- setJson(res, 400, { error: "Invalid body: expected object" });
2086
+ setJson2(res, 400, { error: "Invalid body: expected object" });
1478
2087
  return;
1479
2088
  }
1480
2089
  const nextState = {
@@ -1483,33 +2092,33 @@ function createCodexBridgeMiddleware() {
1483
2092
  active: normalizeStringArray(record.active)
1484
2093
  };
1485
2094
  await writeWorkspaceRootsState(nextState);
1486
- setJson(res, 200, { ok: true });
2095
+ setJson2(res, 200, { ok: true });
1487
2096
  return;
1488
2097
  }
1489
2098
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
1490
- const payload = asRecord(await readJsonBody(req));
2099
+ const payload = asRecord2(await readJsonBody(req));
1491
2100
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
1492
2101
  const createIfMissing = payload?.createIfMissing === true;
1493
2102
  const label = typeof payload?.label === "string" ? payload.label : "";
1494
2103
  if (!rawPath) {
1495
- setJson(res, 400, { error: "Missing path" });
2104
+ setJson2(res, 400, { error: "Missing path" });
1496
2105
  return;
1497
2106
  }
1498
2107
  const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
1499
2108
  let pathExists = true;
1500
2109
  try {
1501
- const info = await stat(normalizedPath);
2110
+ const info = await stat2(normalizedPath);
1502
2111
  if (!info.isDirectory()) {
1503
- setJson(res, 400, { error: "Path exists but is not a directory" });
2112
+ setJson2(res, 400, { error: "Path exists but is not a directory" });
1504
2113
  return;
1505
2114
  }
1506
2115
  } catch {
1507
2116
  pathExists = false;
1508
2117
  }
1509
2118
  if (!pathExists && createIfMissing) {
1510
- await mkdir(normalizedPath, { recursive: true });
2119
+ await mkdir2(normalizedPath, { recursive: true });
1511
2120
  } else if (!pathExists) {
1512
- setJson(res, 404, { error: "Directory does not exist" });
2121
+ setJson2(res, 404, { error: "Directory does not exist" });
1513
2122
  return;
1514
2123
  }
1515
2124
  const existingState = await readWorkspaceRootsState();
@@ -1524,388 +2133,103 @@ function createCodexBridgeMiddleware() {
1524
2133
  labels: nextLabels,
1525
2134
  active: nextActive
1526
2135
  });
1527
- setJson(res, 200, { data: { path: normalizedPath } });
2136
+ setJson2(res, 200, { data: { path: normalizedPath } });
1528
2137
  return;
1529
2138
  }
1530
2139
  if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
1531
2140
  const basePath = url.searchParams.get("basePath")?.trim() ?? "";
1532
2141
  if (!basePath) {
1533
- setJson(res, 400, { error: "Missing basePath" });
2142
+ setJson2(res, 400, { error: "Missing basePath" });
1534
2143
  return;
1535
2144
  }
1536
2145
  const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
1537
2146
  try {
1538
- const baseInfo = await stat(normalizedBasePath);
2147
+ const baseInfo = await stat2(normalizedBasePath);
1539
2148
  if (!baseInfo.isDirectory()) {
1540
- setJson(res, 400, { error: "basePath is not a directory" });
2149
+ setJson2(res, 400, { error: "basePath is not a directory" });
1541
2150
  return;
1542
2151
  }
1543
2152
  } catch {
1544
- setJson(res, 404, { error: "basePath does not exist" });
2153
+ setJson2(res, 404, { error: "basePath does not exist" });
1545
2154
  return;
1546
2155
  }
1547
2156
  let index = 1;
1548
2157
  while (index < 1e5) {
1549
2158
  const candidateName = `New Project (${String(index)})`;
1550
- const candidatePath = join(normalizedBasePath, candidateName);
2159
+ const candidatePath = join2(normalizedBasePath, candidateName);
1551
2160
  try {
1552
- await stat(candidatePath);
2161
+ await stat2(candidatePath);
1553
2162
  index += 1;
1554
2163
  continue;
1555
2164
  } catch {
1556
- setJson(res, 200, { data: { name: candidateName, path: candidatePath } });
2165
+ setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
1557
2166
  return;
1558
2167
  }
1559
2168
  }
1560
- setJson(res, 500, { error: "Failed to compute project name suggestion" });
2169
+ setJson2(res, 500, { error: "Failed to compute project name suggestion" });
1561
2170
  return;
1562
2171
  }
1563
2172
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
1564
- const payload = asRecord(await readJsonBody(req));
2173
+ const payload = asRecord2(await readJsonBody(req));
1565
2174
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
1566
2175
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1567
2176
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
1568
2177
  const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
1569
2178
  if (!rawCwd) {
1570
- setJson(res, 400, { error: "Missing cwd" });
2179
+ setJson2(res, 400, { error: "Missing cwd" });
1571
2180
  return;
1572
2181
  }
1573
2182
  const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
1574
2183
  try {
1575
- const info = await stat(cwd);
2184
+ const info = await stat2(cwd);
1576
2185
  if (!info.isDirectory()) {
1577
- setJson(res, 400, { error: "cwd is not a directory" });
2186
+ setJson2(res, 400, { error: "cwd is not a directory" });
1578
2187
  return;
1579
2188
  }
1580
2189
  } catch {
1581
- setJson(res, 404, { error: "cwd does not exist" });
2190
+ setJson2(res, 404, { error: "cwd does not exist" });
1582
2191
  return;
1583
2192
  }
1584
2193
  try {
1585
2194
  const files = await listFilesWithRipgrep(cwd);
1586
2195
  const scored = files.map((path) => ({ path, score: scoreFileCandidate(path, query) })).filter((row) => query.length === 0 || row.score < 10).sort((a, b) => a.score - b.score || a.path.localeCompare(b.path)).slice(0, limit).map((row) => ({ path: row.path }));
1587
- setJson(res, 200, { data: scored });
2196
+ setJson2(res, 200, { data: scored });
1588
2197
  } catch (error) {
1589
- setJson(res, 500, { error: getErrorMessage(error, "Failed to search files") });
2198
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
1590
2199
  }
1591
2200
  return;
1592
2201
  }
1593
2202
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
1594
2203
  const cache = await readThreadTitleCache();
1595
- setJson(res, 200, { data: cache });
2204
+ setJson2(res, 200, { data: cache });
2205
+ return;
2206
+ }
2207
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
2208
+ const payload = asRecord2(await readJsonBody(req));
2209
+ const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2210
+ const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
2211
+ const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
2212
+ if (!query) {
2213
+ setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
2214
+ return;
2215
+ }
2216
+ const index = await getThreadSearchIndex();
2217
+ const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
2218
+ setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1596
2219
  return;
1597
2220
  }
1598
2221
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1599
- const payload = asRecord(await readJsonBody(req));
2222
+ const payload = asRecord2(await readJsonBody(req));
1600
2223
  const id = typeof payload?.id === "string" ? payload.id : "";
1601
2224
  const title = typeof payload?.title === "string" ? payload.title : "";
1602
2225
  if (!id) {
1603
- setJson(res, 400, { error: "Missing id" });
2226
+ setJson2(res, 400, { error: "Missing id" });
1604
2227
  return;
1605
2228
  }
1606
2229
  const cache = await readThreadTitleCache();
1607
2230
  const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
1608
2231
  await writeThreadTitleCache(next2);
1609
- setJson(res, 200, { ok: true });
1610
- return;
1611
- }
1612
- if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
1613
- try {
1614
- const q = url.searchParams.get("q") || "";
1615
- const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
1616
- const sort = url.searchParams.get("sort") || "date";
1617
- const allEntries = await fetchSkillsTree();
1618
- const installedMap = await scanInstalledSkillsFromDisk();
1619
- try {
1620
- const result = await appServer.rpc("skills/list", {});
1621
- for (const entry of result.data ?? []) {
1622
- for (const skill of entry.skills ?? []) {
1623
- if (skill.name) {
1624
- installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
1625
- }
1626
- }
1627
- }
1628
- } catch {
1629
- }
1630
- const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
1631
- await fetchMetaBatch(installedHubEntries);
1632
- const installed = [];
1633
- for (const [, info] of installedMap) {
1634
- const hubEntry = allEntries.find((e) => e.name === info.name);
1635
- const base = hubEntry ? buildHubEntry(hubEntry) : {
1636
- name: info.name,
1637
- owner: "local",
1638
- description: "",
1639
- displayName: "",
1640
- publishedAt: 0,
1641
- avatarUrl: "",
1642
- url: "",
1643
- installed: false
1644
- };
1645
- installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
1646
- }
1647
- const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
1648
- setJson(res, 200, { data: results, installed, total: allEntries.length });
1649
- } catch (error) {
1650
- setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
1651
- }
1652
- return;
1653
- }
1654
- if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
1655
- const state = await readSkillsSyncState();
1656
- setJson(res, 200, {
1657
- data: {
1658
- loggedIn: Boolean(state.githubToken),
1659
- githubUsername: state.githubUsername ?? "",
1660
- repoOwner: state.repoOwner ?? "",
1661
- repoName: state.repoName ?? "",
1662
- configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
1663
- startup: {
1664
- inProgress: startupSyncStatus.inProgress,
1665
- mode: startupSyncStatus.mode,
1666
- branch: startupSyncStatus.branch,
1667
- lastAction: startupSyncStatus.lastAction,
1668
- lastRunAtIso: startupSyncStatus.lastRunAtIso,
1669
- lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
1670
- lastError: startupSyncStatus.lastError
1671
- }
1672
- }
1673
- });
1674
- return;
1675
- }
1676
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
1677
- try {
1678
- const started = await startGithubDeviceLogin();
1679
- setJson(res, 200, { data: started });
1680
- } catch (error) {
1681
- setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
1682
- }
1683
- return;
1684
- }
1685
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
1686
- try {
1687
- const payload = asRecord(await readJsonBody(req));
1688
- const token = typeof payload?.token === "string" ? payload.token.trim() : "";
1689
- if (!token) {
1690
- setJson(res, 400, { error: "Missing GitHub token" });
1691
- return;
1692
- }
1693
- const username = await resolveGithubUsername(token);
1694
- await finalizeGithubLoginAndSync(token, username, appServer);
1695
- setJson(res, 200, { ok: true, data: { githubUsername: username } });
1696
- } catch (error) {
1697
- setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
1698
- }
1699
- return;
1700
- }
1701
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
1702
- try {
1703
- const state = await readSkillsSyncState();
1704
- await writeSkillsSyncState({
1705
- ...state,
1706
- githubToken: void 0,
1707
- githubUsername: void 0,
1708
- repoOwner: void 0,
1709
- repoName: void 0
1710
- });
1711
- setJson(res, 200, { ok: true });
1712
- } catch (error) {
1713
- setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
1714
- }
1715
- return;
1716
- }
1717
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
1718
- try {
1719
- const payload = asRecord(await readJsonBody(req));
1720
- const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
1721
- if (!deviceCode) {
1722
- setJson(res, 400, { error: "Missing deviceCode" });
1723
- return;
1724
- }
1725
- const result = await completeGithubDeviceLogin(deviceCode);
1726
- if (!result.token) {
1727
- setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
1728
- return;
1729
- }
1730
- const token = result.token;
1731
- const username = await resolveGithubUsername(token);
1732
- await finalizeGithubLoginAndSync(token, username, appServer);
1733
- setJson(res, 200, { ok: true, data: { githubUsername: username } });
1734
- } catch (error) {
1735
- setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
1736
- }
1737
- return;
1738
- }
1739
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
1740
- try {
1741
- const state = await readSkillsSyncState();
1742
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
1743
- setJson(res, 400, { error: "Skills sync is not configured yet" });
1744
- return;
1745
- }
1746
- if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
1747
- setJson(res, 400, { error: "Refusing to push to upstream repository" });
1748
- return;
1749
- }
1750
- const local = await collectLocalSyncedSkills(appServer);
1751
- const installedMap = await scanInstalledSkillsFromDisk();
1752
- await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
1753
- await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
1754
- setJson(res, 200, { ok: true, data: { synced: local.length } });
1755
- } catch (error) {
1756
- setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
1757
- }
1758
- return;
1759
- }
1760
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
1761
- try {
1762
- const state = await readSkillsSyncState();
1763
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
1764
- setJson(res, 400, { error: "Skills sync is not configured yet" });
1765
- return;
1766
- }
1767
- const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
1768
- const tree = await fetchSkillsTree();
1769
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
1770
- const ambiguousNames = /* @__PURE__ */ new Set();
1771
- for (const entry of tree) {
1772
- if (ambiguousNames.has(entry.name)) continue;
1773
- const existingOwner = uniqueOwnerByName.get(entry.name);
1774
- if (!existingOwner) {
1775
- uniqueOwnerByName.set(entry.name, entry.owner);
1776
- continue;
1777
- }
1778
- if (existingOwner !== entry.owner) {
1779
- uniqueOwnerByName.delete(entry.name);
1780
- ambiguousNames.add(entry.name);
1781
- }
1782
- }
1783
- const localDir = await detectUserSkillsDir(appServer);
1784
- await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName, localDir);
1785
- const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
1786
- const localSkills = await scanInstalledSkillsFromDisk();
1787
- for (const skill of remote) {
1788
- const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
1789
- if (!owner) {
1790
- continue;
1791
- }
1792
- if (!localSkills.has(skill.name)) {
1793
- await runCommand("python3", [
1794
- installerScript,
1795
- "--repo",
1796
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1797
- "--path",
1798
- `skills/${owner}/${skill.name}`,
1799
- "--dest",
1800
- localDir,
1801
- "--method",
1802
- "git"
1803
- ]);
1804
- }
1805
- const skillPath = join(localDir, skill.name);
1806
- await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1807
- }
1808
- const remoteNames = new Set(remote.map((row) => row.name));
1809
- for (const [name, localInfo] of localSkills.entries()) {
1810
- if (!remoteNames.has(name)) {
1811
- await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
1812
- }
1813
- }
1814
- const nextOwners = {};
1815
- for (const item of remote) {
1816
- const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
1817
- if (owner) nextOwners[item.name] = owner;
1818
- }
1819
- await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
1820
- try {
1821
- await appServer.rpc("skills/list", { forceReload: true });
1822
- } catch {
1823
- }
1824
- setJson(res, 200, { ok: true, data: { synced: remote.length } });
1825
- } catch (error) {
1826
- setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
1827
- }
1828
- return;
1829
- }
1830
- if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
1831
- try {
1832
- const owner = url.searchParams.get("owner") || "";
1833
- const name = url.searchParams.get("name") || "";
1834
- if (!owner || !name) {
1835
- setJson(res, 400, { error: "Missing owner or name" });
1836
- return;
1837
- }
1838
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1839
- const resp = await fetch(rawUrl);
1840
- if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1841
- const content = await resp.text();
1842
- setJson(res, 200, { content });
1843
- } catch (error) {
1844
- setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
1845
- }
1846
- return;
1847
- }
1848
- if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
1849
- try {
1850
- const payload = asRecord(await readJsonBody(req));
1851
- const owner = typeof payload?.owner === "string" ? payload.owner : "";
1852
- const name = typeof payload?.name === "string" ? payload.name : "";
1853
- if (!owner || !name) {
1854
- setJson(res, 400, { error: "Missing owner or name" });
1855
- return;
1856
- }
1857
- const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
1858
- const installDest = await detectUserSkillsDir(appServer);
1859
- const skillPathInRepo = `skills/${owner}/${name}`;
1860
- await runCommand("python3", [
1861
- installerScript,
1862
- "--repo",
1863
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1864
- "--path",
1865
- skillPathInRepo,
1866
- "--dest",
1867
- installDest,
1868
- "--method",
1869
- "git"
1870
- ]);
1871
- const skillDir = join(installDest, name);
1872
- await ensureInstalledSkillIsValid(appServer, skillDir);
1873
- const syncState = await readSkillsSyncState();
1874
- const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1875
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1876
- await autoPushSyncedSkills(appServer);
1877
- setJson(res, 200, { ok: true, path: skillDir });
1878
- } catch (error) {
1879
- setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
1880
- }
1881
- return;
1882
- }
1883
- if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
1884
- try {
1885
- const payload = asRecord(await readJsonBody(req));
1886
- const name = typeof payload?.name === "string" ? payload.name : "";
1887
- const path = typeof payload?.path === "string" ? payload.path : "";
1888
- const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1889
- if (!target) {
1890
- setJson(res, 400, { error: "Missing name or path" });
1891
- return;
1892
- }
1893
- await rm(target, { recursive: true, force: true });
1894
- if (name) {
1895
- const syncState = await readSkillsSyncState();
1896
- const nextOwners = { ...syncState.installedOwners ?? {} };
1897
- delete nextOwners[name];
1898
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1899
- }
1900
- await autoPushSyncedSkills(appServer);
1901
- try {
1902
- await appServer.rpc("skills/list", { forceReload: true });
1903
- } catch {
1904
- }
1905
- setJson(res, 200, { ok: true, deletedPath: target });
1906
- } catch (error) {
1907
- setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
1908
- }
2232
+ setJson2(res, 200, { ok: true });
1909
2233
  return;
1910
2234
  }
1911
2235
  if (req.method === "GET" && url.pathname === "/codex-api/events") {
@@ -1940,11 +2264,12 @@ data: ${JSON.stringify({ ok: true })}
1940
2264
  }
1941
2265
  next();
1942
2266
  } catch (error) {
1943
- const message = getErrorMessage(error, "Unknown bridge error");
1944
- setJson(res, 502, { error: message });
2267
+ const message = getErrorMessage2(error, "Unknown bridge error");
2268
+ setJson2(res, 502, { error: message });
1945
2269
  }
1946
2270
  };
1947
2271
  middleware.dispose = () => {
2272
+ threadSearchIndex = null;
1948
2273
  appServer.dispose();
1949
2274
  };
1950
2275
  middleware.subscribeNotifications = (listener) => {
@@ -1959,7 +2284,7 @@ data: ${JSON.stringify({ ok: true })}
1959
2284
  }
1960
2285
 
1961
2286
  // src/server/authMiddleware.ts
1962
- import { randomBytes, timingSafeEqual } from "crypto";
2287
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1963
2288
  var TOKEN_COOKIE = "codex_web_local_token";
1964
2289
  function constantTimeCompare(a, b) {
1965
2290
  const bufA = Buffer.from(a);
@@ -2057,7 +2382,7 @@ function createAuthSession(password) {
2057
2382
  res.status(401).json({ error: "Invalid password" });
2058
2383
  return;
2059
2384
  }
2060
- const token = randomBytes(32).toString("hex");
2385
+ const token = randomBytes2(32).toString("hex");
2061
2386
  validTokens.add(token);
2062
2387
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
2063
2388
  res.json({ ok: true });
@@ -2076,11 +2401,277 @@ function createAuthSession(password) {
2076
2401
  };
2077
2402
  }
2078
2403
 
2404
+ // src/server/localBrowseUi.ts
2405
+ import { dirname, extname, join as join3 } from "path";
2406
+ import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
2407
+ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2408
+ ".txt",
2409
+ ".md",
2410
+ ".json",
2411
+ ".js",
2412
+ ".ts",
2413
+ ".tsx",
2414
+ ".jsx",
2415
+ ".css",
2416
+ ".scss",
2417
+ ".html",
2418
+ ".htm",
2419
+ ".xml",
2420
+ ".yml",
2421
+ ".yaml",
2422
+ ".log",
2423
+ ".csv",
2424
+ ".env",
2425
+ ".py",
2426
+ ".sh",
2427
+ ".toml",
2428
+ ".ini",
2429
+ ".conf",
2430
+ ".sql",
2431
+ ".bat",
2432
+ ".cmd",
2433
+ ".ps1"
2434
+ ]);
2435
+ function languageForPath(pathValue) {
2436
+ const extension = extname(pathValue).toLowerCase();
2437
+ switch (extension) {
2438
+ case ".js":
2439
+ return "javascript";
2440
+ case ".ts":
2441
+ return "typescript";
2442
+ case ".jsx":
2443
+ return "javascript";
2444
+ case ".tsx":
2445
+ return "typescript";
2446
+ case ".py":
2447
+ return "python";
2448
+ case ".sh":
2449
+ return "sh";
2450
+ case ".css":
2451
+ case ".scss":
2452
+ return "css";
2453
+ case ".html":
2454
+ case ".htm":
2455
+ return "html";
2456
+ case ".json":
2457
+ return "json";
2458
+ case ".md":
2459
+ return "markdown";
2460
+ case ".yaml":
2461
+ case ".yml":
2462
+ return "yaml";
2463
+ case ".xml":
2464
+ return "xml";
2465
+ case ".sql":
2466
+ return "sql";
2467
+ case ".toml":
2468
+ return "ini";
2469
+ case ".ini":
2470
+ case ".conf":
2471
+ return "ini";
2472
+ default:
2473
+ return "plaintext";
2474
+ }
2475
+ }
2476
+ function normalizeLocalPath(rawPath) {
2477
+ const trimmed = rawPath.trim();
2478
+ if (!trimmed) return "";
2479
+ if (trimmed.startsWith("file://")) {
2480
+ try {
2481
+ return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
2482
+ } catch {
2483
+ return trimmed.replace(/^file:\/\//u, "");
2484
+ }
2485
+ }
2486
+ return trimmed;
2487
+ }
2488
+ function decodeBrowsePath(rawPath) {
2489
+ if (!rawPath) return "";
2490
+ try {
2491
+ return decodeURIComponent(rawPath);
2492
+ } catch {
2493
+ return rawPath;
2494
+ }
2495
+ }
2496
+ function isTextEditablePath(pathValue) {
2497
+ return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
2498
+ }
2499
+ function looksLikeTextBuffer(buffer) {
2500
+ if (buffer.length === 0) return true;
2501
+ for (const byte of buffer) {
2502
+ if (byte === 0) return false;
2503
+ }
2504
+ const decoded = buffer.toString("utf8");
2505
+ const replacementCount = (decoded.match(/\uFFFD/gu) ?? []).length;
2506
+ return replacementCount / decoded.length < 0.05;
2507
+ }
2508
+ async function probeFileIsText(localPath) {
2509
+ const handle = await open(localPath, "r");
2510
+ try {
2511
+ const sample = Buffer.allocUnsafe(4096);
2512
+ const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
2513
+ return looksLikeTextBuffer(sample.subarray(0, bytesRead));
2514
+ } finally {
2515
+ await handle.close();
2516
+ }
2517
+ }
2518
+ async function isTextEditableFile(localPath) {
2519
+ if (isTextEditablePath(localPath)) return true;
2520
+ try {
2521
+ const fileStat = await stat3(localPath);
2522
+ if (!fileStat.isFile()) return false;
2523
+ return await probeFileIsText(localPath);
2524
+ } catch {
2525
+ return false;
2526
+ }
2527
+ }
2528
+ function escapeHtml(value) {
2529
+ return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
2530
+ }
2531
+ function toBrowseHref(pathValue) {
2532
+ return `/codex-local-browse${encodeURI(pathValue)}`;
2533
+ }
2534
+ function toEditHref(pathValue) {
2535
+ return `/codex-local-edit${encodeURI(pathValue)}`;
2536
+ }
2537
+ function escapeForInlineScriptString(value) {
2538
+ return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
2539
+ }
2540
+ async function getDirectoryItems(localPath) {
2541
+ const entries = await readdir3(localPath, { withFileTypes: true });
2542
+ const withMeta = await Promise.all(entries.map(async (entry) => {
2543
+ const entryPath = join3(localPath, entry.name);
2544
+ const entryStat = await stat3(entryPath);
2545
+ const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2546
+ return {
2547
+ name: entry.name,
2548
+ path: entryPath,
2549
+ isDirectory: entry.isDirectory(),
2550
+ editable,
2551
+ mtimeMs: entryStat.mtimeMs
2552
+ };
2553
+ }));
2554
+ return withMeta.sort((a, b) => {
2555
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
2556
+ if (a.isDirectory && !b.isDirectory) return -1;
2557
+ if (!a.isDirectory && b.isDirectory) return 1;
2558
+ return a.name.localeCompare(b.name);
2559
+ });
2560
+ }
2561
+ async function createDirectoryListingHtml(localPath) {
2562
+ const items = await getDirectoryItems(localPath);
2563
+ const parentPath = dirname(localPath);
2564
+ const rows = items.map((item) => {
2565
+ const suffix = item.isDirectory ? "/" : "";
2566
+ const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
2567
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
2568
+ }).join("\n");
2569
+ const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
2570
+ return `<!doctype html>
2571
+ <html lang="en">
2572
+ <head>
2573
+ <meta charset="utf-8" />
2574
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2575
+ <title>Index of ${escapeHtml(localPath)}</title>
2576
+ <style>
2577
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
2578
+ a { color: #8cc2ff; text-decoration: none; }
2579
+ a:hover { text-decoration: underline; }
2580
+ ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
2581
+ .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
2582
+ .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
2583
+ .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; }
2584
+ .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
2585
+ h1 { font-size: 18px; margin: 0; word-break: break-all; }
2586
+ @media (max-width: 640px) {
2587
+ body { margin: 12px; }
2588
+ .file-row { gap: 8px; }
2589
+ .file-link { font-size: 15px; padding: 12px; }
2590
+ .icon-btn { width: 44px; height: 44px; }
2591
+ }
2592
+ </style>
2593
+ </head>
2594
+ <body>
2595
+ <h1>Index of ${escapeHtml(localPath)}</h1>
2596
+ ${parentLink}
2597
+ <ul>${rows}</ul>
2598
+ </body>
2599
+ </html>`;
2600
+ }
2601
+ async function createTextEditorHtml(localPath) {
2602
+ const content = await readFile3(localPath, "utf8");
2603
+ const parentPath = dirname(localPath);
2604
+ const language = languageForPath(localPath);
2605
+ const safeContentLiteral = escapeForInlineScriptString(content);
2606
+ return `<!doctype html>
2607
+ <html lang="en">
2608
+ <head>
2609
+ <meta charset="utf-8" />
2610
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2611
+ <title>Edit ${escapeHtml(localPath)}</title>
2612
+ <style>
2613
+ html, body { width: 100%; height: 100%; margin: 0; }
2614
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
2615
+ .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; }
2616
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
2617
+ button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
2618
+ button:hover, a:hover { filter: brightness(1.08); }
2619
+ #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
2620
+ #status { margin-left: 8px; color: #8cc2ff; }
2621
+ .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
2622
+ .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
2623
+ .ace_marker-layer .ace_active-line { background: #10213c !important; }
2624
+ .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
2625
+ .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
2626
+ </style>
2627
+ </head>
2628
+ <body>
2629
+ <div class="toolbar">
2630
+ <div class="row">
2631
+ <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
2632
+ <button id="saveBtn" type="button">Save</button>
2633
+ <span id="status"></span>
2634
+ </div>
2635
+ <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
2636
+ </div>
2637
+ <div id="editor"></div>
2638
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
2639
+ <script>
2640
+ const saveBtn = document.getElementById('saveBtn');
2641
+ const status = document.getElementById('status');
2642
+ const editor = ace.edit('editor');
2643
+ editor.setTheme('ace/theme/tomorrow_night');
2644
+ editor.session.setMode('ace/mode/${escapeHtml(language)}');
2645
+ editor.setValue(${safeContentLiteral}, -1);
2646
+ editor.setOptions({
2647
+ fontSize: '13px',
2648
+ wrap: true,
2649
+ showPrintMargin: false,
2650
+ useSoftTabs: true,
2651
+ tabSize: 2,
2652
+ behavioursEnabled: true,
2653
+ });
2654
+ editor.resize();
2655
+
2656
+ saveBtn.addEventListener('click', async () => {
2657
+ status.textContent = 'Saving...';
2658
+ const response = await fetch(location.pathname, {
2659
+ method: 'PUT',
2660
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
2661
+ body: editor.getValue(),
2662
+ });
2663
+ status.textContent = response.ok ? 'Saved' : 'Save failed';
2664
+ });
2665
+ </script>
2666
+ </body>
2667
+ </html>`;
2668
+ }
2669
+
2079
2670
  // src/server/httpServer.ts
2080
2671
  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");
2672
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
2673
+ var distDir = join4(__dirname, "..", "dist");
2674
+ var spaEntryFile = join4(distDir, "index.html");
2084
2675
  var IMAGE_CONTENT_TYPES = {
2085
2676
  ".avif": "image/avif",
2086
2677
  ".bmp": "image/bmp",
@@ -2103,6 +2694,11 @@ function normalizeLocalImagePath(rawPath) {
2103
2694
  }
2104
2695
  return trimmed;
2105
2696
  }
2697
+ function readWildcardPathParam(value) {
2698
+ if (typeof value === "string") return value;
2699
+ if (Array.isArray(value)) return value.join("/");
2700
+ return "";
2701
+ }
2106
2702
  function createServer(options = {}) {
2107
2703
  const app = express();
2108
2704
  const bridge = createCodexBridgeMiddleware();
@@ -2118,7 +2714,7 @@ function createServer(options = {}) {
2118
2714
  res.status(400).json({ error: "Expected absolute local file path." });
2119
2715
  return;
2120
2716
  }
2121
- const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
2717
+ const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
2122
2718
  if (!contentType) {
2123
2719
  res.status(415).json({ error: "Unsupported image type." });
2124
2720
  return;
@@ -2130,6 +2726,81 @@ function createServer(options = {}) {
2130
2726
  if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
2131
2727
  });
2132
2728
  });
2729
+ app.get("/codex-local-file", (req, res) => {
2730
+ const rawPath = typeof req.query.path === "string" ? req.query.path : "";
2731
+ const localPath = normalizeLocalPath(rawPath);
2732
+ if (!localPath || !isAbsolute2(localPath)) {
2733
+ res.status(400).json({ error: "Expected absolute local file path." });
2734
+ return;
2735
+ }
2736
+ res.setHeader("Cache-Control", "private, no-store");
2737
+ res.setHeader("Content-Disposition", "inline");
2738
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
2739
+ if (!error) return;
2740
+ if (!res.headersSent) res.status(404).json({ error: "File not found." });
2741
+ });
2742
+ });
2743
+ app.get("/codex-local-browse/*path", async (req, res) => {
2744
+ const rawPath = readWildcardPathParam(req.params.path);
2745
+ const localPath = decodeBrowsePath(`/${rawPath}`);
2746
+ if (!localPath || !isAbsolute2(localPath)) {
2747
+ res.status(400).json({ error: "Expected absolute local file path." });
2748
+ return;
2749
+ }
2750
+ try {
2751
+ const fileStat = await stat4(localPath);
2752
+ res.setHeader("Cache-Control", "private, no-store");
2753
+ if (fileStat.isDirectory()) {
2754
+ const html = await createDirectoryListingHtml(localPath);
2755
+ res.status(200).type("text/html; charset=utf-8").send(html);
2756
+ return;
2757
+ }
2758
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
2759
+ if (!error) return;
2760
+ if (!res.headersSent) res.status(404).json({ error: "File not found." });
2761
+ });
2762
+ } catch {
2763
+ res.status(404).json({ error: "File not found." });
2764
+ }
2765
+ });
2766
+ app.get("/codex-local-edit/*path", async (req, res) => {
2767
+ const rawPath = readWildcardPathParam(req.params.path);
2768
+ const localPath = decodeBrowsePath(`/${rawPath}`);
2769
+ if (!localPath || !isAbsolute2(localPath)) {
2770
+ res.status(400).json({ error: "Expected absolute local file path." });
2771
+ return;
2772
+ }
2773
+ try {
2774
+ const fileStat = await stat4(localPath);
2775
+ if (!fileStat.isFile()) {
2776
+ res.status(400).json({ error: "Expected file path." });
2777
+ return;
2778
+ }
2779
+ const html = await createTextEditorHtml(localPath);
2780
+ res.status(200).type("text/html; charset=utf-8").send(html);
2781
+ } catch {
2782
+ res.status(404).json({ error: "File not found." });
2783
+ }
2784
+ });
2785
+ app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
2786
+ const rawPath = readWildcardPathParam(req.params.path);
2787
+ const localPath = decodeBrowsePath(`/${rawPath}`);
2788
+ if (!localPath || !isAbsolute2(localPath)) {
2789
+ res.status(400).json({ error: "Expected absolute local file path." });
2790
+ return;
2791
+ }
2792
+ if (!await isTextEditableFile(localPath)) {
2793
+ res.status(415).json({ error: "Only text-like files are editable." });
2794
+ return;
2795
+ }
2796
+ const body = typeof req.body === "string" ? req.body : "";
2797
+ try {
2798
+ await writeFile3(localPath, body, "utf8");
2799
+ res.status(200).json({ ok: true });
2800
+ } catch {
2801
+ res.status(404).json({ error: "File not found." });
2802
+ }
2803
+ });
2133
2804
  const hasFrontendAssets = existsSync2(spaEntryFile);
2134
2805
  if (hasFrontendAssets) {
2135
2806
  app.use(express.static(distDir));
@@ -2201,11 +2872,11 @@ function generatePassword() {
2201
2872
 
2202
2873
  // src/cli/index.ts
2203
2874
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2204
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2875
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2205
2876
  async function readCliVersion() {
2206
2877
  try {
2207
- const packageJsonPath = join3(__dirname2, "..", "package.json");
2208
- const raw = await readFile2(packageJsonPath, "utf8");
2878
+ const packageJsonPath = join5(__dirname2, "..", "package.json");
2879
+ const raw = await readFile4(packageJsonPath, "utf8");
2209
2880
  const parsed = JSON.parse(raw);
2210
2881
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2211
2882
  } catch {
@@ -2230,13 +2901,13 @@ function runWithStatus(command, args) {
2230
2901
  return result.status ?? -1;
2231
2902
  }
2232
2903
  function getUserNpmPrefix() {
2233
- return join3(homedir2(), ".npm-global");
2904
+ return join5(homedir3(), ".npm-global");
2234
2905
  }
2235
2906
  function resolveCodexCommand() {
2236
2907
  if (canRun("codex", ["--version"])) {
2237
2908
  return "codex";
2238
2909
  }
2239
- const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
2910
+ const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
2240
2911
  if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
2241
2912
  return userCandidate;
2242
2913
  }
@@ -2244,15 +2915,113 @@ function resolveCodexCommand() {
2244
2915
  if (!prefix) {
2245
2916
  return null;
2246
2917
  }
2247
- const candidate = join3(prefix, "bin", "codex");
2918
+ const candidate = join5(prefix, "bin", "codex");
2248
2919
  if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
2249
2920
  return candidate;
2250
2921
  }
2251
2922
  return null;
2252
2923
  }
2924
+ function resolveCloudflaredCommand() {
2925
+ if (canRun("cloudflared", ["--version"])) {
2926
+ return "cloudflared";
2927
+ }
2928
+ const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
2929
+ if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
2930
+ return localCandidate;
2931
+ }
2932
+ return null;
2933
+ }
2934
+ function mapCloudflaredLinuxArch(arch) {
2935
+ if (arch === "x64") {
2936
+ return "amd64";
2937
+ }
2938
+ if (arch === "arm64") {
2939
+ return "arm64";
2940
+ }
2941
+ return null;
2942
+ }
2943
+ function downloadFile(url, destination) {
2944
+ return new Promise((resolve2, reject) => {
2945
+ const request = (currentUrl) => {
2946
+ httpsGet(currentUrl, (response) => {
2947
+ const code = response.statusCode ?? 0;
2948
+ if (code >= 300 && code < 400 && response.headers.location) {
2949
+ response.resume();
2950
+ request(response.headers.location);
2951
+ return;
2952
+ }
2953
+ if (code !== 200) {
2954
+ response.resume();
2955
+ reject(new Error(`Download failed with HTTP status ${String(code)}`));
2956
+ return;
2957
+ }
2958
+ const file = createWriteStream(destination, { mode: 493 });
2959
+ response.pipe(file);
2960
+ file.on("finish", () => {
2961
+ file.close();
2962
+ resolve2();
2963
+ });
2964
+ file.on("error", reject);
2965
+ }).on("error", reject);
2966
+ };
2967
+ request(url);
2968
+ });
2969
+ }
2970
+ async function ensureCloudflaredInstalledLinux() {
2971
+ const current = resolveCloudflaredCommand();
2972
+ if (current) {
2973
+ return current;
2974
+ }
2975
+ if (process.platform !== "linux") {
2976
+ return null;
2977
+ }
2978
+ const mappedArch = mapCloudflaredLinuxArch(process.arch);
2979
+ if (!mappedArch) {
2980
+ throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2981
+ }
2982
+ const userBinDir = join5(homedir3(), ".local", "bin");
2983
+ mkdirSync(userBinDir, { recursive: true });
2984
+ const destination = join5(userBinDir, "cloudflared");
2985
+ const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2986
+ console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
2987
+ await downloadFile(downloadUrl, destination);
2988
+ chmodSync(destination, 493);
2989
+ process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
2990
+ const installed = resolveCloudflaredCommand();
2991
+ if (!installed) {
2992
+ throw new Error("cloudflared download completed but executable is still not available");
2993
+ }
2994
+ console.log("\ncloudflared installed.\n");
2995
+ return installed;
2996
+ }
2997
+ async function shouldInstallCloudflaredInteractively() {
2998
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2999
+ console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3000
+ return false;
3001
+ }
3002
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
3003
+ try {
3004
+ const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
3005
+ const normalized = answer.trim().toLowerCase();
3006
+ return normalized === "y" || normalized === "yes";
3007
+ } finally {
3008
+ prompt.close();
3009
+ }
3010
+ }
3011
+ async function resolveCloudflaredForTunnel() {
3012
+ const current = resolveCloudflaredCommand();
3013
+ if (current) {
3014
+ return current;
3015
+ }
3016
+ const installApproved = await shouldInstallCloudflaredInteractively();
3017
+ if (!installApproved) {
3018
+ return null;
3019
+ }
3020
+ return ensureCloudflaredInstalledLinux();
3021
+ }
2253
3022
  function hasCodexAuth() {
2254
- const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
2255
- return existsSync3(join3(codexHome, "auth.json"));
3023
+ const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3024
+ return existsSync3(join5(codexHome, "auth.json"));
2256
3025
  }
2257
3026
  function ensureCodexInstalled() {
2258
3027
  let codexCommand = resolveCodexCommand();
@@ -2270,7 +3039,7 @@ function ensureCodexInstalled() {
2270
3039
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
2271
3040
  `);
2272
3041
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
2273
- process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3042
+ process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2274
3043
  };
2275
3044
  if (isTermuxRuntime()) {
2276
3045
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2319,7 +3088,7 @@ function printTermuxKeepAlive(lines) {
2319
3088
  }
2320
3089
  function openBrowser(url) {
2321
3090
  const command = process.platform === "darwin" ? { cmd: "open", args: [url] } : process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
2322
- const child = spawn2(command.cmd, command.args, { detached: true, stdio: "ignore" });
3091
+ const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
2323
3092
  child.on("error", () => {
2324
3093
  });
2325
3094
  child.unref();
@@ -2331,9 +3100,27 @@ function parseCloudflaredUrl(chunk) {
2331
3100
  }
2332
3101
  return urlMatch[urlMatch.length - 1] ?? null;
2333
3102
  }
2334
- async function startCloudflaredTunnel(localPort) {
3103
+ function getAccessibleUrls(port) {
3104
+ const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
3105
+ const interfaces = networkInterfaces();
3106
+ for (const entries of Object.values(interfaces)) {
3107
+ if (!entries) {
3108
+ continue;
3109
+ }
3110
+ for (const entry of entries) {
3111
+ if (entry.internal) {
3112
+ continue;
3113
+ }
3114
+ if (entry.family === "IPv4") {
3115
+ urls.add(`http://${entry.address}:${String(port)}`);
3116
+ }
3117
+ }
3118
+ }
3119
+ return Array.from(urls);
3120
+ }
3121
+ async function startCloudflaredTunnel(command, localPort) {
2335
3122
  return new Promise((resolve2, reject) => {
2336
- const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
3123
+ const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2337
3124
  stdio: ["ignore", "pipe", "pipe"]
2338
3125
  });
2339
3126
  const timeout = setTimeout(() => {
@@ -2384,7 +3171,7 @@ function listenWithFallback(server, startPort) {
2384
3171
  };
2385
3172
  server.once("error", onError);
2386
3173
  server.once("listening", onListening);
2387
- server.listen(port);
3174
+ server.listen(port, "0.0.0.0");
2388
3175
  };
2389
3176
  attempt(startPort);
2390
3177
  });
@@ -2406,7 +3193,11 @@ async function startServer(options) {
2406
3193
  let tunnelUrl = null;
2407
3194
  if (options.tunnel) {
2408
3195
  try {
2409
- const tunnel = await startCloudflaredTunnel(port);
3196
+ const cloudflaredCommand = await resolveCloudflaredForTunnel();
3197
+ if (!cloudflaredCommand) {
3198
+ throw new Error("cloudflared is not installed");
3199
+ }
3200
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
2410
3201
  tunnelChild = tunnel.process;
2411
3202
  tunnelUrl = tunnel.url;
2412
3203
  } catch (error) {
@@ -2421,8 +3212,15 @@ async function startServer(options) {
2421
3212
  ` Version: ${version}`,
2422
3213
  " GitHub: https://github.com/friuns2/codexui",
2423
3214
  "",
2424
- ` Local: http://localhost:${String(port)}`
3215
+ ` Bind: http://0.0.0.0:${String(port)}`
2425
3216
  ];
3217
+ const accessUrls = getAccessibleUrls(port);
3218
+ if (accessUrls.length > 0) {
3219
+ lines.push(` Local: ${accessUrls[0]}`);
3220
+ for (const accessUrl of accessUrls.slice(1)) {
3221
+ lines.push(` Network: ${accessUrl}`);
3222
+ }
3223
+ }
2426
3224
  if (port !== requestedPort) {
2427
3225
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
2428
3226
  }
@@ -2431,9 +3229,7 @@ async function startServer(options) {
2431
3229
  }
2432
3230
  if (tunnelUrl) {
2433
3231
  lines.push(` Tunnel: ${tunnelUrl}`);
2434
- lines.push("");
2435
- lines.push(" Tunnel QR code:");
2436
- lines.push(` URL: ${tunnelUrl}`);
3232
+ lines.push(" Tunnel QR code below");
2437
3233
  }
2438
3234
  printTermuxKeepAlive(lines);
2439
3235
  lines.push("");