codexapp 0.1.53 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cli/index.js CHANGED
@@ -2,22 +2,164 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync } from "fs";
6
6
  import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
7
- import { homedir as homedir3, networkInterfaces } from "os";
8
- import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
9
- import { spawn as spawn3, spawnSync } from "child_process";
10
- import { createInterface } from "readline/promises";
7
+ import { homedir as homedir4, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "path";
9
+ import { spawn as spawn3 } from "child_process";
10
+ import { createInterface as createInterface2 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
- import { dirname as dirname3 } from "path";
12
+ import { dirname as dirname4 } from "path";
13
13
  import { get as httpsGet } from "https";
14
14
  import { Command } from "commander";
15
15
  import qrcode from "qrcode-terminal";
16
16
 
17
+ // src/commandResolution.ts
18
+ import { spawnSync } from "child_process";
19
+ import { existsSync } from "fs";
20
+ import { homedir } from "os";
21
+ import { delimiter, join } from "path";
22
+ function uniqueStrings(values) {
23
+ const unique = [];
24
+ for (const value of values) {
25
+ const normalized = value?.trim();
26
+ if (!normalized || unique.includes(normalized)) continue;
27
+ unique.push(normalized);
28
+ }
29
+ return unique;
30
+ }
31
+ function isPathLike(command) {
32
+ return command.includes("/") || command.includes("\\") || /^[a-zA-Z]:/.test(command);
33
+ }
34
+ function isRunnableCommand(command, args = []) {
35
+ if (isPathLike(command) && !existsSync(command)) {
36
+ return false;
37
+ }
38
+ return canRunCommand(command, args);
39
+ }
40
+ function getWindowsAppDataNpmPrefix() {
41
+ const appData = process.env.APPDATA?.trim();
42
+ return appData ? join(appData, "npm") : null;
43
+ }
44
+ function getPotentialNpmPrefixes() {
45
+ return uniqueStrings([
46
+ process.env.npm_config_prefix,
47
+ process.env.PREFIX,
48
+ getUserNpmPrefix(),
49
+ process.platform === "win32" ? getWindowsAppDataNpmPrefix() : null
50
+ ]);
51
+ }
52
+ function getPotentialCodexPackageDirs(prefix) {
53
+ const dirs = [join(prefix, "node_modules", "@openai", "codex")];
54
+ if (process.platform !== "win32") {
55
+ dirs.push(join(prefix, "lib", "node_modules", "@openai", "codex"));
56
+ }
57
+ return dirs;
58
+ }
59
+ function getPotentialCodexExecutables(prefix) {
60
+ return getPotentialCodexPackageDirs(prefix).map((packageDir) => process.platform === "win32" ? join(
61
+ packageDir,
62
+ "node_modules",
63
+ "@openai",
64
+ "codex-win32-x64",
65
+ "vendor",
66
+ "x86_64-pc-windows-msvc",
67
+ "codex",
68
+ "codex.exe"
69
+ ) : join(packageDir, "bin", "codex"));
70
+ }
71
+ function getPotentialRipgrepExecutables(prefix) {
72
+ return getPotentialCodexPackageDirs(prefix).map((packageDir) => process.platform === "win32" ? join(
73
+ packageDir,
74
+ "node_modules",
75
+ "@openai",
76
+ "codex-win32-x64",
77
+ "vendor",
78
+ "x86_64-pc-windows-msvc",
79
+ "path",
80
+ "rg.exe"
81
+ ) : join(packageDir, "bin", "rg"));
82
+ }
83
+ function canRunCommand(command, args = []) {
84
+ const result = spawnSync(command, args, {
85
+ stdio: "ignore",
86
+ windowsHide: true
87
+ });
88
+ return !result.error && result.status === 0;
89
+ }
90
+ function getUserNpmPrefix() {
91
+ return join(homedir(), ".npm-global");
92
+ }
93
+ function getNpmGlobalBinDir(prefix) {
94
+ return process.platform === "win32" ? prefix : join(prefix, "bin");
95
+ }
96
+ function prependPathEntry(existingPath, entry) {
97
+ const normalizedEntry = entry.trim();
98
+ if (!normalizedEntry) return existingPath;
99
+ const parts = existingPath.split(delimiter).map((value) => value.trim()).filter(Boolean);
100
+ if (parts.includes(normalizedEntry)) {
101
+ return existingPath;
102
+ }
103
+ return existingPath ? `${normalizedEntry}${delimiter}${existingPath}` : normalizedEntry;
104
+ }
105
+ function resolveCodexCommand() {
106
+ const explicit = process.env.CODEXUI_CODEX_COMMAND?.trim();
107
+ const packageCandidates = getPotentialNpmPrefixes().flatMap(getPotentialCodexExecutables);
108
+ const fallbackCandidates = process.platform === "win32" ? [...packageCandidates, "codex"] : ["codex", ...packageCandidates];
109
+ for (const candidate of uniqueStrings([explicit, ...fallbackCandidates])) {
110
+ if (isRunnableCommand(candidate, ["--version"])) {
111
+ return candidate;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ function resolveRipgrepCommand() {
117
+ const explicit = process.env.CODEXUI_RG_COMMAND?.trim();
118
+ const packageCandidates = getPotentialNpmPrefixes().flatMap(getPotentialRipgrepExecutables);
119
+ const fallbackCandidates = process.platform === "win32" ? [...packageCandidates, "rg"] : ["rg", ...packageCandidates];
120
+ for (const candidate of uniqueStrings([explicit, ...fallbackCandidates])) {
121
+ if (isRunnableCommand(candidate, ["--version"])) {
122
+ return candidate;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function resolvePythonCommand() {
128
+ const candidates = process.platform === "win32" ? [
129
+ { command: "python", args: [] },
130
+ { command: "py", args: ["-3"] },
131
+ { command: "python3", args: [] }
132
+ ] : [
133
+ { command: "python3", args: [] },
134
+ { command: "python", args: [] }
135
+ ];
136
+ for (const candidate of candidates) {
137
+ if (isRunnableCommand(candidate.command, [...candidate.args, "--version"])) {
138
+ return candidate;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ function resolveSkillInstallerScriptPath(codexHome) {
144
+ const normalizedCodexHome = codexHome?.trim();
145
+ const candidates = uniqueStrings([
146
+ normalizedCodexHome ? join(normalizedCodexHome, "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
147
+ process.env.CODEX_HOME?.trim() ? join(process.env.CODEX_HOME.trim(), "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
148
+ join(homedir(), ".codex", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
149
+ join(homedir(), ".cursor", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py")
150
+ ]);
151
+ for (const candidate of candidates) {
152
+ if (existsSync(candidate)) {
153
+ return candidate;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
17
159
  // src/server/httpServer.ts
18
160
  import { fileURLToPath } from "url";
19
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
20
- import { existsSync as existsSync2 } from "fs";
161
+ import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute2, join as join5 } from "path";
162
+ import { existsSync as existsSync4 } from "fs";
21
163
  import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
22
164
  import express from "express";
23
165
 
@@ -25,18 +167,21 @@ import express from "express";
25
167
  import { spawn as spawn2 } from "child_process";
26
168
  import { randomBytes } from "crypto";
27
169
  import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
170
+ import { createReadStream } from "fs";
171
+ import { request as httpRequest } from "http";
28
172
  import { request as httpsRequest } from "https";
29
- import { homedir as homedir2 } from "os";
173
+ import { homedir as homedir3 } from "os";
30
174
  import { tmpdir as tmpdir2 } from "os";
31
- import { basename, isAbsolute, join as join2, resolve } from "path";
175
+ import { basename as basename3, dirname, isAbsolute, join as join3, resolve } from "path";
176
+ import { createInterface } from "readline";
32
177
  import { writeFile as writeFile2 } from "fs/promises";
33
178
 
34
179
  // src/server/skillsRoutes.ts
35
180
  import { spawn } from "child_process";
36
181
  import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
37
- import { existsSync } from "fs";
38
- import { homedir, tmpdir } from "os";
39
- import { join } from "path";
182
+ import { existsSync as existsSync2 } from "fs";
183
+ import { homedir as homedir2, tmpdir } from "os";
184
+ import { join as join2 } from "path";
40
185
  import { writeFile } from "fs/promises";
41
186
  function asRecord(value) {
42
187
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -62,28 +207,45 @@ function setJson(res, statusCode, payload) {
62
207
  }
63
208
  function getCodexHomeDir() {
64
209
  const codexHome = process.env.CODEX_HOME?.trim();
65
- return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
210
+ return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
66
211
  }
67
212
  function getSkillsInstallDir() {
68
- return join(getCodexHomeDir(), "skills");
213
+ return join2(getCodexHomeDir(), "skills");
69
214
  }
215
+ var DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
70
216
  async function runCommand(command, args, options = {}) {
217
+ const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
71
218
  await new Promise((resolve3, reject) => {
72
219
  const proc = spawn(command, args, {
73
220
  cwd: options.cwd,
74
221
  env: process.env,
75
222
  stdio: ["ignore", "pipe", "pipe"]
76
223
  });
224
+ let settled = false;
77
225
  let stdout = "";
78
226
  let stderr = "";
227
+ const timer = setTimeout(() => {
228
+ if (settled) return;
229
+ settled = true;
230
+ proc.kill("SIGKILL");
231
+ reject(new Error(`Command timed out after ${timeout}ms (${command} ${args.join(" ")})`));
232
+ }, timeout);
79
233
  proc.stdout.on("data", (chunk) => {
80
234
  stdout += chunk.toString();
81
235
  });
82
236
  proc.stderr.on("data", (chunk) => {
83
237
  stderr += chunk.toString();
84
238
  });
85
- proc.on("error", reject);
239
+ proc.on("error", (err) => {
240
+ if (settled) return;
241
+ settled = true;
242
+ clearTimeout(timer);
243
+ reject(err);
244
+ });
86
245
  proc.on("close", (code) => {
246
+ if (settled) return;
247
+ settled = true;
248
+ clearTimeout(timer);
87
249
  if (code === 0) {
88
250
  resolve3();
89
251
  return;
@@ -95,22 +257,38 @@ async function runCommand(command, args, options = {}) {
95
257
  });
96
258
  }
97
259
  async function runCommandWithOutput(command, args, options = {}) {
260
+ const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
98
261
  return await new Promise((resolve3, reject) => {
99
262
  const proc = spawn(command, args, {
100
263
  cwd: options.cwd,
101
264
  env: process.env,
102
265
  stdio: ["ignore", "pipe", "pipe"]
103
266
  });
267
+ let settled = false;
104
268
  let stdout = "";
105
269
  let stderr = "";
270
+ const timer = setTimeout(() => {
271
+ if (settled) return;
272
+ settled = true;
273
+ proc.kill("SIGKILL");
274
+ reject(new Error(`Command timed out after ${timeout}ms (${command} ${args.join(" ")})`));
275
+ }, timeout);
106
276
  proc.stdout.on("data", (chunk) => {
107
277
  stdout += chunk.toString();
108
278
  });
109
279
  proc.stderr.on("data", (chunk) => {
110
280
  stderr += chunk.toString();
111
281
  });
112
- proc.on("error", reject);
282
+ proc.on("error", (err) => {
283
+ if (settled) return;
284
+ settled = true;
285
+ clearTimeout(timer);
286
+ reject(err);
287
+ });
113
288
  proc.on("close", (code) => {
289
+ if (settled) return;
290
+ settled = true;
291
+ clearTimeout(timer);
114
292
  if (code === 0) {
115
293
  resolve3(stdout.trim());
116
294
  return;
@@ -121,6 +299,21 @@ async function runCommandWithOutput(command, args, options = {}) {
121
299
  });
122
300
  });
123
301
  }
302
+ function withTimeout(promise, ms, label) {
303
+ return new Promise((resolve3, reject) => {
304
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
305
+ promise.then(
306
+ (val) => {
307
+ clearTimeout(timer);
308
+ resolve3(val);
309
+ },
310
+ (err) => {
311
+ clearTimeout(timer);
312
+ reject(err);
313
+ }
314
+ );
315
+ });
316
+ }
124
317
  async function detectUserSkillsDir(appServer) {
125
318
  try {
126
319
  const result = await appServer.rpc("skills/list", {});
@@ -236,13 +429,14 @@ var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
236
429
  var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
237
430
  var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
238
431
  var SYNC_UPSTREAM_SKILLS_REPO = "skills";
432
+ var PRIVATE_SYNC_BRANCH = "main";
239
433
  var HUB_SKILLS_OWNER = "openclaw";
240
434
  var HUB_SKILLS_REPO = "skills";
241
435
  var startupSkillsSyncInitialized = false;
242
436
  var startupSyncStatus = {
243
437
  inProgress: false,
244
438
  mode: "idle",
245
- branch: getPreferredSyncBranch(),
439
+ branch: PRIVATE_SYNC_BRANCH,
246
440
  lastAction: "not-started",
247
441
  lastRunAtIso: "",
248
442
  lastSuccessAtIso: "",
@@ -255,7 +449,7 @@ async function scanInstalledSkillsFromDisk() {
255
449
  const entries = await readdir(skillsDir, { withFileTypes: true });
256
450
  for (const entry of entries) {
257
451
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
258
- const skillMd = join(skillsDir, entry.name, "SKILL.md");
452
+ const skillMd = join2(skillsDir, entry.name, "SKILL.md");
259
453
  try {
260
454
  await stat(skillMd);
261
455
  map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
@@ -266,8 +460,25 @@ async function scanInstalledSkillsFromDisk() {
266
460
  }
267
461
  return map;
268
462
  }
463
+ function extractSkillDescriptionFromMarkdown(markdown) {
464
+ const lines = markdown.split(/\r?\n/);
465
+ let inCodeFence = false;
466
+ for (const rawLine of lines) {
467
+ const line = rawLine.trim();
468
+ if (line.startsWith("```")) {
469
+ inCodeFence = !inCodeFence;
470
+ continue;
471
+ }
472
+ if (inCodeFence || line.length === 0) continue;
473
+ if (line.startsWith("#")) continue;
474
+ if (line.startsWith(">")) continue;
475
+ if (line.startsWith("- ") || line.startsWith("* ")) continue;
476
+ return line;
477
+ }
478
+ return "";
479
+ }
269
480
  function getSkillsSyncStatePath() {
270
- return join(getCodexHomeDir(), "skills-sync.json");
481
+ return join2(getCodexHomeDir(), "skills-sync.json");
271
482
  }
272
483
  async function readSkillsSyncState() {
273
484
  try {
@@ -340,14 +551,14 @@ async function completeGithubDeviceLogin(deviceCode) {
340
551
  }
341
552
  function isAndroidLikeRuntime() {
342
553
  if (process.platform === "android") return true;
343
- if (existsSync("/data/data/com.termux")) return true;
554
+ if (existsSync2("/data/data/com.termux")) return true;
344
555
  if (process.env.TERMUX_VERSION) return true;
345
556
  const prefix = process.env.PREFIX?.toLowerCase() ?? "";
346
557
  if (prefix.includes("/com.termux/")) return true;
347
558
  const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
348
559
  return proot.length > 0;
349
560
  }
350
- function getPreferredSyncBranch() {
561
+ function getPreferredPublicUpstreamBranch() {
351
562
  return isAndroidLikeRuntime() ? "android" : "main";
352
563
  }
353
564
  function isUpstreamSkillsRepo(repoOwner, repoName) {
@@ -402,10 +613,10 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
402
613
  }
403
614
  if (!ready) throw new Error("Private mirror repo was created but is not available yet");
404
615
  if (!created) return;
405
- const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
616
+ const tmp = await mkdtemp(join2(tmpdir(), "codex-skills-seed-"));
406
617
  try {
407
618
  const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
408
- const branch = getPreferredSyncBranch();
619
+ const branch = PRIVATE_SYNC_BRANCH;
409
620
  try {
410
621
  await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
411
622
  } catch {
@@ -476,7 +687,7 @@ function toGitHubTokenRemote(repoOwner, repoName, token) {
476
687
  async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
477
688
  const localDir = getSkillsInstallDir();
478
689
  await mkdir(localDir, { recursive: true });
479
- const gitDir = join(localDir, ".git");
690
+ const gitDir = join2(localDir, ".git");
480
691
  let hasGitDir = false;
481
692
  try {
482
693
  hasGitDir = (await stat(gitDir)).isDirectory();
@@ -522,7 +733,7 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
522
733
  }
523
734
  let pulledMtimes = /* @__PURE__ */ new Map();
524
735
  try {
525
- await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
736
+ await runCommand("git", ["pull", "--no-rebase", "--no-ff", "origin", branch], { cwd: localDir });
526
737
  pulledMtimes = await snapshotFileMtimes(localDir);
527
738
  } catch {
528
739
  await resolveMergeConflictsByNewerCommit(localDir, branch);
@@ -591,7 +802,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
591
802
  for (const entry of entries) {
592
803
  const entryName = String(entry.name);
593
804
  if (entryName === ".git") continue;
594
- const absolutePath = join(currentDir, entryName);
805
+ const absolutePath = join2(currentDir, entryName);
595
806
  const relativePath = absolutePath.slice(rootDir.length + 1);
596
807
  if (entry.isDirectory()) {
597
808
  await walkFileMtimes(rootDir, absolutePath, out);
@@ -606,8 +817,37 @@ async function walkFileMtimes(rootDir, currentDir, out) {
606
817
  }
607
818
  }
608
819
  async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
820
+ function isNonFastForwardPushError(error) {
821
+ const text = getErrorMessage(error, "").toLowerCase();
822
+ return text.includes("non-fast-forward") || text.includes("fetch first") || text.includes("rejected") && text.includes("push");
823
+ }
824
+ async function pushWithNonFastForwardRetry(repoDir2, branch2) {
825
+ const maxAttempts = 3;
826
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
827
+ try {
828
+ await runCommand("git", ["push", "origin", `HEAD:${branch2}`], { cwd: repoDir2 });
829
+ return;
830
+ } catch (error) {
831
+ if (!isNonFastForwardPushError(error) || attempt >= maxAttempts) {
832
+ throw error;
833
+ }
834
+ }
835
+ await runCommand("git", ["fetch", "origin"], { cwd: repoDir2 });
836
+ try {
837
+ await runCommand("git", ["pull", "--no-rebase", "--no-ff", "origin", branch2], { cwd: repoDir2 });
838
+ } catch {
839
+ await resolveMergeConflictsByNewerCommit(repoDir2, branch2);
840
+ }
841
+ await runCommand("git", ["add", "."], { cwd: repoDir2 });
842
+ const statusAfterReconcile = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir2 })).trim();
843
+ if (statusAfterReconcile) {
844
+ await runCommand("git", ["commit", "-m", "Reconcile skills sync before push retry"], { cwd: repoDir2 });
845
+ }
846
+ }
847
+ throw new Error("Failed to push after non-fast-forward retries");
848
+ }
609
849
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
610
- const branch = getPreferredSyncBranch();
850
+ const branch = PRIVATE_SYNC_BRANCH;
611
851
  const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
612
852
  void _installedMap;
613
853
  await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
@@ -616,16 +856,16 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
616
856
  const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
617
857
  if (!status) return;
618
858
  await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
619
- await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
859
+ await pushWithNonFastForwardRetry(repoDir, branch);
620
860
  }
621
861
  async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
622
862
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
623
- const branch = getPreferredSyncBranch();
863
+ const branch = PRIVATE_SYNC_BRANCH;
624
864
  await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
625
865
  }
626
866
  async function bootstrapSkillsFromUpstreamIntoLocal() {
627
867
  const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
628
- const branch = getPreferredSyncBranch();
868
+ const branch = getPreferredPublicUpstreamBranch();
629
869
  await ensureSkillsWorkingTreeRepo(repoUrl, branch);
630
870
  }
631
871
  async function collectLocalSyncedSkills(appServer) {
@@ -685,9 +925,9 @@ async function autoPushSyncedSkills(appServer) {
685
925
  }
686
926
  async function ensureCodexAgentsSymlinkToSkillsAgents() {
687
927
  const codexHomeDir = getCodexHomeDir();
688
- const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
689
- const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
690
- await mkdir(join(codexHomeDir, "skills"), { recursive: true });
928
+ const skillsAgentsPath = join2(codexHomeDir, "skills", "AGENTS.md");
929
+ const codexAgentsPath = join2(codexHomeDir, "AGENTS.md");
930
+ await mkdir(join2(codexHomeDir, "skills"), { recursive: true });
691
931
  let copiedFromCodex = false;
692
932
  try {
693
933
  const codexAgentsStat = await lstat(codexAgentsPath);
@@ -711,7 +951,7 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
711
951
  await writeFile(skillsAgentsPath, "", "utf8");
712
952
  }
713
953
  }
714
- const relativeTarget = join("skills", "AGENTS.md");
954
+ const relativeTarget = join2("skills", "AGENTS.md");
715
955
  try {
716
956
  const current = await lstat(codexAgentsPath);
717
957
  if (current.isSymbolicLink()) {
@@ -729,7 +969,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
729
969
  startupSyncStatus.inProgress = true;
730
970
  startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
731
971
  startupSyncStatus.lastError = "";
732
- startupSyncStatus.branch = getPreferredSyncBranch();
972
+ startupSyncStatus.branch = PRIVATE_SYNC_BRANCH;
733
973
  try {
734
974
  const state = await readSkillsSyncState();
735
975
  if (!state.githubToken) {
@@ -741,6 +981,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
741
981
  return;
742
982
  }
743
983
  startupSyncStatus.mode = "unauthenticated-bootstrap";
984
+ startupSyncStatus.branch = getPreferredPublicUpstreamBranch();
744
985
  startupSyncStatus.lastAction = "pull-upstream";
745
986
  await bootstrapSkillsFromUpstreamIntoLocal();
746
987
  try {
@@ -752,6 +993,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
752
993
  return;
753
994
  }
754
995
  startupSyncStatus.mode = "authenticated-fork-sync";
996
+ startupSyncStatus.branch = PRIVATE_SYNC_BRANCH;
755
997
  startupSyncStatus.lastAction = "ensure-private-fork";
756
998
  const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
757
999
  const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
@@ -991,25 +1233,14 @@ async function handleSkillsRoutes(req, res, url, context) {
991
1233
  }
992
1234
  const localDir = await detectUserSkillsDir(appServer);
993
1235
  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
1236
  const localSkills = await scanInstalledSkillsFromDisk();
996
1237
  for (const skill of remote) {
997
1238
  const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
998
1239
  if (!owner) continue;
999
1240
  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
- ]);
1241
+ continue;
1011
1242
  }
1012
- const skillPath = join(localDir, skill.name);
1243
+ const skillPath = join2(localDir, skill.name);
1013
1244
  await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1014
1245
  }
1015
1246
  const remoteNames = new Set(remote.map((row) => row.name));
@@ -1038,15 +1269,29 @@ async function handleSkillsRoutes(req, res, url, context) {
1038
1269
  try {
1039
1270
  const owner = url.searchParams.get("owner") || "";
1040
1271
  const name = url.searchParams.get("name") || "";
1272
+ const installed = url.searchParams.get("installed") === "true";
1273
+ const skillPath = url.searchParams.get("path") || "";
1041
1274
  if (!owner || !name) {
1042
1275
  setJson(res, 400, { error: "Missing owner or name" });
1043
1276
  return true;
1044
1277
  }
1278
+ if (installed) {
1279
+ const installedMap = await scanInstalledSkillsFromDisk();
1280
+ const installedInfo = installedMap.get(name);
1281
+ const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
1282
+ if (localSkillPath) {
1283
+ const content2 = await readFile(localSkillPath, "utf8");
1284
+ const description2 = extractSkillDescriptionFromMarkdown(content2);
1285
+ setJson(res, 200, { content: content2, description: description2, source: "local" });
1286
+ return true;
1287
+ }
1288
+ }
1045
1289
  const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1046
1290
  const resp = await fetch(rawUrl);
1047
1291
  if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1048
1292
  const content = await resp.text();
1049
- setJson(res, 200, { content });
1293
+ const description = extractSkillDescriptionFromMarkdown(content);
1294
+ setJson(res, 200, { content, description, source: "remote" });
1050
1295
  } catch (error) {
1051
1296
  setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
1052
1297
  }
@@ -1061,9 +1306,25 @@ async function handleSkillsRoutes(req, res, url, context) {
1061
1306
  setJson(res, 400, { error: "Missing owner or name" });
1062
1307
  return true;
1063
1308
  }
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", [
1309
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
1310
+ if (!installerScript) {
1311
+ throw new Error("Skill installer script not found");
1312
+ }
1313
+ const pythonCommand = resolvePythonCommand();
1314
+ if (!pythonCommand) {
1315
+ throw new Error("Python 3 is required to install skills");
1316
+ }
1317
+ const installDest = await withTimeout(
1318
+ detectUserSkillsDir(appServer),
1319
+ 1e4,
1320
+ "detectUserSkillsDir"
1321
+ ).catch(() => getSkillsInstallDir());
1322
+ const skillDir = join2(installDest, name);
1323
+ if (existsSync2(skillDir)) {
1324
+ await rm(skillDir, { recursive: true, force: true });
1325
+ }
1326
+ await runCommand(pythonCommand.command, [
1327
+ ...pythonCommand.args,
1067
1328
  installerScript,
1068
1329
  "--repo",
1069
1330
  `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
@@ -1073,13 +1334,16 @@ async function handleSkillsRoutes(req, res, url, context) {
1073
1334
  installDest,
1074
1335
  "--method",
1075
1336
  "git"
1076
- ]);
1077
- const skillDir = join(installDest, name);
1078
- await ensureInstalledSkillIsValid(appServer, skillDir);
1337
+ ], { timeoutMs: 9e4 });
1338
+ try {
1339
+ await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
1340
+ } catch {
1341
+ }
1079
1342
  const syncState = await readSkillsSyncState();
1080
1343
  const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1081
1344
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1082
- await autoPushSyncedSkills(appServer);
1345
+ autoPushSyncedSkills(appServer).catch(() => {
1346
+ });
1083
1347
  setJson(res, 200, { ok: true, path: skillDir });
1084
1348
  } catch (error) {
1085
1349
  setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
@@ -1091,7 +1355,8 @@ async function handleSkillsRoutes(req, res, url, context) {
1091
1355
  const payload = asRecord(await readJsonBody2(req));
1092
1356
  const name = typeof payload?.name === "string" ? payload.name : "";
1093
1357
  const path = typeof payload?.path === "string" ? payload.path : "";
1094
- const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1358
+ const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
1359
+ const target = normalizedPath || (name ? join2(getSkillsInstallDir(), name) : "");
1095
1360
  if (!target) {
1096
1361
  setJson(res, 400, { error: "Missing name or path" });
1097
1362
  return true;
@@ -1103,9 +1368,10 @@ async function handleSkillsRoutes(req, res, url, context) {
1103
1368
  delete nextOwners[name];
1104
1369
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1105
1370
  }
1106
- await autoPushSyncedSkills(appServer);
1371
+ autoPushSyncedSkills(appServer).catch(() => {
1372
+ });
1107
1373
  try {
1108
- await appServer.rpc("skills/list", { forceReload: true });
1374
+ await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
1109
1375
  } catch {
1110
1376
  }
1111
1377
  setJson(res, 200, { ok: true, deletedPath: target });
@@ -1117,7 +1383,8 @@ async function handleSkillsRoutes(req, res, url, context) {
1117
1383
  return false;
1118
1384
  }
1119
1385
 
1120
- // src/server/codexAppServerBridge.ts
1386
+ // src/server/telegramThreadBridge.ts
1387
+ import { basename } from "path";
1121
1388
  function asRecord2(value) {
1122
1389
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1123
1390
  }
@@ -1135,21 +1402,425 @@ function getErrorMessage2(payload, fallback) {
1135
1402
  }
1136
1403
  return fallback;
1137
1404
  }
1405
+ var TelegramThreadBridge = class {
1406
+ constructor(appServer) {
1407
+ this.threadIdByChatId = /* @__PURE__ */ new Map();
1408
+ this.chatIdsByThreadId = /* @__PURE__ */ new Map();
1409
+ this.lastForwardedTurnByThreadId = /* @__PURE__ */ new Map();
1410
+ this.active = false;
1411
+ this.pollingTask = null;
1412
+ this.nextUpdateOffset = 0;
1413
+ this.lastError = "";
1414
+ this.appServer = appServer;
1415
+ this.token = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
1416
+ this.defaultCwd = process.env.TELEGRAM_DEFAULT_CWD?.trim() ?? process.cwd();
1417
+ }
1418
+ start() {
1419
+ if (!this.token || this.active) return;
1420
+ this.active = true;
1421
+ void this.notifyOnlineForKnownChats().catch(() => {
1422
+ });
1423
+ this.pollingTask = this.pollLoop();
1424
+ this.appServer.onNotification((notification) => {
1425
+ void this.handleNotification(notification).catch(() => {
1426
+ });
1427
+ });
1428
+ }
1429
+ stop() {
1430
+ this.active = false;
1431
+ }
1432
+ async pollLoop() {
1433
+ while (this.active) {
1434
+ try {
1435
+ const updates = await this.getUpdates();
1436
+ this.lastError = "";
1437
+ for (const update of updates) {
1438
+ const updateId = typeof update.update_id === "number" ? update.update_id : -1;
1439
+ if (updateId >= 0) {
1440
+ this.nextUpdateOffset = Math.max(this.nextUpdateOffset, updateId + 1);
1441
+ }
1442
+ await this.handleIncomingUpdate(update);
1443
+ }
1444
+ } catch (error) {
1445
+ this.lastError = getErrorMessage2(error, "Telegram polling failed");
1446
+ await new Promise((resolve3) => setTimeout(resolve3, 1500));
1447
+ }
1448
+ }
1449
+ }
1450
+ async getUpdates() {
1451
+ if (!this.token) {
1452
+ throw new Error("Telegram bot token is not configured");
1453
+ }
1454
+ const response = await fetch(this.apiUrl("getUpdates"), {
1455
+ method: "POST",
1456
+ headers: { "Content-Type": "application/json" },
1457
+ body: JSON.stringify({
1458
+ timeout: 45,
1459
+ offset: this.nextUpdateOffset,
1460
+ allowed_updates: ["message", "callback_query"]
1461
+ })
1462
+ });
1463
+ const payload = asRecord2(await response.json());
1464
+ const result = Array.isArray(payload?.result) ? payload.result : [];
1465
+ return result;
1466
+ }
1467
+ apiUrl(method) {
1468
+ return `https://api.telegram.org/bot${this.token}/${method}`;
1469
+ }
1470
+ configureToken(token) {
1471
+ const normalizedToken = token.trim();
1472
+ if (!normalizedToken) {
1473
+ throw new Error("Telegram bot token is required");
1474
+ }
1475
+ this.token = normalizedToken;
1476
+ }
1477
+ getStatus() {
1478
+ return {
1479
+ configured: this.token.length > 0,
1480
+ active: this.active,
1481
+ mappedChats: this.threadIdByChatId.size,
1482
+ mappedThreads: this.chatIdsByThreadId.size,
1483
+ lastError: this.lastError
1484
+ };
1485
+ }
1486
+ connectThread(threadId, chatId, token) {
1487
+ const normalizedThreadId = threadId.trim();
1488
+ if (!normalizedThreadId) {
1489
+ throw new Error("threadId is required");
1490
+ }
1491
+ if (!Number.isFinite(chatId)) {
1492
+ throw new Error("chatId must be a number");
1493
+ }
1494
+ if (typeof token === "string" && token.trim().length > 0) {
1495
+ this.configureToken(token);
1496
+ }
1497
+ if (!this.token) {
1498
+ throw new Error("Telegram bot token is not configured");
1499
+ }
1500
+ this.bindChatToThread(chatId, normalizedThreadId);
1501
+ this.start();
1502
+ void this.sendOnlineMessage(chatId).catch(() => {
1503
+ });
1504
+ }
1505
+ async sendTelegramMessage(chatId, text, options = {}) {
1506
+ const message = text.trim();
1507
+ if (!message) return;
1508
+ const payload = { chat_id: chatId, text: message };
1509
+ if (options.replyMarkup) {
1510
+ payload.reply_markup = options.replyMarkup;
1511
+ }
1512
+ await fetch(this.apiUrl("sendMessage"), {
1513
+ method: "POST",
1514
+ headers: { "Content-Type": "application/json" },
1515
+ body: JSON.stringify(payload)
1516
+ });
1517
+ }
1518
+ async sendOnlineMessage(chatId) {
1519
+ await this.sendTelegramMessage(chatId, "Codex thread bridge went online.");
1520
+ }
1521
+ async notifyOnlineForKnownChats() {
1522
+ const knownChatIds = Array.from(this.threadIdByChatId.keys());
1523
+ for (const chatId of knownChatIds) {
1524
+ await this.sendOnlineMessage(chatId);
1525
+ }
1526
+ }
1527
+ async handleIncomingUpdate(update) {
1528
+ if (update.callback_query) {
1529
+ await this.handleCallbackQuery(update.callback_query);
1530
+ return;
1531
+ }
1532
+ const message = update.message;
1533
+ const chatId = message?.chat?.id;
1534
+ const text = message?.text?.trim();
1535
+ if (typeof chatId !== "number" || !text) return;
1536
+ if (text === "/start") {
1537
+ await this.sendThreadPicker(chatId);
1538
+ return;
1539
+ }
1540
+ if (text === "/newthread") {
1541
+ const threadId2 = await this.createThreadForChat(chatId);
1542
+ await this.sendTelegramMessage(chatId, `Mapped to new thread: ${threadId2}`);
1543
+ return;
1544
+ }
1545
+ const threadCommand = text.match(/^\/thread\s+(\S+)$/);
1546
+ if (threadCommand) {
1547
+ const threadId2 = threadCommand[1];
1548
+ this.bindChatToThread(chatId, threadId2);
1549
+ await this.sendTelegramMessage(chatId, `Mapped to thread: ${threadId2}`);
1550
+ return;
1551
+ }
1552
+ const threadId = await this.ensureThreadForChat(chatId);
1553
+ try {
1554
+ await this.appServer.rpc("turn/start", {
1555
+ threadId,
1556
+ input: [{ type: "text", text }]
1557
+ });
1558
+ } catch (error) {
1559
+ const message2 = getErrorMessage2(error, "Failed to forward message to thread");
1560
+ await this.sendTelegramMessage(chatId, `Forward failed: ${message2}`);
1561
+ }
1562
+ }
1563
+ async handleCallbackQuery(callbackQuery) {
1564
+ const callbackId = typeof callbackQuery.id === "string" ? callbackQuery.id : "";
1565
+ const data = typeof callbackQuery.data === "string" ? callbackQuery.data : "";
1566
+ const chatId = callbackQuery.message?.chat?.id;
1567
+ if (!callbackId) return;
1568
+ if (!data.startsWith("thread:") || typeof chatId !== "number") {
1569
+ await this.answerCallbackQuery(callbackId, "Invalid selection");
1570
+ return;
1571
+ }
1572
+ const threadId = data.slice("thread:".length).trim();
1573
+ if (!threadId) {
1574
+ await this.answerCallbackQuery(callbackId, "Invalid thread id");
1575
+ return;
1576
+ }
1577
+ this.bindChatToThread(chatId, threadId);
1578
+ await this.answerCallbackQuery(callbackId, "Thread connected");
1579
+ await this.sendTelegramMessage(chatId, `Connected to thread: ${threadId}`);
1580
+ const history = await this.readThreadHistorySummary(threadId);
1581
+ if (history) {
1582
+ await this.sendTelegramMessage(chatId, history);
1583
+ }
1584
+ }
1585
+ async answerCallbackQuery(callbackQueryId, text) {
1586
+ await fetch(this.apiUrl("answerCallbackQuery"), {
1587
+ method: "POST",
1588
+ headers: { "Content-Type": "application/json" },
1589
+ body: JSON.stringify({
1590
+ callback_query_id: callbackQueryId,
1591
+ text
1592
+ })
1593
+ });
1594
+ }
1595
+ async sendThreadPicker(chatId) {
1596
+ const threads = await this.listRecentThreads();
1597
+ if (threads.length === 0) {
1598
+ await this.sendTelegramMessage(chatId, "No threads found. Send /newthread to create one.");
1599
+ return;
1600
+ }
1601
+ const inlineKeyboard = threads.map((thread) => [
1602
+ {
1603
+ text: thread.title,
1604
+ callback_data: `thread:${thread.id}`
1605
+ }
1606
+ ]);
1607
+ await this.sendTelegramMessage(chatId, "Select a thread to connect:", {
1608
+ replyMarkup: { inline_keyboard: inlineKeyboard }
1609
+ });
1610
+ }
1611
+ async listRecentThreads() {
1612
+ const payload = asRecord2(await this.appServer.rpc("thread/list", {
1613
+ archived: false,
1614
+ limit: 20,
1615
+ sortKey: "updated_at"
1616
+ }));
1617
+ const rows = Array.isArray(payload?.data) ? payload.data : [];
1618
+ const threads = [];
1619
+ for (const row of rows) {
1620
+ const record = asRecord2(row);
1621
+ const id = typeof record?.id === "string" ? record.id.trim() : "";
1622
+ if (!id) continue;
1623
+ const name = typeof record?.name === "string" ? record.name.trim() : "";
1624
+ const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
1625
+ const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
1626
+ const projectName = cwd ? basename(cwd) : "project";
1627
+ const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
1628
+ const title = `${projectName}/${threadTitle}`.slice(0, 64);
1629
+ threads.push({ id, title });
1630
+ }
1631
+ return threads;
1632
+ }
1633
+ async createThreadForChat(chatId) {
1634
+ const response = asRecord2(await this.appServer.rpc("thread/start", { cwd: this.defaultCwd }));
1635
+ const thread = asRecord2(response?.thread);
1636
+ const threadId = typeof thread?.id === "string" ? thread.id : "";
1637
+ if (!threadId) {
1638
+ throw new Error("thread/start did not return thread id");
1639
+ }
1640
+ this.bindChatToThread(chatId, threadId);
1641
+ return threadId;
1642
+ }
1643
+ async ensureThreadForChat(chatId) {
1644
+ const existing = this.threadIdByChatId.get(chatId);
1645
+ if (existing) return existing;
1646
+ return this.createThreadForChat(chatId);
1647
+ }
1648
+ bindChatToThread(chatId, threadId) {
1649
+ const previousThreadId = this.threadIdByChatId.get(chatId);
1650
+ if (previousThreadId && previousThreadId !== threadId) {
1651
+ const previousSet = this.chatIdsByThreadId.get(previousThreadId);
1652
+ previousSet?.delete(chatId);
1653
+ if (previousSet && previousSet.size === 0) {
1654
+ this.chatIdsByThreadId.delete(previousThreadId);
1655
+ }
1656
+ }
1657
+ this.threadIdByChatId.set(chatId, threadId);
1658
+ const chatIds = this.chatIdsByThreadId.get(threadId) ?? /* @__PURE__ */ new Set();
1659
+ chatIds.add(chatId);
1660
+ this.chatIdsByThreadId.set(threadId, chatIds);
1661
+ }
1662
+ extractThreadId(notification) {
1663
+ const params = asRecord2(notification.params);
1664
+ if (!params) return "";
1665
+ const directThreadId = typeof params.threadId === "string" ? params.threadId : "";
1666
+ if (directThreadId) return directThreadId;
1667
+ const turn = asRecord2(params.turn);
1668
+ const turnThreadId = typeof turn?.threadId === "string" ? turn.threadId : "";
1669
+ return turnThreadId;
1670
+ }
1671
+ extractTurnId(notification) {
1672
+ const params = asRecord2(notification.params);
1673
+ if (!params) return "";
1674
+ const directTurnId = typeof params.turnId === "string" ? params.turnId : "";
1675
+ if (directTurnId) return directTurnId;
1676
+ const turn = asRecord2(params.turn);
1677
+ const turnId = typeof turn?.id === "string" ? turn.id : "";
1678
+ return turnId;
1679
+ }
1680
+ async handleNotification(notification) {
1681
+ if (notification.method !== "turn/completed") return;
1682
+ const threadId = this.extractThreadId(notification);
1683
+ if (!threadId) return;
1684
+ const chatIds = this.chatIdsByThreadId.get(threadId);
1685
+ if (!chatIds || chatIds.size === 0) return;
1686
+ const turnId = this.extractTurnId(notification);
1687
+ const lastForwardedTurnId = this.lastForwardedTurnByThreadId.get(threadId);
1688
+ if (turnId && lastForwardedTurnId === turnId) return;
1689
+ const assistantReply = await this.readLatestAssistantMessage(threadId);
1690
+ if (!assistantReply) return;
1691
+ for (const chatId of chatIds) {
1692
+ await this.sendTelegramMessage(chatId, assistantReply);
1693
+ }
1694
+ if (turnId) {
1695
+ this.lastForwardedTurnByThreadId.set(threadId, turnId);
1696
+ }
1697
+ }
1698
+ async readLatestAssistantMessage(threadId) {
1699
+ const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
1700
+ const thread = asRecord2(response?.thread);
1701
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1702
+ for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
1703
+ const turn = asRecord2(turns[turnIndex]);
1704
+ const items = Array.isArray(turn?.items) ? turn.items : [];
1705
+ for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex -= 1) {
1706
+ const item = asRecord2(items[itemIndex]);
1707
+ if (item?.type === "agentMessage") {
1708
+ const text = typeof item.text === "string" ? item.text.trim() : "";
1709
+ if (text) return text;
1710
+ }
1711
+ }
1712
+ }
1713
+ return "";
1714
+ }
1715
+ async readThreadHistorySummary(threadId) {
1716
+ const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
1717
+ const thread = asRecord2(response?.thread);
1718
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1719
+ const historyRows = [];
1720
+ for (const turn of turns) {
1721
+ const turnRecord = asRecord2(turn);
1722
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1723
+ for (const item of items) {
1724
+ const itemRecord = asRecord2(item);
1725
+ const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1726
+ if (type === "userMessage") {
1727
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1728
+ for (const block of content) {
1729
+ const blockRecord = asRecord2(block);
1730
+ if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim()) {
1731
+ historyRows.push(`User: ${blockRecord.text.trim()}`);
1732
+ }
1733
+ }
1734
+ }
1735
+ if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim()) {
1736
+ historyRows.push(`Assistant: ${itemRecord.text.trim()}`);
1737
+ }
1738
+ }
1739
+ }
1740
+ if (historyRows.length === 0) {
1741
+ return "Thread has no message history yet.";
1742
+ }
1743
+ const tail = historyRows.slice(-12).join("\n\n");
1744
+ const maxLen = 3800;
1745
+ const summary = tail.length > maxLen ? tail.slice(tail.length - maxLen) : tail;
1746
+ return `Recent history:
1747
+
1748
+ ${summary}`;
1749
+ }
1750
+ };
1751
+
1752
+ // src/utils/commandInvocation.ts
1753
+ import { spawnSync as spawnSync2 } from "child_process";
1754
+ import { basename as basename2, extname } from "path";
1755
+ var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
1756
+ function quoteCmdExeArg(value) {
1757
+ const normalized = value.replace(/"/g, '""');
1758
+ if (!/[\s"]/u.test(normalized)) {
1759
+ return normalized;
1760
+ }
1761
+ return `"${normalized}"`;
1762
+ }
1763
+ function needsCmdExeWrapper(command) {
1764
+ if (process.platform !== "win32") {
1765
+ return false;
1766
+ }
1767
+ const lowerCommand = command.toLowerCase();
1768
+ const baseName = basename2(lowerCommand);
1769
+ if (/\.(cmd|bat)$/i.test(baseName)) {
1770
+ return true;
1771
+ }
1772
+ if (extname(baseName)) {
1773
+ return false;
1774
+ }
1775
+ return WINDOWS_CMD_NAMES.has(baseName);
1776
+ }
1777
+ function getSpawnInvocation(command, args = []) {
1778
+ if (needsCmdExeWrapper(command)) {
1779
+ return {
1780
+ command: "cmd.exe",
1781
+ args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
1782
+ };
1783
+ }
1784
+ return { command, args };
1785
+ }
1786
+ function spawnSyncCommand(command, args = [], options = {}) {
1787
+ const invocation = getSpawnInvocation(command, args);
1788
+ return spawnSync2(invocation.command, invocation.args, options);
1789
+ }
1790
+
1791
+ // src/server/codexAppServerBridge.ts
1792
+ function asRecord3(value) {
1793
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1794
+ }
1795
+ function getErrorMessage3(payload, fallback) {
1796
+ if (payload instanceof Error && payload.message.trim().length > 0) {
1797
+ return payload.message;
1798
+ }
1799
+ const record = asRecord3(payload);
1800
+ if (!record) return fallback;
1801
+ const error = record.error;
1802
+ if (typeof error === "string" && error.length > 0) return error;
1803
+ const nestedError = asRecord3(error);
1804
+ if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
1805
+ return nestedError.message;
1806
+ }
1807
+ return fallback;
1808
+ }
1138
1809
  function setJson2(res, statusCode, payload) {
1139
1810
  res.statusCode = statusCode;
1140
1811
  res.setHeader("Content-Type", "application/json; charset=utf-8");
1141
1812
  res.end(JSON.stringify(payload));
1142
1813
  }
1143
1814
  function extractThreadMessageText(threadReadPayload) {
1144
- const payload = asRecord2(threadReadPayload);
1145
- const thread = asRecord2(payload?.thread);
1815
+ const payload = asRecord3(threadReadPayload);
1816
+ const thread = asRecord3(payload?.thread);
1146
1817
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1147
1818
  const parts = [];
1148
1819
  for (const turn of turns) {
1149
- const turnRecord = asRecord2(turn);
1820
+ const turnRecord = asRecord3(turn);
1150
1821
  const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1151
1822
  for (const item of items) {
1152
- const itemRecord = asRecord2(item);
1823
+ const itemRecord = asRecord3(item);
1153
1824
  const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1154
1825
  if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
1155
1826
  parts.push(itemRecord.text.trim());
@@ -1158,7 +1829,7 @@ function extractThreadMessageText(threadReadPayload) {
1158
1829
  if (type === "userMessage") {
1159
1830
  const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1160
1831
  for (const block of content) {
1161
- const blockRecord = asRecord2(block);
1832
+ const blockRecord = asRecord3(block);
1162
1833
  if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
1163
1834
  parts.push(blockRecord.text.trim());
1164
1835
  }
@@ -1192,9 +1863,62 @@ function scoreFileCandidate(path, query) {
1192
1863
  if (lowerPath.includes(lowerQuery)) return 4;
1193
1864
  return 10;
1194
1865
  }
1866
+ function decodeHtmlEntities(value) {
1867
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/gi, "/");
1868
+ }
1869
+ function stripHtml(value) {
1870
+ return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
1871
+ }
1872
+ function parseGithubTrendingHtml(html, limit) {
1873
+ const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
1874
+ const items = [];
1875
+ let seq = Date.now();
1876
+ for (const row of rows) {
1877
+ const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
1878
+ const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
1879
+ if (!hrefMatch) continue;
1880
+ const fullName = hrefMatch[1] ?? "";
1881
+ if (!fullName || items.some((item) => item.fullName === fullName)) continue;
1882
+ const descriptionMatch = row.match(/<p[^>]*class="[^"]*col-9[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*>([\s\S]*?)<\/p>/);
1883
+ const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
1884
+ const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
1885
+ const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
1886
+ const stars = Number.parseInt(starsText, 10);
1887
+ items.push({
1888
+ id: seq,
1889
+ fullName,
1890
+ url: `https://github.com/${fullName}`,
1891
+ description: stripHtml(descriptionMatch?.[1] ?? ""),
1892
+ language: stripHtml(languageMatch?.[1] ?? ""),
1893
+ stars: Number.isFinite(stars) ? stars : 0
1894
+ });
1895
+ seq += 1;
1896
+ if (items.length >= limit) break;
1897
+ }
1898
+ return items;
1899
+ }
1900
+ async function fetchGithubTrending(since, limit) {
1901
+ const endpoint = `https://github.com/trending?since=${since}`;
1902
+ const response = await fetch(endpoint, {
1903
+ headers: {
1904
+ "User-Agent": "codex-web-local",
1905
+ Accept: "text/html"
1906
+ }
1907
+ });
1908
+ if (!response.ok) {
1909
+ throw new Error(`GitHub trending fetch failed (${response.status})`);
1910
+ }
1911
+ const html = await response.text();
1912
+ return parseGithubTrendingHtml(html, limit);
1913
+ }
1195
1914
  async function listFilesWithRipgrep(cwd) {
1196
1915
  return await new Promise((resolve3, reject) => {
1197
- const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1916
+ const ripgrepCommand = resolveRipgrepCommand();
1917
+ if (!ripgrepCommand) {
1918
+ reject(new Error("ripgrep (rg) is not available"));
1919
+ return;
1920
+ }
1921
+ const proc = spawn2(ripgrepCommand, ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1198
1922
  cwd,
1199
1923
  env: process.env,
1200
1924
  stdio: ["ignore", "pipe", "pipe"]
@@ -1221,7 +1945,7 @@ async function listFilesWithRipgrep(cwd) {
1221
1945
  }
1222
1946
  function getCodexHomeDir2() {
1223
1947
  const codexHome = process.env.CODEX_HOME?.trim();
1224
- return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1948
+ return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
1225
1949
  }
1226
1950
  async function runCommand2(command, args, options = {}) {
1227
1951
  await new Promise((resolve3, reject) => {
@@ -1251,15 +1975,15 @@ async function runCommand2(command, args, options = {}) {
1251
1975
  });
1252
1976
  }
1253
1977
  function isMissingHeadError(error) {
1254
- const message = getErrorMessage2(error, "").toLowerCase();
1978
+ const message = getErrorMessage3(error, "").toLowerCase();
1255
1979
  return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
1256
1980
  }
1257
1981
  function isNotGitRepositoryError(error) {
1258
- const message = getErrorMessage2(error, "").toLowerCase();
1982
+ const message = getErrorMessage3(error, "").toLowerCase();
1259
1983
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
1260
1984
  }
1261
1985
  async function ensureRepoHasInitialCommit(repoRoot) {
1262
- const agentsPath = join2(repoRoot, "AGENTS.md");
1986
+ const agentsPath = join3(repoRoot, "AGENTS.md");
1263
1987
  try {
1264
1988
  await stat2(agentsPath);
1265
1989
  } catch {
@@ -1299,6 +2023,33 @@ async function runCommandCapture(command, args, options = {}) {
1299
2023
  });
1300
2024
  });
1301
2025
  }
2026
+ async function runCommandWithOutput2(command, args, options = {}) {
2027
+ return await new Promise((resolve3, reject) => {
2028
+ const proc = spawn2(command, args, {
2029
+ cwd: options.cwd,
2030
+ env: process.env,
2031
+ stdio: ["ignore", "pipe", "pipe"]
2032
+ });
2033
+ let stdout = "";
2034
+ let stderr = "";
2035
+ proc.stdout.on("data", (chunk) => {
2036
+ stdout += chunk.toString();
2037
+ });
2038
+ proc.stderr.on("data", (chunk) => {
2039
+ stderr += chunk.toString();
2040
+ });
2041
+ proc.on("error", reject);
2042
+ proc.on("close", (code) => {
2043
+ if (code === 0) {
2044
+ resolve3(stdout.trim());
2045
+ return;
2046
+ }
2047
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
2048
+ const suffix = details.length > 0 ? `: ${details}` : "";
2049
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
2050
+ });
2051
+ });
2052
+ }
1302
2053
  function normalizeStringArray(value) {
1303
2054
  if (!Array.isArray(value)) return [];
1304
2055
  const normalized = [];
@@ -1319,8 +2070,88 @@ function normalizeStringRecord(value) {
1319
2070
  }
1320
2071
  return next;
1321
2072
  }
2073
+ function normalizeCommitMessage(value) {
2074
+ if (typeof value !== "string") return "";
2075
+ const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
2076
+ return normalized.slice(0, 2e3);
2077
+ }
2078
+ function getRollbackGitDirForCwd(cwd) {
2079
+ return join3(cwd, ".codex", "rollbacks", ".git");
2080
+ }
2081
+ async function ensureLocalCodexGitignoreHasRollbacks(cwd) {
2082
+ const localCodexDir = join3(cwd, ".codex");
2083
+ const gitignorePath = join3(localCodexDir, ".gitignore");
2084
+ await mkdir2(localCodexDir, { recursive: true });
2085
+ let current = "";
2086
+ try {
2087
+ current = await readFile2(gitignorePath, "utf8");
2088
+ } catch {
2089
+ current = "";
2090
+ }
2091
+ const rows = current.split(/\r?\n/).map((line) => line.trim());
2092
+ if (rows.includes("rollbacks/")) return;
2093
+ const prefix = current.length > 0 && !current.endsWith("\n") ? `${current}
2094
+ ` : current;
2095
+ await writeFile2(gitignorePath, `${prefix}rollbacks/
2096
+ `, "utf8");
2097
+ }
2098
+ async function ensureRollbackGitRepo(cwd) {
2099
+ const gitDir = getRollbackGitDirForCwd(cwd);
2100
+ try {
2101
+ const headInfo = await stat2(join3(gitDir, "HEAD"));
2102
+ if (!headInfo.isFile()) {
2103
+ throw new Error("Invalid rollback git repository");
2104
+ }
2105
+ } catch {
2106
+ await mkdir2(dirname(gitDir), { recursive: true });
2107
+ await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, "init"]);
2108
+ }
2109
+ await runCommand2("git", ["--git-dir", gitDir, "config", "user.email", "codex@local"]);
2110
+ await runCommand2("git", ["--git-dir", gitDir, "config", "user.name", "Codex Rollback"]);
2111
+ try {
2112
+ await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, "rev-parse", "--verify", "HEAD"]);
2113
+ } catch {
2114
+ await runCommand2(
2115
+ "git",
2116
+ ["--git-dir", gitDir, "--work-tree", cwd, "commit", "--allow-empty", "-m", "Initialize rollback history"]
2117
+ );
2118
+ }
2119
+ await ensureLocalCodexGitignoreHasRollbacks(cwd);
2120
+ return gitDir;
2121
+ }
2122
+ async function runRollbackGit(cwd, args) {
2123
+ const gitDir = await ensureRollbackGitRepo(cwd);
2124
+ await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2125
+ }
2126
+ async function runRollbackGitCapture(cwd, args) {
2127
+ const gitDir = await ensureRollbackGitRepo(cwd);
2128
+ return await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2129
+ }
2130
+ async function runRollbackGitWithOutput(cwd, args) {
2131
+ const gitDir = await ensureRollbackGitRepo(cwd);
2132
+ return await runCommandWithOutput2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2133
+ }
2134
+ async function hasRollbackGitWorkingTreeChanges(cwd) {
2135
+ const status = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
2136
+ return status.trim().length > 0;
2137
+ }
2138
+ async function findRollbackCommitByExactMessage(cwd, message) {
2139
+ const normalizedTarget = normalizeCommitMessage(message);
2140
+ if (!normalizedTarget) return "";
2141
+ const raw = await runRollbackGitWithOutput(cwd, ["log", "--format=%H%x1f%B%x1e"]);
2142
+ const entries = raw.split("");
2143
+ for (const entry of entries) {
2144
+ if (!entry.trim()) continue;
2145
+ const [shaRaw, bodyRaw] = entry.split("");
2146
+ const sha = (shaRaw ?? "").trim();
2147
+ const body = normalizeCommitMessage(bodyRaw ?? "");
2148
+ if (!sha) continue;
2149
+ if (body === normalizedTarget) return sha;
2150
+ }
2151
+ return "";
2152
+ }
1322
2153
  function getCodexAuthPath() {
1323
- return join2(getCodexHomeDir2(), "auth.json");
2154
+ return join3(getCodexHomeDir2(), "auth.json");
1324
2155
  }
1325
2156
  async function readCodexAuth() {
1326
2157
  try {
@@ -1334,13 +2165,21 @@ async function readCodexAuth() {
1334
2165
  }
1335
2166
  }
1336
2167
  function getCodexGlobalStatePath() {
1337
- return join2(getCodexHomeDir2(), ".codex-global-state.json");
2168
+ return join3(getCodexHomeDir2(), ".codex-global-state.json");
2169
+ }
2170
+ function getCodexSessionIndexPath() {
2171
+ return join3(getCodexHomeDir2(), "session_index.jsonl");
1338
2172
  }
1339
2173
  var MAX_THREAD_TITLES = 500;
2174
+ var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
2175
+ var sessionIndexThreadTitleCacheState = {
2176
+ fileSignature: null,
2177
+ cache: EMPTY_THREAD_TITLE_CACHE
2178
+ };
1340
2179
  function normalizeThreadTitleCache(value) {
1341
- const record = asRecord2(value);
1342
- if (!record) return { titles: {}, order: [] };
1343
- const rawTitles = asRecord2(record.titles);
2180
+ const record = asRecord3(value);
2181
+ if (!record) return EMPTY_THREAD_TITLE_CACHE;
2182
+ const rawTitles = asRecord3(record.titles);
1344
2183
  const titles = {};
1345
2184
  if (rawTitles) {
1346
2185
  for (const [k, v] of Object.entries(rawTitles)) {
@@ -1363,14 +2202,55 @@ function removeFromThreadTitleCache(cache, id) {
1363
2202
  const { [id]: _, ...titles } = cache.titles;
1364
2203
  return { titles, order: cache.order.filter((o) => o !== id) };
1365
2204
  }
2205
+ function normalizeSessionIndexThreadTitle(value) {
2206
+ const record = asRecord3(value);
2207
+ if (!record) return null;
2208
+ const id = typeof record.id === "string" ? record.id.trim() : "";
2209
+ const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
2210
+ const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
2211
+ const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
2212
+ if (!id || !title) return null;
2213
+ return {
2214
+ id,
2215
+ title,
2216
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
2217
+ };
2218
+ }
2219
+ function trimThreadTitleCache(cache) {
2220
+ const titles = { ...cache.titles };
2221
+ const order = cache.order.filter((id) => {
2222
+ if (!titles[id]) return false;
2223
+ return true;
2224
+ }).slice(0, MAX_THREAD_TITLES);
2225
+ for (const id of Object.keys(titles)) {
2226
+ if (!order.includes(id)) {
2227
+ delete titles[id];
2228
+ }
2229
+ }
2230
+ return { titles, order };
2231
+ }
2232
+ function mergeThreadTitleCaches(base, overlay) {
2233
+ const titles = { ...base.titles, ...overlay.titles };
2234
+ const order = [];
2235
+ for (const id of [...overlay.order, ...base.order]) {
2236
+ if (!titles[id] || order.includes(id)) continue;
2237
+ order.push(id);
2238
+ }
2239
+ for (const id of Object.keys(titles)) {
2240
+ if (!order.includes(id)) {
2241
+ order.push(id);
2242
+ }
2243
+ }
2244
+ return trimThreadTitleCache({ titles, order });
2245
+ }
1366
2246
  async function readThreadTitleCache() {
1367
2247
  const statePath = getCodexGlobalStatePath();
1368
2248
  try {
1369
2249
  const raw = await readFile2(statePath, "utf8");
1370
- const payload = asRecord2(JSON.parse(raw)) ?? {};
2250
+ const payload = asRecord3(JSON.parse(raw)) ?? {};
1371
2251
  return normalizeThreadTitleCache(payload["thread-titles"]);
1372
2252
  } catch {
1373
- return { titles: {}, order: [] };
2253
+ return EMPTY_THREAD_TITLE_CACHE;
1374
2254
  }
1375
2255
  }
1376
2256
  async function writeThreadTitleCache(cache) {
@@ -1378,20 +2258,83 @@ async function writeThreadTitleCache(cache) {
1378
2258
  let payload = {};
1379
2259
  try {
1380
2260
  const raw = await readFile2(statePath, "utf8");
1381
- payload = asRecord2(JSON.parse(raw)) ?? {};
2261
+ payload = asRecord3(JSON.parse(raw)) ?? {};
1382
2262
  } catch {
1383
2263
  payload = {};
1384
2264
  }
1385
2265
  payload["thread-titles"] = cache;
1386
2266
  await writeFile2(statePath, JSON.stringify(payload), "utf8");
1387
2267
  }
2268
+ function getSessionIndexFileSignature(stats) {
2269
+ return `${String(stats.mtimeMs)}:${String(stats.size)}`;
2270
+ }
2271
+ async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
2272
+ const latestById = /* @__PURE__ */ new Map();
2273
+ const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
2274
+ const lines = createInterface({
2275
+ input,
2276
+ crlfDelay: Infinity
2277
+ });
2278
+ try {
2279
+ for await (const line of lines) {
2280
+ const trimmed = line.trim();
2281
+ if (!trimmed) continue;
2282
+ try {
2283
+ const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
2284
+ if (!entry) continue;
2285
+ const previous = latestById.get(entry.id);
2286
+ if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
2287
+ latestById.set(entry.id, entry);
2288
+ }
2289
+ } catch {
2290
+ }
2291
+ }
2292
+ } finally {
2293
+ lines.close();
2294
+ input.close();
2295
+ }
2296
+ const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
2297
+ const titles = {};
2298
+ const order = [];
2299
+ for (const entry of entries) {
2300
+ titles[entry.id] = entry.title;
2301
+ order.push(entry.id);
2302
+ }
2303
+ return trimThreadTitleCache({ titles, order });
2304
+ }
2305
+ async function readThreadTitlesFromSessionIndex() {
2306
+ const sessionIndexPath = getCodexSessionIndexPath();
2307
+ try {
2308
+ const stats = await stat2(sessionIndexPath);
2309
+ const fileSignature = getSessionIndexFileSignature(stats);
2310
+ if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
2311
+ return sessionIndexThreadTitleCacheState.cache;
2312
+ }
2313
+ const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
2314
+ sessionIndexThreadTitleCacheState = { fileSignature, cache };
2315
+ return cache;
2316
+ } catch {
2317
+ sessionIndexThreadTitleCacheState = {
2318
+ fileSignature: "missing",
2319
+ cache: EMPTY_THREAD_TITLE_CACHE
2320
+ };
2321
+ return sessionIndexThreadTitleCacheState.cache;
2322
+ }
2323
+ }
2324
+ async function readMergedThreadTitleCache() {
2325
+ const [sessionIndexCache, persistedCache] = await Promise.all([
2326
+ readThreadTitlesFromSessionIndex(),
2327
+ readThreadTitleCache()
2328
+ ]);
2329
+ return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
2330
+ }
1388
2331
  async function readWorkspaceRootsState() {
1389
2332
  const statePath = getCodexGlobalStatePath();
1390
2333
  let payload = {};
1391
2334
  try {
1392
2335
  const raw = await readFile2(statePath, "utf8");
1393
2336
  const parsed = JSON.parse(raw);
1394
- payload = asRecord2(parsed) ?? {};
2337
+ payload = asRecord3(parsed) ?? {};
1395
2338
  } catch {
1396
2339
  payload = {};
1397
2340
  }
@@ -1406,7 +2349,7 @@ async function writeWorkspaceRootsState(nextState) {
1406
2349
  let payload = {};
1407
2350
  try {
1408
2351
  const raw = await readFile2(statePath, "utf8");
1409
- payload = asRecord2(JSON.parse(raw)) ?? {};
2352
+ payload = asRecord3(JSON.parse(raw)) ?? {};
1410
2353
  } catch {
1411
2354
  payload = {};
1412
2355
  }
@@ -1415,6 +2358,36 @@ async function writeWorkspaceRootsState(nextState) {
1415
2358
  payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
1416
2359
  await writeFile2(statePath, JSON.stringify(payload), "utf8");
1417
2360
  }
2361
+ function normalizeTelegramBridgeConfig(value) {
2362
+ const record = asRecord3(value);
2363
+ if (!record) return { botToken: "" };
2364
+ const botToken = typeof record.botToken === "string" ? record.botToken.trim() : "";
2365
+ return { botToken };
2366
+ }
2367
+ async function readTelegramBridgeConfig() {
2368
+ const statePath = getCodexGlobalStatePath();
2369
+ try {
2370
+ const raw = await readFile2(statePath, "utf8");
2371
+ const payload = asRecord3(JSON.parse(raw)) ?? {};
2372
+ return normalizeTelegramBridgeConfig(payload["telegram-bridge"]);
2373
+ } catch {
2374
+ return { botToken: "" };
2375
+ }
2376
+ }
2377
+ async function writeTelegramBridgeConfig(nextState) {
2378
+ const statePath = getCodexGlobalStatePath();
2379
+ let payload = {};
2380
+ try {
2381
+ const raw = await readFile2(statePath, "utf8");
2382
+ payload = asRecord3(JSON.parse(raw)) ?? {};
2383
+ } catch {
2384
+ payload = {};
2385
+ }
2386
+ payload["telegram-bridge"] = {
2387
+ botToken: nextState.botToken.trim()
2388
+ };
2389
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
2390
+ }
1418
2391
  async function readJsonBody(req) {
1419
2392
  const raw = await readRawBody(req);
1420
2393
  if (raw.length === 0) return null;
@@ -1484,46 +2457,93 @@ function handleFileUpload(req, res) {
1484
2457
  setJson2(res, 400, { error: "No file in request" });
1485
2458
  return;
1486
2459
  }
1487
- const uploadDir = join2(tmpdir2(), "codex-web-uploads");
2460
+ const uploadDir = join3(tmpdir2(), "codex-web-uploads");
1488
2461
  await mkdir2(uploadDir, { recursive: true });
1489
- const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1490
- const destPath = join2(destDir, fileName);
2462
+ const destDir = await mkdtemp2(join3(uploadDir, "f-"));
2463
+ const destPath = join3(destDir, fileName);
1491
2464
  await writeFile2(destPath, fileData);
1492
2465
  setJson2(res, 200, { path: destPath });
1493
2466
  } catch (err) {
1494
- setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
2467
+ setJson2(res, 500, { error: getErrorMessage3(err, "Upload failed") });
1495
2468
  }
1496
2469
  });
1497
2470
  req.on("error", (err) => {
1498
- setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
2471
+ setJson2(res, 500, { error: getErrorMessage3(err, "Upload stream error") });
2472
+ });
2473
+ }
2474
+ function httpPost(url, headers, body) {
2475
+ const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
2476
+ return new Promise((resolve3, reject) => {
2477
+ const req = doRequest(url, { method: "POST", headers }, (res) => {
2478
+ const chunks = [];
2479
+ res.on("data", (c) => chunks.push(c));
2480
+ res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
2481
+ res.on("error", reject);
2482
+ });
2483
+ req.on("error", reject);
2484
+ req.write(body);
2485
+ req.end();
2486
+ });
2487
+ }
2488
+ var curlImpersonateAvailable = null;
2489
+ function curlImpersonatePost(url, headers, body) {
2490
+ return new Promise((resolve3, reject) => {
2491
+ const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
2492
+ for (const [k, v] of Object.entries(headers)) {
2493
+ if (k.toLowerCase() === "content-length") continue;
2494
+ args.push("-H", `${k}: ${String(v)}`);
2495
+ }
2496
+ args.push("--data-binary", "@-");
2497
+ const proc = spawn2("curl-impersonate-chrome", args, {
2498
+ env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
2499
+ stdio: ["pipe", "pipe", "pipe"]
2500
+ });
2501
+ const chunks = [];
2502
+ proc.stdout.on("data", (c) => chunks.push(c));
2503
+ proc.on("error", (e) => {
2504
+ curlImpersonateAvailable = false;
2505
+ reject(e);
2506
+ });
2507
+ proc.on("close", (code) => {
2508
+ const raw = Buffer.concat(chunks).toString("utf8");
2509
+ const lastNewline = raw.lastIndexOf("\n");
2510
+ const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
2511
+ const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
2512
+ const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
2513
+ curlImpersonateAvailable = true;
2514
+ resolve3({ status, body: responseBody });
2515
+ });
2516
+ proc.stdin.write(body);
2517
+ proc.stdin.end();
1499
2518
  });
1500
2519
  }
1501
2520
  async function proxyTranscribe(body, contentType, authToken, accountId) {
1502
- const headers = {
2521
+ const chatgptHeaders = {
1503
2522
  "Content-Type": contentType,
1504
2523
  "Content-Length": body.length,
1505
2524
  Authorization: `Bearer ${authToken}`,
1506
2525
  originator: "Codex Desktop",
1507
2526
  "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
1508
2527
  };
1509
- if (accountId) {
1510
- headers["ChatGPT-Account-Id"] = accountId;
2528
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
2529
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
2530
+ let result;
2531
+ try {
2532
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
2533
+ } catch {
2534
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1511
2535
  }
1512
- return new Promise((resolve3, reject) => {
1513
- const req = httpsRequest(
1514
- "https://chatgpt.com/backend-api/transcribe",
1515
- { method: "POST", headers },
1516
- (res) => {
1517
- const chunks = [];
1518
- res.on("data", (c) => chunks.push(c));
1519
- res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
1520
- res.on("error", reject);
2536
+ if (result.status === 403 && result.body.includes("cf_chl")) {
2537
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
2538
+ try {
2539
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
2540
+ if (ciResult.status !== 403) return ciResult;
2541
+ } catch {
1521
2542
  }
1522
- );
1523
- req.on("error", reject);
1524
- req.write(body);
1525
- req.end();
1526
- });
2543
+ }
2544
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
2545
+ }
2546
+ return result;
1527
2547
  }
1528
2548
  var AppServerProcess = class {
1529
2549
  constructor() {
@@ -1544,10 +2564,18 @@ var AppServerProcess = class {
1544
2564
  'sandbox_mode="danger-full-access"'
1545
2565
  ];
1546
2566
  }
2567
+ getCodexCommand() {
2568
+ const codexCommand = resolveCodexCommand();
2569
+ if (!codexCommand) {
2570
+ throw new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND.");
2571
+ }
2572
+ return codexCommand;
2573
+ }
1547
2574
  start() {
1548
2575
  if (this.process) return;
1549
2576
  this.stopping = false;
1550
- const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
2577
+ const invocation = getSpawnInvocation(this.getCodexCommand(), this.appServerArgs);
2578
+ const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
1551
2579
  this.process = proc;
1552
2580
  proc.stdout.setEncoding("utf8");
1553
2581
  proc.stdout.on("data", (chunk) => {
@@ -1641,7 +2669,7 @@ var AppServerProcess = class {
1641
2669
  }
1642
2670
  this.pendingServerRequests.delete(requestId);
1643
2671
  this.sendServerRequestReply(requestId, reply);
1644
- const requestParams = asRecord2(pendingRequest.params);
2672
+ const requestParams = asRecord3(pendingRequest.params);
1645
2673
  const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
1646
2674
  this.emitNotification({
1647
2675
  method: "server/request/resolved",
@@ -1710,7 +2738,7 @@ var AppServerProcess = class {
1710
2738
  }
1711
2739
  async respondToServerRequest(payload) {
1712
2740
  await this.ensureInitialized();
1713
- const body = asRecord2(payload);
2741
+ const body = asRecord3(payload);
1714
2742
  if (!body) {
1715
2743
  throw new Error("Invalid response payload: expected object");
1716
2744
  }
@@ -1718,7 +2746,7 @@ var AppServerProcess = class {
1718
2746
  if (typeof id !== "number" || !Number.isInteger(id)) {
1719
2747
  throw new Error('Invalid response payload: "id" must be an integer');
1720
2748
  }
1721
- const rawError = asRecord2(body.error);
2749
+ const rawError = asRecord3(body.error);
1722
2750
  if (rawError) {
1723
2751
  const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
1724
2752
  const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
@@ -1773,7 +2801,13 @@ var MethodCatalog = class {
1773
2801
  }
1774
2802
  async runGenerateSchemaCommand(outDir) {
1775
2803
  await new Promise((resolve3, reject) => {
1776
- const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
2804
+ const codexCommand = resolveCodexCommand();
2805
+ if (!codexCommand) {
2806
+ reject(new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND."));
2807
+ return;
2808
+ }
2809
+ const invocation = getSpawnInvocation(codexCommand, ["app-server", "generate-json-schema", "--out", outDir]);
2810
+ const process2 = spawn2(invocation.command, invocation.args, {
1777
2811
  stdio: ["ignore", "ignore", "pipe"]
1778
2812
  });
1779
2813
  let stderr = "";
@@ -1792,13 +2826,13 @@ var MethodCatalog = class {
1792
2826
  });
1793
2827
  }
1794
2828
  extractMethodsFromClientRequest(payload) {
1795
- const root = asRecord2(payload);
2829
+ const root = asRecord3(payload);
1796
2830
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1797
2831
  const methods = /* @__PURE__ */ new Set();
1798
2832
  for (const entry of oneOf) {
1799
- const row = asRecord2(entry);
1800
- const properties = asRecord2(row?.properties);
1801
- const methodDef = asRecord2(properties?.method);
2833
+ const row = asRecord3(entry);
2834
+ const properties = asRecord3(row?.properties);
2835
+ const methodDef = asRecord3(properties?.method);
1802
2836
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1803
2837
  for (const item of methodEnum) {
1804
2838
  if (typeof item === "string" && item.length > 0) {
@@ -1809,13 +2843,13 @@ var MethodCatalog = class {
1809
2843
  return Array.from(methods).sort((a, b) => a.localeCompare(b));
1810
2844
  }
1811
2845
  extractMethodsFromServerNotification(payload) {
1812
- const root = asRecord2(payload);
2846
+ const root = asRecord3(payload);
1813
2847
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1814
2848
  const methods = /* @__PURE__ */ new Set();
1815
2849
  for (const entry of oneOf) {
1816
- const row = asRecord2(entry);
1817
- const properties = asRecord2(row?.properties);
1818
- const methodDef = asRecord2(properties?.method);
2850
+ const row = asRecord3(entry);
2851
+ const properties = asRecord3(row?.properties);
2852
+ const methodDef = asRecord3(properties?.method);
1819
2853
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1820
2854
  for (const item of methodEnum) {
1821
2855
  if (typeof item === "string" && item.length > 0) {
@@ -1829,9 +2863,9 @@ var MethodCatalog = class {
1829
2863
  if (this.methodCache) {
1830
2864
  return this.methodCache;
1831
2865
  }
1832
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2866
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
1833
2867
  await this.runGenerateSchemaCommand(outDir);
1834
- const clientRequestPath = join2(outDir, "ClientRequest.json");
2868
+ const clientRequestPath = join3(outDir, "ClientRequest.json");
1835
2869
  const raw = await readFile2(clientRequestPath, "utf8");
1836
2870
  const parsed = JSON.parse(raw);
1837
2871
  const methods = this.extractMethodsFromClientRequest(parsed);
@@ -1842,9 +2876,9 @@ var MethodCatalog = class {
1842
2876
  if (this.notificationCache) {
1843
2877
  return this.notificationCache;
1844
2878
  }
1845
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2879
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
1846
2880
  await this.runGenerateSchemaCommand(outDir);
1847
- const serverNotificationPath = join2(outDir, "ServerNotification.json");
2881
+ const serverNotificationPath = join3(outDir, "ServerNotification.json");
1848
2882
  const raw = await readFile2(serverNotificationPath, "utf8");
1849
2883
  const parsed = JSON.parse(raw);
1850
2884
  const methods = this.extractMethodsFromServerNotification(parsed);
@@ -1857,9 +2891,11 @@ function getSharedBridgeState() {
1857
2891
  const globalScope = globalThis;
1858
2892
  const existing = globalScope[SHARED_BRIDGE_KEY];
1859
2893
  if (existing) return existing;
2894
+ const appServer = new AppServerProcess();
1860
2895
  const created = {
1861
- appServer: new AppServerProcess(),
1862
- methodCatalog: new MethodCatalog()
2896
+ appServer,
2897
+ methodCatalog: new MethodCatalog(),
2898
+ telegramBridge: new TelegramThreadBridge(appServer)
1863
2899
  };
1864
2900
  globalScope[SHARED_BRIDGE_KEY] = created;
1865
2901
  return created;
@@ -1868,7 +2904,7 @@ async function loadAllThreadsForSearch(appServer) {
1868
2904
  const threads = [];
1869
2905
  let cursor = null;
1870
2906
  do {
1871
- const response = asRecord2(await appServer.rpc("thread/list", {
2907
+ const response = asRecord3(await appServer.rpc("thread/list", {
1872
2908
  archived: false,
1873
2909
  limit: 100,
1874
2910
  sortKey: "updated_at",
@@ -1876,7 +2912,7 @@ async function loadAllThreadsForSearch(appServer) {
1876
2912
  }));
1877
2913
  const data = Array.isArray(response?.data) ? response.data : [];
1878
2914
  for (const row of data) {
1879
- const record = asRecord2(row);
2915
+ const record = asRecord3(row);
1880
2916
  const id = typeof record?.id === "string" ? record.id : "";
1881
2917
  if (!id) continue;
1882
2918
  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";
@@ -1925,7 +2961,7 @@ async function buildThreadSearchIndex(appServer) {
1925
2961
  return { docsById };
1926
2962
  }
1927
2963
  function createCodexBridgeMiddleware() {
1928
- const { appServer, methodCatalog } = getSharedBridgeState();
2964
+ const { appServer, methodCatalog, telegramBridge } = getSharedBridgeState();
1929
2965
  let threadSearchIndex = null;
1930
2966
  let threadSearchIndexPromise = null;
1931
2967
  async function getThreadSearchIndex() {
@@ -1941,6 +2977,12 @@ function createCodexBridgeMiddleware() {
1941
2977
  return threadSearchIndexPromise;
1942
2978
  }
1943
2979
  void initializeSkillsSyncOnStartup(appServer);
2980
+ void readTelegramBridgeConfig().then((config) => {
2981
+ if (!config.botToken) return;
2982
+ telegramBridge.configureToken(config.botToken);
2983
+ telegramBridge.start();
2984
+ }).catch(() => {
2985
+ });
1944
2986
  const middleware = async (req, res, next) => {
1945
2987
  try {
1946
2988
  if (!req.url) {
@@ -1957,7 +2999,7 @@ function createCodexBridgeMiddleware() {
1957
2999
  }
1958
3000
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
1959
3001
  const payload = await readJsonBody(req);
1960
- const body = asRecord2(payload);
3002
+ const body = asRecord3(payload);
1961
3003
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
1962
3004
  setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
1963
3005
  return;
@@ -2006,11 +3048,24 @@ function createCodexBridgeMiddleware() {
2006
3048
  return;
2007
3049
  }
2008
3050
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
2009
- setJson2(res, 200, { data: { path: homedir2() } });
3051
+ setJson2(res, 200, { data: { path: homedir3() } });
3052
+ return;
3053
+ }
3054
+ if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
3055
+ const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
3056
+ const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
3057
+ const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
3058
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
3059
+ try {
3060
+ const data = await fetchGithubTrending(since, limit);
3061
+ setJson2(res, 200, { data });
3062
+ } catch (error) {
3063
+ setJson2(res, 502, { error: getErrorMessage3(error, "Failed to fetch GitHub trending") });
3064
+ }
2010
3065
  return;
2011
3066
  }
2012
3067
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
2013
- const payload = asRecord2(await readJsonBody(req));
3068
+ const payload = asRecord3(await readJsonBody(req));
2014
3069
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
2015
3070
  if (!rawSourceCwd) {
2016
3071
  setJson2(res, 400, { error: "Missing sourceCwd" });
@@ -2036,22 +3091,22 @@ function createCodexBridgeMiddleware() {
2036
3091
  await runCommand2("git", ["init"], { cwd: sourceCwd });
2037
3092
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2038
3093
  }
2039
- const repoName = basename(gitRoot) || "repo";
2040
- const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
3094
+ const repoName = basename3(gitRoot) || "repo";
3095
+ const worktreesRoot = join3(getCodexHomeDir2(), "worktrees");
2041
3096
  await mkdir2(worktreesRoot, { recursive: true });
2042
3097
  let worktreeId = "";
2043
3098
  let worktreeParent = "";
2044
3099
  let worktreeCwd = "";
2045
3100
  for (let attempt = 0; attempt < 12; attempt += 1) {
2046
3101
  const candidate = randomBytes(2).toString("hex");
2047
- const parent = join2(worktreesRoot, candidate);
3102
+ const parent = join3(worktreesRoot, candidate);
2048
3103
  try {
2049
3104
  await stat2(parent);
2050
3105
  continue;
2051
3106
  } catch {
2052
3107
  worktreeId = candidate;
2053
3108
  worktreeParent = parent;
2054
- worktreeCwd = join2(parent, repoName);
3109
+ worktreeCwd = join3(parent, repoName);
2055
3110
  break;
2056
3111
  }
2057
3112
  }
@@ -2075,13 +3130,106 @@ function createCodexBridgeMiddleware() {
2075
3130
  }
2076
3131
  });
2077
3132
  } catch (error) {
2078
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
3133
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to create worktree") });
3134
+ }
3135
+ return;
3136
+ }
3137
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
3138
+ const payload = asRecord3(await readJsonBody(req));
3139
+ const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
3140
+ const commitMessage = normalizeCommitMessage(payload?.message);
3141
+ if (!rawCwd) {
3142
+ setJson2(res, 400, { error: "Missing cwd" });
3143
+ return;
3144
+ }
3145
+ if (!commitMessage) {
3146
+ setJson2(res, 400, { error: "Missing message" });
3147
+ return;
3148
+ }
3149
+ const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
3150
+ try {
3151
+ const cwdInfo = await stat2(cwd);
3152
+ if (!cwdInfo.isDirectory()) {
3153
+ setJson2(res, 400, { error: "cwd is not a directory" });
3154
+ return;
3155
+ }
3156
+ } catch {
3157
+ setJson2(res, 404, { error: "cwd does not exist" });
3158
+ return;
3159
+ }
3160
+ try {
3161
+ await ensureRollbackGitRepo(cwd);
3162
+ const beforeStatus = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
3163
+ if (!beforeStatus.trim()) {
3164
+ setJson2(res, 200, { data: { committed: false } });
3165
+ return;
3166
+ }
3167
+ await runRollbackGit(cwd, ["add", "-A"]);
3168
+ const stagedStatus = await runRollbackGitWithOutput(cwd, ["diff", "--cached", "--name-only"]);
3169
+ if (!stagedStatus.trim()) {
3170
+ setJson2(res, 200, { data: { committed: false } });
3171
+ return;
3172
+ }
3173
+ await runRollbackGit(cwd, ["commit", "-m", commitMessage]);
3174
+ setJson2(res, 200, { data: { committed: true } });
3175
+ } catch (error) {
3176
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit rollback changes") });
3177
+ }
3178
+ return;
3179
+ }
3180
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
3181
+ const payload = asRecord3(await readJsonBody(req));
3182
+ const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
3183
+ const commitMessage = normalizeCommitMessage(payload?.message);
3184
+ if (!rawCwd) {
3185
+ setJson2(res, 400, { error: "Missing cwd" });
3186
+ return;
3187
+ }
3188
+ if (!commitMessage) {
3189
+ setJson2(res, 400, { error: "Missing message" });
3190
+ return;
3191
+ }
3192
+ const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
3193
+ try {
3194
+ const cwdInfo = await stat2(cwd);
3195
+ if (!cwdInfo.isDirectory()) {
3196
+ setJson2(res, 400, { error: "cwd is not a directory" });
3197
+ return;
3198
+ }
3199
+ } catch {
3200
+ setJson2(res, 404, { error: "cwd does not exist" });
3201
+ return;
3202
+ }
3203
+ try {
3204
+ await ensureRollbackGitRepo(cwd);
3205
+ const commitSha = await findRollbackCommitByExactMessage(cwd, commitMessage);
3206
+ if (!commitSha) {
3207
+ setJson2(res, 404, { error: "No matching commit found for this user message" });
3208
+ return;
3209
+ }
3210
+ let resetTargetSha = "";
3211
+ try {
3212
+ resetTargetSha = await runRollbackGitCapture(cwd, ["rev-parse", `${commitSha}^`]);
3213
+ } catch {
3214
+ setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
3215
+ return;
3216
+ }
3217
+ let stashed = false;
3218
+ if (await hasRollbackGitWorkingTreeChanges(cwd)) {
3219
+ const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
3220
+ await runRollbackGit(cwd, ["stash", "push", "-u", "-m", stashMessage]);
3221
+ stashed = true;
3222
+ }
3223
+ await runRollbackGit(cwd, ["reset", "--hard", resetTargetSha]);
3224
+ setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
3225
+ } catch (error) {
3226
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback project to user message commit") });
2079
3227
  }
2080
3228
  return;
2081
3229
  }
2082
3230
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
2083
3231
  const payload = await readJsonBody(req);
2084
- const record = asRecord2(payload);
3232
+ const record = asRecord3(payload);
2085
3233
  if (!record) {
2086
3234
  setJson2(res, 400, { error: "Invalid body: expected object" });
2087
3235
  return;
@@ -2096,7 +3244,7 @@ function createCodexBridgeMiddleware() {
2096
3244
  return;
2097
3245
  }
2098
3246
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
2099
- const payload = asRecord2(await readJsonBody(req));
3247
+ const payload = asRecord3(await readJsonBody(req));
2100
3248
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
2101
3249
  const createIfMissing = payload?.createIfMissing === true;
2102
3250
  const label = typeof payload?.label === "string" ? payload.label : "";
@@ -2156,7 +3304,7 @@ function createCodexBridgeMiddleware() {
2156
3304
  let index = 1;
2157
3305
  while (index < 1e5) {
2158
3306
  const candidateName = `New Project (${String(index)})`;
2159
- const candidatePath = join2(normalizedBasePath, candidateName);
3307
+ const candidatePath = join3(normalizedBasePath, candidateName);
2160
3308
  try {
2161
3309
  await stat2(candidatePath);
2162
3310
  index += 1;
@@ -2170,7 +3318,7 @@ function createCodexBridgeMiddleware() {
2170
3318
  return;
2171
3319
  }
2172
3320
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
2173
- const payload = asRecord2(await readJsonBody(req));
3321
+ const payload = asRecord3(await readJsonBody(req));
2174
3322
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
2175
3323
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2176
3324
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
@@ -2195,17 +3343,17 @@ function createCodexBridgeMiddleware() {
2195
3343
  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 }));
2196
3344
  setJson2(res, 200, { data: scored });
2197
3345
  } catch (error) {
2198
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
3346
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to search files") });
2199
3347
  }
2200
3348
  return;
2201
3349
  }
2202
3350
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
2203
- const cache = await readThreadTitleCache();
3351
+ const cache = await readMergedThreadTitleCache();
2204
3352
  setJson2(res, 200, { data: cache });
2205
3353
  return;
2206
3354
  }
2207
3355
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
2208
- const payload = asRecord2(await readJsonBody(req));
3356
+ const payload = asRecord3(await readJsonBody(req));
2209
3357
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2210
3358
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
2211
3359
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
@@ -2219,7 +3367,7 @@ function createCodexBridgeMiddleware() {
2219
3367
  return;
2220
3368
  }
2221
3369
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
2222
- const payload = asRecord2(await readJsonBody(req));
3370
+ const payload = asRecord3(await readJsonBody(req));
2223
3371
  const id = typeof payload?.id === "string" ? payload.id : "";
2224
3372
  const title = typeof payload?.title === "string" ? payload.title : "";
2225
3373
  if (!id) {
@@ -2232,6 +3380,23 @@ function createCodexBridgeMiddleware() {
2232
3380
  setJson2(res, 200, { ok: true });
2233
3381
  return;
2234
3382
  }
3383
+ if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
3384
+ const payload = asRecord3(await readJsonBody(req));
3385
+ const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
3386
+ if (!botToken) {
3387
+ setJson2(res, 400, { error: "Missing botToken" });
3388
+ return;
3389
+ }
3390
+ telegramBridge.configureToken(botToken);
3391
+ telegramBridge.start();
3392
+ await writeTelegramBridgeConfig({ botToken });
3393
+ setJson2(res, 200, { ok: true });
3394
+ return;
3395
+ }
3396
+ if (req.method === "GET" && url.pathname === "/codex-api/telegram/status") {
3397
+ setJson2(res, 200, { data: telegramBridge.getStatus() });
3398
+ return;
3399
+ }
2235
3400
  if (req.method === "GET" && url.pathname === "/codex-api/events") {
2236
3401
  res.statusCode = 200;
2237
3402
  res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
@@ -2264,12 +3429,13 @@ data: ${JSON.stringify({ ok: true })}
2264
3429
  }
2265
3430
  next();
2266
3431
  } catch (error) {
2267
- const message = getErrorMessage2(error, "Unknown bridge error");
3432
+ const message = getErrorMessage3(error, "Unknown bridge error");
2268
3433
  setJson2(res, 502, { error: message });
2269
3434
  }
2270
3435
  };
2271
3436
  middleware.dispose = () => {
2272
3437
  threadSearchIndex = null;
3438
+ telegramBridge.stop();
2273
3439
  appServer.dispose();
2274
3440
  };
2275
3441
  middleware.subscribeNotifications = (listener) => {
@@ -2402,7 +3568,7 @@ function createAuthSession(password) {
2402
3568
  }
2403
3569
 
2404
3570
  // src/server/localBrowseUi.ts
2405
- import { dirname, extname, join as join3 } from "path";
3571
+ import { dirname as dirname2, extname as extname2, join as join4 } from "path";
2406
3572
  import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
2407
3573
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2408
3574
  ".txt",
@@ -2433,7 +3599,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2433
3599
  ".ps1"
2434
3600
  ]);
2435
3601
  function languageForPath(pathValue) {
2436
- const extension = extname(pathValue).toLowerCase();
3602
+ const extension = extname2(pathValue).toLowerCase();
2437
3603
  switch (extension) {
2438
3604
  case ".js":
2439
3605
  return "javascript";
@@ -2494,7 +3660,7 @@ function decodeBrowsePath(rawPath) {
2494
3660
  }
2495
3661
  }
2496
3662
  function isTextEditablePath(pathValue) {
2497
- return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
3663
+ return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
2498
3664
  }
2499
3665
  function looksLikeTextBuffer(buffer) {
2500
3666
  if (buffer.length === 0) return true;
@@ -2540,7 +3706,7 @@ function escapeForInlineScriptString(value) {
2540
3706
  async function getDirectoryItems(localPath) {
2541
3707
  const entries = await readdir3(localPath, { withFileTypes: true });
2542
3708
  const withMeta = await Promise.all(entries.map(async (entry) => {
2543
- const entryPath = join3(localPath, entry.name);
3709
+ const entryPath = join4(localPath, entry.name);
2544
3710
  const entryStat = await stat3(entryPath);
2545
3711
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2546
3712
  return {
@@ -2560,13 +3726,13 @@ async function getDirectoryItems(localPath) {
2560
3726
  }
2561
3727
  async function createDirectoryListingHtml(localPath) {
2562
3728
  const items = await getDirectoryItems(localPath);
2563
- const parentPath = dirname(localPath);
3729
+ const parentPath = dirname2(localPath);
2564
3730
  const rows = items.map((item) => {
2565
3731
  const suffix = item.isDirectory ? "/" : "";
2566
3732
  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>`;
3733
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
2568
3734
  }).join("\n");
2569
- const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
3735
+ const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
2570
3736
  return `<!doctype html>
2571
3737
  <html lang="en">
2572
3738
  <head>
@@ -2580,8 +3746,27 @@ async function createDirectoryListingHtml(localPath) {
2580
3746
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
2581
3747
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
2582
3748
  .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; }
3749
+ .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
3750
+ .header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
3751
+ .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
3752
+ .header-open-btn {
3753
+ height: 42px;
3754
+ padding: 0 14px;
3755
+ border: 1px solid #4f8de0;
3756
+ border-radius: 10px;
3757
+ background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
3758
+ color: #eef6ff;
3759
+ font-weight: 700;
3760
+ letter-spacing: 0.01em;
3761
+ cursor: pointer;
3762
+ box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
3763
+ }
3764
+ .header-open-btn:hover { filter: brightness(1.08); }
3765
+ .header-open-btn:disabled { opacity: 0.6; cursor: default; }
3766
+ .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
3767
+ .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
2584
3768
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
3769
+ .status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
2585
3770
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
2586
3771
  @media (max-width: 640px) {
2587
3772
  body { margin: 12px; }
@@ -2593,14 +3778,52 @@ async function createDirectoryListingHtml(localPath) {
2593
3778
  </head>
2594
3779
  <body>
2595
3780
  <h1>Index of ${escapeHtml(localPath)}</h1>
2596
- ${parentLink}
3781
+ <div class="header-actions">
3782
+ ${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
3783
+ <button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
3784
+ </div>
3785
+ <p id="status" class="status"></p>
2597
3786
  <ul>${rows}</ul>
3787
+ <script>
3788
+ const status = document.getElementById('status');
3789
+ document.addEventListener('click', async (event) => {
3790
+ const target = event.target;
3791
+ if (!(target instanceof Element)) return;
3792
+ const button = target.closest('.open-folder-btn');
3793
+ if (!(button instanceof HTMLButtonElement)) return;
3794
+
3795
+ const path = button.getAttribute('data-path') || '';
3796
+ if (!path) return;
3797
+ button.disabled = true;
3798
+ status.textContent = 'Opening folder in Codex...';
3799
+ try {
3800
+ const response = await fetch('/codex-api/project-root', {
3801
+ method: 'POST',
3802
+ headers: { 'Content-Type': 'application/json' },
3803
+ body: JSON.stringify({
3804
+ path,
3805
+ createIfMissing: false,
3806
+ label: '',
3807
+ }),
3808
+ });
3809
+ if (!response.ok) {
3810
+ status.textContent = 'Failed to open folder.';
3811
+ button.disabled = false;
3812
+ return;
3813
+ }
3814
+ window.location.assign('/#/');
3815
+ } catch {
3816
+ status.textContent = 'Failed to open folder.';
3817
+ button.disabled = false;
3818
+ }
3819
+ });
3820
+ </script>
2598
3821
  </body>
2599
3822
  </html>`;
2600
3823
  }
2601
3824
  async function createTextEditorHtml(localPath) {
2602
3825
  const content = await readFile3(localPath, "utf8");
2603
- const parentPath = dirname(localPath);
3826
+ const parentPath = dirname2(localPath);
2604
3827
  const language = languageForPath(localPath);
2605
3828
  const safeContentLiteral = escapeForInlineScriptString(content);
2606
3829
  return `<!doctype html>
@@ -2669,9 +3892,9 @@ async function createTextEditorHtml(localPath) {
2669
3892
 
2670
3893
  // src/server/httpServer.ts
2671
3894
  import { WebSocketServer } from "ws";
2672
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2673
- var distDir = join4(__dirname, "..", "dist");
2674
- var spaEntryFile = join4(distDir, "index.html");
3895
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
3896
+ var distDir = join5(__dirname, "..", "dist");
3897
+ var spaEntryFile = join5(distDir, "index.html");
2675
3898
  var IMAGE_CONTENT_TYPES = {
2676
3899
  ".avif": "image/avif",
2677
3900
  ".bmp": "image/bmp",
@@ -2728,7 +3951,7 @@ function createServer(options = {}) {
2728
3951
  res.status(400).json({ error: "Expected absolute local file path." });
2729
3952
  return;
2730
3953
  }
2731
- const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
3954
+ const contentType = IMAGE_CONTENT_TYPES[extname3(localPath).toLowerCase()];
2732
3955
  if (!contentType) {
2733
3956
  res.status(415).json({ error: "Unsupported image type." });
2734
3957
  return;
@@ -2815,7 +4038,7 @@ function createServer(options = {}) {
2815
4038
  res.status(404).json({ error: "File not found." });
2816
4039
  }
2817
4040
  });
2818
- const hasFrontendAssets = existsSync2(spaEntryFile);
4041
+ const hasFrontendAssets = existsSync4(spaEntryFile);
2819
4042
  if (hasFrontendAssets) {
2820
4043
  app.use(express.static(distDir));
2821
4044
  }
@@ -2885,10 +4108,26 @@ function generatePassword() {
2885
4108
 
2886
4109
  // src/cli/index.ts
2887
4110
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2888
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
4111
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
4112
+ var hasPromptedCloudflaredInstall = false;
4113
+ function getCodexHomePath() {
4114
+ return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
4115
+ }
4116
+ function getCloudflaredPromptMarkerPath() {
4117
+ return join6(getCodexHomePath(), ".cloudflared-install-prompted");
4118
+ }
4119
+ function hasPromptedCloudflaredInstallPersisted() {
4120
+ return existsSync5(getCloudflaredPromptMarkerPath());
4121
+ }
4122
+ async function persistCloudflaredInstallPrompted() {
4123
+ const codexHome = getCodexHomePath();
4124
+ mkdirSync(codexHome, { recursive: true });
4125
+ await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
4126
+ `, "utf8");
4127
+ }
2889
4128
  async function readCliVersion() {
2890
4129
  try {
2891
- const packageJsonPath = join5(__dirname2, "..", "package.json");
4130
+ const packageJsonPath = join6(__dirname2, "..", "package.json");
2892
4131
  const raw = await readFile4(packageJsonPath, "utf8");
2893
4132
  const parsed = JSON.parse(raw);
2894
4133
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -2899,47 +4138,22 @@ async function readCliVersion() {
2899
4138
  function isTermuxRuntime() {
2900
4139
  return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
2901
4140
  }
2902
- function canRun(command, args = []) {
2903
- const result = spawnSync(command, args, { stdio: "ignore" });
2904
- return result.status === 0;
2905
- }
2906
4141
  function runOrFail(command, args, label) {
2907
- const result = spawnSync(command, args, { stdio: "inherit" });
4142
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
2908
4143
  if (result.status !== 0) {
2909
4144
  throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
2910
4145
  }
2911
4146
  }
2912
4147
  function runWithStatus(command, args) {
2913
- const result = spawnSync(command, args, { stdio: "inherit" });
4148
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
2914
4149
  return result.status ?? -1;
2915
4150
  }
2916
- function getUserNpmPrefix() {
2917
- return join5(homedir3(), ".npm-global");
2918
- }
2919
- function resolveCodexCommand() {
2920
- if (canRun("codex", ["--version"])) {
2921
- return "codex";
2922
- }
2923
- const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
2924
- if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
2925
- return userCandidate;
2926
- }
2927
- const prefix = process.env.PREFIX?.trim();
2928
- if (!prefix) {
2929
- return null;
2930
- }
2931
- const candidate = join5(prefix, "bin", "codex");
2932
- if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
2933
- return candidate;
2934
- }
2935
- return null;
2936
- }
2937
4151
  function resolveCloudflaredCommand() {
2938
- if (canRun("cloudflared", ["--version"])) {
4152
+ if (canRunCommand("cloudflared", ["--version"])) {
2939
4153
  return "cloudflared";
2940
4154
  }
2941
- const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
2942
- if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
4155
+ const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
4156
+ if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
2943
4157
  return localCandidate;
2944
4158
  }
2945
4159
  return null;
@@ -2992,14 +4206,14 @@ async function ensureCloudflaredInstalledLinux() {
2992
4206
  if (!mappedArch) {
2993
4207
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2994
4208
  }
2995
- const userBinDir = join5(homedir3(), ".local", "bin");
4209
+ const userBinDir = join6(homedir4(), ".local", "bin");
2996
4210
  mkdirSync(userBinDir, { recursive: true });
2997
- const destination = join5(userBinDir, "cloudflared");
4211
+ const destination = join6(userBinDir, "cloudflared");
2998
4212
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2999
4213
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
3000
4214
  await downloadFile(downloadUrl, destination);
3001
4215
  chmodSync(destination, 493);
3002
- process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
4216
+ process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
3003
4217
  const installed = resolveCloudflaredCommand();
3004
4218
  if (!installed) {
3005
4219
  throw new Error("cloudflared download completed but executable is still not available");
@@ -3008,11 +4222,19 @@ async function ensureCloudflaredInstalledLinux() {
3008
4222
  return installed;
3009
4223
  }
3010
4224
  async function shouldInstallCloudflaredInteractively() {
4225
+ if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
4226
+ return false;
4227
+ }
4228
+ hasPromptedCloudflaredInstall = true;
4229
+ await persistCloudflaredInstallPrompted();
4230
+ if (process.platform === "win32") {
4231
+ return false;
4232
+ }
3011
4233
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3012
4234
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3013
4235
  return false;
3014
4236
  }
3015
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
4237
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
3016
4238
  try {
3017
4239
  const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
3018
4240
  const normalized = answer.trim().toLowerCase();
@@ -3026,6 +4248,9 @@ async function resolveCloudflaredForTunnel() {
3026
4248
  if (current) {
3027
4249
  return current;
3028
4250
  }
4251
+ if (process.platform === "win32") {
4252
+ return null;
4253
+ }
3029
4254
  const installApproved = await shouldInstallCloudflaredInteractively();
3030
4255
  if (!installApproved) {
3031
4256
  return null;
@@ -3033,8 +4258,8 @@ async function resolveCloudflaredForTunnel() {
3033
4258
  return ensureCloudflaredInstalledLinux();
3034
4259
  }
3035
4260
  function hasCodexAuth() {
3036
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3037
- return existsSync3(join5(codexHome, "auth.json"));
4261
+ const codexHome = getCodexHomePath();
4262
+ return existsSync5(join6(codexHome, "auth.json"));
3038
4263
  }
3039
4264
  function ensureCodexInstalled() {
3040
4265
  let codexCommand = resolveCodexCommand();
@@ -3052,7 +4277,7 @@ function ensureCodexInstalled() {
3052
4277
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
3053
4278
  `);
3054
4279
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
3055
- process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
4280
+ process.env.PATH = prependPathEntry(process.env.PATH ?? "", getNpmGlobalBinDir(userPrefix));
3056
4281
  };
3057
4282
  if (isTermuxRuntime()) {
3058
4283
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -3115,19 +4340,22 @@ function parseCloudflaredUrl(chunk) {
3115
4340
  }
3116
4341
  function getAccessibleUrls(port) {
3117
4342
  const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
3118
- const interfaces = networkInterfaces();
3119
- for (const entries of Object.values(interfaces)) {
3120
- if (!entries) {
3121
- continue;
3122
- }
3123
- for (const entry of entries) {
3124
- if (entry.internal) {
4343
+ try {
4344
+ const interfaces = networkInterfaces();
4345
+ for (const entries of Object.values(interfaces)) {
4346
+ if (!entries) {
3125
4347
  continue;
3126
4348
  }
3127
- if (entry.family === "IPv4") {
3128
- urls.add(`http://${entry.address}:${String(port)}`);
4349
+ for (const entry of entries) {
4350
+ if (entry.internal) {
4351
+ continue;
4352
+ }
4353
+ if (entry.family === "IPv4") {
4354
+ urls.add(`http://${entry.address}:${String(port)}`);
4355
+ }
3129
4356
  }
3130
4357
  }
4358
+ } catch {
3131
4359
  }
3132
4360
  return Array.from(urls);
3133
4361
  }
@@ -3190,8 +4418,8 @@ function listenWithFallback(server, startPort) {
3190
4418
  });
3191
4419
  }
3192
4420
  function getCodexGlobalStatePath2() {
3193
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3194
- return join5(codexHome, ".codex-global-state.json");
4421
+ const codexHome = getCodexHomePath();
4422
+ return join6(codexHome, ".codex-global-state.json");
3195
4423
  }
3196
4424
  function normalizeUniqueStrings(value) {
3197
4425
  if (!Array.isArray(value)) return [];
@@ -3256,6 +4484,9 @@ async function startServer(options) {
3256
4484
  }
3257
4485
  }
3258
4486
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
4487
+ if (codexCommand) {
4488
+ process.env.CODEXUI_CODEX_COMMAND = codexCommand;
4489
+ }
3259
4490
  if (!hasCodexAuth() && codexCommand) {
3260
4491
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
3261
4492
  runOrFail(codexCommand, ["login"], "Codex login");
@@ -3315,7 +4546,7 @@ async function startServer(options) {
3315
4546
  qrcode.generate(tunnelUrl, { small: true });
3316
4547
  console.log("");
3317
4548
  }
3318
- openBrowser(`http://localhost:${String(port)}`);
4549
+ if (options.open) openBrowser(`http://localhost:${String(port)}`);
3319
4550
  function shutdown() {
3320
4551
  console.log("\nShutting down...");
3321
4552
  if (tunnelChild && !tunnelChild.killed) {
@@ -3335,10 +4566,11 @@ async function startServer(options) {
3335
4566
  }
3336
4567
  async function runLogin() {
3337
4568
  const codexCommand = ensureCodexInstalled() ?? "codex";
4569
+ process.env.CODEXUI_CODEX_COMMAND = codexCommand;
3338
4570
  console.log("\nStarting `codex login`...\n");
3339
4571
  runOrFail(codexCommand, ["login"], "Codex login");
3340
4572
  }
3341
- program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (projectPath, opts) => {
4573
+ program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").action(async (projectPath, opts) => {
3342
4574
  const rawArgv = process.argv.slice(2);
3343
4575
  const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
3344
4576
  let openProjectOnly = (opts.openProject ?? "").trim();