janito 3.1.0__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. janito/cli/chat_mode/bindings.py +0 -26
  2. janito/cli/chat_mode/session.py +1 -7
  3. janito/cli/cli_commands/list_plugins.py +8 -13
  4. janito/cli/prompt_core.py +9 -19
  5. janito/llm/agent.py +17 -30
  6. janito/llm/driver.py +0 -7
  7. janito/plugins/__init__.py +21 -29
  8. janito/plugins/__main__.py +85 -0
  9. janito/plugins/base.py +57 -0
  10. janito/plugins/builtin.py +1 -1
  11. janito/plugins/core/filemanager/tools/copy_file.py +25 -1
  12. janito/plugins/core/filemanager/tools/create_directory.py +28 -1
  13. janito/plugins/core/filemanager/tools/create_file.py +25 -1
  14. janito/plugins/core/filemanager/tools/delete_text_in_file.py +1 -0
  15. janito/plugins/core/imagedisplay/plugin.py +1 -1
  16. janito/plugins/core_loader.py +144 -0
  17. janito/plugins/discovery.py +3 -3
  18. janito/plugins/example_plugin.py +1 -1
  19. janito/plugins/manager.py +1 -1
  20. janito/plugins_backup_20250825_070018/__init__.py +36 -0
  21. janito/plugins_backup_20250825_070018/builtin.py +102 -0
  22. janito/plugins_backup_20250825_070018/config.py +84 -0
  23. janito/plugins_backup_20250825_070018/core/__init__.py +7 -0
  24. janito/plugins_backup_20250825_070018/core/codeanalyzer/__init__.py +43 -0
  25. janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/get_file_outline/__init__.py +1 -0
  26. janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/get_file_outline/core.py +122 -0
  27. janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/search_text/__init__.py +1 -0
  28. janito/plugins_backup_20250825_070018/core/codeanalyzer/tools/search_text/core.py +205 -0
  29. janito/plugins_backup_20250825_070018/core/filemanager/__init__.py +124 -0
  30. janito/plugins_backup_20250825_070018/core/filemanager/tools/copy_file.py +87 -0
  31. janito/plugins_backup_20250825_070018/core/filemanager/tools/create_directory.py +70 -0
  32. janito/plugins_backup_20250825_070018/core/filemanager/tools/create_file.py +87 -0
  33. janito/plugins_backup_20250825_070018/core/filemanager/tools/delete_text_in_file.py +135 -0
  34. janito/plugins_backup_20250825_070018/core/filemanager/tools/find_files.py +143 -0
  35. janito/plugins_backup_20250825_070018/core/filemanager/tools/move_file.py +131 -0
  36. janito/plugins_backup_20250825_070018/core/filemanager/tools/read_files.py +58 -0
  37. janito/plugins_backup_20250825_070018/core/filemanager/tools/remove_directory.py +55 -0
  38. janito/plugins_backup_20250825_070018/core/filemanager/tools/remove_file.py +58 -0
  39. janito/plugins_backup_20250825_070018/core/filemanager/tools/replace_text_in_file.py +270 -0
  40. janito/plugins_backup_20250825_070018/core/filemanager/tools/validate_file_syntax/__init__.py +1 -0
  41. janito/plugins_backup_20250825_070018/core/filemanager/tools/validate_file_syntax/core.py +114 -0
  42. janito/plugins_backup_20250825_070018/core/filemanager/tools/view_file.py +172 -0
  43. janito/plugins_backup_20250825_070018/core/imagedisplay/__init__.py +14 -0
  44. janito/plugins_backup_20250825_070018/core/imagedisplay/plugin.py +51 -0
  45. janito/plugins_backup_20250825_070018/core/imagedisplay/tools/__init__.py +1 -0
  46. janito/plugins_backup_20250825_070018/core/imagedisplay/tools/show_image.py +83 -0
  47. janito/plugins_backup_20250825_070018/core/imagedisplay/tools/show_image_grid.py +84 -0
  48. janito/plugins_backup_20250825_070018/core/system/__init__.py +23 -0
  49. janito/plugins_backup_20250825_070018/core/system/tools/run_bash_command.py +183 -0
  50. janito/plugins_backup_20250825_070018/core/system/tools/run_powershell_command.py +218 -0
  51. janito/plugins_backup_20250825_070018/dev/__init__.py +7 -0
  52. janito/plugins_backup_20250825_070018/dev/pythondev/__init__.py +37 -0
  53. janito/plugins_backup_20250825_070018/dev/pythondev/tools/python_code_run.py +172 -0
  54. janito/plugins_backup_20250825_070018/dev/pythondev/tools/python_command_run.py +171 -0
  55. janito/plugins_backup_20250825_070018/dev/pythondev/tools/python_file_run.py +172 -0
  56. janito/plugins_backup_20250825_070018/dev/visualization/__init__.py +23 -0
  57. janito/plugins_backup_20250825_070018/dev/visualization/tools/read_chart.py +259 -0
  58. janito/plugins_backup_20250825_070018/discovery.py +289 -0
  59. janito/plugins_backup_20250825_070018/example_plugin.py +108 -0
  60. janito/plugins_backup_20250825_070018/manager.py +243 -0
  61. janito/{plugins → plugins_backup_20250825_070018}/tools/delete_text_in_file.py +1 -0
  62. janito/plugins_backup_20250825_070018/tools/get_file_outline/java_outline.py +47 -0
  63. janito/plugins_backup_20250825_070018/tools/get_file_outline/markdown_outline.py +14 -0
  64. janito/plugins_backup_20250825_070018/tools/get_file_outline/python_outline.py +303 -0
  65. janito/plugins_backup_20250825_070018/tools/get_file_outline/search_outline.py +36 -0
  66. janito/plugins_backup_20250825_070018/tools/search_text/match_lines.py +67 -0
  67. janito/plugins_backup_20250825_070018/tools/search_text/pattern_utils.py +73 -0
  68. janito/plugins_backup_20250825_070018/tools/search_text/traverse_directory.py +145 -0
  69. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/css_validator.py +35 -0
  70. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/html_validator.py +100 -0
  71. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/jinja2_validator.py +50 -0
  72. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/js_validator.py +27 -0
  73. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/json_validator.py +6 -0
  74. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/markdown_validator.py +109 -0
  75. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/ps1_validator.py +32 -0
  76. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/python_validator.py +5 -0
  77. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/xml_validator.py +11 -0
  78. janito/plugins_backup_20250825_070018/tools/validate_file_syntax/yaml_validator.py +6 -0
  79. janito/plugins_backup_20250825_070018/ui/__init__.py +7 -0
  80. janito/plugins_backup_20250825_070018/ui/userinterface/__init__.py +16 -0
  81. janito/plugins_backup_20250825_070018/ui/userinterface/tools/ask_user.py +110 -0
  82. janito/plugins_backup_20250825_070018/web/__init__.py +7 -0
  83. janito/plugins_backup_20250825_070018/web/webtools/__init__.py +33 -0
  84. janito/plugins_backup_20250825_070018/web/webtools/tools/fetch_url.py +458 -0
  85. janito/plugins_backup_20250825_070018/web/webtools/tools/open_html_in_browser.py +51 -0
  86. janito/plugins_backup_20250825_070018/web/webtools/tools/open_url.py +37 -0
  87. janito/tools/base.py +31 -1
  88. janito/tools/cli_initializer.py +1 -1
  89. janito/tools/function_adapter.py +35 -1
  90. janito/tools/initialize.py +1 -1
  91. janito/tools/tool_base.py +142 -114
  92. janito/tools/tools_schema.py +12 -6
  93. {janito-3.1.0.dist-info → janito-3.3.0.dist-info}/METADATA +1 -1
  94. {janito-3.1.0.dist-info → janito-3.3.0.dist-info}/RECORD +154 -87
  95. janito/llm/cancellation_manager.py +0 -62
  96. janito/llm/enter_cancellation.py +0 -93
  97. /janito/{plugin_system → plugin_system_backup_20250825_070018}/__init__.py +0 -0
  98. /janito/{plugin_system → plugin_system_backup_20250825_070018}/base.py +0 -0
  99. /janito/{plugin_system → plugin_system_backup_20250825_070018}/core_loader.py +0 -0
  100. /janito/{plugin_system → plugin_system_backup_20250825_070018}/core_loader_fixed.py +0 -0
  101. /janito/{plugins → plugins_backup_20250825_070018}/auto_loader.py +0 -0
  102. /janito/{plugins → plugins_backup_20250825_070018}/auto_loader_fixed.py +0 -0
  103. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/get_file_outline/java_outline.py +0 -0
  104. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/get_file_outline/markdown_outline.py +0 -0
  105. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/get_file_outline/python_outline.py +0 -0
  106. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/get_file_outline/search_outline.py +0 -0
  107. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/search_text/match_lines.py +0 -0
  108. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/search_text/pattern_utils.py +0 -0
  109. /janito/{plugins → plugins_backup_20250825_070018/core/codeanalyzer}/tools/search_text/traverse_directory.py +0 -0
  110. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/css_validator.py +0 -0
  111. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/html_validator.py +0 -0
  112. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/jinja2_validator.py +0 -0
  113. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/js_validator.py +0 -0
  114. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/json_validator.py +0 -0
  115. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/markdown_validator.py +0 -0
  116. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/ps1_validator.py +0 -0
  117. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/python_validator.py +0 -0
  118. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/xml_validator.py +0 -0
  119. /janito/{plugins → plugins_backup_20250825_070018/core/filemanager}/tools/validate_file_syntax/yaml_validator.py +0 -0
  120. /janito/{plugins → plugins_backup_20250825_070018}/core_adapter.py +0 -0
  121. /janito/{plugins → plugins_backup_20250825_070018}/discovery_core.py +0 -0
  122. /janito/{plugins → plugins_backup_20250825_070018}/tools/__init__.py +0 -0
  123. /janito/{plugins → plugins_backup_20250825_070018}/tools/ask_user.py +0 -0
  124. /janito/{plugins → plugins_backup_20250825_070018}/tools/copy_file.py +0 -0
  125. /janito/{plugins → plugins_backup_20250825_070018}/tools/core_tools_plugin.py +0 -0
  126. /janito/{plugins → plugins_backup_20250825_070018}/tools/create_directory.py +0 -0
  127. /janito/{plugins → plugins_backup_20250825_070018}/tools/create_file.py +0 -0
  128. /janito/{plugins → plugins_backup_20250825_070018}/tools/decorators.py +0 -0
  129. /janito/{plugins → plugins_backup_20250825_070018}/tools/fetch_url.py +0 -0
  130. /janito/{plugins → plugins_backup_20250825_070018}/tools/find_files.py +0 -0
  131. /janito/{plugins → plugins_backup_20250825_070018}/tools/get_file_outline/__init__.py +0 -0
  132. /janito/{plugins → plugins_backup_20250825_070018}/tools/get_file_outline/core.py +0 -0
  133. /janito/{plugins → plugins_backup_20250825_070018}/tools/move_file.py +0 -0
  134. /janito/{plugins → plugins_backup_20250825_070018}/tools/open_html_in_browser.py +0 -0
  135. /janito/{plugins → plugins_backup_20250825_070018}/tools/open_url.py +0 -0
  136. /janito/{plugins → plugins_backup_20250825_070018}/tools/python_code_run.py +0 -0
  137. /janito/{plugins → plugins_backup_20250825_070018}/tools/python_command_run.py +0 -0
  138. /janito/{plugins → plugins_backup_20250825_070018}/tools/python_file_run.py +0 -0
  139. /janito/{plugins → plugins_backup_20250825_070018}/tools/read_chart.py +0 -0
  140. /janito/{plugins → plugins_backup_20250825_070018}/tools/read_files.py +0 -0
  141. /janito/{plugins → plugins_backup_20250825_070018}/tools/remove_directory.py +0 -0
  142. /janito/{plugins → plugins_backup_20250825_070018}/tools/remove_file.py +0 -0
  143. /janito/{plugins → plugins_backup_20250825_070018}/tools/replace_text_in_file.py +0 -0
  144. /janito/{plugins → plugins_backup_20250825_070018}/tools/run_bash_command.py +0 -0
  145. /janito/{plugins → plugins_backup_20250825_070018}/tools/run_powershell_command.py +0 -0
  146. /janito/{plugins → plugins_backup_20250825_070018}/tools/search_text/__init__.py +0 -0
  147. /janito/{plugins → plugins_backup_20250825_070018}/tools/search_text/core.py +0 -0
  148. /janito/{plugins → plugins_backup_20250825_070018}/tools/show_image.py +0 -0
  149. /janito/{plugins → plugins_backup_20250825_070018}/tools/show_image_grid.py +0 -0
  150. /janito/{plugins → plugins_backup_20250825_070018}/tools/validate_file_syntax/__init__.py +0 -0
  151. /janito/{plugins → plugins_backup_20250825_070018}/tools/validate_file_syntax/core.py +0 -0
  152. /janito/{plugins → plugins_backup_20250825_070018}/tools/view_file.py +0 -0
  153. {janito-3.1.0.dist-info → janito-3.3.0.dist-info}/WHEEL +0 -0
  154. {janito-3.1.0.dist-info → janito-3.3.0.dist-info}/entry_points.txt +0 -0
  155. {janito-3.1.0.dist-info → janito-3.3.0.dist-info}/licenses/LICENSE +0 -0
  156. {janito-3.1.0.dist-info → janito-3.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,243 @@
1
+ """
2
+ Plugin manager for loading and managing plugins.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import importlib
8
+ import importlib.util
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Any
11
+ import logging
12
+
13
+ from janito.plugin_system.base import Plugin, PluginMetadata
14
+ from .discovery import discover_plugins
15
+ from .config import load_plugins_config, get_user_plugins_dir
16
+ from .builtin import BuiltinPluginRegistry, load_builtin_plugin
17
+ from janito.tools.adapters.local import LocalToolsAdapter
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class PluginManager:
23
+ """
24
+ Manages plugin loading, registration, and lifecycle.
25
+ """
26
+
27
+ def __init__(self, tools_adapter: Optional[LocalToolsAdapter] = None):
28
+ self.tools_adapter = tools_adapter or LocalToolsAdapter()
29
+ self.plugins: Dict[str, Plugin] = {}
30
+ self.plugin_configs: Dict[str, Dict[str, Any]] = {}
31
+ self.plugin_paths: List[Path] = []
32
+
33
+ def add_plugin_path(self, path: str) -> None:
34
+ """Add a directory to search for plugins."""
35
+ plugin_path = Path(path)
36
+ if plugin_path.exists() and plugin_path.is_dir():
37
+ self.plugin_paths.append(plugin_path)
38
+ if str(plugin_path) not in sys.path:
39
+ sys.path.insert(0, str(plugin_path))
40
+
41
+ def load_plugin(
42
+ self, plugin_name: str, config: Optional[Dict[str, Any]] = None
43
+ ) -> bool:
44
+ """
45
+ Load a plugin by name.
46
+
47
+ Args:
48
+ plugin_name: Name of the plugin to load
49
+ config: Optional configuration for the plugin
50
+
51
+ Returns:
52
+ True if plugin loaded successfully
53
+ """
54
+ try:
55
+ if plugin_name in self.plugins:
56
+ logger.warning(f"Plugin {plugin_name} already loaded")
57
+ return True
58
+
59
+ plugin = discover_plugins(plugin_name, self.plugin_paths)
60
+ if not plugin:
61
+ logger.error(f"Plugin {plugin_name} not found")
62
+ return False
63
+
64
+ # Store config
65
+ if config:
66
+ self.plugin_configs[plugin_name] = config
67
+
68
+ # Validate config if provided
69
+ if config and hasattr(plugin, "validate_config"):
70
+ if not plugin.validate_config(config):
71
+ logger.error(f"Invalid configuration for plugin {plugin_name}")
72
+ return False
73
+
74
+ # Initialize plugin
75
+ plugin.initialize()
76
+
77
+ # Register tools
78
+ tools = plugin.get_tools()
79
+ for tool_class in tools:
80
+ self.tools_adapter.register_tool(tool_class)
81
+
82
+ # Store plugin
83
+ self.plugins[plugin_name] = plugin
84
+
85
+ logger.info(f"Successfully loaded plugin: {plugin_name}")
86
+ return True
87
+
88
+ except Exception as e:
89
+ logger.error(f"Failed to load plugin {plugin_name}: {e}")
90
+ return False
91
+
92
+ def unload_plugin(self, plugin_name: str) -> bool:
93
+ """
94
+ Unload a plugin.
95
+
96
+ Args:
97
+ plugin_name: Name of the plugin to unload
98
+
99
+ Returns:
100
+ True if plugin unloaded successfully
101
+ """
102
+ try:
103
+ if plugin_name not in self.plugins:
104
+ logger.warning(f"Plugin {plugin_name} not loaded")
105
+ return False
106
+
107
+ plugin = self.plugins[plugin_name]
108
+
109
+ # Unregister tools
110
+ tools = plugin.get_tools()
111
+ for tool_class in tools:
112
+ tool_name = getattr(tool_class(), "tool_name", None)
113
+ if tool_name:
114
+ self.tools_adapter.unregister_tool(tool_name)
115
+
116
+ # Cleanup plugin
117
+ plugin.cleanup()
118
+
119
+ # Remove from registry
120
+ del self.plugins[plugin_name]
121
+ if plugin_name in self.plugin_configs:
122
+ del self.plugin_configs[plugin_name]
123
+
124
+ logger.info(f"Successfully unloaded plugin: {plugin_name}")
125
+ return True
126
+
127
+ except Exception as e:
128
+ logger.error(f"Failed to unload plugin {plugin_name}: {e}")
129
+ return False
130
+
131
+ def list_plugins(self) -> List[str]:
132
+ """Return list of loaded plugin names."""
133
+ return list(self.plugins.keys())
134
+
135
+ def get_plugin(self, plugin_name: str) -> Optional[Plugin]:
136
+ """Get a loaded plugin by name."""
137
+ return self.plugins.get(plugin_name)
138
+
139
+ def get_plugin_metadata(self, plugin_name: str) -> Optional[PluginMetadata]:
140
+ """Get metadata for a loaded plugin."""
141
+ plugin = self.plugins.get(plugin_name)
142
+ return plugin.metadata if plugin else None
143
+
144
+ def load_plugins_from_config(self, config: Dict[str, Any]) -> None:
145
+ """
146
+ Load plugins from configuration.
147
+
148
+ Args:
149
+ config: Configuration dict with plugin settings
150
+ """
151
+ plugins_config = config.get("plugins", {})
152
+
153
+ # Add plugin paths
154
+ for path in plugins_config.get("paths", []):
155
+ self.add_plugin_path(path)
156
+
157
+ # Load plugins
158
+ for plugin_name, plugin_config in plugins_config.get("load", {}).items():
159
+ if isinstance(plugin_config, bool):
160
+ if plugin_config:
161
+ self.load_plugin(plugin_name)
162
+ else:
163
+ self.load_plugin(plugin_name, plugin_config)
164
+
165
+ def load_plugins_from_user_config(self) -> None:
166
+ """
167
+ Load plugins from user configuration directory.
168
+ Uses ~/.janito/plugins.json instead of janito.json
169
+ """
170
+ config = load_plugins_config()
171
+ self.load_plugins_from_config(config)
172
+
173
+ def reload_plugin(self, plugin_name: str) -> bool:
174
+ """
175
+ Reload a plugin.
176
+
177
+ Args:
178
+ plugin_name: Name of the plugin to reload
179
+
180
+ Returns:
181
+ True if plugin reloaded successfully
182
+ """
183
+ config = self.plugin_configs.get(plugin_name)
184
+ self.unload_plugin(plugin_name)
185
+ return self.load_plugin(plugin_name, config)
186
+
187
+ def get_loaded_plugins_info(self) -> Dict[str, Dict[str, Any]]:
188
+ """Get information about all loaded plugins."""
189
+ info = {}
190
+ for name, plugin in self.plugins.items():
191
+ info[name] = {
192
+ "metadata": plugin.metadata,
193
+ "tools": [tool.__name__ for tool in plugin.get_tools()],
194
+ "commands": list(plugin.get_commands().keys()),
195
+ "config": self.plugin_configs.get(name, {}),
196
+ "builtin": BuiltinPluginRegistry.is_builtin(name),
197
+ "resources": [
198
+ {
199
+ "name": resource.name,
200
+ "type": resource.type,
201
+ "description": resource.description,
202
+ "schema": resource.schema,
203
+ }
204
+ for resource in plugin.get_resources()
205
+ ],
206
+ }
207
+ return info
208
+
209
+ def get_plugin_resources(self, plugin_name: str) -> List[Dict[str, Any]]:
210
+ """
211
+ Get resources provided by a specific plugin.
212
+
213
+ Args:
214
+ plugin_name: Name of the plugin
215
+
216
+ Returns:
217
+ List of resource dictionaries
218
+ """
219
+ plugin = self.plugins.get(plugin_name)
220
+ if not plugin:
221
+ return []
222
+
223
+ return [
224
+ {
225
+ "name": resource.name,
226
+ "type": resource.type,
227
+ "description": resource.description,
228
+ "schema": resource.schema,
229
+ }
230
+ for resource in plugin.get_resources()
231
+ ]
232
+
233
+ def list_all_resources(self) -> Dict[str, List[Dict[str, Any]]]:
234
+ """
235
+ List all resources from all loaded plugins.
236
+
237
+ Returns:
238
+ Dict mapping plugin names to their resources
239
+ """
240
+ all_resources = {}
241
+ for plugin_name in self.plugins:
242
+ all_resources[plugin_name] = self.get_plugin_resources(plugin_name)
243
+ return all_resources
@@ -15,6 +15,7 @@ class DeleteTextInFile(ToolBase):
15
15
  path (str): Path to the file to modify.
16
16
  start_marker (str): The starting delimiter string.
17
17
  end_marker (str): The ending delimiter string.
18
+ backup (bool, optional): Deprecated. No backups are created anymore and this flag is ignored. Defaults to False.
18
19
 
19
20
  Returns:
20
21
  str: Status message indicating the result.
@@ -0,0 +1,47 @@
1
+ import re
2
+ from typing import List, Dict
3
+
4
+
5
+ def parse_java_outline(lines: List[str]) -> List[Dict]:
6
+ """
7
+ Parses Java source code lines and extracts classes and methods with their signatures.
8
+ Returns a list of outline items: {type, name, return_type, parameters, generics, line}
9
+ """
10
+ outline = []
11
+ class_pattern = re.compile(r"\bclass\s+(\w+)(\s*<[^>]+>)?")
12
+ # Match methods with or without visibility modifiers (including package-private)
13
+ method_pattern = re.compile(
14
+ r"^(?:\s*(public|protected|private)\s+)?(?:static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\(([^)]*)\)"
15
+ )
16
+ current_class = None
17
+ for idx, line in enumerate(lines, 1):
18
+ class_match = class_pattern.search(line)
19
+ if class_match:
20
+ class_name = class_match.group(1)
21
+ generics = class_match.group(2) or ""
22
+ outline.append(
23
+ {
24
+ "type": "class",
25
+ "name": class_name,
26
+ "generics": generics.strip("<>") if generics else None,
27
+ "line": idx,
28
+ }
29
+ )
30
+ current_class = class_name
31
+ else:
32
+ method_match = method_pattern.search(line)
33
+ if method_match:
34
+ return_type = method_match.group(2)
35
+ method_name = method_match.group(3)
36
+ params = method_match.group(4)
37
+ outline.append(
38
+ {
39
+ "type": "method",
40
+ "class": current_class,
41
+ "name": method_name,
42
+ "return_type": return_type,
43
+ "parameters": params.strip(),
44
+ "line": idx,
45
+ }
46
+ )
47
+ return outline
@@ -0,0 +1,14 @@
1
+ import re
2
+ from typing import List
3
+
4
+
5
+ def parse_markdown_outline(lines: List[str]):
6
+ header_pat = re.compile(r"^(#+)\s+(.*)")
7
+ outline = []
8
+ for idx, line in enumerate(lines):
9
+ match = header_pat.match(line)
10
+ if match:
11
+ level = len(match.group(1))
12
+ title = match.group(2).strip()
13
+ outline.append({"level": level, "title": title, "line": idx + 1})
14
+ return outline
@@ -0,0 +1,303 @@
1
+ import re
2
+ from typing import List
3
+
4
+
5
+ def handle_assignment(idx, assign_match, outline):
6
+ var_name = assign_match.group(2)
7
+ var_type = "const" if var_name.isupper() else "var"
8
+ outline.append(
9
+ {
10
+ "type": var_type,
11
+ "name": var_name,
12
+ "start": idx + 1,
13
+ "end": idx + 1,
14
+ "parent": "",
15
+ "docstring": "",
16
+ }
17
+ )
18
+
19
+
20
+ def handle_main(idx, outline):
21
+ outline.append(
22
+ {
23
+ "type": "main",
24
+ "name": "__main__",
25
+ "start": idx + 1,
26
+ "end": idx + 1,
27
+ "parent": "",
28
+ "docstring": "",
29
+ }
30
+ )
31
+
32
+
33
+ def close_stack_objects(idx, indent, stack, obj_ranges):
34
+ while stack and indent < stack[-1][2]:
35
+ popped = stack.pop()
36
+ obj_ranges.append((popped[0], popped[1], popped[3], idx, popped[4], popped[2]))
37
+
38
+
39
+ def close_last_top_obj(idx, last_top_obj, stack, obj_ranges):
40
+ if last_top_obj and last_top_obj in stack:
41
+ stack.remove(last_top_obj)
42
+ obj_ranges.append(
43
+ (
44
+ last_top_obj[0],
45
+ last_top_obj[1],
46
+ last_top_obj[3],
47
+ idx,
48
+ last_top_obj[4],
49
+ last_top_obj[2],
50
+ )
51
+ )
52
+ return None
53
+ return last_top_obj
54
+
55
+
56
+ def handle_class(idx, class_match, indent, stack, last_top_obj):
57
+ name = class_match.group(2)
58
+ parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
59
+ obj = ("class", name, indent, idx + 1, parent)
60
+ stack.append(obj)
61
+ if indent == 0:
62
+ last_top_obj = obj
63
+ return last_top_obj
64
+
65
+
66
+ def handle_function(idx, func_match, indent, stack, last_top_obj):
67
+ name = func_match.group(2)
68
+ parent = ""
69
+ for s in reversed(stack):
70
+ if s[0] == "class" and indent > s[2]:
71
+ parent = s[1]
72
+ break
73
+ obj = ("function", name, indent, idx + 1, parent)
74
+ stack.append(obj)
75
+ if indent == 0:
76
+ last_top_obj = obj
77
+ return last_top_obj
78
+
79
+
80
+ def process_line(idx, line, regexes, stack, obj_ranges, outline, last_top_obj):
81
+ class_pat, func_pat, assign_pat, main_pat = regexes
82
+ class_match = class_pat.match(line)
83
+ func_match = func_pat.match(line)
84
+ assign_match = assign_pat.match(line)
85
+ indent = len(line) - len(line.lstrip())
86
+ # If a new top-level class or function starts, close the previous one
87
+ if (class_match or func_match) and indent == 0 and last_top_obj:
88
+ last_top_obj = close_last_top_obj(idx, last_top_obj, stack, obj_ranges)
89
+ if class_match:
90
+ last_top_obj = handle_class(idx, class_match, indent, stack, last_top_obj)
91
+ elif func_match:
92
+ last_top_obj = handle_function(idx, func_match, indent, stack, last_top_obj)
93
+ elif assign_match and indent == 0:
94
+ handle_assignment(idx, assign_match, outline)
95
+ main_match = main_pat.match(line)
96
+ if main_match:
97
+ handle_main(idx, outline)
98
+ close_stack_objects(idx, indent, stack, obj_ranges)
99
+ return last_top_obj
100
+
101
+
102
+ def extract_signature_and_decorators(lines, start_idx):
103
+ """
104
+ Extracts the signature line and leading decorators for a given function/class/method.
105
+ Returns (signature:str, decorators:List[str], signature_lineno:int)
106
+ """
107
+ decorators = []
108
+ sig_line = None
109
+ sig_lineno = start_idx
110
+ for i in range(start_idx - 1, -1, -1):
111
+ striped = lines[i].strip()
112
+ if striped.startswith("@"):
113
+ decorators.insert(0, striped)
114
+ sig_lineno = i
115
+ elif not striped:
116
+ continue
117
+ else:
118
+ break
119
+ # Find the signature line itself
120
+ for k in range(start_idx, len(lines)):
121
+ striped = lines[k].strip()
122
+ if striped.startswith("def ") or striped.startswith("class "):
123
+ sig_line = striped
124
+ sig_lineno = k
125
+ break
126
+ return sig_line, decorators, sig_lineno
127
+
128
+
129
+ def extract_docstring(lines, start_idx, end_idx):
130
+ """Extracts a docstring from lines[start_idx:end_idx] if present."""
131
+ for i in range(start_idx, min(end_idx, len(lines))):
132
+ line = lines[i].lstrip()
133
+ if not line:
134
+ continue
135
+ if line.startswith('"""') or line.startswith("'''"):
136
+ quote = line[:3]
137
+ doc = line[3:]
138
+ if doc.strip().endswith(quote):
139
+ return doc.strip()[:-3].strip()
140
+ docstring_lines = [doc]
141
+ for j in range(i + 1, min(end_idx, len(lines))):
142
+ line = lines[j]
143
+ if line.strip().endswith(quote):
144
+ docstring_lines.append(line.strip()[:-3])
145
+ return "\n".join([d.strip() for d in docstring_lines]).strip()
146
+ docstring_lines.append(line)
147
+ break
148
+ else:
149
+ break
150
+ return ""
151
+
152
+
153
+ def build_outline_entry(obj, lines, outline):
154
+ obj_type, name, start, end, parent, indent = obj
155
+ # Determine if this is a method
156
+ if obj_type == "function" and parent:
157
+ outline_type = "method"
158
+ elif obj_type == "function":
159
+ outline_type = "function"
160
+ else:
161
+ outline_type = obj_type
162
+ docstring = extract_docstring(lines, start, end)
163
+ outline.append(
164
+ {
165
+ "type": outline_type,
166
+ "name": name,
167
+ "start": start,
168
+ "end": end,
169
+ "parent": parent,
170
+ "docstring": docstring,
171
+ }
172
+ )
173
+
174
+
175
+ def process_lines(lines, regexes):
176
+ outline = []
177
+ stack = []
178
+ obj_ranges = []
179
+ last_top_obj = None
180
+ for idx, line in enumerate(lines):
181
+ last_top_obj = process_line(
182
+ idx, line, regexes, stack, obj_ranges, outline, last_top_obj
183
+ )
184
+ # Close any remaining open objects
185
+ for popped in stack:
186
+ obj_ranges.append(
187
+ (popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
188
+ )
189
+ return outline, obj_ranges
190
+
191
+
192
+ def build_outline(obj_ranges, lines, outline):
193
+ for obj in obj_ranges:
194
+ build_outline_entry(obj, lines, outline)
195
+ return outline
196
+
197
+
198
+ def parse_python_outline(lines: List[str]):
199
+ class_pat = re.compile(r"^(\s*)class\s+(\w+)")
200
+ func_pat = re.compile(r"^(\s*)def\s+(\w+)")
201
+ assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
202
+ main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
203
+ outline = []
204
+ stack = []
205
+ obj_ranges = []
206
+ last_top_obj = None
207
+ for idx, line in enumerate(lines):
208
+ class_match = class_pat.match(line)
209
+ func_match = func_pat.match(line)
210
+ assign_match = assign_pat.match(line)
211
+ indent = len(line) - len(line.lstrip())
212
+ parent = ""
213
+ for s in reversed(stack):
214
+ if s[0] == "class" and indent > s[2]:
215
+ parent = s[1]
216
+ break
217
+ if class_match:
218
+ obj = ("class", class_match.group(2), idx + 1, None, parent, indent)
219
+ stack.append(obj)
220
+ last_top_obj = obj
221
+ elif func_match:
222
+ obj = ("function", func_match.group(2), idx + 1, None, parent, indent)
223
+ stack.append(obj)
224
+ last_top_obj = obj
225
+ elif assign_match and indent == 0:
226
+ outline.append(
227
+ {
228
+ "type": "const" if assign_match.group(2).isupper() else "var",
229
+ "name": assign_match.group(2),
230
+ "start": idx + 1,
231
+ "end": idx + 1,
232
+ "parent": "",
233
+ "signature": line.strip(),
234
+ "decorators": [],
235
+ "docstring": "",
236
+ }
237
+ )
238
+ if line.strip().startswith("if __name__ == "):
239
+ outline.append(
240
+ {
241
+ "type": "main",
242
+ "name": "__main__",
243
+ "start": idx + 1,
244
+ "end": idx + 1,
245
+ "parent": "",
246
+ "signature": line.strip(),
247
+ "decorators": [],
248
+ "docstring": "",
249
+ }
250
+ )
251
+ # Close stack objects if indent falls back
252
+ while stack and indent <= stack[-1][5] and idx + 1 > stack[-1][2]:
253
+ finished = stack.pop()
254
+ outline_entry = finished[:2] + (
255
+ finished[2],
256
+ idx + 1,
257
+ finished[4],
258
+ finished[5],
259
+ )
260
+ build_outline_entry(outline_entry, lines, outline)
261
+ # Close any remaining objects
262
+ while stack:
263
+ finished = stack.pop()
264
+ outline_entry = finished[:2] + (
265
+ finished[2],
266
+ len(lines),
267
+ finished[4],
268
+ finished[5],
269
+ )
270
+ build_outline_entry(outline_entry, lines, outline)
271
+ return outline
272
+
273
+ class_pat = re.compile(r"^(\s*)class\s+(\w+)")
274
+ func_pat = re.compile(r"^(\s*)def\s+(\w+)")
275
+ assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
276
+ main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
277
+ regexes = (class_pat, func_pat, assign_pat, main_pat)
278
+ outline, obj_ranges = process_lines(lines, regexes)
279
+ return build_outline(obj_ranges, lines, outline)
280
+
281
+
282
+ def extract_docstring(lines, start_idx, end_idx):
283
+ """Extracts a docstring from lines[start_idx:end_idx] if present."""
284
+ for i in range(start_idx, min(end_idx, len(lines))):
285
+ line = lines[i].lstrip()
286
+ if not line:
287
+ continue
288
+ if line.startswith('"""') or line.startswith("'''"):
289
+ quote = line[:3]
290
+ doc = line[3:]
291
+ if doc.strip().endswith(quote):
292
+ return doc.strip()[:-3].strip()
293
+ docstring_lines = [doc]
294
+ for j in range(i + 1, min(end_idx, len(lines))):
295
+ line = lines[j]
296
+ if line.strip().endswith(quote):
297
+ docstring_lines.append(line.strip()[:-3])
298
+ return "\n".join([d.strip() for d in docstring_lines]).strip()
299
+ docstring_lines.append(line)
300
+ break
301
+ else:
302
+ break
303
+ return ""
@@ -0,0 +1,36 @@
1
+ from janito.tools.tool_base import ToolBase, ToolPermissions
2
+ from janito.report_events import ReportAction
3
+ from janito.tools.loop_protection_decorator import protect_against_loops
4
+
5
+
6
+ class SearchOutlineTool(ToolBase):
7
+ """
8
+ Tool for searching outlines in files.
9
+
10
+ Args:
11
+ path (str): Path to the file for which to generate an outline.
12
+ Returns:
13
+ str: Outline search result or status message.
14
+ """
15
+
16
+ permissions = ToolPermissions(read=True)
17
+ tool_name = "search_outline"
18
+
19
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
20
+ def run(self, path: str) -> str:
21
+ from janito.tools.tool_utils import display_path
22
+ from janito.i18n import tr
23
+
24
+ self.report_action(
25
+ tr(
26
+ "🔍 Searching for outline in '{disp_path}'",
27
+ disp_path=display_path(path),
28
+ ),
29
+ ReportAction.READ,
30
+ )
31
+ # ... rest of implementation ...
32
+ # Example warnings and successes:
33
+ # self.report_warning(tr("No files found with supported extensions."))
34
+ # self.report_warning(tr("Error reading {path}: {error}", path=path, error=e))
35
+ # self.report_success(tr("✅ {count} {match_word} found", count=len(output), match_word=pluralize('match', len(output))))
36
+ pass