@ipv9/tokentracker-cli 0.39.40 → 0.39.41

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 (35) hide show
  1. package/README.md +5 -5
  2. package/dashboard/dist/assets/{Card-JS4gjUZf.js → Card-BwY_Qv6N.js} +1 -1
  3. package/dashboard/dist/assets/DashboardPage-C7b_GjA-.js +60 -0
  4. package/dashboard/dist/assets/{FadeIn-DvLfvwys.js → FadeIn-DiAafUDN.js} +1 -1
  5. package/dashboard/dist/assets/{IpCheckPage-Dcfc0Uts.js → IpCheckPage-CYgWVZJr.js} +1 -1
  6. package/dashboard/dist/assets/{LimitsPage-Vz-GVdi9.js → LimitsPage-C1y75ftN.js} +1 -1
  7. package/dashboard/dist/assets/{LocalOnlyNotice-8xF5couh.js → LocalOnlyNotice-B5_-ydeu.js} +1 -1
  8. package/dashboard/dist/assets/{PopoverPopup-7HrUzeRx.js → PopoverPopup-DDjjQ5CG.js} +1 -1
  9. package/dashboard/dist/assets/{Select-9XDsaO_C.js → Select-BPspkl9p.js} +1 -1
  10. package/dashboard/dist/assets/{SelectItemText-DE5Fs9p6.js → SelectItemText-CX0dWYB1.js} +1 -1
  11. package/dashboard/dist/assets/{SettingsPage-CMnfAptE.js → SettingsPage-9K5ST4D_.js} +1 -1
  12. package/dashboard/dist/assets/{SkillsPage-CbvLHN0L.js → SkillsPage-BpCSS7ls.js} +1 -1
  13. package/dashboard/dist/assets/{WidgetsPage-CeGQlyt7.js → WidgetsPage-aBisLWrM.js} +1 -1
  14. package/dashboard/dist/assets/{WrappedPage-Bux8qLUC.js → WrappedPage-BgkuBG_j.js} +1 -1
  15. package/dashboard/dist/assets/{arrow-up-right-B7rCoZdW.js → arrow-up-right-DPb2FRSP.js} +1 -1
  16. package/dashboard/dist/assets/{download-CtTYpbcx.js → download-CbZ8YL8m.js} +1 -1
  17. package/dashboard/dist/assets/{format-BuQKK6kQ.js → format-aRaCvnht.js} +1 -1
  18. package/dashboard/dist/assets/limitDisplay-1O9-AdgI.js +1 -0
  19. package/dashboard/dist/assets/{main-Ck4SUH4m.js → main-DRcFJLLD.js} +3 -3
  20. package/dashboard/dist/assets/mock-data-sZ3-GZV0.js +1 -0
  21. package/dashboard/dist/assets/{use-limits-display-prefs-BKWYPgeA.js → use-limits-display-prefs-2-qYvffB.js} +1 -1
  22. package/dashboard/dist/assets/{use-native-settings-D7Nj1dXR.js → use-native-settings-C1uhDKdL.js} +1 -1
  23. package/dashboard/dist/assets/{useCurrency-BX3pvyhh.js → useCurrency-BPCF4zUv.js} +1 -1
  24. package/dashboard/dist/index.html +1 -1
  25. package/package.json +3 -2
  26. package/src/commands/serve.js +121 -38
  27. package/src/lib/fs.js +149 -25
  28. package/src/lib/local-api.js +20 -2
  29. package/src/lib/pricing/curated-overrides.json +10 -3
  30. package/src/lib/pricing/index.js +197 -18
  31. package/src/lib/pricing/litellm-fetcher.js +6 -1
  32. package/src/lib/pricing/seed-snapshot.json +1 -1
  33. package/dashboard/dist/assets/DashboardPage-BoXhzJb1.js +0 -60
  34. package/dashboard/dist/assets/limitDisplay-WwuTdS_a.js +0 -1
  35. package/dashboard/dist/assets/mock-data-Bvbe8tYK.js +0 -1
@@ -0,0 +1 @@
1
+ import{t as A}from"./format-aRaCvnht.js";function ht(){if(typeof Intl>"u"||!Intl.DateTimeFormat)return null;try{return Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch{return null}}function kt(t=new Date){const e=t instanceof Date?t:new Date(t);if(!Number.isFinite(e.getTime()))return 0;const n=e.getTimezoneOffset();return Number.isFinite(n)?-n:0}function V(t){if(!Number.isFinite(t)||t===0)return"UTC";const e=t>=0?"+":"-",n=Math.abs(t),o=String(Math.floor(n/60)).padStart(2,"0"),a=String(n%60).padStart(2,"0");return`UTC${e}${o}:${a}`}function bt({timeZone:t,offsetMinutes:e}={}){const n=V(e);return t&&t!=="UTC"?`${t} (${n})`:n}function Dt({timeZone:t,offsetMinutes:e}={}){return Number.isFinite(e)?V(e):t||"UTC"}function wt({timeZone:t,offsetMinutes:e,date:n=new Date}={}){const o=n instanceof Date?n:new Date(n);if(!Number.isFinite(o.getTime()))return"";if(t&&typeof Intl<"u"&&Intl.DateTimeFormat)try{const s=new Intl.DateTimeFormat("en-CA",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(o).reduce((m,c)=>(c.type&&c.value&&(m[c.type]=c.value),m),{}),i=s.year,u=s.month,_=s.day;if(i&&u&&_)return`${i}-${u}-${_}`}catch{}if(typeof e=="number"&&Number.isFinite(e)){const a=new Date(o.getTime()+e*6e4);return Z(a)}return G(o)}function J({timeZone:t,offsetMinutes:e,date:n=new Date}={}){const o=n instanceof Date?n:new Date(n);if(!Number.isFinite(o.getTime()))return null;if(t&&typeof Intl<"u"&&Intl.DateTimeFormat)try{const s=new Intl.DateTimeFormat("en-CA",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(o).reduce((m,c)=>(c.type&&c.value&&(m[c.type]=c.value),m),{}),i=Number(s.year),u=Number(s.month),_=Number(s.day);if(Number.isFinite(i)&&Number.isFinite(u)&&Number.isFinite(_))return{year:i,month:u,day:_}}catch{}if(typeof e=="number"&&Number.isFinite(e)){const a=new Date(o.getTime()+e*6e4);return{year:a.getUTCFullYear(),month:a.getUTCMonth()+1,day:a.getUTCDate()}}return{year:o.getFullYear(),month:o.getMonth()+1,day:o.getDate()}}function yt({timeZone:t,offsetMinutes:e}={}){return t?`tz:${t}`:typeof e=="number"&&Number.isFinite(e)?`offset:${Math.trunc(e)}`:"utc"}function Z(t){const e=t.getUTCFullYear(),n=String(t.getUTCMonth()+1).padStart(2,"0"),o=String(t.getUTCDate()).padStart(2,"0");return`${e}-${n}-${o}`}function G(t){const e=t.getFullYear(),n=String(t.getMonth()+1).padStart(2,"0"),o=String(t.getDate()).padStart(2,"0");return`${e}-${n}-${o}`}function f(t){return new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate())).toISOString().slice(0,10)}function F(t){const e=t instanceof Date?t:new Date(t);if(!Number.isFinite(e.getTime()))return"";const n=e.getFullYear(),o=String(e.getMonth()+1).padStart(2,"0"),a=String(e.getDate()).padStart(2,"0");return`${n}-${o}-${a}`}function Q(t){if(!t)return null;const e=String(t).trim(),n=e.split("-");if(n.length!==3)return null;const o=Number(n[0]),a=Number(n[1])-1,r=Number(n[2]);if(!Number.isFinite(o)||!Number.isFinite(a)||!Number.isFinite(r))return null;const s=new Date(Date.UTC(o,a,r));return Number.isFinite(s.getTime())&&f(s)===e?s:null}function X(t){if(!t)return"";const e=t.year,n=String(t.month).padStart(2,"0"),o=String(t.day).padStart(2,"0");return!e||!n||!o?"":`${e}-${n}-${o}`}function tt(t,{timeZone:e,offsetMinutes:n,now:o}={}){const a=o instanceof Date&&Number.isFinite(o.getTime())?o:new Date,r=J({timeZone:e,offsetMinutes:n,date:a});if(!r){const u=F(new Date);return{from:u,to:u}}const s=X(r),i=Q(s);if(!i)return{from:s,to:s};if(t==="day")return{from:s,to:s};if(t==="24h"){const u=new Date(i);return u.setUTCDate(u.getUTCDate()-1),{from:f(u),to:s}}if(t==="week"){const u=new Date(i);return u.setUTCDate(u.getUTCDate()-6),{from:f(u),to:s}}if(t==="month"){const u=new Date(i);return u.setUTCDate(u.getUTCDate()-29),{from:f(u),to:s}}if(t==="total"){const u=new Date(Date.UTC(i.getUTCFullYear(),i.getUTCMonth()-23,1));return{from:f(u),to:s}}return tt("week")}function P(t){if(typeof t!="string")return null;const e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t.trim());if(!e)return null;const n=Number(e[1]),o=Number(e[2])-1,a=Number(e[3]),r=new Date(Date.UTC(n,o,a));return Number.isFinite(r.getTime())&&f(r)===t.trim()?r:null}function v(t,e){return new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()+e))}function et(t,e){const n=Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())-Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate());return Math.floor(n/864e5)}function R(t,e){if(!Array.isArray(t)||t.length===0)return 0;const n=t.length,o=(n-1)*e,a=Math.floor(o),r=o-a,s=t[a]??t[n-1],i=t[Math.min(n-1,a+1)]??t[n-1];return Math.round(s+(i-s)*r)}function nt(t){return t<=0?0:t>=4?4:t}function q({weeks:t=52,now:e,weekStartsOn:n="sun"}={}){const o=e instanceof Date&&Number.isFinite(e.getTime())?e:new Date,a=F(o),r=P(a)||new Date(Date.UTC(o.getFullYear(),o.getMonth(),o.getDate())),s=n==="mon"?1:0,i=r.getUTCDay(),u=v(r,-((i-s+7)%7)),_=v(u,-7*(Math.max(1,t)-1));return{from:f(_),to:a}}function ot({dailyRows:t,weeks:e=52,to:n,weekStartsOn:o="sun"}={}){const a=P(n)||new Date(Date.UTC(new Date().getFullYear(),new Date().getMonth(),new Date().getDate())),{from:r}=q({weeks:e,now:a,weekStartsOn:o}),s=P(r)||v(a,-(e*7-1)),i=(()=>{const h=(s.getUTCDay()-(o==="mon"?1:0)+7)%7;return v(s,-h)})(),u=new Map;for(const l of Array.isArray(t)?t:[]){const d=typeof l?.day=="string"?l.day:null;if(!d)continue;const h=A(l?.conversation_count)??0,y=A(l?.billable_total_tokens??l?.total_tokens)??0;u.set(d,{value:Math.max(0,h),tokens:Math.max(0,y),models:l?.models||null})}const _=et(i,a)+1,m=Math.ceil(_/7),c=[];for(let l=0;l<_;l++){const d=v(i,l),h=f(d),y=u.get(h),b=y?y.value:0;b>0&&c.push(b)}c.sort((l,d)=>l-d);const p=R(c,.5),C=R(c,.75),w=R(c,.9);function S(l){return!l||l<=0?0:l<=p?1:l<=C?2:l<=w?3:4}const g=[];for(let l=0;l<m;l++){const d=[];for(let h=0;h<7;h++){const y=l*7+h,b=v(i,y);if(b.getTime()>a.getTime()){d.push(null);continue}const T=f(b),D=u.get(T),M=D?D.value:0,N=D?D.tokens:0,x=D?D.models:null;d.push({day:T,value:M,total_tokens:N,level:nt(S(M)),models:x})}g.push(d)}const k=g.length>e?g.slice(g.length-e):g;return{from:r,to:f(a),weeks:k,thresholds:{t1:p,t2:C,t3:w}}}function at({dailyRows:t,to:e}={}){const n=P(e)||new Date(Date.UTC(new Date().getFullYear(),new Date().getMonth(),new Date().getDate())),o=new Map;for(const r of Array.isArray(t)?t:[]){const s=typeof r?.day=="string"?r.day:null;if(!s)continue;const i=A(r?.conversation_count??r?.billable_total_tokens??r?.total_tokens)??0;o.set(s,Math.max(0,i))}let a=0;for(let r=0;r<370;r++){const s=f(v(n,-r));if((o.get(s)??0)>0){a+=1;continue}break}return a}const Y={},H="tokentracker",st=["tokentracker/tokentracker","spacedriveapp/spacedrive","acme/alpha","acme/beta","neo/nebula","signal/flux","matrix/terminal","orbit/atlas","lumen/core","delta/horizon"];function rt(){if(typeof window<"u"){const t=new URLSearchParams(window.location.search),e=String(t.get("mock")||"").toLowerCase();if(e==="0"||e==="false"||e==="off"||e==="no")return!1;if(e==="1"||e==="true"||e==="on"||e==="yes")return!0}if(typeof import.meta<"u"&&Y){const t="".toLowerCase();if(t==="1"||t==="true")return!0}return!1}function it(){if(typeof import.meta<"u"&&Y){const t="".trim();if(t)return t;const e="".trim();if(e)return e}if(typeof window<"u"){const t=new URLSearchParams(window.location.search),e=String(t.get("mock_now")||"").trim();if(e)return e;const n=String(t.get("mock_today")||"").trim();if(n)return n}return""}function ut(t){if(!t)return null;const e=String(t).trim();if(!e)return null;if(/^\d{4}-\d{2}-\d{2}$/.test(e)){const[o,a,r]=e.split("-").map(Number);if(!Number.isFinite(o)||!Number.isFinite(a)||!Number.isFinite(r))return null;const s=new Date(o,a-1,r,12,0,0);return Number.isFinite(s.getTime())?s:null}const n=new Date(e);return Number.isFinite(n.getTime())?n:null}function Mt(){return rt()?ut(it()):null}function ct(){if(typeof import.meta<"u"&&Y){const t="".trim();if(t)return t}if(typeof window<"u"){const t=new URLSearchParams(window.location.search),e=String(t.get("mock_seed")||"").trim();if(e)return e}return H}function lt(){if(typeof import.meta<"u"&&Y){const t="".trim(),e=Number(t);if(Number.isFinite(e)&&e>0)return Math.floor(e)}if(typeof window<"u"){const t=new URLSearchParams(window.location.search),e=String(t.get("mock_missing")||"").trim(),n=Number(e);if(Number.isFinite(n)&&n>0)return Math.floor(n)}return 0}function I(t){return(t==null?ct():String(t)).trim()||H}function L(t){let e=2166136261;for(let n=0;n<t.length;n+=1)e^=t.charCodeAt(n),e=Math.imul(e,16777619);return e>>>0}function U(t){if(typeof t!="string")return null;const e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t.trim());if(!e)return null;const n=Number(e[1]),o=Number(e[2])-1,a=Number(e[3]),r=new Date(Date.UTC(n,o,a));return Number.isFinite(r.getTime())&&f(r)===t.trim()?r:null}function K(t,e){return new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()+e))}function W(t,e){return new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth()+e,1))}function _t(t){return`${t.getUTCFullYear()}-${String(t.getUTCMonth()+1).padStart(2,"0")}`}function $({from:t,to:e,seed:n}){const o=U(F(new Date))||new Date,a=U(t)||o,r=U(e)||a,s=[],i=I(n),u=Math.floor((Date.UTC(r.getUTCFullYear(),r.getUTCMonth(),r.getUTCDate())-Date.UTC(a.getUTCFullYear(),a.getUTCMonth(),a.getUTCDate()))/864e5)+1;for(let _=0;_<u;_+=1){const m=K(a,_),c=f(m),p=L(`${i}:${c}`),C=p%1e3/1e3,w=.6+.4*Math.sin(_/6*Math.PI*.5),S=m.getUTCDay()===0||m.getUTCDay()===6?.65:1,g=18e3+Math.round(12e3*C),k=Math.max(0,Math.round(g*w*S)),l=Math.round(k*.46),d=Math.round(k*.34),h=Math.round(k*.14),y=Math.max(0,k-l-d-h),b={};if(k>0){const T=["gpt-4o","claude-3.5-sonnet","gemini-1.5-pro","deepseek-coder"],D=1+p%3;let M=k;for(let N=0;N<D;N++){const x=T[(p+N)%T.length];if(N===D-1)b[x]=M;else{const O=Math.round(M*(.3+.4*((p+N*17)%10)/10));b[x]=O,M-=O}}}s.push({day:c,total_tokens:k,billable_total_tokens:k,input_tokens:l,output_tokens:d,cached_input_tokens:h,reasoning_output_tokens:y,conversation_count:1+p%5,models:b})}return s}function mt({day:t,seed:e}){const n=U(t)||U(F(new Date))||new Date,o=f(n),a=I(e),r=Array.from({length:48},(i,u)=>{const _=Math.floor(u/2),m=u%2===0?0:30,c=L(`${a}:${o}:${_}:${m}`),p=c%1e3/1e3,C=(_+m/60)/24,w=.4+.6*Math.sin(C*Math.PI*2),S=900+Math.round(700*p),g=Math.max(0,Math.round(S*w)),k=Math.round(g*.46),l=Math.round(g*.34),d=Math.round(g*.14),h=Math.max(0,g-k-l-d),y={};if(g>0){const b=["gpt-4o","claude-3.5-sonnet","gemini-1.5-pro","deepseek-coder"],T=1+c%2;let D=g;for(let M=0;M<T;M++){const N=b[(c+M)%b.length];if(M===T-1)y[N]=D;else{const x=Math.round(D*(.3+.4*((c+M*17)%10)/10));y[N]=x,D-=x}}}return{hour:`${o}T${String(_).padStart(2,"0")}:${String(m).padStart(2,"0")}:00`,total_tokens:g,billable_total_tokens:g,input_tokens:k,output_tokens:l,cached_input_tokens:d,reasoning_output_tokens:h,models:y}}),s=Math.max(0,Math.min(48,lt()));if(s>0){const i=Date.now(),u=r.map((c,p)=>{const C=Date.parse(c.hour);return Number.isFinite(C)&&C<=i?{index:p,ts:C}:null}).filter(Boolean),_=Math.max(0,u.length-s),m=u.slice(_);for(const c of m){const p=r[c.index];p&&(r[c.index]={...p,missing:!0})}}return r}function dt({months:t=24,to:e,seed:n}){const o=U(e)||U(F(new Date))||new Date,a=new Date(Date.UTC(o.getUTCFullYear(),o.getUTCMonth(),1)),r=I(n),s=[];for(let i=t-1;i>=0;i-=1){const u=W(a,-i),_=_t(u),m=L(`${r}:${_}`),c=m%1e3/1e3,p=.7+.3*Math.sin(s.length/6*Math.PI*.5),C=36e4+Math.round(2e5*c),w=Math.max(0,Math.round(C*p)),S=Math.round(w*.46),g=Math.round(w*.34),k=Math.round(w*.14),l=Math.max(0,w-S-g-k),d={};if(w>0){const h=["gpt-4o","claude-3.5-sonnet","gemini-1.5-pro","deepseek-coder"],y=1+m%3;let b=w;for(let T=0;T<y;T++){const D=h[(m+T)%h.length];if(T===y-1)d[D]=b;else{const M=Math.round(b*(.3+.4*((m+T*17)%10)/10));d[D]=M,b-=M}}}s.push({month:_,total_tokens:w,billable_total_tokens:w,input_tokens:S,output_tokens:g,cached_input_tokens:k,reasoning_output_tokens:l,models:d})}return s}function B(t){return t.reduce((e,n)=>(e.total_tokens+=Number(n.total_tokens||0),e.billable_total_tokens+=Number(n.billable_total_tokens||n.total_tokens||0),e.input_tokens+=Number(n.input_tokens||0),e.output_tokens+=Number(n.output_tokens||0),e.cached_input_tokens+=Number(n.cached_input_tokens||0),e.reasoning_output_tokens+=Number(n.reasoning_output_tokens||0),e.conversation_count+=Number(n.conversation_count||0),e),{total_tokens:0,billable_total_tokens:0,input_tokens:0,output_tokens:0,cached_input_tokens:0,reasoning_output_tokens:0,conversation_count:0})}function ft(t,e=1.75){const n=Number(t||0);return!Number.isFinite(n)||n<=0?"0.000000":(n*e/1e6).toFixed(6)}function j(t,e){const n=Number.isFinite(e)?e:0;return{total_tokens:Math.max(0,Math.round(t.total_tokens*n)),billable_total_tokens:Math.max(0,Math.round((t.billable_total_tokens??t.total_tokens)*n)),input_tokens:Math.max(0,Math.round(t.input_tokens*n)),output_tokens:Math.max(0,Math.round(t.output_tokens*n)),cached_input_tokens:Math.max(0,Math.round(t.cached_input_tokens*n)),reasoning_output_tokens:Math.max(0,Math.round(t.reasoning_output_tokens*n))}}function z(t){return{...t,total_cost_usd:ft(t.total_tokens)}}function Tt({from:t,to:e,seed:n}={}){const o=$({from:t,to:e,seed:n});return{from:t,to:e,data:o}}function Ut({day:t,seed:e}={}){const n=U(t)||new Date,o=f(n),a=mt({day:o,seed:e});return{day:o,data:a}}function Ct({months:t=24,to:e,seed:n}={}){const o=U(e)||U(F(new Date))||new Date,a=f(o),r=dt({months:t,to:a,seed:n}),s=W(new Date(Date.UTC(o.getUTCFullYear(),o.getUTCMonth(),1)),-(t-1));return{from:f(s),to:a,months:t,data:r}}function Nt({from:t,to:e,seed:n,rolling:o=!0}={}){const a=$({from:t,to:e,seed:n}),r=B(a),s=z(r),i=o?gt({to:e,seed:n}):null;return{from:t,to:e,days:a.length,totals:s,...i?{rolling:i}:{}}}function gt({to:t,seed:e}={}){const n=U(t)||U(F(new Date))||new Date,o=f(n),a=f(K(n,-6)),r=f(K(n,-29)),s=$({from:a,to:o,seed:e}),i=$({from:r,to:o,seed:e});return{last_7d:E({rows:s,from:a,to:o}),last_30d:E({rows:i,from:r,to:o})}}function E({rows:t,from:e,to:n}={}){const o=B(t||[]),a=(t||[]).filter(s=>Number(s.billable_total_tokens??s.total_tokens)>0).length,r=a>0?Math.floor(o.billable_total_tokens/a):0;return{from:e,to:n,totals:{billable_total_tokens:o.billable_total_tokens,conversation_count:o.conversation_count},active_days:a,avg_per_active_day:r}}function Ft({seed:t,limit:e=3}={}){const n=I(t),o=st.map((a,r)=>{const i=12e4+L(`${n}:${a}`)%9e5,u=r%4*4200,_=Math.max(0,i+u),m=Math.max(0,Math.round(_*.96));return{project_key:a,project_ref:`https://github.com/${a}`,total_tokens:String(_),billable_total_tokens:String(m)}}).sort((a,r)=>Number(r.billable_total_tokens)-Number(a.billable_total_tokens)).slice(0,Math.max(1,Math.min(10,Math.floor(Number(e)||3))));return{generated_at:new Date().toISOString(),entries:o}}function St({weeks:t=52,to:e,weekStartsOn:n="sun",seed:o}={}){const a=q({weeks:t,now:U(e)||U(F(new Date))||new Date,weekStartsOn:n}),r=$({from:a.from,to:a.to,seed:o});return{...ot({dailyRows:r,weeks:t,to:a.to,weekStartsOn:n}),week_starts_on:n,active_days:r.filter(i=>Number(i.billable_total_tokens??i.total_tokens)>0).length,streak_days:at({dailyRows:r,to:a.to})}}function xt({from:t,to:e,seed:n}={}){const o=$({from:t,to:e,seed:n}),a=B(o),s=[{source:"codex",weight:.7,models:[{model:"gpt-5.2-codex",model_id:"gpt-5.2-codex",weight:.2},{model:"unknown",model_id:"unknown",weight:.8}]},{source:"claude",weight:.2,models:[{model:"claude-3.5",model_id:"claude-3.5",weight:.3},{model:"unknown",model_id:"unknown",weight:.7}]},{source:"every-code",weight:.1,models:[{model:"unknown",model_id:"unknown",weight:1}]}].map(i=>{const u=j(a,i.weight),_=i.models.map(m=>{const c=j(u,m.weight);return{model:m.model,model_id:m.model_id||m.model,totals:z(c)}});return{source:i.source,totals:z(u),models:_}});return{from:t,to:e,days:o.length,sources:s,pricing:{model:"mock",pricing_mode:"add",source:"mock",effective_from:F(new Date),rates_per_million_usd:{input:"1.750000",cached_input:"0.175000",output:"14.000000",reasoning_output:"14.000000"}}}}function vt(){return{fetched_at:"2025-12-31T12:00:00Z",claude:{configured:!0,plan_label:"Max",five_hour:{utilization:92},seven_day:{utilization:68}},codex:{configured:!0,plan_label:"Pro",primary_window:{used_percent:34},secondary_window:{used_percent:78}},cursor:{configured:!1},gemini:{configured:!1},kimi:{configured:!1},zai:{configured:!1},kiro:{configured:!1},copilot:{configured:!1},antigravity:{configured:!1}}}function $t({from:t,to:e,source:n="claude"}={}){if(n==="codex")return{from:t,to:e,source:"codex",scope:"supported",totals:{input_tokens:142e4,cached_input_tokens:588e4,cache_creation_input_tokens:92e4,output_tokens:256e4,reasoning_output_tokens:61e4,total_tokens:1078e4},session_count:188,message_count:5436,tool_calls_breakdown:{categories:[{name:"Execution",calls:742,totals:{input_tokens:32e4,cached_input_tokens:104e4,cache_creation_input_tokens:11e4,output_tokens:46e4,reasoning_output_tokens:12e4,total_tokens:193e4},tools:[{name:"exec_command",calls:510,totals:{input_tokens:25e4,cached_input_tokens:82e4,cache_creation_input_tokens:8e4,output_tokens:37e4,reasoning_output_tokens:9e4,total_tokens:152e4}},{name:"write_stdin",calls:232,totals:{input_tokens:7e4,cached_input_tokens:22e4,cache_creation_input_tokens:3e4,output_tokens:9e4,reasoning_output_tokens:3e4,total_tokens:41e4}}]},{name:"Browser",calls:406,totals:{input_tokens:21e4,cached_input_tokens:88e4,cache_creation_input_tokens:9e4,output_tokens:42e4,reasoning_output_tokens:1e5,total_tokens:17e5},tools:[{name:"take_snapshot",calls:180,totals:{input_tokens:1e5,cached_input_tokens:4e5,cache_creation_input_tokens:4e4,output_tokens:18e4,reasoning_output_tokens:4e4,total_tokens:76e4}},{name:"click",calls:126,totals:{input_tokens:6e4,cached_input_tokens:25e4,cache_creation_input_tokens:3e4,output_tokens:12e4,reasoning_output_tokens:35e3,total_tokens:495e3}},{name:"evaluate_script",calls:100,totals:{input_tokens:5e4,cached_input_tokens:23e4,cache_creation_input_tokens:2e4,output_tokens:12e4,reasoning_output_tokens:25e3,total_tokens:445e3}}]}],tools_total:363e4,privacy:{includes_inputs:!1,note:"Aggregated tool names only; no tool arguments or outputs are included."}},exec_command_breakdown:{by_type:[{name:"test",calls:168,failures:14,duration_ms:482e3,max_duration_ms:18400,output_chars:45e4,output_lines:8600,totals:{input_tokens:9e4,cached_input_tokens:32e4,output_tokens:16e4,reasoning_output_tokens:42e3,total_tokens:612e3}},{name:"build",calls:116,failures:9,duration_ms:392e3,max_duration_ms:22100,output_chars:31e4,output_lines:5200,totals:{input_tokens:7e4,cached_input_tokens:24e4,output_tokens:13e4,reasoning_output_tokens:3e4,total_tokens:47e4}}],by_exit:[{name:"completed:0",calls:438,failures:0,duration_ms:104e4,max_duration_ms:22100,output_chars:83e4,output_lines:13100,totals:{input_tokens:22e4,cached_input_tokens:78e4,output_tokens:35e4,reasoning_output_tokens:82e3,total_tokens:1432e3}},{name:"completed:1",calls:72,failures:72,duration_ms:155e3,max_duration_ms:11800,output_chars:12e4,output_lines:2300,totals:{input_tokens:3e4,cached_input_tokens:11e4,output_tokens:55e3,reasoning_output_tokens:8e3,total_tokens:195e3}}]}};if(n!=="claude")return{from:t,to:e,source:n,scope:"unsupported",totals:{input_tokens:0,cached_input_tokens:0,cache_creation_input_tokens:0,output_tokens:0,reasoning_output_tokens:0,total_tokens:0},categories:["system_prefix","conversation_history","user_input","tool_calls","subagents","reasoning","assistant_response"].map(s=>({key:s,totals:{input_tokens:0,cached_input_tokens:0,cache_creation_input_tokens:0,output_tokens:0,reasoning_output_tokens:0,total_tokens:0},percent:0})),session_count:0,message_count:0};const o=[{key:"system_prefix",total:12896539},{key:"conversation_history",total:320207200},{key:"user_input",total:998025},{key:"tool_calls",total:4808739},{key:"subagents",total:1022828},{key:"reasoning",total:11073118},{key:"assistant_response",total:1289174}],a=o.reduce((s,i)=>s+i.total,0),r=o.map(({key:s,total:i})=>({key:s,totals:{input_tokens:s==="user_input"?i:0,cached_input_tokens:s==="conversation_history"?Math.round(i*.95):0,cache_creation_input_tokens:s==="system_prefix"?i:s==="conversation_history"?Math.round(i*.05):0,output_tokens:["tool_calls","subagents","reasoning","assistant_response"].includes(s)?i:0,reasoning_output_tokens:s==="reasoning"?i:0,total_tokens:i},percent:Number((i/a*100).toFixed(2))}));return{from:t,to:e,source:"claude",scope:"supported",totals:{input_tokens:998025,cached_input_tokens:Math.round(320207200*.95),cache_creation_input_tokens:12896539+Math.round(320207200*.05),output_tokens:18193859,reasoning_output_tokens:11073118,total_tokens:a},categories:r,session_count:312,message_count:12046}}export{yt as a,ot as b,at as c,wt as d,kt as e,f,q as g,ht as h,rt as i,Mt as j,tt as k,bt as l,Dt as m,Tt as n,Nt as o,xt as p,Ut as q,Ft as r,Ct as s,St as t,vt as u,$t as v};
@@ -1 +1 @@
1
- import{r as o,$ as I,a0 as h,a1 as C,a2 as R}from"./main-Ck4SUH4m.js";const d=["claude","codex","cursor","gemini","kimi","zai","kiro","copilot","antigravity"],Y={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",zai:"Z.AI",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},P={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kimi:"/brand-logos/kimi.svg",zai:null,kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},w="tt.limits.providerOrder",m="tt.limits.providerVisibility",S="tt.limits.displayMode",p="limitsDisplayMode",f=Object.freeze({USED:"used",REMAINING:"remaining"}),E=new Set(Object.values(f));function O(){if(typeof window>"u")return[...d];try{const n=window.localStorage.getItem(w);if(!n)return[...d];const a=JSON.parse(n);if(!Array.isArray(a))return[...d];const r=a.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function D(){const n=Object.fromEntries(d.map(a=>[a,!0]));if(typeof window>"u")return n;try{const a=window.localStorage.getItem(m);if(!a)return n;const r=JSON.parse(a);if(!r||typeof r!="object")return n;const c={...n};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return n}}function v(){if(typeof window>"u")return f.USED;try{const n=window.localStorage.getItem(S);return E.has(n)?n:f.USED}catch{return f.USED}}function T(){const[n,a]=o.useState(O),[r,c]=o.useState(D),[u,l]=o.useState(v),g=o.useCallback(t=>{E.has(t)&&(l(t),I()&&h(p,t))},[]);o.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(w,JSON.stringify(n))}catch{}},[n]),o.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(m,JSON.stringify(r))}catch{}},[r]),o.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(S,u)}catch{}},[u]),o.useEffect(()=>{if(!I())return;const t=C(e=>{const i=e?.[p];E.has(i)&&l(i)});return R(),t},[]),o.useEffect(()=>{if(typeof window>"u")return;const t=e=>{e.key===w&&a(O()),e.key===m&&c(D()),e.key===S&&l(v())};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[]);const k=o.useCallback(t=>{c(e=>({...e,[t]:!e[t]}))},[]),x=o.useCallback(t=>{a(e=>{const i=e.indexOf(t);if(i<=0)return e;const s=[...e];return[s[i-1],s[i]]=[s[i],s[i-1]],s})},[]),M=o.useCallback(t=>{a(e=>{const i=e.indexOf(t);if(i<0||i>=e.length-1)return e;const s=[...e];return[s[i],s[i+1]]=[s[i+1],s[i]],s})},[]),_=o.useCallback((t,e)=>{t!==e&&a(i=>{const s=i.indexOf(t),b=i.indexOf(e);if(s<0||b<0)return i;const y=[...i],[N]=y.splice(s,1);return y.splice(b,0,N),y})},[]),L=o.useCallback(()=>{a([...d]),c(Object.fromEntries(d.map(t=>[t,!0]))),g(f.USED)},[g]),A=o.useMemo(()=>n.filter(t=>r[t]!==!1),[n,r]);return{order:n,visibility:r,displayMode:u,setDisplayMode:g,visibleOrdered:A,toggle:k,moveUp:x,moveDown:M,moveToward:_,reset:L}}export{f as L,P as a,Y as b,T as u};
1
+ import{r as o,$ as I,a0 as h,a1 as C,a2 as R}from"./main-DRcFJLLD.js";const d=["claude","codex","cursor","gemini","kimi","zai","kiro","copilot","antigravity"],Y={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",zai:"Z.AI",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},P={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kimi:"/brand-logos/kimi.svg",zai:null,kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},w="tt.limits.providerOrder",m="tt.limits.providerVisibility",S="tt.limits.displayMode",p="limitsDisplayMode",f=Object.freeze({USED:"used",REMAINING:"remaining"}),E=new Set(Object.values(f));function O(){if(typeof window>"u")return[...d];try{const n=window.localStorage.getItem(w);if(!n)return[...d];const a=JSON.parse(n);if(!Array.isArray(a))return[...d];const r=a.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function D(){const n=Object.fromEntries(d.map(a=>[a,!0]));if(typeof window>"u")return n;try{const a=window.localStorage.getItem(m);if(!a)return n;const r=JSON.parse(a);if(!r||typeof r!="object")return n;const c={...n};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return n}}function v(){if(typeof window>"u")return f.USED;try{const n=window.localStorage.getItem(S);return E.has(n)?n:f.USED}catch{return f.USED}}function T(){const[n,a]=o.useState(O),[r,c]=o.useState(D),[u,l]=o.useState(v),g=o.useCallback(t=>{E.has(t)&&(l(t),I()&&h(p,t))},[]);o.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(w,JSON.stringify(n))}catch{}},[n]),o.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(m,JSON.stringify(r))}catch{}},[r]),o.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(S,u)}catch{}},[u]),o.useEffect(()=>{if(!I())return;const t=C(e=>{const i=e?.[p];E.has(i)&&l(i)});return R(),t},[]),o.useEffect(()=>{if(typeof window>"u")return;const t=e=>{e.key===w&&a(O()),e.key===m&&c(D()),e.key===S&&l(v())};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[]);const k=o.useCallback(t=>{c(e=>({...e,[t]:!e[t]}))},[]),x=o.useCallback(t=>{a(e=>{const i=e.indexOf(t);if(i<=0)return e;const s=[...e];return[s[i-1],s[i]]=[s[i],s[i-1]],s})},[]),M=o.useCallback(t=>{a(e=>{const i=e.indexOf(t);if(i<0||i>=e.length-1)return e;const s=[...e];return[s[i],s[i+1]]=[s[i+1],s[i]],s})},[]),_=o.useCallback((t,e)=>{t!==e&&a(i=>{const s=i.indexOf(t),b=i.indexOf(e);if(s<0||b<0)return i;const y=[...i],[N]=y.splice(s,1);return y.splice(b,0,N),y})},[]),L=o.useCallback(()=>{a([...d]),c(Object.fromEntries(d.map(t=>[t,!0]))),g(f.USED)},[g]),A=o.useMemo(()=>n.filter(t=>r[t]!==!1),[n,r]);return{order:n,visibility:r,displayMode:u,setDisplayMode:g,visibleOrdered:A,toggle:k,moveUp:x,moveDown:M,moveToward:_,reset:L}}export{f as L,P as a,Y as b,T as u};
@@ -1 +1 @@
1
- import{j as a,x,r as n,b0 as g,b1 as m,a1 as b,a2 as u,a0 as f,a$ as h}from"./main-Ck4SUH4m.js";import{C as y}from"./Card-JS4gjUZf.js";function k({checked:s,onChange:t,disabled:e,ariaLabel:i}){return a.jsx("button",{type:"button",role:"switch","aria-checked":s,"aria-label":i,onClick:t,disabled:e,className:x("relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-oai-brand-500 disabled:opacity-50 disabled:cursor-not-allowed",s?"bg-oai-brand-500":"bg-oai-gray-300 dark:bg-oai-gray-700"),children:a.jsx("span",{className:x("inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",s?"translate-x-[18px]":"translate-x-[3px]")})})}function p({label:s,hint:t,control:e}){return a.jsxs("div",{className:"flex items-center justify-between gap-4 py-3",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("div",{className:"text-sm text-oai-gray-900 dark:text-oai-gray-200",children:s}),t?a.jsx("div",{className:"mt-0.5 text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),a.jsx("div",{className:"shrink-0",children:e})]})}function N({title:s,subtitle:t,action:e,children:i}){return a.jsxs(y,{children:[a.jsxs("div",{className:"mb-3 flex items-start justify-between gap-4",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h2",{className:"text-sm font-medium text-oai-gray-500 dark:text-oai-gray-300 uppercase tracking-wide",children:s}),t?a.jsx("p",{className:"mt-1 truncate text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),e?a.jsx("div",{className:"shrink-0",children:e}):null]}),a.jsx("div",{className:"-mb-3 divide-y divide-oai-gray-200/60 dark:divide-oai-gray-800/60",children:i})]})}function w({options:s,value:t,onChange:e}){return a.jsx("div",{className:"inline-flex items-center rounded-lg border border-oai-gray-200 bg-oai-gray-50 p-0.5 dark:border-oai-gray-800 dark:bg-oai-gray-900",children:s.map(({value:i,label:d,Icon:l})=>{const r=t===i;return a.jsxs("button",{type:"button",onClick:()=>e(i),"aria-pressed":r,className:x("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",r?"bg-white text-oai-black shadow-sm dark:bg-oai-gray-800 dark:text-white":"text-oai-gray-500 hover:text-oai-black dark:text-oai-gray-400 dark:hover:text-white"),children:[l?a.jsx(l,{className:"h-3.5 w-3.5","aria-hidden":!0}):null,a.jsx("span",{children:d})]},i)})})}function S(){const[s,t]=n.useState(null),e=g()&&m();n.useEffect(()=>{if(!e)return;const r=b(o=>t(o));return u(),r},[e]);const i=n.useCallback((r,o)=>{e&&(t(c=>c&&{...c,[r]:o}),f(r,o))},[e]),d=n.useCallback(r=>{e&&h(r)},[e]),l=n.useCallback(()=>{e&&u()},[e]);return{available:e,settings:s,setSetting:i,runAction:d,refresh:l}}export{N as S,k as T,p as a,w as b,S as u};
1
+ import{j as a,x,r as n,b0 as g,b1 as m,a1 as b,a2 as u,a0 as f,a$ as h}from"./main-DRcFJLLD.js";import{C as y}from"./Card-BwY_Qv6N.js";function k({checked:s,onChange:t,disabled:e,ariaLabel:i}){return a.jsx("button",{type:"button",role:"switch","aria-checked":s,"aria-label":i,onClick:t,disabled:e,className:x("relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-oai-brand-500 disabled:opacity-50 disabled:cursor-not-allowed",s?"bg-oai-brand-500":"bg-oai-gray-300 dark:bg-oai-gray-700"),children:a.jsx("span",{className:x("inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",s?"translate-x-[18px]":"translate-x-[3px]")})})}function p({label:s,hint:t,control:e}){return a.jsxs("div",{className:"flex items-center justify-between gap-4 py-3",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("div",{className:"text-sm text-oai-gray-900 dark:text-oai-gray-200",children:s}),t?a.jsx("div",{className:"mt-0.5 text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),a.jsx("div",{className:"shrink-0",children:e})]})}function N({title:s,subtitle:t,action:e,children:i}){return a.jsxs(y,{children:[a.jsxs("div",{className:"mb-3 flex items-start justify-between gap-4",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h2",{className:"text-sm font-medium text-oai-gray-500 dark:text-oai-gray-300 uppercase tracking-wide",children:s}),t?a.jsx("p",{className:"mt-1 truncate text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),e?a.jsx("div",{className:"shrink-0",children:e}):null]}),a.jsx("div",{className:"-mb-3 divide-y divide-oai-gray-200/60 dark:divide-oai-gray-800/60",children:i})]})}function w({options:s,value:t,onChange:e}){return a.jsx("div",{className:"inline-flex items-center rounded-lg border border-oai-gray-200 bg-oai-gray-50 p-0.5 dark:border-oai-gray-800 dark:bg-oai-gray-900",children:s.map(({value:i,label:d,Icon:l})=>{const r=t===i;return a.jsxs("button",{type:"button",onClick:()=>e(i),"aria-pressed":r,className:x("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",r?"bg-white text-oai-black shadow-sm dark:bg-oai-gray-800 dark:text-white":"text-oai-gray-500 hover:text-oai-black dark:text-oai-gray-400 dark:hover:text-white"),children:[l?a.jsx(l,{className:"h-3.5 w-3.5","aria-hidden":!0}):null,a.jsx("span",{children:d})]},i)})})}function S(){const[s,t]=n.useState(null),e=g()&&m();n.useEffect(()=>{if(!e)return;const r=b(o=>t(o));return u(),r},[e]);const i=n.useCallback((r,o)=>{e&&(t(c=>c&&{...c,[r]:o}),f(r,o))},[e]),d=n.useCallback(r=>{e&&h(r)},[e]),l=n.useCallback(()=>{e&&u()},[e]);return{available:e,settings:s,setSetting:i,runAction:d,refresh:l}}export{N as S,k as T,p as a,w as b,S as u};
@@ -1 +1 @@
1
- import{c as e,r as t,aZ as c,a_ as r,C as a}from"./main-Ck4SUH4m.js";const o=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],C=e("info",o),n=Object.freeze({currency:a,rate:1,symbol:"$",rates:{...r},rateSource:"default",rateFetchedAt:null,setCurrency:()=>{}});function y(){return t.useContext(c)??n}export{C as I,y as u};
1
+ import{c as e,r as t,aZ as c,a_ as r,C as a}from"./main-DRcFJLLD.js";const o=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],C=e("info",o),n=Object.freeze({currency:a,rate:1,symbol:"$",rates:{...r},rateSource:"default",rateFetchedAt:null,setCurrency:()=>{}});function y(){return t.useContext(c)??n}export{C as I,y as u};
@@ -225,7 +225,7 @@
225
225
  ]
226
226
  }
227
227
  </script>
228
- <script type="module" crossorigin src="/assets/main-Ck4SUH4m.js"></script>
228
+ <script type="module" crossorigin src="/assets/main-DRcFJLLD.js"></script>
229
229
  <link rel="stylesheet" crossorigin href="/assets/main-ZrWkoMlr.css">
230
230
  </head>
231
231
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ipv9/tokentracker-cli",
3
- "version": "0.39.40",
3
+ "version": "0.39.41",
4
4
  "description": "Local-first token and cost dashboard for 22 AI coding tools, including Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Copilot, Antigravity, Zed, and Goose.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "access": "public"
24
24
  },
25
25
  "scripts": {
26
- "ci:local": "npm run dashboard:build && npm --prefix dashboard run test && npm test && npm run validate:copy && npm run validate:ui-hardcode && npm run validate:guardrails && npm run docs:openwiki:check && node --test test/architecture-guardrails.test.js",
26
+ "ci:local": "npm run dashboard:build && npm --prefix dashboard run test && npm test && npm run validate:copy && npm run validate:ui-hardcode && npm run validate:guardrails && npm run validate:curated-expiry && npm run docs:openwiki:check && node --test test/architecture-guardrails.test.js",
27
27
  "copy:pull": "node scripts/copy-sync.cjs pull",
28
28
  "copy:push": "node scripts/copy-sync.cjs push",
29
29
  "dashboard:build": "npm --prefix dashboard run build",
@@ -42,6 +42,7 @@
42
42
  "prepublishOnly": "node scripts/build-pricing-seed.cjs",
43
43
  "test": "node --test test/*.test.js",
44
44
  "validate:copy": "node scripts/validate-copy-registry.cjs",
45
+ "validate:curated-expiry": "node scripts/validate-curated-expiry.cjs",
45
46
  "validate:guardrails": "node scripts/validate-architecture-guardrails.cjs",
46
47
  "validate:retros": "node scripts/validate-retros.cjs",
47
48
  "validate:ui-hardcode": "node scripts/ops/validate-ui-hardcode.cjs"
@@ -4,7 +4,7 @@ const path = require("node:path");
4
4
  const fssync = require("node:fs");
5
5
 
6
6
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
7
- const { createLocalApiHandler, resolveQueuePath } = require("../lib/local-api");
7
+ const { createLocalApiHandler, resolveQueuePath, isLoopbackHostname } = require("../lib/local-api");
8
8
  const {
9
9
  buildServeDataPreflightMessage,
10
10
  summarizeQueueData,
@@ -18,6 +18,122 @@ const DEFAULT_MAX_PORT_ATTEMPTS = 20;
18
18
  const NPM_PACKAGE_NAME = "@ipv9/tokentracker-cli";
19
19
  const LOCAL_BIND_HOST = "127.0.0.1";
20
20
 
21
+ // Anti-DNS-rebinding guard. Binding the socket to loopback does not make the
22
+ // Host header trustworthy: under DNS rebinding a browser sends
23
+ // `Host: attacker.example:<port>` to 127.0.0.1 and treats the response as
24
+ // same-origin, so CORS never applies. Mutations are already gated on a loopback
25
+ // Origin; without this check every GET /functions/* endpoint — full spend
26
+ // history, model mix, project names — is readable by any page the victim
27
+ // happens to have open. Issue #88.
28
+ //
29
+ // Only the hostname matters: the port is chosen at runtime by
30
+ // listenOnAvailablePort, so pinning it here would be fragile without adding any
31
+ // protection. An absent Host (HTTP/1.0, some local probes) is allowed — the
32
+ // socket is already loopback-bound, and there is no rebinding vector without a
33
+ // browser sending a name.
34
+ function isAllowedHostHeader(hostHeader) {
35
+ if (hostHeader == null || hostHeader === "") return true;
36
+ // Userinfo has no meaning in a Host header, so anything carrying it is
37
+ // malformed. Tested on the RAW value: an EMPTY userinfo ("@localhost",
38
+ // ":@localhost") parses to a falsy url.username, so checking the parsed
39
+ // fields alone lets exactly the malformed forms through.
40
+ if (hostHeader.includes("@")) return false;
41
+ // Same reason as the request target: URL parsing strips these bytes, so they
42
+ // can smuggle a different authority past a check on the raw string.
43
+ if (/[\u0000-\u0020\u007f]/.test(hostHeader)) return false;
44
+ try {
45
+ const url = new URL(`http://${hostHeader}`);
46
+ // `localhost.` is the valid fully-qualified spelling of localhost. WHATWG
47
+ // URL canonicalises the trailing dot away for IPv4 literals but not for
48
+ // names, so strip it here or the FQDN form gets a spurious 403.
49
+ return isLoopbackHostname(url.hostname.replace(/\.$/, ""));
50
+ } catch (_e) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ // An origin server is not a proxy: a request-target must be origin-form
56
+ // ("/path") or asterisk-form ("*"). Absolute-form ("GET http://evil/x") carries
57
+ // its own authority, which WOULD win over the Host header when the URL is
58
+ // parsed for routing — so the Host allowlist and the routing would disagree
59
+ // about which site this request is for. Refuse instead of picking a winner.
60
+ function isAllowedRequestTarget(target) {
61
+ if (typeof target !== "string" || target === "") return false;
62
+ if (target === "*") return true;
63
+ if (!target.startsWith("/")) return false;
64
+ // "//evil/x" is a network-path reference, and WHATWG URL treats a backslash
65
+ // like a slash, so "/\evil/x" behaves the same way: both make the parsed URL
66
+ // adopt a foreign authority even though the Host header said loopback.
67
+ // Routing only reads url.pathname today, but leaving the parsed URL pointing
68
+ // at someone else's origin is the same guard-vs-parser disagreement that
69
+ // absolute-form creates.
70
+ if (target.startsWith("//") || target.startsWith("/\\")) return false;
71
+ // WHATWG URL strips tab, LF and CR from its input BEFORE parsing, so
72
+ // "/<tab>//evil/x" becomes "//evil/x" and adopts a foreign authority after
73
+ // passing the prefix checks above. Node's own parser rejects these bytes in a
74
+ // request-target with a 400 before the handler runs, so this is not reachable
75
+ // over the wire today — but a guard that only holds because a different layer
76
+ // is strict is the disagreement this function exists to prevent.
77
+ if (/[\u0000-\u0020\u007f]/.test(target)) return false;
78
+ return true;
79
+ }
80
+
81
+ // Extracted from cmdServe so the wiring — not just the predicate — is testable:
82
+ // a guard that exists but is never reached is the failure mode this is guarding
83
+ // against in the first place.
84
+ function createRequestHandler({ handleApi, dashboardDir }) {
85
+ return async function handleRequest(req, res) {
86
+ try {
87
+ // Reject rebound hostnames before anything reads the request. Issue #88.
88
+ if (!isAllowedHostHeader(req.headers.host)) {
89
+ res.writeHead(403, { "Content-Type": "text/plain" });
90
+ res.end("Forbidden: TokenTracker only serves loopback hosts.\n");
91
+ return;
92
+ }
93
+
94
+ if (!isAllowedRequestTarget(req.url)) {
95
+ res.writeHead(400, { "Content-Type": "text/plain" });
96
+ res.end("Bad Request: absolute-form request targets are not served.\n");
97
+ return;
98
+ }
99
+
100
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
101
+
102
+ // CORS preflight
103
+ if (req.method === "OPTIONS") {
104
+ res.writeHead(204, {
105
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
106
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
107
+ });
108
+ res.end();
109
+ return;
110
+ }
111
+
112
+ // API routes
113
+ if (
114
+ url.pathname.startsWith("/functions/")
115
+ || url.pathname.startsWith("/api/")
116
+ || url.pathname.startsWith("/proxy/")
117
+ ) {
118
+ const handled = await handleApi(req, res, url);
119
+ if (handled) return;
120
+ }
121
+
122
+ // Static files
123
+ const served = await serveStaticFile(dashboardDir, url.pathname, res);
124
+ if (served) return;
125
+
126
+ // SPA fallback
127
+ await serveStaticFile(dashboardDir, "/index.html", res);
128
+ } catch (e) {
129
+ if (!res.headersSent) {
130
+ res.writeHead(500, { "Content-Type": "text/plain" });
131
+ res.end("Internal Server Error");
132
+ }
133
+ }
134
+ };
135
+ }
136
+
21
137
  function buildPortInUseHint(port) {
22
138
  return `Port ${port} is unavailable. Try: npx ${NPM_PACKAGE_NAME} serve --port ${port + 1}\n`;
23
139
  }
@@ -110,43 +226,7 @@ async function cmdServe(argv) {
110
226
  // 3. Create handler
111
227
  const handleApi = createLocalApiHandler({ queuePath });
112
228
 
113
- const server = http.createServer(async (req, res) => {
114
- try {
115
- const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
116
-
117
- // CORS preflight
118
- if (req.method === "OPTIONS") {
119
- res.writeHead(204, {
120
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
121
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
122
- });
123
- res.end();
124
- return;
125
- }
126
-
127
- // API routes
128
- if (
129
- url.pathname.startsWith("/functions/")
130
- || url.pathname.startsWith("/api/")
131
- || url.pathname.startsWith("/proxy/")
132
- ) {
133
- const handled = await handleApi(req, res, url);
134
- if (handled) return;
135
- }
136
-
137
- // Static files
138
- const served = await serveStaticFile(dashboardDir, url.pathname, res);
139
- if (served) return;
140
-
141
- // SPA fallback
142
- await serveStaticFile(dashboardDir, "/index.html", res);
143
- } catch (e) {
144
- if (!res.headersSent) {
145
- res.writeHead(500, { "Content-Type": "text/plain" });
146
- res.end("Internal Server Error");
147
- }
148
- }
149
- });
229
+ const server = http.createServer(createRequestHandler({ handleApi, dashboardDir }));
150
230
 
151
231
  // 4. Listen. Default startup follows README behavior and picks the next
152
232
  // available port; an explicit --port/PORT remains strict.
@@ -316,6 +396,9 @@ module.exports = {
316
396
  NPM_PACKAGE_NAME,
317
397
  LOCAL_BIND_HOST,
318
398
  isPortUnavailableError,
399
+ isAllowedHostHeader,
400
+ isAllowedRequestTarget,
401
+ createRequestHandler,
319
402
  listenOnAvailablePort,
320
403
  getLocalServerUrl,
321
404
  parseArgs,
package/src/lib/fs.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const fs = require("node:fs/promises");
2
+ const os = require("node:os");
2
3
  const path = require("node:path");
3
4
 
4
5
  async function ensureDir(p) {
@@ -47,37 +48,157 @@ async function chmod600IfPossible(filePath) {
47
48
  } catch (_e) {}
48
49
  }
49
50
 
50
- const LOCK_STALE_MS = 5 * 60 * 1000; // 5 minutes
51
+ // The holder heartbeats the lock's mtime, so "stale" now means "the holder
52
+ // died", not "the holder is slow". That lets the threshold be generous: the old
53
+ // 5-minute window silently stole the lock from any sync that ran longer than
54
+ // one local-sync tick (full-corpus rebuilds and migration reparses do — see the
55
+ // post-mortem at src/commands/sync.js:74-123), letting two writers interleave
56
+ // appends into queue.jsonl. A torn line is skipped by the reader, and a skipped
57
+ // retraction row is a permanent silent overcount. Issue #89.
58
+ const LOCK_STALE_MS = 30 * 60 * 1000; // 30 minutes
59
+ const LOCK_HEARTBEAT_MS = 30 * 1000; // touch mtime every 30s while held
60
+ const MAX_LOCK_ATTEMPTS = 3;
51
61
 
52
- async function openLock(lockPath, { quietIfLocked }) {
62
+ // mkdir is atomic and exclusive, which makes it a usable mutex on every
63
+ // filesystem we care about. Held only for the few syscalls of a takeover.
64
+ const TAKEOVER_ABANDONED_MS = 60 * 1000;
65
+
66
+ function takeoverMutexPath(lockPath) {
67
+ return `${lockPath}.takeover`;
68
+ }
69
+
70
+ async function acquireTakeoverMutex(lockPath) {
71
+ const mutexPath = takeoverMutexPath(lockPath);
53
72
  try {
54
- const handle = await fs.open(lockPath, "wx");
55
- return {
56
- async release() {
57
- await handle.close().catch(() => {});
58
- await fs.unlink(lockPath).catch(() => {});
59
- },
60
- };
73
+ await fs.mkdir(mutexPath);
74
+ return true;
61
75
  } catch (e) {
62
- if (e && e.code === "EEXIST") {
63
- // Check if lock is stale
64
- try {
65
- const stat = await fs.stat(lockPath);
66
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
67
- await fs.unlink(lockPath).catch(() => {});
68
- return openLock(lockPath, { quietIfLocked });
69
- }
70
- } catch (_statErr) {
71
- // Lock file disappeared between checks, retry
72
- return openLock(lockPath, { quietIfLocked });
73
- }
74
- if (!quietIfLocked) {
75
- process.stdout.write("Another sync is already running.\n");
76
- }
77
- return null;
76
+ if (!e || e.code !== "EEXIST") return false;
77
+ }
78
+ // A process that died mid-takeover would otherwise block every future
79
+ // takeover forever. The window is milliseconds, so anything this old is dead.
80
+ try {
81
+ const stat = await fs.stat(mutexPath);
82
+ if (Date.now() - stat.mtimeMs > TAKEOVER_ABANDONED_MS) {
83
+ await fs.rmdir(mutexPath).catch(() => {});
78
84
  }
85
+ } catch (_e) {
86
+ // Vanished under us — the caller retries either way.
87
+ }
88
+ return false;
89
+ }
90
+
91
+ async function readLockOwner(lockPath) {
92
+ try {
93
+ const raw = await fs.readFile(lockPath, "utf8");
94
+ const parsed = JSON.parse(raw);
95
+ if (parsed && typeof parsed === "object") return parsed;
96
+ } catch (_e) {
97
+ // Empty, truncated, or pre-upgrade lock file — fall back to the mtime rule.
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function isProcessAlive(pid) {
103
+ try {
104
+ process.kill(pid, 0);
105
+ return true;
106
+ } catch (e) {
107
+ // EPERM means the process exists but belongs to someone else.
108
+ return Boolean(e && e.code === "EPERM");
109
+ }
110
+ }
111
+
112
+ // A lock whose owner process is gone is stale immediately — no need to wait out
113
+ // the full window. Only trust the pid when the lock was taken on this host, and
114
+ // never when it is our own pid (a recycled pid would look alive forever).
115
+ function isOwnerGone(owner) {
116
+ if (!owner || typeof owner.pid !== "number" || !Number.isInteger(owner.pid)) return false;
117
+ if (owner.host !== os.hostname()) return false;
118
+ if (owner.pid === process.pid) return false;
119
+ return !isProcessAlive(owner.pid);
120
+ }
121
+
122
+ // Throws EEXIST when the lock is already held — that is the caller's signal to
123
+ // decide whether the existing lock is stale.
124
+ async function createHeldLock(lockPath, heartbeatMs) {
125
+ const handle = await fs.open(lockPath, "wx");
126
+ const owner = { pid: process.pid, host: os.hostname(), startedAt: new Date().toISOString() };
127
+
128
+ try {
129
+ await handle.writeFile(`${JSON.stringify(owner, null, 2)}\n`, { encoding: "utf8" });
130
+ } catch (e) {
131
+ // A write failure would otherwise leave a leaked fd plus an empty lock file
132
+ // that nothing releases — blocking every sync until the stale window elapses.
133
+ await handle.close().catch(() => {});
134
+ await fs.unlink(lockPath).catch(() => {});
79
135
  throw e;
80
136
  }
137
+
138
+ // unref'd so a held lock can never keep the CLI process alive on its own.
139
+ const heartbeat = setInterval(() => {
140
+ const now = new Date();
141
+ fs.utimes(lockPath, now, now).catch(() => {});
142
+ }, heartbeatMs);
143
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
144
+
145
+ return {
146
+ owner,
147
+ async release() {
148
+ clearInterval(heartbeat);
149
+ await handle.close().catch(() => {});
150
+ await fs.unlink(lockPath).catch(() => {});
151
+ },
152
+ };
153
+ }
154
+
155
+ // Removing a stale lock is a check-then-act, and the caller's check ran outside
156
+ // any mutual exclusion: by now another waiter may already have reclaimed it and
157
+ // be holding a *fresh* lock. Deleting (or renaming) the path blind would drop
158
+ // that winner's lock and let two syncs run at once. The mkdir mutex makes the
159
+ // re-check and the delete atomic with respect to other waiters.
160
+ async function reclaimStaleLock(lockPath) {
161
+ if (!(await acquireTakeoverMutex(lockPath))) return;
162
+ try {
163
+ const current = await fs.stat(lockPath).catch(() => null);
164
+ if (!current) return;
165
+ const currentOwner = await readLockOwner(lockPath);
166
+ const stillStale = Date.now() - current.mtimeMs > LOCK_STALE_MS || isOwnerGone(currentOwner);
167
+ if (stillStale) await fs.unlink(lockPath).catch(() => {});
168
+ } finally {
169
+ await fs.rmdir(takeoverMutexPath(lockPath)).catch(() => {});
170
+ }
171
+ }
172
+
173
+ async function openLock(lockPath, { quietIfLocked, heartbeatMs = LOCK_HEARTBEAT_MS } = {}, attempt = 1) {
174
+ try {
175
+ return await createHeldLock(lockPath, heartbeatMs);
176
+ } catch (e) {
177
+ if (!e || e.code !== "EEXIST") throw e;
178
+
179
+ const retry = () => openLock(lockPath, { quietIfLocked, heartbeatMs }, attempt + 1);
180
+ const giveUp = () => {
181
+ if (!quietIfLocked) process.stdout.write("Another sync is already running.\n");
182
+ return null;
183
+ };
184
+
185
+ if (attempt >= MAX_LOCK_ATTEMPTS) return giveUp();
186
+
187
+ let stat;
188
+ try {
189
+ stat = await fs.stat(lockPath);
190
+ } catch (_statErr) {
191
+ return retry(); // Lock file disappeared between checks.
192
+ }
193
+
194
+ const owner = await readLockOwner(lockPath);
195
+ const expired = Date.now() - stat.mtimeMs > LOCK_STALE_MS;
196
+ // A live holder heartbeats its lock, so a fresh mtime means "still working".
197
+ if (!expired && !isOwnerGone(owner)) return giveUp();
198
+
199
+ await reclaimStaleLock(lockPath);
200
+ return retry();
201
+ }
81
202
  }
82
203
 
83
204
  module.exports = {
@@ -88,4 +209,7 @@ module.exports = {
88
209
  writeJson,
89
210
  chmod600IfPossible,
90
211
  openLock,
212
+ // Exported for tests.
213
+ LOCK_STALE_MS,
214
+ LOCK_HEARTBEAT_MS,
91
215
  };
@@ -30,6 +30,8 @@ const avatarProxyCache = new Map();
30
30
  const {
31
31
  MODEL_PRICING,
32
32
  getModelPricing,
33
+ getModelPricingMeta,
34
+ getPricingDiagnostics,
33
35
  computeRowCost,
34
36
  ensurePricingLoaded,
35
37
  } = require("./pricing");
@@ -1147,7 +1149,15 @@ function createLocalApiHandler({ queuePath }) {
1147
1149
  model: m.model,
1148
1150
  source: s.source,
1149
1151
  });
1150
- return { ...m, totals: { ...m.totals, total_cost_usd: cost.toFixed(6) } };
1152
+ // How the price was resolved, so the dashboard can say "unpriced"
1153
+ // or "matched by substring" instead of inferring it from a $0 cost
1154
+ // — a genuinely free model and an unknown one both cost $0.
1155
+ const { tier } = getModelPricingMeta(m.model, { source: s.source });
1156
+ return {
1157
+ ...m,
1158
+ pricing_tier: tier,
1159
+ totals: { ...m.totals, total_cost_usd: cost.toFixed(6) },
1160
+ };
1151
1161
  })
1152
1162
  .sort((a, b) => b.totals.total_tokens - a.totals.total_tokens);
1153
1163
  const sourceCost = s.models.reduce((sum, m) => sum + Number(m.totals.total_cost_usd), 0);
@@ -1157,7 +1167,13 @@ function createLocalApiHandler({ queuePath }) {
1157
1167
 
1158
1168
  json(res, {
1159
1169
  from, to, days: 0, scope, excluded_sources: excludedSources, sources,
1160
- pricing: { model: "per-model", pricing_mode: "per_token_type", source: "litellm", effective_from: new Date().toISOString().slice(0, 10) },
1170
+ pricing: {
1171
+ model: "per-model",
1172
+ pricing_mode: "per_token_type",
1173
+ source: "litellm",
1174
+ effective_from: new Date().toISOString().slice(0, 10),
1175
+ ...getPricingDiagnostics(),
1176
+ },
1161
1177
  });
1162
1178
  return true;
1163
1179
  }
@@ -1522,6 +1538,8 @@ module.exports = {
1522
1538
  resolveQueuePath,
1523
1539
  // Exported for cross-consumer tests (pricing + native contract lock).
1524
1540
  MODEL_PRICING,
1541
+ // Shared with serve.js so the Host allowlist and the Origin allowlist agree.
1542
+ isLoopbackHostname,
1525
1543
  getModelPricing,
1526
1544
  computeRowCost,
1527
1545
  ensurePricingLoaded,
@@ -2,8 +2,15 @@
2
2
  "_meta": {
3
3
  "note": "Curated price overrides. Always wins over LiteLLM. Two reasons to live here: (1) self-defined alias names that LiteLLM will never carry (kiro-*, hy3-*, composer-*, kimi-for-coding, free-tier OpenRouter routes); (2) prices we want to pin even if LiteLLM has the model (e.g. cache_write fields LiteLLM often omits). Units: USD per million tokens. Edit this file to override pricing without redeploying.",
4
4
  "units": "usd_per_million_tokens",
5
- "deepseek_v4_pro_discount_expiry": "2026-05-31T15:59:00Z DeepSeek v4-pro is currently at a 75% promotional discount. After expiry the prices revert to 4x: input $1.74/M, output $3.48/M, cache_read $0.0145/M, cache_write $1.74/M. Update this file before the cutover.",
6
- "sonnet5_intro_price_expiry": "2026-08-31 — Sonnet 5 intro price 2/10 reverts to 3/15 sticker. The src pricing path auto-tracks LiteLLM (no edit needed); re-vendor the seed after the cutover. See issue #16."
5
+ "expiries_note": "Time-boxed pricing facts. Machine-checked by scripts/validate-curated-expiry.cjs (npm run validate:curated-expiry, part of ci:local): once expires_at has passed, the check FAILS until a human applies `action` and then removes or advances the entry. Free-text expiry notes are not allowed here a date nobody checks is how deepseek-v4-pro stayed on a 75%-off promo price for 55 days past its cutover (issue #87).",
6
+ "expiries": [
7
+ {
8
+ "id": "sonnet5-intro-price",
9
+ "expires_at": "2026-08-31",
10
+ "what": "Sonnet 5 introductory pricing (2/10) reverts to the 3/15 sticker price.",
11
+ "action": "No curated entry to edit — the src pricing path auto-tracks LiteLLM. Re-vendor the bundled seed (npm run pricing:build-seed) so a cold start also prices Sonnet 5 correctly, then delete this entry. See issue #16."
12
+ }
13
+ ]
7
14
  },
8
15
  "exact": {
9
16
  "claude-fable-5": { "input": 10, "output": 50, "cache_read": 1, "cache_write": 12.5, "note": "Pinned from Anthropic public pricing until bundled LiteLLM seed carries Fable 5." },
@@ -20,7 +27,7 @@
20
27
  "MiniMax-M2.7": { "input": 0.3, "output": 1.2, "cache_read": 0.06, "cache_write": 0.375 },
21
28
  "MiniMax-M2.7-highspeed":{ "input": 0.6, "output": 2.4, "cache_read": 0.06, "cache_write": 0.375 },
22
29
  "deepseek-v4-flash":{ "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
23
- "deepseek-v4-pro": { "input": 0.435,"output": 0.87, "cache_read": 0.003625, "cache_write": 0.435 },
30
+ "deepseek-v4-pro": { "input": 1.74, "output": 3.48, "cache_read": 0.0145, "cache_write": 1.74, "note": "Standard (post-promo) pricing. The 75%-off launch promo expired 2026-05-31; these rates applied 2026-07-25 (issue #87)." },
24
31
  "deepseek-chat": { "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
25
32
  "grok-build": { "input": 1.25, "output": 2.50, "cache_read": 0.20, "note": "Grok Build TUI estimate. Local telemetry currently exposes totalTokens without a stable prompt/output/cache split, so TokenTracker estimates input/output split until Grok exposes per-call usage details." },
26
33
  "grok-4-0709": { "input": 3.00, "output": 15.00, "cache_read": 0.75 },