codexapp 0.1.52 → 0.1.53
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 +0 -51
- package/dist/assets/index-B_hsCD9g.js +1425 -0
- package/dist/assets/index-Dpf9bMnE.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +153 -1069
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-B6t8lUoD.css +0 -1
- package/dist/assets/index-CGldz3Gt.js +0 -1428
package/dist-cli/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync, createWriteStream, existsSync as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
6
|
import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { isAbsolute as isAbsolute3, join as
|
|
9
|
-
import { spawn as spawn3 } from "child_process";
|
|
10
|
-
import { createInterface
|
|
7
|
+
import { homedir as homedir3, networkInterfaces } from "os";
|
|
8
|
+
import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
|
|
9
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import { dirname as dirname3 } from "path";
|
|
13
13
|
import { get as httpsGet } from "https";
|
|
@@ -16,8 +16,8 @@ import qrcode from "qrcode-terminal";
|
|
|
16
16
|
|
|
17
17
|
// src/server/httpServer.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
|
-
import { dirname as dirname2, extname as
|
|
20
|
-
import { existsSync as
|
|
19
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
20
|
+
import { existsSync as existsSync2 } from "fs";
|
|
21
21
|
import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
|
|
22
22
|
import express from "express";
|
|
23
23
|
|
|
@@ -25,13 +25,10 @@ import express from "express";
|
|
|
25
25
|
import { spawn as spawn2 } from "child_process";
|
|
26
26
|
import { randomBytes } from "crypto";
|
|
27
27
|
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
28
|
-
import { createReadStream } from "fs";
|
|
29
|
-
import { request as httpRequest } from "http";
|
|
30
28
|
import { request as httpsRequest } from "https";
|
|
31
|
-
import { homedir as
|
|
29
|
+
import { homedir as homedir2 } from "os";
|
|
32
30
|
import { tmpdir as tmpdir2 } from "os";
|
|
33
|
-
import { basename
|
|
34
|
-
import { createInterface } from "readline";
|
|
31
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
35
32
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
36
33
|
|
|
37
34
|
// src/server/skillsRoutes.ts
|
|
@@ -70,17 +67,6 @@ function getCodexHomeDir() {
|
|
|
70
67
|
function getSkillsInstallDir() {
|
|
71
68
|
return join(getCodexHomeDir(), "skills");
|
|
72
69
|
}
|
|
73
|
-
function resolveSkillInstallerScriptPath() {
|
|
74
|
-
const candidates = [
|
|
75
|
-
join(getCodexHomeDir(), "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
|
|
76
|
-
join(homedir(), ".codex", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
|
|
77
|
-
join(homedir(), ".cursor", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py")
|
|
78
|
-
];
|
|
79
|
-
for (const candidate of candidates) {
|
|
80
|
-
if (existsSync(candidate)) return candidate;
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`Skill installer script not found. Checked: ${candidates.join(", ")}`);
|
|
83
|
-
}
|
|
84
70
|
async function runCommand(command, args, options = {}) {
|
|
85
71
|
await new Promise((resolve3, reject) => {
|
|
86
72
|
const proc = spawn(command, args, {
|
|
@@ -280,23 +266,6 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
280
266
|
}
|
|
281
267
|
return map;
|
|
282
268
|
}
|
|
283
|
-
function extractSkillDescriptionFromMarkdown(markdown) {
|
|
284
|
-
const lines = markdown.split(/\r?\n/);
|
|
285
|
-
let inCodeFence = false;
|
|
286
|
-
for (const rawLine of lines) {
|
|
287
|
-
const line = rawLine.trim();
|
|
288
|
-
if (line.startsWith("```")) {
|
|
289
|
-
inCodeFence = !inCodeFence;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (inCodeFence || line.length === 0) continue;
|
|
293
|
-
if (line.startsWith("#")) continue;
|
|
294
|
-
if (line.startsWith(">")) continue;
|
|
295
|
-
if (line.startsWith("- ") || line.startsWith("* ")) continue;
|
|
296
|
-
return line;
|
|
297
|
-
}
|
|
298
|
-
return "";
|
|
299
|
-
}
|
|
300
269
|
function getSkillsSyncStatePath() {
|
|
301
270
|
return join(getCodexHomeDir(), "skills-sync.json");
|
|
302
271
|
}
|
|
@@ -1022,7 +991,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1022
991
|
}
|
|
1023
992
|
const localDir = await detectUserSkillsDir(appServer);
|
|
1024
993
|
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
|
|
1025
|
-
const installerScript =
|
|
994
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1026
995
|
const localSkills = await scanInstalledSkillsFromDisk();
|
|
1027
996
|
for (const skill of remote) {
|
|
1028
997
|
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
@@ -1069,29 +1038,15 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1069
1038
|
try {
|
|
1070
1039
|
const owner = url.searchParams.get("owner") || "";
|
|
1071
1040
|
const name = url.searchParams.get("name") || "";
|
|
1072
|
-
const installed = url.searchParams.get("installed") === "true";
|
|
1073
|
-
const skillPath = url.searchParams.get("path") || "";
|
|
1074
1041
|
if (!owner || !name) {
|
|
1075
1042
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1076
1043
|
return true;
|
|
1077
1044
|
}
|
|
1078
|
-
if (installed) {
|
|
1079
|
-
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1080
|
-
const installedInfo = installedMap.get(name);
|
|
1081
|
-
const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
|
|
1082
|
-
if (localSkillPath) {
|
|
1083
|
-
const content2 = await readFile(localSkillPath, "utf8");
|
|
1084
|
-
const description2 = extractSkillDescriptionFromMarkdown(content2);
|
|
1085
|
-
setJson(res, 200, { content: content2, description: description2, source: "local" });
|
|
1086
|
-
return true;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
1045
|
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1090
1046
|
const resp = await fetch(rawUrl);
|
|
1091
1047
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1092
1048
|
const content = await resp.text();
|
|
1093
|
-
|
|
1094
|
-
setJson(res, 200, { content, description, source: "remote" });
|
|
1049
|
+
setJson(res, 200, { content });
|
|
1095
1050
|
} catch (error) {
|
|
1096
1051
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1097
1052
|
}
|
|
@@ -1106,7 +1061,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1106
1061
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1107
1062
|
return true;
|
|
1108
1063
|
}
|
|
1109
|
-
const installerScript =
|
|
1064
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1110
1065
|
const installDest = await detectUserSkillsDir(appServer);
|
|
1111
1066
|
await runCommand("python3", [
|
|
1112
1067
|
installerScript,
|
|
@@ -1162,8 +1117,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1162
1117
|
return false;
|
|
1163
1118
|
}
|
|
1164
1119
|
|
|
1165
|
-
// src/server/
|
|
1166
|
-
import { basename } from "path";
|
|
1120
|
+
// src/server/codexAppServerBridge.ts
|
|
1167
1121
|
function asRecord2(value) {
|
|
1168
1122
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1169
1123
|
}
|
|
@@ -1181,464 +1135,21 @@ function getErrorMessage2(payload, fallback) {
|
|
|
1181
1135
|
}
|
|
1182
1136
|
return fallback;
|
|
1183
1137
|
}
|
|
1184
|
-
var TelegramThreadBridge = class {
|
|
1185
|
-
constructor(appServer) {
|
|
1186
|
-
this.threadIdByChatId = /* @__PURE__ */ new Map();
|
|
1187
|
-
this.chatIdsByThreadId = /* @__PURE__ */ new Map();
|
|
1188
|
-
this.lastForwardedTurnByThreadId = /* @__PURE__ */ new Map();
|
|
1189
|
-
this.active = false;
|
|
1190
|
-
this.pollingTask = null;
|
|
1191
|
-
this.nextUpdateOffset = 0;
|
|
1192
|
-
this.lastError = "";
|
|
1193
|
-
this.appServer = appServer;
|
|
1194
|
-
this.token = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
|
|
1195
|
-
this.defaultCwd = process.env.TELEGRAM_DEFAULT_CWD?.trim() ?? process.cwd();
|
|
1196
|
-
}
|
|
1197
|
-
start() {
|
|
1198
|
-
if (!this.token || this.active) return;
|
|
1199
|
-
this.active = true;
|
|
1200
|
-
void this.notifyOnlineForKnownChats().catch(() => {
|
|
1201
|
-
});
|
|
1202
|
-
this.pollingTask = this.pollLoop();
|
|
1203
|
-
this.appServer.onNotification((notification) => {
|
|
1204
|
-
void this.handleNotification(notification).catch(() => {
|
|
1205
|
-
});
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
stop() {
|
|
1209
|
-
this.active = false;
|
|
1210
|
-
}
|
|
1211
|
-
async pollLoop() {
|
|
1212
|
-
while (this.active) {
|
|
1213
|
-
try {
|
|
1214
|
-
const updates = await this.getUpdates();
|
|
1215
|
-
this.lastError = "";
|
|
1216
|
-
for (const update of updates) {
|
|
1217
|
-
const updateId = typeof update.update_id === "number" ? update.update_id : -1;
|
|
1218
|
-
if (updateId >= 0) {
|
|
1219
|
-
this.nextUpdateOffset = Math.max(this.nextUpdateOffset, updateId + 1);
|
|
1220
|
-
}
|
|
1221
|
-
await this.handleIncomingUpdate(update);
|
|
1222
|
-
}
|
|
1223
|
-
} catch (error) {
|
|
1224
|
-
this.lastError = getErrorMessage2(error, "Telegram polling failed");
|
|
1225
|
-
await new Promise((resolve3) => setTimeout(resolve3, 1500));
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
async getUpdates() {
|
|
1230
|
-
if (!this.token) {
|
|
1231
|
-
throw new Error("Telegram bot token is not configured");
|
|
1232
|
-
}
|
|
1233
|
-
const response = await fetch(this.apiUrl("getUpdates"), {
|
|
1234
|
-
method: "POST",
|
|
1235
|
-
headers: { "Content-Type": "application/json" },
|
|
1236
|
-
body: JSON.stringify({
|
|
1237
|
-
timeout: 45,
|
|
1238
|
-
offset: this.nextUpdateOffset,
|
|
1239
|
-
allowed_updates: ["message", "callback_query"]
|
|
1240
|
-
})
|
|
1241
|
-
});
|
|
1242
|
-
const payload = asRecord2(await response.json());
|
|
1243
|
-
const result = Array.isArray(payload?.result) ? payload.result : [];
|
|
1244
|
-
return result;
|
|
1245
|
-
}
|
|
1246
|
-
apiUrl(method) {
|
|
1247
|
-
return `https://api.telegram.org/bot${this.token}/${method}`;
|
|
1248
|
-
}
|
|
1249
|
-
configureToken(token) {
|
|
1250
|
-
const normalizedToken = token.trim();
|
|
1251
|
-
if (!normalizedToken) {
|
|
1252
|
-
throw new Error("Telegram bot token is required");
|
|
1253
|
-
}
|
|
1254
|
-
this.token = normalizedToken;
|
|
1255
|
-
}
|
|
1256
|
-
getStatus() {
|
|
1257
|
-
return {
|
|
1258
|
-
configured: this.token.length > 0,
|
|
1259
|
-
active: this.active,
|
|
1260
|
-
mappedChats: this.threadIdByChatId.size,
|
|
1261
|
-
mappedThreads: this.chatIdsByThreadId.size,
|
|
1262
|
-
lastError: this.lastError
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
connectThread(threadId, chatId, token) {
|
|
1266
|
-
const normalizedThreadId = threadId.trim();
|
|
1267
|
-
if (!normalizedThreadId) {
|
|
1268
|
-
throw new Error("threadId is required");
|
|
1269
|
-
}
|
|
1270
|
-
if (!Number.isFinite(chatId)) {
|
|
1271
|
-
throw new Error("chatId must be a number");
|
|
1272
|
-
}
|
|
1273
|
-
if (typeof token === "string" && token.trim().length > 0) {
|
|
1274
|
-
this.configureToken(token);
|
|
1275
|
-
}
|
|
1276
|
-
if (!this.token) {
|
|
1277
|
-
throw new Error("Telegram bot token is not configured");
|
|
1278
|
-
}
|
|
1279
|
-
this.bindChatToThread(chatId, normalizedThreadId);
|
|
1280
|
-
this.start();
|
|
1281
|
-
void this.sendOnlineMessage(chatId).catch(() => {
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
async sendTelegramMessage(chatId, text, options = {}) {
|
|
1285
|
-
const message = text.trim();
|
|
1286
|
-
if (!message) return;
|
|
1287
|
-
const payload = { chat_id: chatId, text: message };
|
|
1288
|
-
if (options.replyMarkup) {
|
|
1289
|
-
payload.reply_markup = options.replyMarkup;
|
|
1290
|
-
}
|
|
1291
|
-
await fetch(this.apiUrl("sendMessage"), {
|
|
1292
|
-
method: "POST",
|
|
1293
|
-
headers: { "Content-Type": "application/json" },
|
|
1294
|
-
body: JSON.stringify(payload)
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1297
|
-
async sendOnlineMessage(chatId) {
|
|
1298
|
-
await this.sendTelegramMessage(chatId, "Codex thread bridge went online.");
|
|
1299
|
-
}
|
|
1300
|
-
async notifyOnlineForKnownChats() {
|
|
1301
|
-
const knownChatIds = Array.from(this.threadIdByChatId.keys());
|
|
1302
|
-
for (const chatId of knownChatIds) {
|
|
1303
|
-
await this.sendOnlineMessage(chatId);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
async handleIncomingUpdate(update) {
|
|
1307
|
-
if (update.callback_query) {
|
|
1308
|
-
await this.handleCallbackQuery(update.callback_query);
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
const message = update.message;
|
|
1312
|
-
const chatId = message?.chat?.id;
|
|
1313
|
-
const text = message?.text?.trim();
|
|
1314
|
-
if (typeof chatId !== "number" || !text) return;
|
|
1315
|
-
if (text === "/start") {
|
|
1316
|
-
await this.sendThreadPicker(chatId);
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
if (text === "/newthread") {
|
|
1320
|
-
const threadId2 = await this.createThreadForChat(chatId);
|
|
1321
|
-
await this.sendTelegramMessage(chatId, `Mapped to new thread: ${threadId2}`);
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const threadCommand = text.match(/^\/thread\s+(\S+)$/);
|
|
1325
|
-
if (threadCommand) {
|
|
1326
|
-
const threadId2 = threadCommand[1];
|
|
1327
|
-
this.bindChatToThread(chatId, threadId2);
|
|
1328
|
-
await this.sendTelegramMessage(chatId, `Mapped to thread: ${threadId2}`);
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
const threadId = await this.ensureThreadForChat(chatId);
|
|
1332
|
-
try {
|
|
1333
|
-
await this.appServer.rpc("turn/start", {
|
|
1334
|
-
threadId,
|
|
1335
|
-
input: [{ type: "text", text }]
|
|
1336
|
-
});
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
const message2 = getErrorMessage2(error, "Failed to forward message to thread");
|
|
1339
|
-
await this.sendTelegramMessage(chatId, `Forward failed: ${message2}`);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
async handleCallbackQuery(callbackQuery) {
|
|
1343
|
-
const callbackId = typeof callbackQuery.id === "string" ? callbackQuery.id : "";
|
|
1344
|
-
const data = typeof callbackQuery.data === "string" ? callbackQuery.data : "";
|
|
1345
|
-
const chatId = callbackQuery.message?.chat?.id;
|
|
1346
|
-
if (!callbackId) return;
|
|
1347
|
-
if (!data.startsWith("thread:") || typeof chatId !== "number") {
|
|
1348
|
-
await this.answerCallbackQuery(callbackId, "Invalid selection");
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
const threadId = data.slice("thread:".length).trim();
|
|
1352
|
-
if (!threadId) {
|
|
1353
|
-
await this.answerCallbackQuery(callbackId, "Invalid thread id");
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
this.bindChatToThread(chatId, threadId);
|
|
1357
|
-
await this.answerCallbackQuery(callbackId, "Thread connected");
|
|
1358
|
-
await this.sendTelegramMessage(chatId, `Connected to thread: ${threadId}`);
|
|
1359
|
-
const history = await this.readThreadHistorySummary(threadId);
|
|
1360
|
-
if (history) {
|
|
1361
|
-
await this.sendTelegramMessage(chatId, history);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
async answerCallbackQuery(callbackQueryId, text) {
|
|
1365
|
-
await fetch(this.apiUrl("answerCallbackQuery"), {
|
|
1366
|
-
method: "POST",
|
|
1367
|
-
headers: { "Content-Type": "application/json" },
|
|
1368
|
-
body: JSON.stringify({
|
|
1369
|
-
callback_query_id: callbackQueryId,
|
|
1370
|
-
text
|
|
1371
|
-
})
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
async sendThreadPicker(chatId) {
|
|
1375
|
-
const threads = await this.listRecentThreads();
|
|
1376
|
-
if (threads.length === 0) {
|
|
1377
|
-
await this.sendTelegramMessage(chatId, "No threads found. Send /newthread to create one.");
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
const inlineKeyboard = threads.map((thread) => [
|
|
1381
|
-
{
|
|
1382
|
-
text: thread.title,
|
|
1383
|
-
callback_data: `thread:${thread.id}`
|
|
1384
|
-
}
|
|
1385
|
-
]);
|
|
1386
|
-
await this.sendTelegramMessage(chatId, "Select a thread to connect:", {
|
|
1387
|
-
replyMarkup: { inline_keyboard: inlineKeyboard }
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
async listRecentThreads() {
|
|
1391
|
-
const payload = asRecord2(await this.appServer.rpc("thread/list", {
|
|
1392
|
-
archived: false,
|
|
1393
|
-
limit: 20,
|
|
1394
|
-
sortKey: "updated_at"
|
|
1395
|
-
}));
|
|
1396
|
-
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
|
1397
|
-
const threads = [];
|
|
1398
|
-
for (const row of rows) {
|
|
1399
|
-
const record = asRecord2(row);
|
|
1400
|
-
const id = typeof record?.id === "string" ? record.id.trim() : "";
|
|
1401
|
-
if (!id) continue;
|
|
1402
|
-
const name = typeof record?.name === "string" ? record.name.trim() : "";
|
|
1403
|
-
const preview = typeof record?.preview === "string" ? record.preview.trim() : "";
|
|
1404
|
-
const cwd = typeof record?.cwd === "string" ? record.cwd.trim() : "";
|
|
1405
|
-
const projectName = cwd ? basename(cwd) : "project";
|
|
1406
|
-
const threadTitle = (name || preview || id).replace(/\s+/g, " ").trim();
|
|
1407
|
-
const title = `${projectName}/${threadTitle}`.slice(0, 64);
|
|
1408
|
-
threads.push({ id, title });
|
|
1409
|
-
}
|
|
1410
|
-
return threads;
|
|
1411
|
-
}
|
|
1412
|
-
async createThreadForChat(chatId) {
|
|
1413
|
-
const response = asRecord2(await this.appServer.rpc("thread/start", { cwd: this.defaultCwd }));
|
|
1414
|
-
const thread = asRecord2(response?.thread);
|
|
1415
|
-
const threadId = typeof thread?.id === "string" ? thread.id : "";
|
|
1416
|
-
if (!threadId) {
|
|
1417
|
-
throw new Error("thread/start did not return thread id");
|
|
1418
|
-
}
|
|
1419
|
-
this.bindChatToThread(chatId, threadId);
|
|
1420
|
-
return threadId;
|
|
1421
|
-
}
|
|
1422
|
-
async ensureThreadForChat(chatId) {
|
|
1423
|
-
const existing = this.threadIdByChatId.get(chatId);
|
|
1424
|
-
if (existing) return existing;
|
|
1425
|
-
return this.createThreadForChat(chatId);
|
|
1426
|
-
}
|
|
1427
|
-
bindChatToThread(chatId, threadId) {
|
|
1428
|
-
const previousThreadId = this.threadIdByChatId.get(chatId);
|
|
1429
|
-
if (previousThreadId && previousThreadId !== threadId) {
|
|
1430
|
-
const previousSet = this.chatIdsByThreadId.get(previousThreadId);
|
|
1431
|
-
previousSet?.delete(chatId);
|
|
1432
|
-
if (previousSet && previousSet.size === 0) {
|
|
1433
|
-
this.chatIdsByThreadId.delete(previousThreadId);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
this.threadIdByChatId.set(chatId, threadId);
|
|
1437
|
-
const chatIds = this.chatIdsByThreadId.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
1438
|
-
chatIds.add(chatId);
|
|
1439
|
-
this.chatIdsByThreadId.set(threadId, chatIds);
|
|
1440
|
-
}
|
|
1441
|
-
extractThreadId(notification) {
|
|
1442
|
-
const params = asRecord2(notification.params);
|
|
1443
|
-
if (!params) return "";
|
|
1444
|
-
const directThreadId = typeof params.threadId === "string" ? params.threadId : "";
|
|
1445
|
-
if (directThreadId) return directThreadId;
|
|
1446
|
-
const turn = asRecord2(params.turn);
|
|
1447
|
-
const turnThreadId = typeof turn?.threadId === "string" ? turn.threadId : "";
|
|
1448
|
-
return turnThreadId;
|
|
1449
|
-
}
|
|
1450
|
-
extractTurnId(notification) {
|
|
1451
|
-
const params = asRecord2(notification.params);
|
|
1452
|
-
if (!params) return "";
|
|
1453
|
-
const directTurnId = typeof params.turnId === "string" ? params.turnId : "";
|
|
1454
|
-
if (directTurnId) return directTurnId;
|
|
1455
|
-
const turn = asRecord2(params.turn);
|
|
1456
|
-
const turnId = typeof turn?.id === "string" ? turn.id : "";
|
|
1457
|
-
return turnId;
|
|
1458
|
-
}
|
|
1459
|
-
async handleNotification(notification) {
|
|
1460
|
-
if (notification.method !== "turn/completed") return;
|
|
1461
|
-
const threadId = this.extractThreadId(notification);
|
|
1462
|
-
if (!threadId) return;
|
|
1463
|
-
const chatIds = this.chatIdsByThreadId.get(threadId);
|
|
1464
|
-
if (!chatIds || chatIds.size === 0) return;
|
|
1465
|
-
const turnId = this.extractTurnId(notification);
|
|
1466
|
-
const lastForwardedTurnId = this.lastForwardedTurnByThreadId.get(threadId);
|
|
1467
|
-
if (turnId && lastForwardedTurnId === turnId) return;
|
|
1468
|
-
const assistantReply = await this.readLatestAssistantMessage(threadId);
|
|
1469
|
-
if (!assistantReply) return;
|
|
1470
|
-
for (const chatId of chatIds) {
|
|
1471
|
-
await this.sendTelegramMessage(chatId, assistantReply);
|
|
1472
|
-
}
|
|
1473
|
-
if (turnId) {
|
|
1474
|
-
this.lastForwardedTurnByThreadId.set(threadId, turnId);
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
async readLatestAssistantMessage(threadId) {
|
|
1478
|
-
const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
1479
|
-
const thread = asRecord2(response?.thread);
|
|
1480
|
-
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1481
|
-
for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
|
|
1482
|
-
const turn = asRecord2(turns[turnIndex]);
|
|
1483
|
-
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
1484
|
-
for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex -= 1) {
|
|
1485
|
-
const item = asRecord2(items[itemIndex]);
|
|
1486
|
-
if (item?.type === "agentMessage") {
|
|
1487
|
-
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
1488
|
-
if (text) return text;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
return "";
|
|
1493
|
-
}
|
|
1494
|
-
async readThreadHistorySummary(threadId) {
|
|
1495
|
-
const response = asRecord2(await this.appServer.rpc("thread/read", { threadId, includeTurns: true }));
|
|
1496
|
-
const thread = asRecord2(response?.thread);
|
|
1497
|
-
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1498
|
-
const historyRows = [];
|
|
1499
|
-
for (const turn of turns) {
|
|
1500
|
-
const turnRecord = asRecord2(turn);
|
|
1501
|
-
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1502
|
-
for (const item of items) {
|
|
1503
|
-
const itemRecord = asRecord2(item);
|
|
1504
|
-
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1505
|
-
if (type === "userMessage") {
|
|
1506
|
-
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1507
|
-
for (const block of content) {
|
|
1508
|
-
const blockRecord = asRecord2(block);
|
|
1509
|
-
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim()) {
|
|
1510
|
-
historyRows.push(`User: ${blockRecord.text.trim()}`);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim()) {
|
|
1515
|
-
historyRows.push(`Assistant: ${itemRecord.text.trim()}`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
if (historyRows.length === 0) {
|
|
1520
|
-
return "Thread has no message history yet.";
|
|
1521
|
-
}
|
|
1522
|
-
const tail = historyRows.slice(-12).join("\n\n");
|
|
1523
|
-
const maxLen = 3800;
|
|
1524
|
-
const summary = tail.length > maxLen ? tail.slice(tail.length - maxLen) : tail;
|
|
1525
|
-
return `Recent history:
|
|
1526
|
-
|
|
1527
|
-
${summary}`;
|
|
1528
|
-
}
|
|
1529
|
-
};
|
|
1530
|
-
|
|
1531
|
-
// src/utils/commandInvocation.ts
|
|
1532
|
-
import { spawnSync } from "child_process";
|
|
1533
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1534
|
-
import { homedir as homedir2 } from "os";
|
|
1535
|
-
import { basename as basename2, extname, join as join2 } from "path";
|
|
1536
|
-
var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
|
|
1537
|
-
function quoteCmdExeArg(value) {
|
|
1538
|
-
const normalized = value.replace(/"/g, '""');
|
|
1539
|
-
if (!/[\s"]/u.test(normalized)) {
|
|
1540
|
-
return normalized;
|
|
1541
|
-
}
|
|
1542
|
-
return `"${normalized}"`;
|
|
1543
|
-
}
|
|
1544
|
-
function needsCmdExeWrapper(command) {
|
|
1545
|
-
if (process.platform !== "win32") {
|
|
1546
|
-
return false;
|
|
1547
|
-
}
|
|
1548
|
-
const lowerCommand = command.toLowerCase();
|
|
1549
|
-
const baseName = basename2(lowerCommand);
|
|
1550
|
-
if (/\.(cmd|bat)$/i.test(baseName)) {
|
|
1551
|
-
return true;
|
|
1552
|
-
}
|
|
1553
|
-
if (extname(baseName)) {
|
|
1554
|
-
return false;
|
|
1555
|
-
}
|
|
1556
|
-
return WINDOWS_CMD_NAMES.has(baseName);
|
|
1557
|
-
}
|
|
1558
|
-
function getSpawnInvocation(command, args = []) {
|
|
1559
|
-
if (needsCmdExeWrapper(command)) {
|
|
1560
|
-
return {
|
|
1561
|
-
command: "cmd.exe",
|
|
1562
|
-
args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
return { command, args };
|
|
1566
|
-
}
|
|
1567
|
-
function spawnSyncCommand(command, args = [], options = {}) {
|
|
1568
|
-
const invocation = getSpawnInvocation(command, args);
|
|
1569
|
-
return spawnSync(invocation.command, invocation.args, options);
|
|
1570
|
-
}
|
|
1571
|
-
function canRunCommand(command, args = []) {
|
|
1572
|
-
const result = spawnSyncCommand(command, args, { stdio: "ignore" });
|
|
1573
|
-
return result.status === 0;
|
|
1574
|
-
}
|
|
1575
|
-
function getUserNpmPrefix() {
|
|
1576
|
-
return join2(homedir2(), ".npm-global");
|
|
1577
|
-
}
|
|
1578
|
-
function resolveCodexCommand() {
|
|
1579
|
-
if (canRunCommand("codex", ["--version"])) {
|
|
1580
|
-
return "codex";
|
|
1581
|
-
}
|
|
1582
|
-
if (process.platform === "win32") {
|
|
1583
|
-
const windowsCandidates = [
|
|
1584
|
-
process.env.APPDATA ? join2(process.env.APPDATA, "npm", "codex.cmd") : "",
|
|
1585
|
-
join2(homedir2(), ".local", "bin", "codex.cmd"),
|
|
1586
|
-
join2(getUserNpmPrefix(), "bin", "codex.cmd")
|
|
1587
|
-
].filter(Boolean);
|
|
1588
|
-
for (const candidate2 of windowsCandidates) {
|
|
1589
|
-
if (existsSync2(candidate2) && canRunCommand(candidate2, ["--version"])) {
|
|
1590
|
-
return candidate2;
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
const userCandidate = join2(getUserNpmPrefix(), "bin", "codex");
|
|
1595
|
-
if (existsSync2(userCandidate) && canRunCommand(userCandidate, ["--version"])) {
|
|
1596
|
-
return userCandidate;
|
|
1597
|
-
}
|
|
1598
|
-
const prefix = process.env.PREFIX?.trim();
|
|
1599
|
-
if (!prefix) {
|
|
1600
|
-
return null;
|
|
1601
|
-
}
|
|
1602
|
-
const candidate = join2(prefix, "bin", "codex");
|
|
1603
|
-
if (existsSync2(candidate) && canRunCommand(candidate, ["--version"])) {
|
|
1604
|
-
return candidate;
|
|
1605
|
-
}
|
|
1606
|
-
return null;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// src/server/codexAppServerBridge.ts
|
|
1610
|
-
function asRecord3(value) {
|
|
1611
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1612
|
-
}
|
|
1613
|
-
function getErrorMessage3(payload, fallback) {
|
|
1614
|
-
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
1615
|
-
return payload.message;
|
|
1616
|
-
}
|
|
1617
|
-
const record = asRecord3(payload);
|
|
1618
|
-
if (!record) return fallback;
|
|
1619
|
-
const error = record.error;
|
|
1620
|
-
if (typeof error === "string" && error.length > 0) return error;
|
|
1621
|
-
const nestedError = asRecord3(error);
|
|
1622
|
-
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
1623
|
-
return nestedError.message;
|
|
1624
|
-
}
|
|
1625
|
-
return fallback;
|
|
1626
|
-
}
|
|
1627
1138
|
function setJson2(res, statusCode, payload) {
|
|
1628
1139
|
res.statusCode = statusCode;
|
|
1629
1140
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1630
1141
|
res.end(JSON.stringify(payload));
|
|
1631
1142
|
}
|
|
1632
1143
|
function extractThreadMessageText(threadReadPayload) {
|
|
1633
|
-
const payload =
|
|
1634
|
-
const thread =
|
|
1144
|
+
const payload = asRecord2(threadReadPayload);
|
|
1145
|
+
const thread = asRecord2(payload?.thread);
|
|
1635
1146
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1636
1147
|
const parts = [];
|
|
1637
1148
|
for (const turn of turns) {
|
|
1638
|
-
const turnRecord =
|
|
1149
|
+
const turnRecord = asRecord2(turn);
|
|
1639
1150
|
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1640
1151
|
for (const item of items) {
|
|
1641
|
-
const itemRecord =
|
|
1152
|
+
const itemRecord = asRecord2(item);
|
|
1642
1153
|
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1643
1154
|
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
1644
1155
|
parts.push(itemRecord.text.trim());
|
|
@@ -1647,7 +1158,7 @@ function extractThreadMessageText(threadReadPayload) {
|
|
|
1647
1158
|
if (type === "userMessage") {
|
|
1648
1159
|
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1649
1160
|
for (const block of content) {
|
|
1650
|
-
const blockRecord =
|
|
1161
|
+
const blockRecord = asRecord2(block);
|
|
1651
1162
|
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
1652
1163
|
parts.push(blockRecord.text.trim());
|
|
1653
1164
|
}
|
|
@@ -1710,7 +1221,7 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
1710
1221
|
}
|
|
1711
1222
|
function getCodexHomeDir2() {
|
|
1712
1223
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1713
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
1224
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
1714
1225
|
}
|
|
1715
1226
|
async function runCommand2(command, args, options = {}) {
|
|
1716
1227
|
await new Promise((resolve3, reject) => {
|
|
@@ -1740,15 +1251,15 @@ async function runCommand2(command, args, options = {}) {
|
|
|
1740
1251
|
});
|
|
1741
1252
|
}
|
|
1742
1253
|
function isMissingHeadError(error) {
|
|
1743
|
-
const message =
|
|
1254
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
1744
1255
|
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
1745
1256
|
}
|
|
1746
1257
|
function isNotGitRepositoryError(error) {
|
|
1747
|
-
const message =
|
|
1258
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
1748
1259
|
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
1749
1260
|
}
|
|
1750
1261
|
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1751
|
-
const agentsPath =
|
|
1262
|
+
const agentsPath = join2(repoRoot, "AGENTS.md");
|
|
1752
1263
|
try {
|
|
1753
1264
|
await stat2(agentsPath);
|
|
1754
1265
|
} catch {
|
|
@@ -1788,33 +1299,6 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
1788
1299
|
});
|
|
1789
1300
|
});
|
|
1790
1301
|
}
|
|
1791
|
-
async function runCommandWithOutput2(command, args, options = {}) {
|
|
1792
|
-
return await new Promise((resolve3, reject) => {
|
|
1793
|
-
const proc = spawn2(command, args, {
|
|
1794
|
-
cwd: options.cwd,
|
|
1795
|
-
env: process.env,
|
|
1796
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1797
|
-
});
|
|
1798
|
-
let stdout = "";
|
|
1799
|
-
let stderr = "";
|
|
1800
|
-
proc.stdout.on("data", (chunk) => {
|
|
1801
|
-
stdout += chunk.toString();
|
|
1802
|
-
});
|
|
1803
|
-
proc.stderr.on("data", (chunk) => {
|
|
1804
|
-
stderr += chunk.toString();
|
|
1805
|
-
});
|
|
1806
|
-
proc.on("error", reject);
|
|
1807
|
-
proc.on("close", (code) => {
|
|
1808
|
-
if (code === 0) {
|
|
1809
|
-
resolve3(stdout.trim());
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1813
|
-
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
1814
|
-
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
1815
|
-
});
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
1302
|
function normalizeStringArray(value) {
|
|
1819
1303
|
if (!Array.isArray(value)) return [];
|
|
1820
1304
|
const normalized = [];
|
|
@@ -1835,32 +1319,8 @@ function normalizeStringRecord(value) {
|
|
|
1835
1319
|
}
|
|
1836
1320
|
return next;
|
|
1837
1321
|
}
|
|
1838
|
-
function normalizeCommitMessage(value) {
|
|
1839
|
-
if (typeof value !== "string") return "";
|
|
1840
|
-
const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
|
|
1841
|
-
return normalized.slice(0, 2e3);
|
|
1842
|
-
}
|
|
1843
|
-
async function hasGitWorkingTreeChanges(cwd) {
|
|
1844
|
-
const status = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
|
|
1845
|
-
return status.trim().length > 0;
|
|
1846
|
-
}
|
|
1847
|
-
async function findCommitByExactMessage(cwd, message) {
|
|
1848
|
-
const normalizedTarget = normalizeCommitMessage(message);
|
|
1849
|
-
if (!normalizedTarget) return "";
|
|
1850
|
-
const raw = await runCommandWithOutput2("git", ["log", "--format=%H%x1f%B%x1e"], { cwd });
|
|
1851
|
-
const entries = raw.split("");
|
|
1852
|
-
for (const entry of entries) {
|
|
1853
|
-
if (!entry.trim()) continue;
|
|
1854
|
-
const [shaRaw, bodyRaw] = entry.split("");
|
|
1855
|
-
const sha = (shaRaw ?? "").trim();
|
|
1856
|
-
const body = normalizeCommitMessage(bodyRaw ?? "");
|
|
1857
|
-
if (!sha) continue;
|
|
1858
|
-
if (body === normalizedTarget) return sha;
|
|
1859
|
-
}
|
|
1860
|
-
return "";
|
|
1861
|
-
}
|
|
1862
1322
|
function getCodexAuthPath() {
|
|
1863
|
-
return
|
|
1323
|
+
return join2(getCodexHomeDir2(), "auth.json");
|
|
1864
1324
|
}
|
|
1865
1325
|
async function readCodexAuth() {
|
|
1866
1326
|
try {
|
|
@@ -1874,21 +1334,13 @@ async function readCodexAuth() {
|
|
|
1874
1334
|
}
|
|
1875
1335
|
}
|
|
1876
1336
|
function getCodexGlobalStatePath() {
|
|
1877
|
-
return
|
|
1878
|
-
}
|
|
1879
|
-
function getCodexSessionIndexPath() {
|
|
1880
|
-
return join3(getCodexHomeDir2(), "session_index.jsonl");
|
|
1337
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
1881
1338
|
}
|
|
1882
1339
|
var MAX_THREAD_TITLES = 500;
|
|
1883
|
-
var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
|
|
1884
|
-
var sessionIndexThreadTitleCacheState = {
|
|
1885
|
-
fileSignature: null,
|
|
1886
|
-
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1887
|
-
};
|
|
1888
1340
|
function normalizeThreadTitleCache(value) {
|
|
1889
|
-
const record =
|
|
1890
|
-
if (!record) return
|
|
1891
|
-
const rawTitles =
|
|
1341
|
+
const record = asRecord2(value);
|
|
1342
|
+
if (!record) return { titles: {}, order: [] };
|
|
1343
|
+
const rawTitles = asRecord2(record.titles);
|
|
1892
1344
|
const titles = {};
|
|
1893
1345
|
if (rawTitles) {
|
|
1894
1346
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -1911,55 +1363,14 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
1911
1363
|
const { [id]: _, ...titles } = cache.titles;
|
|
1912
1364
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
1913
1365
|
}
|
|
1914
|
-
function normalizeSessionIndexThreadTitle(value) {
|
|
1915
|
-
const record = asRecord3(value);
|
|
1916
|
-
if (!record) return null;
|
|
1917
|
-
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
1918
|
-
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
1919
|
-
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
1920
|
-
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
1921
|
-
if (!id || !title) return null;
|
|
1922
|
-
return {
|
|
1923
|
-
id,
|
|
1924
|
-
title,
|
|
1925
|
-
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
function trimThreadTitleCache(cache) {
|
|
1929
|
-
const titles = { ...cache.titles };
|
|
1930
|
-
const order = cache.order.filter((id) => {
|
|
1931
|
-
if (!titles[id]) return false;
|
|
1932
|
-
return true;
|
|
1933
|
-
}).slice(0, MAX_THREAD_TITLES);
|
|
1934
|
-
for (const id of Object.keys(titles)) {
|
|
1935
|
-
if (!order.includes(id)) {
|
|
1936
|
-
delete titles[id];
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
return { titles, order };
|
|
1940
|
-
}
|
|
1941
|
-
function mergeThreadTitleCaches(base, overlay) {
|
|
1942
|
-
const titles = { ...base.titles, ...overlay.titles };
|
|
1943
|
-
const order = [];
|
|
1944
|
-
for (const id of [...overlay.order, ...base.order]) {
|
|
1945
|
-
if (!titles[id] || order.includes(id)) continue;
|
|
1946
|
-
order.push(id);
|
|
1947
|
-
}
|
|
1948
|
-
for (const id of Object.keys(titles)) {
|
|
1949
|
-
if (!order.includes(id)) {
|
|
1950
|
-
order.push(id);
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
return trimThreadTitleCache({ titles, order });
|
|
1954
|
-
}
|
|
1955
1366
|
async function readThreadTitleCache() {
|
|
1956
1367
|
const statePath = getCodexGlobalStatePath();
|
|
1957
1368
|
try {
|
|
1958
1369
|
const raw = await readFile2(statePath, "utf8");
|
|
1959
|
-
const payload =
|
|
1370
|
+
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1960
1371
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1961
1372
|
} catch {
|
|
1962
|
-
return
|
|
1373
|
+
return { titles: {}, order: [] };
|
|
1963
1374
|
}
|
|
1964
1375
|
}
|
|
1965
1376
|
async function writeThreadTitleCache(cache) {
|
|
@@ -1967,83 +1378,20 @@ async function writeThreadTitleCache(cache) {
|
|
|
1967
1378
|
let payload = {};
|
|
1968
1379
|
try {
|
|
1969
1380
|
const raw = await readFile2(statePath, "utf8");
|
|
1970
|
-
payload =
|
|
1381
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1971
1382
|
} catch {
|
|
1972
1383
|
payload = {};
|
|
1973
1384
|
}
|
|
1974
1385
|
payload["thread-titles"] = cache;
|
|
1975
1386
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1976
1387
|
}
|
|
1977
|
-
function getSessionIndexFileSignature(stats) {
|
|
1978
|
-
return `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
1979
|
-
}
|
|
1980
|
-
async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
|
|
1981
|
-
const latestById = /* @__PURE__ */ new Map();
|
|
1982
|
-
const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
|
|
1983
|
-
const lines = createInterface({
|
|
1984
|
-
input,
|
|
1985
|
-
crlfDelay: Infinity
|
|
1986
|
-
});
|
|
1987
|
-
try {
|
|
1988
|
-
for await (const line of lines) {
|
|
1989
|
-
const trimmed = line.trim();
|
|
1990
|
-
if (!trimmed) continue;
|
|
1991
|
-
try {
|
|
1992
|
-
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
1993
|
-
if (!entry) continue;
|
|
1994
|
-
const previous = latestById.get(entry.id);
|
|
1995
|
-
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
1996
|
-
latestById.set(entry.id, entry);
|
|
1997
|
-
}
|
|
1998
|
-
} catch {
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
} finally {
|
|
2002
|
-
lines.close();
|
|
2003
|
-
input.close();
|
|
2004
|
-
}
|
|
2005
|
-
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
2006
|
-
const titles = {};
|
|
2007
|
-
const order = [];
|
|
2008
|
-
for (const entry of entries) {
|
|
2009
|
-
titles[entry.id] = entry.title;
|
|
2010
|
-
order.push(entry.id);
|
|
2011
|
-
}
|
|
2012
|
-
return trimThreadTitleCache({ titles, order });
|
|
2013
|
-
}
|
|
2014
|
-
async function readThreadTitlesFromSessionIndex() {
|
|
2015
|
-
const sessionIndexPath = getCodexSessionIndexPath();
|
|
2016
|
-
try {
|
|
2017
|
-
const stats = await stat2(sessionIndexPath);
|
|
2018
|
-
const fileSignature = getSessionIndexFileSignature(stats);
|
|
2019
|
-
if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
|
|
2020
|
-
return sessionIndexThreadTitleCacheState.cache;
|
|
2021
|
-
}
|
|
2022
|
-
const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
|
|
2023
|
-
sessionIndexThreadTitleCacheState = { fileSignature, cache };
|
|
2024
|
-
return cache;
|
|
2025
|
-
} catch {
|
|
2026
|
-
sessionIndexThreadTitleCacheState = {
|
|
2027
|
-
fileSignature: "missing",
|
|
2028
|
-
cache: EMPTY_THREAD_TITLE_CACHE
|
|
2029
|
-
};
|
|
2030
|
-
return sessionIndexThreadTitleCacheState.cache;
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
async function readMergedThreadTitleCache() {
|
|
2034
|
-
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
2035
|
-
readThreadTitlesFromSessionIndex(),
|
|
2036
|
-
readThreadTitleCache()
|
|
2037
|
-
]);
|
|
2038
|
-
return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
|
|
2039
|
-
}
|
|
2040
1388
|
async function readWorkspaceRootsState() {
|
|
2041
1389
|
const statePath = getCodexGlobalStatePath();
|
|
2042
1390
|
let payload = {};
|
|
2043
1391
|
try {
|
|
2044
1392
|
const raw = await readFile2(statePath, "utf8");
|
|
2045
1393
|
const parsed = JSON.parse(raw);
|
|
2046
|
-
payload =
|
|
1394
|
+
payload = asRecord2(parsed) ?? {};
|
|
2047
1395
|
} catch {
|
|
2048
1396
|
payload = {};
|
|
2049
1397
|
}
|
|
@@ -2058,7 +1406,7 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
2058
1406
|
let payload = {};
|
|
2059
1407
|
try {
|
|
2060
1408
|
const raw = await readFile2(statePath, "utf8");
|
|
2061
|
-
payload =
|
|
1409
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
2062
1410
|
} catch {
|
|
2063
1411
|
payload = {};
|
|
2064
1412
|
}
|
|
@@ -2067,36 +1415,6 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
2067
1415
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
2068
1416
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
2069
1417
|
}
|
|
2070
|
-
function normalizeTelegramBridgeConfig(value) {
|
|
2071
|
-
const record = asRecord3(value);
|
|
2072
|
-
if (!record) return { botToken: "" };
|
|
2073
|
-
const botToken = typeof record.botToken === "string" ? record.botToken.trim() : "";
|
|
2074
|
-
return { botToken };
|
|
2075
|
-
}
|
|
2076
|
-
async function readTelegramBridgeConfig() {
|
|
2077
|
-
const statePath = getCodexGlobalStatePath();
|
|
2078
|
-
try {
|
|
2079
|
-
const raw = await readFile2(statePath, "utf8");
|
|
2080
|
-
const payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
2081
|
-
return normalizeTelegramBridgeConfig(payload["telegram-bridge"]);
|
|
2082
|
-
} catch {
|
|
2083
|
-
return { botToken: "" };
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
async function writeTelegramBridgeConfig(nextState) {
|
|
2087
|
-
const statePath = getCodexGlobalStatePath();
|
|
2088
|
-
let payload = {};
|
|
2089
|
-
try {
|
|
2090
|
-
const raw = await readFile2(statePath, "utf8");
|
|
2091
|
-
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
2092
|
-
} catch {
|
|
2093
|
-
payload = {};
|
|
2094
|
-
}
|
|
2095
|
-
payload["telegram-bridge"] = {
|
|
2096
|
-
botToken: nextState.botToken.trim()
|
|
2097
|
-
};
|
|
2098
|
-
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
2099
|
-
}
|
|
2100
1418
|
async function readJsonBody(req) {
|
|
2101
1419
|
const raw = await readRawBody(req);
|
|
2102
1420
|
if (raw.length === 0) return null;
|
|
@@ -2166,93 +1484,46 @@ function handleFileUpload(req, res) {
|
|
|
2166
1484
|
setJson2(res, 400, { error: "No file in request" });
|
|
2167
1485
|
return;
|
|
2168
1486
|
}
|
|
2169
|
-
const uploadDir =
|
|
1487
|
+
const uploadDir = join2(tmpdir2(), "codex-web-uploads");
|
|
2170
1488
|
await mkdir2(uploadDir, { recursive: true });
|
|
2171
|
-
const destDir = await mkdtemp2(
|
|
2172
|
-
const destPath =
|
|
1489
|
+
const destDir = await mkdtemp2(join2(uploadDir, "f-"));
|
|
1490
|
+
const destPath = join2(destDir, fileName);
|
|
2173
1491
|
await writeFile2(destPath, fileData);
|
|
2174
1492
|
setJson2(res, 200, { path: destPath });
|
|
2175
1493
|
} catch (err) {
|
|
2176
|
-
setJson2(res, 500, { error:
|
|
1494
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
|
|
2177
1495
|
}
|
|
2178
1496
|
});
|
|
2179
1497
|
req.on("error", (err) => {
|
|
2180
|
-
setJson2(res, 500, { error:
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
function httpPost(url, headers, body) {
|
|
2184
|
-
const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
|
|
2185
|
-
return new Promise((resolve3, reject) => {
|
|
2186
|
-
const req = doRequest(url, { method: "POST", headers }, (res) => {
|
|
2187
|
-
const chunks = [];
|
|
2188
|
-
res.on("data", (c) => chunks.push(c));
|
|
2189
|
-
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
2190
|
-
res.on("error", reject);
|
|
2191
|
-
});
|
|
2192
|
-
req.on("error", reject);
|
|
2193
|
-
req.write(body);
|
|
2194
|
-
req.end();
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
|
-
var curlImpersonateAvailable = null;
|
|
2198
|
-
function curlImpersonatePost(url, headers, body) {
|
|
2199
|
-
return new Promise((resolve3, reject) => {
|
|
2200
|
-
const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
|
|
2201
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
2202
|
-
if (k.toLowerCase() === "content-length") continue;
|
|
2203
|
-
args.push("-H", `${k}: ${String(v)}`);
|
|
2204
|
-
}
|
|
2205
|
-
args.push("--data-binary", "@-");
|
|
2206
|
-
const proc = spawn2("curl-impersonate-chrome", args, {
|
|
2207
|
-
env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
|
|
2208
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2209
|
-
});
|
|
2210
|
-
const chunks = [];
|
|
2211
|
-
proc.stdout.on("data", (c) => chunks.push(c));
|
|
2212
|
-
proc.on("error", (e) => {
|
|
2213
|
-
curlImpersonateAvailable = false;
|
|
2214
|
-
reject(e);
|
|
2215
|
-
});
|
|
2216
|
-
proc.on("close", (code) => {
|
|
2217
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
2218
|
-
const lastNewline = raw.lastIndexOf("\n");
|
|
2219
|
-
const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
|
|
2220
|
-
const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
|
|
2221
|
-
const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
|
|
2222
|
-
curlImpersonateAvailable = true;
|
|
2223
|
-
resolve3({ status, body: responseBody });
|
|
2224
|
-
});
|
|
2225
|
-
proc.stdin.write(body);
|
|
2226
|
-
proc.stdin.end();
|
|
1498
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
2227
1499
|
});
|
|
2228
1500
|
}
|
|
2229
1501
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
2230
|
-
const
|
|
1502
|
+
const headers = {
|
|
2231
1503
|
"Content-Type": contentType,
|
|
2232
1504
|
"Content-Length": body.length,
|
|
2233
1505
|
Authorization: `Bearer ${authToken}`,
|
|
2234
1506
|
originator: "Codex Desktop",
|
|
2235
1507
|
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
|
|
2236
1508
|
};
|
|
2237
|
-
if (accountId)
|
|
2238
|
-
|
|
2239
|
-
let result;
|
|
2240
|
-
try {
|
|
2241
|
-
result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
2242
|
-
} catch {
|
|
2243
|
-
result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1509
|
+
if (accountId) {
|
|
1510
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
2244
1511
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
1512
|
+
return new Promise((resolve3, reject) => {
|
|
1513
|
+
const req = httpsRequest(
|
|
1514
|
+
"https://chatgpt.com/backend-api/transcribe",
|
|
1515
|
+
{ method: "POST", headers },
|
|
1516
|
+
(res) => {
|
|
1517
|
+
const chunks = [];
|
|
1518
|
+
res.on("data", (c) => chunks.push(c));
|
|
1519
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
1520
|
+
res.on("error", reject);
|
|
1521
|
+
}
|
|
1522
|
+
);
|
|
1523
|
+
req.on("error", reject);
|
|
1524
|
+
req.write(body);
|
|
1525
|
+
req.end();
|
|
1526
|
+
});
|
|
2256
1527
|
}
|
|
2257
1528
|
var AppServerProcess = class {
|
|
2258
1529
|
constructor() {
|
|
@@ -2276,8 +1547,7 @@ var AppServerProcess = class {
|
|
|
2276
1547
|
start() {
|
|
2277
1548
|
if (this.process) return;
|
|
2278
1549
|
this.stopping = false;
|
|
2279
|
-
const
|
|
2280
|
-
const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1550
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
2281
1551
|
this.process = proc;
|
|
2282
1552
|
proc.stdout.setEncoding("utf8");
|
|
2283
1553
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -2371,7 +1641,7 @@ var AppServerProcess = class {
|
|
|
2371
1641
|
}
|
|
2372
1642
|
this.pendingServerRequests.delete(requestId);
|
|
2373
1643
|
this.sendServerRequestReply(requestId, reply);
|
|
2374
|
-
const requestParams =
|
|
1644
|
+
const requestParams = asRecord2(pendingRequest.params);
|
|
2375
1645
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
2376
1646
|
this.emitNotification({
|
|
2377
1647
|
method: "server/request/resolved",
|
|
@@ -2440,7 +1710,7 @@ var AppServerProcess = class {
|
|
|
2440
1710
|
}
|
|
2441
1711
|
async respondToServerRequest(payload) {
|
|
2442
1712
|
await this.ensureInitialized();
|
|
2443
|
-
const body =
|
|
1713
|
+
const body = asRecord2(payload);
|
|
2444
1714
|
if (!body) {
|
|
2445
1715
|
throw new Error("Invalid response payload: expected object");
|
|
2446
1716
|
}
|
|
@@ -2448,7 +1718,7 @@ var AppServerProcess = class {
|
|
|
2448
1718
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
2449
1719
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
2450
1720
|
}
|
|
2451
|
-
const rawError =
|
|
1721
|
+
const rawError = asRecord2(body.error);
|
|
2452
1722
|
if (rawError) {
|
|
2453
1723
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
2454
1724
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -2503,8 +1773,7 @@ var MethodCatalog = class {
|
|
|
2503
1773
|
}
|
|
2504
1774
|
async runGenerateSchemaCommand(outDir) {
|
|
2505
1775
|
await new Promise((resolve3, reject) => {
|
|
2506
|
-
const
|
|
2507
|
-
const process2 = spawn2(invocation.command, invocation.args, {
|
|
1776
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
2508
1777
|
stdio: ["ignore", "ignore", "pipe"]
|
|
2509
1778
|
});
|
|
2510
1779
|
let stderr = "";
|
|
@@ -2523,13 +1792,13 @@ var MethodCatalog = class {
|
|
|
2523
1792
|
});
|
|
2524
1793
|
}
|
|
2525
1794
|
extractMethodsFromClientRequest(payload) {
|
|
2526
|
-
const root =
|
|
1795
|
+
const root = asRecord2(payload);
|
|
2527
1796
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
2528
1797
|
const methods = /* @__PURE__ */ new Set();
|
|
2529
1798
|
for (const entry of oneOf) {
|
|
2530
|
-
const row =
|
|
2531
|
-
const properties =
|
|
2532
|
-
const methodDef =
|
|
1799
|
+
const row = asRecord2(entry);
|
|
1800
|
+
const properties = asRecord2(row?.properties);
|
|
1801
|
+
const methodDef = asRecord2(properties?.method);
|
|
2533
1802
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
2534
1803
|
for (const item of methodEnum) {
|
|
2535
1804
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -2540,13 +1809,13 @@ var MethodCatalog = class {
|
|
|
2540
1809
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
2541
1810
|
}
|
|
2542
1811
|
extractMethodsFromServerNotification(payload) {
|
|
2543
|
-
const root =
|
|
1812
|
+
const root = asRecord2(payload);
|
|
2544
1813
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
2545
1814
|
const methods = /* @__PURE__ */ new Set();
|
|
2546
1815
|
for (const entry of oneOf) {
|
|
2547
|
-
const row =
|
|
2548
|
-
const properties =
|
|
2549
|
-
const methodDef =
|
|
1816
|
+
const row = asRecord2(entry);
|
|
1817
|
+
const properties = asRecord2(row?.properties);
|
|
1818
|
+
const methodDef = asRecord2(properties?.method);
|
|
2550
1819
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
2551
1820
|
for (const item of methodEnum) {
|
|
2552
1821
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -2560,9 +1829,9 @@ var MethodCatalog = class {
|
|
|
2560
1829
|
if (this.methodCache) {
|
|
2561
1830
|
return this.methodCache;
|
|
2562
1831
|
}
|
|
2563
|
-
const outDir = await mkdtemp2(
|
|
1832
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
2564
1833
|
await this.runGenerateSchemaCommand(outDir);
|
|
2565
|
-
const clientRequestPath =
|
|
1834
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
2566
1835
|
const raw = await readFile2(clientRequestPath, "utf8");
|
|
2567
1836
|
const parsed = JSON.parse(raw);
|
|
2568
1837
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
@@ -2573,9 +1842,9 @@ var MethodCatalog = class {
|
|
|
2573
1842
|
if (this.notificationCache) {
|
|
2574
1843
|
return this.notificationCache;
|
|
2575
1844
|
}
|
|
2576
|
-
const outDir = await mkdtemp2(
|
|
1845
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
2577
1846
|
await this.runGenerateSchemaCommand(outDir);
|
|
2578
|
-
const serverNotificationPath =
|
|
1847
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
2579
1848
|
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
2580
1849
|
const parsed = JSON.parse(raw);
|
|
2581
1850
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
@@ -2588,11 +1857,9 @@ function getSharedBridgeState() {
|
|
|
2588
1857
|
const globalScope = globalThis;
|
|
2589
1858
|
const existing = globalScope[SHARED_BRIDGE_KEY];
|
|
2590
1859
|
if (existing) return existing;
|
|
2591
|
-
const appServer = new AppServerProcess();
|
|
2592
1860
|
const created = {
|
|
2593
|
-
appServer,
|
|
2594
|
-
methodCatalog: new MethodCatalog()
|
|
2595
|
-
telegramBridge: new TelegramThreadBridge(appServer)
|
|
1861
|
+
appServer: new AppServerProcess(),
|
|
1862
|
+
methodCatalog: new MethodCatalog()
|
|
2596
1863
|
};
|
|
2597
1864
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
2598
1865
|
return created;
|
|
@@ -2601,7 +1868,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
2601
1868
|
const threads = [];
|
|
2602
1869
|
let cursor = null;
|
|
2603
1870
|
do {
|
|
2604
|
-
const response =
|
|
1871
|
+
const response = asRecord2(await appServer.rpc("thread/list", {
|
|
2605
1872
|
archived: false,
|
|
2606
1873
|
limit: 100,
|
|
2607
1874
|
sortKey: "updated_at",
|
|
@@ -2609,7 +1876,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
2609
1876
|
}));
|
|
2610
1877
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
2611
1878
|
for (const row of data) {
|
|
2612
|
-
const record =
|
|
1879
|
+
const record = asRecord2(row);
|
|
2613
1880
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
2614
1881
|
if (!id) continue;
|
|
2615
1882
|
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";
|
|
@@ -2658,7 +1925,7 @@ async function buildThreadSearchIndex(appServer) {
|
|
|
2658
1925
|
return { docsById };
|
|
2659
1926
|
}
|
|
2660
1927
|
function createCodexBridgeMiddleware() {
|
|
2661
|
-
const { appServer, methodCatalog
|
|
1928
|
+
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
2662
1929
|
let threadSearchIndex = null;
|
|
2663
1930
|
let threadSearchIndexPromise = null;
|
|
2664
1931
|
async function getThreadSearchIndex() {
|
|
@@ -2674,12 +1941,6 @@ function createCodexBridgeMiddleware() {
|
|
|
2674
1941
|
return threadSearchIndexPromise;
|
|
2675
1942
|
}
|
|
2676
1943
|
void initializeSkillsSyncOnStartup(appServer);
|
|
2677
|
-
void readTelegramBridgeConfig().then((config) => {
|
|
2678
|
-
if (!config.botToken) return;
|
|
2679
|
-
telegramBridge.configureToken(config.botToken);
|
|
2680
|
-
telegramBridge.start();
|
|
2681
|
-
}).catch(() => {
|
|
2682
|
-
});
|
|
2683
1944
|
const middleware = async (req, res, next) => {
|
|
2684
1945
|
try {
|
|
2685
1946
|
if (!req.url) {
|
|
@@ -2696,7 +1957,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2696
1957
|
}
|
|
2697
1958
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
2698
1959
|
const payload = await readJsonBody(req);
|
|
2699
|
-
const body =
|
|
1960
|
+
const body = asRecord2(payload);
|
|
2700
1961
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
2701
1962
|
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
2702
1963
|
return;
|
|
@@ -2745,11 +2006,11 @@ function createCodexBridgeMiddleware() {
|
|
|
2745
2006
|
return;
|
|
2746
2007
|
}
|
|
2747
2008
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
2748
|
-
setJson2(res, 200, { data: { path:
|
|
2009
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
2749
2010
|
return;
|
|
2750
2011
|
}
|
|
2751
2012
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
2752
|
-
const payload =
|
|
2013
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
2753
2014
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
2754
2015
|
if (!rawSourceCwd) {
|
|
2755
2016
|
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
@@ -2775,22 +2036,22 @@ function createCodexBridgeMiddleware() {
|
|
|
2775
2036
|
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
2776
2037
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2777
2038
|
}
|
|
2778
|
-
const repoName =
|
|
2779
|
-
const worktreesRoot =
|
|
2039
|
+
const repoName = basename(gitRoot) || "repo";
|
|
2040
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2780
2041
|
await mkdir2(worktreesRoot, { recursive: true });
|
|
2781
2042
|
let worktreeId = "";
|
|
2782
2043
|
let worktreeParent = "";
|
|
2783
2044
|
let worktreeCwd = "";
|
|
2784
2045
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2785
2046
|
const candidate = randomBytes(2).toString("hex");
|
|
2786
|
-
const parent =
|
|
2047
|
+
const parent = join2(worktreesRoot, candidate);
|
|
2787
2048
|
try {
|
|
2788
2049
|
await stat2(parent);
|
|
2789
2050
|
continue;
|
|
2790
2051
|
} catch {
|
|
2791
2052
|
worktreeId = candidate;
|
|
2792
2053
|
worktreeParent = parent;
|
|
2793
|
-
worktreeCwd =
|
|
2054
|
+
worktreeCwd = join2(parent, repoName);
|
|
2794
2055
|
break;
|
|
2795
2056
|
}
|
|
2796
2057
|
}
|
|
@@ -2814,106 +2075,13 @@ function createCodexBridgeMiddleware() {
|
|
|
2814
2075
|
}
|
|
2815
2076
|
});
|
|
2816
2077
|
} catch (error) {
|
|
2817
|
-
setJson2(res, 500, { error:
|
|
2818
|
-
}
|
|
2819
|
-
return;
|
|
2820
|
-
}
|
|
2821
|
-
if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
|
|
2822
|
-
const payload = asRecord3(await readJsonBody(req));
|
|
2823
|
-
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2824
|
-
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
2825
|
-
if (!rawCwd) {
|
|
2826
|
-
setJson2(res, 400, { error: "Missing cwd" });
|
|
2827
|
-
return;
|
|
2828
|
-
}
|
|
2829
|
-
if (!commitMessage) {
|
|
2830
|
-
setJson2(res, 400, { error: "Missing message" });
|
|
2831
|
-
return;
|
|
2832
|
-
}
|
|
2833
|
-
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
2834
|
-
try {
|
|
2835
|
-
const cwdInfo = await stat2(cwd);
|
|
2836
|
-
if (!cwdInfo.isDirectory()) {
|
|
2837
|
-
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
} catch {
|
|
2841
|
-
setJson2(res, 404, { error: "cwd does not exist" });
|
|
2842
|
-
return;
|
|
2843
|
-
}
|
|
2844
|
-
try {
|
|
2845
|
-
await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
2846
|
-
const beforeStatus = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
|
|
2847
|
-
if (!beforeStatus.trim()) {
|
|
2848
|
-
setJson2(res, 200, { data: { committed: false } });
|
|
2849
|
-
return;
|
|
2850
|
-
}
|
|
2851
|
-
await runCommand2("git", ["add", "-A"], { cwd });
|
|
2852
|
-
const stagedStatus = await runCommandWithOutput2("git", ["diff", "--cached", "--name-only"], { cwd });
|
|
2853
|
-
if (!stagedStatus.trim()) {
|
|
2854
|
-
setJson2(res, 200, { data: { committed: false } });
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
|
-
await runCommand2("git", ["commit", "-m", commitMessage], { cwd });
|
|
2858
|
-
setJson2(res, 200, { data: { committed: true } });
|
|
2859
|
-
} catch (error) {
|
|
2860
|
-
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit worktree changes") });
|
|
2861
|
-
}
|
|
2862
|
-
return;
|
|
2863
|
-
}
|
|
2864
|
-
if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
|
|
2865
|
-
const payload = asRecord3(await readJsonBody(req));
|
|
2866
|
-
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2867
|
-
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
2868
|
-
if (!rawCwd) {
|
|
2869
|
-
setJson2(res, 400, { error: "Missing cwd" });
|
|
2870
|
-
return;
|
|
2871
|
-
}
|
|
2872
|
-
if (!commitMessage) {
|
|
2873
|
-
setJson2(res, 400, { error: "Missing message" });
|
|
2874
|
-
return;
|
|
2875
|
-
}
|
|
2876
|
-
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
2877
|
-
try {
|
|
2878
|
-
const cwdInfo = await stat2(cwd);
|
|
2879
|
-
if (!cwdInfo.isDirectory()) {
|
|
2880
|
-
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
2881
|
-
return;
|
|
2882
|
-
}
|
|
2883
|
-
} catch {
|
|
2884
|
-
setJson2(res, 404, { error: "cwd does not exist" });
|
|
2885
|
-
return;
|
|
2886
|
-
}
|
|
2887
|
-
try {
|
|
2888
|
-
await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
2889
|
-
const commitSha = await findCommitByExactMessage(cwd, commitMessage);
|
|
2890
|
-
if (!commitSha) {
|
|
2891
|
-
setJson2(res, 404, { error: "No matching commit found for this user message" });
|
|
2892
|
-
return;
|
|
2893
|
-
}
|
|
2894
|
-
let resetTargetSha = "";
|
|
2895
|
-
try {
|
|
2896
|
-
resetTargetSha = await runCommandCapture("git", ["rev-parse", `${commitSha}^`], { cwd });
|
|
2897
|
-
} catch {
|
|
2898
|
-
setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
let stashed = false;
|
|
2902
|
-
if (await hasGitWorkingTreeChanges(cwd)) {
|
|
2903
|
-
const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
|
|
2904
|
-
await runCommand2("git", ["stash", "push", "-u", "-m", stashMessage], { cwd });
|
|
2905
|
-
stashed = true;
|
|
2906
|
-
}
|
|
2907
|
-
await runCommand2("git", ["reset", "--hard", resetTargetSha], { cwd });
|
|
2908
|
-
setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
|
|
2909
|
-
} catch (error) {
|
|
2910
|
-
setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback worktree to user message commit") });
|
|
2078
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
|
|
2911
2079
|
}
|
|
2912
2080
|
return;
|
|
2913
2081
|
}
|
|
2914
2082
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
2915
2083
|
const payload = await readJsonBody(req);
|
|
2916
|
-
const record =
|
|
2084
|
+
const record = asRecord2(payload);
|
|
2917
2085
|
if (!record) {
|
|
2918
2086
|
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
2919
2087
|
return;
|
|
@@ -2928,7 +2096,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2928
2096
|
return;
|
|
2929
2097
|
}
|
|
2930
2098
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
2931
|
-
const payload =
|
|
2099
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
2932
2100
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
2933
2101
|
const createIfMissing = payload?.createIfMissing === true;
|
|
2934
2102
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
@@ -2988,7 +2156,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2988
2156
|
let index = 1;
|
|
2989
2157
|
while (index < 1e5) {
|
|
2990
2158
|
const candidateName = `New Project (${String(index)})`;
|
|
2991
|
-
const candidatePath =
|
|
2159
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
2992
2160
|
try {
|
|
2993
2161
|
await stat2(candidatePath);
|
|
2994
2162
|
index += 1;
|
|
@@ -3002,7 +2170,7 @@ function createCodexBridgeMiddleware() {
|
|
|
3002
2170
|
return;
|
|
3003
2171
|
}
|
|
3004
2172
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
3005
|
-
const payload =
|
|
2173
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
3006
2174
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
3007
2175
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
3008
2176
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
@@ -3027,17 +2195,17 @@ function createCodexBridgeMiddleware() {
|
|
|
3027
2195
|
const scored = files.map((path) => ({ path, score: scoreFileCandidate(path, query) })).filter((row) => query.length === 0 || row.score < 10).sort((a, b) => a.score - b.score || a.path.localeCompare(b.path)).slice(0, limit).map((row) => ({ path: row.path }));
|
|
3028
2196
|
setJson2(res, 200, { data: scored });
|
|
3029
2197
|
} catch (error) {
|
|
3030
|
-
setJson2(res, 500, { error:
|
|
2198
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
|
|
3031
2199
|
}
|
|
3032
2200
|
return;
|
|
3033
2201
|
}
|
|
3034
2202
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
3035
|
-
const cache = await
|
|
2203
|
+
const cache = await readThreadTitleCache();
|
|
3036
2204
|
setJson2(res, 200, { data: cache });
|
|
3037
2205
|
return;
|
|
3038
2206
|
}
|
|
3039
2207
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
3040
|
-
const payload =
|
|
2208
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
3041
2209
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
3042
2210
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
3043
2211
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
@@ -3051,7 +2219,7 @@ function createCodexBridgeMiddleware() {
|
|
|
3051
2219
|
return;
|
|
3052
2220
|
}
|
|
3053
2221
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
3054
|
-
const payload =
|
|
2222
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
3055
2223
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
3056
2224
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
3057
2225
|
if (!id) {
|
|
@@ -3064,23 +2232,6 @@ function createCodexBridgeMiddleware() {
|
|
|
3064
2232
|
setJson2(res, 200, { ok: true });
|
|
3065
2233
|
return;
|
|
3066
2234
|
}
|
|
3067
|
-
if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
|
|
3068
|
-
const payload = asRecord3(await readJsonBody(req));
|
|
3069
|
-
const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
|
|
3070
|
-
if (!botToken) {
|
|
3071
|
-
setJson2(res, 400, { error: "Missing botToken" });
|
|
3072
|
-
return;
|
|
3073
|
-
}
|
|
3074
|
-
telegramBridge.configureToken(botToken);
|
|
3075
|
-
telegramBridge.start();
|
|
3076
|
-
await writeTelegramBridgeConfig({ botToken });
|
|
3077
|
-
setJson2(res, 200, { ok: true });
|
|
3078
|
-
return;
|
|
3079
|
-
}
|
|
3080
|
-
if (req.method === "GET" && url.pathname === "/codex-api/telegram/status") {
|
|
3081
|
-
setJson2(res, 200, { data: telegramBridge.getStatus() });
|
|
3082
|
-
return;
|
|
3083
|
-
}
|
|
3084
2235
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
3085
2236
|
res.statusCode = 200;
|
|
3086
2237
|
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
@@ -3113,13 +2264,12 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
3113
2264
|
}
|
|
3114
2265
|
next();
|
|
3115
2266
|
} catch (error) {
|
|
3116
|
-
const message =
|
|
2267
|
+
const message = getErrorMessage2(error, "Unknown bridge error");
|
|
3117
2268
|
setJson2(res, 502, { error: message });
|
|
3118
2269
|
}
|
|
3119
2270
|
};
|
|
3120
2271
|
middleware.dispose = () => {
|
|
3121
2272
|
threadSearchIndex = null;
|
|
3122
|
-
telegramBridge.stop();
|
|
3123
2273
|
appServer.dispose();
|
|
3124
2274
|
};
|
|
3125
2275
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -3252,7 +2402,7 @@ function createAuthSession(password) {
|
|
|
3252
2402
|
}
|
|
3253
2403
|
|
|
3254
2404
|
// src/server/localBrowseUi.ts
|
|
3255
|
-
import { dirname, extname
|
|
2405
|
+
import { dirname, extname, join as join3 } from "path";
|
|
3256
2406
|
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
3257
2407
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3258
2408
|
".txt",
|
|
@@ -3283,7 +2433,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
3283
2433
|
".ps1"
|
|
3284
2434
|
]);
|
|
3285
2435
|
function languageForPath(pathValue) {
|
|
3286
|
-
const extension =
|
|
2436
|
+
const extension = extname(pathValue).toLowerCase();
|
|
3287
2437
|
switch (extension) {
|
|
3288
2438
|
case ".js":
|
|
3289
2439
|
return "javascript";
|
|
@@ -3344,7 +2494,7 @@ function decodeBrowsePath(rawPath) {
|
|
|
3344
2494
|
}
|
|
3345
2495
|
}
|
|
3346
2496
|
function isTextEditablePath(pathValue) {
|
|
3347
|
-
return TEXT_EDITABLE_EXTENSIONS.has(
|
|
2497
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
3348
2498
|
}
|
|
3349
2499
|
function looksLikeTextBuffer(buffer) {
|
|
3350
2500
|
if (buffer.length === 0) return true;
|
|
@@ -3390,7 +2540,7 @@ function escapeForInlineScriptString(value) {
|
|
|
3390
2540
|
async function getDirectoryItems(localPath) {
|
|
3391
2541
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
3392
2542
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
3393
|
-
const entryPath =
|
|
2543
|
+
const entryPath = join3(localPath, entry.name);
|
|
3394
2544
|
const entryStat = await stat3(entryPath);
|
|
3395
2545
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
3396
2546
|
return {
|
|
@@ -3414,9 +2564,9 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3414
2564
|
const rows = items.map((item) => {
|
|
3415
2565
|
const suffix = item.isDirectory ? "/" : "";
|
|
3416
2566
|
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
3417
|
-
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a
|
|
2567
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
3418
2568
|
}).join("\n");
|
|
3419
|
-
const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
|
|
2569
|
+
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
3420
2570
|
return `<!doctype html>
|
|
3421
2571
|
<html lang="en">
|
|
3422
2572
|
<head>
|
|
@@ -3430,27 +2580,8 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3430
2580
|
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
3431
2581
|
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
3432
2582
|
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
3433
|
-
.
|
|
3434
|
-
.header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
|
|
3435
|
-
.header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
|
|
3436
|
-
.header-open-btn {
|
|
3437
|
-
height: 42px;
|
|
3438
|
-
padding: 0 14px;
|
|
3439
|
-
border: 1px solid #4f8de0;
|
|
3440
|
-
border-radius: 10px;
|
|
3441
|
-
background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
|
|
3442
|
-
color: #eef6ff;
|
|
3443
|
-
font-weight: 700;
|
|
3444
|
-
letter-spacing: 0.01em;
|
|
3445
|
-
cursor: pointer;
|
|
3446
|
-
box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
|
|
3447
|
-
}
|
|
3448
|
-
.header-open-btn:hover { filter: brightness(1.08); }
|
|
3449
|
-
.header-open-btn:disabled { opacity: 0.6; cursor: default; }
|
|
3450
|
-
.row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
|
|
3451
|
-
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
|
|
2583
|
+
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
|
|
3452
2584
|
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
3453
|
-
.status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
|
|
3454
2585
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
3455
2586
|
@media (max-width: 640px) {
|
|
3456
2587
|
body { margin: 12px; }
|
|
@@ -3462,46 +2593,8 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3462
2593
|
</head>
|
|
3463
2594
|
<body>
|
|
3464
2595
|
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
3465
|
-
|
|
3466
|
-
${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
|
|
3467
|
-
<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
|
|
3468
|
-
</div>
|
|
3469
|
-
<p id="status" class="status"></p>
|
|
2596
|
+
${parentLink}
|
|
3470
2597
|
<ul>${rows}</ul>
|
|
3471
|
-
<script>
|
|
3472
|
-
const status = document.getElementById('status');
|
|
3473
|
-
document.addEventListener('click', async (event) => {
|
|
3474
|
-
const target = event.target;
|
|
3475
|
-
if (!(target instanceof Element)) return;
|
|
3476
|
-
const button = target.closest('.open-folder-btn');
|
|
3477
|
-
if (!(button instanceof HTMLButtonElement)) return;
|
|
3478
|
-
|
|
3479
|
-
const path = button.getAttribute('data-path') || '';
|
|
3480
|
-
if (!path) return;
|
|
3481
|
-
button.disabled = true;
|
|
3482
|
-
status.textContent = 'Opening folder in Codex...';
|
|
3483
|
-
try {
|
|
3484
|
-
const response = await fetch('/codex-api/project-root', {
|
|
3485
|
-
method: 'POST',
|
|
3486
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3487
|
-
body: JSON.stringify({
|
|
3488
|
-
path,
|
|
3489
|
-
createIfMissing: false,
|
|
3490
|
-
label: '',
|
|
3491
|
-
}),
|
|
3492
|
-
});
|
|
3493
|
-
if (!response.ok) {
|
|
3494
|
-
status.textContent = 'Failed to open folder.';
|
|
3495
|
-
button.disabled = false;
|
|
3496
|
-
return;
|
|
3497
|
-
}
|
|
3498
|
-
window.location.assign('/#/');
|
|
3499
|
-
} catch {
|
|
3500
|
-
status.textContent = 'Failed to open folder.';
|
|
3501
|
-
button.disabled = false;
|
|
3502
|
-
}
|
|
3503
|
-
});
|
|
3504
|
-
</script>
|
|
3505
2598
|
</body>
|
|
3506
2599
|
</html>`;
|
|
3507
2600
|
}
|
|
@@ -3577,8 +2670,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
3577
2670
|
// src/server/httpServer.ts
|
|
3578
2671
|
import { WebSocketServer } from "ws";
|
|
3579
2672
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
3580
|
-
var distDir =
|
|
3581
|
-
var spaEntryFile =
|
|
2673
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2674
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
3582
2675
|
var IMAGE_CONTENT_TYPES = {
|
|
3583
2676
|
".avif": "image/avif",
|
|
3584
2677
|
".bmp": "image/bmp",
|
|
@@ -3635,7 +2728,7 @@ function createServer(options = {}) {
|
|
|
3635
2728
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
3636
2729
|
return;
|
|
3637
2730
|
}
|
|
3638
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
2731
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
3639
2732
|
if (!contentType) {
|
|
3640
2733
|
res.status(415).json({ error: "Unsupported image type." });
|
|
3641
2734
|
return;
|
|
@@ -3722,7 +2815,7 @@ function createServer(options = {}) {
|
|
|
3722
2815
|
res.status(404).json({ error: "File not found." });
|
|
3723
2816
|
}
|
|
3724
2817
|
});
|
|
3725
|
-
const hasFrontendAssets =
|
|
2818
|
+
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
3726
2819
|
if (hasFrontendAssets) {
|
|
3727
2820
|
app.use(express.static(distDir));
|
|
3728
2821
|
}
|
|
@@ -3793,25 +2886,9 @@ function generatePassword() {
|
|
|
3793
2886
|
// src/cli/index.ts
|
|
3794
2887
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
3795
2888
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3796
|
-
var hasPromptedCloudflaredInstall = false;
|
|
3797
|
-
function getCodexHomePath() {
|
|
3798
|
-
return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
|
|
3799
|
-
}
|
|
3800
|
-
function getCloudflaredPromptMarkerPath() {
|
|
3801
|
-
return join6(getCodexHomePath(), ".cloudflared-install-prompted");
|
|
3802
|
-
}
|
|
3803
|
-
function hasPromptedCloudflaredInstallPersisted() {
|
|
3804
|
-
return existsSync5(getCloudflaredPromptMarkerPath());
|
|
3805
|
-
}
|
|
3806
|
-
async function persistCloudflaredInstallPrompted() {
|
|
3807
|
-
const codexHome = getCodexHomePath();
|
|
3808
|
-
mkdirSync(codexHome, { recursive: true });
|
|
3809
|
-
await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
3810
|
-
`, "utf8");
|
|
3811
|
-
}
|
|
3812
2889
|
async function readCliVersion() {
|
|
3813
2890
|
try {
|
|
3814
|
-
const packageJsonPath =
|
|
2891
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
3815
2892
|
const raw = await readFile4(packageJsonPath, "utf8");
|
|
3816
2893
|
const parsed = JSON.parse(raw);
|
|
3817
2894
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -3823,25 +2900,46 @@ function isTermuxRuntime() {
|
|
|
3823
2900
|
return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
|
|
3824
2901
|
}
|
|
3825
2902
|
function canRun(command, args = []) {
|
|
3826
|
-
const result =
|
|
3827
|
-
return result;
|
|
2903
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
2904
|
+
return result.status === 0;
|
|
3828
2905
|
}
|
|
3829
2906
|
function runOrFail(command, args, label) {
|
|
3830
|
-
const result =
|
|
2907
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
3831
2908
|
if (result.status !== 0) {
|
|
3832
2909
|
throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
|
|
3833
2910
|
}
|
|
3834
2911
|
}
|
|
3835
2912
|
function runWithStatus(command, args) {
|
|
3836
|
-
const result =
|
|
2913
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
3837
2914
|
return result.status ?? -1;
|
|
3838
2915
|
}
|
|
2916
|
+
function getUserNpmPrefix() {
|
|
2917
|
+
return join5(homedir3(), ".npm-global");
|
|
2918
|
+
}
|
|
2919
|
+
function resolveCodexCommand() {
|
|
2920
|
+
if (canRun("codex", ["--version"])) {
|
|
2921
|
+
return "codex";
|
|
2922
|
+
}
|
|
2923
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
2924
|
+
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2925
|
+
return userCandidate;
|
|
2926
|
+
}
|
|
2927
|
+
const prefix = process.env.PREFIX?.trim();
|
|
2928
|
+
if (!prefix) {
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2931
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
2932
|
+
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2933
|
+
return candidate;
|
|
2934
|
+
}
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
3839
2937
|
function resolveCloudflaredCommand() {
|
|
3840
2938
|
if (canRun("cloudflared", ["--version"])) {
|
|
3841
2939
|
return "cloudflared";
|
|
3842
2940
|
}
|
|
3843
|
-
const localCandidate =
|
|
3844
|
-
if (
|
|
2941
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
2942
|
+
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
3845
2943
|
return localCandidate;
|
|
3846
2944
|
}
|
|
3847
2945
|
return null;
|
|
@@ -3894,9 +2992,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3894
2992
|
if (!mappedArch) {
|
|
3895
2993
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
3896
2994
|
}
|
|
3897
|
-
const userBinDir =
|
|
2995
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
3898
2996
|
mkdirSync(userBinDir, { recursive: true });
|
|
3899
|
-
const destination =
|
|
2997
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
3900
2998
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
3901
2999
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
3902
3000
|
await downloadFile(downloadUrl, destination);
|
|
@@ -3910,19 +3008,11 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3910
3008
|
return installed;
|
|
3911
3009
|
}
|
|
3912
3010
|
async function shouldInstallCloudflaredInteractively() {
|
|
3913
|
-
if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
|
|
3914
|
-
return false;
|
|
3915
|
-
}
|
|
3916
|
-
hasPromptedCloudflaredInstall = true;
|
|
3917
|
-
await persistCloudflaredInstallPrompted();
|
|
3918
|
-
if (process.platform === "win32") {
|
|
3919
|
-
return false;
|
|
3920
|
-
}
|
|
3921
3011
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3922
3012
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
3923
3013
|
return false;
|
|
3924
3014
|
}
|
|
3925
|
-
const prompt =
|
|
3015
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
3926
3016
|
try {
|
|
3927
3017
|
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
3928
3018
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -3936,9 +3026,6 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3936
3026
|
if (current) {
|
|
3937
3027
|
return current;
|
|
3938
3028
|
}
|
|
3939
|
-
if (process.platform === "win32") {
|
|
3940
|
-
return null;
|
|
3941
|
-
}
|
|
3942
3029
|
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
3943
3030
|
if (!installApproved) {
|
|
3944
3031
|
return null;
|
|
@@ -3946,8 +3033,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3946
3033
|
return ensureCloudflaredInstalledLinux();
|
|
3947
3034
|
}
|
|
3948
3035
|
function hasCodexAuth() {
|
|
3949
|
-
const codexHome =
|
|
3950
|
-
return
|
|
3036
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3037
|
+
return existsSync3(join5(codexHome, "auth.json"));
|
|
3951
3038
|
}
|
|
3952
3039
|
function ensureCodexInstalled() {
|
|
3953
3040
|
let codexCommand = resolveCodexCommand();
|
|
@@ -3965,7 +3052,7 @@ function ensureCodexInstalled() {
|
|
|
3965
3052
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
3966
3053
|
`);
|
|
3967
3054
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
3968
|
-
process.env.PATH = `${
|
|
3055
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
3969
3056
|
};
|
|
3970
3057
|
if (isTermuxRuntime()) {
|
|
3971
3058
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -4028,22 +3115,19 @@ function parseCloudflaredUrl(chunk) {
|
|
|
4028
3115
|
}
|
|
4029
3116
|
function getAccessibleUrls(port) {
|
|
4030
3117
|
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
3118
|
+
const interfaces = networkInterfaces();
|
|
3119
|
+
for (const entries of Object.values(interfaces)) {
|
|
3120
|
+
if (!entries) {
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
for (const entry of entries) {
|
|
3124
|
+
if (entry.internal) {
|
|
4035
3125
|
continue;
|
|
4036
3126
|
}
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
continue;
|
|
4040
|
-
}
|
|
4041
|
-
if (entry.family === "IPv4") {
|
|
4042
|
-
urls.add(`http://${entry.address}:${String(port)}`);
|
|
4043
|
-
}
|
|
3127
|
+
if (entry.family === "IPv4") {
|
|
3128
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
4044
3129
|
}
|
|
4045
3130
|
}
|
|
4046
|
-
} catch {
|
|
4047
3131
|
}
|
|
4048
3132
|
return Array.from(urls);
|
|
4049
3133
|
}
|
|
@@ -4106,8 +3190,8 @@ function listenWithFallback(server, startPort) {
|
|
|
4106
3190
|
});
|
|
4107
3191
|
}
|
|
4108
3192
|
function getCodexGlobalStatePath2() {
|
|
4109
|
-
const codexHome =
|
|
4110
|
-
return
|
|
3193
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3194
|
+
return join5(codexHome, ".codex-global-state.json");
|
|
4111
3195
|
}
|
|
4112
3196
|
function normalizeUniqueStrings(value) {
|
|
4113
3197
|
if (!Array.isArray(value)) return [];
|
|
@@ -4231,7 +3315,7 @@ async function startServer(options) {
|
|
|
4231
3315
|
qrcode.generate(tunnelUrl, { small: true });
|
|
4232
3316
|
console.log("");
|
|
4233
3317
|
}
|
|
4234
|
-
|
|
3318
|
+
openBrowser(`http://localhost:${String(port)}`);
|
|
4235
3319
|
function shutdown() {
|
|
4236
3320
|
console.log("\nShutting down...");
|
|
4237
3321
|
if (tunnelChild && !tunnelChild.killed) {
|
|
@@ -4254,7 +3338,7 @@ async function runLogin() {
|
|
|
4254
3338
|
console.log("\nStarting `codex login`...\n");
|
|
4255
3339
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
4256
3340
|
}
|
|
4257
|
-
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").
|
|
3341
|
+
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (projectPath, opts) => {
|
|
4258
3342
|
const rawArgv = process.argv.slice(2);
|
|
4259
3343
|
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
4260
3344
|
let openProjectOnly = (opts.openProject ?? "").trim();
|