feat(core): add command, intent, and routing primitives
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import type { CommandContext, CommandDefinition, CommandResult } from './types.js';
|
||||
|
||||
const MAX_INPUT_LENGTH = 2000;
|
||||
|
||||
export class CommandRegistry {
|
||||
private commands = new Map<string, CommandDefinition>();
|
||||
private aliasToCommand = new Map<string, string>();
|
||||
|
||||
register(def: CommandDefinition): void {
|
||||
const canonicalName = this.normalizeName(def.name);
|
||||
if (!canonicalName) {
|
||||
throw new Error('Command name is required');
|
||||
}
|
||||
if (this.commands.has(canonicalName)) {
|
||||
throw new Error(`Command already registered: ${canonicalName}`);
|
||||
}
|
||||
|
||||
this.commands.set(canonicalName, { ...def, name: canonicalName });
|
||||
|
||||
for (const alias of def.aliases ?? []) {
|
||||
const canonicalAlias = this.normalizeName(alias);
|
||||
if (!canonicalAlias) {
|
||||
continue;
|
||||
}
|
||||
if (canonicalAlias === canonicalName || this.aliasToCommand.has(canonicalAlias) || this.commands.has(canonicalAlias)) {
|
||||
throw new Error(`Command alias already registered: ${canonicalAlias}`);
|
||||
}
|
||||
this.aliasToCommand.set(canonicalAlias, canonicalName);
|
||||
}
|
||||
}
|
||||
|
||||
get(nameOrAlias: string): CommandDefinition | undefined {
|
||||
const normalized = this.normalizeName(nameOrAlias);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
const canonicalName = this.aliasToCommand.get(normalized) ?? normalized;
|
||||
return this.commands.get(canonicalName);
|
||||
}
|
||||
|
||||
list(): CommandDefinition[] {
|
||||
return Array.from(this.commands.values());
|
||||
}
|
||||
|
||||
isCommand(input: string): boolean {
|
||||
return this.parse(input) !== null;
|
||||
}
|
||||
|
||||
parse(input: string): { name: string; args: string[] } | null {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed.startsWith('/') || trimmed.length > MAX_INPUT_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutSlash = trimmed.slice(1).trim();
|
||||
if (!withoutSlash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [rawName, ...rest] = withoutSlash.split(/\s+/);
|
||||
const name = this.normalizeName(rawName);
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
args: rest,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(input: string, ctx: CommandContext): Promise<CommandResult> {
|
||||
const parsed = this.parse(input);
|
||||
if (!parsed) {
|
||||
return { handled: false, text: '' };
|
||||
}
|
||||
|
||||
const command = this.get(parsed.name);
|
||||
if (!command) {
|
||||
return { handled: false, text: '' };
|
||||
}
|
||||
|
||||
try {
|
||||
return await command.execute(parsed.args, ctx);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown command error';
|
||||
return {
|
||||
handled: true,
|
||||
text: `Command failed: ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeName(value: string): string {
|
||||
return value.trim().replace(/^\//, '').toLowerCase();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user