codexui-android 0.1.101 → 0.1.103

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,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
5
+ import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
6
6
  import { readFile as readFile5, stat as stat7, writeFile as writeFile6 } from "fs/promises";
7
7
  import { homedir as homedir7, networkInterfaces } from "os";
8
8
  import { isAbsolute as isAbsolute4, join as join10, resolve as resolve3 } from "path";
@@ -194,12 +194,12 @@ import express from "express";
194
194
  import { spawn as spawn4, spawnSync as spawnSync4 } from "child_process";
195
195
  import { createHash as createHash2, randomBytes } from "crypto";
196
196
  import { mkdtemp as mkdtemp3, readFile as readFile3, readdir as readdir2, rm as rm4, mkdir as mkdir4, stat as stat4, lstat as lstat2 } from "fs/promises";
197
- import { createReadStream, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
197
+ import { createReadStream, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
198
198
  import { request as httpRequest2 } from "http";
199
199
  import { request as httpsRequest2 } from "https";
200
200
  import { homedir as homedir5 } from "os";
201
201
  import { tmpdir as tmpdir4 } from "os";
202
- import { basename as basename4, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "path";
202
+ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "path";
203
203
  import { createInterface } from "readline";
204
204
  import { writeFile as writeFile4 } from "fs/promises";
205
205
 
@@ -212,7 +212,11 @@ import { join as join2 } from "path";
212
212
  var ACCOUNT_QUOTA_REFRESH_TTL_MS = 5 * 60 * 1e3;
213
213
  var ACCOUNT_QUOTA_LOADING_STALE_MS = 2 * 60 * 1e3;
214
214
  var ACCOUNT_INSPECTION_TIMEOUT_MS = 25 * 1e3;
215
+ var LOGIN_URL_TIMEOUT_MS = 15 * 1e3;
216
+ var LOGIN_CALLBACK_TIMEOUT_MS = 20 * 1e3;
217
+ var LOGIN_AUTH_FILE_TIMEOUT_MS = 10 * 1e3;
215
218
  var backgroundRefreshPromise = null;
219
+ var activeLogin = null;
216
220
  function asRecord(value) {
217
221
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
218
222
  }
@@ -233,6 +237,18 @@ function setJson(res, statusCode, payload) {
233
237
  res.setHeader("Content-Type", "application/json; charset=utf-8");
234
238
  res.end(JSON.stringify(payload));
235
239
  }
240
+ async function readJsonBody(req) {
241
+ const rawBody = await new Promise((resolve4, reject) => {
242
+ let body = "";
243
+ req.setEncoding("utf8");
244
+ req.on("data", (chunk) => {
245
+ body += chunk;
246
+ });
247
+ req.on("end", () => resolve4(body));
248
+ req.on("error", reject);
249
+ });
250
+ return asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
251
+ }
236
252
  function getErrorMessage(payload, fallback) {
237
253
  if (payload instanceof Error && payload.message.trim().length > 0) {
238
254
  return payload.message;
@@ -734,6 +750,134 @@ async function importAccountFromAuthPath(path) {
734
750
  accounts: sortAccounts(nextState.accounts, nextState.activeAccountId).map((entry) => toPublicAccountEntry(entry, nextState.activeAccountId))
735
751
  };
736
752
  }
753
+ function extractLoginUrl(output) {
754
+ const match = output.match(/https:\/\/auth\.openai\.com\/oauth\/authorize\?\S+/u);
755
+ return match?.[0] ?? null;
756
+ }
757
+ function isLocalCallbackUrl(rawUrl) {
758
+ try {
759
+ const parsed = new URL(rawUrl);
760
+ if (parsed.protocol !== "http:") return false;
761
+ return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
762
+ } catch {
763
+ return false;
764
+ }
765
+ }
766
+ async function waitForLoginUrl() {
767
+ if (activeLogin?.loginUrl) return activeLogin.loginUrl;
768
+ return await new Promise((resolve4, reject) => {
769
+ const startedAt = Date.now();
770
+ const timer = setInterval(() => {
771
+ if (!activeLogin) {
772
+ clearInterval(timer);
773
+ reject(new Error("Login process is not running."));
774
+ return;
775
+ }
776
+ if (activeLogin.loginUrl) {
777
+ clearInterval(timer);
778
+ resolve4(activeLogin.loginUrl);
779
+ return;
780
+ }
781
+ if (activeLogin.exited) {
782
+ clearInterval(timer);
783
+ reject(new Error(activeLogin.output.trim() || "codex login exited before returning a login URL."));
784
+ return;
785
+ }
786
+ if (Date.now() - startedAt > LOGIN_URL_TIMEOUT_MS) {
787
+ clearInterval(timer);
788
+ reject(new Error("Timed out waiting for codex login URL."));
789
+ }
790
+ }, 100);
791
+ });
792
+ }
793
+ async function startCodexLogin() {
794
+ if (activeLogin && !activeLogin.exited) {
795
+ return await waitForLoginUrl();
796
+ }
797
+ const proc = spawn("codex", ["login"], {
798
+ env: process.env,
799
+ stdio: ["pipe", "pipe", "pipe"]
800
+ });
801
+ proc.stdin.end();
802
+ activeLogin = {
803
+ proc,
804
+ loginUrl: null,
805
+ output: "",
806
+ exited: false,
807
+ exitCode: null,
808
+ exitSignal: null,
809
+ exitPromise: new Promise((resolve4) => {
810
+ proc.once("exit", (code, signal) => {
811
+ if (activeLogin?.proc === proc) {
812
+ activeLogin.exited = true;
813
+ activeLogin.exitCode = code;
814
+ activeLogin.exitSignal = signal;
815
+ }
816
+ resolve4();
817
+ });
818
+ })
819
+ };
820
+ const appendOutput = (chunk) => {
821
+ if (!activeLogin || activeLogin.proc !== proc) return;
822
+ activeLogin.output += chunk.toString();
823
+ activeLogin.loginUrl = activeLogin.loginUrl ?? extractLoginUrl(activeLogin.output);
824
+ };
825
+ proc.stdout.on("data", appendOutput);
826
+ proc.stderr.on("data", appendOutput);
827
+ proc.once("error", (error) => {
828
+ if (!activeLogin || activeLogin.proc !== proc) return;
829
+ activeLogin.exited = true;
830
+ activeLogin.output += error.message;
831
+ });
832
+ try {
833
+ return await waitForLoginUrl();
834
+ } catch (error) {
835
+ if (activeLogin?.proc === proc && !activeLogin.exited) {
836
+ proc.kill("SIGTERM");
837
+ }
838
+ activeLogin = null;
839
+ throw error;
840
+ }
841
+ }
842
+ async function curlLoginCallback(callbackUrl) {
843
+ const controller = new AbortController();
844
+ const timer = setTimeout(() => controller.abort(), LOGIN_CALLBACK_TIMEOUT_MS);
845
+ try {
846
+ const response = await fetch(callbackUrl, {
847
+ redirect: "manual",
848
+ signal: controller.signal
849
+ });
850
+ if (response.status >= 400) {
851
+ throw new Error(`Login callback returned HTTP ${response.status}.`);
852
+ }
853
+ } finally {
854
+ clearTimeout(timer);
855
+ }
856
+ }
857
+ async function getActiveAuthMtimeMs() {
858
+ try {
859
+ return (await stat(getActiveAuthPath())).mtimeMs;
860
+ } catch {
861
+ return null;
862
+ }
863
+ }
864
+ async function waitForAuthFileUpdate(previousMtimeMs) {
865
+ const startedAt = Date.now();
866
+ while (Date.now() - startedAt <= LOGIN_AUTH_FILE_TIMEOUT_MS) {
867
+ const nextMtimeMs = await getActiveAuthMtimeMs();
868
+ if (nextMtimeMs !== null && (previousMtimeMs === null || nextMtimeMs > previousMtimeMs)) {
869
+ return;
870
+ }
871
+ await new Promise((resolve4) => setTimeout(resolve4, 250));
872
+ }
873
+ }
874
+ function stopActiveLogin() {
875
+ if (!activeLogin) return;
876
+ if (!activeLogin.exited) {
877
+ activeLogin.proc.kill("SIGTERM");
878
+ }
879
+ activeLogin = null;
880
+ }
737
881
  async function handleAccountRoutes(req, res, url, context) {
738
882
  const { appServer } = context;
739
883
  if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
@@ -813,6 +957,92 @@ async function handleAccountRoutes(req, res, url, context) {
813
957
  }
814
958
  return true;
815
959
  }
960
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/login/start") {
961
+ try {
962
+ const loginUrl = await startCodexLogin();
963
+ setJson(res, 200, {
964
+ ok: true,
965
+ data: {
966
+ loginUrl
967
+ }
968
+ });
969
+ } catch (error) {
970
+ setJson(res, 500, {
971
+ error: "account_login_start_failed",
972
+ message: getErrorMessage(error, "Failed to start codex login")
973
+ });
974
+ }
975
+ return true;
976
+ }
977
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/login/complete") {
978
+ try {
979
+ const payload = await readJsonBody(req);
980
+ const callbackUrl = typeof payload?.callbackUrl === "string" ? payload.callbackUrl.trim() : "";
981
+ if (!callbackUrl) {
982
+ setJson(res, 400, { error: "missing_callback_url", message: "Paste the localhost callback URL from the browser." });
983
+ return true;
984
+ }
985
+ if (!isLocalCallbackUrl(callbackUrl)) {
986
+ setJson(res, 400, { error: "invalid_callback_url", message: "The callback URL must use http://localhost or http://127.0.0.1." });
987
+ return true;
988
+ }
989
+ if (!activeLogin || activeLogin.exited) {
990
+ setJson(res, 409, { error: "login_not_running", message: "Start Codex login before submitting the callback URL." });
991
+ return true;
992
+ }
993
+ const previousAuthMtimeMs = await getActiveAuthMtimeMs();
994
+ await curlLoginCallback(callbackUrl);
995
+ await waitForAuthFileUpdate(previousAuthMtimeMs);
996
+ const imported = await importAccountFromAuthPath(getActiveAuthPath());
997
+ stopActiveLogin();
998
+ appServer.dispose();
999
+ const inspection = await validateSwitchedAccount(appServer);
1000
+ const state = await readStoredAccountsState();
1001
+ const importedAccountId = imported.importedAccountId;
1002
+ const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
1003
+ if (!target) {
1004
+ throw new Error("account_not_found");
1005
+ }
1006
+ const nextEntry = {
1007
+ ...target,
1008
+ email: inspection.metadata.email ?? target.email,
1009
+ planType: inspection.metadata.planType ?? target.planType,
1010
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1011
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
1012
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1013
+ quotaStatus: "ready",
1014
+ quotaError: null,
1015
+ unavailableReason: null
1016
+ };
1017
+ const nextState = withUpsertedAccount({
1018
+ activeAccountId: importedAccountId,
1019
+ accounts: state.accounts
1020
+ }, nextEntry);
1021
+ await writeStoredAccountsState({
1022
+ activeAccountId: importedAccountId,
1023
+ accounts: nextState.accounts
1024
+ });
1025
+ const backgroundState = await scheduleAccountsBackgroundRefresh({
1026
+ force: true,
1027
+ prioritizeAccountId: importedAccountId,
1028
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
1029
+ });
1030
+ setJson(res, 200, {
1031
+ ok: true,
1032
+ data: {
1033
+ activeAccountId: importedAccountId,
1034
+ importedAccountId,
1035
+ accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
1036
+ }
1037
+ });
1038
+ } catch (error) {
1039
+ setJson(res, 500, {
1040
+ error: "account_login_complete_failed",
1041
+ message: getErrorMessage(error, "Failed to complete Codex login")
1042
+ });
1043
+ }
1044
+ return true;
1045
+ }
816
1046
  if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
817
1047
  try {
818
1048
  if (appServer.listPendingServerRequests().length > 0) {
@@ -822,16 +1052,7 @@ async function handleAccountRoutes(req, res, url, context) {
822
1052
  });
823
1053
  return true;
824
1054
  }
825
- const rawBody = await new Promise((resolve4, reject) => {
826
- let body = "";
827
- req.setEncoding("utf8");
828
- req.on("data", (chunk) => {
829
- body += chunk;
830
- });
831
- req.on("end", () => resolve4(body));
832
- req.on("error", reject);
833
- });
834
- const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
1055
+ const payload = await readJsonBody(req);
835
1056
  const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
836
1057
  if (!accountId) {
837
1058
  setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
@@ -915,16 +1136,7 @@ async function handleAccountRoutes(req, res, url, context) {
915
1136
  }
916
1137
  if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
917
1138
  try {
918
- const rawBody = await new Promise((resolve4, reject) => {
919
- let body = "";
920
- req.setEncoding("utf8");
921
- req.on("data", (chunk) => {
922
- body += chunk;
923
- });
924
- req.on("end", () => resolve4(body));
925
- req.on("error", reject);
926
- });
927
- const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
1139
+ const payload = await readJsonBody(req);
928
1140
  const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
929
1141
  if (!accountId) {
930
1142
  setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
@@ -1721,6 +1933,47 @@ import { existsSync as existsSync2 } from "fs";
1721
1933
  import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
1722
1934
  import { join as join4 } from "path";
1723
1935
  import { writeFile as writeFile3 } from "fs/promises";
1936
+
1937
+ // src/utils/commandInvocation.ts
1938
+ import { spawnSync as spawnSync2 } from "child_process";
1939
+ import { basename, extname } from "path";
1940
+ var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
1941
+ function quoteCmdExeArg(value) {
1942
+ const normalized = value.replace(/"/g, '""');
1943
+ if (!/[\s"]/u.test(normalized)) {
1944
+ return normalized;
1945
+ }
1946
+ return `"${normalized}"`;
1947
+ }
1948
+ function needsCmdExeWrapper(command) {
1949
+ if (process.platform !== "win32") {
1950
+ return false;
1951
+ }
1952
+ const lowerCommand = command.toLowerCase();
1953
+ const baseName = basename(lowerCommand);
1954
+ if (/\.(cmd|bat)$/i.test(baseName)) {
1955
+ return true;
1956
+ }
1957
+ if (extname(baseName)) {
1958
+ return false;
1959
+ }
1960
+ return WINDOWS_CMD_NAMES.has(baseName);
1961
+ }
1962
+ function getSpawnInvocation(command, args = []) {
1963
+ if (needsCmdExeWrapper(command)) {
1964
+ return {
1965
+ command: "cmd.exe",
1966
+ args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
1967
+ };
1968
+ }
1969
+ return { command, args };
1970
+ }
1971
+ function spawnSyncCommand(command, args = [], options = {}) {
1972
+ const invocation = getSpawnInvocation(command, args);
1973
+ return spawnSync2(invocation.command, invocation.args, options);
1974
+ }
1975
+
1976
+ // src/server/skillsRoutes.ts
1724
1977
  function asRecord3(value) {
1725
1978
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1726
1979
  }
@@ -1795,10 +2048,13 @@ function getSkillsInstallDir() {
1795
2048
  return join4(getCodexHomeDir2(), "skills");
1796
2049
  }
1797
2050
  var DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
2051
+ var SKILL_SEARCH_METADATA_LIMIT = 20;
2052
+ var SKILL_SEARCH_METADATA_CONCURRENCY = 4;
1798
2053
  async function runCommand2(command, args, options = {}) {
1799
2054
  const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
1800
2055
  await new Promise((resolve4, reject) => {
1801
- const proc = spawn3(command, args, {
2056
+ const invocation = getSpawnInvocation(command, args);
2057
+ const proc = spawn3(invocation.command, invocation.args, {
1802
2058
  cwd: options.cwd,
1803
2059
  env: process.env,
1804
2060
  stdio: ["ignore", "pipe", "pipe"]
@@ -1841,7 +2097,8 @@ async function runCommand2(command, args, options = {}) {
1841
2097
  async function runCommandWithOutput(command, args, options = {}) {
1842
2098
  const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
1843
2099
  return await new Promise((resolve4, reject) => {
1844
- const proc = spawn3(command, args, {
2100
+ const invocation = getSpawnInvocation(command, args);
2101
+ const proc = spawn3(invocation.command, invocation.args, {
1845
2102
  cwd: options.cwd,
1846
2103
  env: process.env,
1847
2104
  stdio: ["ignore", "pipe", "pipe"]
@@ -1911,6 +2168,17 @@ async function detectUserSkillsDir(appServer) {
1911
2168
  }
1912
2169
  return getSkillsInstallDir();
1913
2170
  }
2171
+ async function ensureInstalledSkillIsValid(appServer, skillPath) {
2172
+ const result = await appServer.rpc("skills/list", { forceReload: true });
2173
+ const normalized = skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
2174
+ for (const entry of result.data ?? []) {
2175
+ for (const error of entry.errors ?? []) {
2176
+ if (error.path === normalized) {
2177
+ throw new Error(error.message || "Installed skill is invalid");
2178
+ }
2179
+ }
2180
+ }
2181
+ }
1914
2182
  async function runGitFetchWithRefLockRetry(repoDir, args = ["fetch", "origin"]) {
1915
2183
  try {
1916
2184
  await runCommand2("git", args, { cwd: repoDir });
@@ -1927,11 +2195,18 @@ async function runGitFetchWithRefLockRetry(repoDir, args = ["fetch", "origin"])
1927
2195
  await runCommand2("git", args, { cwd: repoDir });
1928
2196
  }
1929
2197
  }
1930
- function buildLocalHubEntry(info) {
2198
+ async function buildLocalHubEntry(info) {
2199
+ let description = "";
2200
+ if (info.path) {
2201
+ try {
2202
+ description = extractSkillDescriptionFromMarkdown(await readFile2(info.path, "utf8"));
2203
+ } catch {
2204
+ }
2205
+ }
1931
2206
  return {
1932
2207
  name: info.name,
1933
2208
  owner: "local",
1934
- description: "",
2209
+ description,
1935
2210
  displayName: "",
1936
2211
  publishedAt: 0,
1937
2212
  avatarUrl: "",
@@ -1941,6 +2216,143 @@ function buildLocalHubEntry(info) {
1941
2216
  enabled: info.enabled
1942
2217
  };
1943
2218
  }
2219
+ function stripAnsi(value) {
2220
+ return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/gu, "");
2221
+ }
2222
+ function parseNpxSkillsFindOutput(output, installedMap) {
2223
+ const lines = stripAnsi(output).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
2224
+ const results = [];
2225
+ for (let index = 0; index < lines.length; index += 1) {
2226
+ const line = lines[index] ?? "";
2227
+ const match = line.match(/^(.+?@[^@\s]+)\s+([\d.]+[KMB]?)\s+installs$/iu);
2228
+ if (!match) continue;
2229
+ const source = match[1]?.trim() ?? "";
2230
+ const installs = match[2]?.trim() ?? "";
2231
+ const atIndex = source.lastIndexOf("@");
2232
+ if (atIndex <= 0 || atIndex >= source.length - 1) continue;
2233
+ const owner = source.slice(0, atIndex);
2234
+ const name = source.slice(atIndex + 1);
2235
+ let url = "";
2236
+ const next = lines[index + 1] ?? "";
2237
+ const urlMatch = next.match(/(?:^└\s*)?(https?:\/\/\S+)$/u);
2238
+ if (urlMatch?.[1]) {
2239
+ url = urlMatch[1];
2240
+ index += 1;
2241
+ }
2242
+ const installedInfo = installedMap.get(name);
2243
+ results.push({
2244
+ name,
2245
+ owner,
2246
+ displayName: name,
2247
+ description: installs ? `${installs} installs` : "",
2248
+ installCountLabel: installs ? `${installs} installs` : "",
2249
+ publishedAt: 0,
2250
+ avatarUrl: "",
2251
+ url,
2252
+ installed: Boolean(installedInfo),
2253
+ source,
2254
+ path: installedInfo?.path,
2255
+ enabled: installedInfo?.enabled
2256
+ });
2257
+ }
2258
+ return results;
2259
+ }
2260
+ function parseGithubSkillSource(source) {
2261
+ const atIndex = source.lastIndexOf("@");
2262
+ if (atIndex <= 0 || atIndex >= source.length - 1) return null;
2263
+ const ownerRepo = source.slice(0, atIndex).trim();
2264
+ const skillName = source.slice(atIndex + 1).trim();
2265
+ const ownerRepoParts = ownerRepo.split("/").filter(Boolean);
2266
+ if (ownerRepoParts.length !== 2 || skillName.length === 0) return null;
2267
+ if (ownerRepoParts.some((part) => part.includes(":") || part.includes(" "))) return null;
2268
+ return { ownerRepo, skillName };
2269
+ }
2270
+ function getGithubOwnerAvatarUrl(source) {
2271
+ const parsed = parseGithubSkillSource(source);
2272
+ if (!parsed) return "";
2273
+ const owner = parsed.ownerRepo.split("/")[0] ?? "";
2274
+ return owner ? `https://github.com/${encodeURIComponent(owner)}.png?size=64` : "";
2275
+ }
2276
+ function buildGithubSkillRawCandidates(source) {
2277
+ const parsed = parseGithubSkillSource(source);
2278
+ if (!parsed) return [];
2279
+ const ownerRepo = parsed.ownerRepo.split("/").map(encodeURIComponent).join("/");
2280
+ const skillName = encodeURIComponent(parsed.skillName);
2281
+ const branches = ["main", "master"];
2282
+ const paths = [
2283
+ `skills/${skillName}/SKILL.md`,
2284
+ `${skillName}/SKILL.md`,
2285
+ "SKILL.md"
2286
+ ];
2287
+ return branches.flatMap((branch) => paths.map((path) => `https://raw.githubusercontent.com/${ownerRepo}/${branch}/${path}`));
2288
+ }
2289
+ async function fetchTextWithTimeout(url, timeoutMs) {
2290
+ const controller = new AbortController();
2291
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2292
+ try {
2293
+ const resp = await fetch(url, {
2294
+ headers: { "User-Agent": "codex-web-local" },
2295
+ signal: controller.signal
2296
+ });
2297
+ if (!resp.ok) return "";
2298
+ return await resp.text();
2299
+ } finally {
2300
+ clearTimeout(timeout);
2301
+ }
2302
+ }
2303
+ function resolveSkillIconUrl(icon, markdownUrl) {
2304
+ const value = icon.trim().replace(/^['"]|['"]$/gu, "");
2305
+ if (!value) return "";
2306
+ if (/^https?:\/\//iu.test(value)) return value;
2307
+ try {
2308
+ return new URL(value, markdownUrl).toString();
2309
+ } catch {
2310
+ return "";
2311
+ }
2312
+ }
2313
+ async function fetchGithubSkillMetadata(source) {
2314
+ for (const candidate of buildGithubSkillRawCandidates(source)) {
2315
+ try {
2316
+ const markdown = await fetchTextWithTimeout(candidate, 4e3);
2317
+ if (!markdown) continue;
2318
+ const description = extractSkillDescriptionFromMarkdown(markdown);
2319
+ const icon = extractSkillFrontmatterField(markdown, "icon");
2320
+ const avatarUrl = icon ? resolveSkillIconUrl(icon, candidate) : getGithubOwnerAvatarUrl(source);
2321
+ if (description || avatarUrl) return { description, avatarUrl };
2322
+ } catch {
2323
+ }
2324
+ }
2325
+ return { avatarUrl: getGithubOwnerAvatarUrl(source) };
2326
+ }
2327
+ async function mapWithConcurrency(items, concurrency, mapper) {
2328
+ const results = new Array(items.length);
2329
+ let nextIndex = 0;
2330
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
2331
+ await Promise.all(Array.from({ length: workerCount }, async () => {
2332
+ while (nextIndex < items.length) {
2333
+ const index = nextIndex;
2334
+ nextIndex += 1;
2335
+ results[index] = await mapper(items[index], index);
2336
+ }
2337
+ }));
2338
+ return results;
2339
+ }
2340
+ async function enrichSkillSearchDescriptions(results) {
2341
+ const enrichedHead = await mapWithConcurrency(
2342
+ results.slice(0, SKILL_SEARCH_METADATA_LIMIT),
2343
+ SKILL_SEARCH_METADATA_CONCURRENCY,
2344
+ async (result) => {
2345
+ if (!result.source) return result;
2346
+ const metadata = await fetchGithubSkillMetadata(result.source);
2347
+ return {
2348
+ ...result,
2349
+ description: metadata.description || result.description,
2350
+ avatarUrl: metadata.avatarUrl || result.avatarUrl
2351
+ };
2352
+ }
2353
+ );
2354
+ return [...enrichedHead, ...results.slice(SKILL_SEARCH_METADATA_LIMIT)];
2355
+ }
1944
2356
  function groupRpcSkillRecords(skills) {
1945
2357
  const normalizedPathSet = new Set(
1946
2358
  skills.map((skill) => normalizeSkillMarkdownPath(typeof skill.path === "string" ? skill.path : "")).filter(Boolean)
@@ -2027,7 +2439,40 @@ async function scanInstalledSkillsFromDisk() {
2027
2439
  }
2028
2440
  return map;
2029
2441
  }
2442
+ async function collectInstalledSkillsMap(appServer) {
2443
+ const installedMap = await scanInstalledSkillsFromDisk();
2444
+ try {
2445
+ const result = await appServer.rpc("skills/list", {});
2446
+ for (const entry of result.data ?? []) {
2447
+ for (const skill of groupRpcSkillRecords(entry.skills ?? [])) {
2448
+ if (skill.name) {
2449
+ installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2450
+ }
2451
+ }
2452
+ }
2453
+ } catch {
2454
+ }
2455
+ return installedMap;
2456
+ }
2457
+ function extractSkillFrontmatterField(markdown, fieldName) {
2458
+ const lines = markdown.split(/\r?\n/);
2459
+ if (lines[0]?.trim() !== "---") return "";
2460
+ const frontmatter = [];
2461
+ for (let index = 1; index < lines.length; index += 1) {
2462
+ const line = lines[index] ?? "";
2463
+ if (line.trim() === "---") break;
2464
+ frontmatter.push(line);
2465
+ }
2466
+ const escapedFieldName = fieldName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
2467
+ const fieldPattern = new RegExp(`^${escapedFieldName}\\s*:`, "iu");
2468
+ const valuePattern = new RegExp(`^${escapedFieldName}\\s*:\\s*`, "iu");
2469
+ const fieldLine = frontmatter.find((line) => fieldPattern.test(line.trim()));
2470
+ if (!fieldLine) return "";
2471
+ return fieldLine.replace(valuePattern, "").replace(/^['"]|['"]$/gu, "").trim();
2472
+ }
2030
2473
  function extractSkillDescriptionFromMarkdown(markdown) {
2474
+ const frontmatterDescription = extractSkillFrontmatterField(markdown, "description");
2475
+ if (frontmatterDescription) return frontmatterDescription;
2031
2476
  const lines = markdown.split(/\r?\n/);
2032
2477
  let inCodeFence = false;
2033
2478
  for (const rawLine of lines) {
@@ -2229,6 +2674,7 @@ async function readRemoteSkillsManifest(token, repoOwner, repoName) {
2229
2674
  async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
2230
2675
  const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
2231
2676
  let sha = "";
2677
+ const nextContent = JSON.stringify(skills, null, 2);
2232
2678
  const existing = await fetch(url, {
2233
2679
  headers: {
2234
2680
  Accept: "application/vnd.github+json",
@@ -2240,13 +2686,16 @@ async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
2240
2686
  if (existing.ok) {
2241
2687
  const payload = await existing.json();
2242
2688
  sha = payload.sha ?? "";
2689
+ const currentContent = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "";
2690
+ if (currentContent === nextContent) return false;
2243
2691
  }
2244
- const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
2692
+ const content = Buffer.from(nextContent, "utf8").toString("base64");
2245
2693
  await getGithubJson(url, token, "PUT", {
2246
2694
  message: "Update synced skills manifest",
2247
2695
  content,
2248
2696
  ...sha ? { sha } : {}
2249
2697
  });
2698
+ return true;
2250
2699
  }
2251
2700
  function toGitHubTokenRemote(repoOwner, repoName, token) {
2252
2701
  return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
@@ -2417,6 +2866,16 @@ async function hasLocalUncommittedChanges(repoDir) {
2417
2866
  const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
2418
2867
  return status.length > 0;
2419
2868
  }
2869
+ async function hasCommittableWorkingTreeChanges(repoDir) {
2870
+ try {
2871
+ await runCommand2("git", ["diff", "--quiet", "--exit-code", "--ignore-submodules=dirty"], { cwd: repoDir });
2872
+ await runCommand2("git", ["diff", "--cached", "--quiet", "--exit-code", "--ignore-submodules=dirty"], { cwd: repoDir });
2873
+ } catch {
2874
+ return true;
2875
+ }
2876
+ const untracked = (await runCommandWithOutput("git", ["ls-files", "--others", "--exclude-standard"], { cwd: repoDir })).trim();
2877
+ return untracked.length > 0;
2878
+ }
2420
2879
  async function walkFileMtimes(rootDir, currentDir, out) {
2421
2880
  let entries;
2422
2881
  try {
@@ -2525,8 +2984,11 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
2525
2984
  await runCommand2("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
2526
2985
  await restoreProtectedFilesFromOrigin(repoDir, branch);
2527
2986
  await runCommand2("git", ["add", "."], { cwd: repoDir });
2528
- const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
2529
- if (!status) return;
2987
+ try {
2988
+ await runCommand2("git", ["diff", "--cached", "--quiet", "--exit-code"], { cwd: repoDir });
2989
+ return;
2990
+ } catch {
2991
+ }
2530
2992
  await runCommand2("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
2531
2993
  await pushWithNonFastForwardRetry(repoDir, branch);
2532
2994
  }
@@ -2572,8 +3034,8 @@ async function autoPushSyncedSkills(appServer) {
2572
3034
  await runCommand2("git", ["fetch", "origin", PRIVATE_SYNC_BRANCH], { cwd: repoDir });
2573
3035
  const head = (await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: repoDir })).trim();
2574
3036
  const originHead = (await runCommandWithOutput("git", ["rev-parse", `origin/${PRIVATE_SYNC_BRANCH}`], { cwd: repoDir })).trim();
2575
- const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
2576
- if (!status && head === originHead) return;
3037
+ const hasCommittableChanges = await hasCommittableWorkingTreeChanges(repoDir);
3038
+ if (!hasCommittableChanges && head === originHead) return;
2577
3039
  const local = await collectLocalSyncedSkills(appServer);
2578
3040
  const installedMap = await scanInstalledSkillsFromDisk();
2579
3041
  await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
@@ -2689,25 +3151,11 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
2689
3151
  await autoPushSyncedSkills(appServer);
2690
3152
  }
2691
3153
  async function handleSkillsRoutes(req, res, url, context) {
2692
- const { appServer, readJsonBody: readJsonBody2 } = context;
3154
+ const { appServer, readJsonBody: readJsonBody3 } = context;
2693
3155
  if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
2694
3156
  try {
2695
- const installedMap = await scanInstalledSkillsFromDisk();
2696
- try {
2697
- const result = await appServer.rpc("skills/list", {});
2698
- for (const entry of result.data ?? []) {
2699
- for (const skill of groupRpcSkillRecords(entry.skills ?? [])) {
2700
- if (skill.name) {
2701
- installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2702
- }
2703
- }
2704
- }
2705
- } catch {
2706
- }
2707
- const installed = [];
2708
- for (const [, info] of installedMap) {
2709
- installed.push(buildLocalHubEntry(info));
2710
- }
3157
+ const installedMap = await collectInstalledSkillsMap(appServer);
3158
+ const installed = await Promise.all([...installedMap.values()].map((info) => buildLocalHubEntry(info)));
2711
3159
  installed.sort((a, b) => a.name.localeCompare(b.name));
2712
3160
  setJson3(res, 200, { installed });
2713
3161
  } catch (error) {
@@ -2715,6 +3163,22 @@ async function handleSkillsRoutes(req, res, url, context) {
2715
3163
  }
2716
3164
  return true;
2717
3165
  }
3166
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/search") {
3167
+ try {
3168
+ const query = (url.searchParams.get("q") || "").trim();
3169
+ if (query.length < 2) {
3170
+ setJson3(res, 200, { results: [] });
3171
+ return true;
3172
+ }
3173
+ const installedMap = await collectInstalledSkillsMap(appServer);
3174
+ const output = await runCommandWithOutput("npx", ["--yes", "skills", "find", query], { timeoutMs: 6e4 });
3175
+ const results = await enrichSkillSearchDescriptions(parseNpxSkillsFindOutput(output, installedMap));
3176
+ setJson3(res, 200, { results });
3177
+ } catch (error) {
3178
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to search skills") });
3179
+ }
3180
+ return true;
3181
+ }
2718
3182
  if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
2719
3183
  const state = await readSkillsSyncState();
2720
3184
  setJson3(res, 200, {
@@ -2755,7 +3219,7 @@ async function handleSkillsRoutes(req, res, url, context) {
2755
3219
  }
2756
3220
  if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
2757
3221
  try {
2758
- const payload = asRecord3(await readJsonBody2(req));
3222
+ const payload = asRecord3(await readJsonBody3(req));
2759
3223
  const token = typeof payload?.token === "string" ? payload.token.trim() : "";
2760
3224
  if (!token) {
2761
3225
  setJson3(res, 400, { error: "Missing GitHub token" });
@@ -2787,7 +3251,7 @@ async function handleSkillsRoutes(req, res, url, context) {
2787
3251
  }
2788
3252
  if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
2789
3253
  try {
2790
- const payload = asRecord3(await readJsonBody2(req));
3254
+ const payload = asRecord3(await readJsonBody3(req));
2791
3255
  const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
2792
3256
  if (!deviceCode) {
2793
3257
  setJson3(res, 400, { error: "Missing deviceCode" });
@@ -2819,7 +3283,7 @@ async function handleSkillsRoutes(req, res, url, context) {
2819
3283
  return true;
2820
3284
  }
2821
3285
  const local = await collectLocalSyncedSkills(appServer);
2822
- const installedMap = await scanInstalledSkillsFromDisk();
3286
+ const installedMap = await collectInstalledSkillsMap(appServer);
2823
3287
  await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
2824
3288
  await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
2825
3289
  setJson3(res, 200, { ok: true, data: { synced: local.length } });
@@ -2925,12 +3389,38 @@ async function handleSkillsRoutes(req, res, url, context) {
2925
3389
  return true;
2926
3390
  }
2927
3391
  if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
2928
- setJson3(res, 410, { error: "Remote Skills Hub installation is disabled." });
3392
+ try {
3393
+ const payload = asRecord3(await readJsonBody3(req));
3394
+ const source = typeof payload?.source === "string" ? payload.source.trim() : "";
3395
+ const owner = typeof payload?.owner === "string" ? payload.owner.trim() : "";
3396
+ const name = typeof payload?.name === "string" ? payload.name.trim() : "";
3397
+ const installSource = source || (owner && name ? `${owner}@${name}` : "");
3398
+ if (!installSource || !/^[A-Za-z0-9._/-]+@[A-Za-z0-9._-]+$/u.test(installSource)) {
3399
+ setJson3(res, 400, { error: "Missing or invalid skill source" });
3400
+ return true;
3401
+ }
3402
+ await runCommand2("npx", ["--yes", "skills", "add", installSource, "--yes", "--global"], { timeoutMs: 12e4 });
3403
+ try {
3404
+ await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
3405
+ } catch {
3406
+ }
3407
+ const installedMap = await collectInstalledSkillsMap(appServer);
3408
+ const installed = installedMap.get(name || installSource.slice(installSource.lastIndexOf("@") + 1));
3409
+ if (!installed?.path) {
3410
+ throw new Error(`Skill install completed but ${installSource} was not found in local installed skills`);
3411
+ }
3412
+ await ensureInstalledSkillIsValid(appServer, installed.path);
3413
+ autoPushSyncedSkills(appServer).catch(() => {
3414
+ });
3415
+ setJson3(res, 200, { ok: true, path: installed.path });
3416
+ } catch (error) {
3417
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
3418
+ }
2929
3419
  return true;
2930
3420
  }
2931
3421
  if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
2932
3422
  try {
2933
- const payload = asRecord3(await readJsonBody2(req));
3423
+ const payload = asRecord3(await readJsonBody3(req));
2934
3424
  const name = typeof payload?.name === "string" ? payload.name : "";
2935
3425
  const path = typeof payload?.path === "string" ? payload.path : "";
2936
3426
  const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
@@ -2962,7 +3452,7 @@ async function handleSkillsRoutes(req, res, url, context) {
2962
3452
  }
2963
3453
 
2964
3454
  // src/server/telegramThreadBridge.ts
2965
- import { basename } from "path";
3455
+ import { basename as basename2 } from "path";
2966
3456
  var TELEGRAM_MESSAGE_MAX_LENGTH = 3500;
2967
3457
  var TELEGRAM_BOT_COMMANDS = [
2968
3458
  { command: "start", description: "Show quick start and thread picker" },
@@ -3448,7 +3938,7 @@ Add this ID to the bot allowlist before using the bridge.`;
3448
3938
  const name = typeof record?.name === "string" ? record.name.trim() : "";
3449
3939
  const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
3450
3940
  const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
3451
- const projectName = cwd ? basename(cwd) : "project";
3941
+ const projectName = cwd ? basename2(cwd) : "project";
3452
3942
  const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
3453
3943
  const title = `${projectName}/${threadTitle}`.slice(0, 64);
3454
3944
  threads.push({ id, title });
@@ -3675,6 +4165,7 @@ var FALLBACK_FREE_MODELS = [
3675
4165
  var cachedFreeModels = null;
3676
4166
  var cacheTimestamp = 0;
3677
4167
  var CACHE_TTL_MS = 10 * 60 * 1e3;
4168
+ var freeModelsRefreshPromise = null;
3678
4169
  async function fetchFreeModelsFromOpenRouter() {
3679
4170
  try {
3680
4171
  const resp = await fetch("https://openrouter.ai/api/v1/models");
@@ -3696,11 +4187,36 @@ async function getFreeModels() {
3696
4187
  }
3697
4188
  return fetchFreeModelsFromOpenRouter();
3698
4189
  }
4190
+ function getCachedFreeModels() {
4191
+ return cachedFreeModels ?? FALLBACK_FREE_MODELS;
4192
+ }
4193
+ function refreshFreeModelsInBackground() {
4194
+ if (cachedFreeModels && Date.now() - cacheTimestamp < CACHE_TTL_MS) return;
4195
+ if (freeModelsRefreshPromise) return;
4196
+ freeModelsRefreshPromise = fetchFreeModelsFromOpenRouter().finally(() => {
4197
+ freeModelsRefreshPromise = null;
4198
+ });
4199
+ }
3699
4200
  var FREE_MODE_DEFAULT_MODEL = "openrouter/free";
3700
4201
  var FREE_MODE_STATE_FILE = "webui-free-mode.json";
3701
4202
  var CUSTOM_PROVIDER_ID = "custom-endpoint";
3702
4203
  var OPENCODE_ZEN_PROVIDER_ID = "opencode-zen";
3703
4204
  var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
4205
+ var OPENCODE_ZEN_DEFAULT_MODEL = "big-pickle";
4206
+ function createDefaultOpenCodeZenFreeModeState() {
4207
+ return {
4208
+ enabled: true,
4209
+ apiKey: null,
4210
+ model: OPENCODE_ZEN_DEFAULT_MODEL,
4211
+ customKey: false,
4212
+ provider: "opencode-zen",
4213
+ wireApi: "chat",
4214
+ providerKeys: {}
4215
+ };
4216
+ }
4217
+ function shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuth) {
4218
+ return current == null && !hasUsableCodexAuth;
4219
+ }
3704
4220
  function getFreeModeEnvVars(state) {
3705
4221
  if (!state.enabled) return {};
3706
4222
  if (state.provider === "opencode-zen" && state.apiKey) {
@@ -3717,7 +4233,9 @@ function getFreeModeConfigArgs(state, serverPort) {
3717
4233
  const baseUrl2 = serverPort ? `http://127.0.0.1:${serverPort}/codex-api/zen-proxy/v1` : OPENCODE_ZEN_BASE_URL;
3718
4234
  const wireApi = serverPort ? "responses" : state.wireApi || "chat";
3719
4235
  const authArgs = serverPort ? ["-c", `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.experimental_bearer_token="zen-proxy-token"`] : ["-c", `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.env_key="OPENCODE_ZEN_API_KEY"`];
4236
+ const modelArgs = state.model?.trim() ? ["-c", `model="${state.model.trim()}"`] : [];
3720
4237
  return [
4238
+ ...modelArgs,
3721
4239
  "-c",
3722
4240
  `model_provider="${OPENCODE_ZEN_PROVIDER_ID}"`,
3723
4241
  "-c",
@@ -3785,27 +4303,52 @@ function safeStringifyUnknown(value) {
3785
4303
  return String(value ?? "");
3786
4304
  }
3787
4305
  }
3788
- function appendAssistantText(messages, text) {
4306
+ function appendAssistantText(messages, text, reasoningContent) {
3789
4307
  const trimmedText = text.trim();
3790
- if (!trimmedText) return;
4308
+ const trimmedReasoningContent = reasoningContent?.trim() ?? "";
4309
+ if (!trimmedText && !trimmedReasoningContent) return;
3791
4310
  const lastMessage = messages[messages.length - 1];
3792
4311
  if (lastMessage?.role === "assistant" && Array.isArray(lastMessage.tool_calls)) {
3793
4312
  lastMessage.content = lastMessage.content ? `${lastMessage.content}
3794
4313
  ${trimmedText}` : trimmedText;
4314
+ if (trimmedReasoningContent) {
4315
+ lastMessage.reasoning_content = lastMessage.reasoning_content ? `${lastMessage.reasoning_content}
4316
+ ${trimmedReasoningContent}` : trimmedReasoningContent;
4317
+ }
3795
4318
  return;
3796
4319
  }
3797
- messages.push({ role: "assistant", content: trimmedText });
4320
+ messages.push({
4321
+ role: "assistant",
4322
+ content: trimmedText,
4323
+ ...trimmedReasoningContent ? { reasoning_content: trimmedReasoningContent } : {}
4324
+ });
3798
4325
  }
3799
- function appendAssistantToolCall(messages, toolCall) {
4326
+ function appendAssistantToolCall(messages, toolCall, reasoningContent) {
4327
+ const trimmedReasoningContent = reasoningContent?.trim() ?? "";
3800
4328
  const lastMessage = messages[messages.length - 1];
3801
4329
  if (lastMessage?.role === "assistant" && !lastMessage.tool_call_id) {
3802
4330
  lastMessage.tool_calls = [...lastMessage.tool_calls ?? [], toolCall];
4331
+ if (trimmedReasoningContent) {
4332
+ lastMessage.reasoning_content = lastMessage.reasoning_content ? `${lastMessage.reasoning_content}
4333
+ ${trimmedReasoningContent}` : trimmedReasoningContent;
4334
+ }
3803
4335
  return;
3804
4336
  }
3805
- messages.push({ role: "assistant", tool_calls: [toolCall] });
4337
+ messages.push({
4338
+ role: "assistant",
4339
+ content: "",
4340
+ tool_calls: [toolCall],
4341
+ ...trimmedReasoningContent ? { reasoning_content: trimmedReasoningContent } : {}
4342
+ });
4343
+ }
4344
+ function extractTextParts(value) {
4345
+ if (typeof value === "string") return value;
4346
+ if (!Array.isArray(value)) return "";
4347
+ return value.map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter((part) => part.length > 0).join("\n");
3806
4348
  }
3807
4349
  function responsesInputToMessages(input, instructions) {
3808
4350
  const messages = [];
4351
+ let pendingReasoningContent = "";
3809
4352
  if (instructions) {
3810
4353
  messages.push({ role: "system", content: instructions });
3811
4354
  }
@@ -3815,12 +4358,29 @@ function responsesInputToMessages(input, instructions) {
3815
4358
  }
3816
4359
  for (const item of input) {
3817
4360
  if (!item || typeof item !== "object") continue;
4361
+ if (item.type === "reasoning") {
4362
+ const content = extractTextParts(item.content);
4363
+ const summary = extractTextParts(item.summary);
4364
+ const text = content || summary;
4365
+ if (text) {
4366
+ const lastMessage = messages[messages.length - 1];
4367
+ if (lastMessage?.role === "assistant") {
4368
+ lastMessage.reasoning_content = lastMessage.reasoning_content ? `${lastMessage.reasoning_content}
4369
+ ${text}` : text;
4370
+ } else {
4371
+ pendingReasoningContent = pendingReasoningContent ? `${pendingReasoningContent}
4372
+ ${text}` : text;
4373
+ }
4374
+ }
4375
+ continue;
4376
+ }
3818
4377
  if (item.type === "message" && item.role) {
3819
4378
  const content = item.content;
3820
4379
  const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => typeof part?.text === "string" ? part.text : "").filter((part) => part.length > 0).join("\n") : typeof item.text === "string" ? item.text : "";
3821
4380
  const role = item.role === "developer" ? "system" : item.role;
3822
4381
  if (role === "assistant") {
3823
- appendAssistantText(messages, text);
4382
+ appendAssistantText(messages, text, pendingReasoningContent);
4383
+ pendingReasoningContent = "";
3824
4384
  } else {
3825
4385
  messages.push({ role, content: text });
3826
4386
  }
@@ -3842,7 +4402,8 @@ function responsesInputToMessages(input, instructions) {
3842
4402
  name: item.name,
3843
4403
  arguments: typeof item.arguments === "string" ? item.arguments : "{}"
3844
4404
  }
3845
- });
4405
+ }, pendingReasoningContent);
4406
+ pendingReasoningContent = "";
3846
4407
  }
3847
4408
  }
3848
4409
  return messages;
@@ -3905,6 +4466,14 @@ function chatCompletionToResponsesFormat(chatResponse, model) {
3905
4466
  status: "completed"
3906
4467
  });
3907
4468
  }
4469
+ if (message.reasoning_content) {
4470
+ output.push({
4471
+ type: "reasoning",
4472
+ id: `rs_${Date.now()}`,
4473
+ summary: [],
4474
+ content: [{ type: "reasoning_text", text: message.reasoning_content }]
4475
+ });
4476
+ }
3908
4477
  }
3909
4478
  const usage = chatResponse.usage;
3910
4479
  return {
@@ -3929,6 +4498,7 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
3929
4498
  });
3930
4499
  let buffer = "";
3931
4500
  const contentParts = [];
4501
+ const reasoningParts = [];
3932
4502
  let responseId = `resp_${Date.now()}`;
3933
4503
  res.write(`data: {"type":"response.created","response":{"id":"${responseId}","object":"response","status":"in_progress","model":"${model}","output":[]}}
3934
4504
 
@@ -3947,6 +4517,9 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
3947
4517
  const parsed = JSON.parse(data);
3948
4518
  if (parsed.id) responseId = `resp_${parsed.id}`;
3949
4519
  const delta = parsed.choices?.[0]?.delta;
4520
+ if (delta?.reasoning_content) {
4521
+ reasoningParts.push(delta.reasoning_content);
4522
+ }
3950
4523
  if (delta?.content) {
3951
4524
  contentParts.push(delta.content);
3952
4525
  const escaped = JSON.stringify(delta.content).slice(1, -1);
@@ -3960,7 +4533,18 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
3960
4533
  });
3961
4534
  upstreamRes.on("end", () => {
3962
4535
  const fullText = contentParts.join("");
4536
+ const fullReasoningText = reasoningParts.join("");
3963
4537
  const escapedFull = JSON.stringify(fullText).slice(1, -1);
4538
+ const messageItem = { type: "message", role: "assistant", content: [{ type: "output_text", text: fullText }], status: "completed" };
4539
+ const output = [messageItem];
4540
+ if (fullReasoningText) {
4541
+ output.push({
4542
+ type: "reasoning",
4543
+ id: `rs_${Date.now()}`,
4544
+ summary: [],
4545
+ content: [{ type: "reasoning_text", text: fullReasoningText }]
4546
+ });
4547
+ }
3964
4548
  res.write(`data: {"type":"response.output_text.done","output_index":0,"content_index":0,"text":"${escapedFull}"}
3965
4549
 
3966
4550
  `);
@@ -3970,7 +4554,17 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
3970
4554
  res.write(`data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"${escapedFull}"}],"status":"completed"}}
3971
4555
 
3972
4556
  `);
3973
- res.write(`data: {"type":"response.completed","response":{"id":"${responseId}","object":"response","status":"completed","model":"${model}","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"${escapedFull}"}],"status":"completed"}]}}
4557
+ if (fullReasoningText) {
4558
+ const reasoningIndex = output.length - 1;
4559
+ const reasoningItem = output[reasoningIndex];
4560
+ res.write(`data: ${JSON.stringify({ type: "response.output_item.added", output_index: reasoningIndex, item: reasoningItem })}
4561
+
4562
+ `);
4563
+ res.write(`data: ${JSON.stringify({ type: "response.output_item.done", output_index: reasoningIndex, item: reasoningItem })}
4564
+
4565
+ `);
4566
+ }
4567
+ res.write(`data: ${JSON.stringify({ type: "response.completed", response: { id: responseId, object: "response", status: "completed", model, output } })}
3974
4568
 
3975
4569
  `);
3976
4570
  res.end();
@@ -4042,7 +4636,7 @@ function hasToolOutputsInInput(input) {
4042
4636
  function handleUnifiedResponsesProxyRequest(req, res, options) {
4043
4637
  void (async () => {
4044
4638
  try {
4045
- if (!options.bearerToken) {
4639
+ if (options.requireBearerToken !== false && !options.bearerToken) {
4046
4640
  res.writeHead(401, { "Content-Type": "application/json" });
4047
4641
  res.end(JSON.stringify({ error: { message: options.missingKeyMessage } }));
4048
4642
  return;
@@ -4055,14 +4649,14 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
4055
4649
  const useChatCompletions = options.wireApi === "chat" && !useResponsesFallback;
4056
4650
  const useChatPayload = useChatCompletions || options.responsesPayloadFormat === "chat";
4057
4651
  const isStreaming = parsedBody.stream === true;
4058
- const effectiveStreaming = useChatCompletions && isStreaming && !(hasTools || hasToolOutputs);
4652
+ const effectiveStreaming = useChatPayload && isStreaming && !(hasTools || hasToolOutputs);
4059
4653
  let payload = "";
4060
4654
  let upstreamUrl;
4061
4655
  if (useChatPayload) {
4062
4656
  const chatReq = {
4063
4657
  model: parsedBody.model,
4064
4658
  messages: responsesInputToMessages(parsedBody.input, parsedBody.instructions),
4065
- stream: useChatCompletions ? effectiveStreaming : isStreaming
4659
+ stream: effectiveStreaming
4066
4660
  };
4067
4661
  if (parsedBody.temperature != null) chatReq.temperature = parsedBody.temperature;
4068
4662
  if (parsedBody.top_p != null) chatReq.top_p = parsedBody.top_p;
@@ -4072,7 +4666,7 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
4072
4666
  if (chatTools) chatReq.tools = chatTools;
4073
4667
  if (chatToolChoice) chatReq.tool_choice = chatToolChoice;
4074
4668
  payload = JSON.stringify(chatReq);
4075
- upstreamUrl = new URL(useChatCompletions ? options.chatCompletionsEndpoint : options.responsesEndpoint);
4669
+ upstreamUrl = new URL(options.chatCompletionsEndpoint);
4076
4670
  } else {
4077
4671
  const requestBody = parsedBody && typeof parsedBody === "object" && !Array.isArray(parsedBody) ? { ...parsedBody } : {};
4078
4672
  const sanitized = options.sanitizeResponsesRequest ? options.sanitizeResponsesRequest(requestBody) : requestBody;
@@ -4088,11 +4682,11 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
4088
4682
  headers: {
4089
4683
  "Content-Type": "application/json",
4090
4684
  "Content-Length": Buffer.byteLength(payload),
4091
- "Authorization": `Bearer ${options.bearerToken}`
4685
+ ...options.bearerToken ? { "Authorization": `Bearer ${options.bearerToken}` } : {}
4092
4686
  }
4093
4687
  }, (upstreamRes) => {
4094
4688
  const status = upstreamRes.statusCode ?? 502;
4095
- if (useChatCompletions && effectiveStreaming && status >= 200 && status < 300) {
4689
+ if (useChatPayload && effectiveStreaming && status >= 200 && status < 300) {
4096
4690
  forwardStreamingTextResponse(upstreamRes, res, parsedBody.model);
4097
4691
  return;
4098
4692
  }
@@ -4100,7 +4694,7 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
4100
4694
  upstreamRes.on("data", (chunk) => chunks.push(chunk));
4101
4695
  upstreamRes.on("end", () => {
4102
4696
  const rawResponseBody = Buffer.concat(chunks).toString();
4103
- if (!useChatCompletions) {
4697
+ if (!useChatPayload) {
4104
4698
  res.writeHead(status, copyProxyHeaders(upstreamRes.headers));
4105
4699
  res.end(rawResponseBody);
4106
4700
  return;
@@ -4108,6 +4702,14 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
4108
4702
  try {
4109
4703
  const upstreamPayload = JSON.parse(rawResponseBody);
4110
4704
  if (upstreamPayload.error || status >= 400) {
4705
+ if (process.env.CODEXUI_PROXY_DEBUG === "1") {
4706
+ console.warn("[unified-responses-proxy]", JSON.stringify({
4707
+ status,
4708
+ upstreamUrl: upstreamUrl.toString(),
4709
+ request: JSON.parse(payload),
4710
+ response: upstreamPayload
4711
+ }));
4712
+ }
4111
4713
  res.writeHead(status, { "Content-Type": "application/json" });
4112
4714
  res.end(JSON.stringify(upstreamPayload));
4113
4715
  return;
@@ -4193,6 +4795,7 @@ function handleZenProxyRequest(req, res, bearerToken, wireApi) {
4193
4795
  responsesEndpoint: ZEN_RESPONSES_ENDPOINT,
4194
4796
  chatCompletionsEndpoint: ZEN_CHAT_COMPLETIONS_ENDPOINT,
4195
4797
  missingKeyMessage: "Missing OpenCode Zen API key",
4798
+ requireBearerToken: false,
4196
4799
  allowToolFallbackToResponses: false,
4197
4800
  responsesPayloadFormat: "chat"
4198
4801
  });
@@ -4217,9 +4820,9 @@ function handleCustomEndpointProxyRequest(req, res, options) {
4217
4820
  import { chmodSync, existsSync as existsSync3, lstatSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
4218
4821
  import { randomUUID } from "crypto";
4219
4822
  import { createRequire } from "module";
4220
- import { basename as basename2, dirname, join as join5 } from "path";
4823
+ import { basename as basename3, dirname, join as join5 } from "path";
4221
4824
  import { homedir as homedir4 } from "os";
4222
- import { spawnSync as spawnSync2 } from "child_process";
4825
+ import { spawnSync as spawnSync3 } from "child_process";
4223
4826
  var TERMINAL_BUFFER_LIMIT = 16 * 1024;
4224
4827
  var DEFAULT_COLS = 80;
4225
4828
  var DEFAULT_ROWS = 24;
@@ -4352,7 +4955,7 @@ var ThreadTerminalManager = class {
4352
4955
  id: params.sessionId,
4353
4956
  threadId: params.threadId,
4354
4957
  cwd,
4355
- shell: basename2(shell),
4958
+ shell: basename3(shell),
4356
4959
  pty,
4357
4960
  buffer: "",
4358
4961
  truncated: false
@@ -4492,7 +5095,6 @@ function normalizeDimension(value, fallback) {
4492
5095
  return Math.max(1, Math.min(500, Math.trunc(parsed)));
4493
5096
  }
4494
5097
  function loadTerminalSpawn() {
4495
- repairNativePtyBuild("node-pty-prebuilt-multiarch");
4496
5098
  repairNativePtyBuild("node-pty");
4497
5099
  if (resolveNodePtyPrebuiltPath()) {
4498
5100
  try {
@@ -4522,7 +5124,7 @@ function repairNativePtyBuild(packageName) {
4522
5124
  writeFileSync(makefile, patched);
4523
5125
  }
4524
5126
  rmSync(binary, { force: true });
4525
- spawnSync2("make", ["BUILDTYPE=Release", "-C", buildDir], { stdio: "ignore" });
5127
+ spawnSync3("make", ["BUILDTYPE=Release", "-C", buildDir], { stdio: "ignore" });
4526
5128
  } catch {
4527
5129
  }
4528
5130
  }
@@ -4557,9 +5159,12 @@ function resolveNodePtyPrebuiltPath() {
4557
5159
  }
4558
5160
  function ensureNodePtyPrebuiltExecutable() {
4559
5161
  if (process.platform !== "darwin" && process.platform !== "linux") return;
5162
+ ensurePackageSpawnHelperExecutable("node-pty");
5163
+ ensurePackageSpawnHelperExecutable("node-pty-prebuilt-multiarch");
5164
+ }
5165
+ function ensurePackageSpawnHelperExecutable(packageName) {
4560
5166
  try {
4561
- const nodePtyEntry = require2.resolve("node-pty-prebuilt-multiarch");
4562
- const packageRoot = join5(dirname(nodePtyEntry), "..");
5167
+ const packageRoot = dirname(require2.resolve(`${packageName}/package.json`));
4563
5168
  const helperPath = join5(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
4564
5169
  if (existsSync3(helperPath)) {
4565
5170
  chmodSync(helperPath, 493);
@@ -4577,45 +5182,6 @@ function shellQuote(value) {
4577
5182
  return `'${value.replace(/'/g, `'\\''`)}'`;
4578
5183
  }
4579
5184
 
4580
- // src/utils/commandInvocation.ts
4581
- import { spawnSync as spawnSync3 } from "child_process";
4582
- import { basename as basename3, extname } from "path";
4583
- var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
4584
- function quoteCmdExeArg(value) {
4585
- const normalized = value.replace(/"/g, '""');
4586
- if (!/[\s"]/u.test(normalized)) {
4587
- return normalized;
4588
- }
4589
- return `"${normalized}"`;
4590
- }
4591
- function needsCmdExeWrapper(command) {
4592
- if (process.platform !== "win32") {
4593
- return false;
4594
- }
4595
- const lowerCommand = command.toLowerCase();
4596
- const baseName = basename3(lowerCommand);
4597
- if (/\.(cmd|bat)$/i.test(baseName)) {
4598
- return true;
4599
- }
4600
- if (extname(baseName)) {
4601
- return false;
4602
- }
4603
- return WINDOWS_CMD_NAMES.has(baseName);
4604
- }
4605
- function getSpawnInvocation(command, args = []) {
4606
- if (needsCmdExeWrapper(command)) {
4607
- return {
4608
- command: "cmd.exe",
4609
- args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
4610
- };
4611
- }
4612
- return { command, args };
4613
- }
4614
- function spawnSyncCommand(command, args = [], options = {}) {
4615
- const invocation = getSpawnInvocation(command, args);
4616
- return spawnSync3(invocation.command, invocation.args, options);
4617
- }
4618
-
4619
5185
  // src/server/codexAppServerBridge.ts
4620
5186
  var COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX = 1e3;
4621
5187
  var PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5e3;
@@ -4631,6 +5197,152 @@ var DEFAULT_API_PERF_MS_THRESHOLD = 300;
4631
5197
  var DEFAULT_API_PERF_BODY_MB_THRESHOLD = 1;
4632
5198
  var MB_DIVISOR = 1024 * 1024;
4633
5199
  var COMPOSIO_USER_DATA_PATH = join6(homedir5(), ".composio", "user_data.json");
5200
+ var SESSION_SKILL_INPUT_CACHE_LIMIT = 64;
5201
+ var sessionSkillInputCache = /* @__PURE__ */ new Map();
5202
+ function parseSessionSkillText(value) {
5203
+ const trimmed = value.trim();
5204
+ if (!trimmed.startsWith("<skill>")) return null;
5205
+ const name = trimmed.match(/<name>\s*([\s\S]*?)\s*<\/name>/u)?.[1]?.trim() ?? "";
5206
+ const path = trimmed.match(/<path>\s*([\s\S]*?)\s*<\/path>/u)?.[1]?.trim() ?? "";
5207
+ if (!name || !path) return null;
5208
+ return { name, path };
5209
+ }
5210
+ function buildSessionSkillInputsByTurn(sessionLogRaw) {
5211
+ let currentTurnId = "";
5212
+ const skillsByTurnId = /* @__PURE__ */ new Map();
5213
+ for (const line of sessionLogRaw.split("\n")) {
5214
+ if (!line.trim()) continue;
5215
+ let row = null;
5216
+ try {
5217
+ row = JSON.parse(line);
5218
+ } catch {
5219
+ continue;
5220
+ }
5221
+ if (row.type === "turn_context") {
5222
+ const payloadRecord2 = asRecord5(row.payload);
5223
+ currentTurnId = readNonEmptyString(payloadRecord2?.turn_id) || currentTurnId;
5224
+ continue;
5225
+ }
5226
+ if (row.type === "event_msg") {
5227
+ const payloadRecord2 = asRecord5(row.payload);
5228
+ if (payloadRecord2?.type === "task_started") {
5229
+ currentTurnId = readNonEmptyString(payloadRecord2.turn_id) || currentTurnId;
5230
+ }
5231
+ continue;
5232
+ }
5233
+ if (row.type !== "response_item" || !currentTurnId) continue;
5234
+ const payloadRecord = asRecord5(row.payload);
5235
+ if (payloadRecord?.type !== "message" || payloadRecord.role !== "user") continue;
5236
+ const content = Array.isArray(payloadRecord.content) ? payloadRecord.content : [];
5237
+ for (const contentItem of content) {
5238
+ const contentRecord = asRecord5(contentItem);
5239
+ if (contentRecord?.type !== "input_text" || typeof contentRecord.text !== "string") continue;
5240
+ const skill = parseSessionSkillText(contentRecord.text);
5241
+ if (!skill) continue;
5242
+ const existing = skillsByTurnId.get(currentTurnId) ?? [];
5243
+ if (!existing.some((item) => item.path === skill.path)) {
5244
+ existing.push(skill);
5245
+ skillsByTurnId.set(currentTurnId, existing);
5246
+ }
5247
+ }
5248
+ }
5249
+ return skillsByTurnId;
5250
+ }
5251
+ async function readCachedSessionSkillInputsByTurn(sessionPath) {
5252
+ const sessionStat = await stat4(sessionPath);
5253
+ const cached = sessionSkillInputCache.get(sessionPath);
5254
+ if (cached && cached.size === sessionStat.size && cached.mtimeMs === sessionStat.mtimeMs) {
5255
+ return cached.skillsByTurnId;
5256
+ }
5257
+ const sessionLogRaw = await readFile3(sessionPath, "utf8");
5258
+ const skillsByTurnId = buildSessionSkillInputsByTurn(sessionLogRaw);
5259
+ sessionSkillInputCache.set(sessionPath, {
5260
+ size: sessionStat.size,
5261
+ mtimeMs: sessionStat.mtimeMs,
5262
+ skillsByTurnId
5263
+ });
5264
+ if (sessionSkillInputCache.size > SESSION_SKILL_INPUT_CACHE_LIMIT) {
5265
+ const oldestKey = sessionSkillInputCache.keys().next().value;
5266
+ if (oldestKey) sessionSkillInputCache.delete(oldestKey);
5267
+ }
5268
+ return skillsByTurnId;
5269
+ }
5270
+ function mergeSessionSkillInputsIntoTurnsFromMap(turns, skillsByTurnId) {
5271
+ const turnIds = /* @__PURE__ */ new Set();
5272
+ for (const turn of turns) {
5273
+ const turnRecord = asRecord5(turn);
5274
+ const turnId = readNonEmptyString(turnRecord?.id);
5275
+ if (turnId) turnIds.add(turnId);
5276
+ }
5277
+ if (turnIds.size === 0) return turns;
5278
+ if (skillsByTurnId.size === 0) return turns;
5279
+ let changed = false;
5280
+ const nextTurns = turns.map((turn) => {
5281
+ const turnRecord = asRecord5(turn);
5282
+ const turnId = readNonEmptyString(turnRecord?.id);
5283
+ const skills = turnId ? skillsByTurnId.get(turnId) : void 0;
5284
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : null;
5285
+ if (!turnRecord || !skills || skills.length === 0 || !items) return turn;
5286
+ let targetUserMessageIndex = -1;
5287
+ for (let index = items.length - 1; index >= 0; index -= 1) {
5288
+ const itemRecord = asRecord5(items[index]);
5289
+ if (itemRecord?.type === "userMessage" && Array.isArray(itemRecord.content)) {
5290
+ targetUserMessageIndex = index;
5291
+ break;
5292
+ }
5293
+ }
5294
+ if (targetUserMessageIndex < 0) return turn;
5295
+ let addedToMessage = false;
5296
+ const nextItems = items.map((item, index) => {
5297
+ const itemRecord = asRecord5(item);
5298
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : null;
5299
+ if (index !== targetUserMessageIndex || itemRecord?.type !== "userMessage" || !content) return item;
5300
+ const existingSkillPaths = new Set(
5301
+ content.flatMap((contentItem) => {
5302
+ const contentRecord = asRecord5(contentItem);
5303
+ const path = typeof contentRecord?.path === "string" ? contentRecord.path.trim() : "";
5304
+ return contentRecord?.type === "skill" && path ? [path] : [];
5305
+ })
5306
+ );
5307
+ const missingSkills = skills.filter((skill) => !existingSkillPaths.has(skill.path));
5308
+ if (missingSkills.length === 0) return item;
5309
+ addedToMessage = true;
5310
+ changed = true;
5311
+ return {
5312
+ ...itemRecord,
5313
+ content: [
5314
+ ...content,
5315
+ ...missingSkills.map((skill) => ({ type: "skill", name: skill.name, path: skill.path }))
5316
+ ]
5317
+ };
5318
+ });
5319
+ return addedToMessage ? { ...turnRecord, items: nextItems } : turn;
5320
+ });
5321
+ return changed ? nextTurns : turns;
5322
+ }
5323
+ async function mergeSessionSkillInputsIntoThreadResult(result) {
5324
+ const record = asRecord5(result);
5325
+ const thread = asRecord5(record?.thread);
5326
+ const turns = Array.isArray(thread?.turns) ? thread.turns : null;
5327
+ const sessionPath = readNonEmptyString(thread?.path);
5328
+ if (!record || !thread || !turns || turns.length === 0 || !sessionPath || !isAbsolute2(sessionPath)) {
5329
+ return result;
5330
+ }
5331
+ try {
5332
+ const skillsByTurnId = await readCachedSessionSkillInputsByTurn(sessionPath);
5333
+ const mergedTurns = mergeSessionSkillInputsIntoTurnsFromMap(turns, skillsByTurnId);
5334
+ if (mergedTurns === turns) return result;
5335
+ return {
5336
+ ...record,
5337
+ thread: {
5338
+ ...thread,
5339
+ turns: mergedTurns
5340
+ }
5341
+ };
5342
+ } catch {
5343
+ return result;
5344
+ }
5345
+ }
4634
5346
  function readEnvValueFromFile(filePath, key) {
4635
5347
  try {
4636
5348
  const content = readFileSync2(filePath, "utf8");
@@ -5247,6 +5959,48 @@ function extractThreadMessageText(threadReadPayload) {
5247
5959
  function readNonEmptyString(value) {
5248
5960
  return typeof value === "string" && value.trim().length > 0 ? value : "";
5249
5961
  }
5962
+ function readThreadArchiveFallbackName(threadReadResult) {
5963
+ const record = asRecord5(threadReadResult);
5964
+ const thread = asRecord5(record?.thread);
5965
+ return readNonEmptyString(thread?.name) || readNonEmptyString(thread?.title) || readNonEmptyString(thread?.preview) || "Untitled thread";
5966
+ }
5967
+ function isArchivedThreadReadResult(threadReadResult) {
5968
+ const record = asRecord5(threadReadResult);
5969
+ const thread = asRecord5(record?.thread);
5970
+ const sessionPath = readNonEmptyString(thread?.path);
5971
+ return sessionPath.split(/[\\/]+/u).includes("archived_sessions");
5972
+ }
5973
+ async function callRpcWithArchiveRecovery(appServer, method, params) {
5974
+ try {
5975
+ return await appServer.rpc(method, params ?? null);
5976
+ } catch (error) {
5977
+ if (method !== "thread/archive") {
5978
+ throw error;
5979
+ }
5980
+ const paramsRecord = asRecord5(params);
5981
+ const threadId = readNonEmptyString(paramsRecord?.threadId);
5982
+ const errorMessage = getErrorMessage5(error, "");
5983
+ if (!threadId || !errorMessage.includes("no rollout found")) {
5984
+ throw error;
5985
+ }
5986
+ let threadReadResult = null;
5987
+ try {
5988
+ threadReadResult = await appServer.rpc("thread/read", {
5989
+ threadId,
5990
+ includeTurns: false
5991
+ });
5992
+ if (isArchivedThreadReadResult(threadReadResult)) {
5993
+ return null;
5994
+ }
5995
+ } catch {
5996
+ }
5997
+ await appServer.rpc("thread/name/set", {
5998
+ threadId,
5999
+ name: readThreadArchiveFallbackName(threadReadResult)
6000
+ });
6001
+ return appServer.rpc(method, params ?? null);
6002
+ }
6003
+ }
5250
6004
  async function listTerminalQuickCommands(cwd) {
5251
6005
  const normalizedCwd = isAbsolute2(cwd) ? cwd : resolve2(cwd);
5252
6006
  const info = await stat4(normalizedCwd);
@@ -5350,26 +6104,53 @@ function readBoolean2(value) {
5350
6104
  function readNumber2(value) {
5351
6105
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
5352
6106
  }
5353
- function resolveComposioCommand() {
6107
+ function buildComposioInvocation(args) {
6108
+ const overrideCommand = process.env.CODEXUI_COMPOSIO_COMMAND?.trim();
6109
+ if (overrideCommand) {
6110
+ const invocation = getSpawnInvocation(overrideCommand, args);
6111
+ return {
6112
+ command: invocation.command,
6113
+ args: invocation.args,
6114
+ displayCommand: `${overrideCommand} ${args.map(quoteShellTokenIfNeeded).join(" ")}`.trim()
6115
+ };
6116
+ }
6117
+ return buildInstalledComposioInvocation(args);
6118
+ }
6119
+ function buildInstalledComposioInvocation(args) {
5354
6120
  const candidates = [
5355
- process.env.CODEXUI_COMPOSIO_COMMAND?.trim() ?? "",
5356
6121
  join6(homedir5(), ".composio", "composio"),
5357
6122
  "composio"
5358
6123
  ];
5359
6124
  for (const candidate of candidates) {
5360
- if (!candidate) continue;
5361
6125
  if ((candidate.includes("/") || candidate.includes("\\")) && !existsSync4(candidate)) continue;
5362
- const invocation = getSpawnInvocation(candidate, ["--version"]);
5363
- const probe = spawnSync4(invocation.command, invocation.args, {
5364
- stdio: "ignore",
5365
- windowsHide: true
5366
- });
5367
- if (!probe.error && probe.status === 0) {
5368
- return candidate;
5369
- }
6126
+ const invocation = getSpawnInvocation(candidate, args);
6127
+ return {
6128
+ command: invocation.command,
6129
+ args: invocation.args,
6130
+ displayCommand: `${candidate} ${args.map(quoteShellTokenIfNeeded).join(" ")}`.trim()
6131
+ };
5370
6132
  }
5371
6133
  return null;
5372
6134
  }
6135
+ function probeComposioInvocation(invocation) {
6136
+ const probe = spawnSync4(invocation.command, invocation.args, {
6137
+ encoding: "utf8",
6138
+ env: process.env,
6139
+ windowsHide: true
6140
+ });
6141
+ const output = `${probe.stdout ?? ""}${probe.stderr ?? ""}`.trim();
6142
+ return {
6143
+ available: !probe.error && probe.status === 0,
6144
+ cliVersion: probe.status === 0 ? (probe.stdout ?? "").trim() : "",
6145
+ output
6146
+ };
6147
+ }
6148
+ function resolveComposioInvocation(args) {
6149
+ const invocation = buildComposioInvocation(args);
6150
+ const versionInvocation = buildComposioInvocation(["--version"]);
6151
+ if (invocation && versionInvocation && probeComposioInvocation(versionInvocation).available) return invocation;
6152
+ return null;
6153
+ }
5373
6154
  function parseComposioJson(stdout, fallback) {
5374
6155
  const trimmed = stdout.trim();
5375
6156
  if (!trimmed) {
@@ -5378,11 +6159,10 @@ function parseComposioJson(stdout, fallback) {
5378
6159
  return JSON.parse(trimmed);
5379
6160
  }
5380
6161
  async function runComposioJson(args, fallback) {
5381
- const command = resolveComposioCommand();
5382
- if (!command) {
6162
+ const invocation = resolveComposioInvocation(args);
6163
+ if (!invocation) {
5383
6164
  throw new Error("Composio CLI is not installed");
5384
6165
  }
5385
- const invocation = getSpawnInvocation(command, args);
5386
6166
  const child = spawn4(invocation.command, invocation.args, {
5387
6167
  env: process.env,
5388
6168
  stdio: ["ignore", "pipe", "pipe"],
@@ -5488,18 +6268,12 @@ async function readComposioConnectionsBySlug() {
5488
6268
  return bySlug;
5489
6269
  }
5490
6270
  async function readComposioStatus() {
5491
- const cliVersion = (() => {
5492
- const command = resolveComposioCommand();
5493
- if (!command) return "";
5494
- const invocation = getSpawnInvocation(command, ["--version"]);
5495
- const probe = spawnSync4(invocation.command, invocation.args, {
5496
- encoding: "utf8",
5497
- windowsHide: true
5498
- });
5499
- return probe.status === 0 ? probe.stdout.trim() : "";
5500
- })();
6271
+ const versionInvocation = buildComposioInvocation(["--version"]);
6272
+ const probe = versionInvocation ? probeComposioInvocation(versionInvocation) : { available: false, cliVersion: "", output: "" };
6273
+ const available = probe.available;
6274
+ const cliVersion = probe.cliVersion;
5501
6275
  const userData = await readComposioUserData();
5502
- if (!resolveComposioCommand()) {
6276
+ if (!available) {
5503
6277
  return {
5504
6278
  available: false,
5505
6279
  authenticated: false,
@@ -5609,11 +6383,10 @@ async function startComposioLink(slug) {
5609
6383
  };
5610
6384
  }
5611
6385
  async function startComposioLogin() {
5612
- const command = resolveComposioCommand();
5613
- if (!command) {
6386
+ const invocation = resolveComposioInvocation(["login", "--no-browser", "-y"]);
6387
+ if (!invocation) {
5614
6388
  throw new Error("Composio CLI is not installed");
5615
6389
  }
5616
- const invocation = getSpawnInvocation(command, ["login", "--no-browser", "-y"]);
5617
6390
  const proc = spawn4(invocation.command, invocation.args, {
5618
6391
  cwd: process.cwd(),
5619
6392
  env: process.env,
@@ -6470,6 +7243,63 @@ function normalizeBranchRefName(value) {
6470
7243
  if (trimmed.startsWith("refs/remotes/")) return trimmed.slice("refs/remotes/".length);
6471
7244
  return trimmed;
6472
7245
  }
7246
+ function toHeaderGitResetHistoryRef(branchName, commitSha) {
7247
+ return `refs/codex/header-git-reset-history/${branchName}/${commitSha}`;
7248
+ }
7249
+ var HEADER_GIT_RESET_HISTORY_REF_LIMIT = 25;
7250
+ async function assertLocalGitBranch(repoRoot, branchName) {
7251
+ await runCommandCapture2("git", ["show-ref", "--verify", `refs/heads/${branchName}`], { cwd: repoRoot });
7252
+ }
7253
+ async function checkoutGitBranchWithWorktreeRecovery(repoRoot, branchName) {
7254
+ try {
7255
+ await runCommand3("git", ["checkout", branchName], { cwd: repoRoot });
7256
+ } catch (checkoutError) {
7257
+ const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, branchName);
7258
+ if (!blockingWorktreePath) {
7259
+ throw checkoutError;
7260
+ }
7261
+ await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
7262
+ await runCommand3("git", ["checkout", branchName], { cwd: repoRoot });
7263
+ }
7264
+ }
7265
+ async function pruneHeaderGitResetHistoryRefs(repoRoot, branchName) {
7266
+ const resetHistoryRefPrefix = `refs/codex/header-git-reset-history/${branchName}/`;
7267
+ const refsRaw = await runCommandCapture2(
7268
+ "git",
7269
+ ["for-each-ref", "--sort=-creatordate", "--format=%(refname)", resetHistoryRefPrefix],
7270
+ { cwd: repoRoot }
7271
+ ).catch(() => "");
7272
+ const refs = refsRaw.split("\n").map((entry) => entry.trim()).filter(Boolean);
7273
+ const staleRefs = refs.slice(HEADER_GIT_RESET_HISTORY_REF_LIMIT);
7274
+ for (const refName of staleRefs) {
7275
+ await runCommand3("git", ["update-ref", "-d", refName], { cwd: repoRoot });
7276
+ }
7277
+ }
7278
+ async function readGitHeaderState(cwd) {
7279
+ const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
7280
+ const currentBranchRaw = await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot });
7281
+ const currentBranch = currentBranchRaw.trim() || null;
7282
+ const headShaRaw = await runCommandCapture2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: gitRoot });
7283
+ const headCommitRaw = await runCommandCapture2("git", ["show", "-s", "--date=short", "--format=%cd%x09%s", "HEAD"], { cwd: gitRoot });
7284
+ const [headDate = "", ...headSubjectParts] = headCommitRaw.split(" ");
7285
+ const statusRaw = await runCommandCapture2("git", ["status", "--porcelain"], { cwd: gitRoot });
7286
+ return {
7287
+ currentBranch,
7288
+ headSha: headShaRaw.trim() || null,
7289
+ headSubject: headSubjectParts.join(" ").trim() || null,
7290
+ headDate: headDate.trim() || null,
7291
+ detached: !currentBranch,
7292
+ dirty: statusRaw.trim().length > 0,
7293
+ gitRoot
7294
+ };
7295
+ }
7296
+ async function assertNoTrackedGitChanges(repoRoot) {
7297
+ const statusRaw = await runCommandCapture2("git", ["status", "--porcelain"], { cwd: repoRoot });
7298
+ const trackedChanges = statusRaw.split("\n").map((line) => line.trimEnd()).filter((line) => line && !line.startsWith("?? "));
7299
+ if (trackedChanges.length > 0) {
7300
+ throw new Error("Cannot switch branches or reset with tracked uncommitted changes. Commit, stash, or discard tracked changes first. Untracked files are allowed unless Git would overwrite them.");
7301
+ }
7302
+ }
6473
7303
  function extractBranchLockedWorktreePath(error, branchName) {
6474
7304
  const message = getErrorMessage5(error, "");
6475
7305
  if (!message || !branchName) return "";
@@ -6478,6 +7308,35 @@ function extractBranchLockedWorktreePath(error, branchName) {
6478
7308
  const match = pattern.exec(message);
6479
7309
  return match?.[1]?.trim() ?? "";
6480
7310
  }
7311
+ function toPermanentWorktreeBranchNameDraft(worktreeName) {
7312
+ const sanitized = worktreeName.trim().replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/\.+/gu, ".").replace(/-+/gu, "-").replace(/^[.-]+|[.-]+$/gu, "");
7313
+ return sanitized || "worktree";
7314
+ }
7315
+ async function isValidGitBranchName(gitRoot, branchName) {
7316
+ try {
7317
+ await runCommand3("git", ["check-ref-format", "--branch", branchName], { cwd: gitRoot });
7318
+ return true;
7319
+ } catch {
7320
+ return false;
7321
+ }
7322
+ }
7323
+ async function doesLocalGitBranchExist(gitRoot, branchName) {
7324
+ try {
7325
+ await runCommand3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: gitRoot });
7326
+ return true;
7327
+ } catch {
7328
+ return false;
7329
+ }
7330
+ }
7331
+ async function allocatePermanentWorktreeBranchName(gitRoot, worktreeName) {
7332
+ const base = toPermanentWorktreeBranchNameDraft(worktreeName);
7333
+ for (let attempt = 0; attempt < 50; attempt += 1) {
7334
+ const candidate = attempt === 0 ? base : `${base}-${attempt + 1}`;
7335
+ if (!await isValidGitBranchName(gitRoot, candidate)) continue;
7336
+ if (!await doesLocalGitBranchExist(gitRoot, candidate)) return candidate;
7337
+ }
7338
+ throw new Error("Failed to allocate a unique branch name for worktree");
7339
+ }
6481
7340
  function normalizeStringArray(value) {
6482
7341
  if (!Array.isArray(value)) return [];
6483
7342
  const normalized = [];
@@ -6498,9 +7357,133 @@ function normalizeStringRecord(value) {
6498
7357
  }
6499
7358
  return next;
6500
7359
  }
7360
+ function normalizeRemoteProjects(value) {
7361
+ if (!Array.isArray(value)) return [];
7362
+ const next = [];
7363
+ const seen = /* @__PURE__ */ new Set();
7364
+ for (const item of value) {
7365
+ const record = asRecord5(item);
7366
+ if (!record) continue;
7367
+ const id = typeof record.id === "string" ? record.id.trim() : "";
7368
+ if (!id || seen.has(id)) continue;
7369
+ seen.add(id);
7370
+ next.push({
7371
+ id,
7372
+ hostId: typeof record.hostId === "string" ? record.hostId.trim() : "",
7373
+ remotePath: typeof record.remotePath === "string" ? record.remotePath.trim() : "",
7374
+ label: typeof record.label === "string" ? record.label.trim() : ""
7375
+ });
7376
+ }
7377
+ return next;
7378
+ }
6501
7379
  function getCodexAuthPath() {
6502
7380
  return join6(getCodexHomeDir3(), "auth.json");
6503
7381
  }
7382
+ var CODEX_CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
7383
+ var DEFAULT_CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
7384
+ function decodeBase64UrlJson2(value) {
7385
+ try {
7386
+ const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
7387
+ const decoded = Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
7388
+ const parsed = JSON.parse(decoded);
7389
+ return asRecord5(parsed);
7390
+ } catch {
7391
+ return null;
7392
+ }
7393
+ }
7394
+ function decodeJwtPayload(token) {
7395
+ if (!token) return null;
7396
+ const parts = token.split(".");
7397
+ if (parts.length < 2) return null;
7398
+ return decodeBase64UrlJson2(parts[1] ?? "");
7399
+ }
7400
+ function extractChatgptTokenMetadata(accessToken) {
7401
+ const payload = decodeJwtPayload(accessToken);
7402
+ const auth = asRecord5(payload?.["https://api.openai.com/auth"]);
7403
+ return {
7404
+ chatgptAccountId: readNonEmptyString(auth?.chatgpt_account_id) || null,
7405
+ chatgptPlanType: readNonEmptyString(auth?.chatgpt_plan_type) || null
7406
+ };
7407
+ }
7408
+ function readTokenErrorMessage(payload, fallback) {
7409
+ const record = asRecord5(payload);
7410
+ const message = readNonEmptyString(record?.message);
7411
+ if (message) return message;
7412
+ const error = record?.error;
7413
+ if (typeof error === "string" && error.trim().length > 0) return error.trim();
7414
+ const nestedError = asRecord5(error);
7415
+ return readNonEmptyString(nestedError?.message) || readNonEmptyString(nestedError?.error_description) || readNonEmptyString(record?.error_description) || fallback;
7416
+ }
7417
+ function readTokenResponseString(payload, ...keys) {
7418
+ if (!payload) return null;
7419
+ for (const key of keys) {
7420
+ const value = readNonEmptyString(payload[key]);
7421
+ if (value) return value;
7422
+ }
7423
+ return null;
7424
+ }
7425
+ async function refreshChatgptAuthTokensForExternalAuth(params = {}) {
7426
+ const authPath = getCodexAuthPath();
7427
+ const raw = await readFile3(authPath, "utf8");
7428
+ const auth = JSON.parse(raw);
7429
+ const currentRefreshToken = auth.tokens?.refresh_token?.trim() ?? "";
7430
+ if (!currentRefreshToken) {
7431
+ throw new Error("No ChatGPT refresh token is available. Please sign in again.");
7432
+ }
7433
+ const refreshUrl = process.env.CODEX_REFRESH_TOKEN_URL_OVERRIDE?.trim() || DEFAULT_CODEX_REFRESH_TOKEN_URL;
7434
+ const body = new URLSearchParams({
7435
+ grant_type: "refresh_token",
7436
+ refresh_token: currentRefreshToken,
7437
+ client_id: CODEX_CHATGPT_CLIENT_ID
7438
+ });
7439
+ const response = await fetch(refreshUrl, {
7440
+ method: "POST",
7441
+ headers: {
7442
+ "Content-Type": "application/x-www-form-urlencoded"
7443
+ },
7444
+ body: body.toString(),
7445
+ signal: AbortSignal.timeout(25e3)
7446
+ });
7447
+ const text = await response.text();
7448
+ let payload = null;
7449
+ try {
7450
+ payload = asRecord5(JSON.parse(text));
7451
+ } catch {
7452
+ payload = null;
7453
+ }
7454
+ if (!response.ok) {
7455
+ throw new Error(readTokenErrorMessage(payload, `ChatGPT token refresh failed with HTTP ${String(response.status)}`));
7456
+ }
7457
+ const accessToken = readTokenResponseString(payload, "access_token", "accessToken");
7458
+ if (!accessToken) {
7459
+ throw new Error("ChatGPT token refresh response did not include an access token.");
7460
+ }
7461
+ const nextRefreshToken = readTokenResponseString(payload, "refresh_token", "refreshToken") ?? currentRefreshToken;
7462
+ const nextIdToken = readTokenResponseString(payload, "id_token", "idToken") ?? auth.tokens?.id_token;
7463
+ const metadata = extractChatgptTokenMetadata(accessToken);
7464
+ const chatgptAccountId = metadata.chatgptAccountId || readTokenResponseString(payload, "chatgpt_account_id", "chatgptAccountId") || readNonEmptyString(params.previousAccountId) || readNonEmptyString(auth.tokens?.account_id);
7465
+ if (!chatgptAccountId) {
7466
+ throw new Error("ChatGPT token refresh response did not include account metadata.");
7467
+ }
7468
+ const nextAuth = {
7469
+ ...auth,
7470
+ auth_mode: auth.auth_mode || "chatgpt",
7471
+ last_refresh: Date.now(),
7472
+ tokens: {
7473
+ ...auth.tokens,
7474
+ access_token: accessToken,
7475
+ refresh_token: nextRefreshToken,
7476
+ account_id: chatgptAccountId,
7477
+ ...nextIdToken ? { id_token: nextIdToken } : {}
7478
+ }
7479
+ };
7480
+ await writeFile4(authPath, JSON.stringify(nextAuth, null, 2), { encoding: "utf8", mode: 384 });
7481
+ return {
7482
+ accessToken,
7483
+ chatgptAccountId,
7484
+ chatgptPlanType: metadata.chatgptPlanType
7485
+ };
7486
+ }
6504
7487
  async function readCodexAuth() {
6505
7488
  try {
6506
7489
  const raw = await readFile3(getCodexAuthPath(), "utf8");
@@ -6512,6 +7495,37 @@ async function readCodexAuth() {
6512
7495
  return null;
6513
7496
  }
6514
7497
  }
7498
+ function hasUsableCodexAuthSync() {
7499
+ try {
7500
+ const raw = readFileSync2(getCodexAuthPath(), "utf8");
7501
+ const auth = JSON.parse(raw);
7502
+ return Boolean(auth.tokens?.access_token?.trim());
7503
+ } catch {
7504
+ return false;
7505
+ }
7506
+ }
7507
+ function readFreeModeStateSync(statePath) {
7508
+ try {
7509
+ return JSON.parse(readFileSync2(statePath, "utf8"));
7510
+ } catch {
7511
+ return null;
7512
+ }
7513
+ }
7514
+ function ensureDefaultFreeModeStateForMissingAuthSync(statePath) {
7515
+ const current = readFreeModeStateSync(statePath);
7516
+ if (!shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuthSync())) {
7517
+ return current;
7518
+ }
7519
+ const fallback = createDefaultOpenCodeZenFreeModeState();
7520
+ mkdirSync(dirname2(statePath), { recursive: true });
7521
+ writeFileSync2(statePath, JSON.stringify(fallback), { encoding: "utf8", mode: 384 });
7522
+ return fallback;
7523
+ }
7524
+ function isLoopbackRemoteAddress(remoteAddress) {
7525
+ if (!remoteAddress) return false;
7526
+ const normalized = remoteAddress.startsWith("::ffff:") ? remoteAddress.slice("::ffff:".length) : remoteAddress;
7527
+ return normalized === "127.0.0.1" || normalized === "::1";
7528
+ }
6515
7529
  function getCodexGlobalStatePath() {
6516
7530
  return join6(getCodexHomeDir3(), ".codex-global-state.json");
6517
7531
  }
@@ -6615,13 +7629,35 @@ async function listThreadHeartbeatAutomations() {
6615
7629
  if (!entry.isDirectory()) continue;
6616
7630
  const automation = await readAutomationRecordFromFile(join6(automationRoot, entry.name, "automation.toml"));
6617
7631
  if (!automation || automation.kind !== "heartbeat" || !automation.targetThreadId) continue;
6618
- next[automation.targetThreadId] = automation;
7632
+ next[automation.targetThreadId] = [...next[automation.targetThreadId] ?? [], automation];
7633
+ }
7634
+ for (const automations of Object.values(next)) {
7635
+ automations.sort((first, second) => {
7636
+ const firstCreatedAt = first.createdAtMs ?? 0;
7637
+ const secondCreatedAt = second.createdAtMs ?? 0;
7638
+ if (firstCreatedAt !== secondCreatedAt) return firstCreatedAt - secondCreatedAt;
7639
+ return first.id.localeCompare(second.id);
7640
+ });
6619
7641
  }
6620
7642
  return next;
6621
7643
  }
6622
- async function readThreadHeartbeatAutomation(threadId) {
7644
+ async function readThreadHeartbeatAutomations(threadId) {
6623
7645
  const all = await listThreadHeartbeatAutomations();
6624
- return all[threadId] ?? null;
7646
+ return all[threadId] ?? [];
7647
+ }
7648
+ async function readThreadHeartbeatAutomation(threadId, automationId = "") {
7649
+ const automations = await readThreadHeartbeatAutomations(threadId);
7650
+ if (automationId) return automations.find((automation) => automation.id === automationId) ?? null;
7651
+ return automations[0] ?? null;
7652
+ }
7653
+ function resolveUniqueAutomationId(existingIds, threadId, name) {
7654
+ const baseId = slugifyAutomationId(threadId, name);
7655
+ if (!existingIds.has(baseId)) return baseId;
7656
+ for (let index = 2; index < 1e3; index += 1) {
7657
+ const candidate = `${baseId}-${index}`;
7658
+ if (!existingIds.has(candidate)) return candidate;
7659
+ }
7660
+ return `${baseId}-${randomBytes(4).toString("hex")}`;
6625
7661
  }
6626
7662
  async function writeThreadHeartbeatAutomation(input) {
6627
7663
  const threadId = input.threadId.trim();
@@ -6633,8 +7669,10 @@ async function writeThreadHeartbeatAutomation(input) {
6633
7669
  }
6634
7670
  const automationRoot = getCodexAutomationsDir();
6635
7671
  await mkdir4(automationRoot, { recursive: true });
6636
- const existing = await readThreadHeartbeatAutomation(threadId);
6637
- const id = existing?.id ?? slugifyAutomationId(threadId, name);
7672
+ const existing = input.id ? await readThreadHeartbeatAutomation(threadId, input.id.trim()) : null;
7673
+ const entries = await readdir2(automationRoot, { withFileTypes: true }).catch(() => []);
7674
+ const existingIds = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
7675
+ const id = existing?.id ?? resolveUniqueAutomationId(existingIds, threadId, name);
6638
7676
  const automationDir = join6(automationRoot, id);
6639
7677
  const now = Date.now();
6640
7678
  const record = {
@@ -6659,10 +7697,18 @@ async function writeThreadHeartbeatAutomation(input) {
6659
7697
  }
6660
7698
  return record;
6661
7699
  }
6662
- async function deleteThreadHeartbeatAutomation(threadId) {
6663
- const automation = await readThreadHeartbeatAutomation(threadId.trim());
6664
- if (!automation) return false;
6665
- await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
7700
+ async function deleteThreadHeartbeatAutomation(threadId, automationId = "") {
7701
+ const normalizedThreadId = threadId.trim();
7702
+ const normalizedAutomationId = automationId.trim();
7703
+ if (normalizedAutomationId) {
7704
+ const automation = await readThreadHeartbeatAutomation(normalizedThreadId, normalizedAutomationId);
7705
+ if (!automation) return false;
7706
+ await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
7707
+ return true;
7708
+ }
7709
+ const automations = await readThreadHeartbeatAutomations(normalizedThreadId);
7710
+ if (automations.length === 0) return false;
7711
+ await Promise.all(automations.map((automation) => rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })));
6666
7712
  return true;
6667
7713
  }
6668
7714
  var MAX_THREAD_TITLES = 500;
@@ -6840,6 +7886,7 @@ function normalizeThreadQueueState(value) {
6840
7886
  }
6841
7887
  return state;
6842
7888
  }
7889
+ var threadQueueMutationChain = Promise.resolve();
6843
7890
  async function readThreadQueueState() {
6844
7891
  const statePath = getCodexGlobalStatePath();
6845
7892
  try {
@@ -6850,7 +7897,7 @@ async function readThreadQueueState() {
6850
7897
  return {};
6851
7898
  }
6852
7899
  }
6853
- async function writeThreadQueueState(nextState) {
7900
+ async function writeThreadQueueStateUnlocked(nextState) {
6854
7901
  const statePath = getCodexGlobalStatePath();
6855
7902
  let payload = {};
6856
7903
  try {
@@ -6867,6 +7914,107 @@ async function writeThreadQueueState(nextState) {
6867
7914
  }
6868
7915
  await writeFile4(statePath, JSON.stringify(payload), "utf8");
6869
7916
  }
7917
+ async function withThreadQueueStateUpdate(update) {
7918
+ const run = threadQueueMutationChain.then(async () => {
7919
+ const currentState = await readThreadQueueState();
7920
+ const { nextState, result } = await update(currentState);
7921
+ await writeThreadQueueStateUnlocked(nextState);
7922
+ return result;
7923
+ });
7924
+ threadQueueMutationChain = run.catch(() => {
7925
+ });
7926
+ return run;
7927
+ }
7928
+ async function writeThreadQueueState(nextState) {
7929
+ await withThreadQueueStateUpdate(() => ({
7930
+ nextState: normalizeThreadQueueState(nextState),
7931
+ result: void 0
7932
+ }));
7933
+ }
7934
+ async function appendThreadQueuedMessage(threadId, message) {
7935
+ const normalizedThreadId = threadId.trim();
7936
+ if (!normalizedThreadId) throw new Error("threadId is required");
7937
+ await withThreadQueueStateUpdate((state) => ({
7938
+ nextState: {
7939
+ ...state,
7940
+ [normalizedThreadId]: [...state[normalizedThreadId] ?? [], message]
7941
+ },
7942
+ result: void 0
7943
+ }));
7944
+ }
7945
+ function normalizeReasoningEffort(value) {
7946
+ const allowed = ["none", "minimal", "low", "medium", "high", "xhigh"];
7947
+ return typeof value === "string" && allowed.includes(value) ? value : "";
7948
+ }
7949
+ function normalizeCollaborationModeReasoningEffort(value) {
7950
+ return value && value.length > 0 ? value : null;
7951
+ }
7952
+ function extractLocalImagePathFromUrl(value) {
7953
+ if (!value) return null;
7954
+ try {
7955
+ const parsed = new URL(value, "http://localhost");
7956
+ if (parsed.pathname !== "/codex-local-image") return null;
7957
+ const path = parsed.searchParams.get("path")?.trim() ?? "";
7958
+ return path.length > 0 ? path : null;
7959
+ } catch {
7960
+ return null;
7961
+ }
7962
+ }
7963
+ function buildTextWithAttachments(prompt, files) {
7964
+ if (files.length === 0) return prompt;
7965
+ let prefix = "# Files mentioned by the user:\n";
7966
+ for (const f of files) {
7967
+ prefix += `
7968
+ ## ${f.label}: ${f.path}
7969
+ `;
7970
+ }
7971
+ return `${prefix}
7972
+ ## My request for Codex:
7973
+
7974
+ ${prompt}
7975
+ `;
7976
+ }
7977
+ function escapeHeartbeatXmlText(value) {
7978
+ return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;");
7979
+ }
7980
+ function buildHeartbeatQueuedMessage(automation) {
7981
+ return {
7982
+ id: `automation-${automation.id}-${Date.now()}-${randomBytes(3).toString("hex")}`,
7983
+ text: `<heartbeat>
7984
+ <automation_id>${escapeHeartbeatXmlText(automation.id)}</automation_id>
7985
+ <current_time_iso>${(/* @__PURE__ */ new Date()).toISOString()}</current_time_iso>
7986
+ <instructions>
7987
+ ${escapeHeartbeatXmlText(automation.prompt)}
7988
+ </instructions>
7989
+ </heartbeat>`,
7990
+ imageUrls: [],
7991
+ skills: [],
7992
+ fileAttachments: [],
7993
+ collaborationMode: "default"
7994
+ };
7995
+ }
7996
+ function fileNameFromPath(pathValue) {
7997
+ const normalized = pathValue.replace(/\\/g, "/");
7998
+ const segments = normalized.split("/").filter(Boolean);
7999
+ return segments.at(-1) ?? normalized;
8000
+ }
8001
+ function extractThreadIdFromNotificationParams(params) {
8002
+ const record = asRecord5(params);
8003
+ if (!record) return "";
8004
+ const threadId = (typeof record.threadId === "string" ? record.threadId : "") || (typeof record.thread_id === "string" ? record.thread_id : "") || (typeof record.conversationId === "string" ? record.conversationId : "") || (typeof record.conversation_id === "string" ? record.conversation_id : "");
8005
+ if (threadId) return threadId;
8006
+ const thread = asRecord5(record.thread);
8007
+ if (thread && typeof thread.id === "string") return thread.id;
8008
+ const turn = asRecord5(record.turn);
8009
+ if (turn) {
8010
+ const turnThreadId = (typeof turn.threadId === "string" ? turn.threadId : "") || (typeof turn.thread_id === "string" ? turn.thread_id : "");
8011
+ if (turnThreadId) return turnThreadId;
8012
+ }
8013
+ return "";
8014
+ }
8015
+ function isTurnCompletedNotification(notification) {
8016
+ return notification.method === "turn/completed";
8017
+ }
6870
8018
  async function readFirstLaunchPluginsCardDismissed() {
6871
8019
  const statePath = getCodexGlobalStatePath();
6872
8020
  try {
@@ -6965,22 +8113,74 @@ async function readWorkspaceRootsState() {
6965
8113
  return {
6966
8114
  order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
6967
8115
  labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
6968
- active: normalizeStringArray(payload["active-workspace-roots"])
8116
+ active: normalizeStringArray(payload["active-workspace-roots"]),
8117
+ projectOrder: normalizeStringArray(payload["project-order"]),
8118
+ remoteProjects: normalizeRemoteProjects(payload["remote-projects"])
6969
8119
  };
6970
8120
  }
6971
8121
  async function writeWorkspaceRootsState(nextState) {
6972
8122
  const statePath = getCodexGlobalStatePath();
6973
8123
  let payload = {};
6974
8124
  try {
6975
- const raw = await readFile3(statePath, "utf8");
6976
- payload = asRecord5(JSON.parse(raw)) ?? {};
8125
+ const raw = await readFile3(statePath, "utf8");
8126
+ payload = asRecord5(JSON.parse(raw)) ?? {};
8127
+ } catch {
8128
+ payload = {};
8129
+ }
8130
+ payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
8131
+ payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
8132
+ payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
8133
+ payload["project-order"] = normalizeStringArray(nextState.projectOrder);
8134
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
8135
+ }
8136
+ var workspaceRootsMutation = Promise.resolve();
8137
+ function queueWorkspaceRootsMutation(mutation) {
8138
+ const run = workspaceRootsMutation.catch(() => void 0).then(mutation);
8139
+ workspaceRootsMutation = run.then(
8140
+ () => void 0,
8141
+ () => void 0
8142
+ );
8143
+ return run;
8144
+ }
8145
+ function prependUniqueString(value, items) {
8146
+ return [value, ...items.filter((item) => item !== value)];
8147
+ }
8148
+ async function updateWorkspaceRootsState(updater) {
8149
+ await queueWorkspaceRootsMutation(async () => {
8150
+ const existingState = await readWorkspaceRootsState();
8151
+ await writeWorkspaceRootsState(updater(existingState));
8152
+ });
8153
+ }
8154
+ async function persistWorkspaceRoot(workspaceRoot, label = "") {
8155
+ const normalizedRoot = workspaceRoot.trim();
8156
+ if (!normalizedRoot) return;
8157
+ await updateWorkspaceRootsState((existingState) => {
8158
+ const nextLabels = { ...existingState.labels };
8159
+ const trimmedLabel = label.trim();
8160
+ if (trimmedLabel.length > 0) {
8161
+ nextLabels[normalizedRoot] = trimmedLabel;
8162
+ }
8163
+ return {
8164
+ order: prependUniqueString(normalizedRoot, existingState.order),
8165
+ labels: nextLabels,
8166
+ active: prependUniqueString(normalizedRoot, existingState.active),
8167
+ projectOrder: prependUniqueString(normalizedRoot, existingState.projectOrder),
8168
+ remoteProjects: existingState.remoteProjects
8169
+ };
8170
+ });
8171
+ }
8172
+ async function rollbackCreatedWorktree(gitRoot, worktreeCwd, cleanupDirectory, branchName) {
8173
+ try {
8174
+ await runCommand3("git", ["worktree", "remove", "--force", worktreeCwd], { cwd: gitRoot });
6977
8175
  } catch {
6978
- payload = {};
8176
+ await rm4(worktreeCwd, { recursive: true, force: true }).catch(() => void 0);
8177
+ }
8178
+ if (cleanupDirectory && cleanupDirectory !== worktreeCwd) {
8179
+ await rm4(cleanupDirectory, { recursive: true, force: true }).catch(() => void 0);
8180
+ }
8181
+ if (branchName) {
8182
+ await runCommand3("git", ["branch", "-D", branchName], { cwd: gitRoot }).catch(() => void 0);
6979
8183
  }
6980
- payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
6981
- payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
6982
- payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
6983
- await writeFile4(statePath, JSON.stringify(payload), "utf8");
6984
8184
  }
6985
8185
  function normalizeTelegramBridgeConfig(value) {
6986
8186
  const record = asRecord5(value);
@@ -7037,7 +8237,7 @@ function rememberTelegramChatId(chatId) {
7037
8237
  });
7038
8238
  return telegramBridgeConfigMutation;
7039
8239
  }
7040
- async function readJsonBody(req) {
8240
+ async function readJsonBody2(req) {
7041
8241
  const raw = await readRawBody(req);
7042
8242
  if (raw.length === 0) return null;
7043
8243
  const text = raw.toString("utf8").trim();
@@ -7255,6 +8455,7 @@ var AppServerProcess = class {
7255
8455
  this.lastThreadReadSnapshotByThreadId = /* @__PURE__ */ new Map();
7256
8456
  this.capturedItemsByThreadId = /* @__PURE__ */ new Map();
7257
8457
  this.liveStateCache = /* @__PURE__ */ new Map();
8458
+ this.chatgptAuthRefreshPromise = null;
7258
8459
  }
7259
8460
  getCodexCommand() {
7260
8461
  const codexCommand = resolveCodexCommand();
@@ -7275,10 +8476,11 @@ var AppServerProcess = class {
7275
8476
  const serverPort = parseInt(process.env.CODEXUI_SERVER_PORT ?? "", 10) || void 0;
7276
8477
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7277
8478
  try {
7278
- const raw = readFileSync2(statePath, "utf8");
7279
- const state = JSON.parse(raw);
7280
- args.push(...getFreeModeConfigArgs(state, serverPort));
7281
- extraEnv = getFreeModeEnvVars(state);
8479
+ const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
8480
+ if (state) {
8481
+ args.push(...getFreeModeConfigArgs(state, serverPort));
8482
+ extraEnv = getFreeModeEnvVars(state);
8483
+ }
7282
8484
  } catch {
7283
8485
  }
7284
8486
  return { args, env: extraEnv };
@@ -7517,7 +8719,46 @@ var AppServerProcess = class {
7517
8719
  }
7518
8720
  });
7519
8721
  }
8722
+ async refreshChatgptAuthTokens(params) {
8723
+ if (!this.chatgptAuthRefreshPromise) {
8724
+ this.chatgptAuthRefreshPromise = refreshChatgptAuthTokensForExternalAuth(params).finally(() => {
8725
+ this.chatgptAuthRefreshPromise = null;
8726
+ });
8727
+ }
8728
+ return await this.chatgptAuthRefreshPromise;
8729
+ }
8730
+ async handleChatgptAuthTokensRefreshRequest(requestId, params) {
8731
+ const requestParams = asRecord5(params);
8732
+ const previousAccountId = readNonEmptyString(requestParams?.previousAccountId ?? requestParams?.previous_account_id);
8733
+ try {
8734
+ const result = await this.refreshChatgptAuthTokens({
8735
+ reason: readNonEmptyString(requestParams?.reason) || void 0,
8736
+ previousAccountId: previousAccountId || void 0
8737
+ });
8738
+ this.sendServerRequestReply(requestId, { result });
8739
+ this.emitNotification({
8740
+ method: "server/request/resolved",
8741
+ params: {
8742
+ id: requestId,
8743
+ method: "account/chatgptAuthTokens/refresh",
8744
+ mode: "automatic",
8745
+ resolvedAtIso: (/* @__PURE__ */ new Date()).toISOString()
8746
+ }
8747
+ });
8748
+ } catch (error) {
8749
+ this.sendServerRequestReply(requestId, {
8750
+ error: {
8751
+ code: -32001,
8752
+ message: getErrorMessage5(error, "Failed to refresh ChatGPT auth tokens")
8753
+ }
8754
+ });
8755
+ }
8756
+ }
7520
8757
  handleServerRequest(requestId, method, params) {
8758
+ if (method === "account/chatgptAuthTokens/refresh") {
8759
+ void this.handleChatgptAuthTokensRefreshRequest(requestId, params);
8760
+ return;
8761
+ }
7521
8762
  const pendingRequest = {
7522
8763
  id: requestId,
7523
8764
  method,
@@ -7636,6 +8877,218 @@ var AppServerProcess = class {
7636
8877
  forceKillTimer.unref();
7637
8878
  }
7638
8879
  };
8880
+ var BackendQueueProcessor = class {
8881
+ constructor(appServer) {
8882
+ this.appServer = appServer;
8883
+ this.processingThreadIds = /* @__PURE__ */ new Set();
8884
+ this.queueDrainTimersByThreadId = /* @__PURE__ */ new Map();
8885
+ this.queueDrainDueAtByThreadId = /* @__PURE__ */ new Map();
8886
+ this.unsubscribe = appServer.onNotification((notification) => {
8887
+ if (!isTurnCompletedNotification(notification)) return;
8888
+ const threadId = extractThreadIdFromNotificationParams(notification.params);
8889
+ if (!threadId) return;
8890
+ void this.processThreadQueue(threadId);
8891
+ });
8892
+ void this.scheduleAllQueuedThreads(1e3);
8893
+ }
8894
+ dispose() {
8895
+ this.unsubscribe();
8896
+ for (const timer of this.queueDrainTimersByThreadId.values()) {
8897
+ clearTimeout(timer);
8898
+ }
8899
+ this.queueDrainTimersByThreadId.clear();
8900
+ this.queueDrainDueAtByThreadId.clear();
8901
+ this.processingThreadIds.clear();
8902
+ }
8903
+ async scheduleAllQueuedThreads(delayMs = 0) {
8904
+ try {
8905
+ const state = await readThreadQueueState();
8906
+ for (const threadId of Object.keys(state)) {
8907
+ this.scheduleThreadQueueDrain(threadId, delayMs);
8908
+ }
8909
+ } catch {
8910
+ }
8911
+ }
8912
+ scheduleThreadQueueDrain(threadId, delayMs = 5e3) {
8913
+ if (!threadId) return;
8914
+ const normalizedDelayMs = Math.max(0, delayMs);
8915
+ const nextDueAt = Date.now() + normalizedDelayMs;
8916
+ const existingDueAt = this.queueDrainDueAtByThreadId.get(threadId);
8917
+ const existingTimer = this.queueDrainTimersByThreadId.get(threadId);
8918
+ if (existingTimer) {
8919
+ if (existingDueAt !== void 0 && existingDueAt <= nextDueAt) return;
8920
+ clearTimeout(existingTimer);
8921
+ this.queueDrainTimersByThreadId.delete(threadId);
8922
+ this.queueDrainDueAtByThreadId.delete(threadId);
8923
+ }
8924
+ const timer = setTimeout(() => {
8925
+ this.queueDrainTimersByThreadId.delete(threadId);
8926
+ this.queueDrainDueAtByThreadId.delete(threadId);
8927
+ void this.processThreadQueue(threadId);
8928
+ }, normalizedDelayMs);
8929
+ timer.unref?.();
8930
+ this.queueDrainTimersByThreadId.set(threadId, timer);
8931
+ this.queueDrainDueAtByThreadId.set(threadId, nextDueAt);
8932
+ }
8933
+ async processThreadQueue(threadId) {
8934
+ if (this.processingThreadIds.has(threadId)) return;
8935
+ this.processingThreadIds.add(threadId);
8936
+ try {
8937
+ const canStart = await this.canStartQueuedTurn(threadId);
8938
+ if (!canStart) {
8939
+ if (await this.hasQueuedTurns(threadId)) {
8940
+ this.scheduleThreadQueueDrain(threadId);
8941
+ }
8942
+ return;
8943
+ }
8944
+ const next = await this.popNextQueuedTurn(threadId);
8945
+ if (!next) return;
8946
+ try {
8947
+ await this.startQueuedTurn(next);
8948
+ if (await this.hasQueuedTurns(threadId)) {
8949
+ this.scheduleThreadQueueDrain(threadId);
8950
+ }
8951
+ } catch {
8952
+ await this.restoreQueuedTurn(next);
8953
+ this.scheduleThreadQueueDrain(threadId);
8954
+ }
8955
+ } catch {
8956
+ this.scheduleThreadQueueDrain(threadId);
8957
+ } finally {
8958
+ this.processingThreadIds.delete(threadId);
8959
+ }
8960
+ }
8961
+ async hasQueuedTurns(threadId) {
8962
+ const state = await readThreadQueueState();
8963
+ const queue = state[threadId];
8964
+ return Array.isArray(queue) && queue.length > 0;
8965
+ }
8966
+ async canStartQueuedTurn(threadId) {
8967
+ const response = asRecord5(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
8968
+ const thread = asRecord5(response?.thread);
8969
+ if (!thread) return false;
8970
+ const status = asRecord5(thread.status);
8971
+ const statusType = readNonEmptyString(status?.type);
8972
+ if (statusType === "inProgress" || statusType === "running" || statusType === "active") return false;
8973
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
8974
+ return !turns.some((turn) => readNonEmptyString(asRecord5(turn)?.status) === "inProgress");
8975
+ }
8976
+ async popNextQueuedTurn(threadId) {
8977
+ return withThreadQueueStateUpdate((state) => {
8978
+ const queue = state[threadId];
8979
+ if (!queue || queue.length === 0) {
8980
+ return { nextState: state, result: null };
8981
+ }
8982
+ const [message, ...rest] = queue;
8983
+ const nextState = { ...state };
8984
+ if (rest.length > 0) {
8985
+ nextState[threadId] = rest;
8986
+ } else {
8987
+ delete nextState[threadId];
8988
+ }
8989
+ return { nextState, result: { threadId, message } };
8990
+ });
8991
+ }
8992
+ async restoreQueuedTurn(turn) {
8993
+ await withThreadQueueStateUpdate((state) => {
8994
+ const queue = state[turn.threadId] ?? [];
8995
+ return {
8996
+ nextState: {
8997
+ ...state,
8998
+ [turn.threadId]: [turn.message, ...queue]
8999
+ },
9000
+ result: void 0
9001
+ };
9002
+ });
9003
+ }
9004
+ async resolveCollaborationModeSettings(mode) {
9005
+ let currentConfig = null;
9006
+ try {
9007
+ const configPayload = asRecord5(await this.appServer.rpc("config/read", {}));
9008
+ currentConfig = asRecord5(configPayload?.config);
9009
+ } catch {
9010
+ currentConfig = null;
9011
+ }
9012
+ const configuredModel = readNonEmptyString(currentConfig?.model);
9013
+ if (configuredModel) {
9014
+ return {
9015
+ model: configuredModel,
9016
+ reasoningEffort: normalizeCollaborationModeReasoningEffort(normalizeReasoningEffort(currentConfig?.model_reasoning_effort))
9017
+ };
9018
+ }
9019
+ try {
9020
+ const modelsPayload = asRecord5(await this.appServer.rpc("model/list", {}));
9021
+ const models = Array.isArray(modelsPayload?.data) ? modelsPayload.data : [];
9022
+ for (const row of models) {
9023
+ const record = asRecord5(row);
9024
+ const candidate = readNonEmptyString(record?.id) || readNonEmptyString(record?.model);
9025
+ if (candidate) {
9026
+ return {
9027
+ model: candidate,
9028
+ reasoningEffort: normalizeCollaborationModeReasoningEffort(normalizeReasoningEffort(currentConfig?.model_reasoning_effort))
9029
+ };
9030
+ }
9031
+ }
9032
+ } catch {
9033
+ }
9034
+ throw new Error(`${mode === "plan" ? "Plan" : "Default"} mode requires an available model.`);
9035
+ }
9036
+ async buildQueuedTurnParams(turn) {
9037
+ const localImageAttachments = [];
9038
+ for (const imageUrl of turn.message.imageUrls) {
9039
+ const localImagePath = extractLocalImagePathFromUrl(imageUrl.trim());
9040
+ if (!localImagePath) continue;
9041
+ localImageAttachments.push({
9042
+ label: fileNameFromPath(localImagePath),
9043
+ path: localImagePath,
9044
+ fsPath: localImagePath
9045
+ });
9046
+ }
9047
+ const allFileAttachments = [...turn.message.fileAttachments, ...localImageAttachments];
9048
+ const dedupedFileAttachments = allFileAttachments.filter((entry, index) => allFileAttachments.findIndex((candidate) => candidate.fsPath === entry.fsPath) === index);
9049
+ const input = [{
9050
+ type: "text",
9051
+ text: buildTextWithAttachments(turn.message.text, dedupedFileAttachments)
9052
+ }];
9053
+ for (const imageUrl of turn.message.imageUrls) {
9054
+ const normalizedUrl = imageUrl.trim();
9055
+ if (!normalizedUrl) continue;
9056
+ const localImagePath = extractLocalImagePathFromUrl(normalizedUrl);
9057
+ if (localImagePath) {
9058
+ input.push({ type: "localImage", path: localImagePath });
9059
+ } else {
9060
+ input.push({ type: "image", url: normalizedUrl, image_url: normalizedUrl });
9061
+ }
9062
+ }
9063
+ for (const skill of turn.message.skills) {
9064
+ input.push({ type: "skill", name: skill.name, path: skill.path });
9065
+ }
9066
+ const params = {
9067
+ threadId: turn.threadId,
9068
+ input
9069
+ };
9070
+ if (dedupedFileAttachments.length > 0) {
9071
+ params.attachments = dedupedFileAttachments.map((f) => ({ label: f.label, path: f.path, fsPath: f.fsPath }));
9072
+ }
9073
+ try {
9074
+ const settings = await this.resolveCollaborationModeSettings(turn.message.collaborationMode);
9075
+ params.collaborationMode = {
9076
+ mode: turn.message.collaborationMode,
9077
+ settings: {
9078
+ model: settings.model,
9079
+ reasoning_effort: settings.reasoningEffort,
9080
+ developer_instructions: null
9081
+ }
9082
+ };
9083
+ } catch {
9084
+ }
9085
+ return params;
9086
+ }
9087
+ async startQueuedTurn(turn) {
9088
+ await this.appServer.rpc("thread/resume", { threadId: turn.threadId });
9089
+ await this.appServer.rpc("turn/start", await this.buildQueuedTurnParams(turn));
9090
+ }
9091
+ };
7639
9092
  var MethodCatalog = class {
7640
9093
  constructor() {
7641
9094
  this.methodCache = null;
@@ -7738,15 +9191,18 @@ function getSharedBridgeState() {
7738
9191
  return existing;
7739
9192
  }
7740
9193
  existing.appServer.dispose();
9194
+ existing.backendQueueProcessor?.dispose();
7741
9195
  existing.terminalManager?.dispose();
7742
9196
  }
7743
9197
  const appServer = new AppServerProcess();
7744
9198
  const terminalManager = new ThreadTerminalManager();
9199
+ const backendQueueProcessor = new BackendQueueProcessor(appServer);
7745
9200
  const created = {
7746
9201
  version: SHARED_BRIDGE_VERSION,
7747
9202
  appServer,
7748
9203
  terminalManager,
7749
9204
  methodCatalog: new MethodCatalog(),
9205
+ backendQueueProcessor,
7750
9206
  telegramBridge: new TelegramThreadBridge(appServer, {
7751
9207
  onChatSeen: (chatId) => {
7752
9208
  void rememberTelegramChatId(chatId).catch(() => {
@@ -7826,7 +9282,7 @@ async function buildThreadSearchIndex(appServer) {
7826
9282
  return { docsById };
7827
9283
  }
7828
9284
  function createCodexBridgeMiddleware() {
7829
- const { appServer, terminalManager, methodCatalog, telegramBridge } = getSharedBridgeState();
9285
+ const { appServer, terminalManager, methodCatalog, telegramBridge, backendQueueProcessor } = getSharedBridgeState();
7830
9286
  let threadSearchIndex = null;
7831
9287
  let threadSearchIndexPromise = null;
7832
9288
  async function getThreadSearchIndex() {
@@ -7893,6 +9349,10 @@ function createCodexBridgeMiddleware() {
7893
9349
  }
7894
9350
  const url = new URL(req.url, "http://localhost");
7895
9351
  if (url.pathname === "/codex-api/zen-proxy/v1/responses" && req.method === "POST") {
9352
+ if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
9353
+ setJson4(res, 403, { error: "Zen proxy is only available from localhost" });
9354
+ return;
9355
+ }
7896
9356
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7897
9357
  let bearerToken = "";
7898
9358
  let wireApi = "chat";
@@ -7910,9 +9370,9 @@ function createCodexBridgeMiddleware() {
7910
9370
  let bearerToken = "";
7911
9371
  let wireApi = "responses";
7912
9372
  try {
7913
- const state = JSON.parse(readFileSync2(statePath, "utf8"));
7914
- bearerToken = state.apiKey ?? "";
7915
- wireApi = state.wireApi === "chat" ? "chat" : "responses";
9373
+ const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
9374
+ bearerToken = state?.apiKey ?? "";
9375
+ wireApi = state?.wireApi === "chat" ? "chat" : "responses";
7916
9376
  } catch {
7917
9377
  }
7918
9378
  handleOpenRouterProxyRequest(req, res, bearerToken, wireApi);
@@ -7935,17 +9395,13 @@ function createCodexBridgeMiddleware() {
7935
9395
  }
7936
9396
  if (url.pathname.startsWith("/codex-api/free-mode")) {
7937
9397
  let readFreeModeState2 = function() {
7938
- try {
7939
- return JSON.parse(readFileSync2(statePath, "utf8"));
7940
- } catch {
7941
- return { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
7942
- }
9398
+ return ensureDefaultFreeModeStateForMissingAuthSync(statePath) ?? { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
7943
9399
  };
7944
9400
  var readFreeModeState = readFreeModeState2;
7945
9401
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7946
9402
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode") {
7947
9403
  try {
7948
- const body = await readJsonBody(req);
9404
+ const body = await readJsonBody2(req);
7949
9405
  const enable = Boolean(body?.enable);
7950
9406
  if (enable) {
7951
9407
  const apiKey = getRandomFreeKey();
@@ -8001,12 +9457,12 @@ function createCodexBridgeMiddleware() {
8001
9457
  if (req.method === "GET" && url.pathname === "/codex-api/free-mode/status") {
8002
9458
  try {
8003
9459
  const state = readFreeModeState2();
8004
- const freeModels = await getFreeModels();
8005
9460
  const maskedKey = state.apiKey && state.customKey ? state.apiKey.substring(0, 12) + "..." + state.apiKey.substring(state.apiKey.length - 4) : null;
9461
+ refreshFreeModelsInBackground();
8006
9462
  setJson4(res, 200, {
8007
9463
  enabled: state.enabled,
8008
9464
  keyCount: getFreeKeyCount(),
8009
- models: freeModels,
9465
+ models: getCachedFreeModels(),
8010
9466
  currentModel: state.enabled ? state.model : null,
8011
9467
  customKey: Boolean(state.customKey),
8012
9468
  maskedKey,
@@ -8038,7 +9494,7 @@ function createCodexBridgeMiddleware() {
8038
9494
  }
8039
9495
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-key") {
8040
9496
  try {
8041
- const body = await readJsonBody(req);
9497
+ const body = await readJsonBody2(req);
8042
9498
  const key = typeof body?.key === "string" ? body.key.trim() : "";
8043
9499
  const current = readFreeModeState2();
8044
9500
  if (key.length > 0) {
@@ -8073,7 +9529,7 @@ function createCodexBridgeMiddleware() {
8073
9529
  }
8074
9530
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-provider") {
8075
9531
  try {
8076
- const body = await readJsonBody(req);
9532
+ const body = await readJsonBody2(req);
8077
9533
  const baseUrl = typeof body?.baseUrl === "string" ? body.baseUrl.trim() : "";
8078
9534
  const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
8079
9535
  const wireApi = body?.wireApi === "chat" ? "chat" : "responses";
@@ -8116,10 +9572,10 @@ function createCodexBridgeMiddleware() {
8116
9572
  if (await handleAccountRoutes(req, res, url, { appServer })) {
8117
9573
  return;
8118
9574
  }
8119
- if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
9575
+ if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody: readJsonBody2 })) {
8120
9576
  return;
8121
9577
  }
8122
- if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
9578
+ if (await handleReviewRoutes(req, res, url, { readJsonBody: readJsonBody2 })) {
8123
9579
  return;
8124
9580
  }
8125
9581
  if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal/status") {
@@ -8145,7 +9601,7 @@ function createCodexBridgeMiddleware() {
8145
9601
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8146
9602
  return;
8147
9603
  }
8148
- const body = asRecord5(await readJsonBody(req));
9604
+ const body = asRecord5(await readJsonBody2(req));
8149
9605
  const threadId = readNonEmptyString(body?.threadId);
8150
9606
  const cwd = readNonEmptyString(body?.cwd);
8151
9607
  if (!threadId || !cwd) {
@@ -8169,7 +9625,7 @@ function createCodexBridgeMiddleware() {
8169
9625
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8170
9626
  return;
8171
9627
  }
8172
- const body = asRecord5(await readJsonBody(req));
9628
+ const body = asRecord5(await readJsonBody2(req));
8173
9629
  const sessionId = readNonEmptyString(body?.sessionId);
8174
9630
  const data = typeof body?.data === "string" ? body.data : "";
8175
9631
  if (!sessionId) {
@@ -8186,7 +9642,7 @@ function createCodexBridgeMiddleware() {
8186
9642
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8187
9643
  return;
8188
9644
  }
8189
- const body = asRecord5(await readJsonBody(req));
9645
+ const body = asRecord5(await readJsonBody2(req));
8190
9646
  const sessionId = readNonEmptyString(body?.sessionId);
8191
9647
  if (!sessionId) {
8192
9648
  setJson4(res, 400, { error: "Missing sessionId" });
@@ -8202,7 +9658,7 @@ function createCodexBridgeMiddleware() {
8202
9658
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8203
9659
  return;
8204
9660
  }
8205
- const body = asRecord5(await readJsonBody(req));
9661
+ const body = asRecord5(await readJsonBody2(req));
8206
9662
  const sessionId = readNonEmptyString(body?.sessionId);
8207
9663
  if (!sessionId) {
8208
9664
  setJson4(res, 400, { error: "Missing sessionId" });
@@ -8226,7 +9682,7 @@ function createCodexBridgeMiddleware() {
8226
9682
  return;
8227
9683
  }
8228
9684
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
8229
- const payload = await readJsonBody(req);
9685
+ const payload = await readJsonBody2(req);
8230
9686
  const body = asRecord5(payload);
8231
9687
  if (payload !== null && payload !== void 0) {
8232
9688
  requestBodyBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
@@ -8236,9 +9692,10 @@ function createCodexBridgeMiddleware() {
8236
9692
  setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
8237
9693
  return;
8238
9694
  }
8239
- const rpcResult = await appServer.rpc(body.method, body.params ?? null);
9695
+ const rpcResult = await callRpcWithArchiveRecovery(appServer, body.method, body.params ?? null);
8240
9696
  const trimmedResult = trimThreadTurnsInRpcResult(body.method, rpcResult);
8241
- const result = await sanitizeThreadTurnsInlinePayloads(body.method, trimmedResult);
9697
+ const sanitizedResult = await sanitizeThreadTurnsInlinePayloads(body.method, trimmedResult);
9698
+ const result = THREAD_METHODS_WITH_TURNS.has(body.method) ? await mergeSessionSkillInputsIntoThreadResult(sanitizedResult) : sanitizedResult;
8242
9699
  if (THREAD_METHODS_WITH_TURNS.has(body.method)) {
8243
9700
  const rpcRecord = asRecord5(result);
8244
9701
  const rpcThread = asRecord5(rpcRecord?.thread);
@@ -8374,7 +9831,7 @@ function createCodexBridgeMiddleware() {
8374
9831
  }
8375
9832
  if (req.method === "POST" && url.pathname === "/codex-api/thread/rollback-files") {
8376
9833
  try {
8377
- const body = asRecord5(await readJsonBody(req));
9834
+ const body = asRecord5(await readJsonBody2(req));
8378
9835
  const threadId = readNonEmptyString(body?.threadId);
8379
9836
  const turnId = readNonEmptyString(body?.turnId);
8380
9837
  const cwd = readNonEmptyString(body?.cwd);
@@ -8470,7 +9927,7 @@ function createCodexBridgeMiddleware() {
8470
9927
  }
8471
9928
  if (req.method === "POST" && url.pathname === "/codex-api/composio/link") {
8472
9929
  try {
8473
- const payload = asRecord5(await readJsonBody(req));
9930
+ const payload = asRecord5(await readJsonBody2(req));
8474
9931
  const slug = readNonEmptyString(payload?.slug);
8475
9932
  setJson4(res, 200, await startComposioLink(slug));
8476
9933
  } catch (error) {
@@ -8512,7 +9969,7 @@ function createCodexBridgeMiddleware() {
8512
9969
  return;
8513
9970
  }
8514
9971
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
8515
- const payload = await readJsonBody(req);
9972
+ const payload = await readJsonBody2(req);
8516
9973
  await appServer.respondToServerRequest(payload);
8517
9974
  setJson4(res, 200, { ok: true });
8518
9975
  return;
@@ -8533,8 +9990,8 @@ function createCodexBridgeMiddleware() {
8533
9990
  }
8534
9991
  if (req.method === "GET" && url.pathname === "/codex-api/provider-models") {
8535
9992
  try {
8536
- const fmState = JSON.parse(readFileSync2(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE), "utf8"));
8537
- if (fmState.enabled) {
9993
+ const fmState = ensureDefaultFreeModeStateForMissingAuthSync(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE));
9994
+ if (fmState?.enabled) {
8538
9995
  if (fmState.provider === "opencode-zen") {
8539
9996
  try {
8540
9997
  const modelsUrl = "https://opencode.ai/zen/v1/models";
@@ -8602,7 +10059,7 @@ function createCodexBridgeMiddleware() {
8602
10059
  return;
8603
10060
  }
8604
10061
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
8605
- const payload = asRecord5(await readJsonBody(req));
10062
+ const payload = asRecord5(await readJsonBody2(req));
8606
10063
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
8607
10064
  const baseBranch = typeof payload?.baseBranch === "string" ? payload.baseBranch.trim() : "";
8608
10065
  if (!rawSourceCwd) {
@@ -8660,6 +10117,12 @@ function createCodexBridgeMiddleware() {
8660
10117
  await ensureRepoHasInitialCommit(gitRoot);
8661
10118
  await runCommand3("git", ["worktree", "add", "--detach", worktreeCwd, startPoint], { cwd: gitRoot });
8662
10119
  }
10120
+ try {
10121
+ await persistWorkspaceRoot(worktreeCwd);
10122
+ } catch (error) {
10123
+ await rollbackCreatedWorktree(gitRoot, worktreeCwd, worktreeParent);
10124
+ throw error;
10125
+ }
8663
10126
  setJson4(res, 200, {
8664
10127
  data: {
8665
10128
  cwd: worktreeCwd,
@@ -8672,6 +10135,75 @@ function createCodexBridgeMiddleware() {
8672
10135
  }
8673
10136
  return;
8674
10137
  }
10138
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/create-permanent") {
10139
+ const payload = asRecord5(await readJsonBody2(req));
10140
+ const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
10141
+ const rawWorktreeName = typeof payload?.worktreeName === "string" ? payload.worktreeName.trim() : "";
10142
+ if (!rawSourceCwd) {
10143
+ setJson4(res, 400, { error: "Missing sourceCwd" });
10144
+ return;
10145
+ }
10146
+ if (!rawWorktreeName) {
10147
+ setJson4(res, 400, { error: "Missing worktreeName" });
10148
+ return;
10149
+ }
10150
+ if (rawWorktreeName.includes("/") || rawWorktreeName.includes("\\") || rawWorktreeName === "." || rawWorktreeName === "..") {
10151
+ setJson4(res, 400, { error: "Worktree name must be a single folder name" });
10152
+ return;
10153
+ }
10154
+ const sourceCwd = isAbsolute2(rawSourceCwd) ? rawSourceCwd : resolve2(rawSourceCwd);
10155
+ try {
10156
+ const sourceInfo = await stat4(sourceCwd);
10157
+ if (!sourceInfo.isDirectory()) {
10158
+ setJson4(res, 400, { error: "sourceCwd is not a directory" });
10159
+ return;
10160
+ }
10161
+ } catch {
10162
+ setJson4(res, 404, { error: "sourceCwd does not exist" });
10163
+ return;
10164
+ }
10165
+ try {
10166
+ let gitRoot = "";
10167
+ try {
10168
+ gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
10169
+ } catch (error) {
10170
+ if (!isNotGitRepositoryError2(error)) throw error;
10171
+ await runCommand3("git", ["init"], { cwd: sourceCwd });
10172
+ gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
10173
+ }
10174
+ const worktreeCwd = join6(dirname2(gitRoot), rawWorktreeName);
10175
+ try {
10176
+ await stat4(worktreeCwd);
10177
+ setJson4(res, 409, { error: "Worktree folder already exists" });
10178
+ return;
10179
+ } catch {
10180
+ }
10181
+ const branchName = await allocatePermanentWorktreeBranchName(gitRoot, rawWorktreeName);
10182
+ try {
10183
+ await runCommand3("git", ["worktree", "add", "-b", branchName, worktreeCwd, "HEAD"], { cwd: gitRoot });
10184
+ } catch (error) {
10185
+ if (!isMissingHeadError2(error)) throw error;
10186
+ await ensureRepoHasInitialCommit(gitRoot);
10187
+ await runCommand3("git", ["worktree", "add", "-b", branchName, worktreeCwd, "HEAD"], { cwd: gitRoot });
10188
+ }
10189
+ try {
10190
+ await persistWorkspaceRoot(worktreeCwd);
10191
+ } catch (error) {
10192
+ await rollbackCreatedWorktree(gitRoot, worktreeCwd, void 0, branchName);
10193
+ throw error;
10194
+ }
10195
+ setJson4(res, 200, {
10196
+ data: {
10197
+ cwd: worktreeCwd,
10198
+ branch: branchName,
10199
+ gitRoot
10200
+ }
10201
+ });
10202
+ } catch (error) {
10203
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to create worktree") });
10204
+ }
10205
+ return;
10206
+ }
8675
10207
  if (req.method === "GET" && url.pathname === "/codex-api/worktree/branches") {
8676
10208
  const rawSourceCwd = (url.searchParams.get("sourceCwd") ?? "").trim();
8677
10209
  if (!rawSourceCwd) {
@@ -8758,11 +10290,11 @@ function createCodexBridgeMiddleware() {
8758
10290
  });
8759
10291
  return;
8760
10292
  }
8761
- const currentBranchRaw = await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot });
8762
- const currentBranch = currentBranchRaw.trim() || null;
10293
+ const state = await readGitHeaderState(gitRoot);
10294
+ const currentBranch = state.currentBranch;
8763
10295
  const output = await runCommandCapture2(
8764
10296
  "git",
8765
- ["for-each-ref", "--format=%(committerdate:unix) %(refname)", "refs/heads", "refs/remotes"],
10297
+ ["for-each-ref", "--format=%(committerdate:unix) %(refname) %(objectname)", "refs/heads", "refs/remotes"],
8766
10298
  { cwd: gitRoot }
8767
10299
  );
8768
10300
  const branchActivityByName = /* @__PURE__ */ new Map();
@@ -8772,23 +10304,29 @@ function createCodexBridgeMiddleware() {
8772
10304
  if (!normalized || normalized === "origin/HEAD") continue;
8773
10305
  const parsedTimestamp = Number.parseInt(rawTimestamp.trim(), 10);
8774
10306
  const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : 0;
8775
- const current = branchActivityByName.get(normalized) ?? Number.MIN_SAFE_INTEGER;
8776
- if (timestamp > current) {
8777
- branchActivityByName.set(normalized, timestamp);
10307
+ const isRemote = rawRefName.trim().startsWith("refs/remotes/");
10308
+ const current = branchActivityByName.get(normalized);
10309
+ if (!current || timestamp > current.timestamp) {
10310
+ branchActivityByName.set(normalized, { timestamp, isRemote });
8778
10311
  }
8779
10312
  }
8780
10313
  if (currentBranch && !branchActivityByName.has(currentBranch)) {
8781
- branchActivityByName.set(currentBranch, Number.MAX_SAFE_INTEGER);
10314
+ branchActivityByName.set(currentBranch, { timestamp: Number.MAX_SAFE_INTEGER, isRemote: false });
8782
10315
  }
8783
- const options = Array.from(branchActivityByName.entries()).map(([value]) => ({ value, label: value })).sort((a, b) => {
8784
- const aActivity = branchActivityByName.get(a.value) ?? 0;
8785
- const bActivity = branchActivityByName.get(b.value) ?? 0;
10316
+ const options = Array.from(branchActivityByName.entries()).map(([value, metadata]) => ({
10317
+ value,
10318
+ label: value,
10319
+ isCurrent: value === currentBranch,
10320
+ isRemote: metadata.isRemote
10321
+ })).sort((a, b) => {
10322
+ const aActivity = branchActivityByName.get(a.value)?.timestamp ?? 0;
10323
+ const bActivity = branchActivityByName.get(b.value)?.timestamp ?? 0;
8786
10324
  if (bActivity !== aActivity) return bActivity - aActivity;
8787
10325
  return a.value.localeCompare(b.value);
8788
10326
  });
8789
10327
  setJson4(res, 200, {
8790
10328
  data: {
8791
- currentBranch,
10329
+ ...state,
8792
10330
  options
8793
10331
  }
8794
10332
  });
@@ -8797,8 +10335,47 @@ function createCodexBridgeMiddleware() {
8797
10335
  }
8798
10336
  return;
8799
10337
  }
10338
+ if (req.method === "GET" && url.pathname === "/codex-api/git/repository-status") {
10339
+ const rawCwd = (url.searchParams.get("cwd") ?? "").trim();
10340
+ if (!rawCwd) {
10341
+ setJson4(res, 400, { error: "Missing cwd" });
10342
+ return;
10343
+ }
10344
+ const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
10345
+ try {
10346
+ const cwdInfo = await stat4(cwd);
10347
+ if (!cwdInfo.isDirectory()) {
10348
+ setJson4(res, 400, { error: "cwd is not a directory" });
10349
+ return;
10350
+ }
10351
+ } catch {
10352
+ setJson4(res, 404, { error: "cwd does not exist" });
10353
+ return;
10354
+ }
10355
+ try {
10356
+ const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
10357
+ setJson4(res, 200, {
10358
+ data: {
10359
+ isGitRepo: true,
10360
+ gitRoot
10361
+ }
10362
+ });
10363
+ } catch (error) {
10364
+ if (!isNotGitRepositoryError2(error)) {
10365
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to read Git repository status") });
10366
+ return;
10367
+ }
10368
+ setJson4(res, 200, {
10369
+ data: {
10370
+ isGitRepo: false,
10371
+ gitRoot: ""
10372
+ }
10373
+ });
10374
+ }
10375
+ return;
10376
+ }
8800
10377
  if (req.method === "POST" && url.pathname === "/codex-api/git/checkout") {
8801
- const payload = await readJsonBody(req);
10378
+ const payload = await readJsonBody2(req);
8802
10379
  const record = asRecord5(payload);
8803
10380
  if (!record) {
8804
10381
  setJson4(res, 400, { error: "Invalid body: expected object" });
@@ -8827,52 +10404,127 @@ function createCodexBridgeMiddleware() {
8827
10404
  }
8828
10405
  try {
8829
10406
  const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
8830
- try {
8831
- await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
8832
- } catch (checkoutError) {
8833
- const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, targetBranch);
8834
- if (!blockingWorktreePath) {
8835
- throw checkoutError;
8836
- }
8837
- await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
8838
- await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
8839
- }
8840
- const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim() || null;
8841
- setJson4(res, 200, { data: { currentBranch } });
10407
+ await assertNoTrackedGitChanges(gitRoot);
10408
+ await checkoutGitBranchWithWorktreeRecovery(gitRoot, targetBranch);
10409
+ setJson4(res, 200, { data: await readGitHeaderState(gitRoot) });
8842
10410
  } catch (error) {
8843
10411
  setJson4(res, 500, { error: getErrorMessage5(error, "Failed to switch branch") });
8844
10412
  }
8845
10413
  return;
8846
10414
  }
10415
+ if (req.method === "GET" && url.pathname === "/codex-api/git/branch-commits") {
10416
+ const rawCwd = (url.searchParams.get("cwd") ?? "").trim();
10417
+ const branch = (url.searchParams.get("branch") ?? "").trim();
10418
+ if (!rawCwd) {
10419
+ setJson4(res, 400, { error: "Missing cwd" });
10420
+ return;
10421
+ }
10422
+ if (!branch) {
10423
+ setJson4(res, 400, { error: "Missing branch" });
10424
+ return;
10425
+ }
10426
+ const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
10427
+ try {
10428
+ const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
10429
+ await runCommandCapture2("git", ["rev-parse", "--verify", `${branch}^{commit}`], { cwd: gitRoot });
10430
+ const resetHistoryRefPrefix = `refs/codex/header-git-reset-history/${branch}/`;
10431
+ const resetHistoryRefsRaw = await runCommandCapture2(
10432
+ "git",
10433
+ ["for-each-ref", "--sort=-creatordate", "--format=%(refname)", resetHistoryRefPrefix],
10434
+ { cwd: gitRoot }
10435
+ ).catch(() => "");
10436
+ const resetHistoryRefs = resetHistoryRefsRaw.split("\n").map((entry) => entry.trim()).filter(Boolean).slice(0, HEADER_GIT_RESET_HISTORY_REF_LIMIT);
10437
+ const output = await runCommandCapture2(
10438
+ "git",
10439
+ ["log", "-n", "12", "--date=short", "--format=%H%x09%h%x09%cd%x09%s", branch, ...resetHistoryRefs],
10440
+ { cwd: gitRoot }
10441
+ );
10442
+ const commits = output.split("\n").flatMap((line) => {
10443
+ const [sha = "", shortSha = "", date = "", ...subjectParts] = line.split(" ");
10444
+ const subject = subjectParts.join(" ").trim();
10445
+ return sha.trim() && shortSha.trim() ? [{ sha: sha.trim(), shortSha: shortSha.trim(), date: date.trim(), subject: subject || shortSha.trim() }] : [];
10446
+ });
10447
+ setJson4(res, 200, { data: commits });
10448
+ } catch (error) {
10449
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to load branch commits") });
10450
+ }
10451
+ return;
10452
+ }
10453
+ if (req.method === "POST" && url.pathname === "/codex-api/git/reset-to-commit") {
10454
+ const payload = await readJsonBody2(req);
10455
+ const record = asRecord5(payload);
10456
+ if (!record) {
10457
+ setJson4(res, 400, { error: "Invalid body: expected object" });
10458
+ return;
10459
+ }
10460
+ const rawCwd = readNonEmptyString(record.cwd);
10461
+ const branch = readNonEmptyString(record.branch);
10462
+ const sha = readNonEmptyString(record.sha);
10463
+ if (!rawCwd) {
10464
+ setJson4(res, 400, { error: "Missing cwd" });
10465
+ return;
10466
+ }
10467
+ if (!branch) {
10468
+ setJson4(res, 400, { error: "Missing branch" });
10469
+ return;
10470
+ }
10471
+ if (!sha) {
10472
+ setJson4(res, 400, { error: "Missing commit" });
10473
+ return;
10474
+ }
10475
+ const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
10476
+ try {
10477
+ const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
10478
+ await assertNoTrackedGitChanges(gitRoot);
10479
+ await assertLocalGitBranch(gitRoot, branch);
10480
+ const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim();
10481
+ if (currentBranch && currentBranch !== branch) {
10482
+ await checkoutGitBranchWithWorktreeRecovery(gitRoot, branch);
10483
+ } else if (!currentBranch) {
10484
+ await checkoutGitBranchWithWorktreeRecovery(gitRoot, branch);
10485
+ }
10486
+ const previousTip = await runCommandCapture2("git", ["rev-parse", "HEAD"], { cwd: gitRoot });
10487
+ const targetSha = await runCommandCapture2("git", ["rev-parse", "--verify", `${sha}^{commit}`], { cwd: gitRoot });
10488
+ await runCommand3("git", ["update-ref", toHeaderGitResetHistoryRef(branch, previousTip.trim()), previousTip.trim()], { cwd: gitRoot });
10489
+ await pruneHeaderGitResetHistoryRefs(gitRoot, branch);
10490
+ await runCommand3("git", ["reset", "--hard", targetSha.trim()], { cwd: gitRoot });
10491
+ setJson4(res, 200, { data: await readGitHeaderState(gitRoot) });
10492
+ } catch (error) {
10493
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to reset branch to commit") });
10494
+ }
10495
+ return;
10496
+ }
8847
10497
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
8848
- const payload = await readJsonBody(req);
10498
+ const payload = await readJsonBody2(req);
8849
10499
  const record = asRecord5(payload);
8850
10500
  if (!record) {
8851
10501
  setJson4(res, 400, { error: "Invalid body: expected object" });
8852
10502
  return;
8853
10503
  }
8854
- const nextState = {
10504
+ await updateWorkspaceRootsState((existingState) => ({
8855
10505
  order: normalizeStringArray(record.order),
8856
10506
  labels: normalizeStringRecord(record.labels),
8857
- active: normalizeStringArray(record.active)
8858
- };
8859
- await writeWorkspaceRootsState(nextState);
10507
+ active: normalizeStringArray(record.active),
10508
+ projectOrder: Array.isArray(record.projectOrder) ? normalizeStringArray(record.projectOrder) : existingState.projectOrder,
10509
+ remoteProjects: existingState.remoteProjects
10510
+ }));
8860
10511
  setJson4(res, 200, { ok: true });
8861
10512
  return;
8862
10513
  }
8863
10514
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-queue-state") {
8864
- const payload = await readJsonBody(req);
10515
+ const payload = await readJsonBody2(req);
8865
10516
  const record = asRecord5(payload);
8866
10517
  if (!record) {
8867
10518
  setJson4(res, 400, { error: "Invalid body: expected object" });
8868
10519
  return;
8869
10520
  }
8870
10521
  await writeThreadQueueState(normalizeThreadQueueState(record));
10522
+ void backendQueueProcessor.scheduleAllQueuedThreads();
8871
10523
  setJson4(res, 200, { ok: true });
8872
10524
  return;
8873
10525
  }
8874
10526
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
8875
- const payload = asRecord5(await readJsonBody(req));
10527
+ const payload = asRecord5(await readJsonBody2(req));
8876
10528
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
8877
10529
  const createIfMissing = payload?.createIfMissing === true;
8878
10530
  const label = typeof payload?.label === "string" ? payload.label : "";
@@ -8897,23 +10549,12 @@ function createCodexBridgeMiddleware() {
8897
10549
  setJson4(res, 404, { error: "Directory does not exist" });
8898
10550
  return;
8899
10551
  }
8900
- const existingState = await readWorkspaceRootsState();
8901
- const nextOrder = [normalizedPath, ...existingState.order.filter((item) => item !== normalizedPath)];
8902
- const nextActive = [normalizedPath, ...existingState.active.filter((item) => item !== normalizedPath)];
8903
- const nextLabels = { ...existingState.labels };
8904
- if (label.trim().length > 0) {
8905
- nextLabels[normalizedPath] = label.trim();
8906
- }
8907
- await writeWorkspaceRootsState({
8908
- order: nextOrder,
8909
- labels: nextLabels,
8910
- active: nextActive
8911
- });
10552
+ await persistWorkspaceRoot(normalizedPath, label);
8912
10553
  setJson4(res, 200, { data: { path: normalizedPath } });
8913
10554
  return;
8914
10555
  }
8915
10556
  if (req.method === "POST" && url.pathname === "/codex-api/local-directory") {
8916
- const payload = asRecord5(await readJsonBody(req));
10557
+ const payload = asRecord5(await readJsonBody2(req));
8917
10558
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
8918
10559
  if (!rawPath) {
8919
10560
  setJson4(res, 400, { error: "Missing path" });
@@ -8933,7 +10574,7 @@ function createCodexBridgeMiddleware() {
8933
10574
  return;
8934
10575
  }
8935
10576
  if (req.method === "POST" && url.pathname === "/codex-api/projectless-thread-cwd") {
8936
- const payload = asRecord5(await readJsonBody(req));
10577
+ const payload = asRecord5(await readJsonBody2(req));
8937
10578
  const prompt = typeof payload?.prompt === "string" ? payload.prompt : null;
8938
10579
  try {
8939
10580
  const directory = await createProjectlessThreadDirectory(prompt);
@@ -8977,7 +10618,7 @@ function createCodexBridgeMiddleware() {
8977
10618
  return;
8978
10619
  }
8979
10620
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
8980
- const payload = asRecord5(await readJsonBody(req));
10621
+ const payload = asRecord5(await readJsonBody2(req));
8981
10622
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
8982
10623
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
8983
10624
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
@@ -9011,7 +10652,7 @@ function createCodexBridgeMiddleware() {
9011
10652
  return;
9012
10653
  }
9013
10654
  if (req.method === "POST" && url.pathname === "/codex-api/prompts") {
9014
- const payload = asRecord5(await readJsonBody(req));
10655
+ const payload = asRecord5(await readJsonBody2(req));
9015
10656
  const name = typeof payload?.name === "string" ? payload.name.trim() : "";
9016
10657
  const content = typeof payload?.content === "string" ? payload.content : "";
9017
10658
  if (!name || !content.trim()) {
@@ -9062,16 +10703,17 @@ function createCodexBridgeMiddleware() {
9062
10703
  }
9063
10704
  if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
9064
10705
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
10706
+ const automationId = url.searchParams.get("automationId")?.trim() ?? "";
9065
10707
  if (!threadId) {
9066
10708
  setJson4(res, 400, { error: "Missing threadId" });
9067
10709
  return;
9068
10710
  }
9069
- const automation = await readThreadHeartbeatAutomation(threadId);
10711
+ const automation = automationId ? await readThreadHeartbeatAutomation(threadId, automationId) : await readThreadHeartbeatAutomations(threadId);
9070
10712
  setJson4(res, 200, { data: automation });
9071
10713
  return;
9072
10714
  }
9073
10715
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
9074
- const payload = asRecord5(await readJsonBody(req));
10716
+ const payload = asRecord5(await readJsonBody2(req));
9075
10717
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
9076
10718
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
9077
10719
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
@@ -9085,7 +10727,7 @@ function createCodexBridgeMiddleware() {
9085
10727
  return;
9086
10728
  }
9087
10729
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
9088
- const payload = asRecord5(await readJsonBody(req));
10730
+ const payload = asRecord5(await readJsonBody2(req));
9089
10731
  const id = typeof payload?.id === "string" ? payload.id : "";
9090
10732
  const title = typeof payload?.title === "string" ? payload.title : "";
9091
10733
  if (!id) {
@@ -9099,22 +10741,23 @@ function createCodexBridgeMiddleware() {
9099
10741
  return;
9100
10742
  }
9101
10743
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-pins") {
9102
- const payload = asRecord5(await readJsonBody(req));
10744
+ const payload = asRecord5(await readJsonBody2(req));
9103
10745
  const threadIds = normalizePinnedThreadIds(payload?.threadIds);
9104
10746
  await writePinnedThreadIds(threadIds);
9105
10747
  setJson4(res, 200, { ok: true });
9106
10748
  return;
9107
10749
  }
9108
10750
  if (req.method === "PUT" && url.pathname === "/codex-api/preferences/first-launch-plugins-card") {
9109
- const payload = asRecord5(await readJsonBody(req));
10751
+ const payload = asRecord5(await readJsonBody2(req));
9110
10752
  const dismissed = payload?.dismissed === true;
9111
10753
  await writeFirstLaunchPluginsCardDismissed(dismissed);
9112
10754
  setJson4(res, 200, { ok: true });
9113
10755
  return;
9114
10756
  }
9115
10757
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
9116
- const payload = asRecord5(await readJsonBody(req));
10758
+ const payload = asRecord5(await readJsonBody2(req));
9117
10759
  const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
10760
+ const id = typeof payload?.id === "string" ? payload.id.trim() : "";
9118
10761
  const name = typeof payload?.name === "string" ? payload.name.trim() : "";
9119
10762
  const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
9120
10763
  const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
@@ -9123,22 +10766,41 @@ function createCodexBridgeMiddleware() {
9123
10766
  setJson4(res, 400, { error: "threadId, name, prompt, and rrule are required" });
9124
10767
  return;
9125
10768
  }
9126
- const automation = await writeThreadHeartbeatAutomation({ threadId, name, prompt, rrule, status });
10769
+ const automation = await writeThreadHeartbeatAutomation({ threadId, id, name, prompt, rrule, status });
9127
10770
  setJson4(res, 200, { data: automation });
9128
10771
  return;
9129
10772
  }
10773
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-automation/run") {
10774
+ const payload = asRecord5(await readJsonBody2(req));
10775
+ const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
10776
+ const automationId = typeof payload?.automationId === "string" ? payload.automationId.trim() : "";
10777
+ if (!threadId || !automationId) {
10778
+ setJson4(res, 400, { error: "threadId and automationId are required" });
10779
+ return;
10780
+ }
10781
+ const automation = await readThreadHeartbeatAutomation(threadId, automationId);
10782
+ if (!automation) {
10783
+ setJson4(res, 404, { error: "Automation not found for thread" });
10784
+ return;
10785
+ }
10786
+ await appendThreadQueuedMessage(threadId, buildHeartbeatQueuedMessage(automation));
10787
+ backendQueueProcessor.scheduleThreadQueueDrain(threadId, 0);
10788
+ setJson4(res, 200, { data: { queued: true } });
10789
+ return;
10790
+ }
9130
10791
  if (req.method === "DELETE" && url.pathname === "/codex-api/thread-automation") {
9131
10792
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
10793
+ const automationId = url.searchParams.get("automationId")?.trim() ?? "";
9132
10794
  if (!threadId) {
9133
10795
  setJson4(res, 400, { error: "Missing threadId" });
9134
10796
  return;
9135
10797
  }
9136
- const removed = await deleteThreadHeartbeatAutomation(threadId);
10798
+ const removed = await deleteThreadHeartbeatAutomation(threadId, automationId);
9137
10799
  setJson4(res, 200, { data: { removed } });
9138
10800
  return;
9139
10801
  }
9140
10802
  if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
9141
- const payload = asRecord5(await readJsonBody(req));
10803
+ const payload = asRecord5(await readJsonBody2(req));
9142
10804
  const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
9143
10805
  const rawAllowedUserIds = Array.isArray(payload?.allowedUserIds) ? payload.allowedUserIds : [];
9144
10806
  if (!botToken) {
@@ -9219,6 +10881,7 @@ data: ${JSON.stringify({ ok: true })}
9219
10881
  threadSearchIndex = null;
9220
10882
  telegramBridge.stop();
9221
10883
  terminalManager.dispose();
10884
+ backendQueueProcessor.dispose();
9222
10885
  appServer.dispose();
9223
10886
  };
9224
10887
  middleware.subscribeNotifications = (listener) => {
@@ -9244,7 +10907,7 @@ data: ${JSON.stringify({ ok: true })}
9244
10907
 
9245
10908
  // src/server/authMiddleware.ts
9246
10909
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
9247
- import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
10910
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync3 } from "fs";
9248
10911
  import { homedir as homedir6 } from "os";
9249
10912
  import { dirname as dirname3, join as join7 } from "path";
9250
10913
  var TOKEN_COOKIE = "portal_session";
@@ -9326,10 +10989,10 @@ function readPersistedSessions() {
9326
10989
  }
9327
10990
  function persistSessions(validTokens) {
9328
10991
  const sessionStorePath = getSessionStorePath();
9329
- mkdirSync(dirname3(sessionStorePath), { recursive: true });
10992
+ mkdirSync2(dirname3(sessionStorePath), { recursive: true });
9330
10993
  const tokens = Array.from(validTokens.entries()).sort((left, right) => right[1] - left[1]).slice(0, MAX_PERSISTED_TOKENS).map(([value, expiresAt]) => ({ value, expiresAt }));
9331
10994
  const tmpPath = `${sessionStorePath}.tmp`;
9332
- writeFileSync2(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
10995
+ writeFileSync3(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
9333
10996
  `, { encoding: "utf8", mode: 384 });
9334
10997
  renameSync(tmpPath, sessionStorePath);
9335
10998
  }
@@ -10126,7 +11789,7 @@ function hasPromptedCloudflaredInstallPersisted() {
10126
11789
  }
10127
11790
  async function persistCloudflaredInstallPrompted() {
10128
11791
  const codexHome = getCodexHomePath();
10129
- mkdirSync2(codexHome, { recursive: true });
11792
+ mkdirSync3(codexHome, { recursive: true });
10130
11793
  await writeFile6(getCloudflaredPromptMarkerPath(), `${Date.now()}
10131
11794
  `, "utf8");
10132
11795
  }
@@ -10212,7 +11875,7 @@ async function ensureCloudflaredInstalledLinux() {
10212
11875
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
10213
11876
  }
10214
11877
  const userBinDir = join10(homedir7(), ".local", "bin");
10215
- mkdirSync2(userBinDir, { recursive: true });
11878
+ mkdirSync3(userBinDir, { recursive: true });
10216
11879
  const destination = join10(userBinDir, "cloudflared");
10217
11880
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
10218
11881
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
@@ -10312,12 +11975,24 @@ Global npm install requires elevated permissions. Retrying with --prefix ${userP
10312
11975
  }
10313
11976
  function resolvePassword(input) {
10314
11977
  if (input === false) {
10315
- return void 0;
11978
+ return { password: void 0, generated: false };
10316
11979
  }
10317
11980
  if (typeof input === "string") {
10318
- return input;
11981
+ return { password: input, generated: false };
10319
11982
  }
10320
- return generatePassword();
11983
+ return { password: generatePassword(), generated: true };
11984
+ }
11985
+ function getGeneratedPasswordPath() {
11986
+ return join10(getCodexHomePath(), "codexui-password");
11987
+ }
11988
+ async function persistGeneratedPassword(password) {
11989
+ const codexHome = getCodexHomePath();
11990
+ mkdirSync3(codexHome, { recursive: true });
11991
+ const passwordPath = getGeneratedPasswordPath();
11992
+ await writeFile6(passwordPath, `${password}
11993
+ `, { encoding: "utf8", mode: 384 });
11994
+ chmodSync2(passwordPath, 384);
11995
+ return passwordPath;
10321
11996
  }
10322
11997
  function printTermuxKeepAlive(lines) {
10323
11998
  if (!isTermuxRuntime()) {
@@ -10336,9 +12011,8 @@ function openBrowser(url) {
10336
12011
  });
10337
12012
  child.unref();
10338
12013
  }
10339
- function buildTunnelAutologinUrl(tunnelUrl, password) {
10340
- if (!password) return tunnelUrl;
10341
- return `${tunnelUrl}/password=${encodeURIComponent(password)}`;
12014
+ function buildTunnelAutologinUrl(tunnelUrl, _password) {
12015
+ return tunnelUrl;
10342
12016
  }
10343
12017
  function parseCloudflaredUrl(chunk) {
10344
12018
  const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
@@ -10533,7 +12207,9 @@ async function startServer(options) {
10533
12207
  console.log("\nCodex is not logged in. You can log in later via settings or run `codexui login`.\n");
10534
12208
  }
10535
12209
  const requestedPort = parseInt(options.port, 10);
10536
- const password = resolvePassword(options.password);
12210
+ const passwordResolution = resolvePassword(options.password);
12211
+ const password = passwordResolution.password;
12212
+ const generatedPasswordPath = password && passwordResolution.generated ? await persistGeneratedPassword(password) : null;
10537
12213
  const { app, dispose, attachWebSocket } = createServer({ password });
10538
12214
  const server = createServer2(app);
10539
12215
  attachWebSocket(server);
@@ -10575,8 +12251,9 @@ async function startServer(options) {
10575
12251
  if (port !== requestedPort) {
10576
12252
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
10577
12253
  }
10578
- if (password) {
10579
- lines.push(` Password: ${password}`);
12254
+ if (generatedPasswordPath) {
12255
+ lines.push(` Generated password file: ${generatedPasswordPath}`);
12256
+ lines.push(" Use that file to retrieve the password for untrusted origins.");
10580
12257
  }
10581
12258
  const tunnelQrUrl = tunnelUrl ? buildTunnelAutologinUrl(tunnelUrl, password) : null;
10582
12259
  if (tunnelUrl) {