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-
|
|
1
|
+
{"created":"2025-08-20T13:10:12.929Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
|
|
Binary file
|
|
@@ -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()
|