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 +1 -1
- package/src/git-detector.js +61 -0
- package/src/server.js +20 -1
- package/src/store.js +59 -1
package/package.json
CHANGED
|
@@ -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)
|