
Все уже слышали про Gemini CLI, который позволяет взаимодействовать с мощной gemini 2.5 прямо из командной строки. Это удобно, открывает массу возможностей. Но что, если мы захотим не просто использовать готовое решение, а понять, как оно работает изнутри? А если у нас вообще нету VPN для сервисов гугла? Или, что еще интереснее, создать свой собственный, пусть и экспериментальный, аналог? Чем мы хуже? Давайте сверстаем свой вариант на... GOLANG?
Да, именно так. Мы не будем писать продакшн-готовый инструмент, который заменит собой все существующие CLI. Наша цель — эксперимент, погружение в процесс, понимание того, как можно подружить Go, консоль и большую языковую модель.
Моя идея проста: создать относительно не сложного CLI-агента, который будет слушать наши запросы, обращаться к AI за советом или командой, предлагать эту команду нам для выполнения, а затем, после нашего подтверждения, выполнять её и анализировать результат. Если команда завершится ошибкой, AI попытается понять, что пошло не так, и предложит решение. Это позволит нам не только получать ответы, но и автоматизировать рутинные задачи, не выходя из терминала.
Если вам интересен процесс и вы хотите следить за дальнейшими материалами, буду признателен за подписку на мой телеграм-канал. Там я публикую полезныe материалы по разработке, разборы сложных концепций, советы как быть продуктивным и конечно же отборные мемы: https://t.me/nullPointerDotEXE.
В качестве зависимостей будем использовать github.com/fatih/color
для красивого вывода. Поэтому сразу сделайте go get github.com/fatih/color
Шаг 1. Объявляем переменные, структуры, константы и инициализируемся
var (
successColor = color.New(color.FgGreen).Add(color.Bold).SprintFunc()
borderColor = color.New(color.FgWhite).Add(color.Bold).SprintFunc()
labelColor = color.New(color.FgHiWhite).Add(color.Bold).SprintFunc()
aiColor = color.New(color.FgHiCyan).Add(color.Bold).SprintFunc()
errorColor = color.New(color.FgRed).Add(color.Bold).SprintFunc()
)
Начнем с переменных. Это не более, чем просто функции для подсветки текста. Мы же пишем аналог gemini, ведь так? Ну вот и текст будем делать тоже красивым.
Далее у нас идут инструкции для нейросети. Модельки я использую бесплатные, но не самые слабые. Конкретно тут я использую qwen3 на 30b параметров. Api ключ я покажу как позже получить на openrouter.
const (
systemPromptBase = "Вот системный контекст:\n"
simpleChatPromptTemplate = "Ты — AI-агент. Ответь на вопрос пользователя, основываясь на информации которая есть. НИКОГДА НЕ ИСПОЛЬЗУЙ СМАЙЛИКИ. вот история сообщений: %v"
commandGenPromptTemplate = "Ты — AI-агент. Твоя задача — генерировать команды для PowerShell на основе запроса пользователя.Отвечай только командой, без лишних слов, объяснений и markdown-форматирования. Только голая команда. Никогда не используй смайлики. Старайся генерить команды, которые не выводят очень длинный лог. И вот наши прошлые сообщения: %v"
errorAnalysisPromptTemplate = "Проанализируй ошибку выполнения команды PowerShell и объясни простыми словами, что пошло не так. Исходный запрос: '%s'. Команда: '%s'. Ошибка: '%s'"
summaryPromptTemplate = "Кратко объясни результат выполнения команды, основываясь на первоначальном запросе пользователя. Исходный запрос: '%s'. Вывод команды: '%s'"
)
Ну тут сами прочитаете тексты. 1 промпт нужен для предоставлении базовой информации о пк. Это можно убрать, но если вы планируете делать такого агента под mac os или linux, то предоставление базовой информации нейросетке для генерации правильных команд необходимо. 2 промпт для режима чата. 3 для агента. 4 для анализа ошибок и 5 для отчетов.
Идем далее
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type App struct {
APIKey string
APIUrl string
Model string
MaxRetries int
Client *http.Client
Reader *bufio.Reader
History []Message
}
type APIRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
}
type APIResponse struct {
Choices []Choice `json:"choices"`
}
type Choice struct {
Message ResponseMessage `json:"message"`
}
type ResponseMessage struct {
Content string `json:"content"`
}
Структурки.
первая структура для сохранения истории. Все остальные для взаимодействия с API нейросети. А вот app для инициализации приложения. Городить DI, конфигурацию и прочий оверхед в маленьком проекте я не буду. Сейчас у нас максимально лайтовая CLI'ка на каждый день.
Структурку App необходимо проинициализировать.
func NewApp() *App {
return &App{
APIKey: "апи ключ",
APIUrl: "https://openrouter.ai/api/v1/chat/completions",
Model: "qwen/qwen3-30b-a3b:free",
MaxRetries: 5,
Client: &http.Client{
Timeout: 60 * time.Second, //генерация может быть не самой быстрой
},
Reader: bufio.NewReader(os.Stdin),
History: []Message{
{
Role: "system",
Content: systemPromptBase + getSystemInfo(),
},
},
}
}
Но запомните: НИКОГДА НЕ ХАРДКОРДИТЕ АПИ КЛЮЧИ! ВОТ ВООБЩЕ НЕ НАДО ТАК ДЕЛАТЬ!! В данном случае мы пишем CLI чисто для эксперемента, а не в продакшн. Поэтому загружать конфиг с env я не вижу смысла тут. Но если будете расширять, то ОБЯЗАТЕЛЬНО ВЫНОСИТЕ КОНФИГУРАЦИЮ И САМОЕ ГЛАВНОЕ .ENV.
Функция getSystemInfo()
возвращает базовую информацию о пк юзера.
func getSystemInfo() string {
osName := runtime.GOOS
currentUser, err := user.Current()
username := "unknown"
if err == nil {
username = currentUser.Username
}
cwd, err := os.Getwd()
if err != nil {
cwd = "unknown"
}
return fmt.Sprintf(
"System Context:\n- OS: %s\n- Shell: PowerShell\n- User: %s\n- CWD: %s\n",
osName, username, cwd,
)
}
Итак, давайте получим API ключ. Если у вас локальная модель или другая, то можете пропускать смело шаг 2.
Шаг 2. Получаем API ключ для нейросети на openrouter
Перейдите на сайт опенроутера и войдите любым удобным способ

После этого наведите курсор на свою иконку. Там будет вкладка "keys". Вот это вот вам туда вот.

Далее просто создаете ключ, копируете и вставляете.
Сложно? Не думаю.
Шаг 3. Создаем функцию генерации контента.
Cердце нашей утилиты, без которого оно не сможет функционировать хоть как-то. Функция максимально типичная и простая. Она делает запрос к API и возвращает ответ. Это ядро логики взаимодействия с внешним API генерации текста.
func (a *App) GenerateContent(messages []Message) (string, error) {
// Формируем тело запроса с моделью и сообщениями
reqBody := APIRequest{
Model: a.Model,
Messages: messages,
}
// Сериализуем тело запроса в JSON
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("ошибка кодирования JSON: %w", err)
}
// Создаём HTTP POST-запрос к API
req, err := http.NewRequest("POST", a.APIUrl, bytes.NewBuffer(bodyBytes))
if err != nil {
return "", fmt.Errorf("ошибка создания запроса: %w", err)
}
// Устанавливаем заголовки: тип контента и авторизация
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+a.APIKey)
// Отправляем запрос и получаем ответ
resp, err := a.Client.Do(req)
if err != nil {
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
}
defer resp.Body.Close()
// Читаем тело ответа
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("ошибка чтения ответа: %w", err)
}
// Проверяем успешность запроса по статус-коду
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API вернул ошибку (статус %d): %s", resp.StatusCode, string(respBytes))
}
// Десериализуем JSON-ответ в структуру
var result APIResponse
if err := json.Unmarshal(respBytes, &result); err != nil {
return "", fmt.Errorf("ошибка декодирования ответа: %w", err)
}
// Возвращаем контент первого ответа, если он есть
if len(result.Choices) > 0 && result.Choices[0].Message.Content != "" {
return result.Choices[0].Message.Content, nil
}
// Если контент не получен — возвращаем ошибку
return "", fmt.Errorf("API не вернул контент в ответе")
}
Шаг 4. Выполняем команды от нейросети.
Напишем функцию, которая будет преобразовывать слова в действие.
func executeCommand(cmdStr string) (string, error) {
cmd := exec.Command("powershell", "-NoProfile", "-Command", cmdStr)
output, err := cmd.CombinedOutput()
// Возвращаем вывод в любом случае, т.к. там может быть текст ошибки
return strings.ToValidUTF8(string(output), ""), err
}
Функция executeCommand
выполняет указанную строку команды через PowerShell, собирает весь вывод (включая ошибки), преобразует его в корректную UTF-8 строку и возвращает вместе с ошибкой (если она возникла). Даже если команда завершилась с ошибкой, текст её вывода всё равно возвращается — это важно для отображения сообщений об ошибках пользователю. Если в выводе будет кириллица, то символы будут битые. Поэтому мы переводим все в UTF8.
В дополнение к этой команде идет функция для очистки. Зачем? Я отвечу зачем. Хоть мы в промпте явно указываем, что генерировать команду надо сразу, но от фундаментальных проблем нейросети мы не лишены. Нейросеть банально может забыть инструкцию из-за длины контекста. Поэтому мы перестраховываемся.
func cleanCommand(cmdStr string) string {
cmdStr = strings.TrimPrefix(cmdStr, "```powershell")
cmdStr = strings.TrimPrefix(cmdStr, "```bash")
cmdStr = strings.TrimPrefix(cmdStr, "```")
cmdStr = strings.TrimSuffix(cmdStr, "```")
cmdStr = strings.TrimPrefix(cmdStr, "Команда: ")
cmdStr = strings.TrimPrefix(cmdStr, "Command: ")
// Более надежное извлечение команды из `powershell -command "..."`
if strings.HasPrefix(strings.ToLower(cmdStr), "powershell -command ") {
firstQuote := strings.Index(cmdStr, "\"")
lastQuote := strings.LastIndex(cmdStr, "\"")
if firstQuote != -1 && lastQuote > firstQuote {
cmdStr = cmdStr[firstQuote+1 : lastQuote]
}
}
return strings.TrimSpace(cmdStr)
}
Функция cleanCommand
очищает строку команды от лишнего форматирования и обёрток, которые могли быть добавлены нейросетью. Она удаляет префиксы вроде powershell`, bash, "Команда: ", "Command: ", а также обрезает завершающие
````.
Если строка начинается с конструкции powershell -command "..."
, то функция аккуратно извлекает содержимое внутри кавычек. В итоге возвращается чистая команда, готовая к выполнению.
Шаг 5. Пишем вспомогательные функции для красивого вывода
Тут я прям подробно заострять внимание не буду. В целом можно обойтись и без этих функций, но тк мы делаем свою gemini CLI, которая работает в России, то без них не обойтись.
Начнем с функции вывода огромной надписи CLI AGENT.
func printHeader() {
var asciiHeader = []string{
" ███████╗██╗ ██╗ █████╗ ██████╗ █████╗ ███████╗███╗ ██╗████████╗",
" ██╔════╝██║ ██║ ██╔══██╗██╔════╝ ██╔══██╗██╔════╝████╗ ██║╚══██╔══╝",
" ██║ ██║ ██║ ███████║██║ ███╗███████║█████╗ ██╔██╗ ██║ ██║ ",
" ██║ ██║ ██║ ██╔══██║██║ ██║██╔══██║██╔══╝ ██║╚██╗██║ ██║ ",
" ███████╗███████╗██║ ██║ ██║╚██████╔╝██║ ██║███████╗██║ ╚████║ ██║ ",
" ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ",
" ",
" (by oyminirole) ",
}
colors := []*color.Color{
color.New(color.FgHiCyan),
color.New(color.FgCyan),
color.New(color.FgHiBlue),
color.New(color.FgBlue),
color.New(color.FgHiMagenta),
color.New(color.FgMagenta),
}
for _, line := range asciiHeader {
lineLength := len(line)
step := float64(len(colors)) / float64(lineLength)
for i, char := range line {
colorIndex := int(float64(i) * step)
if colorIndex >= len(colors) {
colorIndex = len(colors) - 1
}
colors[colorIndex].Printf("%c", char)
}
fmt.Println()
}
color.New(color.FgHiCyan).Add(color.Bold).Println("\nИнформация:")
fmt.Println(" • Используйте перед запросом \"!\" для получения ответов на вопросы, или просто введите запрос. Пример: !Как создать папку?")
fmt.Println(" • Для режима агента просто введите запрос без !. Агент будет исполнять команды и предоставлять результаты с отчетами.")
fmt.Println(" • Для выхода из программы нажмите Ctrl+C или закройте окно терминала.")
fmt.Println(strings.Repeat("─", 70))
}
Функция printHeader
выводит в терминал красивый ASCII-баннер с градиентной цветовой заливкой, а затем — краткую справку по использованию CLI-интерфейса.
Сначала она построчно печатает логотип с псевдонимом (oyminirole это я кста), используя плавный градиент из шести цветов. Затем выводит инструкции для пользователя: как задавать вопросы (!
), как использовать режим агента и как выйти из программы. Всё оформлено в стиле интерактивной CLI-помощи.
Куда же без спинера.
func startSpinner(text string) chan bool {
stop := make(chan bool)
go func() {
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇"}
i := 0
for {
select {
case <-stop:
fmt.Print("\r\033[K") //выполняет очистку текущей строки в терминале
return
default:
fmt.Print("\r\033[K") //выполняет очистку текущей строки в терминале
fmt.Print(text + " ")
color.New(color.FgHiCyan).Add(color.Bold).Printf(frames[i%len(frames)])
time.Sleep(100 * time.Millisecond)
i++
}
}
}()
return stop
}
Функция startSpinner
запускает спиннер (анимацию загрузки) в отдельной горутине, чтобы визуально показать, что идёт фоновый процесс (например, генерация ответа).
Спиннер крутится в консоли, отображая символы из массива frames
(символы типа ⠋
, ⠙
и т.д.). Каждые 100 мс обновляется кадр, пока в канал stop
не придёт сигнал — тогда спиннер останавливается и строка очищается.
Возвращаемый канал chan bool
используется, чтобы остановить анимацию извне.
Теперь напишем функции для отображения красивых ответов
func printResultBox(commandOutput, aiSummary string, makeAnalize bool) {
width := 80
fmt.Printf(" %s\n", successColor("╭─[ Результат ]"+strings.Repeat("─", width-16)))
fmt.Printf(" %s %s\n", borderColor("│"), labelColor("Вывод команды:"))
for _, line := range strings.Split(strings.TrimSpace(commandOutput), "\n") {
fmt.Printf(" %s %s\n", borderColor("│"), line)
}
if makeAnalize && aiSummary != "" {
fmt.Printf(" %s %s\n", borderColor("│"), borderColor(strings.Repeat("·", width-4)))
fmt.Printf(" %s %s\n", borderColor("│"), aiColor("Анализ AI:"))
for _, line := range strings.Split(strings.TrimSpace(aiSummary), "\n") {
fmt.Printf(" %s %s\n", borderColor("│"), aiColor(line))
}
}
fmt.Printf(" %s\n", successColor("╰"+strings.Repeat("─", width-2)))
}
func printErrorBox(errorOutput, aiAnalysis string) {
width := 80
fmt.Printf(" %s\n", errorColor("╭─[ ✗ Ошибка ]"+strings.Repeat("─", width-16)))
fmt.Printf(" %s %s\n", borderColor("│"), labelColor("Лог ошибки:"))
for _, line := range strings.Split(strings.TrimSpace(errorOutput), "\n") {
fmt.Printf(" %s %s\n", borderColor("│"), errorColor(line))
}
if aiAnalysis != "" {
fmt.Printf(" %s %s\n", borderColor("│"), borderColor(strings.Repeat("·", width-4)))
fmt.Printf(" %s %s\n", borderColor("│"), aiColor("Анализ AI:"))
for _, line := range strings.Split(strings.TrimSpace(aiAnalysis), "\n") {
fmt.Printf(" %s %s\n", borderColor("│"), aiColor(line))
}
}
fmt.Printf(" %s\n", errorColor("╰"+strings.Repeat("─", width-2)))
}
Знаю, что выглядит очень страшно, но ничего страшного они не делают. Запоминать вам это не надо. Эти функции просто выводят красиво текст в консоль.
Шаг 6. Пишем основную логику
Начнем с наименее страшной функции.
// handleSimpleChat обрабатывает запросы в режиме простого чата.
func (a *App) handleSimpleChat(userInput string) {
spinner := startSpinner("Генерация...")
prompt := []Message{
{Role: "system", Content: fmt.Sprintf(simpleChatPromptTemplate, a.History)},
{Role: "user", Content: strings.TrimPrefix(userInput, "!")},
}
response, err := a.GenerateContent(prompt)
if err != nil {
printErrorBox(fmt.Sprintf("Ошибка генерации ответа:\n%v", err), "")
return
}
spinner <- true
printResultBox(response, "", false)
}
Функция handleSimpleChat
обрабатывает пользовательский ввод в режиме простого чата (вопрос-ответ) и выводит результат.
Что делает по шагам:
Запускает спиннер с подписью "Генерация...", чтобы показать, что идёт обработка.
-
Формирует промпт для нейросети:
системное сообщение с шаблоном
simpleChatPromptTemplate
, в который подставляется история общения (a.History
);пользовательское сообщение (ввод без
!
в начале).
Вызывает
GenerateContent
для получения ответа от AI.Обрабатывает ошибку, если она возникла — выводит её в виде красной рамки.
Останавливает спиннер, если всё прошло успешно.
Печатает результат в красивой рамке (
printResultBox
).
Итог: простой чат-режим, в котором мы получаем ответ на свой вопрос с анимацией ожидания и красивым выводом. Ничего сложного.
Идем дальше. Вот теперь мы подходим к самой логике агента. Но для начала напишем простенькую функцию для подтверждения команд.
func (a *App) askForConfirmation(command string) bool {
fmt.Print("Подтвердить команду? [y/n]: ", aiColor(command), "\n> ")
confirmInput, _ := a.Reader.ReadString('\n')
confirmInput = strings.ToLower(strings.TrimSpace(confirmInput))
fmt.Print("\033[2A\033[J")
return confirmInput == "y" || confirmInput == "yes"
}
Функция askForConfirmation
запрашивает у пользователя подтверждение на выполнение команды и возвращает true
, если ответ — положительный. Так же она очищает 2 строки вверх (\033[2A
) и удаляет их содержимое (\033[J
), чтобы убрать следы подтверждения из терминала.
Теперь сам агент.
func (a *App) handleCommandMode(userInput string) {
currentTurnHistory := make([]Message, len(a.History))
copy(currentTurnHistory, a.History)
currentTurnHistory = append(currentTurnHistory, Message{Role: "user", Content: userInput})
var lastError string
for i := 0; i < a.MaxRetries; i++ {
spinnerGen := startSpinner("Генерация команды...")
prompt := []Message{
{Role: "system", Content: fmt.Sprintf(commandGenPromptTemplate, currentTurnHistory)},
{Role: "user", Content: userInput},
}
command, err := a.GenerateContent(prompt)
spinnerGen <- true
if err != nil {
printErrorBox(fmt.Sprintf("Ошибка генерации команды:\n%v", err), "")
return
}
command = cleanCommand(command)
// 2. Подтверждение от пользователя
if !a.askForConfirmation(command) {
log.Println("Команда отменена пользователем.")
return
}
currentTurnHistory = append(currentTurnHistory, Message{Role: "assistant", Content: command})
// 3. Выполнение команды
spinnerExec := startSpinner("Выполнение...")
output, err := executeCommand(command)
spinnerExec <- true
if err != nil {
currentTurnHistory = append(currentTurnHistory, Message{Role: "console error", Content: output})
lastError = output
time.Sleep(time.Second)
continue // Переходим к следующей попытке
}
// 4. Успешное выполнение и подведение итогов
spinnerSummary := startSpinner("Готовлю отчет...")
summaryPrompt := []Message{{Role: "user", Content: fmt.Sprintf(summaryPromptTemplate, userInput, output)}}
aiSummary, err := a.GenerateContent(summaryPrompt)
if err != nil {
log.Println("Ошибка генерации отчета: ", errorColor(err))
}
spinnerSummary <- true
printResultBox(output, aiSummary, true)
currentTurnHistory = append(currentTurnHistory, Message{Role: "AI", Content: aiSummary})
a.History = currentTurnHistory // Сохраняем успешный диалог в основную историю
return // Успешно завершили, выходим из функции
}
// 5. Обработка, если все попытки провалились
spinnerAnalysis := startSpinner("Анализирую ошибки...")
analysisPrompt := []Message{{Role: "user", Content: fmt.Sprintf(errorAnalysisPromptTemplate, userInput, "N/A", lastError)}}
aiAnalysis, _ := a.GenerateContent(analysisPrompt)
spinnerAnalysis <- true
printErrorBox(fmt.Sprintf("Не удалось выполнить задачу после %d попыток.", a.MaxRetries), aiAnalysis)
}
Согласен, пугает. Метод handleCommandMode
реализует режим агента, в котором нейросеть генерирует команду по пользовательскому запросу, предлагает её на подтверждение, выполняет, а затем формирует краткий отчёт или анализирует ошибки при неудаче.
Пошаговая логика
Создаётся копия истории сообщений текущего чата, чтобы работать в рамках одной сессии, не влияя на глобальную историю. Это сделано чтобы не забивать контекст.
-
Запуск до
MaxRetries
попыток:Генерация команды: нейросети передаётся промпт на основе истории и текущего ввода. Показывается спиннер.
Подтверждение: у пользователя спрашивается, можно ли выполнять команду. При отказе — функция завершает работу.
Выполнение команды: запускается в PowerShell, с анимацией выполнения. Если произошла ошибка — результат сохраняется, делается пауза и запускается следующая попытка.
-
Если команда выполнена успешно:
Генерируется краткий AI-отчёт по результатам выполнения.
Вывод команды и отчёт красиво показываются через
printResultBox
.Вся история этого раунда сохраняется в общую историю приложения.
-
Если все попытки провалились:
Генерируется анализ ошибки (AI анализирует, почему не удалось).
Показывается красная рамка с числом неудачных попыток и анализом.
Назначение в CLI
Этот метод — ключевой элемент CLI-агента. Он превращает текстовый ввод пользователя в конкретную команду, контролирует выполнение, перехватывает ошибки и формирует осмысленные отчёты, обеспечивая высокий уровень автономности и интерактивности.
Шаг 7. Собираем все в единое целое.
Все самое страшное теперь позади. Давайте соберем наше приложение.
func isCommandPrefixed(input string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(input)), "!")
}
func (a *App) Run() {
printHeader() //да, это наш красивый заголовок
for {
fmt.Println("╭─" + strings.Repeat("─", 66))
color.New(color.FgHiWhite).Print("│ > Введите запрос: ")
userInput, _ := a.Reader.ReadString('\n')
color.New(color.FgHiWhite).Println("╰" + strings.Repeat("─", 65))
userInput = strings.TrimSpace(userInput)
if userInput == "" {
continue
}
if isCommandPrefixed(userInput) {
a.handleSimpleChat(userInput)
} else {
a.handleCommandMode(userInput)
}
}
}
func main() {
app := NewApp()
defer app.Client.CloseIdleConnections() // Закрываем соединения при завершении
app.Run()
}
Обработка пользовательского ввода
Каждый цикл начинается с вывода декоративной рамки и приглашения ко вводу ( > Введите запрос:
). Введённая строка очищается от пробелов и анализируется:
Если строка начинается с восклицательного знака
!
— система воспринимает её как вопрос или сообщение и передаёт на обработку вhandleSimpleChat
. Это режим диалога с нейросетью без выполнения команд.Если строка не начинается с
!
— считается, что пользователь хочет выполнить команду, и запуск происходит черезhandleCommandMode
. В этом режиме нейросеть сначала генерирует команду, затем спрашивает подтверждение, выполняет её и формирует краткий отчёт.
Такой подход позволяет чётко разделять безопасные текстовые запросы и потенциально опасные системные действия.
Главный цикл приложения
Функция Run
запускает бесконечный цикл обработки пользовательского ввода, обеспечивая непрерывную работу CLI-интерфейса. Перед этим выводится ASCII-заголовок. Каждое сообщение анализируется и передаётся в соответствующий режим, в зависимости от его структуры.
Запуск и завершение
Точка входа программы — функция main
— создаёт экземпляр App
с преднастроенными параметрами (HTTP-клиент, история, ввод и т.д.) и запускает основной цикл. При завершении работы соединения HTTP-клиента закрываются корректно через defer
, что особенно важно при частом использовании API.
Результат
Итогом является интуитивно понятный CLI-интерфейс, где:
!Как создать папку?
→ возвращает ответ ИИ;создай мне папку на рабочем столе
→ превращается в команду, исполняется, а затем поясняется.
Шаг 7. Восхищаемся результатом.
Поздравляю, мы получили максимально простую и красивую CLI'ку, которую спокойно можно переписывать как душе удобно, добавляя новый функционал. Причем с неплохими фишками. У нас нейросеть проводит саморефлексию и чинит ошибки. Что может быть лучше?

Ну и еще примерчик:

Команды можно выполнять и более сложные. Спокойно может поднять докер контейнеры, создать файлы и написать в них код, проанализировать тонну информации о вашем пк, найти всякие операции, которые жрут память и тд.
На мой взгляд приложение вышло крайне полезным и интересным.
Сори, что не выложил полный код на гитхаб. Позже добавлю, все распишу, так как ну вообще нету времени в данный момент у меня. Но поделиться интересным тоже хочется. На гитхаб уже опубликую более полноценную версию с env и конфигами, а так же чуть допилю функционал. Но в целом все основные моменты я вам показал.
По традиции жду ваших комментариев. Гудлак!
Комментарии (3)
Brom95
06.07.2025 00:24Когда-то накидал за два вечера что-то отдаленно похожее:
https://github.com/Brom95/duckllm
Но тут идея была в том, что не нужны ключи для использования. И агентом оно становится через костыли, однако и у этого нашлась своя аудитория
zababurin
06.07.2025 00:24Такие проекты лучше с васм модулем было бы посмотреть.
основной язык это модули java script в любом случае, а остальные языки можно в байткод переделывать сразу, одна кодовая база
Jijiki
спасибо, очень интересно