@ugurcandede/cc-cost 0.1.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.
package/dist/format.js ADDED
@@ -0,0 +1,33 @@
1
+ import { styleText } from 'node:util';
2
+ let color = !!process.stdout.isTTY && !process.env.NO_COLOR;
3
+ export const setColor = (on) => {
4
+ color = on && !!process.stdout.isTTY && !process.env.NO_COLOR;
5
+ };
6
+ export const paint = (style, text) => (color ? styleText(style, text) : text);
7
+ // Separators follow en-US in every language: '.' as thousands separator next to '$' reads as decimals.
8
+ export const usd = (n) => '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
9
+ export const int = (n) => Math.round(n).toLocaleString('en-US');
10
+ export const pct = (part, whole) => (whole ? ((part / whole) * 100).toFixed(1) : '0.0') + '%';
11
+ export function tokens(n) {
12
+ if (n >= 1e9)
13
+ return (n / 1e9).toFixed(2) + 'B';
14
+ if (n >= 1e6)
15
+ return (n / 1e6).toFixed(1) + 'M';
16
+ if (n >= 1e3)
17
+ return (n / 1e3).toFixed(1) + 'K';
18
+ return String(Math.round(n));
19
+ }
20
+ // Plain-text table. Columns after the first are right-aligned unless `left` says otherwise;
21
+ // styling is applied after padding so ANSI codes don't skew the widths.
22
+ export function table(head, body, opts = {}) {
23
+ const all = [head, ...body, ...(opts.foot ? [opts.foot] : [])];
24
+ const width = head.map((_, i) => Math.max(...all.map((r) => (r[i] ?? '').length)));
25
+ const left = new Set([0, ...(opts.left ?? [])]);
26
+ const line = (r) => r.map((cell, i) => (left.has(i) ? cell.padEnd(width[i]) : cell.padStart(width[i]))).join(' ').trimEnd();
27
+ const rule = width.map((w) => '─'.repeat(w)).join(' ');
28
+ const out = [paint('bold', line(head)), paint('dim', rule)];
29
+ body.forEach((r, i) => out.push(opts.dim?.has(i) ? paint('dim', line(r)) : line(r)));
30
+ if (opts.foot)
31
+ out.push(paint('dim', rule), paint('bold', line(opts.foot)));
32
+ return out.join('\n');
33
+ }
package/dist/i18n.js ADDED
@@ -0,0 +1,415 @@
1
+ // All user-facing text. English is the default everywhere; `tr` must mirror every key of `en`.
2
+ const en = {
3
+ help: `cc-cost {version}: API-equivalent cost of your Claude Code usage, across all your machines
4
+
5
+ Usage: cc-cost [command] [options]
6
+
7
+ Commands:
8
+ (none) Sync this machine, print a summary, update the dashboard
9
+ sync Sync this machine and update the dashboard
10
+ daily | weekly | monthly Cost per day, week (starting Monday) or month
11
+ models | machines | projects Cost per model, machine or project
12
+ agents | skills | mcp Cost by main thread vs subagents, by skill, by MCP server
13
+ sessions Most expensive sessions
14
+ blocks Usage in 5-hour windows
15
+ insights Where the money goes and what drives it
16
+ plan API equivalent vs subscription prices, rate-limit hits
17
+ report Write the HTML dashboard (--open to open it)
18
+ pricing Prices in use (--refresh to fetch them again)
19
+ setup Pick the shared folder, schedule daily runs, add the Claude Code hook
20
+ status Settings, machines, last sync, scheduler and hook
21
+ config Show settings; "config set <key> <value>" changes one
22
+
23
+ Filters:
24
+ --since <date> From YYYY-MM-DD
25
+ --until <date> Until YYYY-MM-DD, inclusive
26
+ --last <n> Last n calendar days, including today
27
+ --machine <name> Only these machines (comma-separated, partial match)
28
+ --project <name> Only these projects
29
+ --model <name> Only these models, e.g. opus or fable
30
+ --session <id> Only these sessions (id prefix)
31
+ --agent <name> main, or a subagent type such as Explore
32
+ --skill <name> Only calls attributed to these skills
33
+ --mcp <name> Only calls attributed to these MCP servers
34
+
35
+ Output:
36
+ --json JSON
37
+ --csv CSV (tables only)
38
+ --breakdown Per-model rows under each period
39
+ --limit <n> Rows in sessions, projects and blocks (default 20)
40
+ --lang <en|tr> Language
41
+ --tz <zone> IANA time zone for day boundaries
42
+ --no-color Plain text
43
+ --no-sync Report from stored data without scanning transcripts
44
+ --offline Don't fetch live prices
45
+ --quiet Print nothing on success (for schedulers and hooks)
46
+ --sync-dir <path> Folder shared between machines
47
+ -h, --help This help
48
+ -v, --version Version
49
+
50
+ Setup:
51
+ --yes Accept the defaults without asking
52
+ --no-schedule Don't schedule daily runs
53
+ --no-hook Don't add the Claude Code hook
54
+ --remove Remove the schedule and the hook
55
+
56
+ Docs and issues: {url}`,
57
+ scanned: '{machine}: scanned {files} transcripts ({dupes} duplicate lines skipped)',
58
+ snapshot: 'snapshot: {file}',
59
+ kept: ' (+{n} archived days kept)',
60
+ total: 'TOTAL {cost} · {calls} API calls · {machines} machines',
61
+ dashboard: 'dashboard: {file}',
62
+ unknownModels: 'No price for (left out of totals): {list}',
63
+ pricesLive: 'prices: {source}, fetched {date}',
64
+ pricesFallback: 'live prices unavailable ({reason}); using {source} prices from {date}',
65
+ noData: 'No usage in this range.',
66
+ configFile: 'config: {file}',
67
+ configSet: '{key} = {value}',
68
+ configUnknownKey: 'Unknown setting "{key}". Settings: {keys}',
69
+ unknownCommand: 'Unknown command "{cmd}". Run cc-cost --help.',
70
+ badDate: '{flag} must be YYYY-MM-DD, got "{value}"',
71
+ badNumber: '{flag} must be a positive number, got "{value}"',
72
+ csvUnsupported: '--csv works with table commands only',
73
+ hourFilters: 'note: --project, --session, --agent, --skill and --mcp do not apply to hourly data',
74
+ col: {
75
+ date: 'Date', week: 'Week', month: 'Month', model: 'Model', machine: 'Machine', project: 'Project',
76
+ session: 'Session', input: 'Input', output: 'Output', cacheWrite: 'Cache write', cacheRead: 'Cache read',
77
+ calls: 'Calls', cost: 'Cost', share: 'Share', first: 'First', last: 'Last', avgContext: 'Avg context',
78
+ item: 'Item', total: 'Total', price: 'Input / Write 5m / Write 1h / Read / Output ($/MTok)',
79
+ agent: 'Agent', skill: 'Skill', mcp: 'MCP server', effort: 'Effort', context: 'Context', readCost: 'Cache read $',
80
+ days: 'Days', perMonth: 'Per 30 days', limitHits: 'Limit hits', start: 'Start', end: 'End', tokens: 'Tokens',
81
+ state: 'State',
82
+ },
83
+ item: { read: 'cache read', write: 'cache write', output: 'output', input: 'uncached input', web: 'web search' },
84
+ ins: {
85
+ overview: 'Overview',
86
+ cost: 'Cost',
87
+ calls: 'API calls',
88
+ perCall: '{cost} per call',
89
+ activeDays: 'Active days',
90
+ activeOf: '{active} of {calendar} calendar days',
91
+ avgContext: 'Average context',
92
+ avgContextValue: '{avg} tokens per call (largest {max})',
93
+ cache: 'Cache',
94
+ hitRatio: 'Hit ratio',
95
+ hitRatioValue: '{pct} of prompt tokens came from cache',
96
+ readShare: 'Cache reads',
97
+ writeShare: 'Cache writes',
98
+ ofCost: '{pct} of cost',
99
+ oneHour: '{pct} of cost (1-hour tier: {tier})',
100
+ contextSize: 'Context size',
101
+ above200k: 'Cache reads beyond 200K context cost about {cost} (rough estimate).',
102
+ contextTip: 'Every call re-reads the whole conversation. /clear between unrelated tasks and /compact in long sessions keep it small.',
103
+ bigSessions: 'Largest-context sessions',
104
+ agents: 'Main thread vs subagents',
105
+ skills: 'Skills',
106
+ mcp: 'MCP servers',
107
+ effort: 'Effort',
108
+ models: 'Models',
109
+ whatIf: 'The same tokens on {model}: {cost}',
110
+ fast: 'Fast mode',
111
+ thinking: 'Thinking',
112
+ thinkingValue: '{pct} of output tokens',
113
+ when: 'When',
114
+ weekday: 'Busiest weekday',
115
+ hour: 'Busiest hour',
116
+ limits: 'Rate limits',
117
+ limitsValue: 'limit hits: {n} ({types}), last {last}',
118
+ noLimits: 'no hits recorded',
119
+ bucket: ['under 50K', '50K to 200K', '200K to 500K', '500K and over'],
120
+ },
121
+ plan: {
122
+ note: 'Anthropic does not publish plan limits in tokens, so no tool can tell which plan would have been enough. Rate-limit hits are the direct signal.',
123
+ yours: 'Your plan ({plan}, ${price}/month): over this range the API equivalent is {multiple}× its price.',
124
+ noPlan: 'Set your plan with: cc-cost config set plan max5x (pro, max5x, max20x)',
125
+ },
126
+ blocks: {
127
+ active: 'active, {left} left',
128
+ },
129
+ setup: {
130
+ title: 'cc-cost {version} setup',
131
+ found: 'Shared folders found: {list}',
132
+ usePath: 'Keep usage data in {path}? [Y/n] ',
133
+ askPath: 'Folder shared between your machines (empty: this machine only): ',
134
+ localOnly: 'No shared folder: only this machine will be counted.',
135
+ lang: 'Language [en/tr] ({current}): ',
136
+ plan: 'Your plan [pro/max5x/max20x] ({current}): ',
137
+ schedule: 'Sync automatically every day at {time}? [Y/n] ',
138
+ hook: 'Also sync when a Claude Code session ends (adds a SessionEnd hook to {file})? [Y/n] ',
139
+ saved: 'saved {file}',
140
+ scheduled: 'scheduled: {what}',
141
+ scheduleFailed: 'could not schedule daily runs: {reason}',
142
+ hookAdded: 'hook added to {file}',
143
+ hookKept: 'hook already in {file}',
144
+ npx: 'cc-cost is running from a temporary npx or dlx folder, so a scheduler or hook would point at a path that disappears. Install it first: npm i -g @ugurcandede/cc-cost (Yarn 1: yarn global add @ugurcandede/cc-cost)',
145
+ removed: 'removed: {what}',
146
+ nothingToRemove: 'nothing to remove',
147
+ firstSync: 'First sync:',
148
+ },
149
+ status: {
150
+ version: 'version',
151
+ config: 'config file',
152
+ syncDir: 'shared folder',
153
+ machine: 'this machine',
154
+ machines: 'machines',
155
+ machineLine: '{name}: updated {updated} · days with data: {days}',
156
+ retention: 'transcripts kept',
157
+ retentionValue: '{n} days (cleanupPeriodDays)',
158
+ scheduler: 'daily schedule',
159
+ hook: 'SessionEnd hook',
160
+ prices: 'prices',
161
+ yes: 'installed',
162
+ no: 'not installed',
163
+ none: 'none yet',
164
+ },
165
+ dash: {
166
+ title: 'Claude Code: API equivalent',
167
+ timeRange: 'Time range',
168
+ today: 'Today', last7: 'Last 7 days', last30: 'Last 30 days', last90: 'Last 90 days', all: 'All',
169
+ from: 'From', to: 'To', clear: 'Clear',
170
+ machine: 'Machine', allMachines: 'All machines',
171
+ project: 'Project', allProjects: 'All projects',
172
+ model: 'Model', allModels: 'All models',
173
+ groupBy: 'Chart by', byModelOpt: 'Model', byProjectOpt: 'Project', byMachineOpt: 'Machine',
174
+ other: 'other',
175
+ heroNote: 'API list-price equivalent. Not billed on a subscription.',
176
+ dailyAvg: 'Daily average', activeDays: '{n} active days',
177
+ planMultiple: '× the {price} plan', normalized: '{n} calendar days, normalized to 30',
178
+ apiCalls: 'API calls', perCall: '{cost} / call', machines: 'Machines',
179
+ cacheHit: 'Cache hit ratio', cacheHitNote: 'of prompt tokens',
180
+ avgContext: 'Average context', avgContextNote: 'tokens per call',
181
+ dailyCost: 'Daily cost', dailyCostSub: 'Stacked. Hover or focus a day for its breakdown.',
182
+ chartLabel: 'Daily cost chart',
183
+ whereCost: 'Where the cost goes',
184
+ whereCostSub: 'By token type. Cache read is usually the largest item; without caching the same input would bill at 10×.',
185
+ itemsLabel: 'Cost by token type',
186
+ cacheRead: 'Cache read', cacheWrite: 'Cache write', output: 'Output', input: 'Uncached input', web: 'Web search',
187
+ contextTitle: 'Context size per call', contextSub: 'Calls by how many tokens they re-read. Bigger contexts cost more on every call.',
188
+ buckets: ['< 50K', '50K–200K', '200K–500K', '≥ 500K'],
189
+ byModel: 'By model', byMachine: 'By machine', byProject: 'By project',
190
+ sessions: 'Most expensive sessions', sessionsSub: 'Top 15 in the selected range.',
191
+ attribution: 'Attribution', agents: 'Main thread vs subagents', skills: 'Skills', mcp: 'MCP servers',
192
+ main: 'main thread', none: '(none)', agentCol: 'Agent', skillCol: 'Skill', mcpCol: 'Server',
193
+ limits: 'Rate-limit hits', limitsNone: 'None in this range.',
194
+ cost: 'Cost', share: 'Share', calls: 'Calls', session: 'Session', first: 'First', last: 'Last', avgCtx: 'Avg context',
195
+ allRecords: 'All records ({n} days)', lastDays: 'Last {n} days',
196
+ empty: 'No data in this range. Records cover {min} to {max}.',
197
+ total: 'Total',
198
+ note: 'Updated {updated} · {machines} machines · records {min} to {max} · prices: {prices}',
199
+ language: 'Language',
200
+ footer: 'Generated by cc-cost {version}',
201
+ website: 'Website',
202
+ },
203
+ };
204
+ const tr = {
205
+ help: `cc-cost {version}: Claude Code kullanımının API karşılığı, tüm makinelerin toplamı
206
+
207
+ Kullanım: cc-cost [komut] [seçenekler]
208
+
209
+ Komutlar:
210
+ (yok) Bu makineyi senkronla, özet bas, dashboard'u güncelle
211
+ sync Bu makineyi senkronla, dashboard'u güncelle
212
+ daily | weekly | monthly Günlük, haftalık (pazartesi başlar) veya aylık maliyet
213
+ models | machines | projects Model, makine veya projeye göre maliyet
214
+ agents | skills | mcp Ana akış / subagent, skill ve MCP sunucusuna göre maliyet
215
+ sessions En pahalı session'lar
216
+ blocks 5 saatlik pencerelerde kullanım
217
+ insights Para nereye gidiyor, neyden kaynaklanıyor
218
+ plan API karşılığı ve abonelik fiyatları, limit aşımları
219
+ report HTML dashboard'u yaz (--open ile aç)
220
+ pricing Kullanılan fiyatlar (--refresh ile yeniden çek)
221
+ setup Paylaşılan klasörü seç, günlük çalışmayı zamanla, Claude Code hook'unu ekle
222
+ status Ayarlar, makineler, son senkron, zamanlayıcı ve hook
223
+ config Ayarları göster; "config set <anahtar> <değer>" ile değiştir
224
+
225
+ Filtreler:
226
+ --since <tarih> YYYY-MM-DD'den itibaren
227
+ --until <tarih> YYYY-MM-DD'ye kadar, dahil
228
+ --last <n> Bugün dahil son n takvim günü
229
+ --machine <ad> Sadece bu makineler (virgülle, kısmi eşleşme)
230
+ --project <ad> Sadece bu projeler
231
+ --model <ad> Sadece bu modeller, ör. opus veya fable
232
+ --session <id> Sadece bu session'lar (id başı)
233
+ --agent <ad> main veya Explore gibi bir subagent türü
234
+ --skill <ad> Sadece bu skill'lere atfedilen çağrılar
235
+ --mcp <ad> Sadece bu MCP sunucularına atfedilen çağrılar
236
+
237
+ Çıktı:
238
+ --json JSON
239
+ --csv CSV (sadece tablolar)
240
+ --breakdown Her dönemin altında model satırları
241
+ --limit <n> sessions, projects ve blocks'ta satır sayısı (varsayılan 20)
242
+ --lang <en|tr> Dil
243
+ --tz <bölge> Gün sınırı için IANA saat dilimi
244
+ --no-color Düz metin
245
+ --no-sync Transcript taramadan, kayıtlı veriden raporla
246
+ --offline Canlı fiyat çekme
247
+ --quiet Başarıda hiçbir şey basma (zamanlayıcı ve hook için)
248
+ --sync-dir <yol> Makineler arası paylaşılan klasör
249
+ -h, --help Bu yardım
250
+ -v, --version Sürüm
251
+
252
+ Kurulum:
253
+ --yes Sormadan varsayılanları kabul et
254
+ --no-schedule Günlük çalışmayı zamanlama
255
+ --no-hook Claude Code hook'unu ekleme
256
+ --remove Zamanlamayı ve hook'u kaldır
257
+
258
+ Dokümantasyon ve hata bildirimi: {url}`,
259
+ scanned: '{machine}: {files} transcript tarandı ({dupes} tekrar satır atlandı)',
260
+ snapshot: 'snapshot: {file}',
261
+ kept: ' (+{n} arşiv gün korundu)',
262
+ total: 'TOPLAM {cost} · {calls} API çağrısı · {machines} makine',
263
+ dashboard: 'dashboard: {file}',
264
+ unknownModels: 'Fiyatı bilinmeyen (toplama girmedi): {list}',
265
+ pricesLive: 'fiyatlar: {source}, çekildi {date}',
266
+ pricesFallback: 'canlı fiyatlar alınamadı ({reason}); {source} fiyatları kullanılıyor ({date})',
267
+ noData: 'Bu aralıkta kullanım yok.',
268
+ configFile: 'config: {file}',
269
+ configSet: '{key} = {value}',
270
+ configUnknownKey: 'Bilinmeyen ayar "{key}". Ayarlar: {keys}',
271
+ unknownCommand: 'Bilinmeyen komut "{cmd}". cc-cost --help ile bak.',
272
+ badDate: '{flag} YYYY-MM-DD olmalı, gelen: "{value}"',
273
+ badNumber: '{flag} pozitif bir sayı olmalı, gelen: "{value}"',
274
+ csvUnsupported: '--csv sadece tablo komutlarıyla çalışır',
275
+ hourFilters: 'not: --project, --session, --agent, --skill ve --mcp saatlik veriye uygulanmaz',
276
+ col: {
277
+ date: 'Tarih', week: 'Hafta', month: 'Ay', model: 'Model', machine: 'Makine', project: 'Proje',
278
+ session: 'Session', input: 'Girdi', output: 'Çıktı', cacheWrite: 'Cache yazma', cacheRead: 'Cache okuma',
279
+ calls: 'Çağrı', cost: 'Maliyet', share: 'Pay', first: 'İlk', last: 'Son', avgContext: 'Ort. context',
280
+ item: 'Kalem', total: 'Toplam', price: 'Girdi / Yazma 5dk / Yazma 1sa / Okuma / Çıktı ($/MTok)',
281
+ agent: 'Agent', skill: 'Skill', mcp: 'MCP sunucusu', effort: 'Effort', context: 'Context', readCost: 'Cache okuma $',
282
+ days: 'Gün', perMonth: '30 günde', limitHits: 'Limit aşımı', start: 'Başlangıç', end: 'Bitiş', tokens: 'Token',
283
+ state: 'Durum',
284
+ },
285
+ item: { read: 'cache okuma', write: 'cache yazma', output: 'çıktı', input: 'cache\'siz girdi', web: 'web arama' },
286
+ ins: {
287
+ overview: 'Genel',
288
+ cost: 'Maliyet',
289
+ calls: 'API çağrısı',
290
+ perCall: 'çağrı başına {cost}',
291
+ activeDays: 'Aktif gün',
292
+ activeOf: '{calendar} takvim gününün {active} günü',
293
+ avgContext: 'Ortalama context',
294
+ avgContextValue: 'çağrı başına {avg} token (en büyük {max})',
295
+ cache: 'Cache',
296
+ hitRatio: 'İsabet oranı',
297
+ hitRatioValue: '{pct} (prompt token\'larının cache\'ten okunan kısmı)',
298
+ readShare: 'Cache okuma',
299
+ writeShare: 'Cache yazma',
300
+ ofCost: '{pct} (maliyet payı)',
301
+ oneHour: '{pct} (maliyet payı; 1 saatlik katman: {tier})',
302
+ contextSize: 'Context boyutu',
303
+ above200k: '200K\'nın üzerindeki context\'in cache okuma maliyeti yaklaşık {cost} (kaba tahmin).',
304
+ contextTip: 'Her çağrı tüm konuşmayı yeniden okur. Alakasız işler arasında /clear, uzun session\'larda /compact context\'i küçük tutar.',
305
+ bigSessions: 'En büyük context\'li session\'lar',
306
+ agents: 'Ana akış ve subagent\'lar',
307
+ skills: 'Skill\'ler',
308
+ mcp: 'MCP sunucuları',
309
+ effort: 'Effort',
310
+ models: 'Modeller',
311
+ whatIf: 'Aynı token\'lar {model} ile: {cost}',
312
+ fast: 'Fast mode',
313
+ thinking: 'Thinking',
314
+ thinkingValue: '{pct} (çıktı token\'ları içindeki payı)',
315
+ when: 'Ne zaman',
316
+ weekday: 'En yoğun gün',
317
+ hour: 'En yoğun saat',
318
+ limits: 'Limitler',
319
+ limitsValue: 'limit aşımı: {n} ({types}), son {last}',
320
+ noLimits: 'kayıtlı aşım yok',
321
+ bucket: ['50K altı', '50K – 200K', '200K – 500K', '500K ve üstü'],
322
+ },
323
+ plan: {
324
+ note: 'Anthropic plan limitlerini token cinsinden yayınlamıyor; hangi planın yeteceğini hiçbir araç söyleyemez. Doğrudan sinyal limit aşımları.',
325
+ yours: 'Planın ({plan}, aylık ${price}): bu aralıktaki API karşılığı, plan fiyatının {multiple} katı.',
326
+ noPlan: 'Planını ayarla: cc-cost config set plan max5x (pro, max5x, max20x)',
327
+ },
328
+ blocks: {
329
+ active: 'aktif, {left} kaldı',
330
+ },
331
+ setup: {
332
+ title: 'cc-cost {version} kurulumu',
333
+ found: 'Bulunan paylaşılan klasörler: {list}',
334
+ usePath: 'Kullanım verisi {path} içinde tutulsun mu? [E/h] ',
335
+ askPath: 'Makineler arası paylaşılan klasör (boş: sadece bu makine): ',
336
+ localOnly: 'Paylaşılan klasör yok: sadece bu makine sayılacak.',
337
+ lang: 'Dil [en/tr] ({current}): ',
338
+ plan: 'Planın [pro/max5x/max20x] ({current}): ',
339
+ schedule: 'Her gün {time}\'de otomatik senkronlansın mı? [E/h] ',
340
+ hook: 'Claude Code session\'ı bitince de senkronlansın mı ({file} dosyasına SessionEnd hook\'u eklenir)? [E/h] ',
341
+ saved: 'kaydedildi: {file}',
342
+ scheduled: 'zamanlandı: {what}',
343
+ scheduleFailed: 'günlük çalışma zamanlanamadı: {reason}',
344
+ hookAdded: 'hook eklendi: {file}',
345
+ hookKept: 'hook zaten var: {file}',
346
+ npx: 'cc-cost geçici bir npx ya da dlx klasöründen çalışıyor; zamanlayıcı ve hook silinecek bir yolu gösterir. Önce kur: npm i -g @ugurcandede/cc-cost (Yarn 1: yarn global add @ugurcandede/cc-cost)',
347
+ removed: 'kaldırıldı: {what}',
348
+ nothingToRemove: 'kaldırılacak bir şey yok',
349
+ firstSync: 'İlk senkron:',
350
+ },
351
+ status: {
352
+ version: 'sürüm',
353
+ config: 'config dosyası',
354
+ syncDir: 'paylaşılan klasör',
355
+ machine: 'bu makine',
356
+ machines: 'makineler',
357
+ machineLine: '{name}: güncellendi {updated} · veri olan gün: {days}',
358
+ retention: 'transcript saklama',
359
+ retentionValue: '{n} gün (cleanupPeriodDays)',
360
+ scheduler: 'günlük zamanlama',
361
+ hook: 'SessionEnd hook',
362
+ prices: 'fiyatlar',
363
+ yes: 'kurulu',
364
+ no: 'kurulu değil',
365
+ none: 'henüz yok',
366
+ },
367
+ dash: {
368
+ title: 'Claude Code: API karşılığı',
369
+ timeRange: 'Zaman aralığı',
370
+ today: 'Bugün', last7: 'Son 7 gün', last30: 'Son 30 gün', last90: 'Son 90 gün', all: 'Tümü',
371
+ from: 'Başlangıç', to: 'Bitiş', clear: 'Temizle',
372
+ machine: 'Makine', allMachines: 'Tüm makineler',
373
+ project: 'Proje', allProjects: 'Tüm projeler',
374
+ model: 'Model', allModels: 'Tüm modeller',
375
+ groupBy: 'Grafik', byModelOpt: 'Model', byProjectOpt: 'Proje', byMachineOpt: 'Makine',
376
+ other: 'diğer',
377
+ heroNote: 'API liste fiyatı karşılığı. Abonelikte bu tutar faturalanmıyor.',
378
+ dailyAvg: 'Günlük ortalama', activeDays: '{n} aktif gün',
379
+ planMultiple: '{price} planın katı', normalized: '{n} takvim günü, 30 güne normalize',
380
+ apiCalls: 'API çağrısı', perCall: '{cost} / çağrı', machines: 'Makine',
381
+ cacheHit: 'Cache isabeti', cacheHitNote: 'prompt token\'larında',
382
+ avgContext: 'Ortalama context', avgContextNote: 'çağrı başına token',
383
+ dailyCost: 'Günlük maliyet', dailyCostSub: 'Yığılmış. Bir güne gel, o günün dökümünü gör.',
384
+ chartLabel: 'Günlük maliyet grafiği',
385
+ whereCost: 'Maliyet nereye gidiyor',
386
+ whereCostSub: 'Token türüne göre. En büyük kalem genelde cache okuma; cache olmasa aynı girdi 10 katına faturalanırdı.',
387
+ itemsLabel: 'Token türüne göre maliyet',
388
+ cacheRead: 'Cache okuma', cacheWrite: 'Cache yazma', output: 'Çıktı', input: 'Cache\'siz girdi', web: 'Web arama',
389
+ contextTitle: 'Çağrı başına context', contextSub: 'Çağrıların yeniden okuduğu token miktarına göre dağılımı. Context büyüdükçe her çağrı pahalılaşır.',
390
+ buckets: ['< 50K', '50K–200K', '200K–500K', '≥ 500K'],
391
+ byModel: 'Model dökümü', byMachine: 'Makine dökümü', byProject: 'Proje dökümü',
392
+ sessions: 'En pahalı session\'lar', sessionsSub: 'Seçili aralıktaki ilk 15.',
393
+ attribution: 'Atıf', agents: 'Ana akış ve subagent\'lar', skills: 'Skill\'ler', mcp: 'MCP sunucuları',
394
+ main: 'ana akış', none: '(yok)', agentCol: 'Agent', skillCol: 'Skill', mcpCol: 'Sunucu',
395
+ limits: 'Limit aşımları', limitsNone: 'Bu aralıkta yok.',
396
+ cost: 'Maliyet', share: 'Pay', calls: 'Çağrı', session: 'Session', first: 'İlk', last: 'Son', avgCtx: 'Ort. context',
397
+ allRecords: 'Tüm kayıtlar ({n} gün)', lastDays: 'Son {n} gün',
398
+ empty: 'Bu aralıkta veri yok. Kayıtlar {min} ile {max} arasını kapsıyor.',
399
+ total: 'Toplam',
400
+ note: 'Güncellendi {updated} · {machines} makine · kayıtlar {min} – {max} · fiyatlar: {prices}',
401
+ language: 'Dil',
402
+ footer: 'cc-cost {version} ile oluşturuldu',
403
+ website: 'Web sitesi',
404
+ },
405
+ };
406
+ export const LANGS = { en, tr };
407
+ let current = en;
408
+ let currentLang = 'en';
409
+ export const setLang = (lang) => {
410
+ currentLang = LANGS[lang] ? lang : 'en';
411
+ current = LANGS[currentLang];
412
+ };
413
+ export const L = () => current;
414
+ export const lang = () => currentLang;
415
+ export const fill = (text, vars = {}) => text.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? `{${k}}`));
package/dist/paths.js ADDED
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ const HOME = os.homedir();
5
+ const XDG_CONFIG = process.env.XDG_CONFIG_HOME || path.join(HOME, '.config');
6
+ // Claude Code data roots. CLAUDE_CONFIG_DIR may list several, comma-separated.
7
+ export function claudeDirs() {
8
+ const env = process.env.CLAUDE_CONFIG_DIR;
9
+ const dirs = env
10
+ ? env.split(',').map((s) => s.trim()).filter(Boolean)
11
+ : [path.join(HOME, '.claude'), path.join(XDG_CONFIG, 'claude')];
12
+ return dirs.filter((d) => fs.existsSync(path.join(d, 'projects')));
13
+ }
14
+ export function configDir() {
15
+ if (process.platform === 'win32')
16
+ return path.join(process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'), 'cc-cost');
17
+ return path.join(XDG_CONFIG, 'cc-cost');
18
+ }
19
+ // Claude Code deletes transcripts older than cleanupPeriodDays (default 30). With several roots,
20
+ // the shortest window wins: a day older than that may have been partially cleaned up somewhere.
21
+ export function retentionDays(dirs) {
22
+ let min = Infinity;
23
+ for (const dir of dirs) {
24
+ let days = 30;
25
+ const file = path.join(dir, 'settings.json');
26
+ try {
27
+ const v = JSON.parse(fs.readFileSync(file, 'utf8')).cleanupPeriodDays;
28
+ if (typeof v === 'number' && v > 0)
29
+ days = v;
30
+ }
31
+ catch {
32
+ // missing or unreadable settings: Claude Code falls back to its default too
33
+ }
34
+ min = Math.min(min, days);
35
+ }
36
+ return min === Infinity ? 30 : min;
37
+ }
38
+ export const defaultMachine = () => os.hostname().replace(/[^A-Za-z0-9._-]/g, '_');
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const PRICING_URL = 'https://platform.claude.com/docs/en/about-claude/pricing.md';
4
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
5
+ // Fallback when the live page can't be fetched or parsed. Checked against PRICING_URL on 2026-09-18.
6
+ export const BUNDLED = {
7
+ date: '2026-09-18',
8
+ source: 'bundled',
9
+ models: {
10
+ 'claude-fable-5-1': [10, 12.5, 20, 0.25, 50],
11
+ 'claude-mythos-5-1': [10, 12.5, 20, 0.25, 50],
12
+ 'claude-fable-5': [10, 12.5, 20, 1, 50],
13
+ 'claude-mythos-5': [10, 12.5, 20, 1, 50],
14
+ 'claude-opus-5': [5, 6.25, 10, 0.5, 25],
15
+ 'claude-opus-4-8': [5, 6.25, 10, 0.5, 25],
16
+ 'claude-opus-4-7': [5, 6.25, 10, 0.5, 25],
17
+ 'claude-opus-4-6': [5, 6.25, 10, 0.5, 25],
18
+ 'claude-opus-4-5': [5, 6.25, 10, 0.5, 25],
19
+ 'claude-opus-4-1': [15, 18.75, 30, 1.5, 75],
20
+ 'claude-opus-4': [15, 18.75, 30, 1.5, 75],
21
+ 'claude-sonnet-5': [2, 2.5, 4, 0.2, 10],
22
+ 'claude-sonnet-4-6': [3, 3.75, 6, 0.3, 15],
23
+ 'claude-sonnet-4-5': [3, 3.75, 6, 0.3, 15],
24
+ 'claude-sonnet-4': [3, 3.75, 6, 0.3, 15],
25
+ 'claude-haiku-4-5': [1, 1.25, 2, 0.1, 5],
26
+ 'claude-3-5-haiku': [0.8, 1, 1.6, 0.08, 4],
27
+ },
28
+ fast: {
29
+ 'claude-opus-5': [10, 12.5, 20, 1, 50],
30
+ 'claude-opus-4-8': [10, 12.5, 20, 1, 50],
31
+ },
32
+ webSearch: 0.01,
33
+ };
34
+ export const normModel = (m) => m.replace(/\[1m\]$/, '').replace(/-20\d{6}$/, '');
35
+ // "Claude Opus 4.8" -> claude-opus-4-8, "Claude Haiku 3.5 (retired...)" -> claude-3-5-haiku
36
+ export function modelId(name) {
37
+ const clean = name.replace(/<[^>]*>/g, '').replace(/\(.*$/, '').trim().toLowerCase();
38
+ const m = clean.match(/^claude ([a-z]+) (\d+)(?:\.(\d+))?$/);
39
+ if (!m)
40
+ return undefined;
41
+ const [, family, major, minor] = m;
42
+ const ver = minor ? `${major}-${minor}` : major;
43
+ return Number(major) < 4 ? `claude-${ver}-${family}` : `claude-${family}-${ver}`;
44
+ }
45
+ const money = (cell) => {
46
+ const m = cell.match(/\$([\d.,]+)\s*\/\s*MTok/);
47
+ return m ? Number(m[1].replace(/,/g, '')) : NaN;
48
+ };
49
+ function section(md, heading) {
50
+ const start = md.search(heading);
51
+ if (start < 0)
52
+ return '';
53
+ const rest = md.slice(start + 1);
54
+ const end = rest.search(/\n#{2,3} /);
55
+ return end < 0 ? rest : rest.slice(0, end);
56
+ }
57
+ function tableRows(text) {
58
+ return text
59
+ .split('\n')
60
+ .filter((l) => l.trim().startsWith('|'))
61
+ .map((l) => l.trim().slice(1, -1).split('|').map((c) => c.trim()));
62
+ }
63
+ export function parsePricingPage(md, date) {
64
+ const models = {};
65
+ for (const cells of tableRows(section(md, /^## Model pricing/m))) {
66
+ const id = cells.length === 6 && modelId(cells[0]);
67
+ const v = cells.slice(1).map(money);
68
+ if (id && v.every(Number.isFinite))
69
+ models[id] = v;
70
+ }
71
+ // A page redesign that breaks the table shouldn't silently leave us with a handful of models.
72
+ if (Object.keys(models).length < 5)
73
+ return undefined;
74
+ const fast = {};
75
+ for (const cells of tableRows(section(md, /^### Fast mode pricing/m))) {
76
+ if (cells.length !== 3)
77
+ continue;
78
+ const input = money(cells[1]), output = money(cells[2]);
79
+ if (!Number.isFinite(input) || !Number.isFinite(output))
80
+ continue;
81
+ for (const name of cells[0].split(' / ')) {
82
+ const id = modelId(name), base = id && models[id];
83
+ // cache multipliers apply on top of the fast input rate, same ratios as the base model
84
+ if (base)
85
+ fast[id] = [input, (base[1] * input) / base[0], (base[2] * input) / base[0], (base[3] * input) / base[0], output];
86
+ }
87
+ }
88
+ const search = md.match(/\$([\d.]+) per 1,000 searches/);
89
+ const webSearch = search ? Number(search[1]) / 1000 : BUNDLED.webSearch;
90
+ return { date, source: PRICING_URL, models, fast, webSearch };
91
+ }
92
+ // Live prices over bundled ones: the page drops retired models, old usage still needs them.
93
+ const withFallback = (t) => ({
94
+ ...t,
95
+ models: { ...BUNDLED.models, ...t.models },
96
+ fast: { ...BUNDLED.fast, ...t.fast },
97
+ });
98
+ export async function loadPrices(cacheFile, opts = {}) {
99
+ let cached;
100
+ try {
101
+ cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
102
+ }
103
+ catch {
104
+ // no cache yet
105
+ }
106
+ const fresh = cached && Date.now() - Date.parse(cached.date) < MAX_AGE_MS;
107
+ if (cached && (opts.offline || (fresh && !opts.refresh)))
108
+ return { table: withFallback(cached) };
109
+ let note;
110
+ if (opts.offline)
111
+ note = 'offline';
112
+ else {
113
+ try {
114
+ const res = await fetch(PRICING_URL, { signal: AbortSignal.timeout(5000) });
115
+ if (!res.ok)
116
+ throw new Error(`HTTP ${res.status}`);
117
+ const table = parsePricingPage(await res.text(), new Date().toISOString());
118
+ if (!table)
119
+ throw new Error('pricing page format not recognized');
120
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
121
+ fs.writeFileSync(cacheFile, JSON.stringify(table));
122
+ return { table: withFallback(table) };
123
+ }
124
+ catch (e) {
125
+ note = e.message;
126
+ }
127
+ }
128
+ return { table: cached ? withFallback(cached) : BUNDLED, note };
129
+ }
130
+ export function priceFor(table, model, fast, overrides = {}) {
131
+ const key = fast ? `${model}-fast` : model;
132
+ const own = overrides[key];
133
+ if (own?.length === 5)
134
+ return own;
135
+ return fast ? table.fast[model] : table.models[model];
136
+ }