tunacode-cli 0.0.13__py3-none-any.whl → 0.0.15__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 CHANGED
@@ -138,6 +138,73 @@ class DumpCommand(SimpleCommand):
138
138
  await ui.dump_messages(context.state_manager.session.messages)
139
139
 
140
140
 
141
+ class ThoughtsCommand(SimpleCommand):
142
+ """Toggle display of agent thoughts."""
143
+
144
+ def __init__(self):
145
+ super().__init__(
146
+ CommandSpec(
147
+ name="thoughts",
148
+ aliases=["/thoughts"],
149
+ description="Show or hide agent thought messages",
150
+ category=CommandCategory.DEBUG,
151
+ )
152
+ )
153
+
154
+ async def execute(self, args: List[str], context: CommandContext) -> None:
155
+ state = context.state_manager.session
156
+ if args:
157
+ arg = args[0].lower()
158
+ if arg in {"on", "1", "true"}:
159
+ state.show_thoughts = True
160
+ elif arg in {"off", "0", "false"}:
161
+ state.show_thoughts = False
162
+ else:
163
+ await ui.error("Usage: /thoughts [on|off]")
164
+ return
165
+ else:
166
+ state.show_thoughts = not state.show_thoughts
167
+ status = "ON" if state.show_thoughts else "OFF"
168
+ await ui.success(f"Thought display {status}")
169
+
170
+
171
+ class IterationsCommand(SimpleCommand):
172
+ """Configure maximum agent iterations for ReAct reasoning."""
173
+
174
+ def __init__(self):
175
+ super().__init__(
176
+ CommandSpec(
177
+ name="iterations",
178
+ aliases=["/iterations"],
179
+ description="Set maximum agent iterations for complex reasoning",
180
+ category=CommandCategory.DEBUG,
181
+ )
182
+ )
183
+
184
+ async def execute(self, args: List[str], context: CommandContext) -> None:
185
+ state = context.state_manager.session
186
+ if args:
187
+ try:
188
+ new_limit = int(args[0])
189
+ if new_limit < 1 or new_limit > 50:
190
+ await ui.error("Iterations must be between 1 and 50")
191
+ return
192
+
193
+ # Update the user config
194
+ if "settings" not in state.user_config:
195
+ state.user_config["settings"] = {}
196
+ state.user_config["settings"]["max_iterations"] = new_limit
197
+
198
+ await ui.success(f"Maximum iterations set to {new_limit}")
199
+ await ui.muted("Higher values allow more complex reasoning but may be slower")
200
+ except ValueError:
201
+ await ui.error("Please provide a valid number")
202
+ else:
203
+ current = state.user_config.get("settings", {}).get("max_iterations", 15)
204
+ await ui.info(f"Current maximum iterations: {current}")
205
+ await ui.muted("Usage: /iterations <number> (1-50)")
206
+
207
+
141
208
  class ClearCommand(SimpleCommand):
142
209
  """Clear screen and message history."""
143
210
 
@@ -152,8 +219,127 @@ class ClearCommand(SimpleCommand):
152
219
  )
153
220
 
154
221
  async def execute(self, args: List[str], context: CommandContext) -> None:
222
+ # Patch any orphaned tool calls before clearing
223
+ from tunacode.core.agents.main import patch_tool_messages
224
+ patch_tool_messages("Conversation cleared", context.state_manager)
225
+
155
226
  await ui.clear()
156
227
  context.state_manager.session.messages = []
228
+ await ui.success("Message history cleared")
229
+
230
+
231
+ class FixCommand(SimpleCommand):
232
+ """Fix orphaned tool calls that cause API errors."""
233
+
234
+ def __init__(self):
235
+ super().__init__(
236
+ CommandSpec(
237
+ name="fix",
238
+ aliases=["/fix"],
239
+ description="Fix orphaned tool calls causing API errors",
240
+ category=CommandCategory.DEBUG,
241
+ )
242
+ )
243
+
244
+ async def execute(self, args: List[str], context: CommandContext) -> None:
245
+ from tunacode.core.agents.main import patch_tool_messages
246
+
247
+ # Count current messages
248
+ before_count = len(context.state_manager.session.messages)
249
+
250
+ # Patch orphaned tool calls
251
+ patch_tool_messages("Tool call resolved by /fix command", context.state_manager)
252
+
253
+ # Count after patching
254
+ after_count = len(context.state_manager.session.messages)
255
+ patched_count = after_count - before_count
256
+
257
+ if patched_count > 0:
258
+ await ui.success(f"Fixed {patched_count} orphaned tool call(s)")
259
+ await ui.muted("You can now continue the conversation normally")
260
+ else:
261
+ await ui.info("No orphaned tool calls found")
262
+
263
+
264
+ class ParseToolsCommand(SimpleCommand):
265
+ """Parse and execute JSON tool calls from the last response."""
266
+
267
+ def __init__(self):
268
+ super().__init__(
269
+ CommandSpec(
270
+ name="parsetools",
271
+ aliases=["/parsetools"],
272
+ description="Parse JSON tool calls from last response when structured calling fails",
273
+ category=CommandCategory.DEBUG,
274
+ )
275
+ )
276
+
277
+ async def execute(self, args: List[str], context: CommandContext) -> None:
278
+ from tunacode.core.agents.main import extract_and_execute_tool_calls
279
+
280
+ # Find the last model response in messages
281
+ messages = context.state_manager.session.messages
282
+ if not messages:
283
+ await ui.error("No message history found")
284
+ return
285
+
286
+ # Look for the most recent response with text content
287
+ found_content = False
288
+ for msg in reversed(messages):
289
+ if hasattr(msg, 'parts'):
290
+ for part in msg.parts:
291
+ if hasattr(part, 'content') and isinstance(part.content, str):
292
+ # Create tool callback
293
+ from tunacode.cli.repl import _tool_handler
294
+ def tool_callback_with_state(part, node):
295
+ return _tool_handler(part, node, context.state_manager)
296
+
297
+ try:
298
+ await extract_and_execute_tool_calls(
299
+ part.content,
300
+ tool_callback_with_state,
301
+ context.state_manager
302
+ )
303
+ await ui.success("JSON tool parsing completed")
304
+ found_content = True
305
+ return
306
+ except Exception as e:
307
+ await ui.error(f"Failed to parse tools: {str(e)}")
308
+ return
309
+
310
+ if not found_content:
311
+ await ui.error("No parseable content found in recent messages")
312
+
313
+
314
+ class RefreshConfigCommand(SimpleCommand):
315
+ """Refresh configuration from defaults."""
316
+
317
+ def __init__(self):
318
+ super().__init__(
319
+ CommandSpec(
320
+ name="refresh",
321
+ aliases=["/refresh"],
322
+ description="Refresh configuration from defaults (useful after updates)",
323
+ category=CommandCategory.SYSTEM,
324
+ )
325
+ )
326
+
327
+ async def execute(self, args: List[str], context: CommandContext) -> None:
328
+ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
329
+
330
+ # Update current session config with latest defaults
331
+ for key, value in DEFAULT_USER_CONFIG.items():
332
+ if key not in context.state_manager.session.user_config:
333
+ context.state_manager.session.user_config[key] = value
334
+ elif isinstance(value, dict):
335
+ # Merge dict values, preserving user overrides
336
+ for subkey, subvalue in value.items():
337
+ if subkey not in context.state_manager.session.user_config[key]:
338
+ context.state_manager.session.user_config[key][subkey] = subvalue
339
+
340
+ # Show updated max_iterations
341
+ max_iterations = context.state_manager.session.user_config.get("settings", {}).get("max_iterations", 20)
342
+ await ui.success(f"Configuration refreshed - max iterations: {max_iterations}")
157
343
 
158
344
 
159
345
  class TunaCodeCommand(SimpleCommand):
@@ -232,7 +418,6 @@ class HelpCommand(SimpleCommand):
232
418
  await ui.help(self._command_registry)
233
419
 
234
420
 
235
-
236
421
  class BranchCommand(SimpleCommand):
237
422
  """Create and switch to a new git branch."""
238
423
 
@@ -247,8 +432,8 @@ class BranchCommand(SimpleCommand):
247
432
  )
248
433
 
249
434
  async def execute(self, args: List[str], context: CommandContext) -> None:
250
- import subprocess
251
435
  import os
436
+ import subprocess
252
437
 
253
438
  if not args:
254
439
  await ui.error("Usage: /branch <branch-name>")
@@ -308,6 +493,106 @@ class CompactCommand(SimpleCommand):
308
493
  context.state_manager.session.messages = context.state_manager.session.messages[-2:]
309
494
 
310
495
 
496
+ class UpdateCommand(SimpleCommand):
497
+ """Update TunaCode to the latest version."""
498
+
499
+ def __init__(self):
500
+ super().__init__(
501
+ CommandSpec(
502
+ name="update",
503
+ aliases=["/update"],
504
+ description="Update TunaCode to the latest version",
505
+ category=CommandCategory.SYSTEM,
506
+ )
507
+ )
508
+
509
+ async def execute(self, args: List[str], context: CommandContext) -> None:
510
+ import subprocess
511
+ import sys
512
+ import shutil
513
+
514
+ await ui.info("Checking for TunaCode updates...")
515
+
516
+ # Detect installation method
517
+ installation_method = None
518
+
519
+ # Check if installed via pipx
520
+ if shutil.which("pipx"):
521
+ try:
522
+ result = subprocess.run(
523
+ ["pipx", "list"],
524
+ capture_output=True,
525
+ text=True,
526
+ timeout=10
527
+ )
528
+ if "tunacode" in result.stdout.lower():
529
+ installation_method = "pipx"
530
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
531
+ pass
532
+
533
+ # Check if installed via pip
534
+ if not installation_method:
535
+ try:
536
+ result = subprocess.run(
537
+ [sys.executable, "-m", "pip", "show", "tunacode-cli"],
538
+ capture_output=True,
539
+ text=True,
540
+ timeout=10
541
+ )
542
+ if result.returncode == 0:
543
+ installation_method = "pip"
544
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
545
+ pass
546
+
547
+ if not installation_method:
548
+ await ui.error("Could not detect TunaCode installation method")
549
+ await ui.muted("Manual update options:")
550
+ await ui.muted(" pipx: pipx upgrade tunacode")
551
+ await ui.muted(" pip: pip install --upgrade tunacode-cli")
552
+ return
553
+
554
+ # Perform update based on detected method
555
+ try:
556
+ if installation_method == "pipx":
557
+ await ui.info("Updating via pipx...")
558
+ result = subprocess.run(
559
+ ["pipx", "upgrade", "tunacode"],
560
+ capture_output=True,
561
+ text=True,
562
+ timeout=60
563
+ )
564
+ else: # pip
565
+ await ui.info("Updating via pip...")
566
+ result = subprocess.run(
567
+ [sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
568
+ capture_output=True,
569
+ text=True,
570
+ timeout=60
571
+ )
572
+
573
+ if result.returncode == 0:
574
+ await ui.success("TunaCode updated successfully!")
575
+ await ui.muted("Restart TunaCode to use the new version")
576
+
577
+ # Show update output if available
578
+ if result.stdout.strip():
579
+ output_lines = result.stdout.strip().split('\n')
580
+ for line in output_lines[-5:]: # Show last 5 lines
581
+ if line.strip():
582
+ await ui.muted(f" {line}")
583
+ else:
584
+ await ui.error("Update failed")
585
+ if result.stderr:
586
+ await ui.muted(f"Error: {result.stderr.strip()}")
587
+
588
+ except subprocess.TimeoutExpired:
589
+ await ui.error("Update timed out")
590
+ except subprocess.CalledProcessError as e:
591
+ await ui.error(f"Update failed: {e}")
592
+ except FileNotFoundError:
593
+ await ui.error(f"Could not find {installation_method} executable")
594
+
595
+
311
596
  class ModelCommand(SimpleCommand):
312
597
  """Manage model selection."""
313
598
 
@@ -332,14 +617,16 @@ class ModelCommand(SimpleCommand):
332
617
 
333
618
  # Get the model name from args
334
619
  model_name = args[0]
335
-
620
+
336
621
  # Check if provider prefix is present
337
622
  if ":" not in model_name:
338
623
  await ui.error("Model name must include provider prefix")
339
624
  await ui.muted("Format: provider:model-name")
340
- await ui.muted("Examples: openai:gpt-4.1, anthropic:claude-3-opus, google-gla:gemini-2.0-flash")
625
+ await ui.muted(
626
+ "Examples: openai:gpt-4.1, anthropic:claude-3-opus, google-gla:gemini-2.0-flash"
627
+ )
341
628
  return None
342
-
629
+
343
630
  # No validation - user is responsible for correct model names
344
631
  await ui.warning("Model set without validation - verify the model name is correct")
345
632
 
@@ -416,8 +703,7 @@ class CommandRegistry:
416
703
  category_commands = self._categories[command.category]
417
704
  # Remove any existing instance of this command class
418
705
  self._categories[command.category] = [
419
- cmd for cmd in category_commands
420
- if cmd.__class__ != command.__class__
706
+ cmd for cmd in category_commands if cmd.__class__ != command.__class__
421
707
  ]
422
708
  # Add the new instance
423
709
  self._categories[command.category].append(command)
@@ -436,7 +722,13 @@ class CommandRegistry:
436
722
  command_classes = [
437
723
  YoloCommand,
438
724
  DumpCommand,
725
+ ThoughtsCommand,
726
+ IterationsCommand,
439
727
  ClearCommand,
728
+ FixCommand,
729
+ ParseToolsCommand,
730
+ RefreshConfigCommand,
731
+ UpdateCommand,
440
732
  HelpCommand,
441
733
  BranchCommand,
442
734
  # TunaCodeCommand, # TODO: Temporarily disabled
@@ -459,7 +751,7 @@ class CommandRegistry:
459
751
  # Only update if callback has changed
460
752
  if self._factory.dependencies.process_request_callback == callback:
461
753
  return
462
-
754
+
463
755
  self._factory.update_dependencies(process_request_callback=callback)
464
756
 
465
757
  # Re-register CompactCommand with new dependency if already registered
@@ -494,10 +786,10 @@ class CommandRegistry:
494
786
  if command_name in self._commands:
495
787
  command = self._commands[command_name]
496
788
  return await command.execute(args, context)
497
-
789
+
498
790
  # Try partial matching
499
791
  matches = self.find_matching_commands(command_name)
500
-
792
+
501
793
  if not matches:
502
794
  raise ValidationError(f"Unknown command: {command_name}")
503
795
  elif len(matches) == 1:
@@ -513,10 +805,10 @@ class CommandRegistry:
513
805
  def find_matching_commands(self, partial_command: str) -> List[str]:
514
806
  """
515
807
  Find all commands that start with the given partial command.
516
-
808
+
517
809
  Args:
518
810
  partial_command: The partial command to match
519
-
811
+
520
812
  Returns:
521
813
  List of matching command names
522
814
  """
@@ -534,11 +826,11 @@ class CommandRegistry:
534
826
  return False
535
827
 
536
828
  command_name = parts[0].lower()
537
-
829
+
538
830
  # Check exact match first
539
831
  if command_name in self._commands:
540
832
  return True
541
-
833
+
542
834
  # Check partial match
543
835
  return len(self.find_matching_commands(command_name)) > 0
544
836
 
tunacode/cli/repl.py CHANGED
@@ -167,10 +167,14 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
167
167
  await ui.error(str(e))
168
168
  return
169
169
 
170
+ # Patch any orphaned tool calls from previous requests before proceeding
171
+ patch_tool_messages("Tool execution was interrupted", state_manager)
172
+
170
173
  # Create a partial function that includes state_manager
171
174
  def tool_callback_with_state(part, node):
172
175
  return _tool_handler(part, node, state_manager)
173
176
 
177
+ start_idx = len(state_manager.session.messages)
174
178
  res = await agent.process_request(
175
179
  state_manager.session.current_model,
176
180
  text,
@@ -178,7 +182,17 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
178
182
  tool_callback=tool_callback_with_state,
179
183
  )
180
184
  if output:
181
- await ui.agent(res.result.output)
185
+ if state_manager.session.show_thoughts:
186
+ new_msgs = state_manager.session.messages[start_idx:]
187
+ for msg in new_msgs:
188
+ if isinstance(msg, dict) and "thought" in msg:
189
+ await ui.muted(f"THOUGHT: {msg['thought']}")
190
+ # Check if result exists and has output
191
+ if hasattr(res, 'result') and res.result is not None and hasattr(res.result, 'output'):
192
+ await ui.agent(res.result.output)
193
+ else:
194
+ # Fallback: show that the request was processed
195
+ await ui.muted("Request completed")
182
196
  except CancelledError:
183
197
  await ui.muted("Request cancelled")
184
198
  except UserAbortError:
@@ -188,6 +202,31 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
188
202
  await ui.muted(error_message)
189
203
  patch_tool_messages(error_message, state_manager)
190
204
  except Exception as e:
205
+ # Check if this might be a tool calling failure that we can recover from
206
+ error_str = str(e).lower()
207
+ if any(keyword in error_str for keyword in ['tool', 'function', 'call', 'schema']):
208
+ # Try to extract and execute tool calls from the last response
209
+ if state_manager.session.messages:
210
+ last_msg = state_manager.session.messages[-1]
211
+ if hasattr(last_msg, 'parts'):
212
+ for part in last_msg.parts:
213
+ if hasattr(part, 'content') and isinstance(part.content, str):
214
+ from tunacode.core.agents.main import extract_and_execute_tool_calls
215
+ try:
216
+ # Create a partial function that includes state_manager
217
+ def tool_callback_with_state(part, node):
218
+ return _tool_handler(part, node, state_manager)
219
+
220
+ await extract_and_execute_tool_calls(
221
+ part.content,
222
+ tool_callback_with_state,
223
+ state_manager
224
+ )
225
+ await ui.warning("🔧 Recovered using JSON tool parsing")
226
+ return # Successfully recovered
227
+ except Exception:
228
+ pass # Fallback failed, continue with normal error handling
229
+
191
230
  # Wrap unexpected exceptions in AgentError for better tracking
192
231
  agent_error = AgentError(f"Agent processing failed: {str(e)}")
193
232
  agent_error.__cause__ = e # Preserve the original exception chain
@@ -210,7 +249,7 @@ async def repl(state_manager: StateManager):
210
249
  await ui.muted(f"• Model: {state_manager.session.current_model}")
211
250
  await ui.success("Ready to assist with your development")
212
251
  await ui.line()
213
-
252
+
214
253
  instance = agent.get_or_create_agent(state_manager.session.current_model, state_manager)
215
254
 
216
255
  async with instance.run_mcp_servers():
@@ -18,6 +18,7 @@ DEFAULT_USER_CONFIG: UserConfig = {
18
18
  },
19
19
  "settings": {
20
20
  "max_retries": 10,
21
+ "max_iterations": 20,
21
22
  "tool_ignore": [TOOL_READ_FILE],
22
23
  "guide_file": GUIDE_FILE_NAME,
23
24
  },
tunacode/constants.py CHANGED
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.13"
10
+ APP_VERSION = "0.0.15"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"