codexui-android 0.1.91 → 0.1.92

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 +2330 -1052
  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";
227
+ import { mkdtemp as mkdtemp3, readFile as readFile3, readdir as readdir2, rm as rm4, mkdir as mkdir4, stat as stat4 } from "fs/promises";
230
228
  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";
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}"`,
3716
- "-c",
3717
- `model_providers.${CUSTOM_PROVIDER_ID}.wire_api="responses"`,
3876
+ `model_providers.${CUSTOM_PROVIDER_ID}.base_url="${baseUrl2}"`,
3718
3877
  "-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,752 @@ 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
 
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 } 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";
4354
+ var TERMINAL_BUFFER_LIMIT = 16 * 1024;
4355
+ var DEFAULT_COLS = 80;
4356
+ var DEFAULT_ROWS = 24;
4357
+ var TERMINAL_NAME = "xterm-256color";
4358
+ var require2 = createRequire(import.meta.url);
4359
+ var ThreadTerminalManager = class {
4360
+ constructor(options = {}) {
4361
+ this.sessions = /* @__PURE__ */ new Map();
4362
+ this.activeSessionIdByThreadId = /* @__PURE__ */ new Map();
4363
+ this.listeners = /* @__PURE__ */ new Set();
4364
+ this.spawn = options.spawn ?? loadTerminalSpawn();
4365
+ this.exists = options.exists ?? existsSync3;
4366
+ this.homeDir = options.homeDir ?? homedir4;
4367
+ this.cwd = options.cwd ?? process.cwd;
4368
+ this.platform = options.platform ?? process.platform;
4369
+ this.shell = options.shell ?? null;
4370
+ this.ensureSpawnHelperExecutable = options.ensureSpawnHelperExecutable ?? ensureNodePtyPrebuiltExecutable;
4371
+ }
4372
+ subscribe(listener) {
4373
+ this.listeners.add(listener);
4374
+ return () => {
4375
+ this.listeners.delete(listener);
4376
+ };
4377
+ }
4378
+ attach(params) {
4379
+ const threadId = params.threadId.trim();
4380
+ if (!threadId) {
4381
+ throw new Error("Missing threadId");
4382
+ }
4383
+ const requestedSessionId = params.sessionId?.trim() || "";
4384
+ const existingSessionId = params.newSession ? "" : requestedSessionId || this.activeSessionIdByThreadId.get(threadId) || "";
4385
+ const existing = existingSessionId ? this.sessions.get(existingSessionId) : null;
4386
+ if (existing) {
4387
+ this.activeSessionIdByThreadId.set(threadId, existing.id);
4388
+ this.resize(existing.id, params.cols, params.rows);
4389
+ const nextCwd = this.resolveCwd(params.cwd);
4390
+ if (nextCwd !== existing.cwd) {
4391
+ existing.cwd = nextCwd;
4392
+ existing.pty.write(`cd ${shellQuote(nextCwd)}\r`);
4393
+ }
4394
+ this.emitInit(existing);
4395
+ this.emitAttached(existing);
4396
+ return this.toSnapshot(existing);
4397
+ }
4398
+ const session = this.createSession({
4399
+ threadId,
4400
+ cwd: params.cwd,
4401
+ sessionId: requestedSessionId || randomUUID(),
4402
+ cols: params.cols,
4403
+ rows: params.rows
4404
+ });
4405
+ this.sessions.set(session.id, session);
4406
+ this.activeSessionIdByThreadId.set(threadId, session.id);
4407
+ this.emitAttached(session);
4408
+ return this.toSnapshot(session);
4409
+ }
4410
+ write(sessionId, data) {
4411
+ const session = this.requireSession(sessionId);
4412
+ session.pty.write(data);
4413
+ }
4414
+ resize(sessionId, cols, rows) {
4415
+ const session = this.sessions.get(sessionId);
4416
+ if (!session) return;
4417
+ const nextCols = normalizeDimension(cols, DEFAULT_COLS);
4418
+ const nextRows = normalizeDimension(rows, DEFAULT_ROWS);
4419
+ session.pty.resize(nextCols, nextRows);
4420
+ }
4421
+ close(sessionId) {
4422
+ const session = this.sessions.get(sessionId);
4423
+ if (!session) return;
4424
+ this.sessions.delete(session.id);
4425
+ if (this.activeSessionIdByThreadId.get(session.threadId) === session.id) {
4426
+ this.activeSessionIdByThreadId.delete(session.threadId);
4427
+ }
4428
+ session.pty.kill();
4429
+ this.emit({
4430
+ method: "terminal-exit",
4431
+ params: {
4432
+ sessionId: session.id,
4433
+ threadId: session.threadId,
4434
+ code: null,
4435
+ signal: null
4436
+ }
4437
+ });
4438
+ }
4439
+ getSnapshotForThread(threadId) {
4440
+ const sessionId = this.activeSessionIdByThreadId.get(threadId.trim());
4441
+ if (!sessionId) return null;
4442
+ const session = this.sessions.get(sessionId);
4443
+ return session ? this.toSnapshot(session) : null;
4444
+ }
4445
+ dispose() {
4446
+ for (const sessionId of Array.from(this.sessions.keys())) {
4447
+ this.close(sessionId);
4448
+ }
4449
+ this.listeners.clear();
4450
+ }
4451
+ createSession(params) {
4452
+ const cwd = this.resolveCwd(params.cwd);
4453
+ const shell = this.resolveShell();
4454
+ const env = {
4455
+ ...process.env,
4456
+ TERM: TERMINAL_NAME
4457
+ };
4458
+ normalizeLocaleEnv(env, this.platform);
4459
+ delete env.TERMINFO;
4460
+ delete env.TERMINFO_DIRS;
4461
+ this.ensureSpawnHelperExecutable();
4462
+ const pty = this.spawn(shell, [], {
4463
+ name: TERMINAL_NAME,
4464
+ cols: normalizeDimension(params.cols, DEFAULT_COLS),
4465
+ rows: normalizeDimension(params.rows, DEFAULT_ROWS),
4466
+ cwd,
4467
+ env
4468
+ });
4469
+ const session = {
4470
+ id: params.sessionId,
4471
+ threadId: params.threadId,
4472
+ cwd,
4473
+ shell: basename2(shell),
4474
+ pty,
4475
+ buffer: "",
4476
+ truncated: false
4477
+ };
4478
+ pty.onData((data) => {
4479
+ this.appendOutput(session, data);
4480
+ });
4481
+ pty.onExit(({ exitCode, signal }) => {
4482
+ if (this.sessions.get(session.id) === session) {
4483
+ this.sessions.delete(session.id);
4484
+ }
4485
+ if (this.activeSessionIdByThreadId.get(session.threadId) === session.id) {
4486
+ this.activeSessionIdByThreadId.delete(session.threadId);
4487
+ }
4488
+ this.emit({
4489
+ method: "terminal-exit",
4490
+ params: {
4491
+ sessionId: session.id,
4492
+ threadId: session.threadId,
4493
+ code: exitCode,
4494
+ signal: signal == null ? null : String(signal)
4495
+ }
4496
+ });
4497
+ });
4498
+ return session;
4499
+ }
4500
+ appendOutput(session, data) {
4501
+ const next = `${session.buffer}${data}`;
4502
+ if (next.length > TERMINAL_BUFFER_LIMIT) {
4503
+ session.buffer = next.slice(-TERMINAL_BUFFER_LIMIT);
4504
+ session.truncated = true;
4505
+ } else {
4506
+ session.buffer = next;
4507
+ }
4508
+ this.emit({
4509
+ method: "terminal-data",
4510
+ params: {
4511
+ sessionId: session.id,
4512
+ threadId: session.threadId,
4513
+ data
4514
+ }
4515
+ });
4516
+ }
4517
+ emitInit(session) {
4518
+ if (!session.buffer) return;
4519
+ this.emit({
4520
+ method: "terminal-init-log",
4521
+ params: {
4522
+ sessionId: session.id,
4523
+ threadId: session.threadId,
4524
+ log: session.buffer,
4525
+ truncated: session.truncated
4526
+ }
4527
+ });
4528
+ }
4529
+ emitAttached(session) {
4530
+ this.emit({
4531
+ method: "terminal-attached",
4532
+ params: {
4533
+ sessionId: session.id,
4534
+ threadId: session.threadId,
4535
+ cwd: session.cwd,
4536
+ shell: session.shell
4537
+ }
4538
+ });
4539
+ }
4540
+ emit(notification) {
4541
+ for (const listener of this.listeners) {
4542
+ listener(notification);
4543
+ }
4544
+ }
4545
+ requireSession(sessionId) {
4546
+ const session = this.sessions.get(sessionId.trim());
4547
+ if (!session) {
4548
+ throw new Error("Terminal session missing");
4549
+ }
4550
+ return session;
4551
+ }
4552
+ resolveShell() {
4553
+ if (this.shell) return this.shell;
4554
+ if (this.platform === "win32") {
4555
+ return process.env.COMSPEC || "cmd.exe";
4556
+ }
4557
+ return process.env.SHELL || "/bin/zsh";
4558
+ }
4559
+ resolveCwd(value) {
4560
+ const cwd = value.trim();
4561
+ if (cwd && this.exists(cwd)) {
4562
+ return cwd;
4563
+ }
4564
+ const home = this.homeDir();
4565
+ if (home && this.exists(home)) {
4566
+ return home;
4567
+ }
4568
+ return this.cwd();
4569
+ }
4570
+ toSnapshot(session) {
4571
+ return {
4572
+ id: session.id,
4573
+ threadId: session.threadId,
4574
+ cwd: session.cwd,
4575
+ shell: session.shell,
4576
+ buffer: session.buffer,
4577
+ truncated: session.truncated
4578
+ };
4579
+ }
4580
+ };
4581
+ function normalizeDimension(value, fallback) {
4582
+ const parsed = typeof value === "number" ? value : Number(value);
4583
+ if (!Number.isFinite(parsed)) return fallback;
4584
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
4585
+ }
4586
+ function loadTerminalSpawn() {
4587
+ if (resolveNodePtyPrebuiltPath()) {
4588
+ try {
4589
+ const terminal2 = require2("node-pty-prebuilt-multiarch");
4590
+ return terminal2.spawn;
4591
+ } catch {
4592
+ }
4593
+ }
4594
+ const terminal = require2("node-pty");
4595
+ return terminal.spawn;
4596
+ }
4597
+ function resolveNodePtyPrebuiltPath() {
4598
+ try {
4599
+ const packageJson = require2.resolve("node-pty-prebuilt-multiarch/package.json");
4600
+ const packageRoot = dirname(packageJson);
4601
+ const builtPath = join5(packageRoot, "build", "Release", "pty.node");
4602
+ if (existsSync3(builtPath)) {
4603
+ return builtPath;
4604
+ }
4605
+ const runtime = Object.prototype.hasOwnProperty.call(process.versions, "electron") ? "electron" : "node";
4606
+ const libc = process.platform === "linux" && existsSync3("/etc/alpine-release") ? ".musl" : "";
4607
+ const binaryName = `${runtime}.abi${process.versions.modules}${libc}.node`;
4608
+ const binaryPath = join5(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, binaryName);
4609
+ return existsSync3(binaryPath) ? binaryPath : null;
4610
+ } catch {
4611
+ return null;
4612
+ }
4613
+ }
4614
+ function ensureNodePtyPrebuiltExecutable() {
4615
+ if (process.platform !== "darwin" && process.platform !== "linux") return;
4616
+ try {
4617
+ const nodePtyEntry = require2.resolve("node-pty-prebuilt-multiarch");
4618
+ const packageRoot = join5(dirname(nodePtyEntry), "..");
4619
+ const helperPath = join5(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
4620
+ if (existsSync3(helperPath)) {
4621
+ chmodSync(helperPath, 493);
4622
+ }
4623
+ } catch {
4624
+ }
4625
+ }
4626
+ function normalizeLocaleEnv(env, platform) {
4627
+ const locale = platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8";
4628
+ env.LANG = locale;
4629
+ env.LC_ALL = locale;
4630
+ env.LC_CTYPE = locale;
4631
+ }
4632
+ function shellQuote(value) {
4633
+ return `'${value.replace(/'/g, `'\\''`)}'`;
4634
+ }
4635
+
3738
4636
  // src/utils/commandInvocation.ts
3739
4637
  import { spawnSync as spawnSync2 } from "child_process";
3740
- import { basename as basename2, extname } from "path";
4638
+ import { basename as basename3, extname } from "path";
3741
4639
  var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
3742
4640
  function quoteCmdExeArg(value) {
3743
4641
  const normalized = value.replace(/"/g, '""');
@@ -3751,7 +4649,7 @@ function needsCmdExeWrapper(command) {
3751
4649
  return false;
3752
4650
  }
3753
4651
  const lowerCommand = command.toLowerCase();
3754
- const baseName = basename2(lowerCommand);
4652
+ const baseName = basename3(lowerCommand);
3755
4653
  if (/\.(cmd|bat)$/i.test(baseName)) {
3756
4654
  return true;
3757
4655
  }
@@ -3856,6 +4754,14 @@ function asRecord5(value) {
3856
4754
  function isInlineDataUrl(value) {
3857
4755
  return /^data:/iu.test(value.trim());
3858
4756
  }
4757
+ function normalizeBase64ImageDataUrl(value, mimeType) {
4758
+ const trimmed = value.trim();
4759
+ if (!trimmed) return null;
4760
+ if (isInlineDataUrl(trimmed)) return trimmed;
4761
+ const compact = trimmed.replace(/\s+/gu, "");
4762
+ if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(compact)) return null;
4763
+ return `data:${mimeType};base64,${compact}`;
4764
+ }
3859
4765
  function extensionFromMimeType(mimeType) {
3860
4766
  const normalized = mimeType.trim().toLowerCase();
3861
4767
  if (normalized === "image/png") return ".png";
@@ -3892,10 +4798,10 @@ async function persistInlineDataUrlToLocalFile(dataUrl, baseName) {
3892
4798
  if (bytes.length === 0) return null;
3893
4799
  const hash = createHash2("sha1").update(bytes).digest("hex");
3894
4800
  const ext = extensionFromMimeType(mimeType);
3895
- const mediaDir = join5(tmpdir4(), "codex-web-inline-media");
4801
+ const mediaDir = join6(tmpdir4(), "codex-web-inline-media");
3896
4802
  await mkdir4(mediaDir, { recursive: true });
3897
4803
  const fileName = `${baseName}-${hash}${ext}`;
3898
- const filePath = join5(mediaDir, fileName);
4804
+ const filePath = join6(mediaDir, fileName);
3899
4805
  try {
3900
4806
  await stat4(filePath);
3901
4807
  } catch {
@@ -3926,6 +4832,21 @@ async function sanitizeInlineUserContentBlock(block, context) {
3926
4832
  text: `Image attachment: ${target}`
3927
4833
  };
3928
4834
  }
4835
+ if (type === "imageGeneration" || type === "image_generation") {
4836
+ const rawResult = asNonEmptyString(record.result) ?? asNonEmptyString(record.b64_json) ?? asNonEmptyString(record.image);
4837
+ const mimeType = asNonEmptyString(record.mime_type) ?? asNonEmptyString(record.mimeType) ?? "image/png";
4838
+ const dataUrl = rawResult ? normalizeBase64ImageDataUrl(rawResult, mimeType) : null;
4839
+ if (dataUrl) {
4840
+ const localUrl = await persistInlineDataUrlToLocalFile(dataUrl, `generated-image-${context.turnId}-${context.itemId}`);
4841
+ if (localUrl) {
4842
+ return {
4843
+ ...record,
4844
+ type: "imageView",
4845
+ path: localUrl
4846
+ };
4847
+ }
4848
+ }
4849
+ }
3929
4850
  const inlineFileData = asNonEmptyString(record.file_data) ?? asNonEmptyString(record.data) ?? asNonEmptyString(record.base64);
3930
4851
  if ((type.includes("file") || type === "input_file" || type === "file") && inlineFileData) {
3931
4852
  const mimeType = asNonEmptyString(record.mime_type) ?? "application/octet-stream";
@@ -4119,110 +5040,123 @@ function normalizeProviderModelsData(payload) {
4119
5040
  if (!candidate || ids.includes(candidate)) continue;
4120
5041
  ids.push(candidate);
4121
5042
  }
4122
- return ids;
5043
+ return ids;
5044
+ }
5045
+ async function fetchCustomEndpointDefaultModel(baseUrl, apiKey) {
5046
+ const normalizedBaseUrl = baseUrl.trim();
5047
+ if (!normalizedBaseUrl) return "";
5048
+ try {
5049
+ const modelsUrl = buildProviderModelsUrl(normalizedBaseUrl, null);
5050
+ const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
5051
+ const response = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS) });
5052
+ if (!response.ok) return "";
5053
+ const payload = await response.json();
5054
+ const modelIds = normalizeProviderModelsData(payload);
5055
+ return modelIds[0] ?? "";
5056
+ } catch {
5057
+ return "";
5058
+ }
4123
5059
  }
4124
5060
  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
- });
5061
+ const configPayload = asRecord5(await appServer.rpc("config/read", {}));
5062
+ const config = asRecord5(configPayload?.config);
5063
+ const providerId = readNonEmptyString(config?.model_provider);
5064
+ if (!providerId) {
5065
+ return { data: [], providerId: "", source: "provider" };
5066
+ }
5067
+ const providers = asRecord5(config?.model_providers);
5068
+ const provider = asRecord5(providers?.[providerId]);
5069
+ if (!provider) {
5070
+ logProviderModelDiscoveryWarning("configured provider is missing from model_providers", { providerId });
5071
+ return { data: [], providerId, source: "provider" };
5072
+ }
5073
+ const wireApi = readNonEmptyString(provider.wire_api);
5074
+ if (wireApi !== "responses") {
5075
+ return { data: [], providerId, source: "provider" };
5076
+ }
5077
+ const baseUrl = readNonEmptyString(provider.base_url);
5078
+ if (!baseUrl) {
5079
+ logProviderModelDiscoveryWarning("responses provider is missing base_url", { providerId });
5080
+ return { data: [], providerId, source: "provider" };
5081
+ }
5082
+ const headers = new Headers();
5083
+ const configuredHeaders = asRecord5(provider.http_headers);
5084
+ if (configuredHeaders) {
5085
+ for (const [key, rawValue] of Object.entries(configuredHeaders)) {
5086
+ const normalized = normalizeHeaderValue(rawValue);
5087
+ if (!normalized) continue;
5088
+ headers.set(key, normalized);
5089
+ }
5090
+ }
5091
+ const bearerToken = readNonEmptyString(provider.experimental_bearer_token);
5092
+ if (bearerToken && !headers.has("Authorization")) {
5093
+ headers.set("Authorization", `Bearer ${bearerToken}`);
5094
+ }
5095
+ const envKey = readNonEmptyString(provider.env_key);
5096
+ const envHttpHeaders = asRecord5(provider.env_http_headers);
5097
+ if (envKey || envHttpHeaders) {
5098
+ logProviderModelDiscoveryWarning("provider discovery skipped env-backed auth/header expansion", {
5099
+ providerId,
5100
+ hasEnvKey: Boolean(envKey),
5101
+ hasEnvHttpHeaders: Boolean(envHttpHeaders)
5102
+ });
5103
+ }
5104
+ let requestUrl;
5105
+ try {
5106
+ requestUrl = buildProviderModelsUrl(baseUrl, provider.query_params);
5107
+ } catch (error) {
5108
+ logProviderModelDiscoveryWarning("provider /models URL was invalid", {
5109
+ providerId,
5110
+ error: getErrorMessage5(error, "invalid url")
5111
+ });
5112
+ return { data: [], providerId, source: "provider" };
5113
+ }
5114
+ let response;
5115
+ try {
5116
+ response = await fetch(requestUrl, {
5117
+ method: "GET",
5118
+ headers,
5119
+ signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS)
5120
+ });
5121
+ } catch (error) {
5122
+ logProviderModelDiscoveryWarning("provider /models request failed", {
5123
+ providerId,
5124
+ error: isTimeoutError(error) ? `request timed out after ${PROVIDER_MODELS_FETCH_TIMEOUT_MS}ms` : getErrorMessage5(error, "network error")
5125
+ });
5126
+ return { data: [], providerId, source: "provider" };
5127
+ }
5128
+ let payload = null;
5129
+ try {
5130
+ payload = await response.json();
5131
+ } catch (error) {
5132
+ logProviderModelDiscoveryWarning("provider /models response was not valid JSON", {
5133
+ providerId,
5134
+ status: response.status,
5135
+ error: getErrorMessage5(error, "invalid json")
5136
+ });
5137
+ return { data: [], providerId, source: "provider" };
5138
+ }
5139
+ if (!response.ok) {
5140
+ logProviderModelDiscoveryWarning("provider /models request returned non-2xx", {
5141
+ providerId,
5142
+ status: response.status,
5143
+ statusText: response.statusText
5144
+ });
5145
+ return { data: [], providerId, source: "provider" };
5146
+ }
5147
+ try {
5148
+ return {
5149
+ data: normalizeProviderModelsData(payload),
5150
+ providerId,
5151
+ source: "provider"
5152
+ };
5153
+ } catch (error) {
5154
+ logProviderModelDiscoveryWarning("provider /models payload was invalid", {
5155
+ providerId,
5156
+ error: getErrorMessage5(error, "invalid payload")
5157
+ });
5158
+ return { data: [], providerId, source: "provider" };
5159
+ }
4226
5160
  }
4227
5161
  function extractThreadMessageText(threadReadPayload) {
4228
5162
  const payload = asRecord5(threadReadPayload);
@@ -4573,7 +5507,7 @@ function extractFilePathsFromCommand(cmd, cwd) {
4573
5507
  while ((match = redirectPattern.exec(cmd)) !== null) {
4574
5508
  const p = match[1]?.trim();
4575
5509
  if (p && !p.startsWith("-") && !p.startsWith("/dev/")) {
4576
- paths.push(isAbsolute2(p) ? p : join5(cwd, p));
5510
+ paths.push(isAbsolute2(p) ? p : join6(cwd, p));
4577
5511
  }
4578
5512
  }
4579
5513
  return [...new Set(paths)];
@@ -4707,7 +5641,7 @@ async function revertTurnFileChanges(cwd, turnInfos) {
4707
5641
  try {
4708
5642
  const tracked = await runCommandCapture2("git", ["ls-files", "--full-name"], { cwd: gitRoot });
4709
5643
  for (const f of tracked.split("\n")) {
4710
- if (f.trim()) trackedFiles.add(join5(gitRoot, f.trim()));
5644
+ if (f.trim()) trackedFiles.add(join6(gitRoot, f.trim()));
4711
5645
  }
4712
5646
  } catch {
4713
5647
  }
@@ -4717,7 +5651,7 @@ async function revertTurnFileChanges(cwd, turnInfos) {
4717
5651
  const changes = parseApplyPatchInput(patch.input);
4718
5652
  for (let ci = changes.length - 1; ci >= 0; ci--) {
4719
5653
  const change = changes[ci];
4720
- const filePath = isAbsolute2(change.path) ? change.path : join5(cwd, change.path);
5654
+ const filePath = isAbsolute2(change.path) ? change.path : join6(cwd, change.path);
4721
5655
  try {
4722
5656
  if (change.operation === "add") {
4723
5657
  const fileStat = await stat4(filePath).catch(() => null);
@@ -4935,7 +5869,7 @@ async function listFilesWithRipgrep(cwd) {
4935
5869
  }
4936
5870
  function getCodexHomeDir3() {
4937
5871
  const codexHome = process.env.CODEX_HOME?.trim();
4938
- return codexHome && codexHome.length > 0 ? codexHome : join5(homedir4(), ".codex");
5872
+ return codexHome && codexHome.length > 0 ? codexHome : join6(homedir5(), ".codex");
4939
5873
  }
4940
5874
  async function runCommand3(command, args, options = {}) {
4941
5875
  await new Promise((resolve4, reject) => {
@@ -4973,7 +5907,7 @@ function isNotGitRepositoryError2(error) {
4973
5907
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
4974
5908
  }
4975
5909
  async function ensureRepoHasInitialCommit(repoRoot) {
4976
- const agentsPath = join5(repoRoot, "AGENTS.md");
5910
+ const agentsPath = join6(repoRoot, "AGENTS.md");
4977
5911
  try {
4978
5912
  await stat4(agentsPath);
4979
5913
  } catch {
@@ -5049,7 +5983,7 @@ function normalizeStringRecord(value) {
5049
5983
  return next;
5050
5984
  }
5051
5985
  function getCodexAuthPath() {
5052
- return join5(getCodexHomeDir3(), "auth.json");
5986
+ return join6(getCodexHomeDir3(), "auth.json");
5053
5987
  }
5054
5988
  async function readCodexAuth() {
5055
5989
  try {
@@ -5063,13 +5997,157 @@ async function readCodexAuth() {
5063
5997
  }
5064
5998
  }
5065
5999
  function getCodexGlobalStatePath() {
5066
- return join5(getCodexHomeDir3(), ".codex-global-state.json");
6000
+ return join6(getCodexHomeDir3(), ".codex-global-state.json");
5067
6001
  }
5068
6002
  function getTelegramBridgeConfigPath() {
5069
- return join5(getCodexHomeDir3(), "telegram-bridge.json");
6003
+ return join6(getCodexHomeDir3(), "telegram-bridge.json");
5070
6004
  }
5071
6005
  function getCodexSessionIndexPath() {
5072
- return join5(getCodexHomeDir3(), "session_index.jsonl");
6006
+ return join6(getCodexHomeDir3(), "session_index.jsonl");
6007
+ }
6008
+ function getCodexAutomationsDir() {
6009
+ return join6(getCodexHomeDir3(), "automations");
6010
+ }
6011
+ function readTomlString(value) {
6012
+ const trimmed = value.trim();
6013
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
6014
+ try {
6015
+ return JSON.parse(trimmed);
6016
+ } catch {
6017
+ return trimmed.slice(1, -1);
6018
+ }
6019
+ }
6020
+ return trimmed;
6021
+ }
6022
+ function serializeTomlString(value) {
6023
+ return JSON.stringify(value);
6024
+ }
6025
+ function parseAutomationToml(raw) {
6026
+ const values = {};
6027
+ for (const line of raw.split(/\r?\n/u)) {
6028
+ const trimmed = line.trim();
6029
+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
6030
+ const separatorIndex = trimmed.indexOf("=");
6031
+ const key = trimmed.slice(0, separatorIndex).trim();
6032
+ const value = trimmed.slice(separatorIndex + 1).trim();
6033
+ if (key) values[key] = value;
6034
+ }
6035
+ const id = readTomlString(values.id ?? "");
6036
+ const kindValue = readTomlString(values.kind ?? "heartbeat");
6037
+ const name = readTomlString(values.name ?? "");
6038
+ const prompt = readTomlString(values.prompt ?? "");
6039
+ const rrule = readTomlString(values.rrule ?? "");
6040
+ const statusValue = readTomlString(values.status ?? "ACTIVE");
6041
+ const targetThreadId = readTomlString(values.target_thread_id ?? "") || null;
6042
+ const createdAtMs = Number.parseInt(values.created_at ?? "", 10);
6043
+ const updatedAtMs = Number.parseInt(values.updated_at ?? "", 10);
6044
+ if (!id || !name || !prompt || !rrule) return null;
6045
+ if (kindValue !== "heartbeat" && kindValue !== "cron") return null;
6046
+ if (statusValue !== "ACTIVE" && statusValue !== "PAUSED") return null;
6047
+ return {
6048
+ id,
6049
+ kind: kindValue,
6050
+ name,
6051
+ prompt,
6052
+ rrule,
6053
+ status: statusValue,
6054
+ targetThreadId,
6055
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
6056
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
6057
+ nextRunAtMs: null
6058
+ };
6059
+ }
6060
+ function serializeAutomationToml(record) {
6061
+ const lines = [
6062
+ "version = 1",
6063
+ `id = ${serializeTomlString(record.id)}`,
6064
+ `kind = ${serializeTomlString(record.kind)}`,
6065
+ `name = ${serializeTomlString(record.name)}`,
6066
+ `prompt = ${serializeTomlString(record.prompt)}`,
6067
+ `status = ${serializeTomlString(record.status)}`,
6068
+ `rrule = ${serializeTomlString(record.rrule)}`,
6069
+ `target_thread_id = ${serializeTomlString(record.targetThreadId ?? "")}`,
6070
+ `created_at = ${String(record.createdAtMs ?? Date.now())}`,
6071
+ `updated_at = ${String(record.updatedAtMs ?? Date.now())}`
6072
+ ];
6073
+ return `${lines.join("\n")}
6074
+ `;
6075
+ }
6076
+ function slugifyAutomationId(threadId, name) {
6077
+ const preferred = name.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
6078
+ if (preferred) return preferred.slice(0, 48);
6079
+ const fallback = threadId.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
6080
+ return `heartbeat-${fallback.slice(0, 24) || randomBytes(4).toString("hex")}`;
6081
+ }
6082
+ async function readAutomationRecordFromFile(filePath) {
6083
+ try {
6084
+ return parseAutomationToml(await readFile3(filePath, "utf8"));
6085
+ } catch {
6086
+ return null;
6087
+ }
6088
+ }
6089
+ async function listThreadHeartbeatAutomations() {
6090
+ const automationRoot = getCodexAutomationsDir();
6091
+ const next = {};
6092
+ let entries;
6093
+ try {
6094
+ entries = await readdir2(automationRoot, { withFileTypes: true });
6095
+ } catch {
6096
+ return next;
6097
+ }
6098
+ for (const entry of entries) {
6099
+ if (!entry.isDirectory()) continue;
6100
+ const automation = await readAutomationRecordFromFile(join6(automationRoot, entry.name, "automation.toml"));
6101
+ if (!automation || automation.kind !== "heartbeat" || !automation.targetThreadId) continue;
6102
+ next[automation.targetThreadId] = automation;
6103
+ }
6104
+ return next;
6105
+ }
6106
+ async function readThreadHeartbeatAutomation(threadId) {
6107
+ const all = await listThreadHeartbeatAutomations();
6108
+ return all[threadId] ?? null;
6109
+ }
6110
+ async function writeThreadHeartbeatAutomation(input) {
6111
+ const threadId = input.threadId.trim();
6112
+ const name = input.name.trim();
6113
+ const prompt = input.prompt.trim();
6114
+ const rrule = input.rrule.trim();
6115
+ if (!threadId || !name || !prompt || !rrule) {
6116
+ throw new Error("threadId, name, prompt, and rrule are required");
6117
+ }
6118
+ const automationRoot = getCodexAutomationsDir();
6119
+ await mkdir4(automationRoot, { recursive: true });
6120
+ const existing = await readThreadHeartbeatAutomation(threadId);
6121
+ const id = existing?.id ?? slugifyAutomationId(threadId, name);
6122
+ const automationDir = join6(automationRoot, id);
6123
+ const now = Date.now();
6124
+ const record = {
6125
+ id,
6126
+ kind: "heartbeat",
6127
+ name,
6128
+ prompt,
6129
+ rrule,
6130
+ status: input.status,
6131
+ targetThreadId: threadId,
6132
+ createdAtMs: existing?.createdAtMs ?? now,
6133
+ updatedAtMs: now,
6134
+ nextRunAtMs: null
6135
+ };
6136
+ await mkdir4(automationDir, { recursive: true });
6137
+ await writeFile4(join6(automationDir, "automation.toml"), serializeAutomationToml(record), "utf8");
6138
+ const memoryPath = join6(automationDir, "memory.md");
6139
+ try {
6140
+ await stat4(memoryPath);
6141
+ } catch {
6142
+ await writeFile4(memoryPath, "", "utf8");
6143
+ }
6144
+ return record;
6145
+ }
6146
+ async function deleteThreadHeartbeatAutomation(threadId) {
6147
+ const automation = await readThreadHeartbeatAutomation(threadId.trim());
6148
+ if (!automation) return false;
6149
+ await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
6150
+ return true;
5073
6151
  }
5074
6152
  var MAX_THREAD_TITLES = 500;
5075
6153
  var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
@@ -5409,10 +6487,10 @@ function handleFileUpload(req, res) {
5409
6487
  setJson4(res, 400, { error: "No file in request" });
5410
6488
  return;
5411
6489
  }
5412
- const uploadDir = join5(tmpdir4(), "codex-web-uploads");
6490
+ const uploadDir = join6(tmpdir4(), "codex-web-uploads");
5413
6491
  await mkdir4(uploadDir, { recursive: true });
5414
- const destDir = await mkdtemp3(join5(uploadDir, "f-"));
5415
- const destPath = join5(destDir, fileName);
6492
+ const destDir = await mkdtemp3(join6(uploadDir, "f-"));
6493
+ const destPath = join6(destDir, fileName);
5416
6494
  await writeFile4(destPath, fileData);
5417
6495
  setJson4(res, 200, { path: destPath });
5418
6496
  } catch (err) {
@@ -5424,7 +6502,7 @@ function handleFileUpload(req, res) {
5424
6502
  });
5425
6503
  }
5426
6504
  function httpPost(url, headers, body) {
5427
- const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
6505
+ const doRequest = url.startsWith("http://") ? httpRequest2 : httpsRequest2;
5428
6506
  return new Promise((resolve4, reject) => {
5429
6507
  const req = doRequest(url, { method: "POST", headers }, (res) => {
5430
6508
  const chunks = [];
@@ -5470,34 +6548,32 @@ function curlImpersonatePost(url, headers, body) {
5470
6548
  });
5471
6549
  }
5472
6550
  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
- }
6551
+ const chatgptHeaders = {
6552
+ "Content-Type": contentType,
6553
+ "Content-Length": body.length,
6554
+ Authorization: `Bearer ${authToken}`,
6555
+ originator: "Codex Desktop",
6556
+ "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
6557
+ };
6558
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
6559
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
6560
+ let result;
6561
+ try {
6562
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
6563
+ } catch {
6564
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
6565
+ }
6566
+ if (result.status === 403 && result.body.includes("cf_chl")) {
6567
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
6568
+ try {
6569
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
6570
+ if (ciResult.status !== 403) return ciResult;
6571
+ } catch {
5496
6572
  }
5497
- return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
5498
6573
  }
5499
- return result;
5500
- });
6574
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
6575
+ }
6576
+ return result;
5501
6577
  }
5502
6578
  var STREAM_EVENT_BUFFER_LIMIT = 400;
5503
6579
  var MERGEABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
@@ -5528,7 +6604,7 @@ var AppServerProcess = class {
5528
6604
  }
5529
6605
  return codexCommand;
5530
6606
  }
5531
- buildAppServerArgs() {
6607
+ buildAppServerConfig() {
5532
6608
  const args = [
5533
6609
  "app-server",
5534
6610
  "-c",
@@ -5536,20 +6612,25 @@ var AppServerProcess = class {
5536
6612
  "-c",
5537
6613
  'sandbox_mode="danger-full-access"'
5538
6614
  ];
5539
- const statePath = join5(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
6615
+ let extraEnv = {};
6616
+ const serverPort = parseInt(process.env.CODEXUI_SERVER_PORT ?? "", 10) || void 0;
6617
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
5540
6618
  try {
5541
6619
  const raw = readFileSync(statePath, "utf8");
5542
6620
  const state = JSON.parse(raw);
5543
- args.push(...getFreeModeConfigArgs(state));
6621
+ args.push(...getFreeModeConfigArgs(state, serverPort));
6622
+ extraEnv = getFreeModeEnvVars(state);
5544
6623
  } catch {
5545
6624
  }
5546
- return args;
6625
+ return { args, env: extraEnv };
5547
6626
  }
5548
6627
  start() {
5549
6628
  if (this.process) return;
5550
6629
  this.stopping = false;
5551
- const invocation = getSpawnInvocation(this.getCodexCommand(), this.buildAppServerArgs());
5552
- const proc = spawn4(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
6630
+ const config = this.buildAppServerConfig();
6631
+ const invocation = getSpawnInvocation(this.getCodexCommand(), config.args);
6632
+ const spawnEnv = Object.keys(config.env).length > 0 ? { ...process.env, ...config.env } : void 0;
6633
+ const proc = spawn4(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"], ...spawnEnv ? { env: spawnEnv } : {} });
5553
6634
  this.process = proc;
5554
6635
  proc.stdout.setEncoding("utf8");
5555
6636
  proc.stdout.on("data", (chunk) => {
@@ -5830,7 +6911,7 @@ var AppServerProcess = class {
5830
6911
  }
5831
6912
  async rpc(method, params) {
5832
6913
  await this.ensureInitialized();
5833
- return Sentry4.startSpan({ name: `rpc ${method}`, op: "rpc.codex" }, () => this.call(method, params));
6914
+ return this.call(method, params);
5834
6915
  }
5835
6916
  onNotification(listener) {
5836
6917
  this.notificationListeners.add(listener);
@@ -5965,9 +7046,9 @@ var MethodCatalog = class {
5965
7046
  if (this.methodCache) {
5966
7047
  return this.methodCache;
5967
7048
  }
5968
- const outDir = await mkdtemp3(join5(tmpdir4(), "codex-web-local-schema-"));
7049
+ const outDir = await mkdtemp3(join6(tmpdir4(), "codex-web-local-schema-"));
5969
7050
  await this.runGenerateSchemaCommand(outDir);
5970
- const clientRequestPath = join5(outDir, "ClientRequest.json");
7051
+ const clientRequestPath = join6(outDir, "ClientRequest.json");
5971
7052
  const raw = await readFile3(clientRequestPath, "utf8");
5972
7053
  const parsed = JSON.parse(raw);
5973
7054
  const methods = this.extractMethodsFromClientRequest(parsed);
@@ -5978,9 +7059,9 @@ var MethodCatalog = class {
5978
7059
  if (this.notificationCache) {
5979
7060
  return this.notificationCache;
5980
7061
  }
5981
- const outDir = await mkdtemp3(join5(tmpdir4(), "codex-web-local-schema-"));
7062
+ const outDir = await mkdtemp3(join6(tmpdir4(), "codex-web-local-schema-"));
5982
7063
  await this.runGenerateSchemaCommand(outDir);
5983
- const serverNotificationPath = join5(outDir, "ServerNotification.json");
7064
+ const serverNotificationPath = join6(outDir, "ServerNotification.json");
5984
7065
  const raw = await readFile3(serverNotificationPath, "utf8");
5985
7066
  const parsed = JSON.parse(raw);
5986
7067
  const methods = this.extractMethodsFromServerNotification(parsed);
@@ -5994,15 +7075,18 @@ function getSharedBridgeState() {
5994
7075
  const globalScope = globalThis;
5995
7076
  const existing = globalScope[SHARED_BRIDGE_KEY];
5996
7077
  if (existing) {
5997
- if (existing.version === SHARED_BRIDGE_VERSION) {
7078
+ if (existing.version === SHARED_BRIDGE_VERSION && existing.terminalManager) {
5998
7079
  return existing;
5999
7080
  }
6000
7081
  existing.appServer.dispose();
7082
+ existing.terminalManager?.dispose();
6001
7083
  }
6002
7084
  const appServer = new AppServerProcess();
7085
+ const terminalManager = new ThreadTerminalManager();
6003
7086
  const created = {
6004
7087
  version: SHARED_BRIDGE_VERSION,
6005
7088
  appServer,
7089
+ terminalManager,
6006
7090
  methodCatalog: new MethodCatalog(),
6007
7091
  telegramBridge: new TelegramThreadBridge(appServer, {
6008
7092
  onChatSeen: (chatId) => {
@@ -6015,69 +7099,67 @@ function getSharedBridgeState() {
6015
7099
  return created;
6016
7100
  }
6017
7101
  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]);
7102
+ const threads = [];
7103
+ let cursor = null;
7104
+ do {
7105
+ const response = asRecord5(await appServer.rpc("thread/list", {
7106
+ archived: false,
7107
+ limit: 100,
7108
+ sortKey: "updated_at",
7109
+ modelProviders: [],
7110
+ cursor
7111
+ }));
7112
+ const data = Array.isArray(response?.data) ? response.data : [];
7113
+ for (const row of data) {
7114
+ const record = asRecord5(row);
7115
+ const id = typeof record?.id === "string" ? record.id : "";
7116
+ if (!id) continue;
7117
+ 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";
7118
+ const preview = typeof record?.preview === "string" ? record.preview : "";
7119
+ threads.push({ id, title, preview });
7120
+ }
7121
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
7122
+ } while (cursor);
7123
+ const docs = threads.map((thread) => {
7124
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
7125
+ return {
7126
+ id: thread.id,
7127
+ title: thread.title,
7128
+ preview: thread.preview,
7129
+ messageText: "",
7130
+ searchableText
7131
+ };
7132
+ });
7133
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
7134
+ const fullTextThreads = threads.slice(0, THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT);
7135
+ const concurrency = 4;
7136
+ for (let offset = 0; offset < fullTextThreads.length; offset += concurrency) {
7137
+ const batch = fullTextThreads.slice(offset, offset + concurrency);
7138
+ const loaded = await Promise.all(batch.map(async (thread) => {
7139
+ try {
7140
+ const readResponse = await appServer.rpc("thread/read", {
7141
+ threadId: thread.id,
7142
+ includeTurns: true
7143
+ });
7144
+ const messageText = extractThreadMessageText(readResponse);
7145
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
7146
+ return [thread.id, {
7147
+ id: thread.id,
7148
+ title: thread.title,
7149
+ preview: thread.preview,
7150
+ messageText,
7151
+ searchableText
7152
+ }];
7153
+ } catch {
7154
+ return null;
6077
7155
  }
7156
+ }));
7157
+ for (const row of loaded) {
7158
+ if (!row) continue;
7159
+ docsById.set(row[0], row[1]);
6078
7160
  }
6079
- return Array.from(docsById.values());
6080
- });
7161
+ }
7162
+ return Array.from(docsById.values());
6081
7163
  }
6082
7164
  async function buildThreadSearchIndex(appServer) {
6083
7165
  const docs = await loadAllThreadsForSearch(appServer);
@@ -6085,7 +7167,7 @@ async function buildThreadSearchIndex(appServer) {
6085
7167
  return { docsById };
6086
7168
  }
6087
7169
  function createCodexBridgeMiddleware() {
6088
- const { appServer, methodCatalog, telegramBridge } = getSharedBridgeState();
7170
+ const { appServer, terminalManager, methodCatalog, telegramBridge } = getSharedBridgeState();
6089
7171
  let threadSearchIndex = null;
6090
7172
  let threadSearchIndexPromise = null;
6091
7173
  async function getThreadSearchIndex() {
@@ -6141,7 +7223,7 @@ function createCodexBridgeMiddleware() {
6141
7223
  if (!shouldLog) return;
6142
7224
  didLog = true;
6143
7225
  const rpcPart = rpcMethod ? `, rpcMethod=${rpcMethod}` : "";
6144
- console.info(`[codex-api-perf] ${requestMethod} ${requestPath} -> ${res.statusCode} (${durationMs}ms, bodyMB=${bodyMbValue.toFixed(4)}${rpcPart})`);
7226
+ console.info(`[codex-api-perf] ${requestMethod} ${requestPath} -> ${res.statusCode} (${durationMs}ms, bodyMB=${bodyMbValue.toFixed(1)}${rpcPart})`);
6145
7227
  };
6146
7228
  res.once("finish", logApiRequestDuration);
6147
7229
  res.once("close", logApiRequestDuration);
@@ -6151,6 +7233,47 @@ function createCodexBridgeMiddleware() {
6151
7233
  return;
6152
7234
  }
6153
7235
  const url = new URL(req.url, "http://localhost");
7236
+ if (url.pathname === "/codex-api/zen-proxy/v1/responses" && req.method === "POST") {
7237
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7238
+ let bearerToken = "";
7239
+ let wireApi = "chat";
7240
+ try {
7241
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
7242
+ bearerToken = state.apiKey ?? "";
7243
+ wireApi = state.wireApi === "responses" ? "responses" : "chat";
7244
+ } catch {
7245
+ }
7246
+ handleZenProxyRequest(req, res, bearerToken, wireApi);
7247
+ return;
7248
+ }
7249
+ if (url.pathname === "/codex-api/openrouter-proxy/v1/responses" && req.method === "POST") {
7250
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7251
+ let bearerToken = "";
7252
+ let wireApi = "responses";
7253
+ try {
7254
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
7255
+ bearerToken = state.apiKey ?? "";
7256
+ wireApi = state.wireApi === "chat" ? "chat" : "responses";
7257
+ } catch {
7258
+ }
7259
+ handleOpenRouterProxyRequest(req, res, bearerToken, wireApi);
7260
+ return;
7261
+ }
7262
+ if (url.pathname === "/codex-api/custom-proxy/v1/responses" && req.method === "POST") {
7263
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7264
+ let bearerToken = "";
7265
+ let wireApi = "responses";
7266
+ let baseUrl = "";
7267
+ try {
7268
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
7269
+ bearerToken = state.apiKey ?? "";
7270
+ wireApi = state.wireApi === "chat" ? "chat" : "responses";
7271
+ baseUrl = state.customBaseUrl ?? "";
7272
+ } catch {
7273
+ }
7274
+ handleCustomEndpointProxyRequest(req, res, { baseUrl, bearerToken, wireApi });
7275
+ return;
7276
+ }
6154
7277
  if (url.pathname.startsWith("/codex-api/free-mode")) {
6155
7278
  let readFreeModeState2 = function() {
6156
7279
  try {
@@ -6160,7 +7283,7 @@ function createCodexBridgeMiddleware() {
6160
7283
  }
6161
7284
  };
6162
7285
  var readFreeModeState = readFreeModeState2;
6163
- const statePath = join5(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
7286
+ const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
6164
7287
  if (req.method === "POST" && url.pathname === "/codex-api/free-mode") {
6165
7288
  try {
6166
7289
  const body = await readJsonBody(req);
@@ -6171,7 +7294,19 @@ function createCodexBridgeMiddleware() {
6171
7294
  setJson4(res, 500, { error: "No free keys available" });
6172
7295
  return;
6173
7296
  }
6174
- const state = { enabled: true, apiKey, model: FREE_MODE_DEFAULT_MODEL, provider: "openrouter" };
7297
+ const prev = readFreeModeState2();
7298
+ const prevKeys = prev.providerKeys ?? {};
7299
+ if (prev.provider && prev.apiKey) {
7300
+ prevKeys[prev.provider] = prev.apiKey;
7301
+ }
7302
+ const state = {
7303
+ enabled: true,
7304
+ apiKey,
7305
+ model: FREE_MODE_DEFAULT_MODEL,
7306
+ provider: "openrouter",
7307
+ wireApi: prev.wireApi === "chat" ? "chat" : "responses",
7308
+ providerKeys: prevKeys
7309
+ };
6175
7310
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6176
7311
  appServer.dispose();
6177
7312
  const freeModels = await getFreeModels();
@@ -6183,7 +7318,18 @@ function createCodexBridgeMiddleware() {
6183
7318
  models: freeModels
6184
7319
  });
6185
7320
  } else {
6186
- const state = { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
7321
+ const prev = readFreeModeState2();
7322
+ const prevKeys = prev.providerKeys ?? {};
7323
+ if (prev.provider && prev.apiKey) {
7324
+ prevKeys[prev.provider] = prev.apiKey;
7325
+ }
7326
+ const state = {
7327
+ enabled: false,
7328
+ apiKey: null,
7329
+ model: FREE_MODE_DEFAULT_MODEL,
7330
+ wireApi: prev.wireApi === "chat" ? "chat" : "responses",
7331
+ providerKeys: prevKeys
7332
+ };
6187
7333
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6188
7334
  appServer.dispose();
6189
7335
  setJson4(res, 200, { ok: true, enabled: false });
@@ -6206,7 +7352,8 @@ function createCodexBridgeMiddleware() {
6206
7352
  customKey: Boolean(state.customKey),
6207
7353
  maskedKey,
6208
7354
  provider: state.provider ?? "openrouter",
6209
- customBaseUrl: state.customBaseUrl ?? null
7355
+ customBaseUrl: state.customBaseUrl ?? null,
7356
+ wireApi: state.wireApi ?? null
6210
7357
  });
6211
7358
  } catch (error) {
6212
7359
  setJson4(res, 500, { error: getErrorMessage5(error, "Failed to read free mode status") });
@@ -6236,13 +7383,26 @@ function createCodexBridgeMiddleware() {
6236
7383
  const key = typeof body?.key === "string" ? body.key.trim() : "";
6237
7384
  const current = readFreeModeState2();
6238
7385
  if (key.length > 0) {
6239
- const state = { ...current, enabled: true, apiKey: key, customKey: true, provider: "openrouter" };
7386
+ const state = {
7387
+ ...current,
7388
+ enabled: true,
7389
+ apiKey: key,
7390
+ customKey: true,
7391
+ provider: "openrouter",
7392
+ wireApi: current.wireApi === "chat" ? "chat" : "responses"
7393
+ };
6240
7394
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6241
7395
  appServer.dispose();
6242
7396
  setJson4(res, 200, { ok: true, customKey: true });
6243
7397
  } else {
6244
7398
  const communityKey = getRandomFreeKey();
6245
- const state = { ...current, apiKey: communityKey, customKey: false, provider: "openrouter" };
7399
+ const state = {
7400
+ ...current,
7401
+ apiKey: communityKey,
7402
+ customKey: false,
7403
+ provider: "openrouter",
7404
+ wireApi: current.wireApi === "chat" ? "chat" : "responses"
7405
+ };
6246
7406
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6247
7407
  appServer.dispose();
6248
7408
  setJson4(res, 200, { ok: true, customKey: false });
@@ -6257,17 +7417,31 @@ function createCodexBridgeMiddleware() {
6257
7417
  const body = await readJsonBody(req);
6258
7418
  const baseUrl = typeof body?.baseUrl === "string" ? body.baseUrl.trim() : "";
6259
7419
  const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
6260
- if (!baseUrl) {
7420
+ const wireApi = body?.wireApi === "chat" ? "chat" : "responses";
7421
+ const providerType = body?.provider === "opencode-zen" ? "opencode-zen" : body?.provider === "openrouter" ? "openrouter" : "custom";
7422
+ if (providerType === "custom" && !baseUrl) {
6261
7423
  setJson4(res, 400, { error: "baseUrl is required" });
6262
7424
  return;
6263
7425
  }
7426
+ const current = readFreeModeState2();
7427
+ const prevKeys = current.providerKeys ?? {};
7428
+ if (current.provider && current.apiKey) {
7429
+ prevKeys[current.provider] = current.apiKey;
7430
+ }
7431
+ const resolvedKey = apiKey || prevKeys[providerType] || "";
7432
+ if (resolvedKey) {
7433
+ prevKeys[providerType] = resolvedKey;
7434
+ }
7435
+ const resolvedModel = providerType === "openrouter" ? current.model || FREE_MODE_DEFAULT_MODEL : providerType === "custom" ? await fetchCustomEndpointDefaultModel(baseUrl, resolvedKey) : "";
6264
7436
  const state = {
6265
7437
  enabled: true,
6266
- apiKey: apiKey || "dummy",
6267
- model: "",
6268
- customKey: true,
6269
- provider: "custom",
6270
- customBaseUrl: baseUrl
7438
+ apiKey: resolvedKey,
7439
+ model: resolvedModel,
7440
+ customKey: providerType === "openrouter" ? current.customKey : true,
7441
+ provider: providerType,
7442
+ customBaseUrl: providerType === "custom" ? baseUrl : void 0,
7443
+ wireApi,
7444
+ providerKeys: prevKeys
6271
7445
  };
6272
7446
  await writeFile4(statePath, JSON.stringify(state), "utf8");
6273
7447
  appServer.dispose();
@@ -6280,23 +7454,6 @@ function createCodexBridgeMiddleware() {
6280
7454
  next();
6281
7455
  return;
6282
7456
  }
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
7457
  if (await handleAccountRoutes(req, res, url, { appServer })) {
6301
7458
  return;
6302
7459
  }
@@ -6306,10 +7463,66 @@ function createCodexBridgeMiddleware() {
6306
7463
  if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
6307
7464
  return;
6308
7465
  }
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 });
7466
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/attach") {
7467
+ const body = asRecord5(await readJsonBody(req));
7468
+ const threadId = readNonEmptyString(body?.threadId);
7469
+ const cwd = readNonEmptyString(body?.cwd);
7470
+ if (!threadId || !cwd) {
7471
+ setJson4(res, 400, { error: "Missing threadId or cwd" });
7472
+ return;
7473
+ }
7474
+ const session = terminalManager.attach({
7475
+ threadId,
7476
+ cwd,
7477
+ sessionId: readNonEmptyString(body?.sessionId) || void 0,
7478
+ cols: typeof body?.cols === "number" ? body.cols : void 0,
7479
+ rows: typeof body?.rows === "number" ? body.rows : void 0,
7480
+ newSession: body?.newSession === true
7481
+ });
7482
+ setJson4(res, 200, { session });
7483
+ return;
7484
+ }
7485
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/input") {
7486
+ const body = asRecord5(await readJsonBody(req));
7487
+ const sessionId = readNonEmptyString(body?.sessionId);
7488
+ const data = typeof body?.data === "string" ? body.data : "";
7489
+ if (!sessionId) {
7490
+ setJson4(res, 400, { error: "Missing sessionId" });
7491
+ return;
7492
+ }
7493
+ terminalManager.write(sessionId, data);
7494
+ setJson4(res, 200, { ok: true });
7495
+ return;
7496
+ }
7497
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/resize") {
7498
+ const body = asRecord5(await readJsonBody(req));
7499
+ const sessionId = readNonEmptyString(body?.sessionId);
7500
+ if (!sessionId) {
7501
+ setJson4(res, 400, { error: "Missing sessionId" });
7502
+ return;
7503
+ }
7504
+ terminalManager.resize(sessionId, body?.cols, body?.rows);
7505
+ setJson4(res, 200, { ok: true });
7506
+ return;
7507
+ }
7508
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/close") {
7509
+ const body = asRecord5(await readJsonBody(req));
7510
+ const sessionId = readNonEmptyString(body?.sessionId);
7511
+ if (!sessionId) {
7512
+ setJson4(res, 400, { error: "Missing sessionId" });
7513
+ return;
7514
+ }
7515
+ terminalManager.close(sessionId);
7516
+ setJson4(res, 200, { ok: true });
7517
+ return;
7518
+ }
7519
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal-snapshot") {
7520
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
7521
+ if (!threadId) {
7522
+ setJson4(res, 400, { error: "Missing threadId" });
7523
+ return;
7524
+ }
7525
+ setJson4(res, 200, { session: terminalManager.getSnapshotForThread(threadId) });
6313
7526
  return;
6314
7527
  }
6315
7528
  if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
@@ -6319,6 +7532,10 @@ function createCodexBridgeMiddleware() {
6319
7532
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
6320
7533
  const payload = await readJsonBody(req);
6321
7534
  const body = asRecord5(payload);
7535
+ if (payload !== null && payload !== void 0) {
7536
+ requestBodyBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
7537
+ }
7538
+ rpcMethod = body?.method && typeof body.method === "string" ? body.method : null;
6322
7539
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
6323
7540
  setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
6324
7541
  return;
@@ -6343,25 +7560,23 @@ function createCodexBridgeMiddleware() {
6343
7560
  setJson4(res, 400, { error: "Missing threadId" });
6344
7561
  return;
6345
7562
  }
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
- }
7563
+ const threadReadResult = await appServer.rpc("thread/read", {
7564
+ threadId,
7565
+ includeTurns: true
6364
7566
  });
7567
+ const threadReadRecord = asRecord5(threadReadResult);
7568
+ const threadRecord = asRecord5(threadReadRecord?.thread);
7569
+ const sessionPath = readNonEmptyString(threadRecord?.path);
7570
+ if (!sessionPath || !isAbsolute2(sessionPath)) {
7571
+ setJson4(res, 200, { data: [] });
7572
+ return;
7573
+ }
7574
+ try {
7575
+ const sessionLogRaw = await readFile3(sessionPath, "utf8");
7576
+ setJson4(res, 200, { data: buildSessionFileChangeFallback(threadReadResult, sessionLogRaw) });
7577
+ } catch {
7578
+ setJson4(res, 200, { data: [] });
7579
+ }
6365
7580
  return;
6366
7581
  }
6367
7582
  if (req.method === "GET" && url.pathname === "/codex-api/thread-stream-events") {
@@ -6551,8 +7766,29 @@ function createCodexBridgeMiddleware() {
6551
7766
  }
6552
7767
  if (req.method === "GET" && url.pathname === "/codex-api/provider-models") {
6553
7768
  try {
6554
- const fmState = JSON.parse(readFileSync(join5(getCodexHomeDir3(), FREE_MODE_STATE_FILE), "utf8"));
7769
+ const fmState = JSON.parse(readFileSync(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE), "utf8"));
6555
7770
  if (fmState.enabled) {
7771
+ if (fmState.provider === "opencode-zen") {
7772
+ try {
7773
+ const modelsUrl = "https://opencode.ai/zen/v1/models";
7774
+ const headers = {};
7775
+ if (fmState.apiKey && fmState.apiKey !== "dummy") {
7776
+ headers["Authorization"] = `Bearer ${fmState.apiKey}`;
7777
+ }
7778
+ const resp = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8e3) });
7779
+ if (resp.ok) {
7780
+ const json = await resp.json();
7781
+ const allIds = (json.data ?? []).map((m) => m.id).filter(Boolean);
7782
+ const freeIds = allIds.filter((id) => id.endsWith("-free") || id === "big-pickle");
7783
+ const paidIds = allIds.filter((id) => !id.endsWith("-free") && id !== "big-pickle");
7784
+ setJson4(res, 200, { data: [...freeIds, ...paidIds], exclusive: true, source: "opencode-zen" });
7785
+ return;
7786
+ }
7787
+ } catch {
7788
+ }
7789
+ setJson4(res, 200, { data: ["big-pickle", "minimax-m2.5-free", "nemotron-3-super-free", "trinity-large-preview-free"], exclusive: true, source: "opencode-zen" });
7790
+ return;
7791
+ }
6556
7792
  if (fmState.provider === "custom" && fmState.customBaseUrl) {
6557
7793
  try {
6558
7794
  const modelsUrl = fmState.customBaseUrl.replace(/\/+$/, "") + "/models";
@@ -6563,8 +7799,10 @@ function createCodexBridgeMiddleware() {
6563
7799
  const resp = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8e3) });
6564
7800
  if (resp.ok) {
6565
7801
  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" });
7802
+ const ids = normalizeProviderModelsData(json);
7803
+ const currentModel = fmState.model?.trim() ?? "";
7804
+ const orderedIds = currentModel && ids.includes(currentModel) ? [currentModel, ...ids.filter((id) => id !== currentModel)] : ids;
7805
+ setJson4(res, 200, { data: orderedIds, exclusive: true, source: "custom" });
6568
7806
  return;
6569
7807
  }
6570
7808
  } catch {
@@ -6588,7 +7826,7 @@ function createCodexBridgeMiddleware() {
6588
7826
  return;
6589
7827
  }
6590
7828
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
6591
- setJson4(res, 200, { data: { path: homedir4() } });
7829
+ setJson4(res, 200, { data: { path: homedir5() } });
6592
7830
  return;
6593
7831
  }
6594
7832
  if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
@@ -6632,22 +7870,22 @@ function createCodexBridgeMiddleware() {
6632
7870
  await runCommand3("git", ["init"], { cwd: sourceCwd });
6633
7871
  gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
6634
7872
  }
6635
- const repoName = basename3(gitRoot) || "repo";
6636
- const worktreesRoot = join5(getCodexHomeDir3(), "worktrees");
7873
+ const repoName = basename4(gitRoot) || "repo";
7874
+ const worktreesRoot = join6(getCodexHomeDir3(), "worktrees");
6637
7875
  await mkdir4(worktreesRoot, { recursive: true });
6638
7876
  let worktreeId = "";
6639
7877
  let worktreeParent = "";
6640
7878
  let worktreeCwd = "";
6641
7879
  for (let attempt = 0; attempt < 12; attempt += 1) {
6642
7880
  const candidate = randomBytes(2).toString("hex");
6643
- const parent = join5(worktreesRoot, candidate);
7881
+ const parent = join6(worktreesRoot, candidate);
6644
7882
  try {
6645
7883
  await stat4(parent);
6646
7884
  continue;
6647
7885
  } catch {
6648
7886
  worktreeId = candidate;
6649
7887
  worktreeParent = parent;
6650
- worktreeCwd = join5(parent, repoName);
7888
+ worktreeCwd = join6(parent, repoName);
6651
7889
  break;
6652
7890
  }
6653
7891
  }
@@ -6944,7 +8182,7 @@ function createCodexBridgeMiddleware() {
6944
8182
  let index = 1;
6945
8183
  while (index < 1e5) {
6946
8184
  const candidateName = `New Project (${String(index)})`;
6947
- const candidatePath = join5(normalizedBasePath, candidateName);
8185
+ const candidatePath = join6(normalizedBasePath, candidateName);
6948
8186
  try {
6949
8187
  await stat4(candidatePath);
6950
8188
  index += 1;
@@ -6997,6 +8235,21 @@ function createCodexBridgeMiddleware() {
6997
8235
  setJson4(res, 200, { data: { threadIds } });
6998
8236
  return;
6999
8237
  }
8238
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-automations") {
8239
+ const automationsByThreadId = await listThreadHeartbeatAutomations();
8240
+ setJson4(res, 200, { data: automationsByThreadId });
8241
+ return;
8242
+ }
8243
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
8244
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
8245
+ if (!threadId) {
8246
+ setJson4(res, 400, { error: "Missing threadId" });
8247
+ return;
8248
+ }
8249
+ const automation = await readThreadHeartbeatAutomation(threadId);
8250
+ setJson4(res, 200, { data: automation });
8251
+ return;
8252
+ }
7000
8253
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
7001
8254
  const payload = asRecord5(await readJsonBody(req));
7002
8255
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
@@ -7032,6 +8285,31 @@ function createCodexBridgeMiddleware() {
7032
8285
  setJson4(res, 200, { ok: true });
7033
8286
  return;
7034
8287
  }
8288
+ if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
8289
+ const payload = asRecord5(await readJsonBody(req));
8290
+ const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
8291
+ const name = typeof payload?.name === "string" ? payload.name.trim() : "";
8292
+ const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
8293
+ const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
8294
+ const status = payload?.status === "PAUSED" ? "PAUSED" : "ACTIVE";
8295
+ if (!threadId || !name || !prompt || !rrule) {
8296
+ setJson4(res, 400, { error: "threadId, name, prompt, and rrule are required" });
8297
+ return;
8298
+ }
8299
+ const automation = await writeThreadHeartbeatAutomation({ threadId, name, prompt, rrule, status });
8300
+ setJson4(res, 200, { data: automation });
8301
+ return;
8302
+ }
8303
+ if (req.method === "DELETE" && url.pathname === "/codex-api/thread-automation") {
8304
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
8305
+ if (!threadId) {
8306
+ setJson4(res, 400, { error: "Missing threadId" });
8307
+ return;
8308
+ }
8309
+ const removed = await deleteThreadHeartbeatAutomation(threadId);
8310
+ setJson4(res, 200, { data: { removed } });
8311
+ return;
8312
+ }
7035
8313
  if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
7036
8314
  const payload = asRecord5(await readJsonBody(req));
7037
8315
  const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
@@ -7109,19 +8387,30 @@ data: ${JSON.stringify({ ok: true })}
7109
8387
  const message = getErrorMessage5(error, "Unknown bridge error");
7110
8388
  setJson4(res, 502, { error: message });
7111
8389
  }
7112
- }
8390
+ };
7113
8391
  middleware.dispose = () => {
7114
8392
  threadSearchIndex = null;
7115
8393
  telegramBridge.stop();
8394
+ terminalManager.dispose();
7116
8395
  appServer.dispose();
7117
8396
  };
7118
8397
  middleware.subscribeNotifications = (listener) => {
7119
- return appServer.onNotification((notification) => {
8398
+ const unsubscribeAppServer = appServer.onNotification((notification) => {
8399
+ listener({
8400
+ ...notification,
8401
+ atIso: (/* @__PURE__ */ new Date()).toISOString()
8402
+ });
8403
+ });
8404
+ const unsubscribeTerminal = terminalManager.subscribe((notification) => {
7120
8405
  listener({
7121
8406
  ...notification,
7122
8407
  atIso: (/* @__PURE__ */ new Date()).toISOString()
7123
8408
  });
7124
8409
  });
8410
+ return () => {
8411
+ unsubscribeAppServer();
8412
+ unsubscribeTerminal();
8413
+ };
7125
8414
  };
7126
8415
  return middleware;
7127
8416
  }
@@ -7280,7 +8569,7 @@ function createAuthSession(password) {
7280
8569
  }
7281
8570
 
7282
8571
  // src/server/localBrowseUi.ts
7283
- import { dirname as dirname2, extname as extname2, join as join6 } from "path";
8572
+ import { dirname as dirname3, extname as extname2, join as join7 } from "path";
7284
8573
  import { open, readFile as readFile4, readdir as readdir3, stat as stat5 } from "fs/promises";
7285
8574
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
7286
8575
  ".txt",
@@ -7406,7 +8695,7 @@ async function isTextEditableFile(localPath) {
7406
8695
  return false;
7407
8696
  }
7408
8697
  }
7409
- function escapeHtml(value) {
8698
+ function escapeHtml2(value) {
7410
8699
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
7411
8700
  }
7412
8701
  function normalizeNewProjectName(value) {
@@ -7428,7 +8717,7 @@ function escapeForInlineScriptString(value) {
7428
8717
  async function getDirectoryItems(localPath) {
7429
8718
  const entries = await readdir3(localPath, { withFileTypes: true });
7430
8719
  const withMeta = await Promise.all(entries.map(async (entry) => {
7431
- const entryPath = join6(localPath, entry.name);
8720
+ const entryPath = join7(localPath, entry.name);
7432
8721
  const entryStat = await stat5(entryPath);
7433
8722
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
7434
8723
  return {
@@ -7449,7 +8738,7 @@ async function getDirectoryItems(localPath) {
7449
8738
  function projectCreationTargetPath(parentPath, newProjectName) {
7450
8739
  const normalizedName = normalizeNewProjectName(newProjectName);
7451
8740
  if (!normalizedName) return "";
7452
- return join6(parentPath, normalizedName);
8741
+ return join7(parentPath, normalizedName);
7453
8742
  }
7454
8743
  function projectCreationButtonLabel(newProjectName) {
7455
8744
  const normalizedName = normalizeNewProjectName(newProjectName);
@@ -7470,15 +8759,15 @@ function failureStatusText(newProjectName) {
7470
8759
  function actionButtonsHtml(localPath, newProjectName) {
7471
8760
  const normalizedName = normalizeNewProjectName(newProjectName);
7472
8761
  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>`;
8762
+ 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>` : "";
8763
+ 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
8764
  return `${createButton}${openButton}`;
7476
8765
  }
7477
8766
  async function getLocalDirectoryListing(localPath, options = {}) {
7478
8767
  const entries = await readdir3(localPath, { withFileTypes: true });
7479
8768
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
7480
8769
  name: entry.name,
7481
- path: join6(localPath, entry.name)
8770
+ path: join7(localPath, entry.name)
7482
8771
  })).filter((entry) => options.showHidden === true || !isHiddenName(entry.name)).sort((a, b) => {
7483
8772
  const aHidden = isHiddenName(a.name);
7484
8773
  const bHidden = isHiddenName(b.name);
@@ -7487,28 +8776,28 @@ async function getLocalDirectoryListing(localPath, options = {}) {
7487
8776
  });
7488
8777
  return {
7489
8778
  path: localPath,
7490
- parentPath: dirname2(localPath),
8779
+ parentPath: dirname3(localPath),
7491
8780
  entries: directories
7492
8781
  };
7493
8782
  }
7494
8783
  async function createDirectoryListingHtml(localPath, options) {
7495
8784
  const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
7496
8785
  const items = await getDirectoryItems(localPath);
7497
- const parentPath = dirname2(localPath);
8786
+ const parentPath = dirname3(localPath);
7498
8787
  const rows = items.map((item) => {
7499
8788
  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>`;
8789
+ 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>` : "";
8790
+ 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
8791
  }).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>` : "";
8792
+ const parentLink = localPath !== parentPath ? `<a class="header-parent-link" href="${escapeHtml2(toBrowseHref(parentPath, newProjectName))}">..</a>` : "";
8793
+ 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
8794
  const actionButtons = actionButtonsHtml(localPath, newProjectName);
7506
8795
  return `<!doctype html>
7507
8796
  <html lang="en">
7508
8797
  <head>
7509
8798
  <meta charset="utf-8" />
7510
8799
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7511
- <title>Index of ${escapeHtml(localPath)}</title>
8800
+ <title>Index of ${escapeHtml2(localPath)}</title>
7512
8801
  <style>
7513
8802
  body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
7514
8803
  a { color: #8cc2ff; text-decoration: none; }
@@ -7548,7 +8837,7 @@ async function createDirectoryListingHtml(localPath, options) {
7548
8837
  </style>
7549
8838
  </head>
7550
8839
  <body>
7551
- <h1>Index of ${escapeHtml(localPath)}</h1>
8840
+ <h1>Index of ${escapeHtml2(localPath)}</h1>
7552
8841
  ${pickerSummary}
7553
8842
  <div class="header-actions">
7554
8843
  ${parentLink}
@@ -7600,7 +8889,7 @@ async function createDirectoryListingHtml(localPath, options) {
7600
8889
  }
7601
8890
  async function createTextEditorHtml(localPath) {
7602
8891
  const content = await readFile4(localPath, "utf8");
7603
- const parentPath = dirname2(localPath);
8892
+ const parentPath = dirname3(localPath);
7604
8893
  const language = languageForPath(localPath);
7605
8894
  const safeContentLiteral = escapeForInlineScriptString(content);
7606
8895
  return `<!doctype html>
@@ -7608,7 +8897,7 @@ async function createTextEditorHtml(localPath) {
7608
8897
  <head>
7609
8898
  <meta charset="utf-8" />
7610
8899
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7611
- <title>Edit ${escapeHtml(localPath)}</title>
8900
+ <title>Edit ${escapeHtml2(localPath)}</title>
7612
8901
  <style>
7613
8902
  html, body { width: 100%; height: 100%; margin: 0; }
7614
8903
  body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
@@ -7628,11 +8917,11 @@ async function createTextEditorHtml(localPath) {
7628
8917
  <body>
7629
8918
  <div class="toolbar">
7630
8919
  <div class="row">
7631
- <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
8920
+ <a href="${escapeHtml2(toBrowseHref(parentPath))}">Back</a>
7632
8921
  <button id="saveBtn" type="button">Save</button>
7633
8922
  <span id="status"></span>
7634
8923
  </div>
7635
- <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
8924
+ <div class="meta">${escapeHtml2(localPath)} \xB7 ${escapeHtml2(language)}</div>
7636
8925
  </div>
7637
8926
  <div id="editor"></div>
7638
8927
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
@@ -7641,7 +8930,7 @@ async function createTextEditorHtml(localPath) {
7641
8930
  const status = document.getElementById('status');
7642
8931
  const editor = ace.edit('editor');
7643
8932
  editor.setTheme('ace/theme/tomorrow_night');
7644
- editor.session.setMode('ace/mode/${escapeHtml(language)}');
8933
+ editor.session.setMode('ace/mode/${escapeHtml2(language)}');
7645
8934
  editor.setValue(${safeContentLiteral}, -1);
7646
8935
  editor.setOptions({
7647
8936
  fontSize: '13px',
@@ -7669,9 +8958,9 @@ async function createTextEditorHtml(localPath) {
7669
8958
 
7670
8959
  // src/server/httpServer.ts
7671
8960
  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");
8961
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
8962
+ var distDir = join8(__dirname, "..", "dist");
8963
+ var spaEntryFile = join8(distDir, "index.html");
7675
8964
  var IMAGE_CONTENT_TYPES = {
7676
8965
  ".avif": "image/avif",
7677
8966
  ".bmp": "image/bmp",
@@ -7840,7 +9129,7 @@ function createServer(options = {}) {
7840
9129
  res.status(404).json({ error: "File not found." });
7841
9130
  }
7842
9131
  });
7843
- const hasFrontendAssets = existsSync3(spaEntryFile);
9132
+ const hasFrontendAssets = existsSync4(spaEntryFile);
7844
9133
  if (hasFrontendAssets) {
7845
9134
  app.use(express.static(distDir));
7846
9135
  }
@@ -7910,16 +9199,16 @@ function generatePassword() {
7910
9199
 
7911
9200
  // src/cli/index.ts
7912
9201
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
7913
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
9202
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
7914
9203
  var hasPromptedCloudflaredInstall = false;
7915
9204
  function getCodexHomePath() {
7916
- return process.env.CODEX_HOME?.trim() || join8(homedir5(), ".codex");
9205
+ return process.env.CODEX_HOME?.trim() || join9(homedir6(), ".codex");
7917
9206
  }
7918
9207
  function getCloudflaredPromptMarkerPath() {
7919
- return join8(getCodexHomePath(), ".cloudflared-install-prompted");
9208
+ return join9(getCodexHomePath(), ".cloudflared-install-prompted");
7920
9209
  }
7921
9210
  function hasPromptedCloudflaredInstallPersisted() {
7922
- return existsSync4(getCloudflaredPromptMarkerPath());
9211
+ return existsSync5(getCloudflaredPromptMarkerPath());
7923
9212
  }
7924
9213
  async function persistCloudflaredInstallPrompted() {
7925
9214
  const codexHome = getCodexHomePath();
@@ -7929,7 +9218,7 @@ async function persistCloudflaredInstallPrompted() {
7929
9218
  }
7930
9219
  async function readCliVersion() {
7931
9220
  try {
7932
- const packageJsonPath = join8(__dirname2, "..", "package.json");
9221
+ const packageJsonPath = join9(__dirname2, "..", "package.json");
7933
9222
  const raw = await readFile5(packageJsonPath, "utf8");
7934
9223
  const parsed = JSON.parse(raw);
7935
9224
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -7954,8 +9243,8 @@ function resolveCloudflaredCommand() {
7954
9243
  if (canRunCommand("cloudflared", ["--version"])) {
7955
9244
  return "cloudflared";
7956
9245
  }
7957
- const localCandidate = join8(homedir5(), ".local", "bin", "cloudflared");
7958
- if (existsSync4(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
9246
+ const localCandidate = join9(homedir6(), ".local", "bin", "cloudflared");
9247
+ if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
7959
9248
  return localCandidate;
7960
9249
  }
7961
9250
  return null;
@@ -8008,13 +9297,13 @@ async function ensureCloudflaredInstalledLinux() {
8008
9297
  if (!mappedArch) {
8009
9298
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
8010
9299
  }
8011
- const userBinDir = join8(homedir5(), ".local", "bin");
9300
+ const userBinDir = join9(homedir6(), ".local", "bin");
8012
9301
  mkdirSync(userBinDir, { recursive: true });
8013
- const destination = join8(userBinDir, "cloudflared");
9302
+ const destination = join9(userBinDir, "cloudflared");
8014
9303
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
8015
9304
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
8016
9305
  await downloadFile(downloadUrl, destination);
8017
- chmodSync(destination, 493);
9306
+ chmodSync2(destination, 493);
8018
9307
  process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
8019
9308
  const installed = resolveCloudflaredCommand();
8020
9309
  if (!installed) {
@@ -8061,7 +9350,7 @@ async function resolveCloudflaredForTunnel() {
8061
9350
  }
8062
9351
  function hasCodexAuth() {
8063
9352
  const codexHome = getCodexHomePath();
8064
- return existsSync4(join8(codexHome, "auth.json"));
9353
+ return existsSync5(join9(codexHome, "auth.json"));
8065
9354
  }
8066
9355
  function ensureCodexInstalled() {
8067
9356
  let codexCommand = resolveCodexCommand();
@@ -8251,7 +9540,7 @@ function listenWithFallback(server, startPort) {
8251
9540
  }
8252
9541
  function getCodexGlobalStatePath2() {
8253
9542
  const codexHome = getCodexHomePath();
8254
- return join8(codexHome, ".codex-global-state.json");
9543
+ return join9(codexHome, ".codex-global-state.json");
8255
9544
  }
8256
9545
  function normalizeUniqueStrings(value) {
8257
9546
  if (!Array.isArray(value)) return [];
@@ -8326,18 +9615,7 @@ async function startServer(options) {
8326
9615
  process.env.CODEXUI_APPROVAL_POLICY = options.approvalPolicy;
8327
9616
  }
8328
9617
  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()) {
9618
+ if (options.login && !hasCodexAuth()) {
8341
9619
  console.log("\nCodex is not logged in. You can log in later via settings or run `codexui login`.\n");
8342
9620
  }
8343
9621
  const requestedPort = parseInt(options.port, 10);
@@ -8422,7 +9700,7 @@ async function runLogin() {
8422
9700
  console.log("\nStarting `codex login`...\n");
8423
9701
  runOrFail(codexCommand, ["login"], "Codex login");
8424
9702
  }
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) => {
9703
+ 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
9704
  const rawArgv = process.argv.slice(2);
8427
9705
  const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
8428
9706
  const tunnelFlagExplicit = rawArgv.some((arg) => arg === "--tunnel" || arg === "--no-tunnel" || arg.startsWith("--tunnel=") || arg.startsWith("--no-tunnel="));