ai_git 0.0.3 → 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: 36409c80888ac303814accb279c6908e55693271fc92b22c2eb151a0539285a4
4
- data.tar.gz: b17d56e53f81da126bdd231d12964b799555231eab76dc2cf7002c3a4d01e368
3
+ metadata.gz: f3d2a98e6fef0c0ce3deb757a48e43701f1800d8c524f0cea525a92c8b4ae575
4
+ data.tar.gz: 054d32afa6032a6bdfb2d4946673466b2b65ec6628dfecfb359bf67442c54e4e
5
5
  SHA512:
6
- metadata.gz: e760bbd37b63c7ba1cfcb486e084742718033924990b590f03e51fd9c3ec31ece730c287c0f364680fff34962a68a9d4046f66611d2f9b350c8ac520a76f165d
7
- data.tar.gz: e2d581350863a6b9dd6ff67a537b57aa126233f9cbfc921dcd63cd5f64b79cd404befff9e4157940f3ee921a4c47c4f749f37e12ed3d750fdb41f0e5303624c9
6
+ metadata.gz: b9064aeb4a13c6719a3847a41e07e3a933d2404cd7be169cf7806d6efa5ade9d5c3b0f1ebeb12c9951ac1386ec9fe40af83bc8cba5017829f788e1211bc9f566
7
+ data.tar.gz: 6eacdb5d1ad3be96c5893466e02e28f2e5886091e5f8ca19fcd6814d31fc2629938bfc18447c3e07b98d6fde28e7582b63f483b0e3631deb0829f2e424d619ad
data/bin/ai_git CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative "../lib/ai_git"
4
5
 
5
- AIGit.run
6
+ AIGit.start(ARGV)
@@ -0,0 +1,152 @@
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 Default
12
+ module_function
13
+
14
+ def escape_json(string)
15
+ string.gsub("\\", "\\\\")
16
+ .gsub('"', '\"')
17
+ .gsub("\n", '\\n')
18
+ .gsub("\r", '\\r')
19
+ .gsub("\t", '\\t')
20
+ end
21
+
22
+ def generate_commit_message(diff, model_name)
23
+ raise "No staged changes to generate commit message for" if diff.to_s.strip.empty?
24
+
25
+ prompt = <<~PROMPT
26
+ You are an expert Git commit message writer. Output ONLY the commit message — no explanations, no markdown, no backticks, no preamble.
27
+
28
+ Here are the changes:
29
+ #{diff}
30
+
31
+ STRICT OUTPUT FORMAT (follow exactly):
32
+
33
+ <short imperative title, max 72 chars>
34
+
35
+ <blank line>
36
+
37
+ ## Summary
38
+ <2–4 bullet points covering the most important changes. Each bullet starts with a verb.>
39
+
40
+ ## Why
41
+ <1–3 sentences explaining the motivation or context behind the change. Omit if the reason is obvious.>
42
+
43
+ RULES:
44
+ - Title line: short, specific, imperative mood (e.g. "Add JWT login with refresh token support"). Avoid vague titles like "Update stuff" or "Fix bug".
45
+ - Summary bullets: describe WHAT changed, not HOW the code looks. Focus on behaviour and impact.
46
+ - Why section: explain the problem being solved or the goal being achieved. Skip if it adds no value.
47
+ - No filler phrases ("this commit", "this PR", "as per discussion").
48
+ - No line should exceed 72 characters.
49
+
50
+ EXAMPLES OF GOOD OUTPUT:
51
+
52
+ Add JWT-based login with refresh token support
53
+
54
+ ## Summary
55
+ - Implement login endpoint with access and refresh token issuance
56
+ - Add token refresh route with rotation and expiry validation
57
+ - Protect private routes via middleware that verifies access tokens
58
+ - Store refresh tokens using encrypted HTTP-only cookies
59
+
60
+ ## Why
61
+ Users were being logged out on every page reload. Refresh tokens allow
62
+ sessions to persist securely without requiring re-authentication.
63
+
64
+ ---
65
+
66
+ Prevent nil crash when user preferences are missing
67
+
68
+ ## Summary
69
+ - Add nil guard in ReportGenerator#process before accessing preferences
70
+ - Fall back to system defaults when preferences object is absent
71
+
72
+ ## Why
73
+ Reports were raising NoMethodError in production for users created
74
+ before the preferences feature shipped.
75
+
76
+ ---
77
+
78
+ Now generate the commit message:
79
+ PROMPT
80
+
81
+ json_body = {
82
+ model: model_name,
83
+ prompt: prompt,
84
+ stream: false,
85
+ temperature: 0.3,
86
+ top_p: 0.9,
87
+ stop: ["\n\n\n", "```", "Here is", "The commit message"],
88
+ num_predict: 400
89
+ }.to_json
90
+
91
+ uri = URI("http://localhost:11434/api/generate")
92
+ request = Net::HTTP::Post.new(uri)
93
+ request["Content-Type"] = "application/json"
94
+ request.body = json_body
95
+
96
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
97
+ http.request(request)
98
+ end
99
+
100
+ raise "Failed to connect to Ollama. Is it running?" unless response.is_a?(Net::HTTPSuccess)
101
+
102
+ data = JSON.parse(response.body)
103
+ message = data["response"].to_s.strip
104
+
105
+ message = message.gsub(/^(Here is|The commit message is|```|json|markdown)/i, "")
106
+ .gsub(/^>\s*/, "")
107
+ .gsub(/\\n/, "\n")
108
+ .strip
109
+
110
+ lines = message.lines.map(&:strip)
111
+ lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
112
+
113
+ message = lines.join("\n").strip
114
+
115
+ message = "chore: update code" if message.lines.count < 1 || message.strip.empty?
116
+
117
+ message
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
151
+ end
152
+ end
data/lib/ai_git/git.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
1
4
  module AIGit
2
5
  module Git
3
6
  module_function
@@ -17,7 +20,7 @@ module AIGit
17
20
 
18
21
  def run_command(cmd, args)
19
22
  system("#{cmd} #{args} > /dev/null 2>&1")
20
- raise "Command failed: #{cmd} #{args}" if $?.exitstatus != 0
23
+ raise "Command failed: #{cmd} #{args}" if $CHILD_STATUS.exitstatus != 0
21
24
  end
22
25
  end
23
26
  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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AIGit
2
- VERSION = "0.0.3"
4
+ VERSION = "0.1.0"
3
5
  end
data/lib/ai_git.rb CHANGED
@@ -1,40 +1,23 @@
1
- require 'benchmark'
2
- require_relative 'ai_git/version'
3
- require_relative 'ai_git/git'
4
- require_relative 'ai_git/ollama'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ai_git/version"
4
+ require_relative "ai_git/git"
5
+ require_relative "ai_git/review"
6
+ require_relative "ai_git/default"
5
7
 
6
8
  module AIGit
7
9
  module_function
8
10
 
9
- def run
10
- model_name = ENV["AI_GIT_MODEL_NAME"] || "phi4:14b"
11
-
12
- staged = AIGit::Git.staged_files
13
- abort 'Error: No staged files. Use `git add` first.' if staged.to_s.strip.empty?
14
-
15
- diff = AIGit::Git.diff
16
- branch = AIGit::Git.current_branch
17
-
18
- puts "\e[1mModel Name:\e[0m #{model_name}"
19
- puts "\e[1mStaged Files:\e[0m #{staged}"
20
- puts "\e[1mBranch:\e[0m #{branch}"
21
- puts "\e[1mAI Generating Commit Message\e[0m"
22
-
23
- result = Benchmark.measure do
24
- message = AIGit::Ollama.generate_commit_message(diff, model_name)
25
- message = message.gsub(/\n{2,}/, "\n")
26
-
27
- puts "\e[1mCommit Message:\e[0m\n\n#{message}\n"
11
+ SUBCOMMANDS = {
12
+ "review" => AIGit::Review,
13
+ "default" => AIGit::Default
14
+ }.freeze
28
15
 
29
- escaped_msg = message.gsub(/[\\"`$]/) { |c| "\\#{c}" }
30
- AIGit::Git.run_command('git', "commit -m \"#{escaped_msg}\"")
31
- puts "\e[1mGit Commited\e[0m"
16
+ def start(args)
17
+ command = args.first || "default"
32
18
 
33
- AIGit::Git.run_command('git', 'push')
34
- puts "\e[1mGit Pushed\e[0m"
35
- end
19
+ raise "Unknown subcommand: #{command}" unless SUBCOMMANDS.key?(command)
36
20
 
37
- puts "\e[1mBenchmark\e[0m"
38
- puts result
21
+ SUBCOMMANDS[command].call
39
22
  end
40
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.3
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: Generates intelligent commit messages using Ollama LLMs
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 and push tool using Ollama
44
+ summary: AIpowered Git using SLMs
44
45
  test_files: []
data/lib/ai_git/ollama.rb DELETED
@@ -1,121 +0,0 @@
1
- require 'net/http'
2
- require 'uri'
3
- require 'json'
4
-
5
- module AIGit
6
- module Ollama
7
- module_function
8
-
9
- def escape_json(string)
10
- string.gsub('\\', '\\\\')
11
- .gsub('"', '\"')
12
- .gsub("\n", '\\n')
13
- .gsub("\r", '\\r')
14
- .gsub("\t", '\\t')
15
- end
16
-
17
- def generate_commit_message(diff, model_name)
18
- raise 'No staged changes to generate commit message for' if diff.to_s.strip.empty?
19
-
20
- prompt = <<~PROMPT
21
- You are an expert Git commit message writer. Output ONLY the commit message — no explanations, no markdown, no backticks, no preamble.
22
-
23
- Here are the changes:
24
- #{diff}
25
-
26
- STRICT OUTPUT FORMAT (follow exactly):
27
-
28
- <short imperative title, max 72 chars>
29
-
30
- <blank line>
31
-
32
- ## Summary
33
- <2–4 bullet points covering the most important changes. Each bullet starts with a verb.>
34
-
35
- ## Why
36
- <1–3 sentences explaining the motivation or context behind the change. Omit if the reason is obvious.>
37
-
38
- RULES:
39
- - Title line: short, specific, imperative mood (e.g. "Add JWT login with refresh token support"). Avoid vague titles like "Update stuff" or "Fix bug".
40
- - Summary bullets: describe WHAT changed, not HOW the code looks. Focus on behaviour and impact.
41
- - Why section: explain the problem being solved or the goal being achieved. Skip if it adds no value.
42
- - No filler phrases ("this commit", "this PR", "as per discussion").
43
- - No line should exceed 72 characters.
44
-
45
- EXAMPLES OF GOOD OUTPUT:
46
-
47
- Add JWT-based login with refresh token support
48
-
49
- ## Summary
50
- - Implement login endpoint with access and refresh token issuance
51
- - Add token refresh route with rotation and expiry validation
52
- - Protect private routes via middleware that verifies access tokens
53
- - Store refresh tokens using encrypted HTTP-only cookies
54
-
55
- ## Why
56
- Users were being logged out on every page reload. Refresh tokens allow
57
- sessions to persist securely without requiring re-authentication.
58
-
59
- ---
60
-
61
- Prevent nil crash when user preferences are missing
62
-
63
- ## Summary
64
- - Add nil guard in ReportGenerator#process before accessing preferences
65
- - Fall back to system defaults when preferences object is absent
66
-
67
- ## Why
68
- Reports were raising NoMethodError in production for users created
69
- before the preferences feature shipped.
70
-
71
- ---
72
-
73
- Now generate the commit message:
74
- PROMPT
75
-
76
- json_body = {
77
- model: model_name,
78
- prompt: prompt,
79
- stream: false,
80
- # These parameters help a lot with strictness:
81
- temperature: 0.3,
82
- top_p: 0.9,
83
- stop: ["\n\n\n", "```", "Here is", "The commit message"],
84
- num_predict: 400 # Limit output length
85
- }.to_json
86
-
87
- uri = URI('http://localhost:11434/api/generate')
88
- request = Net::HTTP::Post.new(uri)
89
- request['Content-Type'] = 'application/json'
90
- request.body = json_body
91
-
92
- response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
93
- http.request(request)
94
- end
95
-
96
- raise 'Failed to connect to Ollama. Is it running?' unless response.is_a?(Net::HTTPSuccess)
97
-
98
- data = JSON.parse(response.body)
99
- message = data['response'].to_s.strip
100
-
101
- # Aggressive cleaning
102
- message = message.gsub(/^(Here is|The commit message is|```|json|markdown)/i, '')
103
- .gsub(/^>\s*/, '')
104
- .gsub(/\\n/, "\n")
105
- .strip
106
-
107
- # Remove common unwanted prefixes/suffixes
108
- lines = message.lines.map(&:strip)
109
- lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
110
-
111
- message = lines.join("\n").strip
112
-
113
- # Ensure it has at least a title
114
- if message.lines.count < 1 || message.strip.empty?
115
- message = "chore: update code" # fallback
116
- end
117
-
118
- message
119
- end
120
- end
121
- end