@pixelbyte-software/pixcode 1.53.18 → 1.53.20

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.
@@ -75,6 +75,7 @@ const FILE_COUNT_SCAN_MAX_MS = 2500;
75
75
  const CODEX_SESSION_INDEX_TTL_MS = 60 * 1000;
76
76
  const CODEX_SESSION_INDEX_MAX_FILES = 1200;
77
77
  const CODEX_SESSION_INDEX_MAX_DIRS = 2500;
78
+ const CLAUDE_JSONL_LINE_MAX_CHARS = 1024 * 1024;
78
79
  const GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS = 60 * 1000;
79
80
  const CLI_CHAT_FILE_READ_MAX_BYTES = 2 * 1024 * 1024;
80
81
  const CLI_CHAT_FILES_PER_PROJECT_LIMIT = 150;
@@ -932,11 +933,12 @@ async function getSessions(projectName, limit = 5, offset = 0) {
932
933
  filesWithStats.sort((a, b) => b.mtime - a.mtime);
933
934
 
934
935
  const allSessions = new Map();
935
- const allEntries = [];
936
- const uuidToSessionMap = new Map();
936
+ const sessionFirstUserMessageIds = new Map();
937
937
 
938
938
  // Collect all sessions and entries from all files
939
+ let filesScanned = 0;
939
940
  for (const { file } of filesWithStats) {
941
+ filesScanned++;
940
942
  const jsonlFile = path.join(projectDir, file);
941
943
  const result = await parseJsonlSessions(jsonlFile);
942
944
 
@@ -946,35 +948,30 @@ async function getSessions(projectName, limit = 5, offset = 0) {
946
948
  }
947
949
  });
948
950
 
949
- allEntries.push(...result.entries);
951
+ result.firstUserMessageIds.forEach((firstUserMsgId, sessionId) => {
952
+ if (!sessionFirstUserMessageIds.has(sessionId)) {
953
+ sessionFirstUserMessageIds.set(sessionId, firstUserMsgId);
954
+ }
955
+ });
950
956
 
951
957
  // Early exit optimization for large projects
952
- if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
958
+ if (allSessions.size >= (limit + offset) * 2 && filesScanned >= Math.min(3, filesWithStats.length)) {
953
959
  break;
954
960
  }
955
961
  }
956
962
 
957
- // Build UUID-to-session mapping for timeline detection
958
- allEntries.forEach(entry => {
959
- if (entry.uuid && entry.sessionId) {
960
- uuidToSessionMap.set(entry.uuid, entry.sessionId);
961
- }
962
- });
963
-
964
963
  // Group sessions by first user message ID
965
964
  const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
966
965
  const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
967
966
 
968
967
  // Find the first user message for each session
969
- allEntries.forEach(entry => {
970
- if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
971
- // This is a first user message in a session (parentUuid is null)
972
- const firstUserMsgId = entry.uuid;
968
+ sessionFirstUserMessageIds.forEach((firstUserMsgId, sessionId) => {
969
+ if (sessionId && firstUserMsgId) {
973
970
 
974
- if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
975
- sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
971
+ if (!sessionToFirstUserMsgId.has(sessionId)) {
972
+ sessionToFirstUserMsgId.set(sessionId, firstUserMsgId);
976
973
 
977
- const session = allSessions.get(entry.sessionId);
974
+ const session = allSessions.get(sessionId);
978
975
  if (session) {
979
976
  if (!sessionGroups.has(firstUserMsgId)) {
980
977
  sessionGroups.set(firstUserMsgId, {
@@ -1038,7 +1035,7 @@ async function getSessions(projectName, limit = 5, offset = 0) {
1038
1035
 
1039
1036
  async function parseJsonlSessions(filePath) {
1040
1037
  const sessions = new Map();
1041
- const entries = [];
1038
+ const firstUserMessageIds = new Map();
1042
1039
  const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
1043
1040
 
1044
1041
  try {
@@ -1050,9 +1047,12 @@ async function parseJsonlSessions(filePath) {
1050
1047
 
1051
1048
  for await (const line of rl) {
1052
1049
  if (line.trim()) {
1050
+ if (line.length > CLAUDE_JSONL_LINE_MAX_CHARS) {
1051
+ continue;
1052
+ }
1053
+
1053
1054
  try {
1054
1055
  const entry = JSON.parse(line);
1055
- entries.push(entry);
1056
1056
 
1057
1057
  // Handle summary entries that don't have sessionId yet
1058
1058
  if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
@@ -1074,6 +1074,15 @@ async function parseJsonlSessions(filePath) {
1074
1074
 
1075
1075
  const session = sessions.get(entry.sessionId);
1076
1076
 
1077
+ if (
1078
+ entry.type === 'user'
1079
+ && entry.parentUuid === null
1080
+ && entry.uuid
1081
+ && !firstUserMessageIds.has(entry.sessionId)
1082
+ ) {
1083
+ firstUserMessageIds.set(entry.sessionId, entry.uuid);
1084
+ }
1085
+
1077
1086
  // Apply pending summary if this entry has a parentUuid that matches a pending summary
1078
1087
  if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
1079
1088
  session.summary = pendingSummaries.get(entry.parentUuid);
@@ -1180,12 +1189,12 @@ async function parseJsonlSessions(filePath) {
1180
1189
 
1181
1190
  return {
1182
1191
  sessions: filteredSessions,
1183
- entries: entries
1192
+ firstUserMessageIds,
1184
1193
  };
1185
1194
 
1186
1195
  } catch (error) {
1187
1196
  console.error('Error reading JSONL file:', error);
1188
- return { sessions: [], entries: [] };
1197
+ return { sessions: [], firstUserMessageIds: new Map() };
1189
1198
  }
1190
1199
  }
1191
1200
 
@@ -1249,6 +1258,9 @@ async function parseAgentTools(filePath) {
1249
1258
  // Get messages for a specific session with pagination support
1250
1259
  async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
1251
1260
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1261
+ const boundedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
1262
+ const boundedOffset = Math.max(0, Number(offset) || 0);
1263
+ const maxBufferedMessages = boundedLimit === null ? Infinity : boundedLimit + boundedOffset;
1252
1264
 
1253
1265
  try {
1254
1266
  const files = await fs.readdir(projectDir);
@@ -1261,6 +1273,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1261
1273
  }
1262
1274
 
1263
1275
  const messages = [];
1276
+ let totalMessages = 0;
1264
1277
  // Map of agentId -> tools for subagent tool grouping
1265
1278
  const agentToolsCache = new Map();
1266
1279
 
@@ -1276,9 +1289,17 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1276
1289
  for await (const line of rl) {
1277
1290
  if (line.trim()) {
1278
1291
  try {
1292
+ if (line.length > CLAUDE_JSONL_LINE_MAX_CHARS) {
1293
+ continue;
1294
+ }
1295
+
1279
1296
  const entry = JSON.parse(line);
1280
1297
  if (entry.sessionId === sessionId) {
1298
+ totalMessages += 1;
1281
1299
  messages.push(entry);
1300
+ if (messages.length > maxBufferedMessages) {
1301
+ messages.shift();
1302
+ }
1282
1303
  }
1283
1304
  } catch (parseError) {
1284
1305
  // Silently skip malformed JSONL lines (common with concurrent writes)
@@ -1320,30 +1341,30 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1320
1341
  new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
1321
1342
  );
1322
1343
 
1323
- const total = sortedMessages.length;
1344
+ const total = boundedLimit === null ? sortedMessages.length : totalMessages;
1324
1345
 
1325
1346
  // If no limit is specified, return all messages (backward compatibility)
1326
- if (limit === null) {
1347
+ if (boundedLimit === null) {
1327
1348
  return sortedMessages;
1328
1349
  }
1329
1350
 
1330
1351
  // Apply pagination - for recent messages, we need to slice from the end
1331
1352
  // offset 0 should give us the most recent messages
1332
- const startIndex = Math.max(0, total - offset - limit);
1333
- const endIndex = total - offset;
1353
+ const startIndex = Math.max(0, sortedMessages.length - boundedOffset - boundedLimit);
1354
+ const endIndex = sortedMessages.length - boundedOffset;
1334
1355
  const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
1335
- const hasMore = startIndex > 0;
1356
+ const hasMore = total > boundedOffset + boundedLimit;
1336
1357
 
1337
1358
  return {
1338
1359
  messages: paginatedMessages,
1339
1360
  total,
1340
1361
  hasMore,
1341
- offset,
1342
- limit
1362
+ offset: boundedOffset,
1363
+ limit: boundedLimit
1343
1364
  };
1344
1365
  } catch (error) {
1345
1366
  console.error(`Error reading messages for session ${sessionId}:`, error);
1346
- return limit === null ? [] : { messages: [], total: 0, hasMore: false };
1367
+ return boundedLimit === null ? [] : { messages: [], total: 0, hasMore: false };
1347
1368
  }
1348
1369
  }
1349
1370
 
@@ -1702,19 +1723,21 @@ async function findCodexJsonlFiles(dir, {
1702
1723
  visitedDirs += 1;
1703
1724
 
1704
1725
  try {
1705
- const entries = await fs.readdir(currentDir, { withFileTypes: true });
1706
- entries.sort((a, b) => b.name.localeCompare(a.name));
1707
-
1708
- for (const entry of entries) {
1709
- if (files.length >= maxFiles) break;
1710
- const fullPath = path.join(currentDir, entry.name);
1711
- if (entry.isDirectory()) {
1712
- if (visitedDirs + pendingDirs.length < maxDirs) {
1713
- pendingDirs.push(fullPath);
1726
+ const dirHandle = await fs.opendir(currentDir);
1727
+ try {
1728
+ for await (const entry of dirHandle) {
1729
+ if (files.length >= maxFiles) break;
1730
+ const fullPath = path.join(currentDir, entry.name);
1731
+ if (entry.isDirectory()) {
1732
+ if (visitedDirs + pendingDirs.length < maxDirs) {
1733
+ pendingDirs.push(fullPath);
1734
+ }
1735
+ } else if (entry.name.endsWith('.jsonl')) {
1736
+ files.push(fullPath);
1714
1737
  }
1715
- } else if (entry.name.endsWith('.jsonl')) {
1716
- files.push(fullPath);
1717
1738
  }
1739
+ } finally {
1740
+ await dirHandle.close().catch(() => undefined);
1718
1741
  }
1719
1742
  } catch {
1720
1743
  // Skip directories we can't read
@@ -1900,29 +1923,18 @@ async function parseCodexSessionFile(filePath) {
1900
1923
 
1901
1924
  // Get messages for a specific Codex session
1902
1925
  async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1926
+ const boundedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
1927
+ const boundedOffset = Math.max(0, Number(offset) || 0);
1928
+ const maxBufferedMessages = boundedLimit === null ? Infinity : boundedLimit + boundedOffset;
1929
+
1903
1930
  try {
1904
1931
  const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1905
1932
 
1906
- // Find the session file by searching for the session ID
1907
- const findSessionFile = async (dir) => {
1908
- try {
1909
- const entries = await fs.readdir(dir, { withFileTypes: true });
1910
- for (const entry of entries) {
1911
- const fullPath = path.join(dir, entry.name);
1912
- if (entry.isDirectory()) {
1913
- const found = await findSessionFile(fullPath);
1914
- if (found) return found;
1915
- } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
1916
- return fullPath;
1917
- }
1918
- }
1919
- } catch (error) {
1920
- // Skip directories we can't read
1921
- }
1922
- return null;
1923
- };
1924
-
1925
- const sessionFilePath = await findSessionFile(codexSessionsDir);
1933
+ const sessionFiles = await findCodexJsonlFiles(codexSessionsDir, {
1934
+ maxFiles: Math.max(CODEX_SESSION_INDEX_MAX_FILES, 10000),
1935
+ maxDirs: CODEX_SESSION_INDEX_MAX_DIRS,
1936
+ });
1937
+ const sessionFilePath = sessionFiles.find(filePath => path.basename(filePath).includes(sessionId)) || null;
1926
1938
 
1927
1939
  if (!sessionFilePath) {
1928
1940
  console.warn(`Codex session file not found for session ${sessionId}`);
@@ -1930,6 +1942,14 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1930
1942
  }
1931
1943
 
1932
1944
  const messages = [];
1945
+ let totalMessages = 0;
1946
+ const pushMessage = (message) => {
1947
+ totalMessages += 1;
1948
+ messages.push(message);
1949
+ if (messages.length > maxBufferedMessages) {
1950
+ messages.shift();
1951
+ }
1952
+ };
1933
1953
  let tokenUsage = null;
1934
1954
  const fileStream = fsSync.createReadStream(sessionFilePath);
1935
1955
  const rl = readline.createInterface({
@@ -1957,6 +1977,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1957
1977
  for await (const line of rl) {
1958
1978
  if (line.trim()) {
1959
1979
  try {
1980
+ if (line.length > CLAUDE_JSONL_LINE_MAX_CHARS) {
1981
+ continue;
1982
+ }
1983
+
1960
1984
  const entry = JSON.parse(line);
1961
1985
 
1962
1986
  // Extract token usage from token_count events (keep latest)
@@ -1972,7 +1996,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1972
1996
 
1973
1997
  // Use event_msg.user_message for user-visible inputs.
1974
1998
  if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
1975
- messages.push({
1999
+ pushMessage({
1976
2000
  type: 'user',
1977
2001
  timestamp: entry.timestamp,
1978
2002
  message: {
@@ -1994,7 +2018,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1994
2018
 
1995
2019
  // Only add if there's actual content
1996
2020
  if (textContent?.trim()) {
1997
- messages.push({
2021
+ pushMessage({
1998
2022
  type: 'assistant',
1999
2023
  timestamp: entry.timestamp,
2000
2024
  message: {
@@ -2011,7 +2035,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2011
2035
  .filter(Boolean)
2012
2036
  .join('\n');
2013
2037
  if (summaryText?.trim()) {
2014
- messages.push({
2038
+ pushMessage({
2015
2039
  type: 'thinking',
2016
2040
  timestamp: entry.timestamp,
2017
2041
  message: {
@@ -2037,7 +2061,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2037
2061
  }
2038
2062
  }
2039
2063
 
2040
- messages.push({
2064
+ pushMessage({
2041
2065
  type: 'tool_use',
2042
2066
  timestamp: entry.timestamp,
2043
2067
  toolName: toolName,
@@ -2047,7 +2071,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2047
2071
  }
2048
2072
 
2049
2073
  if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
2050
- messages.push({
2074
+ pushMessage({
2051
2075
  type: 'tool_result',
2052
2076
  timestamp: entry.timestamp,
2053
2077
  toolCallId: entry.payload.call_id,
@@ -2077,7 +2101,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2077
2101
  }
2078
2102
  }
2079
2103
 
2080
- messages.push({
2104
+ pushMessage({
2081
2105
  type: 'tool_use',
2082
2106
  timestamp: entry.timestamp,
2083
2107
  toolName: 'Edit',
@@ -2089,7 +2113,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2089
2113
  toolCallId: entry.payload.call_id
2090
2114
  });
2091
2115
  } else {
2092
- messages.push({
2116
+ pushMessage({
2093
2117
  type: 'tool_use',
2094
2118
  timestamp: entry.timestamp,
2095
2119
  toolName: toolName,
@@ -2100,7 +2124,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2100
2124
  }
2101
2125
 
2102
2126
  if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
2103
- messages.push({
2127
+ pushMessage({
2104
2128
  type: 'tool_result',
2105
2129
  timestamp: entry.timestamp,
2106
2130
  toolCallId: entry.payload.call_id,
@@ -2117,21 +2141,21 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2117
2141
  // Sort by timestamp
2118
2142
  messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
2119
2143
 
2120
- const total = messages.length;
2144
+ const total = boundedLimit === null ? messages.length : totalMessages;
2121
2145
 
2122
2146
  // Apply pagination if limit is specified
2123
- if (limit !== null) {
2124
- const startIndex = Math.max(0, total - offset - limit);
2125
- const endIndex = total - offset;
2147
+ if (boundedLimit !== null) {
2148
+ const startIndex = Math.max(0, messages.length - boundedOffset - boundedLimit);
2149
+ const endIndex = messages.length - boundedOffset;
2126
2150
  const paginatedMessages = messages.slice(startIndex, endIndex);
2127
- const hasMore = startIndex > 0;
2151
+ const hasMore = total > boundedOffset + boundedLimit;
2128
2152
 
2129
2153
  return {
2130
2154
  messages: paginatedMessages,
2131
2155
  total,
2132
2156
  hasMore,
2133
- offset,
2134
- limit,
2157
+ offset: boundedOffset,
2158
+ limit: boundedLimit,
2135
2159
  tokenUsage
2136
2160
  };
2137
2161
  }
@@ -29,6 +29,16 @@ const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
29
29
  '.turbo',
30
30
  '.cache',
31
31
  '.pixcode-dev',
32
+ '.gradle',
33
+ '.repo',
34
+ '.venv',
35
+ 'out',
36
+ 'target',
37
+ 'vendor',
38
+ 'prebuilts',
39
+ 'Pods',
40
+ 'DerivedData',
41
+ 'venv',
32
42
  ]);
33
43
 
34
44
  router.use((req, res, next) => {
@@ -80,43 +90,45 @@ async function collectFilesystemSnapshot(projectPath) {
80
90
  return;
81
91
  }
82
92
 
83
- let entries = [];
84
93
  try {
85
- entries = await fs.readdir(directoryPath, { withFileTypes: true });
86
- } catch {
87
- return;
88
- }
89
-
90
- for (const entry of entries) {
91
- if (limitReached || shouldSkipFilesystemEntry(entry.name)) {
92
- continue;
93
- }
94
+ const dir = await fs.opendir(directoryPath);
95
+ try {
96
+ for await (const entry of dir) {
97
+ if (limitReached || shouldSkipFilesystemEntry(entry.name)) {
98
+ continue;
99
+ }
94
100
 
95
- const absolutePath = path.join(directoryPath, entry.name);
101
+ const absolutePath = path.join(directoryPath, entry.name);
96
102
 
97
- if (entry.isDirectory()) {
98
- await walk(absolutePath, depth + 1);
99
- continue;
100
- }
103
+ if (entry.isDirectory()) {
104
+ await walk(absolutePath, depth + 1);
105
+ continue;
106
+ }
101
107
 
102
- if (!entry.isFile()) {
103
- continue;
104
- }
108
+ if (!entry.isFile()) {
109
+ continue;
110
+ }
105
111
 
106
- try {
107
- const stat = await fs.stat(absolutePath);
108
- snapshot.set(toProjectRelativePath(projectPath, absolutePath), {
109
- mtimeMs: Math.round(stat.mtimeMs),
110
- size: stat.size,
111
- });
112
- } catch {
113
- continue;
114
- }
112
+ try {
113
+ const stat = await fs.stat(absolutePath);
114
+ snapshot.set(toProjectRelativePath(projectPath, absolutePath), {
115
+ mtimeMs: Math.round(stat.mtimeMs),
116
+ size: stat.size,
117
+ });
118
+ } catch {
119
+ continue;
120
+ }
115
121
 
116
- if (snapshot.size >= FILESYSTEM_SCAN_MAX_FILES) {
117
- limitReached = true;
118
- break;
122
+ if (snapshot.size >= FILESYSTEM_SCAN_MAX_FILES) {
123
+ limitReached = true;
124
+ break;
125
+ }
126
+ }
127
+ } finally {
128
+ await dir.close().catch(() => undefined);
119
129
  }
130
+ } catch {
131
+ return;
120
132
  }
121
133
  }
122
134