tunacode-cli 0.0.17__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 +3 -3
- tunacode/ui/input.py +8 -5
- tunacode/ui/keybindings.py +1 -3
- tunacode/ui/lexers.py +16 -16
- tunacode/ui/output.py +2 -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.17.dist-info → tunacode_cli-0.0.18.dist-info}/METADATA +43 -2
- tunacode_cli-0.0.18.dist-info/RECORD +68 -0
- tunacode_cli-0.0.17.dist-info/RECORD +0 -67
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.18.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.18.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.18.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.18.dist-info}/top_level.txt +0 -0
tunacode/cli/commands.py
CHANGED
|
@@ -6,7 +6,6 @@ from enum import Enum
|
|
|
6
6
|
from typing import Any, Dict, List, Optional, Type
|
|
7
7
|
|
|
8
8
|
from .. import utils
|
|
9
|
-
from ..configuration.models import ModelRegistry
|
|
10
9
|
from ..exceptions import ValidationError
|
|
11
10
|
from ..types import CommandArgs, CommandContext, CommandResult, ProcessRequestCallback
|
|
12
11
|
from ..ui import console as ui
|
|
@@ -116,9 +115,9 @@ class YoloCommand(SimpleCommand):
|
|
|
116
115
|
state = context.state_manager.session
|
|
117
116
|
state.yolo = not state.yolo
|
|
118
117
|
if state.yolo:
|
|
119
|
-
await ui.success("
|
|
118
|
+
await ui.success("All tools are now active ⚡ Please proceed with caution.\n")
|
|
120
119
|
else:
|
|
121
|
-
await ui.info("
|
|
120
|
+
await ui.info("Tool confirmations re-enabled for safety.\n")
|
|
122
121
|
|
|
123
122
|
|
|
124
123
|
class DumpCommand(SimpleCommand):
|
|
@@ -189,12 +188,12 @@ class IterationsCommand(SimpleCommand):
|
|
|
189
188
|
if new_limit < 1 or new_limit > 50:
|
|
190
189
|
await ui.error("Iterations must be between 1 and 50")
|
|
191
190
|
return
|
|
192
|
-
|
|
191
|
+
|
|
193
192
|
# Update the user config
|
|
194
193
|
if "settings" not in state.user_config:
|
|
195
194
|
state.user_config["settings"] = {}
|
|
196
195
|
state.user_config["settings"]["max_iterations"] = new_limit
|
|
197
|
-
|
|
196
|
+
|
|
198
197
|
await ui.success(f"Maximum iterations set to {new_limit}")
|
|
199
198
|
await ui.muted("Higher values allow more complex reasoning but may be slower")
|
|
200
199
|
except ValueError:
|
|
@@ -221,8 +220,9 @@ class ClearCommand(SimpleCommand):
|
|
|
221
220
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
222
221
|
# Patch any orphaned tool calls before clearing
|
|
223
222
|
from tunacode.core.agents.main import patch_tool_messages
|
|
223
|
+
|
|
224
224
|
patch_tool_messages("Conversation cleared", context.state_manager)
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
await ui.clear()
|
|
227
227
|
context.state_manager.session.messages = []
|
|
228
228
|
await ui.success("Message history cleared")
|
|
@@ -243,17 +243,17 @@ class FixCommand(SimpleCommand):
|
|
|
243
243
|
|
|
244
244
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
245
245
|
from tunacode.core.agents.main import patch_tool_messages
|
|
246
|
-
|
|
246
|
+
|
|
247
247
|
# Count current messages
|
|
248
248
|
before_count = len(context.state_manager.session.messages)
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
# Patch orphaned tool calls
|
|
251
251
|
patch_tool_messages("Tool call resolved by /fix command", context.state_manager)
|
|
252
|
-
|
|
252
|
+
|
|
253
253
|
# Count after patching
|
|
254
254
|
after_count = len(context.state_manager.session.messages)
|
|
255
255
|
patched_count = after_count - before_count
|
|
256
|
-
|
|
256
|
+
|
|
257
257
|
if patched_count > 0:
|
|
258
258
|
await ui.success(f"Fixed {patched_count} orphaned tool call(s)")
|
|
259
259
|
await ui.muted("You can now continue the conversation normally")
|
|
@@ -269,36 +269,37 @@ class ParseToolsCommand(SimpleCommand):
|
|
|
269
269
|
CommandSpec(
|
|
270
270
|
name="parsetools",
|
|
271
271
|
aliases=["/parsetools"],
|
|
272
|
-
description=
|
|
272
|
+
description=(
|
|
273
|
+
"Parse JSON tool calls from last response when structured calling fails"
|
|
274
|
+
),
|
|
273
275
|
category=CommandCategory.DEBUG,
|
|
274
276
|
)
|
|
275
277
|
)
|
|
276
278
|
|
|
277
279
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
278
280
|
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
279
|
-
|
|
281
|
+
|
|
280
282
|
# Find the last model response in messages
|
|
281
283
|
messages = context.state_manager.session.messages
|
|
282
284
|
if not messages:
|
|
283
285
|
await ui.error("No message history found")
|
|
284
286
|
return
|
|
285
|
-
|
|
287
|
+
|
|
286
288
|
# Look for the most recent response with text content
|
|
287
289
|
found_content = False
|
|
288
290
|
for msg in reversed(messages):
|
|
289
|
-
if hasattr(msg,
|
|
291
|
+
if hasattr(msg, "parts"):
|
|
290
292
|
for part in msg.parts:
|
|
291
|
-
if hasattr(part,
|
|
293
|
+
if hasattr(part, "content") and isinstance(part.content, str):
|
|
292
294
|
# Create tool callback
|
|
293
295
|
from tunacode.cli.repl import _tool_handler
|
|
296
|
+
|
|
294
297
|
def tool_callback_with_state(part, node):
|
|
295
298
|
return _tool_handler(part, node, context.state_manager)
|
|
296
|
-
|
|
299
|
+
|
|
297
300
|
try:
|
|
298
301
|
await extract_and_execute_tool_calls(
|
|
299
|
-
part.content,
|
|
300
|
-
tool_callback_with_state,
|
|
301
|
-
context.state_manager
|
|
302
|
+
part.content, tool_callback_with_state, context.state_manager
|
|
302
303
|
)
|
|
303
304
|
await ui.success("JSON tool parsing completed")
|
|
304
305
|
found_content = True
|
|
@@ -306,7 +307,7 @@ class ParseToolsCommand(SimpleCommand):
|
|
|
306
307
|
except Exception as e:
|
|
307
308
|
await ui.error(f"Failed to parse tools: {str(e)}")
|
|
308
309
|
return
|
|
309
|
-
|
|
310
|
+
|
|
310
311
|
if not found_content:
|
|
311
312
|
await ui.error("No parseable content found in recent messages")
|
|
312
313
|
|
|
@@ -326,7 +327,7 @@ class RefreshConfigCommand(SimpleCommand):
|
|
|
326
327
|
|
|
327
328
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
328
329
|
from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
329
|
-
|
|
330
|
+
|
|
330
331
|
# Update current session config with latest defaults
|
|
331
332
|
for key, value in DEFAULT_USER_CONFIG.items():
|
|
332
333
|
if key not in context.state_manager.session.user_config:
|
|
@@ -336,9 +337,11 @@ class RefreshConfigCommand(SimpleCommand):
|
|
|
336
337
|
for subkey, subvalue in value.items():
|
|
337
338
|
if subkey not in context.state_manager.session.user_config[key]:
|
|
338
339
|
context.state_manager.session.user_config[key][subkey] = subvalue
|
|
339
|
-
|
|
340
|
+
|
|
340
341
|
# Show updated max_iterations
|
|
341
|
-
max_iterations = context.state_manager.session.user_config.get("settings", {}).get(
|
|
342
|
+
max_iterations = context.state_manager.session.user_config.get("settings", {}).get(
|
|
343
|
+
"max_iterations", 20
|
|
344
|
+
)
|
|
342
345
|
await ui.success(f"Configuration refreshed - max iterations: {max_iterations}")
|
|
343
346
|
|
|
344
347
|
|
|
@@ -507,23 +510,20 @@ class UpdateCommand(SimpleCommand):
|
|
|
507
510
|
)
|
|
508
511
|
|
|
509
512
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
513
|
+
import shutil
|
|
510
514
|
import subprocess
|
|
511
515
|
import sys
|
|
512
|
-
import shutil
|
|
513
516
|
|
|
514
517
|
await ui.info("Checking for TunaCode updates...")
|
|
515
518
|
|
|
516
519
|
# Detect installation method
|
|
517
520
|
installation_method = None
|
|
518
|
-
|
|
521
|
+
|
|
519
522
|
# Check if installed via pipx
|
|
520
523
|
if shutil.which("pipx"):
|
|
521
524
|
try:
|
|
522
525
|
result = subprocess.run(
|
|
523
|
-
["pipx", "list"],
|
|
524
|
-
capture_output=True,
|
|
525
|
-
text=True,
|
|
526
|
-
timeout=10
|
|
526
|
+
["pipx", "list"], capture_output=True, text=True, timeout=10
|
|
527
527
|
)
|
|
528
528
|
if "tunacode" in result.stdout.lower():
|
|
529
529
|
installation_method = "pipx"
|
|
@@ -534,10 +534,10 @@ class UpdateCommand(SimpleCommand):
|
|
|
534
534
|
if not installation_method:
|
|
535
535
|
try:
|
|
536
536
|
result = subprocess.run(
|
|
537
|
-
[sys.executable, "-m", "pip", "show", "tunacode-cli"],
|
|
538
|
-
capture_output=True,
|
|
539
|
-
text=True,
|
|
540
|
-
timeout=10
|
|
537
|
+
[sys.executable, "-m", "pip", "show", "tunacode-cli"],
|
|
538
|
+
capture_output=True,
|
|
539
|
+
text=True,
|
|
540
|
+
timeout=10,
|
|
541
541
|
)
|
|
542
542
|
if result.returncode == 0:
|
|
543
543
|
installation_method = "pip"
|
|
@@ -556,10 +556,7 @@ class UpdateCommand(SimpleCommand):
|
|
|
556
556
|
if installation_method == "pipx":
|
|
557
557
|
await ui.info("Updating via pipx...")
|
|
558
558
|
result = subprocess.run(
|
|
559
|
-
["pipx", "upgrade", "tunacode"],
|
|
560
|
-
capture_output=True,
|
|
561
|
-
text=True,
|
|
562
|
-
timeout=60
|
|
559
|
+
["pipx", "upgrade", "tunacode"], capture_output=True, text=True, timeout=60
|
|
563
560
|
)
|
|
564
561
|
else: # pip
|
|
565
562
|
await ui.info("Updating via pip...")
|
|
@@ -567,16 +564,16 @@ class UpdateCommand(SimpleCommand):
|
|
|
567
564
|
[sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
|
|
568
565
|
capture_output=True,
|
|
569
566
|
text=True,
|
|
570
|
-
timeout=60
|
|
567
|
+
timeout=60,
|
|
571
568
|
)
|
|
572
569
|
|
|
573
570
|
if result.returncode == 0:
|
|
574
571
|
await ui.success("TunaCode updated successfully!")
|
|
575
572
|
await ui.muted("Restart TunaCode to use the new version")
|
|
576
|
-
|
|
573
|
+
|
|
577
574
|
# Show update output if available
|
|
578
575
|
if result.stdout.strip():
|
|
579
|
-
output_lines = result.stdout.strip().split(
|
|
576
|
+
output_lines = result.stdout.strip().split("\n")
|
|
580
577
|
for line in output_lines[-5:]: # Show last 5 lines
|
|
581
578
|
if line.strip():
|
|
582
579
|
await ui.muted(f" {line}")
|
|
@@ -798,8 +795,9 @@ class CommandRegistry:
|
|
|
798
795
|
return await command.execute(args, context)
|
|
799
796
|
else:
|
|
800
797
|
# Ambiguous - show possibilities
|
|
798
|
+
matches_str = ", ".join(sorted(set(matches)))
|
|
801
799
|
raise ValidationError(
|
|
802
|
-
f"Ambiguous command '{command_name}'. Did you mean: {
|
|
800
|
+
f"Ambiguous command '{command_name}'. Did you mean: {matches_str}?"
|
|
803
801
|
)
|
|
804
802
|
|
|
805
803
|
def find_matching_commands(self, partial_command: str) -> List[str]:
|
tunacode/cli/main.py
CHANGED
|
@@ -24,36 +24,39 @@ state_manager = StateManager()
|
|
|
24
24
|
def main(
|
|
25
25
|
version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
|
|
26
26
|
run_setup: bool = typer.Option(False, "--setup", help="Run setup process."),
|
|
27
|
-
baseurl: str = typer.Option(
|
|
27
|
+
baseurl: str = typer.Option(
|
|
28
|
+
None, "--baseurl", help="API base URL (e.g., https://openrouter.ai/api/v1)"
|
|
29
|
+
),
|
|
28
30
|
model: str = typer.Option(None, "--model", help="Default model to use (e.g., openai/gpt-4)"),
|
|
29
31
|
key: str = typer.Option(None, "--key", help="API key for the provider"),
|
|
30
32
|
):
|
|
31
33
|
"""🚀 Start TunaCode - Your AI-powered development assistant"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
asyncio.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
|
|
35
|
+
async def async_main():
|
|
36
|
+
if version:
|
|
37
|
+
await ui.version()
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
await ui.banner()
|
|
41
|
+
|
|
42
|
+
# Start update check in background
|
|
43
|
+
update_task = asyncio.to_thread(check_for_updates)
|
|
44
|
+
|
|
45
|
+
cli_config = {}
|
|
46
|
+
if baseurl or model or key:
|
|
47
|
+
cli_config = {"baseurl": baseurl, "model": model, "key": key}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
await setup(run_setup, state_manager, cli_config)
|
|
51
|
+
await repl(state_manager)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
await ui.error(str(e))
|
|
54
|
+
|
|
55
|
+
has_update, latest_version = await update_task
|
|
56
|
+
if has_update:
|
|
57
|
+
await ui.update_available(latest_version)
|
|
58
|
+
|
|
59
|
+
asyncio.run(async_main())
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
if __name__ == "__main__":
|
tunacode/cli/repl.py
CHANGED
|
@@ -6,6 +6,8 @@ Handles user input, command processing, and agent interaction in an interactive
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
9
11
|
from asyncio.exceptions import CancelledError
|
|
10
12
|
|
|
11
13
|
from prompt_toolkit.application import run_in_terminal
|
|
@@ -169,7 +171,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
169
171
|
|
|
170
172
|
# Patch any orphaned tool calls from previous requests before proceeding
|
|
171
173
|
patch_tool_messages("Tool execution was interrupted", state_manager)
|
|
172
|
-
|
|
174
|
+
|
|
173
175
|
# Create a partial function that includes state_manager
|
|
174
176
|
def tool_callback_with_state(part, node):
|
|
175
177
|
return _tool_handler(part, node, state_manager)
|
|
@@ -188,7 +190,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
188
190
|
if isinstance(msg, dict) and "thought" in msg:
|
|
189
191
|
await ui.muted(f"THOUGHT: {msg['thought']}")
|
|
190
192
|
# Check if result exists and has output
|
|
191
|
-
if hasattr(res,
|
|
193
|
+
if hasattr(res, "result") and res.result is not None and hasattr(res.result, "output"):
|
|
192
194
|
await ui.agent(res.result.output)
|
|
193
195
|
else:
|
|
194
196
|
# Fallback: show that the request was processed
|
|
@@ -204,29 +206,28 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
204
206
|
except Exception as e:
|
|
205
207
|
# Check if this might be a tool calling failure that we can recover from
|
|
206
208
|
error_str = str(e).lower()
|
|
207
|
-
if any(keyword in error_str for keyword in [
|
|
209
|
+
if any(keyword in error_str for keyword in ["tool", "function", "call", "schema"]):
|
|
208
210
|
# Try to extract and execute tool calls from the last response
|
|
209
211
|
if state_manager.session.messages:
|
|
210
212
|
last_msg = state_manager.session.messages[-1]
|
|
211
|
-
if hasattr(last_msg,
|
|
213
|
+
if hasattr(last_msg, "parts"):
|
|
212
214
|
for part in last_msg.parts:
|
|
213
|
-
if hasattr(part,
|
|
215
|
+
if hasattr(part, "content") and isinstance(part.content, str):
|
|
214
216
|
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
217
|
+
|
|
215
218
|
try:
|
|
216
219
|
# Create a partial function that includes state_manager
|
|
217
220
|
def tool_callback_with_state(part, node):
|
|
218
221
|
return _tool_handler(part, node, state_manager)
|
|
219
|
-
|
|
222
|
+
|
|
220
223
|
await extract_and_execute_tool_calls(
|
|
221
|
-
part.content,
|
|
222
|
-
tool_callback_with_state,
|
|
223
|
-
state_manager
|
|
224
|
+
part.content, tool_callback_with_state, state_manager
|
|
224
225
|
)
|
|
225
226
|
await ui.warning("🔧 Recovered using JSON tool parsing")
|
|
226
227
|
return # Successfully recovered
|
|
227
228
|
except Exception:
|
|
228
229
|
pass # Fallback failed, continue with normal error handling
|
|
229
|
-
|
|
230
|
+
|
|
230
231
|
# Wrap unexpected exceptions in AgentError for better tracking
|
|
231
232
|
agent_error = AgentError(f"Agent processing failed: {str(e)}")
|
|
232
233
|
agent_error.__cause__ = e # Preserve the original exception chain
|
|
@@ -271,6 +272,30 @@ async def repl(state_manager: StateManager):
|
|
|
271
272
|
break
|
|
272
273
|
continue
|
|
273
274
|
|
|
275
|
+
if line.startswith("!"):
|
|
276
|
+
command = line[1:].strip()
|
|
277
|
+
|
|
278
|
+
# Show tool-style header for bash commands
|
|
279
|
+
cmd_display = command if command else "Interactive shell"
|
|
280
|
+
await ui.panel("Tool(bash)", f"Command: {cmd_display}", border_style="yellow")
|
|
281
|
+
|
|
282
|
+
def run_shell():
|
|
283
|
+
try:
|
|
284
|
+
if command:
|
|
285
|
+
result = subprocess.run(command, shell=True, capture_output=False)
|
|
286
|
+
if result.returncode != 0:
|
|
287
|
+
# Use print directly since we're in a terminal context
|
|
288
|
+
print(f"\nCommand exited with code {result.returncode}")
|
|
289
|
+
else:
|
|
290
|
+
shell = os.environ.get("SHELL", "bash")
|
|
291
|
+
subprocess.run(shell)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(f"\nShell command failed: {str(e)}")
|
|
294
|
+
|
|
295
|
+
await run_in_terminal(run_shell)
|
|
296
|
+
await ui.line()
|
|
297
|
+
continue
|
|
298
|
+
|
|
274
299
|
# Check if another task is already running
|
|
275
300
|
if state_manager.session.current_task and not state_manager.session.current_task.done():
|
|
276
301
|
await ui.muted("Agent is busy, press Ctrl+C to interrupt.")
|