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/acp/entrypoint.py ADDED
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from dataclasses import dataclass
5
+ import os
6
+ import sys
7
+
8
+ from vibe import __version__
9
+ from vibe.core.config import VibeConfig
10
+ from vibe.core.paths.config_paths import CONFIG_FILE, HISTORY_FILE, unlock_config_paths
11
+ from vibe.core.utils import logger
12
+
13
+ # Configure line buffering for subprocess communication
14
+ sys.stdout.reconfigure(line_buffering=True) # pyright: ignore[reportAttributeAccessIssue]
15
+ sys.stderr.reconfigure(line_buffering=True) # pyright: ignore[reportAttributeAccessIssue]
16
+ sys.stdin.reconfigure(line_buffering=True) # pyright: ignore[reportAttributeAccessIssue]
17
+
18
+
19
+ @dataclass
20
+ class Arguments:
21
+ setup: bool
22
+
23
+
24
+ def parse_arguments() -> Arguments:
25
+ parser = argparse.ArgumentParser(description="Run codeMaster in ACP mode")
26
+ parser.add_argument(
27
+ "-v", "--version", action="version", version=f"%(prog)s {__version__}"
28
+ )
29
+ parser.add_argument("--setup", action="store_true", help="Setup API key and exit")
30
+ args = parser.parse_args()
31
+ return Arguments(setup=args.setup)
32
+
33
+
34
+ def bootstrap_config_files() -> None:
35
+ if not CONFIG_FILE.path.exists():
36
+ try:
37
+ VibeConfig.save_updates(VibeConfig.create_default())
38
+ except Exception as e:
39
+ logger.error(f"Could not create default config file: {e}")
40
+ raise
41
+
42
+ if not HISTORY_FILE.path.exists():
43
+ try:
44
+ HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True)
45
+ HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8")
46
+ except Exception as e:
47
+ logger.error(f"Could not create history file: {e}")
48
+ raise
49
+
50
+
51
+ def handle_debug_mode() -> None:
52
+ if os.environ.get("DEBUG_MODE") != "true":
53
+ return
54
+
55
+ try:
56
+ import debugpy
57
+ except ImportError:
58
+ return
59
+
60
+ debugpy.listen(("localhost", 5678))
61
+ # uncomment this to wait for the debugger to attach
62
+ # debugpy.wait_for_client()
63
+
64
+
65
+ def main() -> None:
66
+ handle_debug_mode()
67
+ unlock_config_paths()
68
+
69
+ from vibe.acp.acp_agent_loop import run_acp_server
70
+ from vibe.setup.onboarding import run_onboarding
71
+
72
+ bootstrap_config_files()
73
+ args = parse_arguments()
74
+ if args.setup:
75
+ run_onboarding()
76
+ sys.exit(0)
77
+ run_acp_server()
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
File without changes
vibe/acp/tools/base.py ADDED
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from typing import Annotated, Protocol, cast, runtime_checkable
5
+
6
+ from acp import Client
7
+ from acp.helpers import SessionUpdate, ToolCallContentVariant
8
+ from acp.schema import ToolCallProgress
9
+ from pydantic import BaseModel, ConfigDict, Field, SkipValidation
10
+
11
+ from vibe.core.tools.base import BaseTool, ToolError
12
+ from vibe.core.tools.manager import ToolManager
13
+ from vibe.core.types import ToolCallEvent, ToolResultEvent
14
+ from vibe.core.utils import logger
15
+
16
+
17
+ @runtime_checkable
18
+ class ToolCallSessionUpdateProtocol(Protocol):
19
+ @classmethod
20
+ def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None: ...
21
+
22
+
23
+ @runtime_checkable
24
+ class ToolResultSessionUpdateProtocol(Protocol):
25
+ @classmethod
26
+ def tool_result_session_update(
27
+ cls, event: ToolResultEvent
28
+ ) -> SessionUpdate | None: ...
29
+
30
+
31
+ class AcpToolState(BaseModel):
32
+ model_config = ConfigDict(arbitrary_types_allowed=True)
33
+
34
+ client: Annotated[Client | None, SkipValidation] = Field(
35
+ default=None, description="ACP Client"
36
+ )
37
+ session_id: str | None = Field(default=None, description="Current ACP session ID")
38
+ tool_call_id: str | None = Field(
39
+ default=None, description="Current ACP tool call ID"
40
+ )
41
+
42
+
43
+ class BaseAcpTool[ToolState: AcpToolState](BaseTool):
44
+ state: ToolState
45
+
46
+ @classmethod
47
+ def get_tool_instance(
48
+ cls, tool_name: str, tool_manager: ToolManager
49
+ ) -> BaseAcpTool[AcpToolState]:
50
+ return cast(BaseAcpTool[AcpToolState], tool_manager.get(tool_name))
51
+
52
+ @classmethod
53
+ def update_tool_state(
54
+ cls,
55
+ *,
56
+ tool_manager: ToolManager,
57
+ client: Client | None,
58
+ session_id: str | None,
59
+ tool_call_id: str | None,
60
+ ) -> None:
61
+ tool_instance = cls.get_tool_instance(cls.get_name(), tool_manager)
62
+ tool_instance.state.client = client
63
+ tool_instance.state.session_id = session_id
64
+ tool_instance.state.tool_call_id = tool_call_id
65
+
66
+ @classmethod
67
+ @abstractmethod
68
+ def _get_tool_state_class(cls) -> type[ToolState]: ...
69
+
70
+ def _load_state(self) -> tuple[Client, str, str | None]:
71
+ if self.state.client is None:
72
+ raise ToolError(
73
+ "Client not available in tool state. This tool can only be used within an ACP session."
74
+ )
75
+ if self.state.session_id is None:
76
+ raise ToolError(
77
+ "Session ID not available in tool state. This tool can only be used within an ACP session."
78
+ )
79
+
80
+ return self.state.client, self.state.session_id, self.state.tool_call_id
81
+
82
+ async def _send_in_progress_session_update(
83
+ self, content: list[ToolCallContentVariant] | None = None
84
+ ) -> None:
85
+ client, session_id, tool_call_id = self._load_state()
86
+ if tool_call_id is None:
87
+ return
88
+
89
+ try:
90
+ await client.session_update(
91
+ session_id=session_id,
92
+ update=ToolCallProgress(
93
+ session_update="tool_call_update",
94
+ tool_call_id=tool_call_id,
95
+ status="in_progress",
96
+ content=content,
97
+ ),
98
+ )
99
+ except Exception as e:
100
+ logger.error(f"Failed to update session: {e!r}")
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncGenerator
5
+ from pathlib import Path
6
+
7
+ from acp.schema import (
8
+ TerminalToolCallContent,
9
+ ToolCallProgress,
10
+ ToolCallStart,
11
+ WaitForTerminalExitResponse,
12
+ )
13
+
14
+ from vibe import VIBE_ROOT
15
+ from vibe.acp.tools.base import AcpToolState, BaseAcpTool
16
+ from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError
17
+ from vibe.core.tools.builtins.bash import Bash as CoreBashTool, BashArgs, BashResult
18
+ from vibe.core.types import ToolCallEvent, ToolResultEvent, ToolStreamEvent
19
+ from vibe.core.utils import logger
20
+
21
+
22
+ class AcpBashState(BaseToolState, AcpToolState):
23
+ pass
24
+
25
+
26
+ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
27
+ prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "bash.md"
28
+ state: AcpBashState
29
+
30
+ @classmethod
31
+ def _get_tool_state_class(cls) -> type[AcpBashState]:
32
+ return AcpBashState
33
+
34
+ async def run(
35
+ self, args: BashArgs, ctx: InvokeContext | None = None
36
+ ) -> AsyncGenerator[ToolStreamEvent | BashResult, None]:
37
+ client, session_id, _ = self._load_state()
38
+
39
+ timeout = args.timeout or self.config.default_timeout
40
+ max_bytes = self.config.max_output_bytes
41
+
42
+ try:
43
+ terminal = await client.create_terminal(
44
+ session_id=session_id,
45
+ command=args.command,
46
+ cwd=str(Path.cwd()),
47
+ output_byte_limit=max_bytes,
48
+ )
49
+ except Exception as e:
50
+ raise ToolError(f"Failed to create terminal: {e!r}") from e
51
+
52
+ terminal_id = terminal.terminal_id
53
+
54
+ await self._send_in_progress_session_update([
55
+ TerminalToolCallContent(type="terminal", terminal_id=terminal_id)
56
+ ])
57
+
58
+ try:
59
+ exit_response = await self._wait_for_terminal_exit(
60
+ terminal_id=terminal_id, timeout=timeout, command=args.command
61
+ )
62
+
63
+ output_response = await client.terminal_output(
64
+ session_id=session_id, terminal_id=terminal_id
65
+ )
66
+
67
+ yield self._build_result(
68
+ command=args.command,
69
+ stdout=output_response.output,
70
+ stderr="",
71
+ returncode=exit_response.exit_code or 0,
72
+ )
73
+
74
+ finally:
75
+ try:
76
+ await client.release_terminal(
77
+ session_id=session_id, terminal_id=terminal_id
78
+ )
79
+ except Exception as e:
80
+ logger.error(f"Failed to release terminal: {e!r}")
81
+
82
+ @classmethod
83
+ def get_summary(cls, args: BashArgs) -> str:
84
+ summary = f"{args.command}"
85
+ if args.timeout:
86
+ summary += f" (timeout {args.timeout}s)"
87
+
88
+ return summary
89
+
90
+ async def _wait_for_terminal_exit(
91
+ self, terminal_id: str, timeout: int, command: str
92
+ ) -> WaitForTerminalExitResponse:
93
+ client, session_id, _ = self._load_state()
94
+
95
+ try:
96
+ return await asyncio.wait_for(
97
+ client.wait_for_terminal_exit(
98
+ session_id=session_id, terminal_id=terminal_id
99
+ ),
100
+ timeout=timeout,
101
+ )
102
+ except TimeoutError:
103
+ try:
104
+ await client.kill_terminal(
105
+ session_id=session_id, terminal_id=terminal_id
106
+ )
107
+ except Exception as e:
108
+ logger.error(f"Failed to kill terminal: {e!r}")
109
+
110
+ raise self._build_timeout_error(command, timeout)
111
+
112
+ @classmethod
113
+ def tool_call_session_update(cls, event: ToolCallEvent) -> ToolCallStart:
114
+ if not isinstance(event.args, BashArgs):
115
+ raise ValueError(f"Unexpected tool args: {event.args}")
116
+
117
+ return ToolCallStart(
118
+ session_update="tool_call",
119
+ title=Bash.get_summary(event.args),
120
+ content=None,
121
+ tool_call_id=event.tool_call_id,
122
+ kind="execute",
123
+ raw_input=event.args.model_dump_json(),
124
+ )
125
+
126
+ @classmethod
127
+ def tool_result_session_update(
128
+ cls, event: ToolResultEvent
129
+ ) -> ToolCallProgress | None:
130
+ return ToolCallProgress(
131
+ session_update="tool_call_update",
132
+ tool_call_id=event.tool_call_id,
133
+ status="failed" if event.error else "completed",
134
+ )
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from vibe import VIBE_ROOT
6
+ from vibe.acp.tools.base import AcpToolState, BaseAcpTool
7
+ from vibe.core.tools.base import ToolError
8
+ from vibe.core.tools.builtins.read_file import (
9
+ ReadFile as CoreReadFileTool,
10
+ ReadFileArgs,
11
+ ReadFileResult,
12
+ ReadFileState,
13
+ _ReadResult,
14
+ )
15
+
16
+ ReadFileResult = ReadFileResult
17
+
18
+
19
+ class AcpReadFileState(ReadFileState, AcpToolState):
20
+ pass
21
+
22
+
23
+ class ReadFile(CoreReadFileTool, BaseAcpTool[AcpReadFileState]):
24
+ state: AcpReadFileState
25
+ prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "read_file.md"
26
+
27
+ @classmethod
28
+ def _get_tool_state_class(cls) -> type[AcpReadFileState]:
29
+ return AcpReadFileState
30
+
31
+ async def _read_file(self, args: ReadFileArgs, file_path: Path) -> _ReadResult:
32
+ client, session_id, _ = self._load_state()
33
+
34
+ line = args.offset + 1 if args.offset > 0 else None
35
+ limit = args.limit
36
+
37
+ await self._send_in_progress_session_update()
38
+
39
+ try:
40
+ response = await client.read_text_file(
41
+ session_id=session_id, path=str(file_path), line=line, limit=limit
42
+ )
43
+ except Exception as e:
44
+ raise ToolError(f"Error reading {file_path}: {e}") from e
45
+
46
+ content_lines = response.content.splitlines(keepends=True)
47
+ lines_read = len(content_lines)
48
+ bytes_read = sum(len(line.encode("utf-8")) for line in content_lines)
49
+
50
+ was_truncated = args.limit is not None and lines_read >= args.limit
51
+
52
+ return _ReadResult(
53
+ lines=content_lines, bytes_read=bytes_read, was_truncated=was_truncated
54
+ )
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from acp.helpers import SessionUpdate
6
+ from acp.schema import (
7
+ FileEditToolCallContent,
8
+ ToolCallLocation,
9
+ ToolCallProgress,
10
+ ToolCallStart,
11
+ )
12
+
13
+ from vibe import VIBE_ROOT
14
+ from vibe.acp.tools.base import AcpToolState, BaseAcpTool
15
+ from vibe.core.tools.base import ToolError
16
+ from vibe.core.tools.builtins.search_replace import (
17
+ SearchReplace as CoreSearchReplaceTool,
18
+ SearchReplaceArgs,
19
+ SearchReplaceResult,
20
+ SearchReplaceState,
21
+ )
22
+ from vibe.core.types import ToolCallEvent, ToolResultEvent
23
+
24
+
25
+ class AcpSearchReplaceState(SearchReplaceState, AcpToolState):
26
+ file_backup_content: str | None = None
27
+
28
+
29
+ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
30
+ state: AcpSearchReplaceState
31
+ prompt_path = (
32
+ VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "search_replace.md"
33
+ )
34
+
35
+ @classmethod
36
+ def _get_tool_state_class(cls) -> type[AcpSearchReplaceState]:
37
+ return AcpSearchReplaceState
38
+
39
+ async def _read_file(self, file_path: Path) -> str:
40
+ client, session_id, _ = self._load_state()
41
+
42
+ await self._send_in_progress_session_update()
43
+
44
+ try:
45
+ response = await client.read_text_file(
46
+ session_id=session_id, path=str(file_path)
47
+ )
48
+ except Exception as e:
49
+ raise ToolError(f"Unexpected error reading {file_path}: {e}") from e
50
+
51
+ self.state.file_backup_content = response.content
52
+ return response.content
53
+
54
+ async def _backup_file(self, file_path: Path) -> None:
55
+ if self.state.file_backup_content is None:
56
+ return
57
+
58
+ await self._write_file(
59
+ file_path.with_suffix(file_path.suffix + ".bak"),
60
+ self.state.file_backup_content,
61
+ )
62
+
63
+ async def _write_file(self, file_path: Path, content: str) -> None:
64
+ client, session_id, _ = self._load_state()
65
+
66
+ try:
67
+ await client.write_text_file(
68
+ session_id=session_id, path=str(file_path), content=content
69
+ )
70
+ except Exception as e:
71
+ raise ToolError(f"Error writing {file_path}: {e}") from e
72
+
73
+ @classmethod
74
+ def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
75
+ args = event.args
76
+ if not isinstance(args, SearchReplaceArgs):
77
+ return None
78
+
79
+ blocks = cls._parse_search_replace_blocks(args.content)
80
+
81
+ return ToolCallStart(
82
+ session_update="tool_call",
83
+ title=cls.get_call_display(event).summary,
84
+ tool_call_id=event.tool_call_id,
85
+ kind="edit",
86
+ content=[
87
+ FileEditToolCallContent(
88
+ type="diff",
89
+ path=args.file_path,
90
+ old_text=block.search,
91
+ new_text=block.replace,
92
+ )
93
+ for block in blocks
94
+ ],
95
+ locations=[ToolCallLocation(path=args.file_path)],
96
+ raw_input=args.model_dump_json(),
97
+ )
98
+
99
+ @classmethod
100
+ def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
101
+ if event.error:
102
+ return ToolCallProgress(
103
+ session_update="tool_call_update",
104
+ tool_call_id=event.tool_call_id,
105
+ status="failed",
106
+ )
107
+
108
+ result = event.result
109
+ if not isinstance(result, SearchReplaceResult):
110
+ return None
111
+
112
+ blocks = cls._parse_search_replace_blocks(result.content)
113
+
114
+ return ToolCallProgress(
115
+ session_update="tool_call_update",
116
+ tool_call_id=event.tool_call_id,
117
+ status="completed",
118
+ content=[
119
+ FileEditToolCallContent(
120
+ type="diff",
121
+ path=result.file,
122
+ old_text=block.search,
123
+ new_text=block.replace,
124
+ )
125
+ for block in blocks
126
+ ],
127
+ locations=[ToolCallLocation(path=result.file)],
128
+ raw_output=result.model_dump_json(),
129
+ )
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import cast
4
+
5
+ from acp.helpers import SessionUpdate
6
+ from acp.schema import AgentPlanUpdate, PlanEntry, PlanEntryPriority, PlanEntryStatus
7
+
8
+ from vibe import VIBE_ROOT
9
+ from vibe.acp.tools.base import AcpToolState, BaseAcpTool
10
+ from vibe.core.tools.builtins.todo import (
11
+ Todo as CoreTodoTool,
12
+ TodoArgs,
13
+ TodoPriority,
14
+ TodoResult,
15
+ TodoState,
16
+ TodoStatus,
17
+ )
18
+ from vibe.core.types import ToolCallEvent, ToolResultEvent
19
+
20
+ TodoArgs = TodoArgs
21
+
22
+
23
+ class AcpTodoState(TodoState, AcpToolState):
24
+ pass
25
+
26
+
27
+ class Todo(CoreTodoTool, BaseAcpTool[AcpTodoState]):
28
+ state: AcpTodoState
29
+ prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "todo.md"
30
+
31
+ @classmethod
32
+ def _get_tool_state_class(cls) -> type[AcpTodoState]:
33
+ return AcpTodoState
34
+
35
+ @classmethod
36
+ def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
37
+ return None
38
+
39
+ @classmethod
40
+ def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
41
+ result = cast(TodoResult, event.result)
42
+ todos = [todo for todo in result.todos if todo.status != TodoStatus.CANCELLED]
43
+ matched_status: dict[TodoStatus, PlanEntryStatus] = {
44
+ TodoStatus.PENDING: "pending",
45
+ TodoStatus.IN_PROGRESS: "in_progress",
46
+ TodoStatus.COMPLETED: "completed",
47
+ }
48
+ matched_priority: dict[TodoPriority, PlanEntryPriority] = {
49
+ TodoPriority.LOW: "low",
50
+ TodoPriority.MEDIUM: "medium",
51
+ TodoPriority.HIGH: "high",
52
+ }
53
+
54
+ update = AgentPlanUpdate(
55
+ session_update="plan",
56
+ entries=[
57
+ PlanEntry(
58
+ content=todo.content,
59
+ status=matched_status[todo.status],
60
+ priority=matched_priority[todo.priority],
61
+ )
62
+ for todo in todos
63
+ ],
64
+ )
65
+ return update
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from acp.helpers import SessionUpdate
6
+ from acp.schema import (
7
+ FileEditToolCallContent,
8
+ ToolCallLocation,
9
+ ToolCallProgress,
10
+ ToolCallStart,
11
+ )
12
+
13
+ from vibe import VIBE_ROOT
14
+ from vibe.acp.tools.base import AcpToolState, BaseAcpTool
15
+ from vibe.core.tools.base import ToolError
16
+ from vibe.core.tools.builtins.write_file import (
17
+ WriteFile as CoreWriteFileTool,
18
+ WriteFileArgs,
19
+ WriteFileResult,
20
+ WriteFileState,
21
+ )
22
+ from vibe.core.types import ToolCallEvent, ToolResultEvent
23
+
24
+
25
+ class AcpWriteFileState(WriteFileState, AcpToolState):
26
+ pass
27
+
28
+
29
+ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]):
30
+ state: AcpWriteFileState
31
+ prompt_path = (
32
+ VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "write_file.md"
33
+ )
34
+
35
+ @classmethod
36
+ def _get_tool_state_class(cls) -> type[AcpWriteFileState]:
37
+ return AcpWriteFileState
38
+
39
+ async def _write_file(self, args: WriteFileArgs, file_path: Path) -> None:
40
+ client, session_id, _ = self._load_state()
41
+
42
+ await self._send_in_progress_session_update()
43
+
44
+ try:
45
+ await client.write_text_file(
46
+ session_id=session_id, path=str(file_path), content=args.content
47
+ )
48
+ except Exception as e:
49
+ raise ToolError(f"Error writing {file_path}: {e}") from e
50
+
51
+ @classmethod
52
+ def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
53
+ args = event.args
54
+ if not isinstance(args, WriteFileArgs):
55
+ return None
56
+
57
+ return ToolCallStart(
58
+ session_update="tool_call",
59
+ title=cls.get_call_display(event).summary,
60
+ tool_call_id=event.tool_call_id,
61
+ kind="edit",
62
+ content=[
63
+ FileEditToolCallContent(
64
+ type="diff", path=args.path, old_text=None, new_text=args.content
65
+ )
66
+ ],
67
+ locations=[ToolCallLocation(path=args.path)],
68
+ raw_input=args.model_dump_json(),
69
+ )
70
+
71
+ @classmethod
72
+ def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
73
+ if event.error:
74
+ return ToolCallProgress(
75
+ session_update="tool_call_update",
76
+ tool_call_id=event.tool_call_id,
77
+ status="failed",
78
+ )
79
+
80
+ result = event.result
81
+ if not isinstance(result, WriteFileResult):
82
+ return None
83
+
84
+ return ToolCallProgress(
85
+ session_update="tool_call_update",
86
+ tool_call_id=event.tool_call_id,
87
+ status="completed",
88
+ content=[
89
+ FileEditToolCallContent(
90
+ type="diff",
91
+ path=result.path,
92
+ old_text=None,
93
+ new_text=result.content,
94
+ )
95
+ ],
96
+ locations=[ToolCallLocation(path=result.path)],
97
+ raw_output=result.model_dump_json(),
98
+ )