@caius_kong/ccusage-dashboard 0.2.5 → 0.2.7
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 +12 -4
- package/lib/index.html +27 -29
- package/lib/server.py +187 -43
- 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
|
|
|
@@ -50,12 +56,14 @@ CCUSAGE_UI_NO_OPEN=1 npx @caius_kong/ccusage-dashboard # don't auto-open brows
|
|
|
50
56
|
|
|
51
57
|
| | |
|
|
52
58
|
|---|---|
|
|
53
|
-
| **Today / This Week / This Month / Custom Range** | totals + tokens + cache breakdown |
|
|
59
|
+
| **Today / This Week / This Month / Custom Range** | totals + tokens + cache breakdown + **cache hit rate** |
|
|
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, sorted by cost — shares the top time-filter tabs (today/week/month/custom) |
|
|
56
63
|
| **Budget alert** | monthly cap (default $300) — green <80%, yellow <100%, red ≥100% |
|
|
57
64
|
|
|
58
|
-
All costs in USD.
|
|
65
|
+
All costs in USD. **Cache hit rate** is the standard input-side metric:
|
|
66
|
+
`cacheReadTokens / (cacheReadTokens + non-cached inputTokens)`. Auto-refresh every 15s.
|
|
59
67
|
|
|
60
68
|
## How it works
|
|
61
69
|
|
package/lib/index.html
CHANGED
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="week">This Week</div>
|
|
78
78
|
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="month">This Month</div>
|
|
79
79
|
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="range">Custom Range</div>
|
|
80
|
-
<div class="tab" role="tab" tabindex="0" aria-selected="false" data-tab="sessions">Sessions</div>
|
|
81
80
|
<div class="tab-budget">
|
|
82
81
|
<span class="budget-pill" id="budgetPill">budget …</span>
|
|
83
82
|
</div>
|
|
@@ -93,6 +92,7 @@
|
|
|
93
92
|
<div class="card"><div class="label">Tokens</div><div class="value" id="c2">–</div></div>
|
|
94
93
|
<div class="card"><div class="label">Cache Read</div><div class="value" id="c3">–</div></div>
|
|
95
94
|
<div class="card"><div class="label">Cache Write</div><div class="value" id="c4">–</div></div>
|
|
95
|
+
<div class="card"><div class="label">Cache Hit Rate</div><div class="value" id="c5">–</div><div class="period" id="c5p"></div></div>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
98
|
<div id="rangeRow" class="panel" style="display:none">
|
|
@@ -108,18 +108,10 @@
|
|
|
108
108
|
|
|
109
109
|
<div class="panel"><h2>Models <span class="tot" id="modelSum"></span></h2><div id="modelList"><div class="empty">loading…</div></div></div>
|
|
110
110
|
|
|
111
|
-
<div class="panel" id="sessionsPanel"
|
|
111
|
+
<div class="panel" id="sessionsPanel">
|
|
112
112
|
<h2>Sessions <span class="tot" id="sessSum"></span>
|
|
113
113
|
<span class="pill" id="sessRange"></span>
|
|
114
114
|
</h2>
|
|
115
|
-
<div class="range" style="margin-bottom:12px">
|
|
116
|
-
<span class="pill">last</span>
|
|
117
|
-
<button data-sdays="7" class="sday">7d</button>
|
|
118
|
-
<button data-sdays="30" class="sday active">30d</button>
|
|
119
|
-
<button data-sdays="90" class="sday">90d</button>
|
|
120
|
-
<button data-sdays="365" class="sday">all</button>
|
|
121
|
-
<span class="pill" style="margin-left:auto" id="sessHint"></span>
|
|
122
|
-
</div>
|
|
123
115
|
<div id="sessionsList"><div class="empty">loading…</div></div>
|
|
124
116
|
</div>
|
|
125
117
|
|
|
@@ -127,8 +119,8 @@
|
|
|
127
119
|
|
|
128
120
|
<script>
|
|
129
121
|
const $=(id)=>document.getElementById(id);
|
|
130
|
-
const tabs={today:{label:"Today",len:0},week:{label:"This Week",len:0},month:{label:"This Month",len:0},range:{label:"Custom Range",len:1}
|
|
131
|
-
let active="today", modelCtx=[]
|
|
122
|
+
const tabs={today:{label:"Today",len:0},week:{label:"This Week",len:0},month:{label:"This Month",len:0},range:{label:"Custom Range",len:1}};
|
|
123
|
+
let active="today", modelCtx=[];
|
|
132
124
|
|
|
133
125
|
function fmtUsd(n){return "$"+(n==null?"–":Number(n).toFixed(2));}
|
|
134
126
|
function fmtTok(n){
|
|
@@ -138,6 +130,7 @@ function fmtTok(n){
|
|
|
138
130
|
if(n>=1e3)return (n/1e3).toFixed(1)+"k";
|
|
139
131
|
return String(n);
|
|
140
132
|
}
|
|
133
|
+
function fmtPct(n){return n==null?"–":n.toFixed(1)+"%";}
|
|
141
134
|
function esc(s){const d=document.createElement("div");d.textContent=s;return d.innerHTML;}
|
|
142
135
|
|
|
143
136
|
function renderModels(data){
|
|
@@ -173,8 +166,17 @@ function applyBudget(month){
|
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
async function loadSessions(){
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
// follow the top-level time filter (today/week/month/custom range)
|
|
170
|
+
let q="";
|
|
171
|
+
if(active==="today")q="period=today";
|
|
172
|
+
else if(active==="week")q="period=week";
|
|
173
|
+
else if(active==="month")q="period=month";
|
|
174
|
+
else if(active==="range"){
|
|
175
|
+
const f=$("fromDate").value,t=$("toDate").value;
|
|
176
|
+
if(!f||!t){return;}
|
|
177
|
+
q=`from=${f}&to=${t}`;
|
|
178
|
+
}else{return;}
|
|
179
|
+
const data=await(await fetch(`/api/sessions?${q}`)).json();
|
|
178
180
|
if(data.error)throw new Error(data.error);
|
|
179
181
|
renderSessions(data);
|
|
180
182
|
}
|
|
@@ -182,14 +184,14 @@ async function loadSessions(){
|
|
|
182
184
|
function renderSessions(data){
|
|
183
185
|
const list=data.sessions||[];
|
|
184
186
|
const total=data.total||0;
|
|
185
|
-
const sum=list.reduce((a,s)=>a+(s.cost||0),0);
|
|
187
|
+
const sum=data.totalCost!=null?data.totalCost:list.reduce((a,s)=>a+(s.cost||0),0);
|
|
186
188
|
$("sessSum").textContent=fmtUsd(sum);
|
|
187
|
-
$("sessRange").textContent=`${total} sessions
|
|
189
|
+
$("sessRange").textContent=`${total} sessions`;
|
|
188
190
|
if(!list.length){$("sessionsList").innerHTML='<div class="empty">no sessions in range</div>';return;}
|
|
189
191
|
$("sessionsList").innerHTML=list.map(s=>{
|
|
190
192
|
const label = s.hasCwd
|
|
191
|
-
? `<span class="name" title="workdir
|
|
192
|
-
: `<span class="name muted" title="workdir
|
|
193
|
+
? `<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>`
|
|
194
|
+
: `<span class="name muted" title="no workdir found for this ${esc(s.agent)} session">${esc(s.id.slice(0,8))}</span>`;
|
|
193
195
|
const last=(s.lastActivity||"").slice(0,10);
|
|
194
196
|
return `<div class="row">
|
|
195
197
|
<div class="row-top"><span>${label} <span class="pill">${esc(s.agent)}</span></span><span class="cost">${fmtUsd(s.cost)}</span></div>
|
|
@@ -206,6 +208,11 @@ function applyCards(data){
|
|
|
206
208
|
$("c2").textContent=fmtTok(tt);
|
|
207
209
|
$("c3").textContent=fmtTok(data.cacheReadTokens);
|
|
208
210
|
$("c4").textContent=fmtTok(data.cacheCreationTokens);
|
|
211
|
+
// cache hit rate = cache-read input / (cache-read input + non-cached input)
|
|
212
|
+
const hitIn=data.cacheReadTokens||0, plainIn=data.inputTokens||0;
|
|
213
|
+
const hitRate = (hitIn+plainIn)>0 ? (hitIn/(hitIn+plainIn))*100 : null;
|
|
214
|
+
$("c5").innerHTML=fmtPct(hitRate)+(hitRate!=null?"<small> cache</small>":"");
|
|
215
|
+
$("c5p").textContent=hitRate!=null?`${fmtTok(plainIn)} miss · ${fmtTok(hitIn)} hit`:"";
|
|
209
216
|
$("c1label").textContent=tabs[active].label;
|
|
210
217
|
$("modelSum").textContent=fmtUsd(total);
|
|
211
218
|
}
|
|
@@ -298,6 +305,7 @@ async function tick(){
|
|
|
298
305
|
applyBudget(month);
|
|
299
306
|
await loadModels();
|
|
300
307
|
await loadTrend();
|
|
308
|
+
await loadSessions();
|
|
301
309
|
dot.className="dot ok";st.textContent="live";
|
|
302
310
|
$("lastUpd").textContent="last updated "+new Date().toLocaleTimeString();
|
|
303
311
|
}catch(e){dot.className="dot err";st.textContent="error: "+e.message;}
|
|
@@ -310,28 +318,18 @@ document.querySelectorAll(".tab").forEach(t=>{
|
|
|
310
318
|
t.setAttribute("aria-selected","true");
|
|
311
319
|
active=t.dataset.tab;
|
|
312
320
|
$("rangeRow").style.display = active==="range"?"block":"none";
|
|
313
|
-
$("sessionsPanel").style.display = active==="sessions"?"block":"none";
|
|
314
321
|
if(active==="range"){
|
|
315
322
|
if(!$("fromDate").value){const e=new Date();$("toDate").value=e.toISOString().slice(0,10);const s=new Date();s.setDate(s.getDate()-6);$("fromDate").value=s.toISOString().slice(0,10);}
|
|
316
323
|
rangeApply();
|
|
317
|
-
}else if(active==="sessions"){
|
|
318
|
-
loadSessions();
|
|
319
324
|
}else{
|
|
320
325
|
loadModels();
|
|
321
326
|
}
|
|
327
|
+
loadSessions();
|
|
322
328
|
};
|
|
323
329
|
t.addEventListener("click",activate);
|
|
324
330
|
t.addEventListener("keydown",(e)=>{if(e.key==="Enter"||e.key===" "||e.key==="Spacebar"){e.preventDefault();activate();}});
|
|
325
331
|
});
|
|
326
332
|
$("rangeGo").addEventListener("click",rangeApply);
|
|
327
|
-
document.querySelectorAll(".sday").forEach(b=>{
|
|
328
|
-
b.addEventListener("click",()=>{
|
|
329
|
-
document.querySelectorAll(".sday").forEach(x=>x.classList.remove("active"));
|
|
330
|
-
b.classList.add("active");
|
|
331
|
-
sessDays=parseInt(b.dataset.sdays,10);
|
|
332
|
-
loadSessions();
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
333
|
|
|
336
334
|
tick();
|
|
337
335
|
setInterval(tick,15000);
|
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
|
|
@@ -193,9 +194,14 @@ def trend(days: int = 30) -> dict:
|
|
|
193
194
|
return {"start": start.isoformat(), "end": end.isoformat(), "days": out}
|
|
194
195
|
|
|
195
196
|
|
|
196
|
-
def sessions(since_days: int = 30) -> dict:
|
|
197
|
-
"""Return ccusage's session report, with
|
|
198
|
-
|
|
197
|
+
def sessions(since_days: int = 30, period: str = "", from_date: str = "", to_date: str = "") -> dict:
|
|
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.
|
|
201
|
+
|
|
202
|
+
Filtering: pass `period` in ("today" | "week" | "month") OR an explicit
|
|
203
|
+
date range via `from_date`/`to_date` (YYYY-MM-DD). `since_days` is kept for
|
|
204
|
+
backward compatibility with the old 7d/30d/90d/all selector.
|
|
199
205
|
"""
|
|
200
206
|
data = run_ccusage(
|
|
201
207
|
["session", "--by-agent", "--json", "--offline"],
|
|
@@ -203,52 +209,76 @@ def sessions(since_days: int = 30) -> dict:
|
|
|
203
209
|
)
|
|
204
210
|
rows = data.get("session") or []
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
# ---- choose the cutoff window ----------------
|
|
213
|
+
today = date.today()
|
|
214
|
+
lo = hi = None
|
|
215
|
+
if from_date and to_date:
|
|
216
|
+
try:
|
|
217
|
+
lo = date.fromisoformat(from_date)
|
|
218
|
+
hi = date.fromisoformat(to_date)
|
|
219
|
+
except ValueError:
|
|
220
|
+
lo = hi = None
|
|
221
|
+
elif period == "today":
|
|
222
|
+
lo = hi = today
|
|
223
|
+
elif period == "week":
|
|
224
|
+
lo = today - timedelta(days=6)
|
|
225
|
+
hi = today
|
|
226
|
+
elif period == "month":
|
|
227
|
+
lo = today.replace(day=1)
|
|
228
|
+
hi = today
|
|
229
|
+
elif period == "custom":
|
|
230
|
+
pass # fall through to since_days below (legacy)
|
|
231
|
+
if lo is None:
|
|
232
|
+
lo = today - timedelta(days=since_days)
|
|
233
|
+
hi = today
|
|
234
|
+
|
|
207
235
|
out = []
|
|
208
236
|
for r in rows:
|
|
209
237
|
meta = r.get("metadata") or {}
|
|
210
|
-
sid = r.get("period")
|
|
238
|
+
sid = r.get("period")
|
|
211
239
|
last_activity = meta.get("lastActivity") or ""
|
|
212
|
-
# date filter by last activity
|
|
240
|
+
# date filter by last activity against [lo, hi]
|
|
213
241
|
if last_activity:
|
|
214
242
|
try:
|
|
215
243
|
act_date = date.fromisoformat(last_activity[:10])
|
|
216
244
|
except ValueError:
|
|
217
245
|
act_date = None
|
|
218
|
-
if act_date is not None
|
|
219
|
-
|
|
246
|
+
if act_date is not None:
|
|
247
|
+
if act_date < lo or act_date > hi:
|
|
248
|
+
continue
|
|
220
249
|
project_raw = meta.get("projectPath") or ""
|
|
221
|
-
|
|
222
|
-
cwd =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
# cost/token data still comes 100% from ccusage.
|
|
227
|
-
if project_raw and dir_name:
|
|
228
|
-
disk_cwd = _pi_cwd_from_disk(project_raw)
|
|
250
|
+
agent_name = r.get("agent", "?")
|
|
251
|
+
cwd = ""
|
|
252
|
+
dir_name = ""
|
|
253
|
+
if sid:
|
|
254
|
+
disk_cwd = _cwd_for(agent_name, sid, project_raw)
|
|
229
255
|
if disk_cwd:
|
|
230
256
|
cwd = disk_cwd
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
dir_name =
|
|
257
|
+
cwd_base = disk_cwd.rstrip("/").split("/")[-1]
|
|
258
|
+
if cwd_base:
|
|
259
|
+
dir_name = cwd_base
|
|
260
|
+
if not cwd:
|
|
261
|
+
cwd = _decode_cwd(project_raw)
|
|
262
|
+
dir_name = _basename(project_raw)
|
|
234
263
|
out.append(
|
|
235
264
|
{
|
|
236
265
|
"id": sid or "",
|
|
237
|
-
"agent":
|
|
266
|
+
"agent": agent_name,
|
|
238
267
|
"cost": round(r.get("totalCost", 0) or 0, 4),
|
|
239
268
|
"inputTokens": r.get("inputTokens", 0),
|
|
240
269
|
"outputTokens": r.get("outputTokens", 0),
|
|
241
270
|
"cacheReadTokens": r.get("cacheReadTokens", 0),
|
|
242
271
|
"cacheCreationTokens": r.get("cacheCreationTokens", 0),
|
|
243
272
|
"lastActivity": last_activity,
|
|
244
|
-
"cwd": cwd,
|
|
245
|
-
"dirName": dir_name,
|
|
246
|
-
"projectKey": project_raw,
|
|
247
|
-
"hasCwd": bool(
|
|
273
|
+
"cwd": cwd,
|
|
274
|
+
"dirName": dir_name,
|
|
275
|
+
"projectKey": project_raw,
|
|
276
|
+
"hasCwd": bool(cwd),
|
|
248
277
|
}
|
|
249
278
|
)
|
|
250
279
|
out.sort(key=lambda s: s["cost"], reverse=True)
|
|
251
|
-
|
|
280
|
+
total_cost = round(sum(s["cost"] for s in out), 4)
|
|
281
|
+
return {"total": len(out), "totalCost": total_cost, "sessions": out}
|
|
252
282
|
|
|
253
283
|
|
|
254
284
|
def _pi_cwd_from_disk(project_key: str):
|
|
@@ -269,29 +299,141 @@ def _pi_cwd_from_disk(project_key: str):
|
|
|
269
299
|
if not root.is_dir():
|
|
270
300
|
return None
|
|
271
301
|
candidates = sorted(glob.glob(str(root / "*.jsonl")))
|
|
272
|
-
if not candidates:
|
|
273
|
-
return None
|
|
274
302
|
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
|
|
303
|
+
cwd = _cwd_from_jsonl(path)
|
|
304
|
+
if cwd:
|
|
305
|
+
return cwd
|
|
290
306
|
except Exception:
|
|
291
307
|
return None
|
|
292
308
|
return None
|
|
293
309
|
|
|
294
310
|
|
|
311
|
+
# --- Unified authoritative-workdir resolution for every agent that stores one ---
|
|
312
|
+
#
|
|
313
|
+
# ccusage's session report only exposes projectPath for pi (and it's lossy for
|
|
314
|
+
# hyphenated dir names). To show the REAL dir name for all sessions we resolve the
|
|
315
|
+
# workdir from each agent's own local session store. This drives only the DISPLAY
|
|
316
|
+
# label (dirName); every numeric cost/token value still comes 100% from ccusage.
|
|
317
|
+
|
|
318
|
+
_CWD_CACHE = {} # "agent\x1fsid" -> cwd | None
|
|
319
|
+
_AGENT_INDEX = {} # agent -> {session_id: [paths]} (built lazily)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _cwd_from_jsonl(path):
|
|
323
|
+
"""Scan a session .jsonl (first few lines) for the first real 'cwd' string."""
|
|
324
|
+
try:
|
|
325
|
+
with open(path, encoding="utf-8") as fh:
|
|
326
|
+
for _ in range(80):
|
|
327
|
+
line = fh.readline()
|
|
328
|
+
if not line:
|
|
329
|
+
break
|
|
330
|
+
line = line.strip()
|
|
331
|
+
if not line:
|
|
332
|
+
continue
|
|
333
|
+
try:
|
|
334
|
+
rec = json.loads(line)
|
|
335
|
+
except Exception:
|
|
336
|
+
continue
|
|
337
|
+
if isinstance(rec, dict):
|
|
338
|
+
cwd = rec.get("cwd")
|
|
339
|
+
if isinstance(cwd, str) and cwd.strip():
|
|
340
|
+
return cwd.strip().rstrip("/")
|
|
341
|
+
except (OSError, ValueError):
|
|
342
|
+
return None
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _codex_cwd(path):
|
|
347
|
+
"""codex stores cwd inside the session_meta payload, not top-level."""
|
|
348
|
+
try:
|
|
349
|
+
with open(path, encoding="utf-8") as fh:
|
|
350
|
+
for _ in range(200):
|
|
351
|
+
line = fh.readline()
|
|
352
|
+
if not line:
|
|
353
|
+
break
|
|
354
|
+
line = line.strip()
|
|
355
|
+
if not line:
|
|
356
|
+
continue
|
|
357
|
+
try:
|
|
358
|
+
rec = json.loads(line)
|
|
359
|
+
except Exception:
|
|
360
|
+
continue
|
|
361
|
+
if isinstance(rec, dict) and rec.get("type") == "session_meta":
|
|
362
|
+
cwd = (rec.get("payload") or {}).get("cwd")
|
|
363
|
+
if isinstance(cwd, str) and cwd.strip():
|
|
364
|
+
return cwd.strip().rstrip("/")
|
|
365
|
+
except (OSError, ValueError):
|
|
366
|
+
return None
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _agent_index(agent):
|
|
371
|
+
"""Build (once) a {session_id: [paths]} for an agent's local session store."""
|
|
372
|
+
if agent in _AGENT_INDEX:
|
|
373
|
+
return _AGENT_INDEX[agent]
|
|
374
|
+
home = Path.home()
|
|
375
|
+
idx = {}
|
|
376
|
+
if agent == "openclaw":
|
|
377
|
+
base = home / ".openclaw" / "agents" / "main" / "sessions"
|
|
378
|
+
for f in (base.glob("*.jsonl") if base.is_dir() else []):
|
|
379
|
+
idx.setdefault(f.name[:-6], []).append(str(f))
|
|
380
|
+
elif agent == "claude":
|
|
381
|
+
proj = home / ".claude" / "projects"
|
|
382
|
+
if proj.is_dir():
|
|
383
|
+
for sub in proj.iterdir():
|
|
384
|
+
if sub.is_dir():
|
|
385
|
+
for f in sub.glob("*.jsonl"):
|
|
386
|
+
idx.setdefault(f.name[:-6], []).append(str(f))
|
|
387
|
+
elif agent == "codex":
|
|
388
|
+
root = home / ".codex" / "sessions"
|
|
389
|
+
if root.is_dir():
|
|
390
|
+
for f in root.glob("*/*/*/rollout-*.jsonl"):
|
|
391
|
+
idx.setdefault(f.name[len("rollout-"):-len(".jsonl")], []).append(str(f))
|
|
392
|
+
for f in root.glob("*/*/rollout-*.jsonl"):
|
|
393
|
+
idx.setdefault(f.name[len("rollout-"):-len(".jsonl")], []).append(str(f))
|
|
394
|
+
_AGENT_INDEX[agent] = idx
|
|
395
|
+
return idx
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _session_to_cwd(agent, sid):
|
|
399
|
+
"""Locate an agent's session file by id and return its real cwd (or None)."""
|
|
400
|
+
paths = _agent_index(agent).get(sid)
|
|
401
|
+
if not paths:
|
|
402
|
+
return None
|
|
403
|
+
for p in paths:
|
|
404
|
+
cwd = _codex_cwd(p) if agent == "codex" else _cwd_from_jsonl(p)
|
|
405
|
+
if cwd:
|
|
406
|
+
return cwd
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _cwd_for(agent, sid, project_key=""):
|
|
411
|
+
"""Real workdir for a session. pi resolves by project key; the rest by session id.
|
|
412
|
+
Returns the real cwd (or None if unresolvable). Only drives the DISPLAY label."""
|
|
413
|
+
if agent == "pi":
|
|
414
|
+
if not project_key:
|
|
415
|
+
return None
|
|
416
|
+
key = "pi\x1f" + project_key
|
|
417
|
+
if key in _CWD_CACHE:
|
|
418
|
+
return _CWD_CACHE[key]
|
|
419
|
+
cwd = _pi_cwd_from_disk(project_key)
|
|
420
|
+
_CWD_CACHE[key] = cwd
|
|
421
|
+
return cwd
|
|
422
|
+
if not sid:
|
|
423
|
+
return None
|
|
424
|
+
# codex session ids are date-path prefixed ("2025/10/17/rollout-<id>"); the
|
|
425
|
+
# local file is named "rollout-<id>.jsonl", so match on the final segment.
|
|
426
|
+
lookup_sid = sid.split("/")[-1]
|
|
427
|
+
if lookup_sid.startswith("rollout-") and agent == "codex":
|
|
428
|
+
lookup_sid = lookup_sid[len("rollout-"):]
|
|
429
|
+
key = agent + "\x1f" + lookup_sid
|
|
430
|
+
if key in _CWD_CACHE:
|
|
431
|
+
return _CWD_CACHE[key]
|
|
432
|
+
cwd = _session_to_cwd(agent, lookup_sid)
|
|
433
|
+
_CWD_CACHE[key] = cwd
|
|
434
|
+
return cwd
|
|
435
|
+
|
|
436
|
+
|
|
295
437
|
def _decode_cwd(raw: str) -> str:
|
|
296
438
|
"""Best-effort decode of ccusage's projectPath into a readable path.
|
|
297
439
|
|
|
@@ -378,8 +520,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
378
520
|
self._json(trend(days))
|
|
379
521
|
return
|
|
380
522
|
elif path == "/api/sessions":
|
|
523
|
+
period = params.get("period", "")
|
|
524
|
+
frm, to = params.get("from", ""), params.get("to", "")
|
|
381
525
|
days = max(1, min(366, int(params.get("days", "30"))))
|
|
382
|
-
self._json(sessions(days))
|
|
526
|
+
self._json(sessions(days, period, frm, to))
|
|
383
527
|
return
|
|
384
528
|
else:
|
|
385
529
|
self._send(404, b"not found", "text/plain")
|
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.7",
|
|
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",
|