tokenrace 0.1.7 → 0.1.9

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/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>tokenrace</title>
7
- <script type="module" crossorigin src="/assets/index-TdoV2yLC.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-rCnJ9m1Y.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-B0dy8dWP.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenrace",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Monitor en tiempo real para Claude Code",
5
5
  "bin": {
6
6
  "tokenrace": "bin/cli.js"
package/src/store.js CHANGED
@@ -28,23 +28,90 @@ export function setDataPathForTesting(newPath) {
28
28
  // ─── Estado en memoria ───────────────────────────────────────────────────────
29
29
 
30
30
  const state = {
31
- timeseries: new Map(), // metricName → [{ ts, value, labels }]
32
- sessions: new Map(), // sessionId → SessionData
33
- sessionMappings: new Map(), // sessionId → projectName
34
- ignoredSessions: new Set(), // sessionIds ignorados
35
- events: [], // buffer circular, max 1000
36
- eventIndex: 0,
37
- projects: new Map(), // projectName → ProjectAggregates
38
- tools: new Map(), // toolName → ToolStats
39
- agents: new Map(), // agentId → AgentData
40
- models: new Map(), // modelName → ModelStats
41
- lastSeen: null,
42
- startTime: Date.now(),
43
- totalEvents: 0
31
+ timeseries: new Map(), // metricName → [{ ts, value, labels }]
32
+ sessions: new Map(), // sessionId → SessionData
33
+ sessionMappings: new Map(), // sessionId → projectName
34
+ ignoredSessions: new Set(), // sessionIds ignorados
35
+ events: [], // buffer circular, max 1000
36
+ eventIndex: 0,
37
+ projects: new Map(), // projectName → ProjectAggregates
38
+ tools: new Map(), // toolName → ToolStats
39
+ agents: new Map(), // agentId → AgentData
40
+ models: new Map(), // modelName → ModelStats
41
+ cumulativeValues: new Map(), // clave → último valor acumulativo (no persiste)
42
+ lastSeen: null,
43
+ startTime: Date.now(),
44
+ totalEvents: 0
44
45
  }
45
46
 
46
47
  // ─── Helpers internos ────────────────────────────────────────────────────────
47
48
 
49
+ /**
50
+ * Convierte un valor acumulativo en un delta desde la última lectura.
51
+ * Se usa para métricas que Claude Code exporta con temporalidad cumulativa.
52
+ */
53
+ function cumulativeDelta(key, newValue) {
54
+ const last = state.cumulativeValues.get(key) ?? 0
55
+ const delta = Math.max(0, newValue - last)
56
+ state.cumulativeValues.set(key, newValue)
57
+ return delta
58
+ }
59
+
60
+ /**
61
+ * Normaliza los nombres de métricas nuevos de Claude Code a los nombres
62
+ * canónicos que usa el store, convirtiendo valores cumulativos a deltas.
63
+ * Devuelve { name, value } normalizado, o null para ignorar la métrica.
64
+ */
65
+ function normalizeIncoming(name, value, labels) {
66
+ const sid = labels['session.id'] ?? ''
67
+
68
+ switch (name) {
69
+ case 'claude_code.token.usage': {
70
+ const type = labels.type
71
+ const key = `token:${sid}:${type}:${labels.model ?? ''}:${labels.query_source ?? ''}`
72
+ const delta = cumulativeDelta(key, value)
73
+ const nameMap = {
74
+ 'input': 'claude_code.tokens.input',
75
+ 'output': 'claude_code.tokens.output',
76
+ 'cacheRead': 'claude_code.tokens.cache.read',
77
+ 'cacheCreation': 'claude_code.tokens.cache.creation',
78
+ }
79
+ const canonical = nameMap[type]
80
+ return canonical ? { name: canonical, value: delta } : null
81
+ }
82
+
83
+ case 'claude_code.cost.usage': {
84
+ const key = `cost:${sid}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels.effort ?? ''}`
85
+ const delta = cumulativeDelta(key, value)
86
+ return { name: 'claude_code.cost', value: delta }
87
+ }
88
+
89
+ case 'claude_code.lines_of_code.count': {
90
+ const type = labels.type
91
+ const key = `lines:${sid}:${type}`
92
+ const delta = cumulativeDelta(key, value)
93
+ if (type === 'added') return { name: 'claude_code.lines_added', value: delta }
94
+ if (type === 'removed') return { name: 'claude_code.lines_removed', value: delta }
95
+ return null
96
+ }
97
+
98
+ case 'claude_code.commit.count': {
99
+ const key = `commit:${sid}`
100
+ const delta = cumulativeDelta(key, value)
101
+ return { name: 'claude_code.commits', value: delta }
102
+ }
103
+
104
+ // Métricas que no necesitamos agregar (activo ya se calcula desde eventos)
105
+ case 'claude_code.session.count':
106
+ case 'claude_code.active_time.total':
107
+ case 'claude_code.code_edit_tool.decision':
108
+ return null
109
+
110
+ default:
111
+ return { name, value }
112
+ }
113
+ }
114
+
48
115
  /**
49
116
  * Resuelve el proyecto de una sesión.
50
117
  * Prioridad: mapping manual > resource project > null
@@ -137,7 +204,12 @@ function scheduleSave() {
137
204
  * Procesa un punto de métrica normalizado.
138
205
  * @param {{ name, value, timestamp, labels }} metric
139
206
  */
140
- export function processMetric({ name, value, timestamp, labels }) {
207
+ export function processMetric(raw) {
208
+ const normalized = normalizeIncoming(raw.name, raw.value, raw.labels)
209
+ if (!normalized) return
210
+
211
+ const { name, value } = normalized
212
+ const { timestamp, labels } = raw
141
213
  const sessionId = labels['session.id']
142
214
 
143
215
  // Saltar sesiones ignoradas
@@ -263,7 +335,44 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
263
335
  }
264
336
  state.eventIndex++
265
337
 
266
- // ── Tool stats ──
338
+ // ── Asegurar que la sesión existe (puede llegar un evento antes que una métrica) ──
339
+ if (sessionId && !state.sessions.has(sessionId)) {
340
+ state.sessions.set(sessionId, {
341
+ sessionId,
342
+ project,
343
+ feature: attributes.feature ?? null,
344
+ model,
345
+ startTime: timestamp,
346
+ lastSeen: timestamp,
347
+ durationActiveMs: 0,
348
+ tokensInput: 0,
349
+ tokensOutput: 0,
350
+ tokensCache: 0,
351
+ cost: 0,
352
+ apiRequests: 0,
353
+ toolCalls: 0
354
+ })
355
+ }
356
+
357
+ if (sessionId && state.sessions.has(sessionId)) {
358
+ const session = state.sessions.get(sessionId)
359
+ session.lastSeen = Math.max(session.lastSeen, timestamp)
360
+ if (model && !session.model) session.model = model
361
+
362
+ // ── Tiempo activo desde eventos api_request (duration_ms) ──
363
+ if (eventName === 'api_request') {
364
+ const dur = Number(attributes['duration_ms'] ?? 0)
365
+ if (dur > 0) session.durationActiveMs += dur
366
+ session.apiRequests++
367
+ }
368
+
369
+ // ── Tool stats ──
370
+ if (eventName === 'tool_use') {
371
+ session.toolCalls++
372
+ }
373
+ }
374
+
375
+ // ── Tool stats globales ──
267
376
  if (eventName === 'tool_use') {
268
377
  const toolName = attributes['tool.name'] ?? attributes.tool ?? 'unknown'
269
378
 
@@ -279,11 +388,6 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
279
388
  if (attributes['tool.duration_ms'] !== undefined) {
280
389
  tool.totalDurationMs += Number(attributes['tool.duration_ms'])
281
390
  }
282
-
283
- // Incrementar toolCalls en la sesión
284
- if (sessionId && state.sessions.has(sessionId)) {
285
- state.sessions.get(sessionId).toolCalls++
286
- }
287
391
  }
288
392
 
289
393
  state.lastSeen = timestamp
@@ -368,6 +472,7 @@ export function reset() {
368
472
  state.tools.clear()
369
473
  state.agents.clear()
370
474
  state.models.clear()
475
+ state.cumulativeValues.clear()
371
476
  state.lastSeen = null
372
477
  state.startTime = Date.now()
373
478
  state.totalEvents = 0