tokenrace 0.1.15 → 0.1.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenrace",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Monitor en tiempo real para Claude Code",
5
5
  "bin": {
6
6
  "tokenrace": "bin/cli.js"
@@ -0,0 +1,61 @@
1
+ /**
2
+ * git-detector.js
3
+ *
4
+ * Detecta el proyecto git a partir de un path hint (directorio o fichero).
5
+ * Usa execFile (sin shell) para evitar inyección de comandos.
6
+ *
7
+ * Export:
8
+ * detectGitProject(pathHint) → Promise<{ name, remote }|null>
9
+ */
10
+
11
+ import { execFile } from 'node:child_process'
12
+ import path from 'node:path'
13
+ import fs from 'node:fs'
14
+
15
+ function git(args, cwd) {
16
+ return new Promise((resolve, reject) => {
17
+ execFile('git', args, { cwd, timeout: 5000 }, (err, stdout) => {
18
+ if (err) reject(err)
19
+ else resolve(stdout.trim())
20
+ })
21
+ })
22
+ }
23
+
24
+ /**
25
+ * Detecta el nombre del proyecto git a partir de un path hint.
26
+ * @param {string} pathHint - Directorio de trabajo o ruta de fichero
27
+ * @returns {Promise<{ name: string, remote: string|null }|null>}
28
+ */
29
+ export async function detectGitProject(pathHint) {
30
+ if (!pathHint) return null
31
+
32
+ try {
33
+ // Si es un fichero, usar su directorio
34
+ let dir = pathHint
35
+ try {
36
+ if (!fs.statSync(pathHint).isDirectory()) dir = path.dirname(pathHint)
37
+ } catch {
38
+ dir = path.dirname(pathHint)
39
+ }
40
+
41
+ // Obtener el root del repositorio git
42
+ const root = await git(['rev-parse', '--show-toplevel'], dir)
43
+
44
+ // Intentar obtener la URL del remote origin
45
+ let remote = null
46
+ try {
47
+ remote = await git(['remote', 'get-url', 'origin'], root)
48
+ } catch { /* sin remote — usaremos el nombre del directorio */ }
49
+
50
+ // Extraer nombre del proyecto
51
+ // https://github.com/user/repo.git → repo
52
+ // git@github.com:user/repo.git → repo
53
+ const name = remote
54
+ ? remote.replace(/\.git$/, '').split(/[/:]/g).pop()
55
+ : path.basename(root)
56
+
57
+ return { name, remote: remote ?? null }
58
+ } catch {
59
+ return null
60
+ }
61
+ }
package/src/server.js CHANGED
@@ -15,7 +15,8 @@ import express from 'express'
15
15
  import path from 'node:path'
16
16
  import { fileURLToPath } from 'node:url'
17
17
  import { parseMetrics, parseEvents, parseTraces } from './otlp-parser.js'
18
- import { processMetric, processEvent, processTrace, loadFromDisk, startAutoSave, saveSync } from './store.js'
18
+ import { processMetric, processEvent, processTrace, loadFromDisk, startAutoSave, saveSync, drainAutoDetectQueue, labelSession } from './store.js'
19
+ import { detectGitProject } from './git-detector.js'
19
20
  import { createRouter, broadcast } from './api-routes.js'
20
21
 
21
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -39,6 +40,22 @@ export async function startServer({ port = 1337 } = {}) {
39
40
  next()
40
41
  })
41
42
 
43
+ // ── Auto-detección de proyecto vía git ─────────────────────────────────────
44
+
45
+ /**
46
+ * Drena la cola de sesiones sin proyecto y lanza detección git de forma async.
47
+ * Cuando detecta un nombre, etiqueta la sesión y notifica a los clientes SSE.
48
+ */
49
+ function runAutoDetect() {
50
+ for (const { sessionId, pathHint } of drainAutoDetectQueue()) {
51
+ detectGitProject(pathHint).then(result => {
52
+ if (!result) return
53
+ labelSession(sessionId, result.name)
54
+ broadcast('label_updated', { sessionId, project: result.name, auto: true })
55
+ }).catch(() => {})
56
+ }
57
+ }
58
+
42
59
  // ── Endpoints OTLP ──────────────────────────────────────────────────────────
43
60
 
44
61
  /**
@@ -51,6 +68,7 @@ export async function startServer({ port = 1337 } = {}) {
51
68
  processMetric(point)
52
69
  }
53
70
  broadcast('metrics', { count: points.length })
71
+ runAutoDetect()
54
72
  res.json({ partialSuccess: {} })
55
73
  })
56
74
 
@@ -64,6 +82,7 @@ export async function startServer({ port = 1337 } = {}) {
64
82
  const stored = processEvent(ev)
65
83
  broadcast('event', stored)
66
84
  }
85
+ runAutoDetect()
67
86
  res.json({ partialSuccess: {} })
68
87
  })
69
88
 
package/src/store.js CHANGED
@@ -14,6 +14,34 @@ import os from 'node:os'
14
14
  import path from 'node:path'
15
15
  import { estimateCost } from './prices.js'
16
16
 
17
+ // ─── Cola de auto-detección de proyecto vía git ──────────────────────────────
18
+
19
+ const _pendingAutoDetect = [] // { sessionId, pathHint }[]
20
+ const _autoDetectQueued = new Set() // sessionIds con detección en cola ahora mismo
21
+
22
+ /**
23
+ * Encola una sesión sin proyecto para auto-detección por git.
24
+ * No re-encola si ya está en cola o si la sesión ya tiene proyecto.
25
+ */
26
+ function maybeQueueAutoDetect(sessionId, pathHint) {
27
+ if (!pathHint || _autoDetectQueued.has(sessionId)) return
28
+ const session = state.sessions.get(sessionId)
29
+ if (!session || resolveProject(sessionId, session.project) !== null) return
30
+ _autoDetectQueued.add(sessionId)
31
+ _pendingAutoDetect.push({ sessionId, pathHint })
32
+ }
33
+
34
+ /**
35
+ * Devuelve y vacía la cola de sesiones pendientes de auto-detección.
36
+ * Al drenarla, las sessionIds vuelven a estar disponibles para future re-colas
37
+ * (permite reintentar si la primera detección falló).
38
+ */
39
+ export function drainAutoDetectQueue() {
40
+ const items = _pendingAutoDetect.splice(0)
41
+ for (const { sessionId } of items) _autoDetectQueued.delete(sessionId)
42
+ return items
43
+ }
44
+
17
45
  // ─── Ruta de persistencia ───────────────────────────────────────────────────
18
46
 
19
47
  const HOME_DATA_DIR = path.join(os.homedir(), '.tokenrace')
@@ -242,7 +270,8 @@ export function processMetric(raw) {
242
270
  tokensCache: 0,
243
271
  cost: 0,
244
272
  apiRequests: 0,
245
- toolCalls: 0
273
+ toolCalls: 0,
274
+ cwd: null
246
275
  })
247
276
  }
248
277
 
@@ -250,6 +279,15 @@ export function processMetric(raw) {
250
279
  session.lastSeen = Math.max(session.lastSeen, timestamp)
251
280
  if (model && !session.model) session.model = model
252
281
 
282
+ // Capturar directorio de trabajo para auto-detección de proyecto
283
+ if (!session.cwd) {
284
+ const cwd = labels['process.cwd'] ?? labels['cwd'] ?? labels['working_directory'] ?? null
285
+ if (cwd) {
286
+ session.cwd = cwd
287
+ maybeQueueAutoDetect(sessionId, cwd)
288
+ }
289
+ }
290
+
253
291
  switch (name) {
254
292
  case 'claude_code.tokens.input':
255
293
  session.tokensInput += value; break
@@ -347,6 +385,7 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
347
385
  feature: attributes.feature ?? null,
348
386
  model,
349
387
  startTime: timestamp,
388
+ cwd: attributes['process.cwd'] ?? attributes['cwd'] ?? attributes['working_directory'] ?? null,
350
389
  lastSeen: timestamp,
351
390
  durationActiveMs: 0,
352
391
  tokensInput: 0,
@@ -363,6 +402,25 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
363
402
  session.lastSeen = Math.max(session.lastSeen, timestamp)
364
403
  if (model && !session.model) session.model = model
365
404
 
405
+ // Capturar directorio de trabajo para auto-detección de proyecto
406
+ if (!session.cwd) {
407
+ const cwd = attributes['process.cwd'] ?? attributes['cwd'] ?? attributes['working_directory'] ?? null
408
+ if (cwd) {
409
+ session.cwd = cwd
410
+ maybeQueueAutoDetect(sessionId, cwd)
411
+ } else if (eventName === 'tool_use') {
412
+ // Fallback: deducir cwd desde paths de ficheros usados por herramientas
413
+ const filePath = attributes['tool.input.file_path']
414
+ ?? attributes['tool.input.path']
415
+ ?? attributes['file_path']
416
+ ?? null
417
+ if (filePath && filePath.startsWith('/')) {
418
+ session.cwd = path.dirname(filePath)
419
+ maybeQueueAutoDetect(sessionId, session.cwd)
420
+ }
421
+ }
422
+ }
423
+
366
424
  // ── Tiempo activo desde eventos api_request (duration_ms) ──
367
425
  if (eventName === 'api_request') {
368
426
  const dur = Number(attributes['duration_ms'] ?? 0)