diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-06 11:14:27 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-06 11:14:27 +0300 |
| commit | fb267966f7840df222338f57023273a993a73c9a (patch) | |
| tree | d10066412f08b386a6f9fe9289f27124c6ebe9d6 /docs/coverage.html | |
| parent | 1bf4251a7edbd00902f22db77031d0f998569614 (diff) | |
use TOML not JSON for configuration
Diffstat (limited to 'docs/coverage.html')
| -rw-r--r-- | docs/coverage.html | 2838 |
1 files changed, 1508 insertions, 1330 deletions
diff --git a/docs/coverage.html b/docs/coverage.html index d940029..49b89df 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -59,7 +59,7 @@ <option value="file1">codeberg.org/snonux/hexai/cmd/hexai/main.go (0.0%)</option> - <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (86.9%)</option> + <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (87.0%)</option> <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (91.4%)</option> @@ -83,19 +83,19 @@ <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (90.5%)</option> + <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.1%)</option> <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.2%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (86.1%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.5%)</option> - <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (87.4%)</option> + <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> <option value="file18">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> <option value="file19">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (55.6%)</option> - <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.2%)</option> + <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.5%)</option> <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (77.9%)</option> @@ -178,117 +178,118 @@ func main() <span class="cov0" title="0">{ } </pre> - <pre class="file" id="file2" style="display: none">// Summary: Application configuration model and loader; reads ~/.config/hexai/config.json and merges defaults. + <pre class="file" id="file2" style="display: none">// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults. package appconfig import ( - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "slices" - "strconv" - "strings" + "fmt" + "log" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/pelletier/go-toml/v2" ) -// App holds user-configurable settings read from ~/.config/hexai/config.json. +// App holds user-configurable settings read from ~/.config/hexai/config.toml. type App struct { - MaxTokens int `json:"max_tokens"` - ContextMode string `json:"context_mode"` - ContextWindowLines int `json:"context_window_lines"` - MaxContextTokens int `json:"max_context_tokens"` - LogPreviewLimit int `json:"log_preview_limit"` + MaxTokens int `json:"max_tokens" toml:"max_tokens"` + ContextMode string `json:"context_mode" toml:"context_mode"` + ContextWindowLines int `json:"context_window_lines" toml:"context_window_lines"` + MaxContextTokens int `json:"max_context_tokens" toml:"max_context_tokens"` + LogPreviewLimit int `json:"log_preview_limit" toml:"log_preview_limit"` // Single knob for LSP requests; if set, overrides hardcoded temps in LSP. - CodingTemperature *float64 `json:"coding_temperature"` - // Minimum identifier characters required for manual (TriggerKind=1) invoke - // to proceed without structural triggers. 0 means always allow. - ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix"` - - // Completion debounce in milliseconds. When > 0, the server waits until - // there has been no text change for at least this duration before sending - // an LLM completion request. - CompletionDebounceMs int `json:"completion_debounce_ms"` - // Completion throttle in milliseconds. When > 0, caps the minimum spacing - // between LLM requests (both chat and code-completer paths). - CompletionThrottleMs int `json:"completion_throttle_ms"` - - TriggerCharacters []string `json:"trigger_characters"` - Provider string `json:"provider"` + CodingTemperature *float64 `json:"coding_temperature" toml:"coding_temperature"` + // Minimum identifier characters required for manual (TriggerKind=1) invoke + // to proceed without structural triggers. 0 means always allow. + ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix" toml:"manual_invoke_min_prefix"` + + // Completion debounce in milliseconds. When > 0, the server waits until + // there has been no text change for at least this duration before sending + // an LLM completion request. + CompletionDebounceMs int `json:"completion_debounce_ms" toml:"completion_debounce_ms"` + // Completion throttle in milliseconds. When > 0, caps the minimum spacing + // between LLM requests (both chat and code-completer paths). + CompletionThrottleMs int `json:"completion_throttle_ms" toml:"completion_throttle_ms"` + + TriggerCharacters []string `json:"trigger_characters" toml:"trigger_characters"` + Provider string `json:"provider" toml:"provider"` // Inline prompt trigger characters (default: >text> and >>text>) - InlineOpen string `json:"inline_open"` - InlineClose string `json:"inline_close"` + InlineOpen string `json:"inline_open" toml:"inline_open"` + InlineClose string `json:"inline_close" toml:"inline_close"` // In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;]) - ChatSuffix string `json:"chat_suffix"` - ChatPrefixes []string `json:"chat_prefixes"` + ChatSuffix string `json:"chat_suffix" toml:"chat_suffix"` + ChatPrefixes []string `json:"chat_prefixes" toml:"chat_prefixes"` // Provider-specific options - OpenAIBaseURL string `json:"openai_base_url"` - OpenAIModel string `json:"openai_model"` + OpenAIBaseURL string `json:"openai_base_url" toml:"openai_base_url"` + OpenAIModel string `json:"openai_model" toml:"openai_model"` // Default temperature for OpenAI requests (nil means use provider default) - OpenAITemperature *float64 `json:"openai_temperature"` - OllamaBaseURL string `json:"ollama_base_url"` - OllamaModel string `json:"ollama_model"` + OpenAITemperature *float64 `json:"openai_temperature" toml:"openai_temperature"` + OllamaBaseURL string `json:"ollama_base_url" toml:"ollama_base_url"` + OllamaModel string `json:"ollama_model" toml:"ollama_model"` // Default temperature for Ollama requests (nil means use provider default) - OllamaTemperature *float64 `json:"ollama_temperature"` - CopilotBaseURL string `json:"copilot_base_url"` - CopilotModel string `json:"copilot_model"` + OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"` + CopilotBaseURL string `json:"copilot_base_url" toml:"copilot_base_url"` + CopilotModel string `json:"copilot_model" toml:"copilot_model"` // Default temperature for Copilot requests (nil means use provider default) - CopilotTemperature *float64 `json:"copilot_temperature"` + CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` } // Constructor: defaults for App (kept first among functions) func newDefaultConfig() App <span class="cov5" title="9">{ // Coding-friendly default temperature across providers - // Users can override per provider in config.json (including 0.0). + // Users can override per provider in config.toml (including 0.0). t := 0.2 return App{ - MaxTokens: 4000, - ContextMode: "always-full", - ContextWindowLines: 120, - MaxContextTokens: 4000, - LogPreviewLimit: 100, - CodingTemperature: &t, - OpenAITemperature: &t, - OllamaTemperature: &t, - CopilotTemperature: &t, - ManualInvokeMinPrefix: 0, - CompletionDebounceMs: 200, - CompletionThrottleMs: 0, - // Inline/chat trigger defaults - InlineOpen: ">", - InlineClose: ">", - ChatSuffix: ">", - ChatPrefixes: []string{"?", "!", ":", ";"}, - } + MaxTokens: 4000, + ContextMode: "always-full", + ContextWindowLines: 120, + MaxContextTokens: 4000, + LogPreviewLimit: 100, + CodingTemperature: &t, + OpenAITemperature: &t, + OllamaTemperature: &t, + CopilotTemperature: &t, + ManualInvokeMinPrefix: 0, + CompletionDebounceMs: 200, + CompletionThrottleMs: 0, + // Inline/chat trigger defaults + InlineOpen: ">", + InlineClose: ">", + ChatSuffix: ">", + ChatPrefixes: []string{"?", "!", ":", ";"}, + } }</span> // Load reads configuration from a file and merges with defaults. // It respects the XDG Base Directory Specification. func Load(logger *log.Logger) App <span class="cov4" title="8">{ - cfg := newDefaultConfig() - if logger == nil </span><span class="cov3" title="3">{ - return cfg // Return defaults if no logger is provided (e.g. in tests) - }</span> - - <span class="cov4" title="5">configPath, err := getConfigPath() - if err != nil </span><span class="cov0" title="0">{ - logger.Printf("%v", err) - // Even if config path cannot be resolved, still allow env overrides below. - }</span> else<span class="cov4" title="5"> { - if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="3">{ - cfg.mergeWith(fileCfg) - }</span> - // When the config file is missing or invalid, we keep defaults and still - // apply any environment overrides below. - } - - // Environment overrides (take precedence over file) - <span class="cov4" title="5">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ - cfg.mergeWith(envCfg) - }</span> - <span class="cov4" title="5">return cfg</span> + cfg := newDefaultConfig() + if logger == nil </span><span class="cov3" title="3">{ + return cfg // Return defaults if no logger is provided (e.g. in tests) + }</span> + + <span class="cov4" title="5">configPath, err := getConfigPath() + if err != nil </span><span class="cov0" title="0">{ + logger.Printf("%v", err) + // Even if config path cannot be resolved, still allow env overrides below. + }</span> else<span class="cov4" title="5"> { + if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="3">{ + cfg.mergeWith(fileCfg) + }</span> + // When the config file is missing or invalid, we keep defaults and still + // apply any environment overrides below. + } + + // Environment overrides (take precedence over file) + <span class="cov4" title="5">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + cfg.mergeWith(envCfg) + }</span> + <span class="cov4" title="5">return cfg</span> } // Private helpers @@ -296,26 +297,29 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co f, err := os.Open(path) if err != nil </span><span class="cov2" title="2">{ if !os.IsNotExist(err) && logger != nil </span><span class="cov0" title="0">{ - logger.Printf("cannot open config file %s: %v", path, err) + logger.Printf("cannot open TOML config file %s: %v", path, err) }</span> <span class="cov2" title="2">return nil, err</span> } <span class="cov3" title="4">defer f.Close() - dec := json.NewDecoder(f) + dec := toml.NewDecoder(f) var fileCfg App if err := dec.Decode(&fileCfg); err != nil </span><span class="cov1" title="1">{ if logger != nil </span><span class="cov1" title="1">{ - logger.Printf("invalid config file %s: %v", path, err) + logger.Printf("invalid TOML config file %s: %v", path, err) }</span> <span class="cov1" title="1">return nil, err</span> } + <span class="cov3" title="3">if logger != nil </span><span class="cov3" title="3">{ + logger.Printf("loaded configuration from %s (TOML)", path) + }</span> <span class="cov3" title="3">return &fileCfg, nil</span> } func (a *App) mergeWith(other *App) <span class="cov3" title="4">{ - a.mergeBasics(other) - a.mergeProviderFields(other) + a.mergeBasics(other) + a.mergeProviderFields(other) }</span> // mergeBasics merges general (non-provider) fields. @@ -335,32 +339,36 @@ func (a *App) mergeBasics(other *App) <span class="cov3" title="4">{ <span class="cov3" title="4">if other.LogPreviewLimit >= 0 </span><span class="cov3" title="4">{ a.LogPreviewLimit = other.LogPreviewLimit }</span> - <span class="cov3" title="4">if other.CodingTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 - a.CodingTemperature = other.CodingTemperature - }</span> - <span class="cov3" title="4">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov3" title="4">{ - a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix - }</span> - <span class="cov3" title="4">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span> - <span class="cov3" title="4">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span> - <span class="cov3" title="4">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="3">{ - a.TriggerCharacters = slices.Clone(other.TriggerCharacters) - }</span> - <span class="cov3" title="4">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov0" title="0">{ - a.InlineOpen = s - }</span> - <span class="cov3" title="4">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov0" title="0">{ - a.InlineClose = s - }</span> - <span class="cov3" title="4">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov0" title="0">{ - a.ChatSuffix = s - }</span> - <span class="cov3" title="4">if len(other.ChatPrefixes) > 0 </span><span class="cov0" title="0">{ - a.ChatPrefixes = slices.Clone(other.ChatPrefixes) - }</span> - <span class="cov3" title="4">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="4">{ - a.Provider = s - }</span> + <span class="cov3" title="4">if other.CodingTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 + a.CodingTemperature = other.CodingTemperature + }</span> + <span class="cov3" title="4">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov3" title="4">{ + a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix + }</span> + <span class="cov3" title="4">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="3">{ + a.CompletionDebounceMs = other.CompletionDebounceMs + }</span> + <span class="cov3" title="4">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="3">{ + a.CompletionThrottleMs = other.CompletionThrottleMs + }</span> + <span class="cov3" title="4">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="3">{ + a.TriggerCharacters = slices.Clone(other.TriggerCharacters) + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov0" title="0">{ + a.InlineOpen = s + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov0" title="0">{ + a.InlineClose = s + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov0" title="0">{ + a.ChatSuffix = s + }</span> + <span class="cov3" title="4">if len(other.ChatPrefixes) > 0 </span><span class="cov0" title="0">{ + a.ChatPrefixes = slices.Clone(other.ChatPrefixes) + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="4">{ + a.Provider = s + }</span> } // mergeProviderFields merges per-provider configuration. @@ -397,15 +405,15 @@ func (a *App) mergeProviderFields(other *App) <span class="cov3" title="4">{ func getConfigPath() (string, error) <span class="cov4" title="6">{ var configPath string if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov4" title="5">{ - configPath = filepath.Join(xdgConfigHome, "hexai", "config.json") + configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml") }</span> else<span class="cov1" title="1"> { home, err := os.UserHomeDir() if err != nil </span><span class="cov0" title="0">{ return "", fmt.Errorf("cannot find user home directory: %v", err) }</span> - <span class="cov1" title="1">configPath = filepath.Join(home, ".config", "hexai", "config.json")</span> + <span class="cov1" title="1">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> } - <span class="cov4" title="6">return configPath, nil</span> + <span class="cov4" title="6">return configPath, nil</span> } // --- Environment overrides --- @@ -413,100 +421,157 @@ func getConfigPath() (string, error) <span class="cov4" title="6">{ // loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. // These values should take precedence over file config when merged. func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="5">{ - var out App - var any bool - - // helpers - getenv := func(k string) string </span><span class="cov10" title="120">{ return strings.TrimSpace(os.Getenv(k)) }</span> - <span class="cov4" title="5">parseInt := func(k string) (int, bool) </span><span class="cov7" title="35">{ - v := getenv(k) - if v == "" </span><span class="cov7" title="28">{ return 0, false }</span> - <span class="cov4" title="7">n, err := strconv.Atoi(v) - if err != nil </span><span class="cov0" title="0">{ if logger != nil </span><span class="cov0" title="0">{ logger.Printf("invalid %s: %v", k, err) }</span> ; <span class="cov0" title="0">return 0, false</span> } - <span class="cov4" title="7">return n, true</span> - } - <span class="cov4" title="5">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="20">{ - v := getenv(k) - if v == "" </span><span class="cov6" title="16">{ return nil, false }</span> - <span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64) - if err != nil </span><span class="cov0" title="0">{ - if logger != nil </span><span class="cov0" title="0">{ logger.Printf("invalid %s: %v", k, err) }</span> - <span class="cov0" title="0">return nil, false</span> + var out App + var any bool + + // helpers + getenv := func(k string) string </span><span class="cov10" title="120">{ return strings.TrimSpace(os.Getenv(k)) }</span> + <span class="cov4" title="5">parseInt := func(k string) (int, bool) </span><span class="cov7" title="35">{ + v := getenv(k) + if v == "" </span><span class="cov7" title="28">{ + return 0, false + }</span> + <span class="cov4" title="7">n, err := strconv.Atoi(v) + if err != nil </span><span class="cov0" title="0">{ + if logger != nil </span><span class="cov0" title="0">{ + logger.Printf("invalid %s: %v", k, err) + }</span> + <span class="cov0" title="0">return 0, false</span> + } + <span class="cov4" title="7">return n, true</span> } - <span class="cov3" title="4">return &f, true</span> - } - - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ - out.MaxTokens = n; any = true - }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ - out.ContextMode = s; any = true - }</span> - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ - out.ContextWindowLines = n; any = true - }</span> - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ - out.MaxContextTokens = n; any = true - }</span> - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ - out.LogPreviewLimit = n; any = true - }</span> - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ - out.ManualInvokeMinPrefix = n; any = true - }</span> - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ - out.CompletionDebounceMs = n; any = true - }</span> - <span class="cov4" title="5">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ - out.CompletionThrottleMs = n; any = true - }</span> - <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ - out.CodingTemperature = f; any = true - }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ - parts := strings.Split(s, ",") - out.TriggerCharacters = nil - for _, p := range parts </span><span class="cov3" title="3">{ - if t := strings.TrimSpace(p); t != "" </span><span class="cov3" title="3">{ - out.TriggerCharacters = append(out.TriggerCharacters, t) - }</span> + <span class="cov4" title="5">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="20">{ + v := getenv(k) + if v == "" </span><span class="cov6" title="16">{ + return nil, false + }</span> + <span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64) + if err != nil </span><span class="cov0" title="0">{ + if logger != nil </span><span class="cov0" title="0">{ + logger.Printf("invalid %s: %v", k, err) + }</span> + <span class="cov0" title="0">return nil, false</span> + } + <span class="cov3" title="4">return &f, true</span> + } + + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ + out.MaxTokens = n + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ + out.ContextMode = s + any = true + }</span> + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ + out.ContextWindowLines = n + any = true + }</span> + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ + out.MaxContextTokens = n + any = true + }</span> + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ + out.LogPreviewLimit = n + any = true + }</span> + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ + out.ManualInvokeMinPrefix = n + any = true + }</span> + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ + out.CompletionDebounceMs = n + any = true + }</span> + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ + out.CompletionThrottleMs = n + any = true + }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + out.CodingTemperature = f + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ + parts := strings.Split(s, ",") + out.TriggerCharacters = nil + for _, p := range parts </span><span class="cov3" title="3">{ + if t := strings.TrimSpace(p); t != "" </span><span class="cov3" title="3">{ + out.TriggerCharacters = append(out.TriggerCharacters, t) + }</span> + } + <span class="cov1" title="1">any = true</span> } - <span class="cov1" title="1">any = true</span> - } - <span class="cov4" title="5">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ out.InlineOpen = s; any = true }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ out.InlineClose = s; any = true }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ out.ChatSuffix = s; any = true }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ - parts := strings.Split(s, ",") - out.ChatPrefixes = nil - for _, p := range parts </span><span class="cov0" title="0">{ - if t := strings.TrimSpace(p); t != "" </span><span class="cov0" title="0">{ - out.ChatPrefixes = append(out.ChatPrefixes, t) - }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ + out.InlineOpen = s + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ + out.InlineClose = s + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ + out.ChatSuffix = s + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ + parts := strings.Split(s, ",") + out.ChatPrefixes = nil + for _, p := range parts </span><span class="cov0" title="0">{ + if t := strings.TrimSpace(p); t != "" </span><span class="cov0" title="0">{ + out.ChatPrefixes = append(out.ChatPrefixes, t) + }</span> + } + <span class="cov0" title="0">any = true</span> } - <span class="cov0" title="0">any = true</span> - } - <span class="cov4" title="5">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ - out.Provider = s; any = true - }</span> - - // Provider-specific - <span class="cov4" title="5">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s; any = true }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s; any = true }</span> - <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f; any = true }</span> - - <span class="cov4" title="5">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s; any = true }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s; any = true }</span> - <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f; any = true }</span> - - <span class="cov4" title="5">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s; any = true }</span> - <span class="cov4" title="5">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s; any = true }</span> - <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f; any = true }</span> - - <span class="cov4" title="5">if !any </span><span class="cov3" title="4">{ - return nil - }</span> - <span class="cov1" title="1">return &out</span> + <span class="cov4" title="5">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ + out.Provider = s + any = true + }</span> + + // Provider-specific + <span class="cov4" title="5">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + out.OpenAIBaseURL = s + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ + out.OpenAIModel = s + any = true + }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + out.OpenAITemperature = f + any = true + }</span> + + <span class="cov4" title="5">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + out.OllamaBaseURL = s + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ + out.OllamaModel = s + any = true + }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + out.OllamaTemperature = f + any = true + }</span> + + <span class="cov4" title="5">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + out.CopilotBaseURL = s + any = true + }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ + out.CopilotModel = s + any = true + }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + out.CopilotTemperature = f + any = true + }</span> + + <span class="cov4" title="5">if !any </span><span class="cov3" title="4">{ + return nil + }</span> + <span class="cov1" title="1">return &out</span> } </pre> @@ -515,14 +580,14 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="5">{ package hexaicli import ( - "bufio" - "context" - "fmt" - "io" - "log" - "os" - "strings" - "time" + "bufio" + "context" + "fmt" + "io" + "log" + "os" + "strings" + "time" "codeberg.org/snonux/hexai/internal/appconfig" "codeberg.org/snonux/hexai/internal/llm" @@ -532,14 +597,14 @@ import ( // Run executes the Hexai CLI behavior given arguments and I/O streams. // It assumes flags have already been parsed by the caller. func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov1" title="1">{ - // Load configuration with a logger so file-based config is respected. - logger := log.New(stderr, "hexai ", log.LstdFlags|log.Lmsgprefix) - cfg := appconfig.Load(logger) - client, err := newClientFromConfig(cfg) - if err != nil </span><span class="cov1" title="1">{ - fmt.Fprintf(stderr, logging.AnsiBase+"hexai: LLM disabled: %v"+logging.AnsiReset+"\n", err) - return err - }</span> + // Load configuration with a logger so file-based config is respected. + logger := log.New(stderr, "hexai ", log.LstdFlags|log.Lmsgprefix) + cfg := appconfig.Load(logger) + client, err := newClientFromConfig(cfg) + if err != nil </span><span class="cov1" title="1">{ + fmt.Fprintf(stderr, logging.AnsiBase+"hexai: LLM disabled: %v"+logging.AnsiReset+"\n", err) + return err + }</span> <span class="cov0" title="0">return RunWithClient(ctx, args, stdin, stdout, stderr, client)</span> } @@ -583,29 +648,29 @@ func readInput(stdin io.Reader, args []string) (string, error) <span class="cov9 // newClientFromConfig builds an LLM client from the app config and env keys. func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov6" title="3">{ - llmCfg := llm.Config{ - Provider: cfg.Provider, - OpenAIBaseURL: cfg.OpenAIBaseURL, - OpenAIModel: cfg.OpenAIModel, - OpenAITemperature: cfg.OpenAITemperature, - OllamaBaseURL: cfg.OllamaBaseURL, - OllamaModel: cfg.OllamaModel, - OllamaTemperature: cfg.OllamaTemperature, - CopilotBaseURL: cfg.CopilotBaseURL, - CopilotModel: cfg.CopilotModel, - CopilotTemperature: cfg.CopilotTemperature, - } - // Prefer HEXAI_OPENAI_API_KEY; fall back to OPENAI_API_KEY - oaKey := os.Getenv("HEXAI_OPENAI_API_KEY") - if strings.TrimSpace(oaKey) == "" </span><span class="cov6" title="3">{ - oaKey = os.Getenv("OPENAI_API_KEY") - }</span> - // Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY - <span class="cov6" title="3">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") - if strings.TrimSpace(cpKey) == "" </span><span class="cov6" title="3">{ - cpKey = os.Getenv("COPILOT_API_KEY") - }</span> - <span class="cov6" title="3">return llm.NewFromConfig(llmCfg, oaKey, cpKey)</span> + llmCfg := llm.Config{ + Provider: cfg.Provider, + OpenAIBaseURL: cfg.OpenAIBaseURL, + OpenAIModel: cfg.OpenAIModel, + OpenAITemperature: cfg.OpenAITemperature, + OllamaBaseURL: cfg.OllamaBaseURL, + OllamaModel: cfg.OllamaModel, + OllamaTemperature: cfg.OllamaTemperature, + CopilotBaseURL: cfg.CopilotBaseURL, + CopilotModel: cfg.CopilotModel, + CopilotTemperature: cfg.CopilotTemperature, + } + // Prefer HEXAI_OPENAI_API_KEY; fall back to OPENAI_API_KEY + oaKey := os.Getenv("HEXAI_OPENAI_API_KEY") + if strings.TrimSpace(oaKey) == "" </span><span class="cov6" title="3">{ + oaKey = os.Getenv("OPENAI_API_KEY") + }</span> + // Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY + <span class="cov6" title="3">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") + if strings.TrimSpace(cpKey) == "" </span><span class="cov6" title="3">{ + cpKey = os.Getenv("COPILOT_API_KEY") + }</span> + <span class="cov6" title="3">return llm.NewFromConfig(llmCfg, oaKey, cpKey)</span> } // buildMessages creates system and user messages based on input content. @@ -681,7 +746,7 @@ type ServerFactory func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.S func Run(logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error <span class="cov1" title="1">{ logger := log.New(stderr, "hexai-lsp ", log.LstdFlags|log.Lmsgprefix) if strings.TrimSpace(logPath) != "" </span><span class="cov1" title="1">{ - f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil </span><span class="cov0" title="0">{ logger.Fatalf("failed to open log file: %v", err) }</span> @@ -733,16 +798,16 @@ func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span cla CopilotModel: cfg.CopilotModel, CopilotTemperature: cfg.CopilotTemperature, } - // Prefer HEXAI_OPENAI_API_KEY; fall back to OPENAI_API_KEY - oaKey := os.Getenv("HEXAI_OPENAI_API_KEY") - if strings.TrimSpace(oaKey) == "" </span><span class="cov10" title="6">{ - oaKey = os.Getenv("OPENAI_API_KEY") - }</span> - |
