ai_git 0.0.4 → 0.1.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: f3d2a98e6fef0c0ce3deb757a48e43701f1800d8c524f0cea525a92c8b4ae575
4
+ data.tar.gz: 054d32afa6032a6bdfb2d4946673466b2b65ec6628dfecfb359bf67442c54e4e
5
5
  SHA512:
6
- metadata.gz: 03cbbe8cf09690cc3db34f98bf42e39479672ce464d65de18a6a25d901ea9c8d35c318a0efa4eac06abc3d1f07d1848be24506ca4c6146facff7e9dcb18a1e80
7
- data.tar.gz: 47a3c30cbb3ed60d128db9d68af240b5404096ffc95643766cb873296f8e3f2ccacc5cbc284be4fdc6d984dd1c310ea2ff01873c5442abb86745e1d34be59c19
6
+ metadata.gz: b9064aeb4a13c6719a3847a41e07e3a933d2404cd7be169cf7806d6efa5ade9d5c3b0f1ebeb12c9951ac1386ec9fe40af83bc8cba5017829f788e1211bc9f566
7
+ data.tar.gz: 6eacdb5d1ad3be96c5893466e02e28f2e5886091e5f8ca19fcd6814d31fc2629938bfc18447c3e07b98d6fde28e7582b63f483b0e3631deb0829f2e424d619ad
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)
@@ -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)
@@ -79,11 +82,10 @@ module AIGit
79
82
  model: model_name,
80
83
  prompt: prompt,
81
84
  stream: false,
82
- # These parameters help a lot with strictness:
83
85
  temperature: 0.3,
84
86
  top_p: 0.9,
85
87
  stop: ["\n\n\n", "```", "Here is", "The commit message"],
86
- num_predict: 400 # Limit output length
88
+ num_predict: 400
87
89
  }.to_json
88
90
 
89
91
  uri = URI("http://localhost:11434/api/generate")
@@ -100,24 +102,51 @@ module AIGit
100
102
  data = JSON.parse(response.body)
101
103
  message = data["response"].to_s.strip
102
104
 
103
- # Aggressive cleaning
104
105
  message = message.gsub(/^(Here is|The commit message is|```|json|markdown)/i, "")
105
106
  .gsub(/^>\s*/, "")
106
107
  .gsub(/\\n/, "\n")
107
108
  .strip
108
109
 
109
- # Remove common unwanted prefixes/suffixes
110
110
  lines = message.lines.map(&:strip)
111
111
  lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
112
112
 
113
113
  message = lines.join("\n").strip
114
114
 
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
115
+ message = "chore: update code" if message.lines.count < 1 || message.strip.empty?
119
116
 
120
117
  message
121
118
  end
119
+
120
+ def call
121
+ model_name = ENV["AI_GIT_MODEL_NAME"] || "phi4:14b"
122
+
123
+ staged = AIGit::Git.staged_files
124
+ abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
125
+
126
+ diff = AIGit::Git.diff
127
+ branch = AIGit::Git.current_branch
128
+
129
+ puts "\e[1mModel Name:\e[0m #{model_name}"
130
+ puts "\e[1mStaged Files:\e[0m #{staged}"
131
+ puts "\e[1mBranch:\e[0m #{branch}"
132
+ puts "\e[1mAI Generating Commit Message\e[0m"
133
+
134
+ result = Benchmark.measure do
135
+ message = generate_commit_message(diff, model_name)
136
+ message = message.gsub(/\n{2,}/, "\n")
137
+
138
+ puts "\e[1mCommit Message:\e[0m\n\n#{message}\n"
139
+
140
+ escaped_msg = message.gsub(/[\\"`$]/) { |c| "\\#{c}" }
141
+ AIGit::Git.run_command("git", "commit -m \"#{escaped_msg}\"")
142
+ puts "\e[1mGit Commited\e[0m"
143
+
144
+ AIGit::Git.run_command("git", "push")
145
+ puts "\e[1mGit Pushed\e[0m"
146
+ end
147
+
148
+ puts "\e[1mBenchmark\e[0m"
149
+ puts result
150
+ end
122
151
  end
123
152
  end
@@ -0,0 +1,148 @@
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
+ json_body = {
88
+ model: model_name,
89
+ prompt: prompt,
90
+ stream: false,
91
+ temperature: 0.2,
92
+ top_p: 0.9,
93
+ stop: ["\n\n\n", "```", "Here is", "The review"],
94
+ num_predict: 600
95
+ }.to_json
96
+
97
+ uri = URI("http://localhost:11434/api/generate")
98
+ request = Net::HTTP::Post.new(uri)
99
+ request["Content-Type"] = "application/json"
100
+ request.body = json_body
101
+
102
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
103
+ http.request(request)
104
+ end
105
+
106
+ raise "Failed to connect to Ollama. Is it running?" unless response.is_a?(Net::HTTPSuccess)
107
+
108
+ data = JSON.parse(response.body)
109
+ review = data["response"].to_s.strip
110
+
111
+ review = review.gsub(/^(Here is|The review is|```|json|markdown)/i, "")
112
+ .gsub(/^>\s*/, "")
113
+ .gsub(/\\n/, "\n")
114
+ .strip
115
+
116
+ lines = review.lines.map(&:strip)
117
+ lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
118
+
119
+ review = lines.join("\n").strip
120
+ review = "No review generated." if review.empty?
121
+
122
+ review
123
+ end
124
+
125
+ def call
126
+ model_name = ENV["AI_GIT_MODEL_NAME"] || "phi4:14b"
127
+
128
+ staged = AIGit::Git.staged_files
129
+ abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
130
+
131
+ diff = AIGit::Git.diff
132
+ branch = AIGit::Git.current_branch
133
+
134
+ puts "\e[1mModel Name:\e[0m #{model_name}"
135
+ puts "\e[1mStaged Files:\e[0m #{staged}"
136
+ puts "\e[1mBranch:\e[0m #{branch}"
137
+ puts "\e[1mAI Reviewing Changes\e[0m"
138
+
139
+ result = Benchmark.measure do
140
+ review = generate_review(diff, model_name)
141
+ puts "\e[1mCode Review:\e[0m\n\n#{review}\n"
142
+ end
143
+
144
+ puts "\e[1mBenchmark\e[0m"
145
+ puts result
146
+ end
147
+ end
148
+ 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.1.0"
5
5
  end
data/lib/ai_git.rb CHANGED
@@ -1,42 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "benchmark"
4
3
  require_relative "ai_git/version"
5
4
  require_relative "ai_git/git"
6
- require_relative "ai_git/ollama"
5
+ require_relative "ai_git/review"
6
+ require_relative "ai_git/default"
7
7
 
8
8
  module AIGit
9
9
  module_function
10
10
 
11
- def run
12
- model_name = ENV["AI_GIT_MODEL_NAME"] || "phi4:14b"
11
+ SUBCOMMANDS = {
12
+ "review" => AIGit::Review,
13
+ "default" => AIGit::Default
14
+ }.freeze
13
15
 
14
- staged = AIGit::Git.staged_files
15
- abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
16
+ def start(args)
17
+ command = args.first || "default"
16
18
 
17
- diff = AIGit::Git.diff
18
- branch = AIGit::Git.current_branch
19
+ raise "Unknown subcommand: #{command}" unless SUBCOMMANDS.key?(command)
19
20
 
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
21
+ SUBCOMMANDS[command].call
41
22
  end
42
23
  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.1.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,9 @@ extra_rdoc_files: []
17
17
  files:
18
18
  - bin/ai_git
19
19
  - lib/ai_git.rb
20
+ - lib/ai_git/default.rb
20
21
  - lib/ai_git/git.rb
21
- - lib/ai_git/ollama.rb
22
+ - lib/ai_git/review.rb
22
23
  - lib/ai_git/version.rb
23
24
  homepage: https://github.com/kaiquekandykoga/ai_git
24
25
  licenses:
@@ -40,5 +41,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
41
  requirements: []
41
42
  rubygems_version: 4.0.9
42
43
  specification_version: 4
43
- summary: AI‑powered Git commit + push tool using SLMs
44
+ summary: AI‑powered Git using SLMs
44
45
  test_files: []