@statforge/claudestat 1.6.1 → 1.8.0
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/README.md +36 -3
- package/dashboard/dist/assets/AnalyticsView-DDGLDoCN.js +7 -0
- package/dashboard/dist/assets/HistoryView-DkPfrNrv.js +1 -0
- package/dashboard/dist/assets/LineChart-BOWYkkEW.js +2 -0
- package/dashboard/dist/assets/ProjectsView-VRoRiEL4.js +6 -0
- package/dashboard/dist/assets/SystemView-B2zbIxhY.js +1 -0
- package/dashboard/dist/assets/TopView-C2qdsy0Y.js +1 -0
- package/dashboard/dist/assets/index-CMhe3KaT.js +84 -0
- package/dashboard/dist/assets/shared-BbBtsdh1.js +1 -0
- package/dashboard/dist/assets/{vendor-lucide-Cym0q5l_.js → vendor-lucide-ClCW-axQ.js} +79 -64
- package/dashboard/dist/assets/{vendor-react-B_Jzs0gY.js → vendor-react-gHSHIE2L.js} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/daemon.js +113 -9
- package/dist/db.d.ts +87 -2
- package/dist/db.js +325 -65
- package/dist/doctor.js +21 -3
- package/dist/enricher.d.ts +3 -2
- package/dist/enricher.js +10 -5
- package/dist/export.d.ts +2 -1
- package/dist/export.js +41 -6
- package/dist/index.js +406 -20
- package/dist/insights.d.ts +1 -0
- package/dist/insights.js +26 -0
- package/dist/install.js +28 -1
- package/dist/intelligence.d.ts +66 -4
- package/dist/intelligence.js +205 -17
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +49 -0
- package/dist/notifier.d.ts +15 -0
- package/dist/notifier.js +26 -0
- package/dist/paths.d.ts +23 -0
- package/dist/paths.js +42 -0
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +12 -1
- package/dist/routes/events.js +136 -5
- package/dist/routes/helpers.d.ts +5 -0
- package/dist/routes/helpers.js +21 -1
- package/dist/routes/history.js +6 -2
- package/dist/routes/intents.d.ts +1 -0
- package/dist/routes/intents.js +155 -0
- package/dist/routes/misc.js +150 -4
- package/dist/routes/opencode-reader.js +39 -3
- package/dist/routes/projects.js +19 -1
- package/dist/routes/replay.d.ts +1 -0
- package/dist/routes/replay.js +29 -0
- package/dist/routes/reports.js +7 -0
- package/dist/routes/top.js +8 -1
- package/dist/service.js +11 -0
- package/dist/watchers/adapter.d.ts +1 -0
- package/dist/watchers/claude-code.d.ts +16 -1
- package/dist/watchers/claude-code.js +201 -76
- package/dist/watchers/opencode.d.ts +1 -0
- package/dist/watchers/opencode.js +152 -14
- package/hooks/event.js +44 -26
- package/package.json +1 -1
- package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
- package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
- package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
- package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
- package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
- package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
|
@@ -35,13 +35,49 @@ exports.opencodeReaderRouter.get('/api/opencode/session/:id', (req, res) => {
|
|
|
35
35
|
try {
|
|
36
36
|
const { id: sessionId } = req.params;
|
|
37
37
|
const db = openDb();
|
|
38
|
-
//
|
|
38
|
+
// ── Find all related session IDs (OpenCode creates one session per prompt) ──
|
|
39
|
+
// Look up directory for this session
|
|
40
|
+
const sessionRow = db.prepare(`
|
|
41
|
+
SELECT directory, time_created FROM session WHERE id = ?
|
|
42
|
+
`).get(sessionId);
|
|
43
|
+
let sessionIds = [sessionId];
|
|
44
|
+
if (sessionRow?.directory) {
|
|
45
|
+
// Find sessions with same directory, ordered by creation time
|
|
46
|
+
const sameDir = db.prepare(`
|
|
47
|
+
SELECT id, time_created FROM session
|
|
48
|
+
WHERE directory = ? AND time_archived IS NULL
|
|
49
|
+
ORDER BY time_created ASC
|
|
50
|
+
`).all(sessionRow.directory);
|
|
51
|
+
if (sameDir.length > 1) {
|
|
52
|
+
// Locate our session in the sorted list
|
|
53
|
+
let idx = sameDir.findIndex(s => s.id === sessionId);
|
|
54
|
+
if (idx !== -1) {
|
|
55
|
+
// Expand forward: consecutive sessions with gap < 60s
|
|
56
|
+
let end = idx;
|
|
57
|
+
for (let i = idx + 1; i < sameDir.length; i++) {
|
|
58
|
+
if (sameDir[i].time_created - sameDir[i - 1].time_created > 300000)
|
|
59
|
+
break;
|
|
60
|
+
end = i;
|
|
61
|
+
}
|
|
62
|
+
// Expand backward
|
|
63
|
+
let begin = idx;
|
|
64
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
65
|
+
if (sameDir[begin].time_created - sameDir[i].time_created > 300000)
|
|
66
|
+
break;
|
|
67
|
+
begin = i;
|
|
68
|
+
}
|
|
69
|
+
sessionIds = sameDir.slice(begin, end + 1).map(s => s.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Query messages from ALL sessions in the group ──
|
|
74
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
39
75
|
const messages = db.prepare(`
|
|
40
76
|
SELECT id, time_created, time_updated, data
|
|
41
77
|
FROM message
|
|
42
|
-
WHERE session_id
|
|
78
|
+
WHERE session_id IN (${placeholders})
|
|
43
79
|
ORDER BY time_created ASC
|
|
44
|
-
`).all(
|
|
80
|
+
`).all(...sessionIds);
|
|
45
81
|
const events = [];
|
|
46
82
|
const prompts = [];
|
|
47
83
|
let totalParts = 0;
|
package/dist/routes/projects.js
CHANGED
|
@@ -13,6 +13,7 @@ const db_1 = require("../db");
|
|
|
13
13
|
const projects_cache_1 = require("../cache/projects-cache");
|
|
14
14
|
const pattern_analyzer_1 = require("../pattern-analyzer");
|
|
15
15
|
const helpers_1 = require("./helpers");
|
|
16
|
+
const config_1 = require("../config");
|
|
16
17
|
exports.projectsRouter = (0, express_1.Router)();
|
|
17
18
|
/** Infiere el proyecto activo mirando los eventos de archivo de una sesión */
|
|
18
19
|
function inferProjectCwd(events) {
|
|
@@ -125,14 +126,31 @@ exports.projectsRouter.get('/projects', (_req, res) => {
|
|
|
125
126
|
jsonl_source: useJSONL,
|
|
126
127
|
});
|
|
127
128
|
}
|
|
129
|
+
// CLI hours per project (source breakdown from DB)
|
|
130
|
+
const cliHoursRows = db_1.dbOps.getProjectCliHours();
|
|
131
|
+
const cliHoursMap = new Map();
|
|
132
|
+
for (const row of cliHoursRows) {
|
|
133
|
+
if (!cliHoursMap.has(row.project_path))
|
|
134
|
+
cliHoursMap.set(row.project_path, {});
|
|
135
|
+
cliHoursMap.get(row.project_path)[row.source] = row.total_ms / 3600000;
|
|
136
|
+
}
|
|
128
137
|
// Attach pattern insights per project (only if DB has enough data)
|
|
138
|
+
const cfg = (0, config_1.readConfig)();
|
|
129
139
|
const projects = [...projectMap.values()].map(p => {
|
|
130
140
|
const toolCounts = db_1.dbOps.getProjectToolCounts(p.path);
|
|
131
141
|
const sessionStats = db_1.dbOps.getProjectSessionStats(p.path);
|
|
132
142
|
const insights = (sessionStats && sessionStats.session_count >= 2)
|
|
133
143
|
? (0, pattern_analyzer_1.analyzePatterns)(toolCounts, sessionStats)
|
|
134
144
|
: [];
|
|
135
|
-
|
|
145
|
+
const cli_hours = cliHoursMap.get(p.path);
|
|
146
|
+
const aliasName = cfg.projectAliases[p.path];
|
|
147
|
+
return {
|
|
148
|
+
...p,
|
|
149
|
+
name: aliasName ?? p.name,
|
|
150
|
+
alias: aliasName ?? null,
|
|
151
|
+
insights,
|
|
152
|
+
...(cli_hours ? { cli_hours } : {}),
|
|
153
|
+
};
|
|
136
154
|
})
|
|
137
155
|
.sort((a, b) => (b.last_active ?? 0) - (a.last_active ?? 0));
|
|
138
156
|
res.json({ projects, active_project: activeProject });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const replayRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replayRouter = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const db_1 = require("../db");
|
|
6
|
+
exports.replayRouter = (0, express_1.Router)();
|
|
7
|
+
const CONTEXT_WINDOW = 200000;
|
|
8
|
+
exports.replayRouter.get('/session/:id/replay', (req, res) => {
|
|
9
|
+
const { id } = req.params;
|
|
10
|
+
const turns = db_1.dbOps.getAssistantTurns(id);
|
|
11
|
+
res.json({ turns, context_window: CONTEXT_WINDOW });
|
|
12
|
+
});
|
|
13
|
+
function buildTree(sessionId, depth = 0) {
|
|
14
|
+
if (depth > 5)
|
|
15
|
+
return { id: sessionId, started_at: 0, children: [] };
|
|
16
|
+
const session = db_1.dbOps.getSession(sessionId);
|
|
17
|
+
const children = db_1.dbOps.getChildSessions(sessionId);
|
|
18
|
+
return {
|
|
19
|
+
id: sessionId,
|
|
20
|
+
dominant_model: session?.dominant_model ?? undefined,
|
|
21
|
+
total_cost_usd: session?.total_cost_usd ?? undefined,
|
|
22
|
+
started_at: session?.started_at ?? 0,
|
|
23
|
+
children: children.map(c => buildTree(c.id, depth + 1)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
exports.replayRouter.get('/session/:id/agent-tree', (req, res) => {
|
|
27
|
+
const { id } = req.params;
|
|
28
|
+
res.json(buildTree(id));
|
|
29
|
+
});
|
package/dist/routes/reports.js
CHANGED
|
@@ -61,6 +61,13 @@ exports.reportsRouter.get('/api/analytics', (req, res) => {
|
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
+
// ─── GET /api/analytics/coach — eventos combinados para el coach multi-source ─
|
|
65
|
+
exports.reportsRouter.get('/api/analytics/coach', (req, res) => {
|
|
66
|
+
const raw = req.query.session_ids;
|
|
67
|
+
const sessionIds = raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
68
|
+
const events = db_1.dbOps.getCoachEvents(sessionIds);
|
|
69
|
+
res.json({ events });
|
|
70
|
+
});
|
|
64
71
|
// ─── POST /api/weekly-reports/generate-now — generar informe inmediatamente ───
|
|
65
72
|
exports.reportsRouter.post('/api/weekly-reports/generate-now', (_req, res) => {
|
|
66
73
|
const cfg = (0, config_1.readConfig)();
|
package/dist/routes/top.js
CHANGED
|
@@ -8,18 +8,25 @@ exports.topRouter.get('/api/top', (req, res) => {
|
|
|
8
8
|
const by = req.query.by ?? 'cost';
|
|
9
9
|
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
|
|
10
10
|
const days = Math.min(parseInt(req.query.days, 10) || 30, 365);
|
|
11
|
+
const source = req.query.source ?? 'all';
|
|
11
12
|
if (!['cost', 'count', 'duration'].includes(by)) {
|
|
12
13
|
res.status(400).json({ error: 'Invalid "by" parameter. Use: cost, count, duration' });
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
if (!['all', 'claude-code', 'opencode'].includes(source)) {
|
|
17
|
+
res.status(400).json({ error: 'Invalid "source" parameter. Use: all, claude-code, opencode' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const tools = db_1.dbOps.getTopTools(days, by, limit, source);
|
|
16
21
|
const totalCost = tools.reduce((s, t) => s + t.total_cost_usd, 0);
|
|
17
22
|
const totalCount = tools.filter(t => t.tool_name !== 'Other').reduce((s, t) => s + t.count, 0);
|
|
18
23
|
res.json({
|
|
19
24
|
by,
|
|
20
25
|
days,
|
|
26
|
+
source,
|
|
21
27
|
tools: tools.map(t => ({
|
|
22
28
|
tool: t.tool_name,
|
|
29
|
+
source: t.source,
|
|
23
30
|
count: t.count,
|
|
24
31
|
totalDurationMs: t.total_duration_ms,
|
|
25
32
|
estimatedCostUsd: t.total_cost_usd,
|
package/dist/service.js
CHANGED
|
@@ -8,6 +8,14 @@ exports.uninstallService = uninstallService;
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
|
+
function buildEnvPath() {
|
|
12
|
+
const current = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
|
|
13
|
+
const nvmDir = process.env.NVM_DIR;
|
|
14
|
+
if (!nvmDir)
|
|
15
|
+
return current;
|
|
16
|
+
const nvmBin = path_1.default.join(nvmDir, 'versions', 'node', `v${process.versions.node}`, 'bin');
|
|
17
|
+
return current.includes(nvmBin) ? current : `${nvmBin}:${current}`;
|
|
18
|
+
}
|
|
11
19
|
const PLIST_LABEL = 'com.statforge.claudestat';
|
|
12
20
|
const PLIST_PATH = path_1.default.join(process.env.HOME ?? '~', 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
13
21
|
const SYSTEMD_DIR = path_1.default.join(process.env.HOME ?? '~', '.config', 'systemd', 'user');
|
|
@@ -37,6 +45,8 @@ function makePlist() {
|
|
|
37
45
|
<dict>
|
|
38
46
|
<key>CLAUDESTAT_DAEMON</key>
|
|
39
47
|
<string>1</string>
|
|
48
|
+
<key>PATH</key>
|
|
49
|
+
<string>${buildEnvPath()}</string>
|
|
40
50
|
</dict>
|
|
41
51
|
<key>StandardOutPath</key>
|
|
42
52
|
<string>/tmp/claudestat-daemon.log</string>
|
|
@@ -56,6 +66,7 @@ ExecStart=${serviceCommand()} start
|
|
|
56
66
|
Restart=on-failure
|
|
57
67
|
RestartSec=5
|
|
58
68
|
Environment=CLAUDESTAT_DAEMON=1
|
|
69
|
+
Environment=PATH=${buildEnvPath()}
|
|
59
70
|
|
|
60
71
|
[Install]
|
|
61
72
|
WantedBy=default.target`;
|
|
@@ -33,6 +33,7 @@ export interface WatcherAdapter {
|
|
|
33
33
|
export interface PollSession {
|
|
34
34
|
sessionId: string;
|
|
35
35
|
cost: CostUpdate;
|
|
36
|
+
cwd?: string;
|
|
36
37
|
}
|
|
37
38
|
/** Adapters que no usan archivos JSONL sino una DB o API propia */
|
|
38
39
|
export interface PollableAdapter extends WatcherAdapter {
|
|
@@ -6,8 +6,23 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type WatcherAdapter } from './adapter';
|
|
8
8
|
import type { BlockCostEntry } from '../db';
|
|
9
|
-
export declare function getContextWindow(model: string): number;
|
|
10
9
|
export declare const claudeCodeAdapter: WatcherAdapter;
|
|
10
|
+
export interface AssistantTurn {
|
|
11
|
+
turn_index: number;
|
|
12
|
+
ts?: number;
|
|
13
|
+
text_preview: string;
|
|
14
|
+
tool_calls: string[];
|
|
15
|
+
error_count: number;
|
|
16
|
+
output_chars: number;
|
|
17
|
+
context_used: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SemanticData {
|
|
20
|
+
turns: AssistantTurn[];
|
|
21
|
+
avg_output_chars: number;
|
|
22
|
+
error_block_count: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function findJSONLForSession(sessionId: string): Promise<string | null>;
|
|
25
|
+
export declare function extractSemanticData(filePath: string): Promise<SemanticData | null>;
|
|
11
26
|
export declare function getAllBlockCostsForSession(sessionId: string): Promise<BlockCostEntry[]>;
|
|
12
27
|
export interface SessionPrompt {
|
|
13
28
|
index: number;
|
|
@@ -10,7 +10,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.claudeCodeAdapter = void 0;
|
|
13
|
-
exports.
|
|
13
|
+
exports.findJSONLForSession = findJSONLForSession;
|
|
14
|
+
exports.extractSemanticData = extractSemanticData;
|
|
14
15
|
exports.getAllBlockCostsForSession = getAllBlockCostsForSession;
|
|
15
16
|
exports.getSessionPrompts = getSessionPrompts;
|
|
16
17
|
const path_1 = __importDefault(require("path"));
|
|
@@ -20,14 +21,6 @@ const adapter_1 = require("./adapter");
|
|
|
20
21
|
const paths_1 = require("../paths");
|
|
21
22
|
const pricing_1 = require("../pricing");
|
|
22
23
|
function projectsDir() { return path_1.default.join((0, paths_1.getClaudeDir)(), 'projects'); }
|
|
23
|
-
const KNOWN_CONTEXT_WINDOWS = {
|
|
24
|
-
'claude-opus-4-6': 200000,
|
|
25
|
-
'claude-sonnet-4-6': 200000,
|
|
26
|
-
'claude-haiku-4-5': 200000,
|
|
27
|
-
};
|
|
28
|
-
function getContextWindow(model) {
|
|
29
|
-
return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
|
|
30
|
-
}
|
|
31
24
|
const fileOffsets = new Map();
|
|
32
25
|
const FILE_OFFSET_TTL = 30 * 60000;
|
|
33
26
|
function cleanupStaleOffsets() {
|
|
@@ -38,80 +31,106 @@ function cleanupStaleOffsets() {
|
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
33
|
async function processJSONL(filePath) {
|
|
41
|
-
let
|
|
34
|
+
let fd;
|
|
42
35
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
36
|
+
fd = await promises_1.default.open(filePath, 'r');
|
|
37
|
+
const currentSize = (await fd.stat()).size;
|
|
38
|
+
// File was truncated (e.g., /compact) — drop cached state and re-read from start
|
|
39
|
+
let state = fileOffsets.get(filePath);
|
|
40
|
+
if (state && currentSize < state.offset) {
|
|
41
|
+
fileOffsets.delete(filePath);
|
|
42
|
+
state = undefined;
|
|
43
|
+
}
|
|
44
|
+
const fromByte = state?.offset ?? 0;
|
|
45
|
+
// Nothing new — return cached totals without lastEntry to avoid duplicate SSE
|
|
46
|
+
if (currentSize === fromByte && state)
|
|
47
|
+
return { ...state.totals, lastEntry: undefined };
|
|
48
|
+
// Read only the new bytes since last processed offset
|
|
49
|
+
const buf = Buffer.alloc(currentSize - fromByte);
|
|
50
|
+
await fd.read(buf, 0, buf.length, fromByte);
|
|
51
|
+
const newContent = buf.toString('utf8');
|
|
52
|
+
// Accumulate on top of previous totals (or start from zero on first read)
|
|
53
|
+
const prevTotals = state?.totals;
|
|
54
|
+
const totals = {
|
|
55
|
+
input_tokens: prevTotals?.input_tokens ?? 0,
|
|
56
|
+
output_tokens: prevTotals?.output_tokens ?? 0,
|
|
57
|
+
cache_read: prevTotals?.cache_read ?? 0,
|
|
58
|
+
cache_creation: prevTotals?.cache_creation ?? 0,
|
|
59
|
+
cost_usd: prevTotals?.cost_usd ?? 0,
|
|
60
|
+
context_used: prevTotals?.context_used ?? 0,
|
|
61
|
+
context_window: prevTotals?.context_window ?? 200000,
|
|
62
|
+
firstTs: prevTotals?.firstTs,
|
|
63
|
+
lastModel: prevTotals?.lastModel,
|
|
64
|
+
};
|
|
65
|
+
let lastInputUsd = 0;
|
|
66
|
+
let lastOutputUsd = 0;
|
|
67
|
+
let lastInputTokens = 0;
|
|
68
|
+
let lastOutputTokens = 0;
|
|
69
|
+
let hasNewAssistant = false;
|
|
70
|
+
for (const raw of newContent.split('\n')) {
|
|
71
|
+
const line = raw.trim();
|
|
72
|
+
if (!line)
|
|
74
73
|
continue;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
try {
|
|
75
|
+
const obj = JSON.parse(line);
|
|
76
|
+
if (obj.type !== 'assistant')
|
|
77
|
+
continue;
|
|
78
|
+
const msg = obj.message;
|
|
79
|
+
if (!msg?.usage)
|
|
80
|
+
continue;
|
|
81
|
+
const usage = msg.usage;
|
|
82
|
+
const model = msg.model ?? 'claude-sonnet-4-6';
|
|
83
|
+
hasNewAssistant = true;
|
|
84
|
+
if (obj.timestamp) {
|
|
85
|
+
try {
|
|
86
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
87
|
+
if (totals.firstTs === undefined)
|
|
88
|
+
totals.firstTs = ts;
|
|
89
|
+
totals.lastTs = ts;
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore */ }
|
|
80
92
|
}
|
|
81
|
-
|
|
93
|
+
totals.input_tokens += usage.input_tokens ?? 0;
|
|
94
|
+
totals.output_tokens += usage.output_tokens ?? 0;
|
|
95
|
+
totals.cache_read += usage.cache_read_input_tokens ?? 0;
|
|
96
|
+
totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
|
|
97
|
+
totals.cost_usd += (0, pricing_1.calcCost)(model, usage);
|
|
98
|
+
totals.context_used = (usage.input_tokens ?? 0)
|
|
99
|
+
+ (usage.cache_read_input_tokens ?? 0)
|
|
100
|
+
+ (usage.cache_creation_input_tokens ?? 0);
|
|
101
|
+
totals.context_window = (0, pricing_1.getContextWindow)(model);
|
|
102
|
+
const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
|
|
103
|
+
const M = 1000000;
|
|
104
|
+
const inp = usage.input_tokens ?? 0;
|
|
105
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
106
|
+
const cacheCreate = usage.cache_creation_input_tokens ?? 0;
|
|
107
|
+
const out = usage.output_tokens ?? 0;
|
|
108
|
+
lastInputUsd = (inp * price.input + cacheRead * price.cacheRead + cacheCreate * price.cacheCreate) / M;
|
|
109
|
+
lastOutputUsd = out * price.output / M;
|
|
110
|
+
lastInputTokens = inp + cacheRead + cacheCreate;
|
|
111
|
+
lastOutputTokens = out;
|
|
112
|
+
totals.lastModel = model;
|
|
82
113
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
totals.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const M = 1000000;
|
|
94
|
-
lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
|
|
95
|
-
(usage.cache_read_input_tokens ?? 0) * price.cacheRead +
|
|
96
|
-
(usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
|
|
97
|
-
lastOutputUsd = ((usage.output_tokens ?? 0) * price.output) / M;
|
|
98
|
-
lastInputTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
99
|
-
lastOutputTokens = usage.output_tokens ?? 0;
|
|
100
|
-
lastModel = model ?? lastModel;
|
|
114
|
+
catch { /* skip malformed lines */ }
|
|
115
|
+
}
|
|
116
|
+
// lastEntry only when there are new API calls — drives the block_cost SSE event
|
|
117
|
+
if (hasNewAssistant && lastInputUsd + lastOutputUsd > 0) {
|
|
118
|
+
const totalUsd = lastInputUsd + lastOutputUsd;
|
|
119
|
+
totals.lastEntry = {
|
|
120
|
+
inputUsd: lastInputUsd, outputUsd: lastOutputUsd,
|
|
121
|
+
totalUsd,
|
|
122
|
+
inputTokens: lastInputTokens, outputTokens: lastOutputTokens,
|
|
123
|
+
};
|
|
101
124
|
}
|
|
102
|
-
|
|
125
|
+
fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now(), totals });
|
|
126
|
+
return totals;
|
|
103
127
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await fd?.close();
|
|
110
133
|
}
|
|
111
|
-
totals.lastModel = lastModel;
|
|
112
|
-
totals.firstTs = firstTs;
|
|
113
|
-
fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
|
|
114
|
-
return totals;
|
|
115
134
|
}
|
|
116
135
|
exports.claudeCodeAdapter = {
|
|
117
136
|
name: 'claude-code',
|
|
@@ -152,6 +171,112 @@ exports.claudeCodeAdapter = {
|
|
|
152
171
|
};
|
|
153
172
|
setInterval(cleanupStaleOffsets, 5 * 60000).unref();
|
|
154
173
|
(0, adapter_1.registerAdapter)(exports.claudeCodeAdapter);
|
|
174
|
+
async function findJSONLForSession(sessionId) {
|
|
175
|
+
try {
|
|
176
|
+
if (!fs_1.default.existsSync(projectsDir()))
|
|
177
|
+
return null;
|
|
178
|
+
const dirs = await promises_1.default.readdir(projectsDir());
|
|
179
|
+
for (const dir of dirs) {
|
|
180
|
+
const dirPath = path_1.default.join(projectsDir(), dir);
|
|
181
|
+
try {
|
|
182
|
+
const stat = await promises_1.default.stat(dirPath);
|
|
183
|
+
if (!stat.isDirectory())
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
|
|
190
|
+
try {
|
|
191
|
+
await promises_1.default.access(filePath);
|
|
192
|
+
return filePath;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
async function extractSemanticData(filePath) {
|
|
203
|
+
try {
|
|
204
|
+
const content = await promises_1.default.readFile(filePath, 'utf8');
|
|
205
|
+
const turns = [];
|
|
206
|
+
let pendingTurn = null;
|
|
207
|
+
let totalErrorBlocks = 0;
|
|
208
|
+
let turnIndex = 0;
|
|
209
|
+
for (const raw of content.split('\n')) {
|
|
210
|
+
const line = raw.trim();
|
|
211
|
+
if (!line)
|
|
212
|
+
continue;
|
|
213
|
+
try {
|
|
214
|
+
const obj = JSON.parse(line);
|
|
215
|
+
if (obj.type === 'assistant') {
|
|
216
|
+
if (pendingTurn)
|
|
217
|
+
turns.push(pendingTurn);
|
|
218
|
+
const msgContent = obj.message?.content;
|
|
219
|
+
if (!Array.isArray(msgContent)) {
|
|
220
|
+
pendingTurn = null;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
let outputChars = 0;
|
|
224
|
+
const textParts = [];
|
|
225
|
+
const toolCalls = [];
|
|
226
|
+
for (const block of msgContent) {
|
|
227
|
+
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
228
|
+
outputChars += block.text.length;
|
|
229
|
+
textParts.push(block.text);
|
|
230
|
+
}
|
|
231
|
+
else if (block?.type === 'tool_use' && typeof block.name === 'string') {
|
|
232
|
+
toolCalls.push(block.name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let ts;
|
|
236
|
+
if (obj.timestamp) {
|
|
237
|
+
try {
|
|
238
|
+
ts = new Date(obj.timestamp).getTime();
|
|
239
|
+
}
|
|
240
|
+
catch { /* ignore */ }
|
|
241
|
+
}
|
|
242
|
+
const usage = obj.message?.usage;
|
|
243
|
+
const contextUsed = usage
|
|
244
|
+
? ((usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0))
|
|
245
|
+
: 0;
|
|
246
|
+
pendingTurn = {
|
|
247
|
+
turn_index: turnIndex++,
|
|
248
|
+
ts,
|
|
249
|
+
text_preview: textParts.join('\n').slice(0, 500),
|
|
250
|
+
tool_calls: toolCalls,
|
|
251
|
+
error_count: 0,
|
|
252
|
+
output_chars: outputChars,
|
|
253
|
+
context_used: contextUsed,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
else if ((obj.type === 'human' || obj.type === 'user') && pendingTurn) {
|
|
257
|
+
const msgContent = obj.message?.content;
|
|
258
|
+
if (!Array.isArray(msgContent))
|
|
259
|
+
continue;
|
|
260
|
+
for (const block of msgContent) {
|
|
261
|
+
if (block?.type === 'tool_result' && block.is_error === true) {
|
|
262
|
+
pendingTurn.error_count++;
|
|
263
|
+
totalErrorBlocks++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch { /* skip malformed lines */ }
|
|
269
|
+
}
|
|
270
|
+
if (pendingTurn)
|
|
271
|
+
turns.push(pendingTurn);
|
|
272
|
+
const totalOutputChars = turns.reduce((sum, t) => sum + t.output_chars, 0);
|
|
273
|
+
const avg_output_chars = turns.length > 0 ? Math.round(totalOutputChars / turns.length) : 0;
|
|
274
|
+
return { turns, avg_output_chars, error_block_count: totalErrorBlocks };
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
155
280
|
// ─── Session-level utilities (used by routes/stream and routes/misc) ───────────
|
|
156
281
|
const blockCostCache = new Map();
|
|
157
282
|
const costCacheLocks = new Map();
|