@jnst/cursor-usage 0.3.0 → 0.4.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 +62 -13
- package/dist/cli.js +28 -14
- package/dist/web/{chunk-s9hpz9pe.css → chunk-s2te0gss.css} +1 -1
- package/dist/web/chunk-z22gajzk.js +285 -0
- package/dist/web/index.html +1 -1
- package/package.json +4 -1
- package/dist/web/chunk-4hj5mwma.js +0 -285
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npx @jnst/cursor-usage # or: bunx @jnst/cursor-usage
|
|
|
19
19
|
|
|
20
20
|
Starts a local server and opens your browser. Drag & drop a CSV exported from Cursor onto the page. All data is processed in the browser and never sent anywhere.
|
|
21
21
|
|
|
22
|
-
Click any bar in the
|
|
22
|
+
Click any bar in the Daily Window cost chart to drill into that window (hourly breakdown, per-model / per-user / per-kind costs, and every event in the window). Click a user bar to filter the current analysis to that User; the selected User remains visible while other users are dimmed, and clicking the selected user again clears the filter. The selected Daily Window, user, and analysis time zone are reflected in the URL hash (`#daily-window=YYYY-MM-DD&user=jnst%40example.jp&timezone=Asia%2FTokyo`), so the browser back button and shareable links work after loading the same CSV.
|
|
23
23
|
|
|
24
24
|
The default port is 4321; if it is already in use, a free port is picked automatically. When `--port` is specified explicitly, that port is used as-is.
|
|
25
25
|
|
|
@@ -34,13 +34,13 @@ npx @jnst/cursor-usage stats team-usage-events.csv
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
Cursor Usage 2026-06-01 – 2026-06-10 (610 events, 10
|
|
37
|
+
Cursor Usage 2026-06-01 – 2026-06-10 (610 events, 10 daily windows)
|
|
38
38
|
|
|
39
39
|
Total Cost $1446.69 Total Tokens 1.1B
|
|
40
40
|
Avg/Active $144.67 Max Mode 96%
|
|
41
41
|
Users 4 Models 8
|
|
42
42
|
|
|
43
|
-
Daily Cost
|
|
43
|
+
Daily Window Cost
|
|
44
44
|
2026-06-01 $147.44 ████████████████▊ 10% 102.9M tok, 68 ev
|
|
45
45
|
2026-06-02 $246.57 ████████████████████████████ 17% 180.0M tok, 79 ev
|
|
46
46
|
...
|
|
@@ -53,21 +53,22 @@ By Model
|
|
|
53
53
|
|
|
54
54
|
Options:
|
|
55
55
|
|
|
56
|
-
| Option
|
|
57
|
-
|
|
|
58
|
-
| `--by <
|
|
59
|
-
| `--
|
|
60
|
-
| `--
|
|
61
|
-
| `--
|
|
62
|
-
| `--
|
|
63
|
-
| `--
|
|
56
|
+
| Option | Description |
|
|
57
|
+
| ---------------------------------- | -------------------------------------------------------------- |
|
|
58
|
+
| `--by <daily-window\|user\|model>` | Show a single breakdown axis |
|
|
59
|
+
| `--daily-window <YYYY-MM-DD>` | Drill into a single Daily Window |
|
|
60
|
+
| `--start-hour <0-23>` | Daily Window start hour (default: `0`) |
|
|
61
|
+
| `--user <identifier>` | Filter analysis to a single User |
|
|
62
|
+
| `--timezone <iana-tz>` | Group Daily Windows and hours in a specific analysis time zone |
|
|
63
|
+
| `--json` | Output aggregated stats as JSON (pipe to jq etc.) |
|
|
64
|
+
| `--include-no-charge` | Include "Errored, No Charge" events |
|
|
64
65
|
|
|
65
66
|
```bash
|
|
66
67
|
# Extract key numbers
|
|
67
68
|
npx @jnst/cursor-usage stats usage.csv --json | jq .summary.totalCost
|
|
68
69
|
|
|
69
|
-
# Drill into
|
|
70
|
-
npx @jnst/cursor-usage stats usage.csv --
|
|
70
|
+
# Drill into a Daily Window
|
|
71
|
+
npx @jnst/cursor-usage stats usage.csv --daily-window 2026-06-02 --timezone Asia/Tokyo
|
|
71
72
|
|
|
72
73
|
# Filter to a single user
|
|
73
74
|
npx @jnst/cursor-usage stats usage.csv --user jnst@example.jp
|
|
@@ -80,6 +81,54 @@ npm install -g @jnst/cursor-usage # or: bun add -g @jnst/cursor-usage
|
|
|
80
81
|
cursor-usage stats usage.csv
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
### Screenshots
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @jnst/cursor-usage screenshot team-usage-events.csv
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Captures the dashboard as a PNG next to the CSV. The default screenshot is an
|
|
91
|
+
Overview:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
team-usage-events.csv -> team-usage-events.png
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Use `--daily-window` to capture the detail view for one Daily Window:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx @jnst/cursor-usage screenshot team-usage-events.csv --daily-window 2026-06-14
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
team-usage-events.csv --daily-window 2026-06-14 -> team-usage-events-2026-06-14-daily.png
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `--start-hour` when a Daily Window should start after midnight, and
|
|
108
|
+
`--event-limit` to limit the event table in the screenshot:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx @jnst/cursor-usage screenshot usage.csv --daily-window 2026-06-14 --start-hour 5 --event-limit 20
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For a shareable report of the latest work session in the CSV, use
|
|
115
|
+
`daily-report`. It captures the latest 5:00-start Daily Window, limits the
|
|
116
|
+
event table to the top 10 events by cost, and writes `daily-report.png` in the
|
|
117
|
+
current directory:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx @jnst/cursor-usage daily-report usage.csv
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Screenshots use a headless browser and require an installed Chrome/Chromium.
|
|
124
|
+
Set `CHROME_PATH` if Chrome is not available on the default channel. Screenshots
|
|
125
|
+
can also be filtered the same way as terminal stats:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx @jnst/cursor-usage screenshot usage.csv --daily-window 2026-06-14 --user jnst@example.jp --timezone Asia/Tokyo
|
|
129
|
+
npx @jnst/cursor-usage screenshot usage.csv --out dashboard.png
|
|
130
|
+
```
|
|
131
|
+
|
|
83
132
|
## Development
|
|
84
133
|
|
|
85
134
|
Development tooling uses [Bun](https://bun.sh) (runtime code itself is Node-compatible).
|
package/dist/cli.js
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFile as
|
|
3
|
-
`){A(),
|
|
2
|
+
import{readFile as vf}from"node:fs/promises";import{parseArgs as h}from"node:util";var l="Errored, No Charge";var Q="UTC";function _(){return Intl.DateTimeFormat().resolvedOptions().timeZone||Q}function E(f){try{return new Intl.DateTimeFormat("en-US",{timeZone:f}).format(new Date),!0}catch{return!1}}function R(f,g,$){return new Intl.DateTimeFormat("en-GB",{timeZone:g,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",hourCycle:"h23"}).formatToParts(f).find((V)=>V.type===$)?.value??""}function H(f){return Number.isInteger(f)&&f>=0&&f<=23}function y(f){if(!H(f))throw Error(`Invalid Daily Window start hour: ${f}`)}function Jf(f,g){return[R(f,g,"year"),R(f,g,"month"),R(f,g,"day")].join("-")}function Sf(f){let[g,$,Y]=f.split("-").map(Number);if(g===void 0||$===void 0||Y===void 0)throw Error(`Invalid Daily Window Key: ${f}`);return{year:g,month:$,date:Y}}function qf(f){return f.toISOString().slice(0,10)}function Gf(f,g){let{year:$,month:Y,date:V}=Sf(f);return qf(new Date(Date.UTC($,Y-1,V)+g*86400000))}function C(f,g=Q,$=0){y($);let Y=Jf(f,g);return Number(R(f,g,"hour"))<$?Gf(Y,-1):Y}function Lf(f,g=Q){return R(f,g,"hour")}function n(f=0){return y(f),Array.from({length:24},(g,$)=>String((f+$)%24).padStart(2,"0"))}function K(f,g,$=Q,Y=0){return f.filter((V)=>C(V.date,$,Y)===g)}function m(f,g=Q,$=0){let Y=[...f].sort((V,z)=>z.date.getTime()-V.date.getTime())[0];return Y?C(Y.date,g,$):null}function i(f){return f.filter((g)=>g.kind!==l)}function N(f,g=Q,$=0){let Y=0,V=0,z=0,c=new Set,A=new Set,J=new Set;for(let q of f){if(Y+=q.cost,V+=q.totalTokens,q.maxMode)z++;c.add(C(q.date,g,$)),A.add(q.user),J.add(q.model)}let S=[...c].sort();return{totalCost:Y,totalTokens:V,eventCount:f.length,firstDailyWindow:S[0]??null,lastDailyWindow:S[S.length-1]??null,dailyWindowCount:c.size,avgCostPerActiveDailyWindow:c.size>0?Y/c.size:0,maxModeRatio:f.length>0?z/f.length:0,userCount:A.size,modelCount:J.size}}function D(f,g){let $=new Map;for(let Y of f){let V=g(Y),z=$.get(V);if(!z)z={key:V,cost:0,totalTokens:0,inputTokens:0,outputTokens:0,cacheRead:0,eventCount:0},$.set(V,z);z.cost+=Y.cost,z.totalTokens+=Y.totalTokens,z.inputTokens+=Y.inputWithCacheWrite+Y.inputWithoutCacheWrite,z.outputTokens+=Y.outputTokens,z.cacheRead+=Y.cacheRead,z.eventCount++}return[...$.values()]}function k(f,g=Q,$=0){return D(f,(Y)=>C(Y.date,g,$)).sort((Y,V)=>Y.key.localeCompare(V.key))}function O(f){return D(f,(g)=>g.user).sort((g,$)=>$.cost-g.cost)}function U(f){return D(f,(g)=>g.model).sort((g,$)=>$.cost-g.cost)}function W(f){return D(f,(g)=>g.kind).sort((g,$)=>$.cost-g.cost)}function w(f,g=Q){return D(f,($)=>Lf($.date,g)).sort(($,Y)=>$.key.localeCompare(Y.key))}function d(f,g){return[...f].sort(($,Y)=>Y.cost-$.cost).slice(0,g)}function Xf(f){let g=[],$=[],Y="",V=!1,z=0,c=()=>{$.push(Y),Y=""},A=()=>{c(),g.push($),$=[]};while(z<f.length){let J=f[z];if(V){if(J==='"'){if(f[z+1]==='"'){Y+='"',z+=2;continue}V=!1,z++;continue}Y+=J,z++;continue}if(J==='"'){V=!0,z++;continue}if(J===","){c(),z++;continue}if(J==="\r"){z++;continue}if(J===`
|
|
3
|
+
`){A(),z++;continue}Y+=J,z++}if(Y.length>0||$.length>0)A();return g}var Ff=["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 P(f){if(!f)return 0;let g=Number(f);return Number.isFinite(g)?g:0}function r(f){let g=Xf(f.trim());if(g.length===0)return[];let $=g[0],Y=new Map;$.forEach((c,A)=>Y.set(c.trim(),A));for(let c of["Date","User","Model","Cost"])if(!Y.has(c))throw Error(`Invalid CSV: missing column "${c}". Expected a Cursor usage-events export with columns: ${Ff.join(", ")}`);let V=(c,A)=>{let J=Y.get(A);return J===void 0?"":(c[J]??"").trim()},z=[];for(let c of g.slice(1)){if(c.length===1&&c[0]==="")continue;let A=V(c,"Date"),J=new Date(A);if(Number.isNaN(J.getTime()))continue;z.push({date:J,user:V(c,"User"),cloudAgentId:V(c,"Cloud Agent ID")||null,automationId:V(c,"Automation ID")||null,kind:V(c,"Kind"),model:V(c,"Model"),maxMode:V(c,"Max Mode").toLowerCase()==="yes",inputWithCacheWrite:P(V(c,"Input (w/ Cache Write)")),inputWithoutCacheWrite:P(V(c,"Input (w/o Cache Write)")),cacheRead:P(V(c,"Cache Read")),outputTokens:P(V(c,"Output Tokens")),totalTokens:P(V(c,"Total Tokens")),cost:P(V(c,"Cost"))})}return z}import{spawn as Qf}from"node:child_process";import{createReadStream as jf,existsSync as Nf}from"node:fs";import{stat as Pf}from"node:fs/promises";import{createServer as If}from"node:http";import{dirname as Bf,extname as Mf,join as T,normalize as Rf}from"node:path";import{fileURLToPath as Kf}from"node:url";var a=4321,Df={".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 Of(){let f=Bf(Kf(import.meta.url)),g=[T(f,"web"),T(f,"../../dist/web")];for(let $ of g)if(Nf(T($,"index.html")))return $;throw Error("Dashboard assets not found. Run `bun run build` first (expected dist/web/index.html).")}function Uf(f){let[g,...$]=process.platform==="darwin"?["open",f]:process.platform==="win32"?["cmd","/c","start","",f]:["xdg-open",f];try{Qf(g,$,{stdio:"ignore",detached:!0}).unref()}catch{}}function xf(f){return If(async(g,$)=>{let Y=decodeURIComponent(new URL(g.url??"/","http://localhost").pathname),V=Rf(T(f,Y==="/"?"/index.html":Y));if(!V.startsWith(f)){$.writeHead(403),$.end("Forbidden");return}try{let z=await Pf(V);if(!z.isFile())throw Error("not a file");$.writeHead(200,{"Content-Type":Df[Mf(V)]??"application/octet-stream","Content-Length":z.size}),jf(V).pipe($)}catch{$.writeHead(404),$.end("Not Found")}})}function Z(f={}){let g=Of(),$=xf(g);return new Promise((Y,V)=>{let z=()=>{let{port:c}=$.address();Y({server:$,url:`http://localhost:${c}`})};if(f.port!==void 0){$.once("error",V),$.listen(f.port,z);return}$.once("error",(c)=>{if(c.code!=="EADDRINUSE"){V(c);return}console.log(`port ${a} is in use, picked a free port instead`),$.listen(0)}),$.once("listening",z),$.listen(a)})}function s(f){Z({port:f.port}).then(({url:g})=>{if(console.log(`cursor-usage dashboard running at ${g}`),console.log("Drop a Cursor usage-events CSV onto the page. Ctrl+C to stop."),f.open)Uf(g)}).catch((g)=>{throw g})}var _f=process.stdout.isTTY&&!process.env.NO_COLOR,x=(f)=>(g)=>_f?`\x1B[${f}m${g}\x1B[0m`:g,j=x("1"),F=x("2"),o=x("36"),Vg=x("32"),zg=x("33");function B(f){return`$${f.toFixed(2)}`}function p(f){if(f>=1e9)return`${(f/1e9).toFixed(1)}B`;if(f>=1e6)return`${(f/1e6).toFixed(1)}M`;if(f>=1000)return`${(f/1000).toFixed(1)}K`;return String(f)}function t(f,g,$){if(g<=0||f<=0)return"";let Y=Math.round(f/g*$*8),V=Math.floor(Y/8),z=Y%8,c=["","▏","▎","▍","▌","▋","▊","▉"];return"█".repeat(V)+(c[z]??"")}function X(f,g){return f.length>=g?f:f+" ".repeat(g-f.length)}function b(f,g,$){return new Intl.DateTimeFormat("en-GB",{timeZone:g,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(f).find((V)=>V.type===$)?.value??""}function Cf(f,g){return[b(f,g,"hour"),b(f,g,"minute"),b(f,g,"second")].join(":")}function kf(f,g,$,Y){let V=f.firstDailyWindow&&f.lastDailyWindow?`${f.firstDailyWindow} – ${f.lastDailyWindow}`:"no data",z=$?`${g}, start ${Y}:00, user ${$}`:`${g}, start ${Y}:00`,c=(J)=>F(X(J,14)),A=(J)=>j(X(J,12));return[`${j("Cursor Usage")} ${V} ${F(`(${f.eventCount} events, ${f.dailyWindowCount} daily windows, ${z})`)}`,"",` ${c("Total Cost")}${A(B(f.totalCost))} ${c("Total Tokens")}${A(p(f.totalTokens))}`,` ${c("Avg/Active")}${A(B(f.avgCostPerActiveDailyWindow))} ${c("Max Mode")}${A(`${Math.round(f.maxModeRatio*100)}%`)}`,` ${c("Users")}${A(String(f.userCount))} ${c("Models")}${A(String(f.modelCount))}`]}function I(f,g,$={totalCost:0}){let{totalCost:Y,barWidth:V=28,maxRows:z=15}=$,c=g.slice(0,z),A=Math.max(...c.map((q)=>q.key.length),4),J=Math.max(...c.map((q)=>q.cost),0),S=[j(f)];for(let q of c){let G=Y>0?` ${F(`${Math.round(q.cost/Y*100)}%`)}`:"";S.push(` ${X(q.key,A)} ${X(B(q.cost),8)} ${o(X(t(q.cost,J,V),V))}${G} ${F(`${p(q.totalTokens)} tok, ${q.eventCount} ev`)}`)}if(g.length>z)S.push(F(` … and ${g.length-z} more`));return S}function e(f,g,$,Y,V=0){let z=N(f,$,V),c=[kf(z,$,Y,V)],A={"daily-window":()=>I("Daily Window Cost",k(f,$,V),{totalCost:z.totalCost,maxRows:31}),model:()=>I("By Model",U(f),{totalCost:z.totalCost}),user:()=>I("By User",O(f),{totalCost:z.totalCost})};if(g)c.push(A[g]());else c.push(A["daily-window"](),A.model(),A.user());return c.map((J)=>J.join(`
|
|
4
4
|
`)).join(`
|
|
5
5
|
|
|
6
6
|
`)+`
|
|
7
|
-
`}function
|
|
8
|
-
Available
|
|
9
|
-
`}let
|
|
7
|
+
`}function ff(f,g,$,Y=0){return JSON.stringify({timeZone:g,startHour:Y,filters:{user:$??null},summary:N(f,g,Y),byDailyWindow:k(f,g,Y),byModel:U(f),byUser:O(f)},null,2)}function Tf(f,g,$,Y,V,z,c){let A=N(g,$,Y),J=V>0?Math.round(A.totalCost/V*100):0,S=(G)=>F(X(G,14)),q=(G)=>j(X(G,12));return[`${j(`Daily Window ${f}`)} ${F(`(${A.eventCount} events, rank ${z}/${c} by cost, ${$}, start ${Y}:00)`)}`,"",` ${S("Cost")}${q(B(A.totalCost))} ${S("of period")}${q(`${J}%`)}`,` ${S("Total Tokens")}${q(p(A.totalTokens))} ${S("Max Mode")}${q(`${Math.round(A.maxModeRatio*100)}%`)}`,` ${S("Users")}${q(String(A.userCount))} ${S("Models")}${q(String(A.modelCount))}`]}function pf(f,g,$){let Y=new Map(w(f,g).map((c)=>[c.key,c])),V=Math.max(...[...Y.values()].map((c)=>c.cost),0),z=[j(`By Hour (${g})`)];for(let c of n($)){let A=Y.get(c),J=A?.cost??0,S=A?.eventCount??0,q=S>0?F(` ${S} ev`):"";z.push(` ${c} ${X(J>0?B(J):"",8)} ${o(X(t(J,V,24),24))}${q}`)}return z}function hf(f,g,$){let Y=d(f,g),V=[j(`Top Events (${Y.length} of ${f.length})`)],z=Math.max(...Y.map((A)=>A.user.length),4),c=Math.max(...Y.map((A)=>A.model.length),5);for(let A of Y){let J=Cf(A.date,$);V.push(` ${F(J)} ${X(A.user,z)} ${X(A.model,c)} ${X(B(A.cost),8)} ${F(`${p(A.totalTokens)} tok`)}`)}return V}function gf(f,g,$,Y,V=0){let z=k(f,$,V),c=K(f,g,$,V);if(c.length===0){let G=z.map((Af)=>Af.key),M=G.length>0?`
|
|
8
|
+
Available Daily Windows: ${G[0]} – ${G[G.length-1]}`:"";return`No billable events in Daily Window ${g}.${M}
|
|
9
|
+
`}let A=f.reduce((G,M)=>G+M.cost,0),J=[...z].sort((G,M)=>M.cost-G.cost).findIndex((G)=>G.key===g)+1,S=N(c,$,V).totalCost;return[Tf(g,c,$,V,A,J,z.length),...Y?[[F(`Filtered to user: ${Y}`)]]:[],pf(c,$,V),I("By Model",U(c),{totalCost:S}),I("By User",O(c),{totalCost:S}),I("By Kind",W(c),{totalCost:S}),hf(c,20,$)].map((G)=>G.join(`
|
|
10
10
|
`)).join(`
|
|
11
11
|
|
|
12
12
|
`)+`
|
|
13
|
-
`}function
|
|
13
|
+
`}function $f(f,g,$,Y,V=0){let z=K(f,g,$,V);return JSON.stringify({dailyWindow:g,timeZone:$,startHour:V,filters:{user:Y??null},summary:N(z,$,V),byHour:w(z,$),byModel:U(z),byUser:O(z),byKind:W(z)},null,2)}import{basename as Ef,dirname as Hf,extname as Wf,join as Yf}from"node:path";function wf(f){let{csvPath:g,dailyWindow:$,dailyReport:Y,out:V}=f;if(V)return V;if(Y)return Yf(process.cwd(),"daily-report.png");let z=Wf(g),c=Ef(g,z);return Yf(Hf(g),`${c}${$?`-${$}-daily`:""}.png`)}async function Zf(){let f=Function("specifier","return import(specifier)");try{let g=await f("playwright-core");if(!g.chromium)throw Error("playwright-core did not expose chromium");return g.chromium}catch(g){throw Error(`Screenshot export requires playwright-core. Install dependencies and try again.
|
|
14
|
+
${String(g)}`)}}function bf(f){return f.map((g)=>({...g,date:g.date.toISOString()}))}async function v(f){if(f.events.length===0)throw Error("No billable usage events found in the Usage Export.");if(f.dailyWindow){if(K(f.events,f.dailyWindow,f.timeZone,f.startHour).length===0)throw Error(`No billable usage events found in Daily Window ${f.dailyWindow} (start hour ${f.startHour}, ${f.timeZone}).`)}let g=await Zf(),$=process.env.CHROME_PATH!==void 0?{headless:!0,executablePath:process.env.CHROME_PATH}:{headless:!0,channel:process.env.PLAYWRIGHT_CHROME_CHANNEL??"chrome"},Y;try{Y=await g.launch($)}catch(c){throw Error(["Could not launch Chrome for screenshot export.","Install Google Chrome, set CHROME_PATH, or set PLAYWRIGHT_CHROME_CHANNEL to an installed Chromium channel.",String(c)].join(`
|
|
15
|
+
`))}let V=wf(f),z=await Z({port:0});try{let c=await Y.newPage({viewport:{width:1200,height:900}});await c.addInitScript({content:`window.__CURSOR_USAGE_EVENTS__ = ${JSON.stringify(bf(f.events))};`});let A=new URL(z.url);A.hash=new URLSearchParams({timezone:f.timeZone,...f.user?{user:f.user}:{},...f.dailyWindow?{"daily-window":f.dailyWindow}:{},...f.startHour!==0?{"start-hour":String(f.startHour)}:{},...f.eventLimit!==void 0?{"event-limit":String(f.eventLimit)}:{}}).toString(),await c.goto(A.href,{waitUntil:"networkidle"}),await c.waitForSelector(".grid",{timeout:1e4}),await c.screenshot({path:V,fullPage:!0})}finally{z.server.close(),await Y.close()}return V}var Vf=`cursor-usage — visualize Cursor usage-events CSV
|
|
14
16
|
|
|
15
17
|
Usage:
|
|
16
18
|
cursor-usage [serve] [--port <n>] [--no-open] Start the drag & drop dashboard (default)
|
|
17
19
|
cursor-usage stats <csv> [options] Show usage statistics in the terminal
|
|
20
|
+
cursor-usage screenshot <csv> [options] Capture the dashboard as a PNG
|
|
21
|
+
cursor-usage daily-report <csv> Capture a shareable daily report PNG
|
|
18
22
|
|
|
19
23
|
Stats options:
|
|
20
|
-
--by <
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
24
|
+
--by <daily-window|user|model> Show a single breakdown axis (default: all)
|
|
25
|
+
--daily-window <YYYY-MM-DD> Drill into a single Daily Window
|
|
26
|
+
--start-hour <0-23> Daily Window start hour (default: 0)
|
|
27
|
+
--user <identifier> Filter analysis to a single User
|
|
28
|
+
--timezone <iana-tz> Analysis time zone (default: current environment)
|
|
29
|
+
--json Output aggregated stats as JSON
|
|
30
|
+
--include-no-charge Include "Errored, No Charge" events
|
|
31
|
+
|
|
32
|
+
Screenshot options:
|
|
33
|
+
--daily-window <YYYY-MM-DD> Capture a Daily Window detail view
|
|
34
|
+
--start-hour <0-23> Daily Window start hour (default: 0)
|
|
35
|
+
--event-limit <n> Limit Daily Window event table rows
|
|
36
|
+
--out <path> Output path
|
|
37
|
+
--user <identifier> Filter screenshot to a single User
|
|
38
|
+
--timezone <iana-tz> Analysis time zone (default: current environment)
|
|
39
|
+
--include-no-charge Include "Errored, No Charge" events
|
|
26
40
|
|
|
27
41
|
Serve options:
|
|
28
42
|
--port <n> Fixed port to listen on
|
|
@@ -30,5 +44,5 @@ Serve options:
|
|
|
30
44
|
--no-open Do not open the browser automatically
|
|
31
45
|
|
|
32
46
|
-h, --help Show this help
|
|
33
|
-
`;function L(
|
|
34
|
-
`),console.error(
|
|
47
|
+
`;function L(f){console.error(`Error: ${f}
|
|
48
|
+
`),console.error(Vf),process.exit(1)}function zf(f,g){if(f===void 0)return g;let $=Number(f);if(!H($))L(`invalid --start-hour value: ${f} (expected 0-23)`);return $}function uf(f){if(f===void 0)return;let g=Number(f);if(!Number.isInteger(g)||g<=0)L(`invalid --event-limit value: ${f} (expected a positive integer)`);return g}async function u(f,g,$,Y="No usage events found for the requested filters."){let V;try{V=await vf(f,"utf8")}catch{L(`file not found: ${f}`)}let z=r(V);if(!g)z=i(z);if($)z=z.filter((c)=>c.user===$);if(z.length===0)L(Y);return z}async function lf(f){let{values:g,positionals:$}=h({args:f,allowPositionals:!0,options:{by:{type:"string"},"daily-window":{type:"string"},"start-hour":{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 V=g.by;if(V&&!["daily-window","user","model"].includes(V))L(`invalid --by value: ${V} (expected daily-window, user or model)`);let z=g["daily-window"];if(z!==void 0&&!/^\d{4}-\d{2}-\d{2}$/.test(z))L(`invalid --daily-window value: ${z} (expected YYYY-MM-DD)`);let c=g.timezone??_();if(!E(c))L(`invalid --timezone value: ${c}`);let A=zf(g["start-hour"],0),J=g.user,S=await u(Y,g["include-no-charge"],J);if(z){console.log(g.json?$f(S,z,c,J,A):gf(S,z,c,J,A));return}console.log(g.json?ff(S,c,J,A):e(S,V,c,J,A))}async function yf(f){let{values:g,positionals:$}=h({args:f,allowPositionals:!0,options:{"daily-window":{type:"string"},"start-hour":{type:"string"},"event-limit":{type:"string"},out:{type:"string"},user:{type:"string"},timezone:{type:"string"},"include-no-charge":{type:"boolean",default:!1}}}),Y=$[0];if(!Y)L("screenshot requires a path to a CSV file");let V=g.timezone??_();if(!E(V))L(`invalid --timezone value: ${V}`);let z=g["daily-window"];if(z!==void 0&&!/^\d{4}-\d{2}-\d{2}$/.test(z))L(`invalid --daily-window value: ${z} (expected YYYY-MM-DD)`);let c=zf(g["start-hour"],0),A=uf(g["event-limit"]),J=await u(Y,g["include-no-charge"],g.user),S=await v({csvPath:Y,events:J,timeZone:V,dailyWindow:z,startHour:c,eventLimit:A,dailyReport:!1,out:g.out,user:g.user});console.log(`wrote ${S}`)}async function nf(f){let{positionals:g}=h({args:f,allowPositionals:!0,options:{}}),$=g[0];if(!$)L("daily-report requires a path to a CSV file");if(g.length>1)L(`unexpected argument: ${g[1]}`);let Y=_(),V=5,z=await u($,!1,void 0,"No billable usage events found in the Usage Export."),c=m(z,Y,V);if(!c)L("No billable usage events found in the Usage Export.");let A=await v({csvPath:$,events:z,timeZone:Y,dailyWindow:c,startHour:V,eventLimit:10,dailyReport:!0});console.log(`wrote ${A}`)}function cf(f){let{values:g}=h({args:f,allowPositionals:!1,options:{port:{type:"string"},"no-open":{type:"boolean",default:!1}}}),$;if(g.port!==void 0){if($=Number(g.port),!Number.isInteger($)||$<0||$>65535)L(`invalid --port value: ${g.port}`)}s({port:$,open:!g["no-open"]})}async function mf(){let f=process.argv.slice(2);if(f.includes("-h")||f.includes("--help")){console.log(Vf);return}let[g,...$]=f;switch(g){case"stats":await lf($);break;case"screenshot":await yf($);break;case"daily-report":await nf($);break;case"serve":cf($);break;case void 0:cf([]);break;default:L(`unknown command: ${g}`)}}await mf();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg:#0d1117;--panel:#161b22;--panel-border:#21262d;--text:#e6edf3;--text-dim:#8b949e;--accent:#58a6ff;--accent-soft:#58a6ff1f;--green:#3fb950}*{box-sizing:border-box}html,body,#root{min-height:100vh;margin:0}body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Hiragino Sans,Noto Sans JP,sans-serif;font-size:14px;line-height:1.5}.app{max-width:1180px;margin:0 auto;padding:24px 20px 64px}.header{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:16px;margin-bottom:20px}.header h1{letter-spacing:.02em;margin:0;font-size:20px}.header h1.clickable-title{cursor:pointer}.header h1.clickable-title:hover{color:var(--accent)}.header .meta{color:var(--text-dim);font-size:13px}.reload-button{background:var(--panel);border:1px solid var(--panel-border);color:var(--text);cursor:pointer;border-radius:8px;padding:6px 14px;font-size:13px}.reload-button:hover{border-color:var(--accent)}.dropzone{display:flex;border:2px dashed var(--panel-border);cursor:pointer;text-align:center;border-radius:16px;flex-direction:column;justify-content:center;align-items: center;gap:12px;min-height:70vh;padding:40px;transition:border-color .15s,background .15s}.dropzone:hover,.dropzone.dragover{border-color:var(--accent);background:var(--accent-soft)}.dropzone .icon{font-size:44px}.dropzone h2{margin:0;font-size:18px;font-weight:600}.dropzone p{color:var(--text-dim);max-width:460px;margin:0}.dropzone .error{color:#f85149}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:16px}.card{background:var(--panel);border:1px solid var(--panel-border);border-radius:12px;padding:14px 16px}.card .label{color:var(--text-dim);margin-bottom:4px;font-size:12px}.card .value{font-variant-numeric:tabular-nums;font-size:22px;font-weight:700}.card .sub{color:var(--text-dim);margin-top:2px;font-size:12px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.grid .wide{grid-column:1/-1}@media (max-width:860px){.grid{grid-template-columns:1fr}}.panel{background:var(--panel);border:1px solid var(--panel-border);border-radius:12px;padding:16px}.panel h3{color:var(--text);margin:0 0 12px;font-size:14px;font-weight:600}.panel h3 .hint{color:var(--text-dim);margin-left:8px;font-size:12px;font-weight:400}.
|
|
1
|
+
:root{--bg:#0d1117;--panel:#161b22;--panel-border:#21262d;--text:#e6edf3;--text-dim:#8b949e;--accent:#58a6ff;--accent-soft:#58a6ff1f;--green:#3fb950}*{box-sizing:border-box}html,body,#root{min-height:100vh;margin:0}body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Hiragino Sans,Noto Sans JP,sans-serif;font-size:14px;line-height:1.5}.app{max-width:1180px;margin:0 auto;padding:24px 20px 64px}.header{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:16px;margin-bottom:20px}.header h1{letter-spacing:.02em;margin:0;font-size:20px}.header h1.clickable-title{cursor:pointer}.header h1.clickable-title:hover{color:var(--accent)}.header .meta{color:var(--text-dim);font-size:13px}.reload-button{background:var(--panel);border:1px solid var(--panel-border);color:var(--text);cursor:pointer;border-radius:8px;padding:6px 14px;font-size:13px}.reload-button:hover{border-color:var(--accent)}.dropzone{display:flex;border:2px dashed var(--panel-border);cursor:pointer;text-align:center;border-radius:16px;flex-direction:column;justify-content:center;align-items: center;gap:12px;min-height:70vh;padding:40px;transition:border-color .15s,background .15s}.dropzone:hover,.dropzone.dragover{border-color:var(--accent);background:var(--accent-soft)}.dropzone .icon{font-size:44px}.dropzone h2{margin:0;font-size:18px;font-weight:600}.dropzone p{color:var(--text-dim);max-width:460px;margin:0}.dropzone .error{color:#f85149}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:16px}.card{background:var(--panel);border:1px solid var(--panel-border);border-radius:12px;padding:14px 16px}.card .label{color:var(--text-dim);margin-bottom:4px;font-size:12px}.card .value{font-variant-numeric:tabular-nums;font-size:22px;font-weight:700}.card .sub{color:var(--text-dim);margin-top:2px;font-size:12px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.grid .wide{grid-column:1/-1}@media (max-width:860px){.grid{grid-template-columns:1fr}}.panel{background:var(--panel);border:1px solid var(--panel-border);border-radius:12px;padding:16px}.panel h3{color:var(--text);margin:0 0 12px;font-size:14px;font-weight:600}.panel h3 .hint{color:var(--text-dim);margin-left:8px;font-size:12px;font-weight:400}.daily-window-nav{display:flex;flex-wrap:wrap;justify-content:space-between;align-items: center;gap:16px;margin-bottom:16px}.daily-window-title{display:flex;flex:1;justify-content:center;align-items:baseline;gap:12px}.daily-window-title h2{font-variant-numeric:tabular-nums;margin:0;font-size:18px}.daily-window-stepper{display:flex;gap:8px}.reload-button:disabled{opacity:.4;cursor:not-allowed}.reload-button:disabled:hover{border-color:var(--panel-border)}.cost-bar{display:inline-block;background:var(--accent-soft);vertical-align:middle;border-radius:4px;height:8px;margin-right:6px}.table-wrap.scroll{overflow-y:auto;max-height:480px}.table-wrap.scroll thead th{position:sticky;background:var(--panel);z-index:1;top:0}.table-wrap{overflow-x:auto}table{border-collapse:collapse;font-variant-numeric:tabular-nums;width:100%;font-size:13px}th,td{text-align:left;border-bottom:1px solid var(--panel-border);white-space:nowrap;padding:7px 10px}th{color:var(--text-dim);font-size:12px;font-weight:500}td.num,th.num{text-align:right}tr:last-child td{border-bottom:none}.badge{display:inline-block;background:var(--accent-soft);color:var(--accent);border-radius:999px;padding:1px 8px;font-size:11px}
|