diff --git a/README.md b/README.md index d97d016..2e73a81 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,12 @@ A powerful CLI tool for managing multiple Claude Code configurations with ease. - **Smart Directory Launch**: Remember last working directory and support project aliases for quick access - **Environment Import**: Automatically detect and import existing Claude configurations from environment variables - **Automatic Installation**: Automatically installs and updates Claude CLI when needed -- **API Validation**: Tests API connectivity before saving configurations +- **API Validation**: Tests API connectivity before saving configurations and provides testing tools - **Cross-Platform**: Support for Linux, macOS, and Windows (amd64 & arm64) - **Zero Configuration**: Works out of the box with sensible defaults +- **Configuration Management**: Global configuration settings for CLI behavior +- **Permission Control**: Manage --dangerously-skip-permissions flag for Claude CLI +- **Flexible Startup Behavior**: Control where Claude starts when no arguments provided ## Installation @@ -167,6 +170,105 @@ Lists the latest 20 available versions from npm and allows you to: - Enter a specific version number (e.g., `1.2.3`) - Type `latest` to install the newest version +### `codes test` + +Test API configuration connectivity and validate configurations. + +```bash +# Test all configured API endpoints +codes test + +# Test a specific configuration +codes test my-config +``` + +**Features**: +- Tests API connectivity using actual Claude API endpoint (`/v1/messages`) +- Shows model being used for each test +- Updates configuration status (active/inactive) based on test results +- Displays test results with clear success/failure indicators +- Tests all environment variables including authentication tokens and base URLs +- Validates that required fields are present and properly formatted + +### `codes config` + +Manage global CLI configuration settings. + +```bash +# Show all configuration values +codes config get + +# Show specific configuration value +codes config get + +# Set configuration value +codes config set +``` + +**Current Configuration Keys**: +- `defaultBehavior` - Controls where Claude starts when no arguments are provided + +**Environment Variables Managed**: +- While the config command itself doesn't directly manage environment variables, it works with configurations that contain environment variables for: + - `ANTHROPIC_BASE_URL` - API endpoint URL + - `ANTHROPIC_AUTH_TOKEN` - Authentication token + - `ANTHROPIC_MODEL` - Default model + - `ANTHROPIC_DEFAULT_HAIKU_MODEL` - Haiku model override + - `ANTHROPIC_DEFAULT_OPUS_MODEL` - Opus model override + - `ANTHROPIC_DEFAULT_SONNET_MODEL` - Sonnet model override + - And other Claude CLI recognized environment variables + +### `codes defaultbehavior` + +Manage the default startup behavior when running `codes` without arguments. + +```bash +# Show current default behavior +codes defaultbehavior get + +# Set default behavior +codes defaultbehavior set + +# Reset to default +codes defaultbehavior reset +``` + +**Available Behaviors**: +- `current` - Start Claude in the current working directory (default) +- `last` - Start Claude in the last used directory (remembered from previous runs) +- `home` - Start Claude in the user's home directory + +**Important Details**: +- Behavior is saved in config.json under `DefaultBehavior` field +- When `codes` is run without arguments, it uses this setting to determine the working directory +- The `codes start` command still works with project aliases and specific paths + +### `codes skippermissions` + +Manage the global `--dangerously-skip-permissions` flag setting for Claude CLI. + +```bash +# Show current global skipPermissions setting +codes skippermissions get + +# Set global skipPermissions +codes skippermissions set + +# Reset to default +codes skippermissions reset +``` + +**How It Works**: +- Global setting applies to all configurations that don't have their own `skipPermissions` setting +- When `true`, Claude runs with `--dangerously-skip-permissions` flag +- When `false` (default), Claude runs without the flag +- Individual configurations can override this global setting during `codes add` or by editing the config file + +**Important Details**: +- Setting is stored in config.json under `SkipPermissions` field +- Individual configurations can have their own `skipPermissions` setting that takes precedence +- This controls whether Claude bypasses certain security checks for file system access + ### `codes install` Install the codes binary to your system PATH. @@ -243,26 +345,42 @@ Create a `config.json` file with the following structure: "configs": [ { "name": "official", - "ANTHROPIC_BASE_URL": "https://api.anthropic.com", - "ANTHROPIC_AUTH_TOKEN": "sk-ant-xxxxx" - }, - { - "name": "proxy", - "ANTHROPIC_BASE_URL": "https://your-proxy.com/api", - "ANTHROPIC_AUTH_TOKEN": "your-token" + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com", + "ANTHROPIC_AUTH_TOKEN": "sk-ant-xxxxx", + "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022" + }, + "skipPermissions": false } ], - "default": "official" + "default": "official", + "skipPermissions": false, + "defaultBehavior": "current", + "projects": { + "my-project": "/path/to/project" + }, + "lastWorkDir": "/path/to/last/directory" } ``` ### Configuration Fields +#### Global Settings +- `default`: The configuration name to use by default +- `skipPermissions`: Global setting for `--dangerously-skip-permissions` flag (applies to all configs unless overridden) +- `defaultBehavior`: Controls where Claude starts when no arguments provided ("current", "last", "home") +- `projects`: Object mapping project names to their directory paths +- `lastWorkDir`: Last working directory remembered from previous runs + +#### Configuration Object - `name`: Unique identifier for the configuration -- `ANTHROPIC_BASE_URL`: Base URL for the Claude API endpoint -- `ANTHROPIC_AUTH_TOKEN`: Authentication token for the API +- `env`: Object containing environment variables for Claude CLI: + - `ANTHROPIC_BASE_URL`: Base URL for the Claude API endpoint + - `ANTHROPIC_AUTH_TOKEN`: Authentication token for the API + - `ANTHROPIC_MODEL`: Default model to use + - And other Claude CLI recognized environment variables +- `skipPermissions`: Per-configuration setting that overrides global `skipPermissions` - `status`: (optional) API status - "active", "inactive", or "unknown" -- `default`: The configuration name to use by default ### Example Configurations diff --git a/cmd/codes/main.go b/cmd/codes/main.go index be6e0bb..0512563 100644 --- a/cmd/codes/main.go +++ b/cmd/codes/main.go @@ -20,16 +20,20 @@ func init() { rootCmd.AddCommand(commands.InstallCmd) rootCmd.AddCommand(commands.AddCmd) rootCmd.AddCommand(commands.SelectCmd) + rootCmd.AddCommand(commands.TestCmd) rootCmd.AddCommand(commands.UpdateCmd) rootCmd.AddCommand(commands.VersionCmd) rootCmd.AddCommand(commands.StartCmd) rootCmd.AddCommand(commands.ProjectCmd) + rootCmd.AddCommand(commands.ConfigCmd) + rootCmd.AddCommand(commands.DefaultBehaviorCmd) + rootCmd.AddCommand(commands.SkipPermissionsCmd) // 设置默认运行时行为 - 现在使用智能启动 rootCmd.Run = func(cmd *cobra.Command, args []string) { // Check if claude is installed if _, err := exec.LookPath("claude"); err != nil { - commands.RunClaudeWithConfig(nil) + commands.RunClaudeWithConfig([]string{}) return } // 使用 start 命令的逻辑,支持目录记忆 diff --git a/config.json.example b/config.json.example index 34e2ee9..18b1bdd 100644 --- a/config.json.example +++ b/config.json.example @@ -2,19 +2,41 @@ "configs": [ { "name": "official", - "ANTHROPIC_BASE_URL": "https://api.anthropic.com", - "ANTHROPIC_AUTH_TOKEN": "sk-ant-xxxxxxxxxxxxxxxxxxxxx" + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com", + "ANTHROPIC_AUTH_TOKEN": "sk-ant-xxxxxxxxxxxxxxxxxxxxx", + "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022", + "MAX_THINKING_TOKENS": "32768" + }, + "skipPermissions": false }, { "name": "proxy", - "ANTHROPIC_BASE_URL": "https://your-proxy.example.com/api", - "ANTHROPIC_AUTH_TOKEN": "your-proxy-token" + "env": { + "ANTHROPIC_BASE_URL": "https://your-proxy.example.com/api", + "ANTHROPIC_AUTH_TOKEN": "your-proxy-token", + "ANTHROPIC_API_KEY": "your-proxy-api-key", + "HTTP_PROXY": "http://proxy.example.com:8080", + "HTTPS_PROXY": "http://proxy.example.com:8080", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-3-haiku-20240307-custom", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-3-sonnet-20240229-custom" + }, + "skipPermissions": true }, { - "name": "alternative", - "ANTHROPIC_BASE_URL": "https://api.alternative-provider.com", - "ANTHROPIC_AUTH_TOKEN": "your-alternative-token" + "name": "custom-model", + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com", + "ANTHROPIC_AUTH_TOKEN": "sk-ant-xxxxxxxxxxxxxxxxxxxxx", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "my-custom-haiku-model", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "my-custom-sonnet-model", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "my-custom-opus-model", + "CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "3600000", + "MAX_THINKING_TOKENS": "65536" + } } ], - "default": "official" + "default": "official", + "skipPermissions": false, + "defaultBehavior": "current" } diff --git a/internal/commands/cobra.go b/internal/commands/cobra.go index f97cd83..be79a10 100644 --- a/internal/commands/cobra.go +++ b/internal/commands/cobra.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "codes/internal/ui" + "strings" ) // InitCmd represents the init command @@ -48,6 +49,17 @@ var SelectCmd = &cobra.Command{ }, } +// TestCmd represents the test command +var TestCmd = &cobra.Command{ + Use: "test [config-name]", + Short: "Test API configuration", + Long: "Test API connectivity for all configurations or a specific one", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + RunTest(args) + }, +} + // UpdateCmd represents the update command var UpdateCmd = &cobra.Command{ Use: "update", @@ -100,6 +112,122 @@ var ProjectCmd = &cobra.Command{ Long: "Add, remove, or list project aliases for quick access", } +// ConfigCmd represents the config command +var ConfigCmd = &cobra.Command{ + Use: "config", + Short: "Manage configuration", + Long: "Configure codes CLI settings", +} + +// ConfigSetCmd represents the config set command +var ConfigSetCmd = &cobra.Command{ + Use: "set ", + Short: "Set a configuration value", + Long: "Set a configuration value (keys: defaultBehavior)", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + RunConfigSet(args[0], args[1]) + }, +} + +// ConfigGetCmd represents the config get command +var ConfigGetCmd = &cobra.Command{ + Use: "get [key]", + Short: "Get configuration values", + Long: "Get configuration values. If no key is specified, show all configuration", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + RunConfigGet(args) + }, +} + +// DefaultBehaviorCmd represents the default behavior command +var DefaultBehaviorCmd = &cobra.Command{ + Use: "defaultbehavior", + Short: "Manage default behavior setting", + Long: "Configure what directory to use when starting Claude without arguments", +} + +// DefaultBehaviorSetCmd represents the defaultbehavior set command +var DefaultBehaviorSetCmd = &cobra.Command{ + Use: "set ", + Short: "Set the default behavior", + Long: "Set the default startup behavior: 'current' (current directory), 'last' (last used directory), 'home' (home directory)", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + RunDefaultBehaviorSet(args[0]) + }, +} + +// DefaultBehaviorGetCmd represents the defaultbehavior get command +var DefaultBehaviorGetCmd = &cobra.Command{ + Use: "get", + Short: "Get the current default behavior", + Long: "Show the current default behavior setting", + Run: func(cmd *cobra.Command, args []string) { + RunDefaultBehaviorGet() + }, +} + +// DefaultBehaviorResetCmd represents the defaultbehavior reset command +var DefaultBehaviorResetCmd = &cobra.Command{ + Use: "reset", + Short: "Reset to default behavior", + Long: "Reset the default behavior to 'current' (default)", + Run: func(cmd *cobra.Command, args []string) { + RunDefaultBehaviorReset() + }, +} + +// SkipPermissionsCmd represents the skippermissions command +var SkipPermissionsCmd = &cobra.Command{ + Use: "skippermissions", + Short: "Manage global skipPermissions setting", + Long: "Configure the global skipPermissions setting for all Claude configurations", +} + +// SkipPermissionsSetCmd represents the skippermissions set command +var SkipPermissionsSetCmd = &cobra.Command{ + Use: "set ", + Short: "Set the global skipPermissions", + Long: "Set whether to use --dangerously-skip-permissions for all configurations that don't have their own setting", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + value := strings.ToLower(args[0]) + var skip bool + switch value { + case "true", "t", "yes", "y", "1": + skip = true + case "false", "f", "no", "n", "0": + skip = false + default: + ui.ShowError("Invalid value. Must be 'true' or 'false' (case-insensitive)", nil) + return + } + RunSkipPermissionsSet(skip) + }, +} + +// SkipPermissionsGetCmd represents the skippermissions get command +var SkipPermissionsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get the global skipPermissions setting", + Long: "Show the current global skipPermissions setting", + Run: func(cmd *cobra.Command, args []string) { + RunSkipPermissionsGet() + }, +} + +// SkipPermissionsResetCmd represents the skippermissions reset command +var SkipPermissionsResetCmd = &cobra.Command{ + Use: "reset", + Short: "Reset global skipPermissions", + Long: "Reset the global skipPermissions to false (default)", + Run: func(cmd *cobra.Command, args []string) { + RunSkipPermissionsReset() + }, +} + // ProjectAddCmd represents the project add command var ProjectAddCmd = &cobra.Command{ Use: "add ", @@ -136,4 +264,15 @@ func init() { ProjectCmd.AddCommand(ProjectAddCmd) ProjectCmd.AddCommand(ProjectRemoveCmd) ProjectCmd.AddCommand(ProjectListCmd) + + ConfigCmd.AddCommand(ConfigSetCmd) + ConfigCmd.AddCommand(ConfigGetCmd) + + DefaultBehaviorCmd.AddCommand(DefaultBehaviorSetCmd) + DefaultBehaviorCmd.AddCommand(DefaultBehaviorGetCmd) + DefaultBehaviorCmd.AddCommand(DefaultBehaviorResetCmd) + + SkipPermissionsCmd.AddCommand(SkipPermissionsSetCmd) + SkipPermissionsCmd.AddCommand(SkipPermissionsGetCmd) + SkipPermissionsCmd.AddCommand(SkipPermissionsResetCmd) } diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 0c4fcd1..f2ddc82 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -40,25 +40,30 @@ func RunSelect() { fmt.Println() for i, c := range cfg.Configs { + apiURL := c.Env["ANTHROPIC_BASE_URL"] + if apiURL == "" { + apiURL = "unknown" + } + if c.Name == cfg.Default { if c.Status == "active" { - ui.ShowCurrentConfig(i+1, c.Name, c.AnthropicBaseURL) + ui.ShowCurrentConfig(i+1, c.Name, apiURL) ui.ShowInfo(" Status: Active") } else if c.Status == "inactive" { - ui.ShowCurrentConfig(i+1, c.Name, c.AnthropicBaseURL) + ui.ShowCurrentConfig(i+1, c.Name, apiURL) ui.ShowWarning(" Status: Inactive") } else { - ui.ShowCurrentConfig(i+1, c.Name, c.AnthropicBaseURL) + ui.ShowCurrentConfig(i+1, c.Name, apiURL) } } else { if c.Status == "active" { - ui.ShowConfigOption(i+1, c.Name, c.AnthropicBaseURL) + ui.ShowConfigOption(i+1, c.Name, apiURL) ui.ShowInfo(" Status: Active") } else if c.Status == "inactive" { - ui.ShowConfigOption(i+1, c.Name, c.AnthropicBaseURL) + ui.ShowConfigOption(i+1, c.Name, apiURL) ui.ShowWarning(" Status: Inactive") } else { - ui.ShowConfigOption(i+1, c.Name, c.AnthropicBaseURL) + ui.ShowConfigOption(i+1, c.Name, apiURL) } } } @@ -88,7 +93,11 @@ func RunSelect() { } ui.ShowSuccess("Selected: %s", selectedConfig.Name) - ui.ShowInfo("API: %s", selectedConfig.AnthropicBaseURL) + apiURL := selectedConfig.Env["ANTHROPIC_BASE_URL"] + if apiURL == "" { + apiURL = "unknown" + } + ui.ShowInfo("API: %s", apiURL) // 立即启动Claude RunClaudeWithConfig([]string{}) @@ -209,48 +218,158 @@ func RunAdd() { } } - // 获取API URL - fmt.Print("Enter ANTHROPIC_BASE_URL: ") + // 创建新的API配置 + newConfig := config.APIConfig{ + Name: name, + Env: make(map[string]string), + } + + // 显示常用环境变量提示 + defaultVars := config.GetDefaultEnvironmentVars() + + // 基本必需环境变量 + fmt.Println("\nBasic Configuration:") + ui.ShowInfo("Enter values for required environment variables.") + + // 获取ANTHROPIC_BASE_URL(必需) + fmt.Print("Enter ANTHROPIC_BASE_URL (required): ") baseURL, _ := reader.ReadString('\n') baseURL = strings.TrimSpace(baseURL) if baseURL == "" { ui.ShowError("Base URL cannot be empty", nil) return } - - // 验证URL格式 - if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { - ui.ShowError("Invalid URL format. Must start with http:// or https://", nil) + newConfig.Env["ANTHROPIC_BASE_URL"] = baseURL + + // 获取认证令牌(必需) + fmt.Print("Enter ANTHROPIC_AUTH_TOKEN (required): ") + authToken, _ := reader.ReadString('\n') + authToken = strings.TrimSpace(authToken) + if authToken == "" { + ui.ShowError("Authentication token cannot be empty", nil) return } + newConfig.Env["ANTHROPIC_AUTH_TOKEN"] = authToken + + // 显示可选环境变量 + fmt.Println("\nOptional Configuration:") + ui.ShowInfo("The following environment variables are optional. Press Enter to skip.") + + // 询问可选的环境变量 + modelVars := []string{ + "ANTHROPIC_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + } - // 获取API Token - fmt.Print("Enter ANTHROPIC_AUTH_TOKEN: ") - token, _ := reader.ReadString('\n') - token = strings.TrimSpace(token) - if token == "" { - ui.ShowError("Auth token cannot be empty", nil) - return + // 其他可选环境变量 + otherVars := make(map[string]string) + for envVar, description := range defaultVars { + // 跳过已设置的环境变量和模型变量 + if _, exists := newConfig.Env[envVar]; exists { + continue + } + isModelVar := false + for _, mv := range modelVars { + if envVar == mv { + isModelVar = true + break + } + } + if !isModelVar { + otherVars[envVar] = description + } } - // 测试API连接 - ui.ShowLoading("Testing API connection") - testConfig := config.APIConfig{ - Name: name, - AnthropicBaseURL: baseURL, - AnthropicAuthToken: token, + // 首先询问模型相关的环境变量 + fmt.Println("\nModel Configuration:") + ui.ShowInfo("These are model-specific variables. You can enter values or type 'skip' to use defaults.") + + for _, envVar := range modelVars { + // 跳过已经设置的环境变量 + if _, exists := newConfig.Env[envVar]; exists { + continue + } + + description := defaultVars[envVar] + fmt.Printf("Enter %s (%s) [skip]: ", envVar, description) + value, _ := reader.ReadString('\n') + value = strings.TrimSpace(value) + + if value == "skip" { + ui.ShowInfo("Skipping %s", envVar) + } else if value != "" { + newConfig.Env[envVar] = value + } } - if config.TestAPIConfig(testConfig) { + // 然后询问其他可选环境变量 + fmt.Println("\nOther Optional Configuration:") + ui.ShowInfo("The following environment variables are optional. Press Enter to skip.") + + for envVar, description := range otherVars { + fmt.Printf("Enter %s (%s): ", envVar, description) + value, _ := reader.ReadString('\n') + value = strings.TrimSpace(value) + if value != "" { + newConfig.Env[envVar] = value + } + } + + // 询问是否要设置其他环境变量 + fmt.Print("\nWould you like to add any additional environment variables? (y/n): ") + response, _ := reader.ReadString('\n') + response = strings.TrimSpace(strings.ToLower(response)) + + if response == "y" || response == "yes" { + fmt.Println("Enter environment variables in the format: VARIABLE_NAME=value") + fmt.Println("Enter an empty line to finish") + + for { + fmt.Print("> ") + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + + if line == "" { + break + } + + // 解析 VARIABLE_NAME=value 格式 + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + varName := strings.TrimSpace(parts[0]) + varValue := strings.TrimSpace(parts[1]) + if varName != "" { + newConfig.Env[varName] = varValue + } + } else { + ui.ShowWarning("Invalid format. Use VARIABLE_NAME=value") + } + } + } + + // 询问是否跳过权限检查 + fmt.Print("Use --dangerously-skip-permissions? (y/n) [default: n]: ") + skipResp, _ := reader.ReadString('\n') + skipResp = strings.TrimSpace(strings.ToLower(skipResp)) + if skipResp == "y" { + skipPermissions := true + newConfig.SkipPermissions = &skipPermissions + } + + // 测试API连接 + ui.ShowLoading("Testing API connection") + if config.TestAPIConfig(newConfig) { ui.ShowSuccess("API connection successful!") - testConfig.Status = "active" + newConfig.Status = "active" } else { ui.ShowWarning("API connection failed. Configuration added but marked as inactive") - testConfig.Status = "inactive" + newConfig.Status = "inactive" } // 添加新配置 - configData.Configs = append(configData.Configs, testConfig) + configData.Configs = append(configData.Configs, newConfig) // 如果这是第一个配置,设置为默认 if len(configData.Configs) == 1 { @@ -266,11 +385,21 @@ func RunAdd() { ui.ShowSuccess("Configuration '%s' added successfully!", name) ui.ShowInfo("API: %s", baseURL) - if testConfig.Status == "active" { + ui.ShowInfo("Environment variables: %d", len(newConfig.Env)) + + if newConfig.Status == "active" { ui.ShowInfo("Status: Active") } else { ui.ShowWarning("Status: Inactive (API test failed)") } + + if newConfig.SkipPermissions != nil { + if *newConfig.SkipPermissions { + ui.ShowInfo("Permissions: Skip --dangerously-skip-permissions") + } else { + ui.ShowInfo("Permissions: Use default (no --dangerously-skip-permissions)") + } + } } func RunInstall() { @@ -367,12 +496,28 @@ func RunClaudeWithConfig(args []string) { } // Set environment variables - os.Setenv("ANTHROPIC_BASE_URL", selectedConfig.AnthropicBaseURL) - os.Setenv("ANTHROPIC_AUTH_TOKEN", selectedConfig.AnthropicAuthToken) + config.SetEnvironmentVars(&selectedConfig) + + // Get API URL for display + apiURL := selectedConfig.Env["ANTHROPIC_BASE_URL"] + if apiURL == "" { + apiURL = "unknown" + } - ui.ShowInfo("Using configuration: %s (%s)", selectedConfig.Name, selectedConfig.AnthropicBaseURL) - // Run claude with dangerous permissions - cmd := exec.Command("claude", append([]string{"--dangerously-skip-permissions"}, args...)...) + ui.ShowInfo("Using configuration: %s (%s)", selectedConfig.Name, apiURL) + + // Build claude command with or without --dangerously-skip-permissions + var claudeArgs []string + if config.ShouldSkipPermissionsWithConfig(&selectedConfig, cfg) { + claudeArgs = []string{"--dangerously-skip-permissions"} + } + + // Add user arguments + if len(args) > 0 { + claudeArgs = append(claudeArgs, args...) + } + + cmd := exec.Command("claude", claudeArgs...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -468,10 +613,11 @@ func RunInit() { // Create and test configuration ui.ShowLoading("Testing API connection...") testConfig := config.APIConfig{ - Name: name, - AnthropicBaseURL: baseURL, - AnthropicAuthToken: authToken, + Name: name, + Env: make(map[string]string), } + testConfig.Env["ANTHROPIC_BASE_URL"] = baseURL + testConfig.Env["ANTHROPIC_AUTH_TOKEN"] = authToken var cfg config.Config cfg.Configs = []config.APIConfig{testConfig} @@ -544,8 +690,39 @@ func RunInit() { statusText = "inactive" } - fmt.Printf(" %d. %s %s%s - %s [%s]\n", - i+1, statusIcon, c.Name, isDefault, c.AnthropicBaseURL, statusText) + // Check permissions setting + permissionsText := "default" + if config.ShouldSkipPermissions(&c) { + permissionsText = "skip permissions" + } else if c.SkipPermissions != nil && !*c.SkipPermissions { + permissionsText = "use permissions" + } + + // Get API endpoint + apiURL := "unknown" + if baseURL, exists := c.Env["ANTHROPIC_BASE_URL"]; exists { + apiURL = baseURL + } + + fmt.Printf(" %d. %s %s%s - %s [%s, %s]\n", + i+1, statusIcon, c.Name, isDefault, apiURL, statusText, permissionsText) + + // Show environment variables (truncated for display) + if len(c.Env) > 0 { + fmt.Printf(" Environment Variables (%d):\n", len(c.Env)) + for envKey, envValue := range c.Env { + // Truncate sensitive values + displayValue := envValue + if strings.Contains(strings.ToUpper(envKey), "TOKEN") || + strings.Contains(strings.ToUpper(envKey), "KEY") || + strings.Contains(strings.ToUpper(envKey), "SECRET") { + if len(envValue) > 8 { + displayValue = envValue[:4] + "..." + envValue[len(envValue)-4:] + } + } + fmt.Printf(" %s: %s\n", envKey, displayValue) + } + } } // 4. Test default configuration @@ -661,14 +838,43 @@ func RunStart(args []string) { os.Exit(1) } } else { - // 没有参数,使用上次目录 - lastDir, err := config.GetLastWorkDir() - if err != nil { - ui.ShowError("Failed to get last working directory", err) - os.Exit(1) + // 没有参数,根据配置决定使用哪个目录 + var err error + behavior := config.GetDefaultBehavior() + + switch behavior { + case "current": + targetDir, err = os.Getwd() + if err != nil { + ui.ShowError("Failed to get current directory", err) + os.Exit(1) + } + ui.ShowInfo("Using current directory: %s", targetDir) + case "last": + lastDir, err := config.GetLastWorkDir() + if err != nil { + ui.ShowError("Failed to get last working directory", err) + os.Exit(1) + } + targetDir = lastDir + ui.ShowInfo("Using last directory: %s", targetDir) + case "home": + homeDir, err := os.UserHomeDir() + if err != nil { + ui.ShowError("Failed to get home directory", err) + os.Exit(1) + } + targetDir = homeDir + ui.ShowInfo("Using home directory: %s", targetDir) + default: + // 默认使用当前目录 + targetDir, err = os.Getwd() + if err != nil { + ui.ShowError("Failed to get current directory", err) + os.Exit(1) + } + ui.ShowInfo("Using current directory: %s", targetDir) } - targetDir = lastDir - ui.ShowInfo("Using last directory: %s", targetDir) } // 保存当前目录为上次目录 @@ -778,17 +984,418 @@ func runClaudeInDirectory(dir string) { } // Set environment variables - os.Setenv("ANTHROPIC_BASE_URL", selectedConfig.AnthropicBaseURL) - os.Setenv("ANTHROPIC_AUTH_TOKEN", selectedConfig.AnthropicAuthToken) + config.SetEnvironmentVarsWithConfig(&selectedConfig, cfg) ui.ShowInfo("Using configuration: %s", selectedConfig.Name) ui.ShowInfo("Working directory: %s", dir) - // Run claude with dangerous permissions in specified directory - cmd := exec.Command("claude", "--dangerously-skip-permissions") + // Build claude command with or without --dangerously-skip-permissions + var claudeArgs []string + if config.ShouldSkipPermissionsWithConfig(&selectedConfig, cfg) { + claudeArgs = []string{"--dangerously-skip-permissions"} + } + + cmd := exec.Command("claude", claudeArgs...) cmd.Dir = dir // 设置工作目录,而不是作为参数传递 cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() } + +// RunTest 测试 API 配置 +func RunTest(args []string) { + ui.ShowHeader("API Configuration Test") + fmt.Println() + + // 加载配置 + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + if len(cfg.Configs) == 0 { + ui.ShowError("No configurations found", nil) + ui.ShowInfo("Run 'codes add' to add a configuration first") + return + } + + // 检查是否指定了特定配置 + if len(args) > 0 && args[0] != "" { + // 测试特定配置 + configName := args[0] + var targetConfig *config.APIConfig + for i := range cfg.Configs { + if cfg.Configs[i].Name == configName { + targetConfig = &cfg.Configs[i] + break + } + } + + if targetConfig == nil { + ui.ShowError("Configuration '%s' not found", fmt.Errorf("config not found")) + return + } + + ui.ShowInfo("Testing configuration: %s", configName) + testSingleConfiguration(targetConfig) + } else { + // 测试所有配置 + ui.ShowInfo("Testing all %d configurations...", len(cfg.Configs)) + testAllConfigurations(cfg.Configs) + } +} + +// testSingleConfiguration 测试单个配置 +func testSingleConfiguration(apiConfig *config.APIConfig) { + fmt.Println() + + // 获取模型信息用于显示 + envVars := config.GetEnvironmentVars(apiConfig) + model := envVars["ANTHROPIC_MODEL"] + if model == "" { + model = envVars["ANTHROPIC_DEFAULT_HAIKU_MODEL"] + if model == "" { + model = "claude-3-haiku-20240307" + } + } + + ui.ShowInfo("Model: %s", model) + ui.ShowInfo("API: %s", envVars["ANTHROPIC_BASE_URL"]) + + // 测试 API 连接 + ui.ShowLoading("Testing API connection...") + if config.TestAPIConfig(*apiConfig) { + ui.ShowSuccess("✓ API connection successful!") + apiConfig.Status = "active" + } else { + ui.ShowError("✗ API connection failed", nil) + apiConfig.Status = "inactive" + ui.ShowWarning("Check your configuration and network connectivity") + } + + // 保存更新后的状态 + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config for update", err) + return + } + + // 更新配置状态 + for i := range cfg.Configs { + if cfg.Configs[i].Name == apiConfig.Name { + cfg.Configs[i].Status = apiConfig.Status + break + } + } + + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Failed to save config status", err) + } +} + +// testAllConfigurations 测试所有配置 +func testAllConfigurations(configs []config.APIConfig) { + results := make(map[string]bool) + statuses := make(map[string]string) + successCount := 0 + + fmt.Println() + for i := range configs { + fmt.Printf("Testing %s...", configs[i].Name) + + // 获取模型信息 + envVars := config.GetEnvironmentVars(&configs[i]) + model := envVars["ANTHROPIC_MODEL"] + if model == "" { + model = envVars["ANTHROPIC_DEFAULT_HAIKU_MODEL"] + if model == "" { + model = "claude-3-haiku-20240307" + } + } + + // 测试 API 连接 + success := config.TestAPIConfig(configs[i]) + results[configs[i].Name] = success + + if success { + fmt.Printf(" ✓ (Model: %s)\n", model) + statuses[configs[i].Name] = "active" + successCount++ + } else { + fmt.Printf(" ✗ (Model: %s)\n", model) + statuses[configs[i].Name] = "inactive" + } + } + + // 显示总结 + fmt.Println() + ui.ShowHeader("Test Results") + fmt.Printf("Successfully tested: %d/%d\n", successCount, len(configs)) + + if successCount == len(configs) { + ui.ShowSuccess("All configurations are working!") + } else if successCount == 0 { + ui.ShowError("No configurations are working", nil) + ui.ShowInfo("Check your configurations and network connectivity") + } else { + ui.ShowWarning("Some configurations failed") + ui.ShowInfo("Use 'codes test ' to test individual configurations") + } + + // 保存更新后的状态 + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config for update", err) + return + } + + // 更新所有配置状态 + updated := false + for i := range cfg.Configs { + if newStatus, ok := statuses[cfg.Configs[i].Name]; ok { + if cfg.Configs[i].Status != newStatus { + cfg.Configs[i].Status = newStatus + updated = true + } + } + } + + if updated { + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Failed to save config status", err) + } + } +} + +// RunConfigSet 设置配置值 +func RunConfigSet(key, value string) { + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + switch key { + case "defaultBehavior": + // 验证值 + if value != "current" && value != "last" && value != "home" { + ui.ShowError("Invalid value for defaultBehavior. Must be 'current', 'last', or 'home'", nil) + return + } + cfg.DefaultBehavior = value + ui.ShowSuccess("Default behavior set to: %s", value) + default: + ui.ShowError("Unknown configuration key: %s", nil) + fmt.Printf("Available keys: defaultBehavior\n") + return + } + + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Error saving config", err) + return + } +} + +// RunConfigGet 获取配置值 +func RunConfigGet(args []string) { + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + if len(args) > 0 { + // 显示特定配置 + key := args[0] + switch key { + case "defaultBehavior": + fmt.Printf("%s: %s\n", key, cfg.DefaultBehavior) + default: + ui.ShowError("Unknown configuration key: %s", nil) + fmt.Printf("Available keys: defaultBehavior\n") + return + } + } else { + // 显示所有配置 + fmt.Println("Current configuration:") + fmt.Printf(" defaultBehavior: %s\n", cfg.DefaultBehavior) + fmt.Printf(" skipPermissions: %v\n", cfg.SkipPermissions) + fmt.Printf(" lastWorkDir: %s\n", cfg.LastWorkDir) + fmt.Printf(" default: %s\n", cfg.Default) + fmt.Printf(" projects: %d configured\n", len(cfg.Projects)) + } +} + +// RunDefaultBehaviorSet 设置默认行为 +func RunDefaultBehaviorSet(behavior string) { + // 验证值 + if behavior != "current" && behavior != "last" && behavior != "home" { + ui.ShowError("Invalid behavior. Must be 'current', 'last', or 'home'", nil) + fmt.Println() + ui.ShowInfo("Available behaviors:") + ui.ShowInfo(" current - Use current working directory") + ui.ShowInfo(" last - Use last used directory") + ui.ShowInfo(" home - Use home directory") + return + } + + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + oldBehavior := cfg.DefaultBehavior + if oldBehavior == "" { + oldBehavior = "current" + } + + cfg.DefaultBehavior = behavior + + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Error saving config", err) + return + } + + ui.ShowSuccess("Default behavior set to: %s", behavior) + fmt.Println() + ui.ShowInfo("This will affect where Claude starts when you run 'codes' without arguments.") + ui.ShowInfo("Previous behavior: %s", oldBehavior) + ui.ShowInfo("New behavior: %s", behavior) + + // 显示帮助信息 + fmt.Println() + ui.ShowInfo("Examples:") + ui.ShowInfo(" codes - Start Claude with %s directory", behavior) + ui.ShowInfo(" codes start project-name - Start Claude in specific project") + ui.ShowInfo(" codes start /path/to/dir - Start Claude in specific directory") +} + +// RunDefaultBehaviorGet 获取当前默认行为 +func RunDefaultBehaviorGet() { + currentBehavior := config.GetDefaultBehavior() + + fmt.Println("Current default behavior:") + ui.ShowInfo(" %s", currentBehavior) + + fmt.Println() + ui.ShowInfo("Description:") + switch currentBehavior { + case "current": + ui.ShowInfo(" Claude will start in the current working directory") + case "last": + ui.ShowInfo(" Claude will start in the last used directory") + case "home": + ui.ShowInfo(" Claude will start in your home directory") + } + + fmt.Println() + ui.ShowInfo("To change this setting:") + ui.ShowInfo(" codes defaultbehavior set ") +} + +// RunDefaultBehaviorReset 重置默认行为 +func RunDefaultBehaviorReset() { + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + oldBehavior := cfg.DefaultBehavior + if oldBehavior == "" { + oldBehavior = "current" + } + + cfg.DefaultBehavior = "" + + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Error saving config", err) + return + } + + ui.ShowSuccess("Default behavior reset to: current") + fmt.Println() + ui.ShowInfo("Previous behavior: %s", oldBehavior) + ui.ShowInfo("New behavior: current (default)") + ui.ShowInfo("Claude will now start in the current working directory by default.") +} + +// RunSkipPermissionsSet 设置全局 skipPermissions +func RunSkipPermissionsSet(skip bool) { + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + oldValue := cfg.SkipPermissions + cfg.SkipPermissions = skip + + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Error saving config", err) + return + } + + status := "enabled" + if !skip { + status = "disabled" + } + ui.ShowSuccess("Global skipPermissions %s", status) + + fmt.Println() + ui.ShowInfo("Previous setting: %v", oldValue) + ui.ShowInfo("New setting: %v", skip) + + if skip { + ui.ShowInfo("Claude will now run with --dangerously-skip-permissions for all configurations that don't have their own setting.") + } else { + ui.ShowInfo("Claude will run without --dangerously-skip-permissions unless a specific configuration has it enabled.") + } +} + +// RunSkipPermissionsGet 获取全局 skipPermissions 设置 +func RunSkipPermissionsGet() { + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + fmt.Printf("Global skipPermissions: %v\n", cfg.SkipPermissions) + + if cfg.SkipPermissions { + ui.ShowInfo("Claude will run with --dangerously-skip-permissions for all configurations that don't have their own setting.") + } else { + ui.ShowInfo("Claude will run without --dangerously-skip-permissions unless a specific configuration has it enabled.") + } + + fmt.Println() + ui.ShowInfo("Individual configuration settings override this global setting.") + ui.ShowInfo("Use 'codes config get' to see all configurations and their skipPermissions settings.") +} + +// RunSkipPermissionsReset 重置全局 skipPermissions +func RunSkipPermissionsReset() { + cfg, err := config.LoadConfig() + if err != nil { + ui.ShowError("Error loading config", err) + return + } + + oldValue := cfg.SkipPermissions + cfg.SkipPermissions = false // 重置为 false + + if err := config.SaveConfig(cfg); err != nil { + ui.ShowError("Error saving config", err) + return + } + + ui.ShowSuccess("Global skipPermissions reset to: false") + fmt.Println() + ui.ShowInfo("Previous setting: %v", oldValue) + ui.ShowInfo("New setting: false (default)") + ui.ShowInfo("Claude will now run without --dangerously-skip-permissions unless a specific configuration has it enabled.") +} diff --git a/internal/config/config.go b/internal/config/config.go index d5cde92..178e487 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,8 @@ package config import ( "encoding/json" + "fmt" + "log" "net/http" "os" "path/filepath" @@ -10,17 +12,19 @@ import ( ) type Config struct { - Configs []APIConfig `json:"configs"` - Default string `json:"default"` - Projects map[string]string `json:"projects,omitempty"` // 项目别名 -> 目录路径 - LastWorkDir string `json:"lastWorkDir,omitempty"` // 上次工作目录 + Configs []APIConfig `json:"configs"` + Default string `json:"default"` + SkipPermissions bool `json:"skipPermissions,omitempty"` // 全局是否跳过权限检查 + Projects map[string]string `json:"projects,omitempty"` // 项目别名 -> 目录路径 + LastWorkDir string `json:"lastWorkDir,omitempty"` // 上次工作目录 + DefaultBehavior string `json:"defaultBehavior,omitempty"` // 默认启动行为: "current", "last", "home" } type APIConfig struct { - Name string `json:"name"` - AnthropicBaseURL string `json:"ANTHROPIC_BASE_URL"` - AnthropicAuthToken string `json:"ANTHROPIC_AUTH_TOKEN"` - Status string `json:"status,omitempty"` // "active", "inactive", "unknown" + Name string `json:"name"` + Env map[string]string `json:"env,omitempty"` // 环境变量映射 + SkipPermissions *bool `json:"skipPermissions,omitempty"` // 单独配置是否跳过权限检查,nil 表示使用全局设置 + Status string `json:"status,omitempty"` // "active", "inactive", "unknown" } var ConfigPath string @@ -69,16 +73,33 @@ func TestAPIConfig(config APIConfig) bool { Timeout: 10 * time.Second, } + // 获取环境变量 + envVars := GetEnvironmentVars(&config) + // Claude API的messages端点 - apiURL := config.AnthropicBaseURL + apiURL := envVars["ANTHROPIC_BASE_URL"] + if apiURL == "" { + apiURL = "https://api.anthropic.com" // 默认值 + } if apiURL[len(apiURL)-1] != '/' { apiURL += "/" } apiURL += "v1/messages" + // 获取模型名称 + model := envVars["ANTHROPIC_MODEL"] + if model == "" { + // 尝试获取默认模型 + model = envVars["ANTHROPIC_DEFAULT_HAIKU_MODEL"] + if model == "" { + // 最后回退到默认模型 + model = "claude-3-haiku-20240307" + } + } + // 构建测试请求体 - testBody := `{ - "model": "claude-3-haiku-20240307", + testBody := fmt.Sprintf(`{ + "model": "%s", "max_tokens": 10, "messages": [ { @@ -86,7 +107,7 @@ func TestAPIConfig(config APIConfig) bool { "content": "Hello" } ] - }` + }`, model) req, err := http.NewRequest("POST", apiURL, strings.NewReader(testBody)) if err != nil { @@ -94,7 +115,15 @@ func TestAPIConfig(config APIConfig) bool { } // 设置Claude API头 - req.Header.Set("x-api-key", config.AnthropicAuthToken) + authToken := envVars["ANTHROPIC_AUTH_TOKEN"] + if authToken == "" { + authToken = envVars["ANTHROPIC_API_KEY"] // 备选方案 + } + + if authToken != "" { + req.Header.Set("x-api-key", authToken) + } + req.Header.Set("Content-Type", "application/json") req.Header.Set("anthropic-version", "2023-06-01") req.Header.Set("anthropic-dangerous-disable-guardrails", "true") // 测试时禁用护栏 @@ -130,8 +159,14 @@ func testBasicConnectivity(config APIConfig) bool { Timeout: 3 * time.Second, } + // 获取环境变量 + envVars := GetEnvironmentVars(&config) + // 尝试连接基础URL - testURL := config.AnthropicBaseURL + testURL := envVars["ANTHROPIC_BASE_URL"] + if testURL == "" { + testURL = "https://api.anthropic.com" // 默认值 + } if testURL[len(testURL)-1] != '/' { testURL += "/" } @@ -233,3 +268,96 @@ func ListProjects() (map[string]string, error) { return cfg.Projects, nil } + +// ShouldSkipPermissions 判断是否应该跳过权限检查 +func ShouldSkipPermissions(apiConfig *APIConfig) bool { + return ShouldSkipPermissionsWithConfig(apiConfig, nil) +} + +// ShouldSkipPermissionsWithConfig 使用已加载的配置判断是否应该跳过权限检查 +func ShouldSkipPermissionsWithConfig(apiConfig *APIConfig, cfg *Config) bool { + // 如果没有提供配置,加载配置 + var loadedConfig *Config + if cfg == nil { + var err error + loadedConfig, err = LoadConfig() + if err != nil { + return false + } + cfg = loadedConfig + } + + // 如果 API 配置中有单独的设置,使用单独设置 + if apiConfig.SkipPermissions != nil { + return *apiConfig.SkipPermissions + } + + // 否则使用全局设置 + return cfg.SkipPermissions +} + +// GetEnvironmentVars 获取配置的所有环境变量 +func GetEnvironmentVars(apiConfig *APIConfig) map[string]string { + envVars := make(map[string]string) + + // 添加所有配置的环境变量 + for key, value := range apiConfig.Env { + envVars[key] = value + } + + return envVars +} + +// SetEnvironmentVars 设置环境变量到当前进程 +func SetEnvironmentVars(apiConfig *APIConfig) { + SetEnvironmentVarsWithConfig(apiConfig, nil) +} + +// SetEnvironmentVarsWithConfig 使用已加载的配置设置环境变量 +func SetEnvironmentVarsWithConfig(apiConfig *APIConfig, cfg *Config) { + envVars := GetEnvironmentVars(apiConfig) + for key, value := range envVars { + if err := os.Setenv(key, value); err != nil { + log.Printf("Warning: Failed to set environment variable %s: %v", key, err) + } + } +} + +// GetDefaultEnvironmentVars 获取默认的环境变量提示 +func GetDefaultEnvironmentVars() map[string]string { + return map[string]string{ + "ANTHROPIC_BASE_URL": "API endpoint URL (e.g., https://api.anthropic.com)", + "ANTHROPIC_AUTH_TOKEN": "Authentication token (e.g., sk-ant-...)", + "ANTHROPIC_API_KEY": "API key for Claude SDK (interactive use)", + "CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "API key helper TTL in milliseconds", + "HTTP_PROXY": "HTTP proxy server", + "HTTPS_PROXY": "HTTPS proxy server", + "NO_PROXY": "Domains and IPs to bypass proxy", + "ANTHROPIC_MODEL": "Model name to use", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "Default Haiku model", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "Default Sonnet model", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "Default Opus model", + "MAX_THINKING_TOKENS": "Maximum thinking tokens for extended thinking", + } +} + +// GetDefaultBehavior 获取默认启动行为 +func GetDefaultBehavior() string { + cfg, err := LoadConfig() + if err != nil { + // 默认使用当前目录 + return "current" + } + + if cfg.DefaultBehavior == "" { + return "current" + } + + // 验证值是否有效 + if cfg.DefaultBehavior == "current" || cfg.DefaultBehavior == "last" || cfg.DefaultBehavior == "home" { + return cfg.DefaultBehavior + } + + // 无效值,回退到默认 + return "current" +}