tokenrace 0.1.17 → 0.1.18

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.17",
3
+ "version": "0.1.18",
4
4
  "description": "Monitor en tiempo real para Claude Code",
5
5
  "bin": {
6
6
  "tokenrace": "bin/cli.js"
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { execFile } from 'node:child_process'
12
12
  import path from 'node:path'
13
+ import os from 'node:os'
13
14
  import fs from 'node:fs'
14
15
 
15
16
  function git(args, cwd) {
@@ -59,3 +60,59 @@ export async function detectGitProject(pathHint) {
59
60
  return null
60
61
  }
61
62
  }
63
+
64
+ /**
65
+ * Detecta el proyecto de una sesión leyendo los archivos JSONL de Claude Code en
66
+ * ~/.claude/projects/<ruta-encodificada>/<sessionId>.jsonl.
67
+ * Lee las primeras líneas hasta encontrar el campo `cwd`, luego usa detectGitProject.
68
+ *
69
+ * Acoplado al formato de disco de Claude Code: si Claude Code cambia su esquema,
70
+ * esta función dejará de funcionar silenciosamente (retorna null).
71
+ *
72
+ * @param {string} sessionId - UUID de la sesión
73
+ * @returns {Promise<{ name: string, remote: string|null }|null>}
74
+ */
75
+ export async function detectProjectBySessionId(sessionId) {
76
+ if (!sessionId) return null
77
+
78
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects')
79
+
80
+ let dirs
81
+ try {
82
+ dirs = fs.readdirSync(projectsDir)
83
+ } catch {
84
+ return null
85
+ }
86
+
87
+ for (const dir of dirs) {
88
+ const sessionFile = path.join(projectsDir, dir, `${sessionId}.jsonl`)
89
+ try {
90
+ fs.statSync(sessionFile)
91
+ } catch {
92
+ continue // no existe en este directorio
93
+ }
94
+
95
+ // Buscar campo `cwd` en las primeras 10 líneas del transcript
96
+ let cwd = null
97
+ try {
98
+ const content = fs.readFileSync(sessionFile, 'utf8')
99
+ for (const line of content.split('\n').slice(0, 10)) {
100
+ if (!line.trim()) continue
101
+ try {
102
+ const record = JSON.parse(line)
103
+ if (typeof record.cwd === 'string' && record.cwd) {
104
+ cwd = record.cwd
105
+ break
106
+ }
107
+ } catch { /* línea no es JSON válido */ }
108
+ }
109
+ } catch {
110
+ return null
111
+ }
112
+
113
+ if (cwd) return detectGitProject(cwd)
114
+ return null
115
+ }
116
+
117
+ return null
118
+ }
package/src/server.js CHANGED
@@ -16,7 +16,7 @@ import path from 'node:path'
16
16
  import { fileURLToPath } from 'node:url'
17
17
  import { parseMetrics, parseEvents, parseTraces } from './otlp-parser.js'
18
18
  import { processMetric, processEvent, processTrace, loadFromDisk, startAutoSave, saveSync, drainAutoDetectQueue, labelSession } from './store.js'
19
- import { detectGitProject } from './git-detector.js'
19
+ import { detectProjectBySessionId } from './git-detector.js'
20
20
  import { createRouter, broadcast } from './api-routes.js'
21
21
 
22
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -47,8 +47,8 @@ export async function startServer({ port = 1337 } = {}) {
47
47
  * Cuando detecta un nombre, etiqueta la sesión y notifica a los clientes SSE.
48
48
  */
49
49
  function runAutoDetect() {
50
- for (const { sessionId, pathHint } of drainAutoDetectQueue()) {
51
- detectGitProject(pathHint).then(result => {
50
+ for (const { sessionId } of drainAutoDetectQueue()) {
51
+ detectProjectBySessionId(sessionId).then(result => {
52
52
  if (!result) return
53
53
  labelSession(sessionId, result.name)
54
54
  broadcast('label_updated', { sessionId, project: result.name, auto: true })
package/src/store.js CHANGED
@@ -16,19 +16,19 @@ import { estimateCost } from './prices.js'
16
16
 
17
17
  // ─── Cola de auto-detección de proyecto vía git ──────────────────────────────
18
18
 
19
- const _pendingAutoDetect = [] // { sessionId, pathHint }[]
19
+ const _pendingAutoDetect = [] // { sessionId }[]
20
20
  const _autoDetectQueued = new Set() // sessionIds con detección en cola ahora mismo
21
21
 
22
22
  /**
23
- * Encola una sesión sin proyecto para auto-detección por git.
23
+ * Encola una sesión sin proyecto para auto-detección leyendo ~/.claude/projects/.
24
24
  * No re-encola si ya está en cola o si la sesión ya tiene proyecto.
25
25
  */
26
- function maybeQueueAutoDetect(sessionId, pathHint) {
27
- if (!pathHint || _autoDetectQueued.has(sessionId)) return
26
+ function maybeQueueAutoDetect(sessionId) {
27
+ if (_autoDetectQueued.has(sessionId)) return
28
28
  const session = state.sessions.get(sessionId)
29
29
  if (!session || resolveProject(sessionId, session.project) !== null) return
30
30
  _autoDetectQueued.add(sessionId)
31
- _pendingAutoDetect.push({ sessionId, pathHint })
31
+ _pendingAutoDetect.push({ sessionId })
32
32
  }
33
33
 
34
34
  /**
@@ -279,13 +279,9 @@ export function processMetric(raw) {
279
279
  session.lastSeen = Math.max(session.lastSeen, timestamp)
280
280
  if (model && !session.model) session.model = model
281
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
- }
282
+ // Encolar auto-detección si la sesión aún no tiene proyecto
283
+ if (resolveProject(sessionId, session.project) === null) {
284
+ maybeQueueAutoDetect(sessionId)
289
285
  }
290
286
 
291
287
  switch (name) {
@@ -385,7 +381,6 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
385
381
  feature: attributes.feature ?? null,
386
382
  model,
387
383
  startTime: timestamp,
388
- cwd: attributes['process.cwd'] ?? attributes['cwd'] ?? attributes['working_directory'] ?? null,
389
384
  lastSeen: timestamp,
390
385
  durationActiveMs: 0,
391
386
  tokensInput: 0,
@@ -402,23 +397,9 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
402
397
  session.lastSeen = Math.max(session.lastSeen, timestamp)
403
398
  if (model && !session.model) session.model = model
404
399
 
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
- }
400
+ // Encolar auto-detección si la sesión aún no tiene proyecto
401
+ if (resolveProject(sessionId, session.project) === null) {
402
+ maybeQueueAutoDetect(sessionId)
422
403
  }
423
404
 
424
405
  // ── Tiempo activo desde eventos api_request (duration_ms) ──