janito 0.5.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 +0 -47
- janito/__main__.py +96 -15
- janito/agents/__init__.py +2 -8
- janito/agents/claudeai.py +3 -12
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- 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/{analysis → change/analysis}/prompts.py +35 -12
- 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/{changehistory.py → change/history.py} +12 -14
- 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/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- 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/base.py +30 -0
- janito/cli/commands.py +30 -38
- janito/cli/functions.py +19 -194
- 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/common.py +34 -3
- 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 -80
- janito/qa.py +4 -3
- 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.5.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/console/commands.py
DELETED
@@ -1,112 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import List
|
3
|
-
from rich.console import Console
|
4
|
-
from rich.panel import Panel
|
5
|
-
from janito.agents import AIAgent
|
6
|
-
from janito.analysis import build_request_analysis_prompt
|
7
|
-
from janito.scan import collect_files_content
|
8
|
-
from janito.common import progress_send_message
|
9
|
-
from janito.__main__ import handle_option_selection
|
10
|
-
from .display import display_help
|
11
|
-
|
12
|
-
def process_command(command: str, args: str, workdir: Path, include: List[Path], agent: AIAgent) -> None:
|
13
|
-
"""Process console commands using CLI functions for consistent behavior"""
|
14
|
-
console = Console()
|
15
|
-
|
16
|
-
# Parse command options
|
17
|
-
raw = False
|
18
|
-
verbose = False
|
19
|
-
debug = False
|
20
|
-
test_cmd = None
|
21
|
-
|
22
|
-
# Extract options from args
|
23
|
-
words = args.split()
|
24
|
-
filtered_args = []
|
25
|
-
i = 0
|
26
|
-
while i < len(words):
|
27
|
-
if words[i] == '--raw':
|
28
|
-
raw = True
|
29
|
-
elif words[i] == '--verbose':
|
30
|
-
verbose = True
|
31
|
-
elif words[i] == '--debug':
|
32
|
-
debug = True
|
33
|
-
elif words[i] == '--test' and i + 1 < len(words):
|
34
|
-
test_cmd = words[i + 1]
|
35
|
-
i += 1
|
36
|
-
else:
|
37
|
-
filtered_args.append(words[i])
|
38
|
-
i += 1
|
39
|
-
|
40
|
-
args = ' '.join(filtered_args)
|
41
|
-
|
42
|
-
# Update config with command options
|
43
|
-
from janito.config import config
|
44
|
-
config.set_debug(debug)
|
45
|
-
config.set_verbose(verbose)
|
46
|
-
config.set_test_cmd(test_cmd)
|
47
|
-
|
48
|
-
# Remove leading slash if present
|
49
|
-
command = command.lstrip('/')
|
50
|
-
|
51
|
-
# Handle command aliases
|
52
|
-
command_aliases = {
|
53
|
-
'h': 'help',
|
54
|
-
'a': 'ask',
|
55
|
-
'r': 'request',
|
56
|
-
'q': 'quit',
|
57
|
-
'exit': 'quit'
|
58
|
-
}
|
59
|
-
command = command_aliases.get(command, command)
|
60
|
-
|
61
|
-
if command == "help":
|
62
|
-
display_help()
|
63
|
-
return
|
64
|
-
|
65
|
-
if command == "quit":
|
66
|
-
raise EOFError()
|
67
|
-
|
68
|
-
if command == "ask":
|
69
|
-
if not args:
|
70
|
-
console.print(Panel(
|
71
|
-
"[red]Ask command requires a question[/red]",
|
72
|
-
title="Error",
|
73
|
-
border_style="red"
|
74
|
-
))
|
75
|
-
return
|
76
|
-
|
77
|
-
# Use CLI question processing function
|
78
|
-
from janito.__main__ import process_question
|
79
|
-
process_question(args, workdir, include, raw, claude)
|
80
|
-
return
|
81
|
-
|
82
|
-
if command == "request":
|
83
|
-
if not args:
|
84
|
-
console.print(Panel(
|
85
|
-
"[red]Request command requires a description[/red]",
|
86
|
-
title="Error",
|
87
|
-
border_style="red"
|
88
|
-
))
|
89
|
-
return
|
90
|
-
|
91
|
-
paths_to_scan = [workdir] if workdir else []
|
92
|
-
if include:
|
93
|
-
paths_to_scan.extend(include)
|
94
|
-
files_content = collect_files_content(paths_to_scan, workdir)
|
95
|
-
|
96
|
-
# Use CLI request processing functions
|
97
|
-
initial_prompt = build_request_analysis_prompt(files_content, args)
|
98
|
-
initial_response = progress_send_message(initial_prompt)
|
99
|
-
|
100
|
-
from janito.__main__ import save_to_file
|
101
|
-
save_to_file(initial_response, 'analysis', workdir)
|
102
|
-
|
103
|
-
from janito.analysis import format_analysis
|
104
|
-
format_analysis(initial_response, raw)
|
105
|
-
handle_option_selection(initial_response, args, raw, workdir, include)
|
106
|
-
return
|
107
|
-
|
108
|
-
console.print(Panel(
|
109
|
-
f"[red]Unknown command: /{command}[/red]\nType '/help' for available commands",
|
110
|
-
title="Error",
|
111
|
-
border_style="red"
|
112
|
-
))
|
janito/console/core.py
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import List, Optional
|
3
|
-
from prompt_toolkit import PromptSession
|
4
|
-
from prompt_toolkit.history import FileHistory
|
5
|
-
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
6
|
-
from rich.console import Console
|
7
|
-
from janito.agents import AgentSingleton
|
8
|
-
from janito.prompts import SYSTEM_PROMPT
|
9
|
-
from .display import create_completer, format_prompt, display_welcome
|
10
|
-
from .commands import process_command
|
11
|
-
|
12
|
-
def start_console_session(workdir: Path, include: Optional[List[Path]] = None) -> None:
|
13
|
-
"""Start an enhanced interactive console session"""
|
14
|
-
console = Console()
|
15
|
-
agent = AgentSingleton.get_agent()
|
16
|
-
|
17
|
-
# Setup history with persistence
|
18
|
-
history_file = workdir / '.janito' / 'console_history'
|
19
|
-
history_file.parent.mkdir(parents=True, exist_ok=True)
|
20
|
-
|
21
|
-
# Create session with history and completions
|
22
|
-
session = PromptSession(
|
23
|
-
history=FileHistory(str(history_file)),
|
24
|
-
completer=create_completer(workdir),
|
25
|
-
auto_suggest=AutoSuggestFromHistory(),
|
26
|
-
complete_while_typing=True
|
27
|
-
)
|
28
|
-
|
29
|
-
display_welcome(workdir)
|
30
|
-
|
31
|
-
while True:
|
32
|
-
try:
|
33
|
-
# Get input with formatted prompt
|
34
|
-
user_input = session.prompt(
|
35
|
-
lambda: format_prompt(workdir),
|
36
|
-
complete_while_typing=True
|
37
|
-
).strip()
|
38
|
-
|
39
|
-
if not user_input:
|
40
|
-
continue
|
41
|
-
|
42
|
-
if user_input.lower() in ('exit', 'quit'):
|
43
|
-
console.print("\n[cyan]Goodbye! Have a great day![/cyan]\n")
|
44
|
-
break
|
45
|
-
|
46
|
-
# Split input into command and args
|
47
|
-
parts = user_input.split(maxsplit=1)
|
48
|
-
if parts[0].startswith('/'): # Handle /command format
|
49
|
-
command = parts[0][1:] # Remove the / prefix
|
50
|
-
else:
|
51
|
-
command = "request" # Default to request if no command specified
|
52
|
-
|
53
|
-
args = parts[1] if len(parts) > 1 else ""
|
54
|
-
|
55
|
-
# Process command with separated args
|
56
|
-
process_command(command, args, workdir, include, claude)
|
57
|
-
|
58
|
-
except KeyboardInterrupt:
|
59
|
-
continue
|
60
|
-
except EOFError:
|
61
|
-
console.print("\n[cyan]Goodbye! Have a great day![/cyan]\n")
|
62
|
-
break
|
janito/console/display.py
DELETED
@@ -1,157 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
import shutil
|
3
|
-
from prompt_toolkit.completion import WordCompleter
|
4
|
-
from prompt_toolkit.formatted_text import HTML
|
5
|
-
from rich.console import Console
|
6
|
-
from rich.panel import Panel
|
7
|
-
from rich.table import Table
|
8
|
-
from rich.layout import Layout
|
9
|
-
from importlib.metadata import version
|
10
|
-
|
11
|
-
def create_completer(workdir: Path) -> WordCompleter:
|
12
|
-
"""Create command completer with common commands and paths"""
|
13
|
-
commands = [
|
14
|
-
'ask', 'request', 'help', 'exit', 'quit',
|
15
|
-
'--raw', '--verbose', '--debug', '--test'
|
16
|
-
]
|
17
|
-
return WordCompleter(commands, ignore_case=True)
|
18
|
-
|
19
|
-
def format_prompt(workdir: Path) -> HTML:
|
20
|
-
"""Format the prompt with current directory"""
|
21
|
-
cwd = workdir.name
|
22
|
-
return HTML(f'<ansigreen>janito</ansigreen> <ansiblue>{cwd}</ansiblue>> ')
|
23
|
-
|
24
|
-
def display_help() -> None:
|
25
|
-
"""Display available commands, options and their descriptions"""
|
26
|
-
console = Console()
|
27
|
-
|
28
|
-
layout = Layout()
|
29
|
-
layout.split_column(
|
30
|
-
Layout(name="header"),
|
31
|
-
Layout(name="commands"),
|
32
|
-
Layout(name="options"),
|
33
|
-
Layout(name="examples")
|
34
|
-
)
|
35
|
-
|
36
|
-
# Header
|
37
|
-
header_table = Table(box=None, show_header=False)
|
38
|
-
header_table.add_row("[bold cyan]Janito Console Help[/bold cyan]")
|
39
|
-
header_table.add_row("[dim]Your AI-powered software development buddy[/dim]")
|
40
|
-
|
41
|
-
# Commands table
|
42
|
-
commands_table = Table(title="Available Commands", box=None)
|
43
|
-
commands_table.add_column("Command", style="cyan", width=20)
|
44
|
-
commands_table.add_column("Description", style="white")
|
45
|
-
|
46
|
-
commands_table.add_row(
|
47
|
-
"/ask <text> (/a)",
|
48
|
-
"Ask a question about the codebase without making changes"
|
49
|
-
)
|
50
|
-
commands_table.add_row(
|
51
|
-
"<text> or /request <text> (/r)",
|
52
|
-
"Request code modifications or improvements"
|
53
|
-
)
|
54
|
-
commands_table.add_row(
|
55
|
-
"/help (/h)",
|
56
|
-
"Display this help message"
|
57
|
-
)
|
58
|
-
commands_table.add_row(
|
59
|
-
"/quit or /exit (/q)",
|
60
|
-
"Exit the console session"
|
61
|
-
)
|
62
|
-
|
63
|
-
# Options table
|
64
|
-
options_table = Table(title="Common Options", box=None)
|
65
|
-
options_table.add_column("Option", style="cyan", width=20)
|
66
|
-
options_table.add_column("Description", style="white")
|
67
|
-
|
68
|
-
options_table.add_row(
|
69
|
-
"--raw",
|
70
|
-
"Display raw response without formatting"
|
71
|
-
)
|
72
|
-
options_table.add_row(
|
73
|
-
"--verbose",
|
74
|
-
"Show additional information during execution"
|
75
|
-
)
|
76
|
-
options_table.add_row(
|
77
|
-
"--debug",
|
78
|
-
"Display detailed debug information"
|
79
|
-
)
|
80
|
-
options_table.add_row(
|
81
|
-
"--test <cmd>",
|
82
|
-
"Run specified test command before applying changes"
|
83
|
-
)
|
84
|
-
|
85
|
-
# Examples panel
|
86
|
-
examples = Panel(
|
87
|
-
"\n".join([
|
88
|
-
"[dim]Basic Commands:[/dim]",
|
89
|
-
" ask how does the error handling work?",
|
90
|
-
" request add input validation to user functions",
|
91
|
-
"",
|
92
|
-
"[dim]Using Options:[/dim]",
|
93
|
-
" request update tests --verbose",
|
94
|
-
" ask explain auth flow --raw",
|
95
|
-
" request optimize code --test 'pytest'",
|
96
|
-
"",
|
97
|
-
"[dim]Complex Examples:[/dim]",
|
98
|
-
" request refactor login function --verbose --test 'python -m unittest'",
|
99
|
-
" ask code structure --raw --debug"
|
100
|
-
]),
|
101
|
-
title="Examples",
|
102
|
-
border_style="blue"
|
103
|
-
)
|
104
|
-
|
105
|
-
# Update layout
|
106
|
-
layout["header"].update(header_table)
|
107
|
-
layout["commands"].update(commands_table)
|
108
|
-
layout["options"].update(options_table)
|
109
|
-
layout["examples"].update(examples)
|
110
|
-
|
111
|
-
console.print(layout)
|
112
|
-
|
113
|
-
def display_welcome(workdir: Path) -> None:
|
114
|
-
"""Display welcome message and console information"""
|
115
|
-
console = Console()
|
116
|
-
try:
|
117
|
-
ver = version("janito")
|
118
|
-
except:
|
119
|
-
ver = "dev"
|
120
|
-
|
121
|
-
term_width = shutil.get_terminal_size().columns
|
122
|
-
|
123
|
-
COLORS = {
|
124
|
-
'primary': '#729FCF', # Soft blue for primary elements
|
125
|
-
'secondary': '#8AE234', # Bright green for actions/success
|
126
|
-
'accent': '#AD7FA8', # Purple for accents
|
127
|
-
'muted': '#7F9F7F', # Muted green for less important text
|
128
|
-
}
|
129
|
-
|
130
|
-
welcome_text = (
|
131
|
-
f"[bold {COLORS['primary']}]Welcome to Janito v{ver}[/bold {COLORS['primary']}]\n"
|
132
|
-
f"[{COLORS['muted']}]Your AI-Powered Software Development Buddy[/{COLORS['muted']}]\n\n"
|
133
|
-
f"[{COLORS['accent']}]Keyboard Shortcuts:[/{COLORS['accent']}]\n"
|
134
|
-
"• ↑↓ : Navigate command history\n"
|
135
|
-
"• Tab : Complete commands and paths\n"
|
136
|
-
"• Ctrl+D : Exit console\n"
|
137
|
-
"• Ctrl+C : Cancel current operation\n\n"
|
138
|
-
f"[{COLORS['accent']}]Available Commands:[/{COLORS['accent']}]\n"
|
139
|
-
"• /ask (or /a) : Ask questions about code\n"
|
140
|
-
"• /request (or /r) : Request code changes\n"
|
141
|
-
"• /help (or /h) : Show detailed help\n"
|
142
|
-
"• /quit (or /q) : Exit console\n\n"
|
143
|
-
f"[{COLORS['secondary']}]Current Version:[/{COLORS['secondary']}] v{ver}\n"
|
144
|
-
f"[{COLORS['muted']}]Working Directory:[/{COLORS['muted']}] {workdir.absolute()}"
|
145
|
-
)
|
146
|
-
|
147
|
-
welcome_panel = Panel(
|
148
|
-
welcome_text,
|
149
|
-
width=min(80, term_width - 4),
|
150
|
-
border_style="blue",
|
151
|
-
title="Janito Console",
|
152
|
-
subtitle="Press Tab for completions"
|
153
|
-
)
|
154
|
-
|
155
|
-
console.print("\n")
|
156
|
-
console.print(welcome_panel)
|
157
|
-
console.print("\n[cyan]How can I help you with your code today?[/cyan]\n")
|
janito/fileparser.py
DELETED
@@ -1,334 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Dict, Tuple, List, Optional, Union
|
3
|
-
from dataclasses import dataclass
|
4
|
-
import re
|
5
|
-
import ast
|
6
|
-
import sys
|
7
|
-
import os # Add this import
|
8
|
-
from rich.console import Console
|
9
|
-
from rich.panel import Panel
|
10
|
-
from janito.config import config
|
11
|
-
|
12
|
-
def validate_file_path(filepath: Path) -> Tuple[bool, str]:
|
13
|
-
"""
|
14
|
-
Validate that the file path exists and is readable
|
15
|
-
|
16
|
-
Args:
|
17
|
-
filepath: Path object to validate
|
18
|
-
|
19
|
-
Returns:
|
20
|
-
Tuple[bool, str]: (is_valid, error_message)
|
21
|
-
"""
|
22
|
-
if not filepath.exists():
|
23
|
-
return False, f"File does not exist: {filepath}"
|
24
|
-
if not os.access(filepath, os.R_OK):
|
25
|
-
return False, f"File is not readable: {filepath}"
|
26
|
-
return True, ""
|
27
|
-
|
28
|
-
def validate_file_content(content: str) -> Tuple[bool, str]:
|
29
|
-
"""
|
30
|
-
Validate that the content is not empty and contains valid text
|
31
|
-
|
32
|
-
Args:
|
33
|
-
content: String content to validate
|
34
|
-
|
35
|
-
Returns:
|
36
|
-
Tuple[bool, str]: (is_valid, error_message)
|
37
|
-
"""
|
38
|
-
if not content:
|
39
|
-
return False, "Content cannot be empty"
|
40
|
-
if not isinstance(content, str):
|
41
|
-
return False, "Content must be a string"
|
42
|
-
return True, ""
|
43
|
-
|
44
|
-
@dataclass
|
45
|
-
class FileChange:
|
46
|
-
"""Represents a file change with search/replace, search/delete, create or replace instructions"""
|
47
|
-
path: Path
|
48
|
-
description: str
|
49
|
-
is_new_file: bool
|
50
|
-
content: str = "" # For new files or replace operations
|
51
|
-
original_content: str = "" # Original content for file replacements
|
52
|
-
search_blocks: List[Tuple[str, Optional[str], Optional[str]]] = None # (search, replace, description)
|
53
|
-
replace_file: bool = False # Flag for complete file replacement
|
54
|
-
remove_file: bool = False # Flag for file deletion
|
55
|
-
|
56
|
-
def add_search_block(self, search: str, replace: Optional[str], description: Optional[str] = None) -> None:
|
57
|
-
"""Add a search/replace or search/delete block with optional description"""
|
58
|
-
if self.search_blocks is None:
|
59
|
-
self.search_blocks = []
|
60
|
-
self.search_blocks.append((search, replace, description))
|
61
|
-
|
62
|
-
@dataclass
|
63
|
-
class FileBlock:
|
64
|
-
"""Raw file block data extracted from response"""
|
65
|
-
uuid: str
|
66
|
-
filepath: str
|
67
|
-
action: str
|
68
|
-
description: str
|
69
|
-
content: str
|
70
|
-
|
71
|
-
def validate_python_syntax(content: str, filepath: Path) -> Tuple[bool, str]:
|
72
|
-
"""Validate Python syntax and return (is_valid, error_message)"""
|
73
|
-
try:
|
74
|
-
ast.parse(content)
|
75
|
-
console = Console()
|
76
|
-
try:
|
77
|
-
rel_path = filepath.relative_to(Path.cwd())
|
78
|
-
display_path = f"./{rel_path}"
|
79
|
-
except ValueError:
|
80
|
-
display_path = str(filepath)
|
81
|
-
console.print(f"[green]✓ Python syntax validation passed:[/green] {display_path}")
|
82
|
-
return True, ""
|
83
|
-
except SyntaxError as e:
|
84
|
-
error_msg = f"Line {e.lineno}: {e.msg}"
|
85
|
-
console = Console()
|
86
|
-
try:
|
87
|
-
rel_path = filepath.relative_to(Path.cwd())
|
88
|
-
display_path = f"./{rel_path}"
|
89
|
-
except ValueError:
|
90
|
-
display_path = str(filepath)
|
91
|
-
console.print(f"[red]✗ Python syntax validation failed:[/red] {display_path}")
|
92
|
-
console.print(f"[red] {error_msg}[/red]")
|
93
|
-
return False, error_msg
|
94
|
-
|
95
|
-
def count_lines(text: str) -> int:
|
96
|
-
"""Count the number of lines in a text block."""
|
97
|
-
return len(text.splitlines())
|
98
|
-
|
99
|
-
def extract_file_blocks(response_text: str) -> List[Tuple[str, str, str, str]]:
|
100
|
-
"""Extract file blocks from response text and return list of (uuid, filepath, action, description, content)"""
|
101
|
-
file_blocks = []
|
102
|
-
console = Console()
|
103
|
-
|
104
|
-
# Find file blocks with improved quoted description handling
|
105
|
-
file_start_pattern = r'## ([a-f0-9]{8}) file (.*?) (modify|create|remove|replace)(?:\s+"([^"]*?)")?\s*##'
|
106
|
-
# Find the first UUID to check for duplicates
|
107
|
-
first_match = re.search(file_start_pattern, response_text)
|
108
|
-
if not first_match:
|
109
|
-
console.print("[red]FATAL ERROR: No file blocks found in response[/red]")
|
110
|
-
sys.exit(1)
|
111
|
-
fist_uuid = first_match.group(1)
|
112
|
-
|
113
|
-
# Find all file blocks
|
114
|
-
for match in re.finditer(file_start_pattern, response_text):
|
115
|
-
block_uuid, filepath, action, description = match.groups()
|
116
|
-
|
117
|
-
# Show debug info for create actions
|
118
|
-
if config.debug and action == 'create':
|
119
|
-
console.print(f"[green]Found new file block:[/green] {filepath}")
|
120
|
-
|
121
|
-
# Now find the complete block
|
122
|
-
full_block_pattern = (
|
123
|
-
f"## {block_uuid} file.*?##\n?"
|
124
|
-
f"(.*?)"
|
125
|
-
f"## {block_uuid} file end ##"
|
126
|
-
)
|
127
|
-
|
128
|
-
block_match = re.search(full_block_pattern, response_text[match.start():], re.DOTALL)
|
129
|
-
if not block_match:
|
130
|
-
# Show context around the incomplete block
|
131
|
-
context_start = max(0, match.start() - 100)
|
132
|
-
context_end = min(len(response_text), match.start() + 100)
|
133
|
-
context = response_text[context_start:context_end]
|
134
|
-
|
135
|
-
console.print(f"\n[red]FATAL ERROR: Found file start but no matching end for {filepath}[/red]")
|
136
|
-
console.print("[red]Context around incomplete block:[/red]")
|
137
|
-
console.print(Panel(context, title="Context", border_style="red"))
|
138
|
-
sys.exit(1)
|
139
|
-
|
140
|
-
content = block_match.group(1)
|
141
|
-
# For new files, preserve the first newline if it exists
|
142
|
-
if action == 'create' and content.startswith('\n'):
|
143
|
-
content = content[1:]
|
144
|
-
|
145
|
-
file_blocks.append((block_uuid, filepath.strip(), action, description or "", content))
|
146
|
-
|
147
|
-
if config.debug:
|
148
|
-
action_type = "Creating new file" if action == "create" else "Modifying file"
|
149
|
-
console.print(f"[green]Found valid block:[/green] {action_type} {filepath}")
|
150
|
-
console.print(f"[blue]Content length:[/blue] {len(content)} chars")
|
151
|
-
|
152
|
-
return file_blocks
|
153
|
-
|
154
|
-
def extract_modification_blocks(uuid: str, content: str) -> List[Tuple[str, str, Optional[str], str]]:
|
155
|
-
"""Extract all modification blocks from content, returns list of (type, description, replace, search)"""
|
156
|
-
blocks = []
|
157
|
-
console = Console()
|
158
|
-
|
159
|
-
# Find all modification blocks in sequence
|
160
|
-
block_start = r'## ' + re.escape(uuid) + r' (search/replace|search/delete) "(.*?)" ##\n'
|
161
|
-
current_pos = 0
|
162
|
-
|
163
|
-
while True:
|
164
|
-
# Find next block start
|
165
|
-
start_match = re.search(block_start, content[current_pos:], re.DOTALL)
|
166
|
-
if not start_match:
|
167
|
-
break
|
168
|
-
|
169
|
-
block_type, description = start_match.groups()
|
170
|
-
block_start_pos = current_pos + start_match.end()
|
171
|
-
|
172
|
-
# Find block end based on type
|
173
|
-
if block_type == 'search/replace':
|
174
|
-
replace_marker = f"## {uuid} replace with ##\n"
|
175
|
-
end_marker = f"## {uuid}"
|
176
|
-
|
177
|
-
# Find replace marker
|
178
|
-
replace_pos = content.find(replace_marker, block_start_pos)
|
179
|
-
if replace_pos == -1:
|
180
|
-
# Show context around the incomplete block
|
181
|
-
context_start = max(0, current_pos + start_match.start() - 100)
|
182
|
-
context_end = min(len(content), current_pos + start_match.end() + 100)
|
183
|
-
context = content[context_start:context_end]
|
184
|
-
|
185
|
-
console.print(f"\n[red]FATAL ERROR: Missing 'replace with' marker for block:[/red] {description}")
|
186
|
-
console.print("[red]Context around incomplete block:[/red]")
|
187
|
-
console.print(Panel(context, title="Context", border_style="red"))
|
188
|
-
sys.exit(1)
|
189
|
-
|
190
|
-
# Get search content
|
191
|
-
search = content[block_start_pos:replace_pos]
|
192
|
-
|
193
|
-
# Find end of replacement
|
194
|
-
replace_start = replace_pos + len(replace_marker)
|
195
|
-
next_block = content.find(end_marker, replace_start)
|
196
|
-
if next_block == -1:
|
197
|
-
# Use rest of content if no end marker found
|
198
|
-
replace = content[replace_start:]
|
199
|
-
else:
|
200
|
-
replace = content[replace_start:next_block]
|
201
|
-
|
202
|
-
blocks.append(('replace', description, replace, search))
|
203
|
-
current_pos = next_block if next_block != -1 else len(content)
|
204
|
-
|
205
|
-
else: # search/delete
|
206
|
-
# Find either next block start or end of content
|
207
|
-
next_block_marker = content.find(f"## {uuid}", block_start_pos)
|
208
|
-
if next_block_marker == -1:
|
209
|
-
# Use rest of content if no more blocks
|
210
|
-
search = content[block_start_pos:]
|
211
|
-
else:
|
212
|
-
search = content[block_start_pos:next_block_marker]
|
213
|
-
|
214
|
-
blocks.append(('delete', description, None, search))
|
215
|
-
current_pos = next_block_marker if next_block_marker != -1 else len(content)
|
216
|
-
|
217
|
-
if config.debug:
|
218
|
-
console.print(f"[green]Found {block_type} block:[/green] {description}")
|
219
|
-
|
220
|
-
return blocks
|
221
|
-
|
222
|
-
def handle_file_block(block: FileBlock) -> FileChange:
|
223
|
-
"""Process a single file block and return a FileChange object"""
|
224
|
-
console = Console()
|
225
|
-
|
226
|
-
# Handle file removal action
|
227
|
-
if block.action == 'remove':
|
228
|
-
return FileChange(
|
229
|
-
path=Path(block.filepath),
|
230
|
-
description=block.description,
|
231
|
-
is_new_file=False,
|
232
|
-
content="",
|
233
|
-
search_blocks=[],
|
234
|
-
remove_file=True
|
235
|
-
)
|
236
|
-
|
237
|
-
# Validate file path
|
238
|
-
path = Path(block.filepath)
|
239
|
-
is_valid, error = validate_file_path(path)
|
240
|
-
if block.action == 'replace' and not is_valid:
|
241
|
-
console.print(f"[red]Invalid file path for replacement:[/red] {error}")
|
242
|
-
sys.exit(1)
|
243
|
-
|
244
|
-
# Handle file replacement action
|
245
|
-
if block.action == 'replace':
|
246
|
-
# Read original content if file exists
|
247
|
-
original_content = ""
|
248
|
-
if path.exists():
|
249
|
-
try:
|
250
|
-
original_content = path.read_text()
|
251
|
-
except Exception as e:
|
252
|
-
console.print(f"[red]Error reading original file for replacement:[/red] {e}")
|
253
|
-
sys.exit(1)
|
254
|
-
|
255
|
-
return FileChange(
|
256
|
-
path=Path(block.filepath),
|
257
|
-
description=block.description,
|
258
|
-
is_new_file=False,
|
259
|
-
content=block.content.lstrip('\n'),
|
260
|
-
original_content=original_content,
|
261
|
-
search_blocks=[],
|
262
|
-
replace_file=True
|
263
|
-
)
|
264
|
-
|
265
|
-
# Validate content for new files
|
266
|
-
if block.action == 'create':
|
267
|
-
is_valid, error = validate_file_content(block.content)
|
268
|
-
if not is_valid:
|
269
|
-
console.print(f"[red]Invalid file content for {block.filepath}:[/red] {error}")
|
270
|
-
sys.exit(1)
|
271
|
-
|
272
|
-
if block.action == 'create':
|
273
|
-
return FileChange(
|
274
|
-
path=Path(block.filepath),
|
275
|
-
description=block.description,
|
276
|
-
is_new_file=True,
|
277
|
-
content=block.content[1:] if block.content.startswith('\n') else block.content,
|
278
|
-
search_blocks=[]
|
279
|
-
)
|
280
|
-
|
281
|
-
# Extract and process modification blocks
|
282
|
-
search_blocks = []
|
283
|
-
for block_type, description, replace, search in extract_modification_blocks(block.uuid, block.content):
|
284
|
-
# Ensure consistent line endings
|
285
|
-
search = search.rstrip('\n') + '\n'
|
286
|
-
if replace is not None:
|
287
|
-
replace = replace.rstrip('\n') + '\n'
|
288
|
-
|
289
|
-
if config.debug:
|
290
|
-
console.print(f"\n[cyan]Processing {block_type} block:[/cyan] {description}")
|
291
|
-
console.print(Panel(search, title="Search Content"))
|
292
|
-
if replace:
|
293
|
-
console.print(Panel(replace, title="Replace Content"))
|
294
|
-
|
295
|
-
search_blocks.append((search, replace, description))
|
296
|
-
|
297
|
-
return FileChange(
|
298
|
-
path=Path(block.filepath),
|
299
|
-
description=block.description,
|
300
|
-
is_new_file=False,
|
301
|
-
search_blocks=search_blocks
|
302
|
-
)
|
303
|
-
|
304
|
-
def parse_block_changes(response_text: str) -> List[FileChange]:
|
305
|
-
"""Parse file changes from response blocks and return list of FileChange"""
|
306
|
-
changes = []
|
307
|
-
console = Console()
|
308
|
-
|
309
|
-
# First extract all file blocks
|
310
|
-
file_blocks = extract_file_blocks(response_text)
|
311
|
-
|
312
|
-
# Process each file block independently
|
313
|
-
for block_uuid, filepath, action, description, content in file_blocks:
|
314
|
-
path = Path(filepath)
|
315
|
-
|
316
|
-
file_block = FileBlock(
|
317
|
-
uuid=block_uuid,
|
318
|
-
filepath=filepath,
|
319
|
-
action=action,
|
320
|
-
description=description,
|
321
|
-
content=content
|
322
|
-
)
|
323
|
-
|
324
|
-
file_change = handle_file_block(file_block)
|
325
|
-
# For remove action, ensure remove_file flag is set
|
326
|
-
if action == 'remove':
|
327
|
-
file_change.remove_file = True
|
328
|
-
file_change.path = path
|
329
|
-
changes.append(file_change)
|
330
|
-
|
331
|
-
if config.debug:
|
332
|
-
console.print(f"\n[cyan]Processed {len(file_blocks)} file blocks[/cyan]")
|
333
|
-
|
334
|
-
return changes
|