Skip to content

Commit d98668d

Browse files
DevRohit06claude
andcommitted
fix(serve): fix slash commands never registering with Discord
Two root causes: - Commands were added to the tree's global scope but only synced per-guild, which uploads an empty list. Added copy_global_to() before each guild sync so commands are actually sent to Discord. - Handler functions used **kwargs, which discord.py can't inspect for parameter discovery. Commands like /paw registered without their options. Now uses __signature__ injection so discord.py sees proper typed parameters. Also surfaces registration errors via the JSONL error event instead of silently dropping them into stderr at DEBUG level. Bumps to v0.5.3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5322a0a commit d98668d

2 files changed

Lines changed: 50 additions & 41 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "discord-cli-agent"
7-
version = "0.5.2"
7+
version = "0.5.3"
88
description = "Discord CLI for AI agents"
99
readme = "README.md"
1010
license = "MIT"

src/discli/commands/serve.py

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ async def on_ready():
106106
asyncio.create_task(_stdin_reader())
107107
# Sync slash commands (Discord API call, can take seconds)
108108
if slash_defs:
109-
await _register_slash_commands()
109+
try:
110+
await _register_slash_commands()
111+
except Exception as e:
112+
emit({"event": "error", "message": f"Slash command registration failed: {e}"})
110113

111114
@client.event
112115
async def on_message(message):
@@ -239,54 +242,60 @@ async def on_member_remove(member):
239242
# ── Slash Commands ──────────────────────────────────────────────
240243

241244
async def _register_slash_commands():
245+
import inspect
246+
247+
_type_map = {"string": str, "integer": int, "number": float, "boolean": bool}
248+
249+
def _make_slash_callback(param_defs):
250+
"""Create a callback whose __signature__ exposes params to discord.py."""
251+
async def _callback(interaction: discord.Interaction, **kwargs):
252+
cmd_name = interaction.command.name
253+
itk = str(uuid.uuid4())
254+
interactions[itk] = interaction
255+
await interaction.response.defer(thinking=True)
256+
emit({
257+
"event": "slash_command",
258+
"command": cmd_name,
259+
"args": {k: str(v) for k, v in kwargs.items() if v is not None},
260+
"channel_id": str(interaction.channel_id),
261+
"user": str(interaction.user),
262+
"user_id": str(interaction.user.id),
263+
"guild_id": str(interaction.guild_id) if interaction.guild_id else None,
264+
"interaction_token": itk,
265+
})
266+
267+
# Build a proper signature so discord.py registers slash options
268+
sig_params = [
269+
inspect.Parameter("interaction", inspect.Parameter.POSITIONAL_OR_KEYWORD,
270+
annotation=discord.Interaction),
271+
]
272+
for p in param_defs:
273+
annotation = _type_map.get(p.get("type", "string"), str)
274+
required = p.get("required", True)
275+
default = inspect.Parameter.empty if required else None
276+
sig_params.append(
277+
inspect.Parameter(p["name"], inspect.Parameter.POSITIONAL_OR_KEYWORD,
278+
annotation=annotation, default=default),
279+
)
280+
_callback.__signature__ = inspect.Signature(sig_params)
281+
return _callback
282+
242283
for cmd_def in slash_defs:
243284
name = cmd_def["name"]
244285
desc = cmd_def.get("description", name)
245286
params = cmd_def.get("params", [])
246287

288+
callback = _make_slash_callback(params)
247289
if params:
248-
# Command with a single string parameter
249-
param = params[0]
250-
251-
@tree.command(name=name, description=desc)
252-
@app_commands.describe(**{param["name"]: param.get("description", param["name"])})
253-
async def slash_handler(interaction: discord.Interaction, **kwargs):
254-
cmd_name = interaction.command.name
255-
itk = str(uuid.uuid4())
256-
interactions[itk] = interaction
257-
await interaction.response.defer(thinking=True)
258-
emit({
259-
"event": "slash_command",
260-
"command": cmd_name,
261-
"args": {k: str(v) for k, v in kwargs.items()},
262-
"channel_id": str(interaction.channel_id),
263-
"user": str(interaction.user),
264-
"user_id": str(interaction.user.id),
265-
"guild_id": str(interaction.guild_id) if interaction.guild_id else None,
266-
"interaction_token": itk,
267-
})
268-
else:
269-
@tree.command(name=name, description=desc)
270-
async def slash_handler_no_args(interaction: discord.Interaction):
271-
cmd_name = interaction.command.name
272-
itk = str(uuid.uuid4())
273-
interactions[itk] = interaction
274-
await interaction.response.defer(thinking=True)
275-
emit({
276-
"event": "slash_command",
277-
"command": cmd_name,
278-
"args": {},
279-
"channel_id": str(interaction.channel_id),
280-
"user": str(interaction.user),
281-
"user_id": str(interaction.user.id),
282-
"guild_id": str(interaction.guild_id) if interaction.guild_id else None,
283-
"interaction_token": itk,
284-
})
285-
286-
# Sync per guild for instant registration (global sync can take hours)
290+
descriptions = {p["name"]: p.get("description", p["name"]) for p in params}
291+
callback = app_commands.describe(**descriptions)(callback)
292+
tree.command(name=name, description=desc)(callback)
293+
294+
# Copy global commands to each guild for instant availability, then sync
287295
synced_guilds = 0
288296
for guild in client.guilds:
289297
try:
298+
tree.copy_global_to(guild=guild)
290299
await tree.sync(guild=guild)
291300
synced_guilds += 1
292301
except Exception as e:

0 commit comments

Comments
 (0)