tunacode-cli 0.0.16__py3-none-any.whl → 0.0.18__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 +39 -41
- tunacode/cli/main.py +29 -26
- tunacode/cli/repl.py +35 -10
- tunacode/cli/textual_app.py +69 -66
- tunacode/cli/textual_bridge.py +33 -32
- tunacode/configuration/settings.py +2 -9
- tunacode/constants.py +2 -4
- tunacode/context.py +1 -1
- tunacode/core/agents/main.py +88 -62
- tunacode/core/setup/config_setup.py +79 -44
- tunacode/core/setup/coordinator.py +20 -13
- tunacode/core/setup/git_safety_setup.py +35 -49
- tunacode/core/state.py +2 -9
- tunacode/exceptions.py +0 -2
- tunacode/tools/__init__.py +10 -1
- tunacode/tools/base.py +1 -1
- tunacode/tools/bash.py +5 -5
- tunacode/tools/grep.py +210 -250
- tunacode/tools/read_file.py +2 -8
- tunacode/tools/run_command.py +4 -11
- tunacode/tools/update_file.py +2 -6
- tunacode/ui/completers.py +32 -31
- tunacode/ui/console.py +1 -0
- tunacode/ui/input.py +8 -5
- tunacode/ui/keybindings.py +1 -3
- tunacode/ui/lexers.py +16 -16
- tunacode/ui/output.py +7 -2
- tunacode/ui/panels.py +8 -8
- tunacode/ui/prompt_manager.py +19 -7
- tunacode/utils/import_cache.py +11 -0
- tunacode/utils/user_configuration.py +24 -2
- {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/METADATA +56 -2
- tunacode_cli-0.0.18.dist-info/RECORD +68 -0
- tunacode_cli-0.0.16.dist-info/RECORD +0 -67
- {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/top_level.txt +0 -0
tunacode/tools/read_file.py
CHANGED
|
@@ -7,14 +7,8 @@ Provides safe file reading with size limits and proper error handling.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
|
-
from tunacode.constants import (
|
|
11
|
-
|
|
12
|
-
ERROR_FILE_DECODE_DETAILS,
|
|
13
|
-
ERROR_FILE_NOT_FOUND,
|
|
14
|
-
ERROR_FILE_TOO_LARGE,
|
|
15
|
-
MAX_FILE_SIZE,
|
|
16
|
-
MSG_FILE_SIZE_LIMIT,
|
|
17
|
-
)
|
|
10
|
+
from tunacode.constants import (ERROR_FILE_DECODE, ERROR_FILE_DECODE_DETAILS, ERROR_FILE_NOT_FOUND,
|
|
11
|
+
ERROR_FILE_TOO_LARGE, MAX_FILE_SIZE, MSG_FILE_SIZE_LIMIT)
|
|
18
12
|
from tunacode.exceptions import ToolExecutionError
|
|
19
13
|
from tunacode.tools.base import FileBasedTool
|
|
20
14
|
from tunacode.types import ToolResult
|
tunacode/tools/run_command.py
CHANGED
|
@@ -7,17 +7,10 @@ Provides controlled shell command execution with output capture and truncation.
|
|
|
7
7
|
|
|
8
8
|
import subprocess
|
|
9
9
|
|
|
10
|
-
from tunacode.constants import (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
CMD_OUTPUT_TRUNCATED,
|
|
15
|
-
COMMAND_OUTPUT_END_SIZE,
|
|
16
|
-
COMMAND_OUTPUT_START_INDEX,
|
|
17
|
-
COMMAND_OUTPUT_THRESHOLD,
|
|
18
|
-
ERROR_COMMAND_EXECUTION,
|
|
19
|
-
MAX_COMMAND_OUTPUT,
|
|
20
|
-
)
|
|
10
|
+
from tunacode.constants import (CMD_OUTPUT_FORMAT, CMD_OUTPUT_NO_ERRORS, CMD_OUTPUT_NO_OUTPUT,
|
|
11
|
+
CMD_OUTPUT_TRUNCATED, COMMAND_OUTPUT_END_SIZE,
|
|
12
|
+
COMMAND_OUTPUT_START_INDEX, COMMAND_OUTPUT_THRESHOLD,
|
|
13
|
+
ERROR_COMMAND_EXECUTION, MAX_COMMAND_OUTPUT)
|
|
21
14
|
from tunacode.exceptions import ToolExecutionError
|
|
22
15
|
from tunacode.tools.base import BaseTool
|
|
23
16
|
from tunacode.types import ToolResult
|
tunacode/tools/update_file.py
CHANGED
|
@@ -21,9 +21,7 @@ class UpdateFileTool(FileBasedTool):
|
|
|
21
21
|
def tool_name(self) -> str:
|
|
22
22
|
return "Update"
|
|
23
23
|
|
|
24
|
-
async def _execute(
|
|
25
|
-
self, filepath: str, target: str, patch: str
|
|
26
|
-
) -> ToolResult:
|
|
24
|
+
async def _execute(self, filepath: str, target: str, patch: str) -> ToolResult:
|
|
27
25
|
"""Update an existing file by replacing a target text block with a patch.
|
|
28
26
|
|
|
29
27
|
Args:
|
|
@@ -73,9 +71,7 @@ class UpdateFileTool(FileBasedTool):
|
|
|
73
71
|
|
|
74
72
|
return f"File '{filepath}' updated successfully."
|
|
75
73
|
|
|
76
|
-
def _format_args(
|
|
77
|
-
self, filepath: str, target: str = None, patch: str = None
|
|
78
|
-
) -> str:
|
|
74
|
+
def _format_args(self, filepath: str, target: str = None, patch: str = None) -> str:
|
|
79
75
|
"""Format arguments, truncating target and patch for display."""
|
|
80
76
|
args = [repr(filepath)]
|
|
81
77
|
|
tunacode/ui/completers.py
CHANGED
|
@@ -11,53 +11,52 @@ from ..cli.commands import CommandRegistry
|
|
|
11
11
|
|
|
12
12
|
class CommandCompleter(Completer):
|
|
13
13
|
"""Completer for slash commands."""
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
def __init__(self, command_registry: Optional[CommandRegistry] = None):
|
|
16
16
|
self.command_registry = command_registry
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def get_completions(
|
|
19
19
|
self, document: Document, complete_event: CompleteEvent
|
|
20
20
|
) -> Iterable[Completion]:
|
|
21
21
|
"""Get completions for slash commands."""
|
|
22
22
|
# Get the text before cursor
|
|
23
23
|
text = document.text_before_cursor
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
# Check if we're at the start of a line or after whitespace
|
|
26
|
-
if text and not text.isspace() and text[-1] !=
|
|
26
|
+
if text and not text.isspace() and text[-1] != "\n":
|
|
27
27
|
# Only complete commands at the start of input or after a newline
|
|
28
|
-
last_newline = text.rfind(
|
|
29
|
-
line_start = text[last_newline + 1:] if last_newline >= 0 else text
|
|
30
|
-
|
|
28
|
+
last_newline = text.rfind("\n")
|
|
29
|
+
line_start = text[last_newline + 1 :] if last_newline >= 0 else text
|
|
30
|
+
|
|
31
31
|
# Skip if not at the beginning of a line
|
|
32
|
-
if line_start and not line_start.startswith(
|
|
32
|
+
if line_start and not line_start.startswith("/"):
|
|
33
33
|
return
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
# Get the word before cursor
|
|
36
36
|
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
# Only complete if word starts with /
|
|
39
|
-
if not word_before_cursor.startswith(
|
|
39
|
+
if not word_before_cursor.startswith("/"):
|
|
40
40
|
return
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
# Get command names from registry
|
|
43
43
|
if self.command_registry:
|
|
44
44
|
command_names = self.command_registry.get_command_names()
|
|
45
45
|
else:
|
|
46
46
|
# Fallback list of commands
|
|
47
|
-
command_names = [
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
command_names = ["/help", "/clear", "/dump", "/yolo", "/branch", "/compact", "/model"]
|
|
48
|
+
|
|
50
49
|
# Get the partial command (without /)
|
|
51
50
|
partial = word_before_cursor[1:].lower()
|
|
52
|
-
|
|
51
|
+
|
|
53
52
|
# Yield completions for matching commands
|
|
54
53
|
for cmd in command_names:
|
|
55
|
-
if cmd.startswith(
|
|
54
|
+
if cmd.startswith("/") and cmd[1:].lower().startswith(partial):
|
|
56
55
|
yield Completion(
|
|
57
56
|
text=cmd,
|
|
58
57
|
start_position=-len(word_before_cursor),
|
|
59
58
|
display=cmd,
|
|
60
|
-
display_meta=
|
|
59
|
+
display_meta="command",
|
|
61
60
|
)
|
|
62
61
|
|
|
63
62
|
|
|
@@ -70,14 +69,14 @@ class FileReferenceCompleter(Completer):
|
|
|
70
69
|
"""Get completions for @file references."""
|
|
71
70
|
# Get the word before cursor
|
|
72
71
|
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
|
73
|
-
|
|
72
|
+
|
|
74
73
|
# Check if we're in an @file reference
|
|
75
74
|
if not word_before_cursor.startswith("@"):
|
|
76
75
|
return
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
# Get the path part after @
|
|
79
78
|
path_part = word_before_cursor[1:] # Remove @
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
# Determine directory and prefix
|
|
82
81
|
if "/" in path_part:
|
|
83
82
|
# Path includes directory
|
|
@@ -87,18 +86,18 @@ class FileReferenceCompleter(Completer):
|
|
|
87
86
|
# Just filename, search in current directory
|
|
88
87
|
dir_path = "."
|
|
89
88
|
prefix = path_part
|
|
90
|
-
|
|
89
|
+
|
|
91
90
|
# Get matching files
|
|
92
91
|
try:
|
|
93
92
|
if os.path.exists(dir_path) and os.path.isdir(dir_path):
|
|
94
93
|
for item in sorted(os.listdir(dir_path)):
|
|
95
94
|
if item.startswith(prefix):
|
|
96
95
|
full_path = os.path.join(dir_path, item) if dir_path != "." else item
|
|
97
|
-
|
|
96
|
+
|
|
98
97
|
# Skip hidden files unless explicitly requested
|
|
99
98
|
if item.startswith(".") and not prefix.startswith("."):
|
|
100
99
|
continue
|
|
101
|
-
|
|
100
|
+
|
|
102
101
|
# Add / for directories
|
|
103
102
|
if os.path.isdir(full_path):
|
|
104
103
|
display = item + "/"
|
|
@@ -106,15 +105,15 @@ class FileReferenceCompleter(Completer):
|
|
|
106
105
|
else:
|
|
107
106
|
display = item
|
|
108
107
|
completion = full_path
|
|
109
|
-
|
|
108
|
+
|
|
110
109
|
# Calculate how much to replace
|
|
111
110
|
start_position = -len(path_part)
|
|
112
|
-
|
|
111
|
+
|
|
113
112
|
yield Completion(
|
|
114
113
|
text=completion,
|
|
115
114
|
start_position=start_position,
|
|
116
115
|
display=display,
|
|
117
|
-
display_meta="dir" if os.path.isdir(full_path) else "file"
|
|
116
|
+
display_meta="dir" if os.path.isdir(full_path) else "file",
|
|
118
117
|
)
|
|
119
118
|
except (OSError, PermissionError):
|
|
120
119
|
# Silently ignore inaccessible directories
|
|
@@ -123,7 +122,9 @@ class FileReferenceCompleter(Completer):
|
|
|
123
122
|
|
|
124
123
|
def create_completer(command_registry: Optional[CommandRegistry] = None) -> Completer:
|
|
125
124
|
"""Create a merged completer for both commands and file references."""
|
|
126
|
-
return merge_completers(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
return merge_completers(
|
|
126
|
+
[
|
|
127
|
+
CommandCompleter(command_registry),
|
|
128
|
+
FileReferenceCompleter(),
|
|
129
|
+
]
|
|
130
|
+
)
|
tunacode/ui/console.py
CHANGED
|
@@ -11,6 +11,7 @@ from .input import formatted_text, input, multiline_input
|
|
|
11
11
|
from .keybindings import create_key_bindings
|
|
12
12
|
from .output import (banner, clear, info, line, muted, print, spinner, success, sync_print,
|
|
13
13
|
update_available, usage, version, warning)
|
|
14
|
+
# Patch banner to use sync fast version
|
|
14
15
|
from .panels import (agent, dump_messages, error, help, models, panel, sync_panel,
|
|
15
16
|
sync_tool_confirm, tool_confirm)
|
|
16
17
|
from .prompt_manager import PromptConfig, PromptManager
|
tunacode/ui/input.py
CHANGED
|
@@ -72,7 +72,9 @@ async def input(
|
|
|
72
72
|
return await manager.get_input(session_key, pretext, config)
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
async def multiline_input(
|
|
75
|
+
async def multiline_input(
|
|
76
|
+
state_manager: Optional[StateManager] = None, command_registry=None
|
|
77
|
+
) -> str:
|
|
76
78
|
"""Get multiline input from the user with @file completion and highlighting."""
|
|
77
79
|
kb = create_key_bindings()
|
|
78
80
|
placeholder = formatted_text(
|
|
@@ -85,11 +87,12 @@ async def multiline_input(state_manager: Optional[StateManager] = None, command_
|
|
|
85
87
|
)
|
|
86
88
|
)
|
|
87
89
|
return await input(
|
|
88
|
-
"multiline",
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
"multiline",
|
|
91
|
+
pretext="❯ ", # Default prompt
|
|
92
|
+
key_bindings=kb,
|
|
93
|
+
multiline=True,
|
|
91
94
|
placeholder=placeholder,
|
|
92
95
|
completer=create_completer(command_registry),
|
|
93
96
|
lexer=FileReferenceLexer(),
|
|
94
|
-
state_manager=state_manager
|
|
97
|
+
state_manager=state_manager,
|
|
95
98
|
)
|
tunacode/ui/keybindings.py
CHANGED
|
@@ -7,8 +7,6 @@ def create_key_bindings() -> KeyBindings:
|
|
|
7
7
|
"""Create and configure key bindings for the UI."""
|
|
8
8
|
kb = KeyBindings()
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
10
|
@kb.add("enter")
|
|
13
11
|
def _submit(event):
|
|
14
12
|
"""Submit the current buffer."""
|
|
@@ -18,7 +16,7 @@ def create_key_bindings() -> KeyBindings:
|
|
|
18
16
|
def _newline(event):
|
|
19
17
|
"""Insert a newline character."""
|
|
20
18
|
event.current_buffer.insert_text("\n")
|
|
21
|
-
|
|
19
|
+
|
|
22
20
|
@kb.add("escape", "enter")
|
|
23
21
|
def _escape_enter(event):
|
|
24
22
|
"""Insert a newline when escape then enter is pressed."""
|
tunacode/ui/lexers.py
CHANGED
|
@@ -8,39 +8,39 @@ from prompt_toolkit.lexers import Lexer
|
|
|
8
8
|
|
|
9
9
|
class FileReferenceLexer(Lexer):
|
|
10
10
|
"""Lexer that highlights @file references in light blue."""
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
# Pattern to match @file references
|
|
13
|
-
FILE_REF_PATTERN = re.compile(r
|
|
14
|
-
|
|
13
|
+
FILE_REF_PATTERN = re.compile(r"@([\w./_-]+)")
|
|
14
|
+
|
|
15
15
|
def lex_document(self, document):
|
|
16
16
|
"""Return a formatted text list for the given document."""
|
|
17
|
-
lines = document.text.split(
|
|
18
|
-
|
|
17
|
+
lines = document.text.split("\n")
|
|
18
|
+
|
|
19
19
|
def get_line_tokens(line_number):
|
|
20
20
|
"""Get tokens for a specific line."""
|
|
21
21
|
if line_number >= len(lines):
|
|
22
22
|
return []
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
line = lines[line_number]
|
|
25
25
|
tokens = []
|
|
26
26
|
last_end = 0
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
# Find all @file references in the line
|
|
29
29
|
for match in self.FILE_REF_PATTERN.finditer(line):
|
|
30
30
|
start, end = match.span()
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
# Add text before the match
|
|
33
33
|
if start > last_end:
|
|
34
|
-
tokens.append((
|
|
35
|
-
|
|
34
|
+
tokens.append(("", line[last_end:start]))
|
|
35
|
+
|
|
36
36
|
# Add the @file reference with styling
|
|
37
|
-
tokens.append((
|
|
37
|
+
tokens.append(("class:file-reference", match.group(0)))
|
|
38
38
|
last_end = end
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
# Add remaining text
|
|
41
41
|
if last_end < len(line):
|
|
42
|
-
tokens.append((
|
|
43
|
-
|
|
42
|
+
tokens.append(("", line[last_end:]))
|
|
43
|
+
|
|
44
44
|
return tokens
|
|
45
|
-
|
|
46
|
-
return get_line_tokens
|
|
45
|
+
|
|
46
|
+
return get_line_tokens
|
tunacode/ui/output.py
CHANGED
|
@@ -72,9 +72,9 @@ async def version() -> None:
|
|
|
72
72
|
|
|
73
73
|
async def banner() -> None:
|
|
74
74
|
"""Display the application banner."""
|
|
75
|
-
console.clear()
|
|
75
|
+
await run_in_terminal(lambda: console.clear())
|
|
76
76
|
banner_padding = Padding(BANNER, (2, 0, 1, 0))
|
|
77
|
-
await print(banner_padding)
|
|
77
|
+
await run_in_terminal(lambda: console.print(banner_padding))
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
async def clear() -> None:
|
|
@@ -89,6 +89,11 @@ async def update_available(latest_version: str) -> None:
|
|
|
89
89
|
await muted(MSG_UPDATE_INSTRUCTION)
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
async def show_update_message(latest_version: str) -> None:
|
|
93
|
+
"""Display update available message (alias for update_available)."""
|
|
94
|
+
await update_available(latest_version)
|
|
95
|
+
|
|
96
|
+
|
|
92
97
|
async def spinner(show: bool = True, spinner_obj=None, state_manager: StateManager = None):
|
|
93
98
|
"""Manage a spinner display."""
|
|
94
99
|
icon = SPINNER_TYPE
|
tunacode/ui/panels.py
CHANGED
|
@@ -11,10 +11,10 @@ from rich.table import Table
|
|
|
11
11
|
|
|
12
12
|
from tunacode.configuration.models import ModelRegistry
|
|
13
13
|
from tunacode.constants import (APP_NAME, CMD_CLEAR, CMD_COMPACT, CMD_DUMP, CMD_EXIT, CMD_HELP,
|
|
14
|
-
CMD_MODEL, CMD_YOLO, DESC_CLEAR, DESC_COMPACT, DESC_DUMP,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
CMD_MODEL, CMD_YOLO, DESC_CLEAR, DESC_COMPACT, DESC_DUMP, DESC_EXIT,
|
|
15
|
+
DESC_HELP, DESC_MODEL, DESC_MODEL_DEFAULT, DESC_MODEL_SWITCH,
|
|
16
|
+
DESC_YOLO, PANEL_AVAILABLE_COMMANDS, PANEL_ERROR,
|
|
17
|
+
PANEL_MESSAGE_HISTORY, PANEL_MODELS, UI_COLORS)
|
|
18
18
|
from tunacode.core.state import StateManager
|
|
19
19
|
from tunacode.utils.file_utils import DotDict
|
|
20
20
|
|
|
@@ -39,12 +39,12 @@ async def panel(
|
|
|
39
39
|
"""Display a rich panel with modern styling."""
|
|
40
40
|
border_style = border_style or kwargs.get("style") or colors.border
|
|
41
41
|
panel_obj = Panel(
|
|
42
|
-
Padding(text, (0, 1, 0, 1)),
|
|
43
|
-
title=f"[bold]{title}[/bold]",
|
|
44
|
-
title_align="left",
|
|
42
|
+
Padding(text, (0, 1, 0, 1)),
|
|
43
|
+
title=f"[bold]{title}[/bold]",
|
|
44
|
+
title_align="left",
|
|
45
45
|
border_style=border_style,
|
|
46
46
|
padding=(0, 1),
|
|
47
|
-
box=ROUNDED # Use ROUNDED box style
|
|
47
|
+
box=ROUNDED, # Use ROUNDED box style
|
|
48
48
|
)
|
|
49
49
|
await print(Padding(panel_obj, (top, right, bottom, left)), **kwargs)
|
|
50
50
|
|
tunacode/ui/prompt_manager.py
CHANGED
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from prompt_toolkit.completion import Completer
|
|
7
|
-
from prompt_toolkit.formatted_text import FormattedText
|
|
7
|
+
from prompt_toolkit.formatted_text import HTML, FormattedText
|
|
8
8
|
from prompt_toolkit.key_binding import KeyBindings
|
|
9
9
|
from prompt_toolkit.lexers import Lexer
|
|
10
10
|
from prompt_toolkit.shortcuts import PromptSession
|
|
@@ -42,12 +42,14 @@ class PromptManager:
|
|
|
42
42
|
self.state_manager = state_manager
|
|
43
43
|
self._temp_sessions = {} # For when no state manager is available
|
|
44
44
|
self._style = self._create_style()
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
def _create_style(self) -> Style:
|
|
47
47
|
"""Create the style for the prompt with file reference highlighting."""
|
|
48
|
-
return Style.from_dict(
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
return Style.from_dict(
|
|
49
|
+
{
|
|
50
|
+
"file-reference": UI_COLORS.get("file_ref", "light_blue"),
|
|
51
|
+
}
|
|
52
|
+
)
|
|
51
53
|
|
|
52
54
|
def get_session(self, session_key: str, config: PromptConfig) -> PromptSession:
|
|
53
55
|
"""Get or create a prompt session.
|
|
@@ -98,10 +100,20 @@ class PromptManager:
|
|
|
98
100
|
"""
|
|
99
101
|
session = self.get_session(session_key, config)
|
|
100
102
|
|
|
103
|
+
# Create a custom prompt that changes based on input
|
|
104
|
+
def get_prompt():
|
|
105
|
+
# Check if current buffer starts with "!"
|
|
106
|
+
if hasattr(session.app, "current_buffer") and session.app.current_buffer:
|
|
107
|
+
text = session.app.current_buffer.text
|
|
108
|
+
if text.startswith("!"):
|
|
109
|
+
# Use bright yellow background with black text for high visibility
|
|
110
|
+
return HTML('<style bg="#ffcc00" fg="black"><b> ◆ BASH MODE ◆ </b></style> ')
|
|
111
|
+
return HTML(prompt) if isinstance(prompt, str) else prompt
|
|
112
|
+
|
|
101
113
|
try:
|
|
102
|
-
# Get user input
|
|
114
|
+
# Get user input with dynamic prompt
|
|
103
115
|
response = await session.prompt_async(
|
|
104
|
-
|
|
116
|
+
get_prompt,
|
|
105
117
|
is_password=config.is_password,
|
|
106
118
|
validator=config.validator,
|
|
107
119
|
multiline=config.multiline,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Utility for import caching and lazy importing heavy modules."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
|
|
5
|
+
_import_cache = {}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def lazy_import(module_name: str):
|
|
9
|
+
if module_name not in _import_cache:
|
|
10
|
+
_import_cache[module_name] = importlib.import_module(module_name)
|
|
11
|
+
return _import_cache[module_name]
|
|
@@ -18,12 +18,34 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from tunacode.core.state import StateManager
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
import hashlib
|
|
22
|
+
|
|
23
|
+
_config_fingerprint = None
|
|
24
|
+
_config_cache = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compute_config_fingerprint(config_obj) -> str:
|
|
28
|
+
"""Returns a short hash/fingerprint for a config object/searchable for fastpath usage."""
|
|
29
|
+
b = json.dumps(config_obj, sort_keys=True).encode()
|
|
30
|
+
return hashlib.sha1(b).hexdigest()[:12]
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
def load_config() -> Optional[UserConfig]:
|
|
22
|
-
"""Load user config from file"""
|
|
34
|
+
"""Load user config from file, using fingerprint fast path if available."""
|
|
35
|
+
global _config_fingerprint, _config_cache
|
|
23
36
|
app_settings = ApplicationSettings()
|
|
24
37
|
try:
|
|
25
38
|
with open(app_settings.paths.config_file, "r") as f:
|
|
26
|
-
|
|
39
|
+
raw = f.read()
|
|
40
|
+
loaded = json.loads(raw)
|
|
41
|
+
new_fp = hashlib.sha1(raw.encode()).hexdigest()[:12]
|
|
42
|
+
# If hash matches, return in-memory cached config object
|
|
43
|
+
if new_fp == _config_fingerprint and _config_cache is not None:
|
|
44
|
+
return _config_cache
|
|
45
|
+
# else, update fast path
|
|
46
|
+
_config_fingerprint = new_fp
|
|
47
|
+
_config_cache = loaded
|
|
48
|
+
return loaded
|
|
27
49
|
except FileNotFoundError:
|
|
28
50
|
return None
|
|
29
51
|
except JSONDecodeError:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.18
|
|
4
4
|
Summary: Your agentic CLI developer.
|
|
5
5
|
Author-email: larock22 <noreply@github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -58,8 +58,10 @@ Dynamic: license-file
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
### Recent Updates (v0.0.
|
|
61
|
+
### Recent Updates (v0.0.15)
|
|
62
62
|
|
|
63
|
+
- **🐚 Shell Command Support**: Execute shell commands directly with `!command` or open interactive shell with `!`
|
|
64
|
+
- **🔧 Enhanced Bash Tool**: Advanced bash execution with timeouts, working directory, and environment variables
|
|
63
65
|
- **🔧 JSON Tool Parsing Fallback**: Automatic recovery when API providers fail with structured tool calling
|
|
64
66
|
- **⚡ Enhanced Reliability**: Fixed parameter naming issues that caused tool schema errors
|
|
65
67
|
- **🔄 Configuration Management**: New `/refresh` command to reload config without restart
|
|
@@ -84,6 +86,7 @@ Dynamic: license-file
|
|
|
84
86
|
### **Developer Tools**
|
|
85
87
|
|
|
86
88
|
- 6 core tools: bash, grep, read_file, write_file, update_file, run_command
|
|
89
|
+
- Direct shell command execution with `!` prefix
|
|
87
90
|
- MCP (Model Context Protocol) support
|
|
88
91
|
- File operation confirmations with diffs
|
|
89
92
|
- Per-project context guides (TUNACODE.md)
|
|
@@ -284,6 +287,8 @@ Learn more at [modelcontextprotocol.io](https://modelcontextprotocol.io/)
|
|
|
284
287
|
| `/model <provider:name> default` | Set default model |
|
|
285
288
|
| `/branch <name>` | Create and switch Git branch |
|
|
286
289
|
| `/dump` | Show message history (debug) |
|
|
290
|
+
| `!<command>` | Run shell command |
|
|
291
|
+
| `!` | Open interactive shell |
|
|
287
292
|
| `exit` | Exit application |
|
|
288
293
|
|
|
289
294
|
### Debug & Recovery Commands
|
|
@@ -298,6 +303,42 @@ Learn more at [modelcontextprotocol.io](https://modelcontextprotocol.io/)
|
|
|
298
303
|
|
|
299
304
|
---
|
|
300
305
|
|
|
306
|
+
## Available Tools
|
|
307
|
+
|
|
308
|
+
### Bash Tool
|
|
309
|
+
The enhanced bash tool provides advanced shell command execution with safety features:
|
|
310
|
+
|
|
311
|
+
- **Working Directory Support**: Execute commands in specific directories
|
|
312
|
+
- **Environment Variables**: Set custom environment variables for commands
|
|
313
|
+
- **Timeout Control**: Configurable timeouts (1-300 seconds) to prevent hanging
|
|
314
|
+
- **Output Capture**: Full stdout/stderr capture with truncation for large outputs
|
|
315
|
+
- **Safety Checks**: Warns about potentially destructive commands
|
|
316
|
+
- **Error Guidance**: Helpful error messages for common issues (command not found, permission denied, etc.)
|
|
317
|
+
|
|
318
|
+
**Example usage by the AI:**
|
|
319
|
+
```python
|
|
320
|
+
# Simple command
|
|
321
|
+
await bash("ls -la")
|
|
322
|
+
|
|
323
|
+
# With working directory
|
|
324
|
+
await bash("npm install", cwd="/path/to/project")
|
|
325
|
+
|
|
326
|
+
# With timeout for long operations
|
|
327
|
+
await bash("npm run build", timeout=120)
|
|
328
|
+
|
|
329
|
+
# With environment variables
|
|
330
|
+
await bash("python script.py", env={"API_KEY": "secret"})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Other Core Tools
|
|
334
|
+
- **grep**: Fast parallel content search across files
|
|
335
|
+
- **read_file**: Read file contents with line numbers
|
|
336
|
+
- **write_file**: Create new files (fails if file exists)
|
|
337
|
+
- **update_file**: Modify existing files with precise replacements
|
|
338
|
+
- **run_command**: Basic command execution (simpler than bash)
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
301
342
|
## Reliability Features
|
|
302
343
|
|
|
303
344
|
### JSON Tool Parsing Fallback
|
|
@@ -475,3 +516,16 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
475
516
|
## Acknowledgments
|
|
476
517
|
|
|
477
518
|
TunaCode is a fork of [sidekick-cli](https://github.com/geekforbrains/sidekick-cli). Special thanks to the sidekick-cli team for creating the foundation that made TunaCode possible.
|
|
519
|
+
|
|
520
|
+
### Key Differences from sidekick-cli
|
|
521
|
+
|
|
522
|
+
While TunaCode builds on the foundation of sidekick-cli, we've made several architectural changes for our workflow:
|
|
523
|
+
|
|
524
|
+
- **JSON Tool Parsing Fallback**: Added fallback parsing for when API providers fail with structured tool calling
|
|
525
|
+
- **Parallel Search Tools**: New `bash` and `grep` tools with parallel execution for codebase navigation
|
|
526
|
+
- **ReAct Reasoning**: Implemented ReAct (Reasoning + Acting) patterns with configurable iteration limits
|
|
527
|
+
- **Dynamic Configuration**: Added `/refresh` command and modified configuration management
|
|
528
|
+
- **Safety Changes**: Removed automatic git commits and `/undo` command - requires explicit git usage
|
|
529
|
+
- **Error Recovery**: Multiple fallback mechanisms and orphaned tool call recovery
|
|
530
|
+
- **Tool System Rewrite**: Complete overhaul of internal tools with atomic operations and different confirmation UIs
|
|
531
|
+
- **Debug Commands**: Added `/parsetools`, `/thoughts`, `/iterations` for debugging
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tunacode/constants.py,sha256=yUpXwyNffwbcdiQMNrX58wieycOu8UjDx1AzBVXYVRE,3799
|
|
3
|
+
tunacode/context.py,sha256=9FQ2vf9qY4bcRufKtR0g0eOzB8hZ4aesOfFGy_JOMzQ,2634
|
|
4
|
+
tunacode/exceptions.py,sha256=_Dyj6cC0868dMABekdQHXCg5XhucJumbGUMXaSDzgB4,2645
|
|
5
|
+
tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
tunacode/setup.py,sha256=dYn0NeAxtNIDSogWEmGSyjb9wsr8AonZ8vAo5sw9NIw,1909
|
|
7
|
+
tunacode/types.py,sha256=5mMJDgFqVcKzhtHh9unPISBFqkeNje6KISGUpRkqRjY,7146
|
|
8
|
+
tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
|
|
9
|
+
tunacode/cli/commands.py,sha256=tTsEKXZqFx-xdTn3DqKsrz_gNC6_5Z4Bq_biXrfIBc0,29513
|
|
10
|
+
tunacode/cli/main.py,sha256=C3hBF9RGADz8wtdhKngwM-IjMeLA8MhttYnXiGb-L9g,1870
|
|
11
|
+
tunacode/cli/repl.py,sha256=RqI8AnFuKsdhvgI_zKaRBiIi50xEt1hoHoe6Bd3OSlI,11941
|
|
12
|
+
tunacode/cli/textual_app.py,sha256=1GNyYacMqCgIgYl1G2B5sIoksIAzKiGuPJ-wxHhzu5I,12951
|
|
13
|
+
tunacode/cli/textual_bridge.py,sha256=DcIbIbYBW4rDKMTWDqAKgWv7PtxZYBKGUcYo7uLSzuM,6175
|
|
14
|
+
tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
|
|
15
|
+
tunacode/configuration/defaults.py,sha256=7UdHP50NmnZWlx0f1PGV4JZIpmQem191vWisGky6AVA,688
|
|
16
|
+
tunacode/configuration/models.py,sha256=XPobkLM_TzKTuMIWhK-svJfGRGFT9r2LhKEM6rv6QHk,3756
|
|
17
|
+
tunacode/configuration/settings.py,sha256=lm2ov8rG1t4C5JIXMOhIKik5FAsjpuLVYtFmnE1ZQ3k,995
|
|
18
|
+
tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
tunacode/core/state.py,sha256=n1claG-gVVDMBCCS8cDmgas4XbKLJJwKRc-8CtXeTS8,1376
|
|
20
|
+
tunacode/core/tool_handler.py,sha256=OKx7jM8pml6pSEnoARu33_uBY8awJBqvhbVeBn6T4ow,1804
|
|
21
|
+
tunacode/core/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
tunacode/core/agents/main.py,sha256=m9HYOQn-vo2WMdZcltuHHQevLnEEf8f-w5j2jDm8skw,14224
|
|
23
|
+
tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
|
|
24
|
+
tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
|
|
25
|
+
tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
|
|
26
|
+
tunacode/core/setup/config_setup.py,sha256=BZPQjJSVsIF3bIP2TteRf8NsQCnVBnx1JdxzriNjWB4,13266
|
|
27
|
+
tunacode/core/setup/coordinator.py,sha256=oVTN2xIeJERXitVJpkIk9tDGLs1D1bxIRmaogJwZJFI,2049
|
|
28
|
+
tunacode/core/setup/environment_setup.py,sha256=n3IrObKEynHZSwtUJ1FddMg2C4sHz7ca42awemImV8s,2225
|
|
29
|
+
tunacode/core/setup/git_safety_setup.py,sha256=T7hwIf3u3Tq3QtIdUAfuHI6vclMfm2Sqcml5l6x02oA,6799
|
|
30
|
+
tunacode/prompts/system.txt,sha256=wZkP3vTv4h8ukBUkCZ9yR-oZt-ZcdvIRPxl1uN3AHIk,4030
|
|
31
|
+
tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
|
|
32
|
+
tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
|
|
33
|
+
tunacode/tools/__init__.py,sha256=oDcxl7uoi1s9TPz2WNsjHH7xTKL6jM78WlrGbh-9SYQ,318
|
|
34
|
+
tunacode/tools/base.py,sha256=TF71ZE66-W-7GLY8QcPpPJ5CVjod6FHL1caBOTCssvU,7044
|
|
35
|
+
tunacode/tools/bash.py,sha256=mgZqugfDFevZ4BETuUv90pzXvtq7qKGUGFiuDxzmytk,8766
|
|
36
|
+
tunacode/tools/grep.py,sha256=USJayOfFP8l2WYFSqFgeMlpeWrJREJIWpv64fL_fQHI,26502
|
|
37
|
+
tunacode/tools/read_file.py,sha256=Cz1-7sdQwOdaqkVvkVpORiBdNtncCVlP9e9cu37ya80,2988
|
|
38
|
+
tunacode/tools/run_command.py,sha256=kYg_Re397OmZdKtUSjpNfYYNDBjd0vsS1xMK0yP181I,3776
|
|
39
|
+
tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
|
|
40
|
+
tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
|
|
41
|
+
tunacode/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
|
|
42
|
+
tunacode/ui/completers.py,sha256=ORkMqX_L1ChdLqCDUlUr3drBnnxec3Jj7LyE54Mnod0,4837
|
|
43
|
+
tunacode/ui/console.py,sha256=LTEWQLdRP0JkhbEPIHIDrSX3ZZld9iJI2aZ3-eGliOc,1930
|
|
44
|
+
tunacode/ui/constants.py,sha256=A76B_KpM8jCuBYRg4cPmhi8_j6LLyWttO7_jjv47r3w,421
|
|
45
|
+
tunacode/ui/decorators.py,sha256=e2KM-_pI5EKHa2M045IjUe4rPkTboxaKHXJT0K3461g,1914
|
|
46
|
+
tunacode/ui/input.py,sha256=RXoqN8ZsGcsTJc04-3orDHP3BiOTKZ0dEx9tf-xwhUA,3008
|
|
47
|
+
tunacode/ui/keybindings.py,sha256=h0MlD73CW_3i2dQzb9EFSPkqy0raZ_isgjxUiA9u6ts,691
|
|
48
|
+
tunacode/ui/lexers.py,sha256=xb-KkgIqu2Xh1zqiuH4zFfPVHV8pPeKuuRBrhb3zjf4,1396
|
|
49
|
+
tunacode/ui/output.py,sha256=7fIo_WjRtkXrhm9VJd6OqaWEZ_XVqI0hxU8J1XaeLw4,4501
|
|
50
|
+
tunacode/ui/panels.py,sha256=_8B1rGOhPnSLekGM4ZzDJw-dCuaPeacsaCmmCggqxwE,5950
|
|
51
|
+
tunacode/ui/prompt_manager.py,sha256=U2cntB34vm-YwOj3gzFRUK362zccrz8pigQfpxr5sv8,4650
|
|
52
|
+
tunacode/ui/tool_ui.py,sha256=S5-k1HwRlSqiQ8shGQ_QYGXQbuzb6Pg7u3CTqZwffdQ,6533
|
|
53
|
+
tunacode/ui/validators.py,sha256=MMIMT1I2v0l2jIy-gxX_4GSApvUTi8XWIOACr_dmoBA,758
|
|
54
|
+
tunacode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
+
tunacode/utils/bm25.py,sha256=yq7KFWP3g_zIsjUV7l2hFPXYCzXyNQUInLU7u4qsc_4,1909
|
|
56
|
+
tunacode/utils/diff_utils.py,sha256=V9QqQ0q4MfabVTnWptF3IXDp3estnfOKcJtDe_Sj14I,2372
|
|
57
|
+
tunacode/utils/file_utils.py,sha256=AXiAJ_idtlmXEi9pMvwtfPy9Ys3yK-F4K7qb_NpwonU,923
|
|
58
|
+
tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDgs,295
|
|
59
|
+
tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
|
|
60
|
+
tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
|
|
61
|
+
tunacode/utils/text_utils.py,sha256=B9M1cuLTm_SSsr15WNHF6j7WdLWPvWzKZV0Lvfgdbjg,2458
|
|
62
|
+
tunacode/utils/user_configuration.py,sha256=IGvUH37wWtZ4M5xpukZEWYhtuKKyKcl6DaeObGXdleU,2610
|
|
63
|
+
tunacode_cli-0.0.18.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
64
|
+
tunacode_cli-0.0.18.dist-info/METADATA,sha256=gSvYPIonpAUDFNbHkUEEgD7QwwadVFgBEhNTuF2kE9o,17273
|
|
65
|
+
tunacode_cli-0.0.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
66
|
+
tunacode_cli-0.0.18.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
|
|
67
|
+
tunacode_cli-0.0.18.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
|
|
68
|
+
tunacode_cli-0.0.18.dist-info/RECORD,,
|