@kunal_dutt/token-lens 0.2.38 → 0.2.39

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.
@@ -3333,4 +3333,4 @@ ${" ".repeat(e)}}`;default:return"any"}}function ja(i){let e=["namespace functio
3333
3333
  `),O=g[y].trim().endsWith(",")?",":"",ke=s(O?A.replace(/,\s*$/,""):A,`lines ${S+1}-${y+1}`);if(ke!==A&&ke!==A.replace(/,\s*$/,"")){w.push(T[1]+ke+O),S=y;continue}}}w.push(g[S])}let C=w.join(`
3334
3334
  `),E=C.replace(/[ \t]+$/gm,"").replace(/\n{3,}/g,`
3335
3335
 
3336
- `);return E!==C&&(h=!0),C=E,C});return n.length&&r.push(`removed indentation and line breaks from ${n.length} JSON block${n.length>1?"s":""} (same data)`),h&&r.push("removed trailing spaces and extra blank lines outside code"),o(k.join(""),n.length||h?void 0:"Nothing to compact: no pretty-printed JSON or extra whitespace found.")}var v=i=>i,G={"gpt-astra":v({id:"gpt-astra",label:"GPT-6 Astra",family:"openai",contextWindow:105e4,tokenizer:"heuristic",charsPerToken:4.2,reasoning:!0,pricing:{inputPerMTok:10,outputPerMTok:50}}),"gpt-reasoning":v({id:"gpt-reasoning",label:"GPT (thinking)",family:"openai",contextWindow:196e3,tokenizer:"o200k",charsPerToken:4.2,reasoning:!0,lighterAlternative:"the Instant / non-thinking model",pricing:{inputPerMTok:5,outputPerMTok:30,cachedInputPerMTok:.5}}),gpt:v({id:"gpt",label:"GPT",family:"openai",contextWindow:128e3,tokenizer:"o200k",charsPerToken:4.2,reasoning:!1,pricing:{inputPerMTok:2.5,outputPerMTok:15,cachedInputPerMTok:.25}}),"claude-fable":v({id:"claude-fable",label:"Claude Fable",family:"anthropic",contextWindow:1e6,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!0,notes:["Claude reads XML-style tags well: wrap pasted material in <doc>\u2026</doc> instead of explaining it."],pricing:{inputPerMTok:10,outputPerMTok:50,cachedInputPerMTok:.25}}),"claude-opus":v({id:"claude-opus",label:"Claude Opus",family:"anthropic",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!1,lighterAlternative:"Sonnet or Haiku",notes:["Claude reads XML-style tags well: wrap pasted material in <doc>\u2026</doc> instead of explaining it."],pricing:{inputPerMTok:4,outputPerMTok:20,cachedInputPerMTok:.2}}),"claude-sonnet":v({id:"claude-sonnet",label:"Claude Sonnet",family:"anthropic",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!1,lighterAlternative:"Haiku",notes:["Claude reads XML-style tags well: wrap pasted material in <doc>\u2026</doc> instead of explaining it."],pricing:{inputPerMTok:2,outputPerMTok:10,cachedInputPerMTok:.2}}),"claude-haiku":v({id:"claude-haiku",label:"Claude Haiku",family:"anthropic",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!1,pricing:{inputPerMTok:.1,outputPerMTok:.5,cachedInputPerMTok:.01}}),"gemini-pro":v({id:"gemini-pro",label:"Gemini Pro",family:"google",contextWindow:1e6,tokenizer:"heuristic",charsPerToken:4,reasoning:!0,lighterAlternative:"Flash",pricing:{inputPerMTok:2,outputPerMTok:12,cachedInputPerMTok:.2,longContext:{aboveTokens:2e5,inputPerMTok:4,outputPerMTok:18}}}),"gemini-flash":v({id:"gemini-flash",label:"Gemini Flash",family:"google",contextWindow:1e6,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:1.5,outputPerMTok:9,cachedInputPerMTok:.15}}),deepseek:v({id:"deepseek",label:"DeepSeek",family:"deepseek",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.28,outputPerMTok:.42,cachedInputPerMTok:.028}}),"deepseek-reasoning":v({id:"deepseek-reasoning",label:"DeepSeek (thinking)",family:"deepseek",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!0,lighterAlternative:"the non-thinking mode",pricing:{inputPerMTok:.55,outputPerMTok:2.19,cachedInputPerMTok:.14}}),mistral:v({id:"mistral",label:"Mistral",family:"mistral",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:2,outputPerMTok:6}}),grok:v({id:"grok",label:"Grok",family:"xai",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:2,outputPerMTok:6}}),llama:v({id:"llama",label:"Llama",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.9,reasoning:!1,pricing:{inputPerMTok:.27,outputPerMTok:.85}}),qwen:v({id:"qwen",label:"Qwen",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.4,outputPerMTok:1.2}}),kimi:v({id:"kimi",label:"Kimi",family:"other",contextWindow:256e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.6,outputPerMTok:2.5,cachedInputPerMTok:.15}}),glm:v({id:"glm",label:"GLM",family:"other",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.6,outputPerMTok:2.2,cachedInputPerMTok:.11}}),command:v({id:"command",label:"Cohere Command",family:"other",contextWindow:256e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:2.5,outputPerMTok:10}}),sonar:v({id:"sonar",label:"Perplexity Sonar",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:1,outputPerMTok:1}}),gemma:v({id:"gemma",label:"Gemma",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.9,reasoning:!1,pricing:{inputPerMTok:.1,outputPerMTok:.2}}),phi:v({id:"phi",label:"Phi",family:"other",contextWindow:16e3,tokenizer:"heuristic",charsPerToken:3.9,reasoning:!1,pricing:{inputPerMTok:.07,outputPerMTok:.14}}),generic:v({id:"generic",label:"Unknown model",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:2,outputPerMTok:10}})},_i=[[/\b(fable|mythos)\b/,"claude-fable"],[/\bastra\b|\bgpt[- ]?6\b/,"gpt-astra"],[/\bclaude\b.*\bopus\b|\bopus\b/,"claude-opus"],[/\bsonnet\b/,"claude-sonnet"],[/\bhaiku\b/,"claude-haiku"],[/\b(qwen|qwq)/,"qwen"],[/\b(glm|chatglm|zhipu)\b|\bglm-?\d/,"glm"],[/\b(kimi|moonshot)/,"kimi"],[/\bgemma/,"gemma"],[/\bgemini\b.*\bflash\b|\bflash\b/,"gemini-flash"],[/\bgemini\b.*\b(pro|ultra|deep think)\b|^\s*\d(\.\d)?\s*pro\b|\bdeep think\b/,"gemini-pro"],[/\bdeepseek\b.*\b(r1|reason|think|deepthink)|\bdeepthink\b/,"deepseek-reasoning"],[/\bdeepseek\b/,"deepseek"],[/\b(mistral|le chat|magistral|codestral)\b/,"mistral"],[/\bgrok\b/,"grok"],[/\bllama|codellama/,"llama"],[/\bcommand[- ]?(r|a)\b|\bcohere\b/,"command"],[/\bsonar\b|\bperplexity\b/,"sonar"],[/\bphi-?\d/,"phi"],[/\bo[1-9](-mini|-pro)?\b|\bgpt[- ]?\S*\s*(thinking|pro)\b|\bthinking\b/,"gpt-reasoning"],[/\b(gpt|chatgpt)\b|\bgpt-?\d/,"gpt"],[/\bgemini\b/,"gemini-pro"],[/\bclaude\b/,"claude-sonnet"]];var Ei={chatgpt:"gpt",claude:"claude-sonnet",gemini:"gemini-flash",deepseek:"deepseek",mistral:"mistral",grok:"grok",generic:"generic"},Ai=[];var zr={...G};function ie(i,e="generic"){let a=(i??"").toLowerCase().replace(/\s+/g," ").trim();if(a){for(let[n,r]of Ai)if(n.test(a))return G[r];if(e==="gemini"&&/\b(pro|deep think)\b/.test(a))return G["gemini-pro"];if(e==="gemini"&&/\bflash\b/.test(a))return G["gemini-flash"];if(e==="deepseek"&&/\b(deepthink|r1|think)/.test(a))return G["deepseek-reasoning"];for(let[n,r]of _i)if(n.test(a))return G[r]}return G[Ei[e]??"generic"]}var va="ai-toolbox.co ChatGPT limits guide",ne={"chatgpt-free":{id:"chatgpt-free",site:"chatgpt",label:"ChatGPT Free",meter:"messages",caps:[]},"chatgpt-go":{id:"chatgpt-go",site:"chatgpt",label:"ChatGPT Go",meter:"messages",caps:[]},"chatgpt-plus":{id:"chatgpt-plus",site:"chatgpt",label:"ChatGPT Plus",meter:"messages",caps:[{models:["gpt"],messages:160,window:"3h",source:va,asOf:"2026-10"},{models:["gpt-reasoning"],messages:3e3,window:"week",source:va,asOf:"2026-10"}]},"chatgpt-pro":{id:"chatgpt-pro",site:"chatgpt",label:"ChatGPT Pro",meter:"messages",caps:[],note:"Pro is close to unlimited for everyday models, but GPT-6 Astra Pro use is capped (number not published)."},"chatgpt-business":{id:"chatgpt-business",site:"chatgpt",label:"ChatGPT Business",meter:"messages",caps:[]},"claude-free":{id:"claude-free",site:"claude",label:"Claude Free",meter:"compute",caps:[]},"claude-pro":{id:"claude-pro",site:"claude",label:"Claude Pro",meter:"compute",caps:[],note:"5-hour session and weekly limits shared across models; Fable runs on usage credits."},"claude-max":{id:"claude-max",site:"claude",label:"Claude Max",meter:"compute",caps:[],note:"5x or 20x Pro's session usage, with a weekly limit across models and one for the top models."},"claude-team":{id:"claude-team",site:"claude",label:"Claude Team",meter:"compute",caps:[]},"gemini-free":{id:"gemini-free",site:"gemini",label:"Gemini (free)",meter:"messages",caps:[]},"gemini-pro":{id:"gemini-pro",site:"gemini",label:"Google AI Pro",meter:"messages",caps:[]},"gemini-ultra":{id:"gemini-ultra",site:"gemini",label:"Google AI Ultra",meter:"messages",caps:[]}};var pe=5*36e5,Ie=7*864e5,Ci=1e3,Ti=500,re=i=>Math.round(i).toLocaleString("en-US");function ge(i){let e=2166136261;for(let a=0;a<i.length;a++)e=Math.imul(e^i.charCodeAt(a),16777619)>>>0;return e.toString(36)}function ya(i){let e=/\b(Free|Pro|Max|Team|Enterprise)\s+plan\b/.exec(i);if(!e)return null;let a=e[1].toLowerCase();return ne[`claude-${a==="enterprise"?"team":a}`]??null}var Ri=["sun","mon","tue","wed","thu","fri","sat"];function Pi(i,e){let a=/\b(sun|mon|tue|wed|thu|fri|sat)\w*\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i.exec(i);if(!a)return;let n=new Date(e),r=Number(a[2])%12;a[4]?.toLowerCase()==="pm"&&(r+=12),!a[4]&&Number(a[2])>=12&&(r=Number(a[2]));let t=(Ri.indexOf(a[1].slice(0,3).toLowerCase())-n.getDay()+7)%7,o=new Date(n.getFullYear(),n.getMonth(),n.getDate()+t,r,Number(a[3]??0)).getTime();return o<=e?o+Ie:o}function zi(i){let e=/resets?\s+in\s+(?:(\d+)\s*(?:hr|hour|h)s?)?\s*(?:(\d+)\s*(?:min|minute|m)s?)?/i.exec(i);if(!(!e||!e[1]&&!e[2]))return(Number(e[1]??0)*60+Number(e[2]??0))*6e4}function wa(i,e){let a=i.replace(/\s+/g," "),n=/current session(.{0,160}?)(\d{1,3})\s*%\s*used/i.exec(a),r=/weekly (?:limits?|usage)(.{0,260}?)(\d{1,3})\s*%\s*used/i.exec(a),t=!n&&/(\d{1,3})\s*%\s+of\s+your\s+(session|5-hour|weekly)\s+(?:usage\s+)?limit/i.exec(a);if(!n&&!r&&!t)return null;let o={at:e};if(n){o.sessionPct=Math.min(100,Number(n[2]));let s=zi(n[1]);s!==void 0&&(o.sessionResetsAt=e+s)}if(r){o.weekPct=Math.min(100,Number(r[2]));let s=/resets?\s+((?:sun|mon|tue|wed|thu|fri|sat)\w*\s+\d{1,2}(?::\d{2})?\s*(?:am|pm)?)/i.exec(r[1]);s&&(o.weekResetsText=s[1].trim(),o.weekResetsAt=Pi(s[1],e))}return t&&(/weekly/i.test(t[2])?o.weekPct=Math.min(100,Number(t[1])):o.sessionPct=Math.min(100,Number(t[1]))),o}var ze=i=>new Date(i).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});function xe(i,e){return{...i,sends:i.sends.filter(a=>e-a.at<Ie).slice(-5e3),readings:i.readings.filter(a=>e-a.at<Ie).slice(-50)}}function Sa(i,e){let a=[...i.readings].reverse().find(t=>t.sessionPct!==void 0),n=i.pctPer1k;if(a&&e.sessionPct!==void 0&&a.sessionPct!==void 0&&e.sessionPct>a.sessionPct&&e.at-a.at<pe){let t=i.sends.filter(o=>o.at>a.at&&o.at<=e.at).reduce((o,s)=>o+s.tokens,0);if(t>=5e3){let o=(e.sessionPct-a.sessionPct)/t*1e3;n=n===void 0?o:n*.5+o*.5}}let r=a&&a.sessionPct===e.sessionPct&&a.weekPct===e.weekPct&&e.at-a.at<10*6e4;return{...i,pctPer1k:n,readings:r?i.readings:[...i.readings,e]}}function _a(i){let{data:e,now:a,plan:n,current:r}=i,t=[...e.readings].reverse().find(p=>p.sessionPct!==void 0||p.weekPct!==void 0),o=[...e.readings].reverse().find(p=>p.weekPct!==void 0),s=[...e.readings].reverse().find(p=>p.sessionPct!==void 0),l=s&&(s.sessionResetsAt??s.at+pe)>a,u=l&&s.sessionResetsAt?s.sessionResetsAt-pe:a-pe,d=e.sends.filter(p=>p.at>=u),h=p=>p.reduce((y,A)=>y+A.tokens,0),k=h(d),c={plan:n,planSource:i.planSource,session:{messages:d.length,tokens:k,since:u},week:{messages:e.sends.length,tokens:h(e.sends)},topChats:[],insights:[]};if(l&&s?.sessionPct!==void 0){let p=h(e.sends.filter(A=>A.at>s.at)),y=s.sessionPct+(e.pctPer1k?p/1e3*e.pctPer1k:0);c.sessionPct=Math.min(100,Math.round(y)),c.sessionResetsAt=s.sessionResetsAt,c.meterAgeMin=Math.round((a-s.at)/6e4)}o?.weekPct!==void 0&&(o.weekResetsAt??1/0)>a&&(c.weekPct=o.weekPct,c.weekResetsAt=o.weekResetsAt,c.weekResetsText=o.weekResetsText);let f=new Map;for(let p of d){let y=f.get(p.chat)??{chat:p.chat,title:i.titles?.get(p.chat),tokens:0,messages:0,share:0};y.tokens+=p.tokens,y.messages++,f.set(p.chat,y)}c.topChats=[...f.values()].map(p=>({...p,share:k?p.tokens/k:0})).sort((p,y)=>y.tokens-p.tokens).slice(0,5);let g=Ti,w=(r?.tokens??0)+g,C=Ci+g;if(c.sessionPct!==void 0&&e.pctPer1k){let p=Math.max(0,100-c.sessionPct)/e.pctPer1k*1e3;c.left={here:Math.floor(p/w),fresh:Math.floor(p/C)}}let E=c.insights,S=n?n.label:"your plan";if(c.sessionPct!==void 0){let p=c.sessionResetsAt?`; it resets at ${ze(c.sessionResetsAt)}`:"";c.left&&r&&r.tokens>=1e4&&c.left.fresh>=c.left.here*2?E.push(`About ${re(c.left.here)} more messages fit in ${S}'s session at this chat's size, or about ${re(c.left.fresh)} in a new chat (${c.sessionPct}% used${p}). For a new topic, start a new chat. ${z}`):c.left?E.push(`About ${re(c.left.fresh)} more messages like your recent ones fit in this session (${c.sessionPct}% used${p}).`):E.push(`Session ${c.sessionPct}% used${p}.`);let y=s&&e.readings.find(A=>A.sessionPct!==void 0&&A.at>=u);if(y&&s&&y!==s&&c.sessionResetsAt&&c.sessionPct<100){let A=(c.sessionPct-(y.sessionPct??0))/Math.max(1,a-y.at),O=A>0?a+(100-c.sessionPct)/A:1/0;O<c.sessionResetsAt&&E.push(`At this pace the session runs out around ${ze(O)}, before it resets at ${ze(c.sessionResetsAt)}. Save the big asks for after the reset, or start new topics in new chats.`)}}let T=c.topChats[0];return T&&d.length>=4&&T.share>=.5&&f.size>1&&E.push(`${Math.round(T.share*100)}% of this session's usage came from one chat${T.title?` ("${T.title}")`:""}: each message there re-read about ${re(T.tokens/T.messages)} tokens.`),c.weekPct!==void 0&&c.weekResetsAt&&((c.weekResetsAt-a)/36e5<=36&&c.weekPct<=50?E.push(`${100-c.weekPct}% of ${S}'s weekly allowance is unused and resets ${c.weekResetsText??"soon"}. It doesn't carry over, so this is a good time for the big jobs you've been holding back.`):c.weekPct>=80&&E.push(`Week ${c.weekPct}% used${c.weekResetsText?`, resets ${c.weekResetsText}`:""}. New topics in new chats and one complete message per ask stretch what's left.`)),r&&r.tokens>=2e4&&!E.some(p=>p.includes("new chat"))&&E.push(`Every message in this chat re-reads about ${re(r.tokens)} tokens, ${Math.max(2,Math.round(w/C))}\xD7 a new chat. On ${S} that's what uses your allowance fastest.`),n?c.sessionPct===void 0&&n.site==="claude"&&E.push("Open Settings > Usage in Claude once and Token Lens reads your meter from it, to count how many messages you have left."):E.push("Tell Token Lens your plan once (Free, Pro, Max or Team) for advice that fits it."),c}var Ii=/^(?:ok(?:ay)?|k|thanks?(?: you)?(?: so much)?|thx|ty|great|perfect|nice|cool|awesome|got it|understood|sounds good|lgtm|yes|yep|sure|done|👍|🙏)[\s!.,🙂😊👍🙏]*$/i;function Ea(i){return Ii.test(i.trim())}var Me=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim();function Aa(i,e){let a=Me(i);return a.split(" ").length<8?!1:Me(e).includes(a)}function Ca(i){return i.split(/\n\s*\n/).map(e=>e.trim()).filter(e=>e.length>=1200).map(e=>ge(Me(e)))}function he(i){let e=typeof i.model=="string"?ie(i.model,i.site):i.model,a=i.pricing?{...e,pricing:i.pricing}:e,n=le(i.prompt),{tokens:r,exact:t}=_(i.prompt,a),o=J(i.conversationTokens??0,r,a,i.contextWindow),s=De(i.level??"auto",o),l=V(i.prompt,{profile:a,task:n.task,level:s}),u=fa(l.text,a),d=u.saved>0&&u.blocks.length?{...l,text:u.text,tokens:u.after,saved:l.originalTokens-u.after,rules:[...l.rules,"JSON whitespace (same data)"]}:l,h=fe({profile:a,task:n.task,signals:n.signals,promptTokens:r,usage:o,text:i.prompt}),k={pricing:a.pricing,promptUsd:N(r,a),sendUsd:N((i.conversationTokens??0)+r,a),saved:W(d.saved,a)},c=Object.entries(Re(i.prompt,d.text)).map(([g,[w,C]])=>`${C}/${w} ${g}s`).join(", "),f={text:d.text,tokens:d.tokens,changes:d.saved>0?d.rules.map(g=>`removed ${g}`):[],kept:c||"every instruction"};return{profile:a,task:n,promptTokens:r,exact:t,usage:o,level:s,optimized:d,tips:h,handoffPrompt:Ne(n.task),cost:k,best:f}}var ji=60,Di=20,Ni=12,Li=1800,Oi=25e3,Bi=15e3,Fi=80,Gi=3*6e4,K=i=>Math.round(i).toLocaleString("en-US"),Ta=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim(),be=class{constructor(e="claude-sonnet",a={}){m(this,"profile");m(this,"draftText","");m(this,"draftSince",0);m(this,"draftDone","");m(this,"lastDraftNotice",-1/0);m(this,"chatTold",new Map);m(this,"chatTokens",new Map);m(this,"chatEpoch",new Map);m(this,"chatText","");m(this,"chatId","");m(this,"chatTitle","");m(this,"titles",new Map);m(this,"pending",null);m(this,"pastes",new Map);m(this,"told",new Set);m(this,"data");m(this,"pickedPlan");m(this,"appPlan",null);m(this,"onData");m(this,"state");this.profile=ie(e),this.data=xe(a.data??{sends:[],readings:[]},Date.now()),this.pickedPlan=a.plan&&ne[a.plan]||null,this.onData=a.onData,this.state={model:this.profile.label,draft:null,chat:null,coach:null},this.refreshCoach(Date.now())}get currentDraft(){return this.draftText}get plan(){return this.pickedPlan??this.appPlan}setPlan(e,a=Date.now()){return this.pickedPlan=e&&ne[e]||null,this.refreshCoach(a),this.plan}draft(e,a){let n=(e??"").trim();if(n!==this.draftText)return e===""&&this.draftText.length>2&&this.chatId?this.pending={probe:Ta(this.draftText).slice(0,80),tokens:this.state.chat?.tokens??0,chat:this.chatId,at:a,text:this.draftText}:this.pending&&n===this.pending.text&&(this.pending=null),this.draftText=n,this.draftSince=a,this.state.draft=n?this.analyzeDraft(n):null,null;let r=this.state.draft;if(!r||a-this.draftSince<Li)return null;let t=ge(n);if(t===this.draftDone||a-this.lastDraftNotice<Oi)return null;let o=this.draftNotice(n,r);return o?(this.draftDone=t,this.lastDraftNotice=a,o):null}draftNotice(e,a){let n=this.state.chat?.tokens??0,r=this.plan?.label??"your plan";if(Aa(e,this.chatText))return{title:"You already asked this in this chat",body:`Its answer is above. Sending it again re-reads about ${K(n+a.tokens)} tokens of ${r}'s usage for the same question.`};if(Ea(e)&&n>=Bi)return{title:`"${e.slice(0,24)}" re-reads this whole chat`,body:`Sending it uses about ${K(n)} tokens of ${r}'s usage, as much as a long question. If there's nothing to ask, you can skip it, or add it to your next real message.`};let t=Ca(e);for(let s of t){let l=this.pastes.get(s)??new Set;if(l.add(this.chatId),this.pastes.set(s,l),l.size>=2&&!this.told.has(`paste:${s}`))return this.told.add(`paste:${s}`),{title:`You've pasted this text into ${l.size} chats`,body:"Put it in a Claude Project's knowledge or instructions once, and every chat in that Project has it without pasting it again."}}return a.tokens>=ji&&a.saved>=Di&&a.pct>=Ni?{title:`Draft: ${K(a.tokens)} \u2192 ${K(a.optimizedTokens)} tokens (${a.pct}% fewer)`,body:`Same request, ${a.savedUsd===null?"fewer tokens":`${Pe(a.savedUsd)} less per send at API prices`}. One click puts the shorter version in Claude's message box: open the Token Lens panel.${a.tips[0]?` ${a.tips[0]}`:""}`}:a.tips.length?{title:"Token Lens tip for your draft",body:a.tips[0]}:null}chat(e,a,n=Date.now()){let{tokens:r}=_(a,this.profile),t=J(r,0,this.profile),o=N(r,this.profile),s=[],l=null;t.zone==="hot"||t.zone==="full"?(l=t.zone,s.push(`This chat fills about ${Math.round(t.ratio*100)}% of the context, so older messages will soon be dropped or compacted. ${z}`)):(t.zone==="warm"||t.longChat)&&(l=t.zone==="warm"?"warm":"long",s.push(`Every message you send re-reads about ${K(r)} tokens of this chat (${Pe(o)} each at API prices). Starting a new chat for a new topic avoids that.`)),this.state.chat={tokens:r,ratio:t.ratio,window:t.window,perMessageUsd:o,tips:s},r<(this.chatTokens.get(e)??0)*.5&&(this.chatTold.delete(e),this.chatEpoch.set(e,(this.chatEpoch.get(e)??0)+1)),this.chatTokens.set(e,r),this.chatText=a,this.chatId=ge(`${e}#${this.chatEpoch.get(e)??0}`),this.chatTitle=e&&!/^claude$/i.test(e)?e.slice(0,60):"",this.chatTitle&&this.titles.set(this.chatId,this.chatTitle);let u=this.observe(a,n),d=null;if(l){let h=this.chatTold.get(e)??new Set;this.chatTold.set(e,h),h.has(l)||(h.add(l),d={title:`This chat: ${K(r)} tokens (${Math.round(t.ratio*100)}% of context)`,body:s[0]})}return u??d}observe(e,a){let n=!1,r=ya(e);r&&(this.appPlan=r);let t=wa(e,a);t&&(this.data=Sa(this.data,t),n=!0);let o=this.pending;o&&a-o.at>Gi?this.pending=null:o&&o.probe&&Ta(e).includes(o.probe)&&(this.data.sends.push({at:o.at,tokens:o.tokens+_(o.text,this.profile).tokens,chat:o.chat}),this.pending=null,n=!0),n&&(this.data=xe(this.data,a),this.onData?.(this.data));let s=this.refreshCoach(a);if(s.sessionPct!==void 0&&s.sessionPct>=Fi){let u=`session:${Math.round(s.session.since/36e5)}`;if(!this.told.has(u))return this.told.add(u),{title:`${this.plan?.label??"Claude"} session ${s.sessionPct}% used`,body:s.insights[0]??"New topics in new chats stretch what's left."}}let l=s.insights.find(u=>u.includes("weekly allowance is unused"));if(l&&s.weekResetsAt){let u=`week:${Math.round(s.weekResetsAt/864e5)}`;if(!this.told.has(u))return this.told.add(u),{title:`${100-(s.weekPct??0)}% of your week is still unused`,body:l}}return null}refreshCoach(e=Date.now()){let a=_a({data:this.data,now:e,plan:this.plan,planSource:this.pickedPlan?"set":this.appPlan?"app":"unknown",current:this.chatId?{chat:this.chatId,title:this.chatTitle,tokens:this.state.chat?.tokens??0}:void 0,titles:this.titles});return this.state.coach=a,a}analyzeDraft(e){let a=he({prompt:e,model:this.profile,conversationTokens:this.state.chat?.tokens??0,level:"auto"}),n=a.optimized.saved;return{tokens:a.promptTokens,optimizedTokens:a.optimized.tokens,saved:n,pct:a.optimized.originalTokens?Math.round(n/a.optimized.originalTokens*100):0,savedUsd:a.cost.saved.usd,best:n>0?a.best.text:e,kept:a.best.kept,tips:a.tips.filter(r=>!["handoff","price-threshold","family-note"].includes(r.id)).map(r=>r.text)}}};var L=null,je="",qi={version:"0.2.38",start(i,e,a){let n;try{n=e?JSON.parse(e):void 0,(!n||!Array.isArray(n.sends)||!Array.isArray(n.readings))&&(n=void 0)}catch{n=void 0}L=new be(i||"claude-sonnet",{data:n,plan:a||void 0,onData:r=>je=JSON.stringify(r)})},draft(i,e){let a=L?.draft(i,e);return a?JSON.stringify(a):""},chat(i,e,a){let n=L?.chat(i,e,a);return n?JSON.stringify(n):""},state(i){return L?(L.refreshCoach(i),JSON.stringify(L.state)):"{}"},currentDraft(){return L?.currentDraft??""},setPlan(i){L?.setPlan(i||null)},takeData(){let i=je;return je="",i},optimize(i,e){let a=he({prompt:i,model:e,level:"auto"});return JSON.stringify({best:a.best.text,tokens:a.promptTokens,optimizedTokens:a.optimized.tokens,kept:a.best.kept})}};globalThis.TokenLensCore=qi;})();
3336
+ `);return E!==C&&(h=!0),C=E,C});return n.length&&r.push(`removed indentation and line breaks from ${n.length} JSON block${n.length>1?"s":""} (same data)`),h&&r.push("removed trailing spaces and extra blank lines outside code"),o(k.join(""),n.length||h?void 0:"Nothing to compact: no pretty-printed JSON or extra whitespace found.")}var v=i=>i,G={"gpt-astra":v({id:"gpt-astra",label:"GPT-6 Astra",family:"openai",contextWindow:105e4,tokenizer:"heuristic",charsPerToken:4.2,reasoning:!0,pricing:{inputPerMTok:10,outputPerMTok:50}}),"gpt-reasoning":v({id:"gpt-reasoning",label:"GPT (thinking)",family:"openai",contextWindow:196e3,tokenizer:"o200k",charsPerToken:4.2,reasoning:!0,lighterAlternative:"the Instant / non-thinking model",pricing:{inputPerMTok:5,outputPerMTok:30,cachedInputPerMTok:.5}}),gpt:v({id:"gpt",label:"GPT",family:"openai",contextWindow:128e3,tokenizer:"o200k",charsPerToken:4.2,reasoning:!1,pricing:{inputPerMTok:2.5,outputPerMTok:15,cachedInputPerMTok:.25}}),"claude-fable":v({id:"claude-fable",label:"Claude Fable",family:"anthropic",contextWindow:1e6,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!0,notes:["Claude reads XML-style tags well: wrap pasted material in <doc>\u2026</doc> instead of explaining it."],pricing:{inputPerMTok:10,outputPerMTok:50,cachedInputPerMTok:.25}}),"claude-opus":v({id:"claude-opus",label:"Claude Opus",family:"anthropic",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!1,lighterAlternative:"Sonnet or Haiku",notes:["Claude reads XML-style tags well: wrap pasted material in <doc>\u2026</doc> instead of explaining it."],pricing:{inputPerMTok:4,outputPerMTok:20,cachedInputPerMTok:.2}}),"claude-sonnet":v({id:"claude-sonnet",label:"Claude Sonnet",family:"anthropic",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!1,lighterAlternative:"Haiku",notes:["Claude reads XML-style tags well: wrap pasted material in <doc>\u2026</doc> instead of explaining it."],pricing:{inputPerMTok:2,outputPerMTok:10,cachedInputPerMTok:.2}}),"claude-haiku":v({id:"claude-haiku",label:"Claude Haiku",family:"anthropic",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.5,reasoning:!1,pricing:{inputPerMTok:.1,outputPerMTok:.5,cachedInputPerMTok:.01}}),"gemini-pro":v({id:"gemini-pro",label:"Gemini Pro",family:"google",contextWindow:1e6,tokenizer:"heuristic",charsPerToken:4,reasoning:!0,lighterAlternative:"Flash",pricing:{inputPerMTok:2,outputPerMTok:12,cachedInputPerMTok:.2,longContext:{aboveTokens:2e5,inputPerMTok:4,outputPerMTok:18}}}),"gemini-flash":v({id:"gemini-flash",label:"Gemini Flash",family:"google",contextWindow:1e6,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:1.5,outputPerMTok:9,cachedInputPerMTok:.15}}),deepseek:v({id:"deepseek",label:"DeepSeek",family:"deepseek",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.28,outputPerMTok:.42,cachedInputPerMTok:.028}}),"deepseek-reasoning":v({id:"deepseek-reasoning",label:"DeepSeek (thinking)",family:"deepseek",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!0,lighterAlternative:"the non-thinking mode",pricing:{inputPerMTok:.55,outputPerMTok:2.19,cachedInputPerMTok:.14}}),mistral:v({id:"mistral",label:"Mistral",family:"mistral",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:2,outputPerMTok:6}}),grok:v({id:"grok",label:"Grok",family:"xai",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:2,outputPerMTok:6}}),llama:v({id:"llama",label:"Llama",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.9,reasoning:!1,pricing:{inputPerMTok:.27,outputPerMTok:.85}}),qwen:v({id:"qwen",label:"Qwen",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.4,outputPerMTok:1.2}}),kimi:v({id:"kimi",label:"Kimi",family:"other",contextWindow:256e3,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.6,outputPerMTok:2.5,cachedInputPerMTok:.15}}),glm:v({id:"glm",label:"GLM",family:"other",contextWindow:2e5,tokenizer:"heuristic",charsPerToken:3.8,reasoning:!1,pricing:{inputPerMTok:.6,outputPerMTok:2.2,cachedInputPerMTok:.11}}),command:v({id:"command",label:"Cohere Command",family:"other",contextWindow:256e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:2.5,outputPerMTok:10}}),sonar:v({id:"sonar",label:"Perplexity Sonar",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:1,outputPerMTok:1}}),gemma:v({id:"gemma",label:"Gemma",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:3.9,reasoning:!1,pricing:{inputPerMTok:.1,outputPerMTok:.2}}),phi:v({id:"phi",label:"Phi",family:"other",contextWindow:16e3,tokenizer:"heuristic",charsPerToken:3.9,reasoning:!1,pricing:{inputPerMTok:.07,outputPerMTok:.14}}),generic:v({id:"generic",label:"Unknown model",family:"other",contextWindow:128e3,tokenizer:"heuristic",charsPerToken:4,reasoning:!1,pricing:{inputPerMTok:2,outputPerMTok:10}})},_i=[[/\b(fable|mythos)\b/,"claude-fable"],[/\bastra\b|\bgpt[- ]?6\b/,"gpt-astra"],[/\bclaude\b.*\bopus\b|\bopus\b/,"claude-opus"],[/\bsonnet\b/,"claude-sonnet"],[/\bhaiku\b/,"claude-haiku"],[/\b(qwen|qwq)/,"qwen"],[/\b(glm|chatglm|zhipu)\b|\bglm-?\d/,"glm"],[/\b(kimi|moonshot)/,"kimi"],[/\bgemma/,"gemma"],[/\bgemini\b.*\bflash\b|\bflash\b/,"gemini-flash"],[/\bgemini\b.*\b(pro|ultra|deep think)\b|^\s*\d(\.\d)?\s*pro\b|\bdeep think\b/,"gemini-pro"],[/\bdeepseek\b.*\b(r1|reason|think|deepthink)|\bdeepthink\b/,"deepseek-reasoning"],[/\bdeepseek\b/,"deepseek"],[/\b(mistral|le chat|magistral|codestral)\b/,"mistral"],[/\bgrok\b/,"grok"],[/\bllama|codellama/,"llama"],[/\bcommand[- ]?(r|a)\b|\bcohere\b/,"command"],[/\bsonar\b|\bperplexity\b/,"sonar"],[/\bphi-?\d/,"phi"],[/\bo[1-9](-mini|-pro)?\b|\bgpt[- ]?\S*\s*(thinking|pro)\b|\bthinking\b/,"gpt-reasoning"],[/\b(gpt|chatgpt)\b|\bgpt-?\d/,"gpt"],[/\bgemini\b/,"gemini-pro"],[/\bclaude\b/,"claude-sonnet"]];var Ei={chatgpt:"gpt",claude:"claude-sonnet",gemini:"gemini-flash",deepseek:"deepseek",mistral:"mistral",grok:"grok",generic:"generic"},Ai=[];var zr={...G};function ie(i,e="generic"){let a=(i??"").toLowerCase().replace(/\s+/g," ").trim();if(a){for(let[n,r]of Ai)if(n.test(a))return G[r];if(e==="gemini"&&/\b(pro|deep think)\b/.test(a))return G["gemini-pro"];if(e==="gemini"&&/\bflash\b/.test(a))return G["gemini-flash"];if(e==="deepseek"&&/\b(deepthink|r1|think)/.test(a))return G["deepseek-reasoning"];for(let[n,r]of _i)if(n.test(a))return G[r]}return G[Ei[e]??"generic"]}var va="ai-toolbox.co ChatGPT limits guide",ne={"chatgpt-free":{id:"chatgpt-free",site:"chatgpt",label:"ChatGPT Free",meter:"messages",caps:[]},"chatgpt-go":{id:"chatgpt-go",site:"chatgpt",label:"ChatGPT Go",meter:"messages",caps:[]},"chatgpt-plus":{id:"chatgpt-plus",site:"chatgpt",label:"ChatGPT Plus",meter:"messages",caps:[{models:["gpt"],messages:160,window:"3h",source:va,asOf:"2026-10"},{models:["gpt-reasoning"],messages:3e3,window:"week",source:va,asOf:"2026-10"}]},"chatgpt-pro":{id:"chatgpt-pro",site:"chatgpt",label:"ChatGPT Pro",meter:"messages",caps:[],note:"Pro is close to unlimited for everyday models, but GPT-6 Astra Pro use is capped (number not published)."},"chatgpt-business":{id:"chatgpt-business",site:"chatgpt",label:"ChatGPT Business",meter:"messages",caps:[]},"claude-free":{id:"claude-free",site:"claude",label:"Claude Free",meter:"compute",caps:[]},"claude-pro":{id:"claude-pro",site:"claude",label:"Claude Pro",meter:"compute",caps:[],note:"5-hour session and weekly limits shared across models; Fable runs on usage credits."},"claude-max":{id:"claude-max",site:"claude",label:"Claude Max",meter:"compute",caps:[],note:"5x or 20x Pro's session usage, with a weekly limit across models and one for the top models."},"claude-team":{id:"claude-team",site:"claude",label:"Claude Team",meter:"compute",caps:[]},"gemini-free":{id:"gemini-free",site:"gemini",label:"Gemini (free)",meter:"messages",caps:[]},"gemini-pro":{id:"gemini-pro",site:"gemini",label:"Google AI Pro",meter:"messages",caps:[]},"gemini-ultra":{id:"gemini-ultra",site:"gemini",label:"Google AI Ultra",meter:"messages",caps:[]}};var pe=5*36e5,Ie=7*864e5,Ci=1e3,Ti=500,re=i=>Math.round(i).toLocaleString("en-US");function ge(i){let e=2166136261;for(let a=0;a<i.length;a++)e=Math.imul(e^i.charCodeAt(a),16777619)>>>0;return e.toString(36)}function ya(i){let e=/\b(Free|Pro|Max|Team|Enterprise)\s+plan\b/.exec(i);if(!e)return null;let a=e[1].toLowerCase();return ne[`claude-${a==="enterprise"?"team":a}`]??null}var Ri=["sun","mon","tue","wed","thu","fri","sat"];function Pi(i,e){let a=/\b(sun|mon|tue|wed|thu|fri|sat)\w*\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i.exec(i);if(!a)return;let n=new Date(e),r=Number(a[2])%12;a[4]?.toLowerCase()==="pm"&&(r+=12),!a[4]&&Number(a[2])>=12&&(r=Number(a[2]));let t=(Ri.indexOf(a[1].slice(0,3).toLowerCase())-n.getDay()+7)%7,o=new Date(n.getFullYear(),n.getMonth(),n.getDate()+t,r,Number(a[3]??0)).getTime();return o<=e?o+Ie:o}function zi(i){let e=/resets?\s+in\s+(?:(\d+)\s*(?:hr|hour|h)s?)?\s*(?:(\d+)\s*(?:min|minute|m)s?)?/i.exec(i);if(!(!e||!e[1]&&!e[2]))return(Number(e[1]??0)*60+Number(e[2]??0))*6e4}function wa(i,e){let a=i.replace(/\s+/g," "),n=/current session(.{0,160}?)(\d{1,3})\s*%\s*used/i.exec(a),r=/weekly (?:limits?|usage)(.{0,260}?)(\d{1,3})\s*%\s*used/i.exec(a),t=!n&&/(\d{1,3})\s*%\s+of\s+your\s+(session|5-hour|weekly)\s+(?:usage\s+)?limit/i.exec(a);if(!n&&!r&&!t)return null;let o={at:e};if(n){o.sessionPct=Math.min(100,Number(n[2]));let s=zi(n[1]);s!==void 0&&(o.sessionResetsAt=e+s)}if(r){o.weekPct=Math.min(100,Number(r[2]));let s=/resets?\s+((?:sun|mon|tue|wed|thu|fri|sat)\w*\s+\d{1,2}(?::\d{2})?\s*(?:am|pm)?)/i.exec(r[1]);s&&(o.weekResetsText=s[1].trim(),o.weekResetsAt=Pi(s[1],e))}return t&&(/weekly/i.test(t[2])?o.weekPct=Math.min(100,Number(t[1])):o.sessionPct=Math.min(100,Number(t[1]))),o}var ze=i=>new Date(i).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});function xe(i,e){return{...i,sends:i.sends.filter(a=>e-a.at<Ie).slice(-5e3),readings:i.readings.filter(a=>e-a.at<Ie).slice(-50)}}function Sa(i,e){let a=[...i.readings].reverse().find(t=>t.sessionPct!==void 0),n=i.pctPer1k;if(a&&e.sessionPct!==void 0&&a.sessionPct!==void 0&&e.sessionPct>a.sessionPct&&e.at-a.at<pe){let t=i.sends.filter(o=>o.at>a.at&&o.at<=e.at).reduce((o,s)=>o+s.tokens,0);if(t>=5e3){let o=(e.sessionPct-a.sessionPct)/t*1e3;n=n===void 0?o:n*.5+o*.5}}let r=a&&a.sessionPct===e.sessionPct&&a.weekPct===e.weekPct&&e.at-a.at<10*6e4;return{...i,pctPer1k:n,readings:r?i.readings:[...i.readings,e]}}function _a(i){let{data:e,now:a,plan:n,current:r}=i,t=[...e.readings].reverse().find(p=>p.sessionPct!==void 0||p.weekPct!==void 0),o=[...e.readings].reverse().find(p=>p.weekPct!==void 0),s=[...e.readings].reverse().find(p=>p.sessionPct!==void 0),l=s&&(s.sessionResetsAt??s.at+pe)>a,u=l&&s.sessionResetsAt?s.sessionResetsAt-pe:a-pe,d=e.sends.filter(p=>p.at>=u),h=p=>p.reduce((y,A)=>y+A.tokens,0),k=h(d),c={plan:n,planSource:i.planSource,session:{messages:d.length,tokens:k,since:u},week:{messages:e.sends.length,tokens:h(e.sends)},topChats:[],insights:[]};if(l&&s?.sessionPct!==void 0){let p=h(e.sends.filter(A=>A.at>s.at)),y=s.sessionPct+(e.pctPer1k?p/1e3*e.pctPer1k:0);c.sessionPct=Math.min(100,Math.round(y)),c.sessionResetsAt=s.sessionResetsAt,c.meterAgeMin=Math.round((a-s.at)/6e4)}o?.weekPct!==void 0&&(o.weekResetsAt??1/0)>a&&(c.weekPct=o.weekPct,c.weekResetsAt=o.weekResetsAt,c.weekResetsText=o.weekResetsText);let f=new Map;for(let p of d){let y=f.get(p.chat)??{chat:p.chat,title:i.titles?.get(p.chat),tokens:0,messages:0,share:0};y.tokens+=p.tokens,y.messages++,f.set(p.chat,y)}c.topChats=[...f.values()].map(p=>({...p,share:k?p.tokens/k:0})).sort((p,y)=>y.tokens-p.tokens).slice(0,5);let g=Ti,w=(r?.tokens??0)+g,C=Ci+g;if(c.sessionPct!==void 0&&e.pctPer1k){let p=Math.max(0,100-c.sessionPct)/e.pctPer1k*1e3;c.left={here:Math.floor(p/w),fresh:Math.floor(p/C)}}let E=c.insights,S=n?n.label:"your plan";if(c.sessionPct!==void 0){let p=c.sessionResetsAt?`; it resets at ${ze(c.sessionResetsAt)}`:"";c.left&&r&&r.tokens>=1e4&&c.left.fresh>=c.left.here*2?E.push(`About ${re(c.left.here)} more messages fit in ${S}'s session at this chat's size, or about ${re(c.left.fresh)} in a new chat (${c.sessionPct}% used${p}). For a new topic, start a new chat. ${z}`):c.left?E.push(`About ${re(c.left.fresh)} more messages like your recent ones fit in this session (${c.sessionPct}% used${p}).`):E.push(`Session ${c.sessionPct}% used${p}.`);let y=s&&e.readings.find(A=>A.sessionPct!==void 0&&A.at>=u);if(y&&s&&y!==s&&c.sessionResetsAt&&c.sessionPct<100){let A=(c.sessionPct-(y.sessionPct??0))/Math.max(1,a-y.at),O=A>0?a+(100-c.sessionPct)/A:1/0;O<c.sessionResetsAt&&E.push(`At this pace the session runs out around ${ze(O)}, before it resets at ${ze(c.sessionResetsAt)}. Save the big asks for after the reset, or start new topics in new chats.`)}}let T=c.topChats[0];return T&&d.length>=4&&T.share>=.5&&f.size>1&&E.push(`${Math.round(T.share*100)}% of this session's usage came from one chat${T.title?` ("${T.title}")`:""}: each message there re-read about ${re(T.tokens/T.messages)} tokens.`),c.weekPct!==void 0&&c.weekResetsAt&&((c.weekResetsAt-a)/36e5<=36&&c.weekPct<=50?E.push(`${100-c.weekPct}% of ${S}'s weekly allowance is unused and resets ${c.weekResetsText??"soon"}. It doesn't carry over, so this is a good time for the big jobs you've been holding back.`):c.weekPct>=80&&E.push(`Week ${c.weekPct}% used${c.weekResetsText?`, resets ${c.weekResetsText}`:""}. New topics in new chats and one complete message per ask stretch what's left.`)),r&&r.tokens>=2e4&&!E.some(p=>p.includes("new chat"))&&E.push(`Every message in this chat re-reads about ${re(r.tokens)} tokens, ${Math.max(2,Math.round(w/C))}\xD7 a new chat. On ${S} that's what uses your allowance fastest.`),n?c.sessionPct===void 0&&n.site==="claude"&&E.push("Open Settings > Usage in Claude once and Token Lens reads your meter from it, to count how many messages you have left."):E.push("Tell Token Lens your plan once (Free, Pro, Max or Team) for advice that fits it."),c}var Ii=/^(?:ok(?:ay)?|k|thanks?(?: you)?(?: so much)?|thx|ty|great|perfect|nice|cool|awesome|got it|understood|sounds good|lgtm|yes|yep|sure|done|👍|🙏)[\s!.,🙂😊👍🙏]*$/i;function Ea(i){return Ii.test(i.trim())}var Me=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim();function Aa(i,e){let a=Me(i);return a.split(" ").length<8?!1:Me(e).includes(a)}function Ca(i){return i.split(/\n\s*\n/).map(e=>e.trim()).filter(e=>e.length>=1200).map(e=>ge(Me(e)))}function he(i){let e=typeof i.model=="string"?ie(i.model,i.site):i.model,a=i.pricing?{...e,pricing:i.pricing}:e,n=le(i.prompt),{tokens:r,exact:t}=_(i.prompt,a),o=J(i.conversationTokens??0,r,a,i.contextWindow),s=De(i.level??"auto",o),l=V(i.prompt,{profile:a,task:n.task,level:s}),u=fa(l.text,a),d=u.saved>0&&u.blocks.length?{...l,text:u.text,tokens:u.after,saved:l.originalTokens-u.after,rules:[...l.rules,"JSON whitespace (same data)"]}:l,h=fe({profile:a,task:n.task,signals:n.signals,promptTokens:r,usage:o,text:i.prompt}),k={pricing:a.pricing,promptUsd:N(r,a),sendUsd:N((i.conversationTokens??0)+r,a),saved:W(d.saved,a)},c=Object.entries(Re(i.prompt,d.text)).map(([g,[w,C]])=>`${C}/${w} ${g}s`).join(", "),f={text:d.text,tokens:d.tokens,changes:d.saved>0?d.rules.map(g=>`removed ${g}`):[],kept:c||"every instruction"};return{profile:a,task:n,promptTokens:r,exact:t,usage:o,level:s,optimized:d,tips:h,handoffPrompt:Ne(n.task),cost:k,best:f}}var ji=60,Di=20,Ni=12,Li=1800,Oi=25e3,Bi=15e3,Fi=80,Gi=3*6e4,K=i=>Math.round(i).toLocaleString("en-US"),Ta=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim(),be=class{constructor(e="claude-sonnet",a={}){m(this,"profile");m(this,"draftText","");m(this,"draftSince",0);m(this,"draftDone","");m(this,"lastDraftNotice",-1/0);m(this,"chatTold",new Map);m(this,"chatTokens",new Map);m(this,"chatEpoch",new Map);m(this,"chatText","");m(this,"chatId","");m(this,"chatTitle","");m(this,"titles",new Map);m(this,"pending",null);m(this,"pastes",new Map);m(this,"told",new Set);m(this,"data");m(this,"pickedPlan");m(this,"appPlan",null);m(this,"onData");m(this,"state");this.profile=ie(e),this.data=xe(a.data??{sends:[],readings:[]},Date.now()),this.pickedPlan=a.plan&&ne[a.plan]||null,this.onData=a.onData,this.state={model:this.profile.label,draft:null,chat:null,coach:null},this.refreshCoach(Date.now())}get currentDraft(){return this.draftText}get plan(){return this.pickedPlan??this.appPlan}setPlan(e,a=Date.now()){return this.pickedPlan=e&&ne[e]||null,this.refreshCoach(a),this.plan}draft(e,a){let n=(e??"").trim();if(n!==this.draftText)return e===""&&this.draftText.length>2&&this.chatId?this.pending={probe:Ta(this.draftText).slice(0,80),tokens:this.state.chat?.tokens??0,chat:this.chatId,at:a,text:this.draftText}:this.pending&&n===this.pending.text&&(this.pending=null),this.draftText=n,this.draftSince=a,this.state.draft=n?this.analyzeDraft(n):null,null;let r=this.state.draft;if(!r||a-this.draftSince<Li)return null;let t=ge(n);if(t===this.draftDone||a-this.lastDraftNotice<Oi)return null;let o=this.draftNotice(n,r);return o?(this.draftDone=t,this.lastDraftNotice=a,o):null}draftNotice(e,a){let n=this.state.chat?.tokens??0,r=this.plan?.label??"your plan";if(Aa(e,this.chatText))return{title:"You already asked this in this chat",body:`Its answer is above. Sending it again re-reads about ${K(n+a.tokens)} tokens of ${r}'s usage for the same question.`};if(Ea(e)&&n>=Bi)return{title:`"${e.slice(0,24)}" re-reads this whole chat`,body:`Sending it uses about ${K(n)} tokens of ${r}'s usage, as much as a long question. If there's nothing to ask, you can skip it, or add it to your next real message.`};let t=Ca(e);for(let s of t){let l=this.pastes.get(s)??new Set;if(l.add(this.chatId),this.pastes.set(s,l),l.size>=2&&!this.told.has(`paste:${s}`))return this.told.add(`paste:${s}`),{title:`You've pasted this text into ${l.size} chats`,body:"Put it in a Claude Project's knowledge or instructions once, and every chat in that Project has it without pasting it again."}}return a.tokens>=ji&&a.saved>=Di&&a.pct>=Ni?{title:`Draft: ${K(a.tokens)} \u2192 ${K(a.optimizedTokens)} tokens (${a.pct}% fewer)`,body:`Same request, ${a.savedUsd===null?"fewer tokens":`${Pe(a.savedUsd)} less per send at API prices`}. One click puts the shorter version in Claude's message box: open the Token Lens panel.${a.tips[0]?` ${a.tips[0]}`:""}`}:a.tips.length?{title:"Token Lens tip for your draft",body:a.tips[0]}:null}chat(e,a,n=Date.now()){let{tokens:r}=_(a,this.profile),t=J(r,0,this.profile),o=N(r,this.profile),s=[],l=null;t.zone==="hot"||t.zone==="full"?(l=t.zone,s.push(`This chat fills about ${Math.round(t.ratio*100)}% of the context, so older messages will soon be dropped or compacted. ${z}`)):(t.zone==="warm"||t.longChat)&&(l=t.zone==="warm"?"warm":"long",s.push(`Every message you send re-reads about ${K(r)} tokens of this chat (${Pe(o)} each at API prices). Starting a new chat for a new topic avoids that.`)),this.state.chat={tokens:r,ratio:t.ratio,window:t.window,perMessageUsd:o,tips:s},r<(this.chatTokens.get(e)??0)*.5&&(this.chatTold.delete(e),this.chatEpoch.set(e,(this.chatEpoch.get(e)??0)+1)),this.chatTokens.set(e,r),this.chatText=a,this.chatId=ge(`${e}#${this.chatEpoch.get(e)??0}`),this.chatTitle=e&&!/^claude$/i.test(e)?e.slice(0,60):"",this.chatTitle&&this.titles.set(this.chatId,this.chatTitle);let u=this.observe(a,n),d=null;if(l){let h=this.chatTold.get(e)??new Set;this.chatTold.set(e,h),h.has(l)||(h.add(l),d={title:`This chat: ${K(r)} tokens (${Math.round(t.ratio*100)}% of context)`,body:s[0]})}return u??d}observe(e,a){let n=!1,r=ya(e);r&&(this.appPlan=r);let t=wa(e,a);t&&(this.data=Sa(this.data,t),n=!0);let o=this.pending;o&&a-o.at>Gi?this.pending=null:o&&o.probe&&Ta(e).includes(o.probe)&&(this.data.sends.push({at:o.at,tokens:o.tokens+_(o.text,this.profile).tokens,chat:o.chat}),this.pending=null,n=!0),n&&(this.data=xe(this.data,a),this.onData?.(this.data));let s=this.refreshCoach(a);if(s.sessionPct!==void 0&&s.sessionPct>=Fi){let u=`session:${Math.round(s.session.since/36e5)}`;if(!this.told.has(u))return this.told.add(u),{title:`${this.plan?.label??"Claude"} session ${s.sessionPct}% used`,body:s.insights[0]??"New topics in new chats stretch what's left."}}let l=s.insights.find(u=>u.includes("weekly allowance is unused"));if(l&&s.weekResetsAt){let u=`week:${Math.round(s.weekResetsAt/864e5)}`;if(!this.told.has(u))return this.told.add(u),{title:`${100-(s.weekPct??0)}% of your week is still unused`,body:l}}return null}refreshCoach(e=Date.now()){let a=_a({data:this.data,now:e,plan:this.plan,planSource:this.pickedPlan?"set":this.appPlan?"app":"unknown",current:this.chatId?{chat:this.chatId,title:this.chatTitle,tokens:this.state.chat?.tokens??0}:void 0,titles:this.titles});return this.state.coach=a,a}analyzeDraft(e){let a=he({prompt:e,model:this.profile,conversationTokens:this.state.chat?.tokens??0,level:"auto"}),n=a.optimized.saved;return{tokens:a.promptTokens,optimizedTokens:a.optimized.tokens,saved:n,pct:a.optimized.originalTokens?Math.round(n/a.optimized.originalTokens*100):0,savedUsd:a.cost.saved.usd,best:n>0?a.best.text:e,kept:a.best.kept,tips:a.tips.filter(r=>!["handoff","price-threshold","family-note"].includes(r.id)).map(r=>r.text)}}};var L=null,je="",qi={version:"0.2.39",start(i,e,a){let n;try{n=e?JSON.parse(e):void 0,(!n||!Array.isArray(n.sends)||!Array.isArray(n.readings))&&(n=void 0)}catch{n=void 0}L=new be(i||"claude-sonnet",{data:n,plan:a||void 0,onData:r=>je=JSON.stringify(r)})},draft(i,e){let a=L?.draft(i,e);return a?JSON.stringify(a):""},chat(i,e,a){let n=L?.chat(i,e,a);return n?JSON.stringify(n):""},state(i){return L?(L.refreshCoach(i),JSON.stringify(L.state)):"{}"},currentDraft(){return L?.currentDraft??""},setPlan(i){L?.setPlan(i||null)},takeData(){let i=je;return je="",i},optimize(i,e){let a=he({prompt:i,model:e,level:"auto"});return JSON.stringify({best:a.best.text,tokens:a.promptTokens,optimizedTokens:a.optimized.tokens,kept:a.best.kept})}};globalThis.TokenLensCore=qi;})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kunal_dutt/token-lens",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
4
4
  "description": "Count and cut the tokens your prompts, chats and agents use, adapted to the model and task. MCP server and self-hosted gateway. Collects no data.",
5
5
  "keywords": [
6
6
  "mcp",
package/server.mjs CHANGED
@@ -3748,15 +3748,15 @@ tick();setInterval(tick,1000);
3748
3748
  </script>
3749
3749
  </body>
3750
3750
  </html>
3751
- `;var ZF="0.1.0",Zn=G.string().describe('Model in use, e.g. "gpt-5", "claude-sonnet-4-5"; your own if unsure.'),uo=G.object({input_per_mtok:G.number().nonnegative(),output_per_mtok:G.number().nonnegative(),cached_input_per_mtok:G.number().nonnegative().optional()}).optional().describe("USD per 1M tokens, only if not the list price.");function Sn(e,t){let r=gr(e);return t?{...r,pricing:{inputPerMTok:t.input_per_mtok,outputPerMTok:t.output_per_mtok,cachedInputPerMTok:t.cached_input_per_mtok}}:r}var te=e=>e.toLocaleString("en-US"),Pr=e=>`Priced at ${e.label}: ${Nv(e.pricing)}.`;function JF(e){let t=e.optimized.originalTokens?Math.round(e.optimized.saved/e.optimized.originalTokens*100):0,r=[e.optimized.saved>0?`${zi(e.cost.saved,"send")} (${t}% of the prompt). Meaning, numbers, code and constraints kept.`:`No saving: the prompt is already tight at the ${e.level} level.`,`This prompt: ${te(e.promptTokens)} tokens (${xe(e.cost.promptUsd)})${e.usage.conversationTokens?`; sending it with the conversation: ${xe(e.cost.sendUsd)}`:""}. ${Pr(e.profile)}`,"",`Model profile: ${e.profile.label} (${e.exact?"exact count":"estimated count"})`,`Task: ${oA[e.task.task]}`,`Prompt: ${te(e.promptTokens)} tokens`];return e.usage.conversationTokens&&r.push(`Context: ${Math.round(e.usage.ratio*100)}% of ~${te(e.usage.window)} tokens`),e.optimized.saved>0&&r.push(`Optimized (${e.level}): ${te(e.optimized.tokens)} tokens via ${e.optimized.rules.join(", ")}`,"",`Kept: ${e.best.kept}.`,"","--- best prompt, same answer (paste this) ---",e.best.text,"--- end ---"),e.tips.length&&r.push("","Suggestions:",...e.tips.map(a=>`- ${a.text}${a.append?` (add: "${a.append}")`:""}`)),r.join(`
3752
- `)}async function YF(e,t,r){try{let a=typeof process<"u"?process.env:{};if(r==="anthropic"&&a.ANTHROPIC_API_KEY&&/^claude-[\w.-]+$/.test(t)){let n=await fetch("https://api.anthropic.com/v1/messages/count_tokens",{method:"POST",headers:{"x-api-key":a.ANTHROPIC_API_KEY,"anthropic-version":"2023-06-01","content-type":"application/json"},body:JSON.stringify({model:t,messages:[{role:"user",content:e}]}),signal:AbortSignal.timeout(5e3)}),i=await n.json();return n.ok&&typeof i.input_tokens=="number"?{tokens:i.input_tokens,exact:!0}:null}if(r==="google"&&a.GEMINI_API_KEY&&/^gemini-[\w.-]+$/.test(t)){let n=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${t}:countTokens`,{method:"POST",headers:{"x-goog-api-key":a.GEMINI_API_KEY,"content-type":"application/json"},body:JSON.stringify({contents:[{parts:[{text:e}]}]}),signal:AbortSignal.timeout(5e3)}),i=await n.json();return n.ok&&typeof i.totalTokens=="number"?{tokens:i.totalTokens,exact:!0}:null}}catch{}return null}var XF={shorten:"Shorten my prompt before sending",optimize_prompt:"Optimized prompts",optimize_agent:"Agent fixes (per call)",audit_agent_rules:"Rule-file trims (per turn)",compact_data:"Compacted pasted data",compare_prompts:"Checked hand-shortened prompts"},QF={token_lens:{keep:["model"],description:"Open the Token Lens box in the chat, where the user shortens prompts without sending them to you. Use when asked to open or use Token Lens with no prompt given."},optimize_prompt:{keep:["prompt","model"],description:"Shorten a prompt to fewer tokens with the same meaning (every instruction, number, negation and code kept). Returns the shorter prompt and the savings. Use when asked to shorten or optimize a prompt."},check_conversation:{keep:["model","conversation_tokens"],description:"How full this chat's context is and what each new message costs. Pass your estimate of the chat's tokens; never copy the chat in."}};function eq(e){let t=e.registerTool.bind(e);e.registerTool=(r,a,n)=>{let i=QF[r];if(!i)return;let o=Object.fromEntries(Object.entries(a.inputSchema??{}).filter(([s])=>i.keep.includes(s)).map(([s,l])=>[s,s==="model"?l.isOptional()?WP.optional():WP:l]));return t(r,{...a,description:i.description,inputSchema:o},n)}}var WP=G.string().describe('Your model, e.g. "gpt-5".');function __(e={}){let t=new Qp({name:"token-lens",version:ZF}),r=e.ledger,a=(n,i,o,s)=>r?.record(n,i.id,o,s);return e.chat&&eq(t),e.live&&nq(t,e.live),iq(t,e.live),VP(t),t.registerPrompt("shorten",{title:"Shorten my prompt before sending",description:"Paste the prompt you're writing. Token Lens shortens it locally, keeping every instruction, fact, number, code block and constraint, and puts the shorter version in your message box. The long version never reaches the model.",argsSchema:{prompt:G.string().describe("The prompt you want to send, pasted in full."),model:G.string().optional().describe(`The model you'll send it to, e.g. "claude-sonnet-4-5" or "gpt-5". Leave blank for any model.`)}},({prompt:n,model:i})=>{let o=Ys({prompt:n,model:Sn(i??"",void 0),level:"auto"});a("shorten",o.profile,o.optimized.saved,o.cost.saved.usd);let s=o.optimized.originalTokens?Math.round(o.optimized.saved/o.optimized.originalTokens*100):0;return{description:o.optimized.saved>0?`Token Lens: ${te(o.promptTokens)} -> ${te(o.optimized.tokens)} tokens (${s}% fewer${o.cost.saved.usd===null?"":`, ${xe(o.cost.saved.usd)} saved`}). Kept: ${o.best.kept}.`:"Token Lens: already tight, sent unchanged.",messages:[{role:"user",content:{type:"text",text:o.optimized.saved>0?o.best.text:n}}]}}),t.registerTool("token_lens",{title:"Open Token Lens",description:"Open Token Lens inside the chat: a card where the user pastes prompts and gets a shorter version that says the same thing, with tokens and money saved and a check that every number, negation and rule was kept. Prompts pasted there are checked by Token Lens, not sent to you. Use when the user asks to open, start, show or use Token Lens without giving a prompt.",inputSchema:{model:Zn.optional()},annotations:{readOnlyHint:!0,openWorldHint:!1},_meta:eg("Opening Token Lens\u2026","Token Lens is open")},async({model:n})=>{let i=Sn(n??"",void 0);return{content:[{type:"text",text:"Token Lens is open in a box in the chat; prompts pasted there are shortened by Token Lens and not sent to you. If this app shows no box, ask the user to paste the prompt they want shortened, then call optimize_prompt with it and give them the shorter prompt."}],structuredContent:{panel:!0,model:n?i.label:"",profile:i.id}}}),t.registerTool("optimize_prompt",{title:"Optimize a prompt for fewer tokens",description:"Rewrite a prompt to use fewer tokens for a given model, keeping its meaning. Detects the task (coding, debugging, writing, editing, summarizing, translation, research, analysis, brainstorming, chat), leaves code, quotes and pasted source text untouched, trims logs when debugging, and drops step-by-step instructions for reasoning models. Never suggests switching models. Returns the best prompt to paste (same answer, fewer tokens, with what it kept), the savings in tokens and dollars, and model- and task-specific tips. Use when the user asks to shorten, tighten or save tokens on a prompt.",inputSchema:{prompt:G.string().describe("The prompt to optimize, exactly as the user wrote it."),model:Zn,level:G.enum(["auto",...mA]).optional().describe("light: wording only; balanced: + politeness and filler; aggressive: + hedges. Default auto."),conversation_tokens:G.number().int().nonnegative().optional().describe("Tokens already in the conversation, if known."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1},_meta:eg("Shortening the prompt\u2026","Shorter prompt ready")},async({prompt:n,model:i,level:o,conversation_tokens:s,pricing:l})=>{let c=Ys({prompt:n,model:Sn(i,l),conversationTokens:s,level:o??"auto"});return a("optimize_prompt",c.profile,c.optimized.saved,c.cost.saved.usd),{content:[{type:"text",text:JF(c)}],structuredContent:{model:c.profile.label,profile:c.profile.id,task:c.task.task,promptTokens:c.promptTokens,exact:c.exact,level:c.level,optimizedPrompt:c.optimized.text,bestPrompt:c.best.text,kept:c.best.kept,optimizedTokens:c.optimized.tokens,saved:c.optimized.saved,savedUsd:c.cost.saved.usd,savedUsdPer1k:c.cost.saved.usdPer1k,promptUsd:c.cost.promptUsd,sendUsd:c.cost.sendUsd,pricing:c.cost.pricing??null,rules:c.optimized.rules,tips:c.tips.map(u=>u.text)}}}),t.registerTool("count_tokens",{title:"Count tokens",description:"Count the tokens in a text for a given model. Exact for OpenAI models (o200k tokenizer); for Claude and Gemini exact when the server runs with your own ANTHROPIC_API_KEY or GEMINI_API_KEY (it then asks the provider's free token-count endpoint), otherwise a per-family estimate. Use when the user asks how many tokens something is, or before pasting a large document.",inputSchema:{text:G.string(),model:Zn,pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({text:n,model:i,pricing:o})=>{let s=Sn(i,o),l=await YF(n,i,s.family)??Ie(n,s),{tokens:c,exact:u}=l,d=nt(c,s),p=Math.round(c/s.contextWindow*1e3)/10;return{content:[{type:"text",text:`${u?"":"\u2248"}${te(c)} tokens for ${s.label} (${u?"exact":"estimate"}), ${p}% of a ~${te(s.contextWindow)} token context. Costs ${xe(d)} as input each time it is sent (${xe(d===null?null:d*1e3)} per 1,000 sends). ${Pr(s)}`}],structuredContent:{tokens:c,exact:u,profile:s.id,contextWindow:s.contextWindow,inputUsd:d,pricing:s.pricing??null}}}),t.registerTool("check_conversation",{title:"Check conversation token budget",description:"Estimate how full the context window is for a conversation and whether to hand off to a new chat. Pass the conversation text or a token count. When the chat is long, also offers an optional handoff request that produces a compact brief to continue in a fresh chat; this can change later answers, because the new chat only knows what the brief keeps, so present it as optional and never as same-output. Use when the user asks about chat length, usage limits, or the model forgetting earlier context.",inputSchema:{model:Zn,conversation:G.string().optional().describe("The conversation so far, as text."),conversation_tokens:G.number().int().nonnegative().optional().describe("Or: its size in tokens."),latest_request:G.string().optional().describe("The user's latest message, to tailor the handoff to the task."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1},_meta:eg("Checking the chat\u2026","Chat checked")},async({model:n,conversation:i,conversation_tokens:o,latest_request:s,pricing:l})=>{let c=Sn(n,l),u=o??Ie(i??"",c).tokens,d=Xr(u,0,c),p=pr(s??i?.slice(-2e3)??"").task,m=d.zone==="hot"||d.zone==="full"?`Context is nearly full; older messages will be dropped or compacted soon. A handoff to a new chat avoids that. ${Rt}`:d.longChat||d.zone==="warm"?`Every new message re-reads about ${te(u)} tokens of history. A handoff to a new chat would cut that. ${Rt}`:"The conversation is still short; no handoff needed.",g=d.zone!=="ok"||d.longChat?$m(p):void 0,h=nt(u,c),f=g?Math.max(0,u-1e3):0,k=nt(f*10,c);return{content:[{type:"text",text:[`${te(u)} tokens, ${Math.round(d.ratio*100)}% of ${c.label}'s ~${te(d.window)} token context.`,`Each new message re-reads this history: ${xe(h)} of input per message. ${Pr(c)}`,...g?[`Optional: a handoff to a new chat saves about ${te(f)} tokens (${xe(nt(f,c))}) per message, ${xe(k)} over the next 10 messages.`]:[],m,...g?["","Optional handoff request (can change later answers):",g]:[]].join(`
3753
- `)}],structuredContent:{model:c.label,profile:c.id,tokens:u,ratio:d.ratio,zone:d.zone,window:d.window,usdPerMessage:h,handoffSavesTokensPerMessage:f,handoffSavesUsdOver10Messages:k,handoffPrompt:g??null}}}),t.registerTool("optimize_agent",{title:"Find token hotspots and optimize an agent or conversation",description:"Break down where tokens go in an agent or chat (system prompt, tool definitions, examples, conversation history, next prompt), find what is large or growing, and fix it. Safe fixes that keep the meaning (tighter wording with every number, negation and constraint kept; duplicate history removed) are applied and returned as an optimized spec. Returns the optimized system prompt, tool definitions and next prompt ready to paste. Every applied change is verified (facts and constraints kept, every instruction word kept, tool interface and conversation unchanged, same model) and the checks are returned as evidence. Fixes that could change the output (trimming history or tool results, fewer examples, length caps) are only listed, for the user to opt into. Also flags prompt-caching opportunities. Use when the user is building an agent or workflow, shares a system prompt or tool list, or asks why a chat or agent uses so many tokens.",inputSchema:{model:Zn,system:G.string().optional().describe("System prompt / instructions."),tools:G.array(G.object({name:G.string(),description:G.string().optional(),parameters:G.any().optional()})).optional().describe("Tool definitions (parameters = JSON schema)."),examples:G.array(G.string()).optional().describe("Few-shot examples."),messages:G.array(G.object({role:G.enum(["user","assistant","tool","system"]),content:G.string(),toolCalls:G.array(G.string()).optional().describe("Names of tools called in this message.")})).optional().describe("Conversation history, oldest first."),prompt:G.string().optional().describe("The next user message."),pricing:uo,calls_per_month:G.number().int().positive().optional().describe("How many calls this agent makes per month, if known, to show the monthly saving.")},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({model:n,pricing:i,calls_per_month:o,...s})=>{let l=$A(s,Sn(n,i)),c=l.report;l.verification.passed&&a("optimize_agent",c.profile,l.savings.tokens,l.savings.usd);let u=v=>o&&v!==null?v*o:null,d=c.findings.filter(v=>v.risk==="review"),p=c.findings.filter(v=>v.risk==="safe"&&!v.applied),m=[l.verification.passed&&l.savings.tokens>0?`${zi(l.savings)}${u(l.savings.usd)!==null?`, ${xe(u(l.savings.usd))} per month at ${te(o)} calls`:""}: ${te(l.before)} \u2192 ${te(l.after)} tokens per call, same model and same output (checks below).`:l.verification.passed?"No wording savings to apply: the agent's text is already tight.":"No changes applied: a check failed, so the original spec is returned unchanged.",`Cost per call now: ${xe(c.costPerCall)} of input${l.savings.tokens>0?`, ${xe(nt(l.after,c.profile))} after the fixes`:""}. ${Pr(c.profile)}`];p.length&&p[0].usd!==null&&m.push(`Also safe, needs a config change on your side: ${xe(p[0].usd)} more per call (${p[0].id}).`),d.length&&d[0].usd!==null&&m.push(`Opt-in fixes that could change the output (not applied): the biggest saves ${xe(d[0].usd)} more per call (${d[0].id}).`),m.push("",`Model profile: ${c.profile.label}. Total ${te(c.total)} tokens per call; ${te(c.fixedOverhead)} of it is re-sent every call (system + tools + examples).`,"","Where the tokens go:",...c.breakdown.map(v=>`- ${v.label}: ${te(v.tokens)} (${c.total?Math.round(v.tokens/c.total*100):0}%)`)),c.growthPerTurn&&m.push(`Growing ~${te(c.growthPerTurn)} tokens per user turn${c.turnsLeft!==null?`, about ${c.turnsLeft} turns of room left`:""}.`),m.push("",l.verification.passed?"Evidence the output is unchanged (same model, same tools, same instructions):":"Checks (one failed, so nothing was applied):",...l.verification.checks.map(v=>`${v.passed?"\u2713":"\u2717"} ${v.name}: ${v.detail}`));let g=c.findings.filter(v=>v.risk==="safe"),h=c.findings.filter(v=>v.risk==="review"),f=v=>v.id==="structured-output"?"saves every retry after a reply that doesn't parse":v.id==="price-threshold"&&!v.saves?"keeps the call at the lower rate":`saves ~${te(v.saves)} tokens${v.usd!==null?`, ${xe(v.usd)} per call, ${xe(v.usd*1e3)} per 1,000 calls`:""}`;if(g.length&&m.push("","Safe fixes:",...g.map(v=>`- ${v.applied?"[applied] ":"[do this] "}${v.issue} ${v.fix} (${f(v)})`)),h.length&&m.push("","Could change the output, so not applied (ask the user):",...h.map(v=>`- ${v.issue} ${v.fix} (${f(v)})`)),l.verification.passed&&l.savings.tokens>0){let v=(C,S)=>["",`--- ${C} (paste this) ---`,S,"--- end ---"];l.spec.system&&l.spec.system!==s.system&&m.push(...v("optimized system prompt",l.spec.system)),l.spec.tools&&JSON.stringify(l.spec.tools)!==JSON.stringify(s.tools)&&m.push(...v("optimized tool definitions (JSON)",JSON.stringify(l.spec.tools,null,2))),l.spec.prompt&&l.spec.prompt!==s.prompt&&m.push(...v("optimized next prompt",l.spec.prompt)),l.spec.messages&&JSON.stringify(l.spec.messages)!==JSON.stringify(s.messages)&&m.push("","Conversation: exact repeats were replaced with pointers; the full list is in structuredContent.optimized.messages.")}s.system&&c.findings.some(v=>v.id==="volatile-prefix")&&m.push("","--- system prompt with the changing lines last (paste this) ---",Hv(l.spec.system??s.system).text,"--- end ---");let k=c.findings.some(v=>v.id==="structured-output")?Gv(s,c.profile):null;k&&m.push("",`--- strict JSON mode: add to the request (${c.profile.family==="anthropic"?"Anthropic Messages API":c.profile.family==="google"?"Gemini generateContent":"OpenAI-compatible API"}; paste this) ---`,JSON.stringify(k.setup,null,2),"--- end ---");let b=c.findings.some(v=>v.id==="lazy-tools")?qv(l.spec.tools??[]):null;return b&&m.push("","--- on-demand tool loading: send these 2 tools instead of the full list (paste this) ---",JSON.stringify(b.tools,null,2),"--- end ---","Your runtime answers get_tool_schema with the tool's full definition (structuredContent.lazyTools.schemas) and runs invoke_tool by calling that tool with tool_input."),{content:[{type:"text",text:m.join(`
3754
- `)}],structuredContent:{lazyTools:b,structuredOutput:k?.setup??null,before:l.before,after:l.after,savedTokens:l.savings.tokens,savedUsdPerCall:l.savings.usd,savedUsdPer1kCalls:l.savings.usdPer1k,savedUsdPerMonth:u(l.savings.usd),costPerCallUsd:c.costPerCall,pricing:c.profile.pricing??null,breakdown:c.breakdown,growthPerTurn:c.growthPerTurn,turnsLeft:c.turnsLeft,findings:c.findings,optimized:l.spec,verification:l.verification}}}),t.registerTool("audit_agent_rules",{title:"Trim coding-agent rule files",description:"Audit the instruction files a coding agent re-sends on every turn (CLAUDE.md, AGENTS.md, .cursorrules, .github/copilot-instructions.md, GEMINI.md and the like) and return trimmed versions ready to paste. Removes exact repeats within and across the files and tightens wording only where every number, negation, constraint and instruction word is kept, so the agent gets the same instructions. Lines that mention files missing from the repo are listed, never removed. Reports the tokens and dollars saved per turn and per 1,000 turns. Use when the user shares agent rule files or asks why a coding agent uses so many tokens.",inputSchema:{model:Zn,files:G.array(G.object({path:G.string(),content:G.string()})).min(1).describe("Each rule file: its path and full text."),repo_paths:G.array(G.string()).optional().describe("Optional: the repo's file paths, to flag rules that mention files that no longer exist."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({model:n,files:i,repo_paths:o,pricing:s})=>{let l=Sn(n,s),c=jA(i,l,{knownPaths:o}),u=c.before-c.after;a("audit_agent_rules",l,c.saved.tokens,c.saved.usd);let d=[u>0?`${zi(c.saved)} per turn: ${te(c.before)} \u2192 ${te(c.after)} tokens of rules re-sent every turn, same instructions.`:`Nothing to trim: ${te(c.before)} tokens of rules, no repeats, wording already tight.`,Pr(l)],p=c.findings.filter(g=>g.kind==="duplicate");p.length&&d.push("","Exact repeats removed:",...p.map(g=>`- ${g.path}:${g.line} ${g.text} (${g.detail})`));let m=c.findings.filter(g=>g.kind==="stale-path");m.length&&d.push("","Check these (not removed): they mention files that aren't in the repo.",...m.map(g=>`- ${g.path}:${g.line} ${g.detail}`));for(let g of c.files)g.trimmed!==i.find(h=>h.path===g.path)?.content&&d.push("",`--- ${g.path}, trimmed (${te(g.tokens)} \u2192 ${te(g.trimmedTokens)} tokens; paste this) ---`,g.trimmed,"--- end ---");return{content:[{type:"text",text:d.join(`
3755
- `)}],structuredContent:{before:c.before,after:c.after,savedTokensPerTurn:c.saved.tokens,savedUsdPerTurn:c.saved.usd,savedUsdPer1kTurns:c.saved.usdPer1k,files:c.files,findings:c.findings,pricing:l.pricing??null}}}),t.registerTool("list_model_profiles",{title:"List model profiles",description:"List the model families Token Lens adapts to, with their context windows, whether counts are exact, and the price used for cost figures.",inputSchema:{},annotations:{readOnlyHint:!0,openWorldHint:!1}},async()=>({content:[{type:"text",text:Object.values(hn).map(n=>`${n.id}: ${n.label}, ~${te(n.contextWindow)} context, ${n.tokenizer==="o200k"?"exact":"estimated"} counts${n.reasoning?", reasoning model":""}, ${Nv(n.pricing)}`).join(`
3756
- `)}]})),t.registerTool("compact_data",{title:"Shrink pasted data without changing it",description:"Shrink pasted JSON, API responses or logs with the data unchanged (removes only whitespace outside strings; proven by parsing). Returns paste-ready text and savings. Use when the user pastes large data.",inputSchema:{text:G.string().describe("Text with the pasted data, as written."),model:Zn},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({text:n,model:i})=>{let o=Sn(i,void 0),s=Um(n,o),l=$t(s.saved,o);return a("compact_data",o,s.saved,l.usd),{content:[{type:"text",text:(s.saved>0?[`${zi(l,"send")} (${Math.round(s.saved/s.before*100)}%): ${te(s.before)} \u2192 ${te(s.after)} tokens, same data. ${Pr(o)}`,"","What changed:",...s.changes.map(u=>`- ${u}`),...s.checks.length?["","Evidence the data is unchanged:",...s.checks.map(u=>`\u2713 ${u.name}: ${u.detail}`)]:[],"","--- compact text, same data (paste this) ---",s.text,"--- end ---"]:[`No saving: ${s.skipped??"already compact."} ${te(s.before)} tokens.`]).join(`
3757
- `)}],structuredContent:{before:s.before,after:s.after,savedTokens:s.saved,savedUsd:l.usd,compactText:s.text,changes:s.changes,blocks:s.blocks,checks:s.checks,skipped:s.skipped??null}}}),t.registerTool("compare_prompts",{title:"Check a shortened prompt against the original",description:"Check a prompt the user shortened against the original: savings, and any number, negation, constraint or instruction word lost. Use when asked if a shorter prompt is safe to send.",inputSchema:{original:G.string(),edited:G.string(),model:Zn},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({original:n,edited:i,model:o})=>{let s=Sn(o,void 0),l=SA(n,i,s);l.sameInstructions&&a("compare_prompts",s,l.saved.tokens,l.saved.usd);let c=[l.verdict,l.saved.tokens>0?`${zi(l.saved,"send")}: ${te(l.before)} \u2192 ${te(l.after)} tokens. ${Pr(s)}`:`${te(l.before)} \u2192 ${te(l.after)} tokens: the edit is not shorter.`];return l.lostFacts.length&&c.push("","Missing from the shorter version (can change the answer):",...l.lostFacts.map(u=>`- ${u}`)),l.lostWords.length&&c.push("",`Words dropped that may carry instructions: ${l.lostWords.join(", ")}.`),{content:[{type:"text",text:c.join(`
3758
- `)}],structuredContent:{before:l.before,after:l.after,savedTokens:l.saved.tokens,savedUsd:l.saved.usd,sameInstructions:l.sameInstructions,lostFacts:l.lostFacts,lostWords:l.lostWords,verdict:l.verdict}}}),t.registerTool("estimate_cost",{title:"Forecast monthly cost and same-answer savings",description:"Monthly cost of an app or agent on its model (input, output, long-context rates) and what same-answer prompt caching and half-price batch save. Use for budgets and cutting an LLM bill.",inputSchema:{model:Zn,input_tokens:G.number().int().nonnegative().describe("Per call."),output_tokens:G.number().int().nonnegative().describe("Per call, incl. reasoning."),calls_per_day:G.number().positive(),repeated_prefix_tokens:G.number().int().nonnegative().optional().describe("Unchanging start of each call: system, tools, examples."),can_wait_share:G.number().min(0).max(1).optional().describe("Share of calls that can wait 24h."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({model:n,input_tokens:i,output_tokens:o,calls_per_day:s,repeated_prefix_tokens:l,can_wait_share:c,pricing:u})=>{let d=Sn(n,u),p=EA(d,{inputTokens:i,outputTokens:o,callsPerDay:s,repeatedPrefixTokens:l,canWaitShare:c});if(!p.priced)return{content:[{type:"text",text:`No price is set for ${d.label}. Pass pricing with your per-1M-token rates to get a forecast.`}]};let m=p.lines.reduce((h,f)=>f.monthlyUsd<h.monthlyUsd?f:h),g=[`${d.label}, ${te(p.callsPerMonth)} calls a month: ${xe(p.lines[0].monthlyUsd)} a month now${m.id!=="now"?`; ${xe(m.monthlyUsd)} with ${m.label.toLowerCase()}, saving ${xe(m.savedUsd)} a month (${xe(m.savedUsd*12)} a year) with the same answers`:""}.`,Pr(d),"",...p.lines.map(h=>`- ${h.label}: ${xe(h.monthlyUsd)} a month${h.savedUsd>0?`, saves ${xe(h.savedUsd)}`:""}.${h.note?` ${h.note}`:""}`)];return l?p.rates.cached===null&&g.push("",`${d.label} has no cached-input price on file; pass pricing.cached_input_per_mtok to include caching.`):g.push("","Tip: pass repeated_prefix_tokens (system prompt, tools and examples) to see what prompt caching saves."),!c&&["openai","anthropic","google"].includes(d.family)&&g.push("","Tip: if some calls can wait up to 24 hours (reports, bulk jobs), pass can_wait_share to see the half-price batch saving."),{content:[{type:"text",text:g.join(`
3751
+ `;var ZF="0.1.0",Zn=G.string().describe('Model in use, e.g. "gpt-5"; yours if unsure.'),uo=G.object({input_per_mtok:G.number().nonnegative(),output_per_mtok:G.number().nonnegative(),cached_input_per_mtok:G.number().nonnegative().optional()}).optional().describe("USD per 1M tokens, only if not the list price.");function Sn(e,t){let r=gr(e);return t?{...r,pricing:{inputPerMTok:t.input_per_mtok,outputPerMTok:t.output_per_mtok,cachedInputPerMTok:t.cached_input_per_mtok}}:r}var te=e=>e.toLocaleString("en-US"),Pr=e=>`Priced at ${e.label}: ${Nv(e.pricing)}.`;function JF(e){let t=e.optimized.originalTokens?Math.round(e.optimized.saved/e.optimized.originalTokens*100):0,r=[e.optimized.saved>0?`${zi(e.cost.saved,"send")} (${t}% of the prompt). Meaning, numbers, code and constraints kept.`:`No saving: the prompt is already tight at the ${e.level} level.`,`This prompt: ${te(e.promptTokens)} tokens (${xe(e.cost.promptUsd)})${e.usage.conversationTokens?`; sending it with the conversation: ${xe(e.cost.sendUsd)}`:""}. ${Pr(e.profile)}`,"",`Model profile: ${e.profile.label} (${e.exact?"exact count":"estimated count"})`,`Task: ${oA[e.task.task]}`,`Prompt: ${te(e.promptTokens)} tokens`];return e.usage.conversationTokens&&r.push(`Context: ${Math.round(e.usage.ratio*100)}% of ~${te(e.usage.window)} tokens`),e.optimized.saved>0&&r.push(`Optimized (${e.level}): ${te(e.optimized.tokens)} tokens via ${e.optimized.rules.join(", ")}`,"",`Kept: ${e.best.kept}.`,"","--- best prompt, same answer (paste this) ---",e.best.text,"--- end ---"),e.tips.length&&r.push("","Suggestions:",...e.tips.map(a=>`- ${a.text}${a.append?` (add: "${a.append}")`:""}`)),r.join(`
3752
+ `)}async function YF(e,t,r){try{let a=typeof process<"u"?process.env:{};if(r==="anthropic"&&a.ANTHROPIC_API_KEY&&/^claude-[\w.-]+$/.test(t)){let n=await fetch("https://api.anthropic.com/v1/messages/count_tokens",{method:"POST",headers:{"x-api-key":a.ANTHROPIC_API_KEY,"anthropic-version":"2023-06-01","content-type":"application/json"},body:JSON.stringify({model:t,messages:[{role:"user",content:e}]}),signal:AbortSignal.timeout(5e3)}),i=await n.json();return n.ok&&typeof i.input_tokens=="number"?{tokens:i.input_tokens,exact:!0}:null}if(r==="google"&&a.GEMINI_API_KEY&&/^gemini-[\w.-]+$/.test(t)){let n=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${t}:countTokens`,{method:"POST",headers:{"x-goog-api-key":a.GEMINI_API_KEY,"content-type":"application/json"},body:JSON.stringify({contents:[{parts:[{text:e}]}]}),signal:AbortSignal.timeout(5e3)}),i=await n.json();return n.ok&&typeof i.totalTokens=="number"?{tokens:i.totalTokens,exact:!0}:null}}catch{}return null}var XF={shorten:"Shorten my prompt before sending",optimize_prompt:"Optimized prompts",optimize_agent:"Agent fixes (per call)",audit_agent_rules:"Rule-file trims (per turn)",compact_data:"Compacted pasted data",compare_prompts:"Checked hand-shortened prompts"},QF={token_lens:{keep:["model"],description:"Open the Token Lens box in the chat, where the user shortens prompts without sending them to you. Use when asked to open or use Token Lens with no prompt given."},optimize_prompt:{keep:["prompt","model"],description:"Shorten a prompt to fewer tokens with the same meaning (every instruction, number, negation and code kept). Returns the shorter prompt and the savings. Use when asked to shorten or optimize a prompt."},check_conversation:{keep:["model","conversation_tokens"],description:"How full this chat's context is and what each new message costs. Pass your estimate of the chat's tokens; never copy the chat in."}};function eq(e){let t=e.registerTool.bind(e);e.registerTool=(r,a,n)=>{let i=QF[r];if(!i)return;let o=Object.fromEntries(Object.entries(a.inputSchema??{}).filter(([s])=>i.keep.includes(s)).map(([s,l])=>[s,s==="model"?l.isOptional()?WP.optional():WP:l]));return t(r,{...a,description:i.description,inputSchema:o},n)}}var WP=G.string().describe('Your model, e.g. "gpt-5".');function __(e={}){let t=new Qp({name:"token-lens",version:ZF}),r=e.ledger,a=(n,i,o,s)=>r?.record(n,i.id,o,s);return e.chat&&eq(t),e.live&&nq(t,e.live),iq(t,e.live),VP(t),t.registerPrompt("shorten",{title:"Shorten my prompt before sending",description:"Paste the prompt you're writing. Token Lens shortens it locally, keeping every instruction, fact, number, code block and constraint, and puts the shorter version in your message box. The long version never reaches the model.",argsSchema:{prompt:G.string().describe("The prompt you want to send, pasted in full."),model:G.string().optional().describe(`The model you'll send it to, e.g. "claude-sonnet-4-5" or "gpt-5". Leave blank for any model.`)}},({prompt:n,model:i})=>{let o=Ys({prompt:n,model:Sn(i??"",void 0),level:"auto"});a("shorten",o.profile,o.optimized.saved,o.cost.saved.usd);let s=o.optimized.originalTokens?Math.round(o.optimized.saved/o.optimized.originalTokens*100):0;return{description:o.optimized.saved>0?`Token Lens: ${te(o.promptTokens)} -> ${te(o.optimized.tokens)} tokens (${s}% fewer${o.cost.saved.usd===null?"":`, ${xe(o.cost.saved.usd)} saved`}). Kept: ${o.best.kept}.`:"Token Lens: already tight, sent unchanged.",messages:[{role:"user",content:{type:"text",text:o.optimized.saved>0?o.best.text:n}}]}}),t.registerTool("token_lens",{title:"Open Token Lens",description:"Open the Token Lens box in the chat, where the user shortens prompts without sending them to you. Use when asked to open or use Token Lens with no prompt given.",inputSchema:{model:Zn.optional()},annotations:{readOnlyHint:!0,openWorldHint:!1},_meta:eg("Opening Token Lens\u2026","Token Lens is open")},async({model:n})=>{let i=Sn(n??"",void 0);return{content:[{type:"text",text:"Token Lens is open in a box in the chat; prompts pasted there are shortened by Token Lens and not sent to you. If this app shows no box, ask the user to paste the prompt they want shortened, then call optimize_prompt with it and give them the shorter prompt."}],structuredContent:{panel:!0,model:n?i.label:"",profile:i.id}}}),t.registerTool("optimize_prompt",{title:"Optimize a prompt for fewer tokens",description:"Shorten a prompt for a model with the same meaning (instructions, numbers, negations, code and quotes kept; task-aware). Returns the prompt to paste, savings and tips. Use when asked to shorten or optimize a prompt.",inputSchema:{prompt:G.string().describe("The prompt, as written."),model:Zn,level:G.enum(["auto",...mA]).optional().describe("Default auto."),conversation_tokens:G.number().int().nonnegative().optional().describe("Tokens already in the chat, if known."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1},_meta:eg("Shortening the prompt\u2026","Shorter prompt ready")},async({prompt:n,model:i,level:o,conversation_tokens:s,pricing:l})=>{let c=Ys({prompt:n,model:Sn(i,l),conversationTokens:s,level:o??"auto"});return a("optimize_prompt",c.profile,c.optimized.saved,c.cost.saved.usd),{content:[{type:"text",text:JF(c)}],structuredContent:{model:c.profile.label,profile:c.profile.id,task:c.task.task,promptTokens:c.promptTokens,exact:c.exact,level:c.level,optimizedPrompt:c.optimized.text,bestPrompt:c.best.text,kept:c.best.kept,optimizedTokens:c.optimized.tokens,saved:c.optimized.saved,savedUsd:c.cost.saved.usd,savedUsdPer1k:c.cost.saved.usdPer1k,promptUsd:c.cost.promptUsd,sendUsd:c.cost.sendUsd,pricing:c.cost.pricing??null,rules:c.optimized.rules,tips:c.tips.map(u=>u.text)}}}),t.registerTool("count_tokens",{title:"Count tokens",description:"Count a text's tokens for a model (exact for OpenAI; Claude and Gemini exact with the user's own API key set, else estimated). Use when asked how many tokens something is.",inputSchema:{text:G.string(),model:Zn,pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({text:n,model:i,pricing:o})=>{let s=Sn(i,o),l=await YF(n,i,s.family)??Ie(n,s),{tokens:c,exact:u}=l,d=nt(c,s),p=Math.round(c/s.contextWindow*1e3)/10;return{content:[{type:"text",text:`${u?"":"\u2248"}${te(c)} tokens for ${s.label} (${u?"exact":"estimate"}), ${p}% of a ~${te(s.contextWindow)} token context. Costs ${xe(d)} as input each time it is sent (${xe(d===null?null:d*1e3)} per 1,000 sends). ${Pr(s)}`}],structuredContent:{tokens:c,exact:u,profile:s.id,contextWindow:s.contextWindow,inputUsd:d,pricing:s.pricing??null}}}),t.registerTool("check_conversation",{title:"Check conversation token budget",description:"How full a chat's context is and what each message costs. Pass a token count (or the text). A long chat gets an optional new-chat handoff; present it as optional, since it can change later answers. Use for chat length, limits or forgotten context.",inputSchema:{model:Zn,conversation:G.string().optional().describe("The conversation so far, as text."),conversation_tokens:G.number().int().nonnegative().optional().describe("Or: its size in tokens."),latest_request:G.string().optional().describe("The latest message, to tailor the handoff."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1},_meta:eg("Checking the chat\u2026","Chat checked")},async({model:n,conversation:i,conversation_tokens:o,latest_request:s,pricing:l})=>{let c=Sn(n,l),u=o??Ie(i??"",c).tokens,d=Xr(u,0,c),p=pr(s??i?.slice(-2e3)??"").task,m=d.zone==="hot"||d.zone==="full"?`Context is nearly full; older messages will be dropped or compacted soon. A handoff to a new chat avoids that. ${Rt}`:d.longChat||d.zone==="warm"?`Every new message re-reads about ${te(u)} tokens of history. A handoff to a new chat would cut that. ${Rt}`:"The conversation is still short; no handoff needed.",g=d.zone!=="ok"||d.longChat?$m(p):void 0,h=nt(u,c),f=g?Math.max(0,u-1e3):0,k=nt(f*10,c);return{content:[{type:"text",text:[`${te(u)} tokens, ${Math.round(d.ratio*100)}% of ${c.label}'s ~${te(d.window)} token context.`,`Each new message re-reads this history: ${xe(h)} of input per message. ${Pr(c)}`,...g?[`Optional: a handoff to a new chat saves about ${te(f)} tokens (${xe(nt(f,c))}) per message, ${xe(k)} over the next 10 messages.`]:[],m,...g?["","Optional handoff request (can change later answers):",g]:[]].join(`
3753
+ `)}],structuredContent:{model:c.label,profile:c.id,tokens:u,ratio:d.ratio,zone:d.zone,window:d.window,usdPerMessage:h,handoffSavesTokensPerMessage:f,handoffSavesUsdOver10Messages:k,handoffPrompt:g??null}}}),t.registerTool("optimize_agent",{title:"Find token hotspots and optimize an agent or conversation",description:"Find where an agent's tokens go (system prompt, tools, examples, history, next prompt), apply verified same-output fixes and return the optimized spec ready to paste, with the checks as evidence. Fixes that could change output are only listed, opt-in. Flags caching. Use when the user builds an agent or shares a system prompt or tools.",inputSchema:{model:Zn,system:G.string().optional().describe("System prompt / instructions."),tools:G.array(G.object({name:G.string(),description:G.string().optional(),parameters:G.any().optional()})).optional().describe("Tool definitions (parameters = JSON schema)."),examples:G.array(G.string()).optional().describe("Few-shot examples."),messages:G.array(G.object({role:G.enum(["user","assistant","tool","system"]),content:G.string(),toolCalls:G.array(G.string()).optional().describe("Names of tools called in this message.")})).optional().describe("Conversation history, oldest first."),prompt:G.string().optional().describe("The next user message."),pricing:uo,calls_per_month:G.number().int().positive().optional().describe("Calls per month, if known.")},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({model:n,pricing:i,calls_per_month:o,...s})=>{let l=$A(s,Sn(n,i)),c=l.report;l.verification.passed&&a("optimize_agent",c.profile,l.savings.tokens,l.savings.usd);let u=v=>o&&v!==null?v*o:null,d=c.findings.filter(v=>v.risk==="review"),p=c.findings.filter(v=>v.risk==="safe"&&!v.applied),m=[l.verification.passed&&l.savings.tokens>0?`${zi(l.savings)}${u(l.savings.usd)!==null?`, ${xe(u(l.savings.usd))} per month at ${te(o)} calls`:""}: ${te(l.before)} \u2192 ${te(l.after)} tokens per call, same model and same output (checks below).`:l.verification.passed?"No wording savings to apply: the agent's text is already tight.":"No changes applied: a check failed, so the original spec is returned unchanged.",`Cost per call now: ${xe(c.costPerCall)} of input${l.savings.tokens>0?`, ${xe(nt(l.after,c.profile))} after the fixes`:""}. ${Pr(c.profile)}`];p.length&&p[0].usd!==null&&m.push(`Also safe, needs a config change on your side: ${xe(p[0].usd)} more per call (${p[0].id}).`),d.length&&d[0].usd!==null&&m.push(`Opt-in fixes that could change the output (not applied): the biggest saves ${xe(d[0].usd)} more per call (${d[0].id}).`),m.push("",`Model profile: ${c.profile.label}. Total ${te(c.total)} tokens per call; ${te(c.fixedOverhead)} of it is re-sent every call (system + tools + examples).`,"","Where the tokens go:",...c.breakdown.map(v=>`- ${v.label}: ${te(v.tokens)} (${c.total?Math.round(v.tokens/c.total*100):0}%)`)),c.growthPerTurn&&m.push(`Growing ~${te(c.growthPerTurn)} tokens per user turn${c.turnsLeft!==null?`, about ${c.turnsLeft} turns of room left`:""}.`),m.push("",l.verification.passed?"Evidence the output is unchanged (same model, same tools, same instructions):":"Checks (one failed, so nothing was applied):",...l.verification.checks.map(v=>`${v.passed?"\u2713":"\u2717"} ${v.name}: ${v.detail}`));let g=c.findings.filter(v=>v.risk==="safe"),h=c.findings.filter(v=>v.risk==="review"),f=v=>v.id==="structured-output"?"saves every retry after a reply that doesn't parse":v.id==="price-threshold"&&!v.saves?"keeps the call at the lower rate":`saves ~${te(v.saves)} tokens${v.usd!==null?`, ${xe(v.usd)} per call, ${xe(v.usd*1e3)} per 1,000 calls`:""}`;if(g.length&&m.push("","Safe fixes:",...g.map(v=>`- ${v.applied?"[applied] ":"[do this] "}${v.issue} ${v.fix} (${f(v)})`)),h.length&&m.push("","Could change the output, so not applied (ask the user):",...h.map(v=>`- ${v.issue} ${v.fix} (${f(v)})`)),l.verification.passed&&l.savings.tokens>0){let v=(C,S)=>["",`--- ${C} (paste this) ---`,S,"--- end ---"];l.spec.system&&l.spec.system!==s.system&&m.push(...v("optimized system prompt",l.spec.system)),l.spec.tools&&JSON.stringify(l.spec.tools)!==JSON.stringify(s.tools)&&m.push(...v("optimized tool definitions (JSON)",JSON.stringify(l.spec.tools,null,2))),l.spec.prompt&&l.spec.prompt!==s.prompt&&m.push(...v("optimized next prompt",l.spec.prompt)),l.spec.messages&&JSON.stringify(l.spec.messages)!==JSON.stringify(s.messages)&&m.push("","Conversation: exact repeats were replaced with pointers; the full list is in structuredContent.optimized.messages.")}s.system&&c.findings.some(v=>v.id==="volatile-prefix")&&m.push("","--- system prompt with the changing lines last (paste this) ---",Hv(l.spec.system??s.system).text,"--- end ---");let k=c.findings.some(v=>v.id==="structured-output")?Gv(s,c.profile):null;k&&m.push("",`--- strict JSON mode: add to the request (${c.profile.family==="anthropic"?"Anthropic Messages API":c.profile.family==="google"?"Gemini generateContent":"OpenAI-compatible API"}; paste this) ---`,JSON.stringify(k.setup,null,2),"--- end ---");let b=c.findings.some(v=>v.id==="lazy-tools")?qv(l.spec.tools??[]):null;return b&&m.push("","--- on-demand tool loading: send these 2 tools instead of the full list (paste this) ---",JSON.stringify(b.tools,null,2),"--- end ---","Your runtime answers get_tool_schema with the tool's full definition (structuredContent.lazyTools.schemas) and runs invoke_tool by calling that tool with tool_input."),{content:[{type:"text",text:m.join(`
3754
+ `)}],structuredContent:{lazyTools:b,structuredOutput:k?.setup??null,before:l.before,after:l.after,savedTokens:l.savings.tokens,savedUsdPerCall:l.savings.usd,savedUsdPer1kCalls:l.savings.usdPer1k,savedUsdPerMonth:u(l.savings.usd),costPerCallUsd:c.costPerCall,pricing:c.profile.pricing??null,breakdown:c.breakdown,growthPerTurn:c.growthPerTurn,turnsLeft:c.turnsLeft,findings:c.findings,optimized:l.spec,verification:l.verification}}}),t.registerTool("audit_agent_rules",{title:"Trim coding-agent rule files",description:"Trim coding-agent rule files (CLAUDE.md, AGENTS.md, .cursorrules and the like) without changing any instruction: repeats removed, wording tightened with every number and negation kept. Returns files to paste and savings per turn. Use when the user shares agent rule files.",inputSchema:{model:Zn,files:G.array(G.object({path:G.string(),content:G.string()})).min(1).describe("Each rule file: its path and full text."),repo_paths:G.array(G.string()).optional().describe("Repo file paths, to flag rules naming missing files."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({model:n,files:i,repo_paths:o,pricing:s})=>{let l=Sn(n,s),c=jA(i,l,{knownPaths:o}),u=c.before-c.after;a("audit_agent_rules",l,c.saved.tokens,c.saved.usd);let d=[u>0?`${zi(c.saved)} per turn: ${te(c.before)} \u2192 ${te(c.after)} tokens of rules re-sent every turn, same instructions.`:`Nothing to trim: ${te(c.before)} tokens of rules, no repeats, wording already tight.`,Pr(l)],p=c.findings.filter(g=>g.kind==="duplicate");p.length&&d.push("","Exact repeats removed:",...p.map(g=>`- ${g.path}:${g.line} ${g.text} (${g.detail})`));let m=c.findings.filter(g=>g.kind==="stale-path");m.length&&d.push("","Check these (not removed): they mention files that aren't in the repo.",...m.map(g=>`- ${g.path}:${g.line} ${g.detail}`));for(let g of c.files)g.trimmed!==i.find(h=>h.path===g.path)?.content&&d.push("",`--- ${g.path}, trimmed (${te(g.tokens)} \u2192 ${te(g.trimmedTokens)} tokens; paste this) ---`,g.trimmed,"--- end ---");return{content:[{type:"text",text:d.join(`
3755
+ `)}],structuredContent:{before:c.before,after:c.after,savedTokensPerTurn:c.saved.tokens,savedUsdPerTurn:c.saved.usd,savedUsdPer1kTurns:c.saved.usdPer1k,files:c.files,findings:c.findings,pricing:l.pricing??null}}}),t.registerTool("list_model_profiles",{title:"List model profiles",description:"List the model families Token Lens knows, with context windows and prices.",inputSchema:{},annotations:{readOnlyHint:!0,openWorldHint:!1}},async()=>({content:[{type:"text",text:Object.values(hn).map(n=>`${n.id}: ${n.label}, ~${te(n.contextWindow)} context, ${n.tokenizer==="o200k"?"exact":"estimated"} counts${n.reasoning?", reasoning model":""}, ${Nv(n.pricing)}`).join(`
3756
+ `)}]})),t.registerTool("compact_data",{title:"Shrink pasted data without changing it",description:"Shrink pasted JSON or logs with the data unchanged (whitespace only, proven by parsing). Use when the user pastes large data.",inputSchema:{text:G.string().describe("Text with the pasted data, as written."),model:Zn},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({text:n,model:i})=>{let o=Sn(i,void 0),s=Um(n,o),l=$t(s.saved,o);return a("compact_data",o,s.saved,l.usd),{content:[{type:"text",text:(s.saved>0?[`${zi(l,"send")} (${Math.round(s.saved/s.before*100)}%): ${te(s.before)} \u2192 ${te(s.after)} tokens, same data. ${Pr(o)}`,"","What changed:",...s.changes.map(u=>`- ${u}`),...s.checks.length?["","Evidence the data is unchanged:",...s.checks.map(u=>`\u2713 ${u.name}: ${u.detail}`)]:[],"","--- compact text, same data (paste this) ---",s.text,"--- end ---"]:[`No saving: ${s.skipped??"already compact."} ${te(s.before)} tokens.`]).join(`
3757
+ `)}],structuredContent:{before:s.before,after:s.after,savedTokens:s.saved,savedUsd:l.usd,compactText:s.text,changes:s.changes,blocks:s.blocks,checks:s.checks,skipped:s.skipped??null}}}),t.registerTool("compare_prompts",{title:"Check a shortened prompt against the original",description:"Check a shortened prompt against the original: savings and any number, negation or rule lost.",inputSchema:{original:G.string(),edited:G.string(),model:Zn},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({original:n,edited:i,model:o})=>{let s=Sn(o,void 0),l=SA(n,i,s);l.sameInstructions&&a("compare_prompts",s,l.saved.tokens,l.saved.usd);let c=[l.verdict,l.saved.tokens>0?`${zi(l.saved,"send")}: ${te(l.before)} \u2192 ${te(l.after)} tokens. ${Pr(s)}`:`${te(l.before)} \u2192 ${te(l.after)} tokens: the edit is not shorter.`];return l.lostFacts.length&&c.push("","Missing from the shorter version (can change the answer):",...l.lostFacts.map(u=>`- ${u}`)),l.lostWords.length&&c.push("",`Words dropped that may carry instructions: ${l.lostWords.join(", ")}.`),{content:[{type:"text",text:c.join(`
3758
+ `)}],structuredContent:{before:l.before,after:l.after,savedTokens:l.saved.tokens,savedUsd:l.saved.usd,sameInstructions:l.sameInstructions,lostFacts:l.lostFacts,lostWords:l.lostWords,verdict:l.verdict}}}),t.registerTool("estimate_cost",{title:"Forecast monthly cost and same-answer savings",description:"Monthly cost of an app or agent on its model, and what same-answer caching and batching save. Use for LLM budgets.",inputSchema:{model:Zn,input_tokens:G.number().int().nonnegative().describe("Per call."),output_tokens:G.number().int().nonnegative().describe("Per call, incl. reasoning."),calls_per_day:G.number().positive(),repeated_prefix_tokens:G.number().int().nonnegative().optional().describe("Unchanging start of each call: system, tools, examples."),can_wait_share:G.number().min(0).max(1).optional().describe("Share of calls that can wait 24h."),pricing:uo},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({model:n,input_tokens:i,output_tokens:o,calls_per_day:s,repeated_prefix_tokens:l,can_wait_share:c,pricing:u})=>{let d=Sn(n,u),p=EA(d,{inputTokens:i,outputTokens:o,callsPerDay:s,repeatedPrefixTokens:l,canWaitShare:c});if(!p.priced)return{content:[{type:"text",text:`No price is set for ${d.label}. Pass pricing with your per-1M-token rates to get a forecast.`}]};let m=p.lines.reduce((h,f)=>f.monthlyUsd<h.monthlyUsd?f:h),g=[`${d.label}, ${te(p.callsPerMonth)} calls a month: ${xe(p.lines[0].monthlyUsd)} a month now${m.id!=="now"?`; ${xe(m.monthlyUsd)} with ${m.label.toLowerCase()}, saving ${xe(m.savedUsd)} a month (${xe(m.savedUsd*12)} a year) with the same answers`:""}.`,Pr(d),"",...p.lines.map(h=>`- ${h.label}: ${xe(h.monthlyUsd)} a month${h.savedUsd>0?`, saves ${xe(h.savedUsd)}`:""}.${h.note?` ${h.note}`:""}`)];return l?p.rates.cached===null&&g.push("",`${d.label} has no cached-input price on file; pass pricing.cached_input_per_mtok to include caching.`):g.push("","Tip: pass repeated_prefix_tokens (system prompt, tools and examples) to see what prompt caching saves."),!c&&["openai","anthropic","google"].includes(d.family)&&g.push("","Tip: if some calls can wait up to 24 hours (reports, bulk jobs), pass can_wait_share to see the half-price batch saving."),{content:[{type:"text",text:g.join(`
3759
3759
  `)}],structuredContent:{profile:d.id,callsPerMonth:p.callsPerMonth,longContext:p.longContext,rates:p.rates,options:p.lines}}}),t.registerTool("savings_report",{title:"Show total tokens and money saved",description:"Tokens and money Token Lens has saved: today, 7 and 30 days, total, by action and model.",inputSchema:{},annotations:{readOnlyHint:!0,openWorldHint:!1}},async()=>{if(!r)return{content:[{type:"text",text:"Totals aren't kept here: this copy of Token Lens stores nothing. Run Token Lens on your own computer (the npm package) and it keeps a running total of numbers only, never your text."}]};let n=zT(r.rows()),i=s=>`${te(s.tokens)} tokens, ${xe(s.usd)} (${te(s.count)} ${s.count===1?"time":"times"})`;return{content:[{type:"text",text:(n.since?[`Token Lens has saved ${te(n.all.tokens)} tokens and ${xe(n.all.usd)} since ${n.since}, priced for the model each saving was on.`,"",`Today: ${i(n.today)}`,`Last 7 days: ${i(n.last7)}`,`Last 30 days: ${i(n.last30)}`,"","By action:",...n.byAction.map(s=>`- ${XF[s.action]??s.action}: ${i(s)}`),"","By model:",...n.byModel.map(s=>`- ${hn[s.model]?.label??s.model}: ${i(s)}`),"","Counts the saving each time Token Lens found one; agent and rule-file savings are per call or turn, so they repeat every time those run. Kept as numbers only"+(r.where?` in ${r.where} (set TOKEN_LENS_LEDGER=off to stop).`:" for this session.")]:["Nothing saved yet. Totals start the first time Token Lens shortens a prompt, data or an agent."]).join(`
3760
3760
  `)}],structuredContent:{...n,where:r.where}}}),t}var tq={watching:"Live tips are on: Token Lens is watching the message box and the open chat in the Claude app.",starting:"Live tips are starting.","cant-see-box":"Live tips are on but can't read Claude's message box yet: click in it, or quit and reopen Claude.","needs-helper":"Live tips can't read the AI app on this Mac yet. Don't suggest turning on node: macOS would give that access to every Node program. Offer Token Lens Helper instead and explain it first: a small Mac app that gives live tips in the Claude and ChatGPT apps; it reads only the message box and open chat of whichever is in front, keeps and sends none of that text, shows tips as notifications and in its own menu bar window, changes the message box only when the user clicks, checks the npm registry daily for a newer Token Lens, and needs its own switch under System Settings > Privacy & Security > Accessibility. Call live_tips with action setup_helper only after the user says yes in their own words; they can also click Set up Token Lens Helper in the panel.",helper:"Live tips run in Token Lens Helper: tips arrive as notifications, and its menu bar icon opens the shorter draft with Copy and Put it in Claude. If they don't appear, the user turns on Token Lens Helper in System Settings > Privacy & Security > Accessibility. If they turned node on for Token Lens earlier, they can turn it off.","claude-closed":"Live tips are on and wait for the Claude app.",off:"Live tips are off.",unsupported:"Live tips work in the Claude desktop app on macOS only."};function nq(e,t){e.registerTool("live_tips",{title:"Live tips in the Claude app",description:`Turn Token Lens's live tips on or off, check them, or open their panel. When on (the default in the Claude desktop app on macOS), Token Lens reads the draft in Claude's message box and the open chat locally, and shows a macOS notification when a shorter prompt says the same thing or a chat gets costly; the shorter draft waits, ready to copy, at ${t.url}. Action setup_helper sets up Token Lens Helper, which holds the Mac permission for Token Lens alone; use it only after explaining what it reads and the user agrees in their own words. Use when the user asks about live tips, notifications, the panel, or wants them off or on.`,inputSchema:{action:G.enum(["status","on","off","open_panel","setup_helper"]).describe("What to do. Default status.").optional()},annotations:{readOnlyHint:!1,openWorldHint:!1}},async({action:r})=>{if(r==="setup_helper"){let i=t.setupHelper();return{content:[{type:"text",text:i==="started"?"Token Lens Helper is set up. When macOS asks, the user clicks Open System Settings and turns on Token Lens Helper under Privacy & Security > Accessibility. Tips then arrive as notifications, and its menu bar icon opens the shorter draft. No need to turn on node.":i==="unsupported"?"Token Lens Helper needs macOS 13.3 or later.":i==="unavailable"?"This copy of Token Lens doesn't include Token Lens Helper yet.":"Couldn't set up Token Lens Helper on this Mac."}],structuredContent:{status:t.status,helper:i,panel:t.url}}}let a=r==="on"?t.enable():r==="off"?t.disable():t.status;return r==="open_panel"&&t.openPanel(),{content:[{type:"text",text:[tq[a]??a,a==="off"||a==="unsupported"?"":`Panel: ${t.url}`].filter(Boolean).join(" ")}],structuredContent:{status:a,panel:t.url}}})}var rq=e=>new Date(e).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});function aq(e,t){let r=[e.plan?`Plan: ${e.plan.label}${e.planSource==="app"?" (read from the Claude app)":""}.`:"Plan: not known yet."];return e.sessionPct!==void 0&&r.push(`Session: ${e.sessionPct}% used${e.sessionResetsAt?`, resets ${rq(e.sessionResetsAt)}`:""}${e.meterAgeMin?` (meter read ${e.meterAgeMin} min ago, carried forward by what was sent since)`:""}.`),e.weekPct!==void 0&&r.push(`Week: ${e.weekPct}% used${e.weekResetsText?`, resets ${e.weekResetsText}`:""}.`),e.left&&r.push(`Left this session: about ${te(e.left.here)} messages in the open chat, or about ${te(e.left.fresh)} in a new chat.`),t&&e.session.messages&&r.push(`This session: ${te(e.session.messages)} messages, ${te(e.session.tokens)} tokens read.`+(e.topChats.length>1?` Where it went: ${e.topChats.map(a=>`${a.title??"a chat"} ${Math.round(a.share*100)}%`).join(", ")}.`:"")),e.insights.length&&r.push("","What to do:",...e.insights.map(a=>`- ${a}`)),r.push("","None of this changes your model or its answers. Kept on this computer as times and token counts only, never text."),r.join(`
3761
- `)}var KP={free:"claude-free",pro:"claude-pro",max:"claude-max",team:"claude-team"};function iq(e,t){e.registerTool("plan_coach",{title:"Get the most out of your Claude plan",description:"How much of the user's Claude plan (Free, Pro, Max, Team) is used and left, how many more messages fit this session in the open chat versus a new one, what is draining it, and what to do before it runs out or resets unused. Never switches the model or changes answers. Use when the user asks about usage, limits, their plan or subscription, or how to make it last.",inputSchema:{plan:G.enum(["free","pro","max","team"]).optional().describe("The user's plan, if they said; saved for next time."),session_pct_used:G.number().min(0).max(100).optional().describe("Session usage % from Claude's Settings > Usage, if the user gave it."),session_resets_in_minutes:G.number().nonnegative().optional(),week_pct_used:G.number().min(0).max(100).optional(),chat_tokens:G.number().int().nonnegative().optional().describe("Your best estimate of this chat's size in tokens.")},annotations:{readOnlyHint:!1,openWorldHint:!1}},async r=>{let a=Date.now();t&&r.plan&&t.setPlan(KP[r.plan]);let n;if(t&&r.session_pct_used===void 0&&r.week_pct_used===void 0)n=t.insights.refreshCoach(a);else{let i={at:a,sessionPct:r.session_pct_used,sessionResetsAt:r.session_resets_in_minutes!==void 0?a+r.session_resets_in_minutes*6e4:void 0,weekPct:r.week_pct_used},o=r.plan?Bn[KP[r.plan]]:t?.insights.plan??null;n=Hm({data:{sends:[],readings:r.session_pct_used!==void 0||r.week_pct_used!==void 0?[i]:[]},now:a,plan:o,planSource:o?"set":"unknown",current:r.chat_tokens?{chat:"this",tokens:r.chat_tokens}:void 0})}return{content:[{type:"text",text:aq(n,!!t)}],structuredContent:{...n}}})}async function sq(){let e=CT(),t=process.argv.indexOf("--http");if(t===-1){let a=process.platform==="darwin"?new ep:void 0;try{a?.autoStart()}catch{}await __({ledger:e,live:a}).connect(new Sm);return}let r=Number(process.argv[t+1]??process.env.PORT??8787);oq(async(a,n)=>{if(!a.url?.startsWith("/mcp")){n.writeHead(a.url==="/"?200:404,{"content-type":"text/plain"}).end(`Token Lens MCP server: POST /mcp
3761
+ `)}var KP={free:"claude-free",pro:"claude-pro",max:"claude-max",team:"claude-team"};function iq(e,t){e.registerTool("plan_coach",{title:"Get the most out of your Claude plan",description:"How much of the user's Claude plan is used and left, how many messages fit, what drains it, and how to make it last. Never switches the model. Use for usage, limits or plan questions.",inputSchema:{plan:G.enum(["free","pro","max","team"]).optional().describe("The user's plan, if they said; saved for next time."),session_pct_used:G.number().min(0).max(100).optional().describe("Session usage % from Claude's Settings > Usage, if the user gave it."),session_resets_in_minutes:G.number().nonnegative().optional(),week_pct_used:G.number().min(0).max(100).optional(),chat_tokens:G.number().int().nonnegative().optional().describe("Your best estimate of this chat's size in tokens.")},annotations:{readOnlyHint:!1,openWorldHint:!1}},async r=>{let a=Date.now();t&&r.plan&&t.setPlan(KP[r.plan]);let n;if(t&&r.session_pct_used===void 0&&r.week_pct_used===void 0)n=t.insights.refreshCoach(a);else{let i={at:a,sessionPct:r.session_pct_used,sessionResetsAt:r.session_resets_in_minutes!==void 0?a+r.session_resets_in_minutes*6e4:void 0,weekPct:r.week_pct_used},o=r.plan?Bn[KP[r.plan]]:t?.insights.plan??null;n=Hm({data:{sends:[],readings:r.session_pct_used!==void 0||r.week_pct_used!==void 0?[i]:[]},now:a,plan:o,planSource:o?"set":"unknown",current:r.chat_tokens?{chat:"this",tokens:r.chat_tokens}:void 0})}return{content:[{type:"text",text:aq(n,!!t)}],structuredContent:{...n}}})}async function sq(){let e=CT(),t=process.argv.indexOf("--http");if(t===-1){let a=process.platform==="darwin"?new ep:void 0;try{a?.autoStart()}catch{}await __({ledger:e,live:a}).connect(new Sm);return}let r=Number(process.argv[t+1]??process.env.PORT??8787);oq(async(a,n)=>{if(!a.url?.startsWith("/mcp")){n.writeHead(a.url==="/"?200:404,{"content-type":"text/plain"}).end(`Token Lens MCP server: POST /mcp
3762
3762
  `);return}let i=__({ledger:e}),o=new Rm({sessionIdGenerator:void 0});n.on("close",()=>{o.close(),i.close()}),await i.connect(o),await o.handleRequest(a,n)}).listen(r,()=>console.error(`Token Lens MCP server on http://localhost:${r}/mcp`))}sq();
package/token-lens.mcpb CHANGED
Binary file