@zhangferry-dev/tokendash 1.4.2 → 1.6.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-CY4G_b0x.js} +49 -49
- package/dist/client/assets/index-_yA9tOzZ.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/client/popover.html +822 -396
- package/dist/electron-server.cjs +255 -101
- package/dist/electron-server.cjs.map +3 -3
- package/dist/server/analyticsParser.js +66 -2
- package/dist/server/claudeBlocksParser.js +50 -7
- package/dist/server/claudeJsonlParser.d.ts +7 -1
- package/dist/server/claudeJsonlParser.js +75 -23
- package/dist/server/codexParser.d.ts +6 -2
- package/dist/server/codexParser.js +134 -80
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +25 -9
- package/electron/main.cjs +35 -50
- package/electron/preload.cjs +11 -0
- package/electron/updateService.cjs +148 -0
- package/package.json +1 -1
- package/dist/client/assets/index-iYDpTV63.css +0 -1
|
@@ -15,20 +15,24 @@ const TokenUsageSchema = z.object({
|
|
|
15
15
|
}).default({ input_tokens: 0, cached_input_tokens: 0, output_tokens: 0, reasoning_output_tokens: 0, total_tokens: 0 });
|
|
16
16
|
const TokenCountInfoSchema = z.object({
|
|
17
17
|
total_token_usage: TokenUsageSchema,
|
|
18
|
-
last_token_usage: TokenUsageSchema,
|
|
18
|
+
last_token_usage: TokenUsageSchema.optional(),
|
|
19
19
|
}).nullable().default(null);
|
|
20
20
|
const TokenCountPayloadSchema = z.object({
|
|
21
21
|
type: z.literal('token_count'),
|
|
22
22
|
info: TokenCountInfoSchema,
|
|
23
23
|
});
|
|
24
|
-
function
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
function subtractTokenUsage(current, previous) {
|
|
25
|
+
return {
|
|
26
|
+
timestamp: '',
|
|
27
|
+
inputTokens: Math.max(0, current.input_tokens - (previous?.input_tokens ?? 0)),
|
|
28
|
+
cachedInputTokens: Math.max(0, current.cached_input_tokens - (previous?.cached_input_tokens ?? 0)),
|
|
29
|
+
outputTokens: Math.max(0, current.output_tokens - (previous?.output_tokens ?? 0)),
|
|
30
|
+
reasoningOutputTokens: Math.max(0, current.reasoning_output_tokens - (previous?.reasoning_output_tokens ?? 0)),
|
|
31
|
+
totalTokens: Math.max(0, current.total_tokens - (previous?.total_tokens ?? 0)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function displayInputTokens(inputTokens, cachedInputTokens) {
|
|
35
|
+
return Math.max(0, inputTokens - cachedInputTokens);
|
|
32
36
|
}
|
|
33
37
|
// ---------------------------------------------------------------------------
|
|
34
38
|
// Helpers
|
|
@@ -101,7 +105,9 @@ export function parseCodexSession(filepath) {
|
|
|
101
105
|
let model = '';
|
|
102
106
|
let createdAt = '';
|
|
103
107
|
const tokenEvents = [];
|
|
108
|
+
let previousTotalUsage = null;
|
|
104
109
|
const seenTotalUsageSnapshots = new Set();
|
|
110
|
+
const seenUsageEvents = new Set();
|
|
105
111
|
for (const line of lines) {
|
|
106
112
|
const trimmed = line.trim();
|
|
107
113
|
if (!trimmed)
|
|
@@ -139,19 +145,43 @@ export function parseCodexSession(filepath) {
|
|
|
139
145
|
const info = parseResult.data.info;
|
|
140
146
|
if (!info)
|
|
141
147
|
continue;
|
|
142
|
-
const totalUsageKey =
|
|
148
|
+
const totalUsageKey = [
|
|
149
|
+
info.total_token_usage.input_tokens,
|
|
150
|
+
info.total_token_usage.cached_input_tokens,
|
|
151
|
+
info.total_token_usage.output_tokens,
|
|
152
|
+
info.total_token_usage.reasoning_output_tokens,
|
|
153
|
+
info.total_token_usage.total_tokens,
|
|
154
|
+
].join(':');
|
|
143
155
|
if (seenTotalUsageSnapshots.has(totalUsageKey))
|
|
144
156
|
continue;
|
|
145
157
|
seenTotalUsageSnapshots.add(totalUsageKey);
|
|
146
|
-
const last = info.last_token_usage;
|
|
147
|
-
|
|
158
|
+
const last = info.last_token_usage ?? info.total_token_usage;
|
|
159
|
+
const rawEvent = info.last_token_usage
|
|
160
|
+
? subtractTokenUsage(last, null)
|
|
161
|
+
: subtractTokenUsage(last, previousTotalUsage);
|
|
162
|
+
previousTotalUsage = info.total_token_usage;
|
|
163
|
+
if (rawEvent.inputTokens === 0 && rawEvent.cachedInputTokens === 0 && rawEvent.outputTokens === 0 && rawEvent.reasoningOutputTokens === 0) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const event = {
|
|
167
|
+
...rawEvent,
|
|
148
168
|
timestamp,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
cachedInputTokens: Math.min(rawEvent.cachedInputTokens, rawEvent.inputTokens),
|
|
170
|
+
};
|
|
171
|
+
const eventKey = [
|
|
172
|
+
timestamp,
|
|
173
|
+
model,
|
|
174
|
+
event.inputTokens,
|
|
175
|
+
event.cachedInputTokens,
|
|
176
|
+
event.outputTokens,
|
|
177
|
+
event.reasoningOutputTokens,
|
|
178
|
+
event.totalTokens,
|
|
179
|
+
].join(':');
|
|
180
|
+
if (seenUsageEvents.has(eventKey)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
seenUsageEvents.add(eventKey);
|
|
184
|
+
tokenEvents.push(event);
|
|
155
185
|
}
|
|
156
186
|
}
|
|
157
187
|
}
|
|
@@ -212,6 +242,12 @@ function addAcc(a, ev) {
|
|
|
212
242
|
a.reasoningOutputTokens += ev.reasoningOutputTokens;
|
|
213
243
|
a.totalTokens += ev.totalTokens;
|
|
214
244
|
}
|
|
245
|
+
function displayAcc(acc) {
|
|
246
|
+
return {
|
|
247
|
+
...acc,
|
|
248
|
+
inputTokens: displayInputTokens(acc.inputTokens, acc.cachedInputTokens),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
215
251
|
function mergeAcc(a, b) {
|
|
216
252
|
a.inputTokens += b.inputTokens;
|
|
217
253
|
a.cachedInputTokens += b.cachedInputTokens;
|
|
@@ -219,33 +255,43 @@ function mergeAcc(a, b) {
|
|
|
219
255
|
a.reasoningOutputTokens += b.reasoningOutputTokens;
|
|
220
256
|
a.totalTokens += b.totalTokens;
|
|
221
257
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
258
|
+
function addAccToBucket(bucket, ev, model) {
|
|
259
|
+
addAcc(bucket.acc, ev);
|
|
260
|
+
if (!model)
|
|
261
|
+
return;
|
|
262
|
+
if (!bucket.models.has(model))
|
|
263
|
+
bucket.models.set(model, emptyAcc());
|
|
264
|
+
addAcc(bucket.models.get(model), ev);
|
|
265
|
+
}
|
|
266
|
+
function accToEntry(date, acc, modelAccs) {
|
|
267
|
+
const display = displayAcc(acc);
|
|
268
|
+
const modelNames = [...modelAccs.keys()];
|
|
269
|
+
const modelBreakdowns = buildModelBreakdowns(modelAccs);
|
|
270
|
+
const totalCost = modelBreakdowns.reduce((sum, model) => sum + model.cost, 0);
|
|
224
271
|
return {
|
|
225
272
|
date,
|
|
226
|
-
inputTokens:
|
|
227
|
-
outputTokens:
|
|
273
|
+
inputTokens: display.inputTokens,
|
|
274
|
+
outputTokens: display.outputTokens,
|
|
228
275
|
cacheCreationTokens: 0,
|
|
229
|
-
cacheReadTokens:
|
|
230
|
-
totalTokens:
|
|
231
|
-
totalCost
|
|
232
|
-
modelsUsed:
|
|
233
|
-
modelBreakdowns
|
|
276
|
+
cacheReadTokens: display.cachedInputTokens,
|
|
277
|
+
totalTokens: display.totalTokens,
|
|
278
|
+
totalCost,
|
|
279
|
+
modelsUsed: modelNames,
|
|
280
|
+
modelBreakdowns,
|
|
234
281
|
};
|
|
235
282
|
}
|
|
236
|
-
function buildModelBreakdowns(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}));
|
|
283
|
+
function buildModelBreakdowns(modelAccs) {
|
|
284
|
+
return [...modelAccs.entries()].map(([modelName, acc]) => {
|
|
285
|
+
const display = displayAcc(acc);
|
|
286
|
+
return {
|
|
287
|
+
modelName,
|
|
288
|
+
inputTokens: display.inputTokens,
|
|
289
|
+
outputTokens: display.outputTokens,
|
|
290
|
+
cacheCreationTokens: 0,
|
|
291
|
+
cacheReadTokens: display.cachedInputTokens,
|
|
292
|
+
cost: calculateCost(acc, new Set([modelName])),
|
|
293
|
+
};
|
|
294
|
+
});
|
|
249
295
|
}
|
|
250
296
|
function groupSessions(sessions, options) {
|
|
251
297
|
const tz = options.timezone || 'Asia/Shanghai';
|
|
@@ -278,12 +324,9 @@ function groupSessions(sessions, options) {
|
|
|
278
324
|
break;
|
|
279
325
|
}
|
|
280
326
|
if (!grouped.has(key)) {
|
|
281
|
-
grouped.set(key, { acc: emptyAcc(), models: new
|
|
327
|
+
grouped.set(key, { acc: emptyAcc(), models: new Map() });
|
|
282
328
|
}
|
|
283
|
-
|
|
284
|
-
addAcc(entry.acc, ev);
|
|
285
|
-
if (session.model)
|
|
286
|
-
entry.models.add(session.model);
|
|
329
|
+
addAccToBucket(grouped.get(key), ev, session.model);
|
|
287
330
|
}
|
|
288
331
|
}
|
|
289
332
|
return grouped;
|
|
@@ -291,27 +334,33 @@ function groupSessions(sessions, options) {
|
|
|
291
334
|
// ---------------------------------------------------------------------------
|
|
292
335
|
// Public API — response builders for route handlers
|
|
293
336
|
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
337
|
+
export function buildCodexResponsesFromSessions(sessions, options) {
|
|
338
|
+
return {
|
|
339
|
+
daily: buildDailyResponse(sessions, options),
|
|
340
|
+
projects: buildProjectsResponse(sessions, options),
|
|
341
|
+
blocks: buildBlocksResponse(sessions, options),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function buildDailyResponse(sessions, options) {
|
|
297
345
|
const grouped = groupSessions(sessions, { groupBy: 'day', ...options });
|
|
298
346
|
const daily = [];
|
|
299
347
|
const totalsAcc = emptyAcc();
|
|
348
|
+
const totalModels = new Map();
|
|
300
349
|
for (const [date, { acc, models }] of grouped) {
|
|
301
350
|
daily.push(accToEntry(date, acc, models));
|
|
302
351
|
mergeAcc(totalsAcc, acc);
|
|
352
|
+
for (const [model, modelAcc] of models) {
|
|
353
|
+
if (!totalModels.has(model))
|
|
354
|
+
totalModels.set(model, emptyAcc());
|
|
355
|
+
mergeAcc(totalModels.get(model), modelAcc);
|
|
356
|
+
}
|
|
303
357
|
}
|
|
304
|
-
// Sort by date ascending
|
|
305
358
|
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);
|
|
359
|
+
const totalCost = buildModelBreakdowns(totalModels).reduce((sum, model) => sum + model.cost, 0);
|
|
311
360
|
return {
|
|
312
361
|
daily,
|
|
313
362
|
totals: {
|
|
314
|
-
inputTokens: totalsAcc.inputTokens,
|
|
363
|
+
inputTokens: displayInputTokens(totalsAcc.inputTokens, totalsAcc.cachedInputTokens),
|
|
315
364
|
outputTokens: totalsAcc.outputTokens,
|
|
316
365
|
cacheCreationTokens: 0,
|
|
317
366
|
cacheReadTokens: totalsAcc.cachedInputTokens,
|
|
@@ -320,15 +369,16 @@ export function getDailyResponse(options) {
|
|
|
320
369
|
},
|
|
321
370
|
};
|
|
322
371
|
}
|
|
323
|
-
|
|
324
|
-
export function getProjectsResponse(options) {
|
|
325
|
-
const sessions = parseAllSessions();
|
|
372
|
+
function buildProjectsResponse(sessions, options) {
|
|
326
373
|
const tz = options?.timezone || 'Asia/Shanghai';
|
|
327
|
-
const
|
|
374
|
+
const projectGroups = new Map();
|
|
328
375
|
for (const session of sessions) {
|
|
329
376
|
const projectName = extractProjectName(session.cwd);
|
|
330
|
-
|
|
331
|
-
|
|
377
|
+
if (options?.project && projectName !== options.project)
|
|
378
|
+
continue;
|
|
379
|
+
if (!projectGroups.has(projectName))
|
|
380
|
+
projectGroups.set(projectName, new Map());
|
|
381
|
+
const dailyMap = projectGroups.get(projectName);
|
|
332
382
|
for (const ev of session.tokenEvents) {
|
|
333
383
|
const evDate = new Date(ev.timestamp);
|
|
334
384
|
if (options?.since && evDate < options.since)
|
|
@@ -337,32 +387,25 @@ export function getProjectsResponse(options) {
|
|
|
337
387
|
continue;
|
|
338
388
|
const dayKey = getDateKey(ev.timestamp, tz);
|
|
339
389
|
if (!dailyMap.has(dayKey)) {
|
|
340
|
-
dailyMap.set(dayKey, { acc: emptyAcc(), models: new
|
|
390
|
+
dailyMap.set(dayKey, { acc: emptyAcc(), models: new Map() });
|
|
341
391
|
}
|
|
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));
|
|
392
|
+
addAccToBucket(dailyMap.get(dayKey), ev, session.model);
|
|
350
393
|
}
|
|
351
394
|
}
|
|
352
|
-
|
|
353
|
-
for (const
|
|
354
|
-
projects[
|
|
395
|
+
const projects = {};
|
|
396
|
+
for (const [projectName, dailyMap] of projectGroups) {
|
|
397
|
+
projects[projectName] = [...dailyMap.entries()]
|
|
398
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
399
|
+
.map(([date, { acc, models }]) => accToEntry(date, acc, models));
|
|
355
400
|
}
|
|
356
401
|
return { projects };
|
|
357
402
|
}
|
|
358
|
-
|
|
359
|
-
export function getBlocksResponse(options) {
|
|
360
|
-
const sessions = parseAllSessions();
|
|
403
|
+
function buildBlocksResponse(sessions, options) {
|
|
361
404
|
const grouped = groupSessions(sessions, { groupBy: 'hour', ...options });
|
|
362
405
|
const blocks = [];
|
|
363
406
|
let idx = 0;
|
|
364
407
|
for (const [hourKey, { acc, models }] of grouped) {
|
|
365
|
-
const cost =
|
|
408
|
+
const cost = buildModelBreakdowns(models).reduce((sum, model) => sum + model.cost, 0);
|
|
366
409
|
const [datePart, timePart] = hourKey.split(' ');
|
|
367
410
|
const hour = timePart.split(':')[0];
|
|
368
411
|
blocks.push({
|
|
@@ -374,18 +417,29 @@ export function getBlocksResponse(options) {
|
|
|
374
417
|
isGap: false,
|
|
375
418
|
entries: acc.totalTokens > 0 ? 1 : 0,
|
|
376
419
|
tokenCounts: {
|
|
377
|
-
inputTokens: acc.inputTokens,
|
|
420
|
+
inputTokens: displayInputTokens(acc.inputTokens, acc.cachedInputTokens),
|
|
378
421
|
outputTokens: acc.outputTokens,
|
|
379
422
|
cacheCreationInputTokens: 0,
|
|
380
423
|
cacheReadInputTokens: acc.cachedInputTokens,
|
|
381
424
|
},
|
|
382
425
|
totalTokens: acc.totalTokens,
|
|
383
426
|
costUSD: cost,
|
|
384
|
-
models: [...models],
|
|
427
|
+
models: [...models.keys()],
|
|
385
428
|
});
|
|
386
429
|
idx++;
|
|
387
430
|
}
|
|
388
|
-
// Sort by startTime
|
|
389
431
|
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
390
432
|
return { blocks };
|
|
391
433
|
}
|
|
434
|
+
/** Aggregate and return DailyResponse format (for /daily?agent=codex) */
|
|
435
|
+
export function getDailyResponse(options) {
|
|
436
|
+
return buildDailyResponse(parseAllSessions(), options);
|
|
437
|
+
}
|
|
438
|
+
/** Aggregate and return ProjectsResponse format (for /projects?agent=codex) */
|
|
439
|
+
export function getProjectsResponse(options) {
|
|
440
|
+
return buildProjectsResponse(parseAllSessions(), options);
|
|
441
|
+
}
|
|
442
|
+
/** Aggregate and return BlocksResponse format (hourly, for /blocks?agent=codex) */
|
|
443
|
+
export function getBlocksResponse(options) {
|
|
444
|
+
return buildBlocksResponse(parseAllSessions(), options);
|
|
445
|
+
}
|
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) {
|
package/electron/main.cjs
CHANGED
|
@@ -2,7 +2,6 @@ const { app, BrowserWindow, ipcMain, screen, shell } = require('electron');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const http = require('node:http');
|
|
5
|
-
const https = require('node:https');
|
|
6
5
|
const { spawn } = require('node:child_process');
|
|
7
6
|
|
|
8
7
|
// Global debug logger (writes to file since stdout is lost in packaged apps)
|
|
@@ -19,6 +18,7 @@ try {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
const { formatTokens } = require('./trayBadge.cjs');
|
|
21
|
+
const { checkForUpdates, downloadUpdateAsset } = require('./updateService.cjs');
|
|
22
22
|
|
|
23
23
|
// Resolve trayHelper binary: extract from asar if needed
|
|
24
24
|
function resolveTrayHelperPath() {
|
|
@@ -52,6 +52,8 @@ let server = null;
|
|
|
52
52
|
let trayProcess = null;
|
|
53
53
|
let selectedAgents = null; // null = use all available agents
|
|
54
54
|
let serverPort = parseInt(process.env.TOKENDASH_PORT || '3456', 10);
|
|
55
|
+
let lastUpdateInfo = null;
|
|
56
|
+
let isDownloadingUpdate = false;
|
|
55
57
|
const POPOVER_WIDTH = 380;
|
|
56
58
|
const POPOVER_HEIGHT = 540;
|
|
57
59
|
const PACKAGE_NAME = '@zhangferry-dev/tokendash';
|
|
@@ -97,41 +99,6 @@ function fetchJson(url) {
|
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
function fetchHttpsJson(url) {
|
|
101
|
-
return new Promise((resolve, reject) => {
|
|
102
|
-
const opts = new URL(url);
|
|
103
|
-
const reqOpts = {
|
|
104
|
-
hostname: opts.hostname,
|
|
105
|
-
path: opts.pathname + opts.search,
|
|
106
|
-
method: 'GET',
|
|
107
|
-
headers: { 'User-Agent': 'TokenDash' },
|
|
108
|
-
};
|
|
109
|
-
https.get(reqOpts, (res) => {
|
|
110
|
-
let data = '';
|
|
111
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
112
|
-
res.on('end', () => {
|
|
113
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
114
|
-
reject(new Error(`HTTP ${res.statusCode}`));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
try { resolve(JSON.parse(data)); }
|
|
118
|
-
catch (e) { reject(e); }
|
|
119
|
-
});
|
|
120
|
-
}).on('error', reject);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function compareVersions(a, b) {
|
|
125
|
-
const aParts = String(a).split('.').map((part) => parseInt(part, 10) || 0);
|
|
126
|
-
const bParts = String(b).split('.').map((part) => parseInt(part, 10) || 0);
|
|
127
|
-
const maxLen = Math.max(aParts.length, bParts.length);
|
|
128
|
-
for (let i = 0; i < maxLen; i++) {
|
|
129
|
-
const delta = (aParts[i] || 0) - (bParts[i] || 0);
|
|
130
|
-
if (delta !== 0) return delta;
|
|
131
|
-
}
|
|
132
|
-
return 0;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
102
|
function getAppInfo() {
|
|
136
103
|
// app.getVersion() returns Electron's version in dev mode (e.g. 41.5).
|
|
137
104
|
// Always read from package.json to get the app's own version.
|
|
@@ -258,17 +225,19 @@ function getTrayAgentKey(agents) {
|
|
|
258
225
|
|
|
259
226
|
function applyTraySnapshot(snapshot) {
|
|
260
227
|
const totalTokens = Number(snapshot && snapshot.totalTokens) || 0;
|
|
228
|
+
const totalInput = Number(snapshot && snapshot.totalInput) || 0;
|
|
261
229
|
const totalCost = Number(snapshot && snapshot.totalCost) || 0;
|
|
262
230
|
const totalCacheRead = Number(snapshot && snapshot.totalCacheRead) || 0;
|
|
263
231
|
const today = snapshot && snapshot.today;
|
|
264
232
|
const agentKey = snapshot && snapshot.agentKey;
|
|
265
233
|
|
|
266
|
-
lastTraySnapshot = { today, agentKey, totalTokens, totalCost, totalCacheRead };
|
|
234
|
+
lastTraySnapshot = { today, agentKey, totalTokens, totalInput, totalCost, totalCacheRead };
|
|
267
235
|
|
|
268
236
|
const tokenStr = formatTokens(totalTokens);
|
|
269
237
|
sendTrayCommand('title:' + tokenStr);
|
|
270
238
|
|
|
271
|
-
const
|
|
239
|
+
const cacheInput = totalInput + totalCacheRead;
|
|
240
|
+
const cacheRate = cacheInput > 0 ? ((totalCacheRead / cacheInput) * 100).toFixed(1) : '0.0';
|
|
272
241
|
sendTrayCommand('tooltip:TokenDash - ' + tokenStr + ' tokens today ($' + totalCost.toFixed(2) + ') | cache: ' + cacheRate + '%');
|
|
273
242
|
}
|
|
274
243
|
|
|
@@ -337,7 +306,7 @@ function updateTrayBadge() {
|
|
|
337
306
|
return;
|
|
338
307
|
}
|
|
339
308
|
|
|
340
|
-
applyTraySnapshot({ today, agentKey, totalTokens, totalCost, totalCacheRead });
|
|
309
|
+
applyTraySnapshot({ today, agentKey, totalTokens, totalInput, totalCost, totalCacheRead });
|
|
341
310
|
})
|
|
342
311
|
.catch((err) => {
|
|
343
312
|
if (err.code !== 'ECONNREFUSED') {
|
|
@@ -413,19 +382,10 @@ function registerIpcHandlers() {
|
|
|
413
382
|
|
|
414
383
|
ipcMain.handle('tokendash:check-for-updates', async () => {
|
|
415
384
|
const currentVersion = getAppInfo().version;
|
|
416
|
-
const releasesUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
417
385
|
|
|
418
386
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const tag = (latest.tag_name || '').replace(/^v/, '');
|
|
422
|
-
const latestVersion = tag || currentVersion;
|
|
423
|
-
return {
|
|
424
|
-
currentVersion,
|
|
425
|
-
latestVersion,
|
|
426
|
-
upToDate: compareVersions(currentVersion, latestVersion) >= 0,
|
|
427
|
-
releaseUrl: latest.html_url || null,
|
|
428
|
-
};
|
|
387
|
+
lastUpdateInfo = await checkForUpdates({ repo: GITHUB_REPO, currentVersion });
|
|
388
|
+
return lastUpdateInfo;
|
|
429
389
|
} catch (error) {
|
|
430
390
|
return {
|
|
431
391
|
currentVersion,
|
|
@@ -436,6 +396,31 @@ function registerIpcHandlers() {
|
|
|
436
396
|
}
|
|
437
397
|
});
|
|
438
398
|
|
|
399
|
+
ipcMain.handle('tokendash:download-update', async (event) => {
|
|
400
|
+
if (isDownloadingUpdate) {
|
|
401
|
+
return { ok: false, error: 'An update download is already in progress.' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const info = lastUpdateInfo;
|
|
405
|
+
if (!info || info.upToDate || !info.asset || !info.asset.url) {
|
|
406
|
+
return { ok: false, error: 'No downloadable update is available.' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
isDownloadingUpdate = true;
|
|
410
|
+
try {
|
|
411
|
+
const downloadsDir = path.join(app.getPath('downloads'), 'TokenDash Updates');
|
|
412
|
+
const filePath = await downloadUpdateAsset(info.asset, downloadsDir, (progress) => {
|
|
413
|
+
event.sender.send('tokendash:update-download-progress', progress);
|
|
414
|
+
});
|
|
415
|
+
await shell.openPath(filePath);
|
|
416
|
+
return { ok: true, filePath };
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
419
|
+
} finally {
|
|
420
|
+
isDownloadingUpdate = false;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
439
424
|
ipcMain.handle('tokendash:quit', () => {
|
|
440
425
|
app.isQuitting = true;
|
|
441
426
|
stopBadgeUpdates();
|
package/electron/preload.cjs
CHANGED
|
@@ -13,6 +13,17 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
13
13
|
checkForUpdates() {
|
|
14
14
|
return ipcRenderer.invoke('tokendash:check-for-updates');
|
|
15
15
|
},
|
|
16
|
+
downloadUpdate(updateInfo) {
|
|
17
|
+
return ipcRenderer.invoke('tokendash:download-update', updateInfo);
|
|
18
|
+
},
|
|
19
|
+
onUpdateDownloadProgress(callback) {
|
|
20
|
+
if (typeof callback !== 'function') return function noop() {};
|
|
21
|
+
const listener = (_event, progress) => callback(progress);
|
|
22
|
+
ipcRenderer.on('tokendash:update-download-progress', listener);
|
|
23
|
+
return function unsubscribe() {
|
|
24
|
+
ipcRenderer.removeListener('tokendash:update-download-progress', listener);
|
|
25
|
+
};
|
|
26
|
+
},
|
|
16
27
|
quitApp() {
|
|
17
28
|
return ipcRenderer.invoke('tokendash:quit');
|
|
18
29
|
},
|