summaryrefslogtreecommitdiff
path: root/docs/coverage.html
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-06 11:14:27 +0300
committerPaul Buetow <paul@buetow.org>2025-09-06 11:14:27 +0300
commitfb267966f7840df222338f57023273a993a73c9a (patch)
treed10066412f08b386a6f9fe9289f27124c6ebe9d6 /docs/coverage.html
parent1bf4251a7edbd00902f22db77031d0f998569614 (diff)
use TOML not JSON for configuration
Diffstat (limited to 'docs/coverage.html')
-rw-r--r--docs/coverage.html2838
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 &gt; 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 &gt; 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 &gt; 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 &gt; 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: &gt;text&gt; and &gt;&gt;text&gt;)
- 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 "&gt;" 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: &amp;t,
- OpenAITemperature: &amp;t,
- OllamaTemperature: &amp;t,
- CopilotTemperature: &amp;t,
- ManualInvokeMinPrefix: 0,
- CompletionDebounceMs: 200,
- CompletionThrottleMs: 0,
- // Inline/chat trigger defaults
- InlineOpen: "&gt;",
- InlineClose: "&gt;",
- ChatSuffix: "&gt;",
- ChatPrefixes: []string{"?", "!", ":", ";"},
- }
+ MaxTokens: 4000,
+ ContextMode: "always-full",
+ ContextWindowLines: 120,
+ MaxContextTokens: 4000,
+ LogPreviewLimit: 100,
+ CodingTemperature: &amp;t,
+ OpenAITemperature: &amp;t,
+ OllamaTemperature: &amp;t,
+ CopilotTemperature: &amp;t,
+ ManualInvokeMinPrefix: 0,
+ CompletionDebounceMs: 200,
+ CompletionThrottleMs: 0,
+ // Inline/chat trigger defaults
+ InlineOpen: "&gt;",
+ InlineClose: "&gt;",
+ ChatSuffix: "&gt;",
+ 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 &amp;&amp; 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 &amp;&amp; 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) &amp;&amp; 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(&amp;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 &amp;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 &gt;= 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 &gt;= 0 </span><span class="cov3" title="4">{
- a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix
- }</span>
- <span class="cov3" title="4">if other.CompletionDebounceMs &gt; 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span>
- <span class="cov3" title="4">if other.CompletionThrottleMs &gt; 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span>
- <span class="cov3" title="4">if len(other.TriggerCharacters) &gt; 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) &gt; 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 &gt;= 0 </span><span class="cov3" title="4">{
+ a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix
+ }</span>
+ <span class="cov3" title="4">if other.CompletionDebounceMs &gt; 0 </span><span class="cov3" title="3">{
+ a.CompletionDebounceMs = other.CompletionDebounceMs
+ }</span>
+ <span class="cov3" title="4">if other.CompletionThrottleMs &gt; 0 </span><span class="cov3" title="3">{
+ a.CompletionThrottleMs = other.CompletionThrottleMs
+ }</span>
+ <span class="cov3" title="4">if len(other.TriggerCharacters) &gt; 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) &gt; 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 &amp;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 &amp;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 &amp;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 &amp;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>
-