storyforge 0.4.17 → 0.4.19
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.
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import * as os from "os";
|
|
7
|
+
import { loadSessionBlockData } from "ccusage/data-loader";
|
|
7
8
|
var PRICES = {
|
|
8
|
-
// Anthropic —
|
|
9
|
+
// Anthropic — claude.com/pricing
|
|
9
10
|
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
10
11
|
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
11
12
|
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
12
13
|
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
13
14
|
"claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
14
15
|
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
15
|
-
// OpenAI —
|
|
16
|
+
// OpenAI — platform.openai.com/pricing
|
|
16
17
|
"gpt-5.5": { input: 5, output: 30 },
|
|
17
18
|
"gpt-5.4": { input: 2.5, output: 15 },
|
|
18
19
|
"gpt-5.4-mini": { input: 0.75, output: 4.5 },
|
|
19
20
|
"gpt-5.4-nano": { input: 0.2, output: 1.25 },
|
|
20
21
|
"gpt-5": { input: 2.5, output: 15 },
|
|
21
22
|
"gpt-5-codex": { input: 2.5, output: 15 },
|
|
22
|
-
// alias of gpt-5
|
|
23
23
|
"gpt-4o": { input: 2.5, output: 10 }
|
|
24
24
|
};
|
|
25
25
|
function priceFor(model) {
|
|
@@ -30,24 +30,30 @@ function priceFor(model) {
|
|
|
30
30
|
}
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
|
+
function computeCost(model, u) {
|
|
34
|
+
const p = priceFor(model);
|
|
35
|
+
if (!p) return 0;
|
|
36
|
+
let cost = u.input / 1e6 * p.input + u.output / 1e6 * p.output;
|
|
37
|
+
if (p.cacheRead != null) cost += u.cacheRead / 1e6 * p.cacheRead;
|
|
38
|
+
if (p.cacheWrite != null) cost += u.cacheCreate / 1e6 * p.cacheWrite;
|
|
39
|
+
return cost;
|
|
40
|
+
}
|
|
41
|
+
var CLI_USAGE_PARSER_VERSION = 4;
|
|
33
42
|
function emptyModelUsage(model) {
|
|
34
43
|
return { model, inputTokens: 0, cachedReadTokens: 0, cacheCreateTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
35
44
|
}
|
|
36
|
-
function
|
|
37
|
-
const row =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
function addToBucket(bucket, model, u) {
|
|
46
|
+
const row = bucket[model] ??= emptyModelUsage(model);
|
|
47
|
+
row.inputTokens += u.input;
|
|
48
|
+
row.cachedReadTokens += u.cacheRead;
|
|
49
|
+
row.cacheCreateTokens += u.cacheCreate;
|
|
50
|
+
row.outputTokens += u.output;
|
|
51
|
+
row.costUsd += computeCost(model, u);
|
|
42
52
|
}
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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 };
|
|
53
|
+
function startOfTodayMs(now = Date.now()) {
|
|
54
|
+
const d = new Date(now);
|
|
55
|
+
d.setHours(0, 0, 0, 0);
|
|
56
|
+
return d.getTime();
|
|
51
57
|
}
|
|
52
58
|
function listJsonlFiles(rootDir) {
|
|
53
59
|
const out = [];
|
|
@@ -69,47 +75,6 @@ function listJsonlFiles(rootDir) {
|
|
|
69
75
|
}
|
|
70
76
|
return out;
|
|
71
77
|
}
|
|
72
|
-
function parseClaudeFile(file, seenMessageIds) {
|
|
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 messageId = obj?.message?.id;
|
|
91
|
-
if (messageId) {
|
|
92
|
-
if (seenMessageIds.has(messageId)) continue;
|
|
93
|
-
seenMessageIds.add(messageId);
|
|
94
|
-
}
|
|
95
|
-
const model = String(obj?.message?.model ?? "").toLowerCase();
|
|
96
|
-
if (!model) continue;
|
|
97
|
-
const tsRaw = obj?.timestamp ?? obj?.ts;
|
|
98
|
-
const ts = tsRaw ? new Date(tsRaw).getTime() : Date.now();
|
|
99
|
-
if (!Number.isFinite(ts)) continue;
|
|
100
|
-
out.push({
|
|
101
|
-
ts,
|
|
102
|
-
model,
|
|
103
|
-
delta: {
|
|
104
|
-
inputTokens: usage.input_tokens ?? 0,
|
|
105
|
-
cachedReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
106
|
-
cacheCreateTokens: usage.cache_creation_input_tokens ?? 0,
|
|
107
|
-
outputTokens: usage.output_tokens ?? 0
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
78
|
function parseCodexFile(file) {
|
|
114
79
|
let content;
|
|
115
80
|
try {
|
|
@@ -118,7 +83,7 @@ function parseCodexFile(file) {
|
|
|
118
83
|
return [];
|
|
119
84
|
}
|
|
120
85
|
const out = [];
|
|
121
|
-
let prev = { input: 0, cachedInput: 0, output: 0
|
|
86
|
+
let prev = { input: 0, cachedInput: 0, output: 0 };
|
|
122
87
|
let model = "gpt-5";
|
|
123
88
|
for (const raw of content.split(/\r?\n/)) {
|
|
124
89
|
if (!raw.trim()) continue;
|
|
@@ -131,53 +96,63 @@ function parseCodexFile(file) {
|
|
|
131
96
|
const modelHint = obj?.event_msg?.payload?.turn_context?.model ?? obj?.payload?.turn_context?.model ?? obj?.message?.model;
|
|
132
97
|
if (typeof modelHint === "string" && modelHint) model = modelHint.toLowerCase();
|
|
133
98
|
const payload = obj?.event_msg?.payload ?? obj?.payload;
|
|
134
|
-
if (!payload) continue;
|
|
135
|
-
if (payload.type !== "token_count") continue;
|
|
99
|
+
if (!payload || payload.type !== "token_count") continue;
|
|
136
100
|
const t = payload;
|
|
137
101
|
const cur = {
|
|
138
102
|
input: t.input_tokens ?? 0,
|
|
139
103
|
cachedInput: t.cached_input_tokens ?? 0,
|
|
140
|
-
output: t.output_tokens ?? 0
|
|
141
|
-
total: t.total_tokens ?? 0
|
|
142
|
-
};
|
|
143
|
-
const delta = {
|
|
144
|
-
inputTokens: Math.max(0, cur.input - prev.input),
|
|
145
|
-
cachedReadTokens: Math.max(0, cur.cachedInput - prev.cachedInput),
|
|
146
|
-
outputTokens: Math.max(0, cur.output - prev.output)
|
|
104
|
+
output: t.output_tokens ?? 0
|
|
147
105
|
};
|
|
106
|
+
const dInput = Math.max(0, cur.input - prev.input);
|
|
107
|
+
const dCacheRead = Math.max(0, cur.cachedInput - prev.cachedInput);
|
|
108
|
+
const dOutput = Math.max(0, cur.output - prev.output);
|
|
148
109
|
prev = cur;
|
|
149
110
|
const tsRaw = obj?.timestamp ?? obj?.event_msg?.timestamp ?? obj?.event_msg?.event_ts;
|
|
150
111
|
const ts = tsRaw ? new Date(tsRaw).getTime() : Date.now();
|
|
151
112
|
if (!Number.isFinite(ts)) continue;
|
|
152
|
-
out.push({ ts, model,
|
|
113
|
+
out.push({ ts, model, usage: { input: dInput, cacheRead: dCacheRead, cacheCreate: 0, output: dOutput } });
|
|
153
114
|
}
|
|
154
115
|
return out;
|
|
155
116
|
}
|
|
156
|
-
function startOfTodayMs(now = Date.now()) {
|
|
157
|
-
const d = new Date(now);
|
|
158
|
-
d.setHours(0, 0, 0, 0);
|
|
159
|
-
return d.getTime();
|
|
160
|
-
}
|
|
161
|
-
var CLI_USAGE_PARSER_VERSION = 2;
|
|
162
117
|
async function gatherCliUsage() {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
118
|
+
const claudeEntries = [];
|
|
119
|
+
let claudeFileCount = 0;
|
|
120
|
+
try {
|
|
121
|
+
const blocks = await loadSessionBlockData(
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
{ offline: true }
|
|
124
|
+
);
|
|
125
|
+
for (const block of blocks) {
|
|
126
|
+
const entries = block.entries ?? [];
|
|
127
|
+
for (const e of entries) {
|
|
128
|
+
const ts = e.timestamp ? new Date(e.timestamp).getTime() : NaN;
|
|
129
|
+
if (!Number.isFinite(ts)) continue;
|
|
130
|
+
const model = (e.model ?? "").toLowerCase();
|
|
131
|
+
if (!model) continue;
|
|
132
|
+
const u = e.usage ?? {};
|
|
133
|
+
claudeEntries.push({
|
|
134
|
+
ts,
|
|
135
|
+
model,
|
|
136
|
+
usage: {
|
|
137
|
+
input: u.inputTokens ?? 0,
|
|
138
|
+
cacheRead: u.cacheReadInputTokens ?? 0,
|
|
139
|
+
cacheCreate: u.cacheCreationInputTokens ?? 0,
|
|
140
|
+
output: u.outputTokens ?? 0
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
claudeFileCount = listJsonlFiles(path.join(os.homedir(), ".claude", "projects")).length;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
void err;
|
|
175
148
|
}
|
|
149
|
+
const codexDir = path.join(os.homedir(), ".codex", "sessions");
|
|
150
|
+
const codexFiles = listJsonlFiles(codexDir);
|
|
151
|
+
const codexEntries = [];
|
|
176
152
|
for (const f of codexFiles) {
|
|
177
|
-
|
|
178
|
-
if (parsed.length === 0) skipped++;
|
|
179
|
-
else lines.push(...parsed);
|
|
153
|
+
codexEntries.push(...parseCodexFile(f));
|
|
180
154
|
}
|
|
155
|
+
const all = [...claudeEntries, ...codexEntries];
|
|
181
156
|
const now = Date.now();
|
|
182
157
|
const cutoff5h = now - 5 * 60 * 60 * 1e3;
|
|
183
158
|
const cutoff24h = now - 24 * 60 * 60 * 1e3;
|
|
@@ -191,34 +166,29 @@ async function gatherCliUsage() {
|
|
|
191
166
|
today: {},
|
|
192
167
|
lifetime: {}
|
|
193
168
|
};
|
|
194
|
-
for (const
|
|
195
|
-
|
|
169
|
+
for (const e of all) {
|
|
170
|
+
addToBucket(buckets.lifetime, e.model, e.usage);
|
|
196
171
|
messageCounts.lifetime++;
|
|
197
|
-
if (
|
|
198
|
-
|
|
172
|
+
if (e.ts >= cutoff7d) {
|
|
173
|
+
addToBucket(buckets.last7d, e.model, e.usage);
|
|
199
174
|
messageCounts.last7d++;
|
|
200
175
|
}
|
|
201
|
-
if (
|
|
202
|
-
|
|
176
|
+
if (e.ts >= cutoff24h) {
|
|
177
|
+
addToBucket(buckets.last24h, e.model, e.usage);
|
|
203
178
|
messageCounts.last24h++;
|
|
204
179
|
}
|
|
205
|
-
if (
|
|
206
|
-
|
|
180
|
+
if (e.ts >= cutoff5h) {
|
|
181
|
+
addToBucket(buckets.last5h, e.model, e.usage);
|
|
207
182
|
messageCounts.last5h++;
|
|
208
183
|
}
|
|
209
|
-
if (
|
|
210
|
-
|
|
184
|
+
if (e.ts >= startToday) {
|
|
185
|
+
addToBucket(buckets.today, e.model, e.usage);
|
|
211
186
|
messageCounts.today++;
|
|
212
187
|
}
|
|
213
188
|
}
|
|
214
189
|
function finalise(rec) {
|
|
215
|
-
return Object.values(rec).
|
|
190
|
+
return Object.values(rec).sort((a, b) => b.costUsd - a.costUsd);
|
|
216
191
|
}
|
|
217
|
-
const last5h = finalise(buckets.last5h);
|
|
218
|
-
const last24h = finalise(buckets.last24h);
|
|
219
|
-
const last7d = finalise(buckets.last7d);
|
|
220
|
-
const today = finalise(buckets.today);
|
|
221
|
-
const lifetime = finalise(buckets.lifetime);
|
|
222
192
|
function sum(rows, messages) {
|
|
223
193
|
return {
|
|
224
194
|
tokens: rows.reduce((s, r) => s + r.inputTokens + r.cachedReadTokens + r.cacheCreateTokens + r.outputTokens, 0),
|
|
@@ -226,6 +196,11 @@ async function gatherCliUsage() {
|
|
|
226
196
|
messages
|
|
227
197
|
};
|
|
228
198
|
}
|
|
199
|
+
const last5h = finalise(buckets.last5h);
|
|
200
|
+
const last24h = finalise(buckets.last24h);
|
|
201
|
+
const last7d = finalise(buckets.last7d);
|
|
202
|
+
const today = finalise(buckets.today);
|
|
203
|
+
const lifetime = finalise(buckets.lifetime);
|
|
229
204
|
return {
|
|
230
205
|
generatedAt: new Date(now).toISOString(),
|
|
231
206
|
windows: { last5h, last24h, last7d, today, lifetime },
|
|
@@ -236,7 +211,11 @@ async function gatherCliUsage() {
|
|
|
236
211
|
today: sum(today, messageCounts.today),
|
|
237
212
|
lifetime: sum(lifetime, messageCounts.lifetime)
|
|
238
213
|
},
|
|
239
|
-
sources: {
|
|
214
|
+
sources: {
|
|
215
|
+
claudeFiles: claudeFileCount,
|
|
216
|
+
codexFiles: codexFiles.length,
|
|
217
|
+
skipped: 0
|
|
218
|
+
}
|
|
240
219
|
};
|
|
241
220
|
}
|
|
242
221
|
export {
|
package/dist/index.js
CHANGED
|
@@ -866,7 +866,7 @@ async function devCommand(options) {
|
|
|
866
866
|
const pathname = url.pathname;
|
|
867
867
|
if (pathname === "/api/cli-usage") {
|
|
868
868
|
try {
|
|
869
|
-
const { gatherCliUsage, CLI_USAGE_PARSER_VERSION } = await import("./cli-usage-
|
|
869
|
+
const { gatherCliUsage, CLI_USAGE_PARSER_VERSION } = await import("./cli-usage-NX33FYQ6.js");
|
|
870
870
|
const g = global;
|
|
871
871
|
const now = Date.now();
|
|
872
872
|
if (g.__forgeCliUsageCache && g.__forgeCliUsageCache.ver === CLI_USAGE_PARSER_VERSION && now - g.__forgeCliUsageCache.at < 3e4) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyforge",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
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"
|