Status: Implemented; archived 2026-10-18.
Implementation decisions: startup seeds bet, slots, and balance only when
rooms.main["command.aliases"] is absent; aliases may target public or
room-admin commands, whose original authorization remains in force; room admins
manage aliases through /bot world alias add|remove|list; fixedArgs is
supported; player-defined aliases are out of scope; no aliases beyond the
starter set are seeded.
Let players use short commands for common long ones, for example:
| Alias | Expands to |
|---|---|
!bet <args> |
!casino roulette bet <args> |
!slots <args> |
!casino slots <args> |
!balance |
!casino balance |
Aliases are data, not code. Room admins add and remove them through the
world alias commands; changes take effect without a restart through the existing
prototypeGameConfig change-stream reload.
src/core/commands/ that rewrites the leading words of
a parsed command line before the existing longest-path routing.rooms.main["command.aliases"], in
prototypeGameConfig, validated on write and on load./bot ... and bang !... syntax. Both forms work for aliases.aliases, which are owner-prefixed synonyms
declared in code (for example casino roulette paths). They remain as they
are; the new mechanism is separate and operator-managed.| Constraint | Consequence |
|---|---|
| Real commands always win | If an alias name matches a registered command path or its first word, the registered command is routed and the alias is ignored. A feature can never be hidden or hijacked by data. |
| No privilege change | Aliases never carry permissions. Admin-only targets stay admin-only. |
| Single expansion | An alias target may not start with another alias. There is no recursion and no loop detection needed. |
| Fail closed | A structurally invalid config disables all aliases (logged once per revision). Unknown targets are rejected at write time; a target that later stops being registered makes only that alias unknown. |
| Privacy | Sensitive commands are never alias targets (code, whisperer, testcomms, door access-code paths), so alias resolution cannot bypass the parser’s existing argument redaction. |
| Static composition | The resolver is constructed in start.ts and given a read function over GameConfigRuntime; no feature imports it, and no plug-in is discovered dynamically. |
| Dependency direction | The resolver and its validation live in core/ with explicit guards (core does not use zod). The composition root connects it to the world config runtime. |
Stored at prototypeGameConfig.rooms.main["command.aliases"]:
{
"schemaVersion": 1,
"enabled": true,
"aliases": [
{ "name": "bet", "target": "casino roulette bet" },
{ "name": "slots", "target": "casino slots" },
{ "name": "balance", "target": "casino balance" },
{ "name": "spin", "target": "casino slots", "fixedArgs": "10" }
]
}
interface CommandAliasConfig {
readonly schemaVersion: 1;
readonly enabled: boolean;
readonly aliases: readonly CommandAlias[]; // max 64
}
interface CommandAlias {
/** One or two lowercase words, e.g. "bet" or "rb red". Same pattern as command paths. */
readonly name: string;
/** A command path that has a registered help descriptor. */
readonly target: string;
/** Optional words inserted before the player's arguments, max 64 chars. */
readonly fixedArgs?: string;
}
Validation rules (on admin write and on every load):
name and target match the existing command-path pattern
(^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*)*$), are at most 64 characters, and do not
contain the word help.name is unique, is not a registered command path, and its first word is not
the first word of any registered command path. This prevents !casino or
!door from being redefined and prevents ambiguous prefixes such as
!casino x.target must match a command path in the current help catalog. Targets on
the sensitive denylist are rejected.target must not start with an alias name.fixedArgs is plain words only: no newlines, no !, and no / prefix.Because plug-ins register commands at runtime, a valid alias can become invalid later, for example when a feature is disabled. The resolver therefore checks again at resolution time and treats an alias whose target is no longer registered as unknown (the normal “Unknown command” help reply).
prototypeGameConfig and not a new collectionbackup:<revision> history, and a change-stream watcher.prototypeGameConfig and the existing world-admin
authorization, revision, backup, and change-stream behavior.The key is seeded at startup with bet, slots, and balance only when absent.
Removing it disables aliases until the next startup, when the baseline is
inserted again.
sequenceDiagram
participant BC as BC message
participant Parser as BotCommandParser
participant Alias as CommandAliasResolver
participant Config as GameConfigRuntime
participant Handler as Feature command handler
BC->>Parser: "!bet red 10"
Parser->>Parser: syntax/source/sender checks (unchanged)
Parser->>Parser: help request? (unchanged)
Parser->>Parser: longest registered path match
alt registered command matched
Parser->>Handler: route as today
else no registered match
Parser->>Alias: resolve(["bet","red","10"])
Alias->>Config: current validated alias set
alt alias found and target registered
Alias-->>Parser: "casino roulette bet red 10"
Parser->>Parser: route rewritten line (no second alias pass)
Parser->>Handler: casino handler, same context except command line
else no alias
Parser-->>BC: private "Unknown command" help (unchanged)
end
end
Key points:
rb red beats rb), mirroring the parser.BotCommandContext gains an optional alias?: string field so handlers and
traces can see that an alias was used. Handlers do not need it to work.command_alias_resolved with the alias name and
resolved command path only. The argument summary is computed from the
resolved command, so the existing redaction applies./bot help and scoped help show, for each visible descriptor, a line such as
Shortcut: !bet listing the operator aliases that target it. The existing
visibility rules (public/admin, availability, location) decide whether the
alias is shown; an alias for an invisible command is never listed.!bet help resolves through the alias to casino roulette bet help, so
scoped help works for shortcuts too./bot world alias list
/bot world alias add spin casino slots fixedArgs=10
/bot world alias add "red bet" casino roulette bet
/bot world alias remove spin
Two-word alias names are quoted. The optional fixedArgs=<words> value is
inserted before the invoking player’s arguments. The dedicated commands validate
the live help catalog and write the same room-scoped key with a revision check;
generic world config set also validates that key.
flowchart LR
Root["start.ts"] --> Parser["BotCommandParser"]
Root --> Resolver["CommandAliasResolver (core)"]
Root --> Validator["command.aliases write validator"]
Resolver --> Read["read: () => validated alias set"]
Read --> Runtime["GameConfigRuntime.getForRoom(main, command.aliases)"]
Parser --> Resolver
Parser --> Help["CommandHelpCatalog / ScopedHelpService"]
Validator --> Help
Runtime --> DB[("prototypeGameConfig")]
src/core/commands/command-aliases.ts: types, guard-based parser
(parseCommandAliasConfig(unknown)), and CommandAliasResolver. The
resolver caches the parsed set per config revision.src/core/commands/bot-command-parser.ts: an optional resolver constructor
argument, the post-match fallback, and the alias context field.src/core/commands/command-help.ts: an optional alias lookup to render
shortcut lines.src/app/start.ts: wiring and the write validator branch.The resolver holds no timers or listeners; it reads the current runtime value on each lookup, so there is nothing to dispose in shutdown.
| Phase | Content | Gate |
|---|---|---|
| 1 | Config schema, seeding, resolver, parser fallback, write validator, and trace event | Unit tests; seeded test-room shortcuts |
| 2 | Help shortcut lines and !<alias> help |
Unit tests; manual help check |
| 3 | Room-admin alias add/remove/list commands and revision-safe writes | Unit tests |
fixedArgs insertion order; target
no longer registered becomes unknown; registered command beats alias; native
and bang sources; the rewritten line is not re-aliased.!bet help routes
to scoped help for the target.bet and slots aliases, run both from
inside and outside the casino locations, check that access behaves exactly
like the long form, then unset the key and confirm !bet is unknown.bet, slots, and balance only when the key is absent.fixedArgs; player-defined aliases are out of scope.