Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ config.json
.DS_Store
*.log
docs/superpowers/
.claude/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,13 @@ signal, not a verdict.
| Rule | Fires when | Meaning |
|---|---|---|
| Low cache hit ratio | cost ≥ $1 **and** cache-read < 50% of input-side tokens | Context rebuilt instead of served from cache (reads are ~10× cheaper). |
| Premium model, short session | used `fable-5` **and** < 20 messages | A short session rarely needs the most expensive model. Est. saving = `fableCost × 0.7`. |
| Premium model, short session | used a top-tier model (e.g. `fable-5` / `mythos-5`) **and** < 20 messages | A short session rarely needs the most expensive model. Est. saving = `premiumCost × 0.7`. |
| Subagent-heavy | cost ≥ $5 **and** subagents > 60% of cost | Fan-out overhead — check the delegation earned its cost. |

Only the premium-model rule estimates dollars. Known limits: a low cache ratio is
often not your fault (5-minute cache TTL, `/clear`, idle gaps); the Sonnet-saving
estimate assumes the task would have succeeded on Sonnet; the cutoffs will produce
some false positives.
often not your fault (5-minute cache TTL, `/clear`, idle gaps); the saving
estimate assumes the task would have succeeded on a cheaper model; the cutoffs
will produce some false positives.

## How costs are computed

Expand Down
2 changes: 1 addition & 1 deletion docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ tuned):
| Rule | Fires when |
|---|---|
| Low cache hit ratio | cost ≥ $1 and cache-read < 50% of input-side tokens |
| Premium model, short session | used `fable-5` and < 20 messages (est. saving = `fableCost × 0.7`) |
| Premium model, short session | used a top-tier model (`fable-5` / `mythos-5`) and < 20 messages (est. saving = `premiumCost × 0.7`) |
| Subagent-heavy | cost ≥ $5 and subagents > 60% of cost |

Only flagged sessions are returned, sorted by cost, capped at 25. It is a "look
Expand Down
4 changes: 2 additions & 2 deletions extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "cccost-dashboard-vscode",
"displayName": "cccost — Cost Dashboard for Claude Code",
"description": "Unofficial. Local, zero-setup cost dashboard for Claude Code — per project, model, and prompt, with an efficiency advisor. Reads ~/.claude/projects/ logs. Not affiliated with Anthropic. Desktop only.",
"version": "2.0.0",
"version": "2.0.2",
"publisher": "turja",
"icon": "icon.png",
"galleryBanner": {
Expand Down
320 changes: 311 additions & 9 deletions lib/core.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cccost-dashboard",
"version": "2.0.0",
"version": "2.0.2",
"description": "Local, zero-setup cost dashboard for Claude Code — per project, client, model, and prompt, with an efficiency advisor.",
"author": "Simanta Deb Turja",
"license": "MIT",
Expand Down
7 changes: 5 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const { buildResponse, buildReport, parseTurns, attributeSubagentTurns } = requi
const { createStore, loadConfig, sessionKeyFor } = require('./lib/scan');

const PORT = process.env.PORT || 3456;
// Loopback-only: this dashboard has no auth, so binding wider would expose
// project paths, prompts, and error samples to anyone on the LAN.
const HOST = '127.0.0.1';
const DIST_DIR = path.join(__dirname, 'web', 'dist');
const INDEX_HTML = path.join(DIST_DIR, 'index.html');
const MIME = {
Expand Down Expand Up @@ -116,7 +119,7 @@ function start() {
}
throw err;
});
server.listen(PORT, () => {
server.listen(PORT, HOST, () => {
console.log(`Dashboard: http://localhost:${PORT} (${store.size} session files)`);
});
}
Expand All @@ -125,4 +128,4 @@ if (require.main === module) {
start();
}

module.exports = { sessionKeyFor, resolveAssetPath };
module.exports = { sessionKeyFor, resolveAssetPath, server, HOST };
454 changes: 436 additions & 18 deletions test/core.test.js

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { test } = require('node:test');
const assert = require('node:assert');
const path = require('node:path');
// Requiring server.js does not start a listener (guarded by require.main).
const { sessionKeyFor, resolveAssetPath } = require('../server');
const { sessionKeyFor, resolveAssetPath, server, HOST } = require('../server');

test('sessionKeyFor: main file vs nested subagent file share one key', () => {
const main = sessionKeyFor('proj-dir', 'abc123.jsonl');
Expand All @@ -23,3 +23,11 @@ test('resolveAssetPath: serves under assets, rejects traversal', () => {
assert.strictEqual(resolveAssetPath('/assets/../secret'), null);
assert.strictEqual(resolveAssetPath('/assets/../../../etc/passwd'), null);
});

test('server binds to loopback only, not every interface', () => new Promise((resolve) => {
assert.strictEqual(HOST, '127.0.0.1');
server.listen(0, HOST, () => {
assert.strictEqual(server.address().address, '127.0.0.1');
server.close(resolve);
});
}));
4 changes: 2 additions & 2 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "claude-code-cost-dashboard-web",
"private": true,
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT",
"type": "module",
"engines": {
Expand Down
10 changes: 9 additions & 1 deletion web/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import ProjectsTable from './components/ProjectsTable.jsx';
import ModelsTable from './components/ModelsTable.jsx';
import AdvisorTable from './components/AdvisorTable.jsx';
import SessionsTable from './components/SessionsTable.jsx';
import WasteTable from './components/WasteTable.jsx';
import Diagnostics from './components/Diagnostics.jsx';

const TABS = ['overview', 'breakdown', 'advisor', 'sessions'];
const TABS = ['overview', 'breakdown', 'advisor', 'waste', 'sessions'];

function currentTab() {
const h = window.location.hash.slice(1);
Expand Down Expand Up @@ -91,6 +92,13 @@ export default function App() {
</>
)}

{tab === 'waste' && (
<>
<h2 className="section-label">Repeated waste across sessions</h2>
<WasteTable rows={data.waste} />
</>
)}

{tab === 'sessions' && (
<>
<h2 className="section-label">Sessions</h2>
Expand Down
7 changes: 5 additions & 2 deletions web/src/components/AdvisorTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function AdvisorTable({ rows }) {
<th>Session</th>
<th className="num">Date</th>
<th className="num">Cost</th>
<th className="num">Est. saving</th>
<th className="num" title="Quota/capacity value at API-equivalent pricing — not money saved on a subscription.">Est. capacity</th>
<th>Reasons</th>
</tr>
</thead>
Expand All @@ -27,7 +27,10 @@ export default function AdvisorTable({ rows }) {
<td className="num">{a.estSavingUSD ? fmtUSD(a.estSavingUSD) : '—'}</td>
<td style={{ whiteSpace: 'normal' }}>
{a.reasons.map((r, i) => (
<div key={i}>{r}</div>
<div key={i}>
<div><span className="advisor-rule" title="Rule that triggered this advice">{r.rule}</span> {r.text}</div>
<div className="advisor-action">{r.action}</div>
</div>
))}
</td>
</tr>
Expand Down
1 change: 1 addition & 0 deletions web/src/components/TabNav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const LABELS = {
overview: 'Overview',
breakdown: 'Breakdown',
advisor: 'Advisor',
waste: 'Waste',
sessions: 'Sessions',
};

Expand Down
3 changes: 3 additions & 0 deletions web/src/components/Tiles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function Tiles({ summary, roi }) {
<div className="meter-head">
<span className="meter-title">
{monthName(last.month)} value vs ${roi.subscriptionUSDPerMonth}/mo plan
{!roi.configured && ' (default)'}
</span>
<span className="meter-read">
{last.multiple.toFixed(1)}
Expand Down Expand Up @@ -65,6 +66,8 @@ export default function Tiles({ summary, roi }) {
</div>
<div className="meter-note">
{monthName(last.month)} returned {fmtUSD(last.valueUSD)} of API-equivalent value.
{!roi.configured &&
` Multiple assumes a $${roi.subscriptionUSDPerMonth}/mo plan — set subscriptionUSDPerMonth in config.json to match yours.`}
</div>
</div>
)}
Expand Down
Loading
Loading