gitarsenal-cli 1.9.69 → 1.9.71

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-15T05:02:30.288Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-08-15T05:31:11.835Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
package/bin/gitarsenal.js CHANGED
@@ -16,6 +16,89 @@ const fs = require('fs');
16
16
  const https = require('https');
17
17
  const http = require('http');
18
18
 
19
+ // Function to activate virtual environment
20
+ function activateVirtualEnvironment() {
21
+ const isWindows = process.platform === 'win32';
22
+ const venvPath = path.join(__dirname, '..', '.venv');
23
+ const statusFile = path.join(__dirname, '..', '.venv_status.json');
24
+
25
+ // Check if virtual environment exists
26
+ if (!fs.existsSync(venvPath)) {
27
+ console.log(chalk.red('āŒ Virtual environment not found. Please reinstall the package:'));
28
+ console.log(chalk.yellow(' npm uninstall -g gitarsenal-cli'));
29
+ console.log(chalk.yellow(' npm install -g gitarsenal-cli'));
30
+ console.log(chalk.yellow(''));
31
+ console.log(chalk.yellow('šŸ’” Or run the postinstall script manually:'));
32
+ console.log(chalk.yellow(' cd /root/.nvm/versions/node/v22.18.0/lib/node_modules/gitarsenal-cli'));
33
+ console.log(chalk.yellow(' node scripts/postinstall.js'));
34
+ return false;
35
+ }
36
+
37
+ // Check if status file exists (indicates successful installation)
38
+ if (fs.existsSync(statusFile)) {
39
+ try {
40
+ const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
41
+ } catch (error) {
42
+ console.log(chalk.gray('āœ… Virtual environment found'));
43
+ }
44
+ }
45
+
46
+ // Verify virtual environment structure - uv creates different structure
47
+ let pythonPath, pipPath;
48
+
49
+ // Check for uv-style virtual environment first
50
+ const uvPythonPath = path.join(venvPath, 'bin', 'python');
51
+ const uvPipPath = path.join(venvPath, 'bin', 'pip');
52
+
53
+ // Check for traditional venv structure
54
+ const traditionalPythonPath = isWindows ?
55
+ path.join(venvPath, 'Scripts', 'python.exe') :
56
+ path.join(venvPath, 'bin', 'python');
57
+ const traditionalPipPath = isWindows ?
58
+ path.join(venvPath, 'Scripts', 'pip.exe') :
59
+ path.join(venvPath, 'bin', 'pip');
60
+
61
+ // Determine which structure exists
62
+ // console.log(chalk.gray(`šŸ” Checking virtual environment structure:`));
63
+ // console.log(chalk.gray(` Python: ${uvPythonPath} (exists: ${fs.existsSync(uvPythonPath)})`));
64
+ // console.log(chalk.gray(` Pip: ${uvPipPath} (exists: ${fs.existsSync(uvPipPath)})`));
65
+
66
+ // For uv virtual environments, we only need Python to exist
67
+ // uv doesn't create a pip executable, it uses 'uv pip' instead
68
+ if (fs.existsSync(uvPythonPath)) {
69
+ pythonPath = uvPythonPath;
70
+ pipPath = 'uv pip'; // Use uv pip instead of pip executable
71
+ // console.log(chalk.gray('āœ… Found uv-style virtual environment'));
72
+ } else if (fs.existsSync(traditionalPythonPath) && fs.existsSync(traditionalPipPath)) {
73
+ pythonPath = traditionalPythonPath;
74
+ pipPath = traditionalPipPath;
75
+ console.log(chalk.gray('āœ… Found traditional virtual environment'));
76
+ } else {
77
+ console.log(chalk.red('āŒ Virtual environment structure not recognized'));
78
+ console.log(chalk.gray('Expected Python at:'));
79
+ console.log(chalk.gray(` ${uvPythonPath}`));
80
+ console.log(chalk.gray(` ${traditionalPythonPath}`));
81
+ console.log(chalk.yellow('šŸ’” Please reinstall the package'));
82
+ return false;
83
+ }
84
+
85
+ // Update PATH to prioritize virtual environment
86
+ const pathSeparator = isWindows ? ';' : ':';
87
+ const venvBinPath = path.dirname(pythonPath); // Use the same directory as the Python executable
88
+
89
+ process.env.PATH = `${venvBinPath}${pathSeparator}${process.env.PATH}`;
90
+ process.env.VIRTUAL_ENV = venvPath;
91
+ process.env.PYTHONPATH = venvPath;
92
+
93
+ // Set Python executable path for child processes
94
+ process.env.PYTHON_EXECUTABLE = pythonPath;
95
+ process.env.PIP_EXECUTABLE = pipPath;
96
+
97
+ console.log(chalk.green('āœ… Virtual environment activated successfully'));
98
+
99
+ return true;
100
+ }
101
+
19
102
 
20
103
  // Lightweight preview of GPU/Torch/CUDA recommendations prior to GPU selection
21
104
  async function previewRecommendations(repoUrl, optsOrShowSummary = true) {
@@ -372,7 +372,7 @@ The following {len(TOOL_SCHEMAS)} tools are loaded and available:
372
372
 
373
373
  # Execute tool calls
374
374
  if tool_calls:
375
- print(f"šŸ¤– Claude requested {len(tool_calls)} tool call(s)")
375
+ print(f"šŸ¤– Agent requested {len(tool_calls)} tool call(s)")
376
376
  tool_results = []
377
377
 
378
378
  for i, tool_call in enumerate(tool_calls, 1):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.69",
3
+ "version": "1.9.71",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -541,28 +541,89 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
541
541
  print("šŸŽ‰ AGENT OUTPUT (LIVE)")
542
542
  print("="*60)
543
543
 
544
- # Use Popen for real-time output streaming
544
+ # Use Popen for real-time output streaming with optimizations
545
+ import sys
546
+ import select
547
+ import fcntl
548
+ import os as os_module
549
+
545
550
  process = subprocess.Popen(
546
- ["python", "/python/kill_claude/claude_code_agent.py", claude_prompt],
551
+ ["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt], # -u for unbuffered output
547
552
  cwd="/root",
548
553
  stdout=subprocess.PIPE,
549
- stderr=subprocess.STDOUT, # Merge stderr into stdout
554
+ stderr=subprocess.PIPE, # Keep separate for better handling
550
555
  text=True,
551
- bufsize=1, # Line buffered
552
- universal_newlines=True
556
+ bufsize=0, # Unbuffered for fastest output
557
+ universal_newlines=True,
558
+ env=dict(os.environ, PYTHONUNBUFFERED='1') # Force unbuffered Python output
553
559
  )
554
560
 
555
- # Stream output in real-time
561
+ # Make stdout and stderr non-blocking for faster reading
562
+ def make_non_blocking(fd):
563
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL)
564
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags | os_module.O_NONBLOCK)
565
+
566
+ make_non_blocking(process.stdout)
567
+ make_non_blocking(process.stderr)
568
+
569
+ # Stream output in real-time with robust error handling
556
570
  try:
557
- while True:
558
- output = process.stdout.readline()
559
- if output == '' and process.poll() is not None:
560
- break
561
- if output:
562
- print(output.rstrip())
571
+ stdout_buffer = ""
572
+ stderr_buffer = ""
573
+
574
+ while process.poll() is None:
575
+ try:
576
+ # Use select for efficient I/O multiplexing with error handling
577
+ ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1) # 100ms timeout
578
+
579
+ for stream in ready:
580
+ try:
581
+ if stream == process.stdout:
582
+ chunk = stream.read(1024) # Read in chunks for efficiency
583
+ if chunk is not None and chunk:
584
+ stdout_buffer += chunk
585
+ # Process complete lines immediately
586
+ while '\n' in stdout_buffer:
587
+ line, stdout_buffer = stdout_buffer.split('\n', 1)
588
+ print(line, flush=True) # Force immediate flush
589
+ elif stream == process.stderr:
590
+ chunk = stream.read(1024)
591
+ if chunk is not None and chunk:
592
+ stderr_buffer += chunk
593
+ # Process complete lines immediately
594
+ while '\n' in stderr_buffer:
595
+ line, stderr_buffer = stderr_buffer.split('\n', 1)
596
+ print(f"STDERR: {line}", flush=True)
597
+ except (BlockingIOError, OSError, ValueError):
598
+ # Handle various I/O errors gracefully
599
+ continue
600
+ except (select.error, OSError):
601
+ # If select fails, fall back to simple polling
602
+ time.sleep(0.1)
603
+ continue
604
+
605
+ # Process any remaining output after process ends
606
+ try:
607
+ # Read any remaining data from streams
608
+ remaining_stdout = process.stdout.read()
609
+ remaining_stderr = process.stderr.read()
610
+
611
+ if remaining_stdout:
612
+ stdout_buffer += remaining_stdout
613
+ if remaining_stderr:
614
+ stderr_buffer += remaining_stderr
615
+
616
+ # Output remaining buffered content
617
+ if stdout_buffer.strip():
618
+ print(stdout_buffer.strip(), flush=True)
619
+ if stderr_buffer.strip():
620
+ print(f"STDERR: {stderr_buffer.strip()}", flush=True)
621
+ except (OSError, ValueError):
622
+ # Handle cases where streams are already closed
623
+ pass
563
624
 
564
- # Wait for process to complete and get return code
565
- return_code = process.wait(timeout=600) # 10 minute timeout
625
+ # Get final return code
626
+ return_code = process.returncode
566
627
 
567
628
  print("\n" + "="*60)
568
629
  if return_code == 0:
@@ -576,9 +637,48 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
576
637
  process.kill()
577
638
  process.wait()
578
639
  except Exception as stream_error:
579
- print(f"\nāš ļø Error streaming output: {stream_error}")
580
- process.kill()
581
- process.wait()
640
+ # print(f"\nāš ļø Error with advanced streaming: {stream_error}")
641
+ # print("šŸ”„ Falling back to simple streaming...")
642
+ pass
643
+
644
+ # Fallback to simple readline approach
645
+ try:
646
+ # Restart the process with simpler streaming
647
+ if process.poll() is None:
648
+ process.kill()
649
+ process.wait()
650
+
651
+ fallback_process = subprocess.Popen(
652
+ ["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt],
653
+ cwd="/root",
654
+ stdout=subprocess.PIPE,
655
+ stderr=subprocess.STDOUT,
656
+ text=True,
657
+ bufsize=1,
658
+ universal_newlines=True
659
+ )
660
+
661
+ # Simple line-by-line reading
662
+ while True:
663
+ line = fallback_process.stdout.readline()
664
+ if line == '' and fallback_process.poll() is not None:
665
+ break
666
+ if line:
667
+ print(line.rstrip(), flush=True)
668
+
669
+ return_code = fallback_process.returncode
670
+
671
+ print("\n" + "="*60)
672
+ if return_code == 0:
673
+ print("āœ… Agent completed successfully!")
674
+ else:
675
+ print(f"āš ļø Agent exited with code: {return_code}")
676
+ print("="*60)
677
+
678
+ except Exception as fallback_error:
679
+ print(f"\nāŒ Fallback streaming also failed: {fallback_error}")
680
+ print("āš ļø Agent may have completed, but output streaming failed")
681
+ return_code = 1
582
682
 
583
683
  except Exception as e:
584
684
  print(f"āŒ Error during repository setup: {e}")