cctrackr 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/LICENSE +21 -0
- package/README.md +465 -0
- package/dist/index.js +582 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
- package/pricing/models.json +155 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var xo=Object.defineProperty;var _e=(o,e)=>()=>(o&&(e=o(o=0)),e);var ye=(o,e)=>{for(var t in e)xo(o,t,{get:e[t],enumerable:!0})};var Bt={};ye(Bt,{makeEntry:()=>Io});function Io(o={}){return{timestamp:"2025-03-25T10:00:00Z",message:{model:"claude-sonnet-4-20250514",usage:{input_tokens:1e3,output_tokens:500,cache_creation_input_tokens:0,cache_read_input_tokens:0}},...o}}var St=_e(()=>{"use strict"});var Zt={};ye(Zt,{calculateEntryCost:()=>it,calculateTieredCost:()=>et,fetchPricing:()=>ee,getAllPricing:()=>Ht,getModelPricing:()=>W,getPricingInfo:()=>ht,initPricing:()=>se,resetPricing:()=>je,setPricingData:()=>ft,updatePricing:()=>oe});import{readFileSync as Nt,writeFileSync as Ao,existsSync as Ce,mkdirSync as Po}from"fs";import{join as qt,dirname as Ro}from"path";import{fileURLToPath as zo}from"url";import{homedir as Fo}from"os";function Qt(){try{if(!Ce(jt))return 1/0;let o=JSON.parse(Nt(jt,"utf-8"));return Date.now()-new Date(o.fetched_at).getTime()}catch{return 1/0}}function qo(){try{return Ce(jt)?JSON.parse(Nt(jt,"utf-8")).data:null}catch{return null}}function Se(o){try{Po(Be,{recursive:!0});let e={fetched_at:new Date().toISOString(),data:o};Ao(jt,JSON.stringify(e,null,2),"utf-8")}catch{}}function te(){let o=Ro(zo(import.meta.url)),e=qt(o,"..","..","pricing","models.json");try{return JSON.parse(Nt(e,"utf-8"))}catch{let t=qt(process.cwd(),"pricing","models.json");return JSON.parse(Nt(t,"utf-8"))}}function J(o){try{let e={},t={},r=/claude-(?:opus|sonnet|haiku)-[\d.-]+(?:-\d{8})?/g,c=new Set,s;for(;(s=r.exec(o))!==null;)c.add(s[0]);let n=/<tr[^>]*>[\s\S]*?<\/tr>/gi,a=o.match(n)||[];for(let i of a){let d=i.match(/claude-(?:opus|sonnet|haiku)-[\w.-]+/);if(!d)continue;let m=d[0],l=[],u,f=/\$(\d{1,3}(?:,\d{3})*(?:\.\d+)?)/g;for(;(u=f.exec(i))!==null;)l.push(parseFloat(u[1].replace(/,/g,"")));if(l.length>=2){let y={input_cost_per_million:l[0],output_cost_per_million:l[1],cache_creation_cost_per_million:l[0]*1.25,cache_read_cost_per_million:l[0]*.1,context_window:2e5};l.length>=4&&(y.cache_creation_cost_per_million=l[2],y.cache_read_cost_per_million=l[3]),e[m]=y}}if(Object.keys(e).length===0)return null;for(let i of Object.keys(e)){let d=i.match(/^(claude-(?:opus|sonnet|haiku)-[\d.-]+)-\d{8}$/);d&&(t[d[1]]=i,t[d[1]+"-latest"]=i)}return{version:new Date().toISOString().slice(0,10),models:e,aliases:t}}catch{return null}}async function ee(){try{let o=await fetch(No,{headers:{"User-Agent":"cctrack/0.1.0"},signal:AbortSignal.timeout(1e4)});if(!o.ok)return null;let e=await o.text();return J(e)}catch{return null}}async function oe(){let o=te(),e=await ee();if(!e||Object.keys(e.models).length===0)return process.env.DEBUG&&console.warn("cctrack: pricing fetch returned 0 models, using bundled pricing"),{data:o,newModels:[],source:"bundled (fetch failed)"};let t={version:new Date().toISOString().slice(0,10),models:{...o.models},aliases:{...o.aliases}},r=[];for(let[c,s]of Object.entries(e.models))t.models[c]||r.push(c),t.models[c]=s;for(let[c,s]of Object.entries(e.aliases))t.aliases[c]=s;return Se(t),{data:t,newModels:r,source:"fetched + bundled"}}function ne(){if(at)return at;if(Qt()<Xt){let e=qo();if(e)return at=e,at}return at=te(),at}async function se(){if(Qt()>=Xt){let e=await ee();if(e&&Object.keys(e.models).length>0){let t=te(),r={version:new Date().toISOString().slice(0,10),models:{...t.models,...e.models},aliases:{...t.aliases,...e.aliases}};Se(r),at=r}}}function ft(o){at=o}function je(){at=null}function ht(){let o=ne(),e=Qt(),t;if(e===1/0)t="no cache";else{let c=Math.floor(e/36e5),s=Math.floor(e%(3600*1e3)/(60*1e3));t=`${c}h ${s}m ago`}return{source:e<Xt?"cached (fetched)":"bundled",modelCount:Object.keys(o.models).length,version:o.version,cacheAge:t}}function Ht(){return ne()}function W(o){let e=ne();if(e.models[o])return e.models[o];let t=e.aliases[o];return t&&e.models[t]?e.models[t]:null}function et(o,e,t,r=2e5){if(o<=0)return 0;let c=e/1e6;if(t!==void 0&&o>r){let s=t/1e6;return r*c+(o-r)*s}return o*c}function it(o,e,t,r,c,s){let n=W(o);if(!n)return s!==void 0?{input:0,output:0,cacheWrite:0,cacheRead:0,total:s}:{input:0,output:0,cacheWrite:0,cacheRead:0,total:0};let a=et(e,n.input_cost_per_million,n.input_cost_per_million_above_200k),i=et(t,n.output_cost_per_million,n.output_cost_per_million_above_200k),d=et(r,n.cache_creation_cost_per_million,n.cache_creation_cost_per_million_above_200k),m=et(c,n.cache_read_cost_per_million,n.cache_read_cost_per_million_above_200k);return{input:a,output:i,cacheWrite:d,cacheRead:m,total:a+i+d+m}}var at,Be,jt,Xt,No,_t=_e(()=>{"use strict";at=null,Be=qt(Fo(),".cctrack"),jt=qt(Be,"pricing.json"),Xt=1440*60*1e3,No="https://platform.claude.com/docs/en/about-claude/pricing";if(import.meta.vitest){let{describe:o,it:e,expect:t,beforeEach:r}=import.meta.vitest,c={version:"test",models:{"claude-sonnet-4-20250514":{input_cost_per_million:3,output_cost_per_million:15,cache_creation_cost_per_million:3.75,cache_read_cost_per_million:.3,input_cost_per_million_above_200k:6,output_cost_per_million_above_200k:30,cache_creation_cost_per_million_above_200k:7.5,cache_read_cost_per_million_above_200k:.6,context_window:2e5},"claude-opus-4-20250514":{input_cost_per_million:15,output_cost_per_million:75,cache_creation_cost_per_million:18.75,cache_read_cost_per_million:1.5,input_cost_per_million_above_200k:30,output_cost_per_million_above_200k:150,cache_creation_cost_per_million_above_200k:37.5,cache_read_cost_per_million_above_200k:3,context_window:2e5},"claude-haiku-3-5-20241022":{input_cost_per_million:.8,output_cost_per_million:4,cache_creation_cost_per_million:1,cache_read_cost_per_million:.08,context_window:2e5}},aliases:{"claude-sonnet-4-6":"claude-sonnet-4-20250514","claude-opus-4-6":"claude-opus-4-20250514"}};r(()=>{ft(c)}),o("getModelPricing",()=>{e("returns pricing for exact model match",()=>{let s=W("claude-sonnet-4-20250514");t(s).not.toBeNull(),t(s.input_cost_per_million).toBe(3)}),e("resolves alias to pricing",()=>{let s=W("claude-sonnet-4-6");t(s).not.toBeNull(),t(s.input_cost_per_million).toBe(3)}),e("returns null for unknown model (no fuzzy matching)",()=>{let s=W("claude-sonnet");t(s).toBeNull()}),e("returns null for empty string",()=>{t(W("")).toBeNull()}),e("returns null for partial match (no substring matching)",()=>{t(W("sonnet-4-20250514")).toBeNull()})}),o("calculateTieredCost",()=>{e("returns 0 for 0 tokens",()=>{t(et(0,3,6)).toBe(0)}),e("returns 0 for negative tokens",()=>{t(et(-100,3,6)).toBe(0)}),e("calculates base rate for tokens under threshold",()=>{let s=et(1e5,3,6);t(s).toBeCloseTo(.3,6)}),e("calculates base rate at exactly 200k threshold",()=>{let s=et(2e5,3,6);t(s).toBeCloseTo(.6,6)}),e("applies tiered pricing above 200k",()=>{let s=et(200001,3,6),n=2e5*(3/1e6)+1*(6/1e6);t(s).toBeCloseTo(n,10)}),e("calculates large tiered cost correctly",()=>{let s=et(1e6,3,6),n=2e5*(3/1e6)+8e5*(6/1e6);t(s).toBeCloseTo(n,6),t(s).toBeCloseTo(.6+4.8,6)}),e("uses base rate when no tiered price provided",()=>{let s=et(3e5,.8);t(s).toBeCloseTo(3e5*(.8/1e6),6)})}),o("calculateEntryCost",()=>{e("calculates cost for known model",()=>{let s=it("claude-sonnet-4-20250514",1e3,500,200,300);t(s.input).toBeCloseTo(1e3*(3/1e6),10),t(s.output).toBeCloseTo(500*(15/1e6),10),t(s.cacheWrite).toBeCloseTo(200*(3.75/1e6),10),t(s.cacheRead).toBeCloseTo(300*(.3/1e6),10),t(s.total).toBeCloseTo(s.input+s.output+s.cacheWrite+s.cacheRead,10)}),e("falls back to costUSD for unknown model",()=>{let s=it("unknown-model",1e3,500,0,0,.42);t(s.total).toBe(.42),t(s.input).toBe(0)}),e("returns zero cost when no pricing and no embedded cost",()=>{let s=it("unknown-model",1e3,500,0,0);t(s.total).toBe(0)}),e("works with alias model names",()=>{let s=it("claude-opus-4-6",1e3,500,0,0);t(s.input).toBeCloseTo(1e3*(15/1e6),10),t(s.output).toBeCloseTo(500*(75/1e6),10)})}),o("parsePricingHtml",()=>{e("returns null for empty HTML",()=>{t(J("")).toBeNull()}),e("returns null for HTML with no pricing tables",()=>{t(J("<html><body>no data</body></html>")).toBeNull()}),e("extracts pricing from table rows with 2 prices",()=>{let n=J(`
|
|
3
|
+
<tr><td>claude-sonnet-4-20250514</td><td>$3.00 / MTok</td><td>$15.00 / MTok</td></tr>
|
|
4
|
+
`);t(n).not.toBeNull(),t(n.models["claude-sonnet-4-20250514"]).toBeDefined(),t(n.models["claude-sonnet-4-20250514"].input_cost_per_million).toBe(3),t(n.models["claude-sonnet-4-20250514"].output_cost_per_million).toBe(15)}),e("extracts pricing with 4 prices (including cache)",()=>{let n=J(`
|
|
5
|
+
<tr><td>claude-opus-4-20250514</td><td>$15.00</td><td>$75.00</td><td>$18.75</td><td>$1.50</td></tr>
|
|
6
|
+
`);t(n).not.toBeNull();let a=n.models["claude-opus-4-20250514"];t(a.input_cost_per_million).toBe(15),t(a.output_cost_per_million).toBe(75),t(a.cache_creation_cost_per_million).toBe(18.75),t(a.cache_read_cost_per_million).toBe(1.5)}),e("extracts multiple models from HTML",()=>{let n=J(`
|
|
7
|
+
<table>
|
|
8
|
+
<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td><td>$15.00</td></tr>
|
|
9
|
+
<tr><td>claude-opus-4-20250514</td><td>$15.00</td><td>$75.00</td></tr>
|
|
10
|
+
</table>
|
|
11
|
+
`);t(n).not.toBeNull(),t(Object.keys(n.models)).toHaveLength(2)}),e("generates aliases for dated model IDs",()=>{let n=J(`
|
|
12
|
+
<tr><td>claude-sonnet-4-6-20260217</td><td>$3.00</td><td>$15.00</td></tr>
|
|
13
|
+
`);t(n).not.toBeNull(),t(n.aliases["claude-sonnet-4-6"]).toBe("claude-sonnet-4-6-20260217"),t(n.aliases["claude-sonnet-4-6-latest"]).toBe("claude-sonnet-4-6-20260217")}),e("skips rows without model IDs",()=>{let n=J(`
|
|
14
|
+
<tr><td>Some header</td><td>Input</td><td>Output</td></tr>
|
|
15
|
+
<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td><td>$15.00</td></tr>
|
|
16
|
+
`);t(n).not.toBeNull(),t(Object.keys(n.models)).toHaveLength(1)}),e("skips rows with only 1 price",()=>{let n=J(`
|
|
17
|
+
<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td></tr>
|
|
18
|
+
`);t(n).toBeNull()}),e("handles invalid JSON-LD gracefully",()=>{let n=J(`
|
|
19
|
+
<script type="application/ld+json">not json</script>
|
|
20
|
+
<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td><td>$15.00</td></tr>
|
|
21
|
+
`);t(n).not.toBeNull()}),e("sets version to current date",()=>{let n=J("<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td><td>$15.00</td></tr>");t(n.version).toMatch(/^\d{4}-\d{2}-\d{2}$/)}),e("defaults context_window to 200000",()=>{let n=J("<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td><td>$15.00</td></tr>");t(n.models["claude-sonnet-4-20250514"].context_window).toBe(2e5)}),e("derives cache costs from input when only 2 prices",()=>{let a=J("<tr><td>claude-sonnet-4-20250514</td><td>$3.00</td><td>$15.00</td></tr>").models["claude-sonnet-4-20250514"];t(a.cache_creation_cost_per_million).toBeCloseTo(3*1.25,6),t(a.cache_read_cost_per_million).toBeCloseTo(3*.1,6)})}),o("getPricingInfo",()=>{e("returns correct model count",()=>{let s=ht();t(s.modelCount).toBe(3)}),e("returns version",()=>{let s=ht();t(s.version).toBe("test")})}),o("getAllPricing",()=>{e("returns the full pricing data",()=>{let s=Ht();t(s.models).toBeDefined(),t(s.aliases).toBeDefined(),t(Object.keys(s.models)).toHaveLength(3)})}),o("cache functions",()=>{e("resetPricing clears in-memory cache",()=>{ft(c),t(W("claude-sonnet-4-20250514")).not.toBeNull(),je(),ft({version:"empty",models:{},aliases:{}}),t(W("claude-sonnet-4-20250514")).toBeNull(),ft(c)}),e("setPricingData overrides all lookups",()=>{ft({version:"custom",models:{"my-custom-model":{input_cost_per_million:99,output_cost_per_million:199,cache_creation_cost_per_million:10,cache_read_cost_per_million:1,context_window:1e5}},aliases:{"my-alias":"my-custom-model"}}),t(W("my-custom-model")).not.toBeNull(),t(W("my-custom-model").input_cost_per_million).toBe(99),t(W("my-alias").input_cost_per_million).toBe(99),t(W("claude-sonnet-4-20250514")).toBeNull(),ft(c)})}),o("calculateEntryCost edge cases",()=>{e("calculates with all four token types using tiered pricing",()=>{let s=it("claude-opus-4-20250514",3e5,1e5,5e4,1e4);t(s.input).toBeCloseTo(2e5*(15/1e6)+1e5*(30/1e6),6),t(s.output).toBeCloseTo(1e5*(75/1e6),6),t(s.total).toBeGreaterThan(0)}),e("handles zero tokens for all types",()=>{let s=it("claude-sonnet-4-20250514",0,0,0,0);t(s.total).toBe(0),t(s.input).toBe(0),t(s.output).toBe(0)})})}});import{Command as Cn}from"commander";import kt from"chalk";import Pe from"cli-table3";import{readdirSync as ke,statSync as Co,existsSync as be}from"fs";import{join as vt}from"path";import{homedir as Bo}from"os";function I(){let o=[],e=process.env.CLAUDE_CONFIG_DIR;if(e){let c=vt(e,"projects");be(c)&&o.push(c)}let t=Bo(),r=[vt(t,".claude","projects"),vt(t,".config","claude","projects")];for(let c of r)be(c)&&o.push(c);return[...new Set(o)]}var Vt=new Map;function B(o){Vt.clear();let e=[];for(let t of o)try{let r=ke(t,{withFileTypes:!0});for(let c of r){if(!c.isDirectory())continue;let s=jo(c.name),n=vt(t,c.name);ve(n,e,s)}}catch{}return e}function ve(o,e,t){try{let r=ke(o,{withFileTypes:!0});for(let c of r){let s=vt(o,c.name);c.isDirectory()?ve(s,e,t):c.name.endsWith(".jsonl")&&(e.push(s),Vt.set(s,t))}}catch{}}var Pt=new Map;function So(o){if(Pt.has(o))return Pt.get(o);try{let e=Co(o).isDirectory();return Pt.set(o,e),e}catch{return Pt.set(o,!1),!1}}function jo(o){let e=o.replace(/^-/,"").split("-"),t="/",r=0;for(;r<e.length;){let s=!1;for(let n=e.length-r;n>=1;n--){let a=e.slice(r,r+n).join("-"),i=[a,a.replace(/-/g,"_")],d=!1;for(let m of i){let l=vt(t,m);if(So(l)){t=l,r+=n,s=!0,d=!0;break}}if(d)break}if(!s)return e[e.length-1]||o}let c=t.split("/").filter(Boolean);return c[c.length-1]||o}function Rt(o){return Vt.get(o)??"unknown"}function tt(o){return o||"unknown"}if(import.meta.vitest){let{describe:o,it:e,expect:t,beforeEach:r,afterEach:c}=import.meta.vitest,{mkdirSync:s,writeFileSync:n,rmSync:a}=await import("fs"),{join:i}=await import("path"),{tmpdir:d}=await import("os"),m=i(d(),"cctrack-test-fs");r(()=>{s(m,{recursive:!0})}),c(()=>{a(m,{recursive:!0,force:!0})}),o("findJsonlFiles",()=>{e("finds .jsonl files inside project directories",()=>{let l=i(m,"-Users-me-myproject","session1");s(l,{recursive:!0}),n(i(l,"usage.jsonl"),"{}");let u=B([m]);t(u).toHaveLength(1),t(u[0]).toContain("usage.jsonl")}),e("maps files to project names via getProjectForFile",()=>{let l=i(m,"-xtest-zfake-myproject","sess");s(l,{recursive:!0}),n(i(l,"data.jsonl"),"{}");let u=B([m]);t(u).toHaveLength(1),t(Rt(u[0])).toBe("myproject")}),e("returns empty for empty directory",()=>{t(B([m])).toHaveLength(0)}),e("returns empty for non-existent directory",()=>{t(B([i(m,"nope")])).toHaveLength(0)})}),o("getProjectDirs",()=>{e("includes CLAUDE_CONFIG_DIR when set",()=>{let l=i(m,"custom");s(i(l,"projects"),{recursive:!0});let u=process.env.CLAUDE_CONFIG_DIR;process.env.CLAUDE_CONFIG_DIR=l;try{let f=I();t(f.some(y=>y.includes("custom"))).toBe(!0)}finally{u!==void 0?process.env.CLAUDE_CONFIG_DIR=u:delete process.env.CLAUDE_CONFIG_DIR}}),e("deduplicates paths",()=>{let l=I();t(l.length).toBe(new Set(l).size)})}),o("extractProjectName",()=>{e("returns cwd as-is (already normalized by parser)",()=>{t(tt("tradeforge")).toBe("tradeforge")}),e("returns unknown for empty string",()=>{t(tt("")).toBe("unknown")}),e("returns the normalized project name",()=>{t(tt("cctrack")).toBe("cctrack")})}),o("decodeProjectDir",()=>{e("decodes encoded project directory to last component",()=>{let l=i(m,"-xtest-zfake-tradeforge","sess");s(l,{recursive:!0}),n(i(l,"test.jsonl"),"{}");let u=B([m]);t(Rt(u[0])).toBe("tradeforge")})})}import{createReadStream as To,appendFileSync as Do,mkdirSync as Mo}from"fs";import{createInterface as Eo}from"readline";import{join as xe}from"path";import{homedir as $o}from"os";import{z as A}from"zod/v4";var zt=A.object({timestamp:A.string().datetime(),sessionId:A.string().optional(),version:A.string().optional(),cwd:A.string().optional(),message:A.object({id:A.string().optional(),model:A.string().optional(),usage:A.object({input_tokens:A.number(),output_tokens:A.number(),cache_creation_input_tokens:A.number().optional().default(0),cache_read_input_tokens:A.number().optional().default(0),speed:A.enum(["standard","fast"]).optional()}),content:A.array(A.object({text:A.string().optional()})).optional()}),costUSD:A.number().optional(),requestId:A.string().optional(),isApiErrorMessage:A.boolean().optional()}),we={pro:20,max5:100,max20:200},dt=300*60*1e3,Ft={safe:0,warning:50,critical:80,exceeded:100};async function ut(o){let e=[],t=0,r={apiErrors:0,synthetic:0},c=Rt(o),s=Eo({input:To(o,"utf-8"),crlfDelay:1/0});for await(let n of s){let a=n.trim();if(a)try{let i=JSON.parse(a);if(!i.message?.usage)continue;let d=zt.safeParse(i);if(!d.success){t++;continue}let m=d.data;if(m.isApiErrorMessage===!0){r.apiErrors++;try{let l=m.message.content;if(l?.some(f=>f.text?.includes("rate limit")||f.text?.includes("hit your limit"))){let f=xe($o(),".cctrack");Mo(f,{recursive:!0}),Do(xe(f,"rate-events.jsonl"),JSON.stringify({timestamp:m.timestamp,model:m.message.model,content:l?.map(y=>y.text).join(" ")})+`
|
|
22
|
+
`)}}catch{}continue}if(m.message.model==="<synthetic>"){r.synthetic++;continue}c!=="unknown"&&(m.cwd=c),e.push(m)}catch{t++}}return{entries:e,errors:t,skipped:r}}async function j(o){let e={entries:[],errors:0,skipped:{apiErrors:0,synthetic:0}},t=20;for(let r=0;r<o.length;r+=t){let c=o.slice(r,r+t),s=await Promise.all(c.map(ut));for(let n of s){for(let a of n.entries)e.entries.push(a);e.errors+=n.errors,e.skipped.apiErrors+=n.skipped.apiErrors,e.skipped.synthetic+=n.skipped.synthetic}}return e}if(import.meta.vitest){let l=function(g,p){n(m,{recursive:!0});let b=i(m,g);return c(b,p.join(`
|
|
23
|
+
`)),b};Oo=l;let{describe:o,it:e,expect:t,afterAll:r}=import.meta.vitest,{writeFileSync:c,unlinkSync:s,mkdirSync:n,rmSync:a}=await import("fs"),{join:i}=await import("path"),{tmpdir:d}=await import("os"),m=i(d(),"cctrack-test-parser");r(()=>{try{a(m,{recursive:!0,force:!0})}catch{}});let u=JSON.stringify({timestamp:"2025-03-25T10:00:00Z",message:{id:"msg_1",model:"claude-sonnet-4-20250514",usage:{input_tokens:100,output_tokens:50}},requestId:"req_1"}),f=JSON.stringify({timestamp:"2025-03-25T10:00:00Z",message:{model:"claude-sonnet-4-20250514",usage:{input_tokens:0,output_tokens:0}},isApiErrorMessage:!0}),y=JSON.stringify({timestamp:"2025-03-25T10:00:00Z",message:{model:"<synthetic>",usage:{input_tokens:50,output_tokens:20}}});o("parseJsonlFile",()=>{e("parses valid entries",async()=>{let g=l("valid.jsonl",[u]),p=await ut(g);t(p.entries).toHaveLength(1),t(p.entries[0].message.usage.input_tokens).toBe(100),t(p.errors).toBe(0),s(g)}),e("filters API error entries",async()=>{let g=l("api-err.jsonl",[u,f]),p=await ut(g);t(p.entries).toHaveLength(1),t(p.skipped.apiErrors).toBe(1),s(g)}),e("filters synthetic model entries",async()=>{let g=l("synthetic.jsonl",[u,y]),p=await ut(g);t(p.entries).toHaveLength(1),t(p.skipped.synthetic).toBe(1),s(g)}),e("counts invalid JSON as errors",async()=>{let g=l("invalid.jsonl",[u,"not json at all",'{"broken']),p=await ut(g);t(p.entries).toHaveLength(1),t(p.errors).toBe(2),s(g)}),e("counts schema validation failures as errors",async()=>{let g=JSON.stringify({timestamp:"not-a-date",message:{usage:{input_tokens:1,output_tokens:1}}}),p=l("bad-schema.jsonl",[u,g]),b=await ut(p);t(b.entries).toHaveLength(1),t(b.errors).toBe(1),s(p)}),e("defaults cache tokens to 0",async()=>{let g=l("no-cache.jsonl",[u]),p=await ut(g);t(p.entries[0].message.usage.cache_creation_input_tokens).toBe(0),t(p.entries[0].message.usage.cache_read_input_tokens).toBe(0),s(g)}),e("handles empty files",async()=>{let g=l("empty.jsonl",[""]),p=await ut(g);t(p.entries).toHaveLength(0),s(g)})}),o("parseAllFiles",()=>{e("combines entries from multiple files",async()=>{let g=l("multi1.jsonl",[u]),p=l("multi2.jsonl",[u]),b=await j([g,p]);t(b.entries).toHaveLength(2),t(b.errors).toBe(0),s(g),s(p)}),e("combines errors and skipped counts",async()=>{let g=l("comb1.jsonl",[u,f]),p=l("comb2.jsonl",[y,"bad json"]),b=await j([g,p]);t(b.entries).toHaveLength(1),t(b.skipped.apiErrors).toBe(1),t(b.skipped.synthetic).toBe(1),t(b.errors).toBe(1),s(g),s(p)}),e("handles empty file list",async()=>{let g=await j([]);t(g.entries).toHaveLength(0),t(g.errors).toBe(0)}),e("handles more than BATCH_SIZE (20) files",async()=>{let g=[];for(let b=0;b<25;b++)g.push(l(`batch-${b}.jsonl`,[u]));let p=await j(g);t(p.entries).toHaveLength(25),t(p.errors).toBe(0);for(let b of g)s(b)})})}var Oo;import{createHash as Lo}from"crypto";function rt(o){return o.requestId?`req:${o.requestId}`:o.message.id?`msg:${o.message.id}`:`hash:${Lo("sha256").update(`${o.timestamp}|${o.message.model}|${o.message.usage.input_tokens}|${o.message.usage.output_tokens}`).digest("hex").slice(0,16)}`}function x(o){let e=new Set,t=[];for(let r of o){let c=rt(r);e.has(c)||(e.add(c),t.push(r))}return t}if(import.meta.vitest){let{describe:o,it:e,expect:t}=import.meta.vitest,{makeEntry:r}=await Promise.resolve().then(()=>(St(),Bt));o("createDedupKey",()=>{e("uses requestId when available (highest priority)",()=>{let c=r({requestId:"req_123",message:{...r().message,id:"msg_456"}});t(rt(c)).toBe("req:req_123")}),e("falls back to message.id when no requestId",()=>{let c=r({message:{...r().message,id:"msg_456"}});t(rt(c)).toBe("msg:msg_456")}),e("falls back to hash when no requestId or message.id",()=>{let c=r(),s=rt(c);t(s).toMatch(/^hash:[a-f0-9]{16}$/)}),e("produces same hash for identical entries",()=>{let c=r(),s=r();t(rt(c)).toBe(rt(s))}),e("produces different hash for different token counts",()=>{let c=r(),s=r({message:{...r().message,usage:{...r().message.usage,input_tokens:999}}});t(rt(c)).not.toBe(rt(s))}),e("treats empty-string requestId as falsy (falls through to msg or hash)",()=>{let c=r({requestId:""}),s=rt(c);t(s).not.toBe("req:"),t(s).toMatch(/^(msg:|hash:)/)})}),o("deduplicateEntries",()=>{e("removes duplicates by requestId",()=>{let c=[r({requestId:"r1"}),r({requestId:"r1"}),r({requestId:"r2"})];t(x(c)).toHaveLength(2)}),e("removes duplicates by message.id",()=>{let c=[r({message:{...r().message,id:"m1"}}),r({message:{...r().message,id:"m1"}})];t(x(c)).toHaveLength(1)}),e("removes duplicates by hash fallback",()=>{let c=[r(),r()];t(x(c)).toHaveLength(1)}),e("preserves insertion order",()=>{let c=[r({requestId:"r1"}),r({requestId:"r2"}),r({requestId:"r1"})],s=x(c);t(s[0].requestId).toBe("r1"),t(s[1].requestId).toBe("r2")}),e("handles cross-file dedup (same requestId different entries)",()=>{let c=r({requestId:"r1",cwd:"/project-a"}),s=r({requestId:"r1",cwd:"/project-b"});t(x([c,s])).toHaveLength(1)}),e("returns empty array for empty input",()=>{t(x([])).toHaveLength(0)})})}_t();function $(o,e="calculate"){let t=o.message.usage,r=o.message.model??"unknown",c={input_tokens:t.input_tokens,output_tokens:t.output_tokens,cache_write_tokens:t.cache_creation_input_tokens??0,cache_read_tokens:t.cache_read_input_tokens??0,total_tokens:t.input_tokens+t.output_tokens+(t.cache_creation_input_tokens??0)+(t.cache_read_input_tokens??0)},s=it(r,c.input_tokens,c.output_tokens,c.cache_write_tokens,c.cache_read_tokens,o.costUSD),n={input_cost:s.input,output_cost:s.output,cache_write_cost:s.cacheWrite,cache_read_cost:s.cacheRead,total_cost:s.total},a;return e==="display"?a={input_cost:0,output_cost:0,cache_write_cost:0,cache_read_cost:0,total_cost:o.costUSD??0}:a=n,{tokens:c,cost:a,calculatedCost:n,displayCost:o.costUSD}}function re(){return{input_tokens:0,output_tokens:0,cache_write_tokens:0,cache_read_tokens:0,total_tokens:0}}function ae(){return{input_cost:0,output_cost:0,cache_write_cost:0,cache_read_cost:0,total_cost:0}}function Tt(o,e){return{input_tokens:o.input_tokens+e.input_tokens,output_tokens:o.output_tokens+e.output_tokens,cache_write_tokens:o.cache_write_tokens+e.cache_write_tokens,cache_read_tokens:o.cache_read_tokens+e.cache_read_tokens,total_tokens:o.total_tokens+e.total_tokens}}function Dt(o,e){return{input_cost:o.input_cost+e.input_cost,output_cost:o.output_cost+e.output_cost,cache_write_cost:o.cache_write_cost+e.cache_write_cost,cache_read_cost:o.cache_read_cost+e.cache_read_cost,total_cost:o.total_cost+e.total_cost}}if(import.meta.vitest){let{describe:o,it:e,expect:t,beforeEach:r}=import.meta.vitest,{setPricingData:c}=await Promise.resolve().then(()=>(_t(),Zt)),s={version:"test",models:{"claude-sonnet-4-20250514":{input_cost_per_million:3,output_cost_per_million:15,cache_creation_cost_per_million:3.75,cache_read_cost_per_million:.3,context_window:2e5}},aliases:{}};r(()=>{c(s)});let n=(a={})=>({timestamp:"2025-03-25T10:00:00Z",message:{model:"claude-sonnet-4-20250514",usage:{input_tokens:1e3,output_tokens:500,cache_creation_input_tokens:200,cache_read_input_tokens:300}},...a});o("processEntry",()=>{e("extracts correct token breakdown",()=>{let a=$(n());t(a.tokens.input_tokens).toBe(1e3),t(a.tokens.output_tokens).toBe(500),t(a.tokens.cache_write_tokens).toBe(200),t(a.tokens.cache_read_tokens).toBe(300),t(a.tokens.total_tokens).toBe(2e3)}),e("calculates cost in calculate mode",()=>{let a=$(n(),"calculate");t(a.cost.input_cost).toBeCloseTo(1e3*(3/1e6),10),t(a.cost.output_cost).toBeCloseTo(500*(15/1e6),10),t(a.cost.cache_write_cost).toBeCloseTo(200*(3.75/1e6),10),t(a.cost.cache_read_cost).toBeCloseTo(300*(.3/1e6),10)}),e("uses embedded cost in display mode",()=>{let a=$(n({costUSD:.42}),"display");t(a.cost.total_cost).toBe(.42),t(a.cost.input_cost).toBe(0)}),e("uses 0 in display mode when no embedded cost",()=>{let a=$(n(),"display");t(a.cost.total_cost).toBe(0)}),e("provides both calculated and display costs in compare mode data",()=>{let a=n({costUSD:.42}),i=$(a,"compare");t(i.calculatedCost.total_cost).toBeGreaterThan(0),t(i.displayCost).toBe(.42)})}),o("addTokens",()=>{e("sums token breakdowns",()=>{let d=Tt({input_tokens:10,output_tokens:5,cache_write_tokens:2,cache_read_tokens:3,total_tokens:20},{input_tokens:20,output_tokens:10,cache_write_tokens:4,cache_read_tokens:6,total_tokens:40});t(d.input_tokens).toBe(30),t(d.output_tokens).toBe(15),t(d.total_tokens).toBe(60)})}),o("addCosts",()=>{e("sums cost breakdowns",()=>{let d=Dt({input_cost:1,output_cost:2,cache_write_cost:.5,cache_read_cost:.1,total_cost:3.6},{input_cost:3,output_cost:4,cache_write_cost:1.5,cache_read_cost:.2,total_cost:8.7});t(d.input_cost).toBe(4),t(d.total_cost).toBeCloseTo(12.3,6)})}),o("processEntry with missing model",()=>{e("returns zero cost when model is undefined",()=>{let a=n({message:{usage:{input_tokens:100,output_tokens:50,cache_creation_input_tokens:0,cache_read_input_tokens:0}}}),i=$(a);t(i.cost.total_cost).toBe(0),t(i.tokens.input_tokens).toBe(100)})}),o("emptyTokens and emptyCost",()=>{e("emptyTokens returns all zeros",()=>{let a=re();t(a.input_tokens).toBe(0),t(a.output_tokens).toBe(0),t(a.cache_write_tokens).toBe(0),t(a.cache_read_tokens).toBe(0),t(a.total_tokens).toBe(0)}),e("emptyCost returns all zeros",()=>{let a=ae();t(a.input_cost).toBe(0),t(a.output_cost).toBe(0),t(a.cache_write_cost).toBe(0),t(a.cache_read_cost).toBe(0),t(a.total_cost).toBe(0)})})}function yt(o,e){let t=new Date(o);return e?t.toLocaleDateString("en-CA",{timeZone:e}):t.toISOString().slice(0,10)}function Ut(o,e){return yt(o,e).slice(0,7)}function mt(o,e){let t=new Date(o);if(e){let r=new Intl.DateTimeFormat("en-US",{timeZone:e,hour:"numeric",hour12:!1,weekday:"short"}).formatToParts(t),c=r.find(a=>a.type==="hour"),s=r.find(a=>a.type==="weekday"),n={Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6};return{hour:parseInt(c?.value??"0",10),day:n[s?.value??"Sun"]??0}}return{hour:t.getUTCHours(),day:t.getUTCDay()}}function G(o,e,t){let r=o.slice(0,10);return!(e&&r<e||t&&r>t)}if(import.meta.vitest){let{describe:o,it:e,expect:t}=import.meta.vitest;o("toDateString",()=>{e("returns YYYY-MM-DD for UTC",()=>{t(yt("2025-03-25T10:00:00Z")).toBe("2025-03-25")}),e("respects timezone",()=>{t(yt("2025-03-25T23:00:00Z","Asia/Kolkata")).toBe("2025-03-26")}),e("handles midnight boundary",()=>{t(yt("2025-03-25T00:00:00Z")).toBe("2025-03-25")})}),o("toMonthString",()=>{e("returns YYYY-MM",()=>{t(Ut("2025-03-25T10:00:00Z")).toBe("2025-03")}),e("respects timezone for month boundary",()=>{t(Ut("2025-03-31T23:00:00Z","Asia/Kolkata")).toBe("2025-04")})}),o("getHourAndDay",()=>{e("returns UTC hour and day by default",()=>{let r=mt("2025-03-25T14:30:00Z");t(r.hour).toBe(14),t(r.day).toBe(2)}),e("respects timezone",()=>{let r=mt("2025-03-25T14:00:00Z","Asia/Kolkata");t(r.hour).toBe(19),t(r.day).toBe(2)}),e("handles day rollover with timezone",()=>{let r=mt("2025-03-25T20:00:00Z","Asia/Kolkata");t(r.hour).toBe(1),t(r.day).toBe(3)}),e("handles Sunday correctly",()=>{let r=mt("2025-03-23T10:00:00Z");t(r.day).toBe(0)})}),o("isInRange",()=>{e("returns true when no filters",()=>{t(G("2025-03-25T10:00:00Z")).toBe(!0)}),e("filters by since",()=>{t(G("2025-03-24T10:00:00Z","2025-03-25")).toBe(!1),t(G("2025-03-25T10:00:00Z","2025-03-25")).toBe(!0),t(G("2025-03-26T10:00:00Z","2025-03-25")).toBe(!0)}),e("filters by until",()=>{t(G("2025-03-26T10:00:00Z",void 0,"2025-03-25")).toBe(!1),t(G("2025-03-25T10:00:00Z",void 0,"2025-03-25")).toBe(!0)}),e("filters by both since and until",()=>{t(G("2025-03-25T10:00:00Z","2025-03-25","2025-03-25")).toBe(!0),t(G("2025-03-24T10:00:00Z","2025-03-25","2025-03-26")).toBe(!1),t(G("2025-03-27T10:00:00Z","2025-03-25","2025-03-26")).toBe(!1)}),e("includes entry at 23:59 on the same day",()=>{t(G("2025-03-25T23:59:59Z","2025-03-25","2025-03-25")).toBe(!0)}),e("returns false when since > until (impossible range)",()=>{t(G("2025-03-25T10:00:00Z","2025-03-26","2025-03-24")).toBe(!1)})})}function T(){return{tokens:re(),cost:ae(),request_count:0}}function O(o,e){o.tokens=Tt(o.tokens,e.tokens),o.cost=Dt(o.cost,e.cost),o.request_count++}function D(o,e){return o.filter(t=>!(!G(t.timestamp,e.since,e.until)||e.project&&(!t.cwd||!tt(t.cwd).toLowerCase().includes(e.project.toLowerCase()))))}function st(o,e="calculate",t){let r=new Map;for(let c of o){let s=yt(c.timestamp,t),n=c.message.model??"unknown",a=c.cwd?tt(c.cwd):"unknown";r.has(s)||r.set(s,{date:s,...T(),models:{},projects:{}});let i=r.get(s),d=$(c,e);O(i,d),i.models[n]||(i.models[n]=T()),O(i.models[n],d),i.projects[a]||(i.projects[a]=T()),O(i.projects[a],d)}return[...r.values()].sort((c,s)=>c.date.localeCompare(s.date))}function Yt(o,e="calculate",t){let r=new Map;for(let c of o){let s=Ut(c.timestamp,t),n=c.message.model??"unknown";r.has(s)||r.set(s,{month:s,...T(),models:{}});let a=r.get(s),i=$(c,e);O(a,i),a.models[n]||(a.models[n]=T()),O(a.models[n],i)}return[...r.values()].sort((c,s)=>c.month.localeCompare(s.month))}function ct(o,e="calculate"){let t=new Map;for(let r of o){let c=r.sessionId??"unknown",s=r.message.model??"unknown",n=r.cwd?tt(r.cwd):"unknown";t.has(c)||t.set(c,{sessionId:c,project:n,startTime:r.timestamp,endTime:r.timestamp,primaryModel:s,...T(),models:{}});let a=t.get(c),i=$(r,e);O(a,i),r.timestamp<a.startTime&&(a.startTime=r.timestamp),r.timestamp>a.endTime&&(a.endTime=r.timestamp),a.models[s]||(a.models[s]=T()),O(a.models[s],i);let d=a.models[s].request_count,m=a.models[a.primaryModel]?.request_count??0;d>=m&&(a.primaryModel=s)}return[...t.values()].sort((r,c)=>c.startTime.localeCompare(r.startTime))}function ie(o,e="calculate"){let t=new Map;for(let r of o){let c=r.cwd?tt(r.cwd):"unknown",s=r.message.model??"unknown";t.has(c)||t.set(c,{project:c,...T(),models:{}});let n=t.get(c),a=$(r,e);O(n,a),n.models[s]||(n.models[s]=T()),O(n.models[s],a)}return[...t.values()].sort((r,c)=>c.cost.total_cost-r.cost.total_cost)}function Te(o,e="calculate"){let t={};for(let r of o){let c=r.message.model??"unknown";t[c]||(t[c]=T()),O(t[c],$(r,e))}return t}function De(o,e){let t=Array.from({length:7},()=>Array(24).fill(0));for(let r of o){let{hour:c,day:s}=mt(r.timestamp,e);t[s][c]+=r.message.usage.input_tokens+r.message.usage.output_tokens}return t}function pt(o,e="calculate",t){let r=[...o].sort((u,f)=>u.timestamp.localeCompare(f.timestamp)),c=T(),s=new Map,n=new Map,a=new Map,i=new Map,d={},m=Array.from({length:7},()=>Array(24).fill(0)),l={};for(let u of r){let f=$(u,e),y=yt(u.timestamp,t),g=y.slice(0,7),p=u.message.model??"unknown",b=u.sessionId??"unknown",E=u.cwd?tt(u.cwd):"unknown",{hour:X,day:U}=mt(u.timestamp,t),Q=u.message.usage.input_tokens+u.message.usage.output_tokens;O(c,f),s.has(y)||s.set(y,{date:y,...T(),models:{},projects:{}});let L=s.get(y);O(L,f),L.models[p]||(L.models[p]=T()),O(L.models[p],f),L.projects[E]||(L.projects[E]=T()),O(L.projects[E],f),n.has(g)||n.set(g,{month:g,...T(),models:{}});let P=n.get(g);O(P,f),P.models[p]||(P.models[p]=T()),O(P.models[p],f),a.has(b)||a.set(b,{sessionId:b,project:E,startTime:u.timestamp,endTime:u.timestamp,primaryModel:p,...T(),models:{}});let C=a.get(b);O(C,f),u.timestamp<C.startTime&&(C.startTime=u.timestamp),u.timestamp>C.endTime&&(C.endTime=u.timestamp),C.models[p]||(C.models[p]=T()),O(C.models[p],f);let Y=C.models[p].request_count,w=C.models[C.primaryModel]?.request_count??0;Y>=w&&(C.primaryModel=p),i.has(E)||i.set(E,{project:E,...T(),models:{}});let Ct=i.get(E);O(Ct,f),Ct.models[p]||(Ct.models[p]=T()),O(Ct.models[p],f),d[p]||(d[p]=T()),O(d[p],f),m[U][X]+=Q,l[E]||(l[E]=Array.from({length:7},()=>Array(24).fill(0))),l[E][U][X]+=Q}return{generated_at:new Date().toISOString(),date_range:{start:r[0]?.timestamp??"",end:r[r.length-1]?.timestamp??""},totals:c,daily:[...s.values()].sort((u,f)=>u.date.localeCompare(f.date)),monthly:[...n.values()].sort((u,f)=>u.month.localeCompare(f.month)),sessions:[...a.values()].sort((u,f)=>f.startTime.localeCompare(u.startTime)),projects:[...i.values()].sort((u,f)=>f.cost.total_cost-u.cost.total_cost),models:d,heatmap:m,project_heatmaps:l}}function Me(o,e){let t={};for(let r of o){let c=r.cwd?tt(r.cwd):"unknown";t[c]||(t[c]=Array.from({length:7},()=>Array(24).fill(0)));let{hour:s,day:n}=mt(r.timestamp,e);t[c][n][s]+=r.message.usage.input_tokens+r.message.usage.output_tokens}return t}if(import.meta.vitest){let{describe:o,it:e,expect:t,beforeEach:r}=import.meta.vitest,{setPricingData:c}=await Promise.resolve().then(()=>(_t(),Zt)),s={version:"test",models:{"claude-sonnet-4-20250514":{input_cost_per_million:3,output_cost_per_million:15,cache_creation_cost_per_million:3.75,cache_read_cost_per_million:.3,context_window:2e5}},aliases:{}};r(()=>{c(s)});let{makeEntry:n}=await Promise.resolve().then(()=>(St(),Bt));o("aggregateDaily",()=>{e("groups entries by date",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T14:00:00Z"}),n({timestamp:"2025-03-26T10:00:00Z"})],i=st(a);t(i).toHaveLength(2),t(i[0].date).toBe("2025-03-25"),t(i[0].request_count).toBe(2),t(i[1].date).toBe("2025-03-26")}),e("tracks per-model breakdown",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T11:00:00Z",message:{model:"other-model",usage:{input_tokens:500,output_tokens:200,cache_creation_input_tokens:0,cache_read_input_tokens:0}}})],i=st(a);t(Object.keys(i[0].models)).toHaveLength(2)}),e("respects timezone for date grouping",()=>{let a=[n({timestamp:"2025-03-25T23:00:00Z"})],i=st(a,"calculate","Asia/Kolkata");t(i[0].date).toBe("2025-03-26")})}),o("aggregateSessions",()=>{e("groups entries by sessionId",()=>{let a=[n({sessionId:"s1",timestamp:"2025-03-25T10:00:00Z"}),n({sessionId:"s1",timestamp:"2025-03-25T10:05:00Z"}),n({sessionId:"s2",timestamp:"2025-03-25T11:00:00Z"})],i=ct(a);t(i).toHaveLength(2)}),e("tracks session time range",()=>{let a=[n({sessionId:"s1",timestamp:"2025-03-25T10:00:00Z"}),n({sessionId:"s1",timestamp:"2025-03-25T10:30:00Z"})],i=ct(a);t(i[0].startTime).toBe("2025-03-25T10:00:00Z"),t(i[0].endTime).toBe("2025-03-25T10:30:00Z")})}),o("buildHeatmap",()=>{e("creates 7\xD724 grid",()=>{let a=De([]);t(a).toHaveLength(7),t(a[0]).toHaveLength(24)}),e("accumulates tokens in correct cell",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"})],i=De(a);t(i[2][10]).toBe(1500)})}),o("aggregateMonthly",()=>{e("groups entries by month",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-26T10:00:00Z"}),n({timestamp:"2025-04-01T10:00:00Z"})],i=Yt(a);t(i).toHaveLength(2),t(i[0].month).toBe("2025-03"),t(i[0].request_count).toBe(2),t(i[1].month).toBe("2025-04"),t(i[1].request_count).toBe(1)}),e("tracks per-model breakdown in monthly",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T11:00:00Z",message:{model:"other-model",usage:{input_tokens:500,output_tokens:200,cache_creation_input_tokens:0,cache_read_input_tokens:0}}})],i=Yt(a);t(Object.keys(i[0].models)).toHaveLength(2)})}),o("aggregateProjects",()=>{e("groups entries by project (cwd)",()=>{let a=[n({cwd:"/home/.claude/projects/-Users-me-proj1/session.jsonl"}),n({cwd:"/home/.claude/projects/-Users-me-proj1/session.jsonl"}),n({cwd:"/home/.claude/projects/-Users-me-proj2/session.jsonl"})],i=ie(a);t(i).toHaveLength(2)}),e("sorts by cost descending",()=>{let a=[n({cwd:"/home/.claude/projects/cheap/f.jsonl",message:{model:"claude-sonnet-4-20250514",usage:{input_tokens:100,output_tokens:50,cache_creation_input_tokens:0,cache_read_input_tokens:0}}}),n({cwd:"/home/.claude/projects/expensive/f.jsonl",message:{model:"claude-sonnet-4-20250514",usage:{input_tokens:1e5,output_tokens:5e4,cache_creation_input_tokens:0,cache_read_input_tokens:0}}})],i=ie(a);t(i[0].cost.total_cost).toBeGreaterThan(i[1].cost.total_cost)}),e("uses unknown for entries without cwd",()=>{let a=[n()],i=ie(a);t(i[0].project).toBe("unknown")})}),o("aggregateModels",()=>{e("groups entries by model name",()=>{let a=[n(),n({message:{model:"other-model",usage:{input_tokens:100,output_tokens:50,cache_creation_input_tokens:0,cache_read_input_tokens:0}}})],i=Te(a);t(Object.keys(i)).toHaveLength(2),t(i["claude-sonnet-4-20250514"]).toBeDefined(),t(i["other-model"]).toBeDefined()}),e("uses unknown for entries without model",()=>{let a=[n({message:{usage:{input_tokens:100,output_tokens:50,cache_creation_input_tokens:0,cache_read_input_tokens:0}}})],i=Te(a);t(i.unknown).toBeDefined()})}),o("aggregateSessions (extended)",()=>{e("detects primary model by request count",()=>{let a=[n({sessionId:"s1"}),n({sessionId:"s1"}),n({sessionId:"s1",message:{model:"other-model",usage:{input_tokens:100,output_tokens:50,cache_creation_input_tokens:0,cache_read_input_tokens:0}}})],i=ct(a);t(i[0].primaryModel).toBe("claude-sonnet-4-20250514")}),e("uses unknown sessionId when missing",()=>{let a=[n()],i=ct(a);t(i[0].sessionId).toBe("unknown")}),e("sorts sessions by startTime descending (newest first)",()=>{let a=[n({sessionId:"s1",timestamp:"2025-03-25T08:00:00Z"}),n({sessionId:"s2",timestamp:"2025-03-25T12:00:00Z"})],i=ct(a);t(i[0].sessionId).toBe("s2"),t(i[1].sessionId).toBe("s1")})}),o("buildDashboardData",()=>{e("returns all required fields",()=>{let a=[n({sessionId:"s1",timestamp:"2025-03-25T10:00:00Z"}),n({sessionId:"s1",timestamp:"2025-03-25T11:00:00Z"})],i=pt(a);t(i.generated_at).toBeTruthy(),t(i.date_range.start).toBe("2025-03-25T10:00:00Z"),t(i.date_range.end).toBe("2025-03-25T11:00:00Z"),t(i.totals.request_count).toBe(2),t(i.daily).toHaveLength(1),t(i.monthly).toHaveLength(1),t(i.sessions).toHaveLength(1),t(i.heatmap).toHaveLength(7),t(Object.keys(i.models)).toHaveLength(1)}),e("handles empty entries",()=>{let a=pt([]);t(a.totals.request_count).toBe(0),t(a.daily).toHaveLength(0),t(a.date_range.start).toBe("")})}),o("filterEntries",()=>{e("filters by date range",()=>{let a=[n({timestamp:"2025-03-24T10:00:00Z"}),n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-26T10:00:00Z"})],i=D(a,{since:"2025-03-25",until:"2025-03-25"});t(i).toHaveLength(1)}),e("returns all entries with no filters",()=>{let a=[n(),n()];t(D(a,{})).toHaveLength(2)}),e("filters by since only",()=>{let a=[n({timestamp:"2025-03-24T10:00:00Z"}),n({timestamp:"2025-03-25T10:00:00Z"})];t(D(a,{since:"2025-03-25"})).toHaveLength(1)}),e("filters by until only",()=>{let a=[n({timestamp:"2025-03-24T10:00:00Z"}),n({timestamp:"2025-03-25T10:00:00Z"})];t(D(a,{until:"2025-03-24"})).toHaveLength(1)}),e("filters by project name (case-insensitive substring)",()=>{let a=[n({cwd:"tradeforge"}),n({cwd:"cctrack"}),n({cwd:"TradeForge"})],i=D(a,{project:"trade"});t(i).toHaveLength(2)}),e("excludes entries without cwd when project filter is set",()=>{let a=[n(),n({cwd:"cctrack"})];t(D(a,{project:"cctrack"})).toHaveLength(1)})}),o("aggregateDaily (project breakdown)",()=>{e("tracks per-project breakdown in daily",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z",cwd:"proj-a"}),n({timestamp:"2025-03-25T11:00:00Z",cwd:"proj-b"})],i=st(a);t(Object.keys(i[0].projects)).toHaveLength(2),t(i[0].projects["proj-a"].request_count).toBe(1),t(i[0].projects["proj-b"].request_count).toBe(1)}),e("per-project daily data has complete cost and token fields",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z",cwd:"proj-a"})],d=st(a)[0].projects["proj-a"];t(d.request_count).toBe(1),t(d.cost.input_cost).toBeDefined(),t(d.cost.output_cost).toBeDefined(),t(d.cost.cache_write_cost).toBeDefined(),t(d.cost.cache_read_cost).toBeDefined(),t(d.cost.total_cost).toBeDefined(),t(typeof d.cost.cache_read_cost).toBe("number"),t(d.tokens.input_tokens).toBeDefined(),t(d.tokens.output_tokens).toBeDefined(),t(d.tokens.cache_write_tokens).toBeDefined(),t(d.tokens.cache_read_tokens).toBeDefined(),t(d.tokens.total_tokens).toBeDefined()}),e("per-project data produces non-zero ROI when summed (catches the $0.00 bug)",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z",cwd:"proj-a"}),n({timestamp:"2025-03-25T11:00:00Z",cwd:"proj-a"}),n({timestamp:"2025-03-25T12:00:00Z",cwd:"proj-b"})],d=st(a)[0].projects["proj-a"],m=d.cost.total_cost,l=d.request_count,u=d.cost.cache_read_cost,f=l>0?m/l:0,y=u*9;t(l).toBe(2),t(f).toBeGreaterThan(0),t(typeof u).toBe("number")})}),o("buildProjectHeatmaps",()=>{e("creates separate heatmaps per project",()=>{let a=[n({cwd:"proj-a",timestamp:"2025-03-25T10:00:00Z"}),n({cwd:"proj-b",timestamp:"2025-03-25T14:00:00Z"})],i=Me(a);t(Object.keys(i)).toHaveLength(2),t(i["proj-a"]).toHaveLength(7),t(i["proj-a"][2][10]).toBe(1500)}),e("returns empty object for no entries",()=>{t(Me([])).toEqual({})})}),o("buildDashboardData (single-pass)",()=>{e("includes project_heatmaps",()=>{let a=[n({cwd:"proj-a",sessionId:"s1",timestamp:"2025-03-25T10:00:00Z"})],i=pt(a);t(i.project_heatmaps).toBeDefined(),t(i.project_heatmaps["proj-a"]).toHaveLength(7)}),e("produces consistent totals across aggregations",()=>{let a=[n({sessionId:"s1",timestamp:"2025-03-25T10:00:00Z"}),n({sessionId:"s1",timestamp:"2025-03-25T11:00:00Z"}),n({sessionId:"s2",timestamp:"2025-03-26T10:00:00Z"})],i=pt(a);t(i.totals.request_count).toBe(3),t(i.daily.reduce((d,m)=>d+m.request_count,0)).toBe(3),t(i.sessions.reduce((d,m)=>d+m.request_count,0)).toBe(3)})})}function h(o){return o<0?`-$${Math.abs(o).toFixed(2)}`:o<.01&&o>0?`$${o.toFixed(4)}`:`$${o.toFixed(2)}`}function _(o){return o>=1e6?`${(o/1e6).toFixed(1)}M`:o>=1e3?`${(o/1e3).toFixed(1)}K`:o.toString()}function R(o){let e=Math.floor(o/1e3);if(e<60)return`${e}s`;let t=Math.floor(e/60);return t<60?`${t}m ${e%60}s`:`${Math.floor(t/60)}h ${t%60}m`}function S(o){let e=o??"calculate";if(e==="calculate"||e==="display"||e==="compare")return e;console.error(`Invalid mode: "${e}". Choose from: calculate, display, compare`),process.exit(1)}function nt(o){return o.includes(",")||o.includes('"')||o.includes(`
|
|
24
|
+
`)?'"'+o.replace(/"/g,'""')+'"':o}if(import.meta.vitest){let{describe:o,it:e,expect:t}=import.meta.vitest;o("formatCost",()=>{e("formats zero as $0.00",()=>t(h(0)).toBe("$0.00")),e("formats sub-cent with 4 decimals",()=>t(h(.0012)).toBe("$0.0012")),e("formats $0.0099 with 4 decimals",()=>t(h(.0099)).toBe("$0.0099")),e("formats $0.01 with 2 decimals",()=>t(h(.01)).toBe("$0.01")),e("formats $1.50 with 2 decimals",()=>t(h(1.5)).toBe("$1.50")),e("formats large cost",()=>t(h(1234.56)).toBe("$1234.56")),e("formats negative cost with minus before $",()=>t(h(-5)).toBe("-$5.00"))}),o("formatTokens",()=>{e("formats millions",()=>t(_(15e5)).toBe("1.5M")),e("formats thousands",()=>t(_(1500)).toBe("1.5K")),e("formats small numbers as-is",()=>t(_(999)).toBe("999")),e("formats zero",()=>t(_(0)).toBe("0"))}),o("formatDuration",()=>{e("formats seconds",()=>t(R(5e3)).toBe("5s")),e("formats minutes",()=>t(R(125e3)).toBe("2m 5s")),e("formats hours",()=>t(R(3725e3)).toBe("1h 2m")),e("formats zero",()=>t(R(0)).toBe("0s"))}),o("csvEscape",()=>{e("passes plain text through",()=>t(nt("hello")).toBe("hello")),e("wraps text with comma",()=>t(nt("a,b")).toBe('"a,b"')),e("escapes double quotes",()=>t(nt('say "hi"')).toBe('"say ""hi"""')),e("wraps text with newline",()=>t(nt(`a
|
|
25
|
+
b`)).toBe(`"a
|
|
26
|
+
b"`))}),o("shortenModelName",()=>{e("shortens opus-4.6",()=>t(ot("claude-opus-4-6-20260205")).toBe("opus-4.6")),e("shortens sonnet-4.6",()=>t(ot("claude-sonnet-4-6-20260217")).toBe("sonnet-4.6")),e("shortens opus-4",()=>t(ot("claude-opus-4-20250514")).toBe("opus-4")),e("shortens haiku-4.5",()=>t(ot("claude-haiku-4-5-20251001")).toBe("haiku-4.5")),e("shortens legacy sonnet-3.5",()=>t(ot("claude-3-5-sonnet-20241022")).toBe("sonnet-3.5")),e("strips claude- prefix for unknown",()=>t(ot("claude-custom-model")).toBe("custom-model")),e("returns non-claude model as-is",()=>t(ot("gpt-4")).toBe("gpt-4"))}),o("parseCostMode",()=>{e("accepts calculate",()=>t(S("calculate")).toBe("calculate")),e("accepts display",()=>t(S("display")).toBe("display")),e("accepts compare",()=>t(S("compare")).toBe("compare")),e("defaults to calculate",()=>t(S(void 0)).toBe("calculate"))})}function ot(o){let e={"claude-opus-4-6":"opus-4.6","claude-sonnet-4-6":"sonnet-4.6","claude-opus-4-5":"opus-4.5","claude-sonnet-4-5":"sonnet-4.5","claude-haiku-4-5":"haiku-4.5","claude-opus-4":"opus-4","claude-sonnet-4":"sonnet-4","claude-3-7-sonnet":"sonnet-3.7","claude-3-5-sonnet":"sonnet-3.5","claude-3-5-haiku":"haiku-3.5","claude-3-opus":"opus-3","claude-3-sonnet":"sonnet-3","claude-3-haiku":"haiku-3"};for(let[t,r]of Object.entries(e))if(o.startsWith(t))return r;return o.replace(/^claude-/,"").replace(/-\d{8}$/,"")}import{readFileSync as ce,writeFileSync as Ho,mkdirSync as Zo,unlinkSync as Uo,existsSync as Yo}from"fs";import{join as Ee}from"path";import{homedir as Jo}from"os";import Jt from"chalk";var $e=Ee(Jo(),".cctrack"),bt=Ee($e,"config.json");function lt(){try{let o=ce(bt,"utf-8");return JSON.parse(o).budget??{}}catch{return{}}}function Oe(o){Zo($e,{recursive:!0});let e={};try{e=JSON.parse(ce(bt,"utf-8"))}catch{}e.budget=o,Ho(bt,JSON.stringify(e,null,2)+`
|
|
27
|
+
`,"utf-8")}function Ie(){try{let o=ce(bt,"utf-8");return JSON.parse(o)}catch{return{}}}function Le(){Yo(bt)&&Uo(bt)}function Ae(){return bt}function K(o){return o>=Ft.exceeded?"exceeded":o>=Ft.critical?"critical":o>=Ft.warning?"warning":"safe"}function N(o,e){let t=e===0?o>0?100:0:o/e*100,r=K(t),c=Math.max(0,e-o);return{level:r,budget:e,spent:o,remaining:c,percentage:t}}function Mt(o){switch(o){case"safe":return Jt.green;case"warning":return Jt.yellow;case"critical":return Jt.red;case"exceeded":return Jt.bgRed.white}}function V(o,e=20){let t=Math.min(Math.max(o,0),100),r=Math.round(t/100*e),c=e-r,s="\u2588".repeat(r)+"\u2591".repeat(c),n=K(o),a=Mt(n),i=`${Math.round(o)}%`;return`${a(s)} ${i}`}if(import.meta.vitest){let{describe:o,it:e,expect:t,beforeEach:r,afterEach:c}=import.meta.vitest,{mkdtempSync:s,writeFileSync:n,readFileSync:a,rmSync:i}=await import("fs"),{tmpdir:d}=await import("os"),{join:m}=await import("path");o("loadBudgetConfig",()=>{e("returns empty object when no config file exists",()=>{let l=lt();t(typeof l).toBe("object"),t(l).toBeDefined()})}),o("getBudgetLevel",()=>{e("returns safe for 0%",()=>{t(K(0)).toBe("safe")}),e("returns safe for 25%",()=>{t(K(25)).toBe("safe")}),e("returns safe for 49.9%",()=>{t(K(49.9)).toBe("safe")}),e("returns warning at exactly 50%",()=>{t(K(50)).toBe("warning")}),e("returns warning for 75%",()=>{t(K(75)).toBe("warning")}),e("returns critical at exactly 80%",()=>{t(K(80)).toBe("critical")}),e("returns critical for 99%",()=>{t(K(99)).toBe("critical")}),e("returns exceeded at exactly 100%",()=>{t(K(100)).toBe("exceeded")}),e("returns exceeded for 150%",()=>{t(K(150)).toBe("exceeded")})}),o("calculateBudgetStatus",()=>{e("calculates 0% when nothing spent",()=>{let l=N(0,100);t(l.percentage).toBe(0),t(l.level).toBe("safe"),t(l.remaining).toBe(100),t(l.spent).toBe(0),t(l.budget).toBe(100)}),e("calculates 25% spent",()=>{let l=N(25,100);t(l.percentage).toBe(25),t(l.level).toBe("safe"),t(l.remaining).toBe(75)}),e("calculates 50% (warning threshold)",()=>{let l=N(50,100);t(l.percentage).toBe(50),t(l.level).toBe("warning"),t(l.remaining).toBe(50)}),e("calculates 75% (still warning)",()=>{let l=N(75,100);t(l.percentage).toBe(75),t(l.level).toBe("warning"),t(l.remaining).toBe(25)}),e("calculates 99% (critical)",()=>{let l=N(99,100);t(l.percentage).toBe(99),t(l.level).toBe("critical"),t(l.remaining).toBe(1)}),e("calculates 100% (exceeded)",()=>{let l=N(100,100);t(l.percentage).toBe(100),t(l.level).toBe("exceeded"),t(l.remaining).toBe(0)}),e("calculates 150% (exceeded, remaining clamped to 0)",()=>{let l=N(150,100);t(l.percentage).toBe(150),t(l.level).toBe("exceeded"),t(l.remaining).toBe(0)})}),o("formatBudgetBar",()=>{let l=u=>u.replace(/\x1B\[[0-9;]*m/g,"");e("shows empty bar at 0%",()=>{let u=l(V(0,20));t(u).toContain("\u2591".repeat(20)),t(u).toContain("0%")}),e("shows half-filled bar at 50%",()=>{let u=l(V(50,20));t(u).toContain("\u2588".repeat(10)),t(u).toContain("\u2591".repeat(10)),t(u).toContain("50%")}),e("shows full bar at 100%",()=>{let u=l(V(100,20));t(u).toContain("\u2588".repeat(20)),t(u).toContain("100%")}),e("clamps bar fill at 100% for values over 100%",()=>{let u=l(V(150,20));t(u).toContain("\u2588".repeat(20)),t(u).toContain("150%")}),e("uses default width of 20",()=>{let f=l(V(50)).split(" ")[0];t(f).toHaveLength(20)})}),o("budgetColor",()=>{e("returns green for safe",()=>{let l=Mt("safe");t(l("test")).toContain("test")}),e("returns yellow for warning",()=>{let l=Mt("warning");t(l("test")).toContain("test")}),e("returns red for critical",()=>{let l=Mt("critical");t(l("test")).toContain("test")}),e("returns bgRed.white for exceeded",()=>{let l=Mt("exceeded");t(l("test")).toContain("test")})}),o("calculateBudgetStatus edge cases",()=>{e("handles zero budget with zero spending",()=>{let l=N(0,0);t(l.percentage).toBe(0),t(l.level).toBe("safe"),t(l.remaining).toBe(0)}),e("handles spending against zero budget",()=>{let l=N(10,0);t(l.percentage).toBe(100),t(l.level).toBe("exceeded"),t(l.remaining).toBe(0)}),e("handles fractional dollar amounts without precision errors",()=>{let l=N(.30000000000000004,1);t(l.percentage).toBeCloseTo(30,10),t(l.remaining).toBeCloseTo(.7,10),t(l.level).toBe("safe")})}),o("getBudgetLevel edge cases",()=>{e("returns safe for negative percentage",()=>{t(K(-10)).toBe("safe")}),e("returns safe for NaN (documents behavior)",()=>{t(K(NaN)).toBe("safe")})}),o("formatBudgetBar edge cases",()=>{e("handles negative percentage without throwing",()=>{try{let l=V(-50,20);t(typeof l).toBe("string")}catch(l){t(l).toBeInstanceOf(RangeError)}}),e("bar at exactly 80% shows 16 filled blocks",()=>{let u=(f=>f.replace(/\x1B\[[0-9;]*m/g,""))(V(80,20));t(u).toContain("\u2588".repeat(16)),t(u).toContain("80%")})})}function q(o,e,t){if(o.length===0)return{hourly_cost:0,daily_cost:0,projected_monthly:0,hours_analyzed:0,insufficient_data:!0};let r=[...o].sort((y,g)=>y.timestamp.localeCompare(g.timestamp)),c=new Date(r[0].timestamp).getTime(),s=new Date(r[r.length-1].timestamp).getTime(),n=0;for(let y of r){let g=$(y,e);n+=g.cost.total_cost}let a=s-c,i=Math.max(a/(1e3*60*60),1),d=n/i,m=i/24,l=m>=1?n/m:d*24,u=l*30,f={hourly_cost:d,daily_cost:l,projected_monthly:u,hours_analyzed:i,insufficient_data:a<3600*1e3};if(t?.monthly!==void 0&&d>0){let y=t.monthly-n;y<=0?f.time_until_budget_exhausted_ms=0:f.time_until_budget_exhausted_ms=y/d*60*60*1e3}return f}if(import.meta.vitest){let{describe:o,it:e,expect:t,beforeEach:r}=import.meta.vitest,{setPricingData:c}=await Promise.resolve().then(()=>(_t(),Zt)),s={version:"test",models:{"claude-sonnet-4-20250514":{input_cost_per_million:3,output_cost_per_million:15,cache_creation_cost_per_million:3.75,cache_read_cost_per_million:.3,context_window:2e5}},aliases:{}};r(()=>{c(s)});let{makeEntry:n}=await Promise.resolve().then(()=>(St(),Bt));o("calculateBurnRate",()=>{e("returns zero rates for zero entries",()=>{let a=q([],"calculate");t(a.hourly_cost).toBe(0),t(a.daily_cost).toBe(0),t(a.projected_monthly).toBe(0),t(a.hours_analyzed).toBe(0),t(a.time_until_budget_exhausted_ms).toBeUndefined()}),e("calculates burn rate with 1 hour of data",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T11:00:00Z"})],i=q(a,"calculate");t(i.hours_analyzed).toBeCloseTo(1,2),t(i.hourly_cost).toBeGreaterThan(0),t(i.daily_cost).toBeCloseTo(i.hourly_cost*24,6),t(i.projected_monthly).toBeCloseTo(i.daily_cost*30,6)}),e("calculates burn rate with multiple days of data",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-26T10:00:00Z"}),n({timestamp:"2025-03-27T10:00:00Z"})],i=q(a,"calculate");t(i.hours_analyzed).toBeCloseTo(48,2);let d=i.hours_analyzed/24;t(d).toBeGreaterThanOrEqual(1),t(i.projected_monthly).toBeCloseTo(i.daily_cost*30,6)}),e("projected monthly calculation is accurate",()=>{let a=[n({timestamp:"2025-03-25T00:00:00Z"}),n({timestamp:"2025-03-26T00:00:00Z"})],i=q(a,"calculate");t(i.projected_monthly).toBeCloseTo(i.daily_cost*30,6),t(i.projected_monthly).toBeGreaterThan(0)}),e("calculates time_until_budget_exhausted with budget config",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T11:00:00Z"})],i=q(a,"calculate",{monthly:100});t(i.time_until_budget_exhausted_ms).toBeDefined(),t(i.time_until_budget_exhausted_ms).toBeGreaterThan(0)}),e("returns 0 exhaustion time when budget already exceeded",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T11:00:00Z"})],i=q(a,"calculate",{monthly:0});t(i.time_until_budget_exhausted_ms).toBe(0)}),e("does not include exhaustion time without budget config",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T11:00:00Z"})],i=q(a,"calculate");t(i.time_until_budget_exhausted_ms).toBeUndefined()}),e("clamps to minimum 1 hour for a single entry (avoids infinite rates)",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"})],i=q(a,"calculate");t(i.hours_analyzed).toBe(1),t(Number.isFinite(i.hourly_cost)).toBe(!0),t(i.hourly_cost).toBeGreaterThan(0),t(i.daily_cost).toBeCloseTo(i.hourly_cost*24,6)}),e("hourly cost matches hand-calculated value",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T12:00:00Z"})],i=q(a,"calculate");t(i.hourly_cost).toBeCloseTo(.0105,6),t(i.hours_analyzed).toBeCloseTo(2,2)}),e("time_until_budget_exhausted_ms matches expected value",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T12:00:00Z"})],i=q(a,"calculate",{monthly:1}),u=(1-.021)/.0105*60*60*1e3;t(i.time_until_budget_exhausted_ms).toBeDefined(),t(i.time_until_budget_exhausted_ms).toBeCloseTo(u,-1),t(i.time_until_budget_exhausted_ms).toBeGreaterThan(0)}),e("returns 0 exhaustion time when spending exceeds budget (realistic overspend)",()=>{let a=[n({timestamp:"2025-03-25T10:00:00Z"}),n({timestamp:"2025-03-25T12:00:00Z"})],i=q(a,"calculate",{monthly:.01});t(i.time_until_budget_exhausted_ms).toBe(0)}),e("handles unsorted entries correctly",()=>{let a=[n({timestamp:"2025-03-25T12:00:00Z"}),n({timestamp:"2025-03-25T10:00:00Z"})],i=q(a,"calculate");t(i.hours_analyzed).toBeCloseTo(2,2),t(i.hourly_cost).toBeCloseTo(.0105,6)})})}function Wo(o,e){let t=[];if(e){t.push("date,model,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,total_tokens,cost");for(let r of o)for(let[c,s]of Object.entries(r.models))t.push([r.date,c,s.tokens.input_tokens,s.tokens.output_tokens,s.tokens.cache_write_tokens,s.tokens.cache_read_tokens,s.tokens.total_tokens,s.cost.total_cost.toFixed(6)].join(","))}else{t.push("date,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,total_tokens,cost");for(let r of o)t.push([r.date,r.tokens.input_tokens,r.tokens.output_tokens,r.tokens.cache_write_tokens,r.tokens.cache_read_tokens,r.tokens.total_tokens,r.cost.total_cost.toFixed(6)].join(","))}return t.join(`
|
|
28
|
+
`)}function Go(o,e){if(o.length===0){console.log(kt.yellow("No data found for the specified range."));return}if(e){let s=new Pe({head:["Date","Model","Input","Output","Cache Write","Cache Read","Total","Cost"].map(n=>kt.cyan(n)),colAligns:["left","left","right","right","right","right","right","right"],style:{head:[],border:[]}});for(let n of o)for(let[a,i]of Object.entries(n.models))s.push([n.date,a,_(i.tokens.input_tokens),_(i.tokens.output_tokens),_(i.tokens.cache_write_tokens),_(i.tokens.cache_read_tokens),_(i.tokens.total_tokens),h(i.cost.total_cost)]);console.log(s.toString())}else{let s=new Pe({head:["Date","Input","Output","Cache Write","Cache Read","Total","Cost"].map(a=>kt.cyan(a)),colAligns:["left","right","right","right","right","right","right"],style:{head:[],border:[]}}),n=Math.max(...o.map(a=>a.cost.total_cost),1);for(let a of o){let i=Math.round(a.cost.total_cost/n*8),d=kt.dim("\u2588".repeat(i)+"\u2591".repeat(8-i));s.push([a.date,_(a.tokens.input_tokens),_(a.tokens.output_tokens),_(a.tokens.cache_write_tokens),_(a.tokens.cache_read_tokens),_(a.tokens.total_tokens),h(a.cost.total_cost)+" "+d])}console.log(s.toString())}let t=lt();if(t.daily!=null){let s=new Date().toISOString().slice(0,10),a=o.find(d=>d.date===s)?.cost.total_cost??0,i=N(a,t.daily);console.log(),console.log(`Daily Budget: ${V(i.percentage)} (${h(i.spent)} / ${h(i.budget)})`)}let r=o.reduce((s,n)=>s+n.cost.total_cost,0),c=o.reduce((s,n)=>s+n.tokens.total_tokens,0);console.log(kt.dim("\u2500".repeat(60))),console.log(kt.bold(`Total: ${_(c)} tokens, ${h(r)}`))}function Ko(o,e){let t=q(o,e);t.insufficient_data||console.log(kt.dim(`Burn rate: ${h(t.hourly_cost)}/hr, ${h(t.daily_cost)}/day \u2192 projected ${h(t.projected_monthly)}/month`))}function Re(o){o.command("daily").description("Show daily usage breakdown").option("--json","Output as JSON").option("--csv","Output as CSV").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--breakdown","Show per-model breakdown").option("--mode <mode>","Cost mode: calculate|display|compare","calculate").option("--timezone <tz>","Timezone for date grouping (e.g. America/New_York)").action(async e=>{let t=I(),r=B(t),{entries:c}=await j(r),s=x(c),n=D(s,{since:e.since,until:e.until,project:e.project,timezone:e.timezone}),a=S(e.mode),i=st(n,a,e.timezone);e.json?console.log(JSON.stringify(i,null,2)):e.csv?console.log(Wo(i,e.breakdown)):(Go(i,e.breakdown),Ko(n,a))})}import Et from"chalk";import ze from"cli-table3";function Vo(o,e){let t=[];if(e){t.push("month,model,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,total_tokens,cost");for(let r of o)for(let[c,s]of Object.entries(r.models))t.push([r.month,c,s.tokens.input_tokens,s.tokens.output_tokens,s.tokens.cache_write_tokens,s.tokens.cache_read_tokens,s.tokens.total_tokens,s.cost.total_cost.toFixed(6)].join(","))}else{t.push("month,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,total_tokens,cost");for(let r of o)t.push([r.month,r.tokens.input_tokens,r.tokens.output_tokens,r.tokens.cache_write_tokens,r.tokens.cache_read_tokens,r.tokens.total_tokens,r.cost.total_cost.toFixed(6)].join(","))}return t.join(`
|
|
29
|
+
`)}function Xo(o,e){if(o.length===0){console.log(Et.yellow("No data found for the specified range."));return}if(e){let s=new ze({head:["Month","Model","Input","Output","Cache Write","Cache Read","Total","Cost"].map(n=>Et.cyan(n)),colAligns:["left","left","right","right","right","right","right","right"],style:{head:[],border:[]}});for(let n of o)for(let[a,i]of Object.entries(n.models))s.push([n.month,a,_(i.tokens.input_tokens),_(i.tokens.output_tokens),_(i.tokens.cache_write_tokens),_(i.tokens.cache_read_tokens),_(i.tokens.total_tokens),h(i.cost.total_cost)]);console.log(s.toString())}else{let s=new ze({head:["Month","Input","Output","Cache Write","Cache Read","Total","Cost"].map(n=>Et.cyan(n)),colAligns:["left","right","right","right","right","right","right"],style:{head:[],border:[]}});for(let n of o)s.push([n.month,_(n.tokens.input_tokens),_(n.tokens.output_tokens),_(n.tokens.cache_write_tokens),_(n.tokens.cache_read_tokens),_(n.tokens.total_tokens),h(n.cost.total_cost)]);console.log(s.toString())}let t=lt();if(t.monthly!=null){let s=new Date().toISOString().slice(0,7),a=o.find(d=>d.month===s)?.cost.total_cost??0,i=N(a,t.monthly);console.log(),console.log(`Monthly Budget: ${V(i.percentage)} (${h(i.spent)} / ${h(i.budget)})`)}let r=o.reduce((s,n)=>s+n.cost.total_cost,0),c=o.reduce((s,n)=>s+n.tokens.total_tokens,0);console.log(Et.dim("\u2500".repeat(60))),console.log(Et.bold(`Total: ${_(c)} tokens, ${h(r)}`))}function Fe(o){o.command("monthly").description("Show monthly usage breakdown").option("--json","Output as JSON").option("--csv","Output as CSV").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--breakdown","Show per-model breakdown").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for date grouping (e.g. America/New_York)").action(async e=>{let t=I(),r=B(t),{entries:c}=await j(r),s=x(c),n=D(s,{since:e.since,until:e.until,project:e.project,timezone:e.timezone}),a=S(e.mode),i=Yt(n,a,e.timezone);e.json?console.log(JSON.stringify(i,null,2)):e.csv?console.log(Vo(i,e.breakdown)):Xo(i,e.breakdown)})}import Wt from"chalk";import Qo from"cli-table3";function qe(o){return new Date(o.endTime).getTime()-new Date(o.startTime).getTime()}function Ne(o,e=12){return o.length<=e?o:o.slice(0,e)+"..."}function tn(o){let e=[];e.push("session_id,project,model,duration_ms,requests,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,total_tokens,cost");for(let t of o){let r=qe(t);e.push([nt(t.sessionId),nt(t.project),nt(t.primaryModel),r,t.request_count,t.tokens.input_tokens,t.tokens.output_tokens,t.tokens.cache_write_tokens,t.tokens.cache_read_tokens,t.tokens.total_tokens,t.cost.total_cost.toFixed(6)].join(","))}return e.join(`
|
|
30
|
+
`)}function en(o,e=!1){if(o.length===0){console.log(Wt.yellow("No sessions found for the specified range."));return}let t=new Qo({head:["Session ID","Project","Model","Duration","Requests","Tokens","Cost"].map(n=>Wt.cyan(n)),colAligns:["left","left","left","right","right","right","right"],style:{head:[],border:[]}});for(let n of o){let a=qe(n);t.push([e?n.sessionId:Ne(n.sessionId),e?n.project:Ne(n.project,20),ot(n.primaryModel)+(Object.keys(n.models||{}).length>1?Wt.dim(` +${Object.keys(n.models).length-1}`):""),R(a),n.request_count.toString(),_(n.tokens.total_tokens),h(n.cost.total_cost)])}console.log(t.toString());let r=o.reduce((n,a)=>n+a.cost.total_cost,0),c=o.reduce((n,a)=>n+a.tokens.total_tokens,0),s=o.reduce((n,a)=>n+a.request_count,0);console.log(Wt.bold(`
|
|
31
|
+
${o.length} sessions, ${s} requests, ${_(c)} tokens, ${h(r)}`))}function He(o){o.command("session").description("Show session-level usage").option("--json","Output as JSON").option("--csv","Output as CSV").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for filtering").option("--full","Show full session IDs and project names (no truncation)").action(async e=>{let t=I(),r=B(t),{entries:c}=await j(r),s=x(c),n=D(s,{since:e.since,until:e.until,project:e.project,timezone:e.timezone}),a=S(e.mode),i=ct(n,a);e.json?console.log(JSON.stringify(i,null,2)):e.csv?console.log(tn(i)):en(i,!!e.full)})}import{writeFileSync as Ze,mkdirSync as Ue,existsSync as on}from"fs";import{join as Ye,dirname as Je}from"path";import{homedir as We}from"os";import $t from"chalk";function nn(o){let e=JSON.stringify(o),t=JSON.stringify(e),r=a=>a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),c=o.projects.map(a=>`<option value="${r(a.project)}">${r(a.project)}</option>`).join(`
|
|
32
|
+
`),s=o.date_range.start?o.date_range.start.slice(0,10):"",n=o.date_range.end?o.date_range.end.slice(0,10):"";return`<!DOCTYPE html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="UTF-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
37
|
+
<title>CCTrack Dashboard</title>
|
|
38
|
+
<!-- Apache ECharts: Apache License 2.0 - https://echarts.apache.org -->
|
|
39
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script>
|
|
40
|
+
<script>var DATA = JSON.parse(${t});</script>
|
|
41
|
+
<style>
|
|
42
|
+
:root{--bg:#0f172a;--card:#1e293b;--border:#334155;--text:#e2e8f0;--muted:#94a3b8;--accent:#6366f1;--green:#22c55e;--red:#ef4444;--yellow:#eab308;--cyan:#06b6d4;--blue:#3b82f6;--hm0:#1e293b;--hm1:#2d3a4a;--hm2:#365314;--hm3:#4d7c0f;--hm4:#ca8a04;--hm5:#dc2626}
|
|
43
|
+
.light{--bg:#f8fafc;--card:#fff;--border:#e2e8f0;--text:#1e293b;--muted:#64748b;--hm0:#f1f5f9;--hm1:#d9f99d;--hm2:#84cc16;--hm3:#ca8a04;--hm4:#ea580c;--hm5:#dc2626}
|
|
44
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
45
|
+
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);padding:24px;min-height:100vh;max-width:100vw;overflow-x:hidden}
|
|
46
|
+
.header{margin-bottom:20px;padding-right:50px}
|
|
47
|
+
.header h1{font-size:1.4rem;font-weight:700}.header .sub{color:var(--muted);font-size:.8rem;margin-top:2px}
|
|
48
|
+
.toggle{position:fixed;top:24px;right:24px;z-index:200;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:6px 10px;cursor:pointer;color:var(--text);font-size:1rem;box-shadow:0 2px 8px rgba(0,0,0,.2)}
|
|
49
|
+
.filters{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px 20px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:20px}
|
|
50
|
+
.filter-group{display:flex;align-items:center;gap:6px}
|
|
51
|
+
.filters label{color:var(--muted);font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;font-weight:600;white-space:nowrap}
|
|
52
|
+
.filters input,.filters select{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:6px 10px;color:var(--text);font-size:.85rem;max-width:100%}
|
|
53
|
+
.filters select{min-width:140px}
|
|
54
|
+
.filter-actions{display:flex;gap:8px}
|
|
55
|
+
.btn{padding:6px 16px;border-radius:6px;border:none;cursor:pointer;font-size:.85rem;font-weight:600}
|
|
56
|
+
.btn-apply{background:var(--accent);color:#fff}.btn-reset{background:var(--card);color:var(--text);border:1px solid var(--border)}
|
|
57
|
+
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
|
|
58
|
+
.stat{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 20px;min-width:0;overflow:hidden}
|
|
59
|
+
.stat-label{color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
|
|
60
|
+
.stat-value{font-size:1.6rem;font-weight:700;margin-top:4px;font-variant-numeric:tabular-nums}
|
|
61
|
+
.stat-value.green{color:var(--green)}
|
|
62
|
+
.grid{display:grid;gap:16px;margin-bottom:16px}
|
|
63
|
+
.grid-1{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr 1fr}
|
|
64
|
+
.grid-2-1{grid-template-columns:2fr 1fr}
|
|
65
|
+
.panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;position:relative;min-width:0}
|
|
66
|
+
.panel-title{font-size:.85rem;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
67
|
+
.panel-title::before{content:'';width:8px;height:8px;border-radius:50%;display:inline-block}
|
|
68
|
+
.pt-blue::before{background:var(--blue)}.pt-yellow::before{background:var(--yellow)}.pt-green::before{background:var(--green)}.pt-accent::before{background:var(--accent)}.pt-cyan::before{background:var(--cyan)}.pt-red::before{background:var(--red)}
|
|
69
|
+
.chart-container{height:320px;width:100%}
|
|
70
|
+
.chart-sm{height:260px}
|
|
71
|
+
.chart-tall{min-height:300px}
|
|
72
|
+
.heatmap-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
73
|
+
.heatmap{display:grid;grid-template-columns:40px repeat(24,1fr);gap:3px;font-size:.7rem;padding:8px 0;min-width:600px}
|
|
74
|
+
.hm-label{color:var(--muted);display:flex;align-items:center;justify-content:flex-end;padding-right:8px;font-weight:600}
|
|
75
|
+
.hm-cell{aspect-ratio:1;border-radius:3px;min-width:16px;min-height:16px;position:relative;cursor:default}
|
|
76
|
+
.hm-cell:hover .hm-tip{display:block}
|
|
77
|
+
.hm-tip{display:none;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:.7rem;white-space:nowrap;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,.3)}
|
|
78
|
+
.tbl-wrap{max-height:400px;overflow:auto;border:1px solid var(--border);border-radius:8px}
|
|
79
|
+
table{width:100%;border-collapse:collapse}
|
|
80
|
+
th{position:sticky;top:0;background:var(--card);text-align:left;padding:10px 12px;font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);border-bottom:1px solid var(--border);cursor:pointer;user-select:none;font-weight:600}
|
|
81
|
+
th:hover{color:var(--text)}
|
|
82
|
+
td{padding:8px 12px;border-bottom:1px solid var(--border);font-size:.8rem;font-variant-numeric:tabular-nums}
|
|
83
|
+
.text-right{text-align:right}.text-mono{font-family:ui-monospace,monospace;font-size:.75rem}
|
|
84
|
+
.roi-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
|
|
85
|
+
.roi-card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center;min-width:0}
|
|
86
|
+
.roi-label{color:var(--muted);font-size:.65rem;text-transform:uppercase;letter-spacing:.04em;font-weight:600}
|
|
87
|
+
.roi-value{font-size:1.3rem;font-weight:700;margin-top:4px}.roi-sub{color:var(--muted);font-size:.7rem;margin-top:2px}
|
|
88
|
+
.footer{text-align:center;color:var(--muted);font-size:.7rem;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)}
|
|
89
|
+
@media(max-width:1024px){.grid-2-1{grid-template-columns:1fr 1fr}}
|
|
90
|
+
@media(max-width:768px){.stats{grid-template-columns:repeat(2,1fr)}.roi-grid{grid-template-columns:repeat(2,1fr)}.grid-2,.grid-2-1{grid-template-columns:1fr}.filters{flex-direction:column;align-items:stretch}.filter-group{flex-direction:column;align-items:stretch}.filter-group label{margin-bottom:2px}.filters select,.filters input{min-width:0;width:100%}.filter-actions{justify-content:stretch}.filter-actions .btn{flex:1}.header h1{font-size:1.2rem}.toggle{top:16px;right:16px}.stat-value{font-size:1.3rem}.chart-container{height:280px}.chart-sm{height:240px}body{padding:16px}}
|
|
91
|
+
@media(max-width:480px){.stats{grid-template-columns:1fr}.roi-grid{grid-template-columns:repeat(2,1fr)}.stat{padding:12px 16px}.stat-value{font-size:1.1rem}.roi-value{font-size:1rem}.chart-container{height:240px}.chart-sm{height:200px}.toggle{top:10px;right:10px}body{padding:10px}.grid{gap:10px}.panel{padding:10px}.panel-title{font-size:.8rem;margin-bottom:8px}.filters label{font-size:.7rem}.filters input,.filters select{font-size:.8rem;padding:5px 8px}.footer{font-size:.65rem}}
|
|
92
|
+
@media print{body{background:#fff;color:#000}.panel,.stat,.roi-card{border-color:#ddd;break-inside:avoid}.filters,.toggle{display:none!important}.chart-print-img{width:100%;height:auto}}
|
|
93
|
+
</style>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<div class="header">
|
|
97
|
+
<div><h1>CCTrack Dashboard</h1><div class="sub">${r(s)} \u2014 ${r(n)} · Generated ${new Date().toLocaleString()}</div></div>
|
|
98
|
+
<button class="toggle" id="themeToggle" title="Toggle theme">\u2600</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="filters">
|
|
102
|
+
<div class="filter-group"><label>From</label><input type="date" id="dateStart" value="${r(s)}"></div>
|
|
103
|
+
<div class="filter-group"><label>To</label><input type="date" id="dateEnd" value="${r(n)}"></div>
|
|
104
|
+
<div class="filter-group"><label>Project</label><select id="projectFilter"><option value="">All Projects</option>${c}</select></div>
|
|
105
|
+
<div class="filter-actions"><button class="btn btn-apply" id="btnApply">Apply</button><button class="btn btn-reset" id="btnReset">Reset</button></div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="stats">
|
|
109
|
+
<div class="stat"><div class="stat-label">Total Cost</div><div class="stat-value" id="statCost" style="color:var(--accent)"></div></div>
|
|
110
|
+
<div class="stat"><div class="stat-label">Total Tokens</div><div class="stat-value" id="statTokens"></div></div>
|
|
111
|
+
<div class="stat"><div class="stat-label">Total Requests</div><div class="stat-value" id="statReqs"></div></div>
|
|
112
|
+
<div class="stat"><div class="stat-label">Active Sessions</div><div class="stat-value" id="statSessions"></div></div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="grid grid-1"><div class="panel"><div class="panel-title pt-blue">Cost Over Time</div><div class="chart-container" id="chartCost" role="img" aria-label="Bar chart showing daily cost over time with cumulative line"></div></div></div>
|
|
116
|
+
<div class="grid grid-2">
|
|
117
|
+
<div class="panel"><div class="panel-title pt-cyan">Input / Output Tokens</div><div class="chart-container" id="chartIO" role="img" aria-label="Stacked bar chart of input and output tokens per day"></div></div>
|
|
118
|
+
<div class="panel"><div class="panel-title pt-yellow">Cache Tokens</div><div class="chart-container" id="chartCache" role="img" aria-label="Stacked bar chart of cache write and read tokens per day"></div></div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="grid grid-2-1">
|
|
121
|
+
<div class="panel" id="projectPanel"><div class="panel-title pt-accent">Project Breakdown</div><div class="chart-container chart-tall" id="chartProject" style="height:${Math.max(280,o.projects.length*44)}px"></div></div>
|
|
122
|
+
<div class="panel"><div class="panel-title pt-green">Model Distribution</div><div class="chart-container" id="chartModel" style="min-height:320px"></div></div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="grid grid-1"><div class="panel"><div class="panel-title pt-green">Cache Reuse Efficiency</div><div class="chart-container chart-sm" id="chartCacheEff"></div></div></div>
|
|
125
|
+
|
|
126
|
+
<div class="grid grid-1"><div class="panel">
|
|
127
|
+
<div class="panel-title pt-blue">Usage Heatmap</div>
|
|
128
|
+
<div style="color:var(--muted);font-size:.75rem;margin-bottom:10px" id="heatmapDesc">When do you use Claude the most? Each cell shows total tokens processed at that day-of-week + hour, aggregated across all dates in the range.</div>
|
|
129
|
+
<div class="heatmap-wrap"><div id="heatmap" class="heatmap"></div></div>
|
|
130
|
+
<div style="display:flex;align-items:center;gap:8px;margin-top:10px;font-size:.7rem;color:var(--muted)">
|
|
131
|
+
<span>Less</span>
|
|
132
|
+
<span style="width:14px;height:14px;border-radius:2px;background:var(--hm0)"></span>
|
|
133
|
+
<span style="width:14px;height:14px;border-radius:2px;background:var(--hm1)"></span>
|
|
134
|
+
<span style="width:14px;height:14px;border-radius:2px;background:var(--hm2)"></span>
|
|
135
|
+
<span style="width:14px;height:14px;border-radius:2px;background:var(--hm3)"></span>
|
|
136
|
+
<span style="width:14px;height:14px;border-radius:2px;background:var(--hm4)"></span>
|
|
137
|
+
<span style="width:14px;height:14px;border-radius:2px;background:var(--hm5)"></span>
|
|
138
|
+
<span>More</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div></div>
|
|
141
|
+
|
|
142
|
+
<div class="grid grid-1"><div class="panel">
|
|
143
|
+
<div class="panel-title pt-accent">Sessions <span id="sessCount" style="color:var(--muted);font-weight:400;font-size:.75rem"></span></div>
|
|
144
|
+
<div class="tbl-wrap">
|
|
145
|
+
<table id="sessTable">
|
|
146
|
+
<thead><tr><th data-col="session">Session</th><th data-col="project">Project</th><th data-col="model">Model</th><th data-col="duration" class="text-right">Duration</th><th data-col="requests" class="text-right">Requests</th><th data-col="tokens" class="text-right">Tokens</th><th data-col="cost" class="text-right">Cost</th></tr></thead>
|
|
147
|
+
<tbody id="sessBody"></tbody>
|
|
148
|
+
</table>
|
|
149
|
+
</div>
|
|
150
|
+
</div></div>
|
|
151
|
+
|
|
152
|
+
<div class="grid grid-1"><div class="panel">
|
|
153
|
+
<div class="panel-title pt-red">ROI Analysis</div>
|
|
154
|
+
<div class="roi-grid" id="roiCards"></div>
|
|
155
|
+
<div class="chart-container chart-sm" id="chartROI"></div>
|
|
156
|
+
</div></div>
|
|
157
|
+
|
|
158
|
+
<div class="footer">Generated by cctrack · <a href="https://github.com/azharuddinkhan3005/cctrack" style="color:var(--accent)">github</a></div>
|
|
159
|
+
|
|
160
|
+
<script>
|
|
161
|
+
(function(){
|
|
162
|
+
// \u2500\u2500 Helpers \u2500\u2500
|
|
163
|
+
function fmt(n){if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return n.toLocaleString();}
|
|
164
|
+
function fmtCost(n){return n<0.01&&n>0?'$'+n.toFixed(4):'$'+n.toFixed(2);}
|
|
165
|
+
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
|
166
|
+
function cumul(arr){var r=[],s=0;arr.forEach(function(v){s+=v;r.push(s);});return r;}
|
|
167
|
+
function isDark(){return!document.documentElement.classList.contains('light');}
|
|
168
|
+
function duration(start,end){var ms=new Date(end)-new Date(start);var s=Math.floor(ms/1000);if(s<60)return s+'s';var m=Math.floor(s/60);if(m<60)return m+'m '+s%60+'s';var h=Math.floor(m/60);return h+'h '+m%60+'m';}
|
|
169
|
+
|
|
170
|
+
var allData=DATA;
|
|
171
|
+
var charts={};
|
|
172
|
+
var COLORS=['#6366f1','#06b6d4','#22c55e','#eab308','#f97316','#ef4444','#ec4899','#8b5cf6','#14b8a6','#84cc16'];
|
|
173
|
+
|
|
174
|
+
// \u2500\u2500 Theme \u2500\u2500
|
|
175
|
+
function textColor(){return isDark()?'#94a3b8':'#64748b';}
|
|
176
|
+
function gridColor(){return isDark()?'rgba(148,163,184,0.08)':'rgba(148,163,184,0.15)';}
|
|
177
|
+
function tooltipBg(){return isDark()?'rgba(15,23,42,0.95)':'rgba(255,255,255,0.95)';}
|
|
178
|
+
function tooltipBorder(){return isDark()?'#334155':'#e2e8f0';}
|
|
179
|
+
function tooltipText(){return isDark()?'#e2e8f0':'#1e293b';}
|
|
180
|
+
|
|
181
|
+
function baseTooltip(){
|
|
182
|
+
return{trigger:'axis',confine:true,appendToBody:true,backgroundColor:tooltipBg(),borderColor:tooltipBorder(),textStyle:{color:tooltipText(),fontSize:12}};
|
|
183
|
+
}
|
|
184
|
+
function itemTooltip(){
|
|
185
|
+
return{trigger:'item',confine:true,appendToBody:true,backgroundColor:tooltipBg(),borderColor:tooltipBorder(),textStyle:{color:tooltipText(),fontSize:12}};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// \u2500\u2500 Chart Init \u2500\u2500
|
|
189
|
+
function initChart(id,option){
|
|
190
|
+
var dom=document.getElementById(id);
|
|
191
|
+
if(!dom)return null;
|
|
192
|
+
var c=echarts.init(dom,isDark()?'dark':null);
|
|
193
|
+
// Enable ARIA for screen readers
|
|
194
|
+
option.aria={enabled:true,decal:{show:false}};
|
|
195
|
+
c.setOption(option);
|
|
196
|
+
charts[id]=c;
|
|
197
|
+
return c;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// \u2500\u2500 Stats \u2500\u2500
|
|
201
|
+
function updateStats(totals,sessionCount){
|
|
202
|
+
document.getElementById('statCost').textContent=fmtCost(totals.cost.total_cost);
|
|
203
|
+
document.getElementById('statTokens').textContent=fmt(totals.tokens.total_tokens);
|
|
204
|
+
document.getElementById('statReqs').textContent=totals.request_count.toLocaleString();
|
|
205
|
+
document.getElementById('statSessions').textContent=sessionCount.toLocaleString();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// \u2500\u2500 Chart Option Builders \u2500\u2500
|
|
209
|
+
function costOption(daily){
|
|
210
|
+
var labels=daily.map(function(d){return d.date;});
|
|
211
|
+
var costs=daily.map(function(d){return d.cost?d.cost.total_cost:d.cost_val||0;});
|
|
212
|
+
var cum=cumul(costs);
|
|
213
|
+
return{
|
|
214
|
+
tooltip:Object.assign(baseTooltip(),{formatter:function(p){
|
|
215
|
+
var h='<b>'+p[0].axisValueLabel+'</b><br>';
|
|
216
|
+
p.forEach(function(i){h+='<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+i.color+';margin-right:6px"></span>'+i.seriesName+': <b>'+fmtCost(i.value)+'</b><br>';});
|
|
217
|
+
return h;
|
|
218
|
+
}}),
|
|
219
|
+
grid:{left:'3%',right:'3%',top:35,bottom:15,containLabel:true},
|
|
220
|
+
xAxis:{type:'category',data:labels,axisLabel:{color:textColor(),rotate:labels.length>7?45:0,hideOverlap:true},axisLine:{lineStyle:{color:gridColor()}}},
|
|
221
|
+
yAxis:[
|
|
222
|
+
{type:'value',name:'Daily ($)',nameTextStyle:{color:textColor(),padding:[0,0,0,40]},axisLabel:{formatter:function(v){return fmtCost(v);},color:textColor()},splitLine:{lineStyle:{color:gridColor()}}},
|
|
223
|
+
{type:'value',name:'Cumulative ($)',nameTextStyle:{color:textColor(),padding:[0,40,0,0]},axisLabel:{formatter:function(v){return fmtCost(v);},color:textColor()},splitLine:{show:false}}
|
|
224
|
+
],
|
|
225
|
+
series:[
|
|
226
|
+
{name:'Daily Cost',type:'bar',data:costs,barMaxWidth:50,itemStyle:{color:'#6366f1',borderRadius:[4,4,0,0]},emphasis:{itemStyle:{color:'#818cf8'}}},
|
|
227
|
+
{name:'Cumulative',type:'line',yAxisIndex:1,data:cum,smooth:true,symbol:costs.length<=2?'circle':'none',symbolSize:8,lineStyle:{color:'#06b6d4',type:'dashed',width:2},itemStyle:{color:'#06b6d4'}}
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ioOption(daily){
|
|
233
|
+
var labels=daily.map(function(d){return d.date;});
|
|
234
|
+
return{
|
|
235
|
+
tooltip:Object.assign(baseTooltip(),{formatter:function(p){
|
|
236
|
+
var h='<b>'+p[0].axisValueLabel+'</b><br>';
|
|
237
|
+
p.forEach(function(i){h+='<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+i.color+';margin-right:6px"></span>'+i.seriesName+': <b>'+fmt(i.value)+'</b><br>';});
|
|
238
|
+
return h;
|
|
239
|
+
}}),
|
|
240
|
+
grid:{left:'3%',right:'3%',top:35,bottom:15,containLabel:true},
|
|
241
|
+
xAxis:{type:'category',data:labels,axisLabel:{color:textColor(),rotate:labels.length>7?45:0,hideOverlap:true},axisLine:{lineStyle:{color:gridColor()}}},
|
|
242
|
+
yAxis:{type:'value',name:'Tokens',nameTextStyle:{color:textColor(),padding:[0,0,0,30]},axisLabel:{formatter:function(v){return fmt(v);},color:textColor()},splitLine:{lineStyle:{color:gridColor()}}},
|
|
243
|
+
series:[
|
|
244
|
+
{name:'Input',type:'bar',stack:'io',data:daily.map(function(d){return d.tokens?d.tokens.input_tokens:d.input||0;}),barMaxWidth:50,itemStyle:{color:'#3b82f6'}},
|
|
245
|
+
{name:'Output',type:'bar',stack:'io',data:daily.map(function(d){return d.tokens?d.tokens.output_tokens:d.output||0;}),barMaxWidth:50,itemStyle:{color:'#06b6d4'}}
|
|
246
|
+
]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cacheOption(daily){
|
|
251
|
+
var labels=daily.map(function(d){return d.date;});
|
|
252
|
+
return{
|
|
253
|
+
tooltip:Object.assign(baseTooltip(),{formatter:function(p){
|
|
254
|
+
var h='<b>'+p[0].axisValueLabel+'</b><br>';
|
|
255
|
+
p.forEach(function(i){h+='<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+i.color+';margin-right:6px"></span>'+i.seriesName+': <b>'+fmt(i.value)+'</b><br>';});
|
|
256
|
+
return h;
|
|
257
|
+
}}),
|
|
258
|
+
grid:{left:'3%',right:'3%',top:35,bottom:15,containLabel:true},
|
|
259
|
+
xAxis:{type:'category',data:labels,axisLabel:{color:textColor(),rotate:labels.length>7?45:0,hideOverlap:true},axisLine:{lineStyle:{color:gridColor()}}},
|
|
260
|
+
yAxis:{type:'value',name:'Cache Tokens',nameTextStyle:{color:textColor(),padding:[0,0,0,30]},axisLabel:{formatter:function(v){return fmt(v);},color:textColor()},splitLine:{lineStyle:{color:gridColor()}}},
|
|
261
|
+
series:[
|
|
262
|
+
{name:'Cache Write',type:'bar',stack:'cache',data:daily.map(function(d){return d.tokens?d.tokens.cache_write_tokens:d.cw||0;}),barMaxWidth:50,itemStyle:{color:'#eab308'}},
|
|
263
|
+
{name:'Cache Read',type:'bar',stack:'cache',data:daily.map(function(d){return d.tokens?d.tokens.cache_read_tokens:d.cr||0;}),barMaxWidth:50,itemStyle:{color:'#22c55e'}}
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function projectOption(projects){
|
|
269
|
+
var sorted=projects.slice().sort(function(a,b){return a.cost.total_cost-b.cost.total_cost;});
|
|
270
|
+
var maxLabelLen=Math.max.apply(null,sorted.map(function(p){return p.project.length;}));
|
|
271
|
+
return{
|
|
272
|
+
tooltip:Object.assign(itemTooltip(),{formatter:function(p){return'<b>'+esc(p.name)+'</b><br>Cost: <b>'+fmtCost(p.value)+'</b>';}}),
|
|
273
|
+
grid:{left:'3%',right:'8%',top:20,bottom:20,containLabel:true},
|
|
274
|
+
xAxis:{type:'value',axisLabel:{formatter:function(v){return fmtCost(v);},color:textColor()},splitLine:{lineStyle:{color:gridColor()}}},
|
|
275
|
+
yAxis:{type:'category',data:sorted.map(function(p){return p.project;}),axisLabel:{color:textColor(),width:150,overflow:'truncate'},axisLine:{lineStyle:{color:gridColor()}}},
|
|
276
|
+
series:[{type:'bar',data:sorted.map(function(p,i){return{value:p.cost.total_cost,itemStyle:{color:COLORS[i%COLORS.length],borderRadius:[0,4,4,0]}};}),barMaxWidth:28,
|
|
277
|
+
label:{show:true,position:'right',formatter:function(p){return fmtCost(p.value);},color:textColor(),fontSize:11},
|
|
278
|
+
emphasis:{itemStyle:{shadowBlur:4}}}]
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function modelOption(models){
|
|
283
|
+
var entries=Object.entries(models).sort(function(a,b){return b[1].cost.total_cost-a[1].cost.total_cost;});
|
|
284
|
+
return{
|
|
285
|
+
tooltip:Object.assign(itemTooltip(),{formatter:function(p){return'<b>'+esc(p.name)+'</b><br>'+fmtCost(p.value)+' ('+p.percent.toFixed(1)+'%)';}}),
|
|
286
|
+
legend:{orient:'horizontal',bottom:0,textStyle:{color:textColor(),fontSize:11},itemWidth:12,itemHeight:12},
|
|
287
|
+
series:[{type:'pie',radius:['40%','70%'],center:['50%','45%'],
|
|
288
|
+
label:{show:true,formatter:function(p){return p.percent.toFixed(1)+'%';},color:textColor(),fontSize:11},
|
|
289
|
+
labelLine:{show:true,lineStyle:{color:textColor()}},
|
|
290
|
+
emphasis:{label:{show:true,fontSize:14,fontWeight:'bold'}},
|
|
291
|
+
data:entries.map(function(e,i){return{name:e[0],value:e[1].cost.total_cost,itemStyle:{color:COLORS[i%COLORS.length]}};})}]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function cacheEffOption(daily){
|
|
296
|
+
var labels=daily.map(function(d){return d.date;});
|
|
297
|
+
var data=daily.map(function(d){
|
|
298
|
+
var cr=d.tokens?d.tokens.cache_read_tokens:(d.cr||0);
|
|
299
|
+
var cw=d.tokens?d.tokens.cache_write_tokens:(d.cw||0);
|
|
300
|
+
var denom=cr+cw;return denom>0?(cr/denom)*100:0;
|
|
301
|
+
});
|
|
302
|
+
return{
|
|
303
|
+
tooltip:Object.assign(baseTooltip(),{formatter:function(p){return'<b>'+p[0].axisValueLabel+'</b><br>Cache Reuse: <b>'+p[0].value.toFixed(1)+'%</b>';}}),
|
|
304
|
+
grid:{left:'3%',right:'3%',top:20,bottom:15,containLabel:true},
|
|
305
|
+
xAxis:{type:'category',data:labels,axisLabel:{color:textColor(),rotate:labels.length>7?45:0,hideOverlap:true},axisLine:{lineStyle:{color:gridColor()}}},
|
|
306
|
+
yAxis:{type:'value',min:0,max:100,axisLabel:{formatter:function(v){return v+'%';},color:textColor()},splitLine:{lineStyle:{color:gridColor()}}},
|
|
307
|
+
series:[{type:'line',data:data,smooth:true,symbol:data.length<=2?'circle':'none',symbolSize:8,areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:'rgba(34,197,94,0.3)'},{offset:1,color:'rgba(34,197,94,0.02)'}]}},lineStyle:{color:'#22c55e',width:2},itemStyle:{color:'#22c55e'}}]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function roiOption(totalCost,days){
|
|
312
|
+
var monthly=days>0?(totalCost/days)*30:0;
|
|
313
|
+
return{
|
|
314
|
+
tooltip:Object.assign(baseTooltip(),{trigger:'axis'}),
|
|
315
|
+
grid:{left:'3%',right:'3%',top:25,bottom:15,containLabel:true},
|
|
316
|
+
xAxis:{type:'category',data:['Projected\\nMonthly','Pro\\n$20/mo','Max 5x\\n$100/mo','Max 20x\\n$200/mo'],axisLabel:{color:textColor(),interval:0,rotate:0},axisLine:{lineStyle:{color:gridColor()}}},
|
|
317
|
+
yAxis:{type:'value',axisLabel:{formatter:function(v){return fmtCost(v);},color:textColor()},splitLine:{lineStyle:{color:gridColor()}}},
|
|
318
|
+
series:[{type:'bar',barMaxWidth:60,data:[
|
|
319
|
+
{value:monthly,itemStyle:{color:'#6366f1'}},
|
|
320
|
+
{value:20,itemStyle:{color:'#22c55e'}},
|
|
321
|
+
{value:100,itemStyle:{color:'#eab308'}},
|
|
322
|
+
{value:200,itemStyle:{color:'#ef4444'}}
|
|
323
|
+
],label:{show:true,position:'top',formatter:function(p){return fmtCost(p.value);},color:textColor(),fontSize:11}}]
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// \u2500\u2500 Heatmap \u2500\u2500
|
|
328
|
+
function renderHeatmap(hm){
|
|
329
|
+
var el=document.getElementById('heatmap');el.innerHTML='';
|
|
330
|
+
var days=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
331
|
+
var maxVal=Math.max(1,Math.max.apply(null,hm.map(function(r){return Math.max.apply(null,r);})));
|
|
332
|
+
var levels=['var(--hm0)','var(--hm1)','var(--hm2)','var(--hm3)','var(--hm4)','var(--hm5)'];
|
|
333
|
+
function cellBg(v){if(v===0)return levels[0];var p=v/maxVal;if(p<0.15)return levels[1];if(p<0.35)return levels[2];if(p<0.55)return levels[3];if(p<0.75)return levels[4];return levels[5];}
|
|
334
|
+
// Header
|
|
335
|
+
var corner=document.createElement('div');el.appendChild(corner);
|
|
336
|
+
for(var h=0;h<24;h++){var hd=document.createElement('div');hd.className='hm-label';hd.style.justifyContent='center';hd.textContent=h;el.appendChild(hd);}
|
|
337
|
+
for(var d=0;d<7;d++){
|
|
338
|
+
var lbl=document.createElement('div');lbl.className='hm-label';lbl.textContent=days[d];el.appendChild(lbl);
|
|
339
|
+
for(var h2=0;h2<24;h2++){
|
|
340
|
+
var cell=document.createElement('div');cell.className='hm-cell';cell.style.background=cellBg(hm[d][h2]);
|
|
341
|
+
var tip=document.createElement('div');tip.className='hm-tip';tip.textContent=days[d]+' '+h2+':00 \u2014 '+fmt(hm[d][h2])+' tokens';
|
|
342
|
+
cell.appendChild(tip);el.appendChild(cell);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// \u2500\u2500 Session Table \u2500\u2500
|
|
348
|
+
var sortCol='cost',sortDir=-1;
|
|
349
|
+
function renderSessions(sessions){
|
|
350
|
+
var tbody=document.getElementById('sessBody');tbody.innerHTML='';
|
|
351
|
+
var showing=Math.min(sessions.length,100);
|
|
352
|
+
document.getElementById('sessCount').textContent='(showing '+showing+' of '+sessions.length+')';
|
|
353
|
+
var sorted=sessions.slice().sort(function(a,b){
|
|
354
|
+
var va,vb;
|
|
355
|
+
switch(sortCol){
|
|
356
|
+
case'session':va=a.sessionId;vb=b.sessionId;break;
|
|
357
|
+
case'project':va=a.project;vb=b.project;break;
|
|
358
|
+
case'model':va=a.primaryModel;vb=b.primaryModel;break;
|
|
359
|
+
case'duration':va=new Date(a.endTime)-new Date(a.startTime);vb=new Date(b.endTime)-new Date(b.startTime);break;
|
|
360
|
+
case'requests':va=a.request_count;vb=b.request_count;break;
|
|
361
|
+
case'tokens':va=a.tokens.total_tokens;vb=b.tokens.total_tokens;break;
|
|
362
|
+
case'cost':va=a.cost.total_cost;vb=b.cost.total_cost;break;
|
|
363
|
+
default:va=0;vb=0;
|
|
364
|
+
}
|
|
365
|
+
if(typeof va==='string')return sortDir*va.localeCompare(vb);return sortDir*(va-vb);
|
|
366
|
+
}).slice(0,100);
|
|
367
|
+
sorted.forEach(function(s){
|
|
368
|
+
var tr=document.createElement('tr');
|
|
369
|
+
tr.innerHTML='<td class="text-mono">'+esc(s.sessionId.slice(0,14))+'...</td>'
|
|
370
|
+
+'<td>'+esc(s.project)+'</td>'
|
|
371
|
+
+'<td class="text-mono">'+esc(s.primaryModel)+(Object.keys(s.models||{}).length>1?' <span style="color:var(--muted);font-size:.65rem">+'+String(Object.keys(s.models).length-1)+'</span>':'')+'</td>'
|
|
372
|
+
+'<td class="text-right text-mono">'+duration(s.startTime,s.endTime)+'</td>'
|
|
373
|
+
+'<td class="text-right">'+s.request_count+'</td>'
|
|
374
|
+
+'<td class="text-right">'+fmt(s.tokens.total_tokens)+'</td>'
|
|
375
|
+
+'<td class="text-right" style="font-weight:600">'+fmtCost(s.cost.total_cost)+'</td>';
|
|
376
|
+
tbody.appendChild(tr);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// \u2500\u2500 ROI \u2500\u2500
|
|
381
|
+
function renderROI(totals,days){
|
|
382
|
+
var tc=totals.cost.total_cost;var avgD=days>0?tc/days:0;var projM=avgD*30;
|
|
383
|
+
var cpr=totals.request_count>0?tc/totals.request_count:0;
|
|
384
|
+
var cp1k=totals.tokens.total_tokens>0?(tc/totals.tokens.total_tokens)*1000:0;
|
|
385
|
+
var cacheSave=totals.cost.cache_read_cost*9;
|
|
386
|
+
var cards=[
|
|
387
|
+
{label:'Avg Daily Cost',value:fmtCost(avgD),sub:'Projected monthly: '+fmtCost(projM),cls:'green'},
|
|
388
|
+
{label:'Cost Per Request',value:fmtCost(cpr),sub:totals.request_count.toLocaleString()+' total requests',cls:''},
|
|
389
|
+
{label:'Cache Savings',value:'~'+fmtCost(cacheSave),sub:((totals.tokens.cache_read_tokens/(totals.tokens.cache_read_tokens+totals.tokens.input_tokens||1))*100).toFixed(1)+'% cache hit',cls:'green'},
|
|
390
|
+
{label:'Cost Per 1K Tokens',value:fmtCost(cp1k),sub:fmt(totals.tokens.total_tokens)+' total tokens',cls:''}
|
|
391
|
+
];
|
|
392
|
+
var el=document.getElementById('roiCards');el.innerHTML='';
|
|
393
|
+
cards.forEach(function(c){
|
|
394
|
+
el.innerHTML+='<div class="roi-card"><div class="roi-label">'+c.label+'</div><div class="roi-value'+(c.cls?' '+c.cls:'')+'">'+c.value+'</div><div class="roi-sub">'+c.sub+'</div></div>';
|
|
395
|
+
});
|
|
396
|
+
if(charts['chartROI'])charts['chartROI'].setOption(roiOption(tc,days),{notMerge:true});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// \u2500\u2500 Initialize Everything \u2500\u2500
|
|
400
|
+
updateStats(allData.totals,allData.sessions.length);
|
|
401
|
+
initChart('chartCost',costOption(allData.daily));
|
|
402
|
+
initChart('chartIO',ioOption(allData.daily));
|
|
403
|
+
initChart('chartCache',cacheOption(allData.daily));
|
|
404
|
+
initChart('chartProject',projectOption(allData.projects));
|
|
405
|
+
initChart('chartModel',modelOption(allData.models));
|
|
406
|
+
initChart('chartCacheEff',cacheEffOption(allData.daily));
|
|
407
|
+
initChart('chartROI',roiOption(allData.totals.cost.total_cost,allData.daily.length));
|
|
408
|
+
renderHeatmap(allData.heatmap);
|
|
409
|
+
// Show timezone in heatmap description
|
|
410
|
+
try{var tz=Intl.DateTimeFormat().resolvedOptions().timeZone;document.getElementById('heatmapDesc').textContent+=' Hours shown in UTC (your timezone: '+tz+').'}catch(e){}
|
|
411
|
+
renderSessions(allData.sessions);
|
|
412
|
+
renderROI(allData.totals,allData.daily.length);
|
|
413
|
+
|
|
414
|
+
// Sort headers
|
|
415
|
+
document.querySelectorAll('#sessTable th').forEach(function(th){
|
|
416
|
+
th.addEventListener('click',function(){
|
|
417
|
+
var col=th.dataset.col;if(!col)return;
|
|
418
|
+
sortDir=sortCol===col?sortDir*-1:-1;sortCol=col;
|
|
419
|
+
renderSessions(currentSessions);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// \u2500\u2500 Filtering \u2500\u2500
|
|
424
|
+
var currentSessions=allData.sessions;
|
|
425
|
+
function applyFilters(){
|
|
426
|
+
var s=document.getElementById('dateStart').value;
|
|
427
|
+
var e=document.getElementById('dateEnd').value;
|
|
428
|
+
var p=document.getElementById('projectFilter').value;
|
|
429
|
+
|
|
430
|
+
// Filter daily by date
|
|
431
|
+
var fDaily=allData.daily.filter(function(d){
|
|
432
|
+
if(s&&d.date<s)return false;if(e&&d.date>e)return false;return true;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Filter sessions
|
|
436
|
+
var fSessions=allData.sessions.filter(function(ss){
|
|
437
|
+
if(p&&ss.project!==p)return false;
|
|
438
|
+
var d=ss.startTime.slice(0,10);if(s&&d<s)return false;if(e&&d>e)return false;return true;
|
|
439
|
+
});
|
|
440
|
+
currentSessions=fSessions;
|
|
441
|
+
|
|
442
|
+
// Build chart daily data: use per-project breakdown if project filter active
|
|
443
|
+
var chartDaily;
|
|
444
|
+
if(p){
|
|
445
|
+
chartDaily=fDaily.filter(function(d){return d.projects&&d.projects[p];}).map(function(d){var pp=d.projects[p];return{date:d.date,cost:pp.cost,tokens:pp.tokens,request_count:pp.request_count};});
|
|
446
|
+
} else {
|
|
447
|
+
chartDaily=fDaily;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Compute totals
|
|
451
|
+
var totals={tokens:{input_tokens:0,output_tokens:0,cache_write_tokens:0,cache_read_tokens:0,total_tokens:0},cost:{input_cost:0,output_cost:0,cache_write_cost:0,cache_read_cost:0,total_cost:0},request_count:0};
|
|
452
|
+
chartDaily.forEach(function(d){
|
|
453
|
+
var t=d.tokens,c=d.cost;
|
|
454
|
+
totals.tokens.input_tokens+=t.input_tokens;totals.tokens.output_tokens+=t.output_tokens;
|
|
455
|
+
totals.tokens.cache_write_tokens+=t.cache_write_tokens;totals.tokens.cache_read_tokens+=t.cache_read_tokens;
|
|
456
|
+
totals.tokens.total_tokens+=t.total_tokens;
|
|
457
|
+
totals.cost.input_cost+=c.input_cost||0;totals.cost.output_cost+=c.output_cost||0;
|
|
458
|
+
totals.cost.cache_write_cost+=c.cache_write_cost||0;totals.cost.cache_read_cost+=c.cache_read_cost||0;
|
|
459
|
+
totals.cost.total_cost+=c.total_cost;totals.request_count+=(d.request_count||0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
updateStats(totals,fSessions.length);
|
|
463
|
+
|
|
464
|
+
// Update all charts via setOption (no destroy!)
|
|
465
|
+
charts['chartCost'].setOption(costOption(chartDaily),{notMerge:true});
|
|
466
|
+
charts['chartIO'].setOption(ioOption(chartDaily),{notMerge:true});
|
|
467
|
+
charts['chartCache'].setOption(cacheOption(chartDaily),{notMerge:true});
|
|
468
|
+
charts['chartCacheEff'].setOption(cacheEffOption(chartDaily),{notMerge:true});
|
|
469
|
+
|
|
470
|
+
// Model: always aggregate from filtered sessions so date+project filters both apply
|
|
471
|
+
var fm={};fSessions.forEach(function(ss){if(ss.models)Object.entries(ss.models).forEach(function(en){if(!fm[en[0]])fm[en[0]]={cost:{total_cost:0}};fm[en[0]].cost.total_cost+=en[1].cost.total_cost;});});
|
|
472
|
+
charts['chartModel'].setOption(modelOption(fm),{notMerge:true});
|
|
473
|
+
|
|
474
|
+
// Project: hide when single project selected, show otherwise
|
|
475
|
+
var projPanel=document.getElementById('projectPanel');
|
|
476
|
+
if(p){
|
|
477
|
+
projPanel.style.display='none';
|
|
478
|
+
} else {
|
|
479
|
+
projPanel.style.display='';
|
|
480
|
+
var fp=allData.projects.filter(function(pp){if(s||e){var hasDays=fDaily.some(function(d){return d.projects&&d.projects[pp.project];});return hasDays;}return true;});
|
|
481
|
+
charts['chartProject'].setOption(projectOption(fp),{notMerge:true});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Heatmap: rebuild from filtered sessions so date+project filters both apply
|
|
485
|
+
var hasFilter=s||e||p;
|
|
486
|
+
if(hasFilter){
|
|
487
|
+
var hm=[];for(var di=0;di<7;di++){hm[di]=[];for(var hi=0;hi<24;hi++)hm[di][hi]=0;}
|
|
488
|
+
fSessions.forEach(function(ss){
|
|
489
|
+
var dt=new Date(ss.startTime);var day=dt.getDay();var hr=dt.getHours();
|
|
490
|
+
hm[day][hr]+=(ss.tokens?ss.tokens.total_tokens:0);
|
|
491
|
+
});
|
|
492
|
+
renderHeatmap(hm);
|
|
493
|
+
} else {
|
|
494
|
+
renderHeatmap(allData.heatmap);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
renderSessions(fSessions);
|
|
498
|
+
renderROI(totals,chartDaily.length);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
document.getElementById('btnApply').addEventListener('click',applyFilters);
|
|
502
|
+
document.getElementById('dateStart').addEventListener('change',applyFilters);
|
|
503
|
+
document.getElementById('dateEnd').addEventListener('change',applyFilters);
|
|
504
|
+
document.getElementById('projectFilter').addEventListener('change',applyFilters);
|
|
505
|
+
document.getElementById('btnReset').addEventListener('click',function(){
|
|
506
|
+
document.getElementById('dateStart').value='${r(s)}';
|
|
507
|
+
document.getElementById('dateEnd').value='${r(n)}';
|
|
508
|
+
document.getElementById('projectFilter').value='';
|
|
509
|
+
applyFilters();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// \u2500\u2500 Theme Toggle \u2500\u2500
|
|
513
|
+
document.getElementById('themeToggle').addEventListener('click',function(){
|
|
514
|
+
document.documentElement.classList.toggle('light');
|
|
515
|
+
this.textContent=isDark()?'\u2600':'\u{1F319}';
|
|
516
|
+
// Rebuild all charts from scratch with correct theme (getOption carries stale colors)
|
|
517
|
+
Object.keys(charts).forEach(function(id){
|
|
518
|
+
charts[id].dispose();
|
|
519
|
+
charts[id]=echarts.init(document.getElementById(id),isDark()?'dark':null);
|
|
520
|
+
});
|
|
521
|
+
// Re-trigger current filter state to rebuild all chart options with fresh colors
|
|
522
|
+
applyFilters();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// \u2500\u2500 Resize \u2500\u2500
|
|
526
|
+
window.addEventListener('resize',function(){Object.values(charts).forEach(function(c){c.resize();});});
|
|
527
|
+
|
|
528
|
+
// Convert charts to static images before printing so they don't go blank
|
|
529
|
+
window.addEventListener('beforeprint',function(){
|
|
530
|
+
Object.keys(charts).forEach(function(id){
|
|
531
|
+
var c=charts[id];if(!c)return;
|
|
532
|
+
var url=c.getDataURL({type:'png',pixelRatio:2,backgroundColor:isDark()?'#1e293b':'#ffffff'});
|
|
533
|
+
var dom=document.getElementById(id);if(!dom)return;
|
|
534
|
+
var img=document.createElement('img');img.src=url;img.className='chart-print-img';img.style.width='100%';
|
|
535
|
+
dom.style.display='none';dom.parentNode.insertBefore(img,dom);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
window.addEventListener('afterprint',function(){
|
|
539
|
+
document.querySelectorAll('.chart-print-img').forEach(function(img){
|
|
540
|
+
var dom=img.nextElementSibling;if(dom)dom.style.display='';
|
|
541
|
+
img.remove();
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
})();
|
|
545
|
+
</script>
|
|
546
|
+
</body>
|
|
547
|
+
</html>`}function sn(o){let e=process.platform==="darwin"?"open":"xdg-open";import("child_process").then(({execFile:t})=>{t(e,[o],()=>{})})}async function le(o){let e=I(),t=B(e),{entries:r}=await j(t),c=x(r),s=D(c,{since:o.since,until:o.until,project:o.project,timezone:o.timezone});if(s.length===0){console.log($t.yellow("No data found."));return}let n=S(o.mode),a=pt(s,n,o.timezone);if(o.json){console.log(JSON.stringify(a,null,2));return}let i=nn(a);if(o.save){let{resolve:l,normalize:u}=await import("path"),{realpathSync:f}=await import("fs"),y=l(o.save),g=We(),p=process.cwd();if(p==="/"||y==="/"){console.log($t.red("Refusing to write to root directory."));return}let b=y;try{let E=Je(y);on(E)&&(b=l(f(E),y.split("/").pop()))}catch{}if(!b.startsWith(g)&&!b.startsWith(p)&&!b.startsWith("/tmp")&&!b.startsWith("/private/tmp")){console.log($t.red(`Refusing to write outside home directory, cwd, or /tmp: ${b}`));return}Ue(Je(y),{recursive:!0}),Ze(y,i,"utf-8"),console.log($t.green(`Dashboard saved to: ${y}`));return}let d=Ye(We(),".cctrack"),m=Ye(d,"dashboard.html");Ue(d,{recursive:!0}),Ze(m,i,"utf-8"),console.log($t.green(`Dashboard saved to: ${m}`)),sn(m)}function Ge(o){o.command("dashboard").description("Generate and open interactive HTML dashboard").option("--save <path>","Save to custom path (does not auto-open)").option("--json","Output dashboard data as JSON instead of HTML").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for date grouping").action(le)}function rn(o,e){let t=[];t.push("date,session_id,project,model,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,cost_calculated,cost_embedded,request_id");for(let r of o){let c=r.timestamp.slice(0,10),s=r.sessionId??"",n=r.cwd?tt(r.cwd):"",a=r.message.model??"unknown",i=r.message.usage,d=$(r,e);t.push([c,nt(s),nt(n),nt(a),i.input_tokens,i.output_tokens,i.cache_creation_input_tokens??0,i.cache_read_input_tokens??0,d.calculatedCost.total_cost.toFixed(6),r.costUSD?.toFixed(6)??"",r.requestId??""].join(","))}return t.join(`
|
|
548
|
+
`)}function Ke(o){let e=o.command("export").description("Export usage data");e.command("csv").description("Export flat CSV with one row per request").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for filtering").action(async t=>{let r=I(),c=B(r),{entries:s}=await j(c),n=x(s),i=[...D(n,{since:t.since,until:t.until,project:t.project,timezone:t.timezone})].sort((m,l)=>m.timestamp.localeCompare(l.timestamp)),d=S(t.mode);console.log(rn(i,d))}),e.command("json").description("Export structured JSON matching dashboard data format").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for date grouping").action(async t=>{let r=I(),c=B(r),{entries:s}=await j(c),n=x(s),a=D(n,{since:t.since,until:t.until,project:t.project,timezone:t.timezone}),i=S(t.mode),d=pt(a,i,t.timezone);console.log(JSON.stringify(d,null,2))})}import z from"chalk";function Ve(o){o.command("roi").description("Calculate ROI vs API-equivalent cost").option("--plan <plan>","Subscription plan: pro ($20), max5 ($100), max20 ($200)","max5").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--project <name>","Filter by project name").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for filtering").option("--json","Output as JSON").action(async e=>{let t=I(),r=B(t),{entries:c}=await j(r),s=x(c),n=D(s,{since:e.since,until:e.until,project:e.project,timezone:e.timezone});if(n.length===0){console.log(z.yellow("No data found for the specified range."));return}let i={pro:"pro",20:"pro",max5:"max5","max-5x":"max5",100:"max5",max:"max5",max20:"max20","max-20x":"max20",200:"max20"}[e.plan?.toLowerCase()??"max5"];i||(console.error(z.red(`Unknown plan: ${e.plan}. Choose from: pro ($20), max5 ($100), max20 ($200)`)),process.exit(1));let d=S(e.mode),m=we[i],l=0,u=0,f=0,y=0,g=0,p=0;for(let Y of n){let w=$(Y,d);l+=w.calculatedCost.total_cost,u+=w.tokens.input_tokens,f+=w.tokens.output_tokens,y+=w.tokens.cache_write_tokens,g+=w.tokens.cache_read_tokens,p+=w.tokens.total_tokens}let b=[...n].sort((Y,w)=>Y.timestamp.localeCompare(w.timestamp)),E=new Date(b[0].timestamp),X=new Date(b[b.length-1].timestamp),U=Math.max(1,Math.ceil((X.getTime()-E.getTime())/(1e3*60*60*24))+1),Q=l/U,L=Q*30,P=l-m;if(e.json){console.log(JSON.stringify({plan:i,subscription_cost:m,api_equivalent_cost:l,savings:P,savings_percentage:l>0?P/l*100:0,days_analyzed:U,avg_daily_cost:Q,projected_monthly_cost:L,total_tokens:p,token_breakdown:{input:u,output:f,cache_write:y,cache_read:g},request_count:n.length},null,2));return}console.log(z.bold(`
|
|
549
|
+
ROI Analysis
|
|
550
|
+
`)),console.log(z.dim(` Plan: ${i} (${h(m)}/mo)`)),console.log(z.dim(` Period: ${b[0].timestamp.slice(0,10)} to ${b[b.length-1].timestamp.slice(0,10)} (${U} days)`)),console.log(z.dim(` Requests: ${n.length.toLocaleString()}
|
|
551
|
+
`));let C=24;if(console.log(` ${"Total tokens".padEnd(C)} ${z.white(p.toLocaleString())}`),console.log(` ${"API-equivalent cost".padEnd(C)} ${z.white(h(l))}`),console.log(` ${"Subscription cost".padEnd(C)} ${z.white(h(m))}`),P>0)console.log(` ${"Savings".padEnd(C)} ${z.green(h(P))} ${z.green(`(${(P/l*100).toFixed(0)}%)`)}`);else{let Y=Math.abs(P);console.log(` ${"Loss".padEnd(C)} ${z.red(h(Y))} ${z.red("(subscription > API cost)")}`)}console.log(""),console.log(` ${"Avg daily API cost".padEnd(C)} ${z.white(h(Q))}`),console.log(` ${"Projected monthly".padEnd(C)} ${z.white(h(L))}`),L>m?console.log(z.green(`
|
|
552
|
+
Subscription is worth it at projected usage.`)):console.log(z.yellow(`
|
|
553
|
+
API costs are lower than your subscription at current usage.`)),console.log("")})}import{statSync as an}from"fs";import k from"chalk";function Qe(){process.stdout.write("\x1B[2J\x1B[H")}async function Xe(o,e,t){let r=I(),c=B(r);if(c.length===0){console.log(k.yellow("No JSONL files found. Waiting for data..."));return}let{entries:s,errors:n}=await j(c),a=x(s),i=new Date().toISOString().slice(0,10),d=D(a,{since:i,project:e,timezone:t});Qe(),console.log(k.bold.cyan(" CCTrack Live Monitor")),console.log(k.dim(` ${new Date().toLocaleTimeString()} | ${a.length} usage entries | ${c.length} files
|
|
554
|
+
`));let l=st(d,o,t).find(g=>g.date===i);if(l){console.log(k.bold(" Today")),console.log(` Requests: ${k.white(l.request_count.toLocaleString())}`),console.log(` Input: ${k.white(_(l.tokens.input_tokens))}`),console.log(` Output: ${k.white(_(l.tokens.output_tokens))}`),console.log(` Cache Write: ${k.white(_(l.tokens.cache_write_tokens))}`),console.log(` Cache Read: ${k.white(_(l.tokens.cache_read_tokens))}`),console.log(` Total: ${k.white(_(l.tokens.total_tokens))}`),console.log(` Cost: ${k.white(h(l.cost.total_cost))}`);let g=q(d,o);g.insufficient_data||console.log(` Burn rate: ${k.dim(h(g.hourly_cost)+"/hr \u2192 "+h(g.projected_monthly)+"/month projected")}`)}else console.log(k.dim(" No activity today yet."));let u=ct(d,o);if(u.length>0){console.log(k.bold(`
|
|
555
|
+
Recent Sessions`));let g=u.slice(0,5);for(let p of g){let b=new Date(p.endTime).getTime()-new Date(p.startTime).getTime(),E=p.sessionId.length>12?p.sessionId.slice(0,12)+"...":p.sessionId;console.log(` ${k.dim(E)} ${k.white(p.primaryModel)} ${k.dim(R(b))} ${_(p.tokens.total_tokens)} ${k.white(h(p.cost.total_cost))}`)}}if(l&&Object.keys(l.models).length>0){console.log(k.bold(`
|
|
556
|
+
Models Today`));for(let[g,p]of Object.entries(l.models))console.log(` ${k.white(g)} ${k.dim("|")} ${p.request_count} reqs ${k.dim("|")} ${_(p.tokens.total_tokens)} ${k.dim("|")} ${k.white(h(p.cost.total_cost))}`)}let f=c.reduce((g,p)=>{try{let b=an(p).mtimeMs;return b>g.time?{path:p,time:b}:g}catch{return g}},{path:"",time:0});if(f.path){let g=Date.now()-f.time;console.log(k.dim(`
|
|
557
|
+
Last file update: ${R(g)} ago`))}let y=lt();if(y.daily&&l){let g=N(l.cost.total_cost,y.daily);console.log(k.bold(`
|
|
558
|
+
Budget`)),console.log(` Daily: ${V(g.percentage)} (${h(g.spent)} / ${h(g.budget)})`)}console.log(k.dim(`
|
|
559
|
+
Press Ctrl+C to exit`))}function to(o){o.command("live").description("Real-time terminal monitor").option("--interval <seconds>","Refresh interval in seconds","5").option("--project <name>","Filter by project name").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--timezone <tz>","Timezone for date grouping").action(async e=>{let t=Math.max(1,parseInt(e.interval??"5",10))*1e3,r=S(e.mode);await Xe(r,e.project,e.timezone);let c=!1;async function s(){if(!c&&(await new Promise(n=>setTimeout(n,t)),!c)){try{await Xe(r,e.project,e.timezone)}catch{}s()}}s(),process.on("SIGINT",()=>{c=!0,Qe(),console.log(k.dim("Live monitor stopped.")),process.exit(0)})})}_t();import F from"chalk";import cn from"cli-table3";function eo(o){let e=o.command("pricing").description("View and update model pricing data");e.command("list").description("List all known model prices").option("--json","Output as JSON").action(async t=>{let r=ht(),c=Ht();if(t.json){console.log(JSON.stringify(c,null,2));return}console.log(F.bold("Model Pricing")),console.log(F.dim(`Source: ${r.source} | ${r.modelCount} models | version: ${r.version} | cache: ${r.cacheAge}
|
|
560
|
+
`));let s=Object.entries(c.models).sort(([i],[d])=>i.localeCompare(d)),n=new cn({head:["Model","Input/M","Output/M","Cache W/M","Cache R/M","Context"].map(i=>F.cyan(i)),colAligns:["left","right","right","right","right","right"],style:{head:[],border:[]}});for(let[i,d]of s){let m=d.input_cost_per_million_above_200k?F.yellow(" *"):"";n.push([i+m,h(d.input_cost_per_million),h(d.output_cost_per_million),h(d.cache_creation_cost_per_million),h(d.cache_read_cost_per_million),`${(d.context_window/1e3).toFixed(0)}K`])}console.log(n.toString()),console.log(F.dim(`
|
|
561
|
+
* = tiered pricing above 200K context`)),console.log(F.bold(`
|
|
562
|
+
Aliases`));let a=Object.entries(c.aliases).sort(([i],[d])=>i.localeCompare(d));for(let[i,d]of a)console.log(` ${F.white(i)} ${F.dim("\u2192")} ${d}`)}),e.command("update").description("Fetch latest pricing from Anthropic and update cache").action(async()=>{console.log(F.dim("Fetching pricing from Anthropic..."));let{data:t,newModels:r,source:c}=await oe();if(console.log(F.green(`Pricing updated: ${Object.keys(t.models).length} models (${c})`)),r.length>0){console.log(F.yellow(`
|
|
563
|
+
New models discovered:`));for(let n of r)console.log(` + ${F.cyan(n)}`)}let s=ht();console.log(F.dim(`
|
|
564
|
+
Cache: ${s.cacheAge}`))}),e.command("status").description("Show pricing source and cache status").action(async()=>{let t=ht();console.log(`Source: ${F.white(t.source)}`),console.log(`Models: ${F.white(t.modelCount.toString())}`),console.log(`Version: ${F.white(t.version)}`),console.log(`Cache: ${F.white(t.cacheAge)}`)}),e.action(async()=>{await e.commands.find(t=>t.name()==="status")?.parseAsync(["node","cctrack","pricing","status"])})}import H from"chalk";function oo(o){let e=o.command("config").description("Manage cctrack configuration (budgets, etc.)");e.command("set <key> <value>").description("Set a configuration value (e.g. budget.daily 50)").action((t,r)=>{let c=Number(r);(isNaN(c)||c<0)&&(console.error(H.red(`Invalid value: ${r}. Must be a non-negative number.`)),process.exit(1));let s=t.split(".");if(s[0]==="budget"&&s.length===2){let n=s[1];["daily","monthly","block"].includes(n)||(console.error(H.red(`Unknown budget key: ${n}. Valid keys: daily, monthly, block`)),process.exit(1));let a=lt();a[n]=c,Oe(a),console.log(H.green(`Set ${t} = ${h(c)}`))}else console.error(H.red(`Unknown config key: ${t}`)),console.error(H.dim("Valid keys: budget.daily, budget.monthly, budget.block")),process.exit(1)}),e.command("get [key]").description("Show current configuration").action(t=>{let r=Ie(),c=Ae();t&&t!=="budget"&&(console.error(H.red(`Unknown config section: ${t}`)),process.exit(1)),console.log(H.bold("CCTrack Configuration")+H.dim(` (${c})`)),console.log();let s=r.budget??{};console.log(H.bold(" Budget")),console.log(` Daily: ${s.daily!=null?h(s.daily)+H.dim(" (alerts at 50%/80%/100%)"):H.dim("not set")}`),console.log(` Monthly: ${s.monthly!=null?h(s.monthly)+H.dim(" (alerts at 50%/80%/100%)"):H.dim("not set")}`),console.log(` Block: ${s.block!=null?h(s.block):H.dim("not set")}`),console.log(H.dim(`
|
|
565
|
+
Set with: cctrack config set budget.daily <dollars>`))}),e.command("reset").description("Reset configuration to defaults").action(()=>{Le(),console.log(H.green("Configuration reset to defaults."))})}import M from"chalk";import yn from"cli-table3";import{readFileSync as Ot,writeFileSync as co,mkdirSync as lo,statSync as de,existsSync as wt,readdirSync as ln,openSync as dn,readSync as un,closeSync as mn}from"fs";import{join as gt}from"path";import{homedir as me}from"os";var Gt=gt(me(),".cctrack"),It=gt(Gt,"statusline.cache"),ue=gt(Gt,"ratelimits.json"),pn=3e4;function gn(){try{if(process.stdin.isTTY)return null;let o=Ot(0,"utf-8").trim();if(!o)return null;let e=JSON.parse(o);if(!e.rate_limits)return null;let t={source:"statusline",captured_at:new Date().toISOString()},r=e.rate_limits;r.five_hour&&(t.five_hour={used_percentage:r.five_hour.used_percentage,resets_at:r.five_hour.resets_at}),r.seven_day&&(t.seven_day={used_percentage:r.seven_day.used_percentage,resets_at:r.seven_day.resets_at}),r.seven_day_sonnet&&(t.seven_day_sonnet={used_percentage:r.seven_day_sonnet.used_percentage,resets_at:r.seven_day_sonnet.resets_at}),r.seven_day_opus&&(t.seven_day_opus={used_percentage:r.seven_day_opus.used_percentage,resets_at:r.seven_day_opus.resets_at}),r.extra_usage?.is_enabled&&(t.extra_usage={is_enabled:!0,spent:r.extra_usage.used_credits??0,limit:r.extra_usage.monthly_limit??0,utilization:r.extra_usage.utilization??0,resets_at:r.extra_usage.resets_at??0});try{lo(Gt,{recursive:!0}),co(ue,JSON.stringify(t,null,2),"utf-8")}catch{}return t}catch{return null}}function uo(){try{if(!wt(ue))return null;let o=Ot(ue,"utf-8"),e=JSON.parse(o);return Date.now()-new Date(e.captured_at).getTime()>600*1e3?null:e}catch{return null}}function mo(){return uo()}function fn(){try{if(!wt(It))return null;let o=Ot(It,"utf-8");return JSON.parse(o)}catch{return null}}function no(o){try{lo(Gt,{recursive:!0});let e={updated_at:new Date().toISOString(),data:o};co(It,JSON.stringify(e),"utf-8")}catch{}}function hn(){try{if(!wt(It))return!1;let o=de(It);return Date.now()-o.mtimeMs<pn}catch{return!1}}function so(o){let e=[];function t(r){try{let c=ln(r,{withFileTypes:!0});for(let s of c){let n=gt(r,s.name);s.isDirectory()?t(n):s.name.endsWith(".jsonl")&&e.push(n)}}catch{}}for(let r of o)t(r);return e}function ro(){let o=[],e=process.env.CLAUDE_CONFIG_DIR;if(e){let c=gt(e,"projects");wt(c)&&o.push(c)}let t=me(),r=[gt(t,".claude","projects"),gt(t,".config","claude","projects")];for(let c of r)wt(c)&&o.push(c);return[...new Set(o)]}function ao(o,e){let t=Date.now(),r=t-1440*60*1e3,c=new Date().toISOString().slice(0,10),s=t-dt,n=0,a=0,i=0,d=0,m="unknown",l="",u="",f=new Map;for(let X of o){try{if(de(X).mtimeMs<r)continue}catch{continue}let U;try{let L=de(X),P=256*1024;if(L.size>P){let C=dn(X,"r"),Y=Buffer.alloc(P);un(C,Y,0,P,L.size-P),mn(C),U=Y.toString("utf-8");let w=U.indexOf(`
|
|
566
|
+
`);w>=0&&(U=U.slice(w+1))}else U=Ot(X,"utf-8")}catch{continue}let Q=U.split(`
|
|
567
|
+
`);for(let L of Q){let P=L.trim();if(P)try{let C=JSON.parse(P),Y=zt.safeParse(C);if(!Y.success)continue;let w=Y.data;if(w.isApiErrorMessage===!0||w.message.model==="<synthetic>"||w.timestamp.slice(0,10)!==c)continue;let At=$(w,e);n+=At.cost.total_cost,a+=At.tokens.total_tokens,w.timestamp>l&&(l=w.timestamp,m=w.message.model??"unknown",w.sessionId&&(u=w.sessionId)),w.sessionId&&f.set(w.sessionId,(f.get(w.sessionId)??0)+At.cost.total_cost),new Date(w.timestamp).getTime()>=s&&(i+=At.tokens.total_tokens,d++)}catch{continue}}}let y=u?f.get(u)??0:0,g=d>0?Math.min(d,100):0,p=t-s,b=Math.max(dt-p,0),E="safe";try{let X=gt(me(),".cctrack","config.json");if(wt(X)){let Q=JSON.parse(Ot(X,"utf-8"))?.budget?.daily;if(Q&&Q>0){let L=n/Q*100;L>=100?E="exceeded":L>=80?E="critical":L>=50&&(E="warning")}}}catch{}return{today_cost:n,session_cost:y,model:m,total_tokens:a,block_percentage:Math.round(g),block_remaining:R(b),budget_level:E,updated_at:new Date().toISOString()}}function io(o,e=8){let t=Math.min(Math.max(o,0),100),r=Math.round(t/100*e),c=e-r;return"\u2588".repeat(r)+"\u2591".repeat(c)}function _n(o,e){if(e)return e.replace("{cost}",h(o.today_cost)).replace("{model}",ot(o.model)).replace("{tokens}",_(o.total_tokens)).replace("{block_pct}",`${o.block_percentage}%`).replace("{block_remaining}",o.block_remaining);let t=[h(o.today_cost)+" today",ot(o.model),_(o.total_tokens)+" tok"];if(o.rate_limits?.five_hour){let r=Math.round(o.rate_limits.five_hour.used_percentage),c=io(r),s=o.rate_limits.five_hour.resets_at*1e3-Date.now(),n=s>0?R(s):"now";t.push(`${c} ${r}% 5h (${n})`)}if(o.rate_limits?.seven_day&&t.push(`7d: ${Math.round(o.rate_limits.seven_day.used_percentage)}%`),o.rate_limits?.extra_usage){let r=o.rate_limits.extra_usage;t.push(`extra: $${r.spent.toFixed(2)}/$${r.limit.toFixed(0)}`)}return o.rate_limits||t.push(`${io(o.block_percentage)} ~${o.block_percentage}%`),t.join(" \u2502 ")}function po(o){o.command("statusline").description("Ultra-lightweight cached output for tmux/neovim/hooks").option("--format <template>","Custom format with placeholders: {cost}, {model}, {tokens}, {block_pct}, {block_remaining}").option("--no-cache","Force fresh parse (skip cache)").option("--json","Output as JSON").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").action(e=>{let t=S(e.mode),r=e.cache!==!1,c=gn(),s;if(r&&hn()){let n=fn();if(n)s=n.data;else{let a=ro(),i=so(a);s=ao(i,t),no(s)}}else{let n=ro(),a=so(n);s=ao(a,t),r&&no(s)}if(s.rate_limits=c??uo()??void 0,s.rate_limits?.five_hour){s.block_percentage=Math.round(s.rate_limits.five_hour.used_percentage);let n=s.rate_limits.five_hour.resets_at*1e3-Date.now();s.block_remaining=R(Math.max(n,0))}e.json?process.stdout.write(JSON.stringify(s)):process.stdout.write(_n(s,e.format))})}function go(o,e,t=10){let r=Date.now(),c=r-t*dt,s=new Map;for(let n=0;n<t;n++){let a=r-n*dt,i=a-dt;s.set(n,{block_start:new Date(i).toISOString(),block_end:new Date(a).toISOString(),block_index:n,is_current:n===0,time_remaining_ms:n===0?a-r:0,...T(),models:{}})}for(let n of o){let a=new Date(n.timestamp).getTime();if(a<c||a>r)continue;let i=Math.floor((r-a)/dt);if(i<0||i>=t)continue;let d=s.get(i),m=n.message.model??"unknown",l=$(n,e);O(d,l),d.models[m]||(d.models[m]=T());let u=d.models[m];u.tokens=Tt(u.tokens,l.tokens),u.cost=Dt(u.cost,l.cost),u.request_count++}return Array.from(s.values())}function pe(){process.stdout.write("\x1B[2J\x1B[H")}function bn(o){let e=new Date(o),t=e.getFullYear(),r=String(e.getMonth()+1).padStart(2,"0"),c=String(e.getDate()).padStart(2,"0"),s=String(e.getHours()).padStart(2,"0"),n=String(e.getMinutes()).padStart(2,"0");return`${t}-${r}-${c} ${s}:${n}`}function kn(o){let e=mo();if(e){console.log(M.bold("Anthropic Rate Limits")+M.dim(` (via ${e.source}, ${e.captured_at.slice(11,19)} UTC)`));let c=(s,n)=>{if(!n)return;let a=n.used_percentage,i=n.resets_at*1e3-Date.now(),d=i>0?R(i):"now",m=a>=80?M.red:a>=50?M.yellow:M.green,l="\u2588".repeat(Math.round(a/5))+"\u2591".repeat(20-Math.round(a/5));console.log(` ${s.padEnd(16)} ${m(l)} ${m(a.toFixed(1).padStart(5)+"%")} | resets in ${d}`)};if(c("Session (5h)",e.five_hour),c("Weekly (all)",e.seven_day),c("Weekly (Sonnet)",e.seven_day_sonnet),c("Weekly (Opus)",e.seven_day_opus),e.extra_usage){let s=e.extra_usage,n=s.utilization,a=n>=80?M.red:n>=50?M.yellow:M.green,i=s.resets_at*1e3-Date.now(),d=i>0?R(i):"now";console.log(` ${"Extra usage".padEnd(16)} ${a("$"+s.spent.toFixed(2)+" / $"+s.limit.toFixed(0))} (${n.toFixed(0)}%) | resets in ${d}`)}console.log("")}if(o.length===0){console.log(M.yellow("No usage recorded in the last 50 hours.")),console.log(M.dim("Start a Claude Code session and data will appear here."));return}let t=o[0];console.log(M.bold("Last 5 Hours")),t.request_count===0?console.log(M.dim(` No requests in this window.
|
|
568
|
+
`)):(console.log(` ${M.white(h(t.cost.total_cost))} spent | ${M.white(t.request_count.toString())} requests`),console.log(` Input: ${M.white(_(t.tokens.input_tokens))} | Output: ${M.white(_(t.tokens.output_tokens))} | Cache: ${M.white(_(t.tokens.cache_read_tokens))}`));let r=o.filter(c=>!c.is_current&&c.request_count>0);if(r.length>0){console.log(M.bold(`
|
|
569
|
+
Previous 5-Hour Windows`));let c=new yn({head:["Window Start","Requests","Tokens","Cost"].map(s=>M.cyan(s)),colAligns:["left","right","right","right"],style:{head:[],border:[]}});for(let s of r)c.push([bn(s.block_start),s.request_count.toString(),_(s.tokens.total_tokens),h(s.cost.total_cost)]);console.log(c.toString())}console.log(M.dim(`
|
|
570
|
+
Note: These are your usage patterns grouped by 5-hour windows.`)),console.log(M.dim("They do not reflect Anthropic's actual rate limit calculations."))}async function ge(o,e){let t=I(),r=B(t);if(r.length===0){console.log(M.yellow("No JSONL files found. Waiting for data..."));return}let{entries:c}=await j(r),s=x(c),n=D(s,{since:e.since,until:e.until,project:e.project}),a=go(n,o);kn(a)}function fo(o){o.command("blocks").description("Show usage grouped by 5-hour windows").option("--json","Output as JSON").option("--since <date>","Start date (YYYY-MM-DD)").option("--until <date>","End date (YYYY-MM-DD)").option("--mode <mode>","Cost mode: calculate, display, compare","calculate").option("--live","Auto-refresh every 5 seconds").action(async e=>{let t=S(e.mode);if(e.json){let r=I(),c=B(r),{entries:s}=await j(c),n=x(s),a=D(n,{since:e.since,until:e.until}),i=go(a,t);console.log(JSON.stringify(i,null,2));return}if(e.live){pe(),await ge(t,e);let r=setInterval(async()=>{try{pe(),await ge(t,e),console.log(M.dim(`
|
|
571
|
+
Press Ctrl+C to exit`))}catch{}},5e3);process.on("SIGINT",()=>{clearInterval(r),pe(),console.log(M.dim("Block monitor stopped.")),process.exit(0)})}else await ge(t,e)})}import v from"chalk";import{readFileSync as yo,writeFileSync as Jr,existsSync as bo,mkdirSync as Wr,appendFileSync as Gr}from"fs";import{join as fe}from"path";import{homedir as vn}from"os";var ko=fe(vn(),".cctrack"),ho=fe(ko,"rate-events.jsonl"),_o=fe(ko,"rate-model.json"),wn=300*60*1e3;function he(){try{if(bo(_o))return JSON.parse(yo(_o,"utf-8"))}catch{}return{limits:{},updated_at:new Date().toISOString()}}function Lt(o){return o.includes("opus")?"opus":o.includes("sonnet")?"sonnet":o.includes("haiku")?"haiku":"unknown"}function vo(){try{return bo(ho)?yo(ho,"utf-8").split(`
|
|
572
|
+
`).filter(Boolean).map(o=>JSON.parse(o)):[]}catch{return[]}}function xt(o){let e=Date.now(),t=new Date(e-wn),r=0,c=0,s=0;for(let n of o){let a=new Date(n.timestamp).getTime();if(a<t.getTime()||a>e)continue;let i=n.message.usage;r+=i.input_tokens+(i.cache_creation_input_tokens??0),c+=i.input_tokens+i.output_tokens+(i.cache_creation_input_tokens??0)+(i.cache_read_input_tokens??0),s++}return{billable_tokens:r,total_tokens:c,requests:s,window_start:t}}function Kt(o,e){let t=Lt(e),c=he().limits[t],s=xt(o);if(c&&c.sample_count>0){let n=s.billable_tokens/c.estimated_limit*100,i=(Date.now()-s.window_start.getTime())/(1e3*60*60),d=null;if(i>.1&&s.billable_tokens>0){let m=s.billable_tokens/i,l=c.estimated_limit-s.billable_tokens;l>0&&m>0?d=Math.round(l/m*60):d=0}return{model_family:t,estimated_utilization:Math.min(Math.round(n*10)/10,100),confidence:c.confidence,estimated_limit:Math.round(c.estimated_limit),current_consumption:s.billable_tokens,minutes_to_limit:d,calibration_events:c.sample_count,source:"calibrated"}}return{model_family:t,estimated_utilization:0,confidence:0,estimated_limit:0,current_consumption:s.billable_tokens,minutes_to_limit:null,calibration_events:0,source:"uncalibrated"}}if(import.meta.vitest){let{describe:o,it:e,expect:t}=import.meta.vitest,{makeEntry:r}=await Promise.resolve().then(()=>(St(),Bt));o("modelFamily",()=>{e("extracts opus",()=>t(Lt("claude-opus-4-6-20260205")).toBe("opus")),e("extracts sonnet",()=>t(Lt("claude-sonnet-4-20250514")).toBe("sonnet")),e("extracts haiku",()=>t(Lt("claude-haiku-4-5-20251001")).toBe("haiku")),e("returns unknown for unrecognized",()=>t(Lt("gpt-4")).toBe("unknown"))}),o("currentWindowConsumption",()=>{e("counts only entries in last 5 hours",()=>{let c=new Date,s=new Date(c.getTime()-6e4).toISOString(),n=new Date(c.getTime()-360*60*1e3).toISOString(),a=[r({timestamp:s}),r({timestamp:n})],i=xt(a);t(i.requests).toBe(1)}),e("billable tokens exclude cache_read",()=>{let c=new Date,s=new Date(c.getTime()-6e4).toISOString(),n=[r({timestamp:s,message:{model:"claude-opus-4-6",usage:{input_tokens:100,output_tokens:50,cache_creation_input_tokens:200,cache_read_input_tokens:5e3}}})],a=xt(n);t(a.billable_tokens).toBe(300)}),e("returns zero for empty entries",()=>{let c=xt([]);t(c.billable_tokens).toBe(0),t(c.requests).toBe(0)})}),o("predictUtilization",()=>{e("returns uncalibrated when no model data",()=>{let c=Kt([],"claude-opus-4-6");t(c.source).toBe("uncalibrated"),t(c.confidence).toBe(0),t(c.calibration_events).toBe(0)}),e("returns calibrated when model has learned limits",async()=>{let{writeFileSync:c,mkdirSync:s,readFileSync:n,existsSync:a,unlinkSync:i}=await import("fs"),{join:d}=await import("path"),{homedir:m}=await import("os"),l=d(m(),".cctrack");s(l,{recursive:!0});let u=d(l,"rate-model.json"),f=a(u)?n(u,"utf-8"):null;c(u,JSON.stringify({limits:{opus:{estimated_limit:1e6,confidence:.8,sample_count:5,last_calibration:"2026-03-25T00:00:00Z",alpha:.3}},updated_at:"2026-03-25T00:00:00Z"}));let y=new Date,g=[r({timestamp:new Date(y.getTime()-6e4).toISOString(),message:{model:"claude-opus-4-6",usage:{input_tokens:500,output_tokens:200,cache_creation_input_tokens:100,cache_read_input_tokens:0}}})],p=Kt(g,"claude-opus-4-6");if(t(p.source).toBe("calibrated"),t(p.confidence).toBe(.8),t(p.estimated_limit).toBe(1e6),t(p.current_consumption).toBe(600),f)c(u,f);else try{i(u)}catch{}})})}function xn(o,e=30){let t=Math.min(Math.max(o,0),100),r=Math.round(t/100*e),c="\u2588".repeat(r)+"\u2591".repeat(e-r);return o>=80?v.red(c):o>=50?v.yellow(c):v.green(c)}function wo(o){o.command("limits").description("Predict rate limit utilization from your usage patterns").option("--json","Output as JSON").option("--mode <mode>","Cost mode","calculate").action(async e=>{let t=I(),r=B(t),{entries:c}=await j(r),s=x(c),n=S(e.mode),i=[...s].sort((f,y)=>y.timestamp.localeCompare(f.timestamp))[0]?.message.model??"claude-opus-4-6",d=Kt(s,i),m=xt(s),l=he(),u=vo();if(e.json){console.log(JSON.stringify({prediction:d,consumption:m,model:l,events_count:u.length},null,2));return}if(console.log(v.bold("Rate Limit Analysis")),console.log(v.dim(`Model: ${i} (${d.model_family})`)),console.log(""),console.log(v.bold("Current 5-Hour Window")),console.log(` Billable tokens: ${v.white(_(m.billable_tokens))} ${v.dim("(input + cache_creation, excludes cache_read)")}`),console.log(` Total tokens: ${v.white(_(m.total_tokens))}`),console.log(` Requests: ${v.white(m.requests.toString())}`),console.log(""),d.source==="calibrated"){if(console.log(v.bold("Estimated Utilization")+v.dim(` (${d.calibration_events} calibration events, ${Math.round(d.confidence*100)}% confidence)`)),console.log(` ${xn(d.estimated_utilization)} ${v.white(d.estimated_utilization.toFixed(1)+"%")}`),console.log(` Estimated limit: ${v.white(_(d.estimated_limit))} billable tokens / 5h`),d.minutes_to_limit!==null)if(d.minutes_to_limit===0)console.log(` Time to limit: ${v.red("NOW \u2014 you may be rate limited")}`);else{let f=d.minutes_to_limit<30?v.red:d.minutes_to_limit<60?v.yellow:v.green;console.log(` Time to limit: ${f(R(d.minutes_to_limit*60*1e3))}`)}}else console.log(v.bold("Estimated Utilization")),console.log(v.dim(" No calibration data yet. The model learns when you hit rate limits.")),console.log(v.dim(" Keep using Claude Code normally \u2014 the first time you hit a limit,")),console.log(v.dim(" cctrack will record it and start predicting.")),console.log(""),console.log(v.dim(" To see your actual limits right now, use:")),console.log(v.dim(" /usage in Claude Code"));if(console.log(""),u.length>0){console.log(v.bold("Rate Limit History"));let f=u.slice(-5).reverse();for(let y of f){let g=y.timestamp.slice(0,16).replace("T"," ");console.log(` ${v.dim(g)} ${y.model} \u2014 ${_(y.input_tokens_in_window)} billable tokens`),y.reset_time&&console.log(` ${v.dim("Reset: "+y.reset_time)}`)}}else console.log(v.dim("No rate limit events recorded yet."))})}_t();se().catch(()=>{});var Z=new Cn;Z.name("cctrack").description("Claude Code usage analytics \u2014 accurate metrics and a beautiful HTML dashboard").version("0.1.0").addHelpText("after",`
|
|
573
|
+
Examples:
|
|
574
|
+
cctrack Open interactive HTML dashboard
|
|
575
|
+
cctrack daily --since YYYY-MM-DD Daily cost breakdown from a date
|
|
576
|
+
cctrack blocks 5-hour rolling window usage
|
|
577
|
+
cctrack roi --plan max20 ROI analysis vs Max 20 plan
|
|
578
|
+
cctrack live Real-time terminal monitor
|
|
579
|
+
cctrack statusline Compact status for tmux/editors
|
|
580
|
+
cctrack config set budget.daily 50 Set daily budget alert at $50
|
|
581
|
+
`);Re(Z);Fe(Z);He(Z);Ge(Z);Ke(Z);Ve(Z);to(Z);eo(Z);oo(Z);fo(Z);po(Z);wo(Z);Z.action(async()=>{await le({})});Z.parse();
|
|
582
|
+
//# sourceMappingURL=index.js.map
|