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,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from vibe.core.agent_loop import AgentLoop
|
|
9
|
+
from vibe.core.agents.models import AgentType
|
|
10
|
+
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
|
11
|
+
from vibe.core.tools.base import (
|
|
12
|
+
BaseTool,
|
|
13
|
+
BaseToolConfig,
|
|
14
|
+
BaseToolState,
|
|
15
|
+
InvokeContext,
|
|
16
|
+
ToolError,
|
|
17
|
+
ToolPermission,
|
|
18
|
+
)
|
|
19
|
+
from vibe.core.tools.ui import (
|
|
20
|
+
ToolCallDisplay,
|
|
21
|
+
ToolResultDisplay,
|
|
22
|
+
ToolUIData,
|
|
23
|
+
ToolUIDataAdapter,
|
|
24
|
+
)
|
|
25
|
+
from vibe.core.types import (
|
|
26
|
+
AssistantEvent,
|
|
27
|
+
Role,
|
|
28
|
+
ToolCallEvent,
|
|
29
|
+
ToolResultEvent,
|
|
30
|
+
ToolStreamEvent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TaskArgs(BaseModel):
|
|
35
|
+
task: str = Field(description="The task to delegate to the subagent")
|
|
36
|
+
agent: str = Field(
|
|
37
|
+
default="explore",
|
|
38
|
+
description="Name of the agent profile to use (must be a subagent)",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TaskResult(BaseModel):
|
|
43
|
+
response: str = Field(description="The accumulated response from the subagent")
|
|
44
|
+
turns_used: int = Field(description="Number of turns the subagent used")
|
|
45
|
+
completed: bool = Field(description="Whether the task completed normally")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TaskToolConfig(BaseToolConfig):
|
|
49
|
+
permission: ToolPermission = ToolPermission.ASK
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Task(
|
|
53
|
+
BaseTool[TaskArgs, TaskResult, TaskToolConfig, BaseToolState],
|
|
54
|
+
ToolUIData[TaskArgs, TaskResult],
|
|
55
|
+
):
|
|
56
|
+
description: ClassVar[str] = (
|
|
57
|
+
"Delegate a task to a subagent for independent execution. "
|
|
58
|
+
"Useful for exploration, research, or parallel work that doesn't "
|
|
59
|
+
"require user interaction. The subagent runs in-memory without "
|
|
60
|
+
"saving interaction logs."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_call_display(cls, event: ToolCallEvent) -> ToolCallDisplay:
|
|
65
|
+
args = event.args
|
|
66
|
+
if isinstance(args, TaskArgs):
|
|
67
|
+
return ToolCallDisplay(summary=f"Running {args.agent} agent: {args.task}")
|
|
68
|
+
return ToolCallDisplay(summary="Running subagent")
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
|
|
72
|
+
result = event.result
|
|
73
|
+
if isinstance(result, TaskResult):
|
|
74
|
+
turn_word = "turn" if result.turns_used == 1 else "turns"
|
|
75
|
+
if not result.completed:
|
|
76
|
+
return ToolResultDisplay(
|
|
77
|
+
success=False,
|
|
78
|
+
message=f"Agent interrupted after {result.turns_used} {turn_word}",
|
|
79
|
+
)
|
|
80
|
+
return ToolResultDisplay(
|
|
81
|
+
success=True,
|
|
82
|
+
message=f"Agent completed in {result.turns_used} {turn_word}",
|
|
83
|
+
)
|
|
84
|
+
return ToolResultDisplay(success=True, message="Agent completed")
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def get_status_text(cls) -> str:
|
|
88
|
+
return "Running subagent"
|
|
89
|
+
|
|
90
|
+
async def run(
|
|
91
|
+
self, args: TaskArgs, ctx: InvokeContext | None = None
|
|
92
|
+
) -> AsyncGenerator[ToolStreamEvent | TaskResult, None]:
|
|
93
|
+
if not ctx or not ctx.agent_manager:
|
|
94
|
+
raise ToolError("Task tool requires agent_manager in context")
|
|
95
|
+
|
|
96
|
+
agent_manager = ctx.agent_manager
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
agent_profile = agent_manager.get_agent(args.agent)
|
|
100
|
+
except ValueError as e:
|
|
101
|
+
raise ToolError(f"Unknown agent: {args.agent}") from e
|
|
102
|
+
|
|
103
|
+
if agent_profile.agent_type != AgentType.SUBAGENT:
|
|
104
|
+
raise ToolError(
|
|
105
|
+
f"Agent '{args.agent}' is a {agent_profile.agent_type.value} agent. "
|
|
106
|
+
f"Only subagents can be used with the task tool. "
|
|
107
|
+
f"This is a security constraint to prevent recursive spawning."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
base_config = VibeConfig.load(
|
|
111
|
+
session_logging=SessionLoggingConfig(enabled=False)
|
|
112
|
+
)
|
|
113
|
+
subagent_loop = AgentLoop(config=base_config, agent_name=args.agent)
|
|
114
|
+
|
|
115
|
+
if ctx and ctx.approval_callback:
|
|
116
|
+
subagent_loop.set_approval_callback(ctx.approval_callback)
|
|
117
|
+
|
|
118
|
+
accumulated_response: list[str] = []
|
|
119
|
+
completed = True
|
|
120
|
+
try:
|
|
121
|
+
async for event in subagent_loop.act(args.task):
|
|
122
|
+
if isinstance(event, AssistantEvent) and event.content:
|
|
123
|
+
accumulated_response.append(event.content)
|
|
124
|
+
if event.stopped_by_middleware:
|
|
125
|
+
completed = False
|
|
126
|
+
elif isinstance(event, ToolResultEvent):
|
|
127
|
+
if event.skipped:
|
|
128
|
+
completed = False
|
|
129
|
+
elif event.result and event.tool_class:
|
|
130
|
+
adapter = ToolUIDataAdapter(event.tool_class)
|
|
131
|
+
display = adapter.get_result_display(event)
|
|
132
|
+
message = f"{event.tool_name}: {display.message}"
|
|
133
|
+
yield ToolStreamEvent(
|
|
134
|
+
tool_name=self.get_name(),
|
|
135
|
+
message=message,
|
|
136
|
+
tool_call_id=ctx.tool_call_id,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
turns_used = sum(
|
|
140
|
+
msg.role == Role.assistant for msg in subagent_loop.messages
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
completed = False
|
|
145
|
+
accumulated_response.append(f"\n[Subagent error: {e}]")
|
|
146
|
+
turns_used = sum(
|
|
147
|
+
msg.role == Role.assistant for msg in subagent_loop.messages
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
yield TaskResult(
|
|
151
|
+
response="".join(accumulated_response),
|
|
152
|
+
turns_used=turns_used,
|
|
153
|
+
completed=completed,
|
|
154
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from enum import StrEnum, auto
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from vibe.core.tools.base import (
|
|
10
|
+
BaseTool,
|
|
11
|
+
BaseToolConfig,
|
|
12
|
+
BaseToolState,
|
|
13
|
+
InvokeContext,
|
|
14
|
+
ToolError,
|
|
15
|
+
ToolPermission,
|
|
16
|
+
)
|
|
17
|
+
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
|
18
|
+
from vibe.core.types import ToolCallEvent, ToolResultEvent, ToolStreamEvent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TodoStatus(StrEnum):
|
|
22
|
+
PENDING = auto()
|
|
23
|
+
IN_PROGRESS = auto()
|
|
24
|
+
COMPLETED = auto()
|
|
25
|
+
CANCELLED = auto()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TodoPriority(StrEnum):
|
|
29
|
+
LOW = auto()
|
|
30
|
+
MEDIUM = auto()
|
|
31
|
+
HIGH = auto()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TodoItem(BaseModel):
|
|
35
|
+
id: str
|
|
36
|
+
content: str
|
|
37
|
+
status: TodoStatus = TodoStatus.PENDING
|
|
38
|
+
priority: TodoPriority = TodoPriority.MEDIUM
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TodoArgs(BaseModel):
|
|
42
|
+
action: str = Field(description="Either 'read' or 'write'")
|
|
43
|
+
todos: list[TodoItem] | None = Field(
|
|
44
|
+
default=None, description="Complete list of todos when writing."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TodoResult(BaseModel):
|
|
49
|
+
message: str
|
|
50
|
+
todos: list[TodoItem]
|
|
51
|
+
total_count: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TodoConfig(BaseToolConfig):
|
|
55
|
+
permission: ToolPermission = ToolPermission.ALWAYS
|
|
56
|
+
max_todos: int = 100
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TodoState(BaseToolState):
|
|
60
|
+
todos: list[TodoItem] = Field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Todo(
|
|
64
|
+
BaseTool[TodoArgs, TodoResult, TodoConfig, TodoState],
|
|
65
|
+
ToolUIData[TodoArgs, TodoResult],
|
|
66
|
+
):
|
|
67
|
+
description: ClassVar[str] = (
|
|
68
|
+
"Manage todos. Use action='read' to view, action='write' with complete list to update."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def get_call_display(cls, event: ToolCallEvent) -> ToolCallDisplay:
|
|
73
|
+
if not isinstance(event.args, TodoArgs):
|
|
74
|
+
return ToolCallDisplay(summary="Invalid arguments")
|
|
75
|
+
|
|
76
|
+
args = event.args
|
|
77
|
+
|
|
78
|
+
match args.action:
|
|
79
|
+
case "read":
|
|
80
|
+
return ToolCallDisplay(summary="Reading todos")
|
|
81
|
+
case "write":
|
|
82
|
+
count = len(args.todos) if args.todos else 0
|
|
83
|
+
return ToolCallDisplay(summary=f"Writing {count} todos")
|
|
84
|
+
case _:
|
|
85
|
+
return ToolCallDisplay(summary=f"Unknown action: {args.action}")
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
|
|
89
|
+
if not isinstance(event.result, TodoResult):
|
|
90
|
+
return ToolResultDisplay(success=True, message="Success")
|
|
91
|
+
|
|
92
|
+
result = event.result
|
|
93
|
+
|
|
94
|
+
return ToolResultDisplay(success=True, message=result.message)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def get_status_text(cls) -> str:
|
|
98
|
+
return "Managing todos"
|
|
99
|
+
|
|
100
|
+
async def run(
|
|
101
|
+
self, args: TodoArgs, ctx: InvokeContext | None = None
|
|
102
|
+
) -> AsyncGenerator[ToolStreamEvent | TodoResult, None]:
|
|
103
|
+
match args.action:
|
|
104
|
+
case "read":
|
|
105
|
+
yield self._read_todos()
|
|
106
|
+
case "write":
|
|
107
|
+
yield self._write_todos(args.todos or [])
|
|
108
|
+
case _:
|
|
109
|
+
raise ToolError(
|
|
110
|
+
f"Invalid action '{args.action}'. Use 'read' or 'write'."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _read_todos(self) -> TodoResult:
|
|
114
|
+
return TodoResult(
|
|
115
|
+
message=f"Retrieved {len(self.state.todos)} todos",
|
|
116
|
+
todos=self.state.todos,
|
|
117
|
+
total_count=len(self.state.todos),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _write_todos(self, todos: list[TodoItem]) -> TodoResult:
|
|
121
|
+
if len(todos) > self.config.max_todos:
|
|
122
|
+
raise ToolError(f"Cannot store more than {self.config.max_todos} todos")
|
|
123
|
+
|
|
124
|
+
ids = [todo.id for todo in todos]
|
|
125
|
+
if len(ids) != len(set(ids)):
|
|
126
|
+
raise ToolError("Todo IDs must be unique")
|
|
127
|
+
|
|
128
|
+
self.state.todos = todos
|
|
129
|
+
|
|
130
|
+
return TodoResult(
|
|
131
|
+
message=f"Updated {len(todos)} todos",
|
|
132
|
+
todos=self.state.todos,
|
|
133
|
+
total_count=len(self.state.todos),
|
|
134
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ClassVar, final
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from vibe.core.tools.base import (
|
|
11
|
+
BaseTool,
|
|
12
|
+
BaseToolConfig,
|
|
13
|
+
BaseToolState,
|
|
14
|
+
InvokeContext,
|
|
15
|
+
ToolError,
|
|
16
|
+
ToolPermission,
|
|
17
|
+
)
|
|
18
|
+
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
|
19
|
+
from vibe.core.types import ToolCallEvent, ToolResultEvent, ToolStreamEvent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WriteFileArgs(BaseModel):
|
|
23
|
+
path: str
|
|
24
|
+
content: str
|
|
25
|
+
overwrite: bool = Field(
|
|
26
|
+
default=False, description="Must be set to true to overwrite an existing file."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WriteFileResult(BaseModel):
|
|
31
|
+
path: str
|
|
32
|
+
bytes_written: int
|
|
33
|
+
file_existed: bool
|
|
34
|
+
content: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WriteFileConfig(BaseToolConfig):
|
|
38
|
+
permission: ToolPermission = ToolPermission.ASK
|
|
39
|
+
max_write_bytes: int = 64_000
|
|
40
|
+
create_parent_dirs: bool = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WriteFileState(BaseToolState):
|
|
44
|
+
recently_written_files: list[str] = Field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WriteFile(
|
|
48
|
+
BaseTool[WriteFileArgs, WriteFileResult, WriteFileConfig, WriteFileState],
|
|
49
|
+
ToolUIData[WriteFileArgs, WriteFileResult],
|
|
50
|
+
):
|
|
51
|
+
description: ClassVar[str] = (
|
|
52
|
+
"Create or overwrite a UTF-8 file. Fails if file exists unless 'overwrite=True'."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_call_display(cls, event: ToolCallEvent) -> ToolCallDisplay:
|
|
57
|
+
if not isinstance(event.args, WriteFileArgs):
|
|
58
|
+
return ToolCallDisplay(summary="Invalid arguments")
|
|
59
|
+
|
|
60
|
+
args = event.args
|
|
61
|
+
|
|
62
|
+
return ToolCallDisplay(
|
|
63
|
+
summary=f"Writing {args.path}{' (overwrite)' if args.overwrite else ''}",
|
|
64
|
+
content=args.content,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
|
|
69
|
+
if isinstance(event.result, WriteFileResult):
|
|
70
|
+
action = "Overwritten" if event.result.file_existed else "Created"
|
|
71
|
+
return ToolResultDisplay(
|
|
72
|
+
success=True, message=f"{action} {Path(event.result.path).name}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return ToolResultDisplay(success=True, message="File written")
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def get_status_text(cls) -> str:
|
|
79
|
+
return "Writing file"
|
|
80
|
+
|
|
81
|
+
def check_allowlist_denylist(self, args: WriteFileArgs) -> ToolPermission | None:
|
|
82
|
+
import fnmatch
|
|
83
|
+
|
|
84
|
+
file_path = Path(args.path).expanduser()
|
|
85
|
+
if not file_path.is_absolute():
|
|
86
|
+
file_path = Path.cwd() / file_path
|
|
87
|
+
file_str = str(file_path)
|
|
88
|
+
|
|
89
|
+
for pattern in self.config.denylist:
|
|
90
|
+
if fnmatch.fnmatch(file_str, pattern):
|
|
91
|
+
return ToolPermission.NEVER
|
|
92
|
+
|
|
93
|
+
for pattern in self.config.allowlist:
|
|
94
|
+
if fnmatch.fnmatch(file_str, pattern):
|
|
95
|
+
return ToolPermission.ALWAYS
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
@final
|
|
100
|
+
async def run(
|
|
101
|
+
self, args: WriteFileArgs, ctx: InvokeContext | None = None
|
|
102
|
+
) -> AsyncGenerator[ToolStreamEvent | WriteFileResult, None]:
|
|
103
|
+
file_path, file_existed, content_bytes = self._prepare_and_validate_path(args)
|
|
104
|
+
|
|
105
|
+
await self._write_file(args, file_path)
|
|
106
|
+
|
|
107
|
+
BUFFER_SIZE = 10
|
|
108
|
+
self.state.recently_written_files.append(str(file_path))
|
|
109
|
+
if len(self.state.recently_written_files) > BUFFER_SIZE:
|
|
110
|
+
self.state.recently_written_files.pop(0)
|
|
111
|
+
|
|
112
|
+
yield WriteFileResult(
|
|
113
|
+
path=str(file_path),
|
|
114
|
+
bytes_written=content_bytes,
|
|
115
|
+
file_existed=file_existed,
|
|
116
|
+
content=args.content,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _prepare_and_validate_path(self, args: WriteFileArgs) -> tuple[Path, bool, int]:
|
|
120
|
+
if not args.path.strip():
|
|
121
|
+
raise ToolError("Path cannot be empty")
|
|
122
|
+
|
|
123
|
+
content_bytes = len(args.content.encode("utf-8"))
|
|
124
|
+
if content_bytes > self.config.max_write_bytes:
|
|
125
|
+
raise ToolError(
|
|
126
|
+
f"Content exceeds {self.config.max_write_bytes} bytes limit"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
file_path = Path(args.path).expanduser()
|
|
130
|
+
if not file_path.is_absolute():
|
|
131
|
+
file_path = Path.cwd() / file_path
|
|
132
|
+
file_path = file_path.resolve()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
file_path.relative_to(Path.cwd().resolve())
|
|
136
|
+
except ValueError:
|
|
137
|
+
raise ToolError(f"Cannot write outside project directory: {file_path}")
|
|
138
|
+
|
|
139
|
+
file_existed = file_path.exists()
|
|
140
|
+
|
|
141
|
+
if file_existed and not args.overwrite:
|
|
142
|
+
raise ToolError(
|
|
143
|
+
f"File '{file_path}' exists. Set overwrite=True to replace."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if self.config.create_parent_dirs:
|
|
147
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
elif not file_path.parent.exists():
|
|
149
|
+
raise ToolError(f"Parent directory does not exist: {file_path.parent}")
|
|
150
|
+
|
|
151
|
+
return file_path, file_existed, content_bytes
|
|
152
|
+
|
|
153
|
+
async def _write_file(self, args: WriteFileArgs, file_path: Path) -> None:
|
|
154
|
+
try:
|
|
155
|
+
async with await anyio.Path(file_path).open(
|
|
156
|
+
mode="w", encoding="utf-8"
|
|
157
|
+
) as f:
|
|
158
|
+
await f.write(args.content)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise ToolError(f"Error writing {file_path}: {e}") from e
|