metrascope 0.1.0 → 0.1.2
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 +13 -1
- package/package.json +4 -4
- package/src/adapters/claude.js +30 -2
- package/src/public/favicon.png +0 -0
- package/src/public/favicon.svg +9 -0
- package/src/public/index.html +7 -13
package/README.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.svg" width="120" height="120" alt="Metrascope logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">metrascope</h1>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/metrascope)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
2
10
|
|
|
3
11
|
See where your coding-agent tokens go. One local command, no upload.
|
|
4
12
|
|
|
5
13
|
A unified, local dashboard for **multiple coding agents** — pick an agent in the
|
|
6
14
|
header and see its own usage. Auto-detects whatever you have installed.
|
|
7
15
|
|
|
16
|
+
```bash
|
|
17
|
+
npx metrascope
|
|
18
|
+
```
|
|
19
|
+
|
|
8
20
|
| Agent | Reads | Tokens | Reasoning | Cache | Est. cost | Rate limit |
|
|
9
21
|
|---|---|---|---|---|---|---|
|
|
10
22
|
| **Codex** | `~/.codex` sessions | ✓ | ✓ | ✓ | — | ✓ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metrascope",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "See where your coding-agent tokens go. One local command — Codex, Claude Code, Qwen Code, OpenCode & more. Zero upload.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/Buckibarnes17/
|
|
38
|
+
"url": "git+https://github.com/Buckibarnes17/metrascope.git"
|
|
39
39
|
},
|
|
40
|
-
"homepage": "https://github.com/Buckibarnes17/
|
|
40
|
+
"homepage": "https://github.com/Buckibarnes17/metrascope#readme",
|
|
41
41
|
"bugs": {
|
|
42
|
-
"url": "https://github.com/Buckibarnes17/
|
|
42
|
+
"url": "https://github.com/Buckibarnes17/metrascope/issues"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"express": "^4.21.0",
|
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 };
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Metrascope">
|
|
2
|
+
<!-- dark bars -->
|
|
3
|
+
<rect x="255" y="615" width="85" height="210" rx="16" fill="#2f333a"/>
|
|
4
|
+
<rect x="405" y="485" width="85" height="340" rx="16" fill="#2f333a"/>
|
|
5
|
+
<rect x="710" y="565" width="85" height="260" rx="16" fill="#2f333a"/>
|
|
6
|
+
<!-- gold "i" bar + dot (the highlighted metric) -->
|
|
7
|
+
<rect x="560" y="310" width="85" height="515" rx="16" fill="#d4a44a"/>
|
|
8
|
+
<circle cx="602" cy="205" r="48" fill="#d4a44a"/>
|
|
9
|
+
</svg>
|
package/src/public/index.html
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
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
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<link rel="alternate icon" type="image/png" href="/favicon.png">
|
|
7
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
11
|
<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">
|
|
@@ -37,6 +39,7 @@ button,input{font:inherit}
|
|
|
37
39
|
header{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:22px}
|
|
38
40
|
.brand{display:flex;align-items:center;gap:12px}
|
|
39
41
|
.mark{width:36px;height:36px;border-radius:9px;color:var(--brand-fg);background:linear-gradient(160deg,var(--brand),var(--brand-hover));display:grid;place-items:center;font-weight:850;font-size:14px;letter-spacing:.02em;box-shadow:var(--edge)}
|
|
42
|
+
.logo{width:38px;height:38px;display:block;flex:0 0 auto}
|
|
40
43
|
h1{margin:0;font-size:18px;line-height:1.1;letter-spacing:-.01em}
|
|
41
44
|
.sub{margin-top:3px;color:var(--soft);font-size:11px;font-family:var(--mono);max-width:44ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
42
45
|
.actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap;justify-content:flex-end}
|
|
@@ -188,7 +191,7 @@ tbody tr:hover td{background:color-mix(in srgb,var(--surface-2) 70%,transparent)
|
|
|
188
191
|
</style>
|
|
189
192
|
</head>
|
|
190
193
|
<body>
|
|
191
|
-
<div id="app" class="loading">Loading
|
|
194
|
+
<div id="app" class="loading">Loading Metrascope…</div>
|
|
192
195
|
<div class="scrim" id="scrim" onclick="closeDrawer()"></div>
|
|
193
196
|
<div class="chart-tip" id="chartTip"></div>
|
|
194
197
|
<aside class="drawer" id="drawer" aria-hidden="true"><div id="drawerContent"></div></aside>
|
|
@@ -244,17 +247,16 @@ function render(){
|
|
|
244
247
|
const t=DATA.totals, app=document.getElementById('app'), src=DATA.source||{}, c=caps();
|
|
245
248
|
app.className='wrap';
|
|
246
249
|
app.innerHTML=`
|
|
247
|
-
<header class="anim"><div class="brand"><
|
|
250
|
+
<header class="anim"><div class="brand"><img class="logo" src="/favicon.svg" alt="Metrascope" width="38" height="38"><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
251
|
${agentBar()}
|
|
249
252
|
${DATA.warnings?.length?`<div class="warnbox anim">${DATA.warnings.map(w=>esc(w.message)).join('<br>')}</div>`:''}
|
|
250
253
|
${!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
254
|
<div class="summary anim">${kpiStrip(t,c)}</div>
|
|
252
|
-
${c.rateLimit?ratebar(t.latestRateLimit):''}
|
|
253
255
|
<div class="tabs anim">
|
|
254
256
|
${tabBtn('overview','Overview')}${tabBtn('sessions','Sessions')}${tabBtn('prompts','Prompts')}${tabBtn('insights',`Insights · ${DATA.insights.length}`)}
|
|
255
257
|
</div>
|
|
256
258
|
<div id="view" class="view"></div>`}`;
|
|
257
|
-
if(DATA.sessions.length){renderView()
|
|
259
|
+
if(DATA.sessions.length){renderView()}
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
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 +355,6 @@ function promptDetail(p){return`<div><div class="section-head"><h2>Prompt #${p.r
|
|
|
353
355
|
${(p.tools||[]).length?`<div>${p.tools.map(t=>`<span class="tool-chip">${esc(t.tool)} · ${fmt(t.count)}</span>`).join('')}</div>`:''}
|
|
354
356
|
<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
357
|
|
|
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
358
|
/* ===== charts ===== */
|
|
365
359
|
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
360
|
/* ---- chart tooltips ---- */
|