Files

1 line
14 KiB
JSON

{"repo": "blank-16/llm-chat", "files": {"internal/doctor/doctor.go": {"7693c0500f21e3ac2ddaec2dcae2c6c5cf49e0e3": {"t": 1785746072.0239968, "n": 6448, "h": "f0ed0b306f81ae2e", "c": "// Package doctor runs a pre-flight health check on all configured components\n// and prints a colour-coded report. Invoked via: llm-chat doctor\npackage doctor\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"llm-chat/internal/config\"\n)\n\nconst checkTimeout = 8 * time.Second\n\ntype result struct {\n\tname string\n\tok bool\n\tdetail string\n}\n\n// Run executes all checks and prints a report to stdout.\nfunc Run(ctx context.Context, cfg *config.Config) {\n\tfmt.Println()\n\tfmt.Println(\"llm-chat doctor — checking configuration and connectivity\")\n\tfmt.Println(repeat(\"─\", 58))\n\tfmt.Println()\n\n\tresults := []result{\n\t\tcheckConfigFile(),\n\t\tcheckDB(cfg.DBPath),\n\t\tcheckPluginDir(),\n\t}\n\tresults = append(results, checkProviders(ctx, cfg)...)\n\tresults = append(results, checkBinaries()...)\n\n\tpass, fail := 0, 0\n\tfor _, r := range results {\n\t\tif r.ok {\n\t\t\tfmt.Printf(\" \\033[32m✓\\033[0m %-36s %s\\n\", r.name, dim(r.detail))\n\t\t\tpass++\n\t\t} else {\n\t\t\tfmt.Printf(\" \\033[31m✗\\033[0m %-36s \\033[33m%s\\033[0m\\n\", r.name, r.detail)\n\t\t\tfail++\n\t\t}\n\t}\n\n\tfmt.Println()\n\tif fail == 0 {\n\t\tfmt.Printf(\" \\033[32mAll %d checks passed.\\033[0m\\n\\n\", pass)\n\t} else {\n\t\tfmt.Printf(\" \\033[32m%d passed\\033[0m \\033[31m%d failed\\033[0m\\n\\n\", pass, fail)\n\t}\n}\n\nfunc checkConfigFile() result {\n\tpath, err := config.ConfigPath()\n\tif err != nil {\n\t\treturn result{\"Config file\", false, \"cannot determine path: \" + err.Error()}\n\t}\n\tif _, err := os.Stat(path); err != nil {\n\t\treturn result{\"Config file\", false, \"not found at \" + path}\n\t}\n\treturn result{\"Config file\", true, path}\n}\n\nfunc checkDB(dbPath string) result {\n\tif dbPath == \"\" {\n\t\thome, _ := os.UserHomeDir()\n\t\tdbPath = filepath.Join(home, \".llm-chat\", \"sessions.db\")\n\t}\n\tdir := filepath.Dir(dbPath)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn result{\"SQLite database\", false, \"cannot create directory: \" + err.Error()}\n\t}\n\t// Quick write test.\n\tf, err := os.OpenFile(dbPath, os.O_CREATE|os.O_WRONLY, 0600)\n\tif err != nil {\n\t\treturn result{\"SQLite database\", false, \"not writable: \" + err.Error()}\n\t}\n\tf.Close()\n\treturn result{\"SQLite database\", true, dbPath}\n}\n\nfunc checkPluginDir() result {\n\thome, _ := os.UserHomeDir()\n\tdir := filepath.Join(home, \".llm-chat\", \"plugins\")\n\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\treturn result{\"Plugin directory\", true, \"does not exist (no plugins loaded — OK)\"}\n\t}\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn result{\"Plugin directory\", false, \"cannot read: \" + err.Error()}\n\t}\n\texec := 0\n\tfor _, e := range entries {\n\t\tif !e.IsDir() {\n\t\t\tinfo, _ := e.Info()\n\t\t\tif info != nil && info.Mode()&0111 != 0 {\n\t\t\t\texec++\n\t\t\t}\n\t\t}\n\t}\n\treturn result{\"Plugin directory\", true, fmt.Sprintf(\"%s (%d executable)\", dir, exec)}\n}\n\nfunc checkProviders(ctx context.Context, cfg *config.Config) []result {\n\tvar out []result\n\n\tprobeCtx, cancel := context.WithTimeout(ctx, checkTimeout)\n\tdefer cancel()\n\n\tif cfg.Gemini.APIKey != \"\" {\n\t\tout = append(out, probeHTTP(probeCtx, \"Gemini API\",\n\t\t\t\"https://generativelanguage.googleapis.com/v1beta/models?key=\"+cfg.Gemini.APIKey))\n\t} else {\n\t\tout = append(out, result{\"Gemini API key\", false, \"not configured\"})\n\t}\n\n\tif cfg.OpenAI.APIKey != \"\" {\n\t\tbase := cfg.OpenAI.BaseURL\n\t\tif base == \"\" {\n\t\t\tbase = \"https://api.openai.com\"\n\t\t}\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"OpenAI API\", base+\"/v1/models\", \"Bearer \"+cfg.OpenAI.APIKey))\n\t}\n\n\tif cfg.Anthropic.APIKey != \"\" {\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"Anthropic API\",\n\t\t\t\"https://api.anthropic.com/v1/models\",\n\t\t\tcfg.Anthropic.APIKey, \"anthropic-version\", \"2023-06-01\"))\n\t}\n\n\tif cfg.DeepSeek.APIKey != \"\" {\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"DeepSeek API\",\n\t\t\t\"https://api.deepseek.com/v1/models\", \"Bearer \"+cfg.DeepSeek.APIKey))\n\t}\n\n\tif cfg.Kimi.APIKey != \"\" {\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"Kimi API\",\n\t\t\t\"https://api.moonshot.ai/v1/models\", \"Bearer \"+cfg.Kimi.APIKey))\n\t}\n\n\t// Ollama: check if local server is reachable.\n\tollamaURL := cfg.Ollama.BaseURL\n\tif ollamaURL == \"\" {\n\t\tollamaURL = \"http://localhost:11434\"\n\t}\n\tout = append(out, probeHTTP(probeCtx, \"Ollama server\", ollamaURL+\"/api/tags\"))\n\n\treturn out\n}\n\nfunc probeHTTP(ctx context.Context, name, url string) result {\n\treq, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn result{name, false, briefErr(err)}\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode == 401 || resp.StatusCode == 403 {\n\t\treturn result{name, false, fmt.Sprintf(\"authentication failed (%d)\", resp.StatusCode)}\n\t}\n\tif resp.StatusCode >= 500 {\n\t\treturn result{name, false, fmt.Sprintf(\"server error (%d)\", resp.StatusCode)}\n\t}\n\treturn result{name, true, fmt.Sprintf(\"reachable (%d)\", resp.StatusCode)}\n}\n\n// probeHTTPAuth probes with an Authorization header and optional extra headers.\nfunc probeHTTPAuth(ctx context.Context, name, url, authValue string, extraHeaders ...string) result {\n\treq, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\treq.Header.Set(\"Authorization\", authValue)\n\tfor i := 0; i+1 < len(extraHeaders); i += 2 {\n\t\treq.Header.Set(extraHeaders[i], extraHeaders[i+1])\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn result{name, false, briefErr(err)}\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode == 401 || resp.StatusCode == 403 {\n\t\treturn result{name, false, \"invalid API key\"}\n\t}\n\tif resp.StatusCode >= 500 {\n\t\treturn result{name, false, fmt.Sprintf(\"server error (%d)\", resp.StatusCode)}\n\t}\n\treturn result{name, true, \"API key valid\"}\n}\n\nfunc checkBinaries() []result {\n\tvar out []result\n\tbins := map[string]string{\n\t\t\"python3\": \"required for Python plugins\",\n\t\t\"go\": \"required for go install updates\",\n\t}\n\tif runtime.GOOS == \"darwin\" {\n\t\tbins[\"osascript\"] = \"required for /notify (macOS)\"\n\t} else {\n\t\tbins[\"notify-send\"] = \"required for /notify (Linux)\"\n\t}\n\tfor bin, desc := range bins {\n\t\t_, err := exec.LookPath(bin)\n\t\tout = append(out, result{\n\t\t\tname: bin + \" binary\",\n\t\t\tok: err == nil,\n\t\t\tdetail: map[bool]string{true: \"found\", false: \"not found — \" + desc}[err == nil],\n\t\t})\n\t}\n\treturn out\n}\n\nfunc briefErr(err error) string {\n\ts := err.Error()\n\tif len(s) > 60 {\n\t\ts = s[:60] + \"…\"\n\t}\n\treturn s\n}\n\nfunc dim(s string) string {\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"\\033[90m\" + s + \"\\033[0m\"\n}\n\nfunc repeat(s string, n int) string {\n\tout := \"\"\n\tfor i := 0; i < n; i++ {\n\t\tout += s\n\t}\n\treturn out\n}\n"}, "963e94b99cdc8b5f7539f747699ba8adf84cf633": {"t": 1785759520.1922216, "n": 6448, "h": "f0ed0b306f81ae2e", "c": "// Package doctor runs a pre-flight health check on all configured components\n// and prints a colour-coded report. Invoked via: llm-chat doctor\npackage doctor\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"llm-chat/internal/config\"\n)\n\nconst checkTimeout = 8 * time.Second\n\ntype result struct {\n\tname string\n\tok bool\n\tdetail string\n}\n\n// Run executes all checks and prints a report to stdout.\nfunc Run(ctx context.Context, cfg *config.Config) {\n\tfmt.Println()\n\tfmt.Println(\"llm-chat doctor — checking configuration and connectivity\")\n\tfmt.Println(repeat(\"─\", 58))\n\tfmt.Println()\n\n\tresults := []result{\n\t\tcheckConfigFile(),\n\t\tcheckDB(cfg.DBPath),\n\t\tcheckPluginDir(),\n\t}\n\tresults = append(results, checkProviders(ctx, cfg)...)\n\tresults = append(results, checkBinaries()...)\n\n\tpass, fail := 0, 0\n\tfor _, r := range results {\n\t\tif r.ok {\n\t\t\tfmt.Printf(\" \\033[32m✓\\033[0m %-36s %s\\n\", r.name, dim(r.detail))\n\t\t\tpass++\n\t\t} else {\n\t\t\tfmt.Printf(\" \\033[31m✗\\033[0m %-36s \\033[33m%s\\033[0m\\n\", r.name, r.detail)\n\t\t\tfail++\n\t\t}\n\t}\n\n\tfmt.Println()\n\tif fail == 0 {\n\t\tfmt.Printf(\" \\033[32mAll %d checks passed.\\033[0m\\n\\n\", pass)\n\t} else {\n\t\tfmt.Printf(\" \\033[32m%d passed\\033[0m \\033[31m%d failed\\033[0m\\n\\n\", pass, fail)\n\t}\n}\n\nfunc checkConfigFile() result {\n\tpath, err := config.ConfigPath()\n\tif err != nil {\n\t\treturn result{\"Config file\", false, \"cannot determine path: \" + err.Error()}\n\t}\n\tif _, err := os.Stat(path); err != nil {\n\t\treturn result{\"Config file\", false, \"not found at \" + path}\n\t}\n\treturn result{\"Config file\", true, path}\n}\n\nfunc checkDB(dbPath string) result {\n\tif dbPath == \"\" {\n\t\thome, _ := os.UserHomeDir()\n\t\tdbPath = filepath.Join(home, \".llm-chat\", \"sessions.db\")\n\t}\n\tdir := filepath.Dir(dbPath)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn result{\"SQLite database\", false, \"cannot create directory: \" + err.Error()}\n\t}\n\t// Quick write test.\n\tf, err := os.OpenFile(dbPath, os.O_CREATE|os.O_WRONLY, 0600)\n\tif err != nil {\n\t\treturn result{\"SQLite database\", false, \"not writable: \" + err.Error()}\n\t}\n\tf.Close()\n\treturn result{\"SQLite database\", true, dbPath}\n}\n\nfunc checkPluginDir() result {\n\thome, _ := os.UserHomeDir()\n\tdir := filepath.Join(home, \".llm-chat\", \"plugins\")\n\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\treturn result{\"Plugin directory\", true, \"does not exist (no plugins loaded — OK)\"}\n\t}\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn result{\"Plugin directory\", false, \"cannot read: \" + err.Error()}\n\t}\n\texec := 0\n\tfor _, e := range entries {\n\t\tif !e.IsDir() {\n\t\t\tinfo, _ := e.Info()\n\t\t\tif info != nil && info.Mode()&0111 != 0 {\n\t\t\t\texec++\n\t\t\t}\n\t\t}\n\t}\n\treturn result{\"Plugin directory\", true, fmt.Sprintf(\"%s (%d executable)\", dir, exec)}\n}\n\nfunc checkProviders(ctx context.Context, cfg *config.Config) []result {\n\tvar out []result\n\n\tprobeCtx, cancel := context.WithTimeout(ctx, checkTimeout)\n\tdefer cancel()\n\n\tif cfg.Gemini.APIKey != \"\" {\n\t\tout = append(out, probeHTTP(probeCtx, \"Gemini API\",\n\t\t\t\"https://generativelanguage.googleapis.com/v1beta/models?key=\"+cfg.Gemini.APIKey))\n\t} else {\n\t\tout = append(out, result{\"Gemini API key\", false, \"not configured\"})\n\t}\n\n\tif cfg.OpenAI.APIKey != \"\" {\n\t\tbase := cfg.OpenAI.BaseURL\n\t\tif base == \"\" {\n\t\t\tbase = \"https://api.openai.com\"\n\t\t}\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"OpenAI API\", base+\"/v1/models\", \"Bearer \"+cfg.OpenAI.APIKey))\n\t}\n\n\tif cfg.Anthropic.APIKey != \"\" {\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"Anthropic API\",\n\t\t\t\"https://api.anthropic.com/v1/models\",\n\t\t\tcfg.Anthropic.APIKey, \"anthropic-version\", \"2023-06-01\"))\n\t}\n\n\tif cfg.DeepSeek.APIKey != \"\" {\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"DeepSeek API\",\n\t\t\t\"https://api.deepseek.com/v1/models\", \"Bearer \"+cfg.DeepSeek.APIKey))\n\t}\n\n\tif cfg.Kimi.APIKey != \"\" {\n\t\tout = append(out, probeHTTPAuth(probeCtx, \"Kimi API\",\n\t\t\t\"https://api.moonshot.ai/v1/models\", \"Bearer \"+cfg.Kimi.APIKey))\n\t}\n\n\t// Ollama: check if local server is reachable.\n\tollamaURL := cfg.Ollama.BaseURL\n\tif ollamaURL == \"\" {\n\t\tollamaURL = \"http://localhost:11434\"\n\t}\n\tout = append(out, probeHTTP(probeCtx, \"Ollama server\", ollamaURL+\"/api/tags\"))\n\n\treturn out\n}\n\nfunc probeHTTP(ctx context.Context, name, url string) result {\n\treq, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn result{name, false, briefErr(err)}\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode == 401 || resp.StatusCode == 403 {\n\t\treturn result{name, false, fmt.Sprintf(\"authentication failed (%d)\", resp.StatusCode)}\n\t}\n\tif resp.StatusCode >= 500 {\n\t\treturn result{name, false, fmt.Sprintf(\"server error (%d)\", resp.StatusCode)}\n\t}\n\treturn result{name, true, fmt.Sprintf(\"reachable (%d)\", resp.StatusCode)}\n}\n\n// probeHTTPAuth probes with an Authorization header and optional extra headers.\nfunc probeHTTPAuth(ctx context.Context, name, url, authValue string, extraHeaders ...string) result {\n\treq, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\treq.Header.Set(\"Authorization\", authValue)\n\tfor i := 0; i+1 < len(extraHeaders); i += 2 {\n\t\treq.Header.Set(extraHeaders[i], extraHeaders[i+1])\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn result{name, false, briefErr(err)}\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode == 401 || resp.StatusCode == 403 {\n\t\treturn result{name, false, \"invalid API key\"}\n\t}\n\tif resp.StatusCode >= 500 {\n\t\treturn result{name, false, fmt.Sprintf(\"server error (%d)\", resp.StatusCode)}\n\t}\n\treturn result{name, true, \"API key valid\"}\n}\n\nfunc checkBinaries() []result {\n\tvar out []result\n\tbins := map[string]string{\n\t\t\"python3\": \"required for Python plugins\",\n\t\t\"go\": \"required for go install updates\",\n\t}\n\tif runtime.GOOS == \"darwin\" {\n\t\tbins[\"osascript\"] = \"required for /notify (macOS)\"\n\t} else {\n\t\tbins[\"notify-send\"] = \"required for /notify (Linux)\"\n\t}\n\tfor bin, desc := range bins {\n\t\t_, err := exec.LookPath(bin)\n\t\tout = append(out, result{\n\t\t\tname: bin + \" binary\",\n\t\t\tok: err == nil,\n\t\t\tdetail: map[bool]string{true: \"found\", false: \"not found — \" + desc}[err == nil],\n\t\t})\n\t}\n\treturn out\n}\n\nfunc briefErr(err error) string {\n\ts := err.Error()\n\tif len(s) > 60 {\n\t\ts = s[:60] + \"…\"\n\t}\n\treturn s\n}\n\nfunc dim(s string) string {\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"\\033[90m\" + s + \"\\033[0m\"\n}\n\nfunc repeat(s string, n int) string {\n\tout := \"\"\n\tfor i := 0; i < n; i++ {\n\t\tout += s\n\t}\n\treturn out\n}\n"}}}}