Skip to content

feat: discover Claude Code skills + Telegram command name normalization#197

Open
americodias wants to merge 1 commit intoRichardAtCT:mainfrom
americodias:pr/skill-discovery
Open

feat: discover Claude Code skills + Telegram command name normalization#197
americodias wants to merge 1 commit intoRichardAtCT:mainfrom
americodias:pr/skill-discovery

Conversation

@americodias
Copy link
Copy Markdown

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.md into .claude/skills/<name>/, with a YAML frontmatter name: and description:, and Claude can invoke it. The bot already passes through unknown slash commands to Claude (#131), which means skill commands like /git-activity do reach Claude — but:

  1. They're invisible in Telegram's command menu (no autocomplete).
  2. Telegram's Bot API only allows [a-z0-9_] in command names, so any skill named git-activity (the conventional dashed form) can't be a BotCommand at all. Users have to remember to type /git_activity and 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:

{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

with project > user > plugin precedence. Parses YAML frontmatter for name, description, argument-hint. Skips skills declaring user-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 startup
  • agentic_text: rewrites the leading slash before forwarding to Claude
  • agentic_new: re-scans on /new so newly-added skills appear without restarting
  • get_bot_commands: appends discovered skills to the agentic command list

tests/unit/test_bot/test_skill_discovery.py (new, 225 LOC) — 20 unit tests covering the multi-path discovery, precedence, shadowing, normalization, command rewrite, and user-invokable: false filter.

Compatibility

  • Pure addition for projects without .claude/skills/ directories — discover_skills() returns an empty dict, no behavior changes.
  • Uses the existing passthrough unknown slash commands plumbing 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.
  • No new dependencies. PyYAML is already present in pyproject.toml.

Test plan

  • 20 unit tests passing (pytest tests/unit/test_bot/test_skill_discovery.py -v)
  • Tested live against a project with 51 project-level + 21 plugin-level skills — discovery surfaced all 72 in the Telegram command menu, dashed names rendered as _ and round-tripped correctly through Claude
  • Test in a project with no .claude/skills/ (graceful no-op)
  • Test plugin-only skills (no project skills) resolution

Notes

The default _BUILTIN_COMMANDS list (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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant