codexui-android 0.1.102 → 0.1.104
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-R8Bjp_S3.js +2 -0
- package/dist/assets/{ReviewPane-BJuXPOZb.css → ReviewPane-AzgWKh85.css} +1 -1
- package/dist/assets/{ReviewPane-BwJrC8gt.js → ReviewPane-D97Q2j2l.js} +1 -1
- package/dist/assets/ThreadConversation-BptqFdFB.js +39 -0
- package/dist/assets/ThreadConversation-DEwOuZ3c.css +1 -0
- package/dist/assets/{ThreadTerminalPanel-BfzQNi0v.css → ThreadTerminalPanel-BcVTRuCH.css} +1 -1
- package/dist/assets/{ThreadTerminalPanel-CdyQTZa9.js → ThreadTerminalPanel-DMMkfF3u.js} +12 -12
- package/dist/assets/index-CEUy5PY0.css +1 -0
- package/dist/assets/index-Ck3aY8cA.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 +1753 -275
- 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-CGxQHsSZ.js +0 -2
- package/dist/assets/DirectoryHub-DMVpAtTC.css +0 -1
- package/dist/assets/ThreadConversation-B_1XpSrh.js +0 -39
- package/dist/assets/ThreadConversation-Gd3yNs76.css +0 -1
- package/dist/assets/index-CDFerptZ.css +0 -1
- package/dist/assets/index-Dpl251IF.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,34 @@ 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
|
+
}
|
|
6870
7945
|
function normalizeReasoningEffort(value) {
|
|
6871
7946
|
const allowed = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
6872
7947
|
return typeof value === "string" && allowed.includes(value) ? value : "";
|
|
@@ -6899,6 +7974,25 @@ function buildTextWithAttachments(prompt, files) {
|
|
|
6899
7974
|
${prompt}
|
|
6900
7975
|
`;
|
|
6901
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
|
+
}
|
|
6902
7996
|
function fileNameFromPath(pathValue) {
|
|
6903
7997
|
const normalized = pathValue.replace(/\\/g, "/");
|
|
6904
7998
|
const segments = normalized.split("/").filter(Boolean);
|
|
@@ -7019,7 +8113,9 @@ async function readWorkspaceRootsState() {
|
|
|
7019
8113
|
return {
|
|
7020
8114
|
order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
|
|
7021
8115
|
labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
|
|
7022
|
-
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"])
|
|
7023
8119
|
};
|
|
7024
8120
|
}
|
|
7025
8121
|
async function writeWorkspaceRootsState(nextState) {
|
|
@@ -7034,8 +8130,58 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
7034
8130
|
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
7035
8131
|
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
7036
8132
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
8133
|
+
payload["project-order"] = normalizeStringArray(nextState.projectOrder);
|
|
7037
8134
|
await writeFile4(statePath, JSON.stringify(payload), "utf8");
|
|
7038
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 });
|
|
8175
|
+
} catch {
|
|
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);
|
|
8183
|
+
}
|
|
8184
|
+
}
|
|
7039
8185
|
function normalizeTelegramBridgeConfig(value) {
|
|
7040
8186
|
const record = asRecord5(value);
|
|
7041
8187
|
if (!record) return { botToken: "", chatIds: [], allowedUserIds: [] };
|
|
@@ -7091,7 +8237,7 @@ function rememberTelegramChatId(chatId) {
|
|
|
7091
8237
|
});
|
|
7092
8238
|
return telegramBridgeConfigMutation;
|
|
7093
8239
|
}
|
|
7094
|
-
async function
|
|
8240
|
+
async function readJsonBody2(req) {
|
|
7095
8241
|
const raw = await readRawBody(req);
|
|
7096
8242
|
if (raw.length === 0) return null;
|
|
7097
8243
|
const text = raw.toString("utf8").trim();
|
|
@@ -7309,6 +8455,7 @@ var AppServerProcess = class {
|
|
|
7309
8455
|
this.lastThreadReadSnapshotByThreadId = /* @__PURE__ */ new Map();
|
|
7310
8456
|
this.capturedItemsByThreadId = /* @__PURE__ */ new Map();
|
|
7311
8457
|
this.liveStateCache = /* @__PURE__ */ new Map();
|
|
8458
|
+
this.chatgptAuthRefreshPromise = null;
|
|
7312
8459
|
}
|
|
7313
8460
|
getCodexCommand() {
|
|
7314
8461
|
const codexCommand = resolveCodexCommand();
|
|
@@ -7329,10 +8476,11 @@ var AppServerProcess = class {
|
|
|
7329
8476
|
const serverPort = parseInt(process.env.CODEXUI_SERVER_PORT ?? "", 10) || void 0;
|
|
7330
8477
|
const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
|
|
7331
8478
|
try {
|
|
7332
|
-
const
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
8479
|
+
const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
|
|
8480
|
+
if (state) {
|
|
8481
|
+
args.push(...getFreeModeConfigArgs(state, serverPort));
|
|
8482
|
+
extraEnv = getFreeModeEnvVars(state);
|
|
8483
|
+
}
|
|
7336
8484
|
} catch {
|
|
7337
8485
|
}
|
|
7338
8486
|
return { args, env: extraEnv };
|
|
@@ -7571,7 +8719,46 @@ var AppServerProcess = class {
|
|
|
7571
8719
|
}
|
|
7572
8720
|
});
|
|
7573
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
|
+
}
|
|
7574
8757
|
handleServerRequest(requestId, method, params) {
|
|
8758
|
+
if (method === "account/chatgptAuthTokens/refresh") {
|
|
8759
|
+
void this.handleChatgptAuthTokensRefreshRequest(requestId, params);
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
7575
8762
|
const pendingRequest = {
|
|
7576
8763
|
id: requestId,
|
|
7577
8764
|
method,
|
|
@@ -7694,53 +8881,124 @@ var BackendQueueProcessor = class {
|
|
|
7694
8881
|
constructor(appServer) {
|
|
7695
8882
|
this.appServer = appServer;
|
|
7696
8883
|
this.processingThreadIds = /* @__PURE__ */ new Set();
|
|
8884
|
+
this.queueDrainTimersByThreadId = /* @__PURE__ */ new Map();
|
|
8885
|
+
this.queueDrainDueAtByThreadId = /* @__PURE__ */ new Map();
|
|
7697
8886
|
this.unsubscribe = appServer.onNotification((notification) => {
|
|
7698
8887
|
if (!isTurnCompletedNotification(notification)) return;
|
|
7699
8888
|
const threadId = extractThreadIdFromNotificationParams(notification.params);
|
|
7700
8889
|
if (!threadId) return;
|
|
7701
8890
|
void this.processThreadQueue(threadId);
|
|
7702
8891
|
});
|
|
8892
|
+
void this.scheduleAllQueuedThreads(1e3);
|
|
7703
8893
|
}
|
|
7704
8894
|
dispose() {
|
|
7705
8895
|
this.unsubscribe();
|
|
8896
|
+
for (const timer of this.queueDrainTimersByThreadId.values()) {
|
|
8897
|
+
clearTimeout(timer);
|
|
8898
|
+
}
|
|
8899
|
+
this.queueDrainTimersByThreadId.clear();
|
|
8900
|
+
this.queueDrainDueAtByThreadId.clear();
|
|
7706
8901
|
this.processingThreadIds.clear();
|
|
7707
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
|
+
}
|
|
7708
8933
|
async processThreadQueue(threadId) {
|
|
7709
8934
|
if (this.processingThreadIds.has(threadId)) return;
|
|
7710
8935
|
this.processingThreadIds.add(threadId);
|
|
7711
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
|
+
}
|
|
7712
8944
|
const next = await this.popNextQueuedTurn(threadId);
|
|
7713
8945
|
if (!next) return;
|
|
7714
8946
|
try {
|
|
7715
8947
|
await this.startQueuedTurn(next);
|
|
8948
|
+
if (await this.hasQueuedTurns(threadId)) {
|
|
8949
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
8950
|
+
}
|
|
7716
8951
|
} catch {
|
|
7717
8952
|
await this.restoreQueuedTurn(next);
|
|
8953
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
7718
8954
|
}
|
|
7719
8955
|
} catch {
|
|
8956
|
+
this.scheduleThreadQueueDrain(threadId);
|
|
7720
8957
|
} finally {
|
|
7721
8958
|
this.processingThreadIds.delete(threadId);
|
|
7722
8959
|
}
|
|
7723
8960
|
}
|
|
7724
|
-
async
|
|
8961
|
+
async hasQueuedTurns(threadId) {
|
|
7725
8962
|
const state = await readThreadQueueState();
|
|
7726
8963
|
const queue = state[threadId];
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
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
|
+
});
|
|
7737
8991
|
}
|
|
7738
8992
|
async restoreQueuedTurn(turn) {
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
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
|
+
};
|
|
7744
9002
|
});
|
|
7745
9003
|
}
|
|
7746
9004
|
async resolveCollaborationModeSettings(mode) {
|
|
@@ -8091,6 +9349,10 @@ function createCodexBridgeMiddleware() {
|
|
|
8091
9349
|
}
|
|
8092
9350
|
const url = new URL(req.url, "http://localhost");
|
|
8093
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
|
+
}
|
|
8094
9356
|
const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
|
|
8095
9357
|
let bearerToken = "";
|
|
8096
9358
|
let wireApi = "chat";
|
|
@@ -8108,9 +9370,9 @@ function createCodexBridgeMiddleware() {
|
|
|
8108
9370
|
let bearerToken = "";
|
|
8109
9371
|
let wireApi = "responses";
|
|
8110
9372
|
try {
|
|
8111
|
-
const state =
|
|
8112
|
-
bearerToken = state
|
|
8113
|
-
wireApi = state
|
|
9373
|
+
const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath);
|
|
9374
|
+
bearerToken = state?.apiKey ?? "";
|
|
9375
|
+
wireApi = state?.wireApi === "chat" ? "chat" : "responses";
|
|
8114
9376
|
} catch {
|
|
8115
9377
|
}
|
|
8116
9378
|
handleOpenRouterProxyRequest(req, res, bearerToken, wireApi);
|
|
@@ -8133,17 +9395,13 @@ function createCodexBridgeMiddleware() {
|
|
|
8133
9395
|
}
|
|
8134
9396
|
if (url.pathname.startsWith("/codex-api/free-mode")) {
|
|
8135
9397
|
let readFreeModeState2 = function() {
|
|
8136
|
-
|
|
8137
|
-
return JSON.parse(readFileSync2(statePath, "utf8"));
|
|
8138
|
-
} catch {
|
|
8139
|
-
return { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
|
|
8140
|
-
}
|
|
9398
|
+
return ensureDefaultFreeModeStateForMissingAuthSync(statePath) ?? { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
|
|
8141
9399
|
};
|
|
8142
9400
|
var readFreeModeState = readFreeModeState2;
|
|
8143
9401
|
const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
|
|
8144
9402
|
if (req.method === "POST" && url.pathname === "/codex-api/free-mode") {
|
|
8145
9403
|
try {
|
|
8146
|
-
const body = await
|
|
9404
|
+
const body = await readJsonBody2(req);
|
|
8147
9405
|
const enable = Boolean(body?.enable);
|
|
8148
9406
|
if (enable) {
|
|
8149
9407
|
const apiKey = getRandomFreeKey();
|
|
@@ -8199,12 +9457,12 @@ function createCodexBridgeMiddleware() {
|
|
|
8199
9457
|
if (req.method === "GET" && url.pathname === "/codex-api/free-mode/status") {
|
|
8200
9458
|
try {
|
|
8201
9459
|
const state = readFreeModeState2();
|
|
8202
|
-
const freeModels = await getFreeModels();
|
|
8203
9460
|
const maskedKey = state.apiKey && state.customKey ? state.apiKey.substring(0, 12) + "..." + state.apiKey.substring(state.apiKey.length - 4) : null;
|
|
9461
|
+
refreshFreeModelsInBackground();
|
|
8204
9462
|
setJson4(res, 200, {
|
|
8205
9463
|
enabled: state.enabled,
|
|
8206
9464
|
keyCount: getFreeKeyCount(),
|
|
8207
|
-
models:
|
|
9465
|
+
models: getCachedFreeModels(),
|
|
8208
9466
|
currentModel: state.enabled ? state.model : null,
|
|
8209
9467
|
customKey: Boolean(state.customKey),
|
|
8210
9468
|
maskedKey,
|
|
@@ -8236,7 +9494,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8236
9494
|
}
|
|
8237
9495
|
if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-key") {
|
|
8238
9496
|
try {
|
|
8239
|
-
const body = await
|
|
9497
|
+
const body = await readJsonBody2(req);
|
|
8240
9498
|
const key = typeof body?.key === "string" ? body.key.trim() : "";
|
|
8241
9499
|
const current = readFreeModeState2();
|
|
8242
9500
|
if (key.length > 0) {
|
|
@@ -8271,7 +9529,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8271
9529
|
}
|
|
8272
9530
|
if (req.method === "POST" && url.pathname === "/codex-api/free-mode/custom-provider") {
|
|
8273
9531
|
try {
|
|
8274
|
-
const body = await
|
|
9532
|
+
const body = await readJsonBody2(req);
|
|
8275
9533
|
const baseUrl = typeof body?.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
8276
9534
|
const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
|
|
8277
9535
|
const wireApi = body?.wireApi === "chat" ? "chat" : "responses";
|
|
@@ -8314,10 +9572,10 @@ function createCodexBridgeMiddleware() {
|
|
|
8314
9572
|
if (await handleAccountRoutes(req, res, url, { appServer })) {
|
|
8315
9573
|
return;
|
|
8316
9574
|
}
|
|
8317
|
-
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
9575
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody: readJsonBody2 })) {
|
|
8318
9576
|
return;
|
|
8319
9577
|
}
|
|
8320
|
-
if (await handleReviewRoutes(req, res, url, { readJsonBody })) {
|
|
9578
|
+
if (await handleReviewRoutes(req, res, url, { readJsonBody: readJsonBody2 })) {
|
|
8321
9579
|
return;
|
|
8322
9580
|
}
|
|
8323
9581
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal/status") {
|
|
@@ -8343,7 +9601,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8343
9601
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8344
9602
|
return;
|
|
8345
9603
|
}
|
|
8346
|
-
const body = asRecord5(await
|
|
9604
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8347
9605
|
const threadId = readNonEmptyString(body?.threadId);
|
|
8348
9606
|
const cwd = readNonEmptyString(body?.cwd);
|
|
8349
9607
|
if (!threadId || !cwd) {
|
|
@@ -8367,7 +9625,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8367
9625
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8368
9626
|
return;
|
|
8369
9627
|
}
|
|
8370
|
-
const body = asRecord5(await
|
|
9628
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8371
9629
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
8372
9630
|
const data = typeof body?.data === "string" ? body.data : "";
|
|
8373
9631
|
if (!sessionId) {
|
|
@@ -8384,7 +9642,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8384
9642
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8385
9643
|
return;
|
|
8386
9644
|
}
|
|
8387
|
-
const body = asRecord5(await
|
|
9645
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8388
9646
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
8389
9647
|
if (!sessionId) {
|
|
8390
9648
|
setJson4(res, 400, { error: "Missing sessionId" });
|
|
@@ -8400,7 +9658,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8400
9658
|
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
8401
9659
|
return;
|
|
8402
9660
|
}
|
|
8403
|
-
const body = asRecord5(await
|
|
9661
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8404
9662
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
8405
9663
|
if (!sessionId) {
|
|
8406
9664
|
setJson4(res, 400, { error: "Missing sessionId" });
|
|
@@ -8424,7 +9682,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8424
9682
|
return;
|
|
8425
9683
|
}
|
|
8426
9684
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
8427
|
-
const payload = await
|
|
9685
|
+
const payload = await readJsonBody2(req);
|
|
8428
9686
|
const body = asRecord5(payload);
|
|
8429
9687
|
if (payload !== null && payload !== void 0) {
|
|
8430
9688
|
requestBodyBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
@@ -8434,9 +9692,10 @@ function createCodexBridgeMiddleware() {
|
|
|
8434
9692
|
setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
8435
9693
|
return;
|
|
8436
9694
|
}
|
|
8437
|
-
const rpcResult = await appServer
|
|
9695
|
+
const rpcResult = await callRpcWithArchiveRecovery(appServer, body.method, body.params ?? null);
|
|
8438
9696
|
const trimmedResult = trimThreadTurnsInRpcResult(body.method, rpcResult);
|
|
8439
|
-
const
|
|
9697
|
+
const sanitizedResult = await sanitizeThreadTurnsInlinePayloads(body.method, trimmedResult);
|
|
9698
|
+
const result = THREAD_METHODS_WITH_TURNS.has(body.method) ? await mergeSessionSkillInputsIntoThreadResult(sanitizedResult) : sanitizedResult;
|
|
8440
9699
|
if (THREAD_METHODS_WITH_TURNS.has(body.method)) {
|
|
8441
9700
|
const rpcRecord = asRecord5(result);
|
|
8442
9701
|
const rpcThread = asRecord5(rpcRecord?.thread);
|
|
@@ -8572,7 +9831,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8572
9831
|
}
|
|
8573
9832
|
if (req.method === "POST" && url.pathname === "/codex-api/thread/rollback-files") {
|
|
8574
9833
|
try {
|
|
8575
|
-
const body = asRecord5(await
|
|
9834
|
+
const body = asRecord5(await readJsonBody2(req));
|
|
8576
9835
|
const threadId = readNonEmptyString(body?.threadId);
|
|
8577
9836
|
const turnId = readNonEmptyString(body?.turnId);
|
|
8578
9837
|
const cwd = readNonEmptyString(body?.cwd);
|
|
@@ -8668,7 +9927,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8668
9927
|
}
|
|
8669
9928
|
if (req.method === "POST" && url.pathname === "/codex-api/composio/link") {
|
|
8670
9929
|
try {
|
|
8671
|
-
const payload = asRecord5(await
|
|
9930
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8672
9931
|
const slug = readNonEmptyString(payload?.slug);
|
|
8673
9932
|
setJson4(res, 200, await startComposioLink(slug));
|
|
8674
9933
|
} catch (error) {
|
|
@@ -8710,7 +9969,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8710
9969
|
return;
|
|
8711
9970
|
}
|
|
8712
9971
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
8713
|
-
const payload = await
|
|
9972
|
+
const payload = await readJsonBody2(req);
|
|
8714
9973
|
await appServer.respondToServerRequest(payload);
|
|
8715
9974
|
setJson4(res, 200, { ok: true });
|
|
8716
9975
|
return;
|
|
@@ -8731,8 +9990,8 @@ function createCodexBridgeMiddleware() {
|
|
|
8731
9990
|
}
|
|
8732
9991
|
if (req.method === "GET" && url.pathname === "/codex-api/provider-models") {
|
|
8733
9992
|
try {
|
|
8734
|
-
const fmState =
|
|
8735
|
-
if (fmState
|
|
9993
|
+
const fmState = ensureDefaultFreeModeStateForMissingAuthSync(join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE));
|
|
9994
|
+
if (fmState?.enabled) {
|
|
8736
9995
|
if (fmState.provider === "opencode-zen") {
|
|
8737
9996
|
try {
|
|
8738
9997
|
const modelsUrl = "https://opencode.ai/zen/v1/models";
|
|
@@ -8800,7 +10059,7 @@ function createCodexBridgeMiddleware() {
|
|
|
8800
10059
|
return;
|
|
8801
10060
|
}
|
|
8802
10061
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
8803
|
-
const payload = asRecord5(await
|
|
10062
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
8804
10063
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
8805
10064
|
const baseBranch = typeof payload?.baseBranch === "string" ? payload.baseBranch.trim() : "";
|
|
8806
10065
|
if (!rawSourceCwd) {
|
|
@@ -8858,6 +10117,12 @@ function createCodexBridgeMiddleware() {
|
|
|
8858
10117
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
8859
10118
|
await runCommand3("git", ["worktree", "add", "--detach", worktreeCwd, startPoint], { cwd: gitRoot });
|
|
8860
10119
|
}
|
|
10120
|
+
try {
|
|
10121
|
+
await persistWorkspaceRoot(worktreeCwd);
|
|
10122
|
+
} catch (error) {
|
|
10123
|
+
await rollbackCreatedWorktree(gitRoot, worktreeCwd, worktreeParent);
|
|
10124
|
+
throw error;
|
|
10125
|
+
}
|
|
8861
10126
|
setJson4(res, 200, {
|
|
8862
10127
|
data: {
|
|
8863
10128
|
cwd: worktreeCwd,
|
|
@@ -8870,6 +10135,75 @@ function createCodexBridgeMiddleware() {
|
|
|
8870
10135
|
}
|
|
8871
10136
|
return;
|
|
8872
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
|
+
}
|
|
8873
10207
|
if (req.method === "GET" && url.pathname === "/codex-api/worktree/branches") {
|
|
8874
10208
|
const rawSourceCwd = (url.searchParams.get("sourceCwd") ?? "").trim();
|
|
8875
10209
|
if (!rawSourceCwd) {
|
|
@@ -8956,11 +10290,11 @@ function createCodexBridgeMiddleware() {
|
|
|
8956
10290
|
});
|
|
8957
10291
|
return;
|
|
8958
10292
|
}
|
|
8959
|
-
const
|
|
8960
|
-
const currentBranch =
|
|
10293
|
+
const state = await readGitHeaderState(gitRoot);
|
|
10294
|
+
const currentBranch = state.currentBranch;
|
|
8961
10295
|
const output = await runCommandCapture2(
|
|
8962
10296
|
"git",
|
|
8963
|
-
["for-each-ref", "--format=%(committerdate:unix) %(refname)", "refs/heads", "refs/remotes"],
|
|
10297
|
+
["for-each-ref", "--format=%(committerdate:unix) %(refname) %(objectname)", "refs/heads", "refs/remotes"],
|
|
8964
10298
|
{ cwd: gitRoot }
|
|
8965
10299
|
);
|
|
8966
10300
|
const branchActivityByName = /* @__PURE__ */ new Map();
|
|
@@ -8970,23 +10304,29 @@ function createCodexBridgeMiddleware() {
|
|
|
8970
10304
|
if (!normalized || normalized === "origin/HEAD") continue;
|
|
8971
10305
|
const parsedTimestamp = Number.parseInt(rawTimestamp.trim(), 10);
|
|
8972
10306
|
const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : 0;
|
|
8973
|
-
const
|
|
8974
|
-
|
|
8975
|
-
|
|
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 });
|
|
8976
10311
|
}
|
|
8977
10312
|
}
|
|
8978
10313
|
if (currentBranch && !branchActivityByName.has(currentBranch)) {
|
|
8979
|
-
branchActivityByName.set(currentBranch, Number.MAX_SAFE_INTEGER);
|
|
10314
|
+
branchActivityByName.set(currentBranch, { timestamp: Number.MAX_SAFE_INTEGER, isRemote: false });
|
|
8980
10315
|
}
|
|
8981
|
-
const options = Array.from(branchActivityByName.entries()).map(([value]) => ({
|
|
8982
|
-
|
|
8983
|
-
|
|
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;
|
|
8984
10324
|
if (bActivity !== aActivity) return bActivity - aActivity;
|
|
8985
10325
|
return a.value.localeCompare(b.value);
|
|
8986
10326
|
});
|
|
8987
10327
|
setJson4(res, 200, {
|
|
8988
10328
|
data: {
|
|
8989
|
-
|
|
10329
|
+
...state,
|
|
8990
10330
|
options
|
|
8991
10331
|
}
|
|
8992
10332
|
});
|
|
@@ -8995,8 +10335,47 @@ function createCodexBridgeMiddleware() {
|
|
|
8995
10335
|
}
|
|
8996
10336
|
return;
|
|
8997
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
|
+
}
|
|
8998
10377
|
if (req.method === "POST" && url.pathname === "/codex-api/git/checkout") {
|
|
8999
|
-
const payload = await
|
|
10378
|
+
const payload = await readJsonBody2(req);
|
|
9000
10379
|
const record = asRecord5(payload);
|
|
9001
10380
|
if (!record) {
|
|
9002
10381
|
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
@@ -9025,52 +10404,127 @@ function createCodexBridgeMiddleware() {
|
|
|
9025
10404
|
}
|
|
9026
10405
|
try {
|
|
9027
10406
|
const gitRoot = await runCommandCapture2("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
const blockingWorktreePath = extractBranchLockedWorktreePath(checkoutError, targetBranch);
|
|
9032
|
-
if (!blockingWorktreePath) {
|
|
9033
|
-
throw checkoutError;
|
|
9034
|
-
}
|
|
9035
|
-
await runCommand3("git", ["checkout", "--detach"], { cwd: blockingWorktreePath });
|
|
9036
|
-
await runCommand3("git", ["checkout", targetBranch], { cwd: gitRoot });
|
|
9037
|
-
}
|
|
9038
|
-
const currentBranch = (await runCommandCapture2("git", ["branch", "--show-current"], { cwd: gitRoot })).trim() || null;
|
|
9039
|
-
setJson4(res, 200, { data: { currentBranch } });
|
|
10407
|
+
await assertNoTrackedGitChanges(gitRoot);
|
|
10408
|
+
await checkoutGitBranchWithWorktreeRecovery(gitRoot, targetBranch);
|
|
10409
|
+
setJson4(res, 200, { data: await readGitHeaderState(gitRoot) });
|
|
9040
10410
|
} catch (error) {
|
|
9041
10411
|
setJson4(res, 500, { error: getErrorMessage5(error, "Failed to switch branch") });
|
|
9042
10412
|
}
|
|
9043
10413
|
return;
|
|
9044
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
|
+
}
|
|
9045
10497
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
9046
|
-
const payload = await
|
|
10498
|
+
const payload = await readJsonBody2(req);
|
|
9047
10499
|
const record = asRecord5(payload);
|
|
9048
10500
|
if (!record) {
|
|
9049
10501
|
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
9050
10502
|
return;
|
|
9051
10503
|
}
|
|
9052
|
-
|
|
10504
|
+
await updateWorkspaceRootsState((existingState) => ({
|
|
9053
10505
|
order: normalizeStringArray(record.order),
|
|
9054
10506
|
labels: normalizeStringRecord(record.labels),
|
|
9055
|
-
active: normalizeStringArray(record.active)
|
|
9056
|
-
|
|
9057
|
-
|
|
10507
|
+
active: normalizeStringArray(record.active),
|
|
10508
|
+
projectOrder: Array.isArray(record.projectOrder) ? normalizeStringArray(record.projectOrder) : existingState.projectOrder,
|
|
10509
|
+
remoteProjects: existingState.remoteProjects
|
|
10510
|
+
}));
|
|
9058
10511
|
setJson4(res, 200, { ok: true });
|
|
9059
10512
|
return;
|
|
9060
10513
|
}
|
|
9061
10514
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-queue-state") {
|
|
9062
|
-
const payload = await
|
|
10515
|
+
const payload = await readJsonBody2(req);
|
|
9063
10516
|
const record = asRecord5(payload);
|
|
9064
10517
|
if (!record) {
|
|
9065
10518
|
setJson4(res, 400, { error: "Invalid body: expected object" });
|
|
9066
10519
|
return;
|
|
9067
10520
|
}
|
|
9068
10521
|
await writeThreadQueueState(normalizeThreadQueueState(record));
|
|
10522
|
+
void backendQueueProcessor.scheduleAllQueuedThreads();
|
|
9069
10523
|
setJson4(res, 200, { ok: true });
|
|
9070
10524
|
return;
|
|
9071
10525
|
}
|
|
9072
10526
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
9073
|
-
const payload = asRecord5(await
|
|
10527
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9074
10528
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
9075
10529
|
const createIfMissing = payload?.createIfMissing === true;
|
|
9076
10530
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
@@ -9095,23 +10549,12 @@ function createCodexBridgeMiddleware() {
|
|
|
9095
10549
|
setJson4(res, 404, { error: "Directory does not exist" });
|
|
9096
10550
|
return;
|
|
9097
10551
|
}
|
|
9098
|
-
|
|
9099
|
-
const nextOrder = [normalizedPath, ...existingState.order.filter((item) => item !== normalizedPath)];
|
|
9100
|
-
const nextActive = [normalizedPath, ...existingState.active.filter((item) => item !== normalizedPath)];
|
|
9101
|
-
const nextLabels = { ...existingState.labels };
|
|
9102
|
-
if (label.trim().length > 0) {
|
|
9103
|
-
nextLabels[normalizedPath] = label.trim();
|
|
9104
|
-
}
|
|
9105
|
-
await writeWorkspaceRootsState({
|
|
9106
|
-
order: nextOrder,
|
|
9107
|
-
labels: nextLabels,
|
|
9108
|
-
active: nextActive
|
|
9109
|
-
});
|
|
10552
|
+
await persistWorkspaceRoot(normalizedPath, label);
|
|
9110
10553
|
setJson4(res, 200, { data: { path: normalizedPath } });
|
|
9111
10554
|
return;
|
|
9112
10555
|
}
|
|
9113
10556
|
if (req.method === "POST" && url.pathname === "/codex-api/local-directory") {
|
|
9114
|
-
const payload = asRecord5(await
|
|
10557
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9115
10558
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
9116
10559
|
if (!rawPath) {
|
|
9117
10560
|
setJson4(res, 400, { error: "Missing path" });
|
|
@@ -9131,7 +10574,7 @@ function createCodexBridgeMiddleware() {
|
|
|
9131
10574
|
return;
|
|
9132
10575
|
}
|
|
9133
10576
|
if (req.method === "POST" && url.pathname === "/codex-api/projectless-thread-cwd") {
|
|
9134
|
-
const payload = asRecord5(await
|
|
10577
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9135
10578
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt : null;
|
|
9136
10579
|
try {
|
|
9137
10580
|
const directory = await createProjectlessThreadDirectory(prompt);
|
|
@@ -9175,7 +10618,7 @@ function createCodexBridgeMiddleware() {
|
|
|
9175
10618
|
return;
|
|
9176
10619
|
}
|
|
9177
10620
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
9178
|
-
const payload = asRecord5(await
|
|
10621
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9179
10622
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
9180
10623
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
9181
10624
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
@@ -9209,7 +10652,7 @@ function createCodexBridgeMiddleware() {
|
|
|
9209
10652
|
return;
|
|
9210
10653
|
}
|
|
9211
10654
|
if (req.method === "POST" && url.pathname === "/codex-api/prompts") {
|
|
9212
|
-
const payload = asRecord5(await
|
|
10655
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9213
10656
|
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
9214
10657
|
const content = typeof payload?.content === "string" ? payload.content : "";
|
|
9215
10658
|
if (!name || !content.trim()) {
|
|
@@ -9260,16 +10703,17 @@ function createCodexBridgeMiddleware() {
|
|
|
9260
10703
|
}
|
|
9261
10704
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
|
|
9262
10705
|
const threadId = url.searchParams.get("threadId")?.trim() ?? "";
|
|
10706
|
+
const automationId = url.searchParams.get("automationId")?.trim() ?? "";
|
|
9263
10707
|
if (!threadId) {
|
|
9264
10708
|
setJson4(res, 400, { error: "Missing threadId" });
|
|
9265
10709
|
return;
|
|
9266
10710
|
}
|
|
9267
|
-
const automation = await readThreadHeartbeatAutomation(threadId);
|
|
10711
|
+
const automation = automationId ? await readThreadHeartbeatAutomation(threadId, automationId) : await readThreadHeartbeatAutomations(threadId);
|
|
9268
10712
|
setJson4(res, 200, { data: automation });
|
|
9269
10713
|
return;
|
|
9270
10714
|
}
|
|
9271
10715
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
9272
|
-
const payload = asRecord5(await
|
|
10716
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9273
10717
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
9274
10718
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
9275
10719
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
@@ -9283,7 +10727,7 @@ function createCodexBridgeMiddleware() {
|
|
|
9283
10727
|
return;
|
|
9284
10728
|
}
|
|
9285
10729
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
9286
|
-
const payload = asRecord5(await
|
|
10730
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9287
10731
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
9288
10732
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
9289
10733
|
if (!id) {
|
|
@@ -9297,22 +10741,23 @@ function createCodexBridgeMiddleware() {
|
|
|
9297
10741
|
return;
|
|
9298
10742
|
}
|
|
9299
10743
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-pins") {
|
|
9300
|
-
const payload = asRecord5(await
|
|
10744
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9301
10745
|
const threadIds = normalizePinnedThreadIds(payload?.threadIds);
|
|
9302
10746
|
await writePinnedThreadIds(threadIds);
|
|
9303
10747
|
setJson4(res, 200, { ok: true });
|
|
9304
10748
|
return;
|
|
9305
10749
|
}
|
|
9306
10750
|
if (req.method === "PUT" && url.pathname === "/codex-api/preferences/first-launch-plugins-card") {
|
|
9307
|
-
const payload = asRecord5(await
|
|
10751
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9308
10752
|
const dismissed = payload?.dismissed === true;
|
|
9309
10753
|
await writeFirstLaunchPluginsCardDismissed(dismissed);
|
|
9310
10754
|
setJson4(res, 200, { ok: true });
|
|
9311
10755
|
return;
|
|
9312
10756
|
}
|
|
9313
10757
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
|
|
9314
|
-
const payload = asRecord5(await
|
|
10758
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9315
10759
|
const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
|
|
10760
|
+
const id = typeof payload?.id === "string" ? payload.id.trim() : "";
|
|
9316
10761
|
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
9317
10762
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
|
|
9318
10763
|
const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
|
|
@@ -9321,22 +10766,41 @@ function createCodexBridgeMiddleware() {
|
|
|
9321
10766
|
setJson4(res, 400, { error: "threadId, name, prompt, and rrule are required" });
|
|
9322
10767
|
return;
|
|
9323
10768
|
}
|
|
9324
|
-
const automation = await writeThreadHeartbeatAutomation({ threadId, name, prompt, rrule, status });
|
|
10769
|
+
const automation = await writeThreadHeartbeatAutomation({ threadId, id, name, prompt, rrule, status });
|
|
9325
10770
|
setJson4(res, 200, { data: automation });
|
|
9326
10771
|
return;
|
|
9327
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
|
+
}
|
|
9328
10791
|
if (req.method === "DELETE" && url.pathname === "/codex-api/thread-automation") {
|
|
9329
10792
|
const threadId = url.searchParams.get("threadId")?.trim() ?? "";
|
|
10793
|
+
const automationId = url.searchParams.get("automationId")?.trim() ?? "";
|
|
9330
10794
|
if (!threadId) {
|
|
9331
10795
|
setJson4(res, 400, { error: "Missing threadId" });
|
|
9332
10796
|
return;
|
|
9333
10797
|
}
|
|
9334
|
-
const removed = await deleteThreadHeartbeatAutomation(threadId);
|
|
10798
|
+
const removed = await deleteThreadHeartbeatAutomation(threadId, automationId);
|
|
9335
10799
|
setJson4(res, 200, { data: { removed } });
|
|
9336
10800
|
return;
|
|
9337
10801
|
}
|
|
9338
10802
|
if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
|
|
9339
|
-
const payload = asRecord5(await
|
|
10803
|
+
const payload = asRecord5(await readJsonBody2(req));
|
|
9340
10804
|
const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
|
|
9341
10805
|
const rawAllowedUserIds = Array.isArray(payload?.allowedUserIds) ? payload.allowedUserIds : [];
|
|
9342
10806
|
if (!botToken) {
|
|
@@ -9443,7 +10907,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
9443
10907
|
|
|
9444
10908
|
// src/server/authMiddleware.ts
|
|
9445
10909
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
9446
|
-
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";
|
|
9447
10911
|
import { homedir as homedir6 } from "os";
|
|
9448
10912
|
import { dirname as dirname3, join as join7 } from "path";
|
|
9449
10913
|
var TOKEN_COOKIE = "portal_session";
|
|
@@ -9525,10 +10989,10 @@ function readPersistedSessions() {
|
|
|
9525
10989
|
}
|
|
9526
10990
|
function persistSessions(validTokens) {
|
|
9527
10991
|
const sessionStorePath = getSessionStorePath();
|
|
9528
|
-
|
|
10992
|
+
mkdirSync2(dirname3(sessionStorePath), { recursive: true });
|
|
9529
10993
|
const tokens = Array.from(validTokens.entries()).sort((left, right) => right[1] - left[1]).slice(0, MAX_PERSISTED_TOKENS).map(([value, expiresAt]) => ({ value, expiresAt }));
|
|
9530
10994
|
const tmpPath = `${sessionStorePath}.tmp`;
|
|
9531
|
-
|
|
10995
|
+
writeFileSync3(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
|
|
9532
10996
|
`, { encoding: "utf8", mode: 384 });
|
|
9533
10997
|
renameSync(tmpPath, sessionStorePath);
|
|
9534
10998
|
}
|
|
@@ -10325,7 +11789,7 @@ function hasPromptedCloudflaredInstallPersisted() {
|
|
|
10325
11789
|
}
|
|
10326
11790
|
async function persistCloudflaredInstallPrompted() {
|
|
10327
11791
|
const codexHome = getCodexHomePath();
|
|
10328
|
-
|
|
11792
|
+
mkdirSync3(codexHome, { recursive: true });
|
|
10329
11793
|
await writeFile6(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
10330
11794
|
`, "utf8");
|
|
10331
11795
|
}
|
|
@@ -10411,7 +11875,7 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
10411
11875
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
10412
11876
|
}
|
|
10413
11877
|
const userBinDir = join10(homedir7(), ".local", "bin");
|
|
10414
|
-
|
|
11878
|
+
mkdirSync3(userBinDir, { recursive: true });
|
|
10415
11879
|
const destination = join10(userBinDir, "cloudflared");
|
|
10416
11880
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
10417
11881
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
@@ -10511,12 +11975,24 @@ Global npm install requires elevated permissions. Retrying with --prefix ${userP
|
|
|
10511
11975
|
}
|
|
10512
11976
|
function resolvePassword(input) {
|
|
10513
11977
|
if (input === false) {
|
|
10514
|
-
return void 0;
|
|
11978
|
+
return { password: void 0, generated: false };
|
|
10515
11979
|
}
|
|
10516
11980
|
if (typeof input === "string") {
|
|
10517
|
-
return input;
|
|
11981
|
+
return { password: input, generated: false };
|
|
10518
11982
|
}
|
|
10519
|
-
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;
|
|
10520
11996
|
}
|
|
10521
11997
|
function printTermuxKeepAlive(lines) {
|
|
10522
11998
|
if (!isTermuxRuntime()) {
|
|
@@ -10535,9 +12011,8 @@ function openBrowser(url) {
|
|
|
10535
12011
|
});
|
|
10536
12012
|
child.unref();
|
|
10537
12013
|
}
|
|
10538
|
-
function buildTunnelAutologinUrl(tunnelUrl,
|
|
10539
|
-
|
|
10540
|
-
return `${tunnelUrl}/password=${encodeURIComponent(password)}`;
|
|
12014
|
+
function buildTunnelAutologinUrl(tunnelUrl, _password) {
|
|
12015
|
+
return tunnelUrl;
|
|
10541
12016
|
}
|
|
10542
12017
|
function parseCloudflaredUrl(chunk) {
|
|
10543
12018
|
const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
|
|
@@ -10732,7 +12207,9 @@ async function startServer(options) {
|
|
|
10732
12207
|
console.log("\nCodex is not logged in. You can log in later via settings or run `codexui login`.\n");
|
|
10733
12208
|
}
|
|
10734
12209
|
const requestedPort = parseInt(options.port, 10);
|
|
10735
|
-
const
|
|
12210
|
+
const passwordResolution = resolvePassword(options.password);
|
|
12211
|
+
const password = passwordResolution.password;
|
|
12212
|
+
const generatedPasswordPath = password && passwordResolution.generated ? await persistGeneratedPassword(password) : null;
|
|
10736
12213
|
const { app, dispose, attachWebSocket } = createServer({ password });
|
|
10737
12214
|
const server = createServer2(app);
|
|
10738
12215
|
attachWebSocket(server);
|
|
@@ -10774,8 +12251,9 @@ async function startServer(options) {
|
|
|
10774
12251
|
if (port !== requestedPort) {
|
|
10775
12252
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
10776
12253
|
}
|
|
10777
|
-
if (
|
|
10778
|
-
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.");
|
|
10779
12257
|
}
|
|
10780
12258
|
const tunnelQrUrl = tunnelUrl ? buildTunnelAutologinUrl(tunnelUrl, password) : null;
|
|
10781
12259
|
if (tunnelUrl) {
|