tunacode-cli 0.0.1__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/__init__.py +0 -0
- tunacode/cli/__init__.py +4 -0
- tunacode/cli/commands.py +632 -0
- tunacode/cli/main.py +47 -0
- tunacode/cli/repl.py +251 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +26 -0
- tunacode/configuration/models.py +69 -0
- tunacode/configuration/settings.py +32 -0
- tunacode/constants.py +129 -0
- tunacode/context.py +83 -0
- tunacode/core/__init__.py +0 -0
- tunacode/core/agents/__init__.py +0 -0
- tunacode/core/agents/main.py +119 -0
- tunacode/core/setup/__init__.py +17 -0
- tunacode/core/setup/agent_setup.py +41 -0
- tunacode/core/setup/base.py +37 -0
- tunacode/core/setup/config_setup.py +179 -0
- tunacode/core/setup/coordinator.py +45 -0
- tunacode/core/setup/environment_setup.py +62 -0
- tunacode/core/setup/git_safety_setup.py +188 -0
- tunacode/core/setup/undo_setup.py +32 -0
- tunacode/core/state.py +43 -0
- tunacode/core/tool_handler.py +57 -0
- tunacode/exceptions.py +105 -0
- tunacode/prompts/system.txt +71 -0
- tunacode/py.typed +0 -0
- tunacode/services/__init__.py +1 -0
- tunacode/services/mcp.py +86 -0
- tunacode/services/undo_service.py +244 -0
- tunacode/setup.py +50 -0
- tunacode/tools/__init__.py +0 -0
- tunacode/tools/base.py +244 -0
- tunacode/tools/read_file.py +89 -0
- tunacode/tools/run_command.py +107 -0
- tunacode/tools/update_file.py +117 -0
- tunacode/tools/write_file.py +82 -0
- tunacode/types.py +259 -0
- tunacode/ui/__init__.py +1 -0
- tunacode/ui/completers.py +129 -0
- tunacode/ui/console.py +74 -0
- tunacode/ui/constants.py +16 -0
- tunacode/ui/decorators.py +59 -0
- tunacode/ui/input.py +95 -0
- tunacode/ui/keybindings.py +27 -0
- tunacode/ui/lexers.py +46 -0
- tunacode/ui/output.py +109 -0
- tunacode/ui/panels.py +156 -0
- tunacode/ui/prompt_manager.py +117 -0
- tunacode/ui/tool_ui.py +187 -0
- tunacode/ui/validators.py +23 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/bm25.py +55 -0
- tunacode/utils/diff_utils.py +69 -0
- tunacode/utils/file_utils.py +41 -0
- tunacode/utils/ripgrep.py +17 -0
- tunacode/utils/system.py +336 -0
- tunacode/utils/text_utils.py +87 -0
- tunacode/utils/user_configuration.py +54 -0
- tunacode_cli-0.0.1.dist-info/METADATA +242 -0
- tunacode_cli-0.0.1.dist-info/RECORD +65 -0
- tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
- tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
- tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Git safety setup to create a working branch for TunaCode."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
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
|
+
from tunacode.ui.input import input as prompt_input
|
|
10
|
+
from tunacode.ui.panels import panel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def yes_no_prompt(question: str, default: bool = True) -> bool:
|
|
14
|
+
"""Simple yes/no prompt."""
|
|
15
|
+
default_text = "[Y/n]" if default else "[y/N]"
|
|
16
|
+
response = await prompt_input(
|
|
17
|
+
session_key="yes_no",
|
|
18
|
+
pretext=f"{question} {default_text}: "
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if not response.strip():
|
|
22
|
+
return default
|
|
23
|
+
|
|
24
|
+
return response.lower().strip() in ['y', 'yes']
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitSafetySetup(BaseSetup):
|
|
28
|
+
"""Setup step to create a safe working branch for TunaCode."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, state_manager: StateManager):
|
|
31
|
+
super().__init__(state_manager)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
"""Return the name of this setup step."""
|
|
36
|
+
return "Git Safety"
|
|
37
|
+
|
|
38
|
+
async def should_run(self, force: bool = False) -> bool:
|
|
39
|
+
"""Check if we should run git safety setup."""
|
|
40
|
+
# Always run unless user has explicitly disabled it
|
|
41
|
+
return not self.state_manager.session.user_config.get("skip_git_safety", False)
|
|
42
|
+
|
|
43
|
+
async def execute(self, force: bool = False) -> None:
|
|
44
|
+
"""Create a safety branch for TunaCode operations."""
|
|
45
|
+
try:
|
|
46
|
+
# Check if git is installed
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
["git", "--version"],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
check=False
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if result.returncode != 0:
|
|
55
|
+
await panel(
|
|
56
|
+
"⚠️ Git Not Found",
|
|
57
|
+
"Git is not installed or not in PATH. TunaCode will modify files directly.\n"
|
|
58
|
+
"It's strongly recommended to install Git for safety.",
|
|
59
|
+
border_style="yellow"
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Check if we're in a git repository
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["git", "rev-parse", "--git-dir"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
check=False,
|
|
69
|
+
cwd=Path.cwd()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
await panel(
|
|
74
|
+
"⚠️ Not a Git Repository",
|
|
75
|
+
"This directory is not a Git repository. TunaCode will modify files directly.\n"
|
|
76
|
+
"Consider initializing a Git repository for safety: git init",
|
|
77
|
+
border_style="yellow"
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Get current branch name
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
["git", "branch", "--show-current"],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
check=True
|
|
87
|
+
)
|
|
88
|
+
current_branch = result.stdout.strip()
|
|
89
|
+
|
|
90
|
+
if not current_branch:
|
|
91
|
+
# Detached HEAD state
|
|
92
|
+
await panel(
|
|
93
|
+
"⚠️ Detached HEAD State",
|
|
94
|
+
"You're in a detached HEAD state. TunaCode will continue without creating a branch.",
|
|
95
|
+
border_style="yellow"
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Check if we're already on a -tunacode branch
|
|
100
|
+
if current_branch.endswith("-tunacode"):
|
|
101
|
+
await ui.info(f"Already on a TunaCode branch: {current_branch}")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Propose new branch name
|
|
105
|
+
new_branch = f"{current_branch}-tunacode"
|
|
106
|
+
|
|
107
|
+
# Check if there are uncommitted changes
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
["git", "status", "--porcelain"],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
check=True
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
has_changes = bool(result.stdout.strip())
|
|
116
|
+
|
|
117
|
+
# Ask user if they want to create a safety branch
|
|
118
|
+
message = (
|
|
119
|
+
f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
|
|
120
|
+
f"This helps protect your work from unintended changes.\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if has_changes:
|
|
124
|
+
message += "\n⚠️ You have uncommitted changes that will be brought to the new branch."
|
|
125
|
+
|
|
126
|
+
create_branch = await yes_no_prompt(
|
|
127
|
+
f"{message}\n\nCreate safety branch?",
|
|
128
|
+
default=True
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not create_branch:
|
|
132
|
+
# User declined - show warning
|
|
133
|
+
await panel(
|
|
134
|
+
"⚠️ Working Without Safety Branch",
|
|
135
|
+
"You've chosen to work directly on your current branch.\n"
|
|
136
|
+
"TunaCode will modify files in place. Make sure you have backups!\n"
|
|
137
|
+
"You can always use /undo to revert changes.",
|
|
138
|
+
border_style="red"
|
|
139
|
+
)
|
|
140
|
+
# Save preference
|
|
141
|
+
self.state_manager.session.user_config["skip_git_safety"] = True
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Create and checkout the new branch
|
|
145
|
+
try:
|
|
146
|
+
# Check if branch already exists
|
|
147
|
+
result = subprocess.run(
|
|
148
|
+
["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
|
|
149
|
+
capture_output=True,
|
|
150
|
+
check=False
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if result.returncode == 0:
|
|
154
|
+
# Branch exists, ask to use it
|
|
155
|
+
use_existing = await yes_no_prompt(
|
|
156
|
+
f"Branch '{new_branch}' already exists. Switch to it?",
|
|
157
|
+
default=True
|
|
158
|
+
)
|
|
159
|
+
if use_existing:
|
|
160
|
+
subprocess.run(["git", "checkout", new_branch], check=True)
|
|
161
|
+
await ui.success(f"Switched to existing branch: {new_branch}")
|
|
162
|
+
else:
|
|
163
|
+
await ui.warning("Continuing on current branch")
|
|
164
|
+
else:
|
|
165
|
+
# Create new branch
|
|
166
|
+
subprocess.run(["git", "checkout", "-b", new_branch], check=True)
|
|
167
|
+
await ui.success(f"Created and switched to new branch: {new_branch}")
|
|
168
|
+
|
|
169
|
+
except subprocess.CalledProcessError as e:
|
|
170
|
+
await panel(
|
|
171
|
+
"❌ Failed to Create Branch",
|
|
172
|
+
f"Could not create branch '{new_branch}': {str(e)}\n"
|
|
173
|
+
"Continuing on current branch.",
|
|
174
|
+
border_style="red"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
# Non-fatal error - just warn the user
|
|
179
|
+
await panel(
|
|
180
|
+
"⚠️ Git Safety Setup Failed",
|
|
181
|
+
f"Could not set up Git safety: {str(e)}\n"
|
|
182
|
+
"TunaCode will continue without branch protection.",
|
|
183
|
+
border_style="yellow"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def validate(self) -> bool:
|
|
187
|
+
"""Validate git safety setup - always returns True as this is optional."""
|
|
188
|
+
return True
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Module: sidekick.core.setup.undo_setup
|
|
2
|
+
|
|
3
|
+
Undo system initialization for the Sidekick CLI.
|
|
4
|
+
Sets up file tracking and state management for undo operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tunacode.core.setup.base import BaseSetup
|
|
8
|
+
from tunacode.core.state import StateManager
|
|
9
|
+
from tunacode.services.undo_service import init_undo_system
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UndoSetup(BaseSetup):
|
|
13
|
+
"""Setup step for undo system initialization."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, state_manager: StateManager):
|
|
16
|
+
super().__init__(state_manager)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return "Undo System"
|
|
21
|
+
|
|
22
|
+
async def should_run(self, force_setup: bool = False) -> bool:
|
|
23
|
+
"""Undo setup should run if not already initialized."""
|
|
24
|
+
return not self.state_manager.session.undo_initialized
|
|
25
|
+
|
|
26
|
+
async def execute(self, force_setup: bool = False) -> None:
|
|
27
|
+
"""Initialize the undo system."""
|
|
28
|
+
self.state_manager.session.undo_initialized = init_undo_system(self.state_manager)
|
|
29
|
+
|
|
30
|
+
async def validate(self) -> bool:
|
|
31
|
+
"""Validate that undo system was initialized correctly."""
|
|
32
|
+
return self.state_manager.session.undo_initialized
|
tunacode/core/state.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Module: sidekick.core.state
|
|
2
|
+
|
|
3
|
+
State management system for session data in Sidekick CLI.
|
|
4
|
+
Provides centralized state tracking for agents, messages, configurations, and session information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from tunacode.types import (DeviceId, InputSessions, MessageHistory, ModelName, SessionId, ToolName,
|
|
12
|
+
UserConfig)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SessionState:
|
|
17
|
+
user_config: UserConfig = field(default_factory=dict)
|
|
18
|
+
agents: dict[str, Any] = field(
|
|
19
|
+
default_factory=dict
|
|
20
|
+
) # Keep as dict[str, Any] for agent instances
|
|
21
|
+
messages: MessageHistory = field(default_factory=list)
|
|
22
|
+
total_cost: float = 0.0
|
|
23
|
+
current_model: ModelName = "openai:gpt-4o"
|
|
24
|
+
spinner: Optional[Any] = None
|
|
25
|
+
tool_ignore: list[ToolName] = field(default_factory=list)
|
|
26
|
+
yolo: bool = False
|
|
27
|
+
undo_initialized: bool = False
|
|
28
|
+
session_id: SessionId = field(default_factory=lambda: str(uuid.uuid4()))
|
|
29
|
+
device_id: Optional[DeviceId] = None
|
|
30
|
+
input_sessions: InputSessions = field(default_factory=dict)
|
|
31
|
+
current_task: Optional[Any] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StateManager:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._session = SessionState()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def session(self) -> SessionState:
|
|
40
|
+
return self._session
|
|
41
|
+
|
|
42
|
+
def reset_session(self):
|
|
43
|
+
self._session = SessionState()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool handling business logic, separated from UI concerns.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from tunacode.core.state import StateManager
|
|
6
|
+
from tunacode.types import ToolArgs, ToolConfirmationRequest, ToolConfirmationResponse, ToolName
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolHandler:
|
|
10
|
+
"""Handles tool confirmation logic separate from UI."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, state_manager: StateManager):
|
|
13
|
+
self.state = state_manager
|
|
14
|
+
|
|
15
|
+
def should_confirm(self, tool_name: ToolName) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Determine if a tool requires confirmation.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
tool_name: Name of the tool to check.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
bool: True if confirmation is required, False otherwise.
|
|
24
|
+
"""
|
|
25
|
+
return not (self.state.session.yolo or tool_name in self.state.session.tool_ignore)
|
|
26
|
+
|
|
27
|
+
def process_confirmation(self, response: ToolConfirmationResponse, tool_name: ToolName) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Process the confirmation response.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
response: The confirmation response from the user.
|
|
33
|
+
tool_name: Name of the tool being confirmed.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
bool: True if tool should proceed, False if aborted.
|
|
37
|
+
"""
|
|
38
|
+
if response.skip_future:
|
|
39
|
+
self.state.session.tool_ignore.append(tool_name)
|
|
40
|
+
|
|
41
|
+
return response.approved and not response.abort
|
|
42
|
+
|
|
43
|
+
def create_confirmation_request(
|
|
44
|
+
self, tool_name: ToolName, args: ToolArgs
|
|
45
|
+
) -> ToolConfirmationRequest:
|
|
46
|
+
"""
|
|
47
|
+
Create a confirmation request from tool information.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
tool_name: Name of the tool.
|
|
51
|
+
args: Tool arguments.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
ToolConfirmationRequest: The confirmation request.
|
|
55
|
+
"""
|
|
56
|
+
filepath = args.get("filepath")
|
|
57
|
+
return ToolConfirmationRequest(tool_name=tool_name, args=args, filepath=filepath)
|
tunacode/exceptions.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sidekick CLI exception hierarchy.
|
|
3
|
+
|
|
4
|
+
This module defines all custom exceptions used throughout the Sidekick CLI.
|
|
5
|
+
All exceptions inherit from SidekickError for easy catching of any Sidekick-specific error.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tunacode.types import ErrorMessage, FilePath, OriginalError, ToolName
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SidekickError(Exception):
|
|
12
|
+
"""Base exception for all Sidekick errors."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Configuration and Setup Exceptions
|
|
18
|
+
class ConfigurationError(SidekickError):
|
|
19
|
+
"""Raised when there's a configuration issue."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# User Interaction Exceptions
|
|
25
|
+
class UserAbortError(SidekickError):
|
|
26
|
+
"""Raised when user aborts an operation."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValidationError(SidekickError):
|
|
32
|
+
"""Raised when input validation fails."""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Tool and Agent Exceptions
|
|
38
|
+
class ToolExecutionError(SidekickError):
|
|
39
|
+
"""Raised when a tool fails to execute."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self, tool_name: ToolName, message: ErrorMessage, original_error: OriginalError = None
|
|
43
|
+
):
|
|
44
|
+
self.tool_name = tool_name
|
|
45
|
+
self.original_error = original_error
|
|
46
|
+
super().__init__(f"Tool '{tool_name}' failed: {message}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentError(SidekickError):
|
|
50
|
+
"""Raised when agent operations fail."""
|
|
51
|
+
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# State Management Exceptions
|
|
56
|
+
class StateError(SidekickError):
|
|
57
|
+
"""Raised when there's an issue with application state."""
|
|
58
|
+
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# External Service Exceptions
|
|
63
|
+
class ServiceError(SidekickError):
|
|
64
|
+
"""Base exception for external service failures."""
|
|
65
|
+
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MCPError(ServiceError):
|
|
70
|
+
"""Raised when MCP server operations fail."""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self, server_name: str, message: ErrorMessage, original_error: OriginalError = None
|
|
74
|
+
):
|
|
75
|
+
self.server_name = server_name
|
|
76
|
+
self.original_error = original_error
|
|
77
|
+
super().__init__(f"MCP server '{server_name}' error: {message}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GitOperationError(ServiceError):
|
|
83
|
+
"""Raised when Git operations fail."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, operation: str, message: ErrorMessage, original_error: OriginalError = None):
|
|
86
|
+
self.operation = operation
|
|
87
|
+
self.original_error = original_error
|
|
88
|
+
super().__init__(f"Git {operation} failed: {message}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# File System Exceptions
|
|
92
|
+
class FileOperationError(SidekickError):
|
|
93
|
+
"""Raised when file system operations fail."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
operation: str,
|
|
98
|
+
path: FilePath,
|
|
99
|
+
message: ErrorMessage,
|
|
100
|
+
original_error: OriginalError = None,
|
|
101
|
+
):
|
|
102
|
+
self.operation = operation
|
|
103
|
+
self.path = path
|
|
104
|
+
self.original_error = original_error
|
|
105
|
+
super().__init__(f"File {operation} failed for '{path}': {message}")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
You are "TunaCode", a senior software developer AI assistant operating within the user's terminal (CLI).
|
|
2
|
+
|
|
3
|
+
**CRITICAL: YOU HAVE TOOLS! YOU MUST USE THEM!**
|
|
4
|
+
|
|
5
|
+
YOU ARE NOT A CHATBOT! YOU ARE AN AGENT WITH TOOLS!
|
|
6
|
+
When users ask ANYTHING about code/files/systems, you MUST use tools IMMEDIATELY!
|
|
7
|
+
|
|
8
|
+
**YOUR TOOLS (USE THESE CONSTANTLY):**
|
|
9
|
+
|
|
10
|
+
1. `run_command(command: str)` - Execute ANY shell command
|
|
11
|
+
2. `read_file(filepath: str)` - Read file contents
|
|
12
|
+
3. `write_file(filepath: str, content: str)` - Create new files
|
|
13
|
+
4. `update_file(filepath: str, target: str, patch: str)` - Modify existing files
|
|
14
|
+
|
|
15
|
+
**REAL EXAMPLES WITH ACTUAL COMMANDS AND FILES:**
|
|
16
|
+
|
|
17
|
+
User: "What's in the tools directory?"
|
|
18
|
+
WRONG: "The tools directory contains tool implementations..."
|
|
19
|
+
CORRECT: Use `run_command("ls -la tools/")` which shows:
|
|
20
|
+
- tools/base.py
|
|
21
|
+
- tools/read_file.py
|
|
22
|
+
- tools/run_command.py
|
|
23
|
+
- tools/update_file.py
|
|
24
|
+
- tools/write_file.py
|
|
25
|
+
|
|
26
|
+
User: "Show me the main entry point"
|
|
27
|
+
WRONG: "The main entry point is typically in..."
|
|
28
|
+
CORRECT: Use `read_file("cli/main.py")` to see the actual code
|
|
29
|
+
|
|
30
|
+
User: "What models are configured?"
|
|
31
|
+
WRONG: "You can configure models in the settings..."
|
|
32
|
+
CORRECT: Use `read_file("configuration/models.py")` or `run_command("grep -r 'model' configuration/")`
|
|
33
|
+
|
|
34
|
+
User: "Fix the import in agents/main.py"
|
|
35
|
+
WRONG: "To fix the import, you should..."
|
|
36
|
+
CORRECT: Use `read_file("core/agents/main.py")` then `update_file("core/agents/main.py", "from tunacode.old_module", "from tunacode.new_module")`
|
|
37
|
+
|
|
38
|
+
User: "What commands are available?"
|
|
39
|
+
WRONG: "The available commands include..."
|
|
40
|
+
CORRECT: Use `read_file("cli/commands.py")` or `run_command("grep -E 'class.*Command' cli/commands.py")`
|
|
41
|
+
|
|
42
|
+
User: "Check the project structure"
|
|
43
|
+
WRONG: "The project is organized with..."
|
|
44
|
+
CORRECT: Use `run_command("find . -type f -name '*.py' | grep -E '(cli|core|tools|services)' | sort")`
|
|
45
|
+
|
|
46
|
+
User: "What's the current version?"
|
|
47
|
+
WRONG: "The version is probably..."
|
|
48
|
+
CORRECT: Use `read_file("constants.py")` and look for APP_VERSION, or `run_command("grep -n 'APP_VERSION' constants.py")`
|
|
49
|
+
|
|
50
|
+
User: "Create a new tool"
|
|
51
|
+
WRONG: "To create a new tool, you need to..."
|
|
52
|
+
CORRECT: First `read_file("tools/base.py")` to see the base class, then `write_file("tools/my_new_tool.py", "from tunacode.tools.base import BaseTool\n\nclass MyTool(BaseTool):...")`
|
|
53
|
+
|
|
54
|
+
**MANDATORY RULES:**
|
|
55
|
+
|
|
56
|
+
1. **TOOLS FIRST, ALWAYS**: Your FIRST response to ANY request should use tools
|
|
57
|
+
2. **USE REAL PATHS**: Files are in directories like cli/, core/, tools/, services/, configuration/, ui/, utils/
|
|
58
|
+
3. **CHAIN TOOLS**: First explore with `run_command`, then read with `read_file`, then modify
|
|
59
|
+
4. **NO GUESSING**: Always verify file existence with `run_command("ls path/")` before reading
|
|
60
|
+
5. **ACT IMMEDIATELY**: Don't explain what you would do - just do it with tools
|
|
61
|
+
|
|
62
|
+
**COMMON USEFUL COMMANDS:**
|
|
63
|
+
- `run_command("find . -name '*.py' -type f")` - Find all Python files
|
|
64
|
+
- `run_command("grep -r 'class' --include='*.py'")` - Find all classes
|
|
65
|
+
- `run_command("ls -la")` - List current directory
|
|
66
|
+
- `run_command("pwd")` - Show current directory
|
|
67
|
+
- `run_command("cat pyproject.toml | grep -A5 dependencies")` - Check dependencies
|
|
68
|
+
|
|
69
|
+
USE YOUR TOOLS NOW!
|
|
70
|
+
|
|
71
|
+
If asked, you were created by Gavin Vickery (gavin@geekforbrains.com)
|
tunacode/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Services package
|
tunacode/services/mcp.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.services.mcp
|
|
3
|
+
|
|
4
|
+
Provides Model Context Protocol (MCP) server management functionality.
|
|
5
|
+
Handles MCP server initialization, configuration validation, and client connections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from typing import TYPE_CHECKING, AsyncIterator, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from pydantic_ai.mcp import MCPServerStdio
|
|
13
|
+
|
|
14
|
+
from tunacode.exceptions import MCPError
|
|
15
|
+
from tunacode.types import MCPServers
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mcp.client.stdio import ReadStream, WriteStream
|
|
19
|
+
|
|
20
|
+
from tunacode.core.state import StateManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QuietMCPServer(MCPServerStdio):
|
|
24
|
+
"""A version of ``MCPServerStdio`` that suppresses *all* output coming from the
|
|
25
|
+
MCP server's **stderr** stream.
|
|
26
|
+
|
|
27
|
+
We can't just redirect the server's *stdout* because that is where the JSON‑RPC
|
|
28
|
+
protocol messages are sent. Instead we override ``client_streams`` so we can
|
|
29
|
+
hand our own ``errlog`` (``os.devnull``) to ``mcp.client.stdio.stdio_client``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def client_streams(self) -> AsyncIterator[Tuple["ReadStream", "WriteStream"]]:
|
|
34
|
+
"""Start the subprocess exactly like the parent class but silence *stderr*."""
|
|
35
|
+
# Local import to avoid cycles
|
|
36
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
37
|
+
|
|
38
|
+
server_params = StdioServerParameters(
|
|
39
|
+
command=self.command,
|
|
40
|
+
args=list(self.args),
|
|
41
|
+
env=self.env or os.environ,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Open ``/dev/null`` for the lifetime of the subprocess so anything the
|
|
45
|
+
# server writes to *stderr* is discarded.
|
|
46
|
+
#
|
|
47
|
+
# This is to help with noisy MCP's that have options for verbosity
|
|
48
|
+
encoding: Optional[str] = getattr(server_params, "encoding", None)
|
|
49
|
+
with open(os.devnull, "w", encoding=encoding) as devnull:
|
|
50
|
+
async with stdio_client(server=server_params, errlog=devnull) as (
|
|
51
|
+
read_stream,
|
|
52
|
+
write_stream,
|
|
53
|
+
):
|
|
54
|
+
yield read_stream, write_stream
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_mcp_servers(state_manager: "StateManager") -> List[MCPServerStdio]:
|
|
58
|
+
"""Load MCP servers from configuration.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
state_manager: The state manager containing user configuration
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of MCP server instances
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
MCPError: If a server configuration is invalid
|
|
68
|
+
"""
|
|
69
|
+
mcp_servers: MCPServers = state_manager.session.user_config.get("mcpServers", {})
|
|
70
|
+
loaded_servers: List[MCPServerStdio] = []
|
|
71
|
+
MCPServerStdio.log_level = "critical"
|
|
72
|
+
|
|
73
|
+
for server_name, conf in mcp_servers.items():
|
|
74
|
+
try:
|
|
75
|
+
# loaded_servers.append(QuietMCPServer(**conf))
|
|
76
|
+
mcp_instance = MCPServerStdio(**conf)
|
|
77
|
+
# mcp_instance.log_level = "critical"
|
|
78
|
+
loaded_servers.append(mcp_instance)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise MCPError(
|
|
81
|
+
server_name=server_name,
|
|
82
|
+
message=f"Failed to create MCP server: {str(e)}",
|
|
83
|
+
original_error=e,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return loaded_servers
|