ai_git 0.0.4 → 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: a43b04eaad5bed61c5dbe62df46fb915e83d0481e274c2f440b3be2bd7966660
4
- data.tar.gz: c74c24c20d94f28a58938ec1f8d0a0d323c15a0522fd6d3596b7f8e51323182d
3
+ metadata.gz: bc3e320bc55f08d7665a8a6ae9123b6b032a4b4a3861cf439b5ff018f14b415a
4
+ data.tar.gz: 055a7bd2bc06b4d0e18aacb318a6aa6725e1632f0bb1b5c4c3ad2ca5930013ab
5
5
  SHA512:
6
- metadata.gz: 03cbbe8cf09690cc3db34f98bf42e39479672ce464d65de18a6a25d901ea9c8d35c318a0efa4eac06abc3d1f07d1848be24506ca4c6146facff7e9dcb18a1e80
7
- data.tar.gz: 47a3c30cbb3ed60d128db9d68af240b5404096ffc95643766cb873296f8e3f2ccacc5cbc284be4fdc6d984dd1c310ea2ff01873c5442abb86745e1d34be59c19
6
+ metadata.gz: d90a349b399361d0c52fcaec15fec584bb4251197a8a282a7632b5e1078ec571753f8085c6edddf51edffd0f3d6c10ef24b7922afdd6c69df700b564f1f446e1
7
+ data.tar.gz: 603b003ec1cefdae26db68cc026ac1d2950ebd00421e9e1c0fe56c663dc8ae202458a8334bfeef1cd6f1e8cfc9a1d29893578e3e1c34c41b239c8245a11dc744
data/bin/ai_git CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
4
  require_relative "../lib/ai_git"
5
5
 
6
- AIGit.run
6
+ AIGit.start(ARGV)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIGit
4
+ module Config
5
+ PROVIDERS = {
6
+ "jan" => {
7
+ default_model: "Jan-v3.5-4B-Q4_K_XL",
8
+ base_url: "http://127.0.0.1:1337",
9
+ endpoint: "/v1/chat/completions",
10
+ request_format: :openai
11
+ },
12
+ "ollama" => {
13
+ default_model: "gemma4:e4b",
14
+ base_url: "http://localhost:11434",
15
+ endpoint: "/api/generate",
16
+ request_format: :ollama
17
+ }
18
+ }.freeze
19
+
20
+ def self.provider
21
+ ENV["AI_GIT_AI_PROVIDER"] || "jan"
22
+ end
23
+
24
+ def self.model_name
25
+ ENV["AI_GIT_MODEL_NAME"] || PROVIDERS[provider][:default_model]
26
+ end
27
+
28
+ def self.base_url
29
+ ENV["AI_GIT_BASE_URL"] || PROVIDERS[provider][:base_url]
30
+ end
31
+
32
+ def self.endpoint
33
+ PROVIDERS[provider][:endpoint]
34
+ end
35
+
36
+ def self.request_format
37
+ PROVIDERS[provider][:request_format]
38
+ end
39
+
40
+ def self.config
41
+ PROVIDERS[provider]
42
+ end
43
+ end
44
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "benchmark"
3
4
  require "net/http"
4
5
  require "uri"
5
6
  require "json"
7
+ require_relative "version"
8
+ require_relative "git"
6
9
 
7
10
  module AIGit
8
- module Ollama
11
+ module Default
9
12
  module_function
10
13
 
11
14
  def escape_json(string)
@@ -75,49 +78,100 @@ module AIGit
75
78
  Now generate the commit message:
76
79
  PROMPT
77
80
 
78
- json_body = {
79
- model: model_name,
80
- prompt: prompt,
81
- stream: false,
82
- # These parameters help a lot with strictness:
83
- temperature: 0.3,
84
- top_p: 0.9,
85
- stop: ["\n\n\n", "```", "Here is", "The commit message"],
86
- num_predict: 400 # Limit output length
87
- }.to_json
88
-
89
- uri = URI("http://localhost:11434/api/generate")
90
- request = Net::HTTP::Post.new(uri)
91
- request["Content-Type"] = "application/json"
92
- request.body = json_body
93
-
94
- response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
95
- http.request(request)
81
+ if AIGit::Config.request_format == :ollama
82
+ json_body = {
83
+ model: model_name,
84
+ prompt: prompt,
85
+ stream: false,
86
+ temperature: 0.3,
87
+ top_p: 0.9,
88
+ stop: ["\n\n\n", "```", "Here is", "The commit message"],
89
+ num_predict: 400
90
+ }.to_json
91
+
92
+ uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
93
+ request = Net::HTTP::Post.new(uri)
94
+ request["Content-Type"] = "application/json"
95
+ request.body = json_body
96
+
97
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
98
+ http.request(request)
99
+ end
100
+
101
+ raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
102
+
103
+ data = JSON.parse(response.body)
104
+ message = data["response"].to_s.strip
105
+ else # Jan AI
106
+ json_body = {
107
+ model: model_name,
108
+ messages: [{ role: "user", content: prompt }],
109
+ stream: false,
110
+ temperature: 0.3
111
+ }.to_json
112
+
113
+ uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
114
+ request = Net::HTTP::Post.new(uri)
115
+ request["Content-Type"] = "application/json"
116
+ request.body = json_body
117
+
118
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
119
+ http.request(request)
120
+ end
121
+
122
+ raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
123
+
124
+ data = JSON.parse(response.body)
125
+ message = data["choices"][0]["message"]["content"].to_s.strip
96
126
  end
97
127
 
98
- raise "Failed to connect to Ollama. Is it running?" unless response.is_a?(Net::HTTPSuccess)
99
-
100
- data = JSON.parse(response.body)
101
- message = data["response"].to_s.strip
102
-
103
- # Aggressive cleaning
104
128
  message = message.gsub(/^(Here is|The commit message is|```|json|markdown)/i, "")
105
129
  .gsub(/^>\s*/, "")
106
130
  .gsub(/\\n/, "\n")
107
131
  .strip
108
132
 
109
- # Remove common unwanted prefixes/suffixes
110
133
  lines = message.lines.map(&:strip)
111
134
  lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
112
135
 
113
136
  message = lines.join("\n").strip
114
137
 
115
- # Ensure it has at least a title
116
- if message.lines.count < 1 || message.strip.empty?
117
- message = "chore: update code" # fallback
118
- end
138
+ message = "chore: update code" if message.lines.count < 1 || message.strip.empty?
119
139
 
120
140
  message
121
141
  end
142
+
143
+ def call
144
+ provider = AIGit::Config.provider
145
+ model_name = AIGit::Config.model_name
146
+
147
+ staged = AIGit::Git.staged_files
148
+ abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
149
+
150
+ diff = AIGit::Git.diff
151
+ branch = AIGit::Git.current_branch
152
+
153
+ puts "\e[1mAI Provider:\e[0m #{provider}"
154
+ puts "\e[1mModel Name:\e[0m #{model_name}"
155
+ puts "\e[1mStaged Files:\e[0m #{staged}"
156
+ puts "\e[1mBranch:\e[0m #{branch}"
157
+ puts "\e[1mAI Generating Commit Message\e[0m"
158
+
159
+ result = Benchmark.measure do
160
+ message = generate_commit_message(diff, model_name)
161
+ message = message.gsub(/\n{2,}/, "\n")
162
+
163
+ puts "\e[1mCommit Message:\e[0m\n\n#{message}\n"
164
+
165
+ escaped_msg = message.gsub(/[\\"`$]/) { |c| "\\#{c}" }
166
+ AIGit::Git.run_command("git", "commit -m \"#{escaped_msg}\"")
167
+ puts "\e[1mGit Commited\e[0m"
168
+
169
+ AIGit::Git.run_command("git", "push -u origin HEAD")
170
+ puts "\e[1mGit Pushed\e[0m"
171
+ end
172
+
173
+ puts "\e[1mBenchmark\e[0m"
174
+ puts result
175
+ end
122
176
  end
123
177
  end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+ require "net/http"
5
+ require "uri"
6
+ require "json"
7
+ require_relative "version"
8
+ require_relative "git"
9
+
10
+ module AIGit
11
+ module Review
12
+ module_function
13
+
14
+ def generate_review(diff, model_name)
15
+ raise "No staged changes to review" if diff.to_s.strip.empty?
16
+
17
+ prompt = <<~PROMPT
18
+ You are a senior software engineer conducting a thorough code review. Output ONLY the review — no explanations, no markdown preamble, no backticks, no preamble.
19
+
20
+ Here are the staged changes:
21
+ #{diff}
22
+
23
+ STRICT OUTPUT FORMAT (follow exactly):
24
+
25
+ ## Summary
26
+ <2–5 bullet points describing what this change does at a high level. Each bullet starts with a verb.>
27
+
28
+ ## Suggestions
29
+ <For each issue or improvement, use this format:>
30
+
31
+ **<File and location, e.g. "app/models/user.rb">**
32
+ - <Concise description of the issue or suggestion. Start with a verb. Be specific.>
33
+ - <Another suggestion for the same file, if applicable.>
34
+
35
+ <Repeat the file block for each file with suggestions. Omit files that look clean.>
36
+
37
+ ## Verdict
38
+ <One of: ✅ Looks good | ⚠️ Minor issues | 🚨 Needs attention>
39
+ <One sentence explaining the verdict.>
40
+
41
+ RULES:
42
+ - Summary: describe WHAT the change does, not HOW the code looks. Focus on behaviour and intent.
43
+ - Suggestions: flag real issues — bugs, edge cases, security problems, missing error handling, naming confusion, performance concerns, missing tests. Do NOT invent problems. If a file is clean, omit it entirely.
44
+ - Be direct and constructive. No filler like "great job" or "consider possibly maybe".
45
+ - No line should exceed 72 characters.
46
+ - If the diff is a minor or trivial change (e.g. version bump, typo fix), keep the review brief.
47
+
48
+ EXAMPLES OF GOOD OUTPUT:
49
+
50
+ ## Summary
51
+ - Add password reset flow with tokenised email verification
52
+ - Introduce PasswordResetMailer with expiry-aware token links
53
+ - Add DB migration for reset_token and reset_token_expires_at columns
54
+
55
+ ## Suggestions
56
+
57
+ **app/models/user.rb**
58
+ - Ensure reset_token is invalidated after successful use to
59
+ prevent token reuse attacks.
60
+ - Add an index on reset_token column for fast lookup queries.
61
+
62
+ **app/controllers/passwords_controller.rb**
63
+ - Handle the case where the token has expired with a user-facing
64
+ error message rather than a silent redirect.
65
+
66
+ ## Verdict
67
+ ⚠️ Minor issues
68
+ Core logic is solid but token invalidation and expiry feedback
69
+ need addressing before merge.
70
+
71
+ ---
72
+
73
+ ## Summary
74
+ - Fix nil guard in ReportGenerator when preferences are absent
75
+
76
+ ## Suggestions
77
+
78
+ ## Verdict
79
+ ✅ Looks good
80
+ Safe defensive fix with no side effects.
81
+
82
+ ---
83
+
84
+ Now review the staged changes:
85
+ PROMPT
86
+
87
+ if AIGit::Config.request_format == :ollama
88
+ json_body = {
89
+ model: model_name,
90
+ prompt: prompt,
91
+ stream: false,
92
+ temperature: 0.2,
93
+ top_p: 0.9,
94
+ stop: ["\n\n\n", "```", "Here is", "The review"],
95
+ num_predict: 600
96
+ }.to_json
97
+
98
+ uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
99
+ request = Net::HTTP::Post.new(uri)
100
+ request["Content-Type"] = "application/json"
101
+ request.body = json_body
102
+
103
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
104
+ http.request(request)
105
+ end
106
+
107
+ raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
108
+
109
+ data = JSON.parse(response.body)
110
+ review = data["response"].to_s.strip
111
+ else # Jan AI
112
+ json_body = {
113
+ model: model_name,
114
+ messages: [{ role: "user", content: prompt }],
115
+ stream: false,
116
+ temperature: 0.2
117
+ }.to_json
118
+
119
+ uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
120
+ request = Net::HTTP::Post.new(uri)
121
+ request["Content-Type"] = "application/json"
122
+ request.body = json_body
123
+
124
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
125
+ http.request(request)
126
+ end
127
+
128
+ raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
129
+
130
+ data = JSON.parse(response.body)
131
+ review = data["choices"][0]["message"]["content"].to_s.strip
132
+ end
133
+
134
+ review = review.gsub(/^(Here is|The review is|```|json|markdown)/i, "")
135
+ .gsub(/^>\s*/, "")
136
+ .gsub(/\\n/, "\n")
137
+ .strip
138
+
139
+ lines = review.lines.map(&:strip)
140
+ lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
141
+
142
+ review = lines.join("\n").strip
143
+ review = "No review generated." if review.empty?
144
+
145
+ review
146
+ end
147
+
148
+ def call
149
+ provider = AIGit::Config.provider
150
+ model_name = AIGit::Config.model_name
151
+
152
+ staged = AIGit::Git.staged_files
153
+ abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
154
+
155
+ diff = AIGit::Git.diff
156
+ branch = AIGit::Git.current_branch
157
+
158
+ puts "\e[1mAI Provider:\e[0m #{provider}"
159
+ puts "\e[1mModel Name:\e[0m #{model_name}"
160
+ puts "\e[1mStaged Files:\e[0m #{staged}"
161
+ puts "\e[1mBranch:\e[0m #{branch}"
162
+ puts "\e[1mAI Reviewing Changes\e[0m"
163
+
164
+ result = Benchmark.measure do
165
+ review = generate_review(diff, model_name)
166
+ puts "\e[1mCode Review:\e[0m\n\n#{review}\n"
167
+ end
168
+
169
+ puts "\e[1mBenchmark\e[0m"
170
+ puts result
171
+ end
172
+ end
173
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIGit
4
- VERSION = "0.0.4"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/ai_git.rb CHANGED
@@ -1,42 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "benchmark"
4
3
  require_relative "ai_git/version"
4
+ require_relative "ai_git/config"
5
5
  require_relative "ai_git/git"
6
- require_relative "ai_git/ollama"
6
+ require_relative "ai_git/review"
7
+ require_relative "ai_git/default"
7
8
 
8
9
  module AIGit
9
10
  module_function
10
11
 
11
- def run
12
- model_name = ENV["AI_GIT_MODEL_NAME"] || "phi4:14b"
12
+ SUBCOMMANDS = {
13
+ "review" => AIGit::Review,
14
+ "default" => AIGit::Default
15
+ }.freeze
13
16
 
14
- staged = AIGit::Git.staged_files
15
- abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
17
+ def start(args)
18
+ command = args.first || "default"
16
19
 
17
- diff = AIGit::Git.diff
18
- branch = AIGit::Git.current_branch
20
+ raise "Unknown subcommand: #{command}" unless SUBCOMMANDS.key?(command)
19
21
 
20
- puts "\e[1mModel Name:\e[0m #{model_name}"
21
- puts "\e[1mStaged Files:\e[0m #{staged}"
22
- puts "\e[1mBranch:\e[0m #{branch}"
23
- puts "\e[1mAI Generating Commit Message\e[0m"
24
-
25
- result = Benchmark.measure do
26
- message = AIGit::Ollama.generate_commit_message(diff, model_name)
27
- message = message.gsub(/\n{2,}/, "\n")
28
-
29
- puts "\e[1mCommit Message:\e[0m\n\n#{message}\n"
30
-
31
- escaped_msg = message.gsub(/[\\"`$]/) { |c| "\\#{c}" }
32
- AIGit::Git.run_command("git", "commit -m \"#{escaped_msg}\"")
33
- puts "\e[1mGit Commited\e[0m"
34
-
35
- AIGit::Git.run_command("git", "push")
36
- puts "\e[1mGit Pushed\e[0m"
37
- end
38
-
39
- puts "\e[1mBenchmark\e[0m"
40
- puts result
22
+ SUBCOMMANDS[command].call
41
23
  end
42
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai_git
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaíque Kandy Koga
@@ -9,7 +9,7 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: AI‑powered Git commit + push tool using SLMs
12
+ description: AI‑powered Git using SLMs
13
13
  executables:
14
14
  - ai_git
15
15
  extensions: []
@@ -17,8 +17,10 @@ extra_rdoc_files: []
17
17
  files:
18
18
  - bin/ai_git
19
19
  - lib/ai_git.rb
20
+ - lib/ai_git/config.rb
21
+ - lib/ai_git/default.rb
20
22
  - lib/ai_git/git.rb
21
- - lib/ai_git/ollama.rb
23
+ - lib/ai_git/review.rb
22
24
  - lib/ai_git/version.rb
23
25
  homepage: https://github.com/kaiquekandykoga/ai_git
24
26
  licenses:
@@ -31,7 +33,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
31
33
  requirements:
32
34
  - - ">="
33
35
  - !ruby/object:Gem::Version
34
- version: '4.0'
36
+ version: '0'
35
37
  required_rubygems_version: !ruby/object:Gem::Requirement
36
38
  requirements:
37
39
  - - ">="
@@ -40,5 +42,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
42
  requirements: []
41
43
  rubygems_version: 4.0.9
42
44
  specification_version: 4
43
- summary: AI‑powered Git commit + push tool using SLMs
45
+ summary: AI‑powered Git using SLMs
44
46
  test_files: []