gitarsenal-cli 1.9.28 → 1.9.30

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.
package/.venv_status.json CHANGED
@@ -1 +1 @@
1
- {"created":"2025-08-08T04:37:52.316Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-08-09T13:46:26.609Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.28",
3
+ "version": "1.9.30",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,17 +16,24 @@ except ImportError:
16
16
  class CommandListManager:
17
17
  """Manages a dynamic list of setup commands with status tracking and LLM-suggested fixes."""
18
18
 
19
- def __init__(self, initial_commands=None):
19
+ def __init__(self, initial_commands=None, auto_optimize_on_add=False):
20
20
  self.commands = []
21
21
  self.executed_commands = []
22
22
  self.failed_commands = []
23
23
  self.suggested_fixes = []
24
24
  self.current_index = 0
25
25
  self.total_commands = 0
26
+ self.auto_optimize_on_add = auto_optimize_on_add
26
27
 
27
28
  if initial_commands:
28
29
  self.add_commands(initial_commands)
29
30
 
31
+ def enable_auto_llm_optimization(self, enabled=True):
32
+ """Enable or disable automatic LLM optimization after command additions."""
33
+ self.auto_optimize_on_add = bool(enabled)
34
+ state = "enabled" if self.auto_optimize_on_add else "disabled"
35
+ print(f"🤖 Auto LLM optimization {state}")
36
+
30
37
  def add_commands(self, commands):
31
38
  """Add new commands to the list."""
32
39
  if isinstance(commands, str):
@@ -50,6 +57,12 @@ class CommandListManager:
50
57
  self.total_commands = len(self.commands)
51
58
  if added_count > 0:
52
59
  print(f"📋 Added {added_count} commands to list. Total: {self.total_commands}")
60
+ if self.auto_optimize_on_add:
61
+ try:
62
+ print("🤖 Optimizing command list with LLM after addition...")
63
+ self.update_command_list_with_llm()
64
+ except Exception as e:
65
+ print(f"⚠️ Auto-optimization failed: {e}")
53
66
 
54
67
  def add_command_dynamically(self, command, priority='normal'):
55
68
  """Add a single command dynamically during execution."""
@@ -80,6 +93,12 @@ class CommandListManager:
80
93
 
81
94
  self.total_commands = len(self.commands)
82
95
  print(f"📋 Added dynamic command: {command.strip()}")
96
+ if self.auto_optimize_on_add:
97
+ try:
98
+ print("🤖 Optimizing command list with LLM after dynamic addition...")
99
+ self.update_command_list_with_llm()
100
+ except Exception as e:
101
+ print(f"⚠️ Auto-optimization failed: {e}")
83
102
  return True
84
103
 
85
104
  def add_suggested_fix(self, original_command, fix_command, reason=""):
@@ -96,6 +115,12 @@ class CommandListManager:
96
115
  }
97
116
  self.suggested_fixes.append(fix_entry)
98
117
  print(f"🔧 Added suggested fix: {fix_command}")
118
+ if self.auto_optimize_on_add:
119
+ try:
120
+ print("🤖 Optimizing command list with LLM after suggested fix addition...")
121
+ self.update_command_list_with_llm()
122
+ except Exception as e:
123
+ print(f"⚠️ Auto-optimization failed: {e}")
99
124
  return len(self.suggested_fixes) - 1
100
125
 
101
126
  def get_next_command(self):
@@ -143,6 +168,31 @@ class CommandListManager:
143
168
 
144
169
  if success:
145
170
  print(f"✅ Fix command {command_index + 1} completed successfully")
171
+ # After a successful fix, decide whether to skip or modify the original command
172
+ try:
173
+ original_command = self.suggested_fixes[command_index].get('original_command', '')
174
+ fix_command = self.suggested_fixes[command_index].get('fix_command', '')
175
+ if original_command and fix_command:
176
+ should_skip, reason = self.should_skip_original_command(
177
+ original_command, fix_command, stdout, stderr
178
+ )
179
+ if should_skip:
180
+ # Find the original command in the main list and mark it as completed (skipped)
181
+ for i, cmd in enumerate(self.commands):
182
+ if cmd.get('command') == original_command and cmd.get('status') in ('pending', 'failed'):
183
+ self.mark_command_executed(
184
+ i, 'main', True,
185
+ f"Command skipped due to successful fix: {reason}",
186
+ '', 0
187
+ )
188
+ break
189
+ else:
190
+ # If we should not skip, try to optimize the list (may MODIFY or ADD_AFTER)
191
+ if getattr(self, 'auto_optimize_on_add', False):
192
+ print("🤖 Optimizing command list with LLM after fix success...")
193
+ self.update_command_list_with_llm()
194
+ except Exception as e:
195
+ print(f"⚠️ Post-fix optimization error: {e}")
146
196
  else:
147
197
  print(f"❌ Fix command {command_index + 1} failed")
148
198
 
@@ -297,21 +347,18 @@ class CommandListManager:
297
347
  """
298
348
  try:
299
349
  # Import required helpers once for this function scope
300
- from llm_debugging import get_current_debug_model, get_api_key, make_api_request
301
-
302
- # Get API key if not provided
303
- if not api_key:
304
- # Use the same API key retrieval logic as the debugging functions
305
- current_model = get_current_debug_model()
306
- api_key = get_api_key(current_model)
307
-
308
- if not api_key:
309
- print(f"⚠️ No {current_model} API key available for command list analysis")
310
- return False, "No API key available"
350
+ from llm_debugging import (
351
+ get_current_debug_model,
352
+ get_api_key,
353
+ make_api_request,
354
+ get_provider_rotation_order,
355
+ )
311
356
 
312
357
  # Get all commands for context
313
358
  all_commands = self.get_all_commands()
314
- commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}" for i, cmd in enumerate(all_commands)])
359
+ def _cmd_text(c):
360
+ return c.get('command') or c.get('fix_command') or 'UNKNOWN'
361
+ commands_context = "\n".join([f"{i+1}. {_cmd_text(cmd)} - {cmd.get('status', '')}" for i, cmd in enumerate(all_commands)])
315
362
 
316
363
  # Prepare the prompt
317
364
  prompt = f"""
@@ -341,34 +388,44 @@ class CommandListManager:
341
388
  RUN: <reason>
342
389
  """
343
390
 
344
- current_model = get_current_debug_model()
345
-
346
- print(f"🔍 Analyzing if original command should be skipped using {current_model}...")
347
-
348
- response_text = make_api_request(current_model, api_key, prompt)
349
-
350
- if not response_text:
351
- print(f"⚠️ Failed to get response from {current_model}")
352
- return False, f"Failed to get response from {current_model}"
353
-
354
- # Parse the response
355
- if response_text.startswith("SKIP:"):
356
- reason = response_text.replace("SKIP:", "").strip()
357
- print(f"🔍 LLM suggests skipping original command: {reason}")
358
- return True, reason
359
- elif response_text.startswith("RUN:"):
360
- reason = response_text.replace("RUN:", "").strip()
361
- print(f"🔍 LLM suggests running original command: {reason}")
362
- return False, reason
363
- else:
364
- # Try to interpret a free-form response
365
- if "skip" in response_text.lower() and "should" in response_text.lower():
366
- print(f"🔍 Interpreting response as SKIP: {response_text}")
367
- return True, response_text
391
+ preferred = get_current_debug_model()
392
+ providers = get_provider_rotation_order(preferred)
393
+
394
+ for provider in providers:
395
+ # Use provided api_key only for the preferred provider; otherwise fetch per provider
396
+ provider_key = api_key if (api_key and provider == preferred) else get_api_key(provider)
397
+ if not provider_key:
398
+ print(f"⚠️ No {provider} API key available for skip analysis. Trying next provider...")
399
+ continue
400
+
401
+ print(f"🔍 Analyzing if original command should be skipped using {provider}...")
402
+ response_text = make_api_request(provider, provider_key, prompt)
403
+ if not response_text:
404
+ print(f"⚠️ Failed to get response from {provider}. Trying next provider...")
405
+ continue
406
+
407
+ # Parse the response
408
+ if response_text.startswith("SKIP:"):
409
+ reason = response_text.replace("SKIP:", "").strip()
410
+ print(f"🔍 LLM suggests skipping original command: {reason}")
411
+ return True, reason
412
+ elif response_text.startswith("RUN:"):
413
+ reason = response_text.replace("RUN:", "").strip()
414
+ print(f"🔍 LLM suggests running original command: {reason}")
415
+ return False, reason
368
416
  else:
369
- print(f"🔍 Interpreting response as RUN: {response_text}")
370
- return False, response_text
371
-
417
+ # Try to interpret a free-form response
418
+ if "skip" in response_text.lower() and "should" in response_text.lower():
419
+ print(f"🔍 Interpreting response as SKIP: {response_text}")
420
+ return True, response_text
421
+ else:
422
+ print(f"🔍 Interpreting response as RUN: {response_text}")
423
+ return False, response_text
424
+
425
+ # If all providers failed
426
+ print("❌ All providers failed to analyze skip decision.")
427
+ return False, "No provider returned a response"
428
+
372
429
  except Exception as e:
373
430
  print(f"⚠️ Error analyzing command skip decision: {e}")
374
431
  return False, f"Error: {e}"
@@ -412,23 +469,23 @@ class CommandListManager:
412
469
  bool: True if the list was updated, False otherwise
413
470
  """
414
471
  try:
415
- from llm_debugging import get_current_debug_model, get_api_key, make_api_request
472
+ from llm_debugging import (
473
+ get_current_debug_model,
474
+ get_api_key,
475
+ make_api_request,
476
+ get_provider_rotation_order,
477
+ )
416
478
  # Get API key if not provided
417
- if not api_key:
418
- # Use the same API key retrieval logic as the debugging functions
419
- from llm_debugging import get_current_debug_model, get_api_key
420
- current_model = get_current_debug_model()
421
- api_key = get_api_key(current_model)
422
-
423
- if not api_key:
424
- print(f"⚠️ No {current_model} API key available for command list analysis")
425
- return False
479
+ preferred = get_current_debug_model()
480
+ providers = get_provider_rotation_order(preferred)
426
481
 
427
482
  # Get all commands for context
428
483
  all_commands = self.get_all_commands()
429
- commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}"
484
+ def _cmd_text(c):
485
+ return c.get('command') or c.get('fix_command') or 'UNKNOWN'
486
+ commands_context = "\n".join([f"{i+1}. {_cmd_text(cmd)} - {cmd.get('status', '')}"
430
487
  for i, cmd in enumerate(all_commands)])
431
-
488
+
432
489
  # Get executed commands with their outputs for context
433
490
  executed_context = ""
434
491
  for cmd in self.executed_commands:
@@ -476,16 +533,19 @@ class CommandListManager:
476
533
  """
477
534
 
478
535
  # Use the unified LLM API call
479
- from llm_debugging import make_api_request
480
536
  import json
481
- current_model = get_current_debug_model()
482
-
483
- print(f"🔍 Analyzing command list for optimizations using {current_model}...")
484
-
485
- response_text = make_api_request(current_model, api_key, prompt)
486
-
537
+ response_text = None
538
+ for provider in providers:
539
+ provider_key = api_key if (api_key and provider == preferred) else get_api_key(provider)
540
+ if not provider_key:
541
+ print(f"⚠️ No {provider} API key available for command list analysis. Trying next provider...")
542
+ continue
543
+ print(f"🔍 Analyzing command list for optimizations using {provider}...")
544
+ response_text = make_api_request(provider, provider_key, prompt)
545
+ if response_text:
546
+ break
487
547
  if not response_text:
488
- print(f"⚠️ Failed to get response from {current_model}")
548
+ print("⚠️ Failed to get response from all providers for command list optimization")
489
549
  return False
490
550
 
491
551
  # Extract JSON from the response
@@ -35,7 +35,7 @@ def generate_auth_context(stored_credentials):
35
35
 
36
36
  def get_current_debug_model():
37
37
  """Get the currently configured debugging model preference"""
38
- return os.environ.get("GITARSENAL_DEBUG_MODEL", "openai")
38
+ return os.environ.get("GITARSENAL_DEBUG_MODEL", "anthropic")
39
39
 
40
40
 
41
41
  def _to_str(maybe_bytes):
@@ -534,8 +534,16 @@ def make_groq_request(api_key, prompt, retries=2):
534
534
  return None
535
535
 
536
536
 
537
+ def get_provider_rotation_order(preferred=None):
538
+ """Return provider rotation order starting with preferred if valid."""
539
+ default_order = ["anthropic", "openai", "groq", "openrouter"]
540
+ if preferred and preferred in default_order:
541
+ return [preferred] + [p for p in default_order if p != preferred]
542
+ return default_order
543
+
544
+
537
545
  def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None, use_web_search=False):
538
- """Unified function to call LLM for debugging"""
546
+ """Unified function to call LLM for debugging with provider rotation"""
539
547
  # Skip debugging for test commands
540
548
  if command.strip().startswith("test "):
541
549
  return None
@@ -545,65 +553,45 @@ def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sa
545
553
  print("⚠️ Error output is empty. Cannot debug effectively.")
546
554
  return None
547
555
 
548
- current_model = get_current_debug_model()
549
- print(f"🔍 Using {current_model.upper()} for debugging...")
550
-
551
- # Get API key
552
- if not api_key:
553
- api_key = get_api_key(current_model)
554
-
555
- if not api_key:
556
- print(f"❌ No {current_model} API key available. Cannot perform LLM debugging.")
557
- return None
558
-
559
- # Save API key for future use
560
- save_api_key(current_model, api_key)
561
-
562
- # Gather context
556
+ # Gather context once
563
557
  system_info, directory_context, file_context = gather_context(sandbox, current_dir)
564
-
565
- # Get credentials context
566
558
  stored_credentials = get_stored_credentials()
567
559
  auth_context = generate_auth_context(stored_credentials)
568
-
569
- # Create prompt
570
560
  prompt = create_debug_prompt(command, error_output, system_info, directory_context, file_context, auth_context)
571
-
572
- print(f"\n{'='*60}")
573
- print("DEBUG: ERROR_OUTPUT SENT TO LLM:")
574
- print(f"{'='*60}")
575
- print(f"{error_output}")
576
- print(f"{'='*60}\n")
577
-
578
- # Make API request
579
- print(f"🤖 Calling {current_model} to debug the failed command...")
580
- response_text = make_api_request(current_model, api_key, prompt)
581
-
582
- if not response_text:
583
- return None
584
-
585
- # Extract command from response
586
- fix_command = extract_command_from_response(response_text)
587
-
588
- print(f"🔧 Suggested fix: {fix_command}")
589
- return fix_command
561
+
562
+ # Determine rotation order
563
+ preferred = get_current_debug_model()
564
+ providers = get_provider_rotation_order(preferred)
565
+
566
+ # Try providers in order
567
+ for provider in providers:
568
+ print(f"🔍 Using {provider.upper()} for debugging...")
569
+ this_api_key = api_key if api_key and provider == preferred else get_api_key(provider)
570
+ if not this_api_key:
571
+ print(f"❌ No {provider} API key available. Skipping.")
572
+ continue
573
+
574
+ # Save key for reuse
575
+ save_api_key(provider, this_api_key)
576
+
577
+ # Make API request via unified adapter
578
+ response_text = make_api_request(provider, this_api_key, prompt)
579
+ if response_text:
580
+ fix_command = extract_command_from_response(response_text)
581
+ print(f"🔧 Suggested fix ({provider}): {fix_command}")
582
+ return fix_command
583
+ else:
584
+ print(f"⚠️ {provider} did not return a valid response. Trying next provider...")
585
+
586
+ print("❌ All providers failed to produce a fix command.")
587
+ return None
590
588
 
591
589
 
592
590
  def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None, use_web_search=False):
593
- """Call LLM for batch debugging of multiple failed commands"""
591
+ """Call LLM for batch debugging of multiple failed commands with provider rotation"""
594
592
  if not failed_commands:
595
593
  return []
596
594
 
597
- current_model = get_current_debug_model()
598
-
599
- # Get API key
600
- if not api_key:
601
- api_key = get_api_key(current_model)
602
-
603
- if not api_key:
604
- print(f"❌ No {current_model} API key available for batch debugging")
605
- return []
606
-
607
595
  # Prepare context for batch analysis
608
596
  context_parts = [f"Current directory: {current_dir}", f"Sandbox available: {sandbox is not None}"]
609
597
 
@@ -623,7 +611,7 @@ def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sa
623
611
  if stdout:
624
612
  context_parts.append(f"Standard Output: {stdout}")
625
613
 
626
- # Create batch prompt
614
+ # Create batch prompt once
627
615
  prompt = f"""You are a debugging assistant analyzing multiple failed commands.
628
616
 
629
617
  Context:
@@ -643,38 +631,54 @@ Guidelines:
643
631
 
644
632
  Provide fixes for all {len(failed_commands)} failed commands:"""
645
633
 
646
- print(f"🤖 Calling {current_model} for batch debugging of {len(failed_commands)} commands...")
647
- response_text = make_api_request(current_model, api_key, prompt)
648
-
649
- if not response_text:
650
- return []
651
-
652
- # Parse the response to extract fix commands
653
- fixes = []
654
- for i in range(1, len(failed_commands) + 1):
655
- fix_pattern = f"FIX_COMMAND_{i}: (.+)"
656
- reason_pattern = f"REASON_{i}: (.+)"
634
+ # Determine rotation order
635
+ preferred = get_current_debug_model()
636
+ providers = get_provider_rotation_order(preferred)
637
+
638
+ # Try providers in order
639
+ for provider in providers:
640
+ print(f"🤖 Calling {provider.upper()} for batch debugging of {len(failed_commands)} commands...")
641
+ this_api_key = api_key if api_key and provider == preferred else get_api_key(provider)
642
+ if not this_api_key:
643
+ print(f"❌ No {provider} API key available for batch debugging. Skipping.")
644
+ continue
645
+
646
+ save_api_key(provider, this_api_key)
647
+ response_text = make_api_request(provider, this_api_key, prompt)
657
648
 
658
- fix_match = re.search(fix_pattern, response_text, re.MULTILINE)
659
- reason_match = re.search(reason_pattern, response_text, re.MULTILINE)
649
+ if not response_text:
650
+ print(f"⚠️ {provider} returned no response. Trying next provider...")
651
+ continue
660
652
 
661
- if fix_match:
662
- fix_command = fix_match.group(1).strip()
663
- reason = reason_match.group(1).strip() if reason_match else "LLM suggested fix"
653
+ # Parse the response to extract fix commands
654
+ fixes = []
655
+ for i in range(1, len(failed_commands) + 1):
656
+ fix_pattern = f"FIX_COMMAND_{i}: (.+)"
657
+ reason_pattern = f"REASON_{i}: (.+)"
664
658
 
665
- # Clean up the fix command
666
- if fix_command.startswith('`') and fix_command.endswith('`'):
667
- fix_command = fix_command[1:-1]
659
+ fix_match = re.search(fix_pattern, response_text, re.MULTILINE)
660
+ reason_match = re.search(reason_pattern, response_text, re.MULTILINE)
668
661
 
669
- fixes.append({
670
- 'original_command': failed_commands[i-1]['command'],
671
- 'fix_command': fix_command,
672
- 'reason': reason,
673
- 'command_index': i-1
674
- })
675
-
676
- print(f"🔧 Generated {len(fixes)} fix commands from batch analysis")
677
- return fixes
662
+ if fix_match:
663
+ fix_command = fix_match.group(1).strip()
664
+ reason = reason_match.group(1).strip() if reason_match else "LLM suggested fix"
665
+
666
+ # Clean up the fix command
667
+ if fix_command.startswith('`') and fix_command.endswith('`'):
668
+ fix_command = fix_command[1:-1]
669
+
670
+ fixes.append({
671
+ 'original_command': failed_commands[i-1]['command'],
672
+ 'fix_command': fix_command,
673
+ 'reason': reason,
674
+ 'command_index': i-1
675
+ })
676
+
677
+ print(f"🔧 Generated {len(fixes)} fix commands from batch analysis using {provider}")
678
+ return fixes
679
+
680
+ print("❌ All providers failed to produce batch fixes.")
681
+ return []
678
682
 
679
683
 
680
684
  # Legacy function aliases for backward compatibility