tunacode-cli 0.0.4__py3-none-any.whl → 0.0.6__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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands.py +91 -33
- tunacode/cli/model_selector.py +178 -0
- tunacode/cli/repl.py +11 -10
- tunacode/configuration/models.py +11 -1
- tunacode/constants.py +11 -11
- tunacode/context.py +1 -3
- tunacode/core/agents/main.py +52 -94
- tunacode/core/agents/tinyagent_main.py +171 -0
- tunacode/core/setup/git_safety_setup.py +39 -51
- tunacode/core/setup/optimized_coordinator.py +73 -0
- tunacode/exceptions.py +13 -15
- tunacode/services/enhanced_undo_service.py +322 -0
- tunacode/services/project_undo_service.py +311 -0
- tunacode/services/undo_service.py +18 -21
- tunacode/tools/base.py +11 -20
- tunacode/tools/tinyagent_tools.py +103 -0
- tunacode/tools/update_file.py +24 -14
- tunacode/tools/write_file.py +9 -7
- tunacode/types.py +2 -2
- tunacode/ui/completers.py +98 -33
- tunacode/ui/input.py +8 -7
- tunacode/ui/keybindings.py +1 -3
- tunacode/ui/lexers.py +16 -17
- tunacode/ui/output.py +9 -3
- tunacode/ui/panels.py +4 -4
- tunacode/ui/prompt_manager.py +6 -4
- tunacode/utils/lazy_imports.py +59 -0
- tunacode/utils/regex_cache.py +33 -0
- tunacode/utils/system.py +13 -13
- tunacode_cli-0.0.6.dist-info/METADATA +235 -0
- {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/RECORD +35 -27
- tunacode_cli-0.0.4.dist-info/METADATA +0 -247
- {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""TinyAgent-based agent implementation."""
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from tinyagent.react.react_agent import ReactAgent
|
|
8
|
+
|
|
9
|
+
from tunacode.core.state import StateManager
|
|
10
|
+
from tunacode.tools.tinyagent_tools import read_file, run_command, update_file, write_file
|
|
11
|
+
from tunacode.types import ModelName, ToolCallback
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_or_create_react_agent(model: ModelName, state_manager: StateManager) -> ReactAgent:
|
|
15
|
+
"""
|
|
16
|
+
Get or create a ReactAgent for the specified model.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
model: The model name (e.g., "openai:gpt-4o", "openrouter:openai/gpt-4.1")
|
|
20
|
+
state_manager: The state manager instance
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
ReactAgent instance configured for the model
|
|
24
|
+
"""
|
|
25
|
+
agents = state_manager.session.agents
|
|
26
|
+
|
|
27
|
+
if model not in agents:
|
|
28
|
+
# Parse model string to determine provider and actual model name
|
|
29
|
+
# Format: "provider:model" or "openrouter:provider/model"
|
|
30
|
+
if model.startswith("openrouter:"):
|
|
31
|
+
# OpenRouter model - extract the actual model name
|
|
32
|
+
actual_model = model.replace("openrouter:", "")
|
|
33
|
+
# Set environment to use OpenRouter base URL
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
os.environ["OPENAI_BASE_URL"] = "https://openrouter.ai/api/v1"
|
|
37
|
+
# Use OpenRouter API key if available
|
|
38
|
+
if state_manager.session.user_config["env"].get("OPENROUTER_API_KEY"):
|
|
39
|
+
os.environ["OPENAI_API_KEY"] = state_manager.session.user_config["env"][
|
|
40
|
+
"OPENROUTER_API_KEY"
|
|
41
|
+
]
|
|
42
|
+
else:
|
|
43
|
+
# Direct provider (openai, anthropic, google-gla)
|
|
44
|
+
provider, actual_model = model.split(":", 1)
|
|
45
|
+
# Reset to default base URL for direct providers
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
if provider == "openai":
|
|
49
|
+
os.environ["OPENAI_BASE_URL"] = "https://api.openai.com/v1"
|
|
50
|
+
# Set appropriate API key
|
|
51
|
+
provider_key_map = {
|
|
52
|
+
"openai": "OPENAI_API_KEY",
|
|
53
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
54
|
+
"google-gla": "GEMINI_API_KEY",
|
|
55
|
+
}
|
|
56
|
+
if provider in provider_key_map:
|
|
57
|
+
key_name = provider_key_map[provider]
|
|
58
|
+
if state_manager.session.user_config["env"].get(key_name):
|
|
59
|
+
os.environ[key_name] = state_manager.session.user_config["env"][key_name]
|
|
60
|
+
|
|
61
|
+
# Create new ReactAgent with the actual model name
|
|
62
|
+
agent = ReactAgent(model_override=actual_model)
|
|
63
|
+
|
|
64
|
+
# Register our tools
|
|
65
|
+
for fn in (read_file, write_file, update_file, run_command):
|
|
66
|
+
agent.register_tool(fn._tool)
|
|
67
|
+
|
|
68
|
+
# Add MCP compatibility method
|
|
69
|
+
@asynccontextmanager
|
|
70
|
+
async def run_mcp_servers():
|
|
71
|
+
# TinyAgent doesn't have built-in MCP support yet
|
|
72
|
+
# This is a placeholder for compatibility
|
|
73
|
+
yield
|
|
74
|
+
|
|
75
|
+
agent.run_mcp_servers = run_mcp_servers
|
|
76
|
+
|
|
77
|
+
# Cache the agent
|
|
78
|
+
agents[model] = agent
|
|
79
|
+
|
|
80
|
+
return agents[model]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def process_request_with_tinyagent(
|
|
84
|
+
model: ModelName,
|
|
85
|
+
message: str,
|
|
86
|
+
state_manager: StateManager,
|
|
87
|
+
tool_callback: Optional[ToolCallback] = None,
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
Process a request using TinyAgent's ReactAgent.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
model: The model to use
|
|
94
|
+
message: The user message
|
|
95
|
+
state_manager: State manager instance
|
|
96
|
+
tool_callback: Optional callback for tool execution (for UI updates)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dict containing the result and any metadata
|
|
100
|
+
"""
|
|
101
|
+
agent = get_or_create_react_agent(model, state_manager)
|
|
102
|
+
|
|
103
|
+
# Convert message history to format expected by tinyAgent
|
|
104
|
+
# Note: tinyAgent handles message history differently than pydantic-ai
|
|
105
|
+
# We'll need to adapt based on tinyAgent's actual API
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# Run the agent with the message
|
|
109
|
+
result = await agent.run_react(message)
|
|
110
|
+
|
|
111
|
+
# Update message history in state_manager
|
|
112
|
+
# This will need to be adapted based on how tinyAgent returns messages
|
|
113
|
+
state_manager.session.messages.append(
|
|
114
|
+
{
|
|
115
|
+
"role": "user",
|
|
116
|
+
"content": message,
|
|
117
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
state_manager.session.messages.append(
|
|
122
|
+
{
|
|
123
|
+
"role": "assistant",
|
|
124
|
+
"content": result,
|
|
125
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return {"result": result, "success": True, "model": model}
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
# Handle errors
|
|
133
|
+
error_result = {
|
|
134
|
+
"result": f"Error: {str(e)}",
|
|
135
|
+
"success": False,
|
|
136
|
+
"model": model,
|
|
137
|
+
"error": str(e),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Still update message history with the error
|
|
141
|
+
state_manager.session.messages.append(
|
|
142
|
+
{
|
|
143
|
+
"role": "user",
|
|
144
|
+
"content": message,
|
|
145
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
state_manager.session.messages.append(
|
|
150
|
+
{
|
|
151
|
+
"role": "assistant",
|
|
152
|
+
"content": f"Error occurred: {str(e)}",
|
|
153
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
154
|
+
"error": True,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return error_result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def patch_tool_messages(
|
|
162
|
+
error_message: str = "Tool operation failed",
|
|
163
|
+
state_manager: StateManager = None,
|
|
164
|
+
):
|
|
165
|
+
"""
|
|
166
|
+
Compatibility function for patching tool messages.
|
|
167
|
+
With tinyAgent, this may not be needed as it handles tool errors differently.
|
|
168
|
+
"""
|
|
169
|
+
# TinyAgent handles tool retries and errors internally
|
|
170
|
+
# This function is kept for compatibility but may be simplified
|
|
171
|
+
pass
|
|
@@ -13,15 +13,12 @@ from tunacode.ui.panels import panel
|
|
|
13
13
|
async def yes_no_prompt(question: str, default: bool = True) -> bool:
|
|
14
14
|
"""Simple yes/no prompt."""
|
|
15
15
|
default_text = "[Y/n]" if default else "[y/N]"
|
|
16
|
-
response = await prompt_input(
|
|
17
|
-
|
|
18
|
-
pretext=f"{question} {default_text}: "
|
|
19
|
-
)
|
|
20
|
-
|
|
16
|
+
response = await prompt_input(session_key="yes_no", pretext=f"{question} {default_text}: ")
|
|
17
|
+
|
|
21
18
|
if not response.strip():
|
|
22
19
|
return default
|
|
23
|
-
|
|
24
|
-
return response.lower().strip() in [
|
|
20
|
+
|
|
21
|
+
return response.lower().strip() in ["y", "yes"]
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
class GitSafetySetup(BaseSetup):
|
|
@@ -29,7 +26,7 @@ class GitSafetySetup(BaseSetup):
|
|
|
29
26
|
|
|
30
27
|
def __init__(self, state_manager: StateManager):
|
|
31
28
|
super().__init__(state_manager)
|
|
32
|
-
|
|
29
|
+
|
|
33
30
|
@property
|
|
34
31
|
def name(self) -> str:
|
|
35
32
|
"""Return the name of this setup step."""
|
|
@@ -45,18 +42,15 @@ class GitSafetySetup(BaseSetup):
|
|
|
45
42
|
try:
|
|
46
43
|
# Check if git is installed
|
|
47
44
|
result = subprocess.run(
|
|
48
|
-
["git", "--version"],
|
|
49
|
-
capture_output=True,
|
|
50
|
-
text=True,
|
|
51
|
-
check=False
|
|
45
|
+
["git", "--version"], capture_output=True, text=True, check=False
|
|
52
46
|
)
|
|
53
|
-
|
|
47
|
+
|
|
54
48
|
if result.returncode != 0:
|
|
55
49
|
await panel(
|
|
56
50
|
"⚠️ Git Not Found",
|
|
57
51
|
"Git is not installed or not in PATH. TunaCode will modify files directly.\n"
|
|
58
52
|
"It's strongly recommended to install Git for safety.",
|
|
59
|
-
border_style="yellow"
|
|
53
|
+
border_style="yellow",
|
|
60
54
|
)
|
|
61
55
|
return
|
|
62
56
|
|
|
@@ -66,33 +60,31 @@ class GitSafetySetup(BaseSetup):
|
|
|
66
60
|
capture_output=True,
|
|
67
61
|
text=True,
|
|
68
62
|
check=False,
|
|
69
|
-
cwd=Path.cwd()
|
|
63
|
+
cwd=Path.cwd(),
|
|
70
64
|
)
|
|
71
|
-
|
|
65
|
+
|
|
72
66
|
if result.returncode != 0:
|
|
73
67
|
await panel(
|
|
74
68
|
"⚠️ Not a Git Repository",
|
|
75
69
|
"This directory is not a Git repository. TunaCode will modify files directly.\n"
|
|
76
70
|
"Consider initializing a Git repository for safety: git init",
|
|
77
|
-
border_style="yellow"
|
|
71
|
+
border_style="yellow",
|
|
78
72
|
)
|
|
79
73
|
return
|
|
80
74
|
|
|
81
75
|
# Get current branch name
|
|
82
76
|
result = subprocess.run(
|
|
83
|
-
["git", "branch", "--show-current"],
|
|
84
|
-
capture_output=True,
|
|
85
|
-
text=True,
|
|
86
|
-
check=True
|
|
77
|
+
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
|
|
87
78
|
)
|
|
88
79
|
current_branch = result.stdout.strip()
|
|
89
|
-
|
|
80
|
+
|
|
90
81
|
if not current_branch:
|
|
91
82
|
# Detached HEAD state
|
|
92
83
|
await panel(
|
|
93
84
|
"⚠️ Detached HEAD State",
|
|
94
|
-
"You're in a detached HEAD state. TunaCode will continue
|
|
95
|
-
|
|
85
|
+
"You're in a detached HEAD state. TunaCode will continue "
|
|
86
|
+
"without creating a branch.",
|
|
87
|
+
border_style="yellow",
|
|
96
88
|
)
|
|
97
89
|
return
|
|
98
90
|
|
|
@@ -103,31 +95,28 @@ class GitSafetySetup(BaseSetup):
|
|
|
103
95
|
|
|
104
96
|
# Propose new branch name
|
|
105
97
|
new_branch = f"{current_branch}-tunacode"
|
|
106
|
-
|
|
98
|
+
|
|
107
99
|
# Check if there are uncommitted changes
|
|
108
100
|
result = subprocess.run(
|
|
109
|
-
["git", "status", "--porcelain"],
|
|
110
|
-
capture_output=True,
|
|
111
|
-
text=True,
|
|
112
|
-
check=True
|
|
101
|
+
["git", "status", "--porcelain"], capture_output=True, text=True, check=True
|
|
113
102
|
)
|
|
114
|
-
|
|
103
|
+
|
|
115
104
|
has_changes = bool(result.stdout.strip())
|
|
116
|
-
|
|
105
|
+
|
|
117
106
|
# Ask user if they want to create a safety branch
|
|
118
107
|
message = (
|
|
119
|
-
f"For safety, TunaCode can create a new branch '{new_branch}'
|
|
108
|
+
f"For safety, TunaCode can create a new branch '{new_branch}' "
|
|
109
|
+
f"based on '{current_branch}'.\n"
|
|
120
110
|
f"This helps protect your work from unintended changes.\n"
|
|
121
111
|
)
|
|
122
|
-
|
|
112
|
+
|
|
123
113
|
if has_changes:
|
|
124
|
-
message +=
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
message += (
|
|
115
|
+
"\n⚠️ You have uncommitted changes that will be brought to the new branch."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
create_branch = await yes_no_prompt(f"{message}\n\nCreate safety branch?", default=True)
|
|
119
|
+
|
|
131
120
|
if not create_branch:
|
|
132
121
|
# User declined - show warning
|
|
133
122
|
await panel(
|
|
@@ -135,26 +124,25 @@ class GitSafetySetup(BaseSetup):
|
|
|
135
124
|
"You've chosen to work directly on your current branch.\n"
|
|
136
125
|
"TunaCode will modify files in place. Make sure you have backups!\n"
|
|
137
126
|
"You can always use /undo to revert changes.",
|
|
138
|
-
border_style="red"
|
|
127
|
+
border_style="red",
|
|
139
128
|
)
|
|
140
129
|
# Save preference
|
|
141
130
|
self.state_manager.session.user_config["skip_git_safety"] = True
|
|
142
131
|
return
|
|
143
|
-
|
|
132
|
+
|
|
144
133
|
# Create and checkout the new branch
|
|
145
134
|
try:
|
|
146
135
|
# Check if branch already exists
|
|
147
136
|
result = subprocess.run(
|
|
148
137
|
["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
|
|
149
138
|
capture_output=True,
|
|
150
|
-
check=False
|
|
139
|
+
check=False,
|
|
151
140
|
)
|
|
152
|
-
|
|
141
|
+
|
|
153
142
|
if result.returncode == 0:
|
|
154
143
|
# Branch exists, ask to use it
|
|
155
144
|
use_existing = await yes_no_prompt(
|
|
156
|
-
f"Branch '{new_branch}' already exists. Switch to it?",
|
|
157
|
-
default=True
|
|
145
|
+
f"Branch '{new_branch}' already exists. Switch to it?", default=True
|
|
158
146
|
)
|
|
159
147
|
if use_existing:
|
|
160
148
|
subprocess.run(["git", "checkout", new_branch], check=True)
|
|
@@ -165,24 +153,24 @@ class GitSafetySetup(BaseSetup):
|
|
|
165
153
|
# Create new branch
|
|
166
154
|
subprocess.run(["git", "checkout", "-b", new_branch], check=True)
|
|
167
155
|
await ui.success(f"Created and switched to new branch: {new_branch}")
|
|
168
|
-
|
|
156
|
+
|
|
169
157
|
except subprocess.CalledProcessError as e:
|
|
170
158
|
await panel(
|
|
171
159
|
"❌ Failed to Create Branch",
|
|
172
160
|
f"Could not create branch '{new_branch}': {str(e)}\n"
|
|
173
161
|
"Continuing on current branch.",
|
|
174
|
-
border_style="red"
|
|
162
|
+
border_style="red",
|
|
175
163
|
)
|
|
176
|
-
|
|
164
|
+
|
|
177
165
|
except Exception as e:
|
|
178
166
|
# Non-fatal error - just warn the user
|
|
179
167
|
await panel(
|
|
180
168
|
"⚠️ Git Safety Setup Failed",
|
|
181
169
|
f"Could not set up Git safety: {str(e)}\n"
|
|
182
170
|
"TunaCode will continue without branch protection.",
|
|
183
|
-
border_style="yellow"
|
|
171
|
+
border_style="yellow",
|
|
184
172
|
)
|
|
185
173
|
|
|
186
174
|
async def validate(self) -> bool:
|
|
187
175
|
"""Validate git safety setup - always returns True as this is optional."""
|
|
188
|
-
return True
|
|
176
|
+
return True
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Optimized setup coordinator with deferred loading."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import List, Set
|
|
5
|
+
|
|
6
|
+
from tunacode.core.setup.base import BaseSetup
|
|
7
|
+
from tunacode.core.state import StateManager
|
|
8
|
+
from tunacode.ui import console as ui
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OptimizedSetupCoordinator:
|
|
12
|
+
"""Optimized coordinator that defers non-critical setup steps."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, state_manager: StateManager):
|
|
15
|
+
self.state_manager = state_manager
|
|
16
|
+
self.critical_steps: List[BaseSetup] = []
|
|
17
|
+
self.deferred_steps: List[BaseSetup] = []
|
|
18
|
+
self._deferred_task = None
|
|
19
|
+
|
|
20
|
+
# Define critical steps that must run at startup
|
|
21
|
+
self.critical_step_names: Set[str] = {
|
|
22
|
+
"Configuration", # Need config to know which model to use
|
|
23
|
+
"Environment Variables", # Need API keys
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def register_step(self, step: BaseSetup) -> None:
|
|
27
|
+
"""Register a setup step, separating critical from deferred."""
|
|
28
|
+
if step.name in self.critical_step_names:
|
|
29
|
+
self.critical_steps.append(step)
|
|
30
|
+
else:
|
|
31
|
+
self.deferred_steps.append(step)
|
|
32
|
+
|
|
33
|
+
async def run_setup(self, force_setup: bool = False) -> None:
|
|
34
|
+
"""Run critical setup immediately, defer the rest."""
|
|
35
|
+
# Run critical steps synchronously
|
|
36
|
+
for step in self.critical_steps:
|
|
37
|
+
try:
|
|
38
|
+
if await step.should_run(force_setup):
|
|
39
|
+
await step.execute(force_setup)
|
|
40
|
+
if not await step.validate():
|
|
41
|
+
await ui.error(f"Setup validation failed: {step.name}")
|
|
42
|
+
raise RuntimeError(f"Setup step '{step.name}' failed validation")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
await ui.error(f"Setup failed at step '{step.name}': {str(e)}")
|
|
45
|
+
raise
|
|
46
|
+
|
|
47
|
+
# Schedule deferred steps to run in background
|
|
48
|
+
if self.deferred_steps and not self._deferred_task:
|
|
49
|
+
self._deferred_task = asyncio.create_task(self._run_deferred_steps(force_setup))
|
|
50
|
+
|
|
51
|
+
async def _run_deferred_steps(self, force_setup: bool) -> None:
|
|
52
|
+
"""Run deferred steps in the background."""
|
|
53
|
+
# Wait a moment to let the main UI start
|
|
54
|
+
await asyncio.sleep(0.1)
|
|
55
|
+
|
|
56
|
+
for step in self.deferred_steps:
|
|
57
|
+
try:
|
|
58
|
+
if await step.should_run(force_setup):
|
|
59
|
+
await step.execute(force_setup)
|
|
60
|
+
# Don't validate deferred steps - they're non-critical
|
|
61
|
+
except Exception:
|
|
62
|
+
# Log but don't fail on deferred steps
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
async def ensure_deferred_complete(self) -> None:
|
|
66
|
+
"""Ensure deferred steps are complete before certain operations."""
|
|
67
|
+
if self._deferred_task and not self._deferred_task.done():
|
|
68
|
+
await self._deferred_task
|
|
69
|
+
|
|
70
|
+
def clear_steps(self) -> None:
|
|
71
|
+
"""Clear all registered setup steps."""
|
|
72
|
+
self.critical_steps.clear()
|
|
73
|
+
self.deferred_steps.clear()
|
tunacode/exceptions.py
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
TunaCode CLI exception hierarchy.
|
|
3
3
|
|
|
4
|
-
This module defines all custom exceptions used throughout the
|
|
5
|
-
All exceptions inherit from
|
|
4
|
+
This module defines all custom exceptions used throughout the TunaCode CLI.
|
|
5
|
+
All exceptions inherit from TunaCodeError for easy catching of any TunaCode-specific error.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from tunacode.types import ErrorMessage, FilePath, OriginalError, ToolName
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
12
|
-
"""Base exception for all
|
|
11
|
+
class TunaCodeError(Exception):
|
|
12
|
+
"""Base exception for all TunaCode errors."""
|
|
13
13
|
|
|
14
14
|
pass
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
# Configuration and Setup Exceptions
|
|
18
|
-
class ConfigurationError(
|
|
18
|
+
class ConfigurationError(TunaCodeError):
|
|
19
19
|
"""Raised when there's a configuration issue."""
|
|
20
20
|
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
# User Interaction Exceptions
|
|
25
|
-
class UserAbortError(
|
|
25
|
+
class UserAbortError(TunaCodeError):
|
|
26
26
|
"""Raised when user aborts an operation."""
|
|
27
27
|
|
|
28
28
|
pass
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class ValidationError(
|
|
31
|
+
class ValidationError(TunaCodeError):
|
|
32
32
|
"""Raised when input validation fails."""
|
|
33
33
|
|
|
34
34
|
pass
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# Tool and Agent Exceptions
|
|
38
|
-
class ToolExecutionError(
|
|
38
|
+
class ToolExecutionError(TunaCodeError):
|
|
39
39
|
"""Raised when a tool fails to execute."""
|
|
40
40
|
|
|
41
41
|
def __init__(
|
|
@@ -46,21 +46,21 @@ class ToolExecutionError(SidekickError):
|
|
|
46
46
|
super().__init__(f"Tool '{tool_name}' failed: {message}")
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class AgentError(
|
|
49
|
+
class AgentError(TunaCodeError):
|
|
50
50
|
"""Raised when agent operations fail."""
|
|
51
51
|
|
|
52
52
|
pass
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
# State Management Exceptions
|
|
56
|
-
class StateError(
|
|
56
|
+
class StateError(TunaCodeError):
|
|
57
57
|
"""Raised when there's an issue with application state."""
|
|
58
58
|
|
|
59
59
|
pass
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
# External Service Exceptions
|
|
63
|
-
class ServiceError(
|
|
63
|
+
class ServiceError(TunaCodeError):
|
|
64
64
|
"""Base exception for external service failures."""
|
|
65
65
|
|
|
66
66
|
pass
|
|
@@ -77,8 +77,6 @@ class MCPError(ServiceError):
|
|
|
77
77
|
super().__init__(f"MCP server '{server_name}' error: {message}")
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
80
|
class GitOperationError(ServiceError):
|
|
83
81
|
"""Raised when Git operations fail."""
|
|
84
82
|
|
|
@@ -89,7 +87,7 @@ class GitOperationError(ServiceError):
|
|
|
89
87
|
|
|
90
88
|
|
|
91
89
|
# File System Exceptions
|
|
92
|
-
class FileOperationError(
|
|
90
|
+
class FileOperationError(TunaCodeError):
|
|
93
91
|
"""Raised when file system operations fail."""
|
|
94
92
|
|
|
95
93
|
def __init__(
|