codemaster-cli 2.2.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 (170) hide show
  1. codemaster_cli-2.2.0.dist-info/METADATA +645 -0
  2. codemaster_cli-2.2.0.dist-info/RECORD +170 -0
  3. codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
  4. codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +188 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +163 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +165 -0
  81. vibe/core/agents/models.py +122 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +673 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +32 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/project_context.md +8 -0
  120. vibe/core/prompts/tests.md +1 -0
  121. vibe/core/proxy_setup.py +65 -0
  122. vibe/core/session/session_loader.py +222 -0
  123. vibe/core/session/session_logger.py +318 -0
  124. vibe/core/session/session_migration.py +41 -0
  125. vibe/core/skills/__init__.py +7 -0
  126. vibe/core/skills/manager.py +132 -0
  127. vibe/core/skills/models.py +92 -0
  128. vibe/core/skills/parser.py +39 -0
  129. vibe/core/system_prompt.py +466 -0
  130. vibe/core/telemetry/__init__.py +0 -0
  131. vibe/core/telemetry/send.py +185 -0
  132. vibe/core/teleport/errors.py +9 -0
  133. vibe/core/teleport/git.py +196 -0
  134. vibe/core/teleport/nuage.py +180 -0
  135. vibe/core/teleport/teleport.py +208 -0
  136. vibe/core/teleport/types.py +54 -0
  137. vibe/core/tools/base.py +336 -0
  138. vibe/core/tools/builtins/ask_user_question.py +134 -0
  139. vibe/core/tools/builtins/bash.py +357 -0
  140. vibe/core/tools/builtins/grep.py +310 -0
  141. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  142. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  143. vibe/core/tools/builtins/prompts/bash.md +73 -0
  144. vibe/core/tools/builtins/prompts/grep.md +4 -0
  145. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  146. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  147. vibe/core/tools/builtins/prompts/task.md +24 -0
  148. vibe/core/tools/builtins/prompts/todo.md +199 -0
  149. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  150. vibe/core/tools/builtins/read_file.py +222 -0
  151. vibe/core/tools/builtins/search_replace.py +456 -0
  152. vibe/core/tools/builtins/task.py +154 -0
  153. vibe/core/tools/builtins/todo.py +134 -0
  154. vibe/core/tools/builtins/write_file.py +160 -0
  155. vibe/core/tools/manager.py +341 -0
  156. vibe/core/tools/mcp.py +397 -0
  157. vibe/core/tools/ui.py +68 -0
  158. vibe/core/trusted_folders.py +86 -0
  159. vibe/core/types.py +405 -0
  160. vibe/core/utils.py +396 -0
  161. vibe/setup/onboarding/__init__.py +39 -0
  162. vibe/setup/onboarding/base.py +14 -0
  163. vibe/setup/onboarding/onboarding.tcss +134 -0
  164. vibe/setup/onboarding/screens/__init__.py +5 -0
  165. vibe/setup/onboarding/screens/api_key.py +200 -0
  166. vibe/setup/onboarding/screens/provider_selection.py +87 -0
  167. vibe/setup/onboarding/screens/welcome.py +136 -0
  168. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  169. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  170. vibe/whats_new.md +5 -0
@@ -0,0 +1,341 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterator
4
+ import hashlib
5
+ import importlib.util
6
+ import inspect
7
+ from logging import getLogger
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from vibe.core.paths.config_paths import resolve_local_tools_dir
14
+ from vibe.core.paths.global_paths import DEFAULT_TOOL_DIR, GLOBAL_TOOLS_DIR
15
+ from vibe.core.tools.base import BaseTool, BaseToolConfig
16
+ from vibe.core.tools.mcp import (
17
+ RemoteTool,
18
+ create_mcp_http_proxy_tool_class,
19
+ create_mcp_stdio_proxy_tool_class,
20
+ list_tools_http,
21
+ list_tools_stdio,
22
+ )
23
+ from vibe.core.utils import name_matches, run_sync
24
+
25
+ logger = getLogger("vibe")
26
+
27
+ if TYPE_CHECKING:
28
+ from vibe.core.config import MCPHttp, MCPStdio, MCPStreamableHttp, VibeConfig
29
+
30
+
31
+ def _try_canonical_module_name(path: Path) -> str | None:
32
+ """Extract canonical module name for vibe package files.
33
+
34
+ Prevents Pydantic class identity mismatches when the same module
35
+ is imported via dynamic discovery and regular imports.
36
+ """
37
+ try:
38
+ parts = path.resolve().parts
39
+ except (OSError, ValueError):
40
+ return None
41
+
42
+ try:
43
+ vibe_idx = parts.index("vibe")
44
+ except ValueError:
45
+ return None
46
+
47
+ if vibe_idx + 1 >= len(parts):
48
+ return None
49
+
50
+ module_parts = [p.removesuffix(".py") for p in parts[vibe_idx:]]
51
+ return ".".join(module_parts)
52
+
53
+
54
+ def _compute_module_name(path: Path) -> str:
55
+ """Return canonical module name for vibe files, hash-based synthetic name otherwise."""
56
+ if canonical := _try_canonical_module_name(path):
57
+ return canonical
58
+
59
+ resolved = path.resolve()
60
+ path_hash = hashlib.md5(str(resolved).encode()).hexdigest()[:8]
61
+ stem = re.sub(r"[^0-9A-Za-z_]", "_", path.stem) or "mod"
62
+ return f"vibe_tools_discovered_{stem}_{path_hash}"
63
+
64
+
65
+ class NoSuchToolError(Exception):
66
+ """Exception raised when a tool is not found."""
67
+
68
+
69
+ class ToolManager:
70
+ """Manages tool discovery and instantiation for an Agent.
71
+
72
+ Discovers available tools from the provided search paths. Each Agent
73
+ should have its own ToolManager instance.
74
+ """
75
+
76
+ def __init__(self, config_getter: Callable[[], VibeConfig]) -> None:
77
+ self._config_getter = config_getter
78
+ self._instances: dict[str, BaseTool] = {}
79
+ self._search_paths: list[Path] = self._compute_search_paths(self._config)
80
+
81
+ self._available: dict[str, type[BaseTool]] = {
82
+ cls.get_name(): cls for cls in self._iter_tool_classes(self._search_paths)
83
+ }
84
+ self._integrate_mcp()
85
+
86
+ @property
87
+ def _config(self) -> VibeConfig:
88
+ return self._config_getter()
89
+
90
+ @staticmethod
91
+ def _compute_search_paths(config: VibeConfig) -> list[Path]:
92
+ paths: list[Path] = [DEFAULT_TOOL_DIR.path]
93
+
94
+ paths.extend(config.tool_paths)
95
+
96
+ if (tools_dir := resolve_local_tools_dir(Path.cwd())) is not None:
97
+ paths.append(tools_dir)
98
+
99
+ paths.append(GLOBAL_TOOLS_DIR.path)
100
+
101
+ unique: list[Path] = []
102
+ seen: set[Path] = set()
103
+ for p in paths:
104
+ rp = p.resolve()
105
+ if rp not in seen:
106
+ seen.add(rp)
107
+ unique.append(rp)
108
+ return unique
109
+
110
+ @staticmethod
111
+ def _iter_tool_classes(search_paths: list[Path]) -> Iterator[type[BaseTool]]:
112
+ """Iterate over all search_paths to find tool classes.
113
+
114
+ Note: if a search path is not a directory, it is treated as a single tool file.
115
+ """
116
+ for base in search_paths:
117
+ if not base.is_dir() and base.name.endswith(".py"):
118
+ if tools := ToolManager._load_tools_from_file(base):
119
+ for tool in tools:
120
+ yield tool
121
+
122
+ for path in base.rglob("*.py"):
123
+ if tools := ToolManager._load_tools_from_file(path):
124
+ for tool in tools:
125
+ yield tool
126
+
127
+ @staticmethod
128
+ def _load_tools_from_file(file_path: Path) -> list[type[BaseTool]] | None:
129
+ if not file_path.is_file():
130
+ return
131
+ name = file_path.name
132
+ if name.startswith("_"):
133
+ return
134
+
135
+ module_name = _compute_module_name(file_path)
136
+
137
+ if module_name in sys.modules:
138
+ module = sys.modules[module_name]
139
+ else:
140
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
141
+ if spec is None or spec.loader is None:
142
+ return
143
+ module = importlib.util.module_from_spec(spec)
144
+ sys.modules[module_name] = module
145
+ try:
146
+ spec.loader.exec_module(module)
147
+ except Exception:
148
+ return
149
+
150
+ tools = []
151
+ for tool_obj in vars(module).values():
152
+ if not inspect.isclass(tool_obj):
153
+ continue
154
+ if not issubclass(tool_obj, BaseTool) or tool_obj is BaseTool:
155
+ continue
156
+ if inspect.isabstract(tool_obj):
157
+ continue
158
+ tools.append(tool_obj)
159
+ return tools
160
+
161
+ @staticmethod
162
+ def discover_tool_defaults(
163
+ search_paths: list[Path] | None = None,
164
+ ) -> dict[str, dict[str, Any]]:
165
+ if search_paths is None:
166
+ search_paths = [DEFAULT_TOOL_DIR.path]
167
+
168
+ defaults: dict[str, dict[str, Any]] = {}
169
+ for cls in ToolManager._iter_tool_classes(search_paths):
170
+ try:
171
+ tool_name = cls.get_name()
172
+ config_class = cls._get_tool_config_class()
173
+ defaults[tool_name] = config_class().model_dump(exclude_none=True)
174
+ except Exception as e:
175
+ logger.warning(
176
+ "Failed to get defaults for tool %s: %s", cls.__name__, e
177
+ )
178
+ continue
179
+ return defaults
180
+
181
+ @property
182
+ def available_tools(self) -> dict[str, type[BaseTool]]:
183
+ if self._config.enabled_tools:
184
+ return {
185
+ name: cls
186
+ for name, cls in self._available.items()
187
+ if name_matches(name, self._config.enabled_tools)
188
+ }
189
+ if self._config.disabled_tools:
190
+ return {
191
+ name: cls
192
+ for name, cls in self._available.items()
193
+ if not name_matches(name, self._config.disabled_tools)
194
+ }
195
+ return dict(self._available)
196
+
197
+ def _integrate_mcp(self) -> None:
198
+ if not self._config.mcp_servers:
199
+ return
200
+ run_sync(self._integrate_mcp_async())
201
+
202
+ async def _integrate_mcp_async(self) -> None:
203
+ try:
204
+ http_count = 0
205
+ stdio_count = 0
206
+
207
+ for srv in self._config.mcp_servers:
208
+ match srv.transport:
209
+ case "http" | "streamable-http":
210
+ http_count += await self._register_http_server(srv)
211
+ case "stdio":
212
+ stdio_count += await self._register_stdio_server(srv)
213
+ case _:
214
+ logger.warning("Unsupported MCP transport: %r", srv.transport)
215
+
216
+ logger.info(
217
+ "MCP integration registered %d tools (http=%d, stdio=%d)",
218
+ http_count + stdio_count,
219
+ http_count,
220
+ stdio_count,
221
+ )
222
+ except Exception as exc:
223
+ logger.warning("Failed to integrate MCP tools: %s", exc)
224
+
225
+ async def _register_http_server(self, srv: MCPHttp | MCPStreamableHttp) -> int:
226
+ url = (srv.url or "").strip()
227
+ if not url:
228
+ logger.warning("MCP server '%s' missing url for http transport", srv.name)
229
+ return 0
230
+
231
+ headers = srv.http_headers()
232
+ try:
233
+ tools: list[RemoteTool] = await list_tools_http(
234
+ url, headers=headers, startup_timeout_sec=srv.startup_timeout_sec
235
+ )
236
+ except Exception as exc:
237
+ logger.warning("MCP HTTP discovery failed for %s: %s", url, exc)
238
+ return 0
239
+
240
+ added = 0
241
+ for remote in tools:
242
+ try:
243
+ proxy_cls = create_mcp_http_proxy_tool_class(
244
+ url=url,
245
+ remote=remote,
246
+ alias=srv.name,
247
+ server_hint=srv.prompt,
248
+ headers=headers,
249
+ startup_timeout_sec=srv.startup_timeout_sec,
250
+ tool_timeout_sec=srv.tool_timeout_sec,
251
+ )
252
+ self._available[proxy_cls.get_name()] = proxy_cls
253
+ added += 1
254
+ except Exception as exc:
255
+ logger.warning(
256
+ "Failed to register MCP HTTP tool '%s' from %s: %r",
257
+ getattr(remote, "name", "<unknown>"),
258
+ url,
259
+ exc,
260
+ )
261
+ return added
262
+
263
+ async def _register_stdio_server(self, srv: MCPStdio) -> int:
264
+ cmd = srv.argv()
265
+ if not cmd:
266
+ logger.warning("MCP stdio server '%s' has invalid/empty command", srv.name)
267
+ return 0
268
+
269
+ try:
270
+ tools: list[RemoteTool] = await list_tools_stdio(
271
+ cmd, env=srv.env or None, startup_timeout_sec=srv.startup_timeout_sec
272
+ )
273
+ except Exception as exc:
274
+ logger.warning("MCP stdio discovery failed for %r: %s", cmd, exc)
275
+ return 0
276
+
277
+ added = 0
278
+ for remote in tools:
279
+ try:
280
+ proxy_cls = create_mcp_stdio_proxy_tool_class(
281
+ command=cmd,
282
+ remote=remote,
283
+ alias=srv.name,
284
+ server_hint=srv.prompt,
285
+ env=srv.env or None,
286
+ startup_timeout_sec=srv.startup_timeout_sec,
287
+ tool_timeout_sec=srv.tool_timeout_sec,
288
+ )
289
+ self._available[proxy_cls.get_name()] = proxy_cls
290
+ added += 1
291
+ except Exception as exc:
292
+ logger.warning(
293
+ "Failed to register MCP stdio tool '%s' from %r: %r",
294
+ getattr(remote, "name", "<unknown>"),
295
+ cmd,
296
+ exc,
297
+ )
298
+ return added
299
+
300
+ def get_tool_config(self, tool_name: str) -> BaseToolConfig:
301
+ tool_class = self._available.get(tool_name)
302
+
303
+ if tool_class:
304
+ config_class = tool_class._get_tool_config_class()
305
+ default_config = config_class()
306
+ else:
307
+ config_class = BaseToolConfig
308
+ default_config = BaseToolConfig()
309
+
310
+ user_overrides = self._config.tools.get(tool_name)
311
+ if user_overrides is None:
312
+ merged_dict = default_config.model_dump()
313
+ else:
314
+ merged_dict = {**default_config.model_dump(), **user_overrides.model_dump()}
315
+
316
+ return config_class.model_validate(merged_dict)
317
+
318
+ def get(self, tool_name: str) -> BaseTool:
319
+ """Get a tool instance, creating it lazily on first call.
320
+
321
+ Raises:
322
+ NoSuchToolError: If the requested tool is not available.
323
+ """
324
+ if tool_name in self._instances:
325
+ return self._instances[tool_name]
326
+
327
+ if tool_name not in self._available:
328
+ raise NoSuchToolError(
329
+ f"Unknown tool: {tool_name}. Available: {list(self._available.keys())}"
330
+ )
331
+
332
+ tool_class = self._available[tool_name]
333
+ tool_config = self.get_tool_config(tool_name)
334
+ self._instances[tool_name] = tool_class.from_config(tool_config)
335
+ return self._instances[tool_name]
336
+
337
+ def reset_all(self) -> None:
338
+ self._instances.clear()
339
+
340
+ def invalidate_tool(self, tool_name: str) -> None:
341
+ self._instances.pop(tool_name, None)