tokenrace 0.1.20 → 0.1.21

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,8 +4,8 @@
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-C8zpckDk.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BuT9l_n-.css">
7
+ <script type="module" crossorigin src="/assets/index-CclozGtU.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BimQXpAH.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenrace",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Monitor en tiempo real para Claude Code",
5
5
  "bin": {
6
6
  "tokenrace": "bin/cli.js"
package/src/api-routes.js CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { Router } from 'express'
13
13
  import {
14
- getStatus, getSummary, getTimeseries, getProjects,
14
+ getStatus, getSummary, getTimeseries, getTimeseriesByProject, getProjects,
15
15
  getSessions, getUnlabeledSessions, getSessionEvents,
16
16
  getEvents, getTools, getAgents, getModels,
17
17
  labelSession, ignoreSession, reset, resetProject
@@ -114,6 +114,11 @@ export function createRouter({ port = 1337 } = {}) {
114
114
  res.json(getTimeseries(req.query.metric, req.query.from, req.query.bucket))
115
115
  })
116
116
 
117
+ /** GET /api/timeseries/by-project — serie temporal desglosada por proyecto, acepta ?metric, ?from, ?bucket */
118
+ router.get('/api/timeseries/by-project', (req, res) => {
119
+ res.json(getTimeseriesByProject(req.query.metric, req.query.from, req.query.bucket))
120
+ })
121
+
117
122
  /** GET /api/projects — proyectos con métricas, acepta ?from */
118
123
  router.get('/api/projects', (req, res) => {
119
124
  res.json(getProjects(req.query.from))
package/src/store.js CHANGED
@@ -409,15 +409,15 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
409
409
  session.apiRequests++
410
410
  }
411
411
 
412
- // ── Tool stats ──
413
- if (eventName === 'tool_use') {
412
+ // ── Tool stats de sesión ──
413
+ if (eventName === 'tool_result') {
414
414
  session.toolCalls++
415
415
  }
416
416
  }
417
417
 
418
- // ── Tool stats globales ──
419
- if (eventName === 'tool_use') {
420
- const toolName = attributes['tool.name'] ?? attributes.tool ?? 'unknown'
418
+ // ── Tool stats globales — Claude Code envía tool_result (no tool_use) ──
419
+ if (eventName === 'tool_result') {
420
+ const toolName = attributes['tool_name'] ?? attributes['tool.name'] ?? 'unknown'
421
421
 
422
422
  if (!state.tools.has(toolName)) {
423
423
  state.tools.set(toolName, { count: 0, successes: 0, totalDurationMs: 0 })
@@ -425,12 +425,27 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
425
425
  const tool = state.tools.get(toolName)
426
426
  tool.count++
427
427
 
428
- if (attributes.success === true || attributes['tool.success'] === true) {
428
+ if (attributes.success === true || attributes.success === 'true') {
429
429
  tool.successes++
430
430
  }
431
- if (attributes['tool.duration_ms'] !== undefined) {
432
- tool.totalDurationMs += Number(attributes['tool.duration_ms'])
433
- }
431
+ const durMs = Number(attributes['duration_ms'] ?? attributes['tool.duration_ms'] ?? 0)
432
+ if (durMs > 0) tool.totalDurationMs += durMs
433
+ }
434
+
435
+ // ── Subagentes — Claude Code envía subagent_completed ──
436
+ if (eventName === 'subagent_completed') {
437
+ const seq = attributes['event.sequence'] ?? Date.now()
438
+ const agentId = `${sessionId ?? 'unknown'}:${seq}`
439
+ const totalTokens = Number(attributes['total_tokens'] ?? 0)
440
+ state.agents.set(agentId, {
441
+ agentId,
442
+ parentAgentId: null,
443
+ tokensInput: totalTokens,
444
+ tokensOutput: 0,
445
+ cost: estimateCost(attributes['model'] ?? null, totalTokens, 0, 0),
446
+ toolCalls: Number(attributes['total_tool_uses'] ?? 0),
447
+ durationMs: Number(attributes['duration_ms'] ?? 0)
448
+ })
434
449
  }
435
450
 
436
451
  state.lastSeen = timestamp
@@ -694,6 +709,36 @@ export function getTimeseries(metric, from, bucket) {
694
709
  * Proyectos con sus métricas agregadas filtradas por rango temporal.
695
710
  * @param {string} from
696
711
  */
712
+ /**
713
+ * Serie temporal de una métrica desglosada por proyecto.
714
+ * Devuelve [{ timestamp, projects: { [projectName]: value } }]
715
+ */
716
+ export function getTimeseriesByProject(metric, from, bucket) {
717
+ const minTs = parseTimeRange(from)
718
+ const bucketMs = parseBucket(bucket)
719
+ const points = state.timeseries.get(metric) ?? []
720
+
721
+ // Map: bucketKey → Map<projectName, value>
722
+ const buckets = new Map()
723
+ for (const point of points) {
724
+ if (point.ts < minTs) continue
725
+ const sid = point.labels['session.id']
726
+ if (sid && state.ignoredSessions.has(sid)) continue
727
+ const proj = resolveProject(sid, point.labels.project) ?? '(sin proyecto)'
728
+ const key = Math.floor(point.ts / bucketMs) * bucketMs
729
+ if (!buckets.has(key)) buckets.set(key, new Map())
730
+ const byProj = buckets.get(key)
731
+ byProj.set(proj, (byProj.get(proj) ?? 0) + point.value)
732
+ }
733
+
734
+ return Array.from(buckets.entries())
735
+ .map(([timestamp, byProj]) => ({
736
+ timestamp,
737
+ projects: Object.fromEntries(byProj),
738
+ }))
739
+ .sort((a, b) => a.timestamp - b.timestamp)
740
+ }
741
+
697
742
  export function getProjects(from) {
698
743
  const minTs = parseTimeRange(from)
699
744
  const result = []
@@ -856,14 +901,14 @@ export function getTools(from) {
856
901
  avgDurationMs: stats.count > 0 ? stats.totalDurationMs / stats.count : 0
857
902
  })).sort((a, b) => b.count - a.count)
858
903
 
859
- // Tasa de aprobación/rechazo desde eventos en el rango
904
+ // Tasa de aprobación/rechazo desde tool_result (Claude Code usa decision_type)
860
905
  let approved = 0
861
906
  let rejected = 0
862
907
  for (const event of state.events) {
863
908
  if (event.timestamp < minTs) continue
864
- if (event.eventName !== 'tool_use' && !event.eventName.includes('tool')) continue
865
- if (event.attributes.approved === true) approved++
866
- if (event.attributes.approved === false) rejected++
909
+ if (event.eventName !== 'tool_result') continue
910
+ if (event.attributes['decision_type'] === 'accept') approved++
911
+ else if (event.attributes['decision_type'] === 'reject') rejected++
867
912
  }
868
913
 
869
914
  return { usage, decisionRate: { approved, rejected } }
@@ -918,7 +963,9 @@ export function saveSync() {
918
963
  events: state.events,
919
964
  eventIndex: state.eventIndex,
920
965
  totalEvents: state.totalEvents,
921
- startTime: state.startTime
966
+ startTime: state.startTime,
967
+ tools: Array.from(state.tools.entries()),
968
+ agents: Array.from(state.agents.values())
922
969
  }
923
970
 
924
971
  fs.writeFileSync(dataFile, JSON.stringify(data), { mode: 0o600 })
@@ -989,6 +1036,20 @@ export function loadFromDisk() {
989
1036
  state.startTime = data.startTime
990
1037
  }
991
1038
 
1039
+ // Restaurar stats de herramientas
1040
+ if (Array.isArray(data.tools)) {
1041
+ for (const [toolName, stats] of data.tools) {
1042
+ state.tools.set(toolName, stats)
1043
+ }
1044
+ }
1045
+
1046
+ // Restaurar agentes
1047
+ if (Array.isArray(data.agents)) {
1048
+ for (const agent of data.agents) {
1049
+ state.agents.set(agent.agentId, agent)
1050
+ }
1051
+ }
1052
+
992
1053
  // Re-aplicar mappings a sesiones (timeseries no necesitan propagación:
993
1054
  // resolveProject() consulta sessionMappings primero en todos los lectores)
994
1055
  for (const [sessionId, project] of state.sessionMappings.entries()) {