cmdk-engine
v0.3.0
Published
The smart command palette engine for React. Built on cmdk. Auto-discover routes, fuzzy search with synonyms, RBAC filtering, frecency ranking, CLI tooling.
Maintainers
Readme
cmdk-engine
The smart command palette engine for React. Built on cmdk. Auto-discover routes, fuzzy search with synonyms, RBAC filtering, frecency ranking, CLI tooling — all in < 5KB.
Why cmdk-engine?
cmdk gives you beautiful, accessible command menu primitives. But building a production command palette requires more:
| Feature | cmdk | cmdk-engine | |---------|------|-------------| | Composable UI components | Yes | Yes (via cmdk adapter) | | Route auto-discovery | No | Yes — CLI scanner + runtime adapters | | RBAC / permission filtering | No | Yes — any/all modes | | Frecency ranking | No | Yes — exponential decay algorithm | | Keyword synonyms | No | Yes — bidirectional, ranked below direct matches | | Smart route exclusion | No | Yes — auth, error, dynamic routes auto-filtered | | Deterministic sorting | Broken (#264, #375) | Yes — frecency > priority > alphabetical | | First item auto-select | Broken (#280) | Yes — auto-selects on every result update | | Dynamic content updates | Broken (#267) | Yes — reactive pub/sub registry | | CLI tooling | No | Yes — scan, init, validate | | Framework-agnostic core | No | Yes — zero runtime deps |
cmdk-engine owns all filtering (shouldFilter={false}), solving the sorting and selection bugs in cmdk while keeping its composable UI primitives.
Installation
Library (for React projects)
# npm
npm install cmdk-engine cmdk
# bun
bun add cmdk-engine cmdk
# pnpm
pnpm add cmdk-engine cmdk
# yarn
yarn add cmdk-engine cmdk
cmdkandreactare peer dependencies.
Quick Start
1. Wrap your app with the provider
import { CommandEngineProvider } from 'cmdk-engine/react'
function App() {
return (
<CommandEngineProvider
config={{
synonyms: {
billing: ['money', 'payment', 'credits'],
settings: ['preferences', 'config', 'options'],
},
}}
>
<YourApp />
</CommandEngineProvider>
)
}2. Register commands
import { useCommandRegister } from 'cmdk-engine/react'
import { CreditCard } from 'lucide-react'
function BillingPage() {
useCommandRegister([
{
id: 'billing-overview',
label: 'Billing Overview',
href: '/billing/overview',
keywords: ['balance', 'credits'],
group: 'Billing',
icon: <CreditCard size={16} />, // React components, strings, or emoji
},
])
return <div>...</div>
}3. Use the pre-wired cmdk adapter
import { CommandPalette } from 'cmdk-engine/adapters/cmdk'
function CommandMenu() {
return (
<CommandPalette
dialog
placeholder="Type a command or search..."
onSelect={(item) => {
if (item.href) navigate(item.href)
if (item.action) item.action(item)
}}
/>
)
}Or use config.onSelect on the provider to handle all selections in one place:
<CommandEngineProvider
config={{
onSelect: (item) => {
if (item.href) navigate(item.href)
if (item.action) item.action(item)
},
}}
>4. Or build your own UI with hooks
import { useCommandPalette } from 'cmdk-engine/react'
function CustomCommandMenu() {
const { search, setSearch, groupedResults, isOpen, toggle, select } =
useCommandPalette()
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
{groupedResults.map(({ group, items }) => (
<div key={group.id}>
<h3>{group.label}</h3>
{items.map(({ item }) => (
<button key={item.id} onClick={() => select(item)}>
{item.icon} {item.label}
</button>
))}
</div>
))}
</div>
)
}
select()records frecency, runsonSelect/action/href, and closes the palette — all in one call.
React Router Integration
Auto-discover routes from your React Router config:
import { scanRoutes } from 'cmdk-engine/adapters/react-router'
import { useCommandRegister } from 'cmdk-engine/react'
const commands = scanRoutes(routeConfig)
function App() {
useCommandRegister(commands)
return <RouterProvider router={router} />
}Smart defaults
The scanner automatically:
- Excludes auth routes —
/login,/signup,/forgot-password,/oauth/callback, etc. - Excludes error pages —
/404,/500,/error,/not-found - Skips dynamic routes —
/users/:id,/billing/:uuid(can't navigate without a real ID) - Derives labels from the path —
/billing/overview→ "Overview" - Derives groups from the first segment —
/billing/overview→ group "Billing"
Scanner options
const commands = scanRoutes(routeConfig, {
exclude: ['/admin/*', /^\/debug\//, '/internal'], // string, glob, or regex
noDefaultExclude: false, // set true to skip default auth/error exclusion
includeDynamic: false, // set true to include :id routes
})Route metadata
Enrich routes with metadata using the handle convention:
{
path: '/billing/overview',
handle: {
command: {
label: 'Billing Dashboard',
keywords: ['money', 'payment'],
group: 'Billing',
icon: <CreditCard size={16} />,
priority: 10,
}
},
element: <BillingOverview />,
}Routes with handle.command are always included, even if they have dynamic segments. The scanner also falls back to route.title and route.icon if handle.command doesn't define them.
RBAC / Access Control
Filter commands based on user permissions:
import { createSimpleAccessProvider } from 'cmdk-engine'
<CommandEngineProvider
config={{
accessControl: createSimpleAccessProvider(['admin.view', 'billing.read']),
accessCheckMode: 'any', // user needs ANY listed permission
}}
>Commands with permissions: ['admin.view'] will only show for users who have that permission.
Frecency Ranking
Commands you use frequently and recently appear higher in results. No configuration needed — it uses localStorage by default. When you use select(), frecency is recorded automatically.
The algorithm uses exponential decay with a configurable half-life:
score = count * 2^(-timeSinceLastUse / halfLife)Recent commands
Show a "Recent" group at the top of the palette when the search is empty:
<CommandEngineProvider
config={{
frecency: {
showRecent: true, // inject "Recent" group when search is empty
recentCount: 5, // number of recent items (default: 5)
recentLabel: 'Recent', // group label (default: "Recent")
},
}}
>CLI Tool
Auto-discover routes and generate sitemaps for your command palette.
Setup
# Initialize config
npx cmdk-engine init
# Scan routes
npx cmdk-engine scan
# Validate config
npx cmdk-engine validateConfig file
// cmdk-engine.config.ts
import { defineConfig } from 'cmdk-engine'
export default defineConfig({
framework: 'react-router', // or 'nextjs-app', 'nextjs-pages'
routesDir: './src/routes',
output: './src/generated/command-routes.json',
overrides: {
'/billing': { keywords: ['money', 'payment'], group: 'Billing' },
},
exclude: ['/404', '/500', '/_*'],
synonyms: {
billing: ['money', 'payment', 'credits'],
},
})Pre-commit hook
{
"husky": {
"hooks": {
"pre-commit": "npx cmdk-engine scan && git add src/generated/command-routes.json"
}
}
}GitHub Actions
- run: npx cmdk-engine scan
- run: npx cmdk-engine validateArchitecture
Route Config ─→ Route Adapter ─→ Command Registry ─→ Keyword Engine
│
├─→ Access Control Filter
│
├─→ Search Engine (fuzzy / match-sorter)
│
└─→ Frecency Ranking
│
▼
Headless API / Hooks
│
▼
UI Adapter (cmdk)Package Entry Points
| Import | Size | Purpose |
|--------|------|---------|
| cmdk-engine | ~4KB | Core engine (types, registry, search, keywords, access control, frecency) |
| cmdk-engine/react | ~2KB | React hooks (provider, useCommandPalette, useCommandRegister) |
| cmdk-engine/adapters/cmdk | ~1KB | Pre-wired cmdk components |
| cmdk-engine/adapters/react-router | ~1KB | React Router v6/v7 route scanner |
| cmdk-engine/search/match-sorter | ~1KB | Optional match-sorter search backend |
All entry points are tree-shakeable. The core has zero runtime dependencies.
API Reference
Core
import {
createRegistry, // Command store (pub/sub, useSyncExternalStore compatible)
createFuzzySearch, // Built-in lightweight fuzzy search
createKeywordEngine, // Synonym expansion + user aliases
createAccessFilter, // RBAC filter (any/all modes)
createSimpleAccessProvider, // Permission provider from array/Set
createFrecencyEngine, // Frecency ranking with exponential decay
createGroupManager, // Command group management
defineConfig, // Typed config helper for CLI
} from 'cmdk-engine'React
import {
CommandEngineProvider, // Context provider
useCommandPalette, // Main hook: search + filter + rank
useCommandRegister, // Register commands from components
useFrecency, // Direct frecency access
} from 'cmdk-engine/react'Adapters
import { CommandPalette, useCommandPaletteShortcut } from 'cmdk-engine/adapters/cmdk'
import { scanRoutes } from 'cmdk-engine/adapters/react-router'Key hook return values
const {
search, // Current query
setSearch, // Update query
results, // ScoredItem[] (flat)
flatResults, // Same as results
groupedResults, // GroupedResult[] — results grouped by group
groups, // CommandGroup[] — active groups
isOpen, // Palette visibility
open, close, toggle,
select, // Select a command (records frecency + runs handler + closes)
recordUsage, // Record frecency manually
} = useCommandPalette()Type Safety
All types are exported and fully documented:
import type {
CommandItem,
CommandRegistry,
SearchEngine,
ScoredItem,
GroupedResult,
GroupedResults,
AccessControlProvider,
FrecencyOptions,
RecentCommandsConfig,
CommandGroup,
SynonymMap,
RouteCommandMeta,
CmdkEngineConfig,
CommandEngineConfig,
CommandPaletteState,
} from 'cmdk-engine'cmdk Issues We Solve
| Issue | Description | How We Fix It | |-------|-------------|---------------| | #264 | Sort not restored after clearing search | We own filtering; restore original order when query is empty | | #280 | First item not selected with dynamic content | Auto-select first item after each render cycle | | #375 | Non-deterministic sorting | Deterministic: frecency → priority → alphabetical | | #374 | Scroll position jump on filter | We control the result list; reset scroll on search change | | #267 | Items not updating on async changes | Reactive pub/sub registry; items update immediately |
Contributing
See CONTRIBUTING.md for guidelines.
License
If you find cmdk-engine useful, please consider giving it a star on GitHub. It helps others discover the project.
