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.
Files changed (55) hide show
  1. janito/agent/setup_agent.py +48 -4
  2. janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +59 -11
  3. janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +53 -7
  4. janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +110 -0
  5. janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +53 -1
  6. janito/cli/chat_mode/session.py +8 -1
  7. janito/cli/chat_mode/session_profile_select.py +20 -3
  8. janito/cli/chat_mode/shell/commands/__init__.py +2 -0
  9. janito/cli/chat_mode/shell/commands/security/__init__.py +1 -0
  10. janito/cli/chat_mode/shell/commands/security/allowed_sites.py +94 -0
  11. janito/cli/chat_mode/shell/commands/security_command.py +51 -0
  12. janito/cli/cli_commands/list_plugins.py +45 -0
  13. janito/cli/cli_commands/list_profiles.py +29 -1
  14. janito/cli/cli_commands/show_system_prompt.py +24 -10
  15. janito/cli/core/getters.py +4 -0
  16. janito/cli/core/runner.py +7 -2
  17. janito/cli/core/setters.py +10 -1
  18. janito/cli/main_cli.py +25 -3
  19. janito/cli/single_shot_mode/handler.py +3 -1
  20. janito/config_manager.py +10 -0
  21. janito/plugins/__init__.py +17 -0
  22. janito/plugins/base.py +93 -0
  23. janito/plugins/discovery.py +160 -0
  24. janito/plugins/manager.py +185 -0
  25. janito/providers/ibm/model_info.py +9 -0
  26. janito/tools/adapters/local/__init__.py +2 -0
  27. janito/tools/adapters/local/adapter.py +55 -0
  28. janito/tools/adapters/local/ask_user.py +2 -0
  29. janito/tools/adapters/local/fetch_url.py +184 -11
  30. janito/tools/adapters/local/find_files.py +2 -0
  31. janito/tools/adapters/local/get_file_outline/core.py +2 -0
  32. janito/tools/adapters/local/get_file_outline/search_outline.py +2 -0
  33. janito/tools/adapters/local/open_html_in_browser.py +2 -0
  34. janito/tools/adapters/local/open_url.py +2 -0
  35. janito/tools/adapters/local/python_code_run.py +15 -10
  36. janito/tools/adapters/local/python_command_run.py +14 -9
  37. janito/tools/adapters/local/python_file_run.py +15 -10
  38. janito/tools/adapters/local/read_chart.py +252 -0
  39. janito/tools/adapters/local/read_files.py +2 -0
  40. janito/tools/adapters/local/replace_text_in_file.py +1 -1
  41. janito/tools/adapters/local/run_bash_command.py +18 -12
  42. janito/tools/adapters/local/run_powershell_command.py +15 -9
  43. janito/tools/adapters/local/search_text/core.py +2 -0
  44. janito/tools/adapters/local/validate_file_syntax/core.py +6 -0
  45. janito/tools/adapters/local/validate_file_syntax/jinja2_validator.py +47 -0
  46. janito/tools/adapters/local/view_file.py +2 -0
  47. janito/tools/loop_protection.py +115 -0
  48. janito/tools/loop_protection_decorator.py +110 -0
  49. janito/tools/url_whitelist.py +121 -0
  50. {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/METADATA +1 -1
  51. {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/RECORD +55 -41
  52. {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/WHEEL +0 -0
  53. {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/entry_points.txt +0 -0
  54. {janito-2.21.0.dist-info → janito-2.24.0.dist-info}/licenses/LICENSE +0 -0
  55. {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
- return filename.replace("_", " ")
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
- return template_filename, None
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
- if profile:
120
- raise FileNotFoundError(
121
- f"[janito] Could not find profile-specific template '{template_filename}' in {templates_dir / template_filename} nor in janito.agent.templates.profiles package."
122
- )
123
- else:
124
- print(
125
- f"[janito] Could not find {template_filename} in {templates_dir / template_filename} nor in janito.agent.templates.profiles package."
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)
@@ -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
- unrestricted_paths = getattr(args, "unrestricted_paths", False)
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 unrestricted_paths:
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):
@@ -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-paths"],
19
+ ["-u", "--unrestricted"],
19
20
  {
20
21
  "action": "store_true",
21
- "help": "Disable path security: allow tool arguments to use any file/directory path (DANGEROUS)",
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 list_providers, list_tools, show_config, list_drivers
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 flag for single shot mode
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))