@neuralmindlabs/tokn 0.1.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.
Files changed (119) hide show
  1. package/.github/workflows/ci.yml +22 -0
  2. package/.oxlintrc.json +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +71 -0
  5. package/dist/assets/index-Dq74RfU-.css +1 -0
  6. package/dist/assets/index-_9e8Z58Z.js +66 -0
  7. package/dist/favicon.svg +1 -0
  8. package/dist/icons.svg +24 -0
  9. package/dist/index.html +14 -0
  10. package/index.html +13 -0
  11. package/package.json +45 -0
  12. package/packages/tokn/LICENSE +21 -0
  13. package/packages/tokn/README.md +85 -0
  14. package/packages/tokn/package.json +56 -0
  15. package/packages/tokn/src/aggregate.ts +83 -0
  16. package/packages/tokn/src/cli.ts +181 -0
  17. package/packages/tokn/src/cursor.ts +85 -0
  18. package/packages/tokn/src/index.ts +6 -0
  19. package/packages/tokn/src/node.ts +50 -0
  20. package/packages/tokn/src/parsers/claude.ts +53 -0
  21. package/packages/tokn/src/parsers/codex.ts +74 -0
  22. package/packages/tokn/src/pricing/index.ts +106 -0
  23. package/packages/tokn/src/pricing/prices.json +948 -0
  24. package/packages/tokn/src/pricing/prices.ts +947 -0
  25. package/packages/tokn/src/types.ts +51 -0
  26. package/packages/tokn/tests/__snapshots__/eval.test.ts.snap +12 -0
  27. package/packages/tokn/tests/aggregate.test.ts +73 -0
  28. package/packages/tokn/tests/claude.test.ts +87 -0
  29. package/packages/tokn/tests/codex.test.ts +54 -0
  30. package/packages/tokn/tests/cursor.test.ts +126 -0
  31. package/packages/tokn/tests/eval.test.ts +69 -0
  32. package/packages/tokn/tests/fixtures/claude-session.jsonl +3 -0
  33. package/packages/tokn/tests/fixtures/codex-session.jsonl +5 -0
  34. package/packages/tokn/tests/pricing.test.ts +137 -0
  35. package/packages/tokn/tsconfig.json +21 -0
  36. package/public/favicon.svg +1 -0
  37. package/public/icons.svg +24 -0
  38. package/scripts/validate.ts +122 -0
  39. package/src/App.tsx +12 -0
  40. package/src/assets/hero.png +0 -0
  41. package/src/assets/vite.svg +1 -0
  42. package/src/index.css +250 -0
  43. package/src/lib/refresh.ts +134 -0
  44. package/src/lib/scanner.ts +108 -0
  45. package/src/main.tsx +7 -0
  46. package/src/ui/Dashboard.tsx +203 -0
  47. package/src/ui/Popover.tsx +91 -0
  48. package/src/ui/Settings.tsx +114 -0
  49. package/src/ui/useAppData.ts +71 -0
  50. package/src-tauri/Cargo.lock +5972 -0
  51. package/src-tauri/Cargo.toml +35 -0
  52. package/src-tauri/build.rs +3 -0
  53. package/src-tauri/capabilities/default.json +114 -0
  54. package/src-tauri/icons/128x128.png +0 -0
  55. package/src-tauri/icons/128x128@2x.png +0 -0
  56. package/src-tauri/icons/32x32.png +0 -0
  57. package/src-tauri/icons/64x64.png +0 -0
  58. package/src-tauri/icons/Square107x107Logo.png +0 -0
  59. package/src-tauri/icons/Square142x142Logo.png +0 -0
  60. package/src-tauri/icons/Square150x150Logo.png +0 -0
  61. package/src-tauri/icons/Square284x284Logo.png +0 -0
  62. package/src-tauri/icons/Square30x30Logo.png +0 -0
  63. package/src-tauri/icons/Square310x310Logo.png +0 -0
  64. package/src-tauri/icons/Square44x44Logo.png +0 -0
  65. package/src-tauri/icons/Square71x71Logo.png +0 -0
  66. package/src-tauri/icons/Square89x89Logo.png +0 -0
  67. package/src-tauri/icons/StoreLogo.png +0 -0
  68. package/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  69. package/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png +0 -0
  70. package/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  71. package/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  72. package/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png +0 -0
  73. package/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  74. package/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  75. package/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png +0 -0
  76. package/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  77. package/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  78. package/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  79. package/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  80. package/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  81. package/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  82. package/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  83. package/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  84. package/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
  85. package/src-tauri/icons/icon.icns +0 -0
  86. package/src-tauri/icons/icon.ico +0 -0
  87. package/src-tauri/icons/icon.png +0 -0
  88. package/src-tauri/icons/ios/AppIcon-20x20@1x.png +0 -0
  89. package/src-tauri/icons/ios/AppIcon-20x20@2x-1.png +0 -0
  90. package/src-tauri/icons/ios/AppIcon-20x20@2x.png +0 -0
  91. package/src-tauri/icons/ios/AppIcon-20x20@3x.png +0 -0
  92. package/src-tauri/icons/ios/AppIcon-29x29@1x.png +0 -0
  93. package/src-tauri/icons/ios/AppIcon-29x29@2x-1.png +0 -0
  94. package/src-tauri/icons/ios/AppIcon-29x29@2x.png +0 -0
  95. package/src-tauri/icons/ios/AppIcon-29x29@3x.png +0 -0
  96. package/src-tauri/icons/ios/AppIcon-40x40@1x.png +0 -0
  97. package/src-tauri/icons/ios/AppIcon-40x40@2x-1.png +0 -0
  98. package/src-tauri/icons/ios/AppIcon-40x40@2x.png +0 -0
  99. package/src-tauri/icons/ios/AppIcon-40x40@3x.png +0 -0
  100. package/src-tauri/icons/ios/AppIcon-512@2x.png +0 -0
  101. package/src-tauri/icons/ios/AppIcon-60x60@2x.png +0 -0
  102. package/src-tauri/icons/ios/AppIcon-60x60@3x.png +0 -0
  103. package/src-tauri/icons/ios/AppIcon-76x76@1x.png +0 -0
  104. package/src-tauri/icons/ios/AppIcon-76x76@2x.png +0 -0
  105. package/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png +0 -0
  106. package/src-tauri/icons/source/app-icon-1024.png +0 -0
  107. package/src-tauri/icons/source/app-icon.svg +74 -0
  108. package/src-tauri/icons/source/tray-22.png +0 -0
  109. package/src-tauri/icons/source/tray-44.png +0 -0
  110. package/src-tauri/icons/source/tray-icon.svg +14 -0
  111. package/src-tauri/permissions/allow-custom-commands.toml +11 -0
  112. package/src-tauri/src/lib.rs +238 -0
  113. package/src-tauri/src/main.rs +6 -0
  114. package/src-tauri/tauri.conf.json +57 -0
  115. package/tsconfig.app.json +27 -0
  116. package/tsconfig.json +7 -0
  117. package/tsconfig.node.json +23 -0
  118. package/vite.config.ts +7 -0
  119. package/vitest.config.ts +28 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Cross-check our Claude Code parsing + pricing against ccusage
3
+ * (independent implementation over the same JSONL files).
4
+ * Run: npm run validate
5
+ */
6
+ import { execSync } from 'node:child_process';
7
+ import { readFileSync, readdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ import { parseClaudeSession } from 'tokn';
11
+ import { parseCodexSession } from 'tokn';
12
+ import { eventsToDays } from 'tokn';
13
+ import type { UsageEvent } from 'tokn';
14
+
15
+ function* jsonlFiles(dir: string): Generator<string> {
16
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
17
+ const p = join(dir, entry.name);
18
+ if (entry.isDirectory()) yield* jsonlFiles(p);
19
+ else if (entry.name.endsWith('.jsonl')) yield p;
20
+ }
21
+ }
22
+
23
+ const dedupe = new Set<string>();
24
+ const events: UsageEvent[] = [];
25
+ let files = 0;
26
+ for (const file of jsonlFiles(join(homedir(), '.claude', 'projects'))) {
27
+ files++;
28
+ try {
29
+ events.push(...parseClaudeSession(readFileSync(file, 'utf8'), dedupe).events);
30
+ } catch {
31
+ /* unreadable file — skip */
32
+ }
33
+ }
34
+
35
+ // Codex self-check: our per-event deltas must sum to each session's own
36
+ // cumulative counter. (ccusage double-counts Codex — token_count events are
37
+ // emitted twice — so it can't serve as the reference here.)
38
+ let codexMismatches = 0;
39
+ let codexEvents = 0;
40
+ for (const file of jsonlFiles(join(homedir(), '.codex', 'sessions'))) {
41
+ files++;
42
+ let content: string;
43
+ try {
44
+ content = readFileSync(file, 'utf8');
45
+ } catch {
46
+ continue;
47
+ }
48
+ const parsed = parseCodexSession(content).events;
49
+ codexEvents += parsed.length;
50
+ let finalTotal = 0;
51
+ let resets = 0;
52
+ let prevTotal = -1;
53
+ for (const line of content.split('\n')) {
54
+ if (!line.includes('token_count')) continue;
55
+ try {
56
+ const t = JSON.parse(line).payload?.info?.total_token_usage;
57
+ if (!t) continue;
58
+ if (t.total_tokens < prevTotal) resets++;
59
+ prevTotal = t.total_tokens;
60
+ finalTotal = t.total_tokens;
61
+ } catch {
62
+ /* skip */
63
+ }
64
+ }
65
+ const ourTotal = parsed.reduce((s, e) => s + e.inputTokens + e.outputTokens, 0);
66
+ // with counter resets the session total is unrecoverable from the last value alone
67
+ if (resets === 0 && finalTotal > 0 && ourTotal !== finalTotal) {
68
+ codexMismatches++;
69
+ console.error(`codex mismatch ${file}: ours ${ourTotal} vs counter ${finalTotal}`);
70
+ }
71
+ }
72
+ console.log(`codex: ${codexEvents} events across sessions, ${codexMismatches} counter mismatches`);
73
+ console.log(`parsed ${files} files, ${events.length} claude usage events`);
74
+
75
+ const days = eventsToDays(events);
76
+ const ours = new Map<string, number>();
77
+ for (const [date, byModel] of Object.entries(days)) {
78
+ let cost = 0;
79
+ for (const t of Object.values(byModel)) cost += t.costUSD;
80
+ ours.set(date, cost);
81
+ }
82
+
83
+ console.log('running ccusage (this downloads the package on first run)...');
84
+ const raw = execSync('npx --yes ccusage@latest daily --json', {
85
+ encoding: 'utf8',
86
+ maxBuffer: 64 * 1024 * 1024,
87
+ });
88
+ const theirs = JSON.parse(raw);
89
+
90
+ const unpriced = new Map<string, number>();
91
+ for (const byModel of Object.values(days)) {
92
+ for (const [model, t] of Object.entries(byModel)) {
93
+ if (t.unpricedEvents > 0) unpriced.set(model, (unpriced.get(model) ?? 0) + t.unpricedEvents);
94
+ }
95
+ }
96
+ if (unpriced.size > 0) console.log('models missing pricing:', Object.fromEntries(unpriced));
97
+
98
+ let worst = 0;
99
+ let compared = 0;
100
+ const rows: string[] = [];
101
+ for (const d of theirs.daily ?? []) {
102
+ const ourCost = ours.get(d.period ?? d.date) ?? 0;
103
+ // only Claude Code is comparable: ccusage double-counts codex, and we
104
+ // don't parse its other agents (gemini, openclaw)
105
+ const theirCost = (d.modelBreakdowns ?? [])
106
+ .filter((mb: any) => typeof mb.modelName === 'string' && mb.modelName.startsWith('claude-'))
107
+ .reduce((s: number, mb: any) => s + (mb.cost ?? 0), 0);
108
+ if (theirCost < 0.01 && ourCost < 0.01) continue;
109
+ compared++;
110
+ const diff = theirCost === 0 ? (ourCost === 0 ? 0 : 1) : Math.abs(ourCost - theirCost) / theirCost;
111
+ worst = Math.max(worst, diff);
112
+ rows.push(
113
+ `${d.period ?? d.date} ours $${ourCost.toFixed(4).padStart(10)} ccusage $${theirCost.toFixed(4).padStart(10)} diff ${(diff * 100).toFixed(2)}%`,
114
+ );
115
+ }
116
+ console.log(rows.slice(-15).join('\n'));
117
+ console.log(`\ncompared ${compared} days, worst relative diff ${(worst * 100).toFixed(2)}%`);
118
+ if (worst > 0.02) {
119
+ console.error('FAIL: divergence above 2% tolerance');
120
+ process.exit(1);
121
+ }
122
+ console.log('PASS: within 2% of ccusage');
package/src/App.tsx ADDED
@@ -0,0 +1,12 @@
1
+ import { useEffect } from 'react';
2
+ import Popover from './ui/Popover.tsx';
3
+ import Dashboard from './ui/Dashboard.tsx';
4
+ import { initTheme } from './lib/refresh.ts';
5
+
6
+ export default function App() {
7
+ useEffect(() => {
8
+ initTheme();
9
+ }, []);
10
+ const view = new URLSearchParams(window.location.search).get('view');
11
+ return view === 'dashboard' ? <Dashboard /> : <Popover />;
12
+ }
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
package/src/index.css ADDED
@@ -0,0 +1,250 @@
1
+ /* ————————————————————————————————————————————————————————————————
2
+ Tokn — "Terminal Ledger"
3
+ A dark-first developer-finance utility: every figure set in the
4
+ native monospace (SF Mono), hairline rules, tabular columns, one
5
+ restrained violet accent echoing the coin mark. Calm, precise,
6
+ glanceable. Light mode is the same ledger on paper.
7
+ ———————————————————————————————————————————————————————————————— */
8
+
9
+ :root {
10
+ --sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
11
+ --mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;
12
+ font-family: var(--sans);
13
+ font-size: 13px;
14
+ color-scheme: light dark;
15
+
16
+ /* paper */
17
+ --bg: #f1f1ef;
18
+ --card: #ffffff;
19
+ --text: #18181b;
20
+ --muted: #6b6b73;
21
+ --faint: #a6a6ad;
22
+ --border: rgba(0, 0, 0, 0.09);
23
+ --hairline: rgba(0, 0, 0, 0.06);
24
+ --hover: rgba(0, 0, 0, 0.035);
25
+ --accent: #6663e6;
26
+ --accent-soft: rgba(102, 99, 230, 0.1);
27
+ --shadow: 0 1px 2px rgba(20, 20, 25, 0.05), 0 10px 30px rgba(20, 20, 25, 0.07);
28
+ }
29
+
30
+ /* theme=system (default): follow the OS */
31
+ @media (prefers-color-scheme: dark) {
32
+ :root:not([data-theme='light']) {
33
+ color-scheme: dark;
34
+ --bg: #131316;
35
+ --card: #1c1c20;
36
+ --text: #e8e8ec;
37
+ --muted: #8b8b94;
38
+ --faint: #55555e;
39
+ --border: rgba(255, 255, 255, 0.08);
40
+ --hairline: rgba(255, 255, 255, 0.055);
41
+ --hover: rgba(255, 255, 255, 0.04);
42
+ --accent: #8b87ff;
43
+ --accent-soft: rgba(139, 135, 255, 0.16);
44
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 10px 30px rgba(0, 0, 0, 0.38);
45
+ }
46
+ }
47
+ /* explicit overrides from Settings */
48
+ :root[data-theme='dark'] {
49
+ color-scheme: dark;
50
+ --bg: #131316;
51
+ --card: #1c1c20;
52
+ --text: #e8e8ec;
53
+ --muted: #8b8b94;
54
+ --faint: #55555e;
55
+ --border: rgba(255, 255, 255, 0.08);
56
+ --hairline: rgba(255, 255, 255, 0.055);
57
+ --hover: rgba(255, 255, 255, 0.04);
58
+ --accent: #8b87ff;
59
+ --accent-soft: rgba(139, 135, 255, 0.16);
60
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 10px 30px rgba(0, 0, 0, 0.38);
61
+ }
62
+ :root[data-theme='light'] {
63
+ color-scheme: light;
64
+ }
65
+
66
+ * { box-sizing: border-box; }
67
+ html, body, #root { margin: 0; padding: 0; }
68
+ body {
69
+ color: var(--text);
70
+ background: transparent;
71
+ -webkit-font-smoothing: antialiased;
72
+ user-select: none;
73
+ }
74
+
75
+ .muted { color: var(--muted); }
76
+ .small { font-size: 11px; }
77
+ .right { text-align: right; }
78
+
79
+ /* every money/token figure is monospace + tabular */
80
+ .big {
81
+ font-family: var(--mono);
82
+ font-size: 27px;
83
+ font-weight: 600;
84
+ letter-spacing: -1px;
85
+ font-variant-numeric: tabular-nums;
86
+ line-height: 1.05;
87
+ }
88
+ .mid {
89
+ font-family: var(--mono);
90
+ font-size: 15px;
91
+ font-weight: 600;
92
+ letter-spacing: -0.4px;
93
+ font-variant-numeric: tabular-nums;
94
+ }
95
+ .dot { width: 7px; height: 7px; border-radius: 2px; display: inline-block; flex: none; }
96
+
97
+ /* small-caps column heads */
98
+ .sc, h2, th {
99
+ font-size: 10px;
100
+ font-weight: 600;
101
+ letter-spacing: 0.9px;
102
+ text-transform: uppercase;
103
+ color: var(--muted);
104
+ }
105
+
106
+ /* ————— popover: cards on the canvas ————— */
107
+ .popover {
108
+ background: var(--bg);
109
+ border: 0.5px solid var(--border);
110
+ border-radius: 16px;
111
+ overflow: hidden;
112
+ height: 100vh;
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 8px;
116
+ padding: 8px;
117
+ position: relative;
118
+ }
119
+ .pop-card {
120
+ background: var(--card);
121
+ border: 0.5px solid var(--hairline);
122
+ border-radius: 12px;
123
+ padding: 13px 14px;
124
+ }
125
+ .pop-hero { padding-bottom: 14px; }
126
+ .pop-header { display: flex; justify-content: space-between; align-items: baseline; }
127
+ .pop-tools { display: flex; flex-direction: column; gap: 10px; }
128
+ .tool-row { display: flex; align-items: baseline; gap: 8px; }
129
+ .tool-row .dot { align-self: center; }
130
+ .tool-name { flex: none; font-weight: 500; }
131
+ /* dotted leader carries the eye to the figure — the ledger touch */
132
+ .leader {
133
+ flex: 1;
134
+ min-width: 12px;
135
+ border-bottom: 1px dotted var(--faint);
136
+ transform: translateY(-3px);
137
+ opacity: 0.6;
138
+ }
139
+ .tool-cost { font-family: var(--mono); font-weight: 600; min-width: 56px; text-align: right; font-variant-numeric: tabular-nums; letter-spacing: -0.3px; }
140
+ .pop-chart .small { color: var(--muted); }
141
+ .bars { display: flex; align-items: flex-end; gap: 3px; height: 60px; margin-top: 8px; }
142
+ .bar-col { flex: 1; display: flex; flex-direction: column-reverse; gap: 1px; height: 100%; }
143
+ .bar-col div { border-radius: 2px 2px 0 0; }
144
+ .bar-labels { display: flex; justify-content: space-between; margin-top: 5px; font-family: var(--mono); font-variant-numeric: tabular-nums; }
145
+ .pop-footer {
146
+ padding: 4px 8px 2px;
147
+ display: flex;
148
+ justify-content: space-between;
149
+ align-items: center;
150
+ margin-top: auto;
151
+ }
152
+ .linkish { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 12px; padding: 0; font-weight: 550; }
153
+ .linkish:hover { text-decoration: underline; }
154
+
155
+ /* ————— dashboard ————— */
156
+ .dashboard {
157
+ background: var(--bg);
158
+ min-height: 100vh;
159
+ padding: 26px 30px 36px;
160
+ user-select: text;
161
+ position: relative;
162
+ }
163
+ .dash-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 18px; }
164
+ h1 { font-size: 21px; font-weight: 650; letter-spacing: -0.4px; margin: 0 0 3px; }
165
+ h2 { margin: 0 0 10px; }
166
+ .controls { display: flex; gap: 6px; }
167
+ .seg {
168
+ background: var(--card);
169
+ border: 0.5px solid var(--border);
170
+ border-radius: 8px;
171
+ padding: 5px 13px;
172
+ cursor: pointer;
173
+ color: var(--text);
174
+ font-size: 12px;
175
+ font-weight: 500;
176
+ font-variant-numeric: tabular-nums;
177
+ transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
178
+ }
179
+ .seg:hover { background: var(--hover); }
180
+ .seg.active { background: var(--accent); color: #fff; border-color: var(--accent); }
181
+
182
+ .filters { display: flex; gap: 18px; margin-bottom: 12px; }
183
+ .filter { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12.5px; font-weight: 500; }
184
+ .filter input { accent-color: var(--accent); }
185
+
186
+ .chart-box {
187
+ background: var(--card);
188
+ border: 0.5px solid var(--border);
189
+ border-radius: 12px;
190
+ padding: 14px 10px 6px;
191
+ margin-bottom: 18px;
192
+ box-shadow: var(--shadow);
193
+ }
194
+
195
+ .tables { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 18px; }
196
+ .tables section {
197
+ background: var(--card);
198
+ border: 0.5px solid var(--border);
199
+ border-radius: 12px;
200
+ padding: 16px 18px;
201
+ box-shadow: var(--shadow);
202
+ }
203
+ table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
204
+ th { text-align: left; padding: 3px 0 6px; border-bottom: 0.5px solid var(--border); }
205
+ th:not(:first-child), td:not(:first-child) { text-align: right; }
206
+ td { padding: 5px 0; border-bottom: 0.5px solid var(--hairline); }
207
+ td:not(:first-child) { font-family: var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -0.3px; }
208
+ tbody tr:last-child td { border-bottom: none; }
209
+ td:first-child { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
210
+ tbody tr { transition: background 120ms ease; }
211
+ tbody tr:hover { background: var(--hover); }
212
+ tfoot td { border-top: 1px solid var(--border); font-weight: 650; padding: 6px 0 2px; }
213
+ .warn { color: #c98a1b; }
214
+
215
+ /* ————— settings ————— */
216
+ .settings {
217
+ background: var(--card);
218
+ border: 0.5px solid var(--border);
219
+ border-radius: 12px;
220
+ padding: 16px 18px;
221
+ box-shadow: var(--shadow);
222
+ }
223
+ .setting-row { display: flex; justify-content: space-between; align-items: center; gap: 24px; padding: 9px 0; }
224
+ .setting-row + .setting-row { border-top: 0.5px solid var(--hairline); }
225
+ .setting-name { font-weight: 550; }
226
+ .cookie-input { display: flex; gap: 6px; flex: none; }
227
+ .cookie-input input {
228
+ width: 260px;
229
+ background: var(--bg);
230
+ border: 0.5px solid var(--border);
231
+ border-radius: 8px;
232
+ padding: 6px 8px;
233
+ color: var(--text);
234
+ font-size: 12px;
235
+ font-family: var(--mono);
236
+ }
237
+ .cookie-input input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
238
+ .cookie-input button, .settings button {
239
+ background: var(--card);
240
+ border: 0.5px solid var(--border);
241
+ border-radius: 8px;
242
+ padding: 6px 12px;
243
+ cursor: pointer;
244
+ color: var(--text);
245
+ font-weight: 500;
246
+ transition: background 120ms ease;
247
+ }
248
+ .cookie-input button:hover, .settings button:hover { background: var(--hover); }
249
+ .help-link { color: var(--accent); cursor: pointer; text-decoration: none; }
250
+ .help-link:hover { text-decoration: underline; }
@@ -0,0 +1,134 @@
1
+ import { invoke } from '@tauri-apps/api/core';
2
+ import { emit } from '@tauri-apps/api/event';
3
+ import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
4
+ import { load, type Store } from '@tauri-apps/plugin-store';
5
+ import { buildRollups, fetchCursorUsage, localDate, fetchPrices, type CursorStatus, type DaysMap } from 'tokn';
6
+ import type { DayRollup } from 'tokn';
7
+ import { scan, type ScanState } from './scanner.ts';
8
+
9
+ export type Theme = 'system' | 'light' | 'dark';
10
+
11
+ export interface Settings {
12
+ cursorCookie: string;
13
+ refreshSec: number;
14
+ theme: Theme;
15
+ }
16
+
17
+ export interface AppData {
18
+ rollups: DayRollup[];
19
+ cursorStatus: CursorStatus | 'disabled';
20
+ lastRefresh: string;
21
+ }
22
+
23
+ export const ROLLUPS_EVENT = 'rollups-updated';
24
+
25
+ let settingsStore: Store | null = null;
26
+ let cacheStore: Store | null = null;
27
+
28
+ export async function stores() {
29
+ settingsStore ??= await load('settings.json', { autoSave: true, defaults: {} });
30
+ cacheStore ??= await load('cache.json', { autoSave: false, defaults: {} });
31
+ return { settingsStore, cacheStore };
32
+ }
33
+
34
+ export async function getSettings(): Promise<Settings> {
35
+ const { settingsStore } = await stores();
36
+ return {
37
+ cursorCookie: (await settingsStore.get<string>('cursorCookie')) ?? '',
38
+ refreshSec: (await settingsStore.get<number>('refreshSec')) ?? 60,
39
+ theme: (await settingsStore.get<Theme>('theme')) ?? 'system',
40
+ };
41
+ }
42
+
43
+ /** Apply the theme to this window and keep it in sync with the store. */
44
+ export async function initTheme() {
45
+ const { settingsStore } = await stores();
46
+ const apply = (theme: Theme | undefined) => {
47
+ const t = theme ?? 'system';
48
+ if (t === 'system') document.documentElement.removeAttribute('data-theme');
49
+ else document.documentElement.setAttribute('data-theme', t);
50
+ };
51
+ apply((await settingsStore.get<Theme>('theme')) ?? 'system');
52
+ await settingsStore.onKeyChange<Theme>('theme', apply);
53
+ }
54
+
55
+ export async function setSetting<K extends keyof Settings>(key: K, value: Settings[K]) {
56
+ const { settingsStore } = await stores();
57
+ await settingsStore.set(key, value);
58
+ }
59
+
60
+ export async function getCachedData(): Promise<AppData | null> {
61
+ const { cacheStore } = await stores();
62
+ return (await cacheStore.get<AppData>('data')) ?? null;
63
+ }
64
+
65
+ export function fmtUSD(n: number): string {
66
+ return `$${n.toFixed(2)}`;
67
+ }
68
+
69
+ /** Runs only in the popover window (always alive); dashboard just listens. */
70
+ export function startRefreshLoop() {
71
+ let scanState: ScanState = {};
72
+ let cursorDays: DaysMap = {};
73
+ let cursorStatus: CursorStatus | 'disabled' = 'disabled';
74
+ let cursorFetchedAt = 0;
75
+ let running = false;
76
+ let booted = false;
77
+
78
+ const tick = async () => {
79
+ if (running) return;
80
+ running = true;
81
+ try {
82
+ const { cacheStore } = await stores();
83
+ if (!booted) {
84
+ booted = true;
85
+ scanState = (await cacheStore.get<ScanState>('scanState')) ?? {};
86
+ cursorDays = (await cacheStore.get<DaysMap>('cursorDays')) ?? {};
87
+ }
88
+ const settings = await getSettings();
89
+
90
+ await fetchPrices({ fetchImpl: tauriFetch });
91
+
92
+ const result = await scan(scanState);
93
+ scanState = result.state;
94
+
95
+ if (settings.cursorCookie && Date.now() - cursorFetchedAt > 10 * 60 * 1000) {
96
+ const cursor = await fetchCursorUsage(settings.cursorCookie, { fetchImpl: tauriFetch });
97
+ cursorStatus = cursor.status;
98
+ cursorFetchedAt = Date.now();
99
+ if (cursor.status === 'ok') {
100
+ cursorDays = cursor.days;
101
+ await cacheStore.set('cursorDays', cursorDays);
102
+ }
103
+ } else if (!settings.cursorCookie) {
104
+ cursorStatus = 'disabled';
105
+ }
106
+
107
+ const rollups = buildRollups([
108
+ ...result.sources,
109
+ { tool: 'cursor', days: cursorDays },
110
+ ]);
111
+ const data: AppData = {
112
+ rollups,
113
+ cursorStatus,
114
+ lastRefresh: new Date().toISOString(),
115
+ };
116
+
117
+ await cacheStore.set('scanState', scanState);
118
+ await cacheStore.set('data', data);
119
+ await cacheStore.save();
120
+
121
+ const today = rollups.find((r) => r.date === localDate(new Date().toISOString()));
122
+ await invoke('set_tray_title', { title: fmtUSD(today?.totalCostUSD ?? 0) });
123
+ await emit(ROLLUPS_EVENT, data);
124
+ } catch (e) {
125
+ console.error('refresh tick failed', e);
126
+ } finally {
127
+ running = false;
128
+ }
129
+ };
130
+
131
+ tick();
132
+ const timer = setInterval(tick, 60_000);
133
+ return () => clearInterval(timer);
134
+ }