@epiphytic/claudecodeui 1.0.1 → 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.
- package/dist/assets/index-D0xTNXrF.js +1247 -0
- package/dist/assets/index-DKDK7xNY.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +124 -0
- package/server/database/init.sql +15 -1
- package/server/external-session-detector.js +403 -0
- package/server/index.js +816 -110
- package/server/projects-cache.js +196 -0
- package/server/projects.js +759 -464
- package/server/routes/projects.js +248 -92
- package/server/routes/sessions.js +106 -0
- package/server/session-lock.js +253 -0
- package/server/sessions-cache.js +183 -0
- package/server/tmux-manager.js +403 -0
- package/dist/assets/index-COkp1acE.js +0 -1231
- package/dist/assets/index-DfR9xEkp.css +0 -32
package/server/projects.js
CHANGED
|
@@ -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
|
|
61
|
-
import fsSync from
|
|
62
|
-
import path from
|
|
63
|
-
import readline from
|
|
64
|
-
import crypto from
|
|
65
|
-
import sqlite3 from
|
|
66
|
-
import { open } from
|
|
67
|
-
import os from
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
96
|
+
const fileStatus = {};
|
|
97
|
+
let hasEssentialFiles = true;
|
|
136
98
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(),
|
|
208
|
+
const configPath = path.join(os.homedir(), ".claude", "project-config.json");
|
|
208
209
|
try {
|
|
209
|
-
const configData = await fs.readFile(configPath,
|
|
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(),
|
|
220
|
-
const configPath = path.join(claudeDir,
|
|
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 !==
|
|
227
|
+
if (error.code !== "EEXIST") {
|
|
227
228
|
throw error;
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2),
|
|
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,
|
|
242
|
-
const packageData = await fs.readFile(packageJsonPath,
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
368
|
-
extractedPath = projectName.replace(/-/g,
|
|
372
|
+
if (error.code === "ENOENT") {
|
|
373
|
+
extractedPath = projectName.replace(/-/g, "/");
|
|
369
374
|
} else {
|
|
370
|
-
console.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(),
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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:
|
|
473
|
+
status:
|
|
474
|
+
taskMasterResult.hasTaskmaster &&
|
|
475
|
+
taskMasterResult.hasEssentialFiles
|
|
476
|
+
? "configured"
|
|
477
|
+
: "not-configured",
|
|
453
478
|
};
|
|
454
479
|
} catch (e) {
|
|
455
|
-
console.warn(
|
|
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:
|
|
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 !==
|
|
470
|
-
console.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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
521
|
-
if (
|
|
522
|
-
|
|
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(
|
|
571
|
+
console.warn(
|
|
572
|
+
`TaskMaster detection failed for manual project ${projectName}:`,
|
|
573
|
+
error.message,
|
|
574
|
+
);
|
|
533
575
|
project.taskmaster = {
|
|
534
|
-
status:
|
|
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(
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 ===
|
|
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 ===
|
|
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 (
|
|
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 =
|
|
735
|
-
textContent
|
|
736
|
-
textContent.startsWith(
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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 (
|
|
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 ===
|
|
846
|
+
if (part.type === "text" && part.text) {
|
|
762
847
|
assistantText = part.text;
|
|
763
848
|
}
|
|
764
849
|
}
|
|
765
|
-
} else if (typeof entry.message.content ===
|
|
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 =
|
|
771
|
-
assistantText
|
|
772
|
-
assistantText.
|
|
773
|
-
|
|
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 ===
|
|
883
|
+
if (session.summary === "New Session") {
|
|
797
884
|
// Prefer last user message, fall back to last assistant message
|
|
798
|
-
const lastMessage =
|
|
885
|
+
const lastMessage =
|
|
886
|
+
session.lastUserMessage || session.lastAssistantMessage;
|
|
799
887
|
if (lastMessage) {
|
|
800
|
-
session.summary =
|
|
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) {
|
|
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(
|
|
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(
|
|
831
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
925
|
-
|
|
1031
|
+
const jsonlFiles = files.filter((file) => file.endsWith(".jsonl"));
|
|
1032
|
+
|
|
926
1033
|
if (jsonlFiles.length === 0) {
|
|
927
|
-
throw new Error(
|
|
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,
|
|
934
|
-
const lines = content.split(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
1061
|
-
const cursorChatsPath = path.join(os.homedir(),
|
|
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,
|
|
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,
|
|
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 =
|
|
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(
|
|
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(
|
|
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(),
|
|
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(
|
|
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(
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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 ||
|
|
1350
|
+
summary: sessionData.summary || "Codex Session",
|
|
1218
1351
|
messageCount: sessionData.messageCount || 0,
|
|
1219
|
-
lastActivity: sessionData.timestamp
|
|
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:
|
|
1358
|
+
provider: "codex",
|
|
1224
1359
|
});
|
|
1225
1360
|
}
|
|
1226
1361
|
} catch (error) {
|
|
1227
|
-
console.warn(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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(
|
|
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(),
|
|
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 (
|
|
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 ===
|
|
1507
|
+
.map((item) => {
|
|
1508
|
+
if (item.type === "input_text" || item.type === "output_text") {
|
|
1360
1509
|
return item.text;
|
|
1361
1510
|
}
|
|
1362
|
-
if (item.type ===
|
|
1511
|
+
if (item.type === "text") {
|
|
1363
1512
|
return item.text;
|
|
1364
1513
|
}
|
|
1365
|
-
return
|
|
1514
|
+
return "";
|
|
1366
1515
|
})
|
|
1367
1516
|
.filter(Boolean)
|
|
1368
|
-
.join(
|
|
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 (
|
|
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 (
|
|
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 ||
|
|
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(
|
|
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 ===
|
|
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 (
|
|
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(
|
|
1574
|
+
.join("\n");
|
|
1416
1575
|
if (summaryText?.trim()) {
|
|
1417
1576
|
messages.push({
|
|
1418
|
-
type:
|
|
1577
|
+
type: "thinking",
|
|
1419
1578
|
timestamp: entry.timestamp,
|
|
1420
1579
|
message: {
|
|
1421
|
-
role:
|
|
1422
|
-
content: summaryText
|
|
1423
|
-
}
|
|
1580
|
+
role: "assistant",
|
|
1581
|
+
content: summaryText,
|
|
1582
|
+
},
|
|
1424
1583
|
});
|
|
1425
1584
|
}
|
|
1426
1585
|
}
|
|
1427
1586
|
|
|
1428
|
-
if (
|
|
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 ===
|
|
1434
|
-
toolName =
|
|
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:
|
|
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 (
|
|
1614
|
+
if (
|
|
1615
|
+
entry.type === "response_item" &&
|
|
1616
|
+
entry.payload?.type === "function_call_output"
|
|
1617
|
+
) {
|
|
1453
1618
|
messages.push({
|
|
1454
|
-
type:
|
|
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 (
|
|
1462
|
-
|
|
1463
|
-
|
|
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 ===
|
|
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() :
|
|
1636
|
+
const filePath = fileMatch ? fileMatch[1].trim() : "unknown";
|
|
1469
1637
|
|
|
1470
1638
|
// Extract old and new content from patch
|
|
1471
|
-
const lines = input.split(
|
|
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(
|
|
1644
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1477
1645
|
oldLines.push(line.substring(1));
|
|
1478
|
-
} else if (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:
|
|
1652
|
+
type: "tool_use",
|
|
1485
1653
|
timestamp: entry.timestamp,
|
|
1486
|
-
toolName:
|
|
1654
|
+
toolName: "Edit",
|
|
1487
1655
|
toolInput: JSON.stringify({
|
|
1488
1656
|
file_path: filePath,
|
|
1489
|
-
old_string: oldLines.join(
|
|
1490
|
-
new_string: newLines.join(
|
|
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:
|
|
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 (
|
|
1673
|
+
if (
|
|
1674
|
+
entry.type === "response_item" &&
|
|
1675
|
+
entry.payload?.type === "custom_tool_call_output"
|
|
1676
|
+
) {
|
|
1506
1677
|
messages.push({
|
|
1507
|
-
type:
|
|
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(
|
|
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(
|
|
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(),
|
|
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(
|
|
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
|
};
|