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