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:
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:
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:
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:
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.