tunacode-cli 0.0.33__py3-none-any.whl → 0.0.35__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 CHANGED
@@ -6,7 +6,7 @@ from enum import Enum
6
6
  from typing import Any, Dict, List, Optional, Type
7
7
 
8
8
  from .. import utils
9
- from ..exceptions import ValidationError
9
+ from ..exceptions import ConfigurationError, ValidationError
10
10
  from ..types import CommandArgs, CommandContext, CommandResult, ProcessRequestCallback
11
11
  from ..ui import console as ui
12
12
 
@@ -181,26 +181,29 @@ class IterationsCommand(SimpleCommand):
181
181
 
182
182
  async def execute(self, args: List[str], context: CommandContext) -> None:
183
183
  state = context.state_manager.session
184
- if args:
185
- try:
186
- new_limit = int(args[0])
187
- if new_limit < 1 or new_limit > 100:
188
- await ui.error("Iterations must be between 1 and 100")
189
- return
190
-
191
- # Update the user config
192
- if "settings" not in state.user_config:
193
- state.user_config["settings"] = {}
194
- state.user_config["settings"]["max_iterations"] = new_limit
195
-
196
- await ui.success(f"Maximum iterations set to {new_limit}")
197
- await ui.muted("Higher values allow more complex reasoning but may be slower")
198
- except ValueError:
199
- await ui.error("Please provide a valid number")
200
- else:
184
+
185
+ # Guard clause - handle "no args" case first and return early
186
+ if not args:
201
187
  current = state.user_config.get("settings", {}).get("max_iterations", 40)
202
188
  await ui.info(f"Current maximum iterations: {current}")
203
189
  await ui.muted("Usage: /iterations <number> (1-100)")
190
+ return
191
+
192
+ # update the logic to not be as nested messely, the above guars needing to get as messy
193
+ try:
194
+ new_limit = int(args[0])
195
+ if new_limit < 1 or new_limit > 100:
196
+ await ui.error("Iterations must be between 1 and 100")
197
+ return
198
+
199
+ # Update the user config
200
+ if "settings" not in state.user_config:
201
+ state.user_config["settings"] = {}
202
+ state.user_config["settings"]["max_iterations"] = new_limit
203
+
204
+ await ui.success(f"Maximum iterations set to {new_limit}")
205
+ except ValueError:
206
+ await ui.error("Please provide a valid number")
204
207
 
205
208
 
206
209
  class ClearCommand(SimpleCommand):
@@ -288,7 +291,9 @@ class ParseToolsCommand(SimpleCommand):
288
291
 
289
292
  try:
290
293
  await extract_and_execute_tool_calls(
291
- part.content, tool_callback_with_state, context.state_manager
294
+ part.content,
295
+ tool_callback_with_state,
296
+ context.state_manager,
292
297
  )
293
298
  await ui.success("JSON tool parsing completed")
294
299
  found_content = True
@@ -524,7 +529,8 @@ class UpdateCommand(SimpleCommand):
524
529
  result = subprocess.run(
525
530
  ["pipx", "list"], capture_output=True, text=True, timeout=10
526
531
  )
527
- if "tunacode" in result.stdout.lower():
532
+ pipx_installed = "tunacode" in result.stdout.lower()
533
+ if pipx_installed:
528
534
  installation_method = "pipx"
529
535
  except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
530
536
  pass
@@ -555,12 +561,22 @@ class UpdateCommand(SimpleCommand):
555
561
  if installation_method == "pipx":
556
562
  await ui.info("Updating via pipx...")
557
563
  result = subprocess.run(
558
- ["pipx", "upgrade", "tunacode"], capture_output=True, text=True, timeout=60
564
+ ["pipx", "upgrade", "tunacode"],
565
+ capture_output=True,
566
+ text=True,
567
+ timeout=60,
559
568
  )
560
569
  else: # pip
561
570
  await ui.info("Updating via pip...")
562
571
  result = subprocess.run(
563
- [sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
572
+ [
573
+ sys.executable,
574
+ "-m",
575
+ "pip",
576
+ "install",
577
+ "--upgrade",
578
+ "tunacode-cli",
579
+ ],
564
580
  capture_output=True,
565
581
  text=True,
566
582
  timeout=60,
@@ -628,9 +644,13 @@ class ModelCommand(SimpleCommand):
628
644
 
629
645
  # Check if setting as default
630
646
  if len(args) > 1 and args[1] == "default":
631
- utils.user_configuration.set_default_model(model_name, context.state_manager)
632
- await ui.muted("Updating default model")
633
- return "restart"
647
+ try:
648
+ utils.user_configuration.set_default_model(model_name, context.state_manager)
649
+ await ui.muted("Updating default model")
650
+ return "restart"
651
+ except ConfigurationError as e:
652
+ await ui.error(str(e))
653
+ return None
634
654
 
635
655
  # Show success message with the new model
636
656
  await ui.success(f"Switched to model: {model_name}")
@@ -669,6 +689,35 @@ class CommandFactory:
669
689
  setattr(self.dependencies, key, value)
670
690
 
671
691
 
692
+ class InitCommand(SimpleCommand):
693
+ """Creates or updates TUNACODE.md with project-specific context."""
694
+
695
+ spec = CommandSpec(
696
+ name="/init",
697
+ aliases=[],
698
+ description="Analyze codebase and create/update TUNACODE.md file",
699
+ category=CommandCategory.DEVELOPMENT,
700
+ )
701
+
702
+ async def execute(self, args, context: CommandContext) -> CommandResult:
703
+ """Execute the init command."""
704
+ # Minimal implementation to make test pass
705
+ prompt = """Please analyze this codebase and create a TUNACODE.md file containing:
706
+ 1. Build/lint/test commands - especially for running a single test
707
+ 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
708
+
709
+ The file you create will be given to agentic coding agents (such as yourself) that operate in this repository.
710
+ Make it about 20 lines long.
711
+ If there's already a TUNACODE.md, improve it.
712
+ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md),
713
+ make sure to include them."""
714
+
715
+ # Call the agent to analyze and create/update the file
716
+ await context.process_request(prompt, context.state_manager)
717
+
718
+ return None
719
+
720
+
672
721
  class CommandRegistry:
673
722
  """Registry for managing commands with auto-discovery and categories."""
674
723
 
@@ -726,6 +775,7 @@ class CommandRegistry:
726
775
  BranchCommand,
727
776
  CompactCommand,
728
777
  ModelCommand,
778
+ InitCommand,
729
779
  ]
730
780
 
731
781
  # Register all discovered commands
tunacode/cli/repl.py CHANGED
@@ -22,6 +22,7 @@ from tunacode.core.tool_handler import ToolHandler
22
22
  from tunacode.exceptions import AgentError, UserAbortError, ValidationError
23
23
  from tunacode.ui import console as ui
24
24
  from tunacode.ui.tool_ui import ToolUI
25
+ from tunacode.utils.security import CommandSecurityError, safe_subprocess_run
25
26
 
26
27
  from ..types import CommandContext, CommandResult, StateManager, ToolArgs
27
28
  from .commands import CommandRegistry
@@ -320,13 +321,24 @@ async def repl(state_manager: StateManager):
320
321
  def run_shell():
321
322
  try:
322
323
  if command:
323
- result = subprocess.run(command, shell=True, capture_output=False)
324
- if result.returncode != 0:
325
- # Use print directly since we're in a terminal context
326
- print(f"\nCommand exited with code {result.returncode}")
324
+ # Use secure subprocess execution for shell commands
325
+ # Note: User shell commands are inherently risky but this is by design
326
+ # We validate but allow shell features since it's explicit user intent
327
+ try:
328
+ result = safe_subprocess_run(
329
+ command,
330
+ shell=True,
331
+ validate=True, # Still validate for basic safety
332
+ capture_output=False,
333
+ )
334
+ if result.returncode != 0:
335
+ print(f"\nCommand exited with code {result.returncode}")
336
+ except CommandSecurityError as e:
337
+ print(f"\nSecurity validation failed: {str(e)}")
338
+ print("If you need to run this command, please ensure it's safe.")
327
339
  else:
328
340
  shell = os.environ.get("SHELL", "bash")
329
- subprocess.run(shell)
341
+ subprocess.run(shell) # Interactive shell is safe
330
342
  except Exception as e:
331
343
  print(f"\nShell command failed: {str(e)}")
332
344
 
tunacode/constants.py CHANGED
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.33"
10
+ APP_VERSION = "0.0.35"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -457,6 +457,26 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
457
457
  # Use a default system prompt if neither file exists
458
458
  system_prompt = "You are a helpful AI assistant for software development tasks."
459
459
 
460
+ # Load TUNACODE.md context
461
+ # Use sync version of get_code_style to avoid nested event loop issues
462
+ try:
463
+ from pathlib import Path as PathlibPath
464
+
465
+ tunacode_path = PathlibPath.cwd() / "TUNACODE.md"
466
+ if tunacode_path.exists():
467
+ tunacode_content = tunacode_path.read_text(encoding="utf-8")
468
+ if tunacode_content.strip():
469
+ # Log that we found TUNACODE.md
470
+ print("📄 TUNACODE.md located: Loading context...")
471
+
472
+ system_prompt += "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
473
+ else:
474
+ # Log that TUNACODE.md was not found
475
+ print("📄 TUNACODE.md not found: Using default context")
476
+ except Exception:
477
+ # Ignore errors loading TUNACODE.md
478
+ pass
479
+
460
480
  state_manager.session.agents[model] = Agent(
461
481
  model=model,
462
482
  system_prompt=system_prompt,
@@ -670,6 +690,14 @@ async def process_request(
670
690
  # Create a request-level buffer for batching read-only tools across nodes
671
691
  tool_buffer = ToolBuffer()
672
692
 
693
+ # Show TUNACODE.md preview if it was loaded and thoughts are enabled
694
+ if state_manager.session.show_thoughts and hasattr(state_manager, "tunacode_preview"):
695
+ from tunacode.ui import console as ui
696
+
697
+ await ui.muted(state_manager.tunacode_preview)
698
+ # Clear the preview after displaying it once
699
+ delattr(state_manager, "tunacode_preview")
700
+
673
701
  # Show what we're sending to the API when thoughts are enabled
674
702
  if state_manager.session.show_thoughts:
675
703
  from tunacode.ui import console as ui
@@ -82,9 +82,13 @@ class ConfigSetup(BaseSetup):
82
82
  ):
83
83
  self.state_manager.session.user_config = {}
84
84
  self.state_manager.session.user_config = DEFAULT_USER_CONFIG.copy()
85
- user_configuration.save_config(
86
- self.state_manager
87
- ) # Save the default config initially
85
+ try:
86
+ user_configuration.save_config(
87
+ self.state_manager
88
+ ) # Save the default config initially
89
+ except ConfigurationError as e:
90
+ await ui.error(str(e))
91
+ raise
88
92
  await self._onboarding()
89
93
  else:
90
94
  # No config found - show CLI usage instead of onboarding
@@ -172,11 +176,12 @@ class ConfigSetup(BaseSetup):
172
176
  # Compare configs to see if anything changed
173
177
  current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
174
178
  if initial_config != current_config:
175
- if user_configuration.save_config(self.state_manager):
179
+ try:
180
+ user_configuration.save_config(self.state_manager)
176
181
  message = f"Config saved to: [bold]{self.config_file}[/bold]"
177
182
  await ui.panel("Finished", message, top=0, border_style=UI_COLORS["success"])
178
- else:
179
- await ui.error("Failed to save configuration.")
183
+ except ConfigurationError as e:
184
+ await ui.error(str(e))
180
185
  else:
181
186
  await ui.panel(
182
187
  "Setup canceled",
@@ -320,8 +325,9 @@ class ConfigSetup(BaseSetup):
320
325
  ]
321
326
 
322
327
  # Save the configuration
323
- if user_configuration.save_config(self.state_manager):
328
+ try:
329
+ user_configuration.save_config(self.state_manager)
324
330
  await ui.warning("Model set without validation - verify the model name is correct")
325
331
  await ui.success(f"Configuration saved to: {self.config_file}")
326
- else:
327
- await ui.error("Failed to save configuration.")
332
+ except ConfigurationError as e:
333
+ await ui.error(str(e))
@@ -14,6 +14,7 @@ from tunacode.constants import (CMD_OUTPUT_FORMAT, CMD_OUTPUT_NO_ERRORS, CMD_OUT
14
14
  from tunacode.exceptions import ToolExecutionError
15
15
  from tunacode.tools.base import BaseTool
16
16
  from tunacode.types import ToolResult
17
+ from tunacode.utils.security import CommandSecurityError, safe_subprocess_popen
17
18
 
18
19
 
19
20
  class RunCommandTool(BaseTool):
@@ -34,16 +35,23 @@ class RunCommandTool(BaseTool):
34
35
 
35
36
  Raises:
36
37
  FileNotFoundError: If command not found
38
+ CommandSecurityError: If command fails security validation
37
39
  Exception: Any command execution errors
38
40
  """
39
- process = subprocess.Popen(
40
- command,
41
- shell=True,
42
- stdout=subprocess.PIPE,
43
- stderr=subprocess.PIPE,
44
- text=True,
45
- )
46
- stdout, stderr = process.communicate()
41
+ try:
42
+ # Use secure subprocess execution with validation
43
+ process = safe_subprocess_popen(
44
+ command,
45
+ shell=True, # CLI tool requires shell features
46
+ validate=True, # Enable security validation
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.PIPE,
49
+ text=True,
50
+ )
51
+ stdout, stderr = process.communicate()
52
+ except CommandSecurityError as e:
53
+ # Security validation failed - return error without execution
54
+ return f"Security validation failed: {str(e)}"
47
55
  output = stdout.strip() or CMD_OUTPUT_NO_OUTPUT
48
56
  error = stderr.strip() or CMD_OUTPUT_NO_ERRORS
49
57
  resp = CMD_OUTPUT_FORMAT.format(output=output, error=error).strip()
@@ -70,6 +78,8 @@ class RunCommandTool(BaseTool):
70
78
  """
71
79
  if isinstance(error, FileNotFoundError):
72
80
  err_msg = ERROR_COMMAND_EXECUTION.format(command=command, error=error)
81
+ elif isinstance(error, CommandSecurityError):
82
+ err_msg = f"Command blocked for security: {str(error)}"
73
83
  else:
74
84
  # Use parent class handling for other errors
75
85
  await super()._handle_error(error, command)
@@ -0,0 +1,208 @@
1
+ """
2
+ Security utilities for safe command execution and input validation.
3
+ Provides defensive measures against command injection attacks.
4
+ """
5
+
6
+ import logging
7
+ import re
8
+ import shlex
9
+ import subprocess
10
+ from typing import List, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Dangerous shell metacharacters that indicate potential injection
15
+ DANGEROUS_CHARS = [
16
+ ";",
17
+ "&",
18
+ "|",
19
+ "`",
20
+ "$",
21
+ "(",
22
+ ")",
23
+ "{",
24
+ "}",
25
+ "<",
26
+ ">",
27
+ "\n",
28
+ "\r",
29
+ "\\",
30
+ '"',
31
+ "'",
32
+ ]
33
+
34
+ # Common injection patterns
35
+ INJECTION_PATTERNS = [
36
+ r";\s*\w+", # Command chaining with semicolon
37
+ r"&&\s*\w+", # Command chaining with &&
38
+ r"\|\s*\w+", # Piping to another command
39
+ r"`[^`]+`", # Command substitution with backticks
40
+ r"\$\([^)]+\)", # Command substitution with $()
41
+ r">\s*[/\w]", # Output redirection
42
+ r"<\s*[/\w]", # Input redirection
43
+ ]
44
+
45
+
46
+ class CommandSecurityError(Exception):
47
+ """Raised when a command fails security validation."""
48
+
49
+ pass
50
+
51
+
52
+ def validate_command_safety(command: str, allow_shell_features: bool = False) -> None:
53
+ """
54
+ Validate that a command is safe to execute.
55
+
56
+ Args:
57
+ command: The command string to validate
58
+ allow_shell_features: If True, allows some shell features like pipes
59
+
60
+ Raises:
61
+ CommandSecurityError: If the command contains potentially dangerous patterns
62
+ """
63
+ if not command or not command.strip():
64
+ raise CommandSecurityError("Empty command not allowed")
65
+
66
+ # Log the command being validated
67
+ logger.info(f"Validating command: {command[:100]}...")
68
+
69
+ # Always check for the most dangerous patterns regardless of shell features
70
+ dangerous_patterns = [
71
+ r"rm\s+-rf\s+/", # Dangerous rm commands
72
+ r"sudo\s+rm", # Sudo rm commands
73
+ r">\s*/dev/sd[a-z]", # Writing to disk devices
74
+ r"dd\s+.*of=/dev/", # DD to devices
75
+ r"mkfs\.", # Format filesystem
76
+ r"fdisk", # Partition manipulation
77
+ r":\(\)\{.*\}\;", # Fork bomb pattern
78
+ ]
79
+
80
+ for pattern in dangerous_patterns:
81
+ if re.search(pattern, command, re.IGNORECASE):
82
+ logger.error(f"Highly dangerous pattern '{pattern}' detected in command")
83
+ raise CommandSecurityError("Command contains dangerous pattern and is blocked")
84
+
85
+ if not allow_shell_features:
86
+ # Check for dangerous characters (but allow some for CLI tools)
87
+ restricted_chars = [";", "&", "`", "$", "{", "}"] # More permissive for CLI
88
+ for char in restricted_chars:
89
+ if char in command:
90
+ logger.warning(f"Potentially dangerous character '{char}' detected in command")
91
+ raise CommandSecurityError(f"Potentially unsafe character '{char}' in command")
92
+
93
+ # Check for injection patterns (more selective)
94
+ strict_patterns = [
95
+ r";\s*rm\s+", # Command chaining to rm
96
+ r"&&\s*rm\s+", # Command chaining to rm
97
+ r"`[^`]*rm[^`]*`", # Command substitution with rm
98
+ r"\$\([^)]*rm[^)]*\)", # Command substitution with rm
99
+ ]
100
+
101
+ for pattern in strict_patterns:
102
+ if re.search(pattern, command):
103
+ logger.warning(f"Dangerous injection pattern '{pattern}' detected in command")
104
+ raise CommandSecurityError("Potentially unsafe pattern detected in command")
105
+
106
+
107
+ def sanitize_command_args(args: List[str]) -> List[str]:
108
+ """
109
+ Sanitize command arguments by shell-quoting them.
110
+
111
+ Args:
112
+ args: List of command arguments
113
+
114
+ Returns:
115
+ List of sanitized arguments
116
+ """
117
+ return [shlex.quote(arg) for arg in args]
118
+
119
+
120
+ def safe_subprocess_run(
121
+ command: str,
122
+ shell: bool = False,
123
+ validate: bool = True,
124
+ timeout: Optional[int] = None,
125
+ **kwargs,
126
+ ) -> subprocess.CompletedProcess:
127
+ """
128
+ Safely execute a subprocess with security validation.
129
+
130
+ Args:
131
+ command: Command to execute (string if shell=True, list if shell=False)
132
+ shell: Whether to use shell execution (discouraged)
133
+ validate: Whether to validate command safety
134
+ timeout: Timeout in seconds
135
+ **kwargs: Additional subprocess arguments
136
+
137
+ Returns:
138
+ CompletedProcess result
139
+
140
+ Raises:
141
+ CommandSecurityError: If command fails security validation
142
+ """
143
+ if validate and shell and isinstance(command, str):
144
+ validate_command_safety(command, allow_shell_features=shell)
145
+
146
+ # Log the command execution
147
+ logger.info(f"Executing command: {str(command)[:100]}...")
148
+
149
+ try:
150
+ if shell:
151
+ # When using shell=True, command should be a string
152
+ result = subprocess.run(command, shell=True, timeout=timeout, **kwargs)
153
+ else:
154
+ # When shell=False, command should be a list
155
+ if isinstance(command, str):
156
+ # Parse the string into a list
157
+ command_list = shlex.split(command)
158
+ else:
159
+ command_list = command
160
+
161
+ result = subprocess.run(command_list, shell=False, timeout=timeout, **kwargs)
162
+
163
+ logger.info(f"Command completed with return code: {result.returncode}")
164
+ return result
165
+
166
+ except subprocess.TimeoutExpired:
167
+ logger.error(f"Command timed out after {timeout} seconds")
168
+ raise
169
+ except Exception as e:
170
+ logger.error(f"Command execution failed: {str(e)}")
171
+ raise
172
+
173
+
174
+ def safe_subprocess_popen(
175
+ command: str, shell: bool = False, validate: bool = True, **kwargs
176
+ ) -> subprocess.Popen:
177
+ """
178
+ Safely create a subprocess.Popen with security validation.
179
+
180
+ Args:
181
+ command: Command to execute
182
+ shell: Whether to use shell execution (discouraged)
183
+ validate: Whether to validate command safety
184
+ **kwargs: Additional Popen arguments
185
+
186
+ Returns:
187
+ Popen process object
188
+
189
+ Raises:
190
+ CommandSecurityError: If command fails security validation
191
+ """
192
+ if validate and shell and isinstance(command, str):
193
+ validate_command_safety(command, allow_shell_features=shell)
194
+
195
+ # Log the command execution
196
+ logger.info(f"Creating Popen for command: {str(command)[:100]}...")
197
+
198
+ if shell:
199
+ # When using shell=True, command should be a string
200
+ return subprocess.Popen(command, shell=True, **kwargs)
201
+ else:
202
+ # When shell=False, command should be a list
203
+ if isinstance(command, str):
204
+ command_list = shlex.split(command)
205
+ else:
206
+ command_list = command
207
+
208
+ return subprocess.Popen(command_list, shell=False, **kwargs)
@@ -58,11 +58,23 @@ def save_config(state_manager: "StateManager") -> bool:
58
58
  """Save user config to file"""
59
59
  app_settings = ApplicationSettings()
60
60
  try:
61
+ # Ensure config directory exists
62
+ app_settings.paths.config_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
63
+
64
+ # Write config file
61
65
  with open(app_settings.paths.config_file, "w") as f:
62
66
  json.dump(state_manager.session.user_config, f, indent=4)
63
67
  return True
64
- except Exception:
65
- return False
68
+ except PermissionError as e:
69
+ raise ConfigurationError(
70
+ f"Permission denied writing to {app_settings.paths.config_file}: {e}"
71
+ )
72
+ except OSError as e:
73
+ raise ConfigurationError(
74
+ f"Failed to save configuration to {app_settings.paths.config_file}: {e}"
75
+ )
76
+ except Exception as e:
77
+ raise ConfigurationError(f"Unexpected error saving configuration: {e}")
66
78
 
67
79
 
68
80
  def get_mcp_servers(state_manager: "StateManager") -> MCPServers:
@@ -73,4 +85,9 @@ def get_mcp_servers(state_manager: "StateManager") -> MCPServers:
73
85
  def set_default_model(model_name: ModelName, state_manager: "StateManager") -> bool:
74
86
  """Set the default model in the user config and save"""
75
87
  state_manager.session.user_config["default_model"] = model_name
76
- return save_config(state_manager)
88
+ try:
89
+ save_config(state_manager)
90
+ return True
91
+ except ConfigurationError:
92
+ # Re-raise ConfigurationError to be handled by caller
93
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ Dynamic: license-file
40
40
  <div align="center">
41
41
 
42
42
  [![PyPI version](https://badge.fury.io/py/tunacode-cli.svg)](https://badge.fury.io/py/tunacode-cli)
43
+ [![Downloads](https://pepy.tech/badge/tunacode-cli)](https://pepy.tech/project/tunacode-cli)
43
44
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
44
45
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
45
46
 
@@ -78,6 +79,17 @@ tunacode --model "openrouter:openai/gpt-4o" --key "sk-or-your-openrouter-key"
78
79
 
79
80
  Your config is saved to `~/.config/tunacode.json` (edit directly with `nvim ~/.config/tunacode.json`)
80
81
 
82
+ ### Recommended Models
83
+
84
+ Based on extensive testing, these models provide the best performance:
85
+ - `google/gemini-2.5-pro` - Excellent for complex reasoning
86
+ - `openai/gpt-4.1` - Strong general-purpose model
87
+ - `deepseek/deepseek-r1-0528` - Great for code generation
88
+ - `openai/gpt-4.1-mini` - Fast and cost-effective
89
+ - `anthropic/claude-4-sonnet-20250522` - Superior context handling
90
+
91
+ *Note: Formal evaluations coming soon. Any model can work, but these have shown the best results in practice.*
92
+
81
93
  ## Start Coding
82
94
 
83
95
  ```bash
@@ -105,6 +117,13 @@ TunaCode leverages parallel execution for read-only operations, achieving **3x f
105
117
 
106
118
  Multiple file reads, directory listings, and searches execute concurrently using async I/O, making code exploration significantly faster.
107
119
 
120
+ ## Features in Development
121
+
122
+ - **Streaming UI**: Currently working on implementing streaming responses for better user experience
123
+ - **Bug Fixes**: Actively addressing issues - please report any bugs you encounter!
124
+
125
+ *Note: While the tool is fully functional, we're focusing on stability and core features before optimizing for speed.*
126
+
108
127
  ## Safety First
109
128
 
110
129
  ⚠️ **Important**: TunaCode can modify your codebase. Always:
@@ -1,14 +1,14 @@
1
1
  tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tunacode/constants.py,sha256=8uIqAbJSTPl8AM_40T6vjrqGDJ2OuxpBcqrWpZCEUeg,4074
2
+ tunacode/constants.py,sha256=OnQYL4TeNFuMCo_7x9FGWmjQCSDOB544wPPs9oOKk-8,4074
3
3
  tunacode/context.py,sha256=6sterdRvPOyG3LU0nEAXpBsEPZbO3qtPyTlJBi-_VXE,2612
4
4
  tunacode/exceptions.py,sha256=mTWXuWyr1k16CGLWN2tsthDGi7lbx1JK0ekIqogYDP8,3105
5
5
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  tunacode/setup.py,sha256=dYn0NeAxtNIDSogWEmGSyjb9wsr8AonZ8vAo5sw9NIw,1909
7
7
  tunacode/types.py,sha256=BciT-uxnQ44iC-4QiDY72OD23LOtqSyMOuK_N0ttlaA,7676
8
8
  tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
9
- tunacode/cli/commands.py,sha256=U9dP8wbBnSb-J3nKItRCd9wpQz7wD4urBOjLtCvn9Dc,29887
9
+ tunacode/cli/commands.py,sha256=r-WYp5ajzkZfFjiLXuK9pfB5ugq3HWQyhRB8Usr567k,31668
10
10
  tunacode/cli/main.py,sha256=PIcFnfmIoI_pmK2y-zB_ouJbzR5fbSI7zsKQNPB_J8o,2406
11
- tunacode/cli/repl.py,sha256=o3bn9BYQsy3TFCWJq-fzeHKLrM2KInSrMF5E5_RqSOY,13736
11
+ tunacode/cli/repl.py,sha256=ELnJBk3Vn2almXmmCIjGfgi7J5kNNVnO0o4KNYGXF9Q,14556
12
12
  tunacode/cli/textual_app.py,sha256=14-Nt0IIETmyHBrNn9uwSF3EwCcutwTp6gdoKgNm0sY,12593
13
13
  tunacode/cli/textual_bridge.py,sha256=LvqiTtF0hu3gNujzpKaW9h-m6xzEP3OH2M8KL2pCwRc,6333
14
14
  tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
@@ -20,14 +20,14 @@ tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,1
20
20
  tunacode/core/state.py,sha256=PHGCGjx_X03I5jO-T1JkREQm4cwYEXQty59JJlnk24c,1608
21
21
  tunacode/core/tool_handler.py,sha256=BPjR013OOO0cLXPdLeL2FDK0ixUwOYu59FfHdcdFhp4,2277
22
22
  tunacode/core/agents/__init__.py,sha256=UUJiPYb91arwziSpjd7vIk7XNGA_4HQbsOIbskSqevA,149
23
- tunacode/core/agents/main.py,sha256=bzex6MTucOf95je1XHICSsMRDKew4TUzg9RApceebj4,38203
23
+ tunacode/core/agents/main.py,sha256=-QwKSKoPLdD-JlKPjwMUSxNu_TSrj-pdUleWR2FN-A0,39441
24
24
  tunacode/core/background/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  tunacode/core/background/manager.py,sha256=rJdl3eDLTQwjbT7VhxXcJbZopCNR3M8ZGMbmeVnwwMc,1126
26
26
  tunacode/core/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
28
28
  tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
29
29
  tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
30
- tunacode/core/setup/config_setup.py,sha256=A1MEbkZq2FvVg9FNlLD9_JO_vlr0ixP-6Ay0-2W7VvQ,14330
30
+ tunacode/core/setup/config_setup.py,sha256=LnIGZrqDCWUhiMhJMnKX1gyaZhy6pKM8xf_fy_t9B3U,14516
31
31
  tunacode/core/setup/coordinator.py,sha256=oVTN2xIeJERXitVJpkIk9tDGLs1D1bxIRmaogJwZJFI,2049
32
32
  tunacode/core/setup/environment_setup.py,sha256=n3IrObKEynHZSwtUJ1FddMg2C4sHz7ca42awemImV8s,2225
33
33
  tunacode/core/setup/git_safety_setup.py,sha256=CRIqrQt0QUJQRS344njty_iCqTorrDhHlXRuET7w0Tk,6714
@@ -42,7 +42,7 @@ tunacode/tools/grep.py,sha256=jboEVA2ATv0YI8zg9dF89emZ_HWy2vVtsQ_-hDhlr7g,26337
42
42
  tunacode/tools/list_dir.py,sha256=1kNqzYCNlcA5rqXIEVqcjQy6QxlLZLj5AG6YIECfwio,7217
43
43
  tunacode/tools/read_file.py,sha256=BqHxPspZBYotz5wtjuP-zve61obsx98z5TU-aw5BJHg,3273
44
44
  tunacode/tools/read_file_async_poc.py,sha256=0rSfYCmoNcvWk8hB1z86l32-tomSc9yOM4tR4nrty_o,6267
45
- tunacode/tools/run_command.py,sha256=kYg_Re397OmZdKtUSjpNfYYNDBjd0vsS1xMK0yP181I,3776
45
+ tunacode/tools/run_command.py,sha256=2TtndMIeOWHQRC2XwkxUDVb06Ob-RHKSheBdluH3QgQ,4433
46
46
  tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
47
47
  tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
48
48
  tunacode/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
@@ -64,13 +64,14 @@ tunacode/utils/diff_utils.py,sha256=V9QqQ0q4MfabVTnWptF3IXDp3estnfOKcJtDe_Sj14I,
64
64
  tunacode/utils/file_utils.py,sha256=AXiAJ_idtlmXEi9pMvwtfPy9Ys3yK-F4K7qb_NpwonU,923
65
65
  tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDgs,295
66
66
  tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
67
+ tunacode/utils/security.py,sha256=e_zo9VmcOKFjgFMr9GOBIFhAmND4PBlJZgY7zqnsGjI,6548
67
68
  tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
68
69
  tunacode/utils/text_utils.py,sha256=zRBaorvtyd7HBEWtIfCH1Wce1L6rhsQwpORUEGBFMjA,2981
69
70
  tunacode/utils/token_counter.py,sha256=nGCWwrHHFbKywqeDCEuJnADCkfJuzysWiB6cCltJOKI,648
70
- tunacode/utils/user_configuration.py,sha256=IGvUH37wWtZ4M5xpukZEWYhtuKKyKcl6DaeObGXdleU,2610
71
- tunacode_cli-0.0.33.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
72
- tunacode_cli-0.0.33.dist-info/METADATA,sha256=vf_6TFB9fo7MDRHtXDvLKPys_W4E6t_vzNp-cPpRocQ,4023
73
- tunacode_cli-0.0.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
- tunacode_cli-0.0.33.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
75
- tunacode_cli-0.0.33.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
76
- tunacode_cli-0.0.33.dist-info/RECORD,,
71
+ tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
72
+ tunacode_cli-0.0.35.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
73
+ tunacode_cli-0.0.35.dist-info/METADATA,sha256=4ck_-g8eF10l6md95_EaEK1Sd4UmqmRAUEJacINtDA8,4943
74
+ tunacode_cli-0.0.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ tunacode_cli-0.0.35.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
76
+ tunacode_cli-0.0.35.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
77
+ tunacode_cli-0.0.35.dist-info/RECORD,,