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
vibe/core/config.py ADDED
@@ -0,0 +1,673 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from enum import StrEnum, auto
5
+ import os
6
+ from pathlib import Path
7
+ import re
8
+ import shlex
9
+ import tomllib
10
+ from typing import Annotated, Any, Literal
11
+
12
+ from dotenv import dotenv_values
13
+ from pydantic import BaseModel, Field, field_validator, model_validator
14
+ from pydantic.fields import FieldInfo
15
+ from pydantic_core import to_jsonable_python
16
+ from pydantic_settings import (
17
+ BaseSettings,
18
+ PydanticBaseSettingsSource,
19
+ SettingsConfigDict,
20
+ )
21
+ import tomli_w
22
+
23
+ from vibe.core.paths.config_paths import CONFIG_DIR, CONFIG_FILE, PROMPTS_DIR
24
+ from vibe.core.paths.global_paths import (
25
+ GLOBAL_ENV_FILE,
26
+ GLOBAL_PROMPTS_DIR,
27
+ SESSION_LOG_DIR,
28
+ )
29
+ from vibe.core.prompts import SystemPrompt
30
+ from vibe.core.tools.base import BaseToolConfig
31
+
32
+
33
+ def load_dotenv_values(
34
+ env_path: Path = GLOBAL_ENV_FILE.path,
35
+ environ: MutableMapping[str, str] = os.environ,
36
+ ) -> None:
37
+ # We allow FIFO path to support some environment management solutions (e.g. https://developer.1password.com/docs/environments/local-env-file/)
38
+ if not env_path.is_file() and not env_path.is_fifo():
39
+ return
40
+
41
+ env_vars = dotenv_values(env_path)
42
+ for key, value in env_vars.items():
43
+ if not value:
44
+ continue
45
+ environ.update({key: value})
46
+
47
+
48
+ class MissingAPIKeyError(RuntimeError):
49
+ def __init__(self, env_key: str, provider_name: str) -> None:
50
+ super().__init__(
51
+ f"Missing {env_key} environment variable for {provider_name} provider"
52
+ )
53
+ self.env_key = env_key
54
+ self.provider_name = provider_name
55
+
56
+
57
+ class MissingPromptFileError(RuntimeError):
58
+ def __init__(
59
+ self, system_prompt_id: str, prompt_dir: str, global_prompt_dir: str
60
+ ) -> None:
61
+ extra_global_prompt_dir = (
62
+ f" or {global_prompt_dir}" if global_prompt_dir != prompt_dir else ""
63
+ )
64
+
65
+ super().__init__(
66
+ f"Invalid system_prompt_id value: '{system_prompt_id}'. "
67
+ f"Must be one of the available prompts ({', '.join(f'{p.name.lower()}' for p in SystemPrompt)}), "
68
+ f"or correspond to a .md file in {prompt_dir}{extra_global_prompt_dir}"
69
+ )
70
+ self.system_prompt_id = system_prompt_id
71
+ self.prompt_dir = prompt_dir
72
+
73
+
74
+ class WrongBackendError(RuntimeError):
75
+ def __init__(self, backend: Backend, is_mistral_api: bool) -> None:
76
+ super().__init__(
77
+ f"Wrong backend '{backend}' for {'' if is_mistral_api else 'non-'}"
78
+ f"mistral API. Use '{Backend.MISTRAL}' for mistral API and '{Backend.GENERIC}' for others."
79
+ )
80
+ self.backend = backend
81
+ self.is_mistral_api = is_mistral_api
82
+
83
+
84
+ class TomlFileSettingsSource(PydanticBaseSettingsSource):
85
+ def __init__(self, settings_cls: type[BaseSettings]) -> None:
86
+ super().__init__(settings_cls)
87
+ self.toml_data = self._load_toml()
88
+
89
+ def _load_toml(self) -> dict[str, Any]:
90
+ file = CONFIG_FILE.path
91
+ try:
92
+ with file.open("rb") as f:
93
+ return tomllib.load(f)
94
+ except FileNotFoundError:
95
+ return {}
96
+ except tomllib.TOMLDecodeError as e:
97
+ raise RuntimeError(f"Invalid TOML in {file}: {e}") from e
98
+ except OSError as e:
99
+ raise RuntimeError(f"Cannot read {file}: {e}") from e
100
+
101
+ def get_field_value(
102
+ self, field: FieldInfo, field_name: str
103
+ ) -> tuple[Any, str, bool]:
104
+ return self.toml_data.get(field_name), field_name, False
105
+
106
+ def __call__(self) -> dict[str, Any]:
107
+ return self.toml_data
108
+
109
+
110
+ class ProjectContextConfig(BaseSettings):
111
+ max_chars: int = 40_000
112
+ default_commit_count: int = 5
113
+ max_doc_bytes: int = 32 * 1024
114
+ truncation_buffer: int = 1_000
115
+ max_depth: int = 3
116
+ max_files: int = 1000
117
+ max_dirs_per_level: int = 20
118
+ timeout_seconds: float = 2.0
119
+
120
+
121
+ class SessionLoggingConfig(BaseSettings):
122
+ save_dir: str = ""
123
+ session_prefix: str = "session"
124
+ enabled: bool = True
125
+
126
+ @field_validator("save_dir", mode="before")
127
+ @classmethod
128
+ def set_default_save_dir(cls, v: str) -> str:
129
+ if not v:
130
+ return str(SESSION_LOG_DIR.path)
131
+ return v
132
+
133
+ @field_validator("save_dir", mode="after")
134
+ @classmethod
135
+ def expand_save_dir(cls, v: str) -> str:
136
+ return str(Path(v).expanduser().resolve())
137
+
138
+
139
+ class Backend(StrEnum):
140
+ MISTRAL = auto()
141
+ GENERIC = auto()
142
+
143
+
144
+ class ProviderConfig(BaseModel):
145
+ name: str
146
+ api_base: str
147
+ api_key_env_var: str = ""
148
+ api_style: str = "openai"
149
+ backend: Backend = Backend.GENERIC
150
+ reasoning_field_name: str = "reasoning_content"
151
+ project_id: str = ""
152
+ region: str = ""
153
+
154
+
155
+ class _MCPBase(BaseModel):
156
+ name: str = Field(description="Short alias used to prefix tool names")
157
+ prompt: str | None = Field(
158
+ default=None, description="Optional usage hint appended to tool descriptions"
159
+ )
160
+ startup_timeout_sec: float = Field(
161
+ default=10.0,
162
+ gt=0,
163
+ description="Timeout in seconds for the server to start and initialize.",
164
+ )
165
+ tool_timeout_sec: float = Field(
166
+ default=60.0, gt=0, description="Timeout in seconds for tool execution."
167
+ )
168
+
169
+ @field_validator("name", mode="after")
170
+ @classmethod
171
+ def normalize_name(cls, v: str) -> str:
172
+ normalized = re.sub(r"[^a-zA-Z0-9_-]", "_", v)
173
+ normalized = normalized.strip("_-")
174
+ return normalized[:256]
175
+
176
+
177
+ class _MCPHttpFields(BaseModel):
178
+ url: str = Field(description="Base URL of the MCP HTTP server")
179
+ headers: dict[str, str] = Field(
180
+ default_factory=dict,
181
+ description=(
182
+ "Additional HTTP headers when using 'http' transport (e.g., Authorization or X-API-Key)."
183
+ ),
184
+ )
185
+ api_key_env: str = Field(
186
+ default="",
187
+ description=(
188
+ "Environment variable name containing an API token to send for HTTP transport."
189
+ ),
190
+ )
191
+ api_key_header: str = Field(
192
+ default="Authorization",
193
+ description=(
194
+ "HTTP header name to carry the token when 'api_key_env' is set (e.g., 'Authorization' or 'X-API-Key')."
195
+ ),
196
+ )
197
+ api_key_format: str = Field(
198
+ default="Bearer {token}",
199
+ description=(
200
+ "Format string for the header value when 'api_key_env' is set. Use '{token}' placeholder."
201
+ ),
202
+ )
203
+
204
+ def http_headers(self) -> dict[str, str]:
205
+ hdrs = dict(self.headers or {})
206
+ env_var = (self.api_key_env or "").strip()
207
+ if env_var and (token := os.getenv(env_var)):
208
+ target = (self.api_key_header or "").strip() or "Authorization"
209
+ if not any(h.lower() == target.lower() for h in hdrs):
210
+ try:
211
+ value = (self.api_key_format or "{token}").format(token=token)
212
+ except Exception:
213
+ value = token
214
+ hdrs[target] = value
215
+ return hdrs
216
+
217
+
218
+ class MCPHttp(_MCPBase, _MCPHttpFields):
219
+ transport: Literal["http"]
220
+
221
+
222
+ class MCPStreamableHttp(_MCPBase, _MCPHttpFields):
223
+ transport: Literal["streamable-http"]
224
+
225
+
226
+ class MCPStdio(_MCPBase):
227
+ transport: Literal["stdio"]
228
+ command: str | list[str]
229
+ args: list[str] = Field(default_factory=list)
230
+ env: dict[str, str] = Field(
231
+ default_factory=dict,
232
+ description="Environment variables to set for the MCP server process.",
233
+ )
234
+
235
+ def argv(self) -> list[str]:
236
+ base = (
237
+ shlex.split(self.command)
238
+ if isinstance(self.command, str)
239
+ else list(self.command or [])
240
+ )
241
+ return [*base, *self.args] if self.args else base
242
+
243
+
244
+ MCPServer = Annotated[
245
+ MCPHttp | MCPStreamableHttp | MCPStdio, Field(discriminator="transport")
246
+ ]
247
+
248
+
249
+ class ModelConfig(BaseModel):
250
+ name: str
251
+ provider: str
252
+ alias: str
253
+ temperature: float = 0.2
254
+ input_price: float = 0.0 # Price per million input tokens
255
+ output_price: float = 0.0 # Price per million output tokens
256
+ thinking: Literal["off", "low", "medium", "high"] = "off"
257
+
258
+ @model_validator(mode="before")
259
+ @classmethod
260
+ def _default_alias_to_name(cls, data: Any) -> Any:
261
+ if isinstance(data, dict):
262
+ if "alias" not in data or data["alias"] is None:
263
+ data["alias"] = data.get("name")
264
+ return data
265
+
266
+
267
+ DEFAULT_MISTRAL_API_ENV_KEY = "MISTRAL_API_KEY"
268
+
269
+
270
+ DEFAULT_PROVIDERS = [
271
+ ProviderConfig(
272
+ name="mistral",
273
+ api_base="https://api.mistral.ai/v1",
274
+ api_key_env_var=DEFAULT_MISTRAL_API_ENV_KEY,
275
+ backend=Backend.MISTRAL,
276
+ ),
277
+ ProviderConfig(
278
+ name="llamacpp",
279
+ api_base="http://127.0.0.1:8080/v1",
280
+ api_key_env_var="", # NOTE: if you wish to use --api-key in llama-server, change this value
281
+ ),
282
+ ProviderConfig(
283
+ name="ollama",
284
+ api_base="http://localhost:11434/v1",
285
+ api_key_env_var="", # No API key needed for local Ollama
286
+ api_style="openai",
287
+ backend=Backend.GENERIC,
288
+ ),
289
+ ]
290
+
291
+ DEFAULT_MODELS = [
292
+ ModelConfig(
293
+ name="mistral-vibe-cli-latest",
294
+ provider="mistral",
295
+ alias="devstral-2",
296
+ input_price=0.4,
297
+ output_price=2.0,
298
+ ),
299
+ ModelConfig(
300
+ name="devstral-small-latest",
301
+ provider="mistral",
302
+ alias="devstral-small",
303
+ input_price=0.1,
304
+ output_price=0.3,
305
+ ),
306
+ ModelConfig(
307
+ name="devstral",
308
+ provider="llamacpp",
309
+ alias="local",
310
+ input_price=0.0,
311
+ output_price=0.0,
312
+ ),
313
+
314
+ ModelConfig(
315
+ name="deepseek-coder:1.3b",
316
+ provider="ollama",
317
+ alias="deepseek-coder-1.3b",
318
+ input_price=0.0,
319
+ output_price=0.0,
320
+ ),
321
+ ModelConfig(
322
+ name="deepseek-coder:latest",
323
+ provider="ollama",
324
+ alias="deepseek-coder",
325
+ input_price=0.0,
326
+ output_price=0.0,
327
+ ),
328
+ ModelConfig(
329
+ name="codellama:latest",
330
+ provider="ollama",
331
+ alias="codellama",
332
+ input_price=0.0,
333
+ output_price=0.0,
334
+ ),
335
+ ModelConfig(
336
+ name="llama3.1:latest",
337
+ provider="ollama",
338
+ alias="llama3.1",
339
+ input_price=0.0,
340
+ output_price=0.0,
341
+ ),
342
+ ModelConfig(
343
+ name="mistral:latest",
344
+ provider="ollama",
345
+ alias="ollama-mistral",
346
+ input_price=0.0,
347
+ output_price=0.0,
348
+ ),
349
+ # ADD THIS NEW MODEL HERE ⬇️
350
+ ModelConfig(
351
+ name="qwen2.5-coder:7b",
352
+ provider="ollama",
353
+ alias="qwen2.5-coder",
354
+ temperature=0.2,
355
+ input_price=0.0,
356
+ output_price=0.0,
357
+ ),
358
+ ]
359
+
360
+
361
+ class VibeConfig(BaseSettings):
362
+ active_model: str = "devstral-2"
363
+ vim_keybindings: bool = False
364
+ disable_welcome_banner_animation: bool = False
365
+ autocopy_to_clipboard: bool = True
366
+ displayed_workdir: str = ""
367
+ auto_compact_threshold: int = 200_000
368
+ context_warnings: bool = False
369
+ auto_approve: bool = False
370
+ enable_telemetry: bool = True
371
+ system_prompt_id: str = "cli"
372
+ include_commit_signature: bool = True
373
+ include_model_info: bool = True
374
+ include_project_context: bool = True
375
+ include_prompt_detail: bool = True
376
+ enable_update_checks: bool = True
377
+ enable_auto_update: bool = True
378
+ api_timeout: float = 720.0
379
+
380
+ # TODO(vibe-nuage): remove exclude=True once the feature is publicly available
381
+ nuage_enabled: bool = Field(default=False, exclude=True)
382
+ nuage_base_url: str = Field(default="https://api.globalaegis.net", exclude=True)
383
+ nuage_workflow_id: str = Field(default="__shared-nuage-workflow", exclude=True)
384
+ # TODO(vibe-nuage): change default value to MISTRAL_API_KEY once prod has shared vibe-nuage workers
385
+ nuage_api_key_env_var: str = Field(default="STAGING_MISTRAL_API_KEY", exclude=True)
386
+
387
+ providers: list[ProviderConfig] = Field(
388
+ default_factory=lambda: list(DEFAULT_PROVIDERS)
389
+ )
390
+ models: list[ModelConfig] = Field(default_factory=lambda: list(DEFAULT_MODELS))
391
+
392
+ project_context: ProjectContextConfig = Field(default_factory=ProjectContextConfig)
393
+ session_logging: SessionLoggingConfig = Field(default_factory=SessionLoggingConfig)
394
+ tools: dict[str, BaseToolConfig] = Field(default_factory=dict)
395
+ tool_paths: list[Path] = Field(
396
+ default_factory=list,
397
+ description=(
398
+ "Additional directories or files to explore for custom tools. "
399
+ "Paths may be absolute or relative to the current working directory. "
400
+ "Directories are shallow-searched for tool definition files, "
401
+ "while files are loaded directly if valid."
402
+ ),
403
+ )
404
+
405
+ mcp_servers: list[MCPServer] = Field(
406
+ default_factory=list, description="Preferred MCP server configuration entries."
407
+ )
408
+
409
+ enabled_tools: list[str] = Field(
410
+ default_factory=list,
411
+ description=(
412
+ "An explicit list of tool names/patterns to enable. If set, only these"
413
+ " tools will be active. Supports glob patterns (e.g., 'serena_*') and"
414
+ " regex with 're:' prefix (e.g., 're:^serena_.*')."
415
+ ),
416
+ )
417
+ disabled_tools: list[str] = Field(
418
+ default_factory=list,
419
+ description=(
420
+ "A list of tool names/patterns to disable. Ignored if 'enabled_tools'"
421
+ " is set. Supports glob patterns and regex with 're:' prefix."
422
+ ),
423
+ )
424
+ agent_paths: list[Path] = Field(
425
+ default_factory=list,
426
+ description=(
427
+ "Additional directories to search for custom agent profiles. "
428
+ "Each path may be absolute or relative to the current working directory."
429
+ ),
430
+ )
431
+ enabled_agents: list[str] = Field(
432
+ default_factory=list,
433
+ description=(
434
+ "An explicit list of agent names/patterns to enable. If set, only these"
435
+ " agents will be available. Supports glob patterns (e.g., 'custom-*')"
436
+ " and regex with 're:' prefix."
437
+ ),
438
+ )
439
+ disabled_agents: list[str] = Field(
440
+ default_factory=list,
441
+ description=(
442
+ "A list of agent names/patterns to disable. Ignored if 'enabled_agents'"
443
+ " is set. Supports glob patterns and regex with 're:' prefix."
444
+ ),
445
+ )
446
+ skill_paths: list[Path] = Field(
447
+ default_factory=list,
448
+ description=(
449
+ "Additional directories to search for skills. "
450
+ "Each path may be absolute or relative to the current working directory."
451
+ ),
452
+ )
453
+ enabled_skills: list[str] = Field(
454
+ default_factory=list,
455
+ description=(
456
+ "An explicit list of skill names/patterns to enable. If set, only these"
457
+ " skills will be active. Supports glob patterns (e.g., 'search-*') and"
458
+ " regex with 're:' prefix."
459
+ ),
460
+ )
461
+ disabled_skills: list[str] = Field(
462
+ default_factory=list,
463
+ description=(
464
+ "A list of skill names/patterns to disable. Ignored if 'enabled_skills'"
465
+ " is set. Supports glob patterns and regex with 're:' prefix."
466
+ ),
467
+ )
468
+
469
+ model_config = SettingsConfigDict(
470
+ env_prefix="VIBE_", case_sensitive=False, extra="ignore"
471
+ )
472
+
473
+ @property
474
+ def nuage_api_key(self) -> str:
475
+ return os.getenv(self.nuage_api_key_env_var, "")
476
+
477
+ @property
478
+ def system_prompt(self) -> str:
479
+ try:
480
+ return SystemPrompt[self.system_prompt_id.upper()].read()
481
+ except KeyError:
482
+ pass
483
+
484
+ for current_prompt_dir in [PROMPTS_DIR.path, GLOBAL_PROMPTS_DIR.path]:
485
+ custom_sp_path = (current_prompt_dir / self.system_prompt_id).with_suffix(
486
+ ".md"
487
+ )
488
+ if custom_sp_path.is_file():
489
+ return custom_sp_path.read_text()
490
+
491
+ raise MissingPromptFileError(
492
+ self.system_prompt_id, str(PROMPTS_DIR.path), str(GLOBAL_PROMPTS_DIR.path)
493
+ )
494
+
495
+ def get_active_model(self) -> ModelConfig:
496
+ for model in self.models:
497
+ if model.alias == self.active_model:
498
+ return model
499
+ raise ValueError(
500
+ f"Active model '{self.active_model}' not found in configuration."
501
+ )
502
+
503
+ def get_provider_for_model(self, model: ModelConfig) -> ProviderConfig:
504
+ for provider in self.providers:
505
+ if provider.name == model.provider:
506
+ return provider
507
+ raise ValueError(
508
+ f"Provider '{model.provider}' for model '{model.name}' not found in configuration."
509
+ )
510
+
511
+ @classmethod
512
+ def settings_customise_sources(
513
+ cls,
514
+ settings_cls: type[BaseSettings],
515
+ init_settings: PydanticBaseSettingsSource,
516
+ env_settings: PydanticBaseSettingsSource,
517
+ dotenv_settings: PydanticBaseSettingsSource,
518
+ file_secret_settings: PydanticBaseSettingsSource,
519
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
520
+ """Define the priority of settings sources.
521
+
522
+ Note: dotenv_settings is intentionally excluded. API keys and other
523
+ non-config environment variables are stored in .env but loaded manually
524
+ into os.environ for use by providers. Only VIBE_* prefixed environment
525
+ variables (via env_settings) and TOML config are used for Pydantic settings.
526
+ """
527
+ return (
528
+ init_settings,
529
+ env_settings,
530
+ TomlFileSettingsSource(settings_cls),
531
+ file_secret_settings,
532
+ )
533
+
534
+ @model_validator(mode="after")
535
+ def _check_api_key(self) -> VibeConfig:
536
+ try:
537
+ active_model = self.get_active_model()
538
+ provider = self.get_provider_for_model(active_model)
539
+ api_key_env = provider.api_key_env_var
540
+ if api_key_env and not os.getenv(api_key_env):
541
+ raise MissingAPIKeyError(api_key_env, provider.name)
542
+ except ValueError:
543
+ pass
544
+ return self
545
+
546
+ @model_validator(mode="after")
547
+ def _check_api_backend_compatibility(self) -> VibeConfig:
548
+ try:
549
+ active_model = self.get_active_model()
550
+ provider = self.get_provider_for_model(active_model)
551
+ MISTRAL_API_BASES = [
552
+ "https://codestral.mistral.ai",
553
+ "https://api.mistral.ai",
554
+ ]
555
+ is_mistral_api = any(
556
+ provider.api_base.startswith(api_base) for api_base in MISTRAL_API_BASES
557
+ )
558
+ if (is_mistral_api and provider.backend != Backend.MISTRAL) or (
559
+ not is_mistral_api and provider.backend != Backend.GENERIC
560
+ ):
561
+ raise WrongBackendError(provider.backend, is_mistral_api)
562
+
563
+ except ValueError:
564
+ pass
565
+ return self
566
+
567
+ @field_validator("tool_paths", mode="before")
568
+ @classmethod
569
+ def _expand_tool_paths(cls, v: Any) -> list[Path]:
570
+ if not v:
571
+ return []
572
+ return [Path(p).expanduser().resolve() for p in v]
573
+
574
+ @field_validator("skill_paths", mode="before")
575
+ @classmethod
576
+ def _expand_skill_paths(cls, v: Any) -> list[Path]:
577
+ if not v:
578
+ return []
579
+ return [Path(p).expanduser().resolve() for p in v]
580
+
581
+ @field_validator("tools", mode="before")
582
+ @classmethod
583
+ def _normalize_tool_configs(cls, v: Any) -> dict[str, BaseToolConfig]:
584
+ if not isinstance(v, dict):
585
+ return {}
586
+
587
+ normalized: dict[str, BaseToolConfig] = {}
588
+ for tool_name, tool_config in v.items():
589
+ if isinstance(tool_config, BaseToolConfig):
590
+ normalized[tool_name] = tool_config
591
+ elif isinstance(tool_config, dict):
592
+ normalized[tool_name] = BaseToolConfig.model_validate(tool_config)
593
+ else:
594
+ normalized[tool_name] = BaseToolConfig()
595
+
596
+ return normalized
597
+
598
+ @model_validator(mode="after")
599
+ def _validate_model_uniqueness(self) -> VibeConfig:
600
+ seen_aliases: set[str] = set()
601
+ for model in self.models:
602
+ if model.alias in seen_aliases:
603
+ raise ValueError(
604
+ f"Duplicate model alias found: '{model.alias}'. Aliases must be unique."
605
+ )
606
+ seen_aliases.add(model.alias)
607
+ return self
608
+
609
+ @model_validator(mode="after")
610
+ def _check_system_prompt(self) -> VibeConfig:
611
+ _ = self.system_prompt
612
+ return self
613
+
614
+ @classmethod
615
+ def save_updates(cls, updates: dict[str, Any]) -> None:
616
+ CONFIG_DIR.path.mkdir(parents=True, exist_ok=True)
617
+ current_config = TomlFileSettingsSource(cls).toml_data
618
+
619
+ def deep_merge(target: dict, source: dict) -> None:
620
+ for key, value in source.items():
621
+ if (
622
+ key in target
623
+ and isinstance(target.get(key), dict)
624
+ and isinstance(value, dict)
625
+ ):
626
+ deep_merge(target[key], value)
627
+ elif (
628
+ key in target
629
+ and isinstance(target.get(key), list)
630
+ and isinstance(value, list)
631
+ ):
632
+ if key in {"providers", "models"}:
633
+ target[key] = value
634
+ else:
635
+ target[key] = list(set(value + target[key]))
636
+ else:
637
+ target[key] = value
638
+
639
+ deep_merge(current_config, updates)
640
+ cls.dump_config(
641
+ to_jsonable_python(current_config, exclude_none=True, fallback=str)
642
+ )
643
+
644
+ @classmethod
645
+ def dump_config(cls, config: dict[str, Any]) -> None:
646
+ with CONFIG_FILE.path.open("wb") as f:
647
+ tomli_w.dump(config, f)
648
+
649
+ @classmethod
650
+ def _migrate(cls) -> None:
651
+ pass
652
+
653
+ @classmethod
654
+ def load(cls, **overrides: Any) -> VibeConfig:
655
+ cls._migrate()
656
+ return cls(**(overrides or {}))
657
+
658
+ @classmethod
659
+ def create_default(cls) -> dict[str, Any]:
660
+ try:
661
+ config = cls()
662
+ except MissingAPIKeyError:
663
+ config = cls.model_construct()
664
+
665
+ config_dict = config.model_dump(mode="json", exclude_none=True)
666
+
667
+ from vibe.core.tools.manager import ToolManager
668
+
669
+ tool_defaults = ToolManager.discover_tool_defaults()
670
+ if tool_defaults:
671
+ config_dict["tools"] = tool_defaults
672
+
673
+ return config_dict