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-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 = randomBytes(32).toString("hex");
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 });