tunacode-cli 0.0.9__py3-none-any.whl → 0.0.10__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 +148 -78
  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.10.dist-info/METADATA +366 -0
  32. tunacode_cli-0.0.10.dist-info/RECORD +65 -0
  33. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.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.10.dist-info}/WHEEL +0 -0
  45. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/entry_points.txt +0 -0
  46. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/top_level.txt +0 -0
@@ -1,311 +0,0 @@
1
- """
2
- Project-local undo service that stores backups in the working directory.
3
- Designed for pip-installed CLI tool usage.
4
- """
5
-
6
- import json
7
- import shutil
8
- import time
9
- from datetime import datetime
10
- from pathlib import Path
11
- from typing import Dict, Optional, Tuple
12
-
13
- from tunacode.core.state import StateManager
14
-
15
-
16
- class ProjectUndoService:
17
- """Undo system that stores backups in .tunacode/ within the project."""
18
-
19
- # Directory name for local undo data
20
- UNDO_DIR_NAME = ".tunacode"
21
- BACKUP_SUBDIR = "backups"
22
- GITIGNORE_CONTENT = """# TunaCode undo system files
23
- *
24
- !.gitignore
25
- """
26
-
27
- def __init__(self, state_manager: StateManager, project_dir: Optional[Path] = None):
28
- self.state_manager = state_manager
29
- self.project_dir = Path(project_dir or Path.cwd())
30
-
31
- # Local undo directory in the project
32
- self.undo_dir = self.project_dir / self.UNDO_DIR_NAME
33
- self.backup_dir = self.undo_dir / self.BACKUP_SUBDIR
34
- self.op_log_file = self.undo_dir / "operations.jsonl"
35
-
36
- # Git directory (if project uses git)
37
- self.git_dir = self.project_dir / ".git"
38
-
39
- self._init_undo_directory()
40
-
41
- def _init_undo_directory(self):
42
- """Initialize the .tunacode directory in the project."""
43
- try:
44
- # Create directories
45
- self.undo_dir.mkdir(exist_ok=True)
46
- self.backup_dir.mkdir(exist_ok=True)
47
-
48
- # Create .gitignore to exclude undo files from version control
49
- gitignore_path = self.undo_dir / ".gitignore"
50
- if not gitignore_path.exists():
51
- gitignore_path.write_text(self.GITIGNORE_CONTENT)
52
-
53
- # Create operation log
54
- if not self.op_log_file.exists():
55
- self.op_log_file.touch()
56
-
57
- # Add informational README
58
- readme_path = self.undo_dir / "README.md"
59
- if not readme_path.exists():
60
- readme_path.write_text(
61
- """# TunaCode Undo System
62
-
63
- This directory contains local backup files created by TunaCode to enable undo functionality.
64
-
65
- ## Contents:
66
- - `backups/` - Timestamped file backups
67
- - `operations.jsonl` - Operation history log
68
- - `.gitignore` - Excludes these files from git
69
-
70
- ## Notes:
71
- - These files are local to your machine
72
- - Safe to delete if you don't need undo history
73
- - Automatically cleaned up (keeps last 50 backups)
74
- - Not committed to version control
75
-
76
- Created by TunaCode (https://github.com/larock22/tunacode)
77
- """
78
- )
79
-
80
- except PermissionError:
81
- print(f"⚠️ Cannot create {self.UNDO_DIR_NAME}/ directory - undo will be limited")
82
- except Exception as e:
83
- print(f"⚠️ Error initializing undo directory: {e}")
84
-
85
- def should_track_file(self, filepath: Path) -> bool:
86
- """Check if a file should be tracked by undo system."""
87
- # Don't track files in our own undo directory
88
- try:
89
- if self.undo_dir in filepath.parents:
90
- return False
91
- except ValueError:
92
- pass
93
-
94
- # Don't track hidden files/directories (except .gitignore, .env, etc)
95
- parts = filepath.parts
96
- for part in parts:
97
- if part.startswith(".") and part not in {".gitignore", ".env", ".envrc"}:
98
- return False
99
-
100
- # Don't track common build/cache directories
101
- exclude_dirs = {
102
- "node_modules",
103
- "__pycache__",
104
- ".pytest_cache",
105
- "dist",
106
- "build",
107
- ".next",
108
- ".nuxt",
109
- "target",
110
- }
111
- if any(excluded in parts for excluded in exclude_dirs):
112
- return False
113
-
114
- return True
115
-
116
- def backup_file(self, filepath: Path) -> Optional[Path]:
117
- """Create a timestamped backup of a file."""
118
- # Check if we should track this file
119
- if not self.should_track_file(filepath):
120
- return None
121
-
122
- try:
123
- # Use relative path for organizing backups
124
- rel_path = filepath.relative_to(self.project_dir)
125
-
126
- # Create subdirectories in backup dir to mirror project structure
127
- backup_subdir = self.backup_dir / rel_path.parent
128
- backup_subdir.mkdir(parents=True, exist_ok=True)
129
-
130
- # Create backup filename with timestamp
131
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
132
- backup_name = f"{filepath.name}.{timestamp}.bak"
133
- backup_path = backup_subdir / backup_name
134
-
135
- # Copy the file if it exists
136
- if filepath.exists():
137
- shutil.copy2(filepath, backup_path)
138
- else:
139
- # Create empty backup for new files
140
- backup_path.touch()
141
-
142
- # Log the backup
143
- self._log_operation(
144
- {
145
- "type": "backup",
146
- "file": str(rel_path),
147
- "backup": str(backup_path.relative_to(self.undo_dir)),
148
- "timestamp": datetime.now().isoformat(),
149
- }
150
- )
151
-
152
- # Clean up old backups for this file
153
- self._cleanup_file_backups(backup_subdir, filepath.name, keep=10)
154
-
155
- return backup_path
156
-
157
- except Exception:
158
- # Silent fail - don't interrupt user's work
159
- return None
160
-
161
- def _cleanup_file_backups(self, backup_dir: Path, filename: str, keep: int = 10):
162
- """Keep only the most recent backups for a specific file."""
163
- try:
164
- # Find all backups for this file
165
- pattern = f"{filename}.*.bak"
166
- backups = sorted(backup_dir.glob(pattern), key=lambda p: p.stat().st_mtime)
167
-
168
- # Remove old backups
169
- if len(backups) > keep:
170
- for old_backup in backups[:-keep]:
171
- old_backup.unlink()
172
-
173
- except Exception:
174
- pass # Silent cleanup failure
175
-
176
- def _log_operation(self, operation: Dict):
177
- """Log an operation to the operations file."""
178
- try:
179
- with open(self.op_log_file, "a") as f:
180
- json.dump(operation, f)
181
- f.write("\n")
182
-
183
- # Keep log file size reasonable (last 1000 operations)
184
- self._trim_log_file(1000)
185
-
186
- except Exception:
187
- pass
188
-
189
- def _trim_log_file(self, max_lines: int):
190
- """Keep only the last N operations in the log."""
191
- try:
192
- with open(self.op_log_file, "r") as f:
193
- lines = f.readlines()
194
-
195
- if len(lines) > max_lines:
196
- with open(self.op_log_file, "w") as f:
197
- f.writelines(lines[-max_lines:])
198
-
199
- except Exception:
200
- pass
201
-
202
- def get_undo_status(self) -> Dict[str, any]:
203
- """Get current status of undo system."""
204
- try:
205
- backup_count = sum(1 for _ in self.backup_dir.rglob("*.bak"))
206
- backup_size = sum(f.stat().st_size for f in self.backup_dir.rglob("*.bak"))
207
- log_size = self.op_log_file.stat().st_size if self.op_log_file.exists() else 0
208
-
209
- return {
210
- "enabled": True,
211
- "location": str(self.undo_dir),
212
- "backup_count": backup_count,
213
- "backup_size_mb": backup_size / (1024 * 1024),
214
- "log_size_kb": log_size / 1024,
215
- "git_available": self.git_dir.exists(),
216
- }
217
- except Exception:
218
- return {"enabled": False, "error": "Cannot access undo directory"}
219
-
220
- def cleanup_old_backups(self, days: int = 7):
221
- """Remove backups older than specified days."""
222
- try:
223
- cutoff_time = time.time() - (days * 24 * 60 * 60)
224
-
225
- for backup in self.backup_dir.rglob("*.bak"):
226
- if backup.stat().st_mtime < cutoff_time:
227
- backup.unlink()
228
-
229
- except Exception:
230
- pass
231
-
232
-
233
- class ProjectSafeFileOperations:
234
- """File operations with project-local backup."""
235
-
236
- def __init__(self, undo_service: ProjectUndoService):
237
- self.undo = undo_service
238
-
239
- async def safe_write(self, filepath: Path, content: str) -> Tuple[bool, str]:
240
- """Write file with automatic local backup."""
241
- filepath = Path(filepath).resolve()
242
-
243
- # Create backup before write
244
- backup_path = self.undo.backup_file(filepath)
245
-
246
- # Check if file exists for logging
247
- if filepath.exists() and filepath.stat().st_size < 100_000:
248
- try:
249
- filepath.read_text() # Just check it's readable
250
- except Exception:
251
- pass
252
-
253
- # Write the file
254
- try:
255
- filepath.parent.mkdir(parents=True, exist_ok=True)
256
- filepath.write_text(content)
257
-
258
- # Log the operation
259
- self.undo._log_operation(
260
- {
261
- "type": "write",
262
- "file": str(filepath.relative_to(self.undo.project_dir)),
263
- "size": len(content),
264
- "backup": (
265
- str(backup_path.relative_to(self.undo.undo_dir)) if backup_path else None
266
- ),
267
- "timestamp": datetime.now().isoformat(),
268
- }
269
- )
270
-
271
- return True, "File written successfully"
272
-
273
- except Exception as e:
274
- return False, f"Failed to write file: {e}"
275
-
276
- async def safe_delete(self, filepath: Path) -> Tuple[bool, str]:
277
- """Delete file with automatic local backup."""
278
- filepath = Path(filepath).resolve()
279
-
280
- if not filepath.exists():
281
- return False, "File does not exist"
282
-
283
- # Create backup before delete
284
- backup_path = self.undo.backup_file(filepath)
285
-
286
- try:
287
- filepath.unlink()
288
-
289
- # Log the operation
290
- self.undo._log_operation(
291
- {
292
- "type": "delete",
293
- "file": str(filepath.relative_to(self.undo.project_dir)),
294
- "backup": (
295
- str(backup_path.relative_to(self.undo.undo_dir)) if backup_path else None
296
- ),
297
- "timestamp": datetime.now().isoformat(),
298
- }
299
- )
300
-
301
- return True, f"File deleted (backup available in {self.undo.UNDO_DIR_NAME}/)"
302
-
303
- except Exception as e:
304
- return False, f"Failed to delete file: {e}"
305
-
306
-
307
- def get_project_undo_service(state_manager: StateManager) -> ProjectUndoService:
308
- """Get or create project-local undo service."""
309
- if not hasattr(state_manager.session, "project_undo"):
310
- state_manager.session.project_undo = ProjectUndoService(state_manager)
311
- return state_manager.session.project_undo
@@ -1,103 +0,0 @@
1
- """TinyAgent tool implementations with decorators."""
2
-
3
- from typing import Optional
4
-
5
- from tinyagent import tool
6
-
7
- from tunacode.exceptions import ToolExecutionError
8
- from tunacode.ui import console as ui
9
-
10
- # Import the existing tool classes to reuse their logic
11
- from .read_file import ReadFileTool
12
- from .run_command import RunCommandTool
13
- from .update_file import UpdateFileTool
14
- from .write_file import WriteFileTool
15
-
16
-
17
- @tool
18
- async def read_file(filepath: str) -> str:
19
- """Read the contents of a file.
20
-
21
- Args:
22
- filepath: The path to the file to read.
23
-
24
- Returns:
25
- The contents of the file.
26
-
27
- Raises:
28
- Exception: If file cannot be read.
29
- """
30
- tool_instance = ReadFileTool(ui)
31
- try:
32
- result = await tool_instance.execute(filepath)
33
- return result
34
- except ToolExecutionError as e:
35
- # tinyAgent expects exceptions to be raised, not returned as strings
36
- raise Exception(str(e))
37
-
38
-
39
- @tool
40
- async def write_file(filepath: str, content: str) -> str:
41
- """Write content to a file.
42
-
43
- Args:
44
- filepath: The path to the file to write.
45
- content: The content to write to the file.
46
-
47
- Returns:
48
- Success message.
49
-
50
- Raises:
51
- Exception: If file cannot be written.
52
- """
53
- tool_instance = WriteFileTool(ui)
54
- try:
55
- result = await tool_instance.execute(filepath, content)
56
- return result
57
- except ToolExecutionError as e:
58
- raise Exception(str(e))
59
-
60
-
61
- @tool
62
- async def update_file(filepath: str, old_content: str, new_content: str) -> str:
63
- """Update specific content in a file.
64
-
65
- Args:
66
- filepath: The path to the file to update.
67
- old_content: The content to find and replace.
68
- new_content: The new content to insert.
69
-
70
- Returns:
71
- Success message.
72
-
73
- Raises:
74
- Exception: If file cannot be updated.
75
- """
76
- tool_instance = UpdateFileTool(ui)
77
- try:
78
- result = await tool_instance.execute(filepath, old_content, new_content)
79
- return result
80
- except ToolExecutionError as e:
81
- raise Exception(str(e))
82
-
83
-
84
- @tool
85
- async def run_command(command: str, timeout: Optional[int] = None) -> str:
86
- """Run a shell command.
87
-
88
- Args:
89
- command: The command to run.
90
- timeout: Optional timeout in seconds.
91
-
92
- Returns:
93
- The command output.
94
-
95
- Raises:
96
- Exception: If command fails.
97
- """
98
- tool_instance = RunCommandTool(ui)
99
- try:
100
- result = await tool_instance.execute(command, timeout)
101
- return result
102
- except ToolExecutionError as e:
103
- raise Exception(str(e))
@@ -1,59 +0,0 @@
1
- """Lazy imports for heavy modules to improve startup time."""
2
-
3
- import sys
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- # For type checking only
8
- import prompt_toolkit # noqa: F401
9
- import rich # noqa: F401
10
- from prompt_toolkit import PromptSession # noqa: F401
11
- from prompt_toolkit.completion import Completer # noqa: F401
12
- from rich.console import Console # noqa: F401
13
- from rich.markdown import Markdown # noqa: F401
14
- from rich.panel import Panel # noqa: F401
15
- from rich.table import Table # noqa: F401
16
-
17
-
18
- def lazy_import(module_name: str):
19
- """Lazy import a module."""
20
- if module_name not in sys.modules:
21
- __import__(module_name)
22
- return sys.modules[module_name]
23
-
24
-
25
- # Lazy accessors
26
- def get_rich():
27
- """Get rich module lazily."""
28
- return lazy_import("rich")
29
-
30
-
31
- def get_rich_console():
32
- """Get rich console lazily."""
33
- rich_console = lazy_import("rich.console")
34
- return rich_console.Console
35
-
36
-
37
- def get_rich_table():
38
- """Get rich table lazily."""
39
- return lazy_import("rich.table").Table
40
-
41
-
42
- def get_rich_panel():
43
- """Get rich panel lazily."""
44
- return lazy_import("rich.panel").Panel
45
-
46
-
47
- def get_rich_markdown():
48
- """Get rich markdown lazily."""
49
- return lazy_import("rich.markdown").Markdown
50
-
51
-
52
- def get_prompt_toolkit():
53
- """Get prompt_toolkit lazily."""
54
- return lazy_import("prompt_toolkit")
55
-
56
-
57
- def get_prompt_session():
58
- """Get PromptSession lazily."""
59
- return lazy_import("prompt_toolkit").PromptSession
@@ -1,33 +0,0 @@
1
- """Pre-compiled regex patterns for better performance."""
2
-
3
- import re
4
-
5
- # Command patterns
6
- MODEL_COMMAND_PATTERN = re.compile(r"(?:^|\n)\s*(?:/model|/m)\s+\S*$")
7
- COMMAND_START_PATTERN = re.compile(r"^/\w+")
8
-
9
- # File reference patterns
10
- FILE_REF_PATTERN = re.compile(r"@([^\s]+)")
11
- FILE_PATH_PATTERN = re.compile(r"^[a-zA-Z0-9_\-./]+$")
12
-
13
- # Code patterns
14
- IMPORT_PATTERN = re.compile(r"^\s*(?:from|import)\s+\S+")
15
- FUNCTION_DEF_PATTERN = re.compile(r"^\s*def\s+(\w+)\s*\(")
16
- CLASS_DEF_PATTERN = re.compile(r"^\s*class\s+(\w+)")
17
-
18
- # Environment variable patterns
19
- ENV_VAR_PATTERN = re.compile(r"\$\{(\w+)(?::([^}]*))?\}")
20
- API_KEY_PATTERN = re.compile(r"_API_KEY$")
21
-
22
- # Common text patterns
23
- WHITESPACE_PATTERN = re.compile(r"\s+")
24
- WORD_BOUNDARY_PATTERN = re.compile(r"\b\w+\b")
25
- LINE_SPLIT_PATTERN = re.compile(r"\r?\n")
26
-
27
- # Tool output patterns
28
- ANSI_ESCAPE_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
29
- ERROR_PATTERN = re.compile(r"(?i)(error|exception|failed|failure):\s*(.+)")
30
-
31
- # Model name patterns
32
- MODEL_PROVIDER_PATTERN = re.compile(r"^(\w+):(.+)$")
33
- OPENROUTER_MODEL_PATTERN = re.compile(r"^openrouter:(.+)$")