Commands in Plugins
Command Basics
Commands are registered with addCmd at module-load time — not inside
init(). The standard pattern is:
- Create
commands.tsin your plugin folder and calladdCmdthere. - Add
import "./commands.ts"inindex.tsso registrations happen when the
plugin is loaded.
When a player types input, the command parser checks it against every
registered pattern in turn and calls the first matching exec function.
addCmd Reference
// In a plugin inside src/plugins/:
import { addCmd } from "../../services/commands/cmdParser.ts";
import type { IUrsamuSDK } from "../../@types/UrsamuSDK.ts";
// From a child game (outside src/):
import { addCmd } from "jsr:@ursamu/ursamu";
import type { IUrsamuSDK } from "jsr:@ursamu/ursamu";
addCmd({
name: "+myplugin", // unique identifier — shown in logs
pattern: /^\+myplugin(?:\/(\S+))?\s*(.*)/i,
lock: "connected", // lock expression
exec: async (u: IUrsamuSDK) => {
// your logic here
},
});
name
A unique string identifier. By convention:
- Prefix admin/world-manipulation commands with
@(e.g.,@dig,@name) - Prefix player feature commands with
+(e.g.,+bb,+event,+job)
pattern
A RegExp matched against the full input string. Capture groups become
u.cmd.args[0], u.cmd.args[1], etc.
The standard switch pattern for +cmd/switch <args>:
pattern: /^\+cmd(?:\/(\S+))?\s*(.*)/i
// ^^^^^^ ^^
// args[0]: sw args[1]: rest
lock
Controls who can run the command. The player must satisfy the lock expression.
| Value | Meaning |
|---|---|
"" |
No restriction (login-screen commands only) |
"connected" |
Player must be logged in |
"connected builder+" |
Connected + builder flag or higher |
"connected admin+" |
Connected + admin flag or higher |
"connected wizard" |
Connected + wizard flag |
exec
The function called on match. Receives u: IUrsamuSDK — the same SDK object
available to sandbox scripts.
The IUrsamuSDK Object
Identity
u.me.id // actor's DB ID
u.me.name // actor's name (string)
u.me.flags // Set<string> of flags
u.me.location // current room ID
u.socketId // WebSocket connection ID
Command input
u.cmd.args[0] // first capture group from pattern (often the switch)
u.cmd.args[1] // second capture group (often the rest of input)
Messaging
u.send("Message to caller");
u.broadcast("Message to everyone in the room");
Database
// Search the main objects DB
const players = await u.db.search({ flags: /connected/i });
// Create an object
const obj = await u.db.create({
name: "Widget", flags: new Set(["thing"]),
location: u.me.location, state: {}, contents: [],
});
// Modify a field
await u.db.modify(obj.id, "$set", { data: { description: "Updated" } });
// Destroy an object
await u.db.destroy(obj.id);
Target resolution
const tar = await u.util.target(u.me, u.cmd.args[1]);
if (!tar) { u.send("I don't see that here."); return; }
u.util.target searches the room contents and the actor’s inventory.
Permissions
const isAdmin = u.me.flags.has("admin") ||
u.me.flags.has("wizard") ||
u.me.flags.has("superuser");
if (!isAdmin) { u.send("Permission denied."); return; }
Utility helpers
u.util.stripSubs(str) // strip MUSH color codes and ANSI escapes
u.util.ljust(str, width) // left-pad to width
u.util.rjust(str, width) // right-pad to width
u.util.center(str, width) // center in width
u.mail, u.attr, u.eval, u.forceAs
These are also available in the u object. See the
SDK Cookbook for full examples:
| Method | Description |
|---|---|
u.attr.get(id, name) |
Read a soft-coded &ATTR value (returns string | null) |
u.eval(targetId, attr, args?) |
Evaluate &ATTR and return output as string |
u.forceAs(targetId, cmd) |
Execute command as another object (wizard/admin only) |
u.sys.gameTime() |
Read in-game calendar (IGameTime) |
u.sys.setGameTime(t) |
Set in-game calendar |
u.chan.history(name, limit?) |
Recent channel messages |
u.mail.send/read/delete/modify |
Mail system (sandbox scripts only) |
Pattern Reference
Simple command, no arguments
pattern: /^inventory$/i
// "inventory" → u.cmd.args: []
Command with one argument
pattern: /^look\s+(.*)/i
// "look north" → u.cmd.args[0]: "north"
Command with switch and argument
pattern: /^\+job(?:\/(\S+))?\s*(.*)/i
// "+job/view 5" → args[0]: "view", args[1]: "5"
// "+job" → args[0]: "", args[1]: ""
Command with two arguments separated by =
pattern: /^\+event\/edit\s+(\d+)\/(\S+)=(.+)/i
// "+event/edit 3/title=New Title" → args[0]: "3", args[1]: "title", args[2]: "New Title"
Switch Handling
The standard idiom for switch-based commands:
addCmd({
name: "+ticket",
pattern: /^\+ticket(?:\/(\S+))?\s*(.*)/i,
lock: "connected",
exec: async (u: IUrsamuSDK) => {
const sw = (u.cmd.args[0] || "").toLowerCase().trim();
const arg = (u.cmd.args[1] || "").trim();
if (!sw || sw === "list") {
// +ticket or +ticket/list
u.send("Listing tickets ...");
return;
}
if (sw === "view") {
// +ticket/view <num>
const num = parseInt(arg, 10);
if (isNaN(num)) { u.send("Usage: +ticket/view <#>"); return; }
u.send(`Viewing ticket #${num}`);
return;
}
if (sw === "create") {
// +ticket/create <title>
if (!arg) { u.send("Usage: +ticket/create <title>"); return; }
u.send(`Creating ticket: ${arg}`);
return;
}
u.send(`Unknown switch "/${sw}". Try: +ticket, +ticket/view, +ticket/create`);
},
});
Examples
Minimal broadcast command
addCmd({
name: "+shout",
pattern: /^\+shout\s+(.*)/i,
lock: "connected",
exec: (u: IUrsamuSDK) => {
const msg = u.cmd.args[0]?.trim();
if (!msg) { u.send("Shout what?"); return; }
u.send(`You shout: "${msg}"`);
u.broadcast(`${u.me.name} shouts: "${msg}"`);
},
});
Staff-only admin command
addCmd({
name: "@wipe",
pattern: /^@wipe\s+(.*)/i,
lock: "connected admin+",
exec: async (u: IUrsamuSDK) => {
const tar = await u.util.target(u.me, u.cmd.args[0]);
if (!tar) { u.send("I don't see that."); return; }
if (tar.flags.has("superuser")) {
u.send("You cannot wipe a superuser."); return;
}
await u.db.destroy(tar.id);
u.send(`Destroyed ${tar.name}.`);
},
});
Command that reads and writes a custom DBO
// Plugin in src/plugins/ — use relative imports
import { addCmd } from "../../services/commands/cmdParser.ts";
import type { IUrsamuSDK } from "../../@types/UrsamuSDK.ts";
import { myRecords } from "./db.ts";
addCmd({
name: "+record",
pattern: /^\+record(?:\/(\S+))?\s*(.*)/i,
lock: "connected",
exec: async (u: IUrsamuSDK) => {
const sw = (u.cmd.args[0] || "").toLowerCase();
const arg = (u.cmd.args[1] || "").trim();
if (sw === "list") {
const all = await myRecords.find({ playerId: u.me.id });
if (!all.length) { u.send("No records."); return; }
for (const r of all) u.send(`[${r.id.slice(-4)}] ${r.text}`);
return;
}
if (!arg) { u.send("Usage: +record <text>"); return; }
await myRecords.create({
id: crypto.randomUUID(),
playerId: u.me.id,
text: arg,
createdAt: Date.now(),
});
u.send("Saved.");
},
});