codexui-android 0.1.72 → 0.1.80

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 (32) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/ReviewPane-C5squOEL.js +2 -0
  3. package/dist/assets/ReviewPane-C5squOEL.js.map +1 -0
  4. package/dist/assets/{SkillsHub-BkLIHbuN.js → SkillsHub-Crvp1wow.js} +2 -1
  5. package/dist/assets/SkillsHub-Crvp1wow.js.map +1 -0
  6. package/dist/assets/ThreadConversation-BsN7bN3q.css +1 -0
  7. package/dist/assets/ThreadConversation-qnvp4E2o.js +40 -0
  8. package/dist/assets/ThreadConversation-qnvp4E2o.js.map +1 -0
  9. package/dist/assets/common-BeuopZEI.js +1 -0
  10. package/dist/assets/common-BeuopZEI.js.map +1 -0
  11. package/dist/assets/index-C4y0SuPN.js +559 -0
  12. package/dist/assets/index-C4y0SuPN.js.map +1 -0
  13. package/dist/assets/index-CsHtQi-g.css +1 -0
  14. package/dist/assets/index.esm-BilMXo9u.js +1 -0
  15. package/dist/assets/index.esm-BilMXo9u.js.map +1 -0
  16. package/dist/assets/index.esm-DtVW_dfU.js +1 -0
  17. package/dist/assets/index.esm-DtVW_dfU.js.map +1 -0
  18. package/dist/assets/index.esm-mbv_PYjX.js +1 -0
  19. package/dist/assets/index.esm-mbv_PYjX.js.map +1 -0
  20. package/dist/index.html +2 -2
  21. package/dist-cli/chunk-NWKUDLO2.js +111 -0
  22. package/dist-cli/chunk-NWKUDLO2.js.map +1 -0
  23. package/dist-cli/index.js +2170 -868
  24. package/dist-cli/index.js.map +1 -1
  25. package/dist-cli/instrument.js +8 -0
  26. package/dist-cli/instrument.js.map +1 -0
  27. package/package.json +9 -4
  28. package/dist/assets/ReviewPane-jxaR-1Q1.js +0 -1
  29. package/dist/assets/ThreadConversation-1LJi-Pk9.js +0 -36
  30. package/dist/assets/ThreadConversation-Ct-Pc8bX.css +0 -1
  31. package/dist/assets/index-1Zt4k_jO.css +0 -1
  32. package/dist/assets/index-Tdn545FN.js +0 -62
package/dist-cli/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./chunk-NWKUDLO2.js";
2
3
 
3
4
  // src/cli/index.ts
4
5
  import { createServer as createServer2 } from "http";
@@ -156,6 +157,64 @@ function resolveSkillInstallerScriptPath(codexHome) {
156
157
  return null;
157
158
  }
158
159
 
160
+ // src/server/appServerRuntimeConfig.ts
161
+ var SANDBOX_MODES = /* @__PURE__ */ new Set([
162
+ "read-only",
163
+ "workspace-write",
164
+ "danger-full-access"
165
+ ]);
166
+ var APPROVAL_POLICIES = /* @__PURE__ */ new Set([
167
+ "untrusted",
168
+ "on-failure",
169
+ "on-request",
170
+ "never"
171
+ ]);
172
+ var DEFAULT_RUNTIME_CONFIG = {
173
+ sandboxMode: "danger-full-access",
174
+ approvalPolicy: "never"
175
+ };
176
+ function normalizeRuntimeValue(value) {
177
+ return value?.trim().toLowerCase() ?? "";
178
+ }
179
+ function readSandboxModeFromEnv() {
180
+ const candidate = normalizeRuntimeValue(process.env.CODEXUI_SANDBOX_MODE);
181
+ if (SANDBOX_MODES.has(candidate)) {
182
+ return candidate;
183
+ }
184
+ return DEFAULT_RUNTIME_CONFIG.sandboxMode;
185
+ }
186
+ function readApprovalPolicyFromEnv() {
187
+ const candidate = normalizeRuntimeValue(process.env.CODEXUI_APPROVAL_POLICY);
188
+ if (APPROVAL_POLICIES.has(candidate)) {
189
+ return candidate;
190
+ }
191
+ return DEFAULT_RUNTIME_CONFIG.approvalPolicy;
192
+ }
193
+ function resolveAppServerRuntimeConfig() {
194
+ return {
195
+ sandboxMode: readSandboxModeFromEnv(),
196
+ approvalPolicy: readApprovalPolicyFromEnv()
197
+ };
198
+ }
199
+ function buildAppServerArgs() {
200
+ const config = resolveAppServerRuntimeConfig();
201
+ return [
202
+ "app-server",
203
+ "-c",
204
+ `approval_policy="${config.approvalPolicy}"`,
205
+ "-c",
206
+ `sandbox_mode="${config.sandboxMode}"`
207
+ ];
208
+ }
209
+ function parseSandboxMode(value) {
210
+ const candidate = value.trim().toLowerCase();
211
+ return SANDBOX_MODES.has(candidate) ? candidate : null;
212
+ }
213
+ function parseApprovalPolicy(value) {
214
+ const candidate = value.trim().toLowerCase();
215
+ return APPROVAL_POLICIES.has(candidate) ? candidate : null;
216
+ }
217
+
159
218
  // src/server/httpServer.ts
160
219
  import { fileURLToPath } from "url";
161
220
  import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute3, join as join7 } from "path";
@@ -164,9 +223,10 @@ import { writeFile as writeFile5, stat as stat6 } from "fs/promises";
164
223
  import express from "express";
165
224
 
166
225
  // src/server/codexAppServerBridge.ts
226
+ import * as Sentry4 from "@sentry/node";
167
227
  import { spawn as spawn4 } from "child_process";
168
- import { randomBytes } from "crypto";
169
- import { mkdtemp as mkdtemp3, readFile as readFile3, mkdir as mkdir4, stat as stat4 } from "fs/promises";
228
+ 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";
170
230
  import { createReadStream } from "fs";
171
231
  import { request as httpRequest } from "http";
172
232
  import { request as httpsRequest } from "https";
@@ -177,18 +237,12 @@ import { createInterface } from "readline";
177
237
  import { writeFile as writeFile4 } from "fs/promises";
178
238
 
179
239
  // src/server/accountRoutes.ts
240
+ import * as Sentry from "@sentry/node";
180
241
  import { spawn } from "child_process";
181
242
  import { createHash } from "crypto";
182
243
  import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from "fs/promises";
183
244
  import { homedir as homedir2, tmpdir } from "os";
184
245
  import { join as join2 } from "path";
185
- var APP_SERVER_ARGS = [
186
- "app-server",
187
- "-c",
188
- 'approval_policy="never"',
189
- "-c",
190
- 'sandbox_mode="danger-full-access"'
191
- ];
192
246
  var ACCOUNT_QUOTA_REFRESH_TTL_MS = 5 * 60 * 1e3;
193
247
  var ACCOUNT_QUOTA_LOADING_STALE_MS = 2 * 60 * 1e3;
194
248
  var ACCOUNT_INSPECTION_TIMEOUT_MS = 25 * 1e3;
@@ -446,7 +500,7 @@ async function withTemporaryCodexAppServer(authRaw, run) {
446
500
  const tempCodexHome = await mkdtemp(join2(tmpdir(), "codexui-account-"));
447
501
  const authPath = join2(tempCodexHome, "auth.json");
448
502
  await writeFile(authPath, authRaw, { encoding: "utf8", mode: 384 });
449
- const proc = spawn("codex", [...APP_SERVER_ARGS], {
503
+ const proc = spawn("codex", buildAppServerArgs(), {
450
504
  env: { ...process.env, CODEX_HOME: tempCodexHome },
451
505
  stdio: ["pipe", "pipe", "pipe"]
452
506
  });
@@ -716,319 +770,327 @@ async function importAccountFromAuthPath(path) {
716
770
  }
717
771
  async function handleAccountRoutes(req, res, url, context) {
718
772
  const { appServer } = context;
719
- if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
720
- const state = await scheduleAccountsBackgroundRefresh();
721
- setJson(res, 200, {
722
- data: {
723
- activeAccountId: state.activeAccountId,
724
- accounts: sortAccounts(state.accounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
725
- }
726
- });
727
- return true;
728
- }
729
- if (req.method === "GET" && url.pathname === "/codex-api/accounts/active") {
730
- const state = await readStoredAccountsState();
731
- const active = state.activeAccountId ? state.accounts.find((entry) => entry.accountId === state.activeAccountId) ?? null : null;
732
- setJson(res, 200, {
733
- data: active ? toPublicAccountEntry(active, state.activeAccountId) : null
734
- });
735
- return true;
736
- }
737
- if (req.method === "POST" && url.pathname === "/codex-api/accounts/refresh") {
738
- try {
739
- const imported = await importAccountFromAuthPath(getActiveAuthPath());
740
- try {
741
- appServer.dispose();
742
- const inspection = await validateSwitchedAccount(appServer);
743
- const state = await readStoredAccountsState();
744
- const importedAccountId = imported.importedAccountId;
745
- const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
746
- if (!target) {
747
- throw new Error("account_not_found");
773
+ try {
774
+ if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
775
+ const state = await scheduleAccountsBackgroundRefresh();
776
+ setJson(res, 200, {
777
+ data: {
778
+ activeAccountId: state.activeAccountId,
779
+ accounts: sortAccounts(state.accounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
748
780
  }
749
- const nextEntry = {
750
- ...target,
751
- email: inspection.metadata.email ?? target.email,
752
- planType: inspection.metadata.planType ?? target.planType,
753
- lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
754
- quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
755
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
756
- quotaStatus: "ready",
757
- quotaError: null,
758
- unavailableReason: null
759
- };
760
- const nextState = withUpsertedAccount({
761
- activeAccountId: importedAccountId,
762
- accounts: state.accounts
763
- }, nextEntry);
764
- await writeStoredAccountsState({
765
- activeAccountId: importedAccountId,
766
- accounts: nextState.accounts
767
- });
768
- const backgroundState = await scheduleAccountsBackgroundRefresh({
769
- force: true,
770
- prioritizeAccountId: importedAccountId,
771
- accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
772
- });
773
- setJson(res, 200, {
774
- data: {
775
- activeAccountId: importedAccountId,
776
- importedAccountId,
777
- accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
778
- }
779
- });
780
- } catch (error) {
781
- setJson(res, 502, {
782
- error: "account_refresh_failed",
783
- message: getErrorMessage(error, "Failed to refresh account")
784
- });
785
- }
786
- } catch (error) {
787
- const message = getErrorMessage(error, "Failed to refresh account");
788
- if (message === "missing_account_id") {
789
- setJson(res, 400, { error: "missing_account_id", message: "Current auth.json is missing tokens.account_id." });
790
- return true;
791
- }
792
- setJson(res, 400, { error: "invalid_auth_json", message: "Failed to parse the current auth.json file." });
793
- }
794
- return true;
795
- }
796
- if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
797
- try {
798
- if (appServer.listPendingServerRequests().length > 0) {
799
- setJson(res, 409, {
800
- error: "account_switch_blocked",
801
- message: "Finish pending approval requests before switching accounts."
802
- });
803
- return true;
804
- }
805
- const rawBody = await new Promise((resolve4, reject) => {
806
- let body = "";
807
- req.setEncoding("utf8");
808
- req.on("data", (chunk) => {
809
- body += chunk;
810
- });
811
- req.on("end", () => resolve4(body));
812
- req.on("error", reject);
813
781
  });
814
- const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
815
- const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
816
- if (!accountId) {
817
- setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
818
- return true;
819
- }
782
+ return true;
783
+ }
784
+ if (req.method === "GET" && url.pathname === "/codex-api/accounts/active") {
820
785
  const state = await readStoredAccountsState();
821
- const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
822
- if (!target) {
823
- setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
824
- return true;
825
- }
826
- const snapshotPath = getSnapshotPath(target.storageId);
827
- if (!await fileExists(snapshotPath)) {
828
- setJson(res, 404, { error: "account_not_found", message: "The requested account snapshot is missing." });
829
- return true;
830
- }
831
- let previousRaw = null;
786
+ const active = state.activeAccountId ? state.accounts.find((entry) => entry.accountId === state.activeAccountId) ?? null : null;
787
+ setJson(res, 200, {
788
+ data: active ? toPublicAccountEntry(active, state.activeAccountId) : null
789
+ });
790
+ return true;
791
+ }
792
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/refresh") {
832
793
  try {
833
- previousRaw = await readFile(getActiveAuthPath(), "utf8");
834
- } catch {
835
- previousRaw = null;
794
+ const imported = await importAccountFromAuthPath(getActiveAuthPath());
795
+ try {
796
+ appServer.dispose();
797
+ const inspection = await validateSwitchedAccount(appServer);
798
+ const state = await readStoredAccountsState();
799
+ const importedAccountId = imported.importedAccountId;
800
+ const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
801
+ if (!target) {
802
+ throw new Error("account_not_found");
803
+ }
804
+ const nextEntry = {
805
+ ...target,
806
+ email: inspection.metadata.email ?? target.email,
807
+ planType: inspection.metadata.planType ?? target.planType,
808
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
809
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
810
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
811
+ quotaStatus: "ready",
812
+ quotaError: null,
813
+ unavailableReason: null
814
+ };
815
+ const nextState = withUpsertedAccount({
816
+ activeAccountId: importedAccountId,
817
+ accounts: state.accounts
818
+ }, nextEntry);
819
+ await writeStoredAccountsState({
820
+ activeAccountId: importedAccountId,
821
+ accounts: nextState.accounts
822
+ });
823
+ const backgroundState = await scheduleAccountsBackgroundRefresh({
824
+ force: true,
825
+ prioritizeAccountId: importedAccountId,
826
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
827
+ });
828
+ setJson(res, 200, {
829
+ data: {
830
+ activeAccountId: importedAccountId,
831
+ importedAccountId,
832
+ accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
833
+ }
834
+ });
835
+ } catch (error) {
836
+ setJson(res, 502, {
837
+ error: "account_refresh_failed",
838
+ message: getErrorMessage(error, "Failed to refresh account")
839
+ });
840
+ }
841
+ } catch (error) {
842
+ const message = getErrorMessage(error, "Failed to refresh account");
843
+ if (message === "missing_account_id") {
844
+ setJson(res, 400, { error: "missing_account_id", message: "Current auth.json is missing tokens.account_id." });
845
+ return true;
846
+ }
847
+ setJson(res, 400, { error: "invalid_auth_json", message: "Failed to parse the current auth.json file." });
836
848
  }
837
- const targetRaw = await readFile(snapshotPath, "utf8");
838
- await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
849
+ return true;
850
+ }
851
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
839
852
  try {
840
- appServer.dispose();
841
- const inspection = await validateSwitchedAccount(appServer);
842
- const nextEntry = {
843
- ...target,
844
- email: inspection.metadata.email ?? target.email,
845
- planType: inspection.metadata.planType ?? target.planType,
846
- lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
847
- quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
848
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
849
- quotaStatus: "ready",
850
- quotaError: null,
851
- unavailableReason: null
852
- };
853
- const nextState = withUpsertedAccount({
854
- activeAccountId: accountId,
855
- accounts: state.accounts
856
- }, nextEntry);
857
- await writeStoredAccountsState({
858
- activeAccountId: accountId,
859
- accounts: nextState.accounts
860
- });
861
- void scheduleAccountsBackgroundRefresh({
862
- force: true,
863
- prioritizeAccountId: accountId,
864
- accountIds: nextState.accounts.filter((entry) => entry.accountId !== accountId).map((entry) => entry.accountId)
853
+ if (appServer.listPendingServerRequests().length > 0) {
854
+ setJson(res, 409, {
855
+ error: "account_switch_blocked",
856
+ message: "Finish pending approval requests before switching accounts."
857
+ });
858
+ return true;
859
+ }
860
+ const rawBody = await new Promise((resolve4, reject) => {
861
+ let body = "";
862
+ req.setEncoding("utf8");
863
+ req.on("data", (chunk) => {
864
+ body += chunk;
865
+ });
866
+ req.on("end", () => resolve4(body));
867
+ req.on("error", reject);
865
868
  });
866
- setJson(res, 200, {
867
- ok: true,
868
- data: {
869
+ const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
870
+ const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
871
+ if (!accountId) {
872
+ setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
873
+ return true;
874
+ }
875
+ const state = await readStoredAccountsState();
876
+ const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
877
+ if (!target) {
878
+ setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
879
+ return true;
880
+ }
881
+ const snapshotPath = getSnapshotPath(target.storageId);
882
+ if (!await fileExists(snapshotPath)) {
883
+ setJson(res, 404, { error: "account_not_found", message: "The requested account snapshot is missing." });
884
+ return true;
885
+ }
886
+ let previousRaw = null;
887
+ try {
888
+ previousRaw = await readFile(getActiveAuthPath(), "utf8");
889
+ } catch {
890
+ previousRaw = null;
891
+ }
892
+ const targetRaw = await readFile(snapshotPath, "utf8");
893
+ await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
894
+ try {
895
+ appServer.dispose();
896
+ const inspection = await validateSwitchedAccount(appServer);
897
+ const nextEntry = {
898
+ ...target,
899
+ email: inspection.metadata.email ?? target.email,
900
+ planType: inspection.metadata.planType ?? target.planType,
901
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
902
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
903
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
904
+ quotaStatus: "ready",
905
+ quotaError: null,
906
+ unavailableReason: null
907
+ };
908
+ const nextState = withUpsertedAccount({
869
909
  activeAccountId: accountId,
870
- account: toPublicAccountEntry(nextEntry, accountId)
871
- }
872
- });
910
+ accounts: state.accounts
911
+ }, nextEntry);
912
+ await writeStoredAccountsState({
913
+ activeAccountId: accountId,
914
+ accounts: nextState.accounts
915
+ });
916
+ void scheduleAccountsBackgroundRefresh({
917
+ force: true,
918
+ prioritizeAccountId: accountId,
919
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== accountId).map((entry) => entry.accountId)
920
+ });
921
+ setJson(res, 200, {
922
+ ok: true,
923
+ data: {
924
+ activeAccountId: accountId,
925
+ account: toPublicAccountEntry(nextEntry, accountId)
926
+ }
927
+ });
928
+ } catch (error) {
929
+ await restoreActiveAuth(previousRaw);
930
+ appServer.dispose();
931
+ await replaceStoredAccount({
932
+ ...target,
933
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
934
+ quotaStatus: "error",
935
+ quotaError: getErrorMessage(error, "Failed to switch account"),
936
+ unavailableReason: detectAccountUnavailableReason(error)
937
+ }, state.activeAccountId);
938
+ setJson(res, 502, {
939
+ error: "account_switch_failed",
940
+ message: getErrorMessage(error, "Failed to switch account")
941
+ });
942
+ }
873
943
  } catch (error) {
874
- await restoreActiveAuth(previousRaw);
875
- appServer.dispose();
876
- await replaceStoredAccount({
877
- ...target,
878
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
879
- quotaStatus: "error",
880
- quotaError: getErrorMessage(error, "Failed to switch account"),
881
- unavailableReason: detectAccountUnavailableReason(error)
882
- }, state.activeAccountId);
883
- setJson(res, 502, {
884
- error: "account_switch_failed",
944
+ setJson(res, 400, {
945
+ error: "invalid_auth_json",
885
946
  message: getErrorMessage(error, "Failed to switch account")
886
947
  });
887
948
  }
888
- } catch (error) {
889
- setJson(res, 400, {
890
- error: "invalid_auth_json",
891
- message: getErrorMessage(error, "Failed to switch account")
892
- });
949
+ return true;
893
950
  }
894
- return true;
895
- }
896
- if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
897
- try {
898
- const rawBody = await new Promise((resolve4, reject) => {
899
- let body = "";
900
- req.setEncoding("utf8");
901
- req.on("data", (chunk) => {
902
- body += chunk;
903
- });
904
- req.on("end", () => resolve4(body));
905
- req.on("error", reject);
906
- });
907
- const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
908
- const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
909
- if (!accountId) {
910
- setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
911
- return true;
912
- }
913
- const state = await readStoredAccountsState();
914
- const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
915
- if (!target) {
916
- setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
917
- return true;
918
- }
919
- const remainingAccounts = state.accounts.filter((entry) => entry.accountId !== accountId);
920
- if (state.activeAccountId !== accountId) {
921
- await removeSnapshot(target.storageId);
922
- await writeStoredAccountsState({
923
- activeAccountId: state.activeAccountId,
924
- accounts: remainingAccounts
925
- });
926
- setJson(res, 200, {
927
- ok: true,
928
- data: {
929
- activeAccountId: state.activeAccountId,
930
- accounts: sortAccounts(remainingAccounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
931
- }
932
- });
933
- return true;
934
- }
935
- if (appServer.listPendingServerRequests().length > 0) {
936
- setJson(res, 409, {
937
- error: "account_remove_blocked",
938
- message: "Finish pending approval requests before removing the active account."
939
- });
940
- return true;
941
- }
942
- let previousRaw = null;
951
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
943
952
  try {
944
- previousRaw = await readFile(getActiveAuthPath(), "utf8");
945
- } catch {
946
- previousRaw = null;
947
- }
948
- const replacement = await pickReplacementActiveAccount(remainingAccounts);
949
- if (!replacement) {
950
- await restoreActiveAuth(null);
951
- appServer.dispose();
952
- await removeSnapshot(target.storageId);
953
- await writeStoredAccountsState({
954
- activeAccountId: null,
955
- accounts: remainingAccounts
956
- });
957
- void scheduleAccountsBackgroundRefresh({
958
- force: true,
959
- accountIds: remainingAccounts.map((entry) => entry.accountId)
953
+ const rawBody = await new Promise((resolve4, reject) => {
954
+ let body = "";
955
+ req.setEncoding("utf8");
956
+ req.on("data", (chunk) => {
957
+ body += chunk;
958
+ });
959
+ req.on("end", () => resolve4(body));
960
+ req.on("error", reject);
960
961
  });
961
- setJson(res, 200, {
962
- ok: true,
963
- data: {
962
+ const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
963
+ const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
964
+ if (!accountId) {
965
+ setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
966
+ return true;
967
+ }
968
+ const state = await readStoredAccountsState();
969
+ const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
970
+ if (!target) {
971
+ setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
972
+ return true;
973
+ }
974
+ const remainingAccounts = state.accounts.filter((entry) => entry.accountId !== accountId);
975
+ if (state.activeAccountId !== accountId) {
976
+ await removeSnapshot(target.storageId);
977
+ await writeStoredAccountsState({
978
+ activeAccountId: state.activeAccountId,
979
+ accounts: remainingAccounts
980
+ });
981
+ setJson(res, 200, {
982
+ ok: true,
983
+ data: {
984
+ activeAccountId: state.activeAccountId,
985
+ accounts: sortAccounts(remainingAccounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
986
+ }
987
+ });
988
+ return true;
989
+ }
990
+ if (appServer.listPendingServerRequests().length > 0) {
991
+ setJson(res, 409, {
992
+ error: "account_remove_blocked",
993
+ message: "Finish pending approval requests before removing the active account."
994
+ });
995
+ return true;
996
+ }
997
+ let previousRaw = null;
998
+ try {
999
+ previousRaw = await readFile(getActiveAuthPath(), "utf8");
1000
+ } catch {
1001
+ previousRaw = null;
1002
+ }
1003
+ const replacement = await pickReplacementActiveAccount(remainingAccounts);
1004
+ if (!replacement) {
1005
+ await restoreActiveAuth(null);
1006
+ appServer.dispose();
1007
+ await removeSnapshot(target.storageId);
1008
+ await writeStoredAccountsState({
964
1009
  activeAccountId: null,
965
- accounts: sortAccounts(remainingAccounts, null).map((entry) => toPublicAccountEntry(entry, null))
966
- }
967
- });
968
- return true;
969
- }
970
- const replacementSnapshotPath = getSnapshotPath(replacement.storageId);
971
- if (!await fileExists(replacementSnapshotPath)) {
972
- setJson(res, 404, {
973
- error: "account_not_found",
974
- message: "The replacement account snapshot is missing."
975
- });
976
- return true;
977
- }
978
- const replacementRaw = await readFile(replacementSnapshotPath, "utf8");
979
- await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
980
- try {
981
- appServer.dispose();
982
- const inspection = await validateSwitchedAccount(appServer);
983
- const activatedReplacement = {
984
- ...replacement,
985
- email: inspection.metadata.email ?? replacement.email,
986
- planType: inspection.metadata.planType ?? replacement.planType,
987
- lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
988
- quotaSnapshot: inspection.quotaSnapshot ?? replacement.quotaSnapshot,
989
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
990
- quotaStatus: "ready",
991
- quotaError: null,
992
- unavailableReason: null
993
- };
994
- const nextAccounts = remainingAccounts.map((entry) => entry.accountId === activatedReplacement.accountId ? activatedReplacement : entry);
995
- await removeSnapshot(target.storageId);
996
- await writeStoredAccountsState({
997
- activeAccountId: activatedReplacement.accountId,
998
- accounts: nextAccounts
999
- });
1000
- void scheduleAccountsBackgroundRefresh({
1001
- force: true,
1002
- prioritizeAccountId: activatedReplacement.accountId,
1003
- accountIds: nextAccounts.filter((entry) => entry.accountId !== activatedReplacement.accountId).map((entry) => entry.accountId)
1004
- });
1005
- setJson(res, 200, {
1006
- ok: true,
1007
- data: {
1010
+ accounts: remainingAccounts
1011
+ });
1012
+ void scheduleAccountsBackgroundRefresh({
1013
+ force: true,
1014
+ accountIds: remainingAccounts.map((entry) => entry.accountId)
1015
+ });
1016
+ setJson(res, 200, {
1017
+ ok: true,
1018
+ data: {
1019
+ activeAccountId: null,
1020
+ accounts: sortAccounts(remainingAccounts, null).map((entry) => toPublicAccountEntry(entry, null))
1021
+ }
1022
+ });
1023
+ return true;
1024
+ }
1025
+ const replacementSnapshotPath = getSnapshotPath(replacement.storageId);
1026
+ if (!await fileExists(replacementSnapshotPath)) {
1027
+ setJson(res, 404, {
1028
+ error: "account_not_found",
1029
+ message: "The replacement account snapshot is missing."
1030
+ });
1031
+ return true;
1032
+ }
1033
+ const replacementRaw = await readFile(replacementSnapshotPath, "utf8");
1034
+ await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
1035
+ try {
1036
+ appServer.dispose();
1037
+ const inspection = await validateSwitchedAccount(appServer);
1038
+ const activatedReplacement = {
1039
+ ...replacement,
1040
+ email: inspection.metadata.email ?? replacement.email,
1041
+ planType: inspection.metadata.planType ?? replacement.planType,
1042
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1043
+ quotaSnapshot: inspection.quotaSnapshot ?? replacement.quotaSnapshot,
1044
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1045
+ quotaStatus: "ready",
1046
+ quotaError: null,
1047
+ unavailableReason: null
1048
+ };
1049
+ const nextAccounts = remainingAccounts.map((entry) => entry.accountId === activatedReplacement.accountId ? activatedReplacement : entry);
1050
+ await removeSnapshot(target.storageId);
1051
+ await writeStoredAccountsState({
1008
1052
  activeAccountId: activatedReplacement.accountId,
1009
- accounts: sortAccounts(nextAccounts, activatedReplacement.accountId).map((entry) => toPublicAccountEntry(entry, activatedReplacement.accountId))
1010
- }
1011
- });
1053
+ accounts: nextAccounts
1054
+ });
1055
+ void scheduleAccountsBackgroundRefresh({
1056
+ force: true,
1057
+ prioritizeAccountId: activatedReplacement.accountId,
1058
+ accountIds: nextAccounts.filter((entry) => entry.accountId !== activatedReplacement.accountId).map((entry) => entry.accountId)
1059
+ });
1060
+ setJson(res, 200, {
1061
+ ok: true,
1062
+ data: {
1063
+ activeAccountId: activatedReplacement.accountId,
1064
+ accounts: sortAccounts(nextAccounts, activatedReplacement.accountId).map((entry) => toPublicAccountEntry(entry, activatedReplacement.accountId))
1065
+ }
1066
+ });
1067
+ } catch (error) {
1068
+ await restoreActiveAuth(previousRaw);
1069
+ appServer.dispose();
1070
+ await replaceStoredAccount({
1071
+ ...replacement,
1072
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1073
+ quotaStatus: "error",
1074
+ quotaError: getErrorMessage(error, "Failed to switch account"),
1075
+ unavailableReason: detectAccountUnavailableReason(error)
1076
+ }, state.activeAccountId);
1077
+ setJson(res, 502, {
1078
+ error: "account_remove_failed",
1079
+ message: getErrorMessage(error, "Failed to remove account")
1080
+ });
1081
+ }
1012
1082
  } catch (error) {
1013
- await restoreActiveAuth(previousRaw);
1014
- appServer.dispose();
1015
- await replaceStoredAccount({
1016
- ...replacement,
1017
- quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1018
- quotaStatus: "error",
1019
- quotaError: getErrorMessage(error, "Failed to switch account"),
1020
- unavailableReason: detectAccountUnavailableReason(error)
1021
- }, state.activeAccountId);
1022
- setJson(res, 502, {
1023
- error: "account_remove_failed",
1083
+ setJson(res, 400, {
1084
+ error: "invalid_auth_json",
1024
1085
  message: getErrorMessage(error, "Failed to remove account")
1025
1086
  });
1026
1087
  }
1027
- } catch (error) {
1028
- setJson(res, 400, {
1029
- error: "invalid_auth_json",
1030
- message: getErrorMessage(error, "Failed to remove account")
1031
- });
1088
+ return true;
1089
+ }
1090
+ } catch (error) {
1091
+ Sentry.captureException(error);
1092
+ if (!res.headersSent) {
1093
+ setJson(res, 500, { error: "Internal account error" });
1032
1094
  }
1033
1095
  return true;
1034
1096
  }
@@ -1036,6 +1098,7 @@ async function handleAccountRoutes(req, res, url, context) {
1036
1098
  }
1037
1099
 
1038
1100
  // src/server/reviewGit.ts
1101
+ import * as Sentry2 from "@sentry/node";
1039
1102
  import { spawn as spawn2 } from "child_process";
1040
1103
  import { mkdir as mkdir2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1041
1104
  import { tmpdir as tmpdir2 } from "os";
@@ -1647,47 +1710,55 @@ async function applyReviewAction(payload) {
1647
1710
  return await buildReviewSnapshot(normalizedCwd, scope, workspaceView);
1648
1711
  }
1649
1712
  async function handleReviewRoutes(req, res, url, context) {
1650
- if (req.method === "GET" && url.pathname === "/codex-api/review/snapshot") {
1651
- const cwd = url.searchParams.get("cwd")?.trim() ?? "";
1652
- const scope = url.searchParams.get("scope") === "baseBranch" ? "baseBranch" : "workspace";
1653
- const workspaceView = url.searchParams.get("workspaceView") === "staged" ? "staged" : "unstaged";
1654
- const baseBranch = url.searchParams.get("baseBranch")?.trim() ?? "";
1655
- if (!cwd) {
1656
- setJson2(res, 400, { error: "Missing cwd" });
1713
+ try {
1714
+ if (req.method === "GET" && url.pathname === "/codex-api/review/snapshot") {
1715
+ const cwd = url.searchParams.get("cwd")?.trim() ?? "";
1716
+ const scope = url.searchParams.get("scope") === "baseBranch" ? "baseBranch" : "workspace";
1717
+ const workspaceView = url.searchParams.get("workspaceView") === "staged" ? "staged" : "unstaged";
1718
+ const baseBranch = url.searchParams.get("baseBranch")?.trim() ?? "";
1719
+ if (!cwd) {
1720
+ setJson2(res, 400, { error: "Missing cwd" });
1721
+ return true;
1722
+ }
1723
+ try {
1724
+ setJson2(res, 200, {
1725
+ data: await buildReviewSnapshot(cwd, scope, workspaceView, baseBranch)
1726
+ });
1727
+ } catch (error) {
1728
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to load review snapshot") });
1729
+ }
1657
1730
  return true;
1658
1731
  }
1659
- try {
1660
- setJson2(res, 200, {
1661
- data: await buildReviewSnapshot(cwd, scope, workspaceView, baseBranch)
1662
- });
1663
- } catch (error) {
1664
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to load review snapshot") });
1665
- }
1666
- return true;
1667
- }
1668
- if (req.method === "POST" && url.pathname === "/codex-api/review/action") {
1669
- try {
1670
- const payload = await context.readJsonBody(req);
1671
- setJson2(res, 200, {
1672
- data: await applyReviewAction(payload)
1673
- });
1674
- } catch (error) {
1675
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to apply review action") });
1732
+ if (req.method === "POST" && url.pathname === "/codex-api/review/action") {
1733
+ try {
1734
+ const payload = await context.readJsonBody(req);
1735
+ setJson2(res, 200, {
1736
+ data: await applyReviewAction(payload)
1737
+ });
1738
+ } catch (error) {
1739
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to apply review action") });
1740
+ }
1741
+ return true;
1676
1742
  }
1677
- return true;
1678
- }
1679
- if (req.method === "POST" && url.pathname === "/codex-api/review/git/init") {
1680
- const payload = asRecord2(await context.readJsonBody(req));
1681
- const cwd = readString2(payload?.cwd);
1682
- if (!cwd) {
1683
- setJson2(res, 400, { error: "Missing cwd" });
1743
+ if (req.method === "POST" && url.pathname === "/codex-api/review/git/init") {
1744
+ const payload = asRecord2(await context.readJsonBody(req));
1745
+ const cwd = readString2(payload?.cwd);
1746
+ if (!cwd) {
1747
+ setJson2(res, 400, { error: "Missing cwd" });
1748
+ return true;
1749
+ }
1750
+ try {
1751
+ await initializeGitRepository(cwd);
1752
+ setJson2(res, 200, { ok: true });
1753
+ } catch (error) {
1754
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to initialize Git") });
1755
+ }
1684
1756
  return true;
1685
1757
  }
1686
- try {
1687
- await initializeGitRepository(cwd);
1688
- setJson2(res, 200, { ok: true });
1689
- } catch (error) {
1690
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to initialize Git") });
1758
+ } catch (error) {
1759
+ Sentry2.captureException(error);
1760
+ if (!res.headersSent) {
1761
+ setJson2(res, 500, { error: "Internal review error" });
1691
1762
  }
1692
1763
  return true;
1693
1764
  }
@@ -1695,6 +1766,7 @@ async function handleReviewRoutes(req, res, url, context) {
1695
1766
  }
1696
1767
 
1697
1768
  // src/server/skillsRoutes.ts
1769
+ import * as Sentry3 from "@sentry/node";
1698
1770
  import { spawn as spawn3 } from "child_process";
1699
1771
  import { mkdtemp as mkdtemp2, readFile as readFile2, readdir, rm as rm3, mkdir as mkdir3, stat as stat3, lstat, readlink, symlink } from "fs/promises";
1700
1772
  import { existsSync as existsSync2 } from "fs";
@@ -2685,357 +2757,365 @@ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
2685
2757
  }
2686
2758
  async function handleSkillsRoutes(req, res, url, context) {
2687
2759
  const { appServer, readJsonBody: readJsonBody2 } = context;
2688
- if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
2689
- try {
2690
- const q = url.searchParams.get("q") || "";
2691
- const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
2692
- const sort = url.searchParams.get("sort") || "date";
2693
- const allEntries = await fetchSkillsTree();
2694
- const installedMap = await scanInstalledSkillsFromDisk();
2760
+ try {
2761
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
2695
2762
  try {
2696
- const result = await appServer.rpc("skills/list", {});
2697
- for (const entry of result.data ?? []) {
2698
- for (const skill of entry.skills ?? []) {
2699
- if (skill.name) {
2700
- installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2763
+ const q = url.searchParams.get("q") || "";
2764
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
2765
+ const sort = url.searchParams.get("sort") || "date";
2766
+ const allEntries = await fetchSkillsTree();
2767
+ const installedMap = await scanInstalledSkillsFromDisk();
2768
+ try {
2769
+ const result = await appServer.rpc("skills/list", {});
2770
+ for (const entry of result.data ?? []) {
2771
+ for (const skill of entry.skills ?? []) {
2772
+ if (skill.name) {
2773
+ installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2774
+ }
2701
2775
  }
2702
2776
  }
2777
+ } catch {
2703
2778
  }
2704
- } catch {
2705
- }
2706
- const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
2707
- await fetchMetaBatch(installedHubEntries);
2708
- const installed = [];
2709
- for (const [, info] of installedMap) {
2710
- const hubEntry = allEntries.find((e) => e.name === info.name);
2711
- const base = hubEntry ? buildHubEntry(hubEntry) : {
2712
- name: info.name,
2713
- owner: "local",
2714
- description: "",
2715
- displayName: "",
2716
- publishedAt: 0,
2717
- avatarUrl: "",
2718
- url: "",
2719
- installed: false
2720
- };
2721
- installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
2779
+ const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
2780
+ await fetchMetaBatch(installedHubEntries);
2781
+ const installed = [];
2782
+ for (const [, info] of installedMap) {
2783
+ const hubEntry = allEntries.find((e) => e.name === info.name);
2784
+ const base = hubEntry ? buildHubEntry(hubEntry) : {
2785
+ name: info.name,
2786
+ owner: "local",
2787
+ description: "",
2788
+ displayName: "",
2789
+ publishedAt: 0,
2790
+ avatarUrl: "",
2791
+ url: "",
2792
+ installed: false
2793
+ };
2794
+ installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
2795
+ }
2796
+ const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
2797
+ setJson3(res, 200, { data: results, installed, total: allEntries.length });
2798
+ } catch (error) {
2799
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch skills hub") });
2722
2800
  }
2723
- const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
2724
- setJson3(res, 200, { data: results, installed, total: allEntries.length });
2725
- } catch (error) {
2726
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch skills hub") });
2801
+ return true;
2727
2802
  }
2728
- return true;
2729
- }
2730
- if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
2731
- const state = await readSkillsSyncState();
2732
- setJson3(res, 200, {
2733
- data: {
2734
- loggedIn: Boolean(state.githubToken),
2735
- githubUsername: state.githubUsername ?? "",
2736
- repoOwner: state.repoOwner ?? "",
2737
- repoName: state.repoName ?? "",
2738
- configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
2739
- telemetry: {
2740
- lastPullCommitSha: state.lastPullCommitSha ?? "",
2741
- lastPushCommitSha: state.lastPushCommitSha ?? "",
2742
- lastSyncAttemptCount: state.lastSyncAttemptCount ?? 0,
2743
- lastSyncError: state.lastSyncError ?? "",
2744
- lastSyncAtIso: state.lastSyncAtIso ?? ""
2745
- },
2746
- startup: {
2747
- inProgress: startupSyncStatus.inProgress,
2748
- mode: startupSyncStatus.mode,
2749
- branch: startupSyncStatus.branch,
2750
- lastAction: startupSyncStatus.lastAction,
2751
- lastRunAtIso: startupSyncStatus.lastRunAtIso,
2752
- lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
2753
- lastError: startupSyncStatus.lastError
2803
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
2804
+ const state = await readSkillsSyncState();
2805
+ setJson3(res, 200, {
2806
+ data: {
2807
+ loggedIn: Boolean(state.githubToken),
2808
+ githubUsername: state.githubUsername ?? "",
2809
+ repoOwner: state.repoOwner ?? "",
2810
+ repoName: state.repoName ?? "",
2811
+ configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
2812
+ telemetry: {
2813
+ lastPullCommitSha: state.lastPullCommitSha ?? "",
2814
+ lastPushCommitSha: state.lastPushCommitSha ?? "",
2815
+ lastSyncAttemptCount: state.lastSyncAttemptCount ?? 0,
2816
+ lastSyncError: state.lastSyncError ?? "",
2817
+ lastSyncAtIso: state.lastSyncAtIso ?? ""
2818
+ },
2819
+ startup: {
2820
+ inProgress: startupSyncStatus.inProgress,
2821
+ mode: startupSyncStatus.mode,
2822
+ branch: startupSyncStatus.branch,
2823
+ lastAction: startupSyncStatus.lastAction,
2824
+ lastRunAtIso: startupSyncStatus.lastRunAtIso,
2825
+ lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
2826
+ lastError: startupSyncStatus.lastError
2827
+ }
2754
2828
  }
2755
- }
2756
- });
2757
- return true;
2758
- }
2759
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
2760
- try {
2761
- const started = await startGithubDeviceLogin();
2762
- setJson3(res, 200, { data: started });
2763
- } catch (error) {
2764
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to start GitHub login") });
2829
+ });
2830
+ return true;
2765
2831
  }
2766
- return true;
2767
- }
2768
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
2769
- try {
2770
- const payload = asRecord3(await readJsonBody2(req));
2771
- const token = typeof payload?.token === "string" ? payload.token.trim() : "";
2772
- if (!token) {
2773
- setJson3(res, 400, { error: "Missing GitHub token" });
2774
- return true;
2832
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
2833
+ try {
2834
+ const started = await startGithubDeviceLogin();
2835
+ setJson3(res, 200, { data: started });
2836
+ } catch (error) {
2837
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to start GitHub login") });
2775
2838
  }
2776
- const username = await resolveGithubUsername(token);
2777
- await finalizeGithubLoginAndSync(token, username, appServer);
2778
- setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2779
- } catch (error) {
2780
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to login with GitHub token") });
2839
+ return true;
2781
2840
  }
2782
- return true;
2783
- }
2784
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
2785
- try {
2786
- const state = await readSkillsSyncState();
2787
- await writeSkillsSyncState({
2788
- ...state,
2789
- githubToken: void 0,
2790
- githubUsername: void 0,
2791
- repoOwner: void 0,
2792
- repoName: void 0
2793
- });
2794
- setJson3(res, 200, { ok: true });
2795
- } catch (error) {
2796
- setJson3(res, 500, { error: getErrorMessage3(error, "Failed to logout GitHub") });
2797
- }
2798
- return true;
2799
- }
2800
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
2801
- try {
2802
- const payload = asRecord3(await readJsonBody2(req));
2803
- const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
2804
- if (!deviceCode) {
2805
- setJson3(res, 400, { error: "Missing deviceCode" });
2806
- return true;
2807
- }
2808
- const result = await completeGithubDeviceLogin(deviceCode);
2809
- if (!result.token) {
2810
- setJson3(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
2811
- return true;
2841
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
2842
+ try {
2843
+ const payload = asRecord3(await readJsonBody2(req));
2844
+ const token = typeof payload?.token === "string" ? payload.token.trim() : "";
2845
+ if (!token) {
2846
+ setJson3(res, 400, { error: "Missing GitHub token" });
2847
+ return true;
2848
+ }
2849
+ const username = await resolveGithubUsername(token);
2850
+ await finalizeGithubLoginAndSync(token, username, appServer);
2851
+ setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2852
+ } catch (error) {
2853
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to login with GitHub token") });
2812
2854
  }
2813
- const token = result.token;
2814
- const username = await resolveGithubUsername(token);
2815
- await finalizeGithubLoginAndSync(token, username, appServer);
2816
- setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2817
- } catch (error) {
2818
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to complete GitHub login") });
2855
+ return true;
2819
2856
  }
2820
- return true;
2821
- }
2822
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
2823
- try {
2824
- const state = await readSkillsSyncState();
2825
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
2826
- setJson3(res, 400, { error: "Skills sync is not configured yet" });
2827
- return true;
2828
- }
2829
- if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
2830
- setJson3(res, 400, { error: "Refusing to push to upstream repository" });
2831
- return true;
2857
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
2858
+ try {
2859
+ const state = await readSkillsSyncState();
2860
+ await writeSkillsSyncState({
2861
+ ...state,
2862
+ githubToken: void 0,
2863
+ githubUsername: void 0,
2864
+ repoOwner: void 0,
2865
+ repoName: void 0
2866
+ });
2867
+ setJson3(res, 200, { ok: true });
2868
+ } catch (error) {
2869
+ setJson3(res, 500, { error: getErrorMessage3(error, "Failed to logout GitHub") });
2832
2870
  }
2833
- const local = await collectLocalSyncedSkills(appServer);
2834
- const installedMap = await scanInstalledSkillsFromDisk();
2835
- await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
2836
- await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
2837
- setJson3(res, 200, { ok: true, data: { synced: local.length } });
2838
- } catch (error) {
2839
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to push synced skills") });
2840
- }
2841
- return true;
2842
- }
2843
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/startup-sync") {
2844
- try {
2845
- await runSkillsSyncStartup(appServer);
2846
- setJson3(res, 200, { ok: true });
2847
- } catch (error) {
2848
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to run startup sync") });
2871
+ return true;
2849
2872
  }
2850
- return true;
2851
- }
2852
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
2853
- try {
2854
- const state = await readSkillsSyncState();
2855
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
2856
- await bootstrapSkillsFromUpstreamIntoLocal();
2857
- try {
2858
- await appServer.rpc("skills/list", { forceReload: true });
2859
- } catch {
2860
- }
2861
- setJson3(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
2862
- return true;
2863
- }
2864
- const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
2865
- const tree = await fetchSkillsTree();
2866
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
2867
- const ambiguousNames = /* @__PURE__ */ new Set();
2868
- for (const entry of tree) {
2869
- if (ambiguousNames.has(entry.name)) continue;
2870
- const existingOwner = uniqueOwnerByName.get(entry.name);
2871
- if (!existingOwner) {
2872
- uniqueOwnerByName.set(entry.name, entry.owner);
2873
- continue;
2873
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
2874
+ try {
2875
+ const payload = asRecord3(await readJsonBody2(req));
2876
+ const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
2877
+ if (!deviceCode) {
2878
+ setJson3(res, 400, { error: "Missing deviceCode" });
2879
+ return true;
2874
2880
  }
2875
- if (existingOwner !== entry.owner) {
2876
- uniqueOwnerByName.delete(entry.name);
2877
- ambiguousNames.add(entry.name);
2881
+ const result = await completeGithubDeviceLogin(deviceCode);
2882
+ if (!result.token) {
2883
+ setJson3(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
2884
+ return true;
2878
2885
  }
2886
+ const token = result.token;
2887
+ const username = await resolveGithubUsername(token);
2888
+ await finalizeGithubLoginAndSync(token, username, appServer);
2889
+ setJson3(res, 200, { ok: true, data: { githubUsername: username } });
2890
+ } catch (error) {
2891
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to complete GitHub login") });
2879
2892
  }
2880
- const localDir = await detectUserSkillsDir(appServer);
2881
- await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
2882
- const localSkills = await scanInstalledSkillsFromDisk();
2883
- const missingAfterPull = [];
2884
- for (const skill of remote) {
2885
- const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
2886
- if (!owner) continue;
2887
- if (!localSkills.has(skill.name)) {
2888
- missingAfterPull.push(`${owner}/${skill.name}`);
2889
- continue;
2893
+ return true;
2894
+ }
2895
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
2896
+ try {
2897
+ const state = await readSkillsSyncState();
2898
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
2899
+ setJson3(res, 400, { error: "Skills sync is not configured yet" });
2900
+ return true;
2890
2901
  }
2891
- const skillPath = join4(localDir, skill.name);
2892
- await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
2893
- }
2894
- if (missingAfterPull.length > 0) {
2895
- throw new Error(`Missing skill folders after pull: ${missingAfterPull.join(", ")}`);
2896
- }
2897
- const remoteNames = new Set(remote.map((row) => row.name));
2898
- for (const [name, localInfo] of localSkills.entries()) {
2899
- if (!remoteNames.has(name)) {
2900
- await rm3(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
2902
+ if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
2903
+ setJson3(res, 400, { error: "Refusing to push to upstream repository" });
2904
+ return true;
2901
2905
  }
2906
+ const local = await collectLocalSyncedSkills(appServer);
2907
+ const installedMap = await scanInstalledSkillsFromDisk();
2908
+ await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
2909
+ await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
2910
+ setJson3(res, 200, { ok: true, data: { synced: local.length } });
2911
+ } catch (error) {
2912
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to push synced skills") });
2902
2913
  }
2903
- const nextOwners = {};
2904
- for (const item of remote) {
2905
- const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
2906
- if (owner) nextOwners[item.name] = owner;
2907
- }
2908
- const pulledHead = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: getSkillsInstallDir() }).catch(() => "");
2909
- await writeSkillsSyncState({
2910
- ...state,
2911
- installedOwners: nextOwners,
2912
- lastPullCommitSha: pulledHead.trim(),
2913
- lastSyncAttemptCount: 1,
2914
- lastSyncError: "",
2915
- lastSyncAtIso: (/* @__PURE__ */ new Date()).toISOString()
2916
- });
2914
+ return true;
2915
+ }
2916
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/startup-sync") {
2917
2917
  try {
2918
- await appServer.rpc("skills/list", { forceReload: true });
2919
- } catch {
2918
+ await runSkillsSyncStartup(appServer);
2919
+ setJson3(res, 200, { ok: true });
2920
+ } catch (error) {
2921
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to run startup sync") });
2920
2922
  }
2921
- setJson3(res, 200, { ok: true, data: { synced: remote.length } });
2922
- } catch (error) {
2923
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to pull synced skills") });
2923
+ return true;
2924
2924
  }
2925
- return true;
2926
- }
2927
- if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
2928
- try {
2929
- const owner = url.searchParams.get("owner") || "";
2930
- const name = url.searchParams.get("name") || "";
2931
- const installed = url.searchParams.get("installed") === "true";
2932
- const skillPath = url.searchParams.get("path") || "";
2933
- if (!owner || !name) {
2934
- setJson3(res, 400, { error: "Missing owner or name" });
2935
- return true;
2936
- }
2937
- if (installed) {
2938
- const installedMap = await scanInstalledSkillsFromDisk();
2939
- const installedInfo = installedMap.get(name);
2940
- const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
2941
- if (localSkillPath) {
2942
- const content2 = await readFile2(localSkillPath, "utf8");
2943
- const description2 = extractSkillDescriptionFromMarkdown(content2);
2944
- setJson3(res, 200, { content: content2, description: description2, source: "local" });
2925
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
2926
+ try {
2927
+ const state = await readSkillsSyncState();
2928
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
2929
+ await bootstrapSkillsFromUpstreamIntoLocal();
2930
+ try {
2931
+ await appServer.rpc("skills/list", { forceReload: true });
2932
+ } catch {
2933
+ }
2934
+ setJson3(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
2945
2935
  return true;
2946
2936
  }
2937
+ const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
2938
+ const tree = await fetchSkillsTree();
2939
+ const uniqueOwnerByName = /* @__PURE__ */ new Map();
2940
+ const ambiguousNames = /* @__PURE__ */ new Set();
2941
+ for (const entry of tree) {
2942
+ if (ambiguousNames.has(entry.name)) continue;
2943
+ const existingOwner = uniqueOwnerByName.get(entry.name);
2944
+ if (!existingOwner) {
2945
+ uniqueOwnerByName.set(entry.name, entry.owner);
2946
+ continue;
2947
+ }
2948
+ if (existingOwner !== entry.owner) {
2949
+ uniqueOwnerByName.delete(entry.name);
2950
+ ambiguousNames.add(entry.name);
2951
+ }
2952
+ }
2953
+ const localDir = await detectUserSkillsDir(appServer);
2954
+ await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
2955
+ const localSkills = await scanInstalledSkillsFromDisk();
2956
+ const missingAfterPull = [];
2957
+ for (const skill of remote) {
2958
+ const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
2959
+ if (!owner) continue;
2960
+ if (!localSkills.has(skill.name)) {
2961
+ missingAfterPull.push(`${owner}/${skill.name}`);
2962
+ continue;
2963
+ }
2964
+ const skillPath = join4(localDir, skill.name);
2965
+ await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
2966
+ }
2967
+ if (missingAfterPull.length > 0) {
2968
+ throw new Error(`Missing skill folders after pull: ${missingAfterPull.join(", ")}`);
2969
+ }
2970
+ const remoteNames = new Set(remote.map((row) => row.name));
2971
+ for (const [name, localInfo] of localSkills.entries()) {
2972
+ if (!remoteNames.has(name)) {
2973
+ await rm3(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
2974
+ }
2975
+ }
2976
+ const nextOwners = {};
2977
+ for (const item of remote) {
2978
+ const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
2979
+ if (owner) nextOwners[item.name] = owner;
2980
+ }
2981
+ const pulledHead = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: getSkillsInstallDir() }).catch(() => "");
2982
+ await writeSkillsSyncState({
2983
+ ...state,
2984
+ installedOwners: nextOwners,
2985
+ lastPullCommitSha: pulledHead.trim(),
2986
+ lastSyncAttemptCount: 1,
2987
+ lastSyncError: "",
2988
+ lastSyncAtIso: (/* @__PURE__ */ new Date()).toISOString()
2989
+ });
2990
+ try {
2991
+ await appServer.rpc("skills/list", { forceReload: true });
2992
+ } catch {
2993
+ }
2994
+ setJson3(res, 200, { ok: true, data: { synced: remote.length } });
2995
+ } catch (error) {
2996
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to pull synced skills") });
2947
2997
  }
2948
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
2949
- const resp = await fetch(rawUrl);
2950
- if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
2951
- const content = await resp.text();
2952
- const description = extractSkillDescriptionFromMarkdown(content);
2953
- setJson3(res, 200, { content, description, source: "remote" });
2954
- } catch (error) {
2955
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch SKILL.md") });
2998
+ return true;
2956
2999
  }
2957
- return true;
2958
- }
2959
- if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
2960
- try {
2961
- const payload = asRecord3(await readJsonBody2(req));
2962
- const owner = typeof payload?.owner === "string" ? payload.owner : "";
2963
- const name = typeof payload?.name === "string" ? payload.name : "";
2964
- if (!owner || !name) {
2965
- setJson3(res, 400, { error: "Missing owner or name" });
2966
- return true;
2967
- }
2968
- const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir2());
2969
- if (!installerScript) {
2970
- throw new Error("Skill installer script not found");
2971
- }
2972
- const pythonCommand = resolvePythonCommand();
2973
- if (!pythonCommand) {
2974
- throw new Error("Python 3 is required to install skills");
2975
- }
2976
- const installDest = await withTimeout(
2977
- detectUserSkillsDir(appServer),
2978
- 1e4,
2979
- "detectUserSkillsDir"
2980
- ).catch(() => getSkillsInstallDir());
2981
- const skillDir = join4(installDest, name);
2982
- if (existsSync2(skillDir)) {
2983
- await rm3(skillDir, { recursive: true, force: true });
2984
- }
2985
- await runCommand2(pythonCommand.command, [
2986
- ...pythonCommand.args,
2987
- installerScript,
2988
- "--repo",
2989
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
2990
- "--path",
2991
- `skills/${owner}/${name}`,
2992
- "--dest",
2993
- installDest,
2994
- "--method",
2995
- "git"
2996
- ], { timeoutMs: 9e4 });
3000
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
2997
3001
  try {
2998
- await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
2999
- } catch {
3002
+ const owner = url.searchParams.get("owner") || "";
3003
+ const name = url.searchParams.get("name") || "";
3004
+ const installed = url.searchParams.get("installed") === "true";
3005
+ const skillPath = url.searchParams.get("path") || "";
3006
+ if (!owner || !name) {
3007
+ setJson3(res, 400, { error: "Missing owner or name" });
3008
+ return true;
3009
+ }
3010
+ if (installed) {
3011
+ const installedMap = await scanInstalledSkillsFromDisk();
3012
+ const installedInfo = installedMap.get(name);
3013
+ const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
3014
+ if (localSkillPath) {
3015
+ const content2 = await readFile2(localSkillPath, "utf8");
3016
+ const description2 = extractSkillDescriptionFromMarkdown(content2);
3017
+ setJson3(res, 200, { content: content2, description: description2, source: "local" });
3018
+ return true;
3019
+ }
3020
+ }
3021
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
3022
+ const resp = await fetch(rawUrl);
3023
+ if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
3024
+ const content = await resp.text();
3025
+ const description = extractSkillDescriptionFromMarkdown(content);
3026
+ setJson3(res, 200, { content, description, source: "remote" });
3027
+ } catch (error) {
3028
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch SKILL.md") });
3000
3029
  }
3001
- const syncState = await readSkillsSyncState();
3002
- const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
3003
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3004
- autoPushSyncedSkills(appServer).catch(() => {
3005
- });
3006
- setJson3(res, 200, { ok: true, path: skillDir });
3007
- } catch (error) {
3008
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
3030
+ return true;
3009
3031
  }
3010
- return true;
3011
- }
3012
- if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
3013
- try {
3014
- const payload = asRecord3(await readJsonBody2(req));
3015
- const name = typeof payload?.name === "string" ? payload.name : "";
3016
- const path = typeof payload?.path === "string" ? payload.path : "";
3017
- const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
3018
- const target = normalizedPath || (name ? join4(getSkillsInstallDir(), name) : "");
3019
- if (!target) {
3020
- setJson3(res, 400, { error: "Missing name or path" });
3021
- return true;
3022
- }
3023
- await rm3(target, { recursive: true, force: true });
3024
- if (name) {
3032
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
3033
+ try {
3034
+ const payload = asRecord3(await readJsonBody2(req));
3035
+ const owner = typeof payload?.owner === "string" ? payload.owner : "";
3036
+ const name = typeof payload?.name === "string" ? payload.name : "";
3037
+ if (!owner || !name) {
3038
+ setJson3(res, 400, { error: "Missing owner or name" });
3039
+ return true;
3040
+ }
3041
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir2());
3042
+ if (!installerScript) {
3043
+ throw new Error("Skill installer script not found");
3044
+ }
3045
+ const pythonCommand = resolvePythonCommand();
3046
+ if (!pythonCommand) {
3047
+ throw new Error("Python 3 is required to install skills");
3048
+ }
3049
+ const installDest = await withTimeout(
3050
+ detectUserSkillsDir(appServer),
3051
+ 1e4,
3052
+ "detectUserSkillsDir"
3053
+ ).catch(() => getSkillsInstallDir());
3054
+ const skillDir = join4(installDest, name);
3055
+ if (existsSync2(skillDir)) {
3056
+ await rm3(skillDir, { recursive: true, force: true });
3057
+ }
3058
+ await runCommand2(pythonCommand.command, [
3059
+ ...pythonCommand.args,
3060
+ installerScript,
3061
+ "--repo",
3062
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
3063
+ "--path",
3064
+ `skills/${owner}/${name}`,
3065
+ "--dest",
3066
+ installDest,
3067
+ "--method",
3068
+ "git"
3069
+ ], { timeoutMs: 9e4 });
3070
+ try {
3071
+ await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
3072
+ } catch {
3073
+ }
3025
3074
  const syncState = await readSkillsSyncState();
3026
- const nextOwners = { ...syncState.installedOwners ?? {} };
3027
- delete nextOwners[name];
3075
+ const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
3028
3076
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3077
+ autoPushSyncedSkills(appServer).catch(() => {
3078
+ });
3079
+ setJson3(res, 200, { ok: true, path: skillDir });
3080
+ } catch (error) {
3081
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
3029
3082
  }
3030
- autoPushSyncedSkills(appServer).catch(() => {
3031
- });
3083
+ return true;
3084
+ }
3085
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
3032
3086
  try {
3033
- await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
3034
- } catch {
3087
+ const payload = asRecord3(await readJsonBody2(req));
3088
+ const name = typeof payload?.name === "string" ? payload.name : "";
3089
+ const path = typeof payload?.path === "string" ? payload.path : "";
3090
+ const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
3091
+ const target = normalizedPath || (name ? join4(getSkillsInstallDir(), name) : "");
3092
+ if (!target) {
3093
+ setJson3(res, 400, { error: "Missing name or path" });
3094
+ return true;
3095
+ }
3096
+ await rm3(target, { recursive: true, force: true });
3097
+ if (name) {
3098
+ const syncState = await readSkillsSyncState();
3099
+ const nextOwners = { ...syncState.installedOwners ?? {} };
3100
+ delete nextOwners[name];
3101
+ await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3102
+ }
3103
+ autoPushSyncedSkills(appServer).catch(() => {
3104
+ });
3105
+ try {
3106
+ await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
3107
+ } catch {
3108
+ }
3109
+ setJson3(res, 200, { ok: true, deletedPath: target });
3110
+ } catch (error) {
3111
+ setJson3(res, 502, { error: getErrorMessage3(error, "Failed to uninstall skill") });
3035
3112
  }
3036
- setJson3(res, 200, { ok: true, deletedPath: target });
3037
- } catch (error) {
3038
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to uninstall skill") });
3113
+ return true;
3114
+ }
3115
+ } catch (error) {
3116
+ Sentry3.captureException(error);
3117
+ if (!res.headersSent) {
3118
+ setJson3(res, 500, { error: "Internal skills error" });
3039
3119
  }
3040
3120
  return true;
3041
3121
  }
@@ -3281,7 +3361,8 @@ var TelegramThreadBridge = class {
3281
3361
  const payload = asRecord4(await this.appServer.rpc("thread/list", {
3282
3362
  archived: false,
3283
3363
  limit: 20,
3284
- sortKey: "updated_at"
3364
+ sortKey: "updated_at",
3365
+ modelProviders: []
3285
3366
  }));
3286
3367
  const rows = Array.isArray(payload?.data) ? payload.data : [];
3287
3368
  const threads = [];
@@ -3464,6 +3545,190 @@ var THREAD_METHODS_WITH_TURNS = /* @__PURE__ */ new Set(["thread/read", "thread/
3464
3545
  function asRecord5(value) {
3465
3546
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3466
3547
  }
3548
+ function isInlineDataUrl(value) {
3549
+ return /^data:/iu.test(value.trim());
3550
+ }
3551
+ function extensionFromMimeType(mimeType) {
3552
+ const normalized = mimeType.trim().toLowerCase();
3553
+ if (normalized === "image/png") return ".png";
3554
+ if (normalized === "image/jpeg") return ".jpg";
3555
+ if (normalized === "image/webp") return ".webp";
3556
+ if (normalized === "image/gif") return ".gif";
3557
+ if (normalized === "image/svg+xml") return ".svg";
3558
+ if (normalized === "application/pdf") return ".pdf";
3559
+ return "";
3560
+ }
3561
+ function asNonEmptyString(value) {
3562
+ if (typeof value !== "string") return null;
3563
+ const trimmed = value.trim();
3564
+ return trimmed.length > 0 ? trimmed : null;
3565
+ }
3566
+ function toAttachmentLinkTarget(block, fallback) {
3567
+ const candidate = asNonEmptyString(block.path) ?? asNonEmptyString(block.file_path) ?? asNonEmptyString(block.filename) ?? asNonEmptyString(block.file_id) ?? fallback;
3568
+ if (candidate.startsWith("file://")) return candidate;
3569
+ if (candidate.startsWith("/")) return `file://${candidate}`;
3570
+ return `attachment://${candidate}`;
3571
+ }
3572
+ async function persistInlineDataUrlToLocalFile(dataUrl, baseName) {
3573
+ const trimmed = dataUrl.trim();
3574
+ const match = /^data:([^;,]*)(;base64)?,(.*)$/isu.exec(trimmed);
3575
+ if (!match) return null;
3576
+ const mimeType = (match[1] ?? "").trim().toLowerCase();
3577
+ const encodedPayload = match[3] ?? "";
3578
+ let bytes;
3579
+ try {
3580
+ bytes = match[2] ? Buffer.from(encodedPayload, "base64") : Buffer.from(decodeURIComponent(encodedPayload), "utf8");
3581
+ } catch {
3582
+ return null;
3583
+ }
3584
+ if (bytes.length === 0) return null;
3585
+ const hash = createHash2("sha1").update(bytes).digest("hex");
3586
+ const ext = extensionFromMimeType(mimeType);
3587
+ const mediaDir = join5(tmpdir4(), "codex-web-inline-media");
3588
+ await mkdir4(mediaDir, { recursive: true });
3589
+ const fileName = `${baseName}-${hash}${ext}`;
3590
+ const filePath = join5(mediaDir, fileName);
3591
+ try {
3592
+ await stat4(filePath);
3593
+ } catch {
3594
+ await writeFile4(filePath, bytes);
3595
+ }
3596
+ return filePath;
3597
+ }
3598
+ function toLocalImageProxyUrl(path) {
3599
+ return `/codex-local-image?path=${encodeURIComponent(path)}`;
3600
+ }
3601
+ async function sanitizeInlineUserContentBlock(block, context) {
3602
+ const record = asRecord5(block);
3603
+ if (!record) return block;
3604
+ const type = asNonEmptyString(record.type) ?? "";
3605
+ const imageUrl = asNonEmptyString(record.url) ?? asNonEmptyString(record.image_url);
3606
+ if (imageUrl && isInlineDataUrl(imageUrl)) {
3607
+ const localUrl = await persistInlineDataUrlToLocalFile(imageUrl, `inline-image-${context.turnId}-${context.itemId}-${String(context.blockIndex)}`);
3608
+ if (localUrl) {
3609
+ return {
3610
+ ...record,
3611
+ type: "image",
3612
+ url: toLocalImageProxyUrl(localUrl)
3613
+ };
3614
+ }
3615
+ const target = toAttachmentLinkTarget(record, `inline-image/${context.turnId}/${context.itemId}/${String(context.blockIndex)}`);
3616
+ return {
3617
+ type: "text",
3618
+ text: `Image attachment: ${target}`
3619
+ };
3620
+ }
3621
+ const inlineFileData = asNonEmptyString(record.file_data) ?? asNonEmptyString(record.data) ?? asNonEmptyString(record.base64);
3622
+ if ((type.includes("file") || type === "input_file" || type === "file") && inlineFileData) {
3623
+ const mimeType = asNonEmptyString(record.mime_type) ?? "application/octet-stream";
3624
+ const fileDataUrl = `data:${mimeType};base64,${inlineFileData}`;
3625
+ const localUrl = await persistInlineDataUrlToLocalFile(fileDataUrl, `inline-file-${context.turnId}-${context.itemId}-${String(context.blockIndex)}`);
3626
+ if (localUrl) {
3627
+ return {
3628
+ type: "text",
3629
+ text: `File attachment: ${localUrl}`
3630
+ };
3631
+ }
3632
+ const target = toAttachmentLinkTarget(record, `inline-file/${context.turnId}/${context.itemId}/${String(context.blockIndex)}`);
3633
+ return {
3634
+ type: "text",
3635
+ text: `File attachment: ${target}`
3636
+ };
3637
+ }
3638
+ return block;
3639
+ }
3640
+ async function sanitizeInlinePayloadDeep(value, context) {
3641
+ const maybeBlock = await sanitizeInlineUserContentBlock(value, context);
3642
+ if (maybeBlock !== value) {
3643
+ return { value: maybeBlock, changed: true };
3644
+ }
3645
+ if (Array.isArray(value)) {
3646
+ let changed2 = false;
3647
+ const nextArray = [];
3648
+ for (let index = 0; index < value.length; index += 1) {
3649
+ const nested = await sanitizeInlinePayloadDeep(value[index], {
3650
+ turnId: context.turnId,
3651
+ itemId: context.itemId,
3652
+ blockIndex: index
3653
+ });
3654
+ if (nested.changed) changed2 = true;
3655
+ nextArray.push(nested.value);
3656
+ }
3657
+ return changed2 ? { value: nextArray, changed: true } : { value, changed: false };
3658
+ }
3659
+ const record = asRecord5(value);
3660
+ if (!record) return { value, changed: false };
3661
+ let changed = false;
3662
+ const nextRecord = {};
3663
+ for (const [key, nestedValue] of Object.entries(record)) {
3664
+ const nested = await sanitizeInlinePayloadDeep(nestedValue, {
3665
+ turnId: context.turnId,
3666
+ itemId: context.itemId,
3667
+ blockIndex: context.blockIndex
3668
+ });
3669
+ if (nested.changed) changed = true;
3670
+ nextRecord[key] = nested.value;
3671
+ }
3672
+ return changed ? { value: nextRecord, changed: true } : { value, changed: false };
3673
+ }
3674
+ async function sanitizeThreadTurnsInlinePayloads(method, result) {
3675
+ if (!THREAD_METHODS_WITH_TURNS.has(method)) return result;
3676
+ const record = asRecord5(result);
3677
+ const thread = asRecord5(record?.thread);
3678
+ const turns = Array.isArray(thread?.turns) ? thread.turns : null;
3679
+ if (!record || !thread || !turns || turns.length === 0) return result;
3680
+ let changed = false;
3681
+ const nextTurns = [];
3682
+ for (let turnIndex = 0; turnIndex < turns.length; turnIndex += 1) {
3683
+ const turn = turns[turnIndex];
3684
+ const turnRecord = asRecord5(turn);
3685
+ const turnId = asNonEmptyString(turnRecord?.id) ?? "turn";
3686
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : null;
3687
+ if (!turnRecord || !items) {
3688
+ nextTurns.push(turn);
3689
+ continue;
3690
+ }
3691
+ let itemChanged = false;
3692
+ const nextItems = [];
3693
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
3694
+ const item = items[itemIndex];
3695
+ const itemRecord = asRecord5(item);
3696
+ const itemId = asNonEmptyString(itemRecord?.id) ?? "item";
3697
+ if (!itemRecord) {
3698
+ nextItems.push(item);
3699
+ continue;
3700
+ }
3701
+ const sanitizedItem = await sanitizeInlinePayloadDeep(item, {
3702
+ turnId,
3703
+ itemId,
3704
+ blockIndex: itemIndex + turnIndex
3705
+ });
3706
+ if (!sanitizedItem.changed) {
3707
+ nextItems.push(item);
3708
+ continue;
3709
+ }
3710
+ itemChanged = true;
3711
+ nextItems.push(sanitizedItem.value);
3712
+ }
3713
+ if (!itemChanged) {
3714
+ nextTurns.push(turn);
3715
+ continue;
3716
+ }
3717
+ changed = true;
3718
+ nextTurns.push({
3719
+ ...turnRecord,
3720
+ items: nextItems
3721
+ });
3722
+ }
3723
+ if (!changed) return result;
3724
+ return {
3725
+ ...record,
3726
+ thread: {
3727
+ ...thread,
3728
+ turns: nextTurns
3729
+ }
3730
+ };
3731
+ }
3467
3732
  function trimThreadTurnsInRpcResult(method, result) {
3468
3733
  if (!THREAD_METHODS_WITH_TURNS.has(method)) return result;
3469
3734
  const record = asRecord5(result);
@@ -3549,105 +3814,107 @@ function normalizeProviderModelsData(payload) {
3549
3814
  return ids;
3550
3815
  }
3551
3816
  async function readProviderBackedModelIds(appServer) {
3552
- const configPayload = asRecord5(await appServer.rpc("config/read", {}));
3553
- const config = asRecord5(configPayload?.config);
3554
- const providerId = readNonEmptyString(config?.model_provider);
3555
- if (!providerId) {
3556
- return { data: [], providerId: "", source: "provider" };
3557
- }
3558
- const providers = asRecord5(config?.model_providers);
3559
- const provider = asRecord5(providers?.[providerId]);
3560
- if (!provider) {
3561
- logProviderModelDiscoveryWarning("configured provider is missing from model_providers", { providerId });
3562
- return { data: [], providerId, source: "provider" };
3563
- }
3564
- const wireApi = readNonEmptyString(provider.wire_api);
3565
- if (wireApi !== "responses") {
3566
- return { data: [], providerId, source: "provider" };
3567
- }
3568
- const baseUrl = readNonEmptyString(provider.base_url);
3569
- if (!baseUrl) {
3570
- logProviderModelDiscoveryWarning("responses provider is missing base_url", { providerId });
3571
- return { data: [], providerId, source: "provider" };
3572
- }
3573
- const headers = new Headers();
3574
- const configuredHeaders = asRecord5(provider.http_headers);
3575
- if (configuredHeaders) {
3576
- for (const [key, rawValue] of Object.entries(configuredHeaders)) {
3577
- const normalized = normalizeHeaderValue(rawValue);
3578
- if (!normalized) continue;
3579
- headers.set(key, normalized);
3580
- }
3581
- }
3582
- const bearerToken = readNonEmptyString(provider.experimental_bearer_token);
3583
- if (bearerToken && !headers.has("Authorization")) {
3584
- headers.set("Authorization", `Bearer ${bearerToken}`);
3585
- }
3586
- const envKey = readNonEmptyString(provider.env_key);
3587
- const envHttpHeaders = asRecord5(provider.env_http_headers);
3588
- if (envKey || envHttpHeaders) {
3589
- logProviderModelDiscoveryWarning("provider discovery skipped env-backed auth/header expansion", {
3590
- providerId,
3591
- hasEnvKey: Boolean(envKey),
3592
- hasEnvHttpHeaders: Boolean(envHttpHeaders)
3593
- });
3594
- }
3595
- let requestUrl;
3596
- try {
3597
- requestUrl = buildProviderModelsUrl(baseUrl, provider.query_params);
3598
- } catch (error) {
3599
- logProviderModelDiscoveryWarning("provider /models URL was invalid", {
3600
- providerId,
3601
- error: getErrorMessage5(error, "invalid url")
3602
- });
3603
- return { data: [], providerId, source: "provider" };
3604
- }
3605
- let response;
3606
- try {
3607
- response = await fetch(requestUrl, {
3608
- method: "GET",
3609
- headers,
3610
- signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS)
3611
- });
3612
- } catch (error) {
3613
- logProviderModelDiscoveryWarning("provider /models request failed", {
3614
- providerId,
3615
- error: isTimeoutError(error) ? `request timed out after ${PROVIDER_MODELS_FETCH_TIMEOUT_MS}ms` : getErrorMessage5(error, "network error")
3616
- });
3617
- return { data: [], providerId, source: "provider" };
3618
- }
3619
- let payload = null;
3620
- try {
3621
- payload = await response.json();
3622
- } catch (error) {
3623
- logProviderModelDiscoveryWarning("provider /models response was not valid JSON", {
3624
- providerId,
3625
- status: response.status,
3626
- error: getErrorMessage5(error, "invalid json")
3627
- });
3628
- return { data: [], providerId, source: "provider" };
3629
- }
3630
- if (!response.ok) {
3631
- logProviderModelDiscoveryWarning("provider /models request returned non-2xx", {
3632
- providerId,
3633
- status: response.status,
3634
- statusText: response.statusText
3635
- });
3636
- return { data: [], providerId, source: "provider" };
3637
- }
3638
- try {
3639
- return {
3640
- data: normalizeProviderModelsData(payload),
3641
- providerId,
3642
- source: "provider"
3643
- };
3644
- } catch (error) {
3645
- logProviderModelDiscoveryWarning("provider /models payload was invalid", {
3646
- providerId,
3647
- error: getErrorMessage5(error, "invalid payload")
3648
- });
3649
- return { data: [], providerId, source: "provider" };
3650
- }
3817
+ return Sentry4.startSpan({ name: "readProviderBackedModelIds", op: "function.slow" }, async () => {
3818
+ const configPayload = asRecord5(await appServer.rpc("config/read", {}));
3819
+ const config = asRecord5(configPayload?.config);
3820
+ const providerId = readNonEmptyString(config?.model_provider);
3821
+ if (!providerId) {
3822
+ return { data: [], providerId: "", source: "provider" };
3823
+ }
3824
+ const providers = asRecord5(config?.model_providers);
3825
+ const provider = asRecord5(providers?.[providerId]);
3826
+ if (!provider) {
3827
+ logProviderModelDiscoveryWarning("configured provider is missing from model_providers", { providerId });
3828
+ return { data: [], providerId, source: "provider" };
3829
+ }
3830
+ const wireApi = readNonEmptyString(provider.wire_api);
3831
+ if (wireApi !== "responses") {
3832
+ return { data: [], providerId, source: "provider" };
3833
+ }
3834
+ const baseUrl = readNonEmptyString(provider.base_url);
3835
+ if (!baseUrl) {
3836
+ logProviderModelDiscoveryWarning("responses provider is missing base_url", { providerId });
3837
+ return { data: [], providerId, source: "provider" };
3838
+ }
3839
+ const headers = new Headers();
3840
+ const configuredHeaders = asRecord5(provider.http_headers);
3841
+ if (configuredHeaders) {
3842
+ for (const [key, rawValue] of Object.entries(configuredHeaders)) {
3843
+ const normalized = normalizeHeaderValue(rawValue);
3844
+ if (!normalized) continue;
3845
+ headers.set(key, normalized);
3846
+ }
3847
+ }
3848
+ const bearerToken = readNonEmptyString(provider.experimental_bearer_token);
3849
+ if (bearerToken && !headers.has("Authorization")) {
3850
+ headers.set("Authorization", `Bearer ${bearerToken}`);
3851
+ }
3852
+ const envKey = readNonEmptyString(provider.env_key);
3853
+ const envHttpHeaders = asRecord5(provider.env_http_headers);
3854
+ if (envKey || envHttpHeaders) {
3855
+ logProviderModelDiscoveryWarning("provider discovery skipped env-backed auth/header expansion", {
3856
+ providerId,
3857
+ hasEnvKey: Boolean(envKey),
3858
+ hasEnvHttpHeaders: Boolean(envHttpHeaders)
3859
+ });
3860
+ }
3861
+ let requestUrl;
3862
+ try {
3863
+ requestUrl = buildProviderModelsUrl(baseUrl, provider.query_params);
3864
+ } catch (error) {
3865
+ logProviderModelDiscoveryWarning("provider /models URL was invalid", {
3866
+ providerId,
3867
+ error: getErrorMessage5(error, "invalid url")
3868
+ });
3869
+ return { data: [], providerId, source: "provider" };
3870
+ }
3871
+ let response;
3872
+ try {
3873
+ response = await fetch(requestUrl, {
3874
+ method: "GET",
3875
+ headers,
3876
+ signal: AbortSignal.timeout(PROVIDER_MODELS_FETCH_TIMEOUT_MS)
3877
+ });
3878
+ } catch (error) {
3879
+ logProviderModelDiscoveryWarning("provider /models request failed", {
3880
+ providerId,
3881
+ error: isTimeoutError(error) ? `request timed out after ${PROVIDER_MODELS_FETCH_TIMEOUT_MS}ms` : getErrorMessage5(error, "network error")
3882
+ });
3883
+ return { data: [], providerId, source: "provider" };
3884
+ }
3885
+ let payload = null;
3886
+ try {
3887
+ payload = await response.json();
3888
+ } catch (error) {
3889
+ logProviderModelDiscoveryWarning("provider /models response was not valid JSON", {
3890
+ providerId,
3891
+ status: response.status,
3892
+ error: getErrorMessage5(error, "invalid json")
3893
+ });
3894
+ return { data: [], providerId, source: "provider" };
3895
+ }
3896
+ if (!response.ok) {
3897
+ logProviderModelDiscoveryWarning("provider /models request returned non-2xx", {
3898
+ providerId,
3899
+ status: response.status,
3900
+ statusText: response.statusText
3901
+ });
3902
+ return { data: [], providerId, source: "provider" };
3903
+ }
3904
+ try {
3905
+ return {
3906
+ data: normalizeProviderModelsData(payload),
3907
+ providerId,
3908
+ source: "provider"
3909
+ };
3910
+ } catch (error) {
3911
+ logProviderModelDiscoveryWarning("provider /models payload was invalid", {
3912
+ providerId,
3913
+ error: getErrorMessage5(error, "invalid payload")
3914
+ });
3915
+ return { data: [], providerId, source: "provider" };
3916
+ }
3917
+ });
3651
3918
  }
3652
3919
  function extractThreadMessageText(threadReadPayload) {
3653
3920
  const payload = asRecord5(threadReadPayload);
@@ -3866,6 +4133,401 @@ function buildSessionFileChangeFallback(threadReadPayload, sessionLogRaw) {
3866
4133
  }
3867
4134
  return recovered.sort((first, second) => first.turnIndex - second.turnIndex);
3868
4135
  }
4136
+ function parseExecCommandOutput(output) {
4137
+ let exitCode = null;
4138
+ let wallTime = null;
4139
+ const outputLines = [];
4140
+ let pastHeader = false;
4141
+ for (const line of output.split("\n")) {
4142
+ if (!pastHeader) {
4143
+ const exitMatch = line.match(/^Process exited with code (\d+)/);
4144
+ if (exitMatch) {
4145
+ exitCode = Number.parseInt(exitMatch[1], 10);
4146
+ continue;
4147
+ }
4148
+ const wallMatch = line.match(/^Wall time:\s+([\d.]+)\s+seconds/);
4149
+ if (wallMatch) {
4150
+ wallTime = Math.round(Number.parseFloat(wallMatch[1]) * 1e3);
4151
+ continue;
4152
+ }
4153
+ if (line.startsWith("Command:") || line.startsWith("Chunk ID:") || line.startsWith("Original token count:")) {
4154
+ continue;
4155
+ }
4156
+ if (line === "Output:") {
4157
+ pastHeader = true;
4158
+ continue;
4159
+ }
4160
+ }
4161
+ outputLines.push(line);
4162
+ }
4163
+ return { exitCode, wallTime, cleanOutput: outputLines.join("\n").trimEnd() };
4164
+ }
4165
+ function buildSessionItemOrder(sessionLogRaw, turnIds) {
4166
+ let currentTurnId = "";
4167
+ const orderByTurnId = /* @__PURE__ */ new Map();
4168
+ const callIdToCommand = /* @__PURE__ */ new Map();
4169
+ for (const line of sessionLogRaw.split("\n")) {
4170
+ if (!line.trim()) continue;
4171
+ let row = null;
4172
+ try {
4173
+ row = JSON.parse(line);
4174
+ } catch {
4175
+ continue;
4176
+ }
4177
+ if (row.type === "turn_context") {
4178
+ const p = asRecord5(row.payload);
4179
+ currentTurnId = readNonEmptyString(p?.turn_id) || currentTurnId;
4180
+ continue;
4181
+ }
4182
+ if (row.type === "event_msg") {
4183
+ const p = asRecord5(row.payload);
4184
+ if (p?.type === "task_started") {
4185
+ currentTurnId = readNonEmptyString(p.turn_id) || currentTurnId;
4186
+ }
4187
+ continue;
4188
+ }
4189
+ if (row.type !== "response_item" || !currentTurnId || !turnIds.has(currentTurnId)) continue;
4190
+ const payload = asRecord5(row.payload);
4191
+ if (!payload) continue;
4192
+ let slots = orderByTurnId.get(currentTurnId);
4193
+ if (!slots) {
4194
+ slots = [];
4195
+ orderByTurnId.set(currentTurnId, slots);
4196
+ }
4197
+ if (payload.type === "message" && payload.role === "assistant") {
4198
+ slots.push({ type: "agentMessage" });
4199
+ continue;
4200
+ }
4201
+ if (payload.type === "function_call" && payload.name === "exec_command") {
4202
+ const callId = readNonEmptyString(payload.call_id);
4203
+ if (!callId) continue;
4204
+ let cmd = "";
4205
+ try {
4206
+ const args = JSON.parse(payload.arguments);
4207
+ cmd = typeof args.cmd === "string" ? args.cmd : "";
4208
+ } catch {
4209
+ }
4210
+ const command = {
4211
+ id: `session-cmd-${callId}`,
4212
+ type: "commandExecution",
4213
+ command: cmd,
4214
+ cwd: null,
4215
+ status: "completed",
4216
+ aggregatedOutput: "",
4217
+ exitCode: null,
4218
+ durationMs: null
4219
+ };
4220
+ callIdToCommand.set(callId, command);
4221
+ slots.push({ type: "commandExecution", command });
4222
+ continue;
4223
+ }
4224
+ if (payload.type === "function_call_output") {
4225
+ const callId = readNonEmptyString(payload.call_id);
4226
+ if (!callId) continue;
4227
+ const existing = callIdToCommand.get(callId);
4228
+ if (!existing) continue;
4229
+ const rawOutput = typeof payload.output === "string" ? payload.output : "";
4230
+ const parsed = parseExecCommandOutput(rawOutput);
4231
+ existing.aggregatedOutput = parsed.cleanOutput;
4232
+ existing.exitCode = parsed.exitCode;
4233
+ existing.durationMs = parsed.wallTime;
4234
+ existing.status = parsed.exitCode === 0 || parsed.exitCode === null ? "completed" : "failed";
4235
+ }
4236
+ if (payload.type === "custom_tool_call" && payload.name === "apply_patch" && payload.status === "completed") {
4237
+ const input = typeof payload.input === "string" ? payload.input : "";
4238
+ const callId = readNonEmptyString(payload.call_id);
4239
+ if (!input || !callId) continue;
4240
+ const parsedChanges = parseApplyPatchInput(input);
4241
+ if (parsedChanges.length === 0) continue;
4242
+ const fcItem = {
4243
+ id: `session-fc-${callId}`,
4244
+ type: "fileChange",
4245
+ status: "completed",
4246
+ changes: parsedChanges.map((fc) => ({
4247
+ ...fc,
4248
+ kind: { type: fc.operation, ...fc.movedToPath ? { move_path: fc.movedToPath } : {} }
4249
+ }))
4250
+ };
4251
+ slots.push({ type: "fileChange", fileChange: fcItem });
4252
+ }
4253
+ }
4254
+ return orderByTurnId;
4255
+ }
4256
+ function extractFilePathsFromCommand(cmd, cwd) {
4257
+ const paths = [];
4258
+ const absPathPattern = /(?:^|\s|>>|>|<)(\/?(?:Users|home|tmp|var|etc|root)\/[^\s;|&><"']+)/g;
4259
+ let match;
4260
+ while ((match = absPathPattern.exec(cmd)) !== null) {
4261
+ const p = match[1]?.trim();
4262
+ if (p && !p.endsWith("/") && !p.startsWith("-")) paths.push(p);
4263
+ }
4264
+ const redirectPattern = /(?:>>?|cat\s*>\s*)([^\s;|&><"']+)/g;
4265
+ while ((match = redirectPattern.exec(cmd)) !== null) {
4266
+ const p = match[1]?.trim();
4267
+ if (p && !p.startsWith("-") && !p.startsWith("/dev/")) {
4268
+ paths.push(isAbsolute2(p) ? p : join5(cwd, p));
4269
+ }
4270
+ }
4271
+ return [...new Set(paths)];
4272
+ }
4273
+ function collectFileChangesForTurns(sessionLogRaw, turnIdsToRevert, cwd) {
4274
+ let currentTurnId = "";
4275
+ const infoByTurnId = /* @__PURE__ */ new Map();
4276
+ for (const line of sessionLogRaw.split("\n")) {
4277
+ if (!line.trim()) continue;
4278
+ let row = null;
4279
+ try {
4280
+ row = JSON.parse(line);
4281
+ } catch {
4282
+ continue;
4283
+ }
4284
+ if (row.type === "turn_context") {
4285
+ const p = asRecord5(row.payload);
4286
+ currentTurnId = readNonEmptyString(p?.turn_id) || currentTurnId;
4287
+ continue;
4288
+ }
4289
+ if (row.type === "event_msg") {
4290
+ const p = asRecord5(row.payload);
4291
+ if (p?.type === "task_started") {
4292
+ currentTurnId = readNonEmptyString(p.turn_id) || currentTurnId;
4293
+ }
4294
+ continue;
4295
+ }
4296
+ if (row.type !== "response_item" || !currentTurnId || !turnIdsToRevert.has(currentTurnId)) continue;
4297
+ const payload = asRecord5(row.payload);
4298
+ if (!payload) continue;
4299
+ let info = infoByTurnId.get(currentTurnId);
4300
+ if (!info) {
4301
+ info = { patchInputs: [], commandFilePaths: [] };
4302
+ infoByTurnId.set(currentTurnId, info);
4303
+ }
4304
+ if (payload.type === "custom_tool_call" && payload.name === "apply_patch" && payload.status === "completed") {
4305
+ const input = typeof payload.input === "string" ? payload.input : "";
4306
+ const callId = readNonEmptyString(payload.call_id);
4307
+ if (input && callId) {
4308
+ info.patchInputs.push({ callId, input });
4309
+ }
4310
+ }
4311
+ if (payload.type === "function_call" && payload.name === "exec_command") {
4312
+ let cmd = "";
4313
+ try {
4314
+ const args = JSON.parse(payload.arguments);
4315
+ cmd = typeof args.cmd === "string" ? args.cmd : "";
4316
+ } catch {
4317
+ }
4318
+ if (cmd) {
4319
+ const extracted = extractFilePathsFromCommand(cmd, cwd);
4320
+ for (const p of extracted) {
4321
+ if (!info.commandFilePaths.includes(p)) info.commandFilePaths.push(p);
4322
+ }
4323
+ }
4324
+ }
4325
+ }
4326
+ return infoByTurnId;
4327
+ }
4328
+ function reverseV4aDiff(fileContent, diffText) {
4329
+ const fileLines = fileContent.split("\n");
4330
+ const rawDiffLines = diffText.split("\n");
4331
+ while (rawDiffLines.length > 0 && rawDiffLines[rawDiffLines.length - 1]?.trim() === "") rawDiffLines.pop();
4332
+ const diffLines = rawDiffLines;
4333
+ const result = [...fileLines];
4334
+ const hunks = [];
4335
+ let currentHunk = null;
4336
+ for (const dl of diffLines) {
4337
+ if (dl.startsWith("@@")) {
4338
+ if (currentHunk) hunks.push(currentHunk);
4339
+ currentHunk = [];
4340
+ continue;
4341
+ }
4342
+ if (!currentHunk) continue;
4343
+ if (dl.startsWith("+")) {
4344
+ currentHunk.push({ type: "add", text: dl.slice(1) });
4345
+ } else if (dl.startsWith("-")) {
4346
+ currentHunk.push({ type: "remove", text: dl.slice(1) });
4347
+ } else if (dl.startsWith(" ")) {
4348
+ currentHunk.push({ type: "context", text: dl.slice(1) });
4349
+ } else {
4350
+ currentHunk.push({ type: "context", text: dl });
4351
+ }
4352
+ }
4353
+ if (currentHunk) hunks.push(currentHunk);
4354
+ for (let hi = hunks.length - 1; hi >= 0; hi--) {
4355
+ const hunk = hunks[hi];
4356
+ const expectedSequence = hunk.filter((e) => e.type === "context" || e.type === "add").map((e) => e.text);
4357
+ if (expectedSequence.length === 0) continue;
4358
+ let seqStart = -1;
4359
+ outer: for (let ri = result.length - expectedSequence.length; ri >= 0; ri--) {
4360
+ for (let si = 0; si < expectedSequence.length; si++) {
4361
+ if (result[ri + si] !== expectedSequence[si]) continue outer;
4362
+ }
4363
+ seqStart = ri;
4364
+ break;
4365
+ }
4366
+ if (seqStart < 0) return null;
4367
+ const newLines = [];
4368
+ let seqIdx = 0;
4369
+ for (const entry of hunk) {
4370
+ if (entry.type === "context") {
4371
+ newLines.push(result[seqStart + seqIdx]);
4372
+ seqIdx++;
4373
+ } else if (entry.type === "add") {
4374
+ seqIdx++;
4375
+ } else if (entry.type === "remove") {
4376
+ newLines.push(entry.text);
4377
+ }
4378
+ }
4379
+ result.splice(seqStart, expectedSequence.length, ...newLines);
4380
+ }
4381
+ return result.join("\n");
4382
+ }
4383
+ async function revertTurnFileChanges(cwd, turnInfos) {
4384
+ if (turnInfos.size === 0) return { reverted: 0, errors: [] };
4385
+ let reverted = 0;
4386
+ const errors = [];
4387
+ const allEntries = [...turnInfos.values()];
4388
+ const allPatchInputs = allEntries.flatMap((info) => info.patchInputs).reverse();
4389
+ const allCommandPaths = new Set(allEntries.flatMap((info) => info.commandFilePaths));
4390
+ let isGitRepo = false;
4391
+ let gitRoot = "";
4392
+ try {
4393
+ gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
4394
+ isGitRepo = !!gitRoot;
4395
+ } catch {
4396
+ }
4397
+ const trackedFiles = /* @__PURE__ */ new Set();
4398
+ if (isGitRepo) {
4399
+ try {
4400
+ const tracked = await runCommandCapture2("git", ["ls-files", "--full-name"], { cwd: gitRoot });
4401
+ for (const f of tracked.split("\n")) {
4402
+ if (f.trim()) trackedFiles.add(join5(gitRoot, f.trim()));
4403
+ }
4404
+ } catch {
4405
+ }
4406
+ }
4407
+ const patchRevertedPaths = /* @__PURE__ */ new Set();
4408
+ for (const patch of allPatchInputs) {
4409
+ const changes = parseApplyPatchInput(patch.input);
4410
+ for (let ci = changes.length - 1; ci >= 0; ci--) {
4411
+ const change = changes[ci];
4412
+ const filePath = isAbsolute2(change.path) ? change.path : join5(cwd, change.path);
4413
+ try {
4414
+ if (change.operation === "add") {
4415
+ const fileStat = await stat4(filePath).catch(() => null);
4416
+ if (fileStat) {
4417
+ await rm4(filePath, { force: true });
4418
+ reverted++;
4419
+ patchRevertedPaths.add(filePath);
4420
+ }
4421
+ } else if (change.operation === "update" && change.diff) {
4422
+ let reversed = false;
4423
+ try {
4424
+ const currentContent = await readFile3(filePath, "utf8");
4425
+ const newContent = reverseV4aDiff(currentContent, change.diff);
4426
+ if (newContent !== null && newContent !== currentContent) {
4427
+ const { writeFile: writeFile7 } = await import("fs/promises");
4428
+ await writeFile7(filePath, newContent);
4429
+ reverted++;
4430
+ patchRevertedPaths.add(filePath);
4431
+ reversed = true;
4432
+ }
4433
+ } catch {
4434
+ }
4435
+ if (!reversed) {
4436
+ const isTracked = trackedFiles.has(filePath);
4437
+ if (isTracked && isGitRepo) {
4438
+ const relativePath = filePath.startsWith(gitRoot + "/") ? filePath.slice(gitRoot.length + 1) : filePath;
4439
+ try {
4440
+ await runCommand3("git", ["checkout", "HEAD", "--", relativePath], { cwd: gitRoot });
4441
+ reverted++;
4442
+ patchRevertedPaths.add(filePath);
4443
+ } catch {
4444
+ errors.push(`Could not revert: ${filePath}`);
4445
+ }
4446
+ } else {
4447
+ errors.push(`Could not reverse patch for untracked file: ${filePath}`);
4448
+ }
4449
+ }
4450
+ } else if (change.operation === "delete") {
4451
+ const isTracked = trackedFiles.has(filePath);
4452
+ if (isTracked && isGitRepo) {
4453
+ const relativePath = filePath.startsWith(gitRoot + "/") ? filePath.slice(gitRoot.length + 1) : filePath;
4454
+ try {
4455
+ await runCommand3("git", ["checkout", "HEAD", "--", relativePath], { cwd: gitRoot });
4456
+ reverted++;
4457
+ patchRevertedPaths.add(filePath);
4458
+ } catch {
4459
+ errors.push(`Could not restore deleted file: ${filePath}`);
4460
+ }
4461
+ }
4462
+ }
4463
+ } catch (err) {
4464
+ errors.push(`Failed to revert patch for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
4465
+ }
4466
+ }
4467
+ }
4468
+ for (const filePath of allCommandPaths) {
4469
+ if (patchRevertedPaths.has(filePath)) continue;
4470
+ const isTracked = trackedFiles.has(filePath);
4471
+ if (isTracked && isGitRepo) {
4472
+ const relativePath = filePath.startsWith(gitRoot + "/") ? filePath.slice(gitRoot.length + 1) : filePath;
4473
+ try {
4474
+ await runCommand3("git", ["checkout", "HEAD", "--", relativePath], { cwd: gitRoot });
4475
+ reverted++;
4476
+ } catch {
4477
+ errors.push(`Could not restore command-modified file: ${filePath}`);
4478
+ }
4479
+ }
4480
+ }
4481
+ return { reverted, errors };
4482
+ }
4483
+ function mergeSessionCommandsIntoTurns(turns, sessionLogRaw) {
4484
+ const turnIds = /* @__PURE__ */ new Set();
4485
+ for (const turn of turns) {
4486
+ const turnRecord = asRecord5(turn);
4487
+ const turnId = readNonEmptyString(turnRecord?.id);
4488
+ if (turnId) turnIds.add(turnId);
4489
+ }
4490
+ if (turnIds.size === 0) return turns;
4491
+ const orderByTurnId = buildSessionItemOrder(sessionLogRaw, turnIds);
4492
+ if (orderByTurnId.size === 0) return turns;
4493
+ return turns.map((turn) => {
4494
+ const turnRecord = asRecord5(turn);
4495
+ if (!turnRecord) return turn;
4496
+ const turnId = readNonEmptyString(turnRecord.id);
4497
+ if (!turnId) return turn;
4498
+ const slots = orderByTurnId.get(turnId);
4499
+ if (!slots || slots.length === 0) return turn;
4500
+ const existingItems = Array.isArray(turnRecord.items) ? turnRecord.items : [];
4501
+ const alreadyHasRecoveredItems = existingItems.some((it) => it.type === "commandExecution" || it.type === "fileChange");
4502
+ if (alreadyHasRecoveredItems) return turn;
4503
+ const agentMessages = existingItems.filter((it) => it.type === "agentMessage");
4504
+ const nonAgentNonUserItems = existingItems.filter((it) => it.type !== "agentMessage" && it.type !== "userMessage");
4505
+ const userMessages = existingItems.filter((it) => it.type === "userMessage");
4506
+ let agentIdx = 0;
4507
+ const interleaved = [...userMessages];
4508
+ for (const slot of slots) {
4509
+ if (slot.type === "agentMessage") {
4510
+ if (agentIdx < agentMessages.length) {
4511
+ interleaved.push(agentMessages[agentIdx]);
4512
+ agentIdx++;
4513
+ }
4514
+ } else if (slot.type === "commandExecution" && slot.command) {
4515
+ interleaved.push(slot.command);
4516
+ } else if (slot.type === "fileChange" && slot.fileChange) {
4517
+ interleaved.push(slot.fileChange);
4518
+ }
4519
+ }
4520
+ while (agentIdx < agentMessages.length) {
4521
+ interleaved.push(agentMessages[agentIdx]);
4522
+ agentIdx++;
4523
+ }
4524
+ interleaved.push(...nonAgentNonUserItems);
4525
+ return {
4526
+ ...turnRecord,
4527
+ items: interleaved
4528
+ };
4529
+ });
4530
+ }
3869
4531
  function isExactPhraseMatch(query, doc) {
3870
4532
  const q = query.trim().toLowerCase();
3871
4533
  if (!q) return false;
@@ -4043,6 +4705,21 @@ async function runCommandCapture2(command, args, options = {}) {
4043
4705
  });
4044
4706
  });
4045
4707
  }
4708
+ function normalizeBranchRefName(value) {
4709
+ const trimmed = value.trim();
4710
+ if (!trimmed) return "";
4711
+ if (trimmed.startsWith("refs/heads/")) return trimmed.slice("refs/heads/".length);
4712
+ if (trimmed.startsWith("refs/remotes/")) return trimmed.slice("refs/remotes/".length);
4713
+ return trimmed;
4714
+ }
4715
+ function extractBranchLockedWorktreePath(error, branchName) {
4716
+ const message = getErrorMessage5(error, "");
4717
+ if (!message || !branchName) return "";
4718
+ const escapedBranch = branchName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4719
+ const pattern = new RegExp(`'${escapedBranch}' is already checked out at '([^']+)'`, "u");
4720
+ const match = pattern.exec(message);
4721
+ return match?.[1]?.trim() ?? "";
4722
+ }
4046
4723
  function normalizeStringArray(value) {
4047
4724
  if (!Array.isArray(value)) return [];
4048
4725
  const normalized = [];
@@ -4088,6 +4765,7 @@ function getCodexSessionIndexPath() {
4088
4765
  }
4089
4766
  var MAX_THREAD_TITLES = 500;
4090
4767
  var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
4768
+ var PINNED_THREAD_IDS_KEY = "pinned-thread-ids";
4091
4769
  var sessionIndexThreadTitleCacheState = {
4092
4770
  fileSignature: null,
4093
4771
  cache: EMPTY_THREAD_TITLE_CACHE
@@ -4105,6 +4783,9 @@ function normalizeThreadTitleCache(value) {
4105
4783
  const order = normalizeStringArray(record.order);
4106
4784
  return { titles, order };
4107
4785
  }
4786
+ function normalizePinnedThreadIds(value) {
4787
+ return normalizeStringArray(value);
4788
+ }
4108
4789
  function updateThreadTitleCache(cache, id, title) {
4109
4790
  const titles = { ...cache.titles, [id]: title };
4110
4791
  const order = [id, ...cache.order.filter((o) => o !== id)];
@@ -4169,7 +4850,29 @@ async function readThreadTitleCache() {
4169
4850
  return EMPTY_THREAD_TITLE_CACHE;
4170
4851
  }
4171
4852
  }
4172
- async function writeThreadTitleCache(cache) {
4853
+ async function writeThreadTitleCache(cache) {
4854
+ const statePath = getCodexGlobalStatePath();
4855
+ let payload = {};
4856
+ try {
4857
+ const raw = await readFile3(statePath, "utf8");
4858
+ payload = asRecord5(JSON.parse(raw)) ?? {};
4859
+ } catch {
4860
+ payload = {};
4861
+ }
4862
+ payload["thread-titles"] = cache;
4863
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
4864
+ }
4865
+ async function readPinnedThreadIds() {
4866
+ const statePath = getCodexGlobalStatePath();
4867
+ try {
4868
+ const raw = await readFile3(statePath, "utf8");
4869
+ const payload = asRecord5(JSON.parse(raw)) ?? {};
4870
+ return normalizePinnedThreadIds(payload[PINNED_THREAD_IDS_KEY]);
4871
+ } catch {
4872
+ return [];
4873
+ }
4874
+ }
4875
+ async function writePinnedThreadIds(threadIds) {
4173
4876
  const statePath = getCodexGlobalStatePath();
4174
4877
  let payload = {};
4175
4878
  try {
@@ -4178,7 +4881,7 @@ async function writeThreadTitleCache(cache) {
4178
4881
  } catch {
4179
4882
  payload = {};
4180
4883
  }
4181
- payload["thread-titles"] = cache;
4884
+ payload[PINNED_THREAD_IDS_KEY] = normalizePinnedThreadIds(threadIds);
4182
4885
  await writeFile4(statePath, JSON.stringify(payload), "utf8");
4183
4886
  }
4184
4887
  function getSessionIndexFileSignature(stats) {
@@ -4445,33 +5148,40 @@ function curlImpersonatePost(url, headers, body) {
4445
5148
  });
4446
5149
  }
4447
5150
  async function proxyTranscribe(body, contentType, authToken, accountId) {
4448
- const chatgptHeaders = {
4449
- "Content-Type": contentType,
4450
- "Content-Length": body.length,
4451
- Authorization: `Bearer ${authToken}`,
4452
- originator: "Codex Desktop",
4453
- "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
4454
- };
4455
- if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
4456
- const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
4457
- let result;
4458
- try {
4459
- result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
4460
- } catch {
4461
- result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
4462
- }
4463
- if (result.status === 403 && result.body.includes("cf_chl")) {
4464
- if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
4465
- try {
4466
- const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
4467
- if (ciResult.status !== 403) return ciResult;
4468
- } catch {
5151
+ return Sentry4.startSpan({ name: "proxyTranscribe", op: "http.client.transcribe" }, async () => {
5152
+ const chatgptHeaders = {
5153
+ "Content-Type": contentType,
5154
+ "Content-Length": body.length,
5155
+ Authorization: `Bearer ${authToken}`,
5156
+ originator: "Codex Desktop",
5157
+ "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
5158
+ };
5159
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
5160
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
5161
+ let result;
5162
+ try {
5163
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
5164
+ } catch {
5165
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
5166
+ }
5167
+ if (result.status === 403 && result.body.includes("cf_chl")) {
5168
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
5169
+ try {
5170
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
5171
+ if (ciResult.status !== 403) return ciResult;
5172
+ } catch {
5173
+ }
4469
5174
  }
5175
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
4470
5176
  }
4471
- return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
4472
- }
4473
- return result;
5177
+ return result;
5178
+ });
4474
5179
  }
5180
+ var STREAM_EVENT_BUFFER_LIMIT = 400;
5181
+ var MERGEABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
5182
+ "commandExecution",
5183
+ "fileChange"
5184
+ ]);
4475
5185
  var AppServerProcess = class {
4476
5186
  constructor() {
4477
5187
  this.process = null;
@@ -4483,13 +5193,11 @@ var AppServerProcess = class {
4483
5193
  this.pending = /* @__PURE__ */ new Map();
4484
5194
  this.notificationListeners = /* @__PURE__ */ new Set();
4485
5195
  this.pendingServerRequests = /* @__PURE__ */ new Map();
4486
- this.appServerArgs = [
4487
- "app-server",
4488
- "-c",
4489
- 'approval_policy="never"',
4490
- "-c",
4491
- 'sandbox_mode="danger-full-access"'
4492
- ];
5196
+ this.appServerArgs = buildAppServerArgs();
5197
+ this.streamEventsByThreadId = /* @__PURE__ */ new Map();
5198
+ this.lastThreadReadSnapshotByThreadId = /* @__PURE__ */ new Map();
5199
+ this.capturedItemsByThreadId = /* @__PURE__ */ new Map();
5200
+ this.liveStateCache = /* @__PURE__ */ new Map();
4493
5201
  }
4494
5202
  getCodexCommand() {
4495
5203
  const codexCommand = resolveCodexCommand();
@@ -4573,10 +5281,128 @@ var AppServerProcess = class {
4573
5281
  }
4574
5282
  }
4575
5283
  emitNotification(notification) {
5284
+ this.recordStreamEvent(notification);
5285
+ this.captureItemFromNotification(notification);
5286
+ const nThreadId = this.extractThreadIdFromParams(notification.params);
5287
+ if (nThreadId) this.invalidateLiveStateCache(nThreadId);
4576
5288
  for (const listener of this.notificationListeners) {
4577
5289
  listener(notification);
4578
5290
  }
4579
5291
  }
5292
+ extractThreadIdFromParams(params) {
5293
+ const record = asRecord5(params);
5294
+ if (!record) return "";
5295
+ const threadId = (typeof record.threadId === "string" ? record.threadId : "") || (typeof record.thread_id === "string" ? record.thread_id : "") || (typeof record.conversationId === "string" ? record.conversationId : "") || (typeof record.conversation_id === "string" ? record.conversation_id : "");
5296
+ if (threadId) return threadId;
5297
+ const thread = asRecord5(record.thread);
5298
+ if (thread && typeof thread.id === "string") return thread.id;
5299
+ const turn = asRecord5(record.turn);
5300
+ if (turn) {
5301
+ const turnThreadId = (typeof turn.threadId === "string" ? turn.threadId : "") || (typeof turn.thread_id === "string" ? turn.thread_id : "");
5302
+ if (turnThreadId) return turnThreadId;
5303
+ }
5304
+ return "";
5305
+ }
5306
+ recordStreamEvent(notification) {
5307
+ const threadId = this.extractThreadIdFromParams(notification.params);
5308
+ if (!threadId) return;
5309
+ const frame = {
5310
+ method: notification.method,
5311
+ params: notification.params,
5312
+ atIso: (/* @__PURE__ */ new Date()).toISOString()
5313
+ };
5314
+ let buffer = this.streamEventsByThreadId.get(threadId);
5315
+ if (!buffer) {
5316
+ buffer = [];
5317
+ this.streamEventsByThreadId.set(threadId, buffer);
5318
+ }
5319
+ buffer.push(frame);
5320
+ if (buffer.length > STREAM_EVENT_BUFFER_LIMIT) {
5321
+ buffer.splice(0, buffer.length - STREAM_EVENT_BUFFER_LIMIT);
5322
+ }
5323
+ }
5324
+ getStreamEvents(threadId, limit) {
5325
+ const buffer = this.streamEventsByThreadId.get(threadId);
5326
+ if (!buffer || buffer.length === 0) return [];
5327
+ return buffer.slice(-limit);
5328
+ }
5329
+ storeThreadReadSnapshot(threadId, snapshot) {
5330
+ this.lastThreadReadSnapshotByThreadId.set(threadId, snapshot);
5331
+ }
5332
+ getLastThreadReadSnapshot(threadId) {
5333
+ return this.lastThreadReadSnapshotByThreadId.get(threadId) ?? null;
5334
+ }
5335
+ cacheLiveState(threadId, data, turnCount, sessionSize) {
5336
+ this.liveStateCache.set(threadId, { data, turnCount, sessionSize });
5337
+ }
5338
+ getCachedLiveState(threadId, turnCount, sessionSize) {
5339
+ const cached = this.liveStateCache.get(threadId);
5340
+ if (!cached) return null;
5341
+ if (cached.turnCount !== turnCount || cached.sessionSize !== sessionSize) return null;
5342
+ return cached.data;
5343
+ }
5344
+ invalidateLiveStateCache(threadId) {
5345
+ this.liveStateCache.delete(threadId);
5346
+ }
5347
+ captureItemFromNotification(notification) {
5348
+ if (notification.method !== "item/started" && notification.method !== "item/completed") return;
5349
+ const params = asRecord5(notification.params);
5350
+ if (!params) return;
5351
+ const item = asRecord5(params.item);
5352
+ if (!item) return;
5353
+ const itemType = typeof item.type === "string" ? item.type : "";
5354
+ if (!MERGEABLE_ITEM_TYPES.has(itemType)) return;
5355
+ const itemId = typeof item.id === "string" ? item.id : "";
5356
+ if (!itemId) return;
5357
+ const threadId = this.extractThreadIdFromParams(params);
5358
+ if (!threadId) return;
5359
+ const turnId = (typeof params.turnId === "string" ? params.turnId : "") || (typeof params.turn_id === "string" ? params.turn_id : "");
5360
+ if (!turnId) return;
5361
+ let threadItems = this.capturedItemsByThreadId.get(threadId);
5362
+ if (!threadItems) {
5363
+ threadItems = /* @__PURE__ */ new Map();
5364
+ this.capturedItemsByThreadId.set(threadId, threadItems);
5365
+ }
5366
+ const isCompleted = notification.method === "item/completed";
5367
+ const existing = threadItems.get(itemId);
5368
+ if (existing && existing.completed && !isCompleted) return;
5369
+ threadItems.set(itemId, {
5370
+ id: itemId,
5371
+ type: itemType,
5372
+ turnId,
5373
+ data: item,
5374
+ completed: isCompleted
5375
+ });
5376
+ }
5377
+ mergeItemsIntoTurns(threadId, turns) {
5378
+ const capturedMap = this.capturedItemsByThreadId.get(threadId);
5379
+ if (!capturedMap || capturedMap.size === 0) return turns;
5380
+ const itemsByTurnId = /* @__PURE__ */ new Map();
5381
+ for (const captured of capturedMap.values()) {
5382
+ let group = itemsByTurnId.get(captured.turnId);
5383
+ if (!group) {
5384
+ group = [];
5385
+ itemsByTurnId.set(captured.turnId, group);
5386
+ }
5387
+ group.push(captured);
5388
+ }
5389
+ return turns.map((turn) => {
5390
+ const turnRecord = asRecord5(turn);
5391
+ if (!turnRecord) return turn;
5392
+ const turnId = typeof turnRecord.id === "string" ? turnRecord.id : "";
5393
+ if (!turnId) return turn;
5394
+ const captured = itemsByTurnId.get(turnId);
5395
+ if (!captured || captured.length === 0) return turn;
5396
+ const existingItems = Array.isArray(turnRecord.items) ? turnRecord.items : [];
5397
+ const existingIds = new Set(existingItems.map((it) => typeof it.id === "string" ? it.id : "").filter(Boolean));
5398
+ const newItems = captured.filter((c) => !existingIds.has(c.id)).map((c) => c.data);
5399
+ if (newItems.length === 0) return turn;
5400
+ return {
5401
+ ...turnRecord,
5402
+ items: [...existingItems, ...newItems]
5403
+ };
5404
+ });
5405
+ }
4580
5406
  sendServerRequestReply(requestId, reply) {
4581
5407
  if (reply.error) {
4582
5408
  this.sendLine({
@@ -4665,7 +5491,7 @@ var AppServerProcess = class {
4665
5491
  }
4666
5492
  async rpc(method, params) {
4667
5493
  await this.ensureInitialized();
4668
- return this.call(method, params);
5494
+ return Sentry4.startSpan({ name: `rpc ${method}`, op: "rpc.codex" }, () => this.call(method, params));
4669
5495
  }
4670
5496
  onNotification(listener) {
4671
5497
  this.notificationListeners.add(listener);
@@ -4850,59 +5676,62 @@ function getSharedBridgeState() {
4850
5676
  return created;
4851
5677
  }
4852
5678
  async function loadAllThreadsForSearch(appServer) {
4853
- const threads = [];
4854
- let cursor = null;
4855
- do {
4856
- const response = asRecord5(await appServer.rpc("thread/list", {
4857
- archived: false,
4858
- limit: 100,
4859
- sortKey: "updated_at",
4860
- cursor
4861
- }));
4862
- const data = Array.isArray(response?.data) ? response.data : [];
4863
- for (const row of data) {
4864
- const record = asRecord5(row);
4865
- const id = typeof record?.id === "string" ? record.id : "";
4866
- if (!id) continue;
4867
- 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";
4868
- const preview = typeof record?.preview === "string" ? record.preview : "";
4869
- threads.push({ id, title, preview });
4870
- }
4871
- cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
4872
- } while (cursor);
4873
- const docs = [];
4874
- const concurrency = 4;
4875
- for (let offset = 0; offset < threads.length; offset += concurrency) {
4876
- const batch = threads.slice(offset, offset + concurrency);
4877
- const loaded = await Promise.all(batch.map(async (thread) => {
4878
- try {
4879
- const readResponse = await appServer.rpc("thread/read", {
4880
- threadId: thread.id,
4881
- includeTurns: true
4882
- });
4883
- const messageText = extractThreadMessageText(readResponse);
4884
- const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
4885
- return {
4886
- id: thread.id,
4887
- title: thread.title,
4888
- preview: thread.preview,
4889
- messageText,
4890
- searchableText
4891
- };
4892
- } catch {
4893
- const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
4894
- return {
4895
- id: thread.id,
4896
- title: thread.title,
4897
- preview: thread.preview,
4898
- messageText: "",
4899
- searchableText
4900
- };
4901
- }
4902
- }));
4903
- docs.push(...loaded);
4904
- }
4905
- return docs;
5679
+ return Sentry4.startSpan({ name: "loadAllThreadsForSearch", op: "search.index" }, async () => {
5680
+ const threads = [];
5681
+ let cursor = null;
5682
+ do {
5683
+ const response = asRecord5(await appServer.rpc("thread/list", {
5684
+ archived: false,
5685
+ limit: 100,
5686
+ sortKey: "updated_at",
5687
+ modelProviders: [],
5688
+ cursor
5689
+ }));
5690
+ const data = Array.isArray(response?.data) ? response.data : [];
5691
+ for (const row of data) {
5692
+ const record = asRecord5(row);
5693
+ const id = typeof record?.id === "string" ? record.id : "";
5694
+ if (!id) continue;
5695
+ 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";
5696
+ const preview = typeof record?.preview === "string" ? record.preview : "";
5697
+ threads.push({ id, title, preview });
5698
+ }
5699
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
5700
+ } while (cursor);
5701
+ const docs = [];
5702
+ const concurrency = 4;
5703
+ for (let offset = 0; offset < threads.length; offset += concurrency) {
5704
+ const batch = threads.slice(offset, offset + concurrency);
5705
+ const loaded = await Promise.all(batch.map(async (thread) => {
5706
+ try {
5707
+ const readResponse = await appServer.rpc("thread/read", {
5708
+ threadId: thread.id,
5709
+ includeTurns: true
5710
+ });
5711
+ const messageText = extractThreadMessageText(readResponse);
5712
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
5713
+ return {
5714
+ id: thread.id,
5715
+ title: thread.title,
5716
+ preview: thread.preview,
5717
+ messageText,
5718
+ searchableText
5719
+ };
5720
+ } catch {
5721
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
5722
+ return {
5723
+ id: thread.id,
5724
+ title: thread.title,
5725
+ preview: thread.preview,
5726
+ messageText: "",
5727
+ searchableText
5728
+ };
5729
+ }
5730
+ }));
5731
+ docs.push(...loaded);
5732
+ }
5733
+ return docs;
5734
+ });
4906
5735
  }
4907
5736
  async function buildThreadSearchIndex(appServer) {
4908
5737
  const docs = await loadAllThreadsForSearch(appServer);
@@ -4939,6 +5768,23 @@ function createCodexBridgeMiddleware() {
4939
5768
  return;
4940
5769
  }
4941
5770
  const url = new URL(req.url, "http://localhost");
5771
+ if (!url.pathname.startsWith("/codex-api/")) {
5772
+ next();
5773
+ return;
5774
+ }
5775
+ await Sentry4.startSpan(
5776
+ { name: `${req.method ?? "GET"} ${url.pathname}`, op: "http.server" },
5777
+ () => routeRequest(req, res, url, next)
5778
+ );
5779
+ } catch (error) {
5780
+ Sentry4.captureException(error);
5781
+ if (!res.headersSent) {
5782
+ setJson4(res, 500, { error: "Internal server error" });
5783
+ }
5784
+ }
5785
+ };
5786
+ async function routeRequest(req, res, url, next) {
5787
+ try {
4942
5788
  if (await handleAccountRoutes(req, res, url, { appServer })) {
4943
5789
  return;
4944
5790
  }
@@ -4948,6 +5794,12 @@ function createCodexBridgeMiddleware() {
4948
5794
  if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
4949
5795
  return;
4950
5796
  }
5797
+ if (req.method === "GET" && url.pathname === "/codex-api/sentry-config") {
5798
+ const enabled = !process.argv.includes("--no-sentry");
5799
+ const auth = await readCodexAuth();
5800
+ setJson4(res, 200, { enabled, accountId: auth?.accountId ?? null });
5801
+ return;
5802
+ }
4951
5803
  if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
4952
5804
  handleFileUpload(req, res);
4953
5805
  return;
@@ -4960,7 +5812,16 @@ function createCodexBridgeMiddleware() {
4960
5812
  return;
4961
5813
  }
4962
5814
  const rpcResult = await appServer.rpc(body.method, body.params ?? null);
4963
- const result = trimThreadTurnsInRpcResult(body.method, rpcResult);
5815
+ const trimmedResult = trimThreadTurnsInRpcResult(body.method, rpcResult);
5816
+ const result = await sanitizeThreadTurnsInlinePayloads(body.method, trimmedResult);
5817
+ if (THREAD_METHODS_WITH_TURNS.has(body.method)) {
5818
+ const rpcRecord = asRecord5(result);
5819
+ const rpcThread = asRecord5(rpcRecord?.thread);
5820
+ const rpcThreadId = typeof rpcThread?.id === "string" ? rpcThread.id : "";
5821
+ if (rpcThreadId) {
5822
+ appServer.storeThreadReadSnapshot(rpcThreadId, result);
5823
+ }
5824
+ }
4964
5825
  setJson4(res, 200, { result });
4965
5826
  return;
4966
5827
  }
@@ -4970,22 +5831,175 @@ function createCodexBridgeMiddleware() {
4970
5831
  setJson4(res, 400, { error: "Missing threadId" });
4971
5832
  return;
4972
5833
  }
4973
- const threadReadResult = await appServer.rpc("thread/read", {
4974
- threadId,
4975
- includeTurns: true
5834
+ await Sentry4.startSpan({ name: "thread-file-change-fallback", op: "file.session" }, async () => {
5835
+ const threadReadResult = await appServer.rpc("thread/read", {
5836
+ threadId,
5837
+ includeTurns: true
5838
+ });
5839
+ const threadReadRecord = asRecord5(threadReadResult);
5840
+ const threadRecord = asRecord5(threadReadRecord?.thread);
5841
+ const sessionPath = readNonEmptyString(threadRecord?.path);
5842
+ if (!sessionPath || !isAbsolute2(sessionPath)) {
5843
+ setJson4(res, 200, { data: [] });
5844
+ return;
5845
+ }
5846
+ try {
5847
+ const sessionLogRaw = await readFile3(sessionPath, "utf8");
5848
+ setJson4(res, 200, { data: buildSessionFileChangeFallback(threadReadResult, sessionLogRaw) });
5849
+ } catch {
5850
+ setJson4(res, 200, { data: [] });
5851
+ }
4976
5852
  });
4977
- const threadReadRecord = asRecord5(threadReadResult);
4978
- const threadRecord = asRecord5(threadReadRecord?.thread);
4979
- const sessionPath = readNonEmptyString(threadRecord?.path);
4980
- if (!sessionPath || !isAbsolute2(sessionPath)) {
4981
- setJson4(res, 200, { data: [] });
5853
+ return;
5854
+ }
5855
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-stream-events") {
5856
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
5857
+ const limitRaw = url.searchParams.get("limit")?.trim() ?? "80";
5858
+ const limit = Math.max(1, Math.min(400, Number.parseInt(limitRaw, 10) || 80));
5859
+ if (!threadId) {
5860
+ setJson4(res, 400, { error: "Missing threadId" });
5861
+ return;
5862
+ }
5863
+ const events = appServer.getStreamEvents(threadId, limit);
5864
+ setJson4(res, 200, { events });
5865
+ return;
5866
+ }
5867
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-live-state") {
5868
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
5869
+ if (!threadId) {
5870
+ setJson4(res, 400, { error: "Missing threadId" });
4982
5871
  return;
4983
5872
  }
4984
5873
  try {
4985
- const sessionLogRaw = await readFile3(sessionPath, "utf8");
4986
- setJson4(res, 200, { data: buildSessionFileChangeFallback(threadReadResult, sessionLogRaw) });
4987
- } catch {
4988
- setJson4(res, 200, { data: [] });
5874
+ const threadReadResult = await appServer.rpc("thread/read", {
5875
+ threadId,
5876
+ includeTurns: true
5877
+ });
5878
+ const sanitized = await sanitizeThreadTurnsInlinePayloads("thread/read", threadReadResult);
5879
+ appServer.storeThreadReadSnapshot(threadId, sanitized);
5880
+ const record = asRecord5(sanitized);
5881
+ const thread = asRecord5(record?.thread);
5882
+ const rawTurns = Array.isArray(thread?.turns) ? thread.turns : [];
5883
+ const sessionPath = readNonEmptyString(thread?.path);
5884
+ let sessionSize = 0;
5885
+ if (sessionPath && isAbsolute2(sessionPath)) {
5886
+ try {
5887
+ const s = await stat4(sessionPath);
5888
+ sessionSize = s.size;
5889
+ } catch {
5890
+ }
5891
+ }
5892
+ const cached = appServer.getCachedLiveState(threadId, rawTurns.length, sessionSize);
5893
+ if (cached) {
5894
+ setJson4(res, 200, cached);
5895
+ return;
5896
+ }
5897
+ let turns = appServer.mergeItemsIntoTurns(threadId, rawTurns);
5898
+ if (sessionPath && isAbsolute2(sessionPath) && sessionSize > 0) {
5899
+ try {
5900
+ const sessionLogRaw = await readFile3(sessionPath, "utf8");
5901
+ turns = mergeSessionCommandsIntoTurns(turns, sessionLogRaw);
5902
+ } catch {
5903
+ }
5904
+ }
5905
+ const lastTurn = turns.length > 0 ? asRecord5(turns[turns.length - 1]) : null;
5906
+ const isInProgress = lastTurn?.status === "inProgress";
5907
+ const responseData = {
5908
+ threadId,
5909
+ conversationState: {
5910
+ turns
5911
+ },
5912
+ ownerClientId: null,
5913
+ liveStateError: null,
5914
+ isInProgress
5915
+ };
5916
+ if (!isInProgress) {
5917
+ appServer.cacheLiveState(threadId, responseData, rawTurns.length, sessionSize);
5918
+ }
5919
+ setJson4(res, 200, responseData);
5920
+ } catch (error) {
5921
+ const snapshot = appServer.getLastThreadReadSnapshot(threadId);
5922
+ if (snapshot) {
5923
+ const record = asRecord5(snapshot);
5924
+ const thread = asRecord5(record?.thread);
5925
+ const rawTurns = Array.isArray(thread?.turns) ? thread.turns : [];
5926
+ const turns = appServer.mergeItemsIntoTurns(threadId, rawTurns);
5927
+ setJson4(res, 200, {
5928
+ threadId,
5929
+ conversationState: { turns },
5930
+ ownerClientId: null,
5931
+ liveStateError: {
5932
+ kind: "readFailed",
5933
+ message: getErrorMessage5(error, "thread/read failed")
5934
+ },
5935
+ isInProgress: false
5936
+ });
5937
+ } else {
5938
+ setJson4(res, 200, {
5939
+ threadId,
5940
+ conversationState: null,
5941
+ ownerClientId: null,
5942
+ liveStateError: {
5943
+ kind: "readFailed",
5944
+ message: getErrorMessage5(error, "thread/read failed")
5945
+ },
5946
+ isInProgress: false
5947
+ });
5948
+ }
5949
+ }
5950
+ return;
5951
+ }
5952
+ if (req.method === "POST" && url.pathname === "/codex-api/thread/rollback-files") {
5953
+ try {
5954
+ const body = asRecord5(await readJsonBody(req));
5955
+ const threadId = readNonEmptyString(body?.threadId);
5956
+ const turnId = readNonEmptyString(body?.turnId);
5957
+ const cwd = readNonEmptyString(body?.cwd);
5958
+ if (!threadId || !turnId || !cwd) {
5959
+ setJson4(res, 400, { error: "Missing threadId, turnId, or cwd" });
5960
+ return;
5961
+ }
5962
+ const threadReadResult = await appServer.rpc("thread/read", { threadId, includeTurns: true });
5963
+ const record = asRecord5(threadReadResult);
5964
+ const thread = asRecord5(record?.thread);
5965
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
5966
+ const sessionPath = readNonEmptyString(thread?.path);
5967
+ if (!sessionPath || !isAbsolute2(sessionPath)) {
5968
+ setJson4(res, 200, { reverted: 0, errors: [], message: "No session log available" });
5969
+ return;
5970
+ }
5971
+ let foundTurnIndex = -1;
5972
+ const turnIdsToRevert = /* @__PURE__ */ new Set();
5973
+ for (let i = 0; i < turns.length; i++) {
5974
+ const turnRecord = asRecord5(turns[i]);
5975
+ const id = readNonEmptyString(turnRecord?.id);
5976
+ if (id === turnId) {
5977
+ foundTurnIndex = i;
5978
+ }
5979
+ if (foundTurnIndex >= 0 && id) {
5980
+ turnIdsToRevert.add(id);
5981
+ }
5982
+ }
5983
+ if (turnIdsToRevert.size === 0) {
5984
+ setJson4(res, 200, { reverted: 0, errors: [], message: "No turns to revert" });
5985
+ return;
5986
+ }
5987
+ let sessionLogRaw;
5988
+ try {
5989
+ sessionLogRaw = await readFile3(sessionPath, "utf8");
5990
+ } catch {
5991
+ setJson4(res, 200, { reverted: 0, errors: ["Could not read session log"], message: "Session log unreadable" });
5992
+ return;
5993
+ }
5994
+ const turnInfos = collectFileChangesForTurns(sessionLogRaw, turnIdsToRevert, cwd);
5995
+ if (turnInfos.size === 0) {
5996
+ setJson4(res, 200, { reverted: 0, errors: [], message: "No file changes to revert" });
5997
+ return;
5998
+ }
5999
+ const result = await revertTurnFileChanges(cwd, turnInfos);
6000
+ setJson4(res, 200, { ...result, message: `Reverted ${result.reverted} file change(s)` });
6001
+ } catch (error) {
6002
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to revert file changes") });
4989
6003
  }
4990
6004
  return;
4991
6005
  }
@@ -5053,6 +6067,7 @@ function createCodexBridgeMiddleware() {
5053
6067
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
5054
6068
  const payload = asRecord5(await readJsonBody(req));
5055
6069
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
6070
+ const baseBranch = typeof payload?.baseBranch === "string" ? payload.baseBranch.trim() : "";
5056
6071
  if (!rawSourceCwd) {
5057
6072
  setJson4(res, 400, { error: "Missing sourceCwd" });
5058
6073
  return;
@@ -5099,19 +6114,19 @@ function createCodexBridgeMiddleware() {
5099
6114
  if (!worktreeId || !worktreeParent || !worktreeCwd) {
5100
6115
  throw new Error("Failed to allocate a unique worktree id");
5101
6116
  }
5102
- const branch = `codex/${worktreeId}`;
6117
+ const startPoint = baseBranch || "HEAD";
5103
6118
  await mkdir4(worktreeParent, { recursive: true });
5104
6119
  try {
5105
- await runCommand3("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
6120
+ await runCommand3("git", ["worktree", "add", "--detach", worktreeCwd, startPoint], { cwd: gitRoot });
5106
6121
  } catch (error) {
5107
6122
  if (!isMissingHeadError2(error)) throw error;
5108
6123
  await ensureRepoHasInitialCommit(gitRoot);
5109
- await runCommand3("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
6124
+ await runCommand3("git", ["worktree", "add", "--detach", worktreeCwd, startPoint], { cwd: gitRoot });
5110
6125
  }
5111
6126
  setJson4(res, 200, {
5112
6127
  data: {
5113
6128
  cwd: worktreeCwd,
5114
- branch,
6129
+ branch: null,
5115
6130
  gitRoot
5116
6131
  }
5117
6132
  });
@@ -5120,6 +6135,178 @@ function createCodexBridgeMiddleware() {
5120
6135
  }
5121
6136
  return;
5122
6137
  }
6138
+ if (req.method === "GET" && url.pathname === "/codex-api/worktree/branches") {
6139
+ const rawSourceCwd = (url.searchParams.get("sourceCwd") ?? "").trim();
6140
+ if (!rawSourceCwd) {
6141
+ setJson4(res, 400, { error: "Missing sourceCwd" });
6142
+ return;
6143
+ }
6144
+ const sourceCwd = isAbsolute2(rawSourceCwd) ? rawSourceCwd : resolve2(rawSourceCwd);
6145
+ try {
6146
+ const sourceInfo = await stat4(sourceCwd);
6147
+ if (!sourceInfo.isDirectory()) {
6148
+ setJson4(res, 400, { error: "sourceCwd is not a directory" });
6149
+ return;
6150
+ }
6151
+ } catch {
6152
+ setJson4(res, 404, { error: "sourceCwd does not exist" });
6153
+ return;
6154
+ }
6155
+ try {
6156
+ let gitRoot = "";
6157
+ try {
6158
+ gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
6159
+ } catch (error) {
6160
+ if (!isNotGitRepositoryError2(error)) throw error;
6161
+ setJson4(res, 200, { data: [] });
6162
+ return;
6163
+ }
6164
+ const output = await runCommandCapture2(
6165
+ "git",
6166
+ ["for-each-ref", "--format=%(committerdate:unix) %(refname)", "refs/heads", "refs/remotes"],
6167
+ { cwd: gitRoot }
6168
+ );
6169
+ const branchActivityByName = /* @__PURE__ */ new Map();
6170
+ for (const line of output.split("\n")) {
6171
+ const [rawTimestamp = "", rawRefName = ""] = line.split(" ");
6172
+ const normalized = normalizeBranchRefName(rawRefName);
6173
+ if (!normalized || normalized === "origin/HEAD") continue;
6174
+ const parsedTimestamp = Number.parseInt(rawTimestamp.trim(), 10);
6175
+ const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : 0;
6176
+ const current = branchActivityByName.get(normalized) ?? Number.MIN_SAFE_INTEGER;
6177
+ if (timestamp > current) {
6178
+ branchActivityByName.set(normalized, timestamp);
6179
+ }
6180
+ }
6181
+ const branches = Array.from(branchActivityByName.entries()).map(([value]) => ({ value, label: value })).sort((a, b) => {
6182
+ const aActivity = branchActivityByName.get(a.value) ?? 0;
6183
+ const bActivity = branchActivityByName.get(b.value) ?? 0;
6184
+ if (bActivity !== aActivity) return bActivity - aActivity;
6185
+ return a.value.localeCompare(b.value);
6186
+ });
6187
+ setJson4(res, 200, { data: branches });
6188
+ } catch (error) {
6189
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to list branches") });
6190
+ }
6191
+ return;
6192
+ }
6193
+ if (req.method === "GET" && url.pathname === "/codex-api/git/branches") {
6194
+ const rawCwd = (url.searchParams.get("cwd") ?? "").trim();
6195
+ if (!rawCwd) {
6196
+ setJson4(res, 400, { error: "Missing cwd" });
6197
+ return;
6198
+ }
6199
+ const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
6200
+ try {
6201
+ const cwdInfo = await stat4(cwd);
6202
+ if (!cwdInfo.isDirectory()) {
6203
+ setJson4(res, 400, { error: "cwd is not a directory" });
6204
+ return;
6205
+ }
6206
+ } catch {
6207
+ setJson4(res, 404, { error: "cwd does not exist" });
6208
+ return;
6209
+ }
6210
+ try {
6211
+ let gitRoot = "";
6212
+ try {
6213
+ gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
6214
+ } catch (error) {
6215
+ if (!isNotGitRepositoryError2(error)) throw error;
6216
+ setJson4(res, 200, {
6217
+ data: {
6218
+ currentBranch: null,
6219
+ options: []
6220
+ }
6221
+ });
6222
+ return;
6223
+ }
6224
+ const currentBranchRaw = await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot });
6225
+ const currentBranch = currentBranchRaw.trim() || null;
6226
+ const output = await runCommandCapture2(
6227
+ "git",
6228
+ ["for-each-ref", "--format=%(committerdate:unix) %(refname)", "refs/heads", "refs/remotes"],
6229
+ { cwd: gitRoot }
6230
+ );
6231
+ const branchActivityByName = /* @__PURE__ */ new Map();
6232
+ for (const line of output.split("\n")) {
6233
+ const [rawTimestamp = "", rawRefName = ""] = line.split(" ");
6234
+ const normalized = normalizeBranchRefName(rawRefName);
6235
+ if (!normalized || normalized === "origin/HEAD") continue;
6236
+ const parsedTimestamp = Number.parseInt(rawTimestamp.trim(), 10);
6237
+ const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : 0;
6238
+ const current = branchActivityByName.get(normalized) ?? Number.MIN_SAFE_INTEGER;
6239
+ if (timestamp > current) {
6240
+ branchActivityByName.set(normalized, timestamp);
6241
+ }
6242
+ }
6243
+ if (currentBranch && !branchActivityByName.has(currentBranch)) {
6244
+ branchActivityByName.set(currentBranch, Number.MAX_SAFE_INTEGER);
6245
+ }
6246
+ const options = Array.from(branchActivityByName.entries()).map(([value]) => ({ value, label: value })).sort((a, b) => {
6247
+ const aActivity = branchActivityByName.get(a.value) ?? 0;
6248
+ const bActivity = branchActivityByName.get(b.value) ?? 0;
6249
+ if (bActivity !== aActivity) return bActivity - aActivity;
6250
+ return a.value.localeCompare(b.value);
6251
+ });
6252
+ setJson4(res, 200, {
6253
+ data: {
6254
+ currentBranch,
6255
+ options
6256
+ }
6257
+ });
6258
+ } catch (error) {
6259
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to read Git branches") });
6260
+ }
6261
+ return;
6262
+ }
6263
+ if (req.method === "POST" && url.pathname === "/codex-api/git/checkout") {
6264
+ const payload = await readJsonBody(req);
6265
+ const record = asRecord5(payload);
6266
+ if (!record) {
6267
+ setJson4(res, 400, { error: "Invalid body: expected object" });
6268
+ return;
6269
+ }
6270
+ const rawCwd = readNonEmptyString(record.cwd);
6271
+ const targetBranch = readNonEmptyString(record.branch);
6272
+ if (!rawCwd) {
6273
+ setJson4(res, 400, { error: "Missing cwd" });
6274
+ return;
6275
+ }
6276
+ if (!targetBranch) {
6277
+ setJson4(res, 400, { error: "Missing branch" });
6278
+ return;
6279
+ }
6280
+ const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
6281
+ try {
6282
+ const cwdInfo = await stat4(cwd);
6283
+ if (!cwdInfo.isDirectory()) {
6284
+ setJson4(res, 400, { error: "cwd is not a directory" });
6285
+ return;
6286
+ }
6287
+ } catch {
6288
+ setJson4(res, 404, { error: "cwd does not exist" });
6289
+ return;
6290
+ }
6291
+ try {
6292
+ const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
6293
+ try {
6294
+ await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
6295
+ } catch (checkoutError) {
6296
+ const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, targetBranch);
6297
+ if (!blockingWorktreePath) {
6298
+ throw checkoutError;
6299
+ }
6300
+ await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
6301
+ await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
6302
+ }
6303
+ const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim() || null;
6304
+ setJson4(res, 200, { data: { currentBranch } });
6305
+ } catch (error) {
6306
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to switch branch") });
6307
+ }
6308
+ return;
6309
+ }
5123
6310
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
5124
6311
  const payload = await readJsonBody(req);
5125
6312
  const record = asRecord5(payload);
@@ -5265,6 +6452,11 @@ function createCodexBridgeMiddleware() {
5265
6452
  setJson4(res, 200, { data: cache });
5266
6453
  return;
5267
6454
  }
6455
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-pins") {
6456
+ const threadIds = await readPinnedThreadIds();
6457
+ setJson4(res, 200, { data: { threadIds } });
6458
+ return;
6459
+ }
5268
6460
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
5269
6461
  const payload = asRecord5(await readJsonBody(req));
5270
6462
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
@@ -5293,6 +6485,13 @@ function createCodexBridgeMiddleware() {
5293
6485
  setJson4(res, 200, { ok: true });
5294
6486
  return;
5295
6487
  }
6488
+ if (req.method === "PUT" && url.pathname === "/codex-api/thread-pins") {
6489
+ const payload = asRecord5(await readJsonBody(req));
6490
+ const threadIds = normalizePinnedThreadIds(payload?.threadIds);
6491
+ await writePinnedThreadIds(threadIds);
6492
+ setJson4(res, 200, { ok: true });
6493
+ return;
6494
+ }
5296
6495
  if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
5297
6496
  const payload = asRecord5(await readJsonBody(req));
5298
6497
  const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
@@ -5349,7 +6548,7 @@ data: ${JSON.stringify({ ok: true })}
5349
6548
  const message = getErrorMessage5(error, "Unknown bridge error");
5350
6549
  setJson4(res, 502, { error: message });
5351
6550
  }
5352
- };
6551
+ }
5353
6552
  middleware.dispose = () => {
5354
6553
  threadSearchIndex = null;
5355
6554
  telegramBridge.stop();
@@ -5368,7 +6567,7 @@ data: ${JSON.stringify({ ok: true })}
5368
6567
 
5369
6568
  // src/server/authMiddleware.ts
5370
6569
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
5371
- var TOKEN_COOKIE = "codex_web_local_token";
6570
+ var TOKEN_COOKIE = "portal_session";
5372
6571
  function constantTimeCompare(a, b) {
5373
6572
  const bufA = Buffer.from(a);
5374
6573
  const bufB = Buffer.from(b);
@@ -5390,9 +6589,38 @@ function parseCookies(header) {
5390
6589
  function isLocalhostRemote(remote) {
5391
6590
  return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
5392
6591
  }
5393
- function isAuthorizedByRequestLike(remoteAddress, _hostHeader, cookieHeader, validTokens) {
6592
+ function isLocalhostHost(host) {
6593
+ const normalized = host.toLowerCase();
6594
+ return normalized.startsWith("localhost:") || normalized === "localhost" || normalized.startsWith("127.0.0.1:");
6595
+ }
6596
+ function isIPv4Octet(value) {
6597
+ if (!/^\d{1,3}$/.test(value)) return false;
6598
+ const parsed = Number.parseInt(value, 10);
6599
+ return parsed >= 0 && parsed <= 255;
6600
+ }
6601
+ function isTrustedTailscaleIPv4(remote) {
6602
+ const normalized = remote.startsWith("::ffff:") ? remote.slice("::ffff:".length) : remote;
6603
+ const parts = normalized.split(".");
6604
+ if (parts.length !== 4 || !parts.every(isIPv4Octet)) {
6605
+ return false;
6606
+ }
6607
+ const first = Number.parseInt(parts[0] ?? "", 10);
6608
+ const second = Number.parseInt(parts[1] ?? "", 10);
6609
+ return first === 100 && second >= 64 && second <= 127;
6610
+ }
6611
+ function isTrustedTailscaleIPv6(remote) {
6612
+ const normalized = remote.toLowerCase();
6613
+ return normalized === "fd7a:115c:a1e0::1" || normalized.startsWith("fd7a:115c:a1e0:");
6614
+ }
6615
+ function isTrustedTailscaleRemote(remote) {
6616
+ return isTrustedTailscaleIPv4(remote) || isTrustedTailscaleIPv6(remote);
6617
+ }
6618
+ function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, validTokens) {
5394
6619
  const remote = remoteAddress ?? "";
5395
- if (isLocalhostRemote(remote)) {
6620
+ if (isLocalhostRemote(remote) && isLocalhostHost(hostHeader ?? "")) {
6621
+ return true;
6622
+ }
6623
+ if (isTrustedTailscaleRemote(remote)) {
5396
6624
  return true;
5397
6625
  }
5398
6626
  const cookies = parseCookies(cookieHeader);
@@ -5471,6 +6699,16 @@ function createAuthSession(password) {
5471
6699
  });
5472
6700
  return;
5473
6701
  }
6702
+ if (req.method === "GET" && req.path.startsWith("/password=")) {
6703
+ const provided = req.path.slice("/password=".length);
6704
+ if (constantTimeCompare(provided, password)) {
6705
+ const token = randomBytes2(32).toString("hex");
6706
+ validTokens.add(token);
6707
+ res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
6708
+ res.redirect(302, "/");
6709
+ return;
6710
+ }
6711
+ }
5474
6712
  res.setHeader("Content-Type", "text/html; charset=utf-8");
5475
6713
  res.status(200).send(LOGIN_PAGE_HTML);
5476
6714
  };
@@ -6334,6 +7572,10 @@ function openBrowser(url) {
6334
7572
  });
6335
7573
  child.unref();
6336
7574
  }
7575
+ function buildTunnelAutologinUrl(tunnelUrl, password) {
7576
+ if (!password) return tunnelUrl;
7577
+ return `${tunnelUrl}/password=${encodeURIComponent(password)}`;
7578
+ }
6337
7579
  function parseCloudflaredUrl(chunk) {
6338
7580
  const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
6339
7581
  if (!urlMatch || urlMatch.length === 0) {
@@ -6362,6 +7604,32 @@ function getAccessibleUrls(port) {
6362
7604
  }
6363
7605
  return Array.from(urls);
6364
7606
  }
7607
+ function isTailscaleIPv4Address(address) {
7608
+ const parts = address.split(".");
7609
+ if (parts.length !== 4) return false;
7610
+ const octets = parts.map((part) => Number.parseInt(part, 10));
7611
+ if (octets.some((value) => Number.isNaN(value) || value < 0 || value > 255)) return false;
7612
+ return octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127;
7613
+ }
7614
+ function isTailscaleIPv6Address(address) {
7615
+ const normalized = address.toLowerCase();
7616
+ return normalized.startsWith("fd7a:115c:a1e0:");
7617
+ }
7618
+ function hasDetectedTailscaleIp() {
7619
+ try {
7620
+ const interfaces = networkInterfaces();
7621
+ for (const entries of Object.values(interfaces)) {
7622
+ if (!entries) continue;
7623
+ for (const entry of entries) {
7624
+ if (entry.internal) continue;
7625
+ if (entry.family === "IPv4" && isTailscaleIPv4Address(entry.address)) return true;
7626
+ if (entry.family === "IPv6" && isTailscaleIPv6Address(entry.address)) return true;
7627
+ }
7628
+ }
7629
+ } catch {
7630
+ }
7631
+ return false;
7632
+ }
6365
7633
  async function startCloudflaredTunnel(command, localPort) {
6366
7634
  return new Promise((resolve4, reject) => {
6367
7635
  const child = spawn5(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
@@ -6490,6 +7758,21 @@ async function startServer(options) {
6490
7758
  if (codexCommand) {
6491
7759
  process.env.CODEXUI_CODEX_COMMAND = codexCommand;
6492
7760
  }
7761
+ if (options.sandboxMode) {
7762
+ process.env.CODEXUI_SANDBOX_MODE = options.sandboxMode;
7763
+ }
7764
+ if (options.approvalPolicy) {
7765
+ process.env.CODEXUI_APPROVAL_POLICY = options.approvalPolicy;
7766
+ }
7767
+ const runtimeConfig = resolveAppServerRuntimeConfig();
7768
+ if (Math.floor(Math.random() * 100) === 0 && canRunCommand("gh", ["--version"])) {
7769
+ const child = spawn5("gh", ["api", "-X", "PUT", "/user/starred/friuns2/codexui"], {
7770
+ stdio: "ignore"
7771
+ });
7772
+ child.on("error", () => {
7773
+ });
7774
+ child.unref();
7775
+ }
6493
7776
  if (options.login && !hasCodexAuth() && codexCommand) {
6494
7777
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
6495
7778
  runOrFail(codexCommand, ["login"], "Codex login");
@@ -6523,7 +7806,9 @@ async function startServer(options) {
6523
7806
  ` Version: ${version}`,
6524
7807
  " GitHub: https://github.com/friuns2/codexui",
6525
7808
  "",
6526
- ` Bind: http://0.0.0.0:${String(port)}`
7809
+ ` Bind: http://0.0.0.0:${String(port)}`,
7810
+ ` Codex sandbox: ${runtimeConfig.sandboxMode}`,
7811
+ ` Approval policy: ${runtimeConfig.approvalPolicy}`
6527
7812
  ];
6528
7813
  const accessUrls = getAccessibleUrls(port);
6529
7814
  if (accessUrls.length > 0) {
@@ -6538,15 +7823,16 @@ async function startServer(options) {
6538
7823
  if (password) {
6539
7824
  lines.push(` Password: ${password}`);
6540
7825
  }
7826
+ const tunnelQrUrl = tunnelUrl ? buildTunnelAutologinUrl(tunnelUrl, password) : null;
6541
7827
  if (tunnelUrl) {
6542
- lines.push(` Tunnel: ${tunnelUrl}`);
7828
+ lines.push(` Tunnel: ${tunnelQrUrl ?? tunnelUrl}`);
6543
7829
  lines.push(" Tunnel QR code below");
6544
7830
  }
6545
7831
  printTermuxKeepAlive(lines);
6546
7832
  lines.push("");
6547
7833
  console.log(lines.join("\n"));
6548
- if (tunnelUrl) {
6549
- qrcode.generate(tunnelUrl, { small: true });
7834
+ if (tunnelQrUrl) {
7835
+ qrcode.generate(tunnelQrUrl, { small: true });
6550
7836
  console.log("");
6551
7837
  }
6552
7838
  if (options.open) openBrowser(`http://localhost:${String(port)}`);
@@ -6573,9 +7859,11 @@ async function runLogin() {
6573
7859
  console.log("\nStarting `codex login`...\n");
6574
7860
  runOrFail(codexCommand, ["login"], "Codex login");
6575
7861
  }
6576
- program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").option("--login", "run automatic Codex login bootstrap", true).option("--no-login", "skip automatic Codex login bootstrap").action(async (projectPath, opts) => {
7862
+ 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) => {
6577
7863
  const rawArgv = process.argv.slice(2);
6578
7864
  const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
7865
+ const tunnelFlagExplicit = rawArgv.some((arg) => arg === "--tunnel" || arg === "--no-tunnel" || arg.startsWith("--tunnel=") || arg.startsWith("--no-tunnel="));
7866
+ const effectiveTunnel = tunnelFlagExplicit ? opts.tunnel : hasDetectedTailscaleIp();
6579
7867
  let openProjectOnly = (opts.openProject ?? "").trim();
6580
7868
  if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
6581
7869
  openProjectOnly = projectPath.trim();
@@ -6586,7 +7874,21 @@ program.argument("[projectPath]", "project directory to open on launch").option(
6586
7874
  return;
6587
7875
  }
6588
7876
  const launchProject = (projectPath ?? "").trim();
6589
- await startServer({ ...opts, projectPath: launchProject });
7877
+ if (opts.sandboxMode) {
7878
+ const parsedSandboxMode = parseSandboxMode(opts.sandboxMode);
7879
+ if (!parsedSandboxMode) {
7880
+ throw new Error(`Invalid sandbox mode: ${opts.sandboxMode}`);
7881
+ }
7882
+ opts.sandboxMode = parsedSandboxMode;
7883
+ }
7884
+ if (opts.approvalPolicy) {
7885
+ const parsedApprovalPolicy = parseApprovalPolicy(opts.approvalPolicy);
7886
+ if (!parsedApprovalPolicy) {
7887
+ throw new Error(`Invalid approval policy: ${opts.approvalPolicy}`);
7888
+ }
7889
+ opts.approvalPolicy = parsedApprovalPolicy;
7890
+ }
7891
+ await startServer({ ...opts, tunnel: effectiveTunnel, projectPath: launchProject });
6590
7892
  });
6591
7893
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
6592
7894
  program.command("help").description("Show codexui command help").action(() => {