@kunal_dutt/token-lens 0.2.36 → 0.2.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-helper/core.js +1 -1
- package/package.json +1 -1
- package/server.mjs +1 -1
- package/token-lens-helper.zip +0 -0
- package/token-lens.mcpb +0 -0
package/dist-helper/core.js
CHANGED
|
@@ -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.36",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.37",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.
|
|
3
|
+
"version": "0.2.37",
|
|
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
|
@@ -3494,7 +3494,7 @@ function run(argv) {
|
|
|
3494
3494
|
function emit(o) { out.writeData($.NSString.alloc.initWithUTF8String(JSON.stringify(o) + "\n").dataUsingEncoding($.NSUTF8StringEncoding)); }
|
|
3495
3495
|
tokenLensLoop([argv[0]], argv[1], emit, null, false);
|
|
3496
3496
|
}`,ey=MA+String.raw`
|
|
3497
|
-
function run(argv) { return tokenLensReplace(argv[0], argv[1], argv[2]); }`;var SD=60,ED=20,xD=12,TD=1800,AD=25e3,CD=15e3,zD=80,ID=3*6e4,Ii=e=>Math.round(e).toLocaleString("en-US"),LA=e=>e.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim(),Jm=class{profile;draftText="";draftSince=0;draftDone="";lastDraftNotice=-1/0;chatTold=new Map;chatTokens=new Map;chatEpoch=new Map;chatText="";chatId="";chatTitle="";titles=new Map;pending=null;pastes=new Map;told=new Set;data;pickedPlan;appPlan=null;onData;state;constructor(t="claude-sonnet",r={}){this.profile=pr(t),this.data=Lv(r.data??{sends:[],readings:[]},Date.now()),this.pickedPlan=r.plan&&Ln[r.plan]||null,this.onData=r.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(t,r=Date.now()){return this.pickedPlan=t&&Ln[t]||null,this.refreshCoach(r),this.plan}draft(t,r){let a=(t??"").trim();if(a!==this.draftText)return t===""&&this.draftText.length>2&&this.chatId?this.pending={probe:LA(this.draftText).slice(0,80),tokens:this.state.chat?.tokens??0,chat:this.chatId,at:r,text:this.draftText}:this.pending&&a===this.pending.text&&(this.pending=null),this.draftText=a,this.draftSince=r,this.state.draft=a?this.analyzeDraft(a):null,null;let n=this.state.draft;if(!n||r-this.draftSince<TD)return null;let i=qm(a);if(i===this.draftDone||r-this.lastDraftNotice<AD)return null;let o=this.draftNotice(a,n);return o?(this.draftDone=i,this.lastDraftNotice=r,o):null}draftNotice(t,r){let a=this.state.chat?.tokens??0,n=this.plan?.label??"your plan";if(_A(t,this.chatText))return{title:"You already asked this in this chat",body:`Its answer is above. Sending it again re-reads about ${Ii(a+r.tokens)} tokens of ${n}'s usage for the same question.`};if(kA(t)&&a>=CD)return{title:`"${t.slice(0,24)}" re-reads this whole chat`,body:`Sending it uses about ${Ii(a)} tokens of ${n}'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 i=wA(t);for(let s of i){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 r.tokens>=SD&&r.saved>=ED&&r.pct>=xD?{title:`Draft: ${Ii(r.tokens)} \u2192 ${Ii(r.optimizedTokens)} tokens (${r.pct}% fewer)`,body:`Same request, ${r.savedUsd===null?"fewer tokens":`${xe(r.savedUsd)} less per send at API prices`}. One click puts the shorter version in Claude's message box: open the Token Lens panel.${r.tips[0]?` ${r.tips[0]}`:""}`}:r.tips.length?{title:"Token Lens tip for your draft",body:r.tips[0]}:null}chat(t,r,a=Date.now()){let{tokens:n}=Ie(r,this.profile),i=Xr(n,0,this.profile),o=nt(n,this.profile),s=[],l=null;i.zone==="hot"||i.zone==="full"?(l=i.zone,s.push(`This chat fills about ${Math.round(i.ratio*100)}% of the context, so older messages will soon be dropped or compacted. ${Rt}`)):(i.zone==="warm"||i.longChat)&&(l=i.zone==="warm"?"warm":"long",s.push(`Every message you send re-reads about ${Ii(n)} tokens of this chat (${xe(o)} each at API prices). Starting a new chat for a new topic avoids that.`)),this.state.chat={tokens:n,ratio:i.ratio,window:i.window,perMessageUsd:o,tips:s},n<(this.chatTokens.get(t)??0)*.5&&(this.chatTold.delete(t),this.chatEpoch.set(t,(this.chatEpoch.get(t)??0)+1)),this.chatTokens.set(t,n),this.chatText=r,this.chatId=qm(`${t}#${this.chatEpoch.get(t)??0}`),this.chatTitle=t&&!/^claude$/i.test(t)?t.slice(0,60):"",this.chatTitle&&this.titles.set(this.chatId,this.chatTitle);let c=this.observe(r,a),u=null;if(l){let d=this.chatTold.get(t)??new Set;this.chatTold.set(t,d),d.has(l)||(d.add(l),u={title:`This chat: ${Ii(n)} tokens (${Math.round(i.ratio*100)}% of context)`,body:s[0]})}return c??u}observe(t,r){let a=!1,n=bA(t);n&&(this.appPlan=n);let i=vA(t,r);i&&(this.data=yA(this.data,i),a=!0);let o=this.pending;o&&r-o.at>ID?this.pending=null:o&&o.probe&&LA(t).includes(o.probe)&&(this.data.sends.push({at:o.at,tokens:o.tokens+Ie(o.text,this.profile).tokens,chat:o.chat}),this.pending=null,a=!0),a&&(this.data=Lv(this.data,r),this.onData?.(this.data));let s=this.refreshCoach(r);if(s.sessionPct!==void 0&&s.sessionPct>=zD){let c=`session:${Math.round(s.session.since/36e5)}`;if(!this.told.has(c))return this.told.add(c),{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(c=>c.includes("weekly allowance is unused"));if(l&&s.weekResetsAt){let c=`week:${Math.round(s.weekResetsAt/864e5)}`;if(!this.told.has(c))return this.told.add(c),{title:`${100-(s.weekPct??0)}% of your week is still unused`,body:l}}return null}refreshCoach(t=Date.now()){let r=Hm({data:this.data,now:t,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=r,r}analyzeDraft(t){let r=Ys({prompt:t,model:this.profile,conversationTokens:this.state.chat?.tokens??0,level:"auto"}),a=r.optimized.saved;return{tokens:r.promptTokens,optimizedTokens:r.optimized.tokens,saved:a,pct:r.optimized.originalTokens?Math.round(a/r.optimized.originalTokens*100):0,savedUsd:r.cost.saved.usd,best:a>0?r.best.text:t,kept:r.best.kept,tips:r.tips.filter(n=>!["handoff","price-threshold","family-note"].includes(n.id)).map(n=>n.text)}}};var Qm=Number(process.env.TOKEN_LENS_LIVE_PORT??4797),Ym=`http://localhost:${Qm}`,HA=np(tp(),".token-lens","live.json"),GA=np(tp(),".token-lens","usage.json");function ND(){try{let e=JSON.parse(FA(GA,"utf8"));return Array.isArray(e.sends)&&Array.isArray(e.readings)?e:void 0}catch{return}}var Xm=null;function DD(e){Xm||(Xm=setTimeout(()=>{Xm=null;try{UA(np(tp(),".token-lens"),{recursive:!0}),qA(GA,JSON.stringify({note:"Token Lens plan coach. Times and token counts only, no text.",...e}))}catch{}},5e3),Xm.unref())}function ra(){try{return JSON.parse(FA(HA,"utf8"))}catch{return{}}}function Xs(e){try{UA(np(tp(),".token-lens"),{recursive:!0}),qA(HA,JSON.stringify({...ra(),...e},null,2))}catch{}}function MD(){if(process.env.__CFBundleIdentifier===Xv)return!0;let e=process.ppid;for(let t=0;t<10&&e>1;t++){let r="";try{r=PD("ps",["-o","ppid=,command=","-p",String(e)],{encoding:"utf8"}).trim()}catch{return!1}let a=/^(\d+)\s+(.*)$/.exec(r);if(!a)return!1;if(/\/Claude\.app\//.test(a[2]))return!0;e=Number(a[1])}return!1}var BA=(e="")=>/-1719|-25211|-1743|assistive|not allowed|not authori[sz]ed/i.test(e),LD={off:"off",starting:"starting",watching:"watching Claude's message box and chat","cant-see-box":"Claude is open but its message box can't be read yet","needs-helper":`can't read the AI app without Accessibility; Token Lens won't ask for node: ${gr} can be set up at http://localhost:4797`,helper:`${gr} shows live tips (its icon is in the menu bar)`,"claude-closed":"waiting for the Claude app",unsupported:"macOS only"},BD=45,ep=class{url=Ym;insights=new Jm(process.env.TOKEN_LENS_MODEL??"claude-sonnet",{data:ND(),plan:ra().plan,onData:DD});watchers=[];panel=null;running=!1;told=new Set;hooked=!1;blind=0;app=Xv;panelKey=$D();current="off";get status(){return this.current}set status(t){t!==this.current&&console.error(`Token Lens live tips: ${LD[t]}`),this.current=t}autoStart(){if(process.platform!=="darwin")return void(this.status="unsupported");let t=process.env.TOKEN_LENS_LIVE,r=ra().on,a=MD(),n=t==="0"?!1:t==="1"?!0:r??a;console.error(`Token Lens live tips: ${n?"starting":"off"} (macOS; started by the Claude app: ${a?"yes":"no"}; setting: ${r===void 0?"default":r?"on":"off"}${t?`; TOKEN_LENS_LIVE=${t}`:""})`),n&&this.start()}enable(){return Xs({on:!0}),process.platform!=="darwin"?this.status="unsupported":(this.start(),this.status)}disable(){return Xs({on:!1}),ra().nativeHelper&&Yv(),this.stop(),this.status}setPlan(t){Xs({plan:t??void 0}),this.insights.setPlan(t)}replaceDraft(){let t=this.insights.state.draft?.best,r=this.insights.currentDraft;return!t||!r||t===r?Promise.resolve("nothing"):new Promise(a=>ty("osascript",["-l","JavaScript","-e",ey,t,r,this.app],{timeout:8e3},(n,i)=>{let o=String(i??"").trim();a(n?"failed":o==="ok"||o==="changed"||o==="no-box"?o:"failed")}))}setupHelper(){if(process.platform!=="darwin"||!Wv())return"unsupported";let t=Zv();return t==="unavailable"||t==="failed"?t:(Xs({nativeHelper:!0,on:!0}),this.watchers.forEach(r=>r.kill()),this.watchers=[],Jv(),this.status="helper",this.running||this.start(),"started")}get helperOffer(){return!ra().nativeHelper&&Wv()&&Km()!==null}openPanel(){ty("open",[`${Ym}/#k=${this.panelKey}`],()=>{})}start(){this.running||(this.running=!0,this.status="starting",this.panel=jD((t,r)=>{let a=t.headers.host??"",n=t.headers.origin,i=[`localhost:${Qm}`,`127.0.0.1:${Qm}`];if(r.setHeader("x-frame-options","DENY"),r.setHeader("content-security-policy","frame-ancestors 'none'"),!i.includes(a)||n!==void 0&&!i.some(s=>n===`http://${s}`))return void r.writeHead(403).end();let o=t.headers["x-token-lens-key"]===this.panelKey;if(t.url==="/state")return r.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),o?(this.insights.refreshCoach(),void r.end(JSON.stringify({status:this.status,app:Zm[this.app]??"Claude",helperOffer:this.helperOffer,...this.insights.state}))):void r.end(JSON.stringify({status:this.status,locked:!0}));if(t.method==="POST"&&n!==void 0&&t.url==="/unlock")return this.openPanel(),void r.writeHead(204).end();if(t.method==="POST"&&!o)return void r.writeHead(403).end();if(t.method==="POST"&&n!==void 0){let s=c=>r.writeHead(200,{"content-type":"application/json"}).end(JSON.stringify(c));if(t.url==="/helper/setup")return void s({result:this.setupHelper()});if(t.url==="/replace")return void this.replaceDraft().then(c=>s({result:c}));let l=/^\/plan\/([a-z-]+)$/.exec(t.url??"");return l&&(l[1]==="auto"||l[1]in Ln)?(this.setPlan(l[1]==="auto"?null:l[1]),void s({plan:this.insights.plan?.id??null})):void r.writeHead(404).end()}r.writeHead(200,{"content-type":"text/html; charset=utf-8"}).end(UD)}),this.panel.on("error",()=>this.stop()),this.panel.listen(Qm,"127.0.0.1",()=>{if(this.running){if(ra().nativeHelper)return this.useHelper();this.watch("draft"),this.watch("chat")}}),this.hooked||(this.hooked=!0,process.stdin.on("end",()=>process.exit(0)),process.on("exit",()=>this.watchers.forEach(t=>t.kill()))))}stop(){this.running=!1,this.status="off",this.watchers.forEach(t=>t.kill()),this.watchers=[],this.panel?.close(),this.panel=null}watch(t){if(!this.running)return;let r=RD("osascript",["-l","JavaScript","-e",Qv,t,String(process.pid)],{stdio:["ignore","pipe","pipe"]});this.watchers.push(r),r.on("error",()=>{});let a="";r.stderr.on("data",n=>a=(a+n.toString()).slice(-2e3)),OD({input:r.stdout}).on("line",n=>this.onRaw(n)),r.on("exit",()=>{this.watchers=this.watchers.filter(n=>n!==r),BA(a)&&this.needsPermission(),this.running&&setTimeout(()=>this.watch(t),3e4).unref()})}useHelper(){let t=Kv()?"current":Zv();if(t==="unavailable"||t==="failed"){this.watch("draft"),this.watch("chat");return}this.status="helper",Jv(),t==="installed"&&!this.told.has("rebuilt")&&(this.told.add("rebuilt"),Qs(`${gr} was updated`,`If macOS asks, turn on ${gr} again in System Settings > Privacy & Security > Accessibility. The permission belongs to the exact app.`))}onRaw(t){let r;try{r=JSON.parse(t)}catch{return}try{this.onLine(r)}catch{}}onLine(t){if(t.a&&Zm[t.a]&&(this.app=t.a),t.s==="error"){BA(t.e)&&this.needsPermission();return}if(t.s==="closed")return void(this.status="claude-closed");t.s==="ok"&&(t.d===null||t.d===void 0?(this.status!=="watching"&&++this.blind===BD&&this.cantSee(),this.status!=="watching"&&(this.status="cant-see-box")):(this.status="watching",this.welcome()));let r=null;t.s==="ok"?r=this.insights.draft(t.d??null,Date.now()):t.s==="chat"&&t.t&&(r=this.insights.chat(t.k??"",t.t,Date.now())),r&&Qs(r.title,r.body)}welcome(){this.told.has("welcome")||ra().sawBox||(this.told.add("welcome"),Xs({sawBox:!0}),Qs("Token Lens is on in Claude",`As you type, it tells you here when a shorter prompt says the same thing, and when a chat gets costly. Shorter versions wait at ${Ym}. Nothing leaves your Mac.`))}cantSee(){this.told.has("blind")||(this.told.add("blind"),Qs("Token Lens can't see Claude's message box yet","Click in the box where you type to Claude. If this shows again, quit Claude (Claude > Quit Claude) and open it again."))}needsPermission(){this.status="needs-helper",!(this.told.has("helper")||!this.helperOffer)&&(this.told.add("helper"),Qs("Live tips need one step",`Open ${Ym} to read what Token Lens Helper does and set it up if you want it.`))}};function Qs(e,t){ty("osascript",["-e","on run argv","-e","display notification (item 2 of argv) with title (item 1 of argv)","-e","end run",e,t],()=>{})}var UD=`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3497
|
+
function run(argv) { return tokenLensReplace(argv[0], argv[1], argv[2]); }`;var SD=60,ED=20,xD=12,TD=1800,AD=25e3,CD=15e3,zD=80,ID=3*6e4,Ii=e=>Math.round(e).toLocaleString("en-US"),LA=e=>e.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim(),Jm=class{profile;draftText="";draftSince=0;draftDone="";lastDraftNotice=-1/0;chatTold=new Map;chatTokens=new Map;chatEpoch=new Map;chatText="";chatId="";chatTitle="";titles=new Map;pending=null;pastes=new Map;told=new Set;data;pickedPlan;appPlan=null;onData;state;constructor(t="claude-sonnet",r={}){this.profile=pr(t),this.data=Lv(r.data??{sends:[],readings:[]},Date.now()),this.pickedPlan=r.plan&&Ln[r.plan]||null,this.onData=r.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(t,r=Date.now()){return this.pickedPlan=t&&Ln[t]||null,this.refreshCoach(r),this.plan}draft(t,r){let a=(t??"").trim();if(a!==this.draftText)return t===""&&this.draftText.length>2&&this.chatId?this.pending={probe:LA(this.draftText).slice(0,80),tokens:this.state.chat?.tokens??0,chat:this.chatId,at:r,text:this.draftText}:this.pending&&a===this.pending.text&&(this.pending=null),this.draftText=a,this.draftSince=r,this.state.draft=a?this.analyzeDraft(a):null,null;let n=this.state.draft;if(!n||r-this.draftSince<TD)return null;let i=qm(a);if(i===this.draftDone||r-this.lastDraftNotice<AD)return null;let o=this.draftNotice(a,n);return o?(this.draftDone=i,this.lastDraftNotice=r,o):null}draftNotice(t,r){let a=this.state.chat?.tokens??0,n=this.plan?.label??"your plan";if(_A(t,this.chatText))return{title:"You already asked this in this chat",body:`Its answer is above. Sending it again re-reads about ${Ii(a+r.tokens)} tokens of ${n}'s usage for the same question.`};if(kA(t)&&a>=CD)return{title:`"${t.slice(0,24)}" re-reads this whole chat`,body:`Sending it uses about ${Ii(a)} tokens of ${n}'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 i=wA(t);for(let s of i){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 r.tokens>=SD&&r.saved>=ED&&r.pct>=xD?{title:`Draft: ${Ii(r.tokens)} \u2192 ${Ii(r.optimizedTokens)} tokens (${r.pct}% fewer)`,body:`Same request, ${r.savedUsd===null?"fewer tokens":`${xe(r.savedUsd)} less per send at API prices`}. One click puts the shorter version in Claude's message box: open the Token Lens panel.${r.tips[0]?` ${r.tips[0]}`:""}`}:r.tips.length?{title:"Token Lens tip for your draft",body:r.tips[0]}:null}chat(t,r,a=Date.now()){let{tokens:n}=Ie(r,this.profile),i=Xr(n,0,this.profile),o=nt(n,this.profile),s=[],l=null;i.zone==="hot"||i.zone==="full"?(l=i.zone,s.push(`This chat fills about ${Math.round(i.ratio*100)}% of the context, so older messages will soon be dropped or compacted. ${Rt}`)):(i.zone==="warm"||i.longChat)&&(l=i.zone==="warm"?"warm":"long",s.push(`Every message you send re-reads about ${Ii(n)} tokens of this chat (${xe(o)} each at API prices). Starting a new chat for a new topic avoids that.`)),this.state.chat={tokens:n,ratio:i.ratio,window:i.window,perMessageUsd:o,tips:s},n<(this.chatTokens.get(t)??0)*.5&&(this.chatTold.delete(t),this.chatEpoch.set(t,(this.chatEpoch.get(t)??0)+1)),this.chatTokens.set(t,n),this.chatText=r,this.chatId=qm(`${t}#${this.chatEpoch.get(t)??0}`),this.chatTitle=t&&!/^claude$/i.test(t)?t.slice(0,60):"",this.chatTitle&&this.titles.set(this.chatId,this.chatTitle);let c=this.observe(r,a),u=null;if(l){let d=this.chatTold.get(t)??new Set;this.chatTold.set(t,d),d.has(l)||(d.add(l),u={title:`This chat: ${Ii(n)} tokens (${Math.round(i.ratio*100)}% of context)`,body:s[0]})}return c??u}observe(t,r){let a=!1,n=bA(t);n&&(this.appPlan=n);let i=vA(t,r);i&&(this.data=yA(this.data,i),a=!0);let o=this.pending;o&&r-o.at>ID?this.pending=null:o&&o.probe&&LA(t).includes(o.probe)&&(this.data.sends.push({at:o.at,tokens:o.tokens+Ie(o.text,this.profile).tokens,chat:o.chat}),this.pending=null,a=!0),a&&(this.data=Lv(this.data,r),this.onData?.(this.data));let s=this.refreshCoach(r);if(s.sessionPct!==void 0&&s.sessionPct>=zD){let c=`session:${Math.round(s.session.since/36e5)}`;if(!this.told.has(c))return this.told.add(c),{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(c=>c.includes("weekly allowance is unused"));if(l&&s.weekResetsAt){let c=`week:${Math.round(s.weekResetsAt/864e5)}`;if(!this.told.has(c))return this.told.add(c),{title:`${100-(s.weekPct??0)}% of your week is still unused`,body:l}}return null}refreshCoach(t=Date.now()){let r=Hm({data:this.data,now:t,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=r,r}analyzeDraft(t){let r=Ys({prompt:t,model:this.profile,conversationTokens:this.state.chat?.tokens??0,level:"auto"}),a=r.optimized.saved;return{tokens:r.promptTokens,optimizedTokens:r.optimized.tokens,saved:a,pct:r.optimized.originalTokens?Math.round(a/r.optimized.originalTokens*100):0,savedUsd:r.cost.saved.usd,best:a>0?r.best.text:t,kept:r.best.kept,tips:r.tips.filter(n=>!["handoff","price-threshold","family-note"].includes(n.id)).map(n=>n.text)}}};var Qm=Number(process.env.TOKEN_LENS_LIVE_PORT??4797),Ym=`http://localhost:${Qm}`,HA=np(tp(),".token-lens","live.json"),GA=np(tp(),".token-lens","usage.json");function ND(){try{let e=JSON.parse(FA(GA,"utf8"));return Array.isArray(e.sends)&&Array.isArray(e.readings)?e:void 0}catch{return}}var Xm=null;function DD(e){Xm||(Xm=setTimeout(()=>{Xm=null;try{UA(np(tp(),".token-lens"),{recursive:!0}),qA(GA,JSON.stringify({note:"Token Lens plan coach. Times and token counts only, no text.",...e}))}catch{}},5e3),Xm.unref())}function ra(){try{return JSON.parse(FA(HA,"utf8"))}catch{return{}}}function Xs(e){try{UA(np(tp(),".token-lens"),{recursive:!0}),qA(HA,JSON.stringify({...ra(),...e},null,2))}catch{}}function MD(){if(process.env.__CFBundleIdentifier===Xv)return!0;let e=process.ppid;for(let t=0;t<10&&e>1;t++){let r="";try{r=PD("ps",["-o","ppid=,command=","-p",String(e)],{encoding:"utf8"}).trim()}catch{return!1}let a=/^(\d+)\s+(.*)$/.exec(r);if(!a)return!1;if(/\/Claude\.app\//.test(a[2]))return!0;e=Number(a[1])}return!1}var BA=(e="")=>/-1719|-25211|-1743|assistive|not allowed|not authori[sz]ed/i.test(e),LD={off:"off",starting:"starting",watching:"watching Claude's message box and chat","cant-see-box":"Claude is open but its message box can't be read yet","needs-helper":`can't read the AI app without Accessibility; Token Lens won't ask for node: ${gr} can be set up at http://localhost:4797`,helper:`${gr} shows live tips (its icon is in the menu bar)`,"claude-closed":"waiting for the Claude app",unsupported:"macOS only"},BD=45,ep=class{url=Ym;insights=new Jm(process.env.TOKEN_LENS_MODEL??"claude-sonnet",{data:ND(),plan:ra().plan,onData:DD});watchers=[];panel=null;running=!1;told=new Set;hooked=!1;blind=0;helperOn=!1;app=Xv;panelKey=$D();current="off";get status(){return this.current}set status(t){t!==this.current&&console.error(`Token Lens live tips: ${LD[t]}`),this.current=t}autoStart(){if(process.platform!=="darwin")return void(this.status="unsupported");let t=process.env.TOKEN_LENS_LIVE,r=ra().on,a=MD(),n=t==="0"?!1:t==="1"?!0:r??a;console.error(`Token Lens live tips: ${n?"starting":"off"} (macOS; started by the Claude app: ${a?"yes":"no"}; setting: ${r===void 0?"default":r?"on":"off"}${t?`; TOKEN_LENS_LIVE=${t}`:""})`),n&&this.start()}enable(){return Xs({on:!0}),process.platform!=="darwin"?this.status="unsupported":(this.start(),this.status)}disable(){return Xs({on:!1}),ra().nativeHelper&&Yv(),this.stop(),this.status}setPlan(t){Xs({plan:t??void 0}),this.insights.setPlan(t)}replaceDraft(){let t=this.insights.state.draft?.best,r=this.insights.currentDraft;return!t||!r||t===r?Promise.resolve("nothing"):new Promise(a=>ty("osascript",["-l","JavaScript","-e",ey,t,r,this.app],{timeout:8e3},(n,i)=>{let o=String(i??"").trim();a(n?"failed":o==="ok"||o==="changed"||o==="no-box"?o:"failed")}))}setupHelper(){if(process.platform!=="darwin"||!Wv())return"unsupported";let t=Zv();return t==="unavailable"||t==="failed"?t:(Xs({nativeHelper:!0,on:!0}),this.helperOn=!0,this.watchers.forEach(r=>r.kill()),this.watchers=[],Jv(),this.status="helper",this.running||this.start(),"started")}get helperOffer(){return!ra().nativeHelper&&Wv()&&Km()!==null}openPanel(){ty("open",[`${Ym}/#k=${this.panelKey}`],()=>{})}start(){this.running||(this.running=!0,this.status="starting",this.panel=jD((t,r)=>{let a=t.headers.host??"",n=t.headers.origin,i=[`localhost:${Qm}`,`127.0.0.1:${Qm}`];if(r.setHeader("x-frame-options","DENY"),r.setHeader("content-security-policy","frame-ancestors 'none'"),!i.includes(a)||n!==void 0&&!i.some(s=>n===`http://${s}`))return void r.writeHead(403).end();let o=t.headers["x-token-lens-key"]===this.panelKey;if(t.url==="/state")return r.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),o?(this.insights.refreshCoach(),void r.end(JSON.stringify({status:this.status,app:Zm[this.app]??"Claude",helperOffer:this.helperOffer,...this.insights.state}))):void r.end(JSON.stringify({status:this.status,locked:!0}));if(t.method==="POST"&&n!==void 0&&t.url==="/unlock")return this.openPanel(),void r.writeHead(204).end();if(t.method==="POST"&&!o)return void r.writeHead(403).end();if(t.method==="POST"&&n!==void 0){let s=c=>r.writeHead(200,{"content-type":"application/json"}).end(JSON.stringify(c));if(t.url==="/helper/setup")return void s({result:this.setupHelper()});if(t.url==="/replace")return void this.replaceDraft().then(c=>s({result:c}));let l=/^\/plan\/([a-z-]+)$/.exec(t.url??"");return l&&(l[1]==="auto"||l[1]in Ln)?(this.setPlan(l[1]==="auto"?null:l[1]),void s({plan:this.insights.plan?.id??null})):void r.writeHead(404).end()}r.writeHead(200,{"content-type":"text/html; charset=utf-8"}).end(UD)}),this.panel.on("error",()=>this.stop()),this.panel.listen(Qm,"127.0.0.1",()=>{if(this.running){if(ra().nativeHelper)return this.useHelper();this.watch("draft"),this.watch("chat")}}),this.hooked||(this.hooked=!0,process.stdin.on("end",()=>process.exit(0)),process.on("exit",()=>this.watchers.forEach(t=>t.kill()))))}stop(){this.running=!1,this.helperOn=!1,this.status="off",this.watchers.forEach(t=>t.kill()),this.watchers=[],this.panel?.close(),this.panel=null}watch(t){if(!this.running||this.helperOn)return;let r=RD("osascript",["-l","JavaScript","-e",Qv,t,String(process.pid)],{stdio:["ignore","pipe","pipe"]});this.watchers.push(r),r.on("error",()=>{});let a="";r.stderr.on("data",n=>a=(a+n.toString()).slice(-2e3)),OD({input:r.stdout}).on("line",n=>this.onRaw(n)),r.on("exit",()=>{this.watchers=this.watchers.filter(n=>n!==r),!this.helperOn&&(BA(a)&&this.needsPermission(),this.running&&setTimeout(()=>this.watch(t),3e4).unref())})}useHelper(){let t=Kv()?"current":Zv();if(t==="unavailable"||t==="failed"){this.watch("draft"),this.watch("chat");return}this.helperOn=!0,this.status="helper",Jv(),t==="installed"&&!this.told.has("rebuilt")&&(this.told.add("rebuilt"),Qs(`${gr} was updated`,`If macOS asks, turn on ${gr} again in System Settings > Privacy & Security > Accessibility. The permission belongs to the exact app.`))}onRaw(t){if(this.helperOn)return;let r;try{r=JSON.parse(t)}catch{return}try{this.onLine(r)}catch{}}onLine(t){if(t.a&&Zm[t.a]&&(this.app=t.a),t.s==="error"){BA(t.e)&&this.needsPermission();return}if(t.s==="closed")return void(this.status="claude-closed");t.s==="ok"&&(t.d===null||t.d===void 0?(this.status!=="watching"&&++this.blind===BD&&this.cantSee(),this.status!=="watching"&&(this.status="cant-see-box")):(this.status="watching",this.welcome()));let r=null;t.s==="ok"?r=this.insights.draft(t.d??null,Date.now()):t.s==="chat"&&t.t&&(r=this.insights.chat(t.k??"",t.t,Date.now())),r&&Qs(r.title,r.body)}welcome(){this.told.has("welcome")||ra().sawBox||(this.told.add("welcome"),Xs({sawBox:!0}),Qs("Token Lens is on in Claude",`As you type, it tells you here when a shorter prompt says the same thing, and when a chat gets costly. Shorter versions wait at ${Ym}. Nothing leaves your Mac.`))}cantSee(){this.told.has("blind")||(this.told.add("blind"),Qs("Token Lens can't see Claude's message box yet","Click in the box where you type to Claude. If this shows again, quit Claude (Claude > Quit Claude) and open it again."))}needsPermission(){this.status="needs-helper",!(this.told.has("helper")||!this.helperOffer)&&(this.told.add("helper"),Qs("Live tips need one step",`Open ${Ym} to read what Token Lens Helper does and set it up if you want it.`))}};function Qs(e,t){ty("osascript",["-e","on run argv","-e","display notification (item 2 of argv) with title (item 1 of argv)","-e","end run",e,t],()=>{})}var UD=`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3498
3498
|
<title>Token Lens Live</title><style>
|
|
3499
3499
|
:root{--bg:#fafaf9;--fg:#1c1917;--muted:#78716c;--card:#fff;--line:#e7e5e4;--accent:#0f766e}
|
|
3500
3500
|
@media (prefers-color-scheme:dark){:root{--bg:#1c1917;--fg:#f5f5f4;--muted:#a8a29e;--card:#292524;--line:#44403c;--accent:#2dd4bf}}
|
package/token-lens-helper.zip
CHANGED
|
Binary file
|
package/token-lens.mcpb
CHANGED
|
Binary file
|