@jnst/cursor-usage 0.3.0 → 0.5.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 +69 -25
- package/dist/cli.js +28 -14
- package/dist/web/chunk-4pkntcpj.js +285 -0
- package/dist/web/{chunk-s9hpz9pe.css → chunk-s2te0gss.css} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +5 -4
- 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).
|
|
@@ -97,32 +146,27 @@ bun scripts/generate-dummy-csv.ts > dummy-usage.csv
|
|
|
97
146
|
|
|
98
147
|
### Release
|
|
99
148
|
|
|
100
|
-
|
|
101
|
-
|
|
149
|
+
The release command verifies, versions, publishes, pushes commits/tags, and creates a
|
|
150
|
+
GitHub Release with generated notes:
|
|
102
151
|
|
|
103
152
|
```bash
|
|
104
|
-
bun run release
|
|
105
|
-
bun run release:minor
|
|
106
|
-
bun run release:major
|
|
153
|
+
bun run release
|
|
107
154
|
```
|
|
108
155
|
|
|
109
|
-
|
|
110
|
-
Use `--publish-release` to publish the GitHub Release immediately, or
|
|
111
|
-
`--dry-run` to print mutating steps without running them:
|
|
156
|
+
Use `--dry-run` to print mutating steps without running them:
|
|
112
157
|
|
|
113
158
|
```bash
|
|
114
|
-
bun run release
|
|
115
|
-
bun run release:patch --publish-release
|
|
159
|
+
bun run release --dry-run
|
|
116
160
|
```
|
|
117
161
|
|
|
118
|
-
|
|
162
|
+
The release command is safe to rerun after a partial failure. The script checks
|
|
119
163
|
the current tag, npm package version, and GitHub Release before each publishing
|
|
120
164
|
step:
|
|
121
165
|
|
|
122
166
|
```bash
|
|
123
167
|
# If npm publish, git push, or GitHub Release creation failed midway,
|
|
124
168
|
# fix the problem and run the same command again.
|
|
125
|
-
bun run release
|
|
169
|
+
bun run release
|
|
126
170
|
```
|
|
127
171
|
|
|
128
172
|
## Architecture
|
package/dist/cli.js
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFile as
|
|
3
|
-
`){
|
|
2
|
+
import{readFile as yf}from"node:fs/promises";import{parseArgs as p}from"node:util";var u="Errored, No Charge";var F="UTC";function x(){return Intl.DateTimeFormat().resolvedOptions().timeZone||F}function h(f){try{return new Intl.DateTimeFormat("en-US",{timeZone:f}).format(new Date),!0}catch{return!1}}var l=new Map;function Af(f){let g=l.get(f);if(g)return g;let $=new Intl.DateTimeFormat("en-GB",{timeZone:f,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",hourCycle:"h23"});return l.set(f,$),$}function y(f,g){return new Map(Af(g).formatToParts(f).map(($)=>[$.type,$.value]))}function qf(f,g,$){return y(f,g).get($)??""}function E(f){return Number.isInteger(f)&&f>=0&&f<=23}function n(f){if(!E(f))throw Error(`Invalid Daily Window start hour: ${f}`)}function Gf(f,g){let $=y(f,g);return{dateKey:[$.get("year"),$.get("month"),$.get("day")].join("-"),hour:Number($.get("hour")??0)}}function Lf(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 Xf(f){return f.toISOString().slice(0,10)}function Qf(f,g){let{year:$,month:Y,date:z}=Lf(f);return Xf(new Date(Date.UTC($,Y-1,z)+g*86400000))}function _(f,g=F,$=0){n($);let{dateKey:Y,hour:z}=Gf(f,g);return z<$?Qf(Y,-1):Y}function Ff(f,g=F){return qf(f,g,"hour")}function i(f=0){return n(f),Array.from({length:24},(g,$)=>String((f+$)%24).padStart(2,"0"))}function R(f,g,$=F,Y=0){return f.filter((z)=>_(z.date,$,Y)===g)}function m(f,g=F,$=0){let Y=[...f].sort((z,c)=>c.date.getTime()-z.date.getTime())[0];return Y?_(Y.date,g,$):null}function d(f){return f.filter((g)=>g.kind!==u)}function N(f,g=F,$=0){let Y=0,z=0,c=0,V=new Set,J=new Set,S=new Set;for(let q of f){if(Y+=q.cost,z+=q.totalTokens,q.maxMode)c++;V.add(_(q.date,g,$)),J.add(q.user),S.add(q.model)}let A=[...V].sort();return{totalCost:Y,totalTokens:z,eventCount:f.length,firstDailyWindow:A[0]??null,lastDailyWindow:A[A.length-1]??null,dailyWindowCount:V.size,avgCostPerActiveDailyWindow:V.size>0?Y/V.size:0,maxModeRatio:f.length>0?c/f.length:0,userCount:J.size,modelCount:S.size}}function K(f,g){let $=new Map;for(let Y of f){let z=g(Y),c=$.get(z);if(!c)c={key:z,cost:0,totalTokens:0,inputTokens:0,outputTokens:0,cacheRead:0,eventCount:0},$.set(z,c);c.cost+=Y.cost,c.totalTokens+=Y.totalTokens,c.inputTokens+=Y.inputWithCacheWrite+Y.inputWithoutCacheWrite,c.outputTokens+=Y.outputTokens,c.cacheRead+=Y.cacheRead,c.eventCount++}return[...$.values()]}function C(f,g=F,$=0){return K(f,(Y)=>_(Y.date,g,$)).sort((Y,z)=>Y.key.localeCompare(z.key))}function D(f){return K(f,(g)=>g.user).sort((g,$)=>$.cost-g.cost)}function O(f){return K(f,(g)=>g.model).sort((g,$)=>$.cost-g.cost)}function H(f){return K(f,(g)=>g.kind).sort((g,$)=>$.cost-g.cost)}function W(f,g=F){return K(f,($)=>Ff($.date,g)).sort(($,Y)=>$.key.localeCompare(Y.key))}function r(f,g){return[...f].sort(($,Y)=>Y.cost-$.cost).slice(0,g)}function jf(f){let g=[],$=[],Y="",z=!1,c=0,V=()=>{$.push(Y),Y=""},J=()=>{V(),g.push($),$=[]};while(c<f.length){let S=f[c];if(z){if(S==='"'){if(f[c+1]==='"'){Y+='"',c+=2;continue}z=!1,c++;continue}Y+=S,c++;continue}if(S==='"'){z=!0,c++;continue}if(S===","){V(),c++;continue}if(S==="\r"){c++;continue}if(S===`
|
|
3
|
+
`){J(),c++;continue}Y+=S,c++}if(Y.length>0||$.length>0)J();return g}var Nf=["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 a(f){let g=jf(f.trim());if(g.length===0)return[];let $=g[0],Y=new Map;$.forEach((V,J)=>Y.set(V.trim(),J));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: ${Nf.join(", ")}`);let z=(V,J)=>{let S=Y.get(J);return S===void 0?"":(V[S]??"").trim()},c=[];for(let V of g.slice(1)){if(V.length===1&&V[0]==="")continue;let J=z(V,"Date"),S=new Date(J);if(Number.isNaN(S.getTime()))continue;c.push({date:S,user:z(V,"User"),cloudAgentId:z(V,"Cloud Agent ID")||null,automationId:z(V,"Automation ID")||null,kind:z(V,"Kind"),model:z(V,"Model"),maxMode:z(V,"Max Mode").toLowerCase()==="yes",inputWithCacheWrite:P(z(V,"Input (w/ Cache Write)")),inputWithoutCacheWrite:P(z(V,"Input (w/o Cache Write)")),cacheRead:P(z(V,"Cache Read")),outputTokens:P(z(V,"Output Tokens")),totalTokens:P(z(V,"Total Tokens")),cost:P(z(V,"Cost"))})}return c}import{spawn as Pf}from"node:child_process";import{createReadStream as If,existsSync as Bf}from"node:fs";import{stat as Mf}from"node:fs/promises";import{createServer as Rf}from"node:http";import{dirname as Kf,extname as Df,join as k,normalize as Of}from"node:path";import{fileURLToPath as Uf}from"node:url";var s=4321,xf={".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 _f(){let f=Kf(Uf(import.meta.url)),g=[k(f,"web"),k(f,"../../dist/web")];for(let $ of g)if(Bf(k($,"index.html")))return $;throw Error("Dashboard assets not found. Run `bun run build` first (expected dist/web/index.html).")}function Cf(f){let[g,...$]=process.platform==="darwin"?["open",f]:process.platform==="win32"?["cmd","/c","start","",f]:["xdg-open",f];try{Pf(g,$,{stdio:"ignore",detached:!0}).unref()}catch{}}function kf(f){return Rf(async(g,$)=>{let Y=decodeURIComponent(new URL(g.url??"/","http://localhost").pathname),z=Of(k(f,Y==="/"?"/index.html":Y));if(!z.startsWith(f)){$.writeHead(403),$.end("Forbidden");return}try{let c=await Mf(z);if(!c.isFile())throw Error("not a file");$.writeHead(200,{"Content-Type":xf[Df(z)]??"application/octet-stream","Content-Length":c.size}),If(z).pipe($)}catch{$.writeHead(404),$.end("Not Found")}})}function w(f={}){let g=_f(),$=kf(g);return new Promise((Y,z)=>{let c=()=>{let{port:V}=$.address();Y({server:$,url:`http://localhost:${V}`})};if(f.port!==void 0){$.once("error",z),$.listen(f.port,c);return}$.once("error",(V)=>{if(V.code!=="EADDRINUSE"){z(V);return}console.log(`port ${s} is in use, picked a free port instead`),$.listen(0)}),$.once("listening",c),$.listen(s)})}function o(f){w({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)Cf(g)}).catch((g)=>{throw g})}var Tf=process.stdout.isTTY&&!process.env.NO_COLOR,U=(f)=>(g)=>Tf?`\x1B[${f}m${g}\x1B[0m`:g,j=U("1"),Q=U("2"),t=U("36"),Sg=U("32"),Ag=U("33");function B(f){return`$${f.toFixed(2)}`}function T(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 e(f,g,$){if(g<=0||f<=0)return"";let Y=Math.round(f/g*$*8),z=Math.floor(Y/8),c=Y%8,V=["","▏","▎","▍","▌","▋","▊","▉"];return"█".repeat(z)+(V[c]??"")}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((z)=>z.type===$)?.value??""}function pf(f,g){return[b(f,g,"hour"),b(f,g,"minute"),b(f,g,"second")].join(":")}function hf(f,g,$,Y){let z=f.firstDailyWindow&&f.lastDailyWindow?`${f.firstDailyWindow} – ${f.lastDailyWindow}`:"no data",c=$?`${g}, start ${Y}:00, user ${$}`:`${g}, start ${Y}:00`,V=(S)=>Q(X(S,14)),J=(S)=>j(X(S,12));return[`${j("Cursor Usage")} ${z} ${Q(`(${f.eventCount} events, ${f.dailyWindowCount} daily windows, ${c})`)}`,"",` ${V("Total Cost")}${J(B(f.totalCost))} ${V("Total Tokens")}${J(T(f.totalTokens))}`,` ${V("Avg/Active")}${J(B(f.avgCostPerActiveDailyWindow))} ${V("Max Mode")}${J(`${Math.round(f.maxModeRatio*100)}%`)}`,` ${V("Users")}${J(String(f.userCount))} ${V("Models")}${J(String(f.modelCount))}`]}function I(f,g,$={totalCost:0}){let{totalCost:Y,barWidth:z=28,maxRows:c=15}=$,V=g.slice(0,c),J=Math.max(...V.map((q)=>q.key.length),4),S=Math.max(...V.map((q)=>q.cost),0),A=[j(f)];for(let q of V){let G=Y>0?` ${Q(`${Math.round(q.cost/Y*100)}%`)}`:"";A.push(` ${X(q.key,J)} ${X(B(q.cost),8)} ${t(X(e(q.cost,S,z),z))}${G} ${Q(`${T(q.totalTokens)} tok, ${q.eventCount} ev`)}`)}if(g.length>c)A.push(Q(` … and ${g.length-c} more`));return A}function ff(f,g,$,Y,z=0){let c=N(f,$,z),V=[hf(c,$,Y,z)],J={"daily-window":()=>I("Daily Window Cost",C(f,$,z),{totalCost:c.totalCost,maxRows:31}),model:()=>I("By Model",O(f),{totalCost:c.totalCost}),user:()=>I("By User",D(f),{totalCost:c.totalCost})};if(g)V.push(J[g]());else V.push(J["daily-window"](),J.model(),J.user());return V.map((S)=>S.join(`
|
|
4
4
|
`)).join(`
|
|
5
5
|
|
|
6
6
|
`)+`
|
|
7
|
-
`}function
|
|
8
|
-
Available
|
|
9
|
-
`}let
|
|
7
|
+
`}function gf(f,g,$,Y=0){return JSON.stringify({timeZone:g,startHour:Y,filters:{user:$??null},summary:N(f,g,Y),byDailyWindow:C(f,g,Y),byModel:O(f),byUser:D(f)},null,2)}function Ef(f,g,$,Y,z,c,V){let J=N(g,$,Y),S=z>0?Math.round(J.totalCost/z*100):0,A=(G)=>Q(X(G,14)),q=(G)=>j(X(G,12));return[`${j(`Daily Window ${f}`)} ${Q(`(${J.eventCount} events, rank ${c}/${V} by cost, ${$}, start ${Y}:00)`)}`,"",` ${A("Cost")}${q(B(J.totalCost))} ${A("of period")}${q(`${S}%`)}`,` ${A("Total Tokens")}${q(T(J.totalTokens))} ${A("Max Mode")}${q(`${Math.round(J.maxModeRatio*100)}%`)}`,` ${A("Users")}${q(String(J.userCount))} ${A("Models")}${q(String(J.modelCount))}`]}function Hf(f,g,$){let Y=new Map(W(f,g).map((V)=>[V.key,V])),z=Math.max(...[...Y.values()].map((V)=>V.cost),0),c=[j(`By Hour (${g})`)];for(let V of i($)){let J=Y.get(V),S=J?.cost??0,A=J?.eventCount??0,q=A>0?Q(` ${A} ev`):"";c.push(` ${V} ${X(S>0?B(S):"",8)} ${t(X(e(S,z,24),24))}${q}`)}return c}function Wf(f,g,$){let Y=r(f,g),z=[j(`Top Events (${Y.length} of ${f.length})`)],c=Math.max(...Y.map((J)=>J.user.length),4),V=Math.max(...Y.map((J)=>J.model.length),5);for(let J of Y){let S=pf(J.date,$);z.push(` ${Q(S)} ${X(J.user,c)} ${X(J.model,V)} ${X(B(J.cost),8)} ${Q(`${T(J.totalTokens)} tok`)}`)}return z}function $f(f,g,$,Y,z=0){let c=C(f,$,z),V=R(f,g,$,z);if(V.length===0){let G=c.map((Sf)=>Sf.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 J=f.reduce((G,M)=>G+M.cost,0),S=[...c].sort((G,M)=>M.cost-G.cost).findIndex((G)=>G.key===g)+1,A=N(V,$,z).totalCost;return[Ef(g,V,$,z,J,S,c.length),...Y?[[Q(`Filtered to user: ${Y}`)]]:[],Hf(V,$,z),I("By Model",O(V),{totalCost:A}),I("By User",D(V),{totalCost:A}),I("By Kind",H(V),{totalCost:A}),Wf(V,20,$)].map((G)=>G.join(`
|
|
10
10
|
`)).join(`
|
|
11
11
|
|
|
12
12
|
`)+`
|
|
13
|
-
`}function
|
|
13
|
+
`}function Yf(f,g,$,Y,z=0){let c=R(f,g,$,z);return JSON.stringify({dailyWindow:g,timeZone:$,startHour:z,filters:{user:Y??null},summary:N(c,$,z),byHour:W(c,$),byModel:O(c),byUser:D(c),byKind:H(c)},null,2)}import{basename as wf,dirname as bf,extname as Zf,join as Vf}from"node:path";function vf(f){let{csvPath:g,dailyWindow:$,dailyReport:Y,out:z}=f;if(z)return z;if(Y)return Vf(process.cwd(),"daily-report.png");let c=Zf(g),V=wf(g,c);return Vf(bf(g),`${V}${$?`-${$}-daily`:""}.png`)}async function uf(){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 lf(f){return f.map((g)=>({...g,date:g.date.toISOString()}))}async function Z(f){if(f.events.length===0)throw Error("No billable usage events found in the Usage Export.");if(f.dailyWindow){if(R(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 uf(),$=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(V){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(V)].join(`
|
|
15
|
+
`))}let z=vf(f),c=await w({port:0});try{let V=await Y.newPage({viewport:{width:1200,height:900}});await V.addInitScript({content:`window.__CURSOR_USAGE_EVENTS__ = ${JSON.stringify(lf(f.events))};`});let J=new URL(c.url);J.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 V.goto(J.href,{waitUntil:"networkidle"}),await V.waitForSelector(".grid",{timeout:1e4}),await V.screenshot({path:z,fullPage:!0})}finally{c.server.close(),await Y.close()}return z}var cf=`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(cf),process.exit(1)}function Jf(f,g){if(f===void 0)return g;let $=Number(f);if(!E($))L(`invalid --start-hour value: ${f} (expected 0-23)`);return $}function nf(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 v(f,g,$,Y="No usage events found for the requested filters."){let z;try{z=await yf(f,"utf8")}catch{L(`file not found: ${f}`)}let c=a(z);if(!g)c=d(c);if($)c=c.filter((V)=>V.user===$);if(c.length===0)L(Y);return c}async function mf(f){let{values:g,positionals:$}=p({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 z=g.by;if(z&&!["daily-window","user","model"].includes(z))L(`invalid --by value: ${z} (expected daily-window, user or model)`);let c=g["daily-window"];if(c!==void 0&&!/^\d{4}-\d{2}-\d{2}$/.test(c))L(`invalid --daily-window value: ${c} (expected YYYY-MM-DD)`);let V=g.timezone??x();if(!h(V))L(`invalid --timezone value: ${V}`);let J=Jf(g["start-hour"],0),S=g.user,A=await v(Y,g["include-no-charge"],S);if(c){console.log(g.json?Yf(A,c,V,S,J):$f(A,c,V,S,J));return}console.log(g.json?gf(A,V,S,J):ff(A,z,V,S,J))}async function df(f){let{values:g,positionals:$}=p({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 z=g.timezone??x();if(!h(z))L(`invalid --timezone value: ${z}`);let c=g["daily-window"];if(c!==void 0&&!/^\d{4}-\d{2}-\d{2}$/.test(c))L(`invalid --daily-window value: ${c} (expected YYYY-MM-DD)`);let V=Jf(g["start-hour"],0),J=nf(g["event-limit"]),S=await v(Y,g["include-no-charge"],g.user),A=await Z({csvPath:Y,events:S,timeZone:z,dailyWindow:c,startHour:V,eventLimit:J,dailyReport:!1,out:g.out,user:g.user});console.log(`wrote ${A}`)}async function rf(f){let{positionals:g}=p({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=x(),z=5,c=await v($,!1,void 0,"No billable usage events found in the Usage Export."),V=m(c,Y,z);if(!V)L("No billable usage events found in the Usage Export.");let J=await Z({csvPath:$,events:c,timeZone:Y,dailyWindow:V,startHour:z,eventLimit:10,dailyReport:!0});console.log(`wrote ${J}`)}function zf(f){let{values:g}=p({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}`)}o({port:$,open:!g["no-open"]})}async function af(){let f=process.argv.slice(2);if(f.includes("-h")||f.includes("--help")){console.log(cf);return}let[g,...$]=f;switch(g){case"stats":await mf($);break;case"screenshot":await df($);break;case"daily-report":await rf($);break;case"serve":zf($);break;case void 0:zf([]);break;default:L(`unknown command: ${g}`)}}await af();
|