ai-commit-message 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef95bc52d9e0600b53c10e093a2d3c057e61f8746d59495f16e0f29c68699995
4
- data.tar.gz: 531a062526261dd27b1927660b817509aaa0fd79490f82ca3d16883aeab4c736
3
+ metadata.gz: cf7ae3da6684c33dd9b0518bda5c3d0f85398132e1089a51a442bf88892e4b7d
4
+ data.tar.gz: d3d27482ca167d55bffb77c25ebd0553eea74fc93fdf5554bd6a434e80926aeb
5
5
  SHA512:
6
- metadata.gz: e8306cabe049db41c31b8cf9568c8cb52771041e5d9c26671984de3bd9c3b16957a6de1178a5aebd6af35990faa0b634e7f888c61cbcc95aa3a4d49d6342c332
7
- data.tar.gz: 6348572010d2362c88ec3e7a5bd53fa94233663b0887ab3f9201dc20803aabe11e434c9fb7ebbfa1c59ecc3afb195e18f6f8e6fbb9c035795b2675edd82c357e
6
+ metadata.gz: 07d58b95afcdde4e24d7f85e15354d4d437cecbb92ddfc16b68e8a8d13bf2a10a576674b43000e163b0465021947cc6c5a8210894a5583bdb00e278e7600e641
7
+ data.tar.gz: b22d685bb2687103cb3b414f6fc0b002dd92007b377b76ee9a89a7d99e50a052184d641277ac636d46e56780cc8265282350cd40dbb5e3515ba8418a0f8aaca1
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Afonso Neto
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,2 +1,145 @@
1
- # ai-commit-message
2
- Git commit message suggested by AI
1
+ # AI git commit message
2
+
3
+ A Ruby gem that automatically generates concise and meaningful git commit messages using a local LLM via [Ollama](https://github.com/ollama/ollama), LM Studio, llama.cpp `llama-server`, or any other OpenAI-compatible API.
4
+
5
+ ![Terminal demo](docs/demo.gif)
6
+
7
+ ## Installation
8
+
9
+ Install the gem using:
10
+
11
+ ```bash
12
+ gem install ai-commit-message
13
+ ```
14
+
15
+ Or add it to your Gemfile:
16
+
17
+ ```ruby
18
+ gem 'ai-commit-message'
19
+ ```
20
+
21
+ And then run:
22
+
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ ## Requirements
28
+
29
+ - Ruby 3.0 or newer
30
+ - Git repository
31
+ - A local LLM server: [Ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [llama.cpp `llama-server`](https://github.com/ggml-org/llama.cpp), or any other OpenAI-compatible endpoint
32
+
33
+ ## Usage
34
+
35
+ ### Basic Usage
36
+
37
+ Generate a commit message for your staged changes and commit with it after a confirmation:
38
+
39
+ ```bash
40
+ ai-commit-message commit
41
+ ```
42
+
43
+ This will:
44
+ 1. Analyze your staged changes (`git diff --cached`)
45
+ 2. Review your recent commit history for style consistency
46
+ 3. Consider your current branch name
47
+ 4. Generate an appropriate commit message
48
+ 5. Ask for confirmation and create the commit (answering "no" keeps the printed message so you can edit or copy it)
49
+
50
+ ### Message only
51
+
52
+ If you just want the suggested message without the confirmation prompt — e.g. to pipe it somewhere or review it yourself — use `--message-only`:
53
+
54
+ ```bash
55
+ ai-commit-message commit --message-only
56
+ ```
57
+
58
+ ### Conventional Commits
59
+
60
+ Ask the model to follow the [Conventional Commits](https://www.conventionalcommits.org/) format (`feat:`, `fix:`, `chore:`, ...):
61
+
62
+ ```bash
63
+ ai-commit-message commit --conventional
64
+ ```
65
+
66
+ ### Configuration
67
+
68
+ Configure the API URL and model interactively:
69
+
70
+ ```bash
71
+ ai-commit-message config
72
+ ```
73
+
74
+ If your server is reachable, the model prompt becomes a picker populated with the models available on that server.
75
+
76
+ List the models available on your server:
77
+
78
+ ```bash
79
+ ai-commit-message models
80
+ ```
81
+
82
+ ### Command Line Options
83
+
84
+ Override configuration settings directly:
85
+
86
+ ```bash
87
+ ai-commit-message commit --url=http://your-api-endpoint --model=your-model-name
88
+ ```
89
+
90
+ All options:
91
+
92
+ | Option | Description | Default |
93
+ | --- | --- | --- |
94
+ | `--url` | Base URL of the OpenAI-compatible API | `http://localhost:11434` |
95
+ | `--model` | Model name | `qwen3:8b` |
96
+ | `--length` | Maximum message length in characters | `72` |
97
+ | `--conventional` | Use Conventional Commits format | off |
98
+ | `--message-only` | Print the message without the commit confirmation prompt | off |
99
+
100
+ ## How It Works
101
+
102
+ The gem:
103
+ 1. Collects your staged changes using `git diff --cached` (capped at 12,000 characters so large diffs don't overwhelm a local model's context)
104
+ 2. Retrieves your recent commit history for context
105
+ 3. Identifies your current branch name
106
+ 4. Sends a chat request to `{url}/v1/chat/completions` — the standard OpenAI-compatible endpoint
107
+ 5. Cleans the model output (strips code fences, quotes and reasoning blocks) and prints a single-line commit message (limited to 72 characters by default)
108
+
109
+ The URL accepts base addresses with or without a trailing `/v1` and `/`, so `http://localhost:11434` (Ollama), `http://localhost:1234/v1` (LM Studio) and `http://localhost:8080` (llama.cpp `llama-server`) all work as-is.
110
+
111
+ ## Configuration File
112
+
113
+ The gem stores your configuration in `~/.ai-commit-message.conf`. You can manually edit this file if needed. Example:
114
+
115
+ ```bash
116
+ url=http://localhost:11434
117
+ model=qwen3:8b
118
+ ```
119
+
120
+ ## Models
121
+
122
+ The default configuration uses Ollama with the `qwen3:8b` model, but you can use any model available through your API endpoint.
123
+
124
+ ### Recommended Models
125
+
126
+ - **qwen3:8b**: Good balance of quality and speed (default). Note that it is a thinking model, so expect a few seconds of reasoning before the message appears
127
+ - **qwen2.5-coder:7b**: Faster, non-thinking coding model
128
+ - **qwen3.5:4b**: Current-generation small model for modest hardware
129
+ - **gemma4:e4b**: Compact general-purpose option
130
+ - **llama3.1:8b**: Solid general-purpose choice
131
+
132
+ ## Contributing
133
+
134
+ 1. Fork the repository
135
+ 2. Create your feature branch: `git checkout -b my-new-feature`
136
+ 3. Install dependencies: `bundle install`
137
+ 4. Run the tests: `bundle exec rake test`
138
+ 5. Make your changes and add tests if applicable
139
+ 6. Commit your changes: `git commit -m 'Add some feature'`
140
+ 7. Push to the branch: `git push origin my-new-feature`
141
+ 8. Submit a pull request
142
+
143
+ ## License
144
+
145
+ This gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -5,36 +5,95 @@ require_relative 'config_manager'
5
5
 
6
6
  module AiCommitMessage
7
7
  class CLI < Thor
8
- desc "commit", "Generate git commit message"
9
- method_option :url, type: :string, default: 'http://localhost:11434/api/generate'
10
- method_option :model, type: :string, default: 'qwen2.5-coder:7b'
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ DEFAULT_URL = 'http://localhost:11434'
13
+ DEFAULT_MODEL_NAME = 'qwen3:8b'
14
+ MAX_DIFF_LENGTH = 12_000
11
15
 
12
- def commit(options = {})
16
+ desc "commit", "Generate a commit message and commit with it after confirmation"
17
+ method_option :url, type: :string
18
+ method_option :model, type: :string
19
+ method_option :length, type: :numeric, default: Suggester::DEFAULT_LENGTH
20
+ method_option :conventional, type: :boolean, default: false
21
+ method_option :message_only, type: :boolean, default: false
22
+
23
+ def commit
13
24
  git_diff_output = `git diff --cached --no-color`
25
+ if git_diff_output.strip.empty?
26
+ warn 'Nothing is staged. Stage your changes with `git add` first.'
27
+ exit 1
28
+ end
29
+
14
30
  git_log_output = `git log --format=%s -n 30`
15
31
  git_current_branch = `git branch --show-current`
16
32
 
17
- url = options[:url] || ConfigManager.get_url || 'http://localhost:11434/api/generate'
18
- model = options[:model] || ConfigManager.get_model || 'qwen2.5-coder:7b'
19
-
20
- suggester = AiCommitMessage::Suggester.new(git_diff_output, git_log_output, git_current_branch)
21
- commit_message = suggester.generate_commit_message(url:, model:)
33
+ suggester = AiCommitMessage::Suggester.new(git_diff_output[0, MAX_DIFF_LENGTH], git_log_output, git_current_branch)
34
+ commit_message = suggester.generate_commit_message(
35
+ url: url_to_be_used(options[:url]),
36
+ model: model_to_be_used(options[:model]),
37
+ length: options[:length],
38
+ conventional: options[:conventional]
39
+ )
22
40
 
23
41
  puts commit_message
42
+ return if options[:message_only]
43
+
44
+ prompt = TTY::Prompt.new
45
+ begin
46
+ # tty-prompt takes the default on EOF, which would silently commit
47
+ # in non-interactive runs; treat a closed stdin as "no".
48
+ confirmed = !$stdin.eof? && prompt.yes?("Commit with this message?", default: true)
49
+ rescue TTY::Reader::InputInterrupt
50
+ confirmed = false
51
+ end
52
+ return unless confirmed
53
+
54
+ system('git', 'commit', '-m', commit_message)
55
+ rescue AiCommitMessage::Suggester::Error => e
56
+ warn e.message
57
+ exit 1
58
+ end
59
+
60
+ desc "models", "List models available on the configured API URL"
61
+ method_option :url, type: :string
62
+
63
+ def models
64
+ puts AiCommitMessage::Suggester.list_models(url_to_be_used(options[:url]))
65
+ rescue AiCommitMessage::Suggester::Error => e
66
+ warn e.message
67
+ exit 1
24
68
  end
25
69
 
26
70
  desc "config", "Set global configs. API URL and Model name"
27
71
  def config
28
72
  prompt = TTY::Prompt.new
29
73
 
30
- url = prompt.ask("API URL:", default: ConfigManager.get_url || 'http://localhost:11434/api/generate')
31
- model = prompt.ask("Model name:", default: ConfigManager.get_model || 'qwen2.5-coder:7b')
74
+ url = prompt.ask("API URL:", default: ConfigManager.get_url || DEFAULT_URL)
75
+ available_models = AiCommitMessage::Suggester.list_models(url) rescue nil
76
+ model = if available_models && !available_models.empty?
77
+ prompt.select("Model name:", available_models)
78
+ else
79
+ prompt.ask("Model name:", default: ConfigManager.get_model || DEFAULT_MODEL_NAME)
80
+ end
32
81
 
33
82
  ConfigManager.set_url(url)
34
83
  ConfigManager.set_model(model)
35
84
 
36
85
  puts "Configuration updated successfully!"
37
86
  end
87
+
88
+ private
89
+
90
+ def url_to_be_used(options_url)
91
+ options_url || ConfigManager.get_url || DEFAULT_URL
92
+ end
93
+
94
+ def model_to_be_used(options_model)
95
+ options_model || ConfigManager.get_model || DEFAULT_MODEL_NAME
96
+ end
38
97
  end
39
98
  end
40
99
 
@@ -3,17 +3,17 @@ require 'fileutils'
3
3
  class ConfigManager
4
4
  CONFIG_FILE = File.expand_path('~/.ai-commit-message.conf')
5
5
 
6
- def self.get_url = config['url']
6
+ def self.get_url = config[:url]
7
7
 
8
8
  def self.set_url(url)
9
- config['url'] = url
9
+ config[:url] = url
10
10
  save_config
11
11
  end
12
12
 
13
- def self.get_model = config['model']
13
+ def self.get_model = config[:model]
14
14
 
15
15
  def self.set_model(model)
16
- config['model'] = model
16
+ config[:model] = model
17
17
  save_config
18
18
  end
19
19
 
@@ -34,5 +34,8 @@ class ConfigManager
34
34
  @config
35
35
  end
36
36
 
37
- def self.save_config = File.write(CONFIG_FILE, config.map { |key, value| "#{key}=#{value}" }.join("\n"))
37
+ def self.save_config
38
+ File.write(CONFIG_FILE, config.map { |key, value| "#{key}=#{value}" }.join("\n"))
39
+ FileUtils.chmod(0600, CONFIG_FILE)
40
+ end
38
41
  end
@@ -1,41 +1,144 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
  require 'json'
4
+ require 'openssl'
4
5
 
5
6
  module AiCommitMessage
6
7
  class Suggester
8
+ class Error < StandardError; end
9
+ class ConnectionError < Error; end
10
+ class ApiError < Error; end
11
+
12
+ OPEN_TIMEOUT = 5
13
+ READ_TIMEOUT = 120
14
+ DEFAULT_LENGTH = 72
15
+ # Thinking models (qwen3 and friends) spend tokens reasoning before the
16
+ # answer; the budget must leave room for both or content comes back empty.
17
+ MAX_TOKENS = 2048
18
+
19
+ def self.list_models(url)
20
+ uri = URI("#{normalized_base(url)}/v1/models")
21
+ http = build_http(uri)
22
+
23
+ begin
24
+ response = http.request(Net::HTTP::Get.new(uri))
25
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
26
+ raise ConnectionError,
27
+ "Could not connect to #{uri.host}:#{uri.port}. Is your local LLM server running?"
28
+ end
29
+
30
+ parse_response(response).fetch('data', []).map { |m| m['id'] }.compact.sort
31
+ end
32
+
33
+ def self.parse_response(response)
34
+ unless response.is_a?(Net::HTTPSuccess)
35
+ raise ApiError, "API returned HTTP #{response.code}: #{response.body.to_s[0, 200]}"
36
+ end
37
+
38
+ JSON.parse(response.body)
39
+ rescue JSON::ParserError
40
+ raise ApiError, 'API response was not valid JSON'
41
+ end
42
+
43
+ def self.build_http(uri)
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+ http.use_ssl = uri.scheme == 'https'
46
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
47
+ http.open_timeout = OPEN_TIMEOUT
48
+ http.read_timeout = READ_TIMEOUT
49
+ http
50
+ end
51
+
52
+ def self.normalized_base(url)
53
+ url.chomp('/').sub(%r{/v1$}, '')
54
+ end
55
+
7
56
  def initialize(git_diff_output, git_log_output, git_current_branch)
8
57
  @git_diff_output = git_diff_output
9
58
  @git_log_output = git_log_output
10
59
  @git_current_branch = git_current_branch
11
60
  end
12
61
 
13
- def generate_commit_message(url:, model:)
14
- url = URI(url)
15
- body = {
62
+ def generate_commit_message(url:, model:, length: DEFAULT_LENGTH, conventional: false)
63
+ response = post_json(chat_completions_uri(url), request_body(model, length, conventional))
64
+ json = self.class.parse_response(response)
65
+ message = clean_commit_message(json.dig('choices', 0, 'message', 'content'), length)
66
+ return message unless message.empty?
67
+
68
+ raise ApiError,
69
+ 'Model returned no commit message content. Try a non-thinking model or a different model.'
70
+ end
71
+
72
+ private
73
+
74
+ def request_body(model, length, conventional)
75
+ {
16
76
  model: model,
17
- prompt: "Create a concise git commit message with no more than 250 characters. Exclude anything unnecessary such as translation, backticks characters or multiple suggestions, since your entire response will be passed directly into git commit. Consider the following messages as example to follow: #{@git_log_output}.
18
- The First [#XXX] option is the branch name. The current branch name is: #{@git_current_branch}. Now Do it for the following git diff: #{@git_diff_output}",
19
- stream: false,
20
- options: { temperature: 0.8 }
77
+ messages: [
78
+ { role: 'system', content: system_prompt(conventional) },
79
+ { role: 'user', content: user_prompt(length) }
80
+ ],
81
+ temperature: 0.3,
82
+ max_tokens: MAX_TOKENS,
83
+ stream: false
21
84
  }
22
- json_body = body.to_json
23
- headers = { 'Content-Type' => 'application/json' }
85
+ end
86
+
87
+ def system_prompt(conventional)
88
+ prompt = 'You write git commit messages. Respond with a single line containing ' \
89
+ 'only the commit message: no quotes, no backticks, no markdown, no explanations.'
90
+ prompt += ' Use the Conventional Commits format (type: description).' if conventional
91
+ prompt
92
+ end
24
93
 
25
- http = Net::HTTP.new(url.host, url.port)
26
- request = Net::HTTP::Post.new(url)
27
- request.body = json_body
28
- headers.each { |key, value| request[key] = value }
29
- response = http.request(request)
94
+ def user_prompt(length)
95
+ <<~PROMPT
96
+ Write a concise git commit message with no more than #{length} characters for the staged changes below.
97
+ Follow the style of these recent commit messages: #{@git_log_output}
98
+ #{branch_line}
99
+ Git diff:
100
+ #{@git_diff_output}
101
+ PROMPT
102
+ end
103
+
104
+ # git branch --show-current returns nothing on a detached HEAD
105
+ def branch_line
106
+ @git_current_branch.strip.empty? ? '' : "The current branch name is: #{@git_current_branch.strip}"
107
+ end
108
+ # Models tend to wrap answers in code fences or quotes, inline their
109
+ # reasoning in <think> blocks, or offer alternatives; reduce everything to
110
+ # a single clean subject line. Control characters are stripped so that a
111
+ # prompt-injected model cannot smuggle terminal escapes into git history.
112
+ def clean_commit_message(raw, length)
113
+ line = raw.to_s
114
+ .gsub(%r{<think>.*?</think>}m, '')
115
+ .gsub(/\e\][^\x07\e]*(?:\x07|\e\\)/, '')
116
+ .gsub(/\e\[[0-9;:]*[A-Za-z]/, '')
117
+ .gsub(/[\e\a\b\v\f\r]/, '')
118
+ .gsub('`', '')
119
+ .each_line.map(&:strip).reject(&:empty?).first.to_s
120
+ line = line.sub(/\A["'](.*)["']\z/, '\1')
121
+ line.gsub(/\s+/, ' ')[0, length].to_s.strip
122
+ end
123
+
124
+ # Accepts base URLs with or without a trailing /v1 and with or without a
125
+ # trailing slash, e.g. http://localhost:11434, http://localhost:1234/v1/
126
+ def chat_completions_uri(url)
127
+ URI("#{self.class.normalized_base(url)}/v1/chat/completions")
128
+ end
129
+
130
+ def post_json(uri, body)
131
+ http = self.class.build_http(uri)
132
+ request = Net::HTTP::Post.new(uri)
133
+ request['Content-Type'] = 'application/json'
134
+ request.body = body.to_json
30
135
 
31
136
  begin
32
- json = JSON.parse(response.body)
33
- return json['response'] if json
34
- rescue JSON::ParserError => e
35
- puts "Failed to parse API response as JSON: #{e.message}"
137
+ http.request(request)
138
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
139
+ raise ConnectionError,
140
+ "Could not connect to #{uri.host}:#{uri.port}. Is your local LLM server running?"
36
141
  end
37
-
38
- nil
39
142
  end
40
143
  end
41
- end
144
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai-commit-message
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Afonso Neto
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-03-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: thor
@@ -39,13 +38,13 @@ dependencies:
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
40
  - !ruby/object:Gem::Dependency
42
- name: net-http
41
+ name: rake
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
44
  - - ">="
46
45
  - !ruby/object:Gem::Version
47
46
  version: '0'
48
- type: :runtime
47
+ type: :development
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
@@ -53,20 +52,21 @@ dependencies:
53
52
  - !ruby/object:Gem::Version
54
53
  version: '0'
55
54
  - !ruby/object:Gem::Dependency
56
- name: json
55
+ name: minitest
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
58
  - - ">="
60
59
  - !ruby/object:Gem::Version
61
60
  version: '0'
62
- type: :runtime
61
+ type: :development
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: '0'
69
- description: Git commit messages suggested by AI
68
+ description: Git commit messages suggested by AI using a local LLM via any OpenAI-compatible
69
+ API
70
70
  email:
71
71
  - afonso.pontesneto@gmail.com
72
72
  executables:
@@ -74,6 +74,7 @@ executables:
74
74
  extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
+ - LICENSE
77
78
  - README.md
78
79
  - bin/ai-commit-message
79
80
  - lib/ai-commit-message/cli.rb
@@ -81,9 +82,9 @@ files:
81
82
  - lib/ai-commit-message/suggester.rb
82
83
  homepage: https://github.com/AfonsoNeto/ai-commit-message
83
84
  licenses:
84
- -
85
- metadata: {}
86
- post_install_message:
85
+ - MIT
86
+ metadata:
87
+ rubygems_mfa_required: 'true'
87
88
  rdoc_options: []
88
89
  require_paths:
89
90
  - lib
@@ -98,8 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
99
  - !ruby/object:Gem::Version
99
100
  version: '0'
100
101
  requirements: []
101
- rubygems_version: 3.5.11
102
- signing_key:
102
+ rubygems_version: 4.0.12
103
103
  specification_version: 4
104
104
  summary: Git commit messages suggested by AI
105
105
  test_files: []