@siteboon/claude-code-ui 1.20.1 → 1.21.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.
- package/README.md +1 -0
- package/dist/assets/index-Cxnz_sny.css +32 -0
- package/dist/assets/index-DN2ZJcRJ.js +1381 -0
- package/dist/assets/{vendor-codemirror-l-lAmaJ1.js → vendor-codemirror-BMLq5tLB.js} +8 -8
- package/dist/assets/{vendor-xterm-DfaPXD3y.js → vendor-xterm-CJZjLICi.js} +10 -10
- package/dist/icons/gemini-ai-icon.svg +1 -0
- package/dist/index.html +4 -4
- package/package.json +4 -3
- package/server/claude-sdk.js +3 -0
- package/server/gemini-cli.js +455 -0
- package/server/gemini-response-handler.js +140 -0
- package/server/index.js +304 -225
- package/server/projects.js +292 -275
- package/server/routes/agent.js +15 -4
- package/server/routes/cli-auth.js +114 -0
- package/server/routes/gemini.js +46 -0
- package/server/sessionManager.js +226 -0
- package/shared/modelConstants.js +19 -0
- package/dist/assets/index-BPHfv_yU.css +0 -32
- package/dist/assets/index-C88hdQje.js +0 -1413
- package/server/database/auth.db +0 -0
package/server/projects.js
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
930
|
-
|
|
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
|
|