janito 2.21.0__py3-none-any.whl → 2.24.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/agent/setup_agent.py +48 -4
- janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +59 -11
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +53 -7
- janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +110 -0
- janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +53 -1
- janito/cli/chat_mode/session.py +8 -1
- janito/cli/chat_mode/session_profile_select.py +20 -3
- janito/cli/chat_mode/shell/commands/__init__.py +2 -0
- janito/cli/chat_mode/shell/commands/security/__init__.py +1 -0
- janito/cli/chat_mode/shell/commands/security/allowed_sites.py +94 -0
- janito/cli/chat_mode/shell/commands/security_command.py +51 -0
- janito/cli/cli_commands/list_plugins.py +45 -0
- janito/cli/cli_commands/list_profiles.py +29 -1
- janito/cli/cli_commands/show_system_prompt.py +24 -10
- janito/cli/core/getters.py +4 -0
- janito/cli/core/runner.py +7 -2
- janito/cli/core/setters.py +10 -1
- janito/cli/main_cli.py +25 -3
- janito/cli/single_shot_mode/handler.py +3 -1
- janito/config_manager.py +10 -0
- janito/plugins/__init__.py +17 -0
- janito/plugins/base.py +93 -0
- janito/plugins/discovery.py +160 -0
- janito/plugins/manager.py +185 -0
- janito/providers/ibm/model_info.py +9 -0
- janito/tools/adapters/local/__init__.py +2 -0
- janito/tools/adapters/local/adapter.py +55 -0
- janito/tools/adapters/local/ask_user.py +2 -0
- janito/tools/adapters/local/fetch_url.py +184 -11
- janito/tools/adapters/local/find_files.py +2 -0
- janito/tools/adapters/local/get_file_outline/core.py +2 -0
- janito/tools/adapters/local/get_file_outline/search_outline.py +2 -0
- janito/tools/adapters/local/open_html_in_browser.py +2 -0
- janito/tools/adapters/local/open_url.py +2 -0
- janito/tools/adapters/local/python_code_run.py +15 -10
- janito/tools/adapters/local/python_command_run.py +14 -9
- janito/tools/adapters/local/python_file_run.py +15 -10
- janito/tools/adapters/local/read_chart.py +252 -0
- janito/tools/adapters/local/read_files.py +2 -0
- janito/tools/adapters/local/replace_text_in_file.py +1 -1
- janito/tools/adapters/local/run_bash_command.py +18 -12
- janito/tools/adapters/local/run_powershell_command.py +15 -9
- janito/tools/adapters/local/search_text/core.py +2 -0
- janito/tools/adapters/local/validate_file_syntax/core.py +6 -0
- janito/tools/adapters/local/validate_file_syntax/jinja2_validator.py +47 -0
- janito/tools/adapters/local/view_file.py +2 -0
- janito/tools/loop_protection.py +115 -0
- janito/tools/loop_protection_decorator.py +110 -0
- janito/tools/url_whitelist.py +121 -0
- {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/METADATA +1 -1
- {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/RECORD +55 -41
- {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/WHEEL +0 -0
- {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/entry_points.txt +0 -0
- {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/licenses/LICENSE +0 -0
- {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/top_level.txt +0 -0
@@ -19,7 +19,35 @@ def _extract_profile_name(filename: str) -> str:
|
|
19
19
|
filename = filename[len(_PREFIX) :]
|
20
20
|
if filename.endswith(_SUFFIX):
|
21
21
|
filename = filename[: -len(_SUFFIX)]
|
22
|
-
|
22
|
+
|
23
|
+
# Convert to title case for consistent capitalization, but handle common acronyms
|
24
|
+
name = filename.replace("_", " ")
|
25
|
+
|
26
|
+
# Convert to proper title case with consistent capitalization
|
27
|
+
name = filename.replace("_", " ")
|
28
|
+
|
29
|
+
# Handle special cases and acronyms
|
30
|
+
special_cases = {
|
31
|
+
"python": "Python",
|
32
|
+
"tools": "Tools",
|
33
|
+
"model": "Model",
|
34
|
+
"context": "Context",
|
35
|
+
"developer": "Developer",
|
36
|
+
"analyst": "Analyst",
|
37
|
+
"conversation": "Conversation",
|
38
|
+
"without": "Without"
|
39
|
+
}
|
40
|
+
|
41
|
+
words = name.split()
|
42
|
+
capitalized_words = []
|
43
|
+
for word in words:
|
44
|
+
lower_word = word.lower()
|
45
|
+
if lower_word in special_cases:
|
46
|
+
capitalized_words.append(special_cases[lower_word])
|
47
|
+
else:
|
48
|
+
capitalized_words.append(word.capitalize())
|
49
|
+
|
50
|
+
return " ".join(capitalized_words)
|
23
51
|
|
24
52
|
|
25
53
|
def _gather_default_profiles():
|
@@ -9,6 +9,7 @@ from janito.platform_discovery import PlatformDiscovery
|
|
9
9
|
from pathlib import Path
|
10
10
|
from jinja2 import Template
|
11
11
|
import importlib.resources
|
12
|
+
import importlib.resources as resources
|
12
13
|
import re
|
13
14
|
|
14
15
|
|
@@ -60,7 +61,17 @@ def _load_template(profile, templates_dir):
|
|
60
61
|
).open("r", encoding="utf-8") as file:
|
61
62
|
template_content = file.read()
|
62
63
|
except (FileNotFoundError, ModuleNotFoundError, AttributeError):
|
63
|
-
|
64
|
+
# Also check user profiles directory
|
65
|
+
from pathlib import Path
|
66
|
+
import os
|
67
|
+
user_profiles_dir = Path(os.path.expanduser("~/.janito/profiles"))
|
68
|
+
user_template_path = user_profiles_dir / template_filename
|
69
|
+
if user_template_path.exists():
|
70
|
+
with open(user_template_path, "r", encoding="utf-8") as file:
|
71
|
+
template_content = file.read()
|
72
|
+
else:
|
73
|
+
template_content = None
|
74
|
+
return template_filename, template_content
|
64
75
|
return template_filename, template_content
|
65
76
|
|
66
77
|
|
@@ -105,6 +116,11 @@ def handle_show_system_prompt(args):
|
|
105
116
|
Path(__file__).parent.parent.parent / "agent" / "templates" / "profiles"
|
106
117
|
)
|
107
118
|
profile = getattr(args, "profile", None)
|
119
|
+
|
120
|
+
# Handle --market flag mapping to Market Analyst profile
|
121
|
+
if profile is None and getattr(args, "market", False):
|
122
|
+
profile = "Market Analyst"
|
123
|
+
|
108
124
|
if not profile:
|
109
125
|
print(
|
110
126
|
"[janito] No profile specified. The main agent runs without a system prompt template.\n"
|
@@ -116,15 +132,13 @@ def handle_show_system_prompt(args):
|
|
116
132
|
_print_debug_info(debug_flag, template_filename, allowed_permissions, context)
|
117
133
|
|
118
134
|
if not template_content:
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
)
|
127
|
-
print("No system prompt is set or resolved for this configuration.")
|
135
|
+
# Try to load directly from package resources as fallback
|
136
|
+
try:
|
137
|
+
template_content = resources.files("janito.agent.templates.profiles").joinpath(
|
138
|
+
f"system_prompt_template_{profile.lower().replace(' ', '_')}.txt.j2"
|
139
|
+
).read_text(encoding="utf-8")
|
140
|
+
except (FileNotFoundError, ModuleNotFoundError, AttributeError):
|
141
|
+
print(f"[janito] Could not find profile '{profile}'. This may be a configuration issue.")
|
128
142
|
return
|
129
143
|
|
130
144
|
template = Template(template_content)
|
janito/cli/core/getters.py
CHANGED
@@ -10,6 +10,7 @@ from janito.cli.cli_commands.list_config import handle_list_config
|
|
10
10
|
from janito.cli.cli_commands.list_drivers import handle_list_drivers
|
11
11
|
from janito.regions.cli import handle_region_info
|
12
12
|
from janito.cli.cli_commands.list_providers_region import handle_list_providers_region
|
13
|
+
from janito.cli.cli_commands.list_plugins import handle_list_plugins
|
13
14
|
from functools import partial
|
14
15
|
from janito.provider_registry import ProviderRegistry
|
15
16
|
|
@@ -23,6 +24,8 @@ GETTER_KEYS = [
|
|
23
24
|
"list_drivers",
|
24
25
|
"region_info",
|
25
26
|
"list_providers_region",
|
27
|
+
"list_plugins",
|
28
|
+
"list_plugins_available",
|
26
29
|
]
|
27
30
|
|
28
31
|
|
@@ -51,6 +54,7 @@ def handle_getter(args, config_mgr=None):
|
|
51
54
|
"list_drivers": partial(handle_list_drivers, args),
|
52
55
|
"region_info": partial(handle_region_info, args),
|
53
56
|
"list_providers_region": partial(handle_list_providers_region, args),
|
57
|
+
"list_plugins": partial(handle_list_plugins, args),
|
54
58
|
}
|
55
59
|
for arg in GETTER_KEYS:
|
56
60
|
if getattr(args, arg, False) and arg in GETTER_DISPATCH:
|
janito/cli/core/runner.py
CHANGED
@@ -144,13 +144,18 @@ def handle_runner(
|
|
144
144
|
|
145
145
|
load_disabled_tools_from_config()
|
146
146
|
|
147
|
-
|
147
|
+
unrestricted = getattr(args, "unrestricted", False)
|
148
148
|
adapter = janito.tools.get_local_tools_adapter(
|
149
149
|
workdir=getattr(args, "workdir", None)
|
150
150
|
)
|
151
|
-
if
|
151
|
+
if unrestricted:
|
152
152
|
# Patch: disable path security enforcement for this adapter instance
|
153
153
|
setattr(adapter, "unrestricted_paths", True)
|
154
|
+
|
155
|
+
# Also disable URL whitelist restrictions in unrestricted mode
|
156
|
+
from janito.tools.url_whitelist import get_url_whitelist_manager
|
157
|
+
whitelist_manager = get_url_whitelist_manager()
|
158
|
+
whitelist_manager.set_unrestricted_mode(True)
|
154
159
|
|
155
160
|
# Print allowed permissions in verbose mode
|
156
161
|
if getattr(args, "verbose", False):
|
janito/cli/core/setters.py
CHANGED
@@ -69,8 +69,17 @@ def _dispatch_set_key(key, value):
|
|
69
69
|
global_config.file_set("disabled_tools", value)
|
70
70
|
print(f"Disabled tools set to '{value}'")
|
71
71
|
return True
|
72
|
+
if key == "allowed_sites":
|
73
|
+
from janito.tools.url_whitelist import get_url_whitelist_manager
|
74
|
+
|
75
|
+
sites = [site.strip() for site in value.split(',') if site.strip()]
|
76
|
+
whitelist_manager = get_url_whitelist_manager()
|
77
|
+
whitelist_manager.set_allowed_sites(sites)
|
78
|
+
global_config.file_set("allowed_sites", value)
|
79
|
+
print(f"Allowed sites set to: {', '.join(sites)}")
|
80
|
+
return True
|
72
81
|
print(
|
73
|
-
f"Error: Unknown config key '{key}'. Supported: provider, model, max_tokens, base_url, azure_deployment_name, tool_permissions, disabled_tools"
|
82
|
+
f"Error: Unknown config key '{key}'. Supported: provider, model, max_tokens, base_url, azure_deployment_name, tool_permissions, disabled_tools, allowed_sites"
|
74
83
|
)
|
75
84
|
return True
|
76
85
|
|
janito/cli/main_cli.py
CHANGED
@@ -13,12 +13,13 @@ from janito.cli.core.event_logger import (
|
|
13
13
|
inject_debug_event_bus_if_needed,
|
14
14
|
)
|
15
15
|
|
16
|
+
|
16
17
|
definition = [
|
17
18
|
(
|
18
|
-
["-u", "--unrestricted
|
19
|
+
["-u", "--unrestricted"],
|
19
20
|
{
|
20
21
|
"action": "store_true",
|
21
|
-
"help": "
|
22
|
+
"help": "Unrestricted mode: disable path security and URL whitelist restrictions (DANGEROUS)",
|
22
23
|
},
|
23
24
|
),
|
24
25
|
(
|
@@ -43,6 +44,13 @@ definition = [
|
|
43
44
|
"help": "Start with the Python developer profile (equivalent to --profile 'Developer with Python Tools')",
|
44
45
|
},
|
45
46
|
),
|
47
|
+
(
|
48
|
+
["--market"],
|
49
|
+
{
|
50
|
+
"action": "store_true",
|
51
|
+
"help": "Start with the Market Analyst profile (equivalent to --profile 'Market Analyst')",
|
52
|
+
},
|
53
|
+
),
|
46
54
|
(
|
47
55
|
["--role"],
|
48
56
|
{
|
@@ -149,6 +157,7 @@ definition = [
|
|
149
157
|
"help": "List all providers with their regional API information",
|
150
158
|
},
|
151
159
|
),
|
160
|
+
|
152
161
|
(
|
153
162
|
["-l", "--list-models"],
|
154
163
|
{"action": "store_true", "help": "List all supported models"},
|
@@ -213,6 +222,14 @@ definition = [
|
|
213
222
|
"help": "Use custom configuration file ~/.janito/configs/NAME.json instead of default config.json",
|
214
223
|
},
|
215
224
|
),
|
225
|
+
(
|
226
|
+
["--list-plugins"],
|
227
|
+
{"action": "store_true", "help": "List all loaded plugins"},
|
228
|
+
),
|
229
|
+
(
|
230
|
+
["--list-plugins-available"],
|
231
|
+
{"action": "store_true", "help": "List all available plugins"},
|
232
|
+
),
|
216
233
|
]
|
217
234
|
|
218
235
|
MODIFIER_KEYS = [
|
@@ -221,6 +238,7 @@ MODIFIER_KEYS = [
|
|
221
238
|
"role",
|
222
239
|
"profile",
|
223
240
|
"python",
|
241
|
+
"market",
|
224
242
|
"system",
|
225
243
|
"temperature",
|
226
244
|
"verbose",
|
@@ -323,6 +341,8 @@ class JanitoCLI:
|
|
323
341
|
|
324
342
|
argkwargs["version"] = f"Janito {janito_version}"
|
325
343
|
self.parser.add_argument(*argnames, **argkwargs)
|
344
|
+
|
345
|
+
|
326
346
|
|
327
347
|
def _set_all_arg_defaults(self):
|
328
348
|
# Gather all possible keys from definition, MODIFIER_KEYS, SETTER_KEYS, GETTER_KEYS
|
@@ -370,7 +390,7 @@ class JanitoCLI:
|
|
370
390
|
if run_mode == RunMode.SET:
|
371
391
|
if self._run_set_mode():
|
372
392
|
return
|
373
|
-
# Special handling: provider is not required for
|
393
|
+
# Special handling: provider is not required for list commands
|
374
394
|
if run_mode == RunMode.GET and (
|
375
395
|
self.args.list_providers
|
376
396
|
or self.args.list_tools
|
@@ -378,6 +398,8 @@ class JanitoCLI:
|
|
378
398
|
or self.args.show_config
|
379
399
|
or self.args.list_config
|
380
400
|
or self.args.list_drivers
|
401
|
+
or self.args.list_plugins
|
402
|
+
or self.args.list_plugins_available
|
381
403
|
or self.args.ping
|
382
404
|
):
|
383
405
|
self._maybe_print_verbose_provider_model()
|
@@ -24,10 +24,12 @@ class PromptHandler:
|
|
24
24
|
self.llm_driver_config = llm_driver_config
|
25
25
|
self.role = role
|
26
26
|
# Instantiate agent together with prompt handler using the shared helper
|
27
|
-
# Handle --python
|
27
|
+
# Handle --python and --market flags for single shot mode
|
28
28
|
profile = getattr(args, "profile", None)
|
29
29
|
if profile is None and getattr(args, "python", False):
|
30
30
|
profile = "Developer with Python Tools"
|
31
|
+
if profile is None and getattr(args, "market", False):
|
32
|
+
profile = "Market Analyst"
|
31
33
|
|
32
34
|
self.agent, self.generic_handler = setup_agent_and_prompt_handler(
|
33
35
|
args=args,
|
janito/config_manager.py
CHANGED
@@ -55,6 +55,16 @@ class ConfigManager:
|
|
55
55
|
except Exception as e:
|
56
56
|
print(f"Warning: Failed to apply tool_permissions from config: {e}")
|
57
57
|
|
58
|
+
# Load plugins from config
|
59
|
+
plugins_config = self.file_config.get("plugins", {})
|
60
|
+
if plugins_config:
|
61
|
+
try:
|
62
|
+
from janito.plugins.manager import PluginManager
|
63
|
+
plugin_manager = PluginManager()
|
64
|
+
plugin_manager.load_plugins_from_config({"plugins": plugins_config})
|
65
|
+
except Exception as e:
|
66
|
+
print(f"Warning: Failed to load plugins from config: {e}")
|
67
|
+
|
58
68
|
# Load disabled tools from config - skip during startup to avoid circular imports
|
59
69
|
# This will be handled by the CLI when needed
|
60
70
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
Plugin system for janito.
|
3
|
+
|
4
|
+
This package provides a flexible plugin system that allows extending
|
5
|
+
janito's functionality with custom tools, commands, and features.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .manager import PluginManager
|
9
|
+
from .base import Plugin, PluginMetadata
|
10
|
+
from .discovery import discover_plugins
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"PluginManager",
|
14
|
+
"Plugin",
|
15
|
+
"PluginMetadata",
|
16
|
+
"discover_plugins",
|
17
|
+
]
|
janito/plugins/base.py
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
"""
|
2
|
+
Base classes for janito plugins.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from typing import Dict, Any, List, Optional, Type
|
8
|
+
from janito.tools.tool_base import ToolBase
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class PluginMetadata:
|
13
|
+
"""Metadata describing a plugin."""
|
14
|
+
name: str
|
15
|
+
version: str
|
16
|
+
description: str
|
17
|
+
author: str
|
18
|
+
license: str = "MIT"
|
19
|
+
homepage: Optional[str] = None
|
20
|
+
dependencies: List[str] = None
|
21
|
+
|
22
|
+
def __post_init__(self):
|
23
|
+
if self.dependencies is None:
|
24
|
+
self.dependencies = []
|
25
|
+
|
26
|
+
|
27
|
+
class Plugin(ABC):
|
28
|
+
"""
|
29
|
+
Base class for all janito plugins.
|
30
|
+
|
31
|
+
Plugins can provide tools, commands, or other functionality.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self):
|
35
|
+
self.metadata: PluginMetadata = self.get_metadata()
|
36
|
+
|
37
|
+
@abstractmethod
|
38
|
+
def get_metadata(self) -> PluginMetadata:
|
39
|
+
"""Return metadata describing this plugin."""
|
40
|
+
pass
|
41
|
+
|
42
|
+
def get_tools(self) -> List[Type[ToolBase]]:
|
43
|
+
"""
|
44
|
+
Return a list of tool classes provided by this plugin.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
List of ToolBase subclasses that should be registered
|
48
|
+
"""
|
49
|
+
return []
|
50
|
+
|
51
|
+
def get_commands(self) -> Dict[str, Any]:
|
52
|
+
"""
|
53
|
+
Return a dictionary of CLI commands provided by this plugin.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Dict mapping command names to command handlers
|
57
|
+
"""
|
58
|
+
return {}
|
59
|
+
|
60
|
+
def initialize(self) -> None:
|
61
|
+
"""
|
62
|
+
Called when the plugin is loaded.
|
63
|
+
Override to perform any initialization needed.
|
64
|
+
"""
|
65
|
+
pass
|
66
|
+
|
67
|
+
def cleanup(self) -> None:
|
68
|
+
"""
|
69
|
+
Called when the plugin is unloaded.
|
70
|
+
Override to perform any cleanup needed.
|
71
|
+
"""
|
72
|
+
pass
|
73
|
+
|
74
|
+
def get_config_schema(self) -> Dict[str, Any]:
|
75
|
+
"""
|
76
|
+
Return JSON schema for plugin configuration.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
JSON schema dict describing configuration options
|
80
|
+
"""
|
81
|
+
return {}
|
82
|
+
|
83
|
+
def validate_config(self, config: Dict[str, Any]) -> bool:
|
84
|
+
"""
|
85
|
+
Validate plugin configuration.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
config: Configuration dict to validate
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
True if configuration is valid
|
92
|
+
"""
|
93
|
+
return True
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""
|
2
|
+
Plugin discovery utilities.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import sys
|
7
|
+
import importlib
|
8
|
+
import importlib.util
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Optional, List
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from .base import Plugin
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
def discover_plugins(plugin_name: str, search_paths: List[Path] = None) -> Optional[Plugin]:
|
19
|
+
"""
|
20
|
+
Discover and load a plugin by name.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
plugin_name: Name of the plugin to discover
|
24
|
+
search_paths: List of directories to search for plugins
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Plugin instance if found, None otherwise
|
28
|
+
"""
|
29
|
+
if search_paths is None:
|
30
|
+
search_paths = []
|
31
|
+
|
32
|
+
# Add default search paths
|
33
|
+
default_paths = [
|
34
|
+
Path.cwd() / "plugins",
|
35
|
+
Path.home() / ".janito" / "plugins",
|
36
|
+
Path(sys.prefix) / "share" / "janito" / "plugins",
|
37
|
+
]
|
38
|
+
|
39
|
+
all_paths = search_paths + default_paths
|
40
|
+
|
41
|
+
# Try to find plugin in search paths
|
42
|
+
for base_path in all_paths:
|
43
|
+
plugin_path = base_path / plugin_name
|
44
|
+
if plugin_path.exists():
|
45
|
+
return _load_plugin_from_directory(plugin_path)
|
46
|
+
|
47
|
+
# Try as Python module
|
48
|
+
module_path = base_path / f"{plugin_name}.py"
|
49
|
+
if module_path.exists():
|
50
|
+
return _load_plugin_from_file(module_path)
|
51
|
+
|
52
|
+
# Try importing as installed package
|
53
|
+
try:
|
54
|
+
return _load_plugin_from_package(plugin_name)
|
55
|
+
except ImportError:
|
56
|
+
pass
|
57
|
+
|
58
|
+
return None
|
59
|
+
|
60
|
+
|
61
|
+
def _load_plugin_from_directory(plugin_path: Path) -> Optional[Plugin]:
|
62
|
+
"""Load a plugin from a directory."""
|
63
|
+
try:
|
64
|
+
# Look for __init__.py or plugin.py
|
65
|
+
init_file = plugin_path / "__init__.py"
|
66
|
+
plugin_file = plugin_path / "plugin.py"
|
67
|
+
|
68
|
+
if init_file.exists():
|
69
|
+
return _load_plugin_from_file(init_file, plugin_name=plugin_path.name)
|
70
|
+
elif plugin_file.exists():
|
71
|
+
return _load_plugin_from_file(plugin_file, plugin_name=plugin_path.name)
|
72
|
+
|
73
|
+
except Exception as e:
|
74
|
+
logger.error(f"Failed to load plugin from directory {plugin_path}: {e}")
|
75
|
+
|
76
|
+
return None
|
77
|
+
|
78
|
+
|
79
|
+
def _load_plugin_from_file(file_path: Path, plugin_name: str = None) -> Optional[Plugin]:
|
80
|
+
"""Load a plugin from a Python file."""
|
81
|
+
try:
|
82
|
+
if plugin_name is None:
|
83
|
+
plugin_name = file_path.stem
|
84
|
+
|
85
|
+
spec = importlib.util.spec_from_file_location(plugin_name, file_path)
|
86
|
+
if spec is None or spec.loader is None:
|
87
|
+
return None
|
88
|
+
|
89
|
+
module = importlib.util.module_from_spec(spec)
|
90
|
+
spec.loader.exec_module(module)
|
91
|
+
|
92
|
+
# Look for Plugin class
|
93
|
+
for attr_name in dir(module):
|
94
|
+
attr = getattr(module, attr_name)
|
95
|
+
if (isinstance(attr, type) and
|
96
|
+
issubclass(attr, Plugin) and
|
97
|
+
attr != Plugin):
|
98
|
+
return attr()
|
99
|
+
|
100
|
+
except Exception as e:
|
101
|
+
logger.error(f"Failed to load plugin from file {file_path}: {e}")
|
102
|
+
|
103
|
+
return None
|
104
|
+
|
105
|
+
|
106
|
+
def _load_plugin_from_package(package_name: str) -> Optional[Plugin]:
|
107
|
+
"""Load a plugin from an installed package."""
|
108
|
+
try:
|
109
|
+
module = importlib.import_module(package_name)
|
110
|
+
|
111
|
+
# Look for Plugin class
|
112
|
+
for attr_name in dir(module):
|
113
|
+
attr = getattr(module, attr_name)
|
114
|
+
if (isinstance(attr, type) and
|
115
|
+
issubclass(attr, Plugin) and
|
116
|
+
attr != Plugin):
|
117
|
+
return attr()
|
118
|
+
|
119
|
+
except ImportError as e:
|
120
|
+
logger.debug(f"Could not import package {package_name}: {e}")
|
121
|
+
|
122
|
+
return None
|
123
|
+
|
124
|
+
|
125
|
+
def list_available_plugins(search_paths: List[Path] = None) -> List[str]:
|
126
|
+
"""
|
127
|
+
List all available plugins in search paths.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
search_paths: List of directories to search for plugins
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
List of plugin names found
|
134
|
+
"""
|
135
|
+
if search_paths is None:
|
136
|
+
search_paths = []
|
137
|
+
|
138
|
+
# Add default search paths
|
139
|
+
default_paths = [
|
140
|
+
Path.cwd() / "plugins",
|
141
|
+
Path.home() / ".janito" / "plugins",
|
142
|
+
Path(sys.prefix) / "share" / "janito" / "plugins",
|
143
|
+
]
|
144
|
+
|
145
|
+
all_paths = search_paths + default_paths
|
146
|
+
plugins = []
|
147
|
+
|
148
|
+
for base_path in all_paths:
|
149
|
+
if not base_path.exists():
|
150
|
+
continue
|
151
|
+
|
152
|
+
# Look for directories with __init__.py or plugin.py
|
153
|
+
for item in base_path.iterdir():
|
154
|
+
if item.is_dir():
|
155
|
+
if (item / "__init__.py").exists() or (item / "plugin.py").exists():
|
156
|
+
plugins.append(item.name)
|
157
|
+
elif item.suffix == '.py' and item.stem != '__init__':
|
158
|
+
plugins.append(item.stem)
|
159
|
+
|
160
|
+
return sorted(set(plugins))
|