├── .github └── workflows │ ├── build.yml │ ├── golangci-lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── cmd └── gemini │ ├── .gitignore │ ├── .goreleaser.yml │ └── main.go ├── gemini ├── chat_session.go ├── generative_model_builder.go ├── serializable_content.go └── system_instruction.go ├── go.mod ├── go.sum └── internal ├── chat ├── chat.go └── chat_options.go ├── cli └── command.go ├── config ├── application_data.go └── configuration.go ├── handler ├── gemini_query.go ├── handler.go ├── help_command.go ├── history_command.go ├── input_mode_command.go ├── model_command.go ├── prompt_command.go ├── quit_command.go ├── renderer_options.go ├── response.go ├── system_command.go └── terminal_io.go └── terminal ├── color └── color.go ├── io.go ├── prompt.go └── spinner.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go-version: [1.21.x, 1.23.x] 17 | steps: 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Build 27 | run: | 28 | cd cmd/gemini 29 | go build . -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Get go version from go.mod 21 | run: | 22 | echo "GO_VERSION=$(grep '^go ' go.mod | cut -d " " -f 2)" >> $GITHUB_ENV 23 | 24 | - name: Setup-go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ env.GO_VERSION }} 28 | 29 | - name: Run golangci-lint 30 | uses: golangci/golangci-lint-action@v7 31 | with: 32 | version: v2.1.5 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: 1.23.x 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | version: latest 29 | workdir: ./cmd/gemini 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | dist/ 4 | coverage.out 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | timeout: 2m 4 | linters: 5 | default: none 6 | enable: 7 | - dupl 8 | - errcheck 9 | - errname 10 | - errorlint 11 | - funlen 12 | - goconst 13 | - gocritic 14 | - gocyclo 15 | - gosec 16 | - govet 17 | - ineffassign 18 | - lll 19 | - misspell 20 | - nolintlint 21 | - prealloc 22 | - revive 23 | - staticcheck 24 | - thelper 25 | - tparallel 26 | - unconvert 27 | - unparam 28 | - unused 29 | - whitespace 30 | exclusions: 31 | generated: lax 32 | presets: 33 | - comments 34 | - common-false-positives 35 | - legacy 36 | - std-error-handling 37 | rules: 38 | - linters: 39 | - funlen 40 | - unparam 41 | path: _test\.go 42 | paths: 43 | - third_party$ 44 | - builtin$ 45 | - examples$ 46 | formatters: 47 | enable: 48 | - gci 49 | - gofmt 50 | - goimports 51 | exclusions: 52 | generated: lax 53 | paths: 54 | - third_party$ 55 | - builtin$ 56 | - examples$ 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 reugn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gemini-cli 2 | [![Build](https://github.com/reugn/gemini-cli/actions/workflows/build.yml/badge.svg)](https://github.com/reugn/gemini-cli/actions/workflows/build.yml) 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/reugn/gemini-cli)](https://pkg.go.dev/github.com/reugn/gemini-cli) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/reugn/gemini-cli)](https://goreportcard.com/report/github.com/reugn/gemini-cli) 5 | 6 | A command-line interface (CLI) for [Google Gemini](https://deepmind.google/technologies/gemini/). 7 | 8 | Google Gemini is a family of multimodal artificial intelligence (AI) large language models that have 9 | capabilities in language, audio, code and video understanding. 10 | 11 | This application offers a command-line interface for interacting with various generative models through 12 | multi-turn chat. Model selection is controlled via [system command](#system-commands) inputs. 13 | 14 | ## Installation 15 | Choose a binary from [releases](https://github.com/reugn/gemini-cli/releases). 16 | 17 | ### Build from Source 18 | Download and [install Go](https://golang.org/doc/install). 19 | 20 | Install the application: 21 | ```sh 22 | go install github.com/reugn/gemini-cli/cmd/gemini@latest 23 | ``` 24 | 25 | See the [go install](https://go.dev/ref/mod#go-install) instructions for more information about the command. 26 | 27 | ## Usage 28 | > [!NOTE] 29 | > For information on the available regions for the Gemini API and Google AI Studio, 30 | > see [here](https://ai.google.dev/available_regions#available_regions). 31 | 32 | ### API key 33 | To use `gemini-cli`, you'll need an API key set in the `GEMINI_API_KEY` environment variable. 34 | If you don't already have one, create a key in [Google AI Studio](https://makersuite.google.com/app/apikey). 35 | 36 | Set the environment variable in the terminal: 37 | ```sh 38 | export GEMINI_API_KEY= 39 | ``` 40 | 41 | ### System commands 42 | The system chat message must begin with an exclamation mark and is used for internal operations. 43 | A short list of supported system commands: 44 | 45 | | Command | Description | 46 | |---------|----------------------------------------------------------------| 47 | | !p | Select the generative model system prompt 1 | 48 | | !m | Select from a list of generative model operations 2 | 49 | | !h | Select from a list of chat history operations 3 | 50 | | !i | Toggle the input mode (single-line <-> multi-line) | 51 | | !q | Exit the application | 52 | | !help | Show system command instructions | 53 | 54 | 1 System instruction (also known as "system prompt") is a more forceful prompt to the model. 55 | The model will follow instructions more closely than with a standard prompt. 56 | The user must specify system instructions in the [configuration file](#configuration-file). 57 | Note that not all generative models support them. 58 | 59 | 2 Model operations: 60 | * Select a generative model from the list of available models 61 | * Show the selected model information 62 | 63 | 3 History operations: 64 | * Clear the chat history 65 | * Store the chat history to the configuration file 66 | * Load a chat history record from the configuration file 67 | * Delete all history records from the configuration file 68 | 69 | ### Configuration file 70 | The application uses a configuration file to store generative model settings and chat history. This file is optional. 71 | If it doesn't exist, the application will attempt to create it using default values. You can use the 72 | [config flag](#cli-help) to specify the location of the configuration file. 73 | 74 | An example of basic configuration: 75 | ```json 76 | { 77 | "SystemPrompts": { 78 | "Software Engineer": "You are an experienced software engineer.", 79 | "Technical Writer": "Act as a tech writer. I will provide you with the basic steps of an app functionality, and you will come up with an engaging article on how to do those steps." 80 | }, 81 | "SafetySettings": [ 82 | { 83 | "Category": 7, 84 | "Threshold": 1 85 | }, 86 | { 87 | "Category": 10, 88 | "Threshold": 1 89 | } 90 | ], 91 | "History": { 92 | } 93 | } 94 | ``` 95 | Upon user request, the `History` map will be populated with records. Note that the chat history is stored in plain 96 | text format. See [history operations](#system-commands) for details. 97 | 98 | ### CLI help 99 | ```console 100 | $ ./gemini -h 101 | Gemini CLI Tool 102 | 103 | Usage: 104 | [flags] 105 | 106 | Flags: 107 | -c, --config string path to configuration file in JSON format (default "gemini_cli_config.json") 108 | -h, --help help for this command 109 | -m, --model string generative model name (default "gemini-1.5-flash") 110 | --multiline read input as a multi-line string 111 | -s, --style string markdown format style (ascii, dark, light, pink, notty, dracula) (default "auto") 112 | -t, --term string multi-line input terminator (default "$") 113 | -v, --version version for this command 114 | -w, --wrap int line length for response word wrapping (default 80) 115 | ``` 116 | 117 | ## License 118 | MIT 119 | -------------------------------------------------------------------------------- /cmd/gemini/.gitignore: -------------------------------------------------------------------------------- 1 | gemini 2 | *.json 3 | -------------------------------------------------------------------------------- /cmd/gemini/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: gemini 2 | 3 | builds: 4 | - main: . 5 | ldflags: 6 | - -s -w -X main.version={{.Version}} 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | goarch: 13 | - amd64 14 | - arm64 15 | - 386 16 | 17 | archives: 18 | - name_template: >- 19 | {{ .ProjectName }}_{{ .Version }}_ 20 | {{- if eq .Os "darwin" }}macos 21 | {{- else }}{{ .Os }}{{ end }}_ 22 | {{- if eq .Arch "amd64" }}x86_64 23 | {{- else }}{{ .Arch }}{{ end }} 24 | 25 | changelog: 26 | filters: 27 | exclude: 28 | - "^Merge pull request" 29 | -------------------------------------------------------------------------------- /cmd/gemini/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/user" 7 | 8 | "github.com/reugn/gemini-cli/gemini" 9 | "github.com/reugn/gemini-cli/internal/chat" 10 | "github.com/reugn/gemini-cli/internal/config" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | version = "0.4.0" 16 | apiKeyEnv = "GEMINI_API_KEY" //nolint:gosec 17 | defaultConfigPath = "gemini_cli_config.json" 18 | ) 19 | 20 | func run() int { 21 | rootCmd := &cobra.Command{ 22 | Short: "Gemini CLI Tool", 23 | Version: version, 24 | } 25 | 26 | var opts chat.Opts 27 | var configPath string 28 | rootCmd.Flags().StringVarP(&opts.GenerativeModel, "model", "m", gemini.DefaultModel, 29 | "generative model name") 30 | rootCmd.Flags().BoolVar(&opts.Multiline, "multiline", false, 31 | "read input as a multi-line string") 32 | rootCmd.Flags().StringVarP(&opts.LineTerminator, "term", "t", "$", 33 | "multi-line input terminator") 34 | rootCmd.Flags().StringVarP(&opts.StylePath, "style", "s", "auto", 35 | "markdown format style (ascii, dark, light, pink, notty, dracula)") 36 | rootCmd.Flags().IntVarP(&opts.WordWrap, "wrap", "w", 80, 37 | "line length for response word wrapping") 38 | rootCmd.Flags().StringVarP(&configPath, "config", "c", defaultConfigPath, 39 | "path to configuration file in JSON format") 40 | 41 | rootCmd.RunE = func(_ *cobra.Command, _ []string) error { 42 | configuration, err := config.NewConfiguration(configPath) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | modelBuilder := gemini.NewGenerativeModelBuilder(). 48 | WithName(opts.GenerativeModel). 49 | WithSafetySettings(configuration.Data.SafetySettings) 50 | apiKey := os.Getenv(apiKeyEnv) 51 | chatSession, err := gemini.NewChatSession(context.Background(), modelBuilder, apiKey) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | chatHandler, err := chat.New(getCurrentUser(), chatSession, configuration, &opts) 57 | if err != nil { 58 | return err 59 | } 60 | chatHandler.Start() 61 | 62 | return chatSession.Close() 63 | } 64 | 65 | err := rootCmd.Execute() 66 | if err != nil { 67 | return 1 68 | } 69 | return 0 70 | } 71 | 72 | func getCurrentUser() string { 73 | currentUser, err := user.Current() 74 | if err != nil { 75 | return "user" 76 | } 77 | return currentUser.Username 78 | } 79 | 80 | func main() { 81 | // start the application 82 | os.Exit(run()) 83 | } 84 | -------------------------------------------------------------------------------- /gemini/chat_session.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "sync" 8 | 9 | "github.com/google/generative-ai-go/genai" 10 | "google.golang.org/api/option" 11 | ) 12 | 13 | const DefaultModel = "gemini-1.5-flash" 14 | 15 | // ChatSession represents a gemini powered chat session. 16 | type ChatSession struct { 17 | ctx context.Context 18 | 19 | client *genai.Client 20 | model *genai.GenerativeModel 21 | session *genai.ChatSession 22 | 23 | loadModels sync.Once 24 | models []string 25 | } 26 | 27 | // NewChatSession returns a new [ChatSession]. 28 | func NewChatSession( 29 | ctx context.Context, modelBuilder *GenerativeModelBuilder, apiKey string, 30 | ) (*ChatSession, error) { 31 | client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | generativeModel := modelBuilder.build(client) 37 | return &ChatSession{ 38 | ctx: ctx, 39 | client: client, 40 | model: generativeModel, 41 | session: generativeModel.StartChat(), 42 | }, nil 43 | } 44 | 45 | // SendMessage sends a request to the model as part of a chat session. 46 | func (c *ChatSession) SendMessage(input string) (*genai.GenerateContentResponse, error) { 47 | return c.session.SendMessage(c.ctx, genai.Text(input)) 48 | } 49 | 50 | // SendMessageStream is like SendMessage, but with a streaming request. 51 | func (c *ChatSession) SendMessageStream(input string) *genai.GenerateContentResponseIterator { 52 | return c.session.SendMessageStream(c.ctx, genai.Text(input)) 53 | } 54 | 55 | // SetModel sets a new generative model configured with the builder and starts 56 | // a new chat session. It preserves the history of the previous chat session. 57 | func (c *ChatSession) SetModel(modelBuilder *GenerativeModelBuilder) { 58 | history := c.session.History 59 | c.model = modelBuilder.build(c.client) 60 | c.session = c.model.StartChat() 61 | c.session.History = history 62 | } 63 | 64 | // CopyModelBuilder returns a copy builder for the chat generative model. 65 | func (c *ChatSession) CopyModelBuilder() *GenerativeModelBuilder { 66 | return newCopyGenerativeModelBuilder(c.model) 67 | } 68 | 69 | // ModelInfo returns information about the chat generative model in JSON format. 70 | func (c *ChatSession) ModelInfo() (string, error) { 71 | modelInfo, err := c.model.Info(c.ctx) 72 | if err != nil { 73 | return "", err 74 | } 75 | encoded, err := json.MarshalIndent(modelInfo, "", " ") 76 | if err != nil { 77 | return "", fmt.Errorf("error encoding model info: %w", err) 78 | } 79 | return string(encoded), nil 80 | } 81 | 82 | // ListModels returns a list of the supported generative model names. 83 | func (c *ChatSession) ListModels() []string { 84 | c.loadModels.Do(func() { 85 | c.models = []string{DefaultModel} 86 | iter := c.client.ListModels(c.ctx) 87 | for { 88 | modelInfo, err := iter.Next() 89 | if err != nil { 90 | break 91 | } 92 | c.models = append(c.models, modelInfo.Name) 93 | } 94 | }) 95 | return c.models 96 | } 97 | 98 | // GetHistory returns the chat session history. 99 | func (c *ChatSession) GetHistory() []*genai.Content { 100 | return c.session.History 101 | } 102 | 103 | // SetHistory sets the chat session history. 104 | func (c *ChatSession) SetHistory(content []*genai.Content) { 105 | c.session.History = content 106 | } 107 | 108 | // ClearHistory clears the chat session history. 109 | func (c *ChatSession) ClearHistory() { 110 | c.session.History = make([]*genai.Content, 0) 111 | } 112 | 113 | // Close closes the chat session. 114 | func (c *ChatSession) Close() error { 115 | return c.client.Close() 116 | } 117 | -------------------------------------------------------------------------------- /gemini/generative_model_builder.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | "github.com/google/generative-ai-go/genai" 5 | ) 6 | 7 | type boxed[T any] struct { 8 | value T 9 | } 10 | 11 | // GenerativeModelBuilder implements the builder pattern for [genai.GenerativeModel]. 12 | type GenerativeModelBuilder struct { 13 | copy *genai.GenerativeModel 14 | 15 | name *boxed[string] 16 | generationConfig *boxed[genai.GenerationConfig] 17 | safetySettings *boxed[[]*genai.SafetySetting] 18 | tools *boxed[[]*genai.Tool] 19 | toolConfig *boxed[*genai.ToolConfig] 20 | systemInstruction *boxed[*genai.Content] 21 | cachedContentName *boxed[string] 22 | } 23 | 24 | // NewGenerativeModelBuilder returns a new [GenerativeModelBuilder] with empty default values. 25 | func NewGenerativeModelBuilder() *GenerativeModelBuilder { 26 | return &GenerativeModelBuilder{} 27 | } 28 | 29 | // newCopyGenerativeModelBuilder creates a new [GenerativeModelBuilder], 30 | // taking the default values from an existing [genai.GenerativeModel] object. 31 | func newCopyGenerativeModelBuilder(model *genai.GenerativeModel) *GenerativeModelBuilder { 32 | return &GenerativeModelBuilder{copy: model} 33 | } 34 | 35 | // WithName sets the model name. 36 | func (b *GenerativeModelBuilder) WithName( 37 | modelName string, 38 | ) *GenerativeModelBuilder { 39 | b.name = &boxed[string]{modelName} 40 | return b 41 | } 42 | 43 | // WithGenerationConfig sets the generation config. 44 | func (b *GenerativeModelBuilder) WithGenerationConfig( 45 | generationConfig genai.GenerationConfig, 46 | ) *GenerativeModelBuilder { 47 | b.generationConfig = &boxed[genai.GenerationConfig]{generationConfig} 48 | return b 49 | } 50 | 51 | // WithSafetySettings sets the safety settings. 52 | func (b *GenerativeModelBuilder) WithSafetySettings( 53 | safetySettings []*genai.SafetySetting, 54 | ) *GenerativeModelBuilder { 55 | b.safetySettings = &boxed[[]*genai.SafetySetting]{safetySettings} 56 | return b 57 | } 58 | 59 | // WithTools sets the tools. 60 | func (b *GenerativeModelBuilder) WithTools( 61 | tools []*genai.Tool, 62 | ) *GenerativeModelBuilder { 63 | b.tools = &boxed[[]*genai.Tool]{tools} 64 | return b 65 | } 66 | 67 | // WithToolConfig sets the tool config. 68 | func (b *GenerativeModelBuilder) WithToolConfig( 69 | toolConfig *genai.ToolConfig, 70 | ) *GenerativeModelBuilder { 71 | b.toolConfig = &boxed[*genai.ToolConfig]{toolConfig} 72 | return b 73 | } 74 | 75 | // WithSystemInstruction sets the system instruction. 76 | func (b *GenerativeModelBuilder) WithSystemInstruction( 77 | systemInstruction *genai.Content, 78 | ) *GenerativeModelBuilder { 79 | b.systemInstruction = &boxed[*genai.Content]{systemInstruction} 80 | return b 81 | } 82 | 83 | // WithCachedContentName sets the name of the [genai.CachedContent] to use. 84 | func (b *GenerativeModelBuilder) WithCachedContentName( 85 | cachedContentName string, 86 | ) *GenerativeModelBuilder { 87 | b.cachedContentName = &boxed[string]{cachedContentName} 88 | return b 89 | } 90 | 91 | // build builds and returns a new [genai.GenerativeModel] using the given [genai.Client]. 92 | // It will panic if the copy and the model name are not set. 93 | func (b *GenerativeModelBuilder) build(client *genai.Client) *genai.GenerativeModel { 94 | if b.copy == nil && b.name == nil { 95 | panic("model name is required") 96 | } 97 | 98 | model := b.copy 99 | if b.name != nil { 100 | model = client.GenerativeModel(b.name.value) 101 | if b.copy != nil { 102 | model.GenerationConfig = b.copy.GenerationConfig 103 | model.SafetySettings = b.copy.SafetySettings 104 | model.Tools = b.copy.Tools 105 | model.ToolConfig = b.copy.ToolConfig 106 | model.SystemInstruction = b.copy.SystemInstruction 107 | model.CachedContentName = b.copy.CachedContentName 108 | } 109 | } 110 | b.configure(model) 111 | return model 112 | } 113 | 114 | // configure configures the given generative model using the builder values. 115 | func (b *GenerativeModelBuilder) configure(model *genai.GenerativeModel) { 116 | if b.generationConfig != nil { 117 | model.GenerationConfig = b.generationConfig.value 118 | } 119 | if b.safetySettings != nil { 120 | model.SafetySettings = b.safetySettings.value 121 | } 122 | if b.tools != nil { 123 | model.Tools = b.tools.value 124 | } 125 | if b.toolConfig != nil { 126 | model.ToolConfig = b.toolConfig.value 127 | } 128 | if b.systemInstruction != nil { 129 | model.SystemInstruction = b.systemInstruction.value 130 | } 131 | if b.cachedContentName != nil { 132 | model.CachedContentName = b.cachedContentName.value 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /gemini/serializable_content.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/generative-ai-go/genai" 7 | ) 8 | 9 | // SerializableContent is the data type containing multipart text message content. 10 | // It is a serializable equivalent of [genai.Content], where message content parts 11 | // are represented as strings. 12 | type SerializableContent struct { 13 | // Ordered parts that constitute a single message. 14 | Parts []string 15 | // The producer of the content. Must be either 'user' or 'model'. 16 | Role string 17 | } 18 | 19 | // NewSerializableContent instantiates and returns a new SerializableContent from 20 | // the given [genai.Content]. 21 | // It will panic if the content type is not supported. 22 | func NewSerializableContent(c *genai.Content) *SerializableContent { 23 | parts := make([]string, len(c.Parts)) 24 | for i, part := range c.Parts { 25 | parts[i] = partToString(part) 26 | } 27 | return &SerializableContent{ 28 | Parts: parts, 29 | Role: c.Role, 30 | } 31 | } 32 | 33 | // ToContent converts the SerializableContent into a [genai.Content]. 34 | func (c *SerializableContent) ToContent() *genai.Content { 35 | parts := make([]genai.Part, len(c.Parts)) 36 | for i, part := range c.Parts { 37 | parts[i] = genai.Text(part) 38 | } 39 | return &genai.Content{ 40 | Parts: parts, 41 | Role: c.Role, 42 | } 43 | } 44 | 45 | func partToString(part genai.Part) string { 46 | switch p := part.(type) { 47 | case genai.Text: 48 | return string(p) 49 | default: 50 | panic(fmt.Errorf("unsupported part type: %T", part)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /gemini/system_instruction.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import "github.com/google/generative-ai-go/genai" 4 | 5 | // SystemInstruction represents a serializable system prompt, a more forceful 6 | // instruction to the language model. The model will prioritize adhering to 7 | // system instructions over regular prompts. 8 | type SystemInstruction string 9 | 10 | // ToContent converts the SystemInstruction to [genai.Content]. 11 | func (si SystemInstruction) ToContent() *genai.Content { 12 | return genai.NewUserContent(genai.Text(si)) 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/reugn/gemini-cli 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/charmbracelet/glamour v0.7.0 7 | github.com/chzyer/readline v1.5.1 8 | github.com/google/generative-ai-go v0.19.0 9 | github.com/manifoldco/promptui v0.9.0 10 | github.com/muesli/termenv v0.15.2 11 | github.com/spf13/cobra v1.8.1 12 | google.golang.org/api v0.210.0 13 | ) 14 | 15 | require ( 16 | cloud.google.com/go v0.116.0 // indirect 17 | cloud.google.com/go/ai v0.8.2 // indirect 18 | cloud.google.com/go/auth v0.11.0 // indirect 19 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect 20 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 21 | cloud.google.com/go/longrunning v0.6.0 // indirect 22 | github.com/alecthomas/chroma/v2 v2.14.0 // indirect 23 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 24 | github.com/aymerick/douceur v0.2.0 // indirect 25 | github.com/dlclark/regexp2 v1.11.4 // indirect 26 | github.com/felixge/httpsnoop v1.0.4 // indirect 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-logr/stdr v1.2.2 // indirect 29 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 30 | github.com/google/s2a-go v0.1.8 // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 33 | github.com/googleapis/gax-go/v2 v2.14.0 // indirect 34 | github.com/gorilla/css v1.0.1 // indirect 35 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 36 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/mattn/go-runewidth v0.0.16 // indirect 39 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 40 | github.com/muesli/reflow v0.3.0 // indirect 41 | github.com/olekukonko/tablewriter v0.0.5 // indirect 42 | github.com/rivo/uniseg v0.4.7 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/yuin/goldmark v1.7.4 // indirect 45 | github.com/yuin/goldmark-emoji v1.0.3 // indirect 46 | go.opencensus.io v0.24.0 // indirect 47 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 49 | go.opentelemetry.io/otel v1.29.0 // indirect 50 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 51 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 52 | golang.org/x/crypto v0.29.0 // indirect 53 | golang.org/x/net v0.31.0 // indirect 54 | golang.org/x/oauth2 v0.24.0 // indirect 55 | golang.org/x/sync v0.9.0 // indirect 56 | golang.org/x/sys v0.27.0 // indirect 57 | golang.org/x/text v0.20.0 // indirect 58 | golang.org/x/time v0.8.0 // indirect 59 | google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect 60 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect 61 | google.golang.org/grpc v1.67.1 // indirect 62 | google.golang.org/protobuf v1.35.2 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= 3 | cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= 4 | cloud.google.com/go/ai v0.8.2 h1:LEaQwqBv+k2ybrcdTtCTc9OPZXoEdcQaGrfvDYS6Bnk= 5 | cloud.google.com/go/ai v0.8.2/go.mod h1:Wb3EUUGWwB6yHBaUf/+oxUq/6XbCaU1yh0GrwUS8lr4= 6 | cloud.google.com/go/auth v0.11.0 h1:Ic5SZz2lsvbYcWT5dfjNWgw6tTlGi2Wc8hyQSC9BstA= 7 | cloud.google.com/go/auth v0.11.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= 8 | cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= 9 | cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= 10 | cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= 11 | cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= 12 | cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI= 13 | cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= 16 | github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 17 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 18 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 19 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 20 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 21 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 22 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 23 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 24 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 25 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 26 | github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= 27 | github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= 28 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 29 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 30 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 31 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 32 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 33 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 34 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 35 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 36 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 37 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 38 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 39 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 44 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 45 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 46 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 47 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 48 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 49 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 50 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 51 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 52 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 53 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 54 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 55 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 56 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 57 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 58 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 59 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 60 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 61 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 64 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 65 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 66 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 67 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 68 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 69 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 70 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 71 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 72 | github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg= 73 | github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= 74 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 75 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 76 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 77 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 78 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 80 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 81 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 82 | github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= 83 | github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= 84 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 85 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 86 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 87 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 88 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 89 | github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= 90 | github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= 91 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 92 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 93 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 94 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 95 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 96 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 97 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 98 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 99 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 100 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 101 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 102 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 103 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 104 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 105 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 106 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 107 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 108 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 109 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 110 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 111 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 112 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 113 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 114 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 115 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 116 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 117 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 118 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 119 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 120 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 121 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 122 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 123 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 124 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 125 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 126 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 127 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 128 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 129 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 130 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 131 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 132 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 133 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 134 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 135 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 136 | github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= 137 | github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 138 | github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= 139 | github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 140 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 141 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 142 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= 143 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= 144 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 145 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 146 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 147 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 148 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 149 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 150 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 151 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 152 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 153 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 154 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= 155 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 156 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 157 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 158 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 159 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 160 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 164 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 165 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 166 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 167 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 168 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 169 | golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= 170 | golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 171 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 174 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 175 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 176 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 177 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 178 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 179 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 184 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 185 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 186 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 187 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 188 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 189 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 190 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 191 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 192 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 193 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 194 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 195 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 196 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | google.golang.org/api v0.210.0 h1:HMNffZ57OoZCRYSbdWVRoqOa8V8NIHLL0CzdBPLztWk= 198 | google.golang.org/api v0.210.0/go.mod h1:B9XDZGnx2NtyjzVkOVTGrFSAVZgPcbedzKg/gTLwqBs= 199 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 200 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 201 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 202 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 203 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 204 | google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= 205 | google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= 206 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= 207 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 208 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 209 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 210 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 211 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 212 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 213 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 214 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 215 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 216 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 217 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 218 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 219 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 220 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 221 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 222 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 223 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 224 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 225 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 226 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 227 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 228 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 229 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 230 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 231 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 232 | -------------------------------------------------------------------------------- /internal/chat/chat.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "github.com/reugn/gemini-cli/gemini" 5 | "github.com/reugn/gemini-cli/internal/cli" 6 | "github.com/reugn/gemini-cli/internal/config" 7 | "github.com/reugn/gemini-cli/internal/handler" 8 | "github.com/reugn/gemini-cli/internal/terminal" 9 | ) 10 | 11 | // Chat handles the interactive exchange of messages between user and model. 12 | type Chat struct { 13 | io *terminal.IO 14 | 15 | geminiHandler handler.MessageHandler 16 | systemHandler handler.MessageHandler 17 | } 18 | 19 | // New returns a new Chat. 20 | func New( 21 | user string, session *gemini.ChatSession, 22 | configuration *config.Configuration, opts *Opts, 23 | ) (*Chat, error) { 24 | terminalIOConfig := &terminal.IOConfig{ 25 | User: user, 26 | Multiline: opts.Multiline, 27 | LineTerminator: opts.LineTerminator, 28 | } 29 | 30 | terminalIO, err := terminal.NewIO(terminalIOConfig) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | geminiIO := handler.NewIO(terminalIO, terminalIO.Prompt.Gemini) 36 | geminiHandler, err := handler.NewGeminiQuery(geminiIO, session, opts.rendererOptions()) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | systemIO := handler.NewIO(terminalIO, terminalIO.Prompt.Cli) 42 | systemHandler, err := handler.NewSystemCommand(systemIO, session, configuration, 43 | opts.GenerativeModel, opts.rendererOptions()) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &Chat{ 49 | io: terminalIO, 50 | geminiHandler: geminiHandler, 51 | systemHandler: systemHandler, 52 | }, nil 53 | } 54 | 55 | // Start starts the main chat loop between user and model. 56 | func (c *Chat) Start() { 57 | for { 58 | // read query from the user 59 | message := c.io.Read() 60 | if message == "" { 61 | continue 62 | } 63 | 64 | // get handler for the read message 65 | // the message is not empty here 66 | messageHandler := c.getHandler(message[:1]) 67 | 68 | // write the agent terminal prompt 69 | c.io.Write(messageHandler.TerminalPrompt()) 70 | 71 | // process the message 72 | response, quit := messageHandler.Handle(message) 73 | 74 | // write the response 75 | c.io.Write(response.String()) 76 | 77 | if quit { 78 | break 79 | } 80 | } 81 | } 82 | 83 | // getHandler returns the handler for the message. 84 | func (c *Chat) getHandler(prefix string) handler.MessageHandler { 85 | if prefix == cli.SystemCmdPrefix { 86 | return c.systemHandler 87 | } 88 | return c.geminiHandler 89 | } 90 | -------------------------------------------------------------------------------- /internal/chat/chat_options.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import "github.com/reugn/gemini-cli/internal/handler" 4 | 5 | // Opts represents the Chat configuration options. 6 | type Opts struct { 7 | GenerativeModel string 8 | Multiline bool 9 | LineTerminator string 10 | StylePath string 11 | WordWrap int 12 | } 13 | 14 | func (o *Opts) rendererOptions() handler.RendererOptions { 15 | return handler.RendererOptions{ 16 | StylePath: o.StylePath, 17 | WordWrap: o.WordWrap, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/cli/command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | const ( 4 | SystemCmdPrefix = "!" 5 | SystemCmdHelp = "help" 6 | SystemCmdQuit = "q" 7 | SystemCmdSelectPrompt = "p" 8 | SystemCmdSelectInputMode = "i" 9 | SystemCmdModel = "m" 10 | SystemCmdHistory = "h" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/config/application_data.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/google/generative-ai-go/genai" 5 | "github.com/reugn/gemini-cli/gemini" 6 | ) 7 | 8 | // ApplicationData encapsulates application state and configuration. 9 | // Note that the chat history is stored in plain text format. 10 | type ApplicationData struct { 11 | SystemPrompts map[string]gemini.SystemInstruction 12 | SafetySettings []*genai.SafetySetting 13 | History map[string][]*gemini.SerializableContent 14 | } 15 | 16 | // newDefaultApplicationData returns a new ApplicationData with default values. 17 | func newDefaultApplicationData() *ApplicationData { 18 | defaultSafetySettings := []*genai.SafetySetting{ 19 | {Category: genai.HarmCategoryHarassment, Threshold: genai.HarmBlockLowAndAbove}, 20 | {Category: genai.HarmCategoryHateSpeech, Threshold: genai.HarmBlockLowAndAbove}, 21 | {Category: genai.HarmCategorySexuallyExplicit, Threshold: genai.HarmBlockLowAndAbove}, 22 | {Category: genai.HarmCategoryDangerousContent, Threshold: genai.HarmBlockLowAndAbove}, 23 | } 24 | return &ApplicationData{ 25 | SystemPrompts: make(map[string]gemini.SystemInstruction), 26 | SafetySettings: defaultSafetySettings, 27 | History: make(map[string][]*gemini.SerializableContent), 28 | } 29 | } 30 | 31 | // AddHistoryRecord adds a history record to the application data. 32 | func (d *ApplicationData) AddHistoryRecord(label string, content []*genai.Content) { 33 | serializableContent := make([]*gemini.SerializableContent, len(content)) 34 | for i, c := range content { 35 | serializableContent[i] = gemini.NewSerializableContent(c) 36 | } 37 | d.History[label] = serializableContent 38 | } 39 | -------------------------------------------------------------------------------- /internal/config/configuration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // Configuration contains the details of the application configuration. 10 | type Configuration struct { 11 | // filePath is the path to the configuration file. This file contains the 12 | // application data in JSON format. 13 | filePath string 14 | // Data is the application data. This data is loaded from the configuration 15 | // file and is used to configure the application. 16 | Data *ApplicationData 17 | } 18 | 19 | // NewConfiguration returns a new Configuration from a JSON file. 20 | func NewConfiguration(filePath string) (*Configuration, error) { 21 | configuration := &Configuration{ 22 | filePath: filePath, 23 | Data: newDefaultApplicationData(), 24 | } 25 | 26 | file, err := os.Open(filePath) 27 | if err != nil { 28 | if os.IsNotExist(err) { 29 | _ = configuration.Flush() // ignore error if file write failed 30 | return configuration, nil 31 | } 32 | return nil, fmt.Errorf("error opening file: %w", err) 33 | } 34 | defer file.Close() 35 | 36 | decoder := json.NewDecoder(file) 37 | err = decoder.Decode(configuration.Data) 38 | if err != nil { 39 | return nil, fmt.Errorf("error decoding JSON: %w", err) 40 | } 41 | 42 | return configuration, nil 43 | } 44 | 45 | // Flush serializes and writes the configuration to the file. 46 | func (c *Configuration) Flush() error { 47 | file, err := os.Create(c.filePath) 48 | if err != nil { 49 | return fmt.Errorf("error opening file: %w", err) 50 | } 51 | defer file.Close() 52 | 53 | encoder := json.NewEncoder(file) 54 | encoder.SetIndent("", " ") 55 | err = encoder.Encode(c.Data) 56 | if err != nil { 57 | return fmt.Errorf("error encoding JSON: %w", err) 58 | } 59 | 60 | return file.Sync() 61 | } 62 | -------------------------------------------------------------------------------- /internal/handler/gemini_query.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/glamour" 8 | "github.com/reugn/gemini-cli/gemini" 9 | ) 10 | 11 | // GeminiQuery processes queries to gemini models. 12 | // It implements the MessageHandler interface. 13 | type GeminiQuery struct { 14 | *IO 15 | session *gemini.ChatSession 16 | renderer *glamour.TermRenderer 17 | } 18 | 19 | var _ MessageHandler = (*GeminiQuery)(nil) 20 | 21 | // NewGeminiQuery returns a new GeminiQuery message handler. 22 | func NewGeminiQuery(io *IO, session *gemini.ChatSession, opts RendererOptions) (*GeminiQuery, error) { 23 | renderer, err := opts.newTermRenderer() 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to instantiate terminal renderer: %w", err) 26 | } 27 | 28 | return &GeminiQuery{ 29 | IO: io, 30 | session: session, 31 | renderer: renderer, 32 | }, nil 33 | } 34 | 35 | // Handle processes the chat message. 36 | func (h *GeminiQuery) Handle(message string) (Response, bool) { 37 | h.terminal.Spinner.Start() 38 | defer h.terminal.Spinner.Stop() 39 | 40 | response, err := h.session.SendMessage(message) 41 | if err != nil { 42 | return newErrorResponse(err), false 43 | } 44 | 45 | var b strings.Builder 46 | for _, candidate := range response.Candidates { 47 | for _, part := range candidate.Content.Parts { 48 | _, _ = fmt.Fprintf(&b, "%s", part) 49 | } 50 | } 51 | 52 | rendered, err := h.renderer.Render(b.String()) 53 | if err != nil { 54 | return newErrorResponse(fmt.Errorf("failed to format response: %w", err)), false 55 | } 56 | 57 | return dataResponse(rendered), false 58 | } 59 | -------------------------------------------------------------------------------- /internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | // MessageHandler handles chat messages from the user. 4 | type MessageHandler interface { 5 | // Handle processes the message and returns a response, along with a flag 6 | // indicating whether the application should terminate. 7 | Handle(message string) (Response, bool) 8 | 9 | // TerminalPrompt returns the terminal prompt for the handler. 10 | TerminalPrompt() string 11 | } 12 | -------------------------------------------------------------------------------- /internal/handler/help_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/glamour" 8 | "github.com/reugn/gemini-cli/internal/cli" 9 | ) 10 | 11 | // HelpCommand handles the help system command request. 12 | type HelpCommand struct { 13 | *IO 14 | renderer *glamour.TermRenderer 15 | } 16 | 17 | var _ MessageHandler = (*HelpCommand)(nil) 18 | 19 | // NewHelpCommand returns a new HelpCommand. 20 | func NewHelpCommand(io *IO, opts RendererOptions) (*HelpCommand, error) { 21 | renderer, err := opts.newTermRenderer() 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to instantiate terminal renderer: %w", err) 24 | } 25 | 26 | return &HelpCommand{ 27 | IO: io, 28 | renderer: renderer, 29 | }, nil 30 | } 31 | 32 | // Handle processes the help system command. 33 | func (h *HelpCommand) Handle(_ string) (Response, bool) { 34 | var b strings.Builder 35 | b.WriteString("# System commands\n") 36 | b.WriteString("Use a command prefixed with an exclamation mark (e.g., `!h`).\n") 37 | fmt.Fprintf(&b, "* `%s` - Select the generative model system prompt.\n", cli.SystemCmdSelectPrompt) 38 | fmt.Fprintf(&b, "* `%s` - Select from a list of generative model operations.\n", cli.SystemCmdModel) 39 | fmt.Fprintf(&b, "* `%s` - Select from a list of chat history operations.\n", cli.SystemCmdHistory) 40 | fmt.Fprintf(&b, "* `%s` - Toggle the input mode.\n", cli.SystemCmdSelectInputMode) 41 | fmt.Fprintf(&b, "* `%s` - Exit the application.\n", cli.SystemCmdQuit) 42 | 43 | rendered, err := h.renderer.Render(b.String()) 44 | if err != nil { 45 | return newErrorResponse(fmt.Errorf("failed to format instructions: %w", err)), false 46 | } 47 | 48 | return dataResponse(rendered), false 49 | } 50 | -------------------------------------------------------------------------------- /internal/handler/history_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "time" 7 | 8 | "github.com/google/generative-ai-go/genai" 9 | "github.com/manifoldco/promptui" 10 | "github.com/reugn/gemini-cli/gemini" 11 | "github.com/reugn/gemini-cli/internal/config" 12 | ) 13 | 14 | var historyOptions = []string{ 15 | "Clear chat history", 16 | "Store chat history", 17 | "Load chat history", 18 | "Delete stored history records", 19 | } 20 | 21 | // HistoryCommand processes the chat history system commands. 22 | // It implements the MessageHandler interface. 23 | type HistoryCommand struct { 24 | *IO 25 | session *gemini.ChatSession 26 | configuration *config.Configuration 27 | } 28 | 29 | var _ MessageHandler = (*HistoryCommand)(nil) 30 | 31 | // NewHistoryCommand returns a new HistoryCommand. 32 | func NewHistoryCommand(io *IO, session *gemini.ChatSession, 33 | configuration *config.Configuration) *HistoryCommand { 34 | return &HistoryCommand{ 35 | IO: io, 36 | session: session, 37 | configuration: configuration, 38 | } 39 | } 40 | 41 | // Handle processes the history system command. 42 | func (h *HistoryCommand) Handle(_ string) (Response, bool) { 43 | option, err := h.selectHistoryOption() 44 | if err != nil { 45 | return newErrorResponse(err), false 46 | } 47 | var response Response 48 | switch option { 49 | case historyOptions[0]: 50 | response = h.handleClear() 51 | case historyOptions[1]: 52 | response = h.handleStore() 53 | case historyOptions[2]: 54 | response = h.handleLoad() 55 | case historyOptions[3]: 56 | response = h.handleDelete() 57 | default: 58 | response = newErrorResponse(fmt.Errorf("unsupported option: %s", option)) 59 | } 60 | return response, false 61 | } 62 | 63 | // handleClear handles the chat history clear request. 64 | func (h *HistoryCommand) handleClear() Response { 65 | h.terminal.Write(h.terminalPrompt) 66 | h.session.ClearHistory() 67 | return dataResponse("Cleared the chat history.") 68 | } 69 | 70 | // handleStore handles the chat history store request. 71 | func (h *HistoryCommand) handleStore() Response { 72 | defer h.terminal.Write(h.terminalPrompt) 73 | historyLabel, err := h.promptHistoryLabel() 74 | if err != nil { 75 | return newErrorResponse(err) 76 | } 77 | 78 | timeLabel := time.Now().In(time.Local).Format(time.DateTime) 79 | recordLabel := fmt.Sprintf("%s - %s", timeLabel, historyLabel) 80 | h.configuration.Data.AddHistoryRecord( 81 | recordLabel, 82 | h.session.GetHistory(), 83 | ) 84 | 85 | if err := h.configuration.Flush(); err != nil { 86 | return newErrorResponse(err) 87 | } 88 | 89 | return dataResponse(fmt.Sprintf("%q has been saved to the file.", recordLabel)) 90 | } 91 | 92 | // handleLoad handles the chat history load request. 93 | func (h *HistoryCommand) handleLoad() Response { 94 | defer h.terminal.Write(h.terminalPrompt) 95 | label, history, err := h.loadHistory() 96 | if err != nil { 97 | return newErrorResponse(err) 98 | } 99 | 100 | h.session.SetHistory(history) 101 | return dataResponse(fmt.Sprintf("%q has been loaded to the chat history.", label)) 102 | } 103 | 104 | // handleDelete handles deletion of the stored history records. 105 | func (h *HistoryCommand) handleDelete() Response { 106 | h.terminal.Write(h.terminalPrompt) 107 | h.configuration.Data.History = make(map[string][]*gemini.SerializableContent) 108 | if err := h.configuration.Flush(); err != nil { 109 | return newErrorResponse(err) 110 | } 111 | return dataResponse("History records have been removed from the file.") 112 | } 113 | 114 | // loadHistory returns history data to be set. 115 | func (h *HistoryCommand) loadHistory() (string, []*genai.Content, error) { 116 | promptNames := make([]string, len(h.configuration.Data.History)+1) 117 | promptNames[0] = empty 118 | i := 1 119 | for p := range h.configuration.Data.History { 120 | promptNames[i] = p 121 | i++ 122 | } 123 | prompt := promptui.Select{ 124 | Label: "Select conversation history to load", 125 | HideSelected: true, 126 | Items: promptNames, 127 | CursorPos: slices.Index(promptNames, empty), 128 | } 129 | 130 | _, result, err := prompt.Run() 131 | if err != nil { 132 | return result, nil, err 133 | } 134 | 135 | if result == empty { 136 | return result, nil, nil 137 | } 138 | 139 | serializedContent := h.configuration.Data.History[result] 140 | content := make([]*genai.Content, len(serializedContent)) 141 | for i, c := range serializedContent { 142 | content[i] = c.ToContent() 143 | } 144 | 145 | return result, content, nil 146 | } 147 | 148 | // promptHistoryLabel returns a label for the history record. 149 | func (h *HistoryCommand) promptHistoryLabel() (string, error) { 150 | prompt := promptui.Prompt{ 151 | Label: "Enter a label for the history record", 152 | HideEntered: true, 153 | } 154 | 155 | label, err := prompt.Run() 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | return label, nil 161 | } 162 | 163 | // selectHistoryOption returns the selected history action name. 164 | func (h *HistoryCommand) selectHistoryOption() (string, error) { 165 | prompt := promptui.Select{ 166 | Label: "Select history option", 167 | HideSelected: true, 168 | Items: historyOptions, 169 | } 170 | 171 | _, result, err := prompt.Run() 172 | if err != nil { 173 | return "", err 174 | } 175 | 176 | return result, nil 177 | } 178 | -------------------------------------------------------------------------------- /internal/handler/input_mode_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/manifoldco/promptui" 7 | ) 8 | 9 | var inputModeOptions = []string{ 10 | "Single-line", 11 | "Multi-line", 12 | } 13 | 14 | // InputModeCommand processes the chat input mode system command. 15 | // It implements the MessageHandler interface. 16 | type InputModeCommand struct { 17 | *IO 18 | } 19 | 20 | var _ MessageHandler = (*InputModeCommand)(nil) 21 | 22 | // NewInputModeCommand returns a new InputModeCommand. 23 | func NewInputModeCommand(io *IO) *InputModeCommand { 24 | return &InputModeCommand{ 25 | IO: io, 26 | } 27 | } 28 | 29 | // Handle processes the chat input mode system command. 30 | func (h *InputModeCommand) Handle(_ string) (Response, bool) { 31 | defer h.terminal.Write(h.terminalPrompt) 32 | multiline, err := h.selectInputMode() 33 | if err != nil { 34 | return newErrorResponse(err), false 35 | } 36 | 37 | if h.terminal.Config.Multiline == multiline { 38 | // the same input mode is selected 39 | return dataResponse(unchangedMessage), false 40 | } 41 | 42 | h.terminal.Config.Multiline = multiline 43 | h.terminal.SetUserPrompt() 44 | if h.terminal.Config.Multiline { 45 | // disable history for multi-line messages since it is 46 | // unusable for future requests 47 | h.terminal.Reader.HistoryDisable() 48 | } else { 49 | h.terminal.Reader.HistoryEnable() 50 | } 51 | 52 | mode := inputModeOptions[modeIndex(h.terminal.Config.Multiline)] 53 | return dataResponse(fmt.Sprintf("Switched to %q input mode.", mode)), false 54 | } 55 | 56 | // selectInputMode returns true if multiline input is selected; 57 | // otherwise, it returns false. 58 | func (h *InputModeCommand) selectInputMode() (bool, error) { 59 | prompt := promptui.Select{ 60 | Label: "Select input mode", 61 | HideSelected: true, 62 | Items: inputModeOptions, 63 | CursorPos: modeIndex(h.terminal.Config.Multiline), 64 | } 65 | 66 | _, result, err := prompt.Run() 67 | if err != nil { 68 | return false, err 69 | } 70 | 71 | return result == inputModeOptions[1], nil 72 | } 73 | 74 | func modeIndex(b bool) int { 75 | if b { 76 | return 1 77 | } 78 | return 0 79 | } 80 | -------------------------------------------------------------------------------- /internal/handler/model_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/manifoldco/promptui" 9 | "github.com/reugn/gemini-cli/gemini" 10 | ) 11 | 12 | var modelOptions = []string{ 13 | "Select generative model", 14 | "Chat model info", 15 | } 16 | 17 | // ModelCommand processes the chat model system commands. 18 | // It implements the MessageHandler interface. 19 | type ModelCommand struct { 20 | *IO 21 | session *gemini.ChatSession 22 | generativeModelName string 23 | } 24 | 25 | var _ MessageHandler = (*ModelCommand)(nil) 26 | 27 | // NewModelCommand returns a new ModelCommand. 28 | func NewModelCommand(io *IO, session *gemini.ChatSession, modelName string) *ModelCommand { 29 | return &ModelCommand{ 30 | IO: io, 31 | session: session, 32 | generativeModelName: modelName, 33 | } 34 | } 35 | 36 | // Handle processes the chat model system command. 37 | func (h *ModelCommand) Handle(_ string) (Response, bool) { 38 | option, err := h.selectModelOption() 39 | if err != nil { 40 | return newErrorResponse(err), false 41 | } 42 | 43 | var response Response 44 | switch option { 45 | case modelOptions[0]: 46 | response = h.handleSelectModel() 47 | case modelOptions[1]: 48 | response = h.handleModelInfo() 49 | default: 50 | response = newErrorResponse(fmt.Errorf("unsupported option: %s", option)) 51 | } 52 | return response, false 53 | } 54 | 55 | // handleSelectModel handles the generative model selection. 56 | func (h *ModelCommand) handleSelectModel() Response { 57 | defer h.terminal.Write(h.terminalPrompt) 58 | modelName, err := h.selectModel(h.session.ListModels()) 59 | if err != nil { 60 | return newErrorResponse(err) 61 | } 62 | 63 | if h.generativeModelName == modelName { 64 | return dataResponse(unchangedMessage) 65 | } 66 | 67 | modelBuilder := h.session.CopyModelBuilder().WithName(modelName) 68 | h.session.SetModel(modelBuilder) 69 | h.generativeModelName = modelName 70 | 71 | return dataResponse(fmt.Sprintf("Selected %q generative model.", modelName)) 72 | } 73 | 74 | // handleSelectModel handles the current generative model info request. 75 | func (h *ModelCommand) handleModelInfo() Response { 76 | h.terminal.Write(h.terminalPrompt) 77 | h.terminal.Spinner.Start() 78 | defer h.terminal.Spinner.Stop() 79 | 80 | modelInfo, err := h.session.ModelInfo() 81 | if err != nil { 82 | return newErrorResponse(err) 83 | } 84 | return dataResponse(modelInfo) 85 | } 86 | 87 | // selectModelOption returns the selected action name. 88 | func (h *ModelCommand) selectModelOption() (string, error) { 89 | prompt := promptui.Select{ 90 | Label: "Select model option", 91 | HideSelected: true, 92 | Items: modelOptions, 93 | } 94 | 95 | _, result, err := prompt.Run() 96 | if err != nil { 97 | return "", err 98 | } 99 | 100 | return result, nil 101 | } 102 | 103 | // selectModel returns the selected generative model name. 104 | func (h *ModelCommand) selectModel(models []string) (string, error) { 105 | prompt := promptui.Select{ 106 | Label: modelOptions[0], 107 | HideSelected: true, 108 | Items: models, 109 | CursorPos: slices.Index(models, h.generativeModelName), 110 | Searcher: func(input string, index int) bool { 111 | return strings.Contains(models[index], input) 112 | }, 113 | } 114 | 115 | _, result, err := prompt.Run() 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | return result, nil 121 | } 122 | -------------------------------------------------------------------------------- /internal/handler/prompt_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | 7 | "github.com/google/generative-ai-go/genai" 8 | "github.com/manifoldco/promptui" 9 | "github.com/reugn/gemini-cli/gemini" 10 | "github.com/reugn/gemini-cli/internal/config" 11 | ) 12 | 13 | // SystemPromptCommand processes the chat prompt system command. 14 | // It implements the MessageHandler interface. 15 | type SystemPromptCommand struct { 16 | *IO 17 | session *gemini.ChatSession 18 | applicationData *config.ApplicationData 19 | 20 | systemPrompt string 21 | } 22 | 23 | var _ MessageHandler = (*SystemPromptCommand)(nil) 24 | 25 | // NewSystemPromptCommand returns a new SystemPromptCommand. 26 | func NewSystemPromptCommand(io *IO, session *gemini.ChatSession, 27 | applicationData *config.ApplicationData) *SystemPromptCommand { 28 | return &SystemPromptCommand{ 29 | IO: io, 30 | session: session, 31 | applicationData: applicationData, 32 | } 33 | } 34 | 35 | // Handle processes the chat prompt system command. 36 | func (h *SystemPromptCommand) Handle(_ string) (Response, bool) { 37 | defer h.terminal.Write(h.terminalPrompt) 38 | label, systemPrompt, err := h.selectSystemPrompt() 39 | if err != nil { 40 | return newErrorResponse(err), false 41 | } 42 | 43 | modelBuilder := h.session.CopyModelBuilder(). 44 | WithSystemInstruction(systemPrompt) 45 | h.session.SetModel(modelBuilder) 46 | 47 | return dataResponse(fmt.Sprintf("Selected %q system instruction.", label)), false 48 | } 49 | 50 | // selectSystemPrompt returns a system instruction to be set. 51 | func (h *SystemPromptCommand) selectSystemPrompt() (string, *genai.Content, error) { 52 | promptNames := make([]string, len(h.applicationData.SystemPrompts)+1) 53 | promptNames[0] = empty 54 | i := 1 55 | for p := range h.applicationData.SystemPrompts { 56 | promptNames[i] = p 57 | i++ 58 | } 59 | prompt := promptui.Select{ 60 | Label: "Select system instruction", 61 | HideSelected: true, 62 | Items: promptNames, 63 | CursorPos: slices.Index(promptNames, h.systemPrompt), 64 | } 65 | 66 | _, result, err := prompt.Run() 67 | if err != nil { 68 | return result, nil, err 69 | } 70 | 71 | h.systemPrompt = result 72 | if result == empty { 73 | return result, nil, nil 74 | } 75 | 76 | systemInstruction := h.applicationData.SystemPrompts[result] 77 | return result, systemInstruction.ToContent(), nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/handler/quit_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | // QuitCommand processes the chat quit system command. 4 | // It implements the MessageHandler interface. 5 | type QuitCommand struct { 6 | *IO 7 | } 8 | 9 | var _ MessageHandler = (*QuitCommand)(nil) 10 | 11 | // NewQuitCommand returns a new QuitCommand. 12 | func NewQuitCommand(io *IO) *QuitCommand { 13 | return &QuitCommand{IO: io} 14 | } 15 | 16 | // Handle processes the chat quit command. 17 | func (h *QuitCommand) Handle(_ string) (Response, bool) { 18 | return dataResponse("Exiting gemini-cli..."), true 19 | } 20 | -------------------------------------------------------------------------------- /internal/handler/renderer_options.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/charmbracelet/glamour" 7 | ) 8 | 9 | // RendererOptions represents configuration options for the terminal renderer. 10 | type RendererOptions struct { 11 | StylePath string 12 | WordWrap int 13 | } 14 | 15 | func (o RendererOptions) newTermRenderer() (*glamour.TermRenderer, error) { 16 | var styleOption glamour.TermRendererOption 17 | switch { 18 | case o.StylePath == glamour.AutoStyle && os.Getenv("GLAMOUR_STYLE") != "": 19 | styleOption = glamour.WithEnvironmentConfig() 20 | default: 21 | styleOption = glamour.WithStylePath(o.StylePath) 22 | } 23 | 24 | return glamour.NewTermRenderer( 25 | styleOption, 26 | glamour.WithWordWrap(o.WordWrap), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /internal/handler/response.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/reugn/gemini-cli/internal/terminal" 7 | ) 8 | 9 | // Response represents a response from a chat message handler. 10 | type Response interface { 11 | fmt.Stringer 12 | } 13 | 14 | type dataResponse string 15 | 16 | var _ Response = (*dataResponse)(nil) 17 | 18 | func (r dataResponse) String() string { 19 | return fmt.Sprintf("%s\n", string(r)) 20 | } 21 | 22 | //nolint:errname 23 | type errorResponse struct { 24 | error 25 | } 26 | 27 | func newErrorResponse(err error) errorResponse { 28 | return errorResponse{error: err} 29 | } 30 | 31 | var _ Response = (*errorResponse)(nil) 32 | 33 | func (r errorResponse) String() string { 34 | return fmt.Sprintf("%s\n", terminal.Error(r.Error())) 35 | } 36 | -------------------------------------------------------------------------------- /internal/handler/system_command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/reugn/gemini-cli/gemini" 8 | "github.com/reugn/gemini-cli/internal/cli" 9 | "github.com/reugn/gemini-cli/internal/config" 10 | ) 11 | 12 | const ( 13 | empty = "Empty" 14 | unchangedMessage = "The selection is unchanged." 15 | ) 16 | 17 | // SystemCommand processes chat system commands; implements the MessageHandler interface. 18 | // It aggregates the processing by delegating it to one of the underlying handlers. 19 | type SystemCommand struct { 20 | *IO 21 | handlers map[string]MessageHandler 22 | } 23 | 24 | var _ MessageHandler = (*SystemCommand)(nil) 25 | 26 | // NewSystemCommand returns a new SystemCommand. 27 | func NewSystemCommand(io *IO, session *gemini.ChatSession, configuration *config.Configuration, 28 | modelName string, rendererOptions RendererOptions) (*SystemCommand, error) { 29 | helpCommandHandler, err := NewHelpCommand(io, rendererOptions) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | handlers := map[string]MessageHandler{ 35 | cli.SystemCmdHelp: helpCommandHandler, 36 | cli.SystemCmdQuit: NewQuitCommand(io), 37 | cli.SystemCmdSelectPrompt: NewSystemPromptCommand(io, session, configuration.Data), 38 | cli.SystemCmdSelectInputMode: NewInputModeCommand(io), 39 | cli.SystemCmdModel: NewModelCommand(io, session, modelName), 40 | cli.SystemCmdHistory: NewHistoryCommand(io, session, configuration), 41 | } 42 | 43 | return &SystemCommand{ 44 | IO: io, 45 | handlers: handlers, 46 | }, nil 47 | } 48 | 49 | // Handle processes the chat system command. 50 | func (s *SystemCommand) Handle(message string) (Response, bool) { 51 | if !strings.HasPrefix(message, cli.SystemCmdPrefix) { 52 | return newErrorResponse(fmt.Errorf("system command mismatch")), false 53 | } 54 | 55 | var args string 56 | t := strings.SplitN(message, " ", 2) 57 | if len(t) == 2 { 58 | args = t[1] 59 | } 60 | 61 | systemHandler, ok := s.handlers[message[1:]] 62 | if !ok { 63 | return newErrorResponse(fmt.Errorf("unknown system command")), false 64 | } 65 | 66 | return systemHandler.Handle(args) 67 | } 68 | -------------------------------------------------------------------------------- /internal/handler/terminal_io.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import "github.com/reugn/gemini-cli/internal/terminal" 4 | 5 | // IO encapsulates terminal details for handlers. 6 | type IO struct { 7 | terminal *terminal.IO 8 | terminalPrompt string 9 | } 10 | 11 | // NewIO returns a new IO. 12 | func NewIO(terminal *terminal.IO, terminalPrompt string) *IO { 13 | return &IO{ 14 | terminal: terminal, 15 | terminalPrompt: terminalPrompt, 16 | } 17 | } 18 | 19 | // TerminalPrompt returns the terminal prompt string. 20 | func (io *IO) TerminalPrompt() string { 21 | return io.terminalPrompt 22 | } 23 | -------------------------------------------------------------------------------- /internal/terminal/color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import "fmt" 4 | 5 | const ( 6 | reset = "\033[0m" 7 | red = "\033[31m" 8 | green = "\033[32m" 9 | yellow = "\033[33m" 10 | blue = "\033[34m" 11 | magenta = "\033[35m" 12 | cyan = "\033[36m" 13 | gray = "\033[37m" 14 | white = "\033[97m" 15 | ) 16 | 17 | // Red adds red color to str in terminal. 18 | func Red(str string) string { 19 | return fmt.Sprintf("%s%s%s", red, str, reset) 20 | } 21 | 22 | // Green adds green color to str in terminal. 23 | func Green(str string) string { 24 | return fmt.Sprintf("%s%s%s", green, str, reset) 25 | } 26 | 27 | // Yellow adds yellow color to str in terminal. 28 | func Yellow(str string) string { 29 | return fmt.Sprintf("%s%s%s", yellow, str, reset) 30 | } 31 | 32 | // Blue adds blue color to str in terminal. 33 | func Blue(str string) string { 34 | return fmt.Sprintf("%s%s%s", blue, str, reset) 35 | } 36 | 37 | // Magenta adds magenta color to str in terminal. 38 | func Magenta(str string) string { 39 | return fmt.Sprintf("%s%s%s", magenta, str, reset) 40 | } 41 | 42 | // Cyan adds cyan color to str in terminal. 43 | func Cyan(str string) string { 44 | return fmt.Sprintf("%s%s%s", cyan, str, reset) 45 | } 46 | 47 | // Gray adds gray color to str in terminal. 48 | func Gray(str string) string { 49 | return fmt.Sprintf("%s%s%s", gray, str, reset) 50 | } 51 | 52 | // White adds white color to str in terminal. 53 | func White(str string) string { 54 | return fmt.Sprintf("%s%s%s", white, str, reset) 55 | } 56 | -------------------------------------------------------------------------------- /internal/terminal/io.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "time" 9 | 10 | "github.com/chzyer/readline" 11 | "github.com/reugn/gemini-cli/internal/cli" 12 | "github.com/reugn/gemini-cli/internal/terminal/color" 13 | ) 14 | 15 | var Error = color.Red 16 | 17 | // IOConfig represents the configuration settings for IO. 18 | type IOConfig struct { 19 | User string 20 | Multiline bool 21 | LineTerminator string 22 | } 23 | 24 | // IO encapsulates input/output operations. 25 | type IO struct { 26 | Reader *readline.Instance 27 | Prompt *Prompt 28 | Spinner *Spinner 29 | writer io.Writer 30 | 31 | Config *IOConfig 32 | } 33 | 34 | // NewIO returns a new IO based on the provided configuration. 35 | func NewIO(config *IOConfig) (*IO, error) { 36 | reader, err := readline.NewEx(&readline.Config{}) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | terminalPrompt := NewPrompt(config.User) 42 | reader.SetPrompt(terminalPrompt.User) 43 | if config.Multiline { 44 | // disable history for multiline input mode 45 | reader.HistoryDisable() 46 | } 47 | 48 | return &IO{ 49 | Reader: reader, 50 | Prompt: terminalPrompt, 51 | Spinner: NewSpinner(reader.Stdout(), time.Second, 5), 52 | writer: reader.Stdout(), 53 | Config: config, 54 | }, nil 55 | } 56 | 57 | // Read reads input from the underlying source and returns it as a string. 58 | // If multiline is true, it reads all available lines; otherwise, it reads a single line. 59 | func (io *IO) Read() string { 60 | if io.Config.Multiline { 61 | return io.readMultiLine() 62 | } 63 | return io.readLine() 64 | } 65 | 66 | // Write writes the given string data to the underlying data stream. 67 | func (io *IO) Write(data string) { 68 | _, _ = fmt.Fprint(io.writer, data) 69 | } 70 | 71 | func (io *IO) readLine() string { 72 | input, err := io.Reader.Readline() 73 | if err != nil { 74 | return io.handleReadError(err, len(input)) 75 | } 76 | return strings.TrimSpace(input) 77 | } 78 | 79 | func (io *IO) readMultiLine() string { 80 | defer io.SetUserPrompt() 81 | var builder strings.Builder 82 | for { 83 | input, err := io.Reader.Readline() 84 | if err != nil { 85 | return io.handleReadError(err, builder.Len()+len(input)) 86 | } 87 | 88 | if strings.HasSuffix(input, io.Config.LineTerminator) || 89 | strings.HasPrefix(input, cli.SystemCmdPrefix) { 90 | builder.WriteString(strings.TrimSuffix(input, io.Config.LineTerminator)) 91 | break 92 | } 93 | 94 | if builder.Len() == 0 { 95 | io.Reader.SetPrompt(io.Prompt.UserMultilineNext) 96 | } 97 | 98 | builder.WriteString(input) 99 | builder.WriteRune('\n') 100 | } 101 | return strings.TrimSpace(builder.String()) 102 | } 103 | 104 | func (io *IO) handleReadError(err error, inputLen int) string { 105 | if errors.Is(err, readline.ErrInterrupt) { 106 | if inputLen == 0 { 107 | // handle as the quit command 108 | return cli.SystemCmdPrefix + cli.SystemCmdQuit 109 | } 110 | } else { 111 | io.Write(fmt.Sprintf("%s%s\n", io.Prompt.Cli, Error(err.Error()))) 112 | } 113 | return "" 114 | } 115 | 116 | // SetUserPrompt sets the terminal prompt according to the current input mode. 117 | func (io *IO) SetUserPrompt() { 118 | if io.Config.Multiline { 119 | io.Reader.SetPrompt(io.Prompt.UserMultiline) 120 | } else { 121 | io.Reader.SetPrompt(io.Prompt.User) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /internal/terminal/prompt.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/muesli/termenv" 8 | "github.com/reugn/gemini-cli/internal/terminal/color" 9 | ) 10 | 11 | const ( 12 | geminiUser = "gemini" 13 | cliUser = "cli" 14 | ) 15 | 16 | type Prompt struct { 17 | User string 18 | UserMultiline string 19 | UserMultilineNext string 20 | Gemini string 21 | Cli string 22 | } 23 | 24 | type promptColor struct { 25 | user func(string) string 26 | gemini func(string) string 27 | cli func(string) string 28 | } 29 | 30 | func newPromptColor() *promptColor { 31 | if termenv.HasDarkBackground() { 32 | return &promptColor{ 33 | user: color.Cyan, 34 | gemini: color.Green, 35 | cli: color.Yellow, 36 | } 37 | } 38 | return &promptColor{ 39 | user: color.Blue, 40 | gemini: color.Green, 41 | cli: color.Magenta, 42 | } 43 | } 44 | 45 | func NewPrompt(currentUser string) *Prompt { 46 | maxLength := maxLength(currentUser, geminiUser, cliUser) 47 | pc := newPromptColor() 48 | return &Prompt{ 49 | User: pc.user(buildPrompt(currentUser, '>', maxLength)), 50 | UserMultiline: pc.user(buildPrompt(currentUser, '#', maxLength)), 51 | UserMultilineNext: pc.user(buildPrompt(strings.Repeat(" ", len(currentUser)), '>', maxLength)), 52 | Gemini: pc.gemini(buildPrompt(geminiUser, '>', maxLength)), 53 | Cli: pc.cli(buildPrompt(cliUser, '>', maxLength)), 54 | } 55 | } 56 | 57 | func maxLength(strings ...string) int { 58 | var maxLength int 59 | for _, s := range strings { 60 | length := len(s) 61 | if maxLength < length { 62 | maxLength = length 63 | } 64 | } 65 | return maxLength 66 | } 67 | 68 | func buildPrompt(user string, p byte, length int) string { 69 | return fmt.Sprintf("%s%c%s", user, p, strings.Repeat(" ", length-len(user)+1)) 70 | } 71 | -------------------------------------------------------------------------------- /internal/terminal/spinner.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | const ( 11 | moveCursorBackward = "\033[%dD" 12 | clearLineFromCursor = "\033[K" 13 | progressRune = '.' 14 | ) 15 | 16 | // Spinner is a visual indicator of progress displayed in the terminal as a 17 | // scrolling dot animation. 18 | type Spinner struct { 19 | writer *bufio.Writer 20 | interval time.Duration 21 | signal chan struct{} 22 | 23 | maxLength int 24 | length int 25 | } 26 | 27 | // NewSpinner returns a new Spinner. 28 | func NewSpinner(w io.Writer, interval time.Duration, length int) *Spinner { 29 | return &Spinner{ 30 | writer: bufio.NewWriter(w), 31 | interval: interval, 32 | signal: make(chan struct{}), 33 | maxLength: length, 34 | } 35 | } 36 | 37 | //nolint:errcheck 38 | func (s *Spinner) Start() { 39 | go func() { 40 | ticker := time.NewTicker(s.interval) 41 | defer ticker.Stop() 42 | s.length = 0 43 | for { 44 | select { 45 | case <-s.signal: 46 | if s.length > 0 { 47 | s.Clear() 48 | } 49 | s.signal <- struct{}{} 50 | return 51 | case <-ticker.C: 52 | if s.length < s.maxLength { 53 | s.writer.WriteRune(progressRune) 54 | s.writer.Flush() 55 | s.length++ 56 | } else { 57 | s.Clear() 58 | s.length = 0 59 | } 60 | } 61 | } 62 | }() 63 | } 64 | 65 | //nolint:errcheck,staticcheck 66 | func (s *Spinner) Clear() { 67 | s.writer.WriteString(fmt.Sprintf(moveCursorBackward, s.length)) 68 | s.writer.WriteString(clearLineFromCursor) 69 | s.writer.Flush() 70 | } 71 | 72 | func (s *Spinner) Stop() { 73 | s.signal <- struct{}{} 74 | <-s.signal 75 | } 76 | --------------------------------------------------------------------------------