@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.5
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/bin/copilot-session-viewer +2 -2
- package/dist/server.min.js +99 -0
- package/package.json +5 -17
- package/public/vendor/marked.umd.min.js +8 -0
- package/public/vendor/purify.min.js +3 -0
- package/public/vendor/vue-virtual-scroller.css +1 -0
- package/public/vendor/vue-virtual-scroller.min.js +2 -0
- package/public/vendor/vue.global.prod.min.js +19 -0
- package/views/session-vue.ejs +5 -5
- package/views/time-analyze.ejs +2 -2
- package/lib/parsers/README.md +0 -239
- package/lib/parsers/base-parser.js +0 -53
- package/lib/parsers/claude-parser.js +0 -181
- package/lib/parsers/copilot-parser.js +0 -143
- package/lib/parsers/index.js +0 -15
- package/lib/parsers/parser-factory.js +0 -77
- package/lib/parsers/pi-mono-parser.js +0 -119
- package/lib/parsers/vscode-parser.js +0 -591
- package/server.js +0 -29
- package/src/app.js +0 -129
- package/src/config/index.js +0 -27
- package/src/controllers/insightController.js +0 -136
- package/src/controllers/sessionController.js +0 -449
- package/src/controllers/tagController.js +0 -113
- package/src/controllers/uploadController.js +0 -648
- package/src/middleware/common.js +0 -67
- package/src/middleware/rateLimiting.js +0 -62
- package/src/models/Session.js +0 -146
- package/src/routes/api.js +0 -11
- package/src/routes/insights.js +0 -12
- package/src/routes/pages.js +0 -12
- package/src/routes/uploads.js +0 -14
- package/src/schemas/event.schema.js +0 -73
- package/src/services/eventNormalizer.js +0 -291
- package/src/services/insightService.js +0 -535
- package/src/services/sessionRepository.js +0 -1092
- package/src/services/sessionService.js +0 -1919
- package/src/services/tagService.js +0 -205
- package/src/telemetry.js +0 -152
- package/src/utils/fileUtils.js +0 -305
- package/src/utils/helpers.js +0 -45
- package/src/utils/processManager.js +0 -85
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const multer = require('multer');
|
|
5
|
-
const { spawn } = require('child_process');
|
|
6
|
-
const { isValidSessionId } = require('../utils/helpers');
|
|
7
|
-
const { trackEvent, trackException } = require('../telemetry');
|
|
8
|
-
const processManager = require('../utils/processManager');
|
|
9
|
-
const config = require('../config');
|
|
10
|
-
|
|
11
|
-
class UploadController {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.SESSION_DIR = process.env.SESSION_DIR || path.join(os.homedir(), '.copilot', 'session-state');
|
|
14
|
-
this.uploadDir = process.env.UPLOAD_DIR || path.join(os.tmpdir(), 'copilot-session-uploads');
|
|
15
|
-
|
|
16
|
-
// Multi-format session directories
|
|
17
|
-
this.SESSION_DIRS = {
|
|
18
|
-
copilot: this.SESSION_DIR,
|
|
19
|
-
claude: path.join(os.homedir(), '.claude', 'projects'),
|
|
20
|
-
'pi-mono': path.join(os.homedir(), '.pi', 'agent', 'sessions')
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Don't create uploadDir here - multer's DiskStorage will handle it
|
|
24
|
-
// This avoids EEXIST errors when multiple tests run in parallel
|
|
25
|
-
this.upload = this.createMulterInstance();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
createMulterInstance() {
|
|
29
|
-
return multer({
|
|
30
|
-
dest: this.uploadDir,
|
|
31
|
-
limits: { fileSize: config.MAX_UPLOAD_SIZE },
|
|
32
|
-
fileFilter: (req, file, cb) => {
|
|
33
|
-
// Check both file extension and MIME type
|
|
34
|
-
const isZipExtension = file.originalname.toLowerCase().endsWith('.zip');
|
|
35
|
-
const isZipMime = file.mimetype === 'application/zip' ||
|
|
36
|
-
file.mimetype === 'application/x-zip-compressed';
|
|
37
|
-
|
|
38
|
-
if (!isZipExtension || !isZipMime) {
|
|
39
|
-
return cb(new Error('Only .zip files are allowed'));
|
|
40
|
-
}
|
|
41
|
-
cb(null, true);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Share session (export as zip)
|
|
47
|
-
async shareSession(req, res) {
|
|
48
|
-
try {
|
|
49
|
-
const sessionId = req.params.id;
|
|
50
|
-
|
|
51
|
-
if (!isValidSessionId(sessionId)) {
|
|
52
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const sessionPath = path.join(this.SESSION_DIR, sessionId);
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
await fs.promises.access(sessionPath);
|
|
59
|
-
} catch (_err) {
|
|
60
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const zipFile = path.join(os.tmpdir(), `session-${sessionId}.zip`);
|
|
64
|
-
|
|
65
|
-
const zipProcess = spawn('zip', ['-r', '-q', zipFile, sessionId], {
|
|
66
|
-
cwd: this.SESSION_DIR
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
processManager.register(zipProcess, { name: `zip-${sessionId}` });
|
|
70
|
-
|
|
71
|
-
zipProcess.on('close', (code) => {
|
|
72
|
-
if (code !== 0) {
|
|
73
|
-
return res.status(500).json({ error: 'Failed to create zip file' });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Track SessionShared event
|
|
77
|
-
trackEvent('SessionShared', { sessionId });
|
|
78
|
-
|
|
79
|
-
res.download(zipFile, `session-${sessionId}.zip`, (err) => {
|
|
80
|
-
fs.promises.unlink(zipFile).catch(() => {});
|
|
81
|
-
if (err) {
|
|
82
|
-
console.error('Error sending zip:', err);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
zipProcess.on('error', (err) => {
|
|
88
|
-
console.error('Error creating zip:', err);
|
|
89
|
-
res.status(500).json({ error: 'Failed to create zip file' });
|
|
90
|
-
});
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.error('Error sharing session:', err);
|
|
93
|
-
res.status(500).json({ error: 'Error sharing session' });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Import session from zip (with validation)
|
|
98
|
-
async importSession(req, res) {
|
|
99
|
-
try {
|
|
100
|
-
if (!req.file) {
|
|
101
|
-
return res.status(400).json({ error: 'No file uploaded' });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const zipPath = req.file.path;
|
|
105
|
-
const extractDir = path.join(this.uploadDir, `extract-${Date.now()}`);
|
|
106
|
-
|
|
107
|
-
await fs.promises.mkdir(extractDir, { recursive: true });
|
|
108
|
-
|
|
109
|
-
// ZIP bomb protection: Check compressed file size first
|
|
110
|
-
const MAX_COMPRESSED_SIZE = 50 * 1024 * 1024; // 50MB (already enforced by multer)
|
|
111
|
-
const MAX_UNCOMPRESSED_SIZE = 200 * 1024 * 1024; // 200MB
|
|
112
|
-
const MAX_FILE_COUNT = 1000; // Maximum number of files
|
|
113
|
-
const MAX_DEPTH = 5; // Maximum directory nesting depth
|
|
114
|
-
|
|
115
|
-
const stats = await fs.promises.stat(zipPath);
|
|
116
|
-
if (stats.size > MAX_COMPRESSED_SIZE) {
|
|
117
|
-
await fs.promises.unlink(zipPath);
|
|
118
|
-
return res.status(400).json({ error: 'Compressed file too large (max 50MB)' });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// First pass: List zip contents without extracting to check for bombs
|
|
122
|
-
const listProcess = spawn('unzip', ['-l', zipPath]);
|
|
123
|
-
let listOutput = '';
|
|
124
|
-
|
|
125
|
-
listProcess.stdout.on('data', (data) => {
|
|
126
|
-
listOutput += data.toString();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
await new Promise((resolve, reject) => {
|
|
130
|
-
listProcess.on('close', (code) => {
|
|
131
|
-
if (code !== 0) {
|
|
132
|
-
reject(new Error('Failed to list zip contents'));
|
|
133
|
-
} else {
|
|
134
|
-
resolve();
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
listProcess.on('error', reject);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Parse unzip output to check total size and file count
|
|
141
|
-
const lines = listOutput.split('\n');
|
|
142
|
-
let totalUncompressedSize = 0;
|
|
143
|
-
let fileCount = 0;
|
|
144
|
-
let maxDepth = 0;
|
|
145
|
-
|
|
146
|
-
for (const line of lines) {
|
|
147
|
-
const match = line.trim().match(/^\s*(\d+)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+(.+)$/);
|
|
148
|
-
if (match) {
|
|
149
|
-
const size = parseInt(match[1]);
|
|
150
|
-
const filename = match[2];
|
|
151
|
-
totalUncompressedSize += size;
|
|
152
|
-
fileCount++;
|
|
153
|
-
|
|
154
|
-
// Check directory depth
|
|
155
|
-
const depth = (filename.match(/\//g) || []).length;
|
|
156
|
-
maxDepth = Math.max(maxDepth, depth);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Validate against ZIP bomb thresholds
|
|
161
|
-
if (totalUncompressedSize > MAX_UNCOMPRESSED_SIZE) {
|
|
162
|
-
await fs.promises.unlink(zipPath);
|
|
163
|
-
return res.status(400).json({
|
|
164
|
-
error: `Uncompressed size too large (${Math.round(totalUncompressedSize / 1024 / 1024)}MB > ${MAX_UNCOMPRESSED_SIZE / 1024 / 1024}MB)`
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (fileCount > MAX_FILE_COUNT) {
|
|
169
|
-
await fs.promises.unlink(zipPath);
|
|
170
|
-
return res.status(400).json({
|
|
171
|
-
error: `Too many files in archive (${fileCount} > ${MAX_FILE_COUNT})`
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (maxDepth > MAX_DEPTH) {
|
|
176
|
-
await fs.promises.unlink(zipPath);
|
|
177
|
-
return res.status(400).json({
|
|
178
|
-
error: `Directory nesting too deep (${maxDepth} > ${MAX_DEPTH})`
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// If all checks pass, proceed with extraction
|
|
183
|
-
const unzipProcess = spawn('unzip', ['-q', zipPath, '-d', extractDir]);
|
|
184
|
-
|
|
185
|
-
processManager.register(unzipProcess, { name: 'unzip-import' });
|
|
186
|
-
|
|
187
|
-
unzipProcess.on('close', async (code) => {
|
|
188
|
-
let sessionDirName; // Declare here for access in catch block
|
|
189
|
-
try {
|
|
190
|
-
await fs.promises.unlink(zipPath);
|
|
191
|
-
|
|
192
|
-
if (code !== 0) {
|
|
193
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true });
|
|
194
|
-
return res.status(500).json({ error: 'Failed to extract zip file' });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const entries = await fs.promises.readdir(extractDir);
|
|
198
|
-
if (entries.length === 0) {
|
|
199
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true });
|
|
200
|
-
return res.status(400).json({ error: 'Empty zip file' });
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
sessionDirName = entries[0];
|
|
204
|
-
|
|
205
|
-
// Validate session directory name to prevent Zip Slip path traversal
|
|
206
|
-
if (!isValidSessionId(sessionDirName)) {
|
|
207
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true });
|
|
208
|
-
return res.status(400).json({ error: 'Invalid session directory name in zip file' });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const sessionPath = path.join(extractDir, sessionDirName);
|
|
212
|
-
const targetPath = path.join(this.SESSION_DIR, sessionDirName);
|
|
213
|
-
|
|
214
|
-
const eventsFile = path.join(sessionPath, 'events.jsonl');
|
|
215
|
-
try {
|
|
216
|
-
await fs.promises.access(eventsFile);
|
|
217
|
-
} catch (_err) {
|
|
218
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true });
|
|
219
|
-
return res.status(400).json({ error: 'Invalid session structure (no events.jsonl)' });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (fs.existsSync(targetPath)) {
|
|
223
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true });
|
|
224
|
-
return res.status(409).json({ error: 'Session already exists' });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
await fs.promises.rename(sessionPath, targetPath);
|
|
228
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true });
|
|
229
|
-
|
|
230
|
-
// Track SessionImported event
|
|
231
|
-
const stats = await fs.promises.stat(zipPath).catch(() => ({ size: 0 }));
|
|
232
|
-
trackEvent('SessionImported', {
|
|
233
|
-
format: 'copilot',
|
|
234
|
-
fileSize: stats.size.toString()
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
res.json({ success: true, sessionId: sessionDirName });
|
|
238
|
-
} catch (err) {
|
|
239
|
-
console.error('Error importing session:', err);
|
|
240
|
-
|
|
241
|
-
// Track import failure
|
|
242
|
-
trackException(err, {
|
|
243
|
-
operation: 'importSession',
|
|
244
|
-
sessionId: sessionDirName || 'unknown'
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true }).catch(() => {});
|
|
248
|
-
res.status(500).json({ error: 'Error importing session' });
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
unzipProcess.on('error', async (err) => {
|
|
253
|
-
console.error('Error extracting zip:', err);
|
|
254
|
-
|
|
255
|
-
// Track upload/extraction failure
|
|
256
|
-
trackException(err, {
|
|
257
|
-
operation: 'importSession_unzip'
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
await fs.promises.unlink(zipPath).catch(() => {});
|
|
261
|
-
await fs.promises.rm(extractDir, { recursive: true, force: true }).catch(() => {});
|
|
262
|
-
res.status(500).json({ error: 'Failed to extract zip file' });
|
|
263
|
-
});
|
|
264
|
-
} catch (err) {
|
|
265
|
-
console.error('Error processing upload:', err);
|
|
266
|
-
|
|
267
|
-
// Track upload processing failure
|
|
268
|
-
trackException(err, {
|
|
269
|
-
operation: 'importSession_upload'
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
if (req.file) {
|
|
273
|
-
await fs.promises.unlink(req.file.path).catch(() => {});
|
|
274
|
-
}
|
|
275
|
-
res.status(500).json({ error: 'Error processing upload' });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Multer middleware accessor
|
|
280
|
-
getUploadMiddleware() {
|
|
281
|
-
return this.upload.single('zipFile');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Detect the format of a session from extracted directory
|
|
286
|
-
* @param {string} extractDir - Directory containing extracted session files
|
|
287
|
-
* @returns {Promise<Object|null>} Format information or null if unknown
|
|
288
|
-
*/
|
|
289
|
-
async _detectFormat(extractDir) {
|
|
290
|
-
try {
|
|
291
|
-
const entries = await fs.promises.readdir(extractDir);
|
|
292
|
-
|
|
293
|
-
if (entries.length === 0) {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Check for Pi-Mono format: timestamped filename pattern YYYY-MM-DDTHH-MM-SS-SSSZ_sessionId.jsonl
|
|
298
|
-
const piMonoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_([a-zA-Z0-9_-]+)\.jsonl$/;
|
|
299
|
-
for (const entry of entries) {
|
|
300
|
-
const match = entry.match(piMonoPattern);
|
|
301
|
-
if (match) {
|
|
302
|
-
return {
|
|
303
|
-
format: 'pi-mono',
|
|
304
|
-
sessionId: match[1],
|
|
305
|
-
fileName: entry,
|
|
306
|
-
extractDir
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Check for Copilot format: directory with events.jsonl
|
|
312
|
-
for (const entry of entries) {
|
|
313
|
-
const entryPath = path.join(extractDir, entry);
|
|
314
|
-
const stat = await fs.promises.stat(entryPath);
|
|
315
|
-
if (stat.isDirectory()) {
|
|
316
|
-
const eventsFile = path.join(entryPath, 'events.jsonl');
|
|
317
|
-
if (fs.existsSync(eventsFile)) {
|
|
318
|
-
return {
|
|
319
|
-
format: 'copilot',
|
|
320
|
-
sessionId: entry,
|
|
321
|
-
directoryName: entry,
|
|
322
|
-
extractDir
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Check for Claude format: uuid.jsonl file
|
|
329
|
-
const claudePattern = /^([a-zA-Z0-9_-]+)\.jsonl$/;
|
|
330
|
-
for (const entry of entries) {
|
|
331
|
-
const entryPath = path.join(extractDir, entry);
|
|
332
|
-
const stat = await fs.promises.stat(entryPath);
|
|
333
|
-
|
|
334
|
-
if (stat.isFile()) {
|
|
335
|
-
const match = entry.match(claudePattern);
|
|
336
|
-
if (match) {
|
|
337
|
-
const sessionId = match[1];
|
|
338
|
-
// Check if there's an optional directory with the same name
|
|
339
|
-
const sessionDir = path.join(extractDir, sessionId);
|
|
340
|
-
const hasDirectory = fs.existsSync(sessionDir);
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
format: 'claude',
|
|
344
|
-
sessionId,
|
|
345
|
-
fileName: entry,
|
|
346
|
-
hasDirectory,
|
|
347
|
-
directoryName: hasDirectory ? sessionId : undefined,
|
|
348
|
-
extractDir
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return null;
|
|
355
|
-
} catch (err) {
|
|
356
|
-
console.error('Error detecting format:', err);
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Import Copilot format session
|
|
363
|
-
* @param {Object} formatInfo - Format detection result
|
|
364
|
-
* @param {string} extractDir - Extraction directory
|
|
365
|
-
* @returns {Promise<Object>} Import result
|
|
366
|
-
*/
|
|
367
|
-
async _importCopilotSession(formatInfo, extractDir) {
|
|
368
|
-
try {
|
|
369
|
-
const { sessionId, directoryName } = formatInfo;
|
|
370
|
-
|
|
371
|
-
// Validate session ID
|
|
372
|
-
if (!isValidSessionId(sessionId)) {
|
|
373
|
-
return {
|
|
374
|
-
success: false,
|
|
375
|
-
error: 'Invalid session ID',
|
|
376
|
-
statusCode: 400
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const sessionPath = path.join(extractDir, directoryName);
|
|
381
|
-
const targetPath = path.join(this.SESSION_DIRS.copilot, sessionId);
|
|
382
|
-
|
|
383
|
-
// Check for events.jsonl
|
|
384
|
-
const eventsFile = path.join(sessionPath, 'events.jsonl');
|
|
385
|
-
if (!fs.existsSync(eventsFile)) {
|
|
386
|
-
return {
|
|
387
|
-
success: false,
|
|
388
|
-
error: 'Invalid session structure (no events.jsonl)',
|
|
389
|
-
statusCode: 400
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Check if session already exists
|
|
394
|
-
if (fs.existsSync(targetPath)) {
|
|
395
|
-
return {
|
|
396
|
-
success: false,
|
|
397
|
-
error: 'Session already exists',
|
|
398
|
-
statusCode: 409
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Move session directory
|
|
403
|
-
await fs.promises.rename(sessionPath, targetPath);
|
|
404
|
-
|
|
405
|
-
// Mark as imported
|
|
406
|
-
await fs.promises.writeFile(path.join(targetPath, '.imported'), '');
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
success: true,
|
|
410
|
-
sessionId,
|
|
411
|
-
format: 'copilot'
|
|
412
|
-
};
|
|
413
|
-
} catch (err) {
|
|
414
|
-
console.error('Error importing Copilot session:', err);
|
|
415
|
-
return {
|
|
416
|
-
success: false,
|
|
417
|
-
error: `Error importing Copilot session: ${err.message}`,
|
|
418
|
-
statusCode: 500
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Import Claude format session
|
|
425
|
-
* @param {Object} formatInfo - Format detection result
|
|
426
|
-
* @param {string} extractDir - Extraction directory
|
|
427
|
-
* @param {Object} req - Express request object
|
|
428
|
-
* @returns {Promise<Object>} Import result
|
|
429
|
-
*/
|
|
430
|
-
async _importClaudeSession(formatInfo, extractDir, req) {
|
|
431
|
-
try {
|
|
432
|
-
const { sessionId, fileName, hasDirectory, directoryName } = formatInfo;
|
|
433
|
-
|
|
434
|
-
// Validate session ID
|
|
435
|
-
if (!isValidSessionId(sessionId)) {
|
|
436
|
-
return {
|
|
437
|
-
success: false,
|
|
438
|
-
error: 'Invalid session ID',
|
|
439
|
-
statusCode: 400
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Get project from query or use default
|
|
444
|
-
const project = req.query.project || 'imported-sessions';
|
|
445
|
-
|
|
446
|
-
// Create project directory
|
|
447
|
-
const projectPath = path.join(this.SESSION_DIRS.claude, project);
|
|
448
|
-
await fs.promises.mkdir(projectPath, { recursive: true });
|
|
449
|
-
|
|
450
|
-
// Move the .jsonl file
|
|
451
|
-
const sourceFile = path.join(extractDir, fileName);
|
|
452
|
-
const targetFile = path.join(projectPath, fileName);
|
|
453
|
-
await fs.promises.rename(sourceFile, targetFile);
|
|
454
|
-
|
|
455
|
-
// If there's a directory, move it too
|
|
456
|
-
if (hasDirectory && directoryName) {
|
|
457
|
-
const sourceDir = path.join(extractDir, directoryName);
|
|
458
|
-
const targetDir = path.join(projectPath, directoryName);
|
|
459
|
-
await fs.promises.rename(sourceDir, targetDir);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return {
|
|
463
|
-
success: true,
|
|
464
|
-
sessionId,
|
|
465
|
-
format: 'claude',
|
|
466
|
-
project
|
|
467
|
-
};
|
|
468
|
-
} catch (err) {
|
|
469
|
-
console.error('Error importing Claude session:', err);
|
|
470
|
-
return {
|
|
471
|
-
success: false,
|
|
472
|
-
error: `Error importing Claude session: ${err.message}`,
|
|
473
|
-
statusCode: 500
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Import Pi-Mono format session
|
|
480
|
-
* @param {Object} formatInfo - Format detection result
|
|
481
|
-
* @param {string} extractDir - Extraction directory
|
|
482
|
-
* @param {Object} req - Express request object
|
|
483
|
-
* @returns {Promise<Object>} Import result
|
|
484
|
-
*/
|
|
485
|
-
async _importPiMonoSession(formatInfo, extractDir, req) {
|
|
486
|
-
try {
|
|
487
|
-
const { sessionId, fileName } = formatInfo;
|
|
488
|
-
|
|
489
|
-
// Validate session ID
|
|
490
|
-
if (!isValidSessionId(sessionId)) {
|
|
491
|
-
return {
|
|
492
|
-
success: false,
|
|
493
|
-
error: 'Invalid session ID',
|
|
494
|
-
statusCode: 400
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Get project from query or use default
|
|
499
|
-
const project = req.query.project || 'imported-sessions';
|
|
500
|
-
|
|
501
|
-
// Create project directory
|
|
502
|
-
const projectPath = path.join(this.SESSION_DIRS['pi-mono'], project);
|
|
503
|
-
await fs.promises.mkdir(projectPath, { recursive: true });
|
|
504
|
-
|
|
505
|
-
// Move the .jsonl file
|
|
506
|
-
const sourceFile = path.join(extractDir, fileName);
|
|
507
|
-
const targetFile = path.join(projectPath, fileName);
|
|
508
|
-
await fs.promises.rename(sourceFile, targetFile);
|
|
509
|
-
|
|
510
|
-
return {
|
|
511
|
-
success: true,
|
|
512
|
-
sessionId,
|
|
513
|
-
format: 'pi-mono',
|
|
514
|
-
project
|
|
515
|
-
};
|
|
516
|
-
} catch (err) {
|
|
517
|
-
console.error('Error importing Pi-Mono session:', err);
|
|
518
|
-
return {
|
|
519
|
-
success: false,
|
|
520
|
-
error: `Error importing Pi-Mono session: ${err.message}`,
|
|
521
|
-
statusCode: 500
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Import session by detected format
|
|
528
|
-
* @param {Object} formatInfo - Format detection result
|
|
529
|
-
* @param {string} extractDir - Extraction directory
|
|
530
|
-
* @param {Object} req - Express request object
|
|
531
|
-
* @returns {Promise<Object>} Import result
|
|
532
|
-
*/
|
|
533
|
-
async _importByFormat(formatInfo, extractDir, req) {
|
|
534
|
-
// Validate session ID
|
|
535
|
-
if (!isValidSessionId(formatInfo.sessionId)) {
|
|
536
|
-
return {
|
|
537
|
-
success: false,
|
|
538
|
-
error: 'Invalid session ID',
|
|
539
|
-
statusCode: 400
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
switch (formatInfo.format) {
|
|
544
|
-
case 'copilot':
|
|
545
|
-
return await this._importCopilotSession(formatInfo, extractDir);
|
|
546
|
-
case 'claude':
|
|
547
|
-
return await this._importClaudeSession(formatInfo, extractDir, req);
|
|
548
|
-
case 'pi-mono':
|
|
549
|
-
return await this._importPiMonoSession(formatInfo, extractDir, req);
|
|
550
|
-
default:
|
|
551
|
-
return {
|
|
552
|
-
success: false,
|
|
553
|
-
error: `Unsupported format: ${formatInfo.format}`,
|
|
554
|
-
statusCode: 400
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Find session location across all session directories
|
|
561
|
-
* @param {string} sessionId - Session identifier
|
|
562
|
-
* @param {string} preferredSource - Preferred source to search first
|
|
563
|
-
* @returns {Promise<Object|null>} Session location info or null
|
|
564
|
-
*/
|
|
565
|
-
async _findSessionLocation(sessionId, preferredSource = null) {
|
|
566
|
-
try {
|
|
567
|
-
// Define search order based on preference
|
|
568
|
-
const sources = preferredSource
|
|
569
|
-
? [preferredSource, ...Object.keys(this.SESSION_DIRS).filter(s => s !== preferredSource)]
|
|
570
|
-
: Object.keys(this.SESSION_DIRS);
|
|
571
|
-
|
|
572
|
-
for (const source of sources) {
|
|
573
|
-
const baseDir = this.SESSION_DIRS[source];
|
|
574
|
-
|
|
575
|
-
if (source === 'copilot') {
|
|
576
|
-
// For Copilot, sessions are directly in SESSION_DIR
|
|
577
|
-
const sessionPath = path.join(baseDir, sessionId);
|
|
578
|
-
if (fs.existsSync(sessionPath)) {
|
|
579
|
-
const eventsFile = path.join(sessionPath, 'events.jsonl');
|
|
580
|
-
if (fs.existsSync(eventsFile)) {
|
|
581
|
-
return {
|
|
582
|
-
source: 'copilot',
|
|
583
|
-
sessionId,
|
|
584
|
-
sessionPath,
|
|
585
|
-
baseDir
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
} else if (source === 'claude') {
|
|
590
|
-
// For Claude, search in all project directories
|
|
591
|
-
if (fs.existsSync(baseDir)) {
|
|
592
|
-
const projects = await fs.promises.readdir(baseDir);
|
|
593
|
-
for (const project of projects) {
|
|
594
|
-
const projectPath = path.join(baseDir, project);
|
|
595
|
-
const stat = await fs.promises.stat(projectPath);
|
|
596
|
-
if (stat.isDirectory()) {
|
|
597
|
-
const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
|
|
598
|
-
if (fs.existsSync(sessionFile)) {
|
|
599
|
-
return {
|
|
600
|
-
source: 'claude',
|
|
601
|
-
sessionId,
|
|
602
|
-
sessionFile,
|
|
603
|
-
projectPath,
|
|
604
|
-
project,
|
|
605
|
-
baseDir
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
} else if (source === 'pi-mono') {
|
|
612
|
-
// For Pi-Mono, search in all project directories for timestamped files
|
|
613
|
-
if (fs.existsSync(baseDir)) {
|
|
614
|
-
const projects = await fs.promises.readdir(baseDir);
|
|
615
|
-
for (const project of projects) {
|
|
616
|
-
const projectPath = path.join(baseDir, project);
|
|
617
|
-
const stat = await fs.promises.stat(projectPath);
|
|
618
|
-
if (stat.isDirectory()) {
|
|
619
|
-
const files = await fs.promises.readdir(projectPath);
|
|
620
|
-
const piMonoPattern = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z_${sessionId}\\.jsonl$`);
|
|
621
|
-
for (const file of files) {
|
|
622
|
-
if (piMonoPattern.test(file)) {
|
|
623
|
-
return {
|
|
624
|
-
source: 'pi-mono',
|
|
625
|
-
sessionId,
|
|
626
|
-
fileName: file,
|
|
627
|
-
sessionFile: path.join(projectPath, file),
|
|
628
|
-
projectPath,
|
|
629
|
-
project,
|
|
630
|
-
baseDir
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return null;
|
|
641
|
-
} catch (err) {
|
|
642
|
-
console.error('Error finding session location:', err);
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
module.exports = UploadController;
|