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 +1 -1
- package/src/data.js +86 -3
- package/src/frontend/app.js +82 -4
- package/src/frontend/index.html +21 -0
- package/src/frontend/styles.css +15 -1
- package/src/server.js +9 -1
package/package.json
CHANGED
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
|
-
|
|
604
|
-
const
|
|
605
|
-
const
|
|
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,
|
package/src/frontend/app.js
CHANGED
|
@@ -91,9 +91,16 @@ function formatBytes(bytes) {
|
|
|
91
91
|
|
|
92
92
|
function estimateCost(fileSize) {
|
|
93
93
|
if (!fileSize) return 0;
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
return tokens * 0.
|
|
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() {
|
package/src/frontend/index.html
CHANGED
|
@@ -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)">
|
package/src/frontend/styles.css
CHANGED
|
@@ -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-
|
|
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();
|