diff --git a/pkg/cli/fix_command_test.go b/pkg/cli/fix_command_test.go index 7e75b112b0..2a34cd4be6 100644 --- a/pkg/cli/fix_command_test.go +++ b/pkg/cli/fix_command_test.go @@ -542,12 +542,12 @@ This is a test workflow with slash command. } func TestFixCommand_SafeInputsModeRemoval(t *testing.T) { -// Create a temporary directory for test files -tmpDir := t.TempDir() -workflowFile := filepath.Join(tmpDir, "test-workflow.md") + // Create a temporary directory for test files + tmpDir := t.TempDir() + workflowFile := filepath.Join(tmpDir, "test-workflow.md") -// Create a workflow with deprecated safe-inputs.mode field -content := `--- + // Create a workflow with deprecated safe-inputs.mode field + content := `--- on: workflow_dispatch engine: copilot safe-inputs: @@ -563,51 +563,51 @@ safe-inputs: This is a test workflow with safe-inputs mode field. ` -if err := os.WriteFile(workflowFile, []byte(content), 0644); err != nil { -t.Fatalf("Failed to create test file: %v", err) -} + if err := os.WriteFile(workflowFile, []byte(content), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } -// Get the safe-inputs mode removal codemod -modeCodemod := getCodemodByID("safe-inputs-mode-removal") -if modeCodemod == nil { -t.Fatal("safe-inputs-mode-removal codemod not found") -} + // Get the safe-inputs mode removal codemod + modeCodemod := getCodemodByID("safe-inputs-mode-removal") + if modeCodemod == nil { + t.Fatal("safe-inputs-mode-removal codemod not found") + } -// Process the file -fixed, err := processWorkflowFile(workflowFile, []Codemod{*modeCodemod}, true, false) -if err != nil { -t.Fatalf("Failed to process workflow file: %v", err) -} + // Process the file + fixed, err := processWorkflowFile(workflowFile, []Codemod{*modeCodemod}, true, false) + if err != nil { + t.Fatalf("Failed to process workflow file: %v", err) + } -if !fixed { -t.Error("Expected file to be fixed, but no changes were made") -} + if !fixed { + t.Error("Expected file to be fixed, but no changes were made") + } -// Read the updated content -updatedContent, err := os.ReadFile(workflowFile) -if err != nil { -t.Fatalf("Failed to read updated file: %v", err) -} + // Read the updated content + updatedContent, err := os.ReadFile(workflowFile) + if err != nil { + t.Fatalf("Failed to read updated file: %v", err) + } -updatedStr := string(updatedContent) + updatedStr := string(updatedContent) -t.Logf("Updated content:\n%s", updatedStr) + t.Logf("Updated content:\n%s", updatedStr) -// Verify the change - mode field should be removed -if strings.Contains(updatedStr, "mode:") { -t.Errorf("Expected mode field to be removed, but it still exists:\n%s", updatedStr) -} + // Verify the change - mode field should be removed + if strings.Contains(updatedStr, "mode:") { + t.Errorf("Expected mode field to be removed, but it still exists:\n%s", updatedStr) + } -// Verify safe-inputs block and test-tool are preserved -if !strings.Contains(updatedStr, "safe-inputs:") { -t.Error("Expected safe-inputs block to be preserved") -} + // Verify safe-inputs block and test-tool are preserved + if !strings.Contains(updatedStr, "safe-inputs:") { + t.Error("Expected safe-inputs block to be preserved") + } -if !strings.Contains(updatedStr, "test-tool:") { -t.Error("Expected test-tool to be preserved") -} + if !strings.Contains(updatedStr, "test-tool:") { + t.Error("Expected test-tool to be preserved") + } -if !strings.Contains(updatedStr, "description: Test tool") { -t.Error("Expected test-tool description to be preserved") -} + if !strings.Contains(updatedStr, "description: Test tool") { + t.Error("Expected test-tool description to be preserved") + } } diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go new file mode 100644 index 0000000000..b4bc5e1f67 --- /dev/null +++ b/pkg/workflow/tools_parser.go @@ -0,0 +1,461 @@ +package workflow + +import ( + "fmt" + + "github.com/githubnext/gh-aw/pkg/logger" +) + +var toolsParserLog = logger.New("workflow:tools_parser") + +// NewTools creates a new Tools instance from a map +func NewTools(toolsMap map[string]any) *Tools { + toolsParserLog.Printf("Creating tools configuration from map with %d entries", len(toolsMap)) + if toolsMap == nil { + return &Tools{ + Custom: make(map[string]any), + raw: make(map[string]any), + } + } + + tools := &Tools{ + Custom: make(map[string]any), + raw: make(map[string]any), + } + + // Copy raw map + for k, v := range toolsMap { + tools.raw[k] = v + } + + // Extract and parse known tools + if val, exists := toolsMap["github"]; exists { + tools.GitHub = parseGitHubTool(val) + } + if val, exists := toolsMap["bash"]; exists { + tools.Bash = parseBashTool(val) + } + if val, exists := toolsMap["web-fetch"]; exists { + tools.WebFetch = parseWebFetchTool(val) + } + if val, exists := toolsMap["web-search"]; exists { + tools.WebSearch = parseWebSearchTool(val) + } + if val, exists := toolsMap["edit"]; exists { + tools.Edit = parseEditTool(val) + } + if val, exists := toolsMap["playwright"]; exists { + tools.Playwright = parsePlaywrightTool(val) + } + if val, exists := toolsMap["serena"]; exists { + tools.Serena = parseSerenaTool(val) + } + if val, exists := toolsMap["agentic-workflows"]; exists { + tools.AgenticWorkflows = parseAgenticWorkflowsTool(val) + } + if val, exists := toolsMap["cache-memory"]; exists { + tools.CacheMemory = parseCacheMemoryTool(val) + } + if val, exists := toolsMap["repo-memory"]; exists { + tools.RepoMemory = parseRepoMemoryTool(val) + } + if val, exists := toolsMap["safety-prompt"]; exists { + tools.SafetyPrompt = parseSafetyPromptTool(val) + } + if val, exists := toolsMap["timeout"]; exists { + tools.Timeout = parseTimeoutTool(val) + } + if val, exists := toolsMap["startup-timeout"]; exists { + tools.StartupTimeout = parseStartupTimeoutTool(val) + } + + // Extract custom MCP tools (anything not in the known list) + knownTools := map[string]bool{ + "github": true, + "bash": true, + "web-fetch": true, + "web-search": true, + "edit": true, + "playwright": true, + "serena": true, + "agentic-workflows": true, + "cache-memory": true, + "repo-memory": true, + "safety-prompt": true, + "timeout": true, + "startup-timeout": true, + } + + customCount := 0 + for name, config := range toolsMap { + if !knownTools[name] { + tools.Custom[name] = config + customCount++ + } + } + + toolsParserLog.Printf("Parsed tools: github=%v, bash=%v, playwright=%v, serena=%v, custom=%d", tools.GitHub != nil, tools.Bash != nil, tools.Playwright != nil, tools.Serena != nil, customCount) + return tools +} + +// parseGitHubTool converts raw github tool configuration to GitHubToolConfig +func parseGitHubTool(val any) *GitHubToolConfig { + if val == nil { + toolsParserLog.Print("GitHub tool enabled with default configuration") + return &GitHubToolConfig{ + ReadOnly: true, // default to read-only for security + } + } + + // Handle string type (simple enable) + if _, ok := val.(string); ok { + toolsParserLog.Print("GitHub tool enabled with string configuration") + return &GitHubToolConfig{ + ReadOnly: true, // default to read-only for security + } + } + + // Handle map type (detailed configuration) + if configMap, ok := val.(map[string]any); ok { + toolsParserLog.Print("Parsing GitHub tool detailed configuration") + config := &GitHubToolConfig{ + ReadOnly: true, // default to read-only for security + } + + if allowed, ok := configMap["allowed"].([]any); ok { + config.Allowed = make([]string, 0, len(allowed)) + for _, item := range allowed { + if str, ok := item.(string); ok { + config.Allowed = append(config.Allowed, str) + } + } + } + + if mode, ok := configMap["mode"].(string); ok { + config.Mode = mode + } + + if version, ok := configMap["version"].(string); ok { + config.Version = version + } + + if args, ok := configMap["args"].([]any); ok { + config.Args = make([]string, 0, len(args)) + for _, item := range args { + if str, ok := item.(string); ok { + config.Args = append(config.Args, str) + } + } + } + + if readOnly, ok := configMap["read-only"].(bool); ok { + config.ReadOnly = readOnly + } + // else: defaults to true (set above) + + if token, ok := configMap["github-token"].(string); ok { + config.GitHubToken = token + } + + // Check for both "toolset" and "toolsets" (plural is more common in user configs) + if toolset, ok := configMap["toolsets"].([]any); ok { + config.Toolset = make([]string, 0, len(toolset)) + for _, item := range toolset { + if str, ok := item.(string); ok { + config.Toolset = append(config.Toolset, str) + } + } + } else if toolset, ok := configMap["toolset"].([]any); ok { + config.Toolset = make([]string, 0, len(toolset)) + for _, item := range toolset { + if str, ok := item.(string); ok { + config.Toolset = append(config.Toolset, str) + } + } + } + + if lockdown, ok := configMap["lockdown"].(bool); ok { + config.Lockdown = lockdown + } + + return config + } + + return &GitHubToolConfig{ + ReadOnly: true, // default to read-only for security + } +} + +// parseBashTool converts raw bash tool configuration to BashToolConfig +func parseBashTool(val any) *BashToolConfig { + if val == nil { + // nil means all commands allowed + return &BashToolConfig{} + } + + // Handle array of allowed commands + if cmdArray, ok := val.([]any); ok { + config := &BashToolConfig{ + AllowedCommands: make([]string, 0, len(cmdArray)), + } + for _, item := range cmdArray { + if str, ok := item.(string); ok { + config.AllowedCommands = append(config.AllowedCommands, str) + } + } + return config + } + + return &BashToolConfig{} +} + +// parsePlaywrightTool converts raw playwright tool configuration to PlaywrightToolConfig +func parsePlaywrightTool(val any) *PlaywrightToolConfig { + if val == nil { + return &PlaywrightToolConfig{} + } + + if configMap, ok := val.(map[string]any); ok { + config := &PlaywrightToolConfig{} + + if version, ok := configMap["version"].(string); ok { + config.Version = version + } + + // Handle allowed_domains - can be string or array + if allowedDomains, ok := configMap["allowed_domains"]; ok { + if str, ok := allowedDomains.(string); ok { + config.AllowedDomains = []string{str} + } else if arr, ok := allowedDomains.([]any); ok { + config.AllowedDomains = make([]string, 0, len(arr)) + for _, item := range arr { + if str, ok := item.(string); ok { + config.AllowedDomains = append(config.AllowedDomains, str) + } + } + } + } + + if args, ok := configMap["args"].([]any); ok { + config.Args = make([]string, 0, len(args)) + for _, item := range args { + if str, ok := item.(string); ok { + config.Args = append(config.Args, str) + } + } + } + + return config + } + + return &PlaywrightToolConfig{} +} + +// parseSerenaTool converts raw serena tool configuration to SerenaToolConfig +func parseSerenaTool(val any) *SerenaToolConfig { + if val == nil { + return &SerenaToolConfig{} + } + + // Handle array format (short syntax): ["go", "typescript"] + if langArray, ok := val.([]any); ok { + config := &SerenaToolConfig{ + ShortSyntax: make([]string, 0, len(langArray)), + } + for _, item := range langArray { + if str, ok := item.(string); ok { + config.ShortSyntax = append(config.ShortSyntax, str) + } + } + return config + } + + // Handle object format with detailed configuration + if configMap, ok := val.(map[string]any); ok { + config := &SerenaToolConfig{} + + if version, ok := configMap["version"].(string); ok { + config.Version = version + } + + if args, ok := configMap["args"].([]any); ok { + config.Args = make([]string, 0, len(args)) + for _, item := range args { + if str, ok := item.(string); ok { + config.Args = append(config.Args, str) + } + } + } + + // Parse languages configuration + if languagesVal, ok := configMap["languages"].(map[string]any); ok { + config.Languages = make(map[string]*SerenaLangConfig) + for langName, langVal := range languagesVal { + if langVal == nil { + // nil means enable with defaults + config.Languages[langName] = &SerenaLangConfig{} + continue + } + if langMap, ok := langVal.(map[string]any); ok { + langConfig := &SerenaLangConfig{} + if version, ok := langMap["version"].(string); ok { + langConfig.Version = version + } else if versionNum, ok := langMap["version"].(float64); ok { + // Convert numeric version to string + langConfig.Version = fmt.Sprintf("%.0f", versionNum) + } + // Parse Go-specific fields + if langName == "go" { + if goModFile, ok := langMap["go-mod-file"].(string); ok { + langConfig.GoModFile = goModFile + } + if goplsVersion, ok := langMap["gopls-version"].(string); ok { + langConfig.GoplsVersion = goplsVersion + } + } + config.Languages[langName] = langConfig + } + } + } + + return config + } + + return &SerenaToolConfig{} +} + +// parseWebFetchTool converts raw web-fetch tool configuration +func parseWebFetchTool(val any) *WebFetchToolConfig { + // web-fetch is either nil or an empty object + return &WebFetchToolConfig{} +} + +// parseWebSearchTool converts raw web-search tool configuration +func parseWebSearchTool(val any) *WebSearchToolConfig { + // web-search is either nil or an empty object + return &WebSearchToolConfig{} +} + +// parseEditTool converts raw edit tool configuration +func parseEditTool(val any) *EditToolConfig { + // edit is either nil or an empty object + return &EditToolConfig{} +} + +// parseAgenticWorkflowsTool converts raw agentic-workflows tool configuration +func parseAgenticWorkflowsTool(val any) *AgenticWorkflowsToolConfig { + config := &AgenticWorkflowsToolConfig{} + + if boolVal, ok := val.(bool); ok { + config.Enabled = boolVal + } else if val == nil { + config.Enabled = true // nil means enabled + } + + return config +} + +// parseCacheMemoryTool converts raw cache-memory tool configuration +func parseCacheMemoryTool(val any) *CacheMemoryToolConfig { + // cache-memory can be boolean, object, or array - store raw value + return &CacheMemoryToolConfig{Raw: val} +} + +// parseRepoMemoryTool converts raw repo-memory tool configuration +func parseRepoMemoryTool(val any) *RepoMemoryToolConfig { + // repo-memory can be boolean, object, or array - store raw value + return &RepoMemoryToolConfig{Raw: val} +} + +// parseMCPGatewayTool converts raw mcp-gateway tool configuration +func parseMCPGatewayTool(val any) *MCPGatewayConfig { + if val == nil { + return nil + } + + configMap, ok := val.(map[string]any) + if !ok { + return nil + } + + config := &MCPGatewayConfig{ + Port: DefaultMCPGatewayPort, + } + + if container, ok := configMap["container"].(string); ok { + config.Container = container + } + if version, ok := configMap["version"].(string); ok { + config.Version = version + } else if versionNum, ok := configMap["version"].(float64); ok { + config.Version = fmt.Sprintf("%.0f", versionNum) + } + if args, ok := configMap["args"].([]any); ok { + config.Args = make([]string, 0, len(args)) + for _, arg := range args { + if str, ok := arg.(string); ok { + config.Args = append(config.Args, str) + } + } + } + if entrypointArgs, ok := configMap["entrypointArgs"].([]any); ok { + config.EntrypointArgs = make([]string, 0, len(entrypointArgs)) + for _, arg := range entrypointArgs { + if str, ok := arg.(string); ok { + config.EntrypointArgs = append(config.EntrypointArgs, str) + } + } + } + if env, ok := configMap["env"].(map[string]any); ok { + config.Env = make(map[string]string) + for k, v := range env { + if str, ok := v.(string); ok { + config.Env[k] = str + } + } + } + if port, ok := configMap["port"].(int); ok { + config.Port = port + } else if portFloat, ok := configMap["port"].(float64); ok { + config.Port = int(portFloat) + } + if apiKey, ok := configMap["api-key"].(string); ok { + config.APIKey = apiKey + } + + return config +} + +// parseSafetyPromptTool converts raw safety-prompt tool configuration +func parseSafetyPromptTool(val any) *bool { + if boolVal, ok := val.(bool); ok { + return &boolVal + } + // Default to true if not specified or invalid type + defaultVal := true + return &defaultVal +} + +// parseTimeoutTool converts raw timeout tool configuration +func parseTimeoutTool(val any) *int { + if intVal, ok := val.(int); ok { + return &intVal + } + if floatVal, ok := val.(float64); ok { + intVal := int(floatVal) + return &intVal + } + return nil +} + +// parseStartupTimeoutTool converts raw startup-timeout tool configuration +func parseStartupTimeoutTool(val any) *int { + if intVal, ok := val.(int); ok { + return &intVal + } + if floatVal, ok := val.(float64); ok { + intVal := int(floatVal) + return &intVal + } + return nil +} diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 8538cc794f..5138ef3d66 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -1,13 +1,5 @@ package workflow -import ( - "fmt" - - "github.com/githubnext/gh-aw/pkg/logger" -) - -var toolsTypesLog = logger.New("workflow:tools_types") - // ToolsConfig represents the unified configuration for all tools in a workflow. // This type provides a structured alternative to the pervasive map[string]any pattern. // It includes strongly-typed fields for built-in tools and a flexible Custom map for @@ -250,458 +242,6 @@ type MCPGatewayConfig struct { APIKey string `yaml:"api-key,omitempty"` // API key for gateway authentication } -// NewTools creates a new Tools instance from a map -func NewTools(toolsMap map[string]any) *Tools { - toolsTypesLog.Printf("Creating tools configuration from map with %d entries", len(toolsMap)) - if toolsMap == nil { - return &Tools{ - Custom: make(map[string]any), - raw: make(map[string]any), - } - } - - tools := &Tools{ - Custom: make(map[string]any), - raw: make(map[string]any), - } - - // Copy raw map - for k, v := range toolsMap { - tools.raw[k] = v - } - - // Extract and parse known tools - if val, exists := toolsMap["github"]; exists { - tools.GitHub = parseGitHubTool(val) - } - if val, exists := toolsMap["bash"]; exists { - tools.Bash = parseBashTool(val) - } - if val, exists := toolsMap["web-fetch"]; exists { - tools.WebFetch = parseWebFetchTool(val) - } - if val, exists := toolsMap["web-search"]; exists { - tools.WebSearch = parseWebSearchTool(val) - } - if val, exists := toolsMap["edit"]; exists { - tools.Edit = parseEditTool(val) - } - if val, exists := toolsMap["playwright"]; exists { - tools.Playwright = parsePlaywrightTool(val) - } - if val, exists := toolsMap["serena"]; exists { - tools.Serena = parseSerenaTool(val) - } - if val, exists := toolsMap["agentic-workflows"]; exists { - tools.AgenticWorkflows = parseAgenticWorkflowsTool(val) - } - if val, exists := toolsMap["cache-memory"]; exists { - tools.CacheMemory = parseCacheMemoryTool(val) - } - if val, exists := toolsMap["repo-memory"]; exists { - tools.RepoMemory = parseRepoMemoryTool(val) - } - if val, exists := toolsMap["safety-prompt"]; exists { - tools.SafetyPrompt = parseSafetyPromptTool(val) - } - if val, exists := toolsMap["timeout"]; exists { - tools.Timeout = parseTimeoutTool(val) - } - if val, exists := toolsMap["startup-timeout"]; exists { - tools.StartupTimeout = parseStartupTimeoutTool(val) - } - - // Extract custom MCP tools (anything not in the known list) - knownTools := map[string]bool{ - "github": true, - "bash": true, - "web-fetch": true, - "web-search": true, - "edit": true, - "playwright": true, - "serena": true, - "agentic-workflows": true, - "cache-memory": true, - "repo-memory": true, - "safety-prompt": true, - "timeout": true, - "startup-timeout": true, - } - - customCount := 0 - for name, config := range toolsMap { - if !knownTools[name] { - tools.Custom[name] = config - customCount++ - } - } - - toolsTypesLog.Printf("Parsed tools: github=%v, bash=%v, playwright=%v, serena=%v, custom=%d", tools.GitHub != nil, tools.Bash != nil, tools.Playwright != nil, tools.Serena != nil, customCount) - return tools -} - -// parseGitHubTool converts raw github tool configuration to GitHubToolConfig -func parseGitHubTool(val any) *GitHubToolConfig { - if val == nil { - toolsTypesLog.Print("GitHub tool enabled with default configuration") - return &GitHubToolConfig{ - ReadOnly: true, // default to read-only for security - } - } - - // Handle string type (simple enable) - if _, ok := val.(string); ok { - toolsTypesLog.Print("GitHub tool enabled with string configuration") - return &GitHubToolConfig{ - ReadOnly: true, // default to read-only for security - } - } - - // Handle map type (detailed configuration) - if configMap, ok := val.(map[string]any); ok { - toolsTypesLog.Print("Parsing GitHub tool detailed configuration") - config := &GitHubToolConfig{ - ReadOnly: true, // default to read-only for security - } - - if allowed, ok := configMap["allowed"].([]any); ok { - config.Allowed = make([]string, 0, len(allowed)) - for _, item := range allowed { - if str, ok := item.(string); ok { - config.Allowed = append(config.Allowed, str) - } - } - } - - if mode, ok := configMap["mode"].(string); ok { - config.Mode = mode - } - - if version, ok := configMap["version"].(string); ok { - config.Version = version - } - - if args, ok := configMap["args"].([]any); ok { - config.Args = make([]string, 0, len(args)) - for _, item := range args { - if str, ok := item.(string); ok { - config.Args = append(config.Args, str) - } - } - } - - if readOnly, ok := configMap["read-only"].(bool); ok { - config.ReadOnly = readOnly - } - // else: defaults to true (set above) - - if token, ok := configMap["github-token"].(string); ok { - config.GitHubToken = token - } - - // Check for both "toolset" and "toolsets" (plural is more common in user configs) - if toolset, ok := configMap["toolsets"].([]any); ok { - config.Toolset = make([]string, 0, len(toolset)) - for _, item := range toolset { - if str, ok := item.(string); ok { - config.Toolset = append(config.Toolset, str) - } - } - } else if toolset, ok := configMap["toolset"].([]any); ok { - config.Toolset = make([]string, 0, len(toolset)) - for _, item := range toolset { - if str, ok := item.(string); ok { - config.Toolset = append(config.Toolset, str) - } - } - } - - if lockdown, ok := configMap["lockdown"].(bool); ok { - config.Lockdown = lockdown - } - - return config - } - - return &GitHubToolConfig{ - ReadOnly: true, // default to read-only for security - } -} - -// parseBashTool converts raw bash tool configuration to BashToolConfig -func parseBashTool(val any) *BashToolConfig { - if val == nil { - // nil means all commands allowed - return &BashToolConfig{} - } - - // Handle array of allowed commands - if cmdArray, ok := val.([]any); ok { - config := &BashToolConfig{ - AllowedCommands: make([]string, 0, len(cmdArray)), - } - for _, item := range cmdArray { - if str, ok := item.(string); ok { - config.AllowedCommands = append(config.AllowedCommands, str) - } - } - return config - } - - return &BashToolConfig{} -} - -// parsePlaywrightTool converts raw playwright tool configuration to PlaywrightToolConfig -func parsePlaywrightTool(val any) *PlaywrightToolConfig { - if val == nil { - return &PlaywrightToolConfig{} - } - - if configMap, ok := val.(map[string]any); ok { - config := &PlaywrightToolConfig{} - - if version, ok := configMap["version"].(string); ok { - config.Version = version - } - - // Handle allowed_domains - can be string or array - if allowedDomains, ok := configMap["allowed_domains"]; ok { - if str, ok := allowedDomains.(string); ok { - config.AllowedDomains = []string{str} - } else if arr, ok := allowedDomains.([]any); ok { - config.AllowedDomains = make([]string, 0, len(arr)) - for _, item := range arr { - if str, ok := item.(string); ok { - config.AllowedDomains = append(config.AllowedDomains, str) - } - } - } - } - - if args, ok := configMap["args"].([]any); ok { - config.Args = make([]string, 0, len(args)) - for _, item := range args { - if str, ok := item.(string); ok { - config.Args = append(config.Args, str) - } - } - } - - return config - } - - return &PlaywrightToolConfig{} -} - -// parseSerenaTool converts raw serena tool configuration to SerenaToolConfig -func parseSerenaTool(val any) *SerenaToolConfig { - if val == nil { - return &SerenaToolConfig{} - } - - // Handle array format (short syntax): ["go", "typescript"] - if langArray, ok := val.([]any); ok { - config := &SerenaToolConfig{ - ShortSyntax: make([]string, 0, len(langArray)), - } - for _, item := range langArray { - if str, ok := item.(string); ok { - config.ShortSyntax = append(config.ShortSyntax, str) - } - } - return config - } - - // Handle object format with detailed configuration - if configMap, ok := val.(map[string]any); ok { - config := &SerenaToolConfig{} - - if version, ok := configMap["version"].(string); ok { - config.Version = version - } - - if args, ok := configMap["args"].([]any); ok { - config.Args = make([]string, 0, len(args)) - for _, item := range args { - if str, ok := item.(string); ok { - config.Args = append(config.Args, str) - } - } - } - - // Parse languages configuration - if languagesVal, ok := configMap["languages"].(map[string]any); ok { - config.Languages = make(map[string]*SerenaLangConfig) - for langName, langVal := range languagesVal { - if langVal == nil { - // nil means enable with defaults - config.Languages[langName] = &SerenaLangConfig{} - continue - } - if langMap, ok := langVal.(map[string]any); ok { - langConfig := &SerenaLangConfig{} - if version, ok := langMap["version"].(string); ok { - langConfig.Version = version - } else if versionNum, ok := langMap["version"].(float64); ok { - // Convert numeric version to string - langConfig.Version = fmt.Sprintf("%.0f", versionNum) - } - // Parse Go-specific fields - if langName == "go" { - if goModFile, ok := langMap["go-mod-file"].(string); ok { - langConfig.GoModFile = goModFile - } - if goplsVersion, ok := langMap["gopls-version"].(string); ok { - langConfig.GoplsVersion = goplsVersion - } - } - config.Languages[langName] = langConfig - } - } - } - - return config - } - - return &SerenaToolConfig{} -} - -// parseWebFetchTool converts raw web-fetch tool configuration -func parseWebFetchTool(val any) *WebFetchToolConfig { - // web-fetch is either nil or an empty object - return &WebFetchToolConfig{} -} - -// parseWebSearchTool converts raw web-search tool configuration -func parseWebSearchTool(val any) *WebSearchToolConfig { - // web-search is either nil or an empty object - return &WebSearchToolConfig{} -} - -// parseEditTool converts raw edit tool configuration -func parseEditTool(val any) *EditToolConfig { - // edit is either nil or an empty object - return &EditToolConfig{} -} - -// parseAgenticWorkflowsTool converts raw agentic-workflows tool configuration -func parseAgenticWorkflowsTool(val any) *AgenticWorkflowsToolConfig { - config := &AgenticWorkflowsToolConfig{} - - if boolVal, ok := val.(bool); ok { - config.Enabled = boolVal - } else if val == nil { - config.Enabled = true // nil means enabled - } - - return config -} - -// parseCacheMemoryTool converts raw cache-memory tool configuration -func parseCacheMemoryTool(val any) *CacheMemoryToolConfig { - // cache-memory can be boolean, object, or array - store raw value - return &CacheMemoryToolConfig{Raw: val} -} - -// parseRepoMemoryTool converts raw repo-memory tool configuration -func parseRepoMemoryTool(val any) *RepoMemoryToolConfig { - // repo-memory can be boolean, object, or array - store raw value - return &RepoMemoryToolConfig{Raw: val} -} - -// parseMCPGatewayTool converts raw mcp-gateway tool configuration -func parseMCPGatewayTool(val any) *MCPGatewayConfig { - if val == nil { - return nil - } - - configMap, ok := val.(map[string]any) - if !ok { - return nil - } - - config := &MCPGatewayConfig{ - Port: DefaultMCPGatewayPort, - } - - if container, ok := configMap["container"].(string); ok { - config.Container = container - } - if version, ok := configMap["version"].(string); ok { - config.Version = version - } else if versionNum, ok := configMap["version"].(float64); ok { - config.Version = fmt.Sprintf("%.0f", versionNum) - } - if args, ok := configMap["args"].([]any); ok { - config.Args = make([]string, 0, len(args)) - for _, arg := range args { - if str, ok := arg.(string); ok { - config.Args = append(config.Args, str) - } - } - } - if entrypointArgs, ok := configMap["entrypointArgs"].([]any); ok { - config.EntrypointArgs = make([]string, 0, len(entrypointArgs)) - for _, arg := range entrypointArgs { - if str, ok := arg.(string); ok { - config.EntrypointArgs = append(config.EntrypointArgs, str) - } - } - } - if env, ok := configMap["env"].(map[string]any); ok { - config.Env = make(map[string]string) - for k, v := range env { - if str, ok := v.(string); ok { - config.Env[k] = str - } - } - } - if port, ok := configMap["port"].(int); ok { - config.Port = port - } else if portFloat, ok := configMap["port"].(float64); ok { - config.Port = int(portFloat) - } - if apiKey, ok := configMap["api-key"].(string); ok { - config.APIKey = apiKey - } - - return config -} - -// parseSafetyPromptTool converts raw safety-prompt tool configuration -func parseSafetyPromptTool(val any) *bool { - if boolVal, ok := val.(bool); ok { - return &boolVal - } - // Default to true if not specified or invalid type - defaultVal := true - return &defaultVal -} - -// parseTimeoutTool converts raw timeout tool configuration -func parseTimeoutTool(val any) *int { - if intVal, ok := val.(int); ok { - return &intVal - } - if floatVal, ok := val.(float64); ok { - intVal := int(floatVal) - return &intVal - } - return nil -} - -// parseStartupTimeoutTool converts raw startup-timeout tool configuration -func parseStartupTimeoutTool(val any) *int { - if intVal, ok := val.(int); ok { - return &intVal - } - if floatVal, ok := val.(float64); ok { - intVal := int(floatVal) - return &intVal - } - return nil -} - // HasTool checks if a tool is present in the configuration func (t *Tools) HasTool(name string) bool { if t == nil {