musigate 0.1.0a2__tar.gz

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 (39) hide show
  1. musigate-0.1.0a2/LICENSE +21 -0
  2. musigate-0.1.0a2/PKG-INFO +233 -0
  3. musigate-0.1.0a2/README.md +193 -0
  4. musigate-0.1.0a2/pyproject.toml +72 -0
  5. musigate-0.1.0a2/setup.cfg +4 -0
  6. musigate-0.1.0a2/src/musigate/__init__.py +1 -0
  7. musigate-0.1.0a2/src/musigate/adapters/__init__.py +0 -0
  8. musigate-0.1.0a2/src/musigate/adapters/loader.py +149 -0
  9. musigate-0.1.0a2/src/musigate/cli.py +442 -0
  10. musigate-0.1.0a2/src/musigate/gateway/__init__.py +0 -0
  11. musigate-0.1.0a2/src/musigate/gateway/engine.py +47 -0
  12. musigate-0.1.0a2/src/musigate/gateway/executor.py +186 -0
  13. musigate-0.1.0a2/src/musigate/gateway/selector.py +63 -0
  14. musigate-0.1.0a2/src/musigate/resources/__init__.py +1 -0
  15. musigate-0.1.0a2/src/musigate/resources/bots/music163.yaml +40 -0
  16. musigate-0.1.0a2/src/musigate/resources/bots/music_v1.yaml +40 -0
  17. musigate-0.1.0a2/src/musigate/resources/config/settings.yaml +20 -0
  18. musigate-0.1.0a2/src/musigate/telegram/__init__.py +0 -0
  19. musigate-0.1.0a2/src/musigate/telegram/auth.py +95 -0
  20. musigate-0.1.0a2/src/musigate/telegram/client.py +32 -0
  21. musigate-0.1.0a2/src/musigate/telegram/listener.py +204 -0
  22. musigate-0.1.0a2/src/musigate/utils/__init__.py +0 -0
  23. musigate-0.1.0a2/src/musigate/utils/config.py +162 -0
  24. musigate-0.1.0a2/src/musigate/utils/downloader.py +126 -0
  25. musigate-0.1.0a2/src/musigate/utils/helper.py +38 -0
  26. musigate-0.1.0a2/src/musigate.egg-info/PKG-INFO +233 -0
  27. musigate-0.1.0a2/src/musigate.egg-info/SOURCES.txt +37 -0
  28. musigate-0.1.0a2/src/musigate.egg-info/dependency_links.txt +1 -0
  29. musigate-0.1.0a2/src/musigate.egg-info/entry_points.txt +2 -0
  30. musigate-0.1.0a2/src/musigate.egg-info/requires.txt +12 -0
  31. musigate-0.1.0a2/src/musigate.egg-info/top_level.txt +1 -0
  32. musigate-0.1.0a2/tests/test_cli.py +152 -0
  33. musigate-0.1.0a2/tests/test_config.py +31 -0
  34. musigate-0.1.0a2/tests/test_downloader.py +51 -0
  35. musigate-0.1.0a2/tests/test_engine.py +283 -0
  36. musigate-0.1.0a2/tests/test_listener.py +142 -0
  37. musigate-0.1.0a2/tests/test_loader.py +47 -0
  38. musigate-0.1.0a2/tests/test_selector.py +38 -0
  39. musigate-0.1.0a2/tests/test_telegram_client.py +40 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 musigate contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: musigate
3
+ Version: 0.1.0a2
4
+ Summary: YAML-driven Telegram Bot orchestration engine for automated interactions
5
+ Author: musigate contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/opxqo/musigate
8
+ Project-URL: Repository, https://github.com/opxqo/musigate
9
+ Project-URL: Issues, https://github.com/opxqo/musigate/issues
10
+ Keywords: telegram,telegram-bot,music-download,yaml,cli,bot-automation,orchestration
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Communications :: Chat
23
+ Classifier: Topic :: Multimedia :: Sound/Audio
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: telethon>=1.33.0
29
+ Requires-Dist: PySocks>=1.7.1
30
+ Requires-Dist: typer>=0.9.0
31
+ Requires-Dist: pyyaml>=6.0.1
32
+ Requires-Dist: rich>=13.7.0
33
+ Requires-Dist: aiofiles>=23.2.1
34
+ Requires-Dist: python-dotenv>=1.0.0
35
+ Requires-Dist: pydantic>=2.5.3
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=7.4.3; extra == "dev"
38
+ Requires-Dist: pytest-asyncio>=0.23.2; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # musigate
42
+
43
+ [English](#english) | [中文](#中文)
44
+
45
+ ## English
46
+
47
+ `musigate` is a Python CLI for searching and downloading music through Telegram bots with a YAML-driven adapter system.
48
+
49
+ This release is an alpha build. The core CLI flow is working, but bot behavior still depends on upstream Telegram bots and may change over time.
50
+
51
+ ### Install
52
+
53
+ From a published package:
54
+
55
+ ```bash
56
+ pip install musigate
57
+ ```
58
+
59
+ From a local wheel:
60
+
61
+ ```bash
62
+ pip install dist/musigate-0.1.0a2-py3-none-any.whl
63
+ ```
64
+
65
+ For development:
66
+
67
+ ```bash
68
+ pip install -e .[dev]
69
+ ```
70
+
71
+ ### Setup
72
+
73
+ Copy `.env.example` to `.env`, then fill in your Telegram API credentials:
74
+
75
+ ```bash
76
+ cp .env.example .env
77
+ ```
78
+
79
+ Required values:
80
+
81
+ - `TELEGRAM_API_ID`
82
+ - `TELEGRAM_API_HASH`
83
+
84
+ ### Built-in Bots
85
+
86
+ - `music163`
87
+ - `music_v1`
88
+
89
+ ### Agent Skills
90
+
91
+ The repository now includes a local [skills](skills) directory for agent ecosystems such as Codex or OpenClaw.
92
+
93
+ Current skills:
94
+
95
+ - [skills/musigate-cli](skills/musigate-cli)
96
+ - [skills/add-bot-adapter](skills/add-bot-adapter)
97
+
98
+ ### Usage
99
+
100
+ Login to Telegram:
101
+
102
+ ```bash
103
+ musigate login
104
+ ```
105
+
106
+ Search only:
107
+
108
+ ```bash
109
+ musigate search "Numb" --bot music163
110
+ ```
111
+
112
+ Download with smart matching:
113
+
114
+ ```bash
115
+ musigate download "Numb" --bot music163
116
+ ```
117
+
118
+ Download a specific numbered search result:
119
+
120
+ ```bash
121
+ musigate download "海底" --bot music_v1 --pick 2
122
+ ```
123
+
124
+ Write files to a custom directory:
125
+
126
+ ```bash
127
+ musigate download "海底" --bot music_v1 --pick 2 --output ./downloads/favorites
128
+ ```
129
+
130
+ Machine-readable JSON output:
131
+
132
+ ```bash
133
+ musigate search "Numb" --bot music163 --json
134
+ musigate download "Numb" --bot music163 --json
135
+ musigate list-bots --json
136
+ ```
137
+
138
+ Normal CLI downloads show periodic progress updates including speed and ETA.
139
+
140
+ ## 中文
141
+
142
+ `musigate` 是一个 Python 命令行工具,用来通过 Telegram 机器人搜索和下载音乐,核心采用 YAML 驱动的适配器架构。
143
+
144
+ 当前版本是 alpha 初版。CLI 主流程已经可用,但具体机器人的行为仍依赖上游 Telegram 机器人,后续可能会变化。
145
+
146
+ ### 安装
147
+
148
+ 从已发布的包安装:
149
+
150
+ ```bash
151
+ pip install musigate
152
+ ```
153
+
154
+ 从本地 wheel 安装:
155
+
156
+ ```bash
157
+ pip install dist/musigate-0.1.0a2-py3-none-any.whl
158
+ ```
159
+
160
+ 开发模式安装:
161
+
162
+ ```bash
163
+ pip install -e .[dev]
164
+ ```
165
+
166
+ ### 配置
167
+
168
+ 把 `.env.example` 复制为 `.env`,然后填入你的 Telegram API 凭据:
169
+
170
+ ```bash
171
+ cp .env.example .env
172
+ ```
173
+
174
+ 必填项:
175
+
176
+ - `TELEGRAM_API_ID`
177
+ - `TELEGRAM_API_HASH`
178
+
179
+ ### 内置 Bot
180
+
181
+ - `music163`
182
+ - `music_v1`
183
+
184
+ ### 智能体 Skills
185
+
186
+ 仓库根目录新增了一个本地 [skills](skills) 目录,方便 Codex、OpenClaw 这类智能体快速接入。
187
+
188
+ 当前提供:
189
+
190
+ - [skills/musigate-cli](skills/musigate-cli)
191
+ - [skills/add-bot-adapter](skills/add-bot-adapter)
192
+
193
+ ### 使用方式
194
+
195
+ 登录 Telegram:
196
+
197
+ ```bash
198
+ musigate login
199
+ ```
200
+
201
+ 仅搜索,不下载:
202
+
203
+ ```bash
204
+ musigate search "Numb" --bot music163
205
+ ```
206
+
207
+ 按智能匹配下载:
208
+
209
+ ```bash
210
+ musigate download "Numb" --bot music163
211
+ ```
212
+
213
+ 按搜索结果编号精确下载:
214
+
215
+ ```bash
216
+ musigate download "海底" --bot music_v1 --pick 2
217
+ ```
218
+
219
+ 下载到自定义目录:
220
+
221
+ ```bash
222
+ musigate download "海底" --bot music_v1 --pick 2 --output ./downloads/favorites
223
+ ```
224
+
225
+ 输出机器可读 JSON:
226
+
227
+ ```bash
228
+ musigate search "Numb" --bot music163 --json
229
+ musigate download "Numb" --bot music163 --json
230
+ musigate list-bots --json
231
+ ```
232
+
233
+ 普通 CLI 下载模式会定期显示进度、速度和预计剩余时间。
@@ -0,0 +1,193 @@
1
+ # musigate
2
+
3
+ [English](#english) | [中文](#中文)
4
+
5
+ ## English
6
+
7
+ `musigate` is a Python CLI for searching and downloading music through Telegram bots with a YAML-driven adapter system.
8
+
9
+ This release is an alpha build. The core CLI flow is working, but bot behavior still depends on upstream Telegram bots and may change over time.
10
+
11
+ ### Install
12
+
13
+ From a published package:
14
+
15
+ ```bash
16
+ pip install musigate
17
+ ```
18
+
19
+ From a local wheel:
20
+
21
+ ```bash
22
+ pip install dist/musigate-0.1.0a2-py3-none-any.whl
23
+ ```
24
+
25
+ For development:
26
+
27
+ ```bash
28
+ pip install -e .[dev]
29
+ ```
30
+
31
+ ### Setup
32
+
33
+ Copy `.env.example` to `.env`, then fill in your Telegram API credentials:
34
+
35
+ ```bash
36
+ cp .env.example .env
37
+ ```
38
+
39
+ Required values:
40
+
41
+ - `TELEGRAM_API_ID`
42
+ - `TELEGRAM_API_HASH`
43
+
44
+ ### Built-in Bots
45
+
46
+ - `music163`
47
+ - `music_v1`
48
+
49
+ ### Agent Skills
50
+
51
+ The repository now includes a local [skills](skills) directory for agent ecosystems such as Codex or OpenClaw.
52
+
53
+ Current skills:
54
+
55
+ - [skills/musigate-cli](skills/musigate-cli)
56
+ - [skills/add-bot-adapter](skills/add-bot-adapter)
57
+
58
+ ### Usage
59
+
60
+ Login to Telegram:
61
+
62
+ ```bash
63
+ musigate login
64
+ ```
65
+
66
+ Search only:
67
+
68
+ ```bash
69
+ musigate search "Numb" --bot music163
70
+ ```
71
+
72
+ Download with smart matching:
73
+
74
+ ```bash
75
+ musigate download "Numb" --bot music163
76
+ ```
77
+
78
+ Download a specific numbered search result:
79
+
80
+ ```bash
81
+ musigate download "海底" --bot music_v1 --pick 2
82
+ ```
83
+
84
+ Write files to a custom directory:
85
+
86
+ ```bash
87
+ musigate download "海底" --bot music_v1 --pick 2 --output ./downloads/favorites
88
+ ```
89
+
90
+ Machine-readable JSON output:
91
+
92
+ ```bash
93
+ musigate search "Numb" --bot music163 --json
94
+ musigate download "Numb" --bot music163 --json
95
+ musigate list-bots --json
96
+ ```
97
+
98
+ Normal CLI downloads show periodic progress updates including speed and ETA.
99
+
100
+ ## 中文
101
+
102
+ `musigate` 是一个 Python 命令行工具,用来通过 Telegram 机器人搜索和下载音乐,核心采用 YAML 驱动的适配器架构。
103
+
104
+ 当前版本是 alpha 初版。CLI 主流程已经可用,但具体机器人的行为仍依赖上游 Telegram 机器人,后续可能会变化。
105
+
106
+ ### 安装
107
+
108
+ 从已发布的包安装:
109
+
110
+ ```bash
111
+ pip install musigate
112
+ ```
113
+
114
+ 从本地 wheel 安装:
115
+
116
+ ```bash
117
+ pip install dist/musigate-0.1.0a2-py3-none-any.whl
118
+ ```
119
+
120
+ 开发模式安装:
121
+
122
+ ```bash
123
+ pip install -e .[dev]
124
+ ```
125
+
126
+ ### 配置
127
+
128
+ 把 `.env.example` 复制为 `.env`,然后填入你的 Telegram API 凭据:
129
+
130
+ ```bash
131
+ cp .env.example .env
132
+ ```
133
+
134
+ 必填项:
135
+
136
+ - `TELEGRAM_API_ID`
137
+ - `TELEGRAM_API_HASH`
138
+
139
+ ### 内置 Bot
140
+
141
+ - `music163`
142
+ - `music_v1`
143
+
144
+ ### 智能体 Skills
145
+
146
+ 仓库根目录新增了一个本地 [skills](skills) 目录,方便 Codex、OpenClaw 这类智能体快速接入。
147
+
148
+ 当前提供:
149
+
150
+ - [skills/musigate-cli](skills/musigate-cli)
151
+ - [skills/add-bot-adapter](skills/add-bot-adapter)
152
+
153
+ ### 使用方式
154
+
155
+ 登录 Telegram:
156
+
157
+ ```bash
158
+ musigate login
159
+ ```
160
+
161
+ 仅搜索,不下载:
162
+
163
+ ```bash
164
+ musigate search "Numb" --bot music163
165
+ ```
166
+
167
+ 按智能匹配下载:
168
+
169
+ ```bash
170
+ musigate download "Numb" --bot music163
171
+ ```
172
+
173
+ 按搜索结果编号精确下载:
174
+
175
+ ```bash
176
+ musigate download "海底" --bot music_v1 --pick 2
177
+ ```
178
+
179
+ 下载到自定义目录:
180
+
181
+ ```bash
182
+ musigate download "海底" --bot music_v1 --pick 2 --output ./downloads/favorites
183
+ ```
184
+
185
+ 输出机器可读 JSON:
186
+
187
+ ```bash
188
+ musigate search "Numb" --bot music163 --json
189
+ musigate download "Numb" --bot music163 --json
190
+ musigate list-bots --json
191
+ ```
192
+
193
+ 普通 CLI 下载模式会定期显示进度、速度和预计剩余时间。
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "musigate"
7
+ dynamic = ["version"]
8
+ description = "YAML-driven Telegram Bot orchestration engine for automated interactions"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "musigate contributors" }
13
+ ]
14
+ requires-python = ">=3.10"
15
+ keywords = [
16
+ "telegram",
17
+ "telegram-bot",
18
+ "music-download",
19
+ "yaml",
20
+ "cli",
21
+ "bot-automation",
22
+ "orchestration",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: End Users/Desktop",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Topic :: Communications :: Chat",
37
+ "Topic :: Multimedia :: Sound/Audio",
38
+ "Typing :: Typed",
39
+ ]
40
+ dependencies = [
41
+ "telethon>=1.33.0",
42
+ "PySocks>=1.7.1",
43
+ "typer>=0.9.0",
44
+ "pyyaml>=6.0.1",
45
+ "rich>=13.7.0",
46
+ "aiofiles>=23.2.1",
47
+ "python-dotenv>=1.0.0",
48
+ "pydantic>=2.5.3"
49
+ ]
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/opxqo/musigate"
53
+ Repository = "https://github.com/opxqo/musigate"
54
+ Issues = "https://github.com/opxqo/musigate/issues"
55
+
56
+ [project.optional-dependencies]
57
+ dev = [
58
+ "pytest>=7.4.3",
59
+ "pytest-asyncio>=0.23.2"
60
+ ]
61
+
62
+ [project.scripts]
63
+ musigate = "musigate.cli:app"
64
+
65
+ [tool.setuptools.packages.find]
66
+ where = ["src"]
67
+
68
+ [tool.setuptools.dynamic]
69
+ version = { attr = "musigate.__version__" }
70
+
71
+ [tool.setuptools.package-data]
72
+ "musigate.resources" = ["bots/*.yaml", "config/*.yaml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0a2"
File without changes
@@ -0,0 +1,149 @@
1
+ from importlib import resources
2
+ from importlib.resources.abc import Traversable
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import yaml
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Step(BaseModel):
11
+ action: str
12
+ content: Optional[str] = None
13
+ expect: Optional[str] = None
14
+ timeout: Optional[int] = None
15
+ strategy: Optional[str] = None
16
+ query: Optional[str] = None
17
+ output: Optional[str] = None
18
+ cases: Optional[List[Dict[str, Any]]] = None
19
+ extract: Optional[Dict[str, str]] = None
20
+ message: Optional[str] = None
21
+
22
+
23
+ class CommandDef(BaseModel):
24
+ steps: List[Step]
25
+
26
+
27
+ class BotSettings(BaseModel):
28
+ timeout: int = 15
29
+ retry: int = 2
30
+
31
+
32
+ class BotConfig(BaseModel):
33
+ name: str
34
+ bot_username: str
35
+ description: str = ""
36
+ version: str = "1.0"
37
+ settings: BotSettings = Field(default_factory=BotSettings)
38
+ commands: Dict[str, CommandDef]
39
+
40
+
41
+ PROJECT_ROOT = Path(__file__).resolve().parents[3]
42
+ RESOURCE_PACKAGE = "musigate.resources"
43
+
44
+
45
+ def _bot_filename(bot_name: str) -> str:
46
+ return bot_name if bot_name.endswith(".yaml") else f"{bot_name}.yaml"
47
+
48
+
49
+ def _dedupe_paths(paths: list[Path]) -> list[Path]:
50
+ unique_paths: list[Path] = []
51
+ seen: set[str] = set()
52
+ for path in paths:
53
+ key = str(path.expanduser().resolve(strict=False)).lower()
54
+ if key in seen:
55
+ continue
56
+ seen.add(key)
57
+ unique_paths.append(path)
58
+ return unique_paths
59
+
60
+
61
+ def _external_bot_dirs() -> list[Path]:
62
+ return _dedupe_paths(
63
+ [
64
+ Path.cwd() / "bots",
65
+ PROJECT_ROOT / "bots",
66
+ ]
67
+ )
68
+
69
+
70
+ def _explicit_bot_candidates(bot_name: str) -> list[Path]:
71
+ candidate = Path(bot_name).expanduser()
72
+ candidates: list[Path] = []
73
+
74
+ if candidate.is_absolute() or candidate.parent != Path("."):
75
+ candidates.append(candidate if candidate.is_absolute() else (Path.cwd() / candidate))
76
+ elif candidate.suffix == ".yaml":
77
+ candidates.extend(
78
+ [
79
+ Path.cwd() / candidate,
80
+ PROJECT_ROOT / candidate,
81
+ ]
82
+ )
83
+
84
+ return _dedupe_paths(candidates)
85
+
86
+
87
+ def _packaged_bot_resource(filename: str) -> Traversable | None:
88
+ resource = resources.files(RESOURCE_PACKAGE).joinpath("bots", filename)
89
+ return resource if resource.is_file() else None
90
+
91
+
92
+ def _read_yaml(source: Path | Traversable) -> dict:
93
+ if isinstance(source, Path):
94
+ raw = source.read_text(encoding="utf-8")
95
+ else:
96
+ raw = source.read_text(encoding="utf-8")
97
+ return yaml.safe_load(raw) or {}
98
+
99
+
100
+ def _resolve_bot_source(bot_name: str) -> Path | Traversable:
101
+ for candidate in _explicit_bot_candidates(bot_name):
102
+ if candidate.is_file():
103
+ return candidate
104
+
105
+ filename = _bot_filename(bot_name)
106
+ for directory in _external_bot_dirs():
107
+ candidate = directory / filename
108
+ if candidate.is_file():
109
+ return candidate
110
+
111
+ packaged = _packaged_bot_resource(filename)
112
+ if packaged is not None:
113
+ return packaged
114
+
115
+ searched = [str(path) for path in _explicit_bot_candidates(bot_name)]
116
+ searched.extend(str(directory / filename) for directory in _external_bot_dirs())
117
+ raise FileNotFoundError(f"Bot config not found: {filename}. Searched: {', '.join(searched)}")
118
+
119
+
120
+ def load_bot(bot_name: str) -> dict:
121
+ """Load and validate a bot configuration."""
122
+ data = _read_yaml(_resolve_bot_source(bot_name))
123
+ config = BotConfig(**data)
124
+ return config.model_dump()
125
+
126
+
127
+ def list_bots() -> list[dict]:
128
+ configs: list[dict] = []
129
+ seen_names: set[str] = set()
130
+
131
+ for directory in _external_bot_dirs():
132
+ if not directory.exists():
133
+ continue
134
+ for bot_file in sorted(directory.glob("*.yaml")):
135
+ if bot_file.name == "template.yaml" or bot_file.name in seen_names:
136
+ continue
137
+ configs.append(load_bot(str(bot_file)))
138
+ seen_names.add(bot_file.name)
139
+
140
+ packaged_dir = resources.files(RESOURCE_PACKAGE).joinpath("bots")
141
+ for bot_file in sorted(packaged_dir.iterdir(), key=lambda item: item.name):
142
+ if not bot_file.is_file() or not bot_file.name.endswith(".yaml"):
143
+ continue
144
+ if bot_file.name == "template.yaml" or bot_file.name in seen_names:
145
+ continue
146
+ configs.append(load_bot(bot_file.name))
147
+ seen_names.add(bot_file.name)
148
+
149
+ return configs