@siteboon/claude-code-ui 1.20.1 → 1.22.0

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,133 +65,134 @@ import crypto from 'crypto';
65
65
  import sqlite3 from 'sqlite3';
66
66
  import { open } from 'sqlite';
67
67
  import os from 'os';
68
+ import sessionManager from './sessionManager.js';
68
69
 
69
70
  // Import TaskMaster detection functions
70
71
  async function detectTaskMasterFolder(projectPath) {
72
+ try {
73
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
74
+
75
+ // Check if .taskmaster directory exists
71
76
  try {
72
- const taskMasterPath = path.join(projectPath, '.taskmaster');
73
-
74
- // Check if .taskmaster directory exists
75
- try {
76
- const stats = await fs.stat(taskMasterPath);
77
- if (!stats.isDirectory()) {
78
- return {
79
- hasTaskmaster: false,
80
- reason: '.taskmaster exists but is not a directory'
81
- };
82
- }
83
- } catch (error) {
84
- if (error.code === 'ENOENT') {
85
- return {
86
- hasTaskmaster: false,
87
- reason: '.taskmaster directory not found'
88
- };
89
- }
90
- throw error;
91
- }
77
+ const stats = await fs.stat(taskMasterPath);
78
+ if (!stats.isDirectory()) {
79
+ return {
80
+ hasTaskmaster: false,
81
+ reason: '.taskmaster exists but is not a directory'
82
+ };
83
+ }
84
+ } catch (error) {
85
+ if (error.code === 'ENOENT') {
86
+ return {
87
+ hasTaskmaster: false,
88
+ reason: '.taskmaster directory not found'
89
+ };
90
+ }
91
+ throw error;
92
+ }
92
93
 
93
- // Check for key TaskMaster files
94
- const keyFiles = [
95
- 'tasks/tasks.json',
96
- 'config.json'
97
- ];
98
-
99
- const fileStatus = {};
100
- let hasEssentialFiles = true;
101
-
102
- for (const file of keyFiles) {
103
- const filePath = path.join(taskMasterPath, file);
104
- try {
105
- await fs.access(filePath);
106
- fileStatus[file] = true;
107
- } catch (error) {
108
- fileStatus[file] = false;
109
- if (file === 'tasks/tasks.json') {
110
- hasEssentialFiles = false;
111
- }
112
- }
113
- }
94
+ // Check for key TaskMaster files
95
+ const keyFiles = [
96
+ 'tasks/tasks.json',
97
+ 'config.json'
98
+ ];
114
99
 
115
- // Parse tasks.json if it exists for metadata
116
- let taskMetadata = null;
117
- if (fileStatus['tasks/tasks.json']) {
118
- try {
119
- const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
120
- const tasksContent = await fs.readFile(tasksPath, 'utf8');
121
- const tasksData = JSON.parse(tasksContent);
122
-
123
- // Handle both tagged and legacy formats
124
- let tasks = [];
125
- if (tasksData.tasks) {
126
- // Legacy format
127
- tasks = tasksData.tasks;
128
- } else {
129
- // Tagged format - get tasks from all tags
130
- Object.values(tasksData).forEach(tagData => {
131
- if (tagData.tasks) {
132
- tasks = tasks.concat(tagData.tasks);
133
- }
134
- });
135
- }
100
+ const fileStatus = {};
101
+ let hasEssentialFiles = true;
136
102
 
137
- // Calculate task statistics
138
- const stats = tasks.reduce((acc, task) => {
139
- acc.total++;
140
- acc[task.status] = (acc[task.status] || 0) + 1;
141
-
142
- // Count subtasks
143
- if (task.subtasks) {
144
- task.subtasks.forEach(subtask => {
145
- acc.subtotalTasks++;
146
- acc.subtasks = acc.subtasks || {};
147
- acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
148
- });
149
- }
150
-
151
- return acc;
152
- }, {
153
- total: 0,
154
- subtotalTasks: 0,
155
- pending: 0,
156
- 'in-progress': 0,
157
- done: 0,
158
- review: 0,
159
- deferred: 0,
160
- cancelled: 0,
161
- subtasks: {}
162
- });
103
+ for (const file of keyFiles) {
104
+ const filePath = path.join(taskMasterPath, file);
105
+ try {
106
+ await fs.access(filePath);
107
+ fileStatus[file] = true;
108
+ } catch (error) {
109
+ fileStatus[file] = false;
110
+ if (file === 'tasks/tasks.json') {
111
+ hasEssentialFiles = false;
112
+ }
113
+ }
114
+ }
163
115
 
164
- taskMetadata = {
165
- taskCount: stats.total,
166
- subtaskCount: stats.subtotalTasks,
167
- completed: stats.done || 0,
168
- pending: stats.pending || 0,
169
- inProgress: stats['in-progress'] || 0,
170
- review: stats.review || 0,
171
- completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
172
- lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
173
- };
174
- } catch (parseError) {
175
- console.warn('Failed to parse tasks.json:', parseError.message);
176
- taskMetadata = { error: 'Failed to parse tasks.json' };
116
+ // Parse tasks.json if it exists for metadata
117
+ let taskMetadata = null;
118
+ if (fileStatus['tasks/tasks.json']) {
119
+ try {
120
+ const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
121
+ const tasksContent = await fs.readFile(tasksPath, 'utf8');
122
+ const tasksData = JSON.parse(tasksContent);
123
+
124
+ // Handle both tagged and legacy formats
125
+ let tasks = [];
126
+ if (tasksData.tasks) {
127
+ // Legacy format
128
+ tasks = tasksData.tasks;
129
+ } else {
130
+ // Tagged format - get tasks from all tags
131
+ Object.values(tasksData).forEach(tagData => {
132
+ if (tagData.tasks) {
133
+ tasks = tasks.concat(tagData.tasks);
177
134
  }
135
+ });
178
136
  }
179
137
 
180
- return {
181
- hasTaskmaster: true,
182
- hasEssentialFiles,
183
- files: fileStatus,
184
- metadata: taskMetadata,
185
- path: taskMasterPath
186
- };
138
+ // Calculate task statistics
139
+ const stats = tasks.reduce((acc, task) => {
140
+ acc.total++;
141
+ acc[task.status] = (acc[task.status] || 0) + 1;
142
+
143
+ // Count subtasks
144
+ if (task.subtasks) {
145
+ task.subtasks.forEach(subtask => {
146
+ acc.subtotalTasks++;
147
+ acc.subtasks = acc.subtasks || {};
148
+ acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
149
+ });
150
+ }
187
151
 
188
- } catch (error) {
189
- console.error('Error detecting TaskMaster folder:', error);
190
- return {
191
- hasTaskmaster: false,
192
- reason: `Error checking directory: ${error.message}`
152
+ return acc;
153
+ }, {
154
+ total: 0,
155
+ subtotalTasks: 0,
156
+ pending: 0,
157
+ 'in-progress': 0,
158
+ done: 0,
159
+ review: 0,
160
+ deferred: 0,
161
+ cancelled: 0,
162
+ subtasks: {}
163
+ });
164
+
165
+ taskMetadata = {
166
+ taskCount: stats.total,
167
+ subtaskCount: stats.subtotalTasks,
168
+ completed: stats.done || 0,
169
+ pending: stats.pending || 0,
170
+ inProgress: stats['in-progress'] || 0,
171
+ review: stats.review || 0,
172
+ completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
173
+ lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
193
174
  };
175
+ } catch (parseError) {
176
+ console.warn('Failed to parse tasks.json:', parseError.message);
177
+ taskMetadata = { error: 'Failed to parse tasks.json' };
178
+ }
194
179
  }
180
+
181
+ return {
182
+ hasTaskmaster: true,
183
+ hasEssentialFiles,
184
+ files: fileStatus,
185
+ metadata: taskMetadata,
186
+ path: taskMasterPath
187
+ };
188
+
189
+ } catch (error) {
190
+ console.error('Error detecting TaskMaster folder:', error);
191
+ return {
192
+ hasTaskmaster: false,
193
+ reason: `Error checking directory: ${error.message}`
194
+ };
195
+ }
195
196
  }
196
197
 
197
198
  // Cache for extracted project directories
@@ -218,7 +219,7 @@ async function loadProjectConfig() {
218
219
  async function saveProjectConfig(config) {
219
220
  const claudeDir = path.join(os.homedir(), '.claude');
220
221
  const configPath = path.join(claudeDir, 'project-config.json');
221
-
222
+
222
223
  // Ensure the .claude directory exists
223
224
  try {
224
225
  await fs.mkdir(claudeDir, { recursive: true });
@@ -227,7 +228,7 @@ async function saveProjectConfig(config) {
227
228
  throw error;
228
229
  }
229
230
  }
230
-
231
+
231
232
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
232
233
  }
233
234
 
@@ -235,13 +236,13 @@ async function saveProjectConfig(config) {
235
236
  async function generateDisplayName(projectName, actualProjectDir = null) {
236
237
  // Use actual project directory if provided, otherwise decode from project name
237
238
  let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
238
-
239
+
239
240
  // Try to read package.json from the project path
240
241
  try {
241
242
  const packageJsonPath = path.join(projectPath, 'package.json');
242
243
  const packageData = await fs.readFile(packageJsonPath, 'utf8');
243
244
  const packageJson = JSON.parse(packageData);
244
-
245
+
245
246
  // Return the name from package.json if it exists
246
247
  if (packageJson.name) {
247
248
  return packageJson.name;
@@ -249,14 +250,14 @@ async function generateDisplayName(projectName, actualProjectDir = null) {
249
250
  } catch (error) {
250
251
  // Fall back to path-based naming if package.json doesn't exist or can't be read
251
252
  }
252
-
253
+
253
254
  // If it starts with /, it's an absolute path
254
255
  if (projectPath.startsWith('/')) {
255
256
  const parts = projectPath.split('/').filter(Boolean);
256
257
  // Return only the last folder name
257
258
  return parts[parts.length - 1] || projectPath;
258
259
  }
259
-
260
+
260
261
  return projectPath;
261
262
  }
262
263
 
@@ -281,14 +282,14 @@ async function extractProjectDirectory(projectName) {
281
282
  let latestTimestamp = 0;
282
283
  let latestCwd = null;
283
284
  let extractedPath;
284
-
285
+
285
286
  try {
286
287
  // Check if the project directory exists
287
288
  await fs.access(projectDir);
288
-
289
+
289
290
  const files = await fs.readdir(projectDir);
290
291
  const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
291
-
292
+
292
293
  if (jsonlFiles.length === 0) {
293
294
  // Fall back to decoded project name if no sessions
294
295
  extractedPath = projectName.replace(/-/g, '/');
@@ -301,16 +302,16 @@ async function extractProjectDirectory(projectName) {
301
302
  input: fileStream,
302
303
  crlfDelay: Infinity
303
304
  });
304
-
305
+
305
306
  for await (const line of rl) {
306
307
  if (line.trim()) {
307
308
  try {
308
309
  const entry = JSON.parse(line);
309
-
310
+
310
311
  if (entry.cwd) {
311
312
  // Count occurrences of each cwd
312
313
  cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
313
-
314
+
314
315
  // Track the most recent cwd
315
316
  const timestamp = new Date(entry.timestamp || 0).getTime();
316
317
  if (timestamp > latestTimestamp) {
@@ -324,7 +325,7 @@ async function extractProjectDirectory(projectName) {
324
325
  }
325
326
  }
326
327
  }
327
-
328
+
328
329
  // Determine the best cwd to use
329
330
  if (cwdCounts.size === 0) {
330
331
  // No cwd found, fall back to decoded project name
@@ -336,7 +337,7 @@ async function extractProjectDirectory(projectName) {
336
337
  // Multiple cwd values - prefer the most recent one if it has reasonable usage
337
338
  const mostRecentCount = cwdCounts.get(latestCwd) || 0;
338
339
  const maxCount = Math.max(...cwdCounts.values());
339
-
340
+
340
341
  // Use most recent if it has at least 25% of the max count
341
342
  if (mostRecentCount >= maxCount * 0.25) {
342
343
  extractedPath = latestCwd;
@@ -349,19 +350,19 @@ async function extractProjectDirectory(projectName) {
349
350
  }
350
351
  }
351
352
  }
352
-
353
+
353
354
  // Fallback (shouldn't reach here)
354
355
  if (!extractedPath) {
355
356
  extractedPath = latestCwd || projectName.replace(/-/g, '/');
356
357
  }
357
358
  }
358
359
  }
359
-
360
+
360
361
  // Cache the result
361
362
  projectDirectoryCache.set(projectName, extractedPath);
362
-
363
+
363
364
  return extractedPath;
364
-
365
+
365
366
  } catch (error) {
366
367
  // If the directory doesn't exist, just use the decoded project name
367
368
  if (error.code === 'ENOENT') {
@@ -371,10 +372,10 @@ async function extractProjectDirectory(projectName) {
371
372
  // Fall back to decoded project name for other errors
372
373
  extractedPath = projectName.replace(/-/g, '/');
373
374
  }
374
-
375
+
375
376
  // Cache the fallback result too
376
377
  projectDirectoryCache.set(projectName, extractedPath);
377
-
378
+
378
379
  return extractedPath;
379
380
  }
380
381
  }
@@ -408,91 +409,100 @@ async function getProjects(progressCallback = null) {
408
409
  totalProjects = directories.length + manualProjectsCount;
409
410
 
410
411
  for (const entry of directories) {
411
- processedProjects++;
412
-
413
- // Emit progress
414
- if (progressCallback) {
415
- progressCallback({
416
- phase: 'loading',
417
- current: processedProjects,
418
- total: totalProjects,
419
- currentProject: entry.name
420
- });
412
+ processedProjects++;
413
+
414
+ // Emit progress
415
+ if (progressCallback) {
416
+ progressCallback({
417
+ phase: 'loading',
418
+ current: processedProjects,
419
+ total: totalProjects,
420
+ currentProject: entry.name
421
+ });
422
+ }
423
+
424
+ // Extract actual project directory from JSONL sessions
425
+ const actualProjectDir = await extractProjectDirectory(entry.name);
426
+
427
+ // Get display name from config or generate one
428
+ const customName = config[entry.name]?.displayName;
429
+ const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
430
+ const fullPath = actualProjectDir;
431
+
432
+ const project = {
433
+ name: entry.name,
434
+ path: actualProjectDir,
435
+ displayName: customName || autoDisplayName,
436
+ fullPath: fullPath,
437
+ isCustomName: !!customName,
438
+ sessions: [],
439
+ geminiSessions: [],
440
+ sessionMeta: {
441
+ hasMore: false,
442
+ total: 0
421
443
  }
444
+ };
422
445
 
423
- // Extract actual project directory from JSONL sessions
424
- const actualProjectDir = await extractProjectDirectory(entry.name);
425
-
426
- // Get display name from config or generate one
427
- const customName = config[entry.name]?.displayName;
428
- const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
429
- const fullPath = actualProjectDir;
430
-
431
- const project = {
432
- name: entry.name,
433
- path: actualProjectDir,
434
- displayName: customName || autoDisplayName,
435
- fullPath: fullPath,
436
- isCustomName: !!customName,
437
- sessions: [],
438
- sessionMeta: {
439
- hasMore: false,
440
- total: 0
441
- }
446
+ // Try to get sessions for this project (just first 5 for performance)
447
+ try {
448
+ const sessionResult = await getSessions(entry.name, 5, 0);
449
+ project.sessions = sessionResult.sessions || [];
450
+ project.sessionMeta = {
451
+ hasMore: sessionResult.hasMore,
452
+ total: sessionResult.total
442
453
  };
443
-
444
- // Try to get sessions for this project (just first 5 for performance)
445
- try {
446
- const sessionResult = await getSessions(entry.name, 5, 0);
447
- project.sessions = sessionResult.sessions || [];
448
- project.sessionMeta = {
449
- hasMore: sessionResult.hasMore,
450
- total: sessionResult.total
451
- };
452
- } catch (e) {
453
- console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
454
- project.sessionMeta = {
455
- hasMore: false,
456
- total: 0
457
- };
458
- }
459
-
460
- // Also fetch Cursor sessions for this project
461
- try {
462
- project.cursorSessions = await getCursorSessions(actualProjectDir);
463
- } catch (e) {
464
- console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
465
- project.cursorSessions = [];
466
- }
454
+ } catch (e) {
455
+ console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
456
+ project.sessionMeta = {
457
+ hasMore: false,
458
+ total: 0
459
+ };
460
+ }
467
461
 
468
- // Also fetch Codex sessions for this project
469
- try {
470
- project.codexSessions = await getCodexSessions(actualProjectDir, {
471
- indexRef: codexSessionsIndexRef,
472
- });
473
- } catch (e) {
474
- console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
475
- project.codexSessions = [];
476
- }
462
+ // Also fetch Cursor sessions for this project
463
+ try {
464
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
465
+ } catch (e) {
466
+ console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
467
+ project.cursorSessions = [];
468
+ }
477
469
 
478
- // Add TaskMaster detection
479
- try {
480
- const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
481
- project.taskmaster = {
482
- hasTaskmaster: taskMasterResult.hasTaskmaster,
483
- hasEssentialFiles: taskMasterResult.hasEssentialFiles,
484
- metadata: taskMasterResult.metadata,
485
- status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
486
- };
487
- } catch (e) {
488
- console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
489
- project.taskmaster = {
490
- hasTaskmaster: false,
491
- hasEssentialFiles: false,
492
- metadata: null,
493
- status: 'error'
494
- };
495
- }
470
+ // Also fetch Codex sessions for this project
471
+ try {
472
+ project.codexSessions = await getCodexSessions(actualProjectDir, {
473
+ indexRef: codexSessionsIndexRef,
474
+ });
475
+ } catch (e) {
476
+ console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
477
+ project.codexSessions = [];
478
+ }
479
+
480
+ // Also fetch Gemini sessions for this project
481
+ try {
482
+ project.geminiSessions = sessionManager.getProjectSessions(actualProjectDir) || [];
483
+ } catch (e) {
484
+ console.warn(`Could not load Gemini sessions for project ${entry.name}:`, e.message);
485
+ project.geminiSessions = [];
486
+ }
487
+
488
+ // Add TaskMaster detection
489
+ try {
490
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
491
+ project.taskmaster = {
492
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
493
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
494
+ metadata: taskMasterResult.metadata,
495
+ status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
496
+ };
497
+ } catch (e) {
498
+ console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
499
+ project.taskmaster = {
500
+ hasTaskmaster: false,
501
+ hasEssentialFiles: false,
502
+ metadata: null,
503
+ status: 'error'
504
+ };
505
+ }
496
506
 
497
507
  projects.push(project);
498
508
  }
@@ -506,7 +516,7 @@ async function getProjects(progressCallback = null) {
506
516
  .filter(([name, cfg]) => cfg.manuallyAdded)
507
517
  .length;
508
518
  }
509
-
519
+
510
520
  // Add manually configured projects that don't exist as folders yet
511
521
  for (const [projectName, projectConfig] of Object.entries(config)) {
512
522
  if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
@@ -524,7 +534,7 @@ async function getProjects(progressCallback = null) {
524
534
 
525
535
  // Use the original path if available, otherwise extract from potential sessions
526
536
  let actualProjectDir = projectConfig.originalPath;
527
-
537
+
528
538
  if (!actualProjectDir) {
529
539
  try {
530
540
  actualProjectDir = await extractProjectDirectory(projectName);
@@ -533,21 +543,22 @@ async function getProjects(progressCallback = null) {
533
543
  actualProjectDir = projectName.replace(/-/g, '/');
534
544
  }
535
545
  }
536
-
546
+
537
547
  const project = {
538
- name: projectName,
539
- path: actualProjectDir,
540
- displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
541
- fullPath: actualProjectDir,
542
- isCustomName: !!projectConfig.displayName,
543
- isManuallyAdded: true,
544
- sessions: [],
545
- sessionMeta: {
546
- hasMore: false,
547
- total: 0
548
- },
549
- cursorSessions: [],
550
- codexSessions: []
548
+ name: projectName,
549
+ path: actualProjectDir,
550
+ displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
551
+ fullPath: actualProjectDir,
552
+ isCustomName: !!projectConfig.displayName,
553
+ isManuallyAdded: true,
554
+ sessions: [],
555
+ geminiSessions: [],
556
+ sessionMeta: {
557
+ hasMore: false,
558
+ total: 0
559
+ },
560
+ cursorSessions: [],
561
+ codexSessions: []
551
562
  };
552
563
 
553
564
  // Try to fetch Cursor sessions for manual projects too
@@ -566,16 +577,23 @@ async function getProjects(progressCallback = null) {
566
577
  console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
567
578
  }
568
579
 
580
+ // Try to fetch Gemini sessions for manual projects too
581
+ try {
582
+ project.geminiSessions = sessionManager.getProjectSessions(actualProjectDir) || [];
583
+ } catch (e) {
584
+ console.warn(`Could not load Gemini sessions for manual project ${projectName}:`, e.message);
585
+ }
586
+
569
587
  // Add TaskMaster detection for manual projects
570
588
  try {
571
589
  const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
572
-
590
+
573
591
  // Determine TaskMaster status
574
592
  let taskMasterStatus = 'not-configured';
575
593
  if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
576
594
  taskMasterStatus = 'taskmaster-only'; // We don't check MCP for manual projects in bulk
577
595
  }
578
-
596
+
579
597
  project.taskmaster = {
580
598
  status: taskMasterStatus,
581
599
  hasTaskmaster: taskMasterResult.hasTaskmaster,
@@ -591,7 +609,7 @@ async function getProjects(progressCallback = null) {
591
609
  error: error.message
592
610
  };
593
611
  }
594
-
612
+
595
613
  projects.push(project);
596
614
  }
597
615
  }
@@ -616,11 +634,11 @@ async function getSessions(projectName, limit = 5, offset = 0) {
616
634
  // agent-*.jsonl files contain session start data at this point. This needs to be revisited
617
635
  // periodically to make sure only accurate data is there and no new functionality is added there
618
636
  const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
619
-
637
+
620
638
  if (jsonlFiles.length === 0) {
621
639
  return { sessions: [], hasMore: false, total: 0 };
622
640
  }
623
-
641
+
624
642
  // Sort files by modification time (newest first)
625
643
  const filesWithStats = await Promise.all(
626
644
  jsonlFiles.map(async (file) => {
@@ -630,37 +648,37 @@ async function getSessions(projectName, limit = 5, offset = 0) {
630
648
  })
631
649
  );
632
650
  filesWithStats.sort((a, b) => b.mtime - a.mtime);
633
-
651
+
634
652
  const allSessions = new Map();
635
653
  const allEntries = [];
636
654
  const uuidToSessionMap = new Map();
637
-
655
+
638
656
  // Collect all sessions and entries from all files
639
657
  for (const { file } of filesWithStats) {
640
658
  const jsonlFile = path.join(projectDir, file);
641
659
  const result = await parseJsonlSessions(jsonlFile);
642
-
660
+
643
661
  result.sessions.forEach(session => {
644
662
  if (!allSessions.has(session.id)) {
645
663
  allSessions.set(session.id, session);
646
664
  }
647
665
  });
648
-
666
+
649
667
  allEntries.push(...result.entries);
650
-
668
+
651
669
  // Early exit optimization for large projects
652
670
  if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
653
671
  break;
654
672
  }
655
673
  }
656
-
674
+
657
675
  // Build UUID-to-session mapping for timeline detection
658
676
  allEntries.forEach(entry => {
659
677
  if (entry.uuid && entry.sessionId) {
660
678
  uuidToSessionMap.set(entry.uuid, entry.sessionId);
661
679
  }
662
680
  });
663
-
681
+
664
682
  // Group sessions by first user message ID
665
683
  const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
666
684
  const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
@@ -722,7 +740,7 @@ async function getSessions(projectName, limit = 5, offset = 0) {
722
740
  const total = visibleSessions.length;
723
741
  const paginatedSessions = visibleSessions.slice(offset, offset + limit);
724
742
  const hasMore = offset + limit < total;
725
-
743
+
726
744
  return {
727
745
  sessions: paginatedSessions,
728
746
  hasMore,
@@ -926,8 +944,8 @@ async function parseAgentTools(filePath) {
926
944
  if (tool) {
927
945
  tool.toolResult = {
928
946
  content: typeof part.content === 'string' ? part.content :
929
- Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
930
- JSON.stringify(part.content),
947
+ Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
948
+ JSON.stringify(part.content),
931
949
  isError: Boolean(part.is_error)
932
950
  };
933
951
  }
@@ -1015,7 +1033,6 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1015
1033
  }
1016
1034
  }
1017
1035
  }
1018
-
1019
1036
  // Sort messages by timestamp
1020
1037
  const sortedMessages = messages.sort((a, b) =>
1021
1038
  new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
@@ -1051,7 +1068,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1051
1068
  // Rename a project's display name
1052
1069
  async function renameProject(projectName, newDisplayName) {
1053
1070
  const config = await loadProjectConfig();
1054
-
1071
+
1055
1072
  if (!newDisplayName || newDisplayName.trim() === '') {
1056
1073
  // Remove custom name if empty, will fall back to auto-generated
1057
1074
  delete config[projectName];
@@ -1061,7 +1078,7 @@ async function renameProject(projectName, newDisplayName) {
1061
1078
  displayName: newDisplayName.trim()
1062
1079
  };
1063
1080
  }
1064
-
1081
+
1065
1082
  await saveProjectConfig(config);
1066
1083
  return true;
1067
1084
  }
@@ -1069,21 +1086,21 @@ async function renameProject(projectName, newDisplayName) {
1069
1086
  // Delete a session from a project
1070
1087
  async function deleteSession(projectName, sessionId) {
1071
1088
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1072
-
1089
+
1073
1090
  try {
1074
1091
  const files = await fs.readdir(projectDir);
1075
1092
  const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
1076
-
1093
+
1077
1094
  if (jsonlFiles.length === 0) {
1078
1095
  throw new Error('No session files found for this project');
1079
1096
  }
1080
-
1097
+
1081
1098
  // Check all JSONL files to find which one contains the session
1082
1099
  for (const file of jsonlFiles) {
1083
1100
  const jsonlFile = path.join(projectDir, file);
1084
1101
  const content = await fs.readFile(jsonlFile, 'utf8');
1085
1102
  const lines = content.split('\n').filter(line => line.trim());
1086
-
1103
+
1087
1104
  // Check if this file contains the session
1088
1105
  const hasSession = lines.some(line => {
1089
1106
  try {
@@ -1093,7 +1110,7 @@ async function deleteSession(projectName, sessionId) {
1093
1110
  return false;
1094
1111
  }
1095
1112
  });
1096
-
1113
+
1097
1114
  if (hasSession) {
1098
1115
  // Filter out all entries for this session
1099
1116
  const filteredLines = lines.filter(line => {
@@ -1104,13 +1121,13 @@ async function deleteSession(projectName, sessionId) {
1104
1121
  return true; // Keep malformed lines
1105
1122
  }
1106
1123
  });
1107
-
1124
+
1108
1125
  // Write back the filtered content
1109
1126
  await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
1110
1127
  return true;
1111
1128
  }
1112
1129
  }
1113
-
1130
+
1114
1131
  throw new Error(`Session ${sessionId} not found in any files`);
1115
1132
  } catch (error) {
1116
1133
  console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
@@ -1220,10 +1237,10 @@ async function addProjectManually(projectPath, displayName = null) {
1220
1237
  if (displayName) {
1221
1238
  config[projectName].displayName = displayName;
1222
1239
  }
1223
-
1240
+
1224
1241
  await saveProjectConfig(config);
1225
-
1226
-
1242
+
1243
+
1227
1244
  return {
1228
1245
  name: projectName,
1229
1246
  path: absolutePath,
@@ -1241,7 +1258,7 @@ async function getCursorSessions(projectPath) {
1241
1258
  // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
1242
1259
  const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
1243
1260
  const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
1244
-
1261
+
1245
1262
  // Check if the directory exists
1246
1263
  try {
1247
1264
  await fs.access(cursorChatsPath);
@@ -1249,25 +1266,25 @@ async function getCursorSessions(projectPath) {
1249
1266
  // No sessions for this project
1250
1267
  return [];
1251
1268
  }
1252
-
1269
+
1253
1270
  // List all session directories
1254
1271
  const sessionDirs = await fs.readdir(cursorChatsPath);
1255
1272
  const sessions = [];
1256
-
1273
+
1257
1274
  for (const sessionId of sessionDirs) {
1258
1275
  const sessionPath = path.join(cursorChatsPath, sessionId);
1259
1276
  const storeDbPath = path.join(sessionPath, 'store.db');
1260
-
1277
+
1261
1278
  try {
1262
1279
  // Check if store.db exists
1263
1280
  await fs.access(storeDbPath);
1264
-
1281
+
1265
1282
  // Capture store.db mtime as a reliable fallback timestamp
1266
1283
  let dbStatMtimeMs = null;
1267
1284
  try {
1268
1285
  const stat = await fs.stat(storeDbPath);
1269
1286
  dbStatMtimeMs = stat.mtimeMs;
1270
- } catch (_) {}
1287
+ } catch (_) { }
1271
1288
 
1272
1289
  // Open SQLite database
1273
1290
  const db = await open({
@@ -1275,12 +1292,12 @@ async function getCursorSessions(projectPath) {
1275
1292
  driver: sqlite3.Database,
1276
1293
  mode: sqlite3.OPEN_READONLY
1277
1294
  });
1278
-
1295
+
1279
1296
  // Get metadata from meta table
1280
1297
  const metaRows = await db.all(`
1281
1298
  SELECT key, value FROM meta
1282
1299
  `);
1283
-
1300
+
1284
1301
  // Parse metadata
1285
1302
  let metadata = {};
1286
1303
  for (const row of metaRows) {
@@ -1299,17 +1316,17 @@ async function getCursorSessions(projectPath) {
1299
1316
  }
1300
1317
  }
1301
1318
  }
1302
-
1319
+
1303
1320
  // Get message count
1304
1321
  const messageCountResult = await db.get(`
1305
1322
  SELECT COUNT(*) as count FROM blobs
1306
1323
  `);
1307
-
1324
+
1308
1325
  await db.close();
1309
-
1326
+
1310
1327
  // Extract session info
1311
1328
  const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
1312
-
1329
+
1313
1330
  // Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
1314
1331
  let createdAt = null;
1315
1332
  if (metadata.createdAt) {
@@ -1319,7 +1336,7 @@ async function getCursorSessions(projectPath) {
1319
1336
  } else {
1320
1337
  createdAt = new Date().toISOString();
1321
1338
  }
1322
-
1339
+
1323
1340
  sessions.push({
1324
1341
  id: sessionId,
1325
1342
  name: sessionName,
@@ -1328,18 +1345,18 @@ async function getCursorSessions(projectPath) {
1328
1345
  messageCount: messageCountResult.count || 0,
1329
1346
  projectPath: projectPath
1330
1347
  });
1331
-
1348
+
1332
1349
  } catch (error) {
1333
1350
  console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
1334
1351
  }
1335
1352
  }
1336
-
1353
+
1337
1354
  // Sort sessions by creation time (newest first)
1338
1355
  sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
1339
-
1356
+
1340
1357
  // Return only the first 5 sessions for performance
1341
1358
  return sessions.slice(0, 5);
1342
-
1359
+
1343
1360
  } catch (error) {
1344
1361
  console.error('Error fetching Cursor sessions:', error);
1345
1362
  return [];
@@ -1785,7 +1802,7 @@ async function deleteCodexSession(sessionId) {
1785
1802
  files.push(fullPath);
1786
1803
  }
1787
1804
  }
1788
- } catch (error) {}
1805
+ } catch (error) { }
1789
1806
  return files;
1790
1807
  };
1791
1808