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