Skip to content

App Commands

App Commands is the official interface for plugins to expose commands to any host application — Telegram bot, Discord bot, CLI, web UI, or anything else.


The Problem Without App Commands

Without a standard interface, every host app needs to hardcode plugin integrations:

# ❌ Before: bot.py hardcoded for specific plugins
if message == "/semantic":
    plugin = sc._plugins.get("vector_search_plugin")
    result = plugin._search(user_id, query)   # calling internal methods!

This means: - Adding a new plugin requires editing the bot - Community plugins can't add commands - Different host apps implement differently


The Solution: app_commands

Plugins declare their commands. Host apps read and register them automatically:

# ✅ After: plugin declares its own commands
class VectorSearchPlugin(BasePlugin):
    app_commands = {
        "semantic": {
            "description": "Search memory by meaning",
            "usage":       "/semantic <query>",
            "handler":     "bot_cmd_semantic",
        }
    }

    async def bot_cmd_semantic(self, ctx: AppCommandContext) -> str:
        results = self._search(ctx.user_id, ctx.args_str)
        return format_results(results)
# Host app: zero plugin-specific code
commands = sc._plugins.get_all_app_commands()
for cmd_name, cmd_info in commands.items():
    app.register_command(cmd_name, ...)   # generic registration

app_commands Format

app_commands = {
    "command_name": {
        "description": "Short description shown in /help",  # required
        "usage":       "/command_name <arg>",               # optional
        "handler":     "method_name",                       # required — method in this class
        "args_hint":   "<query>",                           # optional
        "hidden":      False,                               # optional, default False
    }
}

Handler Signature

All command handlers receive an AppCommandContext:

async def my_handler(self, ctx: AppCommandContext) -> str:
    ...

AppCommandContext Fields

Field Type Description
command str Command name, e.g. "semantic"
user_id str User ID (string, platform-agnostic)
args list[str] Arguments as tokens
args_str str Arguments as single string
platform str "telegram" · "discord" · "cli" · "web"
raw Any Platform-specific object (e.g. Telegram Update)
sc Any SimpleContext instance
extra dict Additional data from host

Creating a Context (for host apps)

from simplecontext.plugins.base import AppCommandContext

ctx = AppCommandContext.create(
    command  = "semantic",
    user_id  = str(update.effective_user.id),
    args     = ["python", "error"],
    platform = "telegram",
    raw      = update,
    sc       = sc,
)

PluginLoader API

get_all_app_commands()

Aggregate all commands from all loaded plugins:

all_commands = sc._plugins.get_all_app_commands()
# → {
#     "semantic": {
#         "description": "Search memory by meaning",
#         "handler":     "bot_cmd_semantic",
#         "plugin":      <VectorSearchPlugin instance>,
#     }
# }

Conflict detection: if two plugins declare the same command name, a warning is logged and the first one wins.

fire_app_command(ctx)

Execute a command — finds the right plugin and calls its handler:

result = await sc._plugins.fire_app_command(ctx)
# → str response or None

Execution order: 1. Find plugin that owns this command via get_all_app_commands() 2. Call the dedicated handler method 3. If no dedicated handler, fall back to on_app_command() 4. Return string response or None

set_app_info(info)

Inject platform metadata into all loaded plugins:

sc._plugins.set_app_info({
    "platform": "telegram",
    "version":  "1.2.0",
    "bot_name": "MyBot",
})

Plugins can read this in self.app_info:

def setup(self):
    platform = self.app_info.get("platform", "unknown")
    print(f"Running on: {platform}")

get_app_commands() on BasePlugin

Validates that handler methods exist before returning:

cmds = plugin.get_app_commands()
# Only returns commands where self.<handler_name> exists

on_app_command() Fallback

Plugins can handle all their commands in one method instead of separate handlers:

class MyPlugin(BasePlugin):
    app_commands = {
        "foo": {"description": "...", "handler": "on_app_command"},
        "bar": {"description": "...", "handler": "on_app_command"},
    }

    async def on_app_command(self, ctx: AppCommandContext) -> str | None:
        if ctx.command == "foo":
            return "foo response"
        if ctx.command == "bar":
            return "bar response"
        return None   # pass-through

Platform Detection

Use ctx.platform to provide platform-specific behavior:

async def handle_stats(self, ctx: AppCommandContext) -> str:
    total = self.state.get(f"messages:{ctx.user_id}", 0)

    if ctx.platform == "telegram":
        return f"📊 *Messages sent:* `{total}`"   # Markdown
    elif ctx.platform == "discord":
        return f"📊 **Messages sent:** `{total}`"  # Discord Markdown
    else:
        return f"Messages sent: {total}"            # plain text

Full Example

from simplecontext.plugins.base import BasePlugin, AppCommandContext

class StatsPlugin(BasePlugin):
    name        = "stats_plugin"
    version     = "1.0.0"
    description = "Track and display usage statistics."

    app_commands = {
        "stats": {
            "description": "Show your usage statistics",
            "usage":       "/stats",
            "handler":     "handle_stats",
        },
        "reset": {
            "description": "Reset your usage statistics",
            "usage":       "/reset",
            "handler":     "handle_reset",
            "hidden":      True,   # won't appear in /help
        },
    }

    def on_message_saved(self, user_id, role, content, tags, metadata):
        if role == "user":
            self.state.increment(f"msg:{user_id}")

    async def handle_stats(self, ctx: AppCommandContext) -> str:
        count = self.state.get(f"msg:{ctx.user_id}", 0)
        return f"📊 You've sent {count} messages."

    async def handle_reset(self, ctx: AppCommandContext) -> str:
        self.state.set(f"msg:{ctx.user_id}", 0)
        return "✅ Stats reset."

Drop this in plugins/ and SimpleContext-Bot will auto-register /stats and /reset as Telegram commands on next start.