deepctl-cmd-debug-probe 0.0.2__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.
@@ -0,0 +1,5 @@
1
+ """Debug probe subcommand for deepctl — live ffprobe analysis during streaming."""
2
+
3
+ from .command import ProbeCommand
4
+
5
+ __all__ = ["ProbeCommand"]
@@ -0,0 +1,207 @@
1
+ """Debug probe command for deepctl — WebSocket proxy with live ffprobe analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import socket
7
+ import time
8
+ from typing import Any
9
+
10
+ from aiohttp import web
11
+ from deepctl_core import AuthManager, BaseCommand, Config, DeepgramClient
12
+ from deepctl_shared_utils import require_ffprobe
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+
16
+ from .models import ProbeDebugResult
17
+ from .proxy import ProbeProxy
18
+
19
+ console = Console()
20
+
21
+
22
+ def _find_available_port(start: int = 3100, end: int = 3199) -> int | None:
23
+ """Find an available port in the given range."""
24
+ for port in range(start, end + 1):
25
+ try:
26
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
27
+ s.bind(("127.0.0.1", port))
28
+ return port
29
+ except OSError:
30
+ continue
31
+ return None
32
+
33
+
34
+ class ProbeCommand(BaseCommand):
35
+ """WebSocket proxy with periodic ffprobe analysis of streaming audio."""
36
+
37
+ name = "probe"
38
+ help = (
39
+ "Launch a local WebSocket proxy that periodically probes "
40
+ "streaming audio with ffprobe while forwarding to Deepgram."
41
+ )
42
+ short_help = "Stream probe proxy"
43
+
44
+ requires_auth = True
45
+ requires_project = False
46
+ ci_friendly = False
47
+
48
+ examples = [
49
+ "dg debug probe",
50
+ "dg debug probe --port 3100",
51
+ "dg debug probe --probe-interval-bytes 131072",
52
+ "dg debug probe --probe-interval-seconds 10",
53
+ ]
54
+ agent_help = (
55
+ "Launch a local WebSocket proxy that sits between your application "
56
+ "and Deepgram's streaming API. Periodically runs ffprobe on the "
57
+ "accumulated audio buffer to show codec, sample rate, and channel "
58
+ "information alongside real-time transcripts."
59
+ )
60
+
61
+ def get_arguments(self) -> list[dict[str, Any]]:
62
+ """Get command arguments and options."""
63
+ return [
64
+ {
65
+ "names": ["--port", "-p"],
66
+ "help": "Local proxy port (default: auto-select 3100-3199)",
67
+ "type": int,
68
+ "is_option": True,
69
+ },
70
+ {
71
+ "names": ["--probe-interval-bytes"],
72
+ "help": "Probe every N bytes of audio (default: 131072)",
73
+ "type": int,
74
+ "default": 131072,
75
+ "is_option": True,
76
+ },
77
+ {
78
+ "names": ["--probe-interval-seconds"],
79
+ "help": "Probe every N seconds (default: 10)",
80
+ "type": float,
81
+ "default": 10.0,
82
+ "is_option": True,
83
+ },
84
+ {
85
+ "names": ["--verbose", "-v"],
86
+ "help": "Show per-frame details",
87
+ "is_flag": True,
88
+ "is_option": True,
89
+ },
90
+ ]
91
+
92
+ def handle(
93
+ self,
94
+ config: Config,
95
+ auth_manager: AuthManager,
96
+ client: DeepgramClient,
97
+ **kwargs: Any,
98
+ ) -> Any:
99
+ """Handle debug probe command execution."""
100
+ port = kwargs.get("port")
101
+ probe_interval_bytes = kwargs.get("probe_interval_bytes") or 131072
102
+ probe_interval_seconds = kwargs.get("probe_interval_seconds") or 10.0
103
+ verbose = kwargs.get("verbose", False)
104
+
105
+ # Check ffprobe availability
106
+ if not require_ffprobe(config):
107
+ return ProbeDebugResult(
108
+ status="error",
109
+ message="ffprobe is required but not found",
110
+ )
111
+
112
+ # Get API key
113
+ api_key = auth_manager.get_api_key()
114
+ if not api_key:
115
+ console.print("[red]No API key found. Run 'dg login' first.[/red]")
116
+ return ProbeDebugResult(
117
+ status="error",
118
+ message="No API key found",
119
+ )
120
+
121
+ # Find available port
122
+ if port is None:
123
+ port = _find_available_port()
124
+ if port is None:
125
+ console.print("[red]No available ports in range 3100-3199[/red]")
126
+ return ProbeDebugResult(
127
+ status="error",
128
+ message="No available ports",
129
+ )
130
+ else:
131
+ try:
132
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
133
+ s.bind(("127.0.0.1", port))
134
+ except OSError:
135
+ console.print(f"[red]Port {port} is already in use[/red]")
136
+ return ProbeDebugResult(
137
+ status="error",
138
+ message=f"Port {port} is in use",
139
+ port=port,
140
+ )
141
+
142
+ upstream_host = "api.deepgram.com"
143
+
144
+ # Create proxy
145
+ proxy = ProbeProxy(
146
+ api_key=api_key,
147
+ config=config,
148
+ upstream_host=upstream_host,
149
+ probe_interval_bytes=probe_interval_bytes,
150
+ probe_interval_seconds=probe_interval_seconds,
151
+ verbose=verbose,
152
+ )
153
+
154
+ # Print instructions
155
+ console.print(
156
+ Panel(
157
+ f"[bold cyan]WebSocket Probe Proxy[/bold cyan]\n\n"
158
+ f"Listening on: [green]ws://localhost:{port}[/green]\n"
159
+ f"Upstream: [yellow]wss://{upstream_host}[/yellow]\n"
160
+ f"API key: [dim]****{api_key[-4:]}[/dim]\n\n"
161
+ f"Probe every: {probe_interval_bytes:,} bytes "
162
+ f"or {probe_interval_seconds}s\n\n"
163
+ f"[bold]Point your app at the proxy URL:[/bold]\n"
164
+ f" ws://localhost:{port}/v1/listen?encoding=...&sample_rate=...\n\n"
165
+ f"[dim]Press Ctrl+C to stop[/dim]",
166
+ title="Debug Probe Proxy",
167
+ border_style="cyan",
168
+ )
169
+ )
170
+
171
+ # Run the proxy server
172
+ start_time = time.time()
173
+ app = web.Application()
174
+ app.router.add_route("GET", "/{path:.*}", proxy.handle_connection)
175
+
176
+ try:
177
+ loop = asyncio.new_event_loop()
178
+ asyncio.set_event_loop(loop)
179
+
180
+ runner = web.AppRunner(app)
181
+ loop.run_until_complete(runner.setup())
182
+
183
+ site = web.TCPSite(runner, "127.0.0.1", port)
184
+ loop.run_until_complete(site.start())
185
+
186
+ # Run until Ctrl+C
187
+ loop.run_forever()
188
+
189
+ except KeyboardInterrupt:
190
+ console.print("\n[yellow]Shutting down probe proxy...[/yellow]")
191
+ finally:
192
+ loop.run_until_complete(runner.cleanup())
193
+ loop.close()
194
+
195
+ duration = time.time() - start_time
196
+
197
+ console.print(f"\n[green]Probe proxy stopped after {duration:.1f}s[/green]")
198
+ console.print(f"Total connections: {len(proxy.connections)}")
199
+
200
+ return ProbeDebugResult(
201
+ status="success",
202
+ message="Probe proxy session completed",
203
+ port=port,
204
+ upstream_host=upstream_host,
205
+ connections=proxy.connections,
206
+ duration_seconds=duration,
207
+ )
@@ -0,0 +1,42 @@
1
+ """Data models for debug probe command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from deepctl_core import BaseResult
6
+ from deepctl_shared_utils.ffprobe_models import AudioProbeResult # noqa: TC002
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ProbeSnapshot(BaseModel):
11
+ """A single ffprobe snapshot taken during streaming."""
12
+
13
+ timestamp: float = 0.0
14
+ bytes_at_probe: int = 0
15
+ result: AudioProbeResult | None = None
16
+
17
+
18
+ class ProbeConnectionStats(BaseModel):
19
+ """Statistics for a single probe proxy connection."""
20
+
21
+ connection_id: str
22
+ stream_type: str = "unknown"
23
+ path: str = ""
24
+ bytes_sent: int = 0
25
+ bytes_received: int = 0
26
+ frames_sent: int = 0
27
+ frames_received: int = 0
28
+ text_frames_sent: int = 0
29
+ text_frames_received: int = 0
30
+ duration_seconds: float | None = None
31
+ snapshots: list[ProbeSnapshot] = Field(default_factory=list)
32
+ transcripts: list[str] = Field(default_factory=list)
33
+ audio_buffer: bytes = Field(default=b"", exclude=True, repr=False)
34
+
35
+
36
+ class ProbeDebugResult(BaseResult):
37
+ """Result from debug probe command execution."""
38
+
39
+ port: int = 0
40
+ upstream_host: str = ""
41
+ connections: list[ProbeConnectionStats] = Field(default_factory=list)
42
+ duration_seconds: float | None = None
@@ -0,0 +1,278 @@
1
+ """WebSocket proxy with periodic ffprobe analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import time
8
+ import uuid
9
+ from typing import TYPE_CHECKING
10
+
11
+ import aiohttp
12
+ from aiohttp import web
13
+ from deepctl_shared_utils import probe_buffer
14
+ from rich.console import Console
15
+
16
+ from .models import ProbeConnectionStats, ProbeSnapshot
17
+
18
+ if TYPE_CHECKING:
19
+ from deepctl_core import Config
20
+
21
+ console = Console()
22
+
23
+
24
+ class ProbeProxy:
25
+ """WebSocket proxy that probes audio at configurable intervals."""
26
+
27
+ # Maximum audio buffer size (1 MB). Only the most recent bytes are kept
28
+ # to prevent unbounded memory growth on long-running streams.
29
+ MAX_BUFFER_BYTES = 1_048_576
30
+
31
+ def __init__(
32
+ self,
33
+ api_key: str,
34
+ config: Config | None = None,
35
+ upstream_host: str = "api.deepgram.com",
36
+ probe_interval_bytes: int = 131072,
37
+ probe_interval_seconds: float = 10.0,
38
+ verbose: bool = False,
39
+ ) -> None:
40
+ self.api_key = api_key
41
+ self.config = config
42
+ self.upstream_host = upstream_host
43
+ self.probe_interval_bytes = probe_interval_bytes
44
+ self.probe_interval_seconds = probe_interval_seconds
45
+ self.verbose = verbose
46
+ self.connections: list[ProbeConnectionStats] = []
47
+
48
+ def _detect_stream_type(self, path: str) -> str:
49
+ """Detect stream type from the request path."""
50
+ if "/v1/listen" in path:
51
+ return "stt"
52
+ elif "/v1/speak" in path:
53
+ return "tts"
54
+ elif "/agent" in path:
55
+ return "agent"
56
+ return "unknown"
57
+
58
+ async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
59
+ """Handle an incoming WebSocket connection."""
60
+ ws_client = web.WebSocketResponse()
61
+ await ws_client.prepare(request)
62
+
63
+ conn_id = str(uuid.uuid4())[:8]
64
+ path = request.path
65
+ query_string = request.query_string
66
+ stream_type = self._detect_stream_type(path)
67
+
68
+ stats = ProbeConnectionStats(
69
+ connection_id=conn_id,
70
+ stream_type=stream_type,
71
+ path=path,
72
+ )
73
+ self.connections.append(stats)
74
+
75
+ upstream_url = f"wss://{self.upstream_host}{path}"
76
+ if query_string:
77
+ upstream_url = f"{upstream_url}?{query_string}"
78
+
79
+ upstream_headers = {
80
+ "Authorization": f"Token {self.api_key}",
81
+ }
82
+
83
+ console.print(
84
+ f"\n[green]New connection[/green] [{conn_id}] "
85
+ f"type={stream_type} path={path}"
86
+ )
87
+
88
+ start_time = time.time()
89
+ last_probe_time = start_time
90
+ last_probe_bytes = 0
91
+
92
+ try:
93
+ async with (
94
+ aiohttp.ClientSession() as session,
95
+ session.ws_connect(
96
+ upstream_url,
97
+ headers=upstream_headers,
98
+ timeout=aiohttp.ClientWSTimeout(ws_close=30.0),
99
+ ) as ws_upstream,
100
+ ):
101
+
102
+ async def forward_client_to_upstream() -> None:
103
+ nonlocal last_probe_time, last_probe_bytes
104
+
105
+ async for msg in ws_client:
106
+ if msg.type == aiohttp.WSMsgType.BINARY:
107
+ stats.bytes_sent += len(msg.data)
108
+ stats.frames_sent += 1
109
+
110
+ # Keep only the tail of the buffer to bound memory
111
+ stats.audio_buffer += msg.data
112
+ if len(stats.audio_buffer) > self.MAX_BUFFER_BYTES:
113
+ stats.audio_buffer = stats.audio_buffer[
114
+ -self.MAX_BUFFER_BYTES :
115
+ ]
116
+
117
+ await ws_upstream.send_bytes(msg.data)
118
+
119
+ if self.verbose:
120
+ console.print(
121
+ f" [dim]→ binary {len(msg.data)} bytes[/dim]"
122
+ )
123
+
124
+ # Check probe thresholds
125
+ now = time.time()
126
+ bytes_since = stats.bytes_sent - last_probe_bytes
127
+ time_since = now - last_probe_time
128
+
129
+ if (
130
+ bytes_since >= self.probe_interval_bytes
131
+ or time_since >= self.probe_interval_seconds
132
+ ):
133
+ self._run_probe_snapshot(stats, now)
134
+ last_probe_time = now
135
+ last_probe_bytes = stats.bytes_sent
136
+
137
+ elif msg.type == aiohttp.WSMsgType.TEXT:
138
+ stats.text_frames_sent += 1
139
+ await ws_upstream.send_str(msg.data)
140
+ elif msg.type in (
141
+ aiohttp.WSMsgType.CLOSE,
142
+ aiohttp.WSMsgType.ERROR,
143
+ ):
144
+ break
145
+
146
+ async def forward_upstream_to_client() -> None:
147
+ async for msg in ws_upstream:
148
+ if ws_client.closed:
149
+ break
150
+
151
+ if msg.type == aiohttp.WSMsgType.BINARY:
152
+ stats.bytes_received += len(msg.data)
153
+ stats.frames_received += 1
154
+ await ws_client.send_bytes(msg.data)
155
+ elif msg.type == aiohttp.WSMsgType.TEXT:
156
+ stats.text_frames_received += 1
157
+ await ws_client.send_str(msg.data)
158
+
159
+ # Parse transcript from Deepgram response
160
+ self._parse_transcript(msg.data, stats)
161
+ elif msg.type in (
162
+ aiohttp.WSMsgType.CLOSE,
163
+ aiohttp.WSMsgType.ERROR,
164
+ ):
165
+ break
166
+
167
+ # Run both forwarders concurrently. If one fails,
168
+ # cancel the other instead of letting it dangle.
169
+ task_c2u = asyncio.ensure_future(forward_client_to_upstream())
170
+ task_u2c = asyncio.ensure_future(forward_upstream_to_client())
171
+ try:
172
+ await asyncio.gather(task_c2u, task_u2c)
173
+ except Exception:
174
+ pass
175
+ finally:
176
+ for task in (task_c2u, task_u2c):
177
+ if not task.done():
178
+ task.cancel()
179
+ # Suppress CancelledError from the cancelled task
180
+ await asyncio.gather(task_c2u, task_u2c, return_exceptions=True)
181
+
182
+ except aiohttp.ClientError as e:
183
+ console.print(f"[red]Upstream connection failed[/red] [{conn_id}]: {e}")
184
+ except Exception as e:
185
+ console.print(f"[red]Connection error[/red] [{conn_id}]: {e}")
186
+ finally:
187
+ stats.duration_seconds = time.time() - start_time
188
+
189
+ # Final probe
190
+ if stats.audio_buffer:
191
+ console.print(f"\n[bold]Final probe[/bold] [{conn_id}]:")
192
+ self._run_probe_snapshot(stats, time.time())
193
+
194
+ self._print_connection_summary(stats)
195
+
196
+ if not ws_client.closed:
197
+ await ws_client.close()
198
+
199
+ return ws_client
200
+
201
+ def _run_probe_snapshot(
202
+ self, stats: ProbeConnectionStats, timestamp: float
203
+ ) -> None:
204
+ """Run a probe snapshot on the accumulated audio buffer."""
205
+ result = probe_buffer(stats.audio_buffer, "probe", self.config)
206
+ snapshot = ProbeSnapshot(
207
+ timestamp=timestamp,
208
+ bytes_at_probe=stats.bytes_sent,
209
+ result=result,
210
+ )
211
+ stats.snapshots.append(snapshot)
212
+
213
+ if result and result.streams:
214
+ stream = result.streams[0]
215
+ parts = []
216
+ if stream.codec_name:
217
+ parts.append(f"codec={stream.codec_name}")
218
+ if stream.sample_rate:
219
+ parts.append(f"rate={stream.sample_rate}Hz")
220
+ if stream.channels is not None:
221
+ parts.append(f"ch={stream.channels}")
222
+ info = " ".join(parts)
223
+ console.print(
224
+ f" [cyan]probe[/cyan] [{stats.connection_id}] "
225
+ f"@{stats.bytes_sent:,}B: {info}"
226
+ )
227
+ elif result and result.format:
228
+ console.print(
229
+ f" [cyan]probe[/cyan] [{stats.connection_id}] "
230
+ f"@{stats.bytes_sent:,}B: format={result.format.format_name}"
231
+ )
232
+ else:
233
+ console.print(
234
+ f" [dim]probe[/dim] [{stats.connection_id}] "
235
+ f"@{stats.bytes_sent:,}B: could not determine format"
236
+ )
237
+
238
+ def _parse_transcript(self, text: str, stats: ProbeConnectionStats) -> None:
239
+ """Parse Deepgram transcript from upstream TEXT frame."""
240
+ try:
241
+ data = json.loads(text)
242
+ # Deepgram STT responses have channel.alternatives[].transcript
243
+ channel = data.get("channel", {})
244
+ alternatives = channel.get("alternatives", [])
245
+ if alternatives:
246
+ transcript = alternatives[0].get("transcript", "")
247
+ is_final = data.get("is_final", False)
248
+ if transcript and is_final:
249
+ stats.transcripts.append(transcript)
250
+ console.print(
251
+ f" [green]transcript[/green] [{stats.connection_id}]: "
252
+ f"{transcript[:120]}"
253
+ )
254
+ except (json.JSONDecodeError, AttributeError):
255
+ pass
256
+
257
+ def _print_connection_summary(self, stats: ProbeConnectionStats) -> None:
258
+ """Print summary for a completed connection."""
259
+ console.print(f"\n[blue]Connection closed[/blue] [{stats.connection_id}]")
260
+ console.print(f" Type: {stats.stream_type}")
261
+ console.print(
262
+ f" Duration: {stats.duration_seconds:.1f}s"
263
+ if stats.duration_seconds
264
+ else " Duration: <1s"
265
+ )
266
+ console.print(
267
+ f" Sent: {stats.bytes_sent:,} bytes "
268
+ f"({stats.frames_sent} binary, "
269
+ f"{stats.text_frames_sent} text frames)"
270
+ )
271
+ console.print(
272
+ f" Received: {stats.bytes_received:,} bytes "
273
+ f"({stats.frames_received} binary, "
274
+ f"{stats.text_frames_received} text frames)"
275
+ )
276
+ console.print(f" Probe snapshots: {len(stats.snapshots)}")
277
+ if stats.transcripts:
278
+ console.print(f" Transcripts received: {len(stats.transcripts)}")
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-cmd-debug-probe
3
+ Version: 0.0.2
4
+ Summary: Debug probe subcommand for deepctl — live ffprobe analysis during streaming
5
+ Author-email: Deepgram <devrel@deepgram.com>
6
+ Maintainer-email: Deepgram <devrel@deepgram.com>
7
+ License-Expression: MIT
8
+ Keywords: deepgram,cli,debug,probe,ffprobe,websocket
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: deepctl-core>=0.1.10
18
+ Requires-Dist: deepctl-shared-utils>=0.1.10
19
+ Requires-Dist: click>=8.0.0
20
+ Requires-Dist: rich>=13.0.0
21
+ Requires-Dist: pydantic>=2.0.0
22
+ Requires-Dist: aiohttp>=3.8.0
23
+
24
+ # deepctl-cmd-debug-probe
25
+
26
+ > Part of [deepctl](https://github.com/deepgram/cli) — Official Deepgram CLI
27
+
28
+ Debug probe subcommand for deepctl — live ffprobe analysis during streaming
29
+
30
+ This is a subcommand of `deepctl debug`.
31
+
32
+ ## Installation
33
+
34
+ This package is included with deepctl and does not need to be installed separately.
35
+
36
+ ### Install deepctl
37
+
38
+ ```bash
39
+ # Install with pip
40
+ pip install deepctl
41
+
42
+ # Or install with uv
43
+ uv tool install deepctl
44
+
45
+ # Or install with pipx
46
+ pipx install deepctl
47
+
48
+ # Or run without installing
49
+ uvx deepctl --help
50
+ pipx run deepctl --help
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ | Command | Entry Point |
56
+ |---------|-------------|
57
+ | `deepctl debug probe` | `deepctl_cmd_debug_probe.command:ProbeCommand` |
58
+
59
+ ## Dependencies
60
+
61
+ - `click>=8.0.0`
62
+ - `rich>=13.0.0`
63
+ - `pydantic>=2.0.0`
64
+ - `aiohttp>=3.8.0`
65
+
66
+ ## License
67
+
68
+ MIT — see [LICENSE](../../LICENSE)
@@ -0,0 +1,9 @@
1
+ deepctl_cmd_debug_probe/__init__.py,sha256=IBujDn4L6LmDvrjzs7zMHY2G9rfQRqL_nSHxACxPBkY,148
2
+ deepctl_cmd_debug_probe/command.py,sha256=3aZZBse-7mxthPLVzTbxzK4YpPVHYaYLOkz_LrqLSbk,6924
3
+ deepctl_cmd_debug_probe/models.py,sha256=APtFtLPbzQWvEC2_TNdDdpk9QoTrTLibFWt-qE5FHn8,1261
4
+ deepctl_cmd_debug_probe/proxy.py,sha256=0aEbTk8ewR4UF15R1p3bpoGEBLRmwvFcU6zMrebQC1k,10738
5
+ deepctl_cmd_debug_probe-0.0.2.dist-info/METADATA,sha256=6_hxBRC8ms8ArgaIb7kmjCnHUphIGGfJjKaKpNhWoyE,1723
6
+ deepctl_cmd_debug_probe-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ deepctl_cmd_debug_probe-0.0.2.dist-info/entry_points.txt,sha256=KtP7bpTfgLoXR65MjEztjfb7nH_KSvZ-jzCbQG2zcGQ,81
8
+ deepctl_cmd_debug_probe-0.0.2.dist-info/top_level.txt,sha256=zlC3OL8txzyhhvPgBISuWkMK6IN9w-HlqRfXo-Z7G9o,24
9
+ deepctl_cmd_debug_probe-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [deepctl.subcommands.debug]
2
+ probe = deepctl_cmd_debug_probe.command:ProbeCommand
@@ -0,0 +1 @@
1
+ deepctl_cmd_debug_probe