@intentsolutionsio/mempool-analyzer 1.0.0

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.
Files changed (28) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/agents/mempool-agent.md +158 -0
  5. package/package.json +43 -0
  6. package/skills/analyzing-mempool/ARD.md +146 -0
  7. package/skills/analyzing-mempool/PRD.md +71 -0
  8. package/skills/analyzing-mempool/SKILL.md +110 -0
  9. package/skills/analyzing-mempool/config/settings.yaml +43 -0
  10. package/skills/analyzing-mempool/references/errors.md +122 -0
  11. package/skills/analyzing-mempool/references/examples.md +189 -0
  12. package/skills/analyzing-mempool/references/implementation.md +67 -0
  13. package/skills/analyzing-mempool/scripts/formatters.py +244 -0
  14. package/skills/analyzing-mempool/scripts/gas_analyzer.py +299 -0
  15. package/skills/analyzing-mempool/scripts/mempool_analyzer.py +320 -0
  16. package/skills/analyzing-mempool/scripts/mev_detector.py +387 -0
  17. package/skills/analyzing-mempool/scripts/rpc_client.py +311 -0
  18. package/skills/analyzing-mempool/scripts/tx_decoder.py +273 -0
  19. package/skills/skill-adapter/assets/README.md +6 -0
  20. package/skills/skill-adapter/assets/config-template.json +32 -0
  21. package/skills/skill-adapter/assets/skill-schema.json +28 -0
  22. package/skills/skill-adapter/assets/test-data.json +27 -0
  23. package/skills/skill-adapter/references/README.md +4 -0
  24. package/skills/skill-adapter/references/best-practices.md +69 -0
  25. package/skills/skill-adapter/references/examples.md +73 -0
  26. package/skills/skill-adapter/scripts/README.md +8 -0
  27. package/skills/skill-adapter/scripts/helper-template.sh +42 -0
  28. package/skills/skill-adapter/scripts/validation.sh +32 -0
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gas Price Analyzer
4
+
5
+ Analyze gas prices and recommend optimal fees.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ from typing import Dict, Any, List, Optional
13
+ from dataclasses import dataclass
14
+ import statistics
15
+
16
+
17
+ @dataclass
18
+ class GasRecommendation:
19
+ """Gas price recommendation."""
20
+ priority: str # slow, standard, fast, instant
21
+ max_fee: int # in wei
22
+ priority_fee: int # in wei
23
+ estimated_wait: str # e.g., "2-5 blocks"
24
+ confidence: float # 0.0 to 1.0
25
+
26
+
27
+ @dataclass
28
+ class GasDistribution:
29
+ """Gas price distribution analysis."""
30
+ min_gwei: float
31
+ max_gwei: float
32
+ mean_gwei: float
33
+ median_gwei: float
34
+ p10_gwei: float # 10th percentile
35
+ p25_gwei: float # 25th percentile
36
+ p50_gwei: float # 50th percentile
37
+ p75_gwei: float # 75th percentile
38
+ p90_gwei: float # 90th percentile
39
+ sample_count: int
40
+
41
+
42
+ class GasAnalyzer:
43
+ """Analyze mempool gas prices and provide recommendations."""
44
+
45
+ def __init__(self, verbose: bool = False):
46
+ """Initialize gas analyzer."""
47
+ self.verbose = verbose
48
+
49
+ def analyze_pending_gas(
50
+ self,
51
+ pending_txs: List[Any],
52
+ base_fee: int = None
53
+ ) -> GasDistribution:
54
+ """Analyze gas price distribution in pending transactions.
55
+
56
+ Args:
57
+ pending_txs: List of pending transactions
58
+ base_fee: Current base fee in wei
59
+
60
+ Returns:
61
+ GasDistribution with statistics
62
+ """
63
+ # Extract gas prices
64
+ gas_prices = []
65
+ for tx in pending_txs:
66
+ if hasattr(tx, "gas_price"):
67
+ gas_prices.append(tx.gas_price)
68
+ elif isinstance(tx, dict) and "gasPrice" in tx:
69
+ price = tx["gasPrice"]
70
+ if isinstance(price, str):
71
+ price = int(price, 16)
72
+ gas_prices.append(price)
73
+
74
+ if not gas_prices:
75
+ # Return defaults if no data
76
+ default_gwei = 30.0
77
+ return GasDistribution(
78
+ min_gwei=default_gwei,
79
+ max_gwei=default_gwei,
80
+ mean_gwei=default_gwei,
81
+ median_gwei=default_gwei,
82
+ p10_gwei=default_gwei - 5,
83
+ p25_gwei=default_gwei - 2,
84
+ p50_gwei=default_gwei,
85
+ p75_gwei=default_gwei + 5,
86
+ p90_gwei=default_gwei + 10,
87
+ sample_count=0,
88
+ )
89
+
90
+ # Convert to gwei
91
+ gwei_prices = [p / 10**9 for p in gas_prices]
92
+ sorted_prices = sorted(gwei_prices)
93
+ n = len(sorted_prices)
94
+
95
+ def percentile(p: float) -> float:
96
+ idx = int(n * p / 100)
97
+ return sorted_prices[min(idx, n - 1)]
98
+
99
+ return GasDistribution(
100
+ min_gwei=min(gwei_prices),
101
+ max_gwei=max(gwei_prices),
102
+ mean_gwei=statistics.mean(gwei_prices),
103
+ median_gwei=statistics.median(gwei_prices),
104
+ p10_gwei=percentile(10),
105
+ p25_gwei=percentile(25),
106
+ p50_gwei=percentile(50),
107
+ p75_gwei=percentile(75),
108
+ p90_gwei=percentile(90),
109
+ sample_count=n,
110
+ )
111
+
112
+ def recommend_gas(
113
+ self,
114
+ distribution: GasDistribution = None,
115
+ base_fee: int = None,
116
+ priority: str = "standard"
117
+ ) -> GasRecommendation:
118
+ """Get gas price recommendation.
119
+
120
+ Args:
121
+ distribution: Gas distribution from analyze_pending_gas
122
+ base_fee: Current base fee in wei
123
+ priority: slow, standard, fast, or instant
124
+
125
+ Returns:
126
+ GasRecommendation
127
+ """
128
+ # Use distribution or defaults
129
+ if distribution:
130
+ prices = {
131
+ "slow": distribution.p10_gwei,
132
+ "standard": distribution.p50_gwei,
133
+ "fast": distribution.p75_gwei,
134
+ "instant": distribution.p90_gwei,
135
+ }
136
+ else:
137
+ # Default recommendations
138
+ prices = {
139
+ "slow": 25,
140
+ "standard": 35,
141
+ "fast": 50,
142
+ "instant": 75,
143
+ }
144
+
145
+ wait_times = {
146
+ "slow": "10+ blocks (~3 min)",
147
+ "standard": "2-5 blocks (~1 min)",
148
+ "fast": "1-2 blocks (~30 sec)",
149
+ "instant": "Next block (~12 sec)",
150
+ }
151
+
152
+ confidence = {
153
+ "slow": 0.7,
154
+ "standard": 0.9,
155
+ "fast": 0.95,
156
+ "instant": 0.99,
157
+ }
158
+
159
+ selected_gwei = prices.get(priority, prices["standard"])
160
+
161
+ # Calculate max fee and priority fee
162
+ if base_fee:
163
+ base_gwei = base_fee / 10**9
164
+ priority_fee_gwei = max(selected_gwei - base_gwei, 1.0)
165
+ # Max fee should be 2x base fee + priority to handle base fee spikes
166
+ max_fee_gwei = base_gwei * 2 + priority_fee_gwei
167
+ else:
168
+ priority_fee_gwei = 2.0
169
+ max_fee_gwei = selected_gwei
170
+
171
+ return GasRecommendation(
172
+ priority=priority,
173
+ max_fee=int(max_fee_gwei * 10**9),
174
+ priority_fee=int(priority_fee_gwei * 10**9),
175
+ estimated_wait=wait_times.get(priority, "Unknown"),
176
+ confidence=confidence.get(priority, 0.5),
177
+ )
178
+
179
+ def estimate_inclusion_time(
180
+ self,
181
+ gas_price: int,
182
+ distribution: GasDistribution
183
+ ) -> str:
184
+ """Estimate time to transaction inclusion.
185
+
186
+ Args:
187
+ gas_price: Your gas price in wei
188
+ distribution: Current gas distribution
189
+
190
+ Returns:
191
+ Estimated wait time string
192
+ """
193
+ gwei = gas_price / 10**9
194
+
195
+ if gwei >= distribution.p90_gwei:
196
+ return "Next block (~12 sec)"
197
+ elif gwei >= distribution.p75_gwei:
198
+ return "1-2 blocks (~30 sec)"
199
+ elif gwei >= distribution.p50_gwei:
200
+ return "2-5 blocks (~1 min)"
201
+ elif gwei >= distribution.p25_gwei:
202
+ return "5-10 blocks (~2 min)"
203
+ elif gwei >= distribution.p10_gwei:
204
+ return "10+ blocks (~3+ min)"
205
+ else:
206
+ return "May not be included"
207
+
208
+ def format_distribution(self, dist: GasDistribution) -> str:
209
+ """Format gas distribution for display.
210
+
211
+ Args:
212
+ dist: GasDistribution to format
213
+
214
+ Returns:
215
+ Formatted string
216
+ """
217
+ lines = [
218
+ "",
219
+ "GAS PRICE DISTRIBUTION",
220
+ "=" * 50,
221
+ f"Sample Size: {dist.sample_count} pending transactions",
222
+ "",
223
+ f" Min: {dist.min_gwei:6.1f} gwei",
224
+ f" 10th%: {dist.p10_gwei:6.1f} gwei (Slow)",
225
+ f" 25th%: {dist.p25_gwei:6.1f} gwei",
226
+ f" 50th%: {dist.p50_gwei:6.1f} gwei (Standard)",
227
+ f" 75th%: {dist.p75_gwei:6.1f} gwei (Fast)",
228
+ f" 90th%: {dist.p90_gwei:6.1f} gwei (Instant)",
229
+ f" Max: {dist.max_gwei:6.1f} gwei",
230
+ "",
231
+ f" Mean: {dist.mean_gwei:6.1f} gwei",
232
+ f" Median: {dist.median_gwei:6.1f} gwei",
233
+ "=" * 50,
234
+ ]
235
+ return "\n".join(lines)
236
+
237
+ def format_recommendations(self, base_fee: int = None) -> str:
238
+ """Format all gas recommendations for display.
239
+
240
+ Args:
241
+ base_fee: Current base fee in wei
242
+
243
+ Returns:
244
+ Formatted string
245
+ """
246
+ lines = [
247
+ "",
248
+ "GAS RECOMMENDATIONS",
249
+ "=" * 60,
250
+ ]
251
+
252
+ if base_fee:
253
+ lines.append(f"Current Base Fee: {base_fee / 10**9:.1f} gwei")
254
+ lines.append("")
255
+
256
+ lines.append(f"{'Priority':<12} {'Max Fee':<12} {'Priority Fee':<14} {'Est. Wait':<20}")
257
+ lines.append("-" * 60)
258
+
259
+ for priority in ["slow", "standard", "fast", "instant"]:
260
+ rec = self.recommend_gas(priority=priority, base_fee=base_fee)
261
+ max_fee_gwei = rec.max_fee / 10**9
262
+ priority_gwei = rec.priority_fee / 10**9
263
+ lines.append(
264
+ f"{priority.capitalize():<12} {max_fee_gwei:<12.1f} {priority_gwei:<14.1f} {rec.estimated_wait:<20}"
265
+ )
266
+
267
+ lines.append("=" * 60)
268
+ return "\n".join(lines)
269
+
270
+
271
+ def main():
272
+ """CLI entry point for testing."""
273
+ analyzer = GasAnalyzer(verbose=True)
274
+
275
+ # Create mock pending transactions
276
+ class MockTx:
277
+ def __init__(self, gas_price):
278
+ self.gas_price = gas_price
279
+
280
+ import random
281
+ base = 30 * 10**9
282
+ mock_txs = [MockTx(base + random.randint(-10, 30) * 10**9) for _ in range(100)]
283
+
284
+ # Analyze
285
+ dist = analyzer.analyze_pending_gas(mock_txs, base_fee=base)
286
+ print(analyzer.format_distribution(dist))
287
+
288
+ # Recommendations
289
+ print(analyzer.format_recommendations(base_fee=base))
290
+
291
+ # Test inclusion estimate
292
+ print("\n=== Inclusion Time Estimates ===")
293
+ for gwei in [25, 30, 40, 50, 70]:
294
+ est = analyzer.estimate_inclusion_time(gwei * 10**9, dist)
295
+ print(f" {gwei} gwei: {est}")
296
+
297
+
298
+ if __name__ == "__main__":
299
+ main()
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mempool Analyzer CLI
4
+
5
+ Monitor blockchain mempools for pending transactions and MEV opportunities.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+
11
+ Usage:
12
+ python mempool_analyzer.py pending # View pending transactions
13
+ python mempool_analyzer.py gas # Gas price analysis
14
+ python mempool_analyzer.py swaps # Pending DEX swaps
15
+ python mempool_analyzer.py mev # MEV opportunity scan
16
+ python mempool_analyzer.py summary # Overall summary
17
+ """
18
+
19
+ import argparse
20
+ import sys
21
+
22
+ from rpc_client import RPCClient
23
+ from tx_decoder import TransactionDecoder
24
+ from gas_analyzer import GasAnalyzer
25
+ from mev_detector import MEVDetector
26
+ from formatters import (
27
+ format_pending_tx_table,
28
+ format_pending_swaps_table,
29
+ format_mempool_summary,
30
+ format_json,
31
+ )
32
+
33
+
34
+ DEFAULT_ETH_PRICE = 3000.0
35
+
36
+
37
+ def cmd_pending(args):
38
+ """Show pending transactions."""
39
+ client = RPCClient(
40
+ rpc_url=args.rpc_url,
41
+ chain=args.chain,
42
+ verbose=args.verbose,
43
+ )
44
+
45
+ pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
46
+
47
+ if args.format == "json":
48
+ print(format_json(pending))
49
+ else:
50
+ print(format_pending_tx_table(pending, eth_price=args.eth_price))
51
+
52
+
53
+ def cmd_gas(args):
54
+ """Analyze gas prices."""
55
+ client = RPCClient(
56
+ rpc_url=args.rpc_url,
57
+ chain=args.chain,
58
+ verbose=args.verbose,
59
+ )
60
+ analyzer = GasAnalyzer(verbose=args.verbose)
61
+
62
+ # Get gas info
63
+ gas_info = client.get_gas_price()
64
+
65
+ # Get pending transactions for distribution analysis
66
+ pending = client.get_pending_transactions(limit=200, allow_mock=args.demo)
67
+ distribution = analyzer.analyze_pending_gas(pending, base_fee=gas_info.base_fee)
68
+
69
+ if args.format == "json":
70
+ result = {
71
+ "gas_info": vars(gas_info),
72
+ "distribution": vars(distribution),
73
+ }
74
+ print(format_json(result))
75
+ else:
76
+ print(analyzer.format_distribution(distribution))
77
+ print(analyzer.format_recommendations(base_fee=gas_info.base_fee))
78
+
79
+
80
+ def cmd_swaps(args):
81
+ """Show pending DEX swaps."""
82
+ client = RPCClient(
83
+ rpc_url=args.rpc_url,
84
+ chain=args.chain,
85
+ verbose=args.verbose,
86
+ )
87
+ detector = MEVDetector(verbose=args.verbose)
88
+
89
+ pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
90
+ swaps = detector.detect_pending_swaps(pending, eth_price=args.eth_price)
91
+
92
+ if args.format == "json":
93
+ print(format_json(swaps))
94
+ else:
95
+ print(format_pending_swaps_table(swaps))
96
+
97
+
98
+ def cmd_mev(args):
99
+ """Scan for MEV opportunities."""
100
+ client = RPCClient(
101
+ rpc_url=args.rpc_url,
102
+ chain=args.chain,
103
+ verbose=args.verbose,
104
+ )
105
+ detector = MEVDetector(verbose=args.verbose)
106
+
107
+ pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
108
+ results = detector.detect_all_opportunities(pending, eth_price=args.eth_price)
109
+
110
+ if args.format == "json":
111
+ # Convert opportunities to serializable format
112
+ serialized = {
113
+ "pending_swaps": results["pending_swaps"],
114
+ "sandwich": [vars(o) for o in results["sandwich"]],
115
+ "arbitrage": [vars(o) for o in results["arbitrage"]],
116
+ "liquidation": [vars(o) for o in results["liquidation"]],
117
+ }
118
+ print(format_json(serialized))
119
+ else:
120
+ print(f"\nPending Swaps Analyzed: {results['pending_swaps']}")
121
+
122
+ all_opps = results["sandwich"] + results["arbitrage"] + results["liquidation"]
123
+ print(detector.format_opportunities(all_opps))
124
+
125
+
126
+ def cmd_summary(args):
127
+ """Show mempool summary."""
128
+ client = RPCClient(
129
+ rpc_url=args.rpc_url,
130
+ chain=args.chain,
131
+ verbose=args.verbose,
132
+ )
133
+ detector = MEVDetector(verbose=args.verbose)
134
+
135
+ # Gather data
136
+ gas_info = client.get_gas_price()
137
+ pending = client.get_pending_transactions(limit=200, allow_mock=args.demo)
138
+ swaps = detector.detect_pending_swaps(pending, eth_price=args.eth_price)
139
+ results = detector.detect_all_opportunities(pending, eth_price=args.eth_price)
140
+
141
+ total_opportunities = (
142
+ len(results["sandwich"]) +
143
+ len(results["arbitrage"]) +
144
+ len(results["liquidation"])
145
+ )
146
+
147
+ if args.format == "json":
148
+ summary = {
149
+ "pending_count": len(pending),
150
+ "swap_count": len(swaps),
151
+ "opportunity_count": total_opportunities,
152
+ "gas_info": vars(gas_info),
153
+ }
154
+ print(format_json(summary))
155
+ else:
156
+ print(format_mempool_summary(
157
+ pending_count=len(pending),
158
+ gas_info=gas_info,
159
+ swap_count=len(swaps),
160
+ opportunities=total_opportunities,
161
+ ))
162
+
163
+
164
+ def cmd_watch(args):
165
+ """Watch contract for pending interactions."""
166
+ client = RPCClient(
167
+ rpc_url=args.rpc_url,
168
+ chain=args.chain,
169
+ verbose=args.verbose,
170
+ )
171
+ decoder = TransactionDecoder(verbose=args.verbose)
172
+
173
+ contract = args.contract.lower()
174
+
175
+ print(f"\nWatching for pending transactions to: {contract}")
176
+ print("=" * 60)
177
+
178
+ pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
179
+
180
+ matching = [
181
+ tx for tx in pending
182
+ if tx.to_address and tx.to_address.lower() == contract
183
+ ]
184
+
185
+ if not matching:
186
+ print("No pending transactions found for this contract.")
187
+ return
188
+
189
+ if args.format == "json":
190
+ print(format_json(matching))
191
+ else:
192
+ print(f"Found {len(matching)} pending transactions:\n")
193
+ for tx in matching:
194
+ decoded = decoder.decode_input(tx.input_data, tx.to_address)
195
+ print(f" {tx.hash[:16]}...")
196
+ print(f" Method: {decoded.method_name}")
197
+ print(f" From: {tx.from_address[:16]}...")
198
+ print(f" Gas: {tx.gas_price / 10**9:.1f} gwei")
199
+ print()
200
+
201
+
202
+ def cmd_status(args):
203
+ """Show connection status."""
204
+ client = RPCClient(
205
+ rpc_url=args.rpc_url,
206
+ chain=args.chain,
207
+ verbose=args.verbose,
208
+ )
209
+
210
+ print("\nMEMPOOL ANALYZER STATUS")
211
+ print("=" * 50)
212
+ print(f"Chain: {args.chain}")
213
+ print(f"RPC URL: {client.rpc_url[:50]}...")
214
+
215
+ try:
216
+ block = client.get_block_number()
217
+ print(f"Current Block: {block:,}")
218
+ print("Connection: OK")
219
+ except Exception as e:
220
+ print(f"Connection: Error - {e}")
221
+
222
+ gas_info = client.get_gas_price()
223
+ print(f"\nGas Price: {gas_info.gas_price / 10**9:.1f} gwei")
224
+ print(f"Base Fee: {gas_info.base_fee / 10**9:.1f} gwei")
225
+ print("=" * 50)
226
+
227
+
228
+ def main():
229
+ """Main entry point."""
230
+ parser = argparse.ArgumentParser(
231
+ description="Mempool Analyzer - Monitor pending transactions and MEV",
232
+ formatter_class=argparse.RawDescriptionHelpFormatter,
233
+ epilog="""
234
+ Examples:
235
+ %(prog)s pending View pending transactions
236
+ %(prog)s gas Analyze gas prices
237
+ %(prog)s swaps Show pending DEX swaps
238
+ %(prog)s mev Scan for MEV opportunities
239
+ %(prog)s summary Overall mempool summary
240
+ %(prog)s watch 0x7a250d... Watch contract for pending txs
241
+ %(prog)s status Check connection status
242
+ %(prog)s --demo pending Use mock data for testing
243
+ """
244
+ )
245
+
246
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
247
+ parser.add_argument("--format", choices=["table", "json"], default="table",
248
+ help="Output format")
249
+ parser.add_argument("--rpc-url", help="Custom RPC URL")
250
+ parser.add_argument("--chain", default="ethereum",
251
+ choices=["ethereum", "polygon", "arbitrum", "optimism", "base"],
252
+ help="Blockchain network")
253
+ parser.add_argument("--eth-price", type=float, default=DEFAULT_ETH_PRICE,
254
+ help=f"ETH price for USD conversion (default: {DEFAULT_ETH_PRICE})")
255
+ parser.add_argument("--demo", action="store_true",
256
+ help="Use mock data when RPC fails (for testing/demo)")
257
+
258
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
259
+
260
+ # pending command
261
+ pending_parser = subparsers.add_parser("pending", help="View pending transactions")
262
+ pending_parser.add_argument("--limit", type=int, default=50,
263
+ help="Max transactions to show")
264
+
265
+ # gas command
266
+ subparsers.add_parser("gas", help="Analyze gas prices")
267
+
268
+ # swaps command
269
+ swaps_parser = subparsers.add_parser("swaps", help="Show pending DEX swaps")
270
+ swaps_parser.add_argument("--limit", type=int, default=100,
271
+ help="Max transactions to analyze")
272
+
273
+ # mev command
274
+ mev_parser = subparsers.add_parser("mev", help="Scan for MEV opportunities")
275
+ mev_parser.add_argument("--limit", type=int, default=200,
276
+ help="Max transactions to analyze")
277
+
278
+ # summary command
279
+ subparsers.add_parser("summary", help="Mempool summary")
280
+
281
+ # watch command
282
+ watch_parser = subparsers.add_parser("watch", help="Watch contract for pending txs")
283
+ watch_parser.add_argument("contract", help="Contract address to watch")
284
+ watch_parser.add_argument("--limit", type=int, default=100,
285
+ help="Max transactions to check")
286
+
287
+ # status command
288
+ subparsers.add_parser("status", help="Check connection status")
289
+
290
+ args = parser.parse_args()
291
+
292
+ if not args.command:
293
+ # Default to summary
294
+ args.command = "summary"
295
+
296
+ commands = {
297
+ "pending": cmd_pending,
298
+ "gas": cmd_gas,
299
+ "swaps": cmd_swaps,
300
+ "mev": cmd_mev,
301
+ "summary": cmd_summary,
302
+ "watch": cmd_watch,
303
+ "status": cmd_status,
304
+ }
305
+
306
+ try:
307
+ commands[args.command](args)
308
+ except KeyboardInterrupt:
309
+ print("\nInterrupted")
310
+ sys.exit(1)
311
+ except Exception as e:
312
+ print(f"Error: {e}")
313
+ if args.verbose:
314
+ import traceback
315
+ traceback.print_exc()
316
+ sys.exit(1)
317
+
318
+
319
+ if __name__ == "__main__":
320
+ main()