@zhangferry-dev/tokendash 1.6.1 → 1.6.2

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 (61) hide show
  1. package/README.md +146 -83
  2. package/dist/client/assets/index-Bw503sNp.css +1 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/daemon.cjs +3306 -0
  5. package/dist/daemon.cjs.map +7 -0
  6. package/dist/electron-server.cjs +1019 -28
  7. package/dist/electron-server.cjs.map +4 -4
  8. package/dist/server/ccusage.d.ts +7 -0
  9. package/dist/server/ccusage.js +69 -0
  10. package/dist/server/daemon.d.ts +12 -0
  11. package/dist/server/daemon.js +176 -0
  12. package/dist/server/index.js +22 -11
  13. package/dist/server/insightsCalculator.d.ts +15 -0
  14. package/dist/server/insightsCalculator.js +276 -0
  15. package/dist/server/quota/adapter.d.ts +47 -0
  16. package/dist/server/quota/adapter.js +41 -0
  17. package/dist/server/quota/adapters/claude.d.ts +2 -0
  18. package/dist/server/quota/adapters/claude.js +124 -0
  19. package/dist/server/quota/adapters/codex.d.ts +2 -0
  20. package/dist/server/quota/adapters/codex.js +188 -0
  21. package/dist/server/quota/adapters/glm.d.ts +2 -0
  22. package/dist/server/quota/adapters/glm.js +133 -0
  23. package/dist/server/quota/adapters/kimi.d.ts +2 -0
  24. package/dist/server/quota/adapters/kimi.js +184 -0
  25. package/dist/server/quota/adapters/minimax.d.ts +2 -0
  26. package/dist/server/quota/adapters/minimax.js +77 -0
  27. package/dist/server/quota/cache.d.ts +20 -0
  28. package/dist/server/quota/cache.js +44 -0
  29. package/dist/server/quota/credentialsFile.d.ts +13 -0
  30. package/dist/server/quota/credentialsFile.js +23 -0
  31. package/dist/server/quota/helpers.d.ts +39 -0
  32. package/dist/server/quota/helpers.js +93 -0
  33. package/dist/server/quota/index.d.ts +5 -0
  34. package/dist/server/quota/index.js +23 -0
  35. package/dist/server/quota/quotaService.d.ts +37 -0
  36. package/dist/server/quota/quotaService.js +141 -0
  37. package/dist/server/quota/schemas.d.ts +358 -0
  38. package/dist/server/quota/schemas.js +53 -0
  39. package/dist/server/quota/types.d.ts +65 -0
  40. package/dist/server/quota/types.js +10 -0
  41. package/dist/server/routes/api.js +15 -0
  42. package/dist/server/routes/insights.d.ts +2 -0
  43. package/dist/server/routes/insights.js +155 -0
  44. package/package.json +6 -10
  45. package/resources/entitlements.mac.plist +10 -0
  46. package/resources/icon-1024.png +0 -0
  47. package/resources/icon.icns +0 -0
  48. package/resources/icon.png +0 -0
  49. package/resources/product_menu.png +0 -0
  50. package/resources/readme-hero.png +0 -0
  51. package/dist/client/assets/index-_yA9tOzZ.css +0 -1
  52. package/electron/main.cjs +0 -516
  53. package/electron/npmSync.cjs +0 -62
  54. package/electron/preload.cjs +0 -36
  55. package/electron/serverReuse.cjs +0 -59
  56. package/electron/trayBadge.cjs +0 -27
  57. package/electron/trayHelper +0 -0
  58. package/electron/trayHelper.swift +0 -152
  59. package/electron/updateService.cjs +0 -220
  60. package/electron-builder.yml +0 -20
  61. /package/dist/client/assets/{index-CY4G_b0x.js → index-C913wKtU.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangferry-dev/tokendash",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
5
  "description": "Token Usage Analytics Dashboard",
6
6
  "publishConfig": {
@@ -14,18 +14,16 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "bin",
17
- "electron",
18
- "resources",
19
- "electron-builder.yml"
17
+ "resources"
20
18
  ],
21
19
  "scripts": {
22
20
  "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
23
21
  "dev:server": "tsx watch src/server/index.ts",
24
22
  "dev:client": "vite",
25
- "build": "vite build && tsc -p tsconfig.json && cp public/popover.html dist/client/popover.html && node esbuild.config.mjs",
26
- "dev:electron": "ELECTRON_DEV=1 electron .",
27
- "build:electron": "electron-builder --mac",
28
- "build:all": "npm run build && npm run build:electron",
23
+ "build": "vite build && tsc -p tsconfig.json && node esbuild.config.mjs",
24
+ "build:swift": "cd TokenDashSwift && swift build -c release",
25
+ "build:app": "npm run build && npm run build:swift && ./scripts/package-app.sh",
26
+ "build:dmg": "npm run build:app && ./scripts/create-dmg.sh",
29
27
  "start": "node dist/server/index.js",
30
28
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.frontend.json --noEmit",
31
29
  "test": "vitest run",
@@ -51,8 +49,6 @@
51
49
  "@types/react-dom": "^19.1.2",
52
50
  "@vitejs/plugin-react": "^4.4.1",
53
51
  "concurrently": "^9.1.2",
54
- "electron": "^41.5.0",
55
- "electron-builder": "^26.0.0",
56
52
  "esbuild": "^0.25.0",
57
53
  "tailwindcss": "^4.1.4",
58
54
  "tsx": "^4.19.3",
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1 +0,0 @@
1
- /*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:"Geist", "Geist Fallback", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"Geist Mono", "Geist Mono Fallback", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-600:oklch(57.7% .245 27.325);--color-orange-600:oklch(64.6% .222 41.116);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.-top-1\.5{top:calc(var(--spacing) * -1.5)}.top-full{top:100%}.bottom-full{bottom:100%}.left-1\/2{left:50%}.z-20{z-index:20}.z-50{z-index:50}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-10{margin-left:calc(var(--spacing) * 10)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-20{height:calc(var(--spacing) * 20)}.h-48{height:calc(var(--spacing) * 48)}.h-64{height:calc(var(--spacing) * 64)}.h-72{height:calc(var(--spacing) * 72)}.h-\[22px\]{height:22px}.h-\[44px\]{height:44px}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-dvh{min-height:100dvh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-20{width:calc(var(--spacing) * 20)}.w-48{width:calc(var(--spacing) * 48)}.w-72{width:calc(var(--spacing) * 72)}.w-\[320px\]{width:320px}.w-fit{width:fit-content}.w-full{width:100%}.w-px{width:1px}.max-w-\[200px\]{max-width:200px}.max-w-\[220px\]{max-width:220px}.max-w-\[1440px\]{max-width:1440px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-45{rotate:45deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-\[3px\]{border-radius:3px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-amber-200\/60{border-color:#fee68599}@supports (color:color-mix(in lab,red,red)){.border-amber-200\/60{border-color:color-mix(in oklab,var(--color-amber-200) 60%,transparent)}}.border-emerald-100\/50{border-color:#d0fae580}@supports (color:color-mix(in lab,red,red)){.border-emerald-100\/50{border-color:color-mix(in oklab,var(--color-emerald-100) 50%,transparent)}}.border-red-200\/60{border-color:#ffcaca99}@supports (color:color-mix(in lab,red,red)){.border-red-200\/60{border-color:color-mix(in oklab,var(--color-red-200) 60%,transparent)}}.border-stone-100{border-color:var(--color-stone-100)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-200\/40{border-color:#e7e5e466}@supports (color:color-mix(in lab,red,red)){.border-stone-200\/40{border-color:color-mix(in oklab,var(--color-stone-200) 40%,transparent)}}.border-stone-200\/50{border-color:#e7e5e480}@supports (color:color-mix(in lab,red,red)){.border-stone-200\/50{border-color:color-mix(in oklab,var(--color-stone-200) 50%,transparent)}}.border-stone-200\/60{border-color:#e7e5e499}@supports (color:color-mix(in lab,red,red)){.border-stone-200\/60{border-color:color-mix(in oklab,var(--color-stone-200) 60%,transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-emerald-50\/50{background-color:#ecfdf580}@supports (color:color-mix(in lab,red,red)){.bg-emerald-50\/50{background-color:color-mix(in oklab,var(--color-emerald-50) 50%,transparent)}}.bg-emerald-200\/50{background-color:#a4f4cf80}@supports (color:color-mix(in lab,red,red)){.bg-emerald-200\/50{background-color:color-mix(in oklab,var(--color-emerald-200) 50%,transparent)}}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-50\/50{background-color:#eef2ff80}@supports (color:color-mix(in lab,red,red)){.bg-indigo-50\/50{background-color:color-mix(in oklab,var(--color-indigo-50) 50%,transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-50\/40{background-color:#fafaf966}@supports (color:color-mix(in lab,red,red)){.bg-stone-50\/40{background-color:color-mix(in oklab,var(--color-stone-50) 40%,transparent)}}.bg-stone-100{background-color:var(--color-stone-100)}.bg-stone-200\/50{background-color:#e7e5e480}@supports (color:color-mix(in lab,red,red)){.bg-stone-200\/50{background-color:color-mix(in oklab,var(--color-stone-200) 50%,transparent)}}.bg-stone-200\/60{background-color:#e7e5e499}@supports (color:color-mix(in lab,red,red)){.bg-stone-200\/60{background-color:color-mix(in oklab,var(--color-stone-200) 60%,transparent)}}.bg-stone-800{background-color:var(--color-stone-800)}.bg-stone-900{background-color:var(--color-stone-900)}.bg-white{background-color:var(--color-white)}.bg-\[radial-gradient\(ellipse_at_top\,\#eef2ff_0\%\,\#faf9f7_35\%\,\#faf9f7_100\%\)\]{background-image:radial-gradient(at top,#eef2ff,#faf9f7 35%,#faf9f7)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:calc(var(--spacing) * 1)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2\.5{padding-top:calc(var(--spacing) * 2.5)}.pb-0\.5{padding-bottom:calc(var(--spacing) * .5)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-600\/70{color:#009767b3}@supports (color:color-mix(in lab,red,red)){.text-emerald-600\/70{color:color-mix(in oklab,var(--color-emerald-600) 70%,transparent)}}.text-emerald-700\/80{color:#007956cc}@supports (color:color-mix(in lab,red,red)){.text-emerald-700\/80{color:color-mix(in oklab,var(--color-emerald-700) 80%,transparent)}}.text-indigo-500\/70{color:#625fffb3}@supports (color:color-mix(in lab,red,red)){.text-indigo-500\/70{color:color-mix(in oklab,var(--color-indigo-500) 70%,transparent)}}.text-indigo-600{color:var(--color-indigo-600)}.text-orange-600{color:var(--color-orange-600)}.text-red-600{color:var(--color-red-600)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-700{color:var(--color-stone-700)}.text-stone-800{color:var(--color-stone-800)}.text-stone-900{color:var(--color-stone-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-\[0_1px_3px_rgba\(0\,0\,0\,0\.1\)\]{--tw-shadow:0 1px 3px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_1px_3px_rgba\(120\,113\,108\,0\.06\)\]{--tw-shadow:0 1px 3px var(--tw-shadow-color,#78716c0f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_8px_30px_rgba\(120\,113\,108\,0\.12\)\]{--tw-shadow:0 8px 30px var(--tw-shadow-color,#78716c1f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_8px_30px_rgba\(120\,113\,108\,0\.15\)\]{--tw-shadow:0 8px 30px var(--tw-shadow-color,#78716c26);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-stone-900\/5{--tw-ring-color:#1c19170d}@supports (color:color-mix(in lab,red,red)){.ring-stone-900\/5{--tw-ring-color:color-mix(in oklab, var(--color-stone-900) 5%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.outline-none{--tw-outline-style:none;outline-style:none}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:z-10:hover{z-index:10}.hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.hover\:bg-stone-50:hover{background-color:var(--color-stone-50)}.hover\:bg-stone-50\/60:hover{background-color:#fafaf999}@supports (color:color-mix(in lab,red,red)){.hover\:bg-stone-50\/60:hover{background-color:color-mix(in oklab,var(--color-stone-50) 60%,transparent)}}.hover\:bg-stone-200\/50:hover{background-color:#e7e5e480}@supports (color:color-mix(in lab,red,red)){.hover\:bg-stone-200\/50:hover{background-color:color-mix(in oklab,var(--color-stone-200) 50%,transparent)}}.hover\:text-indigo-600:hover{color:var(--color-indigo-600)}.hover\:text-stone-800:hover{color:var(--color-stone-800)}.hover\:shadow-\[0_4px_12px_rgba\(120\,113\,108\,0\.09\)\]:hover{--tw-shadow:0 4px 12px var(--tw-shadow-color,#78716c17);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-emerald-400:hover{--tw-ring-color:var(--color-emerald-400)}.hover\:ring-offset-1:hover{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-indigo-500\/20:focus{--tw-ring-color:#625fff33}@supports (color:color-mix(in lab,red,red)){.focus\:ring-indigo-500\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-indigo-500) 20%, transparent)}}@media(min-width:40rem){.sm\:block{display:block}}@media(min-width:48rem){.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}}@media(min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}html,body{background-color:#faf9f7!important}@font-face{font-family:Geist;src:url(https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-sans/Geist-Variable.woff2)format("woff2");font-weight:100 900;font-display:swap;font-style:normal}@font-face{font-family:Geist Mono;src:url(https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-mono/GeistMono-Variable.woff2)format("woff2");font-weight:100 900;font-display:swap;font-style:normal}body:before{content:"";z-index:9999;pointer-events:none;opacity:.015;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");background-repeat:repeat;background-size:200px 200px;position:fixed;top:0;right:0;bottom:0;left:0}.recharts-cartesian-grid-horizontal line,.recharts-cartesian-grid-vertical line{stroke:#e7e5e4}.recharts-text{fill:#78716c;font-family:var(--font-sans);font-size:11px}.recharts-legend-item-text{color:#57534e;font-size:12px}.recharts-tooltip-wrapper{outline:none}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#c8c4be;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#a8a29e}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.skeleton{background:linear-gradient(90deg,#e7e5e4 25%,#d6d3d1,#e7e5e4 75%) 0 0/200% 100%;animation:1.5s ease-in-out infinite shimmer}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
package/electron/main.cjs DELETED
@@ -1,516 +0,0 @@
1
- const { app, BrowserWindow, ipcMain, screen, shell } = require('electron');
2
- const path = require('node:path');
3
- const fs = require('node:fs');
4
- const http = require('node:http');
5
- const { spawn } = require('node:child_process');
6
-
7
- // Global debug logger (writes to file since stdout is lost in packaged apps)
8
- const DEBUG_LOG = '/tmp/tokendash-debug.log';
9
- try { fs.writeFileSync(DEBUG_LOG, 'main.js loaded\n'); } catch(_){}
10
-
11
- // Import from bundled server (created by esbuild)
12
- let createApp;
13
- try {
14
- createApp = require('../dist/electron-server.cjs').createApp;
15
- } catch (e) {
16
- console.error('Failed to load bundled server. Did you run the build?', e.message);
17
- app.quit();
18
- }
19
-
20
- const { formatTokens } = require('./trayBadge.cjs');
21
- const { checkForUpdates, downloadUpdateAsset } = require('./updateService.cjs');
22
- const { syncNpmPackageVersion } = require('./npmSync.cjs');
23
- const { findCompatibleServer, getDashboardUrl } = require('./serverReuse.cjs');
24
-
25
- // Resolve trayHelper binary: extract from asar if needed
26
- function resolveTrayHelperPath() {
27
- const srcPath = path.join(__dirname, 'trayHelper');
28
- const isAsar = srcPath.includes('.asar');
29
- const debugLog = (msg) => {
30
- const logPath = '/tmp/tokendash-debug.log';
31
- fs.appendFileSync(logPath, msg + '\n');
32
- };
33
- debugLog('[trayHelper] __dirname: ' + __dirname);
34
- debugLog('[trayHelper] srcPath: ' + srcPath + ' isAsar: ' + isAsar);
35
- if (isAsar) {
36
- const destDir = path.join(app.getPath('userData'), 'helpers');
37
- const destPath = path.join(destDir, 'trayHelper');
38
- debugLog('[trayHelper] extracting to: ' + destPath);
39
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
40
- fs.copyFileSync(srcPath, destPath);
41
- fs.chmodSync(destPath, 0o755);
42
- debugLog('[trayHelper] extracted OK');
43
- return destPath;
44
- }
45
- return srcPath;
46
- }
47
-
48
- // ---------------------------------------------------------------------------
49
- // State
50
- // ---------------------------------------------------------------------------
51
-
52
- let popover = null;
53
- let server = null;
54
- let trayProcess = null;
55
- let selectedAgents = null; // null = use all available agents
56
- let serverPort = parseInt(process.env.TOKENDASH_PORT || '3456', 10);
57
- let dashboardUrl = getDashboardUrl(serverPort);
58
- let lastUpdateInfo = null;
59
- let isDownloadingUpdate = false;
60
- const POPOVER_WIDTH = 380;
61
- const POPOVER_HEIGHT = 540;
62
- const PACKAGE_NAME = '@zhangferry-dev/tokendash';
63
- const GITHUB_REPO = 'zhangferry/tokendash';
64
-
65
- // ---------------------------------------------------------------------------
66
- // Helpers
67
- // ---------------------------------------------------------------------------
68
-
69
- function listenWithFallback(expressApp, port) {
70
- return new Promise((resolve, reject) => {
71
- let currentPort = port;
72
- let attempts = 0;
73
-
74
- function tryListen() {
75
- const s = expressApp.listen(currentPort);
76
- s.once('listening', () => resolve({ server: s, port: currentPort }));
77
- s.once('error', (err) => {
78
- if (err.code === 'EADDRINUSE' && attempts < 20) {
79
- attempts++;
80
- currentPort++;
81
- tryListen();
82
- } else {
83
- reject(err);
84
- }
85
- });
86
- }
87
-
88
- tryListen();
89
- });
90
- }
91
-
92
- function fetchJson(url) {
93
- return new Promise((resolve, reject) => {
94
- http.get(url, (res) => {
95
- let data = '';
96
- res.on('data', (chunk) => { data += chunk; });
97
- res.on('end', () => {
98
- try { resolve(JSON.parse(data)); }
99
- catch (e) { reject(e); }
100
- });
101
- }).on('error', reject);
102
- });
103
- }
104
-
105
- function getServerBaseUrl() {
106
- return dashboardUrl || getDashboardUrl(serverPort);
107
- }
108
-
109
- function getAppInfo() {
110
- // app.getVersion() returns Electron's version in dev mode (e.g. 41.5).
111
- // Always read from package.json to get the app's own version.
112
- let version = app.getVersion();
113
- try {
114
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
115
- if (pkg.version) version = pkg.version;
116
- } catch (_) {}
117
- return {
118
- version,
119
- launchAtLogin: app.getLoginItemSettings().openAtLogin,
120
- platform: process.platform,
121
- packageName: PACKAGE_NAME,
122
- };
123
- }
124
-
125
- function positionPopoverAtClick(clickScreenX) {
126
- if (!popover) return;
127
-
128
- // Find which display the click is on
129
- const allDisplays = screen.getAllDisplays();
130
- const clickDisplay = allDisplays.find(d => {
131
- const bounds = d.bounds;
132
- return clickScreenX >= bounds.x && clickScreenX < bounds.x + bounds.width;
133
- }) || screen.getPrimaryDisplay();
134
-
135
- const { x: screenX, y: screenY, width: screenW, height: screenH } = clickDisplay.workArea;
136
- const popoverWidth = POPOVER_WIDTH;
137
- const popoverHeight = POPOVER_HEIGHT;
138
-
139
- // Center horizontally on click position
140
- let x = clickScreenX - popoverWidth / 2;
141
- // Below menu bar, close to the icon
142
- let y = screenY + 6;
143
-
144
- // Clamp to screen bounds
145
- if (x < screenX + 8) x = screenX + 8;
146
- if (x + popoverWidth > screenX + screenW - 8) x = screenX + screenW - popoverWidth - 8;
147
- if (y + popoverHeight > screenY + screenH - 8) y = screenY + screenH - popoverHeight - 8;
148
-
149
- popover.setPosition(Math.round(x), Math.round(y), false);
150
- }
151
-
152
- function togglePopover(clickScreenX) {
153
- if (!popover) return;
154
-
155
- if (popover.isVisible()) {
156
- popover.hide();
157
- } else {
158
- positionPopoverAtClick(clickScreenX || 0);
159
- popover.show();
160
- popover.focus();
161
- }
162
- }
163
-
164
- // ---------------------------------------------------------------------------
165
- // Native tray helper (Swift binary for macOS 26+ compatibility)
166
- // ---------------------------------------------------------------------------
167
-
168
- function startTrayHelper() {
169
- const helperPath = resolveTrayHelperPath();
170
- trayProcess = spawn(helperPath, [], {
171
- stdio: ['pipe', 'pipe', 'inherit'],
172
- });
173
-
174
- let buffer = '';
175
-
176
- trayProcess.stdout.on('data', (data) => {
177
- buffer += data.toString();
178
- const lines = buffer.split('\n');
179
- buffer = lines.pop(); // keep incomplete line in buffer
180
-
181
- for (const line of lines) {
182
- const event = line.trim();
183
- if (event.startsWith('click:')) {
184
- // Format: click:x,y (screen coordinates in macOS points)
185
- const parts = event.split(':')[1];
186
- const clickX = parseInt(parts.split(',')[0], 10) || 0;
187
- // Convert macOS screen coords (origin bottom-left) to top-left for Electron
188
- const primaryDisplay = screen.getPrimaryDisplay();
189
- const screenH = primaryDisplay.size.height;
190
- togglePopover(clickX);
191
- } else if (event === 'ready') {
192
- // Helper is ready, start badge updates
193
- startBadgeUpdates();
194
- }
195
- }
196
- });
197
-
198
- trayProcess.on('close', (code) => {
199
- console.log('Tray helper exited with code', code);
200
- trayProcess = null;
201
- });
202
-
203
- trayProcess.on('error', (err) => {
204
- console.error('Failed to start tray helper:', err.message);
205
- trayProcess = null;
206
- });
207
- }
208
-
209
- function sendTrayCommand(command) {
210
- if (trayProcess && trayProcess.stdin && !trayProcess.stdin.destroyed) {
211
- trayProcess.stdin.write(command + '\n');
212
- }
213
- }
214
-
215
- function stopTrayHelper() {
216
- if (trayProcess) {
217
- sendTrayCommand('quit');
218
- trayProcess = null;
219
- }
220
- }
221
-
222
- // ---------------------------------------------------------------------------
223
- // Tray badge updater
224
- // ---------------------------------------------------------------------------
225
-
226
- let updateTimer = null;
227
- let lastTraySnapshot = null;
228
-
229
- function getTrayAgentKey(agents) {
230
- return agents.slice().sort().join(',');
231
- }
232
-
233
- function applyTraySnapshot(snapshot) {
234
- const totalTokens = Number(snapshot && snapshot.totalTokens) || 0;
235
- const totalInput = Number(snapshot && snapshot.totalInput) || 0;
236
- const totalCost = Number(snapshot && snapshot.totalCost) || 0;
237
- const totalCacheRead = Number(snapshot && snapshot.totalCacheRead) || 0;
238
- const today = snapshot && snapshot.today;
239
- const agentKey = snapshot && snapshot.agentKey;
240
-
241
- lastTraySnapshot = { today, agentKey, totalTokens, totalInput, totalCost, totalCacheRead };
242
-
243
- const tokenStr = formatTokens(totalTokens);
244
- sendTrayCommand('title:' + tokenStr);
245
-
246
- const cacheInput = totalInput + totalCacheRead;
247
- const cacheRate = cacheInput > 0 ? ((totalCacheRead / cacheInput) * 100).toFixed(1) : '0.0';
248
- sendTrayCommand('tooltip:TokenDash - ' + tokenStr + ' tokens today ($' + totalCost.toFixed(2) + ') | cache: ' + cacheRate + '%');
249
- }
250
-
251
- function updateTrayBadge() {
252
- const d = new Date(); const today = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,"0") + "-" + String(d.getDate()).padStart(2,"0");
253
-
254
- // Fetch agents list, then fetch daily data for each agent in parallel
255
- const serverBaseUrl = getServerBaseUrl();
256
- fetchJson(`${serverBaseUrl}/api/agents`)
257
- .then((agentData) => {
258
- let agents = (agentData && Array.isArray(agentData.available)) ? agentData.available : ['claude'];
259
- if (agents.length === 0) {
260
- // Transient agent detection failures should not clear a previously valid tray badge.
261
- return null;
262
- }
263
-
264
- // Apply agent filter from popover settings
265
- if (selectedAgents && selectedAgents.length > 0) {
266
- const filtered = agents.filter(a => selectedAgents.includes(a));
267
- if (filtered.length > 0) agents = filtered;
268
- }
269
-
270
- const agentKey = getTrayAgentKey(agents);
271
- return Promise.all(
272
- agents.map(agent =>
273
- fetchJson(`${serverBaseUrl}/api/daily?agent=${agent}`)
274
- .catch(() => null)
275
- )
276
- ).then(results => ({ agentKey, results }));
277
- })
278
- .then((payload) => {
279
- if (!payload) return;
280
- const { agentKey, results } = payload;
281
- const successfulResults = results.filter(data => data && data.daily);
282
- if (successfulResults.length === 0) {
283
- // Keep the last good value when every daily request failed.
284
- return;
285
- }
286
-
287
- let totalTokens = 0;
288
- let totalCost = 0;
289
- let totalInput = 0;
290
- let totalOutput = 0;
291
- let totalCacheRead = 0;
292
-
293
- for (const data of results) {
294
- if (!data || !data.daily) continue;
295
- const entry = data.daily.find(d => d.date === today);
296
- if (!entry) continue;
297
- totalTokens += entry.totalTokens || 0;
298
- totalCost += entry.totalCost || 0;
299
- totalInput += entry.inputTokens || 0;
300
- totalOutput += entry.outputTokens || 0;
301
- totalCacheRead += entry.cacheReadTokens || 0;
302
- }
303
-
304
- const shouldPreserveLastPositive =
305
- totalTokens === 0 &&
306
- lastTraySnapshot &&
307
- lastTraySnapshot.today === today &&
308
- lastTraySnapshot.agentKey === agentKey &&
309
- lastTraySnapshot.totalTokens > 0;
310
-
311
- if (shouldPreserveLastPositive) {
312
- // Daily usage should not drop to zero during the same day for the same agent filter.
313
- // Treat a zero refresh after a positive value as transient empty data and keep the badge stable.
314
- return;
315
- }
316
-
317
- applyTraySnapshot({ today, agentKey, totalTokens, totalInput, totalCost, totalCacheRead });
318
- })
319
- .catch((err) => {
320
- if (err.code !== 'ECONNREFUSED') {
321
- console.error('Tray badge update error:', err.message);
322
- }
323
- });
324
- }
325
-
326
- function startBadgeUpdates() {
327
- updateTrayBadge();
328
- updateTimer = setInterval(updateTrayBadge, 5000);
329
- }
330
-
331
- function stopBadgeUpdates() {
332
- if (updateTimer) {
333
- clearInterval(updateTimer);
334
- updateTimer = null;
335
- }
336
- }
337
-
338
- // ---------------------------------------------------------------------------
339
- // Create popover window
340
- // ---------------------------------------------------------------------------
341
-
342
- function createPopoverWindow() {
343
- popover = new BrowserWindow({
344
- width: POPOVER_WIDTH,
345
- height: POPOVER_HEIGHT,
346
- frame: false,
347
- resizable: false,
348
- hasShadow: true,
349
- alwaysOnTop: true,
350
- skipTaskbar: true,
351
- show: false,
352
- fullscreenable: false,
353
- transparent: false,
354
- webPreferences: {
355
- nodeIntegration: false,
356
- contextIsolation: true,
357
- preload: path.join(__dirname, 'preload.cjs'),
358
- },
359
- });
360
-
361
- popover.loadURL(`${getServerBaseUrl()}/popover.html`);
362
-
363
- popover.on('blur', () => {
364
- popover.hide();
365
- });
366
-
367
- popover.on('close', (e) => {
368
- if (!app.isQuitting) {
369
- e.preventDefault();
370
- popover.hide();
371
- }
372
- });
373
- }
374
-
375
- function registerIpcHandlers() {
376
- ipcMain.handle('tokendash:open-dashboard', (_event, url) => {
377
- return shell.openExternal(getServerBaseUrl());
378
- });
379
-
380
- ipcMain.handle('tokendash:get-app-info', () => {
381
- return getAppInfo();
382
- });
383
-
384
- ipcMain.handle('tokendash:set-launch-at-login', (_event, enabled) => {
385
- const openAtLogin = Boolean(enabled);
386
- app.setLoginItemSettings({ openAtLogin });
387
- return { launchAtLogin: app.getLoginItemSettings().openAtLogin };
388
- });
389
-
390
- ipcMain.handle('tokendash:check-for-updates', async () => {
391
- const currentVersion = getAppInfo().version;
392
-
393
- try {
394
- lastUpdateInfo = await checkForUpdates({ repo: GITHUB_REPO, currentVersion });
395
- return lastUpdateInfo;
396
- } catch (error) {
397
- return {
398
- currentVersion,
399
- latestVersion: currentVersion,
400
- upToDate: true,
401
- error: error instanceof Error ? error.message : String(error),
402
- };
403
- }
404
- });
405
-
406
- ipcMain.handle('tokendash:download-update', async (event) => {
407
- if (isDownloadingUpdate) {
408
- return { ok: false, error: 'An update download is already in progress.' };
409
- }
410
-
411
- const info = lastUpdateInfo;
412
- if (!info || info.upToDate || !info.asset || !info.asset.url) {
413
- return { ok: false, error: 'No downloadable update is available.' };
414
- }
415
-
416
- isDownloadingUpdate = true;
417
- try {
418
- const downloadsDir = path.join(app.getPath('downloads'), 'TokenDash Updates');
419
- const filePath = await downloadUpdateAsset(info.asset, downloadsDir, (progress) => {
420
- event.sender.send('tokendash:update-download-progress', progress);
421
- });
422
- await shell.openPath(filePath);
423
- return { ok: true, filePath };
424
- } catch (error) {
425
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
426
- } finally {
427
- isDownloadingUpdate = false;
428
- }
429
- });
430
-
431
- ipcMain.handle('tokendash:quit', () => {
432
- app.isQuitting = true;
433
- stopBadgeUpdates();
434
- stopTrayHelper();
435
- if (server) server.close();
436
- app.quit();
437
- });
438
-
439
- ipcMain.handle('tokendash:set-selected-agents', (_event, agents) => {
440
- selectedAgents = Array.isArray(agents) ? agents : null;
441
- lastTraySnapshot = null;
442
- // Immediately refresh badge with new filter
443
- updateTrayBadge();
444
- return { ok: true };
445
- });
446
-
447
- ipcMain.handle('tokendash:update-tray-snapshot', (_event, snapshot) => {
448
- if (!snapshot || typeof snapshot !== 'object') return { ok: false };
449
- applyTraySnapshot(snapshot);
450
- return { ok: true };
451
- });
452
- }
453
-
454
- // ---------------------------------------------------------------------------
455
- // App lifecycle
456
- // ---------------------------------------------------------------------------
457
-
458
- app.whenReady().then(async () => {
459
- if (process.platform === 'darwin' && app.dock) {
460
- app.dock.hide();
461
- }
462
-
463
- registerIpcHandlers();
464
-
465
- app.on('before-quit', () => {
466
- app.isQuitting = true;
467
- stopBadgeUpdates();
468
- stopTrayHelper();
469
- if (server) server.close();
470
- });
471
-
472
- const currentVersion = getAppInfo().version;
473
- syncNpmPackageVersion(PACKAGE_NAME, currentVersion).then((result) => {
474
- if (!result || result.ok) return;
475
- console.warn('Could not sync npm package version:', result.error || 'unknown error');
476
- }).catch((error) => {
477
- console.warn('Could not sync npm package version:', error instanceof Error ? error.message : String(error));
478
- });
479
-
480
- const existingServer = await findCompatibleServer(serverPort, currentVersion, PACKAGE_NAME);
481
- if (existingServer) {
482
- serverPort = existingServer.port;
483
- dashboardUrl = existingServer.dashboardUrl;
484
- console.log(`tokendash reusing CLI server on ${dashboardUrl}`);
485
- startTrayHelper();
486
- createPopoverWindow();
487
- return;
488
- }
489
-
490
- // Create and bind Express server.
491
- // Pass dist/ directory so createApp resolves client assets correctly.
492
- const distDir = path.join(__dirname, '..', 'dist');
493
- const expressApp = createApp(serverPort, distDir);
494
- try {
495
- const result = await listenWithFallback(expressApp, serverPort);
496
- server = result.server;
497
- serverPort = result.port;
498
- dashboardUrl = getDashboardUrl(result.port);
499
- console.log(`tokendash running on ${dashboardUrl}`);
500
- } catch (err) {
501
- console.error('Failed to start server:', err);
502
- app.quit();
503
- return;
504
- }
505
-
506
- // Start native tray helper
507
- startTrayHelper();
508
-
509
- // Create popover
510
- createPopoverWindow();
511
- });
512
-
513
- process.on('uncaughtException', (err) => {
514
- console.error('Fatal error in Electron main:', err);
515
- app.quit();
516
- });