codemaster-cli 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. codemaster_cli-2.2.0.dist-info/METADATA +645 -0
  2. codemaster_cli-2.2.0.dist-info/RECORD +170 -0
  3. codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
  4. codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +188 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +163 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +165 -0
  81. vibe/core/agents/models.py +122 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +673 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +32 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/project_context.md +8 -0
  120. vibe/core/prompts/tests.md +1 -0
  121. vibe/core/proxy_setup.py +65 -0
  122. vibe/core/session/session_loader.py +222 -0
  123. vibe/core/session/session_logger.py +318 -0
  124. vibe/core/session/session_migration.py +41 -0
  125. vibe/core/skills/__init__.py +7 -0
  126. vibe/core/skills/manager.py +132 -0
  127. vibe/core/skills/models.py +92 -0
  128. vibe/core/skills/parser.py +39 -0
  129. vibe/core/system_prompt.py +466 -0
  130. vibe/core/telemetry/__init__.py +0 -0
  131. vibe/core/telemetry/send.py +185 -0
  132. vibe/core/teleport/errors.py +9 -0
  133. vibe/core/teleport/git.py +196 -0
  134. vibe/core/teleport/nuage.py +180 -0
  135. vibe/core/teleport/teleport.py +208 -0
  136. vibe/core/teleport/types.py +54 -0
  137. vibe/core/tools/base.py +336 -0
  138. vibe/core/tools/builtins/ask_user_question.py +134 -0
  139. vibe/core/tools/builtins/bash.py +357 -0
  140. vibe/core/tools/builtins/grep.py +310 -0
  141. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  142. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  143. vibe/core/tools/builtins/prompts/bash.md +73 -0
  144. vibe/core/tools/builtins/prompts/grep.md +4 -0
  145. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  146. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  147. vibe/core/tools/builtins/prompts/task.md +24 -0
  148. vibe/core/tools/builtins/prompts/todo.md +199 -0
  149. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  150. vibe/core/tools/builtins/read_file.py +222 -0
  151. vibe/core/tools/builtins/search_replace.py +456 -0
  152. vibe/core/tools/builtins/task.py +154 -0
  153. vibe/core/tools/builtins/todo.py +134 -0
  154. vibe/core/tools/builtins/write_file.py +160 -0
  155. vibe/core/tools/manager.py +341 -0
  156. vibe/core/tools/mcp.py +397 -0
  157. vibe/core/tools/ui.py +68 -0
  158. vibe/core/trusted_folders.py +86 -0
  159. vibe/core/types.py +405 -0
  160. vibe/core/utils.py +396 -0
  161. vibe/setup/onboarding/__init__.py +39 -0
  162. vibe/setup/onboarding/base.py +14 -0
  163. vibe/setup/onboarding/onboarding.tcss +134 -0
  164. vibe/setup/onboarding/screens/__init__.py +5 -0
  165. vibe/setup/onboarding/screens/api_key.py +200 -0
  166. vibe/setup/onboarding/screens/provider_selection.py +87 -0
  167. vibe/setup/onboarding/screens/welcome.py +136 -0
  168. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  169. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  170. vibe/whats_new.md +5 -0
vibe/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")