@caius_kong/ccusage-dashboard 0.2.3 → 0.2.5

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/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">${esc(s.dirName||s.cwd)} <span class="pill sessid">${esc(s.id.slice(0,8))}</span></span>`
192
- : `<span class="name muted">${esc(s.id.slice(0,8))}</span>`;
191
+ ? `<span class="name" title="workdir from ccusage: ${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="workdir not provided by ccusage for ${esc(s.agent)}">${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
@@ -218,8 +218,19 @@ def sessions(since_days: int = 30) -> dict:
218
218
  if act_date is not None and act_date < cutoff:
219
219
  continue
220
220
  project_raw = meta.get("projectPath") or ""
221
- cwd = _decode_cwd(project_raw)
222
221
  dir_name = _basename(project_raw)
222
+ cwd = _decode_cwd(project_raw)
223
+ # For pi sessions the authoritative workdir lives on disk (session file first-line
224
+ # "cwd"). Your dashboards want the REAL dir name (e.g. "fund-tracker", not the
225
+ # lossy basename "tracker"). This only affects the DISPLAY label — all numeric
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)
229
+ if disk_cwd:
230
+ cwd = disk_cwd
231
+ disk_base = disk_cwd.rstrip("/").split("/")[-1]
232
+ if disk_base:
233
+ dir_name = disk_base
223
234
  out.append(
224
235
  {
225
236
  "id": sid or "",
@@ -230,8 +241,9 @@ def sessions(since_days: int = 30) -> dict:
230
241
  "cacheReadTokens": r.get("cacheReadTokens", 0),
231
242
  "cacheCreationTokens": r.get("cacheCreationTokens", 0),
232
243
  "lastActivity": last_activity,
233
- "cwd": cwd, # absolute-ish path, empty if ccusage didn't provide it
234
- "dirName": dir_name, # last path segment; empty if ccusage didn't provide
244
+ "cwd": cwd, # real workdir when known (pi), else best-effort decode
245
+ "dirName": dir_name, # real dir name when known (pi), else last path segment
246
+ "projectKey": project_raw, # raw ccusage projectPath
235
247
  "hasCwd": bool(project_raw),
236
248
  }
237
249
  )
@@ -239,15 +251,62 @@ def sessions(since_days: int = 30) -> dict:
239
251
  return {"total": len(out), "sessions": out}
240
252
 
241
253
 
254
+ def _pi_cwd_from_disk(project_key: str):
255
+ """Best-effort real workdir for a pi session project.
256
+
257
+ pi stores sessions under ~/.pi/agent/sessions/<projectKey>/<ts>_<id>.jsonl,
258
+ whose first line carries an authoritative "cwd". Reading it lets the dashboard
259
+ show the REAL dir name (e.g. "fund-tracker") instead of the lossy basename that
260
+ the encoded projectPath produces ("tracker").
261
+
262
+ This only supplies the DISPLAY label — costs/tokens still come from ccusage.
263
+ Returns None (caller falls back) if the dir/file is missing or unreadable.
264
+ """
265
+ import glob
266
+
267
+ try:
268
+ root = Path.home() / ".pi" / "agent" / "sessions" / project_key
269
+ if not root.is_dir():
270
+ return None
271
+ candidates = sorted(glob.glob(str(root / "*.jsonl")))
272
+ if not candidates:
273
+ return None
274
+ for path in candidates:
275
+ try:
276
+ with open(path, encoding="utf-8") as fh:
277
+ for line in fh:
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
290
+ except Exception:
291
+ return None
292
+ return None
293
+
294
+
242
295
  def _decode_cwd(raw: str) -> str:
243
296
  """Best-effort decode of ccusage's projectPath into a readable path.
244
297
 
245
- pi's projectPath is a Claude-Code-style encoded dir name where '/' became '-'.
298
+ pi's projectPath is a Claude-Code-style encoded dir name where '/' became '-',
299
+ and literal '-' inside a dir name is NOT distinguishable from the separator
300
+ (encoding is lossy). So this is approximate: 'fund-tracker' may come back as
301
+ 'fund/tracker'. The authoritative string is the raw projectPath itself.
246
302
  Example: '--Users-caius-kong-Documents-...-AutoTrans--' -> /Users/caius_kong/.../AutoTrans
247
303
  """
248
304
  if not raw:
249
305
  return ""
250
- return raw.replace("-", "/")
306
+ parts = [p for p in raw.replace("-", "/").split("/") if p]
307
+ if not parts:
308
+ return ""
309
+ return "/" + "/".join(parts)
251
310
 
252
311
 
253
312
  def _basename(raw: str) -> str:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caius_kong/ccusage-dashboard",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",