@jnst/cursor-usage 0.1.0 → 0.2.1
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 +12 -1
- package/dist/cli.js +14 -5
- package/dist/web/chunk-3gd11c41.js +285 -0
- package/dist/web/chunk-s9hpz9pe.css +1 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/chunk-1vzwq3k1.js +0 -285
- package/dist/web/chunk-gbmr24vb.css +0 -1
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ 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 daily cost chart to drill into that day (hourly breakdown, per-model / per-user / per-kind costs, and every event of the day). 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 day, user, and analysis time zone are reflected in the URL hash (`#day=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
|
+
|
|
22
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.
|
|
23
25
|
|
|
24
26
|
```bash
|
|
@@ -35,7 +37,7 @@ npx @jnst/cursor-usage stats team-usage-events.csv
|
|
|
35
37
|
Cursor Usage 2026-06-01 – 2026-06-10 (610 events, 10 days)
|
|
36
38
|
|
|
37
39
|
Total Cost $1446.69 Total Tokens 1.1B
|
|
38
|
-
Avg/
|
|
40
|
+
Avg/Active $144.67 Max Mode 96%
|
|
39
41
|
Users 4 Models 8
|
|
40
42
|
|
|
41
43
|
Daily Cost
|
|
@@ -54,12 +56,21 @@ Options:
|
|
|
54
56
|
| Option | Description |
|
|
55
57
|
| --- | --- |
|
|
56
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 |
|
|
57
62
|
| `--json` | Output aggregated stats as JSON (pipe to jq etc.) |
|
|
58
63
|
| `--include-no-charge` | Include "Errored, No Charge" events |
|
|
59
64
|
|
|
60
65
|
```bash
|
|
61
66
|
# Extract key numbers
|
|
62
67
|
npx @jnst/cursor-usage stats usage.csv --json | jq .summary.totalCost
|
|
68
|
+
|
|
69
|
+
# Drill into the most expensive day
|
|
70
|
+
npx @jnst/cursor-usage stats usage.csv --day 2026-06-02 --timezone Asia/Tokyo
|
|
71
|
+
|
|
72
|
+
# Filter to a single user
|
|
73
|
+
npx @jnst/cursor-usage stats usage.csv --user jnst@example.jp
|
|
63
74
|
```
|
|
64
75
|
|
|
65
76
|
Or install globally to use the short `cursor-usage` command:
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFile as
|
|
3
|
-
`){
|
|
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(`
|
|
4
4
|
`)).join(`
|
|
5
5
|
|
|
6
6
|
`)+`
|
|
7
|
-
`}function
|
|
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?`
|
|
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(`
|
|
10
|
+
`)).join(`
|
|
11
|
+
|
|
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
|
|
8
14
|
|
|
9
15
|
Usage:
|
|
10
16
|
cursor-usage [serve] [--port <n>] [--no-open] Start the drag & drop dashboard (default)
|
|
@@ -12,6 +18,9 @@ Usage:
|
|
|
12
18
|
|
|
13
19
|
Stats options:
|
|
14
20
|
--by <day|user|model> Show a single breakdown axis (default: all)
|
|
21
|
+
--day <YYYY-MM-DD> Drill into a single day (hourly, model, user, kind, top events)
|
|
22
|
+
--user <identifier> Filter analysis to a single User
|
|
23
|
+
--timezone <iana-tz> Analysis time zone (default: current environment)
|
|
15
24
|
--json Output aggregated stats as JSON
|
|
16
25
|
--include-no-charge Include "Errored, No Charge" events
|
|
17
26
|
|
|
@@ -21,5 +30,5 @@ Serve options:
|
|
|
21
30
|
--no-open Do not open the browser automatically
|
|
22
31
|
|
|
23
32
|
-h, --help Show this help
|
|
24
|
-
`;function
|
|
25
|
-
`),console.error(
|
|
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();
|