mxcpctl 0.0.1__py3-none-any.whl

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.
mxcpctl/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """mxcpctl - CLI tool for MXCP instance management."""
2
+
3
+ __version__ = "0.1.0"
mxcpctl/cli.py ADDED
@@ -0,0 +1,596 @@
1
+ """CLI commands for mxcpctl.
2
+
3
+ This module provides the main command-line interface for managing
4
+ MXCP instances through the mxcpd API.
5
+ """
6
+
7
+ import sys
8
+
9
+ import click
10
+ import httpx
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ def get_headers(token: str | None) -> dict[str, str]:
17
+ """Get HTTP headers with optional authentication token.
18
+
19
+ Args:
20
+ token: Optional API token for Bearer authentication
21
+
22
+ Returns:
23
+ Dictionary of HTTP headers
24
+ """
25
+ headers = {}
26
+ if token:
27
+ headers["Authorization"] = f"Bearer {token}"
28
+ return headers
29
+
30
+
31
+ @click.group()
32
+ @click.option("--host", default="localhost", envvar="MXCPCTL_HOST", help="mxcpd host")
33
+ @click.option("--port", default=8000, envvar="MXCPCTL_PORT", help="mxcpd port")
34
+ @click.option("--tls/--no-tls", default=False, help="Use HTTPS")
35
+ @click.option("--token", envvar="MXCPCTL_TOKEN", help="API token for authentication")
36
+ @click.version_option(version="0.1.0")
37
+ @click.pass_context
38
+ def main(ctx: click.Context, host: str, port: int, tls: bool, token: str | None) -> None:
39
+ """mxcpctl - MXCP instance management CLI.
40
+
41
+ Communicates with mxcpd to monitor and control MXCP instances.
42
+ """
43
+ # Store connection info in context
44
+ ctx.ensure_object(dict)
45
+ ctx.obj["host"] = host
46
+ ctx.obj["port"] = port
47
+ ctx.obj["base_url"] = f"{'https' if tls else 'http'}://{host}:{port}"
48
+ ctx.obj["token"] = token
49
+
50
+
51
+ @main.command()
52
+ @click.pass_context
53
+ def health(ctx: click.Context) -> None:
54
+ """Check mxcpd health."""
55
+ base_url = ctx.obj["base_url"]
56
+ token = ctx.obj.get("token")
57
+
58
+ try:
59
+ response = httpx.get(f"{base_url}/health", headers=get_headers(token), timeout=5.0)
60
+ response.raise_for_status()
61
+ data = response.json()
62
+
63
+ console.print("[green]✓[/green] mxcpd is healthy")
64
+ console.print(f" Version: {data['version']}")
65
+ console.print(f" Instance: {data['instance']}")
66
+ console.print(f" Environment: {data['environment']}")
67
+ console.print(f" Status: {data['status']}")
68
+
69
+ except httpx.ConnectError:
70
+ console.print(f"[red]✗[/red] Failed to connect to {base_url}")
71
+ console.print(" Is mxcpd running?")
72
+ sys.exit(1)
73
+ except httpx.TimeoutException:
74
+ console.print("[red]✗[/red] Connection timeout")
75
+ sys.exit(1)
76
+ except httpx.HTTPStatusError as e:
77
+ console.print(f"[red]✗[/red] HTTP error: {e.response.status_code}")
78
+ sys.exit(1)
79
+ except Exception as e:
80
+ console.print(f"[red]✗[/red] Unexpected error: {e}")
81
+ sys.exit(1)
82
+
83
+
84
+ @main.command()
85
+ @click.option("--instance", "-i", help="Filter by instance ID")
86
+ @click.pass_context
87
+ def status(ctx: click.Context, instance: str | None) -> None:
88
+ """Get instance status."""
89
+ base_url = ctx.obj["base_url"]
90
+ token = ctx.obj.get("token")
91
+ params = {"instance": instance} if instance else {}
92
+
93
+ try:
94
+ response = httpx.get(
95
+ f"{base_url}/api/v1/status", params=params, headers=get_headers(token), timeout=10.0
96
+ )
97
+ response.raise_for_status()
98
+ data = response.json()
99
+
100
+ # Multi-instance response is a list
101
+ for instance_data in data:
102
+ console.print(
103
+ f"\n[cyan]Instance: {instance_data['instance_name']}[/cyan] ({instance_data['instance_id']})" # noqa: E501
104
+ )
105
+ console.print(f" Status: [green]{instance_data.get('status', 'unknown')}[/green]")
106
+ console.print(f" Version: {instance_data.get('version', 'unknown')}")
107
+ console.print(f" Uptime: {instance_data.get('uptime', 'unknown')}")
108
+ console.print(f" Profile: {instance_data.get('profile', 'unknown')}")
109
+ console.print(f" Mode: {instance_data.get('mode', 'unknown')}")
110
+
111
+ if instance_data.get("error"):
112
+ console.print(f" [red]Error: {instance_data['error']}[/red]")
113
+
114
+ except httpx.RequestError as e:
115
+ console.print(f"[red]✗[/red] Request failed: {e}")
116
+ sys.exit(1)
117
+
118
+
119
+ @main.command()
120
+ @click.option("--instance", "-i", help="Filter by instance ID")
121
+ @click.pass_context
122
+ def config(ctx: click.Context, instance: str | None) -> None:
123
+ """Get instance configuration."""
124
+ base_url = ctx.obj["base_url"]
125
+ token = ctx.obj.get("token")
126
+ params = {"instance": instance} if instance else {}
127
+
128
+ try:
129
+ response = httpx.get(
130
+ f"{base_url}/api/v1/config", params=params, headers=get_headers(token), timeout=10.0
131
+ )
132
+ response.raise_for_status()
133
+ data = response.json()
134
+
135
+ for config_data in data:
136
+ console.print(
137
+ f"\n[cyan]Instance: {config_data['instance_name']}[/cyan] ({config_data['instance_id']})" # noqa: E501
138
+ )
139
+ console.print(f" Profile: {config_data.get('profile', 'N/A')}")
140
+ console.print(f" Environment: {config_data.get('environment', 'N/A')}")
141
+ console.print(f" Read-only: {config_data.get('readonly', False)}")
142
+ console.print(f" Debug: {config_data.get('debug', False)}")
143
+
144
+ if config_data.get("features"):
145
+ features = config_data["features"]
146
+ console.print(" Features:")
147
+ console.print(f" SQL Tools: {features.get('sql_tools', False)}")
148
+ console.print(f" Audit Logging: {features.get('audit_logging', False)}")
149
+ console.print(f" Telemetry: {features.get('telemetry', False)}")
150
+
151
+ except httpx.RequestError as e:
152
+ console.print(f"[red]✗[/red] Request failed: {e}")
153
+ sys.exit(1)
154
+
155
+
156
+ @main.command()
157
+ @click.option("--instance", "-i", help="Filter by instance ID")
158
+ @click.pass_context
159
+ def reload(ctx: click.Context, instance: str | None) -> None:
160
+ """Trigger configuration reload."""
161
+ base_url = ctx.obj["base_url"]
162
+ token = ctx.obj.get("token")
163
+ params = {"instance": instance} if instance else {}
164
+
165
+ try:
166
+ response = httpx.post(
167
+ f"{base_url}/api/v1/reload", params=params, headers=get_headers(token), timeout=30.0
168
+ )
169
+ response.raise_for_status()
170
+ data = response.json()
171
+
172
+ for reload_data in data:
173
+ instance_name = reload_data.get("instance_name", "unknown")
174
+ status = reload_data.get("status", "unknown")
175
+
176
+ if status == "success":
177
+ console.print(f"[green]✓[/green] Reload triggered for {instance_name}")
178
+ if reload_data.get("request_id"):
179
+ console.print(f" Request ID: {reload_data['request_id']}")
180
+ else:
181
+ console.print(f"[red]✗[/red] Reload failed for {instance_name}")
182
+ if reload_data.get("error"):
183
+ console.print(f" Error: {reload_data['error']}")
184
+
185
+ except httpx.RequestError as e:
186
+ console.print(f"[red]✗[/red] Request failed: {e}")
187
+ sys.exit(1)
188
+
189
+
190
+ @main.group()
191
+ def endpoints() -> None:
192
+ """Manage endpoints."""
193
+ pass
194
+
195
+
196
+ @endpoints.command("list")
197
+ @click.option("--instance", "-i", help="Filter by instance ID")
198
+ @click.option(
199
+ "--type",
200
+ "-t",
201
+ type=click.Choice(["tool", "resource", "prompt"]),
202
+ help="Filter by endpoint type",
203
+ )
204
+ @click.pass_context
205
+ def list_endpoints(ctx: click.Context, instance: str | None, type: str | None) -> None:
206
+ """List all endpoints."""
207
+ base_url = ctx.obj["base_url"]
208
+ token = ctx.obj.get("token")
209
+ params = {}
210
+ if instance:
211
+ params["instance"] = instance
212
+
213
+ try:
214
+ response = httpx.get(
215
+ f"{base_url}/api/v1/endpoints", params=params, headers=get_headers(token), timeout=10.0
216
+ )
217
+ response.raise_for_status()
218
+ data = response.json()
219
+
220
+ for instance_data in data:
221
+ console.print(
222
+ f"\n[cyan]Instance: {instance_data['instance_name']}[/cyan] ({instance_data['instance_id']})" # noqa: E501
223
+ )
224
+
225
+ endpoints_list = instance_data.get("endpoints", [])
226
+
227
+ # Filter by type if specified
228
+ if type:
229
+ endpoints_list = [ep for ep in endpoints_list if ep.get("type") == type]
230
+
231
+ if not endpoints_list:
232
+ console.print(" No endpoints found")
233
+ continue
234
+
235
+ # Group by type
236
+ by_type: dict[str, list] = {}
237
+ for ep in endpoints_list:
238
+ ep_type = ep.get("type", "unknown")
239
+ if ep_type not in by_type:
240
+ by_type[ep_type] = []
241
+ by_type[ep_type].append(ep)
242
+
243
+ for ep_type, eps in by_type.items():
244
+ console.print(f"\n [yellow]{ep_type.upper()}S[/yellow] ({len(eps)})")
245
+ for ep in eps:
246
+ name = ep.get("name", "unknown")
247
+ enabled = ep.get("enabled", False)
248
+ status = ep.get("status", "unknown")
249
+
250
+ status_icon = "✓" if status == "ok" else "✗"
251
+ status_color = "green" if status == "ok" else "red"
252
+
253
+ console.print(f" [{status_color}]{status_icon}[/{status_color}] {name}")
254
+ if ep.get("description"):
255
+ console.print(f" {ep['description']}")
256
+ if not enabled:
257
+ console.print(" [dim](disabled)[/dim]")
258
+ if ep.get("error"):
259
+ console.print(f" [red]Error: {ep['error']}[/red]")
260
+
261
+ except httpx.RequestError as e:
262
+ console.print(f"[red]✗[/red] Request failed: {e}")
263
+ sys.exit(1)
264
+
265
+
266
+ @main.group()
267
+ def audit() -> None:
268
+ """Query audit logs."""
269
+ pass
270
+
271
+
272
+ @audit.command("query")
273
+ @click.option("--instance", "-i", help="Filter by instance ID")
274
+ @click.option("--operation-type", help="Filter by operation type (tool, resource, prompt)")
275
+ @click.option("--operation-name", help="Filter by operation name")
276
+ @click.option("--status", type=click.Choice(["success", "error"]), help="Filter by status")
277
+ @click.option("--user-id", help="Filter by user ID")
278
+ @click.option("--limit", default=10, help="Maximum number of records (default: 10)")
279
+ @click.option("--offset", default=0, help="Number of records to skip")
280
+ @click.pass_context
281
+ def query_audit(
282
+ ctx: click.Context,
283
+ instance: str | None,
284
+ operation_type: str | None,
285
+ operation_name: str | None,
286
+ status: str | None,
287
+ user_id: str | None,
288
+ limit: int,
289
+ offset: int,
290
+ ) -> None:
291
+ """Query audit logs with filters."""
292
+ base_url = ctx.obj["base_url"]
293
+ token = ctx.obj.get("token")
294
+ params: dict[str, str | int] = {"limit": limit, "offset": offset}
295
+
296
+ if instance:
297
+ params["instance"] = instance
298
+ if operation_type:
299
+ params["operation_type"] = operation_type
300
+ if operation_name:
301
+ params["operation_name"] = operation_name
302
+ if status:
303
+ params["operation_status"] = status
304
+ if user_id:
305
+ params["user_id"] = user_id
306
+
307
+ try:
308
+ response = httpx.get(
309
+ f"{base_url}/api/v1/audit/query",
310
+ params=params,
311
+ headers=get_headers(token),
312
+ timeout=30.0,
313
+ )
314
+ response.raise_for_status()
315
+ data = response.json()
316
+
317
+ for instance_data in data:
318
+ console.print(
319
+ f"\n[cyan]Instance: {instance_data['instance_name']}[/cyan] ({instance_data['instance_id']})" # noqa: E501
320
+ )
321
+
322
+ records = instance_data.get("records", [])
323
+ count = instance_data.get("count", 0)
324
+
325
+ console.print(f" Found {count} record(s)")
326
+
327
+ if not records:
328
+ continue
329
+
330
+ for record in records:
331
+ timestamp = record.get("timestamp", "N/A")
332
+ op_type = record.get("operation_type", "N/A")
333
+ op_name = record.get("operation_name", "N/A")
334
+ op_status = record.get("operation_status", "N/A")
335
+ duration = record.get("duration_ms")
336
+
337
+ status_icon = "✓" if op_status == "success" else "✗"
338
+ status_color = "green" if op_status == "success" else "red"
339
+
340
+ console.print(
341
+ f"\n [{status_color}]{status_icon}[/{status_color}] {op_type}/{op_name}"
342
+ )
343
+ console.print(f" Time: {timestamp}")
344
+ console.print(f" Status: {op_status}")
345
+ if duration is not None:
346
+ console.print(f" Duration: {duration}ms")
347
+ if record.get("user_id"):
348
+ console.print(f" User: {record['user_id']}")
349
+ if record.get("error_message"):
350
+ console.print(f" [red]Error: {record['error_message']}[/red]")
351
+
352
+ except httpx.RequestError as e:
353
+ console.print(f"[red]✗[/red] Request failed: {e}")
354
+ sys.exit(1)
355
+
356
+
357
+ @audit.command("stats")
358
+ @click.option("--instance", "-i", help="Filter by instance ID")
359
+ @click.pass_context
360
+ def audit_stats(ctx: click.Context, instance: str | None) -> None:
361
+ """Get audit log statistics."""
362
+ base_url = ctx.obj["base_url"]
363
+ token = ctx.obj.get("token")
364
+ params = {"instance": instance} if instance else {}
365
+
366
+ try:
367
+ response = httpx.get(
368
+ f"{base_url}/api/v1/audit/stats",
369
+ params=params,
370
+ headers=get_headers(token),
371
+ timeout=10.0,
372
+ )
373
+ response.raise_for_status()
374
+ data = response.json()
375
+
376
+ for instance_data in data:
377
+ console.print(
378
+ f"\n[cyan]Instance: {instance_data['instance_name']}[/cyan] ({instance_data['instance_id']})" # noqa: E501
379
+ )
380
+
381
+ total = instance_data.get("total_records", 0)
382
+ console.print(f" Total Records: {total}")
383
+
384
+ if instance_data.get("by_type"):
385
+ console.print("\n By Type:")
386
+ for op_type, count in instance_data["by_type"].items():
387
+ console.print(f" {op_type}: {count}")
388
+
389
+ if instance_data.get("by_status"):
390
+ console.print("\n By Status:")
391
+ for status, count in instance_data["by_status"].items():
392
+ status_color = "green" if status == "success" else "red"
393
+ console.print(f" [{status_color}]{status}[/{status_color}]: {count}")
394
+
395
+ if instance_data.get("earliest_timestamp"):
396
+ console.print("\n Time Range:")
397
+ console.print(f" Earliest: {instance_data['earliest_timestamp']}")
398
+ console.print(f" Latest: {instance_data.get('latest_timestamp', 'N/A')}")
399
+
400
+ except httpx.RequestError as e:
401
+ console.print(f"[red]✗[/red] Request failed: {e}")
402
+ sys.exit(1)
403
+
404
+
405
+ @main.group()
406
+ def telemetry() -> None:
407
+ """Query telemetry data (traces and metrics)."""
408
+ pass
409
+
410
+
411
+ @telemetry.command("status")
412
+ @click.pass_context
413
+ def telemetry_status(ctx: click.Context) -> None:
414
+ """Get telemetry receiver status."""
415
+ base_url = ctx.obj["base_url"]
416
+ token = ctx.obj.get("token")
417
+
418
+ try:
419
+ response = httpx.get(
420
+ f"{base_url}/api/v1/telemetry/status", headers=get_headers(token), timeout=10.0
421
+ )
422
+ response.raise_for_status()
423
+ result = response.json()
424
+ data = result.get("data", {})
425
+
426
+ console.print("\n[cyan]Telemetry Receiver Status[/cyan]")
427
+ console.print(f" Enabled: {'Yes' if data.get('enabled') else 'No'}")
428
+ console.print(f" Traces Received: {data.get('traces_received', 0)}")
429
+ console.print(f" Traces Stored: {data.get('traces_stored', 0)}")
430
+ console.print(f" Metrics Received: {data.get('metrics_received', 0)}")
431
+ console.print(f" Storage Usage: {data.get('storage_usage_mb', 0):.2f} MB")
432
+
433
+ if data.get("last_trace_time"):
434
+ console.print(f" Last Trace: {data['last_trace_time']}")
435
+
436
+ except httpx.RequestError as e:
437
+ console.print(f"[red]✗[/red] Request failed: {e}")
438
+ sys.exit(1)
439
+
440
+
441
+ @telemetry.command("traces")
442
+ @click.option("--limit", default=20, help="Maximum number of traces to show")
443
+ @click.option("--endpoint", help="Filter by endpoint name")
444
+ @click.pass_context
445
+ def list_traces(ctx: click.Context, limit: int, endpoint: str | None) -> None:
446
+ """List recent traces."""
447
+ base_url = ctx.obj["base_url"]
448
+ token = ctx.obj.get("token")
449
+ params: dict[str, str | int] = {"limit": limit}
450
+
451
+ if endpoint:
452
+ params["endpoint_name"] = endpoint
453
+
454
+ try:
455
+ response = httpx.get(
456
+ f"{base_url}/api/v1/telemetry/traces",
457
+ params=params,
458
+ headers=get_headers(token),
459
+ timeout=10.0,
460
+ )
461
+ response.raise_for_status()
462
+ result = response.json()
463
+ data = result.get("data", {})
464
+ traces = data.get("items", [])
465
+
466
+ if not traces:
467
+ console.print("No traces found")
468
+ return
469
+
470
+ console.print(f"\n[cyan]Recent Traces[/cyan] (showing {len(traces)})")
471
+
472
+ for trace in traces:
473
+ trace_id = trace.get("trace_id", "N/A")
474
+ endpoint_name = trace.get("endpoint_name", "N/A")
475
+ duration = trace.get("duration_ms", 0)
476
+ status = trace.get("status", "unknown")
477
+ span_count = trace.get("span_count", 0)
478
+
479
+ status_icon = "✓" if status == "ok" else "✗"
480
+ status_color = "green" if status == "ok" else "red"
481
+
482
+ console.print(f"\n [{status_color}]{status_icon}[/{status_color}] {endpoint_name}")
483
+ console.print(f" Trace ID: {trace_id}")
484
+ console.print(f" Duration: {duration:.2f}ms")
485
+ console.print(f" Spans: {span_count}")
486
+ console.print(f" Time: {trace.get('start_time', 'N/A')}")
487
+
488
+ except httpx.RequestError as e:
489
+ console.print(f"[red]✗[/red] Request failed: {e}")
490
+ sys.exit(1)
491
+
492
+
493
+ @telemetry.command("trace")
494
+ @click.argument("trace_id")
495
+ @click.pass_context
496
+ def get_trace(ctx: click.Context, trace_id: str) -> None:
497
+ """Get detailed trace information."""
498
+ base_url = ctx.obj["base_url"]
499
+ token = ctx.obj.get("token")
500
+
501
+ try:
502
+ response = httpx.get(
503
+ f"{base_url}/api/v1/telemetry/traces/{trace_id}",
504
+ headers=get_headers(token),
505
+ timeout=10.0,
506
+ )
507
+ response.raise_for_status()
508
+ result = response.json()
509
+ trace = result.get("data", {})
510
+
511
+ console.print("\n[cyan]Trace Details[/cyan]")
512
+ console.print(f" Trace ID: {trace.get('trace_id', 'N/A')}")
513
+ console.print(f" Endpoint: {trace.get('endpoint_name', 'N/A')}")
514
+ console.print(f" Service: {trace.get('service_name', 'N/A')}")
515
+ console.print(f" Duration: {trace.get('duration_ms', 0):.2f}ms")
516
+ console.print(f" Status: {trace.get('status', 'unknown')}")
517
+ console.print(f" Span Count: {trace.get('span_count', 0)}")
518
+
519
+ spans = trace.get("spans", [])
520
+ if spans:
521
+ console.print("\n [yellow]Spans[/yellow]:")
522
+ for span in spans:
523
+ indent = " " if span.get("parent_span_id") else " "
524
+ name = span.get("name", "N/A")
525
+ duration = span.get("duration_ms", 0)
526
+ console.print(f"{indent}• {name} ({duration:.2f}ms)")
527
+
528
+ except httpx.HTTPStatusError as e:
529
+ if e.response.status_code == 404:
530
+ console.print(f"[red]✗[/red] Trace not found: {trace_id}")
531
+ else:
532
+ console.print(f"[red]✗[/red] HTTP error: {e.response.status_code}")
533
+ sys.exit(1)
534
+ except httpx.RequestError as e:
535
+ console.print(f"[red]✗[/red] Request failed: {e}")
536
+ sys.exit(1)
537
+
538
+
539
+ @telemetry.command("metrics")
540
+ @click.option("--endpoint", help="Filter by endpoint name")
541
+ @click.option("--window", type=int, help="Time window in hours (1-24)")
542
+ @click.pass_context
543
+ def get_metrics(ctx: click.Context, endpoint: str | None, window: int | None) -> None:
544
+ """Get aggregated performance metrics."""
545
+ base_url = ctx.obj["base_url"]
546
+ token = ctx.obj.get("token")
547
+ params: dict[str, str | int] = {}
548
+
549
+ if endpoint:
550
+ params["endpoint_name"] = endpoint
551
+ if window:
552
+ params["window_hours"] = window
553
+
554
+ try:
555
+ response = httpx.get(
556
+ f"{base_url}/api/v1/telemetry/metrics",
557
+ params=params,
558
+ headers=get_headers(token),
559
+ timeout=10.0,
560
+ )
561
+ response.raise_for_status()
562
+ result = response.json()
563
+ data = result.get("data", {})
564
+ metrics = data.get("metrics", [])
565
+
566
+ if not metrics:
567
+ console.print("No metrics found")
568
+ return
569
+
570
+ window_str = f"{window}h" if window else "all time"
571
+ console.print(f"\n[cyan]Performance Metrics[/cyan] ({window_str})")
572
+
573
+ for metric in metrics:
574
+ endpoint_name = metric.get("endpoint_name", "N/A")
575
+ requests = metric.get("request_count", 0)
576
+ errors = metric.get("error_count", 0)
577
+ error_rate = metric.get("error_rate", 0) * 100
578
+
579
+ console.print(f"\n [yellow]{endpoint_name}[/yellow]")
580
+ console.print(f" Requests: {requests}")
581
+ console.print(f" Errors: {errors} ({error_rate:.1f}%)")
582
+
583
+ if metric.get("p50_ms") is not None:
584
+ console.print(" Latency:")
585
+ console.print(f" P50: {metric['p50_ms']:.2f}ms")
586
+ console.print(f" P95: {metric.get('p95_ms', 0):.2f}ms")
587
+ console.print(f" P99: {metric.get('p99_ms', 0):.2f}ms")
588
+ console.print(f" Avg: {metric.get('avg_ms', 0):.2f}ms")
589
+
590
+ except httpx.RequestError as e:
591
+ console.print(f"[red]✗[/red] Request failed: {e}")
592
+ sys.exit(1)
593
+
594
+
595
+ if __name__ == "__main__":
596
+ main()
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: mxcpctl
3
+ Version: 0.0.1
4
+ Summary: CLI tool for managing MXCP instances via mxcpd
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: click>=8.1.0
7
+ Requires-Dist: httpx>=0.25.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: pyyaml>=6.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
12
+ Requires-Dist: black>=23.0.0; extra == "dev"
13
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
14
+ Requires-Dist: mypy>=1.6.0; extra == "dev"
15
+ Requires-Dist: build>=1.0.0; extra == "dev"
16
+ Requires-Dist: twine>=4.0.0; extra == "dev"
@@ -0,0 +1,7 @@
1
+ mxcpctl/__init__.py,sha256=tQqWLRLxhYv38rMhgMkvWqLgUmStzsn438kxHNlaQ0A,78
2
+ mxcpctl/cli.py,sha256=4SH8D6U08u-oB_TdLjVU0J00rF3o4fHRIAgOy6J4IMo,21687
3
+ mxcpctl-0.0.1.dist-info/METADATA,sha256=MwRAKlzq_uxD41RkhtVaN21YRtYY2eUeV7pSsjzRsUk,527
4
+ mxcpctl-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ mxcpctl-0.0.1.dist-info/entry_points.txt,sha256=5ITlu28KJS5YWhg0F-iv9lOuh-EIxYDx5YaYI_vWqAA,45
6
+ mxcpctl-0.0.1.dist-info/top_level.txt,sha256=mcV_HZ-XvcgShz4YMvskJBPppB8vxmvthbijikw3YRw,8
7
+ mxcpctl-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mxcpctl = mxcpctl.cli:main
@@ -0,0 +1 @@
1
+ mxcpctl