@zhangferry-dev/tokendash 1.0.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.
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ccusage dashboard</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='28' font-size='28'>⚡</text></svg>" />
8
+ <script type="module" crossorigin src="/assets/index-DOeZtR1c.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-C9UxEhwo.css">
10
+ </head>
11
+ <body class="antialiased" style="background:#faf9f7">
12
+ <div id="root"></div>
13
+ <style>html,body,#root{background:#faf9f7!important;min-height:100vh}</style>
14
+ </body>
15
+ </html>
@@ -0,0 +1,14 @@
1
+ interface CacheEntry<T> {
2
+ data: T;
3
+ expiresAt: number;
4
+ }
5
+ declare class Cache {
6
+ private store;
7
+ get<T>(key: string): T | null;
8
+ set<T>(key: string, data: T, ttl?: number): void;
9
+ clear(): void;
10
+ delete(key: string): boolean;
11
+ has(key: string): boolean;
12
+ }
13
+ export declare const cache: Cache;
14
+ export type { CacheEntry };
@@ -0,0 +1,40 @@
1
+ const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
2
+ class Cache {
3
+ store = new Map();
4
+ get(key) {
5
+ const entry = this.store.get(key);
6
+ if (!entry) {
7
+ return null;
8
+ }
9
+ if (Date.now() > entry.expiresAt) {
10
+ this.store.delete(key);
11
+ return null;
12
+ }
13
+ return entry.data;
14
+ }
15
+ set(key, data, ttl = DEFAULT_TTL) {
16
+ const entry = {
17
+ data,
18
+ expiresAt: Date.now() + ttl,
19
+ };
20
+ this.store.set(key, entry);
21
+ }
22
+ clear() {
23
+ this.store.clear();
24
+ }
25
+ delete(key) {
26
+ return this.store.delete(key);
27
+ }
28
+ has(key) {
29
+ const entry = this.store.get(key);
30
+ if (!entry) {
31
+ return false;
32
+ }
33
+ if (Date.now() > entry.expiresAt) {
34
+ this.store.delete(key);
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ }
40
+ export const cache = new Cache();
@@ -0,0 +1,3 @@
1
+ export declare function runCcusage(args: string[], timeout?: number): Promise<string>;
2
+ export declare function runCodex(args: string[], timeout?: number): Promise<string>;
3
+ export declare function ensureUsageToolsReady(): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ function withJsonFlag(args, asJson) {
5
+ if (!asJson || args.includes('--json')) {
6
+ return args;
7
+ }
8
+ return [...args, '--json'];
9
+ }
10
+ async function runCommand(spec, timeout) {
11
+ const { stdout } = await execFileAsync(spec.command, spec.args, {
12
+ timeout,
13
+ maxBuffer: 10 * 1024 * 1024,
14
+ });
15
+ return stdout;
16
+ }
17
+ function isMissingCommand(error) {
18
+ return typeof error === 'object'
19
+ && error !== null
20
+ && 'code' in error
21
+ && error.code === 'ENOENT';
22
+ }
23
+ async function runCcusageCommand(args, timeout, asJson) {
24
+ const primary = {
25
+ command: 'ccusage',
26
+ args: withJsonFlag(args, asJson),
27
+ };
28
+ const fallback = {
29
+ command: 'npx',
30
+ args: ['--yes', 'ccusage@latest', ...withJsonFlag(args, asJson)],
31
+ };
32
+ try {
33
+ return await runCommand(primary, timeout);
34
+ }
35
+ catch (error) {
36
+ if (isMissingCommand(error)) {
37
+ return runCommand(fallback, timeout);
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ async function runCodexCommand(args, timeout, asJson) {
43
+ return runCommand({
44
+ command: 'npx',
45
+ args: ['--yes', '@ccusage/codex@latest', ...withJsonFlag(args, asJson)],
46
+ }, timeout);
47
+ }
48
+ export async function runCcusage(args, timeout = 30_000) {
49
+ return runCcusageCommand(args, timeout, true);
50
+ }
51
+ export async function runCodex(args, timeout = 30_000) {
52
+ return runCodexCommand(args, timeout, true);
53
+ }
54
+ export async function ensureUsageToolsReady() {
55
+ await runCcusageCommand(['--version'], 120_000, false);
56
+ await runCodexCommand(['--help'], 120_000, false);
57
+ }
@@ -0,0 +1,4 @@
1
+ import type { DailyResponse, ProjectsResponse, BlocksResponse } from '../shared/types.js';
2
+ export declare function normalizeCodexDailyResponse(data: unknown): DailyResponse;
3
+ export declare function normalizeCodexProjectsResponse(data: unknown): ProjectsResponse;
4
+ export declare function emptyBlocksResponse(): BlocksResponse;
@@ -0,0 +1,50 @@
1
+ function parseCodexDate(dateStr) {
2
+ // "Mar 31, 2026" → "2026-03-31"
3
+ const d = new Date(dateStr);
4
+ return d.toISOString().slice(0, 10);
5
+ }
6
+ function normalizeCodexDaily(entry) {
7
+ const modelBreakdowns = Object.entries(entry.models).map(([name, m]) => ({
8
+ modelName: name,
9
+ inputTokens: m.inputTokens,
10
+ outputTokens: m.outputTokens,
11
+ cacheCreationTokens: 0,
12
+ cacheReadTokens: m.cachedInputTokens,
13
+ cost: entry.costUSD / Object.keys(entry.models).length,
14
+ }));
15
+ return {
16
+ date: parseCodexDate(entry.date),
17
+ inputTokens: entry.inputTokens,
18
+ outputTokens: entry.outputTokens,
19
+ cacheCreationTokens: 0,
20
+ cacheReadTokens: entry.cachedInputTokens,
21
+ totalTokens: entry.totalTokens,
22
+ totalCost: entry.costUSD,
23
+ modelsUsed: Object.keys(entry.models),
24
+ modelBreakdowns,
25
+ };
26
+ }
27
+ export function normalizeCodexDailyResponse(data) {
28
+ const codex = data;
29
+ return {
30
+ daily: (codex.daily || []).map(normalizeCodexDaily),
31
+ totals: {
32
+ inputTokens: codex.totals?.inputTokens ?? 0,
33
+ outputTokens: codex.totals?.outputTokens ?? 0,
34
+ cacheCreationTokens: 0,
35
+ cacheReadTokens: codex.totals?.cachedInputTokens ?? 0,
36
+ totalTokens: codex.totals?.totalTokens ?? 0,
37
+ totalCost: codex.totals?.costUSD ?? 0,
38
+ },
39
+ };
40
+ }
41
+ export function normalizeCodexProjectsResponse(data) {
42
+ const codex = data;
43
+ const entries = (codex.daily || []).map(normalizeCodexDaily);
44
+ return {
45
+ projects: { 'OpenAI Codex': entries },
46
+ };
47
+ }
48
+ export function emptyBlocksResponse() {
49
+ return { blocks: [] };
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import express from 'express';
2
+ import { registerApiRoutes } from './routes/api.js';
3
+ import { ensureUsageToolsReady } from './ccusage.js';
4
+ import open from 'open';
5
+ function parseCliArgs() {
6
+ const args = process.argv.slice(2);
7
+ const result = {};
8
+ for (let i = 0; i < args.length; i++) {
9
+ const arg = args[i];
10
+ if (arg === '--port' && i + 1 < args.length) {
11
+ result.port = parseInt(args[i + 1], 10);
12
+ i++;
13
+ }
14
+ else if (arg === '--no-open') {
15
+ result.noOpen = true;
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ async function ensureUsageSupportAvailable() {
21
+ try {
22
+ await ensureUsageToolsReady();
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ console.error('Error: failed to prepare ccusage support for Claude Code or Codex');
27
+ console.error('\nDetails:', error instanceof Error ? error.message : error);
28
+ return false;
29
+ }
30
+ }
31
+ async function main() {
32
+ const args = parseCliArgs();
33
+ const port = args.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3456);
34
+ const shouldOpenBrowser = !args.noOpen;
35
+ const isUsageSupportAvailable = await ensureUsageSupportAvailable();
36
+ if (!isUsageSupportAvailable) {
37
+ process.exit(1);
38
+ }
39
+ const app = express();
40
+ const router = express.Router();
41
+ // Register API routes
42
+ registerApiRoutes(router);
43
+ app.use('/api', router);
44
+ // Check if running from dist (production build)
45
+ const isProduction = import.meta.url.includes('dist/');
46
+ if (isProduction) {
47
+ // Serve static files from client build
48
+ const clientPath = new URL('../client', import.meta.url).pathname;
49
+ const clientIndexPath = new URL('../client/index.html', import.meta.url).pathname;
50
+ app.use(express.static(clientPath));
51
+ // SPA fallback
52
+ app.use('{*path}', (_req, res) => {
53
+ res.sendFile(clientIndexPath);
54
+ });
55
+ }
56
+ const server = app.listen(port, () => {
57
+ console.log(`ccusage-dashboard running on http://localhost:${port}`);
58
+ if (isProduction) {
59
+ console.log('Serving production build');
60
+ }
61
+ else {
62
+ console.log('Development mode - use "npm run dev" for full dev experience');
63
+ }
64
+ });
65
+ // Open browser if requested
66
+ if (shouldOpenBrowser) {
67
+ // Small delay to ensure server is ready
68
+ setTimeout(() => {
69
+ open(`http://localhost:${port}`).catch((err) => {
70
+ console.warn('Could not open browser:', err.message);
71
+ });
72
+ }, 100);
73
+ }
74
+ // Graceful shutdown
75
+ process.on('SIGTERM', () => {
76
+ server.close(() => {
77
+ console.log('Server closed');
78
+ process.exit(0);
79
+ });
80
+ });
81
+ }
82
+ main().catch((error) => {
83
+ console.error('Fatal error:', error);
84
+ process.exit(1);
85
+ });
@@ -0,0 +1,2 @@
1
+ import { type Router } from 'express';
2
+ export declare function registerApiRoutes(router: Router): void;
@@ -0,0 +1,12 @@
1
+ import { getDaily } from './daily.js';
2
+ import { getMonthly } from './monthly.js';
3
+ import { getSession } from './session.js';
4
+ import { getProjects } from './projects.js';
5
+ import { getBlocks } from './blocks.js';
6
+ export function registerApiRoutes(router) {
7
+ router.get('/daily', getDaily);
8
+ router.get('/monthly', getMonthly);
9
+ router.get('/session', getSession);
10
+ router.get('/projects', getProjects);
11
+ router.get('/blocks', getBlocks);
12
+ }
@@ -0,0 +1,2 @@
1
+ import { type Request, type Response } from 'express';
2
+ export declare function getBlocks(req: Request, res: Response): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { runCcusage } from '../ccusage.js';
2
+ import { cache } from '../cache.js';
3
+ import { validateBlocks } from '../../shared/schemas.js';
4
+ import { emptyBlocksResponse } from '../codexNormalizer.js';
5
+ export async function getBlocks(req, res) {
6
+ const agent = req.query.agent || 'claude';
7
+ const cacheKey = `blocks:${agent}`;
8
+ try {
9
+ if (agent === 'codex') {
10
+ res.json(emptyBlocksResponse());
11
+ return;
12
+ }
13
+ const cached = cache.get(cacheKey);
14
+ if (cached) {
15
+ res.json(cached);
16
+ return;
17
+ }
18
+ const stdout = await runCcusage(['blocks']);
19
+ const data = JSON.parse(stdout);
20
+ const validated = validateBlocks(data);
21
+ cache.set(cacheKey, validated);
22
+ res.json(validated);
23
+ }
24
+ catch (error) {
25
+ const message = error instanceof Error ? error.message : 'Unknown error';
26
+ console.error('Error fetching blocks data:', error);
27
+ res.status(502).json({
28
+ error: 'Failed to fetch blocks data from ccusage',
29
+ hint: message,
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,2 @@
1
+ import { type Request, type Response } from 'express';
2
+ export declare function getDaily(req: Request, res: Response): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { runCcusage, runCodex } from '../ccusage.js';
2
+ import { cache } from '../cache.js';
3
+ import { validateDaily } from '../../shared/schemas.js';
4
+ import { normalizeCodexDailyResponse } from '../codexNormalizer.js';
5
+ export async function getDaily(req, res) {
6
+ const agent = req.query.agent || 'claude';
7
+ const cacheKey = `daily:${agent}`;
8
+ try {
9
+ const cached = cache.get(cacheKey);
10
+ if (cached) {
11
+ res.json(cached);
12
+ return;
13
+ }
14
+ if (agent === 'codex') {
15
+ const stdout = await runCodex(['daily']);
16
+ const data = JSON.parse(stdout);
17
+ const normalized = normalizeCodexDailyResponse(data);
18
+ cache.set(cacheKey, normalized);
19
+ res.json(normalized);
20
+ }
21
+ else {
22
+ const stdout = await runCcusage(['daily', '--breakdown']);
23
+ const data = JSON.parse(stdout);
24
+ const validated = validateDaily(data);
25
+ cache.set(cacheKey, validated);
26
+ res.json(validated);
27
+ }
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : 'Unknown error';
31
+ console.error('Error fetching daily data:', error);
32
+ res.status(502).json({
33
+ error: `Failed to fetch daily data from ${agent}`,
34
+ hint: message,
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ import { type Request, type Response } from 'express';
2
+ export declare function getMonthly(_req: Request, res: Response): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { runCcusage } from '../ccusage.js';
2
+ import { cache } from '../cache.js';
3
+ import { validateDaily } from '../../shared/schemas.js';
4
+ export async function getMonthly(_req, res) {
5
+ try {
6
+ const cached = cache.get('monthly');
7
+ if (cached) {
8
+ res.json(cached);
9
+ return;
10
+ }
11
+ const stdout = await runCcusage(['monthly', '--breakdown']);
12
+ const data = JSON.parse(stdout);
13
+ const validated = validateDaily(data);
14
+ cache.set('monthly', validated);
15
+ res.json(validated);
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : 'Unknown error';
19
+ console.error('Error fetching monthly data:', error);
20
+ res.status(502).json({
21
+ error: 'Failed to fetch monthly data from ccusage',
22
+ hint: message,
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,2 @@
1
+ import { type Request, type Response } from 'express';
2
+ export declare function getProjects(req: Request, res: Response): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { runCcusage, runCodex } from '../ccusage.js';
2
+ import { cache } from '../cache.js';
3
+ import { validateProjects } from '../../shared/schemas.js';
4
+ import { normalizeCodexProjectsResponse } from '../codexNormalizer.js';
5
+ export async function getProjects(req, res) {
6
+ const agent = req.query.agent || 'claude';
7
+ const cacheKey = `projects:${agent}`;
8
+ try {
9
+ const cached = cache.get(cacheKey);
10
+ if (cached) {
11
+ res.json(cached);
12
+ return;
13
+ }
14
+ if (agent === 'codex') {
15
+ const stdout = await runCodex(['daily']);
16
+ const data = JSON.parse(stdout);
17
+ const normalized = normalizeCodexProjectsResponse(data);
18
+ cache.set(cacheKey, normalized);
19
+ res.json(normalized);
20
+ }
21
+ else {
22
+ const stdout = await runCcusage(['daily', '--instances', '--breakdown']);
23
+ const data = JSON.parse(stdout);
24
+ const validated = validateProjects(data);
25
+ cache.set(cacheKey, validated);
26
+ res.json(validated);
27
+ }
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : 'Unknown error';
31
+ console.error('Error fetching projects data:', error);
32
+ res.status(502).json({
33
+ error: `Failed to fetch projects data from ${agent}`,
34
+ hint: message,
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ import { type Request, type Response } from 'express';
2
+ export declare function getSession(_req: Request, res: Response): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { runCcusage } from '../ccusage.js';
2
+ import { cache } from '../cache.js';
3
+ import { validateDaily } from '../../shared/schemas.js';
4
+ export async function getSession(_req, res) {
5
+ try {
6
+ const cached = cache.get('session');
7
+ if (cached) {
8
+ res.json(cached);
9
+ return;
10
+ }
11
+ const stdout = await runCcusage(['session']);
12
+ const data = JSON.parse(stdout);
13
+ const validated = validateDaily(data);
14
+ cache.set('session', validated);
15
+ res.json(validated);
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : 'Unknown error';
19
+ console.error('Error fetching session data:', error);
20
+ res.status(502).json({
21
+ error: 'Failed to fetch session data from ccusage',
22
+ hint: message,
23
+ });
24
+ }
25
+ }