Skip to content

Commit 442089f

Browse files
committed
Task 4: Update Python CLI main.py and virtual_runner.py for local mode
Agent-Id: agent-699e5407-af2c-4926-9c29-778f775d9a9a Linked-Note-Id: c509caf1-e1b9-4c01-9551-5c55ecd1e62a
1 parent 948e605 commit 442089f

2 files changed

Lines changed: 157 additions & 17 deletions

File tree

npx/python/cli/main.py

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from . import __version__
2626
from .github_fetcher import parse_github_url, post_comment
27+
from .local_fetcher import validate_local_path
2728
from .output_formatter import format_output
2829
from .virtual_runner import VirtualReviewRunner
2930
from .expert_prompts import get_expert_prompt
@@ -153,6 +154,76 @@ async def run_review(
153154
sys.exit(1)
154155

155156

157+
async def run_local_review(
158+
path: str,
159+
question: str | None = None,
160+
output_format: str = "text",
161+
quiet: bool = False,
162+
model: str | None = None,
163+
expert: bool = False,
164+
):
165+
"""Run a review on a local directory."""
166+
# Validate path first
167+
try:
168+
abs_path = validate_local_path(path)
169+
except ValueError as e:
170+
print_error(str(e))
171+
sys.exit(1)
172+
173+
# Determine the question to use
174+
if expert:
175+
actual_question = get_expert_prompt(question)
176+
review_mode = "Expert Review"
177+
elif question:
178+
actual_question = question
179+
review_mode = "Review"
180+
else:
181+
print_error("Either --question or --expert is required")
182+
sys.exit(1)
183+
184+
if not quiet:
185+
print_info(f"Reviewing local directory: {abs_path}")
186+
if expert:
187+
print_info(f"Mode: Expert Code Review (SOLID, Security, Code Quality)")
188+
else:
189+
print_info(f"Question: {actual_question}")
190+
console.print()
191+
192+
# Create runner
193+
runner = VirtualReviewRunner(
194+
model=model,
195+
quiet=quiet,
196+
on_step=None if quiet else print_step,
197+
)
198+
199+
try:
200+
answer, sources, metadata = await runner.review_local(abs_path, actual_question)
201+
except Exception as e:
202+
print_error(f"Review failed: {e}")
203+
sys.exit(1)
204+
205+
# Format and print output
206+
model_name = metadata.get("model", model or "unknown")
207+
output = format_output(
208+
answer=answer,
209+
sources=sources,
210+
model=model_name,
211+
output_format=output_format,
212+
metadata=metadata if output_format == "json" else None,
213+
)
214+
215+
if quiet or output_format == "json":
216+
# Raw output for scripting
217+
print(output)
218+
else:
219+
# Rich formatted output
220+
console.print()
221+
if output_format == "markdown":
222+
console.print(Panel(Markdown(output), title="Review", border_style="green"))
223+
else:
224+
console.print(Panel(output, title="Review", border_style="green"))
225+
226+
156227
def main():
157228
"""Main CLI entry point."""
158229
parser = argparse.ArgumentParser(
@@ -177,20 +248,28 @@ def main():
177248
# review command
178249
review_parser = subparsers.add_parser(
179250
"review",
180-
help="Review a GitHub PR or Issue",
251+
help="Review a GitHub PR/Issue or local directory",
181252
)
182-
review_parser.add_argument(
253+
254+
# URL and path are mutually exclusive
255+
source_group = review_parser.add_mutually_exclusive_group(required=True)
256+
source_group.add_argument(
183257
"--url", "-u",
184258
type=str,
185-
required=True,
186259
help="GitHub PR or Issue URL",
187260
)
261+
source_group.add_argument(
262+
"--path", "-p",
263+
type=str,
264+
help="Local directory path to review",
265+
)
266+
188267
review_parser.add_argument(
189268
"--question", "-q",
190269
type=str,
191270
required=False,
192271
default=None,
193-
help="Question to ask about the PR/Issue (optional with --expert)",
272+
help="Question to ask about the PR/Issue/directory (optional with --expert)",
194273
)
195274
review_parser.add_argument(
196275
"--expert",
@@ -218,21 +297,37 @@ def main():
218297
review_parser.add_argument(
219298
"--submit",
220299
action="store_true",
221-
help="Post review as a comment on the PR/Issue (requires GITHUB_TOKEN)",
300+
help="Post review as a comment on the PR/Issue (GitHub only, requires GITHUB_TOKEN)",
222301
)
223-
302+
224303
args = parser.parse_args()
225-
304+
226305
if args.command == "review":
227-
asyncio.run(run_review(
228-
url=args.url,
229-
question=args.question,
230-
output_format=args.output,
231-
quiet=args.quiet,
232-
model=args.model,
233-
expert=args.expert,
234-
submit=args.submit,
235-
))
306+
# Validate --submit is only used with --url
307+
if args.submit and args.path:
308+
print_error("--submit can only be used with --url (GitHub reviews)")
309+
sys.exit(1)
310+
311+
# Dispatch to appropriate review function
312+
if args.url:
313+
asyncio.run(run_review(
314+
url=args.url,
315+
question=args.question,
316+
output_format=args.output,
317+
quiet=args.quiet,
318+
model=args.model,
319+
expert=args.expert,
320+
submit=args.submit,
321+
))
322+
elif args.path:
323+
asyncio.run(run_local_review(
324+
path=args.path,
325+
question=args.question,
326+
output_format=args.output,
327+
quiet=args.quiet,
328+
model=args.model,
329+
expert=args.expert,
330+
))
236331
else:
237332
parser.print_help()
238333
sys.exit(1)

npx/python/cli/virtual_runner.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
fetch_issue,
2020
build_review_context,
2121
)
22+
from .local_fetcher import build_local_context, validate_local_path
23+
from .local_repo_tools import LocalRepoTools
2224
from .repo_tools import RepoTools
2325

2426

@@ -235,7 +237,50 @@ async def review(self, url: str, question: str) -> tuple[str, list[str], dict]:
235237
}
236238

237239
return answer, sources, metadata
238-
240+
241+
async def review_local(self, path: str, question: str) -> tuple[str, list[str], dict]:
242+
"""Review a local directory.
243+
244+
Args:
245+
path: Local directory path (relative or absolute)
246+
question: Question to ask about the code
247+
248+
Returns:
249+
Tuple of (answer, sources, metadata)
250+
"""
251+
# Validate and resolve path
252+
abs_path = validate_local_path(path)
253+
254+
# Create local repo tools
255+
local_tools = LocalRepoTools(abs_path)
256+
self._repo_tools = local_tools
257+
self._repo_files = {}
258+
self._repo_dirs = {}
259+
self._search_results = []
260+
261+
# Build context from local directory
262+
context = build_local_context(abs_path)
263+
264+
# Run RLM
265+
self._ensure_configured()
266+
267+
try:
268+
answer, sources = await self._run_rlm_with_tools(context, question)
269+
finally:
270+
# Cleanup
271+
if self._repo_tools:
272+
await self._repo_tools.close()
273+
self._repo_tools = None
274+
275+
metadata = {
276+
"type": "local",
277+
"path": abs_path,
278+
"model": self.model,
279+
"files_fetched": list(self._repo_files.keys()),
280+
}
281+
282+
return answer, sources, metadata
283+
239284
async def _process_tool_requests(self, output: str) -> bool:
240285
"""Parse output for tool requests and execute them.
241286

0 commit comments

Comments
 (0)