@pixelbyte-software/pixcode 1.53.10 → 1.53.12

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.
@@ -65,6 +65,15 @@ import Database from 'better-sqlite3';
65
65
  import sessionManager from './sessionManager.js';
66
66
  import { applyCustomSessionNames } from './database/db.js';
67
67
  const FILE_COUNT_LIMIT = 20000;
68
+ const FILE_COUNT_CACHE_TTL_MS = 5 * 60 * 1000;
69
+ const FILE_COUNT_CACHE_MAX_ENTRIES = 300;
70
+ const FILE_COUNT_SCAN_MAX_MS = 2500;
71
+ const CODEX_SESSION_INDEX_TTL_MS = 60 * 1000;
72
+ const CODEX_SESSION_INDEX_MAX_FILES = 1200;
73
+ const CODEX_SESSION_INDEX_MAX_DIRS = 2500;
74
+ const GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS = 60 * 1000;
75
+ const CLI_CHAT_FILE_READ_MAX_BYTES = 2 * 1024 * 1024;
76
+ const CLI_CHAT_FILES_PER_PROJECT_LIMIT = 150;
68
77
  const FILE_COUNT_IGNORED_DIRECTORIES = new Set([
69
78
  '.cache',
70
79
  '.git',
@@ -84,10 +93,43 @@ const FILE_COUNT_IGNORED_DIRECTORIES = new Set([
84
93
  'target',
85
94
  'vendor'
86
95
  ]);
96
+ const fileCountCache = new Map();
97
+ let codexSessionsIndexCache = null;
98
+ let geminiCliProjectEntriesCache = null;
99
+ let qwenCliProjectEntriesCache = null;
100
+ function pruneMapToMaxEntries(map, maxEntries) {
101
+ while (map.size > maxEntries) {
102
+ const firstKey = map.keys().next().value;
103
+ if (firstKey === undefined)
104
+ break;
105
+ map.delete(firstKey);
106
+ }
107
+ }
108
+ function readFreshCacheEntry(cache, key, now = Date.now()) {
109
+ const entry = cache.get(key);
110
+ if (!entry || entry.expiresAt <= now) {
111
+ if (entry)
112
+ cache.delete(key);
113
+ return undefined;
114
+ }
115
+ return entry.value;
116
+ }
117
+ function writeCacheEntry(cache, key, value, ttlMs, maxEntries) {
118
+ cache.set(key, {
119
+ value,
120
+ expiresAt: Date.now() + ttlMs,
121
+ });
122
+ pruneMapToMaxEntries(cache, maxEntries);
123
+ }
87
124
  async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
88
125
  if (!projectPath || typeof projectPath !== 'string') {
89
126
  return undefined;
90
127
  }
128
+ const cacheKey = `${path.resolve(projectPath)}:${maxFiles}`;
129
+ const cached = readFreshCacheEntry(fileCountCache, cacheKey);
130
+ if (cached !== undefined) {
131
+ return cached;
132
+ }
91
133
  try {
92
134
  const stats = await fs.stat(projectPath);
93
135
  if (!stats.isDirectory()) {
@@ -98,8 +140,13 @@ async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
98
140
  return undefined;
99
141
  }
100
142
  let count = 0;
143
+ const startedAt = Date.now();
101
144
  const pendingDirectories = [projectPath];
102
145
  while (pendingDirectories.length > 0) {
146
+ if (Date.now() - startedAt > FILE_COUNT_SCAN_MAX_MS) {
147
+ writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
148
+ return count;
149
+ }
103
150
  const currentDirectory = pendingDirectories.pop();
104
151
  let entries = [];
105
152
  try {
@@ -118,11 +165,13 @@ async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
118
165
  if (entry.isFile() || entry.isSymbolicLink()) {
119
166
  count += 1;
120
167
  if (count >= maxFiles) {
168
+ writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
121
169
  return count;
122
170
  }
123
171
  }
124
172
  }
125
173
  }
174
+ writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
126
175
  return count;
127
176
  }
128
177
  // Cache for extracted project directories
@@ -186,6 +235,9 @@ function areStringArraysEqual(first, second) {
186
235
  // Gemini ones rather than sharing a parametric implementation, keeping
187
236
  // provider-specific logic discoverable in one place.
188
237
  async function listQwenCliProjectEntries() {
238
+ if (qwenCliProjectEntriesCache?.expiresAt > Date.now()) {
239
+ return qwenCliProjectEntriesCache.entries;
240
+ }
189
241
  const qwenTmpDir = path.join(os.homedir(), '.qwen', 'tmp');
190
242
  try {
191
243
  await fs.access(qwenTmpDir);
@@ -219,9 +271,16 @@ async function listQwenCliProjectEntries() {
219
271
  normalizedProjectRoot,
220
272
  });
221
273
  }
274
+ qwenCliProjectEntriesCache = {
275
+ entries,
276
+ expiresAt: Date.now() + GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS,
277
+ };
222
278
  return entries;
223
279
  }
224
280
  async function listGeminiCliProjectEntries() {
281
+ if (geminiCliProjectEntriesCache?.expiresAt > Date.now()) {
282
+ return geminiCliProjectEntriesCache.entries;
283
+ }
225
284
  const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
226
285
  try {
227
286
  await fs.access(geminiTmpDir);
@@ -256,8 +315,19 @@ async function listGeminiCliProjectEntries() {
256
315
  normalizedProjectRoot,
257
316
  });
258
317
  }
318
+ geminiCliProjectEntriesCache = {
319
+ entries,
320
+ expiresAt: Date.now() + GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS,
321
+ };
259
322
  return entries;
260
323
  }
324
+ async function readJsonTextFileLimited(filePath, maxBytes = CLI_CHAT_FILE_READ_MAX_BYTES) {
325
+ const stat = await fs.stat(filePath);
326
+ if (!stat.isFile() || stat.size > maxBytes) {
327
+ return null;
328
+ }
329
+ return fs.readFile(filePath, 'utf8');
330
+ }
261
331
  function addDiscoveredProject(discoveredProjectsByPath, projectPath, provider) {
262
332
  if (typeof projectPath !== 'string' || projectPath.trim().length === 0) {
263
333
  return;
@@ -1436,26 +1506,40 @@ function normalizeComparablePath(inputPath) {
1436
1506
  const resolved = path.resolve(normalized);
1437
1507
  return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1438
1508
  }
1439
- async function findCodexJsonlFiles(dir) {
1509
+ async function findCodexJsonlFiles(dir, { maxFiles = CODEX_SESSION_INDEX_MAX_FILES, maxDirs = CODEX_SESSION_INDEX_MAX_DIRS, } = {}) {
1440
1510
  const files = [];
1441
- try {
1442
- const entries = await fs.readdir(dir, { withFileTypes: true });
1443
- for (const entry of entries) {
1444
- const fullPath = path.join(dir, entry.name);
1445
- if (entry.isDirectory()) {
1446
- files.push(...await findCodexJsonlFiles(fullPath));
1447
- }
1448
- else if (entry.name.endsWith('.jsonl')) {
1449
- files.push(fullPath);
1511
+ const pendingDirs = [dir];
1512
+ let visitedDirs = 0;
1513
+ while (pendingDirs.length > 0 && files.length < maxFiles && visitedDirs < maxDirs) {
1514
+ const currentDir = pendingDirs.pop();
1515
+ visitedDirs += 1;
1516
+ 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);
1526
+ }
1527
+ }
1528
+ else if (entry.name.endsWith('.jsonl')) {
1529
+ files.push(fullPath);
1530
+ }
1450
1531
  }
1451
1532
  }
1452
- }
1453
- catch (error) {
1454
- // Skip directories we can't read
1533
+ catch {
1534
+ // Skip directories we can't read
1535
+ }
1455
1536
  }
1456
1537
  return files;
1457
1538
  }
1458
1539
  async function buildCodexSessionsIndex() {
1540
+ if (codexSessionsIndexCache?.expiresAt > Date.now()) {
1541
+ return codexSessionsIndexCache.sessionsByProject;
1542
+ }
1459
1543
  const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1460
1544
  const sessionsByProject = new Map();
1461
1545
  try {
@@ -1497,6 +1581,10 @@ async function buildCodexSessionsIndex() {
1497
1581
  for (const sessions of sessionsByProject.values()) {
1498
1582
  sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1499
1583
  }
1584
+ codexSessionsIndexCache = {
1585
+ sessionsByProject,
1586
+ expiresAt: Date.now() + CODEX_SESSION_INDEX_TTL_MS,
1587
+ };
1500
1588
  return sessionsByProject;
1501
1589
  }
1502
1590
  // Fetch Codex sessions for a given project path
@@ -1823,24 +1911,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1823
1911
  async function deleteCodexSession(sessionId) {
1824
1912
  try {
1825
1913
  const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1826
- const findJsonlFiles = async (dir) => {
1827
- const files = [];
1828
- try {
1829
- const entries = await fs.readdir(dir, { withFileTypes: true });
1830
- for (const entry of entries) {
1831
- const fullPath = path.join(dir, entry.name);
1832
- if (entry.isDirectory()) {
1833
- files.push(...await findJsonlFiles(fullPath));
1834
- }
1835
- else if (entry.name.endsWith('.jsonl')) {
1836
- files.push(fullPath);
1837
- }
1838
- }
1839
- }
1840
- catch (error) { }
1841
- return files;
1842
- };
1843
- const jsonlFiles = await findJsonlFiles(codexSessionsDir);
1914
+ const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
1844
1915
  for (const filePath of jsonlFiles) {
1845
1916
  const sessionData = await parseCodexSessionFile(filePath);
1846
1917
  if (sessionData && sessionData.id === sessionId) {
@@ -2305,14 +2376,16 @@ async function searchGeminiSessionsForProject(projectPath, projectResult, allWor
2305
2376
  catch {
2306
2377
  continue;
2307
2378
  }
2308
- for (const chatFile of chatFiles) {
2379
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2309
2380
  if (getTotalMatches() >= limit || isAborted())
2310
2381
  break;
2311
2382
  if (!chatFile.endsWith('.json'))
2312
2383
  continue;
2313
2384
  try {
2314
2385
  const filePath = path.join(chatsDir, chatFile);
2315
- const data = await fs.readFile(filePath, 'utf8');
2386
+ const data = await readJsonTextFileLimited(filePath);
2387
+ if (!data)
2388
+ continue;
2316
2389
  const session = JSON.parse(data);
2317
2390
  if (!session.messages || !Array.isArray(session.messages))
2318
2391
  continue;
@@ -2401,12 +2474,14 @@ async function getQwenCliSessions(projectPath, options = {}) {
2401
2474
  catch {
2402
2475
  continue;
2403
2476
  }
2404
- for (const chatFile of chatFiles) {
2477
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2405
2478
  if (!chatFile.endsWith('.json'))
2406
2479
  continue;
2407
2480
  try {
2408
2481
  const filePath = path.join(chatsDir, chatFile);
2409
- const data = await fs.readFile(filePath, 'utf8');
2482
+ const data = await readJsonTextFileLimited(filePath);
2483
+ if (!data)
2484
+ continue;
2410
2485
  const session = JSON.parse(data);
2411
2486
  if (!session.messages || !Array.isArray(session.messages))
2412
2487
  continue;
@@ -2480,11 +2555,13 @@ async function getOpencodeCliSessions(projectPath) {
2480
2555
  catch {
2481
2556
  continue;
2482
2557
  }
2483
- for (const file of sessionFiles) {
2558
+ for (const file of sessionFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2484
2559
  if (!file.startsWith('ses_') || !file.endsWith('.json'))
2485
2560
  continue;
2486
2561
  try {
2487
- const data = await fs.readFile(path.join(dir, file), 'utf8');
2562
+ const data = await readJsonTextFileLimited(path.join(dir, file));
2563
+ if (!data)
2564
+ continue;
2488
2565
  const sess = JSON.parse(data);
2489
2566
  const sessDir = typeof sess?.directory === 'string'
2490
2567
  ? normalizeComparablePath(sess.directory)
@@ -2547,12 +2624,14 @@ async function getQwenCliSessionMessages(sessionId) {
2547
2624
  catch {
2548
2625
  continue;
2549
2626
  }
2550
- for (const chatFile of chatFiles) {
2627
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2551
2628
  if (!chatFile.endsWith('.json'))
2552
2629
  continue;
2553
2630
  try {
2554
2631
  const filePath = path.join(chatsDir, chatFile);
2555
- const data = await fs.readFile(filePath, 'utf8');
2632
+ const data = await readJsonTextFileLimited(filePath);
2633
+ if (!data)
2634
+ continue;
2556
2635
  const session = JSON.parse(data);
2557
2636
  const fileSessionId = session.sessionId || chatFile.replace('.json', '');
2558
2637
  if (fileSessionId !== sessionId)
@@ -2605,12 +2684,14 @@ async function getGeminiCliSessions(projectPath, options = {}) {
2605
2684
  catch {
2606
2685
  continue;
2607
2686
  }
2608
- for (const chatFile of chatFiles) {
2687
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2609
2688
  if (!chatFile.endsWith('.json'))
2610
2689
  continue;
2611
2690
  try {
2612
2691
  const filePath = path.join(chatsDir, chatFile);
2613
- const data = await fs.readFile(filePath, 'utf8');
2692
+ const data = await readJsonTextFileLimited(filePath);
2693
+ if (!data)
2694
+ continue;
2614
2695
  const session = JSON.parse(data);
2615
2696
  if (!session.messages || !Array.isArray(session.messages))
2616
2697
  continue;
@@ -2658,12 +2739,14 @@ async function getGeminiCliSessionMessages(sessionId) {
2658
2739
  catch {
2659
2740
  continue;
2660
2741
  }
2661
- for (const chatFile of chatFiles) {
2742
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2662
2743
  if (!chatFile.endsWith('.json'))
2663
2744
  continue;
2664
2745
  try {
2665
2746
  const filePath = path.join(chatsDir, chatFile);
2666
- const data = await fs.readFile(filePath, 'utf8');
2747
+ const data = await readJsonTextFileLimited(filePath);
2748
+ if (!data)
2749
+ continue;
2667
2750
  const session = JSON.parse(data);
2668
2751
  const fileSessionId = session.sessionId || chatFile.replace('.json', '');
2669
2752
  if (fileSessionId !== sessionId)