janito 0.3.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.
- janito/__init__.py +2 -0
- janito/__main__.py +260 -0
- janito/changeviewer.py +64 -0
- janito/claude.py +74 -0
- janito/config.py +32 -0
- janito/console.py +60 -0
- janito/contentchange.py +165 -0
- janito/prompts.py +97 -0
- janito/qa.py +32 -0
- janito/scan.py +122 -0
- janito-0.3.0.dist-info/METADATA +138 -0
- janito-0.3.0.dist-info/RECORD +15 -0
- janito-0.3.0.dist-info/WHEEL +4 -0
- janito-0.3.0.dist-info/entry_points.txt +2 -0
- janito-0.3.0.dist-info/licenses/LICENSE +21 -0
janito/__init__.py
ADDED
janito/__main__.py
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
import typer
|
2
|
+
from typing import Optional, Dict, Any, List
|
3
|
+
from pathlib import Path
|
4
|
+
from janito.claude import ClaudeAPIAgent
|
5
|
+
import shutil
|
6
|
+
from janito.prompts import (
|
7
|
+
build_request_analisys_prompt,
|
8
|
+
build_selected_option_prompt,
|
9
|
+
SYSTEM_PROMPT,
|
10
|
+
parse_options
|
11
|
+
)
|
12
|
+
from rich.console import Console
|
13
|
+
from rich.markdown import Markdown
|
14
|
+
import re
|
15
|
+
import tempfile
|
16
|
+
import json
|
17
|
+
from rich.syntax import Syntax
|
18
|
+
from janito.contentchange import (
|
19
|
+
handle_changes_file,
|
20
|
+
get_file_type,
|
21
|
+
parse_block_changes,
|
22
|
+
preview_and_apply_changes,
|
23
|
+
format_parsed_changes,
|
24
|
+
)
|
25
|
+
from rich.table import Table
|
26
|
+
from rich.columns import Columns
|
27
|
+
from rich.panel import Panel
|
28
|
+
from rich.text import Text
|
29
|
+
from rich.rule import Rule
|
30
|
+
from rich import box
|
31
|
+
from datetime import datetime, timezone
|
32
|
+
from itertools import chain
|
33
|
+
from janito.scan import collect_files_content, is_dir_empty, preview_scan
|
34
|
+
from janito.qa import ask_question, display_answer
|
35
|
+
from rich.prompt import Prompt, Confirm
|
36
|
+
from janito.config import config
|
37
|
+
from importlib.metadata import version
|
38
|
+
|
39
|
+
def get_version() -> str:
|
40
|
+
try:
|
41
|
+
return version("janito")
|
42
|
+
except:
|
43
|
+
return "dev"
|
44
|
+
|
45
|
+
def format_analysis(analysis: str, raw: bool = False, claude: Optional[ClaudeAPIAgent] = None) -> None:
|
46
|
+
"""Format and display the analysis output"""
|
47
|
+
console = Console()
|
48
|
+
if raw and claude:
|
49
|
+
console.print("\n=== Message History ===")
|
50
|
+
for role, content in claude.messages_history:
|
51
|
+
console.print(f"\n[bold cyan]{role.upper()}:[/bold cyan]")
|
52
|
+
console.print(content)
|
53
|
+
console.print("\n=== End Message History ===\n")
|
54
|
+
else:
|
55
|
+
md = Markdown(analysis)
|
56
|
+
console.print(md)
|
57
|
+
|
58
|
+
def prompt_user(message: str, choices: List[str] = None) -> str:
|
59
|
+
"""Display a prominent user prompt with optional choices"""
|
60
|
+
console = Console()
|
61
|
+
console.print()
|
62
|
+
console.print(Rule(" User Input Required ", style="bold cyan"))
|
63
|
+
|
64
|
+
if choices:
|
65
|
+
choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
|
66
|
+
console.print(Panel(choice_text, box=box.ROUNDED))
|
67
|
+
|
68
|
+
return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
|
69
|
+
|
70
|
+
def get_option_selection() -> int:
|
71
|
+
"""Get user input for option selection"""
|
72
|
+
while True:
|
73
|
+
try:
|
74
|
+
option = int(prompt_user("Select option number"))
|
75
|
+
return option
|
76
|
+
except ValueError:
|
77
|
+
console = Console()
|
78
|
+
console.print("[red]Please enter a valid number[/red]")
|
79
|
+
|
80
|
+
def get_history_path(workdir: Path) -> Path:
|
81
|
+
"""Create and return the history directory path"""
|
82
|
+
history_dir = workdir / '.janito' / 'history'
|
83
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
84
|
+
return history_dir
|
85
|
+
|
86
|
+
def get_timestamp() -> str:
|
87
|
+
"""Get current UTC timestamp in YMD_HMS format with leading zeros"""
|
88
|
+
return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
89
|
+
|
90
|
+
def save_prompt_to_file(prompt: str) -> Path:
|
91
|
+
"""Save prompt to a named temporary file that won't be deleted"""
|
92
|
+
temp_file = tempfile.NamedTemporaryFile(prefix='selected_', suffix='.txt', delete=False)
|
93
|
+
temp_path = Path(temp_file.name)
|
94
|
+
temp_path.write_text(prompt)
|
95
|
+
return temp_path
|
96
|
+
|
97
|
+
def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
|
98
|
+
"""Save content to a timestamped file in history directory"""
|
99
|
+
history_dir = get_history_path(workdir)
|
100
|
+
timestamp = get_timestamp()
|
101
|
+
filename = f"{timestamp}_{prefix}.txt"
|
102
|
+
file_path = history_dir / filename
|
103
|
+
file_path.write_text(content)
|
104
|
+
return file_path
|
105
|
+
|
106
|
+
def handle_option_selection(claude: ClaudeAPIAgent, initial_response: str, request: str, raw: bool = False, workdir: Optional[Path] = None, include: Optional[List[Path]] = None) -> None:
|
107
|
+
"""Handle option selection and implementation details"""
|
108
|
+
option = get_option_selection()
|
109
|
+
paths_to_scan = [workdir] if workdir else []
|
110
|
+
if include:
|
111
|
+
paths_to_scan.extend(include)
|
112
|
+
files_content = collect_files_content(paths_to_scan, workdir) if paths_to_scan else ""
|
113
|
+
|
114
|
+
selected_prompt = build_selected_option_prompt(option, request, initial_response, files_content)
|
115
|
+
prompt_file = save_to_file(selected_prompt, 'selected', workdir)
|
116
|
+
if config.verbose:
|
117
|
+
print(f"\nSelected prompt saved to: {prompt_file}")
|
118
|
+
|
119
|
+
selected_response = claude.send_message(selected_prompt)
|
120
|
+
changes_file = save_to_file(selected_response, 'changes', workdir)
|
121
|
+
if config.verbose:
|
122
|
+
print(f"\nChanges saved to: {changes_file}")
|
123
|
+
|
124
|
+
changes = parse_block_changes(selected_response)
|
125
|
+
preview_and_apply_changes(changes, workdir)
|
126
|
+
|
127
|
+
def replay_saved_file(filepath: Path, claude: ClaudeAPIAgent, workdir: Path, raw: bool = False) -> None:
|
128
|
+
"""Process a saved prompt file and display the response"""
|
129
|
+
if not filepath.exists():
|
130
|
+
raise FileNotFoundError(f"File {filepath} not found")
|
131
|
+
|
132
|
+
file_type = get_file_type(filepath)
|
133
|
+
content = filepath.read_text()
|
134
|
+
|
135
|
+
if file_type == 'changes':
|
136
|
+
changes = parse_block_changes(content)
|
137
|
+
preview_and_apply_changes(changes, workdir)
|
138
|
+
elif file_type == 'analysis':
|
139
|
+
format_analysis(content, raw, claude)
|
140
|
+
handle_option_selection(claude, content, content, raw, workdir)
|
141
|
+
elif file_type == 'selected':
|
142
|
+
if raw:
|
143
|
+
console = Console()
|
144
|
+
console.print("\n=== Prompt Content ===")
|
145
|
+
console.print(content)
|
146
|
+
console.print("=== End Prompt Content ===\n")
|
147
|
+
response = claude.send_message(content)
|
148
|
+
changes_file = save_to_file(response, 'changes_', workdir)
|
149
|
+
print(f"\nChanges saved to: {changes_file}")
|
150
|
+
|
151
|
+
changes = parse_block_changes(response)
|
152
|
+
preview_and_apply_changes(preview_changes, workdir)
|
153
|
+
else:
|
154
|
+
response = claude.send_message(content)
|
155
|
+
format_analysis(response, raw)
|
156
|
+
|
157
|
+
def process_question(question: str, workdir: Path, include: List[Path], raw: bool, claude: ClaudeAPIAgent) -> None:
|
158
|
+
"""Process a question about the codebase"""
|
159
|
+
paths_to_scan = [workdir] if workdir else []
|
160
|
+
if include:
|
161
|
+
paths_to_scan.extend(include)
|
162
|
+
files_content = collect_files_content(paths_to_scan, workdir)
|
163
|
+
|
164
|
+
answer = ask_question(question, files_content, claude)
|
165
|
+
display_answer(answer, raw)
|
166
|
+
|
167
|
+
def ensure_workdir(workdir: Path) -> Path:
|
168
|
+
"""Ensure working directory exists, prompt for creation if it doesn't"""
|
169
|
+
if workdir.exists():
|
170
|
+
return workdir
|
171
|
+
|
172
|
+
console = Console()
|
173
|
+
console.print(f"\n[yellow]Directory does not exist:[/yellow] {workdir}")
|
174
|
+
if Confirm.ask("Create directory?"):
|
175
|
+
workdir.mkdir(parents=True)
|
176
|
+
console.print(f"[green]Created directory:[/green] {workdir}")
|
177
|
+
return workdir
|
178
|
+
raise typer.Exit(1)
|
179
|
+
|
180
|
+
def typer_main(
|
181
|
+
request: Optional[str] = typer.Argument(None, help="The modification request"),
|
182
|
+
ask: Optional[str] = typer.Option(None, "--ask", help="Ask a question about the codebase"),
|
183
|
+
workdir: Optional[Path] = typer.Option(None, "-w", "--workdir",
|
184
|
+
help="Working directory (defaults to current directory)",
|
185
|
+
file_okay=False, dir_okay=True),
|
186
|
+
raw: bool = typer.Option(False, "--raw", help="Print raw response instead of markdown format"),
|
187
|
+
play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
|
188
|
+
include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include in analysis", exists=True),
|
189
|
+
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
|
190
|
+
debug_line: Optional[int] = typer.Option(None, "--debug-line", help="Show debug information only for specific line number"),
|
191
|
+
verbose: bool = typer.Option(False, "-v", "--verbose", help="Show verbose output"),
|
192
|
+
scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
|
193
|
+
version: bool = typer.Option(False, "--version", help="Show version and exit"),
|
194
|
+
) -> None:
|
195
|
+
"""
|
196
|
+
Analyze files and provide modification instructions.
|
197
|
+
"""
|
198
|
+
if version:
|
199
|
+
console = Console()
|
200
|
+
console.print(f"Janito v{get_version()}")
|
201
|
+
raise typer.Exit()
|
202
|
+
|
203
|
+
config.set_debug(debug)
|
204
|
+
config.set_verbose(verbose)
|
205
|
+
config.set_debug_line(debug_line)
|
206
|
+
|
207
|
+
claude = ClaudeAPIAgent(system_prompt=SYSTEM_PROMPT)
|
208
|
+
|
209
|
+
if not any([request, ask, play, scan]):
|
210
|
+
workdir = workdir or Path.cwd()
|
211
|
+
workdir = ensure_workdir(workdir)
|
212
|
+
from janito.console import start_console_session
|
213
|
+
start_console_session(workdir, include)
|
214
|
+
return
|
215
|
+
|
216
|
+
workdir = workdir or Path.cwd()
|
217
|
+
workdir = ensure_workdir(workdir)
|
218
|
+
|
219
|
+
if include:
|
220
|
+
include = [
|
221
|
+
path if path.is_absolute() else (workdir / path).resolve()
|
222
|
+
for path in include
|
223
|
+
]
|
224
|
+
|
225
|
+
if ask:
|
226
|
+
process_question(ask, workdir, include, raw, claude)
|
227
|
+
return
|
228
|
+
|
229
|
+
if scan:
|
230
|
+
paths_to_scan = include if include else [workdir]
|
231
|
+
preview_scan(paths_to_scan, workdir)
|
232
|
+
return
|
233
|
+
|
234
|
+
if play:
|
235
|
+
replay_saved_file(play, claude, workdir, raw)
|
236
|
+
return
|
237
|
+
|
238
|
+
paths_to_scan = include if include else [workdir]
|
239
|
+
|
240
|
+
is_empty = is_dir_empty(workdir)
|
241
|
+
if is_empty and not include:
|
242
|
+
console = Console()
|
243
|
+
console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
|
244
|
+
files_content = ""
|
245
|
+
else:
|
246
|
+
files_content = collect_files_content(paths_to_scan, workdir)
|
247
|
+
|
248
|
+
initial_prompt = build_request_analisys_prompt(files_content, request)
|
249
|
+
initial_response = claude.send_message(initial_prompt)
|
250
|
+
analysis_file = save_to_file(initial_response, 'analysis', workdir)
|
251
|
+
|
252
|
+
format_analysis(initial_response, raw, claude)
|
253
|
+
|
254
|
+
handle_option_selection(claude, initial_response, request, raw, workdir, include)
|
255
|
+
|
256
|
+
def main():
|
257
|
+
typer.run(typer_main)
|
258
|
+
|
259
|
+
if __name__ == "__main__":
|
260
|
+
main()
|
janito/changeviewer.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from rich.console import Console
|
3
|
+
from rich.text import Text
|
4
|
+
from typing import TypedDict
|
5
|
+
import difflib
|
6
|
+
|
7
|
+
class FileChange(TypedDict):
|
8
|
+
"""Type definition for a file change"""
|
9
|
+
description: str
|
10
|
+
new_content: str
|
11
|
+
|
12
|
+
def show_file_changes(console: Console, filepath: Path, original: str, new_content: str, description: str) -> None:
|
13
|
+
"""Display side by side comparison of file changes"""
|
14
|
+
half_width = (console.width - 3) // 2
|
15
|
+
|
16
|
+
# Show header
|
17
|
+
console.print(f"\n[bold blue]Changes for {filepath}[/bold blue]")
|
18
|
+
console.print(f"[dim]{description}[/dim]\n")
|
19
|
+
|
20
|
+
# Show side by side content
|
21
|
+
console.print(Text("OLD".center(half_width) + "│" + "NEW".center(half_width), style="blue bold"))
|
22
|
+
console.print(Text("─" * half_width + "┼" + "─" * half_width, style="blue"))
|
23
|
+
|
24
|
+
old_lines = original.splitlines()
|
25
|
+
new_lines = new_content.splitlines()
|
26
|
+
|
27
|
+
for i in range(max(len(old_lines), len(new_lines))):
|
28
|
+
old = old_lines[i] if i < len(old_lines) else ""
|
29
|
+
new = new_lines[i] if i < len(new_lines) else ""
|
30
|
+
|
31
|
+
old_text = Text(f"{old:<{half_width}}", style="red" if old != new else None)
|
32
|
+
new_text = Text(f"{new:<{half_width}}", style="green" if old != new else None)
|
33
|
+
console.print(old_text + Text("│", style="blue") + new_text)
|
34
|
+
|
35
|
+
def show_diff_changes(console: Console, filepath: Path, original: str, new_content: str, description: str) -> None:
|
36
|
+
"""Display file changes using unified diff format"""
|
37
|
+
# Show header
|
38
|
+
console.print(f"\n[bold blue]Changes for {filepath}[/bold blue]")
|
39
|
+
console.print(f"[dim]{description}[/dim]\n")
|
40
|
+
|
41
|
+
# Generate diff
|
42
|
+
diff = difflib.unified_diff(
|
43
|
+
original.splitlines(keepends=True),
|
44
|
+
new_content.splitlines(keepends=True),
|
45
|
+
fromfile='old',
|
46
|
+
tofile='new',
|
47
|
+
lineterm=''
|
48
|
+
)
|
49
|
+
|
50
|
+
# Print diff with colors
|
51
|
+
for line in diff:
|
52
|
+
if line.startswith('+++'):
|
53
|
+
console.print(Text(line.rstrip(), style="bold green"))
|
54
|
+
elif line.startswith('---'):
|
55
|
+
console.print(Text(line.rstrip(), style="bold red"))
|
56
|
+
elif line.startswith('+'):
|
57
|
+
console.print(Text(line.rstrip(), style="green"))
|
58
|
+
elif line.startswith('-'):
|
59
|
+
console.print(Text(line.rstrip(), style="red"))
|
60
|
+
elif line.startswith('@@'):
|
61
|
+
console.print(Text(line.rstrip(), style="cyan"))
|
62
|
+
else:
|
63
|
+
console.print(Text(line.rstrip()))
|
64
|
+
|
janito/claude.py
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
from rich.traceback import install
|
2
|
+
import anthropic
|
3
|
+
import os
|
4
|
+
from typing import Optional
|
5
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
6
|
+
from threading import Event
|
7
|
+
|
8
|
+
# Install rich traceback handler
|
9
|
+
install(show_locals=True)
|
10
|
+
|
11
|
+
class ClaudeAPIAgent:
|
12
|
+
"""Handles interaction with Claude API, including message handling"""
|
13
|
+
def __init__(self, api_key: Optional[str] = None, system_prompt: str = None):
|
14
|
+
if not system_prompt:
|
15
|
+
raise ValueError("system_prompt is required")
|
16
|
+
self.api_key = api_key or os.getenv('ANTHROPIC_API_KEY')
|
17
|
+
if not self.api_key:
|
18
|
+
raise ValueError("ANTHROPIC_API_KEY environment variable is required")
|
19
|
+
self.client = anthropic.Client(api_key=self.api_key)
|
20
|
+
self.model = "claude-3-5-sonnet-20241022"
|
21
|
+
self.stop_progress = Event()
|
22
|
+
self.system_message = system_prompt
|
23
|
+
self.last_prompt = None
|
24
|
+
self.last_full_message = None
|
25
|
+
self.last_response = None
|
26
|
+
self.messages_history = []
|
27
|
+
if system_prompt:
|
28
|
+
self.messages_history.append(("system", system_prompt))
|
29
|
+
|
30
|
+
def send_message(self, message: str, stop_event: Event = None) -> str:
|
31
|
+
"""Send message to Claude API and return response"""
|
32
|
+
try:
|
33
|
+
self.messages_history.append(("user", message))
|
34
|
+
# Store the full message
|
35
|
+
self.last_full_message = message
|
36
|
+
|
37
|
+
try:
|
38
|
+
# Check if already cancelled
|
39
|
+
if stop_event and stop_event.is_set():
|
40
|
+
return ""
|
41
|
+
|
42
|
+
# Start API request
|
43
|
+
response = self.client.messages.create(
|
44
|
+
model=self.model, # Use discovered model
|
45
|
+
system=self.system_message,
|
46
|
+
max_tokens=4000,
|
47
|
+
messages=[
|
48
|
+
{"role": "user", "content": message}
|
49
|
+
],
|
50
|
+
temperature=0,
|
51
|
+
)
|
52
|
+
|
53
|
+
# Handle response
|
54
|
+
response_text = response.content[0].text
|
55
|
+
|
56
|
+
# Only store and process response if not cancelled
|
57
|
+
if not (stop_event and stop_event.is_set()):
|
58
|
+
self.last_response = response_text
|
59
|
+
self.messages_history.append(("assistant", response_text))
|
60
|
+
|
61
|
+
# Always return the response, let caller handle cancellation
|
62
|
+
return response_text
|
63
|
+
|
64
|
+
except KeyboardInterrupt:
|
65
|
+
if stop_event:
|
66
|
+
stop_event.set()
|
67
|
+
return ""
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
error_msg = f"Error: {str(e)}"
|
71
|
+
self.messages_history.append(("error", error_msg))
|
72
|
+
if stop_event and stop_event.is_set():
|
73
|
+
return ""
|
74
|
+
return error_msg
|
janito/config.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
import os
|
3
|
+
|
4
|
+
class ConfigManager:
|
5
|
+
_instance = None
|
6
|
+
|
7
|
+
def __init__(self):
|
8
|
+
self.debug = False
|
9
|
+
self.verbose = False
|
10
|
+
self.debug_line = None # Add this line
|
11
|
+
|
12
|
+
@classmethod
|
13
|
+
def get_instance(cls) -> "ConfigManager":
|
14
|
+
if cls._instance is None:
|
15
|
+
cls._instance = cls()
|
16
|
+
return cls._instance
|
17
|
+
|
18
|
+
def set_debug(self, enabled: bool) -> None:
|
19
|
+
self.debug = enabled
|
20
|
+
|
21
|
+
def set_verbose(self, enabled: bool) -> None:
|
22
|
+
self.verbose = enabled
|
23
|
+
|
24
|
+
def set_debug_line(self, line: Optional[int]) -> None: # Add this method
|
25
|
+
self.debug_line = line
|
26
|
+
|
27
|
+
def should_debug_line(self, line: int) -> bool: # Add this method
|
28
|
+
"""Return True if we should show debug for this line number"""
|
29
|
+
return self.debug and (self.debug_line is None or self.debug_line == line)
|
30
|
+
|
31
|
+
# Create a singleton instance
|
32
|
+
config = ConfigManager.get_instance()
|
janito/console.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
from prompt_toolkit import PromptSession
|
2
|
+
from prompt_toolkit.history import FileHistory
|
3
|
+
from pathlib import Path
|
4
|
+
from rich.console import Console
|
5
|
+
from janito.claude import ClaudeAPIAgent
|
6
|
+
from janito.prompts import build_request_analisys_prompt, SYSTEM_PROMPT
|
7
|
+
from janito.scan import collect_files_content
|
8
|
+
from janito.__main__ import handle_option_selection
|
9
|
+
|
10
|
+
def start_console_session(workdir: Path, include: list[Path] = None) -> None:
|
11
|
+
"""Start an interactive console session using prompt_toolkit"""
|
12
|
+
console = Console()
|
13
|
+
claude = ClaudeAPIAgent(system_prompt=SYSTEM_PROMPT)
|
14
|
+
|
15
|
+
# Setup prompt session with history
|
16
|
+
history_file = workdir / '.janito' / 'console_history'
|
17
|
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
18
|
+
session = PromptSession(history=FileHistory(str(history_file)))
|
19
|
+
|
20
|
+
from importlib.metadata import version
|
21
|
+
try:
|
22
|
+
ver = version("janito")
|
23
|
+
except:
|
24
|
+
ver = "dev"
|
25
|
+
|
26
|
+
console.print("\n[bold blue]╔═══════════════════════════════════════════╗[/bold blue]")
|
27
|
+
console.print("[bold blue]║ Janito AI Assistant ║[/bold blue]")
|
28
|
+
console.print("[bold blue]║ v" + ver.ljust(8) + " ║[/bold blue]")
|
29
|
+
console.print("[bold blue]╠═══════════════════════════════════════════╣[/bold blue]")
|
30
|
+
console.print("[bold blue]║ Your AI-powered development companion ║[/bold blue]")
|
31
|
+
console.print("[bold blue]╚═══════════════════════════════════════════╝[/bold blue]")
|
32
|
+
console.print("\n[cyan]Type your requests or 'exit' to quit[/cyan]\n")
|
33
|
+
|
34
|
+
while True:
|
35
|
+
try:
|
36
|
+
request = session.prompt("janito> ")
|
37
|
+
if request.lower() in ('exit', 'quit'):
|
38
|
+
break
|
39
|
+
|
40
|
+
if not request.strip():
|
41
|
+
continue
|
42
|
+
|
43
|
+
# Get current files content
|
44
|
+
paths_to_scan = [workdir] if workdir else []
|
45
|
+
if include:
|
46
|
+
paths_to_scan.extend(include)
|
47
|
+
files_content = collect_files_content(paths_to_scan, workdir)
|
48
|
+
|
49
|
+
# Get initial analysis
|
50
|
+
initial_prompt = build_request_analisys_prompt(files_content, request)
|
51
|
+
initial_response = claude.send_message(initial_prompt)
|
52
|
+
|
53
|
+
# Show response and handle options
|
54
|
+
console.print(initial_response)
|
55
|
+
handle_option_selection(claude, initial_response, request, False, workdir, include)
|
56
|
+
|
57
|
+
except KeyboardInterrupt:
|
58
|
+
continue
|
59
|
+
except EOFError:
|
60
|
+
break
|
janito/contentchange.py
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Dict, Tuple, TypedDict, List
|
4
|
+
from rich.console import Console
|
5
|
+
from rich.prompt import Confirm
|
6
|
+
import tempfile
|
7
|
+
from janito.changeviewer import show_file_changes, FileChange, show_diff_changes
|
8
|
+
import ast
|
9
|
+
from datetime import datetime
|
10
|
+
import shutil
|
11
|
+
|
12
|
+
def get_file_type(filepath: Path) -> str:
|
13
|
+
"""Determine the type of saved file based on its name"""
|
14
|
+
name = filepath.name.lower()
|
15
|
+
if 'changes' in name:
|
16
|
+
return 'changes'
|
17
|
+
elif 'selected' in name:
|
18
|
+
return 'selected'
|
19
|
+
elif 'analysis' in name:
|
20
|
+
return 'analysis'
|
21
|
+
elif 'response' in name:
|
22
|
+
return 'response'
|
23
|
+
return 'unknown'
|
24
|
+
|
25
|
+
def parse_block_changes(content: str) -> Dict[Path, FileChange]:
|
26
|
+
"""Parse file changes from code blocks in the content.
|
27
|
+
Returns dict mapping filepath -> FileChange"""
|
28
|
+
changes = {}
|
29
|
+
pattern = r'##\s*([\da-f-]+)\s+([^\n]+)\s+begin\s*"([^"]*)"[^\n]*##\n(.*?)##\s*\1\s+\2\s+end\s*##'
|
30
|
+
matches = re.finditer(pattern, content, re.DOTALL)
|
31
|
+
|
32
|
+
for match in matches:
|
33
|
+
filepath = Path(match.group(2))
|
34
|
+
description = match.group(3)
|
35
|
+
file_content = match.group(4).strip()
|
36
|
+
changes[filepath] = FileChange(
|
37
|
+
description=description,
|
38
|
+
new_content=file_content
|
39
|
+
)
|
40
|
+
|
41
|
+
return changes
|
42
|
+
|
43
|
+
def save_changes_to_history(content: str, request: str, workdir: Path) -> Path:
|
44
|
+
"""Save change content to history folder with timestamp and request info"""
|
45
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Already in the correct format
|
46
|
+
history_dir = workdir / '.janito' / 'history'
|
47
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
48
|
+
|
49
|
+
# Create history entry with request and changes
|
50
|
+
history_file = history_dir / f"changes_{timestamp}.txt"
|
51
|
+
|
52
|
+
history_content = f"""Request: {request}
|
53
|
+
Timestamp: {timestamp}
|
54
|
+
|
55
|
+
Changes:
|
56
|
+
{content}
|
57
|
+
"""
|
58
|
+
history_file.write_text(history_content)
|
59
|
+
return history_file
|
60
|
+
|
61
|
+
def process_and_save_changes(content: str, request: str, workdir: Path) -> Tuple[Dict[Path, Tuple[str, str]], Path]:
|
62
|
+
"""Parse changes and save to history, returns (changes_dict, history_file)"""
|
63
|
+
changes = parse_block_changes(content)
|
64
|
+
history_file = save_changes_to_history(content, request, workdir)
|
65
|
+
return changes, history_file
|
66
|
+
|
67
|
+
def validate_python_syntax(content: str, filepath: Path) -> Tuple[bool, str]:
|
68
|
+
"""Validate Python syntax and return (is_valid, error_message)"""
|
69
|
+
try:
|
70
|
+
ast.parse(content)
|
71
|
+
return True, ""
|
72
|
+
except SyntaxError as e:
|
73
|
+
return False, f"Line {e.lineno}: {e.msg}"
|
74
|
+
except Exception as e:
|
75
|
+
return False, str(e)
|
76
|
+
|
77
|
+
def format_parsed_changes(changes: Dict[Path, Tuple[str, str]]) -> str:
|
78
|
+
"""Format parsed changes to show only file change descriptions"""
|
79
|
+
result = []
|
80
|
+
for filepath, (_, description) in changes.items(): # Updated tuple unpacking
|
81
|
+
result.append(f"=== {filepath} ===\n{description}\n")
|
82
|
+
return "\n".join(result)
|
83
|
+
|
84
|
+
def validate_changes(changes: Dict[Path, FileChange]) -> Tuple[bool, List[Tuple[Path, str]]]:
|
85
|
+
"""Validate all changes, returns (is_valid, list of errors)"""
|
86
|
+
errors = []
|
87
|
+
for filepath, change in changes.items():
|
88
|
+
if filepath.suffix == '.py':
|
89
|
+
is_valid, error = validate_python_syntax(change['new_content'], filepath)
|
90
|
+
if not is_valid:
|
91
|
+
errors.append((filepath, error))
|
92
|
+
return len(errors) == 0, errors
|
93
|
+
|
94
|
+
def preview_and_apply_changes(changes: Dict[Path, FileChange], workdir: Path) -> bool:
|
95
|
+
"""Preview changes in temporary directory and apply if confirmed."""
|
96
|
+
console = Console()
|
97
|
+
|
98
|
+
if not changes:
|
99
|
+
console.print("\n[yellow]No changes were found to apply[/yellow]")
|
100
|
+
return False
|
101
|
+
|
102
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
103
|
+
preview_dir = Path(temp_dir)
|
104
|
+
if workdir.exists():
|
105
|
+
shutil.copytree(workdir, preview_dir, dirs_exist_ok=True)
|
106
|
+
|
107
|
+
for filepath, change in changes.items():
|
108
|
+
# Get original content
|
109
|
+
orig_path = workdir / filepath
|
110
|
+
original = orig_path.read_text() if orig_path.exists() else ""
|
111
|
+
|
112
|
+
# Prepare preview
|
113
|
+
preview_path = preview_dir / filepath
|
114
|
+
preview_path.parent.mkdir(parents=True, exist_ok=True)
|
115
|
+
preview_path.write_text(change['new_content'])
|
116
|
+
|
117
|
+
# Show changes
|
118
|
+
show_diff_changes(console, filepath, original, change['new_content'], change['description'])
|
119
|
+
|
120
|
+
# Apply changes if confirmed
|
121
|
+
if Confirm.ask("\nApply these changes?"):
|
122
|
+
for filepath, _ in changes.items():
|
123
|
+
preview_path = preview_dir / filepath
|
124
|
+
target_path = workdir / filepath
|
125
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
126
|
+
shutil.copy2(preview_path, target_path)
|
127
|
+
console.print(f"[green]✓[/green] Applied changes to {filepath}")
|
128
|
+
return True
|
129
|
+
|
130
|
+
return False
|
131
|
+
|
132
|
+
def apply_content_changes(content: str, request: str, workdir: Path) -> Tuple[bool, Path]:
|
133
|
+
"""Regular flow: Parse content, save to history, and apply changes."""
|
134
|
+
console = Console()
|
135
|
+
changes = parse_block_changes(content)
|
136
|
+
|
137
|
+
if not changes:
|
138
|
+
console.print("\n[yellow]No file changes were found in the response[/yellow]")
|
139
|
+
return False, None
|
140
|
+
|
141
|
+
# Validate changes before proceeding
|
142
|
+
is_valid, errors = validate_changes(changes)
|
143
|
+
if not is_valid:
|
144
|
+
console = Console()
|
145
|
+
console.print("\n[red bold]⚠️ Cannot apply changes: Python syntax errors detected![/red bold]")
|
146
|
+
for filepath, error in errors:
|
147
|
+
console.print(f"\n[red]⚠️ {filepath}: {error}[/red]")
|
148
|
+
return False, None
|
149
|
+
|
150
|
+
history_file = save_changes_to_history(content, request, workdir)
|
151
|
+
success = preview_and_apply_changes(changes, workdir)
|
152
|
+
return success, history_file
|
153
|
+
|
154
|
+
def handle_changes_file(filepath: Path, workdir: Path) -> Tuple[bool, Path]:
|
155
|
+
"""Replay flow: Load changes from file and apply them."""
|
156
|
+
content = filepath.read_text()
|
157
|
+
changes = parse_block_changes(content)
|
158
|
+
|
159
|
+
if not changes:
|
160
|
+
console = Console()
|
161
|
+
console.print("\n[yellow]No file changes were found in the file[/yellow]")
|
162
|
+
return False, None
|
163
|
+
|
164
|
+
success = preview_and_apply_changes(changes, workdir)
|
165
|
+
return success, filepath
|
janito/prompts.py
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
# Core system prompt focused on role and purpose
|
4
|
+
SYSTEM_PROMPT = """You are Janito, an AI assistant for software development tasks. Be concise.
|
5
|
+
"""
|
6
|
+
|
7
|
+
|
8
|
+
CHANGE_ANALISYS_PROMPT = """
|
9
|
+
Current files:
|
10
|
+
<files>
|
11
|
+
{files_content}
|
12
|
+
</files>
|
13
|
+
|
14
|
+
Considering the current files content, provide a table of options for the requested change.
|
15
|
+
Always provide options using a header label "=== **Option 1** : ...", "=== **Option 2**: ...", etc.
|
16
|
+
Provide the header with a short description followed by the file changes on the next line
|
17
|
+
What files should be modified and what should they contain? (one line description)
|
18
|
+
Do not provide the content of any of the file suggested to be created or modified.
|
19
|
+
|
20
|
+
Request:
|
21
|
+
{request}
|
22
|
+
"""
|
23
|
+
|
24
|
+
SELECTED_OPTION_PROMPT = """
|
25
|
+
Original request: {request}
|
26
|
+
|
27
|
+
Please provide detailed implementation using the following guide:
|
28
|
+
{option_text}
|
29
|
+
|
30
|
+
Current files:
|
31
|
+
<files>
|
32
|
+
{files_content}
|
33
|
+
</files>
|
34
|
+
|
35
|
+
After checking the above files and the provided implementation, please provide the following:
|
36
|
+
|
37
|
+
## <uuid4> filename begin "short description of the change" ##
|
38
|
+
<entire file content>
|
39
|
+
## <uuid4> filename end ##
|
40
|
+
|
41
|
+
ALWAYS provide the entire file content, not just the changes.
|
42
|
+
If no changes are needed answer to any worksppace just reply <
|
43
|
+
"""
|
44
|
+
|
45
|
+
def build_selected_option_prompt(option_number: int, request: str, initial_response: str, files_content: str = "") -> str:
|
46
|
+
"""Build prompt for selected option details"""
|
47
|
+
options = parse_options(initial_response)
|
48
|
+
if option_number not in options:
|
49
|
+
raise ValueError(f"Option {option_number} not found in response")
|
50
|
+
|
51
|
+
return SELECTED_OPTION_PROMPT.format(
|
52
|
+
option_text=options[option_number],
|
53
|
+
request=request,
|
54
|
+
files_content=files_content
|
55
|
+
)
|
56
|
+
|
57
|
+
def parse_options(response: str) -> dict[int, str]:
|
58
|
+
"""Parse options from the response text, including any list items after the option label"""
|
59
|
+
options = {}
|
60
|
+
pattern = r"===\s*\*\*Option (\d+)\*\*\s*:\s*(.+?)(?====\s*\*\*Option|\Z)"
|
61
|
+
matches = re.finditer(pattern, response, re.DOTALL)
|
62
|
+
|
63
|
+
for match in matches:
|
64
|
+
option_num = int(match.group(1))
|
65
|
+
option_text = match.group(2).strip()
|
66
|
+
|
67
|
+
# Split into description and list items
|
68
|
+
lines = option_text.splitlines()
|
69
|
+
description = lines[0]
|
70
|
+
list_items = []
|
71
|
+
|
72
|
+
# Collect list items that follow
|
73
|
+
for line in lines[1:]:
|
74
|
+
line = line.strip()
|
75
|
+
if line.startswith(('- ', '* ', '• ')):
|
76
|
+
list_items.append(line)
|
77
|
+
elif not line:
|
78
|
+
continue
|
79
|
+
else:
|
80
|
+
break
|
81
|
+
|
82
|
+
# Combine description with list items if any exist
|
83
|
+
if list_items:
|
84
|
+
option_text = description + '\n' + '\n'.join(list_items)
|
85
|
+
|
86
|
+
options[option_num] = option_text
|
87
|
+
|
88
|
+
return options
|
89
|
+
|
90
|
+
|
91
|
+
def build_request_analisys_prompt(files_content: str, request: str) -> str:
|
92
|
+
"""Build prompt for information requests"""
|
93
|
+
|
94
|
+
return CHANGE_ANALISYS_PROMPT.format(
|
95
|
+
files_content=files_content,
|
96
|
+
request=request
|
97
|
+
)
|
janito/qa.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.markdown import Markdown
|
3
|
+
from janito.claude import ClaudeAPIAgent
|
4
|
+
|
5
|
+
QA_PROMPT = """Please provide a clear and concise answer to the following question about the codebase:
|
6
|
+
|
7
|
+
Question: {question}
|
8
|
+
|
9
|
+
Current files:
|
10
|
+
<files>
|
11
|
+
{files_content}
|
12
|
+
</files>
|
13
|
+
|
14
|
+
Focus on providing factual information and explanations. Do not suggest code changes.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def ask_question(question: str, files_content: str, claude: ClaudeAPIAgent) -> str:
|
18
|
+
"""Process a question about the codebase and return the answer"""
|
19
|
+
prompt = QA_PROMPT.format(
|
20
|
+
question=question,
|
21
|
+
files_content=files_content
|
22
|
+
)
|
23
|
+
return claude.send_message(prompt)
|
24
|
+
|
25
|
+
def display_answer(answer: str, raw: bool = False) -> None:
|
26
|
+
"""Display the answer in markdown or raw format"""
|
27
|
+
console = Console()
|
28
|
+
if raw:
|
29
|
+
console.print(answer)
|
30
|
+
else:
|
31
|
+
md = Markdown(answer)
|
32
|
+
console.print(md)
|
janito/scan.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List, Tuple
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.columns import Columns
|
5
|
+
from janito.config import config
|
6
|
+
from pathspec import PathSpec
|
7
|
+
from pathspec.patterns import GitWildMatchPattern
|
8
|
+
|
9
|
+
|
10
|
+
SPECIAL_FILES = ["README.md", "__init__.py", "__main__.py"]
|
11
|
+
|
12
|
+
def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], List[str]]:
|
13
|
+
"""Common scanning logic used by both preview and content collection"""
|
14
|
+
content_parts = []
|
15
|
+
file_items = []
|
16
|
+
skipped_files = []
|
17
|
+
console = Console()
|
18
|
+
|
19
|
+
# Load gitignore if it exists
|
20
|
+
gitignore_path = workdir / '.gitignore' if workdir else None
|
21
|
+
gitignore_spec = None
|
22
|
+
if gitignore_path and gitignore_path.exists():
|
23
|
+
with open(gitignore_path) as f:
|
24
|
+
gitignore = f.read()
|
25
|
+
gitignore_spec = PathSpec.from_lines(GitWildMatchPattern, gitignore.splitlines())
|
26
|
+
|
27
|
+
|
28
|
+
def scan_path(path: Path, level: int) -> None:
|
29
|
+
"""
|
30
|
+
Scan a path and add it to the content_parts list
|
31
|
+
level 0 means we are scanning the root directory
|
32
|
+
level 1 we provide both directory directory name and file content
|
33
|
+
level > 1 we just return
|
34
|
+
"""
|
35
|
+
if level > 1:
|
36
|
+
return
|
37
|
+
|
38
|
+
relative_base = workdir
|
39
|
+
if path.is_dir():
|
40
|
+
relative_path = path.relative_to(relative_base)
|
41
|
+
content_parts.append(f'<directory><path>{relative_path}</path>not sent</directory>')
|
42
|
+
file_items.append(f"[blue]•[/blue] {relative_path}/")
|
43
|
+
# Check for special files
|
44
|
+
special_found = []
|
45
|
+
for special_file in SPECIAL_FILES:
|
46
|
+
if (path / special_file).exists():
|
47
|
+
special_found.append(special_file)
|
48
|
+
if special_found:
|
49
|
+
file_items[-1] = f"[blue]•[/blue] {relative_path}/ [cyan]({', '.join(special_found)})[/cyan]"
|
50
|
+
for special_file in special_found:
|
51
|
+
special_path = path / special_file
|
52
|
+
try:
|
53
|
+
relative_path = special_path.relative_to(relative_base)
|
54
|
+
file_content = special_path.read_text(encoding='utf-8')
|
55
|
+
content_parts.append(f"<file>\n<path>{relative_path}</path>\n<content>\n{file_content}\n</content>\n</file>")
|
56
|
+
except UnicodeDecodeError:
|
57
|
+
skipped_files.append(str(relative_path))
|
58
|
+
console.print(f"[yellow]Warning: Skipping file due to encoding issues: {relative_path}[/yellow]")
|
59
|
+
|
60
|
+
for item in path.iterdir():
|
61
|
+
# Skip if matches gitignore patterns
|
62
|
+
if gitignore_spec:
|
63
|
+
rel_path = str(item.relative_to(workdir))
|
64
|
+
if gitignore_spec.match_file(rel_path):
|
65
|
+
continue
|
66
|
+
scan_path(item, level+1)
|
67
|
+
|
68
|
+
else:
|
69
|
+
relative_path = path.relative_to(relative_base)
|
70
|
+
# check if file is binary
|
71
|
+
try:
|
72
|
+
if path.is_file() and path.read_bytes().find(b'\x00') != -1:
|
73
|
+
console.print(f"[red]Skipped binary file found: {relative_path}[/red]")
|
74
|
+
return
|
75
|
+
file_content = path.read_text(encoding='utf-8')
|
76
|
+
content_parts.append(f"<file>\n<path>{relative_path}</path>\n<content>\n{file_content}\n</content>\n</file>")
|
77
|
+
file_items.append(f"[cyan]•[/cyan] {relative_path}")
|
78
|
+
except UnicodeDecodeError:
|
79
|
+
skipped_files.append(str(relative_path))
|
80
|
+
console.print(f"[yellow]Warning: Skipping file due to encoding issues: {relative_path}[/yellow]")
|
81
|
+
|
82
|
+
for path in paths:
|
83
|
+
scan_path(path, 0)
|
84
|
+
|
85
|
+
if skipped_files and config.verbose:
|
86
|
+
console.print("\n[yellow]Files skipped due to encoding issues:[/yellow]")
|
87
|
+
for file in skipped_files:
|
88
|
+
console.print(f" • {file}")
|
89
|
+
|
90
|
+
return content_parts, file_items
|
91
|
+
|
92
|
+
def collect_files_content(paths: List[Path], workdir: Path = None) -> str:
|
93
|
+
"""Collect content from all files in XML format"""
|
94
|
+
console = Console()
|
95
|
+
content_parts, file_items = _scan_paths(paths, workdir)
|
96
|
+
|
97
|
+
if file_items and config.verbose:
|
98
|
+
console.print("\n[bold blue]Contents being analyzed:[/bold blue]")
|
99
|
+
console.print(Columns(file_items, padding=(0, 4), expand=True))
|
100
|
+
|
101
|
+
return "\n".join(content_parts)
|
102
|
+
|
103
|
+
def preview_scan(paths: List[Path], workdir: Path = None) -> None:
|
104
|
+
"""Preview what files and directories would be scanned"""
|
105
|
+
console = Console()
|
106
|
+
_, file_items = _scan_paths(paths, workdir)
|
107
|
+
|
108
|
+
# Change message based on whether we're scanning included paths or workdir
|
109
|
+
if len(paths) == 1 and paths[0] == workdir:
|
110
|
+
console.print(f"\n[bold blue]Scanning working directory:[/bold blue] {workdir.absolute()}")
|
111
|
+
else:
|
112
|
+
console.print(f"\n[bold blue]Working directory:[/bold blue] {workdir.absolute()}")
|
113
|
+
console.print("\n[bold blue]Scanning included paths:[/bold blue]")
|
114
|
+
for path in paths:
|
115
|
+
console.print(f" • {path.absolute()}")
|
116
|
+
|
117
|
+
console.print("\n[bold blue]Files that would be analyzed:[/bold blue]")
|
118
|
+
console.print(Columns(file_items, padding=(0, 4), expand=True))
|
119
|
+
|
120
|
+
def is_dir_empty(path: Path) -> bool:
|
121
|
+
"""Check if directory is empty, ignoring hidden files"""
|
122
|
+
return not any(item for item in path.iterdir() if not item.name.startswith('.'))
|
@@ -0,0 +1,138 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: janito
|
3
|
+
Version: 0.3.0
|
4
|
+
Summary: A CLI tool for software development tasks powered by AI
|
5
|
+
Project-URL: Homepage, https://github.com/joaompinto/janito
|
6
|
+
Project-URL: Repository, https://github.com/joaompinto/janito.git
|
7
|
+
Author-email: João Pinto <lamego.pinto@gmail.com>
|
8
|
+
License: MIT
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Environment :: Console
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
16
|
+
Classifier: Topic :: Software Development
|
17
|
+
Requires-Python: >=3.8
|
18
|
+
Requires-Dist: anthropic
|
19
|
+
Requires-Dist: pathspec
|
20
|
+
Requires-Dist: rich
|
21
|
+
Requires-Dist: typer
|
22
|
+
Description-Content-Type: text/markdown
|
23
|
+
|
24
|
+
# 🤖 Janito CLI
|
25
|
+
|
26
|
+
A CLI tool for software development tasks powered by AI.
|
27
|
+
|
28
|
+
Janito is an AI-powered assistant that helps automate common software development tasks like refactoring, documentation updates, and code optimization.
|
29
|
+
|
30
|
+
## 📥 Installation
|
31
|
+
|
32
|
+
```bash
|
33
|
+
# Install from PyPI
|
34
|
+
pip install janito
|
35
|
+
|
36
|
+
# Install from source
|
37
|
+
git clone https://github.com/joaompinto/janito.git
|
38
|
+
cd janito
|
39
|
+
pip install -e .
|
40
|
+
```
|
41
|
+
|
42
|
+
## ⚡ Requirements
|
43
|
+
|
44
|
+
- Python 3.8+
|
45
|
+
- Anthropic API key
|
46
|
+
- Required packages (automatically installed):
|
47
|
+
- typer
|
48
|
+
- pathspec
|
49
|
+
- rich
|
50
|
+
|
51
|
+
## ⚙️ Configuration
|
52
|
+
|
53
|
+
### 🔑 API Key Setup
|
54
|
+
Janito requires an Anthropic API key to function. Set it as an environment variable:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
export ANTHROPIC_API_KEY='your-api-key-here'
|
58
|
+
```
|
59
|
+
|
60
|
+
You can also add this to your shell profile (~/.bashrc, ~/.zshrc, etc.) for persistence.
|
61
|
+
|
62
|
+
## 📖 Usage
|
63
|
+
|
64
|
+
Janito can be used in two modes: Command Line or Interactive Console.
|
65
|
+
|
66
|
+
### 💻 Command Line Mode
|
67
|
+
|
68
|
+
```bash
|
69
|
+
janito REQUEST [OPTIONS]
|
70
|
+
```
|
71
|
+
|
72
|
+
#### Arguments
|
73
|
+
- `REQUEST`: The modification request
|
74
|
+
|
75
|
+
#### Options
|
76
|
+
- `-w, --workdir PATH`: Working directory (defaults to current directory)
|
77
|
+
- `--raw`: Print raw response instead of markdown format
|
78
|
+
- `--play PATH`: Replay a saved prompt file
|
79
|
+
- `-i, --include PATH`: Additional paths to include in analysis
|
80
|
+
- `--debug`: Show debug information
|
81
|
+
- `-v, --verbose`: Show verbose output
|
82
|
+
- `--ask`: Ask a question about the codebase
|
83
|
+
- `--scan`: Preview files that would be analyzed
|
84
|
+
|
85
|
+
### 🖥️ Interactive Console Mode
|
86
|
+
|
87
|
+
Start the interactive console by running `janito` without arguments:
|
88
|
+
|
89
|
+
```bash
|
90
|
+
janito
|
91
|
+
```
|
92
|
+
|
93
|
+
In console mode, you can:
|
94
|
+
- Enter requests directly
|
95
|
+
- Navigate history with up/down arrows
|
96
|
+
- Use special commands starting with /
|
97
|
+
|
98
|
+
### 📝 Examples
|
99
|
+
|
100
|
+
```bash
|
101
|
+
# Command Line Mode Examples
|
102
|
+
janito "create docstrings for all functions"
|
103
|
+
janito "add error handling" -w ./myproject
|
104
|
+
janito "update tests" -i ./tests -i ./lib
|
105
|
+
janito --ask "explain the authentication flow"
|
106
|
+
janito --scan # Preview files to be analyzed
|
107
|
+
|
108
|
+
# Console Mode
|
109
|
+
janito # Starts interactive session
|
110
|
+
```
|
111
|
+
|
112
|
+
## ✨ Features
|
113
|
+
|
114
|
+
- 🤖 AI-powered code analysis and modifications
|
115
|
+
- 💻 Interactive console mode for continuous interaction
|
116
|
+
- 📁 Support for multiple file types
|
117
|
+
- ✅ Syntax validation for Python files
|
118
|
+
- 👀 Interactive change preview and confirmation
|
119
|
+
- 📜 History tracking of all changes
|
120
|
+
- 🐛 Debug and verbose output modes
|
121
|
+
- ❓ Question-answering about codebase
|
122
|
+
- 🔍 File scanning preview
|
123
|
+
|
124
|
+
## 📚 History and Debugging
|
125
|
+
|
126
|
+
Changes are automatically saved in `.janito/history/` with timestamps:
|
127
|
+
- `*_analysis.txt`: Initial analysis
|
128
|
+
- `*_selected.txt`: Selected implementation
|
129
|
+
- `*_changes.txt`: Actual changes
|
130
|
+
|
131
|
+
Enable debug mode for detailed logging:
|
132
|
+
```bash
|
133
|
+
janito "request" --debug
|
134
|
+
```
|
135
|
+
|
136
|
+
## 📄 License
|
137
|
+
|
138
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
janito/__init__.py,sha256=CLeVFqpY9Ki3R3MgLAiTjNsJjsj1BD3_9CzP2kgCj-k,52
|
2
|
+
janito/__main__.py,sha256=bGp3nWUZC5omneyE3hn78D_J5PkBfk19qKfMV1kIThI,9892
|
3
|
+
janito/changeviewer.py,sha256=C_CRdeD6dE4AIpOM_kryTn_HyD5XC-glaZO8n8zrQPE,2487
|
4
|
+
janito/claude.py,sha256=tj0lNNVE0CW0bBkbhVDFgBl0AFoMHicWaHnYuYOk3_E,2911
|
5
|
+
janito/config.py,sha256=YsS0bNVkjl7cIboP9nSDy0NXsJaVHYAIpPkc6bbErpo,967
|
6
|
+
janito/console.py,sha256=ieKZ7IRbvJO_KW8A3CU4FCoO4YFEjJQT8BN98YotAnM,2770
|
7
|
+
janito/contentchange.py,sha256=BxFmW8JtRjzX5lnfGfzo0JPRnGRw1RQEtqZCK1wVArw,6404
|
8
|
+
janito/prompts.py,sha256=XonVVbfIg3YY1Dpbkx9m0ZSRE4bgDP3MYHO3D-5FcIE,3080
|
9
|
+
janito/qa.py,sha256=F9bd18CBaZDpbJwwvwFL18gXPBA0cq8kRY0nA3_AKPY,916
|
10
|
+
janito/scan.py,sha256=5JV0crOepVUqCZ3LAUqCJL2yerLFvjZ6RYdOwIGHvX0,5450
|
11
|
+
janito-0.3.0.dist-info/METADATA,sha256=fq7zvCHx_PV8RND5SVfOMnM082BEptZQ2G2qWPOVisQ,3681
|
12
|
+
janito-0.3.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
13
|
+
janito-0.3.0.dist-info/entry_points.txt,sha256=wIo5zZxbmu4fC-ZMrsKD0T0vq7IqkOOLYhrqRGypkx4,48
|
14
|
+
janito-0.3.0.dist-info/licenses/LICENSE,sha256=xLIUXRPjtsgQml2zD1Pn4LpgiyZ49raw6jZDlO_gZdo,1062
|
15
|
+
janito-0.3.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 João Pinto
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|