├── .gitignore ├── img └── sreya-log.png ├── COMMITS.md ├── Makefile ├── cmd └── aicommit │ ├── version.go │ ├── savekey.go │ └── main.go ├── brew-bump.sh ├── .github └── workflows │ └── release.yaml ├── .goreleaser.yml ├── go.mod ├── README.md ├── LICENSE ├── prompt.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | bin/aicommit 2 | dist 3 | -------------------------------------------------------------------------------- /img/sreya-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/aicommit/HEAD/img/sreya-log.png -------------------------------------------------------------------------------- /COMMITS.md: -------------------------------------------------------------------------------- 1 | This style guide is used chiefly to test that aicommit follows the 2 | style guide. 3 | 4 | * Only provide a multi-line message when the change is non-trivial. 5 | * For example, a few lines changed is trivial. Prefer a single-line message. 6 | * Most changes under 100 lines changed are trivial and only need a single-line 7 | message. 8 | * Never begin the commit with an emoji -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | SHELL := /bin/bash 3 | 4 | .PHONY: build 5 | # Build the aicommit binary 6 | build: 7 | set -e 8 | mkdir -p bin 9 | # version may be passed in via homebrew formula 10 | if [ -z "$$VERSION" ]; then \ 11 | VERSION=$$(git describe --tags --always --dirty || echo "dev"); \ 12 | fi 13 | echo "Building version $${VERSION}" 14 | go build -ldflags "-X main.Version=$${VERSION}" -o bin/aicommit ./cmd/aicommit 15 | -------------------------------------------------------------------------------- /cmd/aicommit/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/coder/serpent" 7 | ) 8 | 9 | // Version is set during build using ldflags 10 | var Version = "dev" 11 | 12 | func versionCmd() *serpent.Command { 13 | return &serpent.Command{ 14 | Use: "version", 15 | Long: "Print build version information", 16 | Handler: func(inv *serpent.Invocation) error { 17 | fmt.Fprintf(inv.Stdout, "aicommit %s\n", Version) 18 | return nil 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /brew-bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | LATEST_VERSION=$(git describe --tags --always) 7 | echo "Latest version: ${LATEST_VERSION}" 8 | cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core 9 | ARCHIVE_URL=https://github.com/coder/aicommit/archive/refs/tags/${LATEST_VERSION}.tar.gz 10 | ARCHIVE_SHA=$(curl -sL ${ARCHIVE_URL} | sha256sum | awk '{ print $1 }') 11 | echo "Archive URL: ${ARCHIVE_URL}" 12 | echo "Archive SHA: ${ARCHIVE_SHA}" 13 | if [[ -z "${ARCHIVE_SHA}" ]]; then 14 | echo "Archive SHA is empty" 15 | exit 1 16 | fi 17 | brew bump-formula-pr --url=${ARCHIVE_URL} --sha256=${ARCHIVE_SHA} aicommit 18 | -------------------------------------------------------------------------------- /cmd/aicommit/savekey.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func configDir() (string, error) { 10 | cdir, err := os.UserConfigDir() 11 | if err != nil { 12 | return "", err 13 | } 14 | err = os.MkdirAll(filepath.Join(cdir, "aicommit"), 0o700) 15 | if err != nil { 16 | return "", err 17 | } 18 | return filepath.Join(cdir, "aicommit"), nil 19 | } 20 | 21 | func keyPath() (string, error) { 22 | cdir, err := configDir() 23 | if err != nil { 24 | return "", err 25 | } 26 | return filepath.Join(cdir, "openai.key"), nil 27 | } 28 | 29 | func saveKey(key string) error { 30 | if key == "" { 31 | return errors.New("key is empty") 32 | } 33 | kp, err := keyPath() 34 | if err != nil { 35 | return err 36 | } 37 | return os.WriteFile(kp, []byte(key), 0o600) 38 | } 39 | 40 | func loadKey() (string, error) { 41 | kp, err := keyPath() 42 | if err != nil { 43 | return "", err 44 | } 45 | b, err := os.ReadFile(kp) 46 | if err != nil { 47 | return "", err 48 | } 49 | return string(b), nil 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # This GitHub action creates a release when a tag that matches the pattern 4 | # "v*" (e.g. v0.1.0) is created. 5 | on: 6 | push: 7 | tags: 8 | - "v*" 9 | workflow_dispatch: 10 | 11 | # Releases need permissions to read and write the repository contents. 12 | # GitHub considers creating releases and uploading assets as writing contents. 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | goreleaser: 18 | runs-on: ubuntu-latest-8-cores 19 | steps: 20 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 21 | with: 22 | # Allow goreleaser to access older tag information. 23 | fetch-depth: 0 24 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 25 | with: 26 | go-version-file: "go.mod" 27 | cache: true 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 30 | with: 31 | args: release --clean 32 | env: 33 | # GitHub sets the GITHUB_TOKEN secret automatically. 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - main: "./cmd/aicommit" 4 | env: 5 | - CGO_ENABLED=0 6 | mod_timestamp: "{{ .CommitTimestamp }}" 7 | flags: 8 | - -trimpath 9 | ldflags: 10 | - "-s -w -X main.Version={{.Version}}" 11 | goos: 12 | - freebsd 13 | - windows 14 | - linux 15 | - darwin 16 | goarch: 17 | - amd64 18 | - "386" 19 | - arm 20 | - arm64 21 | goarm: 22 | - "7" 23 | ignore: 24 | - goos: darwin 25 | goarch: "386" 26 | binary: "{{ .ProjectName }}" 27 | nfpms: 28 | - vendor: Coder Technologies Inc. 29 | homepage: https://coder.com/ 30 | maintainer: Ammar Bandukwala 31 | description: |- 32 | aicommit is a small command line tool for generating commit messages 33 | license: CC0-1.0 34 | contents: 35 | - src: LICENSE 36 | dst: "/usr/share/doc/{{ .ProjectName }}/copyright" 37 | formats: 38 | - apk 39 | - deb 40 | archives: 41 | - id: "zip" 42 | format: zip 43 | - id: "tarball" 44 | format: tar.gz 45 | checksum: 46 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 47 | algorithm: sha256 48 | 49 | # release: 50 | # draft: true 51 | changelog: 52 | use: github-native 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coder/aicommit 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 7 | github.com/coder/serpent v0.8.0 8 | github.com/muesli/termenv v0.15.2 9 | ) 10 | 11 | require ( 12 | dario.cat/mergo v1.0.0 // indirect 13 | github.com/Microsoft/go-winio v0.6.1 // indirect 14 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 15 | github.com/cloudflare/circl v1.3.7 // indirect 16 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 17 | github.com/dlclark/regexp2 v1.9.0 // indirect 18 | github.com/emirpasic/gods v1.18.1 // indirect 19 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 20 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 21 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 22 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 23 | github.com/kevinburke/ssh_config v1.2.0 // indirect 24 | github.com/pion/transport/v3 v3.0.7 // indirect 25 | github.com/pjbgf/sha1cd v0.3.0 // indirect 26 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 27 | github.com/skeema/knownhosts v1.2.2 // indirect 28 | github.com/xanzy/ssh-agent v0.3.3 // indirect 29 | golang.org/x/mod v0.20.0 // indirect 30 | golang.org/x/net v0.28.0 // indirect 31 | golang.org/x/sync v0.8.0 // indirect 32 | golang.org/x/tools v0.24.0 // indirect 33 | gopkg.in/warnings.v0 v0.1.2 // indirect 34 | ) 35 | 36 | require ( 37 | al.essio.dev/pkg/shellescape v1.5.0 38 | cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 // indirect 39 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 40 | github.com/go-git/go-git/v5 v5.12.0 41 | github.com/hashicorp/errwrap v1.1.0 // indirect 42 | github.com/hashicorp/go-multierror v1.1.1 // indirect 43 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/mattn/go-runewidth v0.0.16 // indirect 46 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 47 | github.com/pion/transport/v2 v2.2.10 // indirect 48 | github.com/pion/udp v0.1.4 // indirect 49 | github.com/rivo/uniseg v0.4.7 // indirect 50 | github.com/sashabaranov/go-openai v1.29.0 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/tiktoken-go/tokenizer v0.1.1 53 | go.opentelemetry.io/otel v1.29.0 // indirect 54 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 55 | golang.org/x/crypto v0.26.0 // indirect 56 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect 57 | golang.org/x/sys v0.24.0 // indirect 58 | golang.org/x/term v0.23.0 // indirect 59 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aicommit 2 | 3 | `aicommit` is a small command line tool for generating commit messages. There 4 | are many of these already out there, some even with the same name. But none 5 | (to my knowledge) follow the repository's existing style, making 6 | them useless when working in an established codebase. 7 | 8 | A good commit message is more than a summary of the code changes. It contains 9 | the intention, context, and external references that help others understand the 10 | change. Thus, `aicommit` has a `-c`/`--context` flag for quickly adding 11 | this detail. 12 | 13 | `aicommit` is inspired by our good friend [@sreya](https://github.com/sreya): 14 | 15 | ![sreya-log](./img/sreya-log.png) 16 | 17 | 18 | ## Install 19 | 20 | Via Homebrew: 21 | ``` 22 | brew install aicommit 23 | ``` 24 | 25 | Or, using Go: 26 | 27 | ``` 28 | go install github.com/coder/aicommit/cmd/aicommit@main 29 | ``` 30 | 31 | Or, download a binary from [the latest release](https://github.com/coder/aicommit/releases). 32 | 33 | ## Usage 34 | 35 | You can run `aicommit` with no arguments to generate a commit message for the 36 | staged changes. 37 | 38 | ```bash 39 | export OPENAI_API_KEY="..." 40 | aicommit 41 | ``` 42 | 43 | You can "retry" a commit message by using the `-a`/`--amend` flag. 44 | 45 | ```bash 46 | aicommit -a 47 | ``` 48 | 49 | You can dry-run with `-d`/`--dry` to see the ideal message without committing. 50 | 51 | ```bash 52 | aicommit -d 53 | ``` 54 | 55 | Or, you can point to a specific ref: 56 | 57 | ```bash 58 | aicommit HEAD~3 59 | ``` 60 | 61 | You can also provide context to the AI to help it generate a better commit message: 62 | 63 | ```bash 64 | aicommit -c "closes #123" 65 | 66 | aicommit -c "improved HTTP performance by 50%" 67 | 68 | aicommit -c "bad code but need for urgent customer fix" 69 | ``` 70 | 71 | When tired of setting environment variables, you can save your key to disk: 72 | 73 | ```bash 74 | export OPENAI_API_KEY="..." 75 | aicommit --save-key 76 | # The environment variable will override the saved key. 77 | ``` 78 | 79 | ## Style Guide 80 | 81 | `aicommit` will read the `COMMITS.md` file in the root of the repository to 82 | determine the style guide. It is optional, but if it exists, it will be followed 83 | even if the rules there diverge from the norm. 84 | 85 | If there is no repo style guide, `aicommit` will look for a user style guide 86 | in `~/COMMITS.md`. 87 | 88 | ## Other Providers 89 | 90 | You may set `OPENAI_BASE_URL` to use other OpenAI compatible APIs with `aicommit`. 91 | So far, I've tested it with [LiteLLM](https://github.com/BerriAI/litellm) across 92 | local models (via ollama) and Anthropic. I have yet to find a local model 93 | that is well-steered by the prompt design here, but the Anthropic Claude 3.5 94 | commit messages are on par with 4o. My theory for why local models don't work well 95 | is they (incl. "Instruct" models) have much worse instruction fine-tuning 96 | than flagship commercial models. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /cmd/aicommit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "al.essio.dev/pkg/shellescape" 14 | "github.com/coder/aicommit" 15 | "github.com/coder/pretty" 16 | "github.com/coder/serpent" 17 | "github.com/muesli/termenv" 18 | "github.com/sashabaranov/go-openai" 19 | ) 20 | 21 | var colorProfile = termenv.ColorProfile() 22 | 23 | func errorf(format string, args ...any) { 24 | c := pretty.FgColor(colorProfile.Color("#ff0000")) 25 | pretty.Fprintf(os.Stderr, c, "err: "+format, args...) 26 | } 27 | 28 | var debugMode = os.Getenv("AICOMMIT_DEBUG") != "" 29 | 30 | func debugf(format string, args ...any) { 31 | if !debugMode { 32 | return 33 | } 34 | // Gray 35 | c := pretty.FgColor(colorProfile.Color("#808080")) 36 | pretty.Fprintf(os.Stderr, c, "debug: "+format+"\n", args...) 37 | } 38 | 39 | func getLastCommitHash() (string, error) { 40 | cmd := exec.Command("git", "rev-parse", "HEAD") 41 | output, err := cmd.Output() 42 | if err != nil { 43 | return "", err 44 | } 45 | return strings.TrimSpace(string(output)), nil 46 | } 47 | 48 | func resolveRef(ref string) (string, error) { 49 | cmd := exec.Command("git", "rev-parse", ref) 50 | output, err := cmd.Output() 51 | if err != nil { 52 | return "", err 53 | } 54 | return strings.TrimSpace(string(output)), nil 55 | } 56 | 57 | func formatShellCommand(cmd *exec.Cmd) string { 58 | buf := &strings.Builder{} 59 | buf.WriteString(filepath.Base(cmd.Path)) 60 | for _, arg := range cmd.Args[1:] { 61 | buf.WriteString(" ") 62 | buf.WriteString(shellescape.Quote(arg)) 63 | } 64 | return buf.String() 65 | } 66 | 67 | func cleanAIMessage(msg string) string { 68 | // As reported by Ben, sometimes GPT returns the message 69 | // wrapped in a code block. 70 | if strings.HasPrefix(msg, "```") { 71 | msg = strings.TrimSuffix(msg, "```") 72 | msg = strings.TrimPrefix(msg, "```") 73 | } 74 | msg = strings.TrimSpace(msg) 75 | return msg 76 | } 77 | 78 | func run(inv *serpent.Invocation, opts runOptions) error { 79 | workdir, err := os.Getwd() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if opts.ref != "" && opts.amend { 85 | return errors.New("cannot use both [ref] and --amend") 86 | } 87 | 88 | hash := "" 89 | if opts.amend { 90 | lastCommitHash, err := getLastCommitHash() 91 | if err != nil { 92 | return err 93 | } 94 | hash = lastCommitHash 95 | } else if opts.ref != "" { 96 | // Resolve the ref to a hash. 97 | hash, err = resolveRef(opts.ref) 98 | if err != nil { 99 | return fmt.Errorf("resolve ref %q: %w", opts.ref, err) 100 | } 101 | } 102 | 103 | msgs, err := aicommit.BuildPrompt(inv.Stdout, workdir, hash, opts.amend, 128000) 104 | if err != nil { 105 | return err 106 | } 107 | if len(opts.context) > 0 { 108 | msgs = append(msgs, openai.ChatCompletionMessage{ 109 | Role: openai.ChatMessageRoleSystem, 110 | Content: "The user has provided additional context that MUST be" + 111 | " included in the commit message", 112 | }) 113 | for _, context := range opts.context { 114 | msgs = append(msgs, openai.ChatCompletionMessage{ 115 | Role: openai.ChatMessageRoleUser, 116 | Content: context, 117 | }) 118 | } 119 | } 120 | 121 | ctx := inv.Context() 122 | if debugMode { 123 | for _, msg := range msgs { 124 | debugf("%s: (%v tokens)\n %s\n\n", msg.Role, aicommit.CountTokens(msg), msg.Content) 125 | } 126 | debugf("prompt includes %d commits\n", len(msgs)/2) 127 | } 128 | 129 | stream, err := opts.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{ 130 | Model: opts.model, 131 | Stream: true, 132 | Temperature: 0, 133 | // Seed must not be set for the amend-retry workflow. 134 | // Seed: &seed, 135 | StreamOptions: &openai.StreamOptions{ 136 | IncludeUsage: true, 137 | }, 138 | Messages: msgs, 139 | }) 140 | if err != nil { 141 | return err 142 | } 143 | defer stream.Close() 144 | 145 | msg := &bytes.Buffer{} 146 | 147 | // Sky blue color 148 | color := pretty.FgColor(colorProfile.Color("#2FA8FF")) 149 | 150 | for { 151 | resp, err := stream.Recv() 152 | if err != nil { 153 | if err == io.EOF { 154 | debugf("stream EOF") 155 | break 156 | } 157 | return err 158 | } 159 | // Usage is only sent in the last message. 160 | if resp.Usage != nil { 161 | debugf("total tokens: %d", resp.Usage.TotalTokens) 162 | break 163 | } 164 | c := resp.Choices[0].Delta.Content 165 | msg.WriteString(c) 166 | pretty.Fprintf(inv.Stdout, color, "%s", c) 167 | } 168 | inv.Stdout.Write([]byte("\n")) 169 | 170 | msg = bytes.NewBufferString(cleanAIMessage(msg.String())) 171 | 172 | cmd := exec.Command("git", "commit", "-m", msg.String()) 173 | if opts.amend { 174 | cmd.Args = append(cmd.Args, "--amend") 175 | } 176 | 177 | if opts.dryRun { 178 | fmt.Fprintf(inv.Stdout, "Run the following command to commit:\n") 179 | inv.Stdout.Write([]byte("" + formatShellCommand(cmd) + "\n")) 180 | return nil 181 | } 182 | if opts.ref != "" { 183 | debugf("targetting old ref, not committing") 184 | return nil 185 | } 186 | 187 | inv.Stdout.Write([]byte("\n")) 188 | 189 | cmd.Stderr = os.Stderr 190 | cmd.Stdout = os.Stdout 191 | cmd.Stdin = os.Stdin 192 | return cmd.Run() 193 | } 194 | 195 | type runOptions struct { 196 | client *openai.Client 197 | openAIBaseURL string 198 | model string 199 | dryRun bool 200 | amend bool 201 | ref string 202 | context []string 203 | } 204 | 205 | func main() { 206 | var ( 207 | opts runOptions 208 | cliOpenAIKey string 209 | doSaveKey bool 210 | ) 211 | cmd := &serpent.Command{ 212 | Use: "aicommit [ref]", 213 | Short: "aicommit is a tool for generating commit messages", 214 | Handler: func(inv *serpent.Invocation) error { 215 | savedKey, err := loadKey() 216 | if err != nil && !os.IsNotExist(err) { 217 | return err 218 | } 219 | var openAIKey string 220 | if savedKey != "" && cliOpenAIKey == "" { 221 | openAIKey = savedKey 222 | } else if cliOpenAIKey != "" { 223 | openAIKey = cliOpenAIKey 224 | } 225 | 226 | if savedKey != "" && cliOpenAIKey != "" { 227 | openAIKeyOpt := inv.Command.Options.ByName("openai-key") 228 | if openAIKeyOpt == nil { 229 | panic("openai-key option not found") 230 | } 231 | // savedKey overrides cliOpenAIKey only when set via environment. 232 | // See https://github.com/coder/aicommit/issues/6. 233 | if openAIKeyOpt.ValueSource == serpent.ValueSourceEnv { 234 | openAIKey = savedKey 235 | } 236 | } 237 | 238 | if openAIKey == "" { 239 | return errors.New("$OPENAI_API_KEY is not set") 240 | } 241 | 242 | if doSaveKey { 243 | err := saveKey(cliOpenAIKey) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | kp, err := keyPath() 249 | if err != nil { 250 | return err 251 | } 252 | 253 | fmt.Fprintf(inv.Stdout, "Saved OpenAI API key to %s\n", kp) 254 | return nil 255 | } 256 | 257 | oaiConfig := openai.DefaultConfig(openAIKey) 258 | oaiConfig.BaseURL = opts.openAIBaseURL 259 | client := openai.NewClientWithConfig(oaiConfig) 260 | opts.client = client 261 | if len(inv.Args) > 0 { 262 | opts.ref = inv.Args[0] 263 | } 264 | return run(inv, opts) 265 | }, 266 | Options: []serpent.Option{ 267 | { 268 | Name: "openai-key", 269 | Description: "The OpenAI API key to use.", 270 | Env: "OPENAI_API_KEY", 271 | Flag: "openai-key", 272 | Value: serpent.StringOf(&cliOpenAIKey), 273 | }, 274 | { 275 | Name: "openai-base-url", 276 | Description: "The base URL to use for the OpenAI API.", 277 | Env: "OPENAI_BASE_URL", 278 | Flag: "openai-base-url", 279 | Value: serpent.StringOf(&opts.openAIBaseURL), 280 | Default: "https://api.openai.com/v1", 281 | }, 282 | { 283 | Name: "model", 284 | Description: "The model to use, e.g. gpt-4o or gpt-4o-mini.", 285 | Flag: "model", 286 | FlagShorthand: "m", 287 | Default: "gpt-4o-2024-08-06", 288 | Env: "AICOMMIT_MODEL", 289 | Value: serpent.StringOf(&opts.model), 290 | }, 291 | { 292 | Name: "save-key", 293 | Description: "Save the OpenAI API key to persistent local configuration and exit.", 294 | Flag: "save-key", 295 | Value: serpent.BoolOf(&doSaveKey), 296 | }, 297 | { 298 | Name: "dry-run", 299 | Flag: "dry", 300 | FlagShorthand: "d", 301 | Description: "Dry run the command.", 302 | Value: serpent.BoolOf(&opts.dryRun), 303 | }, 304 | { 305 | Name: "amend", 306 | Flag: "amend", 307 | FlagShorthand: "a", 308 | Description: "Amend the last commit.", 309 | Value: serpent.BoolOf(&opts.amend), 310 | }, 311 | { 312 | Name: "context", 313 | Description: "Extra context beyond the diff to consider when generating the commit message.", 314 | Flag: "context", 315 | FlagShorthand: "c", 316 | Value: serpent.StringArrayOf(&opts.context), 317 | }, 318 | }, 319 | Children: []*serpent.Command{ 320 | versionCmd(), 321 | }, 322 | } 323 | 324 | err := cmd.Invoke().WithOS().Run() 325 | if err != nil { 326 | var unknownCmdErr *serpent.UnknownSubcommandError 327 | if errors.As(err, &unknownCmdErr) { 328 | // Unknown command is printed by the help function for some reason. 329 | os.Exit(1) 330 | } 331 | var runCommandErr *serpent.RunCommandError 332 | if errors.As(err, &runCommandErr) { 333 | errorf("%s\n", runCommandErr.Err) 334 | os.Exit(1) 335 | } 336 | 337 | errorf("%s\n", err) 338 | os.Exit(1) 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /prompt.go: -------------------------------------------------------------------------------- 1 | package aicommit 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/go-git/go-git/v5" 14 | "github.com/go-git/go-git/v5/plumbing/object" 15 | "github.com/sashabaranov/go-openai" 16 | "github.com/tiktoken-go/tokenizer" 17 | ) 18 | 19 | func CountTokens(msgs ...openai.ChatCompletionMessage) int { 20 | enc, err := tokenizer.Get(tokenizer.Cl100kBase) 21 | if err != nil { 22 | panic("oh oh") 23 | } 24 | 25 | var tokens int 26 | for _, msg := range msgs { 27 | ts, _, _ := enc.Encode(msg.Content) 28 | tokens += len(ts) 29 | 30 | for _, call := range msg.ToolCalls { 31 | ts, _, _ = enc.Encode(call.Function.Arguments) 32 | tokens += len(ts) 33 | } 34 | } 35 | return tokens 36 | } 37 | 38 | // Ellipse returns a string that is truncated to the maximum number of tokens. 39 | func Ellipse(s string, maxTokens int) string { 40 | enc, err := tokenizer.Get(tokenizer.Cl100kBase) 41 | if err != nil { 42 | panic("failed to get tokenizer") 43 | } 44 | 45 | tokens, _, _ := enc.Encode(s) 46 | if len(tokens) <= maxTokens { 47 | return s 48 | } 49 | 50 | // Decode the truncated tokens back to a string 51 | truncated, _ := enc.Decode(tokens[:maxTokens]) 52 | return truncated + "..." 53 | } 54 | 55 | func reverseSlice[S ~[]E, E any](s S) { 56 | for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { 57 | s[i], s[j] = s[j], s[i] 58 | } 59 | } 60 | 61 | func mustJSON(v any) string { 62 | b, err := json.Marshal(v) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return string(b) 67 | } 68 | 69 | func findGitRoot(dir string) (string, error) { 70 | dir = filepath.Clean(dir) 71 | for { 72 | _, err := os.Stat(filepath.Join(dir, ".git")) 73 | if err == nil { 74 | return dir, nil 75 | } 76 | if os.IsNotExist(err) { 77 | if dir == "/" { 78 | return "", fmt.Errorf("not a git repository") 79 | } 80 | dir = filepath.Dir(dir) 81 | } else { 82 | return "", fmt.Errorf("failed to stat .git: %w", err) 83 | } 84 | } 85 | } 86 | 87 | const styleGuideFilename = "COMMITS.md" 88 | const defaultUserStyleGuide = ` 89 | 1. Limit the subject line to 50 characters. 90 | 2. Use the imperative mood in the subject line. 91 | 3. Capitalize the subject line such as "Fix Issue 886" and don't end it with a period. 92 | 4. The subject line should summarize the main change concisely. 93 | 5. Only include a body if absolutely necessary for complex changes. 94 | 6. If a body is needed, separate it from the subject with a blank line. 95 | 7. Wrap the body at 72 characters. 96 | 8. In the body, explain the why, not the what (the diff shows the what). 97 | 9. Use bullet points in the body only for truly distinct changes. 98 | 10. Be extremely concise. Assume the reader can understand the diff. 99 | 11. Never repeat information between the subject and body. 100 | 12. Do not repeat commit messages from previous commits. 101 | 13. Prioritize clarity and brevity over completeness. 102 | 14. Adhere to the repository's commit style if it exists. 103 | ` 104 | 105 | // findRepoStyleGuide searches for "COMMITS.md" in the repository root of dir 106 | // and returns its contents. 107 | func findRepoStyleGuide(dir string) (string, error) { 108 | root, err := findGitRoot(dir) 109 | if err != nil { 110 | return "", fmt.Errorf("find git root: %w", err) 111 | } 112 | 113 | styleGuide, err := os.ReadFile(filepath.Join(root, styleGuideFilename)) 114 | if err != nil { 115 | if os.IsNotExist(err) { 116 | return "", nil 117 | } 118 | return "", fmt.Errorf("read style guide: %w", err) 119 | } 120 | return strings.TrimSpace(string(styleGuide)), nil 121 | } 122 | 123 | func findUserStyleGuide() (string, error) { 124 | home, err := os.UserHomeDir() 125 | if err != nil { 126 | return "", fmt.Errorf("find user home dir: %w", err) 127 | } 128 | styleGuide, err := os.ReadFile(filepath.Join(home, styleGuideFilename)) 129 | if err != nil { 130 | if os.IsNotExist(err) { 131 | return "", nil 132 | } 133 | return "", fmt.Errorf("read user style guide: %w", err) 134 | } 135 | return strings.TrimSpace(string(styleGuide)), nil 136 | } 137 | 138 | func BuildPrompt( 139 | log io.Writer, 140 | dir string, 141 | commitHash string, 142 | amend bool, 143 | maxTokens int, 144 | ) ([]openai.ChatCompletionMessage, error) { 145 | resp := []openai.ChatCompletionMessage{ 146 | { 147 | Role: openai.ChatMessageRoleSystem, 148 | Content: strings.Join([]string{ 149 | "You are a tool called `aicommit` that generates high quality commit messages for git diffs.", 150 | "Generate only the commit message, without any additional text.", 151 | }, "\n"), 152 | }, 153 | } 154 | 155 | gitRoot, err := findGitRoot(dir) 156 | if err != nil { 157 | return nil, fmt.Errorf("find git root: %w", err) 158 | } 159 | 160 | repo, err := git.PlainOpen(gitRoot) 161 | if err != nil { 162 | return nil, fmt.Errorf("open repo %q: %w", dir, err) 163 | } 164 | 165 | var buf bytes.Buffer 166 | // Get the working directory diff 167 | if err := generateDiff(&buf, dir, commitHash, amend); err != nil { 168 | return nil, fmt.Errorf("generate working directory diff: %w", err) 169 | } 170 | 171 | if buf.Len() == 0 { 172 | if commitHash == "" { 173 | return nil, fmt.Errorf("no staged changes, nothing to commit") 174 | } 175 | return nil, fmt.Errorf("no changes detected for %q", commitHash) 176 | } 177 | 178 | const minTokens = 5000 179 | if maxTokens < minTokens { 180 | return nil, fmt.Errorf("maxTokens must be greater than %d", minTokens) 181 | } 182 | 183 | targetDiffString := buf.String() 184 | 185 | // Get the HEAD reference 186 | head, err := repo.Head() 187 | if err != nil { 188 | // No commits yet 189 | fmt.Fprintln(log, "no commits yet") 190 | resp = append(resp, openai.ChatCompletionMessage{ 191 | Role: openai.ChatMessageRoleUser, 192 | Content: targetDiffString, 193 | }) 194 | return resp, nil 195 | } 196 | 197 | // Create a log options struct 198 | logOptions := &git.LogOptions{ 199 | From: head.Hash(), 200 | Order: git.LogOrderCommitterTime, 201 | } 202 | 203 | // Get the commit iterator 204 | commitIter, err := repo.Log(logOptions) 205 | if err != nil { 206 | return nil, fmt.Errorf("get commit iterator: %w", err) 207 | } 208 | defer commitIter.Close() 209 | 210 | // Collect the last N commits 211 | var commits []*object.Commit 212 | for i := 0; i < 300; i++ { 213 | commit, err := commitIter.Next() 214 | if err == io.EOF { 215 | break 216 | } 217 | if err != nil { 218 | return nil, fmt.Errorf("iterate commits: %w", err) 219 | } 220 | // Ignore if commit equals ref, because we are trying to recalculate 221 | // that particular commit's message. 222 | if commit.Hash.String() == commitHash { 223 | continue 224 | } 225 | commits = append(commits, commit) 226 | 227 | } 228 | 229 | // We want to reverse the commits so that the most recent commit is the 230 | // last or "most recent" in the chat. 231 | reverseSlice(commits) 232 | 233 | var commitMsgs []string 234 | for _, commit := range commits { 235 | commitMsgs = append(commitMsgs, Ellipse(commit.Message, 1000)) 236 | } 237 | // We provide the commit messages in case the actual commit diffs are cut 238 | // off due to token limits. 239 | resp = append(resp, openai.ChatCompletionMessage{ 240 | Role: openai.ChatMessageRoleSystem, 241 | Content: "Here are recent commit messages in the same repository:\n" + 242 | mustJSON(commitMsgs), 243 | }, 244 | ) 245 | 246 | // Add style guide after commit messages so it takes priority. 247 | repoStyleGuide, err := findRepoStyleGuide(dir) 248 | if err != nil { 249 | return nil, fmt.Errorf("find style guide: %w", err) 250 | } 251 | if repoStyleGuide != "" { 252 | resp = append(resp, openai.ChatCompletionMessage{ 253 | Role: openai.ChatMessageRoleSystem, 254 | Content: "This repository has a style guide. Follow it even when " + 255 | "it diverges from the norm.\n" + repoStyleGuide, 256 | }) 257 | } else { 258 | userStyleGuide, err := findUserStyleGuide() 259 | if err != nil { 260 | return nil, fmt.Errorf("find user style guide: %w", err) 261 | } 262 | if userStyleGuide == "" { 263 | userStyleGuide = defaultUserStyleGuide 264 | } 265 | resp = append(resp, openai.ChatCompletionMessage{ 266 | Role: openai.ChatMessageRoleSystem, 267 | Content: "This user has a preferred style guide:\n" + userStyleGuide, 268 | }) 269 | } 270 | 271 | resp = append(resp, openai.ChatCompletionMessage{ 272 | Role: openai.ChatMessageRoleUser, 273 | Content: Ellipse(targetDiffString, maxTokens-CountTokens(resp...)), 274 | }) 275 | 276 | return resp, nil 277 | } 278 | 279 | // generateDiff uses the git CLI to generate a diff for the given reference. 280 | // If refName is empty, it will generate a diff of staged changes for the working directory. 281 | func generateDiff(w io.Writer, dir string, refName string, amend bool) error { 282 | // Use the git CLI instead of go-git for more accurate and complete diff generation 283 | cmd := exec.Command("git", "-C", dir, "diff") 284 | 285 | if refName == "" { 286 | // Case 1: No specific commit reference provided 287 | // Generate diff for staged changes in the working directory 288 | cmd.Args = append(cmd.Args, "--cached") 289 | } else { 290 | // Case 2: A specific commit reference is provided 291 | if amend { 292 | // Case 2a: Amending the specified commit 293 | // Show diff of the commit being amended plus any staged changes 294 | cmd.Args = append(cmd.Args, "--cached", refName+"^") 295 | } else { 296 | // Case 2b: Show changes introduced by the specific commit 297 | cmd.Args = append(cmd.Args, refName+"^", refName) 298 | } 299 | } 300 | 301 | var errBuf bytes.Buffer 302 | cmd.Stdout = w 303 | cmd.Stderr = &errBuf 304 | 305 | // Run the git command and return any execution errors 306 | err := cmd.Run() 307 | if err != nil { 308 | return fmt.Errorf("running %s %s: %w\n%s", 309 | cmd.Args[0], strings.Join(cmd.Args[1:], " "), err, errBuf.String()) 310 | } 311 | 312 | return nil 313 | } 314 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | al.essio.dev/pkg/shellescape v1.5.0 h1:7oTvSsQ5kg9WksA9O58y9wjYnY4jP0CL82/Q8WLUGKk= 2 | al.essio.dev/pkg/shellescape v1.5.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= 3 | cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 h1:KHblWIE/KHOwQ6lEbMZt6YpcGve2FEZ1sDtrW1Am5UI= 4 | cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= 5 | cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= 6 | cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= 7 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= 8 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 9 | cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= 10 | cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= 11 | cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= 12 | cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= 13 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 14 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 15 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 16 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 17 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 18 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 19 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 20 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 21 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 22 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 23 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 24 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 25 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 26 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 27 | github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= 28 | github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= 29 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 30 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 31 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 32 | github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= 33 | github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= 34 | github.com/coder/serpent v0.8.0 h1:6OR+k6fekhSeEDmwwzBgnSjaa7FfGGrMlc3GoAEH9dg= 35 | github.com/coder/serpent v0.8.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= 36 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 37 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 38 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 41 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= 43 | github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 44 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 45 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 46 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 47 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 48 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 49 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 50 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 51 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 52 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 53 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 54 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 55 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 56 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 57 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 58 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 59 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 60 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 61 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 62 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 63 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 64 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 65 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 66 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 67 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 68 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 69 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 70 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 71 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 72 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 73 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 74 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 75 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 76 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 77 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 78 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 79 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 80 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 81 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 82 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 83 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 84 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 85 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 86 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 87 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 88 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 89 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 90 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 91 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 92 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 93 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 94 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 95 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 96 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 97 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 98 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 99 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 100 | github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= 101 | github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= 102 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 103 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 104 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 105 | github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= 106 | github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= 107 | github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= 108 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 109 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 110 | github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= 111 | github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= 112 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 113 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 114 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 115 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 116 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 117 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 118 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 123 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 124 | github.com/sashabaranov/go-openai v1.29.0 h1:eBH6LSjtX4md5ImDCX8hNhHQvaRf22zujiERoQpsvLo= 125 | github.com/sashabaranov/go-openai v1.29.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 126 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 127 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 128 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 129 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 130 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 131 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 132 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 135 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 136 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 137 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 138 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 139 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 140 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 141 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 142 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 143 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 144 | github.com/tiktoken-go/tokenizer v0.1.1 h1:C0Y2gshVqVFvXlVXWAqCtzUJ3StcuxwHQ0zx26tL7mA= 145 | github.com/tiktoken-go/tokenizer v0.1.1/go.mod h1:7SZW3pZUKWLJRilTvWCa86TOVIiiJhYj3FQ5V3alWcg= 146 | github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 147 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 148 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 149 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 150 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 151 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 152 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 153 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 154 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 155 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= 156 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 157 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 158 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 159 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 160 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 161 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 162 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 163 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 164 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 165 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 166 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= 167 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 168 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 169 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 170 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 171 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 174 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 175 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 176 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 177 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 178 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 179 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 180 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 181 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 182 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 183 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 184 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 185 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 186 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 187 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 188 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 189 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 190 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 198 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 199 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 200 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 201 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 202 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 203 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 204 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 205 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 206 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 207 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 208 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 209 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 210 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 211 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 212 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 213 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 214 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 215 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 216 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 217 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 218 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 219 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 220 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 221 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 222 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 223 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 224 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 225 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 226 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 227 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 228 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 229 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 230 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 231 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 232 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 233 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 234 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 235 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 236 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 237 | google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= 238 | google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= 239 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= 240 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= 241 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= 242 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= 243 | google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= 244 | google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= 245 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 246 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 247 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 248 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 249 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 250 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 251 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 252 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 253 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 254 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 255 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 256 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 257 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 258 | --------------------------------------------------------------------------------