@epiphytic/claudecodeui 1.0.0 → 1.1.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.
@@ -1,197 +1,198 @@
1
1
  /**
2
2
  * PROJECT DISCOVERY AND MANAGEMENT SYSTEM
3
3
  * ========================================
4
- *
4
+ *
5
5
  * This module manages project discovery for both Claude CLI and Cursor CLI sessions.
6
- *
6
+ *
7
7
  * ## Architecture Overview
8
- *
8
+ *
9
9
  * 1. **Claude Projects** (stored in ~/.claude/projects/)
10
10
  * - Each project is a directory named with the project path encoded (/ replaced with -)
11
11
  * - Contains .jsonl files with conversation history including 'cwd' field
12
12
  * - Project metadata stored in ~/.claude/project-config.json
13
- *
13
+ *
14
14
  * 2. **Cursor Projects** (stored in ~/.cursor/chats/)
15
15
  * - Each project directory is named with MD5 hash of the absolute project path
16
16
  * - Example: /Users/john/myproject -> MD5 -> a1b2c3d4e5f6...
17
17
  * - Contains session directories with SQLite databases (store.db)
18
18
  * - Project path is NOT stored in the database - only in the MD5 hash
19
- *
19
+ *
20
20
  * ## Project Discovery Strategy
21
- *
21
+ *
22
22
  * 1. **Claude Projects Discovery**:
23
23
  * - Scan ~/.claude/projects/ directory for Claude project folders
24
24
  * - Extract actual project path from .jsonl files (cwd field)
25
25
  * - Fall back to decoded directory name if no sessions exist
26
- *
26
+ *
27
27
  * 2. **Cursor Sessions Discovery**:
28
28
  * - For each KNOWN project (from Claude or manually added)
29
29
  * - Compute MD5 hash of the project's absolute path
30
30
  * - Check if ~/.cursor/chats/{md5_hash}/ directory exists
31
31
  * - Read session metadata from SQLite store.db files
32
- *
32
+ *
33
33
  * 3. **Manual Project Addition**:
34
34
  * - Users can manually add project paths via UI
35
35
  * - Stored in ~/.claude/project-config.json with 'manuallyAdded' flag
36
36
  * - Allows discovering Cursor sessions for projects without Claude sessions
37
- *
37
+ *
38
38
  * ## Critical Limitations
39
- *
39
+ *
40
40
  * - **CANNOT discover Cursor-only projects**: From a quick check, there was no mention of
41
41
  * the cwd of each project. if someone has the time, you can try to reverse engineer it.
42
- *
42
+ *
43
43
  * - **Project relocation breaks history**: If a project directory is moved or renamed,
44
44
  * the MD5 hash changes, making old Cursor sessions inaccessible unless the old
45
45
  * path is known and manually added.
46
- *
46
+ *
47
47
  * ## Error Handling
48
- *
48
+ *
49
49
  * - Missing ~/.claude directory is handled gracefully with automatic creation
50
50
  * - ENOENT errors are caught and handled without crashing
51
51
  * - Empty arrays returned when no projects/sessions exist
52
- *
52
+ *
53
53
  * ## Caching Strategy
54
- *
54
+ *
55
55
  * - Project directory extraction is cached to minimize file I/O
56
56
  * - Cache is cleared when project configuration changes
57
57
  * - Session data is fetched on-demand, not cached
58
58
  */
59
59
 
60
- import { promises as fs } from 'fs';
61
- import fsSync from 'fs';
62
- import path from 'path';
63
- import readline from 'readline';
64
- import crypto from 'crypto';
65
- import sqlite3 from 'sqlite3';
66
- import { open } from 'sqlite';
67
- import os from 'os';
60
+ import { promises as fs } from "fs";
61
+ import fsSync from "fs";
62
+ import path from "path";
63
+ import readline from "readline";
64
+ import crypto from "crypto";
65
+ import sqlite3 from "sqlite3";
66
+ import { open } from "sqlite";
67
+ import os from "os";
68
68
 
69
69
  // Import TaskMaster detection functions
70
70
  async function detectTaskMasterFolder(projectPath) {
71
+ try {
72
+ const taskMasterPath = path.join(projectPath, ".taskmaster");
73
+
74
+ // Check if .taskmaster directory exists
71
75
  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
- }
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
+ }
92
92
 
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
- }
93
+ // Check for key TaskMaster files
94
+ const keyFiles = ["tasks/tasks.json", "config.json"];
114
95
 
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
- }
96
+ const fileStatus = {};
97
+ let hasEssentialFiles = true;
136
98
 
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
- });
163
-
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' };
99
+ for (const file of keyFiles) {
100
+ const filePath = path.join(taskMasterPath, file);
101
+ try {
102
+ await fs.access(filePath);
103
+ fileStatus[file] = true;
104
+ } catch (error) {
105
+ fileStatus[file] = false;
106
+ if (file === "tasks/tasks.json") {
107
+ hasEssentialFiles = false;
108
+ }
109
+ }
110
+ }
111
+
112
+ // Parse tasks.json if it exists for metadata
113
+ let taskMetadata = null;
114
+ if (fileStatus["tasks/tasks.json"]) {
115
+ try {
116
+ const tasksPath = path.join(taskMasterPath, "tasks/tasks.json");
117
+ const tasksContent = await fs.readFile(tasksPath, "utf8");
118
+ const tasksData = JSON.parse(tasksContent);
119
+
120
+ // Handle both tagged and legacy formats
121
+ let tasks = [];
122
+ if (tasksData.tasks) {
123
+ // Legacy format
124
+ tasks = tasksData.tasks;
125
+ } else {
126
+ // Tagged format - get tasks from all tags
127
+ Object.values(tasksData).forEach((tagData) => {
128
+ if (tagData.tasks) {
129
+ tasks = tasks.concat(tagData.tasks);
177
130
  }
131
+ });
178
132
  }
179
133
 
180
- return {
181
- hasTaskmaster: true,
182
- hasEssentialFiles,
183
- files: fileStatus,
184
- metadata: taskMetadata,
185
- path: taskMasterPath
186
- };
134
+ // Calculate task statistics
135
+ const stats = tasks.reduce(
136
+ (acc, task) => {
137
+ acc.total++;
138
+ acc[task.status] = (acc[task.status] || 0) + 1;
139
+
140
+ // Count subtasks
141
+ if (task.subtasks) {
142
+ task.subtasks.forEach((subtask) => {
143
+ acc.subtotalTasks++;
144
+ acc.subtasks = acc.subtasks || {};
145
+ acc.subtasks[subtask.status] =
146
+ (acc.subtasks[subtask.status] || 0) + 1;
147
+ });
148
+ }
187
149
 
188
- } catch (error) {
189
- console.error('Error detecting TaskMaster folder:', error);
190
- return {
191
- hasTaskmaster: false,
192
- reason: `Error checking directory: ${error.message}`
150
+ return acc;
151
+ },
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
+ },
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:
173
+ stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
174
+ lastModified: (await fs.stat(tasksPath)).mtime.toISOString(),
193
175
  };
176
+ } catch (parseError) {
177
+ console.warn("Failed to parse tasks.json:", parseError.message);
178
+ taskMetadata = { error: "Failed to parse tasks.json" };
179
+ }
194
180
  }
181
+
182
+ return {
183
+ hasTaskmaster: true,
184
+ hasEssentialFiles,
185
+ files: fileStatus,
186
+ metadata: taskMetadata,
187
+ path: taskMasterPath,
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
@@ -204,9 +205,9 @@ function clearProjectDirectoryCache() {
204
205
 
205
206
  // Load project configuration file
206
207
  async function loadProjectConfig() {
207
- const configPath = path.join(os.homedir(), '.claude', 'project-config.json');
208
+ const configPath = path.join(os.homedir(), ".claude", "project-config.json");
208
209
  try {
209
- const configData = await fs.readFile(configPath, 'utf8');
210
+ const configData = await fs.readFile(configPath, "utf8");
210
211
  return JSON.parse(configData);
211
212
  } catch (error) {
212
213
  // Return empty config if file doesn't exist
@@ -216,32 +217,32 @@ async function loadProjectConfig() {
216
217
 
217
218
  // Save project configuration file
218
219
  async function saveProjectConfig(config) {
219
- const claudeDir = path.join(os.homedir(), '.claude');
220
- const configPath = path.join(claudeDir, 'project-config.json');
221
-
220
+ const claudeDir = path.join(os.homedir(), ".claude");
221
+ const configPath = path.join(claudeDir, "project-config.json");
222
+
222
223
  // Ensure the .claude directory exists
223
224
  try {
224
225
  await fs.mkdir(claudeDir, { recursive: true });
225
226
  } catch (error) {
226
- if (error.code !== 'EEXIST') {
227
+ if (error.code !== "EEXIST") {
227
228
  throw error;
228
229
  }
229
230
  }
230
-
231
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
231
+
232
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
232
233
  }
233
234
 
234
235
  // Generate better display name from path
235
236
  async function generateDisplayName(projectName, actualProjectDir = null) {
236
237
  // Use actual project directory if provided, otherwise decode from project name
237
- let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
238
-
238
+ let projectPath = actualProjectDir || projectName.replace(/-/g, "/");
239
+
239
240
  // Try to read package.json from the project path
240
241
  try {
241
- const packageJsonPath = path.join(projectPath, 'package.json');
242
- const packageData = await fs.readFile(packageJsonPath, 'utf8');
242
+ const packageJsonPath = path.join(projectPath, "package.json");
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
- if (projectPath.startsWith('/')) {
255
- const parts = projectPath.split('/').filter(Boolean);
255
+ if (projectPath.startsWith("/")) {
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
 
@@ -276,22 +277,27 @@ async function extractProjectDirectory(projectName) {
276
277
  return originalPath;
277
278
  }
278
279
 
279
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
280
+ const projectDir = path.join(
281
+ os.homedir(),
282
+ ".claude",
283
+ "projects",
284
+ projectName,
285
+ );
280
286
  const cwdCounts = new Map();
281
287
  let latestTimestamp = 0;
282
288
  let latestCwd = null;
283
289
  let extractedPath;
284
-
290
+
285
291
  try {
286
292
  // Check if the project directory exists
287
293
  await fs.access(projectDir);
288
-
294
+
289
295
  const files = await fs.readdir(projectDir);
290
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
291
-
296
+ const jsonlFiles = files.filter((file) => file.endsWith(".jsonl"));
297
+
292
298
  if (jsonlFiles.length === 0) {
293
299
  // Fall back to decoded project name if no sessions
294
- extractedPath = projectName.replace(/-/g, '/');
300
+ extractedPath = projectName.replace(/-/g, "/");
295
301
  } else {
296
302
  // Process all JSONL files to collect cwd values
297
303
  for (const file of jsonlFiles) {
@@ -299,18 +305,18 @@ async function extractProjectDirectory(projectName) {
299
305
  const fileStream = fsSync.createReadStream(jsonlFile);
300
306
  const rl = readline.createInterface({
301
307
  input: fileStream,
302
- crlfDelay: Infinity
308
+ crlfDelay: Infinity,
303
309
  });
304
-
310
+
305
311
  for await (const line of rl) {
306
312
  if (line.trim()) {
307
313
  try {
308
314
  const entry = JSON.parse(line);
309
-
315
+
310
316
  if (entry.cwd) {
311
317
  // Count occurrences of each cwd
312
318
  cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
313
-
319
+
314
320
  // Track the most recent cwd
315
321
  const timestamp = new Date(entry.timestamp || 0).getTime();
316
322
  if (timestamp > latestTimestamp) {
@@ -324,11 +330,11 @@ async function extractProjectDirectory(projectName) {
324
330
  }
325
331
  }
326
332
  }
327
-
333
+
328
334
  // Determine the best cwd to use
329
335
  if (cwdCounts.size === 0) {
330
336
  // No cwd found, fall back to decoded project name
331
- extractedPath = projectName.replace(/-/g, '/');
337
+ extractedPath = projectName.replace(/-/g, "/");
332
338
  } else if (cwdCounts.size === 1) {
333
339
  // Only one cwd, use it
334
340
  extractedPath = Array.from(cwdCounts.keys())[0];
@@ -336,7 +342,7 @@ async function extractProjectDirectory(projectName) {
336
342
  // Multiple cwd values - prefer the most recent one if it has reasonable usage
337
343
  const mostRecentCount = cwdCounts.get(latestCwd) || 0;
338
344
  const maxCount = Math.max(...cwdCounts.values());
339
-
345
+
340
346
  // Use most recent if it has at least 25% of the max count
341
347
  if (mostRecentCount >= maxCount * 0.25) {
342
348
  extractedPath = latestCwd;
@@ -349,88 +355,99 @@ async function extractProjectDirectory(projectName) {
349
355
  }
350
356
  }
351
357
  }
352
-
358
+
353
359
  // Fallback (shouldn't reach here)
354
360
  if (!extractedPath) {
355
- extractedPath = latestCwd || projectName.replace(/-/g, '/');
361
+ extractedPath = latestCwd || projectName.replace(/-/g, "/");
356
362
  }
357
363
  }
358
364
  }
359
-
365
+
360
366
  // Cache the result
361
367
  projectDirectoryCache.set(projectName, extractedPath);
362
-
368
+
363
369
  return extractedPath;
364
-
365
370
  } catch (error) {
366
371
  // If the directory doesn't exist, just use the decoded project name
367
- if (error.code === 'ENOENT') {
368
- extractedPath = projectName.replace(/-/g, '/');
372
+ if (error.code === "ENOENT") {
373
+ extractedPath = projectName.replace(/-/g, "/");
369
374
  } else {
370
- console.error(`Error extracting project directory for ${projectName}:`, error);
375
+ console.error(
376
+ `Error extracting project directory for ${projectName}:`,
377
+ error,
378
+ );
371
379
  // Fall back to decoded project name for other errors
372
- extractedPath = projectName.replace(/-/g, '/');
380
+ extractedPath = projectName.replace(/-/g, "/");
373
381
  }
374
-
382
+
375
383
  // Cache the fallback result too
376
384
  projectDirectoryCache.set(projectName, extractedPath);
377
-
385
+
378
386
  return extractedPath;
379
387
  }
380
388
  }
381
389
 
382
390
  async function getProjects() {
383
- const claudeDir = path.join(os.homedir(), '.claude', 'projects');
391
+ const claudeDir = path.join(os.homedir(), ".claude", "projects");
384
392
  const config = await loadProjectConfig();
385
393
  const projects = [];
386
394
  const existingProjects = new Set();
387
-
395
+
388
396
  try {
389
397
  // Check if the .claude/projects directory exists
390
398
  await fs.access(claudeDir);
391
-
399
+
392
400
  // First, get existing Claude projects from the file system
393
401
  const entries = await fs.readdir(claudeDir, { withFileTypes: true });
394
-
402
+
395
403
  for (const entry of entries) {
396
404
  if (entry.isDirectory()) {
397
405
  existingProjects.add(entry.name);
398
406
  const projectPath = path.join(claudeDir, entry.name);
399
-
407
+
400
408
  // Extract actual project directory from JSONL sessions
401
409
  const actualProjectDir = await extractProjectDirectory(entry.name);
402
-
410
+
403
411
  // Get display name from config or generate one
404
412
  const customName = config[entry.name]?.displayName;
405
- const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
413
+ const autoDisplayName = await generateDisplayName(
414
+ entry.name,
415
+ actualProjectDir,
416
+ );
406
417
  const fullPath = actualProjectDir;
407
-
418
+
408
419
  const project = {
409
420
  name: entry.name,
410
421
  path: actualProjectDir,
411
422
  displayName: customName || autoDisplayName,
412
423
  fullPath: fullPath,
413
424
  isCustomName: !!customName,
414
- sessions: []
425
+ sessions: [],
415
426
  };
416
-
427
+
417
428
  // Try to get sessions for this project (just first 5 for performance)
418
429
  try {
419
430
  const sessionResult = await getSessions(entry.name, 5, 0);
420
431
  project.sessions = sessionResult.sessions || [];
421
432
  project.sessionMeta = {
422
433
  hasMore: sessionResult.hasMore,
423
- total: sessionResult.total
434
+ total: sessionResult.total,
424
435
  };
425
436
  } catch (e) {
426
- console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
437
+ console.warn(
438
+ `Could not load sessions for project ${entry.name}:`,
439
+ e.message,
440
+ );
427
441
  }
428
-
442
+
429
443
  // Also fetch Cursor sessions for this project
430
444
  try {
431
445
  project.cursorSessions = await getCursorSessions(actualProjectDir);
432
446
  } catch (e) {
433
- console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
447
+ console.warn(
448
+ `Could not load Cursor sessions for project ${entry.name}:`,
449
+ e.message,
450
+ );
434
451
  project.cursorSessions = [];
435
452
  }
436
453
 
@@ -438,173 +455,213 @@ async function getProjects() {
438
455
  try {
439
456
  project.codexSessions = await getCodexSessions(actualProjectDir);
440
457
  } catch (e) {
441
- console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
458
+ console.warn(
459
+ `Could not load Codex sessions for project ${entry.name}:`,
460
+ e.message,
461
+ );
442
462
  project.codexSessions = [];
443
463
  }
444
464
 
445
465
  // Add TaskMaster detection
446
466
  try {
447
- const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
467
+ const taskMasterResult =
468
+ await detectTaskMasterFolder(actualProjectDir);
448
469
  project.taskmaster = {
449
470
  hasTaskmaster: taskMasterResult.hasTaskmaster,
450
471
  hasEssentialFiles: taskMasterResult.hasEssentialFiles,
451
472
  metadata: taskMasterResult.metadata,
452
- status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
473
+ status:
474
+ taskMasterResult.hasTaskmaster &&
475
+ taskMasterResult.hasEssentialFiles
476
+ ? "configured"
477
+ : "not-configured",
453
478
  };
454
479
  } catch (e) {
455
- console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
480
+ console.warn(
481
+ `Could not detect TaskMaster for project ${entry.name}:`,
482
+ e.message,
483
+ );
456
484
  project.taskmaster = {
457
485
  hasTaskmaster: false,
458
486
  hasEssentialFiles: false,
459
487
  metadata: null,
460
- status: 'error'
488
+ status: "error",
461
489
  };
462
490
  }
463
-
491
+
464
492
  projects.push(project);
465
493
  }
466
494
  }
467
495
  } catch (error) {
468
496
  // If the directory doesn't exist (ENOENT), that's okay - just continue with empty projects
469
- if (error.code !== 'ENOENT') {
470
- console.error('Error reading projects directory:', error);
497
+ if (error.code !== "ENOENT") {
498
+ console.error("Error reading projects directory:", error);
471
499
  }
472
500
  }
473
-
501
+
474
502
  // Add manually configured projects that don't exist as folders yet
475
503
  for (const [projectName, projectConfig] of Object.entries(config)) {
476
504
  if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
477
505
  // Use the original path if available, otherwise extract from potential sessions
478
506
  let actualProjectDir = projectConfig.originalPath;
479
-
507
+
480
508
  if (!actualProjectDir) {
481
509
  try {
482
510
  actualProjectDir = await extractProjectDirectory(projectName);
483
511
  } catch (error) {
484
512
  // Fall back to decoded project name
485
- actualProjectDir = projectName.replace(/-/g, '/');
513
+ actualProjectDir = projectName.replace(/-/g, "/");
486
514
  }
487
515
  }
488
-
489
- const project = {
490
- name: projectName,
491
- path: actualProjectDir,
492
- displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
493
- fullPath: actualProjectDir,
494
- isCustomName: !!projectConfig.displayName,
495
- isManuallyAdded: true,
496
- sessions: [],
497
- cursorSessions: [],
498
- codexSessions: []
499
- };
516
+
517
+ const project = {
518
+ name: projectName,
519
+ path: actualProjectDir,
520
+ displayName:
521
+ projectConfig.displayName ||
522
+ (await generateDisplayName(projectName, actualProjectDir)),
523
+ fullPath: actualProjectDir,
524
+ isCustomName: !!projectConfig.displayName,
525
+ isManuallyAdded: true,
526
+ sessions: [],
527
+ cursorSessions: [],
528
+ codexSessions: [],
529
+ };
500
530
 
501
531
  // Try to fetch Cursor sessions for manual projects too
502
532
  try {
503
533
  project.cursorSessions = await getCursorSessions(actualProjectDir);
504
534
  } catch (e) {
505
- console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
535
+ console.warn(
536
+ `Could not load Cursor sessions for manual project ${projectName}:`,
537
+ e.message,
538
+ );
506
539
  }
507
540
 
508
541
  // Try to fetch Codex sessions for manual projects too
509
542
  try {
510
543
  project.codexSessions = await getCodexSessions(actualProjectDir);
511
544
  } catch (e) {
512
- console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
545
+ console.warn(
546
+ `Could not load Codex sessions for manual project ${projectName}:`,
547
+ e.message,
548
+ );
513
549
  }
514
550
 
515
551
  // Add TaskMaster detection for manual projects
516
552
  try {
517
553
  const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
518
-
554
+
519
555
  // Determine TaskMaster status
520
- let taskMasterStatus = 'not-configured';
521
- if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
522
- taskMasterStatus = 'taskmaster-only'; // We don't check MCP for manual projects in bulk
556
+ let taskMasterStatus = "not-configured";
557
+ if (
558
+ taskMasterResult.hasTaskmaster &&
559
+ taskMasterResult.hasEssentialFiles
560
+ ) {
561
+ taskMasterStatus = "taskmaster-only"; // We don't check MCP for manual projects in bulk
523
562
  }
524
-
563
+
525
564
  project.taskmaster = {
526
565
  status: taskMasterStatus,
527
566
  hasTaskmaster: taskMasterResult.hasTaskmaster,
528
567
  hasEssentialFiles: taskMasterResult.hasEssentialFiles,
529
- metadata: taskMasterResult.metadata
568
+ metadata: taskMasterResult.metadata,
530
569
  };
531
570
  } catch (error) {
532
- console.warn(`TaskMaster detection failed for manual project ${projectName}:`, error.message);
571
+ console.warn(
572
+ `TaskMaster detection failed for manual project ${projectName}:`,
573
+ error.message,
574
+ );
533
575
  project.taskmaster = {
534
- status: 'error',
576
+ status: "error",
535
577
  hasTaskmaster: false,
536
578
  hasEssentialFiles: false,
537
- error: error.message
579
+ error: error.message,
538
580
  };
539
581
  }
540
-
582
+
541
583
  projects.push(project);
542
584
  }
543
585
  }
544
-
586
+
545
587
  return projects;
546
588
  }
547
589
 
548
590
  async function getSessions(projectName, limit = 5, offset = 0) {
549
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
591
+ const projectDir = path.join(
592
+ os.homedir(),
593
+ ".claude",
594
+ "projects",
595
+ projectName,
596
+ );
550
597
 
551
598
  try {
552
599
  const files = await fs.readdir(projectDir);
553
600
  // agent-*.jsonl files contain session start data at this point. This needs to be revisited
554
601
  // periodically to make sure only accurate data is there and no new functionality is added there
555
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
556
-
602
+ const jsonlFiles = files.filter(
603
+ (file) => file.endsWith(".jsonl") && !file.startsWith("agent-"),
604
+ );
605
+
557
606
  if (jsonlFiles.length === 0) {
558
607
  return { sessions: [], hasMore: false, total: 0 };
559
608
  }
560
-
609
+
561
610
  // Sort files by modification time (newest first)
562
611
  const filesWithStats = await Promise.all(
563
612
  jsonlFiles.map(async (file) => {
564
613
  const filePath = path.join(projectDir, file);
565
614
  const stats = await fs.stat(filePath);
566
615
  return { file, mtime: stats.mtime };
567
- })
616
+ }),
568
617
  );
569
618
  filesWithStats.sort((a, b) => b.mtime - a.mtime);
570
-
619
+
571
620
  const allSessions = new Map();
572
621
  const allEntries = [];
573
622
  const uuidToSessionMap = new Map();
574
-
623
+
575
624
  // Collect all sessions and entries from all files
576
625
  for (const { file } of filesWithStats) {
577
626
  const jsonlFile = path.join(projectDir, file);
578
627
  const result = await parseJsonlSessions(jsonlFile);
579
-
580
- result.sessions.forEach(session => {
628
+
629
+ result.sessions.forEach((session) => {
581
630
  if (!allSessions.has(session.id)) {
582
631
  allSessions.set(session.id, session);
583
632
  }
584
633
  });
585
-
634
+
586
635
  allEntries.push(...result.entries);
587
-
636
+
588
637
  // Early exit optimization for large projects
589
- if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
638
+ if (
639
+ allSessions.size >= (limit + offset) * 2 &&
640
+ allEntries.length >= Math.min(3, filesWithStats.length)
641
+ ) {
590
642
  break;
591
643
  }
592
644
  }
593
-
645
+
594
646
  // Build UUID-to-session mapping for timeline detection
595
- allEntries.forEach(entry => {
647
+ allEntries.forEach((entry) => {
596
648
  if (entry.uuid && entry.sessionId) {
597
649
  uuidToSessionMap.set(entry.uuid, entry.sessionId);
598
650
  }
599
651
  });
600
-
652
+
601
653
  // Group sessions by first user message ID
602
654
  const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
603
655
  const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
604
656
 
605
657
  // Find the first user message for each session
606
- allEntries.forEach(entry => {
607
- if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
658
+ allEntries.forEach((entry) => {
659
+ if (
660
+ entry.sessionId &&
661
+ entry.type === "user" &&
662
+ entry.parentUuid === null &&
663
+ entry.uuid
664
+ ) {
608
665
  // This is a first user message in a session (parentUuid is null)
609
666
  const firstUserMsgId = entry.uuid;
610
667
 
@@ -616,14 +673,17 @@ async function getSessions(projectName, limit = 5, offset = 0) {
616
673
  if (!sessionGroups.has(firstUserMsgId)) {
617
674
  sessionGroups.set(firstUserMsgId, {
618
675
  latestSession: session,
619
- allSessions: [session]
676
+ allSessions: [session],
620
677
  });
621
678
  } else {
622
679
  const group = sessionGroups.get(firstUserMsgId);
623
680
  group.allSessions.push(session);
624
681
 
625
682
  // Update latest session if this one is more recent
626
- if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
683
+ if (
684
+ new Date(session.lastActivity) >
685
+ new Date(group.latestSession.lastActivity)
686
+ ) {
627
687
  group.latestSession = session;
628
688
  }
629
689
  }
@@ -634,38 +694,39 @@ async function getSessions(projectName, limit = 5, offset = 0) {
634
694
 
635
695
  // Collect all sessions that don't belong to any group (standalone sessions)
636
696
  const groupedSessionIds = new Set();
637
- sessionGroups.forEach(group => {
638
- group.allSessions.forEach(session => groupedSessionIds.add(session.id));
697
+ sessionGroups.forEach((group) => {
698
+ group.allSessions.forEach((session) => groupedSessionIds.add(session.id));
639
699
  });
640
700
 
641
- const standaloneSessionsArray = Array.from(allSessions.values())
642
- .filter(session => !groupedSessionIds.has(session.id));
701
+ const standaloneSessionsArray = Array.from(allSessions.values()).filter(
702
+ (session) => !groupedSessionIds.has(session.id),
703
+ );
643
704
 
644
705
  // Combine grouped sessions (only show latest from each group) + standalone sessions
645
- const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
706
+ const latestFromGroups = Array.from(sessionGroups.values()).map((group) => {
646
707
  const session = { ...group.latestSession };
647
708
  // Add metadata about grouping
648
709
  if (group.allSessions.length > 1) {
649
710
  session.isGrouped = true;
650
711
  session.groupSize = group.allSessions.length;
651
- session.groupSessions = group.allSessions.map(s => s.id);
712
+ session.groupSessions = group.allSessions.map((s) => s.id);
652
713
  }
653
714
  return session;
654
715
  });
655
716
  const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
656
- .filter(session => !session.summary.startsWith('{ "'))
717
+ .filter((session) => !session.summary.startsWith('{ "'))
657
718
  .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
658
719
 
659
720
  const total = visibleSessions.length;
660
721
  const paginatedSessions = visibleSessions.slice(offset, offset + limit);
661
722
  const hasMore = offset + limit < total;
662
-
723
+
663
724
  return {
664
725
  sessions: paginatedSessions,
665
726
  hasMore,
666
727
  total,
667
728
  offset,
668
- limit
729
+ limit,
669
730
  };
670
731
  } catch (error) {
671
732
  console.error(`Error reading sessions for project ${projectName}:`, error);
@@ -682,7 +743,7 @@ async function parseJsonlSessions(filePath) {
682
743
  const fileStream = fsSync.createReadStream(filePath);
683
744
  const rl = readline.createInterface({
684
745
  input: fileStream,
685
- crlfDelay: Infinity
746
+ crlfDelay: Infinity,
686
747
  });
687
748
 
688
749
  for await (const line of rl) {
@@ -692,7 +753,12 @@ async function parseJsonlSessions(filePath) {
692
753
  entries.push(entry);
693
754
 
694
755
  // Handle summary entries that don't have sessionId yet
695
- if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
756
+ if (
757
+ entry.type === "summary" &&
758
+ entry.summary &&
759
+ !entry.sessionId &&
760
+ entry.leafUuid
761
+ ) {
696
762
  pendingSummaries.set(entry.leafUuid, entry.summary);
697
763
  }
698
764
 
@@ -700,55 +766,74 @@ async function parseJsonlSessions(filePath) {
700
766
  if (!sessions.has(entry.sessionId)) {
701
767
  sessions.set(entry.sessionId, {
702
768
  id: entry.sessionId,
703
- summary: 'New Session',
769
+ summary: "New Session",
704
770
  messageCount: 0,
705
771
  lastActivity: new Date(),
706
- cwd: entry.cwd || '',
772
+ cwd: entry.cwd || "",
707
773
  lastUserMessage: null,
708
- lastAssistantMessage: null
774
+ lastAssistantMessage: null,
709
775
  });
710
776
  }
711
777
 
712
778
  const session = sessions.get(entry.sessionId);
713
779
 
714
780
  // Apply pending summary if this entry has a parentUuid that matches a pending summary
715
- if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
781
+ if (
782
+ session.summary === "New Session" &&
783
+ entry.parentUuid &&
784
+ pendingSummaries.has(entry.parentUuid)
785
+ ) {
716
786
  session.summary = pendingSummaries.get(entry.parentUuid);
717
787
  }
718
788
 
719
789
  // Update summary from summary entries with sessionId
720
- if (entry.type === 'summary' && entry.summary) {
790
+ if (entry.type === "summary" && entry.summary) {
721
791
  session.summary = entry.summary;
722
792
  }
723
793
 
724
794
  // Track last user and assistant messages (skip system messages)
725
- if (entry.message?.role === 'user' && entry.message?.content) {
795
+ if (entry.message?.role === "user" && entry.message?.content) {
726
796
  const content = entry.message.content;
727
797
 
728
798
  // Extract text from array format if needed
729
799
  let textContent = content;
730
- if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
800
+ if (
801
+ Array.isArray(content) &&
802
+ content.length > 0 &&
803
+ content[0].type === "text"
804
+ ) {
731
805
  textContent = content[0].text;
732
806
  }
733
807
 
734
- const isSystemMessage = typeof textContent === 'string' && (
735
- textContent.startsWith('<command-name>') ||
736
- textContent.startsWith('<command-message>') ||
737
- textContent.startsWith('<command-args>') ||
738
- textContent.startsWith('<local-command-stdout>') ||
739
- textContent.startsWith('<system-reminder>') ||
740
- textContent.startsWith('Caveat:') ||
741
- textContent.startsWith('This session is being continued from a previous') ||
742
- textContent.startsWith('Invalid API key') ||
743
- textContent.includes('{"subtasks":') || // Filter Task Master prompts
744
- textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
745
- textContent === 'Warmup' // Explicitly filter out "Warmup"
746
- );
747
-
748
- if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
808
+ const isSystemMessage =
809
+ typeof textContent === "string" &&
810
+ (textContent.startsWith("<command-name>") ||
811
+ textContent.startsWith("<command-message>") ||
812
+ textContent.startsWith("<command-args>") ||
813
+ textContent.startsWith("<local-command-stdout>") ||
814
+ textContent.startsWith("<system-reminder>") ||
815
+ textContent.startsWith("Caveat:") ||
816
+ textContent.startsWith(
817
+ "This session is being continued from a previous",
818
+ ) ||
819
+ textContent.startsWith("Invalid API key") ||
820
+ textContent.includes('{"subtasks":') || // Filter Task Master prompts
821
+ textContent.includes(
822
+ "CRITICAL: You MUST respond with ONLY a JSON",
823
+ ) || // Filter Task Master system prompts
824
+ textContent === "Warmup"); // Explicitly filter out "Warmup"
825
+
826
+ if (
827
+ typeof textContent === "string" &&
828
+ textContent.length > 0 &&
829
+ !isSystemMessage
830
+ ) {
749
831
  session.lastUserMessage = textContent;
750
832
  }
751
- } else if (entry.message?.role === 'assistant' && entry.message?.content) {
833
+ } else if (
834
+ entry.message?.role === "assistant" &&
835
+ entry.message?.content
836
+ ) {
752
837
  // Skip API error messages using the isApiErrorMessage flag
753
838
  if (entry.isApiErrorMessage === true) {
754
839
  // Skip this message entirely
@@ -758,20 +843,22 @@ async function parseJsonlSessions(filePath) {
758
843
 
759
844
  if (Array.isArray(entry.message.content)) {
760
845
  for (const part of entry.message.content) {
761
- if (part.type === 'text' && part.text) {
846
+ if (part.type === "text" && part.text) {
762
847
  assistantText = part.text;
763
848
  }
764
849
  }
765
- } else if (typeof entry.message.content === 'string') {
850
+ } else if (typeof entry.message.content === "string") {
766
851
  assistantText = entry.message.content;
767
852
  }
768
853
 
769
854
  // Additional filter for assistant messages with system content
770
- const isSystemAssistantMessage = typeof assistantText === 'string' && (
771
- assistantText.startsWith('Invalid API key') ||
772
- assistantText.includes('{"subtasks":') ||
773
- assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
774
- );
855
+ const isSystemAssistantMessage =
856
+ typeof assistantText === "string" &&
857
+ (assistantText.startsWith("Invalid API key") ||
858
+ assistantText.includes('{"subtasks":') ||
859
+ assistantText.includes(
860
+ "CRITICAL: You MUST respond with ONLY a JSON",
861
+ ));
775
862
 
776
863
  if (assistantText && !isSystemAssistantMessage) {
777
864
  session.lastAssistantMessage = assistantText;
@@ -793,64 +880,79 @@ async function parseJsonlSessions(filePath) {
793
880
 
794
881
  // After processing all entries, set final summary based on last message if no summary exists
795
882
  for (const session of sessions.values()) {
796
- if (session.summary === 'New Session') {
883
+ if (session.summary === "New Session") {
797
884
  // Prefer last user message, fall back to last assistant message
798
- const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
885
+ const lastMessage =
886
+ session.lastUserMessage || session.lastAssistantMessage;
799
887
  if (lastMessage) {
800
- session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
888
+ session.summary =
889
+ lastMessage.length > 50
890
+ ? lastMessage.substring(0, 50) + "..."
891
+ : lastMessage;
801
892
  }
802
893
  }
803
894
  }
804
895
 
805
896
  // Filter out sessions that contain JSON responses (Task Master errors)
806
897
  const allSessions = Array.from(sessions.values());
807
- const filteredSessions = allSessions.filter(session => {
898
+ const filteredSessions = allSessions.filter((session) => {
808
899
  const shouldFilter = session.summary.startsWith('{ "');
809
900
  if (shouldFilter) {
810
901
  }
811
902
  // Log a sample of summaries to debug
812
- if (Math.random() < 0.01) { // Log 1% of sessions
903
+ if (Math.random() < 0.01) {
904
+ // Log 1% of sessions
813
905
  }
814
906
  return !shouldFilter;
815
907
  });
816
908
 
817
-
818
909
  return {
819
910
  sessions: filteredSessions,
820
- entries: entries
911
+ entries: entries,
821
912
  };
822
-
823
913
  } catch (error) {
824
- console.error('Error reading JSONL file:', error);
914
+ console.error("Error reading JSONL file:", error);
825
915
  return { sessions: [], entries: [] };
826
916
  }
827
917
  }
828
918
 
829
919
  // Get messages for a specific session with pagination support
830
- async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
831
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
920
+ async function getSessionMessages(
921
+ projectName,
922
+ sessionId,
923
+ limit = null,
924
+ offset = 0,
925
+ ) {
926
+ const projectDir = path.join(
927
+ os.homedir(),
928
+ ".claude",
929
+ "projects",
930
+ projectName,
931
+ );
832
932
 
833
933
  try {
834
934
  const files = await fs.readdir(projectDir);
835
935
  // agent-*.jsonl files contain session start data at this point. This needs to be revisited
836
936
  // periodically to make sure only accurate data is there and no new functionality is added there
837
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
838
-
937
+ const jsonlFiles = files.filter(
938
+ (file) => file.endsWith(".jsonl") && !file.startsWith("agent-"),
939
+ );
940
+
839
941
  if (jsonlFiles.length === 0) {
840
942
  return { messages: [], total: 0, hasMore: false };
841
943
  }
842
-
944
+
843
945
  const messages = [];
844
-
946
+
845
947
  // Process all JSONL files to find messages for this session
846
948
  for (const file of jsonlFiles) {
847
949
  const jsonlFile = path.join(projectDir, file);
848
950
  const fileStream = fsSync.createReadStream(jsonlFile);
849
951
  const rl = readline.createInterface({
850
952
  input: fileStream,
851
- crlfDelay: Infinity
953
+ crlfDelay: Infinity,
852
954
  });
853
-
955
+
854
956
  for await (const line of rl) {
855
957
  if (line.trim()) {
856
958
  try {
@@ -859,37 +961,37 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
859
961
  messages.push(entry);
860
962
  }
861
963
  } catch (parseError) {
862
- console.warn('Error parsing line:', parseError.message);
964
+ console.warn("Error parsing line:", parseError.message);
863
965
  }
864
966
  }
865
967
  }
866
968
  }
867
-
969
+
868
970
  // Sort messages by timestamp
869
- const sortedMessages = messages.sort((a, b) =>
870
- new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
971
+ const sortedMessages = messages.sort(
972
+ (a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0),
871
973
  );
872
-
974
+
873
975
  const total = sortedMessages.length;
874
-
976
+
875
977
  // If no limit is specified, return all messages (backward compatibility)
876
978
  if (limit === null) {
877
979
  return sortedMessages;
878
980
  }
879
-
981
+
880
982
  // Apply pagination - for recent messages, we need to slice from the end
881
983
  // offset 0 should give us the most recent messages
882
984
  const startIndex = Math.max(0, total - offset - limit);
883
985
  const endIndex = total - offset;
884
986
  const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
885
987
  const hasMore = startIndex > 0;
886
-
988
+
887
989
  return {
888
990
  messages: paginatedMessages,
889
991
  total,
890
992
  hasMore,
891
993
  offset,
892
- limit
994
+ limit,
893
995
  };
894
996
  } catch (error) {
895
997
  console.error(`Error reading messages for session ${sessionId}:`, error);
@@ -900,41 +1002,46 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
900
1002
  // Rename a project's display name
901
1003
  async function renameProject(projectName, newDisplayName) {
902
1004
  const config = await loadProjectConfig();
903
-
904
- if (!newDisplayName || newDisplayName.trim() === '') {
1005
+
1006
+ if (!newDisplayName || newDisplayName.trim() === "") {
905
1007
  // Remove custom name if empty, will fall back to auto-generated
906
1008
  delete config[projectName];
907
1009
  } else {
908
1010
  // Set custom display name
909
1011
  config[projectName] = {
910
- displayName: newDisplayName.trim()
1012
+ displayName: newDisplayName.trim(),
911
1013
  };
912
1014
  }
913
-
1015
+
914
1016
  await saveProjectConfig(config);
915
1017
  return true;
916
1018
  }
917
1019
 
918
1020
  // Delete a session from a project
919
1021
  async function deleteSession(projectName, sessionId) {
920
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
921
-
1022
+ const projectDir = path.join(
1023
+ os.homedir(),
1024
+ ".claude",
1025
+ "projects",
1026
+ projectName,
1027
+ );
1028
+
922
1029
  try {
923
1030
  const files = await fs.readdir(projectDir);
924
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
925
-
1031
+ const jsonlFiles = files.filter((file) => file.endsWith(".jsonl"));
1032
+
926
1033
  if (jsonlFiles.length === 0) {
927
- throw new Error('No session files found for this project');
1034
+ throw new Error("No session files found for this project");
928
1035
  }
929
-
1036
+
930
1037
  // Check all JSONL files to find which one contains the session
931
1038
  for (const file of jsonlFiles) {
932
1039
  const jsonlFile = path.join(projectDir, file);
933
- const content = await fs.readFile(jsonlFile, 'utf8');
934
- const lines = content.split('\n').filter(line => line.trim());
935
-
1040
+ const content = await fs.readFile(jsonlFile, "utf8");
1041
+ const lines = content.split("\n").filter((line) => line.trim());
1042
+
936
1043
  // Check if this file contains the session
937
- const hasSession = lines.some(line => {
1044
+ const hasSession = lines.some((line) => {
938
1045
  try {
939
1046
  const data = JSON.parse(line);
940
1047
  return data.sessionId === sessionId;
@@ -942,10 +1049,10 @@ async function deleteSession(projectName, sessionId) {
942
1049
  return false;
943
1050
  }
944
1051
  });
945
-
1052
+
946
1053
  if (hasSession) {
947
1054
  // Filter out all entries for this session
948
- const filteredLines = lines.filter(line => {
1055
+ const filteredLines = lines.filter((line) => {
949
1056
  try {
950
1057
  const data = JSON.parse(line);
951
1058
  return data.sessionId !== sessionId;
@@ -953,16 +1060,22 @@ async function deleteSession(projectName, sessionId) {
953
1060
  return true; // Keep malformed lines
954
1061
  }
955
1062
  });
956
-
1063
+
957
1064
  // Write back the filtered content
958
- await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
1065
+ await fs.writeFile(
1066
+ jsonlFile,
1067
+ filteredLines.join("\n") + (filteredLines.length > 0 ? "\n" : ""),
1068
+ );
959
1069
  return true;
960
1070
  }
961
1071
  }
962
-
1072
+
963
1073
  throw new Error(`Session ${sessionId} not found in any files`);
964
1074
  } catch (error) {
965
- console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
1075
+ console.error(
1076
+ `Error deleting session ${sessionId} from project ${projectName}:`,
1077
+ error,
1078
+ );
966
1079
  throw error;
967
1080
  }
968
1081
  }
@@ -980,23 +1093,28 @@ async function isProjectEmpty(projectName) {
980
1093
 
981
1094
  // Delete an empty project
982
1095
  async function deleteProject(projectName) {
983
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
984
-
1096
+ const projectDir = path.join(
1097
+ os.homedir(),
1098
+ ".claude",
1099
+ "projects",
1100
+ projectName,
1101
+ );
1102
+
985
1103
  try {
986
1104
  // First check if the project is empty
987
1105
  const isEmpty = await isProjectEmpty(projectName);
988
1106
  if (!isEmpty) {
989
- throw new Error('Cannot delete project with existing sessions');
1107
+ throw new Error("Cannot delete project with existing sessions");
990
1108
  }
991
-
1109
+
992
1110
  // Remove the project directory
993
1111
  await fs.rm(projectDir, { recursive: true, force: true });
994
-
1112
+
995
1113
  // Remove from project config
996
1114
  const config = await loadProjectConfig();
997
1115
  delete config[projectName];
998
1116
  await saveProjectConfig(config);
999
-
1117
+
1000
1118
  return true;
1001
1119
  } catch (error) {
1002
1120
  console.error(`Error deleting project ${projectName}:`, error);
@@ -1007,20 +1125,25 @@ async function deleteProject(projectName) {
1007
1125
  // Add a project manually to the config (without creating folders)
1008
1126
  async function addProjectManually(projectPath, displayName = null) {
1009
1127
  const absolutePath = path.resolve(projectPath);
1010
-
1128
+
1011
1129
  try {
1012
1130
  // Check if the path exists
1013
1131
  await fs.access(absolutePath);
1014
1132
  } catch (error) {
1015
1133
  throw new Error(`Path does not exist: ${absolutePath}`);
1016
1134
  }
1017
-
1135
+
1018
1136
  // Generate project name (encode path for use as directory name)
1019
- const projectName = absolutePath.replace(/\//g, '-');
1020
-
1137
+ const projectName = absolutePath.replace(/\//g, "-");
1138
+
1021
1139
  // Check if project already exists in config
1022
1140
  const config = await loadProjectConfig();
1023
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1141
+ const projectDir = path.join(
1142
+ os.homedir(),
1143
+ ".claude",
1144
+ "projects",
1145
+ projectName,
1146
+ );
1024
1147
 
1025
1148
  if (config[projectName]) {
1026
1149
  throw new Error(`Project already configured for path: ${absolutePath}`);
@@ -1028,28 +1151,28 @@ async function addProjectManually(projectPath, displayName = null) {
1028
1151
 
1029
1152
  // Allow adding projects even if the directory exists - this enables tracking
1030
1153
  // existing Claude Code or Cursor projects in the UI
1031
-
1154
+
1032
1155
  // Add to config as manually added project
1033
1156
  config[projectName] = {
1034
1157
  manuallyAdded: true,
1035
- originalPath: absolutePath
1158
+ originalPath: absolutePath,
1036
1159
  };
1037
-
1160
+
1038
1161
  if (displayName) {
1039
1162
  config[projectName].displayName = displayName;
1040
1163
  }
1041
-
1164
+
1042
1165
  await saveProjectConfig(config);
1043
-
1044
-
1166
+
1045
1167
  return {
1046
1168
  name: projectName,
1047
1169
  path: absolutePath,
1048
1170
  fullPath: absolutePath,
1049
- displayName: displayName || await generateDisplayName(projectName, absolutePath),
1171
+ displayName:
1172
+ displayName || (await generateDisplayName(projectName, absolutePath)),
1050
1173
  isManuallyAdded: true,
1051
1174
  sessions: [],
1052
- cursorSessions: []
1175
+ cursorSessions: [],
1053
1176
  };
1054
1177
  }
1055
1178
 
@@ -1057,9 +1180,9 @@ async function addProjectManually(projectPath, displayName = null) {
1057
1180
  async function getCursorSessions(projectPath) {
1058
1181
  try {
1059
1182
  // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
1060
- const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
1061
- const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
1062
-
1183
+ const cwdId = crypto.createHash("md5").update(projectPath).digest("hex");
1184
+ const cursorChatsPath = path.join(os.homedir(), ".cursor", "chats", cwdId);
1185
+
1063
1186
  // Check if the directory exists
1064
1187
  try {
1065
1188
  await fs.access(cursorChatsPath);
@@ -1067,19 +1190,19 @@ async function getCursorSessions(projectPath) {
1067
1190
  // No sessions for this project
1068
1191
  return [];
1069
1192
  }
1070
-
1193
+
1071
1194
  // List all session directories
1072
1195
  const sessionDirs = await fs.readdir(cursorChatsPath);
1073
1196
  const sessions = [];
1074
-
1197
+
1075
1198
  for (const sessionId of sessionDirs) {
1076
1199
  const sessionPath = path.join(cursorChatsPath, sessionId);
1077
- const storeDbPath = path.join(sessionPath, 'store.db');
1078
-
1200
+ const storeDbPath = path.join(sessionPath, "store.db");
1201
+
1079
1202
  try {
1080
1203
  // Check if store.db exists
1081
1204
  await fs.access(storeDbPath);
1082
-
1205
+
1083
1206
  // Capture store.db mtime as a reliable fallback timestamp
1084
1207
  let dbStatMtimeMs = null;
1085
1208
  try {
@@ -1091,14 +1214,14 @@ async function getCursorSessions(projectPath) {
1091
1214
  const db = await open({
1092
1215
  filename: storeDbPath,
1093
1216
  driver: sqlite3.Database,
1094
- mode: sqlite3.OPEN_READONLY
1217
+ mode: sqlite3.OPEN_READONLY,
1095
1218
  });
1096
-
1219
+
1097
1220
  // Get metadata from meta table
1098
1221
  const metaRows = await db.all(`
1099
1222
  SELECT key, value FROM meta
1100
1223
  `);
1101
-
1224
+
1102
1225
  // Parse metadata
1103
1226
  let metadata = {};
1104
1227
  for (const row of metaRows) {
@@ -1107,7 +1230,7 @@ async function getCursorSessions(projectPath) {
1107
1230
  // Try to decode as hex-encoded JSON
1108
1231
  const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
1109
1232
  if (hexMatch) {
1110
- const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
1233
+ const jsonStr = Buffer.from(row.value, "hex").toString("utf8");
1111
1234
  metadata[row.key] = JSON.parse(jsonStr);
1112
1235
  } else {
1113
1236
  metadata[row.key] = row.value.toString();
@@ -1117,17 +1240,18 @@ async function getCursorSessions(projectPath) {
1117
1240
  }
1118
1241
  }
1119
1242
  }
1120
-
1243
+
1121
1244
  // Get message count
1122
1245
  const messageCountResult = await db.get(`
1123
1246
  SELECT COUNT(*) as count FROM blobs
1124
1247
  `);
1125
-
1248
+
1126
1249
  await db.close();
1127
-
1250
+
1128
1251
  // Extract session info
1129
- const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
1130
-
1252
+ const sessionName =
1253
+ metadata.title || metadata.sessionTitle || "Untitled Session";
1254
+
1131
1255
  // Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
1132
1256
  let createdAt = null;
1133
1257
  if (metadata.createdAt) {
@@ -1137,38 +1261,38 @@ async function getCursorSessions(projectPath) {
1137
1261
  } else {
1138
1262
  createdAt = new Date().toISOString();
1139
1263
  }
1140
-
1264
+
1141
1265
  sessions.push({
1142
1266
  id: sessionId,
1143
1267
  name: sessionName,
1144
1268
  createdAt: createdAt,
1145
1269
  lastActivity: createdAt, // For compatibility with Claude sessions
1146
1270
  messageCount: messageCountResult.count || 0,
1147
- projectPath: projectPath
1271
+ projectPath: projectPath,
1148
1272
  });
1149
-
1150
1273
  } catch (error) {
1151
- console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
1274
+ console.warn(
1275
+ `Could not read Cursor session ${sessionId}:`,
1276
+ error.message,
1277
+ );
1152
1278
  }
1153
1279
  }
1154
-
1280
+
1155
1281
  // Sort sessions by creation time (newest first)
1156
1282
  sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
1157
-
1283
+
1158
1284
  // Return only the first 5 sessions for performance
1159
1285
  return sessions.slice(0, 5);
1160
-
1161
1286
  } catch (error) {
1162
- console.error('Error fetching Cursor sessions:', error);
1287
+ console.error("Error fetching Cursor sessions:", error);
1163
1288
  return [];
1164
1289
  }
1165
1290
  }
1166
1291
 
1167
-
1168
1292
  // Fetch Codex sessions for a given project path
1169
1293
  async function getCodexSessions(projectPath) {
1170
1294
  try {
1171
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1295
+ const codexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
1172
1296
  const sessions = [];
1173
1297
 
1174
1298
  // Check if the directory exists
@@ -1187,8 +1311,8 @@ async function getCodexSessions(projectPath) {
1187
1311
  for (const entry of entries) {
1188
1312
  const fullPath = path.join(dir, entry.name);
1189
1313
  if (entry.isDirectory()) {
1190
- files.push(...await findJsonlFiles(fullPath));
1191
- } else if (entry.name.endsWith('.jsonl')) {
1314
+ files.push(...(await findJsonlFiles(fullPath)));
1315
+ } else if (entry.name.endsWith(".jsonl")) {
1192
1316
  files.push(fullPath);
1193
1317
  }
1194
1318
  }
@@ -1207,35 +1331,50 @@ async function getCodexSessions(projectPath) {
1207
1331
 
1208
1332
  // Check if this session matches the project path
1209
1333
  // Handle Windows long paths with \\?\ prefix
1210
- const sessionCwd = sessionData?.cwd || '';
1211
- const cleanSessionCwd = sessionCwd.startsWith('\\\\?\\') ? sessionCwd.slice(4) : sessionCwd;
1212
- const cleanProjectPath = projectPath.startsWith('\\\\?\\') ? projectPath.slice(4) : projectPath;
1213
-
1214
- if (sessionData && (sessionData.cwd === projectPath || cleanSessionCwd === cleanProjectPath || path.relative(cleanSessionCwd, cleanProjectPath) === '')) {
1334
+ const sessionCwd = sessionData?.cwd || "";
1335
+ const cleanSessionCwd = sessionCwd.startsWith("\\\\?\\")
1336
+ ? sessionCwd.slice(4)
1337
+ : sessionCwd;
1338
+ const cleanProjectPath = projectPath.startsWith("\\\\?\\")
1339
+ ? projectPath.slice(4)
1340
+ : projectPath;
1341
+
1342
+ if (
1343
+ sessionData &&
1344
+ (sessionData.cwd === projectPath ||
1345
+ cleanSessionCwd === cleanProjectPath ||
1346
+ path.relative(cleanSessionCwd, cleanProjectPath) === "")
1347
+ ) {
1215
1348
  sessions.push({
1216
1349
  id: sessionData.id,
1217
- summary: sessionData.summary || 'Codex Session',
1350
+ summary: sessionData.summary || "Codex Session",
1218
1351
  messageCount: sessionData.messageCount || 0,
1219
- lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
1352
+ lastActivity: sessionData.timestamp
1353
+ ? new Date(sessionData.timestamp)
1354
+ : new Date(),
1220
1355
  cwd: sessionData.cwd,
1221
1356
  model: sessionData.model,
1222
1357
  filePath: filePath,
1223
- provider: 'codex'
1358
+ provider: "codex",
1224
1359
  });
1225
1360
  }
1226
1361
  } catch (error) {
1227
- console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
1362
+ console.warn(
1363
+ `Could not parse Codex session file ${filePath}:`,
1364
+ error.message,
1365
+ );
1228
1366
  }
1229
1367
  }
1230
1368
 
1231
1369
  // Sort sessions by last activity (newest first)
1232
- sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1370
+ sessions.sort(
1371
+ (a, b) => new Date(b.lastActivity) - new Date(a.lastActivity),
1372
+ );
1233
1373
 
1234
1374
  // Return only the first 5 sessions for performance
1235
1375
  return sessions.slice(0, 5);
1236
-
1237
1376
  } catch (error) {
1238
- console.error('Error fetching Codex sessions:', error);
1377
+ console.error("Error fetching Codex sessions:", error);
1239
1378
  return [];
1240
1379
  }
1241
1380
  }
@@ -1246,7 +1385,7 @@ async function parseCodexSessionFile(filePath) {
1246
1385
  const fileStream = fsSync.createReadStream(filePath);
1247
1386
  const rl = readline.createInterface({
1248
1387
  input: fileStream,
1249
- crlfDelay: Infinity
1388
+ crlfDelay: Infinity,
1250
1389
  });
1251
1390
 
1252
1391
  let sessionMeta = null;
@@ -1265,28 +1404,34 @@ async function parseCodexSessionFile(filePath) {
1265
1404
  }
1266
1405
 
1267
1406
  // Extract session metadata
1268
- if (entry.type === 'session_meta' && entry.payload) {
1407
+ if (entry.type === "session_meta" && entry.payload) {
1269
1408
  sessionMeta = {
1270
1409
  id: entry.payload.id,
1271
1410
  cwd: entry.payload.cwd,
1272
1411
  model: entry.payload.model || entry.payload.model_provider,
1273
1412
  timestamp: entry.timestamp,
1274
- git: entry.payload.git
1413
+ git: entry.payload.git,
1275
1414
  };
1276
1415
  }
1277
1416
 
1278
1417
  // Count messages and extract user messages for summary
1279
- if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') {
1418
+ if (
1419
+ entry.type === "event_msg" &&
1420
+ entry.payload?.type === "user_message"
1421
+ ) {
1280
1422
  messageCount++;
1281
1423
  if (entry.payload.message) {
1282
1424
  lastUserMessage = entry.payload.message;
1283
1425
  }
1284
1426
  }
1285
1427
 
1286
- if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
1428
+ if (
1429
+ entry.type === "response_item" &&
1430
+ entry.payload?.type === "message" &&
1431
+ entry.payload.role === "assistant"
1432
+ ) {
1287
1433
  messageCount++;
1288
1434
  }
1289
-
1290
1435
  } catch (parseError) {
1291
1436
  // Skip malformed lines
1292
1437
  }
@@ -1297,17 +1442,18 @@ async function parseCodexSessionFile(filePath) {
1297
1442
  return {
1298
1443
  ...sessionMeta,
1299
1444
  timestamp: lastTimestamp || sessionMeta.timestamp,
1300
- summary: lastUserMessage ?
1301
- (lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage) :
1302
- 'Codex Session',
1303
- messageCount
1445
+ summary: lastUserMessage
1446
+ ? lastUserMessage.length > 50
1447
+ ? lastUserMessage.substring(0, 50) + "..."
1448
+ : lastUserMessage
1449
+ : "Codex Session",
1450
+ messageCount,
1304
1451
  };
1305
1452
  }
1306
1453
 
1307
1454
  return null;
1308
-
1309
1455
  } catch (error) {
1310
- console.error('Error parsing Codex session file:', error);
1456
+ console.error("Error parsing Codex session file:", error);
1311
1457
  return null;
1312
1458
  }
1313
1459
  }
@@ -1315,7 +1461,7 @@ async function parseCodexSessionFile(filePath) {
1315
1461
  // Get messages for a specific Codex session
1316
1462
  async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1317
1463
  try {
1318
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1464
+ const codexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
1319
1465
 
1320
1466
  // Find the session file by searching for the session ID
1321
1467
  const findSessionFile = async (dir) => {
@@ -1326,7 +1472,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1326
1472
  if (entry.isDirectory()) {
1327
1473
  const found = await findSessionFile(fullPath);
1328
1474
  if (found) return found;
1329
- } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
1475
+ } else if (
1476
+ entry.name.includes(sessionId) &&
1477
+ entry.name.endsWith(".jsonl")
1478
+ ) {
1330
1479
  return fullPath;
1331
1480
  }
1332
1481
  }
@@ -1348,24 +1497,24 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1348
1497
  const fileStream = fsSync.createReadStream(sessionFilePath);
1349
1498
  const rl = readline.createInterface({
1350
1499
  input: fileStream,
1351
- crlfDelay: Infinity
1500
+ crlfDelay: Infinity,
1352
1501
  });
1353
1502
 
1354
1503
  // Helper to extract text from Codex content array
1355
1504
  const extractText = (content) => {
1356
1505
  if (!Array.isArray(content)) return content;
1357
1506
  return content
1358
- .map(item => {
1359
- if (item.type === 'input_text' || item.type === 'output_text') {
1507
+ .map((item) => {
1508
+ if (item.type === "input_text" || item.type === "output_text") {
1360
1509
  return item.text;
1361
1510
  }
1362
- if (item.type === 'text') {
1511
+ if (item.type === "text") {
1363
1512
  return item.text;
1364
1513
  }
1365
- return '';
1514
+ return "";
1366
1515
  })
1367
1516
  .filter(Boolean)
1368
- .join('\n');
1517
+ .join("\n");
1369
1518
  };
1370
1519
 
1371
1520
  for await (const line of rl) {
@@ -1374,64 +1523,77 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1374
1523
  const entry = JSON.parse(line);
1375
1524
 
1376
1525
  // Extract token usage from token_count events (keep latest)
1377
- if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1526
+ if (
1527
+ entry.type === "event_msg" &&
1528
+ entry.payload?.type === "token_count" &&
1529
+ entry.payload?.info
1530
+ ) {
1378
1531
  const info = entry.payload.info;
1379
1532
  if (info.total_token_usage) {
1380
1533
  tokenUsage = {
1381
1534
  used: info.total_token_usage.total_tokens || 0,
1382
- total: info.model_context_window || 200000
1535
+ total: info.model_context_window || 200000,
1383
1536
  };
1384
1537
  }
1385
1538
  }
1386
1539
 
1387
1540
  // Extract messages from response_item
1388
- if (entry.type === 'response_item' && entry.payload?.type === 'message') {
1541
+ if (
1542
+ entry.type === "response_item" &&
1543
+ entry.payload?.type === "message"
1544
+ ) {
1389
1545
  const content = entry.payload.content;
1390
- const role = entry.payload.role || 'assistant';
1546
+ const role = entry.payload.role || "assistant";
1391
1547
  const textContent = extractText(content);
1392
1548
 
1393
1549
  // Skip system context messages (environment_context)
1394
- if (textContent?.includes('<environment_context>')) {
1550
+ if (textContent?.includes("<environment_context>")) {
1395
1551
  continue;
1396
1552
  }
1397
1553
 
1398
1554
  // Only add if there's actual content
1399
1555
  if (textContent?.trim()) {
1400
1556
  messages.push({
1401
- type: role === 'user' ? 'user' : 'assistant',
1557
+ type: role === "user" ? "user" : "assistant",
1402
1558
  timestamp: entry.timestamp,
1403
1559
  message: {
1404
1560
  role: role,
1405
- content: textContent
1406
- }
1561
+ content: textContent,
1562
+ },
1407
1563
  });
1408
1564
  }
1409
1565
  }
1410
1566
 
1411
- if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
1567
+ if (
1568
+ entry.type === "response_item" &&
1569
+ entry.payload?.type === "reasoning"
1570
+ ) {
1412
1571
  const summaryText = entry.payload.summary
1413
- ?.map(s => s.text)
1572
+ ?.map((s) => s.text)
1414
1573
  .filter(Boolean)
1415
- .join('\n');
1574
+ .join("\n");
1416
1575
  if (summaryText?.trim()) {
1417
1576
  messages.push({
1418
- type: 'thinking',
1577
+ type: "thinking",
1419
1578
  timestamp: entry.timestamp,
1420
1579
  message: {
1421
- role: 'assistant',
1422
- content: summaryText
1423
- }
1580
+ role: "assistant",
1581
+ content: summaryText,
1582
+ },
1424
1583
  });
1425
1584
  }
1426
1585
  }
1427
1586
 
1428
- if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
1587
+ if (
1588
+ entry.type === "response_item" &&
1589
+ entry.payload?.type === "function_call"
1590
+ ) {
1429
1591
  let toolName = entry.payload.name;
1430
1592
  let toolInput = entry.payload.arguments;
1431
1593
 
1432
1594
  // Map Codex tool names to Claude equivalents
1433
- if (toolName === 'shell_command') {
1434
- toolName = 'Bash';
1595
+ if (toolName === "shell_command") {
1596
+ toolName = "Bash";
1435
1597
  try {
1436
1598
  const args = JSON.parse(entry.payload.arguments);
1437
1599
  toolInput = JSON.stringify({ command: args.command });
@@ -1441,76 +1603,84 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1441
1603
  }
1442
1604
 
1443
1605
  messages.push({
1444
- type: 'tool_use',
1606
+ type: "tool_use",
1445
1607
  timestamp: entry.timestamp,
1446
1608
  toolName: toolName,
1447
1609
  toolInput: toolInput,
1448
- toolCallId: entry.payload.call_id
1610
+ toolCallId: entry.payload.call_id,
1449
1611
  });
1450
1612
  }
1451
1613
 
1452
- if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
1614
+ if (
1615
+ entry.type === "response_item" &&
1616
+ entry.payload?.type === "function_call_output"
1617
+ ) {
1453
1618
  messages.push({
1454
- type: 'tool_result',
1619
+ type: "tool_result",
1455
1620
  timestamp: entry.timestamp,
1456
1621
  toolCallId: entry.payload.call_id,
1457
- output: entry.payload.output
1622
+ output: entry.payload.output,
1458
1623
  });
1459
1624
  }
1460
1625
 
1461
- if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
1462
- const toolName = entry.payload.name || 'custom_tool';
1463
- const input = entry.payload.input || '';
1626
+ if (
1627
+ entry.type === "response_item" &&
1628
+ entry.payload?.type === "custom_tool_call"
1629
+ ) {
1630
+ const toolName = entry.payload.name || "custom_tool";
1631
+ const input = entry.payload.input || "";
1464
1632
 
1465
- if (toolName === 'apply_patch') {
1633
+ if (toolName === "apply_patch") {
1466
1634
  // Parse Codex patch format and convert to Claude Edit format
1467
1635
  const fileMatch = input.match(/\*\*\* Update File: (.+)/);
1468
- const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
1636
+ const filePath = fileMatch ? fileMatch[1].trim() : "unknown";
1469
1637
 
1470
1638
  // Extract old and new content from patch
1471
- const lines = input.split('\n');
1639
+ const lines = input.split("\n");
1472
1640
  const oldLines = [];
1473
1641
  const newLines = [];
1474
1642
 
1475
1643
  for (const line of lines) {
1476
- if (line.startsWith('-') && !line.startsWith('---')) {
1644
+ if (line.startsWith("-") && !line.startsWith("---")) {
1477
1645
  oldLines.push(line.substring(1));
1478
- } else if (line.startsWith('+') && !line.startsWith('+++')) {
1646
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
1479
1647
  newLines.push(line.substring(1));
1480
1648
  }
1481
1649
  }
1482
1650
 
1483
1651
  messages.push({
1484
- type: 'tool_use',
1652
+ type: "tool_use",
1485
1653
  timestamp: entry.timestamp,
1486
- toolName: 'Edit',
1654
+ toolName: "Edit",
1487
1655
  toolInput: JSON.stringify({
1488
1656
  file_path: filePath,
1489
- old_string: oldLines.join('\n'),
1490
- new_string: newLines.join('\n')
1657
+ old_string: oldLines.join("\n"),
1658
+ new_string: newLines.join("\n"),
1491
1659
  }),
1492
- toolCallId: entry.payload.call_id
1660
+ toolCallId: entry.payload.call_id,
1493
1661
  });
1494
1662
  } else {
1495
1663
  messages.push({
1496
- type: 'tool_use',
1664
+ type: "tool_use",
1497
1665
  timestamp: entry.timestamp,
1498
1666
  toolName: toolName,
1499
1667
  toolInput: input,
1500
- toolCallId: entry.payload.call_id
1668
+ toolCallId: entry.payload.call_id,
1501
1669
  });
1502
1670
  }
1503
1671
  }
1504
1672
 
1505
- if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
1673
+ if (
1674
+ entry.type === "response_item" &&
1675
+ entry.payload?.type === "custom_tool_call_output"
1676
+ ) {
1506
1677
  messages.push({
1507
- type: 'tool_result',
1678
+ type: "tool_result",
1508
1679
  timestamp: entry.timestamp,
1509
1680
  toolCallId: entry.payload.call_id,
1510
- output: entry.payload.output || ''
1681
+ output: entry.payload.output || "",
1511
1682
  });
1512
1683
  }
1513
-
1514
1684
  } catch (parseError) {
1515
1685
  // Skip malformed lines
1516
1686
  }
@@ -1518,7 +1688,9 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1518
1688
  }
1519
1689
 
1520
1690
  // Sort by timestamp
1521
- messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
1691
+ messages.sort(
1692
+ (a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0),
1693
+ );
1522
1694
 
1523
1695
  const total = messages.length;
1524
1696
 
@@ -1535,21 +1707,23 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1535
1707
  hasMore,
1536
1708
  offset,
1537
1709
  limit,
1538
- tokenUsage
1710
+ tokenUsage,
1539
1711
  };
1540
1712
  }
1541
1713
 
1542
1714
  return { messages, tokenUsage };
1543
-
1544
1715
  } catch (error) {
1545
- console.error(`Error reading Codex session messages for ${sessionId}:`, error);
1716
+ console.error(
1717
+ `Error reading Codex session messages for ${sessionId}:`,
1718
+ error,
1719
+ );
1546
1720
  return { messages: [], total: 0, hasMore: false };
1547
1721
  }
1548
1722
  }
1549
1723
 
1550
1724
  async function deleteCodexSession(sessionId) {
1551
1725
  try {
1552
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1726
+ const codexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
1553
1727
 
1554
1728
  const findJsonlFiles = async (dir) => {
1555
1729
  const files = [];
@@ -1558,8 +1732,8 @@ async function deleteCodexSession(sessionId) {
1558
1732
  for (const entry of entries) {
1559
1733
  const fullPath = path.join(dir, entry.name);
1560
1734
  if (entry.isDirectory()) {
1561
- files.push(...await findJsonlFiles(fullPath));
1562
- } else if (entry.name.endsWith('.jsonl')) {
1735
+ files.push(...(await findJsonlFiles(fullPath)));
1736
+ } else if (entry.name.endsWith(".jsonl")) {
1563
1737
  files.push(fullPath);
1564
1738
  }
1565
1739
  }
@@ -1584,6 +1758,126 @@ async function deleteCodexSession(sessionId) {
1584
1758
  }
1585
1759
  }
1586
1760
 
1761
+ /**
1762
+ * Get full project details for a specific project by name
1763
+ * Used for on-demand loading when a project is expanded in the sidebar
1764
+ */
1765
+ async function getProjectDetailFull(projectName) {
1766
+ const claudeDir = path.join(os.homedir(), ".claude", "projects");
1767
+ const config = await loadProjectConfig();
1768
+
1769
+ // Check if it's a manually added project
1770
+ const isManuallyAdded = config[projectName]?.manuallyAdded || false;
1771
+
1772
+ // Determine the project directory
1773
+ let projectPath;
1774
+ let actualProjectDir;
1775
+
1776
+ if (isManuallyAdded) {
1777
+ actualProjectDir = config[projectName]?.originalPath;
1778
+ if (!actualProjectDir) {
1779
+ actualProjectDir = await extractProjectDirectory(projectName);
1780
+ }
1781
+ } else {
1782
+ projectPath = path.join(claudeDir, projectName);
1783
+
1784
+ // Check if project directory exists
1785
+ try {
1786
+ await fs.access(projectPath);
1787
+ } catch (error) {
1788
+ if (error.code === "ENOENT") {
1789
+ return null; // Project not found
1790
+ }
1791
+ throw error;
1792
+ }
1793
+
1794
+ actualProjectDir = await extractProjectDirectory(projectName);
1795
+ }
1796
+
1797
+ // Build the project object
1798
+ const customName = config[projectName]?.displayName;
1799
+ const autoDisplayName = await generateDisplayName(
1800
+ projectName,
1801
+ actualProjectDir,
1802
+ );
1803
+
1804
+ const project = {
1805
+ name: projectName,
1806
+ path: actualProjectDir,
1807
+ displayName: customName || autoDisplayName,
1808
+ fullPath: actualProjectDir,
1809
+ isCustomName: !!customName,
1810
+ isManuallyAdded,
1811
+ sessions: [],
1812
+ cursorSessions: [],
1813
+ codexSessions: [],
1814
+ };
1815
+
1816
+ // Fetch all Claude sessions (no limit)
1817
+ if (!isManuallyAdded) {
1818
+ try {
1819
+ const sessionResult = await getSessions(projectName, 100, 0); // Higher limit for detail view
1820
+ project.sessions = sessionResult.sessions || [];
1821
+ project.sessionMeta = {
1822
+ hasMore: sessionResult.hasMore,
1823
+ total: sessionResult.total,
1824
+ };
1825
+ } catch (e) {
1826
+ console.warn(
1827
+ `Could not load sessions for project ${projectName}:`,
1828
+ e.message,
1829
+ );
1830
+ }
1831
+ }
1832
+
1833
+ // Fetch Cursor sessions
1834
+ try {
1835
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
1836
+ } catch (e) {
1837
+ console.warn(
1838
+ `Could not load Cursor sessions for project ${projectName}:`,
1839
+ e.message,
1840
+ );
1841
+ }
1842
+
1843
+ // Fetch Codex sessions
1844
+ try {
1845
+ project.codexSessions = await getCodexSessions(actualProjectDir);
1846
+ } catch (e) {
1847
+ console.warn(
1848
+ `Could not load Codex sessions for project ${projectName}:`,
1849
+ e.message,
1850
+ );
1851
+ }
1852
+
1853
+ // Fetch TaskMaster data
1854
+ try {
1855
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
1856
+ project.taskmaster = {
1857
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
1858
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
1859
+ metadata: taskMasterResult.metadata,
1860
+ status:
1861
+ taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles
1862
+ ? "configured"
1863
+ : "not-configured",
1864
+ };
1865
+ } catch (e) {
1866
+ console.warn(
1867
+ `Could not detect TaskMaster for project ${projectName}:`,
1868
+ e.message,
1869
+ );
1870
+ project.taskmaster = {
1871
+ hasTaskmaster: false,
1872
+ hasEssentialFiles: false,
1873
+ metadata: null,
1874
+ status: "error",
1875
+ };
1876
+ }
1877
+
1878
+ return project;
1879
+ }
1880
+
1587
1881
  export {
1588
1882
  getProjects,
1589
1883
  getSessions,
@@ -1600,5 +1894,6 @@ export {
1600
1894
  clearProjectDirectoryCache,
1601
1895
  getCodexSessions,
1602
1896
  getCodexSessionMessages,
1603
- deleteCodexSession
1897
+ deleteCodexSession,
1898
+ getProjectDetailFull,
1604
1899
  };