storyforge 0.4.12 → 0.4.14
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-OFFQXJQN.js +237 -0
- package/dist/index.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -864,6 +864,18 @@ async function devCommand(options) {
|
|
|
864
864
|
}
|
|
865
865
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
866
866
|
const pathname = url.pathname;
|
|
867
|
+
if (pathname === "/api/cli-usage") {
|
|
868
|
+
try {
|
|
869
|
+
const { gatherCliUsage } = await import("./cli-usage-OFFQXJQN.js");
|
|
870
|
+
const report = await gatherCliUsage();
|
|
871
|
+
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
872
|
+
res.end(JSON.stringify(report));
|
|
873
|
+
} catch (err) {
|
|
874
|
+
res.writeHead(500, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
875
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
867
879
|
if (pathname === "/api/health") {
|
|
868
880
|
const probeCli = (binary) => {
|
|
869
881
|
try {
|