@kunal_dutt/token-lens 0.2.49 → 0.2.50
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/README.md +5 -1
- package/dist-helper/core.js +1 -1
- package/package.json +1 -1
- package/server.mjs +4 -4
- package/token-lens-helper.zip +0 -0
- package/token-lens.mcpb +0 -0
package/README.md
CHANGED
|
@@ -57,7 +57,11 @@ Token Lens never asks for it. Instead it offers **Token Lens Helper**, a small
|
|
|
57
57
|
native Mac app set up from the chat only when you agree (say yes when Claude
|
|
58
58
|
offers it, or click **Set up Token Lens Helper** in the Token Lens card): you turn
|
|
59
59
|
it on once in System Settings > Privacy & Security > Accessibility. It reads only
|
|
60
|
-
the message box and chat of
|
|
60
|
+
the message box and chat of the AI app in front (Claude, ChatGPT, Perplexity,
|
|
61
|
+
Copilot, Gemini, Grok, DeepSeek or Le Chat) or of an AI chat site open in Safari,
|
|
62
|
+
Chrome, Edge, Arc, Brave, Vivaldi, Opera or Firefox (chatgpt.com, claude.ai,
|
|
63
|
+
gemini.google.com, perplexity.ai, chat.deepseek.com, chat.mistral.ai, grok.com,
|
|
64
|
+
copilot.microsoft.com), never your other tabs, and runs
|
|
61
65
|
Token Lens's own optimizer inside itself, with no model calls. While you type, a
|
|
62
66
|
**Shorten** button appears just above the message box when a shorter prompt says
|
|
63
67
|
the same thing; one click swaps it in (only if the box still holds the draft it
|
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 K(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"?K(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=$(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,J=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,"modelKnown",!1);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=K(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}useModel(e){let a=K(e);a.id!=="generic"&&(this.profile=a,this.modelKnown=!0,this.state.model=this.profile.label)}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 ${J(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 ${J(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: ${J(a.tokens)} \u2192 ${J(a.optimizedTokens)} tokens (${a.pct}% fewer)`,body:`Same request, ${a.savedUsd===null?"fewer tokens":`${Pe(a.savedUsd)} less per send at API prices`}. Token Lens can put the shorter version in the message box when you click.${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=$(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 ${J(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: ${J(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.49",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 K(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"?K(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=$(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,J=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,"modelKnown",!1);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=K(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}useModel(e){let a=K(e);a.id!=="generic"&&(this.profile=a,this.modelKnown=!0,this.state.model=this.profile.label)}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 ${J(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 ${J(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: ${J(a.tokens)} \u2192 ${J(a.optimizedTokens)} tokens (${a.pct}% fewer)`,body:`Same request, ${a.savedUsd===null?"fewer tokens":`${Pe(a.savedUsd)} less per send at API prices`}. Token Lens can put the shorter version in the message box when you click.${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=$(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 ${J(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: ${J(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.50",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.50",
|
|
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
|
@@ -3602,7 +3602,7 @@ tick();setInterval(tick,1000);
|
|
|
3602
3602
|
<p class="note" id="draftStats"></p>
|
|
3603
3603
|
</div>
|
|
3604
3604
|
<div id="helperBox" hidden>
|
|
3605
|
-
<p class="note"><b>Shorten right where you type:</b> Token Lens Helper adds a small Shorten button just above the message box in
|
|
3605
|
+
<p class="note"><b>Shorten right where you type:</b> Token Lens Helper adds a small Shorten button just above the message box in Claude, ChatGPT and other AI apps, and in AI chat sites open in Safari, Chrome, Edge, Arc, Brave or Firefox, so you never paste anything here. It is a small Mac app. It reads only the message box and the open chat of the AI app or AI chat page in front, never your other tabs, keeps and sends none of it, and changes your message only when you click. Your Mac asks for your OK first, then for its own switch in Privacy & Security > Accessibility.</p>
|
|
3606
3606
|
<div class="bar"><span class="note" id="helperNote"></span><button id="setup" type="button">Set up Token Lens Helper</button></div>
|
|
3607
3607
|
</div>
|
|
3608
3608
|
</div>
|
|
@@ -3858,10 +3858,10 @@ tick();setInterval(tick,1000);
|
|
|
3858
3858
|
`)}],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:H.string(),edited:H.string(),model:Yn},annotations:{readOnlyHint:!0,openWorldHint:!1}},async({original:n,edited:i,model:o})=>{let s=Sn(o,void 0),l=AA(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")}: ${Q(l.before)} \u2192 ${Q(l.after)} tokens. ${va(s)}`:`${Q(l.before)} \u2192 ${Q(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(`
|
|
3859
3859
|
`)}],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:Yn,input_tokens:H.number().int().nonnegative().describe("Per call."),output_tokens:H.number().int().nonnegative().describe("Per call, incl. reasoning."),calls_per_day:H.number().positive(),repeated_prefix_tokens:H.number().int().nonnegative().optional().describe("Unchanging start of each call: system, tools, examples."),can_wait_share:H.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),m=CA(d,{inputTokens:i,outputTokens:o,callsPerDay:s,repeatedPrefixTokens:l,canWaitShare:c});if(!m.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 p=m.lines.reduce((h,f)=>f.monthlyUsd<h.monthlyUsd?f:h),g=[`${d.label}, ${Q(m.callsPerMonth)} calls a month: ${Te(m.lines[0].monthlyUsd)} a month now${p.id!=="now"?`; ${Te(p.monthlyUsd)} with ${p.label.toLowerCase()}, saving ${Te(p.savedUsd)} a month (${Te(p.savedUsd*12)} a year) with the same answers`:""}.`,va(d),"",...m.lines.map(h=>`- ${h.label}: ${Te(h.monthlyUsd)} a month${h.savedUsd>0?`, saves ${Te(h.savedUsd)}`:""}.${h.note?` ${h.note}`:""}`)];return l?m.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(`
|
|
3860
3860
|
`)}],structuredContent:{profile:d.id,callsPerMonth:m.callsPerMonth,longContext:m.longContext,rates:m.rates,options:m.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=Ev(r.rows()),i=s=>`${Q(s.tokens)} tokens, ${Te(s.usd)} (${Q(s.count)} ${s.count===1?"time":"times"})`;return{content:[{type:"text",text:(n.since?[`Token Lens has saved ${Q(n.all.tokens)} tokens and ${Te(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=>`- ${vq[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(`
|
|
3861
|
-
`)}],structuredContent:{...n,where:r.where}}}),t}var Iq={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
|
|
3861
|
+
`)}],structuredContent:{...n,where:r.where}}}),t}var Iq={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 Claude, ChatGPT and other desktop AI apps and in AI chat sites (chatgpt.com, claude.ai, Gemini, Perplexity, DeepSeek, Le Chat, Grok, Copilot) open in a browser; it reads only the message box and open chat of the AI app or AI chat page in front, never other tabs, keeps and sends none of that text, shows a Shorten button just above the message box while they type, 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. The user can click Set up Token Lens Helper in the Token Lens box, or say yes and you call live_tips with action setup_helper; either way their Mac asks them to confirm. Never send the user to a web page or localhost.",helper:"Live tips run in Token Lens Helper: while the user types, a Shorten button appears just above the message box when a shorter prompt says the same thing (one click swaps it in), and its menu bar icon shows the details. 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."},Pq=`Set up Token Lens Helper?
|
|
3862
3862
|
|
|
3863
|
-
It's a small Mac app that shortens prompts while you type in
|
|
3863
|
+
It's a small Mac app that shortens prompts while you type in Claude, ChatGPT and other AI apps, and in AI chat sites in your browser. It reads only the message box and the open chat of the AI app or AI chat page in front (never your other tabs), keeps and sends none of that text, changes your message only when you click, and checks the npm registry daily for a newer Token Lens.
|
|
3864
3864
|
|
|
3865
|
-
macOS will then ask you to turn it on under Privacy & Security > Accessibility.`,S_=!1;function e1(e){if(S_)return Promise.resolve("busy");S_=!0;let t=`display dialog ${JSON.stringify(`${e} ${Pq}`)} with title "Token Lens" buttons {"Not now", "Set up"} default button "Not now" cancel button "Not now" with icon note`;return new Promise(r=>dq("osascript",["-e",t],{timeout:3e5},(a,n)=>{if(S_=!1,!a)return r(/button returned:Set up/.test(n)?"yes":"no");r(/-128/.test(String(a.message)+String(a.stderr??""))?"no":"no-dialog")}))}function Rq(e,t){e.registerTool("token_lens_helper",{title:"Set up Token Lens Helper from the box",description:T_,inputSchema:{box:r1},annotations:{readOnlyHint:!1,openWorldHint:!1},_meta:a1},async(r,a)=>{let i=E_(a,r)?t.status==="helper"?"on":t.helperOffer?await e1("You clicked Set up in the Token Lens box.").then(s=>s==="yes"?t.setupHelper():s):"unavailable":"not-box",o={on:"Token Lens Helper is already on.",started:"Set up. When macOS asks, click Open System Settings and turn on Token Lens Helper under Privacy & Security > Accessibility.",no:"Not set up.",busy:"The Mac is already asking: answer the Token Lens dialog.","no-dialog":"Your Mac couldn't show the question. Quit and reopen Claude, then click Set up again.","not-box":`This app didn't pass on your click. Type "Set up Token Lens Helper" in the chat instead; your Mac will ask you to confirm.`,unsupported:"Token Lens Helper needs macOS 13.3 or later.",unavailable:"This copy of Token Lens doesn't include Token Lens Helper yet.",failed:"Couldn't set up Token Lens Helper on this Mac."};return{content:[{type:"text",text:o[i]??o.failed}],structuredContent:{helper:i,text:o[i]??o.failed}}}),e.registerTool("live_tips",{title:"Live tips in the Claude app",description:"Turn Token Lens's live tips on or off, or check them. Live tips run on this Mac with no model calls: while the user types in
|
|
3865
|
+
macOS will then ask you to turn it on under Privacy & Security > Accessibility.`,S_=!1;function e1(e){if(S_)return Promise.resolve("busy");S_=!0;let t=`display dialog ${JSON.stringify(`${e} ${Pq}`)} with title "Token Lens" buttons {"Not now", "Set up"} default button "Not now" cancel button "Not now" with icon note`;return new Promise(r=>dq("osascript",["-e",t],{timeout:3e5},(a,n)=>{if(S_=!1,!a)return r(/button returned:Set up/.test(n)?"yes":"no");r(/-128/.test(String(a.message)+String(a.stderr??""))?"no":"no-dialog")}))}function Rq(e,t){e.registerTool("token_lens_helper",{title:"Set up Token Lens Helper from the box",description:T_,inputSchema:{box:r1},annotations:{readOnlyHint:!1,openWorldHint:!1},_meta:a1},async(r,a)=>{let i=E_(a,r)?t.status==="helper"?"on":t.helperOffer?await e1("You clicked Set up in the Token Lens box.").then(s=>s==="yes"?t.setupHelper():s):"unavailable":"not-box",o={on:"Token Lens Helper is already on.",started:"Set up. When macOS asks, click Open System Settings and turn on Token Lens Helper under Privacy & Security > Accessibility.",no:"Not set up.",busy:"The Mac is already asking: answer the Token Lens dialog.","no-dialog":"Your Mac couldn't show the question. Quit and reopen Claude, then click Set up again.","not-box":`This app didn't pass on your click. Type "Set up Token Lens Helper" in the chat instead; your Mac will ask you to confirm.`,unsupported:"Token Lens Helper needs macOS 13.3 or later.",unavailable:"This copy of Token Lens doesn't include Token Lens Helper yet.",failed:"Couldn't set up Token Lens Helper on this Mac."};return{content:[{type:"text",text:o[i]??o.failed}],structuredContent:{helper:i,text:o[i]??o.failed}}}),e.registerTool("live_tips",{title:"Live tips in the Claude app",description:"Turn Token Lens's live tips on or off, or check them. Live tips run on this Mac with no model calls: while the user types in an AI app (Claude, ChatGPT, Perplexity and others) or an AI chat site in a browser, a Shorten button appears above the message box when a shorter prompt says the same thing, and a notice comes when a chat gets costly. Action setup_helper asks the user in a Mac dialog to set up Token Lens Helper, which holds the Mac permission for Token Lens alone; nothing is set up unless they click Set up there. Explain what it reads first. Never send the user to a web page or localhost. Use when the user asks about live tips, the Shorten button or notifications, or wants them off or on.",inputSchema:{action:H.enum(["status","on","off","setup_helper"]).describe("What to do. Default status.").optional()},annotations:{readOnlyHint:!1,openWorldHint:!1}},async({action:r})=>{if(r==="setup_helper"){let i=t.status==="helper"?"started":await e1("The AI in your chat asked to set this up.").then(s=>s==="yes"?t.setupHelper():s);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. Then, while they type, a Shorten button appears just above the message box. 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.":i==="no"?"The user chose Not now on their Mac. Don't ask again unless they bring it up.":i==="busy"?"The Mac is already asking the user.":i==="no-dialog"?"The Mac couldn't show its question. The user can click Set up Token Lens Helper in the Token Lens box after reopening Claude.":"Couldn't set up Token Lens Helper on this Mac."}],structuredContent:{status:t.status,helper:i}}}let a=r==="on"?t.enable():r==="off"?t.disable():t.status;return{content:[{type:"text",text:Iq[a]??a}],structuredContent:{status:a,helperOffer:t.helperOffer}}})}var $q=e=>new Date(e).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});function jq(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 ${$q(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 ${Q(e.left.here)} messages in the open chat, or about ${Q(e.left.fresh)} in a new chat.`),t&&e.session.messages&&r.push(`This session: ${Q(e.session.messages)} messages, ${Q(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(`
|
|
3866
3866
|
`)}var t1={free:"claude-free",pro:"claude-pro",max:"claude-max",team:"claude-team"};function Oq(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:H.enum(["free","pro","max","team"]).optional().describe("The user's plan, if they said; saved for next time."),session_pct_used:H.number().min(0).max(100).optional().describe("Session usage % from Claude's Settings > Usage, if the user gave it."),session_resets_in_minutes:H.number().nonnegative().optional(),week_pct_used:H.number().min(0).max(100).optional(),chat_tokens:H.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(t1[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?Un[t1[r.plan]]:t?.insights.plan??null;n=Gp({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:jq(n,!!t)}],structuredContent:{...n}}})}async function Dq(){let e=jT(),t=process.argv.indexOf("--http");if(t===-1){let a=process.platform==="darwin"?new em:void 0,n=A_({ledger:e,live:a}),i=!1,o=()=>{if(!i){i=!0;try{a?.autoStart()}catch{}}};n.server.oninitialized=o,setTimeout(o,1e4).unref(),await n.connect(new Ep);return}let r=Number(process.argv[t+1]??process.env.PORT??8787);Nq(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
|
|
3867
3867
|
`);return}let i=A_({ledger:e}),o=new $p({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`))}Dq();
|
package/token-lens-helper.zip
CHANGED
|
Binary file
|
package/token-lens.mcpb
CHANGED
|
Binary file
|