janito 3.12.0__py3-none-any.whl → 3.12.2__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/README.md +191 -191
- janito/_version.py +55 -55
- janito/agent/setup_agent.py +378 -377
- janito/cli/chat_mode/session.py +505 -505
- janito/cli/cli_commands/list_drivers.py +162 -162
- janito/cli/cli_commands/list_profiles.py +104 -107
- janito/cli/cli_commands/show_system_prompt.py +166 -166
- janito/cli/console.py +3 -3
- janito/cli/core/runner.py +250 -266
- janito/cli/main_cli.py +520 -519
- janito/cli/rich_terminal_reporter.py +2 -4
- janito/cli/single_shot_mode/handler.py +167 -167
- janito/docs/GETTING_STARTED.md +189 -189
- janito/llm/__init__.py +6 -5
- janito/llm/driver.py +290 -254
- janito/llm/response_cache.py +57 -0
- janito/plugins/builtin.py +64 -88
- janito/plugins/discovery.py +289 -289
- janito/plugins/tools/local/__init__.py +82 -80
- janito/plugins/tools/local/markdown_view.py +94 -0
- janito/plugins/tools/local/show_image.py +119 -74
- janito/plugins/tools/local/show_image_grid.py +134 -76
- janito/providers/alibaba/model_info.py +136 -105
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/METADATA +5 -4
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/RECORD +29 -28
- janito/mkdocs.yml +0 -40
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/WHEEL +0 -0
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/entry_points.txt +0 -0
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/licenses/LICENSE +0 -0
- {janito-3.12.0.dist-info → janito-3.12.2.dist-info}/top_level.txt +0 -0
janito/plugins/discovery.py
CHANGED
@@ -1,289 +1,289 @@
|
|
1
|
-
"""
|
2
|
-
Plugin discovery utilities.
|
3
|
-
|
4
|
-
Plugins can be provided in several formats:
|
5
|
-
|
6
|
-
1. Single Python file: A .py file containing a Plugin class
|
7
|
-
Example: plugins/my_plugin.py
|
8
|
-
|
9
|
-
2. Python package directory: A directory with __init__.py or plugin.py
|
10
|
-
Example: plugins/my_plugin/__init__.py
|
11
|
-
Example: plugins/my_plugin/plugin.py
|
12
|
-
|
13
|
-
3. Installed Python package: An installed package with a Plugin class
|
14
|
-
Example: pip install janito-plugin-example
|
15
|
-
|
16
|
-
4. ZIP file: A .zip file containing a Python package structure
|
17
|
-
Example: plugins/my_plugin.zip (containing package structure)
|
18
|
-
|
19
|
-
The plugin discovery system searches these locations in order:
|
20
|
-
- Current working directory/plugins/
|
21
|
-
- ~/.janito/plugins/
|
22
|
-
- Python installation share/janito/plugins/
|
23
|
-
- Any additional paths specified via configuration
|
24
|
-
"""
|
25
|
-
|
26
|
-
import os
|
27
|
-
import sys
|
28
|
-
import importlib
|
29
|
-
import importlib.util
|
30
|
-
from pathlib import Path
|
31
|
-
from typing import Optional, List
|
32
|
-
import logging
|
33
|
-
|
34
|
-
from .base import Plugin
|
35
|
-
from .builtin import load_builtin_plugin, BuiltinPluginRegistry
|
36
|
-
from .core_loader import load_core_plugin
|
37
|
-
|
38
|
-
logger = logging.getLogger(__name__)
|
39
|
-
|
40
|
-
|
41
|
-
def discover_plugins(
|
42
|
-
plugin_name: str, search_paths: List[Path] = None
|
43
|
-
) -> Optional[Plugin]:
|
44
|
-
"""
|
45
|
-
Discover and load a plugin by name.
|
46
|
-
|
47
|
-
Supports multiple plugin formats:
|
48
|
-
- Single .py files
|
49
|
-
- Python package directories
|
50
|
-
- Package-based plugins (e.g., core.filemanager)
|
51
|
-
- Installed Python packages
|
52
|
-
- ZIP files containing packages
|
53
|
-
|
54
|
-
Args:
|
55
|
-
plugin_name: Name of the plugin to discover
|
56
|
-
search_paths: List of directories to search for plugins
|
57
|
-
|
58
|
-
Returns:
|
59
|
-
Plugin instance if found, None otherwise
|
60
|
-
"""
|
61
|
-
if search_paths is None:
|
62
|
-
search_paths = []
|
63
|
-
|
64
|
-
# Add default search paths
|
65
|
-
default_paths = [
|
66
|
-
Path.cwd() / "plugins",
|
67
|
-
Path.home() / ".janito" / "plugins",
|
68
|
-
Path(sys.prefix) / "share" / "janito" / "plugins",
|
69
|
-
]
|
70
|
-
|
71
|
-
all_paths = search_paths + default_paths
|
72
|
-
|
73
|
-
# Handle package-based plugins (e.g., core.filemanager)
|
74
|
-
if "." in plugin_name:
|
75
|
-
parts = plugin_name.split(".")
|
76
|
-
if len(parts) == 2:
|
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
|
-
|
85
|
-
for base_path in all_paths:
|
86
|
-
package_path = base_path / package_name / submodule_name / "__init__.py"
|
87
|
-
if package_path.exists():
|
88
|
-
return _load_plugin_from_file(package_path, plugin_name=plugin_name)
|
89
|
-
|
90
|
-
plugin_path = base_path / package_name / submodule_name / "plugin.py"
|
91
|
-
if plugin_path.exists():
|
92
|
-
return _load_plugin_from_file(plugin_path, plugin_name=plugin_name)
|
93
|
-
|
94
|
-
# Try to find plugin in search paths
|
95
|
-
for base_path in all_paths:
|
96
|
-
plugin_path = base_path / plugin_name
|
97
|
-
if plugin_path.exists():
|
98
|
-
return _load_plugin_from_directory(plugin_path)
|
99
|
-
|
100
|
-
# Try as Python module
|
101
|
-
module_path = base_path / f"{plugin_name}.py"
|
102
|
-
if module_path.exists():
|
103
|
-
return _load_plugin_from_file(module_path)
|
104
|
-
|
105
|
-
# Check for builtin plugins
|
106
|
-
builtin_plugin = load_builtin_plugin(plugin_name)
|
107
|
-
if builtin_plugin:
|
108
|
-
return builtin_plugin
|
109
|
-
|
110
|
-
# Try importing as installed package
|
111
|
-
try:
|
112
|
-
return _load_plugin_from_package(plugin_name)
|
113
|
-
except ImportError:
|
114
|
-
pass
|
115
|
-
|
116
|
-
return None
|
117
|
-
|
118
|
-
|
119
|
-
def _load_plugin_from_directory(plugin_path: Path) -> Optional[Plugin]:
|
120
|
-
"""Load a plugin from a directory."""
|
121
|
-
try:
|
122
|
-
# Look for __init__.py or plugin.py
|
123
|
-
init_file = plugin_path / "__init__.py"
|
124
|
-
plugin_file = plugin_path / "plugin.py"
|
125
|
-
|
126
|
-
if init_file.exists():
|
127
|
-
return _load_plugin_from_file(init_file, plugin_name=plugin_path.name)
|
128
|
-
elif plugin_file.exists():
|
129
|
-
return _load_plugin_from_file(plugin_file, plugin_name=plugin_path.name)
|
130
|
-
|
131
|
-
except Exception as e:
|
132
|
-
logger.error(f"Failed to load plugin from directory {plugin_path}: {e}")
|
133
|
-
|
134
|
-
return None
|
135
|
-
|
136
|
-
|
137
|
-
def _load_plugin_from_file(
|
138
|
-
file_path: Path, plugin_name: str = None
|
139
|
-
) -> Optional[Plugin]:
|
140
|
-
"""Load a plugin from a Python file."""
|
141
|
-
try:
|
142
|
-
if plugin_name is None:
|
143
|
-
plugin_name = file_path.stem
|
144
|
-
|
145
|
-
spec = importlib.util.spec_from_file_location(plugin_name, file_path)
|
146
|
-
if spec is None or spec.loader is None:
|
147
|
-
return None
|
148
|
-
|
149
|
-
module = importlib.util.module_from_spec(spec)
|
150
|
-
spec.loader.exec_module(module)
|
151
|
-
|
152
|
-
# Look for Plugin class
|
153
|
-
for attr_name in dir(module):
|
154
|
-
attr = getattr(module, attr_name)
|
155
|
-
if isinstance(attr, type) and issubclass(attr, Plugin) and attr != Plugin:
|
156
|
-
return attr()
|
157
|
-
|
158
|
-
# Check for package-based plugin with __plugin_name__ metadata
|
159
|
-
if hasattr(module, "__plugin_name__"):
|
160
|
-
from janito.plugins.base import PluginMetadata
|
161
|
-
|
162
|
-
# Create a dynamic plugin class
|
163
|
-
class PackagePlugin(Plugin):
|
164
|
-
def __init__(self):
|
165
|
-
super().__init__()
|
166
|
-
self._module = module
|
167
|
-
|
168
|
-
def get_metadata(self) -> PluginMetadata:
|
169
|
-
return PluginMetadata(
|
170
|
-
name=getattr(module, "__plugin_name__", plugin_name),
|
171
|
-
version=getattr(module, "__plugin_version__", "1.0.0"),
|
172
|
-
description=getattr(
|
173
|
-
module,
|
174
|
-
"__plugin_description__",
|
175
|
-
f"Package plugin: {plugin_name}",
|
176
|
-
),
|
177
|
-
author=getattr(module, "__plugin_author__", "Unknown"),
|
178
|
-
license=getattr(module, "__plugin_license__", "MIT"),
|
179
|
-
)
|
180
|
-
|
181
|
-
def get_tools(self):
|
182
|
-
return getattr(module, "__plugin_tools__", [])
|
183
|
-
|
184
|
-
def initialize(self):
|
185
|
-
if hasattr(module, "initialize"):
|
186
|
-
module.initialize()
|
187
|
-
|
188
|
-
def cleanup(self):
|
189
|
-
if hasattr(module, "cleanup"):
|
190
|
-
module.cleanup()
|
191
|
-
|
192
|
-
return PackagePlugin()
|
193
|
-
|
194
|
-
except Exception as e:
|
195
|
-
logger.error(f"Failed to load plugin from file {file_path}: {e}")
|
196
|
-
|
197
|
-
return None
|
198
|
-
|
199
|
-
|
200
|
-
def _load_plugin_from_package(package_name: str) -> Optional[Plugin]:
|
201
|
-
"""Load a plugin from an installed package."""
|
202
|
-
try:
|
203
|
-
module = importlib.import_module(package_name)
|
204
|
-
|
205
|
-
# Look for Plugin class
|
206
|
-
for attr_name in dir(module):
|
207
|
-
attr = getattr(module, attr_name)
|
208
|
-
if isinstance(attr, type) and issubclass(attr, Plugin) and attr != Plugin:
|
209
|
-
return attr()
|
210
|
-
|
211
|
-
except ImportError as e:
|
212
|
-
logger.debug(f"Could not import package {package_name}: {e}")
|
213
|
-
|
214
|
-
return None
|
215
|
-
|
216
|
-
|
217
|
-
def list_available_plugins(search_paths: List[Path] = None) -> List[str]:
|
218
|
-
"""
|
219
|
-
List all available plugins in search paths.
|
220
|
-
|
221
|
-
Scans for plugins in multiple formats:
|
222
|
-
- .py files (excluding __init__.py)
|
223
|
-
- Directories with __init__.py or plugin.py
|
224
|
-
- Package directories with plugin metadata (__plugin_name__)
|
225
|
-
- Any valid plugin structure in search paths
|
226
|
-
|
227
|
-
Args:
|
228
|
-
search_paths: List of directories to search for plugins
|
229
|
-
|
230
|
-
Returns:
|
231
|
-
List of plugin names found
|
232
|
-
"""
|
233
|
-
if search_paths is None:
|
234
|
-
search_paths = []
|
235
|
-
|
236
|
-
# Add default search paths
|
237
|
-
default_paths = [
|
238
|
-
Path.cwd() / "plugins",
|
239
|
-
Path.home() / ".janito" / "plugins",
|
240
|
-
Path(sys.prefix) / "share" / "janito" / "plugins",
|
241
|
-
]
|
242
|
-
|
243
|
-
all_paths = search_paths + default_paths
|
244
|
-
plugins = []
|
245
|
-
|
246
|
-
for base_path in all_paths:
|
247
|
-
if not base_path.exists():
|
248
|
-
continue
|
249
|
-
|
250
|
-
# Look for directories with __init__.py or plugin.py
|
251
|
-
for item in base_path.iterdir():
|
252
|
-
if item.is_dir():
|
253
|
-
# Check for package-based plugins (subdirectories with __init__.py)
|
254
|
-
if (item / "__init__.py").exists():
|
255
|
-
# Check subdirectories for plugin metadata
|
256
|
-
for subitem in item.iterdir():
|
257
|
-
if subitem.is_dir() and (subitem / "__init__.py").exists():
|
258
|
-
try:
|
259
|
-
import importlib.util
|
260
|
-
|
261
|
-
spec = importlib.util.spec_from_file_location(
|
262
|
-
f"{item.name}.{subitem.name}",
|
263
|
-
subitem / "__init__.py",
|
264
|
-
)
|
265
|
-
if spec and spec.loader:
|
266
|
-
module = importlib.util.module_from_spec(spec)
|
267
|
-
spec.loader.exec_module(module)
|
268
|
-
|
269
|
-
# Check for plugin metadata
|
270
|
-
if hasattr(module, "__plugin_name__"):
|
271
|
-
plugins.append(
|
272
|
-
getattr(module, "__plugin_name__")
|
273
|
-
)
|
274
|
-
except Exception:
|
275
|
-
pass
|
276
|
-
|
277
|
-
# Also check for plugin.py files
|
278
|
-
plugin_file = item / "plugin.py"
|
279
|
-
if plugin_file.exists():
|
280
|
-
plugins.append(item.name)
|
281
|
-
|
282
|
-
elif item.suffix == ".py" and item.stem != "__init__":
|
283
|
-
plugins.append(item.stem)
|
284
|
-
|
285
|
-
# Add builtin plugins
|
286
|
-
builtin_plugins = BuiltinPluginRegistry.list_builtin_plugins()
|
287
|
-
plugins.extend(builtin_plugins)
|
288
|
-
|
289
|
-
return sorted(set(plugins))
|
1
|
+
"""
|
2
|
+
Plugin discovery utilities.
|
3
|
+
|
4
|
+
Plugins can be provided in several formats:
|
5
|
+
|
6
|
+
1. Single Python file: A .py file containing a Plugin class
|
7
|
+
Example: plugins/my_plugin.py
|
8
|
+
|
9
|
+
2. Python package directory: A directory with __init__.py or plugin.py
|
10
|
+
Example: plugins/my_plugin/__init__.py
|
11
|
+
Example: plugins/my_plugin/plugin.py
|
12
|
+
|
13
|
+
3. Installed Python package: An installed package with a Plugin class
|
14
|
+
Example: uv pip install janito-plugin-example
|
15
|
+
|
16
|
+
4. ZIP file: A .zip file containing a Python package structure
|
17
|
+
Example: plugins/my_plugin.zip (containing package structure)
|
18
|
+
|
19
|
+
The plugin discovery system searches these locations in order:
|
20
|
+
- Current working directory/plugins/
|
21
|
+
- ~/.janito/plugins/
|
22
|
+
- Python installation share/janito/plugins/
|
23
|
+
- Any additional paths specified via configuration
|
24
|
+
"""
|
25
|
+
|
26
|
+
import os
|
27
|
+
import sys
|
28
|
+
import importlib
|
29
|
+
import importlib.util
|
30
|
+
from pathlib import Path
|
31
|
+
from typing import Optional, List
|
32
|
+
import logging
|
33
|
+
|
34
|
+
from .base import Plugin
|
35
|
+
from .builtin import load_builtin_plugin, BuiltinPluginRegistry
|
36
|
+
from .core_loader import load_core_plugin
|
37
|
+
|
38
|
+
logger = logging.getLogger(__name__)
|
39
|
+
|
40
|
+
|
41
|
+
def discover_plugins(
|
42
|
+
plugin_name: str, search_paths: List[Path] = None
|
43
|
+
) -> Optional[Plugin]:
|
44
|
+
"""
|
45
|
+
Discover and load a plugin by name.
|
46
|
+
|
47
|
+
Supports multiple plugin formats:
|
48
|
+
- Single .py files
|
49
|
+
- Python package directories
|
50
|
+
- Package-based plugins (e.g., core.filemanager)
|
51
|
+
- Installed Python packages
|
52
|
+
- ZIP files containing packages
|
53
|
+
|
54
|
+
Args:
|
55
|
+
plugin_name: Name of the plugin to discover
|
56
|
+
search_paths: List of directories to search for plugins
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Plugin instance if found, None otherwise
|
60
|
+
"""
|
61
|
+
if search_paths is None:
|
62
|
+
search_paths = []
|
63
|
+
|
64
|
+
# Add default search paths
|
65
|
+
default_paths = [
|
66
|
+
Path.cwd() / "plugins",
|
67
|
+
Path.home() / ".janito" / "plugins",
|
68
|
+
Path(sys.prefix) / "share" / "janito" / "plugins",
|
69
|
+
]
|
70
|
+
|
71
|
+
all_paths = search_paths + default_paths
|
72
|
+
|
73
|
+
# Handle package-based plugins (e.g., core.filemanager)
|
74
|
+
if "." in plugin_name:
|
75
|
+
parts = plugin_name.split(".")
|
76
|
+
if len(parts) == 2:
|
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
|
+
|
85
|
+
for base_path in all_paths:
|
86
|
+
package_path = base_path / package_name / submodule_name / "__init__.py"
|
87
|
+
if package_path.exists():
|
88
|
+
return _load_plugin_from_file(package_path, plugin_name=plugin_name)
|
89
|
+
|
90
|
+
plugin_path = base_path / package_name / submodule_name / "plugin.py"
|
91
|
+
if plugin_path.exists():
|
92
|
+
return _load_plugin_from_file(plugin_path, plugin_name=plugin_name)
|
93
|
+
|
94
|
+
# Try to find plugin in search paths
|
95
|
+
for base_path in all_paths:
|
96
|
+
plugin_path = base_path / plugin_name
|
97
|
+
if plugin_path.exists():
|
98
|
+
return _load_plugin_from_directory(plugin_path)
|
99
|
+
|
100
|
+
# Try as Python module
|
101
|
+
module_path = base_path / f"{plugin_name}.py"
|
102
|
+
if module_path.exists():
|
103
|
+
return _load_plugin_from_file(module_path)
|
104
|
+
|
105
|
+
# Check for builtin plugins
|
106
|
+
builtin_plugin = load_builtin_plugin(plugin_name)
|
107
|
+
if builtin_plugin:
|
108
|
+
return builtin_plugin
|
109
|
+
|
110
|
+
# Try importing as installed package
|
111
|
+
try:
|
112
|
+
return _load_plugin_from_package(plugin_name)
|
113
|
+
except ImportError:
|
114
|
+
pass
|
115
|
+
|
116
|
+
return None
|
117
|
+
|
118
|
+
|
119
|
+
def _load_plugin_from_directory(plugin_path: Path) -> Optional[Plugin]:
|
120
|
+
"""Load a plugin from a directory."""
|
121
|
+
try:
|
122
|
+
# Look for __init__.py or plugin.py
|
123
|
+
init_file = plugin_path / "__init__.py"
|
124
|
+
plugin_file = plugin_path / "plugin.py"
|
125
|
+
|
126
|
+
if init_file.exists():
|
127
|
+
return _load_plugin_from_file(init_file, plugin_name=plugin_path.name)
|
128
|
+
elif plugin_file.exists():
|
129
|
+
return _load_plugin_from_file(plugin_file, plugin_name=plugin_path.name)
|
130
|
+
|
131
|
+
except Exception as e:
|
132
|
+
logger.error(f"Failed to load plugin from directory {plugin_path}: {e}")
|
133
|
+
|
134
|
+
return None
|
135
|
+
|
136
|
+
|
137
|
+
def _load_plugin_from_file(
|
138
|
+
file_path: Path, plugin_name: str = None
|
139
|
+
) -> Optional[Plugin]:
|
140
|
+
"""Load a plugin from a Python file."""
|
141
|
+
try:
|
142
|
+
if plugin_name is None:
|
143
|
+
plugin_name = file_path.stem
|
144
|
+
|
145
|
+
spec = importlib.util.spec_from_file_location(plugin_name, file_path)
|
146
|
+
if spec is None or spec.loader is None:
|
147
|
+
return None
|
148
|
+
|
149
|
+
module = importlib.util.module_from_spec(spec)
|
150
|
+
spec.loader.exec_module(module)
|
151
|
+
|
152
|
+
# Look for Plugin class
|
153
|
+
for attr_name in dir(module):
|
154
|
+
attr = getattr(module, attr_name)
|
155
|
+
if isinstance(attr, type) and issubclass(attr, Plugin) and attr != Plugin:
|
156
|
+
return attr()
|
157
|
+
|
158
|
+
# Check for package-based plugin with __plugin_name__ metadata
|
159
|
+
if hasattr(module, "__plugin_name__"):
|
160
|
+
from janito.plugins.base import PluginMetadata
|
161
|
+
|
162
|
+
# Create a dynamic plugin class
|
163
|
+
class PackagePlugin(Plugin):
|
164
|
+
def __init__(self):
|
165
|
+
super().__init__()
|
166
|
+
self._module = module
|
167
|
+
|
168
|
+
def get_metadata(self) -> PluginMetadata:
|
169
|
+
return PluginMetadata(
|
170
|
+
name=getattr(module, "__plugin_name__", plugin_name),
|
171
|
+
version=getattr(module, "__plugin_version__", "1.0.0"),
|
172
|
+
description=getattr(
|
173
|
+
module,
|
174
|
+
"__plugin_description__",
|
175
|
+
f"Package plugin: {plugin_name}",
|
176
|
+
),
|
177
|
+
author=getattr(module, "__plugin_author__", "Unknown"),
|
178
|
+
license=getattr(module, "__plugin_license__", "MIT"),
|
179
|
+
)
|
180
|
+
|
181
|
+
def get_tools(self):
|
182
|
+
return getattr(module, "__plugin_tools__", [])
|
183
|
+
|
184
|
+
def initialize(self):
|
185
|
+
if hasattr(module, "initialize"):
|
186
|
+
module.initialize()
|
187
|
+
|
188
|
+
def cleanup(self):
|
189
|
+
if hasattr(module, "cleanup"):
|
190
|
+
module.cleanup()
|
191
|
+
|
192
|
+
return PackagePlugin()
|
193
|
+
|
194
|
+
except Exception as e:
|
195
|
+
logger.error(f"Failed to load plugin from file {file_path}: {e}")
|
196
|
+
|
197
|
+
return None
|
198
|
+
|
199
|
+
|
200
|
+
def _load_plugin_from_package(package_name: str) -> Optional[Plugin]:
|
201
|
+
"""Load a plugin from an installed package."""
|
202
|
+
try:
|
203
|
+
module = importlib.import_module(package_name)
|
204
|
+
|
205
|
+
# Look for Plugin class
|
206
|
+
for attr_name in dir(module):
|
207
|
+
attr = getattr(module, attr_name)
|
208
|
+
if isinstance(attr, type) and issubclass(attr, Plugin) and attr != Plugin:
|
209
|
+
return attr()
|
210
|
+
|
211
|
+
except ImportError as e:
|
212
|
+
logger.debug(f"Could not import package {package_name}: {e}")
|
213
|
+
|
214
|
+
return None
|
215
|
+
|
216
|
+
|
217
|
+
def list_available_plugins(search_paths: List[Path] = None) -> List[str]:
|
218
|
+
"""
|
219
|
+
List all available plugins in search paths.
|
220
|
+
|
221
|
+
Scans for plugins in multiple formats:
|
222
|
+
- .py files (excluding __init__.py)
|
223
|
+
- Directories with __init__.py or plugin.py
|
224
|
+
- Package directories with plugin metadata (__plugin_name__)
|
225
|
+
- Any valid plugin structure in search paths
|
226
|
+
|
227
|
+
Args:
|
228
|
+
search_paths: List of directories to search for plugins
|
229
|
+
|
230
|
+
Returns:
|
231
|
+
List of plugin names found
|
232
|
+
"""
|
233
|
+
if search_paths is None:
|
234
|
+
search_paths = []
|
235
|
+
|
236
|
+
# Add default search paths
|
237
|
+
default_paths = [
|
238
|
+
Path.cwd() / "plugins",
|
239
|
+
Path.home() / ".janito" / "plugins",
|
240
|
+
Path(sys.prefix) / "share" / "janito" / "plugins",
|
241
|
+
]
|
242
|
+
|
243
|
+
all_paths = search_paths + default_paths
|
244
|
+
plugins = []
|
245
|
+
|
246
|
+
for base_path in all_paths:
|
247
|
+
if not base_path.exists():
|
248
|
+
continue
|
249
|
+
|
250
|
+
# Look for directories with __init__.py or plugin.py
|
251
|
+
for item in base_path.iterdir():
|
252
|
+
if item.is_dir():
|
253
|
+
# Check for package-based plugins (subdirectories with __init__.py)
|
254
|
+
if (item / "__init__.py").exists():
|
255
|
+
# Check subdirectories for plugin metadata
|
256
|
+
for subitem in item.iterdir():
|
257
|
+
if subitem.is_dir() and (subitem / "__init__.py").exists():
|
258
|
+
try:
|
259
|
+
import importlib.util
|
260
|
+
|
261
|
+
spec = importlib.util.spec_from_file_location(
|
262
|
+
f"{item.name}.{subitem.name}",
|
263
|
+
subitem / "__init__.py",
|
264
|
+
)
|
265
|
+
if spec and spec.loader:
|
266
|
+
module = importlib.util.module_from_spec(spec)
|
267
|
+
spec.loader.exec_module(module)
|
268
|
+
|
269
|
+
# Check for plugin metadata
|
270
|
+
if hasattr(module, "__plugin_name__"):
|
271
|
+
plugins.append(
|
272
|
+
getattr(module, "__plugin_name__")
|
273
|
+
)
|
274
|
+
except Exception:
|
275
|
+
pass
|
276
|
+
|
277
|
+
# Also check for plugin.py files
|
278
|
+
plugin_file = item / "plugin.py"
|
279
|
+
if plugin_file.exists():
|
280
|
+
plugins.append(item.name)
|
281
|
+
|
282
|
+
elif item.suffix == ".py" and item.stem != "__init__":
|
283
|
+
plugins.append(item.stem)
|
284
|
+
|
285
|
+
# Add builtin plugins
|
286
|
+
builtin_plugins = BuiltinPluginRegistry.list_builtin_plugins()
|
287
|
+
plugins.extend(builtin_plugins)
|
288
|
+
|
289
|
+
return sorted(set(plugins))
|