codedash-app 3.1.1 → 3.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "3.1.1",
3
+ "version": "3.3.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
@@ -589,6 +589,86 @@ function getSessionReplay(sessionId, project) {
589
589
  };
590
590
  }
591
591
 
592
+ // ── Pricing per model (per token, April 2026) ─────────────
593
+
594
+ const MODEL_PRICING = {
595
+ 'claude-opus-4-6': { input: 5.00 / 1e6, output: 25.00 / 1e6, cache_read: 0.50 / 1e6, cache_create: 6.25 / 1e6 },
596
+ 'claude-opus-4-5': { input: 5.00 / 1e6, output: 25.00 / 1e6, cache_read: 0.50 / 1e6, cache_create: 6.25 / 1e6 },
597
+ 'claude-sonnet-4-6': { input: 3.00 / 1e6, output: 15.00 / 1e6, cache_read: 0.30 / 1e6, cache_create: 3.75 / 1e6 },
598
+ 'claude-sonnet-4-5': { input: 3.00 / 1e6, output: 15.00 / 1e6, cache_read: 0.30 / 1e6, cache_create: 3.75 / 1e6 },
599
+ 'claude-haiku-4-5': { input: 1.00 / 1e6, output: 5.00 / 1e6, cache_read: 0.10 / 1e6, cache_create: 1.25 / 1e6 },
600
+ 'codex-mini-latest': { input: 1.50 / 1e6, output: 6.00 / 1e6, cache_read: 0.375 / 1e6, cache_create: 1.875 / 1e6 },
601
+ 'gpt-5': { input: 1.25 / 1e6, output: 10.00 / 1e6, cache_read: 0.625 / 1e6, cache_create: 1.25 / 1e6 },
602
+ };
603
+
604
+ function getModelPricing(model) {
605
+ if (!model) return MODEL_PRICING['claude-sonnet-4-6']; // default
606
+ for (const key in MODEL_PRICING) {
607
+ if (model.includes(key) || model.startsWith(key)) return MODEL_PRICING[key];
608
+ }
609
+ // Fallback: try partial match
610
+ if (model.includes('opus')) return MODEL_PRICING['claude-opus-4-6'];
611
+ if (model.includes('haiku')) return MODEL_PRICING['claude-haiku-4-5'];
612
+ if (model.includes('sonnet')) return MODEL_PRICING['claude-sonnet-4-6'];
613
+ if (model.includes('codex')) return MODEL_PRICING['codex-mini-latest'];
614
+ return MODEL_PRICING['claude-sonnet-4-6'];
615
+ }
616
+
617
+ // ── Compute real cost from session file token usage ────────
618
+
619
+ function computeSessionCost(sessionId, project) {
620
+ const found = findSessionFile(sessionId, project);
621
+ if (!found) return { cost: 0, inputTokens: 0, outputTokens: 0, model: '' };
622
+
623
+ let totalCost = 0;
624
+ let totalInput = 0;
625
+ let totalOutput = 0;
626
+ let model = '';
627
+
628
+ try {
629
+ const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
630
+ for (const line of lines) {
631
+ try {
632
+ const entry = JSON.parse(line);
633
+ if (found.format === 'claude' && entry.type === 'assistant') {
634
+ const msg = entry.message || {};
635
+ if (!model && msg.model) model = msg.model;
636
+ const u = msg.usage;
637
+ if (!u) continue;
638
+
639
+ const pricing = getModelPricing(msg.model || model);
640
+ const inp = u.input_tokens || 0;
641
+ const cacheCreate = u.cache_creation_input_tokens || 0;
642
+ const cacheRead = u.cache_read_input_tokens || 0;
643
+ const out = u.output_tokens || 0;
644
+
645
+ totalInput += inp + cacheCreate + cacheRead;
646
+ totalOutput += out;
647
+ totalCost += inp * pricing.input
648
+ + cacheCreate * pricing.cache_create
649
+ + cacheRead * pricing.cache_read
650
+ + out * pricing.output;
651
+ }
652
+ // Codex: estimate from file size (no token usage in session files)
653
+ } catch {}
654
+ }
655
+ } catch {}
656
+
657
+ // Fallback for Codex or sessions without usage data
658
+ if (totalCost === 0 && found.format === 'codex') {
659
+ try {
660
+ const size = fs.statSync(found.file).size;
661
+ const tokens = size / 4;
662
+ const pricing = MODEL_PRICING['codex-mini-latest'];
663
+ totalInput = Math.round(tokens * 0.3);
664
+ totalOutput = Math.round(tokens * 0.7);
665
+ totalCost = totalInput * pricing.input + totalOutput * pricing.output;
666
+ } catch {}
667
+ }
668
+
669
+ return { cost: totalCost, inputTokens: totalInput, outputTokens: totalOutput, model };
670
+ }
671
+
592
672
  // ── Cost analytics ────────────────────────────────────────
593
673
 
594
674
  function getCostAnalytics(sessions) {
@@ -600,9 +680,10 @@ function getCostAnalytics(sessions) {
600
680
  const sessionCosts = [];
601
681
 
602
682
  for (const s of sessions) {
603
- if (!s.file_size) continue;
604
- const tokens = s.file_size / 4;
605
- const cost = tokens * 0.000015 * 0.3 + tokens * 0.000075 * 0.7;
683
+ const costData = computeSessionCost(s.id, s.project);
684
+ const cost = costData.cost;
685
+ const tokens = costData.inputTokens + costData.outputTokens;
686
+ if (cost === 0 && tokens === 0) continue;
606
687
  totalCost += cost;
607
688
  totalTokens += tokens;
608
689
 
@@ -746,6 +827,8 @@ module.exports = {
746
827
  getActiveSessions,
747
828
  getSessionReplay,
748
829
  getCostAnalytics,
830
+ computeSessionCost,
831
+ MODEL_PRICING,
749
832
  CLAUDE_DIR,
750
833
  CODEX_DIR,
751
834
  HISTORY_FILE,
@@ -91,9 +91,16 @@ function formatBytes(bytes) {
91
91
 
92
92
  function estimateCost(fileSize) {
93
93
  if (!fileSize) return 0;
94
- const tokens = fileSize / 4;
95
- // Rough estimate: 30% input tokens, 70% output tokens
96
- return tokens * 0.000015 * 0.3 + tokens * 0.000075 * 0.7;
94
+ var tokens = fileSize / 4;
95
+ // Quick card badge estimate (Sonnet 4.6: $3/M in, $15/M out)
96
+ return tokens * 0.3 * (3.0 / 1e6) + tokens * 0.7 * (15.0 / 1e6);
97
+ }
98
+
99
+ async function loadRealCost(sessionId, project) {
100
+ try {
101
+ var resp = await fetch('/api/cost/' + sessionId + '?project=' + encodeURIComponent(project));
102
+ return await resp.json();
103
+ } catch (e) { return null; }
97
104
  }
98
105
 
99
106
  // ── Tag system ─────────────────────────────────────────────────
@@ -1050,8 +1057,9 @@ async function openDetail(s) {
1050
1057
  infoHtml += '<div class="detail-row"><span class="detail-label">Messages</span><span>' + (s.detail_messages || s.messages || 0) + '</span></div>';
1051
1058
  infoHtml += '<div class="detail-row"><span class="detail-label">File size</span><span>' + formatBytes(s.file_size) + '</span></div>';
1052
1059
  if (costStr) {
1053
- infoHtml += '<div class="detail-row"><span class="detail-label">Est. cost</span><span class="cost-badge">' + costStr + '</span></div>';
1060
+ infoHtml += '<div class="detail-row"><span class="detail-label">Est. cost</span><span class="cost-badge" id="detail-cost">' + costStr + '</span></div>';
1054
1061
  }
1062
+ infoHtml += '<div class="detail-row" id="detail-real-cost" style="display:none"><span class="detail-label">Real cost</span><span></span></div>';
1055
1063
  // Tags
1056
1064
  infoHtml += '<div class="detail-row"><span class="detail-label">Tags</span><span class="card-tags">';
1057
1065
  sessionTags.forEach(function(t) {
@@ -1110,6 +1118,23 @@ async function openDetail(s) {
1110
1118
  body.querySelector('.detail-messages').innerHTML = '<div class="empty-state">No detail file available for this session.</div>';
1111
1119
  }
1112
1120
 
1121
+ // Load real cost
1122
+ loadRealCost(s.id, s.project || '').then(function(costData) {
1123
+ if (!costData || !costData.cost) return;
1124
+ var row = document.getElementById('detail-real-cost');
1125
+ if (row) {
1126
+ row.style.display = '';
1127
+ row.querySelector('span:last-child').innerHTML =
1128
+ '<span class="cost-badge" style="background:rgba(74,222,128,0.2);color:var(--accent-green)">$' + costData.cost.toFixed(2) + '</span>' +
1129
+ ' <span style="font-size:11px;color:var(--text-muted)">' +
1130
+ formatTokens(costData.inputTokens) + ' in / ' + formatTokens(costData.outputTokens) + ' out' +
1131
+ (costData.model ? ' (' + costData.model + ')' : '') + '</span>';
1132
+ }
1133
+ // Update estimated badge to show it was estimated
1134
+ var estBadge = document.getElementById('detail-cost');
1135
+ if (estBadge) estBadge.style.opacity = '0.5';
1136
+ });
1137
+
1113
1138
  // Load git commits
1114
1139
  if (s.project) {
1115
1140
  var commits = await loadGitCommits(s.project, s.first_ts, s.last_ts);
@@ -1719,6 +1744,59 @@ function focusSession(sessionId) {
1719
1744
  });
1720
1745
  }
1721
1746
 
1747
+ // ── Install agents ────────────────────────────────────────────
1748
+
1749
+ var AGENT_INSTALL = {
1750
+ claude: {
1751
+ name: 'Claude Code',
1752
+ cmd: 'curl -fsSL https://claude.ai/install.sh | bash',
1753
+ alt: 'npm i -g @anthropic-ai/claude-code',
1754
+ url: 'https://code.claude.com',
1755
+ },
1756
+ codex: {
1757
+ name: 'Codex CLI',
1758
+ cmd: 'npm i -g @openai/codex',
1759
+ alt: 'brew install --cask codex',
1760
+ url: 'https://github.com/openai/codex',
1761
+ },
1762
+ kiro: {
1763
+ name: 'Kiro CLI',
1764
+ cmd: 'curl -fsSL https://kiro.dev/install.sh | bash',
1765
+ alt: null,
1766
+ url: 'https://kiro.dev/cli/',
1767
+ },
1768
+ opencode: {
1769
+ name: 'OpenCode',
1770
+ cmd: 'curl -fsSL https://opencode.ai/install | bash',
1771
+ alt: 'npm i -g opencode-ai@latest',
1772
+ url: 'https://opencode.ai',
1773
+ },
1774
+ };
1775
+
1776
+ function installAgent(agent) {
1777
+ var info = AGENT_INSTALL[agent];
1778
+ if (!info) return;
1779
+
1780
+ var overlay = document.getElementById('confirmOverlay');
1781
+ document.getElementById('confirmTitle').textContent = 'Install ' + info.name;
1782
+ var html = '<code style="display:block;margin:8px 0;padding:10px;background:var(--bg-card);border-radius:6px;font-size:13px;cursor:pointer" onclick="navigator.clipboard.writeText(\'' + info.cmd.replace(/'/g, "\\'") + '\');document.querySelector(\'#toast\').textContent=\'Copied!\';document.querySelector(\'#toast\').classList.add(\'show\');setTimeout(function(){document.querySelector(\'#toast\').classList.remove(\'show\')},1500)">' + escHtml(info.cmd) + '</code>';
1783
+ if (info.alt) {
1784
+ html += '<span style="font-size:11px;color:var(--text-muted)">or: <code>' + escHtml(info.alt) + '</code></span><br>';
1785
+ }
1786
+ html += '<br><a href="' + info.url + '" target="_blank" style="color:var(--accent-blue);font-size:12px">' + info.url + '</a>';
1787
+ document.getElementById('confirmText').innerHTML = html;
1788
+ document.getElementById('confirmId').textContent = '';
1789
+ document.getElementById('confirmAction').textContent = 'Copy Install Command';
1790
+ document.getElementById('confirmAction').className = 'launch-btn btn-primary';
1791
+ document.getElementById('confirmAction').onclick = function() {
1792
+ navigator.clipboard.writeText(info.cmd).then(function() {
1793
+ showToast('Copied: ' + info.cmd);
1794
+ });
1795
+ closeConfirm();
1796
+ };
1797
+ if (overlay) overlay.style.display = 'flex';
1798
+ }
1799
+
1722
1800
  // ── Export/Import dialog ──────────────────────────────────────
1723
1801
 
1724
1802
  function showExportDialog() {
@@ -53,10 +53,31 @@
53
53
  Codex
54
54
  </div>
55
55
  <div class="sidebar-divider"></div>
56
+ <div class="sidebar-section">Install Agents</div>
57
+ <div class="sidebar-item small" onclick="installAgent('claude')">
58
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
59
+ Claude Code
60
+ </div>
61
+ <div class="sidebar-item small" onclick="installAgent('codex')">
62
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
63
+ Codex CLI
64
+ </div>
65
+ <div class="sidebar-item small" onclick="installAgent('kiro')">
66
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
67
+ Kiro CLI
68
+ </div>
69
+ <div class="sidebar-item small" onclick="installAgent('opencode')">
70
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
71
+ OpenCode
72
+ </div>
73
+ <div class="sidebar-divider"></div>
56
74
  <div class="sidebar-item" onclick="showExportDialog()">
57
75
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
58
76
  Export / Import
59
77
  </div>
78
+ <div class="sidebar-author">
79
+ <a href="https://t.me/neuraldeep" target="_blank">Valerii Kovalskii</a>
80
+ </div>
60
81
  <div class="sidebar-settings">
61
82
  <label>Terminal</label>
62
83
  <select id="terminalSelect" onchange="saveTerminalPref(this.value)">
@@ -135,6 +135,8 @@ body {
135
135
  }
136
136
  .sidebar-item:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); }
137
137
  .sidebar-item.active { color: var(--text-primary); background: rgba(255,255,255,0.08); }
138
+ .sidebar-item.small { padding: 6px 20px; font-size: 12px; }
139
+ .sidebar-item.small svg { width: 14px; height: 14px; }
138
140
  .sidebar-item svg { width: 18px; height: 18px; opacity: 0.7; }
139
141
 
140
142
  .sidebar-divider { height: 1px; background: var(--border); margin: 12px 20px; }
@@ -147,8 +149,20 @@ body {
147
149
  color: var(--text-muted);
148
150
  }
149
151
 
150
- .sidebar-settings {
152
+ .sidebar-author {
151
153
  margin-top: auto;
154
+ padding: 8px 20px 0;
155
+ font-size: 11px;
156
+ text-align: center;
157
+ }
158
+ .sidebar-author a {
159
+ color: var(--text-muted);
160
+ text-decoration: none;
161
+ transition: color 0.15s;
162
+ }
163
+ .sidebar-author a:hover { color: var(--accent-blue); }
164
+
165
+ .sidebar-settings {
152
166
  padding: 12px 16px;
153
167
  border-top: 1px solid var(--border);
154
168
  }
package/src/server.js CHANGED
@@ -3,7 +3,7 @@ const http = require('http');
3
3
  const https = require('https');
4
4
  const { URL } = require('url');
5
5
  const { exec } = require('child_process');
6
- const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics } = require('./data');
6
+ const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data');
7
7
  const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals');
8
8
  const { getHTML } = require('./html');
9
9
 
@@ -140,6 +140,14 @@ function startServer(port, openBrowser = true) {
140
140
  json(res, results);
141
141
  }
142
142
 
143
+ // ── Session cost ──────────────────────
144
+ else if (req.method === 'GET' && pathname.startsWith('/api/cost/')) {
145
+ const sessionId = pathname.split('/').pop();
146
+ const project = parsed.searchParams.get('project') || '';
147
+ const data = computeSessionCost(sessionId, project);
148
+ json(res, data);
149
+ }
150
+
143
151
  // ── Session replay ─────────────────────
144
152
  else if (req.method === 'GET' && pathname.startsWith('/api/replay/')) {
145
153
  const sessionId = pathname.split('/').pop();