@epiphytic/claudecodeui 1.0.1 → 1.2.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-DqxzEd_8.js +1245 -0
- package/dist/assets/index-r43D8sh4.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +222 -0
- package/server/database/init.sql +27 -1
- package/server/external-session-detector.js +403 -0
- package/server/index.js +885 -116
- package/server/orchestrator/client.js +361 -16
- package/server/orchestrator/index.js +83 -8
- package/server/orchestrator/protocol.js +67 -0
- package/server/projects-cache.js +196 -0
- package/server/projects.js +760 -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,26 @@ 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
|
-
|
|
1020
|
-
|
|
1137
|
+
// Claude's encoding replaces /, :, spaces, ~, _, and . with -
|
|
1138
|
+
const projectName = absolutePath.replace(/[\\/:\s~_.]/g, "-");
|
|
1139
|
+
|
|
1021
1140
|
// Check if project already exists in config
|
|
1022
1141
|
const config = await loadProjectConfig();
|
|
1023
|
-
const projectDir = path.join(
|
|
1142
|
+
const projectDir = path.join(
|
|
1143
|
+
os.homedir(),
|
|
1144
|
+
".claude",
|
|
1145
|
+
"projects",
|
|
1146
|
+
projectName,
|
|
1147
|
+
);
|
|
1024
1148
|
|
|
1025
1149
|
if (config[projectName]) {
|
|
1026
1150
|
throw new Error(`Project already configured for path: ${absolutePath}`);
|
|
@@ -1028,28 +1152,28 @@ async function addProjectManually(projectPath, displayName = null) {
|
|
|
1028
1152
|
|
|
1029
1153
|
// Allow adding projects even if the directory exists - this enables tracking
|
|
1030
1154
|
// existing Claude Code or Cursor projects in the UI
|
|
1031
|
-
|
|
1155
|
+
|
|
1032
1156
|
// Add to config as manually added project
|
|
1033
1157
|
config[projectName] = {
|
|
1034
1158
|
manuallyAdded: true,
|
|
1035
|
-
originalPath: absolutePath
|
|
1159
|
+
originalPath: absolutePath,
|
|
1036
1160
|
};
|
|
1037
|
-
|
|
1161
|
+
|
|
1038
1162
|
if (displayName) {
|
|
1039
1163
|
config[projectName].displayName = displayName;
|
|
1040
1164
|
}
|
|
1041
|
-
|
|
1165
|
+
|
|
1042
1166
|
await saveProjectConfig(config);
|
|
1043
|
-
|
|
1044
|
-
|
|
1167
|
+
|
|
1045
1168
|
return {
|
|
1046
1169
|
name: projectName,
|
|
1047
1170
|
path: absolutePath,
|
|
1048
1171
|
fullPath: absolutePath,
|
|
1049
|
-
displayName:
|
|
1172
|
+
displayName:
|
|
1173
|
+
displayName || (await generateDisplayName(projectName, absolutePath)),
|
|
1050
1174
|
isManuallyAdded: true,
|
|
1051
1175
|
sessions: [],
|
|
1052
|
-
cursorSessions: []
|
|
1176
|
+
cursorSessions: [],
|
|
1053
1177
|
};
|
|
1054
1178
|
}
|
|
1055
1179
|
|
|
@@ -1057,9 +1181,9 @@ async function addProjectManually(projectPath, displayName = null) {
|
|
|
1057
1181
|
async function getCursorSessions(projectPath) {
|
|
1058
1182
|
try {
|
|
1059
1183
|
// Calculate cwdID hash for the project path (Cursor uses MD5 hash)
|
|
1060
|
-
const cwdId = crypto.createHash(
|
|
1061
|
-
const cursorChatsPath = path.join(os.homedir(),
|
|
1062
|
-
|
|
1184
|
+
const cwdId = crypto.createHash("md5").update(projectPath).digest("hex");
|
|
1185
|
+
const cursorChatsPath = path.join(os.homedir(), ".cursor", "chats", cwdId);
|
|
1186
|
+
|
|
1063
1187
|
// Check if the directory exists
|
|
1064
1188
|
try {
|
|
1065
1189
|
await fs.access(cursorChatsPath);
|
|
@@ -1067,19 +1191,19 @@ async function getCursorSessions(projectPath) {
|
|
|
1067
1191
|
// No sessions for this project
|
|
1068
1192
|
return [];
|
|
1069
1193
|
}
|
|
1070
|
-
|
|
1194
|
+
|
|
1071
1195
|
// List all session directories
|
|
1072
1196
|
const sessionDirs = await fs.readdir(cursorChatsPath);
|
|
1073
1197
|
const sessions = [];
|
|
1074
|
-
|
|
1198
|
+
|
|
1075
1199
|
for (const sessionId of sessionDirs) {
|
|
1076
1200
|
const sessionPath = path.join(cursorChatsPath, sessionId);
|
|
1077
|
-
const storeDbPath = path.join(sessionPath,
|
|
1078
|
-
|
|
1201
|
+
const storeDbPath = path.join(sessionPath, "store.db");
|
|
1202
|
+
|
|
1079
1203
|
try {
|
|
1080
1204
|
// Check if store.db exists
|
|
1081
1205
|
await fs.access(storeDbPath);
|
|
1082
|
-
|
|
1206
|
+
|
|
1083
1207
|
// Capture store.db mtime as a reliable fallback timestamp
|
|
1084
1208
|
let dbStatMtimeMs = null;
|
|
1085
1209
|
try {
|
|
@@ -1091,14 +1215,14 @@ async function getCursorSessions(projectPath) {
|
|
|
1091
1215
|
const db = await open({
|
|
1092
1216
|
filename: storeDbPath,
|
|
1093
1217
|
driver: sqlite3.Database,
|
|
1094
|
-
mode: sqlite3.OPEN_READONLY
|
|
1218
|
+
mode: sqlite3.OPEN_READONLY,
|
|
1095
1219
|
});
|
|
1096
|
-
|
|
1220
|
+
|
|
1097
1221
|
// Get metadata from meta table
|
|
1098
1222
|
const metaRows = await db.all(`
|
|
1099
1223
|
SELECT key, value FROM meta
|
|
1100
1224
|
`);
|
|
1101
|
-
|
|
1225
|
+
|
|
1102
1226
|
// Parse metadata
|
|
1103
1227
|
let metadata = {};
|
|
1104
1228
|
for (const row of metaRows) {
|
|
@@ -1107,7 +1231,7 @@ async function getCursorSessions(projectPath) {
|
|
|
1107
1231
|
// Try to decode as hex-encoded JSON
|
|
1108
1232
|
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
|
1109
1233
|
if (hexMatch) {
|
|
1110
|
-
const jsonStr = Buffer.from(row.value,
|
|
1234
|
+
const jsonStr = Buffer.from(row.value, "hex").toString("utf8");
|
|
1111
1235
|
metadata[row.key] = JSON.parse(jsonStr);
|
|
1112
1236
|
} else {
|
|
1113
1237
|
metadata[row.key] = row.value.toString();
|
|
@@ -1117,17 +1241,18 @@ async function getCursorSessions(projectPath) {
|
|
|
1117
1241
|
}
|
|
1118
1242
|
}
|
|
1119
1243
|
}
|
|
1120
|
-
|
|
1244
|
+
|
|
1121
1245
|
// Get message count
|
|
1122
1246
|
const messageCountResult = await db.get(`
|
|
1123
1247
|
SELECT COUNT(*) as count FROM blobs
|
|
1124
1248
|
`);
|
|
1125
|
-
|
|
1249
|
+
|
|
1126
1250
|
await db.close();
|
|
1127
|
-
|
|
1251
|
+
|
|
1128
1252
|
// Extract session info
|
|
1129
|
-
const sessionName =
|
|
1130
|
-
|
|
1253
|
+
const sessionName =
|
|
1254
|
+
metadata.title || metadata.sessionTitle || "Untitled Session";
|
|
1255
|
+
|
|
1131
1256
|
// Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
|
|
1132
1257
|
let createdAt = null;
|
|
1133
1258
|
if (metadata.createdAt) {
|
|
@@ -1137,38 +1262,38 @@ async function getCursorSessions(projectPath) {
|
|
|
1137
1262
|
} else {
|
|
1138
1263
|
createdAt = new Date().toISOString();
|
|
1139
1264
|
}
|
|
1140
|
-
|
|
1265
|
+
|
|
1141
1266
|
sessions.push({
|
|
1142
1267
|
id: sessionId,
|
|
1143
1268
|
name: sessionName,
|
|
1144
1269
|
createdAt: createdAt,
|
|
1145
1270
|
lastActivity: createdAt, // For compatibility with Claude sessions
|
|
1146
1271
|
messageCount: messageCountResult.count || 0,
|
|
1147
|
-
projectPath: projectPath
|
|
1272
|
+
projectPath: projectPath,
|
|
1148
1273
|
});
|
|
1149
|
-
|
|
1150
1274
|
} catch (error) {
|
|
1151
|
-
console.warn(
|
|
1275
|
+
console.warn(
|
|
1276
|
+
`Could not read Cursor session ${sessionId}:`,
|
|
1277
|
+
error.message,
|
|
1278
|
+
);
|
|
1152
1279
|
}
|
|
1153
1280
|
}
|
|
1154
|
-
|
|
1281
|
+
|
|
1155
1282
|
// Sort sessions by creation time (newest first)
|
|
1156
1283
|
sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
1157
|
-
|
|
1284
|
+
|
|
1158
1285
|
// Return only the first 5 sessions for performance
|
|
1159
1286
|
return sessions.slice(0, 5);
|
|
1160
|
-
|
|
1161
1287
|
} catch (error) {
|
|
1162
|
-
console.error(
|
|
1288
|
+
console.error("Error fetching Cursor sessions:", error);
|
|
1163
1289
|
return [];
|
|
1164
1290
|
}
|
|
1165
1291
|
}
|
|
1166
1292
|
|
|
1167
|
-
|
|
1168
1293
|
// Fetch Codex sessions for a given project path
|
|
1169
1294
|
async function getCodexSessions(projectPath) {
|
|
1170
1295
|
try {
|
|
1171
|
-
const codexSessionsDir = path.join(os.homedir(),
|
|
1296
|
+
const codexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
|
|
1172
1297
|
const sessions = [];
|
|
1173
1298
|
|
|
1174
1299
|
// Check if the directory exists
|
|
@@ -1187,8 +1312,8 @@ async function getCodexSessions(projectPath) {
|
|
|
1187
1312
|
for (const entry of entries) {
|
|
1188
1313
|
const fullPath = path.join(dir, entry.name);
|
|
1189
1314
|
if (entry.isDirectory()) {
|
|
1190
|
-
files.push(...await findJsonlFiles(fullPath));
|
|
1191
|
-
} else if (entry.name.endsWith(
|
|
1315
|
+
files.push(...(await findJsonlFiles(fullPath)));
|
|
1316
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
1192
1317
|
files.push(fullPath);
|
|
1193
1318
|
}
|
|
1194
1319
|
}
|
|
@@ -1207,35 +1332,50 @@ async function getCodexSessions(projectPath) {
|
|
|
1207
1332
|
|
|
1208
1333
|
// Check if this session matches the project path
|
|
1209
1334
|
// Handle Windows long paths with \\?\ prefix
|
|
1210
|
-
const sessionCwd = sessionData?.cwd ||
|
|
1211
|
-
const cleanSessionCwd = sessionCwd.startsWith(
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1335
|
+
const sessionCwd = sessionData?.cwd || "";
|
|
1336
|
+
const cleanSessionCwd = sessionCwd.startsWith("\\\\?\\")
|
|
1337
|
+
? sessionCwd.slice(4)
|
|
1338
|
+
: sessionCwd;
|
|
1339
|
+
const cleanProjectPath = projectPath.startsWith("\\\\?\\")
|
|
1340
|
+
? projectPath.slice(4)
|
|
1341
|
+
: projectPath;
|
|
1342
|
+
|
|
1343
|
+
if (
|
|
1344
|
+
sessionData &&
|
|
1345
|
+
(sessionData.cwd === projectPath ||
|
|
1346
|
+
cleanSessionCwd === cleanProjectPath ||
|
|
1347
|
+
path.relative(cleanSessionCwd, cleanProjectPath) === "")
|
|
1348
|
+
) {
|
|
1215
1349
|
sessions.push({
|
|
1216
1350
|
id: sessionData.id,
|
|
1217
|
-
summary: sessionData.summary ||
|
|
1351
|
+
summary: sessionData.summary || "Codex Session",
|
|
1218
1352
|
messageCount: sessionData.messageCount || 0,
|
|
1219
|
-
lastActivity: sessionData.timestamp
|
|
1353
|
+
lastActivity: sessionData.timestamp
|
|
1354
|
+
? new Date(sessionData.timestamp)
|
|
1355
|
+
: new Date(),
|
|
1220
1356
|
cwd: sessionData.cwd,
|
|
1221
1357
|
model: sessionData.model,
|
|
1222
1358
|
filePath: filePath,
|
|
1223
|
-
provider:
|
|
1359
|
+
provider: "codex",
|
|
1224
1360
|
});
|
|
1225
1361
|
}
|
|
1226
1362
|
} catch (error) {
|
|
1227
|
-
console.warn(
|
|
1363
|
+
console.warn(
|
|
1364
|
+
`Could not parse Codex session file ${filePath}:`,
|
|
1365
|
+
error.message,
|
|
1366
|
+
);
|
|
1228
1367
|
}
|
|
1229
1368
|
}
|
|
1230
1369
|
|
|
1231
1370
|
// Sort sessions by last activity (newest first)
|
|
1232
|
-
sessions.sort(
|
|
1371
|
+
sessions.sort(
|
|
1372
|
+
(a, b) => new Date(b.lastActivity) - new Date(a.lastActivity),
|
|
1373
|
+
);
|
|
1233
1374
|
|
|
1234
1375
|
// Return only the first 5 sessions for performance
|
|
1235
1376
|
return sessions.slice(0, 5);
|
|
1236
|
-
|
|
1237
1377
|
} catch (error) {
|
|
1238
|
-
console.error(
|
|
1378
|
+
console.error("Error fetching Codex sessions:", error);
|
|
1239
1379
|
return [];
|
|
1240
1380
|
}
|
|
1241
1381
|
}
|
|
@@ -1246,7 +1386,7 @@ async function parseCodexSessionFile(filePath) {
|
|
|
1246
1386
|
const fileStream = fsSync.createReadStream(filePath);
|
|
1247
1387
|
const rl = readline.createInterface({
|
|
1248
1388
|
input: fileStream,
|
|
1249
|
-
crlfDelay: Infinity
|
|
1389
|
+
crlfDelay: Infinity,
|
|
1250
1390
|
});
|
|
1251
1391
|
|
|
1252
1392
|
let sessionMeta = null;
|
|
@@ -1265,28 +1405,34 @@ async function parseCodexSessionFile(filePath) {
|
|
|
1265
1405
|
}
|
|
1266
1406
|
|
|
1267
1407
|
// Extract session metadata
|
|
1268
|
-
if (entry.type ===
|
|
1408
|
+
if (entry.type === "session_meta" && entry.payload) {
|
|
1269
1409
|
sessionMeta = {
|
|
1270
1410
|
id: entry.payload.id,
|
|
1271
1411
|
cwd: entry.payload.cwd,
|
|
1272
1412
|
model: entry.payload.model || entry.payload.model_provider,
|
|
1273
1413
|
timestamp: entry.timestamp,
|
|
1274
|
-
git: entry.payload.git
|
|
1414
|
+
git: entry.payload.git,
|
|
1275
1415
|
};
|
|
1276
1416
|
}
|
|
1277
1417
|
|
|
1278
1418
|
// Count messages and extract user messages for summary
|
|
1279
|
-
if (
|
|
1419
|
+
if (
|
|
1420
|
+
entry.type === "event_msg" &&
|
|
1421
|
+
entry.payload?.type === "user_message"
|
|
1422
|
+
) {
|
|
1280
1423
|
messageCount++;
|
|
1281
1424
|
if (entry.payload.message) {
|
|
1282
1425
|
lastUserMessage = entry.payload.message;
|
|
1283
1426
|
}
|
|
1284
1427
|
}
|
|
1285
1428
|
|
|
1286
|
-
if (
|
|
1429
|
+
if (
|
|
1430
|
+
entry.type === "response_item" &&
|
|
1431
|
+
entry.payload?.type === "message" &&
|
|
1432
|
+
entry.payload.role === "assistant"
|
|
1433
|
+
) {
|
|
1287
1434
|
messageCount++;
|
|
1288
1435
|
}
|
|
1289
|
-
|
|
1290
1436
|
} catch (parseError) {
|
|
1291
1437
|
// Skip malformed lines
|
|
1292
1438
|
}
|
|
@@ -1297,17 +1443,18 @@ async function parseCodexSessionFile(filePath) {
|
|
|
1297
1443
|
return {
|
|
1298
1444
|
...sessionMeta,
|
|
1299
1445
|
timestamp: lastTimestamp || sessionMeta.timestamp,
|
|
1300
|
-
summary: lastUserMessage
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1446
|
+
summary: lastUserMessage
|
|
1447
|
+
? lastUserMessage.length > 50
|
|
1448
|
+
? lastUserMessage.substring(0, 50) + "..."
|
|
1449
|
+
: lastUserMessage
|
|
1450
|
+
: "Codex Session",
|
|
1451
|
+
messageCount,
|
|
1304
1452
|
};
|
|
1305
1453
|
}
|
|
1306
1454
|
|
|
1307
1455
|
return null;
|
|
1308
|
-
|
|
1309
1456
|
} catch (error) {
|
|
1310
|
-
console.error(
|
|
1457
|
+
console.error("Error parsing Codex session file:", error);
|
|
1311
1458
|
return null;
|
|
1312
1459
|
}
|
|
1313
1460
|
}
|
|
@@ -1315,7 +1462,7 @@ async function parseCodexSessionFile(filePath) {
|
|
|
1315
1462
|
// Get messages for a specific Codex session
|
|
1316
1463
|
async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1317
1464
|
try {
|
|
1318
|
-
const codexSessionsDir = path.join(os.homedir(),
|
|
1465
|
+
const codexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
|
|
1319
1466
|
|
|
1320
1467
|
// Find the session file by searching for the session ID
|
|
1321
1468
|
const findSessionFile = async (dir) => {
|
|
@@ -1326,7 +1473,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1326
1473
|
if (entry.isDirectory()) {
|
|
1327
1474
|
const found = await findSessionFile(fullPath);
|
|
1328
1475
|
if (found) return found;
|
|
1329
|
-
} else if (
|
|
1476
|
+
} else if (
|
|
1477
|
+
entry.name.includes(sessionId) &&
|
|
1478
|
+
entry.name.endsWith(".jsonl")
|
|
1479
|
+
) {
|
|
1330
1480
|
return fullPath;
|
|
1331
1481
|
}
|
|
1332
1482
|
}
|
|
@@ -1348,24 +1498,24 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1348
1498
|
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
1349
1499
|
const rl = readline.createInterface({
|
|
1350
1500
|
input: fileStream,
|
|
1351
|
-
crlfDelay: Infinity
|
|
1501
|
+
crlfDelay: Infinity,
|
|
1352
1502
|
});
|
|
1353
1503
|
|
|
1354
1504
|
// Helper to extract text from Codex content array
|
|
1355
1505
|
const extractText = (content) => {
|
|
1356
1506
|
if (!Array.isArray(content)) return content;
|
|
1357
1507
|
return content
|
|
1358
|
-
.map(item => {
|
|
1359
|
-
if (item.type ===
|
|
1508
|
+
.map((item) => {
|
|
1509
|
+
if (item.type === "input_text" || item.type === "output_text") {
|
|
1360
1510
|
return item.text;
|
|
1361
1511
|
}
|
|
1362
|
-
if (item.type ===
|
|
1512
|
+
if (item.type === "text") {
|
|
1363
1513
|
return item.text;
|
|
1364
1514
|
}
|
|
1365
|
-
return
|
|
1515
|
+
return "";
|
|
1366
1516
|
})
|
|
1367
1517
|
.filter(Boolean)
|
|
1368
|
-
.join(
|
|
1518
|
+
.join("\n");
|
|
1369
1519
|
};
|
|
1370
1520
|
|
|
1371
1521
|
for await (const line of rl) {
|
|
@@ -1374,64 +1524,77 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1374
1524
|
const entry = JSON.parse(line);
|
|
1375
1525
|
|
|
1376
1526
|
// Extract token usage from token_count events (keep latest)
|
|
1377
|
-
if (
|
|
1527
|
+
if (
|
|
1528
|
+
entry.type === "event_msg" &&
|
|
1529
|
+
entry.payload?.type === "token_count" &&
|
|
1530
|
+
entry.payload?.info
|
|
1531
|
+
) {
|
|
1378
1532
|
const info = entry.payload.info;
|
|
1379
1533
|
if (info.total_token_usage) {
|
|
1380
1534
|
tokenUsage = {
|
|
1381
1535
|
used: info.total_token_usage.total_tokens || 0,
|
|
1382
|
-
total: info.model_context_window || 200000
|
|
1536
|
+
total: info.model_context_window || 200000,
|
|
1383
1537
|
};
|
|
1384
1538
|
}
|
|
1385
1539
|
}
|
|
1386
1540
|
|
|
1387
1541
|
// Extract messages from response_item
|
|
1388
|
-
if (
|
|
1542
|
+
if (
|
|
1543
|
+
entry.type === "response_item" &&
|
|
1544
|
+
entry.payload?.type === "message"
|
|
1545
|
+
) {
|
|
1389
1546
|
const content = entry.payload.content;
|
|
1390
|
-
const role = entry.payload.role ||
|
|
1547
|
+
const role = entry.payload.role || "assistant";
|
|
1391
1548
|
const textContent = extractText(content);
|
|
1392
1549
|
|
|
1393
1550
|
// Skip system context messages (environment_context)
|
|
1394
|
-
if (textContent?.includes(
|
|
1551
|
+
if (textContent?.includes("<environment_context>")) {
|
|
1395
1552
|
continue;
|
|
1396
1553
|
}
|
|
1397
1554
|
|
|
1398
1555
|
// Only add if there's actual content
|
|
1399
1556
|
if (textContent?.trim()) {
|
|
1400
1557
|
messages.push({
|
|
1401
|
-
type: role ===
|
|
1558
|
+
type: role === "user" ? "user" : "assistant",
|
|
1402
1559
|
timestamp: entry.timestamp,
|
|
1403
1560
|
message: {
|
|
1404
1561
|
role: role,
|
|
1405
|
-
content: textContent
|
|
1406
|
-
}
|
|
1562
|
+
content: textContent,
|
|
1563
|
+
},
|
|
1407
1564
|
});
|
|
1408
1565
|
}
|
|
1409
1566
|
}
|
|
1410
1567
|
|
|
1411
|
-
if (
|
|
1568
|
+
if (
|
|
1569
|
+
entry.type === "response_item" &&
|
|
1570
|
+
entry.payload?.type === "reasoning"
|
|
1571
|
+
) {
|
|
1412
1572
|
const summaryText = entry.payload.summary
|
|
1413
|
-
?.map(s => s.text)
|
|
1573
|
+
?.map((s) => s.text)
|
|
1414
1574
|
.filter(Boolean)
|
|
1415
|
-
.join(
|
|
1575
|
+
.join("\n");
|
|
1416
1576
|
if (summaryText?.trim()) {
|
|
1417
1577
|
messages.push({
|
|
1418
|
-
type:
|
|
1578
|
+
type: "thinking",
|
|
1419
1579
|
timestamp: entry.timestamp,
|
|
1420
1580
|
message: {
|
|
1421
|
-
role:
|
|
1422
|
-
content: summaryText
|
|
1423
|
-
}
|
|
1581
|
+
role: "assistant",
|
|
1582
|
+
content: summaryText,
|
|
1583
|
+
},
|
|
1424
1584
|
});
|
|
1425
1585
|
}
|
|
1426
1586
|
}
|
|
1427
1587
|
|
|
1428
|
-
if (
|
|
1588
|
+
if (
|
|
1589
|
+
entry.type === "response_item" &&
|
|
1590
|
+
entry.payload?.type === "function_call"
|
|
1591
|
+
) {
|
|
1429
1592
|
let toolName = entry.payload.name;
|
|
1430
1593
|
let toolInput = entry.payload.arguments;
|
|
1431
1594
|
|
|
1432
1595
|
// Map Codex tool names to Claude equivalents
|
|
1433
|
-
if (toolName ===
|
|
1434
|
-
toolName =
|
|
1596
|
+
if (toolName === "shell_command") {
|
|
1597
|
+
toolName = "Bash";
|
|
1435
1598
|
try {
|
|
1436
1599
|
const args = JSON.parse(entry.payload.arguments);
|
|
1437
1600
|
toolInput = JSON.stringify({ command: args.command });
|
|
@@ -1441,76 +1604,84 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1441
1604
|
}
|
|
1442
1605
|
|
|
1443
1606
|
messages.push({
|
|
1444
|
-
type:
|
|
1607
|
+
type: "tool_use",
|
|
1445
1608
|
timestamp: entry.timestamp,
|
|
1446
1609
|
toolName: toolName,
|
|
1447
1610
|
toolInput: toolInput,
|
|
1448
|
-
toolCallId: entry.payload.call_id
|
|
1611
|
+
toolCallId: entry.payload.call_id,
|
|
1449
1612
|
});
|
|
1450
1613
|
}
|
|
1451
1614
|
|
|
1452
|
-
if (
|
|
1615
|
+
if (
|
|
1616
|
+
entry.type === "response_item" &&
|
|
1617
|
+
entry.payload?.type === "function_call_output"
|
|
1618
|
+
) {
|
|
1453
1619
|
messages.push({
|
|
1454
|
-
type:
|
|
1620
|
+
type: "tool_result",
|
|
1455
1621
|
timestamp: entry.timestamp,
|
|
1456
1622
|
toolCallId: entry.payload.call_id,
|
|
1457
|
-
output: entry.payload.output
|
|
1623
|
+
output: entry.payload.output,
|
|
1458
1624
|
});
|
|
1459
1625
|
}
|
|
1460
1626
|
|
|
1461
|
-
if (
|
|
1462
|
-
|
|
1463
|
-
|
|
1627
|
+
if (
|
|
1628
|
+
entry.type === "response_item" &&
|
|
1629
|
+
entry.payload?.type === "custom_tool_call"
|
|
1630
|
+
) {
|
|
1631
|
+
const toolName = entry.payload.name || "custom_tool";
|
|
1632
|
+
const input = entry.payload.input || "";
|
|
1464
1633
|
|
|
1465
|
-
if (toolName ===
|
|
1634
|
+
if (toolName === "apply_patch") {
|
|
1466
1635
|
// Parse Codex patch format and convert to Claude Edit format
|
|
1467
1636
|
const fileMatch = input.match(/\*\*\* Update File: (.+)/);
|
|
1468
|
-
const filePath = fileMatch ? fileMatch[1].trim() :
|
|
1637
|
+
const filePath = fileMatch ? fileMatch[1].trim() : "unknown";
|
|
1469
1638
|
|
|
1470
1639
|
// Extract old and new content from patch
|
|
1471
|
-
const lines = input.split(
|
|
1640
|
+
const lines = input.split("\n");
|
|
1472
1641
|
const oldLines = [];
|
|
1473
1642
|
const newLines = [];
|
|
1474
1643
|
|
|
1475
1644
|
for (const line of lines) {
|
|
1476
|
-
if (line.startsWith(
|
|
1645
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1477
1646
|
oldLines.push(line.substring(1));
|
|
1478
|
-
} else if (line.startsWith(
|
|
1647
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1479
1648
|
newLines.push(line.substring(1));
|
|
1480
1649
|
}
|
|
1481
1650
|
}
|
|
1482
1651
|
|
|
1483
1652
|
messages.push({
|
|
1484
|
-
type:
|
|
1653
|
+
type: "tool_use",
|
|
1485
1654
|
timestamp: entry.timestamp,
|
|
1486
|
-
toolName:
|
|
1655
|
+
toolName: "Edit",
|
|
1487
1656
|
toolInput: JSON.stringify({
|
|
1488
1657
|
file_path: filePath,
|
|
1489
|
-
old_string: oldLines.join(
|
|
1490
|
-
new_string: newLines.join(
|
|
1658
|
+
old_string: oldLines.join("\n"),
|
|
1659
|
+
new_string: newLines.join("\n"),
|
|
1491
1660
|
}),
|
|
1492
|
-
toolCallId: entry.payload.call_id
|
|
1661
|
+
toolCallId: entry.payload.call_id,
|
|
1493
1662
|
});
|
|
1494
1663
|
} else {
|
|
1495
1664
|
messages.push({
|
|
1496
|
-
type:
|
|
1665
|
+
type: "tool_use",
|
|
1497
1666
|
timestamp: entry.timestamp,
|
|
1498
1667
|
toolName: toolName,
|
|
1499
1668
|
toolInput: input,
|
|
1500
|
-
toolCallId: entry.payload.call_id
|
|
1669
|
+
toolCallId: entry.payload.call_id,
|
|
1501
1670
|
});
|
|
1502
1671
|
}
|
|
1503
1672
|
}
|
|
1504
1673
|
|
|
1505
|
-
if (
|
|
1674
|
+
if (
|
|
1675
|
+
entry.type === "response_item" &&
|
|
1676
|
+
entry.payload?.type === "custom_tool_call_output"
|
|
1677
|
+
) {
|
|
1506
1678
|
messages.push({
|
|
1507
|
-
type:
|
|
1679
|
+
type: "tool_result",
|
|
1508
1680
|
timestamp: entry.timestamp,
|
|
1509
1681
|
toolCallId: entry.payload.call_id,
|
|
1510
|
-
output: entry.payload.output ||
|
|
1682
|
+
output: entry.payload.output || "",
|
|
1511
1683
|
});
|
|
1512
1684
|
}
|
|
1513
|
-
|
|
1514
1685
|
} catch (parseError) {
|
|
1515
1686
|
// Skip malformed lines
|
|
1516
1687
|
}
|
|
@@ -1518,7 +1689,9 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1518
1689
|
}
|
|
1519
1690
|
|
|
1520
1691
|
// Sort by timestamp
|
|
1521
|
-
messages.sort(
|
|
1692
|
+
messages.sort(
|
|
1693
|
+
(a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0),
|
|
1694
|
+
);
|
|
1522
1695
|
|
|
1523
1696
|
const total = messages.length;
|
|
1524
1697
|
|
|
@@ -1535,21 +1708,23 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1535
1708
|
hasMore,
|
|
1536
1709
|
offset,
|
|
1537
1710
|
limit,
|
|
1538
|
-
tokenUsage
|
|
1711
|
+
tokenUsage,
|
|
1539
1712
|
};
|
|
1540
1713
|
}
|
|
1541
1714
|
|
|
1542
1715
|
return { messages, tokenUsage };
|
|
1543
|
-
|
|
1544
1716
|
} catch (error) {
|
|
1545
|
-
console.error(
|
|
1717
|
+
console.error(
|
|
1718
|
+
`Error reading Codex session messages for ${sessionId}:`,
|
|
1719
|
+
error,
|
|
1720
|
+
);
|
|
1546
1721
|
return { messages: [], total: 0, hasMore: false };
|
|
1547
1722
|
}
|
|
1548
1723
|
}
|
|
1549
1724
|
|
|
1550
1725
|
async function deleteCodexSession(sessionId) {
|
|
1551
1726
|
try {
|
|
1552
|
-
const codexSessionsDir = path.join(os.homedir(),
|
|
1727
|
+
const codexSessionsDir = path.join(os.homedir(), ".codex", "sessions");
|
|
1553
1728
|
|
|
1554
1729
|
const findJsonlFiles = async (dir) => {
|
|
1555
1730
|
const files = [];
|
|
@@ -1558,8 +1733,8 @@ async function deleteCodexSession(sessionId) {
|
|
|
1558
1733
|
for (const entry of entries) {
|
|
1559
1734
|
const fullPath = path.join(dir, entry.name);
|
|
1560
1735
|
if (entry.isDirectory()) {
|
|
1561
|
-
files.push(...await findJsonlFiles(fullPath));
|
|
1562
|
-
} else if (entry.name.endsWith(
|
|
1736
|
+
files.push(...(await findJsonlFiles(fullPath)));
|
|
1737
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
1563
1738
|
files.push(fullPath);
|
|
1564
1739
|
}
|
|
1565
1740
|
}
|
|
@@ -1584,6 +1759,126 @@ async function deleteCodexSession(sessionId) {
|
|
|
1584
1759
|
}
|
|
1585
1760
|
}
|
|
1586
1761
|
|
|
1762
|
+
/**
|
|
1763
|
+
* Get full project details for a specific project by name
|
|
1764
|
+
* Used for on-demand loading when a project is expanded in the sidebar
|
|
1765
|
+
*/
|
|
1766
|
+
async function getProjectDetailFull(projectName) {
|
|
1767
|
+
const claudeDir = path.join(os.homedir(), ".claude", "projects");
|
|
1768
|
+
const config = await loadProjectConfig();
|
|
1769
|
+
|
|
1770
|
+
// Check if it's a manually added project
|
|
1771
|
+
const isManuallyAdded = config[projectName]?.manuallyAdded || false;
|
|
1772
|
+
|
|
1773
|
+
// Determine the project directory
|
|
1774
|
+
let projectPath;
|
|
1775
|
+
let actualProjectDir;
|
|
1776
|
+
|
|
1777
|
+
if (isManuallyAdded) {
|
|
1778
|
+
actualProjectDir = config[projectName]?.originalPath;
|
|
1779
|
+
if (!actualProjectDir) {
|
|
1780
|
+
actualProjectDir = await extractProjectDirectory(projectName);
|
|
1781
|
+
}
|
|
1782
|
+
} else {
|
|
1783
|
+
projectPath = path.join(claudeDir, projectName);
|
|
1784
|
+
|
|
1785
|
+
// Check if project directory exists
|
|
1786
|
+
try {
|
|
1787
|
+
await fs.access(projectPath);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
if (error.code === "ENOENT") {
|
|
1790
|
+
return null; // Project not found
|
|
1791
|
+
}
|
|
1792
|
+
throw error;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
actualProjectDir = await extractProjectDirectory(projectName);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Build the project object
|
|
1799
|
+
const customName = config[projectName]?.displayName;
|
|
1800
|
+
const autoDisplayName = await generateDisplayName(
|
|
1801
|
+
projectName,
|
|
1802
|
+
actualProjectDir,
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
const project = {
|
|
1806
|
+
name: projectName,
|
|
1807
|
+
path: actualProjectDir,
|
|
1808
|
+
displayName: customName || autoDisplayName,
|
|
1809
|
+
fullPath: actualProjectDir,
|
|
1810
|
+
isCustomName: !!customName,
|
|
1811
|
+
isManuallyAdded,
|
|
1812
|
+
sessions: [],
|
|
1813
|
+
cursorSessions: [],
|
|
1814
|
+
codexSessions: [],
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
// Fetch all Claude sessions (no limit)
|
|
1818
|
+
if (!isManuallyAdded) {
|
|
1819
|
+
try {
|
|
1820
|
+
const sessionResult = await getSessions(projectName, 100, 0); // Higher limit for detail view
|
|
1821
|
+
project.sessions = sessionResult.sessions || [];
|
|
1822
|
+
project.sessionMeta = {
|
|
1823
|
+
hasMore: sessionResult.hasMore,
|
|
1824
|
+
total: sessionResult.total,
|
|
1825
|
+
};
|
|
1826
|
+
} catch (e) {
|
|
1827
|
+
console.warn(
|
|
1828
|
+
`Could not load sessions for project ${projectName}:`,
|
|
1829
|
+
e.message,
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Fetch Cursor sessions
|
|
1835
|
+
try {
|
|
1836
|
+
project.cursorSessions = await getCursorSessions(actualProjectDir);
|
|
1837
|
+
} catch (e) {
|
|
1838
|
+
console.warn(
|
|
1839
|
+
`Could not load Cursor sessions for project ${projectName}:`,
|
|
1840
|
+
e.message,
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// Fetch Codex sessions
|
|
1845
|
+
try {
|
|
1846
|
+
project.codexSessions = await getCodexSessions(actualProjectDir);
|
|
1847
|
+
} catch (e) {
|
|
1848
|
+
console.warn(
|
|
1849
|
+
`Could not load Codex sessions for project ${projectName}:`,
|
|
1850
|
+
e.message,
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Fetch TaskMaster data
|
|
1855
|
+
try {
|
|
1856
|
+
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
1857
|
+
project.taskmaster = {
|
|
1858
|
+
hasTaskmaster: taskMasterResult.hasTaskmaster,
|
|
1859
|
+
hasEssentialFiles: taskMasterResult.hasEssentialFiles,
|
|
1860
|
+
metadata: taskMasterResult.metadata,
|
|
1861
|
+
status:
|
|
1862
|
+
taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles
|
|
1863
|
+
? "configured"
|
|
1864
|
+
: "not-configured",
|
|
1865
|
+
};
|
|
1866
|
+
} catch (e) {
|
|
1867
|
+
console.warn(
|
|
1868
|
+
`Could not detect TaskMaster for project ${projectName}:`,
|
|
1869
|
+
e.message,
|
|
1870
|
+
);
|
|
1871
|
+
project.taskmaster = {
|
|
1872
|
+
hasTaskmaster: false,
|
|
1873
|
+
hasEssentialFiles: false,
|
|
1874
|
+
metadata: null,
|
|
1875
|
+
status: "error",
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
return project;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1587
1882
|
export {
|
|
1588
1883
|
getProjects,
|
|
1589
1884
|
getSessions,
|
|
@@ -1600,5 +1895,6 @@ export {
|
|
|
1600
1895
|
clearProjectDirectoryCache,
|
|
1601
1896
|
getCodexSessions,
|
|
1602
1897
|
getCodexSessionMessages,
|
|
1603
|
-
deleteCodexSession
|
|
1898
|
+
deleteCodexSession,
|
|
1899
|
+
getProjectDetailFull,
|
|
1604
1900
|
};
|