vibestats 1.3.14 → 1.4.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.
Files changed (159) hide show
  1. package/README.md +15 -3
  2. package/dist/index.js +389 -68
  3. package/dist/web/404.html +1 -0
  4. package/dist/web/_next/static/chunks/231.de8a9ee791ed9eef.js +1 -0
  5. package/dist/web/_next/static/chunks/498-74a3a367cb8442f0.js +1 -0
  6. package/dist/web/_next/static/chunks/562.a5731acd5543100e.js +1 -0
  7. package/dist/web/_next/static/chunks/816-8960921093d57453.js +1 -0
  8. package/dist/web/_next/static/chunks/832-93b990b7ceeaa2c5.js +1 -0
  9. package/dist/web/_next/static/chunks/971-0ab6a23b2af361a6.js +1 -0
  10. package/dist/web/_next/static/chunks/app/_not-found/page-91f35a114767b957.js +1 -0
  11. package/dist/web/_next/static/chunks/app/activity/[slug]/page-bd4706aecd453508.js +1 -0
  12. package/dist/web/_next/static/chunks/app/activity/page-5d0587c1d5910e68.js +1 -0
  13. package/dist/web/_next/static/chunks/app/changelog/page-5d0587c1d5910e68.js +1 -0
  14. package/dist/web/_next/static/chunks/app/compare/[slug]/page-5d0587c1d5910e68.js +1 -0
  15. package/dist/web/_next/static/chunks/app/compare/page-5d0587c1d5910e68.js +1 -0
  16. package/dist/web/_next/static/chunks/app/dashboard/activity/page-e84049538e03061e.js +1 -0
  17. package/dist/web/_next/static/chunks/app/dashboard/layout-bd4706aecd453508.js +1 -0
  18. package/dist/web/_next/static/chunks/app/dashboard/page-4834f263664e8663.js +1 -0
  19. package/dist/web/_next/static/chunks/app/dashboard/usage/page-b42b457b8fd3865c.js +1 -0
  20. package/dist/web/_next/static/chunks/app/dashboard/wrapped/page-268dced4ee2726f7.js +1 -0
  21. package/dist/web/_next/static/chunks/app/docs/commands/page-5d0587c1d5910e68.js +1 -0
  22. package/dist/web/_next/static/chunks/app/docs/page-5d0587c1d5910e68.js +1 -0
  23. package/dist/web/_next/static/chunks/app/features/[slug]/page-5d0587c1d5910e68.js +1 -0
  24. package/dist/web/_next/static/chunks/app/features/page-5d0587c1d5910e68.js +1 -0
  25. package/dist/web/_next/static/chunks/app/guides/[slug]/page-5d0587c1d5910e68.js +1 -0
  26. package/dist/web/_next/static/chunks/app/guides/page-5d0587c1d5910e68.js +1 -0
  27. package/dist/web/_next/static/chunks/app/images/[...slug]/route-bd4706aecd453508.js +1 -0
  28. package/dist/web/_next/static/chunks/app/layout-6ebd9b17af55ec02.js +1 -0
  29. package/dist/web/_next/static/chunks/app/page-dfb7bbdb5999dc75.js +1 -0
  30. package/dist/web/_next/static/chunks/app/robots.txt/route-bd4706aecd453508.js +1 -0
  31. package/dist/web/_next/static/chunks/app/s/[slug]/route-bd4706aecd453508.js +1 -0
  32. package/dist/web/_next/static/chunks/app/sitemap.xml/route-bd4706aecd453508.js +1 -0
  33. package/dist/web/_next/static/chunks/app/usage/[slug]/page-bd4706aecd453508.js +1 -0
  34. package/dist/web/_next/static/chunks/app/use-cases/[slug]/page-5d0587c1d5910e68.js +1 -0
  35. package/dist/web/_next/static/chunks/app/use-cases/page-5d0587c1d5910e68.js +1 -0
  36. package/dist/web/_next/static/chunks/app/wrapped/[slug]/page-bd4706aecd453508.js +1 -0
  37. package/dist/web/_next/static/chunks/app/wrapped/page-66b00de7736097db.js +1 -0
  38. package/dist/web/_next/static/chunks/c476d598-9099ed8b975ae1d6.js +1 -0
  39. package/dist/web/_next/static/chunks/framework-1c6a486f6592f084.js +1 -0
  40. package/dist/web/_next/static/chunks/main-app-6619364ab1f13fb7.js +1 -0
  41. package/dist/web/_next/static/chunks/main-f6fa273a9100cc16.js +1 -0
  42. package/dist/web/_next/static/chunks/pages/_app-55552e79b4ca5b96.js +1 -0
  43. package/dist/web/_next/static/chunks/pages/_error-da3c1b00689f457b.js +1 -0
  44. package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  45. package/dist/web/_next/static/chunks/webpack-175b2f5685d7557b.js +1 -0
  46. package/dist/web/_next/static/css/335de1248158d380.css +3 -0
  47. package/dist/web/_next/static/gPirvBMhpRSdtR0tsMj2H/_buildManifest.js +1 -0
  48. package/dist/web/_next/static/gPirvBMhpRSdtR0tsMj2H/_ssgManifest.js +1 -0
  49. package/dist/web/activity.html +1 -0
  50. package/dist/web/activity.txt +19 -0
  51. package/dist/web/changelog.html +1 -0
  52. package/dist/web/changelog.txt +60 -0
  53. package/dist/web/compare/claude-code-vs-codex-cli-tracking.html +1 -0
  54. package/dist/web/compare/claude-code-vs-codex-cli-tracking.txt +25 -0
  55. package/dist/web/compare/daily-vs-monthly-ai-usage-reports.html +1 -0
  56. package/dist/web/compare/daily-vs-monthly-ai-usage-reports.txt +24 -0
  57. package/dist/web/compare/local-vs-cloud-ai-analytics.html +1 -0
  58. package/dist/web/compare/local-vs-cloud-ai-analytics.txt +25 -0
  59. package/dist/web/compare/manual-tracking-vs-vibestats.html +1 -0
  60. package/dist/web/compare/manual-tracking-vs-vibestats.txt +24 -0
  61. package/dist/web/compare/querystring-shares-vs-stored-share-pages.html +1 -0
  62. package/dist/web/compare/querystring-shares-vs-stored-share-pages.txt +25 -0
  63. package/dist/web/compare/session-breakdown-vs-model-breakdown.html +1 -0
  64. package/dist/web/compare/session-breakdown-vs-model-breakdown.txt +25 -0
  65. package/dist/web/compare/single-source-vs-combined-tracking.html +1 -0
  66. package/dist/web/compare/single-source-vs-combined-tracking.txt +25 -0
  67. package/dist/web/compare/terminal-table-vs-json-output.html +1 -0
  68. package/dist/web/compare/terminal-table-vs-json-output.txt +24 -0
  69. package/dist/web/compare/token-volume-vs-cost-estimation.html +1 -0
  70. package/dist/web/compare/token-volume-vs-cost-estimation.txt +24 -0
  71. package/dist/web/compare/wrapped-vs-activity-heatmap.html +1 -0
  72. package/dist/web/compare/wrapped-vs-activity-heatmap.txt +25 -0
  73. package/dist/web/compare.html +1 -0
  74. package/dist/web/compare.txt +32 -0
  75. package/dist/web/dashboard/activity.html +1 -0
  76. package/dist/web/dashboard/activity.txt +20 -0
  77. package/dist/web/dashboard/usage.html +1 -0
  78. package/dist/web/dashboard/usage.txt +20 -0
  79. package/dist/web/dashboard/wrapped.html +1 -0
  80. package/dist/web/dashboard/wrapped.txt +20 -0
  81. package/dist/web/dashboard.html +1 -0
  82. package/dist/web/dashboard.txt +20 -0
  83. package/dist/web/docs/commands.html +1 -0
  84. package/dist/web/docs/commands.txt +33 -0
  85. package/dist/web/docs.html +1 -0
  86. package/dist/web/docs.txt +25 -0
  87. package/dist/web/features/ai-coding-activity-heatmap.html +1 -0
  88. package/dist/web/features/ai-coding-activity-heatmap.txt +25 -0
  89. package/dist/web/features/ai-coding-wrapped.html +1 -0
  90. package/dist/web/features/ai-coding-wrapped.txt +24 -0
  91. package/dist/web/features/claude-code-stats.html +1 -0
  92. package/dist/web/features/claude-code-stats.txt +24 -0
  93. package/dist/web/features/claude-diagnostics.html +1 -0
  94. package/dist/web/features/claude-diagnostics.txt +24 -0
  95. package/dist/web/features/codex-cli-stats.html +1 -0
  96. package/dist/web/features/codex-cli-stats.txt +24 -0
  97. package/dist/web/features/combined-ai-coding-stats.html +1 -0
  98. package/dist/web/features/combined-ai-coding-stats.txt +25 -0
  99. package/dist/web/features/json-and-automation-exports.html +1 -0
  100. package/dist/web/features/json-and-automation-exports.txt +25 -0
  101. package/dist/web/features/model-breakdown.html +1 -0
  102. package/dist/web/features/model-breakdown.txt +24 -0
  103. package/dist/web/features/privacy-first-analytics.html +1 -0
  104. package/dist/web/features/privacy-first-analytics.txt +25 -0
  105. package/dist/web/features/session-breakdown.html +1 -0
  106. package/dist/web/features/session-breakdown.txt +24 -0
  107. package/dist/web/features/share-pages.html +1 -0
  108. package/dist/web/features/share-pages.txt +24 -0
  109. package/dist/web/features/token-and-cost-tracking.html +1 -0
  110. package/dist/web/features/token-and-cost-tracking.txt +24 -0
  111. package/dist/web/features.html +1 -0
  112. package/dist/web/features.txt +33 -0
  113. package/dist/web/guides/how-to-build-ai-activity-heatmap.html +1 -0
  114. package/dist/web/guides/how-to-build-ai-activity-heatmap.txt +24 -0
  115. package/dist/web/guides/how-to-compare-model-usage.html +1 -0
  116. package/dist/web/guides/how-to-compare-model-usage.txt +24 -0
  117. package/dist/web/guides/how-to-create-ai-coding-wrapped.html +1 -0
  118. package/dist/web/guides/how-to-create-ai-coding-wrapped.txt +24 -0
  119. package/dist/web/guides/how-to-measure-ai-token-costs.html +1 -0
  120. package/dist/web/guides/how-to-measure-ai-token-costs.txt +24 -0
  121. package/dist/web/guides/how-to-read-claude-diagnostics.html +1 -0
  122. package/dist/web/guides/how-to-read-claude-diagnostics.txt +24 -0
  123. package/dist/web/guides/how-to-share-ai-coding-stats.html +1 -0
  124. package/dist/web/guides/how-to-share-ai-coding-stats.txt +24 -0
  125. package/dist/web/guides/how-to-track-claude-code-usage.html +1 -0
  126. package/dist/web/guides/how-to-track-claude-code-usage.txt +24 -0
  127. package/dist/web/guides/how-to-track-codex-cli-usage.html +1 -0
  128. package/dist/web/guides/how-to-track-codex-cli-usage.txt +24 -0
  129. package/dist/web/guides.html +1 -0
  130. package/dist/web/guides.txt +27 -0
  131. package/dist/web/index.html +1 -0
  132. package/dist/web/index.txt +23 -0
  133. package/dist/web/robots.txt +7 -0
  134. package/dist/web/sitemap.xml +279 -0
  135. package/dist/web/use-cases/ai-agencies.html +1 -0
  136. package/dist/web/use-cases/ai-agencies.txt +23 -0
  137. package/dist/web/use-cases/consultants.html +1 -0
  138. package/dist/web/use-cases/consultants.txt +23 -0
  139. package/dist/web/use-cases/engineering-managers.html +1 -0
  140. package/dist/web/use-cases/engineering-managers.txt +24 -0
  141. package/dist/web/use-cases/freelancers.html +1 -0
  142. package/dist/web/use-cases/freelancers.txt +23 -0
  143. package/dist/web/use-cases/indie-hackers.html +1 -0
  144. package/dist/web/use-cases/indie-hackers.txt +23 -0
  145. package/dist/web/use-cases/monthly-reporting.html +1 -0
  146. package/dist/web/use-cases/monthly-reporting.txt +23 -0
  147. package/dist/web/use-cases/open-source-maintainers.html +1 -0
  148. package/dist/web/use-cases/open-source-maintainers.txt +24 -0
  149. package/dist/web/use-cases/personal-retrospectives.html +1 -0
  150. package/dist/web/use-cases/personal-retrospectives.txt +24 -0
  151. package/dist/web/use-cases/researchers.html +1 -0
  152. package/dist/web/use-cases/researchers.txt +23 -0
  153. package/dist/web/use-cases/weekly-reviews.html +1 -0
  154. package/dist/web/use-cases/weekly-reviews.txt +23 -0
  155. package/dist/web/use-cases.html +1 -0
  156. package/dist/web/use-cases.txt +31 -0
  157. package/dist/web/wrapped.html +1 -0
  158. package/dist/web/wrapped.txt +20 -0
  159. package/package.json +7 -8
package/README.md CHANGED
@@ -8,6 +8,7 @@ AI coding stats CLI for **Claude Code** and **OpenAI Codex**. Track your usage a
8
8
  npm install -g vibestats
9
9
  # or run directly
10
10
  npx vibestats
11
+ npx vibestats codex
11
12
  ```
12
13
 
13
14
  ## Usage
@@ -15,6 +16,9 @@ npx vibestats
15
16
  ```bash
16
17
  # Usage stats (default)
17
18
  vibestats # Daily usage table
19
+ vibestats claude # Claude-compatible usage
20
+ vibestats codex # Codex CLI usage
21
+ vibestats all # Combined local sources
18
22
  vibestats --monthly # Monthly aggregation
19
23
  vibestats --model # Aggregate by model
20
24
  vibestats --total # Show only totals
@@ -65,10 +69,15 @@ vibestats --claude-limits
65
69
 
66
70
  ### Data Source
67
71
 
68
- | Flag | Description |
69
- |------|-------------|
72
+ | Command or flag | Description |
73
+ |-----------------|-------------|
74
+ | `vibestats claude` | Claude-compatible local usage |
75
+ | `vibestats codex` | OpenAI Codex only |
76
+ | `vibestats all` | All supported local sources combined |
70
77
  | `--codex` | OpenAI Codex only |
71
78
  | `--combined` | Claude + Codex combined |
79
+ | `vibestats usage codex --total` | Codex total usage table |
80
+ | `vibestats limits codex` | Codex local limit windows |
72
81
 
73
82
  ### Config
74
83
 
@@ -99,7 +108,7 @@ Creates `~/.vibestats.json`:
99
108
 
100
109
  | Source | Location |
101
110
  |--------|----------|
102
- | Claude Code | `~/.claude/projects/**/*.jsonl` |
111
+ | Claude-compatible | Claude Code, OpenCode, and Factory Droid local usage |
103
112
  | OpenAI Codex | `~/.codex/sessions/*.jsonl` |
104
113
 
105
114
  Additional Claude diagnostic files:
@@ -125,10 +134,13 @@ Additional Claude diagnostic files:
125
134
 
126
135
  Visit [vibestats.wolfai.dev](https://vibestats.wolfai.dev) to view shares in the browser.
127
136
 
137
+ - [vibestats.wolfai.dev/docs](https://vibestats.wolfai.dev/docs)
128
138
  - [vibestats.wolfai.dev/wrapped](https://vibestats.wolfai.dev/wrapped)
129
139
  - [vibestats.wolfai.dev/activity](https://vibestats.wolfai.dev/activity)
130
140
  - [vibestats.wolfai.dev/changelog](https://vibestats.wolfai.dev/changelog)
131
141
 
142
+ The hosted web app runs from the VPS deployment defined by `docker-compose.vps.yml` and `packages/web/Dockerfile`, behind Traefik at `vibestats.wolfai.dev`.
143
+
132
144
  ## License
133
145
 
134
146
  MIT
package/dist/index.js CHANGED
@@ -1036,7 +1036,7 @@ async function writeUsageCacheFile(cache) {
1036
1036
  await fs.rename(tmpPath, cachePath);
1037
1037
  }
1038
1038
  function sleep(ms) {
1039
- return new Promise((resolve) => setTimeout(resolve, ms));
1039
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1040
1040
  }
1041
1041
  async function withUsageCacheLock(task) {
1042
1042
  const lockPath = `${getUsageCachePath()}.lock`;
@@ -2373,9 +2373,9 @@ function aggregateBySession(entries) {
2373
2373
  }
2374
2374
  return Array.from(sessionMap.entries()).filter(([, data]) => data.totalTokens > 0).map(([sessionId, data]) => {
2375
2375
  const date = data.firstTimestamp ? toLocalDateString3(data.firstTimestamp) : "unknown";
2376
- const shortId = data.displaySessionId.slice(0, 12);
2376
+ const shortId2 = data.displaySessionId.slice(0, 12);
2377
2377
  return {
2378
- key: `${date} ${shortId}`,
2378
+ key: `${date} ${shortId2}`,
2379
2379
  inputTokens: data.inputTokens,
2380
2380
  outputTokens: data.outputTokens,
2381
2381
  cacheWriteTokens: data.cacheWriteTokens,
@@ -3666,6 +3666,359 @@ async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
3666
3666
  };
3667
3667
  }
3668
3668
 
3669
+ // src/shared/args.ts
3670
+ var modelFamilyArgs = {
3671
+ claude: {
3672
+ type: "boolean",
3673
+ description: "Show only Claude family stats across local usage sources",
3674
+ default: false
3675
+ },
3676
+ kimi: {
3677
+ type: "boolean",
3678
+ description: "Show only Kimi family stats across local usage sources",
3679
+ default: false
3680
+ },
3681
+ minimax: {
3682
+ type: "boolean",
3683
+ description: "Show only MiniMax family stats across local usage sources",
3684
+ default: false
3685
+ }
3686
+ };
3687
+ function getSelectedModelFamilies(args) {
3688
+ const families = [];
3689
+ if (args.claude === true) families.push("claude");
3690
+ if (args.kimi === true) families.push("kimi");
3691
+ if (args.minimax === true) families.push("minimax");
3692
+ return families;
3693
+ }
3694
+ var cacheArgs = {
3695
+ "no-cache": {
3696
+ type: "boolean",
3697
+ description: "Bypass the persistent local usage cache for this run",
3698
+ default: false
3699
+ },
3700
+ "refresh-cache": {
3701
+ type: "boolean",
3702
+ description: "Rebuild the persistent local usage cache before showing stats",
3703
+ default: false
3704
+ }
3705
+ };
3706
+ function getUsageCacheOptions(args) {
3707
+ return {
3708
+ useCache: args["no-cache"] !== true,
3709
+ refreshCache: args["refresh-cache"] === true
3710
+ };
3711
+ }
3712
+
3713
+ // src/dashboard/assets-path.ts
3714
+ import { fileURLToPath } from "url";
3715
+ import { dirname as dirname2, resolve, join as join5 } from "path";
3716
+ import { existsSync } from "fs";
3717
+ function resolveDashboardAssets(importMetaUrl) {
3718
+ const here = dirname2(fileURLToPath(importMetaUrl));
3719
+ const candidates = [
3720
+ // Published tarball (tsup single-file bundle): here === .../dist, assets at .../dist/web
3721
+ resolve(here, "web"),
3722
+ // Same tarball layout but caller is nested one deeper (e.g. dist/commands/...)
3723
+ resolve(here, "..", "web"),
3724
+ // Monorepo dev with tsx from packages/cli/src/dashboard/...
3725
+ resolve(here, "..", "..", "..", "web", ".next-static"),
3726
+ // Monorepo dev with tsx from packages/cli/src/commands/...
3727
+ resolve(here, "..", "..", "web", ".next-static")
3728
+ ];
3729
+ for (const candidate of candidates) {
3730
+ if (existsSync(join5(candidate, "index.html"))) return candidate;
3731
+ }
3732
+ throw new Error(
3733
+ `dashboard assets not found. Tried:
3734
+ ` + candidates.map((c) => ` - ${c}`).join("\n") + `
3735
+ Run \`pnpm --filter web build:static\` first.`
3736
+ );
3737
+ }
3738
+
3739
+ // src/dashboard/payloads.ts
3740
+ import { randomBytes } from "crypto";
3741
+ var MAX_INLINE_BYTES = 6e3;
3742
+ var STUB_BASE = "http://local";
3743
+ function extractEncodedQuery(urlWithQuery) {
3744
+ const url = new URL(urlWithQuery);
3745
+ const query = url.search.replace(/^\?/, "");
3746
+ return query || null;
3747
+ }
3748
+ function shortId() {
3749
+ return randomBytes(6).toString("hex");
3750
+ }
3751
+ function inlineOrFallback(paramKey, payload, inlineQuery, out) {
3752
+ if (!payload || !inlineQuery) return;
3753
+ if (Buffer.byteLength(inlineQuery, "utf-8") <= MAX_INLINE_BYTES) {
3754
+ out.hubParams.set(paramKey, inlineQuery);
3755
+ return;
3756
+ }
3757
+ const id = shortId();
3758
+ out.jsonStore.set(id, JSON.stringify(payload));
3759
+ out.hubParams.set(paramKey, `json:${id}`);
3760
+ }
3761
+ function buildDashboardPayloads(input) {
3762
+ const out = {
3763
+ hubParams: new URLSearchParams(),
3764
+ jsonStore: /* @__PURE__ */ new Map()
3765
+ };
3766
+ if (input.usage) {
3767
+ const full = encodeUsageToUrl(input.usage, STUB_BASE);
3768
+ inlineOrFallback("u", input.usage, extractEncodedQuery(full), out);
3769
+ }
3770
+ if (input.wrapped) {
3771
+ const full = encodeStatsToUrl(input.wrapped, STUB_BASE);
3772
+ inlineOrFallback("w", input.wrapped, extractEncodedQuery(full), out);
3773
+ }
3774
+ if (input.activity) {
3775
+ const full = encodeActivityToUrl(input.activity, STUB_BASE);
3776
+ inlineOrFallback("a", input.activity, extractEncodedQuery(full), out);
3777
+ }
3778
+ return out;
3779
+ }
3780
+
3781
+ // src/dashboard/open-browser.ts
3782
+ import { spawn } from "child_process";
3783
+ import { platform } from "os";
3784
+ function openInBrowser(url) {
3785
+ const os = platform();
3786
+ const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
3787
+ const args = os === "win32" ? ["/c", "start", "", url] : [url];
3788
+ try {
3789
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
3790
+ child.on("error", () => {
3791
+ });
3792
+ child.unref();
3793
+ } catch {
3794
+ }
3795
+ }
3796
+
3797
+ // src/dashboard/server.ts
3798
+ import { createServer as createServer2 } from "http";
3799
+ import { readFile } from "fs/promises";
3800
+ import { existsSync as existsSync2, realpathSync, statSync } from "fs";
3801
+ import { extname, join as join6, normalize, resolve as resolve2, sep } from "path";
3802
+
3803
+ // src/dashboard/port.ts
3804
+ import { createServer, createConnection } from "net";
3805
+ async function findFreePort(start, end) {
3806
+ for (let port = start; port <= end; port++) {
3807
+ if (await isPortFree(port)) return port;
3808
+ }
3809
+ throw new Error(`no free port available in range ${start}-${end}`);
3810
+ }
3811
+ function isPortFree(port) {
3812
+ return isConnectable(port).then((connectable) => {
3813
+ if (connectable) return false;
3814
+ return isBindable(port);
3815
+ });
3816
+ }
3817
+ function isConnectable(port) {
3818
+ return new Promise((resolve3) => {
3819
+ const socket = createConnection({ port, host: "127.0.0.1" });
3820
+ const done = (result) => {
3821
+ socket.removeAllListeners();
3822
+ socket.destroy();
3823
+ resolve3(result);
3824
+ };
3825
+ socket.once("connect", () => done(true));
3826
+ socket.once("error", () => done(false));
3827
+ const timer = setTimeout(() => done(false), 250);
3828
+ timer.unref();
3829
+ });
3830
+ }
3831
+ function isBindable(port) {
3832
+ return new Promise((resolve3) => {
3833
+ const server = createServer();
3834
+ const onError = () => {
3835
+ server.removeListener("listening", onListening);
3836
+ resolve3(false);
3837
+ };
3838
+ const onListening = () => {
3839
+ server.removeListener("error", onError);
3840
+ server.close(() => {
3841
+ resolve3(true);
3842
+ });
3843
+ };
3844
+ server.on("error", onError);
3845
+ server.on("listening", onListening);
3846
+ server.listen(port, "127.0.0.1");
3847
+ });
3848
+ }
3849
+
3850
+ // src/dashboard/server.ts
3851
+ var MIME = {
3852
+ ".html": "text/html; charset=utf-8",
3853
+ ".js": "application/javascript; charset=utf-8",
3854
+ ".mjs": "application/javascript; charset=utf-8",
3855
+ ".css": "text/css; charset=utf-8",
3856
+ ".json": "application/json; charset=utf-8",
3857
+ ".svg": "image/svg+xml",
3858
+ ".png": "image/png",
3859
+ ".jpg": "image/jpeg",
3860
+ ".jpeg": "image/jpeg",
3861
+ ".webp": "image/webp",
3862
+ ".ico": "image/x-icon",
3863
+ ".woff": "font/woff",
3864
+ ".woff2": "font/woff2",
3865
+ ".txt": "text/plain; charset=utf-8",
3866
+ ".map": "application/json; charset=utf-8"
3867
+ };
3868
+ async function startDashboardServer(options) {
3869
+ const root = realpathSync(resolve2(options.assetsDir));
3870
+ const preferred = options.preferredPort ?? 8080;
3871
+ const port = await findFreePort(preferred, preferred + 20);
3872
+ const server = createServer2(async (req, res) => {
3873
+ try {
3874
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
3875
+ let pathname = url.pathname;
3876
+ const dataMatch = /^\/data\/([A-Za-z0-9_-]+)\.json$/.exec(pathname);
3877
+ if (dataMatch) {
3878
+ const body2 = options.jsonStore.get(dataMatch[1]);
3879
+ if (!body2) {
3880
+ res.statusCode = 404;
3881
+ res.end("not found");
3882
+ return;
3883
+ }
3884
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
3885
+ res.setHeader("Cache-Control", "no-store");
3886
+ res.end(body2);
3887
+ return;
3888
+ }
3889
+ if (pathname === "/") pathname = "/index.html";
3890
+ if (pathname !== "/index.html" && pathname.endsWith("/")) {
3891
+ pathname = pathname.slice(0, -1);
3892
+ }
3893
+ const safePath = normalize(join6(root, pathname));
3894
+ if (!safePath.startsWith(root + sep) && safePath !== root) {
3895
+ res.statusCode = 400;
3896
+ res.end("bad request");
3897
+ return;
3898
+ }
3899
+ let filePath = safePath;
3900
+ const sibling = filePath + ".html";
3901
+ if (existsSync2(filePath) && statSync(filePath).isFile()) {
3902
+ } else if (existsSync2(sibling) && statSync(sibling).isFile()) {
3903
+ filePath = sibling;
3904
+ } else if (existsSync2(filePath) && statSync(filePath).isDirectory()) {
3905
+ filePath = join6(filePath, "index.html");
3906
+ }
3907
+ let realFilePath;
3908
+ try {
3909
+ realFilePath = realpathSync(filePath);
3910
+ } catch {
3911
+ res.statusCode = 404;
3912
+ res.end("not found");
3913
+ return;
3914
+ }
3915
+ if (!realFilePath.startsWith(root + sep) && realFilePath !== root) {
3916
+ res.statusCode = 400;
3917
+ res.end("bad request");
3918
+ return;
3919
+ }
3920
+ if (!existsSync2(realFilePath) || !statSync(realFilePath).isFile()) {
3921
+ res.statusCode = 404;
3922
+ res.end("not found");
3923
+ return;
3924
+ }
3925
+ const ext = extname(realFilePath).toLowerCase();
3926
+ const mime = MIME[ext] || "application/octet-stream";
3927
+ const body = await readFile(realFilePath);
3928
+ if (pathname.startsWith("/_next/static/")) {
3929
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
3930
+ } else {
3931
+ res.setHeader("Cache-Control", "no-cache");
3932
+ }
3933
+ res.setHeader("Content-Type", mime);
3934
+ res.setHeader("Content-Length", String(body.length));
3935
+ res.end(body);
3936
+ } catch (err) {
3937
+ console.error("[dashboard] server error:", err);
3938
+ res.statusCode = 500;
3939
+ res.end("internal error");
3940
+ }
3941
+ });
3942
+ await new Promise((resolveFn, rejectFn) => {
3943
+ const onError = (err) => rejectFn(err);
3944
+ server.once("error", onError);
3945
+ server.listen(port, "127.0.0.1", () => {
3946
+ server.removeListener("error", onError);
3947
+ resolveFn();
3948
+ });
3949
+ });
3950
+ return {
3951
+ server,
3952
+ port,
3953
+ async close() {
3954
+ await Promise.race([
3955
+ new Promise(
3956
+ (resolveFn, rejectFn) => server.close((err) => err ? rejectFn(err) : resolveFn())
3957
+ ),
3958
+ new Promise((resolveFn) => {
3959
+ const t = setTimeout(resolveFn, 2e3);
3960
+ t.unref();
3961
+ })
3962
+ ]);
3963
+ }
3964
+ };
3965
+ }
3966
+
3967
+ // src/commands/dashboard.ts
3968
+ async function runDashboard(args, config) {
3969
+ void config;
3970
+ const families = getSelectedModelFamilies(args);
3971
+ const scopeLabel = formatModelFamilyLabel(families);
3972
+ const cacheOptions = getUsageCacheOptions(args);
3973
+ const codexOnly = args.codex;
3974
+ const combined = args.combined;
3975
+ const [usage, activity, data] = await Promise.all([
3976
+ loadUsageStats({
3977
+ aggregation: "daily",
3978
+ codexOnly,
3979
+ combined,
3980
+ families,
3981
+ scopeLabel,
3982
+ ...cacheOptions
3983
+ }),
3984
+ loadActivityStats({ codexOnly, combined, families, scopeLabel, ...cacheOptions }),
3985
+ loadData({ codexOnly, combined, families, scopeLabel, ...cacheOptions })
3986
+ ]);
3987
+ validateData(data, { codexOnly, combined, families, scopeLabel, ...cacheOptions });
3988
+ let wrapped = null;
3989
+ if (combined) {
3990
+ const c = data.claude ? computeWrappedStats(data.claude) : null;
3991
+ const x = data.codex ? computeCodexWrappedStats(data.codex) : null;
3992
+ wrapped = combineWrappedStats(c, x);
3993
+ } else if (codexOnly) {
3994
+ wrapped = data.codex ? computeCodexWrappedStats(data.codex) : null;
3995
+ } else {
3996
+ wrapped = data.claude ? computeWrappedStats(data.claude) : null;
3997
+ }
3998
+ const activityPayload = activity ? buildActivityArtifact(activity, "tokens", 365).payload : null;
3999
+ const { hubParams, jsonStore } = buildDashboardPayloads({
4000
+ usage: usage ?? null,
4001
+ wrapped,
4002
+ activity: activityPayload
4003
+ });
4004
+ const assetsDir = resolveDashboardAssets(import.meta.url);
4005
+ const server = await startDashboardServer({ assetsDir, jsonStore });
4006
+ const url = `http://127.0.0.1:${server.port}/dashboard/?${hubParams.toString()}`;
4007
+ const orange = "\x1B[38;5;208m";
4008
+ const reset = "\x1B[0m";
4009
+ console.log(`${orange}vibestats dashboard${reset} \u2192 ${url}`);
4010
+ console.log("Press Ctrl+C to stop.");
4011
+ openInBrowser(url);
4012
+ await new Promise((resolveFn) => {
4013
+ const shutdown = async () => {
4014
+ await server.close();
4015
+ resolveFn();
4016
+ };
4017
+ process.once("SIGINT", shutdown);
4018
+ process.once("SIGTERM", shutdown);
4019
+ });
4020
+ }
4021
+
3669
4022
  // src/cli-intent.ts
3670
4023
  var COMMANDS = /* @__PURE__ */ new Set(["usage", "limits", "limit", "pace"]);
3671
4024
  var SCOPES = /* @__PURE__ */ new Set(["claude", "codex", "all", "combined"]);
@@ -3856,8 +4209,8 @@ function applyCliIntent(args, intent) {
3856
4209
  // src/limits/claude.ts
3857
4210
  import { execFile as execFile2 } from "child_process";
3858
4211
  import { randomUUID } from "crypto";
3859
- import { dirname as dirname2 } from "path";
3860
- import { fileURLToPath } from "url";
4212
+ import { dirname as dirname3 } from "path";
4213
+ import { fileURLToPath as fileURLToPath2 } from "url";
3861
4214
 
3862
4215
  // src/limits/pace.ts
3863
4216
  var PACE_EPSILON_PERCENT = 1;
@@ -4207,7 +4560,7 @@ function isClaudePromptReady(text) {
4207
4560
  return /Claude Code v\d+\.\d+\.\d+/.test(normalized) && (normalized.includes("\u276F") || normalized.includes(">"));
4208
4561
  }
4209
4562
  function execFileCommand(command, args, options = {}) {
4210
- return new Promise((resolve, reject) => {
4563
+ return new Promise((resolve3, reject) => {
4211
4564
  execFile2(
4212
4565
  command,
4213
4566
  [...args],
@@ -4221,18 +4574,18 @@ function execFileCommand(command, args, options = {}) {
4221
4574
  reject(error);
4222
4575
  return;
4223
4576
  }
4224
- resolve({ stdout, stderr });
4577
+ resolve3({ stdout, stderr });
4225
4578
  }
4226
4579
  );
4227
4580
  });
4228
4581
  }
4229
4582
  function sleep2(ms) {
4230
- return new Promise((resolve) => setTimeout(resolve, ms));
4583
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4231
4584
  }
4232
4585
  function resolveDefaultClaudeUsageCwd() {
4233
4586
  const envCwd = process.env.VIBESTATS_CLAUDE_USAGE_CWD ?? process.env.VIBESTATS_USAGE_CWD;
4234
4587
  if (envCwd?.trim()) return envCwd;
4235
- return dirname2(fileURLToPath(import.meta.url));
4588
+ return dirname3(fileURLToPath2(import.meta.url));
4236
4589
  }
4237
4590
  async function captureUsageWithTmux(options) {
4238
4591
  const sessionName = `${TMUX_SESSION_PREFIX}-${randomUUID().replaceAll("-", "").slice(0, 16)}`;
@@ -4329,7 +4682,7 @@ async function fetchClaudeLimits(options = {}) {
4329
4682
  }
4330
4683
 
4331
4684
  // src/limits/codex.ts
4332
- import { spawn } from "child_process";
4685
+ import { spawn as spawn2 } from "child_process";
4333
4686
  function clampPercent3(value) {
4334
4687
  const numeric = typeof value === "number" ? value : Number(value);
4335
4688
  if (!Number.isFinite(numeric)) return 0;
@@ -4462,8 +4815,8 @@ var JsonRpcClient = class {
4462
4815
  method,
4463
4816
  ...params === void 0 ? {} : { params }
4464
4817
  };
4465
- const promise = new Promise((resolve, reject) => {
4466
- this.pending.set(id, { resolve, reject });
4818
+ const promise = new Promise((resolve3, reject) => {
4819
+ this.pending.set(id, { resolve: resolve3, reject });
4467
4820
  });
4468
4821
  try {
4469
4822
  this.child.stdin.write(`${JSON.stringify(message)}
@@ -4543,7 +4896,7 @@ function scriptArgsForCodexStatus(command) {
4543
4896
  async function fetchCodexStatusFallback(options = {}) {
4544
4897
  const command = options.command || "codex";
4545
4898
  const timeoutMs = options.timeoutMs ?? 8e3;
4546
- const child = spawn("script", scriptArgsForCodexStatus(command), {
4899
+ const child = spawn2("script", scriptArgsForCodexStatus(command), {
4547
4900
  stdio: ["ignore", "pipe", "pipe"],
4548
4901
  env: process.env
4549
4902
  });
@@ -4556,19 +4909,19 @@ async function fetchCodexStatusFallback(options = {}) {
4556
4909
  child.stderr.on("data", (chunk) => {
4557
4910
  output += chunk;
4558
4911
  });
4559
- await new Promise((resolve) => {
4912
+ await new Promise((resolve3) => {
4560
4913
  const timer = setTimeout(() => {
4561
4914
  child.kill("SIGTERM");
4562
- resolve();
4915
+ resolve3();
4563
4916
  }, timeoutMs);
4564
4917
  timer.unref();
4565
4918
  child.on("error", () => {
4566
4919
  clearTimeout(timer);
4567
- resolve();
4920
+ resolve3();
4568
4921
  });
4569
4922
  child.on("exit", () => {
4570
4923
  clearTimeout(timer);
4571
- resolve();
4924
+ resolve3();
4572
4925
  });
4573
4926
  });
4574
4927
  const windows = parseCodexStatusText(output);
@@ -4591,7 +4944,7 @@ async function fetchCodexStatusFallback(options = {}) {
4591
4944
  async function fetchCodexLimits(options = {}) {
4592
4945
  const command = options.command || "codex";
4593
4946
  const timeoutMs = options.timeoutMs ?? 8e3;
4594
- const child = spawn(command, ["-s", "read-only", "-a", "untrusted", "app-server"], {
4947
+ const child = spawn2(command, ["-s", "read-only", "-a", "untrusted", "app-server"], {
4595
4948
  stdio: ["pipe", "pipe", "pipe"],
4596
4949
  env: process.env
4597
4950
  });
@@ -4756,10 +5109,10 @@ function displayUsageLimits(limits, options = {}) {
4756
5109
  }
4757
5110
 
4758
5111
  // src/config.ts
4759
- import { readFileSync, existsSync, writeFileSync } from "fs";
5112
+ import { readFileSync, existsSync as existsSync3, writeFileSync } from "fs";
4760
5113
  import { homedir as homedir5 } from "os";
4761
- import { join as join5 } from "path";
4762
- var CONFIG_PATH = join5(homedir5(), ".vibestats.json");
5114
+ import { join as join7 } from "path";
5115
+ var CONFIG_PATH = join7(homedir5(), ".vibestats.json");
4763
5116
  var DEFAULT_CONFIG = {
4764
5117
  baseUrl: "https://vibestats.wolfai.dev",
4765
5118
  outputFormat: "normal",
@@ -4769,7 +5122,7 @@ var DEFAULT_CONFIG = {
4769
5122
  hideCost: false
4770
5123
  };
4771
5124
  function loadConfig() {
4772
- if (!existsSync(CONFIG_PATH)) {
5125
+ if (!existsSync3(CONFIG_PATH)) {
4773
5126
  return DEFAULT_CONFIG;
4774
5127
  }
4775
5128
  try {
@@ -4792,7 +5145,7 @@ function mergeConfig(defaults, user) {
4792
5145
  };
4793
5146
  }
4794
5147
  function initConfig() {
4795
- if (existsSync(CONFIG_PATH)) {
5148
+ if (existsSync3(CONFIG_PATH)) {
4796
5149
  console.log(`Config file already exists at ${CONFIG_PATH}`);
4797
5150
  return;
4798
5151
  }
@@ -4822,50 +5175,6 @@ function resolveOptions(cliArgs, config) {
4822
5175
  };
4823
5176
  }
4824
5177
 
4825
- // src/shared/args.ts
4826
- var modelFamilyArgs = {
4827
- claude: {
4828
- type: "boolean",
4829
- description: "Show only Claude family stats across local usage sources",
4830
- default: false
4831
- },
4832
- kimi: {
4833
- type: "boolean",
4834
- description: "Show only Kimi family stats across local usage sources",
4835
- default: false
4836
- },
4837
- minimax: {
4838
- type: "boolean",
4839
- description: "Show only MiniMax family stats across local usage sources",
4840
- default: false
4841
- }
4842
- };
4843
- function getSelectedModelFamilies(args) {
4844
- const families = [];
4845
- if (args.claude === true) families.push("claude");
4846
- if (args.kimi === true) families.push("kimi");
4847
- if (args.minimax === true) families.push("minimax");
4848
- return families;
4849
- }
4850
- var cacheArgs = {
4851
- "no-cache": {
4852
- type: "boolean",
4853
- description: "Bypass the persistent local usage cache for this run",
4854
- default: false
4855
- },
4856
- "refresh-cache": {
4857
- type: "boolean",
4858
- description: "Rebuild the persistent local usage cache before showing stats",
4859
- default: false
4860
- }
4861
- };
4862
- function getUsageCacheOptions(args) {
4863
- return {
4864
- useCache: args["no-cache"] !== true,
4865
- refreshCache: args["refresh-cache"] === true
4866
- };
4867
- }
4868
-
4869
5178
  // src/index.ts
4870
5179
  function printCommandHelp(error) {
4871
5180
  console.error(`Error: ${error}`);
@@ -4874,6 +5183,9 @@ function printCommandHelp(error) {
4874
5183
  console.error(" vibestats limits all");
4875
5184
  console.error(" vibestats limits claude");
4876
5185
  console.error(" vibestats limits codex");
5186
+ console.error(" vibestats claude");
5187
+ console.error(" vibestats codex");
5188
+ console.error(" vibestats all");
4877
5189
  console.error(" vibestats usage all");
4878
5190
  console.error(" vibestats usage --combined --daily");
4879
5191
  console.error(" vibestats usage codex --total");
@@ -5023,7 +5335,7 @@ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, prefe
5023
5335
  var main = defineCommand({
5024
5336
  meta: {
5025
5337
  name: "vibestats",
5026
- version: "1.3.14",
5338
+ version: "1.4.1",
5027
5339
  description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
5028
5340
  },
5029
5341
  args: {
@@ -5160,6 +5472,11 @@ var main = defineCommand({
5160
5472
  description: "Show current config location and values",
5161
5473
  default: false
5162
5474
  },
5475
+ dashboard: {
5476
+ type: "boolean",
5477
+ description: "Open a local web dashboard with usage, activity, and wrapped views",
5478
+ default: false
5479
+ },
5163
5480
  "claude-system": {
5164
5481
  type: "boolean",
5165
5482
  description: "Inspect ~/.claude.json account and app state",
@@ -5200,6 +5517,10 @@ var main = defineCommand({
5200
5517
  await runLiveLimits(normalizedArgs, config);
5201
5518
  return;
5202
5519
  }
5520
+ if (normalizedArgs.dashboard) {
5521
+ await runDashboard(normalizedArgs, config);
5522
+ return;
5523
+ }
5203
5524
  if (normalizedArgs.activity) {
5204
5525
  await runActivity(normalizedArgs, config);
5205
5526
  } else if (normalizedArgs.wrapped) {