ai-cr 1.0.0__py3-none-any.whl → 2.0.0.dev1__py3-none-any.whl

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.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.3
2
+ Name: ai-cr
3
+ Version: 2.0.0.dev1
4
+ Summary: AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
5
+ License: MIT
6
+ Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
7
+ Author: Nayjest
8
+ Author-email: mail@vitaliy.in
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development
18
+ Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
+ Requires-Dist: ai-microcore (==4.0.0)
20
+ Requires-Dist: anthropic (>=0.52.2,<0.53.0)
21
+ Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
22
+ Requires-Dist: typer (>=0.16.0,<0.17.0)
23
+ Requires-Dist: unidiff (>=0.7.5,<0.8.0)
24
+ Project-URL: Homepage, https://github.com/Nayjest/Gito
25
+ Project-URL: Repository, https://github.com/Nayjest/Gito
26
+ Description-Content-Type: text/markdown
27
+
28
+ <h1 align="center"><a href="#"><img alt="Gito: AI Code Reviewer" src="press-kit/logo/gito-ai-code-reviewer_logo-180.png" align="center" width="180"></a></h1>
29
+ <p align="center">
30
+ <a href="https://pypi.org/project/gito.bot/" target="_blank"><img src="https://img.shields.io/pypi/v/gito.bot" alt="PYPI Release"></a>
31
+ <a href="https://github.com/Nayjest/Gito/actions/workflows/code-style.yml" target="_blank"><img src="https://github.com/Nayjest/Gito/actions/workflows/code-style.yml/badge.svg" alt="PyLint"></a>
32
+ <a href="https://github.com/Nayjest/Gito/actions/workflows/tests.yml" target="_blank"><img src="https://github.com/Nayjest/Gito/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
33
+ <img src="https://github.com/Nayjest/Gito/blob/main/coverage.svg" alt="Code Coverage">
34
+ <a href="https://github.com/Nayjest/Gito/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/static/v1?label=license&message=MIT&color=d08aff" alt="License"></a>
35
+ </p>
36
+
37
+ **Gito** is an open-source **AI code reviewer** that works with any language model provider.
38
+ It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
39
+
40
+ Get consistent, thorough code reviews in seconds—no waiting for human availability.
41
+
42
+ ## ✨ Why Gito?
43
+
44
+ - [⚡] **Lightning Fast:** Get detailed code reviews in seconds, not days — powered by parallelized LLM processing
45
+ - [🔧] **Vendor Agnostic:** Works with any language model provider (OpenAI, Anthropic, Google, local models, etc.)
46
+ - [🌐] **Universal:** Supports all major programming languages and frameworks
47
+ - [🔍] **Comprehensive Analysis:** Detect issues across security, performance, maintainability, best practices, and much more
48
+ - [📈] **Consistent Quality:** Never tired, never biased—consistent review quality every time
49
+ - [🚀] **Easy Integration:** Automatically reviews pull requests via GitHub Actions and posts results as PR comments
50
+ - [🎛️] **Infinitely Flexible:** Adapt to any project's standards—configure review rules, severity levels, and focus areas, build custom workflows
51
+
52
+ ## 🎯 Perfect For
53
+
54
+ - Solo developers who want expert-level code review without the wait
55
+ - Teams looking to catch issues before human review
56
+ - Open source projects maintaining high code quality at scale
57
+ - CI/CD pipelines requiring automated quality gates
58
+
59
+ ✨ See [code review in action](https://github.com/Nayjest/Gito/pull/39#issuecomment-2906968729) ✨
60
+
61
+ ## 🚀 Quickstart
62
+
63
+ ### 1. Review Pull Requests via GitHub Actions
64
+
65
+ Create a `.github/workflows/gito.yml` file:
66
+
67
+ ```yaml
68
+ name: "Gito: AI Code Review"
69
+ on: { pull_request: { types: [opened, synchronize, reopened] } }
70
+ jobs:
71
+ review:
72
+ runs-on: ubuntu-latest
73
+ permissions: { contents: read, pull-requests: write } # 'write' for leaving the summary comment
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+ with: { fetch-depth: 0 }
77
+ - name: Set up Python
78
+ uses: actions/setup-python@v5
79
+ with: { python-version: "3.13" }
80
+ - name: Install AI Code Review tool
81
+ run: pip install gito.bot~=2.0
82
+ - name: Run AI code analysis
83
+ env:
84
+ LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
85
+ LLM_API_TYPE: openai
86
+ MODEL: "gpt-4.1"
87
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88
+ run: |
89
+ gito --verbose review
90
+ gito github-comment --token ${{ secrets.GITHUB_TOKEN }}
91
+ - uses: actions/upload-artifact@v4
92
+ with:
93
+ name: ai-code-review-results
94
+ path: |
95
+ code-review-report.md
96
+ code-review-report.json
97
+ ```
98
+
99
+ > ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
100
+
101
+ 💪 Done!
102
+ PRs to your repository will now receive AI code reviews automatically. ✨
103
+ See [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation/github_setup.md) for more details.
104
+
105
+ ### 2. Running Code Analysis Locally
106
+
107
+ #### Initial Local Setup
108
+
109
+ **Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
110
+
111
+ **Step1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
112
+ ```bash
113
+ pip install gito.bot
114
+ ```
115
+
116
+ > **Troubleshooting:**
117
+ > pip may be also available via cli as `pip3` depending on your Python installation.
118
+
119
+ **Step2:** Perform initial setup
120
+
121
+ The following command will perform one-time setup using an interactive wizard.
122
+ You will be prompted to enter LLM configuration details (API type, API key, etc).
123
+ Configuration will be saved to `~/.gito/.env`.
124
+
125
+ ```bash
126
+ gito setup
127
+ ```
128
+
129
+ > **Troubleshooting:**
130
+ > On some systems, `gito` command may not became available immediately after installation.
131
+ > Try restarting your terminal or running `python -m gito` instead.
132
+
133
+
134
+ #### Perform your first AI code review locally
135
+
136
+ **Step1:** Navigate to your repository root directory.
137
+ **Step2:** Switch to the branch you want to review.
138
+ **Step3:** Run following command
139
+ ```bash
140
+ gito review
141
+ ```
142
+
143
+ > **Note:** This will analyze the current branch against the repository main branch by default.
144
+ > Files that are not staged for commit will be ignored.
145
+ > See `gito --help` for more options.
146
+
147
+ **Reviewing remote repository**
148
+
149
+ ```bash
150
+ gito remote git@github.com:owner/repo.git <FEATURE_BRANCH>..<MAIN_BRANCH>
151
+ ```
152
+ Use interactive help for details:
153
+ ```bash
154
+ gito remote --help
155
+ ```
156
+
157
+ ## 🔧 Configuration
158
+
159
+ Change behavior via `.gito/config.toml`:
160
+
161
+ - Prompt templates, filtering and post-processing using Python code snippets
162
+ - Tagging, severity, and confidence settings
163
+ - Custom AI awards for developer brilliance
164
+ - Output customization
165
+
166
+ You can override the default config by placing `.gito/config.toml` in your repo root.
167
+
168
+
169
+ See default configuration [here](https://github.com/Nayjest/Gito/blob/main/gito/config.toml).
170
+
171
+ More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
172
+
173
+ ## 💻 Development Setup
174
+
175
+ Install dependencies:
176
+
177
+ ```bash
178
+ make install
179
+ ```
180
+
181
+ Format code and check style:
182
+
183
+ ```bash
184
+ make black
185
+ make cs
186
+ ```
187
+
188
+ Run tests:
189
+
190
+ ```bash
191
+ pytest
192
+ ```
193
+
194
+ ## 🤝 Contributing
195
+
196
+ **Looking for a specific feature or having trouble?**
197
+ Contributions are welcome! ❤️
198
+ See [CONTRIBUTING.md](https://github.com/Nayjest/Gito/blob/main/CONTRIBUTING.md) for details.
199
+
200
+ ## 📝 License
201
+
202
+ Licensed under the [MIT License](https://github.com/Nayjest/Gito/blob/main/LICENSE).
203
+
204
+ © 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
205
+
@@ -0,0 +1,18 @@
1
+ gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gito/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
3
+ gito/bootstrap.py,sha256=ETKioiDc2Npc7znd8HJxA5-twd7sZMPCufIGwXFQSbY,2403
4
+ gito/cli.py,sha256=JASFo1NGUqw9k6-ZnU5KXFuexxUc6EKJNOYs6uPPyjs,7943
5
+ gito/commands/__init__.py,sha256=NKUUDskR6taZGbHk5qR9msDS22aHpdzAZlUDxzRdMYA,56
6
+ gito/commands/fix.py,sha256=THEu7vdsK2--hOkz6rha4zESVfRVIDc88mHNNlYfM7Q,4421
7
+ gito/commands/repl.py,sha256=waN7FJBl98gWmDwZWMa8x157iWbPHIDPJEKmTdzWQ70,396
8
+ gito/config.toml,sha256=zd6s_UJs7Du1W7ulR2iGu7Grcs4l4qpq--VAS67pj4w,15003
9
+ gito/constants.py,sha256=DNqh3hOxeLM_KscUH4uFdAc9295o1hiXtwoEw2KxLiU,392
10
+ gito/core.py,sha256=a98HJyZMPswwbmfzJBJvnPZMWYJU7njs3O8B4Z_UGmc,7848
11
+ gito/project_config.py,sha256=tbN1mf2FqpUQ9y9hGsGfM2CRX7AmD-ZT0QkXVUdHi4U,4279
12
+ gito/report_struct.py,sha256=vC9Rs6OZvUspn0Ho4s9gVf0doHFt_dNbdqjdubqoIrY,4162
13
+ gito/utils.py,sha256=Oh1hyg-YpUZgOlWNJdRTDLzZV-YyjMkgLsC5MiN1uow,3431
14
+ ai_cr-2.0.0.dev1.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
15
+ ai_cr-2.0.0.dev1.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
16
+ ai_cr-2.0.0.dev1.dist-info/METADATA,sha256=JTgfajLAdhqrWqNw40BfNHh-B3jQRbgJeoiorkuTAaw,7676
17
+ ai_cr-2.0.0.dev1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
18
+ ai_cr-2.0.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ gito=gito.cli:main
3
+
@@ -3,9 +3,10 @@ import os
3
3
  from datetime import datetime
4
4
 
5
5
  import microcore as mc
6
- from ai_code_review.utils import is_running_in_github_action
6
+ import typer
7
7
 
8
- from .constants import ENV_CONFIG_FILE
8
+ from .utils import is_running_in_github_action
9
+ from .constants import HOME_ENV_PATH, EXECUTABLE
9
10
 
10
11
 
11
12
  def setup_logging():
@@ -32,7 +33,7 @@ def bootstrap():
32
33
  logging.info("Bootstrapping...")
33
34
  try:
34
35
  mc.configure(
35
- DOT_ENV_FILE=ENV_CONFIG_FILE,
36
+ DOT_ENV_FILE=HOME_ENV_PATH,
36
37
  USE_LOGGING=True,
37
38
  EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
38
39
  )
@@ -51,7 +52,7 @@ def bootstrap():
51
52
  )
52
53
  else:
53
54
  msg += (
54
- "\nPlease run 'ai-code-review setup' "
55
+ f"\nPlease run '{EXECUTABLE} setup' "
55
56
  "to configure LLM API access (API keys, model, etc)."
56
57
  )
57
58
  print(mc.ui.red(msg))
@@ -60,3 +61,6 @@ def bootstrap():
60
61
  logging.error(f"Unexpected configuration error: {e}")
61
62
  raise SystemExit(3)
62
63
  mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
64
+
65
+
66
+ app = typer.Typer(pretty_exceptions_show_locals=False)
@@ -12,18 +12,25 @@ from git import Repo
12
12
 
13
13
  from .core import review, get_diff, filter_diff
14
14
  from .report_struct import Report
15
- from .constants import ENV_CONFIG_FILE
16
- from .bootstrap import bootstrap
15
+ from .constants import HOME_ENV_PATH
16
+ from .bootstrap import bootstrap, app
17
17
  from .project_config import ProjectConfig
18
18
  from .utils import no_subcommand, parse_refs_pair
19
19
 
20
- app = typer.Typer(pretty_exceptions_show_locals=False)
20
+ # Import fix command to register it
21
+ from .commands import fix # noqa
22
+
23
+
21
24
  app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
22
25
 
23
26
 
24
27
  def main():
25
28
  if sys.platform == "win32":
26
29
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
30
+ # Help subcommand alias: if 'help' appears as first non-option arg, replace it with '--help'
31
+ if len(sys.argv) > 1 and sys.argv[1] == "help":
32
+ sys.argv = [sys.argv[0]] + sys.argv[2:] + ["--help"]
33
+
27
34
  if no_subcommand(app):
28
35
  bootstrap()
29
36
  app_no_subcommand()
@@ -117,12 +124,21 @@ def cmd_review(
117
124
 
118
125
  @app.command(help="Configure LLM for local usage interactively")
119
126
  def setup():
120
- mc.interactive_setup(ENV_CONFIG_FILE)
121
-
122
-
123
- @app.command()
124
- def render(format: str = Report.Format.MARKDOWN):
125
- print(Report.load().render(format=format))
127
+ mc.interactive_setup(HOME_ENV_PATH)
128
+
129
+
130
+ @app.command(name="render")
131
+ @app.command(name="report", hidden=True)
132
+ def render(
133
+ format: str = typer.Argument(default=Report.Format.CLI),
134
+ source: str = typer.Option(
135
+ "",
136
+ "--src",
137
+ "--source",
138
+ help="Source file (json) to load the report from"
139
+ )
140
+ ):
141
+ Report.load(file_name=source).to_cli(report_format=format)
126
142
 
127
143
 
128
144
  @app.command(help="Review remote code")
@@ -225,7 +241,7 @@ def files(
225
241
  f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
226
242
  f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
227
243
  )
228
-
244
+ repo.close()
229
245
  for patch in patch_set:
230
246
  if patch.is_added_file:
231
247
  color = mc.ui.green
@@ -0,0 +1 @@
1
+ # Command modules register themselves with the CLI app
gito/commands/fix.py ADDED
@@ -0,0 +1,124 @@
1
+ """
2
+ Fix issues from code review report
3
+ """
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from microcore import ui
11
+
12
+ from ..bootstrap import app
13
+ from ..constants import JSON_REPORT_FILE_NAME
14
+ from ..report_struct import Report
15
+
16
+
17
+ @app.command(help="Fix an issue from the code review report")
18
+ def fix(
19
+ issue_number: int = typer.Argument(..., help="Issue number to fix"),
20
+ report_path: Optional[str] = typer.Option(
21
+ None,
22
+ "--report",
23
+ "-r",
24
+ help="Path to the code review report (default: code-review-report.json)"
25
+ ),
26
+ dry_run: bool = typer.Option(
27
+ False, "--dry-run", "-d", help="Only print changes without applying them"
28
+ ),
29
+ ):
30
+ """
31
+ Applies the proposed change for the specified issue number from the code review report.
32
+ """
33
+ # Load the report
34
+ report_path = report_path or JSON_REPORT_FILE_NAME
35
+ try:
36
+ report = Report.load(report_path)
37
+ except (FileNotFoundError, json.JSONDecodeError) as e:
38
+ logging.error(f"Failed to load report from {report_path}: {e}")
39
+ raise typer.Exit(code=1)
40
+
41
+ # Find the issue by number
42
+ issue = None
43
+ for file_issues in report.issues.values():
44
+ for i in file_issues:
45
+ if i.id == issue_number:
46
+ issue = i
47
+ break
48
+ if issue:
49
+ break
50
+
51
+ if not issue:
52
+ logging.error(f"Issue #{issue_number} not found in the report")
53
+ raise typer.Exit(code=1)
54
+
55
+ if not issue.affected_lines:
56
+ logging.error(f"Issue #{issue_number} has no affected lines specified")
57
+ raise typer.Exit(code=1)
58
+
59
+ if not any(affected_line.proposal for affected_line in issue.affected_lines):
60
+ logging.error(f"Issue #{issue_number} has no proposal for fixing")
61
+ raise typer.Exit(code=1)
62
+
63
+ # Apply the fix
64
+ logging.info(f"Fixing issue #{issue_number}: {ui.cyan(issue.title)}")
65
+
66
+ for affected_line in issue.affected_lines:
67
+ if not affected_line.proposal:
68
+ continue
69
+
70
+ file_path = Path(issue.file)
71
+ if not file_path.exists():
72
+ logging.error(f"File {file_path} not found")
73
+ continue
74
+
75
+ try:
76
+ with open(file_path, "r", encoding="utf-8") as f:
77
+ lines = f.readlines()
78
+ except Exception as e:
79
+ logging.error(f"Failed to read file {file_path}: {e}")
80
+ continue
81
+
82
+ # Check if line numbers are valid
83
+ if affected_line.start_line < 1 or affected_line.end_line > len(lines):
84
+ logging.error(
85
+ f"Invalid line range: {affected_line.start_line}-{affected_line.end_line} "
86
+ f"(file has {len(lines)} lines)"
87
+ )
88
+ continue
89
+
90
+ # Get the affected line content for display
91
+ affected_content = "".join(lines[affected_line.start_line - 1:affected_line.end_line])
92
+ print(f"\nFile: {ui.blue(issue.file)}")
93
+ print(f"Lines: {affected_line.start_line}-{affected_line.end_line}")
94
+ print(f"Current content:\n{ui.red(affected_content)}")
95
+ print(f"Proposed change:\n{ui.green(affected_line.proposal)}")
96
+
97
+ if dry_run:
98
+ print(f"{ui.yellow('Dry run')}: Changes not applied")
99
+ continue
100
+
101
+ # Apply the change
102
+ proposal_lines = affected_line.proposal.splitlines(keepends=True)
103
+ if not proposal_lines:
104
+ proposal_lines = [""]
105
+ elif not proposal_lines[-1].endswith(("\n", "\r")):
106
+ # Ensure the last line has a newline if the original does
107
+ if (
108
+ affected_line.end_line < len(lines)
109
+ and lines[affected_line.end_line - 1].endswith(("\n", "\r"))
110
+ ):
111
+ proposal_lines[-1] += "\n"
112
+
113
+ lines[affected_line.start_line - 1:affected_line.end_line] = proposal_lines
114
+
115
+ # Write changes back to the file
116
+ try:
117
+ with open(file_path, "w", encoding="utf-8") as f:
118
+ f.writelines(lines)
119
+ print(f"{ui.green('Success')}: Changes applied to {file_path}")
120
+ except Exception as e:
121
+ logging.error(f"Failed to write changes to {file_path}: {e}")
122
+ raise typer.Exit(code=1)
123
+
124
+ print(f"\n{ui.green('✓')} Issue #{issue_number} fixed successfully")
@@ -3,7 +3,7 @@ Python REPL
3
3
  """
4
4
  # flake8: noqa: F401
5
5
  import code
6
- from ema.cli import app
6
+
7
7
 
8
8
  # Imports for usage in REPL
9
9
  import os
@@ -17,6 +17,8 @@ from rich.pretty import pprint
17
17
  import microcore as mc
18
18
  from microcore import ui
19
19
 
20
+ from ..cli import app
21
+
20
22
  @app.command(help="python REPL")
21
23
  def repl():
22
24
  code.interact(local=globals())
@@ -1,14 +1,15 @@
1
1
  report_template_md = """
2
- # 🤖 I've Reviewed the Code
2
+ <h2><a href="https://github.com/Nayjest/Gito"><img src="https://raw.githubusercontent.com/Nayjest/Gito/main/press-kit/logo/gito-bot-1_64top.png" align="left" width=64 height=50></a>I've Reviewed the Code</h2>
3
3
 
4
4
  {% if report.summary -%}
5
5
  {{ report.summary }}
6
6
  {%- endif %}
7
7
 
8
- **{%- if report.total_issues > 0 %}⚠️{% endif -%}
9
- Total issues: `{{ report.total_issues }}`
10
- {{- ' ' -}}
11
- in `{{ report.number_of_processed_files }}` files**
8
+ {% if report.total_issues > 0 -%}
9
+ **⚠️ {{ report.total_issues }} issue{{ 's' if report.total_issues != 1 else '' }} found** across {{ report.number_of_processed_files }} file{{ 's' if report.number_of_processed_files != 1 else '' }}
10
+ {%- else -%}
11
+ **✅ No issues found** in {{ report.number_of_processed_files }} file{{ 's' if report.number_of_processed_files != 1 else '' }}
12
+ {%- endif -%}
12
13
 
13
14
  {%- for issue in report.plain_issues -%}
14
15
  {{"\n"}}## `#{{ issue.id}}` {{ issue.title -}}
@@ -31,6 +32,77 @@ in `{{ report.number_of_processed_files }}` files**
31
32
  {{ "\n" }}
32
33
  {%- endfor -%}
33
34
 
35
+ """
36
+ report_template_cli = """
37
+ {{ Back.BLUE }} + + + ---==<<[ CODE REVIEW{{Style.NORMAL}} ]>>==--- + + + {{Style.RESET_ALL}}
38
+ {% if report.total_issues > 0 -%}
39
+ {{ Style.BRIGHT }}{{Back.RED}} ⚠️ {{ report.total_issues }} issue{{ 's' if report.total_issues != 1 else '' }} {{Back.RESET}} found across {{Back.BLUE}} {{ report.number_of_processed_files }} {{Back.RESET}} file{{ 's' if report.number_of_processed_files != 1 else '' }}{{ Style.RESET_ALL }}
40
+ {%- else -%}
41
+ {{ Style.BRIGHT }}{{Back.GREEN}} ✅ No issues found {{Back.RESET}} in {{Back.BLUE}} {{ report.number_of_processed_files }} {{Back.RESET}} file{{ 's' if report.number_of_processed_files != 1 else '' }}{{ Style.RESET_ALL }}
42
+ {%- endif -%}
43
+
44
+ {%- if report.summary -%}
45
+ {{- "\n" }}
46
+ {{- "\n" }}{{- Style.BRIGHT }}✨ SUMMARY {{ Style.RESET_ALL -}}
47
+ {{- "\n" }}{{- report.summary -}}
48
+ {%- endif %}
49
+ {% for issue in report.plain_issues -%}
50
+ {{"\n"}}{{ Style.BRIGHT }}{{Back.RED}}[ {{ issue.id}} ]{{Back.RESET}} {{ issue.title -}}{{ Style.RESET_ALL -}}
51
+ {{ "\n"}}{{ file_link(issue.file) -}}
52
+ {%- if issue.affected_lines -%}:{{issue.affected_lines[0].start_line}}{%- endif -%}
53
+ {{' '}}
54
+
55
+ {%- if issue.affected_lines -%}
56
+ {% if issue.affected_lines[0].end_line != issue.affected_lines[0].start_line or issue.affected_lines|length > 1 -%}
57
+ {{ ui.gray }}Lines{{' '}}
58
+ {{- Fore.RESET -}}
59
+ {%- for i in issue.affected_lines -%}
60
+ {{ i.start_line }}{%- if i.end_line != i.start_line -%}{{ ui.gray }}–{{Fore.RESET}}{{ i.end_line }}{%- endif -%}
61
+ {%- if loop.last == false -%}
62
+ {{ ui.gray(', ') }}
63
+ {%- endif -%}
64
+ {%- endfor -%}
65
+ {%- endif -%}
66
+ {%- endif -%}
67
+ {{-"\n"-}}
68
+
69
+ {% if issue.details -%}
70
+ {{- "\n" -}}
71
+ {{- issue.details.strip() -}}
72
+ {{-"\n" -}}
73
+ {%- endif -%}
74
+
75
+ {%- for tag in issue.tags -%}
76
+ {{Back.YELLOW}}{{Fore.BLACK}} {{tag}} {{Style.RESET_ALL}}{{ ' ' }}
77
+ {%- endfor -%}
78
+ {%- if issue.tags %}{{ "\n" }}{% endif -%}
79
+
80
+ {%- for i in issue.affected_lines -%}
81
+ {%- if i.affected_code -%}
82
+ {{- "\n"+Fore.RED + " ╭─" + "─"*4 + "[ 💥 Affected Code ]" + "─"*4 + " ─── ── ─\n" -}}
83
+ {{- textwrap.indent(i.affected_code.strip(), Fore.RED+' │ ') -}}
84
+ {{- "\n ╰─"+"─"*2+Style.RESET_ALL -}}
85
+ {%- endif -%}
86
+ {%- if i.proposal -%}
87
+ {%- set maxlen = 100 -%}
88
+ {%- if not i.affected_code %}{{ Fore.GREEN }} ╭────{% endif -%}
89
+ {#- Wrap right for one-liner, doesn't prevent copying code -#}
90
+ {%- if i.proposal.splitlines() | length == 1 and max_line_len(i.proposal)<80 -%}
91
+ {{- Fore.GREEN + "─"*2 + "[ 💡 Proposed Change ]" + "─"*(max_line_len(i.proposal)-29) + "─╮" +"\n" -}}
92
+ {{- block_wrap_lr(i.proposal, '', ' │', 60) -}}
93
+ {{- "\n" + " ╰──"+"─"*(max_line_len(i.proposal)-5+1)+"─╯" -}}
94
+ {#- Open right side to not prevent multiline code copying -#}
95
+ {%- else -%}
96
+ {{- Fore.GREEN + "─"*2 + "[ 💡 Proposed Change ]" + "─"*([max_line_len(i.proposal)-29+2,maxlen-26-2]|min -2) + "─╮" +"\n" -}}
97
+ {{- i.proposal -}}
98
+ {{- "\n" + " ╰───"+"─"*([[max_line_len(i.proposal)-29+2,maxlen-29+1]|min - 2 + 29 - 5,29-7+2]|max)+"─╯" -}}
99
+ {%- endif -%}
100
+
101
+ {{- Style.RESET_ALL -}}
102
+ {% endif -%}
103
+ {%- endfor -%}
104
+ {{ "\n" }}
105
+ {%- endfor -%}
34
106
  """
35
107
  retries = 3
36
108
  prompt = """
@@ -49,7 +121,7 @@ Review the provided code diff carefully and identify *only* highly confident iss
49
121
 
50
122
  ----TASK GUIDELINES----
51
123
  - Only report issues you are **100% confident** are relevant to any context.
52
- - Never report issues about using software versions that have not yet been released.
124
+ - Never report issues related to software versions, model names, or similar details that you believe have not yet been released—you cannot reliably determine this.
53
125
  - Only include issues that are **significantly valuable** to the maintainers (e.g., bugs, security flaws, or clear maintainability concerns).
54
126
  - Do **not** report vague, theoretical, or overly generic advice.
55
127
  - Do **not** report anything with medium or lower confidence.
@@ -64,7 +136,7 @@ Respond with a valid JSON array of issues in the following format:
64
136
  "details": "<issue_description>",
65
137
  "tags": ["<issue_tag1>", "<issue_tag2>"],
66
138
  "severity": <issue_severity>,
67
- "confidence": <confidence_score>
139
+ "confidence": <confidence_score>,
68
140
  "affected_lines": [ // optional; list of affected lines
69
141
  {
70
142
  "start_line": <start_line:int>,
@@ -119,14 +191,16 @@ summary_prompt = """
119
191
  Summarize the code review in one sentence.
120
192
  --Reviewed Changes--
121
193
  {% for part in diff %}{{ part }}\n{% endfor %}
122
- --Detected Issues--
194
+ --Issues Detected by You--
123
195
  {{ issues | tojson(indent=2) }}
124
196
  ---
125
- If code changes contains exceptional achievements, you may additionally present the award in summary text.
197
+ If the code changes include exceptional achievements, you may also present an award to the author in the summary text.
198
+ Note: Awards should only be given to authors of initial codebase changes, not to code reviewers.
126
199
  --Available Awards--
127
200
  {{ awards }}
128
201
  ---
129
202
  - Your response will be parsed programmatically, so do not include any additional text.
203
+ - Do not include the issues by itself to the summary, they are already provided in the context.
130
204
  - Use Markdown formatting in your response.
131
205
  {{ summary_requirements -}}
132
206
  """
gito/constants.py ADDED
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ PROJECT_GITO_FOLDER = ".gito"
4
+ PROJECT_CONFIG_FILE_NAME = "config.toml"
5
+ PROJECT_CONFIG_FILE_PATH = Path(".gito") / PROJECT_CONFIG_FILE_NAME
6
+ PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE = Path(__file__).resolve().parent / PROJECT_CONFIG_FILE_NAME
7
+ HOME_ENV_PATH = Path("~/.gito/.env").expanduser()
8
+ JSON_REPORT_FILE_NAME = "code-review-report.json"
9
+ EXECUTABLE = "gito"
@@ -14,6 +14,10 @@ from .report_struct import Report
14
14
  from .constants import JSON_REPORT_FILE_NAME
15
15
 
16
16
 
17
+ def review_subject_is_index(what):
18
+ return not what or what == 'INDEX'
19
+
20
+
17
21
  def is_binary_file(repo: Repo, file_path: str) -> bool:
18
22
  """
19
23
  Check if a file is binary by attempting to read it as text.
@@ -25,7 +29,20 @@ def is_binary_file(repo: Repo, file_path: str) -> bool:
25
29
  # Try decoding as UTF-8; if it fails, it's likely binary
26
30
  content.decode("utf-8")
27
31
  return False
28
- except (UnicodeDecodeError, KeyError):
32
+ except KeyError:
33
+ try:
34
+ fs_path = Path(repo.working_tree_dir) / file_path
35
+ fs_path.read_text(encoding='utf-8')
36
+ return False
37
+ except FileNotFoundError:
38
+ logging.error(f"File {file_path} not found in the repository.")
39
+ return True
40
+ except UnicodeDecodeError:
41
+ return True
42
+ except Exception as e:
43
+ logging.error(f"Error reading file {file_path}: {e}")
44
+ return True
45
+ except UnicodeDecodeError:
29
46
  return True
30
47
  except Exception as e:
31
48
  logging.warning(f"Error checking if file {file_path} is binary: {e}")
@@ -40,11 +57,12 @@ def get_diff(
40
57
  ) -> PatchSet | list[PatchedFile]:
41
58
  repo = repo or Repo(".")
42
59
  if not against:
43
- against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
44
- if not what:
60
+ # 'origin/main', 'origin/master', etc
61
+ against = repo.remotes.origin.refs.HEAD.reference.name
62
+ if review_subject_is_index(what):
45
63
  what = None # working copy
46
64
  if use_merge_base:
47
- if what is None:
65
+ if review_subject_is_index(what):
48
66
  try:
49
67
  current_ref = repo.active_branch.name
50
68
  except TypeError:
@@ -65,7 +83,6 @@ def get_diff(
65
83
  )
66
84
  diff_content = repo.git.diff(against, what)
67
85
  diff = PatchSet.from_string(diff_content)
68
- diff = PatchSet.from_string(diff_content)
69
86
 
70
87
  # Filter out binary files
71
88
  non_binary_diff = PatchSet([])
@@ -76,7 +93,9 @@ def get_diff(
76
93
  if patched_file.target_file != DEV_NULL
77
94
  else patched_file.source_file
78
95
  )
79
- if file_path == DEV_NULL or is_binary_file(repo, file_path.lstrip("b/")):
96
+ if file_path == DEV_NULL:
97
+ continue
98
+ if is_binary_file(repo, file_path.lstrip("b/")):
80
99
  logging.info(f"Skipping binary file: {patched_file.path}")
81
100
  continue
82
101
  non_binary_diff.append(patched_file)
@@ -102,8 +121,18 @@ def filter_diff(
102
121
  return files
103
122
 
104
123
 
105
- def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
106
- text = repo.tree()[file].data_stream.read().decode()
124
+ def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: bool = False) -> str:
125
+ if use_local_files:
126
+ file_path = Path(repo.working_tree_dir) / file
127
+ try:
128
+ text = file_path.read_text(encoding='utf-8')
129
+ except (FileNotFoundError, UnicodeDecodeError) as e:
130
+ logging.warning(f"Could not read file {file} from working directory: {e}")
131
+ text = repo.tree()[file].data_stream.read().decode('utf-8')
132
+ else:
133
+ # Read from HEAD (committed version)
134
+ text = repo.tree()[file].data_stream.read().decode('utf-8')
135
+
107
136
  lines = [f"{i + 1}: {line}\n" for i, line in enumerate(text.splitlines())]
108
137
  if max_tokens:
109
138
  lines, removed_qty = mc.tokenizing.fit_to_token_size(lines, max_tokens)
@@ -135,8 +164,8 @@ async def review(
135
164
  use_merge_base: bool = True,
136
165
  out_folder: str | PathLike | None = None,
137
166
  ):
138
- cfg = ProjectConfig.load()
139
167
  repo = repo or Repo(".")
168
+ cfg = ProjectConfig.load_for_repo(repo)
140
169
  out_folder = Path(out_folder or repo.working_tree_dir)
141
170
  diff = get_diff(
142
171
  repo=repo, what=what, against=against, use_merge_base=use_merge_base
@@ -152,6 +181,7 @@ async def review(
152
181
  file_diff.path,
153
182
  cfg.max_code_tokens
154
183
  - mc.tokenizing.num_tokens_from_string(str(file_diff)),
184
+ use_local_files=review_subject_is_index(what)
155
185
  )
156
186
  if file_diff.target_file != DEV_NULL and not file_diff.is_added_file
157
187
  else ""
@@ -186,6 +216,6 @@ async def review(
186
216
  report.summary = make_cr_summary(cfg, report, diff)
187
217
  report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
188
218
  report_text = report.render(cfg, Report.Format.MARKDOWN)
189
- print(mc.ui.yellow(report_text))
190
219
  text_report_path = out_folder / "code-review-report.md"
191
220
  text_report_path.write_text(report_text, encoding="utf-8")
221
+ report.to_cli()
@@ -1,11 +1,14 @@
1
+ import re
1
2
  import logging
2
3
  import tomllib
3
4
  from dataclasses import dataclass, field
4
5
  from pathlib import Path
5
6
 
6
7
  import microcore as mc
8
+ from microcore import ui
9
+ from git import Repo
7
10
 
8
- from .constants import PROJECT_CONFIG_FILE, PROJECT_CONFIG_DEFAULTS_FILE
11
+ from .constants import PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, PROJECT_CONFIG_FILE_PATH
9
12
 
10
13
 
11
14
  def _detect_github_env() -> dict:
@@ -39,23 +42,26 @@ def _detect_github_env() -> dict:
39
42
  }
40
43
  # Fallback for local usage: try to get from git
41
44
  if not repo:
45
+ git_repo = None
42
46
  try:
43
- from git import Repo as GitRepo
44
-
45
- git = GitRepo(".", search_parent_directories=True)
46
- origin = git.remotes.origin.url
47
+ git_repo = Repo(".", search_parent_directories=True)
48
+ origin = git_repo.remotes.origin.url
47
49
  # e.g. git@github.com:Nayjest/ai-code-review.git -> Nayjest/ai-code-review
48
- import re
49
-
50
50
  match = re.search(r"[:/]([\w\-]+)/([\w\-\.]+?)(\.git)?$", origin)
51
51
  if match:
52
52
  d["github_repo"] = f"{match.group(1)}/{match.group(2)}"
53
- d["github_pr_sha"] = git.head.commit.hexsha
53
+ d["github_pr_sha"] = git_repo.head.commit.hexsha
54
54
  d["github_branch"] = (
55
- git.active_branch.name if hasattr(git, "active_branch") else ""
55
+ git_repo.active_branch.name if hasattr(git_repo, "active_branch") else ""
56
56
  )
57
57
  except Exception:
58
58
  pass
59
+ finally:
60
+ if git_repo:
61
+ try:
62
+ git_repo.close()
63
+ except Exception:
64
+ pass
59
65
  # If branch is not a commit SHA, prefer branch for links
60
66
  if d["github_branch"]:
61
67
  d["github_pr_sha_or_branch"] = d["github_branch"]
@@ -72,6 +78,8 @@ class ProjectConfig:
72
78
  summary_prompt: str = ""
73
79
  report_template_md: str = ""
74
80
  """Markdown report template"""
81
+ report_template_cli: str = ""
82
+ """Report template for CLI output"""
75
83
  post_process: str = ""
76
84
  retries: int = 3
77
85
  """LLM retries for one request"""
@@ -79,21 +87,33 @@ class ProjectConfig:
79
87
  prompt_vars: dict = field(default_factory=dict)
80
88
 
81
89
  @staticmethod
82
- def load(custom_config_file: str | Path | None = None) -> "ProjectConfig":
83
- config_file = Path(custom_config_file or PROJECT_CONFIG_FILE)
84
- with open(PROJECT_CONFIG_DEFAULTS_FILE, "rb") as f:
90
+ def _read_bundled_defaults() -> dict:
91
+ with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
85
92
  config = tomllib.load(f)
93
+ return config
94
+
95
+ @staticmethod
96
+ def load_for_repo(repo: Repo):
97
+ return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
98
+
99
+ @staticmethod
100
+ def load(config_path: str | Path | None = None) -> "ProjectConfig":
101
+ config = ProjectConfig._read_bundled_defaults()
86
102
  github_env = _detect_github_env()
87
103
  config["prompt_vars"] |= github_env | dict(github_env=github_env)
88
- if config_file.exists():
104
+
105
+ config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
106
+ if config_path.exists():
89
107
  logging.info(
90
- f"Loading project-specific configuration from {mc.utils.file_link(config_file)}...")
108
+ f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
91
109
  default_prompt_vars = config["prompt_vars"]
92
- with open(config_file, "rb") as f:
110
+ with open(config_path, "rb") as f:
93
111
  config.update(tomllib.load(f))
94
112
  # overriding prompt_vars config section will not empty default values
95
113
  config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
96
114
  else:
97
- logging.info(f"Config file {config_file} not found, using defaults")
115
+ logging.info(
116
+ f"No project config found at {ui.blue(config_path)}, using defaults"
117
+ )
98
118
 
99
119
  return ProjectConfig(**config)
@@ -3,12 +3,16 @@ import logging
3
3
  from dataclasses import dataclass, field, asdict
4
4
  from datetime import datetime
5
5
  from enum import StrEnum
6
+ from pathlib import Path
6
7
 
7
8
  import microcore as mc
9
+ from colorama import Fore, Style, Back
10
+ from microcore.utils import file_link
11
+ import textwrap
8
12
 
9
13
  from .constants import JSON_REPORT_FILE_NAME
10
14
  from .project_config import ProjectConfig
11
- from .utils import syntax_hint
15
+ from .utils import syntax_hint, block_wrap_lr, max_line_len
12
16
 
13
17
 
14
18
  @dataclass
@@ -57,6 +61,7 @@ class Issue:
57
61
  class Report:
58
62
  class Format(StrEnum):
59
63
  MARKDOWN = "md"
64
+ CLI = "cli"
60
65
 
61
66
  issues: dict = field(default_factory=dict)
62
67
  summary: str = field(default="")
@@ -94,15 +99,34 @@ class Report:
94
99
  logging.info(f"Report saved to {mc.utils.file_link(file_name)}")
95
100
 
96
101
  @staticmethod
97
- def load(file_name: str = ""):
102
+ def load(file_name: str | Path = ""):
98
103
  with open(file_name or JSON_REPORT_FILE_NAME, "r") as f:
99
104
  data = json.load(f)
100
105
  data.pop("total_issues", None)
101
106
  return Report(**data)
102
107
 
103
108
  def render(
104
- self, cfg: ProjectConfig = None, format: Format = Format.MARKDOWN
109
+ self,
110
+ config: ProjectConfig = None,
111
+ report_format: Format = Format.MARKDOWN,
105
112
  ) -> str:
106
- cfg = cfg or ProjectConfig.load()
107
- template = getattr(cfg, f"report_template_{format}")
108
- return mc.prompt(template, report=self, **cfg.prompt_vars)
113
+ config = config or ProjectConfig.load()
114
+ template = getattr(config, f"report_template_{report_format}")
115
+ return mc.prompt(
116
+ template,
117
+ report=self,
118
+ ui=mc.ui,
119
+ Fore=Fore,
120
+ Style=Style,
121
+ Back=Back,
122
+ file_link=file_link,
123
+ textwrap=textwrap,
124
+ block_wrap_lr=block_wrap_lr,
125
+ max_line_len=max_line_len,
126
+ **config.prompt_vars
127
+ )
128
+
129
+ def to_cli(self, report_format=Format.CLI):
130
+ output = self.render(report_format=report_format)
131
+ print("")
132
+ print(output)
@@ -112,5 +112,21 @@ def parse_refs_pair(refs: str) -> tuple[str | None, str | None]:
112
112
  return None, None
113
113
  if SEPARATOR not in refs:
114
114
  return refs, None
115
- what, against = refs.split(SEPARATOR)
115
+ what, against = refs.split(SEPARATOR, 1)
116
116
  return what or None, against or None
117
+
118
+
119
+ def max_line_len(text: str) -> int:
120
+ return max((len(line) for line in text.splitlines()), default=0)
121
+
122
+
123
+ def block_wrap_lr(text: str, left: str = "", right: str = "", max_rwrap: int = 60) -> str:
124
+ ml = max_line_len(text)
125
+ lines = text.splitlines()
126
+ wrapped_lines = []
127
+ for line in lines:
128
+ ln = left+line
129
+ if ml <= max_rwrap:
130
+ ln += ' ' * (ml - len(line)) + right
131
+ wrapped_lines.append(ln)
132
+ return "\n".join(wrapped_lines)
@@ -1,7 +0,0 @@
1
- from pathlib import Path
2
-
3
-
4
- PROJECT_CONFIG_FILE = Path(".ai-code-review.toml")
5
- PROJECT_CONFIG_DEFAULTS_FILE = Path(__file__).resolve().parent / PROJECT_CONFIG_FILE
6
- ENV_CONFIG_FILE = Path("~/.env.ai-code-review").expanduser()
7
- JSON_REPORT_FILE_NAME = "code-review-report.json"
@@ -1,197 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: ai-cr
3
- Version: 1.0.0
4
- Summary: LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions
5
- License: MIT
6
- Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
7
- Author: Nayjest
8
- Author-email: mail@vitaliy.in
9
- Requires-Python: >=3.11,<4.0
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Topic :: Software Development
18
- Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
- Requires-Dist: ai-microcore (==4.0.0.dev19)
20
- Requires-Dist: anthropic (>=0.52.2,<0.53.0)
21
- Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
22
- Requires-Dist: typer (>=0.16.0,<0.17.0)
23
- Requires-Dist: unidiff (>=0.7.5,<0.8.0)
24
- Project-URL: Homepage, https://github.com/Nayjest/github-ai-code-review
25
- Project-URL: Repository, https://github.com/Nayjest/github-ai-code-review
26
- Description-Content-Type: text/markdown
27
-
28
- <p align="right">
29
- <a href="https://pypi.org/project/ai-code-review/" target="_blank"><img src="https://badge.fury.io/py/ai-code-review.svg" alt="PYPI Release"></a>
30
- <a href="https://github.com/Nayjest/ai-code-review/actions/workflows/code-style.yml" target="_blank"><img src="https://github.com/Nayjest/ai-code-review/actions/workflows/code-style.yml/badge.svg" alt="Pylint"></a>
31
- <a href="https://github.com/Nayjest/ai-code-review/actions/workflows/tests.yml" target="_blank"><img src="https://github.com/Nayjest/ai-code-review/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
32
- <img src="https://github.com/Nayjest/ai-code-review/blob/main/coverage.svg" alt="Code Coverage">
33
- <a href="https://github.com/Nayjest/ai-code-review/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/static/v1?label=license&message=MIT&color=d08aff" alt="License"></a>
34
- </p>
35
-
36
- # 🤖 AI Code Review Tool
37
-
38
- An AI-powered GitHub code review tool that uses LLMs to detect high-confidence, high-impact issues—such as security vulnerabilities, bugs, and maintainability concerns.
39
-
40
- ## ✨ Features
41
-
42
- - Automatically reviews pull requests via GitHub Actions
43
- - Focuses on critical issues (e.g., bugs, security risks, design flaws)
44
- - Posts review results as a comment on your PR
45
- - Can be used locally; works with both local and remote Git repositories
46
- - Optional, fun AI-generated code awards 🏆
47
- - Easily configurable via [`.ai-code-review.toml`](https://github.com/Nayjest/ai-code-review/blob/main/ai_code_review/.ai-code-review.toml) in your repository root
48
- - Extremely fast, parallel LLM usage
49
- - Model-agnostic (OpenAI, Anthropic, Google, local PyTorch inference, etc.)
50
-
51
- See code review in action: [example](https://github.com/Nayjest/ai-code-review/pull/39#issuecomment-2906968729)
52
-
53
- ## 🚀 Quickstart
54
-
55
- ### 1. Review Pull Requests via GitHub Actions
56
-
57
- Create a `.github/workflows/ai-code-review.yml` file:
58
-
59
- ```yaml
60
- name: AI Code Review
61
- on: { pull_request: { types: [opened, synchronize, reopened] } }
62
- jobs:
63
- review:
64
- runs-on: ubuntu-latest
65
- permissions: { contents: read, pull-requests: write } # 'write' for leaving the summary comment
66
- steps:
67
- - uses: actions/checkout@v4
68
- with: { fetch-depth: 0 }
69
- - name: Set up Python
70
- uses: actions/setup-python@v5
71
- with: { python-version: "3.13" }
72
- - name: Install AI Code Review tool
73
- run: pip install ai-code-review~=1.0
74
- - name: Run AI code analysis
75
- env:
76
- LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
77
- LLM_API_TYPE: openai
78
- MODEL: "gpt-4.1"
79
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80
- run: |
81
- ai-code-review
82
- ai-code-review github-comment --token ${{ secrets.GITHUB_TOKEN }}
83
- - uses: actions/upload-artifact@v4
84
- with:
85
- name: ai-code-review-results
86
- path: |
87
- code-review-report.md
88
- code-review-report.json
89
- ```
90
-
91
- > ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
92
-
93
- 💪 Done!
94
- PRs to your repository will now receive AI code reviews automatically. ✨
95
- See [GitHub Setup Guide](https://github.com/Nayjest/ai-code-review/blob/main/documentation/github_setup.md) for more details.
96
-
97
- ### 2. Running Code Analysis Locally
98
-
99
- #### Initial Local Setup
100
-
101
- **Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
102
-
103
- **Step1:** Install [ai-code-review](https://github.com/Nayjest/ai-code-review) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
104
- ```bash
105
- pip install ai-code-review
106
- ```
107
-
108
- > **Troubleshooting:**
109
- > pip may be also available via cli as `pip3` depending on your Python installation.
110
-
111
- **Step2:** Perform initial setup
112
-
113
- The following command will perform one-time setup using an interactive wizard.
114
- You will be prompted to enter LLM configuration details (API type, API key, etc).
115
- Configuration will be saved to ~/.env.ai-code-review.
116
-
117
- ```bash
118
- ai-code-review setup
119
- ```
120
-
121
- > **Troubleshooting:**
122
- > On some systems, `ai-code-review` command may not became available immediately after installation.
123
- > Try restarting your terminal or running `python -m ai_code_review` instead.
124
-
125
-
126
- #### Perform your first AI code review locally
127
-
128
- **Step1:** Navigate to your repository root directory.
129
- **Step2:** Switch to the branch you want to review.
130
- **Step3:** Run following command
131
- ```bash
132
- ai-code-review
133
- ```
134
-
135
- > **Note:** This will analyze the current branch against the repository main branch by default.
136
- > Files that are not staged for commit will be ignored.
137
- > See `ai-code-review --help` for more options.
138
-
139
- **Reviewing remote repository**
140
-
141
- ```bash
142
- ai-code-review remote git@github.com:owner/repo.git <FEATURE_BRANCH>..<MAIN_BRANCH>
143
- ```
144
- Use interactive help for details:
145
- ```bash
146
- ai-code-review remote --help
147
- ```
148
-
149
- ## 🔧 Configuration
150
-
151
- Change behavior via `.ai-code-review.toml`:
152
-
153
- - Prompt templates, filtering and post-processing using Python code snippets
154
- - Tagging, severity, and confidence settings
155
- - Custom AI awards for developer brilliance
156
- - Output customization
157
-
158
- You can override the default config by placing `.ai-code-review.toml` in your repo root.
159
-
160
-
161
- See default configuration [here](https://github.com/Nayjest/ai-code-review/blob/main/ai_code_review/.ai-code-review.toml).
162
-
163
- More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/ai-code-review/blob/main/documentation/config_cookbook.md)
164
-
165
- ## 💻 Development Setup
166
-
167
- Install dependencies:
168
-
169
- ```bash
170
- make install
171
- ```
172
-
173
- Format code and check style:
174
-
175
- ```bash
176
- make black
177
- make cs
178
- ```
179
-
180
- Run tests:
181
-
182
- ```bash
183
- pytest
184
- ```
185
-
186
- ## 🤝 Contributing
187
-
188
- **Looking for a specific feature or having trouble?**
189
- Contributions are welcome! ❤️
190
- See [CONTRIBUTING.md](https://github.com/Nayjest/ai-code-review/blob/main/CONTRIBUTING.md) for details.
191
-
192
- ## 📝 License
193
-
194
- Licensed under the [MIT License](https://github.com/Nayjest/ai-code-review/blob/main/LICENSE).
195
-
196
- © 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
197
-
@@ -1,16 +0,0 @@
1
- ai_code_review/.ai-code-review.toml,sha256=sIQt7VJWl937VLocF2Vg6bC4qGbHeBgyKF6Bj3XKwIk,10640
2
- ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- ai_code_review/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
4
- ai_code_review/bootstrap.py,sha256=jqioR_UtTsn5nXezmjMLU3aB8tzlVz73ZRBk33ud5F4,2336
5
- ai_code_review/cli.py,sha256=9OWQP2voQOfrhVfJsCzP-nQ9RtLU1cHhMi81QY56vzc,7441
6
- ai_code_review/commands/repl.py,sha256=Ms5p6vgcf0EBAUUWKQfJu3X9XFvzJXB018qcvSiJ-oI,396
7
- ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
8
- ai_code_review/core.py,sha256=BJNs4ZER2-bMulXY2apY6B6hI0nvRCOrLqsJ7L8Bizc,6693
9
- ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
10
- ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
11
- ai_code_review/utils.py,sha256=vlzU3M89qK6_mVkBMnppZaOFsXddVsIBVdfmbN3cxDY,2939
12
- ai_cr-1.0.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
13
- ai_cr-1.0.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
14
- ai_cr-1.0.0.dist-info/METADATA,sha256=SPJm5aNOWFdwCI7KxtDE2AspsP2Kdn5NLwV6DeBJIeg,7109
15
- ai_cr-1.0.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
16
- ai_cr-1.0.0.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- ai-cr=ai_code_review.cli:main
3
-
File without changes
File without changes