tunacode-cli 0.0.35__py3-none-any.whl → 0.0.37__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/__init__.py +62 -0
- tunacode/cli/commands/base.py +99 -0
- tunacode/cli/commands/implementations/__init__.py +38 -0
- tunacode/cli/commands/implementations/conversation.py +115 -0
- tunacode/cli/commands/implementations/debug.py +189 -0
- tunacode/cli/commands/implementations/development.py +77 -0
- tunacode/cli/commands/implementations/model.py +61 -0
- tunacode/cli/commands/implementations/system.py +216 -0
- tunacode/cli/commands/registry.py +236 -0
- tunacode/cli/repl.py +91 -30
- tunacode/configuration/settings.py +9 -2
- tunacode/constants.py +1 -1
- tunacode/core/agents/main.py +53 -3
- tunacode/core/agents/utils.py +304 -0
- tunacode/core/setup/config_setup.py +0 -1
- tunacode/core/state.py +13 -2
- tunacode/setup.py +7 -2
- tunacode/tools/read_file.py +8 -2
- tunacode/tools/read_file_async_poc.py +18 -10
- tunacode/tools/run_command.py +11 -4
- tunacode/ui/console.py +31 -4
- tunacode/ui/output.py +7 -2
- tunacode/ui/panels.py +98 -5
- tunacode/ui/utils.py +3 -0
- tunacode/utils/text_utils.py +6 -2
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/METADATA +17 -17
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/RECORD +31 -21
- tunacode/cli/commands.py +0 -893
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Command system for TunaCode CLI.
|
|
2
|
+
|
|
3
|
+
This package provides a modular command system with:
|
|
4
|
+
- Base classes and infrastructure in `base.py`
|
|
5
|
+
- Command registry and factory in `registry.py`
|
|
6
|
+
- Command implementations organized by category in `implementations/`
|
|
7
|
+
|
|
8
|
+
The main public API provides backward compatibility with the original
|
|
9
|
+
commands.py module while enabling better organization and maintainability.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Import base classes and infrastructure
|
|
13
|
+
from .base import Command, CommandCategory, CommandSpec, SimpleCommand
|
|
14
|
+
|
|
15
|
+
# Import all command implementations for backward compatibility
|
|
16
|
+
from .implementations import (
|
|
17
|
+
BranchCommand,
|
|
18
|
+
ClearCommand,
|
|
19
|
+
CompactCommand,
|
|
20
|
+
DumpCommand,
|
|
21
|
+
FixCommand,
|
|
22
|
+
HelpCommand,
|
|
23
|
+
InitCommand,
|
|
24
|
+
IterationsCommand,
|
|
25
|
+
ModelCommand,
|
|
26
|
+
ParseToolsCommand,
|
|
27
|
+
RefreshConfigCommand,
|
|
28
|
+
ThoughtsCommand,
|
|
29
|
+
UpdateCommand,
|
|
30
|
+
YoloCommand,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Import registry and factory
|
|
34
|
+
from .registry import CommandDependencies, CommandFactory, CommandRegistry
|
|
35
|
+
|
|
36
|
+
# Maintain backward compatibility by exposing the same public API
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Base infrastructure
|
|
39
|
+
"Command",
|
|
40
|
+
"SimpleCommand",
|
|
41
|
+
"CommandSpec",
|
|
42
|
+
"CommandCategory",
|
|
43
|
+
# Registry and factory
|
|
44
|
+
"CommandRegistry",
|
|
45
|
+
"CommandFactory",
|
|
46
|
+
"CommandDependencies",
|
|
47
|
+
# All command classes (imported from implementations)
|
|
48
|
+
"YoloCommand",
|
|
49
|
+
"DumpCommand",
|
|
50
|
+
"ThoughtsCommand",
|
|
51
|
+
"IterationsCommand",
|
|
52
|
+
"ClearCommand",
|
|
53
|
+
"FixCommand",
|
|
54
|
+
"ParseToolsCommand",
|
|
55
|
+
"RefreshConfigCommand",
|
|
56
|
+
"HelpCommand",
|
|
57
|
+
"BranchCommand",
|
|
58
|
+
"CompactCommand",
|
|
59
|
+
"UpdateCommand",
|
|
60
|
+
"ModelCommand",
|
|
61
|
+
"InitCommand",
|
|
62
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Base classes and infrastructure for TunaCode CLI commands."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from ...types import CommandArgs, CommandContext, CommandResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandCategory(Enum):
|
|
12
|
+
"""Categories for organizing commands."""
|
|
13
|
+
|
|
14
|
+
SYSTEM = "system"
|
|
15
|
+
NAVIGATION = "navigation"
|
|
16
|
+
DEVELOPMENT = "development"
|
|
17
|
+
MODEL = "model"
|
|
18
|
+
DEBUG = "debug"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Command(ABC):
|
|
22
|
+
"""Base class for all commands."""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
"""The primary name of the command."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def aliases(self) -> CommandArgs:
|
|
33
|
+
"""Alternative names/aliases for the command."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def description(self) -> str:
|
|
38
|
+
"""Description of what the command does."""
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def category(self) -> CommandCategory:
|
|
43
|
+
"""Category this command belongs to."""
|
|
44
|
+
return CommandCategory.SYSTEM
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def execute(self, args: CommandArgs, context: CommandContext) -> CommandResult:
|
|
48
|
+
"""
|
|
49
|
+
Execute the command.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
args: Command arguments (excluding the command name)
|
|
53
|
+
context: Execution context with state and config
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Command-specific return value
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CommandSpec:
|
|
63
|
+
"""Specification for a command's metadata."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
aliases: List[str]
|
|
67
|
+
description: str
|
|
68
|
+
category: CommandCategory = CommandCategory.SYSTEM
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SimpleCommand(Command):
|
|
72
|
+
"""Base class for simple commands without complex logic.
|
|
73
|
+
|
|
74
|
+
This class provides a standard implementation for commands that don't
|
|
75
|
+
require special initialization or complex behavior. It reads all
|
|
76
|
+
properties from a class-level CommandSpec attribute.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
spec: CommandSpec
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
"""The primary name of the command."""
|
|
84
|
+
return self.__class__.spec.name
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def aliases(self) -> CommandArgs:
|
|
88
|
+
"""Alternative names/aliases for the command."""
|
|
89
|
+
return self.__class__.spec.aliases
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def description(self) -> str:
|
|
93
|
+
"""Description of what the command does."""
|
|
94
|
+
return self.__class__.spec.description
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def category(self) -> CommandCategory:
|
|
98
|
+
"""Category this command belongs to."""
|
|
99
|
+
return self.__class__.spec.category
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Command implementations for TunaCode CLI."""
|
|
2
|
+
|
|
3
|
+
# Import all command classes for easy access
|
|
4
|
+
from .conversation import CompactCommand
|
|
5
|
+
from .debug import (
|
|
6
|
+
DumpCommand,
|
|
7
|
+
FixCommand,
|
|
8
|
+
IterationsCommand,
|
|
9
|
+
ParseToolsCommand,
|
|
10
|
+
ThoughtsCommand,
|
|
11
|
+
YoloCommand,
|
|
12
|
+
)
|
|
13
|
+
from .development import BranchCommand, InitCommand
|
|
14
|
+
from .model import ModelCommand
|
|
15
|
+
from .system import ClearCommand, HelpCommand, RefreshConfigCommand, StreamingCommand, UpdateCommand
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# System commands
|
|
19
|
+
"HelpCommand",
|
|
20
|
+
"ClearCommand",
|
|
21
|
+
"RefreshConfigCommand",
|
|
22
|
+
"StreamingCommand",
|
|
23
|
+
"UpdateCommand",
|
|
24
|
+
# Debug commands
|
|
25
|
+
"YoloCommand",
|
|
26
|
+
"DumpCommand",
|
|
27
|
+
"ThoughtsCommand",
|
|
28
|
+
"IterationsCommand",
|
|
29
|
+
"FixCommand",
|
|
30
|
+
"ParseToolsCommand",
|
|
31
|
+
# Development commands
|
|
32
|
+
"BranchCommand",
|
|
33
|
+
"InitCommand",
|
|
34
|
+
# Model commands
|
|
35
|
+
"ModelCommand",
|
|
36
|
+
# Conversation commands
|
|
37
|
+
"CompactCommand",
|
|
38
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Conversation management commands for TunaCode CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from ....types import CommandContext, ProcessRequestCallback
|
|
6
|
+
from ....ui import console as ui
|
|
7
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CompactCommand(SimpleCommand):
|
|
11
|
+
"""Compact conversation context."""
|
|
12
|
+
|
|
13
|
+
spec = CommandSpec(
|
|
14
|
+
name="compact",
|
|
15
|
+
aliases=["/compact"],
|
|
16
|
+
description="Summarize and compact the conversation history",
|
|
17
|
+
category=CommandCategory.SYSTEM,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def __init__(self, process_request_callback: Optional[ProcessRequestCallback] = None):
|
|
21
|
+
self._process_request = process_request_callback
|
|
22
|
+
|
|
23
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
24
|
+
# Use the injected callback or get it from context
|
|
25
|
+
process_request = self._process_request or context.process_request
|
|
26
|
+
|
|
27
|
+
if not process_request:
|
|
28
|
+
await ui.error("Compact command not available - process_request not configured")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Count current messages
|
|
32
|
+
original_count = len(context.state_manager.session.messages)
|
|
33
|
+
|
|
34
|
+
# Generate summary with output captured
|
|
35
|
+
summary_prompt = (
|
|
36
|
+
"Summarize the conversation so far in a concise paragraph, "
|
|
37
|
+
"focusing on the main topics discussed and any important context "
|
|
38
|
+
"that should be preserved."
|
|
39
|
+
)
|
|
40
|
+
result = await process_request(
|
|
41
|
+
summary_prompt,
|
|
42
|
+
context.state_manager,
|
|
43
|
+
output=False, # We'll handle the output ourselves
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Extract summary text from result
|
|
47
|
+
summary_text = ""
|
|
48
|
+
|
|
49
|
+
# First try: standard result structure
|
|
50
|
+
if (
|
|
51
|
+
result
|
|
52
|
+
and hasattr(result, "result")
|
|
53
|
+
and result.result
|
|
54
|
+
and hasattr(result.result, "output")
|
|
55
|
+
):
|
|
56
|
+
summary_text = result.result.output
|
|
57
|
+
|
|
58
|
+
# Second try: check messages for assistant response
|
|
59
|
+
if not summary_text:
|
|
60
|
+
messages = context.state_manager.session.messages
|
|
61
|
+
# Look through new messages in reverse order
|
|
62
|
+
for i in range(len(messages) - 1, original_count - 1, -1):
|
|
63
|
+
msg = messages[i]
|
|
64
|
+
# Handle ModelResponse objects
|
|
65
|
+
if hasattr(msg, "parts") and msg.parts:
|
|
66
|
+
for part in msg.parts:
|
|
67
|
+
if hasattr(part, "content") and part.content:
|
|
68
|
+
content = part.content
|
|
69
|
+
# Skip JSON thought objects
|
|
70
|
+
if content.strip().startswith('{"thought"'):
|
|
71
|
+
lines = content.split("\n")
|
|
72
|
+
# Find the actual summary after the JSON
|
|
73
|
+
for i, line in enumerate(lines):
|
|
74
|
+
if (
|
|
75
|
+
line.strip()
|
|
76
|
+
and not line.strip().startswith("{")
|
|
77
|
+
and not line.strip().endswith("}")
|
|
78
|
+
):
|
|
79
|
+
summary_text = "\n".join(lines[i:]).strip()
|
|
80
|
+
break
|
|
81
|
+
else:
|
|
82
|
+
summary_text = content
|
|
83
|
+
if summary_text:
|
|
84
|
+
break
|
|
85
|
+
# Handle dict-style messages
|
|
86
|
+
elif isinstance(msg, dict):
|
|
87
|
+
if msg.get("role") == "assistant" and msg.get("content"):
|
|
88
|
+
summary_text = msg["content"]
|
|
89
|
+
break
|
|
90
|
+
# Handle other message types
|
|
91
|
+
elif hasattr(msg, "content") and hasattr(msg, "role"):
|
|
92
|
+
if getattr(msg, "role", None) == "assistant":
|
|
93
|
+
summary_text = msg.content
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
if summary_text:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
if not summary_text:
|
|
100
|
+
await ui.error("Failed to generate summary - no assistant response found")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Display summary in a formatted panel
|
|
104
|
+
from tunacode.ui import panels
|
|
105
|
+
|
|
106
|
+
await panels.panel("Conversation Summary", summary_text, border_style="cyan")
|
|
107
|
+
|
|
108
|
+
# Show statistics
|
|
109
|
+
await ui.info(f"Current message count: {original_count}")
|
|
110
|
+
await ui.info("After compaction: 3 (summary + last 2 messages)")
|
|
111
|
+
|
|
112
|
+
# Truncate the conversation history
|
|
113
|
+
context.state_manager.session.messages = context.state_manager.session.messages[-2:]
|
|
114
|
+
|
|
115
|
+
await ui.success("Context history has been summarized and truncated.")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Debug and troubleshooting commands for TunaCode CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ....types import CommandContext
|
|
6
|
+
from ....ui import console as ui
|
|
7
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YoloCommand(SimpleCommand):
|
|
11
|
+
"""Toggle YOLO mode (skip confirmations)."""
|
|
12
|
+
|
|
13
|
+
spec = CommandSpec(
|
|
14
|
+
name="yolo",
|
|
15
|
+
aliases=["/yolo"],
|
|
16
|
+
description="Toggle YOLO mode (skip tool confirmations)",
|
|
17
|
+
category=CommandCategory.DEVELOPMENT,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
21
|
+
state = context.state_manager.session
|
|
22
|
+
state.yolo = not state.yolo
|
|
23
|
+
if state.yolo:
|
|
24
|
+
await ui.success("All tools are now active ⚡ Please proceed with caution.\n")
|
|
25
|
+
else:
|
|
26
|
+
await ui.info("Tool confirmations re-enabled for safety.\n")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DumpCommand(SimpleCommand):
|
|
30
|
+
"""Dump message history."""
|
|
31
|
+
|
|
32
|
+
spec = CommandSpec(
|
|
33
|
+
name="dump",
|
|
34
|
+
aliases=["/dump"],
|
|
35
|
+
description="Dump the current message history",
|
|
36
|
+
category=CommandCategory.DEBUG,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
40
|
+
await ui.dump_messages(context.state_manager.session.messages)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ThoughtsCommand(SimpleCommand):
|
|
44
|
+
"""Toggle display of agent thoughts."""
|
|
45
|
+
|
|
46
|
+
spec = CommandSpec(
|
|
47
|
+
name="thoughts",
|
|
48
|
+
aliases=["/thoughts"],
|
|
49
|
+
description="Show or hide agent thought messages",
|
|
50
|
+
category=CommandCategory.DEBUG,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
54
|
+
state = context.state_manager.session
|
|
55
|
+
|
|
56
|
+
# No args - toggle
|
|
57
|
+
if not args:
|
|
58
|
+
state.show_thoughts = not state.show_thoughts
|
|
59
|
+
status = "ON" if state.show_thoughts else "OFF"
|
|
60
|
+
await ui.success(f"Thought display {status}")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Parse argument
|
|
64
|
+
arg = args[0].lower()
|
|
65
|
+
if arg in {"on", "1", "true"}:
|
|
66
|
+
state.show_thoughts = True
|
|
67
|
+
elif arg in {"off", "0", "false"}:
|
|
68
|
+
state.show_thoughts = False
|
|
69
|
+
else:
|
|
70
|
+
await ui.error("Usage: /thoughts [on|off]")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
status = "ON" if state.show_thoughts else "OFF"
|
|
74
|
+
await ui.success(f"Thought display {status}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class IterationsCommand(SimpleCommand):
|
|
78
|
+
"""Configure maximum agent iterations for ReAct reasoning."""
|
|
79
|
+
|
|
80
|
+
spec = CommandSpec(
|
|
81
|
+
name="iterations",
|
|
82
|
+
aliases=["/iterations"],
|
|
83
|
+
description="Set maximum agent iterations for complex reasoning",
|
|
84
|
+
category=CommandCategory.DEBUG,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
88
|
+
state = context.state_manager.session
|
|
89
|
+
|
|
90
|
+
# Guard clause - handle "no args" case first and return early
|
|
91
|
+
if not args:
|
|
92
|
+
current = state.user_config.get("settings", {}).get("max_iterations", 40)
|
|
93
|
+
await ui.info(f"Current maximum iterations: {current}")
|
|
94
|
+
await ui.muted("Usage: /iterations <number> (1-100)")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# update the logic to not be as nested messely, the above guars needing to get as messy
|
|
98
|
+
try:
|
|
99
|
+
new_limit = int(args[0])
|
|
100
|
+
if new_limit < 1 or new_limit > 100:
|
|
101
|
+
await ui.error("Iterations must be between 1 and 100")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Update the user config
|
|
105
|
+
if "settings" not in state.user_config:
|
|
106
|
+
state.user_config["settings"] = {}
|
|
107
|
+
state.user_config["settings"]["max_iterations"] = new_limit
|
|
108
|
+
|
|
109
|
+
await ui.success(f"Maximum iterations set to {new_limit}")
|
|
110
|
+
except ValueError:
|
|
111
|
+
await ui.error("Please provide a valid number")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class FixCommand(SimpleCommand):
|
|
115
|
+
"""Fix orphaned tool calls that cause API errors."""
|
|
116
|
+
|
|
117
|
+
spec = CommandSpec(
|
|
118
|
+
name="fix",
|
|
119
|
+
aliases=["/fix"],
|
|
120
|
+
description="Fix orphaned tool calls causing API errors",
|
|
121
|
+
category=CommandCategory.DEBUG,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
125
|
+
from tunacode.core.agents.main import patch_tool_messages
|
|
126
|
+
|
|
127
|
+
# Count current messages
|
|
128
|
+
before_count = len(context.state_manager.session.messages)
|
|
129
|
+
|
|
130
|
+
# Patch orphaned tool calls
|
|
131
|
+
patch_tool_messages("Tool call resolved by /fix command", context.state_manager)
|
|
132
|
+
|
|
133
|
+
# Count after patching
|
|
134
|
+
after_count = len(context.state_manager.session.messages)
|
|
135
|
+
patched_count = after_count - before_count
|
|
136
|
+
|
|
137
|
+
if patched_count > 0:
|
|
138
|
+
await ui.success(f"Fixed {patched_count} orphaned tool call(s)")
|
|
139
|
+
await ui.muted("You can now continue the conversation normally")
|
|
140
|
+
else:
|
|
141
|
+
await ui.info("No orphaned tool calls found")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ParseToolsCommand(SimpleCommand):
|
|
145
|
+
"""Parse and execute JSON tool calls from the last response."""
|
|
146
|
+
|
|
147
|
+
spec = CommandSpec(
|
|
148
|
+
name="parsetools",
|
|
149
|
+
aliases=["/parsetools"],
|
|
150
|
+
description=("Parse JSON tool calls from last response when structured calling fails"),
|
|
151
|
+
category=CommandCategory.DEBUG,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
155
|
+
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
156
|
+
|
|
157
|
+
# Find the last model response in messages
|
|
158
|
+
messages = context.state_manager.session.messages
|
|
159
|
+
if not messages:
|
|
160
|
+
await ui.error("No message history found")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Look for the most recent response with text content
|
|
164
|
+
found_content = False
|
|
165
|
+
for msg in reversed(messages):
|
|
166
|
+
if hasattr(msg, "parts"):
|
|
167
|
+
for part in msg.parts:
|
|
168
|
+
if hasattr(part, "content") and isinstance(part.content, str):
|
|
169
|
+
# Create tool callback
|
|
170
|
+
from tunacode.cli.repl import _tool_handler
|
|
171
|
+
|
|
172
|
+
def tool_callback_with_state(part, node):
|
|
173
|
+
return _tool_handler(part, node, context.state_manager)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
await extract_and_execute_tool_calls(
|
|
177
|
+
part.content,
|
|
178
|
+
tool_callback_with_state,
|
|
179
|
+
context.state_manager,
|
|
180
|
+
)
|
|
181
|
+
await ui.success("JSON tool parsing completed")
|
|
182
|
+
found_content = True
|
|
183
|
+
return
|
|
184
|
+
except Exception as e:
|
|
185
|
+
await ui.error(f"Failed to parse tools: {str(e)}")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if not found_content:
|
|
189
|
+
await ui.error("No parseable content found in recent messages")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Development-focused commands for TunaCode CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from ....types import CommandContext, CommandResult
|
|
8
|
+
from ....ui import console as ui
|
|
9
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BranchCommand(SimpleCommand):
|
|
13
|
+
"""Create and switch to a new git branch."""
|
|
14
|
+
|
|
15
|
+
spec = CommandSpec(
|
|
16
|
+
name="branch",
|
|
17
|
+
aliases=["/branch"],
|
|
18
|
+
description="Create and switch to a new git branch",
|
|
19
|
+
category=CommandCategory.DEVELOPMENT,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
23
|
+
if not args:
|
|
24
|
+
await ui.error("Usage: /branch <branch-name>")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
if not os.path.exists(".git"):
|
|
28
|
+
await ui.error("Not a git repository")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
branch_name = args[0]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
subprocess.run(
|
|
35
|
+
["git", "checkout", "-b", branch_name],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
check=True,
|
|
39
|
+
timeout=5,
|
|
40
|
+
)
|
|
41
|
+
await ui.success(f"Switched to new branch '{branch_name}'")
|
|
42
|
+
except subprocess.TimeoutExpired:
|
|
43
|
+
await ui.error("Git command timed out")
|
|
44
|
+
except subprocess.CalledProcessError as e:
|
|
45
|
+
error_msg = e.stderr.strip() if e.stderr else str(e)
|
|
46
|
+
await ui.error(f"Git error: {error_msg}")
|
|
47
|
+
except FileNotFoundError:
|
|
48
|
+
await ui.error("Git executable not found")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InitCommand(SimpleCommand):
|
|
52
|
+
"""Creates or updates TUNACODE.md with project-specific context."""
|
|
53
|
+
|
|
54
|
+
spec = CommandSpec(
|
|
55
|
+
name="/init",
|
|
56
|
+
aliases=[],
|
|
57
|
+
description="Analyze codebase and create/update TUNACODE.md file",
|
|
58
|
+
category=CommandCategory.DEVELOPMENT,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def execute(self, args, context: CommandContext) -> CommandResult:
|
|
62
|
+
"""Execute the init command."""
|
|
63
|
+
# Minimal implementation to make test pass
|
|
64
|
+
prompt = """Please analyze this codebase and create a TUNACODE.md file containing:
|
|
65
|
+
1. Build/lint/test commands - especially for running a single test
|
|
66
|
+
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
|
|
67
|
+
|
|
68
|
+
The file you create will be given to agentic coding agents (such as yourself) that operate in this repository.
|
|
69
|
+
Make it about 20 lines long.
|
|
70
|
+
If there's already a TUNACODE.md, improve it.
|
|
71
|
+
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md),
|
|
72
|
+
make sure to include them."""
|
|
73
|
+
|
|
74
|
+
# Call the agent to analyze and create/update the file
|
|
75
|
+
await context.process_request(prompt, context.state_manager)
|
|
76
|
+
|
|
77
|
+
return None
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Model management commands for TunaCode CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .... import utils
|
|
6
|
+
from ....exceptions import ConfigurationError
|
|
7
|
+
from ....types import CommandArgs, CommandContext
|
|
8
|
+
from ....ui import console as ui
|
|
9
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelCommand(SimpleCommand):
|
|
13
|
+
"""Manage model selection."""
|
|
14
|
+
|
|
15
|
+
spec = CommandSpec(
|
|
16
|
+
name="model",
|
|
17
|
+
aliases=["/model"],
|
|
18
|
+
description="Switch model (e.g., /model gpt-4 or /model openai:gpt-4)",
|
|
19
|
+
category=CommandCategory.MODEL,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def execute(self, args: CommandArgs, context: CommandContext) -> Optional[str]:
|
|
23
|
+
# No arguments - show current model
|
|
24
|
+
if not args:
|
|
25
|
+
current_model = context.state_manager.session.current_model
|
|
26
|
+
await ui.info(f"Current model: {current_model}")
|
|
27
|
+
await ui.muted("Usage: /model <provider:model-name> [default]")
|
|
28
|
+
await ui.muted("Example: /model openai:gpt-4.1")
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
# Get the model name from args
|
|
32
|
+
model_name = args[0]
|
|
33
|
+
|
|
34
|
+
# Check if provider prefix is present
|
|
35
|
+
if ":" not in model_name:
|
|
36
|
+
await ui.error("Model name must include provider prefix")
|
|
37
|
+
await ui.muted("Format: provider:model-name")
|
|
38
|
+
await ui.muted(
|
|
39
|
+
"Examples: openai:gpt-4.1, anthropic:claude-3-opus, google-gla:gemini-2.0-flash"
|
|
40
|
+
)
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# No validation - user is responsible for correct model names
|
|
44
|
+
await ui.warning("Model set without validation - verify the model name is correct")
|
|
45
|
+
|
|
46
|
+
# Set the model
|
|
47
|
+
context.state_manager.session.current_model = model_name
|
|
48
|
+
|
|
49
|
+
# Check if setting as default
|
|
50
|
+
if len(args) > 1 and args[1] == "default":
|
|
51
|
+
try:
|
|
52
|
+
utils.user_configuration.set_default_model(model_name, context.state_manager)
|
|
53
|
+
await ui.muted("Updating default model")
|
|
54
|
+
return "restart"
|
|
55
|
+
except ConfigurationError as e:
|
|
56
|
+
await ui.error(str(e))
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Show success message with the new model
|
|
60
|
+
await ui.success(f"Switched to model: {model_name}")
|
|
61
|
+
return None
|