tokentracker-cli 0.24.6 → 0.24.7

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 (42) hide show
  1. package/README.ja.md +1 -0
  2. package/README.ko.md +1 -0
  3. package/README.md +1 -0
  4. package/README.zh-CN.md +1 -0
  5. package/dashboard/dist/assets/{Card-BfayTmBt.js → Card-Cy2qG2yw.js} +1 -1
  6. package/dashboard/dist/assets/{DashboardPage-C_ExwqoB.js → DashboardPage-CsG_Mq6k.js} +2 -2
  7. package/dashboard/dist/assets/{DevicePage-Bzc-tOQ7.js → DevicePage-B1I6_Kxp.js} +1 -1
  8. package/dashboard/dist/assets/{FadeIn-C1nCEQAI.js → FadeIn-xb0lBDdT.js} +1 -1
  9. package/dashboard/dist/assets/{HeaderGithubStar-CalgbIws.js → HeaderGithubStar-BNxToTPs.js} +1 -1
  10. package/dashboard/dist/assets/{IpCheckPage-C2_FdWEa.js → IpCheckPage-CCysfQ-C.js} +1 -1
  11. package/dashboard/dist/assets/{LandingPage-Cab2gSJk.js → LandingPage-D7x7fS1R.js} +1 -1
  12. package/dashboard/dist/assets/{LeaderboardPage-BoWSvv8E.js → LeaderboardPage-DpT-Wb4Y.js} +1 -1
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-BxvvRB0p.js → LeaderboardProfilePage-BFx3cfEn.js} +1 -1
  14. package/dashboard/dist/assets/{LimitsPage-JTd7ZYkv.js → LimitsPage-CSXwXe-W.js} +1 -1
  15. package/dashboard/dist/assets/{LoginPage-fU320alu.js → LoginPage-BJckej8m.js} +1 -1
  16. package/dashboard/dist/assets/{PopoverPopup-C76-ba7G.js → PopoverPopup-BKcg8Ouw.js} +1 -1
  17. package/dashboard/dist/assets/{ProviderIcon-xv4cUgTy.js → ProviderIcon-BRpynAI7.js} +1 -1
  18. package/dashboard/dist/assets/{SettingsPage-UtWH1_mF.js → SettingsPage-tXhwdSnC.js} +1 -1
  19. package/dashboard/dist/assets/{SkillsPage-9W2jGGps.js → SkillsPage-CSybau5n.js} +1 -1
  20. package/dashboard/dist/assets/{WidgetsPage-BedujTKv.js → WidgetsPage-DZS8HDJi.js} +1 -1
  21. package/dashboard/dist/assets/{WrappedPage-Bms62oTH.js → WrappedPage-CUWY3_0R.js} +1 -1
  22. package/dashboard/dist/assets/check-qNB7C4Ej.js +1 -0
  23. package/dashboard/dist/assets/{chevron-down-g6db-hJJ.js → chevron-down-C8arMd0V.js} +1 -1
  24. package/dashboard/dist/assets/{download-UDqrzLfH.js → download-CoTKvd8q.js} +1 -1
  25. package/dashboard/dist/assets/{info-BB9X3uhm.js → info-tb06m6pM.js} +1 -1
  26. package/dashboard/dist/assets/{leaderboard-columns-CTGzd-uH.js → leaderboard-columns-45nSY60u.js} +1 -1
  27. package/dashboard/dist/assets/{main-QPJFCBQm.js → main-DDbFansE.js} +2 -2
  28. package/dashboard/dist/assets/{use-limits-display-prefs-DccYvUNZ.js → use-limits-display-prefs-IJ650iD0.js} +1 -1
  29. package/dashboard/dist/assets/{use-native-settings-DAbSUv-n.js → use-native-settings-B0Civ1m0.js} +1 -1
  30. package/dashboard/dist/assets/{use-reduced-motion-B_GaKtWC.js → use-reduced-motion-DnF5Mq2H.js} +1 -1
  31. package/dashboard/dist/assets/{use-usage-limits-3pTcFcoF.js → use-usage-limits-Ig-0cZyA.js} +1 -1
  32. package/dashboard/dist/assets/{useCurrency-CLZ2MqvV.js → useCurrency-kbOycXmD.js} +1 -1
  33. package/dashboard/dist/index.html +1 -1
  34. package/dashboard/dist/share.html +1 -1
  35. package/package.json +1 -1
  36. package/src/commands/serve.js +114 -14
  37. package/src/commands/status.js +13 -0
  38. package/src/commands/sync.js +35 -2
  39. package/src/lib/pricing/matcher.js +54 -3
  40. package/src/lib/pricing/seed-snapshot.json +1 -1
  41. package/src/lib/rollout.js +474 -0
  42. package/dashboard/dist/assets/check-DHKWR9eH.js +0 -1
@@ -1 +1 @@
1
- import{r as a}from"./main-QPJFCBQm.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-DDbFansE.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{C as a,a7 as x,r as n,aH as g,aI as m,aJ as b,aK as u,aL as f,aF as h}from"./main-QPJFCBQm.js";import{C as y}from"./Card-BfayTmBt.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{C as a,a7 as x,r as n,aH as g,aI as m,aJ as b,aK as u,aL as f,aF as h}from"./main-DDbFansE.js";import{C as y}from"./Card-Cy2qG2yw.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{aM as t,aN as o,r,aO as s}from"./main-QPJFCBQm.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
1
+ import{aM as t,aN as o,r,aO as s}from"./main-DDbFansE.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
@@ -1 +1 @@
1
- import{r,al as l}from"./main-QPJFCBQm.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,al as l}from"./main-DDbFansE.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 +1 @@
1
- import{r as e,aA as t,aB as r,I as c}from"./main-QPJFCBQm.js";const a=Object.freeze({currency:c,rate:1,symbol:"$",rates:{...r},rateSource:"default",rateFetchedAt:null,setCurrency:()=>{}});function u(){return e.useContext(t)??a}export{u};
1
+ import{r as e,aA as t,aB as r,I as c}from"./main-DDbFansE.js";const a=Object.freeze({currency:c,rate:1,symbol:"$",rates:{...r},rateSource:"default",rateFetchedAt:null,setCurrency:()=>{}});function u(){return e.useContext(t)??a}export{u};
@@ -210,7 +210,7 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-QPJFCBQm.js"></script>
213
+ <script type="module" crossorigin src="/assets/main-DDbFansE.js"></script>
214
214
  <link rel="stylesheet" crossorigin href="/assets/main-C8k06i2w.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-QPJFCBQm.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-DDbFansE.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-C8k06i2w.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.24.6",
3
+ "version": "0.24.7",
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, Roo Code, Zed Agent, Goose)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const { serveStaticFile } = require("../lib/static-server");
11
11
  const { openInBrowser } = require("../lib/browser-auth");
12
12
 
13
13
  const DEFAULT_PORT = 7680;
14
+ const DEFAULT_MAX_PORT_ATTEMPTS = 20;
14
15
  const NPM_PACKAGE_NAME = "tokentracker-cli";
15
16
  const LOCAL_BIND_HOST = "127.0.0.1";
16
17
 
@@ -18,6 +19,10 @@ function buildPortInUseHint(port) {
18
19
  return `Port ${port} is still in use after cleanup. Try: npx ${NPM_PACKAGE_NAME} serve --port ${port + 1}\n`;
19
20
  }
20
21
 
22
+ function isPortUnavailableError(error) {
23
+ return error?.code === "EADDRINUSE" || error?.code === "EACCES" || error?.code === "EPERM";
24
+ }
25
+
21
26
  function getLocalServerUrl(port) {
22
27
  return `http://${LOCAL_BIND_HOST}:${port}`;
23
28
  }
@@ -127,10 +132,28 @@ async function cmdServe(argv) {
127
132
  }
128
133
  });
129
134
 
130
- // 4. Listen (kill stale process on same port if needed)
131
- const port = opts.port;
132
- await ensurePortFree(port);
133
- server.listen(port, LOCAL_BIND_HOST, () => {
135
+ // 4. Listen. Default startup follows README behavior and picks the next
136
+ // available port; an explicit --port/PORT remains strict.
137
+ let port;
138
+ try {
139
+ port = await listenOnAvailablePort(server, opts.port, {
140
+ allowFallback: !opts.portExplicit,
141
+ ensurePortFreeFn: opts.portExplicit ? ensurePortFree : null,
142
+ onRetry: (failedPort) => {
143
+ process.stdout.write(`Port ${failedPort} unavailable, trying ${failedPort + 1}...\n`);
144
+ },
145
+ });
146
+ } catch (e) {
147
+ if (isPortUnavailableError(e)) {
148
+ process.stderr.write(buildPortInUseHint(opts.port));
149
+ } else {
150
+ process.stderr.write(`Server error: ${e.message}\n`);
151
+ }
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+
156
+ {
134
157
  const url = getLocalServerUrl(port);
135
158
  process.stdout.write(
136
159
  [
@@ -148,14 +171,10 @@ async function cmdServe(argv) {
148
171
  if (opts.open) {
149
172
  openInBrowser(url);
150
173
  }
151
- });
174
+ }
152
175
 
153
176
  server.on("error", (e) => {
154
- if (e.code === "EADDRINUSE") {
155
- process.stderr.write(buildPortInUseHint(port));
156
- } else {
157
- process.stderr.write(`Server error: ${e.message}\n`);
158
- }
177
+ process.stderr.write(`Server error: ${e.message}\n`);
159
178
  process.exitCode = 1;
160
179
  });
161
180
 
@@ -213,6 +232,70 @@ async function ensurePortFree(port) {
213
232
  await new Promise((r) => setTimeout(r, 500));
214
233
  }
215
234
 
235
+ function listenOnce(server, port, host) {
236
+ return new Promise((resolve, reject) => {
237
+ let settled = false;
238
+
239
+ const cleanup = () => {
240
+ server.off("listening", onListening);
241
+ server.off("error", onError);
242
+ };
243
+ const finish = (fn, value) => {
244
+ if (settled) return;
245
+ settled = true;
246
+ cleanup();
247
+ fn(value);
248
+ };
249
+ const onListening = () => finish(resolve);
250
+ const onError = (error) => finish(reject, error);
251
+
252
+ server.once("listening", onListening);
253
+ server.once("error", onError);
254
+ try {
255
+ server.listen(port, host);
256
+ } catch (error) {
257
+ finish(reject, error);
258
+ }
259
+ });
260
+ }
261
+
262
+ async function listenOnAvailablePort(
263
+ server,
264
+ startPort,
265
+ {
266
+ host = LOCAL_BIND_HOST,
267
+ allowFallback = false,
268
+ maxAttempts = DEFAULT_MAX_PORT_ATTEMPTS,
269
+ ensurePortFreeFn = null,
270
+ onRetry = null,
271
+ } = {},
272
+ ) {
273
+ const attempts = allowFallback ? Math.max(1, maxAttempts) : 1;
274
+ let port = startPort;
275
+ let lastError = null;
276
+
277
+ for (let i = 0; i < attempts && port < 65536; i++, port++) {
278
+ if (ensurePortFreeFn) {
279
+ await ensurePortFreeFn(port);
280
+ }
281
+
282
+ try {
283
+ await listenOnce(server, port, host);
284
+ return port;
285
+ } catch (error) {
286
+ lastError = error;
287
+ if (!allowFallback || !isPortUnavailableError(error) || port >= 65535) {
288
+ throw error;
289
+ }
290
+ if (typeof onRetry === "function") {
291
+ onRetry(port, error);
292
+ }
293
+ }
294
+ }
295
+
296
+ throw lastError || new Error(`No available port found from ${startPort}`);
297
+ }
298
+
216
299
  function resolveDashboardDir() {
217
300
  const candidates = [
218
301
  path.resolve(__dirname, "../../dashboard/dist"),
@@ -224,13 +307,27 @@ function resolveDashboardDir() {
224
307
  return null;
225
308
  }
226
309
 
227
- function parseArgs(argv) {
228
- const opts = { port: DEFAULT_PORT, open: true, sync: true };
310
+ function parsePort(value) {
311
+ const n = parseInt(value, 10);
312
+ return Number.isFinite(n) && n > 0 && n < 65536 ? n : null;
313
+ }
314
+
315
+ function parseArgs(argv, env = process.env) {
316
+ const envPort = parsePort(env.PORT);
317
+ const opts = {
318
+ port: envPort || DEFAULT_PORT,
319
+ portExplicit: Boolean(envPort),
320
+ open: true,
321
+ sync: true,
322
+ };
229
323
  for (let i = 0; i < argv.length; i++) {
230
324
  const arg = argv[i];
231
325
  if (arg === "--port" && i + 1 < argv.length) {
232
- const n = parseInt(argv[++i], 10);
233
- if (Number.isFinite(n) && n > 0 && n < 65536) opts.port = n;
326
+ const n = parsePort(argv[++i]);
327
+ if (n) {
328
+ opts.port = n;
329
+ opts.portExplicit = true;
330
+ }
234
331
  } else if (arg === "--no-open") {
235
332
  opts.open = false;
236
333
  } else if (arg === "--no-sync") {
@@ -245,5 +342,8 @@ module.exports = {
245
342
  buildPortInUseHint,
246
343
  NPM_PACKAGE_NAME,
247
344
  LOCAL_BIND_HOST,
345
+ isPortUnavailableError,
346
+ listenOnAvailablePort,
248
347
  getLocalServerUrl,
348
+ parseArgs,
249
349
  };
@@ -51,6 +51,8 @@ const {
51
51
  resolveRoocodeTaskFiles,
52
52
  resolveZedDbPath,
53
53
  resolveGooseDbPath,
54
+ listDroidSettingsFiles,
55
+ resolveDroidSessionsDir,
54
56
  resolveGrokBuildSessions,
55
57
  } = require("../lib/rollout");
56
58
  const { probeGrokHookState, resolveGrokHome } = require("../lib/grok-hook");
@@ -231,6 +233,11 @@ async function cmdStatus(argv = []) {
231
233
  const gooseDbPath = resolveGooseDbPath(process.env);
232
234
  const gooseInstalled = fssync.existsSync(gooseDbPath);
233
235
 
236
+ // Droid (Factory CLI) — passive cumulative-delta read of *.settings.json.
237
+ const droidSessionsDir = resolveDroidSessionsDir(process.env);
238
+ const droidSettingsFiles = listDroidSettingsFiles(process.env);
239
+ const droidInstalled = droidSettingsFiles.length > 0;
240
+
234
241
  // Grok Build (xAI TUI)
235
242
  const grokHookState = await probeGrokHookState({ home, trackerDir, env: process.env });
236
243
  const grokSessions = grokHookState.hasGrokInstall || grokHookState.sessionsDir
@@ -324,6 +331,9 @@ async function cmdStatus(argv = []) {
324
331
  goose: gooseInstalled
325
332
  ? { installed: true, detail: gooseDbPath }
326
333
  : { installed: false },
334
+ droid: droidInstalled
335
+ ? { installed: true, files: droidSettingsFiles.length, detail: droidSessionsDir }
336
+ : { installed: false },
327
337
  grok_build: grokInstalled
328
338
  ? {
329
339
  installed: true,
@@ -408,6 +418,9 @@ async function cmdStatus(argv = []) {
408
418
  gooseInstalled
409
419
  ? `- Goose (Block): passive reader (sessions.db, cumulative-delta)`
410
420
  : null,
421
+ droidInstalled
422
+ ? `- Droid (Factory): passive reader (${droidSettingsFiles.length} session${droidSettingsFiles.length !== 1 ? "s" : ""} in ${droidSessionsDir}, cumulative-delta)`
423
+ : null,
411
424
  ...(() => {
412
425
  const passive = passiveProviders.filter((p) => p.passive);
413
426
  if (passive.length === 0) return [];
@@ -52,6 +52,8 @@ const {
52
52
  parseZedIncremental,
53
53
  resolveGooseDbPath,
54
54
  parseGooseIncremental,
55
+ listDroidSettingsFiles,
56
+ parseDroidIncremental,
55
57
  bucketKey,
56
58
  totalsKey,
57
59
  claudeMessageDedupKey,
@@ -456,6 +458,35 @@ async function cmdSync(argv) {
456
458
  });
457
459
  }
458
460
 
461
+ // ── Droid (Factory CLI) — passive reader for ~/.factory/sessions/*.settings.json ──
462
+ const droidSettingsFiles = listDroidSettingsFiles(process.env);
463
+ let droidResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
464
+ if (droidSettingsFiles.length > 0) {
465
+ if (progress?.enabled) {
466
+ progress.start(
467
+ `Parsing Droid ${renderBar(0)} 0/${formatNumber(droidSettingsFiles.length)} sessions | buckets 0`,
468
+ );
469
+ }
470
+ droidResult = await parseDroidIncremental({
471
+ settingsFiles: droidSettingsFiles,
472
+ cursors,
473
+ queuePath,
474
+ // Full-scan sync: drop cursor entries for any session whose
475
+ // settings.json has disappeared off disk so cursors.droid stays
476
+ // bounded by the actual on-disk session count.
477
+ prune: true,
478
+ onProgress: (p) => {
479
+ if (!progress?.enabled) return;
480
+ const pct = p.total > 0 ? p.index / p.total : 1;
481
+ progress.update(
482
+ `Parsing Droid ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
483
+ p.total,
484
+ )} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
485
+ );
486
+ },
487
+ });
488
+ }
489
+
459
490
  // ── Zed Agent (hosted models only; cumulative-delta over SQLite threads) ──
460
491
  const zedDbPath = resolveZedDbPath(process.env);
461
492
  let zedResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -948,7 +979,8 @@ async function cmdSync(argv) {
948
979
  kilocodeResult.recordsProcessed +
949
980
  roocodeResult.recordsProcessed +
950
981
  zedResult.recordsProcessed +
951
- gooseResult.recordsProcessed;
982
+ gooseResult.recordsProcessed +
983
+ droidResult.recordsProcessed;
952
984
  const totalBuckets =
953
985
  parseResult.bucketsQueued +
954
986
  openclawResult.bucketsQueued +
@@ -971,7 +1003,8 @@ async function cmdSync(argv) {
971
1003
  kilocodeResult.bucketsQueued +
972
1004
  roocodeResult.bucketsQueued +
973
1005
  zedResult.bucketsQueued +
974
- gooseResult.bucketsQueued;
1006
+ gooseResult.bucketsQueued +
1007
+ droidResult.bucketsQueued;
975
1008
  process.stdout.write(
976
1009
  [
977
1010
  "Sync finished:",
@@ -72,6 +72,33 @@ function getSortedKeys(litellm) {
72
72
  return cached;
73
73
  }
74
74
 
75
+ function buildDotRestoredModel(model) {
76
+ if (typeof model !== "string") return "";
77
+ const lower = model.toLowerCase();
78
+ const restored = lower.replace(/(\d+)-(\d+)/g, "$1.$2");
79
+ return restored === lower ? "" : restored;
80
+ }
81
+
82
+ function lookupExactCaseInsensitive(table, model) {
83
+ if (!table || !model) return null;
84
+ if (table[model]) return table[model];
85
+ const lower = model.toLowerCase();
86
+ for (const key of Object.keys(table)) {
87
+ if (key.toLowerCase() === lower) return table[key];
88
+ }
89
+ return null;
90
+ }
91
+
92
+ function lookupContainedExactCaseInsensitive(table, model) {
93
+ if (!table || !model) return null;
94
+ const lower = model.toLowerCase();
95
+ const keys = Object.keys(table).sort((a, b) => b.length - a.length);
96
+ for (const key of keys) {
97
+ if (lower.includes(key.toLowerCase())) return table[key];
98
+ }
99
+ return null;
100
+ }
101
+
75
102
  function lookupPricing(model, { curated, litellm, source } = {}) {
76
103
  if (!model || typeof model !== "string") {
77
104
  return { hit: false, source: "empty", value: null };
@@ -80,16 +107,29 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
80
107
  ? normalizeAntigravityModel(model)
81
108
  : model;
82
109
  const lower = lookupModel.toLowerCase();
110
+ const dotForm = buildDotRestoredModel(lookupModel);
83
111
 
84
112
  // 1. CURATED exact
85
113
  if (curated.exact && curated.exact[lookupModel]) {
86
114
  return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
87
115
  }
116
+ const curatedDotExact = lookupExactCaseInsensitive(curated.exact, dotForm);
117
+ if (curatedDotExact) {
118
+ return { hit: true, source: "curated:exact-dot", value: curatedDotExact };
119
+ }
120
+ const curatedDotContainedExact = lookupContainedExactCaseInsensitive(curated.exact, dotForm);
121
+ if (curatedDotContainedExact) {
122
+ return { hit: true, source: "curated:exact-dot", value: curatedDotContainedExact };
123
+ }
88
124
 
89
125
  // 2. LiteLLM exact
90
126
  if (litellm && litellm[lookupModel]) {
91
127
  return { hit: true, source: "litellm:exact", value: litellm[lookupModel] };
92
128
  }
129
+ const litellmDotExact = lookupExactCaseInsensitive(litellm, dotForm);
130
+ if (litellmDotExact) {
131
+ return { hit: true, source: "litellm:exact-dot", value: litellmDotExact };
132
+ }
93
133
 
94
134
  // 3. CURATED alias (literal mapping like "auto" -> "composer-1")
95
135
  if (curated.alias && curated.alias[lookupModel] && curated.exact[curated.alias[lookupModel]]) {
@@ -100,11 +140,22 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
100
140
  };
101
141
  }
102
142
 
103
- // 4. CURATED fuzzy substring
143
+ // 4. CURATED fuzzy substring. Also try a dot-restored variant of the input
144
+ // (digits separated by `-` rejoined as `.`) so providers that dash-normalize
145
+ // numeric segments — Droid emits `glm-5-1-0` for upstream `GLM-5.1` — still
146
+ // resolve against dot-keyed curated entries like `glm-5.1`, `glm-4.6`, etc.
147
+ // The regex only fires on digit-dash-digit, so `claude-3-7-sonnet`,
148
+ // `gpt-5-codex`, `gemini-2-5-pro` are unaffected (no digit-pair to rejoin or
149
+ // no matching curated key).
104
150
  if (Array.isArray(curated.fuzzy)) {
105
151
  for (const { match, ref } of curated.fuzzy) {
106
152
  if (!match || !ref) continue;
107
- if (lower.includes(match.toLowerCase()) && curated.exact[ref]) {
153
+ const needle = match.toLowerCase();
154
+ if (!curated.exact[ref]) continue;
155
+ if (lower.includes(needle)) {
156
+ return { hit: true, source: "curated:fuzzy", value: curated.exact[ref] };
157
+ }
158
+ if (dotForm && dotForm.includes(needle)) {
108
159
  return { hit: true, source: "curated:fuzzy", value: curated.exact[ref] };
109
160
  }
110
161
  }
@@ -125,7 +176,7 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
125
176
  const keyLower = key.toLowerCase();
126
177
  // Only accept if model is a superset of key (model contains key), to
127
178
  // avoid e.g. "gpt-5" matching "gpt-5-pro" in the wrong direction.
128
- if (lower.includes(keyLower)) {
179
+ if (lower.includes(keyLower) || (dotForm && dotForm.includes(keyLower))) {
129
180
  return { hit: true, source: "litellm:fuzzy", value: litellm[key] };
130
181
  }
131
182
  }