zuppaclaude 1.0.0 → 1.0.1
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/zuppaclaude.js +71 -1
- package/lib/components/index.js +3 -1
- package/lib/components/session.js +425 -0
- package/package.json +1 -1
package/bin/zuppaclaude.js
CHANGED
|
@@ -71,6 +71,49 @@ async function main() {
|
|
|
71
71
|
}
|
|
72
72
|
break;
|
|
73
73
|
|
|
74
|
+
case 'session':
|
|
75
|
+
case 'sessions':
|
|
76
|
+
const { SessionManager } = require('../lib/components/session');
|
|
77
|
+
const sessionManager = new SessionManager();
|
|
78
|
+
const sessionCmd = args[1] || 'list';
|
|
79
|
+
const sessionArg = args[2];
|
|
80
|
+
|
|
81
|
+
switch (sessionCmd) {
|
|
82
|
+
case 'list':
|
|
83
|
+
case 'ls':
|
|
84
|
+
sessionManager.list();
|
|
85
|
+
break;
|
|
86
|
+
case 'backup':
|
|
87
|
+
case 'save':
|
|
88
|
+
sessionManager.backup();
|
|
89
|
+
break;
|
|
90
|
+
case 'backups':
|
|
91
|
+
sessionManager.listBackups();
|
|
92
|
+
break;
|
|
93
|
+
case 'restore':
|
|
94
|
+
if (!sessionArg) {
|
|
95
|
+
logger.error('Please specify backup ID');
|
|
96
|
+
logger.info('Usage: zuppaclaude session restore <backup-id>');
|
|
97
|
+
logger.info('Run "zuppaclaude session backups" to see available backups');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
sessionManager.restore(sessionArg);
|
|
101
|
+
break;
|
|
102
|
+
case 'export':
|
|
103
|
+
if (!sessionArg) {
|
|
104
|
+
logger.error('Please specify session ID');
|
|
105
|
+
logger.info('Usage: zuppaclaude session export <session-id> [output-file]');
|
|
106
|
+
logger.info('Run "zuppaclaude session list" to see session IDs');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
sessionManager.export(sessionArg, args[3]);
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
logger.error(`Unknown session command: ${sessionCmd}`);
|
|
113
|
+
showSessionHelp();
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
74
117
|
case 'version':
|
|
75
118
|
case 'v':
|
|
76
119
|
case '-v':
|
|
@@ -108,6 +151,7 @@ Commands:
|
|
|
108
151
|
install, i Install ZuppaClaude components (default)
|
|
109
152
|
uninstall, u Uninstall ZuppaClaude components
|
|
110
153
|
settings, s Manage settings
|
|
154
|
+
session Manage Claude Code sessions
|
|
111
155
|
version, v Show version
|
|
112
156
|
help, h Show this help
|
|
113
157
|
|
|
@@ -118,12 +162,20 @@ Settings Commands:
|
|
|
118
162
|
settings reset Reset settings to default
|
|
119
163
|
settings path Show settings file path
|
|
120
164
|
|
|
165
|
+
Session Commands:
|
|
166
|
+
session list List all sessions
|
|
167
|
+
session backup Backup all sessions
|
|
168
|
+
session backups List available backups
|
|
169
|
+
session restore Restore from backup
|
|
170
|
+
session export Export a specific session
|
|
171
|
+
|
|
121
172
|
Examples:
|
|
122
173
|
npx zuppaclaude # Install
|
|
123
174
|
npx zuppaclaude install # Install
|
|
124
175
|
npx zuppaclaude uninstall # Uninstall
|
|
125
176
|
npx zuppaclaude settings show # View settings
|
|
126
|
-
npx zuppaclaude
|
|
177
|
+
npx zuppaclaude session backup # Backup sessions
|
|
178
|
+
npx zuppaclaude session restore 2026-01-05T12-00-00
|
|
127
179
|
`);
|
|
128
180
|
}
|
|
129
181
|
|
|
@@ -144,4 +196,22 @@ Examples:
|
|
|
144
196
|
`);
|
|
145
197
|
}
|
|
146
198
|
|
|
199
|
+
function showSessionHelp() {
|
|
200
|
+
console.log(`
|
|
201
|
+
Session Commands:
|
|
202
|
+
list List all Claude Code sessions
|
|
203
|
+
backup Backup all sessions to ~/.config/zuppaclaude/backups/
|
|
204
|
+
backups List available backups
|
|
205
|
+
restore Restore sessions from a backup
|
|
206
|
+
export Export a specific session to a file
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
zuppaclaude session list
|
|
210
|
+
zuppaclaude session backup
|
|
211
|
+
zuppaclaude session backups
|
|
212
|
+
zuppaclaude session restore 2026-01-05T12-00-00
|
|
213
|
+
zuppaclaude session export abc123 ./my-session.jsonl
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
|
|
147
217
|
main();
|
package/lib/components/index.js
CHANGED
|
@@ -7,11 +7,13 @@ const { SpecKitInstaller } = require('./speckit');
|
|
|
7
7
|
const { ConfigInstaller } = require('./config');
|
|
8
8
|
const { ClaudeZInstaller } = require('./claudez');
|
|
9
9
|
const { ClaudeHUDInstaller } = require('./claudehud');
|
|
10
|
+
const { SessionManager } = require('./session');
|
|
10
11
|
|
|
11
12
|
module.exports = {
|
|
12
13
|
SuperClaudeInstaller,
|
|
13
14
|
SpecKitInstaller,
|
|
14
15
|
ConfigInstaller,
|
|
15
16
|
ClaudeZInstaller,
|
|
16
|
-
ClaudeHUDInstaller
|
|
17
|
+
ClaudeHUDInstaller,
|
|
18
|
+
SessionManager
|
|
17
19
|
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager - Backup and restore Claude Code sessions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { Logger } = require('../utils/logger');
|
|
8
|
+
const { Platform } = require('../utils/platform');
|
|
9
|
+
|
|
10
|
+
class SessionManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.platform = new Platform();
|
|
13
|
+
this.logger = new Logger();
|
|
14
|
+
this.claudeDir = this.platform.claudeDir;
|
|
15
|
+
this.projectsDir = path.join(this.claudeDir, 'projects');
|
|
16
|
+
this.backupDir = path.join(this.platform.zuppaconfigDir, 'backups');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get all projects with sessions
|
|
21
|
+
*/
|
|
22
|
+
getProjects() {
|
|
23
|
+
if (!fs.existsSync(this.projectsDir)) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return fs.readdirSync(this.projectsDir)
|
|
28
|
+
.filter(name => {
|
|
29
|
+
const projectPath = path.join(this.projectsDir, name);
|
|
30
|
+
return fs.statSync(projectPath).isDirectory() && name !== '.' && name !== '..';
|
|
31
|
+
})
|
|
32
|
+
.map(name => {
|
|
33
|
+
const projectPath = path.join(this.projectsDir, name);
|
|
34
|
+
const sessions = this.getProjectSessions(projectPath);
|
|
35
|
+
const realPath = name.replace(/-/g, '/').replace(/^\//, '');
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: name,
|
|
39
|
+
path: realPath,
|
|
40
|
+
sessionsDir: projectPath,
|
|
41
|
+
sessions: sessions,
|
|
42
|
+
totalSize: sessions.reduce((sum, s) => sum + s.size, 0)
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.filter(p => p.sessions.length > 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get sessions for a project
|
|
50
|
+
*/
|
|
51
|
+
getProjectSessions(projectPath) {
|
|
52
|
+
if (!fs.existsSync(projectPath)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return fs.readdirSync(projectPath)
|
|
57
|
+
.filter(name => name.endsWith('.jsonl'))
|
|
58
|
+
.map(name => {
|
|
59
|
+
const filePath = path.join(projectPath, name);
|
|
60
|
+
const stats = fs.statSync(filePath);
|
|
61
|
+
const isAgent = name.startsWith('agent-');
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id: name.replace('.jsonl', ''),
|
|
65
|
+
file: name,
|
|
66
|
+
path: filePath,
|
|
67
|
+
size: stats.size,
|
|
68
|
+
modified: stats.mtime,
|
|
69
|
+
isAgent: isAgent,
|
|
70
|
+
type: isAgent ? 'agent' : 'main'
|
|
71
|
+
};
|
|
72
|
+
})
|
|
73
|
+
.sort((a, b) => b.modified - a.modified);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List all sessions
|
|
78
|
+
*/
|
|
79
|
+
list(options = {}) {
|
|
80
|
+
const projects = this.getProjects();
|
|
81
|
+
|
|
82
|
+
if (projects.length === 0) {
|
|
83
|
+
this.logger.warning('No sessions found');
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
89
|
+
console.log('\x1b[35m Claude Code Sessions\x1b[0m');
|
|
90
|
+
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
let totalSessions = 0;
|
|
94
|
+
let totalSize = 0;
|
|
95
|
+
|
|
96
|
+
for (const project of projects) {
|
|
97
|
+
const shortPath = project.path.length > 50
|
|
98
|
+
? '...' + project.path.slice(-47)
|
|
99
|
+
: project.path;
|
|
100
|
+
|
|
101
|
+
console.log(`\x1b[36m📁 ${shortPath}\x1b[0m`);
|
|
102
|
+
|
|
103
|
+
for (const session of project.sessions) {
|
|
104
|
+
const sizeStr = this.formatSize(session.size);
|
|
105
|
+
const dateStr = this.formatDate(session.modified);
|
|
106
|
+
const typeIcon = session.isAgent ? '🤖' : '💬';
|
|
107
|
+
const shortId = session.id.length > 20 ? session.id.slice(0, 20) + '...' : session.id;
|
|
108
|
+
|
|
109
|
+
console.log(` ${typeIcon} ${shortId} ${sizeStr} ${dateStr}`);
|
|
110
|
+
totalSessions++;
|
|
111
|
+
totalSize += session.size;
|
|
112
|
+
}
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`Total: ${totalSessions} sessions, ${this.formatSize(totalSize)}`);
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
return projects;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Backup all sessions
|
|
124
|
+
*/
|
|
125
|
+
backup(options = {}) {
|
|
126
|
+
const projects = this.getProjects();
|
|
127
|
+
|
|
128
|
+
if (projects.length === 0) {
|
|
129
|
+
this.logger.warning('No sessions to backup');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create backup directory with timestamp
|
|
134
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
135
|
+
const backupPath = path.join(this.backupDir, timestamp);
|
|
136
|
+
const sessionsBackupPath = path.join(backupPath, 'sessions');
|
|
137
|
+
|
|
138
|
+
this.platform.ensureDir(sessionsBackupPath);
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
this.logger.step('Backing up Claude Code sessions...');
|
|
142
|
+
console.log('');
|
|
143
|
+
|
|
144
|
+
let backedUp = 0;
|
|
145
|
+
let totalSize = 0;
|
|
146
|
+
const manifest = {
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
version: require('../../package.json').version,
|
|
149
|
+
projects: []
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const project of projects) {
|
|
153
|
+
const projectBackupPath = path.join(sessionsBackupPath, project.id);
|
|
154
|
+
this.platform.ensureDir(projectBackupPath);
|
|
155
|
+
|
|
156
|
+
const projectManifest = {
|
|
157
|
+
id: project.id,
|
|
158
|
+
path: project.path,
|
|
159
|
+
sessions: []
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for (const session of project.sessions) {
|
|
163
|
+
try {
|
|
164
|
+
const destPath = path.join(projectBackupPath, session.file);
|
|
165
|
+
fs.copyFileSync(session.path, destPath);
|
|
166
|
+
|
|
167
|
+
projectManifest.sessions.push({
|
|
168
|
+
id: session.id,
|
|
169
|
+
file: session.file,
|
|
170
|
+
size: session.size,
|
|
171
|
+
modified: session.modified.toISOString(),
|
|
172
|
+
type: session.type
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
backedUp++;
|
|
176
|
+
totalSize += session.size;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.logger.warning(`Failed to backup ${session.id}: ${error.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
manifest.projects.push(projectManifest);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Also backup history.jsonl if exists
|
|
186
|
+
const historyPath = path.join(this.claudeDir, 'history.jsonl');
|
|
187
|
+
if (fs.existsSync(historyPath)) {
|
|
188
|
+
try {
|
|
189
|
+
fs.copyFileSync(historyPath, path.join(backupPath, 'history.jsonl'));
|
|
190
|
+
const historyStats = fs.statSync(historyPath);
|
|
191
|
+
manifest.history = {
|
|
192
|
+
size: historyStats.size,
|
|
193
|
+
modified: historyStats.mtime.toISOString()
|
|
194
|
+
};
|
|
195
|
+
totalSize += historyStats.size;
|
|
196
|
+
this.logger.success('Command history backed up');
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.logger.warning(`Failed to backup history: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Save manifest
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(backupPath, 'manifest.json'),
|
|
205
|
+
JSON.stringify(manifest, null, 2)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
console.log('');
|
|
209
|
+
this.logger.success(`Backup complete: ${backedUp} sessions, ${this.formatSize(totalSize)}`);
|
|
210
|
+
this.logger.info(`Location: ${backupPath}`);
|
|
211
|
+
console.log('');
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
path: backupPath,
|
|
215
|
+
sessions: backedUp,
|
|
216
|
+
size: totalSize,
|
|
217
|
+
timestamp: timestamp
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* List available backups
|
|
223
|
+
*/
|
|
224
|
+
listBackups() {
|
|
225
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
226
|
+
this.logger.warning('No backups found');
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const backups = fs.readdirSync(this.backupDir)
|
|
231
|
+
.filter(name => {
|
|
232
|
+
const backupPath = path.join(this.backupDir, name);
|
|
233
|
+
return fs.statSync(backupPath).isDirectory();
|
|
234
|
+
})
|
|
235
|
+
.map(name => {
|
|
236
|
+
const backupPath = path.join(this.backupDir, name);
|
|
237
|
+
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
238
|
+
|
|
239
|
+
let manifest = null;
|
|
240
|
+
if (fs.existsSync(manifestPath)) {
|
|
241
|
+
try {
|
|
242
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// Ignore parse errors
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const totalSessions = manifest?.projects?.reduce(
|
|
249
|
+
(sum, p) => sum + (p.sessions?.length || 0), 0
|
|
250
|
+
) || 0;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
id: name,
|
|
254
|
+
path: backupPath,
|
|
255
|
+
timestamp: manifest?.timestamp || name,
|
|
256
|
+
sessions: totalSessions,
|
|
257
|
+
projects: manifest?.projects?.length || 0
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
261
|
+
|
|
262
|
+
if (backups.length === 0) {
|
|
263
|
+
this.logger.warning('No backups found');
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
269
|
+
console.log('\x1b[35m Available Backups\x1b[0m');
|
|
270
|
+
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
271
|
+
console.log('');
|
|
272
|
+
|
|
273
|
+
for (const backup of backups) {
|
|
274
|
+
const dateStr = backup.id.replace('T', ' ').replace(/-/g, ':').slice(0, 16).replace(/:/g, '-').slice(0,10) + ' ' + backup.id.slice(11, 16).replace(/-/g, ':');
|
|
275
|
+
console.log(` 📦 ${backup.id}`);
|
|
276
|
+
console.log(` ${backup.projects} projects, ${backup.sessions} sessions`);
|
|
277
|
+
console.log('');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return backups;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Restore from backup
|
|
285
|
+
*/
|
|
286
|
+
restore(backupId) {
|
|
287
|
+
const backupPath = path.join(this.backupDir, backupId);
|
|
288
|
+
|
|
289
|
+
if (!fs.existsSync(backupPath)) {
|
|
290
|
+
this.logger.error(`Backup not found: ${backupId}`);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
295
|
+
if (!fs.existsSync(manifestPath)) {
|
|
296
|
+
this.logger.error('Invalid backup: manifest.json not found');
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
301
|
+
|
|
302
|
+
console.log('');
|
|
303
|
+
this.logger.step(`Restoring from backup: ${backupId}`);
|
|
304
|
+
console.log('');
|
|
305
|
+
|
|
306
|
+
let restored = 0;
|
|
307
|
+
|
|
308
|
+
for (const project of manifest.projects) {
|
|
309
|
+
const projectBackupPath = path.join(backupPath, 'sessions', project.id);
|
|
310
|
+
const projectDestPath = path.join(this.projectsDir, project.id);
|
|
311
|
+
|
|
312
|
+
if (!fs.existsSync(projectBackupPath)) continue;
|
|
313
|
+
|
|
314
|
+
this.platform.ensureDir(projectDestPath);
|
|
315
|
+
|
|
316
|
+
for (const session of project.sessions) {
|
|
317
|
+
const srcPath = path.join(projectBackupPath, session.file);
|
|
318
|
+
const destPath = path.join(projectDestPath, session.file);
|
|
319
|
+
|
|
320
|
+
if (fs.existsSync(srcPath)) {
|
|
321
|
+
try {
|
|
322
|
+
// Don't overwrite existing sessions, create with .restored suffix
|
|
323
|
+
if (fs.existsSync(destPath)) {
|
|
324
|
+
const restoredPath = destPath.replace('.jsonl', '.restored.jsonl');
|
|
325
|
+
fs.copyFileSync(srcPath, restoredPath);
|
|
326
|
+
} else {
|
|
327
|
+
fs.copyFileSync(srcPath, destPath);
|
|
328
|
+
}
|
|
329
|
+
restored++;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.logger.warning(`Failed to restore ${session.id}: ${error.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Restore history if exists
|
|
338
|
+
const historyBackupPath = path.join(backupPath, 'history.jsonl');
|
|
339
|
+
if (fs.existsSync(historyBackupPath)) {
|
|
340
|
+
const historyDestPath = path.join(this.claudeDir, 'history.jsonl');
|
|
341
|
+
try {
|
|
342
|
+
if (fs.existsSync(historyDestPath)) {
|
|
343
|
+
fs.copyFileSync(historyBackupPath, historyDestPath + '.restored');
|
|
344
|
+
this.logger.info('History restored as history.jsonl.restored');
|
|
345
|
+
} else {
|
|
346
|
+
fs.copyFileSync(historyBackupPath, historyDestPath);
|
|
347
|
+
this.logger.success('Command history restored');
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
this.logger.warning(`Failed to restore history: ${error.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log('');
|
|
355
|
+
this.logger.success(`Restored ${restored} sessions`);
|
|
356
|
+
this.logger.info('Restart Claude Code to see restored sessions');
|
|
357
|
+
console.log('');
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Export a specific session to a file
|
|
364
|
+
*/
|
|
365
|
+
export(sessionId, outputPath) {
|
|
366
|
+
const projects = this.getProjects();
|
|
367
|
+
|
|
368
|
+
let foundSession = null;
|
|
369
|
+
let foundProject = null;
|
|
370
|
+
|
|
371
|
+
for (const project of projects) {
|
|
372
|
+
for (const session of project.sessions) {
|
|
373
|
+
if (session.id === sessionId || session.id.startsWith(sessionId)) {
|
|
374
|
+
foundSession = session;
|
|
375
|
+
foundProject = project;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (foundSession) break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!foundSession) {
|
|
383
|
+
this.logger.error(`Session not found: ${sessionId}`);
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const destPath = outputPath || `${foundSession.id}.jsonl`;
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
fs.copyFileSync(foundSession.path, destPath);
|
|
391
|
+
this.logger.success(`Session exported to: ${destPath}`);
|
|
392
|
+
this.logger.info(`Size: ${this.formatSize(foundSession.size)}`);
|
|
393
|
+
return true;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
this.logger.error(`Failed to export: ${error.message}`);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Format file size
|
|
402
|
+
*/
|
|
403
|
+
formatSize(bytes) {
|
|
404
|
+
if (bytes < 1024) return bytes + ' B';
|
|
405
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
406
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Format date
|
|
411
|
+
*/
|
|
412
|
+
formatDate(date) {
|
|
413
|
+
const now = new Date();
|
|
414
|
+
const diff = now - date;
|
|
415
|
+
|
|
416
|
+
if (diff < 60000) return 'just now';
|
|
417
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
418
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
419
|
+
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
|
|
420
|
+
|
|
421
|
+
return date.toISOString().slice(0, 10);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = { SessionManager };
|