codexapp 0.1.26 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-cSjdFqX4.js +48 -0
- package/dist/assets/index-sxLu4gFb.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +252 -3
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-Bmlv0aWl.js +0 -48
- package/dist/assets/index-lTEk-dqp.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -21,11 +21,12 @@ import express from "express";
|
|
|
21
21
|
|
|
22
22
|
// src/server/codexAppServerBridge.ts
|
|
23
23
|
import { spawn } from "child_process";
|
|
24
|
+
import { randomBytes } from "crypto";
|
|
24
25
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
25
26
|
import { request as httpsRequest } from "https";
|
|
26
27
|
import { homedir } from "os";
|
|
27
28
|
import { tmpdir } from "os";
|
|
28
|
-
import { isAbsolute, join, resolve } from "path";
|
|
29
|
+
import { basename, isAbsolute, join, resolve } from "path";
|
|
29
30
|
import { writeFile } from "fs/promises";
|
|
30
31
|
function asRecord(value) {
|
|
31
32
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -49,6 +50,46 @@ function setJson(res, statusCode, payload) {
|
|
|
49
50
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
50
51
|
res.end(JSON.stringify(payload));
|
|
51
52
|
}
|
|
53
|
+
function extractThreadMessageText(threadReadPayload) {
|
|
54
|
+
const payload = asRecord(threadReadPayload);
|
|
55
|
+
const thread = asRecord(payload?.thread);
|
|
56
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
57
|
+
const parts = [];
|
|
58
|
+
for (const turn of turns) {
|
|
59
|
+
const turnRecord = asRecord(turn);
|
|
60
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
const itemRecord = asRecord(item);
|
|
63
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
64
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
65
|
+
parts.push(itemRecord.text.trim());
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (type === "userMessage") {
|
|
69
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
70
|
+
for (const block of content) {
|
|
71
|
+
const blockRecord = asRecord(block);
|
|
72
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
73
|
+
parts.push(blockRecord.text.trim());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (type === "commandExecution") {
|
|
79
|
+
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
80
|
+
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
81
|
+
if (command) parts.push(command);
|
|
82
|
+
if (output) parts.push(output);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return parts.join("\n").trim();
|
|
87
|
+
}
|
|
88
|
+
function isExactPhraseMatch(query, doc) {
|
|
89
|
+
const q = query.trim().toLowerCase();
|
|
90
|
+
if (!q) return false;
|
|
91
|
+
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
92
|
+
}
|
|
52
93
|
function scoreFileCandidate(path, query) {
|
|
53
94
|
if (!query) return 0;
|
|
54
95
|
const lowerPath = path.toLowerCase();
|
|
@@ -122,6 +163,55 @@ async function runCommand(command, args, options = {}) {
|
|
|
122
163
|
});
|
|
123
164
|
});
|
|
124
165
|
}
|
|
166
|
+
function isMissingHeadError(error) {
|
|
167
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
168
|
+
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
|
|
169
|
+
}
|
|
170
|
+
function isNotGitRepositoryError(error) {
|
|
171
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
172
|
+
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
173
|
+
}
|
|
174
|
+
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
175
|
+
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
176
|
+
try {
|
|
177
|
+
await stat(agentsPath);
|
|
178
|
+
} catch {
|
|
179
|
+
await writeFile(agentsPath, "", "utf8");
|
|
180
|
+
}
|
|
181
|
+
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
182
|
+
await runCommand(
|
|
183
|
+
"git",
|
|
184
|
+
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
185
|
+
{ cwd: repoRoot }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async function runCommandCapture(command, args, options = {}) {
|
|
189
|
+
return await new Promise((resolveOutput, reject) => {
|
|
190
|
+
const proc = spawn(command, args, {
|
|
191
|
+
cwd: options.cwd,
|
|
192
|
+
env: process.env,
|
|
193
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
194
|
+
});
|
|
195
|
+
let stdout = "";
|
|
196
|
+
let stderr = "";
|
|
197
|
+
proc.stdout.on("data", (chunk) => {
|
|
198
|
+
stdout += chunk.toString();
|
|
199
|
+
});
|
|
200
|
+
proc.stderr.on("data", (chunk) => {
|
|
201
|
+
stderr += chunk.toString();
|
|
202
|
+
});
|
|
203
|
+
proc.on("error", reject);
|
|
204
|
+
proc.on("close", (code) => {
|
|
205
|
+
if (code === 0) {
|
|
206
|
+
resolveOutput(stdout.trim());
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
210
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
211
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
125
215
|
async function detectUserSkillsDir(appServer) {
|
|
126
216
|
try {
|
|
127
217
|
const result = await appServer.rpc("skills/list", {});
|
|
@@ -842,8 +932,82 @@ function getSharedBridgeState() {
|
|
|
842
932
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
843
933
|
return created;
|
|
844
934
|
}
|
|
935
|
+
async function loadAllThreadsForSearch(appServer) {
|
|
936
|
+
const threads = [];
|
|
937
|
+
let cursor = null;
|
|
938
|
+
do {
|
|
939
|
+
const response = asRecord(await appServer.rpc("thread/list", {
|
|
940
|
+
archived: false,
|
|
941
|
+
limit: 100,
|
|
942
|
+
sortKey: "updated_at",
|
|
943
|
+
cursor
|
|
944
|
+
}));
|
|
945
|
+
const data = Array.isArray(response?.data) ? response.data : [];
|
|
946
|
+
for (const row of data) {
|
|
947
|
+
const record = asRecord(row);
|
|
948
|
+
const id = typeof record?.id === "string" ? record.id : "";
|
|
949
|
+
if (!id) continue;
|
|
950
|
+
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";
|
|
951
|
+
const preview = typeof record?.preview === "string" ? record.preview : "";
|
|
952
|
+
threads.push({ id, title, preview });
|
|
953
|
+
}
|
|
954
|
+
cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
|
|
955
|
+
} while (cursor);
|
|
956
|
+
const docs = [];
|
|
957
|
+
const concurrency = 4;
|
|
958
|
+
for (let offset = 0; offset < threads.length; offset += concurrency) {
|
|
959
|
+
const batch = threads.slice(offset, offset + concurrency);
|
|
960
|
+
const loaded = await Promise.all(batch.map(async (thread) => {
|
|
961
|
+
try {
|
|
962
|
+
const readResponse = await appServer.rpc("thread/read", {
|
|
963
|
+
threadId: thread.id,
|
|
964
|
+
includeTurns: true
|
|
965
|
+
});
|
|
966
|
+
const messageText = extractThreadMessageText(readResponse);
|
|
967
|
+
const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
|
|
968
|
+
return {
|
|
969
|
+
id: thread.id,
|
|
970
|
+
title: thread.title,
|
|
971
|
+
preview: thread.preview,
|
|
972
|
+
messageText,
|
|
973
|
+
searchableText
|
|
974
|
+
};
|
|
975
|
+
} catch {
|
|
976
|
+
const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
|
|
977
|
+
return {
|
|
978
|
+
id: thread.id,
|
|
979
|
+
title: thread.title,
|
|
980
|
+
preview: thread.preview,
|
|
981
|
+
messageText: "",
|
|
982
|
+
searchableText
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
}));
|
|
986
|
+
docs.push(...loaded);
|
|
987
|
+
}
|
|
988
|
+
return docs;
|
|
989
|
+
}
|
|
990
|
+
async function buildThreadSearchIndex(appServer) {
|
|
991
|
+
const docs = await loadAllThreadsForSearch(appServer);
|
|
992
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
993
|
+
return { docsById };
|
|
994
|
+
}
|
|
845
995
|
function createCodexBridgeMiddleware() {
|
|
846
996
|
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
997
|
+
let threadSearchIndex = null;
|
|
998
|
+
let threadSearchIndexPromise = null;
|
|
999
|
+
async function getThreadSearchIndex() {
|
|
1000
|
+
if (threadSearchIndex) return threadSearchIndex;
|
|
1001
|
+
if (!threadSearchIndexPromise) {
|
|
1002
|
+
threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
|
|
1003
|
+
threadSearchIndex = index;
|
|
1004
|
+
return index;
|
|
1005
|
+
}).finally(() => {
|
|
1006
|
+
threadSearchIndexPromise = null;
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return threadSearchIndexPromise;
|
|
1010
|
+
}
|
|
847
1011
|
const middleware = async (req, res, next) => {
|
|
848
1012
|
try {
|
|
849
1013
|
if (!req.url) {
|
|
@@ -909,6 +1073,76 @@ function createCodexBridgeMiddleware() {
|
|
|
909
1073
|
setJson(res, 200, { data: { path: homedir() } });
|
|
910
1074
|
return;
|
|
911
1075
|
}
|
|
1076
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1077
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1078
|
+
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1079
|
+
if (!rawSourceCwd) {
|
|
1080
|
+
setJson(res, 400, { error: "Missing sourceCwd" });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1084
|
+
try {
|
|
1085
|
+
const sourceInfo = await stat(sourceCwd);
|
|
1086
|
+
if (!sourceInfo.isDirectory()) {
|
|
1087
|
+
setJson(res, 400, { error: "sourceCwd is not a directory" });
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
} catch {
|
|
1091
|
+
setJson(res, 404, { error: "sourceCwd does not exist" });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
let gitRoot = "";
|
|
1096
|
+
try {
|
|
1097
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (!isNotGitRepositoryError(error)) throw error;
|
|
1100
|
+
await runCommand("git", ["init"], { cwd: sourceCwd });
|
|
1101
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1102
|
+
}
|
|
1103
|
+
const repoName = basename(gitRoot) || "repo";
|
|
1104
|
+
const worktreesRoot = join(getCodexHomeDir(), "worktrees");
|
|
1105
|
+
await mkdir(worktreesRoot, { recursive: true });
|
|
1106
|
+
let worktreeId = "";
|
|
1107
|
+
let worktreeParent = "";
|
|
1108
|
+
let worktreeCwd = "";
|
|
1109
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1110
|
+
const candidate = randomBytes(2).toString("hex");
|
|
1111
|
+
const parent = join(worktreesRoot, candidate);
|
|
1112
|
+
try {
|
|
1113
|
+
await stat(parent);
|
|
1114
|
+
continue;
|
|
1115
|
+
} catch {
|
|
1116
|
+
worktreeId = candidate;
|
|
1117
|
+
worktreeParent = parent;
|
|
1118
|
+
worktreeCwd = join(parent, repoName);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
1123
|
+
throw new Error("Failed to allocate a unique worktree id");
|
|
1124
|
+
}
|
|
1125
|
+
const branch = `codex/${worktreeId}`;
|
|
1126
|
+
await mkdir(worktreeParent, { recursive: true });
|
|
1127
|
+
try {
|
|
1128
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
if (!isMissingHeadError(error)) throw error;
|
|
1131
|
+
await ensureRepoHasInitialCommit(gitRoot);
|
|
1132
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1133
|
+
}
|
|
1134
|
+
setJson(res, 200, {
|
|
1135
|
+
data: {
|
|
1136
|
+
cwd: worktreeCwd,
|
|
1137
|
+
branch,
|
|
1138
|
+
gitRoot
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
912
1146
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
913
1147
|
const payload = await readJsonBody(req);
|
|
914
1148
|
const record = asRecord(payload);
|
|
@@ -1034,6 +1268,20 @@ function createCodexBridgeMiddleware() {
|
|
|
1034
1268
|
setJson(res, 200, { data: cache });
|
|
1035
1269
|
return;
|
|
1036
1270
|
}
|
|
1271
|
+
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1272
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1273
|
+
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1274
|
+
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1275
|
+
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1276
|
+
if (!query) {
|
|
1277
|
+
setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const index = await getThreadSearchIndex();
|
|
1281
|
+
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1282
|
+
setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1037
1285
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1038
1286
|
const payload = asRecord(await readJsonBody(req));
|
|
1039
1287
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
@@ -1197,6 +1445,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1197
1445
|
}
|
|
1198
1446
|
};
|
|
1199
1447
|
middleware.dispose = () => {
|
|
1448
|
+
threadSearchIndex = null;
|
|
1200
1449
|
appServer.dispose();
|
|
1201
1450
|
};
|
|
1202
1451
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1211,7 +1460,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1211
1460
|
}
|
|
1212
1461
|
|
|
1213
1462
|
// src/server/authMiddleware.ts
|
|
1214
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1463
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1215
1464
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1216
1465
|
function constantTimeCompare(a, b) {
|
|
1217
1466
|
const bufA = Buffer.from(a);
|
|
@@ -1309,7 +1558,7 @@ function createAuthSession(password) {
|
|
|
1309
1558
|
res.status(401).json({ error: "Invalid password" });
|
|
1310
1559
|
return;
|
|
1311
1560
|
}
|
|
1312
|
-
const token =
|
|
1561
|
+
const token = randomBytes2(32).toString("hex");
|
|
1313
1562
|
validTokens.add(token);
|
|
1314
1563
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1315
1564
|
res.json({ ok: true });
|