Skip to content

Commit c4cc8b1

Browse files
committed
test message
1 parent baecb64 commit c4cc8b1

5 files changed

Lines changed: 165 additions & 16 deletions

File tree

cmd/iterate/features_tools.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,38 @@ import (
2121
// ---------------------------------------------------------------------------
2222

2323
// spinnerActive is set to 1 while the spinner goroutine is printing.
24-
// Tool wrappers wait for it to reach 0 before showing a prompt.
2524
var spinnerActive atomic.Int32
2625

26+
// spinnerQuiet is closed by the spinner goroutine when it finishes clearing
27+
// the terminal line. Safe-mode prompts wait on this instead of busy-looping.
28+
var spinnerQuiet = make(chan struct{})
29+
30+
func init() {
31+
// Start closed so the first prompt doesn't block if no spinner ever ran.
32+
close(spinnerQuiet)
33+
}
34+
35+
// notifySpinnerQuiet replaces spinnerQuiet with a new open channel, then
36+
// closes the old one so any waiters unblock. Called by the spinner when done.
37+
func notifySpinnerQuiet() {
38+
old := spinnerQuiet
39+
spinnerQuiet = make(chan struct{})
40+
select {
41+
case <-old:
42+
default:
43+
close(old)
44+
}
45+
}
46+
47+
// waitForSpinner blocks until the spinner has stopped and cleared the line,
48+
// or until 500 ms have elapsed (so a stuck spinner never deadlocks a prompt).
49+
func waitForSpinner() {
50+
select {
51+
case <-spinnerQuiet:
52+
case <-time.After(500 * time.Millisecond):
53+
}
54+
}
55+
2756
// streamingTokenCount is incremented for each token received during streaming.
2857
// The spinner reads this to display tok/s.
2958
var streamingTokenCount atomic.Int64
@@ -245,9 +274,7 @@ func handleSafeModePrompt(cfg iterConfig, tool iteragent.Tool, args map[string]s
245274
}
246275
}
247276

248-
for spinnerActive.Load() == 1 {
249-
time.Sleep(5 * time.Millisecond)
250-
}
277+
waitForSpinner()
251278
fmt.Printf("\n%s⚠ Safe mode: allow %s?%s ", colorYellow, tool.Name, colorReset)
252279
answer, ok := selector.PromptLine("(y/N/always):")
253280
if !ok {

cmd/iterate/main_flags.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ func parseFlags() mainFlags {
3333
flag.IntVar(&f.issueMax, "issue-limit", 5, "Max community issues to include")
3434
flag.BoolVar(&f.socialOnly, "social", false, "Run social loop only (no evolution)")
3535
flag.BoolVar(&f.replyIssues, "reply-issues", true, "Post bot replies to addressed issues")
36-
flag.StringVar(&f.provider, "provider", "gemini", "Provider to use (anthropic, openai, groq, gemini)")
37-
flag.StringVar(&f.model, "model", "", "Model to use")
38-
flag.StringVar(&f.apiKey, "api-key", "", "API key (or set OPENCODE_API_KEY, GEMINI_API_KEY, etc.)")
36+
flag.StringVar(&f.provider, "provider", "gemini",
37+
"LLM provider: anthropic, openai, gemini, groq, ollama, azure, vertex, opencode (default: gemini)")
38+
flag.StringVar(&f.model, "model", "", "Model name override (e.g. claude-opus-4-6, gpt-4o, gemini-2.0-flash)")
39+
flag.StringVar(&f.apiKey, "api-key", "", "API key override (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, etc.)")
3940
flag.StringVar(&f.thinking, "thinking", "off", "Extended thinking depth: off, minimal, low, medium, high")
4041
flag.BoolVar(&f.chat, "chat", false, "Start interactive REPL (default when no other mode set)")
4142
flag.BoolVar(&f.evolve, "evolve", false, "Run one evolution session (non-interactive)")

cmd/iterate/provider.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package main
22

33
import (
4+
"context"
5+
"fmt"
46
"log/slog"
57
"os"
8+
"strings"
9+
"time"
610

711
iteragent "github.com/GrayCodeAI/iteragent"
812
"github.com/GrayCodeAI/iterate/internal/ui/selector"
@@ -44,13 +48,50 @@ func resolveThinkingLevel(flagThinking string, cfg iterConfig) string {
4448
}
4549

4650
// initProvider creates an LLM provider from the given name and API key.
47-
// It also wires the provider's context window into the selector for display.
51+
// It also wires the provider's context window into the selector for display,
52+
// and runs a background health check to surface auth errors early.
4853
func initProvider(providerName, apiKey string, logger *slog.Logger) (iteragent.Provider, error) {
4954
p, err := iteragent.NewProvider(providerName, apiKey)
5055
if err != nil {
5156
return nil, err
5257
}
5358
logger.Info("using provider", "name", p.Name())
5459
selector.ContextWindow = iteragent.ProviderContextWindow(p)
60+
61+
// Skip health check for Ollama (local — no auth needed) and when
62+
// the user explicitly passed --no-health-check (not currently a flag,
63+
// but the env var ITERATE_SKIP_HEALTH_CHECK=1 disables it).
64+
if os.Getenv("ITERATE_SKIP_HEALTH_CHECK") != "1" &&
65+
!strings.EqualFold(providerName, "ollama") {
66+
go runProviderHealthCheck(p, logger)
67+
}
68+
5569
return p, nil
5670
}
71+
72+
// runProviderHealthCheck sends a minimal prompt and prints a warning if the
73+
// provider returns an auth / connectivity error. Runs in a goroutine so it
74+
// doesn't block startup.
75+
func runProviderHealthCheck(p iteragent.Provider, logger *slog.Logger) {
76+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
77+
defer cancel()
78+
79+
ag := iteragent.New(p, nil, logger)
80+
events := ag.Prompt(ctx, "ping")
81+
for e := range events {
82+
if iteragent.EventType(e.Type) == iteragent.EventError {
83+
hint := authErrorHint(e.Content)
84+
fmt.Printf("\n%s⚠ Provider health check failed: %s%s\n", colorYellow, e.Content, colorReset)
85+
if hint != "" {
86+
fmt.Printf("%s Fix: %s%s\n", colorYellow, hint, colorReset)
87+
}
88+
logger.Warn("provider health check failed", "error", e.Content)
89+
return
90+
}
91+
// Got any non-error event — provider is reachable.
92+
if iteragent.EventType(e.Type) == iteragent.EventTokenUpdate {
93+
ag.Finish()
94+
return
95+
}
96+
}
97+
}

cmd/iterate/repl_streaming.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func spinner(stop <-chan struct{}, done chan<- struct{}, label string) {
6060
case <-stop:
6161
spinnerActive.Store(0)
6262
fmt.Print("\r\033[K")
63+
notifySpinnerQuiet()
6364
close(done)
6465
return
6566
default:
@@ -247,7 +248,12 @@ func processStreamEvent(e iteragent.Event, fullContent string, toolStart time.Ti
247248
case iteragent.EventContextCompacted:
248249
fmt.Printf("\r\033[K%s[context compacted]%s\n", colorDim, colorReset)
249250
case iteragent.EventError:
250-
fmt.Printf("\r\033[K%sError: %s%s\n", colorRed, e.Content, colorReset)
251+
msg := e.Content
252+
hint := authErrorHint(msg)
253+
fmt.Printf("\r\033[K%sError: %s%s\n", colorRed, msg, colorReset)
254+
if hint != "" {
255+
fmt.Printf("%sFix: %s%s\n", colorYellow, hint, colorReset)
256+
}
251257
}
252258
return fullContent, toolStart
253259
}
@@ -298,3 +304,41 @@ func printFinalStats(elapsed, ttft time.Duration, beforeTokens int, requestCostU
298304

299305
slog.Debug("request completed", "elapsed_ms", elapsed.Milliseconds(), "ttft_ms", ttft.Milliseconds(), "response_chars", len(fullContent), "total_tokens", sess.Tokens, "cost_usd", requestCostUSD)
300306
}
307+
308+
// authErrorHint returns a human-readable fix suggestion for known API error
309+
// patterns, or an empty string when the error doesn't look auth-related.
310+
func authErrorHint(errMsg string) string {
311+
lower := strings.ToLower(errMsg)
312+
switch {
313+
case strings.Contains(lower, "401") || strings.Contains(lower, "unauthorized") ||
314+
strings.Contains(lower, "invalid_api_key") || strings.Contains(lower, "authentication_error") ||
315+
strings.Contains(lower, "invalid api key"):
316+
provider := os.Getenv("ITERATE_PROVIDER")
317+
switch strings.ToLower(provider) {
318+
case "anthropic", "":
319+
return "Set ANTHROPIC_API_KEY in your shell: export ANTHROPIC_API_KEY=sk-ant-..."
320+
case "openai":
321+
return "Set OPENAI_API_KEY in your shell: export OPENAI_API_KEY=sk-..."
322+
case "gemini":
323+
return "Set GEMINI_API_KEY in your shell: export GEMINI_API_KEY=AIza..."
324+
case "groq":
325+
return "Set GROQ_API_KEY in your shell: export GROQ_API_KEY=gsk_..."
326+
case "ollama":
327+
return "Ollama should not require an API key — check that Ollama is running (ollama serve)"
328+
case "azure":
329+
return "Set AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT in your shell"
330+
default:
331+
return "Check that your API key environment variable is set correctly (see /providers)"
332+
}
333+
case strings.Contains(lower, "403") || strings.Contains(lower, "forbidden") ||
334+
strings.Contains(lower, "permission_denied"):
335+
return "Your API key may lack permissions for this model — check your billing/plan"
336+
case strings.Contains(lower, "429") || strings.Contains(lower, "rate_limit") ||
337+
strings.Contains(lower, "too many requests"):
338+
return "Rate limit hit — wait a moment and try again, or use /model to switch to a different model"
339+
case strings.Contains(lower, "no api key") || strings.Contains(lower, "api key not set") ||
340+
strings.Contains(lower, "missing api key"):
341+
return "Run /providers to see which environment variable needs to be set"
342+
}
343+
return ""
344+
}

internal/commands/config.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,16 +235,52 @@ func cmdNotify(ctx Context) Result {
235235

236236
func cmdEnv(ctx Context) Result {
237237
if !ctx.HasArg(1) {
238-
filter := []string{"ITERATE", "OLLAMA", "ANTHROPIC", "OPENAI", "GEMINI", "GROQ", "GITHUB", "GO"}
238+
// Prefixes: any env var whose name starts with one of these is shown.
239+
prefixes := []string{
240+
"ITERATE_", "OLLAMA_", "ANTHROPIC_", "OPENAI_", "GEMINI_",
241+
"GROQ_", "GITHUB_", "AZURE_", "VERTEX_", "OPENCODE_",
242+
}
243+
// Exact names also shown (without the _ prefix requirement).
244+
exact := []string{"GOPATH", "GOROOT", "HOME", "SHELL"}
245+
239246
fmt.Printf("%s── Environment ─────────────────────%s\n", ColorDim, ColorReset)
240-
for _, f := range filter {
241-
val := os.Getenv(f)
242-
if val != "" {
243-
if len(val) > 60 {
244-
val = val[:60] + "…"
247+
for _, e := range os.Environ() {
248+
kv := strings.SplitN(e, "=", 2)
249+
if len(kv) != 2 {
250+
continue
251+
}
252+
k, v := kv[0], kv[1]
253+
matched := false
254+
for _, p := range prefixes {
255+
if strings.HasPrefix(k, p) {
256+
matched = true
257+
break
258+
}
259+
}
260+
if !matched {
261+
for _, ex := range exact {
262+
if k == ex {
263+
matched = true
264+
break
265+
}
266+
}
267+
}
268+
if !matched {
269+
continue
270+
}
271+
display := v
272+
// Mask keys — show first 8 chars + …
273+
lk := strings.ToLower(k)
274+
if strings.Contains(lk, "key") || strings.Contains(lk, "token") || strings.Contains(lk, "secret") {
275+
if len(v) > 8 {
276+
display = v[:8] + "…"
277+
} else if len(v) > 0 {
278+
display = "***"
245279
}
246-
fmt.Printf(" %s=%s\n", f, val)
280+
} else if len(display) > 80 {
281+
display = display[:80] + "…"
247282
}
283+
fmt.Printf(" %s%s%s=%s\n", ColorBold, k, ColorReset, display)
248284
}
249285
fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset)
250286
return Result{Handled: true}

0 commit comments

Comments
 (0)