breadcrumb-cli 0.1.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.
@@ -0,0 +1,123 @@
1
+ """
2
+ Interactive chat with the codebase using a TUI.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.prompt import Prompt
11
+ from rich.rule import Rule
12
+
13
+ from breadcrumb.ai.prompts import get_system_prompt
14
+ from breadcrumb.ai.router import AIRouter
15
+ from breadcrumb.history import SessionManager
16
+ from breadcrumb.ingest import FileIngester
17
+
18
+ console = Console()
19
+
20
+
21
+ def cmd_chat(
22
+ repo_path: Path,
23
+ provider: Optional[str] = None,
24
+ session_name: str = "default",
25
+ ) -> None:
26
+ """
27
+ Interactive chat session with the codebase.
28
+
29
+ Args:
30
+ repo_path: Repository path
31
+ provider: AI provider (overrides config)
32
+ session_name: Name of the chat session
33
+ """
34
+ # Initialize
35
+ ingester = FileIngester(repo_path)
36
+ router = AIRouter(provider)
37
+ session = SessionManager(repo_path, session_name)
38
+
39
+ try:
40
+ context = ingester.get_content()
41
+ except Exception as e:
42
+ console.print(f"[red]Error reading repository: {e}[/red]")
43
+ context = ""
44
+
45
+ console.print(
46
+ Panel.fit(
47
+ "[bold cyan]🍞 Bread Crumb[/bold cyan]\n"
48
+ f"[dim]Chat with your codebase using {router.provider}[/dim]",
49
+ border_style="cyan",
50
+ )
51
+ )
52
+ console.print(
53
+ f"[dim]Session: {session_name} | Provider: {router.provider} | Model: {router.model}[/dim]"
54
+ )
55
+ console.print("[dim]Type 'exit' to quit, 'clear' to clear history[/dim]")
56
+ console.print(Rule())
57
+
58
+ # Chat loop
59
+ while True:
60
+ try:
61
+ question = Prompt.ask("[cyan]You")
62
+ except KeyboardInterrupt:
63
+ console.print("\n[dim]Goodbye![/dim]")
64
+ break
65
+ except EOFError:
66
+ console.print("\n[dim]Goodbye![/dim]")
67
+ break
68
+
69
+ if not question.strip():
70
+ continue
71
+
72
+ if question.lower() == "exit":
73
+ console.print("[dim]Goodbye![/dim]")
74
+ break
75
+
76
+ if question.lower() == "clear":
77
+ session.clear()
78
+ console.print("[yellow]History cleared[/yellow]")
79
+ continue
80
+
81
+ if question.lower() == "sessions":
82
+ sessions = SessionManager.list_sessions(repo_path)
83
+ if sessions:
84
+ console.print("[cyan]Available sessions:[/cyan]")
85
+ for s in sessions:
86
+ console.print(f" • {s}")
87
+ else:
88
+ console.print("[dim]No other sessions[/dim]")
89
+ continue
90
+
91
+ # Add user message to history
92
+ session.add_message("user", question)
93
+
94
+ # Build messages for API
95
+ system = get_system_prompt("chat")
96
+
97
+ # Include context only in first message of session
98
+ if len(session.messages) <= 1:
99
+ api_messages = [
100
+ {"role": "user", "content": f"Repo context:\n{context}\n\nQuestion: {question}"}
101
+ ]
102
+ else:
103
+ api_messages = session.get_messages_for_api()
104
+
105
+ # Stream response
106
+ try:
107
+ response_text = ""
108
+ console.print("[cyan]Assistant[/cyan]", end=" ")
109
+ for chunk in router.stream(api_messages, system):
110
+ console.print(chunk, end="", highlight=False)
111
+ response_text += chunk
112
+ console.print() # Newline after response
113
+ except Exception as e:
114
+ console.print(f"[red]Error: {e}[/red]")
115
+ continue
116
+
117
+ # Add assistant response to history
118
+ session.add_message("assistant", response_text)
119
+
120
+ # Show stats
121
+ tokens = session.count_tokens()
122
+ console.print(f"[dim]~{tokens:,} tokens in session[/dim]")
123
+ console.print(Rule())
@@ -0,0 +1,87 @@
1
+ """
2
+ Generate conventional commit messages from staged changes.
3
+ """
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from rich.console import Console
10
+
11
+ from breadcrumb.ai.prompts import get_system_prompt
12
+ from breadcrumb.ai.router import AIRouter
13
+
14
+ console = Console()
15
+
16
+
17
+ def get_git_diff_staged(repo_path: Path) -> str:
18
+ """Get staged changes via git diff --staged."""
19
+ try:
20
+ result = subprocess.run(
21
+ ["git", "diff", "--staged"],
22
+ cwd=repo_path,
23
+ capture_output=True,
24
+ text=True,
25
+ )
26
+ return result.stdout
27
+ except Exception as e:
28
+ console.print(f"[red]Error running git diff: {e}[/red]")
29
+ return ""
30
+
31
+
32
+ def cmd_commit(
33
+ repo_path: Path,
34
+ provider: Optional[str] = None,
35
+ silent: bool = False,
36
+ ) -> str:
37
+ """
38
+ Generate a conventional commit message from staged changes.
39
+
40
+ Args:
41
+ repo_path: Repository path
42
+ provider: AI provider (overrides config)
43
+ silent: If True, only print the message (no other output)
44
+
45
+ Returns:
46
+ The generated commit message
47
+ """
48
+ # Get staged diff
49
+ diff = get_git_diff_staged(repo_path)
50
+
51
+ if not diff:
52
+ if not silent:
53
+ console.print("[yellow]No staged changes[/yellow]")
54
+ return ""
55
+
56
+ # Setup AI
57
+ router = AIRouter(provider)
58
+ system = get_system_prompt("commit")
59
+
60
+ prompt = f"""Analyze these staged changes and generate a conventional commit message.
61
+
62
+ {diff}
63
+
64
+ Generate ONLY the commit message in conventional format (type(scope): description).
65
+ No explanation, no markdown, just the message."""
66
+
67
+ messages = [{"role": "user", "content": prompt}]
68
+
69
+ try:
70
+ if not silent:
71
+ with console.status("[bold cyan]Generating commit message...", spinner="dots"):
72
+ message = router.chat(messages, system)
73
+ else:
74
+ message = router.chat(messages, system)
75
+
76
+ message = message.strip()
77
+
78
+ if not silent:
79
+ console.print(f"\n[green]Suggested commit:[/green]\n{message}")
80
+ else:
81
+ print(message)
82
+
83
+ return message
84
+ except Exception as e:
85
+ if not silent:
86
+ console.print(f"[red]Error: {e}[/red]")
87
+ return ""
@@ -0,0 +1,90 @@
1
+ """
2
+ Review git diffs with AI.
3
+ """
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from rich.console import Console
10
+ from rich.markdown import Markdown
11
+
12
+ from breadcrumb.ai.prompts import get_system_prompt
13
+ from breadcrumb.ai.router import AIRouter
14
+
15
+ console = Console()
16
+
17
+
18
+ def get_git_diff(repo_path: Path, revision: str = "") -> str:
19
+ """Get git diff for a revision."""
20
+ try:
21
+ if revision:
22
+ result = subprocess.run(
23
+ ["git", "diff", revision],
24
+ cwd=repo_path,
25
+ capture_output=True,
26
+ text=True,
27
+ )
28
+ else:
29
+ result = subprocess.run(
30
+ ["git", "diff"],
31
+ cwd=repo_path,
32
+ capture_output=True,
33
+ text=True,
34
+ )
35
+ return result.stdout
36
+ except Exception as e:
37
+ console.print(f"[red]Error running git diff: {e}[/red]")
38
+ return ""
39
+
40
+
41
+ def cmd_diff(
42
+ repo_path: Path,
43
+ revision: str = "",
44
+ provider: Optional[str] = None,
45
+ ) -> str:
46
+ """
47
+ Review a git diff with AI.
48
+
49
+ Args:
50
+ repo_path: Repository path
51
+ revision: Git revision (e.g., 'HEAD~1', 'main..feature/auth')
52
+ provider: AI provider (overrides config)
53
+
54
+ Returns:
55
+ The review
56
+ """
57
+ diff = get_git_diff(repo_path, revision)
58
+
59
+ if not diff:
60
+ console.print("[yellow]No changes found for the specified revision[/yellow]")
61
+ return ""
62
+
63
+ # Limit diff size
64
+ if len(diff) > 50000:
65
+ diff = diff[:50000] + "\n... [diff truncated]"
66
+
67
+ router = AIRouter(provider)
68
+ system = get_system_prompt("diff")
69
+
70
+ prompt = f"""Review this git diff and provide:
71
+ 1. Summary of changes
72
+ 2. Any potential issues or bugs
73
+ 3. Suggestions for improvement
74
+ 4. Impact assessment
75
+
76
+ {diff}"""
77
+
78
+ messages = [{"role": "user", "content": prompt}]
79
+
80
+ try:
81
+ response_text = ""
82
+ with console.status("[bold cyan]Reviewing diff...", spinner="dots"):
83
+ for chunk in router.stream(messages, system):
84
+ response_text += chunk
85
+
86
+ console.print(Markdown(response_text))
87
+ return response_text
88
+ except Exception as e:
89
+ console.print(f"[red]Error: {e}[/red]")
90
+ return ""
@@ -0,0 +1,80 @@
1
+ """
2
+ Generate a daily digest of changes from git commits.
3
+ """
4
+
5
+ import subprocess
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from rich.console import Console
11
+ from rich.markdown import Markdown
12
+
13
+ from breadcrumb.ai.prompts import get_system_prompt
14
+ from breadcrumb.ai.router import AIRouter
15
+
16
+ console = Console()
17
+
18
+
19
+ def get_commits_since(repo_path: Path, hours: int = 24) -> str:
20
+ """Get git log for the past N hours."""
21
+ try:
22
+ since = datetime.now() - timedelta(hours=hours)
23
+ since_str = since.strftime("%Y-%m-%d %H:%M:%S")
24
+
25
+ result = subprocess.run(
26
+ ["git", "log", f"--since={since_str}", "--oneline"],
27
+ cwd=repo_path,
28
+ capture_output=True,
29
+ text=True,
30
+ )
31
+ return result.stdout
32
+ except Exception as e:
33
+ console.print(f"[red]Error getting commits: {e}[/red]")
34
+ return ""
35
+
36
+
37
+ def cmd_digest(
38
+ repo_path: Path,
39
+ hours: int = 24,
40
+ provider: Optional[str] = None,
41
+ ) -> str:
42
+ """
43
+ Generate a digest of recent commits.
44
+
45
+ Args:
46
+ repo_path: Repository path
47
+ hours: How many hours back to look
48
+ provider: AI provider (overrides config)
49
+
50
+ Returns:
51
+ The digest
52
+ """
53
+ commits = get_commits_since(repo_path, hours)
54
+
55
+ if not commits:
56
+ console.print("[yellow]No commits in the past 24 hours[/yellow]")
57
+ return ""
58
+
59
+ router = AIRouter(provider)
60
+ system = get_system_prompt("digest")
61
+
62
+ prompt = f"""Summarize these commits into a concise digest.
63
+ What changed? What was fixed? Notable improvements?
64
+
65
+ Commits:
66
+ {commits}"""
67
+
68
+ messages = [{"role": "user", "content": prompt}]
69
+
70
+ try:
71
+ response_text = ""
72
+ with console.status("[bold cyan]Generating digest...", spinner="dots"):
73
+ for chunk in router.stream(messages, system):
74
+ response_text += chunk
75
+
76
+ console.print(Markdown(response_text))
77
+ return response_text
78
+ except Exception as e:
79
+ console.print(f"[red]Error: {e}[/red]")
80
+ return ""
@@ -0,0 +1,63 @@
1
+ """
2
+ Explain errors and stack traces.
3
+ """
4
+
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+ from rich.markdown import Markdown
10
+
11
+ from breadcrumb.ai.prompts import get_system_prompt
12
+ from breadcrumb.ai.router import AIRouter
13
+
14
+ console = Console()
15
+
16
+
17
+ def cmd_explain_error(
18
+ error_text: Optional[str] = None,
19
+ provider: Optional[str] = None,
20
+ ) -> str:
21
+ """
22
+ Explain an error or stack trace.
23
+
24
+ Args:
25
+ error_text: Error text (reads from stdin if not provided)
26
+ provider: AI provider (overrides config)
27
+
28
+ Returns:
29
+ The explanation
30
+ """
31
+ if not error_text:
32
+ error_text = sys.stdin.read()
33
+
34
+ if not error_text:
35
+ console.print("[red]Error: No error text provided[/red]")
36
+ return ""
37
+
38
+ error_text = error_text.strip()
39
+
40
+ router = AIRouter(provider)
41
+ system = get_system_prompt("explain-error")
42
+
43
+ prompt = f"""Analyze this error and provide:
44
+ 1. Plain English explanation
45
+ 2. Root cause
46
+ 3. How to fix it
47
+
48
+ Error:
49
+ {error_text}"""
50
+
51
+ messages = [{"role": "user", "content": prompt}]
52
+
53
+ try:
54
+ response_text = ""
55
+ with console.status("[bold cyan]Analyzing error...", spinner="dots"):
56
+ for chunk in router.stream(messages, system):
57
+ response_text += chunk
58
+
59
+ console.print(Markdown(response_text))
60
+ return response_text
61
+ except Exception as e:
62
+ console.print(f"[red]Error: {e}[/red]")
63
+ return ""
@@ -0,0 +1,67 @@
1
+ """
2
+ Initialize a .breadcrumb.yaml config file for a repository.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+ BREADCRUMB_YAML_TEMPLATE = """# Bread Crumb Configuration
12
+ # Place this file in your repository root to customize Bread Crumb behavior
13
+
14
+ # AI Provider: anthropic, openai, gemini, or ollama
15
+ provider: anthropic
16
+
17
+ # Model name for the chosen provider
18
+ model: claude-3-5-sonnet-20241022
19
+
20
+ # Patterns to ignore (like .gitignore)
21
+ # These files won't be included in context
22
+ ignore_patterns:
23
+ - "*.min.js"
24
+ - "*.min.css"
25
+ - "dist/"
26
+ - "build/"
27
+ - "node_modules/"
28
+
29
+ # Custom system prompt for this repository
30
+ # Gets prepended to all conversations
31
+ system_prompt: |
32
+ You are analyzing a [PROJECT_TYPE] project.
33
+ Key technologies: [TECH_STACK]
34
+ Important context: [ANY_SPECIAL_RULES]
35
+
36
+ # Temperature for AI responses (0.0 - 1.0)
37
+ temperature: 0.7
38
+
39
+ # Maximum tokens per request
40
+ max_tokens: 4096
41
+ """
42
+
43
+
44
+ def cmd_init(repo_path: Path) -> bool:
45
+ """
46
+ Initialize a .breadcrumb.yaml config file in the repository.
47
+
48
+ Args:
49
+ repo_path: Repository path
50
+
51
+ Returns:
52
+ True if successful
53
+ """
54
+ config_file = repo_path / ".breadcrumb.yaml"
55
+
56
+ if config_file.exists():
57
+ console.print("[yellow].breadcrumb.yaml already exists[/yellow]")
58
+ return False
59
+
60
+ try:
61
+ config_file.write_text(BREADCRUMB_YAML_TEMPLATE)
62
+ console.print(f"[green]✓ Created {config_file}[/green]")
63
+ console.print("[dim]Edit it to customize Bread Crumb for your project[/dim]")
64
+ return True
65
+ except Exception as e:
66
+ console.print(f"[red]Error: {e}[/red]")
67
+ return False