summaryrefslogtreecommitdiff
path: root/docs/coverage.html
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-05 21:17:25 +0300
committerPaul Buetow <paul@buetow.org>2025-09-05 21:17:25 +0300
commit61137206eb7dd6a3df865591d710923838f59f18 (patch)
tree127b4738703547436848e799e91fc87cc19b0c26 /docs/coverage.html
parentb5bbf0f183a39353be0fb469d6aca1c3e03b78d5 (diff)
over 80% coverage now
Diffstat (limited to 'docs/coverage.html')
-rw-r--r--docs/coverage.html1697
1 files changed, 864 insertions, 833 deletions
diff --git a/docs/coverage.html b/docs/coverage.html
index 4976a0c..df02a90 100644
--- a/docs/coverage.html
+++ b/docs/coverage.html
@@ -61,45 +61,47 @@
<option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (94.6%)</option>
- <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (63.8%)</option>
+ <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (91.4%)</option>
<option value="file4">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option>
- <option value="file5">codeberg.org/snonux/hexai/internal/llm/copilot.go (7.9%)</option>
+ <option value="file5">codeberg.org/snonux/hexai/internal/llm/copilot.go (81.8%)</option>
- <option value="file6">codeberg.org/snonux/hexai/internal/llm/ollama.go (13.9%)</option>
+ <option value="file6">codeberg.org/snonux/hexai/internal/llm/ollama.go (88.0%)</option>
- <option value="file7">codeberg.org/snonux/hexai/internal/llm/openai.go (32.6%)</option>
+ <option value="file7">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option>
- <option value="file8">codeberg.org/snonux/hexai/internal/llm/provider.go (37.9%)</option>
+ <option value="file8">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option>
- <option value="file9">codeberg.org/snonux/hexai/internal/llm/util.go (0.0%)</option>
+ <option value="file9">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option>
- <option value="file10">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (14.3%)</option>
+ <option value="file10">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option>
- <option value="file11">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option>
+ <option value="file11">codeberg.org/snonux/hexai/internal/logging/logging.go (100.0%)</option>
- <option value="file12">codeberg.org/snonux/hexai/internal/lsp/context.go (71.8%)</option>
+ <option value="file12">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option>
- <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (56.3%)</option>
+ <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 (77.8%)</option>
+ <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (91.3%)</option>
- <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (22.9%)</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 (69.8%)</option>
+ <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (85.1%)</option>
- <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (1.8%)</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 (0.0%)</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 (0.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 (65.3%)</option>
+ <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.1%)</option>
<option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (68.8%)</option>
- <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (17.1%)</option>
+ <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option>
+
+ <option value="file23">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (60.0%)</option>
</select>
</div>
@@ -230,7 +232,7 @@ type App struct {
}
// Constructor: defaults for App (kept first among functions)
-func newDefaultConfig() App <span class="cov5" title="8">{
+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).
t := 0.2
@@ -252,18 +254,18 @@ func newDefaultConfig() App <span class="cov5" title="8">{
// 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="7">{
+func Load(logger *log.Logger) App <span class="cov5" 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="cov3" title="4">configPath, err := getConfigPath()
+ <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="cov3" title="4"> {
- if fileCfg, err := loadFromFile(configPath, logger); err == nil &amp;&amp; fileCfg != nil </span><span class="cov2" title="2">{
+ }</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
@@ -271,14 +273,14 @@ func Load(logger *log.Logger) App <span class="cov4" title="7">{
}
// Environment overrides (take precedence over file)
- <span class="cov3" title="4">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{
+ <span class="cov4" title="5">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{
cfg.mergeWith(envCfg)
}</span>
- <span class="cov3" title="4">return cfg</span>
+ <span class="cov4" title="5">return cfg</span>
}
// Private helpers
-func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="5">{
+func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="6">{
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">{
@@ -286,7 +288,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}</span>
<span class="cov2" title="2">return nil, err</span>
}
- <span class="cov3" title="3">defer f.Close()
+ <span class="cov3" title="4">defer f.Close()
dec := json.NewDecoder(f)
var fileCfg App
@@ -296,81 +298,81 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co
}</span>
<span class="cov1" title="1">return nil, err</span>
}
- <span class="cov2" title="2">return &amp;fileCfg, nil</span>
+ <span class="cov3" title="3">return &amp;fileCfg, nil</span>
}
-func (a *App) mergeWith(other *App) <span class="cov3" title="3">{
+func (a *App) mergeWith(other *App) <span class="cov3" title="4">{
a.mergeBasics(other)
a.mergeProviderFields(other)
}</span>
// mergeBasics merges general (non-provider) fields.
-func (a *App) mergeBasics(other *App) <span class="cov3" title="3">{
+func (a *App) mergeBasics(other *App) <span class="cov3" title="4">{
if other.MaxTokens &gt; 0 </span><span class="cov3" title="3">{
a.MaxTokens = other.MaxTokens
}</span>
- <span class="cov3" title="3">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="3">{
a.ContextMode = s
}</span>
- <span class="cov3" title="3">if other.ContextWindowLines &gt; 0 </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if other.ContextWindowLines &gt; 0 </span><span class="cov3" title="3">{
a.ContextWindowLines = other.ContextWindowLines
}</span>
- <span class="cov3" title="3">if other.MaxContextTokens &gt; 0 </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if other.MaxContextTokens &gt; 0 </span><span class="cov3" title="3">{
a.MaxContextTokens = other.MaxContextTokens
}</span>
- <span class="cov3" title="3">if other.LogPreviewLimit &gt;= 0 </span><span class="cov3" title="3">{
+ <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="3">if other.CodingTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
+ <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="3">if other.ManualInvokeMinPrefix &gt;= 0 </span><span class="cov3" title="3">{
+ <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="3">if other.CompletionDebounceMs &gt; 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span>
- <span class="cov3" title="3">if other.CompletionThrottleMs &gt; 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span>
- <span class="cov3" title="3">if len(other.TriggerCharacters) &gt; 0 </span><span class="cov3" title="3">{
+ <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="3">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="3">{
+ <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.
-func (a *App) mergeProviderFields(other *App) <span class="cov3" title="3">{
+func (a *App) mergeProviderFields(other *App) <span class="cov3" title="4">{
if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" </span><span class="cov3" title="3">{
a.OpenAIBaseURL = s
}</span>
- <span class="cov3" title="3">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov3" title="4">{
a.OpenAIModel = s
}</span>
- <span class="cov3" title="3">if other.OpenAITemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
+ <span class="cov3" title="4">if other.OpenAITemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
a.OpenAITemperature = other.OpenAITemperature
}</span>
- <span class="cov3" title="3">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="3">{
a.OllamaBaseURL = s
}</span>
- <span class="cov3" title="3">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="3">{
a.OllamaModel = s
}</span>
- <span class="cov3" title="3">if other.OllamaTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
+ <span class="cov3" title="4">if other.OllamaTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
a.OllamaTemperature = other.OllamaTemperature
}</span>
- <span class="cov3" title="3">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="3">{
a.CopilotBaseURL = s
}</span>
- <span class="cov3" title="3">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="3">{
+ <span class="cov3" title="4">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="3">{
a.CopilotModel = s
}</span>
- <span class="cov3" title="3">if other.CopilotTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
+ <span class="cov3" title="4">if other.CopilotTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0
a.CopilotTemperature = other.CopilotTemperature
}</span>
}
-func getConfigPath() (string, error) <span class="cov4" title="5">{
+func getConfigPath() (string, error) <span class="cov4" title="6">{
var configPath string
- if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov3" title="4">{
+ if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov4" title="5">{
configPath = filepath.Join(xdgConfigHome, "hexai", "config.json")
}</span> else<span class="cov1" title="1"> {
home, err := os.UserHomeDir()
@@ -379,29 +381,29 @@ func getConfigPath() (string, error) <span class="cov4" title="5">{
}</span>
<span class="cov1" title="1">configPath = filepath.Join(home, ".config", "hexai", "config.json")</span>
}
- <span class="cov4" title="5">return configPath, nil</span>
+ <span class="cov4" title="6">return configPath, nil</span>
}
// --- Environment overrides ---
// 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="cov3" title="4">{
+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="80">{ return strings.TrimSpace(os.Getenv(k)) }</span>
- <span class="cov3" title="4">parseInt := func(k string) (int, bool) </span><span class="cov7" title="28">{
+ getenv := func(k string) string </span><span class="cov10" title="100">{ 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="21">{ return 0, false }</span>
+ 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">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="16">{
+ <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="12">{ return nil, false }</span>
+ 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>
@@ -410,34 +412,34 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov3" title="4">{
<span class="cov3" title="4">return &amp;f, true</span>
}
- <span class="cov3" title="4">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{
+ <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">{
@@ -447,24 +449,24 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov3" title="4">{
}
<span class="cov1" title="1">any = true</span>
}
- <span class="cov3" title="4">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{
+ <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="cov3" title="4">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s; any = true }</span>
- <span class="cov3" title="4">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s; any = true }</span>
- <span class="cov3" title="4">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_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="cov3" title="4">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s; any = true }</span>
- <span class="cov3" title="4">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s; any = true }</span>
- <span class="cov3" title="4">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_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="cov3" title="4">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s; any = true }</span>
- <span class="cov3" title="4">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s; any = true }</span>
- <span class="cov3" title="4">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 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="cov3" title="4">if !any </span><span class="cov3" title="3">{
+ <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>
@@ -492,12 +494,12 @@ 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="cov0" title="0">{
+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="cov0" title="0">{
+ 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>
@@ -507,35 +509,35 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.
// RunWithClient executes the CLI flow using an already-constructed client.
// Useful for testing and embedding.
-func RunWithClient(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer, client llm.Client) error <span class="cov6" title="3">{
+func RunWithClient(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer, client llm.Client) error <span class="cov1" title="1">{
input, err := readInput(stdin, args)
if err != nil </span><span class="cov0" title="0">{
fmt.Fprintln(stderr, logging.AnsiBase+err.Error()+logging.AnsiReset)
return err
}</span>
- <span class="cov6" title="3">printProviderInfo(stderr, client)
+ <span class="cov1" title="1">printProviderInfo(stderr, client)
msgs := buildMessages(input)
- if err := runChat(ctx, client, msgs, input, stdout, stderr); err != nil </span><span class="cov0" title="0">{
+ if err := runChat(ctx, client, msgs, input, stdout, stderr); err != nil </span><span class="cov1" title="1">{
fmt.Fprintf(stderr, logging.AnsiBase+"hexai: error: %v"+logging.AnsiReset+"\n", err)
return err
}</span>
- <span class="cov6" title="3">return nil</span>
+ <span class="cov0" title="0">return nil</span>
}
// readInput reads from stdin and args, then combines them per CLI rules.
-func readInput(stdin io.Reader, args []string) (string, error) <span class="cov10" title="7">{
+func readInput(stdin io.Reader, args []string) (string, error) <span class="cov9" title="5">{
var stdinData string
- if fi, err := os.Stdin.Stat(); err == nil &amp;&amp; (fi.Mode()&amp;os.ModeCharDevice) == 0 </span><span class="cov10" title="7">{
+ if fi, err := os.Stdin.Stat(); err == nil &amp;&amp; (fi.Mode()&amp;os.ModeCharDevice) == 0 </span><span class="cov7" title="4">{
b, _ := io.ReadAll(bufio.NewReader(stdin))
stdinData = strings.TrimSpace(string(b))
}</span>
- <span class="cov10" title="7">argData := strings.TrimSpace(strings.Join(args, " "))
+ <span class="cov9" title="5">argData := strings.TrimSpace(strings.Join(args, " "))
switch </span>{
- case stdinData != "" &amp;&amp; argData != "":<span class="cov4" title="2">
+ case stdinData != "" &amp;&amp; argData != "":<span class="cov1" title="1">
return fmt.Sprintf("%s:\n\n%s", argData, stdinData), nil</span>
case stdinData != "":<span class="cov1" title="1">
return stdinData, nil</span>
- case argData != "":<span class="cov6" title="3">
+ case argData != "":<span class="cov4" title="2">
return argData, nil</span>
default:<span class="cov1" title="1">
return "", fmt.Errorf("hexai: no input provided; pass text as an argument or via stdin")</span>
@@ -543,7 +545,7 @@ func readInput(stdin io.Reader, args []string) (string, error) <span class="cov1
}
// newClientFromConfig builds an LLM client from the app config and env keys.
-func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov0" title="0">{
+func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov6" title="3">{
llmCfg := llm.Config{
Provider: cfg.Provider,
OpenAIBaseURL: cfg.OpenAIBaseURL,
@@ -558,59 +560,59 @@ func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov
}
// 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="cov0" title="0">{
+ 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="cov0" title="0">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY")
- if strings.TrimSpace(cpKey) == "" </span><span class="cov0" title="0">{
+ <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="cov0" title="0">return llm.NewFromConfig(llmCfg, oaKey, cpKey)</span>
+ <span class="cov6" title="3">return llm.NewFromConfig(llmCfg, oaKey, cpKey)</span>
}
// buildMessages creates system and user messages based on input content.
-func buildMessages(input string) []llm.Message <span class="cov8" title="5">{
+func buildMessages(input string) []llm.Message <span class="cov10" title="6">{
lower := strings.ToLower(input)
system := "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation."
if strings.Contains(lower, "explain") </span><span class="cov1" title="1">{
system = "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context."
}</span>
- <span class="cov8" title="5">return []llm.Message{
+ <span class="cov10" title="6">return []llm.Message{
{Role: "system", Content: system},
{Role: "user", Content: input},
}</span>
}
// runChat executes the chat request, handling streaming and summary output.
-func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input string, out io.Writer, errw io.Writer) error <span class="cov8" title="5">{
+func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input string, out io.Writer, errw io.Writer) error <span class="cov7" title="4">{
start := time.Now()
var output string
- if s, ok := client.(llm.Streamer); ok </span><span class="cov4" title="2">{
+ if s, ok := client.(llm.Streamer); ok </span><span class="cov1" title="1">{
var b strings.Builder
- if err := s.ChatStream(ctx, msgs, func(chunk string) </span><span class="cov7" title="4">{
+ if err := s.ChatStream(ctx, msgs, func(chunk string) </span><span class="cov6" title="3">{
b.WriteString(chunk)
fmt.Fprint(out, chunk)
}</span>); err != nil <span class="cov0" title="0">{
return err
}</span>
- <span class="cov4" title="2">output = b.String()</span>
+ <span class="cov1" title="1">output = b.String()</span>
} else<span class="cov6" title="3"> {
txt, err := client.Chat(ctx, msgs)
- if err != nil </span><span class="cov0" title="0">{
+ if err != nil </span><span class="cov4" title="2">{
return err
}</span>
- <span class="cov6" title="3">output = txt
+ <span class="cov1" title="1">output = txt
fmt.Fprint(out, output)</span>
}
- <span class="cov8" title="5">dur := time.Since(start)
+ <span class="cov4" title="2">dur := time.Since(start)
fmt.Fprintf(errw, "\n"+logging.AnsiBase+"done provider=%s model=%s time=%s in_bytes=%d out_bytes=%d"+logging.AnsiReset+"\n",
client.Name(), client.DefaultModel(), dur.Round(time.Millisecond), len(input), len(output))
return nil</span>
}
// printProviderInfo writes the provider/model line to stderr.
-func printProviderInfo(errw io.Writer, client llm.Client) <span class="cov7" title="4">{
+func printProviderInfo(errw io.Writer, client llm.Client) <span class="cov4" title="2">{
fmt.Fprintf(errw, logging.AnsiBase+"provider=%s model=%s"+logging.AnsiReset+"\n", client.Name(), client.DefaultModel())
}</span>
</pre>
@@ -804,16 +806,16 @@ type copilotChatResponse struct {
}
// Constructor (kept among the first functions by convention)
-func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov0" title="0">{
+func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov3" title="8">{
if strings.TrimSpace(baseURL) == "" </span><span class="cov0" title="0">{
baseURL = "https://api.githubcopilot.com"
}</span>
- <span class="cov0" title="0">if strings.TrimSpace(model) == "" </span><span class="cov0" title="0">{
+ <span class="cov3" title="8">if strings.TrimSpace(model) == "" </span><span class="cov0" title="0">{
// GitHub Models (Copilot API) commonly supports gpt-4o/gpt-4o-mini.
// Default to a broadly available, cost-effective option.
model = "gpt-4o-mini"
}</span>
- <span class="cov0" title="0">return copilotClient{
+ <span class="cov3" title="8">return copilotClient{
httpClient: &amp;http.Client{Timeout: 30 * time.Second},
apiKey: apiKey,
baseURL: strings.TrimRight(baseURL, "/"),
@@ -823,27 +825,27 @@ func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client <spa
}</span>
}
-func (c copilotClient) Chat(ctx context.Context, messages []Message, opts ...RequestOption) (string, error) <span class="cov0" title="0">{
+func (c copilotClient) Chat(ctx context.Context, messages []Message, opts ...RequestOption) (string, error) <span class="cov3" title="5">{
if strings.TrimSpace(c.apiKey) == "" </span><span class="cov0" title="0">{
return nilStringErr("missing Copilot API key")
}</span>
// Ensure we have a fresh session token
- <span class="cov0" title="0">if err := c.ensureSession(ctx); err != nil </span><span class="cov0" title="0">{
+ <span class="cov3" title="5">if err := c.ensureSession(ctx); err != nil </span><span class="cov0" title="0">{
return "", err
}</span>
- <span class="cov0" title="0">o := Options{Model: c.defaultModel}
+ <span class="cov3" title="5">o := Options{Model: c.defaultModel}
for _, opt := range opts </span><span class="cov0" title="0">{
opt(&amp;o)
}</span>
- <span class="cov0" title="0">if o.Model == "" </span><span class="cov0" title="0">{
+ <span class="cov3" title="5">if o.Model == "" </span><span class="cov0" title="0">{
o.Model = c.defaultModel
}</span>
- <span class="cov0" title="0">start := time.Now()
+ <span class="cov3" title="5">start := time.Now()
logMessages := make([]struct{ Role, Content string }, len(messages))
- for i, m := range messages </span><span class="cov0" title="0">{
+ for i, m := range messages </span><span class="cov3" title="5">{
logMessages[i] = struct{ Role, Content string }{m.Role, m.Content}
}</span>
- <span class="cov0" title="0">c.chatLogger.LogStart(false, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)
+ <span class="cov3" title="5">c.chatLogger.LogStart(false, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)
req := buildCopilotChatRequest(o, messages, c.defaultTemperature)
body, err := json.Marshal(req)
@@ -852,70 +854,70 @@ func (c copilotClient) Chat(ctx context.Context, messages []Message, opts ...Req
return "", err
}</span>
- <span class="cov0" title="0">endpoint := c.baseURL + "/chat/completions"
+ <span class="cov3" title="5">endpoint := c.baseURL + "/chat/completions"
logging.Logf("llm/copilot ", "POST %s", endpoint)
resp, err := c.postJSON(ctx, endpoint, body, c.headersChat())
if err != nil </span><span class="cov0" title="0">{
logging.Logf("llm/copilot ", "%shttp error after %s: %v%s", logging.AnsiRed, time.Since(start), err, logging.AnsiBase)
return "", err
}</span>
- <span class="cov0" title="0">defer resp.Body.Close()
- if err := handleCopilotNon2xx(resp, start); err != nil </span><span class="cov0" title="0">{
+ <span class="cov3" title="5">defer resp.Body.Close()
+ if err := handleCopilotNon2xx(resp, start); err != nil </span><span class="cov1" title="1">{
return "", err
}</span>
- <span class="cov0" title="0">out, err := decodeCopilotChat(resp, start)
- if err != nil </span><span class="cov0" title="0">{
+ <span class="cov2" title="4">out, err := decodeCopilotChat(resp, start)
+ if err != nil </span><span class="cov1" title="1">{
return "", err
}</span>
- <span class="cov0" title="0">if len(out.Choices) == 0 </span><span class="cov0" title="0">{
+ <span class="cov2" title="3">if len(out.Choices) == 0 </span><span class="cov1" title="1">{
logging.Logf("llm/copilot ", "%sno choices returned duration=%s%s", logging.AnsiRed, time.Since(start), logging.AnsiBase)
return "", errors.New("copilot: no choices returned")
}</span>
- <span class="cov0" title="0">content := out.Choices[0].Message.Content
+ <span class="cov1" title="2">content := out.Choices[0].Message.Content
logging.Logf("llm/copilot ", "success choice=0 finish=%s size=%d preview=%s%s%s duration=%s", out.Choices[0].FinishReason, len(content), logging.AnsiGreen, logging.PreviewForLog(content), logging.AnsiBase, time.Since(start))
return content, nil</span>
}
// Provider metadata
-func (c copilotClient) Name() string <span class="cov0" title="0">{ return "copilot" }</span>
-func (c copilotClient) DefaultModel() string <span class="cov0" title="0">{ return c.defaultModel }</span>
+func (c copilotClient) Name() string <span class="cov1" title="1">{ return "copilot" }</span>
+func (c copilotClient) DefaultModel() string <span class="cov1" title="1">{ return c.defaultModel }</span>
// helpers
-func buildCopilotChatRequest(o Options, messages []Message, defaultTemp *float64) copilotChatRequest <span class="cov8" title="1">{
+func buildCopilotChatRequest(o Options, messages []Message, defaultTemp *float64) copilotChatRequest <span class="cov3" title="6">{
req := copilotChatRequest{Model: o.Model}
req.Messages = make([]copilotMessage, len(messages))
- for i, m := range messages </span><span class="cov8" title="1">{
+ for i, m := range messages </span><span class="cov3" title="6">{
req.Messages[i] = copilotMessage{Role: m.Role, Content: m.Content}
}</span>
- <span class="cov8" title="1">if o.Temperature != 0 </span><span class="cov0" title="0">{
+ <span class="cov3" title="6">if o.Temperature != 0 </span><span class="cov0" title="0">{
req.Temperature = &amp;o.Temperature
- }</span> else<span class="cov8" title="1"> if defaultTemp != nil </span><span class="cov8" title="1">{
+ }</span> else<span class="cov3" title="6"> if defaultTemp != nil </span><span class="cov3" title="6">{
t := *defaultTemp
req.Temperature = &amp;t
}</span>
- <span class="cov8" title="1">if o.MaxTokens