ai-cr 0.5.0__py3-none-any.whl → 1.0.0__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.
@@ -38,21 +38,23 @@ prompt = """
38
38
  ----TASK----
39
39
  Review the provided code diff carefully and identify *only* highly confident issues which are relevant to any code context.
40
40
 
41
- ----CODE----
41
+ ----CODEBASE CHANGES TO REVIEW----
42
42
  {{ input }}
43
43
  --------
44
44
 
45
45
  {% if file_lines -%}
46
- ----ADDITIONAL CONTEXT----
46
+ ----ADDITIONAL CONTEXT: FULL FILE CONTENT AFTER APPLYING REVIEWED CHANGES----
47
47
  {{ file_lines }}
48
48
  {%- endif %}
49
49
 
50
50
  ----TASK GUIDELINES----
51
51
  - 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.
52
53
  - Only include issues that are **significantly valuable** to the maintainers (e.g., bugs, security flaws, or clear maintainability concerns).
53
54
  - Do **not** report vague, theoretical, or overly generic advice.
54
55
  - Do **not** report anything with medium or lower confidence.
55
- - Typographical errors have highest severity
56
+ - Typographical errors have highest severity.
57
+ {{ requirements -}}
56
58
  {{ json_requirements }}
57
59
 
58
60
  Respond with a valid JSON array of issues in the following format:
@@ -124,8 +126,9 @@ If code changes contains exceptional achievements, you may additionally present
124
126
  --Available Awards--
125
127
  {{ awards }}
126
128
  ---
127
- Your response will be parsed programmatically, so do not include any additional text.
128
- Use Markdown formatting in your response.
129
+ - Your response will be parsed programmatically, so do not include any additional text.
130
+ - Use Markdown formatting in your response.
131
+ {{ summary_requirements -}}
129
132
  """
130
133
 
131
134
  [prompt_vars]
@@ -303,4 +306,6 @@ restored lost knowledge and now bestow it upon a new generation."
303
306
  decorators add depth and texture, and observer masterfully completes the composition.
304
307
  The Gang of Four gives a standing ovation from the stalls."
305
308
  ```
306
- """
309
+ """
310
+ requirements = ""
311
+ summary_requirements = ""
@@ -1,4 +1,4 @@
1
- from .cli import main
2
-
3
- if __name__ == "__main__":
4
- main()
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -59,4 +59,4 @@ def bootstrap():
59
59
  except Exception as e:
60
60
  logging.error(f"Unexpected configuration error: {e}")
61
61
  raise SystemExit(3)
62
- mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [100, 15]
62
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
ai_code_review/cli.py CHANGED
@@ -2,64 +2,44 @@ import asyncio
2
2
  import logging
3
3
  import sys
4
4
  import os
5
- import shutil
5
+ import textwrap
6
+ import tempfile
6
7
  import requests
7
8
 
8
9
  import microcore as mc
9
- import async_typer
10
10
  import typer
11
- from ai_code_review.utils import parse_refs_pair
12
11
  from git import Repo
13
12
 
14
- from .core import review
13
+ from .core import review, get_diff, filter_diff
15
14
  from .report_struct import Report
16
15
  from .constants import ENV_CONFIG_FILE
17
16
  from .bootstrap import bootstrap
18
17
  from .project_config import ProjectConfig
19
- from .utils import is_app_command_invocation
18
+ from .utils import no_subcommand, parse_refs_pair
20
19
 
21
-
22
- app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
23
- default_command_app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
24
- if sys.platform == "win32":
25
- asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
20
+ app = typer.Typer(pretty_exceptions_show_locals=False)
21
+ app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
26
22
 
27
23
 
28
24
  def main():
29
- if is_app_command_invocation(app):
30
- app()
31
- else:
25
+ if sys.platform == "win32":
26
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
27
+ if no_subcommand(app):
32
28
  bootstrap()
33
- default_command_app()
29
+ app_no_subcommand()
30
+ else:
31
+ app()
34
32
 
35
33
 
36
34
  @app.callback(invoke_without_command=True)
37
- def cli(ctx: typer.Context):
35
+ def cli(ctx: typer.Context, verbose: bool = typer.Option(default=False)):
36
+ if verbose:
37
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
38
38
  if ctx.invoked_subcommand != "setup":
39
39
  bootstrap()
40
40
 
41
41
 
42
- @default_command_app.async_command(name="review", help="Perform code review")
43
- @app.async_command(name="review", help="Perform code review")
44
- async def cmd_review(
45
- refs: str = typer.Argument(
46
- default=None,
47
- help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
48
- ),
49
- what: str = typer.Option(None, "--what", "-w", help="Git ref to review"),
50
- against: str = typer.Option(
51
- None,
52
- "--against", "-vs", "--vs",
53
- help="Git ref to compare against"
54
- ),
55
- filters: str = typer.Option(
56
- "", "--filter", "-f", "--filters",
57
- help="""
58
- filter reviewed files by glob / fnmatch pattern(s),
59
- e.g. 'src/**/*.py', may be comma-separated
60
- """,
61
- )
62
- ):
42
+ def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
63
43
  _what, _against = parse_refs_pair(refs)
64
44
  if _what:
65
45
  if what:
@@ -75,34 +55,103 @@ async def cmd_review(
75
55
  )
76
56
  else:
77
57
  _against = against
78
- await review(what=_what, against=_against, filters=filters)
58
+ return _what, _against
79
59
 
80
60
 
81
- @app.async_command(help="Configure LLM for local usage interactively")
82
- async def setup():
83
- mc.interactive_setup(ENV_CONFIG_FILE)
61
+ def arg_refs() -> typer.Argument:
62
+ return typer.Argument(
63
+ default=None,
64
+ help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
65
+ )
84
66
 
85
67
 
86
- @app.async_command()
87
- async def render(format: str = Report.Format.MARKDOWN):
88
- print(Report.load().render(format=format))
68
+ def arg_what() -> typer.Option:
69
+ return typer.Option(None, "--what", "-w", help="Git ref to review")
89
70
 
90
71
 
91
- @app.async_command(help="Review remote code")
92
- async def remote(url=typer.Option(), branch=typer.Option()):
93
- if os.path.exists("reviewed-repo"):
94
- shutil.rmtree("reviewed-repo")
95
- Repo.clone_from(url, branch=branch, to_path="reviewed-repo")
96
- prev_dir = os.getcwd()
97
- try:
98
- os.chdir("reviewed-repo")
99
- await review()
100
- finally:
101
- os.chdir(prev_dir)
72
+ def arg_filters() -> typer.Option:
73
+ return typer.Option(
74
+ "", "--filter", "-f", "--filters",
75
+ help="""
76
+ filter reviewed files by glob / fnmatch pattern(s),
77
+ e.g. 'src/**/*.py', may be comma-separated
78
+ """,
79
+ )
102
80
 
103
81
 
104
- @app.async_command(help="Leave a GitHub PR comment with the review.")
105
- async def github_comment(
82
+ def arg_out() -> typer.Option:
83
+ return typer.Option(
84
+ None,
85
+ "--out", "-o", "--output",
86
+ help="Output folder for the code review report"
87
+ )
88
+
89
+
90
+ def arg_against() -> typer.Option:
91
+ return typer.Option(
92
+ None,
93
+ "--against", "-vs", "--vs",
94
+ help="Git ref to compare against"
95
+ )
96
+
97
+
98
+ @app_no_subcommand.command(name="review", help="Perform code review")
99
+ @app.command(name="review", help="Perform code review")
100
+ def cmd_review(
101
+ refs: str = arg_refs(),
102
+ what: str = arg_what(),
103
+ against: str = arg_against(),
104
+ filters: str = arg_filters(),
105
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
106
+ out: str = arg_out()
107
+ ):
108
+ _what, _against = args_to_target(refs, what, against)
109
+ asyncio.run(review(
110
+ what=_what,
111
+ against=_against,
112
+ filters=filters,
113
+ use_merge_base=merge_base,
114
+ out_folder=out,
115
+ ))
116
+
117
+
118
+ @app.command(help="Configure LLM for local usage interactively")
119
+ 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))
126
+
127
+
128
+ @app.command(help="Review remote code")
129
+ def remote(
130
+ url: str = typer.Argument(..., help="Git repository URL"),
131
+ refs: str = arg_refs(),
132
+ what: str = arg_what(),
133
+ against: str = arg_against(),
134
+ filters: str = arg_filters(),
135
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
136
+ out: str = arg_out()
137
+ ):
138
+ _what, _against = args_to_target(refs, what, against)
139
+ with tempfile.TemporaryDirectory() as temp_dir:
140
+ logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
141
+ repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
142
+ asyncio.run(review(
143
+ repo=repo,
144
+ what=_what,
145
+ against=_against,
146
+ filters=filters,
147
+ use_merge_base=merge_base,
148
+ out_folder=out or '.',
149
+ ))
150
+ repo.close()
151
+
152
+
153
+ @app.command(help="Leave a GitHub PR comment with the review.")
154
+ def github_comment(
106
155
  token: str = typer.Option(
107
156
  os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
108
157
  ),
@@ -110,7 +159,7 @@ async def github_comment(
110
159
  """
111
160
  Leaves a comment with the review on the current GitHub pull request.
112
161
  """
113
- file = "code-review-report.txt"
162
+ file = "code-review-report.md"
114
163
  if not os.path.exists(file):
115
164
  print(f"Review file not found: {file}")
116
165
  raise typer.Exit(4)
@@ -155,3 +204,35 @@ async def github_comment(
155
204
  else:
156
205
  logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
157
206
  raise typer.Exit(5)
207
+
208
+
209
+ @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
210
+ def files(
211
+ refs: str = arg_refs(),
212
+ what: str = arg_what(),
213
+ against: str = arg_against(),
214
+ filters: str = arg_filters(),
215
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
216
+ diff: bool = typer.Option(default=False, help="Show diff content")
217
+ ):
218
+ _what, _against = args_to_target(refs, what, against)
219
+ repo = Repo(".")
220
+ patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
221
+ patch_set = filter_diff(patch_set, filters)
222
+ print(
223
+ f"Changed files: "
224
+ f"{mc.ui.green(_what or 'INDEX')} vs "
225
+ f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
226
+ f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
227
+ )
228
+
229
+ for patch in patch_set:
230
+ if patch.is_added_file:
231
+ color = mc.ui.green
232
+ elif patch.is_removed_file:
233
+ color = mc.ui.red
234
+ else:
235
+ color = mc.ui.blue
236
+ print(f"- {color(patch.path)}")
237
+ if diff:
238
+ print(mc.ui.gray(textwrap.indent(str(patch), " ")))
@@ -0,0 +1,22 @@
1
+ """
2
+ Python REPL
3
+ """
4
+ # flake8: noqa: F401
5
+ import code
6
+ from ema.cli import app
7
+
8
+ # Imports for usage in REPL
9
+ import os
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from time import time
15
+ from rich.pretty import pprint
16
+
17
+ import microcore as mc
18
+ from microcore import ui
19
+
20
+ @app.command(help="python REPL")
21
+ def repl():
22
+ code.interact(local=globals())
ai_code_review/core.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import fnmatch
2
2
  import logging
3
+ from os import PathLike
3
4
  from typing import Iterable
5
+ from pathlib import Path
4
6
 
5
7
  import microcore as mc
6
8
  from git import Repo
@@ -9,22 +11,76 @@ from unidiff.constants import DEV_NULL
9
11
 
10
12
  from .project_config import ProjectConfig
11
13
  from .report_struct import Report
14
+ from .constants import JSON_REPORT_FILE_NAME
15
+
16
+
17
+ def is_binary_file(repo: Repo, file_path: str) -> bool:
18
+ """
19
+ Check if a file is binary by attempting to read it as text.
20
+ Returns True if the file is binary, False otherwise.
21
+ """
22
+ try:
23
+ # Attempt to read the file content from the repository tree
24
+ content = repo.tree()[file_path].data_stream.read()
25
+ # Try decoding as UTF-8; if it fails, it's likely binary
26
+ content.decode("utf-8")
27
+ return False
28
+ except (UnicodeDecodeError, KeyError):
29
+ return True
30
+ except Exception as e:
31
+ logging.warning(f"Error checking if file {file_path} is binary: {e}")
32
+ return True # Conservatively treat errors as binary to avoid issues
12
33
 
13
34
 
14
35
  def get_diff(
15
36
  repo: Repo = None,
16
37
  what: str = None,
17
- against: str = None
38
+ against: str = None,
39
+ use_merge_base: bool = True,
18
40
  ) -> PatchSet | list[PatchedFile]:
19
41
  repo = repo or Repo(".")
20
42
  if not against:
21
43
  against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
22
44
  if not what:
23
45
  what = None # working copy
24
- logging.info(f"Reviewing {mc.ui.green(what or 'working copy')} vs {mc.ui.yellow(against)}")
46
+ if use_merge_base:
47
+ if what is None:
48
+ try:
49
+ current_ref = repo.active_branch.name
50
+ except TypeError:
51
+ # In detached HEAD state, use HEAD directly
52
+ current_ref = "HEAD"
53
+ logging.info(
54
+ "Detected detached HEAD state, using HEAD as current reference"
55
+ )
56
+ else:
57
+ current_ref = what
58
+ merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
59
+ against = merge_base.hexsha
60
+ logging.info(
61
+ f"Using merge base: {mc.ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
62
+ )
63
+ logging.info(
64
+ f"Making diff: {mc.ui.green(what or 'INDEX')} vs {mc.ui.yellow(against)}"
65
+ )
25
66
  diff_content = repo.git.diff(against, what)
26
67
  diff = PatchSet.from_string(diff_content)
27
- return diff
68
+ diff = PatchSet.from_string(diff_content)
69
+
70
+ # Filter out binary files
71
+ non_binary_diff = PatchSet([])
72
+ for patched_file in diff:
73
+ # Check if the file is binary using the source or target file path
74
+ file_path = (
75
+ patched_file.target_file
76
+ if patched_file.target_file != DEV_NULL
77
+ else patched_file.source_file
78
+ )
79
+ if file_path == DEV_NULL or is_binary_file(repo, file_path.lstrip("b/")):
80
+ logging.info(f"Skipping binary file: {patched_file.path}")
81
+ continue
82
+ non_binary_diff.append(patched_file)
83
+ return non_binary_diff
28
84
 
29
85
 
30
86
  def filter_diff(
@@ -33,7 +89,6 @@ def filter_diff(
33
89
  """
34
90
  Filter the diff files by the given fnmatch filters.
35
91
  """
36
- print([f.path for f in patch_set])
37
92
  assert isinstance(filters, (list, str))
38
93
  if not isinstance(filters, list):
39
94
  filters = [f.strip() for f in filters.split(",") if f.strip()]
@@ -44,7 +99,6 @@ def filter_diff(
44
99
  for file in patch_set
45
100
  if any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
46
101
  ]
47
- print([f.path for f in files])
48
102
  return files
49
103
 
50
104
 
@@ -61,22 +115,32 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
61
115
 
62
116
 
63
117
  def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
64
- return mc.prompt(
65
- cfg.summary_prompt,
66
- diff=mc.tokenizing.fit_to_token_size(diff, cfg.max_code_tokens)[0],
67
- issues=report.issues,
68
- **cfg.prompt_vars,
69
- ).to_llm() if cfg.summary_prompt else ""
118
+ return (
119
+ mc.prompt(
120
+ cfg.summary_prompt,
121
+ diff=mc.tokenizing.fit_to_token_size(diff, cfg.max_code_tokens)[0],
122
+ issues=report.issues,
123
+ **cfg.prompt_vars,
124
+ ).to_llm()
125
+ if cfg.summary_prompt
126
+ else ""
127
+ )
70
128
 
71
129
 
72
130
  async def review(
131
+ repo: Repo = None,
73
132
  what: str = None,
74
133
  against: str = None,
75
- filters: str | list[str] = ""
134
+ filters: str | list[str] = "",
135
+ use_merge_base: bool = True,
136
+ out_folder: str | PathLike | None = None,
76
137
  ):
77
138
  cfg = ProjectConfig.load()
78
- repo = Repo(".")
79
- diff = get_diff(repo=repo, what=what, against=against)
139
+ repo = repo or Repo(".")
140
+ out_folder = Path(out_folder or repo.working_tree_dir)
141
+ diff = get_diff(
142
+ repo=repo, what=what, against=against, use_merge_base=use_merge_base
143
+ )
80
144
  diff = filter_diff(diff, filters)
81
145
  if not diff:
82
146
  logging.error("Nothing to review")
@@ -114,12 +178,14 @@ async def review(
114
178
  if lines[file]:
115
179
  f_lines = [""] + lines[file].splitlines()
116
180
  i["affected_code"] = "\n".join(
117
- f_lines[i["start_line"]: i["end_line"]+1]
181
+ f_lines[i["start_line"]: i["end_line"] + 1]
118
182
  )
119
183
  exec(cfg.post_process, {"mc": mc, **locals()})
184
+ out_folder.mkdir(parents=True, exist_ok=True)
120
185
  report = Report(issues=issues, number_of_processed_files=len(diff))
121
186
  report.summary = make_cr_summary(cfg, report, diff)
122
- report.save()
187
+ report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
123
188
  report_text = report.render(cfg, Report.Format.MARKDOWN)
124
189
  print(mc.ui.yellow(report_text))
125
- open("code-review-report.txt", "w", encoding="utf-8").write(report_text)
190
+ text_report_path = out_folder / "code-review-report.md"
191
+ text_report_path.write_text(report_text, encoding="utf-8")
ai_code_review/utils.py CHANGED
@@ -92,11 +92,11 @@ def is_running_in_github_action():
92
92
  return os.getenv("GITHUB_ACTIONS") == "true"
93
93
 
94
94
 
95
- def is_app_command_invocation(app: typer.Typer) -> bool:
95
+ def no_subcommand(app: typer.Typer) -> bool:
96
96
  """
97
97
  Checks if the current script is being invoked as a command in a target Typer application.
98
98
  """
99
- return (
99
+ return not (
100
100
  (first_arg := next((a for a in sys.argv[1:] if not a.startswith('-')), None))
101
101
  and first_arg in (
102
102
  cmd.name or cmd.callback.__name__.replace('_', '-')
@@ -106,7 +106,7 @@ def is_app_command_invocation(app: typer.Typer) -> bool:
106
106
  )
107
107
 
108
108
 
109
- def parse_refs_pair(refs: str):
109
+ def parse_refs_pair(refs: str) -> tuple[str | None, str | None]:
110
110
  SEPARATOR = '..'
111
111
  if not refs:
112
112
  return None, None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-cr
3
- Version: 0.5.0
3
+ Version: 1.0.0
4
4
  Summary: LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions
5
5
  License: MIT
6
6
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
@@ -15,13 +15,12 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Software Development
18
- Requires-Dist: GitPython (==3.1.44)
19
- Requires-Dist: ai-microcore (==4.0.0.dev18)
20
- Requires-Dist: anthropic (==0.52.2)
21
- Requires-Dist: async-typer (==0.1.8)
22
- Requires-Dist: google-generativeai (==0.8.5)
23
- Requires-Dist: typer (==0.9.4)
24
- Requires-Dist: unidiff (==0.7.5)
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)
25
24
  Project-URL: Homepage, https://github.com/Nayjest/github-ai-code-review
26
25
  Project-URL: Repository, https://github.com/Nayjest/github-ai-code-review
27
26
  Description-Content-Type: text/markdown
@@ -30,6 +29,7 @@ Description-Content-Type: text/markdown
30
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>
31
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>
32
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
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
34
  </p>
35
35
 
@@ -70,7 +70,7 @@ jobs:
70
70
  uses: actions/setup-python@v5
71
71
  with: { python-version: "3.13" }
72
72
  - name: Install AI Code Review tool
73
- run: pip install ai-code-review==0.5.0
73
+ run: pip install ai-code-review~=1.0
74
74
  - name: Run AI code analysis
75
75
  env:
76
76
  LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
@@ -84,34 +84,66 @@ jobs:
84
84
  with:
85
85
  name: ai-code-review-results
86
86
  path: |
87
- code-review-report.txt
87
+ code-review-report.md
88
88
  code-review-report.json
89
89
  ```
90
90
 
91
91
  > ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
92
92
 
93
93
  💪 Done!
94
- PRs to your repository will now receive AI code reviews automatically. ✨
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.
95
96
 
96
- ### 2. Run Code Analysis Locally
97
+ ### 2. Running Code Analysis Locally
97
98
 
98
- Install and run:
99
+ #### Initial Local Setup
99
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)).
100
104
  ```bash
101
- # Prerequisites: Python 3.11+
102
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
103
112
 
104
- # One-time setup using interactive wizard (saves configuration in ~/.env.ai-code-review)
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
105
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.
106
124
 
107
- # Run review on committed changes in current branch vs main
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
108
132
  ai-code-review
109
133
  ```
110
134
 
111
- To review a remote repository:
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**
112
140
 
113
141
  ```bash
114
- ai-code-review remote --url https://github.com/owner/repo --branch feature-branch
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
115
147
  ```
116
148
 
117
149
  ## 🔧 Configuration
@@ -128,6 +160,8 @@ You can override the default config by placing `.ai-code-review.toml` in your re
128
160
 
129
161
  See default configuration [here](https://github.com/Nayjest/ai-code-review/blob/main/ai_code_review/.ai-code-review.toml).
130
162
 
163
+ More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/ai-code-review/blob/main/documentation/config_cookbook.md)
164
+
131
165
  ## 💻 Development Setup
132
166
 
133
167
  Install dependencies:
@@ -0,0 +1,16 @@
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,15 +0,0 @@
1
- ai_code_review/.ai-code-review.toml,sha256=z-3iliCvkkpdAr9l_yUyM-IbgRm9hbAA3CIV6ocuurY,10378
2
- ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- ai_code_review/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
4
- ai_code_review/bootstrap.py,sha256=b9BlhRgliwsjmWRCCZcsTivsOw9AbXOf8_AZYjQTx-c,2336
5
- ai_code_review/cli.py,sha256=L1utFMHs4D9HVcuJ1VC96PEvP6q26UHsfCz_Lq49VMg,4893
6
- ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
7
- ai_code_review/core.py,sha256=WMGU2zwZTJ1P6PkUId-RJP7KHQbzB31GKSnI5wsedcA,4177
8
- ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
9
- ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
10
- ai_code_review/utils.py,sha256=UzFp2ShNzQvsydxJ98bdr_j2rBUHcm62pPtl6hBuZD4,2914
11
- ai_cr-0.5.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
12
- ai_cr-0.5.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
13
- ai_cr-0.5.0.dist-info/METADATA,sha256=k3FaB6nbVV0xbgZYPg_VQ6zMauB65Xy6CLWI4qFOMF0,5609
14
- ai_cr-0.5.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
15
- ai_cr-0.5.0.dist-info/RECORD,,
File without changes
File without changes