@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.
@@ -71,6 +71,7 @@ const FILE_COUNT_SCAN_MAX_MS = 2500;
71
71
  const CODEX_SESSION_INDEX_TTL_MS = 60 * 1000;
72
72
  const CODEX_SESSION_INDEX_MAX_FILES = 1200;
73
73
  const CODEX_SESSION_INDEX_MAX_DIRS = 2500;
74
+ const CLAUDE_JSONL_LINE_MAX_CHARS = 1024 * 1024;
74
75
  const GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS = 60 * 1000;
75
76
  const CLI_CHAT_FILE_READ_MAX_BYTES = 2 * 1024 * 1024;
76
77
  const CLI_CHAT_FILES_PER_PROJECT_LIMIT = 150;
@@ -838,10 +839,11 @@ async function getSessions(projectName, limit = 5, offset = 0) {
838
839
  }));
839
840
  filesWithStats.sort((a, b) => b.mtime - a.mtime);
840
841
  const allSessions = new Map();
841
- const allEntries = [];
842
- const uuidToSessionMap = new Map();
842
+ const sessionFirstUserMessageIds = new Map();
843
843
  // Collect all sessions and entries from all files
844
+ let filesScanned = 0;
844
845
  for (const { file } of filesWithStats) {
846
+ filesScanned++;
845
847
  const jsonlFile = path.join(projectDir, file);
846
848
  const result = await parseJsonlSessions(jsonlFile);
847
849
  result.sessions.forEach(session => {
@@ -849,29 +851,25 @@ async function getSessions(projectName, limit = 5, offset = 0) {
849
851
  allSessions.set(session.id, session);
850
852
  }
851
853
  });
852
- allEntries.push(...result.entries);
854
+ result.firstUserMessageIds.forEach((firstUserMsgId, sessionId) => {
855
+ if (!sessionFirstUserMessageIds.has(sessionId)) {
856
+ sessionFirstUserMessageIds.set(sessionId, firstUserMsgId);
857
+ }
858
+ });
853
859
  // Early exit optimization for large projects
854
- if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
860
+ if (allSessions.size >= (limit + offset) * 2 && filesScanned >= Math.min(3, filesWithStats.length)) {
855
861
  break;
856
862
  }
857
863
  }
858
- // Build UUID-to-session mapping for timeline detection
859
- allEntries.forEach(entry => {
860
- if (entry.uuid && entry.sessionId) {
861
- uuidToSessionMap.set(entry.uuid, entry.sessionId);
862
- }
863
- });
864
864
  // Group sessions by first user message ID
865
865
  const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
866
866
  const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
867
867
  // Find the first user message for each session
868
- allEntries.forEach(entry => {
869
- if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
870
- // This is a first user message in a session (parentUuid is null)
871
- const firstUserMsgId = entry.uuid;
872
- if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
873
- sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
874
- const session = allSessions.get(entry.sessionId);
868
+ sessionFirstUserMessageIds.forEach((firstUserMsgId, sessionId) => {
869
+ if (sessionId && firstUserMsgId) {
870
+ if (!sessionToFirstUserMsgId.has(sessionId)) {
871
+ sessionToFirstUserMsgId.set(sessionId, firstUserMsgId);
872
+ const session = allSessions.get(sessionId);
875
873
  if (session) {
876
874
  if (!sessionGroups.has(firstUserMsgId)) {
877
875
  sessionGroups.set(firstUserMsgId, {
@@ -930,7 +928,7 @@ async function getSessions(projectName, limit = 5, offset = 0) {
930
928
  }
931
929
  async function parseJsonlSessions(filePath) {
932
930
  const sessions = new Map();
933
- const entries = [];
931
+ const firstUserMessageIds = new Map();
934
932
  const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
935
933
  try {
936
934
  const fileStream = fsSync.createReadStream(filePath);
@@ -940,9 +938,11 @@ async function parseJsonlSessions(filePath) {
940
938
  });
941
939
  for await (const line of rl) {
942
940
  if (line.trim()) {
941
+ if (line.length > CLAUDE_JSONL_LINE_MAX_CHARS) {
942
+ continue;
943
+ }
943
944
  try {
944
945
  const entry = JSON.parse(line);
945
- entries.push(entry);
946
946
  // Handle summary entries that don't have sessionId yet
947
947
  if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
948
948
  pendingSummaries.set(entry.leafUuid, entry.summary);
@@ -960,6 +960,12 @@ async function parseJsonlSessions(filePath) {
960
960
  });
961
961
  }
962
962
  const session = sessions.get(entry.sessionId);
963
+ if (entry.type === 'user'
964
+ && entry.parentUuid === null
965
+ && entry.uuid
966
+ && !firstUserMessageIds.has(entry.sessionId)) {
967
+ firstUserMessageIds.set(entry.sessionId, entry.uuid);
968
+ }
963
969
  // Apply pending summary if this entry has a parentUuid that matches a pending summary
964
970
  if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
965
971
  session.summary = pendingSummaries.get(entry.parentUuid);
@@ -1053,12 +1059,12 @@ async function parseJsonlSessions(filePath) {
1053
1059
  });
1054
1060
  return {
1055
1061
  sessions: filteredSessions,
1056
- entries: entries
1062
+ firstUserMessageIds,
1057
1063
  };
1058
1064
  }
1059
1065
  catch (error) {
1060
1066
  console.error('Error reading JSONL file:', error);
1061
- return { sessions: [], entries: [] };
1067
+ return { sessions: [], firstUserMessageIds: new Map() };
1062
1068
  }
1063
1069
  }
1064
1070
  // Parse an agent JSONL file and extract tool uses
@@ -1119,6 +1125,9 @@ async function parseAgentTools(filePath) {
1119
1125
  // Get messages for a specific session with pagination support
1120
1126
  async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
1121
1127
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1128
+ const boundedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
1129
+ const boundedOffset = Math.max(0, Number(offset) || 0);
1130
+ const maxBufferedMessages = boundedLimit === null ? Infinity : boundedLimit + boundedOffset;
1122
1131
  try {
1123
1132
  const files = await fs.readdir(projectDir);
1124
1133
  // agent-*.jsonl files contain subagent tool history - we'll process them separately
@@ -1128,6 +1137,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1128
1137
  return { messages: [], total: 0, hasMore: false };
1129
1138
  }
1130
1139
  const messages = [];
1140
+ let totalMessages = 0;
1131
1141
  // Map of agentId -> tools for subagent tool grouping
1132
1142
  const agentToolsCache = new Map();
1133
1143
  // Process all JSONL files to find messages for this session
@@ -1141,9 +1151,16 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1141
1151
  for await (const line of rl) {
1142
1152
  if (line.trim()) {
1143
1153
  try {
1154
+ if (line.length > CLAUDE_JSONL_LINE_MAX_CHARS) {
1155
+ continue;
1156
+ }
1144
1157
  const entry = JSON.parse(line);
1145
1158
  if (entry.sessionId === sessionId) {
1159
+ totalMessages += 1;
1146
1160
  messages.push(entry);
1161
+ if (messages.length > maxBufferedMessages) {
1162
+ messages.shift();
1163
+ }
1147
1164
  }
1148
1165
  }
1149
1166
  catch (parseError) {
@@ -1180,28 +1197,28 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1180
1197
  }
1181
1198
  // Sort messages by timestamp
1182
1199
  const sortedMessages = messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
1183
- const total = sortedMessages.length;
1200
+ const total = boundedLimit === null ? sortedMessages.length : totalMessages;
1184
1201
  // If no limit is specified, return all messages (backward compatibility)
1185
- if (limit === null) {
1202
+ if (boundedLimit === null) {
1186
1203
  return sortedMessages;
1187
1204
  }
1188
1205
  // Apply pagination - for recent messages, we need to slice from the end
1189
1206
  // offset 0 should give us the most recent messages
1190
- const startIndex = Math.max(0, total - offset - limit);
1191
- const endIndex = total - offset;
1207
+ const startIndex = Math.max(0, sortedMessages.length - boundedOffset - boundedLimit);
1208
+ const endIndex = sortedMessages.length - boundedOffset;
1192
1209
  const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
1193
- const hasMore = startIndex > 0;
1210
+ const hasMore = total > boundedOffset + boundedLimit;
1194
1211
  return {
1195
1212
  messages: paginatedMessages,
1196
1213
  total,
1197
1214
  hasMore,
1198
- offset,
1199
- limit
1215
+ offset: boundedOffset,
1216
+ limit: boundedLimit
1200
1217
  };
1201
1218
  }
1202
1219
  catch (error) {
1203
1220
  console.error(`Error reading messages for session ${sessionId}:`, error);
1204
- return limit === null ? [] : { messages: [], total: 0, hasMore: false };
1221
+ return boundedLimit === null ? [] : { messages: [], total: 0, hasMore: false };
1205
1222
  }
1206
1223
  }
1207
1224
  // Rename a project's display name
@@ -1514,20 +1531,24 @@ async function findCodexJsonlFiles(dir, { maxFiles = CODEX_SESSION_INDEX_MAX_FIL
1514
1531
  const currentDir = pendingDirs.pop();
1515
1532
  visitedDirs += 1;
1516
1533
  try {
1517
- const entries = await fs.readdir(currentDir, { withFileTypes: true });
1518
- entries.sort((a, b) => b.name.localeCompare(a.name));
1519
- for (const entry of entries) {
1520
- if (files.length >= maxFiles)
1521
- break;
1522
- const fullPath = path.join(currentDir, entry.name);
1523
- if (entry.isDirectory()) {
1524
- if (visitedDirs + pendingDirs.length < maxDirs) {
1525
- pendingDirs.push(fullPath);
1534
+ const dirHandle = await fs.opendir(currentDir);
1535
+ try {
1536
+ for await (const entry of dirHandle) {
1537
+ if (files.length >= maxFiles)
1538
+ break;
1539
+ const fullPath = path.join(currentDir, entry.name);
1540
+ if (entry.isDirectory()) {
1541
+ if (visitedDirs + pendingDirs.length < maxDirs) {
1542
+ pendingDirs.push(fullPath);
1543
+ }
1544
+ }
1545
+ else if (entry.name.endsWith('.jsonl')) {
1546
+ files.push(fullPath);
1526
1547
  }
1527
1548
  }
1528
- else if (entry.name.endsWith('.jsonl')) {
1529
- files.push(fullPath);
1530
- }
1549
+ }
1550
+ finally {
1551
+ await dirHandle.close().catch(() => undefined);
1531
1552
  }
1532
1553
  }
1533
1554
  catch {
@@ -1686,35 +1707,29 @@ async function parseCodexSessionFile(filePath) {
1686
1707
  }
1687
1708
  // Get messages for a specific Codex session
1688
1709
  async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1710
+ const boundedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
1711
+ const boundedOffset = Math.max(0, Number(offset) || 0);
1712
+ const maxBufferedMessages = boundedLimit === null ? Infinity : boundedLimit + boundedOffset;
1689
1713
  try {
1690
1714
  const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1691
- // Find the session file by searching for the session ID
1692
- const findSessionFile = async (dir) => {
1693
- try {
1694
- const entries = await fs.readdir(dir, { withFileTypes: true });
1695
- for (const entry of entries) {
1696
- const fullPath = path.join(dir, entry.name);
1697
- if (entry.isDirectory()) {
1698
- const found = await findSessionFile(fullPath);
1699
- if (found)
1700
- return found;
1701
- }
1702
- else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
1703
- return fullPath;
1704
- }
1705
- }
1706
- }
1707
- catch (error) {
1708
- // Skip directories we can't read
1709
- }
1710
- return null;
1711
- };
1712
- const sessionFilePath = await findSessionFile(codexSessionsDir);
1715
+ const sessionFiles = await findCodexJsonlFiles(codexSessionsDir, {
1716
+ maxFiles: Math.max(CODEX_SESSION_INDEX_MAX_FILES, 10000),
1717
+ maxDirs: CODEX_SESSION_INDEX_MAX_DIRS,
1718
+ });
1719
+ const sessionFilePath = sessionFiles.find(filePath => path.basename(filePath).includes(sessionId)) || null;
1713
1720
  if (!sessionFilePath) {
1714
1721
  console.warn(`Codex session file not found for session ${sessionId}`);
1715
1722
  return { messages: [], total: 0, hasMore: false };
1716
1723
  }
1717
1724
  const messages = [];
1725
+ let totalMessages = 0;
1726
+ const pushMessage = (message) => {
1727
+ totalMessages += 1;
1728
+ messages.push(message);
1729
+ if (messages.length > maxBufferedMessages) {
1730
+ messages.shift();
1731
+ }
1732
+ };
1718
1733
  let tokenUsage = null;
1719
1734
  const fileStream = fsSync.createReadStream(sessionFilePath);
1720
1735
  const rl = readline.createInterface({
@@ -1741,6 +1756,9 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1741
1756
  for await (const line of rl) {
1742
1757
  if (line.trim()) {
1743
1758
  try {
1759
+ if (line.length > CLAUDE_JSONL_LINE_MAX_CHARS) {
1760
+ continue;
1761
+ }
1744
1762
  const entry = JSON.parse(line);
1745
1763
  // Extract token usage from token_count events (keep latest)
1746
1764
  if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
@@ -1754,7 +1772,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1754
1772
  }
1755
1773
  // Use event_msg.user_message for user-visible inputs.
1756
1774
  if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
1757
- messages.push({
1775
+ pushMessage({
1758
1776
  type: 'user',
1759
1777
  timestamp: entry.timestamp,
1760
1778
  message: {
@@ -1772,7 +1790,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1772
1790
  const textContent = extractText(content);
1773
1791
  // Only add if there's actual content
1774
1792
  if (textContent?.trim()) {
1775
- messages.push({
1793
+ pushMessage({
1776
1794
  type: 'assistant',
1777
1795
  timestamp: entry.timestamp,
1778
1796
  message: {
@@ -1788,7 +1806,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1788
1806
  .filter(Boolean)
1789
1807
  .join('\n');
1790
1808
  if (summaryText?.trim()) {
1791
- messages.push({
1809
+ pushMessage({
1792
1810
  type: 'thinking',
1793
1811
  timestamp: entry.timestamp,
1794
1812
  message: {
@@ -1812,7 +1830,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1812
1830
  // Keep original if parsing fails
1813
1831
  }
1814
1832
  }
1815
- messages.push({
1833
+ pushMessage({
1816
1834
  type: 'tool_use',
1817
1835
  timestamp: entry.timestamp,
1818
1836
  toolName: toolName,
@@ -1821,7 +1839,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1821
1839
  });
1822
1840
  }
1823
1841
  if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
1824
- messages.push({
1842
+ pushMessage({
1825
1843
  type: 'tool_result',
1826
1844
  timestamp: entry.timestamp,
1827
1845
  toolCallId: entry.payload.call_id,
@@ -1847,7 +1865,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1847
1865
  newLines.push(line.substring(1));
1848
1866
  }
1849
1867
  }
1850
- messages.push({
1868
+ pushMessage({
1851
1869
  type: 'tool_use',
1852
1870
  timestamp: entry.timestamp,
1853
1871
  toolName: 'Edit',
@@ -1860,7 +1878,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1860
1878
  });
1861
1879
  }
1862
1880
  else {
1863
- messages.push({
1881
+ pushMessage({
1864
1882
  type: 'tool_use',
1865
1883
  timestamp: entry.timestamp,
1866
1884
  toolName: toolName,
@@ -1870,7 +1888,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1870
1888
  }
1871
1889
  }
1872
1890
  if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
1873
- messages.push({
1891
+ pushMessage({
1874
1892
  type: 'tool_result',
1875
1893
  timestamp: entry.timestamp,
1876
1894
  toolCallId: entry.payload.call_id,
@@ -1885,19 +1903,19 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1885
1903
  }
1886
1904
  // Sort by timestamp
1887
1905
  messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
1888
- const total = messages.length;
1906
+ const total = boundedLimit === null ? messages.length : totalMessages;
1889
1907
  // Apply pagination if limit is specified
1890
- if (limit !== null) {
1891
- const startIndex = Math.max(0, total - offset - limit);
1892
- const endIndex = total - offset;
1908
+ if (boundedLimit !== null) {
1909
+ const startIndex = Math.max(0, messages.length - boundedOffset - boundedLimit);
1910
+ const endIndex = messages.length - boundedOffset;
1893
1911
  const paginatedMessages = messages.slice(startIndex, endIndex);
1894
- const hasMore = startIndex > 0;
1912
+ const hasMore = total > boundedOffset + boundedLimit;
1895
1913
  return {
1896
1914
  messages: paginatedMessages,
1897
1915
  total,
1898
1916
  hasMore,
1899
- offset,
1900
- limit,
1917
+ offset: boundedOffset,
1918
+ limit: boundedLimit,
1901
1919
  tokenUsage
1902
1920
  };
1903
1921
  }