@jnst/cursor-usage 0.2.1 → 0.3.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/README.md CHANGED
@@ -53,14 +53,14 @@ By Model
53
53
 
54
54
  Options:
55
55
 
56
- | Option | Description |
57
- | --- | --- |
58
- | `--by <day\|user\|model>` | Show a single breakdown axis |
59
- | `--day <YYYY-MM-DD>` | Drill into a single day (hourly, model, user, kind, top events) |
60
- | `--user <identifier>` | Filter analysis to a single User |
61
- | `--timezone <iana-tz>` | Group days and hours in a specific analysis time zone |
62
- | `--json` | Output aggregated stats as JSON (pipe to jq etc.) |
63
- | `--include-no-charge` | Include "Errored, No Charge" events |
56
+ | Option | Description |
57
+ | ------------------------- | --------------------------------------------------------------- |
58
+ | `--by <day\|user\|model>` | Show a single breakdown axis |
59
+ | `--day <YYYY-MM-DD>` | Drill into a single day (hourly, model, user, kind, top events) |
60
+ | `--user <identifier>` | Filter analysis to a single User |
61
+ | `--timezone <iana-tz>` | Group days and hours in a specific analysis time zone |
62
+ | `--json` | Output aggregated stats as JSON (pipe to jq etc.) |
63
+ | `--include-no-charge` | Include "Errored, No Charge" events |
64
64
 
65
65
  ```bash
66
66
  # Extract key numbers
@@ -95,6 +95,36 @@ bun dist/cli.js stats usage.csv
95
95
  bun scripts/generate-dummy-csv.ts > dummy-usage.csv
96
96
  ```
97
97
 
98
+ ### Release
99
+
100
+ Release commands verify, version, publish, push commits/tags, and create a
101
+ draft GitHub Release with generated notes:
102
+
103
+ ```bash
104
+ bun run release:patch
105
+ bun run release:minor
106
+ bun run release:major
107
+ ```
108
+
109
+ GitHub Releases are drafts by default so notes can be reviewed before publish.
110
+ Use `--publish-release` to publish the GitHub Release immediately, or
111
+ `--dry-run` to print mutating steps without running them:
112
+
113
+ ```bash
114
+ bun run release:patch --dry-run
115
+ bun run release:patch --publish-release
116
+ ```
117
+
118
+ Release commands are safe to rerun after a partial failure. The script checks
119
+ the current tag, npm package version, and GitHub Release before each publishing
120
+ step:
121
+
122
+ ```bash
123
+ # If npm publish, git push, or GitHub Release creation failed midway,
124
+ # fix the problem and run the same command again.
125
+ bun run release:patch
126
+ ```
127
+
98
128
  ## Architecture
99
129
 
100
130
  - `src/core/` — CSV parsing and aggregation (pure TS, shared between terminal and browser)
package/dist/cli.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import{readFile as Wg}from"node:fs/promises";import{parseArgs as m}from"node:util";var w="Errored, No Charge";var F="UTC";function h(){return Intl.DateTimeFormat().resolvedOptions().timeZone||F}function k(g){try{return new Intl.DateTimeFormat("en-US",{timeZone:g}).format(new Date),!0}catch{return!1}}function R(g,f,$){return new Intl.DateTimeFormat("en-GB",{timeZone:f,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",hourCycle:"h23"}).formatToParts(g).find((J)=>J.type===$)?.value??""}function D(g,f=F){return[R(g,f,"year"),R(g,f,"month"),R(g,f,"day")].join("-")}function a(g,f=F){return R(g,f,"hour")}function _(g,f,$=F){return g.filter((Y)=>D(Y.date,$)===f)}function E(g){return g.filter((f)=>f.kind!==w)}function K(g,f=F){let $=0,Y=0,J=0,S=new Set,V=new Set,A=new Set;for(let G of g){if($+=G.cost,Y+=G.totalTokens,G.maxMode)J++;S.add(D(G.date,f)),V.add(G.user),A.add(G.model)}let j=[...S].sort();return{totalCost:$,totalTokens:Y,eventCount:g.length,firstDay:j[0]??null,lastDay:j[j.length-1]??null,dayCount:S.size,avgCostPerActiveDay:S.size>0?$/S.size:0,maxModeRatio:g.length>0?J/g.length:0,userCount:V.size,modelCount:A.size}}function P(g,f){let $=new Map;for(let Y of g){let J=f(Y),S=$.get(J);if(!S)S={key:J,cost:0,totalTokens:0,inputTokens:0,outputTokens:0,cacheRead:0,eventCount:0},$.set(J,S);S.cost+=Y.cost,S.totalTokens+=Y.totalTokens,S.inputTokens+=Y.inputWithCacheWrite+Y.inputWithoutCacheWrite,S.outputTokens+=Y.outputTokens,S.cacheRead+=Y.cacheRead,S.eventCount++}return[...$.values()]}function p(g,f=F){return P(g,($)=>D($.date,f)).sort(($,Y)=>$.key.localeCompare(Y.key))}function N(g){return P(g,(f)=>f.user).sort((f,$)=>$.cost-f.cost)}function c(g){return P(g,(f)=>f.model).sort((f,$)=>$.cost-f.cost)}function x(g){return P(g,(f)=>f.kind).sort((f,$)=>$.cost-f.cost)}function T(g,f=F){return P(g,($)=>a($.date,f)).sort(($,Y)=>$.key.localeCompare(Y.key))}function l(g,f){return[...g].sort(($,Y)=>Y.cost-$.cost).slice(0,f)}function t(g){let f=[],$=[],Y="",J=!1,S=0,V=()=>{$.push(Y),Y=""},A=()=>{V(),f.push($),$=[]};while(S<g.length){let j=g[S];if(J){if(j==='"'){if(g[S+1]==='"'){Y+='"',S+=2;continue}J=!1,S++;continue}Y+=j,S++;continue}if(j==='"'){J=!0,S++;continue}if(j===","){V(),S++;continue}if(j==="\r"){S++;continue}if(j===`
3
- `){A(),S++;continue}Y+=j,S++}if(Y.length>0||$.length>0)A();return f}var e=["Date","User","Cloud Agent ID","Automation ID","Kind","Model","Max Mode","Input (w/ Cache Write)","Input (w/o Cache Write)","Cache Read","Output Tokens","Total Tokens","Cost"];function H(g){if(!g)return 0;let f=Number(g);return Number.isFinite(f)?f:0}function u(g){let f=t(g.trim());if(f.length===0)return[];let $=f[0],Y=new Map;$.forEach((V,A)=>Y.set(V.trim(),A));for(let V of["Date","User","Model","Cost"])if(!Y.has(V))throw Error(`Invalid CSV: missing column "${V}". Expected a Cursor usage-events export with columns: ${e.join(", ")}`);let J=(V,A)=>{let j=Y.get(A);return j===void 0?"":(V[j]??"").trim()},S=[];for(let V of f.slice(1)){if(V.length===1&&V[0]==="")continue;let A=J(V,"Date"),j=new Date(A);if(Number.isNaN(j.getTime()))continue;S.push({date:j,user:J(V,"User"),cloudAgentId:J(V,"Cloud Agent ID")||null,automationId:J(V,"Automation ID")||null,kind:J(V,"Kind"),model:J(V,"Model"),maxMode:J(V,"Max Mode").toLowerCase()==="yes",inputWithCacheWrite:H(J(V,"Input (w/ Cache Write)")),inputWithoutCacheWrite:H(J(V,"Input (w/o Cache Write)")),cacheRead:H(J(V,"Cache Read")),outputTokens:H(J(V,"Output Tokens")),totalTokens:H(J(V,"Total Tokens")),cost:H(J(V,"Cost"))})}return S}import{spawn as gg}from"node:child_process";import{createReadStream as fg,existsSync as $g}from"node:fs";import{stat as Sg}from"node:fs/promises";import{createServer as Vg}from"node:http";import{dirname as Jg,extname as Yg,join as M,normalize as Ag}from"node:path";import{fileURLToPath as jg}from"node:url";var Z=4321,Gg={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".map":"application/json"};function Xg(){let g=Jg(jg(import.meta.url)),f=[M(g,"web"),M(g,"../../dist/web")];for(let $ of f)if($g(M($,"index.html")))return $;throw Error("Dashboard assets not found. Run `bun run build` first (expected dist/web/index.html).")}function qg(g){let[f,...$]=process.platform==="darwin"?["open",g]:process.platform==="win32"?["cmd","/c","start","",g]:["xdg-open",g];try{gg(f,$,{stdio:"ignore",detached:!0}).unref()}catch{}}function b(g){let f=Xg(),$=Vg(async(J,S)=>{let V=decodeURIComponent(new URL(J.url??"/","http://localhost").pathname),A=Ag(M(f,V==="/"?"/index.html":V));if(!A.startsWith(f)){S.writeHead(403),S.end("Forbidden");return}try{let j=await Sg(A);if(!j.isFile())throw Error("not a file");S.writeHead(200,{"Content-Type":Gg[Yg(A)]??"application/octet-stream","Content-Length":j.size}),fg(A).pipe(S)}catch{S.writeHead(404),S.end("Not Found")}}),Y=()=>{let{port:J}=$.address(),S=`http://localhost:${J}`;if(console.log(`cursor-usage dashboard running at ${S}`),console.log("Drop a Cursor usage-events CSV onto the page. Ctrl+C to stop."),g.open)qg(S)};if(g.port!==void 0){$.listen(g.port,Y);return}$.once("error",(J)=>{if(J.code!=="EADDRINUSE")throw J;console.log(`port ${Z} is in use, picked a free port instead`),$.listen(0)}),$.once("listening",Y),$.listen(Z)}var zg=process.stdout.isTTY&&!process.env.NO_COLOR,B=(g)=>(f)=>zg?`\x1B[${g}m${f}\x1B[0m`:f,Q=B("1"),z=B("2"),v=B("36"),Cg=B("32"),wg=B("33");function I(g){return`$${g.toFixed(2)}`}function O(g){if(g>=1e9)return`${(g/1e9).toFixed(1)}B`;if(g>=1e6)return`${(g/1e6).toFixed(1)}M`;if(g>=1000)return`${(g/1000).toFixed(1)}K`;return String(g)}function y(g,f,$){if(f<=0||g<=0)return"";let Y=Math.round(g/f*$*8),J=Math.floor(Y/8),S=Y%8,V=["","▏","▎","▍","▌","▋","▊","▉"];return"█".repeat(J)+(V[S]??"")}function q(g,f){return g.length>=f?g:g+" ".repeat(f-g.length)}function C(g,f,$){return new Intl.DateTimeFormat("en-GB",{timeZone:f,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(g).find((J)=>J.type===$)?.value??""}function Fg(g,f){return[C(g,f,"hour"),C(g,f,"minute"),C(g,f,"second")].join(":")}function Kg(g,f,$){let Y=g.firstDay&&g.lastDay?`${g.firstDay} – ${g.lastDay}`:"no data",J=$?`${f}, user ${$}`:f,S=(A)=>z(q(A,14)),V=(A)=>Q(q(A,12));return[`${Q("Cursor Usage")} ${Y} ${z(`(${g.eventCount} events, ${g.dayCount} days, ${J})`)}`,"",` ${S("Total Cost")}${V(I(g.totalCost))} ${S("Total Tokens")}${V(O(g.totalTokens))}`,` ${S("Avg/Active")}${V(I(g.avgCostPerActiveDay))} ${S("Max Mode")}${V(`${Math.round(g.maxModeRatio*100)}%`)}`,` ${S("Users")}${V(String(g.userCount))} ${S("Models")}${V(String(g.modelCount))}`]}function W(g,f,$={totalCost:0}){let{totalCost:Y,barWidth:J=28,maxRows:S=15}=$,V=f.slice(0,S),A=Math.max(...V.map((X)=>X.key.length),4),j=Math.max(...V.map((X)=>X.cost),0),G=[Q(g)];for(let X of V){let U=Y>0?` ${z(`${Math.round(X.cost/Y*100)}%`)}`:"";G.push(` ${q(X.key,A)} ${q(I(X.cost),8)} ${v(q(y(X.cost,j,J),J))}${U} ${z(`${O(X.totalTokens)} tok, ${X.eventCount} ev`)}`)}if(f.length>S)G.push(z(` … and ${f.length-S} more`));return G}function i(g,f,$,Y){let J=K(g,$),S=[Kg(J,$,Y)],V={day:()=>W("Daily Cost",p(g,$),{totalCost:J.totalCost,maxRows:31}),model:()=>W("By Model",c(g),{totalCost:J.totalCost}),user:()=>W("By User",N(g),{totalCost:J.totalCost})};if(f)S.push(V[f]());else S.push(V.day(),V.model(),V.user());return S.map((A)=>A.join(`
2
+ import{readFile as Wg}from"node:fs/promises";import{parseArgs as m}from"node:util";var k="Errored, No Charge";var F="UTC";function w(){return Intl.DateTimeFormat().resolvedOptions().timeZone||F}function h(g){try{return new Intl.DateTimeFormat("en-US",{timeZone:g}).format(new Date),!0}catch{return!1}}function M(g,f,$){return new Intl.DateTimeFormat("en-GB",{timeZone:f,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",hourCycle:"h23"}).formatToParts(g).find((J)=>J.type===$)?.value??""}function x(g,f=F){return[M(g,f,"year"),M(g,f,"month"),M(g,f,"day")].join("-")}function a(g,f=F){return M(g,f,"hour")}function D(g,f,$=F){return g.filter((V)=>x(V.date,$)===f)}function E(g){return g.filter((f)=>f.kind!==k)}function K(g,f=F){let $=0,V=0,J=0,S=new Set,Y=new Set,A=new Set;for(let G of g){if($+=G.cost,V+=G.totalTokens,G.maxMode)J++;S.add(x(G.date,f)),Y.add(G.user),A.add(G.model)}let j=[...S].sort();return{totalCost:$,totalTokens:V,eventCount:g.length,firstDay:j[0]??null,lastDay:j[j.length-1]??null,dayCount:S.size,avgCostPerActiveDay:S.size>0?$/S.size:0,maxModeRatio:g.length>0?J/g.length:0,userCount:Y.size,modelCount:A.size}}function P(g,f){let $=new Map;for(let V of g){let J=f(V),S=$.get(J);if(!S)S={key:J,cost:0,totalTokens:0,inputTokens:0,outputTokens:0,cacheRead:0,eventCount:0},$.set(J,S);S.cost+=V.cost,S.totalTokens+=V.totalTokens,S.inputTokens+=V.inputWithCacheWrite+V.inputWithoutCacheWrite,S.outputTokens+=V.outputTokens,S.cacheRead+=V.cacheRead,S.eventCount++}return[...$.values()]}function O(g,f=F){return P(g,($)=>x($.date,f)).sort(($,V)=>$.key.localeCompare(V.key))}function N(g){return P(g,(f)=>f.user).sort((f,$)=>$.cost-f.cost)}function B(g){return P(g,(f)=>f.model).sort((f,$)=>$.cost-f.cost)}function c(g){return P(g,(f)=>f.kind).sort((f,$)=>$.cost-f.cost)}function T(g,f=F){return P(g,($)=>a($.date,f)).sort(($,V)=>$.key.localeCompare(V.key))}function l(g,f){return[...g].sort(($,V)=>V.cost-$.cost).slice(0,f)}function t(g){let f=[],$=[],V="",J=!1,S=0,Y=()=>{$.push(V),V=""},A=()=>{Y(),f.push($),$=[]};while(S<g.length){let j=g[S];if(J){if(j==='"'){if(g[S+1]==='"'){V+='"',S+=2;continue}J=!1,S++;continue}V+=j,S++;continue}if(j==='"'){J=!0,S++;continue}if(j===","){Y(),S++;continue}if(j==="\r"){S++;continue}if(j===`
3
+ `){A(),S++;continue}V+=j,S++}if(V.length>0||$.length>0)A();return f}var e=["Date","User","Cloud Agent ID","Automation ID","Kind","Model","Max Mode","Input (w/ Cache Write)","Input (w/o Cache Write)","Cache Read","Output Tokens","Total Tokens","Cost"];function H(g){if(!g)return 0;let f=Number(g);return Number.isFinite(f)?f:0}function Z(g){let f=t(g.trim());if(f.length===0)return[];let $=f[0],V=new Map;$.forEach((Y,A)=>V.set(Y.trim(),A));for(let Y of["Date","User","Model","Cost"])if(!V.has(Y))throw Error(`Invalid CSV: missing column "${Y}". Expected a Cursor usage-events export with columns: ${e.join(", ")}`);let J=(Y,A)=>{let j=V.get(A);return j===void 0?"":(Y[j]??"").trim()},S=[];for(let Y of f.slice(1)){if(Y.length===1&&Y[0]==="")continue;let A=J(Y,"Date"),j=new Date(A);if(Number.isNaN(j.getTime()))continue;S.push({date:j,user:J(Y,"User"),cloudAgentId:J(Y,"Cloud Agent ID")||null,automationId:J(Y,"Automation ID")||null,kind:J(Y,"Kind"),model:J(Y,"Model"),maxMode:J(Y,"Max Mode").toLowerCase()==="yes",inputWithCacheWrite:H(J(Y,"Input (w/ Cache Write)")),inputWithoutCacheWrite:H(J(Y,"Input (w/o Cache Write)")),cacheRead:H(J(Y,"Cache Read")),outputTokens:H(J(Y,"Output Tokens")),totalTokens:H(J(Y,"Total Tokens")),cost:H(J(Y,"Cost"))})}return S}import{spawn as gg}from"node:child_process";import{createReadStream as fg,existsSync as $g}from"node:fs";import{stat as Sg}from"node:fs/promises";import{createServer as Yg}from"node:http";import{dirname as Jg,extname as Vg,join as p,normalize as Ag}from"node:path";import{fileURLToPath as jg}from"node:url";var u=4321,Gg={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".map":"application/json"};function Xg(){let g=Jg(jg(import.meta.url)),f=[p(g,"web"),p(g,"../../dist/web")];for(let $ of f)if($g(p($,"index.html")))return $;throw Error("Dashboard assets not found. Run `bun run build` first (expected dist/web/index.html).")}function qg(g){let[f,...$]=process.platform==="darwin"?["open",g]:process.platform==="win32"?["cmd","/c","start","",g]:["xdg-open",g];try{gg(f,$,{stdio:"ignore",detached:!0}).unref()}catch{}}function b(g){let f=Xg(),$=Yg(async(J,S)=>{let Y=decodeURIComponent(new URL(J.url??"/","http://localhost").pathname),A=Ag(p(f,Y==="/"?"/index.html":Y));if(!A.startsWith(f)){S.writeHead(403),S.end("Forbidden");return}try{let j=await Sg(A);if(!j.isFile())throw Error("not a file");S.writeHead(200,{"Content-Type":Gg[Vg(A)]??"application/octet-stream","Content-Length":j.size}),fg(A).pipe(S)}catch{S.writeHead(404),S.end("Not Found")}}),V=()=>{let{port:J}=$.address(),S=`http://localhost:${J}`;if(console.log(`cursor-usage dashboard running at ${S}`),console.log("Drop a Cursor usage-events CSV onto the page. Ctrl+C to stop."),g.open)qg(S)};if(g.port!==void 0){$.listen(g.port,V);return}$.once("error",(J)=>{if(J.code!=="EADDRINUSE")throw J;console.log(`port ${u} is in use, picked a free port instead`),$.listen(0)}),$.once("listening",V),$.listen(u)}var zg=process.stdout.isTTY&&!process.env.NO_COLOR,R=(g)=>(f)=>zg?`\x1B[${g}m${f}\x1B[0m`:f,Q=R("1"),z=R("2"),v=R("36"),Cg=R("32"),kg=R("33");function I(g){return`$${g.toFixed(2)}`}function U(g){if(g>=1e9)return`${(g/1e9).toFixed(1)}B`;if(g>=1e6)return`${(g/1e6).toFixed(1)}M`;if(g>=1000)return`${(g/1000).toFixed(1)}K`;return String(g)}function y(g,f,$){if(f<=0||g<=0)return"";let V=Math.round(g/f*$*8),J=Math.floor(V/8),S=V%8,Y=["","▏","▎","▍","▌","▋","▊","▉"];return"█".repeat(J)+(Y[S]??"")}function q(g,f){return g.length>=f?g:g+" ".repeat(f-g.length)}function C(g,f,$){return new Intl.DateTimeFormat("en-GB",{timeZone:f,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(g).find((J)=>J.type===$)?.value??""}function Fg(g,f){return[C(g,f,"hour"),C(g,f,"minute"),C(g,f,"second")].join(":")}function Kg(g,f,$){let V=g.firstDay&&g.lastDay?`${g.firstDay} – ${g.lastDay}`:"no data",J=$?`${f}, user ${$}`:f,S=(A)=>z(q(A,14)),Y=(A)=>Q(q(A,12));return[`${Q("Cursor Usage")} ${V} ${z(`(${g.eventCount} events, ${g.dayCount} days, ${J})`)}`,"",` ${S("Total Cost")}${Y(I(g.totalCost))} ${S("Total Tokens")}${Y(U(g.totalTokens))}`,` ${S("Avg/Active")}${Y(I(g.avgCostPerActiveDay))} ${S("Max Mode")}${Y(`${Math.round(g.maxModeRatio*100)}%`)}`,` ${S("Users")}${Y(String(g.userCount))} ${S("Models")}${Y(String(g.modelCount))}`]}function W(g,f,$={totalCost:0}){let{totalCost:V,barWidth:J=28,maxRows:S=15}=$,Y=f.slice(0,S),A=Math.max(...Y.map((X)=>X.key.length),4),j=Math.max(...Y.map((X)=>X.cost),0),G=[Q(g)];for(let X of Y){let _=V>0?` ${z(`${Math.round(X.cost/V*100)}%`)}`:"";G.push(` ${q(X.key,A)} ${q(I(X.cost),8)} ${v(q(y(X.cost,j,J),J))}${_} ${z(`${U(X.totalTokens)} tok, ${X.eventCount} ev`)}`)}if(f.length>S)G.push(z(` … and ${f.length-S} more`));return G}function i(g,f,$,V){let J=K(g,$),S=[Kg(J,$,V)],Y={day:()=>W("Daily Cost",O(g,$),{totalCost:J.totalCost,maxRows:31}),model:()=>W("By Model",B(g),{totalCost:J.totalCost}),user:()=>W("By User",N(g),{totalCost:J.totalCost})};if(f)S.push(Y[f]());else S.push(Y.day(),Y.model(),Y.user());return S.map((A)=>A.join(`
4
4
  `)).join(`
5
5
 
6
6
  `)+`
7
- `}function r(g,f,$){return JSON.stringify({timeZone:f,filters:{user:$??null},summary:K(g,f),byDay:p(g,f),byModel:c(g),byUser:N(g)},null,2)}function Qg(g,f,$,Y,J,S){let V=K(f,$),A=Y>0?Math.round(V.totalCost/Y*100):0,j=(X)=>z(q(X,14)),G=(X)=>Q(q(X,12));return[`${Q(`Day ${g}`)} ${z(`(${V.eventCount} events, rank ${J}/${S} by cost, ${$})`)}`,"",` ${j("Cost")}${G(I(V.totalCost))} ${j("of period")}${G(`${A}%`)}`,` ${j("Total Tokens")}${G(O(V.totalTokens))} ${j("Max Mode")}${G(`${Math.round(V.maxModeRatio*100)}%`)}`,` ${j("Users")}${G(String(V.userCount))} ${j("Models")}${G(String(V.modelCount))}`]}function Lg(g,f){let $=new Map(T(g,f).map((S)=>[S.key,S])),Y=Math.max(...[...$.values()].map((S)=>S.cost),0),J=[Q(`By Hour (${f})`)];for(let S=0;S<24;S++){let V=String(S).padStart(2,"0"),A=$.get(V),j=A?.cost??0,G=A?.eventCount??0,X=G>0?z(` ${G} ev`):"";J.push(` ${V} ${q(j>0?I(j):"",8)} ${v(q(y(j,Y,24),24))}${X}`)}return J}function Hg(g,f,$){let Y=l(g,f),J=[Q(`Top Events (${Y.length} of ${g.length})`)],S=Math.max(...Y.map((A)=>A.user.length),4),V=Math.max(...Y.map((A)=>A.model.length),5);for(let A of Y){let j=Fg(A.date,$);J.push(` ${z(j)} ${q(A.user,S)} ${q(A.model,V)} ${q(I(A.cost),8)} ${z(`${O(A.totalTokens)} tok`)}`)}return J}function d(g,f,$,Y){let J=p(g,$),S=_(g,f,$);if(S.length===0){let G=J.map((U)=>U.key),X=G.length>0?`
7
+ `}function r(g,f,$){return JSON.stringify({timeZone:f,filters:{user:$??null},summary:K(g,f),byDay:O(g,f),byModel:B(g),byUser:N(g)},null,2)}function Qg(g,f,$,V,J,S){let Y=K(f,$),A=V>0?Math.round(Y.totalCost/V*100):0,j=(X)=>z(q(X,14)),G=(X)=>Q(q(X,12));return[`${Q(`Day ${g}`)} ${z(`(${Y.eventCount} events, rank ${J}/${S} by cost, ${$})`)}`,"",` ${j("Cost")}${G(I(Y.totalCost))} ${j("of period")}${G(`${A}%`)}`,` ${j("Total Tokens")}${G(U(Y.totalTokens))} ${j("Max Mode")}${G(`${Math.round(Y.maxModeRatio*100)}%`)}`,` ${j("Users")}${G(String(Y.userCount))} ${j("Models")}${G(String(Y.modelCount))}`]}function Lg(g,f){let $=new Map(T(g,f).map((S)=>[S.key,S])),V=Math.max(...[...$.values()].map((S)=>S.cost),0),J=[Q(`By Hour (${f})`)];for(let S=0;S<24;S++){let Y=String(S).padStart(2,"0"),A=$.get(Y),j=A?.cost??0,G=A?.eventCount??0,X=G>0?z(` ${G} ev`):"";J.push(` ${Y} ${q(j>0?I(j):"",8)} ${v(q(y(j,V,24),24))}${X}`)}return J}function Hg(g,f,$){let V=l(g,f),J=[Q(`Top Events (${V.length} of ${g.length})`)],S=Math.max(...V.map((A)=>A.user.length),4),Y=Math.max(...V.map((A)=>A.model.length),5);for(let A of V){let j=Fg(A.date,$);J.push(` ${z(j)} ${q(A.user,S)} ${q(A.model,Y)} ${q(I(A.cost),8)} ${z(`${U(A.totalTokens)} tok`)}`)}return J}function d(g,f,$,V){let J=O(g,$),S=D(g,f,$);if(S.length===0){let G=J.map((_)=>_.key),X=G.length>0?`
8
8
  Available days: ${G[0]} – ${G[G.length-1]}`:"";return`No billable events on ${f}.${X}
9
- `}let V=g.reduce((G,X)=>G+X.cost,0),A=[...J].sort((G,X)=>X.cost-G.cost).findIndex((G)=>G.key===f)+1;return[Qg(f,S,$,V,A,J.length),...Y?[[z(`Filtered to user: ${Y}`)]]:[],Lg(S,$),W("By Model",c(S),{totalCost:K(S,$).totalCost}),W("By User",N(S),{totalCost:K(S,$).totalCost}),W("By Kind",x(S),{totalCost:0}),Hg(S,20,$)].map((G)=>G.join(`
9
+ `}let Y=g.reduce((G,X)=>G+X.cost,0),A=[...J].sort((G,X)=>X.cost-G.cost).findIndex((G)=>G.key===f)+1;return[Qg(f,S,$,Y,A,J.length),...V?[[z(`Filtered to user: ${V}`)]]:[],Lg(S,$),W("By Model",B(S),{totalCost:K(S,$).totalCost}),W("By User",N(S),{totalCost:K(S,$).totalCost}),W("By Kind",c(S),{totalCost:0}),Hg(S,20,$)].map((G)=>G.join(`
10
10
  `)).join(`
11
11
 
12
12
  `)+`
13
- `}function o(g,f,$,Y){let J=_(g,f,$);return JSON.stringify({day:f,timeZone:$,filters:{user:Y??null},summary:K(J,$),byHour:T(J,$),byModel:c(J),byUser:N(J),byKind:x(J)},null,2)}var s=`cursor-usage — visualize Cursor usage-events CSV
13
+ `}function o(g,f,$,V){let J=D(g,f,$);return JSON.stringify({day:f,timeZone:$,filters:{user:V??null},summary:K(J,$),byHour:T(J,$),byModel:B(J),byUser:N(J),byKind:c(J)},null,2)}var s=`cursor-usage — visualize Cursor usage-events CSV
14
14
 
15
15
  Usage:
16
16
  cursor-usage [serve] [--port <n>] [--no-open] Start the drag & drop dashboard (default)
@@ -31,4 +31,4 @@ Serve options:
31
31
 
32
32
  -h, --help Show this help
33
33
  `;function L(g){console.error(`Error: ${g}
34
- `),console.error(s),process.exit(1)}async function Ig(g){let{values:f,positionals:$}=m({args:g,allowPositionals:!0,options:{by:{type:"string"},day:{type:"string"},user:{type:"string"},timezone:{type:"string"},json:{type:"boolean",default:!1},"include-no-charge":{type:"boolean",default:!1}}}),Y=$[0];if(!Y)L("stats requires a path to a CSV file");let J;try{J=await Wg(Y,"utf8")}catch{L(`file not found: ${Y}`)}let S=f.by;if(S&&!["day","user","model"].includes(S))L(`invalid --by value: ${S} (expected day, user or model)`);let V=f.day;if(V!==void 0&&!/^\d{4}-\d{2}-\d{2}$/.test(V))L(`invalid --day value: ${V} (expected YYYY-MM-DD)`);let A=f.timezone??h();if(!k(A))L(`invalid --timezone value: ${A}`);let j=u(J);if(!f["include-no-charge"])j=E(j);let G=f.user;if(G)j=j.filter((X)=>X.user===G);if(j.length===0)console.error("No usage events found for the requested filters."),process.exit(1);if(V){console.log(f.json?o(j,V,A,G):d(j,V,A,G));return}console.log(f.json?r(j,A,G):i(j,S,A,G))}function n(g){let{values:f}=m({args:g,allowPositionals:!1,options:{port:{type:"string"},"no-open":{type:"boolean",default:!1}}}),$;if(f.port!==void 0){if($=Number(f.port),!Number.isInteger($)||$<0||$>65535)L(`invalid --port value: ${f.port}`)}b({port:$,open:!f["no-open"]})}async function Pg(){let g=process.argv.slice(2);if(g.includes("-h")||g.includes("--help")){console.log(s);return}let[f,...$]=g;switch(f){case"stats":await Ig($);break;case"serve":n($);break;case void 0:n([]);break;default:L(`unknown command: ${f}`)}}await Pg();
34
+ `),console.error(s),process.exit(1)}async function Ig(g){let{values:f,positionals:$}=m({args:g,allowPositionals:!0,options:{by:{type:"string"},day:{type:"string"},user:{type:"string"},timezone:{type:"string"},json:{type:"boolean",default:!1},"include-no-charge":{type:"boolean",default:!1}}}),V=$[0];if(!V)L("stats requires a path to a CSV file");let J;try{J=await Wg(V,"utf8")}catch{L(`file not found: ${V}`)}let S=f.by;if(S&&!["day","user","model"].includes(S))L(`invalid --by value: ${S} (expected day, user or model)`);let Y=f.day;if(Y!==void 0&&!/^\d{4}-\d{2}-\d{2}$/.test(Y))L(`invalid --day value: ${Y} (expected YYYY-MM-DD)`);let A=f.timezone??w();if(!h(A))L(`invalid --timezone value: ${A}`);let j=Z(J);if(!f["include-no-charge"])j=E(j);let G=f.user;if(G)j=j.filter((X)=>X.user===G);if(j.length===0)console.error("No usage events found for the requested filters."),process.exit(1);if(Y){console.log(f.json?o(j,Y,A,G):d(j,Y,A,G));return}console.log(f.json?r(j,A,G):i(j,S,A,G))}function n(g){let{values:f}=m({args:g,allowPositionals:!1,options:{port:{type:"string"},"no-open":{type:"boolean",default:!1}}}),$;if(f.port!==void 0){if($=Number(f.port),!Number.isInteger($)||$<0||$>65535)L(`invalid --port value: ${f.port}`)}b({port:$,open:!f["no-open"]})}async function Pg(){let g=process.argv.slice(2);if(g.includes("-h")||g.includes("--help")){console.log(s);return}let[f,...$]=g;switch(f){case"stats":await Ig($);break;case"serve":n($);break;case void 0:n([]);break;default:L(`unknown command: ${f}`)}}await Pg();