storyforge 0.4.16 → 0.4.18
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/cli-usage-EC5NOILQ.js +208 -0
- package/dist/index.js +5 -5
- package/package.json +5 -2
- package/dist/cli-usage-OFFQXJQN.js +0 -237
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli-usage.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import { loadSessionBlockData } from "ccusage/data-loader";
|
|
8
|
+
var CODEX_PRICES = {
|
|
9
|
+
"gpt-5.5": { input: 5, output: 30 },
|
|
10
|
+
"gpt-5.4": { input: 2.5, output: 15 },
|
|
11
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5 },
|
|
12
|
+
"gpt-5.4-nano": { input: 0.2, output: 1.25 },
|
|
13
|
+
"gpt-5": { input: 2.5, output: 15 },
|
|
14
|
+
"gpt-5-codex": { input: 2.5, output: 15 },
|
|
15
|
+
"gpt-4o": { input: 2.5, output: 10 }
|
|
16
|
+
};
|
|
17
|
+
function codexPriceFor(model) {
|
|
18
|
+
const lower = model.toLowerCase();
|
|
19
|
+
if (CODEX_PRICES[lower]) return CODEX_PRICES[lower];
|
|
20
|
+
for (const key of Object.keys(CODEX_PRICES)) {
|
|
21
|
+
if (lower.startsWith(key)) return CODEX_PRICES[key];
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
var CLI_USAGE_PARSER_VERSION = 3;
|
|
26
|
+
function emptyModelUsage(model) {
|
|
27
|
+
return { model, inputTokens: 0, cachedReadTokens: 0, cacheCreateTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
28
|
+
}
|
|
29
|
+
function addToBucket(bucket, model, u) {
|
|
30
|
+
const row = bucket[model] ??= emptyModelUsage(model);
|
|
31
|
+
row.inputTokens += u.input;
|
|
32
|
+
row.cachedReadTokens += u.cacheRead;
|
|
33
|
+
row.cacheCreateTokens += u.cacheCreate;
|
|
34
|
+
row.outputTokens += u.output;
|
|
35
|
+
if (typeof u.costUSD === "number") row.costUsd += u.costUSD;
|
|
36
|
+
}
|
|
37
|
+
function startOfTodayMs(now = Date.now()) {
|
|
38
|
+
const d = new Date(now);
|
|
39
|
+
d.setHours(0, 0, 0, 0);
|
|
40
|
+
return d.getTime();
|
|
41
|
+
}
|
|
42
|
+
function listJsonlFiles(rootDir) {
|
|
43
|
+
const out = [];
|
|
44
|
+
if (!fs.existsSync(rootDir)) return out;
|
|
45
|
+
const stack = [rootDir];
|
|
46
|
+
while (stack.length > 0) {
|
|
47
|
+
const cur = stack.pop();
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const e of entries) {
|
|
55
|
+
const full = path.join(cur, e.name);
|
|
56
|
+
if (e.isDirectory()) stack.push(full);
|
|
57
|
+
else if (e.isFile() && e.name.endsWith(".jsonl")) out.push(full);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function parseCodexFile(file) {
|
|
63
|
+
let content;
|
|
64
|
+
try {
|
|
65
|
+
content = fs.readFileSync(file, "utf-8");
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const out = [];
|
|
70
|
+
let prev = { input: 0, cachedInput: 0, output: 0 };
|
|
71
|
+
let model = "gpt-5";
|
|
72
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
73
|
+
if (!raw.trim()) continue;
|
|
74
|
+
let obj;
|
|
75
|
+
try {
|
|
76
|
+
obj = JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const modelHint = obj?.event_msg?.payload?.turn_context?.model ?? obj?.payload?.turn_context?.model ?? obj?.message?.model;
|
|
81
|
+
if (typeof modelHint === "string" && modelHint) model = modelHint.toLowerCase();
|
|
82
|
+
const payload = obj?.event_msg?.payload ?? obj?.payload;
|
|
83
|
+
if (!payload || payload.type !== "token_count") continue;
|
|
84
|
+
const t = payload;
|
|
85
|
+
const cur = {
|
|
86
|
+
input: t.input_tokens ?? 0,
|
|
87
|
+
cachedInput: t.cached_input_tokens ?? 0,
|
|
88
|
+
output: t.output_tokens ?? 0
|
|
89
|
+
};
|
|
90
|
+
const dInput = Math.max(0, cur.input - prev.input);
|
|
91
|
+
const dCacheRead = Math.max(0, cur.cachedInput - prev.cachedInput);
|
|
92
|
+
const dOutput = Math.max(0, cur.output - prev.output);
|
|
93
|
+
prev = cur;
|
|
94
|
+
const tsRaw = obj?.timestamp ?? obj?.event_msg?.timestamp ?? obj?.event_msg?.event_ts;
|
|
95
|
+
const ts = tsRaw ? new Date(tsRaw).getTime() : Date.now();
|
|
96
|
+
if (!Number.isFinite(ts)) continue;
|
|
97
|
+
const price = codexPriceFor(model);
|
|
98
|
+
const costUSD = price ? dInput / 1e6 * price.input + dOutput / 1e6 * price.output : void 0;
|
|
99
|
+
out.push({ ts, model, usage: { input: dInput, cacheRead: dCacheRead, cacheCreate: 0, output: dOutput, costUSD } });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
async function gatherCliUsage() {
|
|
104
|
+
const claudeEntries = [];
|
|
105
|
+
let claudeFileCount = 0;
|
|
106
|
+
try {
|
|
107
|
+
const blocks = await loadSessionBlockData();
|
|
108
|
+
for (const block of blocks) {
|
|
109
|
+
const entries = block.entries ?? [];
|
|
110
|
+
for (const e of entries) {
|
|
111
|
+
const ts = e.timestamp ? new Date(e.timestamp).getTime() : NaN;
|
|
112
|
+
if (!Number.isFinite(ts)) continue;
|
|
113
|
+
const model = (e.model ?? "").toLowerCase();
|
|
114
|
+
if (!model) continue;
|
|
115
|
+
const u = e.usage ?? {};
|
|
116
|
+
claudeEntries.push({
|
|
117
|
+
ts,
|
|
118
|
+
model,
|
|
119
|
+
usage: {
|
|
120
|
+
input: u.inputTokens ?? 0,
|
|
121
|
+
cacheRead: u.cacheReadInputTokens ?? 0,
|
|
122
|
+
cacheCreate: u.cacheCreationInputTokens ?? 0,
|
|
123
|
+
output: u.outputTokens ?? 0,
|
|
124
|
+
costUSD: e.costUSD
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
claudeFileCount = listJsonlFiles(path.join(os.homedir(), ".claude", "projects")).length;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
void err;
|
|
132
|
+
}
|
|
133
|
+
const codexDir = path.join(os.homedir(), ".codex", "sessions");
|
|
134
|
+
const codexFiles = listJsonlFiles(codexDir);
|
|
135
|
+
const codexEntries = [];
|
|
136
|
+
for (const f of codexFiles) {
|
|
137
|
+
codexEntries.push(...parseCodexFile(f));
|
|
138
|
+
}
|
|
139
|
+
const all = [...claudeEntries, ...codexEntries];
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const cutoff5h = now - 5 * 60 * 60 * 1e3;
|
|
142
|
+
const cutoff24h = now - 24 * 60 * 60 * 1e3;
|
|
143
|
+
const cutoff7d = now - 7 * 24 * 60 * 60 * 1e3;
|
|
144
|
+
const startToday = startOfTodayMs(now);
|
|
145
|
+
const messageCounts = { last5h: 0, last24h: 0, last7d: 0, today: 0, lifetime: 0 };
|
|
146
|
+
const buckets = {
|
|
147
|
+
last5h: {},
|
|
148
|
+
last24h: {},
|
|
149
|
+
last7d: {},
|
|
150
|
+
today: {},
|
|
151
|
+
lifetime: {}
|
|
152
|
+
};
|
|
153
|
+
for (const e of all) {
|
|
154
|
+
addToBucket(buckets.lifetime, e.model, e.usage);
|
|
155
|
+
messageCounts.lifetime++;
|
|
156
|
+
if (e.ts >= cutoff7d) {
|
|
157
|
+
addToBucket(buckets.last7d, e.model, e.usage);
|
|
158
|
+
messageCounts.last7d++;
|
|
159
|
+
}
|
|
160
|
+
if (e.ts >= cutoff24h) {
|
|
161
|
+
addToBucket(buckets.last24h, e.model, e.usage);
|
|
162
|
+
messageCounts.last24h++;
|
|
163
|
+
}
|
|
164
|
+
if (e.ts >= cutoff5h) {
|
|
165
|
+
addToBucket(buckets.last5h, e.model, e.usage);
|
|
166
|
+
messageCounts.last5h++;
|
|
167
|
+
}
|
|
168
|
+
if (e.ts >= startToday) {
|
|
169
|
+
addToBucket(buckets.today, e.model, e.usage);
|
|
170
|
+
messageCounts.today++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function finalise(rec) {
|
|
174
|
+
return Object.values(rec).sort((a, b) => b.costUsd - a.costUsd);
|
|
175
|
+
}
|
|
176
|
+
function sum(rows, messages) {
|
|
177
|
+
return {
|
|
178
|
+
tokens: rows.reduce((s, r) => s + r.inputTokens + r.cachedReadTokens + r.cacheCreateTokens + r.outputTokens, 0),
|
|
179
|
+
costUsd: rows.reduce((s, r) => s + r.costUsd, 0),
|
|
180
|
+
messages
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const last5h = finalise(buckets.last5h);
|
|
184
|
+
const last24h = finalise(buckets.last24h);
|
|
185
|
+
const last7d = finalise(buckets.last7d);
|
|
186
|
+
const today = finalise(buckets.today);
|
|
187
|
+
const lifetime = finalise(buckets.lifetime);
|
|
188
|
+
return {
|
|
189
|
+
generatedAt: new Date(now).toISOString(),
|
|
190
|
+
windows: { last5h, last24h, last7d, today, lifetime },
|
|
191
|
+
totals: {
|
|
192
|
+
last5h: sum(last5h, messageCounts.last5h),
|
|
193
|
+
last24h: sum(last24h, messageCounts.last24h),
|
|
194
|
+
last7d: sum(last7d, messageCounts.last7d),
|
|
195
|
+
today: sum(today, messageCounts.today),
|
|
196
|
+
lifetime: sum(lifetime, messageCounts.lifetime)
|
|
197
|
+
},
|
|
198
|
+
sources: {
|
|
199
|
+
claudeFiles: claudeFileCount,
|
|
200
|
+
codexFiles: codexFiles.length,
|
|
201
|
+
skipped: 0
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
CLI_USAGE_PARSER_VERSION,
|
|
207
|
+
gatherCliUsage
|
|
208
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -866,16 +866,16 @@ async function devCommand(options) {
|
|
|
866
866
|
const pathname = url.pathname;
|
|
867
867
|
if (pathname === "/api/cli-usage") {
|
|
868
868
|
try {
|
|
869
|
-
const
|
|
869
|
+
const { gatherCliUsage, CLI_USAGE_PARSER_VERSION } = await import("./cli-usage-EC5NOILQ.js");
|
|
870
|
+
const g = global;
|
|
870
871
|
const now = Date.now();
|
|
871
|
-
if (
|
|
872
|
+
if (g.__forgeCliUsageCache && g.__forgeCliUsageCache.ver === CLI_USAGE_PARSER_VERSION && now - g.__forgeCliUsageCache.at < 3e4) {
|
|
872
873
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
873
|
-
res.end(JSON.stringify(
|
|
874
|
+
res.end(JSON.stringify(g.__forgeCliUsageCache.data));
|
|
874
875
|
return;
|
|
875
876
|
}
|
|
876
|
-
const { gatherCliUsage } = await import("./cli-usage-OFFQXJQN.js");
|
|
877
877
|
const report = await gatherCliUsage();
|
|
878
|
-
|
|
878
|
+
g.__forgeCliUsageCache = { at: now, ver: CLI_USAGE_PARSER_VERSION, data: report };
|
|
879
879
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
880
880
|
res.end(JSON.stringify(report));
|
|
881
881
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyforge",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "StoryForge — local bridge for the Forge video production web app. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"lint": "tsc --noEmit",
|
|
16
16
|
"prepublishOnly": "npm run build"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": {
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@ccusage/codex": "18.0.11",
|
|
20
|
+
"ccusage": "18.0.11"
|
|
21
|
+
},
|
|
19
22
|
"devDependencies": {
|
|
20
23
|
"tsup": "8.5.0",
|
|
21
24
|
"typescript": "5.8.3"
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/cli-usage.ts
|
|
4
|
-
import * as fs from "fs";
|
|
5
|
-
import * as path from "path";
|
|
6
|
-
import * as os from "os";
|
|
7
|
-
var PRICES = {
|
|
8
|
-
// Anthropic — per claude.com/pricing (April 2026)
|
|
9
|
-
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
10
|
-
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
11
|
-
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
12
|
-
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
13
|
-
"claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
14
|
-
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
15
|
-
// OpenAI — per platform.openai.com/pricing (April 2026)
|
|
16
|
-
"gpt-5.5": { input: 5, output: 30 },
|
|
17
|
-
"gpt-5.4": { input: 2.5, output: 15 },
|
|
18
|
-
"gpt-5.4-mini": { input: 0.75, output: 4.5 },
|
|
19
|
-
"gpt-5.4-nano": { input: 0.2, output: 1.25 },
|
|
20
|
-
"gpt-5": { input: 2.5, output: 15 },
|
|
21
|
-
"gpt-5-codex": { input: 2.5, output: 15 },
|
|
22
|
-
// alias of gpt-5
|
|
23
|
-
"gpt-4o": { input: 2.5, output: 10 }
|
|
24
|
-
};
|
|
25
|
-
function priceFor(model) {
|
|
26
|
-
const lower = model.toLowerCase();
|
|
27
|
-
if (PRICES[lower]) return PRICES[lower];
|
|
28
|
-
for (const key of Object.keys(PRICES)) {
|
|
29
|
-
if (lower.startsWith(key)) return PRICES[key];
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
function emptyModelUsage(model) {
|
|
34
|
-
return { model, inputTokens: 0, cachedReadTokens: 0, cacheCreateTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
35
|
-
}
|
|
36
|
-
function addUsage(target, model, delta) {
|
|
37
|
-
const row = target[model] ??= emptyModelUsage(model);
|
|
38
|
-
if (delta.inputTokens) row.inputTokens += delta.inputTokens;
|
|
39
|
-
if (delta.cachedReadTokens) row.cachedReadTokens += delta.cachedReadTokens;
|
|
40
|
-
if (delta.cacheCreateTokens) row.cacheCreateTokens += delta.cacheCreateTokens;
|
|
41
|
-
if (delta.outputTokens) row.outputTokens += delta.outputTokens;
|
|
42
|
-
}
|
|
43
|
-
function recompute(usage) {
|
|
44
|
-
const price = priceFor(usage.model);
|
|
45
|
-
if (!price) return { ...usage, costUsd: 0 };
|
|
46
|
-
const inputCost = usage.inputTokens / 1e6 * price.input;
|
|
47
|
-
const cacheReadCost = price.cacheRead != null ? usage.cachedReadTokens / 1e6 * price.cacheRead : 0;
|
|
48
|
-
const cacheWriteCost = price.cacheWrite != null ? usage.cacheCreateTokens / 1e6 * price.cacheWrite : 0;
|
|
49
|
-
const outputCost = usage.outputTokens / 1e6 * price.output;
|
|
50
|
-
return { ...usage, costUsd: inputCost + cacheReadCost + cacheWriteCost + outputCost };
|
|
51
|
-
}
|
|
52
|
-
function listJsonlFiles(rootDir) {
|
|
53
|
-
const out = [];
|
|
54
|
-
if (!fs.existsSync(rootDir)) return out;
|
|
55
|
-
const stack = [rootDir];
|
|
56
|
-
while (stack.length > 0) {
|
|
57
|
-
const cur = stack.pop();
|
|
58
|
-
let entries;
|
|
59
|
-
try {
|
|
60
|
-
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
61
|
-
} catch {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
for (const e of entries) {
|
|
65
|
-
const full = path.join(cur, e.name);
|
|
66
|
-
if (e.isDirectory()) stack.push(full);
|
|
67
|
-
else if (e.isFile() && e.name.endsWith(".jsonl")) out.push(full);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
function parseClaudeFile(file) {
|
|
73
|
-
let content;
|
|
74
|
-
try {
|
|
75
|
-
content = fs.readFileSync(file, "utf-8");
|
|
76
|
-
} catch {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
const out = [];
|
|
80
|
-
for (const raw of content.split(/\r?\n/)) {
|
|
81
|
-
if (!raw.trim()) continue;
|
|
82
|
-
let obj;
|
|
83
|
-
try {
|
|
84
|
-
obj = JSON.parse(raw);
|
|
85
|
-
} catch {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const usage = obj?.message?.usage;
|
|
89
|
-
if (!usage) continue;
|
|
90
|
-
const model = String(obj?.message?.model ?? "").toLowerCase();
|
|
91
|
-
if (!model) continue;
|
|
92
|
-
const tsRaw = obj?.timestamp ?? obj?.ts;
|
|
93
|
-
const ts = tsRaw ? new Date(tsRaw).getTime() : Date.now();
|
|
94
|
-
if (!Number.isFinite(ts)) continue;
|
|
95
|
-
out.push({
|
|
96
|
-
ts,
|
|
97
|
-
model,
|
|
98
|
-
delta: {
|
|
99
|
-
inputTokens: usage.input_tokens ?? 0,
|
|
100
|
-
cachedReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
101
|
-
cacheCreateTokens: usage.cache_creation_input_tokens ?? 0,
|
|
102
|
-
outputTokens: usage.output_tokens ?? 0
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
function parseCodexFile(file) {
|
|
109
|
-
let content;
|
|
110
|
-
try {
|
|
111
|
-
content = fs.readFileSync(file, "utf-8");
|
|
112
|
-
} catch {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
const out = [];
|
|
116
|
-
let prev = { input: 0, cachedInput: 0, output: 0, total: 0 };
|
|
117
|
-
let model = "gpt-5";
|
|
118
|
-
for (const raw of content.split(/\r?\n/)) {
|
|
119
|
-
if (!raw.trim()) continue;
|
|
120
|
-
let obj;
|
|
121
|
-
try {
|
|
122
|
-
obj = JSON.parse(raw);
|
|
123
|
-
} catch {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const modelHint = obj?.event_msg?.payload?.turn_context?.model ?? obj?.payload?.turn_context?.model ?? obj?.message?.model;
|
|
127
|
-
if (typeof modelHint === "string" && modelHint) model = modelHint.toLowerCase();
|
|
128
|
-
const payload = obj?.event_msg?.payload ?? obj?.payload;
|
|
129
|
-
if (!payload) continue;
|
|
130
|
-
if (payload.type !== "token_count") continue;
|
|
131
|
-
const t = payload;
|
|
132
|
-
const cur = {
|
|
133
|
-
input: t.input_tokens ?? 0,
|
|
134
|
-
cachedInput: t.cached_input_tokens ?? 0,
|
|
135
|
-
output: t.output_tokens ?? 0,
|
|
136
|
-
total: t.total_tokens ?? 0
|
|
137
|
-
};
|
|
138
|
-
const delta = {
|
|
139
|
-
inputTokens: Math.max(0, cur.input - prev.input),
|
|
140
|
-
cachedReadTokens: Math.max(0, cur.cachedInput - prev.cachedInput),
|
|
141
|
-
outputTokens: Math.max(0, cur.output - prev.output)
|
|
142
|
-
};
|
|
143
|
-
prev = cur;
|
|
144
|
-
const tsRaw = obj?.timestamp ?? obj?.event_msg?.timestamp ?? obj?.event_msg?.event_ts;
|
|
145
|
-
const ts = tsRaw ? new Date(tsRaw).getTime() : Date.now();
|
|
146
|
-
if (!Number.isFinite(ts)) continue;
|
|
147
|
-
out.push({ ts, model, delta });
|
|
148
|
-
}
|
|
149
|
-
return out;
|
|
150
|
-
}
|
|
151
|
-
function startOfTodayMs(now = Date.now()) {
|
|
152
|
-
const d = new Date(now);
|
|
153
|
-
d.setHours(0, 0, 0, 0);
|
|
154
|
-
return d.getTime();
|
|
155
|
-
}
|
|
156
|
-
async function gatherCliUsage() {
|
|
157
|
-
const home = os.homedir();
|
|
158
|
-
const claudeDir = path.join(home, ".claude", "projects");
|
|
159
|
-
const codexDir = path.join(home, ".codex", "sessions");
|
|
160
|
-
const claudeFiles = listJsonlFiles(claudeDir);
|
|
161
|
-
const codexFiles = listJsonlFiles(codexDir);
|
|
162
|
-
let skipped = 0;
|
|
163
|
-
const lines = [];
|
|
164
|
-
for (const f of claudeFiles) {
|
|
165
|
-
const parsed = parseClaudeFile(f);
|
|
166
|
-
if (parsed.length === 0) skipped++;
|
|
167
|
-
else lines.push(...parsed);
|
|
168
|
-
}
|
|
169
|
-
for (const f of codexFiles) {
|
|
170
|
-
const parsed = parseCodexFile(f);
|
|
171
|
-
if (parsed.length === 0) skipped++;
|
|
172
|
-
else lines.push(...parsed);
|
|
173
|
-
}
|
|
174
|
-
const now = Date.now();
|
|
175
|
-
const cutoff5h = now - 5 * 60 * 60 * 1e3;
|
|
176
|
-
const cutoff24h = now - 24 * 60 * 60 * 1e3;
|
|
177
|
-
const cutoff7d = now - 7 * 24 * 60 * 60 * 1e3;
|
|
178
|
-
const startToday = startOfTodayMs(now);
|
|
179
|
-
const messageCounts = { last5h: 0, last24h: 0, last7d: 0, today: 0, lifetime: 0 };
|
|
180
|
-
const buckets = {
|
|
181
|
-
last5h: {},
|
|
182
|
-
last24h: {},
|
|
183
|
-
last7d: {},
|
|
184
|
-
today: {},
|
|
185
|
-
lifetime: {}
|
|
186
|
-
};
|
|
187
|
-
for (const ln of lines) {
|
|
188
|
-
addUsage(buckets.lifetime, ln.model, ln.delta);
|
|
189
|
-
messageCounts.lifetime++;
|
|
190
|
-
if (ln.ts >= cutoff7d) {
|
|
191
|
-
addUsage(buckets.last7d, ln.model, ln.delta);
|
|
192
|
-
messageCounts.last7d++;
|
|
193
|
-
}
|
|
194
|
-
if (ln.ts >= cutoff24h) {
|
|
195
|
-
addUsage(buckets.last24h, ln.model, ln.delta);
|
|
196
|
-
messageCounts.last24h++;
|
|
197
|
-
}
|
|
198
|
-
if (ln.ts >= cutoff5h) {
|
|
199
|
-
addUsage(buckets.last5h, ln.model, ln.delta);
|
|
200
|
-
messageCounts.last5h++;
|
|
201
|
-
}
|
|
202
|
-
if (ln.ts >= startToday) {
|
|
203
|
-
addUsage(buckets.today, ln.model, ln.delta);
|
|
204
|
-
messageCounts.today++;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
function finalise(rec) {
|
|
208
|
-
return Object.values(rec).map(recompute).sort((a, b) => b.costUsd - a.costUsd);
|
|
209
|
-
}
|
|
210
|
-
const last5h = finalise(buckets.last5h);
|
|
211
|
-
const last24h = finalise(buckets.last24h);
|
|
212
|
-
const last7d = finalise(buckets.last7d);
|
|
213
|
-
const today = finalise(buckets.today);
|
|
214
|
-
const lifetime = finalise(buckets.lifetime);
|
|
215
|
-
function sum(rows, messages) {
|
|
216
|
-
return {
|
|
217
|
-
tokens: rows.reduce((s, r) => s + r.inputTokens + r.cachedReadTokens + r.cacheCreateTokens + r.outputTokens, 0),
|
|
218
|
-
costUsd: rows.reduce((s, r) => s + r.costUsd, 0),
|
|
219
|
-
messages
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
generatedAt: new Date(now).toISOString(),
|
|
224
|
-
windows: { last5h, last24h, last7d, today, lifetime },
|
|
225
|
-
totals: {
|
|
226
|
-
last5h: sum(last5h, messageCounts.last5h),
|
|
227
|
-
last24h: sum(last24h, messageCounts.last24h),
|
|
228
|
-
last7d: sum(last7d, messageCounts.last7d),
|
|
229
|
-
today: sum(today, messageCounts.today),
|
|
230
|
-
lifetime: sum(lifetime, messageCounts.lifetime)
|
|
231
|
-
},
|
|
232
|
-
sources: { claudeFiles: claudeFiles.length, codexFiles: codexFiles.length, skipped }
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
export {
|
|
236
|
-
gatherCliUsage
|
|
237
|
-
};
|