@mbeato/contextscope 0.1.3 → 0.1.5
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/context/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/items/page.js +3 -2
- package/.next/standalone/.next/server/app/items/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/items/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +3 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page.js +3 -2
- package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08l_yt3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0j40w4k._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lbywin._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p2sxww._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0wj0exw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0j0avc7._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/app/page.tsx +8 -2
- package/.next/standalone/app/sessions/page.tsx +10 -2
- package/.next/standalone/lib/model-prices.json +147 -0
- package/.next/standalone/lib/pricing.ts +80 -0
- package/.next/standalone/lib/sessions.ts +33 -6
- package/.next/standalone/lib/transcripts.ts +218 -54
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/scripts/refresh-prices.mjs +58 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/.next/static/chunks/118uk9v3812u1.css +1 -0
- package/package.json +2 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08jw6yr._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0duau-w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ubqc9u._.js +0 -3
- package/.next/static/chunks/13525lf.7uo9s.css +0 -1
- /package/.next/static/{BT6H4g8OlEYi9snYU0PI- → I-W1iV4XdkTRyjis8Ros1}/_buildManifest.js +0 -0
- /package/.next/static/{BT6H4g8OlEYi9snYU0PI- → I-W1iV4XdkTRyjis8Ros1}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{BT6H4g8OlEYi9snYU0PI- → I-W1iV4XdkTRyjis8Ros1}/_ssgManifest.js +0 -0
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* - invocations (Skill / Agent tool_use events) -> consumed by lib/usage.ts
|
|
7
7
|
* - session stats (turn count, token usage) -> consumed by lib/sessions.ts
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* CC creates a new JSONL when you `--continue` a session, which replays the
|
|
10
|
+
* prior turns. To match ccusage's totals we dedup messages by `msg.id:requestId`
|
|
11
|
+
* across all files at aggregate time. Per-file records are cached unchanged
|
|
12
|
+
* (preserves mtime cache); dedupAndSum runs on every getAllTranscripts call.
|
|
10
13
|
*/
|
|
11
14
|
import { readdir, stat } from "node:fs/promises";
|
|
12
15
|
import { createReadStream } from "node:fs";
|
|
@@ -18,6 +21,28 @@ const PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
|
18
21
|
|
|
19
22
|
export type Invocation = { kind: "skill" | "agent"; name: string; ts: number };
|
|
20
23
|
|
|
24
|
+
export type ModelUsage = {
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
cacheReadTokens: number;
|
|
27
|
+
cacheCreation5mTokens: number;
|
|
28
|
+
cacheCreation1hTokens: number;
|
|
29
|
+
outputTokens: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type UsageRecord = {
|
|
33
|
+
// `${message.id}:${requestId}` — same key ccusage uses. Empty string if msg
|
|
34
|
+
// lacked an id (treat as un-dedupable; count it).
|
|
35
|
+
dedupKey: string;
|
|
36
|
+
model: string;
|
|
37
|
+
ts: number;
|
|
38
|
+
input: number;
|
|
39
|
+
cacheRead: number;
|
|
40
|
+
cacheCreation5m: number;
|
|
41
|
+
cacheCreation1h: number;
|
|
42
|
+
output: number;
|
|
43
|
+
isSidechain: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
21
46
|
export type TranscriptResult = {
|
|
22
47
|
filePath: string;
|
|
23
48
|
mtimeMs: number;
|
|
@@ -28,20 +53,22 @@ export type TranscriptResult = {
|
|
|
28
53
|
endTime: number;
|
|
29
54
|
turnCount: number;
|
|
30
55
|
models: string[];
|
|
56
|
+
// Aggregate totals — sums after dedup (when produced via getAllTranscripts).
|
|
57
|
+
// On raw parseFile output these are pre-dedup; getAllTranscripts rebuilds them.
|
|
31
58
|
inputTokens: number;
|
|
32
59
|
cacheReadTokens: number;
|
|
33
|
-
cacheCreationTokens: number;
|
|
60
|
+
cacheCreationTokens: number; // total: 5m + 1h
|
|
34
61
|
outputTokens: number;
|
|
62
|
+
byModel: Record<string, ModelUsage>;
|
|
63
|
+
// Per-message records — kept on the cached result so dedup at aggregate time
|
|
64
|
+
// is exact across resumed sessions.
|
|
65
|
+
usageRecords: UsageRecord[];
|
|
35
66
|
invocations: Invocation[];
|
|
36
|
-
toolCalls: Record<string, number>;
|
|
37
|
-
toolErrors: number;
|
|
38
|
-
sidechainTurns: number;
|
|
67
|
+
toolCalls: Record<string, number>;
|
|
68
|
+
toolErrors: number;
|
|
69
|
+
sidechainTurns: number;
|
|
39
70
|
};
|
|
40
71
|
|
|
41
|
-
// Module-level cache: persists across server-component renders within the same
|
|
42
|
-
// Next.js dev/prod process. Keyed by filePath; invalidated on mtime change.
|
|
43
|
-
// Capped via simple FIFO eviction (Map preserves insertion order) so a
|
|
44
|
-
// long-running process scanning thousands of transcripts can't grow unbounded.
|
|
45
72
|
const cache = new Map<string, TranscriptResult>();
|
|
46
73
|
const MAX_CACHE_ENTRIES = 5000;
|
|
47
74
|
|
|
@@ -51,8 +78,18 @@ function inferProjectPath(dirName: string): string {
|
|
|
51
78
|
|
|
52
79
|
async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptResult> {
|
|
53
80
|
const parts = filePath.split("/");
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
// Two shapes:
|
|
82
|
+
// <project>/<session-uuid>.jsonl → main session file
|
|
83
|
+
// <project>/<session-uuid>/subagents/agent-<id>.jsonl → subagent file
|
|
84
|
+
// For subagents we want the parent session UUID + parent project, so the
|
|
85
|
+
// tokens roll up to the same Session as the main file.
|
|
86
|
+
const isSubagent = parts[parts.length - 2] === "subagents";
|
|
87
|
+
const sessionId = isSubagent
|
|
88
|
+
? (parts[parts.length - 3] ?? "")
|
|
89
|
+
: parts[parts.length - 1].replace(/\.jsonl$/, "");
|
|
90
|
+
const project = isSubagent
|
|
91
|
+
? (parts[parts.length - 4] ?? "")
|
|
92
|
+
: (parts[parts.length - 2] ?? "");
|
|
56
93
|
const result: TranscriptResult = {
|
|
57
94
|
filePath,
|
|
58
95
|
mtimeMs,
|
|
@@ -67,12 +104,13 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
67
104
|
cacheReadTokens: 0,
|
|
68
105
|
cacheCreationTokens: 0,
|
|
69
106
|
outputTokens: 0,
|
|
107
|
+
byModel: {},
|
|
108
|
+
usageRecords: [],
|
|
70
109
|
invocations: [],
|
|
71
110
|
toolCalls: {},
|
|
72
111
|
toolErrors: 0,
|
|
73
112
|
sidechainTurns: 0,
|
|
74
113
|
};
|
|
75
|
-
const modelSet = new Set<string>();
|
|
76
114
|
|
|
77
115
|
return new Promise((resolve) => {
|
|
78
116
|
const rl = createInterface({
|
|
@@ -81,7 +119,6 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
81
119
|
});
|
|
82
120
|
rl.on("line", (line) => {
|
|
83
121
|
if (!line || line[0] !== "{") return;
|
|
84
|
-
// Prefilter: usage events, tool_use events, and tool_result events (for errors).
|
|
85
122
|
const hasUsage = line.includes('"usage"');
|
|
86
123
|
const hasToolUse = line.includes('"tool_use"');
|
|
87
124
|
const hasToolResult = line.includes('"tool_result"');
|
|
@@ -94,29 +131,49 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
94
131
|
return;
|
|
95
132
|
}
|
|
96
133
|
const msg = rec.message as
|
|
97
|
-
| { model?: string; usage?: Record<string, unknown>; content?: unknown }
|
|
134
|
+
| { id?: string; model?: string; usage?: Record<string, unknown>; content?: unknown }
|
|
98
135
|
| undefined;
|
|
99
136
|
const tsRaw = rec.timestamp;
|
|
100
137
|
const tsMs = typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
101
138
|
const isSidechain = rec.isSidechain === true;
|
|
102
139
|
|
|
103
|
-
// Usage aggregation (assistant messages)
|
|
104
140
|
const usage = msg?.usage;
|
|
105
141
|
if (usage) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
const msgId = typeof msg?.id === "string" ? msg.id : "";
|
|
143
|
+
const requestId = typeof rec.requestId === "string" ? rec.requestId : "";
|
|
144
|
+
const dedupKey = msgId ? `${msgId}:${requestId}` : "";
|
|
145
|
+
// cache_creation may have an ephemeral_{5m,1h}_input_tokens breakdown
|
|
146
|
+
// (priced separately at $6.25/M vs $10/M for opus-4-7). Older transcripts
|
|
147
|
+
// lack the sub-object — fall back to treating the total as 5min.
|
|
148
|
+
const ccTotal = Number(usage.cache_creation_input_tokens) || 0;
|
|
149
|
+
const ccBreakdown = (usage.cache_creation ?? null) as
|
|
150
|
+
| { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number }
|
|
151
|
+
| null;
|
|
152
|
+
let cc5m = 0;
|
|
153
|
+
let cc1h = 0;
|
|
154
|
+
if (ccBreakdown && typeof ccBreakdown === "object") {
|
|
155
|
+
cc5m = Number(ccBreakdown.ephemeral_5m_input_tokens) || 0;
|
|
156
|
+
cc1h = Number(ccBreakdown.ephemeral_1h_input_tokens) || 0;
|
|
157
|
+
// If breakdown is present but sum doesn't match the parent total,
|
|
158
|
+
// trust the parent and attribute the diff to 5min (conservative).
|
|
159
|
+
const diff = ccTotal - (cc5m + cc1h);
|
|
160
|
+
if (diff > 0) cc5m += diff;
|
|
161
|
+
} else {
|
|
162
|
+
cc5m = ccTotal;
|
|
116
163
|
}
|
|
164
|
+
result.usageRecords.push({
|
|
165
|
+
dedupKey,
|
|
166
|
+
model: msg?.model || "<synthetic>",
|
|
167
|
+
ts: Number.isFinite(tsMs) ? tsMs : 0,
|
|
168
|
+
input: Number(usage.input_tokens) || 0,
|
|
169
|
+
cacheRead: Number(usage.cache_read_input_tokens) || 0,
|
|
170
|
+
cacheCreation5m: cc5m,
|
|
171
|
+
cacheCreation1h: cc1h,
|
|
172
|
+
output: Number(usage.output_tokens) || 0,
|
|
173
|
+
isSidechain,
|
|
174
|
+
});
|
|
117
175
|
}
|
|
118
176
|
|
|
119
|
-
// Content scan: tool_use (counts + invocations) and tool_result (errors)
|
|
120
177
|
if (Array.isArray(msg?.content)) {
|
|
121
178
|
for (const c of msg.content) {
|
|
122
179
|
if (!c || typeof c !== "object") continue;
|
|
@@ -146,14 +203,8 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
146
203
|
}
|
|
147
204
|
}
|
|
148
205
|
});
|
|
149
|
-
rl.on("close", () =>
|
|
150
|
-
|
|
151
|
-
resolve(result);
|
|
152
|
-
});
|
|
153
|
-
rl.on("error", () => {
|
|
154
|
-
result.models = [...modelSet];
|
|
155
|
-
resolve(result);
|
|
156
|
-
});
|
|
206
|
+
rl.on("close", () => resolve(result));
|
|
207
|
+
rl.on("error", () => resolve(result));
|
|
157
208
|
});
|
|
158
209
|
}
|
|
159
210
|
|
|
@@ -161,7 +212,7 @@ async function getFileResult(filePath: string, mtimeMs: number): Promise<Transcr
|
|
|
161
212
|
const cached = cache.get(filePath);
|
|
162
213
|
if (cached && cached.mtimeMs === mtimeMs) return cached;
|
|
163
214
|
const fresh = await parseFile(filePath, mtimeMs);
|
|
164
|
-
if (cached) cache.delete(filePath);
|
|
215
|
+
if (cached) cache.delete(filePath);
|
|
165
216
|
cache.set(filePath, fresh);
|
|
166
217
|
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
167
218
|
const oldest = cache.keys().next().value;
|
|
@@ -185,7 +236,135 @@ async function pMapLimit<T, R>(items: T[], limit: number, fn: (x: T) => Promise<
|
|
|
185
236
|
return out;
|
|
186
237
|
}
|
|
187
238
|
|
|
188
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Walk raw per-file results oldest first, dedup messages globally by
|
|
241
|
+
* (msg.id, requestId), and merge multiple files belonging to the same logical
|
|
242
|
+
* session (main + subagents/*) into one TranscriptResult per session.
|
|
243
|
+
*
|
|
244
|
+
* Important: do NOT mutate the cached `raws` objects — they're shared across
|
|
245
|
+
* calls.
|
|
246
|
+
*/
|
|
247
|
+
function dedupAndSum(raws: TranscriptResult[]): TranscriptResult[] {
|
|
248
|
+
const sorted = [...raws].sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
249
|
+
const seen = new Set<string>();
|
|
250
|
+
const merged = new Map<string, TranscriptResult>();
|
|
251
|
+
|
|
252
|
+
for (const r of sorted) {
|
|
253
|
+
const key = `${r.project}\x00${r.sessionId}`;
|
|
254
|
+
let t = merged.get(key);
|
|
255
|
+
if (!t) {
|
|
256
|
+
t = {
|
|
257
|
+
filePath: r.filePath,
|
|
258
|
+
mtimeMs: r.mtimeMs,
|
|
259
|
+
sessionId: r.sessionId,
|
|
260
|
+
project: r.project,
|
|
261
|
+
projectPath: r.projectPath,
|
|
262
|
+
startTime: 0,
|
|
263
|
+
endTime: 0,
|
|
264
|
+
turnCount: 0,
|
|
265
|
+
models: [],
|
|
266
|
+
inputTokens: 0,
|
|
267
|
+
cacheReadTokens: 0,
|
|
268
|
+
cacheCreationTokens: 0,
|
|
269
|
+
outputTokens: 0,
|
|
270
|
+
byModel: {},
|
|
271
|
+
usageRecords: [],
|
|
272
|
+
invocations: [],
|
|
273
|
+
toolCalls: {},
|
|
274
|
+
toolErrors: 0,
|
|
275
|
+
sidechainTurns: 0,
|
|
276
|
+
};
|
|
277
|
+
merged.set(key, t);
|
|
278
|
+
} else {
|
|
279
|
+
// Prefer the main session file's path/mtime as the canonical reference;
|
|
280
|
+
// a subagent file landed first only if no main yet, which is rare.
|
|
281
|
+
const incomingIsMain = !r.filePath.includes("/subagents/");
|
|
282
|
+
if (incomingIsMain) {
|
|
283
|
+
t.filePath = r.filePath;
|
|
284
|
+
t.mtimeMs = Math.max(t.mtimeMs, r.mtimeMs);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Accumulate ancillary fields
|
|
289
|
+
for (const inv of r.invocations) t.invocations.push(inv);
|
|
290
|
+
for (const [name, n] of Object.entries(r.toolCalls)) {
|
|
291
|
+
t.toolCalls[name] = (t.toolCalls[name] ?? 0) + n;
|
|
292
|
+
}
|
|
293
|
+
t.toolErrors += r.toolErrors;
|
|
294
|
+
|
|
295
|
+
const modelSet = new Set<string>(t.models);
|
|
296
|
+
for (const u of r.usageRecords) {
|
|
297
|
+
if (u.dedupKey) {
|
|
298
|
+
if (seen.has(u.dedupKey)) continue;
|
|
299
|
+
seen.add(u.dedupKey);
|
|
300
|
+
}
|
|
301
|
+
t.inputTokens += u.input;
|
|
302
|
+
t.cacheReadTokens += u.cacheRead;
|
|
303
|
+
t.cacheCreationTokens += u.cacheCreation5m + u.cacheCreation1h;
|
|
304
|
+
t.outputTokens += u.output;
|
|
305
|
+
t.turnCount += 1;
|
|
306
|
+
if (u.isSidechain) t.sidechainTurns += 1;
|
|
307
|
+
modelSet.add(u.model);
|
|
308
|
+
const bm = t.byModel[u.model] ?? {
|
|
309
|
+
inputTokens: 0,
|
|
310
|
+
cacheReadTokens: 0,
|
|
311
|
+
cacheCreation5mTokens: 0,
|
|
312
|
+
cacheCreation1hTokens: 0,
|
|
313
|
+
outputTokens: 0,
|
|
314
|
+
};
|
|
315
|
+
bm.inputTokens += u.input;
|
|
316
|
+
bm.cacheReadTokens += u.cacheRead;
|
|
317
|
+
bm.cacheCreation5mTokens += u.cacheCreation5m;
|
|
318
|
+
bm.cacheCreation1hTokens += u.cacheCreation1h;
|
|
319
|
+
bm.outputTokens += u.output;
|
|
320
|
+
t.byModel[u.model] = bm;
|
|
321
|
+
if (u.ts > 0) {
|
|
322
|
+
if (!t.startTime || u.ts < t.startTime) t.startTime = u.ts;
|
|
323
|
+
if (u.ts > t.endTime) t.endTime = u.ts;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
t.models = [...modelSet];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return [...merged.values()];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function collectJsonlFiles(
|
|
333
|
+
dir: string,
|
|
334
|
+
cutoff: number,
|
|
335
|
+
out: { filePath: string; mtimeMs: number }[],
|
|
336
|
+
depth: number = 0
|
|
337
|
+
): Promise<void> {
|
|
338
|
+
if (depth > 3) return; // <project>/<session>/subagents/<file> is the deepest expected shape
|
|
339
|
+
let entries;
|
|
340
|
+
try {
|
|
341
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
342
|
+
} catch {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
for (const e of entries) {
|
|
346
|
+
const fp = join(dir, e.name);
|
|
347
|
+
if (e.isDirectory()) {
|
|
348
|
+
await collectJsonlFiles(fp, cutoff, out, depth + 1);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (!e.isFile() || !e.name.endsWith(".jsonl")) continue;
|
|
352
|
+
try {
|
|
353
|
+
const st = await stat(fp);
|
|
354
|
+
if (st.mtimeMs >= cutoff) out.push({ filePath: fp, mtimeMs: st.mtimeMs });
|
|
355
|
+
} catch {
|
|
356
|
+
// skip
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Scan all transcripts modified in the last N days. Scans both main session
|
|
363
|
+
* files (<project>/<session>.jsonl) and subagent files
|
|
364
|
+
* (<project>/<session>/subagents/agent-*.jsonl), then attributes subagent
|
|
365
|
+
* tokens to their parent session via shared sessionId. Deduped globally by
|
|
366
|
+
* (msg.id, requestId).
|
|
367
|
+
*/
|
|
189
368
|
export async function getAllTranscripts(daysBack: number = 30): Promise<TranscriptResult[]> {
|
|
190
369
|
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1000;
|
|
191
370
|
let projDirs: import("node:fs").Dirent[];
|
|
@@ -198,24 +377,9 @@ export async function getAllTranscripts(daysBack: number = 30): Promise<Transcri
|
|
|
198
377
|
await Promise.all(
|
|
199
378
|
projDirs.map(async (d) => {
|
|
200
379
|
if (!d.isDirectory()) return;
|
|
201
|
-
|
|
202
|
-
let entries;
|
|
203
|
-
try {
|
|
204
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
205
|
-
} catch {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
for (const e of entries) {
|
|
209
|
-
if (!e.isFile() || !e.name.endsWith(".jsonl")) continue;
|
|
210
|
-
const fp = join(dir, e.name);
|
|
211
|
-
try {
|
|
212
|
-
const st = await stat(fp);
|
|
213
|
-
if (st.mtimeMs >= cutoff) candidates.push({ filePath: fp, mtimeMs: st.mtimeMs });
|
|
214
|
-
} catch {
|
|
215
|
-
// skip
|
|
216
|
-
}
|
|
217
|
-
}
|
|
380
|
+
await collectJsonlFiles(join(PROJECTS_DIR, String(d.name)), cutoff, candidates);
|
|
218
381
|
})
|
|
219
382
|
);
|
|
220
|
-
|
|
383
|
+
const raws = await pMapLimit(candidates, 16, (c) => getFileResult(c.filePath, c.mtimeMs));
|
|
384
|
+
return dedupAndSum(raws);
|
|
221
385
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mbeato/contextscope",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Local dashboard auditing Claude Code's per-turn token context (skills, agents, commands, CLAUDE.md, MEMORY.md, hooks, MCP) with toggle-based disable and session analytics.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"start": "next start",
|
|
13
13
|
"prod": "next build && next start",
|
|
14
14
|
"lint": "eslint",
|
|
15
|
+
"refresh-prices": "node scripts/refresh-prices.mjs",
|
|
15
16
|
"prepublishOnly": "next build",
|
|
16
17
|
"release": "test -f .env.publish || (echo 'missing .env.publish — copy .env.publish.example and add your NPM_TOKEN' && exit 1) && set -a && . ./.env.publish && set +a && npm publish"
|
|
17
18
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Refresh lib/model-prices.json from LiteLLM's authoritative price list.
|
|
4
|
+
*
|
|
5
|
+
* Pulls https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json,
|
|
6
|
+
* keeps only Anthropic models with the cost fields we use, and writes a pruned
|
|
7
|
+
* JSON to lib/model-prices.json. Run before each contextscope release so npm
|
|
8
|
+
* users get current prices.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node scripts/refresh-prices.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { writeFile } from "node:fs/promises";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const SRC = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const DST = join(HERE, "..", "lib", "model-prices.json");
|
|
19
|
+
|
|
20
|
+
const res = await fetch(SRC);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
console.error(`fetch failed: ${res.status} ${res.statusText}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const all = await res.json();
|
|
26
|
+
|
|
27
|
+
const pruned = {};
|
|
28
|
+
for (const [name, m] of Object.entries(all)) {
|
|
29
|
+
if (m?.litellm_provider !== "anthropic") continue;
|
|
30
|
+
if (typeof m.input_cost_per_token !== "number") continue;
|
|
31
|
+
const cc5m = m.cache_creation_input_token_cost ?? 0;
|
|
32
|
+
pruned[name] = {
|
|
33
|
+
input: m.input_cost_per_token,
|
|
34
|
+
output: m.output_cost_per_token ?? 0,
|
|
35
|
+
cache_read: m.cache_read_input_token_cost ?? 0,
|
|
36
|
+
cache_creation_5m: cc5m,
|
|
37
|
+
// Anthropic's documented 1hr cache rate is 2x the 5min rate.
|
|
38
|
+
// Fall back to 2x when LiteLLM doesn't list it explicitly.
|
|
39
|
+
cache_creation_1h: m.cache_creation_input_token_cost_above_1hr ?? cc5m * 2,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sorted = Object.fromEntries(Object.entries(pruned).sort(([a], [b]) => a.localeCompare(b)));
|
|
44
|
+
await writeFile(
|
|
45
|
+
DST,
|
|
46
|
+
JSON.stringify(
|
|
47
|
+
{
|
|
48
|
+
_source: SRC,
|
|
49
|
+
_refreshedAt: new Date().toISOString(),
|
|
50
|
+
_description: "Anthropic model prices in USD per token (input, output, cache_read, cache_creation 5min default). Refresh with `npm run refresh-prices`.",
|
|
51
|
+
models: sorted,
|
|
52
|
+
},
|
|
53
|
+
null,
|
|
54
|
+
2
|
|
55
|
+
) + "\n",
|
|
56
|
+
"utf8"
|
|
57
|
+
);
|
|
58
|
+
console.log(`wrote ${Object.keys(sorted).length} anthropic models → ${DST}`);
|