@zhangferry-dev/tokendash 1.4.2 → 1.5.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/dist/client/assets/{index-B4YgU_cb.js → index-BPWY9q0y.js} +45 -45
- package/dist/client/index.html +1 -1
- package/dist/electron-server.cjs +167 -53
- package/dist/electron-server.cjs.map +3 -3
- package/dist/server/analyticsParser.js +66 -2
- package/dist/server/claudeBlocksParser.js +48 -5
- package/dist/server/claudeJsonlParser.d.ts +6 -0
- package/dist/server/claudeJsonlParser.js +53 -4
- package/dist/server/codexParser.d.ts +6 -2
- package/dist/server/codexParser.js +65 -51
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +25 -9
- package/package.json +1 -1
|
@@ -48,9 +48,56 @@ function countLines(text) {
|
|
|
48
48
|
// Claude Code session scanning & tool extraction
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
51
|
+
const projectNameCache = new Map();
|
|
52
|
+
/** Decode Claude's encoded project directory name.
|
|
53
|
+
* Claude encodes paths: /Users/foo/bar → -Users-foo-bar
|
|
54
|
+
* Since '-' replaces '/' and project names can contain '-',
|
|
55
|
+
* we use filesystem checks to find the correct last segment.
|
|
56
|
+
*/
|
|
51
57
|
function extractProjectName(dirName) {
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
if (!dirName.startsWith('-'))
|
|
59
|
+
return dirName;
|
|
60
|
+
const cached = projectNameCache.get(dirName);
|
|
61
|
+
if (cached)
|
|
62
|
+
return cached;
|
|
63
|
+
const segments = dirName.replace(/^-/, '').split('-').filter(Boolean);
|
|
64
|
+
if (segments.length === 0) {
|
|
65
|
+
projectNameCache.set(dirName, dirName);
|
|
66
|
+
return dirName;
|
|
67
|
+
}
|
|
68
|
+
if (segments.length === 1) {
|
|
69
|
+
projectNameCache.set(dirName, segments[0]);
|
|
70
|
+
return segments[0];
|
|
71
|
+
}
|
|
72
|
+
let bestName = segments[segments.length - 1];
|
|
73
|
+
for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
|
|
74
|
+
const parentSegments = segments.slice(0, splitAt);
|
|
75
|
+
const candidateName = segments.slice(splitAt).join('-');
|
|
76
|
+
let parentPath = '/';
|
|
77
|
+
let valid = true;
|
|
78
|
+
for (const seg of parentSegments) {
|
|
79
|
+
const regular = join(parentPath, seg);
|
|
80
|
+
const hidden = join(parentPath, '.' + seg);
|
|
81
|
+
if (existsSync(regular)) {
|
|
82
|
+
parentPath = regular;
|
|
83
|
+
}
|
|
84
|
+
else if (existsSync(hidden)) {
|
|
85
|
+
parentPath = hidden;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
valid = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!valid)
|
|
93
|
+
continue;
|
|
94
|
+
if (existsSync(join(parentPath, candidateName)) || existsSync(join(parentPath, '.' + candidateName))) {
|
|
95
|
+
bestName = candidateName;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
projectNameCache.set(dirName, bestName);
|
|
100
|
+
return bestName;
|
|
54
101
|
}
|
|
55
102
|
function matchesProject(dirName, filter) {
|
|
56
103
|
return extractProjectName(dirName) === extractProjectName(filter);
|
|
@@ -273,5 +320,22 @@ export function computeAnalytics(toolCalls, timezone = 'Asia/Shanghai') {
|
|
|
273
320
|
toolCallTrend.push(entry);
|
|
274
321
|
}
|
|
275
322
|
toolCallTrend.sort((a, b) => a.date.localeCompare(b.date));
|
|
323
|
+
// Fill missing tool values with 0 so chart lines don't break
|
|
324
|
+
if (toolCallTrend.length > 0) {
|
|
325
|
+
const allTools = new Set();
|
|
326
|
+
for (const entry of toolCallTrend) {
|
|
327
|
+
for (const key of Object.keys(entry)) {
|
|
328
|
+
if (key !== 'date')
|
|
329
|
+
allTools.add(key);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const entry of toolCallTrend) {
|
|
333
|
+
for (const tool of allTools) {
|
|
334
|
+
if (entry[tool] === undefined) {
|
|
335
|
+
entry[tool] = 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
276
340
|
return { codeChangeTrend, toolUsageDistribution, productivityKPIs, toolCallTrend };
|
|
277
341
|
}
|
|
@@ -24,13 +24,56 @@ const ClaudeEventSchema = z.object({
|
|
|
24
24
|
// Helpers
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* -Users-
|
|
27
|
+
const projectNameCache = new Map();
|
|
28
|
+
/** Decode Claude's encoded project directory name.
|
|
29
|
+
* Claude encodes paths: /Users/foo/bar → -Users-foo-bar
|
|
30
|
+
* Since '-' replaces '/' and project names can contain '-',
|
|
31
|
+
* we use filesystem checks to find the correct last segment.
|
|
30
32
|
*/
|
|
31
33
|
function extractProjectName(dirName) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
if (!dirName.startsWith('-'))
|
|
35
|
+
return dirName;
|
|
36
|
+
const cached = projectNameCache.get(dirName);
|
|
37
|
+
if (cached)
|
|
38
|
+
return cached;
|
|
39
|
+
const segments = dirName.replace(/^-/, '').split('-').filter(Boolean);
|
|
40
|
+
if (segments.length === 0) {
|
|
41
|
+
projectNameCache.set(dirName, dirName);
|
|
42
|
+
return dirName;
|
|
43
|
+
}
|
|
44
|
+
if (segments.length === 1) {
|
|
45
|
+
projectNameCache.set(dirName, segments[0]);
|
|
46
|
+
return segments[0];
|
|
47
|
+
}
|
|
48
|
+
let bestName = segments[segments.length - 1];
|
|
49
|
+
for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
|
|
50
|
+
const parentSegments = segments.slice(0, splitAt);
|
|
51
|
+
const candidateName = segments.slice(splitAt).join('-');
|
|
52
|
+
let parentPath = '/';
|
|
53
|
+
let valid = true;
|
|
54
|
+
for (const seg of parentSegments) {
|
|
55
|
+
const regular = join(parentPath, seg);
|
|
56
|
+
const hidden = join(parentPath, '.' + seg);
|
|
57
|
+
if (existsSync(regular)) {
|
|
58
|
+
parentPath = regular;
|
|
59
|
+
}
|
|
60
|
+
else if (existsSync(hidden)) {
|
|
61
|
+
parentPath = hidden;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
valid = false;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!valid)
|
|
69
|
+
continue;
|
|
70
|
+
if (existsSync(join(parentPath, candidateName)) || existsSync(join(parentPath, '.' + candidateName))) {
|
|
71
|
+
bestName = candidateName;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
projectNameCache.set(dirName, bestName);
|
|
76
|
+
return bestName;
|
|
34
77
|
}
|
|
35
78
|
/** Match project display name against a filter (also normalizes the filter) */
|
|
36
79
|
function matchesProject(dirName, filter) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { DailyResponse, ProjectsResponse, BlockEntry } from '../shared/types.js';
|
|
2
2
|
export declare function calculateCost(inputTokens: number, cacheReadTokens: number, outputTokens: number, model: string): number;
|
|
3
|
+
/** Decode Claude's encoded project directory name.
|
|
4
|
+
* Claude encodes paths: /Users/foo/bar → -Users-foo-bar
|
|
5
|
+
* Since '-' replaces '/' and project names can contain '-',
|
|
6
|
+
* we use filesystem checks to find the correct last segment.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractProjectName(dirName: string): string;
|
|
3
9
|
export declare function getDateKey(timestamp: string, tz: string): string;
|
|
4
10
|
export declare function getHourKey(timestamp: string, tz: string): string;
|
|
5
11
|
export declare function getDailyResponse(project?: string | null, tz?: string): DailyResponse;
|
|
@@ -38,9 +38,58 @@ export function calculateCost(inputTokens, cacheReadTokens, outputTokens, model)
|
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
40
40
|
const fileCache = new Map();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const projectNameCache = new Map();
|
|
42
|
+
/** Decode Claude's encoded project directory name.
|
|
43
|
+
* Claude encodes paths: /Users/foo/bar → -Users-foo-bar
|
|
44
|
+
* Since '-' replaces '/' and project names can contain '-',
|
|
45
|
+
* we use filesystem checks to find the correct last segment.
|
|
46
|
+
*/
|
|
47
|
+
export function extractProjectName(dirName) {
|
|
48
|
+
if (!dirName.startsWith('-'))
|
|
49
|
+
return dirName;
|
|
50
|
+
const cached = projectNameCache.get(dirName);
|
|
51
|
+
if (cached)
|
|
52
|
+
return cached;
|
|
53
|
+
const segments = dirName.replace(/^-/, '').split('-').filter(Boolean);
|
|
54
|
+
if (segments.length === 0) {
|
|
55
|
+
projectNameCache.set(dirName, dirName);
|
|
56
|
+
return dirName;
|
|
57
|
+
}
|
|
58
|
+
if (segments.length === 1) {
|
|
59
|
+
projectNameCache.set(dirName, segments[0]);
|
|
60
|
+
return segments[0];
|
|
61
|
+
}
|
|
62
|
+
let bestName = segments[segments.length - 1];
|
|
63
|
+
// Try from right: find the longest last segment that forms a valid path
|
|
64
|
+
for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
|
|
65
|
+
const parentSegments = segments.slice(0, splitAt);
|
|
66
|
+
const candidateName = segments.slice(splitAt).join('-');
|
|
67
|
+
// Build parent path, handling hidden directories (dot prefix)
|
|
68
|
+
let parentPath = '/';
|
|
69
|
+
let valid = true;
|
|
70
|
+
for (const seg of parentSegments) {
|
|
71
|
+
const regular = join(parentPath, seg);
|
|
72
|
+
const hidden = join(parentPath, '.' + seg);
|
|
73
|
+
if (existsSync(regular)) {
|
|
74
|
+
parentPath = regular;
|
|
75
|
+
}
|
|
76
|
+
else if (existsSync(hidden)) {
|
|
77
|
+
parentPath = hidden;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
valid = false;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!valid)
|
|
85
|
+
continue;
|
|
86
|
+
if (existsSync(join(parentPath, candidateName)) || existsSync(join(parentPath, '.' + candidateName))) {
|
|
87
|
+
bestName = candidateName;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
projectNameCache.set(dirName, bestName);
|
|
92
|
+
return bestName;
|
|
44
93
|
}
|
|
45
94
|
function matchesProject(dirName, filter) {
|
|
46
95
|
return extractProjectName(dirName) === extractProjectName(filter);
|
|
@@ -228,7 +277,7 @@ export function getProjectsResponse(tz = DEFAULT_TZ) {
|
|
|
228
277
|
const projectMap = new Map();
|
|
229
278
|
for (const e of entries) {
|
|
230
279
|
const date = getDateKey(e.timestamp, tz);
|
|
231
|
-
const projectName = e.projectDir;
|
|
280
|
+
const projectName = extractProjectName(e.projectDir);
|
|
232
281
|
if (!projectMap.has(projectName)) {
|
|
233
282
|
projectMap.set(projectName, new Map());
|
|
234
283
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DailyResponse, ProjectsResponse, BlocksResponse } from '../shared/types.js';
|
|
2
|
-
interface ParsedTokenEvent {
|
|
2
|
+
export interface ParsedTokenEvent {
|
|
3
3
|
timestamp: string;
|
|
4
4
|
inputTokens: number;
|
|
5
5
|
cachedInputTokens: number;
|
|
@@ -37,10 +37,14 @@ export declare function scanCodexSessions(): string[];
|
|
|
37
37
|
export declare function parseCodexSession(filepath: string): ParsedSession | null;
|
|
38
38
|
/** Parse all Codex sessions. */
|
|
39
39
|
export declare function parseAllSessions(): ParsedSession[];
|
|
40
|
+
export declare function buildCodexResponsesFromSessions(sessions: ParsedSession[], options?: Partial<AggregateOptions>): {
|
|
41
|
+
daily: DailyResponse;
|
|
42
|
+
projects: ProjectsResponse;
|
|
43
|
+
blocks: BlocksResponse;
|
|
44
|
+
};
|
|
40
45
|
/** Aggregate and return DailyResponse format (for /daily?agent=codex) */
|
|
41
46
|
export declare function getDailyResponse(options?: Partial<AggregateOptions>): DailyResponse;
|
|
42
47
|
/** Aggregate and return ProjectsResponse format (for /projects?agent=codex) */
|
|
43
48
|
export declare function getProjectsResponse(options?: Partial<AggregateOptions>): ProjectsResponse;
|
|
44
49
|
/** Aggregate and return BlocksResponse format (hourly, for /blocks?agent=codex) */
|
|
45
50
|
export declare function getBlocksResponse(options?: Partial<AggregateOptions>): BlocksResponse;
|
|
46
|
-
export {};
|
|
@@ -219,8 +219,18 @@ function mergeAcc(a, b) {
|
|
|
219
219
|
a.reasoningOutputTokens += b.reasoningOutputTokens;
|
|
220
220
|
a.totalTokens += b.totalTokens;
|
|
221
221
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
222
|
+
function addAccToBucket(bucket, ev, model) {
|
|
223
|
+
addAcc(bucket.acc, ev);
|
|
224
|
+
if (!model)
|
|
225
|
+
return;
|
|
226
|
+
if (!bucket.models.has(model))
|
|
227
|
+
bucket.models.set(model, emptyAcc());
|
|
228
|
+
addAcc(bucket.models.get(model), ev);
|
|
229
|
+
}
|
|
230
|
+
function accToEntry(date, acc, modelAccs) {
|
|
231
|
+
const modelNames = [...modelAccs.keys()];
|
|
232
|
+
const modelBreakdowns = buildModelBreakdowns(modelAccs);
|
|
233
|
+
const totalCost = modelBreakdowns.reduce((sum, model) => sum + model.cost, 0);
|
|
224
234
|
return {
|
|
225
235
|
date,
|
|
226
236
|
inputTokens: acc.inputTokens,
|
|
@@ -228,23 +238,19 @@ function accToEntry(date, acc, models) {
|
|
|
228
238
|
cacheCreationTokens: 0,
|
|
229
239
|
cacheReadTokens: acc.cachedInputTokens,
|
|
230
240
|
totalTokens: acc.totalTokens,
|
|
231
|
-
totalCost
|
|
232
|
-
modelsUsed:
|
|
233
|
-
modelBreakdowns
|
|
241
|
+
totalCost,
|
|
242
|
+
modelsUsed: modelNames,
|
|
243
|
+
modelBreakdowns,
|
|
234
244
|
};
|
|
235
245
|
}
|
|
236
|
-
function buildModelBreakdowns(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return [];
|
|
240
|
-
const costPerModel = totalCost / modelList.length;
|
|
241
|
-
return modelList.map(name => ({
|
|
242
|
-
modelName: name,
|
|
246
|
+
function buildModelBreakdowns(modelAccs) {
|
|
247
|
+
return [...modelAccs.entries()].map(([modelName, acc]) => ({
|
|
248
|
+
modelName,
|
|
243
249
|
inputTokens: acc.inputTokens,
|
|
244
250
|
outputTokens: acc.outputTokens,
|
|
245
251
|
cacheCreationTokens: 0,
|
|
246
252
|
cacheReadTokens: acc.cachedInputTokens,
|
|
247
|
-
cost:
|
|
253
|
+
cost: calculateCost(acc, new Set([modelName])),
|
|
248
254
|
}));
|
|
249
255
|
}
|
|
250
256
|
function groupSessions(sessions, options) {
|
|
@@ -278,12 +284,9 @@ function groupSessions(sessions, options) {
|
|
|
278
284
|
break;
|
|
279
285
|
}
|
|
280
286
|
if (!grouped.has(key)) {
|
|
281
|
-
grouped.set(key, { acc: emptyAcc(), models: new
|
|
287
|
+
grouped.set(key, { acc: emptyAcc(), models: new Map() });
|
|
282
288
|
}
|
|
283
|
-
|
|
284
|
-
addAcc(entry.acc, ev);
|
|
285
|
-
if (session.model)
|
|
286
|
-
entry.models.add(session.model);
|
|
289
|
+
addAccToBucket(grouped.get(key), ev, session.model);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
292
|
return grouped;
|
|
@@ -291,23 +294,29 @@ function groupSessions(sessions, options) {
|
|
|
291
294
|
// ---------------------------------------------------------------------------
|
|
292
295
|
// Public API — response builders for route handlers
|
|
293
296
|
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
export function buildCodexResponsesFromSessions(sessions, options) {
|
|
298
|
+
return {
|
|
299
|
+
daily: buildDailyResponse(sessions, options),
|
|
300
|
+
projects: buildProjectsResponse(sessions, options),
|
|
301
|
+
blocks: buildBlocksResponse(sessions, options),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function buildDailyResponse(sessions, options) {
|
|
297
305
|
const grouped = groupSessions(sessions, { groupBy: 'day', ...options });
|
|
298
306
|
const daily = [];
|
|
299
307
|
const totalsAcc = emptyAcc();
|
|
308
|
+
const totalModels = new Map();
|
|
300
309
|
for (const [date, { acc, models }] of grouped) {
|
|
301
310
|
daily.push(accToEntry(date, acc, models));
|
|
302
311
|
mergeAcc(totalsAcc, acc);
|
|
312
|
+
for (const [model, modelAcc] of models) {
|
|
313
|
+
if (!totalModels.has(model))
|
|
314
|
+
totalModels.set(model, emptyAcc());
|
|
315
|
+
mergeAcc(totalModels.get(model), modelAcc);
|
|
316
|
+
}
|
|
303
317
|
}
|
|
304
|
-
// Sort by date ascending
|
|
305
318
|
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
306
|
-
const
|
|
307
|
-
for (const s of sessions)
|
|
308
|
-
if (s.model)
|
|
309
|
-
models.add(s.model);
|
|
310
|
-
const totalCost = calculateCost(totalsAcc, models);
|
|
319
|
+
const totalCost = buildModelBreakdowns(totalModels).reduce((sum, model) => sum + model.cost, 0);
|
|
311
320
|
return {
|
|
312
321
|
daily,
|
|
313
322
|
totals: {
|
|
@@ -320,15 +329,16 @@ export function getDailyResponse(options) {
|
|
|
320
329
|
},
|
|
321
330
|
};
|
|
322
331
|
}
|
|
323
|
-
|
|
324
|
-
export function getProjectsResponse(options) {
|
|
325
|
-
const sessions = parseAllSessions();
|
|
332
|
+
function buildProjectsResponse(sessions, options) {
|
|
326
333
|
const tz = options?.timezone || 'Asia/Shanghai';
|
|
327
|
-
const
|
|
334
|
+
const projectGroups = new Map();
|
|
328
335
|
for (const session of sessions) {
|
|
329
336
|
const projectName = extractProjectName(session.cwd);
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
if (options?.project && projectName !== options.project)
|
|
338
|
+
continue;
|
|
339
|
+
if (!projectGroups.has(projectName))
|
|
340
|
+
projectGroups.set(projectName, new Map());
|
|
341
|
+
const dailyMap = projectGroups.get(projectName);
|
|
332
342
|
for (const ev of session.tokenEvents) {
|
|
333
343
|
const evDate = new Date(ev.timestamp);
|
|
334
344
|
if (options?.since && evDate < options.since)
|
|
@@ -337,32 +347,25 @@ export function getProjectsResponse(options) {
|
|
|
337
347
|
continue;
|
|
338
348
|
const dayKey = getDateKey(ev.timestamp, tz);
|
|
339
349
|
if (!dailyMap.has(dayKey)) {
|
|
340
|
-
dailyMap.set(dayKey, { acc: emptyAcc(), models: new
|
|
350
|
+
dailyMap.set(dayKey, { acc: emptyAcc(), models: new Map() });
|
|
341
351
|
}
|
|
342
|
-
|
|
343
|
-
if (session.model)
|
|
344
|
-
dailyMap.get(dayKey).models.add(session.model);
|
|
345
|
-
}
|
|
346
|
-
if (!projects[projectName])
|
|
347
|
-
projects[projectName] = [];
|
|
348
|
-
for (const [date, { acc, models }] of dailyMap) {
|
|
349
|
-
projects[projectName].push(accToEntry(date, acc, models));
|
|
352
|
+
addAccToBucket(dailyMap.get(dayKey), ev, session.model);
|
|
350
353
|
}
|
|
351
354
|
}
|
|
352
|
-
|
|
353
|
-
for (const
|
|
354
|
-
projects[
|
|
355
|
+
const projects = {};
|
|
356
|
+
for (const [projectName, dailyMap] of projectGroups) {
|
|
357
|
+
projects[projectName] = [...dailyMap.entries()]
|
|
358
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
359
|
+
.map(([date, { acc, models }]) => accToEntry(date, acc, models));
|
|
355
360
|
}
|
|
356
361
|
return { projects };
|
|
357
362
|
}
|
|
358
|
-
|
|
359
|
-
export function getBlocksResponse(options) {
|
|
360
|
-
const sessions = parseAllSessions();
|
|
363
|
+
function buildBlocksResponse(sessions, options) {
|
|
361
364
|
const grouped = groupSessions(sessions, { groupBy: 'hour', ...options });
|
|
362
365
|
const blocks = [];
|
|
363
366
|
let idx = 0;
|
|
364
367
|
for (const [hourKey, { acc, models }] of grouped) {
|
|
365
|
-
const cost =
|
|
368
|
+
const cost = buildModelBreakdowns(models).reduce((sum, model) => sum + model.cost, 0);
|
|
366
369
|
const [datePart, timePart] = hourKey.split(' ');
|
|
367
370
|
const hour = timePart.split(':')[0];
|
|
368
371
|
blocks.push({
|
|
@@ -381,11 +384,22 @@ export function getBlocksResponse(options) {
|
|
|
381
384
|
},
|
|
382
385
|
totalTokens: acc.totalTokens,
|
|
383
386
|
costUSD: cost,
|
|
384
|
-
models: [...models],
|
|
387
|
+
models: [...models.keys()],
|
|
385
388
|
});
|
|
386
389
|
idx++;
|
|
387
390
|
}
|
|
388
|
-
// Sort by startTime
|
|
389
391
|
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
390
392
|
return { blocks };
|
|
391
393
|
}
|
|
394
|
+
/** Aggregate and return DailyResponse format (for /daily?agent=codex) */
|
|
395
|
+
export function getDailyResponse(options) {
|
|
396
|
+
return buildDailyResponse(parseAllSessions(), options);
|
|
397
|
+
}
|
|
398
|
+
/** Aggregate and return ProjectsResponse format (for /projects?agent=codex) */
|
|
399
|
+
export function getProjectsResponse(options) {
|
|
400
|
+
return buildProjectsResponse(parseAllSessions(), options);
|
|
401
|
+
}
|
|
402
|
+
/** Aggregate and return BlocksResponse format (hourly, for /blocks?agent=codex) */
|
|
403
|
+
export function getBlocksResponse(options) {
|
|
404
|
+
return buildBlocksResponse(parseAllSessions(), options);
|
|
405
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Express } from 'express';
|
|
2
|
+
export declare function resolveStaticAssetBaseDir(moduleUrl?: string, baseDir?: string): {
|
|
3
|
+
baseDir: string;
|
|
4
|
+
isProduction: boolean;
|
|
5
|
+
};
|
|
2
6
|
export declare function createApp(_port: number, baseDir?: string): Express;
|
|
3
7
|
declare function main(): Promise<void>;
|
|
4
8
|
export { main };
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { registerApiRoutes } from './routes/api.js';
|
|
6
6
|
import { detectAvailableAgents } from './agentDetection.js';
|
|
7
7
|
import open from 'open';
|
|
@@ -117,22 +117,38 @@ async function listenWithPortFallback(app, preferredPort) {
|
|
|
117
117
|
}
|
|
118
118
|
throw new Error(`Could not find an available port starting from ${preferredPort}`);
|
|
119
119
|
}
|
|
120
|
+
export function resolveStaticAssetBaseDir(moduleUrl = import.meta.url, baseDir) {
|
|
121
|
+
if (baseDir)
|
|
122
|
+
return { baseDir: resolve(baseDir), isProduction: true };
|
|
123
|
+
const moduleDir = dirname(fileURLToPath(moduleUrl));
|
|
124
|
+
const isProduction = moduleUrl.includes('/dist/');
|
|
125
|
+
if (!isProduction)
|
|
126
|
+
return { baseDir: resolve(moduleDir), isProduction: false };
|
|
127
|
+
// The CLI entrypoint runs from dist/server/index.js while the Vite assets are
|
|
128
|
+
// emitted to dist/client. Resolve the production asset base to dist instead
|
|
129
|
+
// of dist/server so / resolves to dist/client/index.html in installed npm
|
|
130
|
+
// packages. Electron passes dist explicitly and is unaffected by this branch.
|
|
131
|
+
if (basename(moduleDir) === 'server') {
|
|
132
|
+
return { baseDir: resolve(dirname(moduleDir)), isProduction: true };
|
|
133
|
+
}
|
|
134
|
+
return { baseDir: resolve(moduleDir), isProduction: true };
|
|
135
|
+
}
|
|
120
136
|
export function createApp(_port, baseDir) {
|
|
121
137
|
const app = express();
|
|
122
138
|
const router = express.Router();
|
|
123
139
|
// Register API routes
|
|
124
140
|
registerApiRoutes(router);
|
|
125
141
|
app.use('/api', router);
|
|
126
|
-
|
|
127
|
-
const _baseDir = baseDir ?? dirname(fileURLToPath(import.meta.url));
|
|
128
|
-
const isProduction = baseDir
|
|
129
|
-
? true
|
|
130
|
-
: import.meta.url.includes('dist/');
|
|
142
|
+
const { baseDir: _baseDir, isProduction } = resolveStaticAssetBaseDir(import.meta.url, baseDir);
|
|
131
143
|
const popoverPath = isProduction
|
|
132
144
|
? join(_baseDir, 'client', 'popover.html')
|
|
133
145
|
: join(_baseDir, '..', '..', 'public', 'popover.html');
|
|
134
|
-
app.get('/popover.html', (_req, res) => {
|
|
135
|
-
|
|
146
|
+
app.get('/popover.html', (_req, res, next) => {
|
|
147
|
+
if (!existsSync(popoverPath)) {
|
|
148
|
+
next();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
res.type('html').send(readFileSync(popoverPath, 'utf8'));
|
|
136
152
|
});
|
|
137
153
|
// Check if running from dist (production build)
|
|
138
154
|
if (isProduction) {
|