metrascope 0.1.0 → 0.1.1
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 +8 -0
- package/package.json +1 -1
- package/src/adapters/claude.js +30 -2
- package/src/public/index.html +4 -13
package/README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# metrascope
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/metrascope)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
|
|
3
7
|
See where your coding-agent tokens go. One local command, no upload.
|
|
4
8
|
|
|
5
9
|
A unified, local dashboard for **multiple coding agents** — pick an agent in the
|
|
6
10
|
header and see its own usage. Auto-detects whatever you have installed.
|
|
7
11
|
|
|
12
|
+
```bash
|
|
13
|
+
npx metrascope
|
|
14
|
+
```
|
|
15
|
+
|
|
8
16
|
| Agent | Reads | Tokens | Reasoning | Cache | Est. cost | Rate limit |
|
|
9
17
|
|---|---|---|---|---|---|---|
|
|
10
18
|
| **Codex** | `~/.codex` sessions | ✓ | ✓ | ✓ | — | ✓ |
|
package/package.json
CHANGED
package/src/adapters/claude.js
CHANGED
|
@@ -9,7 +9,31 @@ const id = 'claude';
|
|
|
9
9
|
const label = 'Claude Code';
|
|
10
10
|
const mark = 'CC';
|
|
11
11
|
const accent = '#cc785c';
|
|
12
|
-
const capabilities = { cost: true, reasoning: false, rateLimit: false, cache: true, tools: true, contextWindow: false };
|
|
12
|
+
const capabilities = { cost: true, reasoning: false, rateLimit: false, cache: true, tools: true, contextWindow: false, limitEvents: true };
|
|
13
|
+
|
|
14
|
+
// Claude Code logs a synthetic 429 assistant message when you hit a usage limit,
|
|
15
|
+
// e.g. "You've hit your session limit · resets 4:20pm (Asia/Kolkata)".
|
|
16
|
+
const LIMIT_RX = /hit your (?:(\w+) )?limit\s*[·.]?\s*resets\s+([0-9]{1,2}:[0-9]{2}\s*[ap]m)\s*\(([^)]+)\)/i;
|
|
17
|
+
function limitHitFromEntry(entry) {
|
|
18
|
+
if (entry.apiErrorStatus !== 429 && entry.error !== 'rate_limit') return null;
|
|
19
|
+
const content = entry.message && entry.message.content;
|
|
20
|
+
const text = Array.isArray(content) ? content.map((b) => b.text || '').join(' ') : (typeof content === 'string' ? content : '');
|
|
21
|
+
const m = text.match(LIMIT_RX);
|
|
22
|
+
if (!m) return null;
|
|
23
|
+
return {
|
|
24
|
+
timestamp: entry.timestamp || null,
|
|
25
|
+
kind: /session/i.test(m[1] || '') ? 'session' : 'weekly',
|
|
26
|
+
reset: m[2].replace(/\s+/g, ''),
|
|
27
|
+
tz: m[3],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function summarizeLimitHits(hits) {
|
|
31
|
+
const pick = (kind) => {
|
|
32
|
+
const list = hits.filter((h) => h.kind === kind).sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
33
|
+
return { count: list.length, last: list[0] || null };
|
|
34
|
+
};
|
|
35
|
+
return { total: hits.length, session: pick('session'), weekly: pick('weekly') };
|
|
36
|
+
}
|
|
13
37
|
|
|
14
38
|
// Anthropic API per-token pricing (estimate; subscription billing differs).
|
|
15
39
|
const MODEL_PRICING = {
|
|
@@ -122,6 +146,7 @@ async function parse(options = {}) {
|
|
|
122
146
|
|
|
123
147
|
const warnings = [];
|
|
124
148
|
const sessions = [];
|
|
149
|
+
const limitHits = [];
|
|
125
150
|
let projectDirs = [];
|
|
126
151
|
try { projectDirs = fs.readdirSync(projectsDir).filter((d) => { try { return fs.statSync(path.join(projectsDir, d)).isDirectory(); } catch { return false; } }); } catch { /* */ }
|
|
127
152
|
|
|
@@ -135,6 +160,7 @@ async function parse(options = {}) {
|
|
|
135
160
|
let entries;
|
|
136
161
|
try { entries = await parseJSONLFile(filePath); } catch { continue; }
|
|
137
162
|
if (!entries.length) continue;
|
|
163
|
+
for (const e of entries) { const hit = limitHitFromEntry(e); if (hit) limitHits.push(hit); }
|
|
138
164
|
const rawTurns = extractTurns(entries);
|
|
139
165
|
if (!rawTurns.length) continue;
|
|
140
166
|
|
|
@@ -177,7 +203,9 @@ async function parse(options = {}) {
|
|
|
177
203
|
}
|
|
178
204
|
}
|
|
179
205
|
if (!sessions.length) return emptyResult(source, capabilities, [{ type: 'no-sessions', message: 'No Claude Code sessions with usage found.' }]);
|
|
180
|
-
|
|
206
|
+
const result = buildResult(sessions, source, capabilities, warnings);
|
|
207
|
+
result.limitEvents = summarizeLimitHits(limitHits);
|
|
208
|
+
return result;
|
|
181
209
|
}
|
|
182
210
|
|
|
183
211
|
module.exports = { id, label, mark, accent, capabilities, home, detect, parse };
|
package/src/public/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Metrascope</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -188,7 +188,7 @@ tbody tr:hover td{background:color-mix(in srgb,var(--surface-2) 70%,transparent)
|
|
|
188
188
|
</style>
|
|
189
189
|
</head>
|
|
190
190
|
<body>
|
|
191
|
-
<div id="app" class="loading">Loading
|
|
191
|
+
<div id="app" class="loading">Loading Metrascope…</div>
|
|
192
192
|
<div class="scrim" id="scrim" onclick="closeDrawer()"></div>
|
|
193
193
|
<div class="chart-tip" id="chartTip"></div>
|
|
194
194
|
<aside class="drawer" id="drawer" aria-hidden="true"><div id="drawerContent"></div></aside>
|
|
@@ -244,17 +244,16 @@ function render(){
|
|
|
244
244
|
const t=DATA.totals, app=document.getElementById('app'), src=DATA.source||{}, c=caps();
|
|
245
245
|
app.className='wrap';
|
|
246
246
|
app.innerHTML=`
|
|
247
|
-
<header class="anim"><div class="brand"><div class="mark">${esc(src.mark||'CX')}</div><div><h1>
|
|
247
|
+
<header class="anim"><div class="brand"><div class="mark">${esc(src.mark||'CX')}</div><div><h1>Metrascope</h1><div class="sub" title="${esc(src.home||'')}">${esc(src.label||'')} · ${esc(src.home||'')}${t.dateRange?' · '+t.dateRange.from+' → '+t.dateRange.to:''}</div></div></div><div class="actions"><button class="btn" onclick="toggleTheme()">${theme==='dark'?'☀ Light':'☾ Dark'}</button><button class="btn" onclick="openShare()">Share</button><button class="btn primary" onclick="refresh()">↻ Refresh</button></div></header>
|
|
248
248
|
${agentBar()}
|
|
249
249
|
${DATA.warnings?.length?`<div class="warnbox anim">${DATA.warnings.map(w=>esc(w.message)).join('<br>')}</div>`:''}
|
|
250
250
|
${!DATA.sessions.length?`<div class="card empty anim">No ${esc(src.label||'agent')} sessions with token usage found.<div class="muted" style="margin-top:8px">${esc(src.home||'')}</div></div>`:`
|
|
251
251
|
<div class="summary anim">${kpiStrip(t,c)}</div>
|
|
252
|
-
${c.rateLimit?ratebar(t.latestRateLimit):''}
|
|
253
252
|
<div class="tabs anim">
|
|
254
253
|
${tabBtn('overview','Overview')}${tabBtn('sessions','Sessions')}${tabBtn('prompts','Prompts')}${tabBtn('insights',`Insights · ${DATA.insights.length}`)}
|
|
255
254
|
</div>
|
|
256
255
|
<div id="view" class="view"></div>`}`;
|
|
257
|
-
if(DATA.sessions.length){renderView()
|
|
256
|
+
if(DATA.sessions.length){renderView()}
|
|
258
257
|
}
|
|
259
258
|
|
|
260
259
|
function agentBar(){if(!SOURCES.length)return'';return`<div class="agentbar anim">${SOURCES.map(s=>`<button class="agent ${s.id===sourceId?'on':''}" ${s.available?'':'disabled'} title="${s.available?esc(s.home):'not detected on this machine'}" onclick="setSource('${s.id}')"><span class="amark" style="background:${s.available?s.accent:'var(--surface-3)'};color:${s.available?accentFg(s.accent):'var(--faint)'}">${esc(s.mark)}</span>${esc(s.label)}${s.available?'':' <span class="muted" style="font-size:10px">·n/a</span>'}</button>`).join('')}</div>`}
|
|
@@ -353,14 +352,6 @@ function promptDetail(p){return`<div><div class="section-head"><h2>Prompt #${p.r
|
|
|
353
352
|
${(p.tools||[]).length?`<div>${p.tools.map(t=>`<span class="tool-chip">${esc(t.tool)} · ${fmt(t.count)}</span>`).join('')}</div>`:''}
|
|
354
353
|
<div class="turns">${p.turns.map((t,i)=>`<div class="turn"><div class="rk">${i+1}</div><div><strong style="font-size:12px">${timeFmt(t.timestamp)}</strong><div class="muted">${esc(t.model)}${t.contextWindow?' · ctx '+fmt(t.contextWindow):''}</div><div class="token-strip">${strip(t)}</div></div><div class="num">${fmt(t.totalTokens)}<div class="muted" style="font-weight:600">${fmt(freshInput(t))} fresh · ${fmt(t.cachedInputTokens)} cached · ${fmt(t.outputTokens)} out</div></div></div>`).join('')}</div>`}
|
|
355
354
|
|
|
356
|
-
/* ===== rate-limit bar ===== */
|
|
357
|
-
function windowLabel(min){if(!min)return'window';if(min%1440===0)return(min/1440)+'-day';if(min%60===0)return(min/60)+'-hour';return min+'-min'}
|
|
358
|
-
function resetText(unixSec){if(!unixSec)return'';const ms=unixSec*1000-Date.now();if(ms<=0)return'resetting now';const m=Math.floor(ms/60000),h=Math.floor(m/60),d=Math.floor(h/24);if(d>=1)return`resets in ${d}d ${h%24}h`;if(h>=1)return`resets in ${h}h ${m%60}m`;return`resets in ${m}m`}
|
|
359
|
-
function rlColor(pct){if(pct==null)return'var(--blue)';if(pct>=80)return'var(--rose)';if(pct>=50)return'var(--amber)';return'var(--green)'}
|
|
360
|
-
function rlSeg(k,win,pct,reset){const p=pct==null?0:pct;return`<div class="rl"><div class="rl-top"><span class="k">${k} · ${windowLabel(win)}</span><span class="v" style="color:${rlColor(pct)}">${pct==null?'n/a':pct+'%'}</span></div><div class="rl-track"><div class="rl-fill" style="width:${Math.min(100,p)}%;background:${rlColor(pct)}"></div></div><div class="rl-reset" data-reset="${reset||''}">${reset?resetText(reset):'no reset recorded'}</div></div>`}
|
|
361
|
-
function ratebar(r){if(!r)return'';return`<div class="ratebar anim"><div class="plan"><span class="muted" style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em">Plan</span><span class="name">${esc(r.planType||'unknown')}</span>${r.reachedType?`<span class="badge" style="background:var(--rose-bg);border-color:var(--rose-border);color:var(--rose)">limit hit</span>`:''}</div>${rlSeg('Primary',r.primaryWindowMinutes,r.primaryUsedPercent,r.primaryResetsAt)}${rlSeg('Weekly',r.secondaryWindowMinutes,r.secondaryUsedPercent,r.secondaryResetsAt)}</div>`}
|
|
362
|
-
function startRlTimer(){if(rlTimer)clearInterval(rlTimer);rlTimer=setInterval(()=>{document.querySelectorAll('.rl-reset[data-reset]').forEach(el=>{const v=el.getAttribute('data-reset');if(v)el.textContent=resetText(Number(v))})},30000)}
|
|
363
|
-
|
|
364
355
|
/* ===== charts ===== */
|
|
365
356
|
function setupCanvas(id,h){const c=document.getElementById(id);if(!c)return null;const ctx=c.getContext('2d'),dpr=Math.min(window.devicePixelRatio||1,2);let w=c.clientWidth||(c.parentElement&&c.parentElement.clientWidth)||600;w=Math.max(120,Math.min(w,2400));h=h||c.height||210;c.width=Math.round(w*dpr);c.height=Math.round(h*dpr);c.style.height=h+'px';ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);return{ctx,width:w,height:h,el:c}}
|
|
366
357
|
/* ---- chart tooltips ---- */
|