janito 0.6.0__py3-none-any.whl → 0.7.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/__main__.py +37 -30
- janito/agents/__init__.py +8 -2
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +13 -23
- janito/agents/openai.py +5 -1
- janito/change/analysis/analyze.py +8 -7
- janito/change/analysis/prompts.py +4 -12
- janito/change/analysis/view/terminal.py +21 -11
- janito/change/applier/text.py +7 -5
- janito/change/core.py +22 -29
- janito/change/parser.py +0 -2
- janito/change/prompts.py +16 -21
- janito/change/validator.py +27 -9
- janito/change/viewer/content.py +1 -1
- janito/change/viewer/panels.py +93 -115
- janito/change/viewer/styling.py +15 -4
- janito/cli/commands.py +63 -20
- janito/common.py +44 -18
- janito/config.py +44 -44
- janito/prompt.py +36 -0
- janito/qa.py +5 -14
- janito/search_replace/README.md +63 -17
- janito/search_replace/__init__.py +2 -1
- janito/search_replace/core.py +15 -14
- janito/search_replace/logger.py +35 -0
- janito/search_replace/searcher.py +160 -48
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +15 -16
- janito/shell/commands.py +38 -97
- janito/shell/processor.py +7 -27
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/workspace/__init__.py +4 -5
- janito/workspace/analysis.py +2 -2
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/RECORD +44 -43
- janito/change/viewer/pager.py +0 -56
- janito/cli/handlers/ask.py +0 -22
- janito/cli/handlers/demo.py +0 -22
- janito/cli/handlers/request.py +0 -24
- janito/cli/handlers/scan.py +0 -9
- janito/prompts.py +0 -2
- janito/shell/handlers.py +0 -122
- janito/workspace/manager.py +0 -48
- janito/workspace/scan.py +0 -232
- janito-0.6.0.dist-info/METADATA +0 -185
- {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/WHEEL +0 -0
- {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
janito/config.py
CHANGED
@@ -1,36 +1,57 @@
|
|
1
|
-
from typing import Optional
|
1
|
+
from typing import Optional
|
2
2
|
import os
|
3
3
|
from pathlib import Path
|
4
4
|
|
5
5
|
class ConfigManager:
|
6
|
+
"""Singleton configuration manager for the application."""
|
7
|
+
|
6
8
|
_instance = None
|
7
9
|
|
8
10
|
def __init__(self):
|
11
|
+
"""Initialize configuration with default values."""
|
9
12
|
self.debug = False
|
10
13
|
self.verbose = False
|
11
14
|
self.debug_line = None
|
12
15
|
self.test_cmd = os.getenv('JANITO_TEST_CMD')
|
13
16
|
self.workspace_dir = Path.cwd()
|
14
17
|
self.raw = False
|
15
|
-
self.include: List[Path] = []
|
16
|
-
self.recursive: List[Path] = []
|
17
18
|
self.auto_apply: bool = False
|
18
19
|
self.tui: bool = False
|
19
|
-
self.
|
20
|
+
self.skip_work: bool = False
|
20
21
|
|
21
22
|
@classmethod
|
22
23
|
def get_instance(cls) -> "ConfigManager":
|
24
|
+
"""Return the singleton instance of ConfigManager.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
ConfigManager: The singleton instance
|
28
|
+
"""
|
23
29
|
if cls._instance is None:
|
24
30
|
cls._instance = cls()
|
25
31
|
return cls._instance
|
26
32
|
|
27
33
|
def set_debug(self, enabled: bool) -> None:
|
34
|
+
"""Set debug mode.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
enabled: True to enable debug mode, False to disable
|
38
|
+
"""
|
28
39
|
self.debug = enabled
|
29
40
|
|
30
41
|
def set_verbose(self, enabled: bool) -> None:
|
42
|
+
"""Set verbose output mode.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
enabled: True to enable verbose output, False to disable
|
46
|
+
"""
|
31
47
|
self.verbose = enabled
|
32
48
|
|
33
49
|
def set_debug_line(self, line: Optional[int]) -> None:
|
50
|
+
"""Set specific line number for debug output.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
line: Line number to debug, or None for all lines
|
54
|
+
"""
|
34
55
|
self.debug_line = line
|
35
56
|
|
36
57
|
def should_debug_line(self, line: int) -> bool:
|
@@ -46,57 +67,36 @@ class ConfigManager:
|
|
46
67
|
self.workspace_dir = path if path is not None else Path.cwd()
|
47
68
|
|
48
69
|
def set_raw(self, enabled: bool) -> None:
|
49
|
-
"""Set raw output mode
|
50
|
-
|
51
|
-
|
52
|
-
def set_include(self, paths: Optional[List[Path]]) -> None:
|
53
|
-
"""
|
54
|
-
Set additional paths to include.
|
55
|
-
|
70
|
+
"""Set raw output mode.
|
71
|
+
|
56
72
|
Args:
|
57
|
-
|
58
|
-
|
59
|
-
Raises:
|
60
|
-
ValueError: If duplicate paths are provided
|
73
|
+
enabled: True to enable raw output mode, False to disable
|
61
74
|
"""
|
62
|
-
|
63
|
-
self.include = []
|
64
|
-
return
|
65
|
-
|
66
|
-
# Convert paths to absolute and resolve symlinks
|
67
|
-
resolved_paths = [p.absolute().resolve() for p in paths]
|
68
|
-
|
69
|
-
# Check for duplicates
|
70
|
-
seen_paths = set()
|
71
|
-
unique_paths = []
|
72
|
-
|
73
|
-
for path in resolved_paths:
|
74
|
-
if path in seen_paths:
|
75
|
-
raise ValueError(f"Duplicate path provided: {path}")
|
76
|
-
seen_paths.add(path)
|
77
|
-
unique_paths.append(path)
|
78
|
-
|
79
|
-
self.include = unique_paths
|
75
|
+
self.raw = enabled
|
80
76
|
|
81
77
|
def set_auto_apply(self, enabled: bool) -> None:
|
82
|
-
"""Set auto apply mode
|
78
|
+
"""Set auto apply mode for changes.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
enabled: True to enable auto apply mode, False to disable
|
82
|
+
"""
|
83
83
|
self.auto_apply = enabled
|
84
84
|
|
85
85
|
def set_tui(self, enabled: bool) -> None:
|
86
|
-
"""Set
|
86
|
+
"""Set Text User Interface mode.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
enabled: True to enable TUI mode, False to disable
|
90
|
+
"""
|
87
91
|
self.tui = enabled
|
88
92
|
|
89
|
-
def
|
90
|
-
"""Set
|
91
|
-
|
93
|
+
def set_skip_work(self, enabled: bool) -> None:
|
94
|
+
"""Set whether to skip scanning the workspace directory.
|
95
|
+
|
92
96
|
Args:
|
93
|
-
|
97
|
+
enabled: True to skip workspace directory, False to include it
|
94
98
|
"""
|
95
|
-
self.
|
96
|
-
|
97
|
-
def set_skipwork(self, enabled: bool) -> None:
|
98
|
-
"""Set skipwork flag to skip scanning workspace_dir"""
|
99
|
-
self.skipwork = enabled
|
99
|
+
self.skip_work = enabled
|
100
100
|
|
101
101
|
# Create a singleton instance
|
102
102
|
config = ConfigManager.get_instance()
|
janito/prompt.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
from .workspace import workset
|
2
|
+
|
3
|
+
SYSTEM_PROMPT = """I am Janito, your friendly software development buddy.
|
4
|
+
|
5
|
+
I help you with coding tasks while being clear and concise in my responses.
|
6
|
+
|
7
|
+
I have received the following workset for analysis:
|
8
|
+
|
9
|
+
{workset}
|
10
|
+
|
11
|
+
"""
|
12
|
+
|
13
|
+
def build_system_prompt() -> dict:
|
14
|
+
|
15
|
+
system_prompt = [
|
16
|
+
{
|
17
|
+
"type": "text",
|
18
|
+
"text": "You Janito, an AI assistant tasked with analyzing worksets of code. You have received the following workset for analysis:",
|
19
|
+
}
|
20
|
+
]
|
21
|
+
|
22
|
+
blocks = workset.get_cache_blocks()
|
23
|
+
for block in blocks:
|
24
|
+
if not block:
|
25
|
+
continue
|
26
|
+
block_content = ""
|
27
|
+
for file in block:
|
28
|
+
block_content += f'<file name="{file.name}"\n"'
|
29
|
+
block_content += f'<content>\n"{file.content}"\n</content>\n</file>\n'
|
30
|
+
system_prompt.append( {
|
31
|
+
"type": "text",
|
32
|
+
"text": block_content,
|
33
|
+
"cache_control": {"type": "ephemeral"}
|
34
|
+
}
|
35
|
+
)
|
36
|
+
return system_prompt
|
janito/qa.py
CHANGED
@@ -1,35 +1,26 @@
|
|
1
1
|
from rich.console import Console
|
2
2
|
from rich.markdown import Markdown
|
3
3
|
from rich.panel import Panel
|
4
|
-
from rich.syntax import Syntax
|
5
|
-
from rich.table import Table
|
6
4
|
from rich.rule import Rule
|
7
|
-
from janito.agents import AIAgent
|
8
5
|
from janito.common import progress_send_message
|
9
|
-
from janito.workspace import
|
6
|
+
from janito.workspace import workset # Updated import
|
10
7
|
|
11
8
|
|
12
|
-
QA_PROMPT = """Please provide a clear and concise answer to the following question about the
|
9
|
+
QA_PROMPT = """Please provide a clear and concise answer to the following question about the workset you received above.
|
13
10
|
|
14
11
|
Question: {question}
|
15
12
|
|
16
|
-
Current files:
|
17
|
-
<files>
|
18
|
-
{files_content}
|
19
|
-
</files>
|
20
|
-
|
21
13
|
Focus on providing factual information and explanations. Do not suggest code changes.
|
22
14
|
Format your response using markdown with appropriate headers and code blocks.
|
23
15
|
"""
|
24
16
|
|
25
|
-
def ask_question(question: str
|
17
|
+
def ask_question(question: str) -> str:
|
26
18
|
"""Process a question about the codebase and return the answer"""
|
27
|
-
#
|
28
|
-
|
19
|
+
# Ensure content is refreshed and analyzed
|
20
|
+
workset.show()
|
29
21
|
|
30
22
|
prompt = QA_PROMPT.format(
|
31
23
|
question=question,
|
32
|
-
files_content=files_content
|
33
24
|
)
|
34
25
|
return progress_send_message(prompt)
|
35
26
|
|
janito/search_replace/README.md
CHANGED
@@ -62,80 +62,126 @@ Search pattern:
|
|
62
62
|
|
63
63
|
## Search Strategies
|
64
64
|
|
65
|
-
The module
|
65
|
+
The module employs multiple search strategies in a fallback chain to find the best match. Each strategy has specific behaviors and use cases:
|
66
66
|
|
67
67
|
### ExactMatch Strategy
|
68
68
|
- Matches content exactly, including all whitespace and indentation
|
69
69
|
- Strictest matching strategy
|
70
|
+
- Best for precise replacements where indentation matters
|
70
71
|
- Example:
|
71
72
|
```python
|
72
73
|
# Pattern:
|
73
74
|
def hello():
|
74
75
|
print("Hi")
|
75
|
-
|
76
|
+
|
76
77
|
# Will only match exact indentation:
|
77
78
|
def hello():
|
78
79
|
print("Hi")
|
80
|
+
|
81
|
+
# Won't match different indentation:
|
82
|
+
def hello():
|
83
|
+
print("Hi")
|
79
84
|
```
|
80
85
|
|
81
86
|
### IndentAware Strategy
|
82
87
|
- Preserves relative indentation between lines
|
83
88
|
- Allows different base indentation levels
|
89
|
+
- Ideal for matching code blocks inside functions/classes
|
84
90
|
- Example:
|
85
91
|
```python
|
86
92
|
# Pattern:
|
87
93
|
print("Hello")
|
88
94
|
print("World")
|
89
|
-
|
95
|
+
|
90
96
|
# Matches with different base indentation:
|
91
97
|
def test():
|
92
98
|
print("Hello")
|
93
99
|
print("World")
|
94
|
-
|
100
|
+
|
95
101
|
def other():
|
96
102
|
print("Hello")
|
97
103
|
print("World")
|
104
|
+
|
105
|
+
# Won't match if relative indentation differs:
|
106
|
+
def wrong():
|
107
|
+
print("Hello")
|
108
|
+
print("World")
|
98
109
|
```
|
99
110
|
|
100
111
|
### ExactContent Strategy
|
101
112
|
- Ignores all indentation
|
102
113
|
- Matches content after stripping whitespace
|
103
|
-
-
|
114
|
+
- Useful for matching code regardless of formatting
|
104
115
|
- Example:
|
105
116
|
```python
|
106
117
|
# Pattern:
|
107
118
|
print("Hello")
|
108
119
|
print("World")
|
109
|
-
|
110
|
-
# Matches
|
120
|
+
|
121
|
+
# Matches any indentation:
|
111
122
|
print("Hello")
|
112
123
|
print("World")
|
124
|
+
|
125
|
+
# Also matches:
|
126
|
+
print("Hello")
|
127
|
+
print("World")
|
113
128
|
```
|
114
129
|
|
115
130
|
### ExactContentNoComments Strategy
|
116
131
|
- Ignores indentation, comments, and empty lines
|
117
132
|
- Most flexible strategy
|
133
|
+
- Perfect for matching code with varying comments/formatting
|
118
134
|
- Example:
|
119
135
|
```python
|
120
136
|
# Pattern:
|
121
137
|
print("Hello") # greeting
|
122
|
-
|
138
|
+
|
123
139
|
print("World") # message
|
124
140
|
|
125
|
-
# Matches:
|
141
|
+
# Matches all these variations:
|
126
142
|
def test():
|
127
143
|
print("Hello") # different comment
|
128
144
|
# some comment
|
129
145
|
print("World")
|
146
|
+
|
147
|
+
# Or:
|
148
|
+
print("Hello") # no comment
|
149
|
+
print("World") # different note
|
150
|
+
```
|
151
|
+
|
152
|
+
### ExactContentNoCommentsFirstLinePartial Strategy
|
153
|
+
- Matches first line partially, ignoring comments
|
154
|
+
- Useful for finding code fragments or partial matches
|
155
|
+
- Example:
|
156
|
+
```python
|
157
|
+
# Pattern:
|
158
|
+
print("Hello")
|
159
|
+
|
160
|
+
# Matches partial content:
|
161
|
+
message = print("Hello") + "extra"
|
162
|
+
result = print("Hello, World")
|
130
163
|
```
|
131
164
|
|
132
|
-
### Strategy Selection
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
165
|
+
### Strategy Selection and File Types
|
166
|
+
|
167
|
+
Strategies are tried in the following order:
|
168
|
+
1. ExactMatch
|
169
|
+
2. IndentAware
|
170
|
+
3. ExactContent
|
171
|
+
4. ExactContentNoComments
|
172
|
+
5. ExactContentNoCommentsFirstLinePartial
|
173
|
+
|
174
|
+
File extension specific behavior:
|
175
|
+
|
176
|
+
| File Type | Available Strategies |
|
177
|
+
|-----------|---------------------|
|
178
|
+
| Python (.py) | All strategies |
|
179
|
+
| Java (.java) | All strategies |
|
180
|
+
| JavaScript (.js) | All strategies |
|
181
|
+
| TypeScript (.ts) | All strategies |
|
182
|
+
| Other files | ExactMatch, ExactContent, ExactContentNoComments, ExactContentNoCommentsFirstLinePartial |
|
183
|
+
|
184
|
+
The module automatically selects the appropriate strategies based on the file type and tries them in order until a match is found.
|
139
185
|
|
140
186
|
## Debug Output
|
141
187
|
|
@@ -143,4 +189,4 @@ When debugging failed searches, the module provides:
|
|
143
189
|
- Visual whitespace markers (· for spaces, → for tabs)
|
144
190
|
- Indentation analysis
|
145
191
|
- Line-by-line matching attempts
|
146
|
-
- Strategy selection information
|
192
|
+
- Strategy selection information
|
@@ -2,5 +2,6 @@ from .core import SearchReplacer, PatternNotFoundException
|
|
2
2
|
from .searcher import Searcher
|
3
3
|
from .replacer import Replacer
|
4
4
|
from .parser import parse_test_file
|
5
|
+
from .strategy_result import StrategyResult
|
5
6
|
|
6
|
-
__all__ = ['SearchReplacer', 'PatternNotFoundException', 'Searcher', 'Replacer', 'parse_test_file']
|
7
|
+
__all__ = ['SearchReplacer', 'PatternNotFoundException', 'Searcher', 'Replacer', 'parse_test_file', 'StrategyResult']
|
janito/search_replace/core.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
from typing import Optional, List
|
2
2
|
from pathlib import Path
|
3
|
+
import os
|
4
|
+
from datetime import datetime
|
5
|
+
from .logger import log_match, log_failure
|
3
6
|
from .searcher import Searcher
|
4
7
|
from .replacer import Replacer
|
5
8
|
|
@@ -28,23 +31,11 @@ class SearchReplacer:
|
|
28
31
|
def find_pattern(self) -> bool:
|
29
32
|
"""Search for pattern with indentation awareness."""
|
30
33
|
try:
|
31
|
-
|
32
|
-
exact_matches = self.searcher.exact_match(self.source_code, self.search_pattern)
|
33
|
-
if exact_matches:
|
34
|
-
if self.searcher.debug_mode:
|
35
|
-
print("[DEBUG] Found pattern using exact match")
|
36
|
-
return True
|
37
|
-
|
38
|
-
# Fall back to flexible matching
|
39
|
-
if self.searcher.debug_mode:
|
40
|
-
print("[DEBUG] No exact match found, trying flexible matching")
|
34
|
+
source_lines = self.source_code.splitlines()
|
41
35
|
search_first, _ = self.searcher.get_first_non_empty_line(self.search_pattern)
|
42
36
|
search_indent = self.searcher.get_indentation(search_first)
|
43
37
|
normalized_pattern = self.searcher.normalize_pattern(self.search_pattern, search_indent)
|
44
|
-
|
45
|
-
source_lines = self.source_code.splitlines()
|
46
38
|
matches = self._find_matches(source_lines, normalized_pattern)
|
47
|
-
|
48
39
|
return bool(self.searcher._find_best_match_position(matches, source_lines, self.pattern_base_indent))
|
49
40
|
except Exception:
|
50
41
|
return False
|
@@ -65,6 +56,9 @@ class SearchReplacer:
|
|
65
56
|
best_pos = self.searcher._find_best_match_position(matches, source_lines, self.pattern_base_indent)
|
66
57
|
|
67
58
|
if best_pos is None:
|
59
|
+
# Log failed match if not in debug mode
|
60
|
+
if not self.searcher.debug_mode:
|
61
|
+
log_failure(self.file_ext)
|
68
62
|
raise PatternNotFoundException("Pattern not found")
|
69
63
|
|
70
64
|
if self.searcher.debug_mode:
|
@@ -101,6 +95,10 @@ class SearchReplacer:
|
|
101
95
|
while i < len(source_lines):
|
102
96
|
if i == match_pos:
|
103
97
|
self.pattern_found = True
|
98
|
+
# Log successful match if not in debug mode
|
99
|
+
# get the
|
100
|
+
if not self.searcher.debug_mode:
|
101
|
+
log_match("Strategy", self.file_ext)
|
104
102
|
match_indent = self.searcher.get_indentation(source_lines[i])
|
105
103
|
replacement_lines = self.replacer.create_indented_replacement(
|
106
104
|
match_indent, self.search_pattern, self.replacement
|
@@ -116,4 +114,7 @@ class SearchReplacer:
|
|
116
114
|
"""Check if pattern matches at given position."""
|
117
115
|
pattern_lines = normalized_pattern.splitlines()
|
118
116
|
strategies = self.searcher.get_strategies(self.file_ext)
|
119
|
-
|
117
|
+
result = self.searcher.try_match_with_strategies(source_lines, pattern_lines, pos, strategies)
|
118
|
+
if result.success and not self.searcher.debug_mode:
|
119
|
+
log_match(result.strategy_name, self.file_ext)
|
120
|
+
return result.success
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import logging
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
# Configure logging
|
7
|
+
logger = logging.getLogger("janito.search_replace")
|
8
|
+
logger.setLevel(logging.INFO)
|
9
|
+
|
10
|
+
# Create formatter
|
11
|
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
12
|
+
|
13
|
+
# Create file handler
|
14
|
+
def setup_file_handler():
|
15
|
+
"""Setup file handler for logging if .janito directory exists"""
|
16
|
+
if Path(".janito").exists():
|
17
|
+
fh = logging.FileHandler(".janito/search_logs.txt")
|
18
|
+
fh.setFormatter(formatter)
|
19
|
+
logger.addHandler(fh)
|
20
|
+
|
21
|
+
setup_file_handler()
|
22
|
+
|
23
|
+
def log_match(strategy_name: str, file_type: Optional[str] = None):
|
24
|
+
"""Log successful match with strategy info"""
|
25
|
+
msg = f"Match found using {strategy_name}"
|
26
|
+
if file_type:
|
27
|
+
msg += f" for file type {file_type}"
|
28
|
+
logger.info(msg)
|
29
|
+
|
30
|
+
def log_failure(file_type: Optional[str] = None):
|
31
|
+
"""Log failed match attempt"""
|
32
|
+
msg = "Failed to match pattern"
|
33
|
+
if file_type:
|
34
|
+
msg += f" for file type {file_type}"
|
35
|
+
logger.warning(msg)
|