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/cli/cli.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
|
|
8
|
+
from vibe.cli.textual_ui.app import run_textual_ui
|
|
9
|
+
from vibe.core.agent_loop import AgentLoop
|
|
10
|
+
from vibe.core.agents.models import BuiltinAgentName
|
|
11
|
+
from vibe.core.config import (
|
|
12
|
+
MissingAPIKeyError,
|
|
13
|
+
MissingPromptFileError,
|
|
14
|
+
VibeConfig,
|
|
15
|
+
load_dotenv_values,
|
|
16
|
+
)
|
|
17
|
+
from vibe.core.paths.config_paths import CONFIG_FILE, HISTORY_FILE
|
|
18
|
+
from vibe.core.programmatic import run_programmatic
|
|
19
|
+
from vibe.core.session.session_loader import SessionLoader
|
|
20
|
+
from vibe.core.types import LLMMessage, OutputFormat, Role
|
|
21
|
+
from vibe.core.utils import ConversationLimitException, logger
|
|
22
|
+
from vibe.setup.onboarding import run_onboarding
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_initial_agent_name(args: argparse.Namespace) -> str:
|
|
26
|
+
if args.prompt is not None and args.agent == BuiltinAgentName.DEFAULT:
|
|
27
|
+
return BuiltinAgentName.AUTO_APPROVE
|
|
28
|
+
return args.agent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_prompt_from_stdin() -> str | None:
|
|
32
|
+
if sys.stdin.isatty():
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
if content := sys.stdin.read().strip():
|
|
36
|
+
sys.stdin = sys.__stdin__ = open("/dev/tty")
|
|
37
|
+
return content
|
|
38
|
+
except KeyboardInterrupt:
|
|
39
|
+
pass
|
|
40
|
+
except OSError:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_config_or_exit() -> VibeConfig:
|
|
47
|
+
try:
|
|
48
|
+
return VibeConfig.load()
|
|
49
|
+
except MissingAPIKeyError:
|
|
50
|
+
run_onboarding()
|
|
51
|
+
return VibeConfig.load()
|
|
52
|
+
except MissingPromptFileError as e:
|
|
53
|
+
rprint(f"[yellow]Invalid system prompt id: {e}[/]")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
except ValueError as e:
|
|
56
|
+
rprint(f"[yellow]{e}[/]")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def bootstrap_config_files() -> None:
|
|
61
|
+
if not CONFIG_FILE.path.exists():
|
|
62
|
+
try:
|
|
63
|
+
VibeConfig.save_updates(VibeConfig.create_default())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
rprint(f"[yellow]Could not create default config file: {e}[/]")
|
|
66
|
+
|
|
67
|
+
if not HISTORY_FILE.path.exists():
|
|
68
|
+
try:
|
|
69
|
+
HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
rprint(f"[yellow]Could not create history file: {e}[/]")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_session(
|
|
76
|
+
args: argparse.Namespace, config: VibeConfig
|
|
77
|
+
) -> list[LLMMessage] | None:
|
|
78
|
+
if not args.continue_session and not args.resume:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
if not config.session_logging.enabled:
|
|
82
|
+
rprint(
|
|
83
|
+
"[red]Session logging is disabled. "
|
|
84
|
+
"Enable it in config to use --continue or --resume[/]"
|
|
85
|
+
)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
session_to_load = None
|
|
89
|
+
if args.continue_session:
|
|
90
|
+
session_to_load = SessionLoader.find_latest_session(config.session_logging)
|
|
91
|
+
if not session_to_load:
|
|
92
|
+
rprint(
|
|
93
|
+
f"[red]No previous sessions found in "
|
|
94
|
+
f"{config.session_logging.save_dir}[/]"
|
|
95
|
+
)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
else:
|
|
98
|
+
session_to_load = SessionLoader.find_session_by_id(
|
|
99
|
+
args.resume, config.session_logging
|
|
100
|
+
)
|
|
101
|
+
if not session_to_load:
|
|
102
|
+
rprint(
|
|
103
|
+
f"[red]Session '{args.resume}' not found in "
|
|
104
|
+
f"{config.session_logging.save_dir}[/]"
|
|
105
|
+
)
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
loaded_messages, _ = SessionLoader.load_session(session_to_load)
|
|
110
|
+
return loaded_messages
|
|
111
|
+
except Exception as e:
|
|
112
|
+
rprint(f"[red]Failed to load session: {e}[/]")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _load_messages_from_previous_session(
|
|
117
|
+
agent_loop: AgentLoop, loaded_messages: list[LLMMessage]
|
|
118
|
+
) -> None:
|
|
119
|
+
non_system_messages = [msg for msg in loaded_messages if msg.role != Role.system]
|
|
120
|
+
agent_loop.messages.extend(non_system_messages)
|
|
121
|
+
logger.info("Loaded %d messages from previous session", len(non_system_messages))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_cli(args: argparse.Namespace) -> None:
|
|
125
|
+
load_dotenv_values()
|
|
126
|
+
bootstrap_config_files()
|
|
127
|
+
|
|
128
|
+
if args.setup:
|
|
129
|
+
run_onboarding()
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
initial_agent_name = get_initial_agent_name(args)
|
|
134
|
+
config = load_config_or_exit()
|
|
135
|
+
|
|
136
|
+
if args.enabled_tools:
|
|
137
|
+
config.enabled_tools = args.enabled_tools
|
|
138
|
+
|
|
139
|
+
loaded_messages = load_session(args, config)
|
|
140
|
+
|
|
141
|
+
stdin_prompt = get_prompt_from_stdin()
|
|
142
|
+
if args.prompt is not None:
|
|
143
|
+
programmatic_prompt = args.prompt or stdin_prompt
|
|
144
|
+
if not programmatic_prompt:
|
|
145
|
+
print(
|
|
146
|
+
"Error: No prompt provided for programmatic mode", file=sys.stderr
|
|
147
|
+
)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
output_format = OutputFormat(
|
|
150
|
+
args.output if hasattr(args, "output") else "text"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
final_response = run_programmatic(
|
|
155
|
+
config=config,
|
|
156
|
+
prompt=programmatic_prompt,
|
|
157
|
+
max_turns=args.max_turns,
|
|
158
|
+
max_price=args.max_price,
|
|
159
|
+
output_format=output_format,
|
|
160
|
+
previous_messages=loaded_messages,
|
|
161
|
+
agent_name=initial_agent_name,
|
|
162
|
+
)
|
|
163
|
+
if final_response:
|
|
164
|
+
print(final_response)
|
|
165
|
+
sys.exit(0)
|
|
166
|
+
except ConversationLimitException as e:
|
|
167
|
+
print(e, file=sys.stderr)
|
|
168
|
+
sys.exit(1)
|
|
169
|
+
except RuntimeError as e:
|
|
170
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
else:
|
|
173
|
+
agent_loop = AgentLoop(
|
|
174
|
+
config, agent_name=initial_agent_name, enable_streaming=True
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if loaded_messages:
|
|
178
|
+
_load_messages_from_previous_session(agent_loop, loaded_messages)
|
|
179
|
+
|
|
180
|
+
run_textual_ui(
|
|
181
|
+
agent_loop=agent_loop,
|
|
182
|
+
initial_prompt=args.initial_prompt or stdin_prompt,
|
|
183
|
+
teleport_on_start=args.teleport,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
except (KeyboardInterrupt, EOFError):
|
|
187
|
+
rprint("\n[dim]Bye![/]")
|
|
188
|
+
sys.exit(0)
|
vibe/cli/clipboard.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from textual.app import App
|
|
7
|
+
|
|
8
|
+
_PREVIEW_MAX_LENGTH = 40
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _copy_osc52(text: str) -> None:
|
|
12
|
+
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
|
|
13
|
+
osc52_seq = f"\033]52;c;{encoded}\a"
|
|
14
|
+
if os.environ.get("TMUX"):
|
|
15
|
+
osc52_seq = f"\033Ptmux;\033{osc52_seq}\033\\"
|
|
16
|
+
|
|
17
|
+
with open("/dev/tty", "w") as tty:
|
|
18
|
+
tty.write(osc52_seq)
|
|
19
|
+
tty.flush()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _shorten_preview(texts: list[str]) -> str:
|
|
23
|
+
dense_text = "⏎".join(texts).replace("\n", "⏎")
|
|
24
|
+
if len(dense_text) > _PREVIEW_MAX_LENGTH:
|
|
25
|
+
return f"{dense_text[: _PREVIEW_MAX_LENGTH - 1]}…"
|
|
26
|
+
return dense_text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> str | None:
|
|
30
|
+
selected_texts = []
|
|
31
|
+
|
|
32
|
+
for widget in app.query("*"):
|
|
33
|
+
if not hasattr(widget, "text_selection") or not widget.text_selection:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
selection = widget.text_selection
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
result = widget.get_selection(selection)
|
|
40
|
+
except Exception:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
if not result:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
selected_text, _ = result
|
|
47
|
+
if selected_text.strip():
|
|
48
|
+
selected_texts.append(selected_text)
|
|
49
|
+
|
|
50
|
+
if not selected_texts:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
combined_text = "\n".join(selected_texts)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
_copy_osc52(combined_text)
|
|
57
|
+
if show_toast:
|
|
58
|
+
app.notify(
|
|
59
|
+
f'"{_shorten_preview(selected_texts)}" copied to clipboard',
|
|
60
|
+
severity="information",
|
|
61
|
+
timeout=2,
|
|
62
|
+
markup=False,
|
|
63
|
+
)
|
|
64
|
+
return combined_text
|
|
65
|
+
except Exception:
|
|
66
|
+
app.notify(
|
|
67
|
+
"Failed to copy - clipboard not available", severity="warning", timeout=3
|
|
68
|
+
)
|
|
69
|
+
return None
|
vibe/cli/commands.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Command:
|
|
8
|
+
aliases: frozenset[str]
|
|
9
|
+
description: str
|
|
10
|
+
handler: str
|
|
11
|
+
exits: bool = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandRegistry:
|
|
15
|
+
def __init__(self, excluded_commands: list[str] | None = None) -> None:
|
|
16
|
+
if excluded_commands is None:
|
|
17
|
+
excluded_commands = []
|
|
18
|
+
self.commands = {
|
|
19
|
+
"help": Command(
|
|
20
|
+
aliases=frozenset(["/help"]),
|
|
21
|
+
description="Show help message",
|
|
22
|
+
handler="_show_help",
|
|
23
|
+
),
|
|
24
|
+
"config": Command(
|
|
25
|
+
aliases=frozenset(["/config", "/model"]),
|
|
26
|
+
description="Edit config settings",
|
|
27
|
+
handler="_show_config",
|
|
28
|
+
),
|
|
29
|
+
"reload": Command(
|
|
30
|
+
aliases=frozenset(["/reload"]),
|
|
31
|
+
description="Reload configuration from disk",
|
|
32
|
+
handler="_reload_config",
|
|
33
|
+
),
|
|
34
|
+
"clear": Command(
|
|
35
|
+
aliases=frozenset(["/clear"]),
|
|
36
|
+
description="Clear conversation history",
|
|
37
|
+
handler="_clear_history",
|
|
38
|
+
),
|
|
39
|
+
"log": Command(
|
|
40
|
+
aliases=frozenset(["/log"]),
|
|
41
|
+
description="Show path to current interaction log file",
|
|
42
|
+
handler="_show_log_path",
|
|
43
|
+
),
|
|
44
|
+
"compact": Command(
|
|
45
|
+
aliases=frozenset(["/compact"]),
|
|
46
|
+
description="Compact conversation history by summarizing",
|
|
47
|
+
handler="_compact_history",
|
|
48
|
+
),
|
|
49
|
+
"exit": Command(
|
|
50
|
+
aliases=frozenset(["/exit"]),
|
|
51
|
+
description="Exit the application",
|
|
52
|
+
handler="_exit_app",
|
|
53
|
+
exits=True,
|
|
54
|
+
),
|
|
55
|
+
"terminal-setup": Command(
|
|
56
|
+
aliases=frozenset(["/terminal-setup"]),
|
|
57
|
+
description="Configure Shift+Enter for newlines",
|
|
58
|
+
handler="_setup_terminal",
|
|
59
|
+
),
|
|
60
|
+
"status": Command(
|
|
61
|
+
aliases=frozenset(["/status"]),
|
|
62
|
+
description="Display agent statistics",
|
|
63
|
+
handler="_show_status",
|
|
64
|
+
),
|
|
65
|
+
"teleport": Command(
|
|
66
|
+
aliases=frozenset(["/teleport"]),
|
|
67
|
+
description="Teleport session to Vibe Nuage",
|
|
68
|
+
handler="_teleport_command",
|
|
69
|
+
),
|
|
70
|
+
"proxy-setup": Command(
|
|
71
|
+
aliases=frozenset(["/proxy-setup"]),
|
|
72
|
+
description="Configure proxy and SSL certificate settings",
|
|
73
|
+
handler="_show_proxy_setup",
|
|
74
|
+
),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for command in excluded_commands:
|
|
78
|
+
self.commands.pop(command, None)
|
|
79
|
+
|
|
80
|
+
self._alias_map = {}
|
|
81
|
+
for cmd_name, cmd in self.commands.items():
|
|
82
|
+
for alias in cmd.aliases:
|
|
83
|
+
self._alias_map[alias] = cmd_name
|
|
84
|
+
|
|
85
|
+
def find_command(self, user_input: str) -> Command | None:
|
|
86
|
+
cmd_name = self.get_command_name(user_input)
|
|
87
|
+
return self.commands.get(cmd_name) if cmd_name else None
|
|
88
|
+
|
|
89
|
+
def get_command_name(self, user_input: str) -> str | None:
|
|
90
|
+
return self._alias_map.get(user_input.lower().strip())
|
|
91
|
+
|
|
92
|
+
def get_help_text(self) -> str:
|
|
93
|
+
lines: list[str] = [
|
|
94
|
+
"### Keyboard Shortcuts",
|
|
95
|
+
"",
|
|
96
|
+
"- `Enter` Submit message",
|
|
97
|
+
"- `Ctrl+J` / `Shift+Enter` Insert newline",
|
|
98
|
+
"- `Escape` Interrupt agent or close dialogs",
|
|
99
|
+
"- `Ctrl+C` Quit (or clear input if text present)",
|
|
100
|
+
"- `Ctrl+G` Edit input in external editor",
|
|
101
|
+
"- `Ctrl+O` Toggle tool output view",
|
|
102
|
+
"- `Shift+Tab` Toggle auto-approve mode",
|
|
103
|
+
"",
|
|
104
|
+
"### Special Features",
|
|
105
|
+
"",
|
|
106
|
+
"- `!<command>` Execute bash command directly",
|
|
107
|
+
"- `@path/to/file/` Autocompletes file paths",
|
|
108
|
+
"",
|
|
109
|
+
"### Commands",
|
|
110
|
+
"",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for cmd in self.commands.values():
|
|
114
|
+
aliases = ", ".join(f"`{alias}`" for alias in sorted(cmd.aliases))
|
|
115
|
+
lines.append(f"- {aliases}: {cmd.description}")
|
|
116
|
+
return "\n".join(lines)
|
vibe/cli/entrypoint.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from rich import print as rprint
|
|
9
|
+
|
|
10
|
+
from vibe import __version__
|
|
11
|
+
from vibe.core.agents.models import BuiltinAgentName
|
|
12
|
+
from vibe.core.paths.config_paths import unlock_config_paths
|
|
13
|
+
from vibe.core.trusted_folders import has_trustable_content, trusted_folders_manager
|
|
14
|
+
from vibe.setup.trusted_folders.trust_folder_dialog import (
|
|
15
|
+
TrustDialogQuitException,
|
|
16
|
+
ask_trust_folder,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_arguments() -> argparse.Namespace:
|
|
21
|
+
parser = argparse.ArgumentParser(description="Run the codeMaster interactive CLI")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"initial_prompt",
|
|
27
|
+
nargs="?",
|
|
28
|
+
metavar="PROMPT",
|
|
29
|
+
help="Initial prompt to start the interactive session with.",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-p",
|
|
33
|
+
"--prompt",
|
|
34
|
+
nargs="?",
|
|
35
|
+
const="",
|
|
36
|
+
metavar="TEXT",
|
|
37
|
+
help="Run in programmatic mode: send prompt, auto-approve all tools, "
|
|
38
|
+
"output response, and exit.",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--max-turns",
|
|
42
|
+
type=int,
|
|
43
|
+
metavar="N",
|
|
44
|
+
help="Maximum number of assistant turns "
|
|
45
|
+
"(only applies in programmatic mode with -p).",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--max-price",
|
|
49
|
+
type=float,
|
|
50
|
+
metavar="DOLLARS",
|
|
51
|
+
help="Maximum cost in dollars (only applies in programmatic mode with -p). "
|
|
52
|
+
"Session will be interrupted if cost exceeds this limit.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--enabled-tools",
|
|
56
|
+
action="append",
|
|
57
|
+
metavar="TOOL",
|
|
58
|
+
help="Enable specific tools. In programmatic mode (-p), this disables "
|
|
59
|
+
"all other tools. "
|
|
60
|
+
"Can use exact names, glob patterns (e.g., 'bash*'), or "
|
|
61
|
+
"regex with 're:' prefix. Can be specified multiple times.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--output",
|
|
65
|
+
type=str,
|
|
66
|
+
choices=["text", "json", "streaming"],
|
|
67
|
+
default="text",
|
|
68
|
+
help="Output format for programmatic mode (-p): 'text' "
|
|
69
|
+
"for human-readable (default), 'json' for all messages at end, "
|
|
70
|
+
"'streaming' for newline-delimited JSON per message.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--agent",
|
|
74
|
+
metavar="NAME",
|
|
75
|
+
default=BuiltinAgentName.DEFAULT,
|
|
76
|
+
help="Agent to use (builtin: default, plan, accept-edits, auto-approve, "
|
|
77
|
+
"or custom from ~/.vibe/agents/NAME.toml)",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument("--setup", action="store_true", help="Setup API key and exit")
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--workdir",
|
|
82
|
+
type=Path,
|
|
83
|
+
metavar="DIR",
|
|
84
|
+
help="Change to this directory before running",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Feature flag for teleport, not exposed to the user yet
|
|
88
|
+
parser.add_argument("--teleport", action="store_true", help=argparse.SUPPRESS)
|
|
89
|
+
|
|
90
|
+
continuation_group = parser.add_mutually_exclusive_group()
|
|
91
|
+
continuation_group.add_argument(
|
|
92
|
+
"-c",
|
|
93
|
+
"--continue",
|
|
94
|
+
action="store_true",
|
|
95
|
+
dest="continue_session",
|
|
96
|
+
help="Continue from the most recent saved session",
|
|
97
|
+
)
|
|
98
|
+
continuation_group.add_argument(
|
|
99
|
+
"--resume",
|
|
100
|
+
metavar="SESSION_ID",
|
|
101
|
+
help="Resume a specific session by its ID (supports partial matching)",
|
|
102
|
+
)
|
|
103
|
+
return parser.parse_args()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_and_resolve_trusted_folder() -> None:
|
|
107
|
+
try:
|
|
108
|
+
cwd = Path.cwd()
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
rprint(
|
|
111
|
+
"[red]Error: Current working directory no longer exists.[/]\n"
|
|
112
|
+
"[yellow]The directory you started vibe from has been deleted. "
|
|
113
|
+
"Please change to an existing directory and try again, "
|
|
114
|
+
"or use --workdir to specify a working directory.[/]"
|
|
115
|
+
)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
if not has_trustable_content(cwd) or cwd.resolve() == Path.home().resolve():
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
is_folder_trusted = trusted_folders_manager.is_trusted(cwd)
|
|
122
|
+
|
|
123
|
+
if is_folder_trusted is not None:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
is_folder_trusted = ask_trust_folder(cwd)
|
|
128
|
+
except (KeyboardInterrupt, EOFError, TrustDialogQuitException):
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
rprint(f"[yellow]Error showing trust dialog: {e}[/]")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if is_folder_trusted is True:
|
|
135
|
+
trusted_folders_manager.add_trusted(cwd)
|
|
136
|
+
elif is_folder_trusted is False:
|
|
137
|
+
trusted_folders_manager.add_untrusted(cwd)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main() -> None:
|
|
141
|
+
args = parse_arguments()
|
|
142
|
+
|
|
143
|
+
if args.workdir:
|
|
144
|
+
workdir = args.workdir.expanduser().resolve()
|
|
145
|
+
if not workdir.is_dir():
|
|
146
|
+
rprint(
|
|
147
|
+
f"[red]Error: --workdir does not exist or is not a directory: {workdir}[/]"
|
|
148
|
+
)
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
os.chdir(workdir)
|
|
151
|
+
|
|
152
|
+
is_interactive = args.prompt is None
|
|
153
|
+
if is_interactive:
|
|
154
|
+
check_and_resolve_trusted_folder()
|
|
155
|
+
unlock_config_paths()
|
|
156
|
+
|
|
157
|
+
from vibe.cli.cli import run_cli
|
|
158
|
+
|
|
159
|
+
run_cli(args)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HistoryManager:
|
|
8
|
+
def __init__(self, history_file: Path, max_entries: int = 100) -> None:
|
|
9
|
+
self.history_file = history_file
|
|
10
|
+
self.max_entries = max_entries
|
|
11
|
+
self._entries: list[str] = []
|
|
12
|
+
self._current_index: int = -1
|
|
13
|
+
self._temp_input: str = ""
|
|
14
|
+
self._load_history()
|
|
15
|
+
|
|
16
|
+
def _load_history(self) -> None:
|
|
17
|
+
if not self.history_file.exists():
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
with self.history_file.open("r", encoding="utf-8") as f:
|
|
22
|
+
entries = []
|
|
23
|
+
for raw_line in f:
|
|
24
|
+
raw_line = raw_line.rstrip("\n\r")
|
|
25
|
+
if not raw_line:
|
|
26
|
+
continue
|
|
27
|
+
try:
|
|
28
|
+
entry = json.loads(raw_line)
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
entry = raw_line
|
|
31
|
+
entries.append(entry if isinstance(entry, str) else str(entry))
|
|
32
|
+
self._entries = entries[-self.max_entries :]
|
|
33
|
+
except (OSError, UnicodeDecodeError):
|
|
34
|
+
self._entries = []
|
|
35
|
+
|
|
36
|
+
def _save_history(self) -> None:
|
|
37
|
+
try:
|
|
38
|
+
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
with self.history_file.open("w", encoding="utf-8") as f:
|
|
40
|
+
for entry in self._entries:
|
|
41
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def add(self, text: str) -> None:
|
|
46
|
+
text = text.strip()
|
|
47
|
+
if not text or text.startswith("/"):
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if self._entries and self._entries[-1] == text:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
self._entries.append(text)
|
|
54
|
+
|
|
55
|
+
if len(self._entries) > self.max_entries:
|
|
56
|
+
self._entries = self._entries[-self.max_entries :]
|
|
57
|
+
|
|
58
|
+
self._save_history()
|
|
59
|
+
self.reset_navigation()
|
|
60
|
+
|
|
61
|
+
def get_previous(self, current_input: str, prefix: str = "") -> str | None:
|
|
62
|
+
if not self._entries:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if self._current_index == -1:
|
|
66
|
+
self._temp_input = current_input
|
|
67
|
+
self._current_index = len(self._entries)
|
|
68
|
+
|
|
69
|
+
for i in range(self._current_index - 1, -1, -1):
|
|
70
|
+
if self._entries[i].startswith(prefix):
|
|
71
|
+
self._current_index = i
|
|
72
|
+
return self._entries[i]
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def get_next(self, prefix: str = "") -> str | None:
|
|
77
|
+
if self._current_index == -1:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
for i in range(self._current_index + 1, len(self._entries)):
|
|
81
|
+
if self._entries[i].startswith(prefix):
|
|
82
|
+
self._current_index = i
|
|
83
|
+
return self._entries[i]
|
|
84
|
+
|
|
85
|
+
result = self._temp_input
|
|
86
|
+
self.reset_navigation()
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def reset_navigation(self) -> None:
|
|
90
|
+
self._current_index = -1
|
|
91
|
+
self._temp_input = ""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from vibe.cli.plan_offer.ports.whoami_gateway import (
|
|
9
|
+
WhoAmIGatewayError,
|
|
10
|
+
WhoAmIGatewayUnauthorized,
|
|
11
|
+
WhoAmIResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
BASE_URL = "https://console.mistral.ai"
|
|
15
|
+
WHOAMI_PATH = "/api/vibe/whoami"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HttpWhoAmIGateway:
|
|
19
|
+
def __init__(self, base_url: str = BASE_URL) -> None:
|
|
20
|
+
self._base_url = base_url.rstrip("/")
|
|
21
|
+
|
|
22
|
+
async def whoami(self, api_key: str) -> WhoAmIResponse:
|
|
23
|
+
url = f"{self._base_url}{WHOAMI_PATH}"
|
|
24
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
25
|
+
try:
|
|
26
|
+
async with httpx.AsyncClient() as client:
|
|
27
|
+
response = await client.get(url, headers=headers)
|
|
28
|
+
except httpx.RequestError as exc:
|
|
29
|
+
raise WhoAmIGatewayError() from exc
|
|
30
|
+
|
|
31
|
+
if response.status_code in {httpx.codes.UNAUTHORIZED, httpx.codes.FORBIDDEN}:
|
|
32
|
+
raise WhoAmIGatewayUnauthorized()
|
|
33
|
+
if not response.is_success:
|
|
34
|
+
raise WhoAmIGatewayError(f"Unexpected status {response.status_code}")
|
|
35
|
+
|
|
36
|
+
payload = _safe_json(response) or {}
|
|
37
|
+
return WhoAmIResponse(
|
|
38
|
+
is_pro_plan=_parse_bool(payload.get("is_pro_plan")),
|
|
39
|
+
advertise_pro_plan=_parse_bool(payload.get("advertise_pro_plan")),
|
|
40
|
+
prompt_switching_to_pro_plan=_parse_bool(
|
|
41
|
+
payload.get("prompt_switching_to_pro_plan")
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _safe_json(response: httpx.Response) -> Mapping[str, object] | None:
|
|
47
|
+
try:
|
|
48
|
+
data = response.json()
|
|
49
|
+
except ValueError:
|
|
50
|
+
return None
|
|
51
|
+
return cast(Mapping[str, object], data) if isinstance(data, dict) else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_bool(value: object | None) -> bool:
|
|
55
|
+
if value is None:
|
|
56
|
+
return False
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
return value
|
|
59
|
+
if isinstance(value, str):
|
|
60
|
+
match value.strip().lower():
|
|
61
|
+
case "true":
|
|
62
|
+
return True
|
|
63
|
+
case "false":
|
|
64
|
+
return False
|
|
65
|
+
case _:
|
|
66
|
+
raise WhoAmIGatewayError("Invalid boolean string in whoami response")
|
|
67
|
+
raise WhoAmIGatewayError("Invalid boolean value in whoami response")
|