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 +1 -1
- package/package.json +1 -1
- package/python/command_manager.py +120 -60
- package/python/llm_debugging.py +84 -80
- package/python/test_modalSandboxScript.py +25 -103
- package/python/api_integration.py +0 -0
- package/python/modal_container.py +0 -722
package/.venv_status.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"created":"2025-08-
|
|
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
|
@@ -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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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(
|
|
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
|
package/python/llm_debugging.py
CHANGED
|
@@ -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", "
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
659
|
-
|
|
649
|
+
if not response_text:
|
|
650
|
+
print(f"⚠️ {provider} returned no response. Trying next provider...")
|
|
651
|
+
continue
|
|
660
652
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|