@winston.wan/burn-your-money 2.1.2 → 2.2.0

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.
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Burn Your Money - Claude Code 状态栏脚本 (Node.js 版本)
4
+ * 格式:[模型] 今日:token 费用 🔥速度 | 总计:token 费用
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ // 配置文件路径
12
+ const CONFIG_FILE = path.join(os.homedir(), '.claude', 'burn-your-money-config.json');
13
+ const SESSION_STATE = path.join(os.homedir(), '.claude', 'cache', 'session-state.json');
14
+ const TODAY_STATE = path.join(os.homedir(), '.claude', 'cache', 'today-state.json');
15
+ const HISTORY_CACHE = path.join(os.homedir(), '.claude', 'cache', 'history-cache.json');
16
+
17
+ // 默认配置
18
+ const DEFAULT_CONFIG = {
19
+ theme: 'fire',
20
+ alert_daily: 10.0,
21
+ alert_weekly: 50.0,
22
+ show_burn_rate: true,
23
+ show_eta: true,
24
+ show_trend: false
25
+ };
26
+
27
+ // 主题颜色定义
28
+ const THEMES = {
29
+ fire: { TODAY: '31', COST: '31', BURN: '31' },
30
+ ocean: { TODAY: '34', COST: '36', BURN: '36' },
31
+ forest: { TODAY: '32', COST: '33', BURN: '32' },
32
+ golden: { TODAY: '33', COST: '33', BURN: '1;33' }
33
+ };
34
+
35
+ /**
36
+ * 读取配置文件
37
+ */
38
+ function readConfig() {
39
+ try {
40
+ if (fs.existsSync(CONFIG_FILE)) {
41
+ const data = fs.readFileSync(CONFIG_FILE, 'utf8');
42
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
43
+ }
44
+ } catch (e) {
45
+ // 忽略错误,使用默认配置
46
+ }
47
+ return DEFAULT_CONFIG;
48
+ }
49
+
50
+ /**
51
+ * 格式化数字(添加 K/M 后缀)
52
+ */
53
+ function formatNumber(num) {
54
+ if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'B';
55
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
56
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
57
+ return num.toString();
58
+ }
59
+
60
+ /**
61
+ * 格式化费用
62
+ */
63
+ function formatCost(cost) {
64
+ return '$' + parseFloat(cost).toFixed(2);
65
+ }
66
+
67
+ /**
68
+ * 格式化燃烧速度
69
+ */
70
+ function formatBurnRate(rate) {
71
+ if (rate >= 1000) return (rate / 1000).toFixed(1) + 'K tok/s';
72
+ return Math.round(rate).toString() + ' tok/s';
73
+ }
74
+
75
+ /**
76
+ * 读取 JSON 文件
77
+ */
78
+ function readJsonFile(filePath, defaultValue = {}) {
79
+ try {
80
+ if (fs.existsSync(filePath)) {
81
+ const data = fs.readFileSync(filePath, 'utf8');
82
+ return JSON.parse(data);
83
+ }
84
+ } catch (e) {
85
+ // 忽略错误
86
+ }
87
+ return defaultValue;
88
+ }
89
+
90
+ /**
91
+ * 写入 JSON 文件
92
+ */
93
+ function writeJsonFile(filePath, data) {
94
+ try {
95
+ const tmpPath = filePath + '.tmp.' + process.pid;
96
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
97
+ fs.renameSync(tmpPath, filePath);
98
+ } catch (e) {
99
+ // 忽略错误
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 确保目录存在
105
+ */
106
+ function ensureDir(filePath) {
107
+ const dir = path.dirname(filePath);
108
+ if (!fs.existsSync(dir)) {
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 主函数
115
+ */
116
+ function main() {
117
+ // 读取配置
118
+ const config = readConfig();
119
+ const theme = THEMES[config.theme] || THEMES.fire;
120
+
121
+ // 从 stdin 读取 Claude Code 传入的数据
122
+ let inputData = '';
123
+ process.stdin.on('data', (chunk) => {
124
+ inputData += chunk;
125
+ });
126
+
127
+ process.stdin.on('end', () => {
128
+ try {
129
+ const data = JSON.parse(inputData || '{}');
130
+
131
+ // 提取数据
132
+ const modelName = data.model?.display_name || 'Unknown';
133
+ const sessionId = data.session_id || 'default';
134
+ const inputTokens = data.context_window?.total_input_tokens || 0;
135
+ const outputTokens = data.context_window?.total_output_tokens || 0;
136
+ const cacheReadTokens = data.context_window?.current_usage?.cache_read_input_tokens || 0;
137
+ const currentCost = data.cost?.total_cost_usd || 0;
138
+
139
+ const currentSessionTokens = inputTokens + outputTokens + cacheReadTokens;
140
+
141
+ // 计算燃烧速度
142
+ const currentTime = Math.floor(Date.now() / 1000);
143
+ let startTime = currentTime;
144
+
145
+ // 读取 session state
146
+ const sessionState = readJsonFile(SESSION_STATE);
147
+ if (sessionState.session_id === sessionId && sessionState.start_time) {
148
+ startTime = sessionState.start_time;
149
+ } else {
150
+ // 新会话,记录开始时间
151
+ ensureDir(SESSION_STATE);
152
+ writeJsonFile(SESSION_STATE, {
153
+ session_id: sessionId,
154
+ start_time: currentTime
155
+ });
156
+ }
157
+
158
+ const elapsed = currentTime - startTime;
159
+ let burnRate = 0;
160
+ if (elapsed > 0 && currentSessionTokens > 0) {
161
+ burnRate = currentSessionTokens / elapsed;
162
+ }
163
+
164
+ // 读取历史缓存
165
+ ensureDir(HISTORY_CACHE);
166
+ const now = Math.floor(Date.now() / 1000);
167
+ let historyData = readJsonFile(HISTORY_CACHE, {});
168
+
169
+ // 检查缓存是否过期(30秒)
170
+ const cacheAge = now - (historyData._cache_time || 0);
171
+ if (cacheAge > 30 || !historyData.total_tokens_all) {
172
+ // 缓存过期,重新生成(这里简化处理,实际应该调用 token-history)
173
+ historyData = {
174
+ total_tokens_all: 0,
175
+ total_cost: 0,
176
+ _cache_time: now
177
+ };
178
+ }
179
+
180
+ // 今日数据持久化
181
+ const todayDate = new Date().toISOString().split('T')[0];
182
+ ensureDir(TODAY_STATE);
183
+ const todayState = readJsonFile(TODAY_STATE, {
184
+ date: todayDate,
185
+ tokens: 0,
186
+ cost: 0,
187
+ last_session_tokens: 0,
188
+ last_session_cost: 0
189
+ });
190
+
191
+ // 如果是新的一天,重置
192
+ if (todayState.date !== todayDate) {
193
+ todayState.date = todayDate;
194
+ todayState.tokens = 0;
195
+ todayState.cost = 0;
196
+ todayState.last_session_tokens = 0;
197
+ todayState.last_session_cost = 0;
198
+ }
199
+
200
+ // 计算增量
201
+ let tokenIncrement = currentSessionTokens - (todayState.last_session_tokens || 0);
202
+ if (tokenIncrement < 0) tokenIncrement = 0;
203
+
204
+ let costIncrement = currentCost - (todayState.last_session_cost || 0);
205
+ if (costIncrement < 0) costIncrement = 0;
206
+
207
+ // 更新今日数据
208
+ const newTodayTokens = (todayState.tokens || 0) + tokenIncrement;
209
+ const newTodayCost = (todayState.cost || 0) + costIncrement;
210
+
211
+ writeJsonFile(TODAY_STATE, {
212
+ date: todayDate,
213
+ tokens: newTodayTokens,
214
+ cost: newTodayCost,
215
+ last_session_tokens: currentSessionTokens,
216
+ last_session_cost: currentCost
217
+ });
218
+
219
+ // 计算总计
220
+ const totalTokens = (historyData.total_tokens_all || 0) + currentSessionTokens;
221
+ const totalCost = (historyData.total_cost || 0) + currentCost;
222
+
223
+ // 检查是否超过警报阈值
224
+ const isAlert = newTodayCost >= config.alert_daily;
225
+
226
+ // 构建输出
227
+ let output = '';
228
+
229
+ // 模型名称
230
+ if (isAlert) {
231
+ output += `\x1b[1;31m[${modelName}]\x1b[0m `;
232
+ } else {
233
+ output += `\x1b[0;90m[${modelName}]\x1b[0m `;
234
+ }
235
+
236
+ // 今日数据
237
+ if (newTodayTokens > 0 || newTodayCost > 0) {
238
+ output += `\x1b[${theme.TODAY}m今日:${formatNumber(newTodayTokens)}\x1b[0m `;
239
+ output += `\x1b[${theme.COST}m${formatCost(newTodayCost)}\x1b[0m `;
240
+ } else {
241
+ output += `\x1b[0;90m今日:0\x1b[0m \x1b[0;90m$0.00\x1b[0m `;
242
+ }
243
+
244
+ // 燃烧速度
245
+ if (config.show_burn_rate && burnRate > 0) {
246
+ output += `\x1b[${theme.BURN}m🔥${formatBurnRate(burnRate)}\x1b[0m `;
247
+ }
248
+
249
+ // 总计数据
250
+ output += `| \x1b[0;90m总计:${formatNumber(totalTokens)}\x1b[0m `;
251
+ output += `\x1b[0;90m${formatCost(totalCost)}\x1b[0m `;
252
+
253
+ // 输出
254
+ process.stdout.write(output);
255
+
256
+ } catch (e) {
257
+ // 出错时输出简单错误信息
258
+ process.stdout.write('[BurnYourMoney] Error');
259
+ }
260
+ });
261
+ }
262
+
263
+ // 运行
264
+ main();
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Burn Your Money - Token 历史统计脚本 (Node.js 版本)
4
+ * 从 stats-cache.json 读取历史数据,返回今日/本周/本月统计
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ // 文件路径
12
+ const STATS_CACHE = path.join(os.homedir(), '.claude', 'stats-cache.json');
13
+ const TODAY = new Date().toISOString().split('T')[0];
14
+ const CURRENT_MONTH = new Date().toISOString().slice(0, 7); // YYYY-MM
15
+
16
+ /**
17
+ * 获取本周一的日期
18
+ */
19
+ function getWeekStart() {
20
+ const now = new Date();
21
+ const dayOfWeek = now.getDay();
22
+ const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
23
+ const weekStart = new Date(now.setDate(diff));
24
+ return weekStart.toISOString().split('T')[0];
25
+ }
26
+
27
+ /**
28
+ * 获取本月第一天
29
+ */
30
+ function getMonthStart() {
31
+ const now = new Date();
32
+ return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
33
+ }
34
+
35
+ /**
36
+ * 获取上月第一天
37
+ */
38
+ function getLastMonthStart() {
39
+ const now = new Date();
40
+ return new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().split('T')[0];
41
+ }
42
+
43
+ /**
44
+ * 读取统计数据
45
+ */
46
+ function readStats() {
47
+ try {
48
+ if (!fs.existsSync(STATS_CACHE)) {
49
+ return {
50
+ today_tokens: 0,
51
+ today_cost: 0,
52
+ week_tokens: 0,
53
+ week_cost: 0,
54
+ month_tokens: 0,
55
+ month_cost: 0,
56
+ last_month_tokens: 0,
57
+ last_month_cost: 0,
58
+ total_tokens_all: 0,
59
+ total_cost: 0,
60
+ daily_data: []
61
+ };
62
+ }
63
+
64
+ const data = JSON.parse(fs.readFileSync(STATS_CACHE, 'utf8'));
65
+
66
+ // 计算所有模型的 input 和 output tokens
67
+ const totalInputTokens = Object.values(data.modelUsage || {}).reduce((sum, model) => sum + (model.inputTokens || 0), 0);
68
+ const totalOutputTokens = Object.values(data.modelUsage || {}).reduce((sum, model) => sum + (model.outputTokens || 0), 0);
69
+ const totalCacheReadTokens = Object.values(data.modelUsage || {}).reduce((sum, model) => sum + (model.cacheReadInputTokens || 0), 0);
70
+
71
+ // 计算今日 tokens(总计 - 历史每日总和,排除今天)
72
+ const dailyTokens = data.dailyModelTokens || [];
73
+ let historySum = 0;
74
+ dailyTokens.forEach(day => {
75
+ if (day.date !== TODAY) {
76
+ const models = day.tokensByModel || {};
77
+ historySum += Object.values(models).reduce((sum, tokens) => sum + (tokens || 0), 0);
78
+ }
79
+ });
80
+
81
+ const todayTokens = totalInputTokens + totalOutputTokens - historySum;
82
+ if (todayTokens < 0) todayTokens = 0;
83
+
84
+ // 本周 tokens(排除今天,用上面的 todayTokens)
85
+ const WEEK_START = getWeekStart();
86
+ let weekTokens = 0;
87
+ dailyTokens.forEach(day => {
88
+ if (day.date >= WEEK_START && day.date !== TODAY) {
89
+ const models = day.tokensByModel || {};
90
+ weekTokens += Object.values(models).reduce((sum, tokens) => sum + (tokens || 0), 0);
91
+ }
92
+ });
93
+ weekTokens += todayTokens;
94
+
95
+ // 本月 tokens
96
+ const MONTH_START = getMonthStart();
97
+ let monthTokens = 0;
98
+ dailyTokens.forEach(day => {
99
+ if (day.date >= MONTH_START && day.date !== TODAY) {
100
+ const models = day.tokensByModel || {};
101
+ monthTokens += Object.values(models).reduce((sum, tokens) => sum + (tokens || 0), 0);
102
+ }
103
+ });
104
+ monthTokens += todayTokens;
105
+
106
+ // 上月 tokens
107
+ const LAST_MONTH_START = getLastMonthStart();
108
+ let lastMonthTokens = 0;
109
+ dailyTokens.forEach(day => {
110
+ if (day.date >= LAST_MONTH_START && day.date < MONTH_START) {
111
+ const models = day.tokensByModel || {};
112
+ lastMonthTokens += Object.values(models).reduce((sum, tokens) => sum + (tokens || 0), 0);
113
+ }
114
+ });
115
+
116
+ // 计算费用(简化:按 3/15/0.3 规则)
117
+ const totalCost = (totalInputTokens / 1000000 * 3) +
118
+ (totalOutputTokens / 1000000 * 15) +
119
+ (totalCacheReadTokens / 1000000 * 0.3);
120
+
121
+ const totalTokensAll = totalInputTokens + totalOutputTokens + totalCacheReadTokens;
122
+
123
+ // 计算各周期费用(按比例分摊)
124
+ const todayCost = todayTokens > 0 ? (todayTokens / (totalInputTokens + totalOutputTokens) * totalCost) : 0;
125
+ const weekCost = weekTokens > 0 ? (weekTokens / (totalInputTokens + totalOutputTokens) * totalCost) : 0;
126
+ const monthCost = monthTokens > 0 ? (monthTokens / (totalInputTokens + totalOutputTokens) * totalCost) : 0;
127
+ const lastMonthCost = lastMonthTokens > 0 ? (lastMonthTokens / (totalInputTokens + totalOutputTokens) * totalCost) : 0;
128
+
129
+ // 获取每日数据(用于趋势图)
130
+ const dailyData = dailyTokens.slice(-7).map(day => ({
131
+ date: day.date,
132
+ tokens: Object.values(day.tokensByModel || {}).reduce((sum, tokens) => sum + (tokens || 0), 0)
133
+ }));
134
+
135
+ return {
136
+ today_tokens: todayTokens,
137
+ today_cost: todayCost.toFixed(2),
138
+ week_tokens: weekTokens,
139
+ week_cost: weekCost.toFixed(2),
140
+ month_tokens: monthTokens,
141
+ month_cost: monthCost.toFixed(2),
142
+ last_month_tokens: lastMonthTokens,
143
+ last_month_cost: lastMonthCost.toFixed(2),
144
+ total_tokens_all: totalTokensAll,
145
+ total_cost: totalCost.toFixed(2),
146
+ daily_data: dailyData
147
+ };
148
+ } catch (e) {
149
+ console.error('Error reading stats:', e.message);
150
+ return {
151
+ today_tokens: 0,
152
+ today_cost: '0.00',
153
+ week_tokens: 0,
154
+ week_cost: '0.00',
155
+ month_tokens: 0,
156
+ month_cost: '0.00',
157
+ last_month_tokens: 0,
158
+ last_month_cost: '0.00',
159
+ total_tokens_all: 0,
160
+ total_cost: '0.00',
161
+ daily_data: []
162
+ };
163
+ }
164
+ }
165
+
166
+ /**
167
+ * 主函数
168
+ */
169
+ function main() {
170
+ const mode = process.argv[2] || 'summary';
171
+
172
+ const stats = readStats();
173
+
174
+ switch (mode) {
175
+ case 'today_tokens':
176
+ console.log(stats.today_tokens);
177
+ break;
178
+ case 'today_cost':
179
+ console.log(stats.today_cost);
180
+ break;
181
+ case 'week_tokens':
182
+ console.log(stats.week_tokens);
183
+ break;
184
+ case 'week_cost':
185
+ console.log(stats.week_cost);
186
+ break;
187
+ case 'month_tokens':
188
+ console.log(stats.month_tokens);
189
+ break;
190
+ case 'month_cost':
191
+ console.log(stats.month_cost);
192
+ break;
193
+ case 'last_month_tokens':
194
+ console.log(stats.last_month_tokens);
195
+ break;
196
+ case 'last_month_cost':
197
+ console.log(stats.last_month_cost);
198
+ break;
199
+ case 'trend':
200
+ console.log(JSON.stringify(stats.daily_data));
201
+ break;
202
+ case 'export':
203
+ const format = process.argv[3] || 'json';
204
+ if (format === 'csv') {
205
+ console.log('date,tokens,cost');
206
+ stats.daily_data.forEach(day => {
207
+ const cost = (day.tokens * 0.000006).toFixed(2);
208
+ console.log(`${day.date},${day.tokens},${cost}`);
209
+ });
210
+ } else {
211
+ console.log(JSON.stringify(stats, null, 2));
212
+ }
213
+ break;
214
+ case 'chart':
215
+ // 生成简单的趋势图
216
+ const maxTokens = Math.max(...stats.daily_data.map(d => d.tokens), 1);
217
+ console.log('\n📊 Token 使用趋势 (最近 7 天)\n');
218
+ stats.daily_data.forEach(day => {
219
+ const date = day.date.slice(5); // MM-DD
220
+ const tokens = day.tokens;
221
+ const barLength = Math.round((tokens / maxTokens) * 30);
222
+ const bar = '█'.repeat(barLength);
223
+ console.log(` ${date} ${bar} ${tokens.toLocaleString()}`);
224
+ });
225
+ console.log('');
226
+ break;
227
+ default:
228
+ // summary
229
+ console.log(JSON.stringify(stats, null, 2));
230
+ }
231
+ }
232
+
233
+ // 运行
234
+ main();
package/uninstall.js CHANGED
@@ -30,9 +30,8 @@ function getHomeDir() {
30
30
  function uninstallFiles() {
31
31
  const home = getHomeDir();
32
32
  const files = [
33
- path.join(home, '.claude', 'statusline.sh'),
34
- path.join(home, '.claude', 'scripts', 'token-history.sh'),
35
- path.join(home, '.claude', 'statusline-wrapper.sh'),
33
+ path.join(home, '.claude', 'statusline.js'),
34
+ path.join(home, '.claude', 'scripts', 'token-history.js'),
36
35
  // 命令文件
37
36
  path.join(home, '.claude', 'commands', 'burn-your-money-stats.md'),
38
37
  path.join(home, '.claude', 'commands', 'burn-your-money-today.md'),