package main import ( _ "embed" "encoding/json" "fmt" "math" "os" "os/exec" "os/user" "path" "strconv" "strings" "time" "github.com/atotto/clipboard" "github.com/direct-dev-ru/linux-command-gpt/gpt" "github.com/direct-dev-ru/linux-command-gpt/reader" "github.com/urfave/cli/v2" ) //go:embed VERSION.txt var Version string var ( cwd, _ = os.Getwd() HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/") COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat") MODEL = getEnv("LCG_MODEL", "codegeex4") PROMPT = getEnv("LCG_PROMPT", "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.") API_KEY_FILE = getEnv("LCG_API_KEY_FILE", ".openai_api_key") RESULT_FOLDER = getEnv("LCG_RESULT_FOLDER", path.Join(cwd, "gpt_results")) PROVIDER_TYPE = getEnv("LCG_PROVIDER", "ollama") // "ollama", "proxy" JWT_TOKEN = getEnv("LCG_JWT_TOKEN", "") PROMPT_ID = getEnv("LCG_PROMPT_ID", "1") // ID промпта по умолчанию TIMEOUT = getEnv("LCG_TIMEOUT", "120") // Таймаут в секундах по умолчанию RESULT_HISTORY = getEnv("LCG_RESULT_HISTORY", path.Join(RESULT_FOLDER, "lcg_history.json")) NO_HISTORY_ENV = getEnv("LCG_NO_HISTORY", "") ) // disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env) var disableHistory bool const ( colorRed = "\033[31m" colorGreen = "\033[32m" colorYellow = "\033[33m" colorBlue = "\033[34m" colorPurple = "\033[35m" colorCyan = "\033[36m" colorReset = "\033[0m" colorBold = "\033[1m" ) func main() { app := &cli.App{ Name: "lcg", Usage: "Linux Command GPT - Генерация Linux команд из описаний", Version: Version, Commands: getCommands(), UsageText: ` lcg [опции] <описание команды> Примеры: lcg "хочу извлечь файл linux-command-gpt.tar.gz" lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls" `, Description: ` Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке. Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты. Переменные окружения: LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/) LCG_MODEL Название модели (по умолчанию: codegeex4) LCG_PROMPT Текст промпта по умолчанию LCG_PROVIDER Тип провайдера: "ollama" или "proxy" (по умолчанию: ollama) LCG_JWT_TOKEN JWT токен для proxy провайдера `, Flags: []cli.Flag{ &cli.StringFlag{ Name: "file", Aliases: []string{"f"}, Usage: "Read part of the command from a file", }, &cli.BoolFlag{ Name: "no-history", Aliases: []string{"nh"}, Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)", Value: false, }, &cli.StringFlag{ Name: "sys", Aliases: []string{"s"}, Usage: "System prompt content or ID", DefaultText: "Use prompt ID from LCG_PROMPT_ID or default prompt", Value: "", }, &cli.IntFlag{ Name: "prompt-id", Aliases: []string{"pid"}, Usage: "System prompt ID (1-5 for default prompts)", DefaultText: "1", Value: 1, }, &cli.IntFlag{ Name: "timeout", Aliases: []string{"t"}, Usage: "Request timeout in seconds", DefaultText: "120", Value: 120, }, }, Action: func(c *cli.Context) error { file := c.String("file") system := c.String("sys") disableHistory = c.Bool("no-history") || isNoHistoryEnv() promptID := c.Int("prompt-id") timeout := c.Int("timeout") args := c.Args().Slice() if len(args) == 0 { cli.ShowAppHelp(c) showTips() return nil } // Если указан prompt-id, загружаем соответствующий промпт if system == "" && promptID > 0 { currentUser, _ := user.Current() pm := gpt.NewPromptManager(currentUser.HomeDir) if prompt, err := pm.GetPromptByID(promptID); err == nil { system = prompt.Content } else { fmt.Printf("Warning: Prompt ID %d not found, using default prompt\n", promptID) } } executeMain(file, system, strings.Join(args, " "), timeout) return nil }, } cli.VersionFlag = &cli.BoolFlag{ Name: "version", Aliases: []string{"V", "v"}, Usage: "prints out version", } cli.VersionPrinter = func(cCtx *cli.Context) { fmt.Printf("%s\n", cCtx.App.Version) } if err := app.Run(os.Args); err != nil { fmt.Println("Error:", err) os.Exit(1) } } func getCommands() []*cli.Command { return []*cli.Command{ { Name: "update-key", Aliases: []string{"u"}, Usage: "Update the API key", Action: func(c *cli.Context) error { if PROVIDER_TYPE == "ollama" || PROVIDER_TYPE == "proxy" { fmt.Println("API key is not needed for ollama and proxy providers") return nil } timeout := 120 // default timeout if t, err := strconv.Atoi(TIMEOUT); err == nil { timeout = t } gpt3 := initGPT(PROMPT, timeout) gpt3.UpdateKey() fmt.Println("API key updated.") return nil }, }, { Name: "delete-key", Aliases: []string{"d"}, Usage: "Delete the API key", Action: func(c *cli.Context) error { if PROVIDER_TYPE == "ollama" || PROVIDER_TYPE == "proxy" { fmt.Println("API key is not needed for ollama and proxy providers") return nil } timeout := 120 // default timeout if t, err := strconv.Atoi(TIMEOUT); err == nil { timeout = t } gpt3 := initGPT(PROMPT, timeout) gpt3.DeleteKey() fmt.Println("API key deleted.") return nil }, }, { Name: "update-jwt", Aliases: []string{"j"}, Usage: "Update the JWT token for proxy API", Action: func(c *cli.Context) error { if PROVIDER_TYPE != "proxy" { fmt.Println("JWT token is only needed for proxy provider") return nil } var jwtToken string fmt.Print("JWT Token: ") fmt.Scanln(&jwtToken) currentUser, _ := user.Current() jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" if err := os.WriteFile(jwtFile, []byte(strings.TrimSpace(jwtToken)), 0600); err != nil { fmt.Printf("Ошибка сохранения JWT токена: %v\n", err) return err } fmt.Println("JWT token updated.") return nil }, }, { Name: "delete-jwt", Aliases: []string{"dj"}, Usage: "Delete the JWT token for proxy API", Action: func(c *cli.Context) error { if PROVIDER_TYPE != "proxy" { fmt.Println("JWT token is only needed for proxy provider") return nil } currentUser, _ := user.Current() jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" if err := os.Remove(jwtFile); err != nil && !os.IsNotExist(err) { fmt.Printf("Ошибка удаления JWT токена: %v\n", err) return err } fmt.Println("JWT token deleted.") return nil }, }, { Name: "models", Aliases: []string{"m"}, Usage: "Show available models", Action: func(c *cli.Context) error { timeout := 120 // default timeout if t, err := strconv.Atoi(TIMEOUT); err == nil { timeout = t } gpt3 := initGPT(PROMPT, timeout) models, err := gpt3.GetAvailableModels() if err != nil { fmt.Printf("Ошибка получения моделей: %v\n", err) return err } fmt.Printf("Доступные модели для провайдера %s:\n", PROVIDER_TYPE) for i, model := range models { fmt.Printf(" %d. %s\n", i+1, model) } return nil }, }, { Name: "health", Aliases: []string{"he"}, // Изменено с "h" на "he" Usage: "Check API health", Action: func(c *cli.Context) error { timeout := 120 // default timeout if t, err := strconv.Atoi(TIMEOUT); err == nil { timeout = t } gpt3 := initGPT(PROMPT, timeout) if err := gpt3.Health(); err != nil { fmt.Printf("Health check failed: %v\n", err) return err } fmt.Println("API is healthy.") return nil }, }, { Name: "config", Aliases: []string{"co"}, // Изменено с "c" на "co" Usage: "Show current configuration", Action: func(c *cli.Context) error { fmt.Printf("Provider: %s\n", PROVIDER_TYPE) fmt.Printf("Host: %s\n", HOST) fmt.Printf("Model: %s\n", MODEL) fmt.Printf("Prompt: %s\n", PROMPT) fmt.Printf("Timeout: %s seconds\n", TIMEOUT) if PROVIDER_TYPE == "proxy" { fmt.Printf("JWT Token: %s\n", func() string { if JWT_TOKEN != "" { return "***set***" } currentUser, _ := user.Current() jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" if _, err := os.Stat(jwtFile); err == nil { return "***from file***" } return "***not set***" }()) } return nil }, }, { Name: "history", Aliases: []string{"hist"}, Usage: "Show command history", Subcommands: []*cli.Command{ { Name: "list", Aliases: []string{"l"}, Usage: "List history entries", Action: func(c *cli.Context) error { showHistory() return nil }, }, { Name: "view", Aliases: []string{"v"}, Usage: "View history entry by ID", Action: func(c *cli.Context) error { if c.NArg() == 0 { fmt.Println("Укажите ID записи истории") return nil } var id int if _, err := fmt.Sscanf(c.Args().First(), "%d", &id); err != nil || id <= 0 { fmt.Println("Неверный ID") return nil } viewHistoryEntry(id) return nil }, }, { Name: "delete", Aliases: []string{"d"}, Usage: "Delete history entry by ID", Action: func(c *cli.Context) error { if c.NArg() == 0 { fmt.Println("Укажите ID записи истории") return nil } var id int if _, err := fmt.Sscanf(c.Args().First(), "%d", &id); err != nil || id <= 0 { fmt.Println("Неверный ID") return nil } deleteHistoryEntry(id) return nil }, }, }, }, { Name: "prompts", Aliases: []string{"p"}, Usage: "Manage system prompts", Subcommands: []*cli.Command{ { Name: "list", Aliases: []string{"l"}, Usage: "List all available prompts", Action: func(c *cli.Context) error { currentUser, _ := user.Current() pm := gpt.NewPromptManager(currentUser.HomeDir) pm.ListPrompts() return nil }, }, { Name: "add", Aliases: []string{"a"}, Usage: "Add a new custom prompt", Action: func(c *cli.Context) error { currentUser, _ := user.Current() pm := gpt.NewPromptManager(currentUser.HomeDir) var name, description, content string fmt.Print("Название промпта: ") fmt.Scanln(&name) fmt.Print("Описание: ") fmt.Scanln(&description) fmt.Print("Содержание промпта: ") fmt.Scanln(&content) if err := pm.AddCustomPrompt(name, description, content); err != nil { fmt.Printf("Ошибка добавления промпта: %v\n", err) return err } fmt.Println("Промпт успешно добавлен!") return nil }, }, { Name: "delete", Aliases: []string{"d"}, Usage: "Delete a custom prompt", Action: func(c *cli.Context) error { if c.NArg() == 0 { fmt.Println("Укажите ID промпта для удаления") return nil } var id int if _, err := fmt.Sscanf(c.Args().First(), "%d", &id); err != nil { fmt.Println("Неверный ID промпта") return err } currentUser, _ := user.Current() pm := gpt.NewPromptManager(currentUser.HomeDir) if err := pm.DeleteCustomPrompt(id); err != nil { fmt.Printf("Ошибка удаления промпта: %v\n", err) return err } fmt.Println("Промпт успешно удален!") return nil }, }, }, }, { Name: "test-prompt", Aliases: []string{"tp"}, Usage: "Test a specific prompt ID", Action: func(c *cli.Context) error { if c.NArg() == 0 { fmt.Println("Usage: lcg test-prompt ") return nil } var promptID int if _, err := fmt.Sscanf(c.Args().First(), "%d", &promptID); err != nil { fmt.Println("Invalid prompt ID") return err } currentUser, _ := user.Current() pm := gpt.NewPromptManager(currentUser.HomeDir) prompt, err := pm.GetPromptByID(promptID) if err != nil { fmt.Printf("Prompt ID %d not found\n", promptID) return err } fmt.Printf("Testing prompt ID %d: %s\n", promptID, prompt.Name) fmt.Printf("Description: %s\n", prompt.Description) fmt.Printf("Content: %s\n", prompt.Content) if len(c.Args().Slice()) > 1 { command := strings.Join(c.Args().Slice()[1:], " ") fmt.Printf("\nTesting with command: %s\n", command) timeout := 120 // default timeout if t, err := strconv.Atoi(TIMEOUT); err == nil { timeout = t } executeMain("", prompt.Content, command, timeout) } return nil }, }, } } func executeMain(file, system, commandInput string, timeout int) { if file != "" { if err := reader.FileToPrompt(&commandInput, file); err != nil { printColored(fmt.Sprintf("❌ Ошибка чтения файла: %v\n", err), colorRed) return } } // Если system пустой, используем дефолтный промпт if system == "" { system = PROMPT } // Обеспечим папку результатов заранее (может понадобиться при действиях) if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) { if err := os.MkdirAll(RESULT_FOLDER, 0755); err != nil { printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed) return } } // Проверка истории: если такой запрос уже встречался — предложить открыть из истории if !disableHistory { if found, hist := checkAndSuggestFromHistory(commandInput); found && hist != nil { gpt3 := initGPT(system, timeout) printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed) printColored("\n📋 Команда (из истории):\n", colorYellow) printColored(fmt.Sprintf(" %s\n\n", hist.Response), colorBold+colorGreen) if strings.TrimSpace(hist.Explanation) != "" { printColored("\n📖 Подробное объяснение (из истории):\n\n", colorYellow) fmt.Println(hist.Explanation) } // Показали из истории — не выполняем запрос к API, сразу меню действий handlePostResponse(hist.Response, gpt3, system, commandInput, timeout) return } } // Папка уже создана выше gpt3 := initGPT(system, timeout) printColored("🤖 Запрос: ", colorCyan) fmt.Printf("%s\n", commandInput) response, elapsed := getCommand(gpt3, commandInput) if response == "" { printColored("❌ Ответ не получен. Проверьте подключение к API.\n", colorRed) return } printColored(fmt.Sprintf("✅ Выполнено за %.2f сек\n", elapsed), colorGreen) // Обязательное предупреждение перед первым ответом printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed) printColored("\n📋 Команда:\n", colorYellow) printColored(fmt.Sprintf(" %s\n\n", response), colorBold+colorGreen) // Сохраняем в историю (после завершения работы – т.е. позже, в зависимости от выбора действия) // Здесь не сохраняем, чтобы учесть правило: сохранять после действия, отличного от v/vv/vvv handlePostResponse(response, gpt3, system, commandInput, timeout) } // checkAndSuggestFromHistory проверяет файл истории и при совпадении запроса предлагает показать сохраненный результат func checkAndSuggestFromHistory(cmd string) (bool, *CommandHistory) { if disableHistory { return false, nil } data, err := os.ReadFile(RESULT_HISTORY) if err != nil || len(data) == 0 { return false, nil } var fileHistory []CommandHistory if err := json.Unmarshal(data, &fileHistory); err != nil { return false, nil } for _, h := range fileHistory { if strings.TrimSpace(strings.ToLower(h.Command)) == strings.TrimSpace(strings.ToLower(cmd)) { fmt.Printf("\nВ истории найден похожий запрос от %s. Показать сохраненный результат? (y/N): ", h.Timestamp.Format("2006-01-02 15:04:05")) var ans string fmt.Scanln(&ans) if strings.ToLower(ans) == "y" || strings.ToLower(ans) == "yes" { return true, &h } break } } return false, nil } func initGPT(system string, timeout int) gpt.Gpt3 { currentUser, _ := user.Current() // Загружаем JWT токен в зависимости от провайдера var jwtToken string if PROVIDER_TYPE == "proxy" { jwtToken = JWT_TOKEN if jwtToken == "" { // Пытаемся загрузить из файла jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" if data, err := os.ReadFile(jwtFile); err == nil { jwtToken = strings.TrimSpace(string(data)) } } } return *gpt.NewGpt3(PROVIDER_TYPE, HOST, jwtToken, MODEL, system, 0.01, timeout) } func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) { gpt3.InitKey() start := time.Now() done := make(chan bool) go func() { loadingChars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} i := 0 for { select { case <-done: fmt.Printf("\r%s", strings.Repeat(" ", 50)) fmt.Print("\r") return default: fmt.Printf("\r%s Обрабатываю запрос...", loadingChars[i]) i = (i + 1) % len(loadingChars) time.Sleep(100 * time.Millisecond) } } }() response := gpt3.Completions(cmd) done <- true elapsed := math.Round(time.Since(start).Seconds()*100) / 100 return response, elapsed } func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int) { fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ") var choice string fmt.Scanln(&choice) switch strings.ToLower(choice) { case "c": clipboard.WriteAll(response) fmt.Println("✅ Команда скопирована в буфер обмена") if !disableHistory { saveToHistory(cmd, response, gpt3.Prompt) } case "s": saveResponse(response, gpt3, cmd) if !disableHistory { saveToHistory(cmd, response, gpt3.Prompt) } case "r": fmt.Println("🔄 Перегенерирую...") executeMain("", system, cmd, timeout) case "e": executeCommand(response) if !disableHistory { saveToHistory(cmd, response, gpt3.Prompt) } case "v", "vv", "vvv": level := len(choice) // 1, 2, 3 showDetailedExplanation(response, gpt3, system, cmd, timeout, level) default: fmt.Println(" До свидания!") if !disableHistory { saveToHistory(cmd, response, gpt3.Prompt) } } } func saveResponse(response string, gpt3 gpt.Gpt3, cmd string) { timestamp := time.Now().Format("2006-01-02_15-04-05") filename := fmt.Sprintf("gpt_request_%s_%s.md", gpt3.Model, timestamp) filePath := path.Join(RESULT_FOLDER, filename) // Заголовок — сокращенный текст запроса пользователя title := truncateTitle(cmd) content := fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n", title, cmd+". "+gpt3.Prompt, response) if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { fmt.Println("Failed to save response:", err) } else { fmt.Printf("Response saved to %s\n", filePath) } } // saveExplanation сохраняет подробное объяснение и альтернативные способы func saveExplanation(explanation string, model string, originalCmd string, commandResponse string) { timestamp := time.Now().Format("2006-01-02_15-04-05") filename := fmt.Sprintf("gpt_explanation_%s_%s.md", model, timestamp) filePath := path.Join(RESULT_FOLDER, filename) title := truncateTitle(originalCmd) content := fmt.Sprintf( "# %s\n\n## Prompt\n\n%s\n\n## Command\n\n%s\n\n## Explanation and Alternatives (model: %s)\n\n%s\n", title, originalCmd, commandResponse, model, explanation, ) if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { fmt.Println("Failed to save explanation:", err) } else { fmt.Printf("Explanation saved to %s\n", filePath) } } // truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении func truncateTitle(s string) string { const maxLen = 120 if runeCount := len([]rune(s)); runeCount <= maxLen { return s } // взять первые 116 рунических символов и добавить " ..." const head = 116 r := []rune(s) if len(r) <= head { return s } return string(r[:head]) + " ..." } // showDetailedExplanation делает дополнительный запрос с подробным описанием и альтернативами func showDetailedExplanation(command string, gpt3 gpt.Gpt3, system, originalCmd string, timeout int, level int) { // Формируем системный промпт для подробного ответа (на русском) var detailedSystem string switch level { case 1: // v — кратко detailedSystem = "Ты опытный Linux-инженер. Объясни КРАТКО, по делу: что делает команда и самые важные ключи. Без сравнений и альтернатив. Минимум текста. Пиши на русском." case 2: // vv — средне detailedSystem = "Ты опытный Linux-инженер. Дай сбалансированное объяснение: назначение команды, разбор основных ключей, 1-2 примера. Кратко упомяни 1-2 альтернативы без глубокого сравнения. Пиши на русском." default: // vvv — максимально подробно detailedSystem = "Ты опытный Linux-инженер. Дай подробное объяснение команды с полным разбором ключей, подкоманд, сценариев применения, примеров. Затем предложи альтернативные способы решения задачи другой командой/инструментами (со сравнениями и когда что лучше применять). Пиши на русском." } // Текст запроса к модели ask := fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd) // Создаем временный экземпляр с иным системным промптом detailed := gpt.NewGpt3(gpt3.ProviderType, HOST, gpt3.ApiKey, gpt3.Model, detailedSystem, 0.2, timeout) printColored("\n🧠 Получаю подробное объяснение...\n", colorPurple) explanation, elapsed := getCommand(*detailed, ask) if explanation == "" { printColored("❌ Не удалось получить подробное объяснение.\n", colorRed) return } printColored(fmt.Sprintf("✅ Готово за %.2f сек\n", elapsed), colorGreen) // Обязательное предупреждение перед выводом подробного объяснения printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed) printColored("\n📖 Подробное объяснение и альтернативы:\n\n", colorYellow) fmt.Println(explanation) // Вторичное меню действий fmt.Printf("\nДействия: (c)копировать, (s)сохранить, (r)перегенерировать, (n)ничего: ") var choice string fmt.Scanln(&choice) switch strings.ToLower(choice) { case "c": clipboard.WriteAll(explanation) fmt.Println("✅ Объяснение скопировано в буфер обмена") case "s": saveExplanation(explanation, gpt3.Model, originalCmd, command) case "r": fmt.Println("🔄 Перегенерирую подробное объяснение...") showDetailedExplanation(command, gpt3, system, originalCmd, timeout, level) default: fmt.Println(" Возврат в основное меню.") } // После работы с объяснением — сохраняем запись в файл истории, но только если было действие не r if !disableHistory && (strings.ToLower(choice) == "c" || strings.ToLower(choice) == "s" || strings.ToLower(choice) == "n") { saveToHistory(originalCmd, command, system, explanation) } } func executeCommand(command string) { fmt.Printf("🚀 Выполняю: %s\n", command) fmt.Print("Продолжить? (y/N): ") var confirm string fmt.Scanln(&confirm) if strings.ToLower(confirm) == "y" || strings.ToLower(confirm) == "yes" { cmd := exec.Command("bash", "-c", command) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("❌ Ошибка выполнения: %v\n", err) } else { fmt.Println("✅ Команда выполнена успешно") } } else { fmt.Println("❌ Выполнение отменено") } } func getEnv(key, defaultValue string) string { if value, exists := os.LookupEnv(key); exists { return value } return defaultValue } func isNoHistoryEnv() bool { v := strings.TrimSpace(NO_HISTORY_ENV) vLower := strings.ToLower(v) return vLower == "1" || vLower == "true" } type CommandHistory struct { Index int `json:"index"` Command string `json:"command"` Response string `json:"response"` Explanation string `json:"explanation,omitempty"` System string `json:"system_prompt"` Timestamp time.Time `json:"timestamp"` } var commandHistory []CommandHistory func saveToHistory(cmd, response, system string, explanationOptional ...string) { if disableHistory { return } var explanation string if len(explanationOptional) > 0 { explanation = explanationOptional[0] } entry := CommandHistory{ Index: len(commandHistory) + 1, Command: cmd, Response: response, Explanation: explanation, System: system, Timestamp: time.Now(), } commandHistory = append(commandHistory, entry) // Ограничиваем историю 100 командами в оперативной памяти if len(commandHistory) > 100 { commandHistory = commandHistory[1:] // Перепривязать индексы после усечения for i := range commandHistory { commandHistory[i].Index = i + 1 } } // Обеспечим существование папки if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) { _ = os.MkdirAll(RESULT_FOLDER, 0755) } // Загрузим существующий файл истории var fileHistory []CommandHistory if data, err := os.ReadFile(RESULT_HISTORY); err == nil && len(data) > 0 { _ = json.Unmarshal(data, &fileHistory) } // Поиск дубликата по полю Command duplicateIndex := -1 for i, h := range fileHistory { if strings.TrimSpace(strings.ToLower(h.Command)) == strings.TrimSpace(strings.ToLower(cmd)) { duplicateIndex = i break } } if duplicateIndex == -1 { // Добавляем молча, если такого запроса не было fileHistory = append(fileHistory, entry) } else { // Спросим о перезаписи fmt.Printf("\nЗапрос уже есть в истории от %s. Перезаписать? (y/N): ", fileHistory[duplicateIndex].Timestamp.Format("2006-01-02 15:04:05")) var ans string fmt.Scanln(&ans) if strings.ToLower(ans) == "y" || strings.ToLower(ans) == "yes" { entry.Index = fileHistory[duplicateIndex].Index fileHistory[duplicateIndex] = entry } else { // Оставляем как есть, ничего не делаем } } // Пересчитать индексы в файле for i := range fileHistory { fileHistory[i].Index = i + 1 } if out, err := json.MarshalIndent(fileHistory, "", " "); err == nil { _ = os.WriteFile(RESULT_HISTORY, out, 0644) } } func showHistory() { // Пытаемся прочитать историю из файла if disableHistory { printColored("📝 История отключена (--no-history / LCG_NO_HISTORY)\n", colorYellow) return } data, err := os.ReadFile(RESULT_HISTORY) if err == nil && len(data) > 0 { var fileHistory []CommandHistory if err := json.Unmarshal(data, &fileHistory); err == nil && len(fileHistory) > 0 { printColored("📝 История (из файла):\n", colorYellow) for _, hist := range fileHistory { ts := hist.Timestamp.Format("2006-01-02 15:04:05") fmt.Printf("%d. [%s] %s → %s\n", hist.Index, ts, hist.Command, hist.Response) } return } } // Фоллбек к памяти процесса if len(commandHistory) == 0 { printColored("📝 История пуста\n", colorYellow) return } printColored("📝 История команд:\n", colorYellow) for i, hist := range commandHistory { fmt.Printf("%d. %s → %s (%s)\n", i+1, hist.Command, hist.Response, hist.Timestamp.Format("15:04:05")) } } func readFileHistory() ([]CommandHistory, error) { if disableHistory { return nil, fmt.Errorf("history disabled") } data, err := os.ReadFile(RESULT_HISTORY) if err != nil || len(data) == 0 { return nil, err } var fileHistory []CommandHistory if err := json.Unmarshal(data, &fileHistory); err != nil { return nil, err } return fileHistory, nil } func viewHistoryEntry(id int) { fileHistory, err := readFileHistory() if err != nil || len(fileHistory) == 0 { fmt.Println("История пуста или недоступна") return } var h *CommandHistory for i := range fileHistory { if fileHistory[i].Index == id { h = &fileHistory[i] break } } if h == nil { fmt.Println("Запись не найдена") return } printColored("\n📋 Команда:\n", colorYellow) printColored(fmt.Sprintf(" %s\n\n", h.Response), colorBold+colorGreen) if strings.TrimSpace(h.Explanation) != "" { printColored("\n📖 Подробное объяснение:\n\n", colorYellow) fmt.Println(h.Explanation) } } func deleteHistoryEntry(id int) { fileHistory, err := readFileHistory() if err != nil || len(fileHistory) == 0 { fmt.Println("История пуста или недоступна") return } // Найти индекс элемента с совпадающим полем Index pos := -1 for i := range fileHistory { if fileHistory[i].Index == id { pos = i break } } if pos == -1 { fmt.Println("Запись не найдена") return } // Удаляем элемент fileHistory = append(fileHistory[:pos], fileHistory[pos+1:]...) // Перенумеровываем индексы for i := range fileHistory { fileHistory[i].Index = i + 1 } if out, err := json.MarshalIndent(fileHistory, "", " "); err == nil { if err := os.WriteFile(RESULT_HISTORY, out, 0644); err != nil { fmt.Println("Ошибка записи истории:", err) } else { fmt.Println("Запись удалена") } } else { fmt.Println("Ошибка сериализации истории:", err) } } func printColored(text, color string) { fmt.Printf("%s%s%s", color, text, colorReset) } func showTips() { printColored("💡 Подсказки:\n", colorCyan) fmt.Println(" • Используйте --file для чтения из файла") fmt.Println(" • Используйте --sys для изменения системного промпта") fmt.Println(" • Используйте --prompt-id для выбора предустановленного промпта") fmt.Println(" • Используйте --timeout для установки таймаута запроса") fmt.Println(" • Укажите --no-history чтобы не записывать историю (аналог LCG_NO_HISTORY)") fmt.Println(" • Команда 'prompts list' покажет все доступные промпты") fmt.Println(" • Команда 'history list' покажет историю запросов") fmt.Println(" • Команда 'config' покажет текущие настройки") fmt.Println(" • Команда 'health' проверит доступность API") }