ai-cr 0.4.8__py3-none-any.whl → 0.6.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.
- ai_code_review/__main__.py +4 -0
- ai_code_review/bootstrap.py +5 -2
- ai_code_review/cli.py +157 -33
- ai_code_review/constants.py +1 -1
- ai_code_review/core.py +97 -20
- ai_code_review/utils.py +27 -0
- {ai_cr-0.4.8.dist-info → ai_cr-0.6.0.dist-info}/METADATA +12 -11
- ai_cr-0.6.0.dist-info/RECORD +15 -0
- ai_cr-0.6.0.dist-info/entry_points.txt +3 -0
- ai_cr-0.4.8.dist-info/RECORD +0 -14
- ai_cr-0.4.8.dist-info/entry_points.txt +0 -3
- {ai_cr-0.4.8.dist-info → ai_cr-0.6.0.dist-info}/LICENSE +0 -0
- {ai_cr-0.4.8.dist-info → ai_cr-0.6.0.dist-info}/WHEEL +0 -0
ai_code_review/bootstrap.py
CHANGED
@@ -41,7 +41,10 @@ def bootstrap():
|
|
41
41
|
if is_running_in_github_action():
|
42
42
|
ref = os.getenv("GITHUB_WORKFLOW_REF", "")
|
43
43
|
if ref:
|
44
|
-
|
44
|
+
# example value: 'owner/repo/.github/workflows/ai-code-review.yml@refs/pull/1/merge'
|
45
|
+
ref = ref.split("@")[0]
|
46
|
+
ref = ref.split(".github/workflows/")[-1]
|
47
|
+
ref = f" (.github/workflows/{ref})"
|
45
48
|
msg += (
|
46
49
|
f"\nPlease check your GitHub Action Secrets "
|
47
50
|
f"and `env` configuration section of the corresponding workflow step{ref}."
|
@@ -56,4 +59,4 @@ def bootstrap():
|
|
56
59
|
except Exception as e:
|
57
60
|
logging.error(f"Unexpected configuration error: {e}")
|
58
61
|
raise SystemExit(3)
|
59
|
-
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [
|
62
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
|
ai_code_review/cli.py
CHANGED
@@ -2,62 +2,154 @@ import asyncio
|
|
2
2
|
import logging
|
3
3
|
import sys
|
4
4
|
import os
|
5
|
-
import
|
5
|
+
import textwrap
|
6
|
+
import tempfile
|
7
|
+
import requests
|
6
8
|
|
7
9
|
import microcore as mc
|
8
|
-
import async_typer
|
9
10
|
import typer
|
10
|
-
from .core import review
|
11
|
-
from .report_struct import Report
|
12
11
|
from git import Repo
|
13
|
-
import requests
|
14
12
|
|
13
|
+
from .core import review, get_diff, filter_diff
|
14
|
+
from .report_struct import Report
|
15
15
|
from .constants import ENV_CONFIG_FILE
|
16
16
|
from .bootstrap import bootstrap
|
17
17
|
from .project_config import ProjectConfig
|
18
|
+
from .utils import no_subcommand, parse_refs_pair
|
18
19
|
|
19
|
-
app =
|
20
|
-
|
21
|
-
)
|
20
|
+
app = typer.Typer(pretty_exceptions_show_locals=False)
|
21
|
+
app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
|
22
22
|
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
def main():
|
25
|
+
if sys.platform == "win32":
|
26
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
27
|
+
if no_subcommand(app):
|
28
|
+
bootstrap()
|
29
|
+
app_no_subcommand()
|
30
|
+
else:
|
31
|
+
app()
|
26
32
|
|
27
33
|
|
28
34
|
@app.callback(invoke_without_command=True)
|
29
|
-
def cli(ctx: typer.Context
|
35
|
+
def cli(ctx: typer.Context):
|
30
36
|
if ctx.invoked_subcommand != "setup":
|
31
37
|
bootstrap()
|
32
|
-
if not ctx.invoked_subcommand:
|
33
|
-
asyncio.run(review(filters=filters))
|
34
38
|
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
40
|
+
def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
|
41
|
+
_what, _against = parse_refs_pair(refs)
|
42
|
+
if _what:
|
43
|
+
if what:
|
44
|
+
raise typer.BadParameter(
|
45
|
+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
|
46
|
+
)
|
47
|
+
else:
|
48
|
+
_what = what
|
49
|
+
if _against:
|
50
|
+
if against:
|
51
|
+
raise typer.BadParameter(
|
52
|
+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
|
53
|
+
)
|
54
|
+
else:
|
55
|
+
_against = against
|
56
|
+
return _what, _against
|
39
57
|
|
40
58
|
|
41
|
-
|
42
|
-
|
43
|
-
|
59
|
+
def arg_refs() -> typer.Argument:
|
60
|
+
return typer.Argument(
|
61
|
+
default=None,
|
62
|
+
help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
def arg_what() -> typer.Option:
|
67
|
+
return typer.Option(None, "--what", "-w", help="Git ref to review")
|
68
|
+
|
69
|
+
|
70
|
+
def arg_filters() -> typer.Option:
|
71
|
+
return typer.Option(
|
72
|
+
"", "--filter", "-f", "--filters",
|
73
|
+
help="""
|
74
|
+
filter reviewed files by glob / fnmatch pattern(s),
|
75
|
+
e.g. 'src/**/*.py', may be comma-separated
|
76
|
+
""",
|
77
|
+
)
|
44
78
|
|
45
79
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
try:
|
53
|
-
os.chdir("reviewed-repo")
|
54
|
-
await review()
|
55
|
-
finally:
|
56
|
-
os.chdir(prev_dir)
|
80
|
+
def arg_out() -> typer.Option:
|
81
|
+
return typer.Option(
|
82
|
+
None,
|
83
|
+
"--out", "-o", "--output",
|
84
|
+
help="Output folder for the code review report"
|
85
|
+
)
|
57
86
|
|
58
87
|
|
59
|
-
|
60
|
-
|
88
|
+
def arg_against() -> typer.Option:
|
89
|
+
return typer.Option(
|
90
|
+
None,
|
91
|
+
"--against", "-vs", "--vs",
|
92
|
+
help="Git ref to compare against"
|
93
|
+
)
|
94
|
+
|
95
|
+
|
96
|
+
@app_no_subcommand.command(name="review", help="Perform code review")
|
97
|
+
@app.command(name="review", help="Perform code review")
|
98
|
+
def cmd_review(
|
99
|
+
refs: str = arg_refs(),
|
100
|
+
what: str = arg_what(),
|
101
|
+
against: str = arg_against(),
|
102
|
+
filters: str = arg_filters(),
|
103
|
+
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
104
|
+
out: str = arg_out()
|
105
|
+
):
|
106
|
+
_what, _against = args_to_target(refs, what, against)
|
107
|
+
asyncio.run(review(
|
108
|
+
what=_what,
|
109
|
+
against=_against,
|
110
|
+
filters=filters,
|
111
|
+
use_merge_base=merge_base,
|
112
|
+
out_folder=out,
|
113
|
+
))
|
114
|
+
|
115
|
+
|
116
|
+
@app.command(help="Configure LLM for local usage interactively")
|
117
|
+
def setup():
|
118
|
+
mc.interactive_setup(ENV_CONFIG_FILE)
|
119
|
+
|
120
|
+
|
121
|
+
@app.command()
|
122
|
+
def render(format: str = Report.Format.MARKDOWN):
|
123
|
+
print(Report.load().render(format=format))
|
124
|
+
|
125
|
+
|
126
|
+
@app.command(help="Review remote code")
|
127
|
+
def remote(
|
128
|
+
url: str = typer.Argument(..., help="Git repository URL"),
|
129
|
+
refs: str = arg_refs(),
|
130
|
+
what: str = arg_what(),
|
131
|
+
against: str = arg_against(),
|
132
|
+
filters: str = arg_filters(),
|
133
|
+
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
134
|
+
out: str = arg_out()
|
135
|
+
):
|
136
|
+
_what, _against = args_to_target(refs, what, against)
|
137
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
138
|
+
logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
|
139
|
+
repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
|
140
|
+
asyncio.run(review(
|
141
|
+
repo=repo,
|
142
|
+
what=_what,
|
143
|
+
against=_against,
|
144
|
+
filters=filters,
|
145
|
+
use_merge_base=merge_base,
|
146
|
+
out_folder=out or '.',
|
147
|
+
))
|
148
|
+
repo.close()
|
149
|
+
|
150
|
+
|
151
|
+
@app.command(help="Leave a GitHub PR comment with the review.")
|
152
|
+
def github_comment(
|
61
153
|
token: str = typer.Option(
|
62
154
|
os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
|
63
155
|
),
|
@@ -65,7 +157,7 @@ async def github_comment(
|
|
65
157
|
"""
|
66
158
|
Leaves a comment with the review on the current GitHub pull request.
|
67
159
|
"""
|
68
|
-
file = "code-review-report.
|
160
|
+
file = "code-review-report.md"
|
69
161
|
if not os.path.exists(file):
|
70
162
|
print(f"Review file not found: {file}")
|
71
163
|
raise typer.Exit(4)
|
@@ -110,3 +202,35 @@ async def github_comment(
|
|
110
202
|
else:
|
111
203
|
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
112
204
|
raise typer.Exit(5)
|
205
|
+
|
206
|
+
|
207
|
+
@app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
|
208
|
+
def files(
|
209
|
+
refs: str = arg_refs(),
|
210
|
+
what: str = arg_what(),
|
211
|
+
against: str = arg_against(),
|
212
|
+
filters: str = arg_filters(),
|
213
|
+
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
214
|
+
diff: bool = typer.Option(default=False, help="Show diff content")
|
215
|
+
):
|
216
|
+
_what, _against = args_to_target(refs, what, against)
|
217
|
+
repo = Repo(".")
|
218
|
+
patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
|
219
|
+
patch_set = filter_diff(patch_set, filters)
|
220
|
+
print(
|
221
|
+
f"Changed files: "
|
222
|
+
f"{mc.ui.green(_what or 'INDEX')} vs "
|
223
|
+
f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
|
224
|
+
f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
|
225
|
+
)
|
226
|
+
|
227
|
+
for patch in patch_set:
|
228
|
+
if patch.is_added_file:
|
229
|
+
color = mc.ui.green
|
230
|
+
elif patch.is_removed_file:
|
231
|
+
color = mc.ui.red
|
232
|
+
else:
|
233
|
+
color = mc.ui.blue
|
234
|
+
print(f"- {color(patch.path)}")
|
235
|
+
if diff:
|
236
|
+
print(mc.ui.gray(textwrap.indent(str(patch), " ")))
|
ai_code_review/constants.py
CHANGED
@@ -3,5 +3,5 @@ from pathlib import Path
|
|
3
3
|
|
4
4
|
PROJECT_CONFIG_FILE = Path(".ai-code-review.toml")
|
5
5
|
PROJECT_CONFIG_DEFAULTS_FILE = Path(__file__).resolve().parent / PROJECT_CONFIG_FILE
|
6
|
-
ENV_CONFIG_FILE =
|
6
|
+
ENV_CONFIG_FILE = Path("~/.env.ai-code-review").expanduser()
|
7
7
|
JSON_REPORT_FILE_NAME = "code-review-report.json"
|
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,15 +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
|
12
15
|
|
13
16
|
|
14
|
-
def
|
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
|
33
|
+
|
34
|
+
|
35
|
+
def get_diff(
|
36
|
+
repo: Repo = None,
|
37
|
+
what: str = None,
|
38
|
+
against: str = None,
|
39
|
+
use_merge_base: bool = True,
|
40
|
+
) -> PatchSet | list[PatchedFile]:
|
15
41
|
repo = repo or Repo(".")
|
16
|
-
|
17
|
-
|
18
|
-
|
42
|
+
if not against:
|
43
|
+
against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
|
44
|
+
if not what:
|
45
|
+
what = None # working copy
|
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
|
+
)
|
66
|
+
diff_content = repo.git.diff(against, what)
|
19
67
|
diff = PatchSet.from_string(diff_content)
|
20
|
-
|
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
|
21
84
|
|
22
85
|
|
23
86
|
def filter_diff(
|
@@ -26,7 +89,6 @@ def filter_diff(
|
|
26
89
|
"""
|
27
90
|
Filter the diff files by the given fnmatch filters.
|
28
91
|
"""
|
29
|
-
print([f.path for f in patch_set])
|
30
92
|
assert isinstance(filters, (list, str))
|
31
93
|
if not isinstance(filters, list):
|
32
94
|
filters = [f.strip() for f in filters.split(",") if f.strip()]
|
@@ -37,7 +99,6 @@ def filter_diff(
|
|
37
99
|
for file in patch_set
|
38
100
|
if any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
|
39
101
|
]
|
40
|
-
print([f.path for f in files])
|
41
102
|
return files
|
42
103
|
|
43
104
|
|
@@ -54,18 +115,32 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
|
|
54
115
|
|
55
116
|
|
56
117
|
def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
|
57
|
-
return
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
+
)
|
63
128
|
|
64
129
|
|
65
|
-
async def review(
|
130
|
+
async def review(
|
131
|
+
repo: Repo = None,
|
132
|
+
what: str = None,
|
133
|
+
against: str = None,
|
134
|
+
filters: str | list[str] = "",
|
135
|
+
use_merge_base: bool = True,
|
136
|
+
out_folder: str | PathLike | None = None,
|
137
|
+
):
|
66
138
|
cfg = ProjectConfig.load()
|
67
|
-
repo = Repo(".")
|
68
|
-
|
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
|
+
)
|
69
144
|
diff = filter_diff(diff, filters)
|
70
145
|
if not diff:
|
71
146
|
logging.error("Nothing to review")
|
@@ -78,7 +153,7 @@ async def review(filters: str | list[str] = ""):
|
|
78
153
|
cfg.max_code_tokens
|
79
154
|
- mc.tokenizing.num_tokens_from_string(str(file_diff)),
|
80
155
|
)
|
81
|
-
if file_diff.target_file != DEV_NULL
|
156
|
+
if file_diff.target_file != DEV_NULL and not file_diff.is_added_file
|
82
157
|
else ""
|
83
158
|
)
|
84
159
|
for file_diff in diff
|
@@ -103,12 +178,14 @@ async def review(filters: str | list[str] = ""):
|
|
103
178
|
if lines[file]:
|
104
179
|
f_lines = [""] + lines[file].splitlines()
|
105
180
|
i["affected_code"] = "\n".join(
|
106
|
-
f_lines[i["start_line"]: i["end_line"]+1]
|
181
|
+
f_lines[i["start_line"]: i["end_line"] + 1]
|
107
182
|
)
|
108
183
|
exec(cfg.post_process, {"mc": mc, **locals()})
|
184
|
+
out_folder.mkdir(parents=True, exist_ok=True)
|
109
185
|
report = Report(issues=issues, number_of_processed_files=len(diff))
|
110
186
|
report.summary = make_cr_summary(cfg, report, diff)
|
111
|
-
report.save()
|
187
|
+
report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
|
112
188
|
report_text = report.render(cfg, Report.Format.MARKDOWN)
|
113
189
|
print(mc.ui.yellow(report_text))
|
114
|
-
|
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
@@ -1,5 +1,8 @@
|
|
1
|
+
import sys
|
1
2
|
import os
|
2
3
|
from pathlib import Path
|
4
|
+
import typer
|
5
|
+
|
3
6
|
|
4
7
|
_EXT_TO_HINT: dict[str, str] = {
|
5
8
|
# scripting & languages
|
@@ -87,3 +90,27 @@ def syntax_hint(file_path: str | Path) -> str:
|
|
87
90
|
|
88
91
|
def is_running_in_github_action():
|
89
92
|
return os.getenv("GITHUB_ACTIONS") == "true"
|
93
|
+
|
94
|
+
|
95
|
+
def no_subcommand(app: typer.Typer) -> bool:
|
96
|
+
"""
|
97
|
+
Checks if the current script is being invoked as a command in a target Typer application.
|
98
|
+
"""
|
99
|
+
return not (
|
100
|
+
(first_arg := next((a for a in sys.argv[1:] if not a.startswith('-')), None))
|
101
|
+
and first_arg in (
|
102
|
+
cmd.name or cmd.callback.__name__.replace('_', '-')
|
103
|
+
for cmd in app.registered_commands
|
104
|
+
)
|
105
|
+
or '--help' in sys.argv
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
def parse_refs_pair(refs: str) -> tuple[str | None, str | None]:
|
110
|
+
SEPARATOR = '..'
|
111
|
+
if not refs:
|
112
|
+
return None, None
|
113
|
+
if SEPARATOR not in refs:
|
114
|
+
return refs, None
|
115
|
+
what, against = refs.split(SEPARATOR)
|
116
|
+
return what or None, against or None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ai-cr
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.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 (
|
19
|
-
Requires-Dist: ai-microcore (==4.0.0.
|
20
|
-
Requires-Dist: anthropic (
|
21
|
-
Requires-Dist:
|
22
|
-
Requires-Dist:
|
23
|
-
Requires-Dist:
|
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.
|
73
|
+
run: pip install ai-code-review==0.5.0
|
74
74
|
- name: Run AI code analysis
|
75
75
|
env:
|
76
76
|
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
@@ -91,7 +91,8 @@ jobs:
|
|
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
97
|
### 2. Run Code Analysis Locally
|
97
98
|
|
@@ -151,7 +152,7 @@ pytest
|
|
151
152
|
|
152
153
|
## 🤝 Contributing
|
153
154
|
|
154
|
-
**Looking for a specific feature or having trouble?**
|
155
|
+
**Looking for a specific feature or having trouble?**
|
155
156
|
Contributions are welcome! ❤️
|
156
157
|
See [CONTRIBUTING.md](https://github.com/Nayjest/ai-code-review/blob/main/CONTRIBUTING.md) for details.
|
157
158
|
|
@@ -0,0 +1,15 @@
|
|
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=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
|
4
|
+
ai_code_review/bootstrap.py,sha256=jqioR_UtTsn5nXezmjMLU3aB8tzlVz73ZRBk33ud5F4,2336
|
5
|
+
ai_code_review/cli.py,sha256=1wrF-s6U_f8BjwCl70G7IXIBEhmMUbQDvij5AMDxQ5M,7318
|
6
|
+
ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
|
7
|
+
ai_code_review/core.py,sha256=BJNs4ZER2-bMulXY2apY6B6hI0nvRCOrLqsJ7L8Bizc,6693
|
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=vlzU3M89qK6_mVkBMnppZaOFsXddVsIBVdfmbN3cxDY,2939
|
11
|
+
ai_cr-0.6.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
|
12
|
+
ai_cr-0.6.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
13
|
+
ai_cr-0.6.0.dist-info/METADATA,sha256=poIlyrRUJQEKwRN7JIgKGOjgL1DR291tw6jibTi4EMs,5835
|
14
|
+
ai_cr-0.6.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
15
|
+
ai_cr-0.6.0.dist-info/RECORD,,
|
ai_cr-0.4.8.dist-info/RECORD
DELETED
@@ -1,14 +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/bootstrap.py,sha256=qiI_jcNYQ30BC0exNzUgjmI2TE_-BrsBSVzMW9JjoFI,2116
|
4
|
-
ai_code_review/cli.py,sha256=lulkEEUmQ1Fn3j3aQ0ZXc6aPHPcQDsr4YqA_C6gFFH4,3409
|
5
|
-
ai_code_review/constants.py,sha256=jac2dNiGjShe5jd64WXKCYxpM-4lSh1WcXsbQTIYnAU,284
|
6
|
-
ai_code_review/core.py,sha256=LhstEanCpMD0rVEYja0EQ4GuwB47RXGJJNOTkD6WqYk,3888
|
7
|
-
ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
|
8
|
-
ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
|
9
|
-
ai_code_review/utils.py,sha256=xHocdWXr-_Ghna31Hz80Et37gIXokz59UbcM_zOqK_k,2171
|
10
|
-
ai_cr-0.4.8.dist-info/entry_points.txt,sha256=fQYGUz6CVCClhei9kNCTtJUX6ia-dTca6p1sZ9pnst8,48
|
11
|
-
ai_cr-0.4.8.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
12
|
-
ai_cr-0.4.8.dist-info/METADATA,sha256=mQCK_8YgDCeq-bsVCS56pcKxl2v0ufTS8K_vmWsHnfM,5607
|
13
|
-
ai_cr-0.4.8.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
14
|
-
ai_cr-0.4.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|