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.
Files changed (54) hide show
  1. janito/__main__.py +37 -30
  2. janito/agents/__init__.py +8 -2
  3. janito/agents/agent.py +10 -3
  4. janito/agents/claudeai.py +13 -23
  5. janito/agents/openai.py +5 -1
  6. janito/change/analysis/analyze.py +8 -7
  7. janito/change/analysis/prompts.py +4 -12
  8. janito/change/analysis/view/terminal.py +21 -11
  9. janito/change/applier/text.py +7 -5
  10. janito/change/core.py +22 -29
  11. janito/change/parser.py +0 -2
  12. janito/change/prompts.py +16 -21
  13. janito/change/validator.py +27 -9
  14. janito/change/viewer/content.py +1 -1
  15. janito/change/viewer/panels.py +93 -115
  16. janito/change/viewer/styling.py +15 -4
  17. janito/cli/commands.py +63 -20
  18. janito/common.py +44 -18
  19. janito/config.py +44 -44
  20. janito/prompt.py +36 -0
  21. janito/qa.py +5 -14
  22. janito/search_replace/README.md +63 -17
  23. janito/search_replace/__init__.py +2 -1
  24. janito/search_replace/core.py +15 -14
  25. janito/search_replace/logger.py +35 -0
  26. janito/search_replace/searcher.py +160 -48
  27. janito/search_replace/strategy_result.py +10 -0
  28. janito/shell/__init__.py +15 -16
  29. janito/shell/commands.py +38 -97
  30. janito/shell/processor.py +7 -27
  31. janito/shell/prompt.py +48 -0
  32. janito/shell/registry.py +60 -0
  33. janito/workspace/__init__.py +4 -5
  34. janito/workspace/analysis.py +2 -2
  35. janito/workspace/show.py +141 -0
  36. janito/workspace/stats.py +43 -0
  37. janito/workspace/types.py +98 -0
  38. janito/workspace/workset.py +108 -0
  39. janito/workspace/workspace.py +114 -0
  40. janito-0.7.0.dist-info/METADATA +167 -0
  41. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/RECORD +44 -43
  42. janito/change/viewer/pager.py +0 -56
  43. janito/cli/handlers/ask.py +0 -22
  44. janito/cli/handlers/demo.py +0 -22
  45. janito/cli/handlers/request.py +0 -24
  46. janito/cli/handlers/scan.py +0 -9
  47. janito/prompts.py +0 -2
  48. janito/shell/handlers.py +0 -122
  49. janito/workspace/manager.py +0 -48
  50. janito/workspace/scan.py +0 -232
  51. janito-0.6.0.dist-info/METADATA +0 -185
  52. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/WHEEL +0 -0
  53. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  54. {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, List
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.skipwork: bool = False
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
- self.raw = enabled
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
- paths: List of paths to include
58
-
59
- Raises:
60
- ValueError: If duplicate paths are provided
73
+ enabled: True to enable raw output mode, False to disable
61
74
  """
62
- if paths is None:
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 TUI mode"""
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 set_recursive(self, paths: Optional[List[Path]]) -> None:
90
- """Set paths to scan recursively
91
-
93
+ def set_skip_work(self, enabled: bool) -> None:
94
+ """Set whether to skip scanning the workspace directory.
95
+
92
96
  Args:
93
- paths: List of directory paths to scan recursively, or None to disable recursive scanning
97
+ enabled: True to skip workspace directory, False to include it
94
98
  """
95
- self.recursive = paths
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 workspace
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 codebase:
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, files_content: str) -> str:
17
+ def ask_question(question: str) -> str:
26
18
  """Process a question about the codebase and return the answer"""
27
- # Analyze workspace content if needed
28
- workspace.analyze()
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
 
@@ -62,80 +62,126 @@ Search pattern:
62
62
 
63
63
  ## Search Strategies
64
64
 
65
- The module uses multiple search strategies in a fallback chain to find the best match:
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
- - Most flexible strategy
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 regardless of indentation:
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
- - Strategies are tried in order: ExactMatch → IndentAware → ExactContent → ExactContentNoComments
134
- - File extension specific behavior:
135
- - Python files (.py): All strategies
136
- - Java files (.java): All strategies
137
- - JavaScript/TypeScript (.js/.ts): All strategies
138
- - Other files: ExactMatch, ExactContent, and ExactContentNoComments
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']
@@ -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
- # Try exact matching first
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
- return self.searcher.try_match_with_strategies(source_lines, pattern_lines, pos, strategies)
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)