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
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
import fnmatch
|
|
5
|
+
import html
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from vibe.core.prompts import UtilityPrompt
|
|
14
|
+
from vibe.core.trusted_folders import AGENTS_MD_FILENAMES, trusted_folders_manager
|
|
15
|
+
from vibe.core.utils import is_dangerous_directory, is_windows
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from vibe.core.agents import AgentManager
|
|
19
|
+
from vibe.core.config import ProjectContextConfig, VibeConfig
|
|
20
|
+
from vibe.core.skills.manager import SkillManager
|
|
21
|
+
from vibe.core.tools.manager import ToolManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_project_doc(workdir: Path, max_bytes: int) -> str:
|
|
25
|
+
if not trusted_folders_manager.is_trusted(workdir):
|
|
26
|
+
return ""
|
|
27
|
+
for name in AGENTS_MD_FILENAMES:
|
|
28
|
+
path = workdir / name
|
|
29
|
+
try:
|
|
30
|
+
return path.read_text("utf-8", errors="ignore")[:max_bytes]
|
|
31
|
+
except (FileNotFoundError, OSError):
|
|
32
|
+
continue
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProjectContextProvider:
|
|
37
|
+
def __init__(
|
|
38
|
+
self, config: ProjectContextConfig, root_path: str | Path = "."
|
|
39
|
+
) -> None:
|
|
40
|
+
self.root_path = Path(root_path).resolve()
|
|
41
|
+
self.config = config
|
|
42
|
+
self.gitignore_patterns = self._load_gitignore_patterns()
|
|
43
|
+
self._file_count = 0
|
|
44
|
+
self._start_time = 0.0
|
|
45
|
+
|
|
46
|
+
def _load_gitignore_patterns(self) -> list[str]:
|
|
47
|
+
gitignore_path = self.root_path / ".gitignore"
|
|
48
|
+
patterns = []
|
|
49
|
+
|
|
50
|
+
if gitignore_path.exists():
|
|
51
|
+
try:
|
|
52
|
+
patterns.extend(
|
|
53
|
+
line.strip()
|
|
54
|
+
for line in gitignore_path.read_text(encoding="utf-8").splitlines()
|
|
55
|
+
if line.strip() and not line.startswith("#")
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Warning: Could not read .gitignore: {e}", file=sys.stderr)
|
|
59
|
+
|
|
60
|
+
default_patterns = [
|
|
61
|
+
".git",
|
|
62
|
+
".git/*",
|
|
63
|
+
"*.pyc",
|
|
64
|
+
"__pycache__",
|
|
65
|
+
"node_modules",
|
|
66
|
+
"node_modules/*",
|
|
67
|
+
".env",
|
|
68
|
+
".DS_Store",
|
|
69
|
+
"*.log",
|
|
70
|
+
".vscode/settings.json",
|
|
71
|
+
".idea/*",
|
|
72
|
+
"dist",
|
|
73
|
+
"build",
|
|
74
|
+
"target",
|
|
75
|
+
".next",
|
|
76
|
+
".nuxt",
|
|
77
|
+
"coverage",
|
|
78
|
+
".nyc_output",
|
|
79
|
+
"*.egg-info",
|
|
80
|
+
".pytest_cache",
|
|
81
|
+
".tox",
|
|
82
|
+
"vendor",
|
|
83
|
+
"third_party",
|
|
84
|
+
"deps",
|
|
85
|
+
"*.min.js",
|
|
86
|
+
"*.min.css",
|
|
87
|
+
"*.bundle.js",
|
|
88
|
+
"*.chunk.js",
|
|
89
|
+
".cache",
|
|
90
|
+
"tmp",
|
|
91
|
+
"temp",
|
|
92
|
+
"logs",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
return patterns + default_patterns
|
|
96
|
+
|
|
97
|
+
def _is_ignored(self, path: Path) -> bool:
|
|
98
|
+
try:
|
|
99
|
+
relative_path = path.relative_to(self.root_path)
|
|
100
|
+
path_str = str(relative_path)
|
|
101
|
+
|
|
102
|
+
for pattern in self.gitignore_patterns:
|
|
103
|
+
if pattern.endswith("/"):
|
|
104
|
+
if path.is_dir() and fnmatch.fnmatch(f"{path_str}/", pattern):
|
|
105
|
+
return True
|
|
106
|
+
elif fnmatch.fnmatch(path_str, pattern):
|
|
107
|
+
return True
|
|
108
|
+
elif "*" in pattern or "?" in pattern:
|
|
109
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
return False
|
|
113
|
+
except (ValueError, OSError):
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def _should_stop(self) -> bool:
|
|
117
|
+
return (
|
|
118
|
+
self._file_count >= self.config.max_files
|
|
119
|
+
or (time.time() - self._start_time) > self.config.timeout_seconds
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _build_tree_structure_iterative(self) -> Generator[str]:
|
|
123
|
+
self._start_time = time.time()
|
|
124
|
+
self._file_count = 0
|
|
125
|
+
|
|
126
|
+
yield from self._process_directory(self.root_path, "", 0, is_root=True)
|
|
127
|
+
|
|
128
|
+
def _process_directory(
|
|
129
|
+
self, path: Path, prefix: str, depth: int, is_root: bool = False
|
|
130
|
+
) -> Generator[str]:
|
|
131
|
+
if depth > self.config.max_depth or self._should_stop():
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
all_items = list(path.iterdir())
|
|
136
|
+
items = [item for item in all_items if not self._is_ignored(item)]
|
|
137
|
+
|
|
138
|
+
items.sort(key=lambda p: (not p.is_dir(), p.name.lower()))
|
|
139
|
+
|
|
140
|
+
show_truncation = len(items) > self.config.max_dirs_per_level
|
|
141
|
+
if show_truncation:
|
|
142
|
+
items = items[: self.config.max_dirs_per_level]
|
|
143
|
+
|
|
144
|
+
for i, item in enumerate(items):
|
|
145
|
+
if self._should_stop():
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
is_last = i == len(items) - 1 and not show_truncation
|
|
149
|
+
connector = "└── " if is_last else "├── "
|
|
150
|
+
name = f"{item.name}{'/' if item.is_dir() else ''}"
|
|
151
|
+
|
|
152
|
+
yield f"{prefix}{connector}{name}"
|
|
153
|
+
self._file_count += 1
|
|
154
|
+
|
|
155
|
+
if item.is_dir() and depth < self.config.max_depth:
|
|
156
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
157
|
+
yield from self._process_directory(item, child_prefix, depth + 1)
|
|
158
|
+
|
|
159
|
+
if show_truncation and not self._should_stop():
|
|
160
|
+
remaining = len(all_items) - len(items)
|
|
161
|
+
yield f"{prefix}└── ... ({remaining} more items)"
|
|
162
|
+
|
|
163
|
+
except (PermissionError, OSError):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def get_directory_structure(self) -> str:
|
|
167
|
+
lines = []
|
|
168
|
+
header = f"Directory structure of {self.root_path.name} (depth≤{self.config.max_depth}, max {self.config.max_files} items):\n"
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
for line in self._build_tree_structure_iterative():
|
|
172
|
+
lines.append(line)
|
|
173
|
+
|
|
174
|
+
current_text = header + "\n".join(lines)
|
|
175
|
+
if (
|
|
176
|
+
len(current_text)
|
|
177
|
+
> self.config.max_chars - self.config.truncation_buffer
|
|
178
|
+
):
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
lines.append(f"Error building structure: {e}")
|
|
183
|
+
|
|
184
|
+
structure = header + "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
if self._file_count >= self.config.max_files:
|
|
187
|
+
structure += f"\n... (truncated at {self.config.max_files} files limit)"
|
|
188
|
+
elif (time.time() - self._start_time) > self.config.timeout_seconds:
|
|
189
|
+
structure += (
|
|
190
|
+
f"\n... (truncated due to {self.config.timeout_seconds}s timeout)"
|
|
191
|
+
)
|
|
192
|
+
elif len(structure) > self.config.max_chars:
|
|
193
|
+
structure += f"\n... (truncated at {self.config.max_chars} characters)"
|
|
194
|
+
|
|
195
|
+
return structure
|
|
196
|
+
|
|
197
|
+
def get_git_status(self) -> str:
|
|
198
|
+
try:
|
|
199
|
+
timeout = min(self.config.timeout_seconds, 10.0)
|
|
200
|
+
num_commits = self.config.default_commit_count
|
|
201
|
+
|
|
202
|
+
current_branch = subprocess.run(
|
|
203
|
+
["git", "branch", "--show-current"],
|
|
204
|
+
capture_output=True,
|
|
205
|
+
check=True,
|
|
206
|
+
cwd=self.root_path,
|
|
207
|
+
stdin=subprocess.DEVNULL if is_windows() else None,
|
|
208
|
+
text=True,
|
|
209
|
+
timeout=timeout,
|
|
210
|
+
).stdout.strip()
|
|
211
|
+
|
|
212
|
+
main_branch = "main"
|
|
213
|
+
try:
|
|
214
|
+
branches_output = subprocess.run(
|
|
215
|
+
["git", "branch", "-r"],
|
|
216
|
+
capture_output=True,
|
|
217
|
+
check=True,
|
|
218
|
+
cwd=self.root_path,
|
|
219
|
+
stdin=subprocess.DEVNULL if is_windows() else None,
|
|
220
|
+
text=True,
|
|
221
|
+
timeout=timeout,
|
|
222
|
+
).stdout
|
|
223
|
+
if "origin/master" in branches_output:
|
|
224
|
+
main_branch = "master"
|
|
225
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
status_output = subprocess.run(
|
|
229
|
+
["git", "status", "--porcelain"],
|
|
230
|
+
capture_output=True,
|
|
231
|
+
check=True,
|
|
232
|
+
cwd=self.root_path,
|
|
233
|
+
stdin=subprocess.DEVNULL if is_windows() else None,
|
|
234
|
+
text=True,
|
|
235
|
+
timeout=timeout,
|
|
236
|
+
).stdout.strip()
|
|
237
|
+
|
|
238
|
+
if status_output:
|
|
239
|
+
status_lines = status_output.splitlines()
|
|
240
|
+
MAX_GIT_STATUS_SIZE = 50
|
|
241
|
+
if len(status_lines) > MAX_GIT_STATUS_SIZE:
|
|
242
|
+
status = (
|
|
243
|
+
f"({len(status_lines)} changes - use 'git status' for details)"
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
status = f"({len(status_lines)} changes)"
|
|
247
|
+
else:
|
|
248
|
+
status = "(clean)"
|
|
249
|
+
|
|
250
|
+
log_output = subprocess.run(
|
|
251
|
+
["git", "log", "--oneline", f"-{num_commits}", "--decorate"],
|
|
252
|
+
capture_output=True,
|
|
253
|
+
check=True,
|
|
254
|
+
cwd=self.root_path,
|
|
255
|
+
stdin=subprocess.DEVNULL if is_windows() else None,
|
|
256
|
+
text=True,
|
|
257
|
+
timeout=timeout,
|
|
258
|
+
).stdout.strip()
|
|
259
|
+
|
|
260
|
+
recent_commits = []
|
|
261
|
+
for line in log_output.split("\n"):
|
|
262
|
+
if not (line := line.strip()):
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
if " " in line:
|
|
266
|
+
commit_hash, commit_msg = line.split(" ", 1)
|
|
267
|
+
if (
|
|
268
|
+
"(" in commit_msg
|
|
269
|
+
and ")" in commit_msg
|
|
270
|
+
and (paren_index := commit_msg.rfind("(")) > 0
|
|
271
|
+
):
|
|
272
|
+
commit_msg = commit_msg[:paren_index].strip()
|
|
273
|
+
recent_commits.append(f"{commit_hash} {commit_msg}")
|
|
274
|
+
else:
|
|
275
|
+
recent_commits.append(line)
|
|
276
|
+
|
|
277
|
+
git_info_parts = [
|
|
278
|
+
f"Current branch: {current_branch}",
|
|
279
|
+
f"Main branch (you will usually use this for PRs): {main_branch}",
|
|
280
|
+
f"Status: {status}",
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
if recent_commits:
|
|
284
|
+
git_info_parts.append("Recent commits:")
|
|
285
|
+
git_info_parts.extend(recent_commits)
|
|
286
|
+
|
|
287
|
+
return "\n".join(git_info_parts)
|
|
288
|
+
|
|
289
|
+
except subprocess.TimeoutExpired:
|
|
290
|
+
return "Git operations timed out (large repository)"
|
|
291
|
+
except subprocess.CalledProcessError:
|
|
292
|
+
return "Not a git repository or git not available"
|
|
293
|
+
except Exception as e:
|
|
294
|
+
return f"Error getting git status: {e}"
|
|
295
|
+
|
|
296
|
+
def get_full_context(self) -> str:
|
|
297
|
+
structure = self.get_directory_structure()
|
|
298
|
+
git_status = self.get_git_status()
|
|
299
|
+
|
|
300
|
+
large_repo_warning = ""
|
|
301
|
+
if len(structure) >= self.config.max_chars - self.config.truncation_buffer:
|
|
302
|
+
large_repo_warning = (
|
|
303
|
+
f" Large repository detected - showing summary view with depth limit {self.config.max_depth}. "
|
|
304
|
+
f"Use the LS tool (passing a specific path), Bash tool, and other tools to explore nested directories in detail."
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
template = UtilityPrompt.PROJECT_CONTEXT.read()
|
|
308
|
+
return template.format(
|
|
309
|
+
large_repo_warning=large_repo_warning,
|
|
310
|
+
structure=structure,
|
|
311
|
+
abs_path=self.root_path,
|
|
312
|
+
git_status=git_status,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _get_platform_name() -> str:
|
|
317
|
+
platform_names = {
|
|
318
|
+
"win32": "Windows",
|
|
319
|
+
"darwin": "macOS",
|
|
320
|
+
"linux": "Linux",
|
|
321
|
+
"freebsd": "FreeBSD",
|
|
322
|
+
"openbsd": "OpenBSD",
|
|
323
|
+
"netbsd": "NetBSD",
|
|
324
|
+
}
|
|
325
|
+
return platform_names.get(sys.platform, "Unix-like")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _get_default_shell() -> str:
|
|
329
|
+
"""Get the default shell used by asyncio.create_subprocess_shell.
|
|
330
|
+
|
|
331
|
+
On Unix, uses $SHELL env var and default to sh.
|
|
332
|
+
On Windows, this is COMSPEC or cmd.exe.
|
|
333
|
+
"""
|
|
334
|
+
if is_windows():
|
|
335
|
+
return os.environ.get("COMSPEC", "cmd.exe")
|
|
336
|
+
return os.environ.get("SHELL", "sh")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _get_os_system_prompt() -> str:
|
|
340
|
+
shell = _get_default_shell()
|
|
341
|
+
platform_name = _get_platform_name()
|
|
342
|
+
prompt = f"The operating system is {platform_name} with shell `{shell}`"
|
|
343
|
+
|
|
344
|
+
if is_windows():
|
|
345
|
+
prompt += "\n" + _get_windows_system_prompt()
|
|
346
|
+
return prompt
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _get_windows_system_prompt() -> str:
|
|
350
|
+
return (
|
|
351
|
+
"### COMMAND COMPATIBILITY RULES (MUST FOLLOW):\n"
|
|
352
|
+
"- DO NOT use Unix commands like `ls`, `grep`, `cat` - they won't work on Windows\n"
|
|
353
|
+
"- Use: `dir` (Windows) for directory listings\n"
|
|
354
|
+
"- Use: backslashes (\\\\) for paths\n"
|
|
355
|
+
"- Check command availability with: `where command` (Windows)\n"
|
|
356
|
+
"- Script shebang: Not applicable on Windows\n"
|
|
357
|
+
"### ALWAYS verify commands work on the detected platform before suggesting them"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _add_commit_signature() -> str:
|
|
362
|
+
return (
|
|
363
|
+
"When you want to commit changes, you will always use the 'git commit' bash command.\n"
|
|
364
|
+
"It will always be suffixed with a line telling it was generated by codeMaster with the appropriate co-authoring information.\n"
|
|
365
|
+
"The format you will always uses is the following heredoc.\n\n"
|
|
366
|
+
"```bash\n"
|
|
367
|
+
"git commit -m <Commit message here>\n\n"
|
|
368
|
+
"Generated by codeMaster.\n"
|
|
369
|
+
"Co-Authored-By: codeMaster <codemaster@codemaster.ai>\n"
|
|
370
|
+
"```"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _get_available_skills_section(skill_manager: SkillManager) -> str:
|
|
375
|
+
skills = skill_manager.available_skills
|
|
376
|
+
if not skills:
|
|
377
|
+
return ""
|
|
378
|
+
|
|
379
|
+
lines = [
|
|
380
|
+
"# Available Skills",
|
|
381
|
+
"",
|
|
382
|
+
"You have access to the following skills. When a task matches a skill's description,",
|
|
383
|
+
"read the full SKILL.md file to load detailed instructions.",
|
|
384
|
+
"",
|
|
385
|
+
"<available_skills>",
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
for name, info in sorted(skills.items()):
|
|
389
|
+
lines.append(" <skill>")
|
|
390
|
+
lines.append(f" <name>{html.escape(str(name))}</name>")
|
|
391
|
+
lines.append(
|
|
392
|
+
f" <description>{html.escape(str(info.description))}</description>"
|
|
393
|
+
)
|
|
394
|
+
lines.append(f" <path>{html.escape(str(info.skill_path))}</path>")
|
|
395
|
+
lines.append(" </skill>")
|
|
396
|
+
|
|
397
|
+
lines.append("</available_skills>")
|
|
398
|
+
|
|
399
|
+
return "\n".join(lines)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _get_available_subagents_section(agent_manager: AgentManager) -> str:
|
|
403
|
+
agents = agent_manager.get_subagents()
|
|
404
|
+
if not agents:
|
|
405
|
+
return ""
|
|
406
|
+
|
|
407
|
+
lines = ["# Available Subagents", ""]
|
|
408
|
+
lines.append("The following subagents can be spawned via the Task tool:")
|
|
409
|
+
for agent in agents:
|
|
410
|
+
lines.append(f"- **{agent.name}**: {agent.description}")
|
|
411
|
+
|
|
412
|
+
return "\n".join(lines)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def get_universal_system_prompt(
|
|
416
|
+
tool_manager: ToolManager,
|
|
417
|
+
config: VibeConfig,
|
|
418
|
+
skill_manager: SkillManager,
|
|
419
|
+
agent_manager: AgentManager,
|
|
420
|
+
) -> str:
|
|
421
|
+
sections = [config.system_prompt]
|
|
422
|
+
|
|
423
|
+
if config.include_commit_signature:
|
|
424
|
+
sections.append(_add_commit_signature())
|
|
425
|
+
|
|
426
|
+
if config.include_model_info:
|
|
427
|
+
sections.append(f"Your model name is: `{config.active_model}`")
|
|
428
|
+
|
|
429
|
+
if config.include_prompt_detail:
|
|
430
|
+
sections.append(_get_os_system_prompt())
|
|
431
|
+
tool_prompts = []
|
|
432
|
+
for tool_class in tool_manager.available_tools.values():
|
|
433
|
+
if prompt := tool_class.get_tool_prompt():
|
|
434
|
+
tool_prompts.append(prompt)
|
|
435
|
+
if tool_prompts:
|
|
436
|
+
sections.append("\n---\n".join(tool_prompts))
|
|
437
|
+
|
|
438
|
+
skills_section = _get_available_skills_section(skill_manager)
|
|
439
|
+
if skills_section:
|
|
440
|
+
sections.append(skills_section)
|
|
441
|
+
|
|
442
|
+
subagents_section = _get_available_subagents_section(agent_manager)
|
|
443
|
+
if subagents_section:
|
|
444
|
+
sections.append(subagents_section)
|
|
445
|
+
|
|
446
|
+
if config.include_project_context:
|
|
447
|
+
is_dangerous, reason = is_dangerous_directory()
|
|
448
|
+
if is_dangerous:
|
|
449
|
+
template = UtilityPrompt.DANGEROUS_DIRECTORY.read()
|
|
450
|
+
context = template.format(
|
|
451
|
+
reason=reason.lower(), abs_path=Path(".").resolve()
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
context = ProjectContextProvider(
|
|
455
|
+
config=config.project_context, root_path=Path.cwd()
|
|
456
|
+
).get_full_context()
|
|
457
|
+
|
|
458
|
+
sections.append(context)
|
|
459
|
+
|
|
460
|
+
project_doc = _load_project_doc(
|
|
461
|
+
Path.cwd(), config.project_context.max_doc_bytes
|
|
462
|
+
)
|
|
463
|
+
if project_doc.strip():
|
|
464
|
+
sections.append(project_doc)
|
|
465
|
+
|
|
466
|
+
return "\n\n".join(sections)
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
import os
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from vibe import __version__
|
|
11
|
+
from vibe.core.config import Backend, VibeConfig
|
|
12
|
+
from vibe.core.llm.format import ResolvedToolCall
|
|
13
|
+
from vibe.core.utils import get_user_agent
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from vibe.core.agent_loop import ToolDecision
|
|
17
|
+
|
|
18
|
+
DATALAKE_EVENTS_URL = "https://codestral.mistral.ai/v1/datalake/events"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TelemetryClient:
|
|
22
|
+
def __init__(self, config_getter: Callable[[], VibeConfig]) -> None:
|
|
23
|
+
self._config_getter = config_getter
|
|
24
|
+
self._client: httpx.AsyncClient | None = None
|
|
25
|
+
self._pending_tasks: set[asyncio.Task[Any]] = set()
|
|
26
|
+
|
|
27
|
+
def _get_telemetry_user_agent(self) -> str:
|
|
28
|
+
try:
|
|
29
|
+
config = self._config_getter()
|
|
30
|
+
active_model = config.get_active_model()
|
|
31
|
+
provider = config.get_provider_for_model(active_model)
|
|
32
|
+
return get_user_agent(provider.backend)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return get_user_agent(None)
|
|
35
|
+
|
|
36
|
+
def _get_mistral_api_key(self) -> str | None:
|
|
37
|
+
"""Get the current API key from the active provider.
|
|
38
|
+
|
|
39
|
+
Only returns an API key if the provider is a Mistral provider
|
|
40
|
+
to avoid leaking third-party credentials to the telemetry endpoint.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
config = self._config_getter()
|
|
44
|
+
model = config.get_active_model()
|
|
45
|
+
provider = config.get_provider_for_model(model)
|
|
46
|
+
if provider.backend != Backend.MISTRAL:
|
|
47
|
+
return None
|
|
48
|
+
env_var = provider.api_key_env_var
|
|
49
|
+
return os.getenv(env_var) if env_var else None
|
|
50
|
+
except ValueError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def _is_enabled(self) -> bool:
|
|
54
|
+
"""Check if telemetry is enabled in the current config."""
|
|
55
|
+
try:
|
|
56
|
+
return self._config_getter().enable_telemetry
|
|
57
|
+
except ValueError:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def client(self) -> httpx.AsyncClient:
|
|
62
|
+
if self._client is None:
|
|
63
|
+
self._client = httpx.AsyncClient(
|
|
64
|
+
timeout=httpx.Timeout(5.0),
|
|
65
|
+
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
|
66
|
+
)
|
|
67
|
+
return self._client
|
|
68
|
+
|
|
69
|
+
def send_telemetry_event(self, event_name: str, properties: dict[str, Any]) -> None:
|
|
70
|
+
mistral_api_key = self._get_mistral_api_key()
|
|
71
|
+
if mistral_api_key is None or not self._is_enabled():
|
|
72
|
+
return
|
|
73
|
+
user_agent = self._get_telemetry_user_agent()
|
|
74
|
+
|
|
75
|
+
async def _send() -> None:
|
|
76
|
+
try:
|
|
77
|
+
await self.client.post(
|
|
78
|
+
DATALAKE_EVENTS_URL,
|
|
79
|
+
json={"event": event_name, "properties": properties},
|
|
80
|
+
headers={
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
"Authorization": f"Bearer {mistral_api_key}",
|
|
83
|
+
"User-Agent": user_agent,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass # Silently swallow all exceptions for fire-and-forget telemetry
|
|
88
|
+
|
|
89
|
+
task = asyncio.create_task(_send())
|
|
90
|
+
self._pending_tasks.add(task)
|
|
91
|
+
task.add_done_callback(self._pending_tasks.discard)
|
|
92
|
+
|
|
93
|
+
async def aclose(self) -> None:
|
|
94
|
+
if self._pending_tasks:
|
|
95
|
+
await asyncio.gather(*self._pending_tasks, return_exceptions=True)
|
|
96
|
+
if self._client is not None:
|
|
97
|
+
await self._client.aclose()
|
|
98
|
+
self._client = None
|
|
99
|
+
|
|
100
|
+
def _calculate_file_metrics(
|
|
101
|
+
self,
|
|
102
|
+
tool_call: ResolvedToolCall,
|
|
103
|
+
status: Literal["success", "failure", "skipped"],
|
|
104
|
+
result: dict[str, Any] | None = None,
|
|
105
|
+
) -> tuple[int, int]:
|
|
106
|
+
nb_files_created = 0
|
|
107
|
+
nb_files_modified = 0
|
|
108
|
+
if status == "success" and result is not None:
|
|
109
|
+
if tool_call.tool_name == "write_file":
|
|
110
|
+
file_existed = result.get("file_existed", False)
|
|
111
|
+
if file_existed:
|
|
112
|
+
nb_files_modified = 1
|
|
113
|
+
else:
|
|
114
|
+
nb_files_created = 1
|
|
115
|
+
elif tool_call.tool_name == "search_replace":
|
|
116
|
+
nb_files_modified = 1 if result.get("blocks_applied", 0) > 0 else 0
|
|
117
|
+
return nb_files_created, nb_files_modified
|
|
118
|
+
|
|
119
|
+
def send_tool_call_finished(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
tool_call: ResolvedToolCall,
|
|
123
|
+
status: Literal["success", "failure", "skipped"],
|
|
124
|
+
decision: ToolDecision | None,
|
|
125
|
+
agent_profile_name: str,
|
|
126
|
+
result: dict[str, Any] | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
verdict_value = decision.verdict.value if decision else None
|
|
129
|
+
approval_type_value = decision.approval_type.value if decision else None
|
|
130
|
+
|
|
131
|
+
nb_files_created, nb_files_modified = self._calculate_file_metrics(
|
|
132
|
+
tool_call, status, result
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
payload = {
|
|
136
|
+
"tool_name": tool_call.tool_name,
|
|
137
|
+
"status": status,
|
|
138
|
+
"decision": verdict_value,
|
|
139
|
+
"approval_type": approval_type_value,
|
|
140
|
+
"agent_profile_name": agent_profile_name,
|
|
141
|
+
"nb_files_created": nb_files_created,
|
|
142
|
+
"nb_files_modified": nb_files_modified,
|
|
143
|
+
}
|
|
144
|
+
self.send_telemetry_event("vibe/tool_call_finished", payload)
|
|
145
|
+
|
|
146
|
+
def send_user_copied_text(self, text: str) -> None:
|
|
147
|
+
payload = {"text_length": len(text)}
|
|
148
|
+
self.send_telemetry_event("vibe/user_copied_text", payload)
|
|
149
|
+
|
|
150
|
+
def send_user_cancelled_action(self, action: str) -> None:
|
|
151
|
+
payload = {"action": action}
|
|
152
|
+
self.send_telemetry_event("vibe/user_cancelled_action", payload)
|
|
153
|
+
|
|
154
|
+
def send_auto_compact_triggered(self) -> None:
|
|
155
|
+
payload = {}
|
|
156
|
+
self.send_telemetry_event("vibe/auto_compact_triggered", payload)
|
|
157
|
+
|
|
158
|
+
def send_slash_command_used(
|
|
159
|
+
self, command: str, command_type: Literal["builtin", "skill"]
|
|
160
|
+
) -> None:
|
|
161
|
+
payload = {"command": command.lstrip("/"), "command_type": command_type}
|
|
162
|
+
self.send_telemetry_event("vibe/slash_command_used", payload)
|
|
163
|
+
|
|
164
|
+
def send_new_session(
|
|
165
|
+
self,
|
|
166
|
+
has_agents_md: bool,
|
|
167
|
+
nb_skills: int,
|
|
168
|
+
nb_mcp_servers: int,
|
|
169
|
+
nb_models: int,
|
|
170
|
+
entrypoint: Literal["cli", "acp", "programmatic"],
|
|
171
|
+
) -> None:
|
|
172
|
+
payload = {
|
|
173
|
+
"has_agents_md": has_agents_md,
|
|
174
|
+
"nb_skills": nb_skills,
|
|
175
|
+
"nb_mcp_servers": nb_mcp_servers,
|
|
176
|
+
"nb_models": nb_models,
|
|
177
|
+
"entrypoint": entrypoint,
|
|
178
|
+
"version": __version__,
|
|
179
|
+
}
|
|
180
|
+
self.send_telemetry_event("vibe/new_session", payload)
|
|
181
|
+
|
|
182
|
+
def send_onboarding_api_key_added(self) -> None:
|
|
183
|
+
self.send_telemetry_event(
|
|
184
|
+
"vibe/onboarding_api_key_added", {"version": __version__}
|
|
185
|
+
)
|