Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
53 KiB
JSON

{"repo": "aandrew-me/tgpt", "files": {"src/helper/helper.go": {"f4be93e0bde87f9568e0ac5b26910e371cf29e49": {"t": 1786068284.3430617, "n": 48531, "h": "7e0f1430757cd43e", "c": "package helper\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/aandrew-me/tgpt/v2/src/bubbletea\"\n\t\"github.com/aandrew-me/tgpt/v2/src/client\"\n\t\"github.com/aandrew-me/tgpt/v2/src/clipboard\"\n\t\"github.com/aandrew-me/tgpt/v2/src/providers\"\n\t\"github.com/aandrew-me/tgpt/v2/src/search\"\n\t\"github.com/aandrew-me/tgpt/v2/src/structs\"\n\t\"github.com/aandrew-me/tgpt/v2/src/tools\"\n\t\"github.com/aandrew-me/tgpt/v2/src/utils\"\n\thttp \"github.com/bogdanfinn/fhttp\"\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/ts\"\n\t\"golang.org/x/mod/semver\"\n)\n\ntype Data struct {\n\tVersion string `json:\"version\"`\n}\n\ntype Response struct {\n\tCompletion string `json:\"completion\"`\n}\n\ntype ImgResponse struct {\n\tImages []string `json:\"images\"`\n}\n\nvar (\n\tOperatingSystem string\n\tShellName string\n\tShellOptions []string\n\tShellConfigFile string\n)\n\ntype versionResponse struct {\n\tVersion string `json:\"version\"`\n}\n\nvar (\n\tbold = color.New(color.Bold)\n\tboldBlue = color.New(color.Bold, color.FgBlue)\n\tboldViolet = color.New(color.Bold, color.FgMagenta)\n\tcodeText = color.New(color.FgGreen, color.Bold)\n)\n\nvar lastSuccessfulProvider string\n\ntype streamFormatter struct {\n\ttickCount int\n\tpreviousWasTick bool\n\tisCode bool\n\tisGreen bool\n\tisTick bool\n\tisRealCode bool\n\tlineLength int\n\ttermWidth int\n\thasTermWidth bool\n\tprovider string\n}\n\nfunc newStreamFormatter(provider string) *streamFormatter {\n\tf := &streamFormatter{provider: provider}\n\tif size, err := ts.GetSize(); err == nil {\n\t\tf.termWidth = size.Col()\n\t\tf.hasTermWidth = true\n\t}\n\treturn f\n}\n\nfunc (f *streamFormatter) updateLineLength(word string) {\n\tif !f.hasTermWidth || f.provider == \"gemini\" {\n\t\treturn\n\t}\n\tif word == \"\\n\" {\n\t\tf.lineLength = 0\n\n\t\treturn\n\t}\n\twordLength := len([]rune(word))\n\tif f.termWidth-f.lineLength < wordLength {\n\t\tfmt.Print(\"\\n\")\n\t\tf.lineLength = 0\n\t}\n\tf.lineLength += wordLength\n}\n\nfunc (f *streamFormatter) writeChar(word string) {\n\tf.updateLineLength(word)\n\n\tif word == \"`\" {\n\t\tf.tickCount++\n\t\tf.isTick = true\n\t\tif f.tickCount == 2 && !f.previousWasTick {\n\t\t\tf.tickCount = 0\n\t\t} else if f.tickCount >= 6 && f.tickCount%2 == 0 && f.previousWasTick {\n\t\t\tf.tickCount = 0\n\t\t}\n\t\tf.isGreen = false\n\t\tf.isCode = false\n\t} else {\n\t\tif word == \"\\n\" {\n\t\t\tf.lineLength = 0\n\t\t}\n\t\tf.isTick = false\n\t\tif f.tickCount == 1 {\n\t\t\tf.isGreen = true\n\t\t} else if f.tickCount >= 3 {\n\t\t\tf.isCode = true\n\t\t}\n\t}\n\n\tswitch {\n\tcase f.isCode:\n\t\tcodeText.Print(word)\n\tcase f.isGreen:\n\t\tboldBlue.Print(word)\n\tcase !f.isTick:\n\t\tfmt.Print(word)\n\tdefault:\n\t\tif f.tickCount > 3 || f.isRealCode || (f.tickCount == 0 && f.previousWasTick) {\n\t\t\tfmt.Print(word)\n\t\t}\n\t}\n\n\tf.previousWasTick = word == \"`\"\n}\n\nfunc (f *streamFormatter) writeText(text string) {\n\tf.isRealCode = text == \"``\" || text == \"```\"\n\tfor _, ch := range text {\n\t\tf.writeChar(string(ch))\n\t}\n}\n\nvar xmlTagTargets = []string{\"cmd>\", \"/cmd>\", \"search>\", \"/search>\"}\n\nfunc isXMLTagPrefix(s string) bool {\n\tif len(s) < 1 || s[0] != '<' {\n\t\treturn false\n\t}\n\trest := s[1:]\n\tfor _, t := range xmlTagTargets {\n\t\tif strings.HasPrefix(t, rest) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isCompleteXMLTag(s string) bool {\n\tswitch s {\n\tcase \"<cmd>\", \"</cmd>\", \"<search>\", \"</search>\":\n\t\treturn true\n\t}\n\treturn false\n}\n\ntype interactiveFormatter struct {\n\t*streamFormatter\n\txmlBuffer strings.Builder\n\tinXMLTag bool\n}\n\nfunc newInteractiveFormatter(provider string) *interactiveFormatter {\n\treturn &interactiveFormatter{streamFormatter: newStreamFormatter(provider)}\n}\n\nfunc (f *interactiveFormatter) flushXMLBuffer() {\n\tbuf := f.xmlBuffer.String()\n\tf.inXMLTag = false\n\tf.xmlBuffer.Reset()\n\n\tif buf == \"\" {\n\t\treturn\n\t}\n\tf.streamFormatter.writeChar(buf[:1])\n\tf.writeText(buf[1:])\n}\n\nfunc (f *interactiveFormatter) writeText(text string) {\n\tfor _, ch := range text {\n\t\tchar := string(ch)\n\t\tif !f.inXMLTag {\n\t\t\tif ch == '<' {\n\t\t\t\tf.inXMLTag = true\n\t\t\t\tf.xmlBuffer.Reset()\n\t\t\t\tf.xmlBuffer.WriteRune(ch)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tf.streamFormatter.writeChar(char)\n\t\t\tcontinue\n\t\t}\n\t\tf.xmlBuffer.WriteRune(ch)\n\t\tbuf := f.xmlBuffer.String()\n\t\tswitch {\n\t\tcase isCompleteXMLTag(buf):\n\t\t\tf.inXMLTag = false\n\t\t\tf.xmlBuffer.Reset()\n\t\tcase !isXMLTagPrefix(buf):\n\t\t\tf.flushXMLBuffer()\n\t\t}\n\t}\n}\n\nfunc GetData(input string, params structs.Params, extraOptions structs.ExtraOptions) ([]interface{}, string) {\n\tresponseTxt, turnMessages, err := MakeRequestAndGetData(input, params, extraOptions)\n\tif err != nil {\n\t\treturn nil, \"\"\n\t}\n\n\tfmt.Print(\"\\n\\n\")\n\n\t// turnMessages holds the fully-ordered messages for this turn (user input,\n\t// any tool call/tool result pairs, and the final assistant response) when\n\t// tool calling was involved. Fall back to a plain user/assistant pair\n\t// otherwise.\n\tif len(turnMessages) > 0 {\n\t\treturn turnMessages, responseTxt\n\t}\n\n\treturn []interface{}{\n\t\tstructs.DefaultMessage{Content: input, Role: \"user\"},\n\t\tstructs.DefaultMessage{Content: responseTxt, Role: \"assistant\"},\n\t}, responseTxt\n}\n\nvar spinChars = []string{\"⣾\", \"⣽\", \"⣻\", \"⢿\", \"⡿\", \"⣟\", \"⣯\", \"⣷\"}\n\ntype Spinner struct {\n\tstop atomic.Bool\n\tdone chan struct{}\n\tmu sync.Mutex\n\tmessage string\n\twidth int\n}\n\nfunc StartSpinner(message string) *Spinner {\n\ts := &Spinner{\n\t\tmessage: message,\n\t\tdone: make(chan struct{}),\n\t}\n\tgo s.run()\n\treturn s\n}\n\nfunc (s *Spinner) SetMessage(message string) {\n\tif s == nil {\n\t\treturn\n\t}\n\ts.mu.Lock()\n\ts.message = message\n\ts.mu.Unlock()\n}\n\nfunc (s *Spinner) Stop() {\n\tif s == nil {\n\t\treturn\n\t}\n\ts.stop.Store(true)\n\t<-s.done\n}\n\nvar (\n\tspinnerMu sync.Mutex\n\tactiveSpinner *Spinner\n)\n\n// Whether to show status during tool calls\nfunc statusEnabled(extraOptions structs.ExtraOptions) bool {\n\treturn !extraOptions.IsGetSilent && !extraOptions.IsGetWhole\n}\n\nfunc showStatus(enabled bool, message string) {\n\tif !enabled {\n\t\treturn\n\t}\n\tspinnerMu.Lock()\n\tdefer spinnerMu.Unlock()\n\tif activeSpinner == nil {\n\t\tactiveSpinner = StartSpinner(message)\n\t\treturn\n\t}\n\tactiveSpinner.SetMessage(message)\n}\n\nfunc hideStatus() {\n\tspinnerMu.Lock()\n\ts := activeSpinner\n\tactiveSpinner = nil\n\tspinnerMu.Unlock()\n\ts.Stop()\n}\n\nfunc (s *Spinner) run() {\n\tdefer close(s.done)\n\n\ti := 0\n\tfor !s.stop.Load() {\n\t\ts.mu.Lock()\n\t\tline := spinChars[i] + \" \" + s.message\n\t\tif runes := len([]rune(line)); runes > s.width {\n\t\t\ts.width = runes\n\t\t}\n\t\ts.mu.Unlock()\n\n\t\tfmt.Print(\"\\r\" + line)\n\t\ti = (i + 1) % len(spinChars)\n\t\ttime.Sleep(80 * time.Millisecond)\n\t}\n\n\ts.mu.Lock()\n\twidth := s.width\n\ts.mu.Unlock()\n\tfmt.Print(\"\\r\" + strings.Repeat(\" \", width) + \"\\r\\033[K\")\n}\n\nfunc canWriteToDir(path string) bool {\n\tdir := filepath.Dir(path)\n\ttmpFile, err := os.CreateTemp(dir, \".perm_test_*\")\n\tif err != nil {\n\t\treturn false\n\t}\n\ttmpFile.Close()\n\tos.Remove(tmpFile.Name())\n\treturn true\n}\n\nfunc isOwnedBySystemPkgMgr(execPath string) bool {\n\tif pacman, err := exec.LookPath(\"pacman\"); err == nil {\n\t\tif err := exec.Command(pacman, \"-Qo\", execPath).Run(); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\tif dpkg, err := exec.LookPath(\"dpkg\"); err == nil {\n\t\tif err := exec.Command(dpkg, \"-S\", execPath).Run(); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\tif rpm, err := exec.LookPath(\"rpm\"); err == nil {\n\t\tif err := exec.Command(rpm, \"-qf\", execPath).Run(); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc DetectPackageManager(executablePath string) (isPkgMgr bool, pkgName string, updateCmd string) {\n\tlowerPath := strings.ToLower(filepath.ToSlash(executablePath))\n\n\t// Scoop (Windows)\n\tif strings.Contains(lowerPath, \"/scoop/\") {\n\t\treturn true, \"Scoop\", \"scoop update tgpt\"\n\t}\n\n\t// Chocolatey (Windows)\n\tif strings.Contains(lowerPath, \"/chocolatey/\") || strings.Contains(lowerPath, \"/choco/\") {\n\t\treturn true, \"Chocolatey\", \"choco upgrade tgpt\"\n\t}\n\n\t// Homebrew (macOS / Linux)\n\tif strings.Contains(lowerPath, \"/cellar/\") || strings.Contains(lowerPath, \"/homebrew/\") || strings.Contains(lowerPath, \"/linuxbrew/\") {\n\t\treturn true, \"Homebrew\", \"brew upgrade tgpt\"\n\t}\n\n\t// System package manager (Linux pacman / dpkg / rpm)\n\tif runtime.GOOS == \"linux\" && isOwnedBySystemPkgMgr(executablePath) {\n\t\treturn true, \"system package manager\", \"use your package manager (e.g. pacman, apt, dnf) to update\"\n\t}\n\n\t// Go install (go/bin/tgpt)\n\tgobin := os.Getenv(\"GOBIN\")\n\tif gobin != \"\" && strings.HasPrefix(lowerPath, strings.ToLower(filepath.ToSlash(gobin))) {\n\t\treturn true, \"Go\", \"go install github.com/aandrew-me/tgpt/v2@latest\"\n\t}\n\tgopath := os.Getenv(\"GOPATH\")\n\tif gopath != \"\" {\n\t\tfor _, entry := range filepath.SplitList(gopath) {\n\t\t\tentrySlash := strings.ToLower(filepath.ToSlash(entry))\n\t\t\tif entrySlash != \"\" && strings.Contains(lowerPath, entrySlash+\"/bin\") {\n\t\t\t\treturn true, \"Go\", \"go install github.com/aandrew-me/tgpt/v2@latest\"\n\t\t\t}\n\t\t}\n\t}\n\tif strings.Contains(lowerPath, \"/go/bin/\") {\n\t\treturn true, \"Go\", \"go install github.com/aandrew-me/tgpt/v2@latest\"\n\t}\n\n\treturn false, \"\", \"\"\n}\n\nfunc escapePowerShellArg(s string) string {\n\treturn \"'\" + strings.ReplaceAll(s, \"'\", \"''\") + \"'\"\n}\n\nfunc Update(localVersion string, executablePath string) {\n\tif runtime.GOOS == \"android\" {\n\t\tfmt.Println(\"This feature is not supported on your Operating System\")\n\t\treturn\n\t}\n\n\tif isPkgMgr, pkgName, updateCmd := DetectPackageManager(executablePath); isPkgMgr {\n\t\tfmt.Printf(\"tgpt was installed via a package manager (%s).\\n\", pkgName)\n\t\tfmt.Printf(\"Please update it using your package manager: %s\\n\", updateCmd)\n\t\treturn\n\t}\n\n\turl := \"https://raw.githubusercontent.com/aandrew-me/tgpt/main/version.txt\"\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error creating request:\", err)\n\t\treturn\n\t}\n\n\thttpClient, err := client.NewClient()\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error creating HTTP client:\", err)\n\t\treturn\n\t}\n\n\tres, err := httpClient.Do(req)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error fetching remote version:\", err)\n\t\treturn\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\tfmt.Fprintf(os.Stderr, \"Error fetching version: HTTP %d\\n\", res.StatusCode)\n\t\treturn\n\t}\n\n\tvar verData versionResponse\n\tif err := json.NewDecoder(res.Body).Decode(&verData); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error parsing version JSON:\", err)\n\t\treturn\n\t}\n\n\tlocalSemver := \"v\" + strings.TrimPrefix(localVersion, \"v\")\n\tremoteSemver := \"v\" + strings.TrimPrefix(verData.Version, \"v\")\n\n\tif semver.Compare(localSemver, remoteSemver) >= 0 {\n\t\tfmt.Println(\"You are already using the latest version:\", remoteSemver)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Updating from %s to %s...\\n\", localSemver, remoteSemver)\n\n\tvar cmd *exec.Cmd\n\tif runtime.GOOS == \"windows\" {\n\t\tscript := fmt.Sprintf(\"& { $(irm https://raw.githubusercontent.com/aandrew-me/tgpt/refs/heads/main/install-win.ps1) } -Path %s\", escapePowerShellArg(executablePath))\n\t\tcmd = exec.Command(\"powershell\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", script)\n\t} else {\n\t\tuseSudo := !canWriteToDir(executablePath)\n\t\tvar script string\n\t\tif useSudo {\n\t\t\tfmt.Println(\"Elevated privileges required to write to:\", filepath.Dir(executablePath))\n\t\t\tfmt.Println(\"Requesting sudo access...\")\n\t\t\tscript = `set -o pipefail; curl -sSLf https://raw.githubusercontent.com/aandrew-me/tgpt/main/install | sudo bash -s -- \"$1\"`\n\t\t} else {\n\t\t\tscript = `set -o pipefail; curl -sSLf https://raw.githubusercontent.com/aandrew-me/tgpt/main/install | bash -s -- \"$1\"`\n\t\t}\n\n\t\t// Pass executablePath as positional parameter $1 to safely prevent shell injection.\n\t\tcmd = exec.Command(\"bash\", \"-c\", script, \"bash\", executablePath)\n\t}\n\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"\\nFailed to update. Error:\", err)\n\t\treturn\n\t}\n\n\tfmt.Println(\"Successfully updated.\")\n}\n\nfunc CodeGenerate(input string, params structs.Params, extraOptions structs.ExtraOptions) {\n\tcodePrompt := fmt.Sprintf(\n\t\t\"Your Role: Provide only code as output without any description.\\n\"+\n\t\t\t\"IMPORTANT: Provide only plain text without Markdown formatting.\\n\"+\n\t\t\t\"IMPORTANT: Do not include markdown formatting.\\n\"+\n\t\t\t\"If there is a lack of details, provide most logical solution. You are not allowed to ask for more details.\\n\"+\n\t\t\t\"Ignore any potential risk of errors or confusion.\\n\\n\"+\n\t\t\t\"Request:%s\\nCode:\", input,\n\t)\n\n\t_, _, _ = MakeRequestAndGetData(codePrompt, params, extraOptions)\n}\n\nfunc SetShellAndOSVars() {\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tOperatingSystem = \"Windows\"\n\t\tif len(os.Getenv(\"PSModulePath\")) > 0 {\n\t\t\tShellName = \"powershell.exe\"\n\t\t\tShellOptions = []string{\"-Command\"}\n\t\t} else {\n\t\t\tShellName = \"cmd.exe\"\n\t\t\tShellOptions = []string{\"/C\"}\n\t\t}\n\t\tShellConfigFile = \"\"\n\t\treturn\n\tcase \"darwin\":\n\t\tOperatingSystem = \"MacOS\"\n\tcase \"linux\":\n\t\tdistro := \"\"\n\t\tif path, err := exec.LookPath(\"lsb_release\"); err == nil {\n\t\t\tif result, err := exec.Command(path, \"-si\").Output(); err == nil {\n\t\t\t\tdistro = strings.TrimSpace(string(result))\n\t\t\t}\n\t\t}\n\t\tOperatingSystem = \"Linux/\" + distro\n\tdefault:\n\t\tOperatingSystem = runtime.GOOS\n\t}\n\n\thomeDir := os.Getenv(\"HOME\")\n\tshellEnv := os.Getenv(\"SHELL\")\n\n\tswitch {\n\tcase strings.Contains(shellEnv, \"zsh\"):\n\t\tShellName = shellEnv\n\t\tShellConfigFile = homeDir + \"/.zshrc\"\n\tcase strings.Contains(shellEnv, \"bash\"):\n\t\tShellName = shellEnv\n\t\tShellConfigFile = homeDir + \"/.bashrc\"\n\tcase strings.Contains(shellEnv, \"fish\"):\n\t\tShellName = shellEnv\n\t\tShellConfigFile = homeDir + \"/.config/fish/config.fish\"\n\tcase shellEnv != \"\":\n\t\tShellName = shellEnv\n\t\tShellConfigFile = homeDir + \"/.bashrc\"\n\tdefault:\n\t\tif _, err := exec.LookPath(\"bash\"); err == nil {\n\t\t\tShellName = \"bash\"\n\t\t\tShellConfigFile = homeDir + \"/.bashrc\"\n\t\t} else {\n\t\t\tShellName = \"/bin/sh\"\n\t\t\tShellConfigFile = homeDir + \"/.profile\"\n\t\t}\n\t}\n\tShellOptions = []string{\"-c\"}\n}\n\nfunc ShellCommand(input string, params structs.Params, extraOptions structs.ExtraOptions) {\n\tSetShellAndOSVars()\n\tshellPrompt := fmt.Sprintf(\n\t\t\"Your role: Provide only plain text without Markdown formatting. \"+\n\t\t\t\"Do not show any warnings or information regarding your capabilities. \"+\n\t\t\t\"Do not provide any description. If you need to store any data, assume it will be stored in the chat. \"+\n\t\t\t\"Provide only %s command for %s without any description. \"+\n\t\t\t\"If there is a lack of details, provide most logical solution. \"+\n\t\t\t\"Ensure the output is a valid shell command. If multiple steps required try to combine them together. \"+\n\t\t\t\"Prompt: %s\\n\\nCommand:\",\n\t\tShellName, OperatingSystem, input,\n\t)\n\tGetCommand(shellPrompt, params, extraOptions)\n}\n\nfunc GetCommand(shellPrompt string, params structs.Params, extraOptions structs.ExtraOptions) {\n\t_, _, _ = MakeRequestAndGetData(shellPrompt, params, extraOptions)\n}\n\ntype RESPONSE struct {\n\tTagname string `json:\"tag_name\"`\n\tBody string `json:\"body\"`\n}\n\nfunc GetVersionHistory() {\n\treq, err := http.NewRequest(\"GET\", \"https://api.github.com/repos/aandrew-me/tgpt/releases/latest\", nil)\n\tif err != nil {\n\t\tfmt.Fprint(os.Stderr, \"Some error has occurred\\n\\n\")\n\t\tfmt.Fprintln(os.Stderr, \"Error:\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpClient, err := client.NewClient()\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error creating client:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tres, err := httpClient.Do(req)\n\tif err != nil {\n\t\tfmt.Fprint(os.Stderr, \"Check your internet connection\\n\\n\")\n\t\tfmt.Fprintln(os.Stderr, \"Error:\", err)\n\t\tos.Exit(1)\n\t}\n\tdefer res.Body.Close()\n\n\tresBody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n\n\tvar release RESPONSE\n\tif err := json.Unmarshal(resBody, &release); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error parsing release JSON:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tboldBlue.Println(\"Release\", release.Tagname)\n\tfmt.Println(release.Body)\n\tfmt.Println()\n}\n\nfunc GetWholeText(input string, extraOptions structs.ExtraOptions, params structs.Params) {\n\t_, _, _ = MakeRequestAndGetData(input, params, extraOptions)\n}\n\nfunc GetLastCodeBlock(markdown string) string {\n\treturn utils.GetLastCodeBlock(markdown)\n}\n\nfunc formatToolArgs(rawArgs string) string {\n\tvar args map[string]any\n\tif err := json.Unmarshal([]byte(rawArgs), &args); err != nil {\n\t\tif len(rawArgs) > 100 {\n\t\t\trunes := []rune(rawArgs)\n\t\t\treturn string(runes[:100]) + \"...\"\n\t\t}\n\t\treturn rawArgs\n\t}\n\n\tkeys := make([]string, 0, len(args))\n\tfor k := range args {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\n\tmaxValLen := 60\n\tparts := make([]string, 0, len(keys))\n\tfor _, k := range keys {\n\t\tv := args[k]\n\t\tvar valStr string\n\t\tswitch val := v.(type) {\n\t\tcase string:\n\t\t\tif len(val) > maxValLen {\n\t\t\t\trunes := []rune(val)\n\t\t\t\tvalStr = fmt.Sprintf(\"%q...\", string(runes[:maxValLen]))\n\t\t\t} else {\n\t\t\t\tvalStr = fmt.Sprintf(\"%q\", val)\n\t\t\t}\n\t\tdefault:\n\t\t\tb, err := json.Marshal(val)\n\t\t\tstr := string(b)\n\t\t\tif err != nil {\n\t\t\t\tstr = fmt.Sprintf(\"%v\", val)\n\t\t\t}\n\t\t\tif len(str) > maxValLen {\n\t\t\t\trunes := []rune(str)\n\t\t\t\tvalStr = string(runes[:maxValLen]) + \"...\"\n\t\t\t} else {\n\t\t\t\tvalStr = str\n\t\t\t}\n\t\t}\n\t\tparts = append(parts, fmt.Sprintf(\"%s=%s\", k, valStr))\n\t}\n\n\tresult := strings.Join(parts, \", \")\n\tmaxTotalLen := 120\n\tif len(result) > maxTotalLen {\n\t\trunes := []rune(result)\n\t\treturn string(runes[:maxTotalLen]) + \"...\"\n\t}\n\treturn result\n}\n\ntype toolCallAccumulator struct {\n\tid string\n\tname string\n\targs strings.Builder\n}\n\nfunc HandleEachPart(resp *http.Response, input string, params structs.Params, extraOptions structs.ExtraOptions) (string, []interface{}) {\n\tscanner := bufio.NewScanner(resp.Body)\n\tformatter := newStreamFormatter(params.Provider)\n\tfullText := \"\"\n\ttoolCallMap := make(map[int]*toolCallAccumulator)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tmainText := providers.GetMainText(line, params.Provider, input)\n\t\tif len(mainText) > 0 {\n\t\t\tfullText += mainText\n\t\t\tformatter.writeText(mainText)\n\t\t}\n\n\t\tvar obj = \"{}\"\n\t\tif after, ok := strings.CutPrefix(line, \"data: \"); ok {\n\t\t\tobj = after\n\t\t}\n\t\tvar d structs.CommonResponse\n\t\tif err := json.Unmarshal([]byte(obj), &d); err == nil && len(d.Choices) > 0 {\n\t\t\tfor _, tcDelta := range d.Choices[0].Delta.ToolCalls {\n\t\t\t\tacc, ok := toolCallMap[tcDelta.Index]\n\t\t\t\tif !ok {\n\t\t\t\t\tacc = &toolCallAccumulator{}\n\t\t\t\t\ttoolCallMap[tcDelta.Index] = acc\n\t\t\t\t}\n\t\t\t\tif tcDelta.ID != \"\" {\n\t\t\t\t\tacc.id = tcDelta.ID\n\t\t\t\t}\n\t\t\t\tif tcDelta.Function.Name != \"\" {\n\t\t\t\t\tacc.name = tcDelta.Function.Name\n\t\t\t\t}\n\t\t\t\tif tcDelta.Function.Arguments != \"\" {\n\t\t\t\t\tacc.args.WriteString(tcDelta.Function.Arguments)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Some error has occurred. Error:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tif len(toolCallMap) > 0 {\n\t\tkeys := make([]int, 0, len(toolCallMap))\n\t\tfor k := range toolCallMap {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\tsort.Ints(keys)\n\n\t\tvar toolCalls []structs.ToolCall\n\t\tfor _, k := range keys {\n\t\t\tacc := toolCallMap[k]\n\t\t\tif acc != nil && acc.name != \"\" {\n\t\t\t\ttoolCalls = append(toolCalls, structs.ToolCall{\n\t\t\t\t\tID: acc.id,\n\t\t\t\t\tType: \"function\",\n\t\t\t\t\tFunction: structs.ToolCallFunction{\n\t\t\t\t\t\tName: acc.name,\n\t\t\t\t\t\tArguments: acc.args.String(),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(toolCalls) > 0 {\n\t\t\tresp.Body.Close()\n\n\t\t\tif fullText != \"\" && !strings.HasSuffix(fullText, \"\\n\") {\n\t\t\t\tfmt.Println()\n\t\t\t}\n\n\t\t\t// Terminal follow-up: never process tool calls again.\n\t\t\tif extraOptions.ToolDepth >= 5 && extraOptions.IsToolFollowUp {\n\t\t\t\treturn fullText, nil\n\t\t\t}\n\n\t\t\tif extraOptions.ToolDepth >= 5 {\n\t\t\t\tfmt.Fprintln(os.Stderr, \"\\nReached maximum tool execution depth. Stopping tool calls.\")\n\t\t\t\tnoToolsParams := params\n\t\t\t\tnoToolsParams.Tools = nil\n\t\t\t\tfollowUpOptions := extraOptions\n\t\t\t\tfollowUpOptions.IsToolFollowUp = true\n\t\t\t\tfollowUpText, followUpTurnMessages, _ := MakeRequestAndGetData(\"\", noToolsParams, followUpOptions)\n\t\t\t\tif len(followUpTurnMessages) > 0 {\n\t\t\t\t\treturn fullText + followUpText, followUpTurnMessages\n\t\t\t\t}\n\t\t\t\tfinalAssistantMsg := structs.DefaultMessage{\n\t\t\t\t\tRole: \"assistant\",\n\t\t\t\t\tContent: followUpText,\n\t\t\t\t}\n\t\t\t\treturn fullText + followUpText, []any{finalAssistantMsg}\n\t\t\t}\n\n\t\t\tturnMessages := make([]any, 0)\n\n\t\t\tif input != \"\" {\n\t\t\t\tuserMsg := structs.DefaultMessage{\n\t\t\t\t\tRole: \"user\",\n\t\t\t\t\tContent: input,\n\t\t\t\t}\n\t\t\t\tparams.PrevMessages = append(params.PrevMessages, userMsg)\n\t\t\t\tturnMessages = append(turnMessages, userMsg)\n\t\t\t}\n\n\t\t\tassistantMsg := structs.AssistantToolCallMessage{\n\t\t\t\tRole: \"assistant\",\n\t\t\t\tToolCalls: toolCalls,\n\t\t\t}\n\t\t\tparams.PrevMessages = append(params.PrevMessages, assistantMsg)\n\t\t\tturnMessages = append(turnMessages, assistantMsg)\n\n\t\t\tstatusOn := statusEnabled(extraOptions)\n\n\t\t\tfor _, tc := range toolCalls {\n\t\t\t\tif extraOptions.Verbose {\n\t\t\t\t\tboldBlue.Printf(\"\\n[Tool Call] %s(%s)\\n\", tc.Function.Name, tc.Function.Arguments)\n\t\t\t\t}\n\n\t\t\t\tif tc.Function.Name == \"execute_command\" && !extraOptions.AutoExec {\n\t\t\t\t\thideStatus()\n\t\t\t\t} else {\n\t\t\t\t\tshowStatus(statusOn, \"Running \"+tc.Function.Name)\n\t\t\t\t}\n\n\t\t\t\tpreConfirmCtx := context.Background()\n\t\t\t\tif extraOptions.AutoExec {\n\t\t\t\t\tpreConfirmCtx = context.WithValue(preConfirmCtx, tools.AutoExecKey, true)\n\t\t\t\t}\n\n\t\t\t\tvar toolOutput string\n\t\t\t\tvar err error\n\t\t\t\tproceed, cancelMsg := tools.PreConfirm(preConfirmCtx, tc.Function.Name, tc.Function.Arguments)\n\t\t\t\tif !proceed {\n\t\t\t\t\ttoolOutput = cancelMsg\n\t\t\t\t\terr = fmt.Errorf(\"%s\", cancelMsg)\n\t\t\t\t} else {\n\t\t\t\t\t// The confirmation (if any) has already been obtained above,\n\t\t\t\t\t// so the 60s execution timeout starts only now and is not\n\t\t\t\t\t// consumed by time spent waiting on user input.\n\t\t\t\t\texecCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n\t\t\t\t\texecCtx = context.WithValue(execCtx, tools.ConfirmedKey, true)\n\t\t\t\t\tif extraOptions.AutoExec {\n\t\t\t\t\t\texecCtx = context.WithValue(execCtx, tools.AutoExecKey, true)\n\t\t\t\t\t}\n\n\t\t\t\t\ttoolOutput, err = tools.DefaultRegistry.Execute(execCtx, tc.Function.Name, tc.Function.Arguments)\n\t\t\t\t\tcancel()\n\t\t\t\t}\n\t\t\t\thideStatus()\n\n\t\t\t\tif err != nil && proceed {\n\t\t\t\t\ttoolOutput = fmt.Sprintf(\"Error executing tool: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif !extraOptions.Verbose && extraOptions.IsNormal {\n\t\t\t\t\tmark := \"\\u2705\"\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tmark = \"\\u274c\"\n\t\t\t\t\t}\n\t\t\t\t\tboldViolet.Printf(\"Used Tool %s(%s) %s\\n\", tc.Function.Name, formatToolArgs(tc.Function.Arguments), mark)\n\t\t\t\t}\n\n\t\t\t\tif extraOptions.Verbose {\n\t\t\t\t\tbold.Printf(\"[Tool Output] %s\\n\", toolOutput)\n\t\t\t\t}\n\n\t\t\t\ttoolMsg := structs.ToolMessage{\n\t\t\t\t\tRole: \"tool\",\n\t\t\t\t\tToolCallID: tc.ID,\n\t\t\t\t\tName: tc.Function.Name,\n\t\t\t\t\tContent: toolOutput,\n\t\t\t\t}\n\t\t\t\tparams.PrevMessages = append(params.PrevMessages, toolMsg)\n\t\t\t\tturnMessages = append(turnMessages, toolMsg)\n\t\t\t}\n\n\t\t\tfollowUpOptions := extraOptions\n\t\t\tfollowUpOptions.IsToolFollowUp = true\n\t\t\tfollowUpOptions.ToolDepth++\n\n\t\t\tfollowUpText, followUpTurnMessages, _ := MakeRequestAndGetData(\"\", params, followUpOptions)\n\n\t\t\tif len(followUpTurnMessages) > 0 {\n\t\t\t\tturnMessages = append(turnMessages, followUpTurnMessages...)\n\t\t\t\treturn fullText + followUpText, turnMessages\n\t\t\t}\n\n\t\t\tfinalAssistantMsg := structs.DefaultMessage{\n\t\t\t\tRole: \"assistant\",\n\t\t\t\tContent: followUpText,\n\t\t\t}\n\t\t\tturnMessages = append(turnMessages, finalAssistantMsg)\n\n\t\t\treturn fullText + followUpText, turnMessages\n\t\t}\n\t}\n\n\treturn fullText, nil\n}\n\nfunc HandleEachPartInteractiveShell(resp *http.Response, input string, params structs.Params) string {\n\tscanner := bufio.NewScanner(resp.Body)\n\tformatter := newInteractiveFormatter(params.Provider)\n\tfullText := \"\"\n\n\tfor scanner.Scan() {\n\t\tmainText := providers.GetMainText(scanner.Text(), params.Provider, input)\n\t\tif len(mainText) < 1 {\n\t\t\tcontinue\n\t\t}\n\t\tfullText += mainText\n\t\tformatter.writeText(mainText)\n\t}\n\n\t// Flush any buffered non-tag content left in the XML buffer\n\tif formatter.inXMLTag && formatter.xmlBuffer.Len() > 0 {\n\t\tformatter.flushXMLBuffer()\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Some error has occurred. Error:\", err)\n\t\treturn \"\"\n\t}\n\n\treturn fullText\n}\n\nfunc printConnectionErrorMsg(err error) {\n\tbold.Fprintln(os.Stderr, \"\\rSome error has occurred. Check your internet connection.\")\n\tfmt.Fprintln(os.Stderr, \"\\nError:\", err)\n\tos.Exit(1)\n}\n\nfunc handleStatus400(resp *http.Response) {\n\tbold.Fprintln(os.Stderr, \"\\rSome error has occurred. Statuscode:\", resp.StatusCode)\n\trespBody, _ := io.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tfmt.Println(string(respBody))\n\tos.Exit(1)\n}\n\nfunc ExecuteCommand(shellName string, shellOptions []string, fullLine string) string {\n\treturn ExecuteCommandWithCapture(shellName, shellOptions, fullLine, false, false)\n}\n\nfunc ExecuteCommandWithAlias(shellName string, shellOptions []string, fullLine string) string {\n\treturn ExecuteCommandWithCapture(shellName, shellOptions, fullLine, false, true)\n}\n\nfunc ExecuteCommandWithCapture(shellName string, shellOptions []string, fullLine string, captureOutput bool, useAliases bool) string {\n\tif runtime.GOOS != \"windows\" {\n\t\trawModeOff := exec.Command(\"stty\", \"-raw\", \"echo\")\n\t\trawModeOff.Stdin = os.Stdin\n\t\t_ = rawModeOff.Run()\n\t}\n\n\tvar cmd *exec.Cmd\n\tif useAliases && runtime.GOOS != \"windows\" && ShellConfigFile != \"\" {\n\t\tif _, err := os.Stat(ShellConfigFile); err == nil {\n\t\t\tquotedCfg := \"'\" + strings.ReplaceAll(ShellConfigFile, \"'\", `'\\''`) + \"'\"\n\t\t\tsourceCmd := fmt.Sprintf(\"source %s && %s\", quotedCfg, fullLine)\n\t\t\tcmd = exec.Command(shellName, shellOptions[0], sourceCmd)\n\t\t} else {\n\t\t\tcmd = exec.Command(shellName, append(shellOptions, fullLine)...)\n\t\t}\n\t} else {\n\t\tcmd = exec.Command(shellName, append(shellOptions, fullLine)...)\n\t}\n\n\tvar result string\n\tif captureOutput {\n\t\toutput, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tresult = fmt.Sprintf(\"Command failed with error: %v\\nOutput: %s\", err, string(output))\n\t\t} else {\n\t\t\tresult = string(output)\n\t\t}\n\t\tfmt.Print(result)\n\t} else {\n\t\tcmd.Stdin = os.Stdin\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tAddToShellHistory(fullLine)\n\treturn result\n}\n\nfunc AddToShellHistory(command string) {\n\tshell := os.Getenv(\"SHELL\")\n\thomeDir := os.Getenv(\"HOME\")\n\tif homeDir == \"\" {\n\t\treturn\n\t}\n\n\tvar historyPath string\n\tvar prefix string\n\n\tshellBase := filepath.Base(shell)\n\n\tswitch {\n\tcase shellBase == \"bash\":\n\t\thistoryPath = os.Getenv(\"HISTFILE\")\n\t\tif historyPath == \"\" {\n\t\t\thistoryPath = homeDir + \"/.bash_history\"\n\t\t}\n\t\tprefix = \"\"\n\tcase shellBase == \"zsh\":\n\t\thistoryPath = os.Getenv(\"HISTFILE\")\n\t\tif historyPath == \"\" {\n\t\t\thistoryPath = homeDir + \"/.zsh_history\"\n\t\t}\n\t\tprefix = fmt.Sprintf(\": %d:0;\", time.Now().Unix())\n\tdefault:\n\t\treturn\n\t}\n\n\tfile, err := os.OpenFile(historyPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer file.Close()\n\t_, _ = file.WriteString(prefix + command + \"\\n\")\n}\n\nfunc GetToolsSystemPrompt() string {\n\tSetShellAndOSVars()\n\ttoday := time.Now().Format(\"2006-01-02\")\n\treturn fmt.Sprintf(\n\t\t\"You are tgpt, a terminal assistant. Today is %s. \"+\n\t\t\t\"The shell environment you are in is %s. The operating system you are on is %s.\",\n\t\ttoday, ShellName, OperatingSystem,\n\t)\n}\n\nfunc MakeRequestAndGetData(input string, params structs.Params, extraOptions structs.ExtraOptions) (string, []interface{}, error) {\n\tif extraOptions.ToolDepth >= 5 {\n\t\tparams.Tools = nil\n\t}\n\n\tif len(params.Tools) > 0 && params.SystemPrompt == \"\" {\n\t\tparams.SystemPrompt = GetToolsSystemPrompt()\n\t}\n\n\tprovidersToTry := providersForRotation(params)\n\n\tisInteractive := extraOptions.IsInteractive || extraOptions.IsInteractiveShell || extraOptions.IsInteractiveFind\n\n\t// In interactive mode, try the last successful provider first\n\tif isInteractive && lastSuccessfulProvider != \"\" {\n\t\tfor i, p := range providersToTry {\n\t\t\tif p == lastSuccessfulProvider {\n\t\t\t\tprovidersToTry = append(providersToTry[i:], providersToTry[:i]...)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\toriginalModel := params.ApiModel\n\n\tfor i, provider := range providersToTry {\n\t\tparams.Provider = provider\n\t\tparams.ApiModel = originalModel\n\n\t\tkey := strings.ToUpper(provider)\n\t\tif alias := os.Getenv(\"MODEL_ALIAS_\" + key); alias != \"\" {\n\t\t\tparams.ApiModel = alias\n\t\t}\n\n\t\tshowStatus(statusEnabled(extraOptions), \"Loading\")\n\n\t\tresp, err := providers.NewRequest(input, params, extraOptions)\n\t\tif err != nil {\n\t\t\thideStatus()\n\n\t\t\tif resp != nil {\n\t\t\t\tresp.Body.Close()\n\t\t\t}\n\t\t\tif i < len(providersToTry)-1 {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\rProvider %s failed: %v\\n\", provider, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tprintConnectionErrorMsg(err)\n\t\t}\n\n\t\tcode := resp.StatusCode\n\t\thasMore := i < len(providersToTry)-1\n\n\t\tif code >= 400 {\n\t\t\thideStatus()\n\t\t\tif hasMore {\n\t\t\t\tresp.Body.Close()\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\rProvider %s failed (status %d)\\n\", provider, code)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !isInteractive {\n\t\t\t\thandleStatus400(resp)\n\t\t\t}\n\t\t\trespBody, _ := io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tfmt.Fprintln(os.Stderr, \"Some error has occurred, try again\")\n\t\t\tfmt.Fprintln(os.Stderr, string(respBody))\n\n\t\t\treturn \"\", nil, fmt.Errorf(\"provider %s returned status %d\", provider, code)\n\t\t}\n\n\t\thideStatus()\n\n\t\tif isInteractive {\n\t\t\tlastSuccessfulProvider = provider\n\t\t}\n\n\t\tif i > 0 {\n\t\t\tfmt.Printf(\"Fell back to \\033[1m%s\\033[0m\\n\", provider)\n\t\t}\n\n\t\t// --- Normal path (formatted output) ---\n\t\tif extraOptions.IsNormal {\n\t\t\tif extraOptions.IsToolFollowUp {\n\t\t\t\t// no header\n\t\t\t} else if !isInteractive {\n\t\t\t\tfmt.Print(\"\\r \\r\")\n\t\t\t\tbold.Println()\n\t\t\t} else {\n\t\t\t\tfmt.Println()\n\t\t\t\tboldViolet.Println(\"╭─ Bot\")\n\t\t\t}\n\n\t\t\tif extraOptions.IsInteractiveShell || extraOptions.IsInteractiveFind {\n\t\t\t\tresult := HandleEachPartInteractiveShell(resp, input, params)\n\t\t\t\tresp.Body.Close()\n\t\t\t\treturn result, nil, nil\n\t\t\t}\n\t\t\tresultText, resultMessages := HandleEachPart(resp, input, params, extraOptions)\n\t\t\tresp.Body.Close()\n\t\t\treturn resultText, resultMessages, nil\n\t\t}\n\n\t\t// --- Non-normal path (raw streaming) ---\n\t\tif extraOptions.IsGetCommand {\n\t\t\tfmt.Print(\"\\r \\r\")\n\t\t}\n\n\t\tscanner := bufio.NewScanner(resp.Body)\n\t\tfullText := \"\"\n\n\t\tfor scanner.Scan() {\n\t\t\tmainText := providers.GetMainText(scanner.Text(), params.Provider, input)\n\t\t\tif len(mainText) < 1 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfullText += mainText\n\n\t\t\tif !extraOptions.IsGetWhole {\n\t\t\t\tfmt.Print(mainText)\n\t\t\t}\n\t\t}\n\n\t\tresp.Body.Close()\n\n\t\tif err := scanner.Err(); err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, \"Some error has occurred. Error:\", err)\n\t\t\treturn \"\", nil, err\n\t\t}\n\n\t\tif extraOptions.IsGetWhole {\n\t\t\tfmt.Println(fullText)\n\t\t}\n\n\t\tif extraOptions.IsGetSilent || extraOptions.IsGetCode {\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tif extraOptions.IsGetCommand {\n\t\t\tlineCount := strings.Count(fullText, \"\\n\") + 1\n\t\t\tif lineCount == 1 {\n\t\t\t\tif extraOptions.AutoExec {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tExecuteCommand(ShellName, ShellOptions, fullText)\n\t\t\t\t} else {\n\t\t\t\t\tconfirmed, _ := bubbletea.ConfirmMenu(\"\\nExecute shell command?\", true)\n\t\t\t\t\tif confirmed {\n\t\t\t\t\t\tExecuteCommand(ShellName, ShellOptions, fullText)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclipboard.CopyToClipboard(fullText)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn fullText, nil, nil\n\t}\n\n\treturn \"\", nil, nil\n}\n\nfunc providersForRotation(params structs.Params) []string {\n\tif params.RotateProviders == \"\" {\n\t\treturn []string{params.Provider}\n\t}\n\traw := strings.Split(params.RotateProviders, \",\")\n\tseenRaw := make(map[string]bool, len(raw))\n\tlist := make([]string, 0, len(raw))\n\tfor _, p := range raw {\n\t\tp = strings.TrimSpace(p)\n\t\tif p == \"\" || seenRaw[p] {\n\t\t\tcontinue\n\t\t}\n\t\tif providers.IsValidProvider(p) {\n\t\t\tseenRaw[p] = true\n\t\t\tlist = append(list, p)\n\t\t}\n\t}\n\tif len(list) == 0 {\n\t\tfmt.Fprintf(os.Stderr, \"\\rWarning: all rotation providers are invalid, falling back to %s\\n\", params.Provider)\n\t\treturn []string{params.Provider}\n\t}\n\n\t// Deduplicate all providers while preserving order, prepending\n\t// the primary provider as the first attempt so rotation entries\n\t// act as true fallbacks.\n\tdeduped := make([]string, 0, len(list)+1)\n\tseen := make(map[string]struct{}, len(list)+1)\n\tif params.Provider != \"\" && providers.IsValidProvider(params.Provider) {\n\t\tdeduped = append(deduped, params.Provider)\n\t\tseen[params.Provider] = struct{}{}\n\t}\n\tfor _, p := range list {\n\t\tif _, ok := seen[p]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[p] = struct{}{}\n\t\tdeduped = append(deduped, p)\n\t}\n\treturn deduped\n}\n\nfunc ShowHelpMessage() {\n\tboldBlue.Println(`Usage: tgpt [Flags] [Prompt]`)\n\n\tboldBlue.Println(\"\\nFlags:\")\n\tfmt.Printf(\"%-50v Generate and Execute shell commands. \\n\", \"-s, --shell\")\n\tfmt.Printf(\"%-50v Generate Code.\\n\", \"-c, --code\")\n\tfmt.Printf(\"%-50v Gives response back without loading animation and extra text\\n\", \"-q, --quiet\")\n\tfmt.Printf(\"%-50v Gives response back as a whole text instead of streaming it\\n\", \"-w, --whole\")\n\tfmt.Printf(\"%-50v Generate images from text\\n\", \"-img, --image\")\n\tfmt.Printf(\"%-50v Set Provider. Detailed information has been provided below. (Env: AI_PROVIDER for chat and IMG_PROVIDER for image gen.)\\n\", \"--provider\")\n\tfmt.Printf(\"%-50v Find information using web search \\n\", \"-f, --find\")\n\tfmt.Printf(\"%-50v Search provider for web search: exa (default) or google (Env: SEARCH_PROVIDER).\\n%-50v Exa works without api key with rate limits and supports EXA_API_KEY env variable.\\n%-50v google requires TGPT_GOOGLE_API_KEY and TGPT_GOOGLE_SEARCH_ENGINE_ID env variables.\\n%-50s Check SEARCH_SETUP.md for google: https://github.com/aandrew-me/tgpt/blob/main/SEARCH_SETUP.md\\n\", \"--search-provider\", \"\", \"\", \"\")\n\tfmt.Printf(\"%-50v Enable built-in tool calling (all or comma-separated list: web_search_exa, web_search_firecrawl, read_directory, read_file, execute_command, web_fetch, write_file)\\n\", \"-t, --tools [tools]\")\n\tfmt.Printf(\"%-50v Path to MCP server configuration JSON file (Env: MCP_CONFIG). See 'Tool calling & MCP' section below.\\n\", \"--mcp-config\")\n\tfmt.Printf(\"%-50v Command to run a stdio MCP server directly, e.g. --mcp-server \\\"npx -y some-mcp-server\\\"\\n\", \"--mcp-server\")\n\tfmt.Printf(\"%-50v Interactively configure and test a new MCP server in mcp_config.json\\n\", \"--mcp-add\")\n\tfmt.Printf(\"%-50v Interactively remove an existing MCP server from mcp_config.json\\n\", \"--mcp-remove\")\n\n\tboldBlue.Println(\"\\nSome additional options can be set. However not all options are supported by all providers. Not supported options will just be ignored.\")\n\tfmt.Printf(\"%-50v Set Model\\n\", \"--model\")\n\tfmt.Printf(\"%-50v Set API Key. (Env: AI_API_KEY)\\n\", \"--key\")\n\tfmt.Printf(\"%-50v Set API endpoint url. You need to provide the full URL. Supported by openai, opencode, ollama, litellm, groq, gemini, deepseek\\n\", \"--url\")\n\tfmt.Printf(\"%-50v Set filepath to log conversation to (For interactive modes)\\n\", \"--log\")\n\tfmt.Printf(\"%-50v Set preprompt\\n\", \"--preprompt\")\n\tfmt.Printf(\"%-50v Comma-separated fallback providers (Env: AI_ROTATE_PROVIDERS)\\n\", \"--rotate\")\n\tfmt.Printf(\"%-50v Execute shell command without confirmation\\n\", \"-y\")\n\n\tboldBlue.Println(\"\\nOptions supported for image generation (with -image flag)\")\n\tfmt.Printf(\"%-50v Output image filename (Supported by pollinations)\\n\", \"--out\")\n\tfmt.Printf(\"%-50v Output image height (Supported by pollinations)\\n\", \"--height\")\n\tfmt.Printf(\"%-50v Output image width (Supported by pollinations)\\n\", \"--width\")\n\n\tboldBlue.Println(\"\\nOptions:\")\n\tfmt.Printf(\"%-50v Print version \\n\", \"-v, --version\")\n\tfmt.Printf(\"%-50v Print help message \\n\", \"-h, --help\")\n\tfmt.Printf(\"%-50v Start normal interactive mode \\n\", \"-i, --interactive\")\n\tfmt.Printf(\"%-50v Start multi-line interactive mode \\n\", \"-m, --multiline\")\n\tfmt.Printf(\"%-50v Start interactive shell mode. (Doesn't work with all providers) \\n\", \"-is, --interactive-shell\")\n\tfmt.Printf(\"%-50v Interactive find mode with web search \\n\", \"-if, --interactive-find\")\n\tfmt.Printf(\"%-50v Start interactive shell mode with aliases and functions \\n\", \"-ia, --interactive-alias\")\n\tfmt.Printf(\"%-50v See changelog of latest version \\n\", \"-cl, --changelog\")\n\n\tif runtime.GOOS != \"windows\" {\n\t\tfmt.Printf(\"%-50v Update program \\n\", \"-u, --update\")\n\t}\n\n\tboldBlue.Println(\"\\nProviders:\")\n\tfmt.Println(\"The default provider is opencode. The AI_PROVIDER environment variable can be used to specify a different provider.\")\n\tfmt.Println(\"Available providers to use: anyapi, deepseek, gemini, groq, isou, koboldai, minimax, ollama, ollamacloud, openai, opencode, pollinations, powerbrain.\")\n\n\tbold.Println(\"\\nProvider: anyapi\")\n\tfmt.Println(\"Multi-model API with 100k free anytokens per day. Recognizes ANYAPI_API_KEY and ANYAPI_MODEL env vars. Default model: openai/gpt-4o-mini. Supports chat and image generation. Docs: https://docs.anyapi.ai/\")\n\n\tbold.Println(\"Provider: aihorde\")\n\tfmt.Println(\"A free, community-powered generation service: volunteers share spare computer power so anyone can generate images and text. Supports AIHORDE_MODEL and AIHORDE_API_KEY env variables. Site: https://aihorde.net/\")\n\n\tbold.Println(\"\\nProvider: deepseek\")\n\tfmt.Println(\"Uses DeepSeek-V4-Flash model by default. Requires API key. Recognizes DEEPSEEK_API_KEY and DEEPSEEK_MODEL env vars. Docs: https://api-docs.deepseek.com/\")\n\n\tbold.Println(\"\\nProvider: groq\")\n\tfmt.Println(\"Requires a free API key. Recognizes GROQ_API_KEY and GROQ_MODEL env vars. Models: https://console.groq.com/docs/models\")\n\n\tbold.Println(\"\\nProvider: gemini\")\n\tfmt.Println(\"Requires a free API key. Recognizes GEMINI_API_KEY and GEMINI_MODEL env vars. https://aistudio.google.com/apikey\")\n\n\tbold.Println(\"\\nProvider: isou\")\n\tfmt.Println(\"Free provider with web search. Site: https://isou.chat/\")\n\n\tbold.Println(\"\\nProvider: koboldai\")\n\tfmt.Println(\"Uses koboldcpp/HF_SPACE_Tiefighter-13B only, answers from novels\")\n\n\tbold.Println(\"\\nProvider: litellm\")\n\tfmt.Println(\"Proxy/gateway provider. Recognizes LITELLM_API_KEY and LITELLM_MODEL env vars. Requires --model flag or LITELLM_MODEL env var. Supports custom URLs via LITELLM_URL or --url.\")\n\n\tbold.Println(\"\\nProvider: minimax\")\n\tfmt.Println(\"Requires API key. Uses MiniMax-M2.7 model by default. Recognizes MINIMAX_API_KEY and MINIMAX_MODEL env vars. https://platform.minimaxi.com\")\n\n\tbold.Println(\"\\nProvider: ollama\")\n\tfmt.Println(\"Needs to be run locally. Supports many models. \")\n\n\tbold.Println(\"\\nProvider: ollamacloud\")\n\tfmt.Println(\"Uses Ollama Cloud API. Recognizes OLLAMA_API_KEY and OLLAMA_MODEL env vars. Default model: gpt-oss:120b\")\n\n\tbold.Println(\"\\nProvider: opencode\")\n\tfmt.Println(\"Free provider using opencode.ai/zen API. Uses deepseek-v4-flash-free model by default. API key defaults to 'public'. Recognizes OPENCODE_API_KEY, OPENCODE_MODEL and OPENCODE_URL env vars. Available models: https://opencode.ai/docs/zen/#endpoints\")\n\n\tbold.Println(\"\\nProvider: openai\")\n\tfmt.Println(\"Needs API key to work and supports various models. Recognizes OPENAI_API_KEY, CEREBRAS_API_KEY and OPENAI_MODEL env vars. Supports custom urls with --url\")\n\n\tbold.Println(\"\\nProvider: pollinations\")\n\tfmt.Println(\"Works without an API key. Recognizes POLLINATIONS_API_KEY and POLLINATIONS_MODEL env vars. Free API key: https://enter.pollinations.ai/\")\n\n\tbold.Println(\"\\nProvider: powerbrain\")\n\tfmt.Println(\"Free provider using powerbrainai.com API. Uses gpt-5 model by default. No API key required.\")\n\n\tboldBlue.Println(\"\\nImage generation providers:\")\n\n\tbold.Println(\"\\nProvider: magicstudio\")\n\tfmt.Println(\"Free provider, very fast.\")\n\n\tbold.Println(\"\\nProvider: anyapi\")\n\tfmt.Println(\"Requires API key via ANYAPI_API_KEY env var or --key. Supports many models including google/gemini-2.5-flash-image\")\n\n\tbold.Println(\"\\nProvider: pollinations\")\n\tfmt.Println(\"Supported models: flux, turbo\")\n\n\tboldBlue.Println(\"\\nTool calling & MCP:\")\n\tfmt.Println(\"tgpt can let the model call tools (functions) while answering, and can also connect to Model Context Protocol (MCP) servers to add more tools at runtime.\")\n\tfmt.Println(\"Not all providers support tool calling; unsupported providers will just ignore the tool definitions.\")\n\n\tbold.Println(\"\\nBuilt-in tools (enabled with -t / --tools [tools]):\")\n\tfmt.Println(\"web_search_exa Search the web using Exa (supports EXA_API_KEY env var)\")\n\tfmt.Println(\"web_search_firecrawl Search the web using Firecrawl (supports FIRECRAWL_API_KEY env var)\")\n\tfmt.Println(\"read_directory List the contents of a directory\")\n\tfmt.Println(\"read_file Read content from a text file\")\n\tfmt.Println(\"execute_command Execute a shell command\")\n\tfmt.Println(\"web_fetch Fetch the contents of a webpage or URL\")\n\tfmt.Println(\"write_file Write content to a file\")\n\n\tbold.Println(\"\\nMCP servers:\")\n\tfmt.Println(\"Use --mcp-config to load one or more MCP servers from a JSON file, or --mcp-server to run a single stdio MCP server directly.\")\n\tfmt.Println(\"Tools discovered from MCP servers are made available to the model for tool calling.\")\n\tfmt.Println(\"\\nSupported server fields in mcp_config.json:\")\n\tfmt.Println(\" • Stdio servers: \\\"command\\\", \\\"args\\\" (array), \\\"env\\\" (array of KEY=VALUE strings)\")\n\tfmt.Println(\" • HTTP/SSE servers: \\\"url\\\", \\\"type\\\" (\\\"streamable-http\\\"|\\\"sse\\\"), \\\"headers\\\" (map of key-value pairs)\")\n\tfmt.Println(\" • Authentication: Pass Bearer tokens or API keys via \\\"headers\\\" (e.g., \\\"Authorization\\\": \\\"Bearer <key>\\\")\")\n\n\tboldBlue.Println(\"\\nExample MCP config file (mcp_config.json):\")\n\tcodeText.Println(`{`)\n\tcodeText.Println(` \"mcpServers\": {`)\n\tcodeText.Println(` \"filesystem\": {`)\n\tcodeText.Println(` \"command\": \"npx\",`)\n\tcodeText.Println(` \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/path/to/dir\"],`)\n\tcodeText.Println(` \"env\": [\"LOG_LEVEL=info\"]`)\n\tcodeText.Println(` },`)\n\tcodeText.Println(` \"firecrawl\": {`)\n\tcodeText.Println(` \"url\": \"https://mcp.firecrawl.dev/v2/mcp\",`)\n\tcodeText.Println(` \"headers\": {`)\n\tcodeText.Println(` \"Authorization\": \"Bearer YOUR_FIRECRAWL_API_KEY\"`)\n\tcodeText.Println(` }`)\n\tcodeText.Println(` }`)\n\tcodeText.Println(` }`)\n\tcodeText.Println(`}`)\n\n\tboldBlue.Println(\"\\nTool calling & MCP examples:\")\n\tfmt.Println(`tgpt --mcp-add # Interactively configure a new MCP server`)\n\tfmt.Println(`tgpt --mcp-remove # Interactively remove an MCP server`)\n\tfmt.Println(`tgpt -t \"What files are in the current directory?\"`)\n\tfmt.Println(`tgpt -t web_search_exa,read_file \"Search and read specified file\"`)\n\tfmt.Println(`tgpt --mcp-server \"npx -y @modelcontextprotocol/server-filesystem /path/to/dir\" \"List the files in /path/to/dir\"`)\n\tfmt.Println(`tgpt --mcp-config mcp_config.json \"Use the filesystem tool to read README.md\"`)\n\tfmt.Println(`tgpt -t --mcp-config mcp_config.json \"Use both built-in tools and MCP tools\"`)\n\n\tboldBlue.Println(\"\\nConfiguration file\")\n\tuserProfileEnv := \"%USERPROFILE%\"\n\tfmt.Println(\"You can create a configuration file - config.conf in ~/.config/tgpt (\" + userProfileEnv + \"\\\\.config\\\\tgpt on Windows) or in current directory (has higher priority). The configuration file supports all the environment variables supported by tgpt.\")\n\tboldBlue.Println(\"\\nExample config file:\")\n\tcodeText.Println(\"EXA_API_KEY=sk_xxxxx\")\n\tcodeText.Println(\"FIRECRAWL_API_KEY=fc_xxxxx\")\n\tcodeText.Println(\"POLLINATIONS_API_KEY=sk_xxxxx\")\n\tcodeText.Println(\"ANYAPI_API_KEY=sk_xxxxxxxx\")\n\tcodeText.Println(\"GROQ_API_KEY=gsk_xxxx\")\n\tcodeText.Println(\"HTTP_PROXY=http://localhost:8080\")\n\tcodeText.Println(\"AI_PROVIDER=groq\")\n\tcodeText.Println(\"OPENCODE_MODEL=mimo-v2.5-free\")\n\tcodeText.Println(\"AI_ROTATE_PROVIDERS=deepseek,groq,anyapi,opencode\")\n\n\tboldBlue.Println(\"\\nExamples:\")\n\tfmt.Println(`tgpt \"What is internet?\"`)\n\tfmt.Println(`tgpt -m`)\n\tfmt.Println(`tgpt -s \"How to update my system?\"`)\n\tfmt.Println(`tgpt -c \"Write a function in Go that reverses a string\"`)\n\tfmt.Println(`tgpt --provider deepseek \"What is 1+1\"`)\n\tfmt.Println(`tgpt --img \"cat\"`)\n\tfmt.Println(`tgpt --img --out ~/my-cat.jpg --height 256 --width 256 \"cat\"`)\n\tfmt.Println(`tgpt --provider openai --key \"sk-xxxx\" --model \"gpt-5.6\" \"What is 1+1\"`)\n\tfmt.Println(`cat install.sh | tgpt \"Explain the code\"`)\n}\n\nfunc SearchQuery(input string, params structs.Params, extraOptions structs.ExtraOptions, isQuiet bool, logFile string) {\n\tif extraOptions.Verbose {\n\t\tfmt.Printf(\"DEBUG: searchQuery called with input: %s\\n\", input)\n\t}\n\n\t// For one-shot find mode (-f), skip confirmation. For interactive find mode (-if), show confirmation\n\tskipConfirmation := extraOptions.IsFind && !extraOptions.IsInteractiveFind\n\tsearchResults, err := search.ProcessSearchWithConfirmation(input, params, extraOptions.Verbose, skipConfirmation, isQuiet, nil, extraOptions.SearchProvider)\n\tif err != nil {\n\t\tfmt.Printf(\"Search failed: %v\\n\", err)\n\t\treturn\n\t}\n\n\tif searchResults == \"Search cancelled by user.\" {\n\t\tfmt.Println(searchResults)\n\t\treturn\n\t}\n\n\tif len(logFile) > 0 {\n\t\tutils.LogToFile(input, \"SEARCH_QUERY\", logFile)\n\t}\n\n\t// Use IsNormal so the response text is returned (and printed by HandleEachPart)\n\tsearchOptions := structs.ExtraOptions{\n\t\tIsNormal: !isQuiet,\n\t\tIsGetSilent: isQuiet,\n\t}\n\n\tqueryWithContext := fmt.Sprintf(\"Here is the output of the search results: %s\\n\\nBased on these search results, answer the user's question: %s\", searchResults, input)\n\n\tresponse, _, _ := MakeRequestAndGetData(queryWithContext, params, searchOptions)\n\n\tif len(logFile) > 0 {\n\t\tutils.LogToFile(response, \"SEARCH_RESPONSE\", logFile)\n\t}\n}\n\nfunc InteractiveFindSession(params structs.Params, extraOptions structs.ExtraOptions, logFile string, inputReader func() (string, error)) func(string) {\n\tvar previousMessages []any\n\n\tthreadID := utils.RandomString(36)\n\n\tpromptFind := \"You are an intelligent search assistant. When a user asks a question that requires current information, web search, or factual lookup, \" +\n\t\t\"wrap your search intent in XML tags like <search>search query here</search>. \" +\n\t\t\"For follow-up questions about previous search results, you can reference the context. \" +\n\t\t\"For general conversation that doesn't need search, respond normally without search tags. \" +\n\t\t\"Examples:\\n\" +\n\t\t\"User: What's the weather like in Paris today?\\n\" +\n\t\t\"Assistant: <search>current weather Paris France today</search>\\n\" +\n\t\t\"User: Tell me more about the first result\\n\" +\n\t\t\"Assistant: Based on the previous search results, [provide more detail from context]\\n\" +\n\t\t\"User: How are you?\\n\" +\n\t\t\"Assistant: I'm doing well, thank you! How can I help you with finding information today?\"\n\n\tsearchRegex := regexp.MustCompile(`<search>(.*?)</search>`)\n\n\tgetAndPrintFindResponse := func(input string) {\n\t\tinput = strings.TrimSpace(input)\n\t\tif len(input) <= 1 {\n\t\t\treturn\n\t\t}\n\t\tif input == \"exit\" {\n\t\t\tbold.Println(\"Exiting...\")\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\tif len(logFile) > 0 {\n\t\t\tutils.LogToFile(input, \"USER_QUERY\", logFile)\n\t\t}\n\n\t\t// Use preprompt for first message\n\t\tif len(previousMessages) == 0 && len(params.Preprompt) > 0 {\n\t\t\tinput = params.Preprompt + input\n\t\t}\n\n\t\t// Set up conversation context\n\t\tparams.PrevMessages = previousMessages\n\t\tparams.ThreadID = threadID\n\t\tparams.SystemPrompt = promptFind\n\n\t\tresponseObjects, responseTxt := GetData(input, params, structs.ExtraOptions{\n\t\t\tIsInteractiveFind: true,\n\t\t\tIsGetSilent: true,\n\t\t})\n\t\tmatches := searchRegex.FindStringSubmatch(responseTxt)\n\n\t\tif len(matches) > 1 {\n\t\t\t// Search intent detected\n\t\t\tsearchQuery := strings.TrimSpace(matches[1])\n\t\t\tif extraOptions.Verbose {\n\t\t\t\tfmt.Printf(\"DEBUG: Search intent detected: '%s'\\n\", searchQuery)\n\t\t\t}\n\n\t\t\tsearchResults, err := search.ProcessSearchWithConfirmation(searchQuery, params, extraOptions.Verbose, false, false, inputReader, extraOptions.SearchProvider)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Search failed: %v\\n\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif searchResults == \"Search cancelled by user.\" {\n\t\t\t\tfmt.Println(searchResults)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpreviousMessages = append(previousMessages, responseObjects...)\n\n\t\t\tsearchContextMsg := structs.DefaultMessage{\n\t\t\t\tRole: \"system\",\n\t\t\t\tContent: fmt.Sprintf(\"Search results for '%s':\\n%s\", searchQuery, searchResults),\n\t\t\t}\n\t\t\tpreviousMessages = append(previousMessages, searchContextMsg)\n\n\t\t\tparams.PrevMessages = previousMessages\n\t\t\tfinalResponseObjects, finalResponseTxt := GetData(\n\t\t\t\tfmt.Sprintf(\"Based on these search results, answer the user's question: %s\", input),\n\t\t\t\tparams,\n\t\t\t\tstructs.ExtraOptions{IsInteractiveFind: true, IsNormal: true},\n\t\t\t)\n\n\t\t\tif len(logFile) > 0 {\n\t\t\t\tutils.LogToFile(finalResponseTxt, \"ASSISTANT_RESPONSE\", logFile)\n\t\t\t}\n\n\t\t\tpreviousMessages = append(previousMessages, finalResponseObjects...)\n\t\t} else {\n\t\t\tfmt.Println()\n\t\t\tboldViolet.Println(\"╭─ Bot\")\n\n\t\t\tformatter := newStreamFormatter(params.Provider)\n\t\t\tformatter.writeText(responseTxt)\n\t\t\tfmt.Print(\"\\n\\n\")\n\n\t\t\tif len(logFile) > 0 {\n\t\t\t\tutils.LogToFile(responseTxt, \"ASSISTANT_RESPONSE\", logFile)\n\t\t\t}\n\n\t\t\tpreviousMessages = append(previousMessages, responseObjects...)\n\t\t}\n\t}\n\n\treturn getAndPrintFindResponse\n}\n"}}}}