@caius_kong/ccusage-dashboard 0.2.5 → 0.2.6
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/README.md +10 -3
- package/lib/index.html +2 -2
- package/lib/server.py +154 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
**A tiny local dashboard built specifically for [ccusage](https://github.com/ccusage/ccusage).**
|
|
6
6
|
|
|
7
7
|
> ⚠️ **This is not a re-implementation.** Every number you see comes straight from
|
|
8
|
-
> `ccusage ... --json`. No own pricing tables, no re-parsing of session logs —
|
|
9
|
-
> dashboard is a thin view over ccusage's own accurate cost engine. If `ccusage`
|
|
8
|
+
> `ccusage ... --json`. No own pricing tables, no re-parsing of session logs for costs —
|
|
9
|
+
> the dashboard is a thin view over ccusage's own accurate cost engine. If `ccusage`
|
|
10
10
|
> says it, this dashboard shows it.
|
|
11
|
+
>
|
|
12
|
+
> The only exception is the **workdir label** in the Sessions tab: ccusage's session
|
|
13
|
+
> report doesn't expose a real directory name for every agent, so the dashboard reads
|
|
14
|
+
> each agent's own local session file (pi/openclaw/claude/codex stores) to display the
|
|
15
|
+
> actual working directory. This is purely a display label — every cost/token number
|
|
16
|
+
> still comes 100% from ccusage.
|
|
11
17
|
|
|
12
18
|
</div>
|
|
13
19
|
|
|
@@ -53,9 +59,10 @@ CCUSAGE_UI_NO_OPEN=1 npx @caius_kong/ccusage-dashboard # don't auto-open brows
|
|
|
53
59
|
| **Today / This Week / This Month / Custom Range** | totals + tokens + cache breakdown |
|
|
54
60
|
| **By model** | per-model cost, % of total, in/out/cache-read/cache-write tokens |
|
|
55
61
|
| **30-day trend** | daily cost bar chart (hover for values, weekends marked) |
|
|
62
|
+
| **Sessions** | per-session rows grouped by workdir name + short session id, filterable 7d/30d/90d/all, sorted by cost |
|
|
56
63
|
| **Budget alert** | monthly cap (default $300) — green <80%, yellow <100%, red ≥100% |
|
|
57
64
|
|
|
58
|
-
All costs in USD. Auto-refresh
|
|
65
|
+
All costs in USD. Auto-refresh every 15s.
|
|
59
66
|
|
|
60
67
|
## How it works
|
|
61
68
|
|
package/lib/index.html
CHANGED
|
@@ -188,8 +188,8 @@ function renderSessions(data){
|
|
|
188
188
|
if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
|
|
189
189
|
$("sessionsList").innerHTML=list.map(s=>{
|
|
190
190
|
const label = s.hasCwd
|
|
191
|
-
? `<span class="name" title="workdir
|
|
192
|
-
: `<span class="name muted" title="workdir
|
|
191
|
+
? `<span class="name" title="workdir: ${esc(s.cwd||s.projectKey)}">${esc(s.dirName||s.id.slice(0,8))} <span class="pill sessid">${esc(s.id.slice(0,8))}</span></span>`
|
|
192
|
+
: `<span class="name muted" title="no workdir found for this ${esc(s.agent)} session">${esc(s.id.slice(0,8))}</span>`;
|
|
193
193
|
const last=(s.lastActivity||"").slice(0,10);
|
|
194
194
|
return `<div class="row">
|
|
195
195
|
<div class="row-top"><span>${label} <span class="pill">${esc(s.agent)}</span></span><span class="cost">${fmtUsd(s.cost)}</span></div>
|
package/lib/server.py
CHANGED
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import argparse
|
|
20
20
|
import json
|
|
21
|
+
import os
|
|
21
22
|
import re
|
|
22
23
|
import shutil
|
|
23
24
|
import subprocess
|
|
@@ -194,8 +195,9 @@ def trend(days: int = 30) -> dict:
|
|
|
194
195
|
|
|
195
196
|
|
|
196
197
|
def sessions(since_days: int = 30) -> dict:
|
|
197
|
-
"""Return ccusage's session report, with
|
|
198
|
-
|
|
198
|
+
"""Return ccusage's session report, each with its real workdir (dirName) when
|
|
199
|
+
the agent's local session store records one. Every numeric cost/token field
|
|
200
|
+
comes 100% from ccusage; the workdir is only a display label.
|
|
199
201
|
"""
|
|
200
202
|
data = run_ccusage(
|
|
201
203
|
["session", "--by-agent", "--json", "--offline"],
|
|
@@ -218,33 +220,39 @@ def sessions(since_days: int = 30) -> dict:
|
|
|
218
220
|
if act_date is not None and act_date < cutoff:
|
|
219
221
|
continue
|
|
220
222
|
project_raw = meta.get("projectPath") or ""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
# Resolve the authoritative workdir (dir name) for every agent that stores one
|
|
224
|
+
# locally. ccusage's projectPath only covers pi and is lossy for hyphenated names
|
|
225
|
+
# ("fund-tracker" -> "tracker"). Each agent keeps its own session store whose
|
|
226
|
+
# files carry the real cwd, so we read those. This ONLY affects the display label
|
|
227
|
+
# (dirName/cwd) — every numeric cost/token field still comes 100% from ccusage.
|
|
228
|
+
agent_name = r.get("agent", "?")
|
|
229
|
+
cwd = ""
|
|
230
|
+
dir_name = ""
|
|
231
|
+
if sid:
|
|
232
|
+
disk_cwd = _cwd_for(agent_name, sid, project_raw)
|
|
229
233
|
if disk_cwd:
|
|
230
234
|
cwd = disk_cwd
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
dir_name =
|
|
235
|
+
cwd_base = disk_cwd.rstrip("/").split("/")[-1]
|
|
236
|
+
if cwd_base:
|
|
237
|
+
dir_name = cwd_base
|
|
238
|
+
if not cwd:
|
|
239
|
+
# fall back to a best-effort decode of ccusage's projectPath (pi only)
|
|
240
|
+
cwd = _decode_cwd(project_raw)
|
|
241
|
+
dir_name = _basename(project_raw)
|
|
234
242
|
out.append(
|
|
235
243
|
{
|
|
236
244
|
"id": sid or "",
|
|
237
|
-
"agent":
|
|
245
|
+
"agent": agent_name,
|
|
238
246
|
"cost": round(r.get("totalCost", 0) or 0, 4),
|
|
239
247
|
"inputTokens": r.get("inputTokens", 0),
|
|
240
248
|
"outputTokens": r.get("outputTokens", 0),
|
|
241
249
|
"cacheReadTokens": r.get("cacheReadTokens", 0),
|
|
242
250
|
"cacheCreationTokens": r.get("cacheCreationTokens", 0),
|
|
243
251
|
"lastActivity": last_activity,
|
|
244
|
-
"cwd": cwd, # real workdir when
|
|
245
|
-
"dirName": dir_name, # real dir name when
|
|
252
|
+
"cwd": cwd, # real workdir when resolved, else best-effort decode
|
|
253
|
+
"dirName": dir_name, # real dir name when resolved, else last path segment
|
|
246
254
|
"projectKey": project_raw, # raw ccusage projectPath
|
|
247
|
-
"hasCwd": bool(
|
|
255
|
+
"hasCwd": bool(cwd),
|
|
248
256
|
}
|
|
249
257
|
)
|
|
250
258
|
out.sort(key=lambda s: s["cost"], reverse=True)
|
|
@@ -269,29 +277,141 @@ def _pi_cwd_from_disk(project_key: str):
|
|
|
269
277
|
if not root.is_dir():
|
|
270
278
|
return None
|
|
271
279
|
candidates = sorted(glob.glob(str(root / "*.jsonl")))
|
|
272
|
-
if not candidates:
|
|
273
|
-
return None
|
|
274
280
|
for path in candidates:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
line = line.strip()
|
|
279
|
-
if not line:
|
|
280
|
-
continue
|
|
281
|
-
try:
|
|
282
|
-
rec = json.loads(line)
|
|
283
|
-
except Exception:
|
|
284
|
-
continue
|
|
285
|
-
cwd = rec.get("cwd")
|
|
286
|
-
if isinstance(cwd, str) and cwd.strip():
|
|
287
|
-
return cwd.strip().rstrip("/")
|
|
288
|
-
except (OSError, ValueError):
|
|
289
|
-
continue
|
|
281
|
+
cwd = _cwd_from_jsonl(path)
|
|
282
|
+
if cwd:
|
|
283
|
+
return cwd
|
|
290
284
|
except Exception:
|
|
291
285
|
return None
|
|
292
286
|
return None
|
|
293
287
|
|
|
294
288
|
|
|
289
|
+
# --- Unified authoritative-workdir resolution for every agent that stores one ---
|
|
290
|
+
#
|
|
291
|
+
# ccusage's session report only exposes projectPath for pi (and it's lossy for
|
|
292
|
+
# hyphenated dir names). To show the REAL dir name for all sessions we resolve the
|
|
293
|
+
# workdir from each agent's own local session store. This drives only the DISPLAY
|
|
294
|
+
# label (dirName); every numeric cost/token value still comes 100% from ccusage.
|
|
295
|
+
|
|
296
|
+
_CWD_CACHE = {} # "agent\x1fsid" -> cwd | None
|
|
297
|
+
_AGENT_INDEX = {} # agent -> {session_id: [paths]} (built lazily)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _cwd_from_jsonl(path):
|
|
301
|
+
"""Scan a session .jsonl (first few lines) for the first real 'cwd' string."""
|
|
302
|
+
try:
|
|
303
|
+
with open(path, encoding="utf-8") as fh:
|
|
304
|
+
for _ in range(80):
|
|
305
|
+
line = fh.readline()
|
|
306
|
+
if not line:
|
|
307
|
+
break
|
|
308
|
+
line = line.strip()
|
|
309
|
+
if not line:
|
|
310
|
+
continue
|
|
311
|
+
try:
|
|
312
|
+
rec = json.loads(line)
|
|
313
|
+
except Exception:
|
|
314
|
+
continue
|
|
315
|
+
if isinstance(rec, dict):
|
|
316
|
+
cwd = rec.get("cwd")
|
|
317
|
+
if isinstance(cwd, str) and cwd.strip():
|
|
318
|
+
return cwd.strip().rstrip("/")
|
|
319
|
+
except (OSError, ValueError):
|
|
320
|
+
return None
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _codex_cwd(path):
|
|
325
|
+
"""codex stores cwd inside the session_meta payload, not top-level."""
|
|
326
|
+
try:
|
|
327
|
+
with open(path, encoding="utf-8") as fh:
|
|
328
|
+
for _ in range(200):
|
|
329
|
+
line = fh.readline()
|
|
330
|
+
if not line:
|
|
331
|
+
break
|
|
332
|
+
line = line.strip()
|
|
333
|
+
if not line:
|
|
334
|
+
continue
|
|
335
|
+
try:
|
|
336
|
+
rec = json.loads(line)
|
|
337
|
+
except Exception:
|
|
338
|
+
continue
|
|
339
|
+
if isinstance(rec, dict) and rec.get("type") == "session_meta":
|
|
340
|
+
cwd = (rec.get("payload") or {}).get("cwd")
|
|
341
|
+
if isinstance(cwd, str) and cwd.strip():
|
|
342
|
+
return cwd.strip().rstrip("/")
|
|
343
|
+
except (OSError, ValueError):
|
|
344
|
+
return None
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _agent_index(agent):
|
|
349
|
+
"""Build (once) a {session_id: [paths]} for an agent's local session store."""
|
|
350
|
+
if agent in _AGENT_INDEX:
|
|
351
|
+
return _AGENT_INDEX[agent]
|
|
352
|
+
home = Path.home()
|
|
353
|
+
idx = {}
|
|
354
|
+
if agent == "openclaw":
|
|
355
|
+
base = home / ".openclaw" / "agents" / "main" / "sessions"
|
|
356
|
+
for f in (base.glob("*.jsonl") if base.is_dir() else []):
|
|
357
|
+
idx.setdefault(f.name[:-6], []).append(str(f))
|
|
358
|
+
elif agent == "claude":
|
|
359
|
+
proj = home / ".claude" / "projects"
|
|
360
|
+
if proj.is_dir():
|
|
361
|
+
for sub in proj.iterdir():
|
|
362
|
+
if sub.is_dir():
|
|
363
|
+
for f in sub.glob("*.jsonl"):
|
|
364
|
+
idx.setdefault(f.name[:-6], []).append(str(f))
|
|
365
|
+
elif agent == "codex":
|
|
366
|
+
root = home / ".codex" / "sessions"
|
|
367
|
+
if root.is_dir():
|
|
368
|
+
for f in root.glob("*/*/*/rollout-*.jsonl"):
|
|
369
|
+
idx.setdefault(f.name[len("rollout-"):-len(".jsonl")], []).append(str(f))
|
|
370
|
+
for f in root.glob("*/*/rollout-*.jsonl"):
|
|
371
|
+
idx.setdefault(f.name[len("rollout-"):-len(".jsonl")], []).append(str(f))
|
|
372
|
+
_AGENT_INDEX[agent] = idx
|
|
373
|
+
return idx
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _session_to_cwd(agent, sid):
|
|
377
|
+
"""Locate an agent's session file by id and return its real cwd (or None)."""
|
|
378
|
+
paths = _agent_index(agent).get(sid)
|
|
379
|
+
if not paths:
|
|
380
|
+
return None
|
|
381
|
+
for p in paths:
|
|
382
|
+
cwd = _codex_cwd(p) if agent == "codex" else _cwd_from_jsonl(p)
|
|
383
|
+
if cwd:
|
|
384
|
+
return cwd
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _cwd_for(agent, sid, project_key=""):
|
|
389
|
+
"""Real workdir for a session. pi resolves by project key; the rest by session id.
|
|
390
|
+
Returns the real cwd (or None if unresolvable). Only drives the DISPLAY label."""
|
|
391
|
+
if agent == "pi":
|
|
392
|
+
if not project_key:
|
|
393
|
+
return None
|
|
394
|
+
key = "pi\x1f" + project_key
|
|
395
|
+
if key in _CWD_CACHE:
|
|
396
|
+
return _CWD_CACHE[key]
|
|
397
|
+
cwd = _pi_cwd_from_disk(project_key)
|
|
398
|
+
_CWD_CACHE[key] = cwd
|
|
399
|
+
return cwd
|
|
400
|
+
if not sid:
|
|
401
|
+
return None
|
|
402
|
+
# codex session ids are date-path prefixed ("2025/10/17/rollout-<id>"); the
|
|
403
|
+
# local file is named "rollout-<id>.jsonl", so match on the final segment.
|
|
404
|
+
lookup_sid = sid.split("/")[-1]
|
|
405
|
+
if lookup_sid.startswith("rollout-") and agent == "codex":
|
|
406
|
+
lookup_sid = lookup_sid[len("rollout-"):]
|
|
407
|
+
key = agent + "\x1f" + lookup_sid
|
|
408
|
+
if key in _CWD_CACHE:
|
|
409
|
+
return _CWD_CACHE[key]
|
|
410
|
+
cwd = _session_to_cwd(agent, lookup_sid)
|
|
411
|
+
_CWD_CACHE[key] = cwd
|
|
412
|
+
return cwd
|
|
413
|
+
|
|
414
|
+
|
|
295
415
|
def _decode_cwd(raw: str) -> str:
|
|
296
416
|
"""Best-effort decode of ccusage's projectPath into a readable path.
|
|
297
417
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caius_kong/ccusage-dashboard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "One-command local dashboard for ccusage: today/week/month/custom cost by model, 30-day trend, monthly budget alert. Numbers straight from ccusage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|