tunacode-cli 0.0.9__py3-none-any.whl → 0.0.11__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.

Files changed (46) hide show
  1. tunacode/cli/commands.py +34 -165
  2. tunacode/cli/main.py +15 -38
  3. tunacode/cli/repl.py +24 -18
  4. tunacode/configuration/defaults.py +1 -1
  5. tunacode/configuration/models.py +4 -11
  6. tunacode/configuration/settings.py +10 -3
  7. tunacode/constants.py +6 -4
  8. tunacode/context.py +3 -1
  9. tunacode/core/agents/main.py +94 -52
  10. tunacode/core/setup/agent_setup.py +1 -1
  11. tunacode/core/setup/config_setup.py +161 -81
  12. tunacode/core/setup/coordinator.py +4 -2
  13. tunacode/core/setup/environment_setup.py +1 -1
  14. tunacode/core/setup/git_safety_setup.py +51 -39
  15. tunacode/exceptions.py +2 -0
  16. tunacode/prompts/system.txt +1 -1
  17. tunacode/services/undo_service.py +16 -13
  18. tunacode/setup.py +6 -2
  19. tunacode/tools/base.py +20 -11
  20. tunacode/tools/update_file.py +14 -24
  21. tunacode/tools/write_file.py +7 -9
  22. tunacode/ui/completers.py +33 -98
  23. tunacode/ui/input.py +9 -13
  24. tunacode/ui/keybindings.py +3 -1
  25. tunacode/ui/lexers.py +17 -16
  26. tunacode/ui/output.py +8 -14
  27. tunacode/ui/panels.py +7 -5
  28. tunacode/ui/prompt_manager.py +4 -8
  29. tunacode/ui/tool_ui.py +3 -3
  30. tunacode/utils/system.py +0 -40
  31. tunacode_cli-0.0.11.dist-info/METADATA +387 -0
  32. tunacode_cli-0.0.11.dist-info/RECORD +65 -0
  33. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.11.dist-info}/licenses/LICENSE +1 -1
  34. tunacode/cli/model_selector.py +0 -178
  35. tunacode/core/agents/tinyagent_main.py +0 -194
  36. tunacode/core/setup/optimized_coordinator.py +0 -73
  37. tunacode/services/enhanced_undo_service.py +0 -322
  38. tunacode/services/project_undo_service.py +0 -311
  39. tunacode/tools/tinyagent_tools.py +0 -103
  40. tunacode/utils/lazy_imports.py +0 -59
  41. tunacode/utils/regex_cache.py +0 -33
  42. tunacode_cli-0.0.9.dist-info/METADATA +0 -321
  43. tunacode_cli-0.0.9.dist-info/RECORD +0 -73
  44. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.11.dist-info}/WHEEL +0 -0
  45. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.11.dist-info}/entry_points.txt +0 -0
  46. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.11.dist-info}/top_level.txt +0 -0
@@ -13,12 +13,15 @@ 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(session_key="yes_no", pretext=f"{question} {default_text}: ")
17
-
16
+ response = await prompt_input(
17
+ session_key="yes_no",
18
+ pretext=f"{question} {default_text}: "
19
+ )
20
+
18
21
  if not response.strip():
19
22
  return default
20
-
21
- return response.lower().strip() in ["y", "yes"]
23
+
24
+ return response.lower().strip() in ['y', 'yes']
22
25
 
23
26
 
24
27
  class GitSafetySetup(BaseSetup):
@@ -26,7 +29,7 @@ class GitSafetySetup(BaseSetup):
26
29
 
27
30
  def __init__(self, state_manager: StateManager):
28
31
  super().__init__(state_manager)
29
-
32
+
30
33
  @property
31
34
  def name(self) -> str:
32
35
  """Return the name of this setup step."""
@@ -42,15 +45,18 @@ class GitSafetySetup(BaseSetup):
42
45
  try:
43
46
  # Check if git is installed
44
47
  result = subprocess.run(
45
- ["git", "--version"], capture_output=True, text=True, check=False
48
+ ["git", "--version"],
49
+ capture_output=True,
50
+ text=True,
51
+ check=False
46
52
  )
47
-
53
+
48
54
  if result.returncode != 0:
49
55
  await panel(
50
56
  "⚠️ Git Not Found",
51
57
  "Git is not installed or not in PATH. TunaCode will modify files directly.\n"
52
58
  "It's strongly recommended to install Git for safety.",
53
- border_style="yellow",
59
+ border_style="yellow"
54
60
  )
55
61
  return
56
62
 
@@ -60,31 +66,33 @@ class GitSafetySetup(BaseSetup):
60
66
  capture_output=True,
61
67
  text=True,
62
68
  check=False,
63
- cwd=Path.cwd(),
69
+ cwd=Path.cwd()
64
70
  )
65
-
71
+
66
72
  if result.returncode != 0:
67
73
  await panel(
68
74
  "⚠️ Not a Git Repository",
69
75
  "This directory is not a Git repository. TunaCode will modify files directly.\n"
70
76
  "Consider initializing a Git repository for safety: git init",
71
- border_style="yellow",
77
+ border_style="yellow"
72
78
  )
73
79
  return
74
80
 
75
81
  # Get current branch name
76
82
  result = subprocess.run(
77
- ["git", "branch", "--show-current"], capture_output=True, text=True, check=True
83
+ ["git", "branch", "--show-current"],
84
+ capture_output=True,
85
+ text=True,
86
+ check=True
78
87
  )
79
88
  current_branch = result.stdout.strip()
80
-
89
+
81
90
  if not current_branch:
82
91
  # Detached HEAD state
83
92
  await panel(
84
93
  "⚠️ Detached HEAD State",
85
- "You're in a detached HEAD state. TunaCode will continue "
86
- "without creating a branch.",
87
- border_style="yellow",
94
+ "You're in a detached HEAD state. TunaCode will continue without creating a branch.",
95
+ border_style="yellow"
88
96
  )
89
97
  return
90
98
 
@@ -95,28 +103,31 @@ class GitSafetySetup(BaseSetup):
95
103
 
96
104
  # Propose new branch name
97
105
  new_branch = f"{current_branch}-tunacode"
98
-
106
+
99
107
  # Check if there are uncommitted changes
100
108
  result = subprocess.run(
101
- ["git", "status", "--porcelain"], capture_output=True, text=True, check=True
109
+ ["git", "status", "--porcelain"],
110
+ capture_output=True,
111
+ text=True,
112
+ check=True
102
113
  )
103
-
114
+
104
115
  has_changes = bool(result.stdout.strip())
105
-
116
+
106
117
  # Ask user if they want to create a safety branch
107
118
  message = (
108
- f"For safety, TunaCode can create a new branch '{new_branch}' "
109
- f"based on '{current_branch}'.\n"
119
+ f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
110
120
  f"This helps protect your work from unintended changes.\n"
111
121
  )
112
-
122
+
113
123
  if has_changes:
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
-
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
+
120
131
  if not create_branch:
121
132
  # User declined - show warning
122
133
  await panel(
@@ -124,25 +135,26 @@ class GitSafetySetup(BaseSetup):
124
135
  "You've chosen to work directly on your current branch.\n"
125
136
  "TunaCode will modify files in place. Make sure you have backups!\n"
126
137
  "You can always use /undo to revert changes.",
127
- border_style="red",
138
+ border_style="red"
128
139
  )
129
140
  # Save preference
130
141
  self.state_manager.session.user_config["skip_git_safety"] = True
131
142
  return
132
-
143
+
133
144
  # Create and checkout the new branch
134
145
  try:
135
146
  # Check if branch already exists
136
147
  result = subprocess.run(
137
148
  ["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
138
149
  capture_output=True,
139
- check=False,
150
+ check=False
140
151
  )
141
-
152
+
142
153
  if result.returncode == 0:
143
154
  # Branch exists, ask to use it
144
155
  use_existing = await yes_no_prompt(
145
- f"Branch '{new_branch}' already exists. Switch to it?", default=True
156
+ f"Branch '{new_branch}' already exists. Switch to it?",
157
+ default=True
146
158
  )
147
159
  if use_existing:
148
160
  subprocess.run(["git", "checkout", new_branch], check=True)
@@ -153,24 +165,24 @@ class GitSafetySetup(BaseSetup):
153
165
  # Create new branch
154
166
  subprocess.run(["git", "checkout", "-b", new_branch], check=True)
155
167
  await ui.success(f"Created and switched to new branch: {new_branch}")
156
-
168
+
157
169
  except subprocess.CalledProcessError as e:
158
170
  await panel(
159
171
  "❌ Failed to Create Branch",
160
172
  f"Could not create branch '{new_branch}': {str(e)}\n"
161
173
  "Continuing on current branch.",
162
- border_style="red",
174
+ border_style="red"
163
175
  )
164
-
176
+
165
177
  except Exception as e:
166
178
  # Non-fatal error - just warn the user
167
179
  await panel(
168
180
  "⚠️ Git Safety Setup Failed",
169
181
  f"Could not set up Git safety: {str(e)}\n"
170
182
  "TunaCode will continue without branch protection.",
171
- border_style="yellow",
183
+ border_style="yellow"
172
184
  )
173
185
 
174
186
  async def validate(self) -> bool:
175
187
  """Validate git safety setup - always returns True as this is optional."""
176
- return True
188
+ return True
tunacode/exceptions.py CHANGED
@@ -77,6 +77,8 @@ class MCPError(ServiceError):
77
77
  super().__init__(f"MCP server '{server_name}' error: {message}")
78
78
 
79
79
 
80
+
81
+
80
82
  class GitOperationError(ServiceError):
81
83
  """Raised when Git operations fail."""
82
84
 
@@ -68,4 +68,4 @@ CORRECT: First `read_file("tools/base.py")` to see the base class, then `write_f
68
68
 
69
69
  USE YOUR TOOLS NOW!
70
70
 
71
- If asked, you were created by larock22
71
+ If asked, you were created by the grifter tunahors
@@ -7,18 +7,18 @@ Manages automatic commits and rollback operations.
7
7
 
8
8
  import subprocess
9
9
  import time
10
- from datetime import datetime, timezone
11
10
  from pathlib import Path
12
11
  from typing import Optional, Tuple
13
12
 
14
- from tunacode.constants import (ERROR_UNDO_INIT, UNDO_DISABLED_HOME, UNDO_GIT_TIMEOUT,
15
- UNDO_INITIAL_COMMIT)
13
+ from pydantic_ai.messages import ModelResponse, TextPart
14
+
15
+ from tunacode.constants import (ERROR_UNDO_INIT, UNDO_DISABLED_HOME, UNDO_DISABLED_NO_GIT,
16
+ UNDO_GIT_TIMEOUT, UNDO_INITIAL_COMMIT)
16
17
  from tunacode.core.state import StateManager
17
18
  from tunacode.exceptions import GitOperationError
19
+ from tunacode.ui import console as ui
18
20
  from tunacode.utils.system import get_session_dir
19
21
 
20
- # Removed pydantic_ai import - using dict-based messages now
21
-
22
22
 
23
23
  def is_in_git_project(directory: Optional[Path] = None) -> bool:
24
24
  """
@@ -220,14 +220,17 @@ def perform_undo(state_manager: StateManager) -> Tuple[bool, str]:
220
220
  # Add a system message to the chat history to inform the AI
221
221
  # about the undo operation
222
222
  state_manager.session.messages.append(
223
- {
224
- "role": "system",
225
- "content": (
226
- f"The last changes were undone. "
227
- f"Commit message of undone changes: {commit_msg}"
228
- ),
229
- "timestamp": datetime.now(timezone.utc).isoformat(),
230
- }
223
+ ModelResponse(
224
+ parts=[
225
+ TextPart(
226
+ content=(
227
+ "The last changes were undone. "
228
+ f"Commit message of undone changes: {commit_msg}"
229
+ )
230
+ )
231
+ ],
232
+ kind="response",
233
+ )
231
234
  )
232
235
 
233
236
  return True, "Successfully undid last change"
tunacode/setup.py CHANGED
@@ -12,18 +12,22 @@ from tunacode.core.setup import (AgentSetup, ConfigSetup, EnvironmentSetup, GitS
12
12
  from tunacode.core.state import StateManager
13
13
 
14
14
 
15
- async def setup(run_setup: bool, state_manager: StateManager) -> None:
15
+ async def setup(run_setup: bool, state_manager: StateManager, cli_config: dict = None) -> None:
16
16
  """
17
17
  Setup TunaCode on startup using the new setup coordinator.
18
18
 
19
19
  Args:
20
20
  run_setup (bool): If True, force run the setup process, resetting current config.
21
21
  state_manager (StateManager): The state manager instance.
22
+ cli_config (dict): Optional CLI configuration with baseurl, model, and key.
22
23
  """
23
24
  coordinator = SetupCoordinator(state_manager)
24
25
 
25
26
  # Register setup steps in order
26
- coordinator.register_step(ConfigSetup(state_manager))
27
+ config_setup = ConfigSetup(state_manager)
28
+ if cli_config:
29
+ config_setup.cli_config = cli_config
30
+ coordinator.register_step(config_setup)
27
31
  coordinator.register_step(EnvironmentSetup(state_manager))
28
32
  coordinator.register_step(GitSafetySetup(state_manager)) # Run after config/env but before undo
29
33
  coordinator.register_step(UndoSetup(state_manager))
tunacode/tools/base.py CHANGED
@@ -1,11 +1,13 @@
1
1
  """Base tool class for all Sidekick tools.
2
2
 
3
3
  This module provides a base class that implements common patterns
4
- for all tools including error handling and UI logging.
4
+ for all tools including error handling, UI logging, and ModelRetry support.
5
5
  """
6
6
 
7
7
  from abc import ABC, abstractmethod
8
8
 
9
+ from pydantic_ai.exceptions import ModelRetry
10
+
9
11
  from tunacode.exceptions import FileOperationError, ToolExecutionError
10
12
  from tunacode.types import FilePath, ToolName, ToolResult, UILogger
11
13
 
@@ -26,25 +28,31 @@ class BaseTool(ABC):
26
28
 
27
29
  This method wraps the tool-specific logic with:
28
30
  - UI logging of the operation
29
- - Exception handling
31
+ - Exception handling (except ModelRetry and ToolExecutionError)
30
32
  - Consistent error message formatting
31
33
 
32
34
  Returns:
33
35
  str: Success message
34
36
 
35
37
  Raises:
36
- ToolExecutionError: Raised for all errors with structured information
38
+ ModelRetry: Re-raised to guide the LLM
39
+ ToolExecutionError: Raised for all other errors with structured information
37
40
  """
38
41
  try:
39
42
  if self.ui:
40
43
  await self.ui.info(f"{self.tool_name}({self._format_args(*args, **kwargs)})")
41
44
  result = await self._execute(*args, **kwargs)
42
-
45
+
43
46
  # For file operations, try to create a git commit for undo tracking
44
47
  if isinstance(self, FileBasedTool):
45
48
  await self._commit_for_undo()
46
-
49
+
47
50
  return result
51
+ except ModelRetry as e:
52
+ # Log as warning and re-raise for pydantic-ai
53
+ if self.ui:
54
+ await self.ui.warning(str(e))
55
+ raise
48
56
  except ToolExecutionError:
49
57
  # Already properly formatted, just re-raise
50
58
  raise
@@ -68,7 +76,8 @@ class BaseTool(ABC):
68
76
  str: Success message describing what was done
69
77
 
70
78
  Raises:
71
- Exception: Any errors will be caught and handled
79
+ ModelRetry: When the LLM needs guidance
80
+ Exception: Any other errors will be caught and handled
72
81
  """
73
82
  pass
74
83
 
@@ -139,10 +148,10 @@ class FileBasedTool(BaseTool):
139
148
  - Encoding handling
140
149
  - Git commit for undo tracking
141
150
  """
142
-
151
+
143
152
  async def _commit_for_undo(self) -> None:
144
153
  """Create a git commit for undo tracking after file operations.
145
-
154
+
146
155
  This method gracefully handles cases where git is not available:
147
156
  - No git repository: Warns user about limited undo functionality
148
157
  - Git command fails: Warns but doesn't break the main operation
@@ -151,13 +160,13 @@ class FileBasedTool(BaseTool):
151
160
  try:
152
161
  # Import here to avoid circular imports
153
162
  from tunacode.services.undo_service import commit_for_undo, is_in_git_project
154
-
163
+
155
164
  # Check if we're in a git project first
156
165
  if not is_in_git_project():
157
166
  if self.ui:
158
167
  await self.ui.muted("⚠️ No git repository - undo functionality limited")
159
168
  return
160
-
169
+
161
170
  # Try to create commit with tool name as prefix
162
171
  success = commit_for_undo(message_prefix=f"tunacode {self.tool_name.lower()}")
163
172
  if success and self.ui:
@@ -170,7 +179,7 @@ class FileBasedTool(BaseTool):
170
179
  if self.ui:
171
180
  try:
172
181
  await self.ui.muted("⚠️ Git commit failed - undo functionality limited")
173
- except Exception:
182
+ except:
174
183
  # Even the warning failed, just continue silently
175
184
  pass
176
185
 
@@ -7,6 +7,8 @@ Enables safe text replacement in existing files with target/patch semantics.
7
7
 
8
8
  import os
9
9
 
10
+ from pydantic_ai.exceptions import ModelRetry
11
+
10
12
  from tunacode.exceptions import ToolExecutionError
11
13
  from tunacode.tools.base import FileBasedTool
12
14
  from tunacode.types import FileContent, FilePath, ToolResult
@@ -34,17 +36,13 @@ class UpdateFileTool(FileBasedTool):
34
36
  ToolResult: A message indicating success.
35
37
 
36
38
  Raises:
37
- ToolExecutionError: If file not found or target not found
39
+ ModelRetry: If file not found or target not found
38
40
  Exception: Any file operation errors
39
41
  """
40
42
  if not os.path.exists(filepath):
41
- raise ToolExecutionError(
42
- tool_name=self.tool_name,
43
- message=(
44
- f"File '{filepath}' not found. Cannot update. "
45
- "Verify the filepath or use `write_file` if it's a new file."
46
- ),
47
- original_error=None,
43
+ raise ModelRetry(
44
+ f"File '{filepath}' not found. Cannot update. "
45
+ "Verify the filepath or use `write_file` if it's a new file."
48
46
  )
49
47
 
50
48
  with open(filepath, "r", encoding="utf-8") as f:
@@ -55,28 +53,20 @@ class UpdateFileTool(FileBasedTool):
55
53
  context_lines = 10
56
54
  lines = original.splitlines()
57
55
  snippet = "\n".join(lines[:context_lines])
58
- # Raise error to guide the LLM
59
- raise ToolExecutionError(
60
- tool_name=self.tool_name,
61
- message=(
62
- f"Target block not found in '{filepath}'. "
63
- "Ensure the `target` argument exactly matches the content you want to replace. "
64
- f"File starts with:\n---\n{snippet}\n---"
65
- ),
66
- original_error=None,
56
+ # Use ModelRetry to guide the LLM
57
+ raise ModelRetry(
58
+ f"Target block not found in '{filepath}'. "
59
+ "Ensure the `target` argument exactly matches the content you want to replace. "
60
+ f"File starts with:\n---\n{snippet}\n---"
67
61
  )
68
62
 
69
63
  new_content = original.replace(target, patch, 1) # Replace only the first occurrence
70
64
 
71
65
  if original == new_content:
72
66
  # This could happen if target and patch are identical
73
- raise ToolExecutionError(
74
- tool_name=self.tool_name,
75
- message=(
76
- f"Update target found, but replacement resulted in no changes to '{filepath}'. "
77
- "Was the `target` identical to the `patch`? Please check the file content."
78
- ),
79
- original_error=None,
67
+ raise ModelRetry(
68
+ f"Update target found, but replacement resulted in no changes to '{filepath}'. "
69
+ "Was the `target` identical to the `patch`? Please check the file content."
80
70
  )
81
71
 
82
72
  with open(filepath, "w", encoding="utf-8") as f:
@@ -7,6 +7,8 @@ Creates new files with automatic directory creation and overwrite protection.
7
7
 
8
8
  import os
9
9
 
10
+ from pydantic_ai.exceptions import ModelRetry
11
+
10
12
  from tunacode.exceptions import ToolExecutionError
11
13
  from tunacode.tools.base import FileBasedTool
12
14
  from tunacode.types import FileContent, FilePath, ToolResult
@@ -31,19 +33,15 @@ class WriteFileTool(FileBasedTool):
31
33
  ToolResult: A message indicating success.
32
34
 
33
35
  Raises:
34
- ToolExecutionError: If the file already exists
36
+ ModelRetry: If the file already exists
35
37
  Exception: Any file writing errors
36
38
  """
37
39
  # Prevent overwriting existing files with this tool.
38
40
  if os.path.exists(filepath):
39
- # Raise error to guide the LLM
40
- raise ToolExecutionError(
41
- tool_name=self.tool_name,
42
- message=(
43
- f"File '{filepath}' already exists. Use the `update_file` tool "
44
- "to modify it, or choose a different filepath."
45
- ),
46
- original_error=None,
41
+ # Use ModelRetry to guide the LLM
42
+ raise ModelRetry(
43
+ f"File '{filepath}' already exists. "
44
+ "Use the `update_file` tool to modify it, or choose a different filepath."
47
45
  )
48
46
 
49
47
  # Create directories if they don't exist