tokentracker-cli 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dashboard/dist/assets/{Card-bWpoRQRe.js → Card-CgMq55sY.js} +1 -1
  2. package/dashboard/dist/assets/DashboardPage-DfBWYjnA.js +1 -0
  3. package/dashboard/dist/assets/{FadeIn-DiEMVE-8.js → FadeIn-DLhKBgqY.js} +1 -1
  4. package/dashboard/dist/assets/{HeaderGithubStar-CcIqYx9A.js → HeaderGithubStar-C9HsEaL7.js} +1 -1
  5. package/dashboard/dist/assets/{IpCheckPage-YGHfRDyX.js → IpCheckPage-XRIx0rfG.js} +1 -1
  6. package/dashboard/dist/assets/{LandingPage-CHa_w_86.js → LandingPage-DXlrkwEx.js} +1 -1
  7. package/dashboard/dist/assets/{LeaderboardPage-D9y42SSP.js → LeaderboardPage-CIqzV9cT.js} +1 -1
  8. package/dashboard/dist/assets/{LeaderboardProfilePage-Ddx744mB.js → LeaderboardProfilePage-IUgRwHTr.js} +1 -1
  9. package/dashboard/dist/assets/{LimitsPage-CgEIOfAl.js → LimitsPage-OrcWqg7m.js} +1 -1
  10. package/dashboard/dist/assets/{LoginPage-NeXU_sFF.js → LoginPage-BuSNuEnm.js} +1 -1
  11. package/dashboard/dist/assets/{PopoverPopup-CMkowlli.js → PopoverPopup-D6I_CIFY.js} +1 -1
  12. package/dashboard/dist/assets/{ProviderIcon-oYzuUM7i.js → ProviderIcon-Bp-wDvoj.js} +1 -1
  13. package/dashboard/dist/assets/{SettingsPage-DFT95z8p.js → SettingsPage-DiyJd4az.js} +1 -1
  14. package/dashboard/dist/assets/{SkillsPage-DqjyRl-E.js → SkillsPage-d8zU8wsf.js} +1 -1
  15. package/dashboard/dist/assets/{WidgetsPage-ArGaipcO.js → WidgetsPage-DJsxeeBY.js} +1 -1
  16. package/dashboard/dist/assets/{chevron-down-_lDbEo22.js → chevron-down-CFtbeaPZ.js} +1 -1
  17. package/dashboard/dist/assets/{download-Nsityl8b.js → download-HjhL2_eA.js} +1 -1
  18. package/dashboard/dist/assets/{leaderboard-columns-Bxb0Pjta.js → leaderboard-columns-CrSoLL2s.js} +1 -1
  19. package/dashboard/dist/assets/{main-BwYPxpC9.js → main-CgJNueY2.js} +2 -2
  20. package/dashboard/dist/assets/{use-limits-display-prefs-D0yLjuaQ.js → use-limits-display-prefs-BW2QZr21.js} +1 -1
  21. package/dashboard/dist/assets/{use-native-settings-BqgTrCrJ.js → use-native-settings-Bd1d-CVd.js} +1 -1
  22. package/dashboard/dist/assets/{use-reduced-motion-BLi3kUPO.js → use-reduced-motion-B0JB6MTu.js} +1 -1
  23. package/dashboard/dist/assets/{use-usage-limits-BPizTFcW.js → use-usage-limits-OtRz0apS.js} +1 -1
  24. package/dashboard/dist/index.html +1 -1
  25. package/dashboard/dist/share.html +1 -1
  26. package/package.json +1 -1
  27. package/src/lib/claude-categorizer.js +80 -15
  28. package/src/lib/pricing/seed-snapshot.json +1 -1
  29. package/dashboard/dist/assets/DashboardPage-Dyc6PbEz.js +0 -1
@@ -1 +1 @@
1
- import{r as a}from"./main-BwYPxpC9.js";const d=["claude","codex","cursor","gemini","kimi","kiro","copilot","antigravity"],S={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},v={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",kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},l="tt.limits.providerOrder",g="tt.limits.providerVisibility";function w(){if(typeof window>"u")return[...d];try{const i=window.localStorage.getItem(l);if(!i)return[...d];const s=JSON.parse(i);if(!Array.isArray(s))return[...d];const r=s.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function y(){const i=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return i;try{const s=window.localStorage.getItem(g);if(!s)return i;const r=JSON.parse(s);if(!r||typeof r!="object")return i;const c={...i};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return i}}function C(){const[i,s]=a.useState(w),[r,c]=a.useState(y);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(i))}catch{}},[i]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(w()),t.key===g&&c(y())};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)},[]);const u=a.useCallback(o=>{c(t=>({...t,[o]:!t[o]}))},[]),b=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<=0)return t;const n=[...t];return[n[e-1],n[e]]=[n[e],n[e-1]],n})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const n=[...t];return[n[e],n[e+1]]=[n[e+1],n[e]],n})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const n=e.indexOf(o),m=e.indexOf(t);if(n<0||m<0)return e;const f=[...e],[I]=f.splice(n,1);return f.splice(m,0,I),f})},[]),k=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),x=a.useMemo(()=>i.filter(o=>r[o]!==!1),[i,r]);return{order:i,visibility:r,visibleOrdered:x,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:k}}export{v as L,S as a,C as u};
1
+ import{r as a}from"./main-CgJNueY2.js";const d=["claude","codex","cursor","gemini","kimi","kiro","copilot","antigravity"],S={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},v={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",kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},l="tt.limits.providerOrder",g="tt.limits.providerVisibility";function w(){if(typeof window>"u")return[...d];try{const i=window.localStorage.getItem(l);if(!i)return[...d];const s=JSON.parse(i);if(!Array.isArray(s))return[...d];const r=s.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function y(){const i=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return i;try{const s=window.localStorage.getItem(g);if(!s)return i;const r=JSON.parse(s);if(!r||typeof r!="object")return i;const c={...i};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return i}}function C(){const[i,s]=a.useState(w),[r,c]=a.useState(y);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(i))}catch{}},[i]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(w()),t.key===g&&c(y())};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)},[]);const u=a.useCallback(o=>{c(t=>({...t,[o]:!t[o]}))},[]),b=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<=0)return t;const n=[...t];return[n[e-1],n[e]]=[n[e],n[e-1]],n})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const n=[...t];return[n[e],n[e+1]]=[n[e+1],n[e]],n})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const n=e.indexOf(o),m=e.indexOf(t);if(n<0||m<0)return e;const f=[...e],[I]=f.splice(n,1);return f.splice(m,0,I),f})},[]),k=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),x=a.useMemo(()=>i.filter(o=>r[o]!==!1),[i,r]);return{order:i,visibility:r,visibleOrdered:x,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:k}}export{v as L,S as a,C as u};
@@ -1 +1 @@
1
- import{D as a,a5 as x,r as n,aB as g,aC as m,aD as b,aE as u,aF as f,az as h}from"./main-BwYPxpC9.js";import{C as y}from"./Card-bWpoRQRe.js";function p({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 j({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,p as T,j as a,w as b,S as u};
1
+ import{D as a,a5 as x,r as n,aB as g,aC as m,aD as b,aE as u,aF as f,az as h}from"./main-CgJNueY2.js";import{C as y}from"./Card-CgMq55sY.js";function p({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 j({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,p as T,j as a,w as b,S as u};
@@ -1 +1 @@
1
- import{aG as t,aH as o,r,aI as s}from"./main-BwYPxpC9.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
1
+ import{aG as t,aH as o,r,aI as s}from"./main-CgJNueY2.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
@@ -1 +1 @@
1
- import{r,aj as l}from"./main-BwYPxpC9.js";function y(i){const[o,a]=r.useState(null),[u,s]=r.useState(null),[c,f]=r.useState(!0),n=!!i?.initialRefresh,g=r.useCallback(async()=>{try{const e=await l({refresh:!0});a(e&&typeof e=="object"?e:null),s(null)}catch(e){s(e?.message||String(e))}},[]);return r.useEffect(()=>{let e=!1;return(async()=>{try{const t=await l(n?{refresh:!0}:{});if(e)return;a(t&&typeof t=="object"?t:null),s(null)}catch(t){if(e)return;s(t?.message||String(t))}finally{e||f(!1)}})(),()=>{e=!0}},[n]),{data:o,error:u,isLoading:c,refresh:g}}export{y as u};
1
+ import{r,aj as l}from"./main-CgJNueY2.js";function y(i){const[o,a]=r.useState(null),[u,s]=r.useState(null),[c,f]=r.useState(!0),n=!!i?.initialRefresh,g=r.useCallback(async()=>{try{const e=await l({refresh:!0});a(e&&typeof e=="object"?e:null),s(null)}catch(e){s(e?.message||String(e))}},[]);return r.useEffect(()=>{let e=!1;return(async()=>{try{const t=await l(n?{refresh:!0}:{});if(e)return;a(t&&typeof t=="object"?t:null),s(null)}catch(t){if(e)return;s(t?.message||String(t))}finally{e||f(!1)}})(),()=>{e=!0}},[n]),{data:o,error:u,isLoading:c,refresh:g}}export{y as u};
@@ -210,7 +210,7 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-BwYPxpC9.js"></script>
213
+ <script type="module" crossorigin src="/assets/main-CgJNueY2.js"></script>
214
214
  <link rel="stylesheet" crossorigin href="/assets/main-CITVpx5B.css">
215
215
  </head>
216
216
  <body>
@@ -51,7 +51,7 @@
51
51
  "description": "Shareable Token Tracker dashboard snapshot."
52
52
  }
53
53
  </script>
54
- <script type="module" crossorigin src="/assets/main-BwYPxpC9.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-CgJNueY2.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-CITVpx5B.css">
56
56
  </head>
57
57
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, Grok Build, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  const fssync = require("node:fs");
14
14
  const os = require("node:os");
15
15
  const path = require("node:path");
16
+ const crypto = require("node:crypto");
16
17
  const readline = require("node:readline");
17
18
 
18
19
  const {
@@ -514,11 +515,56 @@ function dayKeyToIsoBounds(from, to) {
514
515
  };
515
516
  }
516
517
 
517
- // Cache: keyed on (rootDir|from|to|maxMtime). 60s TTL is a safety net in
518
- // case the watcher misses something.
518
+ // Cache: keyed on (rootDir|from|to|files.length|maxMtime). The key already
519
+ // changes the moment any session file is touched, so a long TTL is safe — we
520
+ // don't need a short window as a "safety net". Mirror the in-memory cache
521
+ // to disk so a fresh tracker process (e.g. menu bar app restart) doesn't pay
522
+ // the cold-scan cost again.
519
523
  const CACHE = new Map();
520
- const CACHE_TTL_MS = 60_000;
521
- const CACHE_SCHEMA_VERSION = "skills-exec-v2";
524
+ const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6h
525
+ const CACHE_SCHEMA_VERSION = "skills-exec-v3";
526
+ const DISK_CACHE_DIR = path.join(os.homedir(), ".tokentracker", "cache", "claude-categorizer");
527
+
528
+ function cacheKeyHash(key) {
529
+ return crypto.createHash("sha1").update(key).digest("hex").slice(0, 32);
530
+ }
531
+
532
+ function readDiskCache(key) {
533
+ try {
534
+ const fp = path.join(DISK_CACHE_DIR, `${cacheKeyHash(key)}.json`);
535
+ const raw = fssync.readFileSync(fp, "utf8");
536
+ const obj = JSON.parse(raw);
537
+ if (!obj || obj.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
538
+ if (Date.now() - Number(obj.at || 0) >= CACHE_TTL_MS) return null;
539
+ return obj.value;
540
+ } catch (_e) {
541
+ return null;
542
+ }
543
+ }
544
+
545
+ function writeDiskCache(key, value) {
546
+ try {
547
+ fssync.mkdirSync(DISK_CACHE_DIR, { recursive: true });
548
+ const fp = path.join(DISK_CACHE_DIR, `${cacheKeyHash(key)}.json`);
549
+ const payload = { schemaVersion: CACHE_SCHEMA_VERSION, at: Date.now(), value };
550
+ fssync.writeFileSync(fp, JSON.stringify(payload));
551
+ // Bound on-disk size; categorizer is cheap to recompute when miss.
552
+ try {
553
+ const entries = fssync.readdirSync(DISK_CACHE_DIR)
554
+ .filter((n) => n.endsWith(".json"))
555
+ .map((n) => {
556
+ const p = path.join(DISK_CACHE_DIR, n);
557
+ let mtime = 0;
558
+ try { mtime = fssync.statSync(p).mtimeMs; } catch (_e) {}
559
+ return { p, mtime };
560
+ })
561
+ .sort((a, b) => b.mtime - a.mtime);
562
+ for (const e of entries.slice(16)) {
563
+ try { fssync.unlinkSync(e.p); } catch (_e) {}
564
+ }
565
+ } catch (_e) {}
566
+ } catch (_e) {}
567
+ }
522
568
 
523
569
  function maxMtimeMs(files) {
524
570
  let max = 0;
@@ -556,6 +602,11 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
556
602
  if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
557
603
  return cached.value;
558
604
  }
605
+ const onDisk = readDiskCache(cacheKey);
606
+ if (onDisk) {
607
+ CACHE.set(cacheKey, { at: Date.now(), value: onDisk });
608
+ return onDisk;
609
+ }
559
610
 
560
611
  const { fromIso, toIso } = dayKeyToIsoBounds(from, to);
561
612
  const breakdown = emptyCategoryMap();
@@ -575,18 +626,31 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
575
626
  by_exit: new Map(),
576
627
  };
577
628
 
578
- for (const fp of files) {
579
- const counted = await categorizeSessionFile(
580
- fp,
581
- { fromIso, toIso, seenHashes },
582
- breakdown,
583
- toolLedger,
584
- skillLedger,
585
- execLedger,
586
- );
587
- if (counted > 0) sessionCount += 1;
588
- messageCount += counted;
629
+ // Process files with bounded parallelism. CPU-bound (JSON.parse per line)
630
+ // limits the win, but overlapping the per-file fs.open + first-block read
631
+ // I/O behind the previous file's parsing still shaves ~15% off cold scans.
632
+ // `seenHashes`/`breakdown`/ledgers are mutated only inside synchronous
633
+ // sections of `classifyOneMessage`; `for await` in `categorizeSessionFile`
634
+ // only yields between lines, so workers can't tear shared state.
635
+ const SCAN_CONCURRENCY = 4;
636
+ let cursor = 0;
637
+ async function worker() {
638
+ while (cursor < files.length) {
639
+ const idx = cursor++;
640
+ if (idx >= files.length) return;
641
+ const counted = await categorizeSessionFile(
642
+ files[idx],
643
+ { fromIso, toIso, seenHashes },
644
+ breakdown,
645
+ toolLedger,
646
+ skillLedger,
647
+ execLedger,
648
+ );
649
+ if (counted > 0) sessionCount += 1;
650
+ messageCount += counted;
651
+ }
589
652
  }
653
+ await Promise.all(Array.from({ length: SCAN_CONCURRENCY }, () => worker()));
590
654
 
591
655
  const totals = emptyTotals();
592
656
  for (const key of CATEGORY_KEYS) addInto(totals, breakdown[key]);
@@ -612,6 +676,7 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
612
676
  };
613
677
 
614
678
  CACHE.set(cacheKey, { at: Date.now(), value: result });
679
+ writeDiskCache(cacheKey, result);
615
680
  // Bound cache size — categorizer is cheap to recompute, no point hoarding.
616
681
  while (CACHE.size > 32) {
617
682
  const oldest = [...CACHE.entries()].sort((a, b) => a[1].at - b[1].at)[0];