feat: discover Claude Code skills + Telegram command name normalization#197
Open
americodias wants to merge 1 commit intoRichardAtCT:mainfrom
Open
feat: discover Claude Code skills + Telegram command name normalization#197americodias wants to merge 1 commit intoRichardAtCT:mainfrom
americodias wants to merge 1 commit intoRichardAtCT:mainfrom
Conversation
Discovers Claude Code skills from project, user, and plugin locations:
{project_dir}/.claude/skills/<skill>/SKILL.md (project)
~/.claude/skills/<skill>/SKILL.md (user)
~/.claude/plugins/marketplaces/<m>/{plugins,external_plugins}/<p>/skills/<s>/SKILL.md
Project takes precedence, then user, then plugin. Discovered skills are
exposed as Telegram bot commands so they appear in the command menu and
can be invoked with /<skill-name>.
Telegram's Bot API only allows [a-z0-9_] in command names, so dashed
skill names are normalized for the menu (git-activity -> git_activity)
and rewritten back to the original dashed form before forwarding to
Claude's skill dispatcher (which matches the raw frontmatter name).
Skips skills whose frontmatter declares 'user-invokable: false' so
non-user-facing skills (e.g. agent-only ones) don't pollute the menu.
Skills are re-scanned on /new so newly added skills appear without a
bot restart.
Includes unit tests covering the multi-path discovery, precedence,
shadowing, normalization, command rewrite, and user-invokable filter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Discovers Claude Code skills from the standard on-disk locations and surfaces them as Telegram bot commands so they appear in the command menu and can be invoked with
/<skill-name>. Also handles the dash-to-underscore normalization Telegram requires, transparently rewriting back before forwarding to Claude.Why
Claude Code skills are first-class extensibility: drop a
SKILL.mdinto.claude/skills/<name>/, with a YAML frontmattername:anddescription:, and Claude can invoke it. The bot already passes through unknown slash commands to Claude (#131), which means skill commands like/git-activitydo reach Claude — but:[a-z0-9_]in command names, so any skill namedgit-activity(the conventional dashed form) can't be aBotCommandat all. Users have to remember to type/git_activityand that doesn't match Claude's skill dispatcher, which uses the raw frontmatter name.This PR fixes both: discovers skills from the same paths Claude Code itself uses, exposes them with normalized names in the menu, and rewrites the command back to the original form before passing to Claude.
What
src/bot/features/skill_discovery.py(new, 172 LOC) — scans the standard skill locations:with project > user > plugin precedence. Parses YAML frontmatter for
name,description,argument-hint. Skips skills declaringuser-invokable: false(so non-user-facing skills like agent-only ones don't pollute the menu) and skips collisions with built-in commands (start,new,status, etc).The
rewrite_skill_command(text, skills)helper maps a leading/<normalized>back to/<original_name>for discovered skills — leaves non-command text, unknown commands, and already-original-form commands untouched.src/bot/orchestrator.py(+30 LOC) — wires it in:__init__: scans skills once at startupagentic_text: rewrites the leading slash before forwarding to Claudeagentic_new: re-scans on/newso newly-added skills appear without restartingget_bot_commands: appends discovered skills to the agentic command listtests/unit/test_bot/test_skill_discovery.py(new, 225 LOC) — 20 unit tests covering the multi-path discovery, precedence, shadowing, normalization, command rewrite, anduser-invokable: falsefilter.Compatibility
.claude/skills/directories —discover_skills()returns an empty dict, no behavior changes.passthrough unknown slash commandsplumbing from feat: passthrough unknown slash commands to Claude in agentic mode #131 — slash commands that don't match a registered handler still flow through to Claude as before. The rewrite step is a no-op when the command isn't a discovered skill.PyYAMLis already present inpyproject.toml.Test plan
pytest tests/unit/test_bot/test_skill_discovery.py -v)_and round-tripped correctly through Claude.claude/skills/(graceful no-op)Notes
The default
_BUILTIN_COMMANDSlist (skipped during discovery) is hardcoded to match the agentic-mode handlers (start,new,status,verbose,repo,tts,restart,help,sync_threads). If a user names a skill the same as a built-in, the built-in wins and a debug log notes the skip — this matches Claude Code's own behavior.