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.

Files changed (38) hide show
  1. tunacode/cli/commands.py +39 -41
  2. tunacode/cli/main.py +29 -26
  3. tunacode/cli/repl.py +35 -10
  4. tunacode/cli/textual_app.py +69 -66
  5. tunacode/cli/textual_bridge.py +33 -32
  6. tunacode/configuration/settings.py +2 -9
  7. tunacode/constants.py +2 -4
  8. tunacode/context.py +1 -1
  9. tunacode/core/agents/main.py +88 -62
  10. tunacode/core/setup/config_setup.py +79 -44
  11. tunacode/core/setup/coordinator.py +20 -13
  12. tunacode/core/setup/git_safety_setup.py +35 -49
  13. tunacode/core/state.py +2 -9
  14. tunacode/exceptions.py +0 -2
  15. tunacode/tools/__init__.py +10 -1
  16. tunacode/tools/base.py +1 -1
  17. tunacode/tools/bash.py +5 -5
  18. tunacode/tools/grep.py +210 -250
  19. tunacode/tools/read_file.py +2 -8
  20. tunacode/tools/run_command.py +4 -11
  21. tunacode/tools/update_file.py +2 -6
  22. tunacode/ui/completers.py +32 -31
  23. tunacode/ui/console.py +1 -0
  24. tunacode/ui/input.py +8 -5
  25. tunacode/ui/keybindings.py +1 -3
  26. tunacode/ui/lexers.py +16 -16
  27. tunacode/ui/output.py +7 -2
  28. tunacode/ui/panels.py +8 -8
  29. tunacode/ui/prompt_manager.py +19 -7
  30. tunacode/utils/import_cache.py +11 -0
  31. tunacode/utils/user_configuration.py +24 -2
  32. {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/METADATA +56 -2
  33. tunacode_cli-0.0.18.dist-info/RECORD +68 -0
  34. tunacode_cli-0.0.16.dist-info/RECORD +0 -67
  35. {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/WHEEL +0 -0
  36. {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/entry_points.txt +0 -0
  37. {tunacode_cli-0.0.16.dist-info → tunacode_cli-0.0.18.dist-info}/licenses/LICENSE +0 -0
  38. {tunacode_cli-0.0.16.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("Ooh shit, its YOLO time!\n")
118
+ await ui.success("All tools are now active ⚡ Please proceed with caution.\n")
120
119
  else:
121
- await ui.info("Pfft, boring...\n")
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="Parse JSON tool calls from last response when structured calling fails",
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, 'parts'):
291
+ if hasattr(msg, "parts"):
290
292
  for part in msg.parts:
291
- if hasattr(part, 'content') and isinstance(part.content, str):
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("max_iterations", 20)
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('\n')
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: {', '.join(sorted(set(matches)))}?"
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(None, "--baseurl", help="API base URL (e.g., https://openrouter.ai/api/v1)"),
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
- if version:
34
- asyncio.run(ui.version())
35
- return
36
-
37
- asyncio.run(ui.banner())
38
-
39
- has_update, latest_version = check_for_updates()
40
- if has_update:
41
- asyncio.run(ui.show_update_message(latest_version))
42
-
43
- # Pass CLI args to setup
44
- cli_config = {}
45
- if baseurl or model or key:
46
- cli_config = {
47
- "baseurl": baseurl,
48
- "model": model,
49
- "key": key
50
- }
51
-
52
- try:
53
- asyncio.run(setup(run_setup, state_manager, cli_config))
54
- asyncio.run(repl(state_manager))
55
- except Exception as e:
56
- asyncio.run(ui.error(str(e)))
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, 'result') and res.result is not None and hasattr(res.result, 'output'):
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 ['tool', 'function', 'call', 'schema']):
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, 'parts'):
213
+ if hasattr(last_msg, "parts"):
212
214
  for part in last_msg.parts:
213
- if hasattr(part, 'content') and isinstance(part.content, str):
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.")