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.
- codemaster_cli-2.2.0.dist-info/METADATA +645 -0
- codemaster_cli-2.2.0.dist-info/RECORD +170 -0
- codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
- codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
- vibe/__init__.py +6 -0
- vibe/acp/__init__.py +0 -0
- vibe/acp/acp_agent_loop.py +746 -0
- vibe/acp/entrypoint.py +81 -0
- vibe/acp/tools/__init__.py +0 -0
- vibe/acp/tools/base.py +100 -0
- vibe/acp/tools/builtins/bash.py +134 -0
- vibe/acp/tools/builtins/read_file.py +54 -0
- vibe/acp/tools/builtins/search_replace.py +129 -0
- vibe/acp/tools/builtins/todo.py +65 -0
- vibe/acp/tools/builtins/write_file.py +98 -0
- vibe/acp/tools/session_update.py +118 -0
- vibe/acp/utils.py +213 -0
- vibe/cli/__init__.py +0 -0
- vibe/cli/autocompletion/__init__.py +0 -0
- vibe/cli/autocompletion/base.py +22 -0
- vibe/cli/autocompletion/path_completion.py +177 -0
- vibe/cli/autocompletion/slash_command.py +99 -0
- vibe/cli/cli.py +188 -0
- vibe/cli/clipboard.py +69 -0
- vibe/cli/commands.py +116 -0
- vibe/cli/entrypoint.py +163 -0
- vibe/cli/history_manager.py +91 -0
- vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
- vibe/cli/plan_offer/decide_plan_offer.py +87 -0
- vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
- vibe/cli/terminal_setup.py +323 -0
- vibe/cli/textual_ui/__init__.py +0 -0
- vibe/cli/textual_ui/ansi_markdown.py +58 -0
- vibe/cli/textual_ui/app.py +1546 -0
- vibe/cli/textual_ui/app.tcss +1020 -0
- vibe/cli/textual_ui/external_editor.py +32 -0
- vibe/cli/textual_ui/handlers/__init__.py +5 -0
- vibe/cli/textual_ui/handlers/event_handler.py +147 -0
- vibe/cli/textual_ui/widgets/__init__.py +0 -0
- vibe/cli/textual_ui/widgets/approval_app.py +192 -0
- vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
- vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
- vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
- vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
- vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
- vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
- vibe/cli/textual_ui/widgets/compact.py +41 -0
- vibe/cli/textual_ui/widgets/config_app.py +171 -0
- vibe/cli/textual_ui/widgets/context_progress.py +30 -0
- vibe/cli/textual_ui/widgets/load_more.py +43 -0
- vibe/cli/textual_ui/widgets/loading.py +201 -0
- vibe/cli/textual_ui/widgets/messages.py +277 -0
- vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
- vibe/cli/textual_ui/widgets/path_display.py +28 -0
- vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
- vibe/cli/textual_ui/widgets/question_app.py +496 -0
- vibe/cli/textual_ui/widgets/spinner.py +194 -0
- vibe/cli/textual_ui/widgets/status_message.py +76 -0
- vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
- vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
- vibe/cli/textual_ui/widgets/tools.py +201 -0
- vibe/cli/textual_ui/windowing/__init__.py +29 -0
- vibe/cli/textual_ui/windowing/history.py +105 -0
- vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
- vibe/cli/textual_ui/windowing/state.py +105 -0
- vibe/cli/update_notifier/__init__.py +47 -0
- vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
- vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
- vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
- vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
- vibe/cli/update_notifier/ports/update_gateway.py +53 -0
- vibe/cli/update_notifier/update.py +139 -0
- vibe/cli/update_notifier/whats_new.py +49 -0
- vibe/core/__init__.py +5 -0
- vibe/core/agent_loop.py +1075 -0
- vibe/core/agents/__init__.py +31 -0
- vibe/core/agents/manager.py +165 -0
- vibe/core/agents/models.py +122 -0
- vibe/core/auth/__init__.py +6 -0
- vibe/core/auth/crypto.py +137 -0
- vibe/core/auth/github.py +178 -0
- vibe/core/autocompletion/__init__.py +0 -0
- vibe/core/autocompletion/completers.py +257 -0
- vibe/core/autocompletion/file_indexer/__init__.py +10 -0
- vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
- vibe/core/autocompletion/file_indexer/indexer.py +179 -0
- vibe/core/autocompletion/file_indexer/store.py +169 -0
- vibe/core/autocompletion/file_indexer/watcher.py +71 -0
- vibe/core/autocompletion/fuzzy.py +189 -0
- vibe/core/autocompletion/path_prompt.py +108 -0
- vibe/core/autocompletion/path_prompt_adapter.py +149 -0
- vibe/core/config.py +673 -0
- vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
- vibe/core/llm/__init__.py +0 -0
- vibe/core/llm/backend/anthropic.py +630 -0
- vibe/core/llm/backend/base.py +38 -0
- vibe/core/llm/backend/factory.py +7 -0
- vibe/core/llm/backend/generic.py +425 -0
- vibe/core/llm/backend/mistral.py +381 -0
- vibe/core/llm/backend/vertex.py +115 -0
- vibe/core/llm/exceptions.py +195 -0
- vibe/core/llm/format.py +184 -0
- vibe/core/llm/message_utils.py +24 -0
- vibe/core/llm/types.py +120 -0
- vibe/core/middleware.py +209 -0
- vibe/core/output_formatters.py +85 -0
- vibe/core/paths/__init__.py +0 -0
- vibe/core/paths/config_paths.py +68 -0
- vibe/core/paths/global_paths.py +40 -0
- vibe/core/programmatic.py +56 -0
- vibe/core/prompts/__init__.py +32 -0
- vibe/core/prompts/cli.md +111 -0
- vibe/core/prompts/compact.md +48 -0
- vibe/core/prompts/dangerous_directory.md +5 -0
- vibe/core/prompts/explore.md +50 -0
- vibe/core/prompts/project_context.md +8 -0
- vibe/core/prompts/tests.md +1 -0
- vibe/core/proxy_setup.py +65 -0
- vibe/core/session/session_loader.py +222 -0
- vibe/core/session/session_logger.py +318 -0
- vibe/core/session/session_migration.py +41 -0
- vibe/core/skills/__init__.py +7 -0
- vibe/core/skills/manager.py +132 -0
- vibe/core/skills/models.py +92 -0
- vibe/core/skills/parser.py +39 -0
- vibe/core/system_prompt.py +466 -0
- vibe/core/telemetry/__init__.py +0 -0
- vibe/core/telemetry/send.py +185 -0
- vibe/core/teleport/errors.py +9 -0
- vibe/core/teleport/git.py +196 -0
- vibe/core/teleport/nuage.py +180 -0
- vibe/core/teleport/teleport.py +208 -0
- vibe/core/teleport/types.py +54 -0
- vibe/core/tools/base.py +336 -0
- vibe/core/tools/builtins/ask_user_question.py +134 -0
- vibe/core/tools/builtins/bash.py +357 -0
- vibe/core/tools/builtins/grep.py +310 -0
- vibe/core/tools/builtins/prompts/__init__.py +0 -0
- vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
- vibe/core/tools/builtins/prompts/bash.md +73 -0
- vibe/core/tools/builtins/prompts/grep.md +4 -0
- vibe/core/tools/builtins/prompts/read_file.md +13 -0
- vibe/core/tools/builtins/prompts/search_replace.md +43 -0
- vibe/core/tools/builtins/prompts/task.md +24 -0
- vibe/core/tools/builtins/prompts/todo.md +199 -0
- vibe/core/tools/builtins/prompts/write_file.md +42 -0
- vibe/core/tools/builtins/read_file.py +222 -0
- vibe/core/tools/builtins/search_replace.py +456 -0
- vibe/core/tools/builtins/task.py +154 -0
- vibe/core/tools/builtins/todo.py +134 -0
- vibe/core/tools/builtins/write_file.py +160 -0
- vibe/core/tools/manager.py +341 -0
- vibe/core/tools/mcp.py +397 -0
- vibe/core/tools/ui.py +68 -0
- vibe/core/trusted_folders.py +86 -0
- vibe/core/types.py +405 -0
- vibe/core/utils.py +396 -0
- vibe/setup/onboarding/__init__.py +39 -0
- vibe/setup/onboarding/base.py +14 -0
- vibe/setup/onboarding/onboarding.tcss +134 -0
- vibe/setup/onboarding/screens/__init__.py +5 -0
- vibe/setup/onboarding/screens/api_key.py +200 -0
- vibe/setup/onboarding/screens/provider_selection.py +87 -0
- vibe/setup/onboarding/screens/welcome.py +136 -0
- vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
- vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
- 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
|