humanlayer icon indicating copy to clipboard operation
humanlayer copied to clipboard

Add Claude Code plugin command discovery

Open crdant opened this issue 1 month ago • 4 comments

What problem(s) was I solving?

Problem: CodeLayer users cannot see or use slash commands from Claude Code marketplace plugins in the CodeLayer UI.

Claude Code marketplace plugins install their commands to a different location (~/.config/claude-code/plugins/{plugin-name}/commands/), and their metadata is stored in separate JSON files (installed_plugins.json and settings.json). The daemon had no logic to discover these plugin commands.

Impact: Users installing plugins like "Superpowers" or other marketplace plugins would not see those commands available in CodeLayer, creating a disconnected experience between Claude Code and CodeLayer.

What user-facing changes did I ship?

CodeLayer will now display slash commands from installed Claude Code plugins:

  • Plugin commands appear in the slash command list with source: "plugin"
  • Commands are namespaced as /plugin-name:command-name (e.g., /superpowers:brainstorm)
  • Only enabled plugins' commands appear (respects user's Claude Code settings)
  • Nested plugin commands use colon separators (e.g., /plugin-name:config:show)
  • Fuzzy search works across plugin commands
  • Commands seamlessly integrate with existing local/global commands

No breaking changes - existing local and global commands work exactly as before.

How I implemented it

Architecture Decision: Isolated Plugin Discovery

I chose to encapsulate all plugin-specific logic in isolated functions to make the plugin system maintainable and adaptable to future changes by Anthropic:

New File: hld/api/handlers/plugins.go (122 lines)

  • discoverPluginCommands(configDir string) - Main discovery orchestration
  • scanPluginCommandsDir(dir, pluginName string) - Scans individual plugin commands
  • Type definitions for plugin metadata structures

Implementation Details

1. OpenAPI Schema Extension (hld/api/openapi.yaml)

  • Added "plugin" to the SlashCommand.source enum
  • Regenerated Go types → SlashCommandSourcePlugin constant

2. Plugin Discovery Logic (hld/api/handlers/plugins.go)

Discovery Flow:
1. Read $CLAUDE_CONFIG_DIR/plugins/installed_plugins.json
   - Contains plugin metadata: version, install path, git commit
2. Read $CLAUDE_CONFIG_DIR/settings.json
   - Contains enabledPlugins map (plugin-id → true/false)
3. For each enabled plugin:
   - Extract plugin name from "plugin-name@marketplace" ID
   - Scan {installPath}/commands/ for .md files
   - Create namespaced command: /plugin-name:command-name
4. Return list of plugin commands with source="plugin"

if CLAUDE_CONFIG_DIR is discovery will use ~/.claude

3. Error Handling (Graceful Degradation)

  • Missing installed_plugins.json → return empty list (no plugins installed)
  • Missing settings.json → treat all plugins as enabled (default behavior)
  • Malformed JSON → log warning, return empty list
  • Missing plugin commands directory → skip that plugin
  • All errors logged but don't break the entire API request

4. Integration (hld/api/handlers/sessions.go)

  • Added plugin discovery after global command discovery (line 1699-1721)
  • Plugin commands merged into same deduplication map as local/global
  • Maintains existing precedence: local → global → plugin (no conflicts due to namespacing)

5. Comprehensive Testing

Unit Tests (hld/api/handlers/plugins_test.go - 12 tests):

  • No installed plugins file
  • Malformed installed_plugins.json
  • No settings file (default enabled behavior)
  • Disabled plugin filtering
  • Enabled plugin discovery
  • Multiple plugins
  • Non-existent directories
  • Empty directories
  • Single command
  • Nested commands (colon separator)
  • Non-markdown file filtering
  • Multiple commands per plugin

Integration Tests (hld/daemon/daemon_slash_commands_integration_test.go):

  • TestPluginCommandsIntegration with 3 subtests:
    • Enabled/disabled plugin filtering via settings.json
    • Default behavior without settings.json (all enabled)
    • Graceful handling of missing installed_plugins.json

Key Design Decisions

1. Namespace Format: /plugin-name:command

  • Plugin name is in the command name itself
  • Source field remains simple enum: "plugin" (not "plugin:name")
  • Easy to parse: command.name.split(':')[0].slice(1) gives plugin name

2. Config Directory Detection

  • Uses existing CLAUDE_CONFIG_DIR environment variable logic
  • Falls back to ~/.claude (macOS/Linux standard)
  • Leverages existing expandTilde() helper function

3. Isolation Pattern

  • All plugin logic in separate plugins.go file
  • Can be updated independently if Anthropic changes plugin format
  • No pollution of main session handler logic

How to verify it

  • [x] I have ensured make check test passes

Verification Results:

✓ Format check passed
✓ Vet check passed
✓ Lint check passed
✓ Unit tests passed (424 tests)
✓ Unit tests with race detection passed (424 tests)
✓ Integration tests passed (50 tests)
✓ Integration tests with race detection passed (50 tests)

Test Coverage:

  • 12 new unit tests for plugin discovery (all passing)
  • 3 new integration test scenarios (all passing)
  • 0 failing tests
  • 0 lint issues

Manual Testing (optional but recommended):

With real Claude Code plugins installed:

# Start daemon
cd hld && make build && ./hld

# Query plugin commands
curl "http://127.0.0.1:5188/api/v1/slash-commands?working_dir=$(pwd)" | \
  jq '.data[] | select(.source == "plugin")'

# Test fuzzy search
curl "http://127.0.0.1:5188/api/v1/slash-commands?working_dir=$(pwd)&query=brain" | \
  jq '.data[] | select(.source == "plugin")'

Description for the changelog

Added: Claude Code marketplace plugin commands now appear in CodeLayer's slash command list. Plugin commands are discovered from ~/.config/claude-code/plugins/ and respect enabled/disabled state from Claude Code settings. Commands are namespaced as /plugin-name:command-name.

A picture of a cute animal (not mandatory but encouraged)

9A5FAC61-39FA-43EF-A96D-328D009BCA86_1_102_o

Technical Notes

Files Modified:

  • hld/api/openapi.yaml - Added "plugin" to source enum
  • hld/api/server.gen.go - Auto-generated constant
  • hld/api/handlers/sessions.go - Added plugin discovery call (24 lines)
  • hld/daemon/daemon_slash_commands_integration_test.go - Added integration test (274 lines)
  • hld/go.mod / hld/go.sum - Added test mock dependencies

Files Created:

  • hld/api/handlers/plugins.go - Plugin discovery implementation (122 lines)
  • hld/api/handlers/plugins_test.go - Comprehensive unit tests (250+ lines)

Implementation Plan: thoughts/shared/plans/2025-01-08-plugin-command-discovery.md Research Document: docs/research/2025-11-04-codelayer-slash-command-loading.md

Total Changes: +635 lines, -156 lines across 6 modified + 2 new files


[!IMPORTANT] Adds discovery and display of Claude Code marketplace plugin commands in CodeLayer UI, with new logic in plugins.go and comprehensive testing.

  • Behavior:
    • Adds discovery of slash commands from Claude Code marketplace plugins in hld/api/handlers/plugins.go.
    • Commands are namespaced as /plugin-name:command-name and appear with source: "plugin".
    • Only enabled plugins' commands are shown, respecting settings.json.
    • Supports nested commands with colon separators.
    • Integrates with existing local/global commands without breaking changes.
  • Implementation:
    • New file plugins.go for plugin command discovery logic.
    • Updates openapi.yaml to add "plugin" to SlashCommand.source enum.
    • Modifies sessions.go to include plugin command discovery.
  • Testing:
    • Adds unit tests in plugins_test.go for various scenarios including missing files and malformed JSON.
    • Adds integration tests in daemon_slash_commands_integration_test.go for plugin command discovery.
  • Misc:
    • Updates go.mod and go.sum for new dependencies.

This description was created by Ellipsis for c3542968d8d014c879f8aa6e1d4909e3b3f7e45a. You can customize this summary. It will automatically update as commits are pushed.

crdant avatar Nov 05 '25 02:11 crdant

Thanks for the PR @crdant !

Plugins are still considered to be in beta by anthropic right now so I am hesitant to merge something that depends on what the Claude code team considers an unstable API

Will discuss internally and let you know where we wind up on this

K-Mistele avatar Nov 05 '25 04:11 K-Mistele

Plugins are still considered to be in beta by anthropic right now so I am hesitant to merge something that depends on what the Claude code team considers an unstable API

Makes a lot of sense.

This PR and my last one have a common theme. hld is in the loop for autocompleting slash commands, so as soon as I type / and don't see a command I expect I'm back to the terminal until I have time to create a PR, get it approved, and install a new release.

This whack-a-mole game isn't the best thing for me as a user or for your team as creators. Perhaps there's a better approach I can't see to stay aligned so the / makes the same recommendations either in or out of CodeLayer.

crdant avatar Nov 05 '25 12:11 crdant

Appreciate both sides of this convo, and thanks chuck for the PR!

@K-Mistele is this something we could drop in an opt-in “experimental features” thing?

I would guess Claude will continue to launch beta features that ppl wanna use and we should weigh the tradeoffs of not supporting them

dexhorthy avatar Nov 05 '25 20:11 dexhorthy

@K-Mistele @dexhorthy if the experimental feature model makes sense, I can imagine I wouldn't be too hard for me to wrap it and contribute the whole thing.

crdant avatar Nov 11 '25 16:11 crdant