tunacode-cli 0.0.66__py3-none-any.whl → 0.0.68__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 (39) hide show
  1. tunacode/cli/commands/__init__.py +2 -0
  2. tunacode/cli/commands/implementations/__init__.py +2 -0
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/quickstart.py +43 -0
  5. tunacode/cli/commands/implementations/system.py +27 -3
  6. tunacode/cli/commands/registry.py +131 -1
  7. tunacode/cli/commands/slash/__init__.py +32 -0
  8. tunacode/cli/commands/slash/command.py +157 -0
  9. tunacode/cli/commands/slash/loader.py +134 -0
  10. tunacode/cli/commands/slash/processor.py +294 -0
  11. tunacode/cli/commands/slash/types.py +93 -0
  12. tunacode/cli/commands/slash/validator.py +399 -0
  13. tunacode/cli/main.py +4 -1
  14. tunacode/cli/repl.py +25 -0
  15. tunacode/configuration/defaults.py +1 -0
  16. tunacode/constants.py +1 -1
  17. tunacode/core/agents/agent_components/agent_helpers.py +14 -13
  18. tunacode/core/agents/main.py +1 -1
  19. tunacode/core/agents/utils.py +4 -3
  20. tunacode/core/setup/config_setup.py +231 -6
  21. tunacode/core/setup/coordinator.py +13 -5
  22. tunacode/core/setup/git_safety_setup.py +5 -1
  23. tunacode/exceptions.py +119 -5
  24. tunacode/setup.py +5 -2
  25. tunacode/tools/glob.py +9 -46
  26. tunacode/tools/grep.py +9 -51
  27. tunacode/tools/xml_helper.py +83 -0
  28. tunacode/tutorial/__init__.py +9 -0
  29. tunacode/tutorial/content.py +98 -0
  30. tunacode/tutorial/manager.py +182 -0
  31. tunacode/tutorial/steps.py +124 -0
  32. tunacode/ui/output.py +1 -1
  33. tunacode/utils/user_configuration.py +45 -0
  34. tunacode_cli-0.0.68.dist-info/METADATA +192 -0
  35. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/RECORD +38 -25
  36. tunacode_cli-0.0.66.dist-info/METADATA +0 -327
  37. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/WHEEL +0 -0
  38. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/entry_points.txt +0 -0
  39. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/licenses/LICENSE +0 -0
@@ -18,6 +18,7 @@ from .base import Command, CommandCategory, CommandSpec, SimpleCommand
18
18
  from .implementations import (
19
19
  BranchCommand,
20
20
  ClearCommand,
21
+ CommandReloadCommand,
21
22
  CompactCommand,
22
23
  DumpCommand,
23
24
  FixCommand,
@@ -63,4 +64,5 @@ __all__ = [
63
64
  "ModelCommand",
64
65
  "InitCommand",
65
66
  "TodoCommand",
67
+ "CommandReloadCommand",
66
68
  ]
@@ -1,6 +1,7 @@
1
1
  """Command implementations for TunaCode CLI."""
2
2
 
3
3
  # Import all command classes for easy access
4
+ from .command_reload import CommandReloadCommand
4
5
  from .conversation import CompactCommand
5
6
  from .debug import (
6
7
  DumpCommand,
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "RefreshConfigCommand",
29
30
  "StreamingCommand",
30
31
  "UpdateCommand",
32
+ "CommandReloadCommand",
31
33
  # Debug commands
32
34
  "YoloCommand",
33
35
  "DumpCommand",
@@ -0,0 +1,48 @@
1
+ """Command reload implementation."""
2
+
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from ....types import CommandContext
7
+ from ....ui import console as ui
8
+ from ..base import CommandCategory, CommandSpec, SimpleCommand
9
+
10
+
11
+ class CommandReloadCommand(SimpleCommand):
12
+ """Reload slash commands to discover new commands."""
13
+
14
+ spec = CommandSpec(
15
+ name="command-reload",
16
+ aliases=["/command-reload"],
17
+ description="Reload slash commands to discover newly added commands",
18
+ category=CommandCategory.DEVELOPMENT,
19
+ )
20
+
21
+ def __init__(self, command_registry=None):
22
+ self._command_registry = command_registry
23
+
24
+ async def execute(self, args: List[str], context: CommandContext) -> None:
25
+ # Check if any command directories exist
26
+ command_dirs = [
27
+ Path(".tunacode/commands"),
28
+ Path(".claude/commands"),
29
+ Path.home() / ".tunacode/commands",
30
+ Path.home() / ".claude/commands",
31
+ ]
32
+
33
+ dirs_exist = any(cmd_dir.exists() for cmd_dir in command_dirs)
34
+
35
+ if not dirs_exist:
36
+ await ui.info("No commands directory found")
37
+ return
38
+
39
+ # Reload commands using registry
40
+ if self._command_registry:
41
+ try:
42
+ self._command_registry.reload_slash_commands()
43
+ await ui.success("Commands reloaded")
44
+ return
45
+ except Exception as e:
46
+ await ui.error(f"Reload failed: {e}")
47
+ else:
48
+ await ui.error("Command registry not available")
@@ -0,0 +1,43 @@
1
+ """
2
+ Module: tunacode.cli.commands.implementations.quickstart
3
+
4
+ QuickStart command implementation for interactive tutorial system.
5
+ Provides guided introduction to TunaCode for new users.
6
+ """
7
+
8
+ import logging
9
+
10
+ from tunacode.types import CommandContext, CommandResult
11
+
12
+ from ..base import CommandCategory, CommandSpec, SimpleCommand
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class QuickStartCommand(SimpleCommand):
18
+ """Interactive quickstart tutorial command."""
19
+
20
+ spec = CommandSpec(
21
+ name="quickstart",
22
+ aliases=["/quickstart", "/qs"],
23
+ description="Interactive tutorial for getting started with TunaCode",
24
+ category=CommandCategory.SYSTEM,
25
+ )
26
+
27
+ async def execute(self, _args: list[str], context: CommandContext) -> CommandResult:
28
+ """Execute the quickstart tutorial."""
29
+ try:
30
+ from ....tutorial import TutorialManager
31
+ except ImportError:
32
+ await context.ui.error("Tutorial system is not available")
33
+ return
34
+
35
+ tutorial_manager = TutorialManager(context.state_manager)
36
+
37
+ # Always run tutorial when explicitly requested
38
+ success = await tutorial_manager.run_tutorial()
39
+
40
+ if success:
41
+ await context.ui.success("✅ Tutorial completed successfully!")
42
+ else:
43
+ await context.ui.info("Tutorial was cancelled or interrupted")
@@ -143,6 +143,21 @@ class UpdateCommand(SimpleCommand):
143
143
  except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
144
144
  pass
145
145
 
146
+ # Check if installed via uv tool
147
+ if not installation_method:
148
+ if shutil.which("uv"):
149
+ try:
150
+ result = subprocess.run(
151
+ ["uv", "tool", "list"],
152
+ capture_output=True,
153
+ text=True,
154
+ timeout=10,
155
+ )
156
+ if result.returncode == 0 and "tunacode-cli" in result.stdout.lower():
157
+ installation_method = "uv_tool"
158
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
159
+ pass
160
+
146
161
  # Check if installed via pip
147
162
  if not installation_method:
148
163
  try:
@@ -160,10 +175,11 @@ class UpdateCommand(SimpleCommand):
160
175
  if not installation_method:
161
176
  await ui.error("Could not detect TunaCode installation method")
162
177
  await ui.muted("Manual update options:")
163
- await ui.muted(" pipx: pipx upgrade tunacode")
164
- await ui.muted(" pip: pip install --upgrade tunacode-cli")
178
+ await ui.muted(" pipx: pipx upgrade tunacode")
179
+ await ui.muted(" pip: pip install --upgrade tunacode-cli")
180
+ await ui.muted(" uv tool: uv tool upgrade tunacode-cli")
165
181
  await ui.muted(
166
- " venv: uv pip install --python ~/.tunacode-venv/bin/python --upgrade tunacode-cli"
182
+ " venv: uv pip install --python ~/.tunacode-venv/bin/python --upgrade tunacode-cli"
167
183
  )
168
184
  return
169
185
 
@@ -206,6 +222,14 @@ class UpdateCommand(SimpleCommand):
206
222
  text=True,
207
223
  timeout=60,
208
224
  )
225
+ elif installation_method == "uv_tool":
226
+ await ui.info("Updating via UV tool...")
227
+ result = subprocess.run(
228
+ ["uv", "tool", "upgrade", "tunacode-cli"],
229
+ capture_output=True,
230
+ text=True,
231
+ timeout=60,
232
+ )
209
233
  else: # pip
210
234
  await ui.info("Updating via pip...")
211
235
  result = subprocess.run(
@@ -3,7 +3,9 @@
3
3
  CLAUDE_ANCHOR[command-registry]: Central command registration and execution
4
4
  """
5
5
 
6
+ import logging
6
7
  from dataclasses import dataclass
8
+ from pathlib import Path
7
9
  from typing import Any, Dict, List, Optional, Type
8
10
 
9
11
  from ...exceptions import ValidationError
@@ -12,6 +14,7 @@ from ...types import CommandArgs, CommandContext, ProcessRequestCallback
12
14
  from .base import Command, CommandCategory
13
15
 
14
16
  # Import all command implementations
17
+ from .implementations.command_reload import CommandReloadCommand
15
18
  from .implementations.conversation import CompactCommand
16
19
  from .implementations.debug import (
17
20
  DumpCommand,
@@ -24,6 +27,7 @@ from .implementations.debug import (
24
27
  from .implementations.development import BranchCommand, InitCommand
25
28
  from .implementations.model import ModelCommand
26
29
  from .implementations.plan import ExitPlanCommand, PlanCommand
30
+ from .implementations.quickstart import QuickStartCommand
27
31
  from .implementations.system import (
28
32
  ClearCommand,
29
33
  HelpCommand,
@@ -35,6 +39,8 @@ from .implementations.template import TemplateCommand
35
39
  from .implementations.todo import TodoCommand
36
40
  from .template_shortcut import TemplateShortcutCommand
37
41
 
42
+ logger = logging.getLogger(__name__)
43
+
38
44
 
39
45
  @dataclass
40
46
  class CommandDependencies:
@@ -57,6 +63,8 @@ class CommandFactory:
57
63
  return CompactCommand(self.dependencies.process_request_callback)
58
64
  elif command_class == HelpCommand:
59
65
  return HelpCommand(self.dependencies.command_registry)
66
+ elif command_class == CommandReloadCommand:
67
+ return CommandReloadCommand(self.dependencies.command_registry)
60
68
 
61
69
  # Default creation for commands without dependencies
62
70
  return command_class()
@@ -80,6 +88,11 @@ class CommandRegistry:
80
88
  self._discovered = False
81
89
  self._shortcuts_loaded = False
82
90
 
91
+ # Slash command support
92
+ self._slash_loader: Optional[Any] = None # SlashCommandLoader
93
+ self._slash_discovery_result: Optional[Any] = None # CommandDiscoveryResult
94
+ self._slash_enabled: bool = True # Feature flag
95
+
83
96
  # Set registry reference in factory dependencies
84
97
  self._factory.update_dependencies(command_registry=self)
85
98
 
@@ -111,6 +124,17 @@ class CommandRegistry:
111
124
  if self._discovered:
112
125
  return
113
126
 
127
+ # Step 1: Discover built-in commands
128
+ self._discover_builtin_commands()
129
+
130
+ # Step 2: Discover slash commands (if enabled)
131
+ if self._slash_enabled:
132
+ self._discover_slash_commands()
133
+
134
+ self._discovered = True
135
+
136
+ def _discover_builtin_commands(self) -> None:
137
+ """Discover and register built-in command classes."""
114
138
  # List of all command classes to register
115
139
  command_classes = [
116
140
  YoloCommand,
@@ -130,15 +154,69 @@ class CommandRegistry:
130
154
  InitCommand,
131
155
  TemplateCommand,
132
156
  TodoCommand,
157
+ CommandReloadCommand,
133
158
  PlanCommand, # Add plan command
134
159
  ExitPlanCommand, # Add exit plan command
160
+ QuickStartCommand, # Add quickstart command
135
161
  ]
136
162
 
137
163
  # Register all discovered commands
138
164
  for command_class in command_classes:
139
165
  self.register_command_class(command_class) # type: ignore[arg-type]
140
166
 
141
- self._discovered = True
167
+ def _discover_slash_commands(self) -> None:
168
+ """Discover and register markdown-based slash commands."""
169
+ try:
170
+ if not self._slash_loader:
171
+ # Dynamic import to avoid circular dependency
172
+ from .slash.loader import SlashCommandLoader
173
+
174
+ project_root = Path.cwd()
175
+ user_home = Path.home()
176
+ self._slash_loader = SlashCommandLoader(project_root, user_home)
177
+
178
+ self._slash_discovery_result = self._slash_loader.discover_commands()
179
+
180
+ # Register all discovered commands
181
+ registered_count = 0
182
+ for command_name, command in self._slash_discovery_result.commands.items():
183
+ try:
184
+ self.register(command)
185
+ registered_count += 1
186
+ except Exception as e:
187
+ logger.warning(f"Failed to register slash command '{command_name}': {e}")
188
+
189
+ # Log discovery summary
190
+ if registered_count > 0:
191
+ logger.info(f"Registered {registered_count} slash commands")
192
+
193
+ # Report conflicts and errors
194
+ self._report_slash_command_issues()
195
+
196
+ except Exception as e:
197
+ logger.error(f"Slash command discovery failed: {e}")
198
+ # Don't fail the entire system if slash commands can't load
199
+
200
+ def _report_slash_command_issues(self) -> None:
201
+ """Report conflicts and errors from slash command discovery."""
202
+ if not self._slash_discovery_result:
203
+ return
204
+
205
+ # Report conflicts
206
+ if self._slash_discovery_result.conflicts:
207
+ logger.info(f"Resolved {len(self._slash_discovery_result.conflicts)} command conflicts")
208
+ for cmd_name, conflicting_paths in self._slash_discovery_result.conflicts:
209
+ logger.debug(
210
+ f" {cmd_name}: {conflicting_paths[1]} overrode {conflicting_paths[0]}"
211
+ )
212
+
213
+ # Report errors (limit to first 3 for brevity)
214
+ if self._slash_discovery_result.errors:
215
+ logger.warning(
216
+ f"Failed to load {len(self._slash_discovery_result.errors)} command files"
217
+ )
218
+ for path, error in self._slash_discovery_result.errors[:3]:
219
+ logger.warning(f" {path}: {str(error)[:100]}...")
142
220
 
143
221
  def register_all_default_commands(self) -> None:
144
222
  """Register all default commands (backward compatibility)."""
@@ -269,3 +347,55 @@ class CommandRegistry:
269
347
  """Get all commands organized by category."""
270
348
  self.discover_commands()
271
349
  return self._categories.copy()
350
+
351
+ # Slash command utilities
352
+ def get_slash_commands(self) -> Dict[str, Command]:
353
+ """Get all registered slash commands."""
354
+ slash_commands = {}
355
+ for name, command in self._commands.items():
356
+ # Duck typing for SlashCommand - check if it has file_path attribute
357
+ if hasattr(command, "file_path") and hasattr(command, "namespace"):
358
+ slash_commands[name] = command
359
+ return slash_commands
360
+
361
+ def reload_slash_commands(self) -> int:
362
+ """Reload slash commands (useful for development)."""
363
+ if not self._slash_enabled:
364
+ return 0
365
+
366
+ slash_commands = self.get_slash_commands()
367
+ for cmd_name in list(slash_commands.keys()):
368
+ if cmd_name in self._commands:
369
+ del self._commands[cmd_name]
370
+ # Also remove from category
371
+ for category_commands in self._categories.values():
372
+ category_commands[:] = [
373
+ cmd
374
+ for cmd in category_commands
375
+ if not (hasattr(cmd, "file_path") and cmd.name == cmd_name)
376
+ ]
377
+
378
+ # Rediscover slash commands
379
+ self._slash_loader = None
380
+ self._slash_discovery_result = None
381
+ self._discover_slash_commands()
382
+
383
+ return len(self.get_slash_commands())
384
+
385
+ def enable_slash_commands(self, enabled: bool = True) -> None:
386
+ """Enable or disable slash command discovery."""
387
+ self._slash_enabled = enabled
388
+
389
+ def get_slash_command_stats(self) -> Dict[str, Any]:
390
+ """Get detailed statistics about slash command discovery."""
391
+ if not self._slash_discovery_result:
392
+ return {"enabled": self._slash_enabled, "discovered": False}
393
+
394
+ return {
395
+ "enabled": self._slash_enabled,
396
+ "discovered": True,
397
+ "stats": self._slash_discovery_result.stats,
398
+ "conflicts": len(self._slash_discovery_result.conflicts),
399
+ "errors": len(self._slash_discovery_result.errors),
400
+ "registered_commands": len(self.get_slash_commands()),
401
+ }
@@ -0,0 +1,32 @@
1
+ """Slash command system for TunaCode.
2
+
3
+ This module provides extensible markdown-based custom commands that can be
4
+ created by users and shared across teams.
5
+ """
6
+
7
+ # Only export the main classes that external code needs
8
+ from .types import (
9
+ CommandDiscoveryResult,
10
+ CommandSource,
11
+ ContextInjectionResult,
12
+ SecurityLevel,
13
+ SecurityViolation,
14
+ SlashCommandMetadata,
15
+ ValidationResult,
16
+ )
17
+
18
+ __all__ = [
19
+ "CommandSource",
20
+ "SlashCommandMetadata",
21
+ "CommandDiscoveryResult",
22
+ "ContextInjectionResult",
23
+ "SecurityLevel",
24
+ "SecurityViolation",
25
+ "ValidationResult",
26
+ ]
27
+
28
+ # Other classes can be imported directly when needed:
29
+ # from .command import SlashCommand
30
+ # from .loader import SlashCommandLoader
31
+ # from .processor import MarkdownTemplateProcessor
32
+ # from .validator import CommandValidator
@@ -0,0 +1,157 @@
1
+ """SlashCommand implementation for markdown-based commands."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, List, Optional
6
+
7
+ from ..base import Command, CommandCategory
8
+ from .processor import MarkdownTemplateProcessor
9
+ from .types import SlashCommandMetadata
10
+
11
+ if TYPE_CHECKING:
12
+ from ....types import CommandArgs, CommandContext, CommandResult
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SlashCommand(Command):
18
+ """Markdown-based slash command implementation."""
19
+
20
+ def __init__(self, file_path: Path, namespace: str, command_parts: List[str]):
21
+ self.file_path = file_path
22
+ self.namespace = namespace # "project" or "user"
23
+ self.command_parts = command_parts # ["test", "unit"] for test:unit
24
+ self._metadata: Optional[SlashCommandMetadata] = None
25
+ self._content: Optional[str] = None
26
+ self._loaded = False
27
+
28
+ def _lazy_load(self) -> None:
29
+ """Load content and metadata on first access."""
30
+ if self._loaded:
31
+ return
32
+
33
+ try:
34
+ content = self.file_path.read_text(encoding="utf-8")
35
+ processor = MarkdownTemplateProcessor()
36
+ frontmatter, markdown = processor.parse_frontmatter(content)
37
+
38
+ self._content = markdown
39
+ self._metadata = SlashCommandMetadata(
40
+ description=frontmatter.get("description", "Custom command")
41
+ if frontmatter
42
+ else "Custom command",
43
+ allowed_tools=frontmatter.get("allowed-tools") if frontmatter else None,
44
+ timeout=frontmatter.get("timeout") if frontmatter else None,
45
+ parameters=frontmatter.get("parameters", {}) if frontmatter else {},
46
+ )
47
+ self._loaded = True
48
+
49
+ except Exception as e:
50
+ logger.error(f"Failed to load slash command {self.file_path}: {e}")
51
+ self._metadata = SlashCommandMetadata(description=f"Error loading command: {str(e)}")
52
+ self._content = ""
53
+ self._loaded = True
54
+
55
+ @property
56
+ def name(self) -> str:
57
+ """The primary name of the command."""
58
+ return f"{self.namespace}:{':'.join(self.command_parts)}"
59
+
60
+ @property
61
+ def aliases(self) -> List[str]:
62
+ """Alternative names/aliases for the command."""
63
+ return [f"/{self.name}"] # Slash prefix for invocation
64
+
65
+ @property
66
+ def description(self) -> str:
67
+ """Description of what the command does."""
68
+ self._lazy_load()
69
+ return self._metadata.description if self._metadata else "Custom command"
70
+
71
+ @property
72
+ def category(self) -> CommandCategory:
73
+ """Category this command belongs to."""
74
+ return CommandCategory.SYSTEM # Could be made configurable
75
+
76
+ async def execute(self, args: "CommandArgs", context: "CommandContext") -> "CommandResult":
77
+ """Execute the slash command."""
78
+ self._lazy_load()
79
+
80
+ if not self._content:
81
+ return "Error: Could not load command content"
82
+
83
+ # Process template with context injection
84
+ max_context_size = 100_000
85
+ max_files = 50
86
+ if self._metadata and self._metadata.parameters:
87
+ max_context_size = int(self._metadata.parameters.get("max_context_size", 100_000))
88
+ max_files = int(self._metadata.parameters.get("max_files", 50))
89
+
90
+ processor = MarkdownTemplateProcessor(
91
+ max_context_size=max_context_size, max_files=max_files
92
+ )
93
+
94
+ injection_result = processor.process_template_with_context(self._content, args, context)
95
+
96
+ # Show warnings if any (import ui dynamically to avoid circular imports)
97
+ if injection_result.warnings:
98
+ try:
99
+ from ....ui import console as ui
100
+
101
+ for warning in injection_result.warnings:
102
+ await ui.warning(f"Context injection: {warning}")
103
+ except ImportError:
104
+ # Fallback to logging if UI not available
105
+ for warning in injection_result.warnings:
106
+ logger.warning(f"Context injection: {warning}")
107
+
108
+ # Show context stats for debugging (if enabled)
109
+ try:
110
+ if context.state_manager.session.show_thoughts:
111
+ from ....ui import console as ui
112
+
113
+ await ui.muted(
114
+ f"Context: {len(injection_result.included_files)} files, "
115
+ f"{injection_result.total_size} chars, "
116
+ f"{len(injection_result.executed_commands)} commands"
117
+ )
118
+ except (ImportError, AttributeError):
119
+ pass # Skip context stats if not available
120
+
121
+ # Apply tool restrictions if specified
122
+ if self._metadata and self._metadata.allowed_tools:
123
+ # Set tool handler restrictions similar to template system
124
+ if (
125
+ hasattr(context.state_manager, "tool_handler")
126
+ and context.state_manager.tool_handler
127
+ ):
128
+ # Store current restrictions and apply new ones
129
+ original_restrictions = getattr(
130
+ context.state_manager.tool_handler, "allowed_tools", None
131
+ )
132
+ context.state_manager.tool_handler.allowed_tools = self._metadata.allowed_tools
133
+
134
+ try:
135
+ # Execute with restrictions
136
+ if context.process_request:
137
+ result = await context.process_request(
138
+ injection_result.processed_content, context.state_manager, True
139
+ )
140
+ return result
141
+ else:
142
+ return "Error: No process_request callback available"
143
+ finally:
144
+ # Restore original restrictions
145
+ context.state_manager.tool_handler.allowed_tools = original_restrictions
146
+ else:
147
+ # Execute without restrictions
148
+ if context.process_request:
149
+ result = await context.process_request(
150
+ injection_result.processed_content, context.state_manager, True
151
+ )
152
+ return result
153
+ else:
154
+ return "Error: No process_request callback available"
155
+
156
+ # Default return if no metadata
157
+ return "Command executed"