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 +1 -1
- package/src/git-detector.js +57 -0
- package/src/server.js +3 -3
- package/src/store.js +11 -30
package/package.json
CHANGED
package/src/git-detector.js
CHANGED
|
@@ -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 {
|
|
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
|
|
51
|
-
|
|
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
|
|
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
|
|
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
|
|
27
|
-
if (
|
|
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
|
|
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
|
-
//
|
|
283
|
-
if (
|
|
284
|
-
|
|
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
|
-
//
|
|
406
|
-
if (
|
|
407
|
-
|
|
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) ──
|