@zhangferry-dev/tokendash 1.1.3 → 1.2.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 +3 -3
- package/dist/client/assets/index-92lvfG3S.css +1 -0
- package/dist/client/assets/index-DJOsmILU.js +121 -0
- package/dist/client/index.html +2 -2
- package/dist/server/agentDetection.d.ts +6 -0
- package/dist/server/agentDetection.js +25 -0
- package/dist/server/analyticsParser.d.ts +13 -0
- package/dist/server/analyticsParser.js +277 -0
- package/dist/server/cache.d.ts +5 -0
- package/dist/server/cache.js +54 -10
- package/dist/server/claudeJsonlParser.d.ts +9 -0
- package/dist/server/claudeJsonlParser.js +314 -0
- package/dist/server/index.js +3 -3
- package/dist/server/openclawParser.js +50 -31
- package/dist/server/routes/analytics.d.ts +2 -0
- package/dist/server/routes/analytics.js +40 -0
- package/dist/server/routes/api.js +8 -3
- package/dist/server/routes/blocks.js +32 -37
- package/dist/server/routes/daily.js +29 -13
- package/dist/server/routes/monthly.d.ts +1 -1
- package/dist/server/routes/monthly.js +26 -9
- package/dist/server/routes/projects.js +29 -13
- package/dist/server/routes/session.d.ts +1 -1
- package/dist/server/routes/session.js +26 -9
- package/dist/shared/schemas.d.ts +53 -31
- package/dist/shared/schemas.js +29 -0
- package/dist/shared/types.d.ts +29 -0
- package/package.json +8 -3
- package/dist/client/assets/index-C3o5PaD5.js +0 -121
- package/dist/client/assets/index-CaxfZubD.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TokenDash</title>
|
|
7
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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DJOsmILU.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-92lvfG3S.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="antialiased" style="background:#faf9f7">
|
|
12
12
|
<div id="root"></div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
5
|
+
const CODEX_SESSIONS_DIR = join(homedir(), '.codex', 'sessions');
|
|
6
|
+
export function isClaudeCodeAvailable() {
|
|
7
|
+
if (!existsSync(CLAUDE_PROJECTS_DIR))
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
const dirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
|
|
11
|
+
return dirs.some(d => d.isDirectory());
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isCodexAvailable() {
|
|
18
|
+
return existsSync(CODEX_SESSIONS_DIR);
|
|
19
|
+
}
|
|
20
|
+
export function detectAvailableAgents() {
|
|
21
|
+
return {
|
|
22
|
+
claude: isClaudeCodeAvailable(),
|
|
23
|
+
codex: isCodexAvailable(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnalyticsResponse } from '../shared/types.js';
|
|
2
|
+
interface ToolCallRecord {
|
|
3
|
+
toolName: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
filePath?: string;
|
|
6
|
+
linesAdded: number;
|
|
7
|
+
linesDeleted: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function normalizeToolName(name: string): string;
|
|
10
|
+
export declare function extractClaudeToolCalls(project?: string | null): ToolCallRecord[];
|
|
11
|
+
export declare function extractOpenClawToolCalls(project?: string | null): ToolCallRecord[];
|
|
12
|
+
export declare function computeAnalytics(toolCalls: ToolCallRecord[], timezone?: string): AnalyticsResponse;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { scanOpenClawSessions } from './openclawParser.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Timezone helpers (same as other parsers)
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const TZ_OFFSETS = {
|
|
9
|
+
'Asia/Shanghai': 8,
|
|
10
|
+
'Asia/Tokyo': 9,
|
|
11
|
+
'America/New_York': -5,
|
|
12
|
+
'America/Los_Angeles': -8,
|
|
13
|
+
'Europe/London': 0,
|
|
14
|
+
'UTC': 0,
|
|
15
|
+
};
|
|
16
|
+
function getDateKey(ms, tz) {
|
|
17
|
+
const offset = (TZ_OFFSETS[tz] ?? 8) * 3_600_000;
|
|
18
|
+
const d = new Date(ms + offset);
|
|
19
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Tool name normalization
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export function normalizeToolName(name) {
|
|
25
|
+
const lower = name.toLowerCase();
|
|
26
|
+
if (lower.startsWith('mcp__')) {
|
|
27
|
+
const parts = name.split('__');
|
|
28
|
+
const serverPart = parts.length >= 3 ? parts[2] : 'mcp';
|
|
29
|
+
return `MCP:${serverPart}`;
|
|
30
|
+
}
|
|
31
|
+
const mapping = {
|
|
32
|
+
'exec': 'Bash',
|
|
33
|
+
'read': 'Read',
|
|
34
|
+
'edit': 'Edit',
|
|
35
|
+
'write': 'Write',
|
|
36
|
+
};
|
|
37
|
+
return mapping[lower] || name;
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Line counting
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function countLines(text) {
|
|
43
|
+
if (!text)
|
|
44
|
+
return 0;
|
|
45
|
+
return text.split('\n').length;
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Claude Code session scanning & tool extraction
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
51
|
+
function extractProjectName(dirName) {
|
|
52
|
+
const parts = dirName.replace(/^-/, '').split('-');
|
|
53
|
+
return parts[parts.length - 1] || dirName;
|
|
54
|
+
}
|
|
55
|
+
function matchesProject(dirName, filter) {
|
|
56
|
+
return extractProjectName(dirName) === extractProjectName(filter);
|
|
57
|
+
}
|
|
58
|
+
// Session-level cache (mtime-based)
|
|
59
|
+
const claudeSessionCache = new Map();
|
|
60
|
+
export function extractClaudeToolCalls(project) {
|
|
61
|
+
if (!existsSync(CLAUDE_PROJECTS_DIR))
|
|
62
|
+
return [];
|
|
63
|
+
const results = [];
|
|
64
|
+
const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true })
|
|
65
|
+
.filter(d => d.isDirectory())
|
|
66
|
+
.map(d => d.name);
|
|
67
|
+
for (const dirName of projectDirs) {
|
|
68
|
+
if (project && !matchesProject(dirName, project))
|
|
69
|
+
continue;
|
|
70
|
+
const dirPath = join(CLAUDE_PROJECTS_DIR, dirName);
|
|
71
|
+
let files;
|
|
72
|
+
try {
|
|
73
|
+
files = readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const filePath = join(dirPath, file);
|
|
80
|
+
let mtime = 0;
|
|
81
|
+
try {
|
|
82
|
+
mtime = statSync(filePath).mtimeMs;
|
|
83
|
+
}
|
|
84
|
+
catch { /* ok */ }
|
|
85
|
+
const cached = claudeSessionCache.get(filePath);
|
|
86
|
+
if (cached && cached.mtime === mtime) {
|
|
87
|
+
results.push(...cached.toolCalls);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const toolCalls = [];
|
|
91
|
+
let content;
|
|
92
|
+
try {
|
|
93
|
+
content = readFileSync(filePath, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const line of content.split('\n')) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (!trimmed)
|
|
101
|
+
continue;
|
|
102
|
+
let obj;
|
|
103
|
+
try {
|
|
104
|
+
obj = JSON.parse(trimmed);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (obj.type !== 'assistant' || !obj.message)
|
|
110
|
+
continue;
|
|
111
|
+
const msg = obj.message;
|
|
112
|
+
const timestamp = new Date(obj.timestamp).getTime();
|
|
113
|
+
const content_arr = msg.content;
|
|
114
|
+
if (!content_arr)
|
|
115
|
+
continue;
|
|
116
|
+
for (const item of content_arr) {
|
|
117
|
+
if (item.type !== 'tool_use')
|
|
118
|
+
continue;
|
|
119
|
+
const toolName = normalizeToolName(item.name);
|
|
120
|
+
const input = item.input || {};
|
|
121
|
+
let linesAdded = 0;
|
|
122
|
+
let linesDeleted = 0;
|
|
123
|
+
const filePath2 = input.file_path || undefined;
|
|
124
|
+
if (toolName === 'Edit') {
|
|
125
|
+
linesDeleted = countLines(input.old_string || '');
|
|
126
|
+
linesAdded = countLines(input.new_string || '');
|
|
127
|
+
}
|
|
128
|
+
else if (toolName === 'Write') {
|
|
129
|
+
linesAdded = countLines(input.content || '');
|
|
130
|
+
}
|
|
131
|
+
toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
claudeSessionCache.set(filePath, { mtime, toolCalls });
|
|
135
|
+
results.push(...toolCalls);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// OpenClaw tool extraction
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
const openclawSessionCache = new Map();
|
|
144
|
+
export function extractOpenClawToolCalls(project) {
|
|
145
|
+
const results = [];
|
|
146
|
+
const refs = scanOpenClawSessions();
|
|
147
|
+
for (const ref of refs) {
|
|
148
|
+
if (project && ref.agentId !== project)
|
|
149
|
+
continue;
|
|
150
|
+
let mtime = 0;
|
|
151
|
+
try {
|
|
152
|
+
mtime = statSync(ref.sessionFile).mtimeMs;
|
|
153
|
+
}
|
|
154
|
+
catch { /* ok */ }
|
|
155
|
+
const cached = openclawSessionCache.get(ref.sessionFile);
|
|
156
|
+
if (cached && cached.mtime === mtime) {
|
|
157
|
+
results.push(...cached.toolCalls);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const toolCalls = [];
|
|
161
|
+
let content;
|
|
162
|
+
try {
|
|
163
|
+
content = readFileSync(ref.sessionFile, 'utf-8');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const line of content.split('\n')) {
|
|
169
|
+
const trimmed = line.trim();
|
|
170
|
+
if (!trimmed)
|
|
171
|
+
continue;
|
|
172
|
+
let obj;
|
|
173
|
+
try {
|
|
174
|
+
obj = JSON.parse(trimmed);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (obj.type !== 'message')
|
|
180
|
+
continue;
|
|
181
|
+
const msg = obj.message;
|
|
182
|
+
if (msg.role !== 'assistant')
|
|
183
|
+
continue;
|
|
184
|
+
const timestamp = Number(msg.timestamp ?? 0);
|
|
185
|
+
const content_arr = msg.content;
|
|
186
|
+
if (!content_arr)
|
|
187
|
+
continue;
|
|
188
|
+
for (const item of content_arr) {
|
|
189
|
+
if (item.type !== 'toolCall')
|
|
190
|
+
continue;
|
|
191
|
+
const toolName = normalizeToolName(item.name);
|
|
192
|
+
const args = item.arguments || {};
|
|
193
|
+
let linesAdded = 0;
|
|
194
|
+
let linesDeleted = 0;
|
|
195
|
+
const filePath2 = args.path || undefined;
|
|
196
|
+
if (toolName === 'Edit') {
|
|
197
|
+
linesDeleted = countLines(args.oldText || '');
|
|
198
|
+
linesAdded = countLines(args.newText || '');
|
|
199
|
+
}
|
|
200
|
+
else if (toolName === 'Write') {
|
|
201
|
+
linesAdded = countLines(args.content || '');
|
|
202
|
+
}
|
|
203
|
+
toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
openclawSessionCache.set(ref.sessionFile, { mtime, toolCalls });
|
|
207
|
+
results.push(...toolCalls);
|
|
208
|
+
}
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Analytics computation
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
export function computeAnalytics(toolCalls, timezone = 'Asia/Shanghai') {
|
|
215
|
+
// 1. Code Change Trend — group edit/write calls by date
|
|
216
|
+
const changeMap = new Map();
|
|
217
|
+
for (const tc of toolCalls) {
|
|
218
|
+
if (tc.linesAdded === 0 && tc.linesDeleted === 0)
|
|
219
|
+
continue;
|
|
220
|
+
const key = getDateKey(tc.timestamp, timezone);
|
|
221
|
+
if (!changeMap.has(key))
|
|
222
|
+
changeMap.set(key, { added: 0, deleted: 0, files: new Set() });
|
|
223
|
+
const entry = changeMap.get(key);
|
|
224
|
+
entry.added += tc.linesAdded;
|
|
225
|
+
entry.deleted += tc.linesDeleted;
|
|
226
|
+
if (tc.filePath)
|
|
227
|
+
entry.files.add(tc.filePath);
|
|
228
|
+
}
|
|
229
|
+
const codeChangeTrend = [];
|
|
230
|
+
for (const [date, { added, deleted, files }] of changeMap) {
|
|
231
|
+
codeChangeTrend.push({ date, linesAdded: added, linesDeleted: deleted, netChange: added - deleted, filesModified: files.size });
|
|
232
|
+
}
|
|
233
|
+
codeChangeTrend.sort((a, b) => a.date.localeCompare(b.date));
|
|
234
|
+
// 2. Tool Usage Distribution — count per tool
|
|
235
|
+
const toolCountMap = new Map();
|
|
236
|
+
for (const tc of toolCalls) {
|
|
237
|
+
toolCountMap.set(tc.toolName, (toolCountMap.get(tc.toolName) || 0) + 1);
|
|
238
|
+
}
|
|
239
|
+
const toolUsageDistribution = [...toolCountMap.entries()]
|
|
240
|
+
.map(([name, count]) => ({ name, count }))
|
|
241
|
+
.sort((a, b) => b.count - a.count);
|
|
242
|
+
// 3. Productivity KPIs
|
|
243
|
+
const editCalls = toolCalls.filter(tc => tc.toolName === 'Edit' || tc.toolName === 'Write');
|
|
244
|
+
const totalEdits = editCalls.length;
|
|
245
|
+
const totalLinesChanged = editCalls.reduce((s, tc) => s + tc.linesAdded + tc.linesDeleted, 0);
|
|
246
|
+
const totalLinesAdded = editCalls.reduce((s, tc) => s + tc.linesAdded, 0);
|
|
247
|
+
const totalLinesDeleted = editCalls.reduce((s, tc) => s + tc.linesDeleted, 0);
|
|
248
|
+
const uniqueFiles = new Set(editCalls.filter(tc => tc.filePath).map(tc => tc.filePath));
|
|
249
|
+
const editDates = new Set(editCalls.map(tc => getDateKey(tc.timestamp, timezone)));
|
|
250
|
+
const productivityKPIs = {
|
|
251
|
+
avgLinesPerEdit: totalEdits > 0 ? Math.round(totalLinesChanged / totalEdits) : 0,
|
|
252
|
+
filesModifiedPerDay: editDates.size > 0 ? Math.round(uniqueFiles.size / editDates.size) : 0,
|
|
253
|
+
addDeleteRatio: totalLinesDeleted > 0 ? Math.round((totalLinesAdded / totalLinesDeleted) * 100) / 100 : totalLinesAdded > 0 ? 1 : 0,
|
|
254
|
+
totalEdits,
|
|
255
|
+
totalFilesModified: uniqueFiles.size,
|
|
256
|
+
activeDaysWithEdits: editDates.size,
|
|
257
|
+
};
|
|
258
|
+
// 4. Tool Call Trend — group all calls by (date, toolName)
|
|
259
|
+
const trendMap = new Map();
|
|
260
|
+
for (const tc of toolCalls) {
|
|
261
|
+
const date = getDateKey(tc.timestamp, timezone);
|
|
262
|
+
if (!trendMap.has(date))
|
|
263
|
+
trendMap.set(date, new Map());
|
|
264
|
+
const dayMap = trendMap.get(date);
|
|
265
|
+
dayMap.set(tc.toolName, (dayMap.get(tc.toolName) || 0) + 1);
|
|
266
|
+
}
|
|
267
|
+
const toolCallTrend = [];
|
|
268
|
+
for (const [date, dayMap] of trendMap) {
|
|
269
|
+
const entry = { date };
|
|
270
|
+
for (const [tool, count] of dayMap) {
|
|
271
|
+
entry[tool] = count;
|
|
272
|
+
}
|
|
273
|
+
toolCallTrend.push(entry);
|
|
274
|
+
}
|
|
275
|
+
toolCallTrend.sort((a, b) => a.date.localeCompare(b.date));
|
|
276
|
+
return { codeChangeTrend, toolUsageDistribution, productivityKPIs, toolCallTrend };
|
|
277
|
+
}
|
package/dist/server/cache.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
interface CacheEntry<T> {
|
|
2
2
|
data: T;
|
|
3
3
|
expiresAt: number;
|
|
4
|
+
updatedAt: number;
|
|
4
5
|
}
|
|
5
6
|
declare class Cache {
|
|
6
7
|
private store;
|
|
7
8
|
get<T>(key: string): T | null;
|
|
9
|
+
/** Get data even if stale (for stale-while-revalidate) */
|
|
10
|
+
getStale<T>(key: string): T | null;
|
|
8
11
|
set<T>(key: string, data: T, ttl?: number): void;
|
|
9
12
|
clear(): void;
|
|
10
13
|
delete(key: string): boolean;
|
|
11
14
|
has(key: string): boolean;
|
|
15
|
+
private writeToDisk;
|
|
16
|
+
private readFromDisk;
|
|
12
17
|
}
|
|
13
18
|
export declare const cache: Cache;
|
|
14
19
|
export type { CacheEntry };
|
package/dist/server/cache.js
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes (fresh)
|
|
5
|
+
const DISK_TTL = 60 * 60 * 1000; // 1 hour (stale but usable)
|
|
6
|
+
const CACHE_DIR = join(tmpdir(), 'tokendash-cache');
|
|
7
|
+
function diskPath(key) {
|
|
8
|
+
const safe = key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
9
|
+
return join(CACHE_DIR, `${safe}.json`);
|
|
10
|
+
}
|
|
2
11
|
class Cache {
|
|
3
12
|
store = new Map();
|
|
4
13
|
get(key) {
|
|
5
14
|
const entry = this.store.get(key);
|
|
6
|
-
if (
|
|
7
|
-
return
|
|
8
|
-
}
|
|
9
|
-
if (Date.now() > entry.expiresAt) {
|
|
10
|
-
this.store.delete(key);
|
|
11
|
-
return null;
|
|
15
|
+
if (entry && Date.now() <= entry.expiresAt) {
|
|
16
|
+
return entry.data;
|
|
12
17
|
}
|
|
13
|
-
return
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/** Get data even if stale (for stale-while-revalidate) */
|
|
21
|
+
getStale(key) {
|
|
22
|
+
// Try memory first
|
|
23
|
+
const entry = this.store.get(key);
|
|
24
|
+
if (entry)
|
|
25
|
+
return entry.data;
|
|
26
|
+
// Try disk
|
|
27
|
+
return this.readFromDisk(key);
|
|
14
28
|
}
|
|
15
29
|
set(key, data, ttl = DEFAULT_TTL) {
|
|
16
30
|
const entry = {
|
|
17
31
|
data,
|
|
18
32
|
expiresAt: Date.now() + ttl,
|
|
33
|
+
updatedAt: Date.now(),
|
|
19
34
|
};
|
|
20
35
|
this.store.set(key, entry);
|
|
36
|
+
this.writeToDisk(key, entry);
|
|
21
37
|
}
|
|
22
38
|
clear() {
|
|
23
39
|
this.store.clear();
|
|
@@ -27,14 +43,42 @@ class Cache {
|
|
|
27
43
|
}
|
|
28
44
|
has(key) {
|
|
29
45
|
const entry = this.store.get(key);
|
|
30
|
-
if (!entry)
|
|
46
|
+
if (!entry)
|
|
31
47
|
return false;
|
|
32
|
-
}
|
|
33
48
|
if (Date.now() > entry.expiresAt) {
|
|
34
49
|
this.store.delete(key);
|
|
35
50
|
return false;
|
|
36
51
|
}
|
|
37
52
|
return true;
|
|
38
53
|
}
|
|
54
|
+
writeToDisk(key, entry) {
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(CACHE_DIR))
|
|
57
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
58
|
+
writeFileSync(diskPath(key), JSON.stringify(entry), 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Disk cache is best-effort
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
readFromDisk(key) {
|
|
65
|
+
try {
|
|
66
|
+
const path = diskPath(key);
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return null;
|
|
69
|
+
const raw = readFileSync(path, 'utf-8');
|
|
70
|
+
const entry = JSON.parse(raw);
|
|
71
|
+
// Only use disk cache if less than DISK_TTL old
|
|
72
|
+
if (Date.now() - entry.updatedAt < DISK_TTL) {
|
|
73
|
+
// Promote to memory cache (with 0 TTL so it'll be treated as stale)
|
|
74
|
+
this.store.set(key, { ...entry, expiresAt: 0 });
|
|
75
|
+
return entry.data;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Disk cache is best-effort
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
39
83
|
}
|
|
40
84
|
export const cache = new Cache();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DailyResponse, ProjectsResponse, BlockEntry } from '../shared/types.js';
|
|
2
|
+
export declare function calculateCost(inputTokens: number, cacheReadTokens: number, outputTokens: number, model: string): number;
|
|
3
|
+
export declare function getDateKey(timestamp: string, tz: string): string;
|
|
4
|
+
export declare function getHourKey(timestamp: string, tz: string): string;
|
|
5
|
+
export declare function getDailyResponse(project?: string | null, tz?: string): DailyResponse;
|
|
6
|
+
export declare function getProjectsResponse(tz?: string): ProjectsResponse;
|
|
7
|
+
export declare function getBlocksResponse(project?: string | null, tz?: string): {
|
|
8
|
+
blocks: BlockEntry[];
|
|
9
|
+
};
|