codexui-android 0.1.101 → 0.1.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/DirectoryHub-DKVkrxQJ.css +1 -0
- package/dist/assets/DirectoryHub-wFeYwLJR.js +2 -0
- package/dist/assets/{ReviewPane-BJuXPOZb.css → ReviewPane-AzgWKh85.css} +1 -1
- package/dist/assets/{ReviewPane-Ce6QI-Sf.js → ReviewPane-DDBDehxi.js} +1 -1
- package/dist/assets/ThreadConversation-CWqez-yM.js +39 -0
- package/dist/assets/ThreadConversation-DEwOuZ3c.css +1 -0
- package/dist/assets/{ThreadTerminalPanel-DuEjX4F5.js → ThreadTerminalPanel-BEyQ2ISH.js} +12 -12
- package/dist/assets/{ThreadTerminalPanel-BfzQNi0v.css → ThreadTerminalPanel-BcVTRuCH.css} +1 -1
- package/dist/assets/index-CnFxp8la.css +1 -0
- package/dist/assets/index-CvQj_rw7.js +63 -0
- package/dist/assets/{index.esm-TywpZG2x.js → index.esm--s_sgTDk.js} +2 -2
- package/dist/assets/{index.esm-BMAgl1KF.js → index.esm-D_uN0D-X.js} +1 -1
- package/dist/assets/{index.esm-O-ekzJz9.js → index.esm-DvadSfWE.js} +2 -2
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1944 -267
- package/dist-cli/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/fix-pty-native-build.cjs +10 -2
- package/dist/assets/DirectoryHub-B_wdXTQL.css +0 -1
- package/dist/assets/DirectoryHub-Bm5sZZRJ.js +0 -2
- package/dist/assets/ThreadConversation-BQnEV6OL.js +0 -39
- package/dist/assets/ThreadConversation-DdzQ_R3z.css +0 -1
- package/dist/assets/index-B6Gx9sFJ.css +0 -1
- package/dist/assets/index-DzzwK-KG.js +0 -63
package/dist-cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync7, mkdirSync as
|
|
5
|
+
import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
|
|
6
6
|
import { readFile as readFile5, stat as stat7, writeFile as writeFile6 } from "fs/promises";
|
|
7
7
|
import { homedir as homedir7, networkInterfaces } from "os";
|
|
8
8
|
import { isAbsolute as isAbsolute4, join as join10, resolve as resolve3 } from "path";
|
|
@@ -194,12 +194,12 @@ import express from "express";
|
|
|
194
194
|
import { spawn as spawn4, spawnSync as spawnSync4 } from "child_process";
|
|
195
195
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
196
196
|
import { mkdtemp as mkdtemp3, readFile as readFile3, readdir as readdir2, rm as rm4, mkdir as mkdir4, stat as stat4, lstat as lstat2 } from "fs/promises";
|
|
197
|
-
import { createReadStream, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
197
|
+
import { createReadStream, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
198
198
|
import { request as httpRequest2 } from "http";
|
|
199
199
|
import { request as httpsRequest2 } from "https";
|
|
200
200
|
import { homedir as homedir5 } from "os";
|
|
201
201
|
import { tmpdir as tmpdir4 } from "os";
|
|
202
|
-
import { basename as basename4, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "path";
|
|
202
|
+
import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "path";
|
|
203
203
|
import { createInterface } from "readline";
|
|
204
204
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
205
205
|
|
|
@@ -212,7 +212,11 @@ import { join as join2 } from "path";
|
|
|
212
212
|
var ACCOUNT_QUOTA_REFRESH_TTL_MS = 5 * 60 * 1e3;
|
|
213
213
|
var ACCOUNT_QUOTA_LOADING_STALE_MS = 2 * 60 * 1e3;
|
|
214
214
|
var ACCOUNT_INSPECTION_TIMEOUT_MS = 25 * 1e3;
|
|
215
|
+
var LOGIN_URL_TIMEOUT_MS = 15 * 1e3;
|
|
216
|
+
var LOGIN_CALLBACK_TIMEOUT_MS = 20 * 1e3;
|
|
217
|
+
var LOGIN_AUTH_FILE_TIMEOUT_MS = 10 * 1e3;
|
|
215
218
|
var backgroundRefreshPromise = null;
|
|
219
|
+
var activeLogin = null;
|
|
216
220
|
function asRecord(value) {
|
|
217
221
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
218
222
|
}
|
|
@@ -233,6 +237,18 @@ function setJson(res, statusCode, payload) {
|
|
|
233
237
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
234
238
|
res.end(JSON.stringify(payload));
|
|
235
239
|
}
|
|
240
|
+
async function readJsonBody(req) {
|
|
241
|
+
const rawBody = await new Promise((resolve4, reject) => {
|
|
242
|
+
let body = "";
|
|
243
|
+
req.setEncoding("utf8");
|
|
244
|
+
req.on("data", (chunk) => {
|
|
245
|
+
body += chunk;
|
|
246
|
+
});
|
|
247
|
+
req.on("end", () => resolve4(body));
|
|
248
|
+
req.on("error", reject);
|
|
249
|
+
});
|
|
250
|
+
return asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
|
|
251
|
+
}
|
|
236
252
|
function getErrorMessage(payload, fallback) {
|
|
237
253
|
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
238
254
|
return payload.message;
|
|
@@ -734,6 +750,134 @@ async function importAccountFromAuthPath(path) {
|
|
|
734
750
|
accounts: sortAccounts(nextState.accounts, nextState.activeAccountId).map((entry) => toPublicAccountEntry(entry, nextState.activeAccountId))
|
|
735
751
|
};
|
|
736
752
|
}
|
|
753
|
+
function extractLoginUrl(output) {
|
|
754
|
+
const match = output.match(/https:\/\/auth\.openai\.com\/oauth\/authorize\?\S+/u);
|
|
755
|
+
return match?.[0] ?? null;
|
|
756
|
+
}
|
|
757
|
+
function isLocalCallbackUrl(rawUrl) {
|
|
758
|
+
try {
|
|
759
|
+
const parsed = new URL(rawUrl);
|
|
760
|
+
if (parsed.protocol !== "http:") return false;
|
|
761
|
+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
|
|
762
|
+
} catch {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async function waitForLoginUrl() {
|
|
767
|
+
if (activeLogin?.loginUrl) return activeLogin.loginUrl;
|
|
768
|
+
return await new Promise((resolve4, reject) => {
|
|
769
|
+
const startedAt = Date.now();
|
|
770
|
+
const timer = setInterval(() => {
|
|
771
|
+
if (!activeLogin) {
|
|
772
|
+
clearInterval(timer);
|
|
773
|
+
reject(new Error("Login process is not running."));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (activeLogin.loginUrl) {
|
|
777
|
+
clearInterval(timer);
|
|
778
|
+
resolve4(activeLogin.loginUrl);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (activeLogin.exited) {
|
|
782
|
+
clearInterval(timer);
|
|
783
|
+
reject(new Error(activeLogin.output.trim() || "codex login exited before returning a login URL."));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (Date.now() - startedAt > LOGIN_URL_TIMEOUT_MS) {
|
|
787
|
+
clearInterval(timer);
|
|
788
|
+
reject(new Error("Timed out waiting for codex login URL."));
|
|
789
|
+
}
|
|
790
|
+
}, 100);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async function startCodexLogin() {
|
|
794
|
+
if (activeLogin && !activeLogin.exited) {
|
|
795
|
+
return await waitForLoginUrl();
|
|
796
|
+
}
|
|
797
|
+
const proc = spawn("codex", ["login"], {
|
|
798
|
+
env: process.env,
|
|
799
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
800
|
+
});
|
|
801
|
+
proc.stdin.end();
|
|
802
|
+
activeLogin = {
|
|
803
|
+
proc,
|
|
804
|
+
loginUrl: null,
|
|
805
|
+
output: "",
|
|
806
|
+
exited: false,
|
|
807
|
+
exitCode: null,
|
|
808
|
+
exitSignal: null,
|
|
809
|
+
exitPromise: new Promise((resolve4) => {
|
|
810
|
+
proc.once("exit", (code, signal) => {
|
|
811
|
+
if (activeLogin?.proc === proc) {
|
|
812
|
+
activeLogin.exited = true;
|
|
813
|
+
activeLogin.exitCode = code;
|
|
814
|
+
activeLogin.exitSignal = signal;
|
|
815
|
+
}
|
|
816
|
+
resolve4();
|
|
817
|
+
});
|
|
818
|
+
})
|
|
819
|
+
};
|
|
820
|
+
const appendOutput = (chunk) => {
|
|
821
|
+
if (!activeLogin || activeLogin.proc !== proc) return;
|
|
822
|
+
activeLogin.output += chunk.toString();
|
|
823
|
+
activeLogin.loginUrl = activeLogin.loginUrl ?? extractLoginUrl(activeLogin.output);
|
|
824
|
+
};
|
|
825
|
+
proc.stdout.on("data", appendOutput);
|
|
826
|
+
proc.stderr.on("data", appendOutput);
|
|
827
|
+
proc.once("error", (error) => {
|
|
828
|
+
if (!activeLogin || activeLogin.proc !== proc) return;
|
|
829
|
+
activeLogin.exited = true;
|
|
830
|
+
activeLogin.output += error.message;
|
|
831
|
+
});
|
|
832
|
+
try {
|
|
833
|
+
return await waitForLoginUrl();
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (activeLogin?.proc === proc && !activeLogin.exited) {
|
|
836
|
+
proc.kill("SIGTERM");
|
|
837
|
+
}
|
|
838
|
+
activeLogin = null;
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function curlLoginCallback(callbackUrl) {
|
|
843
|
+
const controller = new AbortController();
|
|
844
|
+
const timer = setTimeout(() => controller.abort(), LOGIN_CALLBACK_TIMEOUT_MS);
|
|
845
|
+
try {
|
|
846
|
+
const response = await fetch(callbackUrl, {
|
|
847
|
+
redirect: "manual",
|
|
848
|
+
signal: controller.signal
|
|
849
|
+
});
|
|
850
|
+
if (response.status >= 400) {
|
|
851
|
+
throw new Error(`Login callback returned HTTP ${response.status}.`);
|
|
852
|
+
}
|
|
853
|
+
} finally {
|
|
854
|
+
clearTimeout(timer);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function getActiveAuthMtimeMs() {
|
|
858
|
+
try {
|
|
859
|
+
return (await stat(getActiveAuthPath())).mtimeMs;
|
|
860
|
+
} catch {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function waitForAuthFileUpdate(previousMtimeMs) {
|
|
865
|
+
const startedAt = Date.now();
|
|
866
|
+
while (Date.now() - startedAt <= LOGIN_AUTH_FILE_TIMEOUT_MS) {
|
|
867
|
+
const nextMtimeMs = await getActiveAuthMtimeMs();
|
|
868
|
+
if (nextMtimeMs !== null && (previousMtimeMs === null || nextMtimeMs > previousMtimeMs)) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function stopActiveLogin() {
|
|
875
|
+
if (!activeLogin) return;
|
|
876
|
+
if (!activeLogin.exited) {
|
|
877
|
+
activeLogin.proc.kill("SIGTERM");
|
|
878
|
+
}
|
|
879
|
+
activeLogin = null;
|
|
880
|
+
}
|
|
737
881
|
async function handleAccountRoutes(req, res, url, context) {
|
|
738
882
|
const { appServer } = context;
|
|
739
883
|
if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
|
|
@@ -813,6 +957,92 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
813
957
|
}
|
|
814
958
|
return true;
|
|
815
959
|
}
|
|
960
|
+
if (req.method === "POST" && url.pathname === "/codex-api/accounts/login/start") {
|
|
961
|
+
try {
|
|
962
|
+
const loginUrl = await startCodexLogin();
|
|
963
|
+
setJson(res, 200, {
|
|
964
|
+
ok: true,
|
|
965
|
+
data: {
|
|
966
|
+
loginUrl
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
} catch (error) {
|
|
970
|
+
setJson(res, 500, {
|
|
971
|
+
error: "account_login_start_failed",
|
|
972
|
+
message: getErrorMessage(error, "Failed to start codex login")
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
if (req.method === "POST" && url.pathname === "/codex-api/accounts/login/complete") {
|
|
978
|
+
try {
|
|
979
|
+
const payload = await readJsonBody(req);
|
|
980
|
+
const callbackUrl = typeof payload?.callbackUrl === "string" ? payload.callbackUrl.trim() : "";
|
|
981
|
+
if (!callbackUrl) {
|
|
982
|
+
setJson(res, 400, { error: "missing_callback_url", message: "Paste the localhost callback URL from the browser." });
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
if (!isLocalCallbackUrl(callbackUrl)) {
|
|
986
|
+
setJson(res, 400, { error: "invalid_callback_url", message: "The callback URL must use http://localhost or http://127.0.0.1." });
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
if (!activeLogin || activeLogin.exited) {
|
|
990
|
+
setJson(res, 409, { error: "login_not_running", message: "Start Codex login before submitting the callback URL." });
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
const previousAuthMtimeMs = await getActiveAuthMtimeMs();
|
|
994
|
+
await curlLoginCallback(callbackUrl);
|
|
995
|
+
await waitForAuthFileUpdate(previousAuthMtimeMs);
|
|
996
|
+
const imported = await importAccountFromAuthPath(getActiveAuthPath());
|
|
997
|
+
stopActiveLogin();
|
|
998
|
+
appServer.dispose();
|
|
999
|
+
const inspection = await validateSwitchedAccount(appServer);
|
|
1000
|
+
const state = await readStoredAccountsState();
|
|
1001
|
+
const importedAccountId = imported.importedAccountId;
|
|
1002
|
+
const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
|
|
1003
|
+
if (!target) {
|
|
1004
|
+
throw new Error("account_not_found");
|
|
1005
|
+
}
|
|
1006
|
+
const nextEntry = {
|
|
1007
|
+
...target,
|
|
1008
|
+
email: inspection.metadata.email ?? target.email,
|
|
1009
|
+
planType: inspection.metadata.planType ?? target.planType,
|
|
1010
|
+
lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1011
|
+
quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
|
|
1012
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1013
|
+
quotaStatus: "ready",
|
|
1014
|
+
quotaError: null,
|
|
1015
|
+
unavailableReason: null
|
|
1016
|
+
};
|
|
1017
|
+
const nextState = withUpsertedAccount({
|
|
1018
|
+
activeAccountId: importedAccountId,
|
|
1019
|
+
accounts: state.accounts
|
|
1020
|
+
}, nextEntry);
|
|
1021
|
+
await writeStoredAccountsState({
|
|
1022
|
+
activeAccountId: importedAccountId,
|
|
1023
|
+
accounts: nextState.accounts
|
|
1024
|
+
});
|
|
1025
|
+
const backgroundState = await scheduleAccountsBackgroundRefresh({
|
|
1026
|
+
force: true,
|
|
1027
|
+
prioritizeAccountId: importedAccountId,
|
|
1028
|
+
accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
|
|
1029
|
+
});
|
|
1030
|
+
setJson(res, 200, {
|
|
1031
|
+
ok: true,
|
|
1032
|
+
data: {
|
|
1033
|
+
activeAccountId: importedAccountId,
|
|
1034
|
+
importedAccountId,
|
|
1035
|
+
accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
setJson(res, 500, {
|
|
1040
|
+
error: "account_login_complete_failed",
|
|
1041
|
+
message: getErrorMessage(error, "Failed to complete Codex login")
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
816
1046
|
if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
|
|
817
1047
|
try {
|
|
818
1048
|
if (appServer.listPendingServerRequests().length > 0) {
|
|
@@ -822,16 +1052,7 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
822
1052
|
});
|
|
823
1053
|
return true;
|
|
824
1054
|
}
|
|
825
|
-
const
|
|
826
|
-
let body = "";
|
|
827
|
-
req.setEncoding("utf8");
|
|
828
|
-
req.on("data", (chunk) => {
|
|
829
|
-
body += chunk;
|
|
830
|
-
});
|
|
831
|
-
req.on("end", () => resolve4(body));
|
|
832
|
-
req.on("error", reject);
|
|
833
|
-
});
|
|
834
|
-
const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
|
|
1055
|
+
const payload = await readJsonBody(req);
|
|
835
1056
|
const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
|
|
836
1057
|
if (!accountId) {
|
|
837
1058
|
setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
|
|
@@ -915,16 +1136,7 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
915
1136
|
}
|
|
916
1137
|
if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
|
|
917
1138
|
try {
|
|
918
|
-
const
|
|
919
|
-
let body = "";
|
|
920
|
-
req.setEncoding("utf8");
|
|
921
|
-
req.on("data", (chunk) => {
|
|
922
|
-
body += chunk;
|
|
923
|
-
});
|
|
924
|
-
req.on("end", () => resolve4(body));
|
|
925
|
-
req.on("error", reject);
|
|
926
|
-
});
|
|
927
|
-
const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
|
|
1139
|
+
const payload = await readJsonBody(req);
|
|
928
1140
|
const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
|
|
929
1141
|
if (!accountId) {
|
|
930
1142
|
setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
|
|
@@ -1721,6 +1933,47 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
1721
1933
|
import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
|
|
1722
1934
|
import { join as join4 } from "path";
|
|
1723
1935
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
1936
|
+
|
|
1937
|
+
// src/utils/commandInvocation.ts
|
|
1938
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1939
|
+
import { basename, extname } from "path";
|
|
1940
|
+
var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
|
|
1941
|
+
function quoteCmdExeArg(value) {
|
|
1942
|
+
const normalized = value.replace(/"/g, '""');
|
|
1943
|
+
if (!/[\s"]/u.test(normalized)) {
|
|
1944
|
+
return normalized;
|
|
1945
|
+
}
|
|
1946
|
+
return `"${normalized}"`;
|
|
1947
|
+
}
|
|
1948
|
+
function needsCmdExeWrapper(command) {
|
|
1949
|
+
if (process.platform !== "win32") {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
const lowerCommand = command.toLowerCase();
|
|
1953
|
+
const baseName = basename(lowerCommand);
|
|
1954
|
+
if (/\.(cmd|bat)$/i.test(baseName)) {
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
if (extname(baseName)) {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
return WINDOWS_CMD_NAMES.has(baseName);
|
|
1961
|
+
}
|
|
1962
|
+
function getSpawnInvocation(command, args = []) {
|
|
1963
|
+
if (needsCmdExeWrapper(command)) {
|
|
1964
|
+
return {
|
|
1965
|
+
command: "cmd.exe",
|
|
1966
|
+
args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
return { command, args };
|
|
1970
|
+
}
|
|
1971
|
+
function spawnSyncCommand(command, args = [], options = {}) {
|
|
1972
|
+
const invocation = getSpawnInvocation(command, args);
|
|
1973
|
+
return spawnSync2(invocation.command, invocation.args, options);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/server/skillsRoutes.ts
|
|
1724
1977
|
function asRecord3(value) {
|
|
1725
1978
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1726
1979
|
}
|
|
@@ -1795,10 +2048,13 @@ function getSkillsInstallDir() {
|
|
|
1795
2048
|
return join4(getCodexHomeDir2(), "skills");
|
|
1796
2049
|
}
|
|
1797
2050
|
var DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
|
|
2051
|
+
var SKILL_SEARCH_METADATA_LIMIT = 20;
|
|
2052
|
+
var SKILL_SEARCH_METADATA_CONCURRENCY = 4;
|
|
1798
2053
|
async function runCommand2(command, args, options = {}) {
|
|
1799
2054
|
const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
1800
2055
|
await new Promise((resolve4, reject) => {
|
|
1801
|
-
const
|
|
2056
|
+
const invocation = getSpawnInvocation(command, args);
|
|
2057
|
+
const proc = spawn3(invocation.command, invocation.args, {
|
|
1802
2058
|
cwd: options.cwd,
|
|
1803
2059
|
env: process.env,
|
|
1804
2060
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1841,7 +2097,8 @@ async function runCommand2(command, args, options = {}) {
|
|
|
1841
2097
|
async function runCommandWithOutput(command, args, options = {}) {
|
|
1842
2098
|
const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
1843
2099
|
return await new Promise((resolve4, reject) => {
|
|
1844
|
-
const
|
|
2100
|
+
const invocation = getSpawnInvocation(command, args);
|
|
2101
|
+
const proc = spawn3(invocation.command, invocation.args, {
|
|
1845
2102
|
cwd: options.cwd,
|
|
1846
2103
|
env: process.env,
|
|
1847
2104
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1911,6 +2168,17 @@ async function detectUserSkillsDir(appServer) {
|
|
|
1911
2168
|
}
|
|
1912
2169
|
return getSkillsInstallDir();
|
|
1913
2170
|
}
|
|
2171
|
+
async function ensureInstalledSkillIsValid(appServer, skillPath) {
|
|
2172
|
+
const result = await appServer.rpc("skills/list", { forceReload: true });
|
|
2173
|
+
const normalized = skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
|
|
2174
|
+
for (const entry of result.data ?? []) {
|
|
2175
|
+
for (const error of entry.errors ?? []) {
|
|
2176
|
+
if (error.path === normalized) {
|
|
2177
|
+
throw new Error(error.message || "Installed skill is invalid");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
1914
2182
|
async function runGitFetchWithRefLockRetry(repoDir, args = ["fetch", "origin"]) {
|
|
1915
2183
|
try {
|
|
1916
2184
|
await runCommand2("git", args, { cwd: repoDir });
|
|
@@ -1927,11 +2195,18 @@ async function runGitFetchWithRefLockRetry(repoDir, args = ["fetch", "origin"])
|
|
|
1927
2195
|
await runCommand2("git", args, { cwd: repoDir });
|
|
1928
2196
|
}
|
|
1929
2197
|
}
|
|
1930
|
-
function buildLocalHubEntry(info) {
|
|
2198
|
+
async function buildLocalHubEntry(info) {
|
|
2199
|
+
let description = "";
|
|
2200
|
+
if (info.path) {
|
|
2201
|
+
try {
|
|
2202
|
+
description = extractSkillDescriptionFromMarkdown(await readFile2(info.path, "utf8"));
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
1931
2206
|
return {
|
|
1932
2207
|
name: info.name,
|
|
1933
2208
|
owner: "local",
|
|
1934
|
-
description
|
|
2209
|
+
description,
|
|
1935
2210
|
displayName: "",
|
|
1936
2211
|
publishedAt: 0,
|
|
1937
2212
|
avatarUrl: "",
|
|
@@ -1941,6 +2216,143 @@ function buildLocalHubEntry(info) {
|
|
|
1941
2216
|
enabled: info.enabled
|
|
1942
2217
|
};
|
|
1943
2218
|
}
|
|
2219
|
+
function stripAnsi(value) {
|
|
2220
|
+
return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/gu, "");
|
|
2221
|
+
}
|
|
2222
|
+
function parseNpxSkillsFindOutput(output, installedMap) {
|
|
2223
|
+
const lines = stripAnsi(output).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
|
|
2224
|
+
const results = [];
|
|
2225
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2226
|
+
const line = lines[index] ?? "";
|
|
2227
|
+
const match = line.match(/^(.+?@[^@\s]+)\s+([\d.]+[KMB]?)\s+installs$/iu);
|
|
2228
|
+
if (!match) continue;
|
|
2229
|
+
const source = match[1]?.trim() ?? "";
|
|
2230
|
+
const installs = match[2]?.trim() ?? "";
|
|
2231
|
+
const atIndex = source.lastIndexOf("@");
|
|
2232
|
+
if (atIndex <= 0 || atIndex >= source.length - 1) continue;
|
|
2233
|
+
const owner = source.slice(0, atIndex);
|
|
2234
|
+
const name = source.slice(atIndex + 1);
|
|
2235
|
+
let url = "";
|
|
2236
|
+
const next = lines[index + 1] ?? "";
|
|
2237
|
+
const urlMatch = next.match(/(?:^└\s*)?(https?:\/\/\S+)$/u);
|
|
2238
|
+
if (urlMatch?.[1]) {
|
|
2239
|
+
url = urlMatch[1];
|
|
2240
|
+
index += 1;
|
|
2241
|
+
}
|
|
2242
|
+
const installedInfo = installedMap.get(name);
|
|
2243
|
+
results.push({
|
|
2244
|
+
name,
|
|
2245
|
+
owner,
|
|
2246
|
+
displayName: name,
|
|
2247
|
+
description: installs ? `${installs} installs` : "",
|
|
2248
|
+
installCountLabel: installs ? `${installs} installs` : "",
|
|
2249
|
+
publishedAt: 0,
|
|
2250
|
+
avatarUrl: "",
|
|
2251
|
+
url,
|
|
2252
|
+
installed: Boolean(installedInfo),
|
|
2253
|
+
source,
|
|
2254
|
+
path: installedInfo?.path,
|
|
2255
|
+
enabled: installedInfo?.enabled
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
return results;
|
|
2259
|
+
}
|
|
2260
|
+
function parseGithubSkillSource(source) {
|
|
2261
|
+
const atIndex = source.lastIndexOf("@");
|
|
2262
|
+
if (atIndex <= 0 || atIndex >= source.length - 1) return null;
|
|
2263
|
+
const ownerRepo = source.slice(0, atIndex).trim();
|
|
2264
|
+
const skillName = source.slice(atIndex + 1).trim();
|
|
2265
|
+
const ownerRepoParts = ownerRepo.split("/").filter(Boolean);
|
|
2266
|
+
if (ownerRepoParts.length !== 2 || skillName.length === 0) return null;
|
|
2267
|
+
if (ownerRepoParts.some((part) => part.includes(":") || part.includes(" "))) return null;
|
|
2268
|
+
return { ownerRepo, skillName };
|
|
2269
|
+
}
|
|
2270
|
+
function getGithubOwnerAvatarUrl(source) {
|
|
2271
|
+
const parsed = parseGithubSkillSource(source);
|
|
2272
|
+
if (!parsed) return "";
|
|
2273
|
+
const owner = parsed.ownerRepo.split("/")[0] ?? "";
|
|
2274
|
+
return owner ? `https://github.com/${encodeURIComponent(owner)}.png?size=64` : "";
|
|
2275
|
+
}
|
|
2276
|
+
function buildGithubSkillRawCandidates(source) {
|
|
2277
|
+
const parsed = parseGithubSkillSource(source);
|
|
2278
|
+
if (!parsed) return [];
|
|
2279
|
+
const ownerRepo = parsed.ownerRepo.split("/").map(encodeURIComponent).join("/");
|
|
2280
|
+
const skillName = encodeURIComponent(parsed.skillName);
|
|
2281
|
+
const branches = ["main", "master"];
|
|
2282
|
+
const paths = [
|
|
2283
|
+
`skills/${skillName}/SKILL.md`,
|
|
2284
|
+
`${skillName}/SKILL.md`,
|
|
2285
|
+
"SKILL.md"
|
|
2286
|
+
];
|
|
2287
|
+
return branches.flatMap((branch) => paths.map((path) => `https://raw.githubusercontent.com/${ownerRepo}/${branch}/${path}`));
|
|
2288
|
+
}
|
|
2289
|
+
async function fetchTextWithTimeout(url, timeoutMs) {
|
|
2290
|
+
const controller = new AbortController();
|
|
2291
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2292
|
+
try {
|
|
2293
|
+
const resp = await fetch(url, {
|
|
2294
|
+
headers: { "User-Agent": "codex-web-local" },
|
|
2295
|
+
signal: controller.signal
|
|
2296
|
+
});
|
|
2297
|
+
if (!resp.ok) return "";
|
|
2298
|
+
return await resp.text();
|
|
2299
|
+
} finally {
|
|
2300
|
+
clearTimeout(timeout);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function resolveSkillIconUrl(icon, markdownUrl) {
|
|
2304
|
+
const value = icon.trim().replace(/^['"]|['"]$/gu, "");
|
|
2305
|
+
if (!value) return "";
|
|
2306
|
+
if (/^https?:\/\//iu.test(value)) return value;
|
|
2307
|
+
try {
|
|
2308
|
+
return new URL(value, markdownUrl).toString();
|
|
2309
|
+
} catch {
|
|
2310
|
+
return "";
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
async function fetchGithubSkillMetadata(source) {
|
|
2314
|
+
for (const candidate of buildGithubSkillRawCandidates(source)) {
|
|
2315
|
+
try {
|
|
2316
|
+
const markdown = await fetchTextWithTimeout(candidate, 4e3);
|
|
2317
|
+
if (!markdown) continue;
|
|
2318
|
+
const description = extractSkillDescriptionFromMarkdown(markdown);
|
|
2319
|
+
const icon = extractSkillFrontmatterField(markdown, "icon");
|
|
2320
|
+
const avatarUrl = icon ? resolveSkillIconUrl(icon, candidate) : getGithubOwnerAvatarUrl(source);
|
|
2321
|
+
if (description || avatarUrl) return { description, avatarUrl };
|
|
2322
|
+
} catch {
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return { avatarUrl: getGithubOwnerAvatarUrl(source) };
|
|
2326
|
+
}
|
|
2327
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
2328
|
+
const results = new Array(items.length);
|
|
2329
|
+
let nextIndex = 0;
|
|
2330
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
2331
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
2332
|
+
while (nextIndex < items.length) {
|
|
2333
|
+
const index = nextIndex;
|
|
2334
|
+
nextIndex += 1;
|
|
2335
|
+
results[index] = await mapper(items[index], index);
|
|
2336
|
+
}
|
|
2337
|
+
}));
|
|
2338
|
+
return results;
|
|
2339
|
+
}
|
|
2340
|
+
async function enrichSkillSearchDescriptions(results) {
|
|
2341
|
+
const enrichedHead = await mapWithConcurrency(
|
|
2342
|
+
results.slice(0, SKILL_SEARCH_METADATA_LIMIT),
|
|
2343
|
+
SKILL_SEARCH_METADATA_CONCURRENCY,
|
|
2344
|
+
async (result) => {
|
|
2345
|
+
if (!result.source) return result;
|
|
2346
|
+
const metadata = await fetchGithubSkillMetadata(result.source);
|
|
2347
|
+
return {
|
|
2348
|
+
...result,
|
|
2349
|
+
description: metadata.description || result.description,
|
|
2350
|
+
avatarUrl: metadata.avatarUrl || result.avatarUrl
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
);
|
|
2354
|
+
return [...enrichedHead, ...results.slice(SKILL_SEARCH_METADATA_LIMIT)];
|
|
2355
|
+
}
|
|
1944
2356
|
function groupRpcSkillRecords(skills) {
|
|
1945
2357
|
const normalizedPathSet = new Set(
|
|
1946
2358
|
skills.map((skill) => normalizeSkillMarkdownPath(typeof skill.path === "string" ? skill.path : "")).filter(Boolean)
|
|
@@ -2027,7 +2439,40 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
2027
2439
|
}
|
|
2028
2440
|
return map;
|
|
2029
2441
|
}
|
|
2442
|
+
async function collectInstalledSkillsMap(appServer) {
|
|
2443
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
2444
|
+
try {
|
|
2445
|
+
const result = await appServer.rpc("skills/list", {});
|
|
2446
|
+
for (const entry of result.data ?? []) {
|
|
2447
|
+
for (const skill of groupRpcSkillRecords(entry.skills ?? [])) {
|
|
2448
|
+
if (skill.name) {
|
|
2449
|
+
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
} catch {
|
|
2454
|
+
}
|
|
2455
|
+
return installedMap;
|
|
2456
|
+
}
|
|
2457
|
+
function extractSkillFrontmatterField(markdown, fieldName) {
|
|
2458
|
+
const lines = markdown.split(/\r?\n/);
|
|
2459
|
+
if (lines[0]?.trim() !== "---") return "";
|
|
2460
|
+
const frontmatter = [];
|
|
2461
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
2462
|
+
const line = lines[index] ?? "";
|
|
2463
|
+
if (line.trim() === "---") break;
|
|
2464
|
+
frontmatter.push(line);
|
|
2465
|
+
}
|
|
2466
|
+
const escapedFieldName = fieldName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
2467
|
+
const fieldPattern = new RegExp(`^${escapedFieldName}\\s*:`, "iu");
|
|
2468
|
+
const valuePattern = new RegExp(`^${escapedFieldName}\\s*:\\s*`, "iu");
|
|
2469
|
+
const fieldLine = frontmatter.find((line) => fieldPattern.test(line.trim()));
|
|
2470
|
+
if (!fieldLine) return "";
|
|
2471
|
+
return fieldLine.replace(valuePattern, "").replace(/^['"]|['"]$/gu, "").trim();
|
|
2472
|
+
}
|
|
2030
2473
|
function extractSkillDescriptionFromMarkdown(markdown) {
|
|
2474
|
+
const frontmatterDescription = extractSkillFrontmatterField(markdown, "description");
|
|
2475
|
+
if (frontmatterDescription) return frontmatterDescription;
|
|
2031
2476
|
const lines = markdown.split(/\r?\n/);
|
|
2032
2477
|
let inCodeFence = false;
|
|
2033
2478
|
for (const rawLine of lines) {
|
|
@@ -2229,6 +2674,7 @@ async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
|
2229
2674
|
async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
|
|
2230
2675
|
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
2231
2676
|
let sha = "";
|
|
2677
|
+
const nextContent = JSON.stringify(skills, null, 2);
|
|
2232
2678
|
const existing = await fetch(url, {
|
|
2233
2679
|
headers: {
|
|
2234
2680
|
Accept: "application/vnd.github+json",
|
|
@@ -2240,13 +2686,16 @@ async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
|
|
|
2240
2686
|
if (existing.ok) {
|
|
2241
2687
|
const payload = await existing.json();
|
|
2242
2688
|
sha = payload.sha ?? "";
|
|
2689
|
+
const currentContent = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "";
|
|
2690
|
+
if (currentContent === nextContent) return false;
|
|
2243
2691
|
}
|
|
2244
|
-
const content = Buffer.from(
|
|
2692
|
+
const content = Buffer.from(nextContent, "utf8").toString("base64");
|
|
2245
2693
|
await getGithubJson(url, token, "PUT", {
|
|
2246
2694
|
message: "Update synced skills manifest",
|
|
2247
2695
|
content,
|
|
2248
2696
|
...sha ? { sha } : {}
|
|
2249
2697
|
});
|
|
2698
|
+
return true;
|
|
2250
2699
|
}
|
|
2251
2700
|
function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
2252
2701
|
return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
|
|
@@ -2417,6 +2866,16 @@ async function hasLocalUncommittedChanges(repoDir) {
|
|
|
2417
2866
|
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
2418
2867
|
return status.length > 0;
|
|
2419
2868
|
}
|
|
2869
|
+
async function hasCommittableWorkingTreeChanges(repoDir) {
|
|
2870
|
+
try {
|
|
2871
|
+
await runCommand2("git", ["diff", "--quiet", "--exit-code", "--ignore-submodules=dirty"], { cwd: repoDir });
|
|
2872
|
+
await runCommand2("git", ["diff", "--cached", "--quiet", "--exit-code", "--ignore-submodules=dirty"], { cwd: repoDir });
|
|
2873
|
+
} catch {
|
|
2874
|
+
return true;
|
|
2875
|
+
}
|
|
2876
|
+
const untracked = (await runCommandWithOutput("git", ["ls-files", "--others", "--exclude-standard"], { cwd: repoDir })).trim();
|
|
2877
|
+
return untracked.length > 0;
|
|
2878
|
+
}
|
|
2420
2879
|
async function walkFileMtimes(rootDir, currentDir, out) {
|
|
2421
2880
|
let entries;
|
|
2422
2881
|
try {
|
|
@@ -2525,8 +2984,11 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
|
|
|
2525
2984
|
await runCommand2("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
2526
2985
|
await restoreProtectedFilesFromOrigin(repoDir, branch);
|
|
2527
2986
|
await runCommand2("git", ["add", "."], { cwd: repoDir });
|
|
2528
|
-
|
|
2529
|
-
|
|
2987
|
+
try {
|
|
2988
|
+
await runCommand2("git", ["diff", "--cached", "--quiet", "--exit-code"], { cwd: repoDir });
|
|
2989
|
+
return;
|
|
2990
|
+
} catch {
|
|
2991
|
+
}
|
|
2530
2992
|
await runCommand2("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
2531
2993
|
await pushWithNonFastForwardRetry(repoDir, branch);
|
|
2532
2994
|
}
|
|
@@ -2572,8 +3034,8 @@ async function autoPushSyncedSkills(appServer) {
|
|
|
2572
3034
|
await runCommand2("git", ["fetch", "origin", PRIVATE_SYNC_BRANCH], { cwd: repoDir });
|
|
2573
3035
|
const head = (await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: repoDir })).trim();
|
|
2574
3036
|
const originHead = (await runCommandWithOutput("git", ["rev-parse", `origin/${PRIVATE_SYNC_BRANCH}`], { cwd: repoDir })).trim();
|
|
2575
|
-
const
|
|
2576
|
-
if (!
|
|
3037
|
+
const hasCommittableChanges = await hasCommittableWorkingTreeChanges(repoDir);
|
|
3038
|
+
if (!hasCommittableChanges && head === originHead) return;
|
|
2577
3039
|
const local = await collectLocalSyncedSkills(appServer);
|
|
2578
3040
|
const installedMap = await scanInstalledSkillsFromDisk();
|
|
2579
3041
|
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
@@ -2689,25 +3151,11 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
|
2689
3151
|
await autoPushSyncedSkills(appServer);
|
|
2690
3152
|
}
|
|
2691
3153
|
async function handleSkillsRoutes(req, res, url, context) {
|
|
2692
|
-
const { appServer, readJsonBody:
|
|
3154
|
+
const { appServer, readJsonBody: readJsonBody3 } = context;
|
|
2693
3155
|
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
2694
3156
|
try {
|
|
2695
|
-
const installedMap = await
|
|
2696
|
-
|
|
2697
|
-
const result = await appServer.rpc("skills/list", {});
|
|
2698
|
-
for (const entry of result.data ?? []) {
|
|
2699
|
-
for (const skill of groupRpcSkillRecords(entry.skills ?? [])) {
|
|
2700
|
-
if (skill.name) {
|
|
2701
|
-
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
} catch {
|
|
2706
|
-
}
|
|
2707
|
-
const installed = [];
|
|
2708
|
-
for (const [, info] of installedMap) {
|
|
2709
|
-
installed.push(buildLocalHubEntry(info));
|
|
2710
|
-
}
|
|
3157
|
+
const installedMap = await collectInstalledSkillsMap(appServer);
|
|
3158
|
+
const installed = await Promise.all([...installedMap.values()].map((info) => buildLocalHubEntry(info)));
|
|
2711
3159
|
installed.sort((a, b) => a.name.localeCompare(b.name));
|
|
2712
3160
|
setJson3(res, 200, { installed });
|
|
2713
3161
|
} catch (error) {
|
|
@@ -2715,6 +3163,22 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
2715
3163
|
}
|
|
2716
3164
|
return true;
|
|
2717
3165
|
}
|
|
3166
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/search") {
|
|
3167
|
+
try {
|
|
3168
|
+
const query = (url.searchParams.get("q") || "").trim();
|
|
3169
|
+
if (query.length < 2) {
|
|
3170
|
+
setJson3(res, 200, { results: [] });
|
|
3171
|
+
return true;
|
|
3172
|
+
}
|
|
3173
|
+
const installedMap = await collectInstalledSkillsMap(appServer);
|
|
3174
|
+
const output = await runCommandWithOutput("npx", ["--yes", "skills", "find", query], { timeoutMs: 6e4 });
|
|
3175
|
+
const results = await enrichSkillSearchDescriptions(parseNpxSkillsFindOutput(output, installedMap));
|
|
3176
|
+
setJson3(res, 200, { results });
|
|
3177
|
+
} catch (error) {
|
|
3178
|
+
setJson3(res, 502, { error: getErrorMessage3(error, "Failed to search skills") });
|
|
3179
|
+
}
|
|
3180
|
+
return true;
|
|
3181
|
+
}
|
|
2718
3182
|
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
2719
3183
|
const state = await readSkillsSyncState();
|
|
2720
3184
|
setJson3(res, 200, {
|
|
@@ -2755,7 +3219,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
2755
3219
|
}
|
|
2756
3220
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
2757
3221
|
try {
|
|
2758
|
-
const payload = asRecord3(await
|
|
3222
|
+
const payload = asRecord3(await readJsonBody3(req));
|
|
2759
3223
|
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
2760
3224
|
if (!token) {
|
|
2761
3225
|
setJson3(res, 400, { error: "Missing GitHub token" });
|
|
@@ -2787,7 +3251,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
2787
3251
|
}
|
|
2788
3252
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
2789
3253
|
try {
|
|
2790
|
-
const payload = asRecord3(await
|
|
3254
|
+
const payload = asRecord3(await readJsonBody3(req));
|
|
2791
3255
|
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
2792
3256
|
if (!deviceCode) {
|
|
2793
3257
|
setJson3(res, 400, { error: "Missing deviceCode" });
|
|
@@ -2819,7 +3283,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
2819
3283
|
return true;
|
|
2820
3284
|
}
|
|
2821
3285
|
const local = await collectLocalSyncedSkills(appServer);
|
|
2822
|
-
const installedMap = await
|
|
3286
|
+
const installedMap = await collectInstalledSkillsMap(appServer);
|
|
2823
3287
|
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
2824
3288
|
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
2825
3289
|
setJson3(res, 200, { ok: true, data: { synced: local.length } });
|
|
@@ -2925,12 +3389,38 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
2925
3389
|
return true;
|
|
2926
3390
|
}
|
|
2927
3391
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
2928
|
-
|
|
3392
|
+
try {
|
|
3393
|
+
const payload = asRecord3(await readJsonBody3(req));
|
|
3394
|
+
const source = typeof payload?.source === "string" ? payload.source.trim() : "";
|
|
3395
|
+
const owner = typeof payload?.owner === "string" ? payload.owner.trim() : "";
|
|
3396
|
+
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
3397
|
+
const installSource = source || (owner && name ? `${owner}@${name}` : "");
|
|
3398
|
+
if (!installSource || !/^[A-Za-z0-9._/-]+@[A-Za-z0-9._-]+$/u.test(installSource)) {
|
|
3399
|
+
setJson3(res, 400, { error: "Missing or invalid skill source" });
|
|
3400
|
+
return true;
|
|
3401
|
+
}
|
|
3402
|
+
await runCommand2("npx", ["--yes", "skills", "add", installSource, "--yes", "--global"], { timeoutMs: 12e4 });
|
|
3403
|
+
try {
|
|
3404
|
+
await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
|
|
3405
|
+
} catch {
|
|
3406
|
+
}
|
|
3407
|
+
const installedMap = await collectInstalledSkillsMap(appServer);
|
|
3408
|
+
const installed = installedMap.get(name || installSource.slice(installSource.lastIndexOf("@") + 1));
|
|
3409
|
+
if (!installed?.path) {
|
|
3410
|
+
throw new Error(`Skill install completed but ${installSource} was not found in local installed skills`);
|
|
3411
|
+
}
|
|
3412
|
+
await ensureInstalledSkillIsValid(appServer, installed.path);
|
|
3413
|
+
autoPushSyncedSkills(appServer).catch(() => {
|
|
3414
|
+
});
|
|
3415
|
+
setJson3(res, 200, { ok: true, path: installed.path });
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
|
|
3418
|
+
}
|
|
2929
3419
|
return true;
|
|
2930
3420
|
}
|
|
2931
3421
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
2932
3422
|
try {
|
|
2933
|
-
const payload = asRecord3(await
|
|
3423
|
+
const payload = asRecord3(await readJsonBody3(req));
|
|
2934
3424
|
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
2935
3425
|
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
2936
3426
|
const normalizedPath = path.endsWith("/SKILL.md") ? path.slice(0, -"/SKILL.md".length) : path;
|
|
@@ -2962,7 +3452,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
2962
3452
|
}
|
|
2963
3453
|
|
|
2964
3454
|
// src/server/telegramThreadBridge.ts
|
|
2965
|
-
import { basename } from "path";
|
|
3455
|
+
import { basename as basename2 } from "path";
|
|
2966
3456
|
var TELEGRAM_MESSAGE_MAX_LENGTH = 3500;
|
|
2967
3457
|
var TELEGRAM_BOT_COMMANDS = [
|
|
2968
3458
|
{ command: "start", description: "Show quick start and thread picker" },
|
|
@@ -3448,7 +3938,7 @@ Add this ID to the bot allowlist before using the bridge.`;
|
|
|
3448
3938
|
const name = typeof record?.name === "string" ? record.name.trim() : "";
|
|
3449
3939
|
const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
|
|
3450
3940
|
const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
|
|
3451
|
-
const projectName = cwd ?
|
|
3941
|
+
const projectName = cwd ? basename2(cwd) : "project";
|
|
3452
3942
|
const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
|
|
3453
3943
|
const title = `${projectName}/${threadTitle}`.slice(0, 64);
|
|
3454
3944
|
threads.push({ id, title });
|
|
@@ -3675,6 +4165,7 @@ var FALLBACK_FREE_MODELS = [
|
|
|
3675
4165
|
var cachedFreeModels = null;
|
|
3676
4166
|
var cacheTimestamp = 0;
|
|
3677
4167
|
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
4168
|
+
var freeModelsRefreshPromise = null;
|
|
3678
4169
|
async function fetchFreeModelsFromOpenRouter() {
|
|
3679
4170
|
try {
|
|
3680
4171
|
const resp = await fetch("https://openrouter.ai/api/v1/models");
|
|
@@ -3696,11 +4187,36 @@ async function getFreeModels() {
|
|
|
3696
4187
|
}
|
|
3697
4188
|
return fetchFreeModelsFromOpenRouter();
|
|
3698
4189
|
}
|
|
4190
|
+
function getCachedFreeModels() {
|
|
4191
|
+
return cachedFreeModels ?? FALLBACK_FREE_MODELS;
|
|
4192
|
+
}
|
|
4193
|
+
function refreshFreeModelsInBackground() {
|
|
4194
|
+
if (cachedFreeModels && Date.now() - cacheTimestamp < CACHE_TTL_MS) return;
|
|
4195
|
+
if (freeModelsRefreshPromise) return;
|
|
4196
|
+
freeModelsRefreshPromise = fetchFreeModelsFromOpenRouter().finally(() => {
|
|
4197
|
+
freeModelsRefreshPromise = null;
|
|
4198
|
+
});
|
|
4199
|
+
}
|
|
3699
4200
|
var FREE_MODE_DEFAULT_MODEL = "openrouter/free";
|
|
3700
4201
|
var FREE_MODE_STATE_FILE = "webui-free-mode.json";
|
|
3701
4202
|
var CUSTOM_PROVIDER_ID = "custom-endpoint";
|
|
3702
4203
|
var OPENCODE_ZEN_PROVIDER_ID = "opencode-zen";
|
|
3703
4204
|
var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
|
|
4205
|
+
var OPENCODE_ZEN_DEFAULT_MODEL = "big-pickle";
|
|
4206
|
+
function createDefaultOpenCodeZenFreeModeState() {
|
|
4207
|
+
return {
|
|
4208
|
+
enabled: true,
|
|
4209
|
+
apiKey: null,
|
|
4210
|
+
model: OPENCODE_ZEN_DEFAULT_MODEL,
|
|
4211
|
+
customKey: false,
|
|
4212
|
+
provider: "opencode-zen",
|
|
4213
|
+
wireApi: "chat",
|
|
4214
|
+
providerKeys: {}
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
function shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuth) {
|
|
4218
|
+
return current == null && !hasUsableCodexAuth;
|
|
4219
|
+
}
|
|
3704
4220
|
function getFreeModeEnvVars(state) {
|
|
3705
4221
|
if (!state.enabled) return {};
|
|
3706
4222
|
if (state.provider === "opencode-zen" && state.apiKey) {
|
|
@@ -3717,7 +4233,9 @@ function getFreeModeConfigArgs(state, serverPort) {
|
|
|
3717
4233
|
const baseUrl2 = serverPort ? `http://127.0.0.1:${serverPort}/codex-api/zen-proxy/v1` : OPENCODE_ZEN_BASE_URL;
|
|
3718
4234
|
const wireApi = serverPort ? "responses" : state.wireApi || "chat";
|
|
3719
4235
|
const authArgs = serverPort ? ["-c", `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.experimental_bearer_token="zen-proxy-token"`] : ["-c", `model_providers.${OPENCODE_ZEN_PROVIDER_ID}.env_key="OPENCODE_ZEN_API_KEY"`];
|
|
4236
|
+
const modelArgs = state.model?.trim() ? ["-c", `model="${state.model.trim()}"`] : [];
|
|
3720
4237
|
return [
|
|
4238
|
+
...modelArgs,
|
|
3721
4239
|
"-c",
|
|
3722
4240
|
`model_provider="${OPENCODE_ZEN_PROVIDER_ID}"`,
|
|
3723
4241
|
"-c",
|
|
@@ -3785,27 +4303,52 @@ function safeStringifyUnknown(value) {
|
|
|
3785
4303
|
return String(value ?? "");
|
|
3786
4304
|
}
|
|
3787
4305
|
}
|
|
3788
|
-
function appendAssistantText(messages, text) {
|
|
4306
|
+
function appendAssistantText(messages, text, reasoningContent) {
|
|
3789
4307
|
const trimmedText = text.trim();
|
|
3790
|
-
|
|
4308
|
+
const trimmedReasoningContent = reasoningContent?.trim() ?? "";
|
|
4309
|
+
if (!trimmedText && !trimmedReasoningContent) return;
|
|
3791
4310
|
const lastMessage = messages[messages.length - 1];
|
|
3792
4311
|
if (lastMessage?.role === "assistant" && Array.isArray(lastMessage.tool_calls)) {
|
|
3793
4312
|
lastMessage.content = lastMessage.content ? `${lastMessage.content}
|
|
3794
4313
|
${trimmedText}` : trimmedText;
|
|
4314
|
+
if (trimmedReasoningContent) {
|
|
4315
|
+
lastMessage.reasoning_content = lastMessage.reasoning_content ? `${lastMessage.reasoning_content}
|
|
4316
|
+
${trimmedReasoningContent}` : trimmedReasoningContent;
|
|
4317
|
+
}
|
|
3795
4318
|
return;
|
|
3796
4319
|
}
|
|
3797
|
-
messages.push({
|
|
4320
|
+
messages.push({
|
|
4321
|
+
role: "assistant",
|
|
4322
|
+
content: trimmedText,
|
|
4323
|
+
...trimmedReasoningContent ? { reasoning_content: trimmedReasoningContent } : {}
|
|
4324
|
+
});
|
|
3798
4325
|
}
|
|
3799
|
-
function appendAssistantToolCall(messages, toolCall) {
|
|
4326
|
+
function appendAssistantToolCall(messages, toolCall, reasoningContent) {
|
|
4327
|
+
const trimmedReasoningContent = reasoningContent?.trim() ?? "";
|
|
3800
4328
|
const lastMessage = messages[messages.length - 1];
|
|
3801
4329
|
if (lastMessage?.role === "assistant" && !lastMessage.tool_call_id) {
|
|
3802
4330
|
lastMessage.tool_calls = [...lastMessage.tool_calls ?? [], toolCall];
|
|
4331
|
+
if (trimmedReasoningContent) {
|
|
4332
|
+
lastMessage.reasoning_content = lastMessage.reasoning_content ? `${lastMessage.reasoning_content}
|
|
4333
|
+
${trimmedReasoningContent}` : trimmedReasoningContent;
|
|
4334
|
+
}
|
|
3803
4335
|
return;
|
|
3804
4336
|
}
|
|
3805
|
-
messages.push({
|
|
4337
|
+
messages.push({
|
|
4338
|
+
role: "assistant",
|
|
4339
|
+
content: "",
|
|
4340
|
+
tool_calls: [toolCall],
|
|
4341
|
+
...trimmedReasoningContent ? { reasoning_content: trimmedReasoningContent } : {}
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
function extractTextParts(value) {
|
|
4345
|
+
if (typeof value === "string") return value;
|
|
4346
|
+
if (!Array.isArray(value)) return "";
|
|
4347
|
+
return value.map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter((part) => part.length > 0).join("\n");
|
|
3806
4348
|
}
|
|
3807
4349
|
function responsesInputToMessages(input, instructions) {
|
|
3808
4350
|
const messages = [];
|
|
4351
|
+
let pendingReasoningContent = "";
|
|
3809
4352
|
if (instructions) {
|
|
3810
4353
|
messages.push({ role: "system", content: instructions });
|
|
3811
4354
|
}
|
|
@@ -3815,12 +4358,29 @@ function responsesInputToMessages(input, instructions) {
|
|
|
3815
4358
|
}
|
|
3816
4359
|
for (const item of input) {
|
|
3817
4360
|
if (!item || typeof item !== "object") continue;
|
|
4361
|
+
if (item.type === "reasoning") {
|
|
4362
|
+
const content = extractTextParts(item.content);
|
|
4363
|
+
const summary = extractTextParts(item.summary);
|
|
4364
|
+
const text = content || summary;
|
|
4365
|
+
if (text) {
|
|
4366
|
+
const lastMessage = messages[messages.length - 1];
|
|
4367
|
+
if (lastMessage?.role === "assistant") {
|
|
4368
|
+
lastMessage.reasoning_content = lastMessage.reasoning_content ? `${lastMessage.reasoning_content}
|
|
4369
|
+
${text}` : text;
|
|
4370
|
+
} else {
|
|
4371
|
+
pendingReasoningContent = pendingReasoningContent ? `${pendingReasoningContent}
|
|
4372
|
+
${text}` : text;
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
continue;
|
|
4376
|
+
}
|
|
3818
4377
|
if (item.type === "message" && item.role) {
|
|
3819
4378
|
const content = item.content;
|
|
3820
4379
|
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => typeof part?.text === "string" ? part.text : "").filter((part) => part.length > 0).join("\n") : typeof item.text === "string" ? item.text : "";
|
|
3821
4380
|
const role = item.role === "developer" ? "system" : item.role;
|
|
3822
4381
|
if (role === "assistant") {
|
|
3823
|
-
appendAssistantText(messages, text);
|
|
4382
|
+
appendAssistantText(messages, text, pendingReasoningContent);
|
|
4383
|
+
pendingReasoningContent = "";
|
|
3824
4384
|
} else {
|
|
3825
4385
|
messages.push({ role, content: text });
|
|
3826
4386
|
}
|
|
@@ -3842,7 +4402,8 @@ function responsesInputToMessages(input, instructions) {
|
|
|
3842
4402
|
name: item.name,
|
|
3843
4403
|
arguments: typeof item.arguments === "string" ? item.arguments : "{}"
|
|
3844
4404
|
}
|
|
3845
|
-
});
|
|
4405
|
+
}, pendingReasoningContent);
|
|
4406
|
+
pendingReasoningContent = "";
|
|
3846
4407
|
}
|
|
3847
4408
|
}
|
|
3848
4409
|
return messages;
|
|
@@ -3905,6 +4466,14 @@ function chatCompletionToResponsesFormat(chatResponse, model) {
|
|
|
3905
4466
|
status: "completed"
|
|
3906
4467
|
});
|
|
3907
4468
|
}
|
|
4469
|
+
if (message.reasoning_content) {
|
|
4470
|
+
output.push({
|
|
4471
|
+
type: "reasoning",
|
|
4472
|
+
id: `rs_${Date.now()}`,
|
|
4473
|
+
summary: [],
|
|
4474
|
+
content: [{ type: "reasoning_text", text: message.reasoning_content }]
|
|
4475
|
+
});
|
|
4476
|
+
}
|
|
3908
4477
|
}
|
|
3909
4478
|
const usage = chatResponse.usage;
|
|
3910
4479
|
return {
|
|
@@ -3929,6 +4498,7 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
|
|
|
3929
4498
|
});
|
|
3930
4499
|
let buffer = "";
|
|
3931
4500
|
const contentParts = [];
|
|
4501
|
+
const reasoningParts = [];
|
|
3932
4502
|
let responseId = `resp_${Date.now()}`;
|
|
3933
4503
|
res.write(`data: {"type":"response.created","response":{"id":"${responseId}","object":"response","status":"in_progress","model":"${model}","output":[]}}
|
|
3934
4504
|
|
|
@@ -3947,6 +4517,9 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
|
|
|
3947
4517
|
const parsed = JSON.parse(data);
|
|
3948
4518
|
if (parsed.id) responseId = `resp_${parsed.id}`;
|
|
3949
4519
|
const delta = parsed.choices?.[0]?.delta;
|
|
4520
|
+
if (delta?.reasoning_content) {
|
|
4521
|
+
reasoningParts.push(delta.reasoning_content);
|
|
4522
|
+
}
|
|
3950
4523
|
if (delta?.content) {
|
|
3951
4524
|
contentParts.push(delta.content);
|
|
3952
4525
|
const escaped = JSON.stringify(delta.content).slice(1, -1);
|
|
@@ -3960,7 +4533,18 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
|
|
|
3960
4533
|
});
|
|
3961
4534
|
upstreamRes.on("end", () => {
|
|
3962
4535
|
const fullText = contentParts.join("");
|
|
4536
|
+
const fullReasoningText = reasoningParts.join("");
|
|
3963
4537
|
const escapedFull = JSON.stringify(fullText).slice(1, -1);
|
|
4538
|
+
const messageItem = { type: "message", role: "assistant", content: [{ type: "output_text", text: fullText }], status: "completed" };
|
|
4539
|
+
const output = [messageItem];
|
|
4540
|
+
if (fullReasoningText) {
|
|
4541
|
+
output.push({
|
|
4542
|
+
type: "reasoning",
|
|
4543
|
+
id: `rs_${Date.now()}`,
|
|
4544
|
+
summary: [],
|
|
4545
|
+
content: [{ type: "reasoning_text", text: fullReasoningText }]
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
3964
4548
|
res.write(`data: {"type":"response.output_text.done","output_index":0,"content_index":0,"text":"${escapedFull}"}
|
|
3965
4549
|
|
|
3966
4550
|
`);
|
|
@@ -3970,7 +4554,17 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
|
|
|
3970
4554
|
res.write(`data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"${escapedFull}"}],"status":"completed"}}
|
|
3971
4555
|
|
|
3972
4556
|
`);
|
|
3973
|
-
|
|
4557
|
+
if (fullReasoningText) {
|
|
4558
|
+
const reasoningIndex = output.length - 1;
|
|
4559
|
+
const reasoningItem = output[reasoningIndex];
|
|
4560
|
+
res.write(`data: ${JSON.stringify({ type: "response.output_item.added", output_index: reasoningIndex, item: reasoningItem })}
|
|
4561
|
+
|
|
4562
|
+
`);
|
|
4563
|
+
res.write(`data: ${JSON.stringify({ type: "response.output_item.done", output_index: reasoningIndex, item: reasoningItem })}
|
|
4564
|
+
|
|
4565
|
+
`);
|
|
4566
|
+
}
|
|
4567
|
+
res.write(`data: ${JSON.stringify({ type: "response.completed", response: { id: responseId, object: "response", status: "completed", model, output } })}
|
|
3974
4568
|
|
|
3975
4569
|
`);
|
|
3976
4570
|
res.end();
|
|
@@ -4042,7 +4636,7 @@ function hasToolOutputsInInput(input) {
|
|
|
4042
4636
|
function handleUnifiedResponsesProxyRequest(req, res, options) {
|
|
4043
4637
|
void (async () => {
|
|
4044
4638
|
try {
|
|
4045
|
-
if (!options.bearerToken) {
|
|
4639
|
+
if (options.requireBearerToken !== false && !options.bearerToken) {
|
|
4046
4640
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4047
4641
|
res.end(JSON.stringify({ error: { message: options.missingKeyMessage } }));
|
|
4048
4642
|
return;
|
|
@@ -4055,14 +4649,14 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
|
|
|
4055
4649
|
const useChatCompletions = options.wireApi === "chat" && !useResponsesFallback;
|
|
4056
4650
|
const useChatPayload = useChatCompletions || options.responsesPayloadFormat === "chat";
|
|
4057
4651
|
const isStreaming = parsedBody.stream === true;
|
|
4058
|
-
const effectiveStreaming =
|
|
4652
|
+
const effectiveStreaming = useChatPayload && isStreaming && !(hasTools || hasToolOutputs);
|
|
4059
4653
|
let payload = "";
|
|
4060
4654
|
let upstreamUrl;
|
|
4061
4655
|
if (useChatPayload) {
|
|
4062
4656
|
const chatReq = {
|
|
4063
4657
|
model: parsedBody.model,
|
|
4064
4658
|
messages: responsesInputToMessages(parsedBody.input, parsedBody.instructions),
|
|
4065
|
-
stream:
|
|
4659
|
+
stream: effectiveStreaming
|
|
4066
4660
|
};
|
|
4067
4661
|
if (parsedBody.temperature != null) chatReq.temperature = parsedBody.temperature;
|
|
4068
4662
|
if (parsedBody.top_p != null) chatReq.top_p = parsedBody.top_p;
|
|
@@ -4072,7 +4666,7 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
|
|
|
4072
4666
|
if (chatTools) chatReq.tools = chatTools;
|
|
4073
4667
|
if (chatToolChoice) chatReq.tool_choice = chatToolChoice;
|
|
4074
4668
|
payload = JSON.stringify(chatReq);
|
|
4075
|
-
upstreamUrl = new URL(
|
|
4669
|
+
upstreamUrl = new URL(options.chatCompletionsEndpoint);
|
|
4076
4670
|
} else {
|
|
4077
4671
|
const requestBody = parsedBody && typeof parsedBody === "object" && !Array.isArray(parsedBody) ? { ...parsedBody } : {};
|
|
4078
4672
|
const sanitized = options.sanitizeResponsesRequest ? options.sanitizeResponsesRequest(requestBody) : requestBody;
|
|
@@ -4088,11 +4682,11 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
|
|
|
4088
4682
|
headers: {
|
|
4089
4683
|
"Content-Type": "application/json",
|
|
4090
4684
|
"Content-Length": Buffer.byteLength(payload),
|
|
4091
|
-
"Authorization": `Bearer ${options.bearerToken}`
|
|
4685
|
+
...options.bearerToken ? { "Authorization": `Bearer ${options.bearerToken}` } : {}
|
|
4092
4686
|
}
|
|
4093
4687
|
}, (upstreamRes) => {
|
|
4094
4688
|
const status = upstreamRes.statusCode ?? 502;
|
|
4095
|
-
if (
|
|
4689
|
+
if (useChatPayload && effectiveStreaming && status >= 200 && status < 300) {
|
|
4096
4690
|
forwardStreamingTextResponse(upstreamRes, res, parsedBody.model);
|
|
4097
4691
|
return;
|
|
4098
4692
|
}
|
|
@@ -4100,7 +4694,7 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
|
|
|
4100
4694
|
upstreamRes.on("data", (chunk) => chunks.push(chunk));
|
|
4101
4695
|
upstreamRes.on("end", () => {
|
|
4102
4696
|
const rawResponseBody = Buffer.concat(chunks).toString();
|
|
4103
|
-
if (!
|
|
4697
|
+
if (!useChatPayload) {
|
|
4104
4698
|
res.writeHead(status, copyProxyHeaders(upstreamRes.headers));
|
|
4105
4699
|
res.end(rawResponseBody);
|
|
4106
4700
|
return;
|
|
@@ -4108,6 +4702,14 @@ function handleUnifiedResponsesProxyRequest(req, res, options) {
|
|
|
4108
4702
|
try {
|
|
4109
4703
|
const upstreamPayload = JSON.parse(rawResponseBody);
|
|
4110
4704
|
if (upstreamPayload.error || status >= 400) {
|
|
4705
|
+
if (process.env.CODEXUI_PROXY_DEBUG === "1") {
|
|
4706
|
+
console.warn("[unified-responses-proxy]", JSON.stringify({
|
|
4707
|
+
status,
|
|
4708
|
+
upstreamUrl: upstreamUrl.toString(),
|
|
4709
|
+
request: JSON.parse(payload),
|
|
4710
|
+
response: upstreamPayload
|
|
4711
|
+
}));
|
|
4712
|
+
}
|
|
4111
4713
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
4112
4714
|
res.end(JSON.stringify(upstreamPayload));
|
|
4113
4715
|
return;
|
|
@@ -4193,6 +4795,7 @@ function handleZenProxyRequest(req, res, bearerToken, wireApi) {
|
|
|
4193
4795
|
responsesEndpoint: ZEN_RESPONSES_ENDPOINT,
|
|
4194
4796
|
chatCompletionsEndpoint: ZEN_CHAT_COMPLETIONS_ENDPOINT,
|
|
4195
4797
|
missingKeyMessage: "Missing OpenCode Zen API key",
|
|
4798
|
+
requireBearerToken: false,
|
|
4196
4799
|
allowToolFallbackToResponses: false,
|
|
4197
4800
|
responsesPayloadFormat: "chat"
|
|
4198
4801
|
});
|
|
@@ -4217,9 +4820,9 @@ function handleCustomEndpointProxyRequest(req, res, options) {
|
|
|
4217
4820
|
import { chmodSync, existsSync as existsSync3, lstatSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
|
|
4218
4821
|
import { randomUUID } from "crypto";
|
|
4219
4822
|
import { createRequire } from "module";
|
|
4220
|
-
import { basename as
|
|
4823
|
+
import { basename as basename3, dirname, join as join5 } from "path";
|
|
4221
4824
|
import { homedir as homedir4 } from "os";
|
|
4222
|
-
import { spawnSync as
|
|
4825
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4223
4826
|
var TERMINAL_BUFFER_LIMIT = 16 * 1024;
|
|
4224
4827
|
var DEFAULT_COLS = 80;
|
|
4225
4828
|
var DEFAULT_ROWS = 24;
|
|
@@ -4352,7 +4955,7 @@ var ThreadTerminalManager = class {
|
|
|
4352
4955
|
id: params.sessionId,
|
|
4353
4956
|
threadId: params.threadId,
|
|
4354
4957
|
cwd,
|
|
4355
|
-
shell:
|
|
4958
|
+
shell: basename3(shell),
|
|
4356
4959
|
pty,
|
|
4357
4960
|
buffer: "",
|
|
4358
4961
|
truncated: false
|
|
@@ -4492,7 +5095,6 @@ function normalizeDimension(value, fallback) {
|
|
|
4492
5095
|
return Math.max(1, Math.min(500, Math.trunc(parsed)));
|
|
4493
5096
|
}
|
|
4494
5097
|
function loadTerminalSpawn() {
|
|
4495
|
-
repairNativePtyBuild("node-pty-prebuilt-multiarch");
|
|
4496
5098
|
repairNativePtyBuild("node-pty");
|
|
4497
5099
|
if (resolveNodePtyPrebuiltPath()) {
|
|
4498
5100
|
try {
|
|
@@ -4522,7 +5124,7 @@ function repairNativePtyBuild(packageName) {
|
|
|
4522
5124
|
writeFileSync(makefile, patched);
|
|
4523
5125
|
}
|
|
4524
5126
|
rmSync(binary, { force: true });
|
|
4525
|
-
|
|
5127
|
+
spawnSync3("make", ["BUILDTYPE=Release", "-C", buildDir], { stdio: "ignore" });
|
|
4526
5128
|
} catch {
|
|
4527
5129
|
}
|
|
4528
5130
|
}
|
|
@@ -4557,9 +5159,12 @@ function resolveNodePtyPrebuiltPath() {
|
|
|
4557
5159
|
}
|
|
4558
5160
|
function ensureNodePtyPrebuiltExecutable() {
|
|
4559
5161
|
if (process.platform !== "darwin" && process.platform !== "linux") return;
|
|
5162
|
+
ensurePackageSpawnHelperExecutable("node-pty");
|
|
5163
|
+
ensurePackageSpawnHelperExecutable("node-pty-prebuilt-multiarch");
|
|
5164
|
+
}
|
|
5165
|
+
function ensurePackageSpawnHelperExecutable(packageName) {
|
|
4560
5166
|
try {
|
|
4561
|
-
const
|
|
4562
|
-
const packageRoot = join5(dirname(nodePtyEntry), "..");
|
|
5167
|
+
const packageRoot = dirname(require2.resolve(`${packageName}/package.json`));
|
|
4563
5168
|
const helperPath = join5(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
|
|
4564
5169
|
if (existsSync3(helperPath)) {
|
|
4565
5170
|
chmodSync(helperPath, 493);
|
|
@@ -4577,45 +5182,6 @@ function shellQuote(value) {
|
|
|
4577
5182
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4578
5183
|
}
|
|
4579
5184
|
|
|
4580
|
-
// src/utils/commandInvocation.ts
|
|
4581
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
4582
|
-
import { basename as basename3, extname } from "path";
|
|
4583
|
-
var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
|
|
4584
|
-
function quoteCmdExeArg(value) {
|
|
4585
|
-
const normalized = value.replace(/"/g, '""');
|
|
4586
|
-
if (!/[\s"]/u.test(normalized)) {
|
|
4587
|
-
return normalized;
|
|
4588
|
-
}
|
|
4589
|
-
return `"${normalized}"`;
|
|
4590
|
-
}
|
|
4591
|
-
function needsCmdExeWrapper(command) {
|
|
4592
|
-
if (process.platform !== "win32") {
|
|
4593
|
-
return false;
|
|
4594
|
-
}
|
|
4595
|
-
const lowerCommand = command.toLowerCase();
|
|
4596
|
-
const baseName = basename3(lowerCommand);
|
|
4597
|
-
if (/\.(cmd|bat)$/i.test(baseName)) {
|
|
4598
|
-
return true;
|
|
4599
|
-
}
|
|
4600
|
-
if (extname(baseName)) {
|
|
4601
|
-
return false;
|
|
4602
|
-
}
|
|
4603
|
-
return WINDOWS_CMD_NAMES.has(baseName);
|
|
4604
|
-
}
|
|
4605
|
-
function getSpawnInvocation(command, args = []) {
|
|
4606
|
-
if (needsCmdExeWrapper(command)) {
|
|
4607
|
-
return {
|
|
4608
|
-
command: "cmd.exe",
|
|
4609
|
-
args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
|
|
4610
|
-
};
|
|
4611
|
-
}
|
|
4612
|
-
return { command, args };
|
|
4613
|
-
}
|
|
4614
|
-
function spawnSyncCommand(command, args = [], options = {}) {
|
|
4615
|
-
const invocation = getSpawnInvocation(command, args);
|
|
4616
|
-
return spawnSync3(invocation.command, invocation.args, options);
|
|
4617
|
-
}
|
|
4618
|
-
|
|
4619
5185
|
// src/server/codexAppServerBridge.ts
|
|
4620
5186
|
var COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX = 1e3;
|
|
4621
5187
|
var PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -4631,6 +5197,152 @@ var DEFAULT_API_PERF_MS_THRESHOLD = 300;
|
|
|
4631
5197
|
var DEFAULT_API_PERF_BODY_MB_THRESHOLD = 1;
|
|
4632
5198
|
var MB_DIVISOR = 1024 * 1024;
|
|
4633
5199
|
var COMPOSIO_USER_DATA_PATH = join6(homedir5(), ".composio", "user_data.json");
|
|
5200
|
+
var SESSION_SKILL_INPUT_CACHE_LIMIT = 64;
|
|
5201
|
+
var sessionSkillInputCache = /* @__PURE__ */ new Map();
|
|
5202
|
+
function parseSessionSkillText(value) {
|
|
5203
|
+
const trimmed = value.trim();
|
|
5204
|
+
if (!trimmed.startsWith("<skill>")) return null;
|
|
5205
|
+
const name = trimmed.match(/<name>\s*([\s\S]*?)\s*<\/name>/u)?.[1]?.trim() ?? "";
|
|
5206
|
+
const path = trimmed.match(/<path>\s*([\s\S]*?)\s*<\/path>/u)?.[1]?.trim() ?? "";
|
|
5207
|
+
if (!name || !path) return null;
|
|
5208
|
+
return { name, path };
|
|
5209
|
+
}
|
|
5210
|
+
function buildSessionSkillInputsByTurn(sessionLogRaw) {
|
|
5211
|
+
let currentTurnId = "";
|
|
5212
|
+
const skillsByTurnId = /* @__PURE__ */ new Map();
|
|
5213
|
+
for (const line of sessionLogRaw.split("\n")) {
|
|
5214
|
+
if (!line.trim()) continue;
|
|
5215
|
+
let row = null;
|
|
5216
|
+
try {
|
|
5217
|
+
row = JSON.parse(line);
|
|
5218
|
+
} catch {
|
|
5219
|
+
continue;
|
|
5220
|
+
}
|
|
5221
|
+
if (row.type === "turn_context") {
|
|
5222
|
+
const payloadRecord2 = asRecord5(row.payload);
|
|
5223
|
+
currentTurnId = readNonEmptyString(payloadRecord2?.turn_id) || currentTurnId;
|
|
5224
|
+
continue;
|
|
5225
|
+
}
|
|
5226
|
+
if (row.type === "event_msg") {
|
|
5227
|
+
const payloadRecord2 = asRecord5(row.payload);
|
|
5228
|
+
if (payloadRecord2?.type === "task_started") {
|
|
5229
|
+
currentTurnId = readNonEmptyString(payloadRecord2.turn_id) || currentTurnId;
|
|
5230
|
+
}
|
|
5231
|
+
continue;
|
|
5232
|
+
}
|
|
5233
|
+
if (row.type !== "response_item" || !currentTurnId) continue;
|
|
5234
|
+
const payloadRecord = asRecord5(row.payload);
|
|
5235
|
+
if (payloadRecord?.type !== "message" || payloadRecord.role !== "user") continue;
|
|
5236
|
+
const content = Array.isArray(payloadRecord.content) ? payloadRecord.content : [];
|
|
5237
|
+
for (const contentItem of content) {
|
|
5238
|
+
const contentRecord = asRecord5(contentItem);
|
|
5239
|
+
if (contentRecord?.type !== "input_text" || typeof contentRecord.text !== "string") continue;
|
|
5240
|
+
const skill = parseSessionSkillText(contentRecord.text);
|
|
5241
|
+
if (!skill) continue;
|
|
5242
|
+
const existing = skillsByTurnId.get(currentTurnId) ?? [];
|
|
5243
|
+
if (!existing.some((item) => item.path === skill.path)) {
|
|
5244
|
+
existing.push(skill);
|
|
5245
|
+
skillsByTurnId.set(currentTurnId, existing);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
return skillsByTurnId;
|
|
5250
|
+
}
|
|
5251
|
+
async function readCachedSessionSkillInputsByTurn(sessionPath) {
|
|
5252
|
+
const sessionStat = await stat4(sessionPath);
|
|
5253
|
+
const cached = sessionSkillInputCache.get(sessionPath);
|
|
5254
|
+
if (cached && cached.size === sessionStat.size && cached.mtimeMs === sessionStat.mtimeMs) {
|
|
5255
|
+
return cached.skillsByTurnId;
|
|
5256
|
+
}
|
|
5257
|
+
const sessionLogRaw = await readFile3(sessionPath, "utf8");
|
|
5258
|
+
const skillsByTurnId = buildSessionSkillInputsByTurn(sessionLogRaw);
|
|
5259
|
+
sessionSkillInputCache.set(sessionPath, {
|
|
5260
|
+
size: sessionStat.size,
|
|
5261
|
+
mtimeMs: sessionStat.mtimeMs,
|
|
5262
|
+
skillsByTurnId
|
|
5263
|
+
});
|
|
5264
|
+
if (sessionSkillInputCache.size > SESSION_SKILL_INPUT_CACHE_LIMIT) {
|
|
5265
|
+
const oldestKey = sessionSkillInputCache.keys().next().value;
|
|
5266
|
+
if (oldestKey) sessionSkillInputCache.delete(oldestKey);
|
|
5267
|
+
}
|
|
5268
|
+
return skillsByTurnId;
|
|
5269
|
+
}
|
|
5270
|
+
function mergeSessionSkillInputsIntoTurnsFromMap(turns, skillsByTurnId) {
|
|
5271
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
5272
|
+
for (const turn of turns) {
|
|
5273
|
+
const turnRecord = asRecord5(turn);
|
|
5274
|
+
const turnId = readNonEmptyString(turnRecord?.id);
|
|
5275
|
+
if (turnId) turnIds.add(turnId);
|
|
5276
|
+
}
|
|
5277
|
+
if (turnIds.size === 0) return turns;
|
|
5278
|
+
if (skillsByTurnId.size === 0) return turns;
|
|
5279
|
+
let changed = false;
|
|
5280
|
+
const nextTurns = turns.map((turn) => {
|
|
5281
|
+
const turnRecord = asRecord5(turn);
|
|
5282
|
+
const turnId = readNonEmptyString(turnRecord?.id);
|
|
5283
|
+
const skills = turnId ? skillsByTurnId.get(turnId) : void 0;
|
|
5284
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : null;
|
|
5285
|
+
if (!turnRecord || !skills || skills.length === 0 || !items) return turn;
|
|
5286
|
+
let targetUserMessageIndex = -1;
|
|
5287
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
5288
|
+
const itemRecord = asRecord5(items[index]);
|
|
5289
|
+
if (itemRecord?.type === "userMessage" && Array.isArray(itemRecord.content)) {
|
|
5290
|
+
targetUserMessageIndex = index;
|
|
5291
|
+
break;
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
if (targetUserMessageIndex < 0) return turn;
|
|
5295
|
+
let addedToMessage = false;
|
|
5296
|
+
const nextItems = items.map((item, index) => {
|
|
5297
|
+
const itemRecord = asRecord5(item);
|
|
5298
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : null;
|
|
5299
|
+
if (index !== targetUserMessageIndex || itemRecord?.type !== "userMessage" || !content) return item;
|
|
5300
|
+
const existingSkillPaths = new Set(
|
|
5301
|
+
content.flatMap((contentItem) => {
|
|
5302
|
+
const contentRecord = asRecord5(contentItem);
|
|
5303
|
+
const path = typeof contentRecord?.path === "string" ? contentRecord.path.trim() : "";
|
|
5304
|
+
return contentRecord?.type === "skill" && path ? [path] : [];
|
|
5305
|
+
})
|
|
5306
|
+
);
|
|
5307
|
+
const missingSkills = skills.filter((skill) => !existingSkillPaths.has(skill.path));
|
|
5308
|
+
if (missingSkills.length === 0) return item;
|
|
5309
|
+
addedToMessage = true;
|
|
5310
|
+
changed = true;
|
|
5311
|
+
return {
|
|
5312
|
+
...itemRecord,
|
|
5313
|
+
content: [
|
|
5314
|
+
...content,
|
|
5315
|
+
...missingSkills.map((skill) => ({ type: "skill", name: skill.name, path: skill.path }))
|
|
5316
|
+
]
|
|
5317
|
+
};
|
|
5318
|
+
});
|
|
5319
|
+
return addedToMessage ? { ...turnRecord, items: nextItems } : turn;
|
|
5320
|
+
});
|
|
5321
|
+
return changed ? nextTurns : turns;
|
|
5322
|
+
}
|
|
5323
|
+
async function mergeSessionSkillInputsIntoThreadResult(result) {
|
|
5324
|
+
const record = asRecord5(result);
|
|
5325
|
+
const thread = asRecord5(record?.thread);
|
|
5326
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : null;
|
|
5327
|
+
const sessionPath = readNonEmptyString(thread?.path);
|
|
5328
|
+
if (!record || !thread || !turns || turns.length === 0 || !sessionPath || !isAbsolute2(sessionPath)) {
|
|
5329
|
+
return result;
|
|
5330
|
+
}
|
|
5331
|
+
try {
|
|
5332
|
+
const skillsByTurnId = await readCachedSessionSkillInputsByTurn(sessionPath);
|
|
5333
|
+
const mergedTurns = mergeSessionSkillInputsIntoTurnsFromMap(turns, skillsByTurnId);
|
|
5334
|
+
if (mergedTurns === turns) return result;
|
|
5335
|
+
return {
|
|
5336
|
+
...record,
|
|
5337
|
+
thread: {
|
|
5338
|
+
...thread,
|
|
5339
|
+
turns: mergedTurns
|
|
5340
|
+
}
|
|
5341
|
+
};
|
|
5342
|
+
} catch {
|
|
5343
|
+
return result;
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
4634
5346
|
function readEnvValueFromFile(filePath, key) {
|
|
4635
5347
|
try {
|
|
4636
5348
|
const content = readFileSync2(filePath, "utf8");
|
|
@@ -5247,6 +5959,48 @@ function extractThreadMessageText(threadReadPayload) {
|
|
|
5247
5959
|
function readNonEmptyString(value) {
|
|
5248
5960
|
return typeof value === "string" && value.trim().length > 0 ? value : "";
|
|
5249
5961
|
}
|
|
5962
|
+
function readThreadArchiveFallbackName(threadReadResult) {
|
|
5963
|
+
const record = asRecord5(threadReadResult);
|
|
5964
|
+
const thread = asRecord5(record?.thread);
|
|
5965
|
+
return readNonEmptyString(thread?.name) || readNonEmptyString(thread?.title) || readNonEmptyString(thread?.preview) || "Untitled thread";
|
|
5966
|
+
}
|
|
5967
|
+
function isArchivedThreadReadResult(threadReadResult) {
|
|
5968
|
+
const record = asRecord5(threadReadResult);
|
|
5969
|
+
const thread = asRecord5(record?.thread);
|
|
5970
|
+
const sessionPath = readNonEmptyString(thread?.path);
|
|
5971
|
+
return sessionPath.split(/[\\/]+/u).includes("archived_sessions");
|
|
5972
|
+
}
|
|
5973
|
+
async function callRpcWithArchiveRecovery(appServer, method, params) {
|
|
5974
|
+
try {
|
|
5975
|
+
return await appServer.rpc(method, params ?? null);
|
|
5976
|
+
} catch (error) {
|
|
5977
|
+
if (method !== "thread/archive") {
|
|
5978
|
+
throw error;
|
|
5979
|
+
}
|
|
5980
|
+
const paramsRecord = asRecord5(params);
|
|
5981
|
+
const threadId = readNonEmptyString(paramsRecord?.threadId);
|
|
5982
|
+
const errorMessage = getErrorMessage5(error, "");
|
|
5983
|
+
if (!threadId || !errorMessage.includes("no rollout found")) {
|
|
5984
|
+
throw error;
|
|
5985
|
+
}
|
|
5986
|
+
let threadReadResult = null;
|
|
5987
|
+
try {
|
|
5988
|
+
threadReadResult = await appServer.rpc("thread/read", {
|
|
5989
|
+
threadId,
|
|
5990
|
+
includeTurns: false
|
|
5991
|
+
});
|
|
5992
|
+
if (isArchivedThreadReadResult(threadReadResult)) {
|
|
5993
|
+
return null;
|
|
5994
|
+
}
|
|
5995
|
+
} catch {
|
|
5996
|
+
}
|
|
5997
|
+
await appServer.rpc("thread/name/set", {
|
|
5998
|
+
threadId,
|
|
5999
|
+
name: readThreadArchiveFallbackName(threadReadResult)
|
|
6000
|
+
});
|
|
6001
|
+
return appServer.rpc(method, params ?? null);
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
5250
6004
|
async function listTerminalQuickCommands(cwd) {
|
|
5251
6005
|
const normalizedCwd = isAbsolute2(cwd) ? cwd : resolve2(cwd);
|
|
5252
6006
|
const info = await stat4(normalizedCwd);
|
|
@@ -5350,26 +6104,53 @@ function readBoolean2(value) {
|
|
|
5350
6104
|
function readNumber2(value) {
|
|
5351
6105
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
5352
6106
|
}
|
|
5353
|
-
function
|
|
6107
|
+
function buildComposioInvocation(args) {
|
|
6108
|
+
const overrideCommand = process.env.CODEXUI_COMPOSIO_COMMAND?.trim();
|
|
6109
|
+
if (overrideCommand) {
|
|
6110
|
+
const invocation = getSpawnInvocation(overrideCommand, args);
|
|
6111
|
+
return {
|
|
6112
|
+
command: invocation.command,
|
|
6113
|
+
args: invocation.args,
|
|
6114
|
+
displayCommand: `${overrideCommand} ${args.map(quoteShellTokenIfNeeded).join(" ")}`.trim()
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
return buildInstalledComposioInvocation(args);
|
|
6118
|
+
}
|
|
6119
|
+
function buildInstalledComposioInvocation(args) {
|
|
5354
6120
|
const candidates = [
|
|
5355
|
-
process.env.CODEXUI_COMPOSIO_COMMAND?.trim() ?? "",
|
|
5356
6121
|
join6(homedir5(), ".composio", "composio"),
|
|
5357
6122
|
"composio"
|
|
5358
6123
|
];
|
|
5359
6124
|
for (const candidate of candidates) {
|
|
5360
|
-
if (!candidate) continue;
|
|
5361
6125
|
if ((candidate.includes("/") || candidate.includes("\\")) && !existsSync4(candidate)) continue;
|
|
5362
|
-
const invocation = getSpawnInvocation(candidate,
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
return candidate;
|
|
5369
|
-
}
|
|
6126
|
+
const invocation = getSpawnInvocation(candidate, args);
|
|
6127
|
+
return {
|
|
6128
|
+
command: invocation.command,
|
|
6129
|
+
args: invocation.args,
|
|
6130
|
+
displayCommand: `${candidate} ${args.map(quoteShellTokenIfNeeded).join(" ")}`.trim()
|
|
6131
|
+
};
|
|
5370
6132
|
}
|
|
5371
6133
|
return null;
|
|
5372
6134
|
}
|
|
6135
|
+
function probeComposioInvocation(invocation) {
|
|
6136
|
+
const probe = spawnSync4(invocation.command, invocation.args, {
|
|
6137
|
+
encoding: "utf8",
|
|
6138
|
+
env: process.env,
|
|
6139
|
+
windowsHide: true
|
|
6140
|
+
});
|
|
6141
|
+
const output = `${probe.stdout ?? ""}${probe.stderr ?? ""}`.trim();
|
|
6142
|
+
return {
|
|
6143
|
+
available: !probe.error && probe.status === 0,
|
|
6144
|
+
cliVersion: probe.status === 0 ? (probe.stdout ?? "").trim() : "",
|
|
6145
|
+
output
|
|
6146
|
+
};
|
|
6147
|
+
}
|
|
6148
|
+
function resolveComposioInvocation(args) {
|
|
6149
|
+
const invocation = buildComposioInvocation(args);
|
|
6150
|
+
const versionInvocation = buildComposioInvocation(["--version"]);
|
|
6151
|
+
if (invocation && versionInvocation && probeComposioInvocation(versionInvocation).available) return invocation;
|
|
6152
|
+
return null;
|
|
6153
|
+
}
|
|
5373
6154
|
function parseComposioJson(stdout, fallback) {
|
|
5374
6155
|
const trimmed = stdout.trim();
|
|
5375
6156
|
if (!trimmed) {
|
|
@@ -5378,11 +6159,10 @@ function parseComposioJson(stdout, fallback) {
|
|
|
5378
6159
|
return JSON.parse(trimmed);
|
|
5379
6160
|
}
|
|
5380
6161
|
async function runComposioJson(args, fallback) {
|
|
5381
|
-
const
|
|
5382
|
-
if (!
|
|
6162
|
+
const invocation = resolveComposioInvocation(args);
|
|
6163
|
+
if (!invocation) {
|
|
5383
6164
|
throw new Error("Composio CLI is not installed");
|
|
5384
6165
|
}
|
|
5385
|
-
const invocation = getSpawnInvocation(command, args);
|
|
5386
6166
|
const child = spawn4(invocation.command, invocation.args, {
|
|
5387
6167
|
env: process.env,
|
|
5388
6168
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -5488,18 +6268,12 @@ async function readComposioConnectionsBySlug() {
|
|
|
5488
6268
|
return bySlug;
|
|
5489
6269
|
}
|
|
5490
6270
|
async function readComposioStatus() {
|
|
5491
|
-
const
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
const probe = spawnSync4(invocation.command, invocation.args, {
|
|
5496
|
-
encoding: "utf8",
|
|
5497
|
-
windowsHide: true
|
|
5498
|
-
});
|
|
5499
|
-
return probe.status === 0 ? probe.stdout.trim() : "";
|
|
5500
|
-
})();
|
|
6271
|
+
const versionInvocation = buildComposioInvocation(["--version"]);
|
|
6272
|
+
const probe = versionInvocation ? probeComposioInvocation(versionInvocation) : { available: false, cliVersion: "", output: "" };
|
|
6273
|
+
const available = probe.available;
|
|
6274
|
+
const cliVersion = probe.cliVersion;
|
|
5501
6275
|
const userData = await readComposioUserData();
|
|
5502
|
-
if (!
|
|
6276
|
+
if (!available) {
|
|
5503
6277
|
return {
|
|
5504
6278
|
available: false,
|
|
5505
6279
|
authenticated: false,
|
|
@@ -5609,11 +6383,10 @@ async function startComposioLink(slug) {
|
|
|
5609
6383
|
};
|
|
5610
6384
|
}
|
|
5611
6385
|
async function startComposioLogin() {
|
|
5612
|
-
const
|
|
5613
|
-
if (!
|
|
6386
|
+
const invocation = resolveComposioInvocation(["login", "--no-browser", "-y"]);
|
|
6387
|
+
if (!invocation) {
|
|
5614
6388
|
throw new Error("Composio CLI is not installed");
|
|
5615
6389
|
}
|
|
5616
|
-
const invocation = getSpawnInvocation(command, ["login", "--no-browser", "-y"]);
|
|
5617
6390
|
const proc = spawn4(invocation.command, invocation.args, {
|
|
5618
6391
|
cwd: process.cwd(),
|
|
5619
6392
|
env: process.env,
|
|
@@ -6470,6 +7243,63 @@ function normalizeBranchRefName(value) {
|
|
|
6470
7243
|
if (trimmed.startsWith("refs/remotes/")) return trimmed.slice("refs/remotes/".length);
|
|
6471
7244
|
return trimmed;
|
|
6472
7245
|
}
|
|
7246
|
+
function toHeaderGitResetHistoryRef(branchName, commitSha) {
|
|
7247
|
+
return `refs/codex/header-git-reset-history/${branchName}/${commitSha}`;
|
|
7248
|
+
}
|
|
7249
|
+
var HEADER_GIT_RESET_HISTORY_REF_LIMIT = 25;
|
|
7250
|
+
async function assertLocalGitBranch(repoRoot, branchName) {
|
|
7251
|
+
await runCommandCapture2("git", ["show-ref", "--verify", `refs/heads/${branchName}`], { cwd: repoRoot });
|
|
7252
|
+
}
|
|
7253
|
+
async function checkoutGitBranchWithWorktreeRecovery(repoRoot, branchName) {
|
|
7254
|
+
try {
|
|
7255
|
+
await runCommand3("git", ["checkout", branchName], { cwd: repoRoot });
|
|
7256
|
+
} catch (checkoutError) {
|
|
7257
|
+
const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, branchName);
|
|
7258
|
+
if (!blockingWorktreePath) {
|
|
7259
|
+
throw checkoutError;
|
|
7260
|
+
}
|
|
7261
|
+
await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
|
|
7262
|
+
await runCommand3("git", ["checkout", branchName], { cwd: repoRoot });
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
7265
|
+
async function pruneHeaderGitResetHistoryRefs(repoRoot, branchName) {
|
|
7266
|
+
const resetHistoryRefPrefix = `refs/codex/header-git-reset-history/${branchName}/`;
|
|
7267
|
+
const refsRaw = await runCommandCapture2(
|
|
7268
|
+
"git",
|
|
7269
|
+
["for-each-ref", "--sort=-creatordate", "--format=%(refname)", resetHistoryRefPrefix],
|
|
7270
|
+
{ cwd: repoRoot }
|
|
7271
|
+
).catch(() => "");
|
|
7272
|
+
const refs = refsRaw.split("\n").map((entry) => entry.trim()).filter(Boolean);
|
|
7273
|
+
const staleRefs = refs.slice(HEADER_GIT_RESET_HISTORY_REF_LIMIT);
|
|
7274
|
+
for (const refName of staleRefs) {
|
|
7275
|
+
await runCommand3("git", ["update-ref", "-d", refName], { cwd: repoRoot });
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
async function readGitHeaderState(cwd) {
|
|
7279
|
+
const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
7280
|
+
const currentBranchRaw = await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot });
|
|
7281
|
+
const currentBranch = currentBranchRaw.trim() || null;
|
|
7282
|
+
const headShaRaw = await runCommandCapture2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: gitRoot });
|
|
7283
|
+
const headCommitRaw = await runCommandCapture2("git", ["show", "-s", "--date=short", "--format=%cd%x09%s", "HEAD"], { cwd: gitRoot });
|
|
7284
|
+
const [headDate = "", ...headSubjectParts] = headCommitRaw.split(" ");
|
|
7285
|
+
const statusRaw = await runCommandCapture2("git", ["status", "--porcelain"], { cwd: gitRoot });
|
|
7286
|
+
return {
|
|
7287
|
+
currentBranch,
|
|
7288
|
+
headSha: headShaRaw.trim() || null,
|
|
7289
|
+
headSubject: headSubjectParts.join(" ").trim() || null,
|
|
7290
|
+
headDate: headDate.trim() || null,
|
|
7291
|
+
detached: !currentBranch,
|
|
7292
|
+
dirty: statusRaw.trim().length > 0,
|
|
7293
|
+
gitRoot
|
|
7294
|
+
};
|
|
7295
|
+
}
|
|
7296
|
+
async function assertNoTrackedGitChanges(repoRoot) {
|
|
7297
|
+
const statusRaw = await runCommandCapture2("git", ["status", "--porcelain"], { cwd: repoRoot });
|
|
7298
|
+
const trackedChanges = statusRaw.split("\n").map((line) => line.trimEnd()).filter((line) => line && !line.startsWith("?? "));
|
|
7299
|
+
if (trackedChanges.length > 0) {
|
|
7300
|
+
throw new Error("Cannot switch branches or reset with tracked uncommitted changes. Commit, stash, or discard tracked changes first. Untracked files are allowed unless Git would overwrite them.");
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
6473
7303
|
function extractBranchLockedWorktreePath(error, branchName) {
|
|
6474
7304
|
const message = getErrorMessage5(error, "");
|
|
6475
7305
|
if (!message || !branchName) return "";
|
|
@@ -6478,6 +7308,35 @@ function extractBranchLockedWorktreePath(error, branchName) {
|
|
|
6478
7308
|
const match = pattern.exec(message);
|
|
6479
7309
|
return match?.[1]?.trim() ?? "";
|
|
6480
7310
|
}
|
|
7311
|
+
function toPermanentWorktreeBranchNameDraft(worktreeName) {
|
|
7312
|
+
const sanitized = worktreeName.trim().replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/\.+/gu, ".").replace(/-+/gu, "-").replace(/^[.-]+|[.-]+$/gu, "");
|
|
7313
|
+
return sanitized || "worktree";
|
|
7314
|
+
}
|
|
7315
|
+
async function isValidGitBranchName(gitRoot, branchName) {
|
|
7316
|
+
try {
|
|
7317
|
+
await runCommand3("git", ["check-ref-format", "--branch", branchName], { cwd: gitRoot });
|
|
7318
|
+
return true;
|
|
7319
|
+
} catch {
|
|
7320
|
+
return false;
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7323
|
+
async function doesLocalGitBranchExist(gitRoot, branchName) {
|
|
7324
|
+
try {
|
|
7325
|
+
await runCommand3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: gitRoot });
|
|
7326
|
+
return true;
|
|
7327
|
+
} catch {
|
|
7328
|
+
return false;
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
async function allocatePermanentWorktreeBranchName(gitRoot, worktreeName) {
|
|
7332
|
+
const base = toPermanentWorktreeBranchNameDraft(worktreeName);
|
|
7333
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
7334
|
+
const candidate = attempt === 0 ? base : `${base}-${attempt + 1}`;
|
|
7335
|
+
if (!await isValidGitBranchName(gitRoot, candidate)) continue;
|
|
7336
|
+
if (!await doesLocalGitBranchExist(gitRoot, candidate)) return candidate;
|
|
7337
|
+
}
|
|
7338
|
+
throw new Error("Failed to allocate a unique branch name for worktree");
|
|
7339
|
+
}
|
|
6481
7340
|
function normalizeStringArray(value) {
|
|
6482
7341
|
if (!Array.isArray(value)) return [];
|
|
6483
7342
|
const normalized = [];
|
|
@@ -6498,9 +7357,133 @@ function normalizeStringRecord(value) {
|
|
|
6498
7357
|
}
|
|
6499
7358
|
return next;
|
|
6500
7359
|
}
|
|
7360
|
+
function normalizeRemoteProjects(value) {
|
|
7361
|
+
if (!Array.isArray(value)) return [];
|
|
7362
|
+
const next = [];
|
|
7363
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7364
|
+
for (const item of value) {
|
|
7365
|
+
const record = asRecord5(item);
|
|
7366
|
+
if (!record) continue;
|
|
7367
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
7368
|
+
if (!id || seen.has(id)) continue;
|
|
7369
|
+
seen.add(id);
|
|
7370
|
+
next.push({
|
|
7371
|
+
id,
|
|
7372
|
+
hostId: typeof record.hostId === "string" ? record.hostId.trim() : "",
|
|
7373
|
+
remotePath: typeof record.remotePath === "string" ? record.remotePath.trim() : "",
|
|
7374
|
+
label: typeof record.label === "string" ? record.label.trim() : ""
|
|
7375
|
+
});
|
|
7376
|
+
}
|
|
7377
|
+
return next;
|
|
7378
|
+
}
|
|
6501
7379
|
function getCodexAuthPath() {
|
|
6502
7380
|
return join6(getCodexHomeDir3(), "auth.json");
|
|
6503
7381
|
}
|
|
7382
|
+
var CODEX_CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
7383
|
+
var DEFAULT_CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
7384
|
+
function decodeBase64UrlJson2(value) {
|
|
7385
|
+
try {
|
|
7386
|
+
const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
|
|
7387
|
+
const decoded = Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
|
7388
|
+
const parsed = JSON.parse(decoded);
|
|
7389
|
+
return asRecord5(parsed);
|
|
7390
|
+
} catch {
|
|
7391
|
+
return null;
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
function decodeJwtPayload(token) {
|
|
7395
|
+
if (!token) return null;
|
|
7396
|
+
const parts = token.split(".");
|
|
7397
|
+
if (parts.length < 2) return null;
|
|
7398
|
+
return decodeBase64UrlJson2(parts[1] ?? "");
|
|
7399
|
+
}
|
|
7400
|
+
function extractChatgptTokenMetadata(accessToken) {
|
|
7401
|
+
const payload = decodeJwtPayload(accessToken);
|
|
7402
|
+
const auth = asRecord5(payload?.["https://api.openai.com/auth"]);
|
|
7403
|
+
return {
|
|
7404
|
+
chatgptAccountId: readNonEmptyString(auth?.chatgpt_account_id) || null,
|
|
7405
|
+
chatgptPlanType: readNonEmptyString(auth?.chatgpt_plan_type) || null
|
|
7406
|
+
};
|
|
7407
|
+
}
|
|
7408
|
+
function readTokenErrorMessage(payload, fallback) {
|
|
7409
|
+
const record = asRecord5(payload);
|
|
7410
|
+
const message = readNonEmptyString(record?.message);
|
|
7411
|
+
if (message) return message;
|
|
7412
|
+
const error = record?.error;
|
|
7413
|
+
if (typeof error === "string" && error.trim().length > 0) return error.trim();
|
|
7414
|
+
const nestedError = asRecord5(error);
|
|
7415
|
+
return readNonEmptyString(nestedError?.message) || readNonEmptyString(nestedError?.error_description) || readNonEmptyString(record?.error_description) || fallback;
|
|
7416
|
+
}
|
|
7417
|
+
function readTokenResponseString(payload, ...keys) {
|
|
7418
|
+
if (!payload) return null;
|
|
7419
|
+
for (const key of keys) {
|
|
7420
|
+
const value = readNonEmptyString(payload[key]);
|
|
7421
|
+
if (value) return value;
|
|
7422
|
+
}
|
|
7423
|
+
return null;
|
|
7424
|
+
}
|
|
7425
|
+
async function refreshChatgptAuthTokensForExternalAuth(params = {}) {
|
|
7426
|
+
const authPath = getCodexAuthPath();
|
|
7427
|
+
const raw = await readFile3(authPath, "utf8");
|
|
7428
|
+
const auth = JSON.parse(raw);
|
|
7429
|
+
const currentRefreshToken = auth.tokens?.refresh_token?.trim() ?? "";
|
|
7430
|
+
if (!currentRefreshToken) {
|
|
7431
|
+
throw new Error("No ChatGPT refresh token is available. Please sign in again.");
|
|
7432
|
+
}
|
|
7433
|
+
const refreshUrl = process.env.CODEX_REFRESH_TOKEN_URL_OVERRIDE?.trim() || DEFAULT_CODEX_REFRESH_TOKEN_URL;
|
|
7434
|
+
const body = new URLSearchParams({
|
|
7435
|
+
grant_type: "refresh_token",
|
|
7436
|
+
refresh_token: currentRefreshToken,
|
|
7437
|
+
client_id: CODEX_CHATGPT_CLIENT_ID
|
|
7438
|
+
});
|
|
7439
|
+
const response = await fetch(refreshUrl, {
|
|
7440
|
+
method: "POST",
|
|
7441
|
+
headers: {
|
|
7442
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7443
|
+
},
|
|
7444
|
+
body: body.toString(),
|
|
7445
|
+
signal: AbortSignal.timeout(25e3)
|
|
7446
|
+
});
|
|
7447
|
+
const text = await response.text();
|
|
7448
|
+
let payload = null;
|
|
7449
|
+
try {
|
|
7450
|
+
payload = asRecord5(JSON.parse(text));
|
|
7451
|
+
} catch {
|
|
7452
|
+
payload = null;
|
|
7453
|
+
}
|
|
7454
|
+
if (!response.ok) {
|
|
7455
|
+
throw new Error(readTokenErrorMessage(payload, `ChatGPT token refresh failed with HTTP ${String(response.status)}`));
|
|
7456
|
+
}
|
|
7457
|
+
const accessToken = readTokenResponseString(payload, "access_token", "accessToken");
|
|
7458
|
+
if (!accessToken) {
|
|
7459
|
+
throw new Error("ChatGPT token refresh response did not include an access token.");
|
|
7460
|
+
}
|
|
7461
|
+
const nextRefreshToken = readTokenResponseString(payload, "refresh_token", "refreshToken") ?? currentRefreshToken;
|
|
7462
|
+
const nextIdToken = readTokenResponseString(payload, "id_token", "idToken") ?? auth.tokens?.id_token;
|
|
7463
|
+
const metadata = extractChatgptTokenMetadata(accessToken);
|
|
7464
|
+
const chatgptAccountId = metadata.chatgptAccountId || readTokenResponseString(payload, "chatgpt_account_id", "chatgptAccountId") || readNonEmptyString(params.previousAccountId) || readNonEmptyString(auth.tokens?.account_id);
|
|
7465
|
+
if (!chatgptAccountId) {
|
|
7466
|
+
throw new Error("ChatGPT token refresh response did not include account metadata.");
|
|
7467
|
+
}
|
|
7468
|
+
const nextAuth = {
|
|
7469
|
+
...auth,
|
|
7470
|
+
auth_mode: auth.auth_mode || "chatgpt",
|
|
7471
|
+
last_refresh: Date.now(),
|
|
7472
|
+
tokens: {
|
|
7473
|
+
...auth.tokens,
|
|
7474
|
+
access_token: accessToken,
|
|
7475
|
+
refresh_token: nextRefreshToken,
|
|
7476
|
+
account_id: chatgptAccountId,
|
|
7477
|
+
...nextIdToken ? { id_token: nextIdToken } : {}
|
|
7478
|
+
}
|
|
7479
|
+
};
|
|
7480
|
+
await writeFile4(authPath, JSON.stringify(nextAuth, null, 2), { encoding: "utf8", mode: 384 });
|
|
7481
|
+
return {
|
|
7482
|
+
accessToken,
|
|
7483
|
+
chatgptAccountId,
|
|
7484
|
+
chatgptPlanType: metadata.chatgptPlanType
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
6504
7487
|
async function readCodexAuth() {
|
|
6505
7488
|
try {
|
|
6506
7489
|
const raw = await readFile3(getCodexAuthPath(), "utf8");
|
|
@@ -6512,6 +7495,37 @@ async function readCodexAuth() {
|
|
|
6512
7495
|
return null;
|
|
6513
7496
|
}
|
|
6514
7497
|
}
|
|
7498
|
+
function hasUsableCodexAuthSync() {
|
|
7499
|
+
try {
|
|
7500
|
+
const raw = readFileSync2(getCodexAuthPath(), "utf8");
|
|
7501
|
+
const auth = JSON.parse(raw);
|
|
7502
|
+
return Boolean(auth.tokens?.access_token?.trim());
|
|
7503
|
+
} catch {
|
|
7504
|
+
return false;
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
function readFreeModeStateSync(statePath) {
|
|
7508
|
+
try {
|
|
7509
|
+
return JSON.parse(readFileSync2(statePath, "utf8"));
|
|
7510
|
+
} catch {
|
|
7511
|
+
return null;
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
function ensureDefaultFreeModeStateForMissingAuthSync(statePath) {
|
|
7515
|
+
const current = readFreeModeStateSync(statePath);
|
|
7516
|
+
if (!shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuthSync())) {
|
|
7517
|
+
return current;
|
|
7518
|
+
}
|
|
7519
|
+
const fallback = createDefaultOpenCodeZenFreeModeState();
|
|
7520
|
+
mkdirSync(dirname2(statePath), { recursive: true });
|
|
7521
|
+
writeFileSync2(statePath, JSON.stringify(fallback), { encoding: "utf8", mode: 384 });
|
|
7522
|
+
return fallback;
|
|
7523
|
+
}
|
|
7524
|
+
function isLoopbackRemoteAddress(remoteAddress) {
|
|
7525
|
+
if (!remoteAddress) return false;
|
|
7526
|
+
const normalized = remoteAddress.startsWith("::ffff:") ? remoteAddress.slice("::ffff:".length) : remoteAddress;
|
|
7527
|
+
return normalized === "127.0.0.1" || normalized === "::1";
|
|
7528
|
+
}
|
|
6515
7529
|
function getCodexGlobalStatePath() {
|
|
6516
7530
|
return join6(getCodexHomeDir3(), ".codex-global-state.json");
|
|
6517
7531
|
}
|
|
@@ -6615,13 +7629,35 @@ async function listThreadHeartbeatAutomations() {
|
|
|
6615
7629
|
if (!entry.isDirectory()) continue;
|
|
6616
7630
|
const automation = await readAutomationRecordFromFile(join6(automationRoot, entry.name, "automation.toml"));
|
|
6617
7631
|
if (!automation || automation.kind !== "heartbeat" || !automation.targetThreadId) continue;
|
|
6618
|
-
next[automation.targetThreadId] = automation;
|
|
7632
|
+
next[automation.targetThreadId] = [...next[automation.targetThreadId] ?? [], automation];
|
|
7633
|
+
}
|
|
7634
|
+
for (const automations of Object.values(next)) {
|
|
7635
|
+
automations.sort((first, second) => {
|
|
7636
|
+
const firstCreatedAt = first.createdAtMs ?? 0;
|
|
7637
|
+
const secondCreatedAt = second.createdAtMs ?? 0;
|
|
7638
|
+
if (firstCreatedAt !== secondCreatedAt) return firstCreatedAt - secondCreatedAt;
|
|
7639
|
+
return first.id.localeCompare(second.id);
|
|
7640
|
+
});
|
|
6619
7641
|
}
|
|
6620
7642
|
return next;
|
|
6621
7643
|
}
|
|
6622
|
-
async function
|
|
7644
|
+
async function readThreadHeartbeatAutomations(threadId) {
|
|
6623
7645
|
const all = await listThreadHeartbeatAutomations();
|
|
6624
|
-
return all[threadId] ??
|
|
7646
|
+
return all[threadId] ?? [];
|
|
7647
|
+
}
|
|
7648
|
+
async function readThreadHeartbeatAutomation(threadId, automationId = "") {
|
|
7649
|
+
const automations = await readThreadHeartbeatAutomations(threadId);
|
|
7650
|
+
if (automationId) return automations.find((automation) => automation.id === automationId) ?? null;
|
|
7651
|
+
return automations[0] ?? null;
|
|
7652
|
+
}
|
|
7653
|
+
function resolveUniqueAutomationId(existingIds, threadId, name) {
|
|
7654
|
+
const baseId = slugifyAutomationId(threadId, name);
|
|
7655
|
+
if (!existingIds.has(baseId)) return baseId;
|
|
7656
|
+
for (let index = 2; index < 1e3; index += 1) {
|
|
7657
|
+
const candidate = `${baseId}-${index}`;
|
|
7658
|
+
if (!existingIds.has(candidate)) return candidate;
|
|
7659
|
+
}
|
|
7660
|
+
return `${baseId}-${randomBytes(4).toString("hex")}`;
|
|
6625
7661
|
}
|
|
6626
7662
|
async function writeThreadHeartbeatAutomation(input) {
|
|
6627
7663
|
const threadId = input.threadId.trim();
|
|
@@ -6633,8 +7669,10 @@ async function writeThreadHeartbeatAutomation(input) {
|
|
|
6633
7669
|
}
|
|
6634
7670
|
const automationRoot = getCodexAutomationsDir();
|
|
6635
7671
|
await mkdir4(automationRoot, { recursive: true });
|
|
6636
|
-
const existing = await readThreadHeartbeatAutomation(threadId);
|
|
6637
|
-
const
|
|
7672
|
+
const existing = input.id ? await readThreadHeartbeatAutomation(threadId, input.id.trim()) : null;
|
|
7673
|
+
const entries = await readdir2(automationRoot, { withFileTypes: true }).catch(() => []);
|
|
7674
|
+
const existingIds = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
|
|
7675
|
+
const id = existing?.id ?? resolveUniqueAutomationId(existingIds, threadId, name);
|
|
6638
7676
|
const automationDir = join6(automationRoot, id);
|
|
6639
7677
|
const now = Date.now();
|
|
6640
7678
|
const record = {
|
|
@@ -6659,10 +7697,18 @@ async function writeThreadHeartbeatAutomation(input) {
|
|
|
6659
7697
|
}
|
|
6660
7698
|
return record;
|
|
6661
7699
|
}
|
|
6662
|
-
async function deleteThreadHeartbeatAutomation(threadId) {
|
|
6663
|
-
const
|
|
6664
|
-
|
|
6665
|
-
|
|
7700
|
+
async function deleteThreadHeartbeatAutomation(threadId, automationId = "") {
|
|
7701
|
+
const normalizedThreadId = threadId.trim();
|
|
7702
|
+
const normalizedAutomationId = automationId.trim();
|
|
7703
|
+
if (normalizedAutomationId) {
|
|
7704
|
+
const automation = await readThreadHeartbeatAutomation(normalizedThreadId, normalizedAutomationId);
|
|
7705
|
+
if (!automation) return false;
|
|
7706
|
+
await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
|
|
7707
|
+
return true;
|
|
7708
|
+
}
|
|
7709
|
+
const automations = await readThreadHeartbeatAutomations(normalizedThreadId);
|
|
7710
|
+
if (automations.length === 0) return false;
|
|
7711
|
+
await Promise.all(automations.map((automation) => rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })));
|
|
6666
7712
|
return true;
|
|
6667
7713
|
}
|
|
6668
7714
|
var MAX_THREAD_TITLES = 500;
|
|
@@ -6840,6 +7886,7 @@ function normalizeThreadQueueState(value) {
|
|
|
6840
7886
|
}
|
|
6841
7887
|
return state;
|
|
6842
7888
|
}
|
|
7889
|
+
var threadQueueMutationChain = Promise.resolve();
|
|
6843
7890
|
async function readThreadQueueState() {
|
|
6844
7891
|
const statePath = getCodexGlobalStatePath();
|
|
6845
7892
|
try {
|
|
@@ -6850,7 +7897,7 @@ async function readThreadQueueState() {
|
|
|
6850
7897
|
return {};
|
|
6851
7898
|
}
|
|
6852
7899
|
}
|
|
6853
|
-
async function
|
|
7900
|
+
async function writeThreadQueueStateUnlocked(nextState) {
|
|
6854
7901
|
const statePath = getCodexGlobalStatePath();
|
|
6855
7902
|
let payload = {};
|
|
6856
7903
|
try {
|
|
@@ -6867,6 +7914,107 @@ async function writeThreadQueueState(nextState) {
|
|
|
6867
7914
|
}
|
|
6868
7915
|
await writeFile4(statePath, JSON.stringify(payload), "utf8");
|
|
6869
7916
|
}
|
|
7917
|
+
async function withThreadQueueStateUpdate(update) {
|
|
7918
|
+
const run = threadQueueMutationChain.then(async () => {
|
|
7919
|
+
const currentState = await readThreadQueueState();
|
|
7920
|
+
const { nextState, result } = await update(currentState);
|
|
7921
|
+
await writeThreadQueueStateUnlocked(nextState);
|
|
7922
|
+
return result;
|
|
7923
|
+
});
|
|
7924
|
+
threadQueueMutationChain = run.catch(() => {
|
|
7925
|
+
});
|
|
7926
|
+
return run;
|
|
7927
|
+
}
|
|
7928
|
+
async function writeThreadQueueState(nextState) {
|
|
7929
|
+
await withThreadQueueStateUpdate(() => ({
|
|
7930
|
+
nextState: normalizeThreadQueueState(nextState),
|
|
7931
|
+
result: void 0
|
|
7932
|
+
}));
|
|
7933
|
+
}
|
|
7934
|
+
async function appendThreadQueuedMessage(threadId, message) {
|
|
7935
|
+
const normalizedThreadId = threadId.trim();
|
|
7936
|
+
if (!normalizedThreadId) throw new Error("threadId is required");
|
|
7937
|
+
await withThreadQueueStateUpdate((state) => ({
|
|
7938
|
+
nextState: {
|
|
7939
|
+
...state,
|
|
7940
|
+
[normalizedThreadId]: [...state[normalizedThreadId] ?? [], message]
|
|
7941
|
+
},
|
|
7942
|
+
result: void 0
|
|
7943
|
+
}));
|
|
7944
|
+
}
|
|
7945
|
+
function normalizeReasoningEffort(value) {
|
|
7946
|
+
const allowed = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
7947
|
+
return typeof value === "string" && allowed.includes(value) ? value : "";
|
|
7948
|
+
}
|
|
7949
|
+
function normalizeCollaborationModeReasoningEffort(value) {
|
|
7950
|
+
return value && value.length > 0 ? value : null;
|
|
7951
|
+
}
|
|
7952
|
+
function extractLocalImagePathFromUrl(value) {
|
|
7953
|
+
if (!value) return null;
|
|
7954
|
+
try {
|
|
7955
|
+
const parsed = new URL(value, "http://localhost");
|
|
7956
|
+
if (parsed.pathname !== "/codex-local-image") return null;
|
|
7957
|
+
const path = parsed.searchParams.get("path")?.trim() ?? "";
|
|
7958
|
+
return path.length > 0 ? path : null;
|
|
7959
|
+
} catch {
|
|
7960
|
+
return null;
|
|
7961
|
+
}
|
|
7962
|
+
}
|
|
7963
|
+
function buildTextWithAttachments(prompt, files) {
|
|
7964
|
+
if (files.length === 0) return prompt;
|
|
7965
|
+
let prefix = "# Files mentioned by the user:\n";
|
|
7966
|
+
for (const f of files) {
|
|
7967
|
+
prefix += `
|
|
7968
|
+
## ${f.label}: ${f.path}
|
|
7969
|
+
`;
|
|
7970
|
+
}
|
|
7971
|
+
return `${prefix}
|
|
7972
|
+
## My request for Codex:
|
|
7973
|
+
|
|
7974
|
+
${prompt}
|
|
7975
|
+
`;
|
|
7976
|
+
}
|
|
7977
|
+
function escapeHeartbeatXmlText(value) {
|
|
7978
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">");
|
|
7979
|
+
}
|
|
7980
|
+
function buildHeartbeatQueuedMessage(automation) {
|
|
7981
|
+
return {
|
|
7982
|
+
id: `automation-${automation.id}-${Date.now()}-${randomBytes(3).toString("hex")}`,
|
|
7983
|
+
text: `<heartbeat>
|
|
7984
|
+
<automation_id>${escapeHeartbeatXmlText(automation.id)}</automation_id>
|
|
7985
|
+
<current_time_iso>${(/* @__PURE__ */ new Date()).toISOString()}</current_time_iso>
|
|
7986
|
+
<instructions>
|
|
7987
|
+
${escapeHeartbeatXmlText(automation.prompt)}
|
|
7988
|
+
</instructions>
|
|
7989
|
+
</heartbeat>`,
|
|
7990
|
+
imageUrls: [],
|
|
7991
|
+
skills: [],
|
|
7992
|
+
fileAttachments: [],
|
|
7993
|
+
collaborationMode: "default"
|
|
7994
|
+
};
|
|
7995
|
+
}
|
|
7996
|
+
function fileNameFromPath(pathValue) {
|
|
7997
|
+
const normalized = pathValue.replace(/\\/g, "/");
|
|
7998
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
7999
|
+
return segments.at(-1) ?? normalized;
|
|
8000
|
+
}
|
|
8001
|
+
function extractThreadIdFromNotificationParams(params) {
|
|
8002
|
+
const record = asRecord5(params);
|
|
8003
|
+
if (!record) return "";
|
|
8004
|
+
const threadId = (typeof record.threadId === "string" ? record.threadId : "") || (typeof record.thread_id === "string" ? record.thread_id : "") || (typeof record.conversationId === "string" ? record.conversationId : "") || (typeof record.conversation_id === "string" ? record.conversation_id : "");
|
|
8005
|
+
if (threadId) return threadId;
|
|
8006
|
+
const thread = asRecord5(record.thread);
|
|
8007
|
+
if (thread && typeof thread.id === "string") return thread.id;
|
|
8008
|
+
const turn = asRecord5(record.turn);
|
|
8009
|
+
if (turn) {
|
|
8010
|
+
const turnThreadId = (typeof turn.threadId === "string" ? turn.threadId : "") || (typeof turn.thread_id === "string" ? turn.thread_id : "");
|
|
8011
|
+
if (turnThreadId) return turnThreadId;
|
|
8012
|
+
}
|
|
8013
|
+
return "";
|
|
8014
|
+
}
|
|
8015
|
+
function isTurnCompletedNotification(notification) {
|
|
8016
|
+
return notification.method === "turn/completed";
|
|
8017
|
+
}
|
|
6870
8018
|
async function readFirstLaunchPluginsCardDismissed() {
|
|
6871
8019
|
const statePath = getCodexGlobalStatePath();
|
|
6872
8020
|
try {
|
|
@@ -6965,22 +8113,74 @@ async function readWorkspaceRootsState() {
|
|
|
6965
8113
|
return {
|
|
6966
8114
|
order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
|
|
6967
8115
|
labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
|
|
6968
|
-
active: normalizeStringArray(payload["active-workspace-roots"])
|
|
8116
|
+
active: normalizeStringArray(payload["active-workspace-roots"]),
|
|
8117
|
+
projectOrder: normalizeStringArray(payload["project-order"]),
|
|
8118
|
+
remoteProjects: normalizeRemoteProjects(payload["remote-projects"])
|
|
6969
8119
|
};
|
|
6970
8120
|
}
|
|
6971
8121
|
async function writeWorkspaceRootsState(nextState) {
|
|
6972
8122
|
const statePath = getCodexGlobalStatePath();
|
|
6973
8123
|
let payload = {};
|
|
6974
8124
|
try {
|
|
6975
|
-
const raw = await readFile3(statePath, "utf8");
|
|
6976
|
-
payload = asRecord5(JSON.parse(raw)) ?? {};
|
|
8125
|
+
const raw = await readFile3(statePath, "utf8");
|
|
8126
|
+
payload = asRecord5(JSON.parse(raw)) ?? {};
|
|
8127
|
+
} catch {
|
|
8128
|
+
payload = {};
|
|
8129
|
+
}
|
|
8130
|
+
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
8131
|
+
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
8132
|
+
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
8133
|
+
payload["project-order"] = normalizeStringArray(nextState.projectOrder);
|
|
8134
|
+
await writeFile4(statePath, JSON.stringify(payload), "utf8");
|
|
8135
|
+
}
|
|
8136
|
+
var workspaceRootsMutation = Promise.resolve();
|
|
8137
|
+
function queueWorkspaceRootsMutation(mutation) {
|
|
8138
|
+
const run = workspaceRootsMutation.catch(() => void 0).then(mutation);
|
|
8139
|
+
workspaceRootsMutation = run.then(
|
|
8140
|
+
() => void 0,
|
|
8141
|
+
() => void 0
|
|
8142
|
+
);
|
|
8143
|
+
return run;
|
|
8144
|
+
}
|
|
8145
|
+
function prependUniqueString(value, items) {
|
|
8146
|
+
return [value, ...items.filter((item) => item !== value)];
|
|
8147
|
+
}
|
|
8148
|
+
async function updateWorkspaceRootsState(updater) {
|
|
8149
|
+
await queueWorkspaceRootsMutation(async () => {
|
|
8150
|
+
const existingState = await readWorkspaceRootsState();
|
|
8151
|
+
await writeWorkspaceRootsState(updater(existingState));
|
|
8152
|
+
});
|
|
8153
|
+
}
|
|
8154
|
+
async function persistWorkspaceRoot(workspaceRoot, label = "") {
|
|
8155
|
+
const normalizedRoot = workspaceRoot.trim();
|
|
8156
|
+
if (!normalizedRoot) return;
|
|
8157
|
+
await updateWorkspaceRootsState((existingState) => {
|
|
8158
|
+
const nextLabels = { ...existingState.labels };
|
|
8159
|
+
const trimmedLabel = label.trim();
|
|
8160
|
+
if (trimmedLabel.length > 0) {
|
|
8161
|
+
nextLabels[normalizedRoot] = trimmedLabel;
|
|
8162
|
+
}
|
|
8163
|
+
return {
|
|
8164
|
+
order: prependUniqueString(normalizedRoot, existingState.order),
|
|
8165
|
+
labels: nextLabels,
|
|
8166
|
+
active: prependUniqueString(normalizedRoot, existingState.active),
|
|
8167
|
+
projectOrder: prependUniqueString(normalizedRoot, existingState.projectOrder),
|
|
8168
|
+
remoteProjects: existingState.remoteProjects
|
|
8169
|
+
};
|
|
8170
|
+
});
|
|
8171
|
+
}
|
|
8172
|
+
async function rollbackCreatedWorktree(gitRoot, worktreeCwd, cleanupDirectory, branchName) {
|
|
8173
|
+
try {
|
|
8174
|
+
await runCommand3("git", ["worktree", "remove", "--force", worktreeCwd], { cwd: gitRoot });
|
|
6977
8175
|
} catch {
|
|
6978
|
-
|
|
8176
|
+
await rm4(worktreeCwd, { recursive: true, force: true }).catch(() => void 0);
|
|
8177
|
+
}
|
|
8178
|
+
if (cleanupDirectory && cleanupDirectory !== worktreeCwd) {
|
|
8179
|
+
await rm4(cleanupDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
8180
|
+
}
|
|
8181
|
+
if (branchName) {
|
|
8182
|
+
await runCommand3("git", ["branch", "-D", branchName], { cwd: gitRoot }).catch(() => void 0);
|
|
6979
8183
|
}
|
|
6980
|
-
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
6981
|
-
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
6982
|
-
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
6983
|
-
await writeFile4(statePath, JSON.stringify(payload), "utf8");
|
|
6984
8184
|
}
|
|
6985
8185
|
function normalizeTelegramBridgeConfig(value) {
|
|
6986
8186
|
const record = asRecord5(value);
|
|
@@ -7037,7 +8237,7 @@ function rememberTelegramChatId(chatId) {
|
|
|
7037
8237
|
});
|
|
7038
8238
|
return telegramBridgeConfigMutation;
|
|
7039
8239
|
}
|
|
7040
|
-
async function
|
|
8240
|
+
async function readJsonBody2(req) {
|
|
7041
8241
|
const raw = await readRawBody(req);
|
|
7042
8242
|
if (raw.length === 0) return null;
|
|
7043
8243
|
const text = raw.toString("utf8").trim();
|
|
@@ -7255,6 +8455,7 @@ var AppServerProcess = class {
|
|
|
7255
8455
|
this.lastThreadReadSnapshotByThreadId = /* @__PURE__ */ new Map();
|
|
7256
8456
|
this.capturedItemsByThreadId = /* @__PURE__ */ new Map();
|
|
7257
8457
|
this.liveStateCache = /* @__PURE__ */ new Map();
|
|
8458
|
+
this.chatgptAuthRefreshPromise = null;
|
|
7258
8459
|
}
|
|
7259
8460
|
getCodexCommand() {
|
|
7260
8461
|
const codexCommand = resolveCodexCommand();
|
|
@@ -7275,10 +8476,11 @@ var AppServerProcess = class {
|
|
|
7275
8476
|
const serverPort = parseInt(process.env.CODEXUI_SERVER_PORT ?? "", 10) || void 0;
|
|
7276
8477
|
const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
|
|
7277
8478
|
try {
|
|
7278
|
-
const
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
8479
|
+
const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
|
|
8480
|
+
if (state) {
|
|
8481
|
+
args.push(...getFreeModeConfigArgs(state, serverPort));
|
|
8482
|
+
extraEnv = getFreeModeEnvVars(state);
|
|
8483
|
+
}
|
|
7282
8484
|
} catch {
|
|
7283
8485
|
}
|
|
7284
8486
|
return { args, env: extraEnv };
|
|
@@ -7517,7 +8719,46 @@ var AppServerProcess = class {
|
|
|
7517
8719
|
}
|
|
7518
8720
|
});
|
|
7519
8721
|
}
|
|
8722
|
+
async refreshChatgptAuthTokens(params) {
|
|
8723
|
+
if (!this.chatgptAuthRefreshPromise) {
|
|
8724
|
+
this.chatgptAuthRefreshPromise = refreshChatgptAuthTokensForExternalAuth(params).finally(() => {
|
|
8725
|
+
this.chatgptAuthRefreshPromise = null;
|
|
8726
|
+
});
|
|
8727
|
+
}
|
|
8728
|
+
return await this.chatgptAuthRefreshPromise;
|
|
8729
|
+
}
|
|
8730
|
+
async handleChatgptAuthTokensRefreshRequest(requestId, params) {
|
|
8731
|
+
const requestParams = asRecord5(params);
|
|
8732
|
+
const previousAccountId = readNonEmptyString(requestParams?.previousAccountId ?? requestParams?.previous_account_id);
|
|
8733
|
+
try {
|
|
8734
|
+
const result = await this.refreshChatgptAuthTokens({
|
|
8735
|
+
reason: readNonEmptyString(requestParams?.reason) || void 0,
|
|
8736
|
+
previousAccountId: previousAccountId || void 0
|
|
8737
|
+
});
|
|
8738
|
+
this.sendServerRequestReply(requestId, { result });
|
|
8739
|
+
this.emitNotification({
|
|
8740
|
+
method: "server/request/resolved",
|
|
8741
|
+
params: {
|
|
8742
|
+
id: requestId,
|
|
8743
|
+
method: "account/chatgptAuthTokens/refresh",
|
|
8744
|
+
mode: "automatic",
|
|
8745
|
+
resolvedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
8746
|
+
}
|
|
8747
|
+
});
|
|
8748
|
+
} catch (error) {
|
|
8749
|
+
this.sendServerRequestReply(requestId, {
|
|
8750
|
+
error: {
|
|
8751
|
+
code: -32001,
|
|
8752
|
+
message: getErrorMessage5(error, "Failed to refresh ChatGPT auth tokens")
|
|
8753
|
+
}
|
|
8754
|
+
});
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
7520
8757
|
handleServerRequest(requestId, method, params) {
|
|
8758
|
+
if (method === "account/chatgptAuthTokens/refresh") {
|
|
8759
|
+
void this.handleChatgptAuthTokensRefreshRequest(requestId, params);
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
7521
8762
|
const pendingRequest = {
|
|
7522
8763
|
id: requestId,
|
|
7523
8764
|
method,
|
|
@@ -7636,6 +8877,218 @@ var AppServerProcess = class {
|
|
|
7636
8877
|
forceKillTimer.unref();
|
|
7637
8878
|
}
|
|
7638
8879
|
};
|
|
8880
|
+
var BackendQueueProcessor = class {
|
|
8881
|
+
constructor(appServer) {
|
|
8882
|
+
this.appServer = appServer;
|
|
8883
|
+
this.processingThreadIds = /* @__PURE__ */ new Set();
|
|
8884
|
+
this.queueDrainTimersByThreadId = /* @__PURE__ */ new Map();
|
|
8885
|
+
this.queueDrainDueAtByThreadId = /* @__PURE__ */ new Map();
|
|
8886
|
+
this.unsubscribe = appServer.onNotification((notification) => {
|
|
8887
|
+
if (!isTurnCompletedNotification(notification)) return;
|
|
8888
|
+
const threadId = extractThreadIdFromNotificationParams(notification.params);
|
|
8889
|
+
if (!threadId) return;
|
|
8890
|
+
void this.processThreadQueue(threadId);
|
|
8891
|
+
});
|
|
8892
|
+
void this.scheduleAllQueuedThreads(1e3);
|
|
8893
|
+
}
|
|
8894
|
+
dispose() {
|
|
8895
|
+
this.unsubscribe();
|
|
8896
|
+
for (const timer of this.queueDrainTimersByThreadId.values()) {
|
|
8897
|
+
clearTimeout(timer);
|
|
8898
|
+
}
|
|
8899
|
+
this.queueDrainTimersByThreadId.clear();
|
|
8900
|
+
this.queueDrainDueAtByThreadId.clear();
|
|
8901
|
+
this.processingThreadIds.clear();
|
|
8902
|
+
}
|
|
8903
|
+
async scheduleAllQueuedThreads(delayMs = 0) {
|
|
8904
|
+
try {
|
|
8905
|
+
const state = await readThreadQueueState();
|
|
8906
|
+
for (const threadId of Object.keys(state)) {
|
|
8907
|
+
this.scheduleThreadQueueDrain(threadId, delayMs);
|
|
8908
|
+
}
|
|
8909
|
+
} catch {
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
scheduleThreadQueueDrain(threadId, delayMs = 5e3) {
|
|
8913
|
+
if (!threadId) return;
|
|
8914
|
+
const normalizedDelayMs = Math.max(0, delayMs);
|
|
8915
|
+
const nextDueAt = Date.now() + normalizedDelayMs;
|
|
8916
|
+
const existingDueAt = this.queueDrainDueAtByThreadId.get(threadId);
|
|
8917
|
+
const existingTimer = this.queueDrainTimersByThreadId.get(threadId);
|
|
8918
|
+
if (existingTimer) {
|
|
8919
|
+
if (existingDueAt !== void 0 && existingDueAt <= nextDueAt) return;
|
|
8920
|
+
clearTimeout(existingTimer);
|
|
8921
|
+
this.queueDrainTimersByThreadId.delete(threadId);
|
|
8922
|
+
this.queueDrainDueAtByThreadId.delete(threadId);
|
|
8923
|
+
}
|
|
8924
|
+
const timer = setTimeout(() => {
|
|
8925
|
+
this.queueDrainTimersByThreadId.delete(threadId);
|
|
8926
|
+
this.queueDrainDueAtByThreadId.delete(threadId);
|
|
8927
|
+
void this.processThreadQueue(threadId);
|
|
8928
|
+
}, normalizedDelayMs);
|
|
8929
|
+
timer.unref?.();
|
|
8930
|
+
this.queueDrainTimersByThreadId.set(threadId, timer);
|
|
8931
|
+
this.queueDrainDueAtByThreadId.set(threadId, nextDueAt);
|
|
8932
|
+
}
|
|
8933
|
+
async processThreadQueue(threadId) {
|
|
8934
|
+
if (this.processingThreadIds.has(threadId)) return;
|
|
8935
|
+
this.processingThreadIds.add(threadId);
|
|
8936
|
+
try {
|
|
8937
|
+
const canStart = await this.canStartQueuedTurn(threadId);
|
|
8938
|
+
if (!canStart) {
|
|
8939
|
+
if (await this.hasQueuedTurns(threadId)) {
|
|
8940
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
8941
|
+
}
|
|
8942
|
+
return;
|
|
8943
|
+
}
|
|
8944
|
+
const next = await this.popNextQueuedTurn(threadId);
|
|
8945
|
+
if (!next) return;
|
|
8946
|
+
try {
|
|
8947
|
+
await this.startQueuedTurn(next);
|
|
8948
|
+
if (await this.hasQueuedTurns(threadId)) {
|
|
8949
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
8950
|
+
}
|
|
8951
|
+
} catch {
|
|
8952
|
+
await this.restoreQueuedTurn(next);
|
|
8953
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
8954
|
+
}
|
|
8955
|
+
} catch {
|
|
8956
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
8957
|
+
} finally {
|
|
8958
|
+
this.processingThreadIds.delete(threadId);
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
async hasQueuedTurns(threadId) {
|
|
8962
|
+
const state = await readThreadQueueState();
|
|
8963
|
+
const queue = state[threadId];
|
|
8964
|
+
return Array.isArray(queue) && queue.length > 0;
|
|
8965
|
+
}
|
|
8966
|
+
async canStartQueuedTurn(threadId) {
|
|
8967
|
+
const response = asRecord5(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
8968
|
+
const thread = asRecord5(response?.thread);
|
|
8969
|
+
if (!thread) return false;
|
|
8970
|
+
const status = asRecord5(thread.status);
|
|
8971
|
+
const statusType = readNonEmptyString(status?.type);
|
|
8972
|
+
if (statusType === "inProgress" || statusType === "running" || statusType === "active") return false;
|
|
8973
|
+
const turns = Array.isArray(thread.turns) ? thread.turns : [];
|
|
8974
|
+
return !turns.some((turn) => readNonEmptyString(asRecord5(turn)?.status) === "inProgress");
|
|
8975
|
+
}
|
|
8976
|
+
async popNextQueuedTurn(threadId) {
|
|
8977
|
+
return withThreadQueueStateUpdate((state) => {
|
|
8978
|
+
const queue = state[threadId];
|
|
8979
|
+
if (!queue || queue.length === 0) {
|
|
8980
|
+
return { nextState: state, result: null };
|
|
8981
|
+
}
|
|
8982
|
+
const [message, ...rest] = queue;
|
|
8983
|
+
const nextState = { ...state };
|
|
8984
|
+
if (rest.length > 0) {
|
|
8985
|
+
nextState[threadId] = rest;
|
|
8986
|
+
} else {
|
|
8987
|
+
delete nextState[threadId];
|
|
8988
|
+
}
|
|
8989
|
+
return { nextState, result: { threadId, message } };
|
|
8990
|
+
});
|
|
8991
|
+
}
|
|
8992
|
+
async restoreQueuedTurn(turn) {
|
|
8993
|
+
await withThreadQueueStateUpdate((state) => {
|
|
8994
|
+
const queue = state[turn.threadId] ?? [];
|
|
8995
|
+
return {
|
|
8996
|
+
nextState: {
|
|
8997
|
+
...state,
|
|
8998
|
+
[turn.threadId]: [turn.message, ...queue]
|
|
8999
|
+
},
|
|
9000
|
+
result: void 0
|
|
9001
|
+
};
|
|
9002
|
+
});
|
|
9003
|
+
}
|
|
9004
|
+
async resolveCollaborationModeSettings(mode) {
|
|
9005
|
+
let currentConfig = null;
|
|
9006
|
+
try {
|
|
9007
|
+
const configPayload = asRecord5(await this.appServer.rpc("config/read", {}));
|
|
9008
|
+
currentConfig = asRecord5(configPayload?.config);
|
|
9009
|
+
} catch {
|
|
9010
|
+
currentConfig = null;
|
|
9011
|
+
}
|
|
9012
|
+
const configuredModel = readNonEmptyString(currentConfig?.model);
|
|
9013
|
+
if (configuredModel) {
|
|
9014
|
+
return {
|
|
9015
|
+
model: configuredModel,
|
|
9016
|
+
reasoningEffort: normalizeCollaborationModeReasoningEffort(normalizeReasoningEffort(currentConfig?.model_reasoning_effort))
|
|
9017
|
+
};
|
|
9018
|
+
}
|
|
9019
|
+
try {
|
|
9020
|
+
const modelsPayload = asRecord5(await this.appServer.rpc("model/list", {}));
|
|
9021
|
+
const models = Array.isArray(modelsPayload?.data) ? modelsPayload.data : [];
|
|
9022
|
+
for (const row of models) {
|
|
9023
|
+
const record = asRecord5(row);
|
|
9024
|
+
const candidate = readNonEmptyString(record?.id) || readNonEmptyString(record?.model);
|
|
9025
|
+
if (candidate) {
|
|
9026
|
+
return {
|
|
9027
|
+
model: candidate,
|
|
9028
|
+
reasoningEffort: normalizeCollaborationModeReasoningEffort(normalizeReasoningEffort(currentConfig?.model_reasoning_effort))
|
|
9029
|
+
};
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
} catch {
|
|
9033
|
+
}
|
|
9034
|
+
throw new Error(`${mode === "plan" ? "Plan" : "Default"} mode requires an available model.`);
|
|
9035
|
+
}
|
|
9036
|
+
async buildQueuedTurnParams(turn) {
|
|
9037
|
+
const localImageAttachments = [];
|
|
9038
|
+
for (const imageUrl of turn.message.imageUrls) {
|
|
9039
|
+
const localImagePath = extractLocalImagePathFromUrl(imageUrl.trim());
|
|
9040
|
+
if (!localImagePath) continue;
|
|
9041
|
+
localImageAttachments.push({
|
|
9042
|
+
label: fileNameFromPath(localImagePath),
|
|
9043
|
+
path: localImagePath,
|
|
9044
|
+
fsPath: localImagePath
|
|
9045
|
+
});
|
|
9046
|
+
}
|
|
9047
|
+
const allFileAttachments = [...turn.message.fileAttachments, ...localImageAttachments];
|
|
9048
|
+
const dedupedFileAttachments = allFileAttachments.filter((entry, index) => allFileAttachments.findIndex((candidate) => candidate.fsPath === entry.fsPath) === index);
|
|
9049
|
+
const input = [{
|
|
9050
|
+
type: "text",
|
|
9051
|
+
text: buildTextWithAttachments(turn.message.text, dedupedFileAttachments)
|
|
9052
|
+
}];
|
|
9053
|
+
for (const imageUrl of turn.message.imageUrls) {
|
|
9054
|
+
const normalizedUrl = imageUrl.trim();
|
|
9055
|
+
if (!normalizedUrl) continue;
|
|
9056
|
+
const localImagePath = extractLocalImagePathFromUrl(normalizedUrl);
|
|
9057
|
+
if (localImagePath) {
|
|
9058
|
+
input.push({ type: "localImage", path: localImagePath });
|
|
9059
|
+
} else {
|
|
9060
|
+
input.push({ type: "image", url: normalizedUrl, image_url: normalizedUrl });
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
for (const skill of turn.message.skills) {
|
|
9064
|
+
input.push({ type: "skill", name: skill.name, path: skill.path });
|
|
9065
|
+
}
|
|
9066
|
+
const params = {
|
|
9067
|
+
threadId: turn.threadId,
|
|
9068
|
+
input
|
|
9069
|
+
};
|
|
9070
|
+
if (dedupedFileAttachments.length > 0) {
|
|
9071
|
+
params.attachments = dedupedFileAttachments.map((f) => ({ label: f.label, path: f.path, fsPath: f.fsPath }));
|
|
9072
|
+
}
|
|
9073
|
+
try {
|
|
9074
|
+
const settings = await this.resolveCollaborationModeSettings(turn.message.collaborationMode);
|
|
9075
|
+
params.collaborationMode = {
|
|
9076
|
+
mode: turn.message.collaborationMode,
|
|
9077
|
+
settings: {
|
|
9078
|
+
model: settings.model,
|
|
9079
|
+
reasoning_effort: settings.reasoningEffort,
|
|
9080
|
+
developer_instructions: null
|
|
9081
|
+
}
|
|
9082
|
+
};
|
|
9083
|
+
} catch {
|
|
9084
|
+
}
|
|
9085
|
+
return params;
|
|
9086
|
+
}
|
|
9087
|
+
async startQueuedTurn(turn) {
|
|
9088
|
+
await this.appServer.rpc("thread/resume", { threadId: turn.threadId });
|
|
9089
|
+
await this.appServer.rpc("turn/start", await this.buildQueuedTurnParams(turn));
|
|
9090
|
+
}
|
|
9091
|
+
};
|
|
7639
9092
|
var MethodCatalog = class {
|
|
7640
9093
|
constructor() {
|
|
7641
9094
|
this.methodCache = null;
|
|
@@ -7738,15 +9191,18 @@ function getSharedBridgeState() {
|
|
|
7738
9191
|
return existing;
|
|
7739
9192
|
}
|
|
7740
9193
|
existing.appServer.dispose();
|
|
9194
|
+
existing.backendQueueProcessor?.dispose();
|
|
7741
9195
|
existing.terminalManager?.dispose();
|
|
7742
9196
|
}
|
|
7743
9197
|
const appServer = new AppServerProcess();
|
|
7744
9198
|
const terminalManager = new ThreadTerminalManager();
|
|
9199
|
+
const backendQueueProcessor = new BackendQueueProcessor(appServer);
|
|
7745
9200
|
const created = {
|
|
7746
9201
|
version: SHARED_BRIDGE_VERSION,
|
|
7747
9202
|
appServer,
|
|
7748
9203
|
terminalManager,
|
|
7749
9204
|
methodCatalog: new MethodCatalog(),
|
|
9205
|
+
backendQueueProcessor,
|
|
7750
9206
|
telegramBridge: new TelegramThreadBridge(appServer, {
|
|
7751
9207
|
onChatSeen: (chatId) => {
|
|
7752
9208
|
void rememberTelegramChatId(chatId).catch(() => {
|
|
@@ -7826,7 +9282,7 @@ async function buildThreadSearchIndex(appServer) {
|
|
|
7826
9282
|
return { docsById };
|
|
7827
9283
|
}
|
|
7828
9284
|
function createCodexBridgeMiddleware() {
|
|
7829
|
-
const { appServer, terminalManager, methodCatalog, telegramBridge } = getSharedBridgeState();
|
|
9285
|
+
const { appServer, terminalManager, methodCatalog, telegramBridge, backendQueueProcessor } = getSharedBridgeState();
|
|
7830
9286
|
let threadSearchIndex = null;
|
|
7831
9287
|
let threadSearchIndexPromise = null;
|
|
7832
9288
|
async function getThreadSearchIndex() {
|
|
@@ -7893,6 +9349,10 @@ function createCodexBridgeMiddleware() {
|
|
|
7893
9349
|
}
|
|
7894
9350
|
const url = new URL(req.url, "http://localhost");
|
|
7895
9351
|
if (url.pathname === "/codex-api/zen-proxy/v1/responses" && req.method === "POST") {
|
|
9352
|
+
if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
|
9353
|
+
setJson4(res, 403, { error: "Zen proxy is only available from localhost" });
|
|
9354
|
+
return;
|
|
9355
|
+
}
|
|
7896
9356
|
const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
|
|
7897
9357
|
let bearerToken = "";
|
|
7898
9358
|
let wireApi = "chat";
|
|
@@ -7910,9 +9370,9 @@ function createCodexBridgeMiddleware() {
|
|
|
7910
9370
|
let bearerToken = "";
|
|
7911
9371
|
let wireApi = "responses";
|
|
7912
9372
|
try {
|
|
7913
|
-
const state =
|
|
7914
|
-
bearerToken = state
|
|
7915
|
-
wireApi = state
|
|
9373
|
+
const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
|
|
9374
|
+
bearerToken = state?.apiKey ?? "";
|
|
9375
|
+
wireApi = state?.wireApi === "chat" ? "chat" : "responses";
|
|
7916
9376
|
} catch {
|
|
7917
9377
|
}
|
|
7918
9378
|
handleOpenRouterProxyRequest(req, res, bearerToken, wireApi);
|
|
@@ -7935,17 +9395,13 @@ function createCodexBridgeMiddleware() {
|
|
|
7935
9395
|
}
|
|
7936
9396
|
if (url.pathname.startsWith("/codex-api/free-mode")) {
|
|
7937
9397
|
let readFreeModeState2 = function() {
|
|
7938
|
-
|
|
7939
|
-
return JSON.parse(readFileSync2(statePath, "utf8"));
|
|
7940
|
-
} catch {
|
|
7941
|
-
return { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
|
|
7942
|
-
}
|
|
9398
|
+
return ensureDefaultFreeModeStateForMissingAuthSync(statePath) ?? { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
|
|
7943
9399
|
};
|
|
7944
9400
|
var readFreeModeState = readFreeModeState2;
|
|
7945
9401
|
const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
|
|
7946
9402
|
if (req.method === "POST" && url.pathname === "/codex-api/free-mode") {
|
|
7947
9403
|
try {
|
|
7948
|
-
const body = await
|
|
9404
|
+
const body = await readJsonBody2(req);
|
|
7949
9405
|
const enable = Boolean(body?.enable);
|
|
7950
9406
|
if (enable) {
|
|
7951
9407
|
const apiKey = getRandomFreeKey();
|
|
@@ -8001,12 +9457,12 @@ function createCodexBridgeMiddleware() {
|
|
|
8001
9457
|
if (req.method === "GET" && url.pathname === "/codex-api/free-mode/status") {
|
|
8002
9458
|
try {
|
|
8003
9459
|
const state = readFreeModeState2();
|
|
8004
|
-
const freeModels = await getFreeModels();
|
|
8005
9460
|
const maskedKey = state.apiKey && state.customKey ? state.apiKey.substring(0, 12) + "..." + state.apiKey.substring(state.apiKey.length - 4) : null;
|
|
9461
|
+
refreshFreeModelsInBackground();
|
|
8006
9462
|
setJson4(res, 200, {
|
|
8007
9463
|
enabled: state.enabled,
|
|
8008
9464
|
keyCount: getFreeKeyCount(),
|
|
8009
|
-
models:
|
|
9465
|
+
models: getCachedFreeModels(),
|
|
8010
9466
|
currentModel: state.enabled ? state.model : null,
|
|
8011
9467
|
customKey: Boolean(state.customKey),
|
|
8012
9468
|
maskedKey,
|
|
@@ -8038,7 +9494,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8038
9494
|
}
|
|
8039
9495
|
if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-key") {
|
|
8040
9496
|
try {
|
|
8041
|
-
const body = await
|
|
9497
|
+
const body = await readJsonBody2(req);
|
|
8042
9498
|
const key = typeof body?.key === "string" ? body.key.trim() : "";
|
|
8043
9499
|
const current = readFreeModeState2();
|
|
8044
9500
|
if (key.length > 0) {
|
|
@@ -8073,7 +9529,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8073
9529
|
}
|
|
8074
9530
|
if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-provider") {
|
|
8075
9531
|
try {
|
|
8076
|
-
const body = await
|
|
9532
|
+
const body = await readJsonBody2(req);
|
|
8077
9533
|
const baseUrl = typeof body?.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
8078
9534
|
const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
|
|
8079
9535
|
const wireApi = body?.wireApi === "chat" ? "chat" : "responses";
|
|
@@ -8116,10 +9572,10 @@ function createCodexBridgeMiddleware() {
|
|
|
8116
9572
|
if (await handleAccountRoutes(req, res, url, { appServer })) {
|
|
8117
9573
|
return;
|
|
8118
9574
|
}
|
|
8119
|
-
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
9575
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody: readJsonBody2 })) {
|
|
8120
9576
|
return;
|
|
8121
9577
|
}
|
|
8122
|
-
if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
|
|
9578
|
+
if (await handleReviewRoutes(req, res, url, { readJsonBody: readJsonBody2 })) {
|
|
8123
9579
|
return;
|
|
8124
9580
|
}
|
|
8125
9581
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal/status") {
|
|
@@ -8145,7 +9601,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8145
9601
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8146
9602
|
return;
|
|
8147
9603
|
}
|
|
8148
|
-
const body = asRecord5(await
|
|
9604
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8149
9605
|
const threadId = readNonEmptyString(body?.threadId);
|
|
8150
9606
|
const cwd = readNonEmptyString(body?.cwd);
|
|
8151
9607
|
if (!threadId || !cwd) {
|
|
@@ -8169,7 +9625,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8169
9625
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8170
9626
|
return;
|
|
8171
9627
|
}
|
|
8172
|
-
const body = asRecord5(await
|
|
9628
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8173
9629
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
8174
9630
|
const data = typeof body?.data === "string" ? body.data : "";
|
|
8175
9631
|
if (!sessionId) {
|
|
@@ -8186,7 +9642,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8186
9642
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8187
9643
|
return;
|
|
8188
9644
|
}
|
|
8189
|
-
const body = asRecord5(await
|
|
9645
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8190
9646
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
8191
9647
|
if (!sessionId) {
|
|
8192
9648
|
setJson4(res, 400, { error: "Missing sessionId" });
|
|
@@ -8202,7 +9658,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8202
9658
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8203
9659
|
return;
|
|
8204
9660
|
}
|
|
8205
|
-
const body = asRecord5(await
|
|
9661
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8206
9662
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
8207
9663
|
if (!sessionId) {
|
|
8208
9664
|
setJson4(res, 400, { error: "Missing sessionId" });
|
|
@@ -8226,7 +9682,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8226
9682
|
return;
|
|
8227
9683
|
}
|
|
8228
9684
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
8229
|
-
const payload = await
|
|
9685
|
+
const payload = await readJsonBody2(req);
|
|
8230
9686
|
const body = asRecord5(payload);
|
|
8231
9687
|
if (payload !== null && payload !== void 0) {
|
|
8232
9688
|
requestBodyBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
@@ -8236,9 +9692,10 @@ function createCodexBridgeMiddleware() {
|
|
|
8236
9692
|
setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
8237
9693
|
return;
|
|
8238
9694
|
}
|
|
8239
|
-
const rpcResult = await appServer
|
|
9695
|
+
const rpcResult = await callRpcWithArchiveRecovery(appServer, body.method, body.params ?? null);
|
|
8240
9696
|
const trimmedResult = trimThreadTurnsInRpcResult(body.method, rpcResult);
|
|
8241
|
-
const
|
|
9697
|
+
const sanitizedResult = await sanitizeThreadTurnsInlinePayloads(body.method, trimmedResult);
|
|
9698
|
+
const result = THREAD_METHODS_WITH_TURNS.has(body.method) ? await mergeSessionSkillInputsIntoThreadResult(sanitizedResult) : sanitizedResult;
|
|
8242
9699
|
if (THREAD_METHODS_WITH_TURNS.has(body.method)) {
|
|
8243
9700
|
const rpcRecord = asRecord5(result);
|
|
8244
9701
|
const rpcThread = asRecord5(rpcRecord?.thread);
|
|
@@ -8374,7 +9831,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8374
9831
|
}
|
|
8375
9832
|
if (req.method === "POST" && url.pathname === "/codex-api/thread/rollback-files") {
|
|
8376
9833
|
try {
|
|
8377
|
-
const body = asRecord5(await
|
|
9834
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8378
9835
|
const threadId = readNonEmptyString(body?.threadId);
|
|
8379
9836
|
const turnId = readNonEmptyString(body?.turnId);
|
|
8380
9837
|
const cwd = readNonEmptyString(body?.cwd);
|
|
@@ -8470,7 +9927,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8470
9927
|
}
|
|
8471
9928
|
if (req.method === "POST" && url.pathname === "/codex-api/composio/link") {
|
|
8472
9929
|
try {
|
|
8473
|
-
const payload = asRecord5(await
|
|
9930
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8474
9931
|
const slug = readNonEmptyString(payload?.slug);
|
|
8475
9932
|
setJson4(res, 200, await startComposioLink(slug));
|
|
8476
9933
|
} catch (error) {
|
|
@@ -8512,7 +9969,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8512
9969
|
return;
|
|
8513
9970
|
}
|
|
8514
9971
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
8515
|
-
const payload = await
|
|
9972
|
+
const payload = await readJsonBody2(req);
|
|
8516
9973
|
await appServer.respondToServerRequest(payload);
|
|
8517
9974
|
setJson4(res, 200, { ok: true });
|
|
8518
9975
|
return;
|
|
@@ -8533,8 +9990,8 @@ function createCodexBridgeMiddleware() {
|
|
|
8533
9990
|
}
|
|
8534
9991
|
if (req.method === "GET" && url.pathname === "/codex-api/provider-models") {
|
|
8535
9992
|
try {
|
|
8536
|
-
const fmState =
|
|
8537
|
-
if (fmState
|
|
9993
|
+
const fmState = ensureDefaultFreeModeStateForMissingAuthSync(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE));
|
|
9994
|
+
if (fmState?.enabled) {
|
|
8538
9995
|
if (fmState.provider === "opencode-zen") {
|
|
8539
9996
|
try {
|
|
8540
9997
|
const modelsUrl = "https://opencode.ai/zen/v1/models";
|
|
@@ -8602,7 +10059,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8602
10059
|
return;
|
|
8603
10060
|
}
|
|
8604
10061
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
8605
|
-
const payload = asRecord5(await
|
|
10062
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8606
10063
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
8607
10064
|
const baseBranch = typeof payload?.baseBranch === "string" ? payload.baseBranch.trim() : "";
|
|
8608
10065
|
if (!rawSourceCwd) {
|
|
@@ -8660,6 +10117,12 @@ function createCodexBridgeMiddleware() {
|
|
|
8660
10117
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
8661
10118
|
await runCommand3("git", ["worktree", "add", "--detach", worktreeCwd, startPoint], { cwd: gitRoot });
|
|
8662
10119
|
}
|
|
10120
|
+
try {
|
|
10121
|
+
await persistWorkspaceRoot(worktreeCwd);
|
|
10122
|
+
} catch (error) {
|
|
10123
|
+
await rollbackCreatedWorktree(gitRoot, worktreeCwd, worktreeParent);
|
|
10124
|
+
throw error;
|
|
10125
|
+
}
|
|
8663
10126
|
setJson4(res, 200, {
|
|
8664
10127
|
data: {
|
|
8665
10128
|
cwd: worktreeCwd,
|
|
@@ -8672,6 +10135,75 @@ function createCodexBridgeMiddleware() {
|
|
|
8672
10135
|
}
|
|
8673
10136
|
return;
|
|
8674
10137
|
}
|
|
10138
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create-permanent") {
|
|
10139
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
10140
|
+
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
10141
|
+
const rawWorktreeName = typeof payload?.worktreeName === "string" ? payload.worktreeName.trim() : "";
|
|
10142
|
+
if (!rawSourceCwd) {
|
|
10143
|
+
setJson4(res, 400, { error: "Missing sourceCwd" });
|
|
10144
|
+
return;
|
|
10145
|
+
}
|
|
10146
|
+
if (!rawWorktreeName) {
|
|
10147
|
+
setJson4(res, 400, { error: "Missing worktreeName" });
|
|
10148
|
+
return;
|
|
10149
|
+
}
|
|
10150
|
+
if (rawWorktreeName.includes("/") || rawWorktreeName.includes("\\") || rawWorktreeName === "." || rawWorktreeName === "..") {
|
|
10151
|
+
setJson4(res, 400, { error: "Worktree name must be a single folder name" });
|
|
10152
|
+
return;
|
|
10153
|
+
}
|
|
10154
|
+
const sourceCwd = isAbsolute2(rawSourceCwd) ? rawSourceCwd : resolve2(rawSourceCwd);
|
|
10155
|
+
try {
|
|
10156
|
+
const sourceInfo = await stat4(sourceCwd);
|
|
10157
|
+
if (!sourceInfo.isDirectory()) {
|
|
10158
|
+
setJson4(res, 400, { error: "sourceCwd is not a directory" });
|
|
10159
|
+
return;
|
|
10160
|
+
}
|
|
10161
|
+
} catch {
|
|
10162
|
+
setJson4(res, 404, { error: "sourceCwd does not exist" });
|
|
10163
|
+
return;
|
|
10164
|
+
}
|
|
10165
|
+
try {
|
|
10166
|
+
let gitRoot = "";
|
|
10167
|
+
try {
|
|
10168
|
+
gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
10169
|
+
} catch (error) {
|
|
10170
|
+
if (!isNotGitRepositoryError2(error)) throw error;
|
|
10171
|
+
await runCommand3("git", ["init"], { cwd: sourceCwd });
|
|
10172
|
+
gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
10173
|
+
}
|
|
10174
|
+
const worktreeCwd = join6(dirname2(gitRoot), rawWorktreeName);
|
|
10175
|
+
try {
|
|
10176
|
+
await stat4(worktreeCwd);
|
|
10177
|
+
setJson4(res, 409, { error: "Worktree folder already exists" });
|
|
10178
|
+
return;
|
|
10179
|
+
} catch {
|
|
10180
|
+
}
|
|
10181
|
+
const branchName = await allocatePermanentWorktreeBranchName(gitRoot, rawWorktreeName);
|
|
10182
|
+
try {
|
|
10183
|
+
await runCommand3("git", ["worktree", "add", "-b", branchName, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
10184
|
+
} catch (error) {
|
|
10185
|
+
if (!isMissingHeadError2(error)) throw error;
|
|
10186
|
+
await ensureRepoHasInitialCommit(gitRoot);
|
|
10187
|
+
await runCommand3("git", ["worktree", "add", "-b", branchName, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
10188
|
+
}
|
|
10189
|
+
try {
|
|
10190
|
+
await persistWorkspaceRoot(worktreeCwd);
|
|
10191
|
+
} catch (error) {
|
|
10192
|
+
await rollbackCreatedWorktree(gitRoot, worktreeCwd, void 0, branchName);
|
|
10193
|
+
throw error;
|
|
10194
|
+
}
|
|
10195
|
+
setJson4(res, 200, {
|
|
10196
|
+
data: {
|
|
10197
|
+
cwd: worktreeCwd,
|
|
10198
|
+
branch: branchName,
|
|
10199
|
+
gitRoot
|
|
10200
|
+
}
|
|
10201
|
+
});
|
|
10202
|
+
} catch (error) {
|
|
10203
|
+
setJson4(res, 500, { error: getErrorMessage5(error, "Failed to create worktree") });
|
|
10204
|
+
}
|
|
10205
|
+
return;
|
|
10206
|
+
}
|
|
8675
10207
|
if (req.method === "GET" && url.pathname === "/codex-api/worktree/branches") {
|
|
8676
10208
|
const rawSourceCwd = (url.searchParams.get("sourceCwd") ?? "").trim();
|
|
8677
10209
|
if (!rawSourceCwd) {
|
|
@@ -8758,11 +10290,11 @@ function createCodexBridgeMiddleware() {
|
|
|
8758
10290
|
});
|
|
8759
10291
|
return;
|
|
8760
10292
|
}
|
|
8761
|
-
const
|
|
8762
|
-
const currentBranch =
|
|
10293
|
+
const state = await readGitHeaderState(gitRoot);
|
|
10294
|
+
const currentBranch = state.currentBranch;
|
|
8763
10295
|
const output = await runCommandCapture2(
|
|
8764
10296
|
"git",
|
|
8765
|
-
["for-each-ref", "--format=%(committerdate:unix) %(refname)", "refs/heads", "refs/remotes"],
|
|
10297
|
+
["for-each-ref", "--format=%(committerdate:unix) %(refname) %(objectname)", "refs/heads", "refs/remotes"],
|
|
8766
10298
|
{ cwd: gitRoot }
|
|
8767
10299
|
);
|
|
8768
10300
|
const branchActivityByName = /* @__PURE__ */ new Map();
|
|
@@ -8772,23 +10304,29 @@ function createCodexBridgeMiddleware() {
|
|
|
8772
10304
|
if (!normalized || normalized === "origin/HEAD") continue;
|
|
8773
10305
|
const parsedTimestamp = Number.parseInt(rawTimestamp.trim(), 10);
|
|
8774
10306
|
const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : 0;
|
|
8775
|
-
const
|
|
8776
|
-
|
|
8777
|
-
|
|
10307
|
+
const isRemote = rawRefName.trim().startsWith("refs/remotes/");
|
|
10308
|
+
const current = branchActivityByName.get(normalized);
|
|
10309
|
+
if (!current || timestamp > current.timestamp) {
|
|
10310
|
+
branchActivityByName.set(normalized, { timestamp, isRemote });
|
|
8778
10311
|
}
|
|
8779
10312
|
}
|
|
8780
10313
|
if (currentBranch && !branchActivityByName.has(currentBranch)) {
|
|
8781
|
-
branchActivityByName.set(currentBranch, Number.MAX_SAFE_INTEGER);
|
|
10314
|
+
branchActivityByName.set(currentBranch, { timestamp: Number.MAX_SAFE_INTEGER, isRemote: false });
|
|
8782
10315
|
}
|
|
8783
|
-
const options = Array.from(branchActivityByName.entries()).map(([value]) => ({
|
|
8784
|
-
|
|
8785
|
-
|
|
10316
|
+
const options = Array.from(branchActivityByName.entries()).map(([value, metadata]) => ({
|
|
10317
|
+
value,
|
|
10318
|
+
label: value,
|
|
10319
|
+
isCurrent: value === currentBranch,
|
|
10320
|
+
isRemote: metadata.isRemote
|
|
10321
|
+
})).sort((a, b) => {
|
|
10322
|
+
const aActivity = branchActivityByName.get(a.value)?.timestamp ?? 0;
|
|
10323
|
+
const bActivity = branchActivityByName.get(b.value)?.timestamp ?? 0;
|
|
8786
10324
|
if (bActivity !== aActivity) return bActivity - aActivity;
|
|
8787
10325
|
return a.value.localeCompare(b.value);
|
|
8788
10326
|
});
|
|
8789
10327
|
setJson4(res, 200, {
|
|
8790
10328
|
data: {
|
|
8791
|
-
|
|
10329
|
+
...state,
|
|
8792
10330
|
options
|
|
8793
10331
|
}
|
|
8794
10332
|
});
|
|
@@ -8797,8 +10335,47 @@ function createCodexBridgeMiddleware() {
|
|
|
8797
10335
|
}
|
|
8798
10336
|
return;
|
|
8799
10337
|
}
|
|
10338
|
+
if (req.method === "GET" && url.pathname === "/codex-api/git/repository-status") {
|
|
10339
|
+
const rawCwd = (url.searchParams.get("cwd") ?? "").trim();
|
|
10340
|
+
if (!rawCwd) {
|
|
10341
|
+
setJson4(res, 400, { error: "Missing cwd" });
|
|
10342
|
+
return;
|
|
10343
|
+
}
|
|
10344
|
+
const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
|
|
10345
|
+
try {
|
|
10346
|
+
const cwdInfo = await stat4(cwd);
|
|
10347
|
+
if (!cwdInfo.isDirectory()) {
|
|
10348
|
+
setJson4(res, 400, { error: "cwd is not a directory" });
|
|
10349
|
+
return;
|
|
10350
|
+
}
|
|
10351
|
+
} catch {
|
|
10352
|
+
setJson4(res, 404, { error: "cwd does not exist" });
|
|
10353
|
+
return;
|
|
10354
|
+
}
|
|
10355
|
+
try {
|
|
10356
|
+
const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
10357
|
+
setJson4(res, 200, {
|
|
10358
|
+
data: {
|
|
10359
|
+
isGitRepo: true,
|
|
10360
|
+
gitRoot
|
|
10361
|
+
}
|
|
10362
|
+
});
|
|
10363
|
+
} catch (error) {
|
|
10364
|
+
if (!isNotGitRepositoryError2(error)) {
|
|
10365
|
+
setJson4(res, 500, { error: getErrorMessage5(error, "Failed to read Git repository status") });
|
|
10366
|
+
return;
|
|
10367
|
+
}
|
|
10368
|
+
setJson4(res, 200, {
|
|
10369
|
+
data: {
|
|
10370
|
+
isGitRepo: false,
|
|
10371
|
+
gitRoot: ""
|
|
10372
|
+
}
|
|
10373
|
+
});
|
|
10374
|
+
}
|
|
10375
|
+
return;
|
|
10376
|
+
}
|
|
8800
10377
|
if (req.method === "POST" && url.pathname === "/codex-api/git/checkout") {
|
|
8801
|
-
const payload = await
|
|
10378
|
+
const payload = await readJsonBody2(req);
|
|
8802
10379
|
const record = asRecord5(payload);
|
|
8803
10380
|
if (!record) {
|
|
8804
10381
|
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
@@ -8827,52 +10404,127 @@ function createCodexBridgeMiddleware() {
|
|
|
8827
10404
|
}
|
|
8828
10405
|
try {
|
|
8829
10406
|
const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, targetBranch);
|
|
8834
|
-
if (!blockingWorktreePath) {
|
|
8835
|
-
throw checkoutError;
|
|
8836
|
-
}
|
|
8837
|
-
await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
|
|
8838
|
-
await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
|
|
8839
|
-
}
|
|
8840
|
-
const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim() || null;
|
|
8841
|
-
setJson4(res, 200, { data: { currentBranch } });
|
|
10407
|
+
await assertNoTrackedGitChanges(gitRoot);
|
|
10408
|
+
await checkoutGitBranchWithWorktreeRecovery(gitRoot, targetBranch);
|
|
10409
|
+
setJson4(res, 200, { data: await readGitHeaderState(gitRoot) });
|
|
8842
10410
|
} catch (error) {
|
|
8843
10411
|
setJson4(res, 500, { error: getErrorMessage5(error, "Failed to switch branch") });
|
|
8844
10412
|
}
|
|
8845
10413
|
return;
|
|
8846
10414
|
}
|
|
10415
|
+
if (req.method === "GET" && url.pathname === "/codex-api/git/branch-commits") {
|
|
10416
|
+
const rawCwd = (url.searchParams.get("cwd") ?? "").trim();
|
|
10417
|
+
const branch = (url.searchParams.get("branch") ?? "").trim();
|
|
10418
|
+
if (!rawCwd) {
|
|
10419
|
+
setJson4(res, 400, { error: "Missing cwd" });
|
|
10420
|
+
return;
|
|
10421
|
+
}
|
|
10422
|
+
if (!branch) {
|
|
10423
|
+
setJson4(res, 400, { error: "Missing branch" });
|
|
10424
|
+
return;
|
|
10425
|
+
}
|
|
10426
|
+
const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
|
|
10427
|
+
try {
|
|
10428
|
+
const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
10429
|
+
await runCommandCapture2("git", ["rev-parse", "--verify", `${branch}^{commit}`], { cwd: gitRoot });
|
|
10430
|
+
const resetHistoryRefPrefix = `refs/codex/header-git-reset-history/${branch}/`;
|
|
10431
|
+
const resetHistoryRefsRaw = await runCommandCapture2(
|
|
10432
|
+
"git",
|
|
10433
|
+
["for-each-ref", "--sort=-creatordate", "--format=%(refname)", resetHistoryRefPrefix],
|
|
10434
|
+
{ cwd: gitRoot }
|
|
10435
|
+
).catch(() => "");
|
|
10436
|
+
const resetHistoryRefs = resetHistoryRefsRaw.split("\n").map((entry) => entry.trim()).filter(Boolean).slice(0, HEADER_GIT_RESET_HISTORY_REF_LIMIT);
|
|
10437
|
+
const output = await runCommandCapture2(
|
|
10438
|
+
"git",
|
|
10439
|
+
["log", "-n", "12", "--date=short", "--format=%H%x09%h%x09%cd%x09%s", branch, ...resetHistoryRefs],
|
|
10440
|
+
{ cwd: gitRoot }
|
|
10441
|
+
);
|
|
10442
|
+
const commits = output.split("\n").flatMap((line) => {
|
|
10443
|
+
const [sha = "", shortSha = "", date = "", ...subjectParts] = line.split(" ");
|
|
10444
|
+
const subject = subjectParts.join(" ").trim();
|
|
10445
|
+
return sha.trim() && shortSha.trim() ? [{ sha: sha.trim(), shortSha: shortSha.trim(), date: date.trim(), subject: subject || shortSha.trim() }] : [];
|
|
10446
|
+
});
|
|
10447
|
+
setJson4(res, 200, { data: commits });
|
|
10448
|
+
} catch (error) {
|
|
10449
|
+
setJson4(res, 500, { error: getErrorMessage5(error, "Failed to load branch commits") });
|
|
10450
|
+
}
|
|
10451
|
+
return;
|
|
10452
|
+
}
|
|
10453
|
+
if (req.method === "POST" && url.pathname === "/codex-api/git/reset-to-commit") {
|
|
10454
|
+
const payload = await readJsonBody2(req);
|
|
10455
|
+
const record = asRecord5(payload);
|
|
10456
|
+
if (!record) {
|
|
10457
|
+
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
10458
|
+
return;
|
|
10459
|
+
}
|
|
10460
|
+
const rawCwd = readNonEmptyString(record.cwd);
|
|
10461
|
+
const branch = readNonEmptyString(record.branch);
|
|
10462
|
+
const sha = readNonEmptyString(record.sha);
|
|
10463
|
+
if (!rawCwd) {
|
|
10464
|
+
setJson4(res, 400, { error: "Missing cwd" });
|
|
10465
|
+
return;
|
|
10466
|
+
}
|
|
10467
|
+
if (!branch) {
|
|
10468
|
+
setJson4(res, 400, { error: "Missing branch" });
|
|
10469
|
+
return;
|
|
10470
|
+
}
|
|
10471
|
+
if (!sha) {
|
|
10472
|
+
setJson4(res, 400, { error: "Missing commit" });
|
|
10473
|
+
return;
|
|
10474
|
+
}
|
|
10475
|
+
const cwd = isAbsolute2(rawCwd) ? rawCwd : resolve2(rawCwd);
|
|
10476
|
+
try {
|
|
10477
|
+
const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
10478
|
+
await assertNoTrackedGitChanges(gitRoot);
|
|
10479
|
+
await assertLocalGitBranch(gitRoot, branch);
|
|
10480
|
+
const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim();
|
|
10481
|
+
if (currentBranch && currentBranch !== branch) {
|
|
10482
|
+
await checkoutGitBranchWithWorktreeRecovery(gitRoot, branch);
|
|
10483
|
+
} else if (!currentBranch) {
|
|
10484
|
+
await checkoutGitBranchWithWorktreeRecovery(gitRoot, branch);
|
|
10485
|
+
}
|
|
10486
|
+
const previousTip = await runCommandCapture2("git", ["rev-parse", "HEAD"], { cwd: gitRoot });
|
|
10487
|
+
const targetSha = await runCommandCapture2("git", ["rev-parse", "--verify", `${sha}^{commit}`], { cwd: gitRoot });
|
|
10488
|
+
await runCommand3("git", ["update-ref", toHeaderGitResetHistoryRef(branch, previousTip.trim()), previousTip.trim()], { cwd: gitRoot });
|
|
10489
|
+
await pruneHeaderGitResetHistoryRefs(gitRoot, branch);
|
|
10490
|
+
await runCommand3("git", ["reset", "--hard", targetSha.trim()], { cwd: gitRoot });
|
|
10491
|
+
setJson4(res, 200, { data: await readGitHeaderState(gitRoot) });
|
|
10492
|
+
} catch (error) {
|
|
10493
|
+
setJson4(res, 500, { error: getErrorMessage5(error, "Failed to reset branch to commit") });
|
|
10494
|
+
}
|
|
10495
|
+
return;
|
|
10496
|
+
}
|
|
8847
10497
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
8848
|
-
const payload = await
|
|
10498
|
+
const payload = await readJsonBody2(req);
|
|
8849
10499
|
const record = asRecord5(payload);
|
|
8850
10500
|
if (!record) {
|
|
8851
10501
|
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
8852
10502
|
return;
|
|
8853
10503
|
}
|
|
8854
|
-
|
|
10504
|
+
await updateWorkspaceRootsState((existingState) => ({
|
|
8855
10505
|
order: normalizeStringArray(record.order),
|
|
8856
10506
|
labels: normalizeStringRecord(record.labels),
|
|
8857
|
-
active: normalizeStringArray(record.active)
|
|
8858
|
-
|
|
8859
|
-
|
|
10507
|
+
active: normalizeStringArray(record.active),
|
|
10508
|
+
projectOrder: Array.isArray(record.projectOrder) ? normalizeStringArray(record.projectOrder) : existingState.projectOrder,
|
|
10509
|
+
remoteProjects: existingState.remoteProjects
|
|
10510
|
+
}));
|
|
8860
10511
|
setJson4(res, 200, { ok: true });
|
|
8861
10512
|
return;
|
|
8862
10513
|
}
|
|
8863
10514
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-queue-state") {
|
|
8864
|
-
const payload = await
|
|
10515
|
+
const payload = await readJsonBody2(req);
|
|
8865
10516
|
const record = asRecord5(payload);
|
|
8866
10517
|
if (!record) {
|
|
8867
10518
|
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
8868
10519
|
return;
|
|
8869
10520
|
}
|
|
8870
10521
|
await writeThreadQueueState(normalizeThreadQueueState(record));
|
|
10522
|
+
void backendQueueProcessor.scheduleAllQueuedThreads();
|
|
8871
10523
|
setJson4(res, 200, { ok: true });
|
|
8872
10524
|
return;
|
|
8873
10525
|
}
|
|
8874
10526
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
8875
|
-
const payload = asRecord5(await
|
|
10527
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8876
10528
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
8877
10529
|
const createIfMissing = payload?.createIfMissing === true;
|
|
8878
10530
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
@@ -8897,23 +10549,12 @@ function createCodexBridgeMiddleware() {
|
|
|
8897
10549
|
setJson4(res, 404, { error: "Directory does not exist" });
|
|
8898
10550
|
return;
|
|
8899
10551
|
}
|
|
8900
|
-
|
|
8901
|
-
const nextOrder = [normalizedPath, ...existingState.order.filter((item) => item !== normalizedPath)];
|
|
8902
|
-
const nextActive = [normalizedPath, ...existingState.active.filter((item) => item !== normalizedPath)];
|
|
8903
|
-
const nextLabels = { ...existingState.labels };
|
|
8904
|
-
if (label.trim().length > 0) {
|
|
8905
|
-
nextLabels[normalizedPath] = label.trim();
|
|
8906
|
-
}
|
|
8907
|
-
await writeWorkspaceRootsState({
|
|
8908
|
-
order: nextOrder,
|
|
8909
|
-
labels: nextLabels,
|
|
8910
|
-
active: nextActive
|
|
8911
|
-
});
|
|
10552
|
+
await persistWorkspaceRoot(normalizedPath, label);
|
|
8912
10553
|
setJson4(res, 200, { data: { path: normalizedPath } });
|
|
8913
10554
|
return;
|
|
8914
10555
|
}
|
|
8915
10556
|
if (req.method === "POST" && url.pathname === "/codex-api/local-directory") {
|
|
8916
|
-
const payload = asRecord5(await
|
|
10557
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8917
10558
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
8918
10559
|
if (!rawPath) {
|
|
8919
10560
|
setJson4(res, 400, { error: "Missing path" });
|
|
@@ -8933,7 +10574,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8933
10574
|
return;
|
|
8934
10575
|
}
|
|
8935
10576
|
if (req.method === "POST" && url.pathname === "/codex-api/projectless-thread-cwd") {
|
|
8936
|
-
const payload = asRecord5(await
|
|
10577
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8937
10578
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt : null;
|
|
8938
10579
|
try {
|
|
8939
10580
|
const directory = await createProjectlessThreadDirectory(prompt);
|
|
@@ -8977,7 +10618,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8977
10618
|
return;
|
|
8978
10619
|
}
|
|
8979
10620
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
8980
|
-
const payload = asRecord5(await
|
|
10621
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8981
10622
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
8982
10623
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
8983
10624
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
@@ -9011,7 +10652,7 @@ function createCodexBridgeMiddleware() {
|
|
|
9011
10652
|
return;
|
|
9012
10653
|
}
|
|
9013
10654
|
if (req.method === "POST" && url.pathname === "/codex-api/prompts") {
|
|
9014
|
-
const payload = asRecord5(await
|
|
10655
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9015
10656
|
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
9016
10657
|
const content = typeof payload?.content === "string" ? payload.content : "";
|
|
9017
10658
|
if (!name || !content.trim()) {
|
|
@@ -9062,16 +10703,17 @@ function createCodexBridgeMiddleware() {
|
|
|
9062
10703
|
}
|
|
9063
10704
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
|
|
9064
10705
|
const threadId = url.searchParams.get("threadId")?.trim() ?? "";
|
|
10706
|
+
const automationId = url.searchParams.get("automationId")?.trim() ?? "";
|
|
9065
10707
|
if (!threadId) {
|
|
9066
10708
|
setJson4(res, 400, { error: "Missing threadId" });
|
|
9067
10709
|
return;
|
|
9068
10710
|
}
|
|
9069
|
-
const automation = await readThreadHeartbeatAutomation(threadId);
|
|
10711
|
+
const automation = automationId ? await readThreadHeartbeatAutomation(threadId, automationId) : await readThreadHeartbeatAutomations(threadId);
|
|
9070
10712
|
setJson4(res, 200, { data: automation });
|
|
9071
10713
|
return;
|
|
9072
10714
|
}
|
|
9073
10715
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
9074
|
-
const payload = asRecord5(await
|
|
10716
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9075
10717
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
9076
10718
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
9077
10719
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
@@ -9085,7 +10727,7 @@ function createCodexBridgeMiddleware() {
|
|
|
9085
10727
|
return;
|
|
9086
10728
|
}
|
|
9087
10729
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
9088
|
-
const payload = asRecord5(await
|
|
10730
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9089
10731
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
9090
10732
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
9091
10733
|
if (!id) {
|
|
@@ -9099,22 +10741,23 @@ function createCodexBridgeMiddleware() {
|
|
|
9099
10741
|
return;
|
|
9100
10742
|
}
|
|
9101
10743
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-pins") {
|
|
9102
|
-
const payload = asRecord5(await
|
|
10744
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9103
10745
|
const threadIds = normalizePinnedThreadIds(payload?.threadIds);
|
|
9104
10746
|
await writePinnedThreadIds(threadIds);
|
|
9105
10747
|
setJson4(res, 200, { ok: true });
|
|
9106
10748
|
return;
|
|
9107
10749
|
}
|
|
9108
10750
|
if (req.method === "PUT" && url.pathname === "/codex-api/preferences/first-launch-plugins-card") {
|
|
9109
|
-
const payload = asRecord5(await
|
|
10751
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9110
10752
|
const dismissed = payload?.dismissed === true;
|
|
9111
10753
|
await writeFirstLaunchPluginsCardDismissed(dismissed);
|
|
9112
10754
|
setJson4(res, 200, { ok: true });
|
|
9113
10755
|
return;
|
|
9114
10756
|
}
|
|
9115
10757
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
|
|
9116
|
-
const payload = asRecord5(await
|
|
10758
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9117
10759
|
const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
|
|
10760
|
+
const id = typeof payload?.id === "string" ? payload.id.trim() : "";
|
|
9118
10761
|
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
9119
10762
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
|
|
9120
10763
|
const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
|
|
@@ -9123,22 +10766,41 @@ function createCodexBridgeMiddleware() {
|
|
|
9123
10766
|
setJson4(res, 400, { error: "threadId, name, prompt, and rrule are required" });
|
|
9124
10767
|
return;
|
|
9125
10768
|
}
|
|
9126
|
-
const automation = await writeThreadHeartbeatAutomation({ threadId, name, prompt, rrule, status });
|
|
10769
|
+
const automation = await writeThreadHeartbeatAutomation({ threadId, id, name, prompt, rrule, status });
|
|
9127
10770
|
setJson4(res, 200, { data: automation });
|
|
9128
10771
|
return;
|
|
9129
10772
|
}
|
|
10773
|
+
if (req.method === "POST" && url.pathname === "/codex-api/thread-automation/run") {
|
|
10774
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
10775
|
+
const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
|
|
10776
|
+
const automationId = typeof payload?.automationId === "string" ? payload.automationId.trim() : "";
|
|
10777
|
+
if (!threadId || !automationId) {
|
|
10778
|
+
setJson4(res, 400, { error: "threadId and automationId are required" });
|
|
10779
|
+
return;
|
|
10780
|
+
}
|
|
10781
|
+
const automation = await readThreadHeartbeatAutomation(threadId, automationId);
|
|
10782
|
+
if (!automation) {
|
|
10783
|
+
setJson4(res, 404, { error: "Automation not found for thread" });
|
|
10784
|
+
return;
|
|
10785
|
+
}
|
|
10786
|
+
await appendThreadQueuedMessage(threadId, buildHeartbeatQueuedMessage(automation));
|
|
10787
|
+
backendQueueProcessor.scheduleThreadQueueDrain(threadId, 0);
|
|
10788
|
+
setJson4(res, 200, { data: { queued: true } });
|
|
10789
|
+
return;
|
|
10790
|
+
}
|
|
9130
10791
|
if (req.method === "DELETE" && url.pathname === "/codex-api/thread-automation") {
|
|
9131
10792
|
const threadId = url.searchParams.get("threadId")?.trim() ?? "";
|
|
10793
|
+
const automationId = url.searchParams.get("automationId")?.trim() ?? "";
|
|
9132
10794
|
if (!threadId) {
|
|
9133
10795
|
setJson4(res, 400, { error: "Missing threadId" });
|
|
9134
10796
|
return;
|
|
9135
10797
|
}
|
|
9136
|
-
const removed = await deleteThreadHeartbeatAutomation(threadId);
|
|
10798
|
+
const removed = await deleteThreadHeartbeatAutomation(threadId, automationId);
|
|
9137
10799
|
setJson4(res, 200, { data: { removed } });
|
|
9138
10800
|
return;
|
|
9139
10801
|
}
|
|
9140
10802
|
if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
|
|
9141
|
-
const payload = asRecord5(await
|
|
10803
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9142
10804
|
const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
|
|
9143
10805
|
const rawAllowedUserIds = Array.isArray(payload?.allowedUserIds) ? payload.allowedUserIds : [];
|
|
9144
10806
|
if (!botToken) {
|
|
@@ -9219,6 +10881,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
9219
10881
|
threadSearchIndex = null;
|
|
9220
10882
|
telegramBridge.stop();
|
|
9221
10883
|
terminalManager.dispose();
|
|
10884
|
+
backendQueueProcessor.dispose();
|
|
9222
10885
|
appServer.dispose();
|
|
9223
10886
|
};
|
|
9224
10887
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -9244,7 +10907,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
9244
10907
|
|
|
9245
10908
|
// src/server/authMiddleware.ts
|
|
9246
10909
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
9247
|
-
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync as
|
|
10910
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
9248
10911
|
import { homedir as homedir6 } from "os";
|
|
9249
10912
|
import { dirname as dirname3, join as join7 } from "path";
|
|
9250
10913
|
var TOKEN_COOKIE = "portal_session";
|
|
@@ -9326,10 +10989,10 @@ function readPersistedSessions() {
|
|
|
9326
10989
|
}
|
|
9327
10990
|
function persistSessions(validTokens) {
|
|
9328
10991
|
const sessionStorePath = getSessionStorePath();
|
|
9329
|
-
|
|
10992
|
+
mkdirSync2(dirname3(sessionStorePath), { recursive: true });
|
|
9330
10993
|
const tokens = Array.from(validTokens.entries()).sort((left, right) => right[1] - left[1]).slice(0, MAX_PERSISTED_TOKENS).map(([value, expiresAt]) => ({ value, expiresAt }));
|
|
9331
10994
|
const tmpPath = `${sessionStorePath}.tmp`;
|
|
9332
|
-
|
|
10995
|
+
writeFileSync3(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
|
|
9333
10996
|
`, { encoding: "utf8", mode: 384 });
|
|
9334
10997
|
renameSync(tmpPath, sessionStorePath);
|
|
9335
10998
|
}
|
|
@@ -10126,7 +11789,7 @@ function hasPromptedCloudflaredInstallPersisted() {
|
|
|
10126
11789
|
}
|
|
10127
11790
|
async function persistCloudflaredInstallPrompted() {
|
|
10128
11791
|
const codexHome = getCodexHomePath();
|
|
10129
|
-
|
|
11792
|
+
mkdirSync3(codexHome, { recursive: true });
|
|
10130
11793
|
await writeFile6(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
10131
11794
|
`, "utf8");
|
|
10132
11795
|
}
|
|
@@ -10212,7 +11875,7 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
10212
11875
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
10213
11876
|
}
|
|
10214
11877
|
const userBinDir = join10(homedir7(), ".local", "bin");
|
|
10215
|
-
|
|
11878
|
+
mkdirSync3(userBinDir, { recursive: true });
|
|
10216
11879
|
const destination = join10(userBinDir, "cloudflared");
|
|
10217
11880
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
10218
11881
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
@@ -10312,12 +11975,24 @@ Global npm install requires elevated permissions. Retrying with --prefix ${userP
|
|
|
10312
11975
|
}
|
|
10313
11976
|
function resolvePassword(input) {
|
|
10314
11977
|
if (input === false) {
|
|
10315
|
-
return void 0;
|
|
11978
|
+
return { password: void 0, generated: false };
|
|
10316
11979
|
}
|
|
10317
11980
|
if (typeof input === "string") {
|
|
10318
|
-
return input;
|
|
11981
|
+
return { password: input, generated: false };
|
|
10319
11982
|
}
|
|
10320
|
-
return generatePassword();
|
|
11983
|
+
return { password: generatePassword(), generated: true };
|
|
11984
|
+
}
|
|
11985
|
+
function getGeneratedPasswordPath() {
|
|
11986
|
+
return join10(getCodexHomePath(), "codexui-password");
|
|
11987
|
+
}
|
|
11988
|
+
async function persistGeneratedPassword(password) {
|
|
11989
|
+
const codexHome = getCodexHomePath();
|
|
11990
|
+
mkdirSync3(codexHome, { recursive: true });
|
|
11991
|
+
const passwordPath = getGeneratedPasswordPath();
|
|
11992
|
+
await writeFile6(passwordPath, `${password}
|
|
11993
|
+
`, { encoding: "utf8", mode: 384 });
|
|
11994
|
+
chmodSync2(passwordPath, 384);
|
|
11995
|
+
return passwordPath;
|
|
10321
11996
|
}
|
|
10322
11997
|
function printTermuxKeepAlive(lines) {
|
|
10323
11998
|
if (!isTermuxRuntime()) {
|
|
@@ -10336,9 +12011,8 @@ function openBrowser(url) {
|
|
|
10336
12011
|
});
|
|
10337
12012
|
child.unref();
|
|
10338
12013
|
}
|
|
10339
|
-
function buildTunnelAutologinUrl(tunnelUrl,
|
|
10340
|
-
|
|
10341
|
-
return `${tunnelUrl}/password=${encodeURIComponent(password)}`;
|
|
12014
|
+
function buildTunnelAutologinUrl(tunnelUrl, _password) {
|
|
12015
|
+
return tunnelUrl;
|
|
10342
12016
|
}
|
|
10343
12017
|
function parseCloudflaredUrl(chunk) {
|
|
10344
12018
|
const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
|
|
@@ -10533,7 +12207,9 @@ async function startServer(options) {
|
|
|
10533
12207
|
console.log("\nCodex is not logged in. You can log in later via settings or run `codexui login`.\n");
|
|
10534
12208
|
}
|
|
10535
12209
|
const requestedPort = parseInt(options.port, 10);
|
|
10536
|
-
const
|
|
12210
|
+
const passwordResolution = resolvePassword(options.password);
|
|
12211
|
+
const password = passwordResolution.password;
|
|
12212
|
+
const generatedPasswordPath = password && passwordResolution.generated ? await persistGeneratedPassword(password) : null;
|
|
10537
12213
|
const { app, dispose, attachWebSocket } = createServer({ password });
|
|
10538
12214
|
const server = createServer2(app);
|
|
10539
12215
|
attachWebSocket(server);
|
|
@@ -10575,8 +12251,9 @@ async function startServer(options) {
|
|
|
10575
12251
|
if (port !== requestedPort) {
|
|
10576
12252
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
10577
12253
|
}
|
|
10578
|
-
if (
|
|
10579
|
-
lines.push(`
|
|
12254
|
+
if (generatedPasswordPath) {
|
|
12255
|
+
lines.push(` Generated password file: ${generatedPasswordPath}`);
|
|
12256
|
+
lines.push(" Use that file to retrieve the password for untrusted origins.");
|
|
10580
12257
|
}
|
|
10581
12258
|
const tunnelQrUrl = tunnelUrl ? buildTunnelAutologinUrl(tunnelUrl, password) : null;
|
|
10582
12259
|
if (tunnelUrl) {
|