janito 0.4.0__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.
- janito/__init__.py +1 -1
- janito/__main__.py +102 -326
- janito/agents/__init__.py +16 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +13 -17
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/change/__init__.py +32 -0
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +23 -0
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +61 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/change/analysis/prompts.py +98 -0
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +171 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +245 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +131 -0
- janito/change/history.py +44 -0
- janito/change/operations.py +7 -0
- janito/change/parser.py +289 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +126 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +251 -0
- janito/change/viewer/__init__.py +11 -0
- janito/change/viewer/content.py +66 -0
- janito/change/viewer/diff.py +43 -0
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/change/viewer/themes.py +55 -0
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/__init__.py +2 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +45 -0
- janito/cli/functions.py +111 -0
- janito/cli/handlers/ask.py +22 -0
- janito/cli/handlers/demo.py +22 -0
- janito/cli/handlers/request.py +24 -0
- janito/cli/handlers/scan.py +9 -0
- janito/cli/history.py +61 -0
- janito/cli/registry.py +26 -0
- janito/common.py +41 -10
- janito/config.py +71 -6
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompts.py +1 -65
- janito/qa.py +8 -5
- janito/review.py +13 -0
- janito/search_replace/README.md +146 -0
- janito/search_replace/__init__.py +6 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +119 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +299 -0
- janito/shell/__init__.py +39 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +195 -0
- janito/shell/handlers.py +122 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +52 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +7 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/manager.py +48 -0
- janito/workspace/scan.py +232 -0
- janito-0.6.0.dist-info/METADATA +185 -0
- janito-0.6.0.dist-info/RECORD +95 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito/contentchange.py +0 -84
- janito/contextparser.py +0 -113
- janito/fileparser.py +0 -125
- janito/scan.py +0 -137
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/contentchange.py
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Dict, Tuple
|
3
|
-
from rich.console import Console
|
4
|
-
from datetime import datetime
|
5
|
-
from janito.fileparser import FileChange, parse_block_changes
|
6
|
-
from janito.changeapplier import preview_and_apply_changes
|
7
|
-
|
8
|
-
def get_file_type(filepath: Path) -> str:
|
9
|
-
"""Determine the type of saved file based on its name"""
|
10
|
-
name = filepath.name.lower()
|
11
|
-
if 'changes' in name:
|
12
|
-
return 'changes'
|
13
|
-
elif 'selected' in name:
|
14
|
-
return 'selected'
|
15
|
-
elif 'analysis' in name:
|
16
|
-
return 'analysis'
|
17
|
-
elif 'response' in name:
|
18
|
-
return 'response'
|
19
|
-
return 'unknown'
|
20
|
-
|
21
|
-
def save_changes_to_history(content: str, request: str, workdir: Path) -> Path:
|
22
|
-
"""Save change content to history folder with timestamp and request info"""
|
23
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Already in the correct format
|
24
|
-
history_dir = workdir / '.janito' / 'history'
|
25
|
-
history_dir.mkdir(parents=True, exist_ok=True)
|
26
|
-
|
27
|
-
# Create history entry with request and changes
|
28
|
-
history_file = history_dir / f"changes_{timestamp}.txt"
|
29
|
-
|
30
|
-
history_content = f"""Request: {request}
|
31
|
-
Timestamp: {timestamp}
|
32
|
-
|
33
|
-
Changes:
|
34
|
-
{content}
|
35
|
-
"""
|
36
|
-
history_file.write_text(history_content)
|
37
|
-
return history_file
|
38
|
-
|
39
|
-
def process_and_save_changes(content: str, request: str, workdir: Path) -> Tuple[Dict[Path, Tuple[str, str]], Path]:
|
40
|
-
"""Parse changes and save to history, returns (changes_dict, history_file)"""
|
41
|
-
changes = parse_block_changes(content)
|
42
|
-
history_file = save_changes_to_history(content, request, workdir)
|
43
|
-
return changes, history_file
|
44
|
-
|
45
|
-
def validate_python_syntax(content: str, filepath: Path) -> Tuple[bool, str]:
|
46
|
-
"""Validate Python syntax and return (is_valid, error_message)"""
|
47
|
-
try:
|
48
|
-
ast.parse(content)
|
49
|
-
return True, ""
|
50
|
-
except SyntaxError as e:
|
51
|
-
return False, f"Line {e.lineno}: {e.msg}"
|
52
|
-
|
53
|
-
def format_parsed_changes(changes: Dict[Path, Tuple[str, str]]) -> str:
|
54
|
-
"""Format parsed changes to show only file change descriptions"""
|
55
|
-
result = []
|
56
|
-
for filepath, (_, description) in changes.items(): # Updated tuple unpacking
|
57
|
-
result.append(f"=== {filepath} ===\n{description}\n")
|
58
|
-
return "\n".join(result)
|
59
|
-
|
60
|
-
def apply_content_changes(content: str, request: str, workdir: Path, test_cmd: str = None) -> Tuple[bool, Path]:
|
61
|
-
"""Regular flow: Parse content, save to history, and apply changes."""
|
62
|
-
console = Console()
|
63
|
-
changes = parse_block_changes(content)
|
64
|
-
|
65
|
-
if not changes:
|
66
|
-
console.print("\n[yellow]No file changes were found in the response[/yellow]")
|
67
|
-
return False, None
|
68
|
-
|
69
|
-
history_file = save_changes_to_history(content, request, workdir)
|
70
|
-
success = preview_and_apply_changes(changes, workdir, test_cmd)
|
71
|
-
return success, history_file
|
72
|
-
|
73
|
-
def handle_changes_file(filepath: Path, workdir: Path, test_cmd: str = None) -> Tuple[bool, Path]:
|
74
|
-
"""Replay flow: Load changes from file and apply them."""
|
75
|
-
content = filepath.read_text()
|
76
|
-
changes = parse_block_changes(content)
|
77
|
-
|
78
|
-
if not changes:
|
79
|
-
console = Console()
|
80
|
-
console.print("\n[yellow]No file changes were found in the file[/yellow]")
|
81
|
-
return False, None
|
82
|
-
|
83
|
-
success = preview_and_apply_changes(changes, workdir, test_cmd)
|
84
|
-
return success, filepath
|
janito/contextparser.py
DELETED
@@ -1,113 +0,0 @@
|
|
1
|
-
from typing import List, Tuple, Optional, NamedTuple
|
2
|
-
from difflib import SequenceMatcher
|
3
|
-
from janito.config import config
|
4
|
-
from rich.console import Console
|
5
|
-
|
6
|
-
class ContextError(NamedTuple):
|
7
|
-
"""Contains error details for context matching failures"""
|
8
|
-
pre_context: List[str]
|
9
|
-
post_context: List[str]
|
10
|
-
content: str
|
11
|
-
|
12
|
-
def parse_change_block(content: str) -> Tuple[List[str], List[str], List[str]]:
|
13
|
-
"""Parse a change block into pre-context, post-context and change lines.
|
14
|
-
Returns (pre_context_lines, post_context_lines, change_lines)"""
|
15
|
-
pre_context_lines = []
|
16
|
-
post_context_lines = []
|
17
|
-
change_lines = []
|
18
|
-
in_pre_context = True
|
19
|
-
|
20
|
-
for line in content.splitlines():
|
21
|
-
if line.startswith('='):
|
22
|
-
if in_pre_context:
|
23
|
-
pre_context_lines.append(line[1:])
|
24
|
-
else:
|
25
|
-
post_context_lines.append(line[1:])
|
26
|
-
elif line.startswith('>'):
|
27
|
-
in_pre_context = False
|
28
|
-
change_lines.append(line[1:])
|
29
|
-
|
30
|
-
return pre_context_lines, post_context_lines, change_lines
|
31
|
-
|
32
|
-
def find_context_match(file_content: str, pre_context: List[str], post_context: List[str], min_context: int = 2) -> Optional[Tuple[int, int]]:
|
33
|
-
"""Find exact matching location using line-by-line matching.
|
34
|
-
Returns (start_index, end_index) or None if no match found."""
|
35
|
-
if not (pre_context or post_context) or (len(pre_context) + len(post_context)) < min_context:
|
36
|
-
return None
|
37
|
-
|
38
|
-
file_lines = file_content.splitlines()
|
39
|
-
|
40
|
-
# Function to check if lines match at a given position
|
41
|
-
def lines_match_at(pos: int, target_lines: List[str]) -> bool:
|
42
|
-
if pos + len(target_lines) > len(file_lines):
|
43
|
-
return False
|
44
|
-
return all(a == b for a, b in zip(file_lines[pos:pos + len(target_lines)], target_lines))
|
45
|
-
|
46
|
-
# For debug output
|
47
|
-
debug_matches = []
|
48
|
-
|
49
|
-
# Try to find pre_context match
|
50
|
-
pre_match_pos = None
|
51
|
-
if pre_context:
|
52
|
-
for i in range(len(file_lines) - len(pre_context) + 1):
|
53
|
-
if lines_match_at(i, pre_context):
|
54
|
-
pre_match_pos = i
|
55
|
-
break
|
56
|
-
if config.debug:
|
57
|
-
# Record first 20 non-matches for debug output
|
58
|
-
if len(debug_matches) < 20:
|
59
|
-
debug_matches.append((i, file_lines[i:i + len(pre_context)]))
|
60
|
-
|
61
|
-
# Try to find post_context match after pre_context if found
|
62
|
-
if pre_match_pos is not None and post_context:
|
63
|
-
expected_post_pos = pre_match_pos + len(pre_context)
|
64
|
-
if not lines_match_at(expected_post_pos, post_context):
|
65
|
-
pre_match_pos = None
|
66
|
-
|
67
|
-
if pre_match_pos is None and config.debug:
|
68
|
-
console = Console()
|
69
|
-
console.print("\n[bold red]Context Match Debug:[/bold red]")
|
70
|
-
|
71
|
-
if pre_context:
|
72
|
-
console.print("\n[yellow]Expected pre-context:[/yellow]")
|
73
|
-
for i, line in enumerate(pre_context):
|
74
|
-
console.print(f" {i+1:2d} | '{line}'")
|
75
|
-
|
76
|
-
if post_context:
|
77
|
-
console.print("\n[yellow]Expected post-context:[/yellow]")
|
78
|
-
for i, line in enumerate(post_context):
|
79
|
-
console.print(f" {i+1:2d} | '{line}'")
|
80
|
-
|
81
|
-
console.print("\n[yellow]First 20 attempted matches in file:[/yellow]")
|
82
|
-
for pos, lines in debug_matches:
|
83
|
-
console.print(f"\n[cyan]At line {pos+1}:[/cyan]")
|
84
|
-
for i, line in enumerate(lines):
|
85
|
-
match_status = "≠" if i < len(pre_context) and line != pre_context[i] else "="
|
86
|
-
console.print(f" {i+1:2d} | '{line}' {match_status}")
|
87
|
-
|
88
|
-
return None
|
89
|
-
|
90
|
-
if pre_match_pos is None:
|
91
|
-
return None
|
92
|
-
|
93
|
-
end_pos = pre_match_pos + len(pre_context)
|
94
|
-
|
95
|
-
return pre_match_pos, end_pos
|
96
|
-
|
97
|
-
def apply_changes(content: str,
|
98
|
-
pre_context_lines: List[str],
|
99
|
-
post_context_lines: List[str],
|
100
|
-
change_lines: List[str]) -> Optional[Tuple[str, Optional[ContextError]]]:
|
101
|
-
"""Apply changes with context matching, returns (new_content, error_details)"""
|
102
|
-
if not content.strip() and not pre_context_lines and not post_context_lines:
|
103
|
-
return '\n'.join(change_lines), None
|
104
|
-
|
105
|
-
pre_context = '\n'.join(pre_context_lines)
|
106
|
-
post_context = '\n'.join(post_context_lines)
|
107
|
-
|
108
|
-
if pre_context and pre_context not in content:
|
109
|
-
return None, ContextError(pre_context_lines, post_context_lines, content)
|
110
|
-
|
111
|
-
if post_context and post_context not in content:
|
112
|
-
return None, ContextError(pre_context_lines, post_context_lines, content)
|
113
|
-
|
janito/fileparser.py
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Dict, Tuple, List, Optional
|
3
|
-
from dataclasses import dataclass
|
4
|
-
import re
|
5
|
-
import ast
|
6
|
-
import sys # Add this import
|
7
|
-
from rich.console import Console
|
8
|
-
from rich.panel import Panel # Add this import
|
9
|
-
from janito.config import config # Add this import
|
10
|
-
|
11
|
-
@dataclass
|
12
|
-
class FileChange:
|
13
|
-
"""Represents a file change with search/replace, search/delete or create instructions"""
|
14
|
-
description: str
|
15
|
-
is_new_file: bool
|
16
|
-
content: str = "" # For new files
|
17
|
-
search_blocks: List[Tuple[str, Optional[str], Optional[str]]] = None # (search, replace, description)
|
18
|
-
|
19
|
-
def add_search_block(self, search: str, replace: Optional[str], description: Optional[str] = None) -> None:
|
20
|
-
"""Add a search/replace or search/delete block with optional description"""
|
21
|
-
if self.search_blocks is None:
|
22
|
-
self.search_blocks = []
|
23
|
-
self.search_blocks.append((search, replace, description))
|
24
|
-
|
25
|
-
def validate_python_syntax(content: str, filepath: Path) -> Tuple[bool, str]:
|
26
|
-
"""Validate Python syntax and return (is_valid, error_message)"""
|
27
|
-
try:
|
28
|
-
ast.parse(content)
|
29
|
-
console = Console()
|
30
|
-
console.print(f"[green]✓ Python syntax validation passed:[/green] {filepath.absolute()}")
|
31
|
-
return True, ""
|
32
|
-
except SyntaxError as e:
|
33
|
-
error_msg = f"Line {e.lineno}: {e.msg}"
|
34
|
-
console = Console()
|
35
|
-
console.print(f"[red]✗ Python syntax validation failed:[/red] {filepath.absolute()}")
|
36
|
-
console.print(f"[red] {error_msg}[/red]")
|
37
|
-
return False, error_msg
|
38
|
-
|
39
|
-
|
40
|
-
def parse_block_changes(response_text: str) -> Dict[Path, FileChange]:
|
41
|
-
"""Parse file changes from response blocks"""
|
42
|
-
changes = {}
|
43
|
-
console = Console()
|
44
|
-
# Match file blocks with UUID
|
45
|
-
file_pattern = r'## ([a-f0-9]{8}) file (.*?) (modify|create) "(.*?)" ##\n?(.*?)## \1 file end ##'
|
46
|
-
|
47
|
-
for match in re.finditer(file_pattern, response_text, re.DOTALL):
|
48
|
-
uuid, filepath, action, description, content = match.groups()
|
49
|
-
path = Path(filepath.strip())
|
50
|
-
|
51
|
-
if action == 'create':
|
52
|
-
changes[path] = FileChange(
|
53
|
-
description=description,
|
54
|
-
is_new_file=True,
|
55
|
-
content=content[1:] if content.startswith('\n') else content,
|
56
|
-
search_blocks=[]
|
57
|
-
)
|
58
|
-
continue
|
59
|
-
|
60
|
-
# For modifications, find all search/replace and search/delete blocks
|
61
|
-
search_blocks = []
|
62
|
-
block_patterns = [
|
63
|
-
# Match search/replace blocks with description - updated pattern
|
64
|
-
(r'## ' + re.escape(uuid) + r' search/replace "(.*?)" ##\n?(.*?)## ' +
|
65
|
-
re.escape(uuid) + r' replace with ##\n?(.*?)(?=## ' + re.escape(uuid) + r'|$)', False),
|
66
|
-
# Match search/delete blocks with description
|
67
|
-
(r'## ' + re.escape(uuid) + r' search/delete "(.*?)" ##\n?(.*?)(?=## ' + re.escape(uuid) + r'|$)', True)
|
68
|
-
]
|
69
|
-
|
70
|
-
if config.debug:
|
71
|
-
console.print("\n[blue]Updated regex patterns:[/blue]")
|
72
|
-
for pattern, is_delete in block_patterns:
|
73
|
-
console.print(Panel(pattern, title="Search/Replace Pattern" if not is_delete else "Search/Delete Pattern", border_style="blue"))
|
74
|
-
|
75
|
-
for pattern, is_delete in block_patterns:
|
76
|
-
if config.debug:
|
77
|
-
console.print(f"\n[blue]Looking for pattern:[/blue]")
|
78
|
-
console.print(Panel(pattern, title="Pattern", border_style="blue"))
|
79
|
-
console.print(f"\n[blue]In content:[/blue]")
|
80
|
-
console.print(Panel(content, title="Content", border_style="blue"))
|
81
|
-
|
82
|
-
for block_match in re.finditer(pattern, content, re.DOTALL):
|
83
|
-
if is_delete:
|
84
|
-
description, search = block_match.groups()
|
85
|
-
search = search.rstrip('\n') + '\n' # Ensure single trailing newline
|
86
|
-
replace = None
|
87
|
-
else:
|
88
|
-
description, search, replace = block_match.groups()
|
89
|
-
search = search.rstrip('\n') + '\n' # Ensure single trailing newline
|
90
|
-
replace = (replace.rstrip('\n') + '\n') if replace else None
|
91
|
-
|
92
|
-
# Abort parsing if replace content is empty
|
93
|
-
if not is_delete and (replace is None or replace.strip() == ''):
|
94
|
-
console.print(f"\n[red]Error: Empty replace content found![/red]")
|
95
|
-
console.print(f"[red]File:[/red] {filepath}")
|
96
|
-
console.print(f"[red]Description:[/red] {description}")
|
97
|
-
console.print("[yellow]Search block:[/yellow]")
|
98
|
-
console.print(Panel(search, title="Search Content", border_style="yellow"))
|
99
|
-
console.print("[red]Replace block is empty or contains only whitespace![/red]")
|
100
|
-
console.print("[red]Aborting due to empty replace content.[/red]")
|
101
|
-
sys.exit(1)
|
102
|
-
|
103
|
-
# Enhanced debug info
|
104
|
-
if config.debug or (not is_delete and (replace is None or replace.strip() == '')):
|
105
|
-
console.print(f"\n[yellow]Search/Replace block analysis:[/yellow]")
|
106
|
-
console.print(f"[yellow]File:[/yellow] {filepath}")
|
107
|
-
console.print(f"[yellow]Description:[/yellow] {description}")
|
108
|
-
console.print("[yellow]Search block:[/yellow]")
|
109
|
-
console.print(Panel(search, title="Search Content", border_style="yellow"))
|
110
|
-
console.print("[yellow]Replace block:[/yellow]")
|
111
|
-
console.print(Panel(replace if replace else "<empty>", title="Replace Content", border_style="yellow"))
|
112
|
-
console.print("\n[blue]Match groups:[/blue]")
|
113
|
-
for i, group in enumerate(block_match.groups()):
|
114
|
-
console.print(Panel(str(group), title=f"Group {i}", border_style="blue"))
|
115
|
-
|
116
|
-
search_blocks.append((search, replace, description))
|
117
|
-
|
118
|
-
# Add debug info if no blocks were found
|
119
|
-
if config.debug and not search_blocks:
|
120
|
-
console.print(f"\n[red]No search/replace blocks found for file:[/red] {filepath}")
|
121
|
-
console.print("[red]Check if the content format matches the expected patterns[/red]")
|
122
|
-
|
123
|
-
changes[path] = FileChange(description=description, is_new_file=False, search_blocks=search_blocks)
|
124
|
-
|
125
|
-
return changes
|
janito/scan.py
DELETED
@@ -1,137 +0,0 @@
|
|
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
|
-
|
104
|
-
def preview_scan(paths: List[Path], workdir: Path = None) -> None:
|
105
|
-
"""Preview what files and directories would be scanned"""
|
106
|
-
console = Console()
|
107
|
-
_, file_items = _scan_paths(paths, workdir)
|
108
|
-
|
109
|
-
# Display working directory status
|
110
|
-
console.print("\n[bold blue]Analysis Paths:[/bold blue]")
|
111
|
-
console.print(f"[cyan]Working Directory:[/cyan] {workdir.absolute()}")
|
112
|
-
|
113
|
-
# Show if working directory is being scanned
|
114
|
-
is_workdir_scanned = any(p.resolve() == workdir.resolve() for p in paths)
|
115
|
-
if is_workdir_scanned:
|
116
|
-
console.print("[green]✓ Working directory will be scanned[/green]")
|
117
|
-
else:
|
118
|
-
console.print("[yellow]! Working directory will not be scanned[/yellow]")
|
119
|
-
|
120
|
-
# Show included paths relative to working directory
|
121
|
-
if len(paths) > (1 if is_workdir_scanned else 0):
|
122
|
-
console.print("\n[cyan]Additional Included Paths:[/cyan]")
|
123
|
-
for path in paths:
|
124
|
-
if path.resolve() != workdir.resolve():
|
125
|
-
try:
|
126
|
-
rel_path = path.relative_to(workdir)
|
127
|
-
console.print(f" • ./{rel_path}")
|
128
|
-
except ValueError:
|
129
|
-
# Path is outside working directory
|
130
|
-
console.print(f" • {path.absolute()}")
|
131
|
-
|
132
|
-
console.print("\n[bold blue]Files that will be analyzed:[/bold blue]")
|
133
|
-
console.print(Columns(file_items, padding=(0, 4), expand=True))
|
134
|
-
|
135
|
-
def is_dir_empty(path: Path) -> bool:
|
136
|
-
"""Check if directory is empty, ignoring hidden files"""
|
137
|
-
return not any(item for item in path.iterdir() if not item.name.startswith('.'))
|
janito-0.4.0.dist-info/METADATA
DELETED
@@ -1,164 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.3
|
2
|
-
Name: janito
|
3
|
-
Version: 0.4.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: tomli
|
22
|
-
Requires-Dist: typer
|
23
|
-
Description-Content-Type: text/markdown
|
24
|
-
|
25
|
-
# 🤖 Janito CLI
|
26
|
-
|
27
|
-
A CLI tool for software development tasks powered by AI.
|
28
|
-
|
29
|
-
Meet Janito, your friendly AI-powered software development buddy! Janito helps you with coding tasks like refactoring, documentation updates, and code optimization while being clear and concise in its responses.
|
30
|
-
|
31
|
-
## 📥 Installation
|
32
|
-
|
33
|
-
```bash
|
34
|
-
# Install from PyPI
|
35
|
-
pip install janito
|
36
|
-
|
37
|
-
# Install from source
|
38
|
-
git clone https://github.com/joaompinto/janito.git
|
39
|
-
cd janito
|
40
|
-
pip install -e .
|
41
|
-
```
|
42
|
-
|
43
|
-
## ⚡ Requirements
|
44
|
-
|
45
|
-
- Python 3.8+
|
46
|
-
- Anthropic API key
|
47
|
-
- Required packages (automatically installed):
|
48
|
-
- typer
|
49
|
-
- pathspec
|
50
|
-
- rich
|
51
|
-
|
52
|
-
## ⚙️ Configuration
|
53
|
-
|
54
|
-
### 🔑 API Key Setup
|
55
|
-
Janito requires an Anthropic API key to function. Set it as an environment variable:
|
56
|
-
|
57
|
-
```bash
|
58
|
-
export ANTHROPIC_API_KEY='your-api-key-here'
|
59
|
-
```
|
60
|
-
|
61
|
-
You can also add this to your shell profile (~/.bashrc, ~/.zshrc, etc.) for persistence.
|
62
|
-
|
63
|
-
### ⚙️ Test Command Setup
|
64
|
-
You can configure a test command to run before applying changes:
|
65
|
-
|
66
|
-
```bash
|
67
|
-
export JANITO_TEST_CMD='your-test-command'
|
68
|
-
```
|
69
|
-
|
70
|
-
This command will be executed in the preview directory before applying changes to verify they don't break anything.
|
71
|
-
|
72
|
-
## 📖 Usage
|
73
|
-
|
74
|
-
Janito can be used in two modes: Command Line or Interactive Console.
|
75
|
-
|
76
|
-
### 💻 Command Line Mode
|
77
|
-
|
78
|
-
```bash
|
79
|
-
janito REQUEST [OPTIONS]
|
80
|
-
```
|
81
|
-
|
82
|
-
#### Arguments
|
83
|
-
- `REQUEST`: The modification request to process (optional)
|
84
|
-
|
85
|
-
#### Options
|
86
|
-
##### General Options
|
87
|
-
- `--version`: Show version and exit
|
88
|
-
- `-w, --workdir PATH`: Working directory (defaults to current directory)
|
89
|
-
- `-i, --include PATH`: Additional paths to include in analysis (can be used multiple times)
|
90
|
-
|
91
|
-
##### Operation Modes
|
92
|
-
- `--ask TEXT`: Ask a question about the codebase instead of making changes
|
93
|
-
- `--scan`: Preview files that would be analyzed without making changes
|
94
|
-
- `--play PATH`: Replay a saved prompt file
|
95
|
-
|
96
|
-
##### Output Control
|
97
|
-
- `--raw`: Print raw response instead of markdown format
|
98
|
-
- `-v, --verbose`: Show verbose output
|
99
|
-
- `--debug`: Show debug information
|
100
|
-
|
101
|
-
##### Testing
|
102
|
-
- `-t, --test COMMAND`: Test command to run before applying changes (overrides JANITO_TEST_CMD)
|
103
|
-
|
104
|
-
### 🖥️ Interactive Console Mode
|
105
|
-
|
106
|
-
Start the interactive console by running `janito` without arguments:
|
107
|
-
|
108
|
-
```bash
|
109
|
-
janito
|
110
|
-
```
|
111
|
-
|
112
|
-
In console mode, you can:
|
113
|
-
- Enter requests directly
|
114
|
-
- Navigate history with up/down arrows
|
115
|
-
- Use special commands starting with /
|
116
|
-
|
117
|
-
### 📝 Examples
|
118
|
-
|
119
|
-
```bash
|
120
|
-
# Command Line Mode Examples
|
121
|
-
janito "create docstrings for all functions"
|
122
|
-
janito "add error handling" -w ./myproject
|
123
|
-
janito "update tests" -i ./tests -i ./lib
|
124
|
-
janito --ask "explain the authentication flow"
|
125
|
-
janito --scan # Preview files to be analyzed
|
126
|
-
|
127
|
-
# Test Command Examples
|
128
|
-
janito "update code" --test "pytest" # Run pytest before applying changes
|
129
|
-
janito "refactor module" -t "make test" # Run make test before applying
|
130
|
-
export JANITO_TEST_CMD="python -m unittest" # Set default test command
|
131
|
-
janito "optimize function" # Will use JANITO_TEST_CMD
|
132
|
-
|
133
|
-
# Console Mode
|
134
|
-
janito # Starts interactive session
|
135
|
-
```
|
136
|
-
|
137
|
-
## ✨ Features
|
138
|
-
|
139
|
-
- 🤖 AI-powered code analysis and modifications
|
140
|
-
- 💻 Interactive console mode for continuous interaction
|
141
|
-
- 📁 Support for multiple file types
|
142
|
-
- ✅ Syntax validation for Python files
|
143
|
-
- 👀 Interactive change preview and confirmation
|
144
|
-
- 📜 History tracking of all changes
|
145
|
-
- 🐛 Debug and verbose output modes
|
146
|
-
- ❓ Question-answering about codebase
|
147
|
-
- 🔍 File scanning preview
|
148
|
-
- 🧪 Test command execution before applying changes
|
149
|
-
|
150
|
-
## 📚 History and Debugging
|
151
|
-
|
152
|
-
Changes are automatically saved in `.janito/history/` with timestamps:
|
153
|
-
- `*_analysis.txt`: Initial analysis
|
154
|
-
- `*_selected.txt`: Selected implementation
|
155
|
-
- `*_changes.txt`: Actual changes
|
156
|
-
|
157
|
-
Enable debug mode for detailed logging:
|
158
|
-
```bash
|
159
|
-
janito "request" --debug
|
160
|
-
```
|
161
|
-
|
162
|
-
## 📄 License
|
163
|
-
|
164
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
janito-0.4.0.dist-info/RECORD
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
janito/__init__.py,sha256=CLeVFqpY9Ki3R3MgLAiTjNsJjsj1BD3_9CzP2kgCj-k,52
|
2
|
-
janito/__main__.py,sha256=DqbMlHNWCX-XwiZz1qEYP3qAADKtfmlDhhbTgpXB5_M,14158
|
3
|
-
janito/analysis.py,sha256=dAzMARAcDZ1LN6KC2-Xb0bIB0p5VsaT2lhurtPLLIKk,9368
|
4
|
-
janito/changeapplier.py,sha256=fDP87Hh3uY8qoSv3-SSnafLCRJHLPQZ5U8GIMkdVZ9Y,17697
|
5
|
-
janito/changeviewer.py,sha256=51J0pvGe1rfYpiYo0mhZMonsYJmyAN44mg1TfKRxdM4,12716
|
6
|
-
janito/claude.py,sha256=N7ZX6WCrLKcj9na3o2MMPqH8tlobHYtIG1HnhgMVUKw,2272
|
7
|
-
janito/common.py,sha256=1blM7rY55-mLPXD4L3xwS-RaJ2tBZx8xHhaLYQku3Rk,801
|
8
|
-
janito/config.py,sha256=ocg0lyab9ysgczKaqJTAtv0ZRa2VDMwclTJBgps7Vxw,1171
|
9
|
-
janito/console.py,sha256=E2mLPoBw7Gh1K1hPuCoRDBEFE-klIERt5KFgE3fHqUg,11100
|
10
|
-
janito/contentchange.py,sha256=2HkNhqA_ZIXDHfiv3xRcwfoxMzFUNMuFfbMtf-4jhRM,3282
|
11
|
-
janito/contextparser.py,sha256=iDX6nlqUQr-lj7lkEI4B6jHLci_Kxl-XWOaEiAQtVxA,4531
|
12
|
-
janito/fileparser.py,sha256=8nngFmR9s6NVv2YuMcNKO4udNh-ZcKN4_Jo11vKzLhI,6586
|
13
|
-
janito/prompts.py,sha256=aUDDx16L2ygPJZwrWn4FmrXt1KGNfLeYRcDSPD9BzIA,1879
|
14
|
-
janito/qa.py,sha256=qrAXbsGPCRSW378V88w8e19uU3hfbDnx6lsiRYhq324,1927
|
15
|
-
janito/scan.py,sha256=c_IKSuWx1_525RlIgPtP-tFoH7lManVGGNFMxVg1hMk,6014
|
16
|
-
janito/version.py,sha256=ylfPwGtdY8dEOFJ-DB9gKUQLggqRCvoLxhpnwjzCM94,739
|
17
|
-
janito-0.4.0.dist-info/METADATA,sha256=uUCven38ndkbyLrrLw3B42P7AGyijqTGICKRzLJkDDE,4705
|
18
|
-
janito-0.4.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
19
|
-
janito-0.4.0.dist-info/entry_points.txt,sha256=wIo5zZxbmu4fC-ZMrsKD0T0vq7IqkOOLYhrqRGypkx4,48
|
20
|
-
janito-0.4.0.dist-info/licenses/LICENSE,sha256=xLIUXRPjtsgQml2zD1Pn4LpgiyZ49raw6jZDlO_gZdo,1062
|
21
|
-
janito-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|