tunacode-cli 0.0.55__py3-none-any.whl → 0.0.78.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (114) hide show
  1. tunacode/cli/commands/__init__.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +2 -3
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/debug.py +2 -2
  5. tunacode/cli/commands/implementations/development.py +10 -8
  6. tunacode/cli/commands/implementations/model.py +357 -29
  7. tunacode/cli/commands/implementations/quickstart.py +43 -0
  8. tunacode/cli/commands/implementations/system.py +96 -3
  9. tunacode/cli/commands/implementations/template.py +0 -2
  10. tunacode/cli/commands/registry.py +139 -5
  11. tunacode/cli/commands/slash/__init__.py +32 -0
  12. tunacode/cli/commands/slash/command.py +157 -0
  13. tunacode/cli/commands/slash/loader.py +135 -0
  14. tunacode/cli/commands/slash/processor.py +294 -0
  15. tunacode/cli/commands/slash/types.py +93 -0
  16. tunacode/cli/commands/slash/validator.py +400 -0
  17. tunacode/cli/main.py +23 -2
  18. tunacode/cli/repl.py +217 -190
  19. tunacode/cli/repl_components/command_parser.py +38 -4
  20. tunacode/cli/repl_components/error_recovery.py +85 -4
  21. tunacode/cli/repl_components/output_display.py +12 -1
  22. tunacode/cli/repl_components/tool_executor.py +1 -1
  23. tunacode/configuration/defaults.py +12 -3
  24. tunacode/configuration/key_descriptions.py +284 -0
  25. tunacode/configuration/settings.py +0 -1
  26. tunacode/constants.py +12 -40
  27. tunacode/core/agents/__init__.py +43 -2
  28. tunacode/core/agents/agent_components/__init__.py +7 -0
  29. tunacode/core/agents/agent_components/agent_config.py +249 -55
  30. tunacode/core/agents/agent_components/agent_helpers.py +43 -13
  31. tunacode/core/agents/agent_components/node_processor.py +179 -139
  32. tunacode/core/agents/agent_components/response_state.py +123 -6
  33. tunacode/core/agents/agent_components/state_transition.py +116 -0
  34. tunacode/core/agents/agent_components/streaming.py +296 -0
  35. tunacode/core/agents/agent_components/task_completion.py +19 -6
  36. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  37. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  38. tunacode/core/agents/main.py +522 -370
  39. tunacode/core/agents/main_legact.py +538 -0
  40. tunacode/core/agents/prompts.py +66 -0
  41. tunacode/core/agents/utils.py +29 -121
  42. tunacode/core/code_index.py +83 -29
  43. tunacode/core/setup/__init__.py +0 -2
  44. tunacode/core/setup/config_setup.py +110 -20
  45. tunacode/core/setup/config_wizard.py +230 -0
  46. tunacode/core/setup/coordinator.py +14 -5
  47. tunacode/core/state.py +16 -20
  48. tunacode/core/token_usage/usage_tracker.py +5 -3
  49. tunacode/core/tool_authorization.py +352 -0
  50. tunacode/core/tool_handler.py +67 -40
  51. tunacode/exceptions.py +119 -5
  52. tunacode/prompts/system.xml +751 -0
  53. tunacode/services/mcp.py +125 -7
  54. tunacode/setup.py +5 -25
  55. tunacode/tools/base.py +163 -0
  56. tunacode/tools/bash.py +110 -1
  57. tunacode/tools/glob.py +332 -34
  58. tunacode/tools/grep.py +179 -82
  59. tunacode/tools/grep_components/result_formatter.py +98 -4
  60. tunacode/tools/list_dir.py +132 -2
  61. tunacode/tools/prompts/bash_prompt.xml +72 -0
  62. tunacode/tools/prompts/glob_prompt.xml +45 -0
  63. tunacode/tools/prompts/grep_prompt.xml +98 -0
  64. tunacode/tools/prompts/list_dir_prompt.xml +31 -0
  65. tunacode/tools/prompts/react_prompt.xml +23 -0
  66. tunacode/tools/prompts/read_file_prompt.xml +54 -0
  67. tunacode/tools/prompts/run_command_prompt.xml +64 -0
  68. tunacode/tools/prompts/update_file_prompt.xml +53 -0
  69. tunacode/tools/prompts/write_file_prompt.xml +37 -0
  70. tunacode/tools/react.py +153 -0
  71. tunacode/tools/read_file.py +91 -0
  72. tunacode/tools/run_command.py +114 -0
  73. tunacode/tools/schema_assembler.py +167 -0
  74. tunacode/tools/update_file.py +94 -0
  75. tunacode/tools/write_file.py +86 -0
  76. tunacode/tools/xml_helper.py +83 -0
  77. tunacode/tutorial/__init__.py +9 -0
  78. tunacode/tutorial/content.py +98 -0
  79. tunacode/tutorial/manager.py +182 -0
  80. tunacode/tutorial/steps.py +124 -0
  81. tunacode/types.py +20 -27
  82. tunacode/ui/completers.py +434 -50
  83. tunacode/ui/config_dashboard.py +585 -0
  84. tunacode/ui/console.py +63 -11
  85. tunacode/ui/input.py +20 -3
  86. tunacode/ui/keybindings.py +7 -4
  87. tunacode/ui/model_selector.py +395 -0
  88. tunacode/ui/output.py +40 -19
  89. tunacode/ui/panels.py +212 -43
  90. tunacode/ui/path_heuristics.py +91 -0
  91. tunacode/ui/prompt_manager.py +5 -1
  92. tunacode/ui/tool_ui.py +33 -10
  93. tunacode/utils/api_key_validation.py +93 -0
  94. tunacode/utils/config_comparator.py +340 -0
  95. tunacode/utils/json_utils.py +206 -0
  96. tunacode/utils/message_utils.py +14 -4
  97. tunacode/utils/models_registry.py +593 -0
  98. tunacode/utils/ripgrep.py +332 -9
  99. tunacode/utils/text_utils.py +18 -1
  100. tunacode/utils/user_configuration.py +45 -0
  101. tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
  102. tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
  103. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
  104. tunacode/cli/commands/implementations/todo.py +0 -217
  105. tunacode/context.py +0 -71
  106. tunacode/core/setup/git_safety_setup.py +0 -182
  107. tunacode/prompts/system.md +0 -731
  108. tunacode/tools/read_file_async_poc.py +0 -196
  109. tunacode/tools/todo.py +0 -349
  110. tunacode_cli-0.0.55.dist-info/METADATA +0 -322
  111. tunacode_cli-0.0.55.dist-info/RECORD +0 -126
  112. tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
  113. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  114. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,400 @@
1
+ """Security validation for slash command execution."""
2
+
3
+ import logging
4
+ import re
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ from .types import SecurityLevel, SecurityViolation, ValidationResult
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CommandValidator:
14
+ """Comprehensive security validation system for slash commands."""
15
+
16
+ def __init__(self, security_level: SecurityLevel = SecurityLevel.MODERATE):
17
+ self.security_level = security_level
18
+ self._init_security_rules()
19
+
20
+ def _init_security_rules(self) -> None:
21
+ """Initialize security rules based on security level."""
22
+
23
+ # Always blocked commands (all security levels)
24
+ self.ALWAYS_BLOCKED = {
25
+ "rm",
26
+ "rmdir",
27
+ "del",
28
+ "format",
29
+ "fdisk",
30
+ "mkfs",
31
+ "sudo",
32
+ "su",
33
+ "passwd",
34
+ "useradd",
35
+ "userdel",
36
+ "chmod",
37
+ "chown",
38
+ "chgrp",
39
+ "setfacl",
40
+ "iptables",
41
+ "firewall-cmd",
42
+ "ufw",
43
+ "systemctl",
44
+ "service",
45
+ "initctl",
46
+ "wget",
47
+ "curl",
48
+ "nc",
49
+ "netcat",
50
+ "telnet",
51
+ "dd",
52
+ "mount",
53
+ "umount",
54
+ "fsck",
55
+ }
56
+
57
+ # Dangerous patterns (all security levels)
58
+ self.DANGEROUS_PATTERNS = [
59
+ r"rm\s+.*-[rf]", # Recursive/force delete
60
+ r">\s*/dev/", # Device access
61
+ r"\|\s*sh\b", # Pipe to shell
62
+ r"\|\s*bash\b", # Pipe to bash
63
+ r"`[^`]*`", # Command substitution
64
+ r"\$\([^)]*\)", # Command substitution
65
+ r"(?:;|\|\||&&)", # Command chaining (;, ||, &&)
66
+ r"[|&]+\s*$", # Trailing pipes
67
+ r"eval\s+", # Dynamic evaluation
68
+ r"exec\s+", # Process replacement
69
+ r"source\s+", # Script sourcing
70
+ r"\.\s+/", # Script sourcing (dot)
71
+ ]
72
+
73
+ # Security level specific rules
74
+ if self.security_level == SecurityLevel.STRICT:
75
+ self.ALLOWED_COMMANDS = {
76
+ "git": ["status", "branch", "log", "show"],
77
+ "ls": ["-la", "-l", "-a"],
78
+ "cat": [],
79
+ "echo": [],
80
+ "date": [],
81
+ "pwd": [],
82
+ }
83
+ elif self.security_level == SecurityLevel.MODERATE:
84
+ self.ALLOWED_COMMANDS = {
85
+ "git": ["status", "branch", "log", "diff", "show", "remote", "config"],
86
+ "npm": ["list", "info", "view", "outdated", "audit", "install"],
87
+ "python": ["-c", "-m", "--version", "-V"],
88
+ "node": ["--version", "-v", "-e"],
89
+ "ls": ["-la", "-l", "-a", "-h", "-R"],
90
+ "cat": [],
91
+ "head": [],
92
+ "tail": [],
93
+ "echo": [],
94
+ "date": [],
95
+ "pwd": [],
96
+ "whoami": [],
97
+ "grep": ["-r", "-n", "-i", "-v"],
98
+ "find": ["-name", "-type", "-size"],
99
+ "wc": ["-l", "-w", "-c"],
100
+ "sort": [],
101
+ "uniq": [],
102
+ "cut": [],
103
+ }
104
+ else: # PERMISSIVE
105
+ self.ALLOWED_COMMANDS = {
106
+ # All moderate commands plus more
107
+ "git": ["status", "branch", "log", "diff", "show", "remote", "config"],
108
+ "npm": ["list", "info", "view", "outdated", "audit"],
109
+ "python": ["-c", "-m", "--version", "-V"],
110
+ "node": ["--version", "-v", "-e"],
111
+ "ls": ["-la", "-l", "-a", "-h", "-R"],
112
+ "cat": [],
113
+ "head": [],
114
+ "tail": [],
115
+ "echo": [],
116
+ "date": [],
117
+ "pwd": [],
118
+ "whoami": [],
119
+ "grep": ["-r", "-n", "-i", "-v"],
120
+ "find": ["-name", "-type", "-size"],
121
+ "wc": ["-l", "-w", "-c"],
122
+ "sort": [],
123
+ "uniq": [],
124
+ "cut": [],
125
+ "make": ["--version", "-n"], # Dry run only
126
+ "docker": ["ps", "images", "info", "version"],
127
+ "kubectl": ["get", "describe", "logs", "version"],
128
+ "terraform": ["version", "validate", "plan"],
129
+ }
130
+
131
+ def validate_shell_command(self, command: str) -> ValidationResult:
132
+ """Comprehensive command validation with detailed results."""
133
+ command = command.strip()
134
+ violations = []
135
+
136
+ if not command:
137
+ violations.append(
138
+ SecurityViolation(
139
+ type="empty_command",
140
+ message="Empty command not allowed",
141
+ command=command,
142
+ severity="error",
143
+ )
144
+ )
145
+ return ValidationResult(False, violations)
146
+
147
+ # Parse command
148
+ parts = command.split()
149
+ base_command = parts[0].lower()
150
+
151
+ # Check for always blocked commands
152
+ if base_command in self.ALWAYS_BLOCKED:
153
+ violations.append(
154
+ SecurityViolation(
155
+ type="blocked_command",
156
+ message=f"Command '{base_command}' is always blocked",
157
+ command=command,
158
+ severity="error",
159
+ )
160
+ )
161
+ return ValidationResult(False, violations)
162
+
163
+ # Check dangerous patterns
164
+ for pattern in self.DANGEROUS_PATTERNS:
165
+ if re.search(pattern, command, re.IGNORECASE):
166
+ violations.append(
167
+ SecurityViolation(
168
+ type="dangerous_pattern",
169
+ message=f"Command matches dangerous pattern: {pattern}",
170
+ command=command,
171
+ severity="error",
172
+ )
173
+ )
174
+ return ValidationResult(False, violations)
175
+
176
+ # Check against whitelist
177
+ if base_command not in self.ALLOWED_COMMANDS:
178
+ violations.append(
179
+ SecurityViolation(
180
+ type="unknown_command",
181
+ message=f"Command '{base_command}' not in whitelist",
182
+ command=command,
183
+ severity="warning"
184
+ if self.security_level == SecurityLevel.PERMISSIVE
185
+ else "error",
186
+ )
187
+ )
188
+
189
+ if self.security_level != SecurityLevel.PERMISSIVE:
190
+ return ValidationResult(False, violations)
191
+ else:
192
+ # Validate subcommands if specified
193
+ allowed_subcommands = self.ALLOWED_COMMANDS[base_command]
194
+ if allowed_subcommands and len(parts) > 1:
195
+ subcommand = parts[1]
196
+ if subcommand not in allowed_subcommands:
197
+ # Allow python scripts
198
+ if base_command == "python" and subcommand.endswith(".py"):
199
+ pass
200
+ elif base_command == "grep":
201
+ pass
202
+ else:
203
+ violations.append(
204
+ SecurityViolation(
205
+ type="invalid_subcommand",
206
+ message=f"Subcommand '{subcommand}' not allowed "
207
+ f"for '{base_command}'",
208
+ command=command,
209
+ severity="error",
210
+ )
211
+ )
212
+ return ValidationResult(False, violations)
213
+
214
+ # Additional validation checks
215
+ violations.extend(self._check_file_access_patterns(command))
216
+ violations.extend(self._check_network_patterns(command))
217
+ violations.extend(self._check_privilege_escalation(command))
218
+
219
+ # Determine if command is allowed
220
+ error_violations = [v for v in violations if v.severity == "error"]
221
+ allowed = len(error_violations) == 0
222
+
223
+ return ValidationResult(allowed, violations)
224
+
225
+ def _check_file_access_patterns(self, command: str) -> List[SecurityViolation]:
226
+ """Check for suspicious file access patterns."""
227
+ violations = []
228
+
229
+ # Sensitive file patterns
230
+ sensitive_patterns = [
231
+ r"/etc/passwd",
232
+ r"/etc/shadow",
233
+ r"/etc/hosts",
234
+ r"/home/[^/]+/\.ssh",
235
+ r"~/.ssh",
236
+ r"/var/log/",
237
+ r"/proc/",
238
+ r"/sys/",
239
+ r"\.env",
240
+ r"\.secret",
241
+ r"\.key",
242
+ ]
243
+
244
+ for pattern in sensitive_patterns:
245
+ if re.search(pattern, command, re.IGNORECASE):
246
+ violations.append(
247
+ SecurityViolation(
248
+ type="sensitive_file_access",
249
+ message=f"Potential access to sensitive file: {pattern}",
250
+ command=command,
251
+ severity="warning",
252
+ )
253
+ )
254
+
255
+ return violations
256
+
257
+ def _check_network_patterns(self, command: str) -> List[SecurityViolation]:
258
+ """Check for network access patterns."""
259
+ violations = []
260
+
261
+ network_patterns = [
262
+ r"curl\s+",
263
+ r"wget\s+",
264
+ r"nc\s+",
265
+ r"netcat\s+",
266
+ r"ssh\s+",
267
+ r"scp\s+",
268
+ r"rsync\s+.*:",
269
+ r"ftp\s+",
270
+ r"telnet\s+",
271
+ ]
272
+
273
+ for pattern in network_patterns:
274
+ if re.search(pattern, command, re.IGNORECASE):
275
+ violations.append(
276
+ SecurityViolation(
277
+ type="network_access",
278
+ message=f"Network access detected: {pattern}",
279
+ command=command,
280
+ severity="error",
281
+ )
282
+ )
283
+
284
+ return violations
285
+
286
+ def _check_privilege_escalation(self, command: str) -> List[SecurityViolation]:
287
+ """Check for privilege escalation attempts."""
288
+ violations = []
289
+
290
+ privilege_patterns = [
291
+ r"sudo\s+",
292
+ r"su\s+",
293
+ r"doas\s+",
294
+ r"pkexec\s+",
295
+ r"runuser\s+",
296
+ ]
297
+
298
+ for pattern in privilege_patterns:
299
+ if re.search(pattern, command, re.IGNORECASE):
300
+ violations.append(
301
+ SecurityViolation(
302
+ type="privilege_escalation",
303
+ message=f"Privilege escalation attempt: {pattern}",
304
+ command=command,
305
+ severity="error",
306
+ )
307
+ )
308
+
309
+ return violations
310
+
311
+ def validate_file_path(self, file_path: str, base_path: Path) -> ValidationResult:
312
+ """Enhanced file path validation."""
313
+ violations = []
314
+
315
+ try:
316
+ # Basic path traversal check
317
+ resolved = (base_path / file_path).resolve()
318
+ base_resolved = base_path.resolve()
319
+
320
+ if not str(resolved).startswith(str(base_resolved)):
321
+ violations.append(
322
+ SecurityViolation(
323
+ type="path_traversal",
324
+ message=f"Path traversal detected: {file_path}",
325
+ command=file_path,
326
+ severity="error",
327
+ )
328
+ )
329
+ return ValidationResult(False, violations)
330
+
331
+ # Check for sensitive file access
332
+ sensitive_dirs = [".ssh", ".git", ".env", "node_modules"]
333
+ path_parts = Path(file_path).parts
334
+
335
+ for sensitive in sensitive_dirs:
336
+ if sensitive in path_parts:
337
+ violations.append(
338
+ SecurityViolation(
339
+ type="sensitive_directory",
340
+ message=f"Access to sensitive directory: {sensitive}",
341
+ command=file_path,
342
+ severity="warning",
343
+ )
344
+ )
345
+
346
+ return ValidationResult(True, violations)
347
+
348
+ except (OSError, ValueError) as e:
349
+ violations.append(
350
+ SecurityViolation(
351
+ type="invalid_path",
352
+ message=f"Invalid file path: {str(e)}",
353
+ command=file_path,
354
+ severity="error",
355
+ )
356
+ )
357
+ return ValidationResult(False, violations)
358
+
359
+ def validate_glob_pattern(self, pattern: str) -> ValidationResult:
360
+ """Enhanced glob pattern validation."""
361
+ violations = []
362
+
363
+ # Dangerous glob patterns
364
+ dangerous_globs = [
365
+ r"\.\./.*", # Parent directory traversal
366
+ r"/etc/.*",
367
+ r"/home/.*",
368
+ r"/root/.*", # System directories
369
+ r"/var/.*",
370
+ r"/usr/bin/.*",
371
+ r"/bin/.*",
372
+ r".*\.key$",
373
+ r".*\.secret$",
374
+ r".*\.pem$", # Sensitive files
375
+ ]
376
+
377
+ for dangerous in dangerous_globs:
378
+ if re.match(dangerous, pattern, re.IGNORECASE):
379
+ violations.append(
380
+ SecurityViolation(
381
+ type="dangerous_glob",
382
+ message=f"Dangerous glob pattern: {dangerous}",
383
+ command=pattern,
384
+ severity="error",
385
+ )
386
+ )
387
+ return ValidationResult(False, violations)
388
+
389
+ # Check for overly broad patterns
390
+ if pattern in ["**/*", "*", "**"]:
391
+ violations.append(
392
+ SecurityViolation(
393
+ type="broad_glob",
394
+ message="Overly broad glob pattern",
395
+ command=pattern,
396
+ severity="warning",
397
+ )
398
+ )
399
+
400
+ return ValidationResult(True, violations)
tunacode/cli/main.py CHANGED
@@ -16,6 +16,7 @@ from tunacode.core.tool_handler import ToolHandler
16
16
  from tunacode.exceptions import UserAbortError
17
17
  from tunacode.setup import setup
18
18
  from tunacode.ui import console as ui
19
+ from tunacode.ui.config_dashboard import show_config_dashboard
19
20
  from tunacode.utils.system import check_for_updates
20
21
 
21
22
  app_settings = ApplicationSettings()
@@ -27,6 +28,12 @@ state_manager = StateManager()
27
28
  def main(
28
29
  version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
29
30
  run_setup: bool = typer.Option(False, "--setup", help="Run setup process."),
31
+ wizard: bool = typer.Option(
32
+ False, "--wizard", help="Run interactive setup wizard for guided configuration."
33
+ ),
34
+ show_config: bool = typer.Option(
35
+ False, "--show-config", help="Show configuration dashboard and exit."
36
+ ),
30
37
  baseurl: str = typer.Option(
31
38
  None, "--baseurl", help="API base URL (e.g., https://openrouter.ai/api/v1)"
32
39
  ),
@@ -46,6 +53,11 @@ def main(
46
53
  await ui.version()
47
54
  return
48
55
 
56
+ if show_config:
57
+ await ui.banner()
58
+ show_config_dashboard()
59
+ return
60
+
49
61
  await ui.banner()
50
62
 
51
63
  # Start update check in background
@@ -60,7 +72,7 @@ def main(
60
72
  cli_config = {k: v for k, v in cli_config.items() if v is not None}
61
73
 
62
74
  try:
63
- await setup(run_setup, state_manager, cli_config)
75
+ await setup(run_setup or wizard, state_manager, cli_config, wizard_mode=wizard)
64
76
 
65
77
  # Initialize ToolHandler after setup
66
78
  tool_handler = ToolHandler(state_manager)
@@ -74,7 +86,8 @@ def main(
74
86
  from tunacode.exceptions import ConfigurationError
75
87
 
76
88
  if isinstance(e, ConfigurationError):
77
- # ConfigurationError already printed helpful message, just exit cleanly
89
+ # Display the configuration error message
90
+ await ui.error(str(e))
78
91
  update_task.cancel() # Cancel the update check
79
92
  return
80
93
  import traceback
@@ -84,6 +97,14 @@ def main(
84
97
  has_update, latest_version = await update_task
85
98
  if has_update:
86
99
  await ui.update_available(latest_version)
100
+ else:
101
+ # Normal exit - cleanup MCP servers
102
+ try:
103
+ from tunacode.core.agents import cleanup_mcp_servers
104
+
105
+ await cleanup_mcp_servers()
106
+ except Exception:
107
+ pass # Best effort cleanup
87
108
 
88
109
  asyncio.run(async_main())
89
110