Suggests English words in the prompt box as you type


English | 한국어
A Claude Code plugin that suggests English words above the prompt box while you type, like a phone keyboard. Type two or more letters of an English word and up to five suggestions appear, most common first. The case you started with is kept (Recom -> Recommend).
Requires Claude Code 2.1.292 or later (uses the prompt.autocomplete plugin event).
In a Claude Code terminal session:
/plugin install english-autocomplete --marketplace magatone/claude-autocomplete
Answer y to add the marketplace, then pick a scope (user scope is the default).
codex-auto, fixed version)Use codex-auto to run Codex CLI with native English word completion. This release is fixed at Codex 0.161.0 and does not support automatic updates. Updating official codex does not update codex-auto; codex-auto update is not supported.
It shares the existing local Codex account, settings and sessions. Type two English letters, select with Up/Down, and press Tab to insert. Enter sends your prompt. Typo correction and saved shortcuts below are Claude plugin features.
This is a source release: build tools are required and no prebuilt executable is included. Windows was tested locally; macOS build instructions are unverified. Follow the Windows/macOS installation steps, then run:
codex-auto --version
codex-auto
Expected version: codex-cli 0.161.0. Version synchronization and automatic-update experiments are paused and preserved on feat/codex-version-sync and draft PR #2.
recom).Tab to replace what you typed with the selected word.Misspelt a word? When nothing matches what you typed, the plugin suggests the words you probably meant, each marked typo?. Pick one and press Tab to fix it.
deploy typo?
deployment typo?
❯ depoly
It forgives one slip: two letters swapped (depoly), a missing letter (funtion), an extra letter (commmit) or a wrong letter (deplay). It starts at four letters, assumes the first letter is right, and stays quiet while you type correctly. In our test it caught 58 of 60 common typos and never showed up while typing correctly.
Save a short key for a phrase you type often, then type the key and press Tab. For example, register ptg for push to github: typing ptg shows push to github at the top of the suggestions.
Register shortcuts either way. Both use the same list, which stays after you restart Claude Code.
With a command
/shortcut add ptg push to github
/shortcut list
/shortcut remove ptg
By asking Claude
Add a shortcut: ptg means push to github
You can also ask Claude to show or remove your shortcuts.
A key is two or more letters or digits starting with a letter (ptg, pr2), not case sensitive. Shortcuts appear before word suggestions.
Besides everyday English, the plugin knows about 900 words people use at work in Claude Code, grouped in data/terms/: common, planning, frontend, backend, infra, devtools, design, ai (LLMs and agents), ax (bringing AI into a team), and tools (proper names and acronyms such as TypeScript or LLM, kept in their own case). Up to three of these come first, so comm suggests command and commit before community. When you type three letters or fewer, only one leads, so everyday words still show up. To add words, put one per line in any .txt file in data/terms/.
codex-auto CLI and pause automatic-update experiments. Source-build distribution; no prebuilt binaries.codex-auto.typo?./shortcut add ptg push to github, or ask Claude.comm suggests commit first.claude --plugin-dir ./claude-autocomplete
claude plugin test ./claude-autocomplete
A native-composer Codex prototype was built as a separate modified CLI. Codex plugin/skill integration has not been implemented. See setup and limitations.
This README is available in English and Korean. Support for other languages will be added once there is demand for them.
data/terms/: MIT, see LICENSE.data/wordfreq-en-25000-log.json): from aparrish/wordfreq-en-25000, exported from wordfreq by Robyn Speer. wordfreq data is licensed CC BY-SA 4.0; the word list is redistributed under the same license. It is not filtered for offensive words.hooks/register.ts 79 lines1import type { Register } from 'claude-code'
2import { complete, fixTypos, makeIndex, makeTerms, makeTypoPool } from './complete'
3import { applyShortcut, matchShortcuts, runShortcutCommand, type Shortcuts } from './shortcuts'
4
5const STORE_KEY = 'shortcuts'
6// The model calls the tool as mcp__<plugin name>__<tool name>.
7const TOOL = 'mcp__english-autocomplete__shortcuts'
8
9const asShortcuts = (value: unknown): Shortcuts => (value && typeof value === 'object' ? (value as Shortcuts) : {})
10
11export const register: Register = on => {
12 let general: string[] | undefined
13 let terms: string[] = []
14 let pool: string[] = []
15 let known = new Set<string>()
16 // Cached copy of what $.store holds; the engine wants $.store spelled out at each call, hence no helper.
17 let shortcuts: Shortcuts | undefined
18
19 on('session.start', async ($, e, next) => {
20 await $.command.register({
21 name: 'shortcut',
22 description: 'Prompt-box shortcuts: /shortcut add ptg push to github | remove ptg | list',
23 })
24 await $.tool.register({
25 name: 'shortcuts',
26 description:
27 "Manage the person's prompt-box shortcuts (english-autocomplete plugin). A shortcut is a short key " +
28 'like "ptg" that the person turns into a phrase like "push to github" by pressing Tab. ' +
29 'Use when the person asks to add, change, remove or list shortcuts.',
30 inputSchema: {
31 type: 'object',
32 properties: {
33 action: { type: 'string', enum: ['add', 'remove', 'list'] },
34 key: { type: 'string', description: 'The short key: 2+ letters or digits starting with a letter, e.g. "ptg"' },
35 text: { type: 'string', description: 'For add: the phrase the key turns into' },
36 },
37 required: ['action'],
38 },
39 })
40 return next(e)
41 })
42
43 on('command.run', { command: 'shortcut' }, async ($, e) => {
44 shortcuts ??= asShortcuts(await $.store.get(STORE_KEY))
45 const { map, message } = runShortcutCommand(shortcuts, e.args)
46 if (map !== shortcuts) await $.store.set(STORE_KEY, (shortcuts = map))
47 return { text: message }
48 })
49
50 // ponytail: no matcher, since this build's tool-name types don't list a plugin's own tool; one string compare per call.
51 on('tool.call', async ($, e, next) => {
52 if ((e.tool as string) !== TOOL) return next(e)
53 const input = e as unknown as Record<string, unknown>
54 shortcuts ??= asShortcuts(await $.store.get(STORE_KEY))
55 const { map, message } = applyShortcut(shortcuts, String(input.action ?? ''), String(input.key ?? ''), String(input.text ?? ''))
56 if (map !== shortcuts) await $.store.set(STORE_KEY, (shortcuts = map))
57 return { result: message }
58 }).catch(($, e, next) => next(e)) // a failure here must never block the person's other tools
59
60 on('prompt.autocomplete', { token: /^[A-Za-z][A-Za-z0-9']+$/ }, async ($, e, next) => {
61 if (!general) {
62 const data = `${$.plugin.root}/data`
63 general = makeIndex(JSON.parse(await $.fs.read(`${data}/wordfreq-en-25000-log.json`)))
64 const files = (await $.fs.list(`${data}/terms`)).filter(f => f.name.endsWith('.txt'))
65 terms = makeTerms(await Promise.all(files.map(f => $.fs.read(`${data}/terms/${f.name}`))), general)
66 pool = makeTypoPool(general, terms)
67 known = new Set(pool.map(w => w.toLowerCase()))
68 }
69 shortcuts ??= asShortcuts(await $.store.get(STORE_KEY))
70 // Shortcuts lead, then words; five rows in all. Only when nothing matches do typo fixes fill in, marked as guesses.
71 const exact = [...matchShortcuts(shortcuts, e.token), ...complete(general, e.token, terms)].slice(0, 5)
72 const mine = exact.length
73 ? exact.map(text => ({ text }))
74 : fixTypos(pool, known, e.token).map(text => ({ text, description: 'typo?' }))
75 const below = await next(e)
76 return { suggestions: [...below.suggestions, ...mine] }
77 })
78}
79hooks/complete.ts 104 lines1// Pure matching logic, kept apart from register.ts so it can be tested without the engine.
2
3const WORD = /^[a-z]+(?:'[a-z]+)?$/
4
5// rows come from wordfreq-en-25000-log.json, already sorted by frequency (most common first).
6export function makeIndex(rows: [string, number][]): string[] {
7 return rows.map(r => r[0]).filter(w => WORD.test(w))
8}
9
10// Parses the data/terms/*.txt files (one word per line, # comments) into one list:
11// words the general list knows come first in its frequency order, the rest in file order.
12// A proper name ("TypeScript") wins over the same word in lowercase.
13export function makeTerms(texts: readonly string[], general: readonly string[]): string[] {
14 const byKey = new Map<string, string>()
15 for (const line of texts.flatMap(t => t.split(/\r?\n/))) {
16 const w = line.trim()
17 if (!w || w.startsWith('#')) continue
18 const key = w.toLowerCase()
19 if (!byKey.has(key) || w !== key) byKey.set(key, w)
20 }
21 const rank = new Map(general.map((w, i) => [w, i]))
22 const order = [...byKey.keys()]
23 return order
24 .map((key, i) => ({ word: byKey.get(key)!, at: rank.get(key) ?? general.length + i }))
25 .sort((a, b) => a.at - b.at)
26 .map(t => t.word)
27}
28
29function matches(words: readonly string[], lower: string, limit: number, skip: Set<string>): string[] {
30 const out: string[] = []
31 for (const w of words) {
32 if (out.length === limit) break
33 const key = w.toLowerCase()
34 if (key.length > lower.length && key.startsWith(lower) && !skip.has(key)) out.push(w), skip.add(key)
35 }
36 return out
37}
38
39// ponytail: linear scan over ~25k words per keystroke, fast enough; switch to a sorted array + binary search if it ever lags.
40// Up to 3 work terms come first so "comm" shows "commit" before "community"; the rest is everyday English.
41// With 3 letters or fewer only 1 work term leads, so "the" still offers "they", "their", "there".
42export function complete(general: readonly string[], token: string, terms: readonly string[] = [], limit = 5): string[] {
43 if (token.length < 2 || !/^[A-Za-z][A-Za-z']*$/.test(token)) return []
44 const lower = token.toLowerCase()
45 const seen = new Set<string>()
46 const tech = matches(terms, lower, Math.min(token.length <= 3 ? 1 : 3, limit), seen)
47 return matchCase(token, [...tech, ...matches(general, lower, limit - tech.length, seen)])
48}
49
50// Keep the case the person started with: "Recom" -> "Recommend", "RECOM" -> "RECOMMEND".
51// Proper names ("TypeScript") keep their own case unless the person types in all caps.
52function matchCase(token: string, out: string[]): string[] {
53 if (token === token.toUpperCase()) return out.map(w => w.toUpperCase())
54 if (token.charAt(0) === token.charAt(0).toUpperCase()) return out.map(w => w.charAt(0).toUpperCase() + w.slice(1))
55 return out
56}
57
58// Every known word once, work terms first (in their own case), then everyday English: the order typo fixes are offered in.
59export function makeTypoPool(general: readonly string[], terms: readonly string[]): string[] {
60 const byKey = new Map<string, string>()
61 for (const w of [...terms, ...general]) if (!byKey.has(w.toLowerCase())) byKey.set(w.toLowerCase(), w)
62 return [...byKey.values()]
63}
64
65// Fewest single-letter edits (add, drop, change, or swap two neighbours) between a and b, or max + 1 once it is past max.
66function editDistance(a: string, b: string, max: number): number {
67 if (Math.abs(a.length - b.length) > max) return max + 1
68 let before: number[] = []
69 let prev = Array.from({ length: b.length + 1 }, (_, j) => j)
70 for (let i = 1; i <= a.length; i++) {
71 const cur = [i]
72 let best = i
73 for (let j = 1; j <= b.length; j++) {
74 let v = Math.min(prev[j]! + 1, cur[j - 1]! + 1, prev[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1))
75 if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, before[j - 2]! + 1)
76 cur[j] = v
77 best = Math.min(best, v)
78 }
79 if (best > max) return max + 1
80 before = prev
81 prev = cur
82 }
83 return prev[b.length]!
84}
85
86// "Did you mean" for a misspelt word, used only when nothing matches exactly.
87// The person is mid-word, so the typed text is compared with the start of each word (cut one letter shorter,
88// the same length, or one longer). One slip at most, the first letter taken as right, 4+ letters, and nothing
89// when the typed text is already a known word. Measured on 60 common typos: 58 caught, none shown while typing correctly.
90// ponytail: scans the whole pool per keystroke (~0.4 ms); index by first letter if it ever lags.
91export function fixTypos(pool: readonly string[], known: ReadonlySet<string>, token: string, limit = 5): string[] {
92 const lower = token.toLowerCase()
93 if (token.length < 4 || !/^[A-Za-z]+$/.test(token) || known.has(lower)) return []
94 const out: string[] = []
95 for (const w of pool) {
96 if (out.length === limit) break
97 const key = w.toLowerCase()
98 if (key[0] !== lower[0] || key === lower) continue
99 const cuts = [lower.length - 1, lower.length, lower.length + 1].filter(n => n > 0 && n <= key.length)
100 if (cuts.some(n => editDistance(lower, key.slice(0, n), 1) <= 1)) out.push(w)
101 }
102 return matchCase(token, out)
103}
104hooks/shortcuts.ts 55 lines1// Shortcuts: a short key ("ptg") the person turns into a phrase ("push to github") with Tab.
2// Pure logic only; register.ts keeps the map in $.store and wires the /shortcut command and the model's tool.
3
4export type Shortcuts = Record<string, string>
5
6const KEY = /^[A-Za-z][A-Za-z0-9]+$/
7
8// Returns an error message, or undefined when the pair can be saved.
9export function checkShortcut(key: string, text: string): string | undefined {
10 if (!KEY.test(key)) return `"${key}" can't be a shortcut: use 2+ letters or digits, starting with a letter (e.g. ptg).`
11 if (!text.trim()) return `Give the text "${key}" should turn into.`
12 if (text.length > 500) return 'Keep the text under 500 characters.'
13}
14
15// Shortcut phrases whose key starts with what is typed; an exact key comes first.
16export function matchShortcuts(map: Shortcuts, token: string): string[] {
17 const lower = token.toLowerCase()
18 return Object.keys(map)
19 .filter(k => k.startsWith(lower))
20 .sort((a, b) => Number(b === lower) - Number(a === lower) || a.localeCompare(b))
21 .map(k => map[k] as string)
22}
23
24export function listShortcuts(map: Shortcuts): string {
25 const keys = Object.keys(map).sort()
26 if (keys.length === 0) return 'No shortcuts yet. Add one with: /shortcut add ptg push to github'
27 return keys.map(k => `${k} -> ${map[k]}`).join('\n')
28}
29
30// One change to the map: what the /shortcut command and the model's tool both call.
31export function applyShortcut(map: Shortcuts, action: string, key = '', text = ''): { map: Shortcuts; message: string } {
32 const k = key.trim().toLowerCase()
33 const phrase = text.trim()
34 if (action === 'list') return { map, message: listShortcuts(map) }
35 if (action === 'add') {
36 const error = checkShortcut(k, phrase)
37 if (error) return { map, message: error }
38 const was = map[k]
39 return { map: { ...map, [k]: phrase }, message: was ? `Updated ${k}: ${was} -> ${phrase}` : `Added ${k} -> ${phrase}` }
40 }
41 if (action === 'remove') {
42 if (!(k in map)) return { map, message: `No shortcut named "${k}".` }
43 const rest = { ...map }
44 delete rest[k]
45 return { map: rest, message: `Removed ${k}.` }
46 }
47 return { map, message: 'Usage: /shortcut add <key> <text> | /shortcut remove <key> | /shortcut list' }
48}
49
50// "/shortcut add ptg push to github" -> applyShortcut(map, 'add', 'ptg', 'push to github')
51export function runShortcutCommand(map: Shortcuts, args: string): { map: Shortcuts; message: string } {
52 const [action = 'list', key = '', ...rest] = args.trim().split(/\s+/).filter(Boolean)
53 return applyShortcut(map, action.toLowerCase(), key, rest.join(' '))
54}
55