codexui-android 0.1.102 → 0.1.104

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,34 @@ 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
+ }
6870
7945
  function normalizeReasoningEffort(value) {
6871
7946
  const allowed = ["none", "minimal", "low", "medium", "high", "xhigh"];
6872
7947
  return typeof value === "string" && allowed.includes(value) ? value : "";
@@ -6899,6 +7974,25 @@ function buildTextWithAttachments(prompt, files) {
6899
7974
  ${prompt}
6900
7975
  `;
6901
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
+ }
6902
7996
  function fileNameFromPath(pathValue) {
6903
7997
  const normalized = pathValue.replace(/\\/g, "/");
6904
7998
  const segments = normalized.split("/").filter(Boolean);
@@ -7019,7 +8113,9 @@ async function readWorkspaceRootsState() {
7019
8113
  return {
7020
8114
  order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
7021
8115
  labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
7022
- 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"])
7023
8119
  };
7024
8120
  }
7025
8121
  async function writeWorkspaceRootsState(nextState) {
@@ -7034,8 +8130,58 @@ async function writeWorkspaceRootsState(nextState) {
7034
8130
  payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
7035
8131
  payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
7036
8132
  payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
8133
+ payload["project-order"] = normalizeStringArray(nextState.projectOrder);
7037
8134
  await writeFile4(statePath, JSON.stringify(payload), "utf8");
7038
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 });
8175
+ } catch {
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);
8183
+ }
8184
+ }
7039
8185
  function normalizeTelegramBridgeConfig(value) {
7040
8186
  const record = asRecord5(value);
7041
8187
  if (!record) return { botToken: "", chatIds: [], allowedUserIds: [] };
@@ -7091,7 +8237,7 @@ function rememberTelegramChatId(chatId) {
7091
8237
  });
7092
8238
  return telegramBridgeConfigMutation;
7093
8239
  }
7094
- async function readJsonBody(req) {
8240
+ async function readJsonBody2(req) {
7095
8241
  const raw = await readRawBody(req);
7096
8242
  if (raw.length === 0) return null;
7097
8243
  const text = raw.toString("utf8").trim();
@@ -7309,6 +8455,7 @@ var AppServerProcess = class {
7309
8455
  this.lastThreadReadSnapshotByThreadId = /* @__PURE__ */ new Map();
7310
8456
  this.capturedItemsByThreadId = /* @__PURE__ */ new Map();
7311
8457
  this.liveStateCache = /* @__PURE__ */ new Map();
8458
+ this.chatgptAuthRefreshPromise = null;
7312
8459
  }
7313
8460
  getCodexCommand() {
7314
8461
  const codexCommand = resolveCodexCommand();
@@ -7329,10 +8476,11 @@ var AppServerProcess = class {
7329
8476
  const serverPort = parseInt(process.env.CODEXUI_SERVER_PORT ?? "", 10) || void 0;
7330
8477
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7331
8478
  try {
7332
- const raw = readFileSync2(statePath, "utf8");
7333
- const state = JSON.parse(raw);
7334
- args.push(...getFreeModeConfigArgs(state, serverPort));
7335
- extraEnv = getFreeModeEnvVars(state);
8479
+ const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
8480
+ if (state) {
8481
+ args.push(...getFreeModeConfigArgs(state, serverPort));
8482
+ extraEnv = getFreeModeEnvVars(state);
8483
+ }
7336
8484
  } catch {
7337
8485
  }
7338
8486
  return { args, env: extraEnv };
@@ -7571,7 +8719,46 @@ var AppServerProcess = class {
7571
8719
  }
7572
8720
  });
7573
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
+ }
7574
8757
  handleServerRequest(requestId, method, params) {
8758
+ if (method === "account/chatgptAuthTokens/refresh") {
8759
+ void this.handleChatgptAuthTokensRefreshRequest(requestId, params);
8760
+ return;
8761
+ }
7575
8762
  const pendingRequest = {
7576
8763
  id: requestId,
7577
8764
  method,
@@ -7694,53 +8881,124 @@ var BackendQueueProcessor = class {
7694
8881
  constructor(appServer) {
7695
8882
  this.appServer = appServer;
7696
8883
  this.processingThreadIds = /* @__PURE__ */ new Set();
8884
+ this.queueDrainTimersByThreadId = /* @__PURE__ */ new Map();
8885
+ this.queueDrainDueAtByThreadId = /* @__PURE__ */ new Map();
7697
8886
  this.unsubscribe = appServer.onNotification((notification) => {
7698
8887
  if (!isTurnCompletedNotification(notification)) return;
7699
8888
  const threadId = extractThreadIdFromNotificationParams(notification.params);
7700
8889
  if (!threadId) return;
7701
8890
  void this.processThreadQueue(threadId);
7702
8891
  });
8892
+ void this.scheduleAllQueuedThreads(1e3);
7703
8893
  }
7704
8894
  dispose() {
7705
8895
  this.unsubscribe();
8896
+ for (const timer of this.queueDrainTimersByThreadId.values()) {
8897
+ clearTimeout(timer);
8898
+ }
8899
+ this.queueDrainTimersByThreadId.clear();
8900
+ this.queueDrainDueAtByThreadId.clear();
7706
8901
  this.processingThreadIds.clear();
7707
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
+ }
7708
8933
  async processThreadQueue(threadId) {
7709
8934
  if (this.processingThreadIds.has(threadId)) return;
7710
8935
  this.processingThreadIds.add(threadId);
7711
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
+ }
7712
8944
  const next = await this.popNextQueuedTurn(threadId);
7713
8945
  if (!next) return;
7714
8946
  try {
7715
8947
  await this.startQueuedTurn(next);
8948
+ if (await this.hasQueuedTurns(threadId)) {
8949
+ this.scheduleThreadQueueDrain(threadId);
8950
+ }
7716
8951
  } catch {
7717
8952
  await this.restoreQueuedTurn(next);
8953
+ this.scheduleThreadQueueDrain(threadId);
7718
8954
  }
7719
8955
  } catch {
8956
+ this.scheduleThreadQueueDrain(threadId);
7720
8957
  } finally {
7721
8958
  this.processingThreadIds.delete(threadId);
7722
8959
  }
7723
8960
  }
7724
- async popNextQueuedTurn(threadId) {
8961
+ async hasQueuedTurns(threadId) {
7725
8962
  const state = await readThreadQueueState();
7726
8963
  const queue = state[threadId];
7727
- if (!queue || queue.length === 0) return null;
7728
- const [message, ...rest] = queue;
7729
- const nextState = { ...state };
7730
- if (rest.length > 0) {
7731
- nextState[threadId] = rest;
7732
- } else {
7733
- delete nextState[threadId];
7734
- }
7735
- await writeThreadQueueState(nextState);
7736
- return { threadId, message };
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
+ });
7737
8991
  }
7738
8992
  async restoreQueuedTurn(turn) {
7739
- const state = await readThreadQueueState();
7740
- const queue = state[turn.threadId] ?? [];
7741
- await writeThreadQueueState({
7742
- ...state,
7743
- [turn.threadId]: [turn.message, ...queue]
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
+ };
7744
9002
  });
7745
9003
  }
7746
9004
  async resolveCollaborationModeSettings(mode) {
@@ -8091,6 +9349,10 @@ function createCodexBridgeMiddleware() {
8091
9349
  }
8092
9350
  const url = new URL(req.url, "http://localhost");
8093
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
+ }
8094
9356
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
8095
9357
  let bearerToken = "";
8096
9358
  let wireApi = "chat";
@@ -8108,9 +9370,9 @@ function createCodexBridgeMiddleware() {
8108
9370
  let bearerToken = "";
8109
9371
  let wireApi = "responses";
8110
9372
  try {
8111
- const state = JSON.parse(readFileSync2(statePath, "utf8"));
8112
- bearerToken = state.apiKey ?? "";
8113
- wireApi = state.wireApi === "chat" ? "chat" : "responses";
9373
+ const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
9374
+ bearerToken = state?.apiKey ?? "";
9375
+ wireApi = state?.wireApi === "chat" ? "chat" : "responses";
8114
9376
  } catch {
8115
9377
  }
8116
9378
  handleOpenRouterProxyRequest(req, res, bearerToken, wireApi);
@@ -8133,17 +9395,13 @@ function createCodexBridgeMiddleware() {
8133
9395
  }
8134
9396
  if (url.pathname.startsWith("/codex-api/free-mode")) {
8135
9397
  let readFreeModeState2 = function() {
8136
- try {
8137
- return JSON.parse(readFileSync2(statePath, "utf8"));
8138
- } catch {
8139
- return { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
8140
- }
9398
+ return ensureDefaultFreeModeStateForMissingAuthSync(statePath) ?? { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
8141
9399
  };
8142
9400
  var readFreeModeState = readFreeModeState2;
8143
9401
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
8144
9402
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode") {
8145
9403
  try {
8146
- const body = await readJsonBody(req);
9404
+ const body = await readJsonBody2(req);
8147
9405
  const enable = Boolean(body?.enable);
8148
9406
  if (enable) {
8149
9407
  const apiKey = getRandomFreeKey();
@@ -8199,12 +9457,12 @@ function createCodexBridgeMiddleware() {
8199
9457
  if (req.method === "GET" && url.pathname === "/codex-api/free-mode/status") {
8200
9458
  try {
8201
9459
  const state = readFreeModeState2();
8202
- const freeModels = await getFreeModels();
8203
9460
  const maskedKey = state.apiKey && state.customKey ? state.apiKey.substring(0, 12) + "..." + state.apiKey.substring(state.apiKey.length - 4) : null;
9461
+ refreshFreeModelsInBackground();
8204
9462
  setJson4(res, 200, {
8205
9463
  enabled: state.enabled,
8206
9464
  keyCount: getFreeKeyCount(),
8207
- models: freeModels,
9465
+ models: getCachedFreeModels(),
8208
9466
  currentModel: state.enabled ? state.model : null,
8209
9467
  customKey: Boolean(state.customKey),
8210
9468
  maskedKey,
@@ -8236,7 +9494,7 @@ function createCodexBridgeMiddleware() {
8236
9494
  }
8237
9495
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-key") {
8238
9496
  try {
8239
- const body = await readJsonBody(req);
9497
+ const body = await readJsonBody2(req);
8240
9498
  const key = typeof body?.key === "string" ? body.key.trim() : "";
8241
9499
  const current = readFreeModeState2();
8242
9500
  if (key.length > 0) {
@@ -8271,7 +9529,7 @@ function createCodexBridgeMiddleware() {
8271
9529
  }
8272
9530
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-provider") {
8273
9531
  try {
8274
- const body = await readJsonBody(req);
9532
+ const body = await readJsonBody2(req);
8275
9533
  const baseUrl = typeof body?.baseUrl === "string" ? body.baseUrl.trim() : "";
8276
9534
  const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
8277
9535
  const wireApi = body?.wireApi === "chat" ? "chat" : "responses";
@@ -8314,10 +9572,10 @@ function createCodexBridgeMiddleware() {
8314
9572
  if (await handleAccountRoutes(req, res, url, { appServer })) {
8315
9573
  return;
8316
9574
  }
8317
- if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
9575
+ if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody: readJsonBody2 })) {
8318
9576
  return;
8319
9577
  }
8320
- if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
9578
+ if (await handleReviewRoutes(req, res, url, { readJsonBody: readJsonBody2 })) {
8321
9579
  return;
8322
9580
  }
8323
9581
  if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal/status") {
@@ -8343,7 +9601,7 @@ function createCodexBridgeMiddleware() {
8343
9601
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8344
9602
  return;
8345
9603
  }
8346
- const body = asRecord5(await readJsonBody(req));
9604
+ const body = asRecord5(await readJsonBody2(req));
8347
9605
  const threadId = readNonEmptyString(body?.threadId);
8348
9606
  const cwd = readNonEmptyString(body?.cwd);
8349
9607
  if (!threadId || !cwd) {
@@ -8367,7 +9625,7 @@ function createCodexBridgeMiddleware() {
8367
9625
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8368
9626
  return;
8369
9627
  }
8370
- const body = asRecord5(await readJsonBody(req));
9628
+ const body = asRecord5(await readJsonBody2(req));
8371
9629
  const sessionId = readNonEmptyString(body?.sessionId);
8372
9630
  const data = typeof body?.data === "string" ? body.data : "";
8373
9631
  if (!sessionId) {
@@ -8384,7 +9642,7 @@ function createCodexBridgeMiddleware() {
8384
9642
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8385
9643
  return;
8386
9644
  }
8387
- const body = asRecord5(await readJsonBody(req));
9645
+ const body = asRecord5(await readJsonBody2(req));
8388
9646
  const sessionId = readNonEmptyString(body?.sessionId);
8389
9647
  if (!sessionId) {
8390
9648
  setJson4(res, 400, { error: "Missing sessionId" });
@@ -8400,7 +9658,7 @@ function createCodexBridgeMiddleware() {
8400
9658
  setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
8401
9659
  return;
8402
9660
  }
8403
- const body = asRecord5(await readJsonBody(req));
9661
+ const body = asRecord5(await readJsonBody2(req));
8404
9662
  const sessionId = readNonEmptyString(body?.sessionId);
8405
9663
  if (!sessionId) {
8406
9664
  setJson4(res, 400, { error: "Missing sessionId" });
@@ -8424,7 +9682,7 @@ function createCodexBridgeMiddleware() {
8424
9682
  return;
8425
9683
  }
8426
9684
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
8427
- const payload = await readJsonBody(req);
9685
+ const payload = await readJsonBody2(req);
8428
9686
  const body = asRecord5(payload);
8429
9687
  if (payload !== null && payload !== void 0) {
8430
9688
  requestBodyBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
@@ -8434,9 +9692,10 @@ function createCodexBridgeMiddleware() {
8434
9692
  setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
8435
9693
  return;
8436
9694
  }
8437
- const rpcResult = await appServer.rpc(body.method, body.params ?? null);
9695
+ const rpcResult = await callRpcWithArchiveRecovery(appServer, body.method, body.params ?? null);
8438
9696
  const trimmedResult = trimThreadTurnsInRpcResult(body.method, rpcResult);
8439
- 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;
8440
9699
  if (THREAD_METHODS_WITH_TURNS.has(body.method)) {
8441
9700
  const rpcRecord = asRecord5(result);
8442
9701
  const rpcThread = asRecord5(rpcRecord?.thread);
@@ -8572,7 +9831,7 @@ function createCodexBridgeMiddleware() {
8572
9831
  }
8573
9832
  if (req.method === "POST" && url.pathname === "/codex-api/thread/rollback-files") {
8574
9833
  try {
8575
- const body = asRecord5(await readJsonBody(req));
9834
+ const body = asRecord5(await readJsonBody2(req));
8576
9835
  const threadId = readNonEmptyString(body?.threadId);
8577
9836
  const turnId = readNonEmptyString(body?.turnId);
8578
9837
  const cwd = readNonEmptyString(body?.cwd);
@@ -8668,7 +9927,7 @@ function createCodexBridgeMiddleware() {
8668
9927
  }
8669
9928
  if (req.method === "POST" && url.pathname === "/codex-api/composio/link") {
8670
9929
  try {
8671
- const payload = asRecord5(await readJsonBody(req));
9930
+ const payload = asRecord5(await readJsonBody2(req));
8672
9931
  const slug = readNonEmptyString(payload?.slug);
8673
9932
  setJson4(res, 200, await startComposioLink(slug));
8674
9933
  } catch (error) {
@@ -8710,7 +9969,7 @@ function createCodexBridgeMiddleware() {
8710
9969
  return;
8711
9970
  }
8712
9971
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
8713
- const payload = await readJsonBody(req);
9972
+ const payload = await readJsonBody2(req);
8714
9973
  await appServer.respondToServerRequest(payload);
8715
9974
  setJson4(res, 200, { ok: true });
8716
9975
  return;
@@ -8731,8 +9990,8 @@ function createCodexBridgeMiddleware() {
8731
9990
  }
8732
9991
  if (req.method === "GET" && url.pathname === "/codex-api/provider-models") {
8733
9992
  try {
8734
- const fmState = JSON.parse(readFileSync2(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE), "utf8"));
8735
- if (fmState.enabled) {
9993
+ const fmState = ensureDefaultFreeModeStateForMissingAuthSync(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE));
9994
+ if (fmState?.enabled) {
8736
9995
  if (fmState.provider === "opencode-zen") {
8737
9996
  try {
8738
9997
  const modelsUrl = "https://opencode.ai/zen/v1/models";
@@ -8800,7 +10059,7 @@ function createCodexBridgeMiddleware() {
8800
10059
  return;
8801
10060
  }
8802
10061
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
8803
- const payload = asRecord5(await readJsonBody(req));
10062
+ const payload = asRecord5(await readJsonBody2(req));
8804
10063
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
8805
10064
  const baseBranch = typeof payload?.baseBranch === "string" ? payload.baseBranch.trim() : "";
8806
10065
  if (!rawSourceCwd) {
@@ -8858,6 +10117,12 @@ function createCodexBridgeMiddleware() {
8858
10117
  await ensureRepoHasInitialCommit(gitRoot);
8859
10118
  await runCommand3("git", ["worktree", "add", "--detach", worktreeCwd, startPoint], { cwd: gitRoot });
8860
10119
  }
10120
+ try {
10121
+ await persistWorkspaceRoot(worktreeCwd);
10122
+ } catch (error) {
10123
+ await rollbackCreatedWorktree(gitRoot, worktreeCwd, worktreeParent);
10124
+ throw error;
10125
+ }
8861
10126
  setJson4(res, 200, {
8862
10127
  data: {
8863
10128
  cwd: worktreeCwd,
@@ -8870,6 +10135,75 @@ function createCodexBridgeMiddleware() {
8870
10135
  }
8871
10136
  return;
8872
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
+ }
8873
10207
  if (req.method === "GET" && url.pathname === "/codex-api/worktree/branches") {
8874
10208
  const rawSourceCwd = (url.searchParams.get("sourceCwd") ?? "").trim();
8875
10209
  if (!rawSourceCwd) {
@@ -8956,11 +10290,11 @@ function createCodexBridgeMiddleware() {
8956
10290
  });
8957
10291
  return;
8958
10292
  }
8959
- const currentBranchRaw = await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot });
8960
- const currentBranch = currentBranchRaw.trim() || null;
10293
+ const state = await readGitHeaderState(gitRoot);
10294
+ const currentBranch = state.currentBranch;
8961
10295
  const output = await runCommandCapture2(
8962
10296
  "git",
8963
- ["for-each-ref", "--format=%(committerdate:unix) %(refname)", "refs/heads", "refs/remotes"],
10297
+ ["for-each-ref", "--format=%(committerdate:unix) %(refname) %(objectname)", "refs/heads", "refs/remotes"],
8964
10298
  { cwd: gitRoot }
8965
10299
  );
8966
10300
  const branchActivityByName = /* @__PURE__ */ new Map();
@@ -8970,23 +10304,29 @@ function createCodexBridgeMiddleware() {
8970
10304
  if (!normalized || normalized === "origin/HEAD") continue;
8971
10305
  const parsedTimestamp = Number.parseInt(rawTimestamp.trim(), 10);
8972
10306
  const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : 0;
8973
- const current = branchActivityByName.get(normalized) ?? Number.MIN_SAFE_INTEGER;
8974
- if (timestamp > current) {
8975
- 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 });
8976
10311
  }
8977
10312
  }
8978
10313
  if (currentBranch && !branchActivityByName.has(currentBranch)) {
8979
- branchActivityByName.set(currentBranch, Number.MAX_SAFE_INTEGER);
10314
+ branchActivityByName.set(currentBranch, { timestamp: Number.MAX_SAFE_INTEGER, isRemote: false });
8980
10315
  }
8981
- const options = Array.from(branchActivityByName.entries()).map(([value]) => ({ value, label: value })).sort((a, b) => {
8982
- const aActivity = branchActivityByName.get(a.value) ?? 0;
8983
- 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;
8984
10324
  if (bActivity !== aActivity) return bActivity - aActivity;
8985
10325
  return a.value.localeCompare(b.value);
8986
10326
  });
8987
10327
  setJson4(res, 200, {
8988
10328
  data: {
8989
- currentBranch,
10329
+ ...state,
8990
10330
  options
8991
10331
  }
8992
10332
  });
@@ -8995,8 +10335,47 @@ function createCodexBridgeMiddleware() {
8995
10335
  }
8996
10336
  return;
8997
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
+ }
8998
10377
  if (req.method === "POST" && url.pathname === "/codex-api/git/checkout") {
8999
- const payload = await readJsonBody(req);
10378
+ const payload = await readJsonBody2(req);
9000
10379
  const record = asRecord5(payload);
9001
10380
  if (!record) {
9002
10381
  setJson4(res, 400, { error: "Invalid body: expected object" });
@@ -9025,52 +10404,127 @@ function createCodexBridgeMiddleware() {
9025
10404
  }
9026
10405
  try {
9027
10406
  const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
9028
- try {
9029
- await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
9030
- } catch (checkoutError) {
9031
- const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, targetBranch);
9032
- if (!blockingWorktreePath) {
9033
- throw checkoutError;
9034
- }
9035
- await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
9036
- await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
9037
- }
9038
- const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim() || null;
9039
- setJson4(res, 200, { data: { currentBranch } });
10407
+ await assertNoTrackedGitChanges(gitRoot);
10408
+ await checkoutGitBranchWithWorktreeRecovery(gitRoot, targetBranch);
10409
+ setJson4(res, 200, { data: await readGitHeaderState(gitRoot) });
9040
10410
  } catch (error) {
9041
10411
  setJson4(res, 500, { error: getErrorMessage5(error, "Failed to switch branch") });
9042
10412
  }
9043
10413
  return;
9044
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
+ }
9045
10497
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
9046
- const payload = await readJsonBody(req);
10498
+ const payload = await readJsonBody2(req);
9047
10499
  const record = asRecord5(payload);
9048
10500
  if (!record) {
9049
10501
  setJson4(res, 400, { error: "Invalid body: expected object" });
9050
10502
  return;
9051
10503
  }
9052
- const nextState = {
10504
+ await updateWorkspaceRootsState((existingState) => ({
9053
10505
  order: normalizeStringArray(record.order),
9054
10506
  labels: normalizeStringRecord(record.labels),
9055
- active: normalizeStringArray(record.active)
9056
- };
9057
- await writeWorkspaceRootsState(nextState);
10507
+ active: normalizeStringArray(record.active),
10508
+ projectOrder: Array.isArray(record.projectOrder) ? normalizeStringArray(record.projectOrder) : existingState.projectOrder,
10509
+ remoteProjects: existingState.remoteProjects
10510
+ }));
9058
10511
  setJson4(res, 200, { ok: true });
9059
10512
  return;
9060
10513
  }
9061
10514
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-queue-state") {
9062
- const payload = await readJsonBody(req);
10515
+ const payload = await readJsonBody2(req);
9063
10516
  const record = asRecord5(payload);
9064
10517
  if (!record) {
9065
10518
  setJson4(res, 400, { error: "Invalid body: expected object" });
9066
10519
  return;
9067
10520
  }
9068
10521
  await writeThreadQueueState(normalizeThreadQueueState(record));
10522
+ void backendQueueProcessor.scheduleAllQueuedThreads();
9069
10523
  setJson4(res, 200, { ok: true });
9070
10524
  return;
9071
10525
  }
9072
10526
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
9073
- const payload = asRecord5(await readJsonBody(req));
10527
+ const payload = asRecord5(await readJsonBody2(req));
9074
10528
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
9075
10529
  const createIfMissing = payload?.createIfMissing === true;
9076
10530
  const label = typeof payload?.label === "string" ? payload.label : "";
@@ -9095,23 +10549,12 @@ function createCodexBridgeMiddleware() {
9095
10549
  setJson4(res, 404, { error: "Directory does not exist" });
9096
10550
  return;
9097
10551
  }
9098
- const existingState = await readWorkspaceRootsState();
9099
- const nextOrder = [normalizedPath, ...existingState.order.filter((item) => item !== normalizedPath)];
9100
- const nextActive = [normalizedPath, ...existingState.active.filter((item) => item !== normalizedPath)];
9101
- const nextLabels = { ...existingState.labels };
9102
- if (label.trim().length > 0) {
9103
- nextLabels[normalizedPath] = label.trim();
9104
- }
9105
- await writeWorkspaceRootsState({
9106
- order: nextOrder,
9107
- labels: nextLabels,
9108
- active: nextActive
9109
- });
10552
+ await persistWorkspaceRoot(normalizedPath, label);
9110
10553
  setJson4(res, 200, { data: { path: normalizedPath } });
9111
10554
  return;
9112
10555
  }
9113
10556
  if (req.method === "POST" && url.pathname === "/codex-api/local-directory") {
9114
- const payload = asRecord5(await readJsonBody(req));
10557
+ const payload = asRecord5(await readJsonBody2(req));
9115
10558
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
9116
10559
  if (!rawPath) {
9117
10560
  setJson4(res, 400, { error: "Missing path" });
@@ -9131,7 +10574,7 @@ function createCodexBridgeMiddleware() {
9131
10574
  return;
9132
10575
  }
9133
10576
  if (req.method === "POST" && url.pathname === "/codex-api/projectless-thread-cwd") {
9134
- const payload = asRecord5(await readJsonBody(req));
10577
+ const payload = asRecord5(await readJsonBody2(req));
9135
10578
  const prompt = typeof payload?.prompt === "string" ? payload.prompt : null;
9136
10579
  try {
9137
10580
  const directory = await createProjectlessThreadDirectory(prompt);
@@ -9175,7 +10618,7 @@ function createCodexBridgeMiddleware() {
9175
10618
  return;
9176
10619
  }
9177
10620
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
9178
- const payload = asRecord5(await readJsonBody(req));
10621
+ const payload = asRecord5(await readJsonBody2(req));
9179
10622
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
9180
10623
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
9181
10624
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
@@ -9209,7 +10652,7 @@ function createCodexBridgeMiddleware() {
9209
10652
  return;
9210
10653
  }
9211
10654
  if (req.method === "POST" && url.pathname === "/codex-api/prompts") {
9212
- const payload = asRecord5(await readJsonBody(req));
10655
+ const payload = asRecord5(await readJsonBody2(req));
9213
10656
  const name = typeof payload?.name === "string" ? payload.name.trim() : "";
9214
10657
  const content = typeof payload?.content === "string" ? payload.content : "";
9215
10658
  if (!name || !content.trim()) {
@@ -9260,16 +10703,17 @@ function createCodexBridgeMiddleware() {
9260
10703
  }
9261
10704
  if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
9262
10705
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
10706
+ const automationId = url.searchParams.get("automationId")?.trim() ?? "";
9263
10707
  if (!threadId) {
9264
10708
  setJson4(res, 400, { error: "Missing threadId" });
9265
10709
  return;
9266
10710
  }
9267
- const automation = await readThreadHeartbeatAutomation(threadId);
10711
+ const automation = automationId ? await readThreadHeartbeatAutomation(threadId, automationId) : await readThreadHeartbeatAutomations(threadId);
9268
10712
  setJson4(res, 200, { data: automation });
9269
10713
  return;
9270
10714
  }
9271
10715
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
9272
- const payload = asRecord5(await readJsonBody(req));
10716
+ const payload = asRecord5(await readJsonBody2(req));
9273
10717
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
9274
10718
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
9275
10719
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
@@ -9283,7 +10727,7 @@ function createCodexBridgeMiddleware() {
9283
10727
  return;
9284
10728
  }
9285
10729
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
9286
- const payload = asRecord5(await readJsonBody(req));
10730
+ const payload = asRecord5(await readJsonBody2(req));
9287
10731
  const id = typeof payload?.id === "string" ? payload.id : "";
9288
10732
  const title = typeof payload?.title === "string" ? payload.title : "";
9289
10733
  if (!id) {
@@ -9297,22 +10741,23 @@ function createCodexBridgeMiddleware() {
9297
10741
  return;
9298
10742
  }
9299
10743
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-pins") {
9300
- const payload = asRecord5(await readJsonBody(req));
10744
+ const payload = asRecord5(await readJsonBody2(req));
9301
10745
  const threadIds = normalizePinnedThreadIds(payload?.threadIds);
9302
10746
  await writePinnedThreadIds(threadIds);
9303
10747
  setJson4(res, 200, { ok: true });
9304
10748
  return;
9305
10749
  }
9306
10750
  if (req.method === "PUT" && url.pathname === "/codex-api/preferences/first-launch-plugins-card") {
9307
- const payload = asRecord5(await readJsonBody(req));
10751
+ const payload = asRecord5(await readJsonBody2(req));
9308
10752
  const dismissed = payload?.dismissed === true;
9309
10753
  await writeFirstLaunchPluginsCardDismissed(dismissed);
9310
10754
  setJson4(res, 200, { ok: true });
9311
10755
  return;
9312
10756
  }
9313
10757
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
9314
- const payload = asRecord5(await readJsonBody(req));
10758
+ const payload = asRecord5(await readJsonBody2(req));
9315
10759
  const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
10760
+ const id = typeof payload?.id === "string" ? payload.id.trim() : "";
9316
10761
  const name = typeof payload?.name === "string" ? payload.name.trim() : "";
9317
10762
  const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
9318
10763
  const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
@@ -9321,22 +10766,41 @@ function createCodexBridgeMiddleware() {
9321
10766
  setJson4(res, 400, { error: "threadId, name, prompt, and rrule are required" });
9322
10767
  return;
9323
10768
  }
9324
- const automation = await writeThreadHeartbeatAutomation({ threadId, name, prompt, rrule, status });
10769
+ const automation = await writeThreadHeartbeatAutomation({ threadId, id, name, prompt, rrule, status });
9325
10770
  setJson4(res, 200, { data: automation });
9326
10771
  return;
9327
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
+ }
9328
10791
  if (req.method === "DELETE" && url.pathname === "/codex-api/thread-automation") {
9329
10792
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
10793
+ const automationId = url.searchParams.get("automationId")?.trim() ?? "";
9330
10794
  if (!threadId) {
9331
10795
  setJson4(res, 400, { error: "Missing threadId" });
9332
10796
  return;
9333
10797
  }
9334
- const removed = await deleteThreadHeartbeatAutomation(threadId);
10798
+ const removed = await deleteThreadHeartbeatAutomation(threadId, automationId);
9335
10799
  setJson4(res, 200, { data: { removed } });
9336
10800
  return;
9337
10801
  }
9338
10802
  if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
9339
- const payload = asRecord5(await readJsonBody(req));
10803
+ const payload = asRecord5(await readJsonBody2(req));
9340
10804
  const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
9341
10805
  const rawAllowedUserIds = Array.isArray(payload?.allowedUserIds) ? payload.allowedUserIds : [];
9342
10806
  if (!botToken) {
@@ -9443,7 +10907,7 @@ data: ${JSON.stringify({ ok: true })}
9443
10907
 
9444
10908
  // src/server/authMiddleware.ts
9445
10909
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
9446
- 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";
9447
10911
  import { homedir as homedir6 } from "os";
9448
10912
  import { dirname as dirname3, join as join7 } from "path";
9449
10913
  var TOKEN_COOKIE = "portal_session";
@@ -9525,10 +10989,10 @@ function readPersistedSessions() {
9525
10989
  }
9526
10990
  function persistSessions(validTokens) {
9527
10991
  const sessionStorePath = getSessionStorePath();
9528
- mkdirSync(dirname3(sessionStorePath), { recursive: true });
10992
+ mkdirSync2(dirname3(sessionStorePath), { recursive: true });
9529
10993
  const tokens = Array.from(validTokens.entries()).sort((left, right) => right[1] - left[1]).slice(0, MAX_PERSISTED_TOKENS).map(([value, expiresAt]) => ({ value, expiresAt }));
9530
10994
  const tmpPath = `${sessionStorePath}.tmp`;
9531
- writeFileSync2(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
10995
+ writeFileSync3(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
9532
10996
  `, { encoding: "utf8", mode: 384 });
9533
10997
  renameSync(tmpPath, sessionStorePath);
9534
10998
  }
@@ -10325,7 +11789,7 @@ function hasPromptedCloudflaredInstallPersisted() {
10325
11789
  }
10326
11790
  async function persistCloudflaredInstallPrompted() {
10327
11791
  const codexHome = getCodexHomePath();
10328
- mkdirSync2(codexHome, { recursive: true });
11792
+ mkdirSync3(codexHome, { recursive: true });
10329
11793
  await writeFile6(getCloudflaredPromptMarkerPath(), `${Date.now()}
10330
11794
  `, "utf8");
10331
11795
  }
@@ -10411,7 +11875,7 @@ async function ensureCloudflaredInstalledLinux() {
10411
11875
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
10412
11876
  }
10413
11877
  const userBinDir = join10(homedir7(), ".local", "bin");
10414
- mkdirSync2(userBinDir, { recursive: true });
11878
+ mkdirSync3(userBinDir, { recursive: true });
10415
11879
  const destination = join10(userBinDir, "cloudflared");
10416
11880
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
10417
11881
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
@@ -10511,12 +11975,24 @@ Global npm install requires elevated permissions. Retrying with --prefix ${userP
10511
11975
  }
10512
11976
  function resolvePassword(input) {
10513
11977
  if (input === false) {
10514
- return void 0;
11978
+ return { password: void 0, generated: false };
10515
11979
  }
10516
11980
  if (typeof input === "string") {
10517
- return input;
11981
+ return { password: input, generated: false };
10518
11982
  }
10519
- 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;
10520
11996
  }
10521
11997
  function printTermuxKeepAlive(lines) {
10522
11998
  if (!isTermuxRuntime()) {
@@ -10535,9 +12011,8 @@ function openBrowser(url) {
10535
12011
  });
10536
12012
  child.unref();
10537
12013
  }
10538
- function buildTunnelAutologinUrl(tunnelUrl, password) {
10539
- if (!password) return tunnelUrl;
10540
- return `${tunnelUrl}/password=${encodeURIComponent(password)}`;
12014
+ function buildTunnelAutologinUrl(tunnelUrl, _password) {
12015
+ return tunnelUrl;
10541
12016
  }
10542
12017
  function parseCloudflaredUrl(chunk) {
10543
12018
  const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
@@ -10732,7 +12207,9 @@ async function startServer(options) {
10732
12207
  console.log("\nCodex is not logged in. You can log in later via settings or run `codexui login`.\n");
10733
12208
  }
10734
12209
  const requestedPort = parseInt(options.port, 10);
10735
- 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;
10736
12213
  const { app, dispose, attachWebSocket } = createServer({ password });
10737
12214
  const server = createServer2(app);
10738
12215
  attachWebSocket(server);
@@ -10774,8 +12251,9 @@ async function startServer(options) {
10774
12251
  if (port !== requestedPort) {
10775
12252
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
10776
12253
  }
10777
- if (password) {
10778
- 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.");
10779
12257
  }
10780
12258
  const tunnelQrUrl = tunnelUrl ? buildTunnelAutologinUrl(tunnelUrl, password) : null;
10781
12259
  if (tunnelUrl) {