codexapp 0.1.23 → 0.1.25

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
@@ -2,13 +2,14 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { existsSync as existsSync2 } from "fs";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
6
6
  import { readFile as readFile2 } from "fs/promises";
7
7
  import { homedir as homedir2 } from "os";
8
8
  import { join as join3 } from "path";
9
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
11
  import { dirname as dirname2 } from "path";
12
+ import { get as httpsGet } from "https";
12
13
  import { Command } from "commander";
13
14
  import qrcode from "qrcode-terminal";
14
15
 
@@ -50,6 +51,46 @@ function setJson(res, statusCode, payload) {
50
51
  res.setHeader("Content-Type", "application/json; charset=utf-8");
51
52
  res.end(JSON.stringify(payload));
52
53
  }
54
+ function extractThreadMessageText(threadReadPayload) {
55
+ const payload = asRecord(threadReadPayload);
56
+ const thread = asRecord(payload?.thread);
57
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
58
+ const parts = [];
59
+ for (const turn of turns) {
60
+ const turnRecord = asRecord(turn);
61
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
62
+ for (const item of items) {
63
+ const itemRecord = asRecord(item);
64
+ const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
65
+ if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
66
+ parts.push(itemRecord.text.trim());
67
+ continue;
68
+ }
69
+ if (type === "userMessage") {
70
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
71
+ for (const block of content) {
72
+ const blockRecord = asRecord(block);
73
+ if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
74
+ parts.push(blockRecord.text.trim());
75
+ }
76
+ }
77
+ continue;
78
+ }
79
+ if (type === "commandExecution") {
80
+ const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
81
+ const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
82
+ if (command) parts.push(command);
83
+ if (output) parts.push(output);
84
+ }
85
+ }
86
+ }
87
+ return parts.join("\n").trim();
88
+ }
89
+ function isExactPhraseMatch(query, doc) {
90
+ const q = query.trim().toLowerCase();
91
+ if (!q) return false;
92
+ return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
93
+ }
53
94
  function scoreFileCandidate(path, query) {
54
95
  if (!query) return 0;
55
96
  const lowerPath = path.toLowerCase();
@@ -892,8 +933,82 @@ function getSharedBridgeState() {
892
933
  globalScope[SHARED_BRIDGE_KEY] = created;
893
934
  return created;
894
935
  }
936
+ async function loadAllThreadsForSearch(appServer) {
937
+ const threads = [];
938
+ let cursor = null;
939
+ do {
940
+ const response = asRecord(await appServer.rpc("thread/list", {
941
+ archived: false,
942
+ limit: 100,
943
+ sortKey: "updated_at",
944
+ cursor
945
+ }));
946
+ const data = Array.isArray(response?.data) ? response.data : [];
947
+ for (const row of data) {
948
+ const record = asRecord(row);
949
+ const id = typeof record?.id === "string" ? record.id : "";
950
+ if (!id) continue;
951
+ 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";
952
+ const preview = typeof record?.preview === "string" ? record.preview : "";
953
+ threads.push({ id, title, preview });
954
+ }
955
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
956
+ } while (cursor);
957
+ const docs = [];
958
+ const concurrency = 4;
959
+ for (let offset = 0; offset < threads.length; offset += concurrency) {
960
+ const batch = threads.slice(offset, offset + concurrency);
961
+ const loaded = await Promise.all(batch.map(async (thread) => {
962
+ try {
963
+ const readResponse = await appServer.rpc("thread/read", {
964
+ threadId: thread.id,
965
+ includeTurns: true
966
+ });
967
+ const messageText = extractThreadMessageText(readResponse);
968
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
969
+ return {
970
+ id: thread.id,
971
+ title: thread.title,
972
+ preview: thread.preview,
973
+ messageText,
974
+ searchableText
975
+ };
976
+ } catch {
977
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
978
+ return {
979
+ id: thread.id,
980
+ title: thread.title,
981
+ preview: thread.preview,
982
+ messageText: "",
983
+ searchableText
984
+ };
985
+ }
986
+ }));
987
+ docs.push(...loaded);
988
+ }
989
+ return docs;
990
+ }
991
+ async function buildThreadSearchIndex(appServer) {
992
+ const docs = await loadAllThreadsForSearch(appServer);
993
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
994
+ return { docsById };
995
+ }
895
996
  function createCodexBridgeMiddleware() {
896
997
  const { appServer, methodCatalog } = getSharedBridgeState();
998
+ let threadSearchIndex = null;
999
+ let threadSearchIndexPromise = null;
1000
+ async function getThreadSearchIndex() {
1001
+ if (threadSearchIndex) return threadSearchIndex;
1002
+ if (!threadSearchIndexPromise) {
1003
+ threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
1004
+ threadSearchIndex = index;
1005
+ return index;
1006
+ }).finally(() => {
1007
+ threadSearchIndexPromise = null;
1008
+ });
1009
+ }
1010
+ return threadSearchIndexPromise;
1011
+ }
897
1012
  const middleware = async (req, res, next) => {
898
1013
  try {
899
1014
  if (!req.url) {
@@ -1154,6 +1269,20 @@ function createCodexBridgeMiddleware() {
1154
1269
  setJson(res, 200, { data: cache });
1155
1270
  return;
1156
1271
  }
1272
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1273
+ const payload = asRecord(await readJsonBody(req));
1274
+ const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1275
+ const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1276
+ const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1277
+ if (!query) {
1278
+ setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1279
+ return;
1280
+ }
1281
+ const index = await getThreadSearchIndex();
1282
+ const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
1283
+ setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1284
+ return;
1285
+ }
1157
1286
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1158
1287
  const payload = asRecord(await readJsonBody(req));
1159
1288
  const id = typeof payload?.id === "string" ? payload.id : "";
@@ -1317,6 +1446,7 @@ data: ${JSON.stringify({ ok: true })}
1317
1446
  }
1318
1447
  };
1319
1448
  middleware.dispose = () => {
1449
+ threadSearchIndex = null;
1320
1450
  appServer.dispose();
1321
1451
  };
1322
1452
  middleware.subscribeNotifications = (listener) => {
@@ -1721,6 +1851,79 @@ function resolveCodexCommand() {
1721
1851
  }
1722
1852
  return null;
1723
1853
  }
1854
+ function resolveCloudflaredCommand() {
1855
+ if (canRun("cloudflared", ["--version"])) {
1856
+ return "cloudflared";
1857
+ }
1858
+ const localCandidate = join3(homedir2(), ".local", "bin", "cloudflared");
1859
+ if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
1860
+ return localCandidate;
1861
+ }
1862
+ return null;
1863
+ }
1864
+ function mapCloudflaredLinuxArch(arch) {
1865
+ if (arch === "x64") {
1866
+ return "amd64";
1867
+ }
1868
+ if (arch === "arm64") {
1869
+ return "arm64";
1870
+ }
1871
+ return null;
1872
+ }
1873
+ function downloadFile(url, destination) {
1874
+ return new Promise((resolve2, reject) => {
1875
+ const request = (currentUrl) => {
1876
+ httpsGet(currentUrl, (response) => {
1877
+ const code = response.statusCode ?? 0;
1878
+ if (code >= 300 && code < 400 && response.headers.location) {
1879
+ response.resume();
1880
+ request(response.headers.location);
1881
+ return;
1882
+ }
1883
+ if (code !== 200) {
1884
+ response.resume();
1885
+ reject(new Error(`Download failed with HTTP status ${String(code)}`));
1886
+ return;
1887
+ }
1888
+ const file = createWriteStream(destination, { mode: 493 });
1889
+ response.pipe(file);
1890
+ file.on("finish", () => {
1891
+ file.close();
1892
+ resolve2();
1893
+ });
1894
+ file.on("error", reject);
1895
+ }).on("error", reject);
1896
+ };
1897
+ request(url);
1898
+ });
1899
+ }
1900
+ async function ensureCloudflaredInstalledLinux() {
1901
+ const current = resolveCloudflaredCommand();
1902
+ if (current) {
1903
+ return current;
1904
+ }
1905
+ if (process.platform !== "linux") {
1906
+ return null;
1907
+ }
1908
+ const mappedArch = mapCloudflaredLinuxArch(process.arch);
1909
+ if (!mappedArch) {
1910
+ throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
1911
+ }
1912
+ const userBinDir = join3(homedir2(), ".local", "bin");
1913
+ mkdirSync(userBinDir, { recursive: true });
1914
+ const destination = join3(userBinDir, "cloudflared");
1915
+ const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
1916
+ console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
1917
+ await downloadFile(downloadUrl, destination);
1918
+ chmodSync(destination, 493);
1919
+ process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
1920
+ const installed = resolveCloudflaredCommand();
1921
+ if (!installed) {
1922
+ throw new Error("cloudflared download completed but executable is still not available");
1923
+ }
1924
+ console.log("\ncloudflared installed.\n");
1925
+ return installed;
1926
+ }
1724
1927
  function hasCodexAuth() {
1725
1928
  const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1726
1929
  return existsSync2(join3(codexHome, "auth.json"));
@@ -1802,9 +2005,9 @@ function parseCloudflaredUrl(chunk) {
1802
2005
  }
1803
2006
  return urlMatch[urlMatch.length - 1] ?? null;
1804
2007
  }
1805
- async function startCloudflaredTunnel(localPort) {
2008
+ async function startCloudflaredTunnel(command, localPort) {
1806
2009
  return new Promise((resolve2, reject) => {
1807
- const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2010
+ const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
1808
2011
  stdio: ["ignore", "pipe", "pipe"]
1809
2012
  });
1810
2013
  const timeout = setTimeout(() => {
@@ -1877,7 +2080,8 @@ async function startServer(options) {
1877
2080
  let tunnelUrl = null;
1878
2081
  if (options.tunnel) {
1879
2082
  try {
1880
- const tunnel = await startCloudflaredTunnel(port);
2083
+ const cloudflaredCommand = await ensureCloudflaredInstalledLinux() ?? "cloudflared";
2084
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
1881
2085
  tunnelChild = tunnel.process;
1882
2086
  tunnelUrl = tunnel.url;
1883
2087
  } catch (error) {