codexapp 0.1.53 → 0.1.56

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,13 +1233,21 @@ 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";
1236
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
1237
+ if (!installerScript) {
1238
+ throw new Error("Skill installer script not found");
1239
+ }
1240
+ const pythonCommand = resolvePythonCommand();
1241
+ if (!pythonCommand) {
1242
+ throw new Error("Python 3 is required to install skills");
1243
+ }
995
1244
  const localSkills = await scanInstalledSkillsFromDisk();
996
1245
  for (const skill of remote) {
997
1246
  const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
998
1247
  if (!owner) continue;
999
1248
  if (!localSkills.has(skill.name)) {
1000
- await runCommand("python3", [
1249
+ await runCommand(pythonCommand.command, [
1250
+ ...pythonCommand.args,
1001
1251
  installerScript,
1002
1252
  "--repo",
1003
1253
  `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
@@ -1009,7 +1259,7 @@ async function handleSkillsRoutes(req, res, url, context) {
1009
1259
  "git"
1010
1260
  ]);
1011
1261
  }
1012
- const skillPath = join(localDir, skill.name);
1262
+ const skillPath = join2(localDir, skill.name);
1013
1263
  await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1014
1264
  }
1015
1265
  const remoteNames = new Set(remote.map((row) => row.name));
@@ -1038,15 +1288,29 @@ async function handleSkillsRoutes(req, res, url, context) {
1038
1288
  try {
1039
1289
  const owner = url.searchParams.get("owner") || "";
1040
1290
  const name = url.searchParams.get("name") || "";
1291
+ const installed = url.searchParams.get("installed") === "true";
1292
+ const skillPath = url.searchParams.get("path") || "";
1041
1293
  if (!owner || !name) {
1042
1294
  setJson(res, 400, { error: "Missing owner or name" });
1043
1295
  return true;
1044
1296
  }
1297
+ if (installed) {
1298
+ const installedMap = await scanInstalledSkillsFromDisk();
1299
+ const installedInfo = installedMap.get(name);
1300
+ const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
1301
+ if (localSkillPath) {
1302
+ const content2 = await readFile(localSkillPath, "utf8");
1303
+ const description2 = extractSkillDescriptionFromMarkdown(content2);
1304
+ setJson(res, 200, { content: content2, description: description2, source: "local" });
1305
+ return true;
1306
+ }
1307
+ }
1045
1308
  const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1046
1309
  const resp = await fetch(rawUrl);
1047
1310
  if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1048
1311
  const content = await resp.text();
1049
- setJson(res, 200, { content });
1312
+ const description = extractSkillDescriptionFromMarkdown(content);
1313
+ setJson(res, 200, { content, description, source: "remote" });
1050
1314
  } catch (error) {
1051
1315
  setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
1052
1316
  }
@@ -1061,9 +1325,25 @@ async function handleSkillsRoutes(req, res, url, context) {
1061
1325
  setJson(res, 400, { error: "Missing owner or name" });
1062
1326
  return true;
1063
1327
  }
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", [
1328
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
1329
+ if (!installerScript) {
1330
+ throw new Error("Skill installer script not found");
1331
+ }
1332
+ const pythonCommand = resolvePythonCommand();
1333
+ if (!pythonCommand) {
1334
+ throw new Error("Python 3 is required to install skills");
1335
+ }
1336
+ const installDest = await withTimeout(
1337
+ detectUserSkillsDir(appServer),
1338
+ 1e4,
1339
+ "detectUserSkillsDir"
1340
+ ).catch(() => getSkillsInstallDir());
1341
+ const skillDir = join2(installDest, name);
1342
+ if (existsSync2(skillDir)) {
1343
+ await rm(skillDir, { recursive: true, force: true });
1344
+ }
1345
+ await runCommand(pythonCommand.command, [
1346
+ ...pythonCommand.args,
1067
1347
  installerScript,
1068
1348
  "--repo",
1069
1349
  `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
@@ -1073,13 +1353,16 @@ async function handleSkillsRoutes(req, res, url, context) {
1073
1353
  installDest,
1074
1354
  "--method",
1075
1355
  "git"
1076
- ]);
1077
- const skillDir = join(installDest, name);
1078
- await ensureInstalledSkillIsValid(appServer, skillDir);
1356
+ ], { timeoutMs: 9e4 });
1357
+ try {
1358
+ await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
1359
+ } catch {
1360
+ }
1079
1361
  const syncState = await readSkillsSyncState();
1080
1362
  const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1081
1363
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1082
- await autoPushSyncedSkills(appServer);
1364
+ autoPushSyncedSkills(appServer).catch(() => {
1365
+ });
1083
1366
  setJson(res, 200, { ok: true, path: skillDir });
1084
1367
  } catch (error) {
1085
1368
  setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
@@ -1091,7 +1374,7 @@ async function handleSkillsRoutes(req, res, url, context) {
1091
1374
  const payload = asRecord(await readJsonBody2(req));
1092
1375
  const name = typeof payload?.name === "string" ? payload.name : "";
1093
1376
  const path = typeof payload?.path === "string" ? payload.path : "";
1094
- const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1377
+ const target = path || (name ? join2(getSkillsInstallDir(), name) : "");
1095
1378
  if (!target) {
1096
1379
  setJson(res, 400, { error: "Missing name or path" });
1097
1380
  return true;
@@ -1103,9 +1386,10 @@ async function handleSkillsRoutes(req, res, url, context) {
1103
1386
  delete nextOwners[name];
1104
1387
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1105
1388
  }
1106
- await autoPushSyncedSkills(appServer);
1389
+ autoPushSyncedSkills(appServer).catch(() => {
1390
+ });
1107
1391
  try {
1108
- await appServer.rpc("skills/list", { forceReload: true });
1392
+ await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
1109
1393
  } catch {
1110
1394
  }
1111
1395
  setJson(res, 200, { ok: true, deletedPath: target });
@@ -1117,7 +1401,8 @@ async function handleSkillsRoutes(req, res, url, context) {
1117
1401
  return false;
1118
1402
  }
1119
1403
 
1120
- // src/server/codexAppServerBridge.ts
1404
+ // src/server/telegramThreadBridge.ts
1405
+ import { basename } from "path";
1121
1406
  function asRecord2(value) {
1122
1407
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1123
1408
  }
@@ -1135,21 +1420,425 @@ function getErrorMessage2(payload, fallback) {
1135
1420
  }
1136
1421
  return fallback;
1137
1422
  }
1423
+ var TelegramThreadBridge = class {
1424
+ constructor(appServer) {
1425
+ this.threadIdByChatId = /* @__PURE__ */ new Map();
1426
+ this.chatIdsByThreadId = /* @__PURE__ */ new Map();
1427
+ this.lastForwardedTurnByThreadId = /* @__PURE__ */ new Map();
1428
+ this.active = false;
1429
+ this.pollingTask = null;
1430
+ this.nextUpdateOffset = 0;
1431
+ this.lastError = "";
1432
+ this.appServer = appServer;
1433
+ this.token = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
1434
+ this.defaultCwd = process.env.TELEGRAM_DEFAULT_CWD?.trim() ?? process.cwd();
1435
+ }
1436
+ start() {
1437
+ if (!this.token || this.active) return;
1438
+ this.active = true;
1439
+ void this.notifyOnlineForKnownChats().catch(() => {
1440
+ });
1441
+ this.pollingTask = this.pollLoop();
1442
+ this.appServer.onNotification((notification) => {
1443
+ void this.handleNotification(notification).catch(() => {
1444
+ });
1445
+ });
1446
+ }
1447
+ stop() {
1448
+ this.active = false;
1449
+ }
1450
+ async pollLoop() {
1451
+ while (this.active) {
1452
+ try {
1453
+ const updates = await this.getUpdates();
1454
+ this.lastError = "";
1455
+ for (const update of updates) {
1456
+ const updateId = typeof update.update_id === "number" ? update.update_id : -1;
1457
+ if (updateId >= 0) {
1458
+ this.nextUpdateOffset = Math.max(this.nextUpdateOffset, updateId + 1);
1459
+ }
1460
+ await this.handleIncomingUpdate(update);
1461
+ }
1462
+ } catch (error) {
1463
+ this.lastError = getErrorMessage2(error, "Telegram polling failed");
1464
+ await new Promise((resolve3) => setTimeout(resolve3, 1500));
1465
+ }
1466
+ }
1467
+ }
1468
+ async getUpdates() {
1469
+ if (!this.token) {
1470
+ throw new Error("Telegram bot token is not configured");
1471
+ }
1472
+ const response = await fetch(this.apiUrl("getUpdates"), {
1473
+ method: "POST",
1474
+ headers: { "Content-Type": "application/json" },
1475
+ body: JSON.stringify({
1476
+ timeout: 45,
1477
+ offset: this.nextUpdateOffset,
1478
+ allowed_updates: ["message", "callback_query"]
1479
+ })
1480
+ });
1481
+ const payload = asRecord2(await response.json());
1482
+ const result = Array.isArray(payload?.result) ? payload.result : [];
1483
+ return result;
1484
+ }
1485
+ apiUrl(method) {
1486
+ return `https://api.telegram.org/bot${this.token}/${method}`;
1487
+ }
1488
+ configureToken(token) {
1489
+ const normalizedToken = token.trim();
1490
+ if (!normalizedToken) {
1491
+ throw new Error("Telegram bot token is required");
1492
+ }
1493
+ this.token = normalizedToken;
1494
+ }
1495
+ getStatus() {
1496
+ return {
1497
+ configured: this.token.length > 0,
1498
+ active: this.active,
1499
+ mappedChats: this.threadIdByChatId.size,
1500
+ mappedThreads: this.chatIdsByThreadId.size,
1501
+ lastError: this.lastError
1502
+ };
1503
+ }
1504
+ connectThread(threadId, chatId, token) {
1505
+ const normalizedThreadId = threadId.trim();
1506
+ if (!normalizedThreadId) {
1507
+ throw new Error("threadId is required");
1508
+ }
1509
+ if (!Number.isFinite(chatId)) {
1510
+ throw new Error("chatId must be a number");
1511
+ }
1512
+ if (typeof token === "string" && token.trim().length > 0) {
1513
+ this.configureToken(token);
1514
+ }
1515
+ if (!this.token) {
1516
+ throw new Error("Telegram bot token is not configured");
1517
+ }
1518
+ this.bindChatToThread(chatId, normalizedThreadId);
1519
+ this.start();
1520
+ void this.sendOnlineMessage(chatId).catch(() => {
1521
+ });
1522
+ }
1523
+ async sendTelegramMessage(chatId, text, options = {}) {
1524
+ const message = text.trim();
1525
+ if (!message) return;
1526
+ const payload = { chat_id: chatId, text: message };
1527
+ if (options.replyMarkup) {
1528
+ payload.reply_markup = options.replyMarkup;
1529
+ }
1530
+ await fetch(this.apiUrl("sendMessage"), {
1531
+ method: "POST",
1532
+ headers: { "Content-Type": "application/json" },
1533
+ body: JSON.stringify(payload)
1534
+ });
1535
+ }
1536
+ async sendOnlineMessage(chatId) {
1537
+ await this.sendTelegramMessage(chatId, "Codex thread bridge went online.");
1538
+ }
1539
+ async notifyOnlineForKnownChats() {
1540
+ const knownChatIds = Array.from(this.threadIdByChatId.keys());
1541
+ for (const chatId of knownChatIds) {
1542
+ await this.sendOnlineMessage(chatId);
1543
+ }
1544
+ }
1545
+ async handleIncomingUpdate(update) {
1546
+ if (update.callback_query) {
1547
+ await this.handleCallbackQuery(update.callback_query);
1548
+ return;
1549
+ }
1550
+ const message = update.message;
1551
+ const chatId = message?.chat?.id;
1552
+ const text = message?.text?.trim();
1553
+ if (typeof chatId !== "number" || !text) return;
1554
+ if (text === "/start") {
1555
+ await this.sendThreadPicker(chatId);
1556
+ return;
1557
+ }
1558
+ if (text === "/newthread") {
1559
+ const threadId2 = await this.createThreadForChat(chatId);
1560
+ await this.sendTelegramMessage(chatId, `Mapped to new thread: ${threadId2}`);
1561
+ return;
1562
+ }
1563
+ const threadCommand = text.match(/^\/thread\s+(\S+)$/);
1564
+ if (threadCommand) {
1565
+ const threadId2 = threadCommand[1];
1566
+ this.bindChatToThread(chatId, threadId2);
1567
+ await this.sendTelegramMessage(chatId, `Mapped to thread: ${threadId2}`);
1568
+ return;
1569
+ }
1570
+ const threadId = await this.ensureThreadForChat(chatId);
1571
+ try {
1572
+ await this.appServer.rpc("turn/start", {
1573
+ threadId,
1574
+ input: [{ type: "text", text }]
1575
+ });
1576
+ } catch (error) {
1577
+ const message2 = getErrorMessage2(error, "Failed to forward message to thread");
1578
+ await this.sendTelegramMessage(chatId, `Forward failed: ${message2}`);
1579
+ }
1580
+ }
1581
+ async handleCallbackQuery(callbackQuery) {
1582
+ const callbackId = typeof callbackQuery.id === "string" ? callbackQuery.id : "";
1583
+ const data = typeof callbackQuery.data === "string" ? callbackQuery.data : "";
1584
+ const chatId = callbackQuery.message?.chat?.id;
1585
+ if (!callbackId) return;
1586
+ if (!data.startsWith("thread:") || typeof chatId !== "number") {
1587
+ await this.answerCallbackQuery(callbackId, "Invalid selection");
1588
+ return;
1589
+ }
1590
+ const threadId = data.slice("thread:".length).trim();
1591
+ if (!threadId) {
1592
+ await this.answerCallbackQuery(callbackId, "Invalid thread id");
1593
+ return;
1594
+ }
1595
+ this.bindChatToThread(chatId, threadId);
1596
+ await this.answerCallbackQuery(callbackId, "Thread connected");
1597
+ await this.sendTelegramMessage(chatId, `Connected to thread: ${threadId}`);
1598
+ const history = await this.readThreadHistorySummary(threadId);
1599
+ if (history) {
1600
+ await this.sendTelegramMessage(chatId, history);
1601
+ }
1602
+ }
1603
+ async answerCallbackQuery(callbackQueryId, text) {
1604
+ await fetch(this.apiUrl("answerCallbackQuery"), {
1605
+ method: "POST",
1606
+ headers: { "Content-Type": "application/json" },
1607
+ body: JSON.stringify({
1608
+ callback_query_id: callbackQueryId,
1609
+ text
1610
+ })
1611
+ });
1612
+ }
1613
+ async sendThreadPicker(chatId) {
1614
+ const threads = await this.listRecentThreads();
1615
+ if (threads.length === 0) {
1616
+ await this.sendTelegramMessage(chatId, "No threads found. Send /newthread to create one.");
1617
+ return;
1618
+ }
1619
+ const inlineKeyboard = threads.map((thread) => [
1620
+ {
1621
+ text: thread.title,
1622
+ callback_data: `thread:${thread.id}`
1623
+ }
1624
+ ]);
1625
+ await this.sendTelegramMessage(chatId, "Select a thread to connect:", {
1626
+ replyMarkup: { inline_keyboard: inlineKeyboard }
1627
+ });
1628
+ }
1629
+ async listRecentThreads() {
1630
+ const payload = asRecord2(await this.appServer.rpc("thread/list", {
1631
+ archived: false,
1632
+ limit: 20,
1633
+ sortKey: "updated_at"
1634
+ }));
1635
+ const rows = Array.isArray(payload?.data) ? payload.data : [];
1636
+ const threads = [];
1637
+ for (const row of rows) {
1638
+ const record = asRecord2(row);
1639
+ const id = typeof record?.id === "string" ? record.id.trim() : "";
1640
+ if (!id) continue;
1641
+ const name = typeof record?.name === "string" ? record.name.trim() : "";
1642
+ const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
1643
+ const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
1644
+ const projectName = cwd ? basename(cwd) : "project";
1645
+ const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
1646
+ const title = `${projectName}/${threadTitle}`.slice(0, 64);
1647
+ threads.push({ id, title });
1648
+ }
1649
+ return threads;
1650
+ }
1651
+ async createThreadForChat(chatId) {
1652
+ const response = asRecord2(await this.appServer.rpc("thread/start", { cwd: this.defaultCwd }));
1653
+ const thread = asRecord2(response?.thread);
1654
+ const threadId = typeof thread?.id === "string" ? thread.id : "";
1655
+ if (!threadId) {
1656
+ throw new Error("thread/start did not return thread id");
1657
+ }
1658
+ this.bindChatToThread(chatId, threadId);
1659
+ return threadId;
1660
+ }
1661
+ async ensureThreadForChat(chatId) {
1662
+ const existing = this.threadIdByChatId.get(chatId);
1663
+ if (existing) return existing;
1664
+ return this.createThreadForChat(chatId);
1665
+ }
1666
+ bindChatToThread(chatId, threadId) {
1667
+ const previousThreadId = this.threadIdByChatId.get(chatId);
1668
+ if (previousThreadId && previousThreadId !== threadId) {
1669
+ const previousSet = this.chatIdsByThreadId.get(previousThreadId);
1670
+ previousSet?.delete(chatId);
1671
+ if (previousSet && previousSet.size === 0) {
1672
+ this.chatIdsByThreadId.delete(previousThreadId);
1673
+ }
1674
+ }
1675
+ this.threadIdByChatId.set(chatId, threadId);
1676
+ const chatIds = this.chatIdsByThreadId.get(threadId) ?? /* @__PURE__ */ new Set();
1677
+ chatIds.add(chatId);
1678
+ this.chatIdsByThreadId.set(threadId, chatIds);
1679
+ }
1680
+ extractThreadId(notification) {
1681
+ const params = asRecord2(notification.params);
1682
+ if (!params) return "";
1683
+ const directThreadId = typeof params.threadId === "string" ? params.threadId : "";
1684
+ if (directThreadId) return directThreadId;
1685
+ const turn = asRecord2(params.turn);
1686
+ const turnThreadId = typeof turn?.threadId === "string" ? turn.threadId : "";
1687
+ return turnThreadId;
1688
+ }
1689
+ extractTurnId(notification) {
1690
+ const params = asRecord2(notification.params);
1691
+ if (!params) return "";
1692
+ const directTurnId = typeof params.turnId === "string" ? params.turnId : "";
1693
+ if (directTurnId) return directTurnId;
1694
+ const turn = asRecord2(params.turn);
1695
+ const turnId = typeof turn?.id === "string" ? turn.id : "";
1696
+ return turnId;
1697
+ }
1698
+ async handleNotification(notification) {
1699
+ if (notification.method !== "turn/completed") return;
1700
+ const threadId = this.extractThreadId(notification);
1701
+ if (!threadId) return;
1702
+ const chatIds = this.chatIdsByThreadId.get(threadId);
1703
+ if (!chatIds || chatIds.size === 0) return;
1704
+ const turnId = this.extractTurnId(notification);
1705
+ const lastForwardedTurnId = this.lastForwardedTurnByThreadId.get(threadId);
1706
+ if (turnId && lastForwardedTurnId === turnId) return;
1707
+ const assistantReply = await this.readLatestAssistantMessage(threadId);
1708
+ if (!assistantReply) return;
1709
+ for (const chatId of chatIds) {
1710
+ await this.sendTelegramMessage(chatId, assistantReply);
1711
+ }
1712
+ if (turnId) {
1713
+ this.lastForwardedTurnByThreadId.set(threadId, turnId);
1714
+ }
1715
+ }
1716
+ async readLatestAssistantMessage(threadId) {
1717
+ const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
1718
+ const thread = asRecord2(response?.thread);
1719
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1720
+ for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
1721
+ const turn = asRecord2(turns[turnIndex]);
1722
+ const items = Array.isArray(turn?.items) ? turn.items : [];
1723
+ for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex -= 1) {
1724
+ const item = asRecord2(items[itemIndex]);
1725
+ if (item?.type === "agentMessage") {
1726
+ const text = typeof item.text === "string" ? item.text.trim() : "";
1727
+ if (text) return text;
1728
+ }
1729
+ }
1730
+ }
1731
+ return "";
1732
+ }
1733
+ async readThreadHistorySummary(threadId) {
1734
+ const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
1735
+ const thread = asRecord2(response?.thread);
1736
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1737
+ const historyRows = [];
1738
+ for (const turn of turns) {
1739
+ const turnRecord = asRecord2(turn);
1740
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1741
+ for (const item of items) {
1742
+ const itemRecord = asRecord2(item);
1743
+ const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1744
+ if (type === "userMessage") {
1745
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1746
+ for (const block of content) {
1747
+ const blockRecord = asRecord2(block);
1748
+ if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim()) {
1749
+ historyRows.push(`User: ${blockRecord.text.trim()}`);
1750
+ }
1751
+ }
1752
+ }
1753
+ if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim()) {
1754
+ historyRows.push(`Assistant: ${itemRecord.text.trim()}`);
1755
+ }
1756
+ }
1757
+ }
1758
+ if (historyRows.length === 0) {
1759
+ return "Thread has no message history yet.";
1760
+ }
1761
+ const tail = historyRows.slice(-12).join("\n\n");
1762
+ const maxLen = 3800;
1763
+ const summary = tail.length > maxLen ? tail.slice(tail.length - maxLen) : tail;
1764
+ return `Recent history:
1765
+
1766
+ ${summary}`;
1767
+ }
1768
+ };
1769
+
1770
+ // src/utils/commandInvocation.ts
1771
+ import { spawnSync as spawnSync2 } from "child_process";
1772
+ import { basename as basename2, extname } from "path";
1773
+ var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
1774
+ function quoteCmdExeArg(value) {
1775
+ const normalized = value.replace(/"/g, '""');
1776
+ if (!/[\s"]/u.test(normalized)) {
1777
+ return normalized;
1778
+ }
1779
+ return `"${normalized}"`;
1780
+ }
1781
+ function needsCmdExeWrapper(command) {
1782
+ if (process.platform !== "win32") {
1783
+ return false;
1784
+ }
1785
+ const lowerCommand = command.toLowerCase();
1786
+ const baseName = basename2(lowerCommand);
1787
+ if (/\.(cmd|bat)$/i.test(baseName)) {
1788
+ return true;
1789
+ }
1790
+ if (extname(baseName)) {
1791
+ return false;
1792
+ }
1793
+ return WINDOWS_CMD_NAMES.has(baseName);
1794
+ }
1795
+ function getSpawnInvocation(command, args = []) {
1796
+ if (needsCmdExeWrapper(command)) {
1797
+ return {
1798
+ command: "cmd.exe",
1799
+ args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
1800
+ };
1801
+ }
1802
+ return { command, args };
1803
+ }
1804
+ function spawnSyncCommand(command, args = [], options = {}) {
1805
+ const invocation = getSpawnInvocation(command, args);
1806
+ return spawnSync2(invocation.command, invocation.args, options);
1807
+ }
1808
+
1809
+ // src/server/codexAppServerBridge.ts
1810
+ function asRecord3(value) {
1811
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1812
+ }
1813
+ function getErrorMessage3(payload, fallback) {
1814
+ if (payload instanceof Error && payload.message.trim().length > 0) {
1815
+ return payload.message;
1816
+ }
1817
+ const record = asRecord3(payload);
1818
+ if (!record) return fallback;
1819
+ const error = record.error;
1820
+ if (typeof error === "string" && error.length > 0) return error;
1821
+ const nestedError = asRecord3(error);
1822
+ if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
1823
+ return nestedError.message;
1824
+ }
1825
+ return fallback;
1826
+ }
1138
1827
  function setJson2(res, statusCode, payload) {
1139
1828
  res.statusCode = statusCode;
1140
1829
  res.setHeader("Content-Type", "application/json; charset=utf-8");
1141
1830
  res.end(JSON.stringify(payload));
1142
1831
  }
1143
1832
  function extractThreadMessageText(threadReadPayload) {
1144
- const payload = asRecord2(threadReadPayload);
1145
- const thread = asRecord2(payload?.thread);
1833
+ const payload = asRecord3(threadReadPayload);
1834
+ const thread = asRecord3(payload?.thread);
1146
1835
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1147
1836
  const parts = [];
1148
1837
  for (const turn of turns) {
1149
- const turnRecord = asRecord2(turn);
1838
+ const turnRecord = asRecord3(turn);
1150
1839
  const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1151
1840
  for (const item of items) {
1152
- const itemRecord = asRecord2(item);
1841
+ const itemRecord = asRecord3(item);
1153
1842
  const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1154
1843
  if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
1155
1844
  parts.push(itemRecord.text.trim());
@@ -1158,7 +1847,7 @@ function extractThreadMessageText(threadReadPayload) {
1158
1847
  if (type === "userMessage") {
1159
1848
  const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1160
1849
  for (const block of content) {
1161
- const blockRecord = asRecord2(block);
1850
+ const blockRecord = asRecord3(block);
1162
1851
  if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
1163
1852
  parts.push(blockRecord.text.trim());
1164
1853
  }
@@ -1192,9 +1881,62 @@ function scoreFileCandidate(path, query) {
1192
1881
  if (lowerPath.includes(lowerQuery)) return 4;
1193
1882
  return 10;
1194
1883
  }
1884
+ function decodeHtmlEntities(value) {
1885
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/gi, "/");
1886
+ }
1887
+ function stripHtml(value) {
1888
+ return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
1889
+ }
1890
+ function parseGithubTrendingHtml(html, limit) {
1891
+ const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
1892
+ const items = [];
1893
+ let seq = Date.now();
1894
+ for (const row of rows) {
1895
+ const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
1896
+ const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
1897
+ if (!hrefMatch) continue;
1898
+ const fullName = hrefMatch[1] ?? "";
1899
+ if (!fullName || items.some((item) => item.fullName === fullName)) continue;
1900
+ 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>/);
1901
+ const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
1902
+ const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
1903
+ const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
1904
+ const stars = Number.parseInt(starsText, 10);
1905
+ items.push({
1906
+ id: seq,
1907
+ fullName,
1908
+ url: `https://github.com/${fullName}`,
1909
+ description: stripHtml(descriptionMatch?.[1] ?? ""),
1910
+ language: stripHtml(languageMatch?.[1] ?? ""),
1911
+ stars: Number.isFinite(stars) ? stars : 0
1912
+ });
1913
+ seq += 1;
1914
+ if (items.length >= limit) break;
1915
+ }
1916
+ return items;
1917
+ }
1918
+ async function fetchGithubTrending(since, limit) {
1919
+ const endpoint = `https://github.com/trending?since=${since}`;
1920
+ const response = await fetch(endpoint, {
1921
+ headers: {
1922
+ "User-Agent": "codex-web-local",
1923
+ Accept: "text/html"
1924
+ }
1925
+ });
1926
+ if (!response.ok) {
1927
+ throw new Error(`GitHub trending fetch failed (${response.status})`);
1928
+ }
1929
+ const html = await response.text();
1930
+ return parseGithubTrendingHtml(html, limit);
1931
+ }
1195
1932
  async function listFilesWithRipgrep(cwd) {
1196
1933
  return await new Promise((resolve3, reject) => {
1197
- const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1934
+ const ripgrepCommand = resolveRipgrepCommand();
1935
+ if (!ripgrepCommand) {
1936
+ reject(new Error("ripgrep (rg) is not available"));
1937
+ return;
1938
+ }
1939
+ const proc = spawn2(ripgrepCommand, ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1198
1940
  cwd,
1199
1941
  env: process.env,
1200
1942
  stdio: ["ignore", "pipe", "pipe"]
@@ -1221,7 +1963,7 @@ async function listFilesWithRipgrep(cwd) {
1221
1963
  }
1222
1964
  function getCodexHomeDir2() {
1223
1965
  const codexHome = process.env.CODEX_HOME?.trim();
1224
- return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1966
+ return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
1225
1967
  }
1226
1968
  async function runCommand2(command, args, options = {}) {
1227
1969
  await new Promise((resolve3, reject) => {
@@ -1251,15 +1993,15 @@ async function runCommand2(command, args, options = {}) {
1251
1993
  });
1252
1994
  }
1253
1995
  function isMissingHeadError(error) {
1254
- const message = getErrorMessage2(error, "").toLowerCase();
1996
+ const message = getErrorMessage3(error, "").toLowerCase();
1255
1997
  return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
1256
1998
  }
1257
1999
  function isNotGitRepositoryError(error) {
1258
- const message = getErrorMessage2(error, "").toLowerCase();
2000
+ const message = getErrorMessage3(error, "").toLowerCase();
1259
2001
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
1260
2002
  }
1261
2003
  async function ensureRepoHasInitialCommit(repoRoot) {
1262
- const agentsPath = join2(repoRoot, "AGENTS.md");
2004
+ const agentsPath = join3(repoRoot, "AGENTS.md");
1263
2005
  try {
1264
2006
  await stat2(agentsPath);
1265
2007
  } catch {
@@ -1299,6 +2041,33 @@ async function runCommandCapture(command, args, options = {}) {
1299
2041
  });
1300
2042
  });
1301
2043
  }
2044
+ async function runCommandWithOutput2(command, args, options = {}) {
2045
+ return await new Promise((resolve3, reject) => {
2046
+ const proc = spawn2(command, args, {
2047
+ cwd: options.cwd,
2048
+ env: process.env,
2049
+ stdio: ["ignore", "pipe", "pipe"]
2050
+ });
2051
+ let stdout = "";
2052
+ let stderr = "";
2053
+ proc.stdout.on("data", (chunk) => {
2054
+ stdout += chunk.toString();
2055
+ });
2056
+ proc.stderr.on("data", (chunk) => {
2057
+ stderr += chunk.toString();
2058
+ });
2059
+ proc.on("error", reject);
2060
+ proc.on("close", (code) => {
2061
+ if (code === 0) {
2062
+ resolve3(stdout.trim());
2063
+ return;
2064
+ }
2065
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
2066
+ const suffix = details.length > 0 ? `: ${details}` : "";
2067
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
2068
+ });
2069
+ });
2070
+ }
1302
2071
  function normalizeStringArray(value) {
1303
2072
  if (!Array.isArray(value)) return [];
1304
2073
  const normalized = [];
@@ -1319,8 +2088,88 @@ function normalizeStringRecord(value) {
1319
2088
  }
1320
2089
  return next;
1321
2090
  }
2091
+ function normalizeCommitMessage(value) {
2092
+ if (typeof value !== "string") return "";
2093
+ const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
2094
+ return normalized.slice(0, 2e3);
2095
+ }
2096
+ function getRollbackGitDirForCwd(cwd) {
2097
+ return join3(cwd, ".codex", "rollbacks", ".git");
2098
+ }
2099
+ async function ensureLocalCodexGitignoreHasRollbacks(cwd) {
2100
+ const localCodexDir = join3(cwd, ".codex");
2101
+ const gitignorePath = join3(localCodexDir, ".gitignore");
2102
+ await mkdir2(localCodexDir, { recursive: true });
2103
+ let current = "";
2104
+ try {
2105
+ current = await readFile2(gitignorePath, "utf8");
2106
+ } catch {
2107
+ current = "";
2108
+ }
2109
+ const rows = current.split(/\r?\n/).map((line) => line.trim());
2110
+ if (rows.includes("rollbacks/")) return;
2111
+ const prefix = current.length > 0 && !current.endsWith("\n") ? `${current}
2112
+ ` : current;
2113
+ await writeFile2(gitignorePath, `${prefix}rollbacks/
2114
+ `, "utf8");
2115
+ }
2116
+ async function ensureRollbackGitRepo(cwd) {
2117
+ const gitDir = getRollbackGitDirForCwd(cwd);
2118
+ try {
2119
+ const headInfo = await stat2(join3(gitDir, "HEAD"));
2120
+ if (!headInfo.isFile()) {
2121
+ throw new Error("Invalid rollback git repository");
2122
+ }
2123
+ } catch {
2124
+ await mkdir2(dirname(gitDir), { recursive: true });
2125
+ await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, "init"]);
2126
+ }
2127
+ await runCommand2("git", ["--git-dir", gitDir, "config", "user.email", "codex@local"]);
2128
+ await runCommand2("git", ["--git-dir", gitDir, "config", "user.name", "Codex Rollback"]);
2129
+ try {
2130
+ await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, "rev-parse", "--verify", "HEAD"]);
2131
+ } catch {
2132
+ await runCommand2(
2133
+ "git",
2134
+ ["--git-dir", gitDir, "--work-tree", cwd, "commit", "--allow-empty", "-m", "Initialize rollback history"]
2135
+ );
2136
+ }
2137
+ await ensureLocalCodexGitignoreHasRollbacks(cwd);
2138
+ return gitDir;
2139
+ }
2140
+ async function runRollbackGit(cwd, args) {
2141
+ const gitDir = await ensureRollbackGitRepo(cwd);
2142
+ await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2143
+ }
2144
+ async function runRollbackGitCapture(cwd, args) {
2145
+ const gitDir = await ensureRollbackGitRepo(cwd);
2146
+ return await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2147
+ }
2148
+ async function runRollbackGitWithOutput(cwd, args) {
2149
+ const gitDir = await ensureRollbackGitRepo(cwd);
2150
+ return await runCommandWithOutput2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2151
+ }
2152
+ async function hasRollbackGitWorkingTreeChanges(cwd) {
2153
+ const status = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
2154
+ return status.trim().length > 0;
2155
+ }
2156
+ async function findRollbackCommitByExactMessage(cwd, message) {
2157
+ const normalizedTarget = normalizeCommitMessage(message);
2158
+ if (!normalizedTarget) return "";
2159
+ const raw = await runRollbackGitWithOutput(cwd, ["log", "--format=%H%x1f%B%x1e"]);
2160
+ const entries = raw.split("");
2161
+ for (const entry of entries) {
2162
+ if (!entry.trim()) continue;
2163
+ const [shaRaw, bodyRaw] = entry.split("");
2164
+ const sha = (shaRaw ?? "").trim();
2165
+ const body = normalizeCommitMessage(bodyRaw ?? "");
2166
+ if (!sha) continue;
2167
+ if (body === normalizedTarget) return sha;
2168
+ }
2169
+ return "";
2170
+ }
1322
2171
  function getCodexAuthPath() {
1323
- return join2(getCodexHomeDir2(), "auth.json");
2172
+ return join3(getCodexHomeDir2(), "auth.json");
1324
2173
  }
1325
2174
  async function readCodexAuth() {
1326
2175
  try {
@@ -1334,13 +2183,21 @@ async function readCodexAuth() {
1334
2183
  }
1335
2184
  }
1336
2185
  function getCodexGlobalStatePath() {
1337
- return join2(getCodexHomeDir2(), ".codex-global-state.json");
2186
+ return join3(getCodexHomeDir2(), ".codex-global-state.json");
2187
+ }
2188
+ function getCodexSessionIndexPath() {
2189
+ return join3(getCodexHomeDir2(), "session_index.jsonl");
1338
2190
  }
1339
2191
  var MAX_THREAD_TITLES = 500;
2192
+ var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
2193
+ var sessionIndexThreadTitleCacheState = {
2194
+ fileSignature: null,
2195
+ cache: EMPTY_THREAD_TITLE_CACHE
2196
+ };
1340
2197
  function normalizeThreadTitleCache(value) {
1341
- const record = asRecord2(value);
1342
- if (!record) return { titles: {}, order: [] };
1343
- const rawTitles = asRecord2(record.titles);
2198
+ const record = asRecord3(value);
2199
+ if (!record) return EMPTY_THREAD_TITLE_CACHE;
2200
+ const rawTitles = asRecord3(record.titles);
1344
2201
  const titles = {};
1345
2202
  if (rawTitles) {
1346
2203
  for (const [k, v] of Object.entries(rawTitles)) {
@@ -1363,14 +2220,55 @@ function removeFromThreadTitleCache(cache, id) {
1363
2220
  const { [id]: _, ...titles } = cache.titles;
1364
2221
  return { titles, order: cache.order.filter((o) => o !== id) };
1365
2222
  }
2223
+ function normalizeSessionIndexThreadTitle(value) {
2224
+ const record = asRecord3(value);
2225
+ if (!record) return null;
2226
+ const id = typeof record.id === "string" ? record.id.trim() : "";
2227
+ const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
2228
+ const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
2229
+ const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
2230
+ if (!id || !title) return null;
2231
+ return {
2232
+ id,
2233
+ title,
2234
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
2235
+ };
2236
+ }
2237
+ function trimThreadTitleCache(cache) {
2238
+ const titles = { ...cache.titles };
2239
+ const order = cache.order.filter((id) => {
2240
+ if (!titles[id]) return false;
2241
+ return true;
2242
+ }).slice(0, MAX_THREAD_TITLES);
2243
+ for (const id of Object.keys(titles)) {
2244
+ if (!order.includes(id)) {
2245
+ delete titles[id];
2246
+ }
2247
+ }
2248
+ return { titles, order };
2249
+ }
2250
+ function mergeThreadTitleCaches(base, overlay) {
2251
+ const titles = { ...base.titles, ...overlay.titles };
2252
+ const order = [];
2253
+ for (const id of [...overlay.order, ...base.order]) {
2254
+ if (!titles[id] || order.includes(id)) continue;
2255
+ order.push(id);
2256
+ }
2257
+ for (const id of Object.keys(titles)) {
2258
+ if (!order.includes(id)) {
2259
+ order.push(id);
2260
+ }
2261
+ }
2262
+ return trimThreadTitleCache({ titles, order });
2263
+ }
1366
2264
  async function readThreadTitleCache() {
1367
2265
  const statePath = getCodexGlobalStatePath();
1368
2266
  try {
1369
2267
  const raw = await readFile2(statePath, "utf8");
1370
- const payload = asRecord2(JSON.parse(raw)) ?? {};
2268
+ const payload = asRecord3(JSON.parse(raw)) ?? {};
1371
2269
  return normalizeThreadTitleCache(payload["thread-titles"]);
1372
2270
  } catch {
1373
- return { titles: {}, order: [] };
2271
+ return EMPTY_THREAD_TITLE_CACHE;
1374
2272
  }
1375
2273
  }
1376
2274
  async function writeThreadTitleCache(cache) {
@@ -1378,20 +2276,83 @@ async function writeThreadTitleCache(cache) {
1378
2276
  let payload = {};
1379
2277
  try {
1380
2278
  const raw = await readFile2(statePath, "utf8");
1381
- payload = asRecord2(JSON.parse(raw)) ?? {};
2279
+ payload = asRecord3(JSON.parse(raw)) ?? {};
1382
2280
  } catch {
1383
2281
  payload = {};
1384
2282
  }
1385
2283
  payload["thread-titles"] = cache;
1386
2284
  await writeFile2(statePath, JSON.stringify(payload), "utf8");
1387
2285
  }
2286
+ function getSessionIndexFileSignature(stats) {
2287
+ return `${String(stats.mtimeMs)}:${String(stats.size)}`;
2288
+ }
2289
+ async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
2290
+ const latestById = /* @__PURE__ */ new Map();
2291
+ const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
2292
+ const lines = createInterface({
2293
+ input,
2294
+ crlfDelay: Infinity
2295
+ });
2296
+ try {
2297
+ for await (const line of lines) {
2298
+ const trimmed = line.trim();
2299
+ if (!trimmed) continue;
2300
+ try {
2301
+ const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
2302
+ if (!entry) continue;
2303
+ const previous = latestById.get(entry.id);
2304
+ if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
2305
+ latestById.set(entry.id, entry);
2306
+ }
2307
+ } catch {
2308
+ }
2309
+ }
2310
+ } finally {
2311
+ lines.close();
2312
+ input.close();
2313
+ }
2314
+ const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
2315
+ const titles = {};
2316
+ const order = [];
2317
+ for (const entry of entries) {
2318
+ titles[entry.id] = entry.title;
2319
+ order.push(entry.id);
2320
+ }
2321
+ return trimThreadTitleCache({ titles, order });
2322
+ }
2323
+ async function readThreadTitlesFromSessionIndex() {
2324
+ const sessionIndexPath = getCodexSessionIndexPath();
2325
+ try {
2326
+ const stats = await stat2(sessionIndexPath);
2327
+ const fileSignature = getSessionIndexFileSignature(stats);
2328
+ if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
2329
+ return sessionIndexThreadTitleCacheState.cache;
2330
+ }
2331
+ const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
2332
+ sessionIndexThreadTitleCacheState = { fileSignature, cache };
2333
+ return cache;
2334
+ } catch {
2335
+ sessionIndexThreadTitleCacheState = {
2336
+ fileSignature: "missing",
2337
+ cache: EMPTY_THREAD_TITLE_CACHE
2338
+ };
2339
+ return sessionIndexThreadTitleCacheState.cache;
2340
+ }
2341
+ }
2342
+ async function readMergedThreadTitleCache() {
2343
+ const [sessionIndexCache, persistedCache] = await Promise.all([
2344
+ readThreadTitlesFromSessionIndex(),
2345
+ readThreadTitleCache()
2346
+ ]);
2347
+ return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
2348
+ }
1388
2349
  async function readWorkspaceRootsState() {
1389
2350
  const statePath = getCodexGlobalStatePath();
1390
2351
  let payload = {};
1391
2352
  try {
1392
2353
  const raw = await readFile2(statePath, "utf8");
1393
2354
  const parsed = JSON.parse(raw);
1394
- payload = asRecord2(parsed) ?? {};
2355
+ payload = asRecord3(parsed) ?? {};
1395
2356
  } catch {
1396
2357
  payload = {};
1397
2358
  }
@@ -1406,7 +2367,7 @@ async function writeWorkspaceRootsState(nextState) {
1406
2367
  let payload = {};
1407
2368
  try {
1408
2369
  const raw = await readFile2(statePath, "utf8");
1409
- payload = asRecord2(JSON.parse(raw)) ?? {};
2370
+ payload = asRecord3(JSON.parse(raw)) ?? {};
1410
2371
  } catch {
1411
2372
  payload = {};
1412
2373
  }
@@ -1415,6 +2376,36 @@ async function writeWorkspaceRootsState(nextState) {
1415
2376
  payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
1416
2377
  await writeFile2(statePath, JSON.stringify(payload), "utf8");
1417
2378
  }
2379
+ function normalizeTelegramBridgeConfig(value) {
2380
+ const record = asRecord3(value);
2381
+ if (!record) return { botToken: "" };
2382
+ const botToken = typeof record.botToken === "string" ? record.botToken.trim() : "";
2383
+ return { botToken };
2384
+ }
2385
+ async function readTelegramBridgeConfig() {
2386
+ const statePath = getCodexGlobalStatePath();
2387
+ try {
2388
+ const raw = await readFile2(statePath, "utf8");
2389
+ const payload = asRecord3(JSON.parse(raw)) ?? {};
2390
+ return normalizeTelegramBridgeConfig(payload["telegram-bridge"]);
2391
+ } catch {
2392
+ return { botToken: "" };
2393
+ }
2394
+ }
2395
+ async function writeTelegramBridgeConfig(nextState) {
2396
+ const statePath = getCodexGlobalStatePath();
2397
+ let payload = {};
2398
+ try {
2399
+ const raw = await readFile2(statePath, "utf8");
2400
+ payload = asRecord3(JSON.parse(raw)) ?? {};
2401
+ } catch {
2402
+ payload = {};
2403
+ }
2404
+ payload["telegram-bridge"] = {
2405
+ botToken: nextState.botToken.trim()
2406
+ };
2407
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
2408
+ }
1418
2409
  async function readJsonBody(req) {
1419
2410
  const raw = await readRawBody(req);
1420
2411
  if (raw.length === 0) return null;
@@ -1484,46 +2475,93 @@ function handleFileUpload(req, res) {
1484
2475
  setJson2(res, 400, { error: "No file in request" });
1485
2476
  return;
1486
2477
  }
1487
- const uploadDir = join2(tmpdir2(), "codex-web-uploads");
2478
+ const uploadDir = join3(tmpdir2(), "codex-web-uploads");
1488
2479
  await mkdir2(uploadDir, { recursive: true });
1489
- const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1490
- const destPath = join2(destDir, fileName);
2480
+ const destDir = await mkdtemp2(join3(uploadDir, "f-"));
2481
+ const destPath = join3(destDir, fileName);
1491
2482
  await writeFile2(destPath, fileData);
1492
2483
  setJson2(res, 200, { path: destPath });
1493
2484
  } catch (err) {
1494
- setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
2485
+ setJson2(res, 500, { error: getErrorMessage3(err, "Upload failed") });
1495
2486
  }
1496
2487
  });
1497
2488
  req.on("error", (err) => {
1498
- setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
2489
+ setJson2(res, 500, { error: getErrorMessage3(err, "Upload stream error") });
2490
+ });
2491
+ }
2492
+ function httpPost(url, headers, body) {
2493
+ const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
2494
+ return new Promise((resolve3, reject) => {
2495
+ const req = doRequest(url, { method: "POST", headers }, (res) => {
2496
+ const chunks = [];
2497
+ res.on("data", (c) => chunks.push(c));
2498
+ res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
2499
+ res.on("error", reject);
2500
+ });
2501
+ req.on("error", reject);
2502
+ req.write(body);
2503
+ req.end();
2504
+ });
2505
+ }
2506
+ var curlImpersonateAvailable = null;
2507
+ function curlImpersonatePost(url, headers, body) {
2508
+ return new Promise((resolve3, reject) => {
2509
+ const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
2510
+ for (const [k, v] of Object.entries(headers)) {
2511
+ if (k.toLowerCase() === "content-length") continue;
2512
+ args.push("-H", `${k}: ${String(v)}`);
2513
+ }
2514
+ args.push("--data-binary", "@-");
2515
+ const proc = spawn2("curl-impersonate-chrome", args, {
2516
+ env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
2517
+ stdio: ["pipe", "pipe", "pipe"]
2518
+ });
2519
+ const chunks = [];
2520
+ proc.stdout.on("data", (c) => chunks.push(c));
2521
+ proc.on("error", (e) => {
2522
+ curlImpersonateAvailable = false;
2523
+ reject(e);
2524
+ });
2525
+ proc.on("close", (code) => {
2526
+ const raw = Buffer.concat(chunks).toString("utf8");
2527
+ const lastNewline = raw.lastIndexOf("\n");
2528
+ const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
2529
+ const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
2530
+ const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
2531
+ curlImpersonateAvailable = true;
2532
+ resolve3({ status, body: responseBody });
2533
+ });
2534
+ proc.stdin.write(body);
2535
+ proc.stdin.end();
1499
2536
  });
1500
2537
  }
1501
2538
  async function proxyTranscribe(body, contentType, authToken, accountId) {
1502
- const headers = {
2539
+ const chatgptHeaders = {
1503
2540
  "Content-Type": contentType,
1504
2541
  "Content-Length": body.length,
1505
2542
  Authorization: `Bearer ${authToken}`,
1506
2543
  originator: "Codex Desktop",
1507
2544
  "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
1508
2545
  };
1509
- if (accountId) {
1510
- headers["ChatGPT-Account-Id"] = accountId;
2546
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
2547
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
2548
+ let result;
2549
+ try {
2550
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
2551
+ } catch {
2552
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1511
2553
  }
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);
2554
+ if (result.status === 403 && result.body.includes("cf_chl")) {
2555
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
2556
+ try {
2557
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
2558
+ if (ciResult.status !== 403) return ciResult;
2559
+ } catch {
1521
2560
  }
1522
- );
1523
- req.on("error", reject);
1524
- req.write(body);
1525
- req.end();
1526
- });
2561
+ }
2562
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
2563
+ }
2564
+ return result;
1527
2565
  }
1528
2566
  var AppServerProcess = class {
1529
2567
  constructor() {
@@ -1544,10 +2582,18 @@ var AppServerProcess = class {
1544
2582
  'sandbox_mode="danger-full-access"'
1545
2583
  ];
1546
2584
  }
2585
+ getCodexCommand() {
2586
+ const codexCommand = resolveCodexCommand();
2587
+ if (!codexCommand) {
2588
+ throw new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND.");
2589
+ }
2590
+ return codexCommand;
2591
+ }
1547
2592
  start() {
1548
2593
  if (this.process) return;
1549
2594
  this.stopping = false;
1550
- const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
2595
+ const invocation = getSpawnInvocation(this.getCodexCommand(), this.appServerArgs);
2596
+ const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
1551
2597
  this.process = proc;
1552
2598
  proc.stdout.setEncoding("utf8");
1553
2599
  proc.stdout.on("data", (chunk) => {
@@ -1641,7 +2687,7 @@ var AppServerProcess = class {
1641
2687
  }
1642
2688
  this.pendingServerRequests.delete(requestId);
1643
2689
  this.sendServerRequestReply(requestId, reply);
1644
- const requestParams = asRecord2(pendingRequest.params);
2690
+ const requestParams = asRecord3(pendingRequest.params);
1645
2691
  const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
1646
2692
  this.emitNotification({
1647
2693
  method: "server/request/resolved",
@@ -1710,7 +2756,7 @@ var AppServerProcess = class {
1710
2756
  }
1711
2757
  async respondToServerRequest(payload) {
1712
2758
  await this.ensureInitialized();
1713
- const body = asRecord2(payload);
2759
+ const body = asRecord3(payload);
1714
2760
  if (!body) {
1715
2761
  throw new Error("Invalid response payload: expected object");
1716
2762
  }
@@ -1718,7 +2764,7 @@ var AppServerProcess = class {
1718
2764
  if (typeof id !== "number" || !Number.isInteger(id)) {
1719
2765
  throw new Error('Invalid response payload: "id" must be an integer');
1720
2766
  }
1721
- const rawError = asRecord2(body.error);
2767
+ const rawError = asRecord3(body.error);
1722
2768
  if (rawError) {
1723
2769
  const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
1724
2770
  const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
@@ -1773,7 +2819,13 @@ var MethodCatalog = class {
1773
2819
  }
1774
2820
  async runGenerateSchemaCommand(outDir) {
1775
2821
  await new Promise((resolve3, reject) => {
1776
- const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
2822
+ const codexCommand = resolveCodexCommand();
2823
+ if (!codexCommand) {
2824
+ reject(new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND."));
2825
+ return;
2826
+ }
2827
+ const invocation = getSpawnInvocation(codexCommand, ["app-server", "generate-json-schema", "--out", outDir]);
2828
+ const process2 = spawn2(invocation.command, invocation.args, {
1777
2829
  stdio: ["ignore", "ignore", "pipe"]
1778
2830
  });
1779
2831
  let stderr = "";
@@ -1792,13 +2844,13 @@ var MethodCatalog = class {
1792
2844
  });
1793
2845
  }
1794
2846
  extractMethodsFromClientRequest(payload) {
1795
- const root = asRecord2(payload);
2847
+ const root = asRecord3(payload);
1796
2848
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1797
2849
  const methods = /* @__PURE__ */ new Set();
1798
2850
  for (const entry of oneOf) {
1799
- const row = asRecord2(entry);
1800
- const properties = asRecord2(row?.properties);
1801
- const methodDef = asRecord2(properties?.method);
2851
+ const row = asRecord3(entry);
2852
+ const properties = asRecord3(row?.properties);
2853
+ const methodDef = asRecord3(properties?.method);
1802
2854
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1803
2855
  for (const item of methodEnum) {
1804
2856
  if (typeof item === "string" && item.length > 0) {
@@ -1809,13 +2861,13 @@ var MethodCatalog = class {
1809
2861
  return Array.from(methods).sort((a, b) => a.localeCompare(b));
1810
2862
  }
1811
2863
  extractMethodsFromServerNotification(payload) {
1812
- const root = asRecord2(payload);
2864
+ const root = asRecord3(payload);
1813
2865
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1814
2866
  const methods = /* @__PURE__ */ new Set();
1815
2867
  for (const entry of oneOf) {
1816
- const row = asRecord2(entry);
1817
- const properties = asRecord2(row?.properties);
1818
- const methodDef = asRecord2(properties?.method);
2868
+ const row = asRecord3(entry);
2869
+ const properties = asRecord3(row?.properties);
2870
+ const methodDef = asRecord3(properties?.method);
1819
2871
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1820
2872
  for (const item of methodEnum) {
1821
2873
  if (typeof item === "string" && item.length > 0) {
@@ -1829,9 +2881,9 @@ var MethodCatalog = class {
1829
2881
  if (this.methodCache) {
1830
2882
  return this.methodCache;
1831
2883
  }
1832
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2884
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
1833
2885
  await this.runGenerateSchemaCommand(outDir);
1834
- const clientRequestPath = join2(outDir, "ClientRequest.json");
2886
+ const clientRequestPath = join3(outDir, "ClientRequest.json");
1835
2887
  const raw = await readFile2(clientRequestPath, "utf8");
1836
2888
  const parsed = JSON.parse(raw);
1837
2889
  const methods = this.extractMethodsFromClientRequest(parsed);
@@ -1842,9 +2894,9 @@ var MethodCatalog = class {
1842
2894
  if (this.notificationCache) {
1843
2895
  return this.notificationCache;
1844
2896
  }
1845
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2897
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
1846
2898
  await this.runGenerateSchemaCommand(outDir);
1847
- const serverNotificationPath = join2(outDir, "ServerNotification.json");
2899
+ const serverNotificationPath = join3(outDir, "ServerNotification.json");
1848
2900
  const raw = await readFile2(serverNotificationPath, "utf8");
1849
2901
  const parsed = JSON.parse(raw);
1850
2902
  const methods = this.extractMethodsFromServerNotification(parsed);
@@ -1857,9 +2909,11 @@ function getSharedBridgeState() {
1857
2909
  const globalScope = globalThis;
1858
2910
  const existing = globalScope[SHARED_BRIDGE_KEY];
1859
2911
  if (existing) return existing;
2912
+ const appServer = new AppServerProcess();
1860
2913
  const created = {
1861
- appServer: new AppServerProcess(),
1862
- methodCatalog: new MethodCatalog()
2914
+ appServer,
2915
+ methodCatalog: new MethodCatalog(),
2916
+ telegramBridge: new TelegramThreadBridge(appServer)
1863
2917
  };
1864
2918
  globalScope[SHARED_BRIDGE_KEY] = created;
1865
2919
  return created;
@@ -1868,7 +2922,7 @@ async function loadAllThreadsForSearch(appServer) {
1868
2922
  const threads = [];
1869
2923
  let cursor = null;
1870
2924
  do {
1871
- const response = asRecord2(await appServer.rpc("thread/list", {
2925
+ const response = asRecord3(await appServer.rpc("thread/list", {
1872
2926
  archived: false,
1873
2927
  limit: 100,
1874
2928
  sortKey: "updated_at",
@@ -1876,7 +2930,7 @@ async function loadAllThreadsForSearch(appServer) {
1876
2930
  }));
1877
2931
  const data = Array.isArray(response?.data) ? response.data : [];
1878
2932
  for (const row of data) {
1879
- const record = asRecord2(row);
2933
+ const record = asRecord3(row);
1880
2934
  const id = typeof record?.id === "string" ? record.id : "";
1881
2935
  if (!id) continue;
1882
2936
  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 +2979,7 @@ async function buildThreadSearchIndex(appServer) {
1925
2979
  return { docsById };
1926
2980
  }
1927
2981
  function createCodexBridgeMiddleware() {
1928
- const { appServer, methodCatalog } = getSharedBridgeState();
2982
+ const { appServer, methodCatalog, telegramBridge } = getSharedBridgeState();
1929
2983
  let threadSearchIndex = null;
1930
2984
  let threadSearchIndexPromise = null;
1931
2985
  async function getThreadSearchIndex() {
@@ -1941,6 +2995,12 @@ function createCodexBridgeMiddleware() {
1941
2995
  return threadSearchIndexPromise;
1942
2996
  }
1943
2997
  void initializeSkillsSyncOnStartup(appServer);
2998
+ void readTelegramBridgeConfig().then((config) => {
2999
+ if (!config.botToken) return;
3000
+ telegramBridge.configureToken(config.botToken);
3001
+ telegramBridge.start();
3002
+ }).catch(() => {
3003
+ });
1944
3004
  const middleware = async (req, res, next) => {
1945
3005
  try {
1946
3006
  if (!req.url) {
@@ -1957,7 +3017,7 @@ function createCodexBridgeMiddleware() {
1957
3017
  }
1958
3018
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
1959
3019
  const payload = await readJsonBody(req);
1960
- const body = asRecord2(payload);
3020
+ const body = asRecord3(payload);
1961
3021
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
1962
3022
  setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
1963
3023
  return;
@@ -2006,11 +3066,24 @@ function createCodexBridgeMiddleware() {
2006
3066
  return;
2007
3067
  }
2008
3068
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
2009
- setJson2(res, 200, { data: { path: homedir2() } });
3069
+ setJson2(res, 200, { data: { path: homedir3() } });
3070
+ return;
3071
+ }
3072
+ if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
3073
+ const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
3074
+ const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
3075
+ const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
3076
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
3077
+ try {
3078
+ const data = await fetchGithubTrending(since, limit);
3079
+ setJson2(res, 200, { data });
3080
+ } catch (error) {
3081
+ setJson2(res, 502, { error: getErrorMessage3(error, "Failed to fetch GitHub trending") });
3082
+ }
2010
3083
  return;
2011
3084
  }
2012
3085
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
2013
- const payload = asRecord2(await readJsonBody(req));
3086
+ const payload = asRecord3(await readJsonBody(req));
2014
3087
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
2015
3088
  if (!rawSourceCwd) {
2016
3089
  setJson2(res, 400, { error: "Missing sourceCwd" });
@@ -2036,22 +3109,22 @@ function createCodexBridgeMiddleware() {
2036
3109
  await runCommand2("git", ["init"], { cwd: sourceCwd });
2037
3110
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2038
3111
  }
2039
- const repoName = basename(gitRoot) || "repo";
2040
- const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
3112
+ const repoName = basename3(gitRoot) || "repo";
3113
+ const worktreesRoot = join3(getCodexHomeDir2(), "worktrees");
2041
3114
  await mkdir2(worktreesRoot, { recursive: true });
2042
3115
  let worktreeId = "";
2043
3116
  let worktreeParent = "";
2044
3117
  let worktreeCwd = "";
2045
3118
  for (let attempt = 0; attempt < 12; attempt += 1) {
2046
3119
  const candidate = randomBytes(2).toString("hex");
2047
- const parent = join2(worktreesRoot, candidate);
3120
+ const parent = join3(worktreesRoot, candidate);
2048
3121
  try {
2049
3122
  await stat2(parent);
2050
3123
  continue;
2051
3124
  } catch {
2052
3125
  worktreeId = candidate;
2053
3126
  worktreeParent = parent;
2054
- worktreeCwd = join2(parent, repoName);
3127
+ worktreeCwd = join3(parent, repoName);
2055
3128
  break;
2056
3129
  }
2057
3130
  }
@@ -2075,13 +3148,106 @@ function createCodexBridgeMiddleware() {
2075
3148
  }
2076
3149
  });
2077
3150
  } catch (error) {
2078
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
3151
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to create worktree") });
3152
+ }
3153
+ return;
3154
+ }
3155
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
3156
+ const payload = asRecord3(await readJsonBody(req));
3157
+ const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
3158
+ const commitMessage = normalizeCommitMessage(payload?.message);
3159
+ if (!rawCwd) {
3160
+ setJson2(res, 400, { error: "Missing cwd" });
3161
+ return;
3162
+ }
3163
+ if (!commitMessage) {
3164
+ setJson2(res, 400, { error: "Missing message" });
3165
+ return;
3166
+ }
3167
+ const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
3168
+ try {
3169
+ const cwdInfo = await stat2(cwd);
3170
+ if (!cwdInfo.isDirectory()) {
3171
+ setJson2(res, 400, { error: "cwd is not a directory" });
3172
+ return;
3173
+ }
3174
+ } catch {
3175
+ setJson2(res, 404, { error: "cwd does not exist" });
3176
+ return;
3177
+ }
3178
+ try {
3179
+ await ensureRollbackGitRepo(cwd);
3180
+ const beforeStatus = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
3181
+ if (!beforeStatus.trim()) {
3182
+ setJson2(res, 200, { data: { committed: false } });
3183
+ return;
3184
+ }
3185
+ await runRollbackGit(cwd, ["add", "-A"]);
3186
+ const stagedStatus = await runRollbackGitWithOutput(cwd, ["diff", "--cached", "--name-only"]);
3187
+ if (!stagedStatus.trim()) {
3188
+ setJson2(res, 200, { data: { committed: false } });
3189
+ return;
3190
+ }
3191
+ await runRollbackGit(cwd, ["commit", "-m", commitMessage]);
3192
+ setJson2(res, 200, { data: { committed: true } });
3193
+ } catch (error) {
3194
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit rollback changes") });
3195
+ }
3196
+ return;
3197
+ }
3198
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
3199
+ const payload = asRecord3(await readJsonBody(req));
3200
+ const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
3201
+ const commitMessage = normalizeCommitMessage(payload?.message);
3202
+ if (!rawCwd) {
3203
+ setJson2(res, 400, { error: "Missing cwd" });
3204
+ return;
3205
+ }
3206
+ if (!commitMessage) {
3207
+ setJson2(res, 400, { error: "Missing message" });
3208
+ return;
3209
+ }
3210
+ const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
3211
+ try {
3212
+ const cwdInfo = await stat2(cwd);
3213
+ if (!cwdInfo.isDirectory()) {
3214
+ setJson2(res, 400, { error: "cwd is not a directory" });
3215
+ return;
3216
+ }
3217
+ } catch {
3218
+ setJson2(res, 404, { error: "cwd does not exist" });
3219
+ return;
3220
+ }
3221
+ try {
3222
+ await ensureRollbackGitRepo(cwd);
3223
+ const commitSha = await findRollbackCommitByExactMessage(cwd, commitMessage);
3224
+ if (!commitSha) {
3225
+ setJson2(res, 404, { error: "No matching commit found for this user message" });
3226
+ return;
3227
+ }
3228
+ let resetTargetSha = "";
3229
+ try {
3230
+ resetTargetSha = await runRollbackGitCapture(cwd, ["rev-parse", `${commitSha}^`]);
3231
+ } catch {
3232
+ setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
3233
+ return;
3234
+ }
3235
+ let stashed = false;
3236
+ if (await hasRollbackGitWorkingTreeChanges(cwd)) {
3237
+ const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
3238
+ await runRollbackGit(cwd, ["stash", "push", "-u", "-m", stashMessage]);
3239
+ stashed = true;
3240
+ }
3241
+ await runRollbackGit(cwd, ["reset", "--hard", resetTargetSha]);
3242
+ setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
3243
+ } catch (error) {
3244
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback project to user message commit") });
2079
3245
  }
2080
3246
  return;
2081
3247
  }
2082
3248
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
2083
3249
  const payload = await readJsonBody(req);
2084
- const record = asRecord2(payload);
3250
+ const record = asRecord3(payload);
2085
3251
  if (!record) {
2086
3252
  setJson2(res, 400, { error: "Invalid body: expected object" });
2087
3253
  return;
@@ -2096,7 +3262,7 @@ function createCodexBridgeMiddleware() {
2096
3262
  return;
2097
3263
  }
2098
3264
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
2099
- const payload = asRecord2(await readJsonBody(req));
3265
+ const payload = asRecord3(await readJsonBody(req));
2100
3266
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
2101
3267
  const createIfMissing = payload?.createIfMissing === true;
2102
3268
  const label = typeof payload?.label === "string" ? payload.label : "";
@@ -2156,7 +3322,7 @@ function createCodexBridgeMiddleware() {
2156
3322
  let index = 1;
2157
3323
  while (index < 1e5) {
2158
3324
  const candidateName = `New Project (${String(index)})`;
2159
- const candidatePath = join2(normalizedBasePath, candidateName);
3325
+ const candidatePath = join3(normalizedBasePath, candidateName);
2160
3326
  try {
2161
3327
  await stat2(candidatePath);
2162
3328
  index += 1;
@@ -2170,7 +3336,7 @@ function createCodexBridgeMiddleware() {
2170
3336
  return;
2171
3337
  }
2172
3338
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
2173
- const payload = asRecord2(await readJsonBody(req));
3339
+ const payload = asRecord3(await readJsonBody(req));
2174
3340
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
2175
3341
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2176
3342
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
@@ -2195,17 +3361,17 @@ function createCodexBridgeMiddleware() {
2195
3361
  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
3362
  setJson2(res, 200, { data: scored });
2197
3363
  } catch (error) {
2198
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
3364
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to search files") });
2199
3365
  }
2200
3366
  return;
2201
3367
  }
2202
3368
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
2203
- const cache = await readThreadTitleCache();
3369
+ const cache = await readMergedThreadTitleCache();
2204
3370
  setJson2(res, 200, { data: cache });
2205
3371
  return;
2206
3372
  }
2207
3373
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
2208
- const payload = asRecord2(await readJsonBody(req));
3374
+ const payload = asRecord3(await readJsonBody(req));
2209
3375
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2210
3376
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
2211
3377
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
@@ -2219,7 +3385,7 @@ function createCodexBridgeMiddleware() {
2219
3385
  return;
2220
3386
  }
2221
3387
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
2222
- const payload = asRecord2(await readJsonBody(req));
3388
+ const payload = asRecord3(await readJsonBody(req));
2223
3389
  const id = typeof payload?.id === "string" ? payload.id : "";
2224
3390
  const title = typeof payload?.title === "string" ? payload.title : "";
2225
3391
  if (!id) {
@@ -2232,6 +3398,23 @@ function createCodexBridgeMiddleware() {
2232
3398
  setJson2(res, 200, { ok: true });
2233
3399
  return;
2234
3400
  }
3401
+ if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
3402
+ const payload = asRecord3(await readJsonBody(req));
3403
+ const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
3404
+ if (!botToken) {
3405
+ setJson2(res, 400, { error: "Missing botToken" });
3406
+ return;
3407
+ }
3408
+ telegramBridge.configureToken(botToken);
3409
+ telegramBridge.start();
3410
+ await writeTelegramBridgeConfig({ botToken });
3411
+ setJson2(res, 200, { ok: true });
3412
+ return;
3413
+ }
3414
+ if (req.method === "GET" && url.pathname === "/codex-api/telegram/status") {
3415
+ setJson2(res, 200, { data: telegramBridge.getStatus() });
3416
+ return;
3417
+ }
2235
3418
  if (req.method === "GET" && url.pathname === "/codex-api/events") {
2236
3419
  res.statusCode = 200;
2237
3420
  res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
@@ -2264,12 +3447,13 @@ data: ${JSON.stringify({ ok: true })}
2264
3447
  }
2265
3448
  next();
2266
3449
  } catch (error) {
2267
- const message = getErrorMessage2(error, "Unknown bridge error");
3450
+ const message = getErrorMessage3(error, "Unknown bridge error");
2268
3451
  setJson2(res, 502, { error: message });
2269
3452
  }
2270
3453
  };
2271
3454
  middleware.dispose = () => {
2272
3455
  threadSearchIndex = null;
3456
+ telegramBridge.stop();
2273
3457
  appServer.dispose();
2274
3458
  };
2275
3459
  middleware.subscribeNotifications = (listener) => {
@@ -2402,7 +3586,7 @@ function createAuthSession(password) {
2402
3586
  }
2403
3587
 
2404
3588
  // src/server/localBrowseUi.ts
2405
- import { dirname, extname, join as join3 } from "path";
3589
+ import { dirname as dirname2, extname as extname2, join as join4 } from "path";
2406
3590
  import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
2407
3591
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2408
3592
  ".txt",
@@ -2433,7 +3617,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2433
3617
  ".ps1"
2434
3618
  ]);
2435
3619
  function languageForPath(pathValue) {
2436
- const extension = extname(pathValue).toLowerCase();
3620
+ const extension = extname2(pathValue).toLowerCase();
2437
3621
  switch (extension) {
2438
3622
  case ".js":
2439
3623
  return "javascript";
@@ -2494,7 +3678,7 @@ function decodeBrowsePath(rawPath) {
2494
3678
  }
2495
3679
  }
2496
3680
  function isTextEditablePath(pathValue) {
2497
- return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
3681
+ return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
2498
3682
  }
2499
3683
  function looksLikeTextBuffer(buffer) {
2500
3684
  if (buffer.length === 0) return true;
@@ -2540,7 +3724,7 @@ function escapeForInlineScriptString(value) {
2540
3724
  async function getDirectoryItems(localPath) {
2541
3725
  const entries = await readdir3(localPath, { withFileTypes: true });
2542
3726
  const withMeta = await Promise.all(entries.map(async (entry) => {
2543
- const entryPath = join3(localPath, entry.name);
3727
+ const entryPath = join4(localPath, entry.name);
2544
3728
  const entryStat = await stat3(entryPath);
2545
3729
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2546
3730
  return {
@@ -2560,13 +3744,13 @@ async function getDirectoryItems(localPath) {
2560
3744
  }
2561
3745
  async function createDirectoryListingHtml(localPath) {
2562
3746
  const items = await getDirectoryItems(localPath);
2563
- const parentPath = dirname(localPath);
3747
+ const parentPath = dirname2(localPath);
2564
3748
  const rows = items.map((item) => {
2565
3749
  const suffix = item.isDirectory ? "/" : "";
2566
3750
  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>`;
3751
+ 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
3752
  }).join("\n");
2569
- const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
3753
+ const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
2570
3754
  return `<!doctype html>
2571
3755
  <html lang="en">
2572
3756
  <head>
@@ -2580,8 +3764,27 @@ async function createDirectoryListingHtml(localPath) {
2580
3764
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
2581
3765
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
2582
3766
  .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; }
3767
+ .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
3768
+ .header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
3769
+ .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
3770
+ .header-open-btn {
3771
+ height: 42px;
3772
+ padding: 0 14px;
3773
+ border: 1px solid #4f8de0;
3774
+ border-radius: 10px;
3775
+ background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
3776
+ color: #eef6ff;
3777
+ font-weight: 700;
3778
+ letter-spacing: 0.01em;
3779
+ cursor: pointer;
3780
+ box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
3781
+ }
3782
+ .header-open-btn:hover { filter: brightness(1.08); }
3783
+ .header-open-btn:disabled { opacity: 0.6; cursor: default; }
3784
+ .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
3785
+ .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
3786
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
3787
+ .status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
2585
3788
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
2586
3789
  @media (max-width: 640px) {
2587
3790
  body { margin: 12px; }
@@ -2593,14 +3796,52 @@ async function createDirectoryListingHtml(localPath) {
2593
3796
  </head>
2594
3797
  <body>
2595
3798
  <h1>Index of ${escapeHtml(localPath)}</h1>
2596
- ${parentLink}
3799
+ <div class="header-actions">
3800
+ ${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
3801
+ <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>
3802
+ </div>
3803
+ <p id="status" class="status"></p>
2597
3804
  <ul>${rows}</ul>
3805
+ <script>
3806
+ const status = document.getElementById('status');
3807
+ document.addEventListener('click', async (event) => {
3808
+ const target = event.target;
3809
+ if (!(target instanceof Element)) return;
3810
+ const button = target.closest('.open-folder-btn');
3811
+ if (!(button instanceof HTMLButtonElement)) return;
3812
+
3813
+ const path = button.getAttribute('data-path') || '';
3814
+ if (!path) return;
3815
+ button.disabled = true;
3816
+ status.textContent = 'Opening folder in Codex...';
3817
+ try {
3818
+ const response = await fetch('/codex-api/project-root', {
3819
+ method: 'POST',
3820
+ headers: { 'Content-Type': 'application/json' },
3821
+ body: JSON.stringify({
3822
+ path,
3823
+ createIfMissing: false,
3824
+ label: '',
3825
+ }),
3826
+ });
3827
+ if (!response.ok) {
3828
+ status.textContent = 'Failed to open folder.';
3829
+ button.disabled = false;
3830
+ return;
3831
+ }
3832
+ window.location.assign('/#/');
3833
+ } catch {
3834
+ status.textContent = 'Failed to open folder.';
3835
+ button.disabled = false;
3836
+ }
3837
+ });
3838
+ </script>
2598
3839
  </body>
2599
3840
  </html>`;
2600
3841
  }
2601
3842
  async function createTextEditorHtml(localPath) {
2602
3843
  const content = await readFile3(localPath, "utf8");
2603
- const parentPath = dirname(localPath);
3844
+ const parentPath = dirname2(localPath);
2604
3845
  const language = languageForPath(localPath);
2605
3846
  const safeContentLiteral = escapeForInlineScriptString(content);
2606
3847
  return `<!doctype html>
@@ -2669,9 +3910,9 @@ async function createTextEditorHtml(localPath) {
2669
3910
 
2670
3911
  // src/server/httpServer.ts
2671
3912
  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");
3913
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
3914
+ var distDir = join5(__dirname, "..", "dist");
3915
+ var spaEntryFile = join5(distDir, "index.html");
2675
3916
  var IMAGE_CONTENT_TYPES = {
2676
3917
  ".avif": "image/avif",
2677
3918
  ".bmp": "image/bmp",
@@ -2728,7 +3969,7 @@ function createServer(options = {}) {
2728
3969
  res.status(400).json({ error: "Expected absolute local file path." });
2729
3970
  return;
2730
3971
  }
2731
- const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
3972
+ const contentType = IMAGE_CONTENT_TYPES[extname3(localPath).toLowerCase()];
2732
3973
  if (!contentType) {
2733
3974
  res.status(415).json({ error: "Unsupported image type." });
2734
3975
  return;
@@ -2815,7 +4056,7 @@ function createServer(options = {}) {
2815
4056
  res.status(404).json({ error: "File not found." });
2816
4057
  }
2817
4058
  });
2818
- const hasFrontendAssets = existsSync2(spaEntryFile);
4059
+ const hasFrontendAssets = existsSync4(spaEntryFile);
2819
4060
  if (hasFrontendAssets) {
2820
4061
  app.use(express.static(distDir));
2821
4062
  }
@@ -2885,10 +4126,26 @@ function generatePassword() {
2885
4126
 
2886
4127
  // src/cli/index.ts
2887
4128
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2888
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
4129
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
4130
+ var hasPromptedCloudflaredInstall = false;
4131
+ function getCodexHomePath() {
4132
+ return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
4133
+ }
4134
+ function getCloudflaredPromptMarkerPath() {
4135
+ return join6(getCodexHomePath(), ".cloudflared-install-prompted");
4136
+ }
4137
+ function hasPromptedCloudflaredInstallPersisted() {
4138
+ return existsSync5(getCloudflaredPromptMarkerPath());
4139
+ }
4140
+ async function persistCloudflaredInstallPrompted() {
4141
+ const codexHome = getCodexHomePath();
4142
+ mkdirSync(codexHome, { recursive: true });
4143
+ await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
4144
+ `, "utf8");
4145
+ }
2889
4146
  async function readCliVersion() {
2890
4147
  try {
2891
- const packageJsonPath = join5(__dirname2, "..", "package.json");
4148
+ const packageJsonPath = join6(__dirname2, "..", "package.json");
2892
4149
  const raw = await readFile4(packageJsonPath, "utf8");
2893
4150
  const parsed = JSON.parse(raw);
2894
4151
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -2899,47 +4156,22 @@ async function readCliVersion() {
2899
4156
  function isTermuxRuntime() {
2900
4157
  return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
2901
4158
  }
2902
- function canRun(command, args = []) {
2903
- const result = spawnSync(command, args, { stdio: "ignore" });
2904
- return result.status === 0;
2905
- }
2906
4159
  function runOrFail(command, args, label) {
2907
- const result = spawnSync(command, args, { stdio: "inherit" });
4160
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
2908
4161
  if (result.status !== 0) {
2909
4162
  throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
2910
4163
  }
2911
4164
  }
2912
4165
  function runWithStatus(command, args) {
2913
- const result = spawnSync(command, args, { stdio: "inherit" });
4166
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
2914
4167
  return result.status ?? -1;
2915
4168
  }
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
4169
  function resolveCloudflaredCommand() {
2938
- if (canRun("cloudflared", ["--version"])) {
4170
+ if (canRunCommand("cloudflared", ["--version"])) {
2939
4171
  return "cloudflared";
2940
4172
  }
2941
- const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
2942
- if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
4173
+ const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
4174
+ if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
2943
4175
  return localCandidate;
2944
4176
  }
2945
4177
  return null;
@@ -2992,14 +4224,14 @@ async function ensureCloudflaredInstalledLinux() {
2992
4224
  if (!mappedArch) {
2993
4225
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2994
4226
  }
2995
- const userBinDir = join5(homedir3(), ".local", "bin");
4227
+ const userBinDir = join6(homedir4(), ".local", "bin");
2996
4228
  mkdirSync(userBinDir, { recursive: true });
2997
- const destination = join5(userBinDir, "cloudflared");
4229
+ const destination = join6(userBinDir, "cloudflared");
2998
4230
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2999
4231
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
3000
4232
  await downloadFile(downloadUrl, destination);
3001
4233
  chmodSync(destination, 493);
3002
- process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
4234
+ process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
3003
4235
  const installed = resolveCloudflaredCommand();
3004
4236
  if (!installed) {
3005
4237
  throw new Error("cloudflared download completed but executable is still not available");
@@ -3008,11 +4240,19 @@ async function ensureCloudflaredInstalledLinux() {
3008
4240
  return installed;
3009
4241
  }
3010
4242
  async function shouldInstallCloudflaredInteractively() {
4243
+ if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
4244
+ return false;
4245
+ }
4246
+ hasPromptedCloudflaredInstall = true;
4247
+ await persistCloudflaredInstallPrompted();
4248
+ if (process.platform === "win32") {
4249
+ return false;
4250
+ }
3011
4251
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3012
4252
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3013
4253
  return false;
3014
4254
  }
3015
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
4255
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
3016
4256
  try {
3017
4257
  const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
3018
4258
  const normalized = answer.trim().toLowerCase();
@@ -3026,6 +4266,9 @@ async function resolveCloudflaredForTunnel() {
3026
4266
  if (current) {
3027
4267
  return current;
3028
4268
  }
4269
+ if (process.platform === "win32") {
4270
+ return null;
4271
+ }
3029
4272
  const installApproved = await shouldInstallCloudflaredInteractively();
3030
4273
  if (!installApproved) {
3031
4274
  return null;
@@ -3033,8 +4276,8 @@ async function resolveCloudflaredForTunnel() {
3033
4276
  return ensureCloudflaredInstalledLinux();
3034
4277
  }
3035
4278
  function hasCodexAuth() {
3036
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3037
- return existsSync3(join5(codexHome, "auth.json"));
4279
+ const codexHome = getCodexHomePath();
4280
+ return existsSync5(join6(codexHome, "auth.json"));
3038
4281
  }
3039
4282
  function ensureCodexInstalled() {
3040
4283
  let codexCommand = resolveCodexCommand();
@@ -3052,7 +4295,7 @@ function ensureCodexInstalled() {
3052
4295
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
3053
4296
  `);
3054
4297
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
3055
- process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
4298
+ process.env.PATH = prependPathEntry(process.env.PATH ?? "", getNpmGlobalBinDir(userPrefix));
3056
4299
  };
3057
4300
  if (isTermuxRuntime()) {
3058
4301
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -3115,19 +4358,22 @@ function parseCloudflaredUrl(chunk) {
3115
4358
  }
3116
4359
  function getAccessibleUrls(port) {
3117
4360
  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) {
4361
+ try {
4362
+ const interfaces = networkInterfaces();
4363
+ for (const entries of Object.values(interfaces)) {
4364
+ if (!entries) {
3125
4365
  continue;
3126
4366
  }
3127
- if (entry.family === "IPv4") {
3128
- urls.add(`http://${entry.address}:${String(port)}`);
4367
+ for (const entry of entries) {
4368
+ if (entry.internal) {
4369
+ continue;
4370
+ }
4371
+ if (entry.family === "IPv4") {
4372
+ urls.add(`http://${entry.address}:${String(port)}`);
4373
+ }
3129
4374
  }
3130
4375
  }
4376
+ } catch {
3131
4377
  }
3132
4378
  return Array.from(urls);
3133
4379
  }
@@ -3190,8 +4436,8 @@ function listenWithFallback(server, startPort) {
3190
4436
  });
3191
4437
  }
3192
4438
  function getCodexGlobalStatePath2() {
3193
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3194
- return join5(codexHome, ".codex-global-state.json");
4439
+ const codexHome = getCodexHomePath();
4440
+ return join6(codexHome, ".codex-global-state.json");
3195
4441
  }
3196
4442
  function normalizeUniqueStrings(value) {
3197
4443
  if (!Array.isArray(value)) return [];
@@ -3256,6 +4502,9 @@ async function startServer(options) {
3256
4502
  }
3257
4503
  }
3258
4504
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
4505
+ if (codexCommand) {
4506
+ process.env.CODEXUI_CODEX_COMMAND = codexCommand;
4507
+ }
3259
4508
  if (!hasCodexAuth() && codexCommand) {
3260
4509
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
3261
4510
  runOrFail(codexCommand, ["login"], "Codex login");
@@ -3315,7 +4564,7 @@ async function startServer(options) {
3315
4564
  qrcode.generate(tunnelUrl, { small: true });
3316
4565
  console.log("");
3317
4566
  }
3318
- openBrowser(`http://localhost:${String(port)}`);
4567
+ if (options.open) openBrowser(`http://localhost:${String(port)}`);
3319
4568
  function shutdown() {
3320
4569
  console.log("\nShutting down...");
3321
4570
  if (tunnelChild && !tunnelChild.killed) {
@@ -3335,10 +4584,11 @@ async function startServer(options) {
3335
4584
  }
3336
4585
  async function runLogin() {
3337
4586
  const codexCommand = ensureCodexInstalled() ?? "codex";
4587
+ process.env.CODEXUI_CODEX_COMMAND = codexCommand;
3338
4588
  console.log("\nStarting `codex login`...\n");
3339
4589
  runOrFail(codexCommand, ["login"], "Codex login");
3340
4590
  }
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) => {
4591
+ 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
4592
  const rawArgv = process.argv.slice(2);
3343
4593
  const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
3344
4594
  let openProjectOnly = (opts.openProject ?? "").trim();