codexui-android 0.1.91 → 0.1.93

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.
Files changed (37) hide show
  1. package/README.md +10 -1
  2. package/dist/assets/{ReviewPane-gbhCGpfh.js → ReviewPane-D1nOf7-W.js} +1 -2
  3. package/dist/assets/{ReviewPane-CyugWwTM.css → ReviewPane-DuPX5OZA.css} +1 -1
  4. package/dist/assets/{SkillsHub-xXcF9J80.js → SkillsHub-BpqR2Yu5.js} +2 -3
  5. package/dist/assets/{SkillsHub-Bg1Le103.css → SkillsHub-CTnWejwn.css} +1 -1
  6. package/dist/assets/ThreadConversation-3WaRKicR.css +1 -0
  7. package/dist/assets/ThreadConversation-DgJc2aJG.js +39 -0
  8. package/dist/assets/ThreadTerminalPanel-CGTJQ1BI.css +32 -0
  9. package/dist/assets/ThreadTerminalPanel-Dy-oA46U.js +38 -0
  10. package/dist/assets/common-BeuopZEI.js +0 -1
  11. package/dist/assets/index-Dj8HigAf.css +1 -0
  12. package/dist/assets/index-HcEz2bUL.js +64 -0
  13. package/dist/assets/{index.esm-DtVW_dfU.js → index.esm-Bi-9KxvS.js} +2 -3
  14. package/dist/assets/{index.esm-mbv_PYjX.js → index.esm-DECIu6Fp.js} +1 -2
  15. package/dist/assets/{index.esm-BilMXo9u.js → index.esm-DPq88-QA.js} +2 -3
  16. package/dist/index.html +2 -2
  17. package/dist-cli/index.js +2372 -1058
  18. package/dist-cli/index.js.map +1 -1
  19. package/package.json +17 -15
  20. package/scripts/dev.cjs +58 -0
  21. package/scripts/fix-pty-native-build.cjs +62 -0
  22. package/dist/assets/ReviewPane-gbhCGpfh.js.map +0 -1
  23. package/dist/assets/SkillsHub-xXcF9J80.js.map +0 -1
  24. package/dist/assets/ThreadConversation-BsN7bN3q.css +0 -1
  25. package/dist/assets/ThreadConversation-Dr0u8WbA.js +0 -40
  26. package/dist/assets/ThreadConversation-Dr0u8WbA.js.map +0 -1
  27. package/dist/assets/common-BeuopZEI.js.map +0 -1
  28. package/dist/assets/index-B81KnkV8.js +0 -92
  29. package/dist/assets/index-B81KnkV8.js.map +0 -1
  30. package/dist/assets/index-C_knKlTI.css +0 -1
  31. package/dist/assets/index.esm-BilMXo9u.js.map +0 -1
  32. package/dist/assets/index.esm-DtVW_dfU.js.map +0 -1
  33. package/dist/assets/index.esm-mbv_PYjX.js.map +0 -1
  34. package/dist-cli/chunk-PUR7OUAG.js +0 -127
  35. package/dist-cli/chunk-PUR7OUAG.js.map +0 -1
  36. package/dist-cli/instrument.js +0 -8
  37. package/dist-cli/instrument.js.map +0 -1
package/dist-cli/index.js CHANGED
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-PUR7OUAG.js";
3
2
 
4
3
  // src/cli/index.ts
5
4
  import { createServer as createServer2 } from "http";
6
- import { chmodSync, createWriteStream, existsSync as existsSync4, mkdirSync } from "fs";
5
+ import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync5, mkdirSync } from "fs";
7
6
  import { readFile as readFile5, stat as stat7, writeFile as writeFile6 } from "fs/promises";
8
- import { homedir as homedir5, networkInterfaces } from "os";
9
- import { isAbsolute as isAbsolute4, join as join8, resolve as resolve3 } from "path";
7
+ import { homedir as homedir6, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute4, join as join9, resolve as resolve3 } from "path";
10
9
  import { spawn as spawn5 } from "child_process";
11
10
  import { createInterface as createInterface2 } from "readline/promises";
12
11
  import { fileURLToPath as fileURLToPath2 } from "url";
13
- import { dirname as dirname4 } from "path";
12
+ import { dirname as dirname5 } from "path";
14
13
  import { get as httpsGet } from "https";
15
14
  import { Command } from "commander";
16
15
  import qrcode from "qrcode-terminal";
@@ -217,27 +216,25 @@ function parseApprovalPolicy(value) {
217
216
 
218
217
  // src/server/httpServer.ts
219
218
  import { fileURLToPath } from "url";
220
- import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute3, join as join7 } from "path";
221
- import { existsSync as existsSync3 } from "fs";
219
+ import { dirname as dirname4, extname as extname3, isAbsolute as isAbsolute3, join as join8 } from "path";
220
+ import { existsSync as existsSync4 } from "fs";
222
221
  import { writeFile as writeFile5, stat as stat6 } from "fs/promises";
223
222
  import express from "express";
224
223
 
225
224
  // src/server/codexAppServerBridge.ts
226
- import * as Sentry4 from "@sentry/node";
227
225
  import { spawn as spawn4 } from "child_process";
228
226
  import { createHash as createHash2, randomBytes } from "crypto";
229
- import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm4, mkdir as mkdir4, stat as stat4 } from "fs/promises";
230
- import { createReadStream, readFileSync } from "fs";
231
- import { request as httpRequest } from "http";
232
- import { request as httpsRequest } from "https";
233
- import { homedir as homedir4 } from "os";
227
+ import { mkdtemp as mkdtemp3, readFile as readFile3, readdir as readdir2, rm as rm4, mkdir as mkdir4, stat as stat4 } from "fs/promises";
228
+ import { createReadStream, readFileSync as readFileSync2 } from "fs";
229
+ import { request as httpRequest2 } from "http";
230
+ import { request as httpsRequest2 } from "https";
231
+ import { homedir as homedir5 } from "os";
234
232
  import { tmpdir as tmpdir4 } from "os";
235
- import { basename as basename3, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "path";
233
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "path";
236
234
  import { createInterface } from "readline";
237
235
  import { writeFile as writeFile4 } from "fs/promises";
238
236
 
239
237
  // src/server/accountRoutes.ts
240
- import * as Sentry from "@sentry/node";
241
238
  import { spawn } from "child_process";
242
239
  import { createHash } from "crypto";
243
240
  import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from "fs/promises";
@@ -480,22 +477,6 @@ async function validateSwitchedAccount(appServer) {
480
477
  quotaSnapshot: pickCodexRateLimitSnapshot(quotaPayload)
481
478
  };
482
479
  }
483
- async function validateSwitchedAccountWithTimeout(appServer) {
484
- let timeoutHandle = null;
485
- try {
486
- return await Promise.race([
487
- validateSwitchedAccount(appServer),
488
- new Promise((_, reject) => {
489
- timeoutHandle = setTimeout(() => {
490
- reject(new Error(`Account switch validation timed out after ${ACCOUNT_INSPECTION_TIMEOUT_MS}ms`));
491
- }, ACCOUNT_INSPECTION_TIMEOUT_MS);
492
- timeoutHandle.unref?.();
493
- })
494
- ]);
495
- } finally {
496
- if (timeoutHandle) clearTimeout(timeoutHandle);
497
- }
498
- }
499
480
  async function restoreActiveAuth(raw) {
500
481
  const path = getActiveAuthPath();
501
482
  if (raw === null) {
@@ -786,327 +767,319 @@ async function importAccountFromAuthPath(path) {
786
767
  }
787
768
  async function handleAccountRoutes(req, res, url, context) {
788
769
  const { appServer } = context;
789
- try {
790
- if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
791
- const state = await scheduleAccountsBackgroundRefresh();
792
- setJson(res, 200, {
793
- data: {
794
- activeAccountId: state.activeAccountId,
795
- accounts: sortAccounts(state.accounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
796
- }
797
- });
798
- return true;
799
- }
800
- if (req.method === "GET" && url.pathname === "/codex-api/accounts/active") {
801
- const state = await readStoredAccountsState();
802
- const active = state.activeAccountId ? state.accounts.find((entry) => entry.accountId === state.activeAccountId) ?? null : null;
803
- setJson(res, 200, {
804
- data: active ? toPublicAccountEntry(active, state.activeAccountId) : null
805
- });
806
- return true;
807
- }
808
- if (req.method === "POST" && url.pathname === "/codex-api/accounts/refresh") {
770
+ if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
771
+ const state = await scheduleAccountsBackgroundRefresh();
772
+ setJson(res, 200, {
773
+ data: {
774
+ activeAccountId: state.activeAccountId,
775
+ accounts: sortAccounts(state.accounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
776
+ }
777
+ });
778
+ return true;
779
+ }
780
+ if (req.method === "GET" && url.pathname === "/codex-api/accounts/active") {
781
+ const state = await readStoredAccountsState();
782
+ const active = state.activeAccountId ? state.accounts.find((entry) => entry.accountId === state.activeAccountId) ?? null : null;
783
+ setJson(res, 200, {
784
+ data: active ? toPublicAccountEntry(active, state.activeAccountId) : null
785
+ });
786
+ return true;
787
+ }
788
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/refresh") {
789
+ try {
790
+ const imported = await importAccountFromAuthPath(getActiveAuthPath());
809
791
  try {
810
- const imported = await importAccountFromAuthPath(getActiveAuthPath());
811
- try {
812
- appServer.dispose();
813
- const inspection = await validateSwitchedAccountWithTimeout(appServer);
814
- const state = await readStoredAccountsState();
815
- const importedAccountId = imported.importedAccountId;
816
- const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
817
- if (!target) {
818
- throw new Error("account_not_found");
819
- }
820
- const nextEntry = {
821
- ...target,
822
- email: inspection.metadata.email ?? target.email,
823
- planType: inspection.metadata.planType ?? target.planType,
824
- lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
825
- quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
826
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
827
- quotaStatus: "ready",
828
- quotaError: null,
829
- unavailableReason: null
830
- };
831
- const nextState = withUpsertedAccount({
832
- activeAccountId: importedAccountId,
833
- accounts: state.accounts
834
- }, nextEntry);
835
- await writeStoredAccountsState({
836
- activeAccountId: importedAccountId,
837
- accounts: nextState.accounts
838
- });
839
- const backgroundState = await scheduleAccountsBackgroundRefresh({
840
- force: true,
841
- prioritizeAccountId: importedAccountId,
842
- accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
843
- });
844
- setJson(res, 200, {
845
- data: {
846
- activeAccountId: importedAccountId,
847
- importedAccountId,
848
- accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
849
- }
850
- });
851
- } catch (error) {
852
- setJson(res, 502, {
853
- error: "account_refresh_failed",
854
- message: getErrorMessage(error, "Failed to refresh account")
855
- });
792
+ appServer.dispose();
793
+ const inspection = await validateSwitchedAccount(appServer);
794
+ const state = await readStoredAccountsState();
795
+ const importedAccountId = imported.importedAccountId;
796
+ const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
797
+ if (!target) {
798
+ throw new Error("account_not_found");
856
799
  }
800
+ const nextEntry = {
801
+ ...target,
802
+ email: inspection.metadata.email ?? target.email,
803
+ planType: inspection.metadata.planType ?? target.planType,
804
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
805
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
806
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
807
+ quotaStatus: "ready",
808
+ quotaError: null,
809
+ unavailableReason: null
810
+ };
811
+ const nextState = withUpsertedAccount({
812
+ activeAccountId: importedAccountId,
813
+ accounts: state.accounts
814
+ }, nextEntry);
815
+ await writeStoredAccountsState({
816
+ activeAccountId: importedAccountId,
817
+ accounts: nextState.accounts
818
+ });
819
+ const backgroundState = await scheduleAccountsBackgroundRefresh({
820
+ force: true,
821
+ prioritizeAccountId: importedAccountId,
822
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
823
+ });
824
+ setJson(res, 200, {
825
+ data: {
826
+ activeAccountId: importedAccountId,
827
+ importedAccountId,
828
+ accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
829
+ }
830
+ });
857
831
  } catch (error) {
858
- const message = getErrorMessage(error, "Failed to refresh account");
859
- if (message === "missing_account_id") {
860
- setJson(res, 400, { error: "missing_account_id", message: "Current auth.json is missing tokens.account_id." });
861
- return true;
862
- }
863
- setJson(res, 400, { error: "invalid_auth_json", message: "Failed to parse the current auth.json file." });
832
+ setJson(res, 502, {
833
+ error: "account_refresh_failed",
834
+ message: getErrorMessage(error, "Failed to refresh account")
835
+ });
864
836
  }
865
- return true;
837
+ } catch (error) {
838
+ const message = getErrorMessage(error, "Failed to refresh account");
839
+ if (message === "missing_account_id") {
840
+ setJson(res, 400, { error: "missing_account_id", message: "Current auth.json is missing tokens.account_id." });
841
+ return true;
842
+ }
843
+ setJson(res, 400, { error: "invalid_auth_json", message: "Failed to parse the current auth.json file." });
866
844
  }
867
- if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
845
+ return true;
846
+ }
847
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
848
+ try {
849
+ if (appServer.listPendingServerRequests().length > 0) {
850
+ setJson(res, 409, {
851
+ error: "account_switch_blocked",
852
+ message: "Finish pending approval requests before switching accounts."
853
+ });
854
+ return true;
855
+ }
856
+ const rawBody = await new Promise((resolve4, reject) => {
857
+ let body = "";
858
+ req.setEncoding("utf8");
859
+ req.on("data", (chunk) => {
860
+ body += chunk;
861
+ });
862
+ req.on("end", () => resolve4(body));
863
+ req.on("error", reject);
864
+ });
865
+ const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
866
+ const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
867
+ if (!accountId) {
868
+ setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
869
+ return true;
870
+ }
871
+ const state = await readStoredAccountsState();
872
+ const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
873
+ if (!target) {
874
+ setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
875
+ return true;
876
+ }
877
+ const snapshotPath = getSnapshotPath(target.storageId);
878
+ if (!await fileExists(snapshotPath)) {
879
+ setJson(res, 404, { error: "account_not_found", message: "The requested account snapshot is missing." });
880
+ return true;
881
+ }
882
+ let previousRaw = null;
868
883
  try {
869
- if (appServer.listPendingServerRequests().length > 0) {
870
- setJson(res, 409, {
871
- error: "account_switch_blocked",
872
- message: "Finish pending approval requests before switching accounts."
873
- });
874
- return true;
875
- }
876
- const rawBody = await new Promise((resolve4, reject) => {
877
- let body = "";
878
- req.setEncoding("utf8");
879
- req.on("data", (chunk) => {
880
- body += chunk;
881
- });
882
- req.on("end", () => resolve4(body));
883
- req.on("error", reject);
884
+ previousRaw = await readFile(getActiveAuthPath(), "utf8");
885
+ } catch {
886
+ previousRaw = null;
887
+ }
888
+ const targetRaw = await readFile(snapshotPath, "utf8");
889
+ await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
890
+ try {
891
+ appServer.dispose();
892
+ const inspection = await validateSwitchedAccount(appServer);
893
+ const nextEntry = {
894
+ ...target,
895
+ email: inspection.metadata.email ?? target.email,
896
+ planType: inspection.metadata.planType ?? target.planType,
897
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
898
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
899
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
900
+ quotaStatus: "ready",
901
+ quotaError: null,
902
+ unavailableReason: null
903
+ };
904
+ const nextState = withUpsertedAccount({
905
+ activeAccountId: accountId,
906
+ accounts: state.accounts
907
+ }, nextEntry);
908
+ await writeStoredAccountsState({
909
+ activeAccountId: accountId,
910
+ accounts: nextState.accounts
884
911
  });
885
- const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
886
- const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
887
- if (!accountId) {
888
- setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
889
- return true;
890
- }
891
- const state = await readStoredAccountsState();
892
- const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
893
- if (!target) {
894
- setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
895
- return true;
896
- }
897
- const snapshotPath = getSnapshotPath(target.storageId);
898
- if (!await fileExists(snapshotPath)) {
899
- setJson(res, 404, { error: "account_not_found", message: "The requested account snapshot is missing." });
900
- return true;
901
- }
902
- let previousRaw = null;
903
- try {
904
- previousRaw = await readFile(getActiveAuthPath(), "utf8");
905
- } catch {
906
- previousRaw = null;
907
- }
908
- const targetRaw = await readFile(snapshotPath, "utf8");
909
- await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
910
- try {
911
- appServer.dispose();
912
- const inspection = await validateSwitchedAccountWithTimeout(appServer);
913
- const nextEntry = {
914
- ...target,
915
- email: inspection.metadata.email ?? target.email,
916
- planType: inspection.metadata.planType ?? target.planType,
917
- lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
918
- quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
919
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
920
- quotaStatus: "ready",
921
- quotaError: null,
922
- unavailableReason: null
923
- };
924
- const nextState = withUpsertedAccount({
925
- activeAccountId: accountId,
926
- accounts: state.accounts
927
- }, nextEntry);
928
- await writeStoredAccountsState({
912
+ void scheduleAccountsBackgroundRefresh({
913
+ force: true,
914
+ prioritizeAccountId: accountId,
915
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== accountId).map((entry) => entry.accountId)
916
+ });
917
+ setJson(res, 200, {
918
+ ok: true,
919
+ data: {
929
920
  activeAccountId: accountId,
930
- accounts: nextState.accounts
931
- });
932
- void scheduleAccountsBackgroundRefresh({
933
- force: true,
934
- prioritizeAccountId: accountId,
935
- accountIds: nextState.accounts.filter((entry) => entry.accountId !== accountId).map((entry) => entry.accountId)
936
- });
937
- setJson(res, 200, {
938
- ok: true,
939
- data: {
940
- activeAccountId: accountId,
941
- account: toPublicAccountEntry(nextEntry, accountId)
942
- }
943
- });
944
- } catch (error) {
945
- await restoreActiveAuth(previousRaw);
946
- appServer.dispose();
947
- await replaceStoredAccount({
948
- ...target,
949
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
950
- quotaStatus: "error",
951
- quotaError: getErrorMessage(error, "Failed to switch account"),
952
- unavailableReason: detectAccountUnavailableReason(error)
953
- }, state.activeAccountId);
954
- setJson(res, 502, {
955
- error: "account_switch_failed",
956
- message: getErrorMessage(error, "Failed to switch account")
957
- });
958
- }
921
+ account: toPublicAccountEntry(nextEntry, accountId)
922
+ }
923
+ });
959
924
  } catch (error) {
960
- setJson(res, 400, {
961
- error: "invalid_auth_json",
925
+ await restoreActiveAuth(previousRaw);
926
+ appServer.dispose();
927
+ await replaceStoredAccount({
928
+ ...target,
929
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
930
+ quotaStatus: "error",
931
+ quotaError: getErrorMessage(error, "Failed to switch account"),
932
+ unavailableReason: detectAccountUnavailableReason(error)
933
+ }, state.activeAccountId);
934
+ setJson(res, 502, {
935
+ error: "account_switch_failed",
962
936
  message: getErrorMessage(error, "Failed to switch account")
963
937
  });
964
938
  }
965
- return true;
939
+ } catch (error) {
940
+ setJson(res, 400, {
941
+ error: "invalid_auth_json",
942
+ message: getErrorMessage(error, "Failed to switch account")
943
+ });
966
944
  }
967
- if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
968
- try {
969
- const rawBody = await new Promise((resolve4, reject) => {
970
- let body = "";
971
- req.setEncoding("utf8");
972
- req.on("data", (chunk) => {
973
- body += chunk;
974
- });
975
- req.on("end", () => resolve4(body));
976
- req.on("error", reject);
945
+ return true;
946
+ }
947
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
948
+ try {
949
+ const rawBody = await new Promise((resolve4, reject) => {
950
+ let body = "";
951
+ req.setEncoding("utf8");
952
+ req.on("data", (chunk) => {
953
+ body += chunk;
977
954
  });
978
- const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
979
- const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
980
- if (!accountId) {
981
- setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
982
- return true;
983
- }
984
- const state = await readStoredAccountsState();
985
- const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
986
- if (!target) {
987
- setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
988
- return true;
989
- }
990
- const remainingAccounts = state.accounts.filter((entry) => entry.accountId !== accountId);
991
- if (state.activeAccountId !== accountId) {
992
- await removeSnapshot(target.storageId);
993
- await writeStoredAccountsState({
955
+ req.on("end", () => resolve4(body));
956
+ req.on("error", reject);
957
+ });
958
+ const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
959
+ const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
960
+ if (!accountId) {
961
+ setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
962
+ return true;
963
+ }
964
+ const state = await readStoredAccountsState();
965
+ const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
966
+ if (!target) {
967
+ setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
968
+ return true;
969
+ }
970
+ const remainingAccounts = state.accounts.filter((entry) => entry.accountId !== accountId);
971
+ if (state.activeAccountId !== accountId) {
972
+ await removeSnapshot(target.storageId);
973
+ await writeStoredAccountsState({
974
+ activeAccountId: state.activeAccountId,
975
+ accounts: remainingAccounts
976
+ });
977
+ setJson(res, 200, {
978
+ ok: true,
979
+ data: {
994
980
  activeAccountId: state.activeAccountId,
995
- accounts: remainingAccounts
996
- });
997
- setJson(res, 200, {
998
- ok: true,
999
- data: {
1000
- activeAccountId: state.activeAccountId,
1001
- accounts: sortAccounts(remainingAccounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
1002
- }
1003
- });
1004
- return true;
1005
- }
1006
- if (appServer.listPendingServerRequests().length > 0) {
1007
- setJson(res, 409, {
1008
- error: "account_remove_blocked",
1009
- message: "Finish pending approval requests before removing the active account."
1010
- });
1011
- return true;
1012
- }
1013
- let previousRaw = null;
1014
- try {
1015
- previousRaw = await readFile(getActiveAuthPath(), "utf8");
1016
- } catch {
1017
- previousRaw = null;
1018
- }
1019
- const replacement = await pickReplacementActiveAccount(remainingAccounts);
1020
- if (!replacement) {
1021
- await restoreActiveAuth(null);
1022
- appServer.dispose();
1023
- await removeSnapshot(target.storageId);
1024
- await writeStoredAccountsState({
981
+ accounts: sortAccounts(remainingAccounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
982
+ }
983
+ });
984
+ return true;
985
+ }
986
+ if (appServer.listPendingServerRequests().length > 0) {
987
+ setJson(res, 409, {
988
+ error: "account_remove_blocked",
989
+ message: "Finish pending approval requests before removing the active account."
990
+ });
991
+ return true;
992
+ }
993
+ let previousRaw = null;
994
+ try {
995
+ previousRaw = await readFile(getActiveAuthPath(), "utf8");
996
+ } catch {
997
+ previousRaw = null;
998
+ }
999
+ const replacement = await pickReplacementActiveAccount(remainingAccounts);
1000
+ if (!replacement) {
1001
+ await restoreActiveAuth(null);
1002
+ appServer.dispose();
1003
+ await removeSnapshot(target.storageId);
1004
+ await writeStoredAccountsState({
1005
+ activeAccountId: null,
1006
+ accounts: remainingAccounts
1007
+ });
1008
+ void scheduleAccountsBackgroundRefresh({
1009
+ force: true,
1010
+ accountIds: remainingAccounts.map((entry) => entry.accountId)
1011
+ });
1012
+ setJson(res, 200, {
1013
+ ok: true,
1014
+ data: {
1025
1015
  activeAccountId: null,
1026
- accounts: remainingAccounts
1027
- });
1028
- void scheduleAccountsBackgroundRefresh({
1029
- force: true,
1030
- accountIds: remainingAccounts.map((entry) => entry.accountId)
1031
- });
1032
- setJson(res, 200, {
1033
- ok: true,
1034
- data: {
1035
- activeAccountId: null,
1036
- accounts: sortAccounts(remainingAccounts, null).map((entry) => toPublicAccountEntry(entry, null))
1037
- }
1038
- });
1039
- return true;
1040
- }
1041
- const replacementSnapshotPath = getSnapshotPath(replacement.storageId);
1042
- if (!await fileExists(replacementSnapshotPath)) {
1043
- setJson(res, 404, {
1044
- error: "account_not_found",
1045
- message: "The replacement account snapshot is missing."
1046
- });
1047
- return true;
1048
- }
1049
- const replacementRaw = await readFile(replacementSnapshotPath, "utf8");
1050
- await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
1051
- try {
1052
- appServer.dispose();
1053
- const inspection = await validateSwitchedAccountWithTimeout(appServer);
1054
- const activatedReplacement = {
1055
- ...replacement,
1056
- email: inspection.metadata.email ?? replacement.email,
1057
- planType: inspection.metadata.planType ?? replacement.planType,
1058
- lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1059
- quotaSnapshot: inspection.quotaSnapshot ?? replacement.quotaSnapshot,
1060
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1061
- quotaStatus: "ready",
1062
- quotaError: null,
1063
- unavailableReason: null
1064
- };
1065
- const nextAccounts = remainingAccounts.map((entry) => entry.accountId === activatedReplacement.accountId ? activatedReplacement : entry);
1066
- await removeSnapshot(target.storageId);
1067
- await writeStoredAccountsState({
1016
+ accounts: sortAccounts(remainingAccounts, null).map((entry) => toPublicAccountEntry(entry, null))
1017
+ }
1018
+ });
1019
+ return true;
1020
+ }
1021
+ const replacementSnapshotPath = getSnapshotPath(replacement.storageId);
1022
+ if (!await fileExists(replacementSnapshotPath)) {
1023
+ setJson(res, 404, {
1024
+ error: "account_not_found",
1025
+ message: "The replacement account snapshot is missing."
1026
+ });
1027
+ return true;
1028
+ }
1029
+ const replacementRaw = await readFile(replacementSnapshotPath, "utf8");
1030
+ await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
1031
+ try {
1032
+ appServer.dispose();
1033
+ const inspection = await validateSwitchedAccount(appServer);
1034
+ const activatedReplacement = {
1035
+ ...replacement,
1036
+ email: inspection.metadata.email ?? replacement.email,
1037
+ planType: inspection.metadata.planType ?? replacement.planType,
1038
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1039
+ quotaSnapshot: inspection.quotaSnapshot ?? replacement.quotaSnapshot,
1040
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1041
+ quotaStatus: "ready",
1042
+ quotaError: null,
1043
+ unavailableReason: null
1044
+ };
1045
+ const nextAccounts = remainingAccounts.map((entry) => entry.accountId === activatedReplacement.accountId ? activatedReplacement : entry);
1046
+ await removeSnapshot(target.storageId);
1047
+ await writeStoredAccountsState({
1048
+ activeAccountId: activatedReplacement.accountId,
1049
+ accounts: nextAccounts
1050
+ });
1051
+ void scheduleAccountsBackgroundRefresh({
1052
+ force: true,
1053
+ prioritizeAccountId: activatedReplacement.accountId,
1054
+ accountIds: nextAccounts.filter((entry) => entry.accountId !== activatedReplacement.accountId).map((entry) => entry.accountId)
1055
+ });
1056
+ setJson(res, 200, {
1057
+ ok: true,
1058
+ data: {
1068
1059
  activeAccountId: activatedReplacement.accountId,
1069
- accounts: nextAccounts
1070
- });
1071
- void scheduleAccountsBackgroundRefresh({
1072
- force: true,
1073
- prioritizeAccountId: activatedReplacement.accountId,
1074
- accountIds: nextAccounts.filter((entry) => entry.accountId !== activatedReplacement.accountId).map((entry) => entry.accountId)
1075
- });
1076
- setJson(res, 200, {
1077
- ok: true,
1078
- data: {
1079
- activeAccountId: activatedReplacement.accountId,
1080
- accounts: sortAccounts(nextAccounts, activatedReplacement.accountId).map((entry) => toPublicAccountEntry(entry, activatedReplacement.accountId))
1081
- }
1082
- });
1083
- } catch (error) {
1084
- await restoreActiveAuth(previousRaw);
1085
- appServer.dispose();
1086
- await replaceStoredAccount({
1087
- ...replacement,
1088
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1089
- quotaStatus: "error",
1090
- quotaError: getErrorMessage(error, "Failed to switch account"),
1091
- unavailableReason: detectAccountUnavailableReason(error)
1092
- }, state.activeAccountId);
1093
- setJson(res, 502, {
1094
- error: "account_remove_failed",
1095
- message: getErrorMessage(error, "Failed to remove account")
1096
- });
1097
- }
1060
+ accounts: sortAccounts(nextAccounts, activatedReplacement.accountId).map((entry) => toPublicAccountEntry(entry, activatedReplacement.accountId))
1061
+ }
1062
+ });
1098
1063
  } catch (error) {
1099
- setJson(res, 400, {
1100
- error: "invalid_auth_json",
1064
+ await restoreActiveAuth(previousRaw);
1065
+ appServer.dispose();
1066
+ await replaceStoredAccount({
1067
+ ...replacement,
1068
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1069
+ quotaStatus: "error",
1070
+ quotaError: getErrorMessage(error, "Failed to switch account"),
1071
+ unavailableReason: detectAccountUnavailableReason(error)
1072
+ }, state.activeAccountId);
1073
+ setJson(res, 502, {
1074
+ error: "account_remove_failed",
1101
1075
  message: getErrorMessage(error, "Failed to remove account")
1102
1076
  });
1103
1077
  }
1104
- return true;
1105
- }
1106
- } catch (error) {
1107
- Sentry.captureException(error);
1108
- if (!res.headersSent) {
1109
- setJson(res, 500, { error: "Internal account error" });
1078
+ } catch (error) {
1079
+ setJson(res, 400, {
1080
+ error: "invalid_auth_json",
1081
+ message: getErrorMessage(error, "Failed to remove account")
1082
+ });
1110
1083
  }
1111
1084
  return true;
1112
1085
  }
@@ -1114,7 +1087,6 @@ async function handleAccountRoutes(req, res, url, context) {
1114
1087
  }
1115
1088
 
1116
1089
  // src/server/reviewGit.ts
1117
- import * as Sentry2 from "@sentry/node";
1118
1090
  import { spawn as spawn2 } from "child_process";
1119
1091
  import { mkdir as mkdir2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1120
1092
  import { tmpdir as tmpdir2 } from "os";
@@ -1726,55 +1698,47 @@ async function applyReviewAction(payload) {
1726
1698
  return await buildReviewSnapshot(normalizedCwd, scope, workspaceView);
1727
1699
  }
1728
1700
  async function handleReviewRoutes(req, res, url, context) {
1729
- try {
1730
- if (req.method === "GET" && url.pathname === "/codex-api/review/snapshot") {
1731
- const cwd = url.searchParams.get("cwd")?.trim() ?? "";
1732
- const scope = url.searchParams.get("scope") === "baseBranch" ? "baseBranch" : "workspace";
1733
- const workspaceView = url.searchParams.get("workspaceView") === "staged" ? "staged" : "unstaged";
1734
- const baseBranch = url.searchParams.get("baseBranch")?.trim() ?? "";
1735
- if (!cwd) {
1736
- setJson2(res, 400, { error: "Missing cwd" });
1737
- return true;
1738
- }
1739
- try {
1740
- setJson2(res, 200, {
1741
- data: await buildReviewSnapshot(cwd, scope, workspaceView, baseBranch)
1742
- });
1743
- } catch (error) {
1744
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to load review snapshot") });
1745
- }
1701
+ if (req.method === "GET" && url.pathname === "/codex-api/review/snapshot") {
1702
+ const cwd = url.searchParams.get("cwd")?.trim() ?? "";
1703
+ const scope = url.searchParams.get("scope") === "baseBranch" ? "baseBranch" : "workspace";
1704
+ const workspaceView = url.searchParams.get("workspaceView") === "staged" ? "staged" : "unstaged";
1705
+ const baseBranch = url.searchParams.get("baseBranch")?.trim() ?? "";
1706
+ if (!cwd) {
1707
+ setJson2(res, 400, { error: "Missing cwd" });
1746
1708
  return true;
1747
1709
  }
1748
- if (req.method === "POST" && url.pathname === "/codex-api/review/action") {
1749
- try {
1750
- const payload = await context.readJsonBody(req);
1751
- setJson2(res, 200, {
1752
- data: await applyReviewAction(payload)
1753
- });
1754
- } catch (error) {
1755
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to apply review action") });
1756
- }
1757
- return true;
1710
+ try {
1711
+ setJson2(res, 200, {
1712
+ data: await buildReviewSnapshot(cwd, scope, workspaceView, baseBranch)
1713
+ });
1714
+ } catch (error) {
1715
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to load review snapshot") });
1758
1716
  }
1759
- if (req.method === "POST" && url.pathname === "/codex-api/review/git/init") {
1760
- const payload = asRecord2(await context.readJsonBody(req));
1761
- const cwd = readString2(payload?.cwd);
1762
- if (!cwd) {
1763
- setJson2(res, 400, { error: "Missing cwd" });
1764
- return true;
1765
- }
1766
- try {
1767
- await initializeGitRepository(cwd);
1768
- setJson2(res, 200, { ok: true });
1769
- } catch (error) {
1770
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to initialize Git") });
1771
- }
1717
+ return true;
1718
+ }
1719
+ if (req.method === "POST" && url.pathname === "/codex-api/review/action") {
1720
+ try {
1721
+ const payload = await context.readJsonBody(req);
1722
+ setJson2(res, 200, {
1723
+ data: await applyReviewAction(payload)
1724
+ });
1725
+ } catch (error) {
1726
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to apply review action") });
1727
+ }
1728
+ return true;
1729
+ }
1730
+ if (req.method === "POST" && url.pathname === "/codex-api/review/git/init") {
1731
+ const payload = asRecord2(await context.readJsonBody(req));
1732
+ const cwd = readString2(payload?.cwd);
1733
+ if (!cwd) {
1734
+ setJson2(res, 400, { error: "Missing cwd" });
1772
1735
  return true;
1773
1736
  }
1774
- } catch (error) {
1775
- Sentry2.captureException(error);
1776
- if (!res.headersSent) {
1777
- setJson2(res, 500, { error: "Internal review error" });
1737
+ try {
1738
+ await initializeGitRepository(cwd);
1739
+ setJson2(res, 200, { ok: true });
1740
+ } catch (error) {
1741
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to initialize Git") });
1778
1742
  }
1779
1743
  return true;
1780
1744
  }
@@ -1782,7 +1746,6 @@ async function handleReviewRoutes(req, res, url, context) {
1782
1746
  }
1783
1747
 
1784
1748
  // src/server/skillsRoutes.ts
1785
- import * as Sentry3 from "@sentry/node";
1786
1749
  import { spawn as spawn3 } from "child_process";
1787
1750
  import { mkdtemp as mkdtemp2, readFile as readFile2, readdir, rm as rm3, mkdir as mkdir3, stat as stat3, lstat, readlink, symlink } from "fs/promises";
1788
1751
  import { existsSync as existsSync2 } from "fs";
@@ -2773,365 +2736,357 @@ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
2773
2736
  }
2774
2737
  async function handleSkillsRoutes(req, res, url, context) {
2775
2738
  const { appServer, readJsonBody: readJsonBody2 } = context;
2776
- try {
2777
- if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
2739
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
2740
+ try {
2741
+ const q = url.searchParams.get("q") || "";
2742
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
2743
+ const sort = url.searchParams.get("sort") || "date";
2744
+ const allEntries = await fetchSkillsTree();
2745
+ const installedMap = await scanInstalledSkillsFromDisk();
2778
2746
  try {
2779
- const q = url.searchParams.get("q") || "";
2780
- const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
2781
- const sort = url.searchParams.get("sort") || "date";
2782
- const allEntries = await fetchSkillsTree();
2783
- const installedMap = await scanInstalledSkillsFromDisk();
2784
- try {
2785
- const result = await appServer.rpc("skills/list", {});
2786
- for (const entry of result.data ?? []) {
2787
- for (const skill of entry.skills ?? []) {
2788
- if (skill.name) {
2789
- installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2790
- }
2747
+ const result = await appServer.rpc("skills/list", {});
2748
+ for (const entry of result.data ?? []) {
2749
+ for (const skill of entry.skills ?? []) {
2750
+ if (skill.name) {
2751
+ installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2791
2752
  }
2792
2753
  }
2793
- } catch {
2794
- }
2795
- const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
2796
- await fetchMetaBatch(installedHubEntries);
2797
- const installed = [];
2798
- for (const [, info] of installedMap) {
2799
- const hubEntry = allEntries.find((e) => e.name === info.name);
2800
- const base = hubEntry ? buildHubEntry(hubEntry) : {
2801
- name: info.name,
2802
- owner: "local",
2803
- description: "",
2804
- displayName: "",
2805
- publishedAt: 0,
2806
- avatarUrl: "",
2807
- url: "",
2808
- installed: false
2809
- };
2810
- installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
2811
2754
  }
2812
- const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
2813
- setJson3(res, 200, { data: results, installed, total: allEntries.length });
2814
- } catch (error) {
2815
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch skills hub") });
2755
+ } catch {
2816
2756
  }
2817
- return true;
2757
+ const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
2758
+ await fetchMetaBatch(installedHubEntries);
2759
+ const installed = [];
2760
+ for (const [, info] of installedMap) {
2761
+ const hubEntry = allEntries.find((e) => e.name === info.name);
2762
+ const base = hubEntry ? buildHubEntry(hubEntry) : {
2763
+ name: info.name,
2764
+ owner: "local",
2765
+ description: "",
2766
+ displayName: "",
2767
+ publishedAt: 0,
2768
+ avatarUrl: "",
2769
+ url: "",
2770
+ installed: false
2771
+ };
2772
+ installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
2773
+ }
2774
+ const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
2775
+ setJson3(res, 200, { data: results, installed, total: allEntries.length });
2776
+ } catch (error) {
2777
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch skills hub") });
2818
2778
  }
2819
- if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
2820
- const state = await readSkillsSyncState();
2821
- setJson3(res, 200, {
2822
- data: {
2823
- loggedIn: Boolean(state.githubToken),
2824
- githubUsername: state.githubUsername ?? "",
2825
- repoOwner: state.repoOwner ?? "",
2826
- repoName: state.repoName ?? "",
2827
- configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
2828
- telemetry: {
2829
- lastPullCommitSha: state.lastPullCommitSha ?? "",
2830
- lastPushCommitSha: state.lastPushCommitSha ?? "",
2831
- lastSyncAttemptCount: state.lastSyncAttemptCount ?? 0,
2832
- lastSyncError: state.lastSyncError ?? "",
2833
- lastSyncAtIso: state.lastSyncAtIso ?? ""
2834
- },
2835
- startup: {
2836
- inProgress: startupSyncStatus.inProgress,
2837
- mode: startupSyncStatus.mode,
2838
- branch: startupSyncStatus.branch,
2839
- lastAction: startupSyncStatus.lastAction,
2840
- lastRunAtIso: startupSyncStatus.lastRunAtIso,
2841
- lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
2842
- lastError: startupSyncStatus.lastError
2843
- }
2779
+ return true;
2780
+ }
2781
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
2782
+ const state = await readSkillsSyncState();
2783
+ setJson3(res, 200, {
2784
+ data: {
2785
+ loggedIn: Boolean(state.githubToken),
2786
+ githubUsername: state.githubUsername ?? "",
2787
+ repoOwner: state.repoOwner ?? "",
2788
+ repoName: state.repoName ?? "",
2789
+ configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
2790
+ telemetry: {
2791
+ lastPullCommitSha: state.lastPullCommitSha ?? "",
2792
+ lastPushCommitSha: state.lastPushCommitSha ?? "",
2793
+ lastSyncAttemptCount: state.lastSyncAttemptCount ?? 0,
2794
+ lastSyncError: state.lastSyncError ?? "",
2795
+ lastSyncAtIso: state.lastSyncAtIso ?? ""
2796
+ },
2797
+ startup: {
2798
+ inProgress: startupSyncStatus.inProgress,
2799
+ mode: startupSyncStatus.mode,
2800
+ branch: startupSyncStatus.branch,
2801
+ lastAction: startupSyncStatus.lastAction,
2802
+ lastRunAtIso: startupSyncStatus.lastRunAtIso,
2803
+ lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
2804
+ lastError: startupSyncStatus.lastError
2844
2805
  }
2845
- });
2846
- return true;
2847
- }
2848
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
2849
- try {
2850
- const started = await startGithubDeviceLogin();
2851
- setJson3(res, 200, { data: started });
2852
- } catch (error) {
2853
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to start GitHub login") });
2854
2806
  }
2855
- return true;
2856
- }
2857
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
2858
- try {
2859
- const payload = asRecord3(await readJsonBody2(req));
2860
- const token = typeof payload?.token === "string" ? payload.token.trim() : "";
2861
- if (!token) {
2862
- setJson3(res, 400, { error: "Missing GitHub token" });
2863
- return true;
2864
- }
2865
- const username = await resolveGithubUsername(token);
2866
- await finalizeGithubLoginAndSync(token, username, appServer);
2867
- setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2868
- } catch (error) {
2869
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to login with GitHub token") });
2870
- }
2871
- return true;
2807
+ });
2808
+ return true;
2809
+ }
2810
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
2811
+ try {
2812
+ const started = await startGithubDeviceLogin();
2813
+ setJson3(res, 200, { data: started });
2814
+ } catch (error) {
2815
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to start GitHub login") });
2872
2816
  }
2873
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
2874
- try {
2875
- const state = await readSkillsSyncState();
2876
- await writeSkillsSyncState({
2877
- ...state,
2878
- githubToken: void 0,
2879
- githubUsername: void 0,
2880
- repoOwner: void 0,
2881
- repoName: void 0
2882
- });
2883
- setJson3(res, 200, { ok: true });
2884
- } catch (error) {
2885
- setJson3(res, 500, { error: getErrorMessage3(error, "Failed to logout GitHub") });
2817
+ return true;
2818
+ }
2819
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
2820
+ try {
2821
+ const payload = asRecord3(await readJsonBody2(req));
2822
+ const token = typeof payload?.token === "string" ? payload.token.trim() : "";
2823
+ if (!token) {
2824
+ setJson3(res, 400, { error: "Missing GitHub token" });
2825
+ return true;
2886
2826
  }
2887
- return true;
2827
+ const username = await resolveGithubUsername(token);
2828
+ await finalizeGithubLoginAndSync(token, username, appServer);
2829
+ setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2830
+ } catch (error) {
2831
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to login with GitHub token") });
2888
2832
  }
2889
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
2890
- try {
2891
- const payload = asRecord3(await readJsonBody2(req));
2892
- const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
2893
- if (!deviceCode) {
2894
- setJson3(res, 400, { error: "Missing deviceCode" });
2895
- return true;
2896
- }
2897
- const result = await completeGithubDeviceLogin(deviceCode);
2898
- if (!result.token) {
2899
- setJson3(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
2900
- return true;
2901
- }
2902
- const token = result.token;
2903
- const username = await resolveGithubUsername(token);
2904
- await finalizeGithubLoginAndSync(token, username, appServer);
2905
- setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2906
- } catch (error) {
2907
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to complete GitHub login") });
2908
- }
2909
- return true;
2833
+ return true;
2834
+ }
2835
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
2836
+ try {
2837
+ const state = await readSkillsSyncState();
2838
+ await writeSkillsSyncState({
2839
+ ...state,
2840
+ githubToken: void 0,
2841
+ githubUsername: void 0,
2842
+ repoOwner: void 0,
2843
+ repoName: void 0
2844
+ });
2845
+ setJson3(res, 200, { ok: true });
2846
+ } catch (error) {
2847
+ setJson3(res, 500, { error: getErrorMessage3(error, "Failed to logout GitHub") });
2910
2848
  }
2911
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
2912
- try {
2913
- const state = await readSkillsSyncState();
2914
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
2915
- setJson3(res, 400, { error: "Skills sync is not configured yet" });
2916
- return true;
2917
- }
2918
- if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
2919
- setJson3(res, 400, { error: "Refusing to push to upstream repository" });
2920
- return true;
2921
- }
2922
- const local = await collectLocalSyncedSkills(appServer);
2923
- const installedMap = await scanInstalledSkillsFromDisk();
2924
- await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
2925
- await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
2926
- setJson3(res, 200, { ok: true, data: { synced: local.length } });
2927
- } catch (error) {
2928
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to push synced skills") });
2849
+ return true;
2850
+ }
2851
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
2852
+ try {
2853
+ const payload = asRecord3(await readJsonBody2(req));
2854
+ const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
2855
+ if (!deviceCode) {
2856
+ setJson3(res, 400, { error: "Missing deviceCode" });
2857
+ return true;
2929
2858
  }
2930
- return true;
2859
+ const result = await completeGithubDeviceLogin(deviceCode);
2860
+ if (!result.token) {
2861
+ setJson3(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
2862
+ return true;
2863
+ }
2864
+ const token = result.token;
2865
+ const username = await resolveGithubUsername(token);
2866
+ await finalizeGithubLoginAndSync(token, username, appServer);
2867
+ setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2868
+ } catch (error) {
2869
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to complete GitHub login") });
2931
2870
  }
2932
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/startup-sync") {
2933
- try {
2934
- await runSkillsSyncStartup(appServer);
2935
- setJson3(res, 200, { ok: true });
2936
- } catch (error) {
2937
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to run startup sync") });
2871
+ return true;
2872
+ }
2873
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
2874
+ try {
2875
+ const state = await readSkillsSyncState();
2876
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
2877
+ setJson3(res, 400, { error: "Skills sync is not configured yet" });
2878
+ return true;
2938
2879
  }
2939
- return true;
2880
+ if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
2881
+ setJson3(res, 400, { error: "Refusing to push to upstream repository" });
2882
+ return true;
2883
+ }
2884
+ const local = await collectLocalSyncedSkills(appServer);
2885
+ const installedMap = await scanInstalledSkillsFromDisk();
2886
+ await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
2887
+ await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
2888
+ setJson3(res, 200, { ok: true, data: { synced: local.length } });
2889
+ } catch (error) {
2890
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to push synced skills") });
2940
2891
  }
2941
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
2942
- try {
2943
- const state = await readSkillsSyncState();
2944
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
2945
- await bootstrapSkillsFromUpstreamIntoLocal();
2946
- try {
2947
- await appServer.rpc("skills/list", { forceReload: true });
2948
- } catch {
2949
- }
2950
- setJson3(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
2951
- return true;
2952
- }
2953
- const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
2954
- const tree = await fetchSkillsTree();
2955
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
2956
- const ambiguousNames = /* @__PURE__ */ new Set();
2957
- for (const entry of tree) {
2958
- if (ambiguousNames.has(entry.name)) continue;
2959
- const existingOwner = uniqueOwnerByName.get(entry.name);
2960
- if (!existingOwner) {
2961
- uniqueOwnerByName.set(entry.name, entry.owner);
2962
- continue;
2963
- }
2964
- if (existingOwner !== entry.owner) {
2965
- uniqueOwnerByName.delete(entry.name);
2966
- ambiguousNames.add(entry.name);
2967
- }
2968
- }
2969
- const localDir = await detectUserSkillsDir(appServer);
2970
- await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
2971
- const localSkills = await scanInstalledSkillsFromDisk();
2972
- const missingAfterPull = [];
2973
- for (const skill of remote) {
2974
- const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
2975
- if (!owner) continue;
2976
- if (!localSkills.has(skill.name)) {
2977
- missingAfterPull.push(`${owner}/${skill.name}`);
2978
- continue;
2979
- }
2980
- const skillPath = join4(localDir, skill.name);
2981
- await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
2892
+ return true;
2893
+ }
2894
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/startup-sync") {
2895
+ try {
2896
+ await runSkillsSyncStartup(appServer);
2897
+ setJson3(res, 200, { ok: true });
2898
+ } catch (error) {
2899
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to run startup sync") });
2900
+ }
2901
+ return true;
2902
+ }
2903
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
2904
+ try {
2905
+ const state = await readSkillsSyncState();
2906
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
2907
+ await bootstrapSkillsFromUpstreamIntoLocal();
2908
+ try {
2909
+ await appServer.rpc("skills/list", { forceReload: true });
2910
+ } catch {
2982
2911
  }
2983
- if (missingAfterPull.length > 0) {
2984
- throw new Error(`Missing skill folders after pull: ${missingAfterPull.join(", ")}`);
2912
+ setJson3(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
2913
+ return true;
2914
+ }
2915
+ const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
2916
+ const tree = await fetchSkillsTree();
2917
+ const uniqueOwnerByName = /* @__PURE__ */ new Map();
2918
+ const ambiguousNames = /* @__PURE__ */ new Set();
2919
+ for (const entry of tree) {
2920
+ if (ambiguousNames.has(entry.name)) continue;
2921
+ const existingOwner = uniqueOwnerByName.get(entry.name);
2922
+ if (!existingOwner) {
2923
+ uniqueOwnerByName.set(entry.name, entry.owner);
2924
+ continue;
2985
2925
  }
2986
- const remoteNames = new Set(remote.map((row) => row.name));
2987
- for (const [name, localInfo] of localSkills.entries()) {
2988
- if (!remoteNames.has(name)) {
2989
- await rm3(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
2990
- }
2926
+ if (existingOwner !== entry.owner) {
2927
+ uniqueOwnerByName.delete(entry.name);
2928
+ ambiguousNames.add(entry.name);
2991
2929
  }
2992
- const nextOwners = {};
2993
- for (const item of remote) {
2994
- const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
2995
- if (owner) nextOwners[item.name] = owner;
2930
+ }
2931
+ const localDir = await detectUserSkillsDir(appServer);
2932
+ await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
2933
+ const localSkills = await scanInstalledSkillsFromDisk();
2934
+ const missingAfterPull = [];
2935
+ for (const skill of remote) {
2936
+ const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
2937
+ if (!owner) continue;
2938
+ if (!localSkills.has(skill.name)) {
2939
+ missingAfterPull.push(`${owner}/${skill.name}`);
2940
+ continue;
2996
2941
  }
2997
- const pulledHead = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: getSkillsInstallDir() }).catch(() => "");
2998
- await writeSkillsSyncState({
2999
- ...state,
3000
- installedOwners: nextOwners,
3001
- lastPullCommitSha: pulledHead.trim(),
3002
- lastSyncAttemptCount: 1,
3003
- lastSyncError: "",
3004
- lastSyncAtIso: (/* @__PURE__ */ new Date()).toISOString()
3005
- });
3006
- try {
3007
- await appServer.rpc("skills/list", { forceReload: true });
3008
- } catch {
2942
+ const skillPath = join4(localDir, skill.name);
2943
+ await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
2944
+ }
2945
+ if (missingAfterPull.length > 0) {
2946
+ throw new Error(`Missing skill folders after pull: ${missingAfterPull.join(", ")}`);
2947
+ }
2948
+ const remoteNames = new Set(remote.map((row) => row.name));
2949
+ for (const [name, localInfo] of localSkills.entries()) {
2950
+ if (!remoteNames.has(name)) {
2951
+ await rm3(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
3009
2952
  }
3010
- setJson3(res, 200, { ok: true, data: { synced: remote.length } });
3011
- } catch (error) {
3012
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to pull synced skills") });
3013
2953
  }
3014
- return true;
3015
- }
3016
- if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
2954
+ const nextOwners = {};
2955
+ for (const item of remote) {
2956
+ const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
2957
+ if (owner) nextOwners[item.name] = owner;
2958
+ }
2959
+ const pulledHead = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: getSkillsInstallDir() }).catch(() => "");
2960
+ await writeSkillsSyncState({
2961
+ ...state,
2962
+ installedOwners: nextOwners,
2963
+ lastPullCommitSha: pulledHead.trim(),
2964
+ lastSyncAttemptCount: 1,
2965
+ lastSyncError: "",
2966
+ lastSyncAtIso: (/* @__PURE__ */ new Date()).toISOString()
2967
+ });
3017
2968
  try {
3018
- const owner = url.searchParams.get("owner") || "";
3019
- const name = url.searchParams.get("name") || "";
3020
- const installed = url.searchParams.get("installed") === "true";
3021
- const skillPath = url.searchParams.get("path") || "";
3022
- if (!owner || !name) {
3023
- setJson3(res, 400, { error: "Missing owner or name" });
2969
+ await appServer.rpc("skills/list", { forceReload: true });
2970
+ } catch {
2971
+ }
2972
+ setJson3(res, 200, { ok: true, data: { synced: remote.length } });
2973
+ } catch (error) {
2974
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to pull synced skills") });
2975
+ }
2976
+ return true;
2977
+ }
2978
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
2979
+ try {
2980
+ const owner = url.searchParams.get("owner") || "";
2981
+ const name = url.searchParams.get("name") || "";
2982
+ const installed = url.searchParams.get("installed") === "true";
2983
+ const skillPath = url.searchParams.get("path") || "";
2984
+ if (!owner || !name) {
2985
+ setJson3(res, 400, { error: "Missing owner or name" });
2986
+ return true;
2987
+ }
2988
+ if (installed) {
2989
+ const installedMap = await scanInstalledSkillsFromDisk();
2990
+ const installedInfo = installedMap.get(name);
2991
+ const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
2992
+ if (localSkillPath) {
2993
+ const content2 = await readFile2(localSkillPath, "utf8");
2994
+ const description2 = extractSkillDescriptionFromMarkdown(content2);
2995
+ setJson3(res, 200, { content: content2, description: description2, source: "local" });
3024
2996
  return true;
3025
2997
  }
3026
- if (installed) {
3027
- const installedMap = await scanInstalledSkillsFromDisk();
3028
- const installedInfo = installedMap.get(name);
3029
- const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
3030
- if (localSkillPath) {
3031
- const content2 = await readFile2(localSkillPath, "utf8");
3032
- const description2 = extractSkillDescriptionFromMarkdown(content2);
3033
- setJson3(res, 200, { content: content2, description: description2, source: "local" });
3034
- return true;
3035
- }
3036
- }
3037
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
3038
- const resp = await fetch(rawUrl);
3039
- if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
3040
- const content = await resp.text();
3041
- const description = extractSkillDescriptionFromMarkdown(content);
3042
- setJson3(res, 200, { content, description, source: "remote" });
3043
- } catch (error) {
3044
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch SKILL.md") });
3045
2998
  }
3046
- return true;
2999
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
3000
+ const resp = await fetch(rawUrl);
3001
+ if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
3002
+ const content = await resp.text();
3003
+ const description = extractSkillDescriptionFromMarkdown(content);
3004
+ setJson3(res, 200, { content, description, source: "remote" });
3005
+ } catch (error) {
3006
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch SKILL.md") });
3047
3007
  }
3048
- if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
3008
+ return true;
3009
+ }
3010
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
3011
+ try {
3012
+ const payload = asRecord3(await readJsonBody2(req));
3013
+ const owner = typeof payload?.owner === "string" ? payload.owner : "";
3014
+ const name = typeof payload?.name === "string" ? payload.name : "";
3015
+ if (!owner || !name) {
3016
+ setJson3(res, 400, { error: "Missing owner or name" });
3017
+ return true;
3018
+ }
3019
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir2());
3020
+ if (!installerScript) {
3021
+ throw new Error("Skill installer script not found");
3022
+ }
3023
+ const pythonCommand = resolvePythonCommand();
3024
+ if (!pythonCommand) {
3025
+ throw new Error("Python 3 is required to install skills");
3026
+ }
3027
+ const installDest = await withTimeout(
3028
+ detectUserSkillsDir(appServer),
3029
+ 1e4,
3030
+ "detectUserSkillsDir"
3031
+ ).catch(() => getSkillsInstallDir());
3032
+ const skillDir = join4(installDest, name);
3033
+ if (existsSync2(skillDir)) {
3034
+ await rm3(skillDir, { recursive: true, force: true });
3035
+ }
3036
+ await runCommand2(pythonCommand.command, [
3037
+ ...pythonCommand.args,
3038
+ installerScript,
3039
+ "--repo",
3040
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
3041
+ "--path",
3042
+ `skills/${owner}/${name}`,
3043
+ "--dest",
3044
+ installDest,
3045
+ "--method",
3046
+ "git"
3047
+ ], { timeoutMs: 9e4 });
3049
3048
  try {
3050
- const payload = asRecord3(await readJsonBody2(req));
3051
- const owner = typeof payload?.owner === "string" ? payload.owner : "";
3052
- const name = typeof payload?.name === "string" ? payload.name : "";
3053
- if (!owner || !name) {
3054
- setJson3(res, 400, { error: "Missing owner or name" });
3055
- return true;
3056
- }
3057
- const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir2());
3058
- if (!installerScript) {
3059
- throw new Error("Skill installer script not found");
3060
- }
3061
- const pythonCommand = resolvePythonCommand();
3062
- if (!pythonCommand) {
3063
- throw new Error("Python 3 is required to install skills");
3064
- }
3065
- const installDest = await withTimeout(
3066
- detectUserSkillsDir(appServer),
3067
- 1e4,
3068
- "detectUserSkillsDir"
3069
- ).catch(() => getSkillsInstallDir());
3070
- const skillDir = join4(installDest, name);
3071
- if (existsSync2(skillDir)) {
3072
- await rm3(skillDir, { recursive: true, force: true });
3073
- }
3074
- await runCommand2(pythonCommand.command, [
3075
- ...pythonCommand.args,
3076
- installerScript,
3077
- "--repo",
3078
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
3079
- "--path",
3080
- `skills/${owner}/${name}`,
3081
- "--dest",
3082
- installDest,
3083
- "--method",
3084
- "git"
3085
- ], { timeoutMs: 9e4 });
3086
- try {
3087
- await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
3088
- } catch {
3089
- }
3049
+ await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
3050
+ } catch {
3051
+ }
3052
+ const syncState = await readSkillsSyncState();
3053
+ const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
3054
+ await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3055
+ autoPushSyncedSkills(appServer).catch(() => {
3056
+ });
3057
+ setJson3(res, 200, { ok: true, path: skillDir });
3058
+ } catch (error) {
3059
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
3060
+ }
3061
+ return true;
3062
+ }
3063
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
3064
+ try {
3065
+ const payload = asRecord3(await readJsonBody2(req));
3066
+ const name = typeof payload?.name === "string" ? payload.name : "";
3067
+ const path = typeof payload?.path === "string" ? payload.path : "";
3068
+ const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
3069
+ const target = normalizedPath || (name ? join4(getSkillsInstallDir(), name) : "");
3070
+ if (!target) {
3071
+ setJson3(res, 400, { error: "Missing name or path" });
3072
+ return true;
3073
+ }
3074
+ await rm3(target, { recursive: true, force: true });
3075
+ if (name) {
3090
3076
  const syncState = await readSkillsSyncState();
3091
- const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
3077
+ const nextOwners = { ...syncState.installedOwners ?? {} };
3078
+ delete nextOwners[name];
3092
3079
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3093
- autoPushSyncedSkills(appServer).catch(() => {
3094
- });
3095
- setJson3(res, 200, { ok: true, path: skillDir });
3096
- } catch (error) {
3097
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
3098
3080
  }
3099
- return true;
3100
- }
3101
- if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
3081
+ autoPushSyncedSkills(appServer).catch(() => {
3082
+ });
3102
3083
  try {
3103
- const payload = asRecord3(await readJsonBody2(req));
3104
- const name = typeof payload?.name === "string" ? payload.name : "";
3105
- const path = typeof payload?.path === "string" ? payload.path : "";
3106
- const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
3107
- const target = normalizedPath || (name ? join4(getSkillsInstallDir(), name) : "");
3108
- if (!target) {
3109
- setJson3(res, 400, { error: "Missing name or path" });
3110
- return true;
3111
- }
3112
- await rm3(target, { recursive: true, force: true });
3113
- if (name) {
3114
- const syncState = await readSkillsSyncState();
3115
- const nextOwners = { ...syncState.installedOwners ?? {} };
3116
- delete nextOwners[name];
3117
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3118
- }
3119
- autoPushSyncedSkills(appServer).catch(() => {
3120
- });
3121
- try {
3122
- await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
3123
- } catch {
3124
- }
3125
- setJson3(res, 200, { ok: true, deletedPath: target });
3126
- } catch (error) {
3127
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to uninstall skill") });
3084
+ await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
3085
+ } catch {
3128
3086
  }
3129
- return true;
3130
- }
3131
- } catch (error) {
3132
- Sentry3.captureException(error);
3133
- if (!res.headersSent) {
3134
- setJson3(res, 500, { error: "Internal skills error" });
3087
+ setJson3(res, 200, { ok: true, deletedPath: target });
3088
+ } catch (error) {
3089
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to uninstall skill") });
3135
3090
  }
3136
3091
  return true;
3137
3092
  }
@@ -3140,6 +3095,18 @@ async function handleSkillsRoutes(req, res, url, context) {
3140
3095
 
3141
3096
  // src/server/telegramThreadBridge.ts
3142
3097
  import { basename } from "path";
3098
+ var TELEGRAM_MESSAGE_MAX_LENGTH = 3500;
3099
+ var TELEGRAM_BOT_COMMANDS = [
3100
+ { command: "start", description: "Show quick start and thread picker" },
3101
+ { command: "threads", description: "List recent threads to connect" },
3102
+ { command: "newthread", description: "Create and connect a new thread" },
3103
+ { command: "thread", description: "Connect existing thread: /thread <id>" },
3104
+ { command: "current", description: "Show currently connected thread" },
3105
+ { command: "history", description: "Show recent history for current thread" },
3106
+ { command: "status", description: "Show bridge and mapping status" },
3107
+ { command: "whoami", description: "Show your Telegram IDs" },
3108
+ { command: "help", description: "Show available commands" }
3109
+ ];
3143
3110
  function asRecord4(value) {
3144
3111
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3145
3112
  }
@@ -3174,6 +3141,73 @@ function normalizeTelegramAllowlist(values) {
3174
3141
  }).filter((value) => Number.isFinite(value)))).slice(0, 100);
3175
3142
  return { allowAllUsers, allowedUserIds };
3176
3143
  }
3144
+ function escapeHtml(value) {
3145
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3146
+ }
3147
+ function renderMarkdownInlineToTelegramHtml(value) {
3148
+ let rendered = escapeHtml(value);
3149
+ rendered = rendered.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2">$1</a>');
3150
+ rendered = rendered.replace(/`([^`\n]+)`/g, "<code>$1</code>");
3151
+ rendered = rendered.replace(/\*\*([^*\n][^*\n]*?)\*\*/g, "<b>$1</b>");
3152
+ rendered = rendered.replace(/__([^_\n][^_\n]*?)__/g, "<b>$1</b>");
3153
+ rendered = rendered.replace(/\*([^*\n][^*\n]*?)\*/g, "<i>$1</i>");
3154
+ rendered = rendered.replace(/_([^_\n][^_\n]*?)_/g, "<i>$1</i>");
3155
+ rendered = rendered.replace(/^(#{1,6})\s+(.+)$/gm, (_match, _hashes, content) => `<b>${content}</b>`);
3156
+ return rendered;
3157
+ }
3158
+ function renderMarkdownToTelegramHtml(markdown) {
3159
+ const normalized = markdown.replace(/\r\n/g, "\n");
3160
+ const fencedCodeRegex = /```([a-zA-Z0-9_-]+)?\n([\s\S]*?)```/g;
3161
+ let cursor = 0;
3162
+ const parts = [];
3163
+ let match = fencedCodeRegex.exec(normalized);
3164
+ while (match) {
3165
+ const [fullMatch, lang, code] = match;
3166
+ const matchIndex = match.index;
3167
+ const before = normalized.slice(cursor, matchIndex);
3168
+ if (before) {
3169
+ parts.push(renderMarkdownInlineToTelegramHtml(before));
3170
+ }
3171
+ const escapedCode = escapeHtml((code ?? "").replace(/\n+$/g, ""));
3172
+ const escapedLang = typeof lang === "string" ? escapeHtml(lang) : "";
3173
+ if (escapedLang) {
3174
+ parts.push(`<pre><code class="language-${escapedLang}">${escapedCode}</code></pre>`);
3175
+ } else {
3176
+ parts.push(`<pre>${escapedCode}</pre>`);
3177
+ }
3178
+ cursor = matchIndex + fullMatch.length;
3179
+ match = fencedCodeRegex.exec(normalized);
3180
+ }
3181
+ const tail = normalized.slice(cursor);
3182
+ if (tail) {
3183
+ parts.push(renderMarkdownInlineToTelegramHtml(tail));
3184
+ }
3185
+ return parts.join("");
3186
+ }
3187
+ function splitTelegramText(text, maxLength = TELEGRAM_MESSAGE_MAX_LENGTH) {
3188
+ const normalized = text.replace(/\r\n/g, "\n").trim();
3189
+ if (!normalized) return [];
3190
+ if (normalized.length <= maxLength) return [normalized];
3191
+ const chunks = [];
3192
+ let remaining = normalized;
3193
+ while (remaining.length > maxLength) {
3194
+ let splitIndex = remaining.lastIndexOf("\n\n", maxLength);
3195
+ if (splitIndex < Math.floor(maxLength * 0.5)) {
3196
+ splitIndex = remaining.lastIndexOf("\n", maxLength);
3197
+ }
3198
+ if (splitIndex < Math.floor(maxLength * 0.5)) {
3199
+ splitIndex = remaining.lastIndexOf(" ", maxLength);
3200
+ }
3201
+ if (splitIndex <= 0) {
3202
+ splitIndex = maxLength;
3203
+ }
3204
+ const chunk = remaining.slice(0, splitIndex).trim();
3205
+ if (chunk) chunks.push(chunk);
3206
+ remaining = remaining.slice(splitIndex).trim();
3207
+ }
3208
+ if (remaining) chunks.push(remaining);
3209
+ return chunks;
3210
+ }
3177
3211
  var TelegramThreadBridge = class {
3178
3212
  constructor(appServer, options = {}) {
3179
3213
  this.allowAllUsers = false;
@@ -3196,6 +3230,8 @@ var TelegramThreadBridge = class {
3196
3230
  start() {
3197
3231
  if (!this.token || this.active) return;
3198
3232
  this.active = true;
3233
+ void this.syncBotCommands().catch(() => {
3234
+ });
3199
3235
  void this.notifyOnlineForKnownChats().catch(() => {
3200
3236
  });
3201
3237
  this.pollingTask = this.pollLoop();
@@ -3251,6 +3287,8 @@ var TelegramThreadBridge = class {
3251
3287
  throw new Error("Telegram bot token is required");
3252
3288
  }
3253
3289
  this.token = normalizedToken;
3290
+ void this.syncBotCommands().catch(() => {
3291
+ });
3254
3292
  }
3255
3293
  getStatus() {
3256
3294
  return {
@@ -3293,17 +3331,49 @@ var TelegramThreadBridge = class {
3293
3331
  this.onChatSeen?.(Math.trunc(chatId));
3294
3332
  }
3295
3333
  async sendTelegramMessage(chatId, text, options = {}) {
3296
- const message = text.trim();
3297
- if (!message) return;
3298
- const payload = { chat_id: chatId, text: message };
3334
+ const chunks = splitTelegramText(text);
3335
+ if (chunks.length === 0) return;
3336
+ for (let index = 0; index < chunks.length; index += 1) {
3337
+ const chunk = chunks[index];
3338
+ const replyMarkup = index === 0 ? options.replyMarkup : void 0;
3339
+ const htmlChunk = renderMarkdownToTelegramHtml(chunk);
3340
+ try {
3341
+ await this.sendMessageRequest(chatId, htmlChunk, { replyMarkup, parseMode: "HTML" });
3342
+ } catch {
3343
+ await this.sendMessageRequest(chatId, chunk, { replyMarkup });
3344
+ }
3345
+ }
3346
+ }
3347
+ async sendMessageRequest(chatId, text, options = {}) {
3348
+ const payload = { chat_id: chatId, text };
3299
3349
  if (options.replyMarkup) {
3300
3350
  payload.reply_markup = options.replyMarkup;
3301
3351
  }
3302
- await fetch(this.apiUrl("sendMessage"), {
3352
+ if (options.parseMode) {
3353
+ payload.parse_mode = options.parseMode;
3354
+ }
3355
+ await this.callTelegramApi("sendMessage", payload);
3356
+ }
3357
+ async syncBotCommands() {
3358
+ if (!this.token) return;
3359
+ await this.callTelegramApi("setMyCommands", {
3360
+ commands: TELEGRAM_BOT_COMMANDS
3361
+ });
3362
+ }
3363
+ async callTelegramApi(method, payload) {
3364
+ const response = await fetch(this.apiUrl(method), {
3303
3365
  method: "POST",
3304
3366
  headers: { "Content-Type": "application/json" },
3305
3367
  body: JSON.stringify(payload)
3306
3368
  });
3369
+ const parsed = asRecord4(await response.json());
3370
+ const ok = parsed?.ok === true;
3371
+ if (!response.ok || !ok) {
3372
+ const description = typeof parsed?.description === "string" ? parsed.description : "";
3373
+ const statusPart = `${String(response.status)} ${response.statusText}`.trim();
3374
+ throw new Error(description || statusPart || `Telegram API ${method} failed`);
3375
+ }
3376
+ return parsed ?? {};
3307
3377
  }
3308
3378
  async sendOnlineMessage(chatId) {
3309
3379
  await this.sendTelegramMessage(chatId, "Codex thread bridge went online.");
@@ -3330,6 +3400,11 @@ var TelegramThreadBridge = class {
3330
3400
  }
3331
3401
  this.markChatSeen(chatId);
3332
3402
  if (text === "/start") {
3403
+ await this.sendTelegramMessage(chatId, this.helpMessage());
3404
+ await this.sendThreadPicker(chatId);
3405
+ return;
3406
+ }
3407
+ if (text === "/threads") {
3333
3408
  await this.sendThreadPicker(chatId);
3334
3409
  return;
3335
3410
  }
@@ -3345,6 +3420,59 @@ var TelegramThreadBridge = class {
3345
3420
  await this.sendTelegramMessage(chatId, `Mapped to thread: ${threadId2}`);
3346
3421
  return;
3347
3422
  }
3423
+ if (text === "/current") {
3424
+ const threadId2 = this.threadIdByChatId.get(chatId);
3425
+ await this.sendTelegramMessage(chatId, threadId2 ? `Current thread: \`${threadId2}\`` : "No thread is connected for this chat yet. Use /threads, /newthread, or /thread <id>.");
3426
+ return;
3427
+ }
3428
+ if (text === "/history") {
3429
+ const threadId2 = this.threadIdByChatId.get(chatId);
3430
+ if (!threadId2) {
3431
+ await this.sendTelegramMessage(chatId, "No thread is connected for this chat yet. Use /threads or /newthread first.");
3432
+ return;
3433
+ }
3434
+ const history = await this.readThreadHistorySummary(threadId2);
3435
+ await this.sendTelegramMessage(chatId, history);
3436
+ return;
3437
+ }
3438
+ if (text === "/status") {
3439
+ const status = this.getStatus();
3440
+ const mappedThreadId = this.threadIdByChatId.get(chatId) ?? "none";
3441
+ await this.sendTelegramMessage(
3442
+ chatId,
3443
+ [
3444
+ "**Bridge status**",
3445
+ `configured: ${String(status.configured)}`,
3446
+ `active: ${String(status.active)}`,
3447
+ `mapped chats: ${String(status.mappedChats)}`,
3448
+ `mapped threads: ${String(status.mappedThreads)}`,
3449
+ `allowed users: ${String(status.allowedUsers)}`,
3450
+ `allow all users: ${String(status.allowAllUsers)}`,
3451
+ `chat ${String(chatId)} thread: \`${mappedThreadId}\``,
3452
+ status.lastError ? `last error: ${status.lastError}` : ""
3453
+ ].filter(Boolean).join("\n")
3454
+ );
3455
+ return;
3456
+ }
3457
+ if (text === "/whoami") {
3458
+ const normalizedSenderId = typeof senderId === "number" && Number.isFinite(senderId) ? String(Math.trunc(senderId)) : "unknown";
3459
+ const normalizedChatId = String(Math.trunc(chatId));
3460
+ await this.sendTelegramMessage(
3461
+ chatId,
3462
+ [
3463
+ "**Identity**",
3464
+ `telegram user id: \`${normalizedSenderId}\``,
3465
+ `chat id: \`${normalizedChatId}\``,
3466
+ `authorized: ${String(this.isAllowedSender(senderId))}`,
3467
+ this.allowAllUsers ? "allowlist mode: `*`" : "allowlist mode: explicit ids"
3468
+ ].join("\n")
3469
+ );
3470
+ return;
3471
+ }
3472
+ if (text === "/help") {
3473
+ await this.sendTelegramMessage(chatId, this.helpMessage());
3474
+ return;
3475
+ }
3348
3476
  const threadId = await this.ensureThreadForChat(chatId);
3349
3477
  try {
3350
3478
  await this.appServer.rpc("turn/start", {
@@ -3410,14 +3538,14 @@ Add this ID to the bot allowlist before using the bridge.`;
3410
3538
  }
3411
3539
  return "Unauthorized sender";
3412
3540
  }
3541
+ helpMessage() {
3542
+ const rows = TELEGRAM_BOT_COMMANDS.map((command) => `/${command.command} - ${command.description}`);
3543
+ return ["**Available commands**", ...rows].join("\n");
3544
+ }
3413
3545
  async answerCallbackQuery(callbackQueryId, text) {
3414
- await fetch(this.apiUrl("answerCallbackQuery"), {
3415
- method: "POST",
3416
- headers: { "Content-Type": "application/json" },
3417
- body: JSON.stringify({
3418
- callback_query_id: callbackQueryId,
3419
- text
3420
- })
3546
+ await this.callTelegramApi("answerCallbackQuery", {
3547
+ callback_query_id: callbackQueryId,
3548
+ text
3421
3549
  });
3422
3550
  }
3423
3551
  async sendThreadPicker(chatId) {
@@ -3703,22 +3831,57 @@ async function getFreeModels() {
3703
3831
  var FREE_MODE_DEFAULT_MODEL = "openrouter/free";
3704
3832
  var FREE_MODE_STATE_FILE = "webui-free-mode.json";
3705
3833
  var CUSTOM_PROVIDER_ID = "custom-endpoint";
3706
- function getFreeModeConfigArgs(state) {
3707
- if (!state.enabled || !state.apiKey) return [];
3834
+ var OPENCODE_ZEN_PROVIDER_ID = "opencode-zen";
3835
+ var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
3836
+ function getFreeModeEnvVars(state) {
3837
+ if (!state.enabled) return {};
3838
+ if (state.provider === "opencode-zen" && state.apiKey) {
3839
+ return { OPENCODE_ZEN_API_KEY: state.apiKey };
3840
+ }
3841
+ if (state.provider === "custom" && state.customBaseUrl && state.apiKey) {
3842
+ return { CUSTOM_ENDPOINT_API_KEY: state.apiKey };
3843
+ }
3844
+ return {};
3845
+ }
3846
+ function getFreeModeConfigArgs(state, serverPort) {
3847
+ if (!state.enabled) return [];
3848
+ if (state.provider === "opencode-zen") {
3849
+ const baseUrl2 = serverPort ? `http://127.0.0.1:${serverPort}/codex-api/zen-proxy/v1` : OPENCODE_ZEN_BASE_URL;
3850
+ const wireApi = serverPort ? "responses" : state.wireApi || "chat";
3851
+ 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"`];
3852
+ return [
3853
+ "-c",
3854
+ `model_provider="${OPENCODE_ZEN_PROVIDER_ID}"`,
3855
+ "-c",
3856
+ `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.name="OpenCode Zen"`,
3857
+ "-c",
3858
+ `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.base_url="${baseUrl2}"`,
3859
+ "-c",
3860
+ `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.wire_api="${wireApi}"`,
3861
+ ...authArgs
3862
+ ];
3863
+ }
3708
3864
  if (state.provider === "custom" && state.customBaseUrl) {
3865
+ const baseUrl2 = serverPort ? `http://127.0.0.1:${serverPort}/codex-api/custom-proxy/v1` : state.customBaseUrl;
3866
+ const wireApi = serverPort ? "responses" : state.wireApi || "responses";
3867
+ const authArgs = serverPort ? ["-c", `model_providers.${CUSTOM_PROVIDER_ID}.experimental_bearer_token="custom-proxy-token"`] : ["-c", `model_providers.${CUSTOM_PROVIDER_ID}.env_key="CUSTOM_ENDPOINT_API_KEY"`];
3868
+ const modelArgs = state.model?.trim() ? ["-c", `model="${state.model.trim()}"`] : [];
3709
3869
  return [
3870
+ ...modelArgs,
3710
3871
  "-c",
3711
3872
  `model_provider="${CUSTOM_PROVIDER_ID}"`,
3712
3873
  "-c",
3713
3874
  `model_providers.${CUSTOM_PROVIDER_ID}.name="Custom Endpoint"`,
3714
3875
  "-c",
3715
- `model_providers.${CUSTOM_PROVIDER_ID}.base_url="${state.customBaseUrl}"`,
3876
+ `model_providers.${CUSTOM_PROVIDER_ID}.base_url="${baseUrl2}"`,
3716
3877
  "-c",
3717
- `model_providers.${CUSTOM_PROVIDER_ID}.wire_api="responses"`,
3718
- "-c",
3719
- `model_providers.${CUSTOM_PROVIDER_ID}.experimental_bearer_token="${state.apiKey}"`
3878
+ `model_providers.${CUSTOM_PROVIDER_ID}.wire_api="${wireApi}"`,
3879
+ ...authArgs
3720
3880
  ];
3721
3881
  }
3882
+ if (!state.apiKey) return [];
3883
+ const baseUrl = serverPort ? `http://127.0.0.1:${serverPort}/codex-api/openrouter-proxy/v1` : FREE_MODE_BASE_URL;
3884
+ const bearerToken = serverPort ? "openrouter-proxy-token" : state.apiKey;
3722
3885
  return [
3723
3886
  "-c",
3724
3887
  `model="${state.model}"`,
@@ -3727,17 +3890,788 @@ function getFreeModeConfigArgs(state) {
3727
3890
  "-c",
3728
3891
  `model_providers.${FREE_MODE_PROVIDER_ID}.name="OpenRouter Free"`,
3729
3892
  "-c",
3730
- `model_providers.${FREE_MODE_PROVIDER_ID}.base_url="${FREE_MODE_BASE_URL}"`,
3893
+ `model_providers.${FREE_MODE_PROVIDER_ID}.base_url="${baseUrl}"`,
3731
3894
  "-c",
3732
3895
  `model_providers.${FREE_MODE_PROVIDER_ID}.wire_api="responses"`,
3733
3896
  "-c",
3734
- `model_providers.${FREE_MODE_PROVIDER_ID}.experimental_bearer_token="${state.apiKey}"`
3897
+ `model_providers.${FREE_MODE_PROVIDER_ID}.experimental_bearer_token="${bearerToken}"`
3735
3898
  ];
3736
3899
  }
3737
3900
 
3738
- // src/utils/commandInvocation.ts
3901
+ // src/server/unifiedResponsesProxy.ts
3902
+ import { request as httpRequest } from "http";
3903
+ import { request as httpsRequest } from "https";
3904
+ function readRequestBody(req) {
3905
+ return new Promise((resolve4, reject) => {
3906
+ const chunks = [];
3907
+ req.on("data", (chunk) => chunks.push(chunk));
3908
+ req.on("end", () => resolve4(Buffer.concat(chunks)));
3909
+ req.on("error", reject);
3910
+ });
3911
+ }
3912
+ function safeStringifyUnknown(value) {
3913
+ if (typeof value === "string") return value;
3914
+ try {
3915
+ return JSON.stringify(value ?? "");
3916
+ } catch {
3917
+ return String(value ?? "");
3918
+ }
3919
+ }
3920
+ function appendAssistantText(messages, text) {
3921
+ const trimmedText = text.trim();
3922
+ if (!trimmedText) return;
3923
+ const lastMessage = messages[messages.length - 1];
3924
+ if (lastMessage?.role === "assistant" && Array.isArray(lastMessage.tool_calls)) {
3925
+ lastMessage.content = lastMessage.content ? `${lastMessage.content}
3926
+ ${trimmedText}` : trimmedText;
3927
+ return;
3928
+ }
3929
+ messages.push({ role: "assistant", content: trimmedText });
3930
+ }
3931
+ function appendAssistantToolCall(messages, toolCall) {
3932
+ const lastMessage = messages[messages.length - 1];
3933
+ if (lastMessage?.role === "assistant" && !lastMessage.tool_call_id) {
3934
+ lastMessage.tool_calls = [...lastMessage.tool_calls ?? [], toolCall];
3935
+ return;
3936
+ }
3937
+ messages.push({ role: "assistant", tool_calls: [toolCall] });
3938
+ }
3939
+ function responsesInputToMessages(input, instructions) {
3940
+ const messages = [];
3941
+ if (instructions) {
3942
+ messages.push({ role: "system", content: instructions });
3943
+ }
3944
+ if (typeof input === "string") {
3945
+ messages.push({ role: "user", content: input });
3946
+ return messages;
3947
+ }
3948
+ for (const item of input) {
3949
+ if (!item || typeof item !== "object") continue;
3950
+ if (item.type === "message" && item.role) {
3951
+ const content = item.content;
3952
+ 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 : "";
3953
+ const role = item.role === "developer" ? "system" : item.role;
3954
+ if (role === "assistant") {
3955
+ appendAssistantText(messages, text);
3956
+ } else {
3957
+ messages.push({ role, content: text });
3958
+ }
3959
+ continue;
3960
+ }
3961
+ if ((item.type === "function_call_output" || item.type === "computer_call_output") && item.call_id) {
3962
+ messages.push({
3963
+ role: "tool",
3964
+ tool_call_id: item.call_id,
3965
+ content: safeStringifyUnknown(item.output)
3966
+ });
3967
+ continue;
3968
+ }
3969
+ if (item.type === "function_call" && item.call_id && item.name) {
3970
+ appendAssistantToolCall(messages, {
3971
+ id: item.call_id,
3972
+ type: "function",
3973
+ function: {
3974
+ name: item.name,
3975
+ arguments: typeof item.arguments === "string" ? item.arguments : "{}"
3976
+ }
3977
+ });
3978
+ }
3979
+ }
3980
+ return messages;
3981
+ }
3982
+ function responsesToolsToChatTools(tools) {
3983
+ if (!Array.isArray(tools)) return void 0;
3984
+ const mapped = tools.map((tool) => {
3985
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) return null;
3986
+ const row = tool;
3987
+ if (row.type !== "function") return null;
3988
+ const name = typeof row.name === "string" ? row.name : "";
3989
+ if (!name) return null;
3990
+ const description = typeof row.description === "string" ? row.description : void 0;
3991
+ return {
3992
+ type: "function",
3993
+ function: {
3994
+ name,
3995
+ ...description ? { description } : {},
3996
+ ...row.parameters !== void 0 ? { parameters: row.parameters } : {}
3997
+ }
3998
+ };
3999
+ }).filter((row) => Boolean(row));
4000
+ return mapped.length > 0 ? mapped : void 0;
4001
+ }
4002
+ function responsesToolChoiceToChatToolChoice(toolChoice) {
4003
+ if (typeof toolChoice === "string") return toolChoice;
4004
+ if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) return void 0;
4005
+ const row = toolChoice;
4006
+ if (row.type !== "function") return void 0;
4007
+ const name = typeof row.name === "string" ? row.name : row.function && typeof row.function === "object" && typeof row.function.name === "string" ? String(row.function.name) : "";
4008
+ if (!name) return void 0;
4009
+ return { type: "function", function: { name } };
4010
+ }
4011
+ function chatCompletionToResponsesFormat(chatResponse, model) {
4012
+ const choices = chatResponse.choices ?? [];
4013
+ const output = [];
4014
+ for (const choice of choices) {
4015
+ const message = choice.message;
4016
+ if (!message) continue;
4017
+ if (Array.isArray(message.tool_calls)) {
4018
+ for (const toolCall of message.tool_calls) {
4019
+ if (!toolCall || toolCall.type !== "function") continue;
4020
+ const callId = typeof toolCall.id === "string" && toolCall.id ? toolCall.id : `call_${Date.now()}`;
4021
+ const name = typeof toolCall.function?.name === "string" ? toolCall.function.name : "";
4022
+ if (!name) continue;
4023
+ output.push({
4024
+ type: "function_call",
4025
+ name,
4026
+ call_id: callId,
4027
+ arguments: typeof toolCall.function?.arguments === "string" ? toolCall.function.arguments : "{}",
4028
+ status: "completed"
4029
+ });
4030
+ }
4031
+ }
4032
+ if (message.content) {
4033
+ output.push({
4034
+ type: "message",
4035
+ role: "assistant",
4036
+ content: [{ type: "output_text", text: message.content }],
4037
+ status: "completed"
4038
+ });
4039
+ }
4040
+ }
4041
+ const usage = chatResponse.usage;
4042
+ return {
4043
+ id: chatResponse.id ?? `resp_${Date.now()}`,
4044
+ object: "response",
4045
+ created_at: chatResponse.created ?? Math.floor(Date.now() / 1e3),
4046
+ status: "completed",
4047
+ model,
4048
+ output,
4049
+ usage: usage ? {
4050
+ input_tokens: usage.prompt_tokens ?? 0,
4051
+ output_tokens: usage.completion_tokens ?? 0,
4052
+ total_tokens: usage.total_tokens ?? 0
4053
+ } : void 0
4054
+ };
4055
+ }
4056
+ function forwardStreamingTextResponse(upstreamRes, res, model) {
4057
+ res.writeHead(200, {
4058
+ "Content-Type": "text/event-stream",
4059
+ "Cache-Control": "no-cache",
4060
+ "Connection": "keep-alive"
4061
+ });
4062
+ let buffer = "";
4063
+ const contentParts = [];
4064
+ let responseId = `resp_${Date.now()}`;
4065
+ res.write(`data: {"type":"response.created","response":{"id":"${responseId}","object":"response","status":"in_progress","model":"${model}","output":[]}}
4066
+
4067
+ `);
4068
+ res.write('data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","role":"assistant","content":[],"status":"in_progress"}}\n\n');
4069
+ res.write('data: {"type":"response.content_part.added","output_index":0,"content_index":0,"part":{"type":"output_text","text":""}}\n\n');
4070
+ upstreamRes.on("data", (chunk) => {
4071
+ buffer += chunk.toString();
4072
+ const lines = buffer.split("\n");
4073
+ buffer = lines.pop() ?? "";
4074
+ for (const line of lines) {
4075
+ if (!line.startsWith("data: ")) continue;
4076
+ const data = line.slice(6).trim();
4077
+ if (data === "[DONE]") continue;
4078
+ try {
4079
+ const parsed = JSON.parse(data);
4080
+ if (parsed.id) responseId = `resp_${parsed.id}`;
4081
+ const delta = parsed.choices?.[0]?.delta;
4082
+ if (delta?.content) {
4083
+ contentParts.push(delta.content);
4084
+ const escaped = JSON.stringify(delta.content).slice(1, -1);
4085
+ res.write(`data: {"type":"response.output_text.delta","output_index":0,"content_index":0,"delta":"${escaped}"}
4086
+
4087
+ `);
4088
+ }
4089
+ } catch {
4090
+ }
4091
+ }
4092
+ });
4093
+ upstreamRes.on("end", () => {
4094
+ const fullText = contentParts.join("");
4095
+ const escapedFull = JSON.stringify(fullText).slice(1, -1);
4096
+ res.write(`data: {"type":"response.output_text.done","output_index":0,"content_index":0,"text":"${escapedFull}"}
4097
+
4098
+ `);
4099
+ res.write(`data: {"type":"response.content_part.done","output_index":0,"content_index":0,"part":{"type":"output_text","text":"${escapedFull}"}}
4100
+
4101
+ `);
4102
+ res.write(`data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"${escapedFull}"}],"status":"completed"}}
4103
+
4104
+ `);
4105
+ 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"}]}}
4106
+
4107
+ `);
4108
+ res.end();
4109
+ });
4110
+ upstreamRes.on("error", () => {
4111
+ if (!res.writableEnded) res.end();
4112
+ });
4113
+ }
4114
+ function sendSyntheticStreamingCompletion(res, response) {
4115
+ const responseId = typeof response.id === "string" && response.id ? response.id : `resp_${Date.now()}`;
4116
+ const model = typeof response.model === "string" ? response.model : "";
4117
+ const output = Array.isArray(response.output) ? response.output : [];
4118
+ res.writeHead(200, {
4119
+ "Content-Type": "text/event-stream",
4120
+ "Cache-Control": "no-cache",
4121
+ "Connection": "keep-alive"
4122
+ });
4123
+ const createdPayload = {
4124
+ type: "response.created",
4125
+ response: {
4126
+ id: responseId,
4127
+ object: "response",
4128
+ status: "in_progress",
4129
+ model,
4130
+ output: []
4131
+ }
4132
+ };
4133
+ const completedPayload = {
4134
+ type: "response.completed",
4135
+ response: {
4136
+ id: responseId,
4137
+ object: "response",
4138
+ status: "completed",
4139
+ model,
4140
+ output,
4141
+ usage: response.usage
4142
+ }
4143
+ };
4144
+ res.write(`data: ${JSON.stringify(createdPayload)}
4145
+
4146
+ `);
4147
+ output.forEach((item, index) => {
4148
+ res.write(`data: ${JSON.stringify({ type: "response.output_item.added", output_index: index, item })}
4149
+
4150
+ `);
4151
+ res.write(`data: ${JSON.stringify({ type: "response.output_item.done", output_index: index, item })}
4152
+
4153
+ `);
4154
+ });
4155
+ res.write(`data: ${JSON.stringify(completedPayload)}
4156
+
4157
+ `);
4158
+ res.end();
4159
+ }
4160
+ function copyProxyHeaders(upstreamHeaders) {
4161
+ const headers = {};
4162
+ for (const [key, value] of Object.entries(upstreamHeaders)) {
4163
+ if (!value) continue;
4164
+ const lower = key.toLowerCase();
4165
+ if (lower === "transfer-encoding" || lower === "content-length" || lower === "connection") continue;
4166
+ headers[key] = Array.isArray(value) ? value.join(", ") : value;
4167
+ }
4168
+ return headers;
4169
+ }
4170
+ function hasToolOutputsInInput(input) {
4171
+ if (!Array.isArray(input)) return false;
4172
+ return input.some((item) => item?.type === "function_call_output" || item?.type === "computer_call_output");
4173
+ }
4174
+ function handleUnifiedResponsesProxyRequest(req, res, options) {
4175
+ void (async () => {
4176
+ try {
4177
+ if (!options.bearerToken) {
4178
+ res.writeHead(401, { "Content-Type": "application/json" });
4179
+ res.end(JSON.stringify({ error: { message: options.missingKeyMessage } }));
4180
+ return;
4181
+ }
4182
+ const rawBody = await readRequestBody(req);
4183
+ const parsedBody = JSON.parse(rawBody.toString());
4184
+ const hasTools = Array.isArray(parsedBody.tools) && parsedBody.tools.length > 0;
4185
+ const hasToolOutputs = hasToolOutputsInInput(parsedBody.input);
4186
+ const useResponsesFallback = options.allowToolFallbackToResponses && (hasTools || hasToolOutputs);
4187
+ const useChatCompletions = options.wireApi === "chat" && !useResponsesFallback;
4188
+ const useChatPayload = useChatCompletions || options.responsesPayloadFormat === "chat";
4189
+ const isStreaming = parsedBody.stream === true;
4190
+ const effectiveStreaming = useChatCompletions && isStreaming && !(hasTools || hasToolOutputs);
4191
+ let payload = "";
4192
+ let upstreamUrl;
4193
+ if (useChatPayload) {
4194
+ const chatReq = {
4195
+ model: parsedBody.model,
4196
+ messages: responsesInputToMessages(parsedBody.input, parsedBody.instructions),
4197
+ stream: useChatCompletions ? effectiveStreaming : isStreaming
4198
+ };
4199
+ if (parsedBody.temperature != null) chatReq.temperature = parsedBody.temperature;
4200
+ if (parsedBody.top_p != null) chatReq.top_p = parsedBody.top_p;
4201
+ if (parsedBody.max_output_tokens != null) chatReq.max_tokens = parsedBody.max_output_tokens;
4202
+ const chatTools = responsesToolsToChatTools(parsedBody.tools);
4203
+ const chatToolChoice = responsesToolChoiceToChatToolChoice(parsedBody.tool_choice);
4204
+ if (chatTools) chatReq.tools = chatTools;
4205
+ if (chatToolChoice) chatReq.tool_choice = chatToolChoice;
4206
+ payload = JSON.stringify(chatReq);
4207
+ upstreamUrl = new URL(useChatCompletions ? options.chatCompletionsEndpoint : options.responsesEndpoint);
4208
+ } else {
4209
+ const requestBody = parsedBody && typeof parsedBody === "object" && !Array.isArray(parsedBody) ? { ...parsedBody } : {};
4210
+ const sanitized = options.sanitizeResponsesRequest ? options.sanitizeResponsesRequest(requestBody) : requestBody;
4211
+ payload = JSON.stringify(sanitized);
4212
+ upstreamUrl = new URL(options.responsesEndpoint);
4213
+ }
4214
+ const requestFn = upstreamUrl.protocol === "http:" ? httpRequest : httpsRequest;
4215
+ const proxyReq = requestFn({
4216
+ hostname: upstreamUrl.hostname,
4217
+ port: upstreamUrl.port || (upstreamUrl.protocol === "http:" ? 80 : 443),
4218
+ path: upstreamUrl.pathname,
4219
+ method: "POST",
4220
+ headers: {
4221
+ "Content-Type": "application/json",
4222
+ "Content-Length": Buffer.byteLength(payload),
4223
+ "Authorization": `Bearer ${options.bearerToken}`
4224
+ }
4225
+ }, (upstreamRes) => {
4226
+ const status = upstreamRes.statusCode ?? 502;
4227
+ if (useChatCompletions && effectiveStreaming && status >= 200 && status < 300) {
4228
+ forwardStreamingTextResponse(upstreamRes, res, parsedBody.model);
4229
+ return;
4230
+ }
4231
+ const chunks = [];
4232
+ upstreamRes.on("data", (chunk) => chunks.push(chunk));
4233
+ upstreamRes.on("end", () => {
4234
+ const rawResponseBody = Buffer.concat(chunks).toString();
4235
+ if (!useChatCompletions) {
4236
+ res.writeHead(status, copyProxyHeaders(upstreamRes.headers));
4237
+ res.end(rawResponseBody);
4238
+ return;
4239
+ }
4240
+ try {
4241
+ const upstreamPayload = JSON.parse(rawResponseBody);
4242
+ if (upstreamPayload.error || status >= 400) {
4243
+ res.writeHead(status, { "Content-Type": "application/json" });
4244
+ res.end(JSON.stringify(upstreamPayload));
4245
+ return;
4246
+ }
4247
+ const translated = chatCompletionToResponsesFormat(upstreamPayload, parsedBody.model);
4248
+ if (isStreaming) {
4249
+ sendSyntheticStreamingCompletion(res, translated);
4250
+ } else {
4251
+ res.writeHead(200, { "Content-Type": "application/json" });
4252
+ res.end(JSON.stringify(translated));
4253
+ }
4254
+ } catch {
4255
+ const detail = rawResponseBody.slice(0, 500).trim();
4256
+ res.writeHead(status >= 400 ? status : 502, { "Content-Type": "application/json" });
4257
+ res.end(JSON.stringify({ error: { message: detail || "Bad gateway: failed to parse upstream response" } }));
4258
+ }
4259
+ });
4260
+ });
4261
+ proxyReq.on("error", (error) => {
4262
+ if (!res.headersSent) {
4263
+ res.writeHead(502, { "Content-Type": "application/json" });
4264
+ res.end(JSON.stringify({ error: { message: `Proxy error: ${error.message}` } }));
4265
+ }
4266
+ });
4267
+ proxyReq.write(payload);
4268
+ proxyReq.end();
4269
+ } catch (error) {
4270
+ if (!res.headersSent) {
4271
+ const message = error instanceof Error ? error.message : "Unknown error";
4272
+ res.writeHead(400, { "Content-Type": "application/json" });
4273
+ res.end(JSON.stringify({ error: { message } }));
4274
+ }
4275
+ }
4276
+ })();
4277
+ }
4278
+
4279
+ // src/server/openRouterProxy.ts
4280
+ var OPENROUTER_RESPONSES_ENDPOINT = "https://openrouter.ai/api/v1/responses";
4281
+ var OPENROUTER_CHAT_COMPLETIONS_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions";
4282
+ var OPENROUTER_ALLOWED_TOOL_TYPES = /* @__PURE__ */ new Set([
4283
+ "function",
4284
+ "openrouter:datetime",
4285
+ "openrouter:image_generation",
4286
+ "openrouter:experimental__search_models",
4287
+ "openrouter:web_search"
4288
+ ]);
4289
+ function sanitizeOpenRouterResponsesRequest(payload) {
4290
+ const requestBody = { ...payload };
4291
+ const rawTools = Array.isArray(requestBody.tools) ? requestBody.tools : null;
4292
+ if (!rawTools) return requestBody;
4293
+ const sanitizedTools = rawTools.filter((tool) => {
4294
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false;
4295
+ const type = typeof tool.type === "string" ? String(tool.type) : "";
4296
+ return OPENROUTER_ALLOWED_TOOL_TYPES.has(type);
4297
+ });
4298
+ if (sanitizedTools.length === 0) {
4299
+ delete requestBody.tools;
4300
+ delete requestBody.tool_choice;
4301
+ return requestBody;
4302
+ }
4303
+ requestBody.tools = sanitizedTools;
4304
+ return requestBody;
4305
+ }
4306
+ function handleOpenRouterProxyRequest(req, res, bearerToken, wireApi) {
4307
+ handleUnifiedResponsesProxyRequest(req, res, {
4308
+ bearerToken,
4309
+ wireApi,
4310
+ responsesEndpoint: OPENROUTER_RESPONSES_ENDPOINT,
4311
+ chatCompletionsEndpoint: OPENROUTER_CHAT_COMPLETIONS_ENDPOINT,
4312
+ missingKeyMessage: "Missing OpenRouter API key",
4313
+ allowToolFallbackToResponses: true,
4314
+ sanitizeResponsesRequest: sanitizeOpenRouterResponsesRequest
4315
+ });
4316
+ }
4317
+
4318
+ // src/server/zenProxy.ts
4319
+ var ZEN_RESPONSES_ENDPOINT = "https://opencode.ai/zen/v1/responses";
4320
+ var ZEN_CHAT_COMPLETIONS_ENDPOINT = "https://opencode.ai/zen/v1/chat/completions";
4321
+ function handleZenProxyRequest(req, res, bearerToken, wireApi) {
4322
+ handleUnifiedResponsesProxyRequest(req, res, {
4323
+ bearerToken,
4324
+ wireApi,
4325
+ responsesEndpoint: ZEN_RESPONSES_ENDPOINT,
4326
+ chatCompletionsEndpoint: ZEN_CHAT_COMPLETIONS_ENDPOINT,
4327
+ missingKeyMessage: "Missing OpenCode Zen API key",
4328
+ allowToolFallbackToResponses: false,
4329
+ responsesPayloadFormat: "chat"
4330
+ });
4331
+ }
4332
+
4333
+ // src/server/customEndpointProxy.ts
4334
+ function joinEndpoint(baseUrl, path) {
4335
+ return `${baseUrl.replace(/\/+$/u, "")}${path}`;
4336
+ }
4337
+ function handleCustomEndpointProxyRequest(req, res, options) {
4338
+ handleUnifiedResponsesProxyRequest(req, res, {
4339
+ bearerToken: options.bearerToken,
4340
+ wireApi: options.wireApi,
4341
+ responsesEndpoint: joinEndpoint(options.baseUrl, "/responses"),
4342
+ chatCompletionsEndpoint: joinEndpoint(options.baseUrl, "/chat/completions"),
4343
+ missingKeyMessage: "Missing custom endpoint API key",
4344
+ allowToolFallbackToResponses: false
4345
+ });
4346
+ }
4347
+
4348
+ // src/server/terminalManager.ts
4349
+ import { chmodSync, existsSync as existsSync3, lstatSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
4350
+ import { randomUUID } from "crypto";
4351
+ import { createRequire } from "module";
4352
+ import { basename as basename2, dirname, join as join5 } from "path";
4353
+ import { homedir as homedir4 } from "os";
3739
4354
  import { spawnSync as spawnSync2 } from "child_process";
3740
- import { basename as basename2, extname } from "path";
4355
+ var TERMINAL_BUFFER_LIMIT = 16 * 1024;
4356
+ var DEFAULT_COLS = 80;
4357
+ var DEFAULT_ROWS = 24;
4358
+ var TERMINAL_NAME = "xterm-256color";
4359
+ var require2 = createRequire(import.meta.url);
4360
+ var ThreadTerminalManager = class {
4361
+ constructor(options = {}) {
4362
+ this.sessions = /* @__PURE__ */ new Map();
4363
+ this.activeSessionIdByThreadId = /* @__PURE__ */ new Map();
4364
+ this.listeners = /* @__PURE__ */ new Set();
4365
+ this.spawn = options.spawn ?? loadTerminalSpawn();
4366
+ this.exists = options.exists ?? existsSync3;
4367
+ this.homeDir = options.homeDir ?? homedir4;
4368
+ this.cwd = options.cwd ?? process.cwd;
4369
+ this.platform = options.platform ?? process.platform;
4370
+ this.shell = options.shell ?? null;
4371
+ this.ensureSpawnHelperExecutable = options.ensureSpawnHelperExecutable ?? ensureNodePtyPrebuiltExecutable;
4372
+ }
4373
+ subscribe(listener) {
4374
+ this.listeners.add(listener);
4375
+ return () => {
4376
+ this.listeners.delete(listener);
4377
+ };
4378
+ }
4379
+ attach(params) {
4380
+ const threadId = params.threadId.trim();
4381
+ if (!threadId) {
4382
+ throw new Error("Missing threadId");
4383
+ }
4384
+ const requestedSessionId = params.sessionId?.trim() || "";
4385
+ const existingSessionId = params.newSession ? "" : requestedSessionId || this.activeSessionIdByThreadId.get(threadId) || "";
4386
+ const existing = existingSessionId ? this.sessions.get(existingSessionId) : null;
4387
+ if (existing) {
4388
+ this.activeSessionIdByThreadId.set(threadId, existing.id);
4389
+ this.resize(existing.id, params.cols, params.rows);
4390
+ const nextCwd = this.resolveCwd(params.cwd);
4391
+ if (nextCwd !== existing.cwd) {
4392
+ existing.cwd = nextCwd;
4393
+ existing.pty.write(`cd ${shellQuote(nextCwd)}\r`);
4394
+ }
4395
+ this.emitInit(existing);
4396
+ this.emitAttached(existing);
4397
+ return this.toSnapshot(existing);
4398
+ }
4399
+ const session = this.createSession({
4400
+ threadId,
4401
+ cwd: params.cwd,
4402
+ sessionId: requestedSessionId || randomUUID(),
4403
+ cols: params.cols,
4404
+ rows: params.rows
4405
+ });
4406
+ this.sessions.set(session.id, session);
4407
+ this.activeSessionIdByThreadId.set(threadId, session.id);
4408
+ this.emitAttached(session);
4409
+ return this.toSnapshot(session);
4410
+ }
4411
+ write(sessionId, data) {
4412
+ const session = this.requireSession(sessionId);
4413
+ session.pty.write(data);
4414
+ }
4415
+ resize(sessionId, cols, rows) {
4416
+ const session = this.sessions.get(sessionId);
4417
+ if (!session) return;
4418
+ const nextCols = normalizeDimension(cols, DEFAULT_COLS);
4419
+ const nextRows = normalizeDimension(rows, DEFAULT_ROWS);
4420
+ session.pty.resize(nextCols, nextRows);
4421
+ }
4422
+ close(sessionId) {
4423
+ const session = this.sessions.get(sessionId);
4424
+ if (!session) return;
4425
+ this.sessions.delete(session.id);
4426
+ if (this.activeSessionIdByThreadId.get(session.threadId) === session.id) {
4427
+ this.activeSessionIdByThreadId.delete(session.threadId);
4428
+ }
4429
+ session.pty.kill();
4430
+ this.emit({
4431
+ method: "terminal-exit",
4432
+ params: {
4433
+ sessionId: session.id,
4434
+ threadId: session.threadId,
4435
+ code: null,
4436
+ signal: null
4437
+ }
4438
+ });
4439
+ }
4440
+ getSnapshotForThread(threadId) {
4441
+ const sessionId = this.activeSessionIdByThreadId.get(threadId.trim());
4442
+ if (!sessionId) return null;
4443
+ const session = this.sessions.get(sessionId);
4444
+ return session ? this.toSnapshot(session) : null;
4445
+ }
4446
+ dispose() {
4447
+ for (const sessionId of Array.from(this.sessions.keys())) {
4448
+ this.close(sessionId);
4449
+ }
4450
+ this.listeners.clear();
4451
+ }
4452
+ createSession(params) {
4453
+ const cwd = this.resolveCwd(params.cwd);
4454
+ const shell = this.resolveShell();
4455
+ const env = {
4456
+ ...process.env,
4457
+ TERM: TERMINAL_NAME
4458
+ };
4459
+ normalizeLocaleEnv(env, this.platform);
4460
+ delete env.TERMINFO;
4461
+ delete env.TERMINFO_DIRS;
4462
+ this.ensureSpawnHelperExecutable();
4463
+ const pty = this.spawn(shell, [], {
4464
+ name: TERMINAL_NAME,
4465
+ cols: normalizeDimension(params.cols, DEFAULT_COLS),
4466
+ rows: normalizeDimension(params.rows, DEFAULT_ROWS),
4467
+ cwd,
4468
+ env
4469
+ });
4470
+ const session = {
4471
+ id: params.sessionId,
4472
+ threadId: params.threadId,
4473
+ cwd,
4474
+ shell: basename2(shell),
4475
+ pty,
4476
+ buffer: "",
4477
+ truncated: false
4478
+ };
4479
+ pty.onData((data) => {
4480
+ this.appendOutput(session, data);
4481
+ });
4482
+ pty.onExit(({ exitCode, signal }) => {
4483
+ if (this.sessions.get(session.id) === session) {
4484
+ this.sessions.delete(session.id);
4485
+ }
4486
+ if (this.activeSessionIdByThreadId.get(session.threadId) === session.id) {
4487
+ this.activeSessionIdByThreadId.delete(session.threadId);
4488
+ }
4489
+ this.emit({
4490
+ method: "terminal-exit",
4491
+ params: {
4492
+ sessionId: session.id,
4493
+ threadId: session.threadId,
4494
+ code: exitCode,
4495
+ signal: signal == null ? null : String(signal)
4496
+ }
4497
+ });
4498
+ });
4499
+ return session;
4500
+ }
4501
+ appendOutput(session, data) {
4502
+ const next = `${session.buffer}${data}`;
4503
+ if (next.length > TERMINAL_BUFFER_LIMIT) {
4504
+ session.buffer = next.slice(-TERMINAL_BUFFER_LIMIT);
4505
+ session.truncated = true;
4506
+ } else {
4507
+ session.buffer = next;
4508
+ }
4509
+ this.emit({
4510
+ method: "terminal-data",
4511
+ params: {
4512
+ sessionId: session.id,
4513
+ threadId: session.threadId,
4514
+ data
4515
+ }
4516
+ });
4517
+ }
4518
+ emitInit(session) {
4519
+ if (!session.buffer) return;
4520
+ this.emit({
4521
+ method: "terminal-init-log",
4522
+ params: {
4523
+ sessionId: session.id,
4524
+ threadId: session.threadId,
4525
+ log: session.buffer,
4526
+ truncated: session.truncated
4527
+ }
4528
+ });
4529
+ }
4530
+ emitAttached(session) {
4531
+ this.emit({
4532
+ method: "terminal-attached",
4533
+ params: {
4534
+ sessionId: session.id,
4535
+ threadId: session.threadId,
4536
+ cwd: session.cwd,
4537
+ shell: session.shell
4538
+ }
4539
+ });
4540
+ }
4541
+ emit(notification) {
4542
+ for (const listener of this.listeners) {
4543
+ listener(notification);
4544
+ }
4545
+ }
4546
+ requireSession(sessionId) {
4547
+ const session = this.sessions.get(sessionId.trim());
4548
+ if (!session) {
4549
+ throw new Error("Terminal session missing");
4550
+ }
4551
+ return session;
4552
+ }
4553
+ resolveShell() {
4554
+ if (this.shell) return this.shell;
4555
+ if (this.platform === "win32") {
4556
+ return process.env.COMSPEC || "cmd.exe";
4557
+ }
4558
+ return process.env.SHELL || "/bin/zsh";
4559
+ }
4560
+ resolveCwd(value) {
4561
+ const cwd = value.trim();
4562
+ if (cwd && this.exists(cwd)) {
4563
+ return cwd;
4564
+ }
4565
+ const home = this.homeDir();
4566
+ if (home && this.exists(home)) {
4567
+ return home;
4568
+ }
4569
+ return this.cwd();
4570
+ }
4571
+ toSnapshot(session) {
4572
+ return {
4573
+ id: session.id,
4574
+ threadId: session.threadId,
4575
+ cwd: session.cwd,
4576
+ shell: session.shell,
4577
+ buffer: session.buffer,
4578
+ truncated: session.truncated
4579
+ };
4580
+ }
4581
+ };
4582
+ function normalizeDimension(value, fallback) {
4583
+ const parsed = typeof value === "number" ? value : Number(value);
4584
+ if (!Number.isFinite(parsed)) return fallback;
4585
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
4586
+ }
4587
+ function loadTerminalSpawn() {
4588
+ repairNativePtyBuild("node-pty-prebuilt-multiarch");
4589
+ repairNativePtyBuild("node-pty");
4590
+ if (resolveNodePtyPrebuiltPath()) {
4591
+ try {
4592
+ const terminal2 = require2("node-pty-prebuilt-multiarch");
4593
+ return terminal2.spawn;
4594
+ } catch {
4595
+ }
4596
+ }
4597
+ const terminal = require2("node-pty");
4598
+ return terminal.spawn;
4599
+ }
4600
+ function repairNativePtyBuild(packageName) {
4601
+ try {
4602
+ const packageJson = require2.resolve(`${packageName}/package.json`);
4603
+ const packageRoot = dirname(packageJson);
4604
+ const buildDir = join5(packageRoot, "build");
4605
+ const makefile = join5(buildDir, "Makefile");
4606
+ const binary = join5(buildDir, "Release", "pty.node");
4607
+ if (!existsSync3(makefile) || !isBrokenSymlink(binary)) return;
4608
+ const source = readFileSync(makefile, "utf8");
4609
+ const patched = source.replace(
4610
+ /^cmd_copy = ln -f "\$<" "\$@" 2>\/dev\/null \|\| \(rm -rf "\$@" && cp -af "\$<" "\$@"\)$/m,
4611
+ 'cmd_copy = rm -rf "$@" && cp -af "$<" "$@"'
4612
+ );
4613
+ if (patched !== source) {
4614
+ writeFileSync(makefile, patched);
4615
+ }
4616
+ rmSync(binary, { force: true });
4617
+ spawnSync2("make", ["BUILDTYPE=Release", "-C", buildDir], { stdio: "ignore" });
4618
+ } catch {
4619
+ }
4620
+ }
4621
+ function isBrokenSymlink(path) {
4622
+ try {
4623
+ if (!lstatSync(path).isSymbolicLink()) return false;
4624
+ try {
4625
+ return !existsSync3(realpathSync(path));
4626
+ } catch {
4627
+ return true;
4628
+ }
4629
+ } catch {
4630
+ return false;
4631
+ }
4632
+ }
4633
+ function resolveNodePtyPrebuiltPath() {
4634
+ try {
4635
+ const packageJson = require2.resolve("node-pty-prebuilt-multiarch/package.json");
4636
+ const packageRoot = dirname(packageJson);
4637
+ const builtPath = join5(packageRoot, "build", "Release", "pty.node");
4638
+ if (existsSync3(builtPath)) {
4639
+ return builtPath;
4640
+ }
4641
+ const runtime = Object.prototype.hasOwnProperty.call(process.versions, "electron") ? "electron" : "node";
4642
+ const libc = process.platform === "linux" && existsSync3("/etc/alpine-release") ? ".musl" : "";
4643
+ const binaryName = `${runtime}.abi${process.versions.modules}${libc}.node`;
4644
+ const binaryPath = join5(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, binaryName);
4645
+ return existsSync3(binaryPath) ? binaryPath : null;
4646
+ } catch {
4647
+ return null;
4648
+ }
4649
+ }
4650
+ function ensureNodePtyPrebuiltExecutable() {
4651
+ if (process.platform !== "darwin" && process.platform !== "linux") return;
4652
+ try {
4653
+ const nodePtyEntry = require2.resolve("node-pty-prebuilt-multiarch");
4654
+ const packageRoot = join5(dirname(nodePtyEntry), "..");
4655
+ const helperPath = join5(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
4656
+ if (existsSync3(helperPath)) {
4657
+ chmodSync(helperPath, 493);
4658
+ }
4659
+ } catch {
4660
+ }
4661
+ }
4662
+ function normalizeLocaleEnv(env, platform) {
4663
+ const locale = platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8";
4664
+ env.LANG = locale;
4665
+ env.LC_ALL = locale;
4666
+ env.LC_CTYPE = locale;
4667
+ }
4668
+ function shellQuote(value) {
4669
+ return `'${value.replace(/'/g, `'\\''`)}'`;
4670
+ }
4671
+
4672
+ // src/utils/commandInvocation.ts
4673
+ import { spawnSync as spawnSync3 } from "child_process";
4674
+ import { basename as basename3, extname } from "path";
3741
4675
  var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
3742
4676
  function quoteCmdExeArg(value) {
3743
4677
  const normalized = value.replace(/"/g, '""');
@@ -3751,7 +4685,7 @@ function needsCmdExeWrapper(command) {
3751
4685
  return false;
3752
4686
  }
3753
4687
  const lowerCommand = command.toLowerCase();
3754
- const baseName = basename2(lowerCommand);
4688
+ const baseName = basename3(lowerCommand);
3755
4689
  if (/\.(cmd|bat)$/i.test(baseName)) {
3756
4690
  return true;
3757
4691
  }
@@ -3771,7 +4705,7 @@ function getSpawnInvocation(command, args = []) {
3771
4705
  }
3772
4706
  function spawnSyncCommand(command, args = [], options = {}) {
3773
4707
  const invocation = getSpawnInvocation(command, args);
3774
- return spawnSync2(invocation.command, invocation.args, options);
4708
+ return spawnSync3(invocation.command, invocation.args, options);
3775
4709
  }
3776
4710
 
3777
4711
  // src/server/codexAppServerBridge.ts
@@ -3787,7 +4721,7 @@ var DEFAULT_API_PERF_BODY_MB_THRESHOLD = 1;
3787
4721
  var MB_DIVISOR = 1024 * 1024;
3788
4722
  function readEnvValueFromFile(filePath, key) {
3789
4723
  try {
3790
- const content = readFileSync(filePath, "utf8");
4724
+ const content = readFileSync2(filePath, "utf8");
3791
4725
  const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3792
4726
  const match = content.match(new RegExp(`^\\s*${escapedKey}\\s*=\\s*(.+)\\s*$`, "m"));
3793
4727
  if (!match) return null;
@@ -3856,6 +4790,14 @@ function asRecord5(value) {
3856
4790
  function isInlineDataUrl(value) {
3857
4791
  return /^data:/iu.test(value.trim());
3858
4792
  }
4793
+ function normalizeBase64ImageDataUrl(value, mimeType) {
4794
+ const trimmed = value.trim();
4795
+ if (!trimmed) return null;
4796
+ if (isInlineDataUrl(trimmed)) return trimmed;
4797
+ const compact = trimmed.replace(/\s+/gu, "");
4798
+ if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(compact)) return null;
4799
+ return `data:${mimeType};base64,${compact}`;
4800
+ }
3859
4801
  function extensionFromMimeType(mimeType) {
3860
4802
  const normalized = mimeType.trim().toLowerCase();
3861
4803
  if (normalized === "image/png") return ".png";
@@ -3892,10 +4834,10 @@ async function persistInlineDataUrlToLocalFile(dataUrl, baseName) {
3892
4834
  if (bytes.length === 0) return null;
3893
4835
  const hash = createHash2("sha1").update(bytes).digest("hex");
3894
4836
  const ext = extensionFromMimeType(mimeType);
3895
- const mediaDir = join5(tmpdir4(), "codex-web-inline-media");
4837
+ const mediaDir = join6(tmpdir4(), "codex-web-inline-media");
3896
4838
  await mkdir4(mediaDir, { recursive: true });
3897
4839
  const fileName = `${baseName}-${hash}${ext}`;
3898
- const filePath = join5(mediaDir, fileName);
4840
+ const filePath = join6(mediaDir, fileName);
3899
4841
  try {
3900
4842
  await stat4(filePath);
3901
4843
  } catch {
@@ -3926,6 +4868,21 @@ async function sanitizeInlineUserContentBlock(block, context) {
3926
4868
  text: `Image attachment: ${target}`
3927
4869
  };
3928
4870
  }
4871
+ if (type === "imageGeneration" || type === "image_generation") {
4872
+ const rawResult = asNonEmptyString(record.result) ?? asNonEmptyString(record.b64_json) ?? asNonEmptyString(record.image);
4873
+ const mimeType = asNonEmptyString(record.mime_type) ?? asNonEmptyString(record.mimeType) ?? "image/png";
4874
+ const dataUrl = rawResult ? normalizeBase64ImageDataUrl(rawResult, mimeType) : null;
4875
+ if (dataUrl) {
4876
+ const localUrl = await persistInlineDataUrlToLocalFile(dataUrl, `generated-image-${context.turnId}-${context.itemId}`);
4877
+ if (localUrl) {
4878
+ return {
4879
+ ...record,
4880
+ type: "imageView",
4881
+ path: localUrl
4882
+ };
4883
+ }
4884
+ }
4885
+ }
3929
4886
  const inlineFileData = asNonEmptyString(record.file_data) ?? asNonEmptyString(record.data) ?? asNonEmptyString(record.base64);
3930
4887
  if ((type.includes("file") || type === "input_file" || type === "file") && inlineFileData) {
3931
4888
  const mimeType = asNonEmptyString(record.mime_type) ?? "application/octet-stream";
@@ -4119,110 +5076,123 @@ function normalizeProviderModelsData(payload) {
4119
5076
  if (!candidate || ids.includes(candidate)) continue;
4120
5077
  ids.push(candidate);
4121
5078
  }
4122
- return ids;
5079
+ return ids;
5080
+ }
5081
+ async function fetchCustomEndpointDefaultModel(baseUrl, apiKey) {
5082
+ const normalizedBaseUrl = baseUrl.trim();
5083
+ if (!normalizedBaseUrl) return "";
5084
+ try {
5085
+ const modelsUrl = buildProviderModelsUrl(normalizedBaseUrl, null);
5086
+ const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
5087
+ const response = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS) });
5088
+ if (!response.ok) return "";
5089
+ const payload = await response.json();
5090
+ const modelIds = normalizeProviderModelsData(payload);
5091
+ return modelIds[0] ?? "";
5092
+ } catch {
5093
+ return "";
5094
+ }
4123
5095
  }
4124
5096
  async function readProviderBackedModelIds(appServer) {
4125
- return Sentry4.startSpan({ name: "readProviderBackedModelIds", op: "function.slow" }, async () => {
4126
- const configPayload = asRecord5(await appServer.rpc("config/read", {}));
4127
- const config = asRecord5(configPayload?.config);
4128
- const providerId = readNonEmptyString(config?.model_provider);
4129
- if (!providerId) {
4130
- return { data: [], providerId: "", source: "provider" };
4131
- }
4132
- const providers = asRecord5(config?.model_providers);
4133
- const provider = asRecord5(providers?.[providerId]);
4134
- if (!provider) {
4135
- logProviderModelDiscoveryWarning("configured provider is missing from model_providers", { providerId });
4136
- return { data: [], providerId, source: "provider" };
4137
- }
4138
- const wireApi = readNonEmptyString(provider.wire_api);
4139
- if (wireApi !== "responses") {
4140
- return { data: [], providerId, source: "provider" };
4141
- }
4142
- const baseUrl = readNonEmptyString(provider.base_url);
4143
- if (!baseUrl) {
4144
- logProviderModelDiscoveryWarning("responses provider is missing base_url", { providerId });
4145
- return { data: [], providerId, source: "provider" };
4146
- }
4147
- const headers = new Headers();
4148
- const configuredHeaders = asRecord5(provider.http_headers);
4149
- if (configuredHeaders) {
4150
- for (const [key, rawValue] of Object.entries(configuredHeaders)) {
4151
- const normalized = normalizeHeaderValue(rawValue);
4152
- if (!normalized) continue;
4153
- headers.set(key, normalized);
4154
- }
4155
- }
4156
- const bearerToken = readNonEmptyString(provider.experimental_bearer_token);
4157
- if (bearerToken && !headers.has("Authorization")) {
4158
- headers.set("Authorization", `Bearer ${bearerToken}`);
4159
- }
4160
- const envKey = readNonEmptyString(provider.env_key);
4161
- const envHttpHeaders = asRecord5(provider.env_http_headers);
4162
- if (envKey || envHttpHeaders) {
4163
- logProviderModelDiscoveryWarning("provider discovery skipped env-backed auth/header expansion", {
4164
- providerId,
4165
- hasEnvKey: Boolean(envKey),
4166
- hasEnvHttpHeaders: Boolean(envHttpHeaders)
4167
- });
4168
- }
4169
- let requestUrl;
4170
- try {
4171
- requestUrl = buildProviderModelsUrl(baseUrl, provider.query_params);
4172
- } catch (error) {
4173
- logProviderModelDiscoveryWarning("provider /models URL was invalid", {
4174
- providerId,
4175
- error: getErrorMessage5(error, "invalid url")
4176
- });
4177
- return { data: [], providerId, source: "provider" };
4178
- }
4179
- let response;
4180
- try {
4181
- response = await fetch(requestUrl, {
4182
- method: "GET",
4183
- headers,
4184
- signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS)
4185
- });
4186
- } catch (error) {
4187
- logProviderModelDiscoveryWarning("provider /models request failed", {
4188
- providerId,
4189
- error: isTimeoutError(error) ? `request timed out after ${PROVIDER_MODELS_FETCH_TIMEOUT_MS}ms` : getErrorMessage5(error, "network error")
4190
- });
4191
- return { data: [], providerId, source: "provider" };
4192
- }
4193
- let payload = null;
4194
- try {
4195
- payload = await response.json();
4196
- } catch (error) {
4197
- logProviderModelDiscoveryWarning("provider /models response was not valid JSON", {
4198
- providerId,
4199
- status: response.status,
4200
- error: getErrorMessage5(error, "invalid json")
4201
- });
4202
- return { data: [], providerId, source: "provider" };
4203
- }
4204
- if (!response.ok) {
4205
- logProviderModelDiscoveryWarning("provider /models request returned non-2xx", {
4206
- providerId,
4207
- status: response.status,
4208
- statusText: response.statusText
4209
- });
4210
- return { data: [], providerId, source: "provider" };
4211
- }
4212
- try {
4213
- return {
4214
- data: normalizeProviderModelsData(payload),
4215
- providerId,
4216
- source: "provider"
4217
- };
4218
- } catch (error) {
4219
- logProviderModelDiscoveryWarning("provider /models payload was invalid", {
4220
- providerId,
4221
- error: getErrorMessage5(error, "invalid payload")
4222
- });
4223
- return { data: [], providerId, source: "provider" };
4224
- }
4225
- });
5097
+ const configPayload = asRecord5(await appServer.rpc("config/read", {}));
5098
+ const config = asRecord5(configPayload?.config);
5099
+ const providerId = readNonEmptyString(config?.model_provider);
5100
+ if (!providerId) {
5101
+ return { data: [], providerId: "", source: "provider" };
5102
+ }
5103
+ const providers = asRecord5(config?.model_providers);
5104
+ const provider = asRecord5(providers?.[providerId]);
5105
+ if (!provider) {
5106
+ logProviderModelDiscoveryWarning("configured provider is missing from model_providers", { providerId });
5107
+ return { data: [], providerId, source: "provider" };
5108
+ }
5109
+ const wireApi = readNonEmptyString(provider.wire_api);
5110
+ if (wireApi !== "responses") {
5111
+ return { data: [], providerId, source: "provider" };
5112
+ }
5113
+ const baseUrl = readNonEmptyString(provider.base_url);
5114
+ if (!baseUrl) {
5115
+ logProviderModelDiscoveryWarning("responses provider is missing base_url", { providerId });
5116
+ return { data: [], providerId, source: "provider" };
5117
+ }
5118
+ const headers = new Headers();
5119
+ const configuredHeaders = asRecord5(provider.http_headers);
5120
+ if (configuredHeaders) {
5121
+ for (const [key, rawValue] of Object.entries(configuredHeaders)) {
5122
+ const normalized = normalizeHeaderValue(rawValue);
5123
+ if (!normalized) continue;
5124
+ headers.set(key, normalized);
5125
+ }
5126
+ }
5127
+ const bearerToken = readNonEmptyString(provider.experimental_bearer_token);
5128
+ if (bearerToken && !headers.has("Authorization")) {
5129
+ headers.set("Authorization", `Bearer ${bearerToken}`);
5130
+ }
5131
+ const envKey = readNonEmptyString(provider.env_key);
5132
+ const envHttpHeaders = asRecord5(provider.env_http_headers);
5133
+ if (envKey || envHttpHeaders) {
5134
+ logProviderModelDiscoveryWarning("provider discovery skipped env-backed auth/header expansion", {
5135
+ providerId,
5136
+ hasEnvKey: Boolean(envKey),
5137
+ hasEnvHttpHeaders: Boolean(envHttpHeaders)
5138
+ });
5139
+ }
5140
+ let requestUrl;
5141
+ try {
5142
+ requestUrl = buildProviderModelsUrl(baseUrl, provider.query_params);
5143
+ } catch (error) {
5144
+ logProviderModelDiscoveryWarning("provider /models URL was invalid", {
5145
+ providerId,
5146
+ error: getErrorMessage5(error, "invalid url")
5147
+ });
5148
+ return { data: [], providerId, source: "provider" };
5149
+ }
5150
+ let response;
5151
+ try {
5152
+ response = await fetch(requestUrl, {
5153
+ method: "GET",
5154
+ headers,
5155
+ signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS)
5156
+ });
5157
+ } catch (error) {
5158
+ logProviderModelDiscoveryWarning("provider /models request failed", {
5159
+ providerId,
5160
+ error: isTimeoutError(error) ? `request timed out after ${PROVIDER_MODELS_FETCH_TIMEOUT_MS}ms` : getErrorMessage5(error, "network error")
5161
+ });
5162
+ return { data: [], providerId, source: "provider" };
5163
+ }
5164
+ let payload = null;
5165
+ try {
5166
+ payload = await response.json();
5167
+ } catch (error) {
5168
+ logProviderModelDiscoveryWarning("provider /models response was not valid JSON", {
5169
+ providerId,
5170
+ status: response.status,
5171
+ error: getErrorMessage5(error, "invalid json")
5172
+ });
5173
+ return { data: [], providerId, source: "provider" };
5174
+ }
5175
+ if (!response.ok) {
5176
+ logProviderModelDiscoveryWarning("provider /models request returned non-2xx", {
5177
+ providerId,
5178
+ status: response.status,
5179
+ statusText: response.statusText
5180
+ });
5181
+ return { data: [], providerId, source: "provider" };
5182
+ }
5183
+ try {
5184
+ return {
5185
+ data: normalizeProviderModelsData(payload),
5186
+ providerId,
5187
+ source: "provider"
5188
+ };
5189
+ } catch (error) {
5190
+ logProviderModelDiscoveryWarning("provider /models payload was invalid", {
5191
+ providerId,
5192
+ error: getErrorMessage5(error, "invalid payload")
5193
+ });
5194
+ return { data: [], providerId, source: "provider" };
5195
+ }
4226
5196
  }
4227
5197
  function extractThreadMessageText(threadReadPayload) {
4228
5198
  const payload = asRecord5(threadReadPayload);
@@ -4573,7 +5543,7 @@ function extractFilePathsFromCommand(cmd, cwd) {
4573
5543
  while ((match = redirectPattern.exec(cmd)) !== null) {
4574
5544
  const p = match[1]?.trim();
4575
5545
  if (p && !p.startsWith("-") && !p.startsWith("/dev/")) {
4576
- paths.push(isAbsolute2(p) ? p : join5(cwd, p));
5546
+ paths.push(isAbsolute2(p) ? p : join6(cwd, p));
4577
5547
  }
4578
5548
  }
4579
5549
  return [...new Set(paths)];
@@ -4707,7 +5677,7 @@ async function revertTurnFileChanges(cwd, turnInfos) {
4707
5677
  try {
4708
5678
  const tracked = await runCommandCapture2("git", ["ls-files", "--full-name"], { cwd: gitRoot });
4709
5679
  for (const f of tracked.split("\n")) {
4710
- if (f.trim()) trackedFiles.add(join5(gitRoot, f.trim()));
5680
+ if (f.trim()) trackedFiles.add(join6(gitRoot, f.trim()));
4711
5681
  }
4712
5682
  } catch {
4713
5683
  }
@@ -4717,7 +5687,7 @@ async function revertTurnFileChanges(cwd, turnInfos) {
4717
5687
  const changes = parseApplyPatchInput(patch.input);
4718
5688
  for (let ci = changes.length - 1; ci >= 0; ci--) {
4719
5689
  const change = changes[ci];
4720
- const filePath = isAbsolute2(change.path) ? change.path : join5(cwd, change.path);
5690
+ const filePath = isAbsolute2(change.path) ? change.path : join6(cwd, change.path);
4721
5691
  try {
4722
5692
  if (change.operation === "add") {
4723
5693
  const fileStat = await stat4(filePath).catch(() => null);
@@ -4935,7 +5905,7 @@ async function listFilesWithRipgrep(cwd) {
4935
5905
  }
4936
5906
  function getCodexHomeDir3() {
4937
5907
  const codexHome = process.env.CODEX_HOME?.trim();
4938
- return codexHome && codexHome.length > 0 ? codexHome : join5(homedir4(), ".codex");
5908
+ return codexHome && codexHome.length > 0 ? codexHome : join6(homedir5(), ".codex");
4939
5909
  }
4940
5910
  async function runCommand3(command, args, options = {}) {
4941
5911
  await new Promise((resolve4, reject) => {
@@ -4973,7 +5943,7 @@ function isNotGitRepositoryError2(error) {
4973
5943
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
4974
5944
  }
4975
5945
  async function ensureRepoHasInitialCommit(repoRoot) {
4976
- const agentsPath = join5(repoRoot, "AGENTS.md");
5946
+ const agentsPath = join6(repoRoot, "AGENTS.md");
4977
5947
  try {
4978
5948
  await stat4(agentsPath);
4979
5949
  } catch {
@@ -5049,7 +6019,7 @@ function normalizeStringRecord(value) {
5049
6019
  return next;
5050
6020
  }
5051
6021
  function getCodexAuthPath() {
5052
- return join5(getCodexHomeDir3(), "auth.json");
6022
+ return join6(getCodexHomeDir3(), "auth.json");
5053
6023
  }
5054
6024
  async function readCodexAuth() {
5055
6025
  try {
@@ -5063,13 +6033,157 @@ async function readCodexAuth() {
5063
6033
  }
5064
6034
  }
5065
6035
  function getCodexGlobalStatePath() {
5066
- return join5(getCodexHomeDir3(), ".codex-global-state.json");
6036
+ return join6(getCodexHomeDir3(), ".codex-global-state.json");
5067
6037
  }
5068
6038
  function getTelegramBridgeConfigPath() {
5069
- return join5(getCodexHomeDir3(), "telegram-bridge.json");
6039
+ return join6(getCodexHomeDir3(), "telegram-bridge.json");
5070
6040
  }
5071
6041
  function getCodexSessionIndexPath() {
5072
- return join5(getCodexHomeDir3(), "session_index.jsonl");
6042
+ return join6(getCodexHomeDir3(), "session_index.jsonl");
6043
+ }
6044
+ function getCodexAutomationsDir() {
6045
+ return join6(getCodexHomeDir3(), "automations");
6046
+ }
6047
+ function readTomlString(value) {
6048
+ const trimmed = value.trim();
6049
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
6050
+ try {
6051
+ return JSON.parse(trimmed);
6052
+ } catch {
6053
+ return trimmed.slice(1, -1);
6054
+ }
6055
+ }
6056
+ return trimmed;
6057
+ }
6058
+ function serializeTomlString(value) {
6059
+ return JSON.stringify(value);
6060
+ }
6061
+ function parseAutomationToml(raw) {
6062
+ const values = {};
6063
+ for (const line of raw.split(/\r?\n/u)) {
6064
+ const trimmed = line.trim();
6065
+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
6066
+ const separatorIndex = trimmed.indexOf("=");
6067
+ const key = trimmed.slice(0, separatorIndex).trim();
6068
+ const value = trimmed.slice(separatorIndex + 1).trim();
6069
+ if (key) values[key] = value;
6070
+ }
6071
+ const id = readTomlString(values.id ?? "");
6072
+ const kindValue = readTomlString(values.kind ?? "heartbeat");
6073
+ const name = readTomlString(values.name ?? "");
6074
+ const prompt = readTomlString(values.prompt ?? "");
6075
+ const rrule = readTomlString(values.rrule ?? "");
6076
+ const statusValue = readTomlString(values.status ?? "ACTIVE");
6077
+ const targetThreadId = readTomlString(values.target_thread_id ?? "") || null;
6078
+ const createdAtMs = Number.parseInt(values.created_at ?? "", 10);
6079
+ const updatedAtMs = Number.parseInt(values.updated_at ?? "", 10);
6080
+ if (!id || !name || !prompt || !rrule) return null;
6081
+ if (kindValue !== "heartbeat" && kindValue !== "cron") return null;
6082
+ if (statusValue !== "ACTIVE" && statusValue !== "PAUSED") return null;
6083
+ return {
6084
+ id,
6085
+ kind: kindValue,
6086
+ name,
6087
+ prompt,
6088
+ rrule,
6089
+ status: statusValue,
6090
+ targetThreadId,
6091
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
6092
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
6093
+ nextRunAtMs: null
6094
+ };
6095
+ }
6096
+ function serializeAutomationToml(record) {
6097
+ const lines = [
6098
+ "version = 1",
6099
+ `id = ${serializeTomlString(record.id)}`,
6100
+ `kind = ${serializeTomlString(record.kind)}`,
6101
+ `name = ${serializeTomlString(record.name)}`,
6102
+ `prompt = ${serializeTomlString(record.prompt)}`,
6103
+ `status = ${serializeTomlString(record.status)}`,
6104
+ `rrule = ${serializeTomlString(record.rrule)}`,
6105
+ `target_thread_id = ${serializeTomlString(record.targetThreadId ?? "")}`,
6106
+ `created_at = ${String(record.createdAtMs ?? Date.now())}`,
6107
+ `updated_at = ${String(record.updatedAtMs ?? Date.now())}`
6108
+ ];
6109
+ return `${lines.join("\n")}
6110
+ `;
6111
+ }
6112
+ function slugifyAutomationId(threadId, name) {
6113
+ const preferred = name.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
6114
+ if (preferred) return preferred.slice(0, 48);
6115
+ const fallback = threadId.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
6116
+ return `heartbeat-${fallback.slice(0, 24) || randomBytes(4).toString("hex")}`;
6117
+ }
6118
+ async function readAutomationRecordFromFile(filePath) {
6119
+ try {
6120
+ return parseAutomationToml(await readFile3(filePath, "utf8"));
6121
+ } catch {
6122
+ return null;
6123
+ }
6124
+ }
6125
+ async function listThreadHeartbeatAutomations() {
6126
+ const automationRoot = getCodexAutomationsDir();
6127
+ const next = {};
6128
+ let entries;
6129
+ try {
6130
+ entries = await readdir2(automationRoot, { withFileTypes: true });
6131
+ } catch {
6132
+ return next;
6133
+ }
6134
+ for (const entry of entries) {
6135
+ if (!entry.isDirectory()) continue;
6136
+ const automation = await readAutomationRecordFromFile(join6(automationRoot, entry.name, "automation.toml"));
6137
+ if (!automation || automation.kind !== "heartbeat" || !automation.targetThreadId) continue;
6138
+ next[automation.targetThreadId] = automation;
6139
+ }
6140
+ return next;
6141
+ }
6142
+ async function readThreadHeartbeatAutomation(threadId) {
6143
+ const all = await listThreadHeartbeatAutomations();
6144
+ return all[threadId] ?? null;
6145
+ }
6146
+ async function writeThreadHeartbeatAutomation(input) {
6147
+ const threadId = input.threadId.trim();
6148
+ const name = input.name.trim();
6149
+ const prompt = input.prompt.trim();
6150
+ const rrule = input.rrule.trim();
6151
+ if (!threadId || !name || !prompt || !rrule) {
6152
+ throw new Error("threadId, name, prompt, and rrule are required");
6153
+ }
6154
+ const automationRoot = getCodexAutomationsDir();
6155
+ await mkdir4(automationRoot, { recursive: true });
6156
+ const existing = await readThreadHeartbeatAutomation(threadId);
6157
+ const id = existing?.id ?? slugifyAutomationId(threadId, name);
6158
+ const automationDir = join6(automationRoot, id);
6159
+ const now = Date.now();
6160
+ const record = {
6161
+ id,
6162
+ kind: "heartbeat",
6163
+ name,
6164
+ prompt,
6165
+ rrule,
6166
+ status: input.status,
6167
+ targetThreadId: threadId,
6168
+ createdAtMs: existing?.createdAtMs ?? now,
6169
+ updatedAtMs: now,
6170
+ nextRunAtMs: null
6171
+ };
6172
+ await mkdir4(automationDir, { recursive: true });
6173
+ await writeFile4(join6(automationDir, "automation.toml"), serializeAutomationToml(record), "utf8");
6174
+ const memoryPath = join6(automationDir, "memory.md");
6175
+ try {
6176
+ await stat4(memoryPath);
6177
+ } catch {
6178
+ await writeFile4(memoryPath, "", "utf8");
6179
+ }
6180
+ return record;
6181
+ }
6182
+ async function deleteThreadHeartbeatAutomation(threadId) {
6183
+ const automation = await readThreadHeartbeatAutomation(threadId.trim());
6184
+ if (!automation) return false;
6185
+ await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
6186
+ return true;
5073
6187
  }
5074
6188
  var MAX_THREAD_TITLES = 500;
5075
6189
  var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
@@ -5409,10 +6523,10 @@ function handleFileUpload(req, res) {
5409
6523
  setJson4(res, 400, { error: "No file in request" });
5410
6524
  return;
5411
6525
  }
5412
- const uploadDir = join5(tmpdir4(), "codex-web-uploads");
6526
+ const uploadDir = join6(tmpdir4(), "codex-web-uploads");
5413
6527
  await mkdir4(uploadDir, { recursive: true });
5414
- const destDir = await mkdtemp3(join5(uploadDir, "f-"));
5415
- const destPath = join5(destDir, fileName);
6528
+ const destDir = await mkdtemp3(join6(uploadDir, "f-"));
6529
+ const destPath = join6(destDir, fileName);
5416
6530
  await writeFile4(destPath, fileData);
5417
6531
  setJson4(res, 200, { path: destPath });
5418
6532
  } catch (err) {
@@ -5424,7 +6538,7 @@ function handleFileUpload(req, res) {
5424
6538
  });
5425
6539
  }
5426
6540
  function httpPost(url, headers, body) {
5427
- const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
6541
+ const doRequest = url.startsWith("http://") ? httpRequest2 : httpsRequest2;
5428
6542
  return new Promise((resolve4, reject) => {
5429
6543
  const req = doRequest(url, { method: "POST", headers }, (res) => {
5430
6544
  const chunks = [];
@@ -5470,34 +6584,32 @@ function curlImpersonatePost(url, headers, body) {
5470
6584
  });
5471
6585
  }
5472
6586
  async function proxyTranscribe(body, contentType, authToken, accountId) {
5473
- return Sentry4.startSpan({ name: "proxyTranscribe", op: "http.client.transcribe" }, async () => {
5474
- const chatgptHeaders = {
5475
- "Content-Type": contentType,
5476
- "Content-Length": body.length,
5477
- Authorization: `Bearer ${authToken}`,
5478
- originator: "Codex Desktop",
5479
- "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
5480
- };
5481
- if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
5482
- const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
5483
- let result;
5484
- try {
5485
- result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
5486
- } catch {
5487
- result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
5488
- }
5489
- if (result.status === 403 && result.body.includes("cf_chl")) {
5490
- if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
5491
- try {
5492
- const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
5493
- if (ciResult.status !== 403) return ciResult;
5494
- } catch {
5495
- }
6587
+ const chatgptHeaders = {
6588
+ "Content-Type": contentType,
6589
+ "Content-Length": body.length,
6590
+ Authorization: `Bearer ${authToken}`,
6591
+ originator: "Codex Desktop",
6592
+ "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
6593
+ };
6594
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
6595
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
6596
+ let result;
6597
+ try {
6598
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
6599
+ } catch {
6600
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
6601
+ }
6602
+ if (result.status === 403 && result.body.includes("cf_chl")) {
6603
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
6604
+ try {
6605
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
6606
+ if (ciResult.status !== 403) return ciResult;
6607
+ } catch {
5496
6608
  }
5497
- return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
5498
6609
  }
5499
- return result;
5500
- });
6610
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
6611
+ }
6612
+ return result;
5501
6613
  }
5502
6614
  var STREAM_EVENT_BUFFER_LIMIT = 400;
5503
6615
  var MERGEABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
@@ -5528,7 +6640,7 @@ var AppServerProcess = class {
5528
6640
  }
5529
6641
  return codexCommand;
5530
6642
  }
5531
- buildAppServerArgs() {
6643
+ buildAppServerConfig() {
5532
6644
  const args = [
5533
6645
  "app-server",
5534
6646
  "-c",
@@ -5536,20 +6648,25 @@ var AppServerProcess = class {
5536
6648
  "-c",
5537
6649
  'sandbox_mode="danger-full-access"'
5538
6650
  ];
5539
- const statePath = join5(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
6651
+ let extraEnv = {};
6652
+ const serverPort = parseInt(process.env.CODEXUI_SERVER_PORT ?? "", 10) || void 0;
6653
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
5540
6654
  try {
5541
- const raw = readFileSync(statePath, "utf8");
6655
+ const raw = readFileSync2(statePath, "utf8");
5542
6656
  const state = JSON.parse(raw);
5543
- args.push(...getFreeModeConfigArgs(state));
6657
+ args.push(...getFreeModeConfigArgs(state, serverPort));
6658
+ extraEnv = getFreeModeEnvVars(state);
5544
6659
  } catch {
5545
6660
  }
5546
- return args;
6661
+ return { args, env: extraEnv };
5547
6662
  }
5548
6663
  start() {
5549
6664
  if (this.process) return;
5550
6665
  this.stopping = false;
5551
- const invocation = getSpawnInvocation(this.getCodexCommand(), this.buildAppServerArgs());
5552
- const proc = spawn4(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
6666
+ const config = this.buildAppServerConfig();
6667
+ const invocation = getSpawnInvocation(this.getCodexCommand(), config.args);
6668
+ const spawnEnv = Object.keys(config.env).length > 0 ? { ...process.env, ...config.env } : void 0;
6669
+ const proc = spawn4(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"], ...spawnEnv ? { env: spawnEnv } : {} });
5553
6670
  this.process = proc;
5554
6671
  proc.stdout.setEncoding("utf8");
5555
6672
  proc.stdout.on("data", (chunk) => {
@@ -5830,7 +6947,7 @@ var AppServerProcess = class {
5830
6947
  }
5831
6948
  async rpc(method, params) {
5832
6949
  await this.ensureInitialized();
5833
- return Sentry4.startSpan({ name: `rpc ${method}`, op: "rpc.codex" }, () => this.call(method, params));
6950
+ return this.call(method, params);
5834
6951
  }
5835
6952
  onNotification(listener) {
5836
6953
  this.notificationListeners.add(listener);
@@ -5965,9 +7082,9 @@ var MethodCatalog = class {
5965
7082
  if (this.methodCache) {
5966
7083
  return this.methodCache;
5967
7084
  }
5968
- const outDir = await mkdtemp3(join5(tmpdir4(), "codex-web-local-schema-"));
7085
+ const outDir = await mkdtemp3(join6(tmpdir4(), "codex-web-local-schema-"));
5969
7086
  await this.runGenerateSchemaCommand(outDir);
5970
- const clientRequestPath = join5(outDir, "ClientRequest.json");
7087
+ const clientRequestPath = join6(outDir, "ClientRequest.json");
5971
7088
  const raw = await readFile3(clientRequestPath, "utf8");
5972
7089
  const parsed = JSON.parse(raw);
5973
7090
  const methods = this.extractMethodsFromClientRequest(parsed);
@@ -5978,9 +7095,9 @@ var MethodCatalog = class {
5978
7095
  if (this.notificationCache) {
5979
7096
  return this.notificationCache;
5980
7097
  }
5981
- const outDir = await mkdtemp3(join5(tmpdir4(), "codex-web-local-schema-"));
7098
+ const outDir = await mkdtemp3(join6(tmpdir4(), "codex-web-local-schema-"));
5982
7099
  await this.runGenerateSchemaCommand(outDir);
5983
- const serverNotificationPath = join5(outDir, "ServerNotification.json");
7100
+ const serverNotificationPath = join6(outDir, "ServerNotification.json");
5984
7101
  const raw = await readFile3(serverNotificationPath, "utf8");
5985
7102
  const parsed = JSON.parse(raw);
5986
7103
  const methods = this.extractMethodsFromServerNotification(parsed);
@@ -5994,15 +7111,18 @@ function getSharedBridgeState() {
5994
7111
  const globalScope = globalThis;
5995
7112
  const existing = globalScope[SHARED_BRIDGE_KEY];
5996
7113
  if (existing) {
5997
- if (existing.version === SHARED_BRIDGE_VERSION) {
7114
+ if (existing.version === SHARED_BRIDGE_VERSION && existing.terminalManager) {
5998
7115
  return existing;
5999
7116
  }
6000
7117
  existing.appServer.dispose();
7118
+ existing.terminalManager?.dispose();
6001
7119
  }
6002
7120
  const appServer = new AppServerProcess();
7121
+ const terminalManager = new ThreadTerminalManager();
6003
7122
  const created = {
6004
7123
  version: SHARED_BRIDGE_VERSION,
6005
7124
  appServer,
7125
+ terminalManager,
6006
7126
  methodCatalog: new MethodCatalog(),
6007
7127
  telegramBridge: new TelegramThreadBridge(appServer, {
6008
7128
  onChatSeen: (chatId) => {
@@ -6015,69 +7135,67 @@ function getSharedBridgeState() {
6015
7135
  return created;
6016
7136
  }
6017
7137
  async function loadAllThreadsForSearch(appServer) {
6018
- return Sentry4.startSpan({ name: "loadAllThreadsForSearch", op: "search.index" }, async () => {
6019
- const threads = [];
6020
- let cursor = null;
6021
- do {
6022
- const response = asRecord5(await appServer.rpc("thread/list", {
6023
- archived: false,
6024
- limit: 100,
6025
- sortKey: "updated_at",
6026
- modelProviders: [],
6027
- cursor
6028
- }));
6029
- const data = Array.isArray(response?.data) ? response.data : [];
6030
- for (const row of data) {
6031
- const record = asRecord5(row);
6032
- const id = typeof record?.id === "string" ? record.id : "";
6033
- if (!id) continue;
6034
- const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
6035
- const preview = typeof record?.preview === "string" ? record.preview : "";
6036
- threads.push({ id, title, preview });
6037
- }
6038
- cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
6039
- } while (cursor);
6040
- const docs = threads.map((thread) => {
6041
- const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
6042
- return {
6043
- id: thread.id,
6044
- title: thread.title,
6045
- preview: thread.preview,
6046
- messageText: "",
6047
- searchableText
6048
- };
6049
- });
6050
- const docsById = new Map(docs.map((doc) => [doc.id, doc]));
6051
- const fullTextThreads = threads.slice(0, THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT);
6052
- const concurrency = 4;
6053
- for (let offset = 0; offset < fullTextThreads.length; offset += concurrency) {
6054
- const batch = fullTextThreads.slice(offset, offset + concurrency);
6055
- const loaded = await Promise.all(batch.map(async (thread) => {
6056
- try {
6057
- const readResponse = await appServer.rpc("thread/read", {
6058
- threadId: thread.id,
6059
- includeTurns: true
6060
- });
6061
- const messageText = extractThreadMessageText(readResponse);
6062
- const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
6063
- return [thread.id, {
6064
- id: thread.id,
6065
- title: thread.title,
6066
- preview: thread.preview,
6067
- messageText,
6068
- searchableText
6069
- }];
6070
- } catch {
6071
- return null;
6072
- }
6073
- }));
6074
- for (const row of loaded) {
6075
- if (!row) continue;
6076
- docsById.set(row[0], row[1]);
7138
+ const threads = [];
7139
+ let cursor = null;
7140
+ do {
7141
+ const response = asRecord5(await appServer.rpc("thread/list", {
7142
+ archived: false,
7143
+ limit: 100,
7144
+ sortKey: "updated_at",
7145
+ modelProviders: [],
7146
+ cursor
7147
+ }));
7148
+ const data = Array.isArray(response?.data) ? response.data : [];
7149
+ for (const row of data) {
7150
+ const record = asRecord5(row);
7151
+ const id = typeof record?.id === "string" ? record.id : "";
7152
+ if (!id) continue;
7153
+ const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
7154
+ const preview = typeof record?.preview === "string" ? record.preview : "";
7155
+ threads.push({ id, title, preview });
7156
+ }
7157
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
7158
+ } while (cursor);
7159
+ const docs = threads.map((thread) => {
7160
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
7161
+ return {
7162
+ id: thread.id,
7163
+ title: thread.title,
7164
+ preview: thread.preview,
7165
+ messageText: "",
7166
+ searchableText
7167
+ };
7168
+ });
7169
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
7170
+ const fullTextThreads = threads.slice(0, THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT);
7171
+ const concurrency = 4;
7172
+ for (let offset = 0; offset < fullTextThreads.length; offset += concurrency) {
7173
+ const batch = fullTextThreads.slice(offset, offset + concurrency);
7174
+ const loaded = await Promise.all(batch.map(async (thread) => {
7175
+ try {
7176
+ const readResponse = await appServer.rpc("thread/read", {
7177
+ threadId: thread.id,
7178
+ includeTurns: true
7179
+ });
7180
+ const messageText = extractThreadMessageText(readResponse);
7181
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
7182
+ return [thread.id, {
7183
+ id: thread.id,
7184
+ title: thread.title,
7185
+ preview: thread.preview,
7186
+ messageText,
7187
+ searchableText
7188
+ }];
7189
+ } catch {
7190
+ return null;
6077
7191
  }
7192
+ }));
7193
+ for (const row of loaded) {
7194
+ if (!row) continue;
7195
+ docsById.set(row[0], row[1]);
6078
7196
  }
6079
- return Array.from(docsById.values());
6080
- });
7197
+ }
7198
+ return Array.from(docsById.values());
6081
7199
  }
6082
7200
  async function buildThreadSearchIndex(appServer) {
6083
7201
  const docs = await loadAllThreadsForSearch(appServer);
@@ -6085,7 +7203,7 @@ async function buildThreadSearchIndex(appServer) {
6085
7203
  return { docsById };
6086
7204
  }
6087
7205
  function createCodexBridgeMiddleware() {
6088
- const { appServer, methodCatalog, telegramBridge } = getSharedBridgeState();
7206
+ const { appServer, terminalManager, methodCatalog, telegramBridge } = getSharedBridgeState();
6089
7207
  let threadSearchIndex = null;
6090
7208
  let threadSearchIndexPromise = null;
6091
7209
  async function getThreadSearchIndex() {
@@ -6141,7 +7259,7 @@ function createCodexBridgeMiddleware() {
6141
7259
  if (!shouldLog) return;
6142
7260
  didLog = true;
6143
7261
  const rpcPart = rpcMethod ? `, rpcMethod=${rpcMethod}` : "";
6144
- console.info(`[codex-api-perf] ${requestMethod} ${requestPath} -> ${res.statusCode} (${durationMs}ms, bodyMB=${bodyMbValue.toFixed(4)}${rpcPart})`);
7262
+ console.info(`[codex-api-perf] ${requestMethod} ${requestPath} -> ${res.statusCode} (${durationMs}ms, bodyMB=${bodyMbValue.toFixed(1)}${rpcPart})`);
6145
7263
  };
6146
7264
  res.once("finish", logApiRequestDuration);
6147
7265
  res.once("close", logApiRequestDuration);
@@ -6151,16 +7269,57 @@ function createCodexBridgeMiddleware() {
6151
7269
  return;
6152
7270
  }
6153
7271
  const url = new URL(req.url, "http://localhost");
7272
+ if (url.pathname === "/codex-api/zen-proxy/v1/responses" && req.method === "POST") {
7273
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7274
+ let bearerToken = "";
7275
+ let wireApi = "chat";
7276
+ try {
7277
+ const state = JSON.parse(readFileSync2(statePath, "utf8"));
7278
+ bearerToken = state.apiKey ?? "";
7279
+ wireApi = state.wireApi === "responses" ? "responses" : "chat";
7280
+ } catch {
7281
+ }
7282
+ handleZenProxyRequest(req, res, bearerToken, wireApi);
7283
+ return;
7284
+ }
7285
+ if (url.pathname === "/codex-api/openrouter-proxy/v1/responses" && req.method === "POST") {
7286
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7287
+ let bearerToken = "";
7288
+ let wireApi = "responses";
7289
+ try {
7290
+ const state = JSON.parse(readFileSync2(statePath, "utf8"));
7291
+ bearerToken = state.apiKey ?? "";
7292
+ wireApi = state.wireApi === "chat" ? "chat" : "responses";
7293
+ } catch {
7294
+ }
7295
+ handleOpenRouterProxyRequest(req, res, bearerToken, wireApi);
7296
+ return;
7297
+ }
7298
+ if (url.pathname === "/codex-api/custom-proxy/v1/responses" && req.method === "POST") {
7299
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7300
+ let bearerToken = "";
7301
+ let wireApi = "responses";
7302
+ let baseUrl = "";
7303
+ try {
7304
+ const state = JSON.parse(readFileSync2(statePath, "utf8"));
7305
+ bearerToken = state.apiKey ?? "";
7306
+ wireApi = state.wireApi === "chat" ? "chat" : "responses";
7307
+ baseUrl = state.customBaseUrl ?? "";
7308
+ } catch {
7309
+ }
7310
+ handleCustomEndpointProxyRequest(req, res, { baseUrl, bearerToken, wireApi });
7311
+ return;
7312
+ }
6154
7313
  if (url.pathname.startsWith("/codex-api/free-mode")) {
6155
7314
  let readFreeModeState2 = function() {
6156
7315
  try {
6157
- return JSON.parse(readFileSync(statePath, "utf8"));
7316
+ return JSON.parse(readFileSync2(statePath, "utf8"));
6158
7317
  } catch {
6159
7318
  return { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
6160
7319
  }
6161
7320
  };
6162
7321
  var readFreeModeState = readFreeModeState2;
6163
- const statePath = join5(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7322
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
6164
7323
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode") {
6165
7324
  try {
6166
7325
  const body = await readJsonBody(req);
@@ -6171,7 +7330,19 @@ function createCodexBridgeMiddleware() {
6171
7330
  setJson4(res, 500, { error: "No free keys available" });
6172
7331
  return;
6173
7332
  }
6174
- const state = { enabled: true, apiKey, model: FREE_MODE_DEFAULT_MODEL, provider: "openrouter" };
7333
+ const prev = readFreeModeState2();
7334
+ const prevKeys = prev.providerKeys ?? {};
7335
+ if (prev.provider && prev.apiKey) {
7336
+ prevKeys[prev.provider] = prev.apiKey;
7337
+ }
7338
+ const state = {
7339
+ enabled: true,
7340
+ apiKey,
7341
+ model: FREE_MODE_DEFAULT_MODEL,
7342
+ provider: "openrouter",
7343
+ wireApi: prev.wireApi === "chat" ? "chat" : "responses",
7344
+ providerKeys: prevKeys
7345
+ };
6175
7346
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6176
7347
  appServer.dispose();
6177
7348
  const freeModels = await getFreeModels();
@@ -6183,7 +7354,18 @@ function createCodexBridgeMiddleware() {
6183
7354
  models: freeModels
6184
7355
  });
6185
7356
  } else {
6186
- const state = { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
7357
+ const prev = readFreeModeState2();
7358
+ const prevKeys = prev.providerKeys ?? {};
7359
+ if (prev.provider && prev.apiKey) {
7360
+ prevKeys[prev.provider] = prev.apiKey;
7361
+ }
7362
+ const state = {
7363
+ enabled: false,
7364
+ apiKey: null,
7365
+ model: FREE_MODE_DEFAULT_MODEL,
7366
+ wireApi: prev.wireApi === "chat" ? "chat" : "responses",
7367
+ providerKeys: prevKeys
7368
+ };
6187
7369
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6188
7370
  appServer.dispose();
6189
7371
  setJson4(res, 200, { ok: true, enabled: false });
@@ -6206,7 +7388,8 @@ function createCodexBridgeMiddleware() {
6206
7388
  customKey: Boolean(state.customKey),
6207
7389
  maskedKey,
6208
7390
  provider: state.provider ?? "openrouter",
6209
- customBaseUrl: state.customBaseUrl ?? null
7391
+ customBaseUrl: state.customBaseUrl ?? null,
7392
+ wireApi: state.wireApi ?? null
6210
7393
  });
6211
7394
  } catch (error) {
6212
7395
  setJson4(res, 500, { error: getErrorMessage5(error, "Failed to read free mode status") });
@@ -6236,13 +7419,26 @@ function createCodexBridgeMiddleware() {
6236
7419
  const key = typeof body?.key === "string" ? body.key.trim() : "";
6237
7420
  const current = readFreeModeState2();
6238
7421
  if (key.length > 0) {
6239
- const state = { ...current, enabled: true, apiKey: key, customKey: true, provider: "openrouter" };
7422
+ const state = {
7423
+ ...current,
7424
+ enabled: true,
7425
+ apiKey: key,
7426
+ customKey: true,
7427
+ provider: "openrouter",
7428
+ wireApi: current.wireApi === "chat" ? "chat" : "responses"
7429
+ };
6240
7430
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6241
7431
  appServer.dispose();
6242
7432
  setJson4(res, 200, { ok: true, customKey: true });
6243
7433
  } else {
6244
7434
  const communityKey = getRandomFreeKey();
6245
- const state = { ...current, apiKey: communityKey, customKey: false, provider: "openrouter" };
7435
+ const state = {
7436
+ ...current,
7437
+ apiKey: communityKey,
7438
+ customKey: false,
7439
+ provider: "openrouter",
7440
+ wireApi: current.wireApi === "chat" ? "chat" : "responses"
7441
+ };
6246
7442
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6247
7443
  appServer.dispose();
6248
7444
  setJson4(res, 200, { ok: true, customKey: false });
@@ -6257,17 +7453,31 @@ function createCodexBridgeMiddleware() {
6257
7453
  const body = await readJsonBody(req);
6258
7454
  const baseUrl = typeof body?.baseUrl === "string" ? body.baseUrl.trim() : "";
6259
7455
  const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
6260
- if (!baseUrl) {
7456
+ const wireApi = body?.wireApi === "chat" ? "chat" : "responses";
7457
+ const providerType = body?.provider === "opencode-zen" ? "opencode-zen" : body?.provider === "openrouter" ? "openrouter" : "custom";
7458
+ if (providerType === "custom" && !baseUrl) {
6261
7459
  setJson4(res, 400, { error: "baseUrl is required" });
6262
7460
  return;
6263
7461
  }
7462
+ const current = readFreeModeState2();
7463
+ const prevKeys = current.providerKeys ?? {};
7464
+ if (current.provider && current.apiKey) {
7465
+ prevKeys[current.provider] = current.apiKey;
7466
+ }
7467
+ const resolvedKey = apiKey || prevKeys[providerType] || "";
7468
+ if (resolvedKey) {
7469
+ prevKeys[providerType] = resolvedKey;
7470
+ }
7471
+ const resolvedModel = providerType === "openrouter" ? current.model || FREE_MODE_DEFAULT_MODEL : providerType === "custom" ? await fetchCustomEndpointDefaultModel(baseUrl, resolvedKey) : "";
6264
7472
  const state = {
6265
7473
  enabled: true,
6266
- apiKey: apiKey || "dummy",
6267
- model: "",
6268
- customKey: true,
6269
- provider: "custom",
6270
- customBaseUrl: baseUrl
7474
+ apiKey: resolvedKey,
7475
+ model: resolvedModel,
7476
+ customKey: providerType === "openrouter" ? current.customKey : true,
7477
+ provider: providerType,
7478
+ customBaseUrl: providerType === "custom" ? baseUrl : void 0,
7479
+ wireApi,
7480
+ providerKeys: prevKeys
6271
7481
  };
6272
7482
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6273
7483
  appServer.dispose();
@@ -6280,23 +7490,6 @@ function createCodexBridgeMiddleware() {
6280
7490
  next();
6281
7491
  return;
6282
7492
  }
6283
- if (!url.pathname.startsWith("/codex-api/")) {
6284
- next();
6285
- return;
6286
- }
6287
- await Sentry4.startSpan(
6288
- { name: `${req.method ?? "GET"} ${url.pathname}`, op: "http.server" },
6289
- () => routeRequest(req, res, url, next)
6290
- );
6291
- } catch (error) {
6292
- Sentry4.captureException(error);
6293
- if (!res.headersSent) {
6294
- setJson4(res, 500, { error: "Internal server error" });
6295
- }
6296
- }
6297
- };
6298
- async function routeRequest(req, res, url, next) {
6299
- try {
6300
7493
  if (await handleAccountRoutes(req, res, url, { appServer })) {
6301
7494
  return;
6302
7495
  }
@@ -6306,10 +7499,66 @@ function createCodexBridgeMiddleware() {
6306
7499
  if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
6307
7500
  return;
6308
7501
  }
6309
- if (req.method === "GET" && url.pathname === "/codex-api/sentry-config") {
6310
- const enabled = !process.argv.includes("--no-sentry");
6311
- const auth = await readCodexAuth();
6312
- setJson4(res, 200, { enabled, accountId: auth?.accountId ?? null });
7502
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/attach") {
7503
+ const body = asRecord5(await readJsonBody(req));
7504
+ const threadId = readNonEmptyString(body?.threadId);
7505
+ const cwd = readNonEmptyString(body?.cwd);
7506
+ if (!threadId || !cwd) {
7507
+ setJson4(res, 400, { error: "Missing threadId or cwd" });
7508
+ return;
7509
+ }
7510
+ const session = terminalManager.attach({
7511
+ threadId,
7512
+ cwd,
7513
+ sessionId: readNonEmptyString(body?.sessionId) || void 0,
7514
+ cols: typeof body?.cols === "number" ? body.cols : void 0,
7515
+ rows: typeof body?.rows === "number" ? body.rows : void 0,
7516
+ newSession: body?.newSession === true
7517
+ });
7518
+ setJson4(res, 200, { session });
7519
+ return;
7520
+ }
7521
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/input") {
7522
+ const body = asRecord5(await readJsonBody(req));
7523
+ const sessionId = readNonEmptyString(body?.sessionId);
7524
+ const data = typeof body?.data === "string" ? body.data : "";
7525
+ if (!sessionId) {
7526
+ setJson4(res, 400, { error: "Missing sessionId" });
7527
+ return;
7528
+ }
7529
+ terminalManager.write(sessionId, data);
7530
+ setJson4(res, 200, { ok: true });
7531
+ return;
7532
+ }
7533
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/resize") {
7534
+ const body = asRecord5(await readJsonBody(req));
7535
+ const sessionId = readNonEmptyString(body?.sessionId);
7536
+ if (!sessionId) {
7537
+ setJson4(res, 400, { error: "Missing sessionId" });
7538
+ return;
7539
+ }
7540
+ terminalManager.resize(sessionId, body?.cols, body?.rows);
7541
+ setJson4(res, 200, { ok: true });
7542
+ return;
7543
+ }
7544
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/close") {
7545
+ const body = asRecord5(await readJsonBody(req));
7546
+ const sessionId = readNonEmptyString(body?.sessionId);
7547
+ if (!sessionId) {
7548
+ setJson4(res, 400, { error: "Missing sessionId" });
7549
+ return;
7550
+ }
7551
+ terminalManager.close(sessionId);
7552
+ setJson4(res, 200, { ok: true });
7553
+ return;
7554
+ }
7555
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal-snapshot") {
7556
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
7557
+ if (!threadId) {
7558
+ setJson4(res, 400, { error: "Missing threadId" });
7559
+ return;
7560
+ }
7561
+ setJson4(res, 200, { session: terminalManager.getSnapshotForThread(threadId) });
6313
7562
  return;
6314
7563
  }
6315
7564
  if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
@@ -6319,6 +7568,10 @@ function createCodexBridgeMiddleware() {
6319
7568
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
6320
7569
  const payload = await readJsonBody(req);
6321
7570
  const body = asRecord5(payload);
7571
+ if (payload !== null && payload !== void 0) {
7572
+ requestBodyBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
7573
+ }
7574
+ rpcMethod = body?.method && typeof body.method === "string" ? body.method : null;
6322
7575
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
6323
7576
  setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
6324
7577
  return;
@@ -6343,25 +7596,23 @@ function createCodexBridgeMiddleware() {
6343
7596
  setJson4(res, 400, { error: "Missing threadId" });
6344
7597
  return;
6345
7598
  }
6346
- await Sentry4.startSpan({ name: "thread-file-change-fallback", op: "file.session" }, async () => {
6347
- const threadReadResult = await appServer.rpc("thread/read", {
6348
- threadId,
6349
- includeTurns: true
6350
- });
6351
- const threadReadRecord = asRecord5(threadReadResult);
6352
- const threadRecord = asRecord5(threadReadRecord?.thread);
6353
- const sessionPath = readNonEmptyString(threadRecord?.path);
6354
- if (!sessionPath || !isAbsolute2(sessionPath)) {
6355
- setJson4(res, 200, { data: [] });
6356
- return;
6357
- }
6358
- try {
6359
- const sessionLogRaw = await readFile3(sessionPath, "utf8");
6360
- setJson4(res, 200, { data: buildSessionFileChangeFallback(threadReadResult, sessionLogRaw) });
6361
- } catch {
6362
- setJson4(res, 200, { data: [] });
6363
- }
7599
+ const threadReadResult = await appServer.rpc("thread/read", {
7600
+ threadId,
7601
+ includeTurns: true
6364
7602
  });
7603
+ const threadReadRecord = asRecord5(threadReadResult);
7604
+ const threadRecord = asRecord5(threadReadRecord?.thread);
7605
+ const sessionPath = readNonEmptyString(threadRecord?.path);
7606
+ if (!sessionPath || !isAbsolute2(sessionPath)) {
7607
+ setJson4(res, 200, { data: [] });
7608
+ return;
7609
+ }
7610
+ try {
7611
+ const sessionLogRaw = await readFile3(sessionPath, "utf8");
7612
+ setJson4(res, 200, { data: buildSessionFileChangeFallback(threadReadResult, sessionLogRaw) });
7613
+ } catch {
7614
+ setJson4(res, 200, { data: [] });
7615
+ }
6365
7616
  return;
6366
7617
  }
6367
7618
  if (req.method === "GET" && url.pathname === "/codex-api/thread-stream-events") {
@@ -6551,8 +7802,29 @@ function createCodexBridgeMiddleware() {
6551
7802
  }
6552
7803
  if (req.method === "GET" && url.pathname === "/codex-api/provider-models") {
6553
7804
  try {
6554
- const fmState = JSON.parse(readFileSync(join5(getCodexHomeDir3(), FREE_MODE_STATE_FILE), "utf8"));
7805
+ const fmState = JSON.parse(readFileSync2(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE), "utf8"));
6555
7806
  if (fmState.enabled) {
7807
+ if (fmState.provider === "opencode-zen") {
7808
+ try {
7809
+ const modelsUrl = "https://opencode.ai/zen/v1/models";
7810
+ const headers = {};
7811
+ if (fmState.apiKey && fmState.apiKey !== "dummy") {
7812
+ headers["Authorization"] = `Bearer ${fmState.apiKey}`;
7813
+ }
7814
+ const resp = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8e3) });
7815
+ if (resp.ok) {
7816
+ const json = await resp.json();
7817
+ const allIds = (json.data ?? []).map((m) => m.id).filter(Boolean);
7818
+ const freeIds = allIds.filter((id) => id.endsWith("-free") || id === "big-pickle");
7819
+ const paidIds = allIds.filter((id) => !id.endsWith("-free") && id !== "big-pickle");
7820
+ setJson4(res, 200, { data: [...freeIds, ...paidIds], exclusive: true, source: "opencode-zen" });
7821
+ return;
7822
+ }
7823
+ } catch {
7824
+ }
7825
+ setJson4(res, 200, { data: ["big-pickle", "minimax-m2.5-free", "nemotron-3-super-free", "trinity-large-preview-free"], exclusive: true, source: "opencode-zen" });
7826
+ return;
7827
+ }
6556
7828
  if (fmState.provider === "custom" && fmState.customBaseUrl) {
6557
7829
  try {
6558
7830
  const modelsUrl = fmState.customBaseUrl.replace(/\/+$/, "") + "/models";
@@ -6563,8 +7835,10 @@ function createCodexBridgeMiddleware() {
6563
7835
  const resp = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8e3) });
6564
7836
  if (resp.ok) {
6565
7837
  const json = await resp.json();
6566
- const ids = (json.data ?? []).map((m) => m.id).filter(Boolean);
6567
- setJson4(res, 200, { data: ids, exclusive: true, source: "custom" });
7838
+ const ids = normalizeProviderModelsData(json);
7839
+ const currentModel = fmState.model?.trim() ?? "";
7840
+ const orderedIds = currentModel && ids.includes(currentModel) ? [currentModel, ...ids.filter((id) => id !== currentModel)] : ids;
7841
+ setJson4(res, 200, { data: orderedIds, exclusive: true, source: "custom" });
6568
7842
  return;
6569
7843
  }
6570
7844
  } catch {
@@ -6588,7 +7862,7 @@ function createCodexBridgeMiddleware() {
6588
7862
  return;
6589
7863
  }
6590
7864
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
6591
- setJson4(res, 200, { data: { path: homedir4() } });
7865
+ setJson4(res, 200, { data: { path: homedir5() } });
6592
7866
  return;
6593
7867
  }
6594
7868
  if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
@@ -6632,22 +7906,22 @@ function createCodexBridgeMiddleware() {
6632
7906
  await runCommand3("git", ["init"], { cwd: sourceCwd });
6633
7907
  gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
6634
7908
  }
6635
- const repoName = basename3(gitRoot) || "repo";
6636
- const worktreesRoot = join5(getCodexHomeDir3(), "worktrees");
7909
+ const repoName = basename4(gitRoot) || "repo";
7910
+ const worktreesRoot = join6(getCodexHomeDir3(), "worktrees");
6637
7911
  await mkdir4(worktreesRoot, { recursive: true });
6638
7912
  let worktreeId = "";
6639
7913
  let worktreeParent = "";
6640
7914
  let worktreeCwd = "";
6641
7915
  for (let attempt = 0; attempt < 12; attempt += 1) {
6642
7916
  const candidate = randomBytes(2).toString("hex");
6643
- const parent = join5(worktreesRoot, candidate);
7917
+ const parent = join6(worktreesRoot, candidate);
6644
7918
  try {
6645
7919
  await stat4(parent);
6646
7920
  continue;
6647
7921
  } catch {
6648
7922
  worktreeId = candidate;
6649
7923
  worktreeParent = parent;
6650
- worktreeCwd = join5(parent, repoName);
7924
+ worktreeCwd = join6(parent, repoName);
6651
7925
  break;
6652
7926
  }
6653
7927
  }
@@ -6944,7 +8218,7 @@ function createCodexBridgeMiddleware() {
6944
8218
  let index = 1;
6945
8219
  while (index < 1e5) {
6946
8220
  const candidateName = `New Project (${String(index)})`;
6947
- const candidatePath = join5(normalizedBasePath, candidateName);
8221
+ const candidatePath = join6(normalizedBasePath, candidateName);
6948
8222
  try {
6949
8223
  await stat4(candidatePath);
6950
8224
  index += 1;
@@ -6997,6 +8271,21 @@ function createCodexBridgeMiddleware() {
6997
8271
  setJson4(res, 200, { data: { threadIds } });
6998
8272
  return;
6999
8273
  }
8274
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-automations") {
8275
+ const automationsByThreadId = await listThreadHeartbeatAutomations();
8276
+ setJson4(res, 200, { data: automationsByThreadId });
8277
+ return;
8278
+ }
8279
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
8280
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
8281
+ if (!threadId) {
8282
+ setJson4(res, 400, { error: "Missing threadId" });
8283
+ return;
8284
+ }
8285
+ const automation = await readThreadHeartbeatAutomation(threadId);
8286
+ setJson4(res, 200, { data: automation });
8287
+ return;
8288
+ }
7000
8289
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
7001
8290
  const payload = asRecord5(await readJsonBody(req));
7002
8291
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
@@ -7032,6 +8321,31 @@ function createCodexBridgeMiddleware() {
7032
8321
  setJson4(res, 200, { ok: true });
7033
8322
  return;
7034
8323
  }
8324
+ if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
8325
+ const payload = asRecord5(await readJsonBody(req));
8326
+ const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
8327
+ const name = typeof payload?.name === "string" ? payload.name.trim() : "";
8328
+ const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
8329
+ const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
8330
+ const status = payload?.status === "PAUSED" ? "PAUSED" : "ACTIVE";
8331
+ if (!threadId || !name || !prompt || !rrule) {
8332
+ setJson4(res, 400, { error: "threadId, name, prompt, and rrule are required" });
8333
+ return;
8334
+ }
8335
+ const automation = await writeThreadHeartbeatAutomation({ threadId, name, prompt, rrule, status });
8336
+ setJson4(res, 200, { data: automation });
8337
+ return;
8338
+ }
8339
+ if (req.method === "DELETE" && url.pathname === "/codex-api/thread-automation") {
8340
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
8341
+ if (!threadId) {
8342
+ setJson4(res, 400, { error: "Missing threadId" });
8343
+ return;
8344
+ }
8345
+ const removed = await deleteThreadHeartbeatAutomation(threadId);
8346
+ setJson4(res, 200, { data: { removed } });
8347
+ return;
8348
+ }
7035
8349
  if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
7036
8350
  const payload = asRecord5(await readJsonBody(req));
7037
8351
  const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
@@ -7109,19 +8423,30 @@ data: ${JSON.stringify({ ok: true })}
7109
8423
  const message = getErrorMessage5(error, "Unknown bridge error");
7110
8424
  setJson4(res, 502, { error: message });
7111
8425
  }
7112
- }
8426
+ };
7113
8427
  middleware.dispose = () => {
7114
8428
  threadSearchIndex = null;
7115
8429
  telegramBridge.stop();
8430
+ terminalManager.dispose();
7116
8431
  appServer.dispose();
7117
8432
  };
7118
8433
  middleware.subscribeNotifications = (listener) => {
7119
- return appServer.onNotification((notification) => {
8434
+ const unsubscribeAppServer = appServer.onNotification((notification) => {
8435
+ listener({
8436
+ ...notification,
8437
+ atIso: (/* @__PURE__ */ new Date()).toISOString()
8438
+ });
8439
+ });
8440
+ const unsubscribeTerminal = terminalManager.subscribe((notification) => {
7120
8441
  listener({
7121
8442
  ...notification,
7122
8443
  atIso: (/* @__PURE__ */ new Date()).toISOString()
7123
8444
  });
7124
8445
  });
8446
+ return () => {
8447
+ unsubscribeAppServer();
8448
+ unsubscribeTerminal();
8449
+ };
7125
8450
  };
7126
8451
  return middleware;
7127
8452
  }
@@ -7280,7 +8605,7 @@ function createAuthSession(password) {
7280
8605
  }
7281
8606
 
7282
8607
  // src/server/localBrowseUi.ts
7283
- import { dirname as dirname2, extname as extname2, join as join6 } from "path";
8608
+ import { dirname as dirname3, extname as extname2, join as join7 } from "path";
7284
8609
  import { open, readFile as readFile4, readdir as readdir3, stat as stat5 } from "fs/promises";
7285
8610
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
7286
8611
  ".txt",
@@ -7406,7 +8731,7 @@ async function isTextEditableFile(localPath) {
7406
8731
  return false;
7407
8732
  }
7408
8733
  }
7409
- function escapeHtml(value) {
8734
+ function escapeHtml2(value) {
7410
8735
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
7411
8736
  }
7412
8737
  function normalizeNewProjectName(value) {
@@ -7428,7 +8753,7 @@ function escapeForInlineScriptString(value) {
7428
8753
  async function getDirectoryItems(localPath) {
7429
8754
  const entries = await readdir3(localPath, { withFileTypes: true });
7430
8755
  const withMeta = await Promise.all(entries.map(async (entry) => {
7431
- const entryPath = join6(localPath, entry.name);
8756
+ const entryPath = join7(localPath, entry.name);
7432
8757
  const entryStat = await stat5(entryPath);
7433
8758
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
7434
8759
  return {
@@ -7449,7 +8774,7 @@ async function getDirectoryItems(localPath) {
7449
8774
  function projectCreationTargetPath(parentPath, newProjectName) {
7450
8775
  const normalizedName = normalizeNewProjectName(newProjectName);
7451
8776
  if (!normalizedName) return "";
7452
- return join6(parentPath, normalizedName);
8777
+ return join7(parentPath, normalizedName);
7453
8778
  }
7454
8779
  function projectCreationButtonLabel(newProjectName) {
7455
8780
  const normalizedName = normalizeNewProjectName(newProjectName);
@@ -7470,15 +8795,15 @@ function failureStatusText(newProjectName) {
7470
8795
  function actionButtonsHtml(localPath, newProjectName) {
7471
8796
  const normalizedName = normalizeNewProjectName(newProjectName);
7472
8797
  const createTargetPath = projectCreationTargetPath(localPath, normalizedName);
7473
- const createButton = createTargetPath ? `<button class="header-open-btn create-project-btn" type="button" aria-label="${escapeHtml(projectCreationButtonLabel(normalizedName))}" title="${escapeHtml(projectCreationButtonLabel(normalizedName))}" data-path="${escapeHtml(createTargetPath)}" data-label="${escapeHtml(normalizedName)}" data-status="${escapeHtml(projectCreationStatusText(normalizedName))}" data-error="${escapeHtml(failureStatusText(normalizedName))}">${escapeHtml(projectCreationButtonLabel(normalizedName))}</button>` : "";
7474
- const openButton = `<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}" data-label="" data-status="${escapeHtml(openFolderStatusText(normalizedName))}" data-error="${escapeHtml(failureStatusText(normalizedName))}">Open folder in Codex</button>`;
8798
+ const createButton = createTargetPath ? `<button class="header-open-btn create-project-btn" type="button" aria-label="${escapeHtml2(projectCreationButtonLabel(normalizedName))}" title="${escapeHtml2(projectCreationButtonLabel(normalizedName))}" data-path="${escapeHtml2(createTargetPath)}" data-label="${escapeHtml2(normalizedName)}" data-status="${escapeHtml2(projectCreationStatusText(normalizedName))}" data-error="${escapeHtml2(failureStatusText(normalizedName))}">${escapeHtml2(projectCreationButtonLabel(normalizedName))}</button>` : "";
8799
+ const openButton = `<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml2(localPath)}" data-label="" data-status="${escapeHtml2(openFolderStatusText(normalizedName))}" data-error="${escapeHtml2(failureStatusText(normalizedName))}">Open folder in Codex</button>`;
7475
8800
  return `${createButton}${openButton}`;
7476
8801
  }
7477
8802
  async function getLocalDirectoryListing(localPath, options = {}) {
7478
8803
  const entries = await readdir3(localPath, { withFileTypes: true });
7479
8804
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
7480
8805
  name: entry.name,
7481
- path: join6(localPath, entry.name)
8806
+ path: join7(localPath, entry.name)
7482
8807
  })).filter((entry) => options.showHidden === true || !isHiddenName(entry.name)).sort((a, b) => {
7483
8808
  const aHidden = isHiddenName(a.name);
7484
8809
  const bHidden = isHiddenName(b.name);
@@ -7487,28 +8812,28 @@ async function getLocalDirectoryListing(localPath, options = {}) {
7487
8812
  });
7488
8813
  return {
7489
8814
  path: localPath,
7490
- parentPath: dirname2(localPath),
8815
+ parentPath: dirname3(localPath),
7491
8816
  entries: directories
7492
8817
  };
7493
8818
  }
7494
8819
  async function createDirectoryListingHtml(localPath, options) {
7495
8820
  const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
7496
8821
  const items = await getDirectoryItems(localPath);
7497
- const parentPath = dirname2(localPath);
8822
+ const parentPath = dirname3(localPath);
7498
8823
  const rows = items.map((item) => {
7499
8824
  const suffix = item.isDirectory ? "/" : "";
7500
- const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path, newProjectName))}" title="Edit">\u270F\uFE0F</a>` : "";
7501
- return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path, newProjectName))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
8825
+ const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml2(item.name)}" href="${escapeHtml2(toEditHref(item.path, newProjectName))}" title="Edit">\u270F\uFE0F</a>` : "";
8826
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml2(toBrowseHref(item.path, newProjectName))}">${escapeHtml2(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
7502
8827
  }).join("\n");
7503
- const parentLink = localPath !== parentPath ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath, newProjectName))}">..</a>` : "";
7504
- const pickerSummary = newProjectName ? `<p class="picker-summary">Browse to the parent folder where you want to create <strong>${escapeHtml(newProjectName)}</strong>, or open the current folder directly.</p>` : "";
8828
+ const parentLink = localPath !== parentPath ? `<a class="header-parent-link" href="${escapeHtml2(toBrowseHref(parentPath, newProjectName))}">..</a>` : "";
8829
+ const pickerSummary = newProjectName ? `<p class="picker-summary">Browse to the parent folder where you want to create <strong>${escapeHtml2(newProjectName)}</strong>, or open the current folder directly.</p>` : "";
7505
8830
  const actionButtons = actionButtonsHtml(localPath, newProjectName);
7506
8831
  return `<!doctype html>
7507
8832
  <html lang="en">
7508
8833
  <head>
7509
8834
  <meta charset="utf-8" />
7510
8835
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7511
- <title>Index of ${escapeHtml(localPath)}</title>
8836
+ <title>Index of ${escapeHtml2(localPath)}</title>
7512
8837
  <style>
7513
8838
  body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
7514
8839
  a { color: #8cc2ff; text-decoration: none; }
@@ -7548,7 +8873,7 @@ async function createDirectoryListingHtml(localPath, options) {
7548
8873
  </style>
7549
8874
  </head>
7550
8875
  <body>
7551
- <h1>Index of ${escapeHtml(localPath)}</h1>
8876
+ <h1>Index of ${escapeHtml2(localPath)}</h1>
7552
8877
  ${pickerSummary}
7553
8878
  <div class="header-actions">
7554
8879
  ${parentLink}
@@ -7600,7 +8925,7 @@ async function createDirectoryListingHtml(localPath, options) {
7600
8925
  }
7601
8926
  async function createTextEditorHtml(localPath) {
7602
8927
  const content = await readFile4(localPath, "utf8");
7603
- const parentPath = dirname2(localPath);
8928
+ const parentPath = dirname3(localPath);
7604
8929
  const language = languageForPath(localPath);
7605
8930
  const safeContentLiteral = escapeForInlineScriptString(content);
7606
8931
  return `<!doctype html>
@@ -7608,7 +8933,7 @@ async function createTextEditorHtml(localPath) {
7608
8933
  <head>
7609
8934
  <meta charset="utf-8" />
7610
8935
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7611
- <title>Edit ${escapeHtml(localPath)}</title>
8936
+ <title>Edit ${escapeHtml2(localPath)}</title>
7612
8937
  <style>
7613
8938
  html, body { width: 100%; height: 100%; margin: 0; }
7614
8939
  body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
@@ -7628,11 +8953,11 @@ async function createTextEditorHtml(localPath) {
7628
8953
  <body>
7629
8954
  <div class="toolbar">
7630
8955
  <div class="row">
7631
- <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
8956
+ <a href="${escapeHtml2(toBrowseHref(parentPath))}">Back</a>
7632
8957
  <button id="saveBtn" type="button">Save</button>
7633
8958
  <span id="status"></span>
7634
8959
  </div>
7635
- <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
8960
+ <div class="meta">${escapeHtml2(localPath)} \xB7 ${escapeHtml2(language)}</div>
7636
8961
  </div>
7637
8962
  <div id="editor"></div>
7638
8963
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
@@ -7641,7 +8966,7 @@ async function createTextEditorHtml(localPath) {
7641
8966
  const status = document.getElementById('status');
7642
8967
  const editor = ace.edit('editor');
7643
8968
  editor.setTheme('ace/theme/tomorrow_night');
7644
- editor.session.setMode('ace/mode/${escapeHtml(language)}');
8969
+ editor.session.setMode('ace/mode/${escapeHtml2(language)}');
7645
8970
  editor.setValue(${safeContentLiteral}, -1);
7646
8971
  editor.setOptions({
7647
8972
  fontSize: '13px',
@@ -7669,9 +8994,9 @@ async function createTextEditorHtml(localPath) {
7669
8994
 
7670
8995
  // src/server/httpServer.ts
7671
8996
  import { WebSocketServer } from "ws";
7672
- var __dirname = dirname3(fileURLToPath(import.meta.url));
7673
- var distDir = join7(__dirname, "..", "dist");
7674
- var spaEntryFile = join7(distDir, "index.html");
8997
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
8998
+ var distDir = join8(__dirname, "..", "dist");
8999
+ var spaEntryFile = join8(distDir, "index.html");
7675
9000
  var IMAGE_CONTENT_TYPES = {
7676
9001
  ".avif": "image/avif",
7677
9002
  ".bmp": "image/bmp",
@@ -7840,7 +9165,7 @@ function createServer(options = {}) {
7840
9165
  res.status(404).json({ error: "File not found." });
7841
9166
  }
7842
9167
  });
7843
- const hasFrontendAssets = existsSync3(spaEntryFile);
9168
+ const hasFrontendAssets = existsSync4(spaEntryFile);
7844
9169
  if (hasFrontendAssets) {
7845
9170
  app.use(express.static(distDir));
7846
9171
  }
@@ -7910,16 +9235,16 @@ function generatePassword() {
7910
9235
 
7911
9236
  // src/cli/index.ts
7912
9237
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
7913
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
9238
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
7914
9239
  var hasPromptedCloudflaredInstall = false;
7915
9240
  function getCodexHomePath() {
7916
- return process.env.CODEX_HOME?.trim() || join8(homedir5(), ".codex");
9241
+ return process.env.CODEX_HOME?.trim() || join9(homedir6(), ".codex");
7917
9242
  }
7918
9243
  function getCloudflaredPromptMarkerPath() {
7919
- return join8(getCodexHomePath(), ".cloudflared-install-prompted");
9244
+ return join9(getCodexHomePath(), ".cloudflared-install-prompted");
7920
9245
  }
7921
9246
  function hasPromptedCloudflaredInstallPersisted() {
7922
- return existsSync4(getCloudflaredPromptMarkerPath());
9247
+ return existsSync5(getCloudflaredPromptMarkerPath());
7923
9248
  }
7924
9249
  async function persistCloudflaredInstallPrompted() {
7925
9250
  const codexHome = getCodexHomePath();
@@ -7929,7 +9254,7 @@ async function persistCloudflaredInstallPrompted() {
7929
9254
  }
7930
9255
  async function readCliVersion() {
7931
9256
  try {
7932
- const packageJsonPath = join8(__dirname2, "..", "package.json");
9257
+ const packageJsonPath = join9(__dirname2, "..", "package.json");
7933
9258
  const raw = await readFile5(packageJsonPath, "utf8");
7934
9259
  const parsed = JSON.parse(raw);
7935
9260
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -7954,8 +9279,8 @@ function resolveCloudflaredCommand() {
7954
9279
  if (canRunCommand("cloudflared", ["--version"])) {
7955
9280
  return "cloudflared";
7956
9281
  }
7957
- const localCandidate = join8(homedir5(), ".local", "bin", "cloudflared");
7958
- if (existsSync4(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
9282
+ const localCandidate = join9(homedir6(), ".local", "bin", "cloudflared");
9283
+ if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
7959
9284
  return localCandidate;
7960
9285
  }
7961
9286
  return null;
@@ -8008,13 +9333,13 @@ async function ensureCloudflaredInstalledLinux() {
8008
9333
  if (!mappedArch) {
8009
9334
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
8010
9335
  }
8011
- const userBinDir = join8(homedir5(), ".local", "bin");
9336
+ const userBinDir = join9(homedir6(), ".local", "bin");
8012
9337
  mkdirSync(userBinDir, { recursive: true });
8013
- const destination = join8(userBinDir, "cloudflared");
9338
+ const destination = join9(userBinDir, "cloudflared");
8014
9339
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
8015
9340
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
8016
9341
  await downloadFile(downloadUrl, destination);
8017
- chmodSync(destination, 493);
9342
+ chmodSync2(destination, 493);
8018
9343
  process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
8019
9344
  const installed = resolveCloudflaredCommand();
8020
9345
  if (!installed) {
@@ -8061,7 +9386,7 @@ async function resolveCloudflaredForTunnel() {
8061
9386
  }
8062
9387
  function hasCodexAuth() {
8063
9388
  const codexHome = getCodexHomePath();
8064
- return existsSync4(join8(codexHome, "auth.json"));
9389
+ return existsSync5(join9(codexHome, "auth.json"));
8065
9390
  }
8066
9391
  function ensureCodexInstalled() {
8067
9392
  let codexCommand = resolveCodexCommand();
@@ -8251,7 +9576,7 @@ function listenWithFallback(server, startPort) {
8251
9576
  }
8252
9577
  function getCodexGlobalStatePath2() {
8253
9578
  const codexHome = getCodexHomePath();
8254
- return join8(codexHome, ".codex-global-state.json");
9579
+ return join9(codexHome, ".codex-global-state.json");
8255
9580
  }
8256
9581
  function normalizeUniqueStrings(value) {
8257
9582
  if (!Array.isArray(value)) return [];
@@ -8326,18 +9651,7 @@ async function startServer(options) {
8326
9651
  process.env.CODEXUI_APPROVAL_POLICY = options.approvalPolicy;
8327
9652
  }
8328
9653
  const runtimeConfig = resolveAppServerRuntimeConfig();
8329
- if (Math.floor(Math.random() * 100) === 0 && canRunCommand("gh", ["--version"])) {
8330
- const child = spawn5("gh", ["api", "-X", "PUT", "/user/starred/friuns2/codexui"], {
8331
- stdio: "ignore"
8332
- });
8333
- child.on("error", () => {
8334
- });
8335
- child.unref();
8336
- }
8337
- if (options.login && !hasCodexAuth() && codexCommand) {
8338
- console.log("\nCodex is not logged in. Starting `codex login`...\n");
8339
- runOrFail(codexCommand, ["login"], "Codex login");
8340
- } else if (options.login && !hasCodexAuth()) {
9654
+ if (options.login && !hasCodexAuth()) {
8341
9655
  console.log("\nCodex is not logged in. You can log in later via settings or run `codexui login`.\n");
8342
9656
  }
8343
9657
  const requestedPort = parseInt(options.port, 10);
@@ -8422,7 +9736,7 @@ async function runLogin() {
8422
9736
  console.log("\nStarting `codex login`...\n");
8423
9737
  runOrFail(codexCommand, ["login"], "Codex login");
8424
9738
  }
8425
- program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5900").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel (default is auto by Tailscale detection)", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").option("--login", "run automatic Codex login bootstrap", true).option("--no-login", "skip automatic Codex login bootstrap").option("--sandbox-mode <mode>", "Codex sandbox mode: read-only, workspace-write, danger-full-access").option("--approval-policy <policy>", "Codex approval policy: untrusted, on-failure, on-request, never").option("--no-sentry", "disable Sentry error tracking and performance monitoring").action(async (projectPath, opts) => {
9739
+ program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5900").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel (default is auto by Tailscale detection)", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").option("--login", "run automatic Codex login bootstrap", true).option("--no-login", "skip automatic Codex login bootstrap").option("--sandbox-mode <mode>", "Codex sandbox mode: read-only, workspace-write, danger-full-access").option("--approval-policy <policy>", "Codex approval policy: untrusted, on-failure, on-request, never").action(async (projectPath, opts) => {
8426
9740
  const rawArgv = process.argv.slice(2);
8427
9741
  const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
8428
9742
  const tunnelFlagExplicit = rawArgv.some((arg) => arg === "--tunnel" || arg === "--no-tunnel" || arg.startsWith("--tunnel=") || arg.startsWith("--no-tunnel="));