@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 +38 -8
- package/dist/cli.js +6 -6
- package/dist/web/chunk-4hj5mwma.js +285 -0
- package/dist/web/index.html +1 -1
- package/package.json +23 -16
- package/dist/web/chunk-3gd11c41.js +0 -285
package/README.md
CHANGED
|
@@ -53,14 +53,14 @@ By Model
|
|
|
53
53
|
|
|
54
54
|
Options:
|
|
55
55
|
|
|
56
|
-
| Option
|
|
57
|
-
|
|
|
58
|
-
| `--by <day\|user\|model>` | Show a single breakdown axis
|
|
59
|
-
| `--day <YYYY-MM-DD>`
|
|
60
|
-
| `--user <identifier>`
|
|
61
|
-
| `--timezone <iana-tz>`
|
|
62
|
-
| `--json`
|
|
63
|
-
| `--include-no-charge`
|
|
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
|
|
3
|
-
`){A(),S++;continue}
|
|
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:
|
|
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
|
|
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,$,
|
|
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}}}),
|
|
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();
|