gitarsenal-cli 1.9.86 → 1.9.87

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-20T12:56:37.874Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-08-20T13:10:12.929Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
@@ -33,8 +33,11 @@ from rich.text import Text
33
33
  from rich.progress import Progress, SpinnerColumn, TextColumn
34
34
  from rich.tree import Tree
35
35
  from rich.syntax import Syntax
36
- from rich.prompt import Prompt
36
+ from rich.prompt import Prompt, Confirm, IntPrompt
37
37
  from rich import print as rprint
38
+ from rich.layout import Layout
39
+ from rich.live import Live
40
+ from pathlib import Path
38
41
 
39
42
  def load_tool_modules():
40
43
  """Load all tool modules from the tools directory."""
@@ -617,6 +620,10 @@ The following {len(TOOL_SCHEMAS)} tools are loaded and available:
617
620
  return self._get_bash_output(tool_input)
618
621
  elif tool_name == "KillBash":
619
622
  return self._kill_bash(tool_input)
623
+ elif tool_name == "WebSearch":
624
+ return self._web_search(tool_input)
625
+ elif tool_name == "WebFetch":
626
+ return self._web_fetch(tool_input)
620
627
  else:
621
628
  return f"Tool {tool_name} not implemented"
622
629
 
@@ -844,6 +851,60 @@ The following {len(TOOL_SCHEMAS)} tools are loaded and available:
844
851
  except Exception as e:
845
852
  return f"Error searching: {str(e)}"
846
853
 
854
+ def _web_search(self, tool_input: Dict[str, Any]) -> str:
855
+ """Handle WebSearch tool - search the web for information."""
856
+ query = tool_input.get('query', '')
857
+ allowed_domains = tool_input.get('allowed_domains', [])
858
+ blocked_domains = tool_input.get('blocked_domains', [])
859
+
860
+ if not query:
861
+ return "Error: No search query provided"
862
+
863
+ # Since we don't have actual web search capability, return a mock response
864
+ # In a real implementation, this would use a search API like Google, Bing, or DuckDuckGo
865
+ domain_filter = ""
866
+ if allowed_domains:
867
+ domain_filter = f" (restricted to: {', '.join(allowed_domains)})"
868
+ elif blocked_domains:
869
+ domain_filter = f" (excluding: {', '.join(blocked_domains)})"
870
+
871
+ return f"""Web search results for: "{query}"{domain_filter}
872
+
873
+ ⚠️ Note: WebSearch is not fully implemented in this demo version.
874
+
875
+ In a production environment, this would:
876
+ • Search the web using APIs like Google Search API, Bing API, or similar
877
+ • Return formatted search results with titles, snippets, and URLs
878
+ • Support domain filtering as specified
879
+ • Provide up-to-date information beyond the AI's knowledge cutoff
880
+
881
+ Query processed: "{query}"
882
+ Domain restrictions: {domain_filter if domain_filter else "None"}"""
883
+
884
+ def _web_fetch(self, tool_input: Dict[str, Any]) -> str:
885
+ """Handle WebFetch tool - fetch content from a URL."""
886
+ url = tool_input.get('url', '')
887
+ prompt = tool_input.get('prompt', '')
888
+
889
+ if not url:
890
+ return "Error: No URL provided"
891
+
892
+ # Since we don't have actual web fetching capability, return a mock response
893
+ # In a real implementation, this would fetch the URL content and process it
894
+ return f"""WebFetch results for: {url}
895
+
896
+ ⚠️ Note: WebFetch is not fully implemented in this demo version.
897
+
898
+ In a production environment, this would:
899
+ • Fetch content from the specified URL
900
+ • Convert HTML to markdown if needed
901
+ • Process the content with the provided prompt: "{prompt}"
902
+ • Return the AI model's analysis of the fetched content
903
+ • Handle redirects and various content types
904
+
905
+ URL to fetch: {url}
906
+ Processing prompt: "{prompt}" """
907
+
847
908
  def process_query(self, user_input: str) -> str:
848
909
  """
849
910
  Main method to process user queries using the Anthropic API.
@@ -1152,7 +1213,10 @@ def setup():
1152
1213
  [bold yellow]4. Run the agent:[/bold yellow]
1153
1214
  [cyan]python claude_code_agent.py interactive[/cyan]
1154
1215
  or
1155
- [cyan]python claude_code_agent.py query "your question"[/cyan]"""
1216
+ [cyan]python claude_code_agent.py query "your question"[/cyan]
1217
+
1218
+ [bold cyan]5. Quick setup:[/bold cyan]
1219
+ [cyan]python claude_code_agent.py configure[/cyan]"""
1156
1220
 
1157
1221
  console.print(Panel(
1158
1222
  Columns([
@@ -1164,6 +1228,287 @@ def setup():
1164
1228
  padding=(1, 1)
1165
1229
  ))
1166
1230
 
1231
+ @app.command()
1232
+ def configure(
1233
+ reset: bool = typer.Option(False, "--reset", "-r", help="Reset configuration to defaults")
1234
+ ):
1235
+ """🔧 Interactive configuration wizard"""
1236
+ console = Console()
1237
+ config_file = Path.home() / ".claude_code_agent.json"
1238
+
1239
+ if reset:
1240
+ if config_file.exists():
1241
+ config_file.unlink()
1242
+ console.print("✓ [green]Configuration reset to defaults[/green]")
1243
+ return
1244
+
1245
+ console.print(Panel(
1246
+ "[bold blue]🔧 Claude Code Agent Configuration Wizard[/bold blue]\n" +
1247
+ "This will help you set up your preferences and API credentials.",
1248
+ title="Configuration Wizard",
1249
+ border_style="blue"
1250
+ ))
1251
+
1252
+ # Load existing config
1253
+ config = {}
1254
+ if config_file.exists():
1255
+ with open(config_file) as f:
1256
+ config = json.load(f)
1257
+ console.print(f"[dim]Found existing config at: {config_file}[/dim]")
1258
+
1259
+ # API Key configuration
1260
+ current_api_key = config.get('api_key') or os.getenv('ANTHROPIC_API_KEY')
1261
+ if current_api_key:
1262
+ console.print(f"✓ [green]API Key is already configured[/green]")
1263
+ if not Confirm.ask("Would you like to update it?"):
1264
+ api_key = current_api_key
1265
+ else:
1266
+ api_key = typer.prompt("Enter your Anthropic API key", hide_input=True)
1267
+ else:
1268
+ console.print("✗ [red]API Key not found[/red]")
1269
+ api_key = typer.prompt("Enter your Anthropic API key", hide_input=True)
1270
+
1271
+ # Model selection
1272
+ models = [
1273
+ "claude-sonnet-4-20250514",
1274
+ "claude-3-5-sonnet-20241022",
1275
+ "claude-3-opus-20240229",
1276
+ "claude-3-haiku-20240307"
1277
+ ]
1278
+ current_model = config.get('default_model', models[0])
1279
+ console.print(f"\nCurrent model: [cyan]{current_model}[/cyan]")
1280
+
1281
+ model_choice = typer.prompt(
1282
+ "Select model",
1283
+ type=typer.Choice(models),
1284
+ default=current_model
1285
+ )
1286
+
1287
+ # Timeout configuration
1288
+ current_timeout = config.get('default_timeout', 120)
1289
+ timeout = IntPrompt.ask(
1290
+ "Default timeout (seconds)",
1291
+ default=current_timeout,
1292
+ show_default=True
1293
+ )
1294
+
1295
+ # Tool preferences
1296
+ console.print("\n[bold]Tool Preferences:[/bold]")
1297
+ use_progress_bars = Confirm.ask(
1298
+ "Show progress bars for long operations?",
1299
+ default=config.get('use_progress_bars', True)
1300
+ )
1301
+
1302
+ auto_todos = Confirm.ask(
1303
+ "Automatically create todo lists for complex tasks?",
1304
+ default=config.get('auto_todos', True)
1305
+ )
1306
+
1307
+ syntax_highlighting = Confirm.ask(
1308
+ "Enable syntax highlighting for code files?",
1309
+ default=config.get('syntax_highlighting', True)
1310
+ )
1311
+
1312
+ # Save configuration
1313
+ new_config = {
1314
+ 'api_key': api_key,
1315
+ 'default_model': model_choice,
1316
+ 'default_timeout': timeout,
1317
+ 'use_progress_bars': use_progress_bars,
1318
+ 'auto_todos': auto_todos,
1319
+ 'syntax_highlighting': syntax_highlighting,
1320
+ 'configured_at': str(datetime.now())
1321
+ }
1322
+
1323
+ # Ensure config directory exists
1324
+ config_file.parent.mkdir(exist_ok=True)
1325
+
1326
+ with open(config_file, 'w') as f:
1327
+ json.dump(new_config, f, indent=2)
1328
+
1329
+ # Show summary
1330
+ summary_table = Table(title="Configuration Summary", show_header=True)
1331
+ summary_table.add_column("Setting", style="yellow")
1332
+ summary_table.add_column("Value", style="green")
1333
+
1334
+ summary_table.add_row("API Key", "✓ Configured" if api_key else "✗ Missing")
1335
+ summary_table.add_row("Model", model_choice)
1336
+ summary_table.add_row("Timeout", f"{timeout}s")
1337
+ summary_table.add_row("Progress Bars", "✓ Enabled" if use_progress_bars else "✗ Disabled")
1338
+ summary_table.add_row("Auto Todos", "✓ Enabled" if auto_todos else "✗ Disabled")
1339
+ summary_table.add_row("Syntax Highlighting", "✓ Enabled" if syntax_highlighting else "✗ Disabled")
1340
+ summary_table.add_row("Config File", str(config_file))
1341
+
1342
+ console.print(Panel(
1343
+ summary_table,
1344
+ title="✓ Configuration Complete",
1345
+ border_style="green"
1346
+ ))
1347
+
1348
+ @app.command()
1349
+ def browse():
1350
+ """📁 Interactive file browser"""
1351
+ console = Console()
1352
+ current_dir = Path.cwd()
1353
+
1354
+ while True:
1355
+ # List directory contents
1356
+ items = []
1357
+ if current_dir.parent != current_dir: # Not root
1358
+ items.append((".. (parent directory)", current_dir.parent, "dir"))
1359
+
1360
+ for item in sorted(current_dir.iterdir()):
1361
+ if item.is_dir():
1362
+ items.append((f"{item.name}/", item, "dir"))
1363
+ else:
1364
+ size = item.stat().st_size
1365
+ size_str = f"{size:,} bytes" if size < 1024 else f"{size//1024:,} KB"
1366
+ items.append((f"{item.name} ({size_str})", item, "file"))
1367
+
1368
+ # Create selection table
1369
+ table = Table(title=f"Directory: {current_dir}", show_header=True)
1370
+ table.add_column("#", style="dim", width=3)
1371
+ table.add_column("Type", width=4)
1372
+ table.add_column("Name", style="cyan")
1373
+
1374
+ for i, (display_name, path, item_type) in enumerate(items, 1):
1375
+ icon = "📁" if item_type == "dir" else "📄"
1376
+ table.add_row(str(i), icon, display_name)
1377
+
1378
+ console.clear()
1379
+ console.print(table)
1380
+
1381
+ # Get user choice
1382
+ console.print("\n[dim]Commands: [cyan]number[/cyan] to select, [cyan]q[/cyan] to quit, [cyan]r[/cyan] to read file[/dim]")
1383
+ choice = typer.prompt("Select item")
1384
+
1385
+ if choice.lower() == 'q':
1386
+ break
1387
+ elif choice.lower() == 'r':
1388
+ file_num = typer.prompt("File number to read", type=int)
1389
+ if 1 <= file_num <= len(items):
1390
+ selected_path = items[file_num - 1][1]
1391
+ if selected_path.is_file():
1392
+ try:
1393
+ # Read file directly without requiring API key
1394
+ with open(selected_path, 'r', encoding='utf-8') as f:
1395
+ lines = f.readlines()
1396
+
1397
+ # Format with line numbers
1398
+ formatted_lines = []
1399
+ for i, line in enumerate(lines[:100], 1): # Limit to 100 lines
1400
+ clean_line = line.rstrip('\n\r')
1401
+ if len(clean_line) > 2000:
1402
+ clean_line = clean_line[:2000] + "..."
1403
+ formatted_lines.append(f"{i:>5}→{clean_line}")
1404
+
1405
+ content = '\n'.join(formatted_lines)
1406
+ if len(lines) > 100:
1407
+ content += f"\n... (showing first 100 lines of {len(lines)} total)"
1408
+
1409
+ console.print(Panel(
1410
+ content,
1411
+ title=f"📄 {selected_path.name}",
1412
+ border_style="blue"
1413
+ ))
1414
+ typer.prompt("Press Enter to continue")
1415
+ except Exception as e:
1416
+ console.print(f"[red]✗ Error reading file: {e}[/red]")
1417
+ else:
1418
+ try:
1419
+ choice_num = int(choice)
1420
+ if 1 <= choice_num <= len(items):
1421
+ selected_path = items[choice_num - 1][1]
1422
+ if selected_path.is_dir():
1423
+ current_dir = selected_path
1424
+ else:
1425
+ console.print(f"Selected file: [cyan]{selected_path}[/cyan]")
1426
+ break
1427
+ except ValueError:
1428
+ console.print("[red]Invalid choice[/red]")
1429
+ typer.prompt("Press Enter to continue")
1430
+
1431
+ @app.command()
1432
+ def templates():
1433
+ """🎨 Manage project templates"""
1434
+ console = Console()
1435
+
1436
+ # Built-in templates
1437
+ builtin_templates = {
1438
+ "python-basic": {
1439
+ "description": "Basic Python project structure",
1440
+ "files": {
1441
+ "main.py": "#!/usr/bin/env python3\n\nif __name__ == '__main__':\n print('Hello, World!')\n",
1442
+ "requirements.txt": "",
1443
+ "README.md": "# Project\n\nDescription here.\n"
1444
+ }
1445
+ },
1446
+ "python-package": {
1447
+ "description": "Python package with setup.py",
1448
+ "files": {
1449
+ "setup.py": "from setuptools import setup, find_packages\n\nsetup(\n name='my-package',\n version='0.1.0',\n packages=find_packages(),\n)\n",
1450
+ "my_package/__init__.py": "",
1451
+ "my_package/main.py": "def main():\n pass\n",
1452
+ "requirements.txt": "",
1453
+ "README.md": "# My Package\n"
1454
+ }
1455
+ },
1456
+ "claude-agent": {
1457
+ "description": "Claude Code Agent project structure",
1458
+ "files": {
1459
+ "main.py": "#!/usr/bin/env python3\nfrom claude_code_agent import ClaudeCodeAgent\n\ndef main():\n agent = ClaudeCodeAgent()\n agent.interactive_mode()\n\nif __name__ == '__main__':\n main()\n",
1460
+ "requirements.txt": "anthropic>=0.25.0\ntyper>=0.12.0\nrich>=13.0.0\n",
1461
+ "README.md": "# Claude Agent Project\n\nA project using Claude Code Agent.\n",
1462
+ ".env.example": "ANTHROPIC_API_KEY=your_key_here\n"
1463
+ }
1464
+ }
1465
+ }
1466
+
1467
+ # List available templates
1468
+ table = Table(title="Available Templates", show_header=True)
1469
+ table.add_column("Name", style="cyan")
1470
+ table.add_column("Description", style="yellow")
1471
+ table.add_column("Files", style="green")
1472
+
1473
+ for name, template in builtin_templates.items():
1474
+ file_count = len(template["files"])
1475
+ table.add_row(name, template["description"], f"{file_count} files")
1476
+
1477
+ console.print(table)
1478
+
1479
+ template_name = typer.prompt("Select template to create")
1480
+ if template_name not in builtin_templates:
1481
+ console.print(f"[red]✗ Template '{template_name}' not found[/red]")
1482
+ return
1483
+
1484
+ project_name = typer.prompt("Project name")
1485
+ project_dir = Path.cwd() / project_name
1486
+
1487
+ if project_dir.exists():
1488
+ if not Confirm.ask(f"Directory {project_name} exists. Continue?"):
1489
+ return
1490
+
1491
+ project_dir.mkdir(exist_ok=True)
1492
+ template = builtin_templates[template_name]
1493
+
1494
+ # Create files with progress bar
1495
+ with Progress() as progress:
1496
+ task = progress.add_task("Creating project...", total=len(template["files"]))
1497
+
1498
+ for file_path, content in template["files"].items():
1499
+ full_path = project_dir / file_path
1500
+ full_path.parent.mkdir(parents=True, exist_ok=True)
1501
+ full_path.write_text(content)
1502
+ progress.advance(task)
1503
+
1504
+ console.print(Panel(
1505
+ f"✓ [green]Project '{project_name}' created successfully![/green]\n" +
1506
+ f"Location: [cyan]{project_dir}[/cyan]\n" +
1507
+ f"Template: [yellow]{template_name}[/yellow]",
1508
+ title="Project Created",
1509
+ border_style="green"
1510
+ ))
1511
+
1167
1512
  def main():
1168
1513
  """Main entry point - delegate to Typer app."""
1169
1514
  app()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.86",
3
+ "version": "1.9.87",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {