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 — per claude.com/pricing (April 2026)
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 — per platform.openai.com/pricing (April 2026)
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 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;
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 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 };
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, total: 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, delta });
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 home = os.homedir();
164
- const claudeDir = path.join(home, ".claude", "projects");
165
- const codexDir = path.join(home, ".codex", "sessions");
166
- const claudeFiles = listJsonlFiles(claudeDir);
167
- const codexFiles = listJsonlFiles(codexDir);
168
- let skipped = 0;
169
- const lines = [];
170
- const seenMessageIds = /* @__PURE__ */ new Set();
171
- for (const f of claudeFiles) {
172
- const parsed = parseClaudeFile(f, seenMessageIds);
173
- if (parsed.length === 0) skipped++;
174
- else lines.push(...parsed);
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
- const parsed = parseCodexFile(f);
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 ln of lines) {
195
- addUsage(buckets.lifetime, ln.model, ln.delta);
169
+ for (const e of all) {
170
+ addToBucket(buckets.lifetime, e.model, e.usage);
196
171
  messageCounts.lifetime++;
197
- if (ln.ts >= cutoff7d) {
198
- addUsage(buckets.last7d, ln.model, ln.delta);
172
+ if (e.ts >= cutoff7d) {
173
+ addToBucket(buckets.last7d, e.model, e.usage);
199
174
  messageCounts.last7d++;
200
175
  }
201
- if (ln.ts >= cutoff24h) {
202
- addUsage(buckets.last24h, ln.model, ln.delta);
176
+ if (e.ts >= cutoff24h) {
177
+ addToBucket(buckets.last24h, e.model, e.usage);
203
178
  messageCounts.last24h++;
204
179
  }
205
- if (ln.ts >= cutoff5h) {
206
- addUsage(buckets.last5h, ln.model, ln.delta);
180
+ if (e.ts >= cutoff5h) {
181
+ addToBucket(buckets.last5h, e.model, e.usage);
207
182
  messageCounts.last5h++;
208
183
  }
209
- if (ln.ts >= startToday) {
210
- addUsage(buckets.today, ln.model, ln.delta);
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).map(recompute).sort((a, b) => b.costUsd - a.costUsd);
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: { claudeFiles: claudeFiles.length, codexFiles: codexFiles.length, skipped }
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-LUATWFYP.js");
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.17",
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"