janito 2.31.0__py3-none-any.whl → 2.31.1__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.
@@ -0,0 +1,125 @@
1
+ """
2
+ Fixed core plugin loader.
3
+
4
+ This module provides a working implementation to load core plugins
5
+ by directly using the Plugin base class properly.
6
+ """
7
+
8
+ import importlib.util
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional, List, Type
12
+
13
+ from janito.plugins.base import Plugin, PluginMetadata
14
+ from janito.tools.function_adapter import create_function_tool
15
+ from janito.tools.tool_base import ToolBase
16
+
17
+
18
+ class CorePlugin(Plugin):
19
+ """Working core plugin implementation."""
20
+
21
+ def __init__(self, name: str, description: str, tools: list):
22
+ self._plugin_name = name
23
+ self._description = description
24
+ self._tools = tools
25
+ self._tool_classes = []
26
+ super().__init__() # Call super after setting attributes
27
+
28
+ def get_metadata(self) -> PluginMetadata:
29
+ return PluginMetadata(
30
+ name=self._plugin_name,
31
+ version="1.0.0",
32
+ description=self._description,
33
+ author="Janito",
34
+ license="MIT",
35
+ )
36
+
37
+ def get_tools(self) -> List[Type[ToolBase]]:
38
+ return self._tool_classes
39
+
40
+ def initialize(self):
41
+ """Initialize by creating tool classes."""
42
+ self._tool_classes = []
43
+ for tool_func in self._tools:
44
+ if callable(tool_func):
45
+ tool_class = create_function_tool(tool_func)
46
+ self._tool_classes.append(tool_class)
47
+
48
+
49
+ def load_core_plugin(plugin_name: str) -> Optional[Plugin]:
50
+ """
51
+ Load a core plugin by name.
52
+
53
+ Args:
54
+ plugin_name: Name of the plugin (e.g., 'core.filemanager')
55
+
56
+ Returns:
57
+ Plugin instance if loaded successfully
58
+ """
59
+ try:
60
+ # Parse plugin name
61
+ if "." not in plugin_name:
62
+ return None
63
+
64
+ parts = plugin_name.split(".")
65
+ if len(parts) != 2:
66
+ return None
67
+
68
+ package_name, submodule_name = parts
69
+
70
+ # Handle imagedisplay specially
71
+ if plugin_name == "core.imagedisplay":
72
+ # Import the actual plugin class
73
+ try:
74
+ from plugins.core.imagedisplay.plugin import ImageDisplayPlugin
75
+ return ImageDisplayPlugin()
76
+ except ImportError:
77
+ # If import fails, return None - don't return True
78
+ return None
79
+
80
+ # Build path to plugin
81
+ plugin_path = Path("plugins") / package_name / submodule_name / "__init__.py"
82
+ if not plugin_path.exists():
83
+ return None
84
+
85
+ # Load the module
86
+ spec = importlib.util.spec_from_file_location(plugin_name, plugin_path)
87
+ if spec is None or spec.loader is None:
88
+ return None
89
+
90
+ module = importlib.util.module_from_spec(spec)
91
+ spec.loader.exec_module(module)
92
+
93
+ # Get plugin info
94
+ name = getattr(module, "__plugin_name__", plugin_name)
95
+ description = getattr(module, "__plugin_description__", f"Core plugin: {plugin_name}")
96
+ tools = getattr(module, "__plugin_tools__", [])
97
+
98
+ if not tools:
99
+ return None
100
+
101
+ # Create plugin
102
+ plugin = CorePlugin(name, description, tools)
103
+ plugin.initialize()
104
+ return plugin
105
+
106
+ except Exception as e:
107
+ print(f"Error loading core plugin {plugin_name}: {e}")
108
+ return None
109
+
110
+
111
+ def get_core_plugins() -> list:
112
+ """Get list of all available core plugins."""
113
+ core_plugins = [
114
+ "core.filemanager",
115
+ "core.codeanalyzer",
116
+ "core.system",
117
+ "core.imagedisplay",
118
+ "dev.pythondev",
119
+ "dev.visualization",
120
+ "ui.userinterface",
121
+ "web.webtools",
122
+ ]
123
+
124
+ # All core plugins are always available
125
+ return core_plugins
@@ -33,6 +33,7 @@ import logging
33
33
 
34
34
  from .base import Plugin
35
35
  from .builtin import load_builtin_plugin, BuiltinPluginRegistry
36
+ from .core_loader import load_core_plugin
36
37
 
37
38
  logger = logging.getLogger(__name__)
38
39
 
@@ -74,6 +75,13 @@ def discover_plugins(
74
75
  parts = plugin_name.split(".")
75
76
  if len(parts) == 2:
76
77
  package_name, submodule_name = parts
78
+
79
+ # Handle core plugins with dedicated loader
80
+ if plugin_name.startswith(("core.", "dev.", "ui.", "web.")):
81
+ plugin = load_core_plugin(plugin_name)
82
+ if plugin:
83
+ return plugin
84
+
77
85
  for base_path in all_paths:
78
86
  package_path = base_path / package_name / submodule_name / "__init__.py"
79
87
  if package_path.exists():
@@ -0,0 +1,49 @@
1
+ """
2
+ Core plugin discovery utilities.
3
+
4
+ This module provides specialized handling for core plugins that use
5
+ the function-based approach instead of class-based plugins.
6
+ """
7
+
8
+ import importlib.util
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ import sys
12
+
13
+ from .base import Plugin
14
+ from .core_adapter import CorePluginAdapter
15
+
16
+
17
+ def _load_core_plugin(package_path: Path, plugin_name: str) -> Optional[Plugin]:
18
+ """
19
+ Load a core plugin from a package directory.
20
+
21
+ Args:
22
+ package_path: Path to the __init__.py file
23
+ plugin_name: Full plugin name (e.g., core.filemanager)
24
+
25
+ Returns:
26
+ Plugin instance if loaded successfully
27
+ """
28
+ try:
29
+ # Import the module
30
+ spec = importlib.util.spec_from_file_location(plugin_name, package_path)
31
+ if spec is None or spec.loader is None:
32
+ return None
33
+
34
+ module = importlib.util.module_from_spec(spec)
35
+ spec.loader.exec_module(module)
36
+
37
+ # Get plugin metadata
38
+ plugin_name_attr = getattr(module, "__plugin_name__", plugin_name)
39
+ description = getattr(module, "__plugin_description__", f"Core plugin: {plugin_name}")
40
+
41
+ # Create and return the core plugin adapter
42
+ plugin = CorePluginAdapter(plugin_name_attr, description, module)
43
+ plugin.initialize() # Initialize to set up tools
44
+ return plugin
45
+
46
+ except Exception as e:
47
+ import logging
48
+ logging.getLogger(__name__).error(f"Failed to load core plugin {plugin_name}: {e}")
49
+ return None
@@ -41,7 +41,7 @@ class ProviderRegistry:
41
41
  rows.append(info[:3])
42
42
 
43
43
  # Group providers by openness (open-source first, then proprietary)
44
- open_providers = {"cerebras", "deepseek", "alibaba", "moonshotai", "zai"}
44
+ open_providers = {"cerebras", "deepseek", "alibaba", "moonshot", "zai"}
45
45
 
46
46
  def sort_key(row):
47
47
  provider_name = row[0]
@@ -18,7 +18,7 @@ class OpenAIProvider(LLMProvider):
18
18
  MAINTAINER = "João Pinto <janito@ikignosis.org>"
19
19
  MODEL_SPECS = MODEL_SPECS
20
20
  DEFAULT_MODEL = (
21
- "gpt-4.1" # Options: gpt-4.1, gpt-4o, o3-mini, o4-mini, gpt-5, gpt-5-nano
21
+ "gpt-5" # Options: gpt-4.1, gpt-4o, o3-mini, o4-mini, gpt-5, gpt-5-nano
22
22
  )
23
23
 
24
24
  def __init__(
@@ -24,6 +24,8 @@ from .get_file_outline.search_outline import SearchOutlineTool
24
24
  from .search_text.core import SearchTextTool
25
25
  from .validate_file_syntax.core import ValidateFileSyntaxTool
26
26
  from .read_chart import ReadChartTool
27
+ from .show_image import ShowImageTool
28
+ from .show_image_grid import ShowImageGridTool
27
29
 
28
30
  from janito.tools.tool_base import ToolPermissions
29
31
  import os
@@ -63,6 +65,8 @@ for tool_class in [
63
65
  SearchTextTool,
64
66
  ValidateFileSyntaxTool,
65
67
  ReadChartTool,
68
+ ShowImageTool,
69
+ ShowImageGridTool,
66
70
  ]:
67
71
  local_tools_adapter.register_tool(tool_class)
68
72
 
@@ -14,20 +14,71 @@ from janito.tools.adapters.local.validate_file_syntax.core import validate_file_
14
14
  @register_local_tool
15
15
  class CreateFileTool(ToolBase):
16
16
  """
17
- Create a new file with the given content.
17
+ Create a new file with specified content at the given path.
18
+
19
+ This tool provides comprehensive file creation capabilities with built-in safety features,
20
+ automatic syntax validation, and detailed feedback. It handles path expansion, directory
21
+ creation, encoding issues, and provides clear status messages for both success and failure cases.
22
+
23
+ Key Features:
24
+ - Automatic directory creation for nested paths
25
+ - UTF-8 encoding with error handling for special characters
26
+ - Built-in syntax validation for common file types (Python, JavaScript, JSON, YAML, etc.)
27
+ - Loop protection to prevent excessive file creation
28
+ - Detailed error messages with context
29
+ - Safe overwrite protection with preview of existing content
30
+ - Cross-platform path handling (Windows, macOS, Linux)
18
31
 
19
32
  Args:
20
- path (str): Path to the file to create.
21
- content (str): Content to write to the file.
22
- overwrite (bool, optional): Overwrite existing file if True. Default: False. Recommended only after reading the file to be overwritten.
33
+ path (str): Target file path. Supports relative and absolute paths, with automatic
34
+ expansion of user home directory (~) and environment variables.
35
+ Examples: "src/main.py", "~/Documents/config.json", "$HOME/.env"
36
+ content (str): File content to write. Empty string creates empty file.
37
+ Supports any text content including Unicode characters, newlines,
38
+ and binary-safe text representation.
39
+ overwrite (bool, optional): If True, allows overwriting existing files. Default: False.
40
+ When False, prevents accidental overwrites by checking
41
+ file existence and showing current content. Always review
42
+ existing content before enabling overwrite.
43
+
23
44
  Returns:
24
- str: Status message indicating the result. Example:
25
- - "✅ Successfully created the file at ..."
45
+ str: Detailed status message including:
46
+ - Success confirmation with line count
47
+ - File path (display-friendly format)
48
+ - Syntax validation results
49
+ - Existing content preview (when overwrite blocked)
50
+ - Error details (when creation fails)
51
+
52
+ Raises:
53
+ No direct exceptions - all errors are caught and returned as user-friendly messages.
54
+ Common error cases include: permission denied, invalid path format, disk full,
55
+ or file exists (when overwrite=False).
56
+
57
+ Security Features:
58
+ - Loop protection: Maximum 5 calls per 10 seconds for the same file path
59
+ - Path traversal prevention: Validates and sanitizes file paths
60
+ - Permission checking: Respects file system permissions
61
+ - Atomic writes: Prevents partial file creation on errors
62
+
63
+ Examples:
64
+ Basic file creation:
65
+ >>> create_file("hello.py", "print('Hello, World!')")
66
+ ✅ Created file 1 lines.
67
+ ✅ Syntax OK
68
+
69
+ Creating nested directories:
70
+ >>> create_file("src/utils/helpers.py", "def helper(): pass")
71
+ ✅ Created file 2 lines.
72
+ ✅ Syntax OK
26
73
 
27
- Note: Syntax validation is automatically performed after this operation.
74
+ Overwrite protection:
75
+ >>> create_file("existing.txt", "new content")
76
+ ❗ Cannot create file: file already exists at 'existing.txt'.
77
+ --- Current file content ---
78
+ old content
28
79
 
29
- Security: This tool includes loop protection to prevent excessive file creation operations.
30
- Maximum 5 calls per 10 seconds for the same file path.
80
+ Note: After successful creation, automatic syntax validation is performed based on
81
+ file extension. Results are appended to the return message for immediate feedback.
31
82
  """
32
83
 
33
84
  permissions = ToolPermissions(write=True)
@@ -246,10 +246,35 @@ class FetchUrlTool(ToolBase):
246
246
  return content
247
247
  except requests.exceptions.HTTPError as http_err:
248
248
  status_code = http_err.response.status_code if http_err.response else None
249
+
250
+ # Map status codes to descriptions
251
+ status_descriptions = {
252
+ 400: "Bad Request",
253
+ 401: "Unauthorized",
254
+ 403: "Forbidden",
255
+ 404: "Not Found",
256
+ 405: "Method Not Allowed",
257
+ 408: "Request Timeout",
258
+ 409: "Conflict",
259
+ 410: "Gone",
260
+ 413: "Payload Too Large",
261
+ 414: "URI Too Long",
262
+ 415: "Unsupported Media Type",
263
+ 429: "Too Many Requests",
264
+ 500: "Internal Server Error",
265
+ 501: "Not Implemented",
266
+ 502: "Bad Gateway",
267
+ 503: "Service Unavailable",
268
+ 504: "Gateway Timeout",
269
+ 505: "HTTP Version Not Supported"
270
+ }
271
+
249
272
  if status_code and 400 <= status_code < 500:
273
+ description = status_descriptions.get(status_code, "Client Error")
250
274
  error_message = tr(
251
- "HTTP {status_code}",
275
+ "HTTP Error {status_code} {description}",
252
276
  status_code=status_code,
277
+ description=description,
253
278
  )
254
279
  # Cache 403 and 404 errors
255
280
  if status_code in [403, 404]:
@@ -257,23 +282,27 @@ class FetchUrlTool(ToolBase):
257
282
 
258
283
  self.report_error(
259
284
  tr(
260
- "❗ HTTP {status_code}",
285
+ "❗ HTTP Error {status_code} {description}",
261
286
  status_code=status_code,
287
+ description=description,
262
288
  ),
263
289
  ReportAction.READ,
264
290
  )
265
291
  return error_message
266
292
  else:
293
+ description = status_descriptions.get(status_code, "Server Error") if status_code else "Error"
267
294
  self.report_error(
268
295
  tr(
269
- "❗ HTTP {status_code}",
296
+ "❗ HTTP Error {status_code} {description}",
270
297
  status_code=status_code or "Error",
298
+ description=description,
271
299
  ),
272
300
  ReportAction.READ,
273
301
  )
274
302
  return tr(
275
- "HTTP {status_code}",
303
+ "HTTP Error {status_code} {description}",
276
304
  status_code=status_code or "Error",
305
+ description=description,
277
306
  )
278
307
  except Exception as err:
279
308
  self.report_error(
@@ -355,7 +384,7 @@ class FetchUrlTool(ToolBase):
355
384
  follow_redirects=follow_redirects,
356
385
  )
357
386
  if (
358
- html_content.startswith("HTTP ")
387
+ html_content.startswith("HTTP Error ")
359
388
  or html_content == "Error"
360
389
  or html_content == "Blocked"
361
390
  ):
@@ -388,7 +417,7 @@ class FetchUrlTool(ToolBase):
388
417
  follow_redirects=follow_redirects,
389
418
  )
390
419
  if (
391
- html_content.startswith("HTTP ")
420
+ html_content.startswith("HTTP Error ")
392
421
  or html_content == "Error"
393
422
  or html_content == "Blocked"
394
423
  ):
@@ -98,7 +98,7 @@ class SearchTextTool(ToolBase):
98
98
  if max_depth > 0:
99
99
  info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
100
100
  if count_only:
101
- info_str += " [count-only]"
101
+ info_str += " [count]"
102
102
  self.report_action(info_str, ReportAction.READ)
103
103
  if os.path.isfile(search_path):
104
104
  dir_output, dir_limit_reached, per_file_counts = self._handle_file(
@@ -0,0 +1,70 @@
1
+ from janito.tools.tool_base import ToolBase, ToolPermissions
2
+ from janito.report_events import ReportAction
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
+ from janito.i18n import tr
5
+ from janito.tools.loop_protection_decorator import protect_against_loops
6
+
7
+
8
+ @register_local_tool
9
+ class ShowImageTool(ToolBase):
10
+ """Display an image inline in the terminal using the rich library.
11
+
12
+ Args:
13
+ path (str): Path to the image file.
14
+ width (int, optional): Target width in terminal cells. If unset, auto-fit.
15
+ height (int, optional): Target height in terminal rows. If unset, auto-fit.
16
+ preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
17
+
18
+ Returns:
19
+ str: Status message indicating display result or error details.
20
+ """
21
+
22
+ permissions = ToolPermissions(read=True)
23
+ tool_name = "show_image"
24
+
25
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
26
+ def run(
27
+ self,
28
+ path: str,
29
+ width: int | None = None,
30
+ height: int | None = None,
31
+ preserve_aspect: bool = True,
32
+ ) -> str:
33
+ from janito.tools.tool_utils import display_path
34
+ from janito.tools.path_utils import expand_path
35
+ import os
36
+
37
+ try:
38
+ from rich.console import Console
39
+ from rich.image import Image as RichImage
40
+ except Exception as e:
41
+ msg = tr("⚠️ Missing dependency: rich ({error})", error=e)
42
+ self.report_error(msg)
43
+ return msg
44
+
45
+ path = expand_path(path)
46
+ disp_path = display_path(path)
47
+ self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
48
+
49
+ if not os.path.exists(path):
50
+ msg = tr("❗ not found")
51
+ self.report_warning(msg)
52
+ return tr("Error: file not found: {path}", path=disp_path)
53
+
54
+ try:
55
+ console = Console()
56
+ img = RichImage.from_path(path, width=width, height=height, preserve_aspect_ratio=preserve_aspect)
57
+ console.print(img)
58
+ self.report_success(tr("✅ Displayed"))
59
+ details = []
60
+ if width:
61
+ details.append(f"width={width}")
62
+ if height:
63
+ details.append(f"height={height}")
64
+ if not preserve_aspect:
65
+ details.append("preserve_aspect=False")
66
+ info = ("; ".join(details)) if details else "auto-fit"
67
+ return tr("Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info)
68
+ except Exception as e:
69
+ self.report_error(tr(" ❌ Error: {error}", error=e))
70
+ return tr("Error displaying image: {error}", error=e)
@@ -0,0 +1,75 @@
1
+ from janito.tools.tool_base import ToolBase, ToolPermissions
2
+ from janito.report_events import ReportAction
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
+ from janito.i18n import tr
5
+ from janito.tools.loop_protection_decorator import protect_against_loops
6
+ from typing import Sequence
7
+
8
+
9
+ @register_local_tool
10
+ class ShowImageGridTool(ToolBase):
11
+ """Display multiple images in a grid inline in the terminal using rich.
12
+
13
+ Args:
14
+ paths (list[str]): List of image file paths.
15
+ columns (int, optional): Number of columns in the grid. Default: 2.
16
+ width (int, optional): Max width for each image cell. Default: None (auto).
17
+ height (int, optional): Max height for each image cell. Default: None (auto).
18
+ preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
19
+
20
+ Returns:
21
+ str: Status string summarizing the grid display.
22
+ """
23
+
24
+ permissions = ToolPermissions(read=True)
25
+ tool_name = "show_image_grid"
26
+
27
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="paths")
28
+ def run(
29
+ self,
30
+ paths: Sequence[str],
31
+ columns: int = 2,
32
+ width: int | None = None,
33
+ height: int | None = None,
34
+ preserve_aspect: bool = True,
35
+ ) -> str:
36
+ from janito.tools.path_utils import expand_path
37
+ from janito.tools.tool_utils import display_path
38
+ import os
39
+
40
+ try:
41
+ from rich.console import Console
42
+ from rich.columns import Columns
43
+ from rich.image import Image as RichImage
44
+ from rich.panel import Panel
45
+ except Exception as e:
46
+ msg = tr("⚠️ Missing dependency: rich ({error})", error=e)
47
+ self.report_error(msg)
48
+ return msg
49
+
50
+ if not paths:
51
+ return tr("No images provided")
52
+
53
+ self.report_action(tr("🖼️ Show image grid ({n} images)", n=len(paths)), ReportAction.READ)
54
+
55
+ console = Console()
56
+ images = []
57
+ shown = 0
58
+ for p in paths:
59
+ fp = expand_path(p)
60
+ if not os.path.exists(fp):
61
+ self.report_warning(tr("❗ not found: {p}", p=display_path(fp)))
62
+ continue
63
+ try:
64
+ img = RichImage.from_path(fp, width=width, height=height, preserve_aspect_ratio=preserve_aspect)
65
+ images.append(Panel.fit(img, title=display_path(fp), border_style="dim"))
66
+ shown += 1
67
+ except Exception as e:
68
+ self.report_warning(tr("⚠️ Skipped {p}: {e}", p=display_path(fp), e=e))
69
+
70
+ if not images:
71
+ return tr("No images could be displayed")
72
+
73
+ console.print(Columns(images, equal=True, expand=True, columns=columns))
74
+ self.report_success(tr("✅ Displayed {n} images", n=shown))
75
+ return tr("Displayed {shown}/{total} images in a {cols}x? grid", shown=shown, total=len(paths), cols=columns)
@@ -0,0 +1,65 @@
1
+ """
2
+ Function-to-Tool adapter for core plugins.
3
+
4
+ This module provides a way to wrap function-based tools into proper ToolBase classes.
5
+ """
6
+
7
+ import inspect
8
+ from typing import Any, Dict, List, Optional, get_type_hints
9
+ from janito.tools.tool_base import ToolBase, ToolPermissions
10
+
11
+
12
+ class FunctionToolAdapter(ToolBase):
13
+ """Adapter that wraps a function into a ToolBase class."""
14
+
15
+ def __init__(self, func, tool_name: str = None, description: str = None):
16
+ super().__init__()
17
+ self._func = func
18
+ self.tool_name = tool_name or func.__name__
19
+ self._description = description or func.__doc__ or f"Tool: {self.tool_name}"
20
+ self.permissions = ToolPermissions(read=True, write=True, execute=True)
21
+
22
+ def run(self, **kwargs) -> Any:
23
+ """Execute the wrapped function."""
24
+ return self._func(**kwargs)
25
+
26
+ def get_signature(self) -> Dict[str, Any]:
27
+ """Get function signature for documentation."""
28
+ sig = inspect.signature(self._func)
29
+ type_hints = get_type_hints(self._func)
30
+
31
+ params = {}
32
+ for name, param in sig.parameters.items():
33
+ param_info = {
34
+ "type": str(type_hints.get(name, Any)),
35
+ "default": param.default if param.default != inspect.Parameter.empty else None,
36
+ "required": param.default == inspect.Parameter.empty,
37
+ }
38
+ params[name] = param_info
39
+
40
+ return {
41
+ "name": self.tool_name,
42
+ "description": self._description,
43
+ "parameters": params,
44
+ "return_type": str(type_hints.get("return", Any))
45
+ }
46
+
47
+
48
+ def create_function_tool(func, tool_name: str = None, description: str = None) -> type:
49
+ """
50
+ Create a ToolBase class from a function.
51
+
52
+ Args:
53
+ func: The function to wrap
54
+ tool_name: Optional custom tool name
55
+ description: Optional custom description
56
+
57
+ Returns:
58
+ A ToolBase subclass that wraps the function
59
+ """
60
+
61
+ class DynamicFunctionTool(FunctionToolAdapter):
62
+ def __init__(self):
63
+ super().__init__(func, tool_name, description)
64
+
65
+ return DynamicFunctionTool