/ run.py
run.py
  1  #!/usr/bin/env python3
  2  """
  3  Universal Proxy Wrapper for Claude Commands.
  4  
  5  This is the main entry point for ALL commands executed by Claude.
  6  It wraps any command, executes it, and returns token-compressed output.
  7  
  8  Usage:
  9      python run.py <command> [args...]
 10  
 11  Examples:
 12      python run.py cargo build
 13      python run.py npm test
 14      python run.py python script.py
 15      python run.py ls -la
 16  
 17  Output Format:
 18      - Success: "." per completed unit, "[S:PASS]" at end
 19      - Errors:  "!ERR:CODE description" with "@ file:line" locations
 20      - Passthrough: Non-boilerplate output preserved
 21  """
 22  import sys
 23  import subprocess
 24  import re
 25  import os
 26  import glob as globmodule
 27  
 28  
 29  def strip_ansi(text):
 30      """Remove ANSI escape codes from text."""
 31      ansi_escape = re.compile(r'\x1B(?:[@-Z\-_]|[0-?]*[@-~])')
 32      return ansi_escape.sub('', text)
 33  
 34  
 35  def detect_stacks():
 36      """Auto-detect project stack(s) from current directory."""
 37      stacks = set()
 38      cwd = os.getcwd()
 39  
 40      # File-based detection
 41      indicators = {
 42          'rust':   ['Cargo.toml'],
 43          'node':   ['package.json'],
 44          'go':     ['go.mod'],
 45          'python': ['requirements.txt', 'pyproject.toml', 'Pipfile', 'setup.py'],
 46          'java':   ['pom.xml', 'build.gradle', 'build.gradle.kts'],
 47      }
 48  
 49      for stack, files in indicators.items():
 50          for f in files:
 51              if os.path.exists(os.path.join(cwd, f)):
 52                  stacks.add(stack)
 53                  break
 54  
 55      # Extension-based fallback
 56      if 'python' not in stacks and globmodule.glob(os.path.join(cwd, '*.py')):
 57          stacks.add('python')
 58      if 'rust' not in stacks and globmodule.glob(os.path.join(cwd, 'src', '*.rs')):
 59          stacks.add('rust')
 60  
 61      # CI/Workflow detection
 62      workflow_files = globmodule.glob(os.path.join(cwd, '.forgejo', 'workflows', '*.yml')) + \
 63                       globmodule.glob(os.path.join(cwd, '.github', 'workflows', '*.yml'))
 64  
 65      for wf in workflow_files:
 66          try:
 67              with open(wf, 'r', encoding='utf-8') as f:
 68                  content = f.read()
 69                  if re.search(r'\bcargo\b', content):
 70                      stacks.add('rust')
 71                  if re.search(r'\b(npm|yarn|pnpm|bun)\b', content):
 72                      stacks.add('node')
 73                  if re.search(r'\b(go build|go test)\b', content):
 74                      stacks.add('go')
 75                  if re.search(r'\b(pip|python|pytest|poetry)\b', content):
 76                      stacks.add('python')
 77          except Exception:
 78              pass
 79  
 80      if not stacks:
 81          stacks.add('generic')
 82  
 83      return stacks
 84  
 85  
 86  # Compile patterns once for performance
 87  SUPPRESS_PATTERNS = re.compile(
 88      r'^(Downloading|Compiling|Fetching|Running|Created|Started|Updated|'
 89      r'Installing|Waiting|Unpacking|Resolving|Building|Linking|Generating)\b',
 90      re.IGNORECASE
 91  )
 92  SUCCESS_PATTERNS = re.compile(
 93      r'^(ok|passed|finished|success|completed|done|built|installed)$',
 94      re.IGNORECASE
 95  )
 96  RUST_ERROR = re.compile(r'error\[(E\d+)\]')
 97  RUST_LOCATION = re.compile(r'-->\s+(.+:\d+)')
 98  NODE_ERROR = re.compile(r'^(\w+Error):')
 99  NODE_LOCATION = re.compile(r'\((.+:\d+:\d+)\)')
100  GO_ERROR = re.compile(r'^(.+\.go:\d+:\d+):\s+(.+)')
101  PYTHON_TRACEBACK = 'Traceback (most recent call last):'
102  PYTHON_LOCATION = re.compile(r'File "([^"]+)", line (\d+)(?:, in (.+))?')
103  PYTHON_ERROR = re.compile(r'^(\w+Error):')
104  GENERIC_ERROR = re.compile(r'\b(Error|Exception|Fail|Fatal)\b', re.IGNORECASE)
105  FILE_LOCATION = re.compile(r'(\S+\.\w+:\d+)')
106  
107  
108  def process_line(line, stacks):
109      """Process a single line of output, returning compressed form or None."""
110      clean = strip_ansi(line).strip()
111      if not clean:
112          return None
113  
114      # Suppress boilerplate
115      if SUPPRESS_PATTERNS.match(clean):
116          return None
117  
118      # Success patterns -> dot
119      if SUCCESS_PATTERNS.match(clean):
120          return '.'
121  
122      # Stack-specific error extraction
123      if 'rust' in stacks:
124          err_match = RUST_ERROR.search(clean)
125          if err_match:
126              return f"!ERR:{err_match.group(1)} {clean[err_match.end():].strip()}"
127          loc_match = RUST_LOCATION.search(clean)
128          if loc_match:
129              return f"@ {loc_match.group(1)}"
130  
131      if 'node' in stacks:
132          err_match = NODE_ERROR.match(clean)
133          if err_match:
134              return f"!ERR:{err_match.group(1)} {clean[err_match.end():].strip()}"
135          loc_match = NODE_LOCATION.search(clean)
136          if loc_match:
137              return f"@ {loc_match.group(1)}"
138  
139      if 'go' in stacks:
140          match = GO_ERROR.match(clean)
141          if match:
142              return f"!ERR:GoError @ {match.group(1)} {match.group(2)}"
143  
144      if 'python' in stacks:
145          if clean.startswith(PYTHON_TRACEBACK):
146              return "!ERR:Traceback"
147          loc_match = PYTHON_LOCATION.match(clean)
148          if loc_match:
149              file_path = loc_match.group(1)
150              line_num = loc_match.group(2)
151              context = loc_match.group(3) or ""
152              return f"@ {file_path}:{line_num} {context}"
153          err_match = PYTHON_ERROR.match(clean)
154          if err_match:
155              return f"!ERR:{err_match.group(1)} {clean[err_match.end():].strip()}"
156  
157      # Generic error fallback
158      if GENERIC_ERROR.search(clean):
159          file_match = FILE_LOCATION.search(clean)
160          loc = f" @ {file_match.group(1)}" if file_match else ""
161          return f"!ERR:GENERIC{loc} {clean}"
162  
163      return clean
164  
165  
166  def main():
167      if len(sys.argv) < 2:
168          print("Usage: python run.py <command> [args...]")
169          print("Universal proxy wrapper for Claude command execution.")
170          sys.exit(0)
171  
172      command = sys.argv[1:]
173  
174      # Docker restriction (create .allow_docker to override)
175      if command and command[0] == 'docker':
176          if not os.path.exists(os.path.join(os.getcwd(), '.allow_docker')):
177              sys.stderr.write("!ERR:RESTRICTED Docker forbidden (Native Runner). Create .allow_docker to override.\n")
178              sys.exit(1)
179  
180      stacks = detect_stacks()
181  
182      process = None
183      dot_count = 0
184  
185      try:
186          process = subprocess.Popen(
187              command,
188              stdout=subprocess.PIPE,
189              stderr=subprocess.STDOUT,
190              text=True,
191              bufsize=1,
192              encoding='utf-8',
193              errors='replace'
194          )
195  
196          while True:
197              line = process.stdout.readline()
198              if not line and process.poll() is not None:
199                  break
200              if not line:
201                  continue
202  
203              result = process_line(line, stacks)
204              if result:
205                  if result == '.':
206                      dot_count += 1
207                      # Batch dots for cleaner output
208                      if dot_count % 10 == 0:
209                          sys.stdout.write('.')
210                          sys.stdout.flush()
211                  elif result.startswith('!ERR'):
212                      # Flush pending dots before error
213                      if dot_count % 10 != 0:
214                          sys.stdout.write('.')
215                      sys.stdout.write('\n' + result + '\n')
216                      sys.stdout.flush()
217                  elif result.startswith('@'):
218                      sys.stdout.write(result + '\n')
219                      sys.stdout.flush()
220                  else:
221                      sys.stdout.write(result + '\n')
222                      sys.stdout.flush()
223  
224          process.wait()
225  
226          # Final status
227          if process.returncode == 0:
228              sys.stdout.write('\n[S:PASS]\n')
229          else:
230              sys.stdout.write(f'\n[S:FAIL] exit={process.returncode}\n')
231  
232          sys.exit(process.returncode)
233  
234      except FileNotFoundError:
235          sys.stderr.write(f"!ERR:NOTFOUND Command not found: {command[0]}\n")
236          sys.exit(127)
237      except KeyboardInterrupt:
238          if process:
239              process.terminate()
240          sys.exit(130)
241      except Exception as e:
242          sys.stderr.write(f"!ERR:PROXY {e}\n")
243          sys.exit(1)
244  
245  
246  if __name__ == "__main__":
247      main()