From 42a1cc3c0af58050e8941c7cb11b26c7fa25ff43 Mon Sep 17 00:00:00 2001 From: Chuck D'Antonio Date: Tue, 4 Nov 2025 21:14:37 -0500 Subject: [PATCH 1/3] feat: add plugin source type to slash command API Add "plugin" as a third source type alongside "local" and "global" in the SlashCommand schema. This enables the API to distinguish commands from Claude Code marketplace plugins. --- hld/api/openapi.yaml | 4 +- hld/api/server.gen.go | 310 +++++++++++++++++++++--------------------- 2 files changed, 158 insertions(+), 156 deletions(-) diff --git a/hld/api/openapi.yaml b/hld/api/openapi.yaml index fba3a7c7..63fdf948 100644 --- a/hld/api/openapi.yaml +++ b/hld/api/openapi.yaml @@ -1909,8 +1909,8 @@ components: description: Command name including slash prefix source: type: string - enum: ["local", "global"] - description: Source of the command - local (repo) or global (user home) + enum: ["local", "global", "plugin"] + description: Source of the command - local (repo), global (user home), or plugin (marketplace) example: "local" SlashCommandsResponse: diff --git a/hld/api/server.gen.go b/hld/api/server.gen.go index a48c6831..9ce5920e 100644 --- a/hld/api/server.gen.go +++ b/hld/api/server.gen.go @@ -1,6 +1,6 @@ // Package api provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. package api import ( @@ -109,6 +109,7 @@ const ( const ( SlashCommandSourceGlobal SlashCommandSource = "global" SlashCommandSourceLocal SlashCommandSource = "local" + SlashCommandSourcePlugin SlashCommandSource = "plugin" ) // Defines values for ListSessionsParamsFilter. @@ -803,11 +804,11 @@ type SlashCommand struct { // Name Command name including slash prefix Name string `json:"name"` - // Source Source of the command - local (repo) or global (user home) + // Source Source of the command - local (repo), global (user home), or plugin (marketplace) Source SlashCommandSource `json:"source"` } -// SlashCommandSource Source of the command - local (repo) or global (user home) +// SlashCommandSource Source of the command - local (repo), global (user home), or plugin (marketplace) type SlashCommandSource string // SlashCommandsResponse defines model for SlashCommandsResponse. @@ -976,7 +977,7 @@ type ListSessionsParamsFilter string // SearchSessionsParams defines parameters for SearchSessions. type SearchSessionsParams struct { - // Query Search query for title matching (uses SQL LIKE) + // Query Search query for matching against title, summary, or query fields (uses SQL LIKE) Query *string `form:"query,omitempty" json:"query,omitempty"` // Limit Maximum number of results to return @@ -1093,7 +1094,7 @@ type ServerInterface interface { // Restore multiple discarded draft sessions // (POST /sessions/restore) BulkRestoreDrafts(c *gin.Context) - // Search sessions by title + // Search sessions // (GET /sessions/search) SearchSessions(c *gin.Context, params SearchSessionsParams) // Get session details @@ -2915,7 +2916,7 @@ type StrictServerInterface interface { // Restore multiple discarded draft sessions // (POST /sessions/restore) BulkRestoreDrafts(ctx context.Context, request BulkRestoreDraftsRequestObject) (BulkRestoreDraftsResponseObject, error) - // Search sessions by title + // Search sessions // (GET /sessions/search) SearchSessions(ctx context.Context, request SearchSessionsRequestObject) (SearchSessionsResponseObject, error) // Get session details @@ -3868,154 +3869,155 @@ func (sh *strictHandler) ValidateDirectory(ctx *gin.Context) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file From 5225b83ada7ef363611472c4afe8d04ffd242a63 Mon Sep 17 00:00:00 2001 From: Chuck D'Antonio Date: Tue, 4 Nov 2025 21:14:48 -0500 Subject: [PATCH 2/3] feat: implement Claude Code plugin command discovery Add discovery of slash commands from Claude Code marketplace plugins. Reads installed_plugins.json and settings.json to find enabled plugins and scans their commands directories. Plugin commands are namespaced as /plugin-name:command-name and respect enabled/disabled state. Gracefully handles missing or malformed plugin metadata files. --- hld/api/handlers/plugins.go | 202 +++++++++++++++++++++++++++++++++++ hld/api/handlers/sessions.go | 24 +++++ hld/go.mod | 1 + hld/go.sum | 22 ++++ 4 files changed, 249 insertions(+) create mode 100644 hld/api/handlers/plugins.go diff --git a/hld/api/handlers/plugins.go b/hld/api/handlers/plugins.go new file mode 100644 index 00000000..e7e7b984 --- /dev/null +++ b/hld/api/handlers/plugins.go @@ -0,0 +1,202 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + "time" + + api "github.com/humanlayer/humanlayer/hld/api" +) + +// InstalledPluginsJSON represents the installed_plugins.json file structure +type InstalledPluginsJSON struct { + Version int `json:"version"` + Plugins map[string]InstalledPlugin `json:"plugins"` +} + +// InstalledPlugin represents a single plugin's metadata +type InstalledPlugin struct { + Version string `json:"version"` + InstalledAt time.Time `json:"installedAt"` + LastUpdated time.Time `json:"lastUpdated"` + InstallPath string `json:"installPath"` + GitCommitSha string `json:"gitCommitSha"` + IsLocal bool `json:"isLocal"` +} + +// SettingsJSON represents the settings.json file structure +type SettingsJSON struct { + EnabledPlugins map[string]bool `json:"enabledPlugins"` + // Other settings fields are ignored +} + +// discoverPluginCommands discovers slash commands from installed Claude Code plugins +// This function encapsulates all plugin-specific logic to isolate it from the main flow +func discoverPluginCommands(configDir string) ([]api.SlashCommand, error) { + var pluginCommands []api.SlashCommand + + // Expand tilde in config directory + configDir = expandTilde(configDir) + + // Plugin files are in a subdirectory + pluginsDir := filepath.Join(configDir, "plugins") + + // 1. Read installed plugins metadata + installedPluginsPath := filepath.Join(pluginsDir, "installed_plugins.json") + installedData, err := os.ReadFile(installedPluginsPath) + if err != nil { + if os.IsNotExist(err) { + // No plugins installed, return empty list + slog.Debug("No installed_plugins.json found", + "path", installedPluginsPath, + "operation", "discoverPluginCommands") + return pluginCommands, nil + } + // Other error (permissions, etc.) + return nil, fmt.Errorf("failed to read installed_plugins.json: %w", err) + } + + var installed InstalledPluginsJSON + if err := json.Unmarshal(installedData, &installed); err != nil { + slog.Warn("Failed to parse installed_plugins.json", + "path", installedPluginsPath, + "error", err.Error(), + "operation", "discoverPluginCommands") + // Return empty rather than failing entirely + return pluginCommands, nil + } + + // 2. Read enabled plugins state + settingsPath := filepath.Join(configDir, "settings.json") + settingsData, err := os.ReadFile(settingsPath) + if err != nil { + if os.IsNotExist(err) { + // No settings file, assume all plugins are enabled + slog.Debug("No settings.json found, treating all plugins as enabled", + "path", settingsPath, + "operation", "discoverPluginCommands") + } else { + // Log but don't fail + slog.Warn("Failed to read settings.json", + "path", settingsPath, + "error", err.Error(), + "operation", "discoverPluginCommands") + } + // Continue with all plugins enabled + settingsData = []byte("{}") + } + + var settings SettingsJSON + if err := json.Unmarshal(settingsData, &settings); err != nil { + slog.Warn("Failed to parse settings.json", + "path", settingsPath, + "error", err.Error(), + "operation", "discoverPluginCommands") + // Continue with empty settings (all plugins enabled) + settings = SettingsJSON{EnabledPlugins: make(map[string]bool)} + } + + // 3. For each plugin, check if enabled and scan commands + for pluginID, plugin := range installed.Plugins { + // Check if plugin is explicitly disabled + if enabled, exists := settings.EnabledPlugins[pluginID]; exists && !enabled { + slog.Debug("Skipping disabled plugin", + "plugin_id", pluginID, + "operation", "discoverPluginCommands") + continue + } + + // Extract plugin name from "plugin-name@marketplace-name" format + pluginName := strings.Split(pluginID, "@")[0] + + // Scan plugin commands directory + commandsDir := filepath.Join(plugin.InstallPath, "commands") + commands, err := scanPluginCommandsDir(commandsDir, pluginName) + if err != nil { + slog.Warn("Failed to scan plugin commands", + "plugin", pluginName, + "commands_dir", commandsDir, + "error", err.Error(), + "operation", "discoverPluginCommands") + // Continue with other plugins + continue + } + + slog.Debug("Discovered plugin commands", + "plugin", pluginName, + "count", len(commands), + "operation", "discoverPluginCommands") + + pluginCommands = append(pluginCommands, commands...) + } + + return pluginCommands, nil +} + +// scanPluginCommandsDir scans a single plugin's commands directory +func scanPluginCommandsDir(dir string, pluginName string) ([]api.SlashCommand, error) { + var commands []api.SlashCommand + + // Check if directory exists + if _, err := os.Stat(dir); os.IsNotExist(err) { + // Commands directory doesn't exist, return empty + return commands, nil + } + + err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + // Log but don't fail the entire scan + slog.Debug("Error walking directory", + "path", path, + "error", err.Error(), + "operation", "scanPluginCommandsDir") + return nil + } + + if d.IsDir() { + return nil + } + + // Only process .md files + if !strings.HasSuffix(path, ".md") { + return nil + } + + // Get relative path from commands directory + relPath, err := filepath.Rel(dir, path) + if err != nil { + slog.Debug("Failed to get relative path", + "path", path, + "base", dir, + "error", err.Error(), + "operation", "scanPluginCommandsDir") + return nil + } + + // Convert to command name + commandName := strings.TrimSuffix(relPath, ".md") + + // Convert path separators to colons for nested commands + commandName = strings.ReplaceAll(commandName, string(filepath.Separator), ":") + + // Create namespaced command: /plugin-name:command-name + fullCommandName := "/" + pluginName + ":" + commandName + + commands = append(commands, api.SlashCommand{ + Name: fullCommandName, + Source: api.SlashCommandSourcePlugin, + }) + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk directory %s: %w", dir, err) + } + + return commands, nil +} diff --git a/hld/api/handlers/sessions.go b/hld/api/handlers/sessions.go index b5a3e3d2..171f4c87 100644 --- a/hld/api/handlers/sessions.go +++ b/hld/api/handlers/sessions.go @@ -1696,6 +1696,30 @@ func (h *SessionHandlers) GetSlashCommands(ctx context.Context, req api.GetSlash ) } + // Discover plugin commands (new code) + slog.Debug("Discovering plugin commands", + "config_dir", configDir, + "operation", "GetSlashCommands") + + pluginCommands, err := discoverPluginCommands(configDir) + if err != nil { + // Log error but don't fail the entire request + slog.Warn("Failed to discover plugin commands", + "error", err.Error(), + "config_dir", configDir, + "operation", "GetSlashCommands") + // Continue without plugin commands + } else { + // Add plugin commands to the map + for _, cmd := range pluginCommands { + commandMap[cmd.Name] = cmd + } + + slog.Debug("Added plugin commands to results", + "count", len(pluginCommands), + "operation", "GetSlashCommands") + } + // Extract all commands from map var allCommands []api.SlashCommand for _, cmd := range commandMap { diff --git a/hld/go.mod b/hld/go.mod index 07d5a5b2..93e5befe 100644 --- a/hld/go.mod +++ b/hld/go.mod @@ -43,6 +43,7 @@ require ( github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/hld/go.sum b/hld/go.sum index 72129803..1db415c3 100644 --- a/hld/go.sum +++ b/hld/go.sum @@ -49,6 +49,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -144,6 +146,7 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= @@ -153,18 +156,37 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= From c3542968d8d014c879f8aa6e1d4909e3b3f7e45a Mon Sep 17 00:00:00 2001 From: Chuck D'Antonio Date: Tue, 4 Nov 2025 21:15:00 -0500 Subject: [PATCH 3/3] test: add comprehensive plugin command discovery tests Add 12 unit tests covering plugin discovery edge cases: - Missing/malformed metadata files - Enabled/disabled plugin filtering - Nested commands and namespacing - Multiple plugins Add integration test with 3 scenarios validating end-to-end behavior. --- hld/api/handlers/plugins_test.go | 289 ++++++++++++++++++ .../daemon_slash_commands_integration_test.go | 274 +++++++++++++++++ 2 files changed, 563 insertions(+) create mode 100644 hld/api/handlers/plugins_test.go diff --git a/hld/api/handlers/plugins_test.go b/hld/api/handlers/plugins_test.go new file mode 100644 index 00000000..7d4b2d6c --- /dev/null +++ b/hld/api/handlers/plugins_test.go @@ -0,0 +1,289 @@ +package handlers + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + api "github.com/humanlayer/humanlayer/hld/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDiscoverPluginCommands_NoInstalledPluginsFile(t *testing.T) { + // Create temporary config directory without installed_plugins.json + tmpDir := t.TempDir() + pluginsDir := filepath.Join(tmpDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + commands, err := discoverPluginCommands(tmpDir) + require.NoError(t, err) + assert.Empty(t, commands) +} + +func TestDiscoverPluginCommands_MalformedInstalledPluginsJSON(t *testing.T) { + // Create temporary config directory with malformed JSON + tmpDir := t.TempDir() + pluginsDir := filepath.Join(tmpDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + // Write invalid JSON + installedPluginsPath := filepath.Join(pluginsDir, "installed_plugins.json") + require.NoError(t, os.WriteFile(installedPluginsPath, []byte("not valid json"), 0644)) + + commands, err := discoverPluginCommands(tmpDir) + require.NoError(t, err) + assert.Empty(t, commands) +} + +func TestDiscoverPluginCommands_NoSettingsFile(t *testing.T) { + // Create temporary config directory with installed plugins but no settings + tmpDir := t.TempDir() + pluginsDir := filepath.Join(tmpDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + // Create plugin directory with commands + pluginPath := filepath.Join(pluginsDir, "test-plugin") + commandsDir := filepath.Join(pluginPath, "commands") + require.NoError(t, os.MkdirAll(commandsDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commandsDir, "test-cmd.md"), []byte("# Test"), 0644)) + + // Create installed_plugins.json + installed := InstalledPluginsJSON{ + Version: 1, + Plugins: map[string]InstalledPlugin{ + "test-plugin@marketplace": { + Version: "1.0.0", + InstalledAt: time.Now(), + LastUpdated: time.Now(), + InstallPath: pluginPath, + GitCommitSha: "abc123", + IsLocal: false, + }, + }, + } + installedData, err := json.Marshal(installed) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(pluginsDir, "installed_plugins.json"), installedData, 0644)) + + // No settings.json - should treat all plugins as enabled + commands, err := discoverPluginCommands(tmpDir) + require.NoError(t, err) + require.Len(t, commands, 1) + assert.Equal(t, "/test-plugin:test-cmd", commands[0].Name) + assert.Equal(t, api.SlashCommandSourcePlugin, commands[0].Source) +} + +func TestDiscoverPluginCommands_DisabledPlugin(t *testing.T) { + // Create temporary config directory + tmpDir := t.TempDir() + pluginsDir := filepath.Join(tmpDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + // Create plugin directory with commands + pluginPath := filepath.Join(pluginsDir, "test-plugin") + commandsDir := filepath.Join(pluginPath, "commands") + require.NoError(t, os.MkdirAll(commandsDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commandsDir, "test-cmd.md"), []byte("# Test"), 0644)) + + // Create installed_plugins.json + installed := InstalledPluginsJSON{ + Version: 1, + Plugins: map[string]InstalledPlugin{ + "test-plugin@marketplace": { + Version: "1.0.0", + InstalledAt: time.Now(), + LastUpdated: time.Now(), + InstallPath: pluginPath, + }, + }, + } + installedData, err := json.Marshal(installed) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(pluginsDir, "installed_plugins.json"), installedData, 0644)) + + // Create settings.json with plugin disabled + settings := SettingsJSON{ + EnabledPlugins: map[string]bool{ + "test-plugin@marketplace": false, + }, + } + settingsData, err := json.Marshal(settings) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "settings.json"), settingsData, 0644)) + + // Should return empty because plugin is disabled + commands, err := discoverPluginCommands(tmpDir) + require.NoError(t, err) + assert.Empty(t, commands) +} + +func TestDiscoverPluginCommands_EnabledPlugin(t *testing.T) { + // Create temporary config directory + tmpDir := t.TempDir() + pluginsDir := filepath.Join(tmpDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + // Create plugin directory with commands + pluginPath := filepath.Join(pluginsDir, "test-plugin") + commandsDir := filepath.Join(pluginPath, "commands") + require.NoError(t, os.MkdirAll(commandsDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commandsDir, "test-cmd.md"), []byte("# Test"), 0644)) + + // Create installed_plugins.json + installed := InstalledPluginsJSON{ + Version: 1, + Plugins: map[string]InstalledPlugin{ + "test-plugin@marketplace": { + Version: "1.0.0", + InstalledAt: time.Now(), + LastUpdated: time.Now(), + InstallPath: pluginPath, + }, + }, + } + installedData, err := json.Marshal(installed) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(pluginsDir, "installed_plugins.json"), installedData, 0644)) + + // Create settings.json with plugin enabled + settings := SettingsJSON{ + EnabledPlugins: map[string]bool{ + "test-plugin@marketplace": true, + }, + } + settingsData, err := json.Marshal(settings) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "settings.json"), settingsData, 0644)) + + commands, err := discoverPluginCommands(tmpDir) + require.NoError(t, err) + require.Len(t, commands, 1) + assert.Equal(t, "/test-plugin:test-cmd", commands[0].Name) + assert.Equal(t, api.SlashCommandSourcePlugin, commands[0].Source) +} + +func TestDiscoverPluginCommands_MultiplePlugins(t *testing.T) { + // Create temporary config directory + tmpDir := t.TempDir() + pluginsDir := filepath.Join(tmpDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + // Create first plugin + plugin1Path := filepath.Join(pluginsDir, "plugin-one") + commands1Dir := filepath.Join(plugin1Path, "commands") + require.NoError(t, os.MkdirAll(commands1Dir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commands1Dir, "cmd1.md"), []byte("# Cmd1"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(commands1Dir, "cmd2.md"), []byte("# Cmd2"), 0644)) + + // Create second plugin + plugin2Path := filepath.Join(pluginsDir, "plugin-two") + commands2Dir := filepath.Join(plugin2Path, "commands") + require.NoError(t, os.MkdirAll(commands2Dir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commands2Dir, "cmd3.md"), []byte("# Cmd3"), 0644)) + + // Create installed_plugins.json + installed := InstalledPluginsJSON{ + Version: 1, + Plugins: map[string]InstalledPlugin{ + "plugin-one@marketplace": { + Version: "1.0.0", + InstallPath: plugin1Path, + }, + "plugin-two@marketplace": { + Version: "2.0.0", + InstallPath: plugin2Path, + }, + }, + } + installedData, err := json.Marshal(installed) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(pluginsDir, "installed_plugins.json"), installedData, 0644)) + + commands, err := discoverPluginCommands(tmpDir) + require.NoError(t, err) + require.Len(t, commands, 3) + + // Verify command names + commandNames := make(map[string]bool) + for _, cmd := range commands { + commandNames[cmd.Name] = true + assert.Equal(t, api.SlashCommandSourcePlugin, cmd.Source) + } + + assert.True(t, commandNames["/plugin-one:cmd1"]) + assert.True(t, commandNames["/plugin-one:cmd2"]) + assert.True(t, commandNames["/plugin-two:cmd3"]) +} + +func TestScanPluginCommandsDir_NonExistentDirectory(t *testing.T) { + commands, err := scanPluginCommandsDir("/nonexistent/path", "test-plugin") + require.NoError(t, err) + assert.Empty(t, commands) +} + +func TestScanPluginCommandsDir_EmptyDirectory(t *testing.T) { + tmpDir := t.TempDir() + + commands, err := scanPluginCommandsDir(tmpDir, "test-plugin") + require.NoError(t, err) + assert.Empty(t, commands) +} + +func TestScanPluginCommandsDir_SingleCommand(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "test.md"), []byte("# Test"), 0644)) + + commands, err := scanPluginCommandsDir(tmpDir, "test-plugin") + require.NoError(t, err) + require.Len(t, commands, 1) + assert.Equal(t, "/test-plugin:test", commands[0].Name) + assert.Equal(t, api.SlashCommandSourcePlugin, commands[0].Source) +} + +func TestScanPluginCommandsDir_NestedCommands(t *testing.T) { + tmpDir := t.TempDir() + nestedDir := filepath.Join(tmpDir, "subdir") + require.NoError(t, os.MkdirAll(nestedDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(nestedDir, "nested.md"), []byte("# Nested"), 0644)) + + commands, err := scanPluginCommandsDir(tmpDir, "test-plugin") + require.NoError(t, err) + require.Len(t, commands, 1) + assert.Equal(t, "/test-plugin:subdir:nested", commands[0].Name) + assert.Equal(t, api.SlashCommandSourcePlugin, commands[0].Source) +} + +func TestScanPluginCommandsDir_IgnoresNonMarkdownFiles(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "test.md"), []byte("# Test"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("Not markdown"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "README"), []byte("Also not markdown"), 0644)) + + commands, err := scanPluginCommandsDir(tmpDir, "test-plugin") + require.NoError(t, err) + require.Len(t, commands, 1) + assert.Equal(t, "/test-plugin:test", commands[0].Name) +} + +func TestScanPluginCommandsDir_MultipleCommands(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "cmd1.md"), []byte("# Cmd1"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "cmd2.md"), []byte("# Cmd2"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "cmd3.md"), []byte("# Cmd3"), 0644)) + + commands, err := scanPluginCommandsDir(tmpDir, "test-plugin") + require.NoError(t, err) + require.Len(t, commands, 3) + + commandNames := make(map[string]bool) + for _, cmd := range commands { + commandNames[cmd.Name] = true + } + + assert.True(t, commandNames["/test-plugin:cmd1"]) + assert.True(t, commandNames["/test-plugin:cmd2"]) + assert.True(t, commandNames["/test-plugin:cmd3"]) +} diff --git a/hld/daemon/daemon_slash_commands_integration_test.go b/hld/daemon/daemon_slash_commands_integration_test.go index 82f18ace..ef449d73 100644 --- a/hld/daemon/daemon_slash_commands_integration_test.go +++ b/hld/daemon/daemon_slash_commands_integration_test.go @@ -395,6 +395,280 @@ A global command in the tmp directory.`, } } +// TestPluginCommandsIntegration tests plugin command discovery through the daemon +func TestPluginCommandsIntegration(t *testing.T) { + socketPath := testutil.SocketPath(t, "plugin-commands") + dbPath := testutil.DatabasePath(t, "plugin-commands") + httpPort := getSlashCommandsFreePort(t) + + // Create temporary working directory for session + workingDir := t.TempDir() + + // Create temporary home directory with Claude config + tempHomeDir := t.TempDir() + originalHome := os.Getenv("HOME") + originalClaudeConfigDir := os.Getenv("CLAUDE_CONFIG_DIR") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } else { + os.Unsetenv("HOME") + } + if originalClaudeConfigDir != "" { + os.Setenv("CLAUDE_CONFIG_DIR", originalClaudeConfigDir) + } + }() + os.Setenv("HOME", tempHomeDir) + os.Unsetenv("CLAUDE_CONFIG_DIR") // Use default location + + // Set environment for test + os.Setenv("HUMANLAYER_DAEMON_SOCKET", socketPath) + os.Setenv("HUMANLAYER_DATABASE_PATH", dbPath) + os.Setenv("HUMANLAYER_DAEMON_HTTP_PORT", fmt.Sprintf("%d", httpPort)) + os.Setenv("HUMANLAYER_DAEMON_HTTP_HOST", "127.0.0.1") + defer func() { + os.Unsetenv("HUMANLAYER_DAEMON_SOCKET") + os.Unsetenv("HUMANLAYER_DATABASE_PATH") + os.Unsetenv("HUMANLAYER_DAEMON_HTTP_PORT") + os.Unsetenv("HUMANLAYER_DAEMON_HTTP_HOST") + }() + + // Create and start daemon + daemon, err := New() + require.NoError(t, err, "Failed to create daemon") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start daemon in background + errCh := make(chan error, 1) + go func() { + errCh <- daemon.Run(ctx) + }() + + // Wait for daemon socket to be ready + deadline := time.Now().Add(5 * time.Second) + var daemonClient client.Client + for time.Now().Before(deadline) { + daemonClient, err = client.New(socketPath) + if err == nil { + break + } + time.Sleep(50 * time.Millisecond) + } + require.NoError(t, err, "Failed to connect to daemon") + defer daemonClient.Close() + + // Wait for HTTP server to be ready + deadline = time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/health", httpPort)) + if err == nil { + resp.Body.Close() + if resp.StatusCode == 200 { + break + } + } + time.Sleep(50 * time.Millisecond) + } + + // Verify daemon is healthy + err = daemonClient.Health() + require.NoError(t, err, "Daemon health check failed") + + t.Run("Plugin commands with enabled plugins", func(t *testing.T) { + // Create Claude config structure with plugins + claudeConfigDir := filepath.Join(tempHomeDir, ".config", "claude-code") + pluginsDir := filepath.Join(claudeConfigDir, "plugins") + require.NoError(t, os.MkdirAll(pluginsDir, 0755)) + + // Create first plugin + plugin1Path := filepath.Join(pluginsDir, "test-plugin-one") + commands1Dir := filepath.Join(plugin1Path, "commands") + require.NoError(t, os.MkdirAll(commands1Dir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commands1Dir, "deploy.md"), []byte("# Deploy"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(commands1Dir, "status.md"), []byte("# Status"), 0644)) + + // Create nested commands in first plugin + nestedDir := filepath.Join(commands1Dir, "config") + require.NoError(t, os.MkdirAll(nestedDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(nestedDir, "show.md"), []byte("# Config Show"), 0644)) + + // Create second plugin + plugin2Path := filepath.Join(pluginsDir, "test-plugin-two") + commands2Dir := filepath.Join(plugin2Path, "commands") + require.NoError(t, os.MkdirAll(commands2Dir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(commands2Dir, "analyze.md"), []byte("# Analyze"), 0644)) + + // Create installed_plugins.json + installedPlugins := map[string]interface{}{ + "version": 1, + "plugins": map[string]interface{}{ + "test-plugin-one@marketplace": map[string]interface{}{ + "version": "1.0.0", + "installedAt": time.Now().Format(time.RFC3339), + "lastUpdated": time.Now().Format(time.RFC3339), + "installPath": plugin1Path, + "gitCommitSha": "abc123", + "isLocal": false, + }, + "test-plugin-two@marketplace": map[string]interface{}{ + "version": "2.0.0", + "installedAt": time.Now().Format(time.RFC3339), + "lastUpdated": time.Now().Format(time.RFC3339), + "installPath": plugin2Path, + "gitCommitSha": "def456", + "isLocal": false, + }, + }, + } + installedData, err := json.Marshal(installedPlugins) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(pluginsDir, "installed_plugins.json"), installedData, 0644)) + + // Create settings.json with one plugin enabled, one disabled + settings := map[string]interface{}{ + "enabledPlugins": map[string]bool{ + "test-plugin-one@marketplace": true, + "test-plugin-two@marketplace": false, + }, + } + settingsData, err := json.Marshal(settings) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(claudeConfigDir, "settings.json"), settingsData, 0644)) + + // Create a session + sessionID := "test-plugin-session" + session := &store.Session{ + ID: sessionID, + RunID: "test-run-plugin", + ClaudeSessionID: "claude-test-plugin", + Query: "Test plugin commands", + Status: store.SessionStatusRunning, + WorkingDir: workingDir, + CreatedAt: time.Now(), + LastActivityAt: time.Now(), + } + + err = daemon.store.CreateSession(ctx, session) + require.NoError(t, err) + + // Get all commands + httpClient := &http.Client{} + resp, err := httpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/slash-commands?working_dir=%s", httpPort, workingDir)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 200, resp.StatusCode) + + var commandsResp struct { + Data []api.SlashCommand `json:"data"` + } + err = json.NewDecoder(resp.Body).Decode(&commandsResp) + require.NoError(t, err) + + // Build a map of plugin commands + pluginCommandMap := make(map[string]api.SlashCommandSource) + for _, cmd := range commandsResp.Data { + if cmd.Source == api.SlashCommandSourcePlugin { + pluginCommandMap[cmd.Name] = cmd.Source + } + } + + // Verify enabled plugin commands exist with plugin source + assert.Equal(t, api.SlashCommandSourcePlugin, pluginCommandMap["/test-plugin-one:deploy"]) + assert.Equal(t, api.SlashCommandSourcePlugin, pluginCommandMap["/test-plugin-one:status"]) + assert.Equal(t, api.SlashCommandSourcePlugin, pluginCommandMap["/test-plugin-one:config:show"]) + + // Verify disabled plugin commands do NOT exist + _, hasAnalyze := pluginCommandMap["/test-plugin-two:analyze"] + assert.False(t, hasAnalyze, "Disabled plugin commands should not be returned") + + // Verify correct count + assert.Equal(t, 3, len(pluginCommandMap), "Should have exactly 3 plugin commands from enabled plugin") + }) + + t.Run("Plugin commands with all plugins enabled by default", func(t *testing.T) { + // Create new Claude config without settings.json + claudeConfigDir := filepath.Join(tempHomeDir, ".config", "claude-code") + + // Remove settings.json to test default behavior + os.Remove(filepath.Join(claudeConfigDir, "settings.json")) + + // Get all commands again + httpClient := &http.Client{} + resp, err := httpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/slash-commands?working_dir=%s", httpPort, workingDir)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 200, resp.StatusCode) + + var commandsResp struct { + Data []api.SlashCommand `json:"data"` + } + err = json.NewDecoder(resp.Body).Decode(&commandsResp) + require.NoError(t, err) + + // Build a map of plugin commands + pluginCommandMap := make(map[string]bool) + for _, cmd := range commandsResp.Data { + if cmd.Source == api.SlashCommandSourcePlugin { + pluginCommandMap[cmd.Name] = true + } + } + + // Without settings.json, all plugins should be enabled + assert.True(t, pluginCommandMap["/test-plugin-one:deploy"]) + assert.True(t, pluginCommandMap["/test-plugin-one:status"]) + assert.True(t, pluginCommandMap["/test-plugin-one:config:show"]) + assert.True(t, pluginCommandMap["/test-plugin-two:analyze"]) + + // Should have all 4 commands now + assert.Equal(t, 4, len(pluginCommandMap), "Should have all plugin commands when no settings.json") + }) + + t.Run("Plugin commands with missing installed_plugins.json", func(t *testing.T) { + // Remove installed_plugins.json + claudeConfigDir := filepath.Join(tempHomeDir, ".config", "claude-code") + os.Remove(filepath.Join(claudeConfigDir, "plugins", "installed_plugins.json")) + + // Get all commands + httpClient := &http.Client{} + resp, err := httpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/slash-commands?working_dir=%s", httpPort, workingDir)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 200, resp.StatusCode) + + var commandsResp struct { + Data []api.SlashCommand `json:"data"` + } + err = json.NewDecoder(resp.Body).Decode(&commandsResp) + require.NoError(t, err) + + // Should have no plugin commands + pluginCommandCount := 0 + for _, cmd := range commandsResp.Data { + if cmd.Source == api.SlashCommandSourcePlugin { + pluginCommandCount++ + } + } + + assert.Equal(t, 0, pluginCommandCount, "Should have no plugin commands when installed_plugins.json is missing") + }) + + // Shutdown daemon + cancel() + + // Wait for daemon to exit + select { + case err := <-errCh: + assert.NoError(t, err, "Daemon exited with error") + case <-time.After(5 * time.Second): + t.Error("Daemon did not exit in time") + } +} + // TestSlashCommandsPerformance tests performance with many commands func TestSlashCommandsPerformance(t *testing.T) { if testing.Short() {