codedash-app 1.5.0 → 1.7.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 +50 -0
- package/package.json +1 -1
- package/src/data.js +112 -41
- package/src/frontend/app.js +20 -5
- package/src/frontend/index.html +1 -0
- package/src/frontend/styles.css +20 -0
package/bin/cli.js
CHANGED
|
@@ -57,6 +57,53 @@ switch (command) {
|
|
|
57
57
|
break;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
case 'update':
|
|
61
|
+
case 'upgrade': {
|
|
62
|
+
const { execSync: execU } = require('child_process');
|
|
63
|
+
console.log('\n \x1b[36m\x1b[1mUpdating codedash-app...\x1b[0m\n');
|
|
64
|
+
try {
|
|
65
|
+
execU('npm i -g codedash-app@latest', { stdio: 'inherit' });
|
|
66
|
+
const newPkg = require('../package.json');
|
|
67
|
+
console.log(`\n \x1b[32mUpdated to v${newPkg.version}!\x1b[0m`);
|
|
68
|
+
console.log(' Run \x1b[2mcodedash restart\x1b[0m to apply.\n');
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(' \x1b[31mUpdate failed.\x1b[0m Try: npm i -g codedash-app@latest\n');
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'restart': {
|
|
76
|
+
const { execSync } = require('child_process');
|
|
77
|
+
const portArg = args.find(a => a.startsWith('--port='));
|
|
78
|
+
const port = portArg ? parseInt(portArg.split('=')[1]) : DEFAULT_PORT;
|
|
79
|
+
console.log(`\n Stopping codedash on port ${port}...`);
|
|
80
|
+
try {
|
|
81
|
+
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
82
|
+
console.log(' Stopped.');
|
|
83
|
+
} catch {
|
|
84
|
+
console.log(' No running instance found.');
|
|
85
|
+
}
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
console.log(' Starting...\n');
|
|
88
|
+
const noBrowser = args.includes('--no-browser');
|
|
89
|
+
startServer(port, !noBrowser);
|
|
90
|
+
}, 500);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'stop': {
|
|
95
|
+
const { execSync: execS } = require('child_process');
|
|
96
|
+
const pArg = args.find(a => a.startsWith('--port='));
|
|
97
|
+
const p = pArg ? parseInt(pArg.split('=')[1]) : DEFAULT_PORT;
|
|
98
|
+
try {
|
|
99
|
+
execS(`lsof -ti:${p} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
100
|
+
console.log(`\n codedash stopped (port ${p})\n`);
|
|
101
|
+
} catch {
|
|
102
|
+
console.log(`\n No codedash running on port ${p}\n`);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
60
107
|
case 'export': {
|
|
61
108
|
const outPath = args[1] || `codedash-export-${new Date().toISOString().slice(0,10)}.tar.gz`;
|
|
62
109
|
exportArchive(outPath);
|
|
@@ -90,6 +137,9 @@ switch (command) {
|
|
|
90
137
|
|
|
91
138
|
\x1b[1mUsage:\x1b[0m
|
|
92
139
|
codedash run [port] [--no-browser] Start the dashboard server
|
|
140
|
+
codedash update Update to latest version
|
|
141
|
+
codedash restart [--port=N] Restart the server
|
|
142
|
+
codedash stop [--port=N] Stop the server
|
|
93
143
|
codedash list [limit] List sessions in terminal
|
|
94
144
|
codedash stats Show session statistics
|
|
95
145
|
codedash export [file.tar.gz] Export all sessions to archive
|
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -157,7 +157,7 @@ function loadSessions() {
|
|
|
157
157
|
// Enrich Claude sessions with detail file info
|
|
158
158
|
for (const [sid, s] of Object.entries(sessions)) {
|
|
159
159
|
if (s.tool !== 'claude') continue;
|
|
160
|
-
const projectKey = s.project.replace(
|
|
160
|
+
const projectKey = s.project.replace(/[\/\.]/g, '-');
|
|
161
161
|
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sid}.jsonl`);
|
|
162
162
|
if (fs.existsSync(sessionFile)) {
|
|
163
163
|
s.has_detail = true;
|
|
@@ -192,33 +192,33 @@ function loadSessions() {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
function loadSessionDetail(sessionId, project) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!fs.existsSync(sessionFile)) {
|
|
199
|
-
return { error: 'Session file not found', messages: [] };
|
|
200
|
-
}
|
|
195
|
+
const found = findSessionFile(sessionId, project);
|
|
196
|
+
if (!found) return { error: 'Session file not found', messages: [] };
|
|
201
197
|
|
|
202
198
|
const messages = [];
|
|
203
|
-
const lines = fs.readFileSync(
|
|
199
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
204
200
|
|
|
205
201
|
for (const line of lines) {
|
|
206
202
|
try {
|
|
207
203
|
const entry = JSON.parse(line);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
content
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
|
|
204
|
+
|
|
205
|
+
if (found.format === 'claude') {
|
|
206
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
207
|
+
const content = extractContent((entry.message || {}).content);
|
|
208
|
+
if (content) {
|
|
209
|
+
messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
if (entry.type === 'response_item' && entry.payload) {
|
|
214
|
+
const role = entry.payload.role;
|
|
215
|
+
if (role === 'user' || role === 'assistant') {
|
|
216
|
+
const content = extractContent(entry.payload.content);
|
|
217
|
+
if (content && !isSystemMessage(content)) {
|
|
218
|
+
messages.push({ role: role, content: content.slice(0, 2000), uuid: '' });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
216
221
|
}
|
|
217
|
-
messages.push({
|
|
218
|
-
role: entry.type,
|
|
219
|
-
content: content.slice(0, 2000),
|
|
220
|
-
uuid: entry.uuid || '',
|
|
221
|
-
});
|
|
222
222
|
}
|
|
223
223
|
} catch {}
|
|
224
224
|
}
|
|
@@ -230,7 +230,7 @@ function deleteSession(sessionId, project) {
|
|
|
230
230
|
const deleted = [];
|
|
231
231
|
|
|
232
232
|
// 1. Remove session JSONL file from project dir
|
|
233
|
-
const projectKey = project.replace(
|
|
233
|
+
const projectKey = project.replace(/[\/\.]/g, '-');
|
|
234
234
|
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
|
|
235
235
|
if (fs.existsSync(sessionFile)) {
|
|
236
236
|
fs.unlinkSync(sessionFile);
|
|
@@ -299,7 +299,7 @@ function getGitCommits(projectDir, fromTs, toTs) {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
function exportSessionMarkdown(sessionId, project) {
|
|
302
|
-
const projectKey = project.replace(
|
|
302
|
+
const projectKey = project.replace(/[\/\.]/g, '-');
|
|
303
303
|
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
|
|
304
304
|
|
|
305
305
|
if (!fs.existsSync(sessionFile)) {
|
|
@@ -332,33 +332,104 @@ function exportSessionMarkdown(sessionId, project) {
|
|
|
332
332
|
|
|
333
333
|
// ── Session Preview (first N messages, lightweight) ────────
|
|
334
334
|
|
|
335
|
+
function findSessionFile(sessionId, project) {
|
|
336
|
+
// Try Claude projects dir
|
|
337
|
+
if (project) {
|
|
338
|
+
const projectKey = project.replace(/[\/\.]/g, '-');
|
|
339
|
+
const claudeFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
|
|
340
|
+
if (fs.existsSync(claudeFile)) return { file: claudeFile, format: 'claude' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Try all Claude project dirs
|
|
344
|
+
if (fs.existsSync(PROJECTS_DIR)) {
|
|
345
|
+
for (const proj of fs.readdirSync(PROJECTS_DIR)) {
|
|
346
|
+
const f = path.join(PROJECTS_DIR, proj, `${sessionId}.jsonl`);
|
|
347
|
+
if (fs.existsSync(f)) return { file: f, format: 'claude' };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Try Codex sessions dir (walk year/month/day)
|
|
352
|
+
const codexSessionsDir = path.join(CODEX_DIR, 'sessions');
|
|
353
|
+
if (fs.existsSync(codexSessionsDir)) {
|
|
354
|
+
const walkDir = (dir) => {
|
|
355
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
356
|
+
const full = path.join(dir, entry.name);
|
|
357
|
+
if (entry.isDirectory()) {
|
|
358
|
+
const result = walkDir(full);
|
|
359
|
+
if (result) return result;
|
|
360
|
+
} else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
|
|
361
|
+
return full;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
};
|
|
366
|
+
const codexFile = walkDir(codexSessionsDir);
|
|
367
|
+
if (codexFile) return { file: codexFile, format: 'codex' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function isSystemMessage(text) {
|
|
374
|
+
if (!text) return true;
|
|
375
|
+
var t = text.trim();
|
|
376
|
+
if (t === 'exit' || t === 'quit' || t === '/exit') return true;
|
|
377
|
+
if (t.startsWith('<permissions')) return true;
|
|
378
|
+
if (t.startsWith('<environment_context')) return true;
|
|
379
|
+
if (t.startsWith('<collaboration_mode')) return true;
|
|
380
|
+
if (t.startsWith('# AGENTS.md')) return true;
|
|
381
|
+
if (t.startsWith('<INSTRUCTIONS>')) return true;
|
|
382
|
+
// Codex developer role system prompts
|
|
383
|
+
if (t.startsWith('You are Codex')) return true;
|
|
384
|
+
if (t.startsWith('Filesystem sandboxing')) return true;
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractContent(raw) {
|
|
389
|
+
if (!raw) return '';
|
|
390
|
+
if (typeof raw === 'string') return raw;
|
|
391
|
+
if (Array.isArray(raw)) {
|
|
392
|
+
return raw
|
|
393
|
+
.map(b => (typeof b === 'string' ? b : (b.text || b.input_text || '')))
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join('\n');
|
|
396
|
+
}
|
|
397
|
+
return String(raw);
|
|
398
|
+
}
|
|
399
|
+
|
|
335
400
|
function getSessionPreview(sessionId, project, limit) {
|
|
336
401
|
limit = limit || 10;
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!fs.existsSync(sessionFile)) return [];
|
|
402
|
+
const found = findSessionFile(sessionId, project);
|
|
403
|
+
if (!found) return [];
|
|
341
404
|
|
|
342
405
|
const messages = [];
|
|
343
|
-
const lines = fs.readFileSync(
|
|
406
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
344
407
|
|
|
345
408
|
for (const line of lines) {
|
|
346
409
|
if (messages.length >= limit) break;
|
|
347
410
|
try {
|
|
348
411
|
const entry = JSON.parse(line);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
content = content
|
|
354
|
-
|
|
355
|
-
.
|
|
356
|
-
|
|
412
|
+
|
|
413
|
+
if (found.format === 'claude') {
|
|
414
|
+
// Claude: {type: "user"|"assistant", message: {content: ...}}
|
|
415
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
416
|
+
const content = extractContent((entry.message || {}).content);
|
|
417
|
+
if (content) {
|
|
418
|
+
messages.push({ role: entry.type, content: content.slice(0, 300) });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
// Codex: {type: "response_item", payload: {role: "user"|"assistant", content: [...]}}
|
|
423
|
+
if (entry.type === 'response_item' && entry.payload) {
|
|
424
|
+
const role = entry.payload.role;
|
|
425
|
+
if (role === 'user' || role === 'assistant') {
|
|
426
|
+
const content = extractContent(entry.payload.content);
|
|
427
|
+
// Skip system-like messages
|
|
428
|
+
if (content && !isSystemMessage(content)) {
|
|
429
|
+
messages.push({ role: role, content: content.slice(0, 300) });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
357
432
|
}
|
|
358
|
-
messages.push({
|
|
359
|
-
role: entry.type,
|
|
360
|
-
content: content.slice(0, 300), // short preview
|
|
361
|
-
});
|
|
362
433
|
}
|
|
363
434
|
} catch {}
|
|
364
435
|
}
|
|
@@ -376,7 +447,7 @@ function searchFullText(query, sessions) {
|
|
|
376
447
|
for (const s of sessions) {
|
|
377
448
|
if (s.tool !== 'claude' || !s.has_detail) continue;
|
|
378
449
|
|
|
379
|
-
const projectKey = s.project.replace(
|
|
450
|
+
const projectKey = s.project.replace(/[\/\.]/g, '-');
|
|
380
451
|
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${s.id}.jsonl`);
|
|
381
452
|
if (!fs.existsSync(sessionFile)) continue;
|
|
382
453
|
|
package/src/frontend/app.js
CHANGED
|
@@ -1344,23 +1344,38 @@ async function checkForUpdates() {
|
|
|
1344
1344
|
try {
|
|
1345
1345
|
var resp = await fetch('/api/version');
|
|
1346
1346
|
var data = await resp.json();
|
|
1347
|
+
var badge = document.getElementById('versionBadge');
|
|
1348
|
+
|
|
1349
|
+
if (badge) {
|
|
1350
|
+
badge.textContent = 'v' + data.current;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1347
1353
|
if (data.updateAvailable) {
|
|
1354
|
+
if (badge) {
|
|
1355
|
+
badge.textContent = 'v' + data.current + ' → v' + data.latest;
|
|
1356
|
+
badge.classList.add('update-available');
|
|
1357
|
+
badge.title = 'Click to copy update command';
|
|
1358
|
+
badge.onclick = function() {
|
|
1359
|
+
navigator.clipboard.writeText('npm i -g codedash-app@latest').then(function() {
|
|
1360
|
+
showToast('Copied: npm i -g codedash-app@latest');
|
|
1361
|
+
});
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1348
1364
|
var banner = document.getElementById('updateBanner');
|
|
1349
1365
|
var text = document.getElementById('updateText');
|
|
1350
1366
|
if (banner && text) {
|
|
1351
|
-
text.textContent = '
|
|
1367
|
+
text.textContent = 'v' + data.latest + ' available — run: npm i -g codedash-app@latest';
|
|
1352
1368
|
banner.style.display = 'flex';
|
|
1353
|
-
banner.dataset.cmd = 'npm
|
|
1369
|
+
banner.dataset.cmd = 'npm i -g codedash-app@latest';
|
|
1354
1370
|
}
|
|
1355
1371
|
}
|
|
1356
1372
|
} catch {}
|
|
1357
1373
|
}
|
|
1358
1374
|
|
|
1359
1375
|
function copyUpdate() {
|
|
1360
|
-
var
|
|
1361
|
-
var cmd = banner ? banner.dataset.cmd : 'npm update -g codedash-app';
|
|
1376
|
+
var cmd = 'codedash update && codedash restart';
|
|
1362
1377
|
navigator.clipboard.writeText(cmd).then(function() {
|
|
1363
|
-
showToast('Copied: ' + cmd);
|
|
1378
|
+
showToast('Copied: ' + cmd + ' (run in terminal)');
|
|
1364
1379
|
});
|
|
1365
1380
|
}
|
|
1366
1381
|
|
package/src/frontend/index.html
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<div class="sidebar-brand">
|
|
13
13
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
14
14
|
codedash
|
|
15
|
+
<span class="version-badge" id="versionBadge"></span>
|
|
15
16
|
</div>
|
|
16
17
|
<div class="sidebar-item active" data-view="sessions">
|
|
17
18
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
package/src/frontend/styles.css
CHANGED
|
@@ -100,6 +100,26 @@ body {
|
|
|
100
100
|
display: flex;
|
|
101
101
|
align-items: center;
|
|
102
102
|
gap: 8px;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.version-badge {
|
|
107
|
+
font-size: 10px;
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
color: var(--text-muted);
|
|
110
|
+
background: rgba(255,255,255,0.06);
|
|
111
|
+
padding: 1px 6px;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.version-badge.update-available {
|
|
116
|
+
color: var(--accent-green);
|
|
117
|
+
background: rgba(74, 222, 128, 0.15);
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.version-badge.update-available:hover {
|
|
122
|
+
background: rgba(74, 222, 128, 0.25);
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
.sidebar-item {
|