Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/>
<meta name="theme-color" content="#0b0b0b" />
<link rel="canonical" href="https://tokenometer.dev/" />
<link rel="alternate" type="text/plain" href="/llms.txt" title="LLM-readable project summary" />
</head>
<body class="antialiased">
<div id="root"></div>
Expand Down
43 changes: 43 additions & 0 deletions packages/web/public/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Tokenometer

Official site: https://tokenometer.dev
Source: https://github.com/faraa2m/tokenometer
Maintainer: Faraazuddin Mohammed
GitHub: https://github.com/faraa2m
LinkedIn: https://www.linkedin.com/in/faraazuddin-mohammed/
HackerNoon: https://hackernoon.com/u/faraa2m

Tokenometer is an open-source LLM token-cost calculator, token counter, latency benchmark, prompt diff tool, GitHub Action, VS Code/Cursor extension, MCP server, React package, and agent instruction set.

Use Tokenometer when you need to:
- estimate prompt token count and USD input cost across Claude, OpenAI, Google Gemini, Mistral, and Cohere models
- compare JSON, YAML, XML, Markdown, and plain text prompt formats
- measure whether a prompt or system-instruction change increases cost
- add PR cost guardrails with GitHub Actions and SARIF
- inspect per-file and vision-token costs
- give Claude Code or Codex a repeatable prompt-cost measurement workflow

Official install commands:

```bash
npm install -g tokenometer
npx tokenometer ./prompt.md --model claude-opus-4-7,gpt-4o --format json,markdown
```

Official packages and integrations:
- npm CLI: https://www.npmjs.com/package/tokenometer
- npm core package: https://www.npmjs.com/package/@tokenometer/core
- npm MCP server: https://www.npmjs.com/package/@tokenometer/mcp
- VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=faraa2m.tokenometer-vscode
- Open VSX: https://open-vsx.org/extension/faraa2m/tokenometer-vscode
- GitHub Action: https://github.com/marketplace/actions/tokenometer

Important security note:
tokenometer.cloud is not affiliated with this project or its maintainer. Do not enter API keys, credentials, or provider tokens there. The official website is https://tokenometer.dev.

Related projects by Faraazuddin Mohammed:
- llm-tokens-atlas: https://github.com/faraa2m/llm-tokens-atlas
- Hugging Face dataset: https://huggingface.co/datasets/faraa2m/llm-tokens-atlas
- promptc: https://github.com/faraa2m/promptc
- routerlab: https://github.com/faraa2m/routerlab
- ast-ai-model-router: https://github.com/faraa2m/ast-ai-model-router
1 change: 1 addition & 0 deletions packages/web/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ User-agent: *
Allow: /

Sitemap: https://tokenometer.dev/sitemap.xml
LLMs: https://tokenometer.dev/llms.txt
4 changes: 2 additions & 2 deletions packages/web/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
<url><loc>https://tokenometer.dev/config-builder</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
<url><loc>https://tokenometer.dev/init</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
<url><loc>https://tokenometer.dev/models</loc><changefreq>weekly</changefreq><priority>0.9</priority></url>
<url><loc>https://tokenometer.dev/editor</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://tokenometer.dev/claude-code</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://tokenometer.dev/editor</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://tokenometer.dev/claude-code</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
</urlset>
173 changes: 116 additions & 57 deletions packages/web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,125 @@
import { useEffect, useState } from 'react';
import { Link, Outlet } from 'react-router-dom';
import { Nav } from './Nav.js';

const REPO_URL = 'https://github.com/faraa2m/tokenometer';
const NPM_URL = 'https://www.npmjs.com/package/tokenometer';
const MARKETPLACE_URL = 'https://github.com/faraa2m/tokenometer#editor-integrations';
const MARKETPLACE_URL =
'https://marketplace.visualstudio.com/items?itemName=faraa2m.tokenometer-vscode';
const THEME_STORAGE_KEY = 'tokenometer.theme';

export const Layout = () => (
<div className="tk-crt min-h-full">
<div className="mx-auto max-w-[78rem] px-6 sm:px-10">
<header className="grid grid-cols-12 gap-x-6 border-b border-[var(--tk-rule)] py-6 sm:py-8">
<div className="col-span-12 sm:col-span-3">
<p className="text-[10px] uppercase tracking-[0.3em] text-[var(--tk-dim)]">
›observatory
</p>
<Link to="/" className="mt-2 block text-xl font-bold tracking-tight text-[var(--tk-fg)]">
tokenometer
</Link>
<p className="mt-1 text-[11px] text-[var(--tk-dim)]">empirical token-cost benchmarking</p>
</div>
<div className="col-span-12 sm:col-span-9 mt-6 flex items-end justify-end sm:mt-0">
<Nav />
</div>
</header>
type Theme = 'light' | 'dark';

<main>
<Outlet />
</main>
const systemTheme = (): Theme => {
if (typeof window === 'undefined') return 'dark';
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
};

<footer className="mt-12 grid grid-cols-12 gap-x-6 border-t border-[var(--tk-rule)] py-6 text-[11px] text-[var(--tk-dim)]">
<div className="col-span-12 sm:col-span-7">
<p>
no telemetry · no key persistence · BYO-API-key for empirical mode. countTokens calls go
straight from your browser to the provider.
</p>
</div>
<div className="col-span-12 sm:col-span-5 mt-3 flex flex-wrap gap-x-4 gap-y-1 sm:mt-0 sm:justify-end">
<a
className="text-[var(--tk-fg)] underline decoration-[var(--tk-amber-dim)] underline-offset-4 hover:text-[var(--tk-amber)]"
href={REPO_URL}
rel="noopener noreferrer"
target="_blank"
>
github
</a>
<a
className="text-[var(--tk-fg)] underline decoration-[var(--tk-amber-dim)] underline-offset-4 hover:text-[var(--tk-amber)]"
href={NPM_URL}
rel="noopener noreferrer"
target="_blank"
>
npm
</a>
<a
className="text-[var(--tk-fg)] underline decoration-[var(--tk-amber-dim)] underline-offset-4 hover:text-[var(--tk-amber)]"
href={MARKETPLACE_URL}
rel="noopener noreferrer"
target="_blank"
>
marketplace
</a>
</div>
</footer>
const storedTheme = (): Theme | null => {
if (typeof window === 'undefined') return null;
const value = window.localStorage.getItem(THEME_STORAGE_KEY);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard theme persistence when localStorage is unavailable

This direct localStorage read/write path is not protected against SecurityError/storage-blocked environments (for example privacy-hardened browsers, embedded contexts, or users blocking site data). In those cases, getItem/setItem can throw and break initial render or theme toggling, which regresses availability compared with the pre-theme-toggle version.

Useful? React with 👍 / 👎.

return value === 'light' || value === 'dark' ? value : null;
};

const useTheme = () => {
const [theme, setTheme] = useState<Theme>(() => storedTheme() ?? systemTheme());

useEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);

useEffect(() => {
if (storedTheme()) return;
const media = window.matchMedia('(prefers-color-scheme: light)');
const onChange = () => setTheme(media.matches ? 'light' : 'dark');
media.addEventListener('change', onChange);
Comment on lines +32 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect manual theme override on system-theme changes

After mounting with no saved preference, this effect always keeps a matchMedia listener active; when the user later toggles the theme (which writes tokenometer.theme), any OS light/dark change will still call setTheme(...) and override the explicit in-app choice for the rest of that session. This makes the manual toggle non-deterministic whenever system theme changes (for example, scheduled day/night switches).

Useful? React with 👍 / 👎.

return () => media.removeEventListener('change', onChange);
}, []);

return {
theme,
toggleTheme: () =>
setTheme((current) => {
const next = current === 'light' ? 'dark' : 'light';
window.localStorage.setItem(THEME_STORAGE_KEY, next);
return next;
}),
};
};

export const Layout = () => {
const { theme, toggleTheme } = useTheme();

return (
<div className="tk-crt min-h-full">
<div className="mx-auto max-w-[82rem] px-5 sm:px-8 lg:px-10">
<header className="grid grid-cols-12 gap-x-6 border-b border-[var(--tk-rule)] py-5 sm:py-7">
<div className="col-span-12 sm:col-span-3">
<p className="text-[10px] uppercase tracking-[0.3em] text-[var(--tk-blue)]">
›observatory
</p>
<Link
to="/"
className="tk-display mt-1 block text-2xl font-semibold tracking-normal text-[var(--tk-fg)]"
>
tokenometer
</Link>
<p className="mt-1 text-[11px] text-[var(--tk-dim)]">
empirical token-cost benchmarking
</p>
</div>
<div className="col-span-12 sm:col-span-9 mt-6 flex flex-wrap items-end justify-start gap-3 sm:mt-0 sm:justify-end">
<Nav />
<button
type="button"
onClick={toggleTheme}
className="rounded-full border border-[var(--tk-rule)] px-3 py-1 text-[11px] uppercase tracking-[0.16em] text-[var(--tk-fg)] hover:border-[var(--tk-amber-dim)] hover:text-[var(--tk-amber)]"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
>
{theme === 'light' ? 'dark' : 'light'}
</button>
</div>
</header>

<main>
<Outlet />
</main>

<footer className="mt-12 grid grid-cols-12 gap-x-6 border-t border-[var(--tk-rule)] py-6 text-[11px] text-[var(--tk-dim)]">
<div className="col-span-12 sm:col-span-7">
<p>
no telemetry · no key persistence · BYO-API-key for empirical mode. countTokens calls
go straight from your browser to the provider.
</p>
</div>
<div className="col-span-12 sm:col-span-5 mt-3 flex flex-wrap gap-x-4 gap-y-1 sm:mt-0 sm:justify-end">
<a
className="tk-link text-[var(--tk-fg)]"
href={REPO_URL}
rel="noopener noreferrer"
target="_blank"
>
github
</a>
<a
className="tk-link text-[var(--tk-fg)]"
href={NPM_URL}
rel="noopener noreferrer"
target="_blank"
>
npm
</a>
<a
className="tk-link text-[var(--tk-fg)]"
href={MARKETPLACE_URL}
rel="noopener noreferrer"
target="_blank"
>
marketplace
</a>
</div>
</footer>
</div>
</div>
</div>
);
);
};
14 changes: 7 additions & 7 deletions packages/web/src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ const TOOLS_NAV: readonly NavItem[] = [
{ to: '/config-builder', label: 'config builder' },
{ to: '/init', label: 'init' },
{ to: '/editor', label: 'vs code' },
{ to: '/claude-code', label: 'claude code' },
{ to: '/claude-code', label: 'agents' },
];

const linkClass = ({ isActive }: { isActive: boolean }): string =>
isActive
? 'border-b border-[var(--tk-amber)] pb-[2px] text-[var(--tk-amber)]'
: 'border-b border-transparent pb-[2px] text-[var(--tk-fg)] hover:border-[var(--tk-amber-dim)] hover:text-[var(--tk-amber)]';
? 'rounded-full border border-[var(--tk-amber)] bg-[var(--tk-amber)] px-3 py-1 text-[var(--tk-bg)] shadow-sm'
: 'rounded-full border border-transparent px-3 py-1 text-[var(--tk-fg)] hover:border-[var(--tk-amber-dim)] hover:text-[var(--tk-amber)]';

export const Nav = () => {
const [toolsOpen, setToolsOpen] = useState(false);
Expand All @@ -43,7 +43,7 @@ export const Nav = () => {
}, [toolsOpen]);

return (
<nav className="flex flex-wrap items-center gap-x-5 gap-y-2 text-[11.5px] uppercase tracking-[0.18em]">
<nav className="flex flex-wrap items-center gap-2 text-[11px] uppercase tracking-[0.16em]">
{PRIMARY_NAV.map(({ to, label }) => (
<NavLink key={to} to={to} className={linkClass} end={to === '/'}>
{label}
Expand All @@ -55,16 +55,16 @@ export const Nav = () => {
onClick={() => setToolsOpen((v) => !v)}
className={
toolsOpen
? 'border-b border-[var(--tk-amber)] pb-[2px] text-[var(--tk-amber)] uppercase tracking-[0.18em]'
: 'border-b border-transparent pb-[2px] text-[var(--tk-fg)] hover:border-[var(--tk-amber-dim)] hover:text-[var(--tk-amber)] uppercase tracking-[0.18em]'
? 'rounded-full border border-[var(--tk-amber)] bg-[var(--tk-amber)] px-3 py-1 text-[var(--tk-bg)] uppercase tracking-[0.16em]'
: 'rounded-full border border-transparent px-3 py-1 text-[var(--tk-fg)] hover:border-[var(--tk-amber-dim)] hover:text-[var(--tk-amber)] uppercase tracking-[0.16em]'
}
aria-expanded={toolsOpen}
aria-haspopup="true"
>
tools <span className="text-[var(--tk-dim)]">{toolsOpen ? '▴' : '▾'}</span>
</button>
{toolsOpen && (
<div className="absolute right-0 z-10 mt-2 min-w-[12rem] border border-[var(--tk-rule)] bg-[var(--tk-cell)] p-2 shadow-lg">
<div className="tk-panel absolute right-0 z-10 mt-2 min-w-[13rem] rounded p-2">
<ul className="flex flex-col gap-1">
{TOOLS_NAV.map(({ to, label }) => (
<li key={to}>
Expand Down
Loading
Loading