UNPKG

levtor

Version:

Levtor — a smart, fuzzy, AI-powered chat command handler and guardian for wallets and AI integration.

163 lines (131 loc) 4.69 kB
import { parseArgs, fuzzyMatch } from './utils'; type PositionalArgs = string[]; type NamedArgs = Record<string, string | boolean>; type FormatFn<TCtx> = ( rawOutput: string, context: { command: string; args: PositionalArgs; named: NamedArgs; ctx?: TCtx; meta?: Record<string, unknown>; } ) => Promise<string>; type PreprocessFn<TCtx> = ( rawMessage: string, ctx?: TCtx ) => Promise<{ message: string; meta?: Record<string, unknown> }>; type CommandHandlerFn<TDeps, TCtx> = ( args: PositionalArgs, named: NamedArgs, deps: TDeps, ctx?: TCtx, meta?: Record<string, unknown> ) => Promise<string>; type LifecycleStage = 'before' | 'after' | 'error'; type HookFn<TCtx> = ( command: string, payload: unknown, ctx?: TCtx ) => Promise<void>; interface CommandOptions { aliases?: string[]; cacheTtlMs?: number; debounceMs?: number; } interface RegisteredCommand<TDeps, TCtx> extends CommandOptions { handler: CommandHandlerFn<TDeps, TCtx>; aliases: string[]; cacheTtlMs?: number; debounceMs?: number; } class CommandHandler<TDeps, TCtx = object> { private commands = new Map<string, RegisteredCommand<TDeps, TCtx>>(); private preprocessors: PreprocessFn<TCtx>[] = []; private hooks: Partial<Record<LifecycleStage, HookFn<TCtx>[]>> = {}; private cache = new Map<string, { value: string; expires: number }>(); private lastRun = new Map<string, number>(); constructor( private deps: TDeps, private formatWithAI?: FormatFn<TCtx> ) {} register(command: string, handler: CommandHandlerFn<TDeps, TCtx>, options: CommandOptions = {}) { this.commands.set(command.toLowerCase(), { handler, aliases: options.aliases || [], cacheTtlMs: options.cacheTtlMs, debounceMs: options.debounceMs, }); } usePreprocessor(fn: PreprocessFn<TCtx>) { this.preprocessors.push(fn); } on(stage: LifecycleStage, fn: HookFn<TCtx>) { this.hooks[stage] = this.hooks[stage] || []; this.hooks[stage].push(fn); } private async emit(stage: LifecycleStage, cmd: string, payload: unknown, ctx?: TCtx) { for (const fn of this.hooks[stage] || []) { await fn(cmd, payload, ctx); } } private async runPreprocessors(raw: string, ctx?: TCtx) { let current = raw; const meta: Record<string, unknown> = {}; for (const fn of this.preprocessors) { const result = await fn(current, ctx); current = result.message; Object.assign(meta, result.meta); } return { message: current, meta }; } async handle(raw: string, ctx?: TCtx): Promise<string> { try { const { message, meta } = await this.runPreprocessors(raw, ctx); if (!message.startsWith('!')) return '❌ Must start with `!`'; const input = message.slice(1).trim(); const [rawCmd, ...rest] = input.split(/\s+/); if (!rawCmd) return '❓ Unknown command: ""'; const all = [...this.commands.entries()].flatMap(([name, cmd]) => [name, ...cmd.aliases].map(alias => ({ alias, command: name })) ); const match = fuzzyMatch(rawCmd.toLowerCase(), all.map(a => a.alias)); if (!match) return `❓ Unknown command: "${rawCmd}"`; const found = all.find(c => c.alias === match); if (!found) return `❓ Unknown command: "${rawCmd}"`; const { command } = found; const cmdObj = this.commands.get(command); if (!cmdObj) return `❓ Unknown command: "${rawCmd}"`; const { handler, cacheTtlMs, debounceMs } = cmdObj; const now = Date.now(); const cacheKey = `${command}|${rest.join(' ')}`; if (cacheTtlMs) { const entry = this.cache.get(cacheKey); if (entry && entry.expires > now) return entry.value; } if (debounceMs) { const last = this.lastRun.get(cacheKey) || 0; if (now - last < debounceMs) return '🕒 Please wait...'; this.lastRun.set(cacheKey, now); } const { positional, named } = parseArgs(rest.join(' ')); await this.emit('before', command, { args: positional, named, meta }, ctx); const result = await handler(positional, named, this.deps, ctx, meta); await this.emit('after', command, result, ctx); const formatted = this.formatWithAI ? await this.formatWithAI(result, { command, args: positional, named, ctx, meta }) : result; if (cacheTtlMs) { this.cache.set(cacheKey, { value: formatted, expires: now + cacheTtlMs, }); } return formatted; } catch (err) { await this.emit('error', 'global', err, ctx); return '⚠️ An error occurred.'; } } } export { CommandHandler };