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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Termius-style browser dashboard for Claude Code sessions. View, search, resume, and delete sessions with a dark-themed UI.",
5
5
  "bin": {
6
6
  "codedash": "./bin/cli.js"
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(/\//g, '-').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 projectKey = project.replace(/\//g, '-').replace(/^-/, '');
196
- const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
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(sessionFile, 'utf8').split('\n').filter(Boolean);
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
- if (entry.type === 'user' || entry.type === 'assistant') {
209
- const msg = entry.message || {};
210
- let content = msg.content || '';
211
- if (Array.isArray(content)) {
212
- content = content
213
- .map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
214
- .filter(Boolean)
215
- .join('\n');
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(/\//g, '-').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(/\//g, '-').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 projectKey = project.replace(/\//g, '-').replace(/^-/, '');
338
- const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
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(sessionFile, 'utf8').split('\n').filter(Boolean);
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
- if (entry.type === 'user' || entry.type === 'assistant') {
350
- const msg = entry.message || {};
351
- let content = msg.content || '';
352
- if (Array.isArray(content)) {
353
- content = content
354
- .map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
355
- .filter(Boolean)
356
- .join('\n');
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(/\//g, '-').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
 
@@ -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 = 'Update available: v' + data.current + ' v' + data.latest;
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 update -g codedash-app && codedash run';
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 banner = document.getElementById('updateBanner');
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
 
@@ -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>
@@ -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 {