Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 86 additions & 24 deletions repo2readme/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import os

import click
from rich import print as rprint
from rich.progress import Progress

from repo2readme.config import get_api_keys, reset_api_keys
import os
from repo2readme.utils.tree import generate_tree
from repo2readme.llm.factory import configure_llm
from repo2readme.utils.detect_language import detect_lang
from repo2readme.utils.tree import generate_tree


@click.group()
Expand Down Expand Up @@ -46,21 +49,53 @@ def main():
is_flag=True,
help="Preview the analysis without making any API calls.",
)
def run(url, local, output, force, include_patterns, exclude_patterns, max_file_size_kb, dry_run):
""" Use --url for GitHub repo url and --local for local repo
"""
@click.option(
"--provider",
default=None,
help="LLM provider (groq, google, openai, anthropic, etc.)",
)
@click.option(
"--model",
default=None,
help="Model name",
)
@click.option(
"--base-url",
default=None,
help="Base URL for OpenAI-compatible APIs",
)
def run(
url,
local,
output,
force,
include_patterns,
exclude_patterns,
max_file_size_kb,
dry_run,
provider,
model,
base_url,
):
"""Use --url for GitHub repo url and --local for local repo"""

if not url and not local:
rprint("[red]Provide either --url or --local[/red]")
return

source = url if url else local

from repo2readme.loaders.repo_loader import RepoLoader

with Progress() as progress:
task = progress.add_task("[cyan]Loading repository...", total=1)
try:
loader = RepoLoader(source, include_patterns=include_patterns, exclude_patterns=exclude_patterns, max_file_size_kb=max_file_size_kb)
loader = RepoLoader(
source,
include_patterns=include_patterns,
exclude_patterns=exclude_patterns,
max_file_size_kb=max_file_size_kb,
)
files, root_path, loader_obj = loader.load()
except Exception as e:
rprint(f"[red]Failed to load repository: {e}[/red]")
Expand All @@ -69,10 +104,13 @@ def run(url, local, output, force, include_patterns, exclude_patterns, max_file_

documents = []
for f in files:
documents.append({
"content": f.page_content,
"metadata": f.metadata
})
documents.append(
{
"content": f.page_content,
"metadata": f.metadata,
}
)

tree = generate_tree(root_path)

# Estimate token count (roughly 3 characters per token)
Expand All @@ -91,21 +129,24 @@ def format_size(size_bytes):
if dry_run:
rprint("\n[bold]Repository Tree[/bold]\n")
rprint(tree)

rprint("\n[bold]Files to be processed[/bold]\n")
for doc in documents:
rel_path = doc["metadata"].get("relative_path", "")
rprint(f"✓ [green]{rel_path}[/green]")

rprint("\n[bold]Repository Analysis[/bold]\n")
rprint(f"Files selected : {total_documents}")
rprint(f"Estimated tokens : ~{estimated_tokens:,}")
rprint(f"Request size : ~{format_size(total_size_bytes)}")

rprint("\n[green]Dry run complete.[/green]")
rprint("[yellow]No API requests were made.[/yellow]")

if hasattr(loader_obj, "cleanup"):
loader_obj.cleanup()
return

# Normal execution: print estimation first
rprint("\n[bold]Repository Analysis[/bold]\n")
rprint(f"Files to summarize : {total_documents}")
rprint(f"Estimated tokens : ~{estimated_tokens:,}")
Expand All @@ -119,33 +160,49 @@ def format_size(size_bytes):
return

try:
groq_key, gemini_key = get_api_keys()
os.environ["GROQ_API_KEY"] = groq_key
os.environ["GOOGLE_API_KEY"] = gemini_key
# Backward-compatible configuration loading
get_api_keys()

# Configure the selected LLM provider/model for the process.
configure_llm(
provider=provider,
model=model,
base_url=base_url,
)

except Exception as e:
rprint(f"[red]Failed to configure API keys: {e}[/red]")
rprint(f"[red]Failed to configure LLM: {e}[/red]")
return

from repo2readme.summarize.summary import summarize_file
from repo2readme.readme.agent_workflow import workflow
from repo2readme.summarize.summary import summarize_file

summaries = []
errors = []

with Progress() as progress:
task = progress.add_task("[cyan]Generating summaries...[/cyan]", total=total_documents)
task = progress.add_task(
"[cyan]Generating summaries...[/cyan]",
total=total_documents,
)

for doc in documents:
meta = doc["metadata"]

try:
lang = detect_lang(meta.get("file_type", "text"))

summary = summarize_file(
file_path=meta["file_path"],
language=lang,
content=doc["content"]
content=doc["content"],
)

summaries.append(summary)

except Exception as e:
errors.append(f"Error processing {meta.get('file_path')}: {e}")

progress.update(task, advance=1)

rprint("[cyan]Generating README...[/cyan]")
Expand All @@ -156,12 +213,12 @@ def format_size(size_bytes):
"iteration_no": 0,
"max_iterations": 3,
"latest_readme": "",
'best_score': 0.0,
"best_readme": ""
"best_score": 0.0,
"best_readme": "",
}

final_state = workflow.invoke(initial_state)
readme = final_state['best_readme']
readme = final_state["best_readme"]

if output is None:
rprint("\n[green]Generated README:[/green]\n")
Expand All @@ -182,6 +239,11 @@ def format_size(size_bytes):

rprint(f"[green]Saved to {output}[/green]")

if errors:
rprint("\n[yellow]Some files could not be processed:[/yellow]")
for err in errors:
rprint(f"[yellow]- {err}[/yellow]")

finally:
if hasattr(loader_obj, "cleanup"):
loader_obj.cleanup()
Expand All @@ -199,4 +261,4 @@ def reset():


if __name__ == "__main__":
main()
main()
65 changes: 49 additions & 16 deletions repo2readme/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@
ENV_PATH = os.path.join(os.path.expanduser("~"), ".repo2readme_env.json")


# -------------------------------------------------------
# Supported API Keys
# Extend this dictionary when adding new providers.
# -------------------------------------------------------

SUPPORTED_API_KEYS = {
"GROQ_API_KEY": "Groq",
"GOOGLE_API_KEY": "Google Gemini",
}


def load_env():
if not os.path.exists(ENV_PATH):
return {}

with open(ENV_PATH, "r") as f:
return json.load(f)

Expand All @@ -18,33 +30,54 @@ def save_env(data):


def get_api_keys():
env = load_env()
"""
Loads all configured API keys.

groq = env.get("GROQ_API_KEY")
gemini = env.get("GOOGLE_API_KEY")
If any required API key is missing,
the user is prompted only for the missing key.

if groq and gemini:
return groq, gemini
Returns
-------
dict

rprint("[yellow]API keys are missing! Let's add them.[/yellow]\n")
Example:

if not groq:
groq = input("Enter your Groq API key: ").strip()
if not gemini:
gemini = input("Enter your Google Gemini API key: ").strip()
{
"GROQ_API_KEY": "...",
"GOOGLE_API_KEY": "..."
}
"""

env["GROQ_API_KEY"] = groq
env["GOOGLE_API_KEY"] = gemini
env = load_env()

missing = [
key
for key in SUPPORTED_API_KEYS
if not env.get(key)
]

if missing:
rprint("[yellow]Some API keys are missing.[/yellow]\n")

save_env(env)
for key in missing:
provider_name = SUPPORTED_API_KEYS[key]
env[key] = input(
f"Enter your {provider_name} API key: "
).strip()

rprint("[green]API keys saved successfully![/green]")
save_env(env)

return groq, gemini
rprint("[green]API keys saved successfully![/green]")
Comment on lines +51 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prompts for every provider's key, even when only one provider is used.

missing collects all of SUPPORTED_API_KEYS, so a user running with only Groq is still forced to enter a Google Gemini key (and vice versa). Since provider selection lives in the CLI/factory, get_api_keys() has no way to prompt only for the relevant key. This is a UX regression against the goal of provider-agnostic selection. Consider scoping the prompt to the active provider(s), or allowing the run to proceed when the selected provider's key is present.

Also note: a blank Enter stores "", which env.get(key) treats as missing and re-prompts on the next run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@repo2readme/config.py` around lines 51 - 70, The key-prompting logic in
get_api_keys() is too broad because it iterates over SUPPORTED_API_KEYS and asks
for every missing provider key, even when only one provider is actually needed.
Update the flow so it only prompts for the active provider(s) selected by the
CLI/factory, or otherwise skips prompting when the selected provider’s key is
already available; use get_api_keys(), SUPPORTED_API_KEYS, and save_env() as the
main touchpoints. Also handle empty input explicitly so a blank response is not
saved as an empty string and retriggered on the next run.


return {
key: env.get(key)
for key in SUPPORTED_API_KEYS
}


def reset_api_keys():
if os.path.exists(ENV_PATH):
os.remove(ENV_PATH)
return True
return False

return False
Loading