@zhangferry-dev/tokendash 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/client/assets/index-BJbeEwyn.js +121 -0
- package/dist/client/assets/index-DI_qK8jk.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/ccusage.d.ts +5 -0
- package/dist/server/ccusage.js +16 -0
- package/dist/server/index.js +13 -3
- package/dist/server/openclawParser.d.ts +37 -0
- package/dist/server/openclawParser.js +385 -0
- package/dist/server/routes/api.js +20 -0
- package/dist/server/routes/blocks.js +17 -2
- package/dist/server/routes/daily.js +7 -0
- package/dist/server/routes/projects.js +7 -0
- package/package.json +1 -1
- package/dist/client/assets/index-BaY47OM2.css +0 -1
- package/dist/client/assets/index-CVEOcjaP.js +0 -121
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! tailwindcss v4.2.2 | 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-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-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-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-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-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;--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}.absolute{position:absolute}.relative{position:relative}.static{position:static}.start{inset-inline-start:var(--spacing)}.bottom-full{bottom:100%}.left-1\/2{left:50%}.z-20{z-index:20}.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\.5{margin-top:calc(var(--spacing) * 2.5)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.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-4{height:calc(var(--spacing) * 4)}.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-72{height:calc(var(--spacing) * 72)}.h-\[22px\]{height:22px}.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-4{width:calc(var(--spacing) * 4)}.w-8{width:calc(var(--spacing) * 8)}.w-48{width:calc(var(--spacing) * 48)}.w-72{width:calc(var(--spacing) * 72)}.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}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.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)}.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-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)}}.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-red-50{background-color:var(--color-red-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-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\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.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-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-\[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-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}.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-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)}}.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,)}.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-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-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-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\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,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-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-duration{syntax:"*";inherits:false}
|
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TokenDash</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='28' font-size='28'>⚡</text></svg>" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BJbeEwyn.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DI_qK8jk.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="antialiased" style="background:#faf9f7">
|
|
12
12
|
<div id="root"></div>
|
package/dist/server/ccusage.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export declare function runCcusage(args: string[], timeout?: number): Promise<string>;
|
|
2
2
|
export declare function ensureUsageToolsReady(): Promise<void>;
|
|
3
|
+
export declare function isClaudeCodeAvailable(): Promise<boolean>;
|
|
4
|
+
export declare function detectAvailableAgents(): Promise<{
|
|
5
|
+
claude: boolean;
|
|
6
|
+
codex: boolean;
|
|
7
|
+
}>;
|
package/dist/server/ccusage.js
CHANGED
|
@@ -51,3 +51,19 @@ export async function ensureUsageToolsReady() {
|
|
|
51
51
|
throw new Error('Codex sessions directory not found at ~/.codex/sessions/');
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
export async function isClaudeCodeAvailable() {
|
|
55
|
+
try {
|
|
56
|
+
await runCcusageCommand(['--version'], 120_000, false);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function detectAvailableAgents() {
|
|
64
|
+
const [claude, codex] = await Promise.all([
|
|
65
|
+
isClaudeCodeAvailable(),
|
|
66
|
+
Promise.resolve(isSessionsDirAccessible()),
|
|
67
|
+
]);
|
|
68
|
+
return { claude, codex };
|
|
69
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { registerApiRoutes } from './routes/api.js';
|
|
4
|
-
import {
|
|
4
|
+
import { detectAvailableAgents } from './ccusage.js';
|
|
5
5
|
import open from 'open';
|
|
6
6
|
const CLI_USAGE = [
|
|
7
7
|
'Usage:',
|
|
@@ -52,11 +52,21 @@ function parseCliArgs() {
|
|
|
52
52
|
}
|
|
53
53
|
async function ensureUsageSupportAvailable() {
|
|
54
54
|
try {
|
|
55
|
-
await
|
|
55
|
+
const agents = await detectAvailableAgents();
|
|
56
|
+
if (!agents.claude && !agents.codex) {
|
|
57
|
+
console.error('Error: No AI coding assistant data found.');
|
|
58
|
+
console.error('\nDetails: Could not find Claude Code (ccusage CLI) or Codex (~/.codex/sessions/) data.');
|
|
59
|
+
console.error('Please install at least one of: Claude Code or Codex CLI.');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (agents.claude)
|
|
63
|
+
console.log(' ✓ Claude Code detected');
|
|
64
|
+
if (agents.codex)
|
|
65
|
+
console.log(' ✓ Codex detected');
|
|
56
66
|
return true;
|
|
57
67
|
}
|
|
58
68
|
catch (error) {
|
|
59
|
-
console.error('Error: failed to
|
|
69
|
+
console.error('Error: failed to detect available AI coding assistants');
|
|
60
70
|
console.error('\nDetails:', error instanceof Error ? error.message : error);
|
|
61
71
|
return false;
|
|
62
72
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DailyResponse, ProjectsResponse, BlocksResponse } from '../shared/types.js';
|
|
2
|
+
interface OpenClawTokenEvent {
|
|
3
|
+
timestampMs: number;
|
|
4
|
+
inputTokens: number;
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
cacheReadTokens: number;
|
|
7
|
+
cacheWriteTokens: number;
|
|
8
|
+
totalTokens: number;
|
|
9
|
+
cost: number;
|
|
10
|
+
model: string;
|
|
11
|
+
}
|
|
12
|
+
interface OpenClawSession {
|
|
13
|
+
id: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
tokenEvents: OpenClawTokenEvent[];
|
|
16
|
+
}
|
|
17
|
+
export declare function isOpenClawAccessible(): boolean;
|
|
18
|
+
interface SessionRef {
|
|
19
|
+
sessionId: string;
|
|
20
|
+
sessionFile: string;
|
|
21
|
+
agentId: string;
|
|
22
|
+
}
|
|
23
|
+
/** Scan all OpenClaw agent dirs and collect session file references. */
|
|
24
|
+
export declare function scanOpenClawSessions(): SessionRef[];
|
|
25
|
+
export declare function parseOpenClawSession(ref: SessionRef): OpenClawSession | null;
|
|
26
|
+
export declare function parseAllOpenClawSessions(): OpenClawSession[];
|
|
27
|
+
export interface OpenClawAggregateOptions {
|
|
28
|
+
groupBy?: 'day' | 'hour' | 'month' | 'session';
|
|
29
|
+
since?: Date | null;
|
|
30
|
+
until?: Date | null;
|
|
31
|
+
timezone?: string;
|
|
32
|
+
project?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export declare function getDailyResponse(options?: OpenClawAggregateOptions): DailyResponse;
|
|
35
|
+
export declare function getProjectsResponse(options?: OpenClawAggregateOptions): ProjectsResponse;
|
|
36
|
+
export declare function getBlocksResponse(options?: OpenClawAggregateOptions): BlocksResponse;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, accessSync, constants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Directory helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** All directories OpenClaw may have used (current + legacy names). */
|
|
8
|
+
function getOpenClawDirs() {
|
|
9
|
+
const home = homedir();
|
|
10
|
+
return [
|
|
11
|
+
join(home, '.openclaw'),
|
|
12
|
+
join(home, '.clawdbot'), // legacy name 1
|
|
13
|
+
join(home, '.moltbot'), // legacy name 2
|
|
14
|
+
join(home, '.moldbot'), // legacy name 3
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
export function isOpenClawAccessible() {
|
|
18
|
+
for (const dir of getOpenClawDirs()) {
|
|
19
|
+
try {
|
|
20
|
+
accessSync(join(dir, 'agents'), constants.R_OK);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// try next
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/** Scan all OpenClaw agent dirs and collect session file references. */
|
|
30
|
+
export function scanOpenClawSessions() {
|
|
31
|
+
const refs = [];
|
|
32
|
+
for (const baseDir of getOpenClawDirs()) {
|
|
33
|
+
const agentsDir = join(baseDir, 'agents');
|
|
34
|
+
let agentEntries;
|
|
35
|
+
try {
|
|
36
|
+
agentEntries = readdirSync(agentsDir);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const agentEntry of agentEntries) {
|
|
42
|
+
const sessionsDir = join(agentsDir, agentEntry, 'sessions');
|
|
43
|
+
const indexedPaths = new Set();
|
|
44
|
+
// Try sessions.json index first
|
|
45
|
+
const indexPath = join(sessionsDir, 'sessions.json');
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(indexPath, 'utf-8');
|
|
48
|
+
const index = JSON.parse(raw);
|
|
49
|
+
for (const entry of Object.values(index)) {
|
|
50
|
+
if (!entry.sessionId)
|
|
51
|
+
continue;
|
|
52
|
+
let sessionPath;
|
|
53
|
+
if (entry.sessionFile) {
|
|
54
|
+
const filePath = entry.sessionFile;
|
|
55
|
+
if (filePath.startsWith('/')) {
|
|
56
|
+
// Validate absolute path stays within an OpenClaw directory
|
|
57
|
+
if (!getOpenClawDirs().some(dir => filePath.startsWith(dir)))
|
|
58
|
+
continue;
|
|
59
|
+
sessionPath = filePath;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
sessionPath = join(sessionsDir, filePath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
sessionPath = join(sessionsDir, `${entry.sessionId}.jsonl`);
|
|
67
|
+
}
|
|
68
|
+
indexedPaths.add(sessionPath);
|
|
69
|
+
refs.push({ sessionId: entry.sessionId, sessionFile: sessionPath, agentId: agentEntry });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// No sessions.json — will scan .jsonl files below
|
|
74
|
+
}
|
|
75
|
+
// Scan for .jsonl files not already covered by the index
|
|
76
|
+
let files;
|
|
77
|
+
try {
|
|
78
|
+
files = readdirSync(sessionsDir);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
if (!f.endsWith('.jsonl'))
|
|
85
|
+
continue;
|
|
86
|
+
const fullPath = join(sessionsDir, f);
|
|
87
|
+
if (indexedPaths.has(fullPath))
|
|
88
|
+
continue;
|
|
89
|
+
const sessionId = f.replace(/\.jsonl.*$/, '');
|
|
90
|
+
refs.push({ sessionId, sessionFile: fullPath, agentId: agentEntry });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return refs;
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Session-level cache (mtime-based invalidation)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
const sessionCache = new Map();
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// JSONL parser
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
export function parseOpenClawSession(ref) {
|
|
104
|
+
let fileMtimeMs = 0;
|
|
105
|
+
try {
|
|
106
|
+
fileMtimeMs = statSync(ref.sessionFile).mtimeMs;
|
|
107
|
+
}
|
|
108
|
+
catch { /* ok */ }
|
|
109
|
+
// Return cached result if file hasn't changed
|
|
110
|
+
const cached = sessionCache.get(ref.sessionFile);
|
|
111
|
+
if (cached && cached.mtime === fileMtimeMs) {
|
|
112
|
+
return cached.result;
|
|
113
|
+
}
|
|
114
|
+
let content;
|
|
115
|
+
try {
|
|
116
|
+
content = readFileSync(ref.sessionFile, 'utf-8');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const tokenEvents = [];
|
|
122
|
+
let currentModel = '';
|
|
123
|
+
let currentProvider = '';
|
|
124
|
+
for (const line of content.split('\n')) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed)
|
|
127
|
+
continue;
|
|
128
|
+
let obj;
|
|
129
|
+
try {
|
|
130
|
+
obj = JSON.parse(trimmed);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const type = obj.type;
|
|
136
|
+
if (type === 'model_change') {
|
|
137
|
+
if (obj.modelId)
|
|
138
|
+
currentModel = obj.modelId;
|
|
139
|
+
if (obj.provider)
|
|
140
|
+
currentProvider = obj.provider;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (type === 'custom' && obj.customType === 'model-snapshot') {
|
|
144
|
+
const data = obj.data || {};
|
|
145
|
+
if (data.modelId)
|
|
146
|
+
currentModel = data.modelId;
|
|
147
|
+
if (data.provider)
|
|
148
|
+
currentProvider = data.provider;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (type === 'message') {
|
|
152
|
+
const msg = obj.message || {};
|
|
153
|
+
if (msg.role !== 'assistant')
|
|
154
|
+
continue;
|
|
155
|
+
const usage = msg.usage || {};
|
|
156
|
+
if (!usage)
|
|
157
|
+
continue;
|
|
158
|
+
// Model: prefer embedded, fall back to tracked state
|
|
159
|
+
const model = (msg.model || currentModel || '').trim();
|
|
160
|
+
const provider = (msg.provider || currentProvider || '').trim();
|
|
161
|
+
if (!model)
|
|
162
|
+
continue; // can't attribute cost without a model
|
|
163
|
+
// Update tracked state
|
|
164
|
+
if (model)
|
|
165
|
+
currentModel = model;
|
|
166
|
+
if (provider)
|
|
167
|
+
currentProvider = provider;
|
|
168
|
+
const input = Number(usage.input ?? 0);
|
|
169
|
+
const output = Number(usage.output ?? 0);
|
|
170
|
+
const cacheRead = Number(usage.cacheRead ?? 0);
|
|
171
|
+
const cacheWrite = Number(usage.cacheWrite ?? 0);
|
|
172
|
+
const costObj = usage.cost || {};
|
|
173
|
+
const cost = Number(costObj.total ?? 0);
|
|
174
|
+
const timestampMs = Number(msg.timestamp ?? fileMtimeMs);
|
|
175
|
+
tokenEvents.push({
|
|
176
|
+
timestampMs,
|
|
177
|
+
inputTokens: Math.max(0, input),
|
|
178
|
+
outputTokens: Math.max(0, output),
|
|
179
|
+
cacheReadTokens: Math.max(0, cacheRead),
|
|
180
|
+
cacheWriteTokens: Math.max(0, cacheWrite),
|
|
181
|
+
totalTokens: Math.max(0, input + output + cacheRead),
|
|
182
|
+
cost: Math.max(0, cost),
|
|
183
|
+
model: `${provider}/${model}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (tokenEvents.length === 0) {
|
|
188
|
+
sessionCache.set(ref.sessionFile, { mtime: fileMtimeMs, result: null });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const result = { id: ref.sessionId, agentId: ref.agentId, tokenEvents };
|
|
192
|
+
sessionCache.set(ref.sessionFile, { mtime: fileMtimeMs, result });
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
export function parseAllOpenClawSessions() {
|
|
196
|
+
return scanOpenClawSessions()
|
|
197
|
+
.map(parseOpenClawSession)
|
|
198
|
+
.filter((s) => s !== null);
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Date / timezone helpers (same logic as codexParser)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
const TZ_OFFSETS = {
|
|
204
|
+
'Asia/Shanghai': 8,
|
|
205
|
+
'Asia/Tokyo': 9,
|
|
206
|
+
'America/New_York': -5,
|
|
207
|
+
'America/Los_Angeles': -8,
|
|
208
|
+
'Europe/London': 0,
|
|
209
|
+
'UTC': 0,
|
|
210
|
+
};
|
|
211
|
+
function getTzOffsetHours(tz) {
|
|
212
|
+
return TZ_OFFSETS[tz] ?? 8;
|
|
213
|
+
}
|
|
214
|
+
function msToLocalDate(ms, tz) {
|
|
215
|
+
return new Date(ms + getTzOffsetHours(tz) * 3_600_000);
|
|
216
|
+
}
|
|
217
|
+
function getDateKey(ms, tz) {
|
|
218
|
+
return msToLocalDate(ms, tz).toISOString().slice(0, 10);
|
|
219
|
+
}
|
|
220
|
+
function getHourKey(ms, tz) {
|
|
221
|
+
const d = msToLocalDate(ms, tz);
|
|
222
|
+
return d.toISOString().slice(0, 13).replace('T', ' ') + ':00';
|
|
223
|
+
}
|
|
224
|
+
function getMonthKey(ms, tz) {
|
|
225
|
+
return getDateKey(ms, tz).slice(0, 7);
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Aggregation helpers
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
function emptyAcc() {
|
|
231
|
+
return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, cost: 0 };
|
|
232
|
+
}
|
|
233
|
+
function addEvent(acc, ev) {
|
|
234
|
+
acc.inputTokens += ev.inputTokens;
|
|
235
|
+
acc.outputTokens += ev.outputTokens;
|
|
236
|
+
acc.cacheReadTokens += ev.cacheReadTokens;
|
|
237
|
+
acc.cacheWriteTokens += ev.cacheWriteTokens;
|
|
238
|
+
acc.totalTokens += ev.totalTokens;
|
|
239
|
+
acc.cost += ev.cost;
|
|
240
|
+
}
|
|
241
|
+
function mergeAcc(a, b) {
|
|
242
|
+
a.inputTokens += b.inputTokens;
|
|
243
|
+
a.outputTokens += b.outputTokens;
|
|
244
|
+
a.cacheReadTokens += b.cacheReadTokens;
|
|
245
|
+
a.cacheWriteTokens += b.cacheWriteTokens;
|
|
246
|
+
a.totalTokens += b.totalTokens;
|
|
247
|
+
a.cost += b.cost;
|
|
248
|
+
}
|
|
249
|
+
function accToEntry(date, acc, models) {
|
|
250
|
+
const modelList = [...models];
|
|
251
|
+
const costPerModel = modelList.length > 0 ? acc.cost / modelList.length : 0;
|
|
252
|
+
return {
|
|
253
|
+
date,
|
|
254
|
+
inputTokens: acc.inputTokens,
|
|
255
|
+
outputTokens: acc.outputTokens,
|
|
256
|
+
cacheCreationTokens: acc.cacheWriteTokens,
|
|
257
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
258
|
+
totalTokens: acc.totalTokens,
|
|
259
|
+
totalCost: acc.cost,
|
|
260
|
+
modelsUsed: modelList,
|
|
261
|
+
modelBreakdowns: modelList.map(name => ({
|
|
262
|
+
modelName: name,
|
|
263
|
+
inputTokens: acc.inputTokens,
|
|
264
|
+
outputTokens: acc.outputTokens,
|
|
265
|
+
cacheCreationTokens: acc.cacheWriteTokens,
|
|
266
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
267
|
+
cost: costPerModel,
|
|
268
|
+
})),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
export function getDailyResponse(options) {
|
|
272
|
+
const sessions = parseAllOpenClawSessions();
|
|
273
|
+
const tz = options?.timezone || 'Asia/Shanghai';
|
|
274
|
+
const grouped = new Map();
|
|
275
|
+
const totalsAcc = emptyAcc();
|
|
276
|
+
for (const session of sessions) {
|
|
277
|
+
if (options?.project && session.agentId !== options.project)
|
|
278
|
+
continue;
|
|
279
|
+
for (const ev of session.tokenEvents) {
|
|
280
|
+
if (options?.since && ev.timestampMs < options.since.getTime())
|
|
281
|
+
continue;
|
|
282
|
+
if (options?.until && ev.timestampMs > options.until.getTime())
|
|
283
|
+
continue;
|
|
284
|
+
const key = getDateKey(ev.timestampMs, tz);
|
|
285
|
+
if (!grouped.has(key))
|
|
286
|
+
grouped.set(key, { acc: emptyAcc(), models: new Set() });
|
|
287
|
+
const entry = grouped.get(key);
|
|
288
|
+
addEvent(entry.acc, ev);
|
|
289
|
+
entry.models.add(ev.model);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const daily = [];
|
|
293
|
+
for (const [date, { acc, models }] of grouped) {
|
|
294
|
+
daily.push(accToEntry(date, acc, models));
|
|
295
|
+
mergeAcc(totalsAcc, acc);
|
|
296
|
+
}
|
|
297
|
+
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
298
|
+
return {
|
|
299
|
+
daily,
|
|
300
|
+
totals: {
|
|
301
|
+
inputTokens: totalsAcc.inputTokens,
|
|
302
|
+
outputTokens: totalsAcc.outputTokens,
|
|
303
|
+
cacheCreationTokens: totalsAcc.cacheWriteTokens,
|
|
304
|
+
cacheReadTokens: totalsAcc.cacheReadTokens,
|
|
305
|
+
totalTokens: totalsAcc.totalTokens,
|
|
306
|
+
totalCost: totalsAcc.cost,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
export function getProjectsResponse(options) {
|
|
311
|
+
const sessions = parseAllOpenClawSessions();
|
|
312
|
+
const tz = options?.timezone || 'Asia/Shanghai';
|
|
313
|
+
const projects = {};
|
|
314
|
+
for (const session of sessions) {
|
|
315
|
+
const projectName = session.agentId;
|
|
316
|
+
const dailyMap = new Map();
|
|
317
|
+
for (const ev of session.tokenEvents) {
|
|
318
|
+
if (options?.since && ev.timestampMs < options.since.getTime())
|
|
319
|
+
continue;
|
|
320
|
+
if (options?.until && ev.timestampMs > options.until.getTime())
|
|
321
|
+
continue;
|
|
322
|
+
const dayKey = getDateKey(ev.timestampMs, tz);
|
|
323
|
+
if (!dailyMap.has(dayKey))
|
|
324
|
+
dailyMap.set(dayKey, { acc: emptyAcc(), models: new Set() });
|
|
325
|
+
addEvent(dailyMap.get(dayKey).acc, ev);
|
|
326
|
+
dailyMap.get(dayKey).models.add(ev.model);
|
|
327
|
+
}
|
|
328
|
+
if (!projects[projectName])
|
|
329
|
+
projects[projectName] = [];
|
|
330
|
+
for (const [date, { acc, models }] of dailyMap) {
|
|
331
|
+
projects[projectName].push(accToEntry(date, acc, models));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const key of Object.keys(projects)) {
|
|
335
|
+
projects[key].sort((a, b) => a.date.localeCompare(b.date));
|
|
336
|
+
}
|
|
337
|
+
return { projects };
|
|
338
|
+
}
|
|
339
|
+
export function getBlocksResponse(options) {
|
|
340
|
+
const sessions = parseAllOpenClawSessions();
|
|
341
|
+
const tz = options?.timezone || 'Asia/Shanghai';
|
|
342
|
+
const grouped = new Map();
|
|
343
|
+
for (const session of sessions) {
|
|
344
|
+
if (options?.project && session.agentId !== options.project)
|
|
345
|
+
continue;
|
|
346
|
+
for (const ev of session.tokenEvents) {
|
|
347
|
+
if (options?.since && ev.timestampMs < options.since.getTime())
|
|
348
|
+
continue;
|
|
349
|
+
if (options?.until && ev.timestampMs > options.until.getTime())
|
|
350
|
+
continue;
|
|
351
|
+
const key = getHourKey(ev.timestampMs, tz);
|
|
352
|
+
if (!grouped.has(key))
|
|
353
|
+
grouped.set(key, { acc: emptyAcc(), models: new Set() });
|
|
354
|
+
addEvent(grouped.get(key).acc, ev);
|
|
355
|
+
grouped.get(key).models.add(ev.model);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const blocks = [];
|
|
359
|
+
let idx = 0;
|
|
360
|
+
for (const [hourKey, { acc, models }] of grouped) {
|
|
361
|
+
const [datePart, timePart] = hourKey.split(' ');
|
|
362
|
+
const hour = timePart.split(':')[0];
|
|
363
|
+
blocks.push({
|
|
364
|
+
id: `openclaw-hour-${idx}`,
|
|
365
|
+
startTime: `${datePart}T${hour}:00:00`,
|
|
366
|
+
endTime: `${datePart}T${hour}:59:59`,
|
|
367
|
+
actualEndTime: null,
|
|
368
|
+
isActive: false,
|
|
369
|
+
isGap: false,
|
|
370
|
+
entries: acc.totalTokens > 0 ? 1 : 0,
|
|
371
|
+
tokenCounts: {
|
|
372
|
+
inputTokens: acc.inputTokens,
|
|
373
|
+
outputTokens: acc.outputTokens,
|
|
374
|
+
cacheCreationInputTokens: acc.cacheWriteTokens,
|
|
375
|
+
cacheReadInputTokens: acc.cacheReadTokens,
|
|
376
|
+
},
|
|
377
|
+
totalTokens: acc.totalTokens,
|
|
378
|
+
costUSD: acc.cost,
|
|
379
|
+
models: [...models],
|
|
380
|
+
});
|
|
381
|
+
idx++;
|
|
382
|
+
}
|
|
383
|
+
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
384
|
+
return { blocks };
|
|
385
|
+
}
|
|
@@ -3,7 +3,27 @@ import { getMonthly } from './monthly.js';
|
|
|
3
3
|
import { getSession } from './session.js';
|
|
4
4
|
import { getProjects } from './projects.js';
|
|
5
5
|
import { getBlocks } from './blocks.js';
|
|
6
|
+
import { detectAvailableAgents } from '../ccusage.js';
|
|
7
|
+
import { isOpenClawAccessible } from '../openclawParser.js';
|
|
8
|
+
async function getAgents(_req, res) {
|
|
9
|
+
try {
|
|
10
|
+
const agents = await detectAvailableAgents();
|
|
11
|
+
const available = [];
|
|
12
|
+
if (agents.claude)
|
|
13
|
+
available.push('claude');
|
|
14
|
+
if (agents.codex)
|
|
15
|
+
available.push('codex');
|
|
16
|
+
if (isOpenClawAccessible())
|
|
17
|
+
available.push('openclaw');
|
|
18
|
+
res.json({ available, default: available[0] || null });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
22
|
+
res.status(500).json({ error: 'Failed to detect agents', hint: message });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
6
25
|
export function registerApiRoutes(router) {
|
|
26
|
+
router.get('/agents', getAgents);
|
|
7
27
|
router.get('/daily', getDaily);
|
|
8
28
|
router.get('/monthly', getMonthly);
|
|
9
29
|
router.get('/session', getSession);
|
|
@@ -2,11 +2,25 @@ import { runCcusage } from '../ccusage.js';
|
|
|
2
2
|
import { cache } from '../cache.js';
|
|
3
3
|
import { validateBlocks } from '../../shared/schemas.js';
|
|
4
4
|
import { getBlocksResponse } from '../codexParser.js';
|
|
5
|
+
import { getBlocksResponse as getOpenClawBlocksResponse } from '../openclawParser.js';
|
|
5
6
|
import { getClaudeBlocksByProject } from '../claudeBlocksParser.js';
|
|
6
7
|
export async function getBlocks(req, res) {
|
|
7
8
|
const agent = req.query.agent || 'claude';
|
|
8
9
|
const project = req.query.project || undefined;
|
|
9
10
|
try {
|
|
11
|
+
if (agent === 'openclaw') {
|
|
12
|
+
const projectCacheKey = `blocks:${agent}:${project || 'all'}`;
|
|
13
|
+
const cached = cache.get(projectCacheKey);
|
|
14
|
+
if (cached) {
|
|
15
|
+
res.json(cached);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const data = getOpenClawBlocksResponse({ project: project || null });
|
|
19
|
+
const validated = validateBlocks(data);
|
|
20
|
+
cache.set(projectCacheKey, validated);
|
|
21
|
+
res.json(validated);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
10
24
|
if (agent === 'codex') {
|
|
11
25
|
const projectCacheKey = `blocks:${agent}:${project || 'all'}`;
|
|
12
26
|
const cached = cache.get(projectCacheKey);
|
|
@@ -15,8 +29,9 @@ export async function getBlocks(req, res) {
|
|
|
15
29
|
return;
|
|
16
30
|
}
|
|
17
31
|
const data = getBlocksResponse({ project: project || null });
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
const validated = validateBlocks(data);
|
|
33
|
+
cache.set(projectCacheKey, validated);
|
|
34
|
+
res.json(validated);
|
|
20
35
|
return;
|
|
21
36
|
}
|
|
22
37
|
// Claude Code with project filter: use custom JSONL parser
|
|
@@ -2,6 +2,7 @@ import { runCcusage } from '../ccusage.js';
|
|
|
2
2
|
import { cache } from '../cache.js';
|
|
3
3
|
import { validateDaily } from '../../shared/schemas.js';
|
|
4
4
|
import { getDailyResponse } from '../codexParser.js';
|
|
5
|
+
import { getDailyResponse as getOpenClawDailyResponse } from '../openclawParser.js';
|
|
5
6
|
export async function getDaily(req, res) {
|
|
6
7
|
const agent = req.query.agent || 'claude';
|
|
7
8
|
const cacheKey = `daily:${agent}`;
|
|
@@ -16,6 +17,12 @@ export async function getDaily(req, res) {
|
|
|
16
17
|
cache.set(cacheKey, data);
|
|
17
18
|
res.json(data);
|
|
18
19
|
}
|
|
20
|
+
else if (agent === 'openclaw') {
|
|
21
|
+
const data = getOpenClawDailyResponse();
|
|
22
|
+
const validated = validateDaily(data);
|
|
23
|
+
cache.set(cacheKey, validated);
|
|
24
|
+
res.json(validated);
|
|
25
|
+
}
|
|
19
26
|
else {
|
|
20
27
|
const stdout = await runCcusage(['daily', '--breakdown']);
|
|
21
28
|
const data = JSON.parse(stdout);
|
|
@@ -2,6 +2,7 @@ import { runCcusage } from '../ccusage.js';
|
|
|
2
2
|
import { cache } from '../cache.js';
|
|
3
3
|
import { validateProjects } from '../../shared/schemas.js';
|
|
4
4
|
import { getProjectsResponse } from '../codexParser.js';
|
|
5
|
+
import { getProjectsResponse as getOpenClawProjectsResponse } from '../openclawParser.js';
|
|
5
6
|
export async function getProjects(req, res) {
|
|
6
7
|
const agent = req.query.agent || 'claude';
|
|
7
8
|
const cacheKey = `projects:${agent}`;
|
|
@@ -16,6 +17,12 @@ export async function getProjects(req, res) {
|
|
|
16
17
|
cache.set(cacheKey, data);
|
|
17
18
|
res.json(data);
|
|
18
19
|
}
|
|
20
|
+
else if (agent === 'openclaw') {
|
|
21
|
+
const data = getOpenClawProjectsResponse();
|
|
22
|
+
const validated = validateProjects(data);
|
|
23
|
+
cache.set(cacheKey, validated);
|
|
24
|
+
res.json(validated);
|
|
25
|
+
}
|
|
19
26
|
else {
|
|
20
27
|
const stdout = await runCcusage(['daily', '--instances', '--breakdown']);
|
|
21
28
|
const data = JSON.parse(stdout);
|