codedash-app 3.3.1 → 4.0.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/bin/cli.js +119 -4
- package/package.json +1 -1
- package/src/convert.js +273 -0
- package/src/data.js +3 -0
- package/src/frontend/app.js +27 -0
- package/src/server.js +14 -0
package/bin/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { loadSessions } = require('../src/data');
|
|
3
|
+
const { loadSessions, searchFullText, getSessionPreview, computeSessionCost } = require('../src/data');
|
|
4
4
|
const { startServer } = require('../src/server');
|
|
5
5
|
const { exportArchive, importArchive } = require('../src/migrate');
|
|
6
|
+
const { convertSession } = require('../src/convert');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_PORT = 3847;
|
|
8
9
|
const args = process.argv.slice(2);
|
|
@@ -57,6 +58,117 @@ switch (command) {
|
|
|
57
58
|
break;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
case 'search':
|
|
62
|
+
case 'find': {
|
|
63
|
+
const query = args.slice(1).join(' ');
|
|
64
|
+
if (!query) {
|
|
65
|
+
console.error(' Usage: codedash search <query>');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const sessions = loadSessions();
|
|
69
|
+
const results = searchFullText(query, sessions);
|
|
70
|
+
if (results.length === 0) {
|
|
71
|
+
console.log(`\n No results for "${query}"\n`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`\n \x1b[36m\x1b[1m${results.length} sessions\x1b[0m matching "${query}"\n`);
|
|
74
|
+
for (const r of results.slice(0, 15)) {
|
|
75
|
+
const s = sessions.find(x => x.id === r.sessionId);
|
|
76
|
+
const proj = s ? (s.project_short || '') : '';
|
|
77
|
+
const tool = s ? s.tool : '?';
|
|
78
|
+
const date = s ? s.last_time : '';
|
|
79
|
+
console.log(` \x1b[1m${r.sessionId.slice(0, 12)}\x1b[0m ${tool} ${date} \x1b[2m${proj}\x1b[0m`);
|
|
80
|
+
for (const m of r.matches.slice(0, 2)) {
|
|
81
|
+
const role = m.role === 'user' ? '\x1b[34mYOU\x1b[0m' : '\x1b[32mAI \x1b[0m';
|
|
82
|
+
console.log(` ${role} ${m.snippet.replace(/\n/g, ' ').slice(0, 100)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (results.length > 15) console.log(`\n \x1b[2m... and ${results.length - 15} more\x1b[0m`);
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'show': {
|
|
92
|
+
const sid = args[1];
|
|
93
|
+
if (!sid) {
|
|
94
|
+
console.error(' Usage: codedash show <session-id>');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const allS = loadSessions();
|
|
98
|
+
const session = allS.find(s => s.id === sid || s.id.startsWith(sid));
|
|
99
|
+
if (!session) {
|
|
100
|
+
console.error(` Session not found: ${sid}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const preview = getSessionPreview(session.id, session.project, 20);
|
|
104
|
+
const cost = computeSessionCost(session.id, session.project);
|
|
105
|
+
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(` \x1b[36m\x1b[1mSession ${session.id}\x1b[0m`);
|
|
108
|
+
console.log(` Tool: ${session.tool}`);
|
|
109
|
+
console.log(` Project: ${session.project_short || session.project || 'unknown'}`);
|
|
110
|
+
console.log(` Started: ${session.first_time}`);
|
|
111
|
+
console.log(` Last: ${session.last_time}`);
|
|
112
|
+
console.log(` Msgs: ${session.messages} inputs, ${session.detail_messages || 0} total`);
|
|
113
|
+
if (cost.cost > 0) {
|
|
114
|
+
console.log(` Cost: $${cost.cost.toFixed(2)} (${cost.model || 'unknown'})`);
|
|
115
|
+
console.log(` Tokens: ${(cost.inputTokens/1000).toFixed(0)}K in / ${(cost.outputTokens/1000).toFixed(0)}K out`);
|
|
116
|
+
}
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
if (preview.length > 0) {
|
|
120
|
+
console.log(' \x1b[1mConversation:\x1b[0m');
|
|
121
|
+
for (const m of preview) {
|
|
122
|
+
const role = m.role === 'user' ? '\x1b[34mYOU\x1b[0m' : '\x1b[32mAI \x1b[0m';
|
|
123
|
+
const text = m.content.replace(/\n/g, ' ').slice(0, 120);
|
|
124
|
+
console.log(` ${role} ${text}`);
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(` Resume: \x1b[2m${session.tool === 'codex' ? 'codex resume' : 'claude --resume'} ${session.id}\x1b[0m`);
|
|
130
|
+
console.log('');
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'convert': {
|
|
135
|
+
const sid = args[1];
|
|
136
|
+
const target = args[2]; // 'claude' or 'codex'
|
|
137
|
+
if (!sid || !target) {
|
|
138
|
+
console.log(`
|
|
139
|
+
\x1b[36m\x1b[1mConvert session between agents\x1b[0m
|
|
140
|
+
|
|
141
|
+
Usage: codedash convert <session-id> <target-format>
|
|
142
|
+
|
|
143
|
+
Formats: claude, codex
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
codedash convert 019d54ed codex Convert Claude session to Codex
|
|
147
|
+
codedash convert 13ae5748 claude Convert Codex session to Claude
|
|
148
|
+
`);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
// Find full session ID
|
|
152
|
+
const allConv = loadSessions();
|
|
153
|
+
const match = allConv.find(s => s.id === sid || s.id.startsWith(sid));
|
|
154
|
+
if (!match) {
|
|
155
|
+
console.error(` Session not found: ${sid}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
console.log(`\n Converting ${match.tool} session \x1b[1m${match.id.slice(0, 12)}\x1b[0m → ${target}...`);
|
|
159
|
+
const result = convertSession(match.id, match.project, target);
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
console.error(` \x1b[31mError:\x1b[0m ${result.error}\n`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
console.log(` \x1b[32mDone!\x1b[0m`);
|
|
165
|
+
console.log(` New session: ${result.target.sessionId}`);
|
|
166
|
+
console.log(` Messages: ${result.target.messages}`);
|
|
167
|
+
console.log(` File: ${result.target.file}`);
|
|
168
|
+
console.log(` Resume: \x1b[2m${result.target.resumeCmd}\x1b[0m\n`);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
60
172
|
case 'update':
|
|
61
173
|
case 'upgrade': {
|
|
62
174
|
const { execSync: execU } = require('child_process');
|
|
@@ -137,13 +249,16 @@ switch (command) {
|
|
|
137
249
|
|
|
138
250
|
\x1b[1mUsage:\x1b[0m
|
|
139
251
|
codedash run [port] [--no-browser] Start the dashboard server
|
|
140
|
-
codedash
|
|
141
|
-
codedash
|
|
142
|
-
codedash stop [--port=N] Stop the server
|
|
252
|
+
codedash search <query> Search across all session messages
|
|
253
|
+
codedash show <session-id> Show session details + messages
|
|
143
254
|
codedash list [limit] List sessions in terminal
|
|
144
255
|
codedash stats Show session statistics
|
|
256
|
+
codedash convert <id> <format> Convert session (claude/codex)
|
|
145
257
|
codedash export [file.tar.gz] Export all sessions to archive
|
|
146
258
|
codedash import <file.tar.gz> Import sessions from archive
|
|
259
|
+
codedash update Update to latest version
|
|
260
|
+
codedash restart [--port=N] Restart the server
|
|
261
|
+
codedash stop [--port=N] Stop the server
|
|
147
262
|
codedash help Show this help
|
|
148
263
|
codedash version Show version
|
|
149
264
|
|
package/package.json
CHANGED
package/src/convert.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { findSessionFile, extractContent, isSystemMessage } = require('./data');
|
|
8
|
+
|
|
9
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
10
|
+
const CODEX_DIR = path.join(os.homedir(), '.codex');
|
|
11
|
+
|
|
12
|
+
// ── Read session into canonical format ────────────────────
|
|
13
|
+
|
|
14
|
+
function readSession(sessionId, project) {
|
|
15
|
+
const found = findSessionFile(sessionId, project);
|
|
16
|
+
if (!found) return null;
|
|
17
|
+
|
|
18
|
+
const messages = [];
|
|
19
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
20
|
+
let sessionMeta = {};
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
try {
|
|
24
|
+
const entry = JSON.parse(line);
|
|
25
|
+
|
|
26
|
+
if (found.format === 'claude') {
|
|
27
|
+
if (entry.type === 'permission-mode') {
|
|
28
|
+
sessionMeta.permissionMode = entry.permissionMode;
|
|
29
|
+
sessionMeta.originalSessionId = entry.sessionId;
|
|
30
|
+
}
|
|
31
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
32
|
+
const msg = entry.message || {};
|
|
33
|
+
let content = '';
|
|
34
|
+
if (typeof msg.content === 'string') {
|
|
35
|
+
content = msg.content;
|
|
36
|
+
} else if (Array.isArray(msg.content)) {
|
|
37
|
+
content = msg.content
|
|
38
|
+
.filter(b => b.type === 'text')
|
|
39
|
+
.map(b => b.text)
|
|
40
|
+
.join('\n');
|
|
41
|
+
}
|
|
42
|
+
if (!content || isSystemMessage(content)) continue;
|
|
43
|
+
|
|
44
|
+
messages.push({
|
|
45
|
+
role: entry.type === 'user' ? 'user' : 'assistant',
|
|
46
|
+
content: content,
|
|
47
|
+
timestamp: entry.timestamp || '',
|
|
48
|
+
model: msg.model || '',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Codex
|
|
53
|
+
if (entry.type === 'session_meta' && entry.payload) {
|
|
54
|
+
sessionMeta.cwd = entry.payload.cwd;
|
|
55
|
+
sessionMeta.originalSessionId = entry.payload.id;
|
|
56
|
+
}
|
|
57
|
+
if (entry.type === 'response_item' && entry.payload) {
|
|
58
|
+
const role = entry.payload.role;
|
|
59
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
60
|
+
const content = extractContent(entry.payload.content);
|
|
61
|
+
if (!content || isSystemMessage(content)) continue;
|
|
62
|
+
|
|
63
|
+
messages.push({
|
|
64
|
+
role: role,
|
|
65
|
+
content: content,
|
|
66
|
+
timestamp: entry.timestamp || '',
|
|
67
|
+
model: '',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
sourceFormat: found.format,
|
|
76
|
+
sourceFile: found.file,
|
|
77
|
+
sessionId: sessionId,
|
|
78
|
+
meta: sessionMeta,
|
|
79
|
+
messages: messages,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Write as Claude Code session ──────────────────────────
|
|
84
|
+
|
|
85
|
+
function writeClaude(canonical, targetProject) {
|
|
86
|
+
const newSessionId = crypto.randomUUID();
|
|
87
|
+
const projectKey = (targetProject || os.homedir()).replace(/[\/\.]/g, '-');
|
|
88
|
+
const projectDir = path.join(CLAUDE_DIR, 'projects', projectKey);
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(projectDir)) {
|
|
91
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const outFile = path.join(projectDir, `${newSessionId}.jsonl`);
|
|
95
|
+
const cwd = targetProject || canonical.meta.cwd || os.homedir();
|
|
96
|
+
const lines = [];
|
|
97
|
+
|
|
98
|
+
// Permission mode entry
|
|
99
|
+
lines.push(JSON.stringify({
|
|
100
|
+
type: 'permission-mode',
|
|
101
|
+
permissionMode: 'default',
|
|
102
|
+
sessionId: newSessionId,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
let prevUuid = null;
|
|
106
|
+
|
|
107
|
+
for (const msg of canonical.messages) {
|
|
108
|
+
const uuid = crypto.randomUUID();
|
|
109
|
+
const entry = {
|
|
110
|
+
parentUuid: prevUuid,
|
|
111
|
+
isSidechain: false,
|
|
112
|
+
type: msg.role === 'user' ? 'user' : 'assistant',
|
|
113
|
+
uuid: uuid,
|
|
114
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
115
|
+
userType: 'external',
|
|
116
|
+
entrypoint: 'cli',
|
|
117
|
+
cwd: cwd,
|
|
118
|
+
sessionId: newSessionId,
|
|
119
|
+
version: '2.1.91',
|
|
120
|
+
gitBranch: 'main',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (msg.role === 'user') {
|
|
124
|
+
entry.message = { role: 'user', content: msg.content };
|
|
125
|
+
entry.promptId = crypto.randomUUID();
|
|
126
|
+
} else {
|
|
127
|
+
entry.message = {
|
|
128
|
+
model: msg.model || 'claude-sonnet-4-6',
|
|
129
|
+
id: 'msg_converted_' + uuid.slice(0, 8),
|
|
130
|
+
type: 'message',
|
|
131
|
+
role: 'assistant',
|
|
132
|
+
content: [{ type: 'text', text: msg.content }],
|
|
133
|
+
stop_reason: 'end_turn',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
lines.push(JSON.stringify(entry));
|
|
138
|
+
prevUuid = uuid;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Write atomically
|
|
142
|
+
const tmpFile = outFile + '.tmp';
|
|
143
|
+
fs.writeFileSync(tmpFile, lines.join('\n') + '\n');
|
|
144
|
+
fs.renameSync(tmpFile, outFile);
|
|
145
|
+
|
|
146
|
+
// Add to history.jsonl
|
|
147
|
+
const historyFile = path.join(CLAUDE_DIR, 'history.jsonl');
|
|
148
|
+
const historyEntry = JSON.stringify({
|
|
149
|
+
sessionId: newSessionId,
|
|
150
|
+
project: cwd,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
display: `[Converted from ${canonical.sourceFormat}] ${canonical.messages[0]?.content?.slice(0, 100) || ''}`,
|
|
153
|
+
pastedContents: {},
|
|
154
|
+
});
|
|
155
|
+
fs.appendFileSync(historyFile, historyEntry + '\n');
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
sessionId: newSessionId,
|
|
159
|
+
file: outFile,
|
|
160
|
+
format: 'claude',
|
|
161
|
+
messages: canonical.messages.length,
|
|
162
|
+
resumeCmd: `claude --resume ${newSessionId}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Write as Codex session ────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function writeCodex(canonical, targetProject) {
|
|
169
|
+
const newSessionId = crypto.randomUUID();
|
|
170
|
+
const now = new Date();
|
|
171
|
+
const dateDir = path.join(
|
|
172
|
+
CODEX_DIR, 'sessions',
|
|
173
|
+
String(now.getFullYear()),
|
|
174
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
175
|
+
String(now.getDate()).padStart(2, '0')
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(dateDir)) {
|
|
179
|
+
fs.mkdirSync(dateDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const fileName = `rollout-${now.toISOString().replace(/[:.]/g, '-').slice(0, 19)}-${newSessionId}.jsonl`;
|
|
183
|
+
const outFile = path.join(dateDir, fileName);
|
|
184
|
+
const cwd = targetProject || canonical.meta.cwd || os.homedir();
|
|
185
|
+
const lines = [];
|
|
186
|
+
|
|
187
|
+
// Session meta
|
|
188
|
+
lines.push(JSON.stringify({
|
|
189
|
+
timestamp: now.toISOString(),
|
|
190
|
+
type: 'session_meta',
|
|
191
|
+
payload: {
|
|
192
|
+
id: newSessionId,
|
|
193
|
+
timestamp: now.toISOString(),
|
|
194
|
+
cwd: cwd,
|
|
195
|
+
originator: 'codex_cli_rs',
|
|
196
|
+
cli_version: '0.101.0',
|
|
197
|
+
source: 'cli',
|
|
198
|
+
model_provider: 'openai',
|
|
199
|
+
},
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Messages
|
|
203
|
+
for (const msg of canonical.messages) {
|
|
204
|
+
lines.push(JSON.stringify({
|
|
205
|
+
timestamp: msg.timestamp || now.toISOString(),
|
|
206
|
+
type: 'response_item',
|
|
207
|
+
payload: {
|
|
208
|
+
type: 'message',
|
|
209
|
+
role: msg.role,
|
|
210
|
+
content: [{ type: 'input_text', text: msg.content }],
|
|
211
|
+
},
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Write atomically
|
|
216
|
+
const tmpFile = outFile + '.tmp';
|
|
217
|
+
fs.writeFileSync(tmpFile, lines.join('\n') + '\n');
|
|
218
|
+
fs.renameSync(tmpFile, outFile);
|
|
219
|
+
|
|
220
|
+
// Add to codex history
|
|
221
|
+
const historyFile = path.join(CODEX_DIR, 'history.jsonl');
|
|
222
|
+
if (!fs.existsSync(path.dirname(historyFile))) {
|
|
223
|
+
fs.mkdirSync(path.dirname(historyFile), { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
const historyEntry = JSON.stringify({
|
|
226
|
+
session_id: newSessionId,
|
|
227
|
+
ts: Math.floor(Date.now() / 1000),
|
|
228
|
+
text: `[Converted from ${canonical.sourceFormat}] ${canonical.messages[0]?.content?.slice(0, 100) || ''}`,
|
|
229
|
+
});
|
|
230
|
+
fs.appendFileSync(historyFile, historyEntry + '\n');
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
sessionId: newSessionId,
|
|
234
|
+
file: outFile,
|
|
235
|
+
format: 'codex',
|
|
236
|
+
messages: canonical.messages.length,
|
|
237
|
+
resumeCmd: `codex resume ${newSessionId}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Main convert function ─────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function convertSession(sessionId, project, targetFormat) {
|
|
244
|
+
const canonical = readSession(sessionId, project);
|
|
245
|
+
if (!canonical) {
|
|
246
|
+
return { ok: false, error: 'Session not found' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (canonical.sourceFormat === targetFormat) {
|
|
250
|
+
return { ok: false, error: `Session is already in ${targetFormat} format` };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (canonical.messages.length === 0) {
|
|
254
|
+
return { ok: false, error: 'Session has no messages to convert' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let result;
|
|
258
|
+
if (targetFormat === 'claude') {
|
|
259
|
+
result = writeClaude(canonical, project);
|
|
260
|
+
} else if (targetFormat === 'codex') {
|
|
261
|
+
result = writeCodex(canonical, project);
|
|
262
|
+
} else {
|
|
263
|
+
return { ok: false, error: `Unknown target format: ${targetFormat}` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
source: { format: canonical.sourceFormat, sessionId: sessionId, messages: canonical.messages.length },
|
|
269
|
+
target: result,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { convertSession, readSession };
|
package/src/data.js
CHANGED
package/src/frontend/app.js
CHANGED
|
@@ -1081,6 +1081,8 @@ async function openDetail(s) {
|
|
|
1081
1081
|
if (s.has_detail) {
|
|
1082
1082
|
infoHtml += '<button class="launch-btn btn-secondary" onclick="closeDetail();openReplay(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Replay</button>';
|
|
1083
1083
|
infoHtml += '<button class="launch-btn btn-secondary" onclick="exportMd(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Export MD</button>';
|
|
1084
|
+
var convertTarget = s.tool === 'codex' ? 'claude' : 'codex';
|
|
1085
|
+
infoHtml += '<button class="launch-btn btn-secondary" onclick="convertTo(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\',\'' + convertTarget + '\')">Convert to ' + convertTarget + '</button>';
|
|
1084
1086
|
}
|
|
1085
1087
|
infoHtml += '<button class="star-btn detail-star' + (isStarred ? ' active' : '') + '" onclick="toggleStar(\'' + s.id + '\')">★ ' + (isStarred ? 'Starred' : 'Star') + '</button>';
|
|
1086
1088
|
infoHtml += '<button class="launch-btn btn-delete" onclick="showDeleteConfirm(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Delete</button>';
|
|
@@ -1744,6 +1746,31 @@ function focusSession(sessionId) {
|
|
|
1744
1746
|
});
|
|
1745
1747
|
}
|
|
1746
1748
|
|
|
1749
|
+
// ── Convert session ───────────────────────────────────────────
|
|
1750
|
+
|
|
1751
|
+
async function convertTo(sessionId, project, targetFormat) {
|
|
1752
|
+
if (!confirm('Convert this session to ' + targetFormat + '? A new session will be created.')) return;
|
|
1753
|
+
showToast('Converting...');
|
|
1754
|
+
try {
|
|
1755
|
+
var resp = await fetch('/api/convert', {
|
|
1756
|
+
method: 'POST',
|
|
1757
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1758
|
+
body: JSON.stringify({ sessionId: sessionId, project: project, targetFormat: targetFormat }),
|
|
1759
|
+
});
|
|
1760
|
+
var data = await resp.json();
|
|
1761
|
+
if (data.ok) {
|
|
1762
|
+
showToast('Converted! New session: ' + data.target.sessionId.slice(0, 12));
|
|
1763
|
+
// Refresh to show new session
|
|
1764
|
+
await loadSessions();
|
|
1765
|
+
closeDetail();
|
|
1766
|
+
} else {
|
|
1767
|
+
showToast('Error: ' + (data.error || 'unknown'));
|
|
1768
|
+
}
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
showToast('Convert failed: ' + e.message);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1747
1774
|
// ── Install agents ────────────────────────────────────────────
|
|
1748
1775
|
|
|
1749
1776
|
var AGENT_INSTALL = {
|
package/src/server.js
CHANGED
|
@@ -5,6 +5,7 @@ const { URL } = require('url');
|
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
6
|
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data');
|
|
7
7
|
const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals');
|
|
8
|
+
const { convertSession } = require('./convert');
|
|
8
9
|
const { getHTML } = require('./html');
|
|
9
10
|
|
|
10
11
|
function startServer(port, openBrowser = true) {
|
|
@@ -110,6 +111,19 @@ function startServer(port, openBrowser = true) {
|
|
|
110
111
|
json(res, active);
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// ── Convert session ─────────────────────
|
|
115
|
+
else if (req.method === 'POST' && pathname === '/api/convert') {
|
|
116
|
+
readBody(req, body => {
|
|
117
|
+
try {
|
|
118
|
+
const { sessionId, project, targetFormat } = JSON.parse(body);
|
|
119
|
+
const result = convertSession(sessionId, project || '', targetFormat);
|
|
120
|
+
json(res, result);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
json(res, { ok: false, error: e.message }, 400);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
// ── Focus terminal ──────────────────────
|
|
114
128
|
else if (req.method === 'POST' && pathname === '/api/focus') {
|
|
115
129
|
readBody(req, body => {
|